noecosystem-design 0.2.3 → 0.4.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 (73) hide show
  1. package/README.md +2 -2
  2. package/apps/storybook/.storybook/preview.tsx +1 -1
  3. package/apps/storybook/stories/catalog/app-frame.stories.tsx +213 -1
  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/dialog.stories.tsx +2 -2
  7. package/apps/storybook/stories/catalog/icon-system.stories.tsx +11 -0
  8. package/apps/storybook/stories/catalog/menu.stories.tsx +3 -3
  9. package/apps/web/styles.css +0 -1
  10. package/docs/design-system/brand-theme.md +8 -0
  11. package/docs/design-system/components.md +38 -0
  12. package/docs/design-system/inventory.md +1 -0
  13. package/docs/design-system/visual-qa.md +6 -0
  14. package/package.json +1 -1
  15. package/packages/data-ui/package.json +6 -1
  16. package/packages/data-ui/src/data-table.tsx +2 -99
  17. package/packages/data-ui/src/pagination.tsx +2 -2
  18. package/packages/data-ui/src/table/adapters/tanstack.test.ts +116 -0
  19. package/packages/data-ui/src/table/adapters/tanstack.ts +179 -0
  20. package/packages/data-ui/src/table/contracts.ts +133 -0
  21. package/packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx +157 -0
  22. package/packages/data-ui/src/table/data-table.browser.test.tsx +180 -0
  23. package/packages/data-ui/src/table/data-table.tsx +524 -0
  24. package/packages/design-tokens/src/tailwind.css +5 -0
  25. package/packages/design-tokens/src/tokens.css +10 -4
  26. package/packages/design-tokens/src/tokens.json +8 -4
  27. package/packages/design-tokens/src/tokens.mjs +8 -4
  28. package/packages/icons/package.json +6 -1
  29. package/packages/icons/src/brands/nocfo.svg +8 -0
  30. package/packages/icons/src/nocfo-logo.browser.test.tsx +14 -0
  31. package/packages/icons/src/nocfo-logo.tsx +23 -0
  32. package/packages/registry/catalog-index.json +2 -2
  33. package/packages/registry/manifest.json +60 -18
  34. package/packages/registry/r/activity-feed.json +1 -1
  35. package/packages/registry/r/agent-composer.json +7 -1
  36. package/packages/registry/r/agent-message.json +1 -1
  37. package/packages/registry/r/agent-plan.json +1 -1
  38. package/packages/registry/r/agent-workspace.json +1 -1
  39. package/packages/registry/r/app-frame.json +9 -2
  40. package/packages/registry/r/approval-request.json +7 -1
  41. package/packages/registry/r/calendar.json +6 -5
  42. package/packages/registry/r/chart-frame.json +1 -1
  43. package/packages/registry/r/chart.json +1 -1
  44. package/packages/registry/r/dashboard-01.json +22 -2
  45. package/packages/registry/r/dashboard-02.json +22 -2
  46. package/packages/registry/r/dashboard-shell.json +1 -1
  47. package/packages/registry/r/data-table.json +22 -2
  48. package/packages/registry/r/date-picker.json +6 -5
  49. package/packages/registry/r/filter-bar.json +1 -1
  50. package/packages/registry/r/icon-system.json +18 -5
  51. package/packages/registry/r/inline-cta.json +6 -6
  52. package/packages/registry/r/metric-card.json +1 -1
  53. package/packages/registry/r/pagination.json +7 -5
  54. package/packages/registry/r/run-inspector.json +1 -1
  55. package/packages/registry/r/sidebar-navigation.json +5 -2
  56. package/packages/registry/r/tool-call.json +1 -1
  57. package/packages/registry/r/workflow-canvas.json +1 -1
  58. package/packages/registry/r/workflow-node.json +1 -1
  59. package/packages/registry/registry.json +12 -3
  60. package/packages/registry/search-index.json +23 -8
  61. package/packages/ui/package.json +5 -1
  62. package/packages/ui/src/app-frame.browser.test.tsx +6 -0
  63. package/packages/ui/src/app-frame.tsx +9 -2
  64. package/packages/ui/src/badge.tsx +4 -4
  65. package/packages/ui/src/calendar.tsx +1 -1
  66. package/packages/ui/src/checkbox.tsx +1 -1
  67. package/packages/ui/src/code.tsx +3 -1
  68. package/packages/ui/src/field.tsx +1 -1
  69. package/packages/ui/src/file-uploader.tsx +1 -1
  70. package/packages/ui/src/progress-steps.tsx +1 -1
  71. package/packages/ui/src/sidebar-navigation.tsx +11 -1
  72. package/scripts/generate-manifest.mjs +71 -0
  73. package/vitest.config.ts +5 -0
@@ -0,0 +1,157 @@
1
+ import type * as React from 'react';
2
+ import { expect, test } from 'vitest';
3
+ import { render } from 'vitest-browser-react';
4
+
5
+ import '../../../design-tokens/src/tailwind.css';
6
+
7
+ import type { DataColumn, DataSort } from './contracts';
8
+ import { DataTable } from './data-table';
9
+
10
+ /**
11
+ * Deterministic PNG evidence for the NOE DataTable (docs/design-system/visual-qa.md).
12
+ * Captures regular/compact density, RTL Persian, selection and loading states
13
+ * into evidence/data-table/. Pure layout evidence: no mock data leaves the repo.
14
+ *
15
+ * All variants render once; per-variant wrappers are captured individually
16
+ * because repeated render/unmount cycles invalidate the shared mount wrapper.
17
+ */
18
+
19
+ // Resolved relative to this test file by the browser screenshot driver.
20
+ const OUT = '../../../../evidence/data-table';
21
+
22
+ type Row = { id: string; service: string; owner: string; status: string; updated: string };
23
+
24
+ const columns: DataColumn<Row>[] = [
25
+ { key: 'service', header: 'Service', technical: true },
26
+ { key: 'owner', header: 'Owner' },
27
+ { key: 'status', header: 'Status' },
28
+ { key: 'updated', header: 'Updated', technical: true, align: 'end' },
29
+ ];
30
+
31
+ const rows: Row[] = [
32
+ { id: '1', service: 'catalog-api', owner: 'Platform', status: 'Healthy', updated: '2m ago' },
33
+ {
34
+ id: '2',
35
+ service: 'agent-runner',
36
+ owner: 'AI Platform',
37
+ status: 'Degraded',
38
+ updated: '11m ago',
39
+ },
40
+ {
41
+ id: '3',
42
+ service: 'registry-sync',
43
+ owner: 'Design Systems',
44
+ status: 'Healthy',
45
+ updated: '1h ago',
46
+ },
47
+ {
48
+ id: '4',
49
+ service: 'registry-api',
50
+ owner: 'Design Systems',
51
+ status: 'Healthy',
52
+ updated: '3h ago',
53
+ },
54
+ { id: '5', service: 'agent-gateway', owner: 'AI Platform', status: 'Healthy', updated: '5h ago' },
55
+ ];
56
+
57
+ const faColumns: DataColumn<Row>[] = [
58
+ { key: 'service', header: 'سرویس', technical: true },
59
+ { key: 'owner', header: 'مالک' },
60
+ { key: 'status', header: 'وضعیت' },
61
+ ];
62
+ const faSorting: DataSort[] = [{ field: 'service', direction: 'asc' }];
63
+
64
+ const captures: Array<{ name: string; ui: React.ReactElement }> = [
65
+ {
66
+ name: 'data-table-regular-en-light',
67
+ ui: (
68
+ <DataTable columns={columns} rows={rows} caption="Service health" empty="No services yet." />
69
+ ),
70
+ },
71
+ {
72
+ name: 'data-table-compact-fa-rtl',
73
+ ui: (
74
+ <div dir="rtl" lang="fa">
75
+ <DataTable
76
+ columns={faColumns}
77
+ rows={rows}
78
+ caption="وضعیت سرویس‌ها"
79
+ density="compact"
80
+ empty="هنوز سرویسی ثبت نشده است."
81
+ sorting={faSorting}
82
+ onSortingChange={() => {}}
83
+ />
84
+ </div>
85
+ ),
86
+ },
87
+ {
88
+ name: 'data-table-selection',
89
+ ui: (
90
+ <DataTable
91
+ columns={columns}
92
+ rows={rows}
93
+ caption="Service health"
94
+ empty="No services yet."
95
+ defaultSelection={['2']}
96
+ selectAllLabel="Select all services"
97
+ selectedCountLabel={(count: number) => `${count} selected`}
98
+ renderSelectionActions={() => <button type="button">Restart</button>}
99
+ />
100
+ ),
101
+ },
102
+ {
103
+ name: 'data-table-loading',
104
+ ui: (
105
+ <DataTable
106
+ columns={columns}
107
+ rows={[]}
108
+ caption="Service health"
109
+ empty="No services yet."
110
+ loading
111
+ loadingRows={4}
112
+ />
113
+ ),
114
+ },
115
+ {
116
+ name: 'data-table-selection-fa-dark-compact',
117
+ ui: (
118
+ <div
119
+ data-theme="dark"
120
+ dir="rtl"
121
+ lang="fa"
122
+ className="rounded-lg bg-background p-3 text-foreground"
123
+ >
124
+ <DataTable
125
+ columns={faColumns}
126
+ rows={rows}
127
+ caption="وضعیت سرویس‌ها"
128
+ density="compact"
129
+ empty="هنوز سرویسی ثبت نشده است."
130
+ defaultSelection={['2']}
131
+ selectAllLabel="انتخاب همه سرویس‌ها"
132
+ selectedCountLabel={(count: number) => `${count} مورد انتخاب شده`}
133
+ />
134
+ </div>
135
+ ),
136
+ },
137
+ ];
138
+
139
+ test('captures DataTable visual evidence', async () => {
140
+ const screen = await render(
141
+ <div className="flex flex-col">
142
+ {captures.map(({ name, ui }) => (
143
+ <div data-testid={`capture-${name}`} key={name}>
144
+ {ui}
145
+ </div>
146
+ ))}
147
+ </div>,
148
+ );
149
+
150
+ for (const { name } of captures) {
151
+ await screen
152
+ .getByTestId(`capture-${name}`)
153
+ .screenshot({ animations: 'disabled', caret: 'hide', path: `${OUT}/${name}.png` });
154
+ }
155
+
156
+ expect(captures).toHaveLength(5);
157
+ });
@@ -0,0 +1,180 @@
1
+ import { useState } from 'react';
2
+ import { expect, test, vi } from 'vitest';
3
+ import { render } from 'vitest-browser-react';
4
+
5
+ import type { DataColumn } from './contracts';
6
+ import { DataTable } from './data-table';
7
+
8
+ type Row = { id: string; service: string; owner: string };
9
+
10
+ const columns: DataColumn<Row>[] = [
11
+ { key: 'service', header: 'Service', technical: true },
12
+ { key: 'owner', header: 'Owner' },
13
+ ];
14
+
15
+ const rows: Row[] = [
16
+ { id: '1', service: 'catalog-api', owner: 'Platform' },
17
+ { id: '2', service: 'agent-runner', owner: 'AI Platform' },
18
+ { id: '3', service: 'registry-sync', owner: 'Design Systems' },
19
+ ];
20
+
21
+ interface ScreenLike {
22
+ getByRole(
23
+ role: 'table' | 'button' | 'checkbox' | 'columnheader',
24
+ options?: { name?: string },
25
+ ): {
26
+ element(): Element;
27
+ click(): Promise<void>;
28
+ };
29
+ getByText(text: string): { element(): Element };
30
+ }
31
+
32
+ const bodyRows = (screen: ScreenLike) =>
33
+ (screen.getByRole('table').element() as HTMLTableElement).querySelectorAll('tbody tr');
34
+
35
+ test('renders a semantic table with logical alignment metadata', async () => {
36
+ const screen = await render(
37
+ <DataTable columns={columns} rows={rows} caption="Service health" empty="No services yet." />,
38
+ );
39
+
40
+ const table = screen.getByRole('table').element() as HTMLTableElement;
41
+ expect(table.querySelector('caption')?.textContent).toBe('Service health');
42
+ const header = screen
43
+ .getByRole('columnheader', { name: 'Service' })
44
+ .element() as HTMLTableCellElement;
45
+ expect(header.getAttribute('data-align')).toBe('start');
46
+ expect(header.getAttribute('aria-sort')).toBe('none');
47
+ expect(bodyRows(screen)).toHaveLength(3);
48
+ });
49
+
50
+ test('sorting cycles ascending, descending, then unsorted with aria-sort state', async () => {
51
+ const onSortingChange = vi.fn();
52
+ const screen = await render(
53
+ <DataTable
54
+ columns={columns}
55
+ rows={rows}
56
+ caption="Service health"
57
+ empty="No services yet."
58
+ onSortingChange={onSortingChange}
59
+ />,
60
+ );
61
+
62
+ const header = () =>
63
+ screen.getByRole('columnheader', { name: 'Service' }).element() as HTMLTableCellElement;
64
+ const firstRowService = () =>
65
+ (bodyRows(screen)[0] as HTMLTableRowElement).querySelector('td')?.textContent;
66
+
67
+ await screen.getByRole('button', { name: 'Service' }).click();
68
+ expect(header().getAttribute('aria-sort')).toBe('ascending');
69
+ expect(firstRowService()).toBe('agent-runner');
70
+ expect(onSortingChange).toHaveBeenLastCalledWith([{ field: 'service', direction: 'asc' }]);
71
+
72
+ await screen.getByRole('button', { name: 'Service' }).click();
73
+ expect(header().getAttribute('aria-sort')).toBe('descending');
74
+ expect(firstRowService()).toBe('registry-sync');
75
+ expect(onSortingChange).toHaveBeenLastCalledWith([{ field: 'service', direction: 'desc' }]);
76
+
77
+ await screen.getByRole('button', { name: 'Service' }).click();
78
+ expect(header().getAttribute('aria-sort')).toBe('none');
79
+ expect(firstRowService()).toBe('catalog-api');
80
+ });
81
+
82
+ test('selection keeps stable row ids and reflects indeterminate state', async () => {
83
+ const onSelectionChange = vi.fn();
84
+ const screen = await render(
85
+ <DataTable
86
+ columns={columns}
87
+ rows={rows}
88
+ caption="Service health"
89
+ empty="No services yet."
90
+ defaultSelection={['2']}
91
+ onSelectionChange={onSelectionChange}
92
+ selectAllLabel="Select all rows"
93
+ rowSelectionLabel={(row: Row) => `Select ${row.service}`}
94
+ selectedCountLabel={(count: number) => `${count} selected`}
95
+ />,
96
+ );
97
+
98
+ await screen.getByRole('checkbox', { name: 'Select catalog-api' }).click();
99
+ expect(onSelectionChange).toHaveBeenLastCalledWith(['1', '2']);
100
+ expect(
101
+ screen.getByRole('checkbox', { name: 'Select all rows' }).element() as HTMLInputElement,
102
+ ).toBePartiallyChecked();
103
+
104
+ await screen.getByRole('checkbox', { name: 'Select all rows' }).click();
105
+ expect(onSelectionChange).toHaveBeenLastCalledWith(['1', '2', '3']);
106
+ expect(screen.getByText('3 selected').element()).toBeTruthy();
107
+ });
108
+
109
+ test('client-side pagination slices rows and reports status', async () => {
110
+ function PaginationHost() {
111
+ const [page, setPage] = useState({ index: 0, size: 2 });
112
+ return (
113
+ <DataTable
114
+ columns={columns}
115
+ rows={rows}
116
+ caption="Service health"
117
+ empty="No services yet."
118
+ page={page}
119
+ onPageChange={setPage}
120
+ previousPageLabel="Previous page"
121
+ nextPageLabel="Next page"
122
+ />
123
+ );
124
+ }
125
+ const screen = await render(<PaginationHost />);
126
+
127
+ expect(bodyRows(screen)).toHaveLength(2);
128
+ expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled();
129
+ expect(screen.getByText('1–2 / 3').element()).toBeTruthy();
130
+
131
+ await screen.getByRole('button', { name: 'Next page' }).click();
132
+ expect(bodyRows(screen)).toHaveLength(1);
133
+ expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
134
+ expect(screen.getByText('3–3 / 3').element()).toBeTruthy();
135
+ });
136
+
137
+ test('renders logical alignment inside an RTL container without mirroring hacks', async () => {
138
+ const screen = await render(
139
+ <div dir="rtl">
140
+ <DataTable columns={columns} rows={rows} caption="Service health" empty="No services yet." />
141
+ </div>,
142
+ );
143
+
144
+ const header = screen
145
+ .getByRole('columnheader', { name: 'Owner' })
146
+ .element() as HTMLTableCellElement;
147
+ expect(header.getAttribute('data-align')).toBe('start');
148
+ expect(header.className).toContain('text-start');
149
+ const technicalCell = screen
150
+ .getByRole('cell', { name: 'catalog-api' })
151
+ .element() as HTMLTableCellElement;
152
+ expect(technicalCell.querySelector('[dir="ltr"]')).toBeTruthy();
153
+ });
154
+
155
+ test('loading renders skeleton rows instead of data', async () => {
156
+ const screen = await render(
157
+ <DataTable
158
+ columns={columns}
159
+ rows={rows}
160
+ caption="Service health"
161
+ empty="No services yet."
162
+ loading
163
+ loadingRows={2}
164
+ />,
165
+ );
166
+
167
+ expect(
168
+ (screen.getByRole('table').element() as HTMLTableElement).querySelectorAll(
169
+ '[data-slot="data-table-loading-row"]',
170
+ ),
171
+ ).toHaveLength(2);
172
+ });
173
+
174
+ test('empty model renders the empty affordance', async () => {
175
+ const screen = await render(
176
+ <DataTable columns={columns} rows={[]} caption="Service health" empty="No services yet." />,
177
+ );
178
+
179
+ expect(screen.getByText('No services yet.').element()).toBeTruthy();
180
+ });