@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.
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +28 -13
- package/dist/Chat/ChatLayout.d.ts +0 -19
- package/dist/Chat/ChatLayout.d.ts.map +1 -1
- package/dist/Chat/ChatLayout.js +0 -19
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +34 -44
- package/dist/Citation/Citation.d.ts +1 -1
- package/dist/Citation/Citation.d.ts.map +1 -1
- package/dist/Citation/Citation.js +2 -3
- package/dist/CodeBlock/highlightRanges.d.ts.map +1 -1
- package/dist/CodeBlock/highlightRanges.js +3 -14
- package/dist/Icon/Icon.d.ts +31 -6
- package/dist/Icon/Icon.d.ts.map +1 -1
- package/dist/Icon/Icon.js +71 -8
- package/dist/Link/useLinkComponent.d.ts +15 -5
- package/dist/Link/useLinkComponent.d.ts.map +1 -1
- package/dist/Link/useLinkComponent.js +51 -34
- package/dist/Markdown/Markdown.js +2 -2
- package/dist/Markdown/url.d.ts +0 -2
- package/dist/Markdown/url.d.ts.map +1 -1
- package/dist/Markdown/url.js +2 -8
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +0 -5
- package/dist/TopNav/TopNavMenu.d.ts +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -3
- package/dist/astryx.css +4 -2
- package/dist/hooks/useClickableContainer.d.ts +1 -10
- package/dist/hooks/useClickableContainer.d.ts.map +1 -1
- package/dist/hooks/useClickableContainer.js +4 -15
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -3
- package/dist/theme/generateThemeRules.d.ts +7 -13
- package/dist/theme/generateThemeRules.d.ts.map +1 -1
- package/dist/theme/generateThemeRules.js +35 -131
- package/package.json +4 -9
- package/src/Button/Button.doc.mjs +5 -5
- package/src/Button/Button.tsx +13 -15
- package/src/Chat/ChatLayout.doc.mjs +4 -12
- package/src/Chat/ChatLayout.test.tsx +0 -28
- package/src/Chat/ChatLayout.tsx +0 -19
- package/src/Chat/ChatLayoutScrollButton.test.tsx +0 -31
- package/src/Chat/ChatLayoutScrollButton.tsx +14 -31
- package/src/Citation/Citation.doc.mjs +2 -2
- package/src/Citation/Citation.test.tsx +0 -17
- package/src/Citation/Citation.tsx +2 -4
- package/src/ClickableCard/ClickableCard.doc.mjs +2 -2
- package/src/CodeBlock/highlightRanges.test.ts +1 -143
- package/src/CodeBlock/highlightRanges.ts +5 -15
- package/src/Icon/Icon.doc.mjs +5 -7
- package/src/Icon/Icon.spec.md +40 -81
- package/src/Icon/Icon.test.tsx +3 -6
- package/src/Icon/Icon.tsx +68 -20
- package/src/IconButton/IconButton.doc.mjs +3 -3
- package/src/IconButton/IconButton.test.tsx +0 -40
- package/src/Item/Item.doc.mjs +2 -2
- package/src/Link/Link.doc.mjs +4 -6
- package/src/Link/LinkProvider.doc.mjs +2 -2
- package/src/Link/useLinkComponent.test.tsx +0 -333
- package/src/Link/useLinkComponent.ts +57 -29
- package/src/List/ListItem.doc.mjs +2 -2
- package/src/Markdown/Markdown.doc.mjs +2 -2
- package/src/Markdown/Markdown.spec.md +15 -19
- package/src/Markdown/Markdown.test.tsx +0 -52
- package/src/Markdown/Markdown.tsx +2 -2
- package/src/Markdown/parser.test.ts +0 -20
- package/src/Markdown/url.ts +3 -9
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +0 -51
- package/src/PowerSearch/PowerSearchValueEditor.tsx +0 -4
- package/src/Token/Token.doc.mjs +2 -2
- package/src/TopNav/TopNavMenu.doc.mjs +2 -2
- package/src/TopNav/TopNavMenu.test.tsx +0 -30
- package/src/TopNav/TopNavMenu.tsx +2 -7
- package/src/hooks/useClickableContainer.ts +4 -20
- package/src/index.ts +0 -3
- package/src/tailwind-theme.css +6 -0
- package/src/theme/Theme.test.tsx +0 -32
- package/src/theme/generateThemeRules.test.ts +2 -470
- package/src/theme/generateThemeRules.ts +51 -219
- package/src/theme/tokenValueCompat.test.ts +1 -4
- package/dist/Icon/IconDefaultSizeContext.d.ts +0 -4
- package/dist/Icon/IconDefaultSizeContext.d.ts.map +0 -1
- package/dist/Icon/IconDefaultSizeContext.js +0 -18
- package/dist/Icon/IconSize.stylex.d.ts +0 -65
- package/dist/Icon/IconSize.stylex.d.ts.map +0 -1
- package/dist/Icon/IconSize.stylex.js +0 -75
- package/dist/Timer/Timer.d.ts +0 -49
- package/dist/Timer/Timer.d.ts.map +0 -1
- package/dist/Timer/Timer.js +0 -158
- package/dist/Timer/index.d.ts +0 -9
- package/dist/Timer/index.d.ts.map +0 -1
- package/dist/Timer/index.js +0 -10
- package/dist/theme/declarationBoundary.d.ts +0 -17
- package/dist/theme/declarationBoundary.d.ts.map +0 -1
- package/dist/theme/declarationBoundary.js +0 -343
- package/dist/utils/safeUrl.d.ts +0 -11
- package/dist/utils/safeUrl.d.ts.map +0 -1
- package/dist/utils/safeUrl.js +0 -78
- package/src/Chat/ChatLayout.spec.md +0 -271
- package/src/Chat/ChatLayoutScrollButton.spec.md +0 -338
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +0 -514
- package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +0 -740
- package/src/Icon/IconDefaultSizeContext.ts +0 -23
- package/src/Icon/IconSize.stylex.ts +0 -70
- package/src/Link/__tests__/Link.navigation.a11y.chromium.spec.ts +0 -261
- package/src/Markdown/Markdown.renderBoundary.test.tsx +0 -72
- package/src/Timer/Timer.doc.mjs +0 -183
- package/src/Timer/Timer.spec.md +0 -215
- package/src/Timer/Timer.test.tsx +0 -302
- package/src/Timer/Timer.tsx +0 -258
- package/src/Timer/index.ts +0 -11
- package/src/hooks/useClickableContainer.test.tsx +0 -236
- package/src/theme/declarationBoundary.test.ts +0 -126
- package/src/theme/declarationBoundary.ts +0 -347
- package/src/utils/safeUrl.test.ts +0 -88
- 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)
|
|
21
|
-
* @output CSS rule strings, split by layer (component vs prose)
|
|
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
|
|
306
|
+
export function generateThemeRules(theme) {
|
|
385
307
|
const parts = [];
|
|
386
|
-
const
|
|
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
|
|
395
|
-
if (
|
|
396
|
-
|
|
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 (
|
|
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'
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
733
|
-
const allRules = generateThemeRules(theme
|
|
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
|
|
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 =
|
|
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
|
|
710
|
+
function generateAdaptationRuleRules(rule) {
|
|
805
711
|
const parts = [];
|
|
806
|
-
const
|
|
807
|
-
if (
|
|
808
|
-
|
|
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
|
|
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
|
|
861
|
-
const parts = generateAdaptationRuleRules(rule
|
|
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
|
|
800
|
+
/** Indent a generated rule one level further, for nesting inside `@media`. */
|
|
897
801
|
function indentRule(rule) {
|
|
898
|
-
return ` ${
|
|
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
|
|
841
|
+
export function generateThemeCSS(theme) {
|
|
938
842
|
const {
|
|
939
843
|
component,
|
|
940
844
|
prose
|
|
941
|
-
} = generateThemeRulesSplit(theme
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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).
|
|
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: '
|
|
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
|
|
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',
|
package/src/Button/Button.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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.
|
|
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.
|
|
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: '
|
|
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
|
|
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
|
//
|
package/src/Chat/ChatLayout.tsx
CHANGED
|
@@ -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
|
});
|