@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
|
@@ -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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
{...
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/Icon/Icon.doc.mjs
CHANGED
|
@@ -49,9 +49,8 @@ export const docs = {
|
|
|
49
49
|
{
|
|
50
50
|
name: 'size',
|
|
51
51
|
type: "'xsm' | 'sm' | 'md' | 'lg'",
|
|
52
|
-
description:
|
|
53
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
package/src/Icon/Icon.spec.md
CHANGED
|
@@ -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`
|
|
43
|
-
- Compatibility class:
|
|
44
|
-
|
|
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:
|
|
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
|
|
75
|
-
| ------------- | --------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------- |
|
|
76
|
-
| Glyph source | semantic key, namespaced extension key, or icon component | Selects the visual symbol. | Every render | Required
|
|
77
|
-
| Size | `xsm`, `sm`, `md`, `lg` | Selects the icon box size. | Every rendered glyph |
|
|
78
|
-
| Color | documented semantic and palette values | Selects the glyph color or inherits it from context. | Every rendered glyph | `inherit`
|
|
79
|
-
| Accessibility | decorative or meaningfully labelled | Controls whether assistive technology receives the glyph. | Every rendered glyph | Decorative
|
|
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
|
|
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.
|
|
92
|
-
| FR2 | Every rendered glyph MUST carry the `icon` theming target with its
|
|
93
|
-
| FR3 | Icon MUST apply the
|
|
94
|
-
| FR4 | Supported SVG and styling escape hatches MUST retain their established merge and override behavior. | Current source and regression tests.
|
|
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
|
|
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
|
|
113
|
-
|
|
|
114
|
-
| Semantic key
|
|
115
|
-
| Namespaced extension key
|
|
116
|
-
| Supplied icon component
|
|
117
|
-
|
|
|
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 —
|
|
125
|
-
|
|
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
|
|
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
|
|
147
|
-
| ---------------- |
|
|
148
|
-
| Glyph | Carries the
|
|
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
|
|
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
|
|
185
|
-
| ------------------- |
|
|
186
|
-
| FR1, FR3
|
|
187
|
-
| FR2, FR4 | `Icon.test.tsx` target/styling suites plus source inspection
|
|
188
|
-
| AR1, AR2, AR3 | `Icon.test.tsx` accessible-name suites
|
|
189
|
-
| Theming anatomy map | `scripts/check-knowledge.mjs`
|
|
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
|
|
192
|
-
gap; source inspection is the current evidence for that part of
|
|
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.
|