Add Plan 5: UI System implementation plan
This commit is contained in:
610
docs/superpowers/plans/2026-04-06-plan-5-ui-system.md
Normal file
610
docs/superpowers/plans/2026-04-06-plan-5-ui-system.md
Normal file
@@ -0,0 +1,610 @@
|
||||
# Plan 5: UI System — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Set up `@repo/ui` with Atomic Design folder structure, Tailwind CSS v4, shadcn/ui, base components (atoms + a molecule), and `apps/storybook` with Storybook 8.
|
||||
|
||||
**Architecture:** `@repo/ui` uses Atomic Design (atoms/molecules/organisms/templates). Tailwind v4 uses CSS-first config (`@import "tailwindcss"` + `@theme`). shadcn/ui components land in `atoms/` by default. Storybook runs as a separate app pulling stories from the UI package via `@storybook/react-vite` with `@tailwindcss/vite` plugin.
|
||||
|
||||
**Tech Stack:** Tailwind CSS v4, shadcn/ui, clsx, tailwind-merge, Storybook 8, @storybook/react-vite
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Set up @repo/ui with Tailwind v4 + Atomic Design structure
|
||||
|
||||
**Files:**
|
||||
- Modify: `packages/ui/package.json`
|
||||
- Modify: `packages/ui/tsconfig.json`
|
||||
- Create: `packages/ui/src/styles/globals.css`
|
||||
- Create: `packages/ui/src/lib/utils.ts`
|
||||
- Create: `packages/ui/src/atoms/index.ts`
|
||||
- Create: `packages/ui/src/molecules/index.ts`
|
||||
- Create: `packages/ui/src/organisms/index.ts`
|
||||
- Create: `packages/ui/src/templates/index.ts`
|
||||
- Modify: `packages/ui/src/index.ts`
|
||||
|
||||
- [ ] **Step 1: Update packages/ui/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "echo 'typechecked by consuming app bundler'",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.0",
|
||||
"tailwind-merge": "^3.0.0",
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/react": "^19.0.0",
|
||||
"tailwindcss": "^4.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create src/styles/globals.css**
|
||||
|
||||
Tailwind v4 CSS-first config — no tailwind.config.ts needed.
|
||||
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(0 0% 100%);
|
||||
--color-foreground: hsl(240 10% 3.9%);
|
||||
--color-card: hsl(0 0% 100%);
|
||||
--color-card-foreground: hsl(240 10% 3.9%);
|
||||
--color-popover: hsl(0 0% 100%);
|
||||
--color-popover-foreground: hsl(240 10% 3.9%);
|
||||
--color-primary: hsl(240 5.9% 10%);
|
||||
--color-primary-foreground: hsl(0 0% 98%);
|
||||
--color-secondary: hsl(240 4.8% 95.9%);
|
||||
--color-secondary-foreground: hsl(240 5.9% 10%);
|
||||
--color-muted: hsl(240 4.8% 95.9%);
|
||||
--color-muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--color-accent: hsl(240 4.8% 95.9%);
|
||||
--color-accent-foreground: hsl(240 5.9% 10%);
|
||||
--color-destructive: hsl(0 84.2% 60.2%);
|
||||
--color-destructive-foreground: hsl(0 0% 98%);
|
||||
--color-border: hsl(240 5.9% 90%);
|
||||
--color-input: hsl(240 5.9% 90%);
|
||||
--color-ring: hsl(240 5.9% 10%);
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create src/lib/utils.ts**
|
||||
|
||||
```typescript
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Create atomic design barrel files**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/index.ts
|
||||
// Atom components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/index.ts
|
||||
// Molecule components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/organisms/index.ts
|
||||
// Organism components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/templates/index.ts
|
||||
// Template components are exported from here
|
||||
export {};
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Update src/index.ts**
|
||||
|
||||
```typescript
|
||||
export { cn } from "./lib/utils.js";
|
||||
export * from "./atoms/index.js";
|
||||
export * from "./molecules/index.js";
|
||||
export * from "./organisms/index.js";
|
||||
export * from "./templates/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run pnpm install and commit**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
git add packages/ui/ pnpm-lock.yaml
|
||||
git commit -m "feat(ui): set up Atomic Design structure with Tailwind v4"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add Button atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/button/button.tsx`
|
||||
- Create: `packages/ui/src/atoms/button/button.stories.tsx`
|
||||
- Create: `packages/ui/src/atoms/button/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create button.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type ButtonHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost";
|
||||
size?: "sm" | "default" | "lg";
|
||||
}
|
||||
|
||||
const variantStyles: Record<NonNullable<ButtonProps["variant"]>, string> = {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
};
|
||||
|
||||
const sizeStyles: Record<NonNullable<ButtonProps["size"]>, string> = {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
default: "h-10 px-4 py-2",
|
||||
lg: "h-11 px-8 text-lg",
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create button.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Button } from "./button.js";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Button",
|
||||
component: Button,
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["default", "secondary", "destructive", "outline", "ghost"],
|
||||
},
|
||||
size: { control: "select", options: ["sm", "default", "lg"] },
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { children: "Button", variant: "default" },
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: { children: "Secondary", variant: "secondary" },
|
||||
};
|
||||
|
||||
export const Destructive: Story = {
|
||||
args: { children: "Destructive", variant: "destructive" },
|
||||
};
|
||||
|
||||
export const Outline: Story = {
|
||||
args: { children: "Outline", variant: "outline" },
|
||||
};
|
||||
|
||||
export const Ghost: Story = {
|
||||
args: { children: "Ghost", variant: "ghost" },
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create button/index.ts and update atoms/index.ts**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/button/index.ts
|
||||
export { Button, type ButtonProps } from "./button.js";
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/index.ts
|
||||
export { Button, type ButtonProps } from "./button/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/button/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Button atom with Storybook story"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add Input atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/input/input.tsx`
|
||||
- Create: `packages/ui/src/atoms/input/input.stories.tsx`
|
||||
- Create: `packages/ui/src/atoms/input/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create input.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type InputHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create input.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Input } from "./input.js";
|
||||
|
||||
const meta = {
|
||||
title: "Atoms/Input",
|
||||
component: Input,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Input>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { placeholder: "Enter text..." },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { placeholder: "Disabled", disabled: true },
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create index and update atoms barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/input/index.ts
|
||||
export { Input, type InputProps } from "./input.js";
|
||||
```
|
||||
|
||||
Update atoms/index.ts to add:
|
||||
```typescript
|
||||
export { Input, type InputProps } from "./input/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/input/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Input atom with Storybook story"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Add Label atom
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/atoms/label/label.tsx`
|
||||
- Create: `packages/ui/src/atoms/label/index.ts`
|
||||
- Modify: `packages/ui/src/atoms/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create label.tsx**
|
||||
|
||||
```tsx
|
||||
import { forwardRef, type LabelHTMLAttributes } from "react";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {}
|
||||
|
||||
export const Label = forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Label.displayName = "Label";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create label/index.ts and update atoms barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/atoms/label/index.ts
|
||||
export { Label, type LabelProps } from "./label.js";
|
||||
```
|
||||
|
||||
Update atoms/index.ts to add:
|
||||
```typescript
|
||||
export { Label, type LabelProps } from "./label/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/atoms/label/ packages/ui/src/atoms/index.ts
|
||||
git commit -m "feat(ui): add Label atom"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Add FormField molecule
|
||||
|
||||
**Files:**
|
||||
- Create: `packages/ui/src/molecules/form-field/form-field.tsx`
|
||||
- Create: `packages/ui/src/molecules/form-field/form-field.stories.tsx`
|
||||
- Create: `packages/ui/src/molecules/form-field/index.ts`
|
||||
- Modify: `packages/ui/src/molecules/index.ts`
|
||||
|
||||
- [ ] **Step 1: Create form-field.tsx**
|
||||
|
||||
```tsx
|
||||
import { type ReactNode } from "react";
|
||||
import { Label } from "../../atoms/label/index.js";
|
||||
import { Input, type InputProps } from "../../atoms/input/index.js";
|
||||
import { cn } from "../../lib/utils.js";
|
||||
|
||||
export interface FormFieldProps extends InputProps {
|
||||
label: string;
|
||||
error?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export function FormField({
|
||||
label,
|
||||
error,
|
||||
description,
|
||||
className,
|
||||
id,
|
||||
...inputProps
|
||||
}: FormFieldProps) {
|
||||
const fieldId = id ?? label.toLowerCase().replace(/\s+/g, "-");
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2", className)}>
|
||||
<Label htmlFor={fieldId}>{label}</Label>
|
||||
<Input id={fieldId} {...inputProps} />
|
||||
{description && (
|
||||
<p className="text-sm text-muted-foreground">{description}</p>
|
||||
)}
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create form-field.stories.tsx**
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { FormField } from "./form-field.js";
|
||||
|
||||
const meta = {
|
||||
title: "Molecules/FormField",
|
||||
component: FormField,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof FormField>;
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { label: "Email", placeholder: "you@example.com", type: "email" },
|
||||
};
|
||||
|
||||
export const WithDescription: Story = {
|
||||
args: {
|
||||
label: "Username",
|
||||
placeholder: "johndoe",
|
||||
description: "Must be 3-31 characters",
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
error: "Password must be at least 6 characters",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create index and update molecules barrel**
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/form-field/index.ts
|
||||
export { FormField, type FormFieldProps } from "./form-field.js";
|
||||
```
|
||||
|
||||
```typescript
|
||||
// packages/ui/src/molecules/index.ts
|
||||
export { FormField, type FormFieldProps } from "./form-field/index.js";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/ui/src/molecules/
|
||||
git commit -m "feat(ui): add FormField molecule (Label + Input + error)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Set up apps/storybook
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/storybook/package.json`
|
||||
- Create: `apps/storybook/.storybook/main.ts`
|
||||
- Create: `apps/storybook/.storybook/preview.ts`
|
||||
|
||||
- [ ] **Step 1: Update apps/storybook/package.json**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@repo/storybook",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "storybook build",
|
||||
"dev": "storybook dev -p 6006",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@storybook/addon-essentials": "^8.6.0",
|
||||
"@storybook/react-vite": "^8.6.0",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"storybook": "^8.6.0",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"vite": "^6.3.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create .storybook/main.ts**
|
||||
|
||||
```typescript
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
framework: "@storybook/react-vite",
|
||||
stories: [
|
||||
"../../../packages/ui/src/**/*.stories.@(ts|tsx)",
|
||||
],
|
||||
addons: ["@storybook/addon-essentials"],
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig } = await import("vite");
|
||||
const tailwindPlugin = await import("@tailwindcss/vite");
|
||||
|
||||
return mergeConfig(config, {
|
||||
plugins: [tailwindPlugin.default()],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Create .storybook/preview.ts**
|
||||
|
||||
```typescript
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "../../../packages/ui/src/styles/globals.css";
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run pnpm install and commit**
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
git add apps/storybook/ pnpm-lock.yaml
|
||||
git commit -m "feat(storybook): add Storybook 8 with Tailwind v4 pulling stories from @repo/ui"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Verify build
|
||||
|
||||
- [ ] **Step 1: Run pnpm build**
|
||||
|
||||
Run: `pnpm build`
|
||||
Expected: All workspaces pass.
|
||||
|
||||
- [ ] **Step 2: Verify core tests still pass**
|
||||
|
||||
Run: `cd packages/core && pnpm vitest run`
|
||||
Expected: 22 tests pass.
|
||||
|
||||
- [ ] **Step 3: Commit any remaining fixes**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: finalize Plan 5 — UI system with Atomic Design"
|
||||
```
|
||||
Reference in New Issue
Block a user