@pyreon/styler 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.
- package/package.json +6 -5
- package/src/ThemeProvider.ts +37 -0
- package/src/__tests__/ThemeProvider.test.ts +67 -0
- package/src/__tests__/benchmark.bench.ts +189 -0
- package/src/__tests__/composition-chain.test.ts +489 -0
- package/src/__tests__/css.test.ts +70 -0
- package/src/__tests__/forward.test.ts +282 -0
- package/src/__tests__/globalStyle.test.ts +72 -0
- package/src/__tests__/hash.test.ts +70 -0
- package/src/__tests__/hybrid-injection.test.ts +205 -0
- package/src/__tests__/index.ts +14 -0
- package/src/__tests__/insertion-effect.test.ts +106 -0
- package/src/__tests__/integration.test.ts +149 -0
- package/src/__tests__/keyframes.test.ts +68 -0
- package/src/__tests__/memory-growth.test.ts +152 -0
- package/src/__tests__/p3-features.test.ts +258 -0
- package/src/__tests__/resolve.test.ts +249 -0
- package/src/__tests__/shared.test.ts +73 -0
- package/src/__tests__/sheet-advanced.test.ts +669 -0
- package/src/__tests__/sheet-split-atrules.test.ts +411 -0
- package/src/__tests__/sheet.test.ts +164 -0
- package/src/__tests__/styled-ssr.test.ts +67 -0
- package/src/__tests__/styled.test.ts +303 -0
- package/src/__tests__/theme.test.ts +33 -0
- package/src/__tests__/useCSS.test.ts +142 -0
- package/src/css.ts +13 -0
- package/src/forward.ts +276 -0
- package/src/globalStyle.ts +48 -0
- package/src/hash.ts +30 -0
- package/src/index.ts +15 -0
- package/src/keyframes.ts +36 -0
- package/src/resolve.ts +172 -0
- package/src/shared.ts +12 -0
- package/src/sheet.ts +387 -0
- package/src/styled.tsx +277 -0
- package/src/useCSS.ts +20 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
3
|
+
import { sheet } from "../sheet"
|
|
4
|
+
import { styled } from "../styled"
|
|
5
|
+
|
|
6
|
+
describe("styled", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
sheet.reset()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe("basic creation", () => {
|
|
12
|
+
it("returns a tagged template function", () => {
|
|
13
|
+
const tagFn = styled("div")
|
|
14
|
+
expect(typeof tagFn).toBe("function")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("tagged template returns a ComponentFn", () => {
|
|
18
|
+
const Comp = styled("div")`display: flex;`
|
|
19
|
+
expect(typeof Comp).toBe("function")
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe("static CSS (no function interpolations)", () => {
|
|
24
|
+
it("produces a VNode with the correct tag", () => {
|
|
25
|
+
const Comp = styled("div")`display: flex;`
|
|
26
|
+
const vnode = Comp({}) as VNode
|
|
27
|
+
expect(vnode.type).toBe("div")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it("produces a VNode with the correct tag for span", () => {
|
|
31
|
+
const Comp = styled("span")`color: red;`
|
|
32
|
+
const vnode = Comp({}) as VNode
|
|
33
|
+
expect(vnode.type).toBe("span")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("applies a generated className", () => {
|
|
37
|
+
const Comp = styled("div")`display: flex;`
|
|
38
|
+
const vnode = Comp({}) as VNode
|
|
39
|
+
expect(vnode.props.class).toMatch(/^pyr-[0-9a-z]+$/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("same component produces same className across calls", () => {
|
|
43
|
+
const Comp = styled("div")`display: flex; color: red;`
|
|
44
|
+
const vnode1 = Comp({}) as VNode
|
|
45
|
+
const vnode2 = Comp({}) as VNode
|
|
46
|
+
expect(vnode1.props.class).toBe(vnode2.props.class)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("different CSS produces different classNames", () => {
|
|
50
|
+
const Comp1 = styled("div")`color: red;`
|
|
51
|
+
const Comp2 = styled("div")`color: blue;`
|
|
52
|
+
const vnode1 = Comp1({}) as VNode
|
|
53
|
+
const vnode2 = Comp2({}) as VNode
|
|
54
|
+
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe("empty CSS", () => {
|
|
59
|
+
it("renders element without className for empty template", () => {
|
|
60
|
+
const Comp = styled("div")``
|
|
61
|
+
const vnode = Comp({}) as VNode
|
|
62
|
+
expect(vnode.props.class).toBeFalsy()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("renders element without className for whitespace-only template", () => {
|
|
66
|
+
const Comp = styled("div")` `
|
|
67
|
+
const vnode = Comp({}) as VNode
|
|
68
|
+
expect(vnode.props.class).toBeFalsy()
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe("static interpolations (non-function)", () => {
|
|
73
|
+
it("treats string interpolations as static", () => {
|
|
74
|
+
const color = "red"
|
|
75
|
+
const Comp = styled("div")`color: ${color};`
|
|
76
|
+
const vnode = Comp({}) as VNode
|
|
77
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("treats number interpolations as static", () => {
|
|
81
|
+
const size = 16
|
|
82
|
+
const Comp = styled("div")`font-size: ${size}px;`
|
|
83
|
+
const vnode = Comp({}) as VNode
|
|
84
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("dynamic CSS (function interpolations)", () => {
|
|
89
|
+
it("resolves function interpolations with props", () => {
|
|
90
|
+
const Comp = styled("div")`
|
|
91
|
+
color: ${(props: any) => props.color};
|
|
92
|
+
`
|
|
93
|
+
const vnode = Comp({ color: "red" }) as VNode
|
|
94
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("different prop values produce different classNames", () => {
|
|
98
|
+
const Comp = styled("div")`
|
|
99
|
+
color: ${(props: any) => props.color};
|
|
100
|
+
`
|
|
101
|
+
const vnode1 = Comp({ color: "red" }) as VNode
|
|
102
|
+
const vnode2 = Comp({ color: "blue" }) as VNode
|
|
103
|
+
expect(vnode1.props.class).not.toBe(vnode2.props.class)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("same prop values produce same className (dedup)", () => {
|
|
107
|
+
const Comp = styled("div")`
|
|
108
|
+
color: ${(props: any) => props.color};
|
|
109
|
+
`
|
|
110
|
+
const vnode1 = Comp({ color: "red" }) as VNode
|
|
111
|
+
const vnode2 = Comp({ color: "red" }) as VNode
|
|
112
|
+
expect(vnode1.props.class).toBe(vnode2.props.class)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("handles functions returning empty string", () => {
|
|
116
|
+
const Comp = styled("div")`${() => ""}`
|
|
117
|
+
const vnode = Comp({}) as VNode
|
|
118
|
+
expect(vnode.props.class).toBeFalsy()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("handles functions returning false", () => {
|
|
122
|
+
const Comp = styled("div")`${(props: any) => (props.active ? "color: red;" : false)}`
|
|
123
|
+
const vnode = Comp({ active: false }) as VNode
|
|
124
|
+
expect(vnode.props.class).toBeFalsy()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe("className merging", () => {
|
|
129
|
+
it("merges user class with generated className", () => {
|
|
130
|
+
const Comp = styled("div")`display: flex;`
|
|
131
|
+
const vnode = Comp({ class: "custom" }) as VNode
|
|
132
|
+
expect(vnode.props.class).toContain("pyr-")
|
|
133
|
+
expect(vnode.props.class).toContain("custom")
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("merges user className with generated className", () => {
|
|
137
|
+
const Comp = styled("div")`display: flex;`
|
|
138
|
+
const vnode = Comp({ className: "custom" }) as VNode
|
|
139
|
+
expect(vnode.props.class).toContain("pyr-")
|
|
140
|
+
expect(vnode.props.class).toContain("custom")
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("handles class without generated className (empty CSS)", () => {
|
|
144
|
+
const Comp = styled("div")``
|
|
145
|
+
const vnode = Comp({ class: "custom" }) as VNode
|
|
146
|
+
expect(vnode.props.class).toBe("custom")
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe("as prop (polymorphic rendering)", () => {
|
|
151
|
+
it("changes the rendered element type", () => {
|
|
152
|
+
const Comp = styled("div")`display: flex;`
|
|
153
|
+
const vnode = Comp({ as: "section" }) as VNode
|
|
154
|
+
expect(vnode.type).toBe("section")
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("renders as button", () => {
|
|
158
|
+
const Comp = styled("div")`cursor: pointer;`
|
|
159
|
+
const vnode = Comp({ as: "button" }) as VNode
|
|
160
|
+
expect(vnode.type).toBe("button")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("defaults to original tag when as is not provided", () => {
|
|
164
|
+
const Comp = styled("span")`color: red;`
|
|
165
|
+
const vnode = Comp({}) as VNode
|
|
166
|
+
expect(vnode.type).toBe("span")
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe("prop filtering (HTML elements)", () => {
|
|
171
|
+
it("forwards valid HTML attributes", () => {
|
|
172
|
+
const Comp = styled("input")`display: block;`
|
|
173
|
+
const vnode = Comp({ type: "text", placeholder: "test" }) as VNode
|
|
174
|
+
expect(vnode.props.type).toBe("text")
|
|
175
|
+
expect(vnode.props.placeholder).toBe("test")
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("forwards data-* attributes", () => {
|
|
179
|
+
const Comp = styled("div")`display: flex;`
|
|
180
|
+
const vnode = Comp({ "data-testid": "hello" }) as VNode
|
|
181
|
+
expect(vnode.props["data-testid"]).toBe("hello")
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("forwards aria-* attributes", () => {
|
|
185
|
+
const Comp = styled("div")`display: flex;`
|
|
186
|
+
const vnode = Comp({ "aria-label": "world" }) as VNode
|
|
187
|
+
expect(vnode.props["aria-label"]).toBe("world")
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it("forwards event handlers (on* props)", () => {
|
|
191
|
+
const handler = () => {
|
|
192
|
+
/* no-op */
|
|
193
|
+
}
|
|
194
|
+
const Comp = styled("button")`cursor: pointer;`
|
|
195
|
+
const vnode = Comp({ onClick: handler }) as VNode
|
|
196
|
+
expect(vnode.props.onClick).toBe(handler)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it("filters unknown props for HTML elements", () => {
|
|
200
|
+
const Comp = styled("div")`display: flex;`
|
|
201
|
+
const vnode = Comp({ unknownProp: "test" }) as VNode
|
|
202
|
+
expect(vnode.props.unknownProp).toBeUndefined()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("filters $-prefixed transient props", () => {
|
|
206
|
+
const Comp = styled("div")`display: flex;`
|
|
207
|
+
const vnode = Comp({ $variant: "primary" }) as VNode
|
|
208
|
+
expect(vnode.props.$variant).toBeUndefined()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("does not forward class/className as separate props", () => {
|
|
212
|
+
const Comp = styled("div")`display: flex;`
|
|
213
|
+
const vnode = Comp({ class: "extra", className: "another" }) as VNode
|
|
214
|
+
expect(vnode.props.className).toBeUndefined()
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe("shouldForwardProp option", () => {
|
|
219
|
+
it("uses custom filter when provided", () => {
|
|
220
|
+
const Comp = styled("div", {
|
|
221
|
+
shouldForwardProp: (prop) => prop !== "color",
|
|
222
|
+
})`display: flex;`
|
|
223
|
+
const vnode = Comp({ color: "red", "data-testid": "test" }) as VNode
|
|
224
|
+
expect(vnode.props.color).toBeUndefined()
|
|
225
|
+
expect(vnode.props["data-testid"]).toBe("test")
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("custom filter controls all prop forwarding", () => {
|
|
229
|
+
const Comp = styled("div", {
|
|
230
|
+
shouldForwardProp: () => false,
|
|
231
|
+
})`display: flex;`
|
|
232
|
+
const vnode = Comp({ id: "test", role: "button" }) as VNode
|
|
233
|
+
expect(vnode.props.id).toBeUndefined()
|
|
234
|
+
expect(vnode.props.role).toBeUndefined()
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
describe("boost option", () => {
|
|
239
|
+
it("accepts boost option without error", () => {
|
|
240
|
+
const Comp = styled("div", { boost: true })`color: red;`
|
|
241
|
+
const vnode = Comp({}) as VNode
|
|
242
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
describe("children forwarding", () => {
|
|
247
|
+
it("passes single child through", () => {
|
|
248
|
+
const Comp = styled("div")`display: flex;`
|
|
249
|
+
const vnode = Comp({ children: "hello" }) as VNode
|
|
250
|
+
expect(vnode.children).toEqual(["hello"])
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("passes array children through", () => {
|
|
254
|
+
const Comp = styled("div")`display: flex;`
|
|
255
|
+
const children = ["a", "b", "c"]
|
|
256
|
+
const vnode = Comp({ children }) as VNode
|
|
257
|
+
expect(vnode.children).toEqual(["a", "b", "c"])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it("handles null children", () => {
|
|
261
|
+
const Comp = styled("div")`display: flex;`
|
|
262
|
+
const vnode = Comp({ children: null }) as VNode
|
|
263
|
+
expect(vnode.children).toEqual([])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it("handles undefined children", () => {
|
|
267
|
+
const Comp = styled("div")`display: flex;`
|
|
268
|
+
const vnode = Comp({}) as VNode
|
|
269
|
+
expect(vnode.children).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe("styled.tag (Proxy)", () => {
|
|
275
|
+
afterEach(() => {
|
|
276
|
+
sheet.reset()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("styled.div creates a div component", () => {
|
|
280
|
+
const Comp = styled.div`color: red;`
|
|
281
|
+
const vnode = Comp({}) as VNode
|
|
282
|
+
expect(vnode.type).toBe("div")
|
|
283
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it("styled.span creates a span component", () => {
|
|
287
|
+
const Comp = styled.span`font-size: 16px;`
|
|
288
|
+
const vnode = Comp({}) as VNode
|
|
289
|
+
expect(vnode.type).toBe("span")
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it("styled.button creates a button component", () => {
|
|
293
|
+
const Comp = styled.button`cursor: pointer;`
|
|
294
|
+
const vnode = Comp({}) as VNode
|
|
295
|
+
expect(vnode.type).toBe("button")
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it("styled.section creates a section component", () => {
|
|
299
|
+
const Comp = styled.section`padding: 20px;`
|
|
300
|
+
const vnode = Comp({}) as VNode
|
|
301
|
+
expect(vnode.type).toBe("section")
|
|
302
|
+
})
|
|
303
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { ThemeContext, useTheme } from "../ThemeProvider"
|
|
3
|
+
|
|
4
|
+
describe("ThemeContext", () => {
|
|
5
|
+
it("is a Context object", () => {
|
|
6
|
+
expect(ThemeContext).toBeDefined()
|
|
7
|
+
expect(ThemeContext.id).toBeDefined()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it("has an id property for context identification", () => {
|
|
11
|
+
expect(typeof ThemeContext.id).toBe("symbol")
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe("useTheme", () => {
|
|
16
|
+
it("is a function", () => {
|
|
17
|
+
expect(typeof useTheme).toBe("function")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("returns the default theme (empty object) when called outside a provider", () => {
|
|
21
|
+
const theme = useTheme()
|
|
22
|
+
expect(theme).toEqual({})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("can be called with a type parameter", () => {
|
|
26
|
+
interface MyTheme {
|
|
27
|
+
primary: string
|
|
28
|
+
spacing: number
|
|
29
|
+
}
|
|
30
|
+
const theme = useTheme<MyTheme>()
|
|
31
|
+
expect(theme).toBeDefined()
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest"
|
|
2
|
+
import { css } from "../css"
|
|
3
|
+
import { sheet } from "../sheet"
|
|
4
|
+
import { useCSS } from "../useCSS"
|
|
5
|
+
|
|
6
|
+
describe("useCSS", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
sheet.clearAll()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe("basic usage", () => {
|
|
12
|
+
it("returns a className string for static CSS", () => {
|
|
13
|
+
const template = css`display: flex;`
|
|
14
|
+
const result = useCSS(template)
|
|
15
|
+
expect(typeof result).toBe("string")
|
|
16
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("returns different classNames for different CSS", () => {
|
|
20
|
+
const template1 = css`display: flex;`
|
|
21
|
+
const template2 = css`display: block;`
|
|
22
|
+
|
|
23
|
+
const r1 = useCSS(template1)
|
|
24
|
+
const r2 = useCSS(template2)
|
|
25
|
+
|
|
26
|
+
expect(r1).toMatch(/^pyr-/)
|
|
27
|
+
expect(r2).toMatch(/^pyr-/)
|
|
28
|
+
expect(r1).not.toBe(r2)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("returns empty string for empty CSS", () => {
|
|
32
|
+
const template = css``
|
|
33
|
+
const result = useCSS(template)
|
|
34
|
+
expect(result).toBe("")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("returns empty string for whitespace-only CSS", () => {
|
|
38
|
+
const template = css` `
|
|
39
|
+
const result = useCSS(template)
|
|
40
|
+
expect(result).toBe("")
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("dynamic values", () => {
|
|
45
|
+
it("works with static interpolation values", () => {
|
|
46
|
+
const color = "red"
|
|
47
|
+
const template = css`color: ${color};`
|
|
48
|
+
const result = useCSS(template)
|
|
49
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("works with function interpolations resolved via props", () => {
|
|
53
|
+
const template = css`color: ${(p: any) => p.color};`
|
|
54
|
+
const result = useCSS(template, { color: "blue" })
|
|
55
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("different prop values produce different classNames", () => {
|
|
59
|
+
const template = css`color: ${(p: any) => p.color};`
|
|
60
|
+
|
|
61
|
+
const r1 = useCSS(template, { color: "red" })
|
|
62
|
+
const r2 = useCSS(template, { color: "green" })
|
|
63
|
+
|
|
64
|
+
expect(r1).not.toBe(r2)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe("caching", () => {
|
|
69
|
+
it("same CSS returns same className on repeated calls", () => {
|
|
70
|
+
const template = css`display: flex;`
|
|
71
|
+
const cls1 = useCSS(template)
|
|
72
|
+
const cls2 = useCSS(template)
|
|
73
|
+
expect(cls1).toBe(cls2)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it("same dynamic CSS with same props returns same className", () => {
|
|
77
|
+
const template = css`color: ${(p: any) => p.color};`
|
|
78
|
+
const cls1 = useCSS(template, { color: "red" })
|
|
79
|
+
const cls2 = useCSS(template, { color: "red" })
|
|
80
|
+
expect(cls1).toBe(cls2)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe("cache hit path", () => {
|
|
85
|
+
it("reuses cached className with identical resolved CSS", () => {
|
|
86
|
+
const template = css`color: ${(p: any) => p.color};`
|
|
87
|
+
const cls1 = useCSS(template, { color: "red" })
|
|
88
|
+
const cls2 = useCSS(template, { color: "red" })
|
|
89
|
+
expect(cls1).toBe(cls2)
|
|
90
|
+
expect(cls1).toMatch(/^pyr-/)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("updates className when resolved CSS changes", () => {
|
|
94
|
+
const template = css`color: ${(p: any) => p.color};`
|
|
95
|
+
const cls1 = useCSS(template, { color: "red" })
|
|
96
|
+
const cls2 = useCSS(template, { color: "blue" })
|
|
97
|
+
expect(cls1).not.toBe(cls2)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe("boost parameter", () => {
|
|
102
|
+
it("does not throw when boost is true", () => {
|
|
103
|
+
const template = css`display: flex;`
|
|
104
|
+
const result = useCSS(template, undefined, true)
|
|
105
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("does not throw when boost is false", () => {
|
|
109
|
+
const template = css`display: flex;`
|
|
110
|
+
const result = useCSS(template, undefined, false)
|
|
111
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe("without theme and without props", () => {
|
|
116
|
+
it("uses empty object when no props and no theme", () => {
|
|
117
|
+
const template = css`display: flex;`
|
|
118
|
+
const result = useCSS(template)
|
|
119
|
+
expect(result).toMatch(/^pyr-[0-9a-z]+$/)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("handles dynamic template without theme or props", () => {
|
|
123
|
+
const template = css`color: ${(p: any) => p.color ?? "red"};`
|
|
124
|
+
const result = useCSS(template, undefined)
|
|
125
|
+
expect(result).toMatch(/^pyr-/)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe("empty CSS from dynamic resolution", () => {
|
|
130
|
+
it("returns empty className when dynamic CSS resolves to empty", () => {
|
|
131
|
+
const template = css`${(p: any) => (p.color ? `color: ${p.color};` : "")}`
|
|
132
|
+
|
|
133
|
+
// First call: non-empty CSS
|
|
134
|
+
const cls1 = useCSS(template, { color: "red" })
|
|
135
|
+
expect(cls1).toMatch(/^pyr-/)
|
|
136
|
+
|
|
137
|
+
// Second call: empty CSS
|
|
138
|
+
const cls2 = useCSS(template, { color: undefined })
|
|
139
|
+
expect(cls2).toBe("")
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
})
|
package/src/css.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CSSResult, type Interpolation } from "./resolve"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tagged template function for CSS. Captures the template strings and
|
|
5
|
+
* interpolation values as a lazy CSSResult — resolution is deferred
|
|
6
|
+
* until a styled component renders.
|
|
7
|
+
*
|
|
8
|
+
* Works as both a tagged template (`css\`...\``) and a regular function
|
|
9
|
+
* call (`css(...args)`) since tagged templates are syntactic sugar for
|
|
10
|
+
* function calls with (TemplateStringsArray, ...values).
|
|
11
|
+
*/
|
|
12
|
+
export const css = (strings: TemplateStringsArray, ...values: Interpolation[]): CSSResult =>
|
|
13
|
+
new CSSResult(strings, values)
|