@praxisjs/css 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/builder/css-builder.d.ts.map +1 -1
  3. package/dist/builder/css-builder.js +68 -74
  4. package/dist/builder/css-builder.js.map +1 -1
  5. package/dist/builder/keyframes.d.ts +5 -0
  6. package/dist/builder/keyframes.d.ts.map +1 -1
  7. package/dist/builder/keyframes.js +12 -14
  8. package/dist/builder/keyframes.js.map +1 -1
  9. package/dist/cx.d.ts.map +1 -1
  10. package/dist/cx.js +23 -18
  11. package/dist/cx.js.map +1 -1
  12. package/dist/decorators/param.d.ts +1 -2
  13. package/dist/decorators/param.d.ts.map +1 -1
  14. package/dist/decorators/param.js +9 -17
  15. package/dist/decorators/param.js.map +1 -1
  16. package/dist/decorators/style.d.ts.map +1 -1
  17. package/dist/decorators/style.js +61 -33
  18. package/dist/decorators/style.js.map +1 -1
  19. package/dist/decorators/styled.d.ts.map +1 -1
  20. package/dist/decorators/styled.js +112 -95
  21. package/dist/decorators/styled.js.map +1 -1
  22. package/dist/extract.d.ts.map +1 -1
  23. package/dist/extract.js +19 -35
  24. package/dist/extract.js.map +1 -1
  25. package/dist/global.d.ts +3 -0
  26. package/dist/global.d.ts.map +1 -1
  27. package/dist/global.js +9 -4
  28. package/dist/global.js.map +1 -1
  29. package/dist/internal/kebab.d.ts +5 -0
  30. package/dist/internal/kebab.d.ts.map +1 -0
  31. package/dist/internal/kebab.js +23 -0
  32. package/dist/internal/kebab.js.map +1 -0
  33. package/dist/internal/lifecycle.d.ts +15 -0
  34. package/dist/internal/lifecycle.d.ts.map +1 -0
  35. package/dist/internal/lifecycle.js +16 -0
  36. package/dist/internal/lifecycle.js.map +1 -0
  37. package/dist/internal/registry.d.ts +5 -1
  38. package/dist/internal/registry.d.ts.map +1 -1
  39. package/dist/internal/registry.js +9 -23
  40. package/dist/internal/registry.js.map +1 -1
  41. package/dist/internal/sheet.d.ts +20 -0
  42. package/dist/internal/sheet.d.ts.map +1 -0
  43. package/dist/internal/sheet.js +56 -0
  44. package/dist/internal/sheet.js.map +1 -0
  45. package/dist/internal/static-mode.d.ts +2 -0
  46. package/dist/internal/static-mode.d.ts.map +1 -0
  47. package/dist/internal/static-mode.js +2 -0
  48. package/dist/internal/static-mode.js.map +1 -0
  49. package/dist/preflight.d.ts +3 -1
  50. package/dist/preflight.d.ts.map +1 -1
  51. package/dist/preflight.js +7 -3
  52. package/dist/preflight.js.map +1 -1
  53. package/dist/tokens/decorators.d.ts.map +1 -1
  54. package/dist/tokens/decorators.js +13 -4
  55. package/dist/tokens/decorators.js.map +1 -1
  56. package/dist/tokens/theme-instance.d.ts +5 -0
  57. package/dist/tokens/theme-instance.d.ts.map +1 -1
  58. package/dist/tokens/theme-instance.js +25 -17
  59. package/dist/tokens/theme-instance.js.map +1 -1
  60. package/dist/tokens/token-sheet.d.ts.map +1 -1
  61. package/dist/tokens/token-sheet.js +12 -9
  62. package/dist/tokens/token-sheet.js.map +1 -1
  63. package/package.json +13 -5
  64. package/src/builder/css-builder.ts +79 -83
  65. package/src/builder/keyframes.ts +12 -20
  66. package/src/cx.ts +20 -13
  67. package/src/decorators/param.ts +12 -16
  68. package/src/decorators/style.ts +79 -35
  69. package/src/decorators/styled.ts +127 -111
  70. package/src/extract.ts +18 -34
  71. package/src/global.ts +10 -9
  72. package/src/internal/kebab.ts +23 -0
  73. package/src/internal/lifecycle.ts +21 -0
  74. package/src/internal/registry.ts +9 -35
  75. package/src/internal/sheet.ts +71 -0
  76. package/src/internal/static-mode.ts +6 -0
  77. package/src/preflight.ts +8 -3
  78. package/src/tokens/decorators.ts +15 -3
  79. package/src/tokens/theme-instance.ts +26 -16
  80. package/src/tokens/token-sheet.ts +12 -12
  81. package/dist/__tests__/css-builder.test.d.ts +0 -2
  82. package/dist/__tests__/css-builder.test.d.ts.map +0 -1
  83. package/dist/__tests__/css-builder.test.js +0 -198
  84. package/dist/__tests__/css-builder.test.js.map +0 -1
  85. package/dist/__tests__/cx.test.d.ts +0 -2
  86. package/dist/__tests__/cx.test.d.ts.map +0 -1
  87. package/dist/__tests__/cx.test.js +0 -47
  88. package/dist/__tests__/cx.test.js.map +0 -1
  89. package/dist/__tests__/decorators.test.d.ts +0 -2
  90. package/dist/__tests__/decorators.test.d.ts.map +0 -1
  91. package/dist/__tests__/decorators.test.js +0 -264
  92. package/dist/__tests__/decorators.test.js.map +0 -1
  93. package/dist/__tests__/extract.test.d.ts +0 -2
  94. package/dist/__tests__/extract.test.d.ts.map +0 -1
  95. package/dist/__tests__/extract.test.js +0 -416
  96. package/dist/__tests__/extract.test.js.map +0 -1
  97. package/dist/__tests__/global.test.d.ts +0 -2
  98. package/dist/__tests__/global.test.d.ts.map +0 -1
  99. package/dist/__tests__/global.test.js +0 -136
  100. package/dist/__tests__/global.test.js.map +0 -1
  101. package/dist/__tests__/keyframes.test.d.ts +0 -2
  102. package/dist/__tests__/keyframes.test.d.ts.map +0 -1
  103. package/dist/__tests__/keyframes.test.js +0 -90
  104. package/dist/__tests__/keyframes.test.js.map +0 -1
  105. package/dist/__tests__/preflight.test.d.ts +0 -2
  106. package/dist/__tests__/preflight.test.d.ts.map +0 -1
  107. package/dist/__tests__/preflight.test.js +0 -74
  108. package/dist/__tests__/preflight.test.js.map +0 -1
  109. package/dist/__tests__/registry.test.d.ts +0 -2
  110. package/dist/__tests__/registry.test.d.ts.map +0 -1
  111. package/dist/__tests__/registry.test.js +0 -132
  112. package/dist/__tests__/registry.test.js.map +0 -1
  113. package/dist/__tests__/style.test.d.ts +0 -2
  114. package/dist/__tests__/style.test.d.ts.map +0 -1
  115. package/dist/__tests__/style.test.js +0 -281
  116. package/dist/__tests__/style.test.js.map +0 -1
  117. package/dist/__tests__/styled.test.d.ts +0 -2
  118. package/dist/__tests__/styled.test.d.ts.map +0 -1
  119. package/dist/__tests__/styled.test.js +0 -904
  120. package/dist/__tests__/styled.test.js.map +0 -1
  121. package/dist/__tests__/theme-instance.test.d.ts +0 -2
  122. package/dist/__tests__/theme-instance.test.d.ts.map +0 -1
  123. package/dist/__tests__/theme-instance.test.js +0 -309
  124. package/dist/__tests__/theme-instance.test.js.map +0 -1
  125. package/dist/__tests__/theme-uninit.test.d.ts +0 -2
  126. package/dist/__tests__/theme-uninit.test.d.ts.map +0 -1
  127. package/dist/__tests__/theme-uninit.test.js +0 -10
  128. package/dist/__tests__/theme-uninit.test.js.map +0 -1
  129. package/dist/__tests__/token-sheet.test.d.ts +0 -2
  130. package/dist/__tests__/token-sheet.test.d.ts.map +0 -1
  131. package/dist/__tests__/token-sheet.test.js +0 -74
  132. package/dist/__tests__/token-sheet.test.js.map +0 -1
  133. package/src/__tests__/css-builder.test.ts +0 -230
  134. package/src/__tests__/cx.test.ts +0 -63
  135. package/src/__tests__/decorators.test.ts +0 -127
  136. package/src/__tests__/extract.test.ts +0 -459
  137. package/src/__tests__/global.test.ts +0 -163
  138. package/src/__tests__/keyframes.test.ts +0 -103
  139. package/src/__tests__/preflight.test.ts +0 -85
  140. package/src/__tests__/registry.test.ts +0 -151
  141. package/src/__tests__/style.test.ts +0 -170
  142. package/src/__tests__/styled.test.ts +0 -443
  143. package/src/__tests__/theme-instance.test.ts +0 -374
  144. package/src/__tests__/theme-uninit.test.ts +0 -12
  145. package/src/__tests__/token-sheet.test.ts +0 -87
  146. 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
- });