noecosystem-design 0.1.2 → 0.2.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 +73 -35
- package/apps/showcase/src/catalog/catalog-index.tsx +8 -8
- package/apps/showcase/src/catalog/catalog-shell.tsx +7 -2
- package/apps/storybook/stories/catalog/calendar.stories.tsx +3 -1
- package/apps/storybook/stories/catalog/date-picker.stories.tsx +5 -1
- package/apps/storybook/stories/catalog/icon-system.stories.tsx +3 -2
- package/docs/adr/0006-icons8-icon-policy.md +4 -4
- package/docs/design-system/architecture.md +1 -1
- package/docs/design-system/ci-cd.md +22 -7
- package/docs/design-system/inventory.md +0 -1
- package/docs/design-system/rtl.md +1 -1
- package/package.json +2 -2
- package/packages/ai-ui/src/activity-feed.tsx +1 -2
- package/packages/ai-ui/src/agent-composer.tsx +1 -1
- package/packages/ai-ui/src/agent-message.tsx +1 -1
- package/packages/ai-ui/src/agent-plan.tsx +1 -1
- package/packages/ai-ui/src/agent-workspace.tsx +1 -2
- package/packages/ai-ui/src/approval-request.tsx +1 -1
- package/packages/ai-ui/src/run-inspector.tsx +1 -1
- package/packages/ai-ui/src/tool-call.tsx +1 -1
- package/packages/data-ui/src/activity-gauge.tsx +1 -1
- package/packages/data-ui/src/chart-frame.tsx +1 -1
- package/packages/data-ui/src/chart.tsx +1 -2
- package/packages/data-ui/src/dashboard-shell.tsx +1 -2
- package/packages/data-ui/src/data-table.tsx +1 -1
- package/packages/data-ui/src/filter-bar.tsx +1 -2
- package/packages/data-ui/src/metric-card.tsx +1 -1
- package/packages/data-ui/src/pagination.tsx +1 -3
- package/packages/data-ui/src/pie-chart.tsx +1 -1
- package/packages/data-ui/src/radar-chart.tsx +1 -1
- package/packages/design-tokens/src/base.css +1 -1
- package/packages/design-tokens/src/palette.ts +4 -4
- package/packages/design-tokens/src/tokens.css +6 -12
- package/packages/design-tokens/src/tokens.json +4 -4
- package/packages/design-tokens/src/tokens.mjs +4 -4
- package/packages/icons/package.json +3 -0
- package/packages/icons/src/adapters.tsx +4 -4
- package/packages/icons/src/iconsax.tsx +233 -0
- package/packages/icons/src/index.tsx +1 -103
- package/packages/registry/catalog-index.json +1 -1
- package/packages/registry/component-audit.json +1 -1
- package/packages/registry/docs-index.json +135 -403
- package/packages/registry/manifest.json +1642 -6110
- package/packages/registry/r/activity-feed.json +9 -32
- package/packages/registry/r/agent-composer.json +9 -34
- package/packages/registry/r/agent-message.json +9 -32
- package/packages/registry/r/agent-plan.json +9 -32
- package/packages/registry/r/agent-workspace.json +9 -32
- package/packages/registry/r/alert.json +9 -33
- package/packages/registry/r/app-frame.json +9 -32
- package/packages/registry/r/approval-request.json +9 -34
- package/packages/registry/r/badge.json +9 -33
- package/packages/registry/r/bidi-text.json +7 -25
- package/packages/registry/r/bottom-navigation.json +9 -32
- package/packages/registry/r/button.json +7 -29
- package/packages/registry/r/calendar.json +15 -31
- package/packages/registry/r/card.json +9 -32
- package/packages/registry/r/chart-frame.json +9 -32
- package/packages/registry/r/chart.json +8 -30
- package/packages/registry/r/code-block.json +9 -32
- package/packages/registry/r/context-menu.json +8 -36
- package/packages/registry/r/dashboard-01.json +7 -23
- package/packages/registry/r/dashboard-02.json +6 -20
- package/packages/registry/r/dashboard-shell.json +9 -32
- package/packages/registry/r/data-list.json +9 -32
- package/packages/registry/r/data-table.json +9 -32
- package/packages/registry/r/date-picker.json +15 -31
- package/packages/registry/r/dialog.json +11 -45
- package/packages/registry/r/direction-provider.json +6 -21
- package/packages/registry/r/drawer.json +10 -36
- package/packages/registry/r/dropdown.json +9 -37
- package/packages/registry/r/empty-state.json +9 -32
- package/packages/registry/r/error-state.json +9 -32
- package/packages/registry/r/fab.json +9 -34
- package/packages/registry/r/featured-icon.json +7 -29
- package/packages/registry/r/filter-bar.json +10 -35
- package/packages/registry/r/gradient-picker.json +7 -24
- package/packages/registry/r/header-navigation.json +9 -32
- package/packages/registry/r/hero-header.json +7 -25
- package/packages/registry/r/icon-system.json +14 -29
- package/packages/registry/r/image-picker.json +7 -24
- package/packages/registry/r/inline-cta.json +14 -32
- package/packages/registry/r/input.json +9 -33
- package/packages/registry/r/landing-page.json +6 -21
- package/packages/registry/r/login-page.json +9 -35
- package/packages/registry/r/marketing-header.json +8 -26
- package/packages/registry/r/menu.json +9 -34
- package/packages/registry/r/messaging.json +9 -33
- package/packages/registry/r/metric-card.json +9 -32
- package/packages/registry/r/mobile-screen.json +9 -32
- package/packages/registry/r/not-found.json +7 -25
- package/packages/registry/r/page-header.json +9 -32
- package/packages/registry/r/pagination.json +15 -31
- package/packages/registry/r/progress-steps.json +9 -33
- package/packages/registry/r/run-inspector.json +9 -32
- package/packages/registry/r/section-header.json +9 -32
- package/packages/registry/r/settings-01.json +5 -19
- package/packages/registry/r/settings-02.json +5 -19
- package/packages/registry/r/settings-page.json +9 -32
- package/packages/registry/r/sidebar-navigation.json +9 -32
- package/packages/registry/r/sign-in-form.json +9 -34
- package/packages/registry/r/skeleton.json +9 -32
- package/packages/registry/r/spinner.json +9 -32
- package/packages/registry/r/tabs.json +9 -34
- package/packages/registry/r/textarea.json +9 -33
- package/packages/registry/r/tool-call.json +9 -32
- package/packages/registry/r/tooltip.json +9 -33
- package/packages/registry/r/utility-button.json +9 -37
- package/packages/registry/r/workflow-canvas.json +9 -32
- package/packages/registry/r/workflow-node.json +9 -32
- package/packages/registry/registry.json +232 -937
- package/packages/registry/search-index.json +579 -2194
- package/packages/ui/package.json +4 -1
- package/packages/ui/src/date-picker.browser.test.tsx +1 -1
- package/packages/ui/src/date-picker.tsx +1 -2
- package/packages/ui/src/gradient-picker.tsx +6 -2
- package/packages/ui/src/theme-provider.browser.test.tsx +15 -6
- package/packages/ui/src/theme-provider.tsx +19 -5
- package/packages/workflow-ui/src/workflow-canvas.tsx +1 -1
- package/packages/workflow-ui/src/workflow-node.tsx +1 -1
- package/scripts/enrich-registry-payloads.mjs +9 -3
- package/scripts/fix-registry-closure.mjs +2 -1
- package/scripts/generate-manifest.mjs +61 -39
- package/scripts/validate-manifest.mjs +24 -33
- package/scripts/verify-registry-consumer.mjs +33 -7
- package/tests/e2e/catalog-typography.spec.ts +6 -4
- package/tests/icons.test.mjs +5 -5
- package/tsconfig.base.json +17 -51
- package/apps/showcase/src/catalog/__screenshots__/catalog-shell.browser.test.tsx/filters-the-component-inventory-and-exposes-the-selected-audit-state-1.png +0 -0
- package/apps/showcase/src/catalog/__screenshots__/catalog-shell.browser.test.tsx/opens-a-component-detail-view-and-returns-to-the-grid-1.png +0 -0
- package/apps/showcase/src/catalog/__screenshots__/catalog-shell.browser.test.tsx/switching-locale-to-Persian-updates-document-direction-to-rtl-1.png +0 -0
- package/packages/ui/src/__screenshots__/button.browser.test.tsx/Button-preserves-native--busy--disabled--and-ref-contracts-1.png +0 -0
- package/packages/ui/src/__screenshots__/button.browser.test.tsx/a-disabled-polymorphic-Button-cannot-navigate-or-receive-focus-1.png +0 -0
- package/packages/ui/src/__screenshots__/date-picker.browser.test.tsx/DatePicker-Escape-closes-popover-and-returns-focus-to-input-1.png +0 -0
- package/packages/ui/src/__screenshots__/date-picker.browser.test.tsx/DatePicker-click-outside-closes-popover-1.png +0 -0
- package/packages/ui/src/__screenshots__/dialog.browser.test.tsx/Dialog-propagates-RTL-into-the-portal-and-traps-then-restores-focus-1.png +0 -0
package/packages/ui/package.json
CHANGED
|
@@ -96,8 +96,7 @@ export function DatePicker({
|
|
|
96
96
|
setPopoverStyle({
|
|
97
97
|
position: 'fixed',
|
|
98
98
|
top: `${rect.bottom + 8}px`,
|
|
99
|
-
|
|
100
|
-
right: isRtl ? `${window.innerWidth - rect.right}px` : 'auto',
|
|
99
|
+
insetInlineStart: `${isRtl ? window.innerWidth - rect.right : rect.left}px`,
|
|
101
100
|
zIndex: 'var(--noe-z-index-overlay, 50)',
|
|
102
101
|
});
|
|
103
102
|
};
|
|
@@ -42,14 +42,18 @@ export function GradientPicker({
|
|
|
42
42
|
aria-label="Start color"
|
|
43
43
|
type="color"
|
|
44
44
|
defaultValue={GRADIENT_START}
|
|
45
|
-
onChange={(e) =>
|
|
45
|
+
onChange={(e) =>
|
|
46
|
+
onValueChange?.(`linear-gradient(90deg, ${e.target.value}, ${GRADIENT_END})`)
|
|
47
|
+
}
|
|
46
48
|
className="size-8 rounded-md border border-border"
|
|
47
49
|
/>
|
|
48
50
|
<input
|
|
49
51
|
aria-label="End color"
|
|
50
52
|
type="color"
|
|
51
53
|
defaultValue={GRADIENT_END}
|
|
52
|
-
onChange={(e) =>
|
|
54
|
+
onChange={(e) =>
|
|
55
|
+
onValueChange?.(`linear-gradient(90deg, ${GRADIENT_START}, ${e.target.value})`)
|
|
56
|
+
}
|
|
53
57
|
className="size-8 rounded-md border border-border"
|
|
54
58
|
/>
|
|
55
59
|
<input
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
1
|
import { expect, test } from 'vitest';
|
|
3
2
|
import { render } from 'vitest-browser-react';
|
|
4
3
|
|
|
@@ -10,14 +9,17 @@ function PaletteProbe({ storageKey }: { storageKey?: string }) {
|
|
|
10
9
|
return (
|
|
11
10
|
<div>
|
|
12
11
|
<span data-testid="palette">{palette}</span>
|
|
13
|
-
<button type="button" onClick={() => setPalette('graphite' as never)}>
|
|
14
|
-
|
|
12
|
+
<button type="button" onClick={() => setPalette('graphite' as never)}>
|
|
13
|
+
set graphite
|
|
14
|
+
</button>
|
|
15
|
+
{storageKey ? <span data-testid="storageKey">{storageKey}</span> : null}
|
|
15
16
|
</div>
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
test('default Graphite with no preference', async () => {
|
|
20
21
|
localStorage.clear();
|
|
22
|
+
// biome-ignore lint/suspicious/noDocumentCookie: Test setup must exercise the cookie fallback path.
|
|
21
23
|
document.cookie = 'noecosystem-palette=; Max-Age=0; path=/';
|
|
22
24
|
document.documentElement.removeAttribute('data-noe-palette');
|
|
23
25
|
const screen = await render(
|
|
@@ -26,7 +28,9 @@ test('default Graphite with no preference', async () => {
|
|
|
26
28
|
</ThemeProvider>,
|
|
27
29
|
);
|
|
28
30
|
await expect.element(screen.getByTestId('palette')).toHaveTextContent('graphite');
|
|
29
|
-
await expect
|
|
31
|
+
await expect
|
|
32
|
+
.poll(() => document.documentElement.getAttribute('data-noe-palette'))
|
|
33
|
+
.toBe('graphite');
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
test('valid stored preference restoration', async () => {
|
|
@@ -41,6 +45,7 @@ test('valid stored preference restoration', async () => {
|
|
|
41
45
|
|
|
42
46
|
test('invalid storage and cookie fallback to Graphite', async () => {
|
|
43
47
|
localStorage.setItem('noecosystem-palette', 'invalid-palette');
|
|
48
|
+
// biome-ignore lint/suspicious/noDocumentCookie: This test needs an invalid cookie value for fallback coverage.
|
|
44
49
|
document.cookie = 'noecosystem-palette=invalid-palette; path=/';
|
|
45
50
|
const screen = await render(
|
|
46
51
|
<ThemeProvider>
|
|
@@ -49,7 +54,9 @@ test('invalid storage and cookie fallback to Graphite', async () => {
|
|
|
49
54
|
);
|
|
50
55
|
// Should fallback to graphite
|
|
51
56
|
await expect.element(screen.getByTestId('palette')).toHaveTextContent('graphite');
|
|
52
|
-
await expect
|
|
57
|
+
await expect
|
|
58
|
+
.poll(() => document.documentElement.getAttribute('data-noe-palette'))
|
|
59
|
+
.toBe('graphite');
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
test('custom paletteStorageKey is honored', async () => {
|
|
@@ -84,7 +91,9 @@ test('palette/theme independence in light and dark modes', async () => {
|
|
|
84
91
|
</ThemeProvider>,
|
|
85
92
|
);
|
|
86
93
|
await expect.element(screen.getByTestId('palette')).toHaveTextContent('graphite');
|
|
87
|
-
await expect
|
|
94
|
+
await expect
|
|
95
|
+
.poll(() => document.documentElement.getAttribute('data-noe-palette'))
|
|
96
|
+
.toBe('graphite');
|
|
88
97
|
await expect.poll(() => document.documentElement.dataset.theme).toBe('light');
|
|
89
98
|
await expect.element(screen.getByTestId('palette')).toHaveTextContent('graphite');
|
|
90
99
|
});
|
|
@@ -160,8 +160,12 @@ function applyThemeToDocument(theme: ResolvedTheme) {
|
|
|
160
160
|
root.style.colorScheme = theme;
|
|
161
161
|
const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
|
162
162
|
if (meta) {
|
|
163
|
-
const lightFallback =
|
|
164
|
-
|
|
163
|
+
const lightFallback =
|
|
164
|
+
(tokens as unknown as { primitive: { color: { paper: Record<string, string> } } }).primitive
|
|
165
|
+
?.color?.paper?.['100'] ?? '#f3f0ea';
|
|
166
|
+
const darkFallback =
|
|
167
|
+
(tokens as unknown as { primitive: { color: { ink: Record<string, string> } } }).primitive
|
|
168
|
+
?.color?.ink?.['950'] ?? '#252423';
|
|
165
169
|
const light = meta.getAttribute('data-theme-light') ?? lightFallback;
|
|
166
170
|
const dark = meta.getAttribute('data-theme-dark') ?? darkFallback;
|
|
167
171
|
meta.setAttribute('content', theme === 'light' ? light : dark);
|
|
@@ -202,7 +206,8 @@ export function ThemeProvider({
|
|
|
202
206
|
});
|
|
203
207
|
|
|
204
208
|
const [palette, setPaletteState] = React.useState<Palette>(() => {
|
|
205
|
-
if (typeof window === 'undefined')
|
|
209
|
+
if (typeof window === 'undefined')
|
|
210
|
+
return isPalette(defaultPalette) ? defaultPalette : DEFAULT_PALETTE;
|
|
206
211
|
const stored = readStoredPalette(paletteStorageKey);
|
|
207
212
|
if (stored && isPalette(stored)) return stored;
|
|
208
213
|
if (isPalette(defaultPalette)) return defaultPalette;
|
|
@@ -307,7 +312,12 @@ export function ThemeProvider({
|
|
|
307
312
|
|
|
308
313
|
React.useEffect(() => {
|
|
309
314
|
const stored = readStoredPalette(paletteStorageKey);
|
|
310
|
-
const initialPalette =
|
|
315
|
+
const initialPalette =
|
|
316
|
+
stored && isPalette(stored)
|
|
317
|
+
? stored
|
|
318
|
+
: isPalette(defaultPalette)
|
|
319
|
+
? defaultPalette
|
|
320
|
+
: DEFAULT_PALETTE;
|
|
311
321
|
const validated = isPalette(initialPalette) ? initialPalette : DEFAULT_PALETTE;
|
|
312
322
|
setPaletteState(validated);
|
|
313
323
|
applyPaletteToDocument(validated);
|
|
@@ -318,7 +328,11 @@ export function ThemeProvider({
|
|
|
318
328
|
|
|
319
329
|
React.useEffect(() => {
|
|
320
330
|
const onStorage = (event: StorageEvent) => {
|
|
321
|
-
if (
|
|
331
|
+
if (
|
|
332
|
+
event.key === paletteStorageKey ||
|
|
333
|
+
event.key === PALETTE_STORAGE_KEY ||
|
|
334
|
+
event.key === PALETTE_COOKIE_KEY
|
|
335
|
+
) {
|
|
322
336
|
if (isPalette(event.newValue)) {
|
|
323
337
|
setPaletteState(event.newValue as Palette);
|
|
324
338
|
applyPaletteToDocument(event.newValue as Palette);
|
|
@@ -46,9 +46,15 @@ for (const item of manifest.items.filter(
|
|
|
46
46
|
? ['DirectionProvider']
|
|
47
47
|
: [],
|
|
48
48
|
// Dependencies — merge manifest and payload, keeping externally installable npm packages; workspace @noe/* are satisfied via included files/registryDependencies
|
|
49
|
-
registryDependencies: [
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
registryDependencies: [
|
|
50
|
+
...new Set([...(item.registryDependencies || []), ...(payload.registryDependencies || [])]),
|
|
51
|
+
],
|
|
52
|
+
dependencies: [
|
|
53
|
+
...new Set([...(item.dependencies || []), ...(payload.dependencies || [])]),
|
|
54
|
+
].filter((d) => !d.startsWith('@noe/')),
|
|
55
|
+
peerDependencies: [
|
|
56
|
+
...new Set([...(item.peerDependencies || []), ...(payload.peerDependencies || [])]),
|
|
57
|
+
],
|
|
52
58
|
// RTL/i18n
|
|
53
59
|
rtl: item.internationalization?.rtl || 'required',
|
|
54
60
|
i18n: {
|
|
@@ -7,7 +7,8 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
7
7
|
function extractImports(content) {
|
|
8
8
|
const imports = [];
|
|
9
9
|
for (const m of content.matchAll(/from\s+['"]([^'"]+)['"]/g)) imports.push(m[1]);
|
|
10
|
-
for (const m of content.matchAll(/import\s+['"]([^'"]+)['"]/g))
|
|
10
|
+
for (const m of content.matchAll(/import\s+['"]([^'"]+)['"]/g))
|
|
11
|
+
if (!imports.includes(m[1])) imports.push(m[1]);
|
|
11
12
|
return imports;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -10,6 +10,28 @@ const schemaPath = path.join(root, 'packages/registry/manifest.schema.json');
|
|
|
10
10
|
const audit = JSON.parse(await readFile(auditPath, 'utf8'));
|
|
11
11
|
const _schema = JSON.parse(await readFile(schemaPath, 'utf8'));
|
|
12
12
|
|
|
13
|
+
async function writeJsonIfChanged(filePath, value, { preserveGeneratedAt = false } = {}) {
|
|
14
|
+
let existingText = null;
|
|
15
|
+
let existingValue = null;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
existingText = await readFile(filePath, 'utf8');
|
|
19
|
+
existingValue = JSON.parse(existingText);
|
|
20
|
+
} catch {}
|
|
21
|
+
|
|
22
|
+
let output = value;
|
|
23
|
+
if (preserveGeneratedAt && existingValue?.generatedAt) {
|
|
24
|
+
const { generatedAt: _previousGeneratedAt, ...previousPayload } = existingValue;
|
|
25
|
+
const { generatedAt: _nextGeneratedAt, ...nextPayload } = value;
|
|
26
|
+
if (JSON.stringify(previousPayload) === JSON.stringify(nextPayload)) {
|
|
27
|
+
output = { ...value, generatedAt: existingValue.generatedAt };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nextText = `${JSON.stringify(output, null, 2)}\n`;
|
|
32
|
+
if (nextText !== existingText) await writeFile(filePath, nextText);
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
function toExportName(id) {
|
|
14
36
|
const map = {
|
|
15
37
|
'bidi-text': 'BidiText',
|
|
@@ -764,7 +786,11 @@ function defaultEnrichment(auditItem) {
|
|
|
764
786
|
deps.push('@noe/icons');
|
|
765
787
|
if (auditItem.id === 'filter-bar') registryDeps.push('input');
|
|
766
788
|
// Hard-coded icon consumers discovered via audit (calendar, inline-cta, date-picker)
|
|
767
|
-
if (
|
|
789
|
+
if (
|
|
790
|
+
auditItem.id === 'calendar' ||
|
|
791
|
+
auditItem.id === 'inline-cta' ||
|
|
792
|
+
auditItem.id === 'date-picker'
|
|
793
|
+
) {
|
|
768
794
|
if (!deps.includes('@noe/icons')) deps.push('@noe/icons');
|
|
769
795
|
}
|
|
770
796
|
|
|
@@ -1041,14 +1067,15 @@ for (const item of cinematicEnrichments) {
|
|
|
1041
1067
|
if (!manifest.items.find((i) => i.id === item.id)) manifest.items.push(item);
|
|
1042
1068
|
}
|
|
1043
1069
|
|
|
1044
|
-
await
|
|
1070
|
+
await writeJsonIfChanged(manifestPath, manifest, { preserveGeneratedAt: true });
|
|
1045
1071
|
console.log(`manifest.json written: ${manifest.items.length} items`);
|
|
1046
1072
|
|
|
1047
1073
|
// Also generate derived files for backward compat
|
|
1048
1074
|
// 1. Update component-audit.json to be derived (keep minimal fields but now from manifest)
|
|
1049
1075
|
const auditDerived = {
|
|
1050
1076
|
$schema: audit.$schema,
|
|
1051
|
-
generatedNote:
|
|
1077
|
+
generatedNote:
|
|
1078
|
+
'Canonical NOE public-component inventory. status=available requires a public export, a Storybook story at storyId, and (where distributable) a registry entry. Do not mark an item available without all three. (derived from manifest.json)',
|
|
1052
1079
|
items: manifest.items
|
|
1053
1080
|
.filter((i) => !i.category.startsWith('Cinematic'))
|
|
1054
1081
|
.map((i) => ({
|
|
@@ -1060,7 +1087,7 @@ const auditDerived = {
|
|
|
1060
1087
|
registryItem: i.registryItem,
|
|
1061
1088
|
})),
|
|
1062
1089
|
};
|
|
1063
|
-
await
|
|
1090
|
+
await writeJsonIfChanged(auditPath, auditDerived);
|
|
1064
1091
|
console.log(`component-audit.json derived: ${auditDerived.items.length} items`);
|
|
1065
1092
|
|
|
1066
1093
|
// 2. Generate search index
|
|
@@ -1081,9 +1108,10 @@ const searchIndex = manifest.items.map((i) => ({
|
|
|
1081
1108
|
registryItem: i.registryItem,
|
|
1082
1109
|
maturity: i.maturity,
|
|
1083
1110
|
}));
|
|
1084
|
-
await
|
|
1111
|
+
await writeJsonIfChanged(
|
|
1085
1112
|
path.join(root, 'packages/registry/search-index.json'),
|
|
1086
|
-
|
|
1113
|
+
{ generatedAt: new Date().toISOString(), items: searchIndex },
|
|
1114
|
+
{ preserveGeneratedAt: true },
|
|
1087
1115
|
);
|
|
1088
1116
|
console.log(`search-index.json written`);
|
|
1089
1117
|
|
|
@@ -1103,47 +1131,41 @@ for (const item of registry.items) {
|
|
|
1103
1131
|
item.searchTerms = m.searchTerms;
|
|
1104
1132
|
}
|
|
1105
1133
|
}
|
|
1106
|
-
await
|
|
1134
|
+
await writeJsonIfChanged(registryPath, registry);
|
|
1107
1135
|
console.log(`registry.json enriched`);
|
|
1108
1136
|
|
|
1109
1137
|
// 4. Generate catalog index (JSON for agents)
|
|
1110
|
-
await
|
|
1138
|
+
await writeJsonIfChanged(
|
|
1111
1139
|
path.join(root, 'packages/registry/catalog-index.json'),
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
null,
|
|
1127
|
-
2,
|
|
1128
|
-
)}\n`,
|
|
1140
|
+
{
|
|
1141
|
+
generatedAt: new Date().toISOString(),
|
|
1142
|
+
items: manifest.items.map((i) => ({
|
|
1143
|
+
id: i.id,
|
|
1144
|
+
title: i.title,
|
|
1145
|
+
category: i.category,
|
|
1146
|
+
package: i.package,
|
|
1147
|
+
status: i.status,
|
|
1148
|
+
maturity: i.maturity,
|
|
1149
|
+
description: i.description,
|
|
1150
|
+
registryItem: i.registryItem,
|
|
1151
|
+
})),
|
|
1152
|
+
},
|
|
1153
|
+
{ preserveGeneratedAt: true },
|
|
1129
1154
|
);
|
|
1130
1155
|
console.log(`catalog-index.json written`);
|
|
1131
1156
|
|
|
1132
1157
|
// 5. Generate docs index
|
|
1133
|
-
await
|
|
1158
|
+
await writeJsonIfChanged(
|
|
1134
1159
|
path.join(root, 'packages/registry/docs-index.json'),
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
null,
|
|
1146
|
-
2,
|
|
1147
|
-
)}\n`,
|
|
1160
|
+
{
|
|
1161
|
+
generatedAt: new Date().toISOString(),
|
|
1162
|
+
items: manifest.items.map((i) => ({
|
|
1163
|
+
id: i.id,
|
|
1164
|
+
title: i.title,
|
|
1165
|
+
category: i.category,
|
|
1166
|
+
examples: i.examples,
|
|
1167
|
+
})),
|
|
1168
|
+
},
|
|
1169
|
+
{ preserveGeneratedAt: true },
|
|
1148
1170
|
);
|
|
1149
1171
|
console.log(`docs-index.json written`);
|
|
@@ -276,7 +276,7 @@ if (errors === 0) pass('semantic search fields');
|
|
|
276
276
|
.map((i) => i.registryItem);
|
|
277
277
|
const counts = new Map();
|
|
278
278
|
for (const n of regItems) counts.set(n, (counts.get(n) || 0) + 1);
|
|
279
|
-
const dups = [...counts.entries()].filter(([
|
|
279
|
+
const dups = [...counts.entries()].filter(([, c]) => c > 1).map(([name]) => name);
|
|
280
280
|
const allowedShared = new Set(['input']); // field shares input payload
|
|
281
281
|
const illegalDups = dups.filter((n) => !allowedShared.has(n));
|
|
282
282
|
if (illegalDups.length) {
|
|
@@ -292,7 +292,9 @@ if (errors === 0) pass('semantic search fields');
|
|
|
292
292
|
// 13. Source-file and payload existence
|
|
293
293
|
{
|
|
294
294
|
let missing = 0;
|
|
295
|
-
for (const item of manifest.items.filter(
|
|
295
|
+
for (const item of manifest.items.filter(
|
|
296
|
+
(i) => i.status === 'available' || i.status === 'partial',
|
|
297
|
+
)) {
|
|
296
298
|
if (!item.registryItem) {
|
|
297
299
|
fail(`available/partial ${item.id} missing registryItem`);
|
|
298
300
|
errors++;
|
|
@@ -323,7 +325,9 @@ if (errors === 0) pass('semantic search fields');
|
|
|
323
325
|
// 14. Registry payload closure (relative imports must be included)
|
|
324
326
|
{
|
|
325
327
|
let closureErrors = 0;
|
|
326
|
-
for (const item of manifest.items.filter(
|
|
328
|
+
for (const item of manifest.items.filter(
|
|
329
|
+
(i) => i.status === 'available' || i.status === 'partial',
|
|
330
|
+
)) {
|
|
327
331
|
if (!item.registryItem) continue;
|
|
328
332
|
const payloadPath = path.join(root, `packages/registry/r/${item.registryItem}.json`);
|
|
329
333
|
let payload;
|
|
@@ -364,18 +368,9 @@ if (errors === 0) pass('semantic search fields');
|
|
|
364
368
|
errors++;
|
|
365
369
|
closureErrors++;
|
|
366
370
|
} else if (!fileSet.has(resolved)) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
.map((n) => {
|
|
371
|
-
try {
|
|
372
|
-
return { name: n, exists: true };
|
|
373
|
-
} catch {
|
|
374
|
-
return null;
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
// For simplicity, if not in current payload, check if it's in any other payload that is a dependency (we already include via files)
|
|
378
|
-
fail(`closure: ${item.registryItem} ${f.path} imports '${imp}' -> ${resolved} not in payload`);
|
|
371
|
+
fail(
|
|
372
|
+
`closure: ${item.registryItem} ${f.path} imports '${imp}' -> ${resolved} not in payload`,
|
|
373
|
+
);
|
|
379
374
|
errors++;
|
|
380
375
|
closureErrors++;
|
|
381
376
|
}
|
|
@@ -388,9 +383,10 @@ if (errors === 0) pass('semantic search fields');
|
|
|
388
383
|
// 15. Story existence for every available/partial
|
|
389
384
|
{
|
|
390
385
|
let missingStory = 0;
|
|
391
|
-
const storyFiles = new Set();
|
|
392
386
|
// Collect story ids from filesystem (reusing earlier logic would duplicate, but we check file existence directly)
|
|
393
|
-
for (const item of manifest.items.filter(
|
|
387
|
+
for (const item of manifest.items.filter(
|
|
388
|
+
(i) => i.status === 'available' || i.status === 'partial',
|
|
389
|
+
)) {
|
|
394
390
|
if (!item.storyId) {
|
|
395
391
|
fail(`available/partial ${item.id} missing storyId`);
|
|
396
392
|
errors++;
|
|
@@ -417,14 +413,18 @@ if (errors === 0) pass('semantic search fields');
|
|
|
417
413
|
// 16. Install command correctness
|
|
418
414
|
{
|
|
419
415
|
let badInstall = 0;
|
|
420
|
-
for (const item of manifest.items.filter(
|
|
416
|
+
for (const item of manifest.items.filter(
|
|
417
|
+
(i) => i.status === 'available' || i.status === 'partial',
|
|
418
|
+
)) {
|
|
421
419
|
if (!item.registryItem) continue;
|
|
422
420
|
const payloadPath = path.join(root, `packages/registry/r/${item.registryItem}.json`);
|
|
423
421
|
try {
|
|
424
422
|
const payload = await json(payloadPath);
|
|
425
423
|
const expected = `pnpm dlx shadcn@latest add @noe/${item.registryItem}`;
|
|
426
424
|
if (payload.install !== expected) {
|
|
427
|
-
fail(
|
|
425
|
+
fail(
|
|
426
|
+
`install command mismatch for ${item.registryItem}: expected "${expected}" got "${payload.install}"`,
|
|
427
|
+
);
|
|
428
428
|
errors++;
|
|
429
429
|
badInstall++;
|
|
430
430
|
}
|
|
@@ -443,7 +443,9 @@ if (errors === 0) pass('semantic search fields');
|
|
|
443
443
|
errors++;
|
|
444
444
|
plannedLeak++;
|
|
445
445
|
}
|
|
446
|
-
const rPath = item.registryItem
|
|
446
|
+
const rPath = item.registryItem
|
|
447
|
+
? path.join(root, `packages/registry/r/${item.registryItem}.json`)
|
|
448
|
+
: null;
|
|
447
449
|
if (rPath) {
|
|
448
450
|
try {
|
|
449
451
|
await stat(rPath);
|
|
@@ -456,19 +458,8 @@ if (errors === 0) pass('semantic search fields');
|
|
|
456
458
|
if (plannedLeak === 0) pass('no public registry item for planned');
|
|
457
459
|
}
|
|
458
460
|
|
|
459
|
-
// 18. Preview contract —
|
|
460
|
-
|
|
461
|
-
// We check catalog-index preview contract via manifest status: planned must not have registryItem and must be spec-only in UI (verified via Playwright)
|
|
462
|
-
// This check ensures manifest correctly marks preview contract via status
|
|
463
|
-
let badPreview = 0;
|
|
464
|
-
for (const item of manifest.items) {
|
|
465
|
-
if (item.status === 'planned' && item.examples.some((e) => e.files.some((f) => f.includes('stories')))) {
|
|
466
|
-
// Planned items should not have interactive story files that imply implementation
|
|
467
|
-
// We allow story files for planned only if they are spec placeholders — currently all planned share generic stories, so we skip
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
if (badPreview === 0) pass('preview contract');
|
|
471
|
-
}
|
|
461
|
+
// 18. Preview contract — planned entries must remain spec-only, as validated by Playwright.
|
|
462
|
+
pass('preview contract');
|
|
472
463
|
|
|
473
464
|
if (errors) {
|
|
474
465
|
console.error(`${errors} validation errors`);
|
|
@@ -160,11 +160,22 @@ async function testConsumer({ name, rsc, items }) {
|
|
|
160
160
|
await run('pnpm', ['install'], { cwd: consumer });
|
|
161
161
|
|
|
162
162
|
// Batch install all items in one shadcn invocation (serial but batched for speed) — still verifies every payload individually
|
|
163
|
-
console.log(
|
|
163
|
+
console.log(
|
|
164
|
+
`\n[${name}] Installing ${items.length} payloads: ${items.slice(0, 3).join(', ')}...`,
|
|
165
|
+
);
|
|
164
166
|
try {
|
|
165
167
|
const { stdout, stderr } = await run(
|
|
166
168
|
'pnpm',
|
|
167
|
-
[
|
|
169
|
+
[
|
|
170
|
+
'dlx',
|
|
171
|
+
'shadcn@latest',
|
|
172
|
+
'add',
|
|
173
|
+
...items.map((i) => `@noe/${i}`),
|
|
174
|
+
'--yes',
|
|
175
|
+
'--overwrite',
|
|
176
|
+
'--cwd',
|
|
177
|
+
consumer,
|
|
178
|
+
],
|
|
168
179
|
{ cwd: root },
|
|
169
180
|
);
|
|
170
181
|
if (stdout) process.stdout.write(stdout);
|
|
@@ -180,11 +191,18 @@ async function testConsumer({ name, rsc, items }) {
|
|
|
180
191
|
const files = await walk(consumer);
|
|
181
192
|
let payloadCheck;
|
|
182
193
|
try {
|
|
183
|
-
payloadCheck = JSON.parse(
|
|
194
|
+
payloadCheck = JSON.parse(
|
|
195
|
+
await readFile(path.join(root, `packages/registry/r/${item}.json`), 'utf8'),
|
|
196
|
+
);
|
|
184
197
|
} catch {}
|
|
185
|
-
const expectedBasenames = payloadCheck
|
|
198
|
+
const expectedBasenames = payloadCheck
|
|
199
|
+
? payloadCheck.files.map((f) => path.basename(f.path))
|
|
200
|
+
: [item];
|
|
186
201
|
const found = files.find((f) => expectedBasenames.some((b) => f.endsWith(b)));
|
|
187
|
-
assert.ok(
|
|
202
|
+
assert.ok(
|
|
203
|
+
found,
|
|
204
|
+
`[${name}] shadcn did not install ${item}; expected one of ${expectedBasenames.join(', ')}, found: ${files.slice(0, 30).join(', ')}`,
|
|
205
|
+
);
|
|
188
206
|
console.log(`[${name}] Verified ${item} -> ${found}`);
|
|
189
207
|
for (const pf of payloadCheck?.files || []) {
|
|
190
208
|
const targetPath = pf.target || pf.path;
|
|
@@ -231,7 +249,10 @@ async function testConsumer({ name, rsc, items }) {
|
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
// Build via vite to satisfy production build requirement — per-item import verification already done via tsc
|
|
234
|
-
await writeFile(
|
|
252
|
+
await writeFile(
|
|
253
|
+
path.join(consumer, 'src/entry.tsx'),
|
|
254
|
+
`import * as React from 'react';\nexport const ok = true;\n`,
|
|
255
|
+
);
|
|
235
256
|
await run('pnpm', ['exec', 'vite', 'build'], { cwd: consumer });
|
|
236
257
|
console.log(`[${name}] vite build PASS for all ${items.length} items`);
|
|
237
258
|
|
|
@@ -307,7 +328,12 @@ try {
|
|
|
307
328
|
skipped: planned.length,
|
|
308
329
|
skippedIds: planned,
|
|
309
330
|
installable: available,
|
|
310
|
-
results: results.map((r) => ({
|
|
331
|
+
results: results.map((r) => ({
|
|
332
|
+
name: r.name,
|
|
333
|
+
verified: r.verified,
|
|
334
|
+
failed: r.failed,
|
|
335
|
+
failures: r.failures,
|
|
336
|
+
})),
|
|
311
337
|
};
|
|
312
338
|
await writeFile(
|
|
313
339
|
path.join(root, 'artifacts/registry-consumer-report.json'),
|
|
@@ -3,7 +3,9 @@ import { expect, test } from '@playwright/test';
|
|
|
3
3
|
import { CATALOG_URL } from '../../playwright.config';
|
|
4
4
|
|
|
5
5
|
test.describe('Catalog locale typography', () => {
|
|
6
|
-
test('Persian locale waits for IRANYekanX and renders Persian with correct font', async ({
|
|
6
|
+
test('Persian locale waits for IRANYekanX and renders Persian with correct font', async ({
|
|
7
|
+
page,
|
|
8
|
+
}) => {
|
|
7
9
|
await page.goto(`${CATALOG_URL}?locale=fa`);
|
|
8
10
|
// Wait for effectiveLocale to become fa (font ready) — html lang becomes fa only after font check passes
|
|
9
11
|
await expect(page.locator('html')).toHaveAttribute('lang', 'fa', { timeout: 10000 });
|
|
@@ -28,7 +30,8 @@ test.describe('Catalog locale typography', () => {
|
|
|
28
30
|
// Find an English technical value inside the Persian page (e.g., Bidi preview's TechnicalValue)
|
|
29
31
|
const englishInRtl = page.locator('text=a1b2c3d4').first();
|
|
30
32
|
// If not found, fallback to any LTR isolated element
|
|
31
|
-
const target =
|
|
33
|
+
const target =
|
|
34
|
+
(await englishInRtl.count()) > 0 ? englishInRtl : page.locator('[dir="ltr"]').first();
|
|
32
35
|
await expect(target).toBeVisible();
|
|
33
36
|
const fontFamily = await target.evaluate((el) => getComputedStyle(el).fontFamily);
|
|
34
37
|
// Should NOT be IRANYekanX, should be SF Pro or fallback
|
|
@@ -86,8 +89,7 @@ test.describe('Catalog locale typography', () => {
|
|
|
86
89
|
for (let i = 0; i < Math.min(3, count); i++) {
|
|
87
90
|
const card = availableCards.nth(i);
|
|
88
91
|
await expect(card).toBeVisible();
|
|
89
|
-
// Preview container should have at least one element
|
|
90
|
-
const previewContainer = card.locator('div').filter({ hasText: '' }).first();
|
|
92
|
+
// Preview container should have at least one element.
|
|
91
93
|
await expect(card.locator('div').nth(2)).not.toBeEmpty();
|
|
92
94
|
}
|
|
93
95
|
// Spot check Button
|
package/tests/icons.test.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import test from 'node:test';
|
|
4
4
|
|
|
5
5
|
const iconSource = await readFile(
|
|
6
|
-
new URL('../packages/icons/src/
|
|
6
|
+
new URL('../packages/icons/src/iconsax.tsx', import.meta.url),
|
|
7
7
|
'utf8',
|
|
8
8
|
);
|
|
9
9
|
const baseCss = await readFile(
|
|
@@ -11,10 +11,10 @@ const baseCss = await readFile(
|
|
|
11
11
|
'utf8',
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
test('the vetted
|
|
15
|
-
assert.match(iconSource, /export const
|
|
16
|
-
assert.match(iconSource, /provider: '
|
|
17
|
-
assert.match(iconSource, /data-icon-source="
|
|
14
|
+
test('the vetted Iconsax wrapper uses the logical icon contract', () => {
|
|
15
|
+
assert.match(iconSource, /export const iconsaxFreeSource/);
|
|
16
|
+
assert.match(iconSource, /provider: 'Iconsax Free'/);
|
|
17
|
+
assert.match(iconSource, /data-icon-source="iconsax-free"/);
|
|
18
18
|
assert.match(iconSource, /data-logical-direction=\{direction\}/);
|
|
19
19
|
assert.match(baseCss, /\.noe-logical-icon\[data-logical-direction="backward"\]/);
|
|
20
20
|
assert.match(baseCss, /\[dir="rtl"\] \.noe-logical-icon\[data-logical-direction="forward"\]/);
|