noecosystem-design 0.2.2 → 0.3.0

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.
Files changed (65) hide show
  1. package/README.md +2 -2
  2. package/apps/storybook/stories/catalog/app-frame.stories.tsx +43 -0
  3. package/apps/storybook/stories/catalog/button.stories.tsx +1 -0
  4. package/apps/storybook/stories/catalog/checkbox.stories.tsx +7 -2
  5. package/apps/storybook/stories/catalog/data-table.stories.tsx +226 -3
  6. package/apps/storybook/stories/catalog/sidebar-navigation.stories.tsx +22 -0
  7. package/docs/design-system/brand-theme.md +3 -2
  8. package/docs/design-system/components.md +45 -0
  9. package/docs/design-system/inventory.md +1 -0
  10. package/docs/design-system/surface-policy.md +11 -8
  11. package/docs/design-system/visual-qa.md +6 -0
  12. package/package.json +1 -1
  13. package/packages/data-ui/package.json +6 -1
  14. package/packages/data-ui/src/data-table.tsx +2 -99
  15. package/packages/data-ui/src/pagination.tsx +2 -2
  16. package/packages/data-ui/src/table/adapters/tanstack.test.ts +116 -0
  17. package/packages/data-ui/src/table/adapters/tanstack.ts +179 -0
  18. package/packages/data-ui/src/table/contracts.ts +133 -0
  19. package/packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx +157 -0
  20. package/packages/data-ui/src/table/data-table.browser.test.tsx +180 -0
  21. package/packages/data-ui/src/table/data-table.tsx +524 -0
  22. package/packages/design-tokens/src/tokens.css +16 -10
  23. package/packages/design-tokens/src/tokens.json +12 -8
  24. package/packages/design-tokens/src/tokens.mjs +12 -8
  25. package/packages/registry/catalog-index.json +2 -2
  26. package/packages/registry/manifest.json +60 -18
  27. package/packages/registry/r/activity-feed.json +1 -1
  28. package/packages/registry/r/agent-composer.json +7 -1
  29. package/packages/registry/r/agent-message.json +1 -1
  30. package/packages/registry/r/agent-plan.json +1 -1
  31. package/packages/registry/r/agent-workspace.json +1 -1
  32. package/packages/registry/r/app-frame.json +12 -4
  33. package/packages/registry/r/approval-request.json +7 -1
  34. package/packages/registry/r/button.json +1 -1
  35. package/packages/registry/r/calendar.json +6 -5
  36. package/packages/registry/r/chart-frame.json +1 -1
  37. package/packages/registry/r/chart.json +1 -1
  38. package/packages/registry/r/dashboard-01.json +22 -2
  39. package/packages/registry/r/dashboard-02.json +22 -2
  40. package/packages/registry/r/dashboard-shell.json +1 -1
  41. package/packages/registry/r/data-table.json +22 -2
  42. package/packages/registry/r/date-picker.json +6 -5
  43. package/packages/registry/r/filter-bar.json +1 -1
  44. package/packages/registry/r/header-navigation.json +3 -3
  45. package/packages/registry/r/inline-cta.json +6 -6
  46. package/packages/registry/r/metric-card.json +1 -1
  47. package/packages/registry/r/pagination.json +7 -5
  48. package/packages/registry/r/run-inspector.json +1 -1
  49. package/packages/registry/r/sidebar-navigation.json +8 -3
  50. package/packages/registry/r/tool-call.json +1 -1
  51. package/packages/registry/r/workflow-canvas.json +1 -1
  52. package/packages/registry/r/workflow-node.json +1 -1
  53. package/packages/registry/registry.json +12 -3
  54. package/packages/registry/search-index.json +23 -8
  55. package/packages/ui/package.json +5 -1
  56. package/packages/ui/src/app-frame.browser.test.tsx +47 -0
  57. package/packages/ui/src/app-frame.tsx +33 -6
  58. package/packages/ui/src/button.browser.test.tsx +8 -0
  59. package/packages/ui/src/button.tsx +1 -1
  60. package/packages/ui/src/checkbox.tsx +1 -1
  61. package/packages/ui/src/header-navigation.tsx +1 -1
  62. package/packages/ui/src/sidebar-navigation.tsx +13 -2
  63. package/scripts/generate-manifest.mjs +71 -0
  64. package/tests/tokens.test.mjs +7 -13
  65. package/vitest.config.ts +5 -0
@@ -3,7 +3,7 @@
3
3
  "name": "header-navigation",
4
4
  "type": "registry:block",
5
5
  "title": "Header Navigation",
6
- "description": "Header Navigation — Patterns / Application component. header-navigation provides patterns / application behavior with RTL, i18n and accessibility built-in.",
6
+ "description": "Header Navigation — Patterns / Application component. Provides a compact 56px application header with RTL, i18n and accessibility built in.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/header-navigation.tsx",
@@ -37,8 +37,8 @@
37
37
  "announcements": [],
38
38
  "focusBehavior": ["visible ring 2px"]
39
39
  },
40
- "tokenContracts": ["color.foreground", "color.border"],
41
- "capabilityAliases": ["header navigation", "header-navigation"],
40
+ "tokenContracts": ["color.foreground", "color.surfaceRaised", "control.height.md"],
41
+ "capabilityAliases": ["header navigation", "header-navigation", "compact app header"],
42
42
  "useWhen": ["using header navigation in Patterns / Application"],
43
43
  "doNotUseWhen": ["when header navigation semantics do not match"]
44
44
  }
@@ -6,14 +6,14 @@
6
6
  "description": "Inline call to action link with trailing logical chevron, tokenized, for use within prose.",
7
7
  "files": [
8
8
  {
9
- "path": "packages/icons/src/index.tsx",
9
+ "path": "packages/icons/src/iconsax.tsx",
10
10
  "type": "registry:component",
11
- "target": "packages/icons/src/index.tsx"
11
+ "target": "packages/icons/src/iconsax.tsx"
12
12
  },
13
13
  {
14
- "path": "packages/icons/src/iconsax.tsx",
14
+ "path": "packages/icons/src/index.tsx",
15
15
  "type": "registry:component",
16
- "target": "packages/icons/src/iconsax.tsx"
16
+ "target": "packages/icons/src/index.tsx"
17
17
  },
18
18
  {
19
19
  "path": "packages/ui/src/inline-cta.tsx",
@@ -26,8 +26,8 @@
26
26
  "target": "packages/ui/src/lib/cn.ts"
27
27
  }
28
28
  ],
29
- "registryDependencies": [],
30
- "dependencies": ["clsx", "iconsax-react", "tailwind-merge"],
29
+ "registryDependencies": ["icon-system"],
30
+ "dependencies": ["@noe/icons", "clsx", "iconsax-react", "tailwind-merge"],
31
31
  "status": "available",
32
32
  "maturity": "beta",
33
33
  "package": "@noe/ui",
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ],
19
19
  "registryDependencies": [],
20
- "dependencies": ["clsx", "tailwind-merge"],
20
+ "dependencies": ["@noe/ui", "clsx", "tailwind-merge"],
21
21
  "status": "available",
22
22
  "maturity": "beta",
23
23
  "package": "@noe/data-ui",
@@ -11,14 +11,14 @@
11
11
  "target": "packages/data-ui/src/pagination.tsx"
12
12
  },
13
13
  {
14
- "path": "packages/icons/src/index.tsx",
14
+ "path": "packages/icons/src/iconsax.tsx",
15
15
  "type": "registry:component",
16
- "target": "packages/icons/src/index.tsx"
16
+ "target": "packages/icons/src/iconsax.tsx"
17
17
  },
18
18
  {
19
- "path": "packages/icons/src/iconsax.tsx",
19
+ "path": "packages/icons/src/index.tsx",
20
20
  "type": "registry:component",
21
- "target": "packages/icons/src/iconsax.tsx"
21
+ "target": "packages/icons/src/index.tsx"
22
22
  },
23
23
  {
24
24
  "path": "packages/ui/src/button.tsx",
@@ -36,9 +36,11 @@
36
36
  "target": "packages/ui/src/spinner.tsx"
37
37
  }
38
38
  ],
39
- "registryDependencies": [],
39
+ "registryDependencies": ["icon-system"],
40
40
  "dependencies": [
41
41
  "@base-ui/react",
42
+ "@noe/icons",
43
+ "@noe/ui",
42
44
  "class-variance-authority",
43
45
  "clsx",
44
46
  "iconsax-react",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "registryDependencies": [],
25
- "dependencies": ["clsx", "tailwind-merge"],
25
+ "dependencies": ["@noe/ui", "clsx", "tailwind-merge"],
26
26
  "status": "available",
27
27
  "maturity": "beta",
28
28
  "package": "@noe/ai-ui",
@@ -3,7 +3,7 @@
3
3
  "name": "sidebar-navigation",
4
4
  "type": "registry:block",
5
5
  "title": "Sidebar Navigation",
6
- "description": "Sidebar Navigation — Patterns / Application component. sidebar-navigation provides patterns / application behavior with RTL, i18n and accessibility built-in.",
6
+ "description": "Sidebar Navigation — Patterns / Application component. Uses logical icon, label and end-adornment order without row reversal; RTL, i18n and accessibility are built in.",
7
7
  "files": [
8
8
  {
9
9
  "path": "packages/ui/src/lib/cn.ts",
@@ -37,8 +37,13 @@
37
37
  "announcements": [],
38
38
  "focusBehavior": ["visible ring 2px"]
39
39
  },
40
- "tokenContracts": ["color.foreground", "color.border"],
41
- "capabilityAliases": ["sidebar navigation", "sidebar-navigation"],
40
+ "tokenContracts": [
41
+ "color.foreground",
42
+ "color.surface",
43
+ "color.surfaceRaised",
44
+ "color.mutedForeground"
45
+ ],
46
+ "capabilityAliases": ["sidebar navigation", "sidebar-navigation", "finance sidebar"],
42
47
  "useWhen": ["using sidebar navigation in Patterns / Application"],
43
48
  "doNotUseWhen": ["when sidebar navigation semantics do not match"]
44
49
  }
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "registryDependencies": [],
25
- "dependencies": ["clsx", "tailwind-merge"],
25
+ "dependencies": ["@noe/ui", "clsx", "tailwind-merge"],
26
26
  "status": "available",
27
27
  "maturity": "beta",
28
28
  "package": "@noe/ai-ui",
@@ -17,7 +17,7 @@
17
17
  }
18
18
  ],
19
19
  "registryDependencies": [],
20
- "dependencies": ["clsx", "tailwind-merge"],
20
+ "dependencies": ["@noe/ui", "clsx", "tailwind-merge"],
21
21
  "status": "available",
22
22
  "maturity": "beta",
23
23
  "package": "@noe/workflow-ui",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "registryDependencies": [],
25
- "dependencies": ["clsx", "tailwind-merge"],
25
+ "dependencies": ["@noe/ui", "clsx", "tailwind-merge"],
26
26
  "status": "available",
27
27
  "maturity": "beta",
28
28
  "package": "@noe/workflow-ui",
@@ -133,11 +133,20 @@
133
133
  "target": "packages/data-ui/src/data-table.tsx"
134
134
  }
135
135
  ],
136
- "dependencies": ["@noe/ui"],
136
+ "dependencies": ["@noe/ui", "@tanstack/react-table"],
137
137
  "peerDependencies": ["react"],
138
138
  "manifestId": "data-table",
139
- "capabilityAliases": ["data table", "data-table"],
140
- "searchTerms": ["data-table", "data table", "data"]
139
+ "capabilityAliases": ["data table", "sortable table", "server-side table", "grid"],
140
+ "searchTerms": [
141
+ "data-table",
142
+ "table",
143
+ "sorting",
144
+ "filtering",
145
+ "pagination",
146
+ "selection",
147
+ "server-side",
148
+ "aria-sort"
149
+ ]
141
150
  },
142
151
  {
143
152
  "name": "metric-card",
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-08-29T09:30:43.933Z",
2
+ "generatedAt": "2026-08-30T12:09:40.536Z",
3
3
  "items": [
4
4
  {
5
5
  "id": "bidi-text",
@@ -381,15 +381,30 @@
381
381
  "id": "data-table",
382
382
  "package": "@noe/data-ui",
383
383
  "title": "Data Table",
384
- "description": "Data Table Data component. data-table provides data behavior with RTL, i18n and accessibility built-in.",
384
+ "description": "Semantic data table with TanStack Table v9 as a headless engine behind the internal NOE adapter: sorting, NOE filter operators, controlled pagination, stable-id selection, loading skeletons and server mode.",
385
385
  "category": "Data",
386
386
  "status": "available",
387
- "capabilityAliases": ["data table", "data-table"],
388
- "searchTerms": ["data-table", "data table", "data"],
389
- "useWhen": ["using data table in Data"],
390
- "doNotUseWhen": ["when data table semantics do not match"],
391
- "alternatives": [],
392
- "relatedPatterns": [],
387
+ "capabilityAliases": ["data table", "sortable table", "server-side table", "grid"],
388
+ "searchTerms": [
389
+ "data-table",
390
+ "table",
391
+ "sorting",
392
+ "filtering",
393
+ "pagination",
394
+ "selection",
395
+ "server-side",
396
+ "aria-sort"
397
+ ],
398
+ "useWhen": [
399
+ "tabular product data with sorting, filtering, pagination or row selection",
400
+ "server-driven pages composed from toDataQuery against Django APIs"
401
+ ],
402
+ "doNotUseWhen": [
403
+ "spreadsheet-style cell editing grids",
404
+ "static lists without tabular semantics"
405
+ ],
406
+ "alternatives": ["DataList"],
407
+ "relatedPatterns": ["filter-bar", "pagination", "empty-state"],
393
408
  "exportName": "DataTable",
394
409
  "registryItem": "data-table",
395
410
  "maturity": "beta"
@@ -24,6 +24,10 @@
24
24
  ".": "./src/index.ts",
25
25
  "./bidi": "./src/bidi.tsx",
26
26
  "./button": "./src/button.tsx",
27
- "./lib/cn": "./src/lib/cn.ts"
27
+ "./lib/cn": "./src/lib/cn.ts",
28
+ "./checkbox": "./src/checkbox.tsx",
29
+ "./skeleton": "./src/skeleton.tsx",
30
+ "./empty-state": "./src/empty-state.tsx",
31
+ "./input": "./src/input.tsx"
28
32
  }
29
33
  }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from 'vitest';
2
+ import { render } from 'vitest-browser-react';
3
+
4
+ import { DirectionProvider } from './direction';
5
+ import { AppFrame } from './app-frame';
6
+ import { SidebarNavigation } from './sidebar-navigation';
7
+
8
+ test('finance app frame keeps a flat, logical RTL sidebar structure', async () => {
9
+ const screen = await render(
10
+ <DirectionProvider direction="rtl">
11
+ <AppFrame
12
+ contentId="finance-main"
13
+ data-testid="finance-app-frame"
14
+ navigation={
15
+ <SidebarNavigation
16
+ label="ناوبری"
17
+ sections={[
18
+ {
19
+ id: 'finance',
20
+ items: [
21
+ {
22
+ href: '#dashboard',
23
+ icon: <svg data-testid="dashboard-icon" />,
24
+ label: 'داشبورد',
25
+ current: true,
26
+ },
27
+ ],
28
+ },
29
+ ]}
30
+ />
31
+ }
32
+ variant="finance"
33
+ >
34
+ <p>محتوا</p>
35
+ </AppFrame>
36
+ </DirectionProvider>,
37
+ );
38
+
39
+ const frame = screen.getByTestId('finance-app-frame');
40
+ await expect.element(frame).toHaveAttribute('data-variant', 'finance');
41
+ await expect.element(screen.getByRole('navigation', { name: 'ناوبری' })).toBeVisible();
42
+ await expect
43
+ .element(screen.getByRole('link', { name: 'داشبورد' }))
44
+ .toHaveAttribute('aria-current', 'page');
45
+ await expect.element(screen.getByTestId('dashboard-icon')).toBeVisible();
46
+ await expect.element(screen.getByRole('main')).toHaveAttribute('id', 'finance-main');
47
+ });
@@ -1,36 +1,55 @@
1
+ import { useId } from 'react';
1
2
  import type * as React from 'react';
2
3
 
3
4
  import { cn } from './lib/cn';
4
5
 
5
6
  export interface AppFrameProps extends React.ComponentProps<'div'> {
7
+ /** `finance` is the compact, flat shell for dense business applications. */
8
+ variant?: 'default' | 'finance';
6
9
  header?: React.ReactNode;
7
10
  navigation?: React.ReactNode;
8
11
  mobileNavigation?: React.ReactNode;
9
12
  inspector?: React.ReactNode;
13
+ contentId?: string;
10
14
  children: React.ReactNode;
11
15
  }
12
16
 
13
17
  export function AppFrame({
18
+ variant = 'default',
14
19
  header,
15
20
  navigation,
16
21
  mobileNavigation,
17
22
  inspector,
23
+ contentId,
18
24
  children,
19
25
  className,
20
26
  ...props
21
27
  }: AppFrameProps) {
28
+ const isFinanceShell = variant === 'finance';
29
+ const generatedContentId = useId();
22
30
  return (
23
31
  <div
24
- className={cn('grid min-h-dvh w-full grid-rows-[auto_1fr]', className)}
32
+ className={cn(
33
+ 'grid min-h-dvh w-full grid-rows-[auto_1fr] bg-background font-sans text-foreground',
34
+ className,
35
+ )}
25
36
  data-slot="app-frame"
37
+ data-variant={variant}
26
38
  {...props}
27
39
  >
28
40
  {header ? <div data-slot="app-frame-header">{header}</div> : null}
29
- <div className="grid lg:grid-cols-[15rem_minmax(0,1fr)] xl:grid-cols-[15rem_minmax(0,1fr)_18rem]">
41
+ <div
42
+ className={cn(
43
+ 'grid',
44
+ isFinanceShell
45
+ ? 'lg:grid-cols-[17.5rem_minmax(0,1fr)] xl:grid-cols-[17.5rem_minmax(0,1fr)_18rem]'
46
+ : 'lg:grid-cols-[15rem_minmax(0,1fr)] xl:grid-cols-[15rem_minmax(0,1fr)_18rem]',
47
+ )}
48
+ >
30
49
  {mobileNavigation ? (
31
50
  <nav
32
51
  aria-label="Primary"
33
- className="border-b border-border bg-surface-raised p-4 lg:hidden"
52
+ className="bg-surface p-4 lg:hidden"
34
53
  data-slot="app-frame-mobile-navigation"
35
54
  >
36
55
  {mobileNavigation}
@@ -39,19 +58,27 @@ export function AppFrame({
39
58
  {navigation ? (
40
59
  <aside
41
60
  aria-label="Primary"
42
- className="hidden border-e border-border bg-surface-raised p-4 lg:block"
61
+ className={cn(
62
+ 'hidden p-4 lg:block',
63
+ isFinanceShell ? 'bg-surface-sunken px-3 py-4' : 'bg-surface-raised',
64
+ )}
43
65
  data-slot="app-frame-navigation"
44
66
  >
45
67
  {navigation}
46
68
  </aside>
47
69
  ) : null}
48
- <main className="min-w-0 p-4 sm:p-6" data-slot="app-frame-content">
70
+ <main
71
+ className={cn('min-w-0 bg-background p-4 sm:p-6', isFinanceShell && 'sm:p-5')}
72
+ data-slot="app-frame-content"
73
+ id={contentId ?? generatedContentId}
74
+ tabIndex={-1}
75
+ >
49
76
  {children}
50
77
  </main>
51
78
  {inspector ? (
52
79
  <aside
53
80
  aria-label="Inspector"
54
- className="hidden border-s border-border bg-surface-raised p-5 xl:block"
81
+ className="hidden bg-surface p-5 xl:block"
55
82
  data-slot="app-frame-inspector"
56
83
  >
57
84
  {inspector}
@@ -36,3 +36,11 @@ test('a disabled polymorphic Button cannot navigate or receive focus', async ()
36
36
  expect(onClick).not.toHaveBeenCalled();
37
37
  expect(window.location.hash).not.toBe('#danger');
38
38
  });
39
+
40
+ test('secondary Button remains a transparent currentColor outline', async () => {
41
+ const screen = await render(<Button variant="secondary">Reject</Button>);
42
+ const button = screen.getByRole('button', { name: 'Reject' });
43
+ await expect.element(button).toHaveClass('border-current');
44
+ await expect.element(button).toHaveClass('bg-transparent');
45
+ await expect.element(button).toHaveClass('text-foreground');
46
+ });
@@ -12,7 +12,7 @@ export const buttonVariants = cva(
12
12
  variants: {
13
13
  variant: {
14
14
  primary: 'border-accent bg-accent text-accent-foreground hover:bg-accent-hover',
15
- secondary: 'border-border bg-surface-raised text-foreground hover:bg-muted',
15
+ secondary: 'border-current bg-transparent text-foreground hover:bg-muted/70',
16
16
  outline: 'border-border bg-transparent text-foreground hover:bg-muted',
17
17
  ghost: 'border-transparent bg-transparent text-foreground hover:bg-muted',
18
18
  quiet: 'border-transparent bg-transparent text-foreground hover:bg-muted',
@@ -11,7 +11,7 @@ export function Checkbox({ className, label, ...props }: CheckboxProps) {
11
11
  className="size-4 rounded-sm border border-border bg-background focus-visible:outline-2 focus-visible:outline-focus-ring"
12
12
  {...props}
13
13
  />
14
- <span>{label ?? 'Checkbox'}</span>
14
+ <span>{label}</span>
15
15
  </label>
16
16
  );
17
17
  }
@@ -29,7 +29,7 @@ export function HeaderNavigation({
29
29
  return (
30
30
  <header
31
31
  className={cn(
32
- 'flex w-full items-center gap-x-6 gap-y-2 bg-surface-raised px-4 py-3',
32
+ 'flex min-h-14 w-full items-center gap-x-4 gap-y-2 bg-surface-raised px-4 py-2',
33
33
  className,
34
34
  )}
35
35
  data-slot="header-navigation"
@@ -6,8 +6,11 @@ export interface SidebarNavigationItem {
6
6
  id?: string;
7
7
  href: string;
8
8
  label: React.ReactNode;
9
+ /** Rendered at the logical start; follows document direction without row reversal. */
10
+ icon?: React.ReactNode;
9
11
  current?: boolean;
10
12
  badge?: React.ReactNode;
13
+ endAdornment?: React.ReactNode;
11
14
  }
12
15
 
13
16
  export interface SidebarNavigationSection {
@@ -53,16 +56,24 @@ export function SidebarNavigation({
53
56
  <a
54
57
  aria-current={item.current ? 'page' : undefined}
55
58
  className={cn(
56
- 'flex items-center gap-2 rounded-md px-3 py-2 text-sm leading-normal no-underline hover:bg-surface-sunken/60',
59
+ 'flex min-h-11 items-center gap-2 rounded-md px-3 py-2 text-sm leading-normal no-underline hover:bg-surface/80',
57
60
  item.current
58
- ? 'bg-surface-sunken font-medium text-foreground'
61
+ ? 'bg-surface-raised font-medium text-foreground'
59
62
  : 'text-muted-foreground',
60
63
  )}
61
64
  data-current={item.current || undefined}
62
65
  data-slot="sidebar-navigation-item"
63
66
  href={item.href}
64
67
  >
68
+ {item.icon ? (
69
+ <span aria-hidden="true" className="shrink-0 text-current [&_svg]:size-5">
70
+ {item.icon}
71
+ </span>
72
+ ) : null}
65
73
  <span className="min-w-0 flex-1 truncate">{item.label}</span>
74
+ {item.endAdornment ? (
75
+ <span className="ms-auto shrink-0 text-current">{item.endAdornment}</span>
76
+ ) : null}
66
77
  {item.badge ? (
67
78
  <span className="ms-auto shrink-0 text-xs tabular-nums">{item.badge}</span>
68
79
  ) : null}
@@ -119,6 +119,76 @@ function toTitle(id) {
119
119
 
120
120
  // Enrichment database — semantic, agent-friendly metadata per spec
121
121
  const enrichments = {
122
+ 'data-table': {
123
+ description:
124
+ 'Semantic data table with TanStack Table v9 as a headless engine behind the internal NOE adapter: sorting, NOE filter operators, controlled pagination, stable-id selection, loading skeletons and server mode.',
125
+ capabilityAliases: ['data table', 'sortable table', 'server-side table', 'grid'],
126
+ searchTerms: [
127
+ 'data-table',
128
+ 'table',
129
+ 'sorting',
130
+ 'filtering',
131
+ 'pagination',
132
+ 'selection',
133
+ 'server-side',
134
+ 'aria-sort',
135
+ ],
136
+ useWhen: [
137
+ 'tabular product data with sorting, filtering, pagination or row selection',
138
+ 'server-driven pages composed from toDataQuery against Django APIs',
139
+ ],
140
+ doNotUseWhen: [
141
+ 'spreadsheet-style cell editing grids',
142
+ 'static lists without tabular semantics',
143
+ ],
144
+ alternatives: ['DataList'],
145
+ variants: ['regular', 'compact'],
146
+ states: ['default', 'hover', 'loading', 'selected', 'empty', 'focus-visible'],
147
+ tokenContracts: [
148
+ 'color.border',
149
+ 'color.foreground',
150
+ 'color.muted-foreground',
151
+ 'surface.sunken',
152
+ 'muted',
153
+ ],
154
+ propsSummary: {
155
+ columns: 'DataColumn<Row>[]',
156
+ rows: 'Row[]',
157
+ caption: 'string',
158
+ empty: 'ReactNode',
159
+ sorting: 'readonly DataSort[]',
160
+ filters: 'readonly FilterSpec[]',
161
+ page: 'PageSpec',
162
+ selection: 'readonly RowId[]',
163
+ density: "'compact' | 'regular'",
164
+ loading: 'boolean',
165
+ manualSorting: 'boolean',
166
+ manualFiltering: 'boolean',
167
+ manualPagination: 'boolean',
168
+ totalRowCount: 'number',
169
+ },
170
+ accessibility: {
171
+ role: 'table',
172
+ keyboard: [
173
+ 'Tab moves into the scrollable region',
174
+ 'Enter/Space on header toggles sort',
175
+ 'Enter/Space on checkbox toggles row selection',
176
+ ],
177
+ announcements: ['aria-sort on sorted column headers', 'aria-live pagination status'],
178
+ focusBehavior: ['visible ring 2px', 'scroll container focusable for keyboard overflow'],
179
+ },
180
+ internationalization: {
181
+ messageKeys: [],
182
+ rtl: 'required',
183
+ bidiNotes: [
184
+ 'logical start/end alignment only',
185
+ 'technical cells render dir=ltr via TechnicalValue',
186
+ 'pagination controls use caller-provided accessible labels',
187
+ ],
188
+ },
189
+ relatedPatterns: ['filter-bar', 'pagination', 'empty-state'],
190
+ untitledMapping: null,
191
+ },
122
192
  'bidi-text': {
123
193
  description:
124
194
  'Mixed-direction content primitives for user text, URLs, hashes, commands and technical identifiers with explicit LTR isolation.',
@@ -815,6 +885,7 @@ function defaultEnrichment(auditItem) {
815
885
  const registryDeps = [];
816
886
  const peerDeps = ['react'];
817
887
  if (pkg === '@noe/data-ui') deps.push('@noe/ui');
888
+ if (auditItem.id === 'data-table') deps.push('@tanstack/react-table');
818
889
  if (pkg === '@noe/ai-ui') deps.push('@noe/ui');
819
890
  if (pkg === '@noe/workflow-ui') deps.push('@noe/ui');
820
891
  if (auditItem.id === 'pagination' || auditItem.id === 'fab' || auditItem.id === 'filter-bar')
@@ -23,22 +23,16 @@ test('token CSS includes theme and reduced-motion contracts', async () => {
23
23
  assert.match(css, /--noe-primitive-color-mint-100:\s*#[0-9a-f]{6}/i);
24
24
  });
25
25
 
26
- test('surface elevation follows the official NOE soft-depth contract', async () => {
27
- assert.match(tokens.shadow.light.raised, /rgba\(23, 25, 24, 0\.055\)/);
28
- assert.match(tokens.shadow.light.overlay, /rgba\(23, 25, 24, 0\.28\)/);
29
- assert.match(tokens.shadow.dark.raised, /rgba\(0, 0, 0, 0\.14\)/);
30
- assert.match(tokens.shadow.dark.overlay, /rgba\(0, 0, 0, 0\.42\)/);
26
+ test('surfaces remain flat in every NOE theme', async () => {
27
+ assert.equal(tokens.shadow.light.raised, 'none');
28
+ assert.equal(tokens.shadow.light.overlay, 'none');
29
+ assert.equal(tokens.shadow.dark.raised, 'none');
30
+ assert.equal(tokens.shadow.dark.overlay, 'none');
31
31
 
32
32
  const css = await readFile(
33
33
  new URL('../packages/design-tokens/src/tokens.css', import.meta.url),
34
34
  'utf8',
35
35
  );
36
- assert.match(
37
- css,
38
- /\[data-theme="light"\][\s\S]*?--noe-shadow-raised:\s*0 8px 20px rgba\(23, 25, 24, 0\.055\)/,
39
- );
40
- assert.match(
41
- css,
42
- /\[data-theme="dark"\][\s\S]*?--noe-shadow-overlay:\s*0 36px 100px rgba\(0, 0, 0, 0\.42\)/,
43
- );
36
+ assert.match(css, /\[data-theme="light"\][\s\S]*?--noe-shadow-raised:\s*none/);
37
+ assert.match(css, /\[data-theme="dark"\][\s\S]*?--noe-shadow-overlay:\s*none/);
44
38
  });
package/vitest.config.ts CHANGED
@@ -2,6 +2,7 @@ import { dirname } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
 
4
4
  import react from '@vitejs/plugin-react';
5
+ import tailwindcss from '@tailwindcss/vite';
5
6
  import { playwright } from '@vitest/browser-playwright';
6
7
  import tsconfigPaths from 'vite-tsconfig-paths';
7
8
  import { defineConfig } from 'vitest/config';
@@ -46,6 +47,9 @@ export default defineConfig({
46
47
  },
47
48
  },
48
49
  {
50
+ // Tailwind expands `@import "tailwindcss"` so component browser tests and
51
+ // visual evidence render with the real NOE utility classes, matching Storybook.
52
+ plugins: [tailwindcss()],
49
53
  optimizeDeps: {
50
54
  entries: ['apps/**/*.browser.test.tsx', 'packages/**/*.browser.test.tsx'],
51
55
  },
@@ -83,6 +87,7 @@ export default defineConfig({
83
87
  provider: playwright(),
84
88
  headless: true,
85
89
  instances: [{ browser: 'chromium' }],
90
+ viewport: { width: 1280, height: 900 },
86
91
  },
87
92
  },
88
93
  },