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.
- package/README.md +2 -2
- package/apps/storybook/.storybook/preview.tsx +1 -1
- package/apps/storybook/stories/catalog/app-frame.stories.tsx +213 -1
- 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/dialog.stories.tsx +2 -2
- package/apps/storybook/stories/catalog/icon-system.stories.tsx +11 -0
- package/apps/storybook/stories/catalog/menu.stories.tsx +3 -3
- package/apps/web/styles.css +0 -1
- package/docs/design-system/brand-theme.md +8 -0
- package/docs/design-system/components.md +38 -0
- package/docs/design-system/inventory.md +1 -0
- 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/tailwind.css +5 -0
- package/packages/design-tokens/src/tokens.css +10 -4
- package/packages/design-tokens/src/tokens.json +8 -4
- package/packages/design-tokens/src/tokens.mjs +8 -4
- package/packages/icons/package.json +6 -1
- package/packages/icons/src/brands/nocfo.svg +8 -0
- package/packages/icons/src/nocfo-logo.browser.test.tsx +14 -0
- package/packages/icons/src/nocfo-logo.tsx +23 -0
- 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 +9 -2
- package/packages/registry/r/approval-request.json +7 -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/icon-system.json +18 -5
- 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 +5 -2
- 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 +6 -0
- package/packages/ui/src/app-frame.tsx +9 -2
- package/packages/ui/src/badge.tsx +4 -4
- package/packages/ui/src/calendar.tsx +1 -1
- package/packages/ui/src/checkbox.tsx +1 -1
- package/packages/ui/src/code.tsx +3 -1
- package/packages/ui/src/field.tsx +1 -1
- package/packages/ui/src/file-uploader.tsx +1 -1
- package/packages/ui/src/progress-steps.tsx +1 -1
- package/packages/ui/src/sidebar-navigation.tsx +11 -1
- package/scripts/generate-manifest.mjs +71 -0
- 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
|
+
});
|