@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.
Files changed (247) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +201 -0
  3. package/README.md +365 -0
  4. package/dist/cjs/components/ChildThemeProvider/ChildThemeProvider.d.ts +24 -0
  5. package/dist/cjs/components/ChildThemeProvider/ChildThemeProvider.js +25 -0
  6. package/dist/cjs/components/ChildThemeProvider/index.d.ts +1 -0
  7. package/dist/cjs/components/ChildThemeProvider/index.js +17 -0
  8. package/dist/cjs/components/RootThemeProvider/RootThemeProvider.d.ts +49 -0
  9. package/dist/cjs/components/RootThemeProvider/RootThemeProvider.js +30 -0
  10. package/dist/cjs/components/RootThemeProvider/index.d.ts +1 -0
  11. package/dist/cjs/components/RootThemeProvider/index.js +17 -0
  12. package/dist/cjs/components/ThemeProvider/ThemeProvider.d.ts +28 -0
  13. package/dist/cjs/components/ThemeProvider/ThemeProvider.js +16 -0
  14. package/dist/cjs/components/ThemeProvider/index.d.ts +1 -0
  15. package/dist/cjs/components/ThemeProvider/index.js +17 -0
  16. package/dist/cjs/components/ThemeScope/ThemeScope.d.ts +32 -0
  17. package/dist/cjs/components/ThemeScope/ThemeScope.js +65 -0
  18. package/dist/cjs/components/ThemeScope/index.d.ts +1 -0
  19. package/dist/cjs/components/ThemeScope/index.js +17 -0
  20. package/dist/cjs/components/index.d.ts +3 -0
  21. package/dist/cjs/components/index.js +19 -0
  22. package/dist/cjs/constants/appearance.d.ts +22 -0
  23. package/dist/cjs/constants/appearance.js +27 -0
  24. package/dist/cjs/constants/colorScheme.d.ts +17 -0
  25. package/dist/cjs/constants/colorScheme.js +23 -0
  26. package/dist/cjs/constants/index.d.ts +2 -0
  27. package/dist/cjs/constants/index.js +18 -0
  28. package/dist/cjs/context/appearanceContext.d.ts +20 -0
  29. package/dist/cjs/context/appearanceContext.js +22 -0
  30. package/dist/cjs/context/index.d.ts +4 -0
  31. package/dist/cjs/context/index.js +7 -0
  32. package/dist/cjs/context/themeContext.d.ts +12 -0
  33. package/dist/cjs/context/themeContext.js +15 -0
  34. package/dist/cjs/hooks/index.d.ts +4 -0
  35. package/dist/cjs/hooks/index.js +20 -0
  36. package/dist/cjs/hooks/useApplyCustomTheme.d.ts +27 -0
  37. package/dist/cjs/hooks/useApplyCustomTheme.js +39 -0
  38. package/dist/cjs/hooks/useColorScheme.d.ts +37 -0
  39. package/dist/cjs/hooks/useColorScheme.js +80 -0
  40. package/dist/cjs/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
  41. package/dist/cjs/hooks/useIsomorphicLayoutEffect.js +16 -0
  42. package/dist/cjs/hooks/useThemeClassnames.d.ts +11 -0
  43. package/dist/cjs/hooks/useThemeClassnames.js +18 -0
  44. package/dist/cjs/hooks/useThemeConfig.d.ts +24 -0
  45. package/dist/cjs/hooks/useThemeConfig.js +15 -0
  46. package/dist/cjs/index.d.ts +7 -0
  47. package/dist/cjs/index.js +23 -0
  48. package/dist/cjs/keys.d.ts +4 -0
  49. package/dist/cjs/keys.js +19 -0
  50. package/dist/cjs/package.json +3 -0
  51. package/dist/cjs/ssr.d.ts +15 -0
  52. package/dist/cjs/ssr.js +38 -0
  53. package/dist/cjs/store/globalStore.d.ts +29 -0
  54. package/dist/cjs/store/globalStore.js +72 -0
  55. package/dist/cjs/store/index.d.ts +1 -0
  56. package/dist/cjs/store/index.js +17 -0
  57. package/dist/cjs/types/appearance.d.ts +23 -0
  58. package/dist/cjs/types/appearance.js +3 -0
  59. package/dist/cjs/types/colorScheme.d.ts +5 -0
  60. package/dist/cjs/types/colorScheme.js +2 -0
  61. package/dist/cjs/types/index.d.ts +2 -0
  62. package/dist/cjs/types/index.js +18 -0
  63. package/dist/cjs/utils/colorSchemeFromHeaders.d.ts +22 -0
  64. package/dist/cjs/utils/colorSchemeFromHeaders.js +44 -0
  65. package/dist/cjs/utils/colorSchemeStorage.d.ts +35 -0
  66. package/dist/cjs/utils/colorSchemeStorage.js +85 -0
  67. package/dist/cjs/utils/customTheme/buildBrandPaletteCss.d.ts +11 -0
  68. package/dist/cjs/utils/customTheme/buildBrandPaletteCss.js +35 -0
  69. package/dist/cjs/utils/customTheme/buildBrandPaletteVars.d.ts +11 -0
  70. package/dist/cjs/utils/customTheme/buildBrandPaletteVars.js +42 -0
  71. package/dist/cjs/utils/customTheme/color/apca.d.ts +4 -0
  72. package/dist/cjs/utils/customTheme/color/apca.js +62 -0
  73. package/dist/cjs/utils/customTheme/color/colorSpace.d.ts +5 -0
  74. package/dist/cjs/utils/customTheme/color/colorSpace.js +72 -0
  75. package/dist/cjs/utils/customTheme/color/conversions.d.ts +17 -0
  76. package/dist/cjs/utils/customTheme/color/conversions.js +101 -0
  77. package/dist/cjs/utils/customTheme/color/hex.d.ts +10 -0
  78. package/dist/cjs/utils/customTheme/color/hex.js +34 -0
  79. package/dist/cjs/utils/customTheme/color/index.d.ts +4 -0
  80. package/dist/cjs/utils/customTheme/color/index.js +20 -0
  81. package/dist/cjs/utils/customTheme/color/oklch.d.ts +3 -0
  82. package/dist/cjs/utils/customTheme/color/oklch.js +21 -0
  83. package/dist/cjs/utils/customTheme/color/types.d.ts +40 -0
  84. package/dist/cjs/utils/customTheme/color/types.js +2 -0
  85. package/dist/cjs/utils/customTheme/constants.d.ts +31 -0
  86. package/dist/cjs/utils/customTheme/constants.js +49 -0
  87. package/dist/cjs/utils/customTheme/generateBrandPalette.d.ts +10 -0
  88. package/dist/cjs/utils/customTheme/generateBrandPalette.js +25 -0
  89. package/dist/cjs/utils/customTheme/index.d.ts +4 -0
  90. package/dist/cjs/utils/customTheme/index.js +20 -0
  91. package/dist/cjs/utils/getThemeClassnames.d.ts +17 -0
  92. package/dist/cjs/utils/getThemeClassnames.js +38 -0
  93. package/dist/cjs/utils/index.d.ts +7 -0
  94. package/dist/cjs/utils/index.js +23 -0
  95. package/dist/cjs/utils/mergeAppearance.d.ts +7 -0
  96. package/dist/cjs/utils/mergeAppearance.js +17 -0
  97. package/dist/cjs/utils/resolveColorScheme.d.ts +7 -0
  98. package/dist/cjs/utils/resolveColorScheme.js +15 -0
  99. package/dist/cjs/utils/themeBootstrap.d.ts +12 -0
  100. package/dist/cjs/utils/themeBootstrap.js +19 -0
  101. package/dist/esm/components/ChildThemeProvider/ChildThemeProvider.d.ts +24 -0
  102. package/dist/esm/components/ChildThemeProvider/ChildThemeProvider.js +22 -0
  103. package/dist/esm/components/ChildThemeProvider/index.d.ts +1 -0
  104. package/dist/esm/components/ChildThemeProvider/index.js +1 -0
  105. package/dist/esm/components/RootThemeProvider/RootThemeProvider.d.ts +49 -0
  106. package/dist/esm/components/RootThemeProvider/RootThemeProvider.js +27 -0
  107. package/dist/esm/components/RootThemeProvider/index.d.ts +1 -0
  108. package/dist/esm/components/RootThemeProvider/index.js +1 -0
  109. package/dist/esm/components/ThemeProvider/ThemeProvider.d.ts +28 -0
  110. package/dist/esm/components/ThemeProvider/ThemeProvider.js +13 -0
  111. package/dist/esm/components/ThemeProvider/index.d.ts +1 -0
  112. package/dist/esm/components/ThemeProvider/index.js +1 -0
  113. package/dist/esm/components/ThemeScope/ThemeScope.d.ts +32 -0
  114. package/dist/esm/components/ThemeScope/ThemeScope.js +59 -0
  115. package/dist/esm/components/ThemeScope/index.d.ts +1 -0
  116. package/dist/esm/components/ThemeScope/index.js +1 -0
  117. package/dist/esm/components/index.d.ts +3 -0
  118. package/dist/esm/components/index.js +3 -0
  119. package/dist/esm/constants/appearance.d.ts +22 -0
  120. package/dist/esm/constants/appearance.js +24 -0
  121. package/dist/esm/constants/colorScheme.d.ts +17 -0
  122. package/dist/esm/constants/colorScheme.js +20 -0
  123. package/dist/esm/constants/index.d.ts +2 -0
  124. package/dist/esm/constants/index.js +2 -0
  125. package/dist/esm/context/appearanceContext.d.ts +20 -0
  126. package/dist/esm/context/appearanceContext.js +18 -0
  127. package/dist/esm/context/index.d.ts +4 -0
  128. package/dist/esm/context/index.js +2 -0
  129. package/dist/esm/context/themeContext.d.ts +12 -0
  130. package/dist/esm/context/themeContext.js +11 -0
  131. package/dist/esm/hooks/index.d.ts +4 -0
  132. package/dist/esm/hooks/index.js +4 -0
  133. package/dist/esm/hooks/useApplyCustomTheme.d.ts +27 -0
  134. package/dist/esm/hooks/useApplyCustomTheme.js +36 -0
  135. package/dist/esm/hooks/useColorScheme.d.ts +37 -0
  136. package/dist/esm/hooks/useColorScheme.js +76 -0
  137. package/dist/esm/hooks/useIsomorphicLayoutEffect.d.ts +7 -0
  138. package/dist/esm/hooks/useIsomorphicLayoutEffect.js +13 -0
  139. package/dist/esm/hooks/useThemeClassnames.d.ts +11 -0
  140. package/dist/esm/hooks/useThemeClassnames.js +15 -0
  141. package/dist/esm/hooks/useThemeConfig.d.ts +24 -0
  142. package/dist/esm/hooks/useThemeConfig.js +12 -0
  143. package/dist/esm/index.d.ts +7 -0
  144. package/dist/esm/index.js +7 -0
  145. package/dist/esm/keys.d.ts +4 -0
  146. package/dist/esm/keys.js +16 -0
  147. package/dist/esm/ssr.d.ts +15 -0
  148. package/dist/esm/ssr.js +15 -0
  149. package/dist/esm/store/globalStore.d.ts +29 -0
  150. package/dist/esm/store/globalStore.js +69 -0
  151. package/dist/esm/store/index.d.ts +1 -0
  152. package/dist/esm/store/index.js +1 -0
  153. package/dist/esm/types/appearance.d.ts +23 -0
  154. package/dist/esm/types/appearance.js +2 -0
  155. package/dist/esm/types/colorScheme.d.ts +5 -0
  156. package/dist/esm/types/colorScheme.js +1 -0
  157. package/dist/esm/types/index.d.ts +2 -0
  158. package/dist/esm/types/index.js +2 -0
  159. package/dist/esm/utils/colorSchemeFromHeaders.d.ts +22 -0
  160. package/dist/esm/utils/colorSchemeFromHeaders.js +40 -0
  161. package/dist/esm/utils/colorSchemeStorage.d.ts +35 -0
  162. package/dist/esm/utils/colorSchemeStorage.js +81 -0
  163. package/dist/esm/utils/customTheme/buildBrandPaletteCss.d.ts +11 -0
  164. package/dist/esm/utils/customTheme/buildBrandPaletteCss.js +32 -0
  165. package/dist/esm/utils/customTheme/buildBrandPaletteVars.d.ts +11 -0
  166. package/dist/esm/utils/customTheme/buildBrandPaletteVars.js +39 -0
  167. package/dist/esm/utils/customTheme/color/apca.d.ts +4 -0
  168. package/dist/esm/utils/customTheme/color/apca.js +59 -0
  169. package/dist/esm/utils/customTheme/color/colorSpace.d.ts +5 -0
  170. package/dist/esm/utils/customTheme/color/colorSpace.js +68 -0
  171. package/dist/esm/utils/customTheme/color/conversions.d.ts +17 -0
  172. package/dist/esm/utils/customTheme/color/conversions.js +91 -0
  173. package/dist/esm/utils/customTheme/color/hex.d.ts +10 -0
  174. package/dist/esm/utils/customTheme/color/hex.js +29 -0
  175. package/dist/esm/utils/customTheme/color/index.d.ts +4 -0
  176. package/dist/esm/utils/customTheme/color/index.js +4 -0
  177. package/dist/esm/utils/customTheme/color/oklch.d.ts +3 -0
  178. package/dist/esm/utils/customTheme/color/oklch.js +18 -0
  179. package/dist/esm/utils/customTheme/color/types.d.ts +40 -0
  180. package/dist/esm/utils/customTheme/color/types.js +1 -0
  181. package/dist/esm/utils/customTheme/constants.d.ts +31 -0
  182. package/dist/esm/utils/customTheme/constants.js +46 -0
  183. package/dist/esm/utils/customTheme/generateBrandPalette.d.ts +10 -0
  184. package/dist/esm/utils/customTheme/generateBrandPalette.js +22 -0
  185. package/dist/esm/utils/customTheme/index.d.ts +4 -0
  186. package/dist/esm/utils/customTheme/index.js +4 -0
  187. package/dist/esm/utils/getThemeClassnames.d.ts +17 -0
  188. package/dist/esm/utils/getThemeClassnames.js +34 -0
  189. package/dist/esm/utils/index.d.ts +7 -0
  190. package/dist/esm/utils/index.js +7 -0
  191. package/dist/esm/utils/mergeAppearance.d.ts +7 -0
  192. package/dist/esm/utils/mergeAppearance.js +14 -0
  193. package/dist/esm/utils/resolveColorScheme.d.ts +7 -0
  194. package/dist/esm/utils/resolveColorScheme.js +12 -0
  195. package/dist/esm/utils/themeBootstrap.d.ts +12 -0
  196. package/dist/esm/utils/themeBootstrap.js +16 -0
  197. package/dist/tsconfig.cjs.tsbuildinfo +1 -0
  198. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  199. package/package.json +45 -0
  200. package/src/components/ChildThemeProvider/ChildThemeProvider.tsx +54 -0
  201. package/src/components/ChildThemeProvider/index.ts +1 -0
  202. package/src/components/RootThemeProvider/RootThemeProvider.tsx +87 -0
  203. package/src/components/RootThemeProvider/index.ts +1 -0
  204. package/src/components/ThemeProvider/ThemeProvider.tsx +41 -0
  205. package/src/components/ThemeProvider/index.ts +1 -0
  206. package/src/components/ThemeScope/ThemeScope.tsx +106 -0
  207. package/src/components/ThemeScope/index.ts +1 -0
  208. package/src/components/index.ts +3 -0
  209. package/src/constants/appearance.ts +27 -0
  210. package/src/constants/colorScheme.ts +23 -0
  211. package/src/constants/index.ts +2 -0
  212. package/src/context/appearanceContext.tsx +39 -0
  213. package/src/context/index.ts +4 -0
  214. package/src/context/themeContext.ts +18 -0
  215. package/src/hooks/index.ts +4 -0
  216. package/src/hooks/useApplyCustomTheme.ts +56 -0
  217. package/src/hooks/useColorScheme.ts +131 -0
  218. package/src/hooks/useIsomorphicLayoutEffect.ts +17 -0
  219. package/src/hooks/useThemeClassnames.ts +18 -0
  220. package/src/hooks/useThemeConfig.tsx +44 -0
  221. package/src/index.ts +7 -0
  222. package/src/keys.ts +18 -0
  223. package/src/ssr.ts +25 -0
  224. package/src/store/globalStore.ts +93 -0
  225. package/src/store/index.ts +1 -0
  226. package/src/types/appearance.ts +29 -0
  227. package/src/types/colorScheme.ts +7 -0
  228. package/src/types/index.ts +2 -0
  229. package/src/utils/colorSchemeFromHeaders.ts +57 -0
  230. package/src/utils/colorSchemeStorage.ts +117 -0
  231. package/src/utils/customTheme/buildBrandPaletteCss.ts +37 -0
  232. package/src/utils/customTheme/buildBrandPaletteVars.ts +56 -0
  233. package/src/utils/customTheme/color/apca.ts +74 -0
  234. package/src/utils/customTheme/color/colorSpace.ts +82 -0
  235. package/src/utils/customTheme/color/conversions.ts +113 -0
  236. package/src/utils/customTheme/color/hex.ts +39 -0
  237. package/src/utils/customTheme/color/index.ts +4 -0
  238. package/src/utils/customTheme/color/oklch.ts +22 -0
  239. package/src/utils/customTheme/color/types.ts +32 -0
  240. package/src/utils/customTheme/constants.ts +57 -0
  241. package/src/utils/customTheme/generateBrandPalette.ts +30 -0
  242. package/src/utils/customTheme/index.ts +4 -0
  243. package/src/utils/getThemeClassnames.ts +40 -0
  244. package/src/utils/index.ts +7 -0
  245. package/src/utils/mergeAppearance.ts +16 -0
  246. package/src/utils/resolveColorScheme.ts +15 -0
  247. package/src/utils/themeBootstrap.ts +18 -0
@@ -0,0 +1,87 @@
1
+ import { staticStore } from '@cloud-ru/ds-context-kit';
2
+ import { ReactNode, RefObject, useMemo } from 'react';
3
+
4
+ import {
5
+ ThemeAppearanceContextValue,
6
+ ThemeAppearanceStore,
7
+ ThemeAppearanceStoreProvider,
8
+ } from '../../context/appearanceContext';
9
+ import { ThemeAppearance } from '../../types/appearance';
10
+ import { ThemeScope } from '../ThemeScope';
11
+
12
+ export type RootThemeProviderProps = {
13
+ /**
14
+ * Оформление приложения. Используется в static-режиме (один React-корень: SSR — одно значение на
15
+ * запрос, либо CSR с собственным state, напр. `colorScheme` из `useColorScheme`). Для multi-root
16
+ * (single-spa) — см. `store`.
17
+ */
18
+ value?: ThemeAppearance;
19
+ /**
20
+ * Внешний реактивный стор оформления (`getGlobalThemeStore().store`). Если задан — приоритетнее
21
+ * `value`; подписанные провайдеры обновляются при смене темы без перерендера провайдера. Так один
22
+ * глобальный стор охватывает все микрофронты. Сеттер для shell — `getGlobalThemeStore().setAppearance`.
23
+ */
24
+ store?: ThemeAppearanceStore;
25
+ /**
26
+ * Внешний элемент для полного набора `sn-*` (обычно `<html>`/`<body>`). Если не задан — провайдер
27
+ * оборачивает children в `<div>` с этим набором.
28
+ */
29
+ rootRef?: RefObject<HTMLElement | null>;
30
+ /** Дополнительный класс на wrapper-`<div>` (паддинги/фон). Только в wrapper-режиме (без `rootRef`). */
31
+ className?: string;
32
+ /**
33
+ * Кастомный бренд-цвет потребителя (hex `#rrggbb`) для white-label. Генерирует бренд-палитру из
34
+ * seed-цвета и инжектит scoped `<style>` на бренд-классы этого поддерева. Правило на бренд-классе
35
+ * (а не inline на одном элементе) переживает переэмиты `sn-*` вложенными компонентами (Table и т.п.),
36
+ * поэтому кастомный цвет доходит до всей вложенности. Невалидный hex игнорируется. Глобальная
37
+ * (app-root) альтернатива для порталов — хук `useApplyCustomTheme`.
38
+ */
39
+ brandColor?: string;
40
+ /** CSP-`nonce` для инжектируемого `<style>` кастомного бренд-цвета. */
41
+ nonce?: string;
42
+ children: ReactNode;
43
+ };
44
+
45
+ /**
46
+ * Корневой провайдер оформления. Ставится один раз в корне приложения: задаёт контекст осей
47
+ * (`colorScheme`/`brand`/`brandRole`/`density`/`acrylic`) и эмитит полный набор `sn-*` на корень
48
+ * (`rootRef`) либо на собственный wrapper. Объект контекста — глобальный синглтон (`Symbol.for`),
49
+ * поэтому провайдер работает даже при нескольких версиях пакета в разных микрофронтах, а потребитель
50
+ * читает ближайший провайдер. Локальные переопределения осей в поддереве — через `ChildThemeProvider`.
51
+ *
52
+ * Этот провайдер НЕ управляет состоянием цветовой схемы (override/system/cross-tab) — это `useColorScheme`;
53
+ * результат передаётся в `value.colorScheme`. Не путать со старым `ThemeProvider` (themeMap/changeTheme).
54
+ */
55
+ export function RootThemeProvider({
56
+ value,
57
+ store,
58
+ rootRef,
59
+ className,
60
+ brandColor,
61
+ nonce,
62
+ children,
63
+ }: RootThemeProviderProps) {
64
+ const { colorScheme, brand, brandRole, density, acrylic } = value ?? {};
65
+
66
+ // Мемо по значениям осей (не по идентичности объекта `value`): иначе литерал `value={{…}}` у
67
+ // потребителя пересоздавал бы стор на каждом рендере. Сеттер в static-режиме не нужен — значением
68
+ // владеет сам потребитель через проп `value`.
69
+ const staticThemeStore = useMemo(
70
+ () =>
71
+ staticStore<ThemeAppearanceContextValue>({
72
+ appearance: { colorScheme, brand, brandRole, density, acrylic },
73
+ setAppearance: undefined,
74
+ }),
75
+ [colorScheme, brand, brandRole, density, acrylic],
76
+ );
77
+
78
+ return (
79
+ <ThemeAppearanceStoreProvider store={store ?? staticThemeStore}>
80
+ <ThemeScope rootRef={rootRef} className={className} brandColor={brandColor} nonce={nonce}>
81
+ {children}
82
+ </ThemeScope>
83
+ </ThemeAppearanceStoreProvider>
84
+ );
85
+ }
86
+
87
+ RootThemeProvider.displayName = 'RootThemeProvider';
@@ -0,0 +1 @@
1
+ export * from './RootThemeProvider';
@@ -0,0 +1,41 @@
1
+ import { ReactNode, useMemo } from 'react';
2
+
3
+ import { ThemeValueProvider } from '../../context/themeContext';
4
+ import { useThemeConfig } from '../../hooks/useThemeConfig';
5
+
6
+ type UseThemePropsWithDefaultTheme = {
7
+ /**
8
+ * Объект с указанием соответсвия темы и css-класса
9
+ */
10
+ themeMap: Record<string, string>;
11
+ /**
12
+ * Значение темы по умолчанию
13
+ */
14
+ defaultTheme: string;
15
+ };
16
+
17
+ type UseThemeProps = {
18
+ /**
19
+ * Объект с указанием соответсвия темы и css-класса
20
+ */
21
+ themeMap: Record<string, string>;
22
+ };
23
+
24
+ export type ThemeProviderProps = {
25
+ /**
26
+ * Дети, которые будут обёрнуты в провайдер
27
+ */
28
+ children: ReactNode;
29
+ } & (UseThemePropsWithDefaultTheme | UseThemeProps);
30
+
31
+ /**
32
+ * Провайдер, предназначенный для работы с темами
33
+ */
34
+ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
35
+ const { theme, themeClassName, changeTheme } = useThemeConfig(props);
36
+
37
+ // Мемоизируем объект значения (createSharedContext оборачивает его в staticStore по ссылке).
38
+ const value = useMemo(() => ({ theme, themeClassName, changeTheme }), [theme, themeClassName, changeTheme]);
39
+
40
+ return <ThemeValueProvider value={value}>{children}</ThemeValueProvider>;
41
+ }
@@ -0,0 +1 @@
1
+ export * from './ThemeProvider';
@@ -0,0 +1,106 @@
1
+ import cn from 'classnames';
2
+ import { ReactNode, RefObject, useId, useMemo } from 'react';
3
+
4
+ import { useThemeAppearance } from '../../context/appearanceContext';
5
+ import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
6
+ import { buildBrandPaletteCss } from '../../utils/customTheme';
7
+ import { getThemeClassnameList } from '../../utils/getThemeClassnames';
8
+
9
+ // Атрибут-маркер поддерева для scope кастомной палитры. Правило нацеливается на бренд-классы внутри
10
+ // этого маркера (см. `buildBrandPaletteCss`), поэтому переживает переэмиты `sn-*` вложенными обёртками.
11
+ const BRAND_SCOPE_ATTR = 'data-ds-brand-scope';
12
+
13
+ export type ThemeScopeProps = {
14
+ /**
15
+ * Внешний элемент, на который ставится полный набор `sn-*` (обычно `<html>`/`<body>` для корня).
16
+ * Если не задан — `ThemeScope` рендерит собственный wrapper `<div>` с этим набором.
17
+ */
18
+ rootRef?: RefObject<HTMLElement | null>;
19
+ /**
20
+ * Дополнительный класс на wrapper-`<div>` (паддинги/фон вокруг темы). Действует только в wrapper-режиме;
21
+ * при `rootRef` игнорируется (внешним элементом владеет потребитель).
22
+ */
23
+ className?: string;
24
+ /**
25
+ * Кастомный бренд-цвет (hex). Инжектит scoped `<style>`, переопределяющий бренд-палитру на бренд-классах
26
+ * поддерева. В отличие от inline-переменных, правило на бренд-классе переживает переэмиты `sn-*` внутри
27
+ * (Table/Stepper и т.п.), поэтому кастомный цвет доходит до вложенных компонентов.
28
+ */
29
+ brandColor?: string;
30
+ /** CSP-`nonce` для инжектируемого `<style>`. */
31
+ nonce?: string;
32
+ children: ReactNode;
33
+ };
34
+
35
+ /**
36
+ * Внутренний помощник Root/Child-провайдеров: читает ближайшее оформление (= собственный стор
37
+ * провайдера, под которым отрендерен) и эмитит полный набор `sn-*`. Реактивен — при смене темы
38
+ * пересобирает и переустанавливает классы.
39
+ *
40
+ * - `rootRef` задан → классы добавляются на внешний элемент через эффект (без лишнего DOM-узла);
41
+ * при смене набора старые классы снимаются, новые ставятся.
42
+ * - `rootRef` не задан → классы навешиваются на wrapper `<div>` (+ `className` потребителя).
43
+ */
44
+ export function ThemeScope({ rootRef, className, brandColor, nonce, children }: ThemeScopeProps) {
45
+ const { appearance } = useThemeAppearance();
46
+ // Список токенов — источник истины: `classList.add(...)` принимает их напрямую (без `split`),
47
+ // а wrapper-режим джойнит через `cn`. Мемо по `appearance` держит ссылку стабильной между
48
+ // рендерами, поэтому эффект переустанавливает классы только при реальной смене темы.
49
+ const themeClasses = useMemo(() => getThemeClassnameList(appearance), [appearance]);
50
+
51
+ const scopeId = useId();
52
+ const brandCss = useMemo(
53
+ () => (brandColor ? buildBrandPaletteCss(brandColor, `[${BRAND_SCOPE_ATTR}="${scopeId}"]`) : null),
54
+ [brandColor, scopeId],
55
+ );
56
+
57
+ // Layout-эффект (не `useEffect`): классы/scope-маркер ставим на внешний элемент синхронно до пейнта,
58
+ // иначе первый кадр рендерится без набора `sn-*` (и без scoped бренд-правила) — видно моргание темы.
59
+ useIsomorphicLayoutEffect(() => {
60
+ const element = rootRef?.current;
61
+ if (!element) {
62
+ return undefined;
63
+ }
64
+
65
+ element.classList.add(...themeClasses);
66
+
67
+ return () => {
68
+ element.classList.remove(...themeClasses);
69
+ };
70
+ }, [rootRef, themeClasses]);
71
+
72
+ // В rootRef-режиме scope-маркер ставим на внешний элемент, чтобы scoped-правило достало его поддерево
73
+ // (в wrapper-режиме маркер — атрибут `div`а ниже).
74
+ useIsomorphicLayoutEffect(() => {
75
+ const element = rootRef?.current;
76
+ if (!element || !brandCss) {
77
+ return undefined;
78
+ }
79
+
80
+ element.setAttribute(BRAND_SCOPE_ATTR, scopeId);
81
+
82
+ return () => {
83
+ element.removeAttribute(BRAND_SCOPE_ATTR);
84
+ };
85
+ }, [rootRef, brandCss, scopeId]);
86
+
87
+ // `<style>` рендерится в дерево (React управляет монтированием/размонтированием, без ручной инъекции).
88
+ // В body он не имеет визуального бокса — только применяет правило по scoped-селектору.
89
+ const styleTag = brandCss ? <style nonce={nonce}>{brandCss}</style> : null;
90
+
91
+ if (rootRef) {
92
+ return (
93
+ <>
94
+ {styleTag}
95
+ {children}
96
+ </>
97
+ );
98
+ }
99
+
100
+ return (
101
+ <div className={cn(themeClasses, className)} data-ds-brand-scope={brandCss ? scopeId : undefined}>
102
+ {styleTag}
103
+ {children}
104
+ </div>
105
+ );
106
+ }
@@ -0,0 +1 @@
1
+ export * from './ThemeScope';
@@ -0,0 +1,3 @@
1
+ export * from './RootThemeProvider';
2
+ export * from './ChildThemeProvider';
3
+ export * from './ThemeProvider';
@@ -0,0 +1,27 @@
1
+ // Оси оформления дизайн-системы, спроецированные на классы `sn-*` из `@cloud-ru/figma-variables`.
2
+ // Чистый модуль (без React/DOM) — реэкспортируется из `@cloud-ru/ds-theme/ssr` (RSC-safe).
3
+
4
+ /** Плотность раскладки. Класс `sn-comfort` / `sn-compact` / `sn-spacious`. */
5
+ export const DENSITY = {
6
+ Comfort: 'comfort',
7
+ Compact: 'compact',
8
+ Spacious: 'spacious',
9
+ } as const;
10
+
11
+ /** Бренд. Класс `sn-brandA` / `sn-brandB` / `sn-brandC` / `sn-brandD` / `sn-brandE`. */
12
+ export const BRAND = {
13
+ A: 'brandA',
14
+ B: 'brandB',
15
+ C: 'brandC',
16
+ D: 'brandD',
17
+ E: 'brandE',
18
+ } as const;
19
+
20
+ /** Роль бренда (палитра). Класс `sn-main` / `sn-alter` … */
21
+ export const BRAND_ROLE = {
22
+ Main: 'main',
23
+ Alter: 'alter',
24
+ Alter2: 'alter2',
25
+ Alter3: 'alter3',
26
+ Alter4: 'alter4',
27
+ } as const;
@@ -0,0 +1,23 @@
1
+ // Цветовая схема = класс `sn-light`/`sn-dark` на корне. Чистый модуль (RSC-safe), реэкспорт из `@cloud-ru/ds-theme/ssr`.
2
+ // Персиста по умолчанию нет: `useColorScheme` держит выбор in-memory, а cookie/localStorage подключает
3
+ // потребитель явным адаптером. Этот ключ — дефолтное имя для опциональных адаптеров и SSR/bootstrap-хелперов.
4
+
5
+ /** Разрешённая цветовая схема (то, что реально применяется на корень). */
6
+ export const COLOR_SCHEME = {
7
+ Light: 'light',
8
+ Dark: 'dark',
9
+ } as const;
10
+
11
+ /** Пользовательский выбор: фиксированная схема или `system` (следовать `prefers-color-scheme`). */
12
+ export const THEME_OVERRIDE = {
13
+ Light: 'light',
14
+ Dark: 'dark',
15
+ System: 'system',
16
+ } as const;
17
+
18
+ /**
19
+ * Дефолтное имя ключа персиста override для опциональных адаптеров (cookie/localStorage) и
20
+ * SSR/bootstrap-хелперов. Namespace'нут под DS, чтобы не пересекаться с ключами хост-приложения.
21
+ * Конкретный адаптер может переопределить его через `storageKey`.
22
+ */
23
+ export const THEME_OVERRIDE_STORAGE_KEY = 'snack-uikit-theme';
@@ -0,0 +1,2 @@
1
+ export * from './appearance';
2
+ export * from './colorScheme';
@@ -0,0 +1,39 @@
1
+ import { createSharedStoreContext, ExternalStore } from '@cloud-ru/ds-context-kit';
2
+
3
+ import { THEME_KEYS } from '../keys';
4
+ import { getGlobalThemeStore } from '../store/globalStore';
5
+ import { ThemeAppearance } from '../types/appearance';
6
+
7
+ /**
8
+ * Значение контекста оформления: слитые оси (`appearance`) + опциональный сеттер. Сеттер задаёт
9
+ * только корневой провайдер (`RootThemeProvider`) — у `ChildThemeProvider` он `undefined` (детям
10
+ * менять глобальное значение нельзя, они только локально переопределяют оси).
11
+ */
12
+ export type ThemeAppearanceContextValue = {
13
+ appearance: ThemeAppearance;
14
+ setAppearance?: (patch: Partial<ThemeAppearance>) => void;
15
+ };
16
+
17
+ /** Внешний стор оформления (формат `useSyncExternalStore`). Mutable-global — см. `getGlobalThemeStore`. */
18
+ export type ThemeAppearanceStore = ExternalStore<ThemeAppearanceContextValue>;
19
+
20
+ // Отдельный домен `theme-appearance` — НЕ пересекается с `providerKey('theme', 1)` старого
21
+ // `ThemeContext` (themeMap/changeTheme). Объект контекста — `Symbol.for`-синглтон, общий для всех
22
+ // версий пакета, поэтому потребитель в микрофронте читает ближайший провайдер независимо от версии.
23
+ //
24
+ // Дефолт контекста = глобальный singleton-стор оформления. Поэтому компонент, вызывающий
25
+ // `useThemeClassnames` (мобильная обёртка, `ChildThemeProvider`, портал вне `<body>`) БЕЗ
26
+ // `RootThemeProvider` в своём корне, реэмитит полный набор `sn-*` не с пустым оформлением
27
+ // (теряя `colorScheme`/`density`), а с тем, что shell выставил в `getGlobalThemeStore` из источника
28
+ // истины. Так микрофронту не нужен провайдер. Явный `RootThemeProvider` затеняет дефолт (back-compat).
29
+ const { StoreProvider, useStoreValue } = createSharedStoreContext<ThemeAppearanceContextValue>(
30
+ THEME_KEYS.context,
31
+ getGlobalThemeStore().store,
32
+ );
33
+
34
+ export { StoreProvider as ThemeAppearanceStoreProvider };
35
+
36
+ /** Возвращает ближайшее (уже слитое по цепочке провайдеров) значение оформления. Реактивно к стору. */
37
+ export function useThemeAppearance(): ThemeAppearanceContextValue {
38
+ return useStoreValue();
39
+ }
@@ -0,0 +1,4 @@
1
+ export { useThemeAppearance } from './appearanceContext';
2
+ export type { ThemeAppearanceContextValue, ThemeAppearanceStore } from './appearanceContext';
3
+ export { useThemeContext } from './themeContext';
4
+ export type { ThemeContextType } from './themeContext';
@@ -0,0 +1,18 @@
1
+ import { createSharedContext, providerKey } from '@cloud-ru/ds-context-kit';
2
+
3
+ export type ThemeContextType = {
4
+ theme: string | undefined;
5
+ themeClassName: string | undefined;
6
+ changeTheme: ((theme: string) => void) | undefined;
7
+ };
8
+
9
+ // Общий контекст темы через @cloud-ru/ds-context-kit (Symbol.for-синглтон, SSR-safe). `ThemeValueProvider`
10
+ // внутренний — публично доступны `ThemeProvider` (обёртка) и `useThemeContext`.
11
+ export const { Provider: ThemeValueProvider, useValue: useThemeContext } = createSharedContext<ThemeContextType>({
12
+ key: providerKey('theme', 1),
13
+ defaultValue: {
14
+ theme: undefined,
15
+ themeClassName: undefined,
16
+ changeTheme: undefined,
17
+ },
18
+ });
@@ -0,0 +1,4 @@
1
+ export * from './useApplyCustomTheme';
2
+ export * from './useColorScheme';
3
+ export * from './useThemeClassnames';
4
+ export * from './useThemeConfig';
@@ -0,0 +1,56 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { buildBrandPaletteCss } from '../utils/customTheme';
4
+ import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
5
+
6
+ export type UseApplyCustomThemeProps = {
7
+ /** Бренд-цвет потребителя (hex `#rrggbb`). Из него генерируется бренд-палитра. */
8
+ color: string;
9
+ /** Применять ли кастомную палитру. При `false` `<style>` не инжектится (а ранее добавленный снимается). */
10
+ enabled?: boolean;
11
+ /**
12
+ * CSS-скоуп — селектор корня поддерева (напр. `#app`, `[data-x]`). Без него правило глобальное:
13
+ * переопределяет бренд-классы на всей странице (white-label в корне приложения — покрывает и порталы,
14
+ * и переэмиченные внутренние обёртки). Со скоупом область ограничена его поддеревом.
15
+ */
16
+ scope?: string;
17
+ /** CSP-`nonce` для инжектируемого `<style>`. */
18
+ nonce?: string;
19
+ };
20
+
21
+ /**
22
+ * Применяет кастомный бренд-цвет: генерирует из `color` бренд-палитру и инжектит `<style>`,
23
+ * переопределяющий её на бренд-классах (`.sn-brandA/B/C/D/E`), а не inline на одном элементе — поэтому
24
+ * цвет переживает переэмиты `sn-*` вложенными компонентами и доходит до порталов. Семантический слой
25
+ * каскадит из палитры, красятся обе схемы.
26
+ *
27
+ * Императивная (обычно app-root) альтернатива декларативному пропу `brandColor` у `RootThemeProvider`.
28
+ *
29
+ * ```tsx
30
+ * useApplyCustomTheme({ color: brand.primaryColor, enabled: Boolean(brand), nonce });
31
+ * ```
32
+ */
33
+ export function useApplyCustomTheme({ color, enabled = true, scope, nonce }: UseApplyCustomThemeProps): void {
34
+ const css = useMemo(() => buildBrandPaletteCss(color, scope), [color, scope]);
35
+
36
+ // Layout-эффект (не `useEffect`): `<style>` добавляем в `head` синхронно до пейнта, иначе первый кадр
37
+ // успевает отрисоваться с дефолтным акцентом и виден проблеск старого цвета при смене бренд-цвета.
38
+ /* eslint-disable @cloud-ru/ssr-safe-react/domApi -- DOM трогаем только в layout-эффекте под guard `typeof document` */
39
+ useIsomorphicLayoutEffect(() => {
40
+ if (!enabled || !css || typeof document === 'undefined') {
41
+ return undefined;
42
+ }
43
+
44
+ const styleElement = document.createElement('style');
45
+ if (nonce) {
46
+ styleElement.setAttribute('nonce', nonce);
47
+ }
48
+ styleElement.textContent = css;
49
+ document.head.appendChild(styleElement);
50
+
51
+ return () => {
52
+ styleElement.remove();
53
+ };
54
+ }, [css, enabled, nonce]);
55
+ /* eslint-enable @cloud-ru/ssr-safe-react/domApi */
56
+ }
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+
5
+ import { COLOR_SCHEME, THEME_OVERRIDE } from '../constants/colorScheme';
6
+ import { ColorScheme, ThemeOverride } from '../types/colorScheme';
7
+ import { ColorSchemeStorage, createInMemoryColorSchemeStorage } from '../utils/colorSchemeStorage';
8
+ import { resolveColorScheme } from '../utils/resolveColorScheme';
9
+
10
+ /* eslint-disable @cloud-ru/ssr-safe-react/domApi -- guarded by isBrowser(); хук помечен 'use client' */
11
+
12
+ function isBrowser(): boolean {
13
+ return Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
14
+ }
15
+
16
+ const SYSTEM_DARK_QUERY = '(prefers-color-scheme: dark)';
17
+
18
+ function getSystemPrefersDark(): boolean {
19
+ return isBrowser() && window.matchMedia(SYSTEM_DARK_QUERY).matches;
20
+ }
21
+
22
+ function applyColorScheme(target: HTMLElement, colorScheme: ColorScheme): void {
23
+ target.classList.toggle('sn-dark', colorScheme === COLOR_SCHEME.Dark);
24
+ target.classList.toggle('sn-light', colorScheme === COLOR_SCHEME.Light);
25
+ }
26
+
27
+ // Общий синглтон in-memory-адаптера: несколько `useColorScheme` без своего `storage` смотрят в один
28
+ // источник, поэтому переключатели в разных поддеревьях синхронны в пределах сессии.
29
+ const defaultColorSchemeStorage = createInMemoryColorSchemeStorage();
30
+
31
+ export type UseColorSchemeOptions = {
32
+ /**
33
+ * Server-resolved начальный override (например, `getThemeOverrideFromHeaders(headers)` из cookie
34
+ * или из бэкенд-сессии). Делает первый рендер детерминированным и совпадающим с SSR — без
35
+ * hydration mismatch и без прыжка подсветки переключателя. Если не задан — старт с `system`.
36
+ */
37
+ initialOverride?: ThemeOverride;
38
+ /**
39
+ * Адаптер персиста. По умолчанию — in-memory (без персиста между перезагрузками): DS не пишет в
40
+ * cookie/localStorage сам. Подключите явно: `createCookieColorSchemeStorage()` (no-flash SSR) или
41
+ * собственный адаптер (localStorage / бэкенд-сессия).
42
+ */
43
+ storage?: ColorSchemeStorage;
44
+ /** Корень, на который вешается `sn-light`/`sn-dark`. По умолчанию `<html>`. */
45
+ target?: HTMLElement;
46
+ };
47
+
48
+ export type UseColorSchemeResult = {
49
+ /** Итоговая применённая схема (override ?? система). */
50
+ colorScheme: ColorScheme;
51
+ /** Текущий выбор пользователя: `light` / `dark` / `system`. */
52
+ override: ThemeOverride;
53
+ /** Системная тема (`prefers-color-scheme: dark`). */
54
+ systemPrefersDark: boolean;
55
+ /** Сменить выбор: персистит через `storage.write` (cookie / бэкенд). */
56
+ setOverride(next: ThemeOverride): void;
57
+ };
58
+
59
+ /**
60
+ * Управляет цветовой схемой DS: резолвит `override ?? prefers-color-scheme`, вешает `sn-light`/`sn-dark`
61
+ * на корень, реагирует на живое переключение темы ОС (`matchMedia`) и внешние изменения override
62
+ * (`storage.subscribe` — кросс-таб / бэкенд push). No-flash на reload обеспечивает inline
63
+ * `getThemeBootstrapScript` в `<head>` (а на SSR — корректный класс из `getColorSchemeFromHeaders`);
64
+ * хук синхронизирует состояние и переключатель override.
65
+ * @function React hook
66
+ */
67
+ export function useColorScheme(options?: UseColorSchemeOptions): UseColorSchemeResult {
68
+ const { initialOverride, target } = options ?? {};
69
+ const providedStorage = options?.storage;
70
+
71
+ // Дефолт — общий in-memory-адаптер: DS не пишет в cookie/localStorage без явного `storage`.
72
+ const storage = useMemo(() => providedStorage ?? defaultColorSchemeStorage, [providedStorage]);
73
+
74
+ // override инициализируется СИНХРОННО из источника правды, чтобы SegmentControl не «прыгал» с
75
+ // System на реальный выбор после mount: `initialOverride` (server-resolved — бэкенд-сессия / SSR
76
+ // cookie, authoritative) → синхронное чтение store (`storage.read`) → `system`.
77
+ // SSR-консистентность: на сервере `storage.read()` обязан вернуть undefined (in-memory- и cookie-
78
+ // адаптеры так и делают), поэтому и сервер, и первый клиентский рендер берут одно значение — мисматча
79
+ // нет. В чистом клиенте с персист-адаптером (cookie/localStorage) `read()` отдаёт сохранённый выбор
80
+ // сразу при первом рендере, без прыжка; дефолтный in-memory стартует с `system`.
81
+ const [override, setOverrideState] = useState<ThemeOverride>(
82
+ () => initialOverride ?? storage.read() ?? THEME_OVERRIDE.System,
83
+ );
84
+ const [systemPrefersDark, setSystemPrefersDark] = useState(false);
85
+ const [mounted, setMounted] = useState(false);
86
+
87
+ // matchMedia нельзя читать в initializer (на SSR-хосте рассинхронит colorScheme-readout) — системную
88
+ // тему добираем после mount. override уже корректен из initializer, его здесь перечитывать не нужно.
89
+ useEffect(() => {
90
+ setSystemPrefersDark(getSystemPrefersDark());
91
+ setMounted(true);
92
+ }, []);
93
+
94
+ const colorScheme = resolveColorScheme(override, systemPrefersDark);
95
+
96
+ // Применяем класс к корню ТОЛЬКО после mount: до этого корректный класс уже стоит из SSR/bootstrap,
97
+ // и детерминированный первый рендер не должен его перетереть (иначе моргание).
98
+ useEffect(() => {
99
+ if (!mounted || !isBrowser()) {
100
+ return;
101
+ }
102
+ applyColorScheme(target ?? document.documentElement, colorScheme);
103
+ }, [mounted, colorScheme, target]);
104
+
105
+ // Живое переключение темы ОС.
106
+ useEffect(() => {
107
+ if (!isBrowser()) {
108
+ return undefined;
109
+ }
110
+ const mql = window.matchMedia(SYSTEM_DARK_QUERY);
111
+ const onChange = (event: MediaQueryListEvent) => setSystemPrefersDark(event.matches);
112
+ mql.addEventListener('change', onChange);
113
+
114
+ return () => mql.removeEventListener('change', onChange);
115
+ }, []);
116
+
117
+ // Внешняя синхронизация override (кросс-таб через BroadcastChannel / бэкенд push).
118
+ useEffect(() => storage.subscribe(() => setOverrideState(storage.read() ?? THEME_OVERRIDE.System)), [storage]);
119
+
120
+ const setOverride = useCallback(
121
+ (next: ThemeOverride) => {
122
+ setOverrideState(next);
123
+ storage.write(next);
124
+ },
125
+ [storage],
126
+ );
127
+
128
+ return { colorScheme, override, systemPrefersDark, setOverride };
129
+ }
130
+
131
+ /* eslint-enable @cloud-ru/ssr-safe-react/domApi */
@@ -0,0 +1,17 @@
1
+ // eslint-disable-next-line no-restricted-imports -- единственная точка, где берём сырой useLayoutEffect
2
+ import { useEffect, useLayoutEffect } from 'react';
3
+
4
+ /* eslint-disable @cloud-ru/ssr-safe-react/domApi -- это и есть SSR-guard, DOM читается только для детекта браузера */
5
+
6
+ function isBrowser(): boolean {
7
+ return Boolean(typeof window !== 'undefined' && window.document && window.document.createElement);
8
+ }
9
+
10
+ /* eslint-enable @cloud-ru/ssr-safe-react/domApi */
11
+
12
+ /**
13
+ * `useLayoutEffect`, безопасный для SSR: на клиенте — настоящий `useLayoutEffect` (применяет эффект
14
+ * синхронно до пейнта, поэтому инъекция бренд-`<style>` не даёт кадра с дефолтным акцентом — «моргания»),
15
+ * на сервере — `useEffect` (сырой `useLayoutEffect` на сервере варнит и всё равно не выполняется).
16
+ */
17
+ export const useIsomorphicLayoutEffect = isBrowser() ? useLayoutEffect : useEffect;
@@ -0,0 +1,18 @@
1
+ import { useThemeAppearance } from '../context/appearanceContext';
2
+ import { ThemeAppearance } from '../types/appearance';
3
+ import { getThemeClassnames } from '../utils/getThemeClassnames';
4
+
5
+ /**
6
+ * Возвращает полный набор `sn-*` классов = ближайшее оформление из контекста (`RootThemeProvider` +
7
+ * цепочка `ChildThemeProvider`) ⊕ переданные `overrides`. Навешивается на DOM-границу компонента,
8
+ * который хочет переопределить ось (например, мобильная обёртка с `density: 'comfort'`).
9
+ *
10
+ * Поскольку набор включает текущий `colorScheme`/`brand`/`brandRole` из контекста, такая обёртка —
11
+ * самосогласованный scope и трекает переключение темы. Без провайдера в дереве оформление пустое,
12
+ * и поведение совпадает со старым `getThemeClassnames({ density })`.
13
+ */
14
+ export function useThemeClassnames(overrides?: Partial<ThemeAppearance>): string {
15
+ const { appearance } = useThemeAppearance();
16
+
17
+ return getThemeClassnames({ ...appearance, ...overrides });
18
+ }
@@ -0,0 +1,44 @@
1
+ import { useState } from 'react';
2
+
3
+ type UseThemePropsWithDefaultTheme<T extends string> = { themeMap: Record<T, string>; defaultTheme: T };
4
+ type UseThemeProps<T extends string> = {
5
+ /**
6
+ * Объект с указанием соответсвия темы и css-класса
7
+ */
8
+ themeMap: Record<T, string>;
9
+ };
10
+
11
+ function hasDefaultTheme<T extends string>(
12
+ props: UseThemePropsWithDefaultTheme<T> | UseThemeProps<T>,
13
+ ): props is UseThemePropsWithDefaultTheme<T> {
14
+ return Boolean((props as UseThemePropsWithDefaultTheme<T>).defaultTheme);
15
+ }
16
+
17
+ /**
18
+ * Хук для работы с темами
19
+ */
20
+ export function useThemeConfig<T extends string>(
21
+ props: UseThemeProps<T>,
22
+ ): {
23
+ theme: T | undefined;
24
+ themeClassName: string | undefined;
25
+ changeTheme(theme: T): void;
26
+ };
27
+
28
+ export function useThemeConfig<T extends string>(
29
+ props: UseThemePropsWithDefaultTheme<T>,
30
+ ): {
31
+ theme: T;
32
+ themeClassName: string;
33
+ changeTheme(theme: T): void;
34
+ };
35
+
36
+ export function useThemeConfig<T extends string>(props: UseThemePropsWithDefaultTheme<T> | UseThemeProps<T>) {
37
+ const [theme, setTheme] = useState(hasDefaultTheme(props) ? props.defaultTheme : undefined);
38
+
39
+ return {
40
+ theme,
41
+ themeClassName: theme ? props.themeMap[theme] : undefined,
42
+ changeTheme: setTheme,
43
+ };
44
+ }