@pyreon/rocketstyle 0.12.12 → 0.12.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/rocketstyle",
3
- "version": "0.12.12",
3
+ "version": "0.12.14",
4
4
  "description": "Multi-dimensional style composition for Pyreon components",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,20 +35,23 @@
35
35
  "build:watch": "bun run vl_rolldown_build-watch",
36
36
  "lint": "oxlint .",
37
37
  "test": "vitest run",
38
+ "test:browser": "vitest run --config ./vitest.browser.config.ts",
38
39
  "test:coverage": "vitest run --coverage",
39
40
  "test:watch": "vitest",
40
41
  "typecheck": "tsc --noEmit"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@pyreon/test-utils": "^0.12.10",
44
- "@pyreon/typescript": "^0.12.12",
45
+ "@pyreon/typescript": "^0.12.14",
46
+ "@pyreon/ui-core": "^0.12.14",
47
+ "@vitest/browser-playwright": "^4.1.4",
45
48
  "@vitus-labs/tools-rolldown": "^1.15.3"
46
49
  },
47
50
  "peerDependencies": {
48
- "@pyreon/core": "^0.12.12",
49
- "@pyreon/reactivity": "^0.12.12",
50
- "@pyreon/styler": "^0.12.12",
51
- "@pyreon/ui-core": "^0.12.12"
51
+ "@pyreon/core": "^0.12.14",
52
+ "@pyreon/reactivity": "^0.12.14",
53
+ "@pyreon/styler": "^0.12.14",
54
+ "@pyreon/ui-core": "^0.12.14"
52
55
  },
53
56
  "engines": {
54
57
  "node": ">= 22"
@@ -0,0 +1,185 @@
1
+ /** @jsxImportSource @pyreon/core */
2
+ import type { ComponentFn, VNodeChild } from '@pyreon/core'
3
+ import { h } from '@pyreon/core'
4
+ import { signal } from '@pyreon/reactivity'
5
+ import { sheet } from '@pyreon/styler'
6
+ import { mountInBrowser } from '@pyreon/test-utils/browser'
7
+ import { PyreonUI } from '@pyreon/ui-core'
8
+ import { afterEach, describe, expect, it } from 'vitest'
9
+ import rocketstyle from '../init'
10
+
11
+ // Real-Chromium smoke for @pyreon/rocketstyle.
12
+ //
13
+ // Production usage wraps component functions (Element/Text/etc.), not
14
+ // string tags — so the base is a real ComponentFn here. This also
15
+ // satisfies the rocketstyle `ElementType` generic without `as any`.
16
+
17
+ const Base: ComponentFn<{ id?: string; children?: VNodeChild; class?: string }> = (
18
+ props,
19
+ ) => h('div', props, props.children)
20
+ ;(Base as ComponentFn & { displayName?: string }).displayName = 'Base'
21
+
22
+ describe('@pyreon/rocketstyle in real browser', () => {
23
+ afterEach(() => {
24
+ sheet.clearCache()
25
+ })
26
+
27
+ it('rocketstyle(Base) with .theme() applies the authored color in Chromium', () => {
28
+ const Box: any = rocketstyle()({ name: 'Box', component: Base })
29
+ .styles(
30
+ (css: any) => css`
31
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
32
+ padding: 8px;
33
+ `,
34
+ )
35
+ .theme({ color: 'rgb(255, 0, 0)' })
36
+
37
+ const { container, unmount } = mountInBrowser(h(Box, { id: 'rs' }))
38
+ const el = container.querySelector<HTMLElement>('#rs')!
39
+ expect(el.className).toMatch(/pyr-/)
40
+ expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')
41
+ expect(getComputedStyle(el).padding).toBe('8px')
42
+ unmount()
43
+ })
44
+
45
+ it('the `state` prop swaps the resolved $rocketstyle theme', () => {
46
+ const Box: any = rocketstyle()({ name: 'StateBox', component: Base })
47
+ .styles(
48
+ (css: any) => css`
49
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
50
+ `,
51
+ )
52
+ .theme({ color: 'rgb(255, 0, 0)' })
53
+ .states({ danger: { color: 'rgb(0, 0, 255)' } })
54
+
55
+ const base = mountInBrowser(h(Box, { id: 'b' }))
56
+ const danger = mountInBrowser(h(Box, { id: 'd', state: 'danger' }))
57
+ expect(getComputedStyle(base.container.querySelector<HTMLElement>('#b')!).color).toBe(
58
+ 'rgb(255, 0, 0)',
59
+ )
60
+ expect(getComputedStyle(danger.container.querySelector<HTMLElement>('#d')!).color).toBe(
61
+ 'rgb(0, 0, 255)',
62
+ )
63
+ base.unmount()
64
+ danger.unmount()
65
+ })
66
+
67
+ it('reactive mode swap: classList changes in place via styler `isReactiveRS` effect (no remount)', async () => {
68
+ // Exercises the load-bearing `isReactiveRS` effect in
69
+ // styler/src/styled.tsx — when `$rocketstyle` is a function
70
+ // accessor, an effect tracks it and swaps classList in place.
71
+ // Mode switching is the canonical reactive path: PyreonUI
72
+ // provides a signal-backed mode, rocketstyle's
73
+ // `$rocketstyleAccessor` reads `themeAttrs.mode` (a getter on a
74
+ // ReactiveContext), and the styler effect observes the change.
75
+ //
76
+ // (Reactive *dimension props* like `state={stateSig()}` are NOT
77
+ // yet end-to-end reactive through rocketstyle's HOC chain — the
78
+ // inner spread in `rocketstyleAttrsHoc` collapses getter props
79
+ // to values. Mode is the only reactive axis that survives the
80
+ // spread because it flows via ReactiveContext, not via props.)
81
+ const modeSig = signal<'light' | 'dark'>('light')
82
+
83
+ const Box: any = rocketstyle()({ name: 'ModeSwapBox', component: Base })
84
+ .styles(
85
+ (css: any) => css`
86
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
87
+ `,
88
+ )
89
+ .theme((_t: any, m: any) => ({
90
+ color: m('rgb(255, 0, 0)', 'rgb(0, 0, 255)'),
91
+ }))
92
+
93
+ const { container, unmount } = mountInBrowser(
94
+ h(PyreonUI, { theme: {}, mode: modeSig }, h(Box, { id: 'rx' })),
95
+ )
96
+ const el = container.querySelector<HTMLElement>('#rx')!
97
+ const classBefore = el.className
98
+ expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')
99
+
100
+ modeSig.set('dark')
101
+ await new Promise((r) => setTimeout(r, 0))
102
+ await new Promise((r) => requestAnimationFrame(() => r(undefined)))
103
+
104
+ const classAfter = el.className
105
+ expect(getComputedStyle(el).color).toBe('rgb(0, 0, 255)')
106
+ // Class swapped in place — not a remount (same element reference,
107
+ // different class). This is the styler `isReactiveRS` effect
108
+ // doing `el.classList.remove(old); el.classList.add(new)`.
109
+ expect(classAfter).not.toBe(classBefore)
110
+ unmount()
111
+ })
112
+
113
+ it('the `variant` prop layers on top of state', () => {
114
+ const Box: any = rocketstyle()({ name: 'VariantBox', component: Base })
115
+ .styles(
116
+ (css: any) => css`
117
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
118
+ background-color: ${({ $rocketstyle }: any) => $rocketstyle.bg};
119
+ `,
120
+ )
121
+ .theme({ color: 'rgb(0, 0, 0)', bg: 'rgb(240, 240, 240)' })
122
+ .variants({ box: { bg: 'rgb(20, 30, 40)' } })
123
+
124
+ const { container, unmount } = mountInBrowser(
125
+ h(Box, { id: 'v', variant: 'box' }),
126
+ )
127
+ const el = container.querySelector<HTMLElement>('#v')!
128
+ expect(getComputedStyle(el).color).toBe('rgb(0, 0, 0)')
129
+ expect(getComputedStyle(el).backgroundColor).toBe('rgb(20, 30, 40)')
130
+ unmount()
131
+ })
132
+
133
+ it('the `modifier` transform derives styles from the accumulated state theme', () => {
134
+ const Box: any = rocketstyle()({ name: 'ModBox', component: Base })
135
+ .styles(
136
+ (css: any) => css`
137
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
138
+ background-color: ${({ $rocketstyle }: any) => $rocketstyle.bg};
139
+ `,
140
+ )
141
+ .theme({ color: 'rgb(255, 255, 255)', bg: 'rgb(0, 112, 243)' })
142
+ .states({ danger: { color: 'rgb(255, 255, 255)', bg: 'rgb(220, 53, 69)' } })
143
+ .modifiers({
144
+ outlined: (acc: any) => ({
145
+ color: acc.bg,
146
+ bg: 'rgb(255, 255, 255)',
147
+ }),
148
+ })
149
+
150
+ const { container, unmount } = mountInBrowser(
151
+ h(Box, { id: 'm', state: 'danger', modifier: 'outlined' }),
152
+ )
153
+ const el = container.querySelector<HTMLElement>('#m')!
154
+ expect(getComputedStyle(el).color).toBe('rgb(220, 53, 69)')
155
+ expect(getComputedStyle(el).backgroundColor).toBe('rgb(255, 255, 255)')
156
+ unmount()
157
+ })
158
+
159
+ it('m(light, dark) theme callback resolves per PyreonUI mode', () => {
160
+ const Box: any = rocketstyle()({ name: 'ModeBox', component: Base })
161
+ .styles(
162
+ (css: any) => css`
163
+ color: ${({ $rocketstyle }: any) => $rocketstyle.color};
164
+ `,
165
+ )
166
+ .theme((_t: any, m: any) => ({
167
+ color: m('rgb(12, 34, 56)', 'rgb(210, 220, 230)'),
168
+ }))
169
+
170
+ const light = mountInBrowser(
171
+ h(PyreonUI, { theme: {}, mode: 'light' }, h(Box, { id: 'lt' })),
172
+ )
173
+ const dark = mountInBrowser(
174
+ h(PyreonUI, { theme: {}, mode: 'dark' }, h(Box, { id: 'dk' })),
175
+ )
176
+ expect(getComputedStyle(light.container.querySelector<HTMLElement>('#lt')!).color).toBe(
177
+ 'rgb(12, 34, 56)',
178
+ )
179
+ expect(getComputedStyle(dark.container.querySelector<HTMLElement>('#dk')!).color).toBe(
180
+ 'rgb(210, 220, 230)',
181
+ )
182
+ light.unmount()
183
+ dark.unmount()
184
+ })
185
+ })