@astryxdesign/core 0.4.3 → 0.4.4-canary.0f4d45f
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 +60 -0
- package/README.md +79 -45
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +13 -3
- package/dist/BottomSheet/BottomSheet.d.ts +66 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +392 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +60 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +398 -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 +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +180 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +989 -0
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +12 -6
- 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/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- 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 +21 -7
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +9 -1
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +14 -3
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +5 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +8 -1
- package/dist/Tokenizer/Tokenizer.js +11 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +21 -0
- 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/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- 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 +22 -4
- package/dist/theme/defineTheme.d.ts.map +1 -1
- 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/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 +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +48 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +36 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +9 -8
- package/src/Banner/Banner.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +40 -0
- package/src/Banner/Banner.tsx +27 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +266 -0
- package/src/BottomSheet/BottomSheet.test.tsx +2114 -0
- package/src/BottomSheet/BottomSheet.tsx +545 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +547 -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 +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1059 -0
- package/src/BottomSheet/useSheetGestures.ts +1459 -0
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/ComplexSelector/ComplexSelector.tsx +12 -5
- 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/Field/Field.doc.mjs +1 -0
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FileInput/FileInput.test.tsx +53 -9
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- 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 +195 -51
- package/src/MultiSelector/MultiSelector.tsx +22 -8
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +9 -1
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +13 -4
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +7 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +8 -0
- package/src/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/Typeahead/BaseTypeahead.tsx +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- 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/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.ts +22 -4
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +2 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -192,6 +192,144 @@ describe('expandColorScale — neutral-only themes (#2279)', () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
describe('expandColorScale — tuple accent (#2279)', () => {
|
|
196
|
+
const LIGHT_SEED = '#B7410E';
|
|
197
|
+
const DARK_SEED = '#48CAE4';
|
|
198
|
+
|
|
199
|
+
it('accepts a [light, dark] tuple and generates the accent tokens', () => {
|
|
200
|
+
const tokens = expandColorScale({accent: [LIGHT_SEED, DARK_SEED]});
|
|
201
|
+
for (const key of ACCENT_TOKENS) {
|
|
202
|
+
expect(tokens).toHaveProperty(key);
|
|
203
|
+
}
|
|
204
|
+
expect(tokens['--color-accent']).toMatch(/^light-dark\(#/);
|
|
205
|
+
expect(tokens['--color-on-accent']).toMatch(/^light-dark\(#/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('a same-halves tuple matches the single-string expansion, token for token', () => {
|
|
209
|
+
// Pins the string path as the tuple fast path: introducing tuples must
|
|
210
|
+
// not re-color any existing single-accent theme.
|
|
211
|
+
const fromString = expandColorScale({accent: LIGHT_SEED});
|
|
212
|
+
const fromTuple = expandColorScale({accent: [LIGHT_SEED, LIGHT_SEED]});
|
|
213
|
+
expect(fromTuple).toEqual(fromString);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('derives the light palette from the light seed and the dark palette from the dark seed', () => {
|
|
217
|
+
// Every generated light-dark() pair takes its light half from the light
|
|
218
|
+
// seed's palettes and its dark half from the dark seed's, so resolving
|
|
219
|
+
// each mode must agree with the matching single-accent expansion.
|
|
220
|
+
const tuple = defineTheme({
|
|
221
|
+
name: 'tuple',
|
|
222
|
+
color: {accent: [LIGHT_SEED, DARK_SEED]},
|
|
223
|
+
});
|
|
224
|
+
const fromLight = defineTheme({name: 'light', color: {accent: LIGHT_SEED}});
|
|
225
|
+
const fromDark = defineTheme({name: 'dark', color: {accent: DARK_SEED}});
|
|
226
|
+
|
|
227
|
+
const generatedKeys = Object.keys(
|
|
228
|
+
expandColorScale({accent: [LIGHT_SEED, DARK_SEED]}),
|
|
229
|
+
);
|
|
230
|
+
const tupleLight = resolveThemeTokens(tuple, {mode: 'light'});
|
|
231
|
+
const refLight = resolveThemeTokens(fromLight, {mode: 'light'});
|
|
232
|
+
const tupleDark = resolveThemeTokens(tuple, {mode: 'dark'});
|
|
233
|
+
const refDark = resolveThemeTokens(fromDark, {mode: 'dark'});
|
|
234
|
+
for (const key of generatedKeys) {
|
|
235
|
+
expect(tupleLight[key], `${key} (light)`).toBe(refLight[key]);
|
|
236
|
+
expect(tupleDark[key], `${key} (dark)`).toBe(refDark[key]);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('differing seeds produce a dark half the light seed would not', () => {
|
|
241
|
+
// Regression guard: the dark half must not silently reuse the light seed.
|
|
242
|
+
const tuple = defineTheme({
|
|
243
|
+
name: 'tuple-differs',
|
|
244
|
+
color: {accent: [LIGHT_SEED, DARK_SEED]},
|
|
245
|
+
});
|
|
246
|
+
const single = defineTheme({
|
|
247
|
+
name: 'single',
|
|
248
|
+
color: {accent: LIGHT_SEED},
|
|
249
|
+
});
|
|
250
|
+
expect(
|
|
251
|
+
resolveThemeTokens(tuple, {mode: 'dark'})['--color-accent'],
|
|
252
|
+
).not.toBe(resolveThemeTokens(single, {mode: 'dark'})['--color-accent']);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('keeps the derived accent tokens as references with a tuple accent', () => {
|
|
256
|
+
const tokens = expandColorScale({accent: [LIGHT_SEED, DARK_SEED]});
|
|
257
|
+
expect(tokens['--color-text-accent']).toBe('var(--color-accent)');
|
|
258
|
+
expect(tokens['--color-icon-accent']).toBe('var(--color-accent)');
|
|
259
|
+
expect(tokens['--color-accent-muted']).toContain('var(--color-accent)');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('honours neutralStyle with a tuple accent', () => {
|
|
263
|
+
const warm = expandColorScale({
|
|
264
|
+
accent: [LIGHT_SEED, DARK_SEED],
|
|
265
|
+
neutralStyle: 'warm',
|
|
266
|
+
});
|
|
267
|
+
const cool = expandColorScale({
|
|
268
|
+
accent: [LIGHT_SEED, DARK_SEED],
|
|
269
|
+
neutralStyle: 'cool',
|
|
270
|
+
});
|
|
271
|
+
expect(warm['--color-neutral']).not.toBe(cool['--color-neutral']);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('accent precedence — color config vs tokens overrides (#2279)', () => {
|
|
276
|
+
const CONFIG_ACCENT: [string, string] = ['#B7410E', '#48CAE4'];
|
|
277
|
+
const TOKEN_ACCENT: [string, string] = ['#AA0000', '#FF5555'];
|
|
278
|
+
|
|
279
|
+
it('tokens["--color-accent"] wins over the color-generated accent', () => {
|
|
280
|
+
const theme = defineTheme({
|
|
281
|
+
name: 'precedence-base',
|
|
282
|
+
color: {accent: CONFIG_ACCENT},
|
|
283
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
284
|
+
});
|
|
285
|
+
expect(theme.tokens['--color-accent']).toBe(
|
|
286
|
+
`light-dark(${TOKEN_ACCENT[0]}, ${TOKEN_ACCENT[1]})`,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('reference tokens follow the tokens override at runtime', () => {
|
|
291
|
+
const theme = defineTheme({
|
|
292
|
+
name: 'precedence-refs',
|
|
293
|
+
color: {accent: CONFIG_ACCENT},
|
|
294
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
295
|
+
});
|
|
296
|
+
const light = resolveThemeTokens(theme, {mode: 'light'});
|
|
297
|
+
expect(light['--color-text-accent']).toBe(TOKEN_ACCENT[0]);
|
|
298
|
+
expect(light['--color-icon-accent']).toBe(TOKEN_ACCENT[0]);
|
|
299
|
+
const dark = resolveThemeTokens(theme, {mode: 'dark'});
|
|
300
|
+
expect(dark['--color-text-accent']).toBe(TOKEN_ACCENT[1]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('baked --color-on-accent stays derived from the color.accent seed', () => {
|
|
304
|
+
// Documented divergence: a tokens override of --color-accent does not
|
|
305
|
+
// re-derive --color-on-accent, which is a baked contrast computation
|
|
306
|
+
// against the color.accent seed. Consumers who override the accent via
|
|
307
|
+
// tokens must override --color-on-accent too (or pass the tuple through
|
|
308
|
+
// color.accent so the whole palette re-derives).
|
|
309
|
+
const overridden = defineTheme({
|
|
310
|
+
name: 'precedence-on-accent',
|
|
311
|
+
color: {accent: CONFIG_ACCENT},
|
|
312
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
313
|
+
});
|
|
314
|
+
const generated = defineTheme({
|
|
315
|
+
name: 'generated-on-accent',
|
|
316
|
+
color: {accent: CONFIG_ACCENT},
|
|
317
|
+
});
|
|
318
|
+
expect(overridden.tokens['--color-on-accent']).toBe(
|
|
319
|
+
generated.tokens['--color-on-accent'],
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('an explicit --color-on-accent token wins over the generated one', () => {
|
|
324
|
+
const theme = defineTheme({
|
|
325
|
+
name: 'precedence-on-accent-explicit',
|
|
326
|
+
color: {accent: CONFIG_ACCENT},
|
|
327
|
+
tokens: {'--color-on-accent': '#FFFFFF'},
|
|
328
|
+
});
|
|
329
|
+
expect(theme.tokens['--color-on-accent']).toBe('#FFFFFF');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
195
333
|
describe('expandColorScale + defineTheme integration', () => {
|
|
196
334
|
it('explicit token overrides win over generated values', () => {
|
|
197
335
|
const theme = defineTheme({
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* @output Token overrides for derivable color tokens
|
|
7
7
|
* @position Theme utility; consumed by defineTheme.ts
|
|
8
8
|
*
|
|
9
|
-
* Generates color token overrides from
|
|
10
|
-
*
|
|
9
|
+
* Generates color token overrides from an accent seed using the HCT
|
|
10
|
+
* perceptual color model. Only produces tokens that meaningfully
|
|
11
11
|
* derive from the accent — status colors, categorical hues, and fixed
|
|
12
12
|
* tokens (on-dark/on-light) fall through to colorDefaults.
|
|
13
13
|
*
|
|
@@ -15,11 +15,19 @@
|
|
|
15
15
|
* ramp (seeded from the default accent's hue) while the accent tokens
|
|
16
16
|
* themselves fall through to colorDefaults, same as the tokens above.
|
|
17
17
|
*
|
|
18
|
+
* `accent` also accepts a `[light, dark]` tuple (matching `TokenValue` in
|
|
19
|
+
* defineTheme). A tuple seeds each color scheme from its own half: the
|
|
20
|
+
* light side of every generated `light-dark()` pair derives from the light
|
|
21
|
+
* accent's palettes, the dark side from the dark accent's. A plain string
|
|
22
|
+
* seeds both sides identically, token for token the same output as before
|
|
23
|
+
* tuples existed.
|
|
24
|
+
*
|
|
18
25
|
* WCAG contrast guarantees (asserted in expandColorScale.test.ts):
|
|
19
26
|
* - Text tones are guaranteed >= 4.5:1 against their surfaces by tone
|
|
20
27
|
* spacing alone — HCT tone is CIE L*, which fixes relative luminance
|
|
21
28
|
* regardless of hue/chroma, so the fixed tone assignments hold for any
|
|
22
|
-
* accent/neutralStyle (WCAG 1.4.3)
|
|
29
|
+
* accent/neutralStyle (WCAG 1.4.3), and each half of a tuple pairs with
|
|
30
|
+
* surfaces derived from that same seed.
|
|
23
31
|
* - --color-border-emphasized (form-control boundaries) is tone-bumped
|
|
24
32
|
* until it reaches >= 3:1 against the generated surface (WCAG 1.4.11).
|
|
25
33
|
* - --color-border, --color-skeleton, and --color-track are intentionally
|
|
@@ -46,6 +54,9 @@ import {hexToHct, hctToHex, tonalPalette, hexWithAlpha} from './hct';
|
|
|
46
54
|
* // Minimal — just a seed color
|
|
47
55
|
* { accent: '#0064E0' }
|
|
48
56
|
*
|
|
57
|
+
* // Per-scheme seeds: light palettes from '#0064E0', dark from '#48CAE4'
|
|
58
|
+
* { accent: ['#0064E0', '#48CAE4'] }
|
|
59
|
+
*
|
|
49
60
|
* // With customization
|
|
50
61
|
* { accent: '#B7410E', neutralStyle: 'warm', contrast: 'high' }
|
|
51
62
|
*
|
|
@@ -55,14 +66,18 @@ import {hexToHct, hctToHex, tonalPalette, hexWithAlpha} from './hct';
|
|
|
55
66
|
*/
|
|
56
67
|
export interface ColorScaleConfig {
|
|
57
68
|
/**
|
|
58
|
-
* Seed accent color
|
|
69
|
+
* Seed accent color. Everything derives from this.
|
|
70
|
+
*
|
|
71
|
+
* Either a single hex (#RRGGBB) used for both color schemes, or a
|
|
72
|
+
* `[light, dark]` hex tuple: the light scheme's palettes derive from
|
|
73
|
+
* the light seed and the dark scheme's palettes from the dark seed.
|
|
59
74
|
*
|
|
60
75
|
* Optional. When omitted, the neutral palettes are seeded from the
|
|
61
76
|
* default accent's hue and the accent tokens (--color-accent,
|
|
62
77
|
* --color-accent-muted, --color-on-accent) are not generated — they
|
|
63
78
|
* fall through to colorDefaults.
|
|
64
79
|
*/
|
|
65
|
-
accent?: string;
|
|
80
|
+
accent?: string | [light: string, dark: string];
|
|
66
81
|
|
|
67
82
|
/**
|
|
68
83
|
* Neutral tone warmth. Controls how much of the seed's hue bleeds
|
|
@@ -159,6 +174,11 @@ export function ensureContrastTone(
|
|
|
159
174
|
* --color-on-dark/on-light) are NOT generated — they fall through
|
|
160
175
|
* to colorDefaults.
|
|
161
176
|
*
|
|
177
|
+
* A `[light, dark]` tuple accent seeds each scheme separately: the light
|
|
178
|
+
* half of every generated `light-dark()` pair comes from the light seed's
|
|
179
|
+
* palettes, the dark half from the dark seed's. A string accent seeds both
|
|
180
|
+
* halves from the same palettes, exactly as before tuples were supported.
|
|
181
|
+
*
|
|
162
182
|
* Without an `accent`, the accent tokens join that fall-through set: the
|
|
163
183
|
* neutrals are seeded from the default accent's hue, and --color-accent,
|
|
164
184
|
* --color-accent-muted and --color-on-accent keep their colorDefaults values.
|
|
@@ -168,6 +188,9 @@ export function ensureContrastTone(
|
|
|
168
188
|
* const tokens = expandColorScale({ accent: '#0064E0' });
|
|
169
189
|
* // tokens['--color-accent'] === 'light-dark(#..., #...)'
|
|
170
190
|
*
|
|
191
|
+
* const perScheme = expandColorScale({ accent: ['#0064E0', '#48CAE4'] });
|
|
192
|
+
* // light half derives from #0064E0, dark half from #48CAE4
|
|
193
|
+
*
|
|
171
194
|
* const neutralOnly = expandColorScale({ neutralStyle: 'warm' });
|
|
172
195
|
* // neutralOnly['--color-accent'] === undefined
|
|
173
196
|
* ```
|
|
@@ -175,16 +198,33 @@ export function ensureContrastTone(
|
|
|
175
198
|
export function expandColorScale(config: ColorScaleConfig): ColorScaleTokens {
|
|
176
199
|
const {accent, neutralStyle = 'cool', contrast = 'standard'} = config;
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
// Normalize to per-scheme seeds. A string accent (or an absent one) uses
|
|
202
|
+
// the same seed for both halves, which keeps single-seed output identical
|
|
203
|
+
// to the pre-tuple implementation.
|
|
204
|
+
const [lightAccent, darkAccent] = Array.isArray(accent)
|
|
205
|
+
? accent
|
|
206
|
+
: [accent, accent];
|
|
207
|
+
|
|
208
|
+
const lightSeed = hexToHct(lightAccent ?? DEFAULT_ACCENT_SEED);
|
|
209
|
+
const sameSeed = darkAccent === lightAccent;
|
|
210
|
+
const darkSeed = sameSeed
|
|
211
|
+
? lightSeed
|
|
212
|
+
: hexToHct(darkAccent ?? DEFAULT_ACCENT_SEED);
|
|
180
213
|
|
|
181
|
-
const primaryChroma = Math.max(seed.chroma, 48);
|
|
182
214
|
const neutralChroma = NEUTRAL_CHROMA[neutralStyle] ?? 5;
|
|
183
215
|
const neutralVariantChroma = NEUTRAL_VARIANT_CHROMA[neutralStyle] ?? 8;
|
|
184
216
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
217
|
+
// Palette naming: *L palettes feed the light (first) half of each ld()
|
|
218
|
+
// pair below, *D palettes the dark (second) half. With a single seed the
|
|
219
|
+
// D palettes alias the L ones.
|
|
220
|
+
const PL = tonalPalette(lightSeed.hue, Math.max(lightSeed.chroma, 48));
|
|
221
|
+
const NL = tonalPalette(lightSeed.hue, neutralChroma);
|
|
222
|
+
const NVL = tonalPalette(lightSeed.hue, neutralVariantChroma);
|
|
223
|
+
const PD = sameSeed
|
|
224
|
+
? PL
|
|
225
|
+
: tonalPalette(darkSeed.hue, Math.max(darkSeed.chroma, 48));
|
|
226
|
+
const ND = sameSeed ? NL : tonalPalette(darkSeed.hue, neutralChroma);
|
|
227
|
+
const NVD = sameSeed ? NVL : tonalPalette(darkSeed.hue, neutralVariantChroma);
|
|
188
228
|
|
|
189
229
|
const isHigh = contrast === 'high';
|
|
190
230
|
|
|
@@ -209,19 +249,19 @@ export function expandColorScale(config: ColorScaleConfig): ColorScaleTokens {
|
|
|
209
249
|
const borderEmphasizedStartDark = isHigh ? 50 : 30;
|
|
210
250
|
const borderEmphasized = ld(
|
|
211
251
|
ensureContrastTone(
|
|
212
|
-
|
|
252
|
+
lightSeed.hue,
|
|
213
253
|
neutralVariantChroma,
|
|
214
254
|
borderEmphasizedStartLight,
|
|
215
255
|
-1,
|
|
216
|
-
|
|
256
|
+
NL[99],
|
|
217
257
|
NON_TEXT_MIN_CONTRAST,
|
|
218
258
|
),
|
|
219
259
|
ensureContrastTone(
|
|
220
|
-
|
|
260
|
+
darkSeed.hue,
|
|
221
261
|
neutralVariantChroma,
|
|
222
262
|
borderEmphasizedStartDark,
|
|
223
263
|
1,
|
|
224
|
-
|
|
264
|
+
ND[10],
|
|
225
265
|
NON_TEXT_MIN_CONTRAST,
|
|
226
266
|
),
|
|
227
267
|
);
|
|
@@ -234,7 +274,7 @@ export function expandColorScale(config: ColorScaleConfig): ColorScaleTokens {
|
|
|
234
274
|
// seed above, so a supplied-but-malformed accent keeps its old behavior.
|
|
235
275
|
...(accent != null
|
|
236
276
|
? {
|
|
237
|
-
'--color-accent': ld(
|
|
277
|
+
'--color-accent': ld(PL[40], PD[80]),
|
|
238
278
|
// Derived accent tokens reference --color-accent instead of baking its
|
|
239
279
|
// resolved hex, so a scoped override of the base token re-accents the
|
|
240
280
|
// whole subtree at runtime. --color-on-accent stays baked: it is a
|
|
@@ -243,65 +283,70 @@ export function expandColorScale(config: ColorScaleConfig): ColorScaleTokens {
|
|
|
243
283
|
accentWithAlpha(0.2),
|
|
244
284
|
accentWithAlpha(0.25),
|
|
245
285
|
),
|
|
246
|
-
'--color-on-accent': ld(
|
|
286
|
+
'--color-on-accent': ld(PL[100], PD[20]),
|
|
247
287
|
}
|
|
248
288
|
: null),
|
|
249
|
-
'--color-neutral': ld(hexWithAlpha(
|
|
250
|
-
'--color-background-surface': ld(
|
|
251
|
-
'--color-background-body': ld(
|
|
252
|
-
'--color-overlay': ld(hexWithAlpha(
|
|
289
|
+
'--color-neutral': ld(hexWithAlpha(NL[10], 0.1), hexWithAlpha(ND[90], 0.2)),
|
|
290
|
+
'--color-background-surface': ld(NL[99], ND[10]),
|
|
291
|
+
'--color-background-body': ld(NL[95], ND[5]),
|
|
292
|
+
'--color-overlay': ld(hexWithAlpha(NL[10], 0.4), hexWithAlpha(ND[10], 0.6)),
|
|
253
293
|
'--color-overlay-hover': ld(
|
|
254
|
-
hexWithAlpha(
|
|
255
|
-
hexWithAlpha(
|
|
294
|
+
hexWithAlpha(NL[10], 0.05),
|
|
295
|
+
hexWithAlpha(ND[100], 0.05),
|
|
256
296
|
),
|
|
257
297
|
'--color-overlay-pressed': ld(
|
|
258
|
-
hexWithAlpha(
|
|
259
|
-
hexWithAlpha(
|
|
298
|
+
hexWithAlpha(NL[10], 0.1),
|
|
299
|
+
hexWithAlpha(ND[100], 0.1),
|
|
260
300
|
),
|
|
261
301
|
'--color-background-muted': ld(
|
|
262
|
-
hexWithAlpha(
|
|
263
|
-
hexWithAlpha(
|
|
302
|
+
hexWithAlpha(NL[10], 0.05),
|
|
303
|
+
hexWithAlpha(ND[10], 0.5),
|
|
264
304
|
),
|
|
265
305
|
|
|
266
306
|
// Text
|
|
267
|
-
'--color-text-primary': ld(
|
|
307
|
+
'--color-text-primary': ld(
|
|
308
|
+
NL[textPrimaryLightTone],
|
|
309
|
+
ND[textPrimaryDarkTone],
|
|
310
|
+
),
|
|
268
311
|
'--color-text-secondary': ld(
|
|
269
|
-
|
|
270
|
-
|
|
312
|
+
NVL[textSecondaryLightTone],
|
|
313
|
+
NVD[textSecondaryDarkTone],
|
|
271
314
|
),
|
|
272
|
-
'--color-text-disabled': ld(
|
|
315
|
+
'--color-text-disabled': ld(NVL[60], NVD[40]),
|
|
273
316
|
'--color-text-accent': 'var(--color-accent)',
|
|
274
317
|
|
|
275
318
|
// Icon
|
|
276
319
|
'--color-icon-accent': 'var(--color-accent)',
|
|
277
|
-
'--color-icon-primary': ld(
|
|
320
|
+
'--color-icon-primary': ld(
|
|
321
|
+
NL[textPrimaryLightTone],
|
|
322
|
+
ND[textPrimaryDarkTone],
|
|
323
|
+
),
|
|
278
324
|
'--color-icon-secondary': ld(
|
|
279
|
-
|
|
280
|
-
|
|
325
|
+
NVL[textSecondaryLightTone],
|
|
326
|
+
NVD[textSecondaryDarkTone],
|
|
281
327
|
),
|
|
282
|
-
'--color-icon-disabled': ld(
|
|
328
|
+
'--color-icon-disabled': ld(NVL[60], NVD[40]),
|
|
283
329
|
|
|
284
330
|
// Surface variants
|
|
285
|
-
'--color-background-card': ld(
|
|
286
|
-
'--color-background-popover': ld(
|
|
287
|
-
'--color-background-inverted': ld(
|
|
331
|
+
'--color-background-card': ld(NL[99], ND[10]),
|
|
332
|
+
'--color-background-popover': ld(NL[99], ND[20]),
|
|
333
|
+
'--color-background-inverted': ld(NL[10], ND[99]),
|
|
288
334
|
|
|
289
|
-
// Border
|
|
290
335
|
// Border
|
|
291
336
|
// Decorative hairline — not a WCAG 1.4.11 boundary. High contrast
|
|
292
337
|
// doubles the alpha so structural boundaries stay perceivable.
|
|
293
338
|
'--color-border': ld(
|
|
294
|
-
hexWithAlpha(
|
|
295
|
-
hexWithAlpha(
|
|
339
|
+
hexWithAlpha(NL[10], borderSubtleAlpha),
|
|
340
|
+
hexWithAlpha(ND[95], borderSubtleAlpha),
|
|
296
341
|
),
|
|
297
342
|
'--color-border-emphasized': borderEmphasized,
|
|
298
343
|
|
|
299
344
|
// Effects
|
|
300
|
-
'--color-skeleton': ld(
|
|
345
|
+
'--color-skeleton': ld(NVL[70], NVD[30]),
|
|
301
346
|
// Channel-on-body surface (ProgressBar/Slider tracks, Switch off-state).
|
|
302
347
|
// Defaults to the same NV[70]/NV[30] ramp stop as --color-skeleton.
|
|
303
|
-
'--color-track': ld(
|
|
304
|
-
'--color-shadow': ld(hexWithAlpha(
|
|
348
|
+
'--color-track': ld(NVL[70], NVD[30]),
|
|
349
|
+
'--color-shadow': ld(hexWithAlpha(NL[0], 0.1), hexWithAlpha(ND[0], 0.3)),
|
|
305
350
|
'--color-tint-hover': ld('black', 'white'),
|
|
306
351
|
};
|
|
307
352
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, expect, it} from 'vitest';
|
|
4
|
+
import {isImeKeyEvent} from './ime';
|
|
5
|
+
|
|
6
|
+
describe('isImeKeyEvent', () => {
|
|
7
|
+
it('detects the modern isComposing signal', () => {
|
|
8
|
+
expect(isImeKeyEvent({isComposing: true})).toBe(true);
|
|
9
|
+
expect(isImeKeyEvent({isComposing: true, keyCode: 13})).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('detects the legacy keyCode 229 fallback (IME processing sentinel)', () => {
|
|
13
|
+
// Some IMEs / older Safari fire the composing keydown with isComposing
|
|
14
|
+
// not yet set to true but report keyCode 229.
|
|
15
|
+
expect(isImeKeyEvent({keyCode: 229})).toBe(true);
|
|
16
|
+
expect(isImeKeyEvent({isComposing: false, keyCode: 229})).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns false for ordinary (non-composing) keydowns', () => {
|
|
20
|
+
expect(isImeKeyEvent({})).toBe(false);
|
|
21
|
+
expect(isImeKeyEvent({isComposing: false})).toBe(false);
|
|
22
|
+
expect(isImeKeyEvent({keyCode: 13})).toBe(false); // plain Enter
|
|
23
|
+
expect(isImeKeyEvent({keyCode: 27})).toBe(false); // plain Escape
|
|
24
|
+
expect(isImeKeyEvent({isComposing: false, keyCode: 13})).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('only treats a literal `true` isComposing as composing', () => {
|
|
28
|
+
// Guards against truthy-but-not-true values leaking through.
|
|
29
|
+
// @ts-expect-error intentionally passing a non-boolean to assert strictness
|
|
30
|
+
expect(isImeKeyEvent({isComposing: 1})).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/utils/ime.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file ime.ts
|
|
5
|
+
* @input Receives a keyboard event (React SyntheticEvent's `nativeEvent`, or a
|
|
6
|
+
* native DOM KeyboardEvent) with optional `isComposing` / `keyCode`.
|
|
7
|
+
* @output Exports `isImeKeyEvent`, the canonical predicate for "this keydown is
|
|
8
|
+
* part of an in-progress IME composition and must not be treated as a command".
|
|
9
|
+
* @position Shared low-level utility; consumed by every editable/overlay surface
|
|
10
|
+
* that handles Enter/Escape/arrows in `onKeyDown` (Typeahead, PowerSearch,
|
|
11
|
+
* Dialog, ContextMenu, Tooltip, Chat composer, focus-trap Escape, ...).
|
|
12
|
+
*
|
|
13
|
+
* ── Why this exists (read once, here — do not re-explain at call sites) ──
|
|
14
|
+
*
|
|
15
|
+
* When a CJK user (Korean / Japanese / Chinese) is composing text via an IME,
|
|
16
|
+
* the browser fires a `keydown` event to COMMIT or CANCEL the pending
|
|
17
|
+
* composition (Enter commits the highlighted candidate; Escape cancels it;
|
|
18
|
+
* ArrowUp/Down/Home/End navigate the candidate window). Crucially this
|
|
19
|
+
* `keydown` fires BEFORE the `compositionend` event that actually writes the
|
|
20
|
+
* committed text into the field. So a naive `onKeyDown` handler sees a bare
|
|
21
|
+
* "Enter" or "Escape" and misreads the composition-commit/cancel as an
|
|
22
|
+
* application command — accepting a typeahead suggestion, submitting a chat
|
|
23
|
+
* message, closing a dialog/menu/tooltip, or saving a filter — mid-composition.
|
|
24
|
+
* The fix is to detect the composing keydown and early-return before running
|
|
25
|
+
* any command logic.
|
|
26
|
+
*
|
|
27
|
+
* Two signals, both needed:
|
|
28
|
+
*
|
|
29
|
+
* 1. `event.isComposing === true` — the modern, spec'd signal
|
|
30
|
+
* (https://www.w3.org/TR/uievents/#dom-keyboardevent-iscomposing). This is
|
|
31
|
+
* the primary check and is reliable in current evergreen browsers.
|
|
32
|
+
*
|
|
33
|
+
* 2. `event.keyCode === 229` — the legacy fallback. `229` is the sentinel
|
|
34
|
+
* keyCode browsers report for "the key event is being processed by an IME".
|
|
35
|
+
* It is still load-bearing: some IMEs and older Safari fire the composing
|
|
36
|
+
* `keydown` with `isComposing` NOT yet set to `true`, but DO report
|
|
37
|
+
* keyCode 229. Keeping both makes the guard robust across the browser
|
|
38
|
+
* matrix we support. Do not drop the 229 fallback without a browser-matrix
|
|
39
|
+
* audit.
|
|
40
|
+
*
|
|
41
|
+
* ── Which event object to pass ──
|
|
42
|
+
*
|
|
43
|
+
* React's SyntheticEvent for KeyboardEvent *does* surface `isComposing`, but to
|
|
44
|
+
* avoid any cross-browser normalization gap prefer passing the *native* event
|
|
45
|
+
* (`e.nativeEvent`) from React handlers — that is what BaseTypeahead,
|
|
46
|
+
* PowerSearch, and the Chat composer do. Native DOM listeners (Dialog,
|
|
47
|
+
* ContextMenu, Tooltip, focus-trap) pass the DOM `KeyboardEvent` directly.
|
|
48
|
+
* The parameter is intentionally structurally typed so both shapes are accepted.
|
|
49
|
+
*
|
|
50
|
+
* Note: `keyCode` is deprecated on the DOM `KeyboardEvent` type but is still
|
|
51
|
+
* present at runtime; we read it defensively via the optional structural field.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The sentinel `keyCode` browsers report while a key event is being processed
|
|
56
|
+
* by an IME (the composing keydown that fires before `compositionend`). See the
|
|
57
|
+
* file header for why this legacy signal is still load-bearing alongside
|
|
58
|
+
* `isComposing`.
|
|
59
|
+
*/
|
|
60
|
+
const IME_PROCESSING_KEY_CODE = 229;
|
|
61
|
+
|
|
62
|
+
export function isImeKeyEvent(event: {
|
|
63
|
+
isComposing?: boolean;
|
|
64
|
+
keyCode?: number;
|
|
65
|
+
}): boolean {
|
|
66
|
+
return (
|
|
67
|
+
event.isComposing === true || event.keyCode === IME_PROCESSING_KEY_CODE
|
|
68
|
+
);
|
|
69
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -107,3 +107,5 @@ export {rtlStyles} from './rtlStyles';
|
|
|
107
107
|
// The shared focus ring, exported for @astryxdesign/lab — same reason
|
|
108
108
|
// rtlStyles is: a style that must be identical across packages, not copied.
|
|
109
109
|
export {focusOutlineStyles, focusOutlineProps} from './focusOutline.stylex';
|
|
110
|
+
|
|
111
|
+
export {isImeKeyEvent} from './ime';
|