@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.
- package/package.json +7 -9
- package/src/__tests__/alignContent.test.ts +0 -121
- package/src/__tests__/borderRadius.test.ts +0 -125
- package/src/__tests__/camelToKebab.test.ts +0 -44
- package/src/__tests__/context.test.ts +0 -147
- package/src/__tests__/createMediaQueries.test.ts +0 -98
- package/src/__tests__/edge.test.ts +0 -164
- package/src/__tests__/enrichTheme.test.ts +0 -56
- package/src/__tests__/extendCss.test.ts +0 -45
- package/src/__tests__/index.test.ts +0 -79
- package/src/__tests__/makeItResponsive.test.ts +0 -431
- package/src/__tests__/manifest-snapshot.test.ts +0 -34
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/optimizeBreakpointDeltas.test.ts +0 -124
- package/src/__tests__/processDescriptor.test.ts +0 -322
- package/src/__tests__/responsive.test.ts +0 -221
- package/src/__tests__/special-keys.test.ts +0 -120
- package/src/__tests__/styles.test.ts +0 -273
- package/src/__tests__/unistyle.browser.test.tsx +0 -169
- package/src/__tests__/units.test.ts +0 -134
- package/src/context.tsx +0 -44
- package/src/enrichTheme.ts +0 -42
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -91
- package/src/manifest.ts +0 -197
- package/src/responsive/breakpoints.ts +0 -15
- package/src/responsive/createMediaQueries.ts +0 -43
- package/src/responsive/index.ts +0 -15
- package/src/responsive/makeItResponsive.ts +0 -223
- package/src/responsive/normalizeTheme.ts +0 -79
- package/src/responsive/optimizeBreakpointDeltas.ts +0 -190
- package/src/responsive/optimizeTheme.ts +0 -60
- package/src/responsive/sortBreakpoints.ts +0 -10
- package/src/responsive/transformTheme.ts +0 -54
- package/src/styles/alignContent.ts +0 -62
- package/src/styles/extendCss.ts +0 -26
- package/src/styles/index.ts +0 -16
- package/src/styles/shorthands/borderRadius.ts +0 -89
- package/src/styles/shorthands/edge.ts +0 -108
- package/src/styles/shorthands/index.ts +0 -4
- package/src/styles/styles/camelToKebab.ts +0 -3
- package/src/styles/styles/index.ts +0 -132
- package/src/styles/styles/processDescriptor.ts +0 -136
- package/src/styles/styles/propertyMap.ts +0 -438
- package/src/styles/styles/types.ts +0 -368
- package/src/types.ts +0 -175
- package/src/units/index.ts +0 -6
- package/src/units/stripUnit.ts +0 -25
- package/src/units/value.ts +0 -47
- 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
|
package/src/enrichTheme.ts
DELETED
|
@@ -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