@santi020k/lumen 2.0.0 → 3.0.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 CHANGED
@@ -68,6 +68,9 @@ diagnostics are scoped to the nearest package boundary, generated build trees ar
68
68
  application-controlled Astro `Toggle` instances do not require the shared runtime. It also suggests
69
69
  matching Lumen primitives for likely hand-built dropdown, theme, dialog-focus, and keyboard-menu
70
70
  behavior. These suggestions stay advisory because product-specific composition can be intentional.
71
+ Publishable shared UI libraries are not required to load global styles. Private wrapper packages
72
+ can declare `"lumen": { "styleOwnership": "consumer" }` when their applications own adapter CSS,
73
+ or `"application"` when the package itself owns setup.
71
74
  Use `--json` for CI and rollout automation. `lumen init --framework <astro|react|elements>
72
75
  [--tailwind]` prints the canonical non-destructive setup.
73
76
 
package/dist/cli.js CHANGED
@@ -91,6 +91,29 @@ const getRegistryItemComponents = (item) => 'components' in item && Array.isArra
91
91
  const getRegistryItemDependencies = (item) => 'dependencies' in item && item.dependencies.length > 0 ?
92
92
  `dependencies: ${item.dependencies.join(', ')}` :
93
93
  undefined;
94
+ const formatContractProp = (prop) => `${prop.name}: ${prop.type}${prop.required ? ' (required)' : ''}${prop.default ? ` = ${prop.default}` : ''}`;
95
+ const formatContractList = (label, values, separator) => values?.length ? `${label}: ${values.join(separator)}` : undefined;
96
+ const getRegistryItemContract = (item) => {
97
+ if (item.type !== 'component')
98
+ return [];
99
+ const contract = item.contract;
100
+ if (!contract)
101
+ return [];
102
+ const optionalLines = [
103
+ formatContractList('props', contract.props?.map(formatContractProp), '; '),
104
+ formatContractList('composition', contract.composition, ' '),
105
+ formatContractList('events', contract.events, ', '),
106
+ formatContractList('slots', contract.slots, ', '),
107
+ contract.runtime ? `runtime: ${contract.runtime}` : undefined,
108
+ formatContractList('styling parts', contract.stylingParts, ', '),
109
+ formatContractList('tokens', contract.tokens, ', ')
110
+ ].filter((line) => Boolean(line));
111
+ return [
112
+ ...Object.entries(contract.imports).map(([framework, value]) => `import (${framework}): ${value}`),
113
+ ...Object.entries(contract.usage).map(([framework, value]) => `usage (${framework}): ${value}`),
114
+ ...optionalLines
115
+ ];
116
+ };
94
117
  const formatItem = (itemName, registry = lumenRegistry) => {
95
118
  const item = getLumenRegistryItem(itemName, registry);
96
119
  if (!item) {
@@ -102,7 +125,8 @@ const formatItem = (itemName, registry = lumenRegistry) => {
102
125
  getRegistryItemDescription(item),
103
126
  getRegistryItemCategory(item),
104
127
  getRegistryItemComponents(item),
105
- getRegistryItemDependencies(item)
128
+ getRegistryItemDependencies(item),
129
+ ...getRegistryItemContract(item)
106
130
  ];
107
131
  const files = getRegistryItemFiles(item);
108
132
  if (files) {
@@ -260,8 +284,10 @@ const run = async () => {
260
284
  break;
261
285
  }
262
286
  case 'show': {
263
- if (name)
264
- output = formatItem(name, registry);
287
+ if (name) {
288
+ const item = getLumenRegistryItem(name, registry);
289
+ output = json && item ? JSON.stringify(item, undefined, 2) : formatItem(name, registry);
290
+ }
265
291
  break;
266
292
  }
267
293
  }
@@ -1 +1 @@
1
- {"version":3,"file":"integration-diagnostics.d.ts","sourceRoot":"","sources":["../src/integration-diagnostics.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,OAAO,CAAA;AAE1D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,uBAAuB,CAAA;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,QAAQ,EAAE,sBAAsB,EAAE,CAAA;IAClC,UAAU,EAAE,CAAC,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC,EAAE,CAAA;IAC9C,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B;AA8eD,eAAO,MAAM,uBAAuB,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,qBAAqB,CA8C/F,CAAA;AAED,eAAO,MAAM,sBAAsB,GAAI,QAAQ,qBAAqB,KAAG,MAWtE,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,EAAE,kBAAgB,KAAG,MAsC9F,CAAA"}
1
+ {"version":3,"file":"integration-diagnostics.d.ts","sourceRoot":"","sources":["../src/integration-diagnostics.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,OAAO,CAAA;AAE1D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,uBAAuB,CAAA;CAClC;AAED,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,EAAE,MAAM,CAAA;IAC1B,QAAQ,EAAE,sBAAsB,EAAE,CAAA;IAClC,UAAU,EAAE,CAAC,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC,EAAE,CAAA;IAC9C,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,EAAE,CAAA;CAC5B;AAowBD,eAAO,MAAM,uBAAuB,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,qBAAqB,CA4D/F,CAAA;AAED,eAAO,MAAM,sBAAsB,GAAI,QAAQ,qBAAqB,KAAG,MAWtE,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,WAAW,OAAO,GAAG,UAAU,GAAG,OAAO,EAAE,kBAAgB,KAAG,MAsC9F,CAAA"}
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readdir, readFile, stat } from 'node:fs/promises';
3
3
  import { extname, join, relative, resolve, sep } from 'node:path';
4
- import { lumenComponentBehavior, lumenGlobalBehaviors, lumenStylingContracts } from '@santi020k/lumen-core';
4
+ import { lumenComponentBehavior, lumenComponentNames, lumenGlobalBehaviors, lumenStylingContracts } from '@santi020k/lumen-core';
5
5
  const sourceExtensions = new Set(['.astro', '.css', '.js', '.jsx', '.mjs', '.ts', '.tsx']);
6
6
  const ignoredDirectories = new Set([
7
7
  '.astro',
@@ -19,7 +19,15 @@ const ignoredDirectories = new Set([
19
19
  'node_modules',
20
20
  'out'
21
21
  ]);
22
- const publicRootSlots = new Set(Object.values(lumenStylingContracts).flatMap(contract => [contract.rootSlot, ...contract.parts]));
22
+ const stylingContracts = lumenStylingContracts;
23
+ const publicRootSlots = new Set(Object.values(stylingContracts).flatMap(contract => [contract.rootSlot, ...contract.parts]));
24
+ const componentClassName = (name) => name
25
+ .replace(/([a-z\d])([A-Z])/gu, '$1-$2')
26
+ .toLowerCase();
27
+ const componentByClassName = new Map(lumenComponentNames.map((name) => [
28
+ componentClassName(name),
29
+ name
30
+ ]));
23
31
  const discoverProject = async (root) => {
24
32
  const packageRoots = [];
25
33
  const sourceFiles = [];
@@ -86,7 +94,41 @@ const getFrameworks = (sources) => [
86
94
  const getBoundaryRoot = (file, packageRoots, fallback) => packageRoots
87
95
  .filter(packageRoot => file === packageRoot || file.startsWith(`${packageRoot}${sep}`))
88
96
  .sort((left, right) => right.length - left.length)[0] ?? fallback;
89
- const createBoundaries = (root, packageRoots, sources) => {
97
+ const readPackageManifest = async (root) => {
98
+ try {
99
+ return JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ };
105
+ const isPublishablePackageBoundary = (repositoryPath, manifest) => repositoryPath.includes('packages') && manifest?.private !== true;
106
+ const hasApplicationSource = (boundaryRoot, sources) => sources.some(item => /(?:^|\/)(?:app|pages|routes|layouts?)(?:\/|$)/iu.test(relative(boundaryRoot, item.file)));
107
+ const hasApplicationScript = (manifest) => {
108
+ if (manifest?.private !== true)
109
+ return false;
110
+ return Boolean(manifest.scripts?.dev ?? manifest.scripts?.start);
111
+ };
112
+ const isWorkspaceBoundary = (repositoryRoot, boundaryRoot, packageRootCount) => boundaryRoot === repositoryRoot && packageRootCount > 1;
113
+ const getFallbackBoundaryKind = (manifest) => manifest?.private === false ? 'library' : 'application';
114
+ const classifyBoundary = (repositoryRoot, boundaryRoot, manifest, sources, packageRootCount) => {
115
+ const declaredKind = manifest?.lumen?.integrationBoundary;
116
+ if (declaredKind)
117
+ return declaredKind;
118
+ const repositoryPath = relative(repositoryRoot, boundaryRoot).split(sep);
119
+ if (repositoryPath.includes('apps'))
120
+ return 'application';
121
+ if (isPublishablePackageBoundary(repositoryPath, manifest))
122
+ return 'library';
123
+ if (hasApplicationSource(boundaryRoot, sources))
124
+ return 'application';
125
+ if (hasApplicationScript(manifest))
126
+ return 'application';
127
+ if (isWorkspaceBoundary(repositoryRoot, boundaryRoot, packageRootCount))
128
+ return 'workspace';
129
+ return getFallbackBoundaryKind(manifest);
130
+ };
131
+ const createBoundaries = async (root, packageRoots, sources) => {
90
132
  const grouped = new Map();
91
133
  for (const source of sources) {
92
134
  const boundaryRoot = getBoundaryRoot(source.file, packageRoots, root);
@@ -94,11 +136,43 @@ const createBoundaries = (root, packageRoots, sources) => {
94
136
  entries.push(source);
95
137
  grouped.set(boundaryRoot, entries);
96
138
  }
97
- return [...grouped].map(([boundaryRoot, boundarySources]) => ({
98
- root: boundaryRoot,
99
- sources: boundarySources
139
+ return Promise.all([...grouped].map(async ([boundaryRoot, boundarySources]) => {
140
+ const manifest = await readPackageManifest(boundaryRoot);
141
+ const dependencies = [
142
+ ...Object.keys(manifest?.dependencies ?? {}),
143
+ ...Object.keys(manifest?.devDependencies ?? {}),
144
+ ...Object.keys(manifest?.peerDependencies ?? {})
145
+ ];
146
+ return {
147
+ dependencies,
148
+ kind: classifyBoundary(root, boundaryRoot, manifest, boundarySources, packageRoots.length),
149
+ ...(manifest?.name ? { packageName: manifest.name } : {}),
150
+ root: boundaryRoot,
151
+ sources: boundarySources
152
+ };
100
153
  }));
101
154
  };
155
+ const getApplicationSources = (application, boundaries) => {
156
+ const byName = new Map(boundaries.flatMap(boundary => boundary.packageName ?
157
+ [[boundary.packageName, boundary]] :
158
+ []));
159
+ const sources = [...application.sources];
160
+ const visited = new Set();
161
+ const includeDependencies = (boundary) => {
162
+ for (const dependencyName of boundary.dependencies) {
163
+ if (visited.has(dependencyName))
164
+ continue;
165
+ visited.add(dependencyName);
166
+ const dependency = byName.get(dependencyName);
167
+ if (dependency?.kind !== 'library')
168
+ continue;
169
+ sources.push(...dependency.sources);
170
+ includeDependencies(dependency);
171
+ }
172
+ };
173
+ includeDependencies(application);
174
+ return sources;
175
+ };
102
176
  const collectCompatibilityFindings = (root, sources) => sources.flatMap(item => {
103
177
  const file = relative(root, item.file);
104
178
  const findings = [];
@@ -108,6 +182,9 @@ const collectCompatibilityFindings = (root, sources) => sources.flatMap(item =>
108
182
  if (item.source.includes('ui:datatable-selection-change')) {
109
183
  findings.push(finding(file, 'deprecated-datatable-event', 'The ui:datatable-selection-change event alias is deprecated.', 'Rename it to ui:data-table-selection-change.', 'advisory'));
110
184
  }
185
+ if (importedNames(item.source, '@santi020k/lumen-astro').includes('UIPrimitives')) {
186
+ findings.push(finding(file, 'removed-root-export', 'UIPrimitives is no longer exported from the @santi020k/lumen-astro package root.', 'Import the default runtime from @santi020k/lumen-astro/runtime, or run lumen migrate-v2.'));
187
+ }
111
188
  return findings;
112
189
  });
113
190
  const hasImportedPrimitive = (sources, names) => [
@@ -156,6 +233,47 @@ const duplicatedBehaviorRules = [
156
233
  message: 'A raw menu with arrow-key handling may duplicate Lumen menu behavior.',
157
234
  remediation: 'Review DropdownMenu, Menubar, or NavigationMenu before maintaining a separate keyboard controller.',
158
235
  rule: 'hand-built-menu-keyboard'
236
+ },
237
+ {
238
+ detects: (source, sourceLower) => (source.includes('scrollY') || sourceLower.includes('scrolltop')) && (sourceLower.includes('scrollheight') || sourceLower.includes('scrollprogress')) && (source.includes('addEventListener(\'scroll\'') || source.includes('addEventListener("scroll"') ||
239
+ sourceLower.includes('onscroll')),
240
+ imports: ['ScrollProgress'],
241
+ message: 'Custom document scroll progress may duplicate Lumen ScrollProgress behavior.',
242
+ remediation: 'Review ScrollProgress before maintaining viewport measurement and progress synchronization.',
243
+ rule: 'hand-built-scroll-progress'
244
+ },
245
+ {
246
+ detects: (_source, sourceLower) => (sourceLower.includes('href="#main') || sourceLower.includes('href=\'#main')) && sourceLower.includes('skip'),
247
+ imports: ['SkipLink'],
248
+ message: 'A custom skip-to-content link may duplicate Lumen SkipLink behavior.',
249
+ remediation: 'Review SkipLink for its stable slot and fixed-position accessibility contract.',
250
+ rule: 'hand-built-skip-link'
251
+ },
252
+ {
253
+ detects: (source, sourceLower) => (source.includes('@radix-ui/react-slot') || sourceLower.includes('<slot')) && sourceLower.includes('aschild'),
254
+ imports: ['Button'],
255
+ message: 'A Radix Slot or asChild branch may duplicate Lumen Button composition.',
256
+ remediation: 'Review Button asChild before retaining a separate polymorphic action wrapper.',
257
+ rule: 'hand-built-polymorphic-button'
258
+ },
259
+ {
260
+ detects: (source, sourceLower) => sourceLower.includes('<input') &&
261
+ /\bsize\s*=\s*\{(?:size|props\.size)\}/u.test(source) &&
262
+ !source.includes('visualSize'),
263
+ imports: ['Input'],
264
+ message: 'A native input size passthrough may confuse HTML character width with Lumen visual sizing.',
265
+ remediation: 'Review Input visualSize and keep the native size attribute only when character width is intentional.',
266
+ rule: 'native-input-size-fallback'
267
+ },
268
+ {
269
+ detects: (source, sourceLower) => sourceLower.includes('card') &&
270
+ sourceLower.includes('card-header') &&
271
+ sourceLower.includes('card-content') &&
272
+ !source.includes('CardHeader'),
273
+ imports: ['Card'],
274
+ message: 'A hand-built card section hierarchy may duplicate Lumen Card compound parts.',
275
+ remediation: 'Review CardHeader, CardContent, and CardFooter while keeping domain-specific card content application-owned.',
276
+ rule: 'hand-built-card-structure'
159
277
  }
160
278
  ];
161
279
  const collectDuplicatedBehaviorFindings = (root, sources) => sources.flatMap(item => {
@@ -246,25 +364,63 @@ const styleImportByFramework = {
246
364
  react: '@santi020k/lumen-react/styles.css'
247
365
  };
248
366
  const countOccurrences = (source, value) => source.split(value).length - 1;
249
- const collectCssFindings = (root, sources) => sources
250
- .filter(item => item.file.endsWith('.css'))
251
- .flatMap(item => {
252
- const layerIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["'][^"']*\/layers\.css["']/mu);
253
- const tailwindIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["']tailwindcss["']/mu);
254
- const styleIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["'][^"']*\/styles\.css["']/mu);
255
- const findings = [];
256
- if (tailwindIndex >= 0 &&
257
- styleIndex >= 0 &&
258
- (layerIndex < 0 || !(layerIndex < tailwindIndex && tailwindIndex < styleIndex))) {
259
- findings.push(finding(relative(root, item.file), 'tailwind-layer-order', 'Tailwind and Lumen imports do not use the verified cascade order.', 'Import Lumen layers.css, then tailwindcss, then the matching Lumen styles.css.'));
367
+ const getInternalSelectorContract = (selector) => {
368
+ const [componentClass = ''] = selector.split(/--|__/u, 1);
369
+ const component = componentByClassName.get(componentClass);
370
+ if (!component)
371
+ return {};
372
+ const contract = stylingContracts[component];
373
+ if (!contract)
374
+ return { component };
375
+ if (selector.includes('__')) {
376
+ const partName = selector.split('__', 2)[1]?.replaceAll('_', '-') ?? '';
377
+ const slot = `${contract.rootSlot}-${partName}`;
378
+ return contract.parts.includes(slot) ? { component, slot } : { component };
260
379
  }
261
- for (const match of item.source.matchAll(/\.ui-([a-z0-9-]+)/g)) {
262
- const selector = match[1] ?? '';
263
- if (!publicRootSlots.has(selector)) {
264
- findings.push(finding(relative(root, item.file), 'internal-selector', `Selector .ui-${selector} is not a documented stable root hook.`, 'Prefer component props, semantic tokens, documented custom properties, or [data-slot].', 'advisory'));
265
- }
380
+ return { component, slot: contract.rootSlot };
381
+ };
382
+ const consumerClassGuidance = (frameworks) => {
383
+ const detectedFrameworks = new Set(frameworks);
384
+ const adapters = [
385
+ detectedFrameworks.has('astro') && 'the public class prop in Astro',
386
+ detectedFrameworks.has('react') && 'the public className prop in React',
387
+ detectedFrameworks.has('elements') && 'a class attribute on the Elements host'
388
+ ].filter((value) => Boolean(value));
389
+ if (adapters.length === 0) {
390
+ return 'Pass a consumer-owned class through the adapter\'s public class API.';
266
391
  }
267
- return findings;
392
+ return `Pass a consumer-owned class through ${adapters.join(', ')}.`;
393
+ };
394
+ const internalSelectorFinding = (file, selector, frameworks) => {
395
+ const contract = getInternalSelectorContract(selector);
396
+ const componentContext = contract.component ?
397
+ ` It most likely targets the ${contract.component} component.` :
398
+ '';
399
+ const stableSlotGuidance = contract.slot ?
400
+ ` For a library-owned hook, use [data-slot="${contract.slot}"].` :
401
+ '';
402
+ return finding(file, 'internal-selector', `Selector .ui-${selector} is not a documented stable hook.${componentContext}`, `${consumerClassGuidance(frameworks)} Prefer semantic tokens and documented custom properties.${stableSlotGuidance}`, 'advisory');
403
+ };
404
+ const collectCssFindings = (root, boundaries) => boundaries.flatMap(boundary => {
405
+ const frameworks = getFrameworks(boundary.sources);
406
+ return boundary.sources.filter(item => item.file.endsWith('.css')).flatMap(item => {
407
+ const layerIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["'][^"']*\/layers\.css["']/mu);
408
+ const tailwindIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["']tailwindcss["']/mu);
409
+ const styleIndex = item.source.search(/^\s*@import\s+(?:url\(\s*)?["'][^"']*\/styles\.css["']/mu);
410
+ const findings = [];
411
+ if (tailwindIndex >= 0 &&
412
+ styleIndex >= 0 &&
413
+ (layerIndex < 0 || !(layerIndex < tailwindIndex && tailwindIndex < styleIndex))) {
414
+ findings.push(finding(relative(root, item.file), 'tailwind-layer-order', 'Tailwind and Lumen imports do not use the verified cascade order.', 'Import Lumen layers.css, then tailwindcss, then the matching Lumen styles.css.'));
415
+ }
416
+ for (const match of item.source.matchAll(/\.ui-([a-z0-9_-]+)/g)) {
417
+ const selector = match[1] ?? '';
418
+ if (!publicRootSlots.has(selector)) {
419
+ findings.push(internalSelectorFinding(relative(root, item.file), selector, frameworks));
420
+ }
421
+ }
422
+ return findings;
423
+ });
268
424
  });
269
425
  const collectFrameworkStyleFindings = (repositoryRoot, boundary, frameworks) => frameworks.flatMap(framework => {
270
426
  const styleImport = styleImportByFramework[framework];
@@ -291,6 +447,10 @@ const collectAdapterMismatchFindings = (repositoryRoot, boundary, frameworks) =>
291
447
  .filter(adapter => !frameworks.includes(adapter))
292
448
  .map(adapter => finding(relative(repositoryRoot, boundary.root) || '.', 'adapter-style-mismatch', `The ${adapter} stylesheet is loaded without matching ${adapter} imports in this application boundary.`, 'Load the stylesheet from the adapter used by this application boundary.'));
293
449
  };
450
+ const collectSharedLibraryStyleFindings = (repositoryRoot, boundary) => Object.entries(styleImportByFramework)
451
+ .flatMap(([framework, styleImport]) => boundary.sources
452
+ .filter(item => item.source.includes(styleImport))
453
+ .map(item => finding(relative(repositoryRoot, item.file), 'shared-library-adapter-style', `A shared library loads the ${framework} adapter stylesheet, which can duplicate application-owned setup.`, 'Remove the adapter stylesheet from the shared package and load it once in each consuming application root.', 'advisory')));
294
454
  const uniqueFindings = (findings) => [
295
455
  ...new Map(findings.map(item => [
296
456
  `${item.file}\u0000${item.rule}\u0000${item.message}`,
@@ -306,18 +466,26 @@ export const inspectLumenIntegration = async (repository) => {
306
466
  file,
307
467
  source: await readFile(file, 'utf8')
308
468
  })));
309
- const boundaries = createBoundaries(root, discovered.packageRoots, sources);
310
- const frameworks = [...new Set(boundaries.flatMap(boundary => getFrameworks(boundary.sources)))];
469
+ const boundaries = await createBoundaries(root, discovered.packageRoots, sources);
470
+ const applicationBoundaries = boundaries.filter(boundary => boundary.kind === 'application');
471
+ const frameworks = [...new Set(applicationBoundaries.flatMap(boundary => getFrameworks(getApplicationSources(boundary, boundaries))))];
311
472
  const runtimeComponents = new Set();
312
473
  const findings = [
313
474
  ...collectCompatibilityFindings(root, sources),
314
- ...collectCssFindings(root, sources)
475
+ ...collectCssFindings(root, boundaries)
315
476
  ];
316
- for (const boundary of boundaries) {
317
- const boundaryFrameworks = getFrameworks(boundary.sources);
318
- const runtime = collectRuntimeFindings(root, boundary);
477
+ for (const boundary of boundaries.filter(item => item.kind === 'library')) {
478
+ findings.push(...collectSharedLibraryStyleFindings(root, boundary));
479
+ }
480
+ for (const boundary of applicationBoundaries) {
481
+ const effectiveBoundary = {
482
+ ...boundary,
483
+ sources: getApplicationSources(boundary, boundaries)
484
+ };
485
+ const boundaryFrameworks = getFrameworks(effectiveBoundary.sources);
486
+ const runtime = collectRuntimeFindings(root, effectiveBoundary);
319
487
  runtime.runtimeComponents.forEach(component => runtimeComponents.add(component));
320
- findings.push(...collectDuplicatedBehaviorFindings(root, boundary.sources), ...runtime.findings, ...collectFrameworkStyleFindings(root, boundary, boundaryFrameworks), ...collectAdapterMismatchFindings(root, boundary, boundaryFrameworks));
488
+ findings.push(...collectDuplicatedBehaviorFindings(root, effectiveBoundary.sources), ...runtime.findings, ...collectFrameworkStyleFindings(root, boundary, boundaryFrameworks), ...collectAdapterMismatchFindings(root, boundary, boundaryFrameworks));
321
489
  }
322
490
  const deduplicatedFindings = uniqueFindings(findings);
323
491
  return {
@@ -321,6 +321,42 @@ export declare const lumenRegistry: {
321
321
  readonly type: "component";
322
322
  readonly description: "Copies arbitrary text or the content of another element with accessible feedback.";
323
323
  readonly category: "Actions";
324
+ readonly contract: {
325
+ readonly imports: {
326
+ readonly astro: "import { CopyButton } from '@santi020k/lumen-astro'";
327
+ };
328
+ readonly usage: {
329
+ readonly astro: "<CopyButton value=\"https://example.com\" variant=\"default\">Copy link<span slot=\"copied\">Copied!</span><span slot=\"error\">Try again</span></CopyButton>";
330
+ };
331
+ readonly props: readonly [{
332
+ readonly name: "value";
333
+ readonly type: "string";
334
+ }, {
335
+ readonly name: "target";
336
+ readonly type: "string";
337
+ }, {
338
+ readonly name: "variant";
339
+ readonly type: "Button variant";
340
+ readonly default: "outline";
341
+ }, {
342
+ readonly name: "size";
343
+ readonly type: "default | sm | lg | icon";
344
+ readonly default: "default";
345
+ }, {
346
+ readonly name: "resetAfter";
347
+ readonly type: "number";
348
+ readonly default: "2000";
349
+ }, {
350
+ readonly name: "toast";
351
+ readonly type: "boolean";
352
+ readonly default: "false";
353
+ }];
354
+ readonly events: readonly ["ui:copy-success", "ui:copy-error"];
355
+ readonly slots: readonly ["default (idle)", "copied", "error"];
356
+ readonly runtime: "Mount the default export from @santi020k/lumen-astro/runtime once above the component.";
357
+ readonly stylingParts: readonly ["copy-button", "copy-idle", "copy-copied", "copy-error"];
358
+ readonly tokens: readonly ["brand-solid", "line", "surface-muted", "ink"];
359
+ };
324
360
  readonly files: readonly ["packages/astro/components/CopyButton.astro", "packages/astro/styles/lumen.css", "packages/astro/runtime/UIPrimitives.astro"];
325
361
  readonly dependencies: readonly ["styles", "runtime"];
326
362
  }, {
@@ -935,6 +971,38 @@ export declare const lumenRegistry: {
935
971
  readonly type: "component";
936
972
  readonly category: "Layout";
937
973
  readonly description: "Reveals a group of direct children with tokenized, reduced-motion-aware staggering.";
974
+ readonly contract: {
975
+ readonly imports: {
976
+ readonly astro: "import { RevealGroup } from '@santi020k/lumen-astro'";
977
+ };
978
+ readonly usage: {
979
+ readonly astro: "<RevealGroup as=\"ul\"><li>First</li><li>Second</li></RevealGroup>";
980
+ };
981
+ readonly props: readonly [{
982
+ readonly name: "as";
983
+ readonly type: "div | ul | ol | section";
984
+ readonly default: "div";
985
+ }, {
986
+ readonly name: "animation";
987
+ readonly type: "fade | slide-up | scale";
988
+ readonly default: "slide-up";
989
+ }, {
990
+ readonly name: "duration";
991
+ readonly type: "fast | standard | slow";
992
+ readonly default: "slow";
993
+ }, {
994
+ readonly name: "stagger";
995
+ readonly type: "number";
996
+ readonly default: "80";
997
+ }, {
998
+ readonly name: "once";
999
+ readonly type: "boolean";
1000
+ readonly default: "true";
1001
+ }];
1002
+ readonly composition: readonly ["Children remain direct children of the selected semantic root."];
1003
+ readonly runtime: "Mount the Astro runtime once; content remains readable before JavaScript and with reduced motion.";
1004
+ readonly tokens: readonly ["motion-duration-fast", "motion-duration-standard", "motion-duration-slow"];
1005
+ };
938
1006
  readonly files: readonly ["packages/astro/components/RevealGroup.astro", "packages/astro/runtime/UIPrimitives.astro", "packages/astro/styles/lumen.css", "packages/react/src/components.tsx"];
939
1007
  readonly dependencies: readonly ["styles", "runtime"];
940
1008
  }, {
@@ -949,6 +1017,34 @@ export declare const lumenRegistry: {
949
1017
  readonly type: "component";
950
1018
  readonly category: "Data display";
951
1019
  readonly description: "A stat component.";
1020
+ readonly contract: {
1021
+ readonly imports: {
1022
+ readonly astro: "import { Stat, StatLabel, StatValue } from '@santi020k/lumen-astro'";
1023
+ readonly react: "import { Stat, StatLabel, StatValue } from '@santi020k/lumen-react'";
1024
+ };
1025
+ readonly usage: {
1026
+ readonly astro: "<Stat as=\"article\"><StatLabel>Revenue</StatLabel><StatValue>$12,400</StatValue></Stat>";
1027
+ readonly react: "<Stat as=\"article\"><StatLabel>Revenue</StatLabel><StatValue>$12,400</StatValue></Stat>";
1028
+ };
1029
+ readonly props: readonly [{
1030
+ readonly name: "as";
1031
+ readonly type: "article | div | section";
1032
+ readonly default: "div";
1033
+ }, {
1034
+ readonly name: "variant";
1035
+ readonly type: "accent | bare | default | glass";
1036
+ readonly default: "default";
1037
+ }, {
1038
+ readonly name: "label";
1039
+ readonly type: "string";
1040
+ }, {
1041
+ readonly name: "value";
1042
+ readonly type: "string";
1043
+ }];
1044
+ readonly composition: readonly ["Prefer StatLabel, StatValue, StatDescription, StatIcon, and StatTrend for structured content."];
1045
+ readonly stylingParts: readonly ["stat", "stat-label", "stat-value", "stat-description", "stat-icon", "stat-trend"];
1046
+ readonly tokens: readonly ["surface", "line", "ink", "ink-muted", "brand-soft"];
1047
+ };
952
1048
  readonly files: readonly ["packages/astro/components/Stat.astro", "packages/react/src/components.tsx"];
953
1049
  }, {
954
1050
  readonly name: "StatDescription";
@@ -1 +1 @@
1
- {"version":3,"file":"registry-data.d.ts","sourceRoot":"","sources":["../src/registry-data.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAm5EQ,CAAA"}
1
+ {"version":3,"file":"registry-data.d.ts","sourceRoot":"","sources":["../src/registry-data.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2iFQ,CAAA"}
@@ -730,6 +730,66 @@ export const lumenRegistry = {
730
730
  type: 'component',
731
731
  description: 'Copies arbitrary text or the content of another element with accessible feedback.',
732
732
  category: 'Actions',
733
+ contract: {
734
+ imports: {
735
+ astro: 'import { CopyButton } from \'@santi020k/lumen-astro\''
736
+ },
737
+ usage: {
738
+ astro: '<CopyButton value="https://example.com" variant="default">Copy link<span slot="copied">Copied!</span><span slot="error">Try again</span></CopyButton>'
739
+ },
740
+ props: [
741
+ {
742
+ name: 'value',
743
+ type: 'string'
744
+ },
745
+ {
746
+ name: 'target',
747
+ type: 'string'
748
+ },
749
+ {
750
+ name: 'variant',
751
+ type: 'Button variant',
752
+ default: 'outline'
753
+ },
754
+ {
755
+ name: 'size',
756
+ type: 'default | sm | lg | icon',
757
+ default: 'default'
758
+ },
759
+ {
760
+ name: 'resetAfter',
761
+ type: 'number',
762
+ default: '2000'
763
+ },
764
+ {
765
+ name: 'toast',
766
+ type: 'boolean',
767
+ default: 'false'
768
+ }
769
+ ],
770
+ events: [
771
+ 'ui:copy-success',
772
+ 'ui:copy-error'
773
+ ],
774
+ slots: [
775
+ 'default (idle)',
776
+ 'copied',
777
+ 'error'
778
+ ],
779
+ runtime: 'Mount the default export from @santi020k/lumen-astro/runtime once above the component.',
780
+ stylingParts: [
781
+ 'copy-button',
782
+ 'copy-idle',
783
+ 'copy-copied',
784
+ 'copy-error'
785
+ ],
786
+ tokens: [
787
+ 'brand-solid',
788
+ 'line',
789
+ 'surface-muted',
790
+ 'ink'
791
+ ]
792
+ },
733
793
  files: [
734
794
  'packages/astro/components/CopyButton.astro',
735
795
  'packages/astro/styles/lumen.css',
@@ -1988,6 +2048,50 @@ export const lumenRegistry = {
1988
2048
  type: 'component',
1989
2049
  category: 'Layout',
1990
2050
  description: 'Reveals a group of direct children with tokenized, reduced-motion-aware staggering.',
2051
+ contract: {
2052
+ imports: {
2053
+ astro: 'import { RevealGroup } from \'@santi020k/lumen-astro\''
2054
+ },
2055
+ usage: {
2056
+ astro: '<RevealGroup as="ul"><li>First</li><li>Second</li></RevealGroup>'
2057
+ },
2058
+ props: [
2059
+ {
2060
+ name: 'as',
2061
+ type: 'div | ul | ol | section',
2062
+ default: 'div'
2063
+ },
2064
+ {
2065
+ name: 'animation',
2066
+ type: 'fade | slide-up | scale',
2067
+ default: 'slide-up'
2068
+ },
2069
+ {
2070
+ name: 'duration',
2071
+ type: 'fast | standard | slow',
2072
+ default: 'slow'
2073
+ },
2074
+ {
2075
+ name: 'stagger',
2076
+ type: 'number',
2077
+ default: '80'
2078
+ },
2079
+ {
2080
+ name: 'once',
2081
+ type: 'boolean',
2082
+ default: 'true'
2083
+ }
2084
+ ],
2085
+ composition: [
2086
+ 'Children remain direct children of the selected semantic root.'
2087
+ ],
2088
+ runtime: 'Mount the Astro runtime once; content remains readable before JavaScript and with reduced motion.',
2089
+ tokens: [
2090
+ 'motion-duration-fast',
2091
+ 'motion-duration-standard',
2092
+ 'motion-duration-slow'
2093
+ ]
2094
+ },
1991
2095
  files: [
1992
2096
  'packages/astro/components/RevealGroup.astro',
1993
2097
  'packages/astro/runtime/UIPrimitives.astro',
@@ -2020,6 +2124,54 @@ export const lumenRegistry = {
2020
2124
  type: 'component',
2021
2125
  category: 'Data display',
2022
2126
  description: 'A stat component.',
2127
+ contract: {
2128
+ imports: {
2129
+ astro: 'import { Stat, StatLabel, StatValue } from \'@santi020k/lumen-astro\'',
2130
+ react: 'import { Stat, StatLabel, StatValue } from \'@santi020k/lumen-react\''
2131
+ },
2132
+ usage: {
2133
+ astro: '<Stat as="article"><StatLabel>Revenue</StatLabel><StatValue>$12,400</StatValue></Stat>',
2134
+ react: '<Stat as="article"><StatLabel>Revenue</StatLabel><StatValue>$12,400</StatValue></Stat>'
2135
+ },
2136
+ props: [
2137
+ {
2138
+ name: 'as',
2139
+ type: 'article | div | section',
2140
+ default: 'div'
2141
+ },
2142
+ {
2143
+ name: 'variant',
2144
+ type: 'accent | bare | default | glass',
2145
+ default: 'default'
2146
+ },
2147
+ {
2148
+ name: 'label',
2149
+ type: 'string'
2150
+ },
2151
+ {
2152
+ name: 'value',
2153
+ type: 'string'
2154
+ }
2155
+ ],
2156
+ composition: [
2157
+ 'Prefer StatLabel, StatValue, StatDescription, StatIcon, and StatTrend for structured content.'
2158
+ ],
2159
+ stylingParts: [
2160
+ 'stat',
2161
+ 'stat-label',
2162
+ 'stat-value',
2163
+ 'stat-description',
2164
+ 'stat-icon',
2165
+ 'stat-trend'
2166
+ ],
2167
+ tokens: [
2168
+ 'surface',
2169
+ 'line',
2170
+ 'ink',
2171
+ 'ink-muted',
2172
+ 'brand-soft'
2173
+ ]
2174
+ },
2023
2175
  files: [
2024
2176
  'packages/astro/components/Stat.astro',
2025
2177
  'packages/react/src/components.tsx'
@@ -2,8 +2,25 @@ export interface LumenRegistryFile {
2
2
  path: string;
3
3
  source: string;
4
4
  }
5
+ export interface LumenRegistryComponentContract {
6
+ composition?: readonly string[];
7
+ events?: readonly string[];
8
+ imports: Readonly<Record<string, string>>;
9
+ props?: readonly {
10
+ default?: string;
11
+ name: string;
12
+ required?: boolean;
13
+ type: string;
14
+ }[];
15
+ runtime?: string;
16
+ slots?: readonly string[];
17
+ stylingParts?: readonly string[];
18
+ tokens?: readonly string[];
19
+ usage: Readonly<Record<string, string>>;
20
+ }
5
21
  export interface LumenRegistryComponent {
6
22
  category: string;
23
+ contract?: LumenRegistryComponentContract;
7
24
  dependencies?: readonly string[];
8
25
  description: string;
9
26
  files: readonly (LumenRegistryFile | string)[];
@@ -1 +1 @@
1
- {"version":3,"file":"registry-types.d.ts","sourceRoot":"","sources":["../src/registry-types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,SAAS,CAAC,iBAAiB,GAAG,MAAM,CAAC,EAAE,CAAA;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,iBAAiB,GAAG,MAAM,CAAC,EAAE,CAAA;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAA;CACnD;AAED,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG,iBAAiB,CAAA;AAE3E,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAA;IAC9C,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
1
+ {"version":3,"file":"registry-types.d.ts","sourceRoot":"","sources":["../src/registry-types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,8BAA8B;IAC7C,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC/B,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,KAAK,CAAC,EAAE,SAAS;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,IAAI,EAAE,MAAM,CAAA;KACb,EAAE,CAAA;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,8BAA8B,CAAA;IACzC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,SAAS,CAAC,iBAAiB,GAAG,MAAM,CAAC,EAAE,CAAA;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,WAAW,CAAA;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,iBAAiB,GAAG,MAAM,CAAC,EAAE,CAAA;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,eAAe,GAAG,eAAe,GAAG,QAAQ,CAAA;CACnD;AAED,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,GAAG,iBAAiB,CAAA;AAE3E,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAA;IAC9C,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAA;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
@@ -1,7 +1,7 @@
1
1
  import { lumenRegistry } from './registry-data.js';
2
- import type { LumenRegistry, LumenRegistryComponent, LumenRegistryEntry, LumenRegistryFile, LumenRegistryItem, LumenRegistryLoadOptions } from './registry-types.js';
2
+ import type { LumenRegistry, LumenRegistryComponent, LumenRegistryComponentContract, LumenRegistryEntry, LumenRegistryFile, LumenRegistryItem, LumenRegistryLoadOptions } from './registry-types.js';
3
3
  export { lumenRegistry };
4
- export type { LumenRegistry, LumenRegistryComponent, LumenRegistryEntry, LumenRegistryFile, LumenRegistryItem, LumenRegistryLoadOptions };
4
+ export type { LumenRegistry, LumenRegistryComponent, LumenRegistryComponentContract, LumenRegistryEntry, LumenRegistryFile, LumenRegistryItem, LumenRegistryLoadOptions };
5
5
  export declare const getLumenRegistryEntries: (registry?: LumenRegistry) => LumenRegistryEntry[];
6
6
  export declare const getLumenRegistryItem: (name: string, registry?: LumenRegistry) => LumenRegistryEntry | undefined;
7
7
  export declare const loadLumenRegistry: (source: string | URL, options?: LumenRegistryLoadOptions) => Promise<LumenRegistry>;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,aAAa,EAAE,CAAA;AAExB,YAAY,EACV,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,EACzB,CAAA;AAwBD,eAAO,MAAM,uBAAuB,GAAI,WAAU,aAA6B,KAAG,kBAAkB,EAGnG,CAAA;AAED,eAAO,MAAM,oBAAoB,GAC/B,MAAM,MAAM,EACZ,WAAU,aAA6B,KACtC,kBAAkB,GAAG,SAQvB,CAAA;AA6JD,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,MAAM,GAAG,GAAG,EACpB,UAAS,wBAA6B,KACrC,OAAO,CAAC,aAAa,CAsBvB,CAAA;AAED,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EACtB,8BAA8B,EAC9B,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,aAAa,EAAE,CAAA;AAExB,YAAY,EACV,aAAa,EACb,sBAAsB,EACtB,8BAA8B,EAC9B,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,wBAAwB,EACzB,CAAA;AAwBD,eAAO,MAAM,uBAAuB,GAAI,WAAU,aAA6B,KAAG,kBAAkB,EAGnG,CAAA;AAED,eAAO,MAAM,oBAAoB,GAC/B,MAAM,MAAM,EACZ,WAAU,aAA6B,KACtC,kBAAkB,GAAG,SAQvB,CAAA;AA6JD,eAAO,MAAM,iBAAiB,GAC5B,QAAQ,MAAM,GAAG,GAAG,EACpB,UAAS,wBAA6B,KACrC,OAAO,CAAC,aAAa,CAsBvB,CAAA;AAED,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,gBAAgB,CAAA"}
@@ -23,12 +23,12 @@
23
23
  "packages/compose/wear/src/main/kotlin/com/santi020k/lumen/wear"
24
24
  ],
25
25
  "toolchain": {
26
- "androidGradlePlugin": "9.3.1",
26
+ "androidGradlePlugin": "9.4.1",
27
27
  "compileSdk": 37,
28
- "gradle": "9.7.1",
28
+ "gradle": "9.8.0",
29
29
  "instrumentationApi": 35,
30
30
  "jdk": 21,
31
- "kotlin": "2.4.10",
31
+ "kotlin": "2.4.20",
32
32
  "minSdk": 23
33
33
  }
34
34
  }
@@ -70,8 +70,8 @@
70
70
  {
71
71
  "id": "icon-button",
72
72
  "tier": "universal",
73
- "contract": "Shared intent and native touch target",
74
- "accessibility": "Requires an action label",
73
+ "contract": "Shared intent and native touch target; SwiftUI supports semantic roles and loading",
74
+ "accessibility": "Requires an action label; SwiftUI loading preserves the name and disables repeat activation",
75
75
  "symbols": {
76
76
  "reactNative": "LumenIconButton",
77
77
  "swiftUI": "LumenIconButton",
@@ -92,7 +92,7 @@
92
92
  {
93
93
  "id": "button",
94
94
  "tier": "universal",
95
- "contract": "Intent, size, loading, and disabled states",
95
+ "contract": "Intent, size, loading, and disabled states; SwiftUI supports semantic button roles",
96
96
  "accessibility": "Exposes native button and state semantics",
97
97
  "symbols": {
98
98
  "reactNative": "LumenButton",
@@ -126,7 +126,7 @@
126
126
  "id": "field-group",
127
127
  "tier": "shared",
128
128
  "contract": "Label, description, required state, control content, and validation message",
129
- "accessibility": "Keeps child control semantics independent while presenting readable field context",
129
+ "accessibility": "Keeps child control semantics independent, presents readable field context, and announces validation updates",
130
130
  "symbols": {
131
131
  "reactNative": "LumenFieldGroup",
132
132
  "swiftUI": "LumenFieldGroup",
@@ -136,8 +136,8 @@
136
136
  {
137
137
  "id": "toggle",
138
138
  "tier": "shared",
139
- "contract": "Native switch behavior with visible label and disabled state",
140
- "accessibility": "Exposes native switch state and a readable label",
139
+ "contract": "Full-row native switch behavior with visible label and disabled state",
140
+ "accessibility": "Exposes one labeled switch whose full row toggles the controlled state and preserves supporting guidance",
141
141
  "symbols": {
142
142
  "reactNative": "LumenToggle",
143
143
  "swiftUI": "LumenToggle",
@@ -147,7 +147,7 @@
147
147
  {
148
148
  "id": "settings-row",
149
149
  "tier": "shared",
150
- "contract": "Title, supporting text, optional graphic, and trailing control",
150
+ "contract": "Title, supporting text, optional graphic, trailing control, and 56-unit minimum row height",
151
151
  "accessibility": "Contains the native control without merging its interaction",
152
152
  "symbols": {
153
153
  "reactNative": "LumenSettingsRow",
@@ -158,7 +158,7 @@
158
158
  {
159
159
  "id": "search-field",
160
160
  "tier": "shared",
161
- "contract": "Native search input with semantic surface and clear action",
161
+ "contract": "Native search input with semantic surface and a density-aware clear action",
162
162
  "accessibility": "Labels the input and clear action while preserving native editing",
163
163
  "symbols": {
164
164
  "reactNative": "LumenSearchField",
@@ -202,7 +202,7 @@
202
202
  {
203
203
  "id": "checkbox",
204
204
  "tier": "shared",
205
- "contract": "Controlled Boolean selection with visible label and optional supporting text",
205
+ "contract": "Controlled Boolean selection with visible label, optional supporting text, and density-aware row target",
206
206
  "accessibility": "Exposes native checkbox state, disabled state, and a readable label",
207
207
  "symbols": {
208
208
  "reactNative": "LumenCheckbox",
@@ -213,7 +213,7 @@
213
213
  {
214
214
  "id": "radio-group",
215
215
  "tier": "shared",
216
- "contract": "Single selection from labeled options with optional supporting text",
216
+ "contract": "Single selection from labeled options with optional supporting text and density-aware row targets",
217
217
  "accessibility": "Exposes a named native radio group and selected or disabled option states",
218
218
  "symbols": {
219
219
  "reactNative": "LumenRadioGroup",
@@ -246,7 +246,7 @@
246
246
  {
247
247
  "id": "chip",
248
248
  "tier": "shared",
249
- "contract": "Compact display, selection, and removal with semantic selected and disabled states",
249
+ "contract": "Compact display, selection, and removal with semantic states and a 24-unit minimum remove target",
250
250
  "accessibility": "Separates selection and removal actions while exposing selected and disabled state",
251
251
  "symbols": {
252
252
  "reactNative": "LumenChip",
@@ -290,7 +290,7 @@
290
290
  {
291
291
  "id": "spinner",
292
292
  "tier": "universal",
293
- "contract": "Brand-colored indeterminate activity",
293
+ "contract": "Brand-default indeterminate activity with an optional semantic tint",
294
294
  "accessibility": "Exposes a loading label",
295
295
  "symbols": {
296
296
  "reactNative": "LumenSpinner",
@@ -324,7 +324,7 @@
324
324
  "id": "toast",
325
325
  "tier": "shared",
326
326
  "contract": "App-controlled transient feedback with semantic tone, optional action, and dismissal",
327
- "accessibility": "Announces concise status while preserving independent action and dismissal controls",
327
+ "accessibility": "Uses native live feedback semantics while preserving independently labeled, full-target action and dismissal controls",
328
328
  "symbols": {
329
329
  "reactNative": "LumenToast",
330
330
  "swiftUI": "LumenToast",
@@ -544,7 +544,7 @@
544
544
  "id": "banner",
545
545
  "tier": "shared",
546
546
  "contract": "Semantic notice with optional actions and dismissal",
547
- "accessibility": "Does not announce automatically and labels dismissal",
547
+ "accessibility": "Does not announce automatically and exposes a labeled, full-target dismissal control",
548
548
  "symbols": {
549
549
  "reactNative": "LumenBanner",
550
550
  "swiftUI": "LumenBanner",
@@ -576,8 +576,8 @@
576
576
  {
577
577
  "id": "status-bar",
578
578
  "tier": "shared",
579
- "contract": "Compact semantic status with optional trailing content",
580
- "accessibility": "Status meaning remains present in text, not color alone",
579
+ "contract": "Compact semantic status that wraps beside fixed optional trailing content",
580
+ "accessibility": "Status meaning remains present in text before independently operable trailing content",
581
581
  "symbols": {
582
582
  "reactNative": "LumenStatusBar",
583
583
  "swiftUI": "LumenStatusBar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@santi020k/lumen",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -45,18 +45,18 @@
45
45
  "lumen": "./bin/lumen.mjs"
46
46
  },
47
47
  "dependencies": {
48
- "semver": "^7.8.0",
49
- "@santi020k/lumen-core": "^2.0.0"
48
+ "@santi020k/lumen-core": "^3.0.0",
49
+ "semver": "^7.8.5"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/node": "^26.2.0",
53
- "@types/react": "^19.2.18",
52
+ "@types/node": "^26.6.2",
53
+ "@types/react": "~19.2.4",
54
54
  "@types/semver": "^7.8.0",
55
- "@vitest/coverage-v8": "^4.1.11",
56
- "react": "^19.2.8",
57
- "typescript": "^6.0.3",
55
+ "@vitest/coverage-v8": "^5.0.1",
56
+ "react": "19.2.3",
58
57
  "tailwindcss": "^4.3.3",
59
- "vitest": "^4.1.11"
58
+ "typescript": "~6.0.3",
59
+ "vitest": "^5.0.1"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=22.12.0"
@@ -6,7 +6,7 @@
6
6
  "com.santi020k:lumen-compose",
7
7
  "com.santi020k:lumen-compose-wear"
8
8
  ],
9
- "version": "2.0.0"
9
+ "version": "3.0.0"
10
10
  },
11
11
  "npm": {
12
12
  "packages": {
@@ -14,41 +14,41 @@
14
14
  "peerDependencies": {
15
15
  "astro": ">=5"
16
16
  },
17
- "version": "2.0.0"
17
+ "version": "3.0.0"
18
18
  },
19
19
  "@santi020k/lumen-core": {
20
20
  "peerDependencies": {},
21
- "version": "2.0.0"
21
+ "version": "3.0.0"
22
22
  },
23
23
  "@santi020k/lumen-elements": {
24
24
  "peerDependencies": {},
25
- "version": "2.0.0"
25
+ "version": "3.0.0"
26
26
  },
27
27
  "@santi020k/lumen-icons-brand": {
28
28
  "peerDependencies": {},
29
- "version": "2.0.0"
29
+ "version": "3.0.0"
30
30
  },
31
31
  "@santi020k/lumen": {
32
32
  "peerDependencies": {},
33
- "version": "2.0.0"
33
+ "version": "3.0.0"
34
34
  },
35
35
  "@santi020k/lumen-mcp": {
36
36
  "peerDependencies": {},
37
- "version": "2.0.0"
37
+ "version": "3.0.0"
38
38
  },
39
39
  "@santi020k/lumen-react": {
40
40
  "peerDependencies": {
41
41
  "react": ">=19"
42
42
  },
43
- "version": "2.0.0"
43
+ "version": "3.0.0"
44
44
  },
45
45
  "@santi020k/lumen-react-hook-form": {
46
46
  "peerDependencies": {
47
- "@santi020k/lumen-react": "^2.0.0",
47
+ "@santi020k/lumen-react": "^3.0.0",
48
48
  "react": ">=19",
49
49
  "react-hook-form": ">=7.76.0 <8"
50
50
  },
51
- "version": "2.0.0"
51
+ "version": "3.0.0"
52
52
  },
53
53
  "@santi020k/lumen-react-native": {
54
54
  "peerDependencies": {
@@ -57,42 +57,32 @@
57
57
  "react-native": ">=0.86.2",
58
58
  "react-native-svg": ">=12 <16"
59
59
  },
60
- "version": "2.0.0"
60
+ "version": "3.0.0"
61
61
  },
62
62
  "@santi020k/lumen-tokens": {
63
63
  "peerDependencies": {},
64
- "version": "2.0.0"
64
+ "version": "3.0.0"
65
65
  }
66
66
  }
67
67
  },
68
68
  "swift": {
69
69
  "package": "https://github.com/santi020k/lumen.git",
70
70
  "revision": null,
71
- "tag": "v2.0.0"
71
+ "tag": "v3.0.0"
72
72
  },
73
- "version": "2.0.0"
73
+ "version": "3.0.0"
74
74
  },
75
75
  "migration": {
76
- "codemod": "lumen migrate v2 --dry-run",
76
+ "codemod": null,
77
77
  "css": {
78
78
  "astro": "@santi020k/lumen-astro/styles.css",
79
79
  "elements": "@santi020k/lumen-elements/styles.css",
80
80
  "react": "@santi020k/lumen-react/styles.css"
81
81
  },
82
82
  "deprecatedExports": [],
83
- "removedExports": [
84
- "@santi020k/lumen-astro#Sonner",
85
- "@santi020k/lumen-astro#UIPrimitives",
86
- "@santi020k/lumen-react#Sonner",
87
- "@santi020k/lumen-react#SonnerProps",
88
- "@santi020k/lumen-react-native#LumenDateField",
89
- "@santi020k/lumen-react-native#LumenDateFieldProps",
90
- "@santi020k/lumen-react-native#LumenDateRangeField",
91
- "@santi020k/lumen-react-native#LumenDateRangeFieldProps",
92
- "@santi020k/lumen-react-native#LumenDateRangeValue"
93
- ],
83
+ "removedExports": [],
94
84
  "runtime": {
95
- "astro": "Import UIPrimitives from @santi020k/lumen-astro/runtime and mount it once when an imported component declares the ui-primitives runtime.",
85
+ "astro": "Mount UIPrimitives once when an imported component declares the ui-primitives runtime.",
96
86
  "elements": "Call defineLumenElements once before rendering lumen-* elements.",
97
87
  "react": "Use the public React behavior hooks for behavior-heavy primitives."
98
88
  }
@@ -698,7 +698,7 @@
698
698
  .ui-badge--warning {
699
699
  background: hsl(var(--warning) / 0.12);
700
700
  border-color: hsl(var(--warning) / 0.36);
701
- color: hsl(var(--warning));
701
+ color: hsl(var(--ink));
702
702
  }
703
703
 
704
704
  .ui-input,
@@ -893,7 +893,7 @@
893
893
  .ui-field > p,
894
894
  .ui-field > small {
895
895
  margin: 0;
896
- color: hsl(var(--ink-muted));
896
+ color: hsl(var(--ink-soft));
897
897
  font-size: 0.8125rem;
898
898
  line-height: 1.5;
899
899
  }
@@ -1190,6 +1190,61 @@
1190
1190
  font-size: 0.875rem;
1191
1191
  }
1192
1192
 
1193
+ .ui-table__label {
1194
+ display: none;
1195
+ }
1196
+
1197
+ @media (max-width: 48rem) {
1198
+ .ui-table-wrap--records table,
1199
+ .ui-table-wrap--records tbody {
1200
+ display: block;
1201
+ min-width: 0;
1202
+ }
1203
+
1204
+ .ui-table-wrap--records thead {
1205
+ position: absolute;
1206
+ width: 1px;
1207
+ height: 1px;
1208
+ padding: 0;
1209
+ overflow: hidden;
1210
+ clip-path: inset(50%);
1211
+ white-space: nowrap;
1212
+ }
1213
+
1214
+ .ui-table-wrap--records tbody tr {
1215
+ display: grid;
1216
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
1217
+ gap: 1rem;
1218
+ padding: 1rem;
1219
+ }
1220
+
1221
+ .ui-table-wrap--records tbody tr + tr {
1222
+ border-top: 1px solid hsl(var(--line));
1223
+ }
1224
+
1225
+ .ui-table-wrap--records tbody td {
1226
+ display: block;
1227
+ min-width: 0;
1228
+ padding: 0;
1229
+ border: 0;
1230
+ overflow-wrap: anywhere;
1231
+ white-space: normal;
1232
+ text-align: start;
1233
+ }
1234
+
1235
+ .ui-table-wrap--records .ui-table__label {
1236
+ display: block;
1237
+ margin-bottom: 0.25rem;
1238
+ color: hsl(var(--ink-soft));
1239
+ font-size: 0.75rem;
1240
+ font-weight: 600;
1241
+ }
1242
+
1243
+ .ui-table-wrap--records .ui-table__cell--wide {
1244
+ grid-column: 1 / -1;
1245
+ }
1246
+ }
1247
+
1193
1248
  .ui-table-wrap th,
1194
1249
  .ui-data-table th {
1195
1250
  border-bottom: 1px solid hsl(var(--line));
package/styles.css CHANGED
@@ -1491,7 +1491,7 @@
1491
1491
  .ui-badge--warning {
1492
1492
  background: hsl(var(--warning) / 0.12);
1493
1493
  border-color: hsl(var(--warning) / 0.36);
1494
- color: hsl(var(--warning));
1494
+ color: hsl(var(--ink));
1495
1495
  }
1496
1496
 
1497
1497
  .ui-input,
@@ -1734,7 +1734,7 @@
1734
1734
  .ui-field > p,
1735
1735
  .ui-field > small {
1736
1736
  margin: 0;
1737
- color: hsl(var(--ink-muted));
1737
+ color: hsl(var(--ink-soft));
1738
1738
  font-size: 0.8125rem;
1739
1739
  line-height: 1.5;
1740
1740
  }
@@ -1921,6 +1921,17 @@
1921
1921
  color: hsl(var(--danger));
1922
1922
  }
1923
1923
 
1924
+ .ui-copy-button [data-slot^="copy-"] {
1925
+ display: inline-flex;
1926
+ align-items: center;
1927
+ justify-content: center;
1928
+ gap: inherit;
1929
+ }
1930
+
1931
+ .ui-copy-button [hidden] {
1932
+ display: none;
1933
+ }
1934
+
1924
1935
  .ui-skeleton {
1925
1936
  min-height: 1rem;
1926
1937
  border-radius: var(--ui-radius-sm);
@@ -2784,6 +2795,62 @@
2784
2795
  font-size: 0.875rem;
2785
2796
  }
2786
2797
 
2798
+ /* Mobile record labels are aria-hidden; retain explicit table roles. */
2799
+ .ui-table__label {
2800
+ display: none;
2801
+ }
2802
+
2803
+ @media (max-width: 48rem) {
2804
+ .ui-table-wrap--records table,
2805
+ .ui-table-wrap--records tbody {
2806
+ display: block;
2807
+ min-width: 0;
2808
+ }
2809
+
2810
+ .ui-table-wrap--records thead {
2811
+ position: absolute;
2812
+ width: 1px;
2813
+ height: 1px;
2814
+ padding: 0;
2815
+ overflow: hidden;
2816
+ clip-path: inset(50%);
2817
+ white-space: nowrap;
2818
+ }
2819
+
2820
+ .ui-table-wrap--records tbody tr {
2821
+ display: grid;
2822
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
2823
+ gap: 1rem;
2824
+ padding: 1rem;
2825
+ }
2826
+
2827
+ .ui-table-wrap--records tbody tr + tr {
2828
+ border-top: 1px solid hsl(var(--line));
2829
+ }
2830
+
2831
+ .ui-table-wrap--records tbody td {
2832
+ display: block;
2833
+ min-width: 0;
2834
+ padding: 0;
2835
+ border: 0;
2836
+ overflow-wrap: anywhere;
2837
+ white-space: normal;
2838
+ text-align: start;
2839
+ }
2840
+
2841
+ .ui-table-wrap--records .ui-table__label {
2842
+ display: block;
2843
+ margin-bottom: 0.25rem;
2844
+ color: hsl(var(--ink-soft));
2845
+ font-size: 0.75rem;
2846
+ font-weight: 600;
2847
+ }
2848
+
2849
+ .ui-table-wrap--records .ui-table__cell--wide {
2850
+ grid-column: 1 / -1;
2851
+ }
2852
+ }
2853
+
2787
2854
  .ui-table-wrap th,
2788
2855
  .ui-data-table th {
2789
2856
  border-bottom: 1px solid hsl(var(--line));