@ntatoud/styra 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.
- package/README.md +57 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.mjs +133 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# styra
|
|
2
|
+
|
|
3
|
+
Type-safe class variance builder — a maintained, boosted CVA replacement.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { styra } from "styra";
|
|
9
|
+
|
|
10
|
+
const button = styra("btn")
|
|
11
|
+
.variants({
|
|
12
|
+
size: { sm: "text-sm", md: "text-md", lg: "text-lg" },
|
|
13
|
+
color: { red: "bg-red", blue: "bg-blue" },
|
|
14
|
+
})
|
|
15
|
+
.defaults({ size: "md" })
|
|
16
|
+
.compound([{ size: "sm", color: "red", class: "ring-red" }]);
|
|
17
|
+
|
|
18
|
+
button({ color: "red" }); // 'btn text-md bg-red'
|
|
19
|
+
button({ size: "sm", color: "red" }); // 'btn text-sm bg-red ring-red'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### With a custom merge function (e.g. `tailwind-merge`)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createStyra } from "styra";
|
|
26
|
+
import { twMerge } from "tailwind-merge";
|
|
27
|
+
|
|
28
|
+
export const { styra } = createStyra({ merge: twMerge });
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Negation in compound rules
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const btn = styra("btn")
|
|
35
|
+
.variants({ size: { sm: "text-sm", lg: "text-lg" }, disabled: { yes: "opacity-50", no: "" } })
|
|
36
|
+
.compound([{ disabled: { not: "yes" }, class: "hover:opacity-80" }]);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
- Install dependencies:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
vp install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Run the unit tests:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
vp test
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Build the library:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
vp pack
|
|
57
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type MergeFn = (...classes: string[]) => string;
|
|
3
|
+
/**
|
|
4
|
+
* A map of variant keys to their possible values and classes.
|
|
5
|
+
* Values can be a `Record<string, string>` (explicit map) or a plain `string`
|
|
6
|
+
* (boolean shorthand — applied when the prop is `true`, skipped when `false`/`undefined`).
|
|
7
|
+
*/
|
|
8
|
+
type VariantMap = Record<string, Record<string, string> | string>;
|
|
9
|
+
/** Resolve the call-site prop type for a single variant value definition. */
|
|
10
|
+
type PropTypeOf<V> = V extends string ? boolean : keyof V;
|
|
11
|
+
/** Partial defaults for a given variant map. */
|
|
12
|
+
type DefaultsOf<V extends VariantMap> = Partial<{ [K in keyof V]: PropTypeOf<V[K]> }>;
|
|
13
|
+
/** A single negation condition. */
|
|
14
|
+
type Not<T> = {
|
|
15
|
+
not: T;
|
|
16
|
+
};
|
|
17
|
+
/** Per-key condition in a compound rule: exact value or negation. */
|
|
18
|
+
type CompoundCondition<V extends VariantMap> = { [K in keyof V]?: PropTypeOf<V[K]> | Not<PropTypeOf<V[K]>> };
|
|
19
|
+
/** A compound variant rule: conditions + the class to apply when they match. */
|
|
20
|
+
type CompoundRule<V extends VariantMap> = CompoundCondition<V> & {
|
|
21
|
+
class: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Infer call-site props from a variant map and its defaults.
|
|
25
|
+
* - Variants with a default → optional
|
|
26
|
+
* - Variants without a default → required
|
|
27
|
+
*/
|
|
28
|
+
type InferProps<V extends VariantMap, D extends DefaultsOf<V>> = { [K in keyof V as K extends keyof D ? never : K]: PropTypeOf<V[K]> } & { [K in keyof V as K extends keyof D ? K : never]?: PropTypeOf<V[K]> } & {
|
|
29
|
+
class?: string;
|
|
30
|
+
className?: string | ((...args: never[]) => string | undefined);
|
|
31
|
+
};
|
|
32
|
+
/** A callable builder that also exposes `.variants()`, `.defaults()`, `.compound()`. */
|
|
33
|
+
interface StyraBuilder<V extends VariantMap, D extends DefaultsOf<V>> {
|
|
34
|
+
(props: keyof V extends never ? {
|
|
35
|
+
class?: string;
|
|
36
|
+
className?: string | ((...args: never[]) => string | undefined);
|
|
37
|
+
} : InferProps<V, D>): string;
|
|
38
|
+
/**
|
|
39
|
+
* Define variant keys and their class mappings.
|
|
40
|
+
* Can only be called once — throws at runtime if called again.
|
|
41
|
+
*/
|
|
42
|
+
variants<NV extends VariantMap>(v: NV): StyraBuilder<NV, Record<never, never>>;
|
|
43
|
+
/** Set default values for variants, making them optional at call-site. */
|
|
44
|
+
defaults<ND extends DefaultsOf<V>>(d: ND): StyraBuilder<V, ND>;
|
|
45
|
+
/** Add compound variant rules applied when multiple variant conditions are met. */
|
|
46
|
+
compound(rules: Array<CompoundRule<V>>): StyraBuilder<V, D>;
|
|
47
|
+
}
|
|
48
|
+
/** Custom class merge function, e.g. `twMerge` from tailwind-merge. */
|
|
49
|
+
interface StyraOptions {
|
|
50
|
+
merge?: MergeFn;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract the variant props from a `StyraBuilder`, stripping `class` and `className`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const buttonVariants = styra("btn").variants({ size: { sm: "...", md: "..." } });
|
|
58
|
+
* type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
59
|
+
* // → { size: "sm" | "md" }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
type VariantProps<T extends (...args: never[]) => string> = Omit<Parameters<T>[0], "class" | "className">;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/index.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Create a configured `styra` factory.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { createStyra } from 'styra'
|
|
71
|
+
* import { twMerge } from 'tailwind-merge'
|
|
72
|
+
*
|
|
73
|
+
* export const { styra } = createStyra({ merge: twMerge })
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function createStyra(options?: StyraOptions): {
|
|
77
|
+
styra: (base: string) => StyraBuilder<Record<never, never>, Record<never, never>>;
|
|
78
|
+
};
|
|
79
|
+
/** Default `styra` instance with no custom merge function. */
|
|
80
|
+
declare const styra: (base: string) => StyraBuilder<Record<never, never>, Record<never, never>>;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { type CompoundRule, type InferProps, type StyraBuilder, type StyraOptions, type VariantMap, type VariantProps, createStyra, styra };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
function matchesCompound(rule, resolved) {
|
|
3
|
+
for (const key in rule) {
|
|
4
|
+
if (key === "class") continue;
|
|
5
|
+
const rawCondition = rule[key];
|
|
6
|
+
const value = resolved[key];
|
|
7
|
+
if (rawCondition !== null && typeof rawCondition === "object" && "not" in rawCondition) {
|
|
8
|
+
if (value === (toKey(rawCondition.not) ?? rawCondition.not)) return false;
|
|
9
|
+
} else if (value !== (toKey(rawCondition) ?? rawCondition)) return false;
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
/** Resolve a className prop that may be a string or a render-prop function. */
|
|
14
|
+
function resolveClassName(v) {
|
|
15
|
+
if (typeof v === "string") return v || void 0;
|
|
16
|
+
if (typeof v === "function") return v() || void 0;
|
|
17
|
+
}
|
|
18
|
+
/** Coerce a prop value to its string key for map lookup (handles booleans from boolean shorthand). */
|
|
19
|
+
function toKey(v) {
|
|
20
|
+
if (v === void 0 || v === null) return void 0;
|
|
21
|
+
if (v === true) return "true";
|
|
22
|
+
if (v === false) return "false";
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
function makeBuilder(base, variantMap, defaultMap, compoundRules, customMerge, variantsLocked) {
|
|
26
|
+
const defaultMapRaw = defaultMap;
|
|
27
|
+
const resolvers = Object.keys(variantMap).map((key) => {
|
|
28
|
+
const raw = variantMap[key];
|
|
29
|
+
const map = typeof raw === "string" ? {
|
|
30
|
+
true: raw,
|
|
31
|
+
false: ""
|
|
32
|
+
} : raw;
|
|
33
|
+
const def = defaultMapRaw[key];
|
|
34
|
+
return {
|
|
35
|
+
key,
|
|
36
|
+
map,
|
|
37
|
+
def: def === true ? "true" : def === false ? "false" : def
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
const hasCompound = compoundRules.length > 0;
|
|
41
|
+
function call(props) {
|
|
42
|
+
if (customMerge) {
|
|
43
|
+
const classes = [base];
|
|
44
|
+
if (hasCompound) {
|
|
45
|
+
const resolved = {};
|
|
46
|
+
for (let i = 0; i < resolvers.length; i++) {
|
|
47
|
+
const { key, map, def } = resolvers[i];
|
|
48
|
+
const value = toKey(props[key]) ?? def;
|
|
49
|
+
resolved[key] = value;
|
|
50
|
+
if (value !== void 0) {
|
|
51
|
+
const cls = map[value];
|
|
52
|
+
if (cls) classes.push(cls);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (let i = 0; i < compoundRules.length; i++) if (matchesCompound(compoundRules[i], resolved)) classes.push(compoundRules[i].class);
|
|
56
|
+
} else for (let i = 0; i < resolvers.length; i++) {
|
|
57
|
+
const { key, map, def } = resolvers[i];
|
|
58
|
+
const value = toKey(props[key]) ?? def;
|
|
59
|
+
if (value !== void 0) {
|
|
60
|
+
const cls = map[value];
|
|
61
|
+
if (cls) classes.push(cls);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const extra = resolveClassName(props["class"]);
|
|
65
|
+
if (extra) classes.push(extra);
|
|
66
|
+
const extraCn = resolveClassName(props["className"]);
|
|
67
|
+
if (extraCn) classes.push(extraCn);
|
|
68
|
+
return customMerge(...classes);
|
|
69
|
+
}
|
|
70
|
+
let result = base;
|
|
71
|
+
if (hasCompound) {
|
|
72
|
+
const resolved = {};
|
|
73
|
+
for (let i = 0; i < resolvers.length; i++) {
|
|
74
|
+
const { key, map, def } = resolvers[i];
|
|
75
|
+
const value = toKey(props[key]) ?? def;
|
|
76
|
+
resolved[key] = value;
|
|
77
|
+
if (value !== void 0) {
|
|
78
|
+
const cls = map[value];
|
|
79
|
+
if (cls) result = result ? result + " " + cls : cls;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (let i = 0; i < compoundRules.length; i++) if (matchesCompound(compoundRules[i], resolved)) {
|
|
83
|
+
const cls = compoundRules[i].class;
|
|
84
|
+
result = result ? result + " " + cls : cls;
|
|
85
|
+
}
|
|
86
|
+
} else for (let i = 0; i < resolvers.length; i++) {
|
|
87
|
+
const { key, map, def } = resolvers[i];
|
|
88
|
+
const value = toKey(props[key]) ?? def;
|
|
89
|
+
if (value !== void 0) {
|
|
90
|
+
const cls = map[value];
|
|
91
|
+
if (cls) result = result ? result + " " + cls : cls;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const extra = resolveClassName(props["class"]);
|
|
95
|
+
if (extra) result = result ? result + " " + extra : extra;
|
|
96
|
+
const extraCn = resolveClassName(props["className"]);
|
|
97
|
+
if (extraCn) result = result ? result + " " + extraCn : extraCn;
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
call.variants = function(v) {
|
|
101
|
+
if (variantsLocked) throw new Error("styra: .variants() can only be called once per builder");
|
|
102
|
+
return makeBuilder(base, v, {}, [], customMerge, true);
|
|
103
|
+
};
|
|
104
|
+
call.defaults = function(d) {
|
|
105
|
+
return makeBuilder(base, variantMap, d, compoundRules, customMerge, variantsLocked);
|
|
106
|
+
};
|
|
107
|
+
call.compound = function(rules) {
|
|
108
|
+
return makeBuilder(base, variantMap, defaultMap, rules, customMerge, variantsLocked);
|
|
109
|
+
};
|
|
110
|
+
return call;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a configured `styra` factory.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* import { createStyra } from 'styra'
|
|
118
|
+
* import { twMerge } from 'tailwind-merge'
|
|
119
|
+
*
|
|
120
|
+
* export const { styra } = createStyra({ merge: twMerge })
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
function createStyra(options) {
|
|
124
|
+
const customMerge = options?.merge;
|
|
125
|
+
function styra(base) {
|
|
126
|
+
return makeBuilder(base, {}, {}, [], customMerge, false);
|
|
127
|
+
}
|
|
128
|
+
return { styra };
|
|
129
|
+
}
|
|
130
|
+
/** Default `styra` instance with no custom merge function. */
|
|
131
|
+
const { styra } = createStyra();
|
|
132
|
+
//#endregion
|
|
133
|
+
export { createStyra, styra };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ntatoud/styra",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe class variance builder — a maintained, boosted CVA replacement.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"class-variance-authority",
|
|
7
|
+
"cva",
|
|
8
|
+
"tailwind",
|
|
9
|
+
"typescript",
|
|
10
|
+
"variants"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/ntatoud/styra#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ntatoud/styra/issues"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ntatoud/styra.git"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/index.mjs",
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "vp pack",
|
|
31
|
+
"dev": "vp pack --watch",
|
|
32
|
+
"test": "vp test",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@arethetypeswrong/core": "catalog:",
|
|
37
|
+
"@types/node": "^25.5.0",
|
|
38
|
+
"typescript": "^6.0.2",
|
|
39
|
+
"vite-plus": "catalog:",
|
|
40
|
+
"vitest": "catalog:"
|
|
41
|
+
}
|
|
42
|
+
}
|