@praxisjs/css 0.1.0

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 (164) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/dist/__tests__/css-builder.test.d.ts +2 -0
  4. package/dist/__tests__/css-builder.test.d.ts.map +1 -0
  5. package/dist/__tests__/css-builder.test.js +198 -0
  6. package/dist/__tests__/css-builder.test.js.map +1 -0
  7. package/dist/__tests__/cx.test.d.ts +2 -0
  8. package/dist/__tests__/cx.test.d.ts.map +1 -0
  9. package/dist/__tests__/cx.test.js +47 -0
  10. package/dist/__tests__/cx.test.js.map +1 -0
  11. package/dist/__tests__/decorators.test.d.ts +2 -0
  12. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  13. package/dist/__tests__/decorators.test.js +268 -0
  14. package/dist/__tests__/decorators.test.js.map +1 -0
  15. package/dist/__tests__/extract.test.d.ts +2 -0
  16. package/dist/__tests__/extract.test.d.ts.map +1 -0
  17. package/dist/__tests__/extract.test.js +420 -0
  18. package/dist/__tests__/extract.test.js.map +1 -0
  19. package/dist/__tests__/global.test.d.ts +2 -0
  20. package/dist/__tests__/global.test.d.ts.map +1 -0
  21. package/dist/__tests__/global.test.js +117 -0
  22. package/dist/__tests__/global.test.js.map +1 -0
  23. package/dist/__tests__/keyframes.test.d.ts +2 -0
  24. package/dist/__tests__/keyframes.test.d.ts.map +1 -0
  25. package/dist/__tests__/keyframes.test.js +90 -0
  26. package/dist/__tests__/keyframes.test.js.map +1 -0
  27. package/dist/__tests__/preflight.test.d.ts +2 -0
  28. package/dist/__tests__/preflight.test.d.ts.map +1 -0
  29. package/dist/__tests__/preflight.test.js +55 -0
  30. package/dist/__tests__/preflight.test.js.map +1 -0
  31. package/dist/__tests__/registry.test.d.ts +2 -0
  32. package/dist/__tests__/registry.test.d.ts.map +1 -0
  33. package/dist/__tests__/registry.test.js +132 -0
  34. package/dist/__tests__/registry.test.js.map +1 -0
  35. package/dist/__tests__/style.test.d.ts +2 -0
  36. package/dist/__tests__/style.test.d.ts.map +1 -0
  37. package/dist/__tests__/style.test.js +257 -0
  38. package/dist/__tests__/style.test.js.map +1 -0
  39. package/dist/__tests__/styled.test.d.ts +2 -0
  40. package/dist/__tests__/styled.test.d.ts.map +1 -0
  41. package/dist/__tests__/styled.test.js +951 -0
  42. package/dist/__tests__/styled.test.js.map +1 -0
  43. package/dist/__tests__/theme-instance.test.d.ts +2 -0
  44. package/dist/__tests__/theme-instance.test.d.ts.map +1 -0
  45. package/dist/__tests__/theme-instance.test.js +317 -0
  46. package/dist/__tests__/theme-instance.test.js.map +1 -0
  47. package/dist/__tests__/theme-uninit.test.d.ts +2 -0
  48. package/dist/__tests__/theme-uninit.test.d.ts.map +1 -0
  49. package/dist/__tests__/theme-uninit.test.js +10 -0
  50. package/dist/__tests__/theme-uninit.test.js.map +1 -0
  51. package/dist/__tests__/token-sheet.test.d.ts +2 -0
  52. package/dist/__tests__/token-sheet.test.d.ts.map +1 -0
  53. package/dist/__tests__/token-sheet.test.js +74 -0
  54. package/dist/__tests__/token-sheet.test.js.map +1 -0
  55. package/dist/base/stylesheet.d.ts +30 -0
  56. package/dist/base/stylesheet.d.ts.map +1 -0
  57. package/dist/base/stylesheet.js +29 -0
  58. package/dist/base/stylesheet.js.map +1 -0
  59. package/dist/builder/css-builder.d.ts +98 -0
  60. package/dist/builder/css-builder.d.ts.map +1 -0
  61. package/dist/builder/css-builder.js +79 -0
  62. package/dist/builder/css-builder.js.map +1 -0
  63. package/dist/builder/keyframes.d.ts +23 -0
  64. package/dist/builder/keyframes.d.ts.map +1 -0
  65. package/dist/builder/keyframes.js +54 -0
  66. package/dist/builder/keyframes.js.map +1 -0
  67. package/dist/cx.d.ts +3 -0
  68. package/dist/cx.d.ts.map +1 -0
  69. package/dist/cx.js +23 -0
  70. package/dist/cx.js.map +1 -0
  71. package/dist/decorators/param.d.ts +23 -0
  72. package/dist/decorators/param.d.ts.map +1 -0
  73. package/dist/decorators/param.js +39 -0
  74. package/dist/decorators/param.js.map +1 -0
  75. package/dist/decorators/style.d.ts +15 -0
  76. package/dist/decorators/style.d.ts.map +1 -0
  77. package/dist/decorators/style.js +53 -0
  78. package/dist/decorators/style.js.map +1 -0
  79. package/dist/decorators/styled.d.ts +37 -0
  80. package/dist/decorators/styled.d.ts.map +1 -0
  81. package/dist/decorators/styled.js +138 -0
  82. package/dist/decorators/styled.js.map +1 -0
  83. package/dist/extract.d.ts +14 -0
  84. package/dist/extract.d.ts.map +1 -0
  85. package/dist/extract.js +124 -0
  86. package/dist/extract.js.map +1 -0
  87. package/dist/global.d.ts +27 -0
  88. package/dist/global.d.ts.map +1 -0
  89. package/dist/global.js +49 -0
  90. package/dist/global.js.map +1 -0
  91. package/dist/index.d.ts +11 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +10 -0
  94. package/dist/index.js.map +1 -0
  95. package/dist/internal/collector.d.ts +15 -0
  96. package/dist/internal/collector.d.ts.map +1 -0
  97. package/dist/internal/collector.js +33 -0
  98. package/dist/internal/collector.js.map +1 -0
  99. package/dist/internal/hash.d.ts +5 -0
  100. package/dist/internal/hash.d.ts.map +1 -0
  101. package/dist/internal/hash.js +11 -0
  102. package/dist/internal/hash.js.map +1 -0
  103. package/dist/internal/registry.d.ts +3 -0
  104. package/dist/internal/registry.d.ts.map +1 -0
  105. package/dist/internal/registry.js +41 -0
  106. package/dist/internal/registry.js.map +1 -0
  107. package/dist/preflight.d.ts +18 -0
  108. package/dist/preflight.d.ts.map +1 -0
  109. package/dist/preflight.js +191 -0
  110. package/dist/preflight.js.map +1 -0
  111. package/dist/server.d.ts +8 -0
  112. package/dist/server.d.ts.map +1 -0
  113. package/dist/server.js +8 -0
  114. package/dist/server.js.map +1 -0
  115. package/dist/tokens/decorators.d.ts +41 -0
  116. package/dist/tokens/decorators.d.ts.map +1 -0
  117. package/dist/tokens/decorators.js +73 -0
  118. package/dist/tokens/decorators.js.map +1 -0
  119. package/dist/tokens/index.d.ts +4 -0
  120. package/dist/tokens/index.d.ts.map +1 -0
  121. package/dist/tokens/index.js +4 -0
  122. package/dist/tokens/index.js.map +1 -0
  123. package/dist/tokens/theme-instance.d.ts +33 -0
  124. package/dist/tokens/theme-instance.d.ts.map +1 -0
  125. package/dist/tokens/theme-instance.js +130 -0
  126. package/dist/tokens/theme-instance.js.map +1 -0
  127. package/dist/tokens/token-sheet.d.ts +46 -0
  128. package/dist/tokens/token-sheet.d.ts.map +1 -0
  129. package/dist/tokens/token-sheet.js +55 -0
  130. package/dist/tokens/token-sheet.js.map +1 -0
  131. package/package.json +33 -0
  132. package/src/__tests__/css-builder.test.ts +230 -0
  133. package/src/__tests__/cx.test.ts +63 -0
  134. package/src/__tests__/decorators.test.ts +127 -0
  135. package/src/__tests__/extract.test.ts +418 -0
  136. package/src/__tests__/global.test.ts +140 -0
  137. package/src/__tests__/keyframes.test.ts +103 -0
  138. package/src/__tests__/preflight.test.ts +63 -0
  139. package/src/__tests__/registry.test.ts +151 -0
  140. package/src/__tests__/style.test.ts +153 -0
  141. package/src/__tests__/styled.test.ts +427 -0
  142. package/src/__tests__/theme-instance.test.ts +374 -0
  143. package/src/__tests__/theme-uninit.test.ts +12 -0
  144. package/src/__tests__/token-sheet.test.ts +87 -0
  145. package/src/base/stylesheet.ts +35 -0
  146. package/src/builder/css-builder.ts +204 -0
  147. package/src/builder/keyframes.ts +66 -0
  148. package/src/cx.ts +26 -0
  149. package/src/decorators/param.ts +40 -0
  150. package/src/decorators/style.ts +55 -0
  151. package/src/decorators/styled.ts +167 -0
  152. package/src/extract.ts +133 -0
  153. package/src/global.ts +60 -0
  154. package/src/index.ts +10 -0
  155. package/src/internal/collector.ts +38 -0
  156. package/src/internal/hash.ts +10 -0
  157. package/src/internal/registry.ts +51 -0
  158. package/src/preflight.ts +192 -0
  159. package/src/server.ts +12 -0
  160. package/src/tokens/decorators.ts +108 -0
  161. package/src/tokens/index.ts +3 -0
  162. package/src/tokens/theme-instance.ts +149 -0
  163. package/src/tokens/token-sheet.ts +86 -0
  164. package/tsconfig.json +8 -0
@@ -0,0 +1,63 @@
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
+ });
@@ -0,0 +1,151 @@
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
+ });
@@ -0,0 +1,153 @@
1
+ // @vitest-environment jsdom
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { Style } from "../decorators/style";
4
+
5
+ let container: HTMLDivElement;
6
+
7
+ beforeEach(() => {
8
+ container = document.createElement("div");
9
+ document.body.appendChild(container);
10
+ });
11
+
12
+ afterEach(() => {
13
+ container.remove();
14
+ });
15
+
16
+ /** Attaches an anchor comment to the element, simulating what the runtime does. */
17
+ function makeAnchor(parent: HTMLElement): Comment {
18
+ const anchor = document.createComment("[/Comp]");
19
+ parent.appendChild(anchor);
20
+ return anchor;
21
+ }
22
+
23
+ type WithLifecycle = { onMount?(): void; onUnmount?(): void };
24
+
25
+ describe("@Style field decorator", () => {
26
+ it("sets a CSS custom property on the container element after onMount", () => {
27
+ const anchor = makeAnchor(container);
28
+
29
+ class Comp {
30
+ _anchor = anchor;
31
+ @Style("--accent")
32
+ accent = "#3b82f6";
33
+ }
34
+
35
+ const inst = new Comp() as WithLifecycle;
36
+ inst.onMount?.();
37
+
38
+ expect(container.style.getPropertyValue("--accent")).toBe("#3b82f6");
39
+ });
40
+
41
+ it("updates CSS custom property reactively when field value changes", () => {
42
+ const anchor = makeAnchor(container);
43
+
44
+ class Comp {
45
+ _anchor = anchor;
46
+ @Style("--color")
47
+ color = "red";
48
+ }
49
+
50
+ const inst = new Comp() as WithLifecycle & { color: string };
51
+ inst.onMount?.();
52
+ expect(container.style.getPropertyValue("--color")).toBe("red");
53
+
54
+ inst.color = "blue";
55
+ expect(container.style.getPropertyValue("--color")).toBe("blue");
56
+ });
57
+
58
+ it("removes the CSS custom property on unmount", () => {
59
+ const anchor = makeAnchor(container);
60
+
61
+ class Comp {
62
+ _anchor = anchor;
63
+ @Style("--accent")
64
+ accent = "red";
65
+ }
66
+
67
+ const inst = new Comp() as WithLifecycle;
68
+ inst.onMount?.();
69
+ expect(container.style.getPropertyValue("--accent")).toBe("red");
70
+
71
+ inst.onUnmount?.();
72
+ expect(container.style.getPropertyValue("--accent")).toBe("");
73
+ });
74
+
75
+ it("does not throw when _anchor has no parentElement", () => {
76
+ const anchor = document.createComment("[/Comp]");
77
+
78
+ class Comp {
79
+ _anchor = anchor;
80
+ @Style("--color")
81
+ color = "red";
82
+ }
83
+
84
+ const inst = new Comp() as WithLifecycle;
85
+ expect(() => inst.onMount?.()).not.toThrow();
86
+ expect(() => inst.onUnmount?.()).not.toThrow();
87
+ });
88
+
89
+ it("stops the reactive effect after onUnmount — further changes are silent", () => {
90
+ const anchor = makeAnchor(container);
91
+
92
+ class Comp {
93
+ _anchor = anchor;
94
+ @Style("--val")
95
+ val = "a";
96
+ }
97
+
98
+ const inst = new Comp() as WithLifecycle & { val: string };
99
+ inst.onMount?.();
100
+ expect(container.style.getPropertyValue("--val")).toBe("a");
101
+
102
+ inst.onUnmount?.();
103
+ inst.val = "b";
104
+ expect(container.style.getPropertyValue("--val")).toBe("");
105
+ });
106
+
107
+ it("supports numeric values (converted to string)", () => {
108
+ const anchor = makeAnchor(container);
109
+
110
+ class Comp {
111
+ _anchor = anchor;
112
+ @Style("--size")
113
+ size = 16;
114
+ }
115
+
116
+ const inst = new Comp() as WithLifecycle;
117
+ inst.onMount?.();
118
+ expect(container.style.getPropertyValue("--size")).toBe("16");
119
+ });
120
+
121
+ it("getter returns the current signal value", () => {
122
+ const anchor = makeAnchor(container);
123
+
124
+ class Comp {
125
+ _anchor = anchor;
126
+ @Style("--size")
127
+ size = "16px";
128
+ }
129
+
130
+ const inst = new Comp() as WithLifecycle & { size: string | number };
131
+ // getter before mount
132
+ expect(inst.size).toBe("16px");
133
+
134
+ inst.onMount?.();
135
+ inst.size = "24px";
136
+ // getter after reactive update
137
+ expect(inst.size).toBe("24px");
138
+ });
139
+
140
+ it("supports multi-word custom property names", () => {
141
+ const anchor = makeAnchor(container);
142
+
143
+ class Comp {
144
+ _anchor = anchor;
145
+ @Style("--card-border-radius")
146
+ radius = "12px";
147
+ }
148
+
149
+ const inst = new Comp() as WithLifecycle;
150
+ inst.onMount?.();
151
+ expect(container.style.getPropertyValue("--card-border-radius")).toBe("12px");
152
+ });
153
+ });