@astryxdesign/core 0.4.2 → 0.4.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 (79) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/Avatar/Avatar.d.ts.map +1 -1
  3. package/dist/Avatar/Avatar.js +3 -19
  4. package/dist/Banner/Banner.d.ts +6 -2
  5. package/dist/Banner/Banner.d.ts.map +1 -1
  6. package/dist/Banner/Banner.js +71 -30
  7. package/dist/Banner/index.d.ts +14 -12
  8. package/dist/Banner/index.d.ts.map +1 -1
  9. package/dist/Banner/index.js +10 -8
  10. package/dist/Chat/ChatTokenizedText.js +1 -1
  11. package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
  12. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  13. package/dist/ComplexSelector/ComplexSelector.js +96 -35
  14. package/dist/ComplexSelector/index.d.ts +2 -2
  15. package/dist/ComplexSelector/index.d.ts.map +1 -1
  16. package/dist/ComplexSelector/index.js +1 -1
  17. package/dist/Markdown/parser.d.ts.map +1 -1
  18. package/dist/Markdown/parser.js +55 -12
  19. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  20. package/dist/PowerSearch/PowerSearch.js +4 -1
  21. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  22. package/dist/PowerSearch/formatFilterValue.js +2 -4
  23. package/dist/Table/columnUtils.d.ts.map +1 -1
  24. package/dist/Table/columnUtils.js +4 -1
  25. package/dist/TextArea/TextArea.d.ts +6 -3
  26. package/dist/TextArea/TextArea.d.ts.map +1 -1
  27. package/dist/TextArea/TextArea.js +17 -6
  28. package/dist/TreeList/TreeList.js +2 -1
  29. package/dist/astryx.css +6 -4
  30. package/dist/astryx.umd.js +51 -51
  31. package/dist/astryx.umd.js.map +4 -4
  32. package/dist/theme/defineTheme.d.ts +8 -3
  33. package/dist/theme/defineTheme.d.ts.map +1 -1
  34. package/dist/theme/defineTheme.js +36 -47
  35. package/dist/theme/mergeComponents.d.ts +20 -0
  36. package/dist/theme/mergeComponents.d.ts.map +1 -0
  37. package/dist/theme/mergeComponents.js +56 -0
  38. package/dist/theme/onMediaTokens.d.ts +6 -1
  39. package/dist/theme/onMediaTokens.d.ts.map +1 -1
  40. package/dist/theme/onMediaTokens.js +11 -3
  41. package/dist/utils/characters.d.ts +27 -0
  42. package/dist/utils/characters.d.ts.map +1 -0
  43. package/dist/utils/characters.js +83 -0
  44. package/dist/utils/index.d.ts +1 -0
  45. package/dist/utils/index.d.ts.map +1 -1
  46. package/dist/utils/index.js +1 -0
  47. package/package.json +2 -2
  48. package/src/Avatar/Avatar.test.tsx +6 -1
  49. package/src/Avatar/Avatar.tsx +3 -21
  50. package/src/Banner/Banner.doc.mjs +9 -7
  51. package/src/Banner/Banner.test.tsx +68 -0
  52. package/src/Banner/Banner.tsx +97 -36
  53. package/src/Banner/index.ts +15 -13
  54. package/src/Chat/ChatTokenizedText.tsx +1 -1
  55. package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
  56. package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
  57. package/src/ComplexSelector/ComplexSelector.tsx +153 -32
  58. package/src/ComplexSelector/index.ts +3 -1
  59. package/src/Markdown/parser.test.ts +53 -0
  60. package/src/Markdown/parser.ts +53 -12
  61. package/src/PowerSearch/PowerSearch.test.tsx +48 -3
  62. package/src/PowerSearch/PowerSearch.tsx +4 -1
  63. package/src/PowerSearch/formatFilterValue.test.ts +22 -0
  64. package/src/PowerSearch/formatFilterValue.ts +2 -4
  65. package/src/Table/Table.test.tsx +6 -0
  66. package/src/Table/columnUtils.ts +3 -1
  67. package/src/TextArea/TextArea.doc.mjs +1 -1
  68. package/src/TextArea/TextArea.test.tsx +72 -0
  69. package/src/TextArea/TextArea.tsx +26 -8
  70. package/src/TreeList/TreeList.doc.mjs +2 -2
  71. package/src/TreeList/TreeList.tsx +1 -1
  72. package/src/theme/defineTheme.test.ts +127 -0
  73. package/src/theme/defineTheme.ts +48 -51
  74. package/src/theme/extensibleAxes.test.ts +365 -0
  75. package/src/theme/mergeComponents.ts +59 -0
  76. package/src/theme/onMediaTokens.ts +9 -2
  77. package/src/utils/characters.test.ts +141 -0
  78. package/src/utils/characters.ts +88 -0
  79. package/src/utils/index.ts +2 -0
@@ -0,0 +1,141 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file characters.test.ts
5
+ * @input character-counting utilities
6
+ * @output Tests for characterCount, firstCharacter, truncateCharacters
7
+ */
8
+
9
+ import {describe, it, expect, vi, afterEach} from 'vitest';
10
+ import {characterCount, firstCharacter, truncateCharacters} from './characters';
11
+
12
+ // Multi-code-unit fixtures: an emoji surrogate pair (2 units), a flag
13
+ // sequence (4 units), a ZWJ family (11 units), and a combining mark (2 units).
14
+ const EMOJI = '\u{1F600}'; // 😀
15
+ const FLAG = '\u{1F1F9}\u{1F1F7}'; // 🇹🇷
16
+ const FAMILY = '\u{1F468}‍\u{1F469}‍\u{1F467}‍\u{1F466}'; // 👨‍👩‍👧‍👦
17
+ // é as base + combining mark, spelled as an escape so NFC-normalizing
18
+ // tools cannot silently precompose the fixture into single-code-unit é.
19
+ const E_ACUTE = 'e\u0301';
20
+
21
+ describe('characterCount', () => {
22
+ it('returns 0 for the empty string', () => {
23
+ expect(characterCount('')).toBe(0);
24
+ });
25
+
26
+ it('counts ASCII one per character', () => {
27
+ expect(characterCount('hello')).toBe(5);
28
+ });
29
+
30
+ it('counts an emoji surrogate pair as one', () => {
31
+ expect(characterCount(EMOJI.repeat(2))).toBe(2);
32
+ expect(EMOJI.repeat(2).length).toBe(4); // sanity: code units differ
33
+ });
34
+
35
+ it('counts a flag sequence as one', () => {
36
+ expect(characterCount(FLAG)).toBe(1);
37
+ });
38
+
39
+ it('counts a ZWJ family emoji as one', () => {
40
+ expect(characterCount(FAMILY)).toBe(1);
41
+ expect(FAMILY.length).toBe(11); // sanity: code units differ
42
+ });
43
+
44
+ it('counts a combining-mark character as one', () => {
45
+ expect(characterCount(E_ACUTE)).toBe(1);
46
+ expect(E_ACUTE.length).toBe(2); // sanity: code units differ
47
+ });
48
+
49
+ it('counts mixed content by user-perceived characters', () => {
50
+ expect(characterCount(`a${EMOJI}b`)).toBe(3);
51
+ });
52
+ });
53
+
54
+ describe('firstCharacter', () => {
55
+ it('returns the empty string for the empty string', () => {
56
+ expect(firstCharacter('')).toBe('');
57
+ });
58
+
59
+ it('returns the first ASCII character', () => {
60
+ expect(firstCharacter('abc')).toBe('a');
61
+ });
62
+
63
+ it('returns a whole emoji, not half a surrogate pair', () => {
64
+ expect(firstCharacter(`${EMOJI}x`)).toBe(EMOJI);
65
+ });
66
+
67
+ it('returns a whole ZWJ family emoji', () => {
68
+ expect(firstCharacter(`${FAMILY}x`)).toBe(FAMILY);
69
+ });
70
+
71
+ it('keeps a combining mark attached to its base', () => {
72
+ expect(firstCharacter(`${E_ACUTE}cole`)).toBe(E_ACUTE);
73
+ });
74
+ });
75
+
76
+ describe('truncateCharacters', () => {
77
+ it('returns strings within max unchanged', () => {
78
+ expect(truncateCharacters('abc', 5)).toBe('abc');
79
+ });
80
+
81
+ it('returns strings exactly at max unchanged', () => {
82
+ expect(truncateCharacters('abcde', 5)).toBe('abcde');
83
+ });
84
+
85
+ it('truncates so the result, including the ellipsis, is max characters', () => {
86
+ expect(truncateCharacters('abcdefghij', 5)).toBe('abcd…');
87
+ });
88
+
89
+ it('counts a multi-character ellipsis against max', () => {
90
+ // "..." is 3 characters, so 5 content characters remain.
91
+ expect(truncateCharacters('aaaaaaaaaa', 8, '...')).toBe('aaaaa...');
92
+ });
93
+
94
+ it('passes strings within max through under a multi-character ellipsis', () => {
95
+ expect(truncateCharacters('aaaaaaaa', 8, '...')).toBe('aaaaaaaa');
96
+ });
97
+
98
+ it('cuts between characters, never splitting an emoji', () => {
99
+ // The exact-string assertion also proves no surrogate pair was split.
100
+ expect(truncateCharacters(EMOJI.repeat(4), 3)).toBe(`${EMOJI.repeat(2)}…`);
101
+ });
102
+
103
+ it('treats a ZWJ family emoji as a single unit when cutting', () => {
104
+ expect(truncateCharacters(`${FAMILY}abc`, 2)).toBe(`${FAMILY}…`);
105
+ });
106
+
107
+ it('returns the empty string unchanged', () => {
108
+ expect(truncateCharacters('', 5)).toBe('');
109
+ });
110
+
111
+ it('degrades to just the ellipsis when max is smaller than the ellipsis', () => {
112
+ // Documented degenerate behavior: no consumer passes max below the
113
+ // ellipsis length, but the clamp must stay predictable.
114
+ expect(truncateCharacters('abcdef', 2, '...')).toBe('...');
115
+ expect(truncateCharacters('abcdef', 0)).toBe('…');
116
+ });
117
+ });
118
+
119
+ describe('code-point fallback (no Intl.Segmenter)', () => {
120
+ afterEach(() => {
121
+ vi.unstubAllGlobals();
122
+ vi.resetModules();
123
+ });
124
+
125
+ it('keeps surrogate pairs intact while counting by code points', async () => {
126
+ vi.resetModules();
127
+ // characters.ts only reads Intl.Segmenter, so a minimal stub suffices.
128
+ vi.stubGlobal('Intl', {Segmenter: undefined});
129
+ const fallback = await import('./characters');
130
+
131
+ expect(fallback.characterCount(EMOJI.repeat(2))).toBe(2);
132
+ expect(fallback.firstCharacter(`${EMOJI}x`)).toBe(EMOJI);
133
+ expect(fallback.firstCharacter('')).toBe('');
134
+ expect(fallback.truncateCharacters(EMOJI.repeat(4), 3)).toBe(
135
+ `${EMOJI.repeat(2)}…`,
136
+ );
137
+ // Documented degradation: a flag sequence splits into its two
138
+ // regional-indicator code points under the fallback.
139
+ expect(fallback.characterCount(FLAG)).toBe(2);
140
+ });
141
+ });
@@ -0,0 +1,88 @@
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 =
22
+ typeof Intl.Segmenter === 'function'
23
+ ? new Intl.Segmenter(undefined, {granularity: 'grapheme'})
24
+ : null;
25
+
26
+ /**
27
+ * Split a string into user-perceived characters. The code-point fallback
28
+ * keeps surrogate pairs intact but may split joined emoji sequences and flag
29
+ * pairs on runtimes without Intl.Segmenter.
30
+ */
31
+ function splitCharacters(str: string): string[] {
32
+ if (characterSegmenter) {
33
+ return [...characterSegmenter.segment(str)].map(s => s.segment);
34
+ }
35
+ return [...str];
36
+ }
37
+
38
+ /**
39
+ * Number of characters in a string — the count a person would give, where one
40
+ * emoji, flag, or accented letter is one character. Replacement for `.length`
41
+ * on user-visible strings, which counts internal storage units instead.
42
+ */
43
+ export function characterCount(str: string): number {
44
+ if (str === '') {
45
+ return 0;
46
+ }
47
+ return splitCharacters(str).length;
48
+ }
49
+
50
+ /**
51
+ * The first character of a string, or '' when empty. Replacement for
52
+ * `.charAt(0)`, which can return half of an emoji. Reads only the first
53
+ * character, so cost does not scale with the length of the string.
54
+ */
55
+ export function firstCharacter(str: string): string {
56
+ if (characterSegmenter) {
57
+ const first = characterSegmenter.segment(str)[Symbol.iterator]().next();
58
+ return first.done ? '' : first.value.segment;
59
+ }
60
+ const codePoint = str.codePointAt(0);
61
+ return codePoint == null ? '' : String.fromCodePoint(codePoint);
62
+ }
63
+
64
+ /**
65
+ * Truncate to at most `max` characters, ellipsis included: strings within
66
+ * `max` characters pass through unchanged; longer strings are cut so the
67
+ * result — content plus `ellipsis` — is exactly `max` characters (or just the
68
+ * ellipsis, when `max` is smaller than the ellipsis itself). Replacement for
69
+ * `str.slice(0, n) + '…'`, which can cut an emoji in half.
70
+ *
71
+ * @example
72
+ * ```
73
+ * truncateCharacters('abcdefghij', 5) // 'abcd…'
74
+ * truncateCharacters('abcdefghij', 8, '...') // 'abcde...'
75
+ * ```
76
+ */
77
+ export function truncateCharacters(
78
+ str: string,
79
+ max: number,
80
+ ellipsis = '…',
81
+ ): string {
82
+ const characters = splitCharacters(str);
83
+ if (characters.length <= max) {
84
+ return str;
85
+ }
86
+ const keep = Math.max(max - characterCount(ellipsis), 0);
87
+ return characters.slice(0, keep).join('') + ellipsis;
88
+ }
@@ -70,6 +70,8 @@ export type {ISOTimeString, ParsedTime} from './timeParser';
70
70
  export {parseStyleKey} from './parseStyleKey';
71
71
  export {getKey, type Key, type KeyFallback} from './getKey';
72
72
 
73
+ export {characterCount, firstCharacter, truncateCharacters} from './characters';
74
+
73
75
  export {mergeProps} from './mergeProps';
74
76
  export {mergeRefs} from './mergeRefs';
75
77
  export {isFocusDetached} from './focusReturn';