@open-mercato/ui 0.6.6-develop.6542.1.fee3e68aa6 → 0.6.6-develop.6551.1.1511bfa890

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/primitives/tabs.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from './button'\n\n/**\n * Tab navigation primitive \u2014 backward compatible with the original\n * pill-style API. Phase B.5 rewrite adds the Figma underline variant\n * (`Tab Menu Horizontal [1.1]` in `553:734`), the vertical orientation\n * (`Tab Menu Vertical [1.1]`), and per-tab `leading` icon + `count`\n * badge slots.\n *\n * Backward compatibility (6 import sites):\n * integrations/[id]/page.tsx\n * scheduler JobLogsModal.tsx\n * checkout LinkTemplateForm.tsx\n * search VectorSearchSection.tsx + FulltextSearchSection.tsx\n * ai-assistant AiPlaygroundPageClient.tsx\n *\n * All six call `<Tabs>` + `<TabsList>` + `<TabsTrigger>` +\n * `<TabsContent>` with the default pill look. The default `variant`\n * stays `pill` so they keep rendering verbatim.\n */\n\ntype TabsContextValue = {\n value: string\n onValueChange: (value: string) => void\n variant: 'pill' | 'underline'\n orientation: 'horizontal' | 'vertical'\n}\n\nexport const TabsContext = createContext<TabsContextValue | undefined>(undefined)\n\nexport function useTabsContext() {\n const context = useContext(TabsContext)\n if (!context) {\n throw new Error('Tabs components must be used within a Tabs provider')\n }\n return context\n}\n\nexport type TabsVariant = 'pill' | 'underline'\nexport type TabsOrientation = 'horizontal' | 'vertical'\n\nexport type TabsProps = {\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n /** Visual style. `pill` is the original look (bg-muted container,\n * bg-background selected pill). `underline` matches Figma `Tab Menu\n * Horizontal [1.1]` \u2014 flat strip with a thin border-bottom and an\n * accent-indigo underline on the active tab. */\n variant?: TabsVariant\n /** Tab strip orientation. `horizontal` lays the strip in a row;\n * `vertical` lays it in a column. The default `pill` variant\n * supports both; the underline variant supports only `horizontal`\n * (per Figma \u2014 vertical underline is not a documented pattern). */\n orientation?: TabsOrientation\n children: React.ReactNode\n className?: string\n}\n\nexport function Tabs({\n value: controlledValue,\n defaultValue,\n onValueChange,\n variant = 'pill',\n orientation = 'horizontal',\n children,\n className,\n}: TabsProps) {\n const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue ?? '')\n const isControlled = controlledValue !== undefined\n const value = isControlled ? controlledValue : uncontrolledValue\n\n const handleValueChange = React.useCallback(\n (newValue: string) => {\n if (!isControlled) {\n setUncontrolledValue(newValue)\n }\n onValueChange?.(newValue)\n },\n [isControlled, onValueChange],\n )\n\n const contextValue = React.useMemo<TabsContextValue>(\n () => ({ value, onValueChange: handleValueChange, variant, orientation }),\n [value, handleValueChange, variant, orientation],\n )\n\n return (\n <TabsContext.Provider value={contextValue}>\n <div\n data-slot=\"tabs\"\n data-variant={variant}\n data-orientation={orientation}\n className={cn(orientation === 'vertical' ? 'flex gap-4' : '', className)}\n >\n {children}\n </div>\n </TabsContext.Provider>\n )\n}\n\nexport type TabsListProps = {\n children: React.ReactNode\n className?: string\n}\n\nexport function TabsList({ children, className }: TabsListProps) {\n const { variant, orientation } = useTabsContext()\n\n const baseClasses =\n variant === 'underline'\n ? // Underline strip \u2014 flat, full-width border-bottom for the rail,\n // selected trigger gets its own border-b-2 accent below.\n orientation === 'vertical'\n ? 'inline-flex flex-col items-stretch gap-1 border-r border-input pr-3'\n : 'inline-flex h-10 items-center justify-start gap-4 border-b border-input text-muted-foreground'\n : // Pill strip (original) \u2014 rounded container, bg-muted shell.\n orientation === 'vertical'\n ? 'inline-flex flex-col items-stretch gap-1 rounded-lg bg-muted p-1 text-muted-foreground'\n : 'inline-flex h-9 items-center justify-start gap-1 rounded-lg bg-muted p-1 text-muted-foreground'\n\n return (\n <div\n data-slot=\"tabs-list\"\n className={cn(baseClasses, className)}\n role=\"tablist\"\n aria-orientation={orientation}\n >\n {children}\n </div>\n )\n}\n\nexport type TabsTriggerProps = {\n value: string\n children: React.ReactNode\n className?: string\n disabled?: boolean\n /** Optional leading icon slot \u2014 typically a lucide-react icon at\n * `size-4`. Per Figma `Tab Menu Horizontal Items [1.1]`. */\n leading?: React.ReactNode\n /** Optional trailing count badge \u2014 typically a numeric count or\n * \"New\". Rendered as a muted pill that highlights to indigo when\n * the tab is selected. */\n count?: React.ReactNode\n}\n\nexport function TabsTrigger({\n value,\n children,\n className,\n disabled,\n leading,\n count,\n}: TabsTriggerProps) {\n const { value: selectedValue, onValueChange, variant, orientation } = useTabsContext()\n const isSelected = selectedValue === value\n\n if (variant === 'underline') {\n return (\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected}\n disabled={disabled}\n onClick={() => onValueChange(value)}\n data-slot=\"tabs-trigger\"\n data-state={isSelected ? 'active' : 'inactive'}\n data-variant=\"underline\"\n className={cn(\n // Underline trigger \u2014 flat, bottom-border accent when active.\n // Negative margin-bottom -1px so the active accent sits on\n // top of the rail's border-bottom (rather than below it).\n orientation === 'vertical'\n ? 'group relative inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground outline-none transition-colors'\n : 'group relative -mb-px inline-flex items-center gap-2 border-b-2 border-transparent px-1 py-2 text-sm font-medium text-muted-foreground outline-none transition-colors',\n 'hover:text-foreground focus-visible:shadow-focus',\n 'disabled:pointer-events-none disabled:opacity-50',\n isSelected\n ? orientation === 'vertical'\n ? 'bg-muted/40 text-foreground'\n : 'border-accent-indigo font-semibold text-foreground'\n : '',\n className,\n )}\n >\n {leading ? (\n <span\n data-slot=\"tabs-trigger-leading\"\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex shrink-0 items-center justify-center',\n isSelected ? 'text-accent-indigo' : 'text-muted-foreground',\n )}\n >\n {leading}\n </span>\n ) : null}\n <span className=\"min-w-0 truncate\">{children}</span>\n {count !== undefined ? (\n <span\n data-slot=\"tabs-trigger-count\"\n className={cn(\n 'inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium',\n isSelected\n ? 'bg-accent-indigo/10 text-accent-indigo'\n : 'bg-muted text-muted-foreground',\n )}\n >\n {count}\n </span>\n ) : null}\n </button>\n )\n }\n\n // Pill variant (original look) \u2014 `<Button variant=\"ghost\" size=\"sm\">`\n // with state classes. Backward compatible.\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"tab\"\n aria-selected={isSelected}\n disabled={disabled}\n onClick={() => onValueChange(value)}\n data-slot=\"tabs-trigger\"\n data-state={isSelected ? 'active' : 'inactive'}\n data-variant=\"pill\"\n className={cn(\n 'gap-2',\n isSelected\n ? 'bg-background text-foreground shadow'\n : 'hover:bg-background/80 hover:text-foreground',\n className,\n )}\n >\n {leading ? (\n <span\n data-slot=\"tabs-trigger-leading\"\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center\"\n >\n {leading}\n </span>\n ) : null}\n <span>{children}</span>\n {count !== undefined ? (\n <span\n data-slot=\"tabs-trigger-count\"\n className={cn(\n 'inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium',\n isSelected\n ? 'bg-accent-indigo/10 text-accent-indigo'\n : 'bg-background/60 text-muted-foreground',\n )}\n >\n {count}\n </span>\n ) : null}\n </Button>\n )\n}\n\nexport type TabsContentProps = {\n value: string\n children: React.ReactNode\n className?: string\n}\n\nexport function TabsContent({ value, children, className }: TabsContentProps) {\n const { value: selectedValue, orientation } = useTabsContext()\n\n if (selectedValue !== value) {\n return null\n }\n\n return (\n <div\n role=\"tabpanel\"\n data-slot=\"tabs-content\"\n className={cn(\n orientation === 'vertical' ? 'flex-1 min-w-0' : 'mt-2',\n 'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { createContext, useContext } from 'react'\n\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { Button } from './button'\n\n/**\n * Tab navigation primitive \u2014 backward compatible with the original\n * pill-style API. Phase B.5 rewrite adds the Figma underline variant\n * (`Tab Menu Horizontal [1.1]` in `553:734`), the vertical orientation\n * (`Tab Menu Vertical [1.1]`), and per-tab `leading` icon + `count`\n * badge slots.\n *\n * Backward compatibility (6 import sites):\n * integrations/[id]/page.tsx\n * scheduler JobLogsModal.tsx\n * checkout LinkTemplateForm.tsx\n * search VectorSearchSection.tsx + FulltextSearchSection.tsx\n * ai-assistant AiPlaygroundPageClient.tsx\n *\n * All six call `<Tabs>` + `<TabsList>` + `<TabsTrigger>` +\n * `<TabsContent>` with the default pill look. The default `variant`\n * stays `pill` so they keep rendering verbatim.\n */\n\ntype TabsContextValue = {\n value: string\n onValueChange: (value: string) => void\n variant: 'pill' | 'underline'\n orientation: 'horizontal' | 'vertical'\n}\n\nexport const TabsContext = createContext<TabsContextValue | undefined>(undefined)\n\nexport function useTabsContext() {\n const context = useContext(TabsContext)\n if (!context) {\n throw new Error('Tabs components must be used within a Tabs provider')\n }\n return context\n}\n\nexport type TabsVariant = 'pill' | 'underline'\nexport type TabsOrientation = 'horizontal' | 'vertical'\n\nexport type TabsProps = {\n value?: string\n defaultValue?: string\n onValueChange?: (value: string) => void\n /** Visual style. `pill` is the original look (bg-muted container,\n * bg-background selected pill). `underline` matches Figma `Tab Menu\n * Horizontal [1.1]` \u2014 flat strip with a thin border-bottom and an\n * accent-indigo underline on the active tab. */\n variant?: TabsVariant\n /** Tab strip orientation. `horizontal` lays the strip in a row;\n * `vertical` lays it in a column. The default `pill` variant\n * supports both; the underline variant supports only `horizontal`\n * (per Figma \u2014 vertical underline is not a documented pattern). */\n orientation?: TabsOrientation\n children: React.ReactNode\n className?: string\n}\n\nexport function Tabs({\n value: controlledValue,\n defaultValue,\n onValueChange,\n variant = 'pill',\n orientation = 'horizontal',\n children,\n className,\n}: TabsProps) {\n const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue ?? '')\n const isControlled = controlledValue !== undefined\n const value = isControlled ? controlledValue : uncontrolledValue\n\n const handleValueChange = React.useCallback(\n (newValue: string) => {\n if (!isControlled) {\n setUncontrolledValue(newValue)\n }\n onValueChange?.(newValue)\n },\n [isControlled, onValueChange],\n )\n\n const contextValue = React.useMemo<TabsContextValue>(\n () => ({ value, onValueChange: handleValueChange, variant, orientation }),\n [value, handleValueChange, variant, orientation],\n )\n\n return (\n <TabsContext.Provider value={contextValue}>\n <div\n data-slot=\"tabs\"\n data-variant={variant}\n data-orientation={orientation}\n className={cn(orientation === 'vertical' ? 'flex gap-4' : '', className)}\n >\n {children}\n </div>\n </TabsContext.Provider>\n )\n}\n\nexport type TabsListProps = {\n children: React.ReactNode\n className?: string\n /** Accessible name for the `role=\"tablist\"` element. Set it when the\n * surrounding context does not already label the strip (e.g. a bare\n * detail-page section switcher). */\n 'aria-label'?: string\n}\n\nexport function TabsList({ children, className, 'aria-label': ariaLabel }: TabsListProps) {\n const { variant, orientation } = useTabsContext()\n\n const baseClasses =\n variant === 'underline'\n ? // Underline strip \u2014 flat, full-width border-bottom for the rail,\n // selected trigger gets its own border-b-2 accent below.\n orientation === 'vertical'\n ? 'inline-flex flex-col items-stretch gap-1 border-r border-input pr-3'\n : 'inline-flex h-10 items-center justify-start gap-4 border-b border-input text-muted-foreground'\n : // Pill strip (original) \u2014 rounded container, bg-muted shell.\n orientation === 'vertical'\n ? 'inline-flex flex-col items-stretch gap-1 rounded-lg bg-muted p-1 text-muted-foreground'\n : 'inline-flex h-9 items-center justify-start gap-1 rounded-lg bg-muted p-1 text-muted-foreground'\n\n return (\n <div\n data-slot=\"tabs-list\"\n className={cn(baseClasses, className)}\n role=\"tablist\"\n aria-label={ariaLabel}\n aria-orientation={orientation}\n >\n {children}\n </div>\n )\n}\n\nexport type TabsTriggerProps = {\n value: string\n children: React.ReactNode\n className?: string\n disabled?: boolean\n /** Optional leading icon slot \u2014 typically a lucide-react icon at\n * `size-4`. Per Figma `Tab Menu Horizontal Items [1.1]`. */\n leading?: React.ReactNode\n /** Optional trailing count badge \u2014 typically a numeric count or\n * \"New\". Rendered as a muted pill that highlights to indigo when\n * the tab is selected. */\n count?: React.ReactNode\n}\n\nexport function TabsTrigger({\n value,\n children,\n className,\n disabled,\n leading,\n count,\n}: TabsTriggerProps) {\n const { value: selectedValue, onValueChange, variant, orientation } = useTabsContext()\n const isSelected = selectedValue === value\n\n if (variant === 'underline') {\n return (\n <button\n type=\"button\"\n role=\"tab\"\n aria-selected={isSelected}\n disabled={disabled}\n onClick={() => onValueChange(value)}\n data-slot=\"tabs-trigger\"\n data-state={isSelected ? 'active' : 'inactive'}\n data-variant=\"underline\"\n className={cn(\n // Underline trigger \u2014 flat, bottom-border accent when active.\n // Negative margin-bottom -1px so the active accent sits on\n // top of the rail's border-bottom (rather than below it).\n orientation === 'vertical'\n ? 'group relative inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground outline-none transition-colors'\n : 'group relative -mb-px inline-flex items-center gap-2 border-b-2 border-transparent px-1 py-2 text-sm font-medium text-muted-foreground outline-none transition-colors',\n 'hover:text-foreground focus-visible:shadow-focus',\n 'disabled:pointer-events-none disabled:opacity-50',\n isSelected\n ? orientation === 'vertical'\n ? 'bg-muted/40 text-foreground'\n : 'border-accent-indigo font-semibold text-foreground'\n : '',\n className,\n )}\n >\n {leading ? (\n <span\n data-slot=\"tabs-trigger-leading\"\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex shrink-0 items-center justify-center',\n isSelected ? 'text-accent-indigo' : 'text-muted-foreground',\n )}\n >\n {leading}\n </span>\n ) : null}\n <span className=\"min-w-0 truncate\">{children}</span>\n {count !== undefined ? (\n <span\n data-slot=\"tabs-trigger-count\"\n className={cn(\n 'inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium',\n isSelected\n ? 'bg-accent-indigo/10 text-accent-indigo'\n : 'bg-muted text-muted-foreground',\n )}\n >\n {count}\n </span>\n ) : null}\n </button>\n )\n }\n\n // Pill variant (original look) \u2014 `<Button variant=\"ghost\" size=\"sm\">`\n // with state classes. Backward compatible.\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n role=\"tab\"\n aria-selected={isSelected}\n disabled={disabled}\n onClick={() => onValueChange(value)}\n data-slot=\"tabs-trigger\"\n data-state={isSelected ? 'active' : 'inactive'}\n data-variant=\"pill\"\n className={cn(\n 'gap-2',\n isSelected\n ? 'bg-background text-foreground shadow'\n : 'hover:bg-background/80 hover:text-foreground',\n className,\n )}\n >\n {leading ? (\n <span\n data-slot=\"tabs-trigger-leading\"\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center\"\n >\n {leading}\n </span>\n ) : null}\n <span>{children}</span>\n {count !== undefined ? (\n <span\n data-slot=\"tabs-trigger-count\"\n className={cn(\n 'inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 text-xs font-medium',\n isSelected\n ? 'bg-accent-indigo/10 text-accent-indigo'\n : 'bg-background/60 text-muted-foreground',\n )}\n >\n {count}\n </span>\n ) : null}\n </Button>\n )\n}\n\nexport type TabsContentProps = {\n value: string\n children: React.ReactNode\n className?: string\n}\n\nexport function TabsContent({ value, children, className }: TabsContentProps) {\n const { value: selectedValue, orientation } = useTabsContext()\n\n if (selectedValue !== value) {\n return null\n }\n\n return (\n <div\n role=\"tabpanel\"\n data-slot=\"tabs-content\"\n className={cn(\n orientation === 'vertical' ? 'flex-1 min-w-0' : 'mt-2',\n 'ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',\n className,\n )}\n >\n {children}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/ui",
3
- "version": "0.6.6-develop.6542.1.fee3e68aa6",
3
+ "version": "0.6.6-develop.6551.1.1511bfa890",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -155,13 +155,13 @@
155
155
  "remark-gfm": "^4.0.1"
156
156
  },
157
157
  "peerDependencies": {
158
- "@open-mercato/shared": "0.6.6-develop.6542.1.fee3e68aa6",
158
+ "@open-mercato/shared": "0.6.6-develop.6551.1.1511bfa890",
159
159
  "react": ">=18.0.0",
160
160
  "react-dom": ">=18.0.0",
161
161
  "react-is": ">=18.0.0"
162
162
  },
163
163
  "devDependencies": {
164
- "@open-mercato/shared": "0.6.6-develop.6542.1.fee3e68aa6",
164
+ "@open-mercato/shared": "0.6.6-develop.6551.1.1511bfa890",
165
165
  "@testing-library/dom": "^10.4.1",
166
166
  "@testing-library/jest-dom": "^6.9.1",
167
167
  "@testing-library/react": "^16.3.1",
@@ -97,7 +97,7 @@ export function ChatPaneTabs({ agentId, className }: ChatPaneTabsProps) {
97
97
  className={cn(
98
98
  'group flex max-w-[12rem] shrink-0 items-center gap-1 rounded-t-md border-b-2 px-2 py-1',
99
99
  isActive
100
- ? 'border-primary bg-background text-foreground'
100
+ ? 'border-accent-indigo bg-background text-foreground'
101
101
  : 'border-transparent text-muted-foreground hover:text-foreground',
102
102
  )}
103
103
  >
@@ -126,7 +126,7 @@ export function ChatPaneTabs({ agentId, className }: ChatPaneTabsProps) {
126
126
  onClick={() => sessions.setActiveSession(session.id)}
127
127
  onDoubleClick={() => startRename(session)}
128
128
  title={label}
129
- className="truncate text-xs font-medium"
129
+ className="truncate rounded-sm text-xs font-medium outline-none focus-visible:shadow-focus"
130
130
  >
131
131
  {label}
132
132
  </button>
@@ -1494,13 +1494,13 @@ function AppShellBody({ productName, logo, email, canManageUpgradeActions = fals
1494
1494
  aria-selected={isActive}
1495
1495
  aria-controls="mobile-drawer-tabpanel"
1496
1496
  onClick={() => setMobileDrawerView(tab.id === 'main' ? 'main' : 'auto')}
1497
- className="relative inline-flex items-center pb-2 text-sm font-medium leading-5 tracking-tight transition-colors focus:outline-none data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground"
1497
+ className="relative inline-flex items-center pb-2 text-sm font-medium leading-5 tracking-tight transition-colors outline-none focus-visible:shadow-focus data-[active=true]:text-foreground data-[active=false]:text-muted-foreground hover:text-foreground"
1498
1498
  data-active={isActive}
1499
1499
  >
1500
1500
  <span>{tab.label}</span>
1501
1501
  {isActive ? (
1502
1502
  <span
1503
- className="absolute -bottom-px left-0 right-0 h-0.5 bg-foreground"
1503
+ className="absolute -bottom-px left-0 right-0 h-0.5 bg-accent-indigo"
1504
1504
  aria-hidden="true"
1505
1505
  />
1506
1506
  ) : null}
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as React from 'react'
5
5
  import { renderToString } from 'react-dom/server'
6
- import { fireEvent, render, screen, waitFor } from '@testing-library/react'
6
+ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
7
7
  import type { ColumnDef } from '@tanstack/react-table'
8
8
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
9
9
  import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
@@ -21,6 +21,16 @@ const flashMock = jest.fn()
21
21
  jest.mock('../injection/useInjectionDataWidgets', () => ({
22
22
  useInjectionDataWidgets: (spotId: string) => useInjectionDataWidgetsMock(spotId),
23
23
  }))
24
+ // InjectionSpot/useInjectionSpotEvents (rendered whenever injectionSpotId is
25
+ // set) load spot widgets through the real async registry loader; unmocked it
26
+ // resolves at an arbitrary point of the test run, racing waitFor() flushes
27
+ // and firing setState outside act().
28
+ jest.mock('@open-mercato/shared/modules/widgets/injection-loader', () => ({
29
+ getInjectionRegistryVersion: () => 0,
30
+ subscribeToInjectionRegistryChanges: () => () => {},
31
+ loadInjectionWidgetsForSpot: jest.fn(async () => []),
32
+ loadInjectionDataWidgetsForSpot: jest.fn(async () => []),
33
+ }))
24
34
  jest.mock('../FlashMessages', () => ({
25
35
  flash: (...args: unknown[]) => flashMock(...args),
26
36
  }))
@@ -297,19 +307,25 @@ describe('DataTable extensions', () => {
297
307
  try {
298
308
  fireEvent.click(screen.getByRole('button', { name: 'Delete all filtered' }))
299
309
 
310
+ // onExecute being CALLED is not enough — job-1 is registered as tracked
311
+ // in the click handler's continuation after `await onExecute()`. The
312
+ // "started" flash is the observable proof that continuation ran; without
313
+ // it the completion event below can race the registration and be ignored.
300
314
  await waitFor(() => expect(onExecute).toHaveBeenCalledTimes(1))
301
- await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
315
+ await waitFor(() => expect(flashMock).toHaveBeenCalledWith(expect.any(String), 'success'))
302
316
  refreshButtonMock.mockClear()
303
317
  mockRouterRefresh.mockClear()
304
318
 
305
- window.dispatchEvent(new CustomEvent(APP_EVENT_DOM_NAME, {
306
- detail: {
307
- id: 'progress.job.completed',
308
- payload: { jobId: 'job-1' },
309
- timestamp: Date.now(),
310
- organizationId: 'org-1',
311
- },
312
- }))
319
+ act(() => {
320
+ window.dispatchEvent(new CustomEvent(APP_EVENT_DOM_NAME, {
321
+ detail: {
322
+ id: 'progress.job.completed',
323
+ payload: { jobId: 'job-1' },
324
+ timestamp: Date.now(),
325
+ organizationId: 'org-1',
326
+ },
327
+ }))
328
+ })
313
329
 
314
330
  await waitFor(() => expect(refreshButtonMock).toHaveBeenCalledTimes(1))
315
331
  expect(mockRouterRefresh).not.toHaveBeenCalled()
@@ -6,6 +6,7 @@ import { Button } from '../../primitives/button'
6
6
  import { IconButton } from '../../primitives/icon-button'
7
7
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
8
8
  import { Input } from '@open-mercato/ui/primitives/input'
9
+ import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
9
10
  import { TagsInput } from '@open-mercato/ui/backend/inputs/TagsInput'
10
11
  import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
11
12
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
@@ -576,29 +577,21 @@ export function AttachmentMetadataDialog({ open, onOpenChange, item, availableTa
576
577
  </div>
577
578
  {isImage ? (
578
579
  <div className="rounded border">
579
- <div className="flex flex-wrap gap-4 border-b px-3 py-2 text-sm font-medium" role="tablist">
580
- {(['preview', 'resize'] as const).map((tab) => (
581
- <Button
582
- key={tab}
583
- type="button"
584
- variant="ghost"
585
- size="sm"
586
- role="tab"
587
- aria-selected={imageTab === tab}
588
- onClick={() => setImageTab(tab)}
589
- className={cn(
590
- 'h-auto -mb-px rounded-none border-b-2 border-transparent px-0 py-1',
591
- imageTab === tab
592
- ? 'border-accent-indigo text-foreground'
593
- : 'text-muted-foreground hover:text-foreground',
594
- )}
595
- >
596
- {tab === 'preview'
597
- ? t('attachments.library.metadata.preview', 'Preview')
598
- : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}
599
- </Button>
600
- ))}
601
- </div>
580
+ <Tabs
581
+ value={imageTab}
582
+ onValueChange={(value) => setImageTab(value as 'preview' | 'resize')}
583
+ variant="underline"
584
+ >
585
+ <TabsList className="w-full flex-wrap px-3">
586
+ {(['preview', 'resize'] as const).map((tab) => (
587
+ <TabsTrigger key={tab} value={tab}>
588
+ {tab === 'preview'
589
+ ? t('attachments.library.metadata.preview', 'Preview')
590
+ : t('attachments.library.metadata.resizeTool.title', 'Generate resized URL')}
591
+ </TabsTrigger>
592
+ ))}
593
+ </TabsList>
594
+ </Tabs>
602
595
  <div className="space-y-3 p-3">
603
596
  {imageTab === 'preview' ? (
604
597
  previewUrl ? (
@@ -332,7 +332,7 @@ function AndOrToggle({
332
332
  const andLabel = t('ui.advancedFilter.combinator.and', 'And')
333
333
  const orLabel = t('ui.advancedFilter.combinator.or', 'Or')
334
334
  const baseBtn = 'h-8 px-3 text-sm font-medium outline-none focus-visible:shadow-focus disabled:cursor-not-allowed'
335
- const selBtn = 'bg-brand-violet text-white hover:bg-brand-violet/90'
335
+ const selBtn = 'bg-brand-violet text-brand-violet-foreground hover:bg-brand-violet/90'
336
336
  const unselBtn = 'bg-background text-foreground hover:bg-accent'
337
337
  return (
338
338
  <div
@@ -86,7 +86,8 @@ describe('Drawer', () => {
86
86
  const content = document.querySelector('[data-slot="drawer-content"]') as HTMLElement
87
87
  expect(content.className).toContain('right-0')
88
88
  expect(content.className).toContain('inset-y-0')
89
- expect(content.className).toContain('max-w-md')
89
+ // Figma Drawer [1.1] width contract: 400px
90
+ expect(content.className).toContain('max-w-[400px]')
90
91
  // Per Figma: inner edge rounded, no panel border on the seam.
91
92
  expect(content.className).toContain('rounded-l-xl')
92
93
  expect(content.className).not.toMatch(/\bborder-l\b/)
@@ -94,10 +94,11 @@ const drawerContentVariants = cva(
94
94
  variants: {
95
95
  side: {
96
96
  right:
97
- 'inset-y-0 right-0 h-full w-full max-w-md rounded-l-xl ' +
97
+ // Figma Drawer [1.1] width: 400px (not the Tailwind max-w-md 448px)
98
+ 'inset-y-0 right-0 h-full w-full max-w-[400px] rounded-l-xl ' +
98
99
  'data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right',
99
100
  left:
100
- 'inset-y-0 left-0 h-full w-full max-w-md rounded-r-xl ' +
101
+ 'inset-y-0 left-0 h-full w-full max-w-[400px] rounded-r-xl ' +
101
102
  'data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left',
102
103
  top:
103
104
  'inset-x-0 top-0 max-h-[80vh] w-full rounded-b-xl ' +
@@ -303,7 +304,7 @@ const DrawerTitle = React.forwardRef<
303
304
  <DialogPrimitive.Title
304
305
  ref={ref}
305
306
  data-slot="drawer-title"
306
- className={cn('text-base font-semibold leading-tight text-foreground', className)}
307
+ className={cn('text-lg font-semibold leading-tight text-foreground', className)}
307
308
  {...props}
308
309
  />
309
310
  ))
@@ -108,9 +108,13 @@ export function Tabs({
108
108
  export type TabsListProps = {
109
109
  children: React.ReactNode
110
110
  className?: string
111
+ /** Accessible name for the `role="tablist"` element. Set it when the
112
+ * surrounding context does not already label the strip (e.g. a bare
113
+ * detail-page section switcher). */
114
+ 'aria-label'?: string
111
115
  }
112
116
 
113
- export function TabsList({ children, className }: TabsListProps) {
117
+ export function TabsList({ children, className, 'aria-label': ariaLabel }: TabsListProps) {
114
118
  const { variant, orientation } = useTabsContext()
115
119
 
116
120
  const baseClasses =
@@ -130,6 +134,7 @@ export function TabsList({ children, className }: TabsListProps) {
130
134
  data-slot="tabs-list"
131
135
  className={cn(baseClasses, className)}
132
136
  role="tablist"
137
+ aria-label={ariaLabel}
133
138
  aria-orientation={orientation}
134
139
  >
135
140
  {children}