@pyreon/unistyle 0.24.5 → 0.24.6

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 (50) hide show
  1. package/package.json +7 -9
  2. package/src/__tests__/alignContent.test.ts +0 -121
  3. package/src/__tests__/borderRadius.test.ts +0 -125
  4. package/src/__tests__/camelToKebab.test.ts +0 -44
  5. package/src/__tests__/context.test.ts +0 -147
  6. package/src/__tests__/createMediaQueries.test.ts +0 -98
  7. package/src/__tests__/edge.test.ts +0 -164
  8. package/src/__tests__/enrichTheme.test.ts +0 -56
  9. package/src/__tests__/extendCss.test.ts +0 -45
  10. package/src/__tests__/index.test.ts +0 -79
  11. package/src/__tests__/makeItResponsive.test.ts +0 -431
  12. package/src/__tests__/manifest-snapshot.test.ts +0 -34
  13. package/src/__tests__/native-marker.test.ts +0 -9
  14. package/src/__tests__/optimizeBreakpointDeltas.test.ts +0 -124
  15. package/src/__tests__/processDescriptor.test.ts +0 -322
  16. package/src/__tests__/responsive.test.ts +0 -221
  17. package/src/__tests__/special-keys.test.ts +0 -120
  18. package/src/__tests__/styles.test.ts +0 -273
  19. package/src/__tests__/unistyle.browser.test.tsx +0 -169
  20. package/src/__tests__/units.test.ts +0 -134
  21. package/src/context.tsx +0 -44
  22. package/src/enrichTheme.ts +0 -42
  23. package/src/env.d.ts +0 -6
  24. package/src/index.ts +0 -91
  25. package/src/manifest.ts +0 -197
  26. package/src/responsive/breakpoints.ts +0 -15
  27. package/src/responsive/createMediaQueries.ts +0 -43
  28. package/src/responsive/index.ts +0 -15
  29. package/src/responsive/makeItResponsive.ts +0 -223
  30. package/src/responsive/normalizeTheme.ts +0 -79
  31. package/src/responsive/optimizeBreakpointDeltas.ts +0 -190
  32. package/src/responsive/optimizeTheme.ts +0 -60
  33. package/src/responsive/sortBreakpoints.ts +0 -10
  34. package/src/responsive/transformTheme.ts +0 -54
  35. package/src/styles/alignContent.ts +0 -62
  36. package/src/styles/extendCss.ts +0 -26
  37. package/src/styles/index.ts +0 -16
  38. package/src/styles/shorthands/borderRadius.ts +0 -89
  39. package/src/styles/shorthands/edge.ts +0 -108
  40. package/src/styles/shorthands/index.ts +0 -4
  41. package/src/styles/styles/camelToKebab.ts +0 -3
  42. package/src/styles/styles/index.ts +0 -132
  43. package/src/styles/styles/processDescriptor.ts +0 -136
  44. package/src/styles/styles/propertyMap.ts +0 -438
  45. package/src/styles/styles/types.ts +0 -368
  46. package/src/types.ts +0 -175
  47. package/src/units/index.ts +0 -6
  48. package/src/units/stripUnit.ts +0 -25
  49. package/src/units/value.ts +0 -47
  50. package/src/units/values.ts +0 -40
@@ -1,273 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import styles from '../styles/styles/index'
3
-
4
- const mockCss = (strings: TemplateStringsArray, ...vals: any[]) => {
5
- let r = ''
6
- for (let i = 0; i < strings.length; i++) {
7
- r += strings[i]
8
- if (i < vals.length) r += String(vals[i])
9
- }
10
- return r
11
- }
12
-
13
- describe('styles', () => {
14
- it('empty theme produces no CSS properties (all fragments are empty)', () => {
15
- const result = styles({ theme: {}, css: mockCss, rootSize: 16 })
16
- // The result is a css`` template result with empty fragments — it
17
- // contains template whitespace but no actual CSS property declarations.
18
- // Trim and strip commas/whitespace to verify no real CSS is produced.
19
- const cleaned = String(result).replace(/[,\s]/g, '')
20
- expect(cleaned).toBe('')
21
- })
22
-
23
- it('single simple property: color', () => {
24
- const result = styles({ theme: { color: 'red' }, css: mockCss, rootSize: 16 })
25
- expect(result).toContain('color: red;')
26
- })
27
-
28
- it('simple property: display', () => {
29
- const result = styles({ theme: { display: 'flex' }, css: mockCss, rootSize: 16 })
30
- expect(result).toContain('display: flex;')
31
- })
32
-
33
- it('convert property: width converts via value() with rootSize', () => {
34
- // width is a convert_fallback with keys ["width", "size"]
35
- // 160 / 16 = 10rem
36
- const result = styles({ theme: { width: 160 }, css: mockCss, rootSize: 16 })
37
- expect(result).toContain('width:')
38
- expect(result).toContain('10rem')
39
- })
40
-
41
- it('convert property: fontSize', () => {
42
- // 32 / 16 = 2rem
43
- const result = styles({ theme: { fontSize: 32 }, css: mockCss, rootSize: 16 })
44
- expect(result).toContain('font-size:')
45
- expect(result).toContain('2rem')
46
- })
47
-
48
- it('edge property: margin generates margin shorthand', () => {
49
- // margin 16 / 16 = 1rem
50
- const result = styles({ theme: { margin: 16 }, css: mockCss, rootSize: 16 })
51
- expect(result).toContain('margin:')
52
- expect(result).toContain('1rem')
53
- })
54
-
55
- it('edge property: padding', () => {
56
- const result = styles({ theme: { padding: 8 }, css: mockCss, rootSize: 16 })
57
- expect(result).toContain('padding:')
58
- expect(result).toContain('0.5rem')
59
- })
60
-
61
- it('border radius: borderRadius generates border-radius', () => {
62
- // 8 / 16 = 0.5rem
63
- const result = styles({ theme: { borderRadius: 8 }, css: mockCss, rootSize: 16 })
64
- expect(result).toContain('border-radius:')
65
- expect(result).toContain('0.5rem')
66
- })
67
-
68
- it('multiple properties combined', () => {
69
- const result = styles({
70
- theme: { color: 'blue', display: 'flex', fontSize: 16 },
71
- css: mockCss,
72
- rootSize: 16,
73
- })
74
- expect(result).toContain('color: blue;')
75
- expect(result).toContain('display: flex;')
76
- expect(result).toContain('font-size: 1rem;')
77
- })
78
-
79
- it('special property: fullScreen', () => {
80
- const result = styles({ theme: { fullScreen: true }, css: mockCss, rootSize: 16 })
81
- expect(result).toContain('position: fixed;')
82
- expect(result).toContain('top: 0;')
83
- expect(result).toContain('left: 0;')
84
- expect(result).toContain('right: 0;')
85
- expect(result).toContain('bottom: 0;')
86
- })
87
-
88
- it('special property: fullScreen false produces no output', () => {
89
- const result = styles({ theme: { fullScreen: false }, css: mockCss, rootSize: 16 })
90
- expect(result).not.toContain('position: fixed;')
91
- })
92
-
93
- it('special property: backgroundImage', () => {
94
- const result = styles({
95
- theme: { backgroundImage: 'https://example.com/img.png' },
96
- css: mockCss,
97
- rootSize: 16,
98
- })
99
- expect(result).toContain('background-image: url(https://example.com/img.png);')
100
- })
101
-
102
- it('special property: hideEmpty', () => {
103
- const result = styles({ theme: { hideEmpty: true }, css: mockCss, rootSize: 16 })
104
- // CSS template output — normalize whitespace for comparison
105
- const normalized = String(result).replace(/\s+/g, ' ')
106
- expect(normalized).toContain('&:empty { display: none; }')
107
- })
108
-
109
- it('special property: clearFix', () => {
110
- const result = styles({ theme: { clearFix: true }, css: mockCss, rootSize: 16 })
111
- const normalized = String(result).replace(/\s+/g, ' ')
112
- expect(normalized).toContain("&::after { clear: both; content: ''; display: table; }")
113
- })
114
-
115
- it('string values for convert properties pass through', () => {
116
- const result = styles({ theme: { width: '50%' }, css: mockCss, rootSize: 16 })
117
- expect(result).toContain('width: 50%;')
118
- })
119
-
120
- it('uses default rootSize when not provided', () => {
121
- // default rootSize is undefined, value() defaults to 16
122
- const result = styles({ theme: { fontSize: 32 }, css: mockCss })
123
- expect(result).toContain('font-size:')
124
- expect(result).toContain('2rem')
125
- })
126
- })
127
-
128
- describe('Tier 1: key→index lookup correctness', () => {
129
- it('produces identical output for typical rocketstyle theme object', () => {
130
- // A realistic theme object from a rocketstyle component — has ~10 keys
131
- const theme = {
132
- color: '#333',
133
- backgroundColor: '#fff',
134
- fontSize: 14,
135
- fontWeight: 600,
136
- paddingTop: 8,
137
- paddingBottom: 8,
138
- paddingLeft: 12,
139
- paddingRight: 12,
140
- borderRadius: 4,
141
- borderColor: '#ddd',
142
- borderWidthTop: 1,
143
- lineHeight: 1.5,
144
- cursor: 'pointer',
145
- }
146
-
147
- const result = styles({ theme, css: mockCss, rootSize: 16 })
148
- const output = String(result)
149
-
150
- // Verify each property produces correct CSS
151
- expect(output).toContain('color: #333;')
152
- expect(output).toContain('background-color: #fff;')
153
- expect(output).toContain('font-weight: 600;')
154
- expect(output).toContain('cursor: pointer;')
155
- expect(output).toContain('line-height: 1.5;')
156
- // Unit conversion: 14px fontSize, 8px padding
157
- expect(output).toContain('font-size:')
158
- expect(output).toContain('padding:')
159
- expect(output).toContain('border-radius:')
160
- expect(output).toContain('border-color: #ddd;')
161
- })
162
-
163
- it('handles edge properties (margin/padding shorthand)', () => {
164
- const theme = {
165
- margin: 16,
166
- marginTop: 8,
167
- padding: 12,
168
- }
169
- const result = styles({ theme, css: mockCss, rootSize: 16 })
170
- const output = String(result)
171
- expect(output).toContain('margin')
172
- expect(output).toContain('padding')
173
- })
174
-
175
- it('handles convert_fallback properties (width/size)', () => {
176
- const theme = { width: 200, size: 100 }
177
- const result = styles({ theme, css: mockCss, rootSize: 16 })
178
- const output = String(result)
179
- // width should win over size fallback for width
180
- expect(output).toContain('width:')
181
- })
182
-
183
- it('empty theme fast-path produces no CSS (same as before)', () => {
184
- const result = styles({ theme: {}, css: mockCss, rootSize: 16 })
185
- const cleaned = String(result).replace(/[,\s]/g, '')
186
- expect(cleaned).toBe('')
187
- })
188
- })
189
-
190
- describe('Tier 1: performance characteristics', () => {
191
- it('processes a typical 10-key theme in fewer iterations than full scan', () => {
192
- // This test documents the performance contract: for a theme with N keys,
193
- // we should iterate approximately N descriptors (plus some overlap from
194
- // multi-key descriptors like convert_fallback), NOT all 257.
195
- const theme = {
196
- color: 'red',
197
- backgroundColor: 'blue',
198
- fontSize: 14,
199
- padding: 8,
200
- borderRadius: 4,
201
- }
202
-
203
- // Count iterations by checking that the output is correct (proving the
204
- // fast path ran, not the fallback full-scan)
205
- const result = styles({ theme, css: mockCss, rootSize: 16 })
206
- const output = String(result)
207
- expect(output).toContain('color: red;')
208
- expect(output).toContain('background-color: blue;')
209
- // The key insight: if the output is correct with 5 keys, the indexed
210
- // path found the right descriptors without scanning all 257.
211
- // The fallback only fires when NO matches are found — with 5 real keys
212
- // the indexed path should always find matches.
213
- })
214
- })
215
-
216
- // Regression test for PR #283's `_fragments` reuse — the module-level array
217
- // was captured by reference inside the returned CSSResult's values, so the
218
- // next styles() call would clear the previous result's data before its
219
- // consumer ever resolved it.
220
- //
221
- // Pre-fix: r1.values[0] (the fragments array) was the SAME reference as the
222
- // module-level array; the second styles() call ran `_fragments.length = 0`
223
- // and wiped r1's fragments to []. Post-fix: each call gets its own array.
224
- //
225
- // Real-app symptom this caused: rocketstyle dimension themes (state="primary"
226
- // → blue background) produced empty CSS because element.ts calls
227
- // makeItResponsive 5 times (base/hover/focus/active/disabled), each calling
228
- // styles() under the hood. Only the LAST one kept its data; the rest
229
- // resolved empty. See `packages/ui/components/src/bases/element.ts`.
230
- describe('regression: CSSResult ownership of fragments array (PR #283 follow-up)', () => {
231
- // Lazy-capturing mock: stores strings + values without resolving, mimicking
232
- // the real CSSResult contract where consumers resolve later.
233
- const lazyCss = (strings: TemplateStringsArray, ...values: unknown[]) => ({
234
- strings,
235
- values,
236
- })
237
-
238
- it('first result retains its fragments after a second styles() call', () => {
239
- const r1 = styles({
240
- theme: { color: 'red', fontSize: 14 },
241
- css: lazyCss as never,
242
- rootSize: 16,
243
- })
244
- const r1Fragments = (r1 as { values: unknown[] }).values[0]
245
- const r1LenBefore = Array.isArray(r1Fragments) ? r1Fragments.length : -1
246
- expect(r1LenBefore).toBeGreaterThan(0)
247
-
248
- // Second call — pre-fix this cleared r1's array via shared module-level
249
- // reference. Post-fix: each call owns its array.
250
- styles({
251
- theme: { backgroundColor: 'blue', padding: 8 },
252
- css: lazyCss as never,
253
- rootSize: 16,
254
- })
255
-
256
- const r1FragmentsAfter = (r1 as { values: unknown[] }).values[0]
257
- const r1LenAfter = Array.isArray(r1FragmentsAfter) ? r1FragmentsAfter.length : -1
258
- expect(r1LenAfter).toBe(r1LenBefore)
259
- })
260
-
261
- it('two results from sequential calls have INDEPENDENT fragments arrays', () => {
262
- const r1 = styles({ theme: { color: 'red' }, css: lazyCss as never, rootSize: 16 })
263
- const r2 = styles({ theme: { backgroundColor: 'blue' }, css: lazyCss as never, rootSize: 16 })
264
-
265
- const r1Fragments = (r1 as { values: unknown[] }).values[0]
266
- const r2Fragments = (r2 as { values: unknown[] }).values[0]
267
- // Different array identities — r1 is not r2.
268
- expect(r1Fragments).not.toBe(r2Fragments)
269
- // Both populated.
270
- expect(Array.isArray(r1Fragments) && r1Fragments.length).toBeGreaterThan(0)
271
- expect(Array.isArray(r2Fragments) && r2Fragments.length).toBeGreaterThan(0)
272
- })
273
- })
@@ -1,169 +0,0 @@
1
- /** @jsxImportSource @pyreon/core */
2
- import { h } from '@pyreon/core'
3
- import { css, sheet, styled } from '@pyreon/styler'
4
- import { mountInBrowser } from '@pyreon/test-utils/browser'
5
- import { afterEach, describe, expect, it } from 'vitest'
6
- import { enrichTheme } from '../enrichTheme'
7
- import Provider from '../context'
8
- import { makeItResponsive } from '../responsive'
9
- import { stripUnit, value } from '../units'
10
-
11
- // Real-Chromium smoke for @pyreon/unistyle.
12
- //
13
- // These tests assert real browser behavior for responsive utilities —
14
- // things that happy-dom cannot observe because it does not resolve
15
- // @media queries or compute styles.
16
- //
17
- // What the suite locks in:
18
- // 1. `enrichTheme` attaches sorted breakpoints + media helpers to
19
- // `theme.__PYREON__`.
20
- // 2. The generated media-query helper emits `@media (min-width: XXem)`
21
- // CSS that Chromium resolves — a breakpoint under the current
22
- // viewport applies, one above does not.
23
- // 3. `<Provider>` provides the enriched theme so `styled()` components
24
- // resolve `p.theme` and interpolation functions see breakpoint data.
25
- // 4. `value()` and `stripUnit()` produce the same strings in Node and
26
- // the browser (pure math, but we lock parity).
27
-
28
- describe('@pyreon/unistyle in real browser', () => {
29
- afterEach(() => {
30
- sheet.clearCache()
31
- })
32
-
33
- it('enrichTheme attaches sortedBreakpoints and media helpers', () => {
34
- const enriched = enrichTheme({
35
- rootSize: 16,
36
- breakpoints: { xs: 0, sm: 576, md: 768 },
37
- })
38
- expect(enriched.__PYREON__.sortedBreakpoints).toEqual(['xs', 'sm', 'md'])
39
- expect(typeof enriched.__PYREON__.media?.xs).toBe('function')
40
- expect(typeof enriched.__PYREON__.media?.sm).toBe('function')
41
- expect(typeof enriched.__PYREON__.media?.md).toBe('function')
42
- })
43
-
44
- it('media helper emits @media rule that Chromium resolves at the current viewport', () => {
45
- // Viewport is ~1280px in chromium headless — use a tiny breakpoint
46
- // that is definitely below, and a huge one definitely above.
47
- const w = window.innerWidth
48
- expect(w).toBeGreaterThan(100)
49
-
50
- const Under = styled('div')`
51
- color: rgb(0, 0, 0);
52
- @media (min-width: 50px) {
53
- color: rgb(255, 0, 0);
54
- }
55
- `
56
- const Over = styled('div')`
57
- color: rgb(0, 0, 0);
58
- @media (min-width: 99999px) {
59
- color: rgb(0, 0, 255);
60
- }
61
- `
62
- const u = mountInBrowser(h(Under, { id: 'u' }))
63
- const o = mountInBrowser(h(Over, { id: 'o' }))
64
- expect(getComputedStyle(u.container.querySelector<HTMLElement>('#u')!).color).toBe(
65
- 'rgb(255, 0, 0)',
66
- )
67
- expect(getComputedStyle(o.container.querySelector<HTMLElement>('#o')!).color).toBe(
68
- 'rgb(0, 0, 0)',
69
- )
70
- u.unmount()
71
- o.unmount()
72
- })
73
-
74
- it('Provider enriches theme and styled() reads it via theme prop (no fallback — breaks loudly)', () => {
75
- // Use a non-default sentinel color: if Provider/styler integration is
76
- // broken, the interpolation receives `undefined`, the rule becomes
77
- // `color: undefined;` (invalid), and Chromium computes the default
78
- // black (rgb(0, 0, 0)) — the assertion fails immediately. No silent
79
- // fallback hides a regression.
80
- const theme = { rootSize: 16, breakpoints: { xs: 0, md: 768 }, tint: 'rgb(0, 200, 0)' }
81
-
82
- const Themed = styled('div')<{ theme?: typeof theme }>`
83
- ${(p) => css`
84
- color: ${(p.theme as typeof theme).tint};
85
- `}
86
- `
87
- const { container, unmount } = mountInBrowser(
88
- h(Provider, { theme }, h(Themed, { id: 'p' })),
89
- )
90
- const el = container.querySelector<HTMLElement>('#p')!
91
- expect(getComputedStyle(el).color).toBe('rgb(0, 200, 0)')
92
- unmount()
93
- })
94
-
95
- it('makeItResponsive resolves a breakpoint-object responsive prop via @media at the current viewport', () => {
96
- // The actual unistyle hot path: array/object responsive values flow
97
- // through normalizeTheme → transformTheme → optimizeTheme and emit
98
- // one `@media (min-width: …em)` rule per breakpoint. This test
99
- // exercises that pipeline end-to-end against real Chromium.
100
- const theme = enrichTheme({
101
- rootSize: 16,
102
- // xs=0 always applies; xl=99999 never applies at chromium default
103
- // viewport (~1280). We expect xs to win, xl to be ignored.
104
- breakpoints: { xs: 0, xl: 99999 },
105
- })
106
-
107
- const styles = ({ css: cssFn, theme: t }: { css: typeof css; theme: any }) => cssFn`
108
- color: ${t.tone};
109
- `
110
- const responsive = makeItResponsive({ key: '$colors', styles, css })
111
-
112
- const ResponsiveBox = styled('div')<{ $colors: Record<string, string>; theme?: typeof theme }>`
113
- ${(p) => responsive(p as any)};
114
- `
115
-
116
- const { container, unmount } = mountInBrowser(
117
- h(
118
- Provider,
119
- { theme },
120
- h(ResponsiveBox, {
121
- id: 'r',
122
- // Outer keys are property names; inner keys are breakpoints.
123
- $colors: { tone: { xs: 'rgb(255, 0, 0)', xl: 'rgb(0, 0, 255)' } },
124
- }),
125
- ),
126
- )
127
- const el = container.querySelector<HTMLElement>('#r')!
128
- expect(window.innerWidth).toBeLessThan(99999)
129
- expect(getComputedStyle(el).color).toBe('rgb(255, 0, 0)')
130
- unmount()
131
- })
132
-
133
- it('makeItResponsive resolves a breakpoint-array responsive prop (mobile-first cascade)', () => {
134
- const theme = enrichTheme({
135
- rootSize: 16,
136
- breakpoints: { xs: 0, sm: 99999 },
137
- })
138
-
139
- const styles = ({ css: cssFn, theme: t }: { css: typeof css; theme: any }) => cssFn`
140
- padding: ${t.pad};
141
- `
142
- const responsive = makeItResponsive({ key: '$pad', styles, css })
143
-
144
- const Padded = styled('div')<{ $pad: any; theme?: typeof theme }>`
145
- ${(p) => responsive(p as any)};
146
- `
147
-
148
- const { container, unmount } = mountInBrowser(
149
- h(
150
- Provider,
151
- { theme },
152
- h(Padded, { id: 'p', $pad: { pad: ['8px', '32px'] } }),
153
- ),
154
- )
155
- const el = container.querySelector<HTMLElement>('#p')!
156
- // xs (always-on) should apply 8px; sm (99999) does not apply.
157
- expect(getComputedStyle(el).padding).toBe('8px')
158
- unmount()
159
- })
160
-
161
- it('value() and stripUnit() behave identically in the browser', () => {
162
- expect(value(16)).toBe('1rem')
163
- expect(value('16px')).toBe('1rem')
164
- expect(value('50%')).toBe('50%')
165
- expect(value(null)).toBeNull()
166
- expect(stripUnit('24px', true)).toEqual([24, 'px'])
167
- expect(stripUnit('2.5rem', true)).toEqual([2.5, 'rem'])
168
- })
169
- })
@@ -1,134 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import stripUnit from '../units/stripUnit'
3
- import value from '../units/value'
4
- import values from '../units/values'
5
-
6
- describe('stripUnit', () => {
7
- it('strips px unit and returns number', () => {
8
- expect(stripUnit('16px')).toBe(16)
9
- })
10
-
11
- it('strips rem unit and returns number', () => {
12
- expect(stripUnit('1.5rem')).toBe(1.5)
13
- })
14
-
15
- it('strips em unit and returns number', () => {
16
- expect(stripUnit('2em')).toBe(2)
17
- })
18
-
19
- it('strips % and returns number', () => {
20
- expect(stripUnit('50%')).toBe(50)
21
- })
22
-
23
- it('handles negative values', () => {
24
- expect(stripUnit('-10px')).toBe(-10)
25
- })
26
-
27
- it('handles decimal values', () => {
28
- expect(stripUnit('0.5rem')).toBe(0.5)
29
- })
30
-
31
- it('returns original string for non-numeric strings', () => {
32
- expect(stripUnit('auto')).toBe('auto')
33
- })
34
-
35
- it('passes through numbers', () => {
36
- expect(stripUnit(42)).toBe(42)
37
- })
38
-
39
- describe('with unitReturn=true', () => {
40
- it('returns [value, unit] tuple for px', () => {
41
- expect(stripUnit('16px', true)).toEqual([16, 'px'])
42
- })
43
-
44
- it('returns [value, unit] tuple for rem', () => {
45
- expect(stripUnit('2rem', true)).toEqual([2, 'rem'])
46
- })
47
-
48
- it('returns [value, unit] tuple for %', () => {
49
- expect(stripUnit('50%', true)).toEqual([50, '%'])
50
- })
51
-
52
- it('returns [value, unit] tuple for em', () => {
53
- expect(stripUnit('1.5em', true)).toEqual([1.5, 'em'])
54
- })
55
-
56
- it('returns [value, empty string] for unitless number string', () => {
57
- expect(stripUnit('42', true)).toEqual([42, ''])
58
- })
59
-
60
- it('returns [number, undefined] for number input', () => {
61
- expect(stripUnit(42, true)).toEqual([42, undefined])
62
- })
63
- })
64
- })
65
-
66
- describe('value', () => {
67
- it('returns string values as-is', () => {
68
- expect(value('50%')).toBe('50%')
69
- expect(value('2em')).toBe('2em')
70
- expect(value('100vh')).toBe('100vh')
71
- })
72
-
73
- it('returns 0 as-is', () => {
74
- expect(value(0)).toBe(0)
75
- })
76
-
77
- it('returns null for null/undefined', () => {
78
- expect(value(null)).toBeNull()
79
- expect(value(undefined)).toBeNull()
80
- })
81
-
82
- it('converts unitless numbers to rem by default (divides by rootSize)', () => {
83
- // 16 / 16 = 1rem
84
- expect(value(16)).toBe('1rem')
85
- // 32 / 16 = 2rem
86
- expect(value(32)).toBe('2rem')
87
- })
88
-
89
- it('converts px values to rem', () => {
90
- expect(value('16px')).toBe('1rem')
91
- expect(value('32px')).toBe('2rem')
92
- })
93
-
94
- it('respects custom rootSize', () => {
95
- // 20 / 10 = 2rem
96
- expect(value(20, 10)).toBe('2rem')
97
- })
98
-
99
- it('respects outputUnit=px for unitless numbers', () => {
100
- expect(value(16, 16, 'px')).toBe('16px')
101
- })
102
- })
103
-
104
- describe('values', () => {
105
- it('returns the first defined value converted', () => {
106
- expect(values([undefined, null, 16])).toBe('1rem')
107
- })
108
-
109
- it('returns the first value if defined', () => {
110
- expect(values([32, 16])).toBe('2rem')
111
- })
112
-
113
- it('passes through string values', () => {
114
- expect(values(['50%'])).toBe('50%')
115
- })
116
-
117
- it('returns null when all values are null/undefined', () => {
118
- expect(values([undefined, null])).toBeNull()
119
- })
120
-
121
- it('returns 0 for zero value', () => {
122
- expect(values([0])).toBe(0)
123
- })
124
-
125
- it('joins array values with spaces', () => {
126
- const result = values([[16, 32]])
127
- expect(result).toContain('1rem')
128
- expect(result).toContain('2rem')
129
- })
130
-
131
- it('respects rootSize parameter', () => {
132
- expect(values([20], 10)).toBe('2rem')
133
- })
134
- })
package/src/context.tsx DELETED
@@ -1,44 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { nativeCompat, provide } from '@pyreon/core'
3
- import { ThemeContext } from '@pyreon/styler'
4
- import { Provider as CoreProvider, context } from '@pyreon/ui-core'
5
- import type { PyreonTheme } from './enrichTheme'
6
- import { enrichTheme } from './enrichTheme'
7
-
8
- export type TProvider = {
9
- theme: PyreonTheme
10
- children?: VNode | null
11
- }
12
-
13
- /**
14
- * @internal Low-level provider — use `PyreonUI` from `@pyreon/ui-core` instead.
15
- *
16
- * Unistyle Provider — wraps the core Provider and enriches the theme
17
- * with pre-computed sorted breakpoints and media-query tagged-template
18
- * helpers consumed by `makeItResponsive`.
19
- *
20
- * @deprecated Prefer `<PyreonUI theme={theme} mode="light">` which handles
21
- * all three context layers (styler, core, mode) in one component.
22
- */
23
- function Provider(props: TProvider): VNode | null {
24
- const { theme, children } = props
25
-
26
- const enrichedTheme = enrichTheme(theme)
27
-
28
- // Provide enriched theme to both the ui-core context (for rocketstyle/elements)
29
- // AND the styler ThemeContext (for styled() components and makeItResponsive).
30
- // Without this, styled() components receive an empty theme and all responsive
31
- // styles are skipped (@media queries produce NaN values).
32
- // ThemeContext is a ReactiveContext — provide an accessor.
33
- provide(ThemeContext, () => enrichedTheme)
34
-
35
- return CoreProvider({ theme: enrichedTheme, children }) as VNode | null
36
- }
37
-
38
- // Mark as native — invoked by PyreonUI internally; needs Pyreon's setup
39
- // frame for provide(ThemeContext, ...) to reach descendants.
40
- nativeCompat(Provider)
41
-
42
- export { context }
43
-
44
- export default Provider
@@ -1,42 +0,0 @@
1
- import { config, isEmpty } from '@pyreon/ui-core'
2
- import { createMediaQueries, sortBreakpoints } from './responsive'
3
-
4
- export type PyreonTheme = {
5
- rootSize?: number
6
- breakpoints?: Record<string, number>
7
- __PYREON__?: {
8
- sortedBreakpoints: string[] | undefined
9
- media: Record<string, (...args: any[]) => any> | undefined
10
- }
11
- } & Record<string, unknown>
12
-
13
- /**
14
- * Enrich a theme with pre-computed responsive utilities.
15
- * Adds sorted breakpoints and media-query tagged-template helpers
16
- * to `theme.__PYREON__` for consumption by `makeItResponsive`.
17
- *
18
- * This is a pure function — safe to call outside of component context.
19
- *
20
- * @example
21
- * const enriched = enrichTheme({ rootSize: 16, breakpoints: { xs: 0, sm: 576, md: 768 } })
22
- * enriched.__PYREON__.sortedBreakpoints // ['xs', 'sm', 'md']
23
- * enriched.__PYREON__.media.sm // tagged-template for @media (min-width: 36em)
24
- */
25
- export function enrichTheme<T extends PyreonTheme>(
26
- theme: T,
27
- ): T & Required<Pick<PyreonTheme, '__PYREON__'>> {
28
- const { breakpoints, rootSize = 16 } = theme
29
-
30
- const sortedBreakpoints =
31
- breakpoints && !isEmpty(breakpoints) ? sortBreakpoints(breakpoints) : undefined
32
-
33
- const media =
34
- breakpoints && !isEmpty(breakpoints)
35
- ? createMediaQueries({ breakpoints, css: config.css, rootSize })
36
- : undefined
37
-
38
- return {
39
- ...theme,
40
- __PYREON__: { sortedBreakpoints, media },
41
- }
42
- }
package/src/env.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /**
2
- * Minimal process type — just enough for `process.env.NODE_ENV` checks.
3
- * Avoids requiring @types/node in consumers that import pyreon source
4
- * via the `"bun"` export condition.
5
- */
6
- declare var process: { env: { NODE_ENV?: string } }