@praxisjs/css 0.3.3 → 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 +31 -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,103 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { keyframes } from "../builder/keyframes";
|
|
4
|
-
import { createCollector, setCollector } from "../internal/collector";
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
document.head.querySelectorAll("style[data-praxis-keyframes]").forEach((el) => el.remove());
|
|
8
|
-
setCollector(null);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
document.head.querySelectorAll("style[data-praxis-keyframes]").forEach((el) => el.remove());
|
|
13
|
-
setCollector(null);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe("keyframes()", () => {
|
|
17
|
-
it("injects a @keyframes style element into document.head", () => {
|
|
18
|
-
keyframes("pulse", { from: { opacity: 1 }, to: { opacity: 0.4 } });
|
|
19
|
-
const el = document.head.querySelector("style[data-praxis-keyframes]");
|
|
20
|
-
expect(el).not.toBeNull();
|
|
21
|
-
expect(el?.textContent).toContain("@keyframes prx-pulse-");
|
|
22
|
-
expect(el?.textContent).toContain("from");
|
|
23
|
-
expect(el?.textContent).toContain("opacity: 1;");
|
|
24
|
-
expect(el?.textContent).toContain("opacity: 0.4;");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("returns a scoped animation name prefixed with prx-<name>-", () => {
|
|
28
|
-
const name = keyframes("spin", { from: { transform: "rotate(0deg)" }, to: { transform: "rotate(360deg)" } });
|
|
29
|
-
expect(name).toMatch(/^prx-spin-/);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("injects only once for identical keyframes (content-hashed)", () => {
|
|
33
|
-
const stops = { from: { opacity: 1 }, to: { opacity: 0 } };
|
|
34
|
-
keyframes("fade", stops);
|
|
35
|
-
keyframes("fade", stops);
|
|
36
|
-
expect(document.head.querySelectorAll("style[data-praxis-keyframes]").length).toBe(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns the same scoped name for identical frames", () => {
|
|
40
|
-
const stops = { from: { opacity: 0 }, to: { opacity: 1 } };
|
|
41
|
-
const a = keyframes("fadeIn", stops);
|
|
42
|
-
const b = keyframes("fadeIn", stops);
|
|
43
|
-
expect(a).toBe(b);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("returns different names for different frames even with same animation name", () => {
|
|
47
|
-
const a = keyframes("anim", { from: { opacity: 0 }, to: { opacity: 1 } });
|
|
48
|
-
const b = keyframes("anim", { from: { opacity: 1 }, to: { opacity: 0 } });
|
|
49
|
-
expect(a).not.toBe(b);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("converts camelCase frame properties to kebab-case in the output", () => {
|
|
53
|
-
keyframes("slide", { from: { translateX: "0" }, to: { translateX: "100px" } } as Parameters<typeof keyframes>[1]);
|
|
54
|
-
const el = document.head.querySelector("style[data-praxis-keyframes]");
|
|
55
|
-
expect(el?.textContent).toContain("translate-x:");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("supports percentage stops", () => {
|
|
59
|
-
keyframes("bounce", { "0%": { transform: "scale(1)" }, "50%": { transform: "scale(1.1)" }, "100%": { transform: "scale(1)" } });
|
|
60
|
-
const el = document.head.querySelector("style[data-praxis-keyframes]");
|
|
61
|
-
expect(el?.textContent).toContain("0%");
|
|
62
|
-
expect(el?.textContent).toContain("50%");
|
|
63
|
-
expect(el?.textContent).toContain("100%");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("keyframes — collector mode", () => {
|
|
68
|
-
it("routes to collector when one is active (no DOM injection)", () => {
|
|
69
|
-
const col = createCollector();
|
|
70
|
-
setCollector(col);
|
|
71
|
-
const name = keyframes("float", { from: { opacity: 0 }, to: { opacity: 1 } });
|
|
72
|
-
expect(document.head.querySelector("style[data-praxis-keyframes]")).toBeNull();
|
|
73
|
-
expect(col.getCSS()).toContain(`@keyframes ${name}`);
|
|
74
|
-
expect(col.getCSS()).toContain("opacity: 0;");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("deduplicates identical keyframes in the collector", () => {
|
|
78
|
-
const col = createCollector();
|
|
79
|
-
setCollector(col);
|
|
80
|
-
const stops = { from: { opacity: 0 }, to: { opacity: 1 } };
|
|
81
|
-
keyframes("same", stops);
|
|
82
|
-
keyframes("same", stops);
|
|
83
|
-
expect(col.getCSS().split("@keyframes").length - 1).toBe(1);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// ─── STATIC_MODE ──────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
describe("keyframes() — static mode (__PRAXIS_CSS_STATIC__)", () => {
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
vi.unstubAllGlobals();
|
|
92
|
-
vi.resetModules();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("returns the scoped name without DOM injection when __PRAXIS_CSS_STATIC__ is true", async () => {
|
|
96
|
-
vi.stubGlobal("__PRAXIS_CSS_STATIC__", true);
|
|
97
|
-
vi.resetModules();
|
|
98
|
-
const { keyframes: staticKF } = await import("../builder/keyframes");
|
|
99
|
-
const name = staticKF("pulse", { from: { opacity: 1 }, to: { opacity: 0 } });
|
|
100
|
-
expect(name).toMatch(/^prx-pulse-/);
|
|
101
|
-
expect(document.head.querySelector("style[data-praxis-keyframes]")).toBeNull();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
// The preflight CSS is a fixed string, so globalStyle's module-level injected Set
|
|
5
|
-
// remembers it across tests. Reset modules in beforeEach to get a fresh Set each time.
|
|
6
|
-
|
|
7
|
-
function cleanup() {
|
|
8
|
-
document.head.querySelectorAll("style[data-praxis-global]").forEach((el) => el.remove());
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.resetModules();
|
|
13
|
-
cleanup();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
cleanup();
|
|
18
|
-
vi.resetModules();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
describe("preflight()", () => {
|
|
22
|
-
it("injects a style element into document.head", async () => {
|
|
23
|
-
const { preflight } = await import("../preflight.js");
|
|
24
|
-
preflight();
|
|
25
|
-
expect(document.head.querySelector("style[data-praxis-global]")).not.toBeNull();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("contains core reset rules", async () => {
|
|
29
|
-
const { preflight } = await import("../preflight.js");
|
|
30
|
-
preflight();
|
|
31
|
-
const css = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
32
|
-
expect(css).toContain("box-sizing: border-box");
|
|
33
|
-
expect(css).toContain("margin: 0");
|
|
34
|
-
expect(css).toContain("padding: 0");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("uses standard font stacks without --theme() references", async () => {
|
|
38
|
-
const { preflight } = await import("../preflight.js");
|
|
39
|
-
preflight();
|
|
40
|
-
const css = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
41
|
-
expect(css).toContain("ui-sans-serif");
|
|
42
|
-
expect(css).toContain("ui-monospace");
|
|
43
|
-
expect(css).not.toContain("--theme(");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("is idempotent — calling twice injects only once", async () => {
|
|
47
|
-
const { preflight } = await import("../preflight.js");
|
|
48
|
-
preflight();
|
|
49
|
-
preflight();
|
|
50
|
-
expect(document.head.querySelectorAll("style[data-praxis-global]").length).toBe(1);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("routes through the collector when one is active", async () => {
|
|
54
|
-
const { setCollector, createCollector } = await import("../internal/collector.js");
|
|
55
|
-
const { preflight } = await import("../preflight.js");
|
|
56
|
-
const col = createCollector();
|
|
57
|
-
setCollector(col);
|
|
58
|
-
preflight();
|
|
59
|
-
setCollector(null);
|
|
60
|
-
expect(document.head.querySelector("style[data-praxis-global]")).toBeNull();
|
|
61
|
-
expect(col.getCSS()).toContain("box-sizing: border-box");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("wraps the reset in `@layer reset` by default", async () => {
|
|
65
|
-
const { preflight } = await import("../preflight.js");
|
|
66
|
-
preflight();
|
|
67
|
-
const css = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
68
|
-
expect(css).toContain("@layer reset {");
|
|
69
|
-
expect(css).toContain("box-sizing: border-box");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("wraps the reset in a custom named layer", async () => {
|
|
73
|
-
const { preflight } = await import("../preflight.js");
|
|
74
|
-
preflight({ layer: "tailwind-base" });
|
|
75
|
-
const css = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
76
|
-
expect(css).toContain("@layer tailwind-base {");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("injects un-layered CSS when layer: false", async () => {
|
|
80
|
-
const { preflight } = await import("../preflight.js");
|
|
81
|
-
preflight({ layer: false });
|
|
82
|
-
const css = document.head.querySelector("style[data-praxis-global]")?.textContent ?? "";
|
|
83
|
-
expect(css).not.toContain("@layer");
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { injectStyle, releaseStyle } from "../internal/registry";
|
|
4
|
-
import { createCollector, setCollector } from "../internal/collector";
|
|
5
|
-
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
document
|
|
8
|
-
.head
|
|
9
|
-
.querySelectorAll("style[data-praxis-hash]")
|
|
10
|
-
.forEach((el) => el.remove());
|
|
11
|
-
setCollector(null);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
document
|
|
16
|
-
.head
|
|
17
|
-
.querySelectorAll("style[data-praxis-hash]")
|
|
18
|
-
.forEach((el) => el.remove());
|
|
19
|
-
setCollector(null);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
describe("injectStyle / releaseStyle", () => {
|
|
23
|
-
it("injects a style element with the given key and css", () => {
|
|
24
|
-
injectStyle("abc", ".a { color: red; }");
|
|
25
|
-
const el = document.head.querySelector("style[data-praxis-hash='abc']");
|
|
26
|
-
expect(el).not.toBeNull();
|
|
27
|
-
expect(el?.textContent).toBe(".a { color: red; }");
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("reference-counts — increments count on duplicate key", () => {
|
|
31
|
-
injectStyle("k1", ".x { display: flex; }");
|
|
32
|
-
injectStyle("k1", ".x { display: flex; }");
|
|
33
|
-
expect(document.head.querySelectorAll("style[data-praxis-hash='k1']").length).toBe(1);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("removes the element when count reaches zero", () => {
|
|
37
|
-
injectStyle("k2", ".y { opacity: 1; }");
|
|
38
|
-
injectStyle("k2", ".y { opacity: 1; }");
|
|
39
|
-
releaseStyle("k2");
|
|
40
|
-
expect(document.head.querySelector("style[data-praxis-hash='k2']")).not.toBeNull();
|
|
41
|
-
releaseStyle("k2");
|
|
42
|
-
expect(document.head.querySelector("style[data-praxis-hash='k2']")).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("releaseStyle on non-existent key is a no-op", () => {
|
|
46
|
-
expect(() => releaseStyle("does-not-exist")).not.toThrow();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("re-injects when element was removed externally (DOM divergence guard)", () => {
|
|
50
|
-
injectStyle("k3", ".z { color: blue; }");
|
|
51
|
-
document.head.querySelector("style[data-praxis-hash='k3']")?.remove();
|
|
52
|
-
injectStyle("k3", ".z { color: blue; }");
|
|
53
|
-
expect(document.head.querySelector("style[data-praxis-hash='k3']")).not.toBeNull();
|
|
54
|
-
releaseStyle("k3");
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("collector mode", () => {
|
|
59
|
-
it("routes injectStyle to the collector when one is active", () => {
|
|
60
|
-
const col = createCollector();
|
|
61
|
-
setCollector(col);
|
|
62
|
-
injectStyle("hash1", ".cls { color: red; }");
|
|
63
|
-
expect(document.head.querySelector("style[data-praxis-hash]")).toBeNull();
|
|
64
|
-
expect(col.getCSS()).toBe(".cls { color: red; }");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("deduplicates identical keys in the collector", () => {
|
|
68
|
-
const col = createCollector();
|
|
69
|
-
setCollector(col);
|
|
70
|
-
injectStyle("dup", ".a { }");
|
|
71
|
-
injectStyle("dup", ".a { }");
|
|
72
|
-
expect(col.getCSS().split("\n").filter(Boolean).length).toBe(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("collector accumulates multiple distinct injections", () => {
|
|
76
|
-
const col = createCollector();
|
|
77
|
-
setCollector(col);
|
|
78
|
-
injectStyle("a", ".a { color: red; }");
|
|
79
|
-
injectStyle("b", ".b { color: blue; }");
|
|
80
|
-
const css = col.getCSS();
|
|
81
|
-
expect(css).toContain(".a { color: red; }");
|
|
82
|
-
expect(css).toContain(".b { color: blue; }");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("releaseStyle is a no-op when a collector is active", () => {
|
|
86
|
-
const col = createCollector();
|
|
87
|
-
setCollector(col);
|
|
88
|
-
injectStyle("x", ".x { }");
|
|
89
|
-
expect(() => releaseStyle("x")).not.toThrow();
|
|
90
|
-
expect(col.getCSS()).toContain(".x { }");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("clear() resets the collector", () => {
|
|
94
|
-
const col = createCollector();
|
|
95
|
-
setCollector(col);
|
|
96
|
-
injectStyle("c", ".c { }");
|
|
97
|
-
col.clear();
|
|
98
|
-
expect(col.getCSS()).toBe("");
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("DOM injection resumes after setCollector(null)", () => {
|
|
102
|
-
const col = createCollector();
|
|
103
|
-
setCollector(col);
|
|
104
|
-
injectStyle("pre", ".pre { }");
|
|
105
|
-
setCollector(null);
|
|
106
|
-
injectStyle("post", ".post { }");
|
|
107
|
-
expect(document.head.querySelector("style[data-praxis-hash='pre']")).toBeNull();
|
|
108
|
-
expect(document.head.querySelector("style[data-praxis-hash='post']")).not.toBeNull();
|
|
109
|
-
releaseStyle("post");
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ─── STATIC_MODE ──────────────────────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
describe("injectStyle / releaseStyle — static mode (__PRAXIS_CSS_STATIC__)", () => {
|
|
116
|
-
afterEach(() => {
|
|
117
|
-
vi.unstubAllGlobals();
|
|
118
|
-
vi.resetModules();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("injectStyle is a no-op when __PRAXIS_CSS_STATIC__ is true", async () => {
|
|
122
|
-
vi.stubGlobal("__PRAXIS_CSS_STATIC__", true);
|
|
123
|
-
vi.resetModules();
|
|
124
|
-
const { injectStyle: staticInject } = await import("../internal/registry");
|
|
125
|
-
staticInject("sm1", ".a { color: red; }");
|
|
126
|
-
expect(document.head.querySelector("style[data-praxis-hash='sm1']")).toBeNull();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("releaseStyle is a no-op when __PRAXIS_CSS_STATIC__ is true", async () => {
|
|
130
|
-
vi.stubGlobal("__PRAXIS_CSS_STATIC__", true);
|
|
131
|
-
vi.resetModules();
|
|
132
|
-
const { releaseStyle: staticRelease } = await import("../internal/registry");
|
|
133
|
-
expect(() => staticRelease("sm2")).not.toThrow();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// ─── SSR (document === undefined) ────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
describe("injectStyle — SSR (document undefined)", () => {
|
|
140
|
-
afterEach(() => {
|
|
141
|
-
vi.unstubAllGlobals();
|
|
142
|
-
vi.resetModules();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("does not throw when document is undefined", async () => {
|
|
146
|
-
vi.stubGlobal("document", undefined);
|
|
147
|
-
vi.resetModules();
|
|
148
|
-
const { injectStyle: ssrInject } = await import("../internal/registry");
|
|
149
|
-
expect(() => ssrInject("ssr1", ".a { }")).not.toThrow();
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
-
import { setComponentAnchor } from "@praxisjs/core/internal";
|
|
5
|
-
import { Style } from "../decorators/style";
|
|
6
|
-
|
|
7
|
-
let container: HTMLDivElement;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
container = document.createElement("div");
|
|
11
|
-
document.body.appendChild(container);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
container.remove();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
/** Attaches an anchor comment to the element, simulating what the runtime does. */
|
|
19
|
-
function makeAnchor(parent: HTMLElement): Comment {
|
|
20
|
-
const anchor = document.createComment("[/Comp]");
|
|
21
|
-
parent.appendChild(anchor);
|
|
22
|
-
return anchor;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type WithLifecycle = { onMount?(): void; onUnmount?(): void };
|
|
26
|
-
|
|
27
|
-
abstract class AnchoredComponent extends StatefulComponent {
|
|
28
|
-
constructor(anchor?: Comment) {
|
|
29
|
-
super();
|
|
30
|
-
setComponentAnchor(this, anchor);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe("@Style field decorator", () => {
|
|
35
|
-
it("sets a CSS custom property on the container element after onMount", () => {
|
|
36
|
-
const anchor = makeAnchor(container);
|
|
37
|
-
|
|
38
|
-
class Comp extends AnchoredComponent {
|
|
39
|
-
constructor() { super(anchor); }
|
|
40
|
-
@Style("--accent")
|
|
41
|
-
accent = "#3b82f6";
|
|
42
|
-
render() { return null; }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const inst = new Comp() as WithLifecycle;
|
|
46
|
-
inst.onMount?.();
|
|
47
|
-
|
|
48
|
-
expect(container.style.getPropertyValue("--accent")).toBe("#3b82f6");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("updates CSS custom property reactively when field value changes", () => {
|
|
52
|
-
const anchor = makeAnchor(container);
|
|
53
|
-
|
|
54
|
-
class Comp extends AnchoredComponent {
|
|
55
|
-
constructor() { super(anchor); }
|
|
56
|
-
@Style("--color")
|
|
57
|
-
color = "red";
|
|
58
|
-
render() { return null; }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const inst = new Comp() as WithLifecycle & { color: string };
|
|
62
|
-
inst.onMount?.();
|
|
63
|
-
expect(container.style.getPropertyValue("--color")).toBe("red");
|
|
64
|
-
|
|
65
|
-
inst.color = "blue";
|
|
66
|
-
expect(container.style.getPropertyValue("--color")).toBe("blue");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("removes the CSS custom property on unmount", () => {
|
|
70
|
-
const anchor = makeAnchor(container);
|
|
71
|
-
|
|
72
|
-
class Comp extends AnchoredComponent {
|
|
73
|
-
constructor() { super(anchor); }
|
|
74
|
-
@Style("--accent")
|
|
75
|
-
accent = "red";
|
|
76
|
-
render() { return null; }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const inst = new Comp() as WithLifecycle;
|
|
80
|
-
inst.onMount?.();
|
|
81
|
-
expect(container.style.getPropertyValue("--accent")).toBe("red");
|
|
82
|
-
|
|
83
|
-
inst.onUnmount?.();
|
|
84
|
-
expect(container.style.getPropertyValue("--accent")).toBe("");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("does not throw when anchor has no parentElement", () => {
|
|
88
|
-
const anchor = document.createComment("[/Comp]");
|
|
89
|
-
|
|
90
|
-
class Comp extends AnchoredComponent {
|
|
91
|
-
constructor() { super(anchor); }
|
|
92
|
-
@Style("--color")
|
|
93
|
-
color = "red";
|
|
94
|
-
render() { return null; }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const inst = new Comp() as WithLifecycle;
|
|
98
|
-
expect(() => inst.onMount?.()).not.toThrow();
|
|
99
|
-
expect(() => inst.onUnmount?.()).not.toThrow();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("stops the reactive effect after onUnmount — further changes are silent", () => {
|
|
103
|
-
const anchor = makeAnchor(container);
|
|
104
|
-
|
|
105
|
-
class Comp extends AnchoredComponent {
|
|
106
|
-
constructor() { super(anchor); }
|
|
107
|
-
@Style("--val")
|
|
108
|
-
val = "a";
|
|
109
|
-
render() { return null; }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const inst = new Comp() as WithLifecycle & { val: string };
|
|
113
|
-
inst.onMount?.();
|
|
114
|
-
expect(container.style.getPropertyValue("--val")).toBe("a");
|
|
115
|
-
|
|
116
|
-
inst.onUnmount?.();
|
|
117
|
-
inst.val = "b";
|
|
118
|
-
expect(container.style.getPropertyValue("--val")).toBe("");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("supports numeric values (converted to string)", () => {
|
|
122
|
-
const anchor = makeAnchor(container);
|
|
123
|
-
|
|
124
|
-
class Comp extends AnchoredComponent {
|
|
125
|
-
constructor() { super(anchor); }
|
|
126
|
-
@Style("--size")
|
|
127
|
-
size = 16;
|
|
128
|
-
render() { return null; }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const inst = new Comp() as WithLifecycle;
|
|
132
|
-
inst.onMount?.();
|
|
133
|
-
expect(container.style.getPropertyValue("--size")).toBe("16");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("getter returns the current signal value", () => {
|
|
137
|
-
const anchor = makeAnchor(container);
|
|
138
|
-
|
|
139
|
-
class Comp extends AnchoredComponent {
|
|
140
|
-
constructor() { super(anchor); }
|
|
141
|
-
@Style("--size")
|
|
142
|
-
size = "16px";
|
|
143
|
-
render() { return null; }
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const inst = new Comp() as WithLifecycle & { size: string | number };
|
|
147
|
-
// getter before mount
|
|
148
|
-
expect(inst.size).toBe("16px");
|
|
149
|
-
|
|
150
|
-
inst.onMount?.();
|
|
151
|
-
inst.size = "24px";
|
|
152
|
-
// getter after reactive update
|
|
153
|
-
expect(inst.size).toBe("24px");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("supports multi-word custom property names", () => {
|
|
157
|
-
const anchor = makeAnchor(container);
|
|
158
|
-
|
|
159
|
-
class Comp extends AnchoredComponent {
|
|
160
|
-
constructor() { super(anchor); }
|
|
161
|
-
@Style("--card-border-radius")
|
|
162
|
-
radius = "12px";
|
|
163
|
-
render() { return null; }
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const inst = new Comp() as WithLifecycle;
|
|
167
|
-
inst.onMount?.();
|
|
168
|
-
expect(container.style.getPropertyValue("--card-border-radius")).toBe("12px");
|
|
169
|
-
});
|
|
170
|
-
});
|