@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,230 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { createCSSBuilder, isCSSBuilder } from "../builder/css-builder";
3
-
4
- describe("createCSSBuilder()", () => {
5
- it("serialises a flat property object to CSS declarations", () => {
6
- const css = createCSSBuilder({ display: "flex", gap: "12px", padding: "16px" });
7
- expect(String(css)).toBe("display: flex;\ngap: 12px;\npadding: 16px;");
8
- });
9
-
10
- it("converts camelCase properties to kebab-case", () => {
11
- const css = createCSSBuilder({ flexDirection: "column", borderRadius: "8px", fontSize: "1rem" });
12
- expect(String(css)).toContain("flex-direction: column;");
13
- expect(String(css)).toContain("border-radius: 8px;");
14
- expect(String(css)).toContain("font-size: 1rem;");
15
- });
16
-
17
- it("skips null and undefined values", () => {
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
- const css = createCSSBuilder({ display: "flex", color: undefined as any });
20
- expect(String(css)).toBe("display: flex;");
21
- });
22
-
23
- it("returns empty string when no properties are given", () => {
24
- expect(String(createCSSBuilder({}))).toBe("");
25
- });
26
-
27
- it("accepts CSS custom properties (--var)", () => {
28
- const css = createCSSBuilder({ "--accent": "#3b82f6" } as Parameters<typeof createCSSBuilder>[0]);
29
- expect(String(css)).toContain("--accent: #3b82f6;");
30
- });
31
- });
32
-
33
- describe("CSSBuilder — pseudo-state nesting", () => {
34
- it(".hover() adds &:hover block", () => {
35
- const css = createCSSBuilder({ display: "flex" }).hover({ opacity: 0.9 });
36
- const out = String(css);
37
- expect(out).toContain("display: flex;");
38
- expect(out).toContain("&:hover {");
39
- expect(out).toContain(" opacity: 0.9;");
40
- });
41
-
42
- it(".focus() adds &:focus block", () => {
43
- const out = String(createCSSBuilder({ color: "red" }).focus({ outline: "2px solid blue" }));
44
- expect(out).toContain("&:focus {");
45
- expect(out).toContain(" outline: 2px solid blue;");
46
- });
47
-
48
- it(".focusWithin() adds &:focus-within block", () => {
49
- expect(String(createCSSBuilder({}).focusWithin({ color: "red" }))).toContain("&:focus-within {");
50
- });
51
-
52
- it(".focusVisible() adds &:focus-visible block", () => {
53
- expect(String(createCSSBuilder({}).focusVisible({ outline: "none" }))).toContain("&:focus-visible {");
54
- });
55
-
56
- it(".active() adds &:active block", () => {
57
- expect(String(createCSSBuilder({}).active({ transform: "scale(0.98)" }))).toContain("&:active {");
58
- });
59
-
60
- it(".disabled() adds &:disabled + aria-disabled block", () => {
61
- const out = String(createCSSBuilder({}).disabled({ opacity: 0.4 }));
62
- expect(out).toContain("&:disabled");
63
- expect(out).toContain("aria-disabled");
64
- });
65
-
66
- it(".checked() adds &:checked block", () => {
67
- expect(String(createCSSBuilder({}).checked({ background: "blue" }))).toContain("&:checked {");
68
- });
69
-
70
- it(".placeholder() adds &::placeholder block", () => {
71
- expect(String(createCSSBuilder({}).placeholder({ color: "#999" }))).toContain("&::placeholder {");
72
- });
73
-
74
- it(".first() adds &:first-child block", () => {
75
- expect(String(createCSSBuilder({}).first({ marginTop: 0 }))).toContain("&:first-child {");
76
- });
77
-
78
- it(".last() adds &:last-child block", () => {
79
- expect(String(createCSSBuilder({}).last({ marginBottom: 0 }))).toContain("&:last-child {");
80
- });
81
-
82
- it(".nthChild() adds &:nth-child(n) block", () => {
83
- expect(String(createCSSBuilder({}).nthChild("2n+1", { background: "#f0f0f0" }))).toContain("&:nth-child(2n+1) {");
84
- });
85
-
86
- it(".not() adds &:not(selector) block", () => {
87
- expect(String(createCSSBuilder({}).not(".active", { opacity: 0.5 }))).toContain("&:not(.active) {");
88
- });
89
- });
90
-
91
- describe("CSSBuilder — at-rule nesting", () => {
92
- it(".media() adds @media (query) block", () => {
93
- const out = String(createCSSBuilder({ display: "flex" }).media("max-width: 640px", { flexDirection: "column" }));
94
- expect(out).toContain("@media (max-width: 640px) {");
95
- expect(out).toContain(" flex-direction: column;");
96
- });
97
-
98
- it(".container() adds @container (query) block", () => {
99
- const out = String(createCSSBuilder({}).container("min-width: 400px", { padding: "16px" }));
100
- expect(out).toContain("@container (min-width: 400px) {");
101
- });
102
-
103
- it(".on() adds any arbitrary selector or at-rule", () => {
104
- const out = String(createCSSBuilder({}).on("&::before", { content: '""', display: "block" }));
105
- expect(out).toContain("&::before {");
106
- expect(out).toContain(" content: \"\";");
107
- });
108
- });
109
-
110
- describe("CSSBuilder — chaining", () => {
111
- it("chains multiple nesting calls correctly", () => {
112
- const out = String(
113
- createCSSBuilder({ display: "flex" })
114
- .hover({ opacity: 0.9 })
115
- .focus({ outline: "2px solid blue" })
116
- .media("max-width: 640px", { flexDirection: "column" }),
117
- );
118
- expect(out).toContain("display: flex;");
119
- expect(out).toContain("&:hover {");
120
- expect(out).toContain("&:focus {");
121
- expect(out).toContain("@media (max-width: 640px) {");
122
- });
123
-
124
- it("each chained call returns the same builder (fluent pattern)", () => {
125
- const b1 = createCSSBuilder({ display: "flex" });
126
- const b2 = b1.hover({ opacity: 0.9 });
127
- expect(b1).toBe(b2);
128
- });
129
-
130
- it("skips nested block when props are empty", () => {
131
- const out = String(createCSSBuilder({ display: "flex" }).hover({}));
132
- expect(out).not.toContain("&:hover");
133
- });
134
- });
135
-
136
- describe("CSSBuilder — pseudo-elements", () => {
137
- it(".before() adds &::before block", () => {
138
- expect(String(createCSSBuilder({}).before({ content: '""', display: "block" }))).toContain("&::before {");
139
- });
140
- it(".after() adds &::after block", () => {
141
- expect(String(createCSSBuilder({}).after({ content: '""' }))).toContain("&::after {");
142
- });
143
- it(".selection() adds &::selection block", () => {
144
- expect(String(createCSSBuilder({}).selection({ background: "blue" }))).toContain("&::selection {");
145
- });
146
- it(".firstLine() adds &::first-line block", () => {
147
- expect(String(createCSSBuilder({}).firstLine({ fontWeight: "bold" }))).toContain("&::first-line {");
148
- });
149
- it(".firstLetter() adds &::first-letter block", () => {
150
- expect(String(createCSSBuilder({}).firstLetter({ fontSize: "2em" }))).toContain("&::first-letter {");
151
- });
152
- it(".marker() adds &::marker block", () => {
153
- expect(String(createCSSBuilder({}).marker({ color: "red" }))).toContain("&::marker {");
154
- });
155
- it(".backdrop() adds &::backdrop block", () => {
156
- expect(String(createCSSBuilder({}).backdrop({ background: "rgba(0,0,0,0.5)" }))).toContain("&::backdrop {");
157
- });
158
- });
159
-
160
- describe("CSSBuilder — form pseudo-classes", () => {
161
- it(".enabled() adds &:enabled block", () => {
162
- expect(String(createCSSBuilder({}).enabled({ opacity: 1 }))).toContain("&:enabled {");
163
- });
164
- it(".indeterminate() adds &:indeterminate block", () => {
165
- expect(String(createCSSBuilder({}).indeterminate({ opacity: 0.5 }))).toContain("&:indeterminate {");
166
- });
167
- it(".required() adds &:required block", () => {
168
- expect(String(createCSSBuilder({}).required({ borderColor: "red" }))).toContain("&:required {");
169
- });
170
- it(".optional() adds &:optional block", () => {
171
- expect(String(createCSSBuilder({}).optional({ borderColor: "gray" }))).toContain("&:optional {");
172
- });
173
- it(".valid() adds &:valid block", () => {
174
- expect(String(createCSSBuilder({}).valid({ borderColor: "green" }))).toContain("&:valid {");
175
- });
176
- it(".invalid() adds &:invalid block", () => {
177
- expect(String(createCSSBuilder({}).invalid({ borderColor: "red" }))).toContain("&:invalid {");
178
- });
179
- it(".readOnly() adds &:read-only block", () => {
180
- expect(String(createCSSBuilder({}).readOnly({ opacity: 0.7 }))).toContain("&:read-only {");
181
- });
182
- it(".visited() adds &:visited block", () => {
183
- expect(String(createCSSBuilder({}).visited({ color: "purple" }))).toContain("&:visited {");
184
- });
185
- it(".target() adds &:target block", () => {
186
- expect(String(createCSSBuilder({}).target({ outline: "2px solid blue" }))).toContain("&:target {");
187
- });
188
- it(".empty() adds &:empty block", () => {
189
- expect(String(createCSSBuilder({}).empty({ display: "none" }))).toContain("&:empty {");
190
- });
191
- });
192
-
193
- describe("CSSBuilder — relational pseudo-classes", () => {
194
- it(".has() adds &:has(selector) block", () => {
195
- expect(String(createCSSBuilder({}).has(".active", { fontWeight: "bold" }))).toContain("&:has(.active) {");
196
- });
197
- it(".is() adds &:is(selector) block", () => {
198
- expect(String(createCSSBuilder({}).is("h1, h2", { lineHeight: 1.2 }))).toContain("&:is(h1, h2) {");
199
- });
200
- it(".where() adds &:where(selector) block", () => {
201
- expect(String(createCSSBuilder({}).where("a, button", { cursor: "pointer" }))).toContain("&:where(a, button) {");
202
- });
203
- });
204
-
205
- describe("CSSBuilder — @supports", () => {
206
- it(".supports() adds @supports (condition) block", () => {
207
- const out = String(createCSSBuilder({ display: "flex" }).supports("display: grid", { display: "grid" }));
208
- expect(out).toContain("@supports (display: grid) {");
209
- expect(out).toContain(" display: grid;");
210
- });
211
- });
212
-
213
- describe("isCSSBuilder()", () => {
214
- it("returns true for a CSSBuilder instance", () => {
215
- expect(isCSSBuilder(createCSSBuilder({ display: "flex" }))).toBe(true);
216
- });
217
-
218
- it("returns false for plain strings", () => {
219
- expect(isCSSBuilder("display: flex;")).toBe(false);
220
- });
221
-
222
- it("returns false for null and undefined", () => {
223
- expect(isCSSBuilder(null)).toBe(false);
224
- expect(isCSSBuilder(undefined)).toBe(false);
225
- });
226
-
227
- it("returns false for plain objects", () => {
228
- expect(isCSSBuilder({ display: "flex" })).toBe(false);
229
- });
230
- });
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { cx } from "../cx";
3
-
4
- describe("cx()", () => {
5
- it("returns empty string with no args", () => {
6
- expect(cx()).toBe("");
7
- });
8
-
9
- it("handles plain strings", () => {
10
- expect(cx("a", "b", "c")).toBe("a b c");
11
- });
12
-
13
- it("filters falsy values", () => {
14
- expect(cx(null, undefined, false, "valid")).toBe("valid");
15
- });
16
-
17
- it("includes 0 as a string", () => {
18
- expect(cx(0)).toBe("0");
19
- });
20
-
21
- it("includes numeric class names", () => {
22
- expect(cx(1, 2)).toBe("1 2");
23
- });
24
-
25
- it("handles objects — includes truthy keys", () => {
26
- expect(cx({ active: true, disabled: false, loading: true })).toBe(
27
- "active loading",
28
- );
29
- });
30
-
31
- it("handles objects with zero values as falsy", () => {
32
- expect(cx({ a: 1, b: 0, c: "", d: null, e: "yes" })).toBe("a e");
33
- });
34
-
35
- it("handles arrays recursively", () => {
36
- expect(cx("a", ["b", false, "c"])).toBe("a b c");
37
- });
38
-
39
- it("handles deeply nested arrays", () => {
40
- expect(cx(["a", ["b", ["c"]]])).toBe("a b c");
41
- });
42
-
43
- it("combines strings, objects, and arrays", () => {
44
- expect(cx("base", { active: true, disabled: false }, ["extra"])).toBe(
45
- "base active extra",
46
- );
47
- });
48
-
49
- it("returns empty string for all-falsy input", () => {
50
- expect(cx(false, null, undefined, "")).toBe("");
51
- });
52
-
53
- it("skips empty result from all-falsy nested array", () => {
54
- // inner = cx(false, null) = "" → falsy → not pushed
55
- expect(cx("a", [false, null, undefined])).toBe("a");
56
- expect(cx([false, null])).toBe("");
57
- });
58
-
59
- it("ignores boolean true — not a string, number, array, or object", () => {
60
- // boolean true passes the !arg guard but hits the else-if(object) false branch
61
- expect(cx(true as unknown as string, "valid")).toBe("valid");
62
- });
63
- });
@@ -1,127 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
- import { Themed, Theme } from "../tokens/decorators";
4
- import { theme, ThemeInstance, createThemeInstance } from "../tokens/theme-instance";
5
- import { TokenSheet } from "../tokens/token-sheet";
6
-
7
- // ─── Fixtures ─────────────────────────────────────────────────────────────────
8
-
9
- class AppTokens extends TokenSheet {
10
- colorPrimary!: string;
11
- colorBg!: string;
12
- }
13
-
14
- class LightTheme extends AppTokens {
15
- colorPrimary = "#3b82f6";
16
- colorBg = "#ffffff";
17
- }
18
-
19
- class DarkTheme extends LightTheme {
20
- colorPrimary = "#60a5fa";
21
- colorBg = "#0f172a";
22
- }
23
-
24
- function styleEl(): HTMLStyleElement | null {
25
- return document.querySelector("style[data-praxis-theme]");
26
- }
27
-
28
- beforeEach(() => { styleEl()?.remove(); });
29
- afterEach(() => { styleEl()?.remove(); });
30
-
31
- // ─── @Themed ──────────────────────────────────────────────────────────────────
32
-
33
- describe("@Themed — class decorator", () => {
34
- it("initialize() creates a ThemeInstance singleton at class-decoration time", () => {
35
- @Themed(AppTokens, LightTheme)
36
- class App {}
37
- void App;
38
-
39
- expect(theme()).toBeInstanceOf(ThemeInstance);
40
- expect(styleEl()).not.toBeNull();
41
- });
42
-
43
- it("injected :root vars match the default theme", () => {
44
- @Themed(AppTokens, LightTheme)
45
- class App {}
46
- void App;
47
-
48
- const css = styleEl()?.textContent ?? "";
49
- expect(css).toContain("--color-primary: #3b82f6");
50
- expect(css).toContain("--color-bg: #ffffff");
51
- });
52
-
53
- it("create() re-creates the singleton when the class is instantiated", () => {
54
- @Themed(AppTokens, DarkTheme)
55
- class App {
56
- // minimal render so createClassDecorator doesn't strip it
57
- render() { return null; }
58
- }
59
-
60
- const t = theme();
61
- expect(t.current).toBeInstanceOf(DarkTheme);
62
-
63
- // Instantiating the class triggers create() which calls createThemeInstance again
64
- new App();
65
- expect(theme()).toBeInstanceOf(ThemeInstance);
66
- });
67
-
68
- it("passes persist config to ThemeInstance", () => {
69
- @Themed(AppTokens, LightTheme, { persist: true })
70
- class App {}
71
- void App;
72
-
73
- theme().switch(DarkTheme);
74
- expect(localStorage.getItem("praxis-theme")).not.toBeNull();
75
- localStorage.clear();
76
- });
77
-
78
- it("second @Themed decoration overwrites the singleton", () => {
79
- @Themed(AppTokens, LightTheme)
80
- class App {}
81
- void App;
82
-
83
- @Themed(AppTokens, DarkTheme)
84
- class App2 {}
85
- void App2;
86
-
87
- expect((theme().current as DarkTheme).colorPrimary).toBe("#60a5fa");
88
- });
89
- });
90
-
91
- // ─── @Theme ───────────────────────────────────────────────────────────────────
92
-
93
- describe("@Theme — field decorator", () => {
94
- it("returns the singleton ThemeInstance via getter", () => {
95
- createThemeInstance(LightTheme);
96
-
97
- class Comp {
98
- @Theme() theme!: ThemeInstance;
99
- }
100
-
101
- expect(new Comp().theme).toBe(theme());
102
- });
103
-
104
- it("same reference on every access", () => {
105
- createThemeInstance(LightTheme);
106
-
107
- class Comp {
108
- @Theme() theme!: ThemeInstance;
109
- }
110
-
111
- const c = new Comp();
112
- expect(c.theme).toBe(c.theme);
113
- });
114
-
115
- it("setter is a no-op — value stays unchanged", () => {
116
- createThemeInstance(LightTheme);
117
-
118
- class Comp {
119
- @Theme() theme!: ThemeInstance;
120
- }
121
-
122
- const c = new Comp();
123
- const before = c.theme;
124
- (c as unknown as Record<string, unknown>).theme = "anything";
125
- expect(c.theme).toBe(before);
126
- });
127
- });