clava 0.1.19 → 0.2.1

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.
@@ -0,0 +1,298 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ CONFIGS,
4
+ createCVFromConfig,
5
+ getConfigDescription,
6
+ getConfigMode,
7
+ getConfigTransformClass,
8
+ getModeComponent,
9
+ getStyleClass,
10
+ } from "./_utils.ts";
11
+
12
+ for (const config of Object.values(CONFIGS)) {
13
+ const mode = getConfigMode(config);
14
+ const cv = createCVFromConfig(config);
15
+ const cls = getConfigTransformClass(config);
16
+
17
+ describe(getConfigDescription(config), () => {
18
+ test("computedVariants", () => {
19
+ const component = getModeComponent(
20
+ mode,
21
+ cv({
22
+ computedVariants: {
23
+ size: (value: "sm" | "lg") => (value === "sm" ? "small" : "large"),
24
+ },
25
+ }),
26
+ );
27
+ const props = component({ size: "lg" });
28
+ expect(getStyleClass(props)).toEqual({ class: cls("large") });
29
+ });
30
+
31
+ test("computedVariants with style", () => {
32
+ const component = getModeComponent(
33
+ mode,
34
+ cv({
35
+ computedVariants: {
36
+ size: (value: "sm" | "lg") => ({
37
+ class: value === "sm" ? "small" : "large",
38
+ style: { fontSize: value === "sm" ? "12px" : "16px" },
39
+ }),
40
+ },
41
+ }),
42
+ );
43
+ const props = component({ size: "lg" });
44
+ expect(getStyleClass(props)).toEqual({
45
+ class: cls("large"),
46
+ fontSize: "16px",
47
+ });
48
+ });
49
+
50
+ test("computedVariants can return another component default result", () => {
51
+ const button = cv({
52
+ variants: {
53
+ size: {
54
+ sm: { class: "button-sm", style: { fontSize: "12px" } },
55
+ lg: { class: "button-lg", style: { fontSize: "16px" } },
56
+ },
57
+ },
58
+ });
59
+ const component = getModeComponent(
60
+ mode,
61
+ cv({
62
+ computedVariants: {
63
+ size: (value: "sm" | "lg") => {
64
+ return button({ size: value });
65
+ },
66
+ },
67
+ }),
68
+ );
69
+ const props = component({ size: "lg" });
70
+ expect(getStyleClass(props)).toEqual({
71
+ class: cls("button-lg"),
72
+ fontSize: "16px",
73
+ });
74
+ });
75
+
76
+ test("computedVariants overrides extended object variants", () => {
77
+ const base = cv({
78
+ variants: {
79
+ size: {
80
+ sm: { class: "base-sm", style: { fontSize: "12px" } },
81
+ lg: { class: "base-lg", style: { fontSize: "16px" } },
82
+ },
83
+ },
84
+ });
85
+ const component = getModeComponent(
86
+ mode,
87
+ cv({
88
+ extend: [base],
89
+ computedVariants: {
90
+ size: (value: "sm" | "lg") => ({
91
+ class: value === "sm" ? "extended-sm" : "extended-lg",
92
+ style: {
93
+ backgroundColor: value === "sm" ? "lightgray" : "gray",
94
+ },
95
+ }),
96
+ },
97
+ }),
98
+ );
99
+ const props = component({ size: "lg" });
100
+ expect(getStyleClass(props)).toEqual({
101
+ class: cls("extended-lg"),
102
+ backgroundColor: "gray",
103
+ });
104
+ });
105
+
106
+ test("computedVariants overrides extended computedVariants", () => {
107
+ const base = cv({
108
+ computedVariants: {
109
+ size: (value: "sm" | "lg") => ({
110
+ class: value === "sm" ? "base-sm" : "base-lg",
111
+ style: { fontSize: value === "sm" ? "12px" : "16px" },
112
+ }),
113
+ },
114
+ });
115
+ const component = getModeComponent(
116
+ mode,
117
+ cv({
118
+ extend: [base],
119
+ computedVariants: {
120
+ size: (value: "sm" | "lg") => ({
121
+ class: value === "sm" ? "extended-sm" : "extended-lg",
122
+ style: {
123
+ backgroundColor: value === "sm" ? "lightgray" : "gray",
124
+ },
125
+ }),
126
+ },
127
+ }),
128
+ );
129
+ const props = component({ size: "lg" });
130
+ expect(getStyleClass(props)).toEqual({
131
+ class: cls("extended-lg"),
132
+ backgroundColor: "gray",
133
+ });
134
+ });
135
+
136
+ test("computedVariants style does not accept numbers", () => {
137
+ const component = getModeComponent(
138
+ mode,
139
+ cv({
140
+ computedVariants: {
141
+ // @ts-expect-error
142
+ size: (value: "sm" | "lg") => ({
143
+ class: value === "sm" ? "small" : "large",
144
+ style: { fontSize: value === "sm" ? 12 : 16 },
145
+ }),
146
+ },
147
+ }),
148
+ );
149
+ const props = component({ size: "lg" });
150
+ expect(getStyleClass(props)).toEqual({
151
+ class: cls("large"),
152
+ fontSize: expect.toBeOneOf(["16", "16px"]),
153
+ });
154
+ });
155
+
156
+ test("computedVariants changes extended boolean variant to string", () => {
157
+ const base = cv({
158
+ variants: { disabled: { true: "disabled", false: "enabled" } },
159
+ });
160
+ const component = getModeComponent(
161
+ mode,
162
+ cv({
163
+ extend: [base],
164
+ computedVariants: {
165
+ disabled: (value: "yes" | "no" | "maybe") => {
166
+ if (value === "yes") return "state-disabled";
167
+ if (value === "no") return "state-enabled";
168
+ return "state-pending";
169
+ },
170
+ },
171
+ }),
172
+ );
173
+ component({
174
+ // @ts-expect-error
175
+ disabled: true,
176
+ });
177
+ const props = component({ disabled: "maybe" });
178
+ expect(getStyleClass(props)).toEqual({ class: cls("state-pending") });
179
+ });
180
+
181
+ test("computedVariants changes extended string variant to boolean", () => {
182
+ const base = cv({ variants: { size: { sm: "sm", md: "md", lg: "lg" } } });
183
+ const component = getModeComponent(
184
+ mode,
185
+ cv({
186
+ extend: [base],
187
+ computedVariants: {
188
+ size: (value: boolean) => (value ? "size-large" : "size-small"),
189
+ },
190
+ }),
191
+ );
192
+ component({
193
+ // @ts-expect-error
194
+ size: "sm",
195
+ });
196
+ const propsTrue = component({ size: true });
197
+ expect(getStyleClass(propsTrue)).toEqual({ class: cls("size-large") });
198
+ const propsFalse = component({ size: false });
199
+ expect(getStyleClass(propsFalse)).toEqual({ class: cls("size-small") });
200
+ });
201
+
202
+ test("computedVariants with number type", () => {
203
+ const component = getModeComponent(
204
+ mode,
205
+ cv({
206
+ computedVariants: {
207
+ columns: (value: number) => ({
208
+ class: `grid-cols-${value}`,
209
+ style: { "--grid-columns": `${value}` },
210
+ }),
211
+ },
212
+ }),
213
+ );
214
+ const props = component({ columns: 3 });
215
+ expect(getStyleClass(props)).toEqual({
216
+ class: cls("grid-cols-3"),
217
+ "--grid-columns": "3",
218
+ });
219
+ });
220
+
221
+ test("computedVariants with number type returns dynamic styles", () => {
222
+ const component = getModeComponent(
223
+ mode,
224
+ cv({
225
+ computedVariants: {
226
+ gap: (value: number) => ({
227
+ style: { "--gap": `${value * 4}px` },
228
+ }),
229
+ padding: (value: number) => ({
230
+ style: {
231
+ "--padding-x": `${value}px`,
232
+ "--padding-y": `${value * 0.5}px`,
233
+ },
234
+ }),
235
+ },
236
+ }),
237
+ );
238
+ const props = component({ gap: 4, padding: 16 });
239
+ expect(getStyleClass(props)).toEqual({
240
+ class: "",
241
+ "--gap": "16px",
242
+ "--padding-x": "16px",
243
+ "--padding-y": "8px",
244
+ });
245
+ });
246
+
247
+ test("computedVariants with nullable type", () => {
248
+ const component = getModeComponent(
249
+ mode,
250
+ cv({
251
+ computedVariants: {
252
+ color: (value: string | null) => ({
253
+ class: value ? `color-${value}` : "color-default",
254
+ style: { "--color": value ?? "inherit" },
255
+ }),
256
+ },
257
+ }),
258
+ );
259
+ const propsWithValue = component({ color: "red" });
260
+ expect(getStyleClass(propsWithValue)).toEqual({
261
+ class: cls("color-red"),
262
+ "--color": "red",
263
+ });
264
+ const propsWithNull = component({ color: null });
265
+ expect(getStyleClass(propsWithNull)).toEqual({
266
+ class: cls("color-default"),
267
+ "--color": "inherit",
268
+ });
269
+ });
270
+
271
+ test("computedVariants changes extended variant from string to number", () => {
272
+ const base = cv({
273
+ variants: { size: { sm: "text-sm", md: "text-md", lg: "text-lg" } },
274
+ });
275
+ const component = getModeComponent(
276
+ mode,
277
+ cv({
278
+ extend: [base],
279
+ computedVariants: {
280
+ size: (value: number) => ({
281
+ class: "text-custom",
282
+ style: { fontSize: `${value}px` },
283
+ }),
284
+ },
285
+ }),
286
+ );
287
+ component({
288
+ // @ts-expect-error
289
+ size: "sm",
290
+ });
291
+ const props = component({ size: 18 });
292
+ expect(getStyleClass(props)).toEqual({
293
+ class: cls("text-custom"),
294
+ fontSize: "18px",
295
+ });
296
+ });
297
+ });
298
+ }
@@ -0,0 +1,301 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ CONFIGS,
4
+ createCVFromConfig,
5
+ getConfigDescription,
6
+ getConfigMode,
7
+ getConfigTransformClass,
8
+ getModeComponent,
9
+ getStyleClass,
10
+ } from "./_utils.ts";
11
+
12
+ for (const config of Object.values(CONFIGS)) {
13
+ const mode = getConfigMode(config);
14
+ const cv = createCVFromConfig(config);
15
+ const cls = getConfigTransformClass(config);
16
+
17
+ describe(getConfigDescription(config), () => {
18
+ test("extend single component", () => {
19
+ const base = cv({ class: "base", variants: { size: { sm: "sm" } } });
20
+ const component = getModeComponent(
21
+ mode,
22
+ cv({ extend: [base], class: "extended" }),
23
+ );
24
+ const props = component({ size: "sm" });
25
+ expect(getStyleClass(props)).toEqual({ class: cls("base extended sm") });
26
+ });
27
+
28
+ test("extend multiple components", () => {
29
+ const base1 = cv({ class: "base1" });
30
+ const base2 = cv({ class: "base2" });
31
+ const component = getModeComponent(
32
+ mode,
33
+ cv({ extend: [base1, base2], class: "extended" }),
34
+ );
35
+ const props = component();
36
+ expect(getStyleClass(props)).toEqual({
37
+ class: cls("base1 base2 extended"),
38
+ });
39
+ });
40
+
41
+ test("extend with variant merging", () => {
42
+ const base = cv({ variants: { size: { sm: "base-sm", lg: "base-lg" } } });
43
+ const component = getModeComponent(
44
+ mode,
45
+ cv({ extend: [base], variants: { size: { sm: "extended-sm" } } }),
46
+ );
47
+ const props = component({ size: "sm" });
48
+ expect(getStyleClass(props)).toEqual({
49
+ class: cls("base-sm extended-sm"),
50
+ });
51
+ });
52
+
53
+ test("extend with variant merging setting base variant", () => {
54
+ const base = cv({ variants: { size: { sm: "base-sm", lg: "base-lg" } } });
55
+ const component = getModeComponent(
56
+ mode,
57
+ cv({
58
+ extend: [base],
59
+ variants: { size: { sm: "extended-sm" } },
60
+ }),
61
+ );
62
+ const props = component({ size: "lg" });
63
+ expect(getStyleClass(props)).toEqual({ class: cls("base-lg") });
64
+ });
65
+
66
+ test("extend can disable whole variant with null", () => {
67
+ const base = cv({
68
+ variants: { size: { sm: "base-sm", lg: "base-lg" } },
69
+ defaultVariants: { size: "sm" },
70
+ });
71
+ const component = getModeComponent(
72
+ mode,
73
+ cv({
74
+ extend: [base],
75
+ variants: { size: null },
76
+ defaultVariants: {
77
+ // @ts-expect-error disabled variant cannot be set
78
+ size:
79
+ // no error
80
+ "lg",
81
+ },
82
+ }),
83
+ );
84
+ const props = component({
85
+ // @ts-expect-error disabled variant cannot be set
86
+ size:
87
+ // no error
88
+ "lg",
89
+ });
90
+ expect(getStyleClass(props)).toEqual({ class: "" });
91
+ });
92
+
93
+ test("extend can disable variant value with null", () => {
94
+ const base = cv({
95
+ variants: { size: { sm: "base-sm", lg: "base-lg" } },
96
+ defaultVariants: { size: "sm" },
97
+ });
98
+ const component = getModeComponent(
99
+ mode,
100
+ cv({
101
+ extend: [base],
102
+ variants: { size: { sm: null } },
103
+ defaultVariants: {
104
+ // @ts-expect-error disabled variant value cannot be set
105
+ size:
106
+ // no error
107
+ "sm",
108
+ },
109
+ }),
110
+ );
111
+ const disabledProps = component({
112
+ // @ts-expect-error disabled variant value cannot be set
113
+ size:
114
+ // no error
115
+ "sm",
116
+ });
117
+ expect(getStyleClass(disabledProps)).toEqual({ class: "" });
118
+ const enabledProps = component({ size: "lg" });
119
+ expect(getStyleClass(enabledProps)).toEqual({ class: cls("base-lg") });
120
+ });
121
+
122
+ test("extend disabled variant value accepts valid defaultVariants", () => {
123
+ const base = cv({
124
+ variants: {
125
+ size: {
126
+ sm: { class: "base-sm", style: { fontSize: "12px" } },
127
+ lg: { class: "base-lg", style: { fontSize: "16px" } },
128
+ },
129
+ },
130
+ });
131
+ const component = getModeComponent(
132
+ mode,
133
+ cv({
134
+ extend: [base],
135
+ variants: { size: { sm: null } },
136
+ defaultVariants: { size: "lg" },
137
+ }),
138
+ );
139
+ const props = component();
140
+ expect(getStyleClass(props)).toEqual({
141
+ class: cls("base-lg"),
142
+ fontSize: "16px",
143
+ });
144
+ });
145
+
146
+ test("extend disabled variant value with computed setDefaultVariants", () => {
147
+ const base = cv({
148
+ variants: {
149
+ size: {
150
+ sm: { class: "base-sm", style: { fontSize: "12px" } },
151
+ lg: { class: "base-lg", style: { fontSize: "16px" } },
152
+ },
153
+ },
154
+ });
155
+ const validComponent = getModeComponent(
156
+ mode,
157
+ cv({
158
+ extend: [base],
159
+ variants: { size: { sm: null } },
160
+ computed: ({ setDefaultVariants }) => {
161
+ setDefaultVariants({ size: "lg" });
162
+ },
163
+ }),
164
+ );
165
+ expect(getStyleClass(validComponent())).toEqual({
166
+ class: cls("base-lg"),
167
+ fontSize: "16px",
168
+ });
169
+
170
+ const invalidComponent = getModeComponent(
171
+ mode,
172
+ cv({
173
+ extend: [base],
174
+ variants: { size: { sm: null } },
175
+ computed: ({ setDefaultVariants }) => {
176
+ setDefaultVariants({
177
+ // @ts-expect-error disabled variant value cannot be set
178
+ size:
179
+ // no error
180
+ "sm",
181
+ });
182
+ },
183
+ }),
184
+ );
185
+ expect(getStyleClass(invalidComponent())).toEqual({ class: "" });
186
+ });
187
+
188
+ test("extend disabled variant value with computed setVariants", () => {
189
+ const base = cv({
190
+ variants: {
191
+ size: {
192
+ sm: { class: "base-sm", style: { fontSize: "12px" } },
193
+ lg: { class: "base-lg", style: { fontSize: "16px" } },
194
+ },
195
+ },
196
+ });
197
+ const validComponent = getModeComponent(
198
+ mode,
199
+ cv({
200
+ extend: [base],
201
+ variants: { size: { sm: null } },
202
+ computed: ({ setVariants }) => {
203
+ setVariants({ size: "lg" });
204
+ },
205
+ }),
206
+ );
207
+ expect(getStyleClass(validComponent())).toEqual({
208
+ class: cls("base-lg"),
209
+ fontSize: "16px",
210
+ });
211
+
212
+ const invalidComponent = getModeComponent(
213
+ mode,
214
+ cv({
215
+ extend: [base],
216
+ variants: { size: { sm: null } },
217
+ computed: ({ setVariants }) => {
218
+ setVariants({
219
+ // @ts-expect-error disabled variant value cannot be set
220
+ size:
221
+ // no error
222
+ "sm",
223
+ });
224
+ },
225
+ }),
226
+ );
227
+ expect(getStyleClass(invalidComponent())).toEqual({ class: "" });
228
+ });
229
+
230
+ test("extend inherits defaultVariants", () => {
231
+ const base = cv({
232
+ variants: { size: { sm: "sm", lg: "lg" } },
233
+ defaultVariants: { size: "sm" },
234
+ });
235
+ const component = getModeComponent(mode, cv({ extend: [base] }));
236
+ const props = component();
237
+ expect(getStyleClass(props)).toEqual({ class: cls("sm") });
238
+ });
239
+
240
+ test("extend override defaultVariants", () => {
241
+ const base = cv({
242
+ variants: { size: { sm: "sm", lg: "lg" } },
243
+ defaultVariants: { size: "sm" },
244
+ });
245
+ const component = getModeComponent(
246
+ mode,
247
+ cv({ extend: [base], defaultVariants: { size: "lg" } }),
248
+ );
249
+ const props = component();
250
+ expect(getStyleClass(props)).toEqual({ class: cls("lg") });
251
+ });
252
+
253
+ test("extend jsx modal component preserves style", () => {
254
+ const base = cv({
255
+ class: "base",
256
+ style: { backgroundColor: "red" },
257
+ });
258
+ const component = getModeComponent(
259
+ mode,
260
+ cv({ extend: [base.jsx], class: "extended" }),
261
+ );
262
+ const props = component();
263
+ expect(getStyleClass(props)).toEqual({
264
+ class: cls("base extended"),
265
+ backgroundColor: "red",
266
+ });
267
+ });
268
+
269
+ test("extend html modal component preserves style", () => {
270
+ const base = cv({
271
+ class: "base",
272
+ style: { backgroundColor: "red" },
273
+ });
274
+ const component = getModeComponent(
275
+ mode,
276
+ cv({ extend: [base.html], class: "extended" }),
277
+ );
278
+ const props = component();
279
+ expect(getStyleClass(props)).toEqual({
280
+ class: cls("base extended"),
281
+ backgroundColor: "red",
282
+ });
283
+ });
284
+
285
+ test("extend htmlObj modal component preserves style", () => {
286
+ const base = cv({
287
+ class: "base",
288
+ style: { backgroundColor: "red" },
289
+ });
290
+ const component = getModeComponent(
291
+ mode,
292
+ cv({ extend: [base.htmlObj], class: "extended" }),
293
+ );
294
+ const props = component();
295
+ expect(getStyleClass(props)).toEqual({
296
+ class: cls("base extended"),
297
+ backgroundColor: "red",
298
+ });
299
+ });
300
+ });
301
+ }
@@ -10,8 +10,9 @@ import { fileURLToPath } from "node:url";
10
10
  import ts from "typescript";
11
11
  import { afterEach, describe, expect, test } from "vitest";
12
12
 
13
- const sourceDir = dirname(fileURLToPath(import.meta.url));
14
- const packageDir = resolve(sourceDir, "..");
13
+ const testDir = dirname(fileURLToPath(import.meta.url));
14
+ const packageDir = resolve(testDir, "..");
15
+ const sourceDir = resolve(packageDir, "src");
15
16
  const workspaceDir = resolve(packageDir, "../..");
16
17
  const tempDirs: string[] = [];
17
18
 
@@ -26,6 +27,9 @@ function createCompilerOptions(): ts.CompilerOptions {
26
27
  target: ts.ScriptTarget.ES2020,
27
28
  module: ts.ModuleKind.NodeNext,
28
29
  moduleResolution: ts.ModuleResolutionKind.NodeNext,
30
+ // Match the repo's Node-aware TS environment so language-service
31
+ // navigation in the fixture reflects real editor behavior.
32
+ types: ["node"],
29
33
  allowImportingTsExtensions: true,
30
34
  strict: true,
31
35
  skipLibCheck: true,
@@ -1,7 +1,7 @@
1
1
  import type { CSSProperties, ComponentProps } from "react";
2
2
  import { expect, expectTypeOf, test } from "vitest";
3
- import { type VariantProps, cv, splitProps } from "./index.ts";
4
- import type { JSXProps } from "./types.ts";
3
+ import { type VariantProps, cv, splitProps } from "../src/index.ts";
4
+ import type { JSXProps } from "../src/types.ts";
5
5
 
6
6
  test("splitProps", () => {
7
7
  const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
@@ -30,7 +30,7 @@ test("component props", () => {
30
30
  const component = cv({
31
31
  style: { fontSize: "16px" },
32
32
  variants: { size: { sm: "sm", md: "md" } },
33
- });
33
+ }).jsx;
34
34
  const props = component({ size: "sm", className: "custom" });
35
35
  expectTypeOf(props).toEqualTypeOf<JSXProps>();
36
36
  expect(props).toEqual({
@@ -1,9 +1,7 @@
1
1
  import type { ComponentProps, JSX } from "solid-js";
2
2
  import { expect, expectTypeOf, test } from "vitest";
3
- import { type VariantProps, create, splitProps } from "./index.ts";
4
- import { type HTMLObjProps } from "./types.ts";
5
-
6
- const { cv } = create({ defaultMode: "htmlObj" });
3
+ import { type VariantProps, cv, splitProps } from "../src/index.ts";
4
+ import { type HTMLObjProps } from "../src/types.ts";
7
5
 
8
6
  test("splitProps", () => {
9
7
  const component = cv({ variants: { size: { sm: "sm", md: "md" } } });
@@ -26,7 +24,7 @@ test("component props", () => {
26
24
  const component = cv({
27
25
  style: { fontSize: "16px" },
28
26
  variants: { size: { sm: "sm", md: "md" } },
29
- });
27
+ }).htmlObj;
30
28
  const props = component({ size: "sm", className: "custom" });
31
29
  expectTypeOf(props).toEqualTypeOf<HTMLObjProps>();
32
30
  expect(props).toEqual({