@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
@@ -23,7 +23,6 @@ import {
23
23
  spacingVars,
24
24
  radiusVars,
25
25
  shadowVars,
26
- sizeVars,
27
26
  durationVars,
28
27
  easeVars,
29
28
  } from '../theme/tokens.stylex';
@@ -68,14 +67,8 @@ const styles = stylex.create({
68
67
  borderRadius: radiusVars['--radius-full'],
69
68
  backgroundColor: colorVars['--color-background-popover'],
70
69
  boxShadow: shadowVars['--shadow-med'],
71
- // The pill clips its own content, so it must track the height of the
72
- // md Button it wraps. A literal would clip that Button under any theme
73
- // that retunes the element scale.
74
- height: sizeVars['--size-element-md'],
75
- // `visibility` rides the same transition so the fade-out still plays:
76
- // it flips to `visible` immediately on the way in and only at the end
77
- // of the duration on the way out.
78
- transitionProperty: 'opacity, transform, max-width, visibility',
70
+ height: '32px',
71
+ transitionProperty: 'opacity, transform, max-width',
79
72
  transitionTimingFunction: easeVars['--ease-standard'],
80
73
  transitionDuration: {
81
74
  default: durationVars['--duration-fast-max'],
@@ -85,19 +78,14 @@ const styles = stylex.create({
85
78
  hidden: {
86
79
  opacity: 0,
87
80
  pointerEvents: 'none',
88
- // The hidden pill paints nothing, so focus landing on it would have no
89
- // visible indicator (WCAG 2.2 SC 2.4.7). `opacity` and `pointer-events`
90
- // leave the button in sequential focus navigation; `visibility` removes it.
91
- visibility: 'hidden',
92
- maxWidth: sizeVars['--size-element-md'],
81
+ maxWidth: '32px',
93
82
  },
94
83
  visible: {
95
84
  opacity: 1,
96
85
  pointerEvents: 'auto',
97
- visibility: 'visible',
98
86
  },
99
87
  collapsed: {
100
- maxWidth: sizeVars['--size-element-md'],
88
+ maxWidth: '32px',
101
89
  },
102
90
  expanded: {
103
91
  maxWidth: '200px',
@@ -139,25 +127,20 @@ export function ChatLayoutScrollButton({
139
127
  }: ChatLayoutScrollButtonProps) {
140
128
  const t = useTranslator();
141
129
  return (
142
- // Two elements, two responsibilities. The outer one centres the pill and
143
- // holds the gap above the composer — spacing outside the pill's border
144
- // box, which the pill cannot own itself. The inner one is the pill: it is
145
- // what a reader sees, so it carries the painted surface AND the public
146
- // theming target. Keeping the target on the outer element would satisfy
147
- // every automated check while leaving a theme styling an invisible
148
- // full-width row (architecture:component-theming-surface INV4).
149
130
  <div
150
131
  ref={ref}
151
- {...mergeProps(stylex.props(styles.wrapper, xstyle), className, style)}
132
+ {...mergeProps(
133
+ themeProps('chat-layout-scroll-button'),
134
+ stylex.props(styles.wrapper, xstyle),
135
+ className,
136
+ style,
137
+ )}
152
138
  {...rest}>
153
139
  <div
154
- {...mergeProps(
155
- themeProps('chat-layout-scroll-button'),
156
- stylex.props(
157
- styles.container,
158
- isVisible ? styles.visible : styles.hidden,
159
- label ? styles.expanded : styles.collapsed,
160
- ),
140
+ {...stylex.props(
141
+ styles.container,
142
+ isVisible ? styles.visible : styles.hidden,
143
+ label ? styles.expanded : styles.collapsed,
161
144
  )}>
162
145
  <Button
163
146
  label={label ?? t('@astryx.chatLayoutScrollButton.scrollToBottom')}
@@ -71,7 +71,7 @@ export const docs = {
71
71
  name: 'source',
72
72
  type: 'CitationSource',
73
73
  description:
74
- 'The citation source object containing title, url, an optional image src, and an optional icon node. The url follows the shared navigation rule described on the Link `href` prop; rejected destinations leave the citation visible without navigation. Image src uses separate resource handling.',
74
+ 'The citation source object containing title, url, an optional image src, and an optional icon node.',
75
75
  required: true,
76
76
  },
77
77
  {
@@ -122,7 +122,7 @@ export const docsDense = {
122
122
  },
123
123
  propDescriptions: {
124
124
  source:
125
- 'citation source with title, url, optional image src, and optional icon. url follows the Link href navigation rule; rejected destinations stay visible without navigation. Image src handling is separate.',
125
+ 'citation source object with title, url, optional image src, and optional icon node.',
126
126
  number: 'display index for this citation.',
127
127
  variant: 'display style: label chip with source title or compact numbered badge.',
128
128
  },
@@ -36,23 +36,6 @@ function atomicClasses(style: (typeof probe)[keyof typeof probe]): string[] {
36
36
  describe('Citation', () => {
37
37
  const source = {title: 'Example Source', url: 'https://example.com'};
38
38
 
39
- it.each([
40
- 'javascript:alert(1)',
41
- 'vbscript:MsgBox(1)',
42
- 'data:text/html,<b>x</b>',
43
- 'java\nscript:alert(1)',
44
- ])('renders rejected citation URL %s without navigation', url => {
45
- const {container} = render(
46
- <>
47
- <Citation source={{title: 'Source', url}} number={1} />
48
- <Citation source={{title: 'Source', url}} number={1} variant="number" />
49
- </>,
50
- );
51
- expect(container.querySelector('a')).toBeNull();
52
- expect(container.querySelector('[href]')).toBeNull();
53
- expect(container.textContent).toBe('Source1');
54
- });
55
-
56
39
  it('renders the source title as a link in the label variant', () => {
57
40
  render(<Citation source={source} number={1} data-testid="citation" />);
58
41
  const el = screen.getByTestId('citation');
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file Citation.tsx
7
- * @input Uses React, StyleX, theme tokens, and the shared navigation policy
7
+ * @input Uses React, StyleX, theme tokens
8
8
  * @output Exports Citation component for inline citation references
9
9
  * @position Core implementation; consumed by index.ts
10
10
  *
@@ -30,7 +30,6 @@ import {
30
30
  easeVars,
31
31
  } from '../theme/tokens.stylex';
32
32
  import {mergeProps} from '../utils';
33
- import {isSafeUrl} from '../utils/safeUrl';
34
33
  import type {BaseProps} from '../BaseProps';
35
34
  import {themeProps} from '../utils/themeProps';
36
35
  import {useTranslator} from '../i18n';
@@ -190,8 +189,7 @@ export function Citation({
190
189
  }: CitationProps): React.ReactElement {
191
190
  const t = useTranslator();
192
191
  const title = source.title ?? String(number);
193
- const href =
194
- source.url != null && isSafeUrl(source.url) ? source.url : undefined;
192
+ const href = source.url;
195
193
 
196
194
  // Resolve the source icon. A non-string `icon` node renders as-is (an Astryx
197
195
  // <Icon>, SVG, avatar, etc.). Otherwise fall back to an image URL: `src`, or
@@ -22,7 +22,7 @@ export const docs = {
22
22
  props: [
23
23
  {name: 'label', type: 'string', description: 'Accessibility label.', required: true},
24
24
  {name: 'onClick', type: '(event: MouseEvent) => void', description: 'Click handler: fires on card surface only.'},
25
- {name: 'href', type: 'string', description: 'Navigation URL. Plain, new-tab, Cmd/Ctrl-click, and middle-click activation all follow the shared navigation rule described on the Link `href` prop.'},
25
+ {name: 'href', type: 'string', description: 'Navigation URL.'},
26
26
  {name: 'target', type: 'string', description: 'Link target.', default: "'_self'"},
27
27
  {name: 'isDisabled', type: 'boolean', description: 'Disables the card.', default: 'false'},
28
28
  {name: 'children', type: 'ReactNode', description: 'Card content.'},
@@ -69,7 +69,7 @@ export const docsDense = {
69
69
  propDescriptions: {
70
70
  label: 'accessibility label',
71
71
  onClick: 'click handler: fires on card surface only',
72
- href: 'navigation URL; every activation follows the shared navigation rule (see Link href)',
72
+ href: 'navigation URL',
73
73
  target: 'link target',
74
74
  isDisabled: 'disables card',
75
75
  padding: 'inner padding',
@@ -4,18 +4,14 @@ import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
4
4
  import {applyHighlightRangesChunked} from './highlightRanges';
5
5
  import type {TokenLine} from './tokenizer';
6
6
 
7
- // Mock CSS Highlight API. `escape` is real behavior, not a mock: the code
8
- // under test escapes generated names with it, so jsdom's implementation is
9
- // carried over when the CSS global is replaced.
7
+ // Mock CSS Highlight API
10
8
  class MockHighlight extends Set<Range> {}
11
9
  const mockHighlightsMap = new Map<string, MockHighlight>();
12
- const realCssEscape = globalThis.CSS.escape;
13
10
 
14
11
  beforeEach(() => {
15
12
  mockHighlightsMap.clear();
16
13
 
17
14
  globalThis.CSS = {
18
- escape: realCssEscape,
19
15
  highlights: {
20
16
  get: (name: string) => mockHighlightsMap.get(name),
21
17
  set: (name: string, h: MockHighlight) => mockHighlightsMap.set(name, h),
@@ -48,30 +44,6 @@ function createCodeElement(lines: string[]): HTMLElement {
48
44
  return code;
49
45
  }
50
46
 
51
- /**
52
- * The dynamic stylesheet is module-level state shared by every test in this
53
- * file, so each test drives unique token types and looks its rules up by the
54
- * escaped name rather than by position.
55
- */
56
- function dynamicRules(): CSSStyleRule[] {
57
- const dynamicSheet = document.querySelector<HTMLStyleElement>(
58
- 'style[data-astryx-highlight-dynamic]',
59
- );
60
- return Array.from(dynamicSheet?.sheet?.cssRules ?? []) as CSSStyleRule[];
61
- }
62
-
63
- /**
64
- * Exactly the two highlight selectors and nothing else: an escaped ident may
65
- * contain any character, but only behind a backslash, so an unescaped `)`
66
- * that closed the function early (and the `, body` after it) cannot match.
67
- */
68
- const HIGHLIGHT_ONLY_SELECTOR =
69
- /^\.astryx-code-block code::highlight\((?:[^()\\]|\\.)+\), \.astryx-codeeditor code::highlight\((?:[^()\\]|\\.)+\)$/;
70
-
71
- function highlightRule(name: string): string {
72
- return `.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name})`;
73
- }
74
-
75
47
  describe('applyHighlightRangesChunked', () => {
76
48
  it('creates ranges for tokens on each line', () => {
77
49
  const codeEl = createCodeElement(['const x = 1;', 'let y = 2;']);
@@ -95,120 +67,6 @@ describe('applyHighlightRangesChunked', () => {
95
67
  expect(kwHighlight!.size).toBe(0);
96
68
  });
97
69
 
98
- it('keeps every parser-accepted tokenizer type coloured under its raw name', () => {
99
- // The dotted type that crashed the block, plus names a narrow allowlist
100
- // would wrongly reject even though the CSS parser accepts them all.
101
- const types = ['keyword.control.sql', '_private', 'キーワード', '9start'];
102
- const codeEl = createCodeElement(types.map(() => 'line'));
103
- const tokenLines: TokenLine[] = types.map(type => [
104
- {type, start: 0, end: 4},
105
- ]);
106
-
107
- const mockStyle = document.createElement('style');
108
- mockStyle.setAttribute('data-astryx-highlight-styles', '');
109
- document.head.appendChild(mockStyle);
110
- const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
111
-
112
- const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
113
-
114
- // Ranges register under the RAW names: a ::highlight() selector matches
115
- // by ident value, so the escaped rule below still paints them.
116
- for (const type of types) {
117
- expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
118
- }
119
-
120
- // One rule per type, with only the characters that need it escaped.
121
- expect(insertRule.mock.calls.map(([rule]) => rule)).toEqual([
122
- `${highlightRule('astryx-keyword\\.control\\.sql')} { color: var(--color-syntax-keyword\\.control\\.sql, currentColor); }`,
123
- `${highlightRule('astryx-_private')} { color: var(--color-syntax-_private, currentColor); }`,
124
- `${highlightRule('astryx-キーワード')} { color: var(--color-syntax-キーワード, currentColor); }`,
125
- `${highlightRule('astryx-9start')} { color: var(--color-syntax-9start, currentColor); }`,
126
- ]);
127
-
128
- cleanup();
129
- });
130
-
131
- it('escapes the highlight name so a token type cannot leave its ::highlight() selector', () => {
132
- // Unescaped, this type closes ::highlight() early and the CSS parser
133
- // accepts what follows as a valid second selector plus a declaration
134
- // block, with the trailing comment opener swallowing the rest of the
135
- // generated rule text. The result is a rule that styles <body>.
136
- const type = 'sel), body { background: red } /*';
137
- const escapedName =
138
- 'astryx-sel\\)\\,\\ body\\ \\{\\ background\\:\\ red\\ \\}\\ \\/\\*';
139
- const codeEl = createCodeElement(['line']);
140
- const tokenLines: TokenLine[] = [[{type, start: 0, end: 4}]];
141
-
142
- const mockStyle = document.createElement('style');
143
- mockStyle.setAttribute('data-astryx-highlight-styles', '');
144
- document.head.appendChild(mockStyle);
145
- const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
146
-
147
- const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
148
-
149
- expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
150
-
151
- // The exact rule text handed to the engine: both generated names fully
152
- // escaped, one colour declaration, nothing of the payload's making.
153
- expect(insertRule).toHaveBeenCalledTimes(1);
154
- expect(insertRule).toHaveBeenCalledWith(
155
- `${highlightRule(escapedName)} { color: var(--color-syntax-sel\\)\\,\\ body\\ \\{\\ background\\:\\ red\\ \\}\\ \\/\\*, currentColor); }`,
156
- );
157
-
158
- // And as parsed: the rule for this type keeps the payload inside the
159
- // ident, and no rule in the sheet has any selector beyond the two
160
- // highlight selectors.
161
- const rules = dynamicRules();
162
- const rule = rules.find(r => r.selectorText.includes('astryx-sel'));
163
- expect(rule?.selectorText).toBe(highlightRule(escapedName));
164
- for (const r of rules) {
165
- expect(r.selectorText).toMatch(HIGHLIGHT_ONLY_SELECTOR);
166
- }
167
-
168
- cleanup();
169
- });
170
-
171
- it('escapes the custom property so a token type cannot add declarations', () => {
172
- // Unescaped, this type closes var() early and the CSS parser accepts
173
- // what follows as two more valid declarations.
174
- const type = 'p); background: red; --x: var(--y';
175
- const escapedName =
176
- 'astryx-p\\)\\;\\ background\\:\\ red\\;\\ --x\\:\\ var\\(--y';
177
- const escapedColor =
178
- 'var(--color-syntax-p\\)\\;\\ background\\:\\ red\\;\\ --x\\:\\ var\\(--y, currentColor)';
179
- const codeEl = createCodeElement(['line']);
180
- const tokenLines: TokenLine[] = [[{type, start: 0, end: 4}]];
181
-
182
- const mockStyle = document.createElement('style');
183
- mockStyle.setAttribute('data-astryx-highlight-styles', '');
184
- document.head.appendChild(mockStyle);
185
- const insertRule = vi.spyOn(CSSStyleSheet.prototype, 'insertRule');
186
-
187
- const cleanup = applyHighlightRangesChunked(codeEl, tokenLines);
188
-
189
- expect(mockHighlightsMap.get(`astryx-${type}`)?.size).toBe(1);
190
-
191
- expect(insertRule).toHaveBeenCalledTimes(1);
192
- expect(insertRule).toHaveBeenCalledWith(
193
- `${highlightRule(escapedName)} { color: ${escapedColor}; }`,
194
- );
195
-
196
- // As parsed: exactly one declaration, `color`, whose value is the
197
- // escaped custom property with its fallback.
198
- const rule = dynamicRules().find(r => r.selectorText.includes('astryx-p'));
199
- expect(rule).toBeDefined();
200
- expect(rule!.selectorText).toBe(highlightRule(escapedName));
201
- const declared = Array.from({length: rule!.style.length}, (_, i) =>
202
- rule!.style.item(i),
203
- );
204
- expect(declared).toEqual(['color']);
205
- expect(rule!.style.getPropertyValue('color')).toBe(escapedColor);
206
- expect(rule!.style.getPropertyValue('background')).toBe('');
207
- expect(rule!.style.getPropertyValue('--x')).toBe('');
208
-
209
- cleanup();
210
- });
211
-
212
70
  it('handles empty token lines', () => {
213
71
  const codeEl = createCodeElement(['', 'const x = 1;']);
214
72
  const tokenLines: TokenLine[] = [[], [{type: 'keyword', start: 0, end: 5}]];
@@ -64,21 +64,11 @@ function ensureDynamicHighlightType(tokenType: string): void {
64
64
  }
65
65
  }
66
66
 
67
- // CSS.escape both generated names before they land in rule text: the
68
- // registry key stays the raw string (a ::highlight() selector matches by
69
- // ident VALUE, escapes and all), so dotted, non-ASCII, `_private`, and
70
- // digit-led token types keep their colours — while nothing a token stream
71
- // carries can step outside its ident and shape the rule.
72
- const name = CSS.escape(`astryx-${tokenType}`);
73
- const colorVar = `var(${CSS.escape(`--color-syntax-${tokenType}`)}, currentColor)`;
74
- try {
75
- dynamicStyleSheet.insertRule(
76
- `.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`,
77
- );
78
- } catch {
79
- // An engine that still refuses the rule costs that type its colour
80
- // (ranges paint with currentColor) — never the whole code block.
81
- }
67
+ const name = `astryx-${tokenType}`;
68
+ const colorVar = `var(--color-syntax-${tokenType}, currentColor)`;
69
+ dynamicStyleSheet.insertRule(
70
+ `.astryx-code-block code::highlight(${name}), .astryx-codeeditor code::highlight(${name}) { color: ${colorVar}; }`,
71
+ );
82
72
  }
83
73
 
84
74
  // ---------------------------------------------------------------------------
@@ -49,9 +49,8 @@ export const docs = {
49
49
  {
50
50
  name: 'size',
51
51
  type: "'xsm' | 'sm' | 'md' | 'lg'",
52
- description:
53
- 'Icon size. An explicit value wins. When omitted, Icon uses the nearest default supplied by an owning Astryx component for its icon slot, then falls back to md when no contextual default exists.',
54
- default: "Contextual; otherwise 'md'",
52
+ description: 'Icon size.',
53
+ default: "'md'",
55
54
  },
56
55
  {
57
56
  name: 'label',
@@ -154,9 +153,8 @@ export const docsZh = {
154
153
  {
155
154
  name: 'size',
156
155
  type: "'xsm' | 'sm' | 'md' | 'lg'",
157
- description:
158
- '图标尺寸。显式值优先。省略时,Icon 使用最近的 Astryx 所属组件为其图标槽提供的默认尺寸;如果没有上下文默认值,则回退为 md。',
159
- default: "上下文默认值;否则为 'md'",
156
+ description: '图标尺寸。',
157
+ default: "'md'",
160
158
  },
161
159
  {
162
160
  name: 'label',
@@ -296,7 +294,7 @@ export const docsDense = {
296
294
  propDescriptions: {
297
295
  icon: 'Semantic icon name or SVG component. Valid names: close, chevronDown, chevronLeft, chevronRight, chevronsLeft, chevronsRight, check, success, error, warning, info, calendar, clock, externalLink, menu, moreHorizontal, search, arrowUp, arrowDown, arrowsUpDown, funnel, eyeSlash, viewColumns, copy, checkDouble, wrench, stop, microphone. For others, pass an SVG component.',
298
296
  color: 'Color variant mapped to Astryx icon color tokens.',
299
- size: "explicit Icon size; otherwise nearest owning-component default, then 'md' when no contextual default exists",
297
+ size: 'Icon size.',
300
298
  label:
301
299
  'Accessible name for a meaningful, standalone icon. Sets role="img" + aria-label and drops the default aria-hidden. Omit (default) for decorative icons (stays aria-hidden). Empty string = decorative. The accessible-name/alt-text prop for icons.',
302
300
  xstyle:
@@ -10,18 +10,12 @@ approved_by: cixzhang
10
10
  approved_at: 2026-08-30
11
11
  owners: [cixzhang, imdreamrunner]
12
12
  review_triggers: [public-api, behavior, theming, accessibility]
13
- verified_by:
14
- [
15
- packages/core/src/Icon/Icon.test.tsx,
16
- packages/core/src/IconButton/IconButton.test.tsx,
17
- scripts/check-knowledge.mjs,
18
- ]
13
+ verified_by: [packages/core/src/Icon/Icon.test.tsx, scripts/check-knowledge.mjs]
19
14
  modules: []
20
15
  families: []
21
16
  design_specs: []
22
17
  architecture:
23
18
  [
24
- architecture:component-size-cascade,
25
19
  architecture:component-theming-surface,
26
20
  architecture:icon-resolution-and-component-slots,
27
21
  architecture:public-component-api,
@@ -39,12 +33,11 @@ semantics. Consumer usage remains documented in `Icon.doc.mjs`.
39
33
 
40
34
  ## Compatibility and migration
41
35
 
42
- - Released default preserved: `yes` for explicit sizes and standalone Icons
43
- - Compatibility class: component-owned Icon slots may supply a contextual default;
44
- explicit Icon sizes and the standalone `md` fallback remain unchanged
36
+ - Released default preserved: `yes`
37
+ - Compatibility class: additive documentation only; runtime, DOM, styling, and
38
+ public API remain unchanged
45
39
  - Controlled/uncontrolled behavior: not applicable
46
- - Migration decision: no consumer migration; omit `size` when the owning Astryx
47
- component should select the contextual default
40
+ - Migration decision: none; this record characterizes the released component
48
41
 
49
42
  Consumer migration instructions belong in consumer docs and release notes.
50
43
 
@@ -53,7 +46,6 @@ Consumer migration instructions belong in consumer docs and release notes.
53
46
  **Owns**
54
47
 
55
48
  - Accepting a semantic icon key or a supplied icon component as the glyph source.
56
- - Resolving explicit, component-owned contextual, and standalone size defaults.
57
49
  - Applying Icon's size, color, theming target, and accessibility semantics to
58
50
  the rendered glyph.
59
51
 
@@ -61,22 +53,19 @@ Consumer migration instructions belong in consumer docs and release notes.
61
53
 
62
54
  - The shared registry's key-resolution and fallback order — owned by the shared
63
55
  icon system.
64
- - Which contextual default an owning Astryx component selects for its icon slot —
65
- owned by that component or family contract.
66
56
  - The meaning of a glyph in product context or whether nearby text makes it
67
57
  decorative — owned by the product callsite.
68
58
  - Interaction, focus, or control naming — owned by the interactive parent.
69
59
  - The artwork supplied by a consumer or registered through a theme.
70
- - A public Icon size-provider API; contextual transport is implementation detail.
71
60
 
72
61
  ## Public concepts
73
62
 
74
- | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
75
- | ------------- | --------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------- | ----------------------------------------------- | ---------------- | --------- | ------------------------------------------------------ |
76
- | Glyph source | semantic key, namespaced extension key, or icon component | Selects the visual symbol. | Every render | Required | `component:Icon` | Stable | TypeScript rejects unsupported built-in string values. |
77
- | Size | `xsm`, `sm`, `md`, `lg` | Selects the icon box size. | Every rendered glyph | nearest component-owned default; otherwise `md` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
78
- | Color | documented semantic and palette values | Selects the glyph color or inherits it from context. | Every rendered glyph | `inherit` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
79
- | Accessibility | decorative or meaningfully labelled | Controls whether assistive technology receives the glyph. | Every rendered glyph | Decorative | `component:Icon` | Stable | Empty labels use the decorative behavior. |
63
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
64
+ | ------------- | --------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------- | ---------- | ---------------- | --------- | ------------------------------------------------------ |
65
+ | Glyph source | semantic key, namespaced extension key, or icon component | Selects the visual symbol. | Every render | Required | `component:Icon` | Stable | TypeScript rejects unsupported built-in string values. |
66
+ | Size | `xsm`, `sm`, `md`, `lg` | Selects the icon box size. | Every rendered glyph | `md` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
67
+ | Color | documented semantic and palette values | Selects the glyph color or inherits it from context. | Every rendered glyph | `inherit` | `component:Icon` | Stable | TypeScript rejects unsupported values. |
68
+ | Accessibility | decorative or meaningfully labelled | Controls whether assistive technology receives the glyph. | Every rendered glyph | Decorative | `component:Icon` | Stable | Empty labels use the decorative behavior. |
80
69
 
81
70
  A namespaced key that does not resolve currently renders nothing. This is
82
71
  existing behavior, not an intentional fallback promise.
@@ -86,51 +75,42 @@ existing behavior, not an intentional fallback promise.
86
75
  Requirements identify their basis so observed code is not mistaken for an
87
76
  intentional decision.
88
77
 
89
- | ID | Candidate invariant | Basis | Draft review state |
90
- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------- |
91
- | FR1 | Icon MUST present at most one glyph from the supplied semantic key, namespaced key, or icon component. | Documented promise and current tests. | Settled intent. |
92
- | FR2 | Every rendered glyph MUST carry the `icon` theming target with its resolved size and selected color reflected as target data. | Current source, docs, and theming tests. | Settled intent. |
93
- | FR3 | Icon MUST apply the resolved size as width and height for every glyph, plus font size where needed for 1em-based icon sources. This sizing contract MUST NOT promise an HTML or SVG element type. | Human decision, current source, and tests. | Settled intent. |
94
- | FR4 | Supported SVG and styling escape hatches MUST retain their established merge and override behavior. | Current source and regression tests. | Current compatibility behavior; verify before changing. |
95
- | FR5 | An explicit Icon `size` MUST win. Without one, Icon MUST use the nearest default supplied by an owning Astryx component for its documented icon slot; without such a default, Icon MUST use `md`. | Component-size precedence and Button sizing decision. | Settled intent. |
78
+ | ID | Candidate invariant | Basis | Draft review state |
79
+ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------- |
80
+ | FR1 | Icon MUST present at most one glyph from the supplied semantic key, namespaced key, or icon component. | Documented promise and current tests. | Settled intent. |
81
+ | FR2 | Every rendered glyph MUST carry the `icon` theming target with its selected size and color reflected as target data. | Current source, docs, and theming tests. | Settled intent. |
82
+ | FR3 | Icon MUST apply the selected size as width and height for every glyph, plus font size where needed for 1em-based icon sources. This sizing contract MUST NOT promise an HTML or SVG element type. | Human decision, current source, and tests. | Settled intent. |
83
+ | FR4 | Supported SVG and styling escape hatches MUST retain their established merge and override behavior. | Current source and regression tests. | Current compatibility behavior; verify before changing. |
96
84
 
97
85
  ### Allowed variation
98
86
 
99
87
  - **AV1 — Artwork.** The path, view box, and internal structure may vary by icon
100
88
  source or theme without changing Icon's one-part consumer anatomy.
101
- - **AV2 — Rendering strategy.** Icon may render a supplied icon component directly or
89
+ - **AV2 — Rendering strategy.** Icon may render a supplied component directly or
102
90
  use an internal wrapper for a resolved key; neither element shape is public
103
91
  anatomy.
104
92
  - **AV3 — Theme and consumer styling.** Existing theme and styling escape hatches
105
93
  may change visual CSS properties without changing glyph ownership.
106
- - **AV4 — Contextual mapping.** An owning Astryx component may map its own public
107
- size or variant to an Icon default. That mapping belongs to the owner and MUST
108
- preserve explicit Icon sizes and the standalone fallback.
109
94
 
110
95
  ### Representative states
111
96
 
112
- | State | Required invariant | Allowed variation |
113
- | -------------------------------- | -------------------------------------------------------- | ---------------------------------------------- |
114
- | Semantic key | One resolved glyph carries the `icon` target. | Theme, registry, or built-in artwork. |
115
- | Namespaced extension key | A resolved extension glyph carries the `icon` target. | Consumer- or library-owned artwork. |
116
- | Supplied icon component | The supplied glyph carries the `icon` target. | Component implementation and SVG internals. |
117
- | Standalone Icon without `size` | The resolved size is `md`. | Glyph source and color. |
118
- | Owned icon slot without `size` | The nearest documented component default is used. | The owning component's declared size mapping. |
119
- | Explicit Icon size in owned slot | The explicit Icon size wins over the contextual default. | Any supported Icon size. |
120
- | Unresolved namespaced key | No glyph is currently rendered. | No fallback behavior is established by intent. |
97
+ | State | Required invariant | Allowed variation |
98
+ | ------------------------- | ----------------------------------------------------- | ---------------------------------------------- |
99
+ | Semantic key | One resolved glyph carries the `icon` target. | Theme, registry, or built-in artwork. |
100
+ | Namespaced extension key | A resolved extension glyph carries the `icon` target. | Consumer- or library-owned artwork. |
101
+ | Supplied icon component | The supplied glyph carries the `icon` target. | Component implementation and SVG internals. |
102
+ | Unresolved namespaced key | No glyph is currently rendered. | No fallback behavior is established by intent. |
121
103
 
122
104
  ### Transformation and precedence order
123
105
 
124
- - **ORD1 — Size resolution.** Resolve explicit Icon `size` → nearest
125
- component-owned contextual default → standalone `md` fallback.
126
- - **ORD2 — Presentation.** Apply the target, resolved size, and component color
127
- styles, then merge consumer `xstyle`, `className`, inline `style`, and supported
106
+ - **ORD1 — Presentation.** Apply the target, component size and color styles,
107
+ then merge consumer `xstyle`, `className`, inline `style`, and supported
128
108
  pass-through props in their established order.
129
109
 
130
110
  ### Performance and resources
131
111
 
132
112
  - **PR1 — Render work.** Icon owns no listeners, observers, or layout
133
- measurement; semantic and size resolution are synchronous during render.
113
+ measurement; semantic resolution is synchronous during render.
134
114
 
135
115
  ## Accessibility contract
136
116
 
@@ -143,9 +123,9 @@ intentional decision.
143
123
 
144
124
  ## Design relationships
145
125
 
146
- | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
147
- | ---------------- | ------------------------------------------------------------------ | ------------------------------------------------------------- | ----------------- | ------------------ |
148
- | Glyph | Carries the resolved visual symbol at the resolved size and color. | Consumer or registry selects artwork; Icon owns presentation. | Context-dependent | FR1, FR2, FR3, FR5 |
126
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
127
+ | ---------------- | ------------------------------------------------------------------- | ------------------------------------------------------------- | ----------------- | ------------------ |
128
+ | Glyph | Carries the selected visual symbol at the requested size and color. | Consumer or registry selects artwork; Icon owns presentation. | Context-dependent | FR1, FR2, FR3 |
149
129
 
150
130
  `Glyph` is the single conceptual consumer part in both source modes. Current
151
131
  implementation may render a supplied icon component directly or place a
@@ -164,32 +144,26 @@ not promise a `span`, `svg`, or other element type.
164
144
 
165
145
  ## Family and system relationships
166
146
 
167
- - `architecture:component-size-cascade` owns the shared explicit → nearest
168
- provider → component fallback pattern and requires provider ownership to be
169
- declared. Icon retains its own `xsm | sm | md | lg` axis outside the standard
170
- element-size cascade and adopts that precedence for component-owned defaults.
171
147
  - `architecture:component-theming-surface` owns the qualification and validation
172
148
  rules for the `icon` target.
173
149
  - `architecture:icon-resolution-and-component-slots` owns semantic-key and
174
150
  component-slot resolution before Icon renders the selected source.
175
151
  - `architecture:public-component-api` owns admission and compatibility rules for
176
- Icon's public props and observable defaults.
177
- - The owning component or family specifies each contextual Icon-size mapping.
178
- The Button family currently maps `sm` and `md` controls to `sm` Icons and `lg`
179
- controls to `md` Icons.
152
+ Icon's public props.
180
153
  - Icon has no current family contract.
181
154
 
182
155
  ## Verification map
183
156
 
184
- | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
185
- | ------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
186
- | FR1, FR3, FR5 | `Icon.test.tsx` sizing suites and `IconButton.test.tsx` contextual-size suites | standalone default; Button sizes; explicit override; both modes | Breaking source rendering, standalone fallback, contextual mapping, explicit precedence, or cross-mode sizing fails assertions. | `audit:Icon/behavior` |
187
- | FR2, FR4 | `Icon.test.tsx` target/styling suites plus source inspection | Both source modes; size and color variants | Removing the target or breaking override composition fails existing assertions; `data-color` reflection currently lacks focused assertions. | `audit:Icon/theming` |
188
- | AR1, AR2, AR3 | `Icon.test.tsx` accessible-name suites | Decorative, labelled, explicit ARIA override | Changing default or labelled semantics fails accessibility assertions. | `audit:Icon/accessibility` |
189
- | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical consumer anatomy and current target | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:Icon/theming` |
157
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
158
+ | ------------------- | ------------------------------------------------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- |
159
+ | FR1, FR3 | `Icon.test.tsx` source-mode and sizing suites | Built-in, themed, namespaced, component | Breaking source rendering or cross-mode sizing fails assertions. | `audit:Icon/behavior` |
160
+ | FR2, FR4 | `Icon.test.tsx` target/styling suites plus source inspection | Both source modes; size and color variants | Removing the target or breaking override composition fails existing assertions; `data-size` and `data-color` reflection currently lack focused assertions. | `audit:Icon/theming` |
161
+ | AR1, AR2, AR3 | `Icon.test.tsx` accessible-name suites | Decorative, labelled, explicit ARIA override | Changing default or labelled semantics fails accessibility assertions. | `audit:Icon/accessibility` |
162
+ | Theming anatomy map | `scripts/check-knowledge.mjs` | Canonical consumer anatomy and current target | Missing, extra, prefixed, or stale mappings fail repository validation. | `audit:Icon/theming` |
190
163
 
191
- Focused coverage for `data-color` on both glyph source modes is a checkable test
192
- gap; source inspection is the current evidence for that part of FR2.
164
+ Focused coverage for `data-size` and `data-color` on both glyph source modes is
165
+ a checkable test gap; source inspection is the current evidence for that part of
166
+ FR2.
193
167
 
194
168
  ## Decision log
195
169
 
@@ -208,21 +182,6 @@ a `span`, `svg`, or other element.
208
182
  Rejected: separate wrapper and SVG anatomy entries, because those describe
209
183
  implementation strategies rather than stable consumer concepts.
210
184
 
211
- ### DEC-2 — Owning components may provide contextual Icon defaults
212
-
213
- **Reference:** `component:Icon/DEC-2`
214
- **Decider:** cixzhang, 2026-09-23
215
-
216
- A person should see an icon that is proportionate to the Astryx component that
217
- owns its slot without every caller repeating a size. The owning component may
218
- therefore provide the default while an explicit Icon size remains authoritative
219
- and a standalone Icon remains `md`. Provider objects, hooks, and context shape
220
- remain private implementation details rather than public API.
221
-
222
- Rejected: an unconditional `md` default inside every composition, forcing every
223
- caller to repeat the owner-derived size, and exposing the provider mechanism as
224
- public API.
225
-
226
185
  ## Open questions
227
186
 
228
187
  None.