Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
TabStrip,
TabStripAction,
type TabStripItem,
Tooltip,
} from '@sim/emcn'
Expand All @@ -25,7 +26,6 @@ import type { LeadRecord } from '@/app/(landing)/tables/components/tables-record
import { TablesRecordsTable } from '@/app/(landing)/tables/components/tables-records-preview/tables-records-table'
import {
RESOURCE_HEADER_CLASSES,
RESOURCE_TAB_ICON_BUTTON_CLASS,
RESOURCE_TAB_ICON_CLASS,
resourceTabWidthClass,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
Expand Down Expand Up @@ -133,15 +133,14 @@ export function HeroResourcePanel({
activeId === 'workflow' ? (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
type='button'
variant='subtle'
onClick={onRunWorkflow}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Run workflow'
>
<PlayOutline className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>Run workflow</Tooltip.Content>
</Tooltip.Root>
Expand All @@ -162,14 +161,9 @@ export function HeroResourcePanel({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenuTrigger asChild>
<Button
type='button'
variant='subtle'
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Add resource'
>
<TabStripAction type='button' variant='subtle' aria-label='Add resource'>
<Plus className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</DropdownMenuTrigger>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>Add resource</Tooltip.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
Expand All @@ -14,6 +13,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
NATIVE_SURFACE_OCCLUSION_PREPARE_EVENT,
TabStripAction,
Tooltip,
} from '@sim/emcn'
import { Folder, Plus } from '@sim/emcn/icons'
Expand All @@ -30,10 +30,7 @@ import {
byResourceMenuOrder,
getResourceConfig,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import {
RESOURCE_TAB_ICON_BUTTON_CLASS,
RESOURCE_TAB_ICON_CLASS,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
import { RESOURCE_TAB_ICON_CLASS } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
import type {
MothershipResource,
MothershipResourceType,
Expand Down Expand Up @@ -676,13 +673,9 @@ export function AddResourceDropdown({
<Tooltip.Root>
<Tooltip.Trigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant='subtle'
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Add resource tab'
>
<TabStripAction variant='subtle' aria-label='Add resource tab'>
<Plus className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</DropdownMenuTrigger>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Button, OverflowText, PlayOutline, Skeleton, Tooltip, toast } from '@sim/emcn'
import { OverflowText, PlayOutline, Skeleton, TabStripAction, Tooltip, toast } from '@sim/emcn'
import {
Download,
FileX,
Expand Down Expand Up @@ -36,10 +36,7 @@ import type { BrowserPanelOverlayController } from '@/app/workspace/[workspaceId
import { BrowserSession } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session'
import { GenericResourceContent } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/generic-resource-content'
import { TerminalSession } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session'
import {
RESOURCE_TAB_ICON_BUTTON_CLASS,
RESOURCE_TAB_ICON_CLASS,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
import { RESOURCE_TAB_ICON_CLASS } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
import { hasRenderableFilePreviewContent } from '@/app/workspace/[workspaceId]/home/hooks/preview'
import type {
GenericResourceData,
Expand Down Expand Up @@ -465,34 +462,28 @@ export function EmbeddedWorkflowActions({ workspaceId, workflowId }: EmbeddedWor
<>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenWorkflow}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open workflow'
>
<TabStripAction variant='subtle' onClick={handleOpenWorkflow} aria-label='Open workflow'>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open workflow</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
variant='subtle'
onClick={() => void handleRun()}
disabled={isRunButtonDisabled}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label={isExecuting ? 'Stop workflow' : 'Run workflow'}
>
{isExecuting ? (
<Square className={RESOURCE_TAB_ICON_CLASS} />
) : (
<PlayOutline className={RESOURCE_TAB_ICON_CLASS} />
)}
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>{isExecuting ? 'Stop' : 'Run workflow'}</p>
Expand Down Expand Up @@ -520,14 +511,13 @@ export function EmbeddedKnowledgeBaseActions({
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
variant='subtle'
onClick={handleOpenKnowledgeBase}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open knowledge base'
>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open knowledge base</p>
Expand Down Expand Up @@ -562,29 +552,23 @@ function EmbeddedTableActions({ workspaceId, tableId }: EmbeddedTableActionsProp
<>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenTable}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open table'
>
<TabStripAction variant='subtle' onClick={handleOpenTable} aria-label='Open table'>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open table</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
variant='subtle'
onClick={() => void handleExport()}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Export table as CSV'
>
<Download className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Export CSV</p>
Expand Down Expand Up @@ -639,30 +623,24 @@ function EmbeddedFileActions({
<>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenInFiles}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open in files'
>
<TabStripAction variant='subtle' onClick={handleOpenInFiles} aria-label='Open in files'>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open in files</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
variant='subtle'
onClick={() => void handleDownload()}
disabled={!file}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Download file'
>
<Download className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Download</p>
Expand Down Expand Up @@ -896,14 +874,9 @@ export function EmbeddedLogActions({ workspaceId, logId }: EmbeddedLogActionsPro
return (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenInLogs}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Open in logs'
>
<TabStripAction variant='subtle' onClick={handleOpenInLogs} aria-label='Open in logs'>
<SquareArrowUpRight className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open in logs</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* Icon-only controls in the resource header — add, preview mode, the per-resource
* actions — fill the tab strip's control band, so they match the strip's own
* new-tab button and the panel's collapse toggle and the header reads as one row.
*/
export const RESOURCE_TAB_ICON_BUTTON_CLASS = 'size-[var(--tab-strip-band,30px)] shrink-0 p-0'

export const RESOURCE_TAB_ICON_CLASS = 'size-[16px] text-[var(--text-icon)]'

/** Shared geometry for the resource header and controls positioned over it. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
useState,
} from 'react'
import {
Button,
cn,
TabStrip,
TabStripAction,
type TabStripDragContext,
type TabStripItem,
type TabStripSelectionSource,
Expand Down Expand Up @@ -41,7 +41,6 @@ import { useTerminalCloseConfirmation } from '@/app/workspace/[workspaceId]/home
import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import {
RESOURCE_HEADER_CLASSES,
RESOURCE_TAB_ICON_BUTTON_CLASS,
RESOURCE_TAB_ICON_CLASS,
resourceTabWidthClass,
} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls'
Expand Down Expand Up @@ -545,14 +544,13 @@ export function ResourceTabs({
previewMode && onCyclePreviewMode ? (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
<TabStripAction
variant='subtle'
onClick={onCyclePreviewMode}
className={RESOURCE_TAB_ICON_BUTTON_CLASS}
aria-label='Cycle preview mode'
>
<PreviewModeIcon mode={previewMode} className={RESOURCE_TAB_ICON_CLASS} />
</Button>
</TabStripAction>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>{PREVIEW_MODE_LABELS[previewMode]}</p>
Expand Down
1 change: 1 addition & 0 deletions packages/emcn/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ export {
tabStripItemSelector,
tabStripWheelPosition,
} from './tab-strip/tab-strip'
export { TabStripAction, type TabStripActionProps } from './tab-strip/tab-strip-action'
export {
Table,
TableBody,
Expand Down
29 changes: 29 additions & 0 deletions packages/emcn/src/components/tab-strip/tab-strip-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { forwardRef } from 'react'
import { cn } from '../../lib/cn'
import { Button, type ButtonProps } from '../button/button'

export interface TabStripActionProps
extends Omit<ButtonProps, 'size' | 'iconSize' | 'iconPadding'> {
/** Accessible name for the icon action. */
'aria-label': string
/** Keeps the Button text scale without applying its separate icon geometry. */
size?: Exclude<ButtonProps['size'], 'icon'>
}

/**
* Icon action sized to its tab strip's control band, defaulting to 30px.
* Callers retain the Button variant, icon, tooltip and action behavior.
*
* @example <TabStripAction variant='subtle' aria-label='Export' onClick={onExport}><Download /></TabStripAction>
*/
export const TabStripAction = forwardRef<HTMLButtonElement, TabStripActionProps>(
({ className, ...props }, ref) => (
<Button
{...props}
ref={ref}
className={cn('size-[var(--tab-strip-band,30px)] shrink-0 p-0', className)}
/>
)
)

TabStripAction.displayName = 'TabStripAction'
36 changes: 35 additions & 1 deletion packages/emcn/src/components/tab-strip/tab-strip.dom.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/**
* @vitest-environment jsdom
*/
import { act, type ReactNode } from 'react'
import { act, createRef, type ReactNode } from 'react'
import { Slot } from '@radix-ui/react-slot'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TabStrip, type TabStripItem } from './tab-strip'
import { TabStripAction } from './tab-strip-action'

let root: Root | null = null
let container: HTMLDivElement | null = null
Expand Down Expand Up @@ -62,6 +64,38 @@ function scrollRow(): HTMLDivElement {
}

describe('TabStrip interactions', () => {
it('forwards action refs and native props through slotted triggers', () => {
const ref = createRef<HTMLButtonElement>()
const onTrigger = vi.fn()
const onAction = vi.fn()
const renderAction = (disabled = false) => (
<Slot onClick={onTrigger} data-state='closed'>
<TabStripAction ref={ref} aria-label='Export' disabled={disabled} onClick={onAction}>
Export
</TabStripAction>
</Slot>
)
mount(renderAction())
const button = ref.current
expect(button).toBe(container?.querySelector('button'))
expect(button?.type).toBe('submit')
expect(button?.getAttribute('data-state')).toBe('closed')
expect(button?.getAttribute('aria-label')).toBe('Export')
act(() => {
button?.focus()
button?.click()
})
expect(document.activeElement).toBe(button)
expect(onTrigger).toHaveBeenCalledTimes(1)
expect(onAction).toHaveBeenCalledTimes(1)

act(() => root?.render(renderAction(true)))
act(() => button?.click())
expect(ref.current).toBe(button)
expect(onAction).toHaveBeenCalledTimes(1)
expect(onTrigger).toHaveBeenCalledTimes(1)
})

it('uses one keyboard tab stop and exposes tab semantics', () => {
mount(renderStrip(tabs))

Expand Down
Loading
Loading