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.
- package/CHANGELOG.md +39 -0
- package/dist/index.d.ts +13 -30
- package/dist/index.js +569 -370
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/perfs/component.bench.ts +233 -0
- package/src/index.ts +759 -609
- package/src/types.ts +13 -4
- package/src/utils.ts +100 -32
- package/tests/_utils.ts +176 -0
- package/tests/class-style-test.ts +341 -0
- package/tests/component-api-test.ts +214 -0
- package/tests/computed-test.ts +800 -0
- package/tests/computed-variants-test.ts +298 -0
- package/tests/extend-test.ts +301 -0
- package/{src/test-language-service.ts → tests/language-service-test.ts} +6 -2
- package/{src/test-react.ts → tests/react-test.ts} +3 -3
- package/{src/test-solid.ts → tests/solid-test.ts} +3 -5
- package/tests/split-props-test.ts +590 -0
- package/tests/variants-test.ts +380 -0
- package/tsconfig.json +1 -1
- package/src/test.ts +0 -2873
package/src/types.ts
CHANGED
|
@@ -32,13 +32,22 @@ export interface HTMLObjProps {
|
|
|
32
32
|
style: HTMLCSSProperties;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export interface StyleClassProps {
|
|
36
|
+
class: string;
|
|
37
|
+
style: StyleValue;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
export interface StyleProps {
|
|
36
41
|
jsx: JSXProps;
|
|
37
42
|
html: HTMLProps;
|
|
38
43
|
htmlObj: HTMLObjProps;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
export type ComponentResult =
|
|
46
|
+
export type ComponentResult =
|
|
47
|
+
| JSXProps
|
|
48
|
+
| HTMLProps
|
|
49
|
+
| HTMLObjProps
|
|
50
|
+
| StyleClassProps;
|
|
42
51
|
|
|
43
52
|
type AllComponentResultKeys =
|
|
44
53
|
| keyof JSXProps
|
|
@@ -160,16 +169,16 @@ export interface ModalComponent<V, R extends ComponentResult> {
|
|
|
160
169
|
class: (props?: ComponentProps<V>) => string;
|
|
161
170
|
style: (props?: ComponentProps<V>) => R["style"];
|
|
162
171
|
getVariants: GetVariants<V>;
|
|
163
|
-
keys: (keyof V | keyof
|
|
172
|
+
keys: (keyof V | keyof NullableComponentResult)[];
|
|
164
173
|
variantKeys: (keyof V)[];
|
|
165
|
-
propKeys: (keyof V | keyof
|
|
174
|
+
propKeys: (keyof V | keyof NullableComponentResult)[];
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
export interface CVComponent<
|
|
169
178
|
V extends Variants = {},
|
|
170
179
|
CV extends ComputedVariants = {},
|
|
171
180
|
E extends AnyComponent[] = [],
|
|
172
|
-
R extends ComponentResult =
|
|
181
|
+
R extends ComponentResult = StyleClassProps,
|
|
173
182
|
> extends ModalComponent<MergeVariants<V, CV, E>, R> {
|
|
174
183
|
jsx: ModalComponent<MergeVariants<V, CV, E>, JSXProps>;
|
|
175
184
|
html: ModalComponent<MergeVariants<V, CV, E>, HTMLProps>;
|
package/src/utils.ts
CHANGED
|
@@ -8,6 +8,9 @@ import type {
|
|
|
8
8
|
export const MODES = ["jsx", "html", "htmlObj"] as const;
|
|
9
9
|
export type Mode = (typeof MODES)[number];
|
|
10
10
|
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
12
|
+
const hasOwn = Object.prototype.hasOwnProperty;
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* Returns the appropriate class property name based on the mode.
|
|
13
16
|
* @example
|
|
@@ -26,9 +29,11 @@ export function getClassPropertyName(mode: Mode) {
|
|
|
26
29
|
*/
|
|
27
30
|
export function hyphenToCamel(str: string) {
|
|
28
31
|
// CSS custom properties (variables) should not be converted
|
|
29
|
-
if (str.
|
|
32
|
+
if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
|
|
30
33
|
return str;
|
|
31
34
|
}
|
|
35
|
+
// Fast path: no hyphen -> return as-is
|
|
36
|
+
if (str.indexOf("-") === -1) return str;
|
|
32
37
|
return str.replace(/-([a-z])/gi, (_, letter) => letter.toUpperCase());
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -40,7 +45,7 @@ export function hyphenToCamel(str: string) {
|
|
|
40
45
|
*/
|
|
41
46
|
export function camelToHyphen(str: string) {
|
|
42
47
|
// CSS custom properties (variables) should not be converted
|
|
43
|
-
if (str.
|
|
48
|
+
if (str.length >= 2 && str.charCodeAt(0) === 45 && str.charCodeAt(1) === 45) {
|
|
44
49
|
return str;
|
|
45
50
|
}
|
|
46
51
|
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
@@ -67,20 +72,60 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
67
72
|
if (!styleString) return {};
|
|
68
73
|
|
|
69
74
|
const result: StyleValue = {};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
const len = styleString.length;
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < len) {
|
|
78
|
+
// Skip leading whitespace and stray semicolons
|
|
79
|
+
while (i < len) {
|
|
80
|
+
const c = styleString.charCodeAt(i);
|
|
81
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13 && c !== 59) break;
|
|
82
|
+
i++;
|
|
83
|
+
}
|
|
84
|
+
if (i >= len) break;
|
|
85
|
+
// Read property name until ':' or ';'
|
|
86
|
+
const propStart = i;
|
|
87
|
+
while (i < len) {
|
|
88
|
+
const c = styleString.charCodeAt(i);
|
|
89
|
+
if (c === 58 || c === 59) break;
|
|
90
|
+
i++;
|
|
91
|
+
}
|
|
92
|
+
if (i >= len || styleString.charCodeAt(i) === 59) {
|
|
93
|
+
// No colon found - skip this declaration
|
|
94
|
+
if (i < len) i++; // skip ';'
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let propEnd = i;
|
|
98
|
+
// Trim trailing whitespace from property name
|
|
99
|
+
while (propEnd > propStart) {
|
|
100
|
+
const c = styleString.charCodeAt(propEnd - 1);
|
|
101
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
102
|
+
propEnd--;
|
|
103
|
+
}
|
|
104
|
+
if (propEnd === propStart) {
|
|
105
|
+
// Empty property - skip
|
|
106
|
+
while (i < len && styleString.charCodeAt(i) !== 59) i++;
|
|
107
|
+
if (i < len) i++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const property = styleString.slice(propStart, propEnd);
|
|
111
|
+
i++; // skip ':'
|
|
112
|
+
// Skip whitespace before value
|
|
113
|
+
while (i < len) {
|
|
114
|
+
const c = styleString.charCodeAt(i);
|
|
115
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
const valStart = i;
|
|
119
|
+
while (i < len && styleString.charCodeAt(i) !== 59) i++;
|
|
120
|
+
let valEnd = i;
|
|
121
|
+
while (valEnd > valStart) {
|
|
122
|
+
const c = styleString.charCodeAt(valEnd - 1);
|
|
123
|
+
if (c !== 32 && c !== 9 && c !== 10 && c !== 13) break;
|
|
124
|
+
valEnd--;
|
|
125
|
+
}
|
|
126
|
+
if (i < len) i++; // skip ';'
|
|
127
|
+
if (valEnd === valStart) continue;
|
|
128
|
+
const value = styleString.slice(valStart, valEnd);
|
|
84
129
|
// CSS property names and values are dynamic - cast required for index access
|
|
85
130
|
(result as Record<string, string>)[hyphenToCamel(property)] = value;
|
|
86
131
|
}
|
|
@@ -96,11 +141,14 @@ export function htmlStyleToStyleValue(styleString: string) {
|
|
|
96
141
|
*/
|
|
97
142
|
export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
|
|
98
143
|
const result: StyleValue = {};
|
|
99
|
-
for (const
|
|
144
|
+
for (const key in style) {
|
|
145
|
+
if (!hasOwn.call(style, key)) continue;
|
|
146
|
+
const value = (style as Record<string, unknown>)[key];
|
|
100
147
|
if (value == null) continue;
|
|
101
148
|
// CSS property names and values are dynamic - cast required for index access
|
|
102
|
-
(result as Record<string, string>)[hyphenToCamel(key)] =
|
|
103
|
-
|
|
149
|
+
(result as Record<string, string>)[hyphenToCamel(key)] = parseLengthValue(
|
|
150
|
+
value as string | number,
|
|
151
|
+
);
|
|
104
152
|
}
|
|
105
153
|
return result;
|
|
106
154
|
}
|
|
@@ -113,10 +161,14 @@ export function htmlObjStyleToStyleValue(style: HTMLCSSProperties) {
|
|
|
113
161
|
*/
|
|
114
162
|
export function jsxStyleToStyleValue(style: JSXCSSProperties) {
|
|
115
163
|
const result: StyleValue = {};
|
|
116
|
-
for (const
|
|
164
|
+
for (const key in style) {
|
|
165
|
+
if (!hasOwn.call(style, key)) continue;
|
|
166
|
+
const value = (style as Record<string, unknown>)[key];
|
|
117
167
|
if (value == null) continue;
|
|
118
168
|
// CSS property names and values are dynamic - cast required for index access
|
|
119
|
-
(result as Record<string, string>)[key] = parseLengthValue(
|
|
169
|
+
(result as Record<string, string>)[key] = parseLengthValue(
|
|
170
|
+
value as string | number,
|
|
171
|
+
);
|
|
120
172
|
}
|
|
121
173
|
return result;
|
|
122
174
|
}
|
|
@@ -128,13 +180,18 @@ export function jsxStyleToStyleValue(style: JSXCSSProperties) {
|
|
|
128
180
|
* // "background-color: red; font-size: 16px;"
|
|
129
181
|
*/
|
|
130
182
|
export function styleValueToHTMLStyle(style: StyleValue): string {
|
|
131
|
-
|
|
132
|
-
for (const
|
|
183
|
+
let result = "";
|
|
184
|
+
for (const key in style) {
|
|
185
|
+
if (!hasOwn.call(style, key)) continue;
|
|
186
|
+
const value = (style as Record<string, unknown>)[key];
|
|
133
187
|
if (value == null) continue;
|
|
134
|
-
|
|
188
|
+
if (result) result += "; ";
|
|
189
|
+
result += camelToHyphen(key);
|
|
190
|
+
result += ": ";
|
|
191
|
+
result += value as string | number;
|
|
135
192
|
}
|
|
136
|
-
if (!
|
|
137
|
-
return `${
|
|
193
|
+
if (!result) return "";
|
|
194
|
+
return `${result};`;
|
|
138
195
|
}
|
|
139
196
|
|
|
140
197
|
/**
|
|
@@ -145,10 +202,11 @@ export function styleValueToHTMLStyle(style: StyleValue): string {
|
|
|
145
202
|
*/
|
|
146
203
|
export function styleValueToHTMLObjStyle(style: StyleValue) {
|
|
147
204
|
const result: CSS.PropertiesHyphen = {};
|
|
148
|
-
for (const
|
|
205
|
+
for (const key in style) {
|
|
206
|
+
if (!hasOwn.call(style, key)) continue;
|
|
207
|
+
const value = (style as Record<string, unknown>)[key];
|
|
149
208
|
if (value == null) continue;
|
|
150
|
-
|
|
151
|
-
result[property] = value;
|
|
209
|
+
(result as Record<string, unknown>)[camelToHyphen(key)] = value;
|
|
152
210
|
}
|
|
153
211
|
return result;
|
|
154
212
|
}
|
|
@@ -172,7 +230,17 @@ export function styleValueToJSXStyle(style: StyleValue) {
|
|
|
172
230
|
export function isHTMLObjStyle(
|
|
173
231
|
style: CSS.Properties<any> | CSS.PropertiesHyphen<any>,
|
|
174
232
|
): style is CSS.PropertiesHyphen {
|
|
175
|
-
|
|
176
|
-
(
|
|
177
|
-
|
|
233
|
+
for (const key in style) {
|
|
234
|
+
if (!hasOwn.call(style, key)) continue;
|
|
235
|
+
// Quick exclusion of CSS custom properties (--foo)
|
|
236
|
+
if (
|
|
237
|
+
key.length >= 2 &&
|
|
238
|
+
key.charCodeAt(0) === 45 &&
|
|
239
|
+
key.charCodeAt(1) === 45
|
|
240
|
+
) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (key.indexOf("-") !== -1) return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
178
246
|
}
|
package/tests/_utils.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { expect } from "vitest";
|
|
2
|
+
import { type VariantProps, create, cv as cvBase } from "../src/index.ts";
|
|
3
|
+
import type {
|
|
4
|
+
AnyComponent,
|
|
5
|
+
CVComponent,
|
|
6
|
+
ComponentResult,
|
|
7
|
+
ComputedVariants,
|
|
8
|
+
HTMLObjProps,
|
|
9
|
+
HTMLProps,
|
|
10
|
+
JSXProps,
|
|
11
|
+
StyleClassProps,
|
|
12
|
+
StyleProperty,
|
|
13
|
+
Variants,
|
|
14
|
+
} from "../src/types.ts";
|
|
15
|
+
import {
|
|
16
|
+
htmlObjStyleToStyleValue,
|
|
17
|
+
htmlStyleToStyleValue,
|
|
18
|
+
isHTMLObjStyle,
|
|
19
|
+
jsxStyleToStyleValue,
|
|
20
|
+
} from "../src/utils.ts";
|
|
21
|
+
|
|
22
|
+
const MODES = ["jsx", "html", "htmlObj"] as const;
|
|
23
|
+
type Mode = (typeof MODES)[number] | null;
|
|
24
|
+
|
|
25
|
+
export type HTMLProperties<T extends AnyComponent> = VariantProps<T> & {
|
|
26
|
+
id?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
style?: StyleProperty;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type ConfigParams = NonNullable<Parameters<typeof create>[0]>;
|
|
33
|
+
|
|
34
|
+
interface Config {
|
|
35
|
+
mode: Mode;
|
|
36
|
+
transformClass?: ConfigParams["transformClass"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const transformClass = {
|
|
40
|
+
uppercase: (className: string) => className.toUpperCase(),
|
|
41
|
+
} satisfies Record<string, Config["transformClass"]>;
|
|
42
|
+
|
|
43
|
+
export const CONFIGS = {
|
|
44
|
+
default: { mode: null },
|
|
45
|
+
jsx: { mode: "jsx" },
|
|
46
|
+
html: { mode: "html" },
|
|
47
|
+
htmlObj: { mode: "htmlObj" },
|
|
48
|
+
uppercase: {
|
|
49
|
+
mode: null,
|
|
50
|
+
transformClass: transformClass.uppercase,
|
|
51
|
+
},
|
|
52
|
+
} satisfies Record<string, Config>;
|
|
53
|
+
|
|
54
|
+
export function getConfigMode<T extends Config>(config: T): T["mode"] {
|
|
55
|
+
if (!("mode" in config)) return null;
|
|
56
|
+
return config.mode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getConfigTransformClass(config: Config) {
|
|
60
|
+
if (!("transformClass" in config) || !config.transformClass) {
|
|
61
|
+
return (className: string) => className;
|
|
62
|
+
}
|
|
63
|
+
return config.transformClass;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getConfigDescription(config: Config) {
|
|
67
|
+
for (const [name, currentConfig] of Object.entries(CONFIGS)) {
|
|
68
|
+
if (currentConfig !== config) continue;
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
return "custom";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createCVFromConfig(
|
|
75
|
+
config: Config,
|
|
76
|
+
): ReturnType<typeof create>["cv"] {
|
|
77
|
+
const transformClass = getConfigTransformClass(config);
|
|
78
|
+
const hasTransform = "transformClass" in config && config.transformClass;
|
|
79
|
+
if (!hasTransform) {
|
|
80
|
+
return cvBase;
|
|
81
|
+
}
|
|
82
|
+
const { cv } = create({ transformClass });
|
|
83
|
+
return cv;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getModeComponent<
|
|
87
|
+
M extends Mode,
|
|
88
|
+
V extends Variants = {},
|
|
89
|
+
CV extends ComputedVariants = {},
|
|
90
|
+
const E extends AnyComponent[] = [],
|
|
91
|
+
>(mode: M, component: CVComponent<V, CV, E>) {
|
|
92
|
+
if (!mode) return component;
|
|
93
|
+
return component[mode];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getClass(props: ComponentResult) {
|
|
97
|
+
if ("class" in props) return props.class;
|
|
98
|
+
return props.className;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getClassPropertyName(config: Config) {
|
|
102
|
+
const mode = config.mode;
|
|
103
|
+
if (mode === "jsx" || mode === null) return "className";
|
|
104
|
+
return "class";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getExpectedPropsKeys(config: Config, ...variantKeys: string[]) {
|
|
108
|
+
if (config.mode === null) {
|
|
109
|
+
return ["class", "className", "style", ...variantKeys];
|
|
110
|
+
}
|
|
111
|
+
return [getClassPropertyName(config), "style", ...variantKeys];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function assertDefaultProps(
|
|
115
|
+
props: ComponentResult,
|
|
116
|
+
): asserts props is StyleClassProps {
|
|
117
|
+
if (!("class" in props)) {
|
|
118
|
+
expect.fail("Expected default props to have class");
|
|
119
|
+
}
|
|
120
|
+
if (!("style" in props) || typeof props.style !== "object") {
|
|
121
|
+
expect.fail("Expected default props to have style");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function assertJSXProps(
|
|
126
|
+
props: ComponentResult,
|
|
127
|
+
): asserts props is JSXProps {
|
|
128
|
+
if (!("className" in props)) {
|
|
129
|
+
expect.fail("Expected jsx props to have className");
|
|
130
|
+
}
|
|
131
|
+
if (!("style" in props) || typeof props.style !== "object") {
|
|
132
|
+
expect.fail("Expected jsx props to have style");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function assertHTMLProps(
|
|
137
|
+
props: ComponentResult,
|
|
138
|
+
): asserts props is HTMLProps {
|
|
139
|
+
if (!("class" in props)) {
|
|
140
|
+
expect.fail("Expected html props to have class");
|
|
141
|
+
}
|
|
142
|
+
if (!("style" in props) || typeof props.style !== "string") {
|
|
143
|
+
expect.fail("Expected html props to have style");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function assertHTMLObjProps(
|
|
148
|
+
props: ComponentResult,
|
|
149
|
+
): asserts props is HTMLObjProps {
|
|
150
|
+
if (!("class" in props)) {
|
|
151
|
+
expect.fail("Expected htmlObj props to have class");
|
|
152
|
+
}
|
|
153
|
+
if (!("style" in props) || typeof props.style !== "object") {
|
|
154
|
+
expect.fail("Expected htmlObj props to have style");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getStyle(props: Pick<ComponentResult, "style">) {
|
|
159
|
+
if (typeof props.style === "string") {
|
|
160
|
+
return htmlStyleToStyleValue(props.style);
|
|
161
|
+
}
|
|
162
|
+
if (typeof props.style === "object") {
|
|
163
|
+
if (isHTMLObjStyle(props.style)) {
|
|
164
|
+
return htmlObjStyleToStyleValue(props.style);
|
|
165
|
+
}
|
|
166
|
+
return jsxStyleToStyleValue(props.style);
|
|
167
|
+
}
|
|
168
|
+
return {};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function getStyleClass(props: ComponentResult): Record<string, unknown> {
|
|
172
|
+
return {
|
|
173
|
+
...getStyle(props),
|
|
174
|
+
class: getClass(props),
|
|
175
|
+
};
|
|
176
|
+
}
|