compound-workflow 1.7.3 → 1.9.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.
Files changed (92) hide show
  1. package/README.md +61 -69
  2. package/package.json +2 -8
  3. package/scripts/check-pack-readme.mjs +1 -16
  4. package/scripts/check-workflow-contracts.mjs +34 -44
  5. package/scripts/install-cli.mjs +273 -417
  6. package/src/AGENTS.md +59 -192
  7. package/src/{.agents/agents → agents}/research/best-practices-researcher.md +2 -2
  8. package/src/{.agents/commands → commands}/assess.md +4 -4
  9. package/src/commands/install.md +43 -0
  10. package/src/{.agents/commands → commands}/metrics.md +1 -1
  11. package/src/commands/workflow-agents.md +101 -0
  12. package/src/{.agents/commands → commands}/workflow-compound.md +1 -1
  13. package/src/{.agents/commands → commands}/workflow-plan.md +24 -33
  14. package/src/commands/workflow-work.md +836 -0
  15. package/src/{.agents/references → references}/README.md +1 -1
  16. package/src/{.agents/skills → skills}/capture-skill/SKILL.md +1 -1
  17. package/src/{.agents/skills → skills}/compound-docs/SKILL.md +6 -6
  18. package/src/{.agents/skills → skills}/compound-docs/references/yaml-schema.md +2 -2
  19. package/src/skills/setup-agents/SKILL.md +250 -0
  20. package/src/skills/standards/SKILL.md +79 -0
  21. package/src/skills/standards/references/architecture.md +228 -0
  22. package/src/skills/standards/references/code-quality.md +192 -0
  23. package/src/skills/standards/references/presentation.md +515 -0
  24. package/src/skills/standards/references/services.md +172 -0
  25. package/src/skills/standards/references/state-management.md +936 -0
  26. package/.claude-plugin/plugin.json +0 -7
  27. package/.cursor-plugin/plugin.json +0 -20
  28. package/.cursor-plugin/registration.json +0 -5
  29. package/scripts/check-version-parity.mjs +0 -36
  30. package/scripts/generate-platform-artifacts.mjs +0 -230
  31. package/src/.agents/commands/install.md +0 -51
  32. package/src/.agents/commands/workflow-work.md +0 -690
  33. package/src/.agents/registry.json +0 -48
  34. package/src/.agents/scripts/self-check.mjs +0 -227
  35. package/src/.agents/scripts/sync-opencode.mjs +0 -362
  36. package/src/.agents/skills/presentation-composability/SKILL.md +0 -72
  37. package/src/.agents/skills/react-ddd-mvc-frontend/SKILL.md +0 -51
  38. package/src/.agents/skills/react-ddd-mvc-frontend/references/feature-structure.md +0 -25
  39. package/src/.agents/skills/react-ddd-mvc-frontend/references/implementation-principles.md +0 -21
  40. package/src/.agents/skills/react-ddd-mvc-frontend/references/responsibility-gates.md +0 -41
  41. package/src/.agents/skills/react-ddd-mvc-frontend/references/source-map.md +0 -11
  42. package/src/.agents/skills/standards/SKILL.md +0 -747
  43. package/src/.agents/skills/xstate-actor-orchestration/SKILL.md +0 -197
  44. package/src/.agents/skills/xstate-actor-orchestration/agents/openai.yaml +0 -4
  45. package/src/.agents/skills/xstate-actor-orchestration/assets/statecharts/.gitkeep +0 -0
  46. package/src/.agents/skills/xstate-actor-orchestration/references/actor-system-patterns.md +0 -71
  47. package/src/.agents/skills/xstate-actor-orchestration/references/event-contracts.md +0 -73
  48. package/src/.agents/skills/xstate-actor-orchestration/references/functional-domain-patterns.md +0 -53
  49. package/src/.agents/skills/xstate-actor-orchestration/references/machine-structure-and-tags.md +0 -36
  50. package/src/.agents/skills/xstate-actor-orchestration/references/react-container-pattern.md +0 -45
  51. package/src/.agents/skills/xstate-actor-orchestration/references/reliability-observability.md +0 -39
  52. package/src/.agents/skills/xstate-actor-orchestration/references/skill-validation.md +0 -33
  53. package/src/.agents/skills/xstate-actor-orchestration/references/source-map.md +0 -44
  54. package/src/.agents/skills/xstate-actor-orchestration/references/statechart-review-and-signoff.md +0 -59
  55. package/src/.agents/skills/xstate-actor-orchestration/references/testing-strategy.md +0 -35
  56. package/src/.agents/skills/xstate-actor-orchestration/scripts/create-statechart-artifact.sh +0 -71
  57. package/src/.agents/skills/xstate-actor-orchestration/scripts/validate-skill.sh +0 -138
  58. package/src/generated/opencode.managed.json +0 -115
  59. /package/src/{.agents/agents → agents}/research/framework-docs-researcher.md +0 -0
  60. /package/src/{.agents/agents → agents}/research/git-history-analyzer.md +0 -0
  61. /package/src/{.agents/agents → agents}/research/learnings-researcher.md +0 -0
  62. /package/src/{.agents/agents → agents}/research/repo-research-analyst.md +0 -0
  63. /package/src/{.agents/agents → agents}/review/agent-native-reviewer.md +0 -0
  64. /package/src/{.agents/agents → agents}/review/planning-technical-reviewer.md +0 -0
  65. /package/src/{.agents/agents → agents}/workflow/bug-reproduction-validator.md +0 -0
  66. /package/src/{.agents/agents → agents}/workflow/lint.md +0 -0
  67. /package/src/{.agents/agents → agents}/workflow/spec-flow-analyzer.md +0 -0
  68. /package/src/{.agents/commands → commands}/test-browser.md +0 -0
  69. /package/src/{.agents/commands → commands}/workflow-brainstorm.md +0 -0
  70. /package/src/{.agents/commands → commands}/workflow-review.md +0 -0
  71. /package/src/{.agents/commands → commands}/workflow-tech-review.md +0 -0
  72. /package/src/{.agents/commands → commands}/workflow-triage.md +0 -0
  73. /package/src/{.agents/references → references}/standards/README.md +0 -0
  74. /package/src/{.agents/skills → skills}/agent-browser/SKILL.md +0 -0
  75. /package/src/{.agents/skills → skills}/audit-traceability/SKILL.md +0 -0
  76. /package/src/{.agents/skills → skills}/brainstorming/SKILL.md +0 -0
  77. /package/src/{.agents/skills → skills}/compound-docs/assets/critical-pattern-template.md +0 -0
  78. /package/src/{.agents/skills → skills}/compound-docs/assets/resolution-template.md +0 -0
  79. /package/src/{.agents/skills → skills}/compound-docs/schema.project.yaml +0 -0
  80. /package/src/{.agents/skills → skills}/compound-docs/schema.yaml +0 -0
  81. /package/src/{.agents/skills → skills}/data-foundations/SKILL.md +0 -0
  82. /package/src/{.agents/skills → skills}/document-review/SKILL.md +0 -0
  83. /package/src/{.agents/skills → skills}/file-todos/SKILL.md +0 -0
  84. /package/src/{.agents/skills → skills}/file-todos/assets/todo-template.md +0 -0
  85. /package/src/{.agents/skills → skills}/financial-workflow-integrity/SKILL.md +0 -0
  86. /package/src/{.agents/skills → skills}/git-worktree/SKILL.md +0 -0
  87. /package/src/{.agents/skills → skills}/pii-protection-prisma/SKILL.md +0 -0
  88. /package/src/{.agents/skills → skills}/process-metrics/SKILL.md +0 -0
  89. /package/src/{.agents/skills → skills}/process-metrics/assets/daily-template.md +0 -0
  90. /package/src/{.agents/skills → skills}/process-metrics/assets/monthly-template.md +0 -0
  91. /package/src/{.agents/skills → skills}/process-metrics/assets/weekly-template.md +0 -0
  92. /package/src/{.agents/skills → skills}/technical-review/SKILL.md +0 -0
@@ -0,0 +1,192 @@
1
+ # Code Quality Reference
2
+
3
+ ## Readable, Flat Control Flow
4
+
5
+ Code should be scannable top to bottom without mental stack tracking. The reader should never need to hold multiple branches in their head simultaneously.
6
+
7
+ ### Early Exits
8
+
9
+ Return early for each case. Each branch is independent and complete.
10
+
11
+ ```typescript
12
+ // ❌ Nested conditionals with else-if
13
+ export const createAction = (params) => {
14
+ if (params.pendingFolder) {
15
+ return { metadata: { createFolder: params.pendingFolder, mode: "create" } };
16
+ } else if (params.existingFolder) {
17
+ return { metadata: { targetFolder: params.existingFolder, mode: "existing" } };
18
+ } else {
19
+ throw new Error("Invalid params");
20
+ }
21
+ };
22
+
23
+ // ✅ Flat early exits
24
+ export const createAction = (params) => {
25
+ if (params.pendingFolder) {
26
+ return { metadata: { createFolder: params.pendingFolder, mode: "create" } };
27
+ }
28
+ if (params.existingFolder) {
29
+ return { metadata: { targetFolder: params.existingFolder, mode: "existing" } };
30
+ }
31
+ throw new Error("Invalid params");
32
+ };
33
+ ```
34
+
35
+ ### Rules
36
+
37
+ - **No `else` or `else-if`** — `else-if` is a code smell indicating a missed early exit opportunity
38
+ - **No conditional spreading** — `...(condition && { prop: value })` obscures intent and mutates implicitly
39
+ - **No nested ternaries** — a single ternary for a simple inline value is acceptable; nesting is not
40
+ - **No `let` variables modified in conditionals** — use pure transforms instead
41
+
42
+ ---
43
+
44
+ ## Immutable Transforms
45
+
46
+ Data operations return new values. Never mutate existing objects or arrays.
47
+
48
+ ```typescript
49
+ // ❌ Mutation
50
+ const updatePlaylist = (playlists, id, label) => {
51
+ const playlist = playlists.find(p => p.id === id);
52
+ playlist.label = label; // mutates
53
+ return playlists;
54
+ };
55
+
56
+ // ✅ Immutable transform
57
+ const updatePlaylist = (playlists, id, label) =>
58
+ playlists.map(p => p.id === id ? { ...p, label } : p);
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Error Handling
64
+
65
+ ### Always Throw on Unexpected State
66
+
67
+ Unexpected states must throw. Silent returns hide bugs and data inconsistencies.
68
+
69
+ ```typescript
70
+ // ❌ Silent return — hides bugs
71
+ if (!playlist) return;
72
+
73
+ // ✅ Throw — makes failures visible
74
+ if (!playlist) throw new Error("Playlist not found");
75
+ ```
76
+
77
+ ### Suppress Only With Documented Intent
78
+
79
+ If suppressing an error is the right call, document why. A suppress with no comment is never acceptable.
80
+
81
+ ```typescript
82
+ // ❌ Silent suppress — intent unknown
83
+ try {
84
+ doSomething();
85
+ } catch {}
86
+
87
+ // ✅ Intent explicit
88
+ try {
89
+ setPreference(value);
90
+ } catch {
91
+ // localStorage unavailable in SSR context — safe to ignore
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Validation Boundary
98
+
99
+ Validate at the boundary between layers. Controllers validate runtime inputs before passing to entities. Entities validate inbound data from the backend as part of the transform — applying defaults and ensuring shape correctness.
100
+
101
+ ### Controller Validates Runtime Inputs
102
+
103
+ ```typescript
104
+ // Controller validates before calling entity
105
+ if (!ctx.activeTabId || !ctx.activeFolderId) {
106
+ throw new Error("Active tab ID and folder ID are required");
107
+ }
108
+ const playlist = ctx.availablePlaylists.find(p => p.id === evt.payload.playlistId);
109
+ if (!playlist) throw new Error("Playlist not found");
110
+
111
+ // Entity receives clean, validated data
112
+ const action = tapesActionEntity.createApplyPlaylistActionForFolder({
113
+ playlist,
114
+ activeTabId: ctx.activeTabId,
115
+ activeFolderId: ctx.activeFolderId,
116
+ });
117
+ ```
118
+
119
+ ### Entity Validates Inbound Backend Data
120
+
121
+ Entities are the data boundary for backend responses. Apply defaults and ensure correct shape as part of the transform — protect the UI from unexpected or missing values.
122
+
123
+ ```typescript
124
+ // Entity applies defaults as part of transform
125
+ export const toPlaylistItem = (raw: RawPlaylist): PlaylistItem => ({
126
+ id: raw.playlistId,
127
+ label: raw.playlistTitle ?? "Untitled",
128
+ isActive: raw.isActive ?? false,
129
+ });
130
+ ```
131
+
132
+ ### Make Parameters Required
133
+
134
+ When inputs are always validated at call sites, make them required. TypeScript catches missing parameters at compile time, removing redundant runtime checks.
135
+
136
+ ```typescript
137
+ // ✅ Required — validated at call site, TypeScript enforces
138
+ export const createAction = ({
139
+ playlist,
140
+ activeFolderId,
141
+ }: {
142
+ playlist: InputOption;
143
+ activeFolderId: string;
144
+ }) => {
145
+ // No runtime check needed — caller is responsible
146
+ };
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Quick Check — Common Violations
152
+
153
+ **`else`/`else-if` instead of early exits:**
154
+ ```typescript
155
+ // ❌ Nested conditionals
156
+ if (a) { return x; } else if (b) { return y; } else { throw ... }
157
+
158
+ // ✅ Flat early exits
159
+ if (a) return x;
160
+ if (b) return y;
161
+ throw new Error("Unexpected state");
162
+ ```
163
+
164
+ **Conditional spreading:**
165
+ ```typescript
166
+ // ❌ Obscures intent
167
+ const action = { ...(condition && { prop: value }) };
168
+
169
+ // ✅ Explicit branch
170
+ if (condition) return { ...base, prop: value };
171
+ return base;
172
+ ```
173
+
174
+ **Silent error suppression:**
175
+ ```typescript
176
+ // ❌ No intent documented
177
+ try { doSomething(); } catch {}
178
+
179
+ // ✅ Intent explicit
180
+ try { doSomething(); } catch {
181
+ // SSR context — localStorage unavailable, safe to ignore
182
+ }
183
+ ```
184
+
185
+ **Silent return on unexpected state:**
186
+ ```typescript
187
+ // ❌ Hides bugs
188
+ if (!playlist) return;
189
+
190
+ // ✅ Makes failures visible
191
+ if (!playlist) throw new Error("Playlist not found");
192
+ ```
@@ -0,0 +1,515 @@
1
+ # Presentation Reference
2
+
3
+ Presentation components are UI only — they receive props and render. Styling is handled with **Tailwind CSS** utility classes. Variant logic is handled with **CVA** (Class Variance Authority). Class merging is handled with **`cn()`**.
4
+
5
+ ---
6
+
7
+ ## CSS-Only Responsive Layout
8
+
9
+ **Use Tailwind responsive classes instead of JavaScript resize listeners.** Layout is a CSS concern, not a state machine concern.
10
+
11
+ ```typescript
12
+ // ❌ Bad — JavaScript-driven layout
13
+ const isMobile = snapshot.context.isMobile; // state machine tracks breakpoint
14
+ useEffect(() => {
15
+ const handler = () => send({ type: 'WINDOW_RESIZED', width: window.innerWidth });
16
+ window.addEventListener('resize', handler);
17
+ }, [send]);
18
+ {isMobile ? <MobileDrawer /> : <Sidebar />}
19
+
20
+ // ✅ Good — CSS-only responsive layout
21
+ // No isMobile in machine context, no WINDOW_RESIZED event, no resize listeners
22
+ <button className="md:hidden"> {/* Only show on mobile */}
23
+ <div className="hidden md:block"> {/* Only show on desktop */}
24
+ ```
25
+
26
+ Layout is a presentation concern. CSS handles responsive breakpoints without JavaScript overhead — no re-renders on resize, no state synchronisation issues.
27
+
28
+ ---
29
+
30
+ ## No Base Element CSS Overrides
31
+
32
+ **When using Tailwind's token-based design system, don't define base element styles (`a`, `h1`, `button`, etc.) with hardcoded values.**
33
+
34
+ ```css
35
+ /* ❌ Bad — conflicts with Tailwind's token system */
36
+ a { color: #646cff; }
37
+ h1 { font-size: 3.2em; }
38
+ button { background-color: #1a1a1a; }
39
+
40
+ /* ✅ Good — use @layer base with semantic tokens */
41
+ @layer base {
42
+ * { @apply border-border; }
43
+ body { @apply bg-background text-foreground; }
44
+ }
45
+ ```
46
+
47
+ Base element styles with hex colors bypass the design token system and create inconsistencies. Use `@apply` with semantic tokens or utility classes.
48
+
49
+ ---
50
+
51
+ ## No Separate `styles.ts` Files
52
+
53
+ **With Tailwind CSS, inline Tailwind classes directly in JSX.** Do not create separate `styles.ts` files that export class strings.
54
+
55
+ ```typescript
56
+ // ❌ Bad — separate styles.ts with namespace import
57
+ // styles.ts
58
+ export const overlay = 'fixed inset-0 z-50 bg-black/50';
59
+
60
+ // index.tsx
61
+ import * as S from './styles';
62
+ <div className={S.overlay} />
63
+
64
+ // ✅ Good — inline Tailwind classes directly in JSX
65
+ <div className="fixed inset-0 z-50 bg-black/50" />
66
+ ```
67
+
68
+ Separate `styles.ts` files add indirection without benefit when using Tailwind. Classes are already semantic and self-documenting. Inline keeps component logic and styling co-located.
69
+
70
+ ---
71
+
72
+ ## Pure Presentation Rule
73
+
74
+ Presentation components must have **zero framework imports**. They accept only props — no router hooks, no auth hooks, no state machine references, no `Outlet`, no `Navigate`.
75
+
76
+ ```tsx
77
+ // ✅ Pure — accepts children and a derived value as props
78
+ import type { ReactNode } from 'react'
79
+
80
+ interface AppLayoutProps { children: ReactNode }
81
+
82
+ export function AppLayout({ children }: AppLayoutProps) {
83
+ return <div>{children}</div>
84
+ }
85
+ ```
86
+
87
+ ```tsx
88
+ // ✅ Pure — accepts extracted state as a prop, not the hook itself
89
+ interface GlobalLoaderProps { isNavigating: boolean }
90
+
91
+ export function GlobalLoader({ isNavigating }: GlobalLoaderProps) {
92
+ if (!isNavigating) return null
93
+ return <div className="..." />
94
+ }
95
+ ```
96
+
97
+ ```tsx
98
+ // ❌ Framework import in presentation — belongs in a container
99
+ import { Outlet, useNavigation } from 'react-router-dom'
100
+
101
+ export function AppLayout() {
102
+ const navigation = useNavigation() // router concern — wrong layer
103
+ return <><GlobalLoader /><Outlet /></>
104
+ }
105
+ ```
106
+
107
+ When a presentation component needs data that comes from a framework hook, create a corresponding container that calls the hook and passes the extracted value as a prop. The presentation component never knows where the value came from.
108
+
109
+ ---
110
+
111
+ ## Design Tokens
112
+
113
+ Design tokens are the foundation of the visual system. They are defined once in `tailwind.config` and consumed everywhere as Tailwind utility classes — never as raw values inline.
114
+
115
+ ### Defining tokens
116
+
117
+ Tokens are defined in `tailwind.config.ts` under `theme.extend`. This is the single source of truth for colours, spacing, typography, and any other design decisions.
118
+
119
+ ```typescript
120
+ // tailwind.config.ts
121
+ export default {
122
+ theme: {
123
+ extend: {
124
+ colors: {
125
+ brand: {
126
+ primary: "var(--color-brand-primary)",
127
+ secondary: "var(--color-brand-secondary)",
128
+ },
129
+ surface: {
130
+ default: "var(--color-surface-default)",
131
+ elevated: "var(--color-surface-elevated)",
132
+ overlay: "var(--color-surface-overlay)",
133
+ },
134
+ content: {
135
+ primary: "var(--color-content-primary)",
136
+ secondary: "var(--color-content-secondary)",
137
+ disabled: "var(--color-content-disabled)",
138
+ },
139
+ feedback: {
140
+ error: "var(--color-feedback-error)",
141
+ success: "var(--color-feedback-success)",
142
+ warning: "var(--color-feedback-warning)",
143
+ },
144
+ },
145
+ borderRadius: {
146
+ sm: "var(--radius-sm)",
147
+ md: "var(--radius-md)",
148
+ lg: "var(--radius-lg)",
149
+ },
150
+ },
151
+ },
152
+ };
153
+ ```
154
+
155
+ The CSS custom properties are defined in the global stylesheet and can be swapped per theme.
156
+
157
+ ### Consuming tokens
158
+
159
+ Always use Tailwind utility classes that reference tokens — never use raw hex values, `style` props, or arbitrary Tailwind values for anything that should be a token.
160
+
161
+ ```typescript
162
+ // ✅ Uses tokens via Tailwind classes
163
+ <div className="bg-surface-default text-content-primary rounded-md" />
164
+
165
+ // ❌ Raw value — not a token, not themeable
166
+ <div className="bg-[#1a1a2e] text-[#ffffff]" />
167
+
168
+ // ❌ Inline style — bypasses the token system entirely
169
+ <div style={{ backgroundColor: "#1a1a2e" }} />
170
+ ```
171
+
172
+ ---
173
+
174
+ ## cn() — Class Merging Utility
175
+
176
+ `cn()` is the standard utility for composing and merging Tailwind classes. It combines `clsx` (conditional classes) with `tailwind-merge` (conflict resolution).
177
+
178
+ ```typescript
179
+ // src/lib/cn.ts
180
+ import { clsx, type ClassValue } from "clsx";
181
+ import { twMerge } from "tailwind-merge";
182
+
183
+ export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
184
+ ```
185
+
186
+ Use `cn()` whenever classes are conditional or need to be merged from multiple sources:
187
+
188
+ ```typescript
189
+ // ✅ Conditional classes
190
+ <div className={cn(
191
+ "rounded-md px-4 py-2",
192
+ isActive && "bg-brand-primary text-white",
193
+ isDisabled && "opacity-50 cursor-not-allowed",
194
+ )} />
195
+
196
+ // ✅ Merging variant classes with overrides
197
+ <button className={cn(buttonVariants({ intent: "primary" }), className)} />
198
+ ```
199
+
200
+ ---
201
+
202
+ ## CVA — Variant Definitions
203
+
204
+ CVA defines the variant logic for a component. CVA definitions live **below the component's JSX** in the same file — no separate file needed.
205
+
206
+ ### Basic variant
207
+
208
+ ```typescript
209
+ // FolderItem/index.tsx
210
+ import { cva, type VariantProps } from "class-variance-authority";
211
+ import { cn } from "src/lib/cn";
212
+
213
+ interface FolderItemProps extends VariantProps<typeof folderItemVariants> {
214
+ label: string;
215
+ className?: string;
216
+ }
217
+
218
+ export const FolderItem = ({ label, intent, size, className }: FolderItemProps) => (
219
+ <div className={cn(folderItemVariants({ intent, size }), className)}>
220
+ {label}
221
+ </div>
222
+ );
223
+
224
+ // CVA definition — below the component
225
+ const folderItemVariants = cva(
226
+ // Base classes — always applied
227
+ "flex items-center gap-2 rounded-md transition-colors",
228
+ {
229
+ variants: {
230
+ intent: {
231
+ default: "bg-surface-default text-content-primary hover:bg-surface-elevated",
232
+ active: "bg-brand-primary text-white",
233
+ ghost: "text-content-secondary hover:bg-surface-overlay",
234
+ },
235
+ size: {
236
+ sm: "px-2 py-1 text-sm",
237
+ md: "px-3 py-2 text-base",
238
+ lg: "px-4 py-3 text-lg",
239
+ },
240
+ },
241
+ defaultVariants: {
242
+ intent: "default",
243
+ size: "md",
244
+ },
245
+ }
246
+ );
247
+ ```
248
+
249
+ ### Compound variants
250
+
251
+ Use `compoundVariants` when classes only apply under a specific combination of variants:
252
+
253
+ ```typescript
254
+ const buttonVariants = cva("font-medium rounded-md transition-colors", {
255
+ variants: {
256
+ intent: {
257
+ primary: "bg-brand-primary text-white",
258
+ secondary: "bg-surface-elevated text-content-primary",
259
+ ghost: "text-content-secondary",
260
+ },
261
+ size: {
262
+ sm: "px-2 py-1 text-sm",
263
+ md: "px-4 py-2 text-base",
264
+ },
265
+ disabled: {
266
+ true: "opacity-50 cursor-not-allowed",
267
+ false: "",
268
+ },
269
+ },
270
+ compoundVariants: [
271
+ // Hover only applies when not disabled
272
+ { intent: "primary", disabled: false, class: "hover:bg-brand-secondary" },
273
+ { intent: "secondary", disabled: false, class: "hover:bg-surface-overlay" },
274
+ ],
275
+ defaultVariants: {
276
+ intent: "primary",
277
+ size: "md",
278
+ disabled: false,
279
+ },
280
+ });
281
+ ```
282
+
283
+ ### Required variants
284
+
285
+ When a variant must always be provided, use TypeScript utility types to make it required:
286
+
287
+ ```typescript
288
+ type FolderItemVariantProps = VariantProps<typeof folderItemVariants>;
289
+
290
+ // Make `intent` required, keep everything else optional
291
+ interface FolderItemProps
292
+ extends Omit<FolderItemVariantProps, "intent">,
293
+ Required<Pick<FolderItemVariantProps, "intent">> {
294
+ label: string;
295
+ className?: string;
296
+ }
297
+ ```
298
+
299
+ ### CVA in compound components
300
+
301
+ In a compound component each sub-component owns its own CVA definition in its own file. Sub-components do not share CVA instances — each is self-contained.
302
+
303
+ ```typescript
304
+ // FolderSelectorRoot/index.tsx
305
+ export const FolderSelectorRoot = ({ children, className }: FolderSelectorRootProps) => (
306
+ <div className={cn(rootVariants(), className)}>{children}</div>
307
+ );
308
+
309
+ const rootVariants = cva("flex flex-col w-full bg-surface-default rounded-lg");
310
+
311
+ // FolderItem/index.tsx
312
+ export const FolderItem = ({ label, isActive, className }: FolderItemProps) => (
313
+ <div className={cn(itemVariants({ isActive }), className)}>{label}</div>
314
+ );
315
+
316
+ const itemVariants = cva("flex items-center gap-2 px-3 py-2 rounded-md", {
317
+ variants: {
318
+ isActive: {
319
+ true: "bg-brand-primary text-white",
320
+ false: "text-content-primary hover:bg-surface-elevated",
321
+ },
322
+ },
323
+ defaultVariants: { isActive: false },
324
+ });
325
+ ```
326
+
327
+ ---
328
+
329
+ ## Folder Structure
330
+
331
+ Every component lives in its own PascalCase folder under `src/features/{feature}/presentation/`.
332
+
333
+ ### Simple component
334
+
335
+ A self-contained component with no sub-components. One file only:
336
+
337
+ ```
338
+ FolderItem/
339
+ └── index.tsx # component JSX + CVA definitions below
340
+ ```
341
+
342
+ ### Compound component
343
+
344
+ A larger component composed of multiple sub-components, exposed as a single public API via the barrel:
345
+
346
+ ```
347
+ FolderSelector/
348
+ ├── index.ts # barrel — Object.assign composition
349
+ ├── FolderSelectorRoot/
350
+ │ └── index.tsx # root JSX + CVA below
351
+ ├── FolderItem/
352
+ │ └── index.tsx # item JSX + CVA below
353
+ ├── FolderActions/
354
+ │ ├── index.tsx # actions root JSX + CVA below
355
+ │ ├── FolderActionsAdd.tsx # tightly related — stays in parent folder
356
+ │ └── FolderActionsMore.tsx # tightly related — stays in parent folder
357
+ └── FolderSelectorCollapsed/
358
+ └── index.tsx
359
+ ```
360
+
361
+ Sub-components that belong to the same concern stay in the parent folder as named exports. Only promote to a subfolder when independently meaningful.
362
+
363
+ ---
364
+
365
+ ## Barrel Pattern
366
+
367
+ The `index.ts` barrel assembles the public API of the compound component. It is composition only — no component definitions, no logic, no CVA.
368
+
369
+ ```typescript
370
+ // FolderSelector/index.ts
371
+ import { FolderSelectorRoot } from "./FolderSelectorRoot";
372
+ import { FolderItem } from "./FolderItem";
373
+ import { FolderActions, FolderActionsAdd, FolderActionsMore } from "./FolderActions";
374
+ import { FolderSelectorCollapsed } from "./FolderSelectorCollapsed";
375
+ import { FolderSelectorExpanded } from "./FolderSelectorExpanded";
376
+
377
+ export const FolderSelector = Object.assign(FolderSelectorRoot, {
378
+ Folder: FolderItem,
379
+ Actions: FolderActions,
380
+ ActionsAdd: FolderActionsAdd,
381
+ ActionsMore: FolderActionsMore,
382
+ Collapsed: FolderSelectorCollapsed,
383
+ Expanded: FolderSelectorExpanded,
384
+ });
385
+ ```
386
+
387
+ Usage at the call site makes relationships explicit:
388
+
389
+ ```typescript
390
+ <FolderSelector>
391
+ <FolderSelector.Collapsed />
392
+ <FolderSelector.Expanded>
393
+ <FolderSelector.Folder intent="active" label="My Playlist" />
394
+ </FolderSelector.Expanded>
395
+ </FolderSelector>
396
+ ```
397
+
398
+ ### Rules
399
+
400
+ - Relative imports only — the barrel imports from its own subfolders, never from outside the component boundary
401
+ - No logic in the barrel — composition only
402
+ - What's in `Object.assign` is the public API — if it's not in the barrel, it's internal
403
+ - No `styles.ts` — styling belongs in the component file via CVA and Tailwind
404
+
405
+ ---
406
+
407
+ ## className Prop
408
+
409
+ All components accept an optional `className` prop for overrides at the call site. Always merge with `cn()` — never concatenate strings directly.
410
+
411
+ ```typescript
412
+ interface FolderItemProps extends VariantProps<typeof folderItemVariants> {
413
+ className?: string;
414
+ }
415
+
416
+ export const FolderItem = ({ intent, size, className }: FolderItemProps) => (
417
+ <div className={cn(folderItemVariants({ intent, size }), className)} />
418
+ );
419
+ ```
420
+
421
+ This allows containers to apply layout-specific classes without breaking the component's internal variant logic.
422
+
423
+ ---
424
+
425
+ ## Quick Check — Common Violations
426
+
427
+ **Raw value instead of a token:**
428
+ ```typescript
429
+ // ❌
430
+ <div className="bg-[#1a1a2e] text-[14px]" />
431
+
432
+ // ✅
433
+ <div className="bg-surface-default text-sm" />
434
+ ```
435
+
436
+ **CVA in a separate file:**
437
+ ```typescript
438
+ // ❌ Separate variants.ts file
439
+ import { buttonVariants } from "./variants";
440
+
441
+ // ✅ CVA defined below the component in the same file
442
+ export const Button = ({ intent }: ButtonProps) => (
443
+ <button className={buttonVariants({ intent })} />
444
+ );
445
+ const buttonVariants = cva("...", { variants: { intent: { ... } } });
446
+ ```
447
+
448
+ **String concatenation instead of cn():**
449
+ ```typescript
450
+ // ❌ Class conflicts not resolved
451
+ className={`${baseClasses} ${isActive ? "bg-brand-primary" : ""}`}
452
+
453
+ // ✅
454
+ className={cn(baseClasses, isActive && "bg-brand-primary")}
455
+ ```
456
+
457
+ **Styles outside the component file:**
458
+ ```typescript
459
+ // ❌ styles.ts with styled components
460
+ import { Base, BaseContent } from "./styles";
461
+
462
+ // ✅ Tailwind classes inline, CVA below the component
463
+ <div className={cn(rootVariants(), className)} />
464
+ ```
465
+
466
+ **Barrel importing from outside component boundary:**
467
+ ```typescript
468
+ // ❌
469
+ import { FolderItem } from "src/features/other/presentation/FolderItem";
470
+
471
+ // ✅
472
+ import { FolderItem } from "./FolderItem";
473
+ ```
474
+
475
+ **Variant logic inline in JSX instead of CVA:**
476
+ ```typescript
477
+ // ❌ Variant logic scattered through JSX
478
+ <div className={`rounded-md ${intent === "primary" ? "bg-brand-primary text-white" : "bg-surface-default text-content-primary"}`} />
479
+
480
+ // ✅ Variant logic in CVA, JSX stays clean
481
+ <div className={cn(itemVariants({ intent }), className)} />
482
+ ```
483
+
484
+ **JavaScript-driven responsive layout:**
485
+ ```typescript
486
+ // ❌ Resize listener + state machine tracking breakpoint
487
+ const isMobile = snapshot.context.isMobile;
488
+ {isMobile ? <MobileDrawer /> : <Sidebar />}
489
+
490
+ // ✅ CSS-only
491
+ <div className="hidden md:block"><Sidebar /></div>
492
+ <div className="md:hidden"><MobileDrawer /></div>
493
+ ```
494
+
495
+ **Base element styles with hardcoded values:**
496
+ ```css
497
+ /* ❌ Bypasses token system */
498
+ a { color: #646cff; }
499
+ button { background-color: #1a1a1a; }
500
+
501
+ /* ✅ Use @layer base with semantic tokens */
502
+ @layer base {
503
+ body { @apply bg-background text-foreground; }
504
+ }
505
+ ```
506
+
507
+ **Separate `styles.ts` file:**
508
+ ```typescript
509
+ // ❌
510
+ import * as S from './styles';
511
+ <div className={S.overlay} />
512
+
513
+ // ✅
514
+ <div className="fixed inset-0 z-50 bg-black/50" />
515
+ ```