@feugene/granularity 0.18.0 → 0.19.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/dist/chunks/chevron-left-D8AUBAM7.js +23 -0
- package/dist/chunks/chevron-left-D8AUBAM7.js.map +1 -0
- package/dist/chunks/{chevron-right-BfqMVpbB.js → chevron-right-C6Z01h0X.js} +1 -22
- package/dist/chunks/chevron-right-C6Z01h0X.js.map +1 -0
- package/dist/chunks/config-8Pcutu1j2.js.map +1 -0
- package/dist/chunks/{config-K0C2tcLB.js → config-9PYRZlKE.js} +1 -1
- package/dist/chunks/{config-K0C2tcLB.js.map → config-9PYRZlKE.js.map} +1 -1
- package/dist/chunks/{config-DEvsMTQE.js → config-BJ7Bv_G2.js} +1 -3
- package/dist/chunks/config-BJ7Bv_G2.js.map +1 -0
- package/dist/chunks/config-BKvX0q102.js.map +1 -0
- package/dist/chunks/{config-gvq_hWNM.js → config-Be9k9bOf.js} +1 -7
- package/dist/chunks/config-Be9k9bOf.js.map +1 -0
- package/dist/chunks/{config-DpqOnJiz2.js → config-CDat1wXf2.js} +1 -1
- package/dist/chunks/config-CDat1wXf2.js.map +1 -0
- package/dist/chunks/config-CY0_6cMi2.js.map +1 -0
- package/dist/chunks/config-D94Z9po52.js.map +1 -0
- package/dist/chunks/{config-D2aNP7Es.js → config-DAX8Deh1.js} +1 -3
- package/dist/chunks/config-DAX8Deh1.js.map +1 -0
- package/dist/chunks/config-DCFvmHEv.js.map +1 -0
- package/dist/chunks/config-DKdlqXiY.js.map +1 -1
- package/dist/chunks/config-Rw9OyW702.js.map +1 -0
- package/dist/chunks/generated-BRkbhyxk.js +3270 -0
- package/dist/chunks/generated-BRkbhyxk.js.map +1 -0
- package/dist/chunks/{granular-provider-B9nXQCMu.js → granular-provider-C6uAujdJ.js} +12 -12
- package/dist/chunks/{granular-provider-B9nXQCMu.js.map → granular-provider-C6uAujdJ.js.map} +1 -1
- package/dist/chunks/grip-vertical-lRha2S7q.js +16 -0
- package/dist/chunks/grip-vertical-lRha2S7q.js.map +1 -0
- package/dist/chunks/icon-Dag2tIMs.js +21 -0
- package/dist/chunks/icon-Dag2tIMs.js.map +1 -0
- package/dist/chunks/{portalRoot-CWD4zHBm.js → portalRoot-CVhCgdG6.js} +10 -1
- package/dist/chunks/{portalRoot-CWD4zHBm.js.map → portalRoot-CVhCgdG6.js.map} +1 -1
- package/dist/chunks/{useModalOverlay-BTq-0ImG.js → useModalOverlay-DrQVYwwA.js} +1 -1
- package/dist/chunks/{useModalOverlay-BTq-0ImG.js.map → useModalOverlay-DrQVYwwA.js.map} +1 -1
- package/dist/chunks/{usePortalTarget-RyzEgNTb.js → usePortalTarget-Cn-Wiz5d.js} +1 -1
- package/dist/chunks/{usePortalTarget-RyzEgNTb.js.map → usePortalTarget-Cn-Wiz5d.js.map} +1 -1
- package/dist/components/GrAutocomplete/chunks/{GrAutocomplete-CPEQxmLZ.js → GrAutocomplete-gY4Xopza.js} +32 -24
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-gY4Xopza.js.map +1 -0
- package/dist/components/GrAutocomplete/index.js +1 -1
- package/dist/components/GrAutocomplete/styles.css +1 -1
- package/dist/components/GrBreadcrumbs/chunks/{GrBreadcrumbs-BCNGDS6k.js → GrBreadcrumbs-DS_-PBVt.js} +8 -7
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-DS_-PBVt.js.map +1 -0
- package/dist/components/GrBreadcrumbs/index.js +1 -1
- package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js.map → GrCollapse-Kk0DE8tb.js.map} +1 -1
- package/dist/components/GrCollapse/index.js +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js → GrColorPicker-CkBTzkqn.js} +1 -1
- package/dist/components/GrColorPicker/chunks/{GrColorPicker-DAdQ-Gc3.js.map → GrColorPicker-CkBTzkqn.js.map} +1 -1
- package/dist/components/GrColorPicker/index.js +1 -1
- package/dist/components/GrCommandPalette/chunks/{GrCommandPalette-DMeSqkEv.js → GrCommandPalette-BugTlGdj.js} +37 -11
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-BugTlGdj.js.map +1 -0
- package/dist/components/GrCommandPalette/index.js +2 -2
- package/dist/components/GrCommandPalette/styles.css +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js → GrConfirmDialog-Ds5T0GiR.js} +1 -1
- package/dist/components/GrConfirmDialog/chunks/{GrConfirmDialog-hUTi-GdJ.js.map → GrConfirmDialog-Ds5T0GiR.js.map} +1 -1
- package/dist/components/GrConfirmDialog/index.js +1 -1
- package/dist/components/GrDataTable/chunks/{GrDataTable-DzaGLG3A.js → GrDataTable-CYe2A2WA.js} +9 -7
- package/dist/components/GrDataTable/chunks/GrDataTable-CYe2A2WA.js.map +1 -0
- package/dist/components/GrDataTable/index.js +2 -2
- package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js → GrDialog-C3tYaA8v.js} +1 -1
- package/dist/components/GrDialog/chunks/{GrDialog-MD9Fzy9C.js.map → GrDialog-C3tYaA8v.js.map} +1 -1
- package/dist/components/GrDialog/index.js +1 -1
- package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js → GrDialogService-CTBiiJDI.js} +3 -3
- package/dist/components/GrDialogService/chunks/{GrDialogService-DtgUijMj.js.map → GrDialogService-CTBiiJDI.js.map} +1 -1
- package/dist/components/GrDialogService/index.js +1 -1
- package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js → GrDivider-xxq1_fIl.js} +1 -1
- package/dist/components/GrDivider/chunks/{GrDivider-B2Xwc_QO.js.map → GrDivider-xxq1_fIl.js.map} +1 -1
- package/dist/components/GrDivider/index.js +2 -2
- package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js → GrDrawer-BSSuiNHU.js} +2 -2
- package/dist/components/GrDrawer/chunks/{GrDrawer-DWHdZZzW.js.map → GrDrawer-BSSuiNHU.js.map} +1 -1
- package/dist/components/GrDrawer/index.js +2 -2
- package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js → GrDropdown-Bh6UK2M5.js} +1 -1
- package/dist/components/GrDropdown/chunks/{GrDropdown-0tJK01Kq.js.map → GrDropdown-Bh6UK2M5.js.map} +1 -1
- package/dist/components/GrDropdown/index.js +1 -1
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js → GrDropdownMenu-LyfEFUbg.js} +2 -2
- package/dist/components/GrDropdownMenu/chunks/{GrDropdownMenu-VPPrpCoA.js.map → GrDropdownMenu-LyfEFUbg.js.map} +1 -1
- package/dist/components/GrDropdownMenu/index.js +2 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js → GrImageViewer-DfD4euH5.js} +3 -2
- package/dist/components/GrImageViewer/chunks/{GrImageViewer-DE7MBbF8.js.map → GrImageViewer-DfD4euH5.js.map} +1 -1
- package/dist/components/GrImageViewer/index.js +1 -1
- package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js → GrModal-CbMMr6W1.js} +1 -1
- package/dist/components/GrModal/chunks/{GrModal-BbhcsDQ8.js.map → GrModal-CbMMr6W1.js.map} +1 -1
- package/dist/components/GrModal/index.js +1 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js → GrNumberInput-B5CHZDBj.js} +2 -1
- package/dist/components/GrNumberInput/chunks/{GrNumberInput-CZOyPz4J.js.map → GrNumberInput-B5CHZDBj.js.map} +1 -1
- package/dist/components/GrNumberInput/index.js +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js → GrPagination-BM4FrD6Q.js} +1 -1
- package/dist/components/GrPagination/chunks/{GrPagination-waKhxBP7.js.map → GrPagination-BM4FrD6Q.js.map} +1 -1
- package/dist/components/GrPagination/index.js +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js → GrPopover-DMH4Jjst.js} +1 -1
- package/dist/components/GrPopover/chunks/{GrPopover-DuqeoFaI.js.map → GrPopover-DMH4Jjst.js.map} +1 -1
- package/dist/components/GrPopover/index.js +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js → GrPromptDialog-Pwr9NPxK.js} +1 -1
- package/dist/components/GrPromptDialog/chunks/{GrPromptDialog-JVVqBsPY.js.map → GrPromptDialog-Pwr9NPxK.js.map} +1 -1
- package/dist/components/GrPromptDialog/index.js +1 -1
- package/dist/components/GrRating/chunks/{GrRating-LarIiwJL.js → GrRating-DVFH3lYu.js} +8 -7
- package/dist/components/GrRating/chunks/GrRating-DVFH3lYu.js.map +1 -0
- package/dist/components/GrRating/index.js +1 -1
- package/dist/components/GrSelect/chunks/{GrSelect-DAdu044K.js → GrSelect-B-Voni7D.js} +65 -52
- package/dist/components/GrSelect/chunks/GrSelect-B-Voni7D.js.map +1 -0
- package/dist/components/GrSelect/index.js +1 -1
- package/dist/components/GrSelect/styles.css +1 -1
- package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js → GrSidebar-i7AxjhY_.js} +3 -2
- package/dist/components/GrSidebar/chunks/{GrSidebar-DRGnnViw.js.map → GrSidebar-i7AxjhY_.js.map} +1 -1
- package/dist/components/GrSidebar/index.js +2 -2
- package/dist/components/GrSkeleton/index.js +1 -1
- package/dist/components/GrSortableList/chunks/{GrSortableList-BefVfWKh.js → GrSortableList-XB7mXM1W.js} +6 -5
- package/dist/components/GrSortableList/chunks/GrSortableList-XB7mXM1W.js.map +1 -0
- package/dist/components/GrSortableList/index.js +2 -2
- package/dist/components/GrStatistic/chunks/{GrStatistic-J4qLs3TH.js → GrStatistic-DOsM6s5V.js} +54 -7
- package/dist/components/GrStatistic/chunks/GrStatistic-DOsM6s5V.js.map +1 -0
- package/dist/components/GrStatistic/index.js +2 -2
- package/dist/components/GrTabs/chunks/{GrTabs-TeXaU5Uh.js → GrTabs-f3-oQenu.js} +5 -4
- package/dist/components/GrTabs/chunks/GrTabs-f3-oQenu.js.map +1 -0
- package/dist/components/GrTabs/index.js +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js → GrToaster-Dqja0Lf9.js} +1 -1
- package/dist/components/GrToaster/chunks/{GrToaster-Ct-39hga.js.map → GrToaster-Dqja0Lf9.js.map} +1 -1
- package/dist/components/GrToaster/index.js +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js → GrTooltip-Cn08heWz.js} +1 -1
- package/dist/components/GrTooltip/chunks/{GrTooltip-xAcqs0bP.js.map → GrTooltip-Cn08heWz.js.map} +1 -1
- package/dist/components/GrTooltip/index.js +1 -1
- package/dist/components/GrTree/chunks/{GrTree-CaYK8dGZ.js → GrTree-BR9a2pay.js} +25 -20
- package/dist/components/GrTree/chunks/GrTree-BR9a2pay.js.map +1 -0
- package/dist/components/GrTree/index.js +2 -2
- package/dist/components/GrTree/styles.css +1 -1
- package/dist/components/GrTreeSelect/chunks/{GrTreeSelect-DZp7CqqW.js → GrTreeSelect--muUeB0v.js} +27 -13
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect--muUeB0v.js.map +1 -0
- package/dist/components/GrTreeSelect/index.js +2 -2
- package/dist/composables/useAnnouncer.js +1 -1
- package/dist/composables/usePortalTarget.js +1 -1
- package/dist/directives/loading.js +1 -1
- package/dist/granular-provider-node.js +1 -1
- package/dist/granular-provider.js +1 -1
- package/dist/index.js +41 -41
- package/dist/styles/themes/dark.css +6 -1
- package/dist/testing.js +199 -0
- package/dist/testing.js.map +1 -0
- package/dist/theme-apply.js +33 -0
- package/dist/theme-apply.js.map +1 -0
- package/dist/theme.js +501 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.js +1 -3215
- package/dist/types/src/components/GrBreadcrumbs/grBreadcrumbsStyles.d.ts +5 -2
- package/dist/types/src/components/GrCommandPalette/GrCommandPalette.vue.d.ts +6 -6
- package/dist/types/src/components/GrCommandPalette/filtering.d.ts +6 -2
- package/dist/types/src/components/GrDataTable/grDataTableStyles.d.ts +0 -1
- package/dist/types/src/components/GrRating/GrRating.vue.d.ts +9 -4
- package/dist/types/src/components/GrSortableList/grSortableListStyles.d.ts +0 -1
- package/dist/types/src/components/GrStatistic/GrStatistic.vue.d.ts +12 -9
- package/dist/types/src/components/GrStatistic/grStatisticStyles.d.ts +0 -1
- package/dist/types/src/components/GrTabs/GrTabs.vue.d.ts +6 -2
- package/dist/types/src/components/GrTree/grTreeProps.d.ts +11 -4
- package/dist/types/src/components/GrTree/safelist.d.ts +1 -1
- package/dist/types/src/components/shared/icon.d.ts +13 -0
- package/dist/types/src/testing/announcer.d.ts +10 -0
- package/dist/types/src/testing/context.d.ts +36 -0
- package/dist/types/src/testing/env.d.ts +25 -0
- package/dist/types/src/testing/geometry.d.ts +23 -0
- package/dist/types/src/testing/index.d.ts +12 -0
- package/dist/types/src/testing/pointer.d.ts +31 -0
- package/dist/types/src/theme/apply.d.ts +27 -0
- package/dist/types/src/theme/color.d.ts +35 -0
- package/dist/types/src/theme/extendTheme.d.ts +63 -0
- package/dist/types/src/theme/index.d.ts +22 -0
- package/dist/types/src/theme/roles.d.ts +16 -0
- package/dist/types/src/theme/tone.d.ts +32 -0
- package/dist/types/src/theme/validate.d.ts +16 -0
- package/dist/types/src/tokens/types.d.ts +8 -0
- package/dist/web-types.json +14 -11
- package/package.json +17 -2
- package/tokens/themes/dark.json +2 -1
- package/dist/chunks/chevron-right-BfqMVpbB.js.map +0 -1
- package/dist/chunks/config-8Pcutu1j.js.map +0 -1
- package/dist/chunks/config-BKvX0q10.js.map +0 -1
- package/dist/chunks/config-CY0_6cMi.js.map +0 -1
- package/dist/chunks/config-D2aNP7Es.js.map +0 -1
- package/dist/chunks/config-D94Z9po5.js.map +0 -1
- package/dist/chunks/config-DCFvmHEv2.js.map +0 -1
- package/dist/chunks/config-DEvsMTQE.js.map +0 -1
- package/dist/chunks/config-DpqOnJiz2.js.map +0 -1
- package/dist/chunks/config-Rw9OyW70.js.map +0 -1
- package/dist/chunks/config-gvq_hWNM.js.map +0 -1
- package/dist/components/GrAutocomplete/chunks/GrAutocomplete-CPEQxmLZ.js.map +0 -1
- package/dist/components/GrBreadcrumbs/chunks/GrBreadcrumbs-BCNGDS6k.js.map +0 -1
- package/dist/components/GrCommandPalette/chunks/GrCommandPalette-DMeSqkEv.js.map +0 -1
- package/dist/components/GrDataTable/chunks/GrDataTable-DzaGLG3A.js.map +0 -1
- package/dist/components/GrRating/chunks/GrRating-LarIiwJL.js.map +0 -1
- package/dist/components/GrSelect/chunks/GrSelect-DAdu044K.js.map +0 -1
- package/dist/components/GrSortableList/chunks/GrSortableList-BefVfWKh.js.map +0 -1
- package/dist/components/GrStatistic/chunks/GrStatistic-J4qLs3TH.js.map +0 -1
- package/dist/components/GrTabs/chunks/GrTabs-TeXaU5Uh.js.map +0 -1
- package/dist/components/GrTree/chunks/GrTree-CaYK8dGZ.js.map +0 -1
- package/dist/components/GrTreeSelect/chunks/GrTreeSelect-DZp7CqqW.js.map +0 -1
- package/dist/tokens.js.map +0 -1
- package/dist/chunks/{config-8Pcutu1j.js → config-8Pcutu1j2.js} +0 -0
- package/dist/chunks/{config-BKvX0q10.js → config-BKvX0q102.js} +0 -0
- package/dist/chunks/{config-CY0_6cMi.js → config-CY0_6cMi2.js} +0 -0
- package/dist/chunks/{config-D94Z9po5.js → config-D94Z9po52.js} +0 -0
- package/dist/chunks/{config-DCFvmHEv2.js → config-DCFvmHEv.js} +0 -0
- package/dist/chunks/{config-Rw9OyW70.js → config-Rw9OyW702.js} +0 -0
- package/dist/components/GrCollapse/chunks/{GrCollapse-B2Fc6A9r.js → GrCollapse-Kk0DE8tb.js} +1 -1
package/dist/theme.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { a as grThemeTokens, n as grDerivedTokens, t as grComponentTokens } from "./chunks/generated-BRkbhyxk.js";
|
|
2
|
+
//#region src/theme/color.ts
|
|
3
|
+
function parseHex(value) {
|
|
4
|
+
const match = value.trim().match(/^#([\da-f]{3}|[\da-f]{6})$/i);
|
|
5
|
+
if (!match) throw new Error(`ожидался hex-цвет, получено: ${value}`);
|
|
6
|
+
const hex = match[1].length === 3 ? match[1].split("").map((char) => char + char).join("") : match[1];
|
|
7
|
+
return [
|
|
8
|
+
0,
|
|
9
|
+
2,
|
|
10
|
+
4
|
|
11
|
+
].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
|
|
12
|
+
}
|
|
13
|
+
function toHex(channels) {
|
|
14
|
+
return `#${channels.map((channel) => Math.round(Math.min(255, Math.max(0, channel))).toString(16).padStart(2, "0")).join("")}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `color-mix(in srgb, base <amount>%, other)` для непрозрачных цветов — это
|
|
18
|
+
* линейная интерполяция в gamma-кодированном sRGB, то есть ровно то, что делает
|
|
19
|
+
* браузер. Поэтому посчитанный здесь фолбэк совпадает с живым `color-mix`.
|
|
20
|
+
*/
|
|
21
|
+
function mixSrgb(baseHex, otherHex, amount) {
|
|
22
|
+
const base = parseHex(baseHex);
|
|
23
|
+
const other = parseHex(otherHex);
|
|
24
|
+
const ratio = amount / 100;
|
|
25
|
+
return toHex(base.map((channel, index) => channel * ratio + other[index] * (1 - ratio)));
|
|
26
|
+
}
|
|
27
|
+
function luminance(color) {
|
|
28
|
+
const channels = (typeof color === "string" ? parseHex(color) : color).map((channel) => {
|
|
29
|
+
const normalized = channel / 255;
|
|
30
|
+
return normalized <= .03928 ? normalized / 12.92 : ((normalized + .055) / 1.055) ** 2.4;
|
|
31
|
+
});
|
|
32
|
+
return .2126 * channels[0] + .7152 * channels[1] + .0722 * channels[2];
|
|
33
|
+
}
|
|
34
|
+
/** Отношение контраста по WCAG 2.1: от 1 (одинаковые) до 21 (чёрное на белом). */
|
|
35
|
+
function contrast(foreground, background) {
|
|
36
|
+
const first = luminance(foreground);
|
|
37
|
+
const second = luminance(background);
|
|
38
|
+
const [lighter, darker] = first > second ? [first, second] : [second, first];
|
|
39
|
+
return (lighter + .05) / (darker + .05);
|
|
40
|
+
}
|
|
41
|
+
function toLab(color) {
|
|
42
|
+
const [red, green, blue] = (typeof color === "string" ? parseHex(color) : color).map((channel) => {
|
|
43
|
+
const normalized = channel / 255;
|
|
44
|
+
return normalized <= .04045 ? normalized / 12.92 : ((normalized + .055) / 1.055) ** 2.4;
|
|
45
|
+
});
|
|
46
|
+
const x = (red * .4124 + green * .3576 + blue * .1805) / .95047;
|
|
47
|
+
const y = red * .2126 + green * .7152 + blue * .0722;
|
|
48
|
+
const z = (red * .0193 + green * .1192 + blue * .9505) / 1.08883;
|
|
49
|
+
const f = (value) => value > .008856 ? value ** (1 / 3) : 7.787 * value + 16 / 116;
|
|
50
|
+
const [fx, fy, fz] = [
|
|
51
|
+
f(x),
|
|
52
|
+
f(y),
|
|
53
|
+
f(z)
|
|
54
|
+
];
|
|
55
|
+
return [
|
|
56
|
+
116 * fy - 16,
|
|
57
|
+
500 * (fx - fy),
|
|
58
|
+
200 * (fy - fz)
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Воспринимаемое расстояние между цветами (ΔE, CIE76).
|
|
63
|
+
*
|
|
64
|
+
* Контраст отвечает на вопрос «читается ли текст», а этот — на вопрос
|
|
65
|
+
* «различит ли человек два тона рядом». Порог заметности около 2.3; всё, что
|
|
66
|
+
* ниже, для глаза один цвет, каким бы разным ни выглядел hex.
|
|
67
|
+
*/
|
|
68
|
+
function deltaE(first, second) {
|
|
69
|
+
const [firstLab, secondLab] = [toLab(first), toLab(second)];
|
|
70
|
+
return Math.hypot(...firstLab.map((value, index) => value - secondLab[index]));
|
|
71
|
+
}
|
|
72
|
+
/** Цвет — это hex, а не `var()`/`rgb()`: производные и контраст считаются только по нему. */
|
|
73
|
+
function isHex(value) {
|
|
74
|
+
return /^#(?:[\da-f]{3}|[\da-f]{6})$/i.test(value.trim());
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/theme/roles.ts
|
|
78
|
+
/** Роли, известные пакету, в порядке объявления. */
|
|
79
|
+
function themeRoleNames() {
|
|
80
|
+
return grThemeTokens.map((token) => token.name);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Значение роли, годное для арифметики.
|
|
84
|
+
*
|
|
85
|
+
* Роль вправе ссылаться на другую (`--gr-invalid-brd: var(--gr-danger)`) — такую
|
|
86
|
+
* ссылку разворачиваем, иначе фолбэк и проверки считать не из чего. Всё, что не
|
|
87
|
+
* разворачивается в hex (`rgb(… / .45)` у подложки, `var()` за пределы темы),
|
|
88
|
+
* возвращается как `null`: посчитать нельзя, и выдумывать не надо.
|
|
89
|
+
*
|
|
90
|
+
* Цикл рвём с ошибкой: тема потребителя вправе ошибиться, и упасть надо с
|
|
91
|
+
* именами ролей, а не переполнением стека.
|
|
92
|
+
*/
|
|
93
|
+
function resolveRole(tokens, name, seen = []) {
|
|
94
|
+
const value = tokens[name];
|
|
95
|
+
if (value === void 0) return null;
|
|
96
|
+
if (isHex(value)) return value;
|
|
97
|
+
const reference = value.trim().match(/^var\(\s*(--[\w-]+)\s*\)$/);
|
|
98
|
+
if (!reference) return null;
|
|
99
|
+
if (seen.includes(name)) throw new Error(`роль ${name} ссылается сама на себя: ${[...seen, name].join(" → ")}`);
|
|
100
|
+
return resolveRole(tokens, reference[1], [...seen, name]);
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/theme/validate.ts
|
|
104
|
+
/**
|
|
105
|
+
* AA для обычного текста.
|
|
106
|
+
*
|
|
107
|
+
* Порога для самой заливки здесь нет намеренно: `--gr-success` в светлой теме
|
|
108
|
+
* даёт 2.42:1 к фону, и это не дефект — информацию бейдж несёт текстом `-fg`
|
|
109
|
+
* поверх заливки, а не самой заливкой. Правило «≥3:1 к фону» уронило бы
|
|
110
|
+
* встроенные темы пакета, то есть проверяло бы не то, о чём договаривались.
|
|
111
|
+
*/
|
|
112
|
+
var AA_TEXT$1 = 4.5;
|
|
113
|
+
/** Порог заметности ΔE: ниже — для глаза один цвет, каким бы разным ни был hex. */
|
|
114
|
+
var NOTICEABLE = 2.3;
|
|
115
|
+
/**
|
|
116
|
+
* Тона, выведенные из данных, а не списком: тон — это роль, у которой есть
|
|
117
|
+
* пара `-fg`. Новый тон пакета попадает под проверки сам.
|
|
118
|
+
*/
|
|
119
|
+
function toneNames() {
|
|
120
|
+
const names = new Set(grThemeTokens.map((token) => token.name));
|
|
121
|
+
return [...names].filter((name) => names.has(`${name}-fg`)).map((name) => name.replace("--gr-", ""));
|
|
122
|
+
}
|
|
123
|
+
function derivedOf(role) {
|
|
124
|
+
return grDerivedTokens.filter((token) => token.base === role).map((token) => ({
|
|
125
|
+
amount: token.amount,
|
|
126
|
+
mixWith: token.mixWith
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function validateTheme(theme) {
|
|
130
|
+
const { tokens } = theme;
|
|
131
|
+
const issues = [];
|
|
132
|
+
const value = (role) => resolveRole(tokens, role);
|
|
133
|
+
function requireContrast(token, against, expected, what) {
|
|
134
|
+
const [foreground, background] = [value(token), value(against)];
|
|
135
|
+
if (!foreground || !background) return;
|
|
136
|
+
const actual = contrast(foreground, background);
|
|
137
|
+
if (actual >= expected) return;
|
|
138
|
+
issues.push({
|
|
139
|
+
kind: "contrast",
|
|
140
|
+
token,
|
|
141
|
+
against,
|
|
142
|
+
actual: Math.round(actual * 100) / 100,
|
|
143
|
+
expected,
|
|
144
|
+
message: `${what}: ${token} на ${against} даёт ${actual.toFixed(2)}:1 при пороге ${expected}:1`
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Заливка в hover/active уходит темнее — текст на ней может провалиться именно там. */
|
|
148
|
+
function requireContrastWithStates(fgRole, fillRole, what) {
|
|
149
|
+
requireContrast(fgRole, fillRole, AA_TEXT$1, what);
|
|
150
|
+
const fill = value(fillRole);
|
|
151
|
+
const foreground = value(fgRole);
|
|
152
|
+
if (!fill || !foreground) return;
|
|
153
|
+
for (const { amount, mixWith } of derivedOf(fillRole)) {
|
|
154
|
+
const mixed = value(mixWith);
|
|
155
|
+
if (!mixed) continue;
|
|
156
|
+
const state = contrast(foreground, mixSrgb(fill, mixed, amount));
|
|
157
|
+
if (state >= AA_TEXT$1) continue;
|
|
158
|
+
issues.push({
|
|
159
|
+
kind: "contrast",
|
|
160
|
+
token: fgRole,
|
|
161
|
+
against: `${fillRole} (${amount}%)`,
|
|
162
|
+
actual: Math.round(state * 100) / 100,
|
|
163
|
+
expected: AA_TEXT$1,
|
|
164
|
+
message: `${what}: ${fgRole} на состоянии ${fillRole} (${amount}%) даёт ${state.toFixed(2)}:1 при пороге ${AA_TEXT$1}:1`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const withOwnFg = new Set(toneNames().map((tone) => `--gr-${tone}`));
|
|
169
|
+
for (const surface of [
|
|
170
|
+
"--gr-bg",
|
|
171
|
+
"--gr-card",
|
|
172
|
+
"--gr-popover",
|
|
173
|
+
"--gr-sidebar"
|
|
174
|
+
]) {
|
|
175
|
+
if (withOwnFg.has(surface)) continue;
|
|
176
|
+
requireContrast("--gr-fg", surface, AA_TEXT$1, "основной текст");
|
|
177
|
+
}
|
|
178
|
+
for (const surface of [
|
|
179
|
+
"--gr-bg",
|
|
180
|
+
"--gr-card",
|
|
181
|
+
"--gr-muted",
|
|
182
|
+
"--gr-secondary"
|
|
183
|
+
]) requireContrast("--gr-muted-fg", surface, AA_TEXT$1, "вторичный текст");
|
|
184
|
+
for (const tone of toneNames()) {
|
|
185
|
+
const fill = `--gr-${tone}`;
|
|
186
|
+
requireContrastWithStates(`${fill}-fg`, fill, `текст на заливке ${tone}`);
|
|
187
|
+
for (const state of ["-hover", "-active"]) {
|
|
188
|
+
if (tokens[`${fill}${state}`] === void 0) continue;
|
|
189
|
+
requireContrast(`${fill}-fg`, `${fill}${state}`, AA_TEXT$1, `текст на состоянии ${tone}${state}`);
|
|
190
|
+
}
|
|
191
|
+
if (tokens[`${fill}-text`] !== void 0) {
|
|
192
|
+
requireContrast(`${fill}-text`, `${fill}-light`, AA_TEXT$1, `текст тона ${tone}`);
|
|
193
|
+
requireContrast(`${fill}-text`, "--gr-bg", AA_TEXT$1, `текст тона ${tone}`);
|
|
194
|
+
requireContrast(`${fill}-text`, "--gr-card", AA_TEXT$1, `текст тона ${tone}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const distinct = [
|
|
198
|
+
"primary",
|
|
199
|
+
"success",
|
|
200
|
+
"warning",
|
|
201
|
+
"danger",
|
|
202
|
+
"info",
|
|
203
|
+
"slate",
|
|
204
|
+
"azure"
|
|
205
|
+
];
|
|
206
|
+
for (let i = 0; i < distinct.length; i += 1) for (let j = i + 1; j < distinct.length; j += 1) {
|
|
207
|
+
const [first, second] = [`--gr-${distinct[i]}`, `--gr-${distinct[j]}`];
|
|
208
|
+
const [firstValue, secondValue] = [value(first), value(second)];
|
|
209
|
+
if (!firstValue || !secondValue) continue;
|
|
210
|
+
const distance = deltaE(firstValue, secondValue);
|
|
211
|
+
if (distance >= NOTICEABLE) continue;
|
|
212
|
+
issues.push({
|
|
213
|
+
kind: "distance",
|
|
214
|
+
token: first,
|
|
215
|
+
against: second,
|
|
216
|
+
actual: Math.round(distance * 100) / 100,
|
|
217
|
+
expected: NOTICEABLE,
|
|
218
|
+
message: `тона неразличимы: ΔE между ${first} и ${second} равно ${distance.toFixed(2)} при пороге ${NOTICEABLE}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return issues;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/theme/extendTheme.ts
|
|
225
|
+
var GrThemeError = class extends Error {
|
|
226
|
+
issues;
|
|
227
|
+
constructor(name, issues) {
|
|
228
|
+
super([`тема "${name}" не проходит проверку:`, ...issues.map((issue) => ` • ${issue.message}`)].join("\n"));
|
|
229
|
+
this.name = "GrThemeError";
|
|
230
|
+
this.issues = issues;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
function baseTokens(base) {
|
|
234
|
+
if (typeof base === "string") {
|
|
235
|
+
const known = ["light", "dark"];
|
|
236
|
+
if (!known.includes(base)) throw new Error(`неизвестная базовая тема "${base}"; встроенные: ${known.join(", ")}`);
|
|
237
|
+
return Object.fromEntries(grThemeTokens.map((token) => [token.name, token.values[base]]));
|
|
238
|
+
}
|
|
239
|
+
return "tokens" in base ? { ...base.tokens } : { ...base };
|
|
240
|
+
}
|
|
241
|
+
function renderDeclarations(tokens, indent) {
|
|
242
|
+
return grThemeTokens.map((token) => `${indent}${token.name}: ${tokens[token.name]};`);
|
|
243
|
+
}
|
|
244
|
+
function renderComponentDeclarations(tokens, indent) {
|
|
245
|
+
const names = Object.keys(tokens);
|
|
246
|
+
if (names.length === 0) return [];
|
|
247
|
+
return [
|
|
248
|
+
"",
|
|
249
|
+
`${indent}/* Покомпонентные токены темы. */`,
|
|
250
|
+
...names.map((name) => `${indent}${name}: ${tokens[name]};`)
|
|
251
|
+
];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Фолбэк производных состояний для браузеров без `color-mix`.
|
|
255
|
+
*
|
|
256
|
+
* Без него тема наследует фолбэки из `:root`, то есть чужие: hover у кастомной
|
|
257
|
+
* тёмной темы окажется от светлой. Формула та же, что в `tokens.css`, — считает
|
|
258
|
+
* её общая математика (`color.ts`), поэтому разойтись с живым `color-mix` не
|
|
259
|
+
* может.
|
|
260
|
+
*/
|
|
261
|
+
function renderFallback(tokens, selector) {
|
|
262
|
+
const lines = [
|
|
263
|
+
"",
|
|
264
|
+
"@supports not (color: color-mix(in srgb, #000 50%, #fff)) {",
|
|
265
|
+
` ${selector} {`
|
|
266
|
+
];
|
|
267
|
+
for (const token of grDerivedTokens) {
|
|
268
|
+
const base = resolveRole(tokens, token.base);
|
|
269
|
+
const mixWith = resolveRole(tokens, token.mixWith);
|
|
270
|
+
if (!base || !mixWith) continue;
|
|
271
|
+
lines.push(` ${token.name}: ${mixSrgb(base, mixWith, token.amount)};`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(" }", "}");
|
|
274
|
+
return lines;
|
|
275
|
+
}
|
|
276
|
+
function renderThemeCss(theme) {
|
|
277
|
+
return [
|
|
278
|
+
`/* Тема "${theme.name}" собрана из @feugene/granularity/theme — правки здесь потеряются. */`,
|
|
279
|
+
`${theme.selector} {`,
|
|
280
|
+
...renderDeclarations(theme.tokens, " "),
|
|
281
|
+
...renderComponentDeclarations(theme.componentTokens ?? {}, " "),
|
|
282
|
+
"}",
|
|
283
|
+
...renderFallback(theme.tokens, theme.selector),
|
|
284
|
+
""
|
|
285
|
+
].join("\n");
|
|
286
|
+
}
|
|
287
|
+
function buildTheme(options) {
|
|
288
|
+
const { name, tokens, componentTokens, selector, validate, explainMissing } = options;
|
|
289
|
+
const known = new Set(themeRoleNames());
|
|
290
|
+
const unknown = Object.keys(tokens).filter((role) => !known.has(role));
|
|
291
|
+
if (unknown.length > 0) throw new Error(`тема "${name}" объявляет роли, которых у пакета нет: ${unknown.join(", ")}.\nПримитивы (\`--gr-space-*\`, \`--gr-radius-*\`) и производные (\`-hover\`/\`-active\`) темой не задаются: первые от темы не зависят, вторые выводятся формулой.`);
|
|
292
|
+
const knownComponent = new Set(grComponentTokens.map((token) => token.name));
|
|
293
|
+
const unknownComponent = Object.keys(componentTokens).filter((token) => !knownComponent.has(token));
|
|
294
|
+
if (unknownComponent.length > 0) throw new Error(`тема "${name}" объявляет покомпонентные токены, которых у пакета нет: ${unknownComponent.join(", ")}`);
|
|
295
|
+
const missing = themeRoleNames().filter((role) => tokens[role] === void 0);
|
|
296
|
+
if (missing.length > 0) throw new Error(explainMissing(missing));
|
|
297
|
+
const theme = {
|
|
298
|
+
name,
|
|
299
|
+
selector,
|
|
300
|
+
tokens,
|
|
301
|
+
componentTokens,
|
|
302
|
+
css: renderThemeCss({
|
|
303
|
+
name,
|
|
304
|
+
selector,
|
|
305
|
+
tokens,
|
|
306
|
+
componentTokens
|
|
307
|
+
})
|
|
308
|
+
};
|
|
309
|
+
if (validate) {
|
|
310
|
+
const issues = validateTheme(theme);
|
|
311
|
+
if (issues.length > 0) throw new GrThemeError(name, issues);
|
|
312
|
+
}
|
|
313
|
+
return theme;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Тема поверх готовой: объявляем только то, что меняем.
|
|
317
|
+
*
|
|
318
|
+
* Роль, которую тема не объявила, наследуется не от «своей» базы, а из `:root`,
|
|
319
|
+
* то есть из светлой темы: тёмная тема с забытой ролью получает светлое пятно.
|
|
320
|
+
* Поэтому здесь тема достраивается до полного набора **всех** ролей пакета — и
|
|
321
|
+
* роль, добавленная пакетом завтра, приедет в неё сама, без правок потребителя.
|
|
322
|
+
*/
|
|
323
|
+
function extendTheme(options) {
|
|
324
|
+
const { name, base, tokens = {}, componentTokens = {}, selector = `[data-theme='${name}']`, validate = true } = options;
|
|
325
|
+
return buildTheme({
|
|
326
|
+
name,
|
|
327
|
+
tokens: {
|
|
328
|
+
...baseTokens(base),
|
|
329
|
+
...tokens
|
|
330
|
+
},
|
|
331
|
+
componentTokens,
|
|
332
|
+
selector,
|
|
333
|
+
validate,
|
|
334
|
+
explainMissing: (missing) => `база темы "${name}" не даёт ролей: ${missing.join(", ")}`
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Тема с нуля: без базы, все роли свои.
|
|
339
|
+
*
|
|
340
|
+
* «Без базы» — это не «ни от чего не наследуется»: в CSS так нельзя. Роль,
|
|
341
|
+
* которую тема не объявила, придёт из `:root`, а `:root` — это **светлая тема
|
|
342
|
+
* пакета**. Независимой тема становится ровно тогда, когда объявляет все роли
|
|
343
|
+
* сама, и здесь это проверяется: не хватает — сборка падает со списком, а не
|
|
344
|
+
* молча подмешивает чужую палитру.
|
|
345
|
+
*
|
|
346
|
+
* Цена честная и её видно: `extendTheme` получает новую роль пакета
|
|
347
|
+
* автоматически, а тема с нуля — упадёт на следующей сборке и потребует решения.
|
|
348
|
+
* Это и есть смысл выбора: полный контроль в обмен на сопровождение.
|
|
349
|
+
*/
|
|
350
|
+
function createTheme(options) {
|
|
351
|
+
const { name, tokens = {}, componentTokens = {}, selector = `[data-theme='${name}']`, validate = true } = options;
|
|
352
|
+
return buildTheme({
|
|
353
|
+
name,
|
|
354
|
+
tokens,
|
|
355
|
+
componentTokens,
|
|
356
|
+
selector,
|
|
357
|
+
validate,
|
|
358
|
+
explainMissing: (missing) => [
|
|
359
|
+
`тема "${name}" объявлена без базы, но не задаёт ${missing.length} ролей:`,
|
|
360
|
+
` ${missing.join(", ")}`,
|
|
361
|
+
"Незаявленная роль придёт не «ниоткуда», а из `:root` — то есть из светлой темы пакета.",
|
|
362
|
+
"Задайте их, либо соберите тему поверх готовой: `extendTheme({ base: 'light' | 'dark', tokens })`."
|
|
363
|
+
].join("\n")
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/theme/tone.ts
|
|
368
|
+
var AA_TEXT = 4.5;
|
|
369
|
+
/** Шаг подбора: 100 шагов по 1% дают точность, которой глаз не различает. */
|
|
370
|
+
var STEP = 1;
|
|
371
|
+
/**
|
|
372
|
+
* Насколько темнеет solid в hover и active.
|
|
373
|
+
*
|
|
374
|
+
* Обычные состояния пакет считает подмесом `--gr-fg` (`tokens/derived.json`), но
|
|
375
|
+
* solid — кнопочная заливка: в светлой теме `--gr-fg` тёмный, в тёмной светлый,
|
|
376
|
+
* и вторая уводила бы кнопку в сторону текста, то есть высветляла. Кнопка при
|
|
377
|
+
* нажатии темнеет в обеих темах, поэтому здесь подмес чёрного.
|
|
378
|
+
*
|
|
379
|
+
* Значения встроенных тем взяты из шкал палитры и лежат в диапазоне 81–99%:
|
|
380
|
+
* `tone` даёт согласованную семью, а не копию пакетных цветов.
|
|
381
|
+
*/
|
|
382
|
+
var SOLID_HOVER = 92;
|
|
383
|
+
var SOLID_ACTIVE = 84;
|
|
384
|
+
var GrToneError = class extends Error {
|
|
385
|
+
constructor(role, actual, expected) {
|
|
386
|
+
super(`${role}: не удалось выйти на контраст ${expected}:1, максимум ${actual.toFixed(2)}:1.\nВозьмите семя дальше от фона темы либо задайте роль явно.`);
|
|
387
|
+
this.name = "GrToneError";
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Роли, которые у этого тона есть в реестре. Лишняя роль — не «запас», а ошибка
|
|
392
|
+
* сборки («роли, которых у пакета нет»), поэтому набор берётся из данных.
|
|
393
|
+
*/
|
|
394
|
+
function rolesOf(name) {
|
|
395
|
+
const known = new Set(grThemeTokens.map((token) => token.name));
|
|
396
|
+
const prefix = `--gr-${name}`;
|
|
397
|
+
if (!known.has(prefix)) return [
|
|
398
|
+
"fill",
|
|
399
|
+
"solid",
|
|
400
|
+
"light",
|
|
401
|
+
"text"
|
|
402
|
+
];
|
|
403
|
+
return [
|
|
404
|
+
["fill", prefix],
|
|
405
|
+
["solid", `${prefix}-solid`],
|
|
406
|
+
["light", `${prefix}-light`],
|
|
407
|
+
["text", `${prefix}-text`]
|
|
408
|
+
].filter(([, role]) => known.has(role)).map(([kind]) => kind);
|
|
409
|
+
}
|
|
410
|
+
function baseTokensOf(base) {
|
|
411
|
+
if (typeof base === "string") return Object.fromEntries(grThemeTokens.map((token) => [token.name, token.values[base]]));
|
|
412
|
+
return "tokens" in base ? base.tokens : base;
|
|
413
|
+
}
|
|
414
|
+
function requireColor(tokens, role) {
|
|
415
|
+
const value = resolveRole(tokens, role);
|
|
416
|
+
if (!value) throw new Error(`тон выводится от темы, а роль ${role} в ней не цвет`);
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Сдвигает цвет к цели, пока не наберётся нужный контраст с эталоном.
|
|
421
|
+
* Возвращает первый годный шаг — самый близкий к исходному цвету.
|
|
422
|
+
*/
|
|
423
|
+
function shiftUntil(from, towards, against, expected, role) {
|
|
424
|
+
let best = {
|
|
425
|
+
value: from,
|
|
426
|
+
ratio: contrast(from, against)
|
|
427
|
+
};
|
|
428
|
+
for (let amount = 100; amount >= 0; amount -= STEP) {
|
|
429
|
+
const candidate = mixSrgb(from, towards, amount);
|
|
430
|
+
const ratio = contrast(candidate, against);
|
|
431
|
+
if (ratio >= expected) return candidate;
|
|
432
|
+
if (ratio > best.ratio) best = {
|
|
433
|
+
value: candidate,
|
|
434
|
+
ratio
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new GrToneError(role, best.ratio, expected);
|
|
438
|
+
}
|
|
439
|
+
/** Тёмная ли тема: фон темнее текста. Отсюда направление всех сдвигов. */
|
|
440
|
+
function isDark(tokens) {
|
|
441
|
+
const [bg, fg] = [requireColor(tokens, "--gr-bg"), requireColor(tokens, "--gr-fg")];
|
|
442
|
+
const luminanceOf = (hex) => parseHex(hex).reduce((sum, channel) => sum + channel, 0);
|
|
443
|
+
return luminanceOf(bg) < luminanceOf(fg);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Семья ролей тона из одного цвета.
|
|
447
|
+
*
|
|
448
|
+
* Без неё композиция снимает только объём работы: перекрасив `--gr-success`, вы
|
|
449
|
+
* оставляете `-light`, `-text` и `-solid*` от базовой темы, и они расходятся с
|
|
450
|
+
* новым тоном по контрасту — ловушка №3 из `docs/theming.md`. Правила вывода
|
|
451
|
+
* взяты оттуда же (§«Суффиксы ролей»), пороги проверяются на каждом шаге:
|
|
452
|
+
* не вышло — падаем с именем роли, а не отдаём нечитаемое.
|
|
453
|
+
*
|
|
454
|
+
* ```ts
|
|
455
|
+
* extendTheme({ name: 'ocean', base: 'dark', tokens: { ...tone('success', '#3ddc97', { base: 'dark' }) } })
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
function tone(name, seed, options) {
|
|
459
|
+
const tokens = baseTokensOf(options.base);
|
|
460
|
+
const roles = options.roles ?? rolesOf(name);
|
|
461
|
+
const bg = requireColor(tokens, "--gr-bg");
|
|
462
|
+
const fg = requireColor(tokens, "--gr-fg");
|
|
463
|
+
const card = requireColor(tokens, "--gr-card");
|
|
464
|
+
const dark = isDark(tokens);
|
|
465
|
+
const result = {};
|
|
466
|
+
const prefix = `--gr-${name}`;
|
|
467
|
+
if (roles.includes("fill")) {
|
|
468
|
+
result[prefix] = seed;
|
|
469
|
+
const candidates = [fg, bg];
|
|
470
|
+
const scoreOf = (candidate) => Math.min(contrast(candidate, seed), contrast(candidate, mixSrgb(seed, fg, 92)), contrast(candidate, mixSrgb(seed, fg, 84)));
|
|
471
|
+
const [best] = [...candidates].sort((left, right) => scoreOf(right) - scoreOf(left));
|
|
472
|
+
if (scoreOf(best) < AA_TEXT) throw new GrToneError(`${prefix}-fg`, scoreOf(best), AA_TEXT);
|
|
473
|
+
result[`${prefix}-fg`] = best;
|
|
474
|
+
}
|
|
475
|
+
if (roles.includes("solid")) {
|
|
476
|
+
const solidFg = dark ? fg : "#ffffff";
|
|
477
|
+
const solid = shiftUntil(seed, "#000000", solidFg, AA_TEXT, `${prefix}-solid`);
|
|
478
|
+
result[`${prefix}-solid`] = solid;
|
|
479
|
+
result[`${prefix}-solid-fg`] = solidFg;
|
|
480
|
+
for (const [suffix, amount] of [["-solid-hover", SOLID_HOVER], ["-solid-active", SOLID_ACTIVE]]) {
|
|
481
|
+
const state = mixSrgb(solid, "#000000", amount);
|
|
482
|
+
const ratio = contrast(solidFg, state);
|
|
483
|
+
if (ratio < AA_TEXT) throw new GrToneError(`${prefix}${suffix}`, ratio, AA_TEXT);
|
|
484
|
+
result[`${prefix}${suffix}`] = state;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (roles.includes("light")) result[`${prefix}-light`] = mixSrgb(seed, card, dark ? 22 : 14);
|
|
488
|
+
if (roles.includes("text")) {
|
|
489
|
+
const light = result[`${prefix}-light`] ?? mixSrgb(seed, card, dark ? 22 : 14);
|
|
490
|
+
let value = seed;
|
|
491
|
+
for (const surface of [
|
|
492
|
+
light,
|
|
493
|
+
bg,
|
|
494
|
+
card
|
|
495
|
+
]) value = shiftUntil(value, fg, surface, AA_TEXT, `${prefix}-text`);
|
|
496
|
+
result[`${prefix}-text`] = value;
|
|
497
|
+
}
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
501
|
+
export { GrThemeError, GrToneError, contrast, createTheme, deltaE, extendTheme, mixSrgb, renderThemeCss, resolveRole, themeRoleNames, tone, validateTheme };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.js","names":[],"sources":["../src/theme/color.ts","../src/theme/roles.ts","../src/theme/validate.ts","../src/theme/extendTheme.ts","../src/theme/tone.ts"],"sourcesContent":["/**\n * Цветовая математика пакета — один источник на всех.\n *\n * Одну и ту же формулу считают три разных потребителя: генератор токенов\n * (фолбэки `@supports not (color-mix)`), композиция тем (`extendTheme`, `tone`)\n * и гейты контраста. Расхождение здесь не абстрактно: фолбэки уже жили\n * отдельной копией из сорока хексов, которые обязаны были совпадать с живым\n * `color-mix` до байта, и не совпадали.\n *\n * Модуль читает и генератор (`scripts/generate-tokens.mjs`, запускается\n * `node --experimental-strip-types`), поэтому синтаксис здесь только стираемый:\n * ни `enum`, ни `namespace`, ни параметров-свойств в конструкторах.\n */\n\nexport type Rgb = [number, number, number]\n\nexport function parseHex(value: string): Rgb {\n const match = value.trim().match(/^#([\\da-f]{3}|[\\da-f]{6})$/i)\n\n if (!match)\n throw new Error(`ожидался hex-цвет, получено: ${value}`)\n\n const hex = match[1].length === 3\n ? match[1].split('').map(char => char + char).join('')\n : match[1]\n\n return [0, 2, 4].map(offset => Number.parseInt(hex.slice(offset, offset + 2), 16)) as Rgb\n}\n\nexport function toHex(channels: Rgb): string {\n return `#${channels\n .map(channel => Math.round(Math.min(255, Math.max(0, channel))).toString(16).padStart(2, '0'))\n .join('')}`\n}\n\n/**\n * `color-mix(in srgb, base <amount>%, other)` для непрозрачных цветов — это\n * линейная интерполяция в gamma-кодированном sRGB, то есть ровно то, что делает\n * браузер. Поэтому посчитанный здесь фолбэк совпадает с живым `color-mix`.\n */\nexport function mixSrgb(baseHex: string, otherHex: string, amount: number): string {\n const base = parseHex(baseHex)\n const other = parseHex(otherHex)\n const ratio = amount / 100\n\n return toHex(base.map((channel, index) => channel * ratio + other[index] * (1 - ratio)) as Rgb)\n}\n\nexport function luminance(color: string | Rgb): number {\n const channels = (typeof color === 'string' ? parseHex(color) : color).map((channel) => {\n const normalized = channel / 255\n\n return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4\n })\n\n return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]\n}\n\n/** Отношение контраста по WCAG 2.1: от 1 (одинаковые) до 21 (чёрное на белом). */\nexport function contrast(foreground: string | Rgb, background: string | Rgb): number {\n const first = luminance(foreground)\n const second = luminance(background)\n const [lighter, darker] = first > second ? [first, second] : [second, first]\n\n return (lighter + 0.05) / (darker + 0.05)\n}\n\nfunction toLab(color: string | Rgb): [number, number, number] {\n const [red, green, blue] = (typeof color === 'string' ? parseHex(color) : color).map((channel) => {\n const normalized = channel / 255\n\n return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4\n })\n\n // sRGB → XYZ (D65) → Lab, точки белого из CIE.\n const x = (red * 0.4124 + green * 0.3576 + blue * 0.1805) / 0.95047\n const y = red * 0.2126 + green * 0.7152 + blue * 0.0722\n const z = (red * 0.0193 + green * 0.1192 + blue * 0.9505) / 1.08883\n\n const f = (value: number): number => (value > 0.008856 ? value ** (1 / 3) : 7.787 * value + 16 / 116)\n const [fx, fy, fz] = [f(x), f(y), f(z)]\n\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)]\n}\n\n/**\n * Воспринимаемое расстояние между цветами (ΔE, CIE76).\n *\n * Контраст отвечает на вопрос «читается ли текст», а этот — на вопрос\n * «различит ли человек два тона рядом». Порог заметности около 2.3; всё, что\n * ниже, для глаза один цвет, каким бы разным ни выглядел hex.\n */\nexport function deltaE(first: string | Rgb, second: string | Rgb): number {\n const [firstLab, secondLab] = [toLab(first), toLab(second)]\n\n return Math.hypot(...firstLab.map((value, index) => value - secondLab[index]))\n}\n\n/** Цвет — это hex, а не `var()`/`rgb()`: производные и контраст считаются только по нему. */\nexport function isHex(value: string): boolean {\n return /^#(?:[\\da-f]{3}|[\\da-f]{6})$/i.test(value.trim())\n}\n","import { grThemeTokens } from '../tokens/generated'\nimport { isHex } from './color'\n\n/** Значения ролей темы: `--gr-bg` → `#041e2b`. */\nexport type GrThemeTokens = Record<string, string>\n\n/** Роли, известные пакету, в порядке объявления. */\nexport function themeRoleNames(): string[] {\n return grThemeTokens.map(token => token.name)\n}\n\n/**\n * Значение роли, годное для арифметики.\n *\n * Роль вправе ссылаться на другую (`--gr-invalid-brd: var(--gr-danger)`) — такую\n * ссылку разворачиваем, иначе фолбэк и проверки считать не из чего. Всё, что не\n * разворачивается в hex (`rgb(… / .45)` у подложки, `var()` за пределы темы),\n * возвращается как `null`: посчитать нельзя, и выдумывать не надо.\n *\n * Цикл рвём с ошибкой: тема потребителя вправе ошибиться, и упасть надо с\n * именами ролей, а не переполнением стека.\n */\nexport function resolveRole(tokens: GrThemeTokens, name: string, seen: string[] = []): string | null {\n const value = tokens[name]\n if (value === undefined) return null\n if (isHex(value)) return value\n\n const reference = value.trim().match(/^var\\(\\s*(--[\\w-]+)\\s*\\)$/)\n if (!reference) return null\n\n if (seen.includes(name))\n throw new Error(`роль ${name} ссылается сама на себя: ${[...seen, name].join(' → ')}`)\n\n return resolveRole(tokens, reference[1], [...seen, name])\n}\n","import { grDerivedTokens, grThemeTokens } from '../tokens/generated'\nimport { contrast, deltaE, mixSrgb } from './color'\nimport type { GrTheme } from './extendTheme'\nimport { resolveRole, type GrThemeTokens } from './roles'\n\nexport interface GrThemeIssue {\n kind: 'contrast' | 'distance'\n /** Роль, из-за которой всё встало. */\n token: string\n /** На чём проверяли: поверхность либо второй тон. */\n against: string\n actual: number\n expected: number\n message: string\n}\n\n/**\n * AA для обычного текста.\n *\n * Порога для самой заливки здесь нет намеренно: `--gr-success` в светлой теме\n * даёт 2.42:1 к фону, и это не дефект — информацию бейдж несёт текстом `-fg`\n * поверх заливки, а не самой заливкой. Правило «≥3:1 к фону» уронило бы\n * встроенные темы пакета, то есть проверяло бы не то, о чём договаривались.\n */\nconst AA_TEXT = 4.5\n/** Порог заметности ΔE: ниже — для глаза один цвет, каким бы разным ни был hex. */\nconst NOTICEABLE = 2.3\n\n/**\n * Тона, выведенные из данных, а не списком: тон — это роль, у которой есть\n * пара `-fg`. Новый тон пакета попадает под проверки сам.\n */\nfunction toneNames(): string[] {\n const names = new Set(grThemeTokens.map(token => token.name))\n\n return [...names]\n .filter(name => names.has(`${name}-fg`))\n .map(name => name.replace('--gr-', ''))\n}\n\nfunction derivedOf(role: string): { amount: number, mixWith: string }[] {\n return grDerivedTokens\n .filter(token => token.base === role)\n .map(token => ({ amount: token.amount, mixWith: token.mixWith }))\n}\n\nexport function validateTheme(theme: GrTheme | { name: string, tokens: GrThemeTokens }): GrThemeIssue[] {\n const { tokens } = theme\n const issues: GrThemeIssue[] = []\n\n const value = (role: string): string | null => resolveRole(tokens, role)\n\n function requireContrast(token: string, against: string, expected: number, what: string): void {\n const [foreground, background] = [value(token), value(against)]\n // Роль не разворачивается в цвет (например `rgb(… / .45)` у подложки) —\n // считать нечего: молчаливая «проверка» хуже её отсутствия.\n if (!foreground || !background) return\n\n const actual = contrast(foreground, background)\n if (actual >= expected) return\n\n issues.push({\n kind: 'contrast',\n token,\n against,\n actual: Math.round(actual * 100) / 100,\n expected,\n message: `${what}: ${token} на ${against} даёт ${actual.toFixed(2)}:1 при пороге ${expected}:1`,\n })\n }\n\n /** Заливка в hover/active уходит темнее — текст на ней может провалиться именно там. */\n function requireContrastWithStates(fgRole: string, fillRole: string, what: string): void {\n requireContrast(fgRole, fillRole, AA_TEXT, what)\n\n const fill = value(fillRole)\n const foreground = value(fgRole)\n if (!fill || !foreground) return\n\n for (const { amount, mixWith } of derivedOf(fillRole)) {\n const mixed = value(mixWith)\n if (!mixed) continue\n\n const state = contrast(foreground, mixSrgb(fill, mixed, amount))\n if (state >= AA_TEXT) continue\n\n issues.push({\n kind: 'contrast',\n token: fgRole,\n against: `${fillRole} (${amount}%)`,\n actual: Math.round(state * 100) / 100,\n expected: AA_TEXT,\n message: `${what}: ${fgRole} на состоянии ${fillRole} (${amount}%) даёт ${state.toFixed(2)}:1 при пороге ${AA_TEXT}:1`,\n })\n }\n }\n\n // Основной текст — только на поверхностях БЕЗ собственной пары `-fg`.\n // У карточки, поповера и сайдбара она есть, и проверяется ниже вместе с\n // тонами: сайдбар вправе быть инвертированным (тёмная панель в светлой теме),\n // и требовать на нём общий `--gr-fg` значило бы запретить это.\n const withOwnFg = new Set(toneNames().map(tone => `--gr-${tone}`))\n\n for (const surface of ['--gr-bg', '--gr-card', '--gr-popover', '--gr-sidebar']) {\n if (withOwnFg.has(surface)) continue\n\n requireContrast('--gr-fg', surface, AA_TEXT, 'основной текст')\n }\n\n for (const surface of ['--gr-bg', '--gr-card', '--gr-muted', '--gr-secondary'])\n requireContrast('--gr-muted-fg', surface, AA_TEXT, 'вторичный текст')\n\n for (const tone of toneNames()) {\n const fill = `--gr-${tone}`\n\n requireContrastWithStates(`${fill}-fg`, fill, `текст на заливке ${tone}`)\n\n // Состояния solid — свои роли темы, а не производные формулой, поэтому\n // текст на них проверяется отдельно: у темы с нуля это единственная защита.\n for (const state of ['-hover', '-active']) {\n if (tokens[`${fill}${state}`] === undefined) continue\n\n requireContrast(`${fill}-fg`, `${fill}${state}`, AA_TEXT, `текст на состоянии ${tone}${state}`)\n }\n\n if (tokens[`${fill}-text`] !== undefined) {\n requireContrast(`${fill}-text`, `${fill}-light`, AA_TEXT, `текст тона ${tone}`)\n requireContrast(`${fill}-text`, '--gr-bg', AA_TEXT, `текст тона ${tone}`)\n requireContrast(`${fill}-text`, '--gr-card', AA_TEXT, `текст тона ${tone}`)\n }\n }\n\n // Ловушка №5: два тона в двух шагах друг от друга. Пакет наступил на это сам —\n // `info` был индиго рядом с `primary`, и ссылки обоих тонов красились одинаково.\n const distinct = ['primary', 'success', 'warning', 'danger', 'info', 'slate', 'azure']\n\n for (let i = 0; i < distinct.length; i += 1) {\n for (let j = i + 1; j < distinct.length; j += 1) {\n const [first, second] = [`--gr-${distinct[i]}`, `--gr-${distinct[j]}`]\n const [firstValue, secondValue] = [value(first), value(second)]\n if (!firstValue || !secondValue) continue\n\n const distance = deltaE(firstValue, secondValue)\n if (distance >= NOTICEABLE) continue\n\n issues.push({\n kind: 'distance',\n token: first,\n against: second,\n actual: Math.round(distance * 100) / 100,\n expected: NOTICEABLE,\n message: `тона неразличимы: ΔE между ${first} и ${second} равно ${distance.toFixed(2)} при пороге ${NOTICEABLE}`,\n })\n }\n }\n\n return issues\n}\n","import { grComponentTokens, grDerivedTokens, grThemeTokens } from '../tokens/generated'\nimport type { GrThemeName } from '../tokens/types'\nimport { mixSrgb } from './color'\nimport { resolveRole, themeRoleNames, type GrThemeTokens } from './roles'\nimport { validateTheme, type GrThemeIssue } from './validate'\n\nexport interface GrTheme {\n name: string\n /** Селектор, под которым тема объявлена: по умолчанию `[data-theme='<name>']`. */\n selector: string\n /** Полный набор ролей: своё поверх базы. */\n tokens: GrThemeTokens\n /** Покомпонентные токены темы, если заданы. */\n componentTokens: GrThemeTokens\n /** Готовый CSS, включая фолбэк производных для браузеров без `color-mix`. */\n css: string\n}\n\nexport interface ExtendThemeOptions {\n name: string\n /** Готовая тема пакета, чужая тема или просто набор ролей. */\n base: GrThemeName | GrTheme | GrThemeTokens\n selector?: string\n /** Что меняем. Всё, чего здесь нет, приходит из базы. */\n tokens?: GrThemeTokens\n /**\n * Покомпонентные токены (`--gr-button-primary-bg`) — у них своя природа:\n * пакет держит для них отдельный слой и **не** наследует их между темами.\n * Поэтому они не достраиваются из базы, а просто едут в тот же селектор:\n * заданы — переопределены, не заданы — работает дефолт компонента.\n */\n componentTokens?: GrThemeTokens\n /**\n * Проверка контраста и различимости тонов перед выдачей. Включена: тема,\n * которая не читается, — это дефект, и узнать о нём на сборке дешевле, чем от\n * пользователя. Выключать осознанно, для заведомо декоративных тем.\n */\n validate?: boolean\n}\n\nexport class GrThemeError extends Error {\n issues: GrThemeIssue[]\n\n constructor(name: string, issues: GrThemeIssue[]) {\n super([\n `тема \"${name}\" не проходит проверку:`,\n ...issues.map(issue => ` • ${issue.message}`),\n ].join('\\n'))\n\n this.name = 'GrThemeError'\n this.issues = issues\n }\n}\n\nfunction baseTokens(base: ExtendThemeOptions['base']): GrThemeTokens {\n if (typeof base === 'string') {\n const known = ['light', 'dark']\n if (!known.includes(base))\n throw new Error(`неизвестная базовая тема \"${base}\"; встроенные: ${known.join(', ')}`)\n\n return Object.fromEntries(grThemeTokens.map(token => [token.name, token.values[base]]))\n }\n\n return 'tokens' in base ? { ...(base as GrTheme).tokens } : { ...base }\n}\n\nfunction renderDeclarations(tokens: GrThemeTokens, indent: string): string[] {\n return grThemeTokens.map(token => `${indent}${token.name}: ${tokens[token.name]};`)\n}\n\nfunction renderComponentDeclarations(tokens: GrThemeTokens, indent: string): string[] {\n const names = Object.keys(tokens)\n if (names.length === 0) return []\n\n return ['', `${indent}/* Покомпонентные токены темы. */`, ...names.map(name => `${indent}${name}: ${tokens[name]};`)]\n}\n\n/**\n * Фолбэк производных состояний для браузеров без `color-mix`.\n *\n * Без него тема наследует фолбэки из `:root`, то есть чужие: hover у кастомной\n * тёмной темы окажется от светлой. Формула та же, что в `tokens.css`, — считает\n * её общая математика (`color.ts`), поэтому разойтись с живым `color-mix` не\n * может.\n */\nfunction renderFallback(tokens: GrThemeTokens, selector: string): string[] {\n const lines = [\n '',\n '@supports not (color: color-mix(in srgb, #000 50%, #fff)) {',\n ` ${selector} {`,\n ]\n\n for (const token of grDerivedTokens) {\n const base = resolveRole(tokens, token.base)\n const mixWith = resolveRole(tokens, token.mixWith)\n\n // Роль ссылается не на цвет (например на `var()` вне темы) — фолбэк для неё\n // посчитать нечем, и выдумывать значение хуже, чем его не давать.\n if (!base || !mixWith) continue\n\n lines.push(` ${token.name}: ${mixSrgb(base, mixWith, token.amount)};`)\n }\n\n lines.push(' }', '}')\n\n return lines\n}\n\nexport function renderThemeCss(theme: Pick<GrTheme, 'name' | 'selector' | 'tokens'> & Partial<Pick<GrTheme, 'componentTokens'>>): string {\n return [\n `/* Тема \"${theme.name}\" собрана из @feugene/granularity/theme — правки здесь потеряются. */`,\n `${theme.selector} {`,\n ...renderDeclarations(theme.tokens, ' '),\n ...renderComponentDeclarations(theme.componentTokens ?? {}, ' '),\n '}',\n ...renderFallback(theme.tokens, theme.selector),\n '',\n ].join('\\n')\n}\n\ninterface BuildOptions {\n name: string\n tokens: GrThemeTokens\n componentTokens: GrThemeTokens\n selector: string\n validate: boolean\n /** Что сказать, если ролей не хватает: у темы с базой и без базы причины разные. */\n explainMissing: (missing: string[]) => string\n}\n\nfunction buildTheme(options: BuildOptions): GrTheme {\n const { name, tokens, componentTokens, selector, validate, explainMissing } = options\n\n const known = new Set(themeRoleNames())\n const unknown = Object.keys(tokens).filter(role => !known.has(role))\n\n if (unknown.length > 0) {\n throw new Error(\n `тема \"${name}\" объявляет роли, которых у пакета нет: ${unknown.join(', ')}.\\n`\n + 'Примитивы (`--gr-space-*`, `--gr-radius-*`) и производные (`-hover`/`-active`) темой не задаются: '\n + 'первые от темы не зависят, вторые выводятся формулой.',\n )\n }\n\n const knownComponent = new Set(grComponentTokens.map(token => token.name))\n const unknownComponent = Object.keys(componentTokens).filter(token => !knownComponent.has(token))\n\n if (unknownComponent.length > 0)\n throw new Error(`тема \"${name}\" объявляет покомпонентные токены, которых у пакета нет: ${unknownComponent.join(', ')}`)\n\n const missing = themeRoleNames().filter(role => tokens[role] === undefined)\n if (missing.length > 0) throw new Error(explainMissing(missing))\n\n const theme: GrTheme = {\n name,\n selector,\n tokens,\n componentTokens,\n css: renderThemeCss({ name, selector, tokens, componentTokens }),\n }\n\n if (validate) {\n const issues = validateTheme(theme)\n if (issues.length > 0) throw new GrThemeError(name, issues)\n }\n\n return theme\n}\n\n/**\n * Тема поверх готовой: объявляем только то, что меняем.\n *\n * Роль, которую тема не объявила, наследуется не от «своей» базы, а из `:root`,\n * то есть из светлой темы: тёмная тема с забытой ролью получает светлое пятно.\n * Поэтому здесь тема достраивается до полного набора **всех** ролей пакета — и\n * роль, добавленная пакетом завтра, приедет в неё сама, без правок потребителя.\n */\nexport function extendTheme(options: ExtendThemeOptions): GrTheme {\n const {\n name,\n base,\n tokens = {},\n componentTokens = {},\n selector = `[data-theme='${name}']`,\n validate = true,\n } = options\n\n return buildTheme({\n name,\n tokens: { ...baseTokens(base), ...tokens },\n componentTokens,\n selector,\n validate,\n explainMissing: missing => `база темы \"${name}\" не даёт ролей: ${missing.join(', ')}`,\n })\n}\n\n/**\n * Тема с нуля: без базы, все роли свои.\n *\n * «Без базы» — это не «ни от чего не наследуется»: в CSS так нельзя. Роль,\n * которую тема не объявила, придёт из `:root`, а `:root` — это **светлая тема\n * пакета**. Независимой тема становится ровно тогда, когда объявляет все роли\n * сама, и здесь это проверяется: не хватает — сборка падает со списком, а не\n * молча подмешивает чужую палитру.\n *\n * Цена честная и её видно: `extendTheme` получает новую роль пакета\n * автоматически, а тема с нуля — упадёт на следующей сборке и потребует решения.\n * Это и есть смысл выбора: полный контроль в обмен на сопровождение.\n */\nexport function createTheme(options: Omit<ExtendThemeOptions, 'base'>): GrTheme {\n const {\n name,\n tokens = {},\n componentTokens = {},\n selector = `[data-theme='${name}']`,\n validate = true,\n } = options\n\n return buildTheme({\n name,\n tokens,\n componentTokens,\n selector,\n validate,\n explainMissing: missing => [\n `тема \"${name}\" объявлена без базы, но не задаёт ${missing.length} ролей:`,\n ` ${missing.join(', ')}`,\n 'Незаявленная роль придёт не «ниоткуда», а из `:root` — то есть из светлой темы пакета.',\n 'Задайте их, либо соберите тему поверх готовой: `extendTheme({ base: \\'light\\' | \\'dark\\', tokens })`.',\n ].join('\\n'),\n })\n}\n","import { grThemeTokens } from '../tokens/generated'\nimport type { GrThemeName } from '../tokens/types'\nimport { contrast, mixSrgb, parseHex } from './color'\nimport { resolveRole, type GrThemeTokens } from './roles'\n\nexport type ToneRole = 'fill' | 'solid' | 'light' | 'text'\n\nexport interface ToneOptions {\n /** Тема, в которой тон будет жить: от неё зависят фон, текст и направление сдвигов. */\n base: GrThemeName | { tokens: GrThemeTokens } | GrThemeTokens\n /**\n * Что выводить. По умолчанию — ровно те роли, которые у этого тона есть **в\n * реестре пакета**: у `--gr-accent` это только `-fg`, у `--gr-primary` нет\n * `-light`. Для имени, которого в реестре нет, выводится полная семья.\n */\n roles?: ToneRole[]\n}\n\nconst AA_TEXT = 4.5\n/** Шаг подбора: 100 шагов по 1% дают точность, которой глаз не различает. */\nconst STEP = 1\n/**\n * Насколько темнеет solid в hover и active.\n *\n * Обычные состояния пакет считает подмесом `--gr-fg` (`tokens/derived.json`), но\n * solid — кнопочная заливка: в светлой теме `--gr-fg` тёмный, в тёмной светлый,\n * и вторая уводила бы кнопку в сторону текста, то есть высветляла. Кнопка при\n * нажатии темнеет в обеих темах, поэтому здесь подмес чёрного.\n *\n * Значения встроенных тем взяты из шкал палитры и лежат в диапазоне 81–99%:\n * `tone` даёт согласованную семью, а не копию пакетных цветов.\n */\nconst SOLID_HOVER = 92\nconst SOLID_ACTIVE = 84\n\nexport class GrToneError extends Error {\n constructor(role: string, actual: number, expected: number) {\n super(\n `${role}: не удалось выйти на контраст ${expected}:1, максимум ${actual.toFixed(2)}:1.\\n`\n + 'Возьмите семя дальше от фона темы либо задайте роль явно.',\n )\n\n this.name = 'GrToneError'\n }\n}\n\n/**\n * Роли, которые у этого тона есть в реестре. Лишняя роль — не «запас», а ошибка\n * сборки («роли, которых у пакета нет»), поэтому набор берётся из данных.\n */\nfunction rolesOf(name: string): ToneRole[] {\n const known = new Set(grThemeTokens.map(token => token.name))\n const prefix = `--gr-${name}`\n\n if (!known.has(prefix)) return ['fill', 'solid', 'light', 'text']\n\n return ([\n ['fill', prefix],\n ['solid', `${prefix}-solid`],\n ['light', `${prefix}-light`],\n ['text', `${prefix}-text`],\n ] as const)\n .filter(([, role]) => known.has(role))\n .map(([kind]) => kind)\n}\n\nfunction baseTokensOf(base: ToneOptions['base']): GrThemeTokens {\n if (typeof base === 'string')\n return Object.fromEntries(grThemeTokens.map(token => [token.name, token.values[base]]))\n\n return 'tokens' in base ? (base as { tokens: GrThemeTokens }).tokens : base\n}\n\nfunction requireColor(tokens: GrThemeTokens, role: string): string {\n const value = resolveRole(tokens, role)\n if (!value) throw new Error(`тон выводится от темы, а роль ${role} в ней не цвет`)\n\n return value\n}\n\n/**\n * Сдвигает цвет к цели, пока не наберётся нужный контраст с эталоном.\n * Возвращает первый годный шаг — самый близкий к исходному цвету.\n */\nfunction shiftUntil(from: string, towards: string, against: string, expected: number, role: string): string {\n let best = { value: from, ratio: contrast(from, against) }\n\n for (let amount = 100; amount >= 0; amount -= STEP) {\n const candidate = mixSrgb(from, towards, amount)\n const ratio = contrast(candidate, against)\n\n if (ratio >= expected) return candidate\n if (ratio > best.ratio) best = { value: candidate, ratio }\n }\n\n throw new GrToneError(role, best.ratio, expected)\n}\n\n/** Тёмная ли тема: фон темнее текста. Отсюда направление всех сдвигов. */\nfunction isDark(tokens: GrThemeTokens): boolean {\n const [bg, fg] = [requireColor(tokens, '--gr-bg'), requireColor(tokens, '--gr-fg')]\n const luminanceOf = (hex: string): number => parseHex(hex).reduce((sum, channel) => sum + channel, 0)\n\n return luminanceOf(bg) < luminanceOf(fg)\n}\n\n/**\n * Семья ролей тона из одного цвета.\n *\n * Без неё композиция снимает только объём работы: перекрасив `--gr-success`, вы\n * оставляете `-light`, `-text` и `-solid*` от базовой темы, и они расходятся с\n * новым тоном по контрасту — ловушка №3 из `docs/theming.md`. Правила вывода\n * взяты оттуда же (§«Суффиксы ролей»), пороги проверяются на каждом шаге:\n * не вышло — падаем с именем роли, а не отдаём нечитаемое.\n *\n * ```ts\n * extendTheme({ name: 'ocean', base: 'dark', tokens: { ...tone('success', '#3ddc97', { base: 'dark' }) } })\n * ```\n */\nexport function tone(name: string, seed: string, options: ToneOptions): GrThemeTokens {\n const tokens = baseTokensOf(options.base)\n const roles = options.roles ?? rolesOf(name)\n\n const bg = requireColor(tokens, '--gr-bg')\n const fg = requireColor(tokens, '--gr-fg')\n const card = requireColor(tokens, '--gr-card')\n const dark = isDark(tokens)\n\n const result: GrThemeTokens = {}\n const prefix = `--gr-${name}`\n\n if (roles.includes('fill')) {\n result[prefix] = seed\n\n // Текст на заливке — тот из полюсов темы, что читается лучше. Проверяем\n // не только базу: hover/active уводят заливку к `--gr-fg`, и провалиться\n // текст может именно там (ловушка №2).\n const candidates = [fg, bg]\n const scoreOf = (candidate: string): number => Math.min(\n contrast(candidate, seed),\n contrast(candidate, mixSrgb(seed, fg, 92)),\n contrast(candidate, mixSrgb(seed, fg, 84)),\n )\n\n const [best] = [...candidates].sort((left, right) => scoreOf(right) - scoreOf(left))\n if (scoreOf(best) < AA_TEXT) throw new GrToneError(`${prefix}-fg`, scoreOf(best), AA_TEXT)\n\n result[`${prefix}-fg`] = best\n }\n\n if (roles.includes('solid')) {\n // Solid — кнопочный вес, и он одинаков в обеих темах: тёмная заливка со\n // светлым текстом. Это не симметрия ради симметрии — кнопки всех тонов\n // обязаны выглядеть одной семьёй (docs/theming.md, «Зачем -solid»), а\n // светлая кнопка в тёмной теме читалась бы как другой компонент.\n // Отсюда же направление состояний: заливка темнеет при наведении и нажатии.\n const solidFg = dark ? fg : '#ffffff'\n\n const solid = shiftUntil(seed, '#000000', solidFg, AA_TEXT, `${prefix}-solid`)\n\n result[`${prefix}-solid`] = solid\n result[`${prefix}-solid-fg`] = solidFg\n\n // Состояния solid — темовые роли, а не производные: `color-mix` их не\n // считает, и без них тема с нуля не соберётся вовсе.\n for (const [suffix, amount] of [['-solid-hover', SOLID_HOVER], ['-solid-active', SOLID_ACTIVE]] as const) {\n const state = mixSrgb(solid, '#000000', amount)\n const ratio = contrast(solidFg, state)\n\n if (ratio < AA_TEXT) throw new GrToneError(`${prefix}${suffix}`, ratio, AA_TEXT)\n\n result[`${prefix}${suffix}`] = state\n }\n }\n\n if (roles.includes('light')) {\n // Мягкая подложка: тон, подмешанный в поверхность карточки.\n result[`${prefix}-light`] = mixSrgb(seed, card, dark ? 22 : 14)\n }\n\n if (roles.includes('text')) {\n const light = result[`${prefix}-light`] ?? mixSrgb(seed, card, dark ? 22 : 14)\n\n // Текст тона живёт и на мягкой подложке, и на фоне страницы, и на карточке —\n // сдвигаем до худшего из трёх.\n let value = seed\n for (const surface of [light, bg, card])\n value = shiftUntil(value, fg, surface, AA_TEXT, `${prefix}-text`)\n\n result[`${prefix}-text`] = value\n }\n\n return result\n}\n"],"mappings":"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"}
|