@cloud-ru/ds-theme 1.0.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/CHANGELOG.md +8 -0
- package/LICENSE +201 -0
- package/README.md +365 -0
- package/dist/cjs/components/ChildThemeProvider/ChildThemeProvider.d.ts +24 -0
- package/dist/cjs/components/ChildThemeProvider/ChildThemeProvider.js +25 -0
- package/dist/cjs/components/ChildThemeProvider/index.d.ts +1 -0
- package/dist/cjs/components/ChildThemeProvider/index.js +17 -0
- package/dist/cjs/components/RootThemeProvider/RootThemeProvider.d.ts +49 -0
- package/dist/cjs/components/RootThemeProvider/RootThemeProvider.js +30 -0
- package/dist/cjs/components/RootThemeProvider/index.d.ts +1 -0
- package/dist/cjs/components/RootThemeProvider/index.js +17 -0
- package/dist/cjs/components/ThemeProvider/ThemeProvider.d.ts +28 -0
- package/dist/cjs/components/ThemeProvider/ThemeProvider.js +16 -0
- package/dist/cjs/components/ThemeProvider/index.d.ts +1 -0
- package/dist/cjs/components/ThemeProvider/index.js +17 -0
- package/dist/cjs/components/ThemeScope/ThemeScope.d.ts +32 -0
- package/dist/cjs/components/ThemeScope/ThemeScope.js +65 -0
- package/dist/cjs/components/ThemeScope/index.d.ts +1 -0
- package/dist/cjs/components/ThemeScope/index.js +17 -0
- package/dist/cjs/components/index.d.ts +3 -0
- package/dist/cjs/components/index.js +19 -0
- package/dist/cjs/constants/appearance.d.ts +22 -0
- package/dist/cjs/constants/appearance.js +27 -0
- package/dist/cjs/constants/colorScheme.d.ts +17 -0
- package/dist/cjs/constants/colorScheme.js +23 -0
- package/dist/cjs/constants/index.d.ts +2 -0
- package/dist/cjs/constants/index.js +18 -0
- package/dist/cjs/context/appearanceContext.d.ts +20 -0
- package/dist/cjs/context/appearanceContext.js +22 -0
- package/dist/cjs/context/index.d.ts +4 -0
- package/dist/cjs/context/index.js +7 -0
- package/dist/cjs/context/themeContext.d.ts +12 -0
- package/dist/cjs/context/themeContext.js +15 -0
- package/dist/cjs/hooks/index.d.ts +4 -0
- package/dist/cjs/hooks/index.js +20 -0
- package/dist/cjs/hooks/useApplyCustomTheme.d.ts +27 -0
- package/dist/cjs/hooks/useApplyCustomTheme.js +39 -0
- package/dist/cjs/hooks/useColorScheme.d.ts +37 -0
- package/dist/cjs/hooks/useColorScheme.js +80 -0
- package/dist/cjs/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
- package/dist/cjs/hooks/useIsomorphicLayoutEffect.js +16 -0
- package/dist/cjs/hooks/useThemeClassnames.d.ts +11 -0
- package/dist/cjs/hooks/useThemeClassnames.js +18 -0
- package/dist/cjs/hooks/useThemeConfig.d.ts +24 -0
- package/dist/cjs/hooks/useThemeConfig.js +15 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +23 -0
- package/dist/cjs/keys.d.ts +4 -0
- package/dist/cjs/keys.js +19 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/ssr.d.ts +15 -0
- package/dist/cjs/ssr.js +38 -0
- package/dist/cjs/store/globalStore.d.ts +29 -0
- package/dist/cjs/store/globalStore.js +72 -0
- package/dist/cjs/store/index.d.ts +1 -0
- package/dist/cjs/store/index.js +17 -0
- package/dist/cjs/types/appearance.d.ts +23 -0
- package/dist/cjs/types/appearance.js +3 -0
- package/dist/cjs/types/colorScheme.d.ts +5 -0
- package/dist/cjs/types/colorScheme.js +2 -0
- package/dist/cjs/types/index.d.ts +2 -0
- package/dist/cjs/types/index.js +18 -0
- package/dist/cjs/utils/colorSchemeFromHeaders.d.ts +22 -0
- package/dist/cjs/utils/colorSchemeFromHeaders.js +44 -0
- package/dist/cjs/utils/colorSchemeStorage.d.ts +35 -0
- package/dist/cjs/utils/colorSchemeStorage.js +85 -0
- package/dist/cjs/utils/customTheme/buildBrandPaletteCss.d.ts +11 -0
- package/dist/cjs/utils/customTheme/buildBrandPaletteCss.js +35 -0
- package/dist/cjs/utils/customTheme/buildBrandPaletteVars.d.ts +11 -0
- package/dist/cjs/utils/customTheme/buildBrandPaletteVars.js +42 -0
- package/dist/cjs/utils/customTheme/color/apca.d.ts +4 -0
- package/dist/cjs/utils/customTheme/color/apca.js +62 -0
- package/dist/cjs/utils/customTheme/color/colorSpace.d.ts +5 -0
- package/dist/cjs/utils/customTheme/color/colorSpace.js +72 -0
- package/dist/cjs/utils/customTheme/color/conversions.d.ts +17 -0
- package/dist/cjs/utils/customTheme/color/conversions.js +101 -0
- package/dist/cjs/utils/customTheme/color/hex.d.ts +10 -0
- package/dist/cjs/utils/customTheme/color/hex.js +34 -0
- package/dist/cjs/utils/customTheme/color/index.d.ts +4 -0
- package/dist/cjs/utils/customTheme/color/index.js +20 -0
- package/dist/cjs/utils/customTheme/color/oklch.d.ts +3 -0
- package/dist/cjs/utils/customTheme/color/oklch.js +21 -0
- package/dist/cjs/utils/customTheme/color/types.d.ts +40 -0
- package/dist/cjs/utils/customTheme/color/types.js +2 -0
- package/dist/cjs/utils/customTheme/constants.d.ts +31 -0
- package/dist/cjs/utils/customTheme/constants.js +49 -0
- package/dist/cjs/utils/customTheme/generateBrandPalette.d.ts +10 -0
- package/dist/cjs/utils/customTheme/generateBrandPalette.js +25 -0
- package/dist/cjs/utils/customTheme/index.d.ts +4 -0
- package/dist/cjs/utils/customTheme/index.js +20 -0
- package/dist/cjs/utils/getThemeClassnames.d.ts +17 -0
- package/dist/cjs/utils/getThemeClassnames.js +38 -0
- package/dist/cjs/utils/index.d.ts +7 -0
- package/dist/cjs/utils/index.js +23 -0
- package/dist/cjs/utils/mergeAppearance.d.ts +7 -0
- package/dist/cjs/utils/mergeAppearance.js +17 -0
- package/dist/cjs/utils/resolveColorScheme.d.ts +7 -0
- package/dist/cjs/utils/resolveColorScheme.js +15 -0
- package/dist/cjs/utils/themeBootstrap.d.ts +12 -0
- package/dist/cjs/utils/themeBootstrap.js +19 -0
- package/dist/esm/components/ChildThemeProvider/ChildThemeProvider.d.ts +24 -0
- package/dist/esm/components/ChildThemeProvider/ChildThemeProvider.js +22 -0
- package/dist/esm/components/ChildThemeProvider/index.d.ts +1 -0
- package/dist/esm/components/ChildThemeProvider/index.js +1 -0
- package/dist/esm/components/RootThemeProvider/RootThemeProvider.d.ts +49 -0
- package/dist/esm/components/RootThemeProvider/RootThemeProvider.js +27 -0
- package/dist/esm/components/RootThemeProvider/index.d.ts +1 -0
- package/dist/esm/components/RootThemeProvider/index.js +1 -0
- package/dist/esm/components/ThemeProvider/ThemeProvider.d.ts +28 -0
- package/dist/esm/components/ThemeProvider/ThemeProvider.js +13 -0
- package/dist/esm/components/ThemeProvider/index.d.ts +1 -0
- package/dist/esm/components/ThemeProvider/index.js +1 -0
- package/dist/esm/components/ThemeScope/ThemeScope.d.ts +32 -0
- package/dist/esm/components/ThemeScope/ThemeScope.js +59 -0
- package/dist/esm/components/ThemeScope/index.d.ts +1 -0
- package/dist/esm/components/ThemeScope/index.js +1 -0
- package/dist/esm/components/index.d.ts +3 -0
- package/dist/esm/components/index.js +3 -0
- package/dist/esm/constants/appearance.d.ts +22 -0
- package/dist/esm/constants/appearance.js +24 -0
- package/dist/esm/constants/colorScheme.d.ts +17 -0
- package/dist/esm/constants/colorScheme.js +20 -0
- package/dist/esm/constants/index.d.ts +2 -0
- package/dist/esm/constants/index.js +2 -0
- package/dist/esm/context/appearanceContext.d.ts +20 -0
- package/dist/esm/context/appearanceContext.js +18 -0
- package/dist/esm/context/index.d.ts +4 -0
- package/dist/esm/context/index.js +2 -0
- package/dist/esm/context/themeContext.d.ts +12 -0
- package/dist/esm/context/themeContext.js +11 -0
- package/dist/esm/hooks/index.d.ts +4 -0
- package/dist/esm/hooks/index.js +4 -0
- package/dist/esm/hooks/useApplyCustomTheme.d.ts +27 -0
- package/dist/esm/hooks/useApplyCustomTheme.js +36 -0
- package/dist/esm/hooks/useColorScheme.d.ts +37 -0
- package/dist/esm/hooks/useColorScheme.js +76 -0
- package/dist/esm/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
- package/dist/esm/hooks/useIsomorphicLayoutEffect.js +13 -0
- package/dist/esm/hooks/useThemeClassnames.d.ts +11 -0
- package/dist/esm/hooks/useThemeClassnames.js +15 -0
- package/dist/esm/hooks/useThemeConfig.d.ts +24 -0
- package/dist/esm/hooks/useThemeConfig.js +12 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/keys.d.ts +4 -0
- package/dist/esm/keys.js +16 -0
- package/dist/esm/ssr.d.ts +15 -0
- package/dist/esm/ssr.js +15 -0
- package/dist/esm/store/globalStore.d.ts +29 -0
- package/dist/esm/store/globalStore.js +69 -0
- package/dist/esm/store/index.d.ts +1 -0
- package/dist/esm/store/index.js +1 -0
- package/dist/esm/types/appearance.d.ts +23 -0
- package/dist/esm/types/appearance.js +2 -0
- package/dist/esm/types/colorScheme.d.ts +5 -0
- package/dist/esm/types/colorScheme.js +1 -0
- package/dist/esm/types/index.d.ts +2 -0
- package/dist/esm/types/index.js +2 -0
- package/dist/esm/utils/colorSchemeFromHeaders.d.ts +22 -0
- package/dist/esm/utils/colorSchemeFromHeaders.js +40 -0
- package/dist/esm/utils/colorSchemeStorage.d.ts +35 -0
- package/dist/esm/utils/colorSchemeStorage.js +81 -0
- package/dist/esm/utils/customTheme/buildBrandPaletteCss.d.ts +11 -0
- package/dist/esm/utils/customTheme/buildBrandPaletteCss.js +32 -0
- package/dist/esm/utils/customTheme/buildBrandPaletteVars.d.ts +11 -0
- package/dist/esm/utils/customTheme/buildBrandPaletteVars.js +39 -0
- package/dist/esm/utils/customTheme/color/apca.d.ts +4 -0
- package/dist/esm/utils/customTheme/color/apca.js +59 -0
- package/dist/esm/utils/customTheme/color/colorSpace.d.ts +5 -0
- package/dist/esm/utils/customTheme/color/colorSpace.js +68 -0
- package/dist/esm/utils/customTheme/color/conversions.d.ts +17 -0
- package/dist/esm/utils/customTheme/color/conversions.js +91 -0
- package/dist/esm/utils/customTheme/color/hex.d.ts +10 -0
- package/dist/esm/utils/customTheme/color/hex.js +29 -0
- package/dist/esm/utils/customTheme/color/index.d.ts +4 -0
- package/dist/esm/utils/customTheme/color/index.js +4 -0
- package/dist/esm/utils/customTheme/color/oklch.d.ts +3 -0
- package/dist/esm/utils/customTheme/color/oklch.js +18 -0
- package/dist/esm/utils/customTheme/color/types.d.ts +40 -0
- package/dist/esm/utils/customTheme/color/types.js +1 -0
- package/dist/esm/utils/customTheme/constants.d.ts +31 -0
- package/dist/esm/utils/customTheme/constants.js +46 -0
- package/dist/esm/utils/customTheme/generateBrandPalette.d.ts +10 -0
- package/dist/esm/utils/customTheme/generateBrandPalette.js +22 -0
- package/dist/esm/utils/customTheme/index.d.ts +4 -0
- package/dist/esm/utils/customTheme/index.js +4 -0
- package/dist/esm/utils/getThemeClassnames.d.ts +17 -0
- package/dist/esm/utils/getThemeClassnames.js +34 -0
- package/dist/esm/utils/index.d.ts +7 -0
- package/dist/esm/utils/index.js +7 -0
- package/dist/esm/utils/mergeAppearance.d.ts +7 -0
- package/dist/esm/utils/mergeAppearance.js +14 -0
- package/dist/esm/utils/resolveColorScheme.d.ts +7 -0
- package/dist/esm/utils/resolveColorScheme.js +12 -0
- package/dist/esm/utils/themeBootstrap.d.ts +12 -0
- package/dist/esm/utils/themeBootstrap.js +16 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +45 -0
- package/src/components/ChildThemeProvider/ChildThemeProvider.tsx +54 -0
- package/src/components/ChildThemeProvider/index.ts +1 -0
- package/src/components/RootThemeProvider/RootThemeProvider.tsx +87 -0
- package/src/components/RootThemeProvider/index.ts +1 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +41 -0
- package/src/components/ThemeProvider/index.ts +1 -0
- package/src/components/ThemeScope/ThemeScope.tsx +106 -0
- package/src/components/ThemeScope/index.ts +1 -0
- package/src/components/index.ts +3 -0
- package/src/constants/appearance.ts +27 -0
- package/src/constants/colorScheme.ts +23 -0
- package/src/constants/index.ts +2 -0
- package/src/context/appearanceContext.tsx +39 -0
- package/src/context/index.ts +4 -0
- package/src/context/themeContext.ts +18 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useApplyCustomTheme.ts +56 -0
- package/src/hooks/useColorScheme.ts +131 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +17 -0
- package/src/hooks/useThemeClassnames.ts +18 -0
- package/src/hooks/useThemeConfig.tsx +44 -0
- package/src/index.ts +7 -0
- package/src/keys.ts +18 -0
- package/src/ssr.ts +25 -0
- package/src/store/globalStore.ts +93 -0
- package/src/store/index.ts +1 -0
- package/src/types/appearance.ts +29 -0
- package/src/types/colorScheme.ts +7 -0
- package/src/types/index.ts +2 -0
- package/src/utils/colorSchemeFromHeaders.ts +57 -0
- package/src/utils/colorSchemeStorage.ts +117 -0
- package/src/utils/customTheme/buildBrandPaletteCss.ts +37 -0
- package/src/utils/customTheme/buildBrandPaletteVars.ts +56 -0
- package/src/utils/customTheme/color/apca.ts +74 -0
- package/src/utils/customTheme/color/colorSpace.ts +82 -0
- package/src/utils/customTheme/color/conversions.ts +113 -0
- package/src/utils/customTheme/color/hex.ts +39 -0
- package/src/utils/customTheme/color/index.ts +4 -0
- package/src/utils/customTheme/color/oklch.ts +22 -0
- package/src/utils/customTheme/color/types.ts +32 -0
- package/src/utils/customTheme/constants.ts +57 -0
- package/src/utils/customTheme/generateBrandPalette.ts +30 -0
- package/src/utils/customTheme/index.ts +4 -0
- package/src/utils/getThemeClassnames.ts +40 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/mergeAppearance.ts +16 -0
- package/src/utils/resolveColorScheme.ts +15 -0
- package/src/utils/themeBootstrap.ts +18 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { THEME_KEYS } from '../keys.js';
|
|
2
|
+
import { mergeAppearance } from '../utils/mergeAppearance.js';
|
|
3
|
+
// Ключ — из локального keys.ts (одна точка бампа версии для контекста и стора домена).
|
|
4
|
+
const REGISTRY_KEY = Symbol.for(THEME_KEYS.store);
|
|
5
|
+
// SSR-snapshot — пустое оформление без сеттера. Мутабельный глобал на сервере запрещён (утечёт
|
|
6
|
+
// между запросами), поэтому серверный рендер всегда отдаёт дефолт; реальные классы на SSR ставит
|
|
7
|
+
// строка на `<html>` (см. providers-standard.md).
|
|
8
|
+
const SERVER_VALUE = { appearance: {}, setAppearance: undefined };
|
|
9
|
+
const APPEARANCE_KEYS = ['colorScheme', 'brand', 'brandRole', 'density', 'acrylic'];
|
|
10
|
+
function isSameAppearance(a, b) {
|
|
11
|
+
return APPEARANCE_KEYS.every(key => a[key] === b[key]);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Глобальный singleton-стор оформления, общий для всех React-корней процесса: микрофронтов
|
|
15
|
+
* single-spa, островков Astro и любых сценариев, где один `RootThemeProvider` нельзя поставить
|
|
16
|
+
* общим предком в одном дереве. Хранится в `globalThis` через `Symbol.for`, поэтому любой модуль
|
|
17
|
+
* получает один экземпляр без проброса по импортам и переживает несколько копий пакета.
|
|
18
|
+
*
|
|
19
|
+
* Shell контейнера меняет оформление через `setAppearance({ colorScheme, density, ... })` — все
|
|
20
|
+
* подписанные `RootThemeProvider` реэмитят полный набор `sn-*` классов. Когда у приложения один
|
|
21
|
+
* React-корень — используйте `<RootThemeProvider value={…} />`, этот хелпер нужен только для multi-root.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* // там, где меняется тема (один раз):
|
|
25
|
+
* getGlobalThemeStore().setAppearance({ colorScheme });
|
|
26
|
+
*
|
|
27
|
+
* // в каждом независимом корне:
|
|
28
|
+
* <RootThemeProvider store={getGlobalThemeStore().store} rootRef={htmlRef}>{root}</RootThemeProvider>;
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function getGlobalThemeStore() {
|
|
32
|
+
const registry = globalThis;
|
|
33
|
+
if (!registry[REGISTRY_KEY]) {
|
|
34
|
+
let appearance = {};
|
|
35
|
+
// Стабильный snapshot: новый объект только после смены оформления, иначе `useSyncExternalStore`
|
|
36
|
+
// зациклится на «getSnapshot вернул новую ссылку».
|
|
37
|
+
let cache;
|
|
38
|
+
const listeners = new Set();
|
|
39
|
+
const setAppearance = (patch) => {
|
|
40
|
+
const next = mergeAppearance(appearance, patch);
|
|
41
|
+
if (isSameAppearance(appearance, next))
|
|
42
|
+
return;
|
|
43
|
+
appearance = next;
|
|
44
|
+
cache = undefined;
|
|
45
|
+
listeners.forEach(listener => listener());
|
|
46
|
+
};
|
|
47
|
+
const read = () => {
|
|
48
|
+
if (!cache) {
|
|
49
|
+
cache = { appearance, setAppearance };
|
|
50
|
+
}
|
|
51
|
+
return cache;
|
|
52
|
+
};
|
|
53
|
+
registry[REGISTRY_KEY] = {
|
|
54
|
+
store: {
|
|
55
|
+
subscribe: onChange => {
|
|
56
|
+
listeners.add(onChange);
|
|
57
|
+
return () => {
|
|
58
|
+
listeners.delete(onChange);
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
getSnapshot: read,
|
|
62
|
+
getServerSnapshot: () => SERVER_VALUE,
|
|
63
|
+
},
|
|
64
|
+
getAppearance: () => appearance,
|
|
65
|
+
setAppearance,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return registry[REGISTRY_KEY];
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './globalStore.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './globalStore.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BRAND, BRAND_ROLE, DENSITY } from '../constants/appearance.js';
|
|
2
|
+
import { ColorScheme } from './colorScheme.js';
|
|
3
|
+
export type Density = (typeof DENSITY)[keyof typeof DENSITY];
|
|
4
|
+
export type Brand = (typeof BRAND)[keyof typeof BRAND];
|
|
5
|
+
export type BrandRole = (typeof BRAND_ROLE)[keyof typeof BRAND_ROLE];
|
|
6
|
+
/**
|
|
7
|
+
* Набор осей оформления, которые определяют полный набор `sn-*` классов на DOM-границе. Любая
|
|
8
|
+
* незаданная ось наследуется от вышестоящего провайдера (слияние в `ChildThemeProvider`) — но на сам
|
|
9
|
+
* элемент всё равно эмитится **полный** набор классов, потому что токены `@cloud-ru/figma-variables`
|
|
10
|
+
* не переопределяются по одной оси через CSS-каскад (см. providers-standard.md).
|
|
11
|
+
*/
|
|
12
|
+
export type ThemeAppearance = {
|
|
13
|
+
/** Цветовая схема — `sn-light` / `sn-dark`. */
|
|
14
|
+
colorScheme?: ColorScheme;
|
|
15
|
+
/** Бренд — `sn-brandA` … */
|
|
16
|
+
brand?: Brand;
|
|
17
|
+
/** Роль бренда (палитра) — `sn-main` … */
|
|
18
|
+
brandRole?: BrandRole;
|
|
19
|
+
/** Плотность — `sn-comfort` / `sn-compact` / `sn-spacious`. */
|
|
20
|
+
density?: Density;
|
|
21
|
+
/** Акрил (blur-материал) — `sn-yes` при `true`, иначе `sn-no`. */
|
|
22
|
+
acrylic?: boolean;
|
|
23
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { COLOR_SCHEME, THEME_OVERRIDE } from '../constants/colorScheme.js';
|
|
2
|
+
/** Разрешённая цветовая схема (то, что реально применяется на корень). */
|
|
3
|
+
export type ColorScheme = (typeof COLOR_SCHEME)[keyof typeof COLOR_SCHEME];
|
|
4
|
+
/** Пользовательский выбор: фиксированная схема или `system`. */
|
|
5
|
+
export type ThemeOverride = (typeof THEME_OVERRIDE)[keyof typeof THEME_OVERRIDE];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ColorScheme, ThemeOverride } from '../types/colorScheme.js';
|
|
2
|
+
type HeadersLike = {
|
|
3
|
+
get(name: string): string | null;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* SSR-резолв override из cookie заголовка запроса (стенд-ин бэкенд-сессии: в сервисе тут читается
|
|
7
|
+
* сохранённая тема пользователя). Прокидывается в `useColorScheme({ initialOverride })` —
|
|
8
|
+
* детерминированный первый рендер без hydration mismatch и без прыжка подсветки переключателя.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getThemeOverrideFromHeaders(headers: HeadersLike, options?: {
|
|
11
|
+
storageKey?: string;
|
|
12
|
+
}): ThemeOverride | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* SSR-резолв итоговой схемы из заголовков (зеркало `getAdaptive(userAgent)` для adaptive):
|
|
15
|
+
* cookie `snack-uikit-theme` даёт override, `Sec-CH-Prefers-Color-Scheme` (client hint) — системную тему.
|
|
16
|
+
* Включите client hint ответными заголовками `Accept-CH`/`Critical-CH`, иначе на первом визите
|
|
17
|
+
* хинта нет (системная вернётся `light`), а корректную тему доставит inline-bootstrap до отрисовки.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getColorSchemeFromHeaders(headers: HeadersLike, options?: {
|
|
20
|
+
storageKey?: string;
|
|
21
|
+
}): ColorScheme;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { COLOR_SCHEME, THEME_OVERRIDE, THEME_OVERRIDE_STORAGE_KEY } from '../constants/colorScheme.js';
|
|
2
|
+
import { resolveColorScheme } from './resolveColorScheme.js';
|
|
3
|
+
function isOverride(value) {
|
|
4
|
+
return value === THEME_OVERRIDE.Light || value === THEME_OVERRIDE.Dark || value === THEME_OVERRIDE.System;
|
|
5
|
+
}
|
|
6
|
+
function parseOverrideFromCookie(cookieHeader, storageKey) {
|
|
7
|
+
if (!cookieHeader) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
for (const part of cookieHeader.split(';')) {
|
|
11
|
+
const [name, ...rest] = part.trim().split('=');
|
|
12
|
+
if (name === storageKey) {
|
|
13
|
+
const value = decodeURIComponent(rest.join('='));
|
|
14
|
+
if (isOverride(value)) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* SSR-резолв override из cookie заголовка запроса (стенд-ин бэкенд-сессии: в сервисе тут читается
|
|
23
|
+
* сохранённая тема пользователя). Прокидывается в `useColorScheme({ initialOverride })` —
|
|
24
|
+
* детерминированный первый рендер без hydration mismatch и без прыжка подсветки переключателя.
|
|
25
|
+
*/
|
|
26
|
+
export function getThemeOverrideFromHeaders(headers, options) {
|
|
27
|
+
const storageKey = options?.storageKey ?? THEME_OVERRIDE_STORAGE_KEY;
|
|
28
|
+
return parseOverrideFromCookie(headers.get('cookie'), storageKey);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* SSR-резолв итоговой схемы из заголовков (зеркало `getAdaptive(userAgent)` для adaptive):
|
|
32
|
+
* cookie `snack-uikit-theme` даёт override, `Sec-CH-Prefers-Color-Scheme` (client hint) — системную тему.
|
|
33
|
+
* Включите client hint ответными заголовками `Accept-CH`/`Critical-CH`, иначе на первом визите
|
|
34
|
+
* хинта нет (системная вернётся `light`), а корректную тему доставит inline-bootstrap до отрисовки.
|
|
35
|
+
*/
|
|
36
|
+
export function getColorSchemeFromHeaders(headers, options) {
|
|
37
|
+
const override = getThemeOverrideFromHeaders(headers, options);
|
|
38
|
+
const systemPrefersDark = headers.get('sec-ch-prefers-color-scheme') === COLOR_SCHEME.Dark;
|
|
39
|
+
return resolveColorScheme(override, systemPrefersDark);
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ThemeOverride } from '../types/colorScheme.js';
|
|
2
|
+
/**
|
|
3
|
+
* Точка расширения персиста цветовой схемы. DS не «зашивает» хранилище: дефолт — in-memory
|
|
4
|
+
* (`createInMemoryColorSchemeStorage`, без персиста между перезагрузками). Персист подключает
|
|
5
|
+
* потребитель явным адаптером: `createCookieColorSchemeStorage` (no-flash SSR) или собственный
|
|
6
|
+
* (localStorage / бэкенд-сессия — `read()` возвращает `undefined`, начальное даёт `initialOverride`
|
|
7
|
+
* из server-сессии, `write()` шлёт POST, `subscribe()` — push через SSE/ws). Контракт намеренно про
|
|
8
|
+
* одну ось `override`; brand/density персистит сам апп. Модель `useColorScheme` при любом адаптере
|
|
9
|
+
* не меняется.
|
|
10
|
+
*/
|
|
11
|
+
export type ColorSchemeStorage = {
|
|
12
|
+
/** Синхронно прочитать сохранённый override. Для бэкенд-адаптера — `undefined` (см. `initialOverride`). */
|
|
13
|
+
read(): ThemeOverride | undefined;
|
|
14
|
+
/** Персистнуть выбор (cookie / POST на бэкенд). */
|
|
15
|
+
write(next: ThemeOverride): void;
|
|
16
|
+
/** Подписка на внешние изменения (кросс-таб / push). Возвращает отписку. */
|
|
17
|
+
subscribe(onChange: () => void): () => void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Дефолтный storage-адаптер: держит выбор в памяти модуля на время жизни страницы. Переживает
|
|
21
|
+
* ремаунты и синхронит несколько хуков между собой, но НЕ персистит между перезагрузками — DS
|
|
22
|
+
* намеренно не пишет в cookie/localStorage без явного согласия потребителя. Для персиста подключите
|
|
23
|
+
* `createCookieColorSchemeStorage` (или свой адаптер) через проп `storage`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createInMemoryColorSchemeStorage(): ColorSchemeStorage;
|
|
26
|
+
/**
|
|
27
|
+
* Cookie storage-адаптер (opt-in): cookie как источник истины (читается и на сервере, и inline-
|
|
28
|
+
* bootstrap'ом) + `BroadcastChannel` для живой кросс-таб синхронизации. Нужен для no-flash SSR —
|
|
29
|
+
* подключается потребителем через `useColorScheme({ storage: createCookieColorSchemeStorage() })`.
|
|
30
|
+
* Создание SSR-безопасно: DOM/Channel трогаются только при вызове методов, не в фабрике.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createCookieColorSchemeStorage(options?: {
|
|
33
|
+
storageKey?: string;
|
|
34
|
+
channelName?: string;
|
|
35
|
+
}): ColorSchemeStorage;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { THEME_OVERRIDE, THEME_OVERRIDE_STORAGE_KEY } from '../constants/colorScheme.js';
|
|
2
|
+
/* eslint-disable @cloud-ru/ssr-safe-react/domApi -- guarded by isBrowser(); адаптеры трогают DOM только при вызове методов */
|
|
3
|
+
function isBrowser() {
|
|
4
|
+
return Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
|
|
5
|
+
}
|
|
6
|
+
function isOverride(value) {
|
|
7
|
+
return value === THEME_OVERRIDE.Light || value === THEME_OVERRIDE.Dark || value === THEME_OVERRIDE.System;
|
|
8
|
+
}
|
|
9
|
+
function readCookieOverride(storageKey) {
|
|
10
|
+
if (!isBrowser()) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${storageKey}=([^;]*)`));
|
|
14
|
+
const value = match ? decodeURIComponent(match[1]) : undefined;
|
|
15
|
+
return isOverride(value) ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Дефолтный storage-адаптер: держит выбор в памяти модуля на время жизни страницы. Переживает
|
|
19
|
+
* ремаунты и синхронит несколько хуков между собой, но НЕ персистит между перезагрузками — DS
|
|
20
|
+
* намеренно не пишет в cookie/localStorage без явного согласия потребителя. Для персиста подключите
|
|
21
|
+
* `createCookieColorSchemeStorage` (или свой адаптер) через проп `storage`.
|
|
22
|
+
*/
|
|
23
|
+
export function createInMemoryColorSchemeStorage() {
|
|
24
|
+
let value;
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
return {
|
|
27
|
+
read: () => value,
|
|
28
|
+
write: next => {
|
|
29
|
+
value = next;
|
|
30
|
+
listeners.forEach(listener => listener());
|
|
31
|
+
},
|
|
32
|
+
subscribe: onChange => {
|
|
33
|
+
listeners.add(onChange);
|
|
34
|
+
return () => {
|
|
35
|
+
listeners.delete(onChange);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Cookie storage-адаптер (opt-in): cookie как источник истины (читается и на сервере, и inline-
|
|
42
|
+
* bootstrap'ом) + `BroadcastChannel` для живой кросс-таб синхронизации. Нужен для no-flash SSR —
|
|
43
|
+
* подключается потребителем через `useColorScheme({ storage: createCookieColorSchemeStorage() })`.
|
|
44
|
+
* Создание SSR-безопасно: DOM/Channel трогаются только при вызове методов, не в фабрике.
|
|
45
|
+
*/
|
|
46
|
+
export function createCookieColorSchemeStorage(options) {
|
|
47
|
+
const storageKey = options?.storageKey ?? THEME_OVERRIDE_STORAGE_KEY;
|
|
48
|
+
const channelName = options?.channelName ?? `${storageKey}-channel`;
|
|
49
|
+
let channel = null;
|
|
50
|
+
const getChannel = () => {
|
|
51
|
+
if (channel) {
|
|
52
|
+
return channel;
|
|
53
|
+
}
|
|
54
|
+
if (!isBrowser() || typeof BroadcastChannel === 'undefined') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
channel = new BroadcastChannel(channelName);
|
|
58
|
+
return channel;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
read: () => readCookieOverride(storageKey),
|
|
62
|
+
write: next => {
|
|
63
|
+
if (!isBrowser()) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// max-age ~1 год, lax — cookie уходит на сервер для SSR-резолва следующей загрузки.
|
|
67
|
+
document.cookie = `${storageKey}=${encodeURIComponent(next)};path=/;max-age=31536000;samesite=lax`;
|
|
68
|
+
getChannel()?.postMessage(next);
|
|
69
|
+
},
|
|
70
|
+
subscribe: onChange => {
|
|
71
|
+
const ch = getChannel();
|
|
72
|
+
if (!ch) {
|
|
73
|
+
return () => { };
|
|
74
|
+
}
|
|
75
|
+
const handler = () => onChange();
|
|
76
|
+
ch.addEventListener('message', handler);
|
|
77
|
+
return () => ch.removeEventListener('message', handler);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/* eslint-enable @cloud-ru/ssr-safe-react/domApi */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Собирает текст CSS-правила, переопределяющего бренд-палитру из seed-цвета на бренд-классах.
|
|
3
|
+
*
|
|
4
|
+
* - Без `scope` — глобально (`:is(.sn-brandA,…){…}`), для white-label в корне приложения: побеждает
|
|
5
|
+
* по source order на всех бренд-классах страницы, включая порталы.
|
|
6
|
+
* - Со `scope` (CSS-селектор корня поддерева) — ограничивает область потомками и самим корнем; выше
|
|
7
|
+
* токенной специфичности, поэтому порядок не важен.
|
|
8
|
+
*
|
|
9
|
+
* Невалидный hex → `null`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildBrandPaletteCss(seedColor: string, scope?: string): string | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BRAND } from '../../constants/appearance.js';
|
|
2
|
+
import { buildBrandPaletteVars } from './buildBrandPaletteVars.js';
|
|
3
|
+
// Правило нацелено на сами бренд-классы (`.sn-brandA/B/C/D/E`), а не на один элемент inline: компоненты,
|
|
4
|
+
// переэмитящие набор `sn-*` на своих обёртках (Table/Stepper/Alert через `useThemeClassnames`), заново
|
|
5
|
+
// объявляют `--sn-brand-color-primary-*` из класса бренда — inline на предке они перекрыли бы, правило
|
|
6
|
+
// на том же бренд-классе — нет.
|
|
7
|
+
// Один seed переопределяет сразу все бренд-классы (потребитель не знает, в какой бренд-слот завернётся
|
|
8
|
+
// его поддерево). TODO FF-8813: возможность задавать seed индивидуально для каждого бренда (brandA/B/C/D/E).
|
|
9
|
+
const BRAND_CLASS_SELECTOR = `:is(${Object.values(BRAND)
|
|
10
|
+
.map(brand => `.sn-${brand}`)
|
|
11
|
+
.join(',')})`;
|
|
12
|
+
/**
|
|
13
|
+
* Собирает текст CSS-правила, переопределяющего бренд-палитру из seed-цвета на бренд-классах.
|
|
14
|
+
*
|
|
15
|
+
* - Без `scope` — глобально (`:is(.sn-brandA,…){…}`), для white-label в корне приложения: побеждает
|
|
16
|
+
* по source order на всех бренд-классах страницы, включая порталы.
|
|
17
|
+
* - Со `scope` (CSS-селектор корня поддерева) — ограничивает область потомками и самим корнем; выше
|
|
18
|
+
* токенной специфичности, поэтому порядок не важен.
|
|
19
|
+
*
|
|
20
|
+
* Невалидный hex → `null`.
|
|
21
|
+
*/
|
|
22
|
+
export function buildBrandPaletteCss(seedColor, scope) {
|
|
23
|
+
const vars = buildBrandPaletteVars(seedColor);
|
|
24
|
+
if (!vars) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const declarations = Object.entries(vars)
|
|
28
|
+
.map(([name, value]) => `${name}:${value}`)
|
|
29
|
+
.join(';');
|
|
30
|
+
const selector = scope ? `${scope} ${BRAND_CLASS_SELECTOR},${scope}${BRAND_CLASS_SELECTOR}` : BRAND_CLASS_SELECTOR;
|
|
31
|
+
return `${selector}{${declarations}}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Собирает карту CSS-переменных `--sn-brand-color-primary-*` из seed-цвета для инъекции в DOM.
|
|
3
|
+
* Кроме числовых тонов задаёт `transparent` (акцент с alpha) и `onAccentLight`/`onAccentDark`
|
|
4
|
+
* (текст на светлой/тёмной акцентной заливке). Семантический слой `--sn-theme-color-primary-*`
|
|
5
|
+
* каскадит из этих тонов, поэтому одна карта красит и light, и dark. Невалидный hex → `null`.
|
|
6
|
+
*
|
|
7
|
+
* Текст на акценте: если белый на акценте контрастен (APCA ≥ 50) — светлая схема берёт near-white,
|
|
8
|
+
* тёмная near-dark; если акцент слишком светлый для белого — обе схемы инвертируются. Одно решение
|
|
9
|
+
* по контрасту seed-акцента с белым, зеркальное для схем.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildBrandPaletteVars(seedColor: string): Record<string, string> | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { apcaContrast } from './color/index.js';
|
|
2
|
+
import { ACTIVATED_ALPHA_SUFFIX, BRAND_ACTIVATED_VAR, BRAND_PRIMARY_VAR_PREFIX, NEAR_DARK_TONE, NEAR_WHITE_TONE, PRIMARY_ACCENT_TONE, TRANSPARENT_ALPHA_SUFFIX, } from './constants.js';
|
|
3
|
+
import { generateBrandPalette } from './generateBrandPalette.js';
|
|
4
|
+
const WHITE_HEX = '#ffffff';
|
|
5
|
+
// Порог APCA-контраста белого текста на акценте: ниже него акцент считается слишком светлым и текст
|
|
6
|
+
// инвертируется на тёмный.
|
|
7
|
+
const WHITE_ON_ACCENT_MIN_CONTRAST = 50;
|
|
8
|
+
/**
|
|
9
|
+
* Собирает карту CSS-переменных `--sn-brand-color-primary-*` из seed-цвета для инъекции в DOM.
|
|
10
|
+
* Кроме числовых тонов задаёт `transparent` (акцент с alpha) и `onAccentLight`/`onAccentDark`
|
|
11
|
+
* (текст на светлой/тёмной акцентной заливке). Семантический слой `--sn-theme-color-primary-*`
|
|
12
|
+
* каскадит из этих тонов, поэтому одна карта красит и light, и dark. Невалидный hex → `null`.
|
|
13
|
+
*
|
|
14
|
+
* Текст на акценте: если белый на акценте контрастен (APCA ≥ 50) — светлая схема берёт near-white,
|
|
15
|
+
* тёмная near-dark; если акцент слишком светлый для белого — обе схемы инвертируются. Одно решение
|
|
16
|
+
* по контрасту seed-акцента с белым, зеркальное для схем.
|
|
17
|
+
*/
|
|
18
|
+
export function buildBrandPaletteVars(seedColor) {
|
|
19
|
+
const palette = generateBrandPalette(seedColor);
|
|
20
|
+
if (!palette) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const vars = {};
|
|
24
|
+
for (const [tone, hex] of Object.entries(palette)) {
|
|
25
|
+
vars[`${BRAND_PRIMARY_VAR_PREFIX}${tone}`] = hex;
|
|
26
|
+
}
|
|
27
|
+
const accent = palette[PRIMARY_ACCENT_TONE];
|
|
28
|
+
const nearWhite = palette[NEAR_WHITE_TONE];
|
|
29
|
+
const nearDark = palette[NEAR_DARK_TONE];
|
|
30
|
+
const accentTooLightForWhite = apcaContrast(accent, WHITE_HEX) < WHITE_ON_ACCENT_MIN_CONTRAST;
|
|
31
|
+
vars[`${BRAND_PRIMARY_VAR_PREFIX}transparent`] = `${accent}${TRANSPARENT_ALPHA_SUFFIX}`;
|
|
32
|
+
vars[`${BRAND_PRIMARY_VAR_PREFIX}onAccentLight`] = accentTooLightForWhite ? nearDark : nearWhite;
|
|
33
|
+
vars[`${BRAND_PRIMARY_VAR_PREFIX}onAccentDark`] = accentTooLightForWhite ? nearWhite : nearDark;
|
|
34
|
+
// Activated-заливки = акцент с alpha (выбранная строка таблицы, active-состояния следуют за брендом).
|
|
35
|
+
vars[BRAND_ACTIVATED_VAR.default] = `${accent}${ACTIVATED_ALPHA_SUFFIX.default}`;
|
|
36
|
+
vars[BRAND_ACTIVATED_VAR.hovered] = `${accent}${ACTIVATED_ALPHA_SUFFIX.hovered}`;
|
|
37
|
+
vars[BRAND_ACTIVATED_VAR.pressed] = `${accent}${ACTIVATED_ALPHA_SUFFIX.pressed}`;
|
|
38
|
+
return vars;
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// APCA (Accessible Perceptual Contrast Algorithm), реализация SA98G (версия 0.0.98G-4g).
|
|
2
|
+
// Используется только для выбора читаемого текста на акцентной заливке при генерации палитры.
|
|
3
|
+
import { parseHex } from './hex.js';
|
|
4
|
+
// Константы SA98G — менять нельзя, иначе разъедется с эталоном контраста APCA.
|
|
5
|
+
const MAIN_TRC = 2.4;
|
|
6
|
+
const S_RCO = 0.2126729;
|
|
7
|
+
const S_GCO = 0.7151522;
|
|
8
|
+
const S_BCO = 0.072175;
|
|
9
|
+
const NORM_BG = 0.56;
|
|
10
|
+
const NORM_TXT = 0.57;
|
|
11
|
+
const REV_TXT = 0.62;
|
|
12
|
+
const REV_BG = 0.65;
|
|
13
|
+
const BLK_THRS = 0.022;
|
|
14
|
+
const BLK_CLMP = 1.414;
|
|
15
|
+
const SCALE_BOW = 1.14;
|
|
16
|
+
const SCALE_WOB = 1.14;
|
|
17
|
+
const LO_BOW_OFFSET = 0.027;
|
|
18
|
+
const LO_WOB_OFFSET = 0.027;
|
|
19
|
+
const DELTA_Y_MIN = 0.0005;
|
|
20
|
+
const LO_CLIP = 0.1;
|
|
21
|
+
/** sRGB-каналы 0–255 → относительная яркость `Y` для APCA. */
|
|
22
|
+
function sRGBtoY([r, g, b]) {
|
|
23
|
+
const linearize = (channel) => Math.pow(channel / 255, MAIN_TRC);
|
|
24
|
+
return S_RCO * linearize(r) + S_GCO * linearize(g) + S_BCO * linearize(b);
|
|
25
|
+
}
|
|
26
|
+
/** Мягкий клэмп яркости у чёрного (soft black clamp). */
|
|
27
|
+
function softClampBlack(y) {
|
|
28
|
+
return y > BLK_THRS ? y : y + Math.pow(BLK_THRS - y, BLK_CLMP);
|
|
29
|
+
}
|
|
30
|
+
/** Сырой APCA-контраст `Lc` между яркостью текста и фона. Полярность важна, не переставлять аргументы. */
|
|
31
|
+
function apcaLc(txtY, bgY) {
|
|
32
|
+
if (Number.isNaN(txtY) || Number.isNaN(bgY) || Math.min(txtY, bgY) < 0 || Math.max(txtY, bgY) > 1.1) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const text = softClampBlack(txtY);
|
|
36
|
+
const bg = softClampBlack(bgY);
|
|
37
|
+
if (Math.abs(bg - text) < DELTA_Y_MIN) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
if (bg > text) {
|
|
41
|
+
// Тёмный текст на светлом фоне (BoW).
|
|
42
|
+
const sapc = (Math.pow(bg, NORM_BG) - Math.pow(text, NORM_TXT)) * SCALE_BOW;
|
|
43
|
+
return sapc < LO_CLIP ? 0 : (sapc - LO_BOW_OFFSET) * 100;
|
|
44
|
+
}
|
|
45
|
+
// Светлый текст на тёмном фоне (WoB) — отрицательный по контракту APCA.
|
|
46
|
+
const sapc = (Math.pow(bg, REV_BG) - Math.pow(text, REV_TXT)) * SCALE_WOB;
|
|
47
|
+
return sapc > -LO_CLIP ? 0 : (sapc + LO_WOB_OFFSET) * 100;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Абсолютный APCA-контраст `Lc` текста на фоне (оба — hex). Невалидный hex → `0`.
|
|
51
|
+
*/
|
|
52
|
+
export function apcaContrast(backgroundHex, textHex) {
|
|
53
|
+
const bg = parseHex(backgroundHex);
|
|
54
|
+
const text = parseHex(textHex);
|
|
55
|
+
if (!bg || !text) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
return Math.round(Math.abs(apcaLc(sRGBtoY(text), sRGBtoY(bg))));
|
|
59
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { LCH, TColor } from './types.js';
|
|
2
|
+
/** LCH → разобранный цвет. `hex` вычисляется лениво (с загонкой в гамму при выходе за неё). */
|
|
3
|
+
export declare function lch2color(lch: LCH): TColor;
|
|
4
|
+
/** hex → разобранный цвет (OKLCH-компоненты + sRGB-каналы). Невалидный hex → `null`. */
|
|
5
|
+
export declare function hex2color(hex: string): TColor | null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Разбор и синтез цветов в модели OKLCH. Алгоритм forceIntoGamut — CSS Color 4
|
|
2
|
+
// (Chris Lilley, https://svgees.us/).
|
|
3
|
+
import { gamSRGB, linSRGB, linSRGBToXYZ, xyzToLinSRGB } from './conversions.js';
|
|
4
|
+
import { parseHex, srgbToHex } from './hex.js';
|
|
5
|
+
import { oklch } from './oklch.js';
|
|
6
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
7
|
+
const xyz2rgb = (xyz) => gamSRGB(xyzToLinSRGB(xyz));
|
|
8
|
+
const rgb2xyz = (rgb) => linSRGBToXYZ(linSRGB(rgb));
|
|
9
|
+
const lch2rgb = (lch) => xyz2rgb(oklch.lch2xyz(lch));
|
|
10
|
+
const rgb2lch = (rgb) => oklch.xyz2lch(rgb2xyz(rgb));
|
|
11
|
+
function isWithinGamut(rgb) {
|
|
12
|
+
const epsilon = 0.000005;
|
|
13
|
+
return rgb.every(channel => channel >= 0 - epsilon && channel <= 1 + epsilon);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Загоняет LCH-цвет в sRGB-гамму: держит `l` и `h`, бинарным поиском ужимает `c` до границы гаммы.
|
|
17
|
+
*/
|
|
18
|
+
function forceIntoGamut(lch) {
|
|
19
|
+
let rgb = lch2rgb(lch);
|
|
20
|
+
if (isWithinGamut(rgb)) {
|
|
21
|
+
return rgb;
|
|
22
|
+
}
|
|
23
|
+
const [l, , h] = lch;
|
|
24
|
+
let hiC = lch[1];
|
|
25
|
+
let loC = 0;
|
|
26
|
+
let c = lch[1] / 2;
|
|
27
|
+
const epsilon = 0.0001;
|
|
28
|
+
while (hiC - loC > epsilon) {
|
|
29
|
+
rgb = lch2rgb([l, c, h]);
|
|
30
|
+
if (isWithinGamut(rgb)) {
|
|
31
|
+
loC = c;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
hiC = c;
|
|
35
|
+
}
|
|
36
|
+
c = (hiC + loC) / 2;
|
|
37
|
+
}
|
|
38
|
+
return rgb;
|
|
39
|
+
}
|
|
40
|
+
/** LCH → разобранный цвет. `hex` вычисляется лениво (с загонкой в гамму при выходе за неё). */
|
|
41
|
+
export function lch2color(lch) {
|
|
42
|
+
const srgb = xyz2rgb(oklch.lch2xyz(lch));
|
|
43
|
+
const withinSRGB = isWithinGamut(srgb);
|
|
44
|
+
const [r, g, b] = srgb.map(channel => clamp(channel * 255, 0, 255));
|
|
45
|
+
const [l, c, h] = lch;
|
|
46
|
+
return {
|
|
47
|
+
l,
|
|
48
|
+
c,
|
|
49
|
+
h,
|
|
50
|
+
r,
|
|
51
|
+
g,
|
|
52
|
+
b,
|
|
53
|
+
get hex() {
|
|
54
|
+
return srgbToHex(withinSRGB ? srgb : forceIntoGamut(lch));
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** hex → разобранный цвет (OKLCH-компоненты + sRGB-каналы). Невалидный hex → `null`. */
|
|
59
|
+
export function hex2color(hex) {
|
|
60
|
+
const parsed = parseHex(hex);
|
|
61
|
+
if (!parsed) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const normalized = parsed.map(channel => channel / 255);
|
|
65
|
+
const [l, c, h] = rgb2lch(normalized);
|
|
66
|
+
const [r, g, b] = parsed;
|
|
67
|
+
return { l, c, h, r, g, b, hex: srgbToHex(normalized) };
|
|
68
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { LAB, LCH, RGB, XYZ } from './types.js';
|
|
2
|
+
/** XYZ (относительно D65) → OKLab. `L` в диапазоне `[0,1]`. */
|
|
3
|
+
export declare function xyzToOklab(xyz: XYZ): LAB;
|
|
4
|
+
/** OKLab → XYZ (относительно D65). */
|
|
5
|
+
export declare function oklabToXyz(lab: LAB): XYZ;
|
|
6
|
+
/** OKLab → OKLCH. Hue возвращается в градусах `[0,360)`. */
|
|
7
|
+
export declare function oklabToOklch(lab: LAB): LCH;
|
|
8
|
+
/** OKLCH → OKLab. */
|
|
9
|
+
export declare function oklchToOklab(lch: LCH): LAB;
|
|
10
|
+
/** sRGB → linear-light sRGB (обратная gamma-коррекция), расширенная функция для отрицательных значений. */
|
|
11
|
+
export declare function linSRGB(rgb: RGB): RGB;
|
|
12
|
+
/** linear-light sRGB → sRGB (gamma-коррекция), расширенная функция для отрицательных значений. */
|
|
13
|
+
export declare function gamSRGB(rgb: RGB): RGB;
|
|
14
|
+
/** linear-light sRGB → CIE XYZ (белая точка D65). */
|
|
15
|
+
export declare function linSRGBToXYZ(rgb: RGB): XYZ;
|
|
16
|
+
/** CIE XYZ → linear-light sRGB. */
|
|
17
|
+
export declare function xyzToLinSRGB(xyz: XYZ): RGB;
|