noecosystem-design 0.1.3 → 0.2.1
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 +3 -3
- package/apps/storybook/stories/catalog/card.stories.tsx +13 -0
- 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/components.md +5 -0
- package/docs/design-system/foundations.md +9 -0
- package/docs/design-system/rtl.md +1 -1
- package/docs/design-system/surface-policy.md +59 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/packages/design-tokens/src/base.css +1 -1
- package/packages/design-tokens/src/tokens.css +12 -12
- package/packages/design-tokens/src/tokens.json +8 -8
- package/packages/design-tokens/src/tokens.mjs +8 -8
- 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 +2 -2
- package/packages/registry/component-audit.json +1 -1
- package/packages/registry/docs-index.json +1 -1
- package/packages/registry/manifest.json +27 -14
- package/packages/registry/r/calendar.json +12 -1
- package/packages/registry/r/card.json +11 -5
- package/packages/registry/r/date-picker.json +12 -1
- package/packages/registry/r/icon-system.json +6 -1
- package/packages/registry/r/inline-cta.json +6 -1
- package/packages/registry/r/pagination.json +12 -1
- package/packages/registry/registry.json +8 -2
- package/packages/registry/search-index.json +21 -8
- package/packages/ui/src/card.tsx +14 -2
- package/packages/ui/src/theme-provider.browser.test.tsx +3 -2
- package/scripts/generate-manifest.mjs +92 -39
- package/scripts/validate-manifest.mjs +3 -29
- package/tests/e2e/catalog-typography.spec.ts +1 -2
- package/tests/icons.test.mjs +5 -5
- package/tests/tokens.test.mjs +14 -0
- 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/src/card.tsx
CHANGED
|
@@ -2,13 +2,25 @@ import type * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { cn } from './lib/cn';
|
|
4
4
|
|
|
5
|
-
export
|
|
5
|
+
export type CardAppearance = 'raised' | 'outlined';
|
|
6
|
+
|
|
7
|
+
export interface CardProps extends React.ComponentProps<'section'> {
|
|
8
|
+
/**
|
|
9
|
+
* Raised surfaces are separated from the page by a semantic surface tone only.
|
|
10
|
+
* Use outlined only where a visible boundary conveys data-entry or selection state.
|
|
11
|
+
*/
|
|
12
|
+
appearance?: CardAppearance;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Card({ appearance = 'raised', className, ...props }: CardProps) {
|
|
6
16
|
return (
|
|
7
17
|
<section
|
|
8
18
|
className={cn(
|
|
9
|
-
'flex flex-col overflow-hidden rounded-lg
|
|
19
|
+
'flex flex-col overflow-hidden rounded-lg bg-surface-raised text-foreground',
|
|
20
|
+
appearance === 'outlined' && 'border border-border',
|
|
10
21
|
className,
|
|
11
22
|
)}
|
|
23
|
+
data-appearance={appearance}
|
|
12
24
|
data-slot="card"
|
|
13
25
|
{...props}
|
|
14
26
|
/>
|
|
@@ -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
|
|
|
@@ -13,13 +12,14 @@ function PaletteProbe({ storageKey }: { storageKey?: string }) {
|
|
|
13
12
|
<button type="button" onClick={() => setPalette('graphite' as never)}>
|
|
14
13
|
set graphite
|
|
15
14
|
</button>
|
|
16
|
-
{storageKey
|
|
15
|
+
{storageKey ? <span data-testid="storageKey">{storageKey}</span> : null}
|
|
17
16
|
</div>
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
test('default Graphite with no preference', async () => {
|
|
22
21
|
localStorage.clear();
|
|
22
|
+
// biome-ignore lint/suspicious/noDocumentCookie: Test setup must exercise the cookie fallback path.
|
|
23
23
|
document.cookie = 'noecosystem-palette=; Max-Age=0; path=/';
|
|
24
24
|
document.documentElement.removeAttribute('data-noe-palette');
|
|
25
25
|
const screen = await render(
|
|
@@ -45,6 +45,7 @@ test('valid stored preference restoration', async () => {
|
|
|
45
45
|
|
|
46
46
|
test('invalid storage and cookie fallback to Graphite', async () => {
|
|
47
47
|
localStorage.setItem('noecosystem-palette', 'invalid-palette');
|
|
48
|
+
// biome-ignore lint/suspicious/noDocumentCookie: This test needs an invalid cookie value for fallback coverage.
|
|
48
49
|
document.cookie = 'noecosystem-palette=invalid-palette; path=/';
|
|
49
50
|
const screen = await render(
|
|
50
51
|
<ThemeProvider>
|
|
@@ -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',
|
|
@@ -196,6 +218,41 @@ const enrichments = {
|
|
|
196
218
|
notes: 'Variants parity full; loading and destructive covered',
|
|
197
219
|
},
|
|
198
220
|
},
|
|
221
|
+
card: {
|
|
222
|
+
description:
|
|
223
|
+
'Flat raised section primitive. It separates content from its page canvas through semantic surface tone, spacing and radius—not decorative borders or shadows. Use outlined only where a boundary conveys selection or data-entry state.',
|
|
224
|
+
capabilityAliases: ['flat card', 'raised surface', 'borderless section', 'outlined card'],
|
|
225
|
+
searchTerms: [
|
|
226
|
+
'card',
|
|
227
|
+
'raised surface',
|
|
228
|
+
'flat section',
|
|
229
|
+
'borderless panel',
|
|
230
|
+
'outlined boundary',
|
|
231
|
+
],
|
|
232
|
+
useWhen: [
|
|
233
|
+
'grouping a top-level application region with a semantic raised surface',
|
|
234
|
+
'separating sections by color tone rather than elevation or decorative chrome',
|
|
235
|
+
'using appearance="outlined" only for selection, data-entry or comparison boundaries',
|
|
236
|
+
],
|
|
237
|
+
doNotUseWhen: [
|
|
238
|
+
'adding a nested card solely to create visual depth',
|
|
239
|
+
'using an outlined boundary when spacing and surface tone already express grouping',
|
|
240
|
+
],
|
|
241
|
+
alternatives: ['SectionHeader', 'Field'],
|
|
242
|
+
variants: ['raised', 'outlined'],
|
|
243
|
+
states: ['default'],
|
|
244
|
+
tokenContracts: ['color.background', 'color.surface', 'color.surfaceRaised', 'radius.lg'],
|
|
245
|
+
accessibility: {
|
|
246
|
+
keyboard: [],
|
|
247
|
+
announcements: [],
|
|
248
|
+
focusBehavior: ['not focusable unless its content contains an interactive control'],
|
|
249
|
+
},
|
|
250
|
+
internationalization: {
|
|
251
|
+
rtl: 'required',
|
|
252
|
+
bidiNotes: ['logical spacing; no directional layout override'],
|
|
253
|
+
},
|
|
254
|
+
relatedPatterns: ['app-frame', 'dashboard-shell', 'section-header'],
|
|
255
|
+
},
|
|
199
256
|
dialog: {
|
|
200
257
|
description:
|
|
201
258
|
'Direction-preserving modal primitive with native focus semantics and portal propagation. Share foundation with Modal/Slideout.',
|
|
@@ -804,7 +861,7 @@ function defaultEnrichment(auditItem) {
|
|
|
804
861
|
dependencies: deps,
|
|
805
862
|
registryDependencies: registryDeps,
|
|
806
863
|
peerDependencies: peerDeps,
|
|
807
|
-
propsSummary: { label: 'string | ReactNode', children: 'ReactNode' },
|
|
864
|
+
propsSummary: base.propsSummary ?? { label: 'string | ReactNode', children: 'ReactNode' },
|
|
808
865
|
variants,
|
|
809
866
|
sizes,
|
|
810
867
|
slots,
|
|
@@ -1045,14 +1102,15 @@ for (const item of cinematicEnrichments) {
|
|
|
1045
1102
|
if (!manifest.items.find((i) => i.id === item.id)) manifest.items.push(item);
|
|
1046
1103
|
}
|
|
1047
1104
|
|
|
1048
|
-
await
|
|
1105
|
+
await writeJsonIfChanged(manifestPath, manifest, { preserveGeneratedAt: true });
|
|
1049
1106
|
console.log(`manifest.json written: ${manifest.items.length} items`);
|
|
1050
1107
|
|
|
1051
1108
|
// Also generate derived files for backward compat
|
|
1052
1109
|
// 1. Update component-audit.json to be derived (keep minimal fields but now from manifest)
|
|
1053
1110
|
const auditDerived = {
|
|
1054
1111
|
$schema: audit.$schema,
|
|
1055
|
-
generatedNote:
|
|
1112
|
+
generatedNote:
|
|
1113
|
+
'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)',
|
|
1056
1114
|
items: manifest.items
|
|
1057
1115
|
.filter((i) => !i.category.startsWith('Cinematic'))
|
|
1058
1116
|
.map((i) => ({
|
|
@@ -1064,7 +1122,7 @@ const auditDerived = {
|
|
|
1064
1122
|
registryItem: i.registryItem,
|
|
1065
1123
|
})),
|
|
1066
1124
|
};
|
|
1067
|
-
await
|
|
1125
|
+
await writeJsonIfChanged(auditPath, auditDerived);
|
|
1068
1126
|
console.log(`component-audit.json derived: ${auditDerived.items.length} items`);
|
|
1069
1127
|
|
|
1070
1128
|
// 2. Generate search index
|
|
@@ -1085,9 +1143,10 @@ const searchIndex = manifest.items.map((i) => ({
|
|
|
1085
1143
|
registryItem: i.registryItem,
|
|
1086
1144
|
maturity: i.maturity,
|
|
1087
1145
|
}));
|
|
1088
|
-
await
|
|
1146
|
+
await writeJsonIfChanged(
|
|
1089
1147
|
path.join(root, 'packages/registry/search-index.json'),
|
|
1090
|
-
|
|
1148
|
+
{ generatedAt: new Date().toISOString(), items: searchIndex },
|
|
1149
|
+
{ preserveGeneratedAt: true },
|
|
1091
1150
|
);
|
|
1092
1151
|
console.log(`search-index.json written`);
|
|
1093
1152
|
|
|
@@ -1107,47 +1166,41 @@ for (const item of registry.items) {
|
|
|
1107
1166
|
item.searchTerms = m.searchTerms;
|
|
1108
1167
|
}
|
|
1109
1168
|
}
|
|
1110
|
-
await
|
|
1169
|
+
await writeJsonIfChanged(registryPath, registry);
|
|
1111
1170
|
console.log(`registry.json enriched`);
|
|
1112
1171
|
|
|
1113
1172
|
// 4. Generate catalog index (JSON for agents)
|
|
1114
|
-
await
|
|
1173
|
+
await writeJsonIfChanged(
|
|
1115
1174
|
path.join(root, 'packages/registry/catalog-index.json'),
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
null,
|
|
1131
|
-
2,
|
|
1132
|
-
)}\n`,
|
|
1175
|
+
{
|
|
1176
|
+
generatedAt: new Date().toISOString(),
|
|
1177
|
+
items: manifest.items.map((i) => ({
|
|
1178
|
+
id: i.id,
|
|
1179
|
+
title: i.title,
|
|
1180
|
+
category: i.category,
|
|
1181
|
+
package: i.package,
|
|
1182
|
+
status: i.status,
|
|
1183
|
+
maturity: i.maturity,
|
|
1184
|
+
description: i.description,
|
|
1185
|
+
registryItem: i.registryItem,
|
|
1186
|
+
})),
|
|
1187
|
+
},
|
|
1188
|
+
{ preserveGeneratedAt: true },
|
|
1133
1189
|
);
|
|
1134
1190
|
console.log(`catalog-index.json written`);
|
|
1135
1191
|
|
|
1136
1192
|
// 5. Generate docs index
|
|
1137
|
-
await
|
|
1193
|
+
await writeJsonIfChanged(
|
|
1138
1194
|
path.join(root, 'packages/registry/docs-index.json'),
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
null,
|
|
1150
|
-
2,
|
|
1151
|
-
)}\n`,
|
|
1195
|
+
{
|
|
1196
|
+
generatedAt: new Date().toISOString(),
|
|
1197
|
+
items: manifest.items.map((i) => ({
|
|
1198
|
+
id: i.id,
|
|
1199
|
+
title: i.title,
|
|
1200
|
+
category: i.category,
|
|
1201
|
+
examples: i.examples,
|
|
1202
|
+
})),
|
|
1203
|
+
},
|
|
1204
|
+
{ preserveGeneratedAt: true },
|
|
1152
1205
|
);
|
|
1153
1206
|
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) {
|
|
@@ -368,17 +368,6 @@ if (errors === 0) pass('semantic search fields');
|
|
|
368
368
|
errors++;
|
|
369
369
|
closureErrors++;
|
|
370
370
|
} else if (!fileSet.has(resolved)) {
|
|
371
|
-
// Allow if resolved is provided via registryDependencies payload (check if in dep payload)
|
|
372
|
-
const depPayloads = (payload.registryDependencies || [])
|
|
373
|
-
.map((d) => d.replace('@noe/', ''))
|
|
374
|
-
.map((n) => {
|
|
375
|
-
try {
|
|
376
|
-
return { name: n, exists: true };
|
|
377
|
-
} catch {
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
// For simplicity, if not in current payload, check if it's in any other payload that is a dependency (we already include via files)
|
|
382
371
|
fail(
|
|
383
372
|
`closure: ${item.registryItem} ${f.path} imports '${imp}' -> ${resolved} not in payload`,
|
|
384
373
|
);
|
|
@@ -394,7 +383,6 @@ if (errors === 0) pass('semantic search fields');
|
|
|
394
383
|
// 15. Story existence for every available/partial
|
|
395
384
|
{
|
|
396
385
|
let missingStory = 0;
|
|
397
|
-
const storyFiles = new Set();
|
|
398
386
|
// Collect story ids from filesystem (reusing earlier logic would duplicate, but we check file existence directly)
|
|
399
387
|
for (const item of manifest.items.filter(
|
|
400
388
|
(i) => i.status === 'available' || i.status === 'partial',
|
|
@@ -470,22 +458,8 @@ if (errors === 0) pass('semantic search fields');
|
|
|
470
458
|
if (plannedLeak === 0) pass('no public registry item for planned');
|
|
471
459
|
}
|
|
472
460
|
|
|
473
|
-
// 18. Preview contract —
|
|
474
|
-
|
|
475
|
-
// We check catalog-index preview contract via manifest status: planned must not have registryItem and must be spec-only in UI (verified via Playwright)
|
|
476
|
-
// This check ensures manifest correctly marks preview contract via status
|
|
477
|
-
let badPreview = 0;
|
|
478
|
-
for (const item of manifest.items) {
|
|
479
|
-
if (
|
|
480
|
-
item.status === 'planned' &&
|
|
481
|
-
item.examples.some((e) => e.files.some((f) => f.includes('stories')))
|
|
482
|
-
) {
|
|
483
|
-
// Planned items should not have interactive story files that imply implementation
|
|
484
|
-
// We allow story files for planned only if they are spec placeholders — currently all planned share generic stories, so we skip
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (badPreview === 0) pass('preview contract');
|
|
488
|
-
}
|
|
461
|
+
// 18. Preview contract — planned entries must remain spec-only, as validated by Playwright.
|
|
462
|
+
pass('preview contract');
|
|
489
463
|
|
|
490
464
|
if (errors) {
|
|
491
465
|
console.error(`${errors} validation errors`);
|
|
@@ -89,8 +89,7 @@ test.describe('Catalog locale typography', () => {
|
|
|
89
89
|
for (let i = 0; i < Math.min(3, count); i++) {
|
|
90
90
|
const card = availableCards.nth(i);
|
|
91
91
|
await expect(card).toBeVisible();
|
|
92
|
-
// Preview container should have at least one element
|
|
93
|
-
const previewContainer = card.locator('div').filter({ hasText: '' }).first();
|
|
92
|
+
// Preview container should have at least one element.
|
|
94
93
|
await expect(card.locator('div').nth(2)).not.toBeEmpty();
|
|
95
94
|
}
|
|
96
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"\]/);
|
package/tests/tokens.test.mjs
CHANGED
|
@@ -22,3 +22,17 @@ test('token CSS includes theme and reduced-motion contracts', async () => {
|
|
|
22
22
|
assert.match(css, /--noe-primitive-color-paper-100:\s*#[0-9a-f]{6}/i);
|
|
23
23
|
assert.match(css, /--noe-primitive-color-mint-100:\s*#[0-9a-f]{6}/i);
|
|
24
24
|
});
|
|
25
|
+
|
|
26
|
+
test('surface elevation is intentionally shadow-free in both themes', async () => {
|
|
27
|
+
assert.equal(tokens.shadow.light.raised, 'none');
|
|
28
|
+
assert.equal(tokens.shadow.light.overlay, 'none');
|
|
29
|
+
assert.equal(tokens.shadow.dark.raised, 'none');
|
|
30
|
+
assert.equal(tokens.shadow.dark.overlay, 'none');
|
|
31
|
+
|
|
32
|
+
const css = await readFile(
|
|
33
|
+
new URL('../packages/design-tokens/src/tokens.css', import.meta.url),
|
|
34
|
+
'utf8',
|
|
35
|
+
);
|
|
36
|
+
assert.match(css, /\[data-theme="light"\][\s\S]*?--noe-shadow-raised:\s*none/);
|
|
37
|
+
assert.match(css, /\[data-theme="dark"\][\s\S]*?--noe-shadow-overlay:\s*none/);
|
|
38
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|