@pyreon/rocketstyle 0.11.0 → 0.11.2

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 (51) hide show
  1. package/package.json +14 -12
  2. package/src/__tests__/attrs.test.ts +190 -0
  3. package/src/__tests__/chaining.test.ts +86 -0
  4. package/src/__tests__/collection.test.ts +35 -0
  5. package/src/__tests__/compose.test.ts +36 -0
  6. package/src/__tests__/context.test.ts +200 -0
  7. package/src/__tests__/createLocalProvider.test.ts +248 -0
  8. package/src/__tests__/dimensions.test.ts +183 -0
  9. package/src/__tests__/e2e-styler.test.ts +291 -0
  10. package/src/__tests__/hooks.test.ts +207 -0
  11. package/src/__tests__/isRocketComponent.test.ts +48 -0
  12. package/src/__tests__/misc.test.ts +204 -0
  13. package/src/__tests__/providerConsumer.test.ts +248 -0
  14. package/src/__tests__/rocketstyleIntegration.test.ts +615 -0
  15. package/src/__tests__/themeUtils.test.ts +463 -0
  16. package/src/cache/LocalThemeManager.ts +14 -0
  17. package/src/cache/index.ts +3 -0
  18. package/src/constants/booleanTags.ts +32 -0
  19. package/src/constants/defaultDimensions.ts +23 -0
  20. package/src/constants/index.ts +44 -0
  21. package/src/context/context.ts +51 -0
  22. package/src/context/createLocalProvider.ts +84 -0
  23. package/src/context/localContext.ts +37 -0
  24. package/src/hoc/index.ts +3 -0
  25. package/src/hoc/rocketstyleAttrsHoc.ts +63 -0
  26. package/src/hooks/index.ts +4 -0
  27. package/src/hooks/usePseudoState.ts +79 -0
  28. package/src/hooks/useTheme.ts +36 -0
  29. package/src/index.ts +77 -0
  30. package/src/init.ts +93 -0
  31. package/src/isRocketComponent.ts +16 -0
  32. package/src/rocketstyle.ts +320 -0
  33. package/src/types/attrs.ts +13 -0
  34. package/src/types/config.ts +48 -0
  35. package/src/types/configuration.ts +69 -0
  36. package/src/types/dimensions.ts +106 -0
  37. package/src/types/hoc.ts +5 -0
  38. package/src/types/pseudo.ts +19 -0
  39. package/src/types/rocketComponent.ts +24 -0
  40. package/src/types/rocketstyle.ts +156 -0
  41. package/src/types/styles.ts +46 -0
  42. package/src/types/theme.ts +19 -0
  43. package/src/types/utils.ts +55 -0
  44. package/src/utils/attrs.ts +134 -0
  45. package/src/utils/chaining.ts +58 -0
  46. package/src/utils/collection.ts +9 -0
  47. package/src/utils/compose.ts +11 -0
  48. package/src/utils/dimensions.ts +126 -0
  49. package/src/utils/statics.ts +44 -0
  50. package/src/utils/styles.ts +18 -0
  51. package/src/utils/theme.ts +196 -0
@@ -0,0 +1,463 @@
1
+ import {
2
+ calculateChainOptions,
3
+ getDimensionThemes,
4
+ getTheme,
5
+ getThemeByMode,
6
+ getThemeFromChain,
7
+ themeModeCallback,
8
+ } from "../utils/theme"
9
+
10
+ describe("themeModeCallback", () => {
11
+ it("returns light value for light mode", () => {
12
+ const cb = themeModeCallback("lightVal", "darkVal")
13
+ expect(cb("light")).toBe("lightVal")
14
+ })
15
+
16
+ it("returns light value when mode is falsy", () => {
17
+ const cb = themeModeCallback("lightVal", "darkVal")
18
+ expect(cb(undefined as any)).toBe("lightVal")
19
+ expect(cb("" as any)).toBe("lightVal")
20
+ })
21
+
22
+ it("returns dark value for dark mode", () => {
23
+ const cb = themeModeCallback("lightVal", "darkVal")
24
+ expect(cb("dark")).toBe("darkVal")
25
+ })
26
+ })
27
+
28
+ describe("getThemeFromChain", () => {
29
+ it("returns empty for empty array", () => {
30
+ expect(getThemeFromChain([], {})).toEqual({})
31
+ })
32
+
33
+ it("returns empty for null", () => {
34
+ expect(getThemeFromChain(null, {})).toEqual({})
35
+ })
36
+
37
+ it("returns empty for undefined", () => {
38
+ expect(getThemeFromChain(undefined, {})).toEqual({})
39
+ })
40
+
41
+ it("evaluates chain of callbacks and deep-merges results", () => {
42
+ const fn1 = (_theme: any) => ({ color: "blue" })
43
+ const fn2 = (theme: any) => ({ bg: theme.primary })
44
+ const result = getThemeFromChain([fn1, fn2], { primary: "red" })
45
+ expect(result).toEqual({ color: "blue", bg: "red" })
46
+ })
47
+
48
+ it("later callbacks override earlier ones", () => {
49
+ const fn1 = () => ({ color: "blue" })
50
+ const fn2 = () => ({ color: "red" })
51
+ const result = getThemeFromChain([fn1, fn2], {})
52
+ expect(result).toEqual({ color: "red" })
53
+ })
54
+
55
+ it("passes themeModeCallback and config.css to each callback", () => {
56
+ const fn = vi.fn(() => ({}))
57
+ getThemeFromChain([fn], { rootSize: 16 })
58
+ expect(fn).toHaveBeenCalledWith({ rootSize: 16 }, expect.any(Function), expect.anything())
59
+ })
60
+ })
61
+
62
+ describe("getDimensionThemes", () => {
63
+ it("returns empty for empty dimensions", () => {
64
+ expect(getDimensionThemes({}, { dimensions: {} })).toEqual({})
65
+ })
66
+
67
+ it("returns empty when dimensions is falsy", () => {
68
+ expect(getDimensionThemes({}, { dimensions: undefined })).toEqual({})
69
+ })
70
+
71
+ it("processes dimension theme chains", () => {
72
+ const theme = { primaryColor: "blue" }
73
+ const options = {
74
+ dimensions: { states: "state" },
75
+ states: [
76
+ (t: any) => ({
77
+ primary: { color: t.primaryColor },
78
+ secondary: { color: "green" },
79
+ }),
80
+ ],
81
+ }
82
+ const result = getDimensionThemes(theme, options)
83
+ expect(result.state).toEqual({
84
+ primary: { color: "blue" },
85
+ secondary: { color: "green" },
86
+ })
87
+ })
88
+
89
+ it("skips dimensions without callback arrays", () => {
90
+ const options = {
91
+ dimensions: { states: "state", sizes: "size" },
92
+ states: [() => ({ primary: { color: "red" } })],
93
+ }
94
+ const result = getDimensionThemes({}, options)
95
+ expect(result.state).toEqual({ primary: { color: "red" } })
96
+ expect(result.size).toBeUndefined()
97
+ })
98
+
99
+ it("strips nullable values from results", () => {
100
+ const options = {
101
+ dimensions: { states: "state" },
102
+ states: [() => ({ primary: { color: "red" }, secondary: null })],
103
+ }
104
+ const result = getDimensionThemes({}, options)
105
+ expect(result.state.primary).toEqual({ color: "red" })
106
+ expect(result.state.secondary).toBeUndefined()
107
+ })
108
+
109
+ it("handles multi-key dimensions", () => {
110
+ const options = {
111
+ dimensions: { multiple: { propName: "multiple", multi: true } },
112
+ multiple: [() => ({ a: { weight: "bold" } })],
113
+ }
114
+ const result = getDimensionThemes({}, options)
115
+ expect(result.multiple).toEqual({ a: { weight: "bold" } })
116
+ })
117
+
118
+ it("skips empty callback arrays", () => {
119
+ const options = {
120
+ dimensions: { states: "state" },
121
+ states: [],
122
+ }
123
+ const result = getDimensionThemes({}, options)
124
+ expect(result.state).toBeUndefined()
125
+ })
126
+ })
127
+
128
+ describe("calculateChainOptions (theme)", () => {
129
+ it("returns empty for null", () => {
130
+ expect(calculateChainOptions(null, [])).toEqual({})
131
+ })
132
+
133
+ it("returns empty for undefined", () => {
134
+ expect(calculateChainOptions(undefined, [])).toEqual({})
135
+ })
136
+
137
+ it("returns empty for empty array", () => {
138
+ expect(calculateChainOptions([], [])).toEqual({})
139
+ })
140
+
141
+ it("evaluates chain and deep-merges results", () => {
142
+ const fn1 = () => ({ nested: { a: 1 } })
143
+ const fn2 = () => ({ nested: { b: 2 } })
144
+ const result = calculateChainOptions([fn1, fn2], [])
145
+ expect(result).toEqual({ nested: { a: 1, b: 2 } })
146
+ })
147
+
148
+ it("passes args to each function", () => {
149
+ const fn = vi.fn(() => ({}))
150
+ calculateChainOptions([fn], ["arg1", "arg2"])
151
+ expect(fn).toHaveBeenCalledWith("arg1", "arg2")
152
+ })
153
+ })
154
+
155
+ describe("getTheme", () => {
156
+ it("returns baseTheme when rocketstate has no matching dimension themes", () => {
157
+ const result = getTheme({
158
+ rocketstate: { state: "unknown" },
159
+ themes: { state: {} },
160
+ baseTheme: { color: "blue" },
161
+ })
162
+ expect(result.color).toBe("blue")
163
+ })
164
+
165
+ it("merges dimension theme into baseTheme", () => {
166
+ const result = getTheme({
167
+ rocketstate: { state: "primary" },
168
+ themes: { state: { primary: { color: "red" } } },
169
+ baseTheme: { color: "blue", bg: "white" },
170
+ })
171
+ expect(result.color).toBe("red")
172
+ expect(result.bg).toBe("white")
173
+ })
174
+
175
+ it("handles array values for multi-key dimensions", () => {
176
+ const result = getTheme({
177
+ rocketstate: { multiple: ["a", "b"] },
178
+ themes: {
179
+ multiple: { a: { weight: "bold" }, b: { style: "italic" } },
180
+ },
181
+ baseTheme: {},
182
+ })
183
+ expect(result.weight).toBe("bold")
184
+ expect(result.style).toBe("italic")
185
+ })
186
+
187
+ it("later dimensions override earlier ones", () => {
188
+ const result = getTheme({
189
+ rocketstate: { state: "primary", size: "large" },
190
+ themes: {
191
+ state: { primary: { fontSize: 14 } },
192
+ size: { large: { fontSize: 20 } },
193
+ },
194
+ baseTheme: {},
195
+ })
196
+ expect(result.fontSize).toBe(20)
197
+ })
198
+
199
+ it("does not mutate baseTheme", () => {
200
+ const baseTheme = { color: "blue" }
201
+ getTheme({
202
+ rocketstate: { state: "primary" },
203
+ themes: { state: { primary: { color: "red" } } },
204
+ baseTheme,
205
+ })
206
+ expect(baseTheme.color).toBe("blue")
207
+ })
208
+ })
209
+
210
+ describe("getTheme with transform dimensions", () => {
211
+ it("applies transform function values after all static dimensions", () => {
212
+ const result = getTheme({
213
+ rocketstate: { state: "primary", modifier: "outlined" },
214
+ themes: {
215
+ state: { primary: { backgroundColor: "blue", color: "white" } },
216
+ modifier: {
217
+ outlined: (theme: any) => ({
218
+ color: theme.backgroundColor,
219
+ backgroundColor: "transparent",
220
+ }),
221
+ },
222
+ },
223
+ baseTheme: { border: "none" },
224
+ transformKeys: { modifier: true },
225
+ })
226
+ expect(result.color).toBe("blue")
227
+ expect(result.backgroundColor).toBe("transparent")
228
+ expect(result.border).toBe("none")
229
+ })
230
+
231
+ it("transform receives accumulated theme, appTheme, mode, and css", () => {
232
+ const transformFn = vi.fn((theme: any) => ({ derived: theme.color }))
233
+ const appTheme = { colors: { primary: "blue" } }
234
+ getTheme({
235
+ rocketstate: { state: "primary", modifier: "test" },
236
+ themes: {
237
+ state: { primary: { color: "red" } },
238
+ modifier: { test: transformFn },
239
+ },
240
+ baseTheme: { bg: "white" },
241
+ transformKeys: { modifier: true },
242
+ appTheme,
243
+ })
244
+ expect(transformFn).toHaveBeenCalledWith(
245
+ expect.objectContaining({ bg: "white", color: "red" }),
246
+ appTheme,
247
+ expect.any(Function),
248
+ expect.anything(),
249
+ )
250
+ })
251
+
252
+ it("transform can use appTheme values", () => {
253
+ const appTheme = { spacing: { lg: "2rem" } }
254
+ const result = getTheme({
255
+ rocketstate: { modifier: "withSpacing" },
256
+ themes: {
257
+ modifier: {
258
+ withSpacing: (_theme: any, app: any) => ({
259
+ padding: app.spacing.lg,
260
+ }),
261
+ },
262
+ },
263
+ baseTheme: { color: "red" },
264
+ transformKeys: { modifier: true },
265
+ appTheme,
266
+ })
267
+ expect(result.padding).toBe("2rem")
268
+ expect(result.color).toBe("red")
269
+ })
270
+
271
+ it("supports multiple transform values (multi dimension)", () => {
272
+ const result = getTheme({
273
+ rocketstate: { modifier: ["outlined", "rounded"] },
274
+ themes: {
275
+ modifier: {
276
+ outlined: (theme: any) => ({
277
+ color: theme.backgroundColor,
278
+ backgroundColor: "transparent",
279
+ }),
280
+ rounded: () => ({ borderRadius: "999px" }),
281
+ },
282
+ },
283
+ baseTheme: { backgroundColor: "blue", color: "white" },
284
+ transformKeys: { modifier: true },
285
+ })
286
+ expect(result.color).toBe("blue")
287
+ expect(result.backgroundColor).toBe("transparent")
288
+ expect(result.borderRadius).toBe("999px")
289
+ })
290
+
291
+ it("transforms compose — later transform sees earlier transform results", () => {
292
+ const result = getTheme({
293
+ rocketstate: { modifier: ["first", "second"] },
294
+ themes: {
295
+ modifier: {
296
+ first: () => ({ step: "one" }),
297
+ second: (theme: any) => ({ sawStep: theme.step }),
298
+ },
299
+ },
300
+ baseTheme: {},
301
+ transformKeys: { modifier: true },
302
+ })
303
+ expect(result.step).toBe("one")
304
+ expect(result.sawStep).toBe("one")
305
+ })
306
+
307
+ it("works without transformKeys (backward compatible)", () => {
308
+ const result = getTheme({
309
+ rocketstate: { state: "primary" },
310
+ themes: { state: { primary: { color: "red" } } },
311
+ baseTheme: { bg: "white" },
312
+ })
313
+ expect(result.color).toBe("red")
314
+ expect(result.bg).toBe("white")
315
+ })
316
+
317
+ it("non-transform dimension treats function values as regular merge (not called as transform)", () => {
318
+ const fn = vi.fn(() => ({ color: "red" }))
319
+ getTheme({
320
+ rocketstate: { state: "primary" },
321
+ themes: { state: { primary: fn } },
322
+ baseTheme: {},
323
+ transformKeys: {},
324
+ })
325
+ expect(fn).not.toHaveBeenCalled()
326
+ })
327
+
328
+ it("transform dimension not in rocketstate is ignored", () => {
329
+ const transformFn = vi.fn(() => ({ color: "red" }))
330
+ const result = getTheme({
331
+ rocketstate: { state: "primary" },
332
+ themes: {
333
+ state: { primary: { color: "blue" } },
334
+ modifier: { outlined: transformFn },
335
+ },
336
+ baseTheme: {},
337
+ transformKeys: { modifier: true },
338
+ })
339
+ expect(transformFn).not.toHaveBeenCalled()
340
+ expect(result.color).toBe("blue")
341
+ })
342
+
343
+ it("transform does not mutate baseTheme", () => {
344
+ const baseTheme = { color: "blue", bg: "white" }
345
+ getTheme({
346
+ rocketstate: { modifier: "flip" },
347
+ themes: {
348
+ modifier: {
349
+ flip: (theme: any) => ({ color: theme.bg, bg: theme.color }),
350
+ },
351
+ },
352
+ baseTheme,
353
+ transformKeys: { modifier: true },
354
+ })
355
+ expect(baseTheme.color).toBe("blue")
356
+ expect(baseTheme.bg).toBe("white")
357
+ })
358
+
359
+ it("transform with deep nested theme values", () => {
360
+ const result = getTheme({
361
+ rocketstate: { state: "primary", modifier: "outlined" },
362
+ themes: {
363
+ state: {
364
+ primary: {
365
+ backgroundColor: "blue",
366
+ color: "white",
367
+ hover: { backgroundColor: "darkblue" },
368
+ },
369
+ },
370
+ modifier: {
371
+ outlined: (theme: any) => ({
372
+ color: theme.backgroundColor,
373
+ backgroundColor: "transparent",
374
+ hover: {
375
+ backgroundColor: theme.backgroundColor,
376
+ color: theme.color,
377
+ },
378
+ }),
379
+ },
380
+ },
381
+ baseTheme: {},
382
+ transformKeys: { modifier: true },
383
+ })
384
+ expect(result.color).toBe("blue")
385
+ expect(result.backgroundColor).toBe("transparent")
386
+ const hover = result.hover as Record<string, any>
387
+ expect(hover.backgroundColor).toBe("blue")
388
+ expect(hover.color).toBe("white")
389
+ })
390
+
391
+ it("appTheme defaults to empty object when not provided", () => {
392
+ const transformFn = vi.fn(() => ({}))
393
+ getTheme({
394
+ rocketstate: { modifier: "test" },
395
+ themes: { modifier: { test: transformFn } },
396
+ baseTheme: {},
397
+ transformKeys: { modifier: true },
398
+ })
399
+ expect(transformFn).toHaveBeenCalledWith(
400
+ expect.any(Object),
401
+ {},
402
+ expect.any(Function),
403
+ expect.anything(),
404
+ )
405
+ })
406
+
407
+ it("transform with mode callback produces light/dark values", () => {
408
+ const result = getTheme({
409
+ rocketstate: { modifier: "themed" },
410
+ themes: {
411
+ modifier: {
412
+ themed: (_theme: any, _app: any, mode: any) => ({
413
+ shadow: mode("0 2px 4px rgba(0,0,0,0.1)", "none"),
414
+ }),
415
+ },
416
+ },
417
+ baseTheme: {},
418
+ transformKeys: { modifier: true },
419
+ })
420
+ expect(typeof result.shadow).toBe("function")
421
+ })
422
+ })
423
+
424
+ describe("getThemeByMode", () => {
425
+ it("returns scalar values as-is", () => {
426
+ const result = getThemeByMode({ color: "red", size: 16 }, "light")
427
+ expect(result).toEqual({ color: "red", size: 16 })
428
+ })
429
+
430
+ it("recursively processes nested objects", () => {
431
+ const result = getThemeByMode({ nested: { color: "red" } }, "light")
432
+ expect(result).toEqual({ nested: { color: "red" } })
433
+ })
434
+
435
+ it("resolves mode callbacks for light", () => {
436
+ const cb = themeModeCallback("lightColor", "darkColor")
437
+ const result: any = getThemeByMode({ color: cb }, "light")
438
+ expect(result.color).toBe("lightColor")
439
+ })
440
+
441
+ it("resolves mode callbacks for dark", () => {
442
+ const cb = themeModeCallback("lightColor", "darkColor")
443
+ const result: any = getThemeByMode({ color: cb }, "dark")
444
+ expect(result.color).toBe("darkColor")
445
+ })
446
+
447
+ it("resolves nested mode callbacks", () => {
448
+ const cb = themeModeCallback("lightBg", "darkBg")
449
+ const result: any = getThemeByMode({ nested: { bg: cb } }, "light")
450
+ expect(result.nested.bg).toBe("lightBg")
451
+ })
452
+
453
+ it("handles mixed values and mode callbacks", () => {
454
+ const cb = themeModeCallback("#fff", "#000")
455
+ const result: any = getThemeByMode(
456
+ { bg: cb, fontSize: 16, nested: { color: "static" } },
457
+ "dark",
458
+ )
459
+ expect(result.bg).toBe("#000")
460
+ expect(result.fontSize).toBe(16)
461
+ expect(result.nested.color).toBe("static")
462
+ })
463
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * WeakMap-based multi-tier cache for computed theme objects.
3
+ * Maintains separate caches for base themes, dimension themes,
4
+ * and their light/dark mode variants to avoid recalculation on re-renders.
5
+ */
6
+ export default class ThemeManager {
7
+ baseTheme = new WeakMap()
8
+
9
+ dimensionsThemes = new WeakMap()
10
+
11
+ modeBaseTheme = { light: new WeakMap(), dark: new WeakMap() }
12
+
13
+ modeDimensionTheme = { light: new WeakMap(), dark: new WeakMap() }
14
+ }
@@ -0,0 +1,3 @@
1
+ import LocalThemeManager from "./LocalThemeManager"
2
+
3
+ export { LocalThemeManager }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * List of standard HTML boolean attributes. Used to identify props
3
+ * that are valid boolean DOM attributes and should not be filtered
4
+ * out as unknown styling props.
5
+ */
6
+ export default [
7
+ "allowFullScreen",
8
+ "allowPaymentRequest",
9
+ "async",
10
+ "autoFocus",
11
+ "autoPlay",
12
+ "checked",
13
+ "controls",
14
+ "default",
15
+ "defer",
16
+ "disabled",
17
+ "formNoValidate",
18
+ "hidden",
19
+ "isMap",
20
+ "itemScope",
21
+ "loop",
22
+ "multiple",
23
+ "muted",
24
+ "noModule",
25
+ "noValidate",
26
+ "open",
27
+ "readOnly",
28
+ "required",
29
+ "reversed",
30
+ "selected",
31
+ "typeMustMatch",
32
+ ]
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Default dimension configuration for rocketstyle components.
3
+ * Defines four built-in dimensions: `states`, `sizes`, `variants`,
4
+ * and `multiple` (a multi-select dimension).
5
+ */
6
+ const DEFAULT_DIMENSIONS = {
7
+ states: "state",
8
+ sizes: "size",
9
+ variants: "variant",
10
+ multiple: {
11
+ propName: "multiple",
12
+ multi: true,
13
+ },
14
+ modifiers: {
15
+ propName: "modifier",
16
+ multi: true,
17
+ transform: true,
18
+ },
19
+ } as const
20
+
21
+ export type DefaultDimensions = typeof DEFAULT_DIMENSIONS
22
+
23
+ export default DEFAULT_DIMENSIONS
@@ -0,0 +1,44 @@
1
+ /** Default theme mode used when no mode is provided via context. */
2
+ export const MODE_DEFAULT = "light"
3
+
4
+ /** Pseudo-state interaction keys tracked for styling (hover, active, focus, pressed). */
5
+ export const PSEUDO_KEYS = ["hover", "active", "focus", "pressed"] as const
6
+
7
+ /** Meta pseudo-state keys representing non-interactive states (disabled, readOnly). */
8
+ export const PSEUDO_META_KEYS = ["disabled", "readOnly"] as const
9
+
10
+ /** Supported theme mode flags. */
11
+ export const THEME_MODES = {
12
+ light: true,
13
+ dark: true,
14
+ } as const
15
+
16
+ /** Maps each theme mode to its inverse (light -> dark, dark -> light). */
17
+ export const THEME_MODES_INVERSED = {
18
+ dark: "light",
19
+ light: "dark",
20
+ } as const
21
+
22
+ /** Reserved configuration keys accepted by the `.config()` chaining method. */
23
+ export const CONFIG_KEYS = [
24
+ "provider",
25
+ "consumer",
26
+ "DEBUG",
27
+ "name",
28
+ "component",
29
+ "inversed",
30
+ "passProps",
31
+ "styled",
32
+ ] as const
33
+
34
+ /** Keys for theme and styles chaining methods. */
35
+ export const STYLING_KEYS = ["theme", "styles"] as const
36
+ export const STATIC_KEYS = [...STYLING_KEYS, "compose"] as const
37
+
38
+ /** Union of all reserved keys that cannot be used as dimension names. */
39
+ export const ALL_RESERVED_KEYS = [
40
+ ...Object.keys(THEME_MODES),
41
+ ...CONFIG_KEYS,
42
+ ...STATIC_KEYS,
43
+ "attrs",
44
+ ] as const
@@ -0,0 +1,51 @@
1
+ import type { VNodeChild } from "@pyreon/core"
2
+ import { useContext } from "@pyreon/core"
3
+ import { Provider as CoreProvider, context } from "@pyreon/ui-core"
4
+ import { MODE_DEFAULT, THEME_MODES_INVERSED } from "../constants"
5
+
6
+ type Theme = {
7
+ rootSize: number
8
+ breakpoints?: Record<string, number>
9
+ } & Record<string, unknown>
10
+
11
+ export type TProvider = {
12
+ children: VNodeChild
13
+ theme?: Theme | undefined
14
+ mode?: "light" | "dark" | undefined
15
+ inversed?: boolean | undefined
16
+ provider?: ((props: Record<string, unknown>) => VNodeChild) | undefined
17
+ }
18
+
19
+ /**
20
+ * Top-level theme and mode provider for rocketstyle components.
21
+ * Reads the parent context, merges incoming props, and resolves
22
+ * the active mode (with optional inversion for nested dark/light switching).
23
+ *
24
+ * In Pyreon, context is provided via provide() instead of React.Provider.
25
+ */
26
+ const Provider = ({ provider = CoreProvider, inversed, ...props }: TProvider): VNodeChild => {
27
+ const ctx = useContext<TProvider>(context)
28
+
29
+ const { theme, mode, provider: RocketstyleProvider, children } = { ...ctx, ...props, provider }
30
+
31
+ let newMode = MODE_DEFAULT
32
+
33
+ if (mode) {
34
+ newMode = inversed ? THEME_MODES_INVERSED[mode] : mode
35
+ }
36
+
37
+ const result = RocketstyleProvider({
38
+ mode: newMode,
39
+ isDark: newMode === "dark",
40
+ isLight: newMode === "light",
41
+ ...(theme !== undefined ? { theme } : {}),
42
+ provider,
43
+ children,
44
+ })
45
+
46
+ return result ?? null
47
+ }
48
+
49
+ export { context }
50
+
51
+ export default Provider