@eifi1/ui-kit 0.35.0 → 0.37.0
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/README.md +35 -5
- package/dist/admin/role-select.d.ts +3 -2
- package/dist/admin/role-select.js +2 -0
- package/dist/admin/role-select.js.map +1 -1
- package/dist/components/amount-input.d.ts +2 -2
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +2 -2
- package/dist/components/autocomplete.js +2 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-look.d.ts +1 -1
- package/dist/components/button-look.js +9 -4
- package/dist/components/button-look.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +5 -2
- package/dist/components/checkbox-group.js +15 -4
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/column-mapper.d.ts +5 -3
- package/dist/components/column-mapper.js +2 -0
- package/dist/components/column-mapper.js.map +1 -1
- package/dist/components/combobox.d.ts +2 -2
- package/dist/components/combobox.js +2 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/country-select.d.ts +3 -3
- package/dist/components/country-select.js +3 -0
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/currency-select.d.ts +3 -2
- package/dist/components/currency-select.js +10 -1
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/date-field.d.ts +2 -1
- package/dist/components/date-field.js.map +1 -1
- package/dist/components/date-picker.js +3 -0
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/date-range-picker.js +3 -0
- package/dist/components/date-range-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +2 -2
- package/dist/components/entity-combobox.js +2 -1
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/field-parts.d.ts +66 -11
- package/dist/components/field-parts.js +32 -10
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/field-strip.d.ts +3 -2
- package/dist/components/field-strip.js +2 -1
- package/dist/components/field-strip.js.map +1 -1
- package/dist/components/field.d.ts +3 -2
- package/dist/components/field.js +24 -15
- package/dist/components/field.js.map +1 -1
- package/dist/components/help-popover.d.ts +9 -7
- package/dist/components/help-popover.js.map +1 -1
- package/dist/components/icon-picker.d.ts +3 -2
- package/dist/components/icon-picker.js +3 -0
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/inline-entity-combobox.d.ts +2 -2
- package/dist/components/inline-entity-combobox.js +2 -1
- package/dist/components/inline-entity-combobox.js.map +1 -1
- package/dist/components/input.d.ts +2 -2
- package/dist/components/input.js +2 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/month-picker.d.ts +2 -1
- package/dist/components/month-picker.js +8 -1
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +2 -2
- package/dist/components/multi-entity-combobox.js +2 -1
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +2 -2
- package/dist/components/multi-select.js +3 -0
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-input.d.ts +2 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numeric-field.d.ts +5 -2
- package/dist/components/numeric-field.js +4 -1
- package/dist/components/numeric-field.js.map +1 -1
- package/dist/components/one-time-code-input.d.ts +2 -1
- package/dist/components/one-time-code-input.js +2 -1
- package/dist/components/one-time-code-input.js.map +1 -1
- package/dist/components/phone-input.d.ts +1 -1
- package/dist/components/phone-input.js +4 -0
- package/dist/components/phone-input.js.map +1 -1
- package/dist/components/popover.js +1 -1
- package/dist/components/popover.js.map +1 -1
- package/dist/components/select.d.ts +4 -3
- package/dist/components/select.js +2 -1
- package/dist/components/select.js.map +1 -1
- package/dist/components/slider.d.ts +3 -2
- package/dist/components/slider.js +7 -1
- package/dist/components/slider.js.map +1 -1
- package/dist/components/stat-tile.js +2 -11
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -2
- package/dist/components/swatch-picker.js +3 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/text-link.js +3 -1
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/textarea.d.ts +2 -1
- package/dist/components/textarea.js +9 -3
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/toggle-group.d.ts +2 -2
- package/dist/components/toggle-group.js +7 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/eslint/role-utilities.d.ts +34 -2
- package/dist/eslint/role-utilities.js +30 -9
- package/dist/eslint/role-utilities.js.map +1 -1
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +97 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +16 -0
- package/dist/i18n/kit-labels.js +21 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +103 -2
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +95 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +96 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +95 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +96 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +95 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +13 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/i18n/shared-labels.js +3 -1
- package/dist/i18n/shared-labels.js.map +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/device-settings.d.ts +72 -0
- package/dist/lib/device-settings.js +64 -0
- package/dist/lib/device-settings.js.map +1 -0
- package/dist/lib/plain-text.d.ts +11 -0
- package/dist/lib/plain-text.js +16 -0
- package/dist/lib/plain-text.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +6 -0
- package/dist/lib/safe-storage.js +11 -0
- package/dist/lib/safe-storage.js.map +1 -1
- package/dist/notifications/notification-parts.d.ts +39 -0
- package/dist/notifications/notification-parts.js +47 -0
- package/dist/notifications/notification-parts.js.map +1 -0
- package/dist/notifications/notification-types-setting.d.ts +62 -0
- package/dist/notifications/notification-types-setting.js +192 -0
- package/dist/notifications/notification-types-setting.js.map +1 -0
- package/dist/notifications/notification-types.d.ts +75 -0
- package/dist/notifications/notification-types.js +2 -0
- package/dist/notifications/notification-types.js.map +1 -0
- package/dist/notifications/notifications-labels.d.ts +131 -0
- package/dist/notifications/notifications-labels.js +83 -0
- package/dist/notifications/notifications-labels.js.map +1 -0
- package/dist/notifications/push-device-setting.d.ts +37 -0
- package/dist/notifications/push-device-setting.js +164 -0
- package/dist/notifications/push-device-setting.js.map +1 -0
- package/dist/notifications/push-devices-setting.d.ts +44 -0
- package/dist/notifications/push-devices-setting.js +203 -0
- package/dist/notifications/push-devices-setting.js.map +1 -0
- package/dist/notifications/quiet-hours-setting.d.ts +47 -0
- package/dist/notifications/quiet-hours-setting.js +131 -0
- package/dist/notifications/quiet-hours-setting.js.map +1 -0
- package/dist/notifications/time-zone-select.d.ts +50 -0
- package/dist/notifications/time-zone-select.js +76 -0
- package/dist/notifications/time-zone-select.js.map +1 -0
- package/dist/notifications/unsubscribe-panel.d.ts +53 -0
- package/dist/notifications/unsubscribe-panel.js +139 -0
- package/dist/notifications/unsubscribe-panel.js.map +1 -0
- package/dist/notifications/use-push-device.d.ts +122 -0
- package/dist/notifications/use-push-device.js +230 -0
- package/dist/notifications/use-push-device.js.map +1 -0
- package/dist/rhf/fields.d.ts +26 -19
- package/dist/rhf/fields.js +51 -22
- package/dist/rhf/fields.js.map +1 -1
- package/dist/settings/device-reset-setting-labels.d.ts +3 -0
- package/dist/settings/device-reset-setting-labels.js +15 -0
- package/dist/settings/device-reset-setting-labels.js.map +1 -0
- package/dist/settings/device-reset-setting.d.ts +58 -0
- package/dist/settings/device-reset-setting.js +51 -0
- package/dist/settings/device-reset-setting.js.map +1 -0
- package/dist/shell/app-shell.js +5 -0
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/theme/contrast-tokens.d.ts +42 -4
- package/dist/theme/contrast-tokens.js +56 -9
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +4 -2
- package/dist/theme/contrast.js +13 -0
- package/dist/theme/contrast.js.map +1 -1
- package/dist/theme/palette-derive.d.ts +15 -1
- package/dist/theme/palette-derive.js +37 -8
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.d.ts +2 -1
- package/dist/theme/palette-presets.js +5 -5
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/theme/palette-store.js +6 -0
- package/dist/theme/palette-store.js.map +1 -1
- package/dist/theme/text-size.js +6 -0
- package/dist/theme/text-size.js.map +1 -1
- package/dist/theme/theme-store.js +6 -0
- package/dist/theme/theme-store.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/role-select.tsx +4 -1
- package/src/components/amount-input.tsx +2 -1
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-look.ts +15 -5
- package/src/components/checkbox-group.tsx +22 -4
- package/src/components/column-mapper.tsx +6 -2
- package/src/components/combobox.tsx +5 -2
- package/src/components/country-select.tsx +6 -1
- package/src/components/currency-select.tsx +12 -3
- package/src/components/date-field.tsx +2 -2
- package/src/components/date-picker.tsx +3 -0
- package/src/components/date-range-picker.tsx +3 -0
- package/src/components/entity-combobox.tsx +5 -2
- package/src/components/field-parts.tsx +107 -19
- package/src/components/field-strip.tsx +4 -3
- package/src/components/field.tsx +28 -15
- package/src/components/help-popover.tsx +9 -7
- package/src/components/icon-picker.tsx +8 -3
- package/src/components/inline-entity-combobox.tsx +5 -2
- package/src/components/input.tsx +4 -2
- package/src/components/month-picker.tsx +10 -3
- package/src/components/multi-entity-combobox.tsx +5 -2
- package/src/components/multi-select.tsx +6 -1
- package/src/components/number-input.tsx +2 -2
- package/src/components/numeric-field.ts +10 -2
- package/src/components/one-time-code-input.tsx +5 -3
- package/src/components/phone-input.tsx +4 -0
- package/src/components/popover.tsx +3 -1
- package/src/components/select.tsx +6 -3
- package/src/components/slider.tsx +10 -3
- package/src/components/stat-tile.tsx +3 -18
- package/src/components/swatch-picker.tsx +9 -4
- package/src/components/text-link.tsx +3 -1
- package/src/components/textarea.tsx +11 -4
- package/src/components/toggle-group.tsx +21 -5
- package/src/eslint/role-utilities.ts +67 -11
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/german.ts +113 -0
- package/src/i18n/kit-labels.tsx +52 -1
- package/src/i18n/locales/en.ts +6 -0
- package/src/i18n/locales/es.ts +106 -0
- package/src/i18n/locales/fr.ts +106 -0
- package/src/i18n/locales/hu.ts +104 -0
- package/src/i18n/locales/it.ts +105 -0
- package/src/i18n/locales/zh.ts +96 -0
- package/src/i18n/review.ts +13 -0
- package/src/i18n/shared-labels.ts +2 -0
- package/src/index.ts +50 -0
- package/src/lib/device-settings.ts +146 -0
- package/src/lib/plain-text.ts +22 -0
- package/src/lib/safe-storage.ts +16 -0
- package/src/notifications/notification-parts.tsx +83 -0
- package/src/notifications/notification-types-setting.tsx +309 -0
- package/src/notifications/notification-types.ts +80 -0
- package/src/notifications/notifications-labels.ts +238 -0
- package/src/notifications/push-device-setting.tsx +224 -0
- package/src/notifications/push-devices-setting.tsx +306 -0
- package/src/notifications/quiet-hours-setting.tsx +185 -0
- package/src/notifications/time-zone-select.tsx +126 -0
- package/src/notifications/unsubscribe-panel.tsx +203 -0
- package/src/notifications/use-push-device.ts +392 -0
- package/src/rhf/fields.tsx +77 -29
- package/src/settings/device-reset-setting-labels.ts +20 -0
- package/src/settings/device-reset-setting.tsx +119 -0
- package/src/shell/app-shell.tsx +8 -0
- package/src/theme/contrast-tokens.ts +106 -12
- package/src/theme/contrast.ts +30 -4
- package/src/theme/palette-derive.ts +68 -7
- package/src/theme/palette-presets.ts +9 -6
- package/src/theme/palette-store.ts +12 -0
- package/src/theme/text-size.ts +8 -0
- package/src/theme/theme-store.ts +10 -0
- package/tokens.css +38 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { contrast, oklchToRgb, parseHex, rgbToOklch, toHex } from "./color";
|
|
1
|
+
import { contrast, hexToOklch, luminance, oklchToHex, oklchToRgb, parseHex, rgbToOklch, solveLightness, toHex } from "./color";
|
|
2
2
|
import type { TokenSet } from "./palette-presets";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -20,17 +20,48 @@ import type { TokenSet } from "./palette-presets";
|
|
|
20
20
|
* - `--border` moves toward `--text-primary` until it clears 3:1 against the worst of the
|
|
21
21
|
* three surfaces (WCAG 1.4.11, a field's boundary), and never less far than
|
|
22
22
|
* `--border-strong`'s 45 % — so every border is at least as strong as a "strong" one was.
|
|
23
|
-
* `--border-strong` stays derived in tokens.css and grows with it
|
|
23
|
+
* `--border-strong` stays derived in tokens.css and grows with it;
|
|
24
|
+
* - in a LIGHT set only (0.36, docs/colour-roles-harmonization.md §13): the well
|
|
25
|
+
* (`--bg-surface-2`) becomes {@link WELL_INK_MORE} of `--text-primary` mixed into the
|
|
26
|
+
* card, and each of the four money colours that falls under 4.5:1 on that well is
|
|
27
|
+
* solved darker at its own hue and chroma. The border above is solved against the
|
|
28
|
+
* stepped well. A dark set's well stands 1.11–1.13:1 from its card already, and its
|
|
29
|
+
* money has no room: nothing of it steps.
|
|
30
|
+
*
|
|
31
|
+
* What does NOT move here, because it is not inline: the light status, hue and field-sync
|
|
32
|
+
* literals and `--bg-active` step in tokens.css's `:root[data-contrast="more"]:not(.dark)`
|
|
33
|
+
* block, solved over these stepped wells.
|
|
24
34
|
*/
|
|
25
35
|
|
|
26
|
-
/** The custom properties a contrast step writes. */
|
|
27
|
-
export const CONTRAST_STEPPED_VARS = [
|
|
36
|
+
/** The custom properties a contrast step writes. The last five only for a light set. */
|
|
37
|
+
export const CONTRAST_STEPPED_VARS = [
|
|
38
|
+
"--text-secondary",
|
|
39
|
+
"--text-muted",
|
|
40
|
+
"--text-placeholder",
|
|
41
|
+
"--border",
|
|
42
|
+
"--bg-surface-2",
|
|
43
|
+
"--money-income",
|
|
44
|
+
"--money-expense",
|
|
45
|
+
"--money-net",
|
|
46
|
+
"--money-neutral",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/** The light well under More contrast and the money that reads on it (§13.4). */
|
|
50
|
+
export interface WellStep {
|
|
51
|
+
bgSurface2: string;
|
|
52
|
+
moneyIncome: string;
|
|
53
|
+
moneyExpense: string;
|
|
54
|
+
moneyNet: string;
|
|
55
|
+
moneyNeutral: string;
|
|
56
|
+
}
|
|
28
57
|
|
|
29
58
|
export interface ContrastStep {
|
|
30
59
|
textSecondary: string;
|
|
31
60
|
textMuted: string;
|
|
32
61
|
textPlaceholder: string;
|
|
33
62
|
border: string;
|
|
63
|
+
/** The stepped well and money, or null for a dark set (which does not step them). */
|
|
64
|
+
well: WellStep | null;
|
|
34
65
|
}
|
|
35
66
|
|
|
36
67
|
interface Lab {
|
|
@@ -82,6 +113,63 @@ export function hoverFill(t: Pick<TokenSet, "textPrimary" | "bgSurface">): strin
|
|
|
82
113
|
return mix(t.textPrimary, t.bgSurface, HOVER_INK);
|
|
83
114
|
}
|
|
84
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The light well under More contrast: this share of `--text-primary` mixed into the card,
|
|
118
|
+
* `--bg-hover`'s formula at a larger share (0.36, docs/colour-roles-harmonization.md §13).
|
|
119
|
+
*
|
|
120
|
+
* In light the well is the page (§5.3), 1.09–1.11:1 from the card. At 10 % it stands
|
|
121
|
+
* 1.19–1.24:1 from it, about twice that. The rule it is held to: **the stepped well stands
|
|
122
|
+
* at least 1.17:1 from the card**, two of the hover rule's 1.08 steps; 9 % is the least
|
|
123
|
+
* that clears it in every shipped preset, with no room. The step cannot ship alone: on a
|
|
124
|
+
* 10 % well money, status and two hues read 4.07–4.28:1, so the money is solved with it
|
|
125
|
+
* here and the status and hue literals in tokens.css.
|
|
126
|
+
*/
|
|
127
|
+
export const WELL_INK_MORE = 0.1;
|
|
128
|
+
|
|
129
|
+
/** The least the stepped light well stands from the card (§13.3, decision 2). */
|
|
130
|
+
export const WELL_STEP_MIN = 1.17;
|
|
131
|
+
|
|
132
|
+
/** True when `t` is a light set: its body ink darker than its card. */
|
|
133
|
+
function isLightSet(t: Pick<TokenSet, "textPrimary" | "bgSurface">): boolean | null {
|
|
134
|
+
const ink = parseHex(t.textPrimary);
|
|
135
|
+
const card = parseHex(t.bgSurface);
|
|
136
|
+
if (!ink || !card) return null;
|
|
137
|
+
return luminance(ink) < luminance(card);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Money as text: WCAG 1.4.3 AA. */
|
|
141
|
+
const MONEY_TARGET = 4.5;
|
|
142
|
+
|
|
143
|
+
/** `color` as it is when it clears 4.5:1 on `well`, else solved darker at its own hue and
|
|
144
|
+
* chroma until it does — measured on the hex (`solveLightness`). */
|
|
145
|
+
function moneyOn(color: string, well: string): string {
|
|
146
|
+
if (contrast(color, well) >= MONEY_TARGET) return color;
|
|
147
|
+
const lch = hexToOklch(color);
|
|
148
|
+
const bg = parseHex(well);
|
|
149
|
+
if (!lch || !bg) return color;
|
|
150
|
+
const l = solveLightness(MONEY_TARGET, bg, lch.h, lch.c, "darker");
|
|
151
|
+
return oklchToHex({ l, c: lch.c, h: lch.h });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The light well under More contrast for `t`, or null for a dark set (or one whose ink or
|
|
155
|
+
* card is not hex, which cannot be mixed). The deriver solves the brand against it. */
|
|
156
|
+
export function steppedWellFill(t: Pick<TokenSet, "textPrimary" | "bgSurface">): string | null {
|
|
157
|
+
return isLightSet(t) ? mix(t.textPrimary, t.bgSurface, WELL_INK_MORE) : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The stepped well and money of a light set, or null (see {@link steppedWellFill}). */
|
|
161
|
+
function wellStep(t: TokenSet): WellStep | null {
|
|
162
|
+
const well = steppedWellFill(t);
|
|
163
|
+
if (!well) return null;
|
|
164
|
+
return {
|
|
165
|
+
bgSurface2: well,
|
|
166
|
+
moneyIncome: moneyOn(t.moneyIncome, well),
|
|
167
|
+
moneyExpense: moneyOn(t.moneyExpense, well),
|
|
168
|
+
moneyNet: moneyOn(t.moneyNet, well),
|
|
169
|
+
moneyNeutral: moneyOn(t.moneyNeutral, well),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
85
173
|
/**
|
|
86
174
|
* `--brand-muted` in tokens.css: this share of `--brand` mixed into `--text-primary`.
|
|
87
175
|
*
|
|
@@ -118,32 +206,38 @@ const BORDER_TARGET = 3;
|
|
|
118
206
|
/** `--border-strong` in tokens.css: 55 % border, 45 % text. */
|
|
119
207
|
const BORDER_STRONG_SHARE = 0.45;
|
|
120
208
|
|
|
121
|
-
|
|
122
|
-
|
|
209
|
+
/** The worst of the three surfaces; `well`, when the step has one, in place of the set's. */
|
|
210
|
+
function worstContrast(color: string, t: TokenSet, well: string): number {
|
|
211
|
+
return Math.min(contrast(color, t.bgPage), contrast(color, t.bgSurface), contrast(color, well));
|
|
123
212
|
}
|
|
124
213
|
|
|
125
|
-
function steppedBorder(t: TokenSet): string {
|
|
214
|
+
function steppedBorder(t: TokenSet, well: WellStep | null): string {
|
|
126
215
|
// The share of `--text-primary` in the border: at least --border-strong's, more until
|
|
127
216
|
// 3:1. Monotonic in the share (the mix walks from the border to the text, away from
|
|
128
|
-
// every surface), so a bisection finds the least that clears it.
|
|
217
|
+
// every surface), so a bisection finds the least that clears it. Against the stepped
|
|
218
|
+
// light well (§13.4), which is the darkest surface under More contrast.
|
|
219
|
+
const surface2 = well?.bgSurface2 ?? t.bgSurface2;
|
|
129
220
|
let lo = BORDER_STRONG_SHARE;
|
|
130
|
-
if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t) >= BORDER_TARGET) return mix(t.border, t.textPrimary, 1 - lo);
|
|
221
|
+
if (worstContrast(mix(t.border, t.textPrimary, 1 - lo), t, surface2) >= BORDER_TARGET) return mix(t.border, t.textPrimary, 1 - lo);
|
|
131
222
|
let hi = 1;
|
|
132
223
|
for (let i = 0; i < 16; i++) {
|
|
133
224
|
const mid = (lo + hi) / 2;
|
|
134
|
-
if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t) >= BORDER_TARGET) hi = mid;
|
|
225
|
+
if (worstContrast(mix(t.border, t.textPrimary, 1 - mid), t, surface2) >= BORDER_TARGET) hi = mid;
|
|
135
226
|
else lo = mid;
|
|
136
227
|
}
|
|
137
228
|
return mix(t.border, t.textPrimary, 1 - hi);
|
|
138
229
|
}
|
|
139
230
|
|
|
140
|
-
/** The
|
|
231
|
+
/** The stepped values for `t`: the four text and line tokens, and for a light set the
|
|
232
|
+
* well and money (`well`, null in dark). Pure. */
|
|
141
233
|
export function contrastStep(t: TokenSet): ContrastStep {
|
|
234
|
+
const well = wellStep(t);
|
|
142
235
|
return {
|
|
143
236
|
textSecondary: mix(t.textSecondary, t.textPrimary, 0.5),
|
|
144
237
|
textMuted: t.textSecondary,
|
|
145
238
|
textPlaceholder: t.textMuted,
|
|
146
|
-
border: steppedBorder(t),
|
|
239
|
+
border: steppedBorder(t, well),
|
|
240
|
+
well,
|
|
147
241
|
};
|
|
148
242
|
}
|
|
149
243
|
|
package/src/theme/contrast.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
4
|
import { logger } from "../lib/logger";
|
|
5
5
|
import { readStored } from "../lib/safe-storage";
|
|
6
|
+
import { registerDeviceSetting } from "../lib/device-settings";
|
|
6
7
|
import { useMediaQuery } from "../hooks/use-media-query";
|
|
7
8
|
import { CONTRAST_STEPPED_VARS, contrastStep, rememberedTokenSet } from "./contrast-tokens";
|
|
8
9
|
import { applyTokenSet, DEFAULT_PRESET } from "./palette-presets";
|
|
@@ -16,8 +17,10 @@ import type { PersistedAppearanceOptions } from "./text-size";
|
|
|
16
17
|
* In force it is `<html data-contrast="more">` (else `"standard"`), which does two things:
|
|
17
18
|
* - the inline colour tokens are re-written one step stronger (`applyTokenSet`'s
|
|
18
19
|
* contrast flag — see contrast-tokens.ts for what moves and why it is JS);
|
|
19
|
-
* - tokens.css raises `--focus-ring-width` from 2 to 3 px
|
|
20
|
-
*
|
|
20
|
+
* - tokens.css raises `--focus-ring-width` from 2 to 3 px, in light deepens the status,
|
|
21
|
+
* hue and field-sync literals and `--bg-active` to read on the stepped well (0.36,
|
|
22
|
+
* colour-roles §13), and turns on the `contrast:` variant for anything else a
|
|
23
|
+
* component wants to change.
|
|
21
24
|
*
|
|
22
25
|
* The names below are frozen like the text size's: server-kit's `CONTRAST_MODES`, the
|
|
23
26
|
* stored format, the attribute.
|
|
@@ -75,8 +78,15 @@ export function readContrastLevel(): ContrastLevel {
|
|
|
75
78
|
*
|
|
76
79
|
* With a palette layer (`applyTokenSet` has written a set here), that set is applied
|
|
77
80
|
* again and steps itself. Without one — an app whose base tokens come from tokens.css,
|
|
78
|
-
* like Kurvenschmiede's — the
|
|
79
|
-
*
|
|
81
|
+
* like Kurvenschmiede's — the stepped tokens are written over the stylesheet for the
|
|
82
|
+
* default preset in the current light/dark, or removed again: the four text and line
|
|
83
|
+
* tokens in both modes, and in light the well and the four money colours too (0.36,
|
|
84
|
+
* docs/colour-roles-harmonization.md §13.3 decision 10). In dark those five stay removed,
|
|
85
|
+
* so the stylesheet's dark well and money paint.
|
|
86
|
+
*
|
|
87
|
+
* It never calls `applyTokenSet` here: that would remember a token set on `<html>`, and
|
|
88
|
+
* this app would then take the palette branch above, which a theme switch does not re-run
|
|
89
|
+
* (the class observer in `useApplyContrast` re-writes only while nothing is remembered).
|
|
80
90
|
*/
|
|
81
91
|
function reapplyContrastTokens(el: HTMLElement): void {
|
|
82
92
|
const written = rememberedTokenSet(el);
|
|
@@ -91,6 +101,14 @@ function reapplyContrastTokens(el: HTMLElement): void {
|
|
|
91
101
|
el.style.setProperty("--text-muted", step.textMuted);
|
|
92
102
|
el.style.setProperty("--text-placeholder", step.textPlaceholder);
|
|
93
103
|
el.style.setProperty("--border", step.border);
|
|
104
|
+
// One line per variable, not a loop: the step's fields are not the variables' names.
|
|
105
|
+
if (step.well) {
|
|
106
|
+
el.style.setProperty("--bg-surface-2", step.well.bgSurface2);
|
|
107
|
+
el.style.setProperty("--money-income", step.well.moneyIncome);
|
|
108
|
+
el.style.setProperty("--money-expense", step.well.moneyExpense);
|
|
109
|
+
el.style.setProperty("--money-net", step.well.moneyNet);
|
|
110
|
+
el.style.setProperty("--money-neutral", step.well.moneyNeutral);
|
|
111
|
+
}
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
/** Put `level` on `<html>` and re-write the colour tokens for it. */
|
|
@@ -197,6 +215,14 @@ export function createContrastStore(storageKey: string) {
|
|
|
197
215
|
"contrast",
|
|
198
216
|
),
|
|
199
217
|
);
|
|
218
|
+
// Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on
|
|
219
|
+
// this device, so `useApplyContrast` re-resolves to the account's mode, else "system",
|
|
220
|
+
// and re-steps the tokens.
|
|
221
|
+
registerDeviceSetting({
|
|
222
|
+
key: storageKey,
|
|
223
|
+
kind: "appearance",
|
|
224
|
+
reset: () => useContrastStore.getState().setContrast(null),
|
|
225
|
+
});
|
|
200
226
|
|
|
201
227
|
function useApplyContrast({ account }: ApplyContrastOptions = {}): ContrastLevel {
|
|
202
228
|
const device = useContrastStore((s) => s.contrast);
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
solveLightness,
|
|
14
14
|
} from "./color";
|
|
15
15
|
import type { CvdType, Oklch } from "./color";
|
|
16
|
-
import {
|
|
16
|
+
import type { ContrastLevel } from "./contrast";
|
|
17
|
+
import { brandSoft, contrastStep, hoverFill, steppedWellFill, WELL_STEP_MIN } from "./contrast-tokens";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Build a whole palette from one brand colour, plus any number of colours pinned by
|
|
@@ -104,6 +105,8 @@ export interface ContrastReport {
|
|
|
104
105
|
export interface DerivedPalette {
|
|
105
106
|
tokens: TokenSet;
|
|
106
107
|
semantic: SemanticTokens;
|
|
108
|
+
/** {@link auditPalette} at both contrast levels: the More-contrast checks carry a
|
|
109
|
+
* " (More contrast)" suffix (0.36). */
|
|
107
110
|
audit: ContrastReport;
|
|
108
111
|
/** Compromises the deriver had to make, in plain words. Never silent. */
|
|
109
112
|
warnings: string[];
|
|
@@ -310,15 +313,24 @@ export function derivePalette({
|
|
|
310
313
|
// ── Brand. Keep the hue the caller asked for; move only the lightness, and only as
|
|
311
314
|
// far as 3:1 against the worst surface — WCAG 1.4.11, since a brand-filled control is
|
|
312
315
|
// a graphical object whose boundary has to be perceivable.
|
|
316
|
+
//
|
|
317
|
+
// In light the worst surface includes More contrast's well (0.36, colour-roles §13.4):
|
|
318
|
+
// 10 % ink into the card, darker than the page. The step is applied, not derived, but
|
|
319
|
+
// the brand is not stepped, so both brand solves measure there too and a derived
|
|
320
|
+
// palette holds at both levels.
|
|
321
|
+
const moreWell = steppedWellFill({ textPrimary, bgSurface: surfaces.bgSurface });
|
|
322
|
+
const brandGround = moreWell
|
|
323
|
+
? [worstSurface, moreWell].reduce((a, b) => (contrast(a, "#000000") < contrast(b, "#000000") ? a : b))
|
|
324
|
+
: worstSurface;
|
|
313
325
|
const brandChroma = Math.max(brand.c, 0.06);
|
|
314
|
-
const brandRatio = contrast(anchors.brand,
|
|
326
|
+
const brandRatio = contrast(anchors.brand, brandGround);
|
|
315
327
|
// NORMALISED, never the caller's raw string. `parseHex` accepts "4f46e5" and "#abc",
|
|
316
328
|
// and passing either through unchanged put a value in the TokenSet that is not a CSS
|
|
317
329
|
// colour — it then fails silently at paint time, three layers from here.
|
|
318
330
|
let brandHex =
|
|
319
331
|
brandRatio >= 3
|
|
320
332
|
? toHex(parseHex(anchors.brand)!)
|
|
321
|
-
: solveAgainst(brand.h, brandChroma, 3,
|
|
333
|
+
: solveAgainst(brand.h, brandChroma, 3, brandGround, mode);
|
|
322
334
|
if (brandRatio < 3) {
|
|
323
335
|
warnings.push(
|
|
324
336
|
`brand ${anchors.brand} sits at ${brandRatio.toFixed(2)}:1 against the ${mode} surfaces, ` +
|
|
@@ -338,6 +350,8 @@ export function derivePalette({
|
|
|
338
350
|
contrast(soft.brandMuted, worstText),
|
|
339
351
|
contrast(soft.brandMuted, soft.brandBg),
|
|
340
352
|
contrast(soft.brandMuted, soft.brandBgHover),
|
|
353
|
+
// …and on More contrast's light well, where a link button reads it (§13.3).
|
|
354
|
+
moreWell ? contrast(soft.brandMuted, moreWell) : Infinity,
|
|
341
355
|
);
|
|
342
356
|
};
|
|
343
357
|
for (let i = 0; i < 40 && brandMutedWorst(brandHex) < 4.5; i++) {
|
|
@@ -525,7 +539,15 @@ export function derivePalette({
|
|
|
525
539
|
heat: deriveHeat(moneyIncome, moneyExpense, surfaces, mode),
|
|
526
540
|
};
|
|
527
541
|
|
|
528
|
-
|
|
542
|
+
// Audited at both contrast levels (0.36, colour-roles §13.5): More contrast steps the
|
|
543
|
+
// text, the border and, in light, the well and money of this set. The derived semantic
|
|
544
|
+
// colours are audited at Standard only: they are solved for the standard grounds, and
|
|
545
|
+
// only tokens.css's shipped literals step for the darker well.
|
|
546
|
+
const standard = auditPalette(tokens, semantic);
|
|
547
|
+
const more = auditPalette(tokens, undefined, { level: "more" });
|
|
548
|
+
const checks = [...standard.checks, ...more.checks];
|
|
549
|
+
const failures = checks.filter((c) => !c.passes);
|
|
550
|
+
return { tokens, semantic, audit: { checks, failures, passes: failures.length === 0 }, warnings };
|
|
529
551
|
}
|
|
530
552
|
|
|
531
553
|
/**
|
|
@@ -560,8 +582,40 @@ function deriveHeat(
|
|
|
560
582
|
* This is the part the shipped presets were missing. `derivePalette` runs it on the way
|
|
561
583
|
* out, and the preset test runs it over everything the package ships, so a palette that
|
|
562
584
|
* does not hold up cannot be merged with a comment claiming that it does.
|
|
585
|
+
*
|
|
586
|
+
* **`level: "more"`** (0.36, docs/colour-roles-harmonization.md §13.5) measures the set as
|
|
587
|
+
* More contrast paints it: the stepped secondary, muted and border, and in a light set the
|
|
588
|
+
* stepped well (as `bgSurface2`) and money — the same pairs, named with a " (More
|
|
589
|
+
* contrast)" suffix. Two things change with the level: the border is a field's boundary
|
|
590
|
+
* there, held to WCAG 1.4.11's 3:1 rather than the hairline's 1.1, and a light set's well
|
|
591
|
+
* must stand {@link WELL_STEP_MIN}:1 from the card. Pass `semantic` with tokens.css's More
|
|
592
|
+
* literals for a light set (the status colours step there); the status text is then
|
|
593
|
+
* measured on the stepped well too.
|
|
563
594
|
*/
|
|
564
|
-
export function auditPalette(
|
|
595
|
+
export function auditPalette(
|
|
596
|
+
set: TokenSet,
|
|
597
|
+
semantic?: SemanticTokens,
|
|
598
|
+
options: { level?: ContrastLevel } = {},
|
|
599
|
+
): ContrastReport {
|
|
600
|
+
const more = options.level === "more";
|
|
601
|
+
const step = more ? contrastStep(set) : null;
|
|
602
|
+
// The set as it paints at this level: what `applyTokenSet` writes for it.
|
|
603
|
+
const t: TokenSet = step
|
|
604
|
+
? {
|
|
605
|
+
...set,
|
|
606
|
+
textSecondary: step.textSecondary,
|
|
607
|
+
textMuted: step.textMuted,
|
|
608
|
+
border: step.border,
|
|
609
|
+
...(step.well && {
|
|
610
|
+
bgSurface2: step.well.bgSurface2,
|
|
611
|
+
moneyIncome: step.well.moneyIncome,
|
|
612
|
+
moneyExpense: step.well.moneyExpense,
|
|
613
|
+
moneyNet: step.well.moneyNet,
|
|
614
|
+
moneyNeutral: step.well.moneyNeutral,
|
|
615
|
+
}),
|
|
616
|
+
}
|
|
617
|
+
: set;
|
|
618
|
+
const suffix = more ? " (More contrast)" : "";
|
|
565
619
|
const checks: ContrastCheck[] = [];
|
|
566
620
|
const surfaces: Array<[string, string]> = [
|
|
567
621
|
["bgPage", t.bgPage],
|
|
@@ -585,12 +639,17 @@ export function auditPalette(t: TokenSet, semantic?: SemanticTokens): ContrastRe
|
|
|
585
639
|
const add = (pair: string, fg: string, bg: string, required: number, rule: string) => {
|
|
586
640
|
const ratio = contrast(fg, bg);
|
|
587
641
|
const slack = MIXED.test(pair) ? 0.05 : 0;
|
|
588
|
-
checks.push({ pair, ratio, required, rule, passes: ratio >= required - slack });
|
|
642
|
+
checks.push({ pair: pair + suffix, ratio, required, rule, passes: ratio >= required - slack });
|
|
589
643
|
};
|
|
590
644
|
|
|
591
645
|
for (const [name, bg] of surfaces) {
|
|
592
646
|
add(`brand on ${name}`, t.brand, bg, 3, "WCAG 1.4.11 graphical object");
|
|
593
|
-
add(`border on ${name}`, t.border, bg,
|
|
647
|
+
if (more) add(`border on ${name}`, t.border, bg, 3, "WCAG 1.4.11 — under More contrast a border bounds a field");
|
|
648
|
+
else add(`border on ${name}`, t.border, bg, 1.1, "house rule: a hairline must be findable");
|
|
649
|
+
}
|
|
650
|
+
// The stepped light well, a hex `applyTokenSet` writes (§13.3, decision 2).
|
|
651
|
+
if (step?.well) {
|
|
652
|
+
add("bgSurface2 on bgSurface", t.bgSurface2, t.bgSurface, WELL_STEP_MIN, "house rule: the well stands two hover steps from the card");
|
|
594
653
|
}
|
|
595
654
|
for (const [name, bg] of textGrounds) {
|
|
596
655
|
add(`textPrimary on ${name}`, t.textPrimary, bg, 7, "WCAG 1.4.6 AAA body text");
|
|
@@ -628,6 +687,8 @@ export function auditPalette(t: TokenSet, semantic?: SemanticTokens): ContrastRe
|
|
|
628
687
|
] as const) {
|
|
629
688
|
add(`${name} on bgSurface`, fg, t.bgSurface, 4.5, "WCAG 1.4.3 — rendered as text");
|
|
630
689
|
add(`${name} on bgHover`, fg, hover, 4.5, "WCAG 1.4.3 — rendered as text, on a hovered row");
|
|
690
|
+
// The darker light well the More literals were solved for (§13.3).
|
|
691
|
+
if (step?.well) add(`${name} on bgSurface2`, fg, t.bgSurface2, 4.5, "WCAG 1.4.3 — rendered as text, on the well");
|
|
631
692
|
}
|
|
632
693
|
add("danger on dangerBg", semantic.danger, semantic.dangerBg, 4.5, "WCAG 1.4.3 — badge text");
|
|
633
694
|
add("warning on warningBg", semantic.warning, semantic.warningBg, 4.5, "WCAG 1.4.3 — badge text");
|
|
@@ -472,7 +472,8 @@ export function presetById(id: string): PalettePreset {
|
|
|
472
472
|
* element, which is what that list was standing in for.
|
|
473
473
|
*
|
|
474
474
|
* **More contrast (0.32, docs/text-size-harmonization.md §5, §10.5).** With `contrast`
|
|
475
|
-
* the text and border tokens are written one step stronger
|
|
475
|
+
* the text and border tokens are written one step stronger — in a light set the well and
|
|
476
|
+
* the four money colours too (0.36, docs/colour-roles-harmonization.md §13) — and `--text-placeholder` —
|
|
476
477
|
* otherwise derived in tokens.css — is written inline too; without it that inline
|
|
477
478
|
* placeholder is removed again, so the stylesheet's formula applies. The step is made
|
|
478
479
|
* HERE because these tokens are inline: no `[data-contrast=more]` rule could beat them.
|
|
@@ -491,7 +492,9 @@ export function applyTokenSet(
|
|
|
491
492
|
const step = contrast ? contrastStep(t) : null;
|
|
492
493
|
el.style.setProperty("--bg-page", t.bgPage);
|
|
493
494
|
el.style.setProperty("--bg-surface", t.bgSurface);
|
|
494
|
-
|
|
495
|
+
// The light well and money step under More contrast (0.36, colour-roles §13); in dark
|
|
496
|
+
// and at Standard `well` is null and the set's own are written.
|
|
497
|
+
el.style.setProperty("--bg-surface-2", step?.well?.bgSurface2 ?? t.bgSurface2);
|
|
495
498
|
el.style.setProperty("--border", step?.border ?? t.border);
|
|
496
499
|
el.style.setProperty("--text-primary", t.textPrimary);
|
|
497
500
|
el.style.setProperty("--text-secondary", step?.textSecondary ?? t.textSecondary);
|
|
@@ -501,9 +504,9 @@ export function applyTokenSet(
|
|
|
501
504
|
el.style.setProperty("--brand", t.brand);
|
|
502
505
|
el.style.setProperty("--brand-hover", t.brandHover);
|
|
503
506
|
el.style.setProperty("--brand-contrast", t.brandContrast);
|
|
504
|
-
el.style.setProperty("--money-income", t.moneyIncome);
|
|
505
|
-
el.style.setProperty("--money-expense", t.moneyExpense);
|
|
506
|
-
el.style.setProperty("--money-net", t.moneyNet);
|
|
507
|
-
el.style.setProperty("--money-neutral", t.moneyNeutral);
|
|
507
|
+
el.style.setProperty("--money-income", step?.well?.moneyIncome ?? t.moneyIncome);
|
|
508
|
+
el.style.setProperty("--money-expense", step?.well?.moneyExpense ?? t.moneyExpense);
|
|
509
|
+
el.style.setProperty("--money-net", step?.well?.moneyNet ?? t.moneyNet);
|
|
510
|
+
el.style.setProperty("--money-neutral", step?.well?.moneyNeutral ?? t.moneyNeutral);
|
|
508
511
|
t.chart.forEach((c, i) => el.style.setProperty(`--chart-${i + 1}`, c));
|
|
509
512
|
}
|
|
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
4
|
import type { StoreApi, UseBoundStore } from "zustand";
|
|
5
5
|
import { logger } from "../lib/logger";
|
|
6
|
+
import { registerDeviceSetting } from "../lib/device-settings";
|
|
6
7
|
import type { HeatStops } from "./chart-palette";
|
|
7
8
|
import { applyTokenSet, DEFAULT_PRESET, presetById, type TokenSet } from "./palette-presets";
|
|
8
9
|
import type { ThemeState, ThemeMode } from "./theme-store";
|
|
@@ -56,6 +57,17 @@ export function createPaletteStore(
|
|
|
56
57
|
"palette",
|
|
57
58
|
),
|
|
58
59
|
);
|
|
60
|
+
// Reset this device's settings (docs/settings-harmonization.md §11.4): the Default
|
|
61
|
+
// palette, STATE ONLY. `useApplyPalette`, where the app mounts it, re-applies through
|
|
62
|
+
// its effect. Never an imperative `applyTokenSet` here: it would remember a token set
|
|
63
|
+
// on <html>, and an app without a palette layer (Kurvenschmiede) would then take the
|
|
64
|
+
// contrast store's palette branch, where a theme switch no longer re-writes the stepped
|
|
65
|
+
// tokens (KS 3.3).
|
|
66
|
+
registerDeviceSetting({
|
|
67
|
+
key: storageKey,
|
|
68
|
+
kind: "appearance",
|
|
69
|
+
reset: () => usePalette.getState().setId(DEFAULT_PRESET.id),
|
|
70
|
+
});
|
|
59
71
|
|
|
60
72
|
/** Apply the active preset's tokens to <html> as inline CSS custom properties,
|
|
61
73
|
* re-applying when the preset or the light/dark theme changes. Tailwind v4
|
package/src/theme/text-size.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
4
|
import { logger } from "../lib/logger";
|
|
5
5
|
import { readStored } from "../lib/safe-storage";
|
|
6
|
+
import { registerDeviceSetting } from "../lib/device-settings";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The text size (docs/text-size-harmonization.md §2.1, §3.1, §10.3, §10.6).
|
|
@@ -267,6 +268,13 @@ export function createTextSizeStore(storageKey: string) {
|
|
|
267
268
|
"text-size",
|
|
268
269
|
),
|
|
269
270
|
);
|
|
271
|
+
// Reset this device's settings (docs/settings-harmonization.md §11.4): no choice on
|
|
272
|
+
// this device, so `useApplyTextSize` re-resolves to the account's size, else Normal.
|
|
273
|
+
registerDeviceSetting({
|
|
274
|
+
key: storageKey,
|
|
275
|
+
kind: "appearance",
|
|
276
|
+
reset: () => useTextSizeStore.getState().setSize(null),
|
|
277
|
+
});
|
|
270
278
|
|
|
271
279
|
function useApplyTextSize({ account }: ApplyTextSizeOptions = {}): TextSize {
|
|
272
280
|
const device = useTextSizeStore((s) => s.size);
|
package/src/theme/theme-store.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useEffect } from "react";
|
|
|
2
2
|
import { create } from "zustand";
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
4
|
import { logger } from "../lib/logger";
|
|
5
|
+
import { registerDeviceSetting } from "../lib/device-settings";
|
|
5
6
|
|
|
6
7
|
export type ThemeMode = "light" | "dark";
|
|
7
8
|
/** What the user chose; "system" tracks the OS `prefers-color-scheme`. */
|
|
@@ -116,6 +117,15 @@ export function createThemeStore(storageKey: string) {
|
|
|
116
117
|
"theme",
|
|
117
118
|
),
|
|
118
119
|
);
|
|
120
|
+
// Reset this device's settings (docs/settings-harmonization.md §11.4): back to
|
|
121
|
+
// "System" through the setter, which re-resolves `mode`; `useApplyTheme` puts the
|
|
122
|
+
// class back and follows the OS again, and the persisted blob is what the pre-paint
|
|
123
|
+
// `applyPersistedTheme` reads next load.
|
|
124
|
+
registerDeviceSetting({
|
|
125
|
+
key: storageKey,
|
|
126
|
+
kind: "appearance",
|
|
127
|
+
reset: () => useTheme.getState().setPreference("system"),
|
|
128
|
+
});
|
|
119
129
|
|
|
120
130
|
function useApplyTheme(): void {
|
|
121
131
|
const mode = useTheme((s) => s.mode);
|
package/tokens.css
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
* - In an app WITH a palette layer (keksdose, kastlan), `applyTokenSet` writes the
|
|
20
20
|
* active preset's values inline on <html>, and those beat these.
|
|
21
21
|
* - In an app WITHOUT one (Kurvenschmiede), these are what paints, and More contrast
|
|
22
|
-
* writes only its
|
|
22
|
+
* writes only its stepped values over them (`reapplyContrastTokens`): four in dark,
|
|
23
|
+
* nine in light, where the well and the money step too.
|
|
23
24
|
*
|
|
24
25
|
* Because it was taken for inert, it drifted silently, and it did — twice. The surfaces
|
|
25
26
|
* and text colour below still held the pre-#328 palette while DEFAULT_PRESET had moved
|
|
@@ -42,9 +43,13 @@
|
|
|
42
43
|
* paint (`applyPersistedTextSize`, or the inline `TEXT_SIZE_INLINE_SCRIPT`).
|
|
43
44
|
*
|
|
44
45
|
* `data-contrast="more"` is "More contrast", from the setting or from
|
|
45
|
-
* `prefers-contrast: more` on "System". The colour tokens
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* `prefers-contrast: more` on "System". The inline colour tokens — the text roles, the
|
|
47
|
+
* border, and in light the well and the money — are stepped in JS (`applyTokenSet`,
|
|
48
|
+
* `contrastStep`), because they are written inline on <html> and no rule here could beat
|
|
49
|
+
* them. What lives here is only what is NOT inline: the focus frame's width below, and
|
|
50
|
+
* in light the status, hue and field-sync literals and `--bg-active`, which step to read
|
|
51
|
+
* on the darker well (0.36, the `:root[data-contrast="more"]:not(.dark)` block after the
|
|
52
|
+
* categorical hues; docs/colour-roles-harmonization.md §13).
|
|
48
53
|
*
|
|
49
54
|
* These names are frozen: apps read them, and so does an app's boot splash. */
|
|
50
55
|
|
|
@@ -635,6 +640,35 @@ html[data-text-size="xlarge"] {
|
|
|
635
640
|
--hue-orange-bg: rgba(124, 45, 18, 0.25);
|
|
636
641
|
}
|
|
637
642
|
|
|
643
|
+
/* ── More contrast in light: the literals on the darker well (0.36) ───────────────
|
|
644
|
+
docs/colour-roles-harmonization.md §13. Under More contrast a light set's well is 10 %
|
|
645
|
+
ink into the card (`WELL_INK_MORE`, stepped in JS with the money, since both are
|
|
646
|
+
inline). On it the light literals above fell to 4.07–4.28:1 as text and
|
|
647
|
+
`--status-edited` to 2.77:1 as a mark, so they step here, each at its light value's
|
|
648
|
+
OKLCH hue and chroma at the lightness that clears its ratio on the stepped well of every
|
|
649
|
+
shipped preset — as §5.5 solved them against the page. On the page and the card they
|
|
650
|
+
then read 5.08:1 and up. The ones not listed hold on the stepped well as they are
|
|
651
|
+
(blue 4.60, indigo 5.42, purple 4.79; synced 4.89, pending 4.60, error 4.44 and the
|
|
652
|
+
danger line 3.22, marks and lines needing 3); `--warning-border-strong` is
|
|
653
|
+
`var(--warning)` in light and follows.
|
|
654
|
+
|
|
655
|
+
`--bg-active` (a digit key's hover, Button primary's, keksdose's assistant bubbles on
|
|
656
|
+
the well) sat 1.04:1 from a 10 % well at 12 %; at 16 % it is 1.12–1.15:1 from it.
|
|
657
|
+
|
|
658
|
+
`(0,3,0)` outranks `:root` and `:root, .dark` whatever the order, and `.dark` on
|
|
659
|
+
<html> leaves it out: dark does not step (§13.3, decision 4).
|
|
660
|
+
tokens-css-audit.test.ts measures every pair here. */
|
|
661
|
+
:root[data-contrast="more"]:not(.dark) {
|
|
662
|
+
--danger: #ba0739; /* solved: 4.53:1 on the worst stepped well (was 4.31) */
|
|
663
|
+
--warning: #9a4300; /* solved: 4.53:1 (was 4.13) */
|
|
664
|
+
--success: #006a4d; /* solved: 4.54:1 (was 4.13) */
|
|
665
|
+
--info: #006197; /* solved: 4.56:1 (was 4.07) */
|
|
666
|
+
--hue-teal: #006862; /* solved: 4.56:1 (was 4.15) */
|
|
667
|
+
--hue-orange: #a93400; /* solved: 4.53:1 (was 4.11) */
|
|
668
|
+
--status-edited: #b66200; /* solved, a mark: 3.04:1 (was 2.77) */
|
|
669
|
+
--bg-active: color-mix(in oklab, var(--text-primary) 16%, var(--bg-surface));
|
|
670
|
+
}
|
|
671
|
+
|
|
638
672
|
/* ── Role utilities (0.33, docs/colour-roles-harmonization.md §7, k25) ─────────────
|
|
639
673
|
*
|
|
640
674
|
* `text-muted`, `bg-surface-2`, `border-subtle`, `ring-brand`, `fill-surface` … instead of
|