@astryxdesign/core 0.6.3-canary.f22695a → 0.6.3

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.
Files changed (118) hide show
  1. package/dist/Button/Button.d.ts.map +1 -1
  2. package/dist/Button/Button.js +28 -13
  3. package/dist/Chat/ChatLayout.d.ts +0 -19
  4. package/dist/Chat/ChatLayout.d.ts.map +1 -1
  5. package/dist/Chat/ChatLayout.js +0 -19
  6. package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
  7. package/dist/Chat/ChatLayoutScrollButton.js +34 -44
  8. package/dist/Citation/Citation.d.ts +1 -1
  9. package/dist/Citation/Citation.d.ts.map +1 -1
  10. package/dist/Citation/Citation.js +2 -3
  11. package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
  12. package/dist/CodeBlock/highlightRanges.js +3 -14
  13. package/dist/Icon/Icon.d.ts +31 -6
  14. package/dist/Icon/Icon.d.ts.map +1 -1
  15. package/dist/Icon/Icon.js +71 -8
  16. package/dist/Link/useLinkComponent.d.ts +15 -5
  17. package/dist/Link/useLinkComponent.d.ts.map +1 -1
  18. package/dist/Link/useLinkComponent.js +51 -34
  19. package/dist/Markdown/Markdown.js +2 -2
  20. package/dist/Markdown/url.d.ts +0 -2
  21. package/dist/Markdown/url.d.ts.map +1 -1
  22. package/dist/Markdown/url.js +2 -8
  23. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  24. package/dist/PowerSearch/PowerSearchValueEditor.js +0 -5
  25. package/dist/TopNav/TopNavMenu.d.ts +1 -1
  26. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  27. package/dist/TopNav/TopNavMenu.js +2 -3
  28. package/dist/astryx.css +4 -2
  29. package/dist/hooks/useClickableContainer.d.ts +1 -10
  30. package/dist/hooks/useClickableContainer.d.ts.map +1 -1
  31. package/dist/hooks/useClickableContainer.js +4 -15
  32. package/dist/index.d.ts +0 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +0 -3
  35. package/dist/theme/generateThemeRules.d.ts +7 -13
  36. package/dist/theme/generateThemeRules.d.ts.map +1 -1
  37. package/dist/theme/generateThemeRules.js +35 -131
  38. package/package.json +4 -9
  39. package/src/Button/Button.doc.mjs +5 -5
  40. package/src/Button/Button.tsx +13 -15
  41. package/src/Chat/ChatLayout.doc.mjs +4 -12
  42. package/src/Chat/ChatLayout.test.tsx +0 -28
  43. package/src/Chat/ChatLayout.tsx +0 -19
  44. package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -31
  45. package/src/Chat/ChatLayoutScrollButton.tsx +14 -31
  46. package/src/Citation/Citation.doc.mjs +2 -2
  47. package/src/Citation/Citation.test.tsx +0 -17
  48. package/src/Citation/Citation.tsx +2 -4
  49. package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
  50. package/src/CodeBlock/highlightRanges.test.ts +1 -143
  51. package/src/CodeBlock/highlightRanges.ts +5 -15
  52. package/src/Icon/Icon.doc.mjs +5 -7
  53. package/src/Icon/Icon.spec.md +40 -81
  54. package/src/Icon/Icon.test.tsx +3 -6
  55. package/src/Icon/Icon.tsx +68 -20
  56. package/src/IconButton/IconButton.doc.mjs +3 -3
  57. package/src/IconButton/IconButton.test.tsx +0 -40
  58. package/src/Item/Item.doc.mjs +2 -2
  59. package/src/Link/Link.doc.mjs +4 -6
  60. package/src/Link/LinkProvider.doc.mjs +2 -2
  61. package/src/Link/useLinkComponent.test.tsx +0 -333
  62. package/src/Link/useLinkComponent.ts +57 -29
  63. package/src/List/ListItem.doc.mjs +2 -2
  64. package/src/Markdown/Markdown.doc.mjs +2 -2
  65. package/src/Markdown/Markdown.spec.md +15 -19
  66. package/src/Markdown/Markdown.test.tsx +0 -52
  67. package/src/Markdown/Markdown.tsx +2 -2
  68. package/src/Markdown/parser.test.ts +0 -20
  69. package/src/Markdown/url.ts +3 -9
  70. package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -51
  71. package/src/PowerSearch/PowerSearchValueEditor.tsx +0 -4
  72. package/src/Token/Token.doc.mjs +2 -2
  73. package/src/TopNav/TopNavMenu.doc.mjs +2 -2
  74. package/src/TopNav/TopNavMenu.test.tsx +0 -30
  75. package/src/TopNav/TopNavMenu.tsx +2 -7
  76. package/src/hooks/useClickableContainer.ts +4 -20
  77. package/src/index.ts +0 -3
  78. package/src/tailwind-theme.css +6 -0
  79. package/src/theme/Theme.test.tsx +0 -32
  80. package/src/theme/generateThemeRules.test.ts +2 -470
  81. package/src/theme/generateThemeRules.ts +51 -219
  82. package/src/theme/tokenValueCompat.test.ts +1 -4
  83. package/dist/Icon/IconDefaultSizeContext.d.ts +0 -4
  84. package/dist/Icon/IconDefaultSizeContext.d.ts.map +0 -1
  85. package/dist/Icon/IconDefaultSizeContext.js +0 -18
  86. package/dist/Icon/IconSize.stylex.d.ts +0 -65
  87. package/dist/Icon/IconSize.stylex.d.ts.map +0 -1
  88. package/dist/Icon/IconSize.stylex.js +0 -75
  89. package/dist/Timer/Timer.d.ts +0 -49
  90. package/dist/Timer/Timer.d.ts.map +0 -1
  91. package/dist/Timer/Timer.js +0 -158
  92. package/dist/Timer/index.d.ts +0 -9
  93. package/dist/Timer/index.d.ts.map +0 -1
  94. package/dist/Timer/index.js +0 -10
  95. package/dist/theme/declarationBoundary.d.ts +0 -17
  96. package/dist/theme/declarationBoundary.d.ts.map +0 -1
  97. package/dist/theme/declarationBoundary.js +0 -343
  98. package/dist/utils/safeUrl.d.ts +0 -11
  99. package/dist/utils/safeUrl.d.ts.map +0 -1
  100. package/dist/utils/safeUrl.js +0 -78
  101. package/src/Chat/ChatLayout.spec.md +0 -271
  102. package/src/Chat/ChatLayoutScrollButton.spec.md +0 -338
  103. package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -514
  104. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
  105. package/src/Icon/IconDefaultSizeContext.ts +0 -23
  106. package/src/Icon/IconSize.stylex.ts +0 -70
  107. package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -261
  108. package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
  109. package/src/Timer/Timer.doc.mjs +0 -183
  110. package/src/Timer/Timer.spec.md +0 -215
  111. package/src/Timer/Timer.test.tsx +0 -302
  112. package/src/Timer/Timer.tsx +0 -258
  113. package/src/Timer/index.ts +0 -11
  114. package/src/hooks/useClickableContainer.test.tsx +0 -236
  115. package/src/theme/declarationBoundary.test.ts +0 -126
  116. package/src/theme/declarationBoundary.ts +0 -347
  117. package/src/utils/safeUrl.test.ts +0 -88
  118. package/src/utils/safeUrl.ts +0 -85
@@ -9,17 +9,11 @@
9
9
  * Used by both the runtime path (Theme injects <style>) and the build
10
10
  * path (`astryx theme build` pre-compiles to CSS files).
11
11
  *
12
- * Every consumer-supplied declaration is checked by declarationBoundary.ts
13
- * before it is emitted, so a value can never end its declaration or rule
14
- * early. A refused declaration is dropped and reported in the supplied
15
- * warnings array (build receipts), or through `console.warn` (runtime).
16
- *
17
12
  * Extracted from defineTheme.ts to reduce cyclomatic complexity and provide
18
13
  * a clear single-responsibility module for CSS generation.
19
14
  *
20
- * @input DefinedTheme (resolved theme object from defineTheme), optional warnings array
21
- * @output CSS rule strings, split by layer (component vs prose), and warning text
22
- * for each dropped declaration
15
+ * @input DefinedTheme (resolved theme object from defineTheme)
16
+ * @output CSS rule strings, split by layer (component vs prose)
23
17
  * @position packages/core/src/theme/generateThemeRules.ts
24
18
  */
25
19
 
@@ -28,7 +22,6 @@ import { parseStyleKey } from "../utils/parseStyleKey.js";
28
22
  import { getDerivedVars } from "./derivedVarRegistry.js";
29
23
  import { dataTokenDefaults } from "./domainTokens/dataTokens.js";
30
24
  import { cssVar, classPrefix, dataAttrNamespace } from "../naming.js";
31
- import { checkDeclarationName, checkDeclarationValue } from "./declarationBoundary.js";
32
25
 
33
26
  /**
34
27
  * Theme @scope selectors.
@@ -291,77 +284,6 @@ function expandContainerPadding(component, parsed, resetInheritedSpecificity = f
291
284
  return tokens;
292
285
  }
293
286
 
294
- // =============================================================================
295
- // Declaration assembly: a declaration always stays one declaration
296
- // =============================================================================
297
-
298
- const warnToConsole = message => {
299
- console.warn(`[astryx theme] ${message}`);
300
- };
301
-
302
- /** Build receipts collect warning text; runtime callers warn by default. */
303
- function diagnosticSink(warnings) {
304
- return warnings ? message => warnings.push(message) : warnToConsole;
305
- }
306
-
307
- /** Shorten an authored value for a one-line message. */
308
- function previewValue(value) {
309
- const oneLine = JSON.stringify(value);
310
- return oneLine.length > 80 ? `${oneLine.slice(0, 77)}..."` : oneLine;
311
- }
312
- function reportDrop(sink, property, value, location, reason) {
313
- sink(`dropped "${property}" in ${location}: ${reason} (value: ${previewValue(value)})`);
314
- }
315
-
316
- /**
317
- * True when `name: value;` stays exactly one declaration; otherwise reports
318
- * the drop and returns false. Both checks follow CSS syntax, so valid CSS a
319
- * browser keeps inside one declaration (a `data:` URI, `Gill\ Sans`, a closed
320
- * comment, a vendor-prefixed property) is never refused; see
321
- * declarationBoundary.ts for the rules.
322
- */
323
- function acceptDeclaration(property, value, location, sink) {
324
- const reason = checkDeclarationName(property) ?? checkDeclarationValue(value);
325
- if (reason === null) {
326
- return true;
327
- }
328
- reportDrop(sink, property, value, location, reason);
329
- return false;
330
- }
331
-
332
- /**
333
- * Assemble `prop: value;` lines, dropping (and reporting) any entry whose
334
- * property or value could not stay a single declaration. Theme definitions
335
- * are code, but apps do assemble them from stored input (brand colors,
336
- * white-labeling), so the generated stylesheet must never extend beyond the
337
- * declarations it means to emit.
338
- */
339
- function joinDeclarations(entries, location, sink, mapProp = p => p) {
340
- const lines = [];
341
- for (const [rawProp, rawValue] of entries) {
342
- // Legacy unenrolled tokens retain non-string values. Scan the same text
343
- // interpolation has always emitted, without changing the normalized theme.
344
- const value = `${rawValue}`;
345
- const prop = mapProp(rawProp);
346
- if (acceptDeclaration(prop, value, location, sink)) {
347
- lines.push(` ${prop}: ${value};`);
348
- }
349
- }
350
- return lines.join('\n');
351
- }
352
-
353
- /** The `:scope` token block for portable and theme-local token values. */
354
- function tokenBlock(source, location, sink) {
355
- const declarations = [joinDeclarations(Object.entries(source.tokens), `${location}tokens`, sink), joinDeclarations(Object.entries(source.localTokens ?? {}), `${location}localTokens`, sink)].filter(block => block.length > 0).join('\n');
356
- const hasEntries = Object.keys(source.tokens).length > 0 || Object.keys(source.localTokens ?? {}).length > 0;
357
- return hasEntries ? ` :scope {\n${declarations}\n }` : null;
358
- }
359
-
360
- /** `components.button["variant:secondary"]`-style location for a rule. */
361
- function componentLocation(prefix, component, key) {
362
- return `${prefix}components.${component}[${JSON.stringify(key)}]`;
363
- }
364
-
365
287
  // =============================================================================
366
288
  // Public API
367
289
  // =============================================================================
@@ -381,9 +303,9 @@ function componentLocation(prefix, component, key) {
381
303
  * lost for every conditional rule, with no type error to catch it.
382
304
  */
383
305
 
384
- export function generateThemeRules(theme, warnings) {
306
+ export function generateThemeRules(theme) {
385
307
  const parts = [];
386
- const sink = diagnosticSink(warnings);
308
+ const tokens = theme.tokens;
387
309
 
388
310
  // Bare prose rules reference semantic variables instead of baking the root
389
311
  // value. That lets adaptation token writes take effect through CSS alone
@@ -391,9 +313,10 @@ export function generateThemeRules(theme, warnings) {
391
313
  const val = key => `var(${key})`;
392
314
 
393
315
  // 1. Token block — CSS custom properties on :scope
394
- const tokenRules = tokenBlock(theme, '', sink);
395
- if (tokenRules !== null) {
396
- parts.push(tokenRules);
316
+ const tokenEntries = [...Object.entries(tokens), ...Object.entries(theme.localTokens ?? {})];
317
+ if (tokenEntries.length > 0) {
318
+ const declarations = tokenEntries.map(([prop, value]) => ` ${prop}: ${value};`).join('\n');
319
+ parts.push(` :scope {\n${declarations}\n }`);
397
320
  }
398
321
 
399
322
  // 1b. Base font — apply the theme's declared body font to the scope root.
@@ -402,16 +325,14 @@ export function generateThemeRules(theme, warnings) {
402
325
  // sets a page font. `font-family` inherits, so one rule on :scope covers
403
326
  // the tree. Only emitted when the theme declares a body font, so a bare
404
327
  // theme still contributes no scope rules of its own.
405
- if (theme.tokens['--font-family-body']) {
328
+ if (tokens['--font-family-body']) {
406
329
  parts.push(` :scope {\n font-family: var(--font-family-body);\n }`);
407
330
  }
408
331
 
409
332
  // 2. Component overrides: stable .astryx-* target classes combined with
410
333
  // reflected data-* selectors for visual props and runtime states.
411
334
  if (theme.components) {
412
- generateComponentRules(theme.components, parts, {
413
- sink
414
- });
335
+ generateComponentRules(theme.components, parts);
415
336
  }
416
337
 
417
338
  // 3. Prose HTML element rules (h1-h6, p, small, code, hr)
@@ -475,21 +396,13 @@ function generateHeadingWeightOverrides(components, parts, {
475
396
  parts.push(` ${selector} { font-weight: ${effectiveWeight}; }`);
476
397
  }
477
398
  }
478
-
479
- /**
480
- * The `fontWeight` a heading rule authors for `styleKey`, or undefined when
481
- * it has none. A value that cannot stay one declaration is treated as not
482
- * authored, so the override falls back to the inherited or token weight
483
- * instead of interpolating it; the drop itself is reported where that same
484
- * rule's declarations are emitted, so it is not reported twice here.
485
- */
486
399
  function getAuthoredHeadingWeight(headingRuleMap, styleKey) {
487
400
  const rule = headingRuleMap[styleKey];
488
401
  if (rule == null || typeof rule !== 'object' || Array.isArray(rule)) {
489
402
  return undefined;
490
403
  }
491
404
  const fontWeight = rule.fontWeight;
492
- return typeof fontWeight === 'string' && checkDeclarationValue(fontWeight) === null ? fontWeight : undefined;
405
+ return typeof fontWeight === 'string' ? fontWeight : undefined;
493
406
  }
494
407
 
495
408
  /** Options shared by root, adaptation, and media-surface component lowering. */
@@ -501,9 +414,7 @@ function getAuthoredHeadingWeight(headingRuleMap, styleKey) {
501
414
  */
502
415
  function generateComponentRules(components, parts, {
503
416
  resetInheritedPaddingSpecificity = false,
504
- surface,
505
- sink = warnToConsole,
506
- location: locationPrefix = ''
417
+ surface
507
418
  } = {}) {
508
419
  for (const [component, rules] of Object.entries(components)) {
509
420
  for (const [key, styles] of Object.entries(rules)) {
@@ -513,7 +424,6 @@ function generateComponentRules(components, parts, {
513
424
  }
514
425
  const suffix = parseStyleKey(key);
515
426
  const baseSelector = componentSelector(component, suffix, surface);
516
- const location = componentLocation(locationPrefix, component, key);
517
427
 
518
428
  // Separate regular properties from pseudo-class overrides
519
429
  const props = [];
@@ -521,7 +431,7 @@ function generateComponentRules(components, parts, {
521
431
  for (const [prop, value] of entries) {
522
432
  if (prop.startsWith(':') && typeof value === 'object') {
523
433
  pseudos.push([prop, value]);
524
- } else if (acceptDeclaration(toKebabCase(prop), `${value}`, location, sink)) {
434
+ } else {
525
435
  props.push([prop, value]);
526
436
  }
527
437
  }
@@ -583,7 +493,7 @@ function generateComponentRules(components, parts, {
583
493
 
584
494
  // Emit base rule
585
495
  if (finalProps.length > 0) {
586
- const declarations = joinDeclarations(finalProps, location, sink, toKebabCase);
496
+ const declarations = finalProps.map(([prop, value]) => ` ${toKebabCase(prop)}: ${value};`).join('\n');
587
497
  parts.push(` ${baseSelector} {\n${declarations}\n }`);
588
498
  }
589
499
 
@@ -591,7 +501,7 @@ function generateComponentRules(components, parts, {
591
501
  for (const [pseudo, pseudoStyles] of pseudos) {
592
502
  const pseudoEntries = Object.entries(pseudoStyles);
593
503
  if (pseudoEntries.length > 0) {
594
- const declarations = joinDeclarations(pseudoEntries, `${location}[${JSON.stringify(pseudo)}]`, sink, toKebabCase);
504
+ const declarations = pseudoEntries.map(([prop, value]) => ` ${toKebabCase(prop)}: ${value};`).join('\n');
595
505
  parts.push(` ${appendPseudoToSelectorList(baseSelector, pseudo)} {\n${declarations}\n }`);
596
506
  }
597
507
  }
@@ -729,8 +639,8 @@ function generateSizeOverrides(components, parts, surface) {
729
639
  * Component rules (tokens, stable .astryx-* targets plus data-* selectors) are
730
640
  * that need to beat StyleX — they stay in astryx-theme (above StyleX layers).
731
641
  */
732
- export function generateThemeRulesSplit(theme, warnings) {
733
- const allRules = generateThemeRules(theme, warnings);
642
+ export function generateThemeRulesSplit(theme) {
643
+ const allRules = generateThemeRules(theme);
734
644
  const prose = [];
735
645
  const component = [];
736
646
  for (const rule of allRules) {
@@ -754,21 +664,19 @@ export function generateThemeRulesSplit(theme, warnings) {
754
664
  * to media contexts — only tokens change. Themes can further customize
755
665
  * via onDark.components / onLight.components.
756
666
  */
757
- export function generateOnMediaCSS(theme, warnings) {
667
+ export function generateOnMediaCSS(theme) {
758
668
  const parts = [];
759
669
  const scopeSelector = themeScopeStart(theme.name);
760
- const sink = diagnosticSink(warnings);
761
670
  for (const surface of ['dark', 'light']) {
762
671
  const onMedia = surface === 'dark' ? theme.__onDark : theme.__onLight;
763
672
  if (!onMedia) {
764
673
  continue;
765
674
  }
766
- const location = surface === 'dark' ? 'onDark.' : 'onLight.';
767
675
 
768
676
  // Token overrides
769
677
  const tokenEntries = Object.entries(onMedia.tokens);
770
678
  if (tokenEntries.length > 0) {
771
- const declarations = joinDeclarations(tokenEntries, `${location}tokens`, sink);
679
+ const declarations = tokenEntries.map(([prop, value]) => ` ${prop}: ${value};`).join('\n');
772
680
  parts.push(` ${mediaSelector(surface)} {\n${declarations}\n }`);
773
681
  }
774
682
 
@@ -777,9 +685,7 @@ export function generateOnMediaCSS(theme, warnings) {
777
685
  if (onMedia.components) {
778
686
  generateComponentRules(onMedia.components, parts, {
779
687
  resetInheritedPaddingSpecificity: true,
780
- surface,
781
- sink,
782
- location
688
+ surface
783
689
  });
784
690
  generateColorOverrides(onMedia.components, parts, surface);
785
691
  generateSizeOverrides(onMedia.components, parts, surface);
@@ -801,17 +707,16 @@ export function generateOnMediaCSS(theme, warnings) {
801
707
  }
802
708
 
803
709
  /** Generate only the declarations one adaptation rule writes. */
804
- function generateAdaptationRuleRules(rule, location, sink) {
710
+ function generateAdaptationRuleRules(rule) {
805
711
  const parts = [];
806
- const tokens = tokenBlock(rule, location, sink);
807
- if (tokens !== null) {
808
- parts.push(tokens);
712
+ const tokenEntries = [...Object.entries(rule.tokens), ...Object.entries(rule.localTokens ?? {})];
713
+ if (tokenEntries.length > 0) {
714
+ const declarations = tokenEntries.map(([prop, value]) => ` ${prop}: ${value};`).join('\n');
715
+ parts.push(` :scope {\n${declarations}\n }`);
809
716
  }
810
717
  if (rule.components) {
811
718
  generateComponentRules(rule.components, parts, {
812
- resetInheritedPaddingSpecificity: true,
813
- sink,
814
- location
719
+ resetInheritedPaddingSpecificity: true
815
720
  });
816
721
  generateColorOverrides(rule.components, parts);
817
722
  generateSizeOverrides(rule.components, parts);
@@ -835,8 +740,7 @@ function generateAdaptationMediaBlock(query, parts, scopeSelector) {
835
740
  * rule. Bare prose follows semantic variables, so token writes need no duplicate
836
741
  * prose selectors here.
837
742
  */
838
- export function generateAdaptationCSS(theme, warnings) {
839
- const sink = diagnosticSink(warnings);
743
+ export function generateAdaptationCSS(theme) {
840
744
  // Built themes can reach this compiler without passing through defineTheme.
841
745
  // Validate retained metadata even when it has no rules (and therefore emits
842
746
  // no CSS): width points remain observable through AppShell and inheritance.
@@ -857,8 +761,8 @@ export function generateAdaptationCSS(theme, warnings) {
857
761
  // active whenever the later condition matches, even if the earlier condition
858
762
  // does not. Pairwise merging cannot repair that: multiple mutually exclusive
859
763
  // earlier rules can each overlap one broad later rule.
860
- for (const [index, rule] of rules.entries()) {
861
- const parts = generateAdaptationRuleRules(rule, `adaptations[${index}].`, sink);
764
+ for (const rule of rules) {
765
+ const parts = generateAdaptationRuleRules(rule);
862
766
  if (parts.length === 0) {
863
767
  continue;
864
768
  }
@@ -893,9 +797,9 @@ export function generateAdaptationCSS(theme, warnings) {
893
797
  };
894
798
  }
895
799
 
896
- /** Indent the rule start without rewriting newlines inside authored values. */
800
+ /** Indent a generated rule one level further, for nesting inside `@media`. */
897
801
  function indentRule(rule) {
898
- return ` ${rule}`;
802
+ return rule.split('\n').map(line => line.length > 0 ? ` ${line}` : line).join('\n');
899
803
  }
900
804
 
901
805
  /**
@@ -934,11 +838,11 @@ export function generateDataTokenDefaultsCSS() {
934
838
  * The theme-independent `--color-data-*` defaults are not part of this output:
935
839
  * see `generateDataTokenDefaultsCSS`.
936
840
  */
937
- export function generateThemeCSS(theme, warnings) {
841
+ export function generateThemeCSS(theme) {
938
842
  const {
939
843
  component,
940
844
  prose
941
- } = generateThemeRulesSplit(theme, warnings);
845
+ } = generateThemeRulesSplit(theme);
942
846
  const scopeSelector = themeScopeStart(theme.name);
943
847
  const scopeTo = THEME_SCOPE_TO;
944
848
  let proseCss = '';
@@ -957,11 +861,11 @@ export function generateThemeCSS(theme, warnings) {
957
861
 
958
862
  // Adaptations follow the root theme in authored order. Media-surface rules
959
863
  // follow adaptations so onDark/onLight keep their specified precedence.
960
- const adaptationCss = generateAdaptationCSS(theme, warnings);
864
+ const adaptationCss = generateAdaptationCSS(theme);
961
865
  if (adaptationCss.component) {
962
866
  componentCss = componentCss ? `${componentCss}\n\n${adaptationCss.component}` : adaptationCss.component;
963
867
  }
964
- const onMediaCss = generateOnMediaCSS(theme, warnings);
868
+ const onMediaCss = generateOnMediaCSS(theme);
965
869
  if (onMediaCss) {
966
870
  componentCss = componentCss ? `${componentCss}\n\n${onMediaCss}` : onMediaCss;
967
871
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.6.3-canary.f22695a",
3
+ "version": "0.6.3",
4
4
  "displayName": "Astryx Core",
5
5
  "description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
6
6
  "author": "Meta Open Source",
@@ -539,11 +539,6 @@
539
539
  "types": "./dist/TimeInput/index.d.ts",
540
540
  "default": "./dist/TimeInput/index.js"
541
541
  },
542
- "./Timer": {
543
- "source": "./src/Timer/index.ts",
544
- "types": "./dist/Timer/index.d.ts",
545
- "default": "./dist/Timer/index.js"
546
- },
547
542
  "./Timestamp": {
548
543
  "source": "./src/Timestamp/index.ts",
549
544
  "types": "./dist/Timestamp/index.d.ts",
@@ -692,8 +687,6 @@
692
687
  "react-dom": ">=19.0.0"
693
688
  },
694
689
  "devDependencies": {
695
- "@astryxdesign/a11y-spec": "0.6.3-canary.f22695a",
696
- "@astryxdesign/cli": "0.6.3-canary.f22695a",
697
690
  "@babel/cli": "^7.29.7",
698
691
  "@babel/core": "^7.29.7",
699
692
  "@babel/preset-react": "^7.29.7",
@@ -703,7 +696,9 @@
703
696
  "@testing-library/dom": "^10.0.0",
704
697
  "@testing-library/jest-dom": "^6.6.0",
705
698
  "@testing-library/react": "^16.3.2",
706
- "@types/babel__core": "^7.20.5"
699
+ "@types/babel__core": "^7.20.5",
700
+ "@astryxdesign/a11y-spec": "0.0.0",
701
+ "@astryxdesign/cli": "0.6.3"
707
702
  },
708
703
  "dependencies": {
709
704
  "intl-messageformat": "^11.2.9"
@@ -157,8 +157,8 @@ export const docs = {
157
157
  name: 'icon',
158
158
  type: 'ReactNode',
159
159
  description:
160
- 'Icon element rendered before the label text. An Astryx Icon with no explicit size defaults to sm for sm/md buttons and md for lg buttons.',
161
- slotElements: [{__element: 'Icon', props: {icon: 'check'}}],
160
+ 'Icon element rendered before the label text.',
161
+ slotElements: [{__element: 'Icon', props: {icon: 'check', size: 'sm'}}],
162
162
  },
163
163
  {
164
164
  name: 'isIconOnly',
@@ -210,7 +210,7 @@ export const docs = {
210
210
  name: 'href',
211
211
  type: 'string',
212
212
  description:
213
- 'When provided, renders the button as a link element (<a> or custom link component). The destination follows the shared navigation rule described on the Link `href` prop.',
213
+ 'When provided, renders the button as a link element (<a> or custom link component).',
214
214
  },
215
215
  {
216
216
  name: 'as',
@@ -294,7 +294,7 @@ export const docsZh = {
294
294
  description: '禁用按钮。存在工具提示时,使用 aria-disabled 代替原生 disabled 以保持可聚焦。',
295
295
  default: 'false',
296
296
  },
297
- {name: 'icon', type: 'ReactNode', description: '图标元素。未显式指定尺寸的 Astryx Icon 在 sm/md 按钮中默认为 sm,在 lg 按钮中默认为 md。仅提供 icon 而不提供 children 时,按钮渲染为正方形的纯图标按钮。'},
297
+ {name: 'icon', type: 'ReactNode', description: '图标元素。仅提供 icon 而不提供 children 时,按钮渲染为正方形的纯图标按钮。'},
298
298
  {name: 'width', type: 'SizeValue', description: "按钮宽度。数字按像素处理,字符串按原样使用(如 '100%' 表示全宽按钮)。默认按内容自适应宽度。"},
299
299
  {name: 'children', type: 'ReactNode', description: '可选的可见内容覆盖;label 仍然是必需的(用于无障碍名称)。大多数情况使用 <Button label="Save" />。'},
300
300
  {
@@ -361,7 +361,7 @@ export const docsDense = {
361
361
  value: 'HTML value for form submission',
362
362
  form: 'associates button with form element by ID',
363
363
  isLoading: 'shows spinner+disables interaction; announces via live region',
364
- icon: 'icon element rendered before label text; unsized Astryx Icon defaults to sm for sm/md buttons and md for lg',
364
+ icon: 'icon element rendered before label text',
365
365
  isIconOnly: 'when true, renders square icon-only button; label becomes aria-label',
366
366
  width: "Width of button. Numbers=pixels, strings=as-is (e.g. '100%' for full-width).",
367
367
  children: 'optional visible override; label is still required for a11y. Prefer <Button label="Save" /> over using children',
@@ -38,8 +38,6 @@ import {
38
38
  } from '../theme/tokens.stylex';
39
39
  import {Spinner} from '../Spinner';
40
40
  import {VisuallyHidden} from '../VisuallyHidden';
41
- import {IconDefaultSizeProvider} from '../Icon/IconDefaultSizeContext';
42
- import {iconBoxSizeStyles, type IconSize} from '../Icon/IconSize.stylex';
43
41
 
44
42
  import {EDGE_COMP_ATTR} from '../Layout/edgeCompensation.stylex';
45
43
  import {useSize} from '../SizeContext/SizeContext';
@@ -172,6 +170,17 @@ const sizeStyles = stylex.create({
172
170
  },
173
171
  });
174
172
 
173
+ /**
174
+ * Icon size per button size.
175
+ * Matches Icon sizing: sm/md=16px, lg=20px.
176
+ * fontSize is set so emoji and text-based icons scale correctly.
177
+ */
178
+ const iconSizeStyles = stylex.create({
179
+ sm: {width: 16, height: 16, fontSize: 16},
180
+ md: {width: 16, height: 16, fontSize: 16},
181
+ lg: {width: 20, height: 20, fontSize: 20},
182
+ });
183
+
175
184
  /**
176
185
  * Resting elevation for floating buttons (e.g. a FAB). `none` is the default
177
186
  * flat button; `low`/`med`/`high` map to the shadow token scale. 'none' stays
@@ -224,12 +233,6 @@ export type ButtonVariant = keyof ButtonVariantMap;
224
233
  */
225
234
  export type ButtonSize = keyof typeof sizeStyles;
226
235
 
227
- const iconSizeByButtonSize = {
228
- sm: 'sm',
229
- md: 'sm',
230
- lg: 'md',
231
- } satisfies Record<ButtonSize, IconSize>;
232
-
233
236
  export interface ButtonProps extends BaseProps<HTMLButtonElement> {
234
237
  /** Ref forwarded to the root element */
235
238
  ref?: React.Ref<HTMLButtonElement>;
@@ -680,8 +683,6 @@ export function Button({
680
683
  style,
681
684
  );
682
685
 
683
- const iconSize = iconSizeByButtonSize[size];
684
-
685
686
  const buttonContent = (
686
687
  <>
687
688
  {isLoadingState && (
@@ -704,11 +705,8 @@ export function Button({
704
705
  )}
705
706
  aria-hidden={isLoadingState || undefined}>
706
707
  {icon && (
707
- <span
708
- {...stylex.props(styles.iconWrapper, iconBoxSizeStyles[iconSize])}>
709
- <IconDefaultSizeProvider value={iconSize}>
710
- {icon}
711
- </IconDefaultSizeProvider>
708
+ <span {...stylex.props(styles.iconWrapper, iconSizeStyles[size])}>
709
+ {icon}
712
710
  </span>
713
711
  )}
714
712
  {isIconOnly ? null : (
@@ -6,7 +6,7 @@ export const docs = {
6
6
  name: 'ChatLayout',
7
7
  subComponentOf: 'Chat',
8
8
  displayName: 'Chat Layout',
9
- description: 'Layout shell for full chat interfaces. Messages flow in normal page flow, composer is fixed to the bottom with a frosted glass dock. Set density (compact/balanced/spacious) to control spacing, message max-width, and blur layer sizing. Includes built-in auto-scroll, a "New messages" scroll-to-bottom button, and a frosted glass blur layer behind the composer. By default the layout root is the scroll container; pass scrollRef to delegate scrolling to a parent element or the document body.',
9
+ description: 'Layout shell for full chat interfaces. Messages flow in normal page flow, composer is fixed to the bottom with a frosted glass dock. Adapts density (compact/balanced/spacious) automatically via container width observation. Includes built-in auto-scroll, a "New messages" scroll-to-bottom button, and a frosted glass blur layer behind the composer. By default the layout root is the scroll container; pass scrollRef to delegate scrolling to a parent element or the document body.',
10
10
  props: [
11
11
  {
12
12
  name: 'children',
@@ -53,15 +53,9 @@ export const docs = {
53
53
  type: 'React.RefObject<HTMLElement | null>',
54
54
  description: 'External scroll container ref. When provided, auto-scroll and scroll-to-bottom target this element instead of the layout root. Use when the chat is embedded in a page where a parent element or the document body scrolls.',
55
55
  },
56
- {
57
- name: 'density',
58
- type: "'compact' | 'balanced' | 'spacious'",
59
- description: 'Visual density: controls dock padding, message-area max-width and inline padding, and the height of the frosted glass blur layer.',
60
- default: "'balanced'",
61
- },
62
56
  ],
63
57
  usage: {
64
- description: 'ChatLayout is the layout shell for full-page chat interfaces. It renders messages in normal page flow and docks the composer to the bottom with a frosted glass blur layer. Set the density prop to control spacing; it defaults to balanced. Use it to wrap ChatMessageList and ChatComposer for a complete chat experience with built-in auto-scroll and a scroll-to-bottom button.',
58
+ description: 'ChatLayout is the layout shell for full-page chat interfaces. It renders messages in normal page flow and docks the composer to the bottom with a frosted glass blur layer. Density adapts automatically via container width observation. Use it to wrap ChatMessageList and ChatComposer for a complete chat experience with built-in auto-scroll and a scroll-to-bottom button.',
65
59
  bestPractices: [
66
60
  {
67
61
  guidance: true,
@@ -117,21 +111,20 @@ export const docs = {
117
111
  export const docsZh = {
118
112
  name: 'ChatLayout',
119
113
  displayName: 'Chat Layout',
120
- description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过 density 属性控制密度。',
114
+ description: '完整聊天界面的布局外壳。消息在页面中自然流动,编写器固定在底部,带有毛玻璃效果。通过容器宽度自动适配密度。',
121
115
  propDescriptions: {
122
116
  children: '消息内容,通常是 ChatMessageList。在页面中自然流动。',
123
117
  composer: '编写器元素,通常是 ChatComposer。固定在底部,带有毛玻璃底座。',
124
118
  emptyState: '子元素为空时显示的内容。',
125
119
  scrollButton: '编写器上方的滚动到底部按钮。默认使用 ChatLayoutScrollButton。传入 null 隐藏。',
126
120
  scrollRef: '外部滚动容器引用。提供时,自动滚动和滚动到底部将目标指向此元素。',
127
- density: '视觉密度:控制底座内边距、消息区最大宽度和毛玻璃层高度。',
128
121
  },
129
122
  };
130
123
 
131
124
  export const docsDense = {
132
125
  name: 'ChatLayout',
133
126
  displayName: 'Chat Layout',
134
- description: 'layout shell for full chat; msgs in page flow, composer fixed bottom w/ frosted glass dock; density prop (default balanced); scrollRef delegates to parent/body',
127
+ description: 'layout shell for full chat; msgs in page flow, composer fixed bottom w/ frosted glass dock; auto density via container width; scrollRef delegates to parent/body',
135
128
  usage: {
136
129
  bestPractices: [
137
130
  {guidance: true, description: 'Pass ChatMessageList as children and ChatComposer as composer prop for complete chat.'},
@@ -147,6 +140,5 @@ export const docsDense = {
147
140
  emptyState: 'content when children empty',
148
141
  scrollButton: 'scroll-to-bottom btn; defaults to ChatLayoutScrollButton; pass null to hide',
149
142
  scrollRef: 'external scroll container ref; targets parent/body instead of layout root',
150
- density: 'visual density; dock padding, message max-width, blur height',
151
143
  },
152
144
  };
@@ -2,7 +2,6 @@
2
2
 
3
3
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
4
4
  import {act, render, screen} from '@testing-library/react';
5
- import userEvent from '@testing-library/user-event';
6
5
  import {ChatLayout} from './ChatLayout';
7
6
  import {ChatMessageList} from './ChatMessageList';
8
7
  import {ChatMessage} from './ChatMessage';
@@ -104,33 +103,6 @@ describe('ChatLayout', () => {
104
103
  });
105
104
  });
106
105
 
107
- // ---------------------------------------------------------------------------
108
- // Keyboard reachability of the default scroll-to-bottom button
109
- //
110
- // At rest the layout is neither scrolled up nor holding new messages, so the
111
- // default ChatLayoutScrollButton renders in its hidden state. It is still in
112
- // the DOM, and it is the first control in the dock — ahead of the composer.
113
- // A control nobody can see must not be a tab stop: WCAG 2.2 SC 2.4.7 requires
114
- // a visible focus indicator wherever focus can land, and the hidden pill paints
115
- // nothing. `opacity: 0` and `pointer-events: none` do not remove an element
116
- // from sequential focus navigation; only visibility/display/inert do.
117
- // ---------------------------------------------------------------------------
118
-
119
- describe('ChatLayout — default scroll button keyboard reachability', () => {
120
- it('does not make the resting scroll-to-bottom button the first tab stop', async () => {
121
- const user = userEvent.setup();
122
- render(
123
- <ChatLayout composer={<button type="button">Send</button>}>
124
- <div>msg</div>
125
- </ChatLayout>,
126
- );
127
-
128
- await user.tab();
129
-
130
- expect(screen.getByRole('button', {name: 'Send'})).toHaveFocus();
131
- });
132
- });
133
-
134
106
  // ---------------------------------------------------------------------------
135
107
  // Self-scroll layout contract (#2573)
136
108
  //
@@ -268,25 +268,6 @@ function hasVisibleContent(children: ReactNode): boolean {
268
268
  // Component
269
269
  // =============================================================================
270
270
 
271
- /**
272
- * Layout shell for a full chat interface: messages in page flow, composer
273
- * docked to the bottom behind a frosted glass layer, with auto-scroll and a
274
- * scroll-to-bottom button wired in.
275
- *
276
- * @example
277
- * ```
278
- * <ChatLayout
279
- * density="spacious"
280
- * composer={<ChatComposer onSubmit={send} />}
281
- * emptyState={<EmptyState title="No messages yet" />}>
282
- * <ChatMessageList>
283
- * <ChatMessage sender="assistant">
284
- * <ChatMessageBubble>How can I help?</ChatMessageBubble>
285
- * </ChatMessage>
286
- * </ChatMessageList>
287
- * </ChatLayout>
288
- * ```
289
- */
290
271
  export function ChatLayout({
291
272
  children,
292
273
  composer,
@@ -2,7 +2,6 @@
2
2
 
3
3
  import {describe, it, expect} from 'vitest';
4
4
  import {render, screen} from '@testing-library/react';
5
- import userEvent from '@testing-library/user-event';
6
5
  import {ChatLayoutScrollButton} from './ChatLayoutScrollButton';
7
6
 
8
7
  describe('ChatLayoutScrollButton', () => {
@@ -53,34 +52,4 @@ describe('ChatLayoutScrollButton', () => {
53
52
  expect(root).toHaveAttribute('data-custom', 'x');
54
53
  expect(root).toHaveAttribute('id', 'scroll-1');
55
54
  });
56
-
57
- // The hidden state paints nothing, so focus landing on it would have no
58
- // visible indicator (WCAG 2.2 SC 2.4.7). `opacity: 0` and
59
- // `pointer-events: none` suppress paint and the pointer but leave the button
60
- // in sequential focus navigation, so the hidden state must also stop being
61
- // focusable — without costing the visible state its keyboard access.
62
- it('is keyboard reachable while visible', async () => {
63
- const user = userEvent.setup();
64
- render(<ChatLayoutScrollButton isVisible onClick={() => {}} />);
65
-
66
- await user.tab();
67
-
68
- expect(
69
- screen.getByRole('button', {name: 'Scroll to bottom'}),
70
- ).toHaveFocus();
71
- });
72
-
73
- it('is not keyboard reachable while hidden', async () => {
74
- const user = userEvent.setup();
75
- render(
76
- <>
77
- <ChatLayoutScrollButton isVisible={false} onClick={() => {}} />
78
- <button type="button">After</button>
79
- </>,
80
- );
81
-
82
- await user.tab();
83
-
84
- expect(screen.getByRole('button', {name: 'After'})).toHaveFocus();
85
- });
86
55
  });