@praxisjs/css 0.3.4 → 0.3.5
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/CHANGELOG.md +22 -0
- package/dist/builder/css-builder.d.ts.map +1 -1
- package/dist/builder/css-builder.js +68 -74
- package/dist/builder/css-builder.js.map +1 -1
- package/dist/builder/keyframes.d.ts +5 -0
- package/dist/builder/keyframes.d.ts.map +1 -1
- package/dist/builder/keyframes.js +12 -14
- package/dist/builder/keyframes.js.map +1 -1
- package/dist/cx.d.ts.map +1 -1
- package/dist/cx.js +23 -18
- package/dist/cx.js.map +1 -1
- package/dist/decorators/param.d.ts +1 -2
- package/dist/decorators/param.d.ts.map +1 -1
- package/dist/decorators/param.js +9 -17
- package/dist/decorators/param.js.map +1 -1
- package/dist/decorators/style.d.ts.map +1 -1
- package/dist/decorators/style.js +61 -33
- package/dist/decorators/style.js.map +1 -1
- package/dist/decorators/styled.d.ts.map +1 -1
- package/dist/decorators/styled.js +112 -95
- package/dist/decorators/styled.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +19 -35
- package/dist/extract.js.map +1 -1
- package/dist/global.d.ts +3 -0
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +9 -4
- package/dist/global.js.map +1 -1
- package/dist/internal/kebab.d.ts +5 -0
- package/dist/internal/kebab.d.ts.map +1 -0
- package/dist/internal/kebab.js +23 -0
- package/dist/internal/kebab.js.map +1 -0
- package/dist/internal/lifecycle.d.ts +15 -0
- package/dist/internal/lifecycle.d.ts.map +1 -0
- package/dist/internal/lifecycle.js +16 -0
- package/dist/internal/lifecycle.js.map +1 -0
- package/dist/internal/registry.d.ts +5 -1
- package/dist/internal/registry.d.ts.map +1 -1
- package/dist/internal/registry.js +9 -23
- package/dist/internal/registry.js.map +1 -1
- package/dist/internal/sheet.d.ts +20 -0
- package/dist/internal/sheet.d.ts.map +1 -0
- package/dist/internal/sheet.js +56 -0
- package/dist/internal/sheet.js.map +1 -0
- package/dist/internal/static-mode.d.ts +2 -0
- package/dist/internal/static-mode.d.ts.map +1 -0
- package/dist/internal/static-mode.js +2 -0
- package/dist/internal/static-mode.js.map +1 -0
- package/dist/preflight.d.ts +3 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +7 -3
- package/dist/preflight.js.map +1 -1
- package/dist/tokens/decorators.d.ts.map +1 -1
- package/dist/tokens/decorators.js +13 -4
- package/dist/tokens/decorators.js.map +1 -1
- package/dist/tokens/theme-instance.d.ts +5 -0
- package/dist/tokens/theme-instance.d.ts.map +1 -1
- package/dist/tokens/theme-instance.js +25 -17
- package/dist/tokens/theme-instance.js.map +1 -1
- package/dist/tokens/token-sheet.d.ts.map +1 -1
- package/dist/tokens/token-sheet.js +12 -9
- package/dist/tokens/token-sheet.js.map +1 -1
- package/package.json +13 -5
- package/src/builder/css-builder.ts +79 -83
- package/src/builder/keyframes.ts +12 -20
- package/src/cx.ts +20 -13
- package/src/decorators/param.ts +12 -16
- package/src/decorators/style.ts +79 -35
- package/src/decorators/styled.ts +127 -111
- package/src/extract.ts +18 -34
- package/src/global.ts +10 -9
- package/src/internal/kebab.ts +23 -0
- package/src/internal/lifecycle.ts +21 -0
- package/src/internal/registry.ts +9 -35
- package/src/internal/sheet.ts +71 -0
- package/src/internal/static-mode.ts +6 -0
- package/src/preflight.ts +8 -3
- package/src/tokens/decorators.ts +15 -3
- package/src/tokens/theme-instance.ts +26 -16
- package/src/tokens/token-sheet.ts +12 -12
- package/dist/__tests__/css-builder.test.d.ts +0 -2
- package/dist/__tests__/css-builder.test.d.ts.map +0 -1
- package/dist/__tests__/css-builder.test.js +0 -198
- package/dist/__tests__/css-builder.test.js.map +0 -1
- package/dist/__tests__/cx.test.d.ts +0 -2
- package/dist/__tests__/cx.test.d.ts.map +0 -1
- package/dist/__tests__/cx.test.js +0 -47
- package/dist/__tests__/cx.test.js.map +0 -1
- package/dist/__tests__/decorators.test.d.ts +0 -2
- package/dist/__tests__/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/decorators.test.js +0 -264
- package/dist/__tests__/decorators.test.js.map +0 -1
- package/dist/__tests__/extract.test.d.ts +0 -2
- package/dist/__tests__/extract.test.d.ts.map +0 -1
- package/dist/__tests__/extract.test.js +0 -416
- package/dist/__tests__/extract.test.js.map +0 -1
- package/dist/__tests__/global.test.d.ts +0 -2
- package/dist/__tests__/global.test.d.ts.map +0 -1
- package/dist/__tests__/global.test.js +0 -136
- package/dist/__tests__/global.test.js.map +0 -1
- package/dist/__tests__/keyframes.test.d.ts +0 -2
- package/dist/__tests__/keyframes.test.d.ts.map +0 -1
- package/dist/__tests__/keyframes.test.js +0 -90
- package/dist/__tests__/keyframes.test.js.map +0 -1
- package/dist/__tests__/preflight.test.d.ts +0 -2
- package/dist/__tests__/preflight.test.d.ts.map +0 -1
- package/dist/__tests__/preflight.test.js +0 -74
- package/dist/__tests__/preflight.test.js.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.js +0 -132
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/style.test.d.ts +0 -2
- package/dist/__tests__/style.test.d.ts.map +0 -1
- package/dist/__tests__/style.test.js +0 -281
- package/dist/__tests__/style.test.js.map +0 -1
- package/dist/__tests__/styled.test.d.ts +0 -2
- package/dist/__tests__/styled.test.d.ts.map +0 -1
- package/dist/__tests__/styled.test.js +0 -904
- package/dist/__tests__/styled.test.js.map +0 -1
- package/dist/__tests__/theme-instance.test.d.ts +0 -2
- package/dist/__tests__/theme-instance.test.d.ts.map +0 -1
- package/dist/__tests__/theme-instance.test.js +0 -309
- package/dist/__tests__/theme-instance.test.js.map +0 -1
- package/dist/__tests__/theme-uninit.test.d.ts +0 -2
- package/dist/__tests__/theme-uninit.test.d.ts.map +0 -1
- package/dist/__tests__/theme-uninit.test.js +0 -10
- package/dist/__tests__/theme-uninit.test.js.map +0 -1
- package/dist/__tests__/token-sheet.test.d.ts +0 -2
- package/dist/__tests__/token-sheet.test.d.ts.map +0 -1
- package/dist/__tests__/token-sheet.test.js +0 -74
- package/dist/__tests__/token-sheet.test.js.map +0 -1
- package/src/__tests__/css-builder.test.ts +0 -230
- package/src/__tests__/cx.test.ts +0 -63
- package/src/__tests__/decorators.test.ts +0 -127
- package/src/__tests__/extract.test.ts +0 -459
- package/src/__tests__/global.test.ts +0 -163
- package/src/__tests__/keyframes.test.ts +0 -103
- package/src/__tests__/preflight.test.ts +0 -85
- package/src/__tests__/registry.test.ts +0 -151
- package/src/__tests__/style.test.ts +0 -170
- package/src/__tests__/styled.test.ts +0 -443
- package/src/__tests__/theme-instance.test.ts +0 -374
- package/src/__tests__/theme-uninit.test.ts +0 -12
- package/src/__tests__/token-sheet.test.ts +0 -87
- package/tsconfig.json +0 -8
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { extractionModule } from "../extract";
|
|
3
|
-
import { Stylesheet, ReactiveStylesheet } from "../base/stylesheet";
|
|
4
|
-
import { createCSSBuilder } from "../builder/css-builder";
|
|
5
|
-
|
|
6
|
-
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
type Mod = ReturnType<typeof extractionModule>;
|
|
9
|
-
|
|
10
|
-
function makeCollector() {
|
|
11
|
-
const emitted: Array<{ css: string; prepend?: boolean }> = [];
|
|
12
|
-
const mod = extractionModule((css, prepend) => emitted.push({ css, prepend }));
|
|
13
|
-
return { mod, emitted };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ─── API shape ────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
describe("extractionModule() — API shape", () => {
|
|
19
|
-
it("returns an object with __esModule: true", () => {
|
|
20
|
-
const mod = extractionModule(() => {});
|
|
21
|
-
expect(mod.__esModule).toBe(true);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("Stylesheet is the real base class — subclasses can use this.css()", () => {
|
|
25
|
-
const { mod } = makeCollector();
|
|
26
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
27
|
-
expect(S).toBe(Stylesheet);
|
|
28
|
-
class BtnStyles extends S {
|
|
29
|
-
$base = this.css({ display: "flex" });
|
|
30
|
-
}
|
|
31
|
-
expect(new BtnStyles()).toBeInstanceOf(S);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("ReactiveStylesheet is the real base class", () => {
|
|
35
|
-
const { mod } = makeCollector();
|
|
36
|
-
const RS = (mod as Mod & { ReactiveStylesheet: typeof ReactiveStylesheet }).ReactiveStylesheet;
|
|
37
|
-
expect(RS).toBe(ReactiveStylesheet);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("createCSSBuilder is the real function", () => {
|
|
41
|
-
const { mod } = makeCollector();
|
|
42
|
-
expect(mod.createCSSBuilder).toBe(createCSSBuilder);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("cx() returns an empty string", () => {
|
|
46
|
-
const { mod } = makeCollector();
|
|
47
|
-
expect((mod.cx as () => string)()).toBe("");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("Param() returns a decorator function", () => {
|
|
51
|
-
const { mod } = makeCollector();
|
|
52
|
-
const result = (mod.Param as () => unknown)();
|
|
53
|
-
expect(typeof result).toBe("function");
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("Style() returns a decorator function", () => {
|
|
57
|
-
const { mod } = makeCollector();
|
|
58
|
-
const result = (mod.Style as () => unknown)();
|
|
59
|
-
expect(typeof result).toBe("function");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("Theme() returns a decorator function", () => {
|
|
63
|
-
const { mod } = makeCollector();
|
|
64
|
-
const result = (mod.Theme as () => unknown)();
|
|
65
|
-
expect(typeof result).toBe("function");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("tokenVars() returns its argument unchanged", () => {
|
|
69
|
-
const { mod } = makeCollector();
|
|
70
|
-
const cls = class {};
|
|
71
|
-
expect((mod.tokenVars as (c: unknown) => unknown)(cls)).toBe(cls);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("TokenSheet is a class (can be instantiated)", () => {
|
|
75
|
-
const { mod } = makeCollector();
|
|
76
|
-
const TS = mod.TokenSheet as new () => unknown;
|
|
77
|
-
expect(new TS()).toBeDefined();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("TokenSheet is the real Proxy — static access on a subclass resolves to var(--kebab-case)", () => {
|
|
81
|
-
const { mod } = makeCollector();
|
|
82
|
-
const TS = mod.TokenSheet as new () => object;
|
|
83
|
-
class AppTokens extends TS {
|
|
84
|
-
colorPrimary!: string;
|
|
85
|
-
}
|
|
86
|
-
expect((AppTokens as unknown as Record<string, string>).colorPrimary).toBe("var(--color-primary)");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("tokenVars() combined with a TokenSheet subclass resolves real CSS var references", () => {
|
|
90
|
-
const { mod } = makeCollector();
|
|
91
|
-
const TS = mod.TokenSheet as new () => object;
|
|
92
|
-
class AppTokens extends TS {
|
|
93
|
-
radius!: string;
|
|
94
|
-
}
|
|
95
|
-
const t = (mod.tokenVars as (c: unknown) => Record<string, string>)(AppTokens);
|
|
96
|
-
expect(t.radius).toBe("var(--radius)");
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ─── Styled() ─────────────────────────────────────────────────────────────────
|
|
101
|
-
|
|
102
|
-
describe("extractionModule() — Styled()", () => {
|
|
103
|
-
it("collects scoped CSS for each $-prefixed field", () => {
|
|
104
|
-
const { mod, emitted } = makeCollector();
|
|
105
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
106
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
107
|
-
|
|
108
|
-
class CardStyles extends S {
|
|
109
|
-
$root = this.css({ display: "flex", gap: "8px" });
|
|
110
|
-
$title = this.css({ fontSize: "1rem", fontWeight: 600 });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
Styled(CardStyles);
|
|
114
|
-
expect(emitted).toHaveLength(1);
|
|
115
|
-
expect(emitted[0].css).toContain(".prx-root-");
|
|
116
|
-
expect(emitted[0].css).toContain(".prx-title-");
|
|
117
|
-
expect(emitted[0].css).toContain("display: flex");
|
|
118
|
-
expect(emitted[0].css).toContain("font-size: 1rem");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("returns a decorator function", () => {
|
|
122
|
-
const { mod } = makeCollector();
|
|
123
|
-
const result = (mod.Styled as (cls: new () => unknown) => unknown)(class {});
|
|
124
|
-
expect(typeof result).toBe("function");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("deduplicates identical sheets — emits only once", () => {
|
|
128
|
-
const { mod, emitted } = makeCollector();
|
|
129
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
130
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
131
|
-
class BtnStyles extends S { $base = this.css({ display: "flex" }); }
|
|
132
|
-
|
|
133
|
-
Styled(BtnStyles);
|
|
134
|
-
Styled(BtnStyles);
|
|
135
|
-
expect(emitted).toHaveLength(1);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("ignores fields that don't start with '$'", () => {
|
|
139
|
-
const { mod, emitted } = makeCollector();
|
|
140
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
141
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
142
|
-
class MixedStyles extends S {
|
|
143
|
-
hidden = "ignored";
|
|
144
|
-
$visible = this.css({ color: "red" });
|
|
145
|
-
}
|
|
146
|
-
Styled(MixedStyles);
|
|
147
|
-
expect(emitted[0].css).not.toContain("ignored");
|
|
148
|
-
expect(emitted[0].css).toContain("color: red");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("ignores $-prefixed fields that are neither strings nor builders", () => {
|
|
152
|
-
const { mod, emitted } = makeCollector();
|
|
153
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
154
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
155
|
-
class WeirdStyles extends S {
|
|
156
|
-
$valid = this.css({ display: "flex" });
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
158
|
-
$invalid = 42 as any;
|
|
159
|
-
}
|
|
160
|
-
Styled(WeirdStyles);
|
|
161
|
-
expect(emitted[0].css).not.toContain("42");
|
|
162
|
-
expect(emitted[0].css).toContain("display: flex");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("handles $-prefixed string fields (raw CSS string)", () => {
|
|
166
|
-
const { mod, emitted } = makeCollector();
|
|
167
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
168
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
169
|
-
class RawStyles extends S {
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
-
$custom = "color: red; font-size: 14px;" as any;
|
|
172
|
-
}
|
|
173
|
-
Styled(RawStyles);
|
|
174
|
-
expect(emitted[0].css).toContain("color: red");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("emits nothing if the sheet has no $-prefixed fields", () => {
|
|
178
|
-
const { mod, emitted } = makeCollector();
|
|
179
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
180
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
181
|
-
class EmptyStyles extends S {}
|
|
182
|
-
Styled(EmptyStyles);
|
|
183
|
-
expect(emitted).toHaveLength(0);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("collects CSS for a sheet whose values are derived from tokenVars()", () => {
|
|
187
|
-
const { mod, emitted } = makeCollector();
|
|
188
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
189
|
-
const TS = mod.TokenSheet as new () => object;
|
|
190
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
191
|
-
|
|
192
|
-
class AppTokens extends TS {
|
|
193
|
-
radius!: string;
|
|
194
|
-
primary!: string;
|
|
195
|
-
}
|
|
196
|
-
const t = (mod.tokenVars as (c: unknown) => Record<string, string>)(AppTokens);
|
|
197
|
-
|
|
198
|
-
class ButtonStyles extends S {
|
|
199
|
-
$root = this.css({ borderRadius: `calc(${t.radius} - 2px)`, backgroundColor: t.primary });
|
|
200
|
-
}
|
|
201
|
-
Styled(ButtonStyles);
|
|
202
|
-
|
|
203
|
-
expect(emitted).toHaveLength(1);
|
|
204
|
-
expect(emitted[0].css).toContain("calc(var(--radius) - 2px)");
|
|
205
|
-
expect(emitted[0].css).toContain("background-color: var(--primary)");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("silently skips sheets whose constructor throws", () => {
|
|
209
|
-
const { mod, emitted } = makeCollector();
|
|
210
|
-
const Styled = mod.Styled as (cls: new () => unknown) => unknown;
|
|
211
|
-
class BadStyles {
|
|
212
|
-
constructor() { throw new Error("oops"); }
|
|
213
|
-
}
|
|
214
|
-
expect(() => Styled(BadStyles)).not.toThrow();
|
|
215
|
-
expect(emitted).toHaveLength(0);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// ─── keyframes() ─────────────────────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
describe("extractionModule() — keyframes()", () => {
|
|
222
|
-
it("calls emit with an @keyframes block", () => {
|
|
223
|
-
const { mod, emitted } = makeCollector();
|
|
224
|
-
const kf = mod.keyframes as (n: string, s: Record<string, object>) => string;
|
|
225
|
-
kf("pulse", { from: { opacity: 1 }, to: { opacity: 0.4 } });
|
|
226
|
-
expect(emitted).toHaveLength(1);
|
|
227
|
-
expect(emitted[0].css).toContain("@keyframes prx-pulse-");
|
|
228
|
-
expect(emitted[0].css).toContain("opacity: 1;");
|
|
229
|
-
expect(emitted[0].css).toContain("opacity: 0.4;");
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("returns the scoped animation name", () => {
|
|
233
|
-
const { mod } = makeCollector();
|
|
234
|
-
const kf = mod.keyframes as (n: string, s: Record<string, object>) => string;
|
|
235
|
-
const name = kf("spin", { from: { transform: "rotate(0deg)" }, to: { transform: "rotate(360deg)" } });
|
|
236
|
-
expect(name).toMatch(/^prx-spin-/);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("deduplicates identical keyframes", () => {
|
|
240
|
-
const { mod, emitted } = makeCollector();
|
|
241
|
-
const kf = mod.keyframes as (n: string, s: Record<string, object>) => string;
|
|
242
|
-
const stops = { from: { opacity: 0 }, to: { opacity: 1 } };
|
|
243
|
-
kf("fade", stops);
|
|
244
|
-
kf("fade", stops);
|
|
245
|
-
expect(emitted).toHaveLength(1);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("converts camelCase properties to kebab-case", () => {
|
|
249
|
-
const { mod, emitted } = makeCollector();
|
|
250
|
-
const kf = mod.keyframes as (n: string, s: Record<string, object>) => string;
|
|
251
|
-
kf("move", { from: { marginTop: "0px" }, to: { marginTop: "20px" } });
|
|
252
|
-
expect(emitted[0].css).toContain("margin-top:");
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// ─── globalStyle() ───────────────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
describe("extractionModule() — globalStyle()", () => {
|
|
259
|
-
type GSFactory = (factory: (_css: unknown) => string) => void;
|
|
260
|
-
|
|
261
|
-
it("calls emit with the CSS returned by the factory", () => {
|
|
262
|
-
const { mod, emitted } = makeCollector();
|
|
263
|
-
(mod.globalStyle as GSFactory)(_css => "body { margin: 0; }");
|
|
264
|
-
expect(emitted).toHaveLength(1);
|
|
265
|
-
expect(emitted[0].css).toBe("body { margin: 0; }");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("deduplicates when factory produces the same CSS twice", () => {
|
|
269
|
-
const { mod, emitted } = makeCollector();
|
|
270
|
-
const gs = mod.globalStyle as GSFactory;
|
|
271
|
-
gs(_css => "body { margin: 0; }");
|
|
272
|
-
gs(_css => "body { margin: 0; }");
|
|
273
|
-
expect(emitted).toHaveLength(1);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("does not set prepend for globalStyle", () => {
|
|
277
|
-
const { mod, emitted } = makeCollector();
|
|
278
|
-
(mod.globalStyle as GSFactory)(_css => "html { box-sizing: border-box; }");
|
|
279
|
-
expect(emitted[0].prepend).toBeFalsy();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("factory can use the builder to produce CSS via .on()", () => {
|
|
283
|
-
const { mod, emitted } = makeCollector();
|
|
284
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
285
|
-
(mod.globalStyle as (f: (css: any) => unknown) => void)(
|
|
286
|
-
css => css({}).on("body", { margin: 0 }),
|
|
287
|
-
);
|
|
288
|
-
expect(emitted[0].css).toContain("body");
|
|
289
|
-
expect(emitted[0].css).toContain("margin: 0");
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// ─── Themed() ────────────────────────────────────────────────────────────────
|
|
294
|
-
|
|
295
|
-
describe("extractionModule() — Themed()", () => {
|
|
296
|
-
it("emits :root vars for the default theme", () => {
|
|
297
|
-
const { mod, emitted } = makeCollector();
|
|
298
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
299
|
-
|
|
300
|
-
class MyTheme {
|
|
301
|
-
colorBrand = "#3b82f6";
|
|
302
|
-
spaceMd = "16px";
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
Themed(null, MyTheme);
|
|
306
|
-
expect(emitted).toHaveLength(1);
|
|
307
|
-
expect(emitted[0].css).toContain(":root");
|
|
308
|
-
expect(emitted[0].css).toContain("--color-brand: #3b82f6");
|
|
309
|
-
expect(emitted[0].css).toContain("--space-md: 16px");
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("passes prepend=true so :root vars appear before class rules", () => {
|
|
313
|
-
const { mod, emitted } = makeCollector();
|
|
314
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
315
|
-
Themed(null, class { colorBrand = "#fff"; });
|
|
316
|
-
expect(emitted[0].prepend).toBe(true);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("returns a function that passes through the decorated class", () => {
|
|
320
|
-
const { mod } = makeCollector();
|
|
321
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
322
|
-
const result = Themed(null, class {});
|
|
323
|
-
expect(typeof result).toBe("function");
|
|
324
|
-
const cls = class {};
|
|
325
|
-
expect(result(cls)).toBe(cls);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("skips non-string and non-number theme properties", () => {
|
|
329
|
-
const { mod, emitted } = makeCollector();
|
|
330
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
331
|
-
class MyTheme {
|
|
332
|
-
colorText = "#111";
|
|
333
|
-
nested = { x: 1 };
|
|
334
|
-
fn = () => {};
|
|
335
|
-
}
|
|
336
|
-
Themed(null, MyTheme);
|
|
337
|
-
expect(emitted[0].css).toContain("--color-text: #111");
|
|
338
|
-
expect(emitted[0].css).not.toContain("--nested");
|
|
339
|
-
expect(emitted[0].css).not.toContain("--fn");
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("emits nothing when theme constructor throws", () => {
|
|
343
|
-
const { mod, emitted } = makeCollector();
|
|
344
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
345
|
-
class BadTheme { constructor() { throw new Error("oops"); } }
|
|
346
|
-
expect(() => Themed(null, BadTheme)).not.toThrow();
|
|
347
|
-
expect(emitted).toHaveLength(0);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("deduplicates identical themes", () => {
|
|
351
|
-
const { mod, emitted } = makeCollector();
|
|
352
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => (c: unknown) => unknown;
|
|
353
|
-
class MyTheme { colorBrand = "#3b82f6"; }
|
|
354
|
-
Themed(null, MyTheme);
|
|
355
|
-
Themed(null, MyTheme);
|
|
356
|
-
expect(emitted).toHaveLength(1);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// ─── ThemeInstance ────────────────────────────────────────────────────────────
|
|
361
|
-
|
|
362
|
-
describe("extractionModule() — ThemeInstance", () => {
|
|
363
|
-
it("constructor emits :root vars for the default theme", () => {
|
|
364
|
-
const { mod, emitted } = makeCollector();
|
|
365
|
-
const TI = mod.ThemeInstance as new (T: new () => unknown) => { switch(T: new () => unknown): void; destroy(): void };
|
|
366
|
-
class LightTheme { colorBrand = "#3b82f6"; }
|
|
367
|
-
new TI(LightTheme);
|
|
368
|
-
expect(emitted[0].css).toContain(":root");
|
|
369
|
-
expect(emitted[0].css).toContain("--color-brand: #3b82f6");
|
|
370
|
-
expect(emitted[0].prepend).toBe(true);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it("deduplicates with Themed() — same theme only emitted once", () => {
|
|
374
|
-
const { mod, emitted } = makeCollector();
|
|
375
|
-
const TI = mod.ThemeInstance as new (T: new () => unknown) => unknown;
|
|
376
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => unknown;
|
|
377
|
-
class MyTheme { colorBrand = "#3b82f6"; }
|
|
378
|
-
|
|
379
|
-
Themed(null, MyTheme);
|
|
380
|
-
new TI(MyTheme);
|
|
381
|
-
expect(emitted).toHaveLength(1);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("current getter returns an empty object", () => {
|
|
385
|
-
const { mod } = makeCollector();
|
|
386
|
-
const TI = mod.ThemeInstance as new (T: new () => unknown) => { current: unknown };
|
|
387
|
-
const inst = new TI(class {});
|
|
388
|
-
expect(inst.current).toEqual({});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("createThemeInstance returns a ThemeInstance-like object", () => {
|
|
392
|
-
const { mod, emitted } = makeCollector();
|
|
393
|
-
const cti = mod.createThemeInstance as (T: new () => unknown) => unknown;
|
|
394
|
-
class MyTheme { colorBrand = "#111"; }
|
|
395
|
-
cti(MyTheme);
|
|
396
|
-
expect(emitted[0].css).toContain("--color-brand: #111");
|
|
397
|
-
});
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
// ─── Dedup across APIs ────────────────────────────────────────────────────────
|
|
401
|
-
|
|
402
|
-
describe("extractionModule() — cross-API deduplication", () => {
|
|
403
|
-
it("Themed() and ThemeInstance share the same seen set", () => {
|
|
404
|
-
const { mod, emitted } = makeCollector();
|
|
405
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => unknown;
|
|
406
|
-
const TI = mod.ThemeInstance as new (T: new () => unknown) => unknown;
|
|
407
|
-
class SharedTheme { colorBrand = "#f00"; }
|
|
408
|
-
|
|
409
|
-
Themed(null, SharedTheme);
|
|
410
|
-
new TI(SharedTheme);
|
|
411
|
-
// Same CSS hash — should only appear once
|
|
412
|
-
expect(emitted.filter(e => e.css.includes("--color-brand")).length).toBe(1);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// ─── Additional coverage ──────────────────────────────────────────────────────
|
|
417
|
-
|
|
418
|
-
describe("extractionModule() — additional coverage", () => {
|
|
419
|
-
it("the decorator returned by Styled() is callable", () => {
|
|
420
|
-
const { mod } = makeCollector();
|
|
421
|
-
const S = (mod as Mod & { Stylesheet: typeof Stylesheet }).Stylesheet;
|
|
422
|
-
class TestStyles extends S { $x = this.css({ color: "red" }); }
|
|
423
|
-
const Styled = mod.Styled as (cls: new () => unknown) => (target: unknown) => void;
|
|
424
|
-
const dec = Styled(TestStyles);
|
|
425
|
-
expect(() => dec(class {})).not.toThrow();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("theme() returns an ExtractionThemeInstance", () => {
|
|
429
|
-
const { mod } = makeCollector();
|
|
430
|
-
const t = (mod.theme as () => { current: unknown })();
|
|
431
|
-
expect(t).toBeDefined();
|
|
432
|
-
expect(t.current).toEqual({});
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it("ExtractionThemeInstance.switch() and destroy() are no-ops", () => {
|
|
436
|
-
const { mod } = makeCollector();
|
|
437
|
-
const TI = mod.ThemeInstance as new (T: new () => unknown) => { switch(T: new () => unknown): void; destroy(): void };
|
|
438
|
-
const inst = new TI(class {});
|
|
439
|
-
expect(() => inst.switch(class {})).not.toThrow();
|
|
440
|
-
expect(() => inst.destroy()).not.toThrow();
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("themeToRootCSS returns null for a theme with no valid properties", () => {
|
|
444
|
-
const { mod, emitted } = makeCollector();
|
|
445
|
-
const Themed = mod.Themed as (s: unknown, T: new () => unknown) => unknown;
|
|
446
|
-
class EmptyTheme { nested = { x: 1 }; }
|
|
447
|
-
Themed(null, EmptyTheme);
|
|
448
|
-
expect(emitted).toHaveLength(0);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it("the inner decorator functions returned by Param/Style/Theme are callable no-ops", () => {
|
|
452
|
-
const { mod } = makeCollector();
|
|
453
|
-
type Maker = () => (...args: unknown[]) => void;
|
|
454
|
-
const callInner = (fn: Maker) => { const dec = fn(); expect(() => dec()).not.toThrow(); };
|
|
455
|
-
callInner(mod.Param as Maker);
|
|
456
|
-
callInner(mod.Style as Maker);
|
|
457
|
-
callInner(mod.Theme as Maker);
|
|
458
|
-
});
|
|
459
|
-
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { globalStyle } from "../global";
|
|
4
|
-
import { createCollector, setCollector } from "../internal/collector";
|
|
5
|
-
|
|
6
|
-
let seed = 0;
|
|
7
|
-
// Injects a unique comment so the content hash differs per test call.
|
|
8
|
-
function unique(css: string): string {
|
|
9
|
-
return css.replace("{", `/* t${++seed} */ {`);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
document.head.querySelectorAll("style[data-praxis-global]").forEach((el) => el.remove());
|
|
14
|
-
setCollector(null);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
document.head.querySelectorAll("style[data-praxis-global]").forEach((el) => el.remove());
|
|
19
|
-
setCollector(null);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("globalStyle()", () => {
|
|
23
|
-
it("injects an unscoped style element into document.head", () => {
|
|
24
|
-
globalStyle(_css => unique("body { margin: 0; }"));
|
|
25
|
-
const el = document.head.querySelector("style[data-praxis-global]");
|
|
26
|
-
expect(el).not.toBeNull();
|
|
27
|
-
expect(el?.textContent).toContain("margin: 0;");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("is idempotent — injects the same CSS only once", () => {
|
|
31
|
-
const css = unique("*, *::before, *::after { box-sizing: border-box; }");
|
|
32
|
-
globalStyle(_css => css);
|
|
33
|
-
globalStyle(_css => css);
|
|
34
|
-
expect(document.head.querySelectorAll("style[data-praxis-global]").length).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("injects separate elements for distinct CSS strings", () => {
|
|
38
|
-
globalStyle(_css => unique("body { margin: 0; }"));
|
|
39
|
-
globalStyle(_css => unique("html { font-size: 16px; }"));
|
|
40
|
-
expect(document.head.querySelectorAll("style[data-praxis-global]").length).toBe(2);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("routes to the collector when one is active", () => {
|
|
44
|
-
const col = createCollector();
|
|
45
|
-
setCollector(col);
|
|
46
|
-
globalStyle(_css => unique("body { margin: 0; }"));
|
|
47
|
-
expect(document.head.querySelector("style[data-praxis-global]")).toBeNull();
|
|
48
|
-
expect(col.getCSS()).toContain("margin: 0;");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("deduplicates in the collector", () => {
|
|
52
|
-
const col = createCollector();
|
|
53
|
-
setCollector(col);
|
|
54
|
-
const css = unique("*, *::before, *::after { box-sizing: border-box; }");
|
|
55
|
-
globalStyle(_css => css);
|
|
56
|
-
globalStyle(_css => css);
|
|
57
|
-
expect(col.getCSS().split("box-sizing").length - 1).toBe(1);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// ─── Factory — builder form ───────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
describe("globalStyle() — factory with builder", () => {
|
|
64
|
-
it("factory receives css (= createCSSBuilder) and uses the fluent builder", () => {
|
|
65
|
-
globalStyle(css =>
|
|
66
|
-
css({})
|
|
67
|
-
.on(unique("body { }").replace(" { }", ""), { margin: 0, fontFamily: "system-ui" })
|
|
68
|
-
);
|
|
69
|
-
const content = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
70
|
-
expect(content).toContain("font-family: system-ui");
|
|
71
|
-
expect(content).toContain("margin: 0");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("chains multiple .on() calls to produce multiple selectors in one injection", () => {
|
|
75
|
-
// Use unique values inside properties so the overall CSS hash is unique per test
|
|
76
|
-
const lh = `1.${seed + 1}`;
|
|
77
|
-
globalStyle(css =>
|
|
78
|
-
css({})
|
|
79
|
-
.on(unique("* { }").replace(" { }", ""), { boxSizing: "border-box" })
|
|
80
|
-
.on("html", { lineHeight: lh })
|
|
81
|
-
);
|
|
82
|
-
const content = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
83
|
-
expect(content).toContain("box-sizing: border-box");
|
|
84
|
-
expect(content).toContain("line-height:");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("factory returning a raw string is accepted", () => {
|
|
88
|
-
const css = unique("button { cursor: pointer; }");
|
|
89
|
-
globalStyle(_css => css);
|
|
90
|
-
expect(document.head.querySelector("style[data-praxis-global]")?.textContent).toContain("cursor: pointer");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("deduplicates when factory produces the same CSS twice", () => {
|
|
94
|
-
const css = unique("a { color: inherit; }");
|
|
95
|
-
globalStyle(_css => css);
|
|
96
|
-
globalStyle(_css => css);
|
|
97
|
-
expect(document.head.querySelectorAll("style[data-praxis-global]").length).toBe(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("routes through the collector when one is active", () => {
|
|
101
|
-
const col = createCollector();
|
|
102
|
-
setCollector(col);
|
|
103
|
-
globalStyle(_css => unique("a { text-decoration: none; }"));
|
|
104
|
-
expect(document.head.querySelector("style[data-praxis-global]")).toBeNull();
|
|
105
|
-
expect(col.getCSS()).toContain("text-decoration: none");
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// ─── layer option ─────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
describe("globalStyle() — layer option", () => {
|
|
112
|
-
it("injects un-layered CSS by default", () => {
|
|
113
|
-
globalStyle(_css => unique("body { margin: 0; }"));
|
|
114
|
-
const content = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
115
|
-
expect(content).not.toContain("@layer");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("wraps the CSS in a named @layer when provided", () => {
|
|
119
|
-
globalStyle(_css => unique("body { margin: 0; }"), { layer: "base" });
|
|
120
|
-
const content = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
121
|
-
expect(content).toContain("@layer base {");
|
|
122
|
-
expect(content).toContain("margin: 0;");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("does not wrap in a layer when layer: false", () => {
|
|
126
|
-
globalStyle(_css => unique("body { margin: 0; }"), { layer: false });
|
|
127
|
-
const content = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
128
|
-
expect(content).not.toContain("@layer");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// ─── STATIC_MODE ──────────────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
describe("globalStyle() — static mode (__PRAXIS_CSS_STATIC__)", () => {
|
|
135
|
-
afterEach(() => {
|
|
136
|
-
vi.unstubAllGlobals();
|
|
137
|
-
vi.resetModules();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("skips DOM injection when __PRAXIS_CSS_STATIC__ is true", async () => {
|
|
141
|
-
vi.stubGlobal("__PRAXIS_CSS_STATIC__", true);
|
|
142
|
-
vi.resetModules();
|
|
143
|
-
const { globalStyle: staticGS } = await import("../global");
|
|
144
|
-
staticGS(_css => unique("body { margin: 0; }"));
|
|
145
|
-
expect(document.head.querySelector("style[data-praxis-global]")).toBeNull();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ─── SSR (document === undefined) ────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
describe("globalStyle() — SSR (document undefined)", () => {
|
|
152
|
-
afterEach(() => {
|
|
153
|
-
vi.unstubAllGlobals();
|
|
154
|
-
vi.resetModules();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("does not throw when document is undefined", async () => {
|
|
158
|
-
vi.stubGlobal("document", undefined);
|
|
159
|
-
vi.resetModules();
|
|
160
|
-
const { globalStyle: ssrGS } = await import("../global");
|
|
161
|
-
expect(() => ssrGS(_css => unique("p { color: red; }"))).not.toThrow();
|
|
162
|
-
});
|
|
163
|
-
});
|