@astryxdesign/core 0.4.2 → 0.4.3-canary.ac850d9

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 (85) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +79 -45
  3. package/dist/Avatar/Avatar.d.ts.map +1 -1
  4. package/dist/Avatar/Avatar.js +3 -19
  5. package/dist/Banner/Banner.d.ts +6 -2
  6. package/dist/Banner/Banner.d.ts.map +1 -1
  7. package/dist/Banner/Banner.js +71 -30
  8. package/dist/Banner/index.d.ts +14 -12
  9. package/dist/Banner/index.d.ts.map +1 -1
  10. package/dist/Banner/index.js +10 -8
  11. package/dist/Chat/ChatTokenizedText.js +1 -1
  12. package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
  13. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  14. package/dist/ComplexSelector/ComplexSelector.js +96 -35
  15. package/dist/ComplexSelector/index.d.ts +2 -2
  16. package/dist/ComplexSelector/index.d.ts.map +1 -1
  17. package/dist/ComplexSelector/index.js +1 -1
  18. package/dist/Markdown/parser.d.ts.map +1 -1
  19. package/dist/Markdown/parser.js +55 -12
  20. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  21. package/dist/PowerSearch/PowerSearch.js +4 -1
  22. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  23. package/dist/PowerSearch/formatFilterValue.js +2 -4
  24. package/dist/Table/columnUtils.d.ts.map +1 -1
  25. package/dist/Table/columnUtils.js +4 -1
  26. package/dist/TextArea/TextArea.d.ts +6 -3
  27. package/dist/TextArea/TextArea.d.ts.map +1 -1
  28. package/dist/TextArea/TextArea.js +17 -6
  29. package/dist/TreeList/TreeList.js +2 -1
  30. package/dist/astryx.css +6 -4
  31. package/dist/theme/defineTheme.d.ts +30 -7
  32. package/dist/theme/defineTheme.d.ts.map +1 -1
  33. package/dist/theme/defineTheme.js +36 -47
  34. package/dist/theme/expandColorScale.d.ts +17 -2
  35. package/dist/theme/expandColorScale.d.ts.map +1 -1
  36. package/dist/theme/expandColorScale.js +63 -33
  37. package/dist/theme/mergeComponents.d.ts +20 -0
  38. package/dist/theme/mergeComponents.d.ts.map +1 -0
  39. package/dist/theme/mergeComponents.js +56 -0
  40. package/dist/theme/onMediaTokens.d.ts +6 -1
  41. package/dist/theme/onMediaTokens.d.ts.map +1 -1
  42. package/dist/theme/onMediaTokens.js +11 -3
  43. package/dist/utils/characters.d.ts +27 -0
  44. package/dist/utils/characters.d.ts.map +1 -0
  45. package/dist/utils/characters.js +83 -0
  46. package/dist/utils/index.d.ts +1 -0
  47. package/dist/utils/index.d.ts.map +1 -1
  48. package/dist/utils/index.js +1 -0
  49. package/package.json +4 -8
  50. package/src/Avatar/Avatar.test.tsx +6 -1
  51. package/src/Avatar/Avatar.tsx +3 -21
  52. package/src/Banner/Banner.doc.mjs +9 -7
  53. package/src/Banner/Banner.test.tsx +68 -0
  54. package/src/Banner/Banner.tsx +97 -36
  55. package/src/Banner/index.ts +15 -13
  56. package/src/Chat/ChatTokenizedText.tsx +1 -1
  57. package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
  58. package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
  59. package/src/ComplexSelector/ComplexSelector.tsx +153 -32
  60. package/src/ComplexSelector/index.ts +3 -1
  61. package/src/Markdown/parser.test.ts +53 -0
  62. package/src/Markdown/parser.ts +53 -12
  63. package/src/PowerSearch/PowerSearch.test.tsx +48 -3
  64. package/src/PowerSearch/PowerSearch.tsx +4 -1
  65. package/src/PowerSearch/formatFilterValue.test.ts +22 -0
  66. package/src/PowerSearch/formatFilterValue.ts +2 -4
  67. package/src/Table/Table.test.tsx +6 -0
  68. package/src/Table/columnUtils.ts +3 -1
  69. package/src/TextArea/TextArea.doc.mjs +1 -1
  70. package/src/TextArea/TextArea.test.tsx +72 -0
  71. package/src/TextArea/TextArea.tsx +26 -8
  72. package/src/TreeList/TreeList.doc.mjs +2 -2
  73. package/src/TreeList/TreeList.tsx +1 -1
  74. package/src/theme/defineTheme.test.ts +127 -0
  75. package/src/theme/defineTheme.ts +70 -55
  76. package/src/theme/expandColorScale.test.ts +138 -0
  77. package/src/theme/expandColorScale.ts +89 -44
  78. package/src/theme/extensibleAxes.test.ts +365 -0
  79. package/src/theme/mergeComponents.ts +59 -0
  80. package/src/theme/onMediaTokens.ts +9 -2
  81. package/src/utils/characters.test.ts +141 -0
  82. package/src/utils/characters.ts +88 -0
  83. package/src/utils/index.ts +2 -0
  84. package/dist/astryx.umd.js +0 -147
  85. package/dist/astryx.umd.js.map +0 -7
@@ -0,0 +1,56 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Component style-map merging
5
+ *
6
+ * One merge rule for component overrides, shared by every layer that composes
7
+ * them: `extends` inheritance, generated type-scale rules, and the on-media
8
+ * (`onDark`/`onLight`) surfaces. Merging is per style key, so a child that
9
+ * restates one property of `button.base` keeps the rest of the base's.
10
+ *
11
+ * @input two ComponentStyleMaps — the base and the overrides that win
12
+ * @output a merged ComponentStyleMap
13
+ * @position packages/core/src/theme/mergeComponents.ts
14
+ */
15
+
16
+ /**
17
+ * Deep-merge component style maps: `overrides` wins per style key, and every
18
+ * component and key the base declared that the overrides do not mention is
19
+ * carried through untouched.
20
+ */
21
+ export function deepMergeComponents(base, overrides) {
22
+ if (!base && !overrides) {
23
+ return undefined;
24
+ }
25
+ if (!base) {
26
+ return overrides;
27
+ }
28
+ if (!overrides) {
29
+ return base;
30
+ }
31
+ const result = {};
32
+
33
+ // Start with all base entries
34
+ for (const [component, rules] of Object.entries(base)) {
35
+ result[component] = {
36
+ ...rules
37
+ };
38
+ }
39
+
40
+ // Merge overrides on top
41
+ for (const [component, rules] of Object.entries(overrides)) {
42
+ if (!result[component]) {
43
+ result[component] = {
44
+ ...rules
45
+ };
46
+ } else {
47
+ for (const [key, styles] of Object.entries(rules)) {
48
+ result[component][key] = {
49
+ ...result[component][key],
50
+ ...styles
51
+ };
52
+ }
53
+ }
54
+ }
55
+ return result;
56
+ }
@@ -53,6 +53,11 @@ export declare const defaultOnLightTokens: Record<string, string>;
53
53
  /**
54
54
  * Resolve on-media overrides: merge user tokens with defaults,
55
55
  * pass through component overrides.
56
+ *
57
+ * `base` is the already-resolved surface of a theme being extended. It sits
58
+ * between the defaults and this theme's own input, so a child theme inherits
59
+ * the surface customizations of the theme it extends instead of silently
60
+ * reverting them to the defaults.
56
61
  */
57
- export declare function resolveOnMedia(surface: 'dark' | 'light', input?: OnMediaOverrides): ResolvedOnMedia;
62
+ export declare function resolveOnMedia(surface: 'dark' | 'light', input?: OnMediaOverrides, base?: ResolvedOnMedia): ResolvedOnMedia;
58
63
  //# sourceMappingURL=onMediaTokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"onMediaTokens.d.ts","sourceRoot":"","sources":["../../src/theme/onMediaTokens.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAE,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAC7C,yDAAyD;IACzD,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAStD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASvD,CAAC;AAYF;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,GAAG,OAAO,EACzB,KAAK,CAAC,EAAE,gBAAgB,GACvB,eAAe,CAkBjB"}
1
+ {"version":3,"file":"onMediaTokens.d.ts","sourceRoot":"","sources":["../../src/theme/onMediaTokens.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAC,UAAU,EAAE,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAGjE;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAC7C,yDAAyD;IACzD,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,yDAAyD;IACzD,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAStD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CASvD,CAAC;AAYF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,GAAG,OAAO,EACzB,KAAK,CAAC,EAAE,gBAAgB,EACxB,IAAI,CAAC,EAAE,eAAe,GACrB,eAAe,CAkBjB"}
@@ -20,6 +20,8 @@
20
20
  * `onDark`/`onLight` fields in `defineTheme()`.
21
21
  */
22
22
 
23
+ import { deepMergeComponents } from "./mergeComponents.js";
24
+
23
25
  /**
24
26
  * On-media theme overrides — same shape as the main theme but scoped
25
27
  * to a surface luminance context.
@@ -71,11 +73,17 @@ function resolveValue(value) {
71
73
  /**
72
74
  * Resolve on-media overrides: merge user tokens with defaults,
73
75
  * pass through component overrides.
76
+ *
77
+ * `base` is the already-resolved surface of a theme being extended. It sits
78
+ * between the defaults and this theme's own input, so a child theme inherits
79
+ * the surface customizations of the theme it extends instead of silently
80
+ * reverting them to the defaults.
74
81
  */
75
- export function resolveOnMedia(surface, input) {
82
+ export function resolveOnMedia(surface, input, base) {
76
83
  const defaults = surface === 'dark' ? defaultOnDarkTokens : defaultOnLightTokens;
77
84
  const tokens = {
78
- ...defaults
85
+ ...defaults,
86
+ ...base?.tokens
79
87
  };
80
88
  if (input?.tokens) {
81
89
  for (const [key, value] of Object.entries(input.tokens)) {
@@ -86,6 +94,6 @@ export function resolveOnMedia(surface, input) {
86
94
  }
87
95
  return {
88
96
  tokens,
89
- components: input?.components
97
+ components: deepMergeComponents(base?.components, input?.components)
90
98
  };
91
99
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Number of characters in a string — the count a person would give, where one
3
+ * emoji, flag, or accented letter is one character. Replacement for `.length`
4
+ * on user-visible strings, which counts internal storage units instead.
5
+ */
6
+ export declare function characterCount(str: string): number;
7
+ /**
8
+ * The first character of a string, or '' when empty. Replacement for
9
+ * `.charAt(0)`, which can return half of an emoji. Reads only the first
10
+ * character, so cost does not scale with the length of the string.
11
+ */
12
+ export declare function firstCharacter(str: string): string;
13
+ /**
14
+ * Truncate to at most `max` characters, ellipsis included: strings within
15
+ * `max` characters pass through unchanged; longer strings are cut so the
16
+ * result — content plus `ellipsis` — is exactly `max` characters (or just the
17
+ * ellipsis, when `max` is smaller than the ellipsis itself). Replacement for
18
+ * `str.slice(0, n) + '…'`, which can cut an emoji in half.
19
+ *
20
+ * @example
21
+ * ```
22
+ * truncateCharacters('abcdefghij', 5) // 'abcd…'
23
+ * truncateCharacters('abcdefghij', 8, '...') // 'abcde...'
24
+ * ```
25
+ */
26
+ export declare function truncateCharacters(str: string, max: number, ellipsis?: string): string;
27
+ //# sourceMappingURL=characters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"characters.d.ts","sourceRoot":"","sources":["../../src/utils/characters.ts"],"names":[],"mappings":"AAqCA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,QAAQ,SAAM,GACb,MAAM,CAOR"}
@@ -0,0 +1,83 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file characters.ts
5
+ * @input Strings that may contain characters made of several code units
6
+ * (emoji, flags, joined emoji sequences, accented letters)
7
+ * @output Exports characterCount, firstCharacter, truncateCharacters —
8
+ * replacements for .length / .charAt(0) / .slice(0, n) that measure
9
+ * and cut user-visible strings the way a person reads them
10
+ * @position Shared utility; consumed by Avatar, TextArea, PowerSearch, Table
11
+ *
12
+ * SYNC: When modified, update:
13
+ * - /packages/core/src/utils/characters.test.ts
14
+ * - /packages/core/src/utils/index.ts
15
+ */
16
+
17
+ /**
18
+ * Reuse a single segmenter when the runtime supports Intl.Segmenter.
19
+ * `grapheme` is the Intl granularity name for a user-perceived character.
20
+ */
21
+ const characterSegmenter = typeof Intl.Segmenter === 'function' ? new Intl.Segmenter(undefined, {
22
+ granularity: 'grapheme'
23
+ }) : null;
24
+
25
+ /**
26
+ * Split a string into user-perceived characters. The code-point fallback
27
+ * keeps surrogate pairs intact but may split joined emoji sequences and flag
28
+ * pairs on runtimes without Intl.Segmenter.
29
+ */
30
+ function splitCharacters(str) {
31
+ if (characterSegmenter) {
32
+ return [...characterSegmenter.segment(str)].map(s => s.segment);
33
+ }
34
+ return [...str];
35
+ }
36
+
37
+ /**
38
+ * Number of characters in a string — the count a person would give, where one
39
+ * emoji, flag, or accented letter is one character. Replacement for `.length`
40
+ * on user-visible strings, which counts internal storage units instead.
41
+ */
42
+ export function characterCount(str) {
43
+ if (str === '') {
44
+ return 0;
45
+ }
46
+ return splitCharacters(str).length;
47
+ }
48
+
49
+ /**
50
+ * The first character of a string, or '' when empty. Replacement for
51
+ * `.charAt(0)`, which can return half of an emoji. Reads only the first
52
+ * character, so cost does not scale with the length of the string.
53
+ */
54
+ export function firstCharacter(str) {
55
+ if (characterSegmenter) {
56
+ const first = characterSegmenter.segment(str)[Symbol.iterator]().next();
57
+ return first.done ? '' : first.value.segment;
58
+ }
59
+ const codePoint = str.codePointAt(0);
60
+ return codePoint == null ? '' : String.fromCodePoint(codePoint);
61
+ }
62
+
63
+ /**
64
+ * Truncate to at most `max` characters, ellipsis included: strings within
65
+ * `max` characters pass through unchanged; longer strings are cut so the
66
+ * result — content plus `ellipsis` — is exactly `max` characters (or just the
67
+ * ellipsis, when `max` is smaller than the ellipsis itself). Replacement for
68
+ * `str.slice(0, n) + '…'`, which can cut an emoji in half.
69
+ *
70
+ * @example
71
+ * ```
72
+ * truncateCharacters('abcdefghij', 5) // 'abcd…'
73
+ * truncateCharacters('abcdefghij', 8, '...') // 'abcde...'
74
+ * ```
75
+ */
76
+ export function truncateCharacters(str, max, ellipsis = '…') {
77
+ const characters = splitCharacters(str);
78
+ if (characters.length <= max) {
79
+ return str;
80
+ }
81
+ const keep = Math.max(max - characterCount(ellipsis), 0);
82
+ return characters.slice(0, keep).join('') + ellipsis;
83
+ }
@@ -13,6 +13,7 @@ export { parseISOTime, formatISOTime, formatDisplayTime12h, formatDisplayTime24h
13
13
  export type { ISOTimeString, ParsedTime } from './timeParser';
14
14
  export { parseStyleKey } from './parseStyleKey';
15
15
  export { getKey, type Key, type KeyFallback } from './getKey';
16
+ export { characterCount, firstCharacter, truncateCharacters } from './characters';
16
17
  export { mergeProps } from './mergeProps';
17
18
  export { mergeRefs } from './mergeRefs';
18
19
  export { isFocusDetached } from './focusReturn';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAElD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAEhE,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EAAC,IAAI,EAAC,MAAM,SAAS,CAAC;AAElC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAE3E,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAItC,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,YAAY,EAAC,SAAS,EAAC,MAAM,SAAS,CAAC;AAEvC,YAAY,EAAC,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC;AAEhF,OAAO,EACL,cAAc,EACd,SAAS,EACT,QAAQ,EACR,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,wBAAwB,EACxB,uBAAuB,EACvB,8BAA8B,EAC9B,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC;AAElD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAC,aAAa,EAAE,UAAU,EAAC,MAAM,cAAc,CAAC;AAE5D,OAAO,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAC,MAAM,EAAE,KAAK,GAAG,EAAE,KAAK,WAAW,EAAC,MAAM,UAAU,CAAC;AAE5D,OAAO,EAAC,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhF,OAAO,EAAC,UAAU,EAAC,MAAM,cAAc,CAAC;AACxC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,eAAe,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAC,UAAU,EAAE,mBAAmB,EAAC,MAAM,cAAc,CAAC;AAC7D,YAAY,EACV,UAAU,EACV,UAAU,EACV,mBAAmB,EACnB,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAAC,UAAU,EAAE,YAAY,EAAC,MAAM,cAAc,CAAC;AACtD,YAAY,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAC,aAAa,EAAE,eAAe,EAAC,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AACzC,YAAY,EAAC,SAAS,EAAE,mBAAmB,EAAC,MAAM,aAAa,CAAC;AAEhE,OAAO,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,WAAW,EACX,UAAU,GACX,MAAM,SAAS,CAAC;AACjB,YAAY,EAAC,IAAI,EAAC,MAAM,SAAS,CAAC;AAElC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,gBAAgB,EAAC,MAAM,cAAc,CAAC;AAE3E,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AAItC,OAAO,EAAC,kBAAkB,EAAE,iBAAiB,EAAC,MAAM,uBAAuB,CAAC"}
@@ -12,6 +12,7 @@ export { plainDateCreate, plainDateFromISO, plainDateToISO, plainDateToDate, pla
12
12
  export { parseISOTime, formatISOTime, formatDisplayTime12h, formatDisplayTime24h, parseTimeInput, compareTime, isTimeInRange, clampTime, adjustTime, createISOTimeString } from "./timeParser.js";
13
13
  export { parseStyleKey } from "./parseStyleKey.js";
14
14
  export { getKey } from "./getKey.js";
15
+ export { characterCount, firstCharacter, truncateCharacters } from "./characters.js";
15
16
  export { mergeProps } from "./mergeProps.js";
16
17
  export { mergeRefs } from "./mergeRefs.js";
17
18
  export { isFocusDetached } from "./focusReturn.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3-canary.ac850d9",
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",
@@ -27,8 +27,6 @@
27
27
  "type": "module",
28
28
  "main": "./dist/index.js",
29
29
  "types": "./dist/index.d.ts",
30
- "unpkg": "./dist/astryx.umd.js",
31
- "jsdelivr": "./dist/astryx.umd.js",
32
30
  "sideEffects": [
33
31
  "**/*.stylex.ts",
34
32
  "**/*.stylex.js",
@@ -49,7 +47,6 @@
49
47
  "types": "./src/astryx.css.d.ts",
50
48
  "default": "./dist/astryx.css"
51
49
  },
52
- "./astryx.umd.js": "./dist/astryx.umd.js",
53
50
  "./tailwind-theme.css": {
54
51
  "types": "./src/tailwind-theme.css.d.ts",
55
52
  "default": "./src/tailwind-theme.css"
@@ -654,6 +651,7 @@
654
651
  "react-dom": ">=19.0.0"
655
652
  },
656
653
  "devDependencies": {
654
+ "@astryxdesign/cli": "0.4.3-canary.ac850d9",
657
655
  "@babel/cli": "^8.0.4",
658
656
  "@babel/core": "^7.29.7",
659
657
  "@babel/preset-react": "^8.0.1",
@@ -662,19 +660,17 @@
662
660
  "@testing-library/dom": "^10.0.0",
663
661
  "@testing-library/jest-dom": "^6.6.0",
664
662
  "@testing-library/react": "^16.3.2",
665
- "rimraf": "^6.0.1",
666
- "@astryxdesign/cli": "0.4.2"
663
+ "rimraf": "^6.0.1"
667
664
  },
668
665
  "dependencies": {
669
666
  "intl-messageformat": "^11.2.9"
670
667
  },
671
668
  "scripts": {
672
669
  "postinstall": "node scripts/postinstall.mjs",
673
- "build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && pnpm build:umd && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
670
+ "build": "rimraf dist && pnpm build:i18n && pnpm build:esm && tsc --project tsconfig.build.json && pnpm build:css && node ../../scripts/check-no-dev-jsx.mjs && node ../../scripts/check-fully-specified.mjs",
674
671
  "build:i18n": "node scripts/build-pseudo-locale.mjs",
675
672
  "build:esm": "babel src --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.ts,**/*.test.tsx,**/__tests__/**,**/*.perf.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css,**/*.css.d.ts\" --config-file ./babel.config.json",
676
673
  "build:css": "node ../../scripts/build-css.mjs",
677
- "build:umd": "node ../../scripts/build-umd.mjs",
678
674
  "dev": "babel src --watch --out-dir dist --extensions .ts,.tsx --out-file-extension .js --ignore \"**/*.test.*,**/*.doc.mjs,**/*.d.ts,**/*.css*\" --config-file ./babel.config.json",
679
675
  "test": "vitest run --root ../..",
680
676
  "test:watch": "vitest --root ../..",
@@ -91,11 +91,16 @@ describe('Avatar', () => {
91
91
  expect(screen.getByTestId('avatar')).toHaveTextContent('😀A');
92
92
  });
93
93
 
94
- it('preserves a complete grapheme when generating initials', () => {
94
+ it('preserves a complete character when generating initials', () => {
95
95
  render(<Avatar name="🇬🇧 Ada" data-testid="avatar" />);
96
96
  expect(screen.getByTestId('avatar')).toHaveTextContent('🇬🇧A');
97
97
  });
98
98
 
99
+ it('keeps a ZWJ family emoji intact when generating initials', () => {
100
+ render(<Avatar name="👨‍👩‍👧‍👦 Ada" data-testid="avatar" />);
101
+ expect(screen.getByTestId('avatar')).toHaveTextContent('👨‍👩‍👧‍👦A');
102
+ });
103
+
99
104
  it('retries a new src after a previous src failed to load', () => {
100
105
  const {rerender} = render(
101
106
  <Avatar name="Ada" src="https://example.com/broken.jpg" />,
@@ -31,6 +31,7 @@ import {AvatarSizeContext} from './AvatarSizeContext';
31
31
  import {useAvatarGroup} from '../AvatarGroup/AvatarGroupContext';
32
32
  import {mergeProps, mergeRefs} from '../utils';
33
33
  import {themeProps} from '../utils/themeProps';
34
+ import {firstCharacter} from '../utils/characters';
34
35
  import {focusOutlineProps} from '../utils/focusOutline.stylex';
35
36
  import {useTooltip} from '../Tooltip/useTooltip';
36
37
  import {useDevWarning} from '../hooks/useDevWarning';
@@ -317,25 +318,6 @@ export interface AvatarProps extends BaseProps<HTMLDivElement> {
317
318
  onClick?: React.MouseEventHandler<HTMLElement>;
318
319
  }
319
320
 
320
- /**
321
- * Reuse a single segmenter when the runtime supports Intl.Segmenter.
322
- */
323
- const graphemeSegmenter =
324
- typeof Intl.Segmenter === 'function'
325
- ? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
326
- : null;
327
-
328
- /**
329
- * Return the first user-perceived character, with a code-point fallback.
330
- */
331
- function firstGrapheme(word: string): string {
332
- if (graphemeSegmenter) {
333
- return [...graphemeSegmenter.segment(word)][0]?.segment ?? '';
334
- }
335
-
336
- return [...word][0] ?? '';
337
- }
338
-
339
321
  /**
340
322
  * Generates initials from a name string.
341
323
  * Takes the first letter of the first two words.
@@ -351,10 +333,10 @@ function getInitials(name: string): string {
351
333
  return '';
352
334
  }
353
335
  if (words.length === 1) {
354
- return firstGrapheme(words[0]).toUpperCase();
336
+ return firstCharacter(words[0]).toUpperCase();
355
337
  }
356
338
  return (
357
- firstGrapheme(words[0]) + firstGrapheme(words[words.length - 1])
339
+ firstCharacter(words[0]) + firstCharacter(words[words.length - 1])
358
340
  ).toUpperCase();
359
341
  }
360
342
 
@@ -17,11 +17,13 @@ export const docs = {
17
17
  {guidance: true, description: 'Keep titles short and scannable: "Payment failed" not "There was a problem processing your most recent payment."'},
18
18
  {guidance: false, description: 'Use Banner for short-lived messages that disappear on their own; use Toast instead.'},
19
19
  {guidance: false, description: 'Stack multiple banners with the same status; combine related messages into one banner.'},
20
+ {guidance: true, description: 'Error and warning banners render as role="alert"; info and success render as role="status". Mount an alert banner in response to an event rather than on first paint, so assistive tech has a change to report.'},
21
+ {guidance: false, description: 'Rely on the status color or icon alone to carry meaning; say which status it is in the title text, because the icon is decorative to a screen reader.'},
20
22
  ],
21
23
  anatomy: [
22
24
  {name: 'Icon', required: true, description: 'Automatically set based on the status (info, warning, error, success).'},
23
- {name: 'Title', required: false, description: 'The main message. Required if no description is provided.'},
24
- {name: 'Description', required: false, description: 'Additional detail below the title. Required if no title is provided.'},
25
+ {name: 'Title', required: true, description: 'The main message. Always required.'},
26
+ {name: 'Description', required: false, description: 'Additional detail below the title.'},
25
27
  {name: 'Action button', required: false, description: 'A button for the user to act on the message, like "Review" or "Retry".'},
26
28
  {name: 'Dismiss button', required: false, description: 'Lets the user close the banner. Enabled by setting isDismissable.'},
27
29
  {name: 'Collapsible content', required: false, description: 'Extra detail that expands below the banner header, like a list of errors.'},
@@ -123,7 +125,7 @@ export const docs = {
123
125
  {className: 'astryx-banner-content', visualProps: ['container', 'status']},
124
126
  ],
125
127
  vars: [
126
- {name: '--_banner-radius', description: 'Border radius (card container only)', default: 'var(--radius-container)', private: true},
128
+ {name: '--_banner-radius', description: 'Border radius of the card container (header, content area and the elevated root silhouette)', default: 'var(--radius-container)', private: true},
127
129
  ],
128
130
  derived: [
129
131
  {property: 'borderRadius', vars: ['--_banner-radius']},
@@ -148,8 +150,8 @@ export const docsZh = {
148
150
  ],
149
151
  anatomy: [
150
152
  {name: 'Icon', required: true, description: 'Automatically set based on the status (info, warning, error, success).'},
151
- {name: 'Title', required: false, description: 'The main message. Required if no description is provided.'},
152
- {name: 'Description', required: false, description: 'Additional detail below the title. Required if no title is provided.'},
153
+ {name: 'Title', required: true, description: 'The main message. Always required.'},
154
+ {name: 'Description', required: false, description: 'Additional detail below the title.'},
153
155
  {name: 'Action button', required: false, description: 'A button for the user to act on the message, like "Review" or "Retry".'},
154
156
  {name: 'Dismiss button', required: false, description: 'Lets the user close the banner. Enabled by setting isDismissable.'},
155
157
  {name: 'Collapsible content', required: false, description: 'Extra detail that expands below the banner header, like a list of errors.'},
@@ -198,7 +200,7 @@ export const docsZh = {
198
200
  },
199
201
  ],
200
202
  vars: [
201
- {name: '--_banner-radius', description: 'Border radius (card container only)', default: 'var(--radius-container)', private: true},
203
+ {name: '--_banner-radius', description: 'Border radius of the card container (header, content area and the elevated root silhouette)', default: 'var(--radius-container)', private: true},
202
204
  ],
203
205
  derived: [
204
206
  {property: 'borderRadius', vars: ['--_banner-radius']},
@@ -222,7 +224,7 @@ export const docsDense = {
222
224
  ],
223
225
  anatomy: [
224
226
  {name: 'Icon', required: true, description: 'Set automatically from status.'},
225
- {name: 'Title', required: false, description: 'Main message text.'},
227
+ {name: 'Title', required: true, description: 'Main message text.'},
226
228
  {name: 'Description', required: false, description: 'Detail below title.'},
227
229
  {name: 'Action button', required: false, description: 'CTA like Review or Retry.'},
228
230
  {name: 'Dismiss button', required: false, description: 'Close button via isDismissable.'},
@@ -455,4 +455,72 @@ describe('Banner', () => {
455
455
  );
456
456
  });
457
457
  });
458
+
459
+ describe('dismiss focus handoff', () => {
460
+ it('returns focus to where it came from instead of dropping it to body', async () => {
461
+ const user = userEvent.setup();
462
+ render(
463
+ <>
464
+ <button type="button">Before</button>
465
+ <Banner status="info" title="Heads up" isDismissable />
466
+ </>,
467
+ );
468
+ const before = screen.getByRole('button', {name: 'Before'});
469
+ before.focus();
470
+
471
+ await user.tab();
472
+ expect(screen.getByRole('button', {name: 'Dismiss'})).toHaveFocus();
473
+
474
+ await user.keyboard('{Enter}');
475
+
476
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
477
+ expect(before).toHaveFocus();
478
+ expect(document.activeElement).not.toBe(document.body);
479
+ });
480
+
481
+ it('leaves focus alone when it never entered the banner', async () => {
482
+ const user = userEvent.setup();
483
+ render(
484
+ <>
485
+ <button type="button">Elsewhere</button>
486
+ <Banner status="info" title="Heads up" isDismissable />
487
+ </>,
488
+ );
489
+ await user.click(screen.getByRole('button', {name: 'Dismiss'}));
490
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
491
+ });
492
+ });
493
+
494
+ describe('empty slots', () => {
495
+ it('does not show the expand affordance for children that render nothing', () => {
496
+ render(
497
+ <Banner status="info" title="Heads up">
498
+ {false}
499
+ </Banner>,
500
+ );
501
+ expect(
502
+ screen.queryByRole('button', {name: 'Expand'}),
503
+ ).not.toBeInTheDocument();
504
+ });
505
+
506
+ it('still shows the expand affordance for real children', () => {
507
+ render(
508
+ <Banner status="info" title="Heads up">
509
+ <p>Detail</p>
510
+ </Banner>,
511
+ );
512
+ expect(
513
+ screen.getByRole('button', {name: 'Expand'}),
514
+ ).toBeInTheDocument();
515
+ });
516
+
517
+ it('renders no description node for a description that renders nothing', () => {
518
+ const {container} = render(
519
+ <Banner status="info" title="Heads up" description="" />,
520
+ );
521
+ const header = container.firstElementChild!.firstElementChild!;
522
+ // icon wrapper + text column, and the text column holds the title alone
523
+ expect(header.children[1].children).toHaveLength(1);
524
+ });
525
+ });
458
526
  });