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
package/README.md
CHANGED
|
@@ -100,8 +100,8 @@ Advanced Security are enabled in repository settings.
|
|
|
100
100
|
CodeQL is similarly opt-in through `ENABLE_CODEQL=true`: first enable **Code
|
|
101
101
|
Security** for this private repository in GitHub’s security settings, then set the
|
|
102
102
|
variable. This avoids a misleading failed workflow when GitHub cannot accept SARIF
|
|
103
|
-
uploads. The workflows
|
|
104
|
-
|
|
103
|
+
uploads. The workflows configure Node 24 and use `actions/setup-node@v6`, whose
|
|
104
|
+
action runtime is also Node 24.
|
|
105
105
|
|
|
106
106
|
## Architecture
|
|
107
107
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import { AppFrame } from '../../../../packages/ui/src/app-frame';
|
|
4
|
+
import { ChartSquare, ReceiptSearch, WalletMoney } from '../../../../packages/icons/src';
|
|
4
5
|
import { SidebarNavigation } from '../../../../packages/ui/src/sidebar-navigation';
|
|
5
6
|
|
|
6
7
|
const navigation = (
|
|
@@ -36,6 +37,44 @@ function AppFrameDemo() {
|
|
|
36
37
|
);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function FinanceAppFrameDemo() {
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ height: '28rem' }}>
|
|
43
|
+
<AppFrame
|
|
44
|
+
header={
|
|
45
|
+
<div className="flex min-h-14 items-center bg-surface-raised px-4 text-sm font-medium">
|
|
46
|
+
NOCFO
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
navigation={
|
|
50
|
+
<SidebarNavigation
|
|
51
|
+
label="NOCFO navigation"
|
|
52
|
+
sections={[
|
|
53
|
+
{
|
|
54
|
+
id: 'finance',
|
|
55
|
+
label: 'Finance',
|
|
56
|
+
items: [
|
|
57
|
+
{ href: '#dashboard', icon: <ChartSquare />, label: 'Dashboard', current: true },
|
|
58
|
+
{ href: '#contracts', icon: <ReceiptSearch />, label: 'Contracts' },
|
|
59
|
+
{ href: '#cashflow', icon: <WalletMoney />, label: 'Cash flow', badge: '12' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
]}
|
|
63
|
+
/>
|
|
64
|
+
}
|
|
65
|
+
variant="finance"
|
|
66
|
+
>
|
|
67
|
+
<section className="rounded-lg bg-surface p-5">
|
|
68
|
+
<h2 className="m-0 text-lg font-semibold">Financial overview</h2>
|
|
69
|
+
<p className="mb-0 mt-2 text-sm text-muted-foreground">
|
|
70
|
+
Flat surfaces are separated by semantic color and spacing, not shadows or card strokes.
|
|
71
|
+
</p>
|
|
72
|
+
</section>
|
|
73
|
+
</AppFrame>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
const meta = {
|
|
40
79
|
title: 'Catalog/Patterns/Pages/App Frame',
|
|
41
80
|
component: AppFrameDemo,
|
|
@@ -45,3 +84,7 @@ export default meta;
|
|
|
45
84
|
type Story = StoryObj<typeof meta>;
|
|
46
85
|
|
|
47
86
|
export const Default: Story = {};
|
|
87
|
+
|
|
88
|
+
export const Finance: Story = {
|
|
89
|
+
render: () => <FinanceAppFrameDemo />,
|
|
90
|
+
};
|
|
@@ -22,6 +22,7 @@ export const Default: Story = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
export const Outline: Story = { args: { variant: 'outline', children: 'Outline' } };
|
|
25
|
+
export const Secondary: Story = { args: { variant: 'secondary', children: 'Secondary' } };
|
|
25
26
|
export const Ghost: Story = { args: { variant: 'ghost', children: 'Ghost' } };
|
|
26
27
|
export const Destructive: Story = { args: { variant: 'destructive', children: 'Delete' } };
|
|
27
28
|
export const Loading: Story = { args: { loading: true, children: 'Saving' } };
|
|
@@ -8,11 +8,16 @@ const meta = {
|
|
|
8
8
|
} satisfies Meta<typeof Checkbox>;
|
|
9
9
|
export default meta;
|
|
10
10
|
type Story = StoryObj<typeof meta>;
|
|
11
|
-
export const Default: Story = {
|
|
11
|
+
export const Default: Story = {
|
|
12
|
+
render: () => <Checkbox label="Enable notifications" />,
|
|
13
|
+
};
|
|
14
|
+
export const Bare: Story = {
|
|
15
|
+
render: () => <Checkbox aria-label="Enable notifications" />,
|
|
16
|
+
};
|
|
12
17
|
export const RTL: Story = {
|
|
13
18
|
render: () => (
|
|
14
19
|
<div dir="rtl">
|
|
15
|
-
<Checkbox />
|
|
20
|
+
<Checkbox label="فعالسازی اعلانها" />
|
|
16
21
|
</div>
|
|
17
22
|
),
|
|
18
23
|
};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Button } from '@noe/ui/button';
|
|
4
|
+
import { EmptyState } from '@noe/ui/empty-state';
|
|
5
|
+
import { Input } from '@noe/ui/input';
|
|
2
6
|
|
|
3
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type DataColumn,
|
|
9
|
+
DataTable,
|
|
10
|
+
type DataSort,
|
|
11
|
+
type FilterSpec,
|
|
12
|
+
type PageSpec,
|
|
13
|
+
type RowId,
|
|
14
|
+
} from '../../../../packages/data-ui/src/data-table';
|
|
15
|
+
import { FilterBar } from '../../../../packages/data-ui/src/filter-bar';
|
|
4
16
|
|
|
5
17
|
type ServiceRow = { id: string; service: string; owner: string; status: string; updated: string };
|
|
6
18
|
|
|
7
19
|
const columns: DataColumn<ServiceRow>[] = [
|
|
8
|
-
{ key: 'service', header: 'Service' },
|
|
20
|
+
{ key: 'service', header: 'Service', technical: true },
|
|
9
21
|
{ key: 'owner', header: 'Owner' },
|
|
10
22
|
{ key: 'status', header: 'Status' },
|
|
11
23
|
{ key: 'updated', header: 'Updated', technical: true, align: 'end' },
|
|
@@ -29,6 +41,18 @@ const rows: ServiceRow[] = [
|
|
|
29
41
|
},
|
|
30
42
|
];
|
|
31
43
|
|
|
44
|
+
const moreRows: ServiceRow[] = [
|
|
45
|
+
...rows,
|
|
46
|
+
{
|
|
47
|
+
id: '4',
|
|
48
|
+
service: 'registry-api',
|
|
49
|
+
owner: 'Design Systems',
|
|
50
|
+
status: 'Healthy',
|
|
51
|
+
updated: '3h ago',
|
|
52
|
+
},
|
|
53
|
+
{ id: '5', service: 'agent-gateway', owner: 'AI Platform', status: 'Healthy', updated: '5h ago' },
|
|
54
|
+
];
|
|
55
|
+
|
|
32
56
|
function DataTableDemo() {
|
|
33
57
|
return (
|
|
34
58
|
<DataTable columns={columns} rows={rows} caption="Service health" empty="No services yet." />
|
|
@@ -44,8 +68,207 @@ export default meta;
|
|
|
44
68
|
type Story = StoryObj<typeof meta>;
|
|
45
69
|
|
|
46
70
|
export const Default: Story = {};
|
|
71
|
+
|
|
47
72
|
export const Empty: Story = {
|
|
48
73
|
render: () => (
|
|
49
|
-
<DataTable
|
|
74
|
+
<DataTable
|
|
75
|
+
columns={columns}
|
|
76
|
+
rows={[]}
|
|
77
|
+
caption="Service health"
|
|
78
|
+
empty={
|
|
79
|
+
<EmptyState
|
|
80
|
+
title="No services yet"
|
|
81
|
+
description="Services appear here once they report health to the platform."
|
|
82
|
+
/>
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
function SortingDemo() {
|
|
89
|
+
const [sorting, setSorting] = useState<DataSort[]>([{ field: 'service', direction: 'asc' }]);
|
|
90
|
+
return (
|
|
91
|
+
<DataTable
|
|
92
|
+
columns={columns}
|
|
93
|
+
rows={[...moreRows]}
|
|
94
|
+
caption="Service health"
|
|
95
|
+
empty="No services yet."
|
|
96
|
+
sorting={sorting}
|
|
97
|
+
onSortingChange={setSorting}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
export const Sorting: Story = { render: () => <SortingDemo /> };
|
|
102
|
+
|
|
103
|
+
function SelectionDemo() {
|
|
104
|
+
const [selected, setSelected] = useState<RowId[]>(['2']);
|
|
105
|
+
return (
|
|
106
|
+
<DataTable
|
|
107
|
+
columns={columns}
|
|
108
|
+
rows={rows}
|
|
109
|
+
caption="Service health"
|
|
110
|
+
empty="No services yet."
|
|
111
|
+
selection={selected}
|
|
112
|
+
onSelectionChange={setSelected}
|
|
113
|
+
selectAllLabel="Select all services"
|
|
114
|
+
rowSelectionLabel={(row: ServiceRow) => `Select ${row.service}`}
|
|
115
|
+
selectedCountLabel={(count: number) => `${count} selected`}
|
|
116
|
+
renderSelectionActions={({ selectedIds }) => (
|
|
117
|
+
<Button size="sm" variant="outline">
|
|
118
|
+
Restart {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
|
119
|
+
</Button>
|
|
120
|
+
)}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
export const Selection: Story = { render: () => <SelectionDemo /> };
|
|
125
|
+
|
|
126
|
+
function PaginationDemo() {
|
|
127
|
+
const [page, setPage] = useState<PageSpec>({ index: 0, size: 2 });
|
|
128
|
+
return (
|
|
129
|
+
<DataTable
|
|
130
|
+
columns={columns}
|
|
131
|
+
rows={[...moreRows]}
|
|
132
|
+
caption="Service health"
|
|
133
|
+
empty="No services yet."
|
|
134
|
+
page={page}
|
|
135
|
+
onPageChange={setPage}
|
|
136
|
+
previousPageLabel="Previous page"
|
|
137
|
+
nextPageLabel="Next page"
|
|
138
|
+
paginationLabel="Service health pagination"
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
export const Pagination: Story = { render: () => <PaginationDemo /> };
|
|
143
|
+
|
|
144
|
+
function WithFiltersDemo() {
|
|
145
|
+
const [search, setSearch] = useState('');
|
|
146
|
+
const [filters, setFilters] = useState<FilterSpec[]>([]);
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex flex-col gap-3">
|
|
149
|
+
<FilterBar
|
|
150
|
+
label="Filter services"
|
|
151
|
+
search={
|
|
152
|
+
<Input
|
|
153
|
+
aria-label="Search services"
|
|
154
|
+
onChange={(event) => {
|
|
155
|
+
setSearch(event.target.value);
|
|
156
|
+
setFilters(
|
|
157
|
+
event.target.value
|
|
158
|
+
? [{ field: 'service', operator: 'contains', value: event.target.value }]
|
|
159
|
+
: [],
|
|
160
|
+
);
|
|
161
|
+
}}
|
|
162
|
+
placeholder="Search services"
|
|
163
|
+
size="sm"
|
|
164
|
+
value={search}
|
|
165
|
+
/>
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
<DataTable
|
|
169
|
+
columns={columns}
|
|
170
|
+
rows={[...moreRows]}
|
|
171
|
+
caption="Service health"
|
|
172
|
+
empty="No services match the current filter."
|
|
173
|
+
filters={filters}
|
|
174
|
+
onFiltersChange={setFilters}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
export const WithFilters: Story = { render: () => <WithFiltersDemo /> };
|
|
180
|
+
|
|
181
|
+
function CompactDemo() {
|
|
182
|
+
return (
|
|
183
|
+
<DataTable
|
|
184
|
+
columns={columns}
|
|
185
|
+
rows={[...moreRows]}
|
|
186
|
+
caption="Service health"
|
|
187
|
+
density="compact"
|
|
188
|
+
empty="No services yet."
|
|
189
|
+
page={{ index: 0, size: 3 }}
|
|
190
|
+
onPageChange={() => {}}
|
|
191
|
+
previousPageLabel="Previous page"
|
|
192
|
+
nextPageLabel="Next page"
|
|
193
|
+
paginationLabel="Service health pagination"
|
|
194
|
+
/>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
export const Compact: Story = { render: () => <CompactDemo /> };
|
|
198
|
+
|
|
199
|
+
function RtlDemo() {
|
|
200
|
+
const [sorting, setSorting] = useState<DataSort[]>([{ field: 'service', direction: 'asc' }]);
|
|
201
|
+
const faColumns: DataColumn<ServiceRow>[] = [
|
|
202
|
+
{ key: 'service', header: 'سرویس', technical: true },
|
|
203
|
+
{ key: 'owner', header: 'مالک' },
|
|
204
|
+
{ key: 'status', header: 'وضعیت' },
|
|
205
|
+
{ key: 'updated', header: 'آخرین بهروزرسانی', align: 'end' },
|
|
206
|
+
];
|
|
207
|
+
return (
|
|
208
|
+
<div dir="rtl" lang="fa">
|
|
209
|
+
<DataTable
|
|
210
|
+
columns={faColumns}
|
|
211
|
+
rows={rows}
|
|
212
|
+
caption="وضعیت سرویسها"
|
|
213
|
+
density="compact"
|
|
214
|
+
empty="هنوز سرویسی ثبت نشده است."
|
|
215
|
+
sorting={sorting}
|
|
216
|
+
onSortingChange={setSorting}
|
|
217
|
+
selectAllLabel="انتخاب همه سرویسها"
|
|
218
|
+
rowSelectionLabel={(row: ServiceRow) => `انتخاب ${row.service}`}
|
|
219
|
+
selectedCountLabel={(count: number) => `${count} مورد انتخاب شده`}
|
|
220
|
+
page={{ index: 0, size: 2 }}
|
|
221
|
+
onPageChange={() => {}}
|
|
222
|
+
previousPageLabel="صفحه قبلی"
|
|
223
|
+
nextPageLabel="صفحه بعدی"
|
|
224
|
+
paginationLabel="صفحهبندی وضعیت سرویسها"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
export const RTL: Story = { render: () => <RtlDemo /> };
|
|
230
|
+
|
|
231
|
+
function ServerSideDemo() {
|
|
232
|
+
const [query, setQuery] = useState<{ sorts: DataSort[]; filters: FilterSpec[]; page: PageSpec }>({
|
|
233
|
+
sorts: [],
|
|
234
|
+
filters: [],
|
|
235
|
+
page: { index: 0, size: 2 },
|
|
236
|
+
});
|
|
237
|
+
// A real product sends `toDataQuery(query)` to Django and renders the page.
|
|
238
|
+
const serverPage = moreRows.slice(
|
|
239
|
+
query.page.index * query.page.size,
|
|
240
|
+
(query.page.index + 1) * query.page.size,
|
|
241
|
+
);
|
|
242
|
+
return (
|
|
243
|
+
<DataTable
|
|
244
|
+
columns={columns}
|
|
245
|
+
rows={serverPage}
|
|
246
|
+
caption="Service health"
|
|
247
|
+
empty="No services yet."
|
|
248
|
+
sorting={query.sorts}
|
|
249
|
+
onSortingChange={(sorts) => setQuery((q) => ({ ...q, sorts }))}
|
|
250
|
+
manualSorting
|
|
251
|
+
page={query.page}
|
|
252
|
+
onPageChange={(next) => setQuery((q) => ({ ...q, page: next }))}
|
|
253
|
+
manualPagination
|
|
254
|
+
totalRowCount={moreRows.length}
|
|
255
|
+
previousPageLabel="Previous page"
|
|
256
|
+
nextPageLabel="Next page"
|
|
257
|
+
paginationLabel="Service health pagination"
|
|
258
|
+
/>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
export const ServerSide: Story = { render: () => <ServerSideDemo /> };
|
|
262
|
+
|
|
263
|
+
export const Loading: Story = {
|
|
264
|
+
render: () => (
|
|
265
|
+
<DataTable
|
|
266
|
+
columns={columns}
|
|
267
|
+
rows={[]}
|
|
268
|
+
caption="Service health"
|
|
269
|
+
empty="No services yet."
|
|
270
|
+
loading
|
|
271
|
+
loadingRows={3}
|
|
272
|
+
/>
|
|
50
273
|
),
|
|
51
274
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
2
|
|
|
3
3
|
import { SidebarNavigation } from '../../../../packages/ui/src/sidebar-navigation';
|
|
4
|
+
import { ChartSquare, ReceiptSearch, WalletMoney } from '../../../../packages/icons/src';
|
|
4
5
|
|
|
5
6
|
const sections = [
|
|
6
7
|
{
|
|
@@ -38,3 +39,24 @@ export default meta;
|
|
|
38
39
|
type Story = StoryObj<typeof meta>;
|
|
39
40
|
|
|
40
41
|
export const Default: Story = {};
|
|
42
|
+
|
|
43
|
+
export const PersianRtl: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<div dir="rtl" lang="fa" style={{ maxWidth: '17.5rem' }}>
|
|
46
|
+
<SidebarNavigation
|
|
47
|
+
label="ناوبری نوکفو"
|
|
48
|
+
sections={[
|
|
49
|
+
{
|
|
50
|
+
id: 'finance',
|
|
51
|
+
label: 'مدیریت مالی',
|
|
52
|
+
items: [
|
|
53
|
+
{ href: '#dashboard', icon: <ChartSquare />, label: 'داشبورد', current: true },
|
|
54
|
+
{ href: '#contracts', icon: <ReceiptSearch />, label: 'قراردادها' },
|
|
55
|
+
{ href: '#cashflow', icon: <WalletMoney />, label: 'جریان نقدی', badge: '۱۲' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
]}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
};
|
|
@@ -18,8 +18,9 @@ interfaces use the same language rather than a related palette.
|
|
|
18
18
|
Use large, confident headings; quiet metadata with tracked uppercase only for
|
|
19
19
|
Latin labels; generous spacing; and low-contrast graphite/paper layers. Interactive
|
|
20
20
|
boundaries use the semantic border token and the shared rounded geometry. Floating
|
|
21
|
-
chat and modal surfaces
|
|
22
|
-
|
|
21
|
+
chat and modal surfaces remain flat, using a quiet mint ambient layer or overlay
|
|
22
|
+
scrim when separation is needed; `shadow.raised` and `shadow.overlay` stay `none`.
|
|
23
|
+
Do not create a separate app palette.
|
|
23
24
|
|
|
24
25
|
## Product mapping
|
|
25
26
|
|
|
@@ -47,6 +47,19 @@ For application pages, compose `app-frame`, `page-header`, `sidebar-navigation`,
|
|
|
47
47
|
Use the [flat surface and navigation policy](surface-policy.md) to decide surface
|
|
48
48
|
tone, CTA hierarchy, adaptive navigation, icon coloring, and review evidence.
|
|
49
49
|
|
|
50
|
+
### Finance application shell
|
|
51
|
+
|
|
52
|
+
For NOCFO-class workspaces use `AppFrame variant="finance"` with
|
|
53
|
+
`HeaderNavigation` and `SidebarNavigation`. It uses a 280px desktop navigation
|
|
54
|
+
column, a compact 56px header, and semantic background/surface/sunken layers for
|
|
55
|
+
separation. It never solves Persian or English layout with `row-reverse`: icons
|
|
56
|
+
render at the logical start and end adornments use `margin-inline-start`.
|
|
57
|
+
|
|
58
|
+
The shell is deliberately flat. Sections and cards are differentiated by the
|
|
59
|
+
surface tokens and spacing; their outer stroke and every shadow are absent. Use
|
|
60
|
+
the standard `Button variant="secondary"` for transparent, currentColor outline
|
|
61
|
+
actions. It is white on dark/colored surfaces and graphite on paper.
|
|
62
|
+
|
|
50
63
|
Mapping to the Untitled UI reference taxonomy (honest as of 2026-08-27 — foundation stabilization):
|
|
51
64
|
|
|
52
65
|
| Untitled UI category | NOE coverage |
|
|
@@ -88,6 +101,38 @@ Implemented source includes Button, Input, Field, Dialog, Drawer, Tabs, auto-dir
|
|
|
88
101
|
- Tab arrow behavior resolves against active direction.
|
|
89
102
|
- Email, URL, telephone, numeric, code and identifier content preserve technical direction.
|
|
90
103
|
|
|
104
|
+
## Data tables
|
|
105
|
+
|
|
106
|
+
`DataTable` in `@noe/data-ui` owns the semantic `<table>` markup, NOE tokens,
|
|
107
|
+
RTL behavior and accessibility, while TanStack Table v9 provides the headless
|
|
108
|
+
state engine behind an internal adapter
|
|
109
|
+
(`packages/data-ui/src/table/adapters/tanstack.ts`, per ADR 0008). TanStack
|
|
110
|
+
types never appear in the public API: products program against NOE contracts
|
|
111
|
+
(`DataColumn`, `DataSort`, `FilterSpec`, `PageSpec`, `RowId`, `SelectionState`,
|
|
112
|
+
`DataQuery`) exported from `@noe/data-ui`.
|
|
113
|
+
|
|
114
|
+
- Sorting headers cycle ascending → descending → unsorted and expose `aria-sort`;
|
|
115
|
+
icons are direction-neutral so RTL needs no mirroring.
|
|
116
|
+
- Filtering uses NOE `FilterSpec` operators (`contains`, `equals`, `in`,
|
|
117
|
+
`between`, …). `FilterBar` composes the controls; `DataTable` owns state.
|
|
118
|
+
- Pagination is controlled via `PageSpec` with caller-provided accessible
|
|
119
|
+
labels, matching the `Pagination` primitive contract.
|
|
120
|
+
- Selection keeps stable row ids (`RowId`), supports single/multiple modes and
|
|
121
|
+
never selects by positional index.
|
|
122
|
+
- Server mode (`manualSorting`/`manualFiltering`/`manualPagination` +
|
|
123
|
+
`totalRowCount`) trusts incoming data order and slicing; pair it with
|
|
124
|
+
`toDataQuery` to serialize state into a Django query.
|
|
125
|
+
- `density="compact"` tightens row rhythm for dense workspaces using the
|
|
126
|
+
spacing scale; `regular` is the default.
|
|
127
|
+
- `empty` accepts composed content — pass an `EmptyState` for full empty
|
|
128
|
+
affordances. The `WithFilters` catalog story demonstrates the FilterBar +
|
|
129
|
+
Input + DataTable composition.
|
|
130
|
+
|
|
131
|
+
Deterministic visual evidence (light, dark, RTL, selection, loading) is
|
|
132
|
+
captured by `packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx`
|
|
133
|
+
into `evidence/data-table/`; the browser test project renders with the real
|
|
134
|
+
Tailwind-expanded NOE tokens.
|
|
135
|
+
|
|
91
136
|
## Product adapters
|
|
92
137
|
|
|
93
138
|
Data, chart, agent and workflow packages own the NOE composition. Product screens should never import an unnormalized marketplace component directly.
|
|
@@ -386,3 +386,4 @@ Total catalog entries: **129** · registry items: **67**
|
|
|
386
386
|
- `fab`: Mobile-only floating action button anchored to the logical end edge above the bottom navigation, with safe-area padding and required accessible label.
|
|
387
387
|
- `mobile-screen`: Single-column mobile page scaffold composing an app bar, scrollable content region, optional floating action button and bottom navigation.
|
|
388
388
|
- `bottom-navigation`: Mobile-only bottom tab bar with current-page state, badges, icon slots and safe-area padding; hidden from the medium breakpoint upward.
|
|
389
|
+
|
|
@@ -14,16 +14,18 @@ passes caused by arbitrary shadows, card borders, or ambiguous mobile navigation
|
|
|
14
14
|
`appearance="outlined"` is reserved for a selection, data-entry, or comparison
|
|
15
15
|
boundary where a stroke conveys real meaning.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
NOE surfaces are fully flat: `shadow.raised` and `shadow.overlay` are `none` in
|
|
18
|
+
every theme, including dialogs, drawers, menus and popovers. Separation comes from
|
|
19
|
+
semantic surface color, radius, spacing and an overlay scrim—not shadow. Sections
|
|
20
|
+
and ordinary cards do not receive ad-hoc elevation. A focus state is an
|
|
21
|
+
outline/ring, not elevation.
|
|
20
22
|
|
|
21
23
|
## Actions
|
|
22
24
|
|
|
23
25
|
- One screen has one contained primary CTA.
|
|
24
|
-
- Secondary actions are outline
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
- Secondary actions are transparent outline actions. Their label and stroke use
|
|
27
|
+
`currentColor`; therefore the standard foreground is white in dark/colored
|
|
28
|
+
contexts and graphite on paper. Never give a secondary action an opaque fill.
|
|
27
29
|
- Destructive actions retain an explicit label and confirmation/recovery path;
|
|
28
30
|
color alone is never the signal.
|
|
29
31
|
- Every focusable action keeps a visible `focus-visible` outline.
|
|
@@ -52,8 +54,9 @@ is an outline/ring, not elevation.
|
|
|
52
54
|
|
|
53
55
|
- Test every route in Persian/RTL and English/LTR, in light and dark themes.
|
|
54
56
|
- Verify no page-level horizontal overflow at the mobile breakpoint.
|
|
55
|
-
- Verify sections/cards have no visible outer stroke or
|
|
56
|
-
|
|
57
|
+
- Verify sections/cards have no visible outer stroke or shadow unless they are an
|
|
58
|
+
explicitly outlined control (for example a secondary action, input or table
|
|
59
|
+
affordance).
|
|
57
60
|
- Verify interactive targets are at least 44 px on touch devices and have
|
|
58
61
|
accessible names.
|
|
59
62
|
- Test desktop sidebar and mobile drawer independently, including keyboard focus
|
|
@@ -7,4 +7,10 @@ The capture script generates deterministic PNG evidence for:
|
|
|
7
7
|
- Consumer, dashboard, agent and workflow surfaces at representative desktop conditions.
|
|
8
8
|
- Pseudo-LTR and pseudo-RTL stress views.
|
|
9
9
|
|
|
10
|
+
The NOE DataTable has its own deterministic component-level evidence:
|
|
11
|
+
`packages/data-ui/src/table/data-table-visual.evidence.browser.test.tsx`
|
|
12
|
+
renders regular and compact density, Persian RTL, selection (light and dark)
|
|
13
|
+
and loading states with the real Tailwind-expanded tokens into
|
|
14
|
+
`evidence/data-table/`.
|
|
15
|
+
|
|
10
16
|
Review checks include overflow, clipped Persian, bidi corruption, arrow direction, sidebar collapse, technical-value isolation, table scrolling, excessive cards, random shadows, coral overuse, mirrored brand marks, focus order and mobile composition.
|
package/package.json
CHANGED
|
@@ -5,11 +5,16 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@noe/icons": "workspace:*",
|
|
8
|
-
"@noe/ui": "workspace:*"
|
|
8
|
+
"@noe/ui": "workspace:*",
|
|
9
|
+
"@tanstack/react-table": "^9.2.4"
|
|
9
10
|
},
|
|
10
11
|
"peerDependencies": {
|
|
11
12
|
"react": "^19.2.0"
|
|
12
13
|
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"vitest": "4.1.11",
|
|
16
|
+
"vitest-browser-react": "2.2.0"
|
|
17
|
+
},
|
|
13
18
|
"exports": {
|
|
14
19
|
".": "./src/index.ts"
|
|
15
20
|
}
|
|
@@ -1,99 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { TechnicalValue } from '@noe/ui/bidi';
|
|
4
|
-
import { cn } from '@noe/ui/lib/cn';
|
|
5
|
-
|
|
6
|
-
export type DataColumn<Row> = {
|
|
7
|
-
key: keyof Row & string;
|
|
8
|
-
header: React.ReactNode;
|
|
9
|
-
align?: 'start' | 'end';
|
|
10
|
-
technical?: boolean;
|
|
11
|
-
render?: (row: Row) => React.ReactNode;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function DataTable<Row extends { id: string }>({
|
|
15
|
-
columns,
|
|
16
|
-
rows,
|
|
17
|
-
caption,
|
|
18
|
-
empty,
|
|
19
|
-
className,
|
|
20
|
-
}: {
|
|
21
|
-
columns: DataColumn<Row>[];
|
|
22
|
-
rows: Row[];
|
|
23
|
-
caption: string;
|
|
24
|
-
empty: React.ReactNode;
|
|
25
|
-
className?: string;
|
|
26
|
-
}) {
|
|
27
|
-
const captionId = useId();
|
|
28
|
-
if (!rows.length)
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
className={cn(
|
|
32
|
-
'flex min-h-32 items-center justify-center rounded-lg border border-dashed border-border bg-surface-sunken/30 p-6 text-center text-sm text-muted-foreground',
|
|
33
|
-
className,
|
|
34
|
-
)}
|
|
35
|
-
data-slot="data-table-empty"
|
|
36
|
-
>
|
|
37
|
-
{empty}
|
|
38
|
-
</div>
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
// WCAG 2.1: keyboard-accessible scrollable container for overflow tables.
|
|
43
|
-
<section
|
|
44
|
-
aria-labelledby={captionId}
|
|
45
|
-
className={cn('overflow-x-auto rounded-lg border border-border', className)}
|
|
46
|
-
data-slot="data-table-scroll"
|
|
47
|
-
// biome-ignore lint/a11y/noNoninteractiveTabindex: scrollable region focus pattern
|
|
48
|
-
tabIndex={0}
|
|
49
|
-
>
|
|
50
|
-
<table className="w-full min-w-full border-collapse text-sm" data-slot="data-table">
|
|
51
|
-
<caption className="sr-only" id={captionId}>
|
|
52
|
-
{caption}
|
|
53
|
-
</caption>
|
|
54
|
-
<thead>
|
|
55
|
-
<tr className="border-b border-border bg-surface-sunken/60">
|
|
56
|
-
{columns.map((column) => (
|
|
57
|
-
<th
|
|
58
|
-
className={cn(
|
|
59
|
-
'whitespace-nowrap px-4 py-2.5 text-xs font-medium text-muted-foreground',
|
|
60
|
-
column.align === 'end' ? 'text-end' : 'text-start',
|
|
61
|
-
)}
|
|
62
|
-
data-align={column.align ?? 'start'}
|
|
63
|
-
key={column.key}
|
|
64
|
-
scope="col"
|
|
65
|
-
>
|
|
66
|
-
{column.header}
|
|
67
|
-
</th>
|
|
68
|
-
))}
|
|
69
|
-
</tr>
|
|
70
|
-
</thead>
|
|
71
|
-
<tbody>
|
|
72
|
-
{rows.map((row) => {
|
|
73
|
-
const cells = columns.map((column) => {
|
|
74
|
-
const content = column.render ? column.render(row) : String(row[column.key] ?? '');
|
|
75
|
-
const value = column.technical ? <TechnicalValue>{content}</TechnicalValue> : content;
|
|
76
|
-
return (
|
|
77
|
-
<td
|
|
78
|
-
className={cn(
|
|
79
|
-
'whitespace-nowrap px-4 py-2.5 text-foreground',
|
|
80
|
-
column.align === 'end' ? 'text-end' : 'text-start',
|
|
81
|
-
)}
|
|
82
|
-
data-align={column.align ?? 'start'}
|
|
83
|
-
key={column.key}
|
|
84
|
-
>
|
|
85
|
-
{value}
|
|
86
|
-
</td>
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
return (
|
|
90
|
-
<tr className="border-b border-border last:border-b-0 hover:bg-muted/50" key={row.id}>
|
|
91
|
-
{cells}
|
|
92
|
-
</tr>
|
|
93
|
-
);
|
|
94
|
-
})}
|
|
95
|
-
</tbody>
|
|
96
|
-
</table>
|
|
97
|
-
</section>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
1
|
+
export * from './table/contracts';
|
|
2
|
+
export * from './table/data-table';
|
|
@@ -8,8 +8,8 @@ export interface PaginationProps extends React.ComponentProps<'nav'> {
|
|
|
8
8
|
status: React.ReactNode;
|
|
9
9
|
previousLabel: string;
|
|
10
10
|
nextLabel: string;
|
|
11
|
-
onPrevious?: () => void;
|
|
12
|
-
onNext?: () => void;
|
|
11
|
+
onPrevious?: (() => void) | undefined;
|
|
12
|
+
onNext?: (() => void) | undefined;
|
|
13
13
|
canPrevious?: boolean;
|
|
14
14
|
canNext?: boolean;
|
|
15
15
|
}
|