@astryxdesign/core 0.4.2 → 0.4.3-canary.0a9618e
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/CHANGELOG.md +45 -0
- package/README.md +79 -45
- package/dist/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +3 -19
- package/dist/Banner/Banner.d.ts +9 -2
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +84 -33
- package/dist/Banner/index.d.ts +14 -12
- package/dist/Banner/index.d.ts.map +1 -1
- package/dist/Banner/index.js +10 -8
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +381 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +57 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +395 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +72 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +130 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +25 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +384 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +170 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +881 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/Chat/ChatTokenizedText.js +1 -1
- package/dist/ComplexSelector/ComplexSelector.d.ts +38 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +98 -31
- package/dist/ComplexSelector/index.d.ts +2 -2
- package/dist/ComplexSelector/index.d.ts.map +1 -1
- package/dist/ComplexSelector/index.js +1 -1
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +9 -1
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +31 -1
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Markdown/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +55 -12
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +10 -1
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +4 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
- package/dist/PowerSearch/formatFilterValue.js +2 -4
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +10 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/columnUtils.d.ts.map +1 -1
- package/dist/Table/columnUtils.js +4 -1
- package/dist/TextArea/TextArea.d.ts +6 -3
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +17 -6
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/TreeList/TreeList.js +2 -1
- package/dist/Typeahead/BaseTypeahead.js +1 -1
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +25 -2
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +30 -7
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +36 -47
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/theme/mergeComponents.d.ts +20 -0
- package/dist/theme/mergeComponents.d.ts.map +1 -0
- package/dist/theme/mergeComponents.js +56 -0
- package/dist/theme/onMediaTokens.d.ts +6 -1
- package/dist/theme/onMediaTokens.d.ts.map +1 -1
- package/dist/theme/onMediaTokens.js +11 -3
- package/dist/utils/characters.d.ts +27 -0
- package/dist/utils/characters.d.ts.map +1 -0
- package/dist/utils/characters.js +83 -0
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/package.json +9 -8
- package/src/Avatar/Avatar.test.tsx +6 -1
- package/src/Avatar/Avatar.tsx +3 -21
- package/src/Banner/Banner.doc.mjs +12 -10
- package/src/Banner/Banner.test.tsx +108 -0
- package/src/Banner/Banner.tsx +124 -38
- package/src/Banner/index.ts +15 -13
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +1865 -0
- package/src/BottomSheet/BottomSheet.tsx +539 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +542 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +15 -0
- package/src/BottomSheet/snapOffsets.test.ts +187 -0
- package/src/BottomSheet/snapOffsets.ts +159 -0
- package/src/BottomSheet/useMobileKeyboard.ts +566 -0
- package/src/BottomSheet/useSheetGestures.test.ts +833 -0
- package/src/BottomSheet/useSheetGestures.ts +1333 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/Chat/ChatTokenizedText.tsx +1 -1
- package/src/ComplexSelector/ComplexSelector.doc.mjs +55 -6
- package/src/ComplexSelector/ComplexSelector.test.tsx +197 -6
- package/src/ComplexSelector/ComplexSelector.tsx +156 -28
- package/src/ComplexSelector/index.ts +3 -1
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +9 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +39 -2
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Markdown/parser.test.ts +53 -0
- package/src/Markdown/parser.ts +53 -12
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +31 -0
- package/src/MultiSelector/MultiSelector.tsx +10 -1
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearch.test.tsx +48 -3
- package/src/PowerSearch/PowerSearch.tsx +4 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/PowerSearch/formatFilterValue.test.ts +22 -0
- package/src/PowerSearch/formatFilterValue.ts +2 -4
- package/src/Selector/Selector.test.tsx +33 -0
- package/src/Selector/Selector.tsx +10 -1
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/Table.test.tsx +6 -0
- package/src/Table/columnUtils.ts +3 -1
- package/src/TextArea/TextArea.doc.mjs +1 -1
- package/src/TextArea/TextArea.test.tsx +72 -0
- package/src/TextArea/TextArea.tsx +26 -8
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/TreeList/TreeList.doc.mjs +2 -2
- package/src/TreeList/TreeList.tsx +1 -1
- package/src/Typeahead/BaseTypeahead.tsx +1 -1
- package/src/Typeahead/Typeahead.test.tsx +35 -0
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.test.ts +127 -0
- package/src/theme/defineTheme.ts +70 -55
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/theme/extensibleAxes.test.ts +365 -0
- package/src/theme/mergeComponents.ts +59 -0
- package/src/theme/onMediaTokens.ts +9 -2
- package/src/utils/characters.test.ts +141 -0
- package/src/utils/characters.ts +88 -0
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +4 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -30,7 +30,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
30
30
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
31
31
|
import {usePopover} from '../Popover/usePopover';
|
|
32
32
|
import {useAnnounce} from '../hooks/useAnnounce';
|
|
33
|
-
import {isImeKeyEvent} from '../
|
|
33
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
34
34
|
import {TypeaheadItem} from './TypeaheadItem';
|
|
35
35
|
import {Icon} from '../Icon';
|
|
36
36
|
import {
|
|
@@ -796,6 +796,41 @@ describe('Typeahead edit mode', () => {
|
|
|
796
796
|
// onChange should not have been called — value restored
|
|
797
797
|
expect(onChange).not.toHaveBeenCalled();
|
|
798
798
|
});
|
|
799
|
+
|
|
800
|
+
it('does not exit edit mode on a composing Escape (IME)', async () => {
|
|
801
|
+
const onChange = vi.fn();
|
|
802
|
+
render(
|
|
803
|
+
<Typeahead
|
|
804
|
+
label="Fruit"
|
|
805
|
+
searchSource={fruitSource}
|
|
806
|
+
value={fruits[0]}
|
|
807
|
+
onChange={onChange}
|
|
808
|
+
/>,
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Enter edit mode by clicking the token; the input uncollapses and
|
|
812
|
+
// rejoins the Tab order (tabindex is cleared).
|
|
813
|
+
const tokenText = screen.getByText(fruits[0].label);
|
|
814
|
+
fireEvent.click(tokenText.closest('div')!);
|
|
815
|
+
await act(async () => {
|
|
816
|
+
await new Promise(r => requestAnimationFrame(r));
|
|
817
|
+
});
|
|
818
|
+
const input = screen.getByRole('combobox');
|
|
819
|
+
expect(input).not.toHaveAttribute('tabindex', '-1');
|
|
820
|
+
|
|
821
|
+
// BaseTypeahead invokes this external handler before its own IME guard, so
|
|
822
|
+
// a composing Escape (isComposing / legacy keyCode 229) — which an IME uses
|
|
823
|
+
// to cancel the pending candidate — must not exit edit mode here.
|
|
824
|
+
fireEvent.keyDown(input, {key: 'Escape', isComposing: true});
|
|
825
|
+
expect(screen.getByRole('combobox')).not.toHaveAttribute('tabindex', '-1');
|
|
826
|
+
fireEvent.keyDown(input, {key: 'Escape', keyCode: 229});
|
|
827
|
+
expect(screen.getByRole('combobox')).not.toHaveAttribute('tabindex', '-1');
|
|
828
|
+
|
|
829
|
+
// A real, non-composing Escape still exits edit mode: the token is
|
|
830
|
+
// restored and the collapsed input drops back out of the Tab order.
|
|
831
|
+
fireEvent.keyDown(input, {key: 'Escape'});
|
|
832
|
+
expect(screen.getByRole('combobox')).toHaveAttribute('tabindex', '-1');
|
|
833
|
+
});
|
|
799
834
|
});
|
|
800
835
|
|
|
801
836
|
describe('Typeahead collapsed input tab order', () => {
|
|
@@ -45,7 +45,7 @@ import {VisuallyHidden} from '../VisuallyHidden';
|
|
|
45
45
|
import {spacingVars, sizeVars} from '../theme/tokens.stylex';
|
|
46
46
|
import {groupStyles} from '../InputGroup/groupStyles';
|
|
47
47
|
import {useInputGroup} from '../InputGroup/InputGroupContext';
|
|
48
|
-
import {getInputARIA, mergeProps, mergeRefs} from '../utils';
|
|
48
|
+
import {getInputARIA, isImeKeyEvent, mergeProps, mergeRefs} from '../utils';
|
|
49
49
|
import type {BaseProps} from '../BaseProps';
|
|
50
50
|
import type {SizeValue} from '../utils/types';
|
|
51
51
|
import type {SearchableItem, SearchSource} from './types';
|
|
@@ -369,6 +369,15 @@ export function Typeahead<T extends SearchableItem>({
|
|
|
369
369
|
// Handle Escape during edit mode — restore token
|
|
370
370
|
const handleKeyDown = useCallback(
|
|
371
371
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
372
|
+
// BaseTypeahead invokes this external handler *before* its own IME
|
|
373
|
+
// guard, so we must guard here too: an IME candidate window uses Escape
|
|
374
|
+
// to cancel the pending composition, and that composing Escape fires
|
|
375
|
+
// before compositionend. Without this, a Korean/Japanese/Chinese user
|
|
376
|
+
// cancelling a candidate would instead exit edit mode and blur the
|
|
377
|
+
// field. See utils/ime.ts.
|
|
378
|
+
if (isImeKeyEvent(e.nativeEvent)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
372
381
|
if (e.key === 'Escape' && editingValue) {
|
|
373
382
|
e.preventDefault();
|
|
374
383
|
setIsEditing(false);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file stubMatchMedia.test.ts
|
|
5
|
+
* @input Uses vitest, stubMatchMedia
|
|
6
|
+
* @output Unit coverage for the shared query-aware matchMedia stub
|
|
7
|
+
* @position Testing; guards the helper other suites trust to route queries
|
|
8
|
+
*
|
|
9
|
+
* The helper exists because a stub that ignores its query silently reroutes
|
|
10
|
+
* code branching on a different one — the failure mode leaves every test
|
|
11
|
+
* green. A helper sold on that promise has to be held to it, so these assert
|
|
12
|
+
* the routing itself rather than any component behaviour.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {describe, it, expect, afterEach, vi} from 'vitest';
|
|
16
|
+
import {stubMatchMedia} from './stubMatchMedia';
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.unstubAllGlobals();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('stubMatchMedia', () => {
|
|
23
|
+
it('routes prefers-reduced-motion independently of every other query', () => {
|
|
24
|
+
// The exact combination the blanket stub could not express: below a
|
|
25
|
+
// breakpoint (matches) while motion is NOT reduced.
|
|
26
|
+
stubMatchMedia({reduceMotion: false, matches: true});
|
|
27
|
+
|
|
28
|
+
expect(matchMedia('(prefers-reduced-motion: reduce)').matches).toBe(false);
|
|
29
|
+
expect(matchMedia('(max-width: 768px)').matches).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('answers no-preference as the opposite of reduce', () => {
|
|
33
|
+
// Two spellings, opposite meanings. A substring test on
|
|
34
|
+
// 'prefers-reduced-motion' alone would hand both the same answer.
|
|
35
|
+
stubMatchMedia({reduceMotion: true});
|
|
36
|
+
|
|
37
|
+
expect(matchMedia('(prefers-reduced-motion: reduce)').matches).toBe(true);
|
|
38
|
+
expect(matchMedia('(prefers-reduced-motion: no-preference)').matches).toBe(
|
|
39
|
+
false,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('treats the bare preference query as reduce', () => {
|
|
44
|
+
stubMatchMedia({reduceMotion: true});
|
|
45
|
+
expect(matchMedia('(prefers-reduced-motion)').matches).toBe(true);
|
|
46
|
+
|
|
47
|
+
stubMatchMedia({reduceMotion: false});
|
|
48
|
+
expect(matchMedia('(prefers-reduced-motion)').matches).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('defaults to full motion and matching everything else', () => {
|
|
52
|
+
stubMatchMedia();
|
|
53
|
+
|
|
54
|
+
expect(matchMedia('(prefers-reduced-motion: reduce)').matches).toBe(false);
|
|
55
|
+
expect(matchMedia('(max-width: 768px)').matches).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('lets a non-motion query be answered false', () => {
|
|
59
|
+
// Above the breakpoint, motion still reduced — the other axis of the
|
|
60
|
+
// combination, so neither knob can be quietly driving both.
|
|
61
|
+
stubMatchMedia({reduceMotion: true, matches: false});
|
|
62
|
+
|
|
63
|
+
expect(matchMedia('(prefers-reduced-motion: reduce)').matches).toBe(true);
|
|
64
|
+
expect(matchMedia('(max-width: 768px)').matches).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('echoes the query and exposes the listener API components subscribe to', () => {
|
|
68
|
+
stubMatchMedia();
|
|
69
|
+
const mql = matchMedia('(max-width: 768px)');
|
|
70
|
+
|
|
71
|
+
// useMediaQuery subscribes; a stub without these throws on mount rather
|
|
72
|
+
// than failing an assertion, which reads as an unrelated crash.
|
|
73
|
+
expect(mql.media).toBe('(max-width: 768px)');
|
|
74
|
+
expect(typeof mql.addEventListener).toBe('function');
|
|
75
|
+
expect(typeof mql.removeEventListener).toBe('function');
|
|
76
|
+
expect(typeof mql.addListener).toBe('function');
|
|
77
|
+
expect(typeof mql.removeListener).toBe('function');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file stubMatchMedia.ts
|
|
5
|
+
* @input Uses vitest
|
|
6
|
+
* @output stubMatchMedia — a query-aware window.matchMedia stub
|
|
7
|
+
* @position Shared test helper; imported by component tests that render below
|
|
8
|
+
* a breakpoint while also exercising motion-sensitive behaviour
|
|
9
|
+
*
|
|
10
|
+
* jsdom has no matchMedia, so tests stub it. The usual stub answers every query
|
|
11
|
+
* with one boolean:
|
|
12
|
+
*
|
|
13
|
+
* vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({matches: true, ...}))
|
|
14
|
+
*
|
|
15
|
+
* That is enough to put AppShell below its breakpoint — and it silently also
|
|
16
|
+
* matches `prefers-reduced-motion`, rerouting any code that branches on motion.
|
|
17
|
+
* MobileNav's close delay is capped at 0 under reduced motion, so a suite using
|
|
18
|
+
* the blanket stub runs against an immediate close while its test names claim
|
|
19
|
+
* it is exercising the real delay. The failure mode is invisible: every test
|
|
20
|
+
* still passes.
|
|
21
|
+
*
|
|
22
|
+
* So the two answers are separate knobs here. `reduceMotion` answers
|
|
23
|
+
* `prefers-reduced-motion` only; `matches` answers everything else (breakpoint
|
|
24
|
+
* width queries, which tests below a breakpoint want to be true).
|
|
25
|
+
*
|
|
26
|
+
* SYNC: When modified, update this header.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {vi} from 'vitest';
|
|
30
|
+
|
|
31
|
+
export interface StubMatchMediaOptions {
|
|
32
|
+
/** Whether the user prefers reduced motion. */
|
|
33
|
+
reduceMotion?: boolean;
|
|
34
|
+
/** Answer for every other query — width breakpoints, hover, etc. */
|
|
35
|
+
matches?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Answer a `prefers-reduced-motion` query, or null if it isn't one.
|
|
40
|
+
*
|
|
41
|
+
* The preference has two spellings and they are opposites, so a single
|
|
42
|
+
* `includes('prefers-reduced-motion')` would answer `no-preference` with the
|
|
43
|
+
* value meant for `reduce` — the same answer-without-reading mistake this
|
|
44
|
+
* helper exists to prevent. Core only queries `reduce` today (MobileNav,
|
|
45
|
+
* Carousel); handling both is what keeps that true if one changes.
|
|
46
|
+
*/
|
|
47
|
+
function answerReducedMotion(
|
|
48
|
+
query: string,
|
|
49
|
+
reduceMotion: boolean,
|
|
50
|
+
): boolean | null {
|
|
51
|
+
if (query.includes('prefers-reduced-motion: reduce')) {
|
|
52
|
+
return reduceMotion;
|
|
53
|
+
}
|
|
54
|
+
if (query.includes('prefers-reduced-motion: no-preference')) {
|
|
55
|
+
return !reduceMotion;
|
|
56
|
+
}
|
|
57
|
+
// Bare `(prefers-reduced-motion)` is true for any value other than
|
|
58
|
+
// no-preference, i.e. the same as `reduce`.
|
|
59
|
+
if (query.includes('prefers-reduced-motion')) {
|
|
60
|
+
return reduceMotion;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install a `window.matchMedia` stub whose answer depends on the query.
|
|
67
|
+
*
|
|
68
|
+
* Call from `beforeEach` (or inside a test that needs different motion
|
|
69
|
+
* settings) and pair with `vi.unstubAllGlobals()` in `afterEach`.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```
|
|
73
|
+
* beforeEach(() => {
|
|
74
|
+
* stubMatchMedia({reduceMotion: false});
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function stubMatchMedia({
|
|
79
|
+
reduceMotion = false,
|
|
80
|
+
matches = true,
|
|
81
|
+
}: StubMatchMediaOptions = {}): void {
|
|
82
|
+
vi.stubGlobal(
|
|
83
|
+
'matchMedia',
|
|
84
|
+
vi.fn((query: string) => ({
|
|
85
|
+
matches: answerReducedMotion(query, reduceMotion) ?? matches,
|
|
86
|
+
media: query,
|
|
87
|
+
onchange: null,
|
|
88
|
+
addEventListener: vi.fn(),
|
|
89
|
+
removeEventListener: vi.fn(),
|
|
90
|
+
addListener: vi.fn(),
|
|
91
|
+
removeListener: vi.fn(),
|
|
92
|
+
dispatchEvent: vi.fn(),
|
|
93
|
+
})),
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
* SYNC: When modified, update this header
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export {
|
|
15
|
-
hasActiveFocusTrapEscape,
|
|
16
|
-
isImeKeyEvent,
|
|
17
|
-
useFocusTrap,
|
|
18
|
-
} from './useFocusTrap';
|
|
14
|
+
export {hasActiveFocusTrapEscape, useFocusTrap} from './useFocusTrap';
|
|
19
15
|
export type {UseFocusTrapOptions, UseFocusTrapReturn} from './useFocusTrap';
|
|
20
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Import from `@astryxdesign/core/utils` instead — this is a pure
|
|
19
|
+
* predicate, not a hook, and the `hooks` barrel is a `'use client'` boundary.
|
|
20
|
+
* Re-exported here for one release so consumers can move; will be removed in
|
|
21
|
+
* an upcoming major.
|
|
22
|
+
*/
|
|
23
|
+
export {isImeKeyEvent} from '../utils/ime';
|
|
24
|
+
|
|
21
25
|
export {useAnnounce} from './useAnnounce';
|
|
22
26
|
export type {AnnounceFn, AnnouncePoliteness} from './useAnnounce';
|
|
23
27
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import {useCallback, useEffect, useRef} from 'react';
|
|
20
20
|
|
|
21
21
|
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
22
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Module-level stack of active focus-trap Escape handlers.
|
|
@@ -94,20 +95,6 @@ export function hasActiveFocusTrapEscape(): boolean {
|
|
|
94
95
|
return escapeStack.length > 0;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
/**
|
|
98
|
-
* Whether an Escape keydown should be ignored because it is cancelling an
|
|
99
|
-
* in-progress IME composition. CJK/IME users press Escape to cancel
|
|
100
|
-
* composition; that must not close the surrounding overlay. `keyCode === 229`
|
|
101
|
-
* covers browsers that fire keydown before `isComposing` is set. Exported so
|
|
102
|
-
* other overlays (Dialog, Drawer, CommandPalette) share one definition.
|
|
103
|
-
*/
|
|
104
|
-
export function isImeKeyEvent(event: {
|
|
105
|
-
isComposing?: boolean;
|
|
106
|
-
keyCode?: number;
|
|
107
|
-
}): boolean {
|
|
108
|
-
return event.isComposing === true || event.keyCode === 229;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
98
|
/**
|
|
112
99
|
* Whether an element is currently perceivable/focusable — excludes ones hidden
|
|
113
100
|
* via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {describe, it, expect, vi} from 'vitest';
|
|
4
4
|
import type {IconRegistry} from '../Icon/globalIconRegistry';
|
|
5
|
+
import type {DefinedTheme} from './defineTheme';
|
|
5
6
|
import {defineTheme, generateThemeCSS, isDefinedTheme} from './defineTheme';
|
|
6
7
|
|
|
7
8
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
@@ -1126,4 +1127,130 @@ describe('defineTheme extends', () => {
|
|
|
1126
1127
|
base.tokens['--font-size-base'],
|
|
1127
1128
|
);
|
|
1128
1129
|
});
|
|
1130
|
+
|
|
1131
|
+
it('inherits indicators when the child has none', () => {
|
|
1132
|
+
const indicator = (() => null) as unknown as NonNullable<
|
|
1133
|
+
DefinedTheme['indicators']
|
|
1134
|
+
>['check'];
|
|
1135
|
+
const base = defineTheme({name: 'base', indicators: {check: indicator}});
|
|
1136
|
+
const child = defineTheme({name: 'child', extends: base});
|
|
1137
|
+
expect(child.indicators?.check).toBe(indicator);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it('inherits onDark token overrides from base theme', () => {
|
|
1141
|
+
const base = defineTheme({
|
|
1142
|
+
name: 'base',
|
|
1143
|
+
onDark: {tokens: {'--color-border': '#ffffff'}},
|
|
1144
|
+
});
|
|
1145
|
+
const child = defineTheme({name: 'child', extends: base});
|
|
1146
|
+
expect(child.__onDark?.tokens['--color-border']).toBe('#ffffff');
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('inherits onLight token overrides from base theme', () => {
|
|
1150
|
+
const base = defineTheme({
|
|
1151
|
+
name: 'base',
|
|
1152
|
+
onLight: {tokens: {'--color-border': '#000000'}},
|
|
1153
|
+
});
|
|
1154
|
+
const child = defineTheme({name: 'child', extends: base});
|
|
1155
|
+
expect(child.__onLight?.tokens['--color-border']).toBe('#000000');
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
it('lets the child override an inherited onDark token', () => {
|
|
1159
|
+
const base = defineTheme({
|
|
1160
|
+
name: 'base',
|
|
1161
|
+
onDark: {tokens: {'--color-border': '#ffffff', '--color-track': '#eee'}},
|
|
1162
|
+
});
|
|
1163
|
+
const child = defineTheme({
|
|
1164
|
+
name: 'child',
|
|
1165
|
+
extends: base,
|
|
1166
|
+
onDark: {tokens: {'--color-border': '#cccccc'}},
|
|
1167
|
+
});
|
|
1168
|
+
expect(child.__onDark?.tokens['--color-border']).toBe('#cccccc');
|
|
1169
|
+
expect(child.__onDark?.tokens['--color-track']).toBe('#eee');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('inherits and deep-merges onDark component overrides', () => {
|
|
1173
|
+
const base = defineTheme({
|
|
1174
|
+
name: 'base',
|
|
1175
|
+
onDark: {
|
|
1176
|
+
components: {
|
|
1177
|
+
card: {base: {borderColor: '#fff', backgroundColor: '#111'}},
|
|
1178
|
+
badge: {base: {color: '#fff'}},
|
|
1179
|
+
},
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
const child = defineTheme({
|
|
1183
|
+
name: 'child',
|
|
1184
|
+
extends: base,
|
|
1185
|
+
onDark: {components: {card: {base: {borderColor: '#ccc'}}}},
|
|
1186
|
+
});
|
|
1187
|
+
expect(child.__onDark?.components?.card?.base).toEqual({
|
|
1188
|
+
borderColor: '#ccc',
|
|
1189
|
+
backgroundColor: '#111',
|
|
1190
|
+
});
|
|
1191
|
+
expect(child.__onDark?.components?.badge?.base).toEqual({color: '#fff'});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it('inherits __inputTokens so inherited [light, dark] tuples survive', () => {
|
|
1195
|
+
const base = defineTheme({
|
|
1196
|
+
name: 'base',
|
|
1197
|
+
tokens: {'--color-accent': ['#111111', '#eeeeee']},
|
|
1198
|
+
});
|
|
1199
|
+
const child = defineTheme({
|
|
1200
|
+
name: 'child',
|
|
1201
|
+
extends: base,
|
|
1202
|
+
tokens: {'--color-border': '#cccccc'},
|
|
1203
|
+
});
|
|
1204
|
+
expect(child.__inputTokens?.['--color-accent']).toEqual([
|
|
1205
|
+
'#111111',
|
|
1206
|
+
'#eeeeee',
|
|
1207
|
+
]);
|
|
1208
|
+
expect(child.__inputTokens?.['--color-border']).toBe('#cccccc');
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
it('__inputTokens from the child win over the base', () => {
|
|
1212
|
+
const base = defineTheme({
|
|
1213
|
+
name: 'base',
|
|
1214
|
+
tokens: {'--color-accent': ['#111111', '#eeeeee']},
|
|
1215
|
+
});
|
|
1216
|
+
const child = defineTheme({
|
|
1217
|
+
name: 'child',
|
|
1218
|
+
extends: base,
|
|
1219
|
+
tokens: {'--color-accent': '#ff0000'},
|
|
1220
|
+
});
|
|
1221
|
+
expect(child.__inputTokens?.['--color-accent']).toBe('#ff0000');
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// The failure that motivated these: `extends` resolving to `undefined` (a
|
|
1225
|
+
// named import that silently missed — see the theme-build resolution tests)
|
|
1226
|
+
// used to inherit nothing and build a stylesheet that looked fine.
|
|
1227
|
+
it('throws when extends is present but undefined', () => {
|
|
1228
|
+
expect(() =>
|
|
1229
|
+
defineTheme({
|
|
1230
|
+
name: 'child',
|
|
1231
|
+
extends: undefined,
|
|
1232
|
+
tokens: {'--color-accent': '#ff0000'},
|
|
1233
|
+
}),
|
|
1234
|
+
).toThrow(/extends/);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('throws when extends is not a theme', () => {
|
|
1238
|
+
expect(() =>
|
|
1239
|
+
defineTheme({
|
|
1240
|
+
name: 'child',
|
|
1241
|
+
// A module namespace object is the shape a bad import hands over.
|
|
1242
|
+
extends: {foo: 'bar'} as unknown as DefinedTheme,
|
|
1243
|
+
}),
|
|
1244
|
+
).toThrow(/extends/);
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
it('accepts a pre-built theme module as a base', () => {
|
|
1248
|
+
const built = {
|
|
1249
|
+
name: 'built-base',
|
|
1250
|
+
__built: true,
|
|
1251
|
+
tokens: {'--color-accent': '#111111'},
|
|
1252
|
+
} as DefinedTheme;
|
|
1253
|
+
const child = defineTheme({name: 'child', extends: built});
|
|
1254
|
+
expect(child.tokens['--color-accent']).toBe('#111111');
|
|
1255
|
+
});
|
|
1129
1256
|
});
|
package/src/theme/defineTheme.ts
CHANGED
|
@@ -72,6 +72,7 @@ import type {DomainTokenName} from './domainTokens';
|
|
|
72
72
|
import {domainTokenDefaults} from './domainTokens';
|
|
73
73
|
import type {SyntaxThemeDefinition} from './syntax';
|
|
74
74
|
import {registerTheme} from './themeRegistry';
|
|
75
|
+
import {deepMergeComponents} from './mergeComponents';
|
|
75
76
|
|
|
76
77
|
// =============================================================================
|
|
77
78
|
// Types
|
|
@@ -165,9 +166,14 @@ export interface DefineThemeInput {
|
|
|
165
166
|
name: string;
|
|
166
167
|
|
|
167
168
|
/**
|
|
168
|
-
* Base theme to extend. When provided, the new theme starts with
|
|
169
|
-
* base
|
|
170
|
-
*
|
|
169
|
+
* Base theme to extend. When provided, the new theme starts with everything
|
|
170
|
+
* the base resolved to — tokens, component overrides, icons, indicators, and
|
|
171
|
+
* its `onDark`/`onLight` surfaces — then applies this input on top. The base
|
|
172
|
+
* theme's values have lowest precedence.
|
|
173
|
+
*
|
|
174
|
+
* The result is flat: an extended theme carries its inheritance in its own
|
|
175
|
+
* resolved output, so `astryx theme build` emits one self-contained
|
|
176
|
+
* stylesheet and the base's CSS does not need to be loaded alongside it.
|
|
171
177
|
*
|
|
172
178
|
* Use this to create variant themes that customize only a few aspects
|
|
173
179
|
* (e.g. icons, accent color) without re-specifying the full theme.
|
|
@@ -242,20 +248,38 @@ export interface DefineThemeInput {
|
|
|
242
248
|
*/
|
|
243
249
|
radius?: RadiusScaleConfig;
|
|
244
250
|
/**
|
|
245
|
-
* Color scale configuration. Generates color token overrides from
|
|
246
|
-
*
|
|
251
|
+
* Color scale configuration. Generates color token overrides from an
|
|
252
|
+
* accent seed using the HCT perceptual color model.
|
|
247
253
|
*
|
|
248
254
|
* Only generates tokens derivable from the accent — status colors,
|
|
249
255
|
* categorical hues, and fixed tokens (on-dark/on-light) use defaults.
|
|
250
|
-
*
|
|
256
|
+
*
|
|
257
|
+
* `accent` accepts a single hex (same seed for both color schemes) or a
|
|
258
|
+
* `[light, dark]` tuple, matching `TokenValue`. With a tuple, the light
|
|
259
|
+
* scheme's full palette derives from the light seed and the dark
|
|
260
|
+
* scheme's from the dark seed.
|
|
251
261
|
*
|
|
252
262
|
* `accent` is optional — omit it for a neutral-only theme, which keeps
|
|
253
263
|
* the default accent tokens and only themes the neutrals.
|
|
254
264
|
*
|
|
265
|
+
* Precedence vs `tokens`: explicit `tokens` entries win over generated
|
|
266
|
+
* values, token by token. Because `--color-accent-muted`,
|
|
267
|
+
* `--color-text-accent` and `--color-icon-accent` are generated as
|
|
268
|
+
* `var(--color-accent)` references, a `tokens['--color-accent']`
|
|
269
|
+
* override re-points them at runtime. `--color-on-accent` does NOT
|
|
270
|
+
* follow: it is baked from the `color.accent` seed (a contrast
|
|
271
|
+
* computation CSS cannot express), so overriding the accent through
|
|
272
|
+
* `tokens` without also overriding `--color-on-accent` leaves the two
|
|
273
|
+
* out of sync. To re-seat the whole palette per scheme, prefer a tuple
|
|
274
|
+
* `color.accent` over the `tokens['--color-accent']` workaround.
|
|
275
|
+
*
|
|
255
276
|
* @example
|
|
256
|
-
* ```
|
|
277
|
+
* ```
|
|
257
278
|
* color: { accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard' }
|
|
258
279
|
*
|
|
280
|
+
* // Per-scheme accents — light palette from the first seed, dark from the second
|
|
281
|
+
* color: { accent: ['#0064E0', '#48CAE4'] }
|
|
282
|
+
*
|
|
259
283
|
* // Neutral-only — accent tokens stay at their defaults
|
|
260
284
|
* color: { neutralStyle: 'warm' }
|
|
261
285
|
* ```
|
|
@@ -416,49 +440,6 @@ function resolveTokenValue(value: TokenValue): string {
|
|
|
416
440
|
return value;
|
|
417
441
|
}
|
|
418
442
|
|
|
419
|
-
/**
|
|
420
|
-
* Deep-merge two component style maps.
|
|
421
|
-
* Properties in `overrides` take precedence over `base`.
|
|
422
|
-
* This allows typeScale-generated rules to be overridden by explicit components.
|
|
423
|
-
*/
|
|
424
|
-
function deepMergeComponents(
|
|
425
|
-
base?: ComponentStyleMap,
|
|
426
|
-
overrides?: ComponentStyleMap,
|
|
427
|
-
): ComponentStyleMap | undefined {
|
|
428
|
-
if (!base && !overrides) {
|
|
429
|
-
return undefined;
|
|
430
|
-
}
|
|
431
|
-
if (!base) {
|
|
432
|
-
return overrides;
|
|
433
|
-
}
|
|
434
|
-
if (!overrides) {
|
|
435
|
-
return base;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const result: ComponentStyleMap = {};
|
|
439
|
-
|
|
440
|
-
// Start with all base entries
|
|
441
|
-
for (const [component, rules] of Object.entries(base)) {
|
|
442
|
-
result[component] = {...rules};
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Merge overrides on top
|
|
446
|
-
for (const [component, rules] of Object.entries(overrides)) {
|
|
447
|
-
if (!result[component]) {
|
|
448
|
-
result[component] = {...rules};
|
|
449
|
-
} else {
|
|
450
|
-
for (const [key, styles] of Object.entries(rules)) {
|
|
451
|
-
result[component][key] = {
|
|
452
|
-
...result[component][key],
|
|
453
|
-
...styles,
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return result;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
443
|
/**
|
|
463
444
|
* Resolve a FontWeight name to a var() reference.
|
|
464
445
|
* Named weights map to var(--font-weight-*); raw values pass through.
|
|
@@ -491,6 +472,24 @@ function buildFontFamily(
|
|
|
491
472
|
return quoted;
|
|
492
473
|
}
|
|
493
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Describe a rejected `extends` value for the error message — enough to tell a
|
|
477
|
+
* missed import (`undefined`) from a module namespace or a plain object.
|
|
478
|
+
*/
|
|
479
|
+
function describeBadBase(value: unknown): string {
|
|
480
|
+
if (value === undefined) {
|
|
481
|
+
return 'undefined';
|
|
482
|
+
}
|
|
483
|
+
if (value === null) {
|
|
484
|
+
return 'null';
|
|
485
|
+
}
|
|
486
|
+
if (typeof value !== 'object') {
|
|
487
|
+
return typeof value;
|
|
488
|
+
}
|
|
489
|
+
const keys = Object.keys(value);
|
|
490
|
+
return `an object with keys [${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ', …' : ''}]`;
|
|
491
|
+
}
|
|
492
|
+
|
|
494
493
|
/**
|
|
495
494
|
* Create an Astryx theme.
|
|
496
495
|
*
|
|
@@ -504,7 +503,19 @@ function buildFontFamily(
|
|
|
504
503
|
export function defineTheme(input: DefineThemeInput): DefinedTheme {
|
|
505
504
|
const tokens: Record<string, string> = {};
|
|
506
505
|
|
|
507
|
-
// 0. Pre-seed from base theme when `extends` is provided (lowest precedence)
|
|
506
|
+
// 0. Pre-seed from base theme when `extends` is provided (lowest precedence).
|
|
507
|
+
// A base that is not a theme is refused rather than ignored: `extends` used
|
|
508
|
+
// to inherit nothing when its value was undefined, which is what a named
|
|
509
|
+
// import silently resolving to the wrong module hands over, and the theme
|
|
510
|
+
// then built into a plausible-looking stylesheet missing everything it was
|
|
511
|
+
// supposed to inherit.
|
|
512
|
+
if ('extends' in input && !isDefinedTheme(input.extends)) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`defineTheme("${input.name}"): \`extends\` must be a theme from defineTheme(), got ${describeBadBase(input.extends)}. ` +
|
|
515
|
+
`Check that the import naming your base theme resolves to its source and exports that name — ` +
|
|
516
|
+
`a generated \`<theme>.js\` artifact sitting next to the source exports \`<name>Theme\`, not the source's own export.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
508
519
|
const base = input.extends;
|
|
509
520
|
if (base) {
|
|
510
521
|
for (const [key, value] of Object.entries(base.tokens)) {
|
|
@@ -640,9 +651,10 @@ export function defineTheme(input: DefineThemeInput): DefinedTheme {
|
|
|
640
651
|
components = deepMergeComponents(base.components, components);
|
|
641
652
|
}
|
|
642
653
|
|
|
643
|
-
// 4. Resolve on-media token overrides (
|
|
644
|
-
|
|
645
|
-
const
|
|
654
|
+
// 4. Resolve on-media token overrides (base's resolved surface, then
|
|
655
|
+
// defaults, then this theme's own overrides)
|
|
656
|
+
const __onDark = resolveOnMedia('dark', input.onDark, base?.__onDark);
|
|
657
|
+
const __onLight = resolveOnMedia('light', input.onLight, base?.__onLight);
|
|
646
658
|
|
|
647
659
|
// 5. Merge icons — input icons override base icons
|
|
648
660
|
const icons =
|
|
@@ -663,7 +675,10 @@ export function defineTheme(input: DefineThemeInput): DefinedTheme {
|
|
|
663
675
|
components,
|
|
664
676
|
icons,
|
|
665
677
|
indicators,
|
|
666
|
-
__inputTokens:
|
|
678
|
+
__inputTokens:
|
|
679
|
+
base?.__inputTokens || input.tokens
|
|
680
|
+
? {...base?.__inputTokens, ...input.tokens}
|
|
681
|
+
: undefined,
|
|
667
682
|
__onDark,
|
|
668
683
|
__onLight,
|
|
669
684
|
};
|