Skip to content

Commit f180a1b

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
improvement(ui): centralize tab strip action geometry
1 parent d5df6f6 commit f180a1b

9 files changed

Lines changed: 100 additions & 87 deletions

File tree

‎apps/sim/app/(landing)/components/hero/components/hero-platform-loop/hero-resource-panel.tsx‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
DropdownMenuItem,
1010
DropdownMenuTrigger,
1111
TabStrip,
12+
TabStripAction,
1213
type TabStripItem,
1314
Tooltip,
1415
} from '@sim/emcn'
@@ -25,7 +26,6 @@ import type { LeadRecord } from '@/app/(landing)/tables/components/tables-record
2526
import { TablesRecordsTable } from '@/app/(landing)/tables/components/tables-records-preview/tables-records-table'
2627
import {
2728
RESOURCE_HEADER_CLASSES,
28-
RESOURCE_TAB_ICON_BUTTON_CLASS,
2929
RESOURCE_TAB_ICON_CLASS,
3030
resourceTabWidthClass,
3131
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
@@ -133,15 +133,14 @@ export function HeroResourcePanel({
133133
activeId === 'workflow' ? (
134134
<Tooltip.Root>
135135
<Tooltip.Trigger asChild>
136-
<Button
136+
<TabStripAction
137137
type='button'
138138
variant='subtle'
139139
onClick={onRunWorkflow}
140-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
141140
aria-label='Run workflow'
142141
>
143142
<PlayOutline className={RESOURCE_TAB_ICON_CLASS} />
144-
</Button>
143+
</TabStripAction>
145144
</Tooltip.Trigger>
146145
<Tooltip.Content side='bottom'>Run workflow</Tooltip.Content>
147146
</Tooltip.Root>
@@ -162,14 +161,9 @@ export function HeroResourcePanel({
162161
<Tooltip.Root>
163162
<Tooltip.Trigger asChild>
164163
<DropdownMenuTrigger asChild>
165-
<Button
166-
type='button'
167-
variant='subtle'
168-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
169-
aria-label='Add resource'
170-
>
164+
<TabStripAction type='button' variant='subtle' aria-label='Add resource'>
171165
<Plus className={RESOURCE_TAB_ICON_CLASS} />
172-
</Button>
166+
</TabStripAction>
173167
</DropdownMenuTrigger>
174168
</Tooltip.Trigger>
175169
<Tooltip.Content side='bottom'>Add resource</Tooltip.Content>

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx‎

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

33
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
44
import {
5-
Button,
65
cn,
76
DropdownMenu,
87
DropdownMenuContent,
@@ -14,6 +13,7 @@ import {
1413
DropdownMenuSubTrigger,
1514
DropdownMenuTrigger,
1615
NATIVE_SURFACE_OCCLUSION_PREPARE_EVENT,
16+
TabStripAction,
1717
Tooltip,
1818
} from '@sim/emcn'
1919
import { Folder, Plus } from '@sim/emcn/icons'
@@ -30,10 +30,7 @@ import {
3030
byResourceMenuOrder,
3131
getResourceConfig,
3232
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
33-
import {
34-
RESOURCE_TAB_ICON_BUTTON_CLASS,
35-
RESOURCE_TAB_ICON_CLASS,
36-
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
33+
import { RESOURCE_TAB_ICON_CLASS } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
3734
import type {
3835
MothershipResource,
3936
MothershipResourceType,
@@ -676,13 +673,9 @@ export function AddResourceDropdown({
676673
<Tooltip.Root>
677674
<Tooltip.Trigger asChild>
678675
<DropdownMenuTrigger asChild>
679-
<Button
680-
variant='subtle'
681-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
682-
aria-label='Add resource tab'
683-
>
676+
<TabStripAction variant='subtle' aria-label='Add resource tab'>
684677
<Plus className={RESOURCE_TAB_ICON_CLASS} />
685-
</Button>
678+
</TabStripAction>
686679
</DropdownMenuTrigger>
687680
</Tooltip.Trigger>
688681
<Tooltip.Content side='bottom'>

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx‎

Lines changed: 18 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
4-
import { Button, OverflowText, PlayOutline, Skeleton, Tooltip, toast } from '@sim/emcn'
4+
import { OverflowText, PlayOutline, Skeleton, TabStripAction, Tooltip, toast } from '@sim/emcn'
55
import {
66
Download,
77
FileX,
@@ -36,10 +36,7 @@ import type { BrowserPanelOverlayController } from '@/app/workspace/[workspaceId
3636
import { BrowserSession } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session'
3737
import { GenericResourceContent } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/generic-resource-content'
3838
import { TerminalSession } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session'
39-
import {
40-
RESOURCE_TAB_ICON_BUTTON_CLASS,
41-
RESOURCE_TAB_ICON_CLASS,
42-
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
39+
import { RESOURCE_TAB_ICON_CLASS } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
4340
import { hasRenderableFilePreviewContent } from '@/app/workspace/[workspaceId]/home/hooks/preview'
4441
import type {
4542
GenericResourceData,
@@ -465,34 +462,28 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor
465462
<>
466463
<Tooltip.Root>
467464
<Tooltip.Trigger asChild>
468-
<Button
469-
variant='subtle'
470-
onClick={handleOpenWorkflow}
471-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
472-
aria-label='Open workflow'
473-
>
465+
<TabStripAction variant='subtle' onClick={handleOpenWorkflow} aria-label='Open workflow'>
474466
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
475-
</Button>
467+
</TabStripAction>
476468
</Tooltip.Trigger>
477469
<Tooltip.Content side='bottom'>
478470
<p>Open workflow</p>
479471
</Tooltip.Content>
480472
</Tooltip.Root>
481473
<Tooltip.Root>
482474
<Tooltip.Trigger asChild>
483-
<Button
475+
<TabStripAction
484476
variant='subtle'
485477
onClick={() => void handleRun()}
486478
disabled={isRunButtonDisabled}
487-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
488479
aria-label={isExecuting ? 'Stop workflow' : 'Run workflow'}
489480
>
490481
{isExecuting ? (
491482
<Square className={RESOURCE_TAB_ICON_CLASS} />
492483
) : (
493484
<PlayOutline className={RESOURCE_TAB_ICON_CLASS} />
494485
)}
495-
</Button>
486+
</TabStripAction>
496487
</Tooltip.Trigger>
497488
<Tooltip.Content side='bottom'>
498489
<p>{isExecuting ? 'Stop' : 'Run workflow'}</p>
@@ -520,14 +511,13 @@ export function EmbeddedKnowledgeBaseActions({
520511
return (
521512
<Tooltip.Root>
522513
<Tooltip.Trigger asChild>
523-
<Button
514+
<TabStripAction
524515
variant='subtle'
525516
onClick={handleOpenKnowledgeBase}
526-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
527517
aria-label='Open knowledge base'
528518
>
529519
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
530-
</Button>
520+
</TabStripAction>
531521
</Tooltip.Trigger>
532522
<Tooltip.Content side='bottom'>
533523
<p>Open knowledge base</p>
@@ -562,29 +552,23 @@ function EmbeddedTableActions({ workspaceId, tableId }: EmbeddedTableActionsProp
562552
<>
563553
<Tooltip.Root>
564554
<Tooltip.Trigger asChild>
565-
<Button
566-
variant='subtle'
567-
onClick={handleOpenTable}
568-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
569-
aria-label='Open table'
570-
>
555+
<TabStripAction variant='subtle' onClick={handleOpenTable} aria-label='Open table'>
571556
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
572-
</Button>
557+
</TabStripAction>
573558
</Tooltip.Trigger>
574559
<Tooltip.Content side='bottom'>
575560
<p>Open table</p>
576561
</Tooltip.Content>
577562
</Tooltip.Root>
578563
<Tooltip.Root>
579564
<Tooltip.Trigger asChild>
580-
<Button
565+
<TabStripAction
581566
variant='subtle'
582567
onClick={() => void handleExport()}
583-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
584568
aria-label='Export table as CSV'
585569
>
586570
<Download className={RESOURCE_TAB_ICON_CLASS} />
587-
</Button>
571+
</TabStripAction>
588572
</Tooltip.Trigger>
589573
<Tooltip.Content side='bottom'>
590574
<p>Export CSV</p>
@@ -639,30 +623,24 @@ function EmbeddedFileActions({
639623
<>
640624
<Tooltip.Root>
641625
<Tooltip.Trigger asChild>
642-
<Button
643-
variant='subtle'
644-
onClick={handleOpenInFiles}
645-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
646-
aria-label='Open in files'
647-
>
626+
<TabStripAction variant='subtle' onClick={handleOpenInFiles} aria-label='Open in files'>
648627
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
649-
</Button>
628+
</TabStripAction>
650629
</Tooltip.Trigger>
651630
<Tooltip.Content side='bottom'>
652631
<p>Open in files</p>
653632
</Tooltip.Content>
654633
</Tooltip.Root>
655634
<Tooltip.Root>
656635
<Tooltip.Trigger asChild>
657-
<Button
636+
<TabStripAction
658637
variant='subtle'
659638
onClick={() => void handleDownload()}
660639
disabled={!file}
661-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
662640
aria-label='Download file'
663641
>
664642
<Download className={RESOURCE_TAB_ICON_CLASS} />
665-
</Button>
643+
</TabStripAction>
666644
</Tooltip.Trigger>
667645
<Tooltip.Content side='bottom'>
668646
<p>Download</p>
@@ -896,14 +874,9 @@ export function EmbeddedLogActions({ workspaceId, logId }: EmbeddedLogActionsPro
896874
return (
897875
<Tooltip.Root>
898876
<Tooltip.Trigger asChild>
899-
<Button
900-
variant='subtle'
901-
onClick={handleOpenInLogs}
902-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
903-
aria-label='Open in logs'
904-
>
877+
<TabStripAction variant='subtle' onClick={handleOpenInLogs} aria-label='Open in logs'>
905878
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
906-
</Button>
879+
</TabStripAction>
907880
</Tooltip.Trigger>
908881
<Tooltip.Content side='bottom'>
909882
<p>Open in logs</p>

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls.ts‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
/**
2-
* Icon-only controls in the resource header — add, preview mode, the per-resource
3-
* actions — fill the tab strip's control band, so they match the strip's own
4-
* new-tab button and the panel's collapse toggle and the header reads as one row.
5-
*/
6-
export const RESOURCE_TAB_ICON_BUTTON_CLASS = 'size-[var(--tab-strip-band,30px)] shrink-0 p-0'
7-
81
export const RESOURCE_TAB_ICON_CLASS = 'size-[16px] text-[var(--text-icon)]'
92

103
/** Shared geometry for the resource header and controls positioned over it. */

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
useState,
1111
} from 'react'
1212
import {
13-
Button,
1413
cn,
1514
TabStrip,
15+
TabStripAction,
1616
type TabStripDragContext,
1717
type TabStripItem,
1818
type TabStripSelectionSource,
@@ -41,7 +41,6 @@ import { useTerminalCloseConfirmation } from '@/app/workspace/[workspaceId]/home
4141
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
4242
import {
4343
RESOURCE_HEADER_CLASSES,
44-
RESOURCE_TAB_ICON_BUTTON_CLASS,
4544
RESOURCE_TAB_ICON_CLASS,
4645
resourceTabWidthClass,
4746
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
@@ -545,14 +544,13 @@ export function ResourceTabs({
545544
previewMode && onCyclePreviewMode ? (
546545
<Tooltip.Root>
547546
<Tooltip.Trigger asChild>
548-
<Button
547+
<TabStripAction
549548
variant='subtle'
550549
onClick={onCyclePreviewMode}
551-
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
552550
aria-label='Cycle preview mode'
553551
>
554552
<PreviewModeIcon mode={previewMode} className={RESOURCE_TAB_ICON_CLASS} />
555-
</Button>
553+
</TabStripAction>
556554
</Tooltip.Trigger>
557555
<Tooltip.Content side='bottom'>
558556
<p>{PREVIEW_MODE_LABELS[previewMode]}</p>

‎packages/emcn/src/components/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ export {
239239
tabStripItemSelector,
240240
tabStripWheelPosition,
241241
} from './tab-strip/tab-strip'
242+
export { TabStripAction, type TabStripActionProps } from './tab-strip/tab-strip-action'
242243
export {
243244
Table,
244245
TableBody,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { forwardRef } from 'react'
2+
import { cn } from '../../lib/cn'
3+
import { Button, type ButtonProps } from '../button/button'
4+
5+
export interface TabStripActionProps
6+
extends Omit<ButtonProps, 'size' | 'iconSize' | 'iconPadding'> {
7+
/** Accessible name for the icon action. */
8+
'aria-label': string
9+
/** Keeps the Button text scale without applying its separate icon geometry. */
10+
size?: Exclude<ButtonProps['size'], 'icon'>
11+
}
12+
13+
/**
14+
* Icon action sized to its tab strip's control band, defaulting to 30px.
15+
* Callers retain the Button variant, icon, tooltip and action behavior.
16+
*
17+
* @example <TabStripAction variant='subtle' aria-label='Export' onClick={onExport}><Download /></TabStripAction>
18+
*/
19+
export const TabStripAction = forwardRef<HTMLButtonElement, TabStripActionProps>(
20+
({ className, ...props }, ref) => (
21+
<Button
22+
{...props}
23+
ref={ref}
24+
className={cn('size-[var(--tab-strip-band,30px)] shrink-0 p-0', className)}
25+
/>
26+
)
27+
)
28+
29+
TabStripAction.displayName = 'TabStripAction'

‎packages/emcn/src/components/tab-strip/tab-strip.dom.test.tsx‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
/**
22
* @vitest-environment jsdom
33
*/
4-
import { act, type ReactNode } from 'react'
4+
import { act, createRef, type ReactNode } from 'react'
5+
import { Slot } from '@radix-ui/react-slot'
56
import { createRoot, type Root } from 'react-dom/client'
67
import { afterEach, describe, expect, it, vi } from 'vitest'
78
import { TabStrip, type TabStripItem } from './tab-strip'
9+
import { TabStripAction } from './tab-strip-action'
810

911
let root: Root | null = null
1012
let container: HTMLDivElement | null = null
@@ -62,6 +64,38 @@ function scrollRow(): HTMLDivElement {
6264
}
6365

6466
describe('TabStrip interactions', () => {
67+
it('forwards action refs and native props through slotted triggers', () => {
68+
const ref = createRef<HTMLButtonElement>()
69+
const onTrigger = vi.fn()
70+
const onAction = vi.fn()
71+
const renderAction = (disabled = false) => (
72+
<Slot onClick={onTrigger} data-state='closed'>
73+
<TabStripAction ref={ref} aria-label='Export' disabled={disabled} onClick={onAction}>
74+
Export
75+
</TabStripAction>
76+
</Slot>
77+
)
78+
mount(renderAction())
79+
const button = ref.current
80+
expect(button).toBe(container?.querySelector('button'))
81+
expect(button?.type).toBe('submit')
82+
expect(button?.getAttribute('data-state')).toBe('closed')
83+
expect(button?.getAttribute('aria-label')).toBe('Export')
84+
act(() => {
85+
button?.focus()
86+
button?.click()
87+
})
88+
expect(document.activeElement).toBe(button)
89+
expect(onTrigger).toHaveBeenCalledTimes(1)
90+
expect(onAction).toHaveBeenCalledTimes(1)
91+
92+
act(() => root?.render(renderAction(true)))
93+
act(() => button?.click())
94+
expect(ref.current).toBe(button)
95+
expect(onAction).toHaveBeenCalledTimes(1)
96+
expect(onTrigger).toHaveBeenCalledTimes(1)
97+
})
98+
6599
it('uses one keyboard tab stop and exposes tab semantics', () => {
66100
mount(renderStrip(tabs))
67101

0 commit comments

Comments
 (0)