@pyreon/rocketstyle 0.11.1 → 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 +8 -7
  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,204 @@
1
+ import ThemeManager from "../cache/LocalThemeManager"
2
+ import {
3
+ ALL_RESERVED_KEYS,
4
+ CONFIG_KEYS,
5
+ MODE_DEFAULT,
6
+ PSEUDO_KEYS,
7
+ PSEUDO_META_KEYS,
8
+ STATIC_KEYS,
9
+ STYLING_KEYS,
10
+ THEME_MODES,
11
+ THEME_MODES_INVERSED,
12
+ } from "../constants"
13
+ import BOOLEAN_TAGS from "../constants/booleanTags"
14
+ import DEFAULT_DIMENSIONS from "../constants/defaultDimensions"
15
+ import { createStaticsChainingEnhancers, createStaticsEnhancers } from "../utils/statics"
16
+ import { calculateStyles } from "../utils/styles"
17
+
18
+ describe("createStaticsChainingEnhancers", () => {
19
+ it("attaches chaining methods for dimension keys + static keys", () => {
20
+ const context: Record<string, any> = {}
21
+ const func = vi.fn()
22
+
23
+ createStaticsChainingEnhancers({
24
+ context,
25
+ dimensionKeys: ["states", "sizes"],
26
+ func,
27
+ options: {} as any,
28
+ })
29
+
30
+ expect(typeof context.states).toBe("function")
31
+ expect(typeof context.sizes).toBe("function")
32
+ expect(typeof context.theme).toBe("function")
33
+ expect(typeof context.styles).toBe("function")
34
+ expect(typeof context.compose).toBe("function")
35
+ })
36
+
37
+ it("calls func with options and key-value pair", () => {
38
+ const context: Record<string, any> = {}
39
+ const func = vi.fn()
40
+ const options = { some: "option" } as any
41
+
42
+ createStaticsChainingEnhancers({
43
+ context,
44
+ dimensionKeys: ["states"],
45
+ func,
46
+ options,
47
+ })
48
+
49
+ context.states({ primary: { color: "red" } })
50
+ expect(func).toHaveBeenCalledWith(options, {
51
+ states: { primary: { color: "red" } },
52
+ })
53
+ })
54
+ })
55
+
56
+ describe("createStaticsEnhancers", () => {
57
+ it("copies options to context", () => {
58
+ const context: Record<string, any> = {}
59
+ createStaticsEnhancers({ context, options: { foo: "bar", baz: 42 } })
60
+ expect(context.foo).toBe("bar")
61
+ expect(context.baz).toBe(42)
62
+ })
63
+
64
+ it("does nothing when options is empty", () => {
65
+ const context: Record<string, any> = { existing: true }
66
+ createStaticsEnhancers({ context, options: {} })
67
+ expect(context).toEqual({ existing: true })
68
+ })
69
+
70
+ it("does nothing when options is undefined", () => {
71
+ const context: Record<string, any> = { existing: true }
72
+ createStaticsEnhancers({ context, options: undefined as any })
73
+ expect(context).toEqual({ existing: true })
74
+ })
75
+ })
76
+
77
+ describe("calculateStyles", () => {
78
+ it("returns empty array when styles is undefined", () => {
79
+ expect(calculateStyles(undefined)).toEqual([])
80
+ })
81
+
82
+ it("evaluates each style callback", () => {
83
+ const styleFn1 = () => "style1"
84
+ const styleFn2 = () => "style2"
85
+ const result = calculateStyles([styleFn1, styleFn2] as any)
86
+ expect(result).toHaveLength(2)
87
+ expect(result[0]).toBe("style1")
88
+ expect(result[1]).toBe("style2")
89
+ })
90
+
91
+ it("returns empty array for empty styles array", () => {
92
+ const result = calculateStyles([])
93
+ expect(result).toEqual([])
94
+ })
95
+ })
96
+
97
+ describe("ThemeManager", () => {
98
+ it("creates instance with WeakMap caches", () => {
99
+ const tm = new ThemeManager()
100
+ expect(tm.baseTheme).toBeInstanceOf(WeakMap)
101
+ expect(tm.dimensionsThemes).toBeInstanceOf(WeakMap)
102
+ expect(tm.modeBaseTheme.light).toBeInstanceOf(WeakMap)
103
+ expect(tm.modeBaseTheme.dark).toBeInstanceOf(WeakMap)
104
+ expect(tm.modeDimensionTheme.light).toBeInstanceOf(WeakMap)
105
+ expect(tm.modeDimensionTheme.dark).toBeInstanceOf(WeakMap)
106
+ })
107
+
108
+ it("caches and retrieves values", () => {
109
+ const tm = new ThemeManager()
110
+ const key = {}
111
+ tm.baseTheme.set(key, { color: "red" })
112
+ expect(tm.baseTheme.get(key)).toEqual({ color: "red" })
113
+ })
114
+
115
+ it("mode caches are independent", () => {
116
+ const tm = new ThemeManager()
117
+ const key = {}
118
+ tm.modeBaseTheme.light.set(key, "light-theme")
119
+ tm.modeBaseTheme.dark.set(key, "dark-theme")
120
+ expect(tm.modeBaseTheme.light.get(key)).toBe("light-theme")
121
+ expect(tm.modeBaseTheme.dark.get(key)).toBe("dark-theme")
122
+ })
123
+ })
124
+
125
+ describe("constants", () => {
126
+ it("MODE_DEFAULT is light", () => {
127
+ expect(MODE_DEFAULT).toBe("light")
128
+ })
129
+
130
+ it("PSEUDO_KEYS", () => {
131
+ expect(PSEUDO_KEYS).toEqual(["hover", "active", "focus", "pressed"])
132
+ })
133
+
134
+ it("PSEUDO_META_KEYS", () => {
135
+ expect(PSEUDO_META_KEYS).toEqual(["disabled", "readOnly"])
136
+ })
137
+
138
+ it("THEME_MODES", () => {
139
+ expect(THEME_MODES.light).toBe(true)
140
+ expect(THEME_MODES.dark).toBe(true)
141
+ })
142
+
143
+ it("THEME_MODES_INVERSED", () => {
144
+ expect(THEME_MODES_INVERSED.light).toBe("dark")
145
+ expect(THEME_MODES_INVERSED.dark).toBe("light")
146
+ })
147
+
148
+ it("CONFIG_KEYS includes expected keys", () => {
149
+ expect(CONFIG_KEYS).toContain("provider")
150
+ expect(CONFIG_KEYS).toContain("consumer")
151
+ expect(CONFIG_KEYS).toContain("name")
152
+ expect(CONFIG_KEYS).toContain("component")
153
+ expect(CONFIG_KEYS).toContain("inversed")
154
+ expect(CONFIG_KEYS).toContain("passProps")
155
+ expect(CONFIG_KEYS).toContain("styled")
156
+ expect(CONFIG_KEYS).toContain("DEBUG")
157
+ })
158
+
159
+ it("STYLING_KEYS", () => {
160
+ expect(STYLING_KEYS).toEqual(["theme", "styles"])
161
+ })
162
+
163
+ it("STATIC_KEYS includes styling keys and compose", () => {
164
+ expect(STATIC_KEYS).toContain("theme")
165
+ expect(STATIC_KEYS).toContain("styles")
166
+ expect(STATIC_KEYS).toContain("compose")
167
+ })
168
+
169
+ it("ALL_RESERVED_KEYS includes mode keys and others", () => {
170
+ expect(ALL_RESERVED_KEYS).toContain("light")
171
+ expect(ALL_RESERVED_KEYS).toContain("dark")
172
+ expect(ALL_RESERVED_KEYS).toContain("attrs")
173
+ expect(ALL_RESERVED_KEYS).toContain("theme")
174
+ expect(ALL_RESERVED_KEYS).toContain("compose")
175
+ })
176
+ })
177
+
178
+ describe("DEFAULT_DIMENSIONS", () => {
179
+ it("has states, sizes, variants, multiple", () => {
180
+ expect(DEFAULT_DIMENSIONS.states).toBe("state")
181
+ expect(DEFAULT_DIMENSIONS.sizes).toBe("size")
182
+ expect(DEFAULT_DIMENSIONS.variants).toBe("variant")
183
+ expect(DEFAULT_DIMENSIONS.multiple).toEqual({
184
+ propName: "multiple",
185
+ multi: true,
186
+ })
187
+ })
188
+ })
189
+
190
+ describe("BOOLEAN_TAGS", () => {
191
+ it("is an array of HTML boolean attributes", () => {
192
+ expect(Array.isArray(BOOLEAN_TAGS)).toBe(true)
193
+ expect(BOOLEAN_TAGS).toContain("disabled")
194
+ expect(BOOLEAN_TAGS).toContain("checked")
195
+ expect(BOOLEAN_TAGS).toContain("readOnly")
196
+ expect(BOOLEAN_TAGS).toContain("required")
197
+ expect(BOOLEAN_TAGS).toContain("hidden")
198
+ expect(BOOLEAN_TAGS).toContain("autoFocus")
199
+ })
200
+
201
+ it("has more than 20 entries", () => {
202
+ expect(BOOLEAN_TAGS.length).toBeGreaterThan(20)
203
+ })
204
+ })
@@ -0,0 +1,248 @@
1
+ import { popContext, pushContext } from "@pyreon/core"
2
+ import { config } from "@pyreon/ui-core"
3
+ import { context } from "../context/context"
4
+ import rocketstyle from "../init"
5
+
6
+ // Mock styled function that returns the component unchanged
7
+ const mockStyled = (component: any) => {
8
+ const taggedTemplate = (_strings: any, ..._args: any[]) => component
9
+ return taggedTemplate
10
+ }
11
+
12
+ const mockCss = (_strings: any, ..._args: any[]) => ""
13
+
14
+ const originalStyled = config.styled
15
+ const originalCss = config.css
16
+
17
+ beforeAll(() => {
18
+ config.init({
19
+ css: mockCss as any,
20
+ styled: mockStyled as any,
21
+ component: "div",
22
+ textComponent: "span",
23
+ })
24
+ })
25
+
26
+ afterAll(() => {
27
+ config.styled = originalStyled
28
+ config.css = originalCss
29
+ })
30
+
31
+ /**
32
+ * Base component that exposes internal props for testing.
33
+ * In Pyreon, components are plain functions — no forwardRef needed.
34
+ */
35
+ const BaseComponent: any = ({ children, $rocketstyle, $rocketstate, ...rest }: any) => ({
36
+ type: "div",
37
+ props: {
38
+ ...rest,
39
+ "data-hover": String($rocketstate?.pseudo?.hover ?? "none"),
40
+ "data-focus": String($rocketstate?.pseudo?.focus ?? "none"),
41
+ "data-pressed": String($rocketstate?.pseudo?.pressed ?? "none"),
42
+ },
43
+ children,
44
+ $rocketstyle,
45
+ $rocketstate,
46
+ })
47
+ BaseComponent.displayName = "BaseComponent"
48
+
49
+ /** Child component that reads consumer context */
50
+ const ChildComponent: any = ({
51
+ children,
52
+ $rocketstyle,
53
+ $rocketstate,
54
+ parentHover,
55
+ ...rest
56
+ }: any) => ({
57
+ type: "div",
58
+ props: { ...rest, "data-parent-hover": parentHover ?? "none" },
59
+ children,
60
+ })
61
+ ChildComponent.displayName = "ChildComponent"
62
+
63
+ /** Push a theme context and run fn, then pop */
64
+ const withThemeContext = (fn: () => any) => {
65
+ pushContext(
66
+ new Map([
67
+ [
68
+ context.id,
69
+ {
70
+ theme: { rootSize: 16 },
71
+ mode: "light",
72
+ isDark: false,
73
+ isLight: true,
74
+ },
75
+ ],
76
+ ]),
77
+ )
78
+ try {
79
+ return fn()
80
+ } finally {
81
+ popContext()
82
+ }
83
+ }
84
+
85
+ // --------------------------------------------------------
86
+ // Provider/Consumer integration
87
+ // --------------------------------------------------------
88
+ describe("Provider/Consumer integration", () => {
89
+ describe("provider component", () => {
90
+ it("renders with provider: true", () => {
91
+ const ParentButton: any = rocketstyle()({
92
+ name: "ProviderButton",
93
+ component: BaseComponent,
94
+ }).config({ provider: true })
95
+
96
+ const result = withThemeContext(() => ParentButton({ children: "Child" }))
97
+ expect(result).toBeDefined()
98
+ })
99
+
100
+ it("detects pseudo-state via $rocketstate on provider", () => {
101
+ const ParentButton: any = rocketstyle()({
102
+ name: "HoverProvider",
103
+ component: BaseComponent,
104
+ }).config({ provider: true })
105
+
106
+ const result = withThemeContext(() => ParentButton({ children: "Child" }))
107
+ // Provider wraps with createLocalProvider which injects pseudo state
108
+ // Initial state should be false
109
+ expect(result.props["data-hover"]).toBe("false")
110
+ expect(result.props["data-focus"]).toBe("false")
111
+ expect(result.props["data-pressed"]).toBe("false")
112
+ })
113
+ })
114
+
115
+ describe("consumer component", () => {
116
+ it("consumer receives pseudo-state from provider context", () => {
117
+ const Parent: any = rocketstyle()({
118
+ name: "ParentProvider",
119
+ component: BaseComponent,
120
+ }).config({ provider: true })
121
+
122
+ const Child: any = rocketstyle()({
123
+ name: "ChildConsumer",
124
+ component: ChildComponent,
125
+ }).config({
126
+ consumer: (ctx: any) =>
127
+ ctx((rawCtx: any) => ({
128
+ parentHover: rawCtx?.pseudo?.hover ? "yes" : "no",
129
+ })),
130
+ })
131
+
132
+ // Render parent, then render child within the same context
133
+ withThemeContext(() => {
134
+ const _parentResult = Parent({ children: null })
135
+ // The parent pushes local context — child should see it
136
+ const childResult = Child({})
137
+ expect(childResult).toBeDefined()
138
+ const childProps = childResult?.props ?? childResult
139
+ expect(childProps["data-parent-hover"]).toBe("no")
140
+ })
141
+ })
142
+
143
+ it("consumer without provider returns default pseudo", () => {
144
+ const Child: any = rocketstyle()({
145
+ name: "OrphanConsumer",
146
+ component: ChildComponent,
147
+ }).config({
148
+ consumer: (ctx: any) =>
149
+ ctx((rawCtx: any) => ({
150
+ parentHover: rawCtx?.pseudo?.hover ? "yes" : "no",
151
+ })),
152
+ })
153
+
154
+ const result = withThemeContext(() => Child({}))
155
+ const props = result?.props ?? result
156
+ expect(props["data-parent-hover"]).toBe("no")
157
+ })
158
+
159
+ it("component without consumer ignores provider context", () => {
160
+ const Parent: any = rocketstyle()({
161
+ name: "IgnoredProvider",
162
+ component: BaseComponent,
163
+ }).config({ provider: true })
164
+
165
+ const Child: any = rocketstyle()({
166
+ name: "NoConsumer",
167
+ component: BaseComponent,
168
+ }).config({})
169
+
170
+ withThemeContext(() => {
171
+ Parent({ children: null })
172
+ const childResult = Child({})
173
+ expect(childResult).toBeDefined()
174
+ })
175
+ })
176
+ })
177
+
178
+ describe("theme mode", () => {
179
+ it("light mode is default", () => {
180
+ const Button: any = rocketstyle()({
181
+ name: "LightButton",
182
+ component: BaseComponent,
183
+ }).config({})
184
+
185
+ const result = withThemeContext(() => Button({}))
186
+ expect(result).toBeDefined()
187
+ })
188
+
189
+ it("dark mode is passed through Provider", () => {
190
+ const Button: any = rocketstyle()({
191
+ name: "DarkButton",
192
+ component: BaseComponent,
193
+ }).config({})
194
+
195
+ pushContext(
196
+ new Map([
197
+ [
198
+ context.id,
199
+ {
200
+ theme: { rootSize: 16 },
201
+ mode: "dark",
202
+ isDark: true,
203
+ isLight: false,
204
+ },
205
+ ],
206
+ ]),
207
+ )
208
+ try {
209
+ const result = Button({})
210
+ expect(result).toBeDefined()
211
+ } finally {
212
+ popContext()
213
+ }
214
+ })
215
+
216
+ it("inversed config flips the mode", () => {
217
+ const Button: any = rocketstyle()({
218
+ name: "InversedButton",
219
+ component: BaseComponent,
220
+ }).config({ inversed: true })
221
+
222
+ const result = withThemeContext(() => Button({}))
223
+ expect(result).toBeDefined()
224
+ })
225
+ })
226
+
227
+ describe("nested providers", () => {
228
+ it("supports nested provider components", () => {
229
+ const Outer: any = rocketstyle()({
230
+ name: "OuterProvider",
231
+ component: BaseComponent,
232
+ }).config({ provider: true })
233
+
234
+ const Inner: any = rocketstyle()({
235
+ name: "InnerProvider",
236
+ component: BaseComponent,
237
+ }).config({ provider: true })
238
+
239
+ withThemeContext(() => {
240
+ const outerResult = Outer({ children: null })
241
+ expect(outerResult).toBeDefined()
242
+
243
+ const innerResult = Inner({ children: null })
244
+ expect(innerResult).toBeDefined()
245
+ })
246
+ })
247
+ })
248
+ })