Skip to content

Commit f494223

Browse files
emir-karabegwaleedlatif1
authored andcommitted
feat(workflow): workflow overhaul (#1906)
* feat: action-bar side-effects; refactor: removed unused styles from globals and tailwind config * feat(terminal): filters/sorting; fix(workflow): zoom bug * feat(sidebar): toggle; feat(terminal): show/hide timestamp * feat(toolbar): triggers ordering * feat: commands, cursors, room-presence, command-list, rename blocks, workspace controls, invite, search modal * removed old imports * ack PR comments * fix tag dropdown * feat: variables UI; fix: terminal keys --------- Co-authored-by: waleed <waleed>
1 parent 8c604dc commit f494223

File tree

51 files changed

+3859
-2287
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3859
-2287
lines changed

apps/sim/app/globals.css

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
--panel-width: 244px;
1212
--toolbar-triggers-height: 300px;
1313
--editor-connections-height: 200px;
14-
--terminal-height: 100px;
14+
--terminal-height: 145px;
1515
}
1616

1717
.sidebar-container {
@@ -260,11 +260,6 @@
260260
/**
261261
* Dark mode specific overrides
262262
*/
263-
.dark .error-badge {
264-
background-color: hsl(0, 70%, 20%) !important;
265-
color: hsl(0, 0%, 100%) !important;
266-
}
267-
268263
.dark .bg-red-500 {
269264
@apply bg-red-700;
270265
}
@@ -285,23 +280,11 @@ input[type="search"]::-ms-clear {
285280
display: none;
286281
}
287282

288-
/**
289-
* Layout utilities
290-
*/
291-
.main-content-overlay {
292-
z-index: 40;
293-
}
294-
295283
/**
296284
* Utilities and special effects
297285
* Animation keyframes are defined in tailwind.config.ts
298286
*/
299287
@layer utilities {
300-
.animation-container {
301-
contain: paint layout style;
302-
will-change: opacity, transform;
303-
}
304-
305288
.scrollbar-none {
306289
-ms-overflow-style: none;
307290
scrollbar-width: none;
@@ -348,46 +331,6 @@ input[type="search"]::-ms-clear {
348331
background-color: hsl(var(--input-background));
349332
}
350333

351-
.bg-brand-primary {
352-
background-color: var(--brand-primary-hex);
353-
}
354-
355-
.bg-brand-primary-hover {
356-
background-color: var(--brand-primary-hover-hex);
357-
}
358-
359-
.hover\:bg-brand-primary-hover:hover {
360-
background-color: var(--brand-primary-hover-hex);
361-
}
362-
363-
.hover\:text-brand-accent-hover:hover {
364-
color: var(--brand-accent-hover-hex);
365-
}
366-
367-
.bg-brand-gradient {
368-
background: linear-gradient(
369-
to bottom,
370-
color-mix(in srgb, var(--brand-primary-hex) 85%, white),
371-
var(--brand-primary-hex)
372-
);
373-
}
374-
375-
.border-brand-gradient {
376-
border-color: var(--brand-primary-hex);
377-
}
378-
379-
.shadow-brand-gradient {
380-
box-shadow: inset 0 2px 4px 0 color-mix(in srgb, var(--brand-primary-hex) 60%, transparent);
381-
}
382-
383-
.hover\:bg-brand-gradient-hover:hover {
384-
background: linear-gradient(
385-
to bottom,
386-
var(--brand-primary-hover-hex),
387-
color-mix(in srgb, var(--brand-primary-hex) 90%, black)
388-
);
389-
}
390-
391334
.auth-card {
392335
background-color: rgba(255, 255, 255, 0.9) !important;
393336
border-color: #e5e5e5 !important;
@@ -419,10 +362,6 @@ input[type="search"]::-ms-clear {
419362
color: #737373 !important;
420363
}
421364

422-
.bg-surface-elevated {
423-
background-color: var(--surface-elevated);
424-
}
425-
426365
.transition-ring {
427366
transition-property: box-shadow, transform;
428367
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
'use client'
2+
3+
import {
4+
createContext,
5+
type ReactNode,
6+
useCallback,
7+
useContext,
8+
useEffect,
9+
useMemo,
10+
useRef,
11+
} from 'react'
12+
import { useRouter } from 'next/navigation'
13+
import { createLogger } from '@/lib/logs/console/logger'
14+
15+
const logger = createLogger('GlobalCommands')
16+
17+
/**
18+
* Detects if the current platform is macOS.
19+
*
20+
* @returns True if running on macOS, false otherwise
21+
*/
22+
function isMacPlatform(): boolean {
23+
if (typeof window === 'undefined') return false
24+
return (
25+
/Mac|iPhone|iPod|iPad/i.test(navigator.platform) ||
26+
/Mac|iPhone|iPod|iPad/i.test(navigator.userAgent)
27+
)
28+
}
29+
30+
/**
31+
* Represents a parsed keyboard shortcut.
32+
*
33+
* We support the following modifiers:
34+
* - Mod: maps to Meta on macOS, Ctrl on other platforms
35+
* - Ctrl, Meta, Shift, Alt
36+
*
37+
* Examples:
38+
* - "Mod+A"
39+
* - "Mod+Shift+T"
40+
* - "Meta+K"
41+
*/
42+
export interface ParsedShortcut {
43+
key: string
44+
mod?: boolean
45+
ctrl?: boolean
46+
meta?: boolean
47+
shift?: boolean
48+
alt?: boolean
49+
}
50+
51+
/**
52+
* Declarative command registration.
53+
*/
54+
export interface GlobalCommand {
55+
/** Unique id for the command. If omitted, one is generated. */
56+
id?: string
57+
/** Shortcut string in the form "Mod+Shift+T", "Mod+A", "Meta+K", etc. */
58+
shortcut: string
59+
/**
60+
* Whether to allow the command to run inside editable elements like inputs,
61+
* textareas or contenteditable. Defaults to true to ensure browser defaults
62+
* are overridden when desired.
63+
*/
64+
allowInEditable?: boolean
65+
/**
66+
* Handler invoked when the shortcut is matched. Use this to trigger actions
67+
* like navigation or dispatching application events.
68+
*/
69+
handler: (event: KeyboardEvent) => void
70+
}
71+
72+
interface RegistryCommand extends GlobalCommand {
73+
id: string
74+
parsed: ParsedShortcut
75+
}
76+
77+
interface GlobalCommandsContextValue {
78+
register: (commands: GlobalCommand[]) => () => void
79+
}
80+
81+
const GlobalCommandsContext = createContext<GlobalCommandsContextValue | null>(null)
82+
83+
/**
84+
* Parses a human-readable shortcut into a structured representation.
85+
*/
86+
function parseShortcut(shortcut: string): ParsedShortcut {
87+
const parts = shortcut.split('+').map((p) => p.trim())
88+
const modifiers = new Set(parts.slice(0, -1).map((p) => p.toLowerCase()))
89+
const last = parts[parts.length - 1]
90+
91+
return {
92+
key: last.length === 1 ? last.toLowerCase() : last, // keep non-letter keys verbatim
93+
mod: modifiers.has('mod'),
94+
ctrl: modifiers.has('ctrl'),
95+
meta: modifiers.has('meta') || modifiers.has('cmd') || modifiers.has('command'),
96+
shift: modifiers.has('shift'),
97+
alt: modifiers.has('alt') || modifiers.has('option'),
98+
}
99+
}
100+
101+
/**
102+
* Checks if a KeyboardEvent matches a parsed shortcut, honoring platform-specific
103+
* interpretation of "Mod" (Meta on macOS, Ctrl elsewhere).
104+
*/
105+
function matchesShortcut(e: KeyboardEvent, parsed: ParsedShortcut): boolean {
106+
const isMac = isMacPlatform()
107+
const expectedCtrl = parsed.ctrl || (parsed.mod ? !isMac : false)
108+
const expectedMeta = parsed.meta || (parsed.mod ? isMac : false)
109+
110+
// Normalize key for comparison: for letters compare lowercase
111+
const eventKey = e.key.length === 1 ? e.key.toLowerCase() : e.key
112+
113+
return (
114+
eventKey === parsed.key &&
115+
!!e.ctrlKey === !!expectedCtrl &&
116+
!!e.metaKey === !!expectedMeta &&
117+
!!e.shiftKey === !!parsed.shift &&
118+
!!e.altKey === !!parsed.alt
119+
)
120+
}
121+
122+
/**
123+
* Provider that captures global keyboard shortcuts and routes them to
124+
* registered commands. Commands can be registered from any descendant component.
125+
*/
126+
export function GlobalCommandsProvider({ children }: { children: ReactNode }) {
127+
const registryRef = useRef<Map<string, RegistryCommand>>(new Map())
128+
const isMac = useMemo(() => isMacPlatform(), [])
129+
const router = useRouter()
130+
131+
const register = useCallback((commands: GlobalCommand[]) => {
132+
const createdIds: string[] = []
133+
for (const cmd of commands) {
134+
const id = cmd.id ?? crypto.randomUUID()
135+
const parsed = parseShortcut(cmd.shortcut)
136+
registryRef.current.set(id, {
137+
...cmd,
138+
id,
139+
parsed,
140+
allowInEditable: cmd.allowInEditable ?? true,
141+
})
142+
createdIds.push(id)
143+
logger.info('Registered global command', { id, shortcut: cmd.shortcut })
144+
}
145+
146+
return () => {
147+
for (const id of createdIds) {
148+
registryRef.current.delete(id)
149+
logger.info('Unregistered global command', { id })
150+
}
151+
}
152+
}, [])
153+
154+
useEffect(() => {
155+
const onKeyDown = (e: KeyboardEvent) => {
156+
if (e.isComposing) return
157+
158+
// Evaluate matches in registration order (latest registration wins naturally
159+
// due to replacement on same id). Break on first match.
160+
for (const [, cmd] of registryRef.current) {
161+
if (!cmd.allowInEditable) {
162+
const ae = document.activeElement
163+
const isEditable =
164+
ae instanceof HTMLInputElement ||
165+
ae instanceof HTMLTextAreaElement ||
166+
ae?.hasAttribute('contenteditable')
167+
if (isEditable) continue
168+
}
169+
170+
if (matchesShortcut(e, cmd.parsed)) {
171+
// Always override default browser behavior for matched commands.
172+
e.preventDefault()
173+
e.stopPropagation()
174+
logger.info('Executing global command', {
175+
id: cmd.id,
176+
shortcut: cmd.shortcut,
177+
key: e.key,
178+
isMac,
179+
path: typeof window !== 'undefined' ? window.location.pathname : undefined,
180+
})
181+
try {
182+
cmd.handler(e)
183+
} catch (err) {
184+
logger.error('Global command handler threw', { id: cmd.id, err })
185+
}
186+
return
187+
}
188+
}
189+
}
190+
191+
window.addEventListener('keydown', onKeyDown, { capture: true })
192+
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
193+
}, [isMac, router])
194+
195+
const value = useMemo<GlobalCommandsContextValue>(() => ({ register }), [register])
196+
197+
return <GlobalCommandsContext.Provider value={value}>{children}</GlobalCommandsContext.Provider>
198+
}
199+
200+
/**
201+
* Registers a set of global commands for the lifetime of the component.
202+
*
203+
* Returns nothing; cleanup is automatic on unmount.
204+
*/
205+
export function useRegisterGlobalCommands(commands: GlobalCommand[] | (() => GlobalCommand[])) {
206+
const ctx = useContext(GlobalCommandsContext)
207+
if (!ctx) {
208+
throw new Error('useRegisterGlobalCommands must be used within GlobalCommandsProvider')
209+
}
210+
211+
useEffect(() => {
212+
const list = typeof commands === 'function' ? commands() : commands
213+
const unregister = ctx.register(list)
214+
return unregister
215+
// We intentionally want to register once for the given commands
216+
// eslint-disable-next-line react-hooks/exhaustive-deps
217+
}, [])
218+
}

apps/sim/app/workspace/[workspaceId]/providers/providers.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React from 'react'
44
import { Tooltip } from '@/components/emcn'
5+
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
56
import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
67
import { SettingsLoader } from './settings-loader'
78

@@ -13,9 +14,11 @@ const Providers = React.memo<ProvidersProps>(({ children }) => {
1314
return (
1415
<>
1516
<SettingsLoader />
16-
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
17-
<WorkspacePermissionsProvider>{children}</WorkspacePermissionsProvider>
18-
</Tooltip.Provider>
17+
<GlobalCommandsProvider>
18+
<Tooltip.Provider delayDuration={600} skipDelayDuration={0}>
19+
<WorkspacePermissionsProvider>{children}</WorkspacePermissionsProvider>
20+
</Tooltip.Provider>
21+
</GlobalCommandsProvider>
1922
</>
2023
)
2124
})

0 commit comments

Comments
 (0)