@ntatoud/styra 0.1.0 → 0.2.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.
@@ -0,0 +1,252 @@
1
+ ---
2
+ name: styra-getting-started
3
+ description: >
4
+ Full setup guide for @ntatoud/styra v0.1.0 — type-safe class variance builder.
5
+ Covers install, styra(base).variants().defaults() builder chain, class/className
6
+ override props with clsx syntax, createStyra({ merge: twMerge }) for Tailwind
7
+ projects, cn utility, and VariantProps type helper for component prop types.
8
+ Preempts: calling .variants() twice (runtime throw), wiring twMerge outside the factory.
9
+ type: lifecycle
10
+ library: "@ntatoud/styra"
11
+ library_version: "0.1.0"
12
+ sources:
13
+ - "ntatoud/styra:packages/styra/src/index.ts"
14
+ - "ntatoud/styra:packages/styra/README.md"
15
+ ---
16
+
17
+ # Getting Started with @ntatoud/styra
18
+
19
+ ## 1. Setup
20
+
21
+ Install the package — no peer dependencies required.
22
+
23
+ ```sh
24
+ # npm
25
+ npm install @ntatoud/styra
26
+
27
+ # pnpm
28
+ pnpm add @ntatoud/styra
29
+
30
+ # yarn
31
+ yarn add @ntatoud/styra
32
+ ```
33
+
34
+ Minimum viable example — a typed button component:
35
+
36
+ ```ts
37
+ // button.ts
38
+ import { styra, type VariantProps } from "@ntatoud/styra";
39
+
40
+ export const button = styra("btn")
41
+ .variants({
42
+ size: { sm: "text-sm px-2 py-1", md: "text-base px-4 py-2", lg: "text-lg px-6 py-3" },
43
+ intent: { primary: "bg-blue-600 text-white", danger: "bg-red-600 text-white" },
44
+ })
45
+ .defaults({ size: "md" });
46
+
47
+ export type ButtonProps = VariantProps<typeof button>;
48
+ // { size?: "sm" | "md" | "lg"; intent: "primary" | "danger" }
49
+ ```
50
+
51
+ ```ts
52
+ // usage
53
+ import { button } from "./button";
54
+
55
+ button({ intent: "primary" });
56
+ // → "btn text-base px-4 py-2 bg-blue-600 text-white"
57
+
58
+ button({ size: "sm", intent: "danger" });
59
+ // → "btn text-sm px-2 py-1 bg-red-600 text-white"
60
+ ```
61
+
62
+ ---
63
+
64
+ ## 2. Core Patterns
65
+
66
+ ### Pattern 1 — Compound variants
67
+
68
+ `.compound()` applies extra classes when multiple variant conditions are met simultaneously.
69
+
70
+ ```ts
71
+ import { styra } from "@ntatoud/styra";
72
+
73
+ const button = styra("btn")
74
+ .variants({
75
+ size: { sm: "text-sm", lg: "text-lg" },
76
+ color: { red: "bg-red-500", blue: "bg-blue-500" },
77
+ })
78
+ .defaults({ size: "sm" })
79
+ .compound([{ size: "sm", color: "red", class: "ring-2 ring-red-300" }]);
80
+
81
+ button({ color: "red" });
82
+ // → "btn text-sm bg-red-500 ring-2 ring-red-300"
83
+
84
+ button({ size: "lg", color: "red" });
85
+ // → "btn text-lg bg-red-500" (compound rule does not match)
86
+ ```
87
+
88
+ ### Pattern 2 — class and className override props
89
+
90
+ Both `class` and `className` are accepted. They support clsx syntax: strings, arrays, objects, and functions.
91
+
92
+ ```ts
93
+ import { styra } from "@ntatoud/styra";
94
+
95
+ const badge = styra("badge").variants({ color: { green: "bg-green-500", gray: "bg-gray-300" } });
96
+
97
+ badge({ color: "green", class: "rounded-full" });
98
+ // → "badge bg-green-500 rounded-full"
99
+
100
+ badge({ color: "gray", className: ["mt-2", { hidden: false, block: true }] });
101
+ // → "badge bg-gray-300 mt-2 block"
102
+ ```
103
+
104
+ ### Pattern 3 — Boolean shorthand variants
105
+
106
+ Pass a plain string instead of a `{ true: "...", false: "..." }` map to get an opt-in boolean prop.
107
+
108
+ ```ts
109
+ import { styra } from "@ntatoud/styra";
110
+
111
+ const input = styra("input border").variants({
112
+ disabled: "opacity-50 cursor-not-allowed",
113
+ full: "w-full",
114
+ });
115
+
116
+ input({ disabled: true, full: true });
117
+ // → "input border opacity-50 cursor-not-allowed w-full"
118
+
119
+ input({});
120
+ // → "input border"
121
+ ```
122
+
123
+ ### Pattern 4 — Tailwind Merge via createStyra
124
+
125
+ Create a project-scoped `styra` and `cn` that pipe every output through `twMerge`. Export them once and import everywhere.
126
+
127
+ ```ts
128
+ // lib/styra.ts
129
+ import { createStyra } from "@ntatoud/styra";
130
+ import { twMerge } from "tailwind-merge";
131
+
132
+ export const { styra, cn } = createStyra({ merge: twMerge });
133
+ ```
134
+
135
+ ```ts
136
+ // components/card.ts
137
+ import { styra, cn } from "@/lib/styra";
138
+ import type { VariantProps } from "@ntatoud/styra";
139
+
140
+ export const card = styra("p-4 rounded-lg")
141
+ .variants({ shadow: { sm: "shadow-sm", lg: "shadow-lg" } })
142
+ .defaults({ shadow: "sm" });
143
+
144
+ export type CardProps = VariantProps<typeof card>;
145
+
146
+ // cn accepts the same clsx-compatible syntax and uses twMerge internally
147
+ const classes = cn("p-4", ["rounded", { "border border-gray-200": true }]);
148
+ ```
149
+
150
+ ---
151
+
152
+ ## 3. Common Mistakes
153
+
154
+ ### [CRITICAL] Calling .variants() twice on the same builder
155
+
156
+ **Wrong**
157
+
158
+ ```ts
159
+ import { styra } from "@ntatoud/styra";
160
+
161
+ const button = styra("btn")
162
+ .variants({ size: { sm: "text-sm", lg: "text-lg" } })
163
+ .variants({ color: { red: "bg-red-500" } }); // throws at runtime
164
+ ```
165
+
166
+ **Correct**
167
+
168
+ ```ts
169
+ import { styra } from "@ntatoud/styra";
170
+
171
+ const button = styra("btn").variants({
172
+ size: { sm: "text-sm", lg: "text-lg" },
173
+ color: { red: "bg-red-500" },
174
+ });
175
+ ```
176
+
177
+ `.variants()` can only be called once per builder. Calling it a second time throws `"styra: .variants() can only be called once per builder"`. Declare all variants in a single call.
178
+
179
+ Source: `ntatoud/styra:packages/styra/src/index.ts`
180
+
181
+ ---
182
+
183
+ ### [HIGH] Wiring twMerge outside the factory
184
+
185
+ **Wrong**
186
+
187
+ ```ts
188
+ import { styra } from "@ntatoud/styra";
189
+ import { twMerge } from "tailwind-merge";
190
+
191
+ const button = styra("btn").variants({ size: { sm: "p-1", lg: "p-4" } });
192
+
193
+ // Bypasses the builder's class/className resolution — merges raw output only
194
+ const classes = twMerge(button({ size: "sm", class: "p-2" }));
195
+ ```
196
+
197
+ **Correct**
198
+
199
+ ```ts
200
+ import { createStyra } from "@ntatoud/styra";
201
+ import { twMerge } from "tailwind-merge";
202
+
203
+ export const { styra, cn } = createStyra({ merge: twMerge });
204
+
205
+ const button = styra("btn").variants({ size: { sm: "p-1", lg: "p-4" } });
206
+
207
+ // twMerge runs on the full resolved class string, including class/className overrides
208
+ button({ size: "sm", class: "p-2" });
209
+ ```
210
+
211
+ Wrapping the output of `button(...)` in `twMerge()` directly means the merge runs after resolution but ignores how override props interact with base and variant classes. Use `createStyra({ merge: twMerge })` so every resolved output — including `class`/`className` overrides — is merged correctly.
212
+
213
+ Source: `ntatoud/styra:packages/styra/src/index.ts`
214
+
215
+ ---
216
+
217
+ ### [MEDIUM] Using raw inferred props instead of VariantProps for component typing
218
+
219
+ **Wrong**
220
+
221
+ ```ts
222
+ import { styra } from "@ntatoud/styra";
223
+
224
+ const button = styra("btn").variants({ intent: { primary: "bg-blue-600" } });
225
+
226
+ // Parameters<typeof button>[0] includes class and className — leaks internal props
227
+ type ButtonProps = Parameters<typeof button>[0];
228
+
229
+ function Button(props: ButtonProps) {
230
+ /* ... */
231
+ }
232
+ ```
233
+
234
+ **Correct**
235
+
236
+ ```ts
237
+ import { styra, type VariantProps } from "@ntatoud/styra";
238
+
239
+ const button = styra("btn").variants({ intent: { primary: "bg-blue-600" } });
240
+
241
+ // VariantProps strips class and className — only variant keys remain
242
+ export type ButtonProps = VariantProps<typeof button>;
243
+ // { intent: "primary" }
244
+
245
+ function Button(props: ButtonProps) {
246
+ /* ... */
247
+ }
248
+ ```
249
+
250
+ `VariantProps<T>` is defined as `Omit<Parameters<T>[0], "class" | "className">`. Using raw `Parameters<T>[0]` exposes `class` and `className` as public component props, which are internal override props not meant for consumers.
251
+
252
+ Source: `ntatoud/styra:packages/styra/src/types.ts`
@@ -0,0 +1,314 @@
1
+ ---
2
+ name: styra-migrate-from-cva
3
+ description: >
4
+ Migrate from CVA (class-variance-authority) to @ntatoud/styra. Maps cva() config object
5
+ syntax to styra builder chain: variants(), defaults(), compound(). Covers cx/clsx → cn,
6
+ VariantProps (unchanged), createStyra for merge config, boolean shorthand variants,
7
+ and negation in compound rules. Use when converting class-variance-authority imports
8
+ or cva() calls to the styra builder API.
9
+ type: lifecycle
10
+ library: "@ntatoud/styra"
11
+ library_version: "0.1.0"
12
+ sources:
13
+ - "ntatoud/styra:packages/styra/src/index.ts"
14
+ - "ntatoud/styra:packages/styra/README.md"
15
+ ---
16
+
17
+ # Migrate from CVA to styra
18
+
19
+ This skill covers replacing `class-variance-authority` with `@ntatoud/styra`. The core
20
+ difference: CVA uses a single config object; styra uses a builder chain.
21
+
22
+ ## API Mapping
23
+
24
+ | CVA | styra |
25
+ | -------------------------------------------------------------- | ---------------------------------------------------- |
26
+ | `import { cva } from "class-variance-authority"` | `import { styra } from "@ntatoud/styra"` |
27
+ | `import { cx } from "class-variance-authority"` | `import { cn } from "@ntatoud/styra"` |
28
+ | `import { type VariantProps } from "class-variance-authority"` | `import { type VariantProps } from "@ntatoud/styra"` |
29
+ | `cva(base, { variants })` | `styra(base).variants({})` |
30
+ | `cva(base, { defaultVariants })` | `.defaults({})` |
31
+ | `cva(base, { compoundVariants })` | `.compound([])` |
32
+ | CVA + clsx + twMerge (manual) | `createStyra({ merge: twMerge })` |
33
+
34
+ `VariantProps<typeof fn>` is identical — no change in usage.
35
+
36
+ ## Setup
37
+
38
+ Remove CVA, install styra:
39
+
40
+ ```bash
41
+ vp remove class-variance-authority
42
+ vp add @ntatoud/styra
43
+ ```
44
+
45
+ ## Patterns
46
+
47
+ ### Basic variant component
48
+
49
+ Before:
50
+
51
+ ```ts
52
+ import { cva, type VariantProps } from "class-variance-authority";
53
+
54
+ const button = cva("btn", {
55
+ variants: {
56
+ size: { sm: "text-sm", md: "text-md", lg: "text-lg" },
57
+ color: { red: "bg-red", blue: "bg-blue" },
58
+ },
59
+ defaultVariants: { size: "md" },
60
+ compoundVariants: [{ size: "sm", color: "red", class: "ring-red" }],
61
+ });
62
+
63
+ type ButtonProps = VariantProps<typeof button>;
64
+ ```
65
+
66
+ After:
67
+
68
+ ```ts
69
+ import { styra, type VariantProps } from "@ntatoud/styra";
70
+
71
+ const button = styra("btn")
72
+ .variants({
73
+ size: { sm: "text-sm", md: "text-md", lg: "text-lg" },
74
+ color: { red: "bg-red", blue: "bg-blue" },
75
+ })
76
+ .defaults({ size: "md" })
77
+ .compound([{ size: "sm", color: "red", class: "ring-red" }]);
78
+
79
+ type ButtonProps = VariantProps<typeof button>;
80
+ // → { size?: "sm" | "md" | "lg"; color: "red" | "blue" }
81
+ ```
82
+
83
+ ### Class merging utility
84
+
85
+ Before:
86
+
87
+ ```ts
88
+ import { cx } from "class-variance-authority";
89
+ // or: import clsx from "clsx";
90
+
91
+ const classes = cx("base", condition && "extra");
92
+ ```
93
+
94
+ After:
95
+
96
+ ```ts
97
+ import { cn } from "@ntatoud/styra";
98
+
99
+ const classes = cn("base", condition && "extra");
100
+ ```
101
+
102
+ ### Tailwind Merge integration
103
+
104
+ CVA does not natively support a merge function — projects typically wired up clsx + twMerge
105
+ manually alongside CVA. Styra integrates merge at creation time via `createStyra`.
106
+
107
+ Before (typical CVA + twMerge pattern):
108
+
109
+ ```ts
110
+ import { cva } from "class-variance-authority";
111
+ import { twMerge } from "tailwind-merge";
112
+ import { clsx, type ClassValue } from "clsx";
113
+
114
+ function cn(...inputs: ClassValue[]) {
115
+ return twMerge(clsx(inputs));
116
+ }
117
+
118
+ const button = cva("btn rounded", {
119
+ variants: { size: { sm: "px-2", lg: "px-6" } },
120
+ });
121
+ ```
122
+
123
+ After:
124
+
125
+ ```ts
126
+ // lib/styra.ts
127
+ import { createStyra } from "@ntatoud/styra";
128
+ import { twMerge } from "tailwind-merge";
129
+
130
+ export const { styra, cn } = createStyra({ merge: twMerge });
131
+ ```
132
+
133
+ ```ts
134
+ // button.ts
135
+ import { styra, cn, type VariantProps } from "./lib/styra";
136
+
137
+ const button = styra("btn rounded").variants({
138
+ size: { sm: "px-2", lg: "px-6" },
139
+ });
140
+
141
+ type ButtonProps = VariantProps<typeof button>;
142
+ ```
143
+
144
+ Import `styra` and `cn` from your local `lib/styra` module everywhere instead of from
145
+ `@ntatoud/styra` directly.
146
+
147
+ ### Variants with no default (required prop)
148
+
149
+ If a variant has no entry in `.defaults()`, its prop is required in the inferred type.
150
+
151
+ ```ts
152
+ import { styra, type VariantProps } from "@ntatoud/styra";
153
+
154
+ const badge = styra("badge")
155
+ .variants({
156
+ status: { info: "bg-blue", warn: "bg-yellow", error: "bg-red" },
157
+ size: { sm: "text-xs", md: "text-sm" },
158
+ })
159
+ .defaults({ size: "md" });
160
+ // size is optional (has default), status is required
161
+
162
+ type BadgeProps = VariantProps<typeof badge>;
163
+ // → { status: "info" | "warn" | "error"; size?: "sm" | "md" }
164
+ ```
165
+
166
+ ### Boolean shorthand variants (new in styra)
167
+
168
+ CVA did not support boolean variants. In styra, a variant value that is a string (not an
169
+ object) defines a boolean prop — the class applies when the prop is `true`.
170
+
171
+ ```ts
172
+ import { styra, type VariantProps } from "@ntatoud/styra";
173
+
174
+ const button = styra("btn")
175
+ .variants({
176
+ size: { sm: "text-sm", lg: "text-lg" },
177
+ disabled: "opacity-50 pointer-events-none", // boolean shorthand
178
+ loading: "cursor-wait", // boolean shorthand
179
+ })
180
+ .defaults({ size: "sm" });
181
+
182
+ type ButtonProps = VariantProps<typeof button>;
183
+ // → { size?: "sm" | "lg"; disabled?: boolean; loading?: boolean }
184
+
185
+ button({ size: "lg", disabled: true });
186
+ // → "btn text-lg opacity-50 pointer-events-none"
187
+ ```
188
+
189
+ There is no CVA equivalent — this is new capability. Use it instead of manually adding
190
+ conditional classes for simple on/off states.
191
+
192
+ ### Negation in compound rules (new in styra)
193
+
194
+ CVA compound variants only matched when all listed keys matched. Styra allows negation via
195
+ `{ not: value }` in compound entries.
196
+
197
+ ```ts
198
+ import { styra, type VariantProps } from "@ntatoud/styra";
199
+
200
+ const button = styra("btn")
201
+ .variants({
202
+ disabled: "opacity-50 pointer-events-none",
203
+ size: { sm: "text-sm", lg: "text-lg" },
204
+ })
205
+ .compound([
206
+ // Apply hover class only when NOT disabled
207
+ { disabled: { not: "true" }, class: "hover:opacity-80" },
208
+ // Apply ring only when size is sm AND NOT disabled
209
+ { size: "sm", disabled: { not: "true" }, class: "ring-1" },
210
+ ]);
211
+ ```
212
+
213
+ There is no CVA equivalent. When migrating compound variants that were previously guarded
214
+ by runtime logic, consider whether negation expresses the intent more cleanly.
215
+
216
+ ## Common Mistakes
217
+
218
+ ### Using the CVA config object syntax
219
+
220
+ Wrong — this is CVA syntax, not valid styra:
221
+
222
+ ```ts
223
+ // WRONG
224
+ import { styra } from "@ntatoud/styra";
225
+
226
+ const button = styra("btn", {
227
+ variants: { size: { sm: "text-sm" } },
228
+ defaultVariants: { size: "sm" },
229
+ compoundVariants: [{ size: "sm", class: "ring" }],
230
+ });
231
+ ```
232
+
233
+ Correct — styra uses a builder chain:
234
+
235
+ ```ts
236
+ // CORRECT
237
+ import { styra } from "@ntatoud/styra";
238
+
239
+ const button = styra("btn")
240
+ .variants({ size: { sm: "text-sm" } })
241
+ .defaults({ size: "sm" })
242
+ .compound([{ size: "sm", class: "ring" }]);
243
+ ```
244
+
245
+ ### Keeping `compoundVariants` as a key
246
+
247
+ Wrong:
248
+
249
+ ```ts
250
+ // WRONG — compoundVariants is a CVA key, not a styra method
251
+ const button = styra("btn")
252
+ .variants({ size: { sm: "text-sm" } })
253
+ .compoundVariants([{ size: "sm", class: "ring" }]);
254
+ ```
255
+
256
+ Correct: the method is `.compound()`.
257
+
258
+ ### Keeping `defaultVariants` as a key
259
+
260
+ Wrong:
261
+
262
+ ```ts
263
+ // WRONG — defaultVariants is a CVA key, not a styra method
264
+ const button = styra("btn")
265
+ .variants({ size: { sm: "text-sm", md: "text-md" } })
266
+ .defaultVariants({ size: "md" });
267
+ ```
268
+
269
+ Correct: the method is `.defaults()`.
270
+
271
+ ### Not using createStyra when twMerge is needed
272
+
273
+ Wrong — this bypasses styra's merge integration:
274
+
275
+ ```ts
276
+ // WRONG — manually merging outside of styra
277
+ import { styra } from "@ntatoud/styra";
278
+ import { twMerge } from "tailwind-merge";
279
+
280
+ const button = styra("btn").variants({ size: { sm: "px-2", lg: "px-6" } });
281
+
282
+ // Calling twMerge separately
283
+ const cls = twMerge(button({ size: "sm" }), "px-4");
284
+ ```
285
+
286
+ Correct — configure merge once in `createStyra` and use the returned `styra` and `cn`:
287
+
288
+ ```ts
289
+ // lib/styra.ts
290
+ import { createStyra } from "@ntatoud/styra";
291
+ import { twMerge } from "tailwind-merge";
292
+ export const { styra, cn } = createStyra({ merge: twMerge });
293
+ ```
294
+
295
+ ### Not using boolean shorthand for simple on/off variants
296
+
297
+ Suboptimal — mirrors old CVA workaround pattern:
298
+
299
+ ```ts
300
+ // SUBOPTIMAL — using object syntax for a boolean state
301
+ const button = styra("btn").variants({
302
+ disabled: { true: "opacity-50", false: "" },
303
+ });
304
+ ```
305
+
306
+ Preferred — use boolean shorthand:
307
+
308
+ ```ts
309
+ // PREFERRED
310
+ const button = styra("btn").variants({
311
+ disabled: "opacity-50 pointer-events-none",
312
+ });
313
+ // disabled prop is inferred as boolean
314
+ ```