@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 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
+ ```
@@ -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
+ }