Skip to content

Commit 801329c

Browse files
BillLeoutsakosvl346Bill Leoutsakoscodexwaleedlatif1
authored
refactor(ui): share field disclosures and icon action geometry (#8038)
* improvement(emcn): share ordinary action geometry in Button * improvement(ui): share additional-field disclosures * fix(ui): preserve tiny actions and use shared sidebar geometry * improvement(ui): use shared geometry for standard icon actions (#8153) Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> * refactor(ui): centralize overlay action buttons in EMCN (#7990) * refactor(ui): centralize overlay action buttons in EMCN * improvement(ui): share workflow preview actions (#8065) * improvement(ui): share workflow preview actions * improvement(ui): reuse inline and overlay workflow actions * refactor(emcn): share Code.Viewer appearances (#8238) * Add shared Code.Viewer appearances for product surfaces * Test flat virtualized Code.Viewer terminal chrome --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> * Share table sidebar shell and scroll body (#8249) * Share table sidebar shell and scroll body * test(tables): cover shared sidebar dialog state * test(tables): preserve sidebar scroll state --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> * Keep workflow preview values readable (#8256) * fix(ui): keep preview values readable and restore version tint * fix(ui): keep preview file actions inert --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed <walif6@gmail.com> --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Waleed <walif6@gmail.com> * improvement(ui): reuse shared button and focus treatments (#8187) * improvement(ui): reuse shared button and focus treatments * fix(emcn): include outline reset in muted focus ring * fix(ui): make closed table sidebars inert and strengthen focus ring * Carry code search overlay into the live EMCN stack (#8275) * refactor(ui): share code search overlay controls * refactor(emcn): own compact code search field chrome * test(ui): cover floating compact code search * refactor(emcn): compose compact search from shared chip surface --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> * fix(ui): announce code search result changes * docs(ui): describe shared table sidebar exports --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Waleed <walif6@gmail.com>
1 parent 5c042a0 commit 801329c

58 files changed

Lines changed: 1290 additions & 542 deletions

File tree

Some content is hidden

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

‎apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { Button } from '@sim/emcn'
44
interface PublicChatActionButtonProps
55
extends Omit<
66
ComponentProps<typeof Button>,
7-
'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className'
7+
'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' | 'shape'
88
> {
99
variant: 'primary' | 'quiet'
1010
'aria-label': string
1111
}
1212

1313
/** Public chat's circular composer action, retaining its primary and quiet palettes. */
1414
export const PublicChatActionButton = forwardRef<HTMLButtonElement, PublicChatActionButtonProps>(
15-
(props, ref) => <Button {...props} ref={ref} className='size-[28px] rounded-full p-0' />
15+
(props, ref) => <Button {...props} ref={ref} iconSize='regular' shape='round' />
1616
)
1717
PublicChatActionButton.displayName = 'PublicChatActionButton'

‎apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ export function ChatMessageContainer({
7272
<Button
7373
onClick={scrollToBottom}
7474
size='sm'
75-
className='gap-1 rounded-full px-3 shadow-medium'
75+
shape='round'
76+
className='gap-1 px-3 shadow-medium'
7677
>
7778
<ArrowDown className='size-3.5' />
7879
<span className='sr-only'>Scroll to bottom</span>

‎apps/sim/app/(interfaces)/chat/components/message/components/file-download.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export function ChatFileDownload({ file }: ChatFileDownloadProps) {
172172
variant='default'
173173
onClick={handleDownload}
174174
disabled={isDownloading}
175-
className='group flex h-auto w-[200px] gap-2 rounded-lg px-3 py-2'
175+
className='group flex w-[200px] gap-2 rounded-lg px-3 py-2'
176176
>
177177
<div className='flex size-8 shrink-0 items-center justify-center'>{renderIcon()}</div>
178178
<div className='min-w-0 flex-1 text-left'>

‎apps/sim/app/playground/page.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export default function PlaygroundPage() {
172172
aria-label='Go back'
173173
variant='ghost'
174174
onClick={() => router.back()}
175-
className='size-8 p-0'
175+
iconSize='roomy'
176176
>
177177
<ArrowLeft className='size-4' />
178178
</Button>
@@ -187,7 +187,7 @@ export default function PlaygroundPage() {
187187
aria-label={isDarkMode ? 'Light mode' : 'Dark mode'}
188188
variant='default'
189189
onClick={toggleDarkMode}
190-
className='size-8 p-0'
190+
iconSize='roomy'
191191
>
192192
{isDarkMode ? <Sun className='size-4' /> : <Moon className='size-4' />}
193193
</Button>
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act, createRef } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import {
8+
CodeSearchOverlay,
9+
type CodeSearchOverlayProps,
10+
} from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay'
11+
12+
let host: HTMLDivElement
13+
let root: Root
14+
const inputRef = createRef<HTMLInputElement>()
15+
16+
const callbacks = {
17+
onQueryChange: vi.fn(),
18+
onPrevious: vi.fn(),
19+
onNext: vi.fn(),
20+
onClose: vi.fn(),
21+
}
22+
const parentClick = vi.fn()
23+
24+
function renderOverlay(props: Partial<CodeSearchOverlayProps> = {}) {
25+
act(() =>
26+
root.render(
27+
<div onClick={parentClick}>
28+
<CodeSearchOverlay
29+
className='top-0 right-0'
30+
inputKind='chip'
31+
inputRef={inputRef}
32+
query='error'
33+
matchCount={3}
34+
currentMatchIndex={1}
35+
{...callbacks}
36+
{...props}
37+
/>
38+
</div>
39+
)
40+
)
41+
const overlay = host.firstElementChild?.firstElementChild as HTMLDivElement
42+
const input = overlay.querySelector('input') as HTMLInputElement
43+
return { overlay, input }
44+
}
45+
46+
beforeEach(() => {
47+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
48+
vi.clearAllMocks()
49+
host = document.createElement('div')
50+
document.body.appendChild(host)
51+
root = createRoot(host)
52+
})
53+
54+
afterEach(() => {
55+
act(() => root.unmount())
56+
host.remove()
57+
})
58+
59+
describe('CodeSearchOverlay', () => {
60+
it('shares the floating chrome and routes query, navigation, and close actions', () => {
61+
const { overlay, input } = renderOverlay()
62+
expect(overlay.className).toContain('h-[34px]')
63+
expect(overlay.className).toContain('rounded-sm bg-[var(--surface-1)]')
64+
expect(overlay.className).toContain('top-0 right-0')
65+
expect(overlay.getAttribute('role')).toBe('presentation')
66+
expect(inputRef.current).toBe(input)
67+
expect(input.getAttribute('aria-label')).toBe('Search code')
68+
expect(input.value).toBe('error')
69+
expect(overlay.textContent).toContain('2/3')
70+
const tally = overlay.querySelector('[aria-live="polite"][aria-atomic="true"]')
71+
expect(tally?.textContent).toBe('2/3')
72+
73+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
74+
act(() => {
75+
setter?.call(input, 'failed')
76+
input.dispatchEvent(new Event('input', { bubbles: true }))
77+
})
78+
expect(callbacks.onQueryChange).toHaveBeenCalledWith('failed')
79+
act(() => {
80+
overlay.querySelector<HTMLButtonElement>('[aria-label="Previous match"]')?.click()
81+
overlay.querySelector<HTMLButtonElement>('[aria-label="Next match"]')?.click()
82+
overlay.querySelector<HTMLButtonElement>('[aria-label="Close search"]')?.click()
83+
})
84+
expect(callbacks.onPrevious).toHaveBeenCalledTimes(1)
85+
expect(callbacks.onNext).toHaveBeenCalledTimes(1)
86+
expect(callbacks.onClose).toHaveBeenCalledTimes(1)
87+
expect(parentClick).not.toHaveBeenCalled()
88+
89+
renderOverlay({ matchCount: 0, currentMatchIndex: 0 })
90+
expect(tally?.textContent).toBe('0/0')
91+
})
92+
93+
it('keeps preview search compact in a floating overlay with a usable input ref', () => {
94+
const { overlay, input } = renderOverlay({
95+
inputKind: 'plain',
96+
className: 'top-10 right-[8px]',
97+
})
98+
expect(overlay.getAttribute('role')).toBe('presentation')
99+
expect(overlay.className).toContain('top-10 right-[8px]')
100+
expect(overlay.hasAttribute('data-toolbar-root')).toBe(false)
101+
expect(input.parentElement?.className).toContain('h-[23px]')
102+
expect(inputRef.current).toBe(input)
103+
104+
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set
105+
act(() => {
106+
setter?.call(input, 'preview')
107+
input.dispatchEvent(new Event('input', { bubbles: true }))
108+
overlay.querySelector<HTMLButtonElement>('[aria-label="Next match"]')?.click()
109+
})
110+
expect(callbacks.onQueryChange).toHaveBeenCalledWith('preview')
111+
expect(callbacks.onNext).toHaveBeenCalledTimes(1)
112+
expect(parentClick).not.toHaveBeenCalled()
113+
})
114+
115+
it('retains the attached terminal edge, marker, wider tally, and disabled navigation', () => {
116+
const { overlay, input } = renderOverlay({
117+
appearance: 'attached',
118+
inputKind: 'plain',
119+
className: 'top-[30px] right-[8px]',
120+
query: '',
121+
matchCount: 0,
122+
currentMatchIndex: 0,
123+
})
124+
expect(overlay.className).toContain('rounded-b-[4px] border-t-0 bg-[var(--bg)]')
125+
expect(overlay.getAttribute('data-toolbar-root')).toBe('true')
126+
expect(overlay.getAttribute('data-search-active')).toBe('true')
127+
expect(overlay.hasAttribute('role')).toBe(false)
128+
expect(input.parentElement?.className).toContain('h-[23px]')
129+
expect(input.parentElement?.className).toContain('w-[94px]')
130+
expect(input.className).toContain('text-caption')
131+
expect(overlay.textContent).toContain('No results')
132+
expect(overlay.querySelector('span.w-\\[58px\\]')).not.toBeNull()
133+
const previous = overlay.querySelector<HTMLButtonElement>('[aria-label="Previous match"]')
134+
const next = overlay.querySelector<HTMLButtonElement>('[aria-label="Next match"]')
135+
const close = overlay.querySelector<HTMLButtonElement>('[aria-label="Close search"]')
136+
expect(previous?.disabled).toBe(true)
137+
expect(next?.disabled).toBe(true)
138+
expect(close?.disabled).toBe(false)
139+
expect(previous?.className).toContain('-m-1.5')
140+
expect(previous?.querySelector('svg')?.getAttribute('class')).toContain('size-[14px]')
141+
act(() => {
142+
previous?.click()
143+
next?.click()
144+
close?.click()
145+
})
146+
expect(callbacks.onPrevious).not.toHaveBeenCalled()
147+
expect(callbacks.onNext).not.toHaveBeenCalled()
148+
expect(callbacks.onClose).toHaveBeenCalledTimes(1)
149+
})
150+
151+
it('shows the compact no-results tally for other code panels', () => {
152+
const { overlay } = renderOverlay({ matchCount: 0, currentMatchIndex: 0 })
153+
expect(overlay.textContent).toContain('0/0')
154+
expect(overlay.getAttribute('data-toolbar-root')).toBeNull()
155+
expect(
156+
overlay.querySelector<HTMLButtonElement>('[aria-label="Previous match"]')?.disabled
157+
).toBe(true)
158+
})
159+
})
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { ChangeEvent, Ref } from 'react'
2+
import { Button, ChipInput, cn } from '@sim/emcn'
3+
import { ArrowDown, ArrowUp, X } from '@sim/emcn/icons'
4+
5+
export interface CodeSearchOverlayProps {
6+
/** The attached terminal panel has a joined lower edge and wider result tally. */
7+
appearance?: 'floating' | 'attached'
8+
/** Position relative to the owning code panel. */
9+
className: string
10+
/** Logs use the 30px chip field; previews and terminal output use compact search. */
11+
inputKind: 'chip' | 'plain'
12+
inputRef: Ref<HTMLInputElement>
13+
query: string
14+
onQueryChange: (query: string) => void
15+
matchCount: number
16+
currentMatchIndex: number
17+
onPrevious: () => void
18+
onNext: () => void
19+
onClose: () => void
20+
}
21+
22+
/** Shared controls for searching a Code.Viewer without owning its search state. */
23+
export function CodeSearchOverlay({
24+
appearance = 'floating',
25+
className,
26+
inputKind,
27+
inputRef,
28+
query,
29+
onQueryChange,
30+
matchCount,
31+
currentMatchIndex,
32+
onPrevious,
33+
onNext,
34+
onClose,
35+
}: CodeSearchOverlayProps) {
36+
const attached = appearance === 'attached'
37+
const inputProps = {
38+
ref: inputRef,
39+
type: 'text',
40+
value: query,
41+
onChange: (event: ChangeEvent<HTMLInputElement>) => onQueryChange(event.target.value),
42+
placeholder: 'Search...',
43+
'aria-label': 'Search code',
44+
} as const
45+
const actionProps = {
46+
type: 'button' as const,
47+
variant: 'ghost' as const,
48+
iconPadding: attached ? ('md' as const) : ('sm' as const),
49+
className: attached ? '-m-1.5' : undefined,
50+
}
51+
const iconClass = attached ? 'size-[14px]' : 'size-[12px]'
52+
53+
return (
54+
<div
55+
role={attached ? undefined : 'presentation'}
56+
className={cn(
57+
'absolute z-30 flex h-[34px] items-center gap-1.5 border border-[var(--border)] px-1.5 shadow-xs',
58+
attached ? 'rounded-b-[4px] border-t-0 bg-[var(--bg)]' : 'rounded-sm bg-[var(--surface-1)]',
59+
className
60+
)}
61+
onClick={(event) => event.stopPropagation()}
62+
data-toolbar-root={attached ? true : undefined}
63+
data-search-active={attached ? true : undefined}
64+
>
65+
{inputKind === 'chip' ? (
66+
<ChipInput {...inputProps} className='mr-0.5 w-[94px]' />
67+
) : (
68+
<ChipInput {...inputProps} appearance='compactSearch' className='mr-0.5 w-[94px]' />
69+
)}
70+
<span
71+
aria-live='polite'
72+
aria-atomic='true'
73+
className={cn(
74+
attached ? 'w-[58px] text-xs' : 'min-w-[45px] text-center text-xs',
75+
matchCount > 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]'
76+
)}
77+
>
78+
{matchCount > 0
79+
? `${currentMatchIndex + 1}/${matchCount}`
80+
: attached
81+
? 'No results'
82+
: '0/0'}
83+
</span>
84+
<Button
85+
{...actionProps}
86+
onClick={onPrevious}
87+
disabled={matchCount === 0}
88+
aria-label='Previous match'
89+
>
90+
<ArrowUp className={iconClass} />
91+
</Button>
92+
<Button {...actionProps} onClick={onNext} disabled={matchCount === 0} aria-label='Next match'>
93+
<ArrowDown className={iconClass} />
94+
</Button>
95+
<Button {...actionProps} onClick={onClose} aria-label='Close search'>
96+
<X className={iconClass} />
97+
</Button>
98+
</div>
99+
)
100+
}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,11 @@ export function ResizableImageView({ node, selected, editor, getPos }: ReactNode
282282
type='button'
283283
variant='ghost'
284284
size='icon'
285+
iconSize={{ base: 'touch', sm: 'roomy' }}
286+
iconPadding='sm'
285287
aria-label='Resize image'
286288
onPointerDown={startResize}
287-
className='absolute right-0 bottom-0 flex size-10 cursor-nwse-resize touch-none items-end justify-end p-1 sm:size-8'
289+
className='absolute right-0 bottom-0 cursor-nwse-resize touch-none items-end justify-end'
288290
>
289291
<span className='size-3 rounded-[3px] border border-[var(--bg)] bg-[var(--brand-secondary)]' />
290292
</Button>

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.test.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ describe('ToolbarButton', () => {
4141
)
4242

4343
const button = host.querySelector('button[aria-label="Add to Chat"]')
44-
expect(button?.className).toContain('size-[28px]')
44+
expect(button?.classList.contains('size-10')).toBe(true)
45+
expect(button?.classList.contains('sm:size-7')).toBe(true)
4546
expect(button?.querySelector('svg')?.className.baseVal).toContain('size-[12px]')
4647
})
4748

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ComponentType, SVGProps } from 'react'
2-
import { Button, cn, Tooltip } from '@sim/emcn'
2+
import { Button, Tooltip } from '@sim/emcn'
33

44
interface ToolbarButtonProps {
55
/** Any SVG icon component, e.g. from `@sim/emcn/icons`. */
@@ -28,17 +28,15 @@ export function ToolbarButton({
2828
<Tooltip.Trigger asChild>
2929
<Button
3030
type='button'
31-
variant={isActive ? 'active' : 'ghost'}
31+
variant={isActive ? 'active' : 'quiet'}
3232
size='icon'
33+
iconSize={{ base: 'touch', sm: 'regular' }}
3334
aria-label={label}
3435
aria-pressed={isActive}
3536
disabled={disabled}
3637
onPointerDown={(event) => event.preventDefault()}
3738
onClick={onClick}
38-
className={cn(
39-
'size-10 focus-visible:bg-[var(--surface-hover)] sm:size-[28px]',
40-
!isActive && 'hover-hover:bg-[var(--surface-hover)]'
41-
)}
39+
className='focus-visible:bg-[var(--surface-hover)]'
4240
>
4341
<Icon className={iconSize === 'compact' ? 'size-[12px]' : 'size-[14px]'} />
4442
</Button>

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/mic-button/mic-button.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe('MicButton', () => {
7272
const waveform = container.querySelector('svg[viewBox="0 0 18 18"]')
7373
const bars = waveform?.querySelectorAll('line')
7474

75-
expect(button?.className).toContain('size-[28px]')
75+
expect(button?.classList.contains('size-7')).toBe(true)
7676
expect(button?.className).toContain('overflow-hidden')
7777
expect(button?.className).toContain('rounded-full')
7878
expect(waveform?.classList.contains('size-[18px]')).toBe(true)

0 commit comments

Comments
 (0)