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.
- package/README.md +2 -2
- package/apps/storybook/stories/catalog/app-frame.stories.tsx +43 -0
- package/apps/storybook/stories/catalog/button.stories.tsx +1 -0
- package/apps/storybook/stories/catalog/checkbox.stories.tsx +7 -2
- package/apps/storybook/stories/catalog/data-table.stories.tsx +226 -3
- package/apps/storybook/stories/catalog/sidebar-navigation.stories.tsx +22 -0
- package/docs/design-system/brand-theme.md +3 -2
- package/docs/design-system/components.md +45 -0
- package/docs/design-system/inventory.md +1 -0
- package/docs/design-system/surface-policy.md +11 -8
- package/docs/design-system/visual-qa.md +6 -0
- package/package.json +1 -1
- package/packages/data-ui/package.json +6 -1
- package/packages/data-ui/src/data-table.tsx +2 -99
- package/packages/data-ui/src/pagination.tsx +2 -2
- package/packages/data-ui/src/table/adapters/tanstack.test.ts +116 -0
- package/packages/data-ui/src/table/adapters/tanstack.ts +179 -0
- package/packages/data-ui/src/table/contracts.ts +133 -0
- package/packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx +157 -0
- package/packages/data-ui/src/table/data-table.browser.test.tsx +180 -0
- package/packages/data-ui/src/table/data-table.tsx +524 -0
- package/packages/design-tokens/src/tokens.css +16 -10
- package/packages/design-tokens/src/tokens.json +12 -8
- package/packages/design-tokens/src/tokens.mjs +12 -8
- package/packages/registry/catalog-index.json +2 -2
- package/packages/registry/manifest.json +60 -18
- package/packages/registry/r/activity-feed.json +1 -1
- package/packages/registry/r/agent-composer.json +7 -1
- package/packages/registry/r/agent-message.json +1 -1
- package/packages/registry/r/agent-plan.json +1 -1
- package/packages/registry/r/agent-workspace.json +1 -1
- package/packages/registry/r/app-frame.json +12 -4
- package/packages/registry/r/approval-request.json +7 -1
- package/packages/registry/r/button.json +1 -1
- package/packages/registry/r/calendar.json +6 -5
- package/packages/registry/r/chart-frame.json +1 -1
- package/packages/registry/r/chart.json +1 -1
- package/packages/registry/r/dashboard-01.json +22 -2
- package/packages/registry/r/dashboard-02.json +22 -2
- package/packages/registry/r/dashboard-shell.json +1 -1
- package/packages/registry/r/data-table.json +22 -2
- package/packages/registry/r/date-picker.json +6 -5
- package/packages/registry/r/filter-bar.json +1 -1
- package/packages/registry/r/header-navigation.json +3 -3
- package/packages/registry/r/inline-cta.json +6 -6
- package/packages/registry/r/metric-card.json +1 -1
- package/packages/registry/r/pagination.json +7 -5
- package/packages/registry/r/run-inspector.json +1 -1
- package/packages/registry/r/sidebar-navigation.json +8 -3
- package/packages/registry/r/tool-call.json +1 -1
- package/packages/registry/r/workflow-canvas.json +1 -1
- package/packages/registry/r/workflow-node.json +1 -1
- package/packages/registry/registry.json +12 -3
- package/packages/registry/search-index.json +23 -8
- package/packages/ui/package.json +5 -1
- package/packages/ui/src/app-frame.browser.test.tsx +47 -0
- package/packages/ui/src/app-frame.tsx +33 -6
- package/packages/ui/src/button.browser.test.tsx +8 -0
- package/packages/ui/src/button.tsx +1 -1
- package/packages/ui/src/checkbox.tsx +1 -1
- package/packages/ui/src/header-navigation.tsx +1 -1
- package/packages/ui/src/sidebar-navigation.tsx +13 -2
- package/scripts/generate-manifest.mjs +71 -0
- package/tests/tokens.test.mjs +7 -13
- 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.
|
|
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.
|
|
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/
|
|
9
|
+
"path": "packages/icons/src/iconsax.tsx",
|
|
10
10
|
"type": "registry:component",
|
|
11
|
-
"target": "packages/icons/src/
|
|
11
|
+
"target": "packages/icons/src/iconsax.tsx"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"path": "packages/icons/src/
|
|
14
|
+
"path": "packages/icons/src/index.tsx",
|
|
15
15
|
"type": "registry:component",
|
|
16
|
-
"target": "packages/icons/src/
|
|
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",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"target": "packages/data-ui/src/pagination.tsx"
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
"path": "packages/icons/src/
|
|
14
|
+
"path": "packages/icons/src/iconsax.tsx",
|
|
15
15
|
"type": "registry:component",
|
|
16
|
-
"target": "packages/icons/src/
|
|
16
|
+
"target": "packages/icons/src/iconsax.tsx"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
"path": "packages/icons/src/
|
|
19
|
+
"path": "packages/icons/src/index.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
|
-
"target": "packages/icons/src/
|
|
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",
|
|
@@ -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.
|
|
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": [
|
|
41
|
-
|
|
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
|
}
|
|
@@ -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", "
|
|
140
|
-
"searchTerms": [
|
|
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-
|
|
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": "
|
|
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", "
|
|
388
|
-
"searchTerms": [
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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"
|
package/packages/ui/package.json
CHANGED
|
@@ -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(
|
|
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
|
|
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="
|
|
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=
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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
|
|
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-
|
|
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')
|
package/tests/tokens.test.mjs
CHANGED
|
@@ -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('
|
|
27
|
-
assert.
|
|
28
|
-
assert.
|
|
29
|
-
assert.
|
|
30
|
-
assert.
|
|
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
|
-
|
|
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
|
},
|