@discourser/design-system 0.22.1 → 0.22.3
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/dist/{chunk-VJN7TIGL.js → chunk-GLPWI7OF.js} +3 -3
- package/dist/chunk-GLPWI7OF.js.map +1 -0
- package/dist/{chunk-IGCGVSG4.cjs → chunk-NN4YW27E.cjs} +3 -3
- package/dist/chunk-NN4YW27E.cjs.map +1 -0
- package/dist/figma-codex.json +2 -2
- package/dist/index.cjs +4 -4
- package/dist/index.js +1 -1
- package/dist/languages/transform.d.ts +1 -1
- package/dist/preset/index.cjs +2 -2
- package/dist/preset/index.js +1 -1
- package/dist/stories/foundations/components/ColorSwatch.d.ts +1 -15
- package/dist/stories/foundations/components/ColorSwatch.d.ts.map +1 -1
- package/dist/stories/foundations/components/ElevationCard.d.ts +3 -6
- package/dist/stories/foundations/components/ElevationCard.d.ts.map +1 -1
- package/dist/stories/foundations/components/SpacingBox.d.ts.map +1 -1
- package/dist/stories/foundations/components/TypeSpecimen.d.ts +2 -1
- package/dist/stories/foundations/components/TypeSpecimen.d.ts.map +1 -1
- package/docs/CSS_USAGE.md +235 -0
- package/docs/FIGMA_MAKE_SETUP.md +339 -0
- package/docs/GUIDELINES_REVIEW.md +728 -0
- package/docs/MAINTAINER_CHECKLIST.md +265 -0
- package/docs/TESTING_QUICK_REFERENCE.md +159 -0
- package/docs/TESTING_TOKENS.md +340 -0
- package/docs/active-stories/README.md +29 -0
- package/docs/active-stories/STORY-006a-figma-translation-foundations.md +324 -0
- package/docs/active-stories/STORY-006b-figma-translation-components.md +201 -0
- package/docs/active-stories/STORY-006c-figma-translation-layout-extension.md +258 -0
- package/docs/active-stories/STORY-008-kai-sidecar-fragments.md +137 -0
- package/docs/active-stories/STORY-011-verify-translation-docs.md +182 -0
- package/docs/archive/ARCHITECTURE-discourser-design-system.md +448 -0
- package/docs/claude-feed-back/ARCHITECTURE_DIAGRAM.md +243 -0
- package/docs/claude-feed-back/STYLING_VERIFICATION.md +89 -0
- package/docs/claude-feed-back/TEST_RESULTS.md +182 -0
- package/docs/context-share/ELEVATION_FIX_PLAN.md +903 -0
- package/docs/context-share/STORY-001-VALIDATION-PASSED.md +192 -0
- package/docs/context-share/STORY-002-IMPLEMENTATION-COMPLETE.md +161 -0
- package/docs/context-share/STORYBOOK_MCP_STRATEGY.md +867 -0
- package/docs/context-share/TESTING_GAPS_FILLED.md +353 -0
- package/docs/context-share/TOKEN_TESTING_SUMMARY.md +388 -0
- package/docs/context-share/code-connect-prompt.md +90 -0
- package/docs/context-share/dds-autonomous-pipeline.md +765 -0
- package/docs/context-share/fix-checkbox-radio-tokens.md +145 -0
- package/docs/context-share/icon-component-prompt.md +154 -0
- package/docs/context-share/icons/Audience.svg +3 -0
- package/docs/context-share/icons/AudioSpeaker.svg +3 -0
- package/docs/context-share/icons/BookmarkPlus.svg +3 -0
- package/docs/context-share/icons/ClipBoard.svg +8 -0
- package/docs/context-share/icons/DiscourserLogo.svg +4 -0
- package/docs/context-share/icons/ExitStudio.svg +4 -0
- package/docs/context-share/icons/Microphone.svg +5 -0
- package/docs/context-share/icons/NotebookPen.svg +3 -0
- package/docs/context-share/icons/PausePlay.svg +5 -0
- package/docs/context-share/icons/Play.svg +4 -0
- package/docs/context-share/icons/Record.svg +6 -0
- package/docs/context-share/icons/RepeatQuestion.svg +3 -0
- package/docs/context-share/icons/ScrollText.svg +3 -0
- package/docs/context-share/icons/Sparkles.svg +3 -0
- package/docs/context-share/icons/Speech.svg +3 -0
- package/docs/context-share/icons/StopPlay.svg +4 -0
- package/docs/context-share/icons/Timer.svg +3 -0
- package/docs/context-share/icons/UserProfile.svg +3 -0
- package/docs/context-share/m3-token-pipeline-audit.md +125 -0
- package/docs/context-share/storybook-mcp-kai-agent-revised-summary.md +211 -0
- package/docs/discourser-design-system-prd.md +3698 -0
- package/docs/figma-captures/01-typography.png +0 -0
- package/docs/figma-captures/02-button-iconbutton.png +0 -0
- package/docs/figma-captures/03-form-inputs.png +0 -0
- package/docs/figma-captures/04-form-controls.png +0 -0
- package/docs/figma-captures/05-data-display.png +0 -0
- package/docs/figma-captures/06-feedback.png +0 -0
- package/docs/figma-captures/07-overlays.png +0 -0
- package/docs/figma-captures/08-navigation-layout.png +0 -0
- package/docs/figma-captures/09-custom-components.png +0 -0
- package/docs/figma-captures/10-scenario-queue.png +0 -0
- package/docs/figma-captures/11-icon-library.png +0 -0
- package/docs/figma-make-docs/01-understanding-templates.md +235 -0
- package/docs/figma-make-docs/02-prerequisites.md +266 -0
- package/docs/figma-make-docs/03-creating-template.md +306 -0
- package/docs/figma-make-docs/04-adding-guidelines.md +448 -0
- package/docs/figma-make-docs/05-example-starter-code.md +590 -0
- package/docs/figma-make-docs/06-publishing-template.md +417 -0
- package/docs/figma-make-docs/07-maintenance.md +536 -0
- package/docs/figma-make-docs/08-faq.md +490 -0
- package/docs/figma-make-docs/README.md +95 -0
- package/docs/material-theme.json +418 -0
- package/docs/plans/2026-03-12-figma-token-export-rewrite.md +504 -0
- package/docs/plans/2026-03-12-step7-panda-token-resolution-design.md +119 -0
- package/docs/plans/2026-03-12-step7-panda-token-resolution.md +993 -0
- package/docs/token-name-mapping.json +850 -0
- package/docs/token-name-mapping.md +251 -0
- package/package.json +6 -4
- package/src/languages/transform.ts +1 -1
- package/src/stories/foundations/Borders.stories.tsx +138 -0
- package/src/stories/foundations/ColorScale.stories.tsx +737 -0
- package/src/stories/foundations/Colors.mdx +2 -131
- package/src/stories/foundations/Elevation.mdx +26 -45
- package/src/stories/foundations/Motion.stories.tsx +306 -0
- package/src/stories/foundations/Shape.stories.tsx +159 -0
- package/src/stories/foundations/Spacing.mdx +24 -25
- package/src/stories/foundations/Typography.mdx +93 -79
- package/src/stories/foundations/components/ColorSwatch.tsx +72 -109
- package/src/stories/foundations/components/ElevationCard.tsx +19 -22
- package/src/stories/foundations/components/SpacingBox.tsx +15 -2
- package/src/stories/foundations/components/TypeSpecimen.tsx +20 -21
- package/dist/chunk-IGCGVSG4.cjs.map +0 -1
- package/dist/chunk-VJN7TIGL.js.map +0 -1
|
@@ -0,0 +1,3698 @@
|
|
|
1
|
+
# Design System Repository Setup PRD
|
|
2
|
+
## Project Overview
|
|
3
|
+
|
|
4
|
+
Create a design system repository called `@discourser/design-system` using **Panda CSS** and **Ark UI**. This system uses an aesthetic-agnostic architecture where design languages (like Material Design 3) can be swapped by changing a single import.
|
|
5
|
+
|
|
6
|
+
## Technical Stack
|
|
7
|
+
|
|
8
|
+
- **Panda CSS** - Zero-runtime CSS-in-JS with token-first architecture
|
|
9
|
+
- **Ark UI** - Headless, accessible components built on Zag.js state machines
|
|
10
|
+
- **TypeScript** - Strict mode, full type safety
|
|
11
|
+
- **tsup** - Build tool for ESM/CJS output
|
|
12
|
+
- **Storybook** - Component documentation and testing
|
|
13
|
+
- **Vitest** - Unit testing
|
|
14
|
+
- **@material/material-color-utilities** - M3 color palette generation
|
|
15
|
+
- **pnpm** - Package manager
|
|
16
|
+
|
|
17
|
+
## Architecture: Three-Layer System
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Layer 1: Infrastructure (Unchanging)
|
|
21
|
+
├── Token pipeline
|
|
22
|
+
├── Build system (tsup, Storybook)
|
|
23
|
+
├── Component logic (Ark UI)
|
|
24
|
+
└── Type contracts
|
|
25
|
+
|
|
26
|
+
Layer 2: Design Language (Swappable)
|
|
27
|
+
├── Token values (colors, spacing, radii)
|
|
28
|
+
├── Semantic mappings
|
|
29
|
+
└── Motion patterns
|
|
30
|
+
|
|
31
|
+
Layer 3: Component Recipes (Derived)
|
|
32
|
+
├── Visual styling via Panda recipes
|
|
33
|
+
└── Variant definitions
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Repository Structure
|
|
37
|
+
|
|
38
|
+
Create this exact folder structure:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
design-system/
|
|
42
|
+
├── .github/
|
|
43
|
+
│ └── workflows/
|
|
44
|
+
│ ├── ci.yml
|
|
45
|
+
│ └── release.yml
|
|
46
|
+
├── .storybook/
|
|
47
|
+
│ ├── main.ts
|
|
48
|
+
│ ├── preview.ts
|
|
49
|
+
│ └── manager.ts
|
|
50
|
+
├── scripts/
|
|
51
|
+
│ ├── generate-palette.ts
|
|
52
|
+
│ ├── sync-tokens.ts
|
|
53
|
+
│ └── build.ts
|
|
54
|
+
├── src/
|
|
55
|
+
│ ├── contracts/
|
|
56
|
+
│ │ └── design-language.contract.ts
|
|
57
|
+
│ ├── languages/
|
|
58
|
+
│ │ ├── material3.language.ts
|
|
59
|
+
│ │ ├── transform.ts
|
|
60
|
+
│ │ └── index.ts
|
|
61
|
+
│ ├── tokens/
|
|
62
|
+
│ │ ├── typography.ts
|
|
63
|
+
│ │ ├── shadows.ts
|
|
64
|
+
│ │ ├── motion.ts
|
|
65
|
+
│ │ └── index.ts
|
|
66
|
+
│ ├── recipes/
|
|
67
|
+
│ │ ├── button.recipe.ts
|
|
68
|
+
│ │ ├── card.recipe.ts
|
|
69
|
+
│ │ ├── icon-button.recipe.ts
|
|
70
|
+
│ │ ├── input.recipe.ts
|
|
71
|
+
│ │ ├── dialog.recipe.ts
|
|
72
|
+
│ │ └── index.ts
|
|
73
|
+
│ ├── components/
|
|
74
|
+
│ │ ├── Button/
|
|
75
|
+
│ │ │ ├── Button.tsx
|
|
76
|
+
│ │ │ ├── Button.stories.tsx
|
|
77
|
+
│ │ │ └── index.ts
|
|
78
|
+
│ │ ├── Card/
|
|
79
|
+
│ │ │ ├── Card.tsx
|
|
80
|
+
│ │ │ ├── Card.stories.tsx
|
|
81
|
+
│ │ │ └── index.ts
|
|
82
|
+
│ │ ├── Dialog/
|
|
83
|
+
│ │ │ ├── Dialog.tsx
|
|
84
|
+
│ │ │ ├── Dialog.stories.tsx
|
|
85
|
+
│ │ │ └── index.ts
|
|
86
|
+
│ │ └── index.ts
|
|
87
|
+
│ ├── utils/
|
|
88
|
+
│ │ └── cn.ts
|
|
89
|
+
│ └── index.ts
|
|
90
|
+
├── tokens/
|
|
91
|
+
│ └── figma-export.json (placeholder)
|
|
92
|
+
├── styled-system/ # Generated by Panda (gitignore)
|
|
93
|
+
├── dist/ # Build output (gitignore)
|
|
94
|
+
├── panda.config.ts
|
|
95
|
+
├── tsconfig.json
|
|
96
|
+
├── tsup.config.ts
|
|
97
|
+
├── vitest.config.ts
|
|
98
|
+
├── package.json
|
|
99
|
+
├── .gitignore
|
|
100
|
+
├── .npmrc
|
|
101
|
+
└── README.md
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## File Contents
|
|
105
|
+
|
|
106
|
+
### package.json
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"name": "@discourser/design-system",
|
|
111
|
+
"version": "0.1.0",
|
|
112
|
+
"description": "Aesthetic-agnostic design system with Panda CSS and Ark UI",
|
|
113
|
+
"type": "module",
|
|
114
|
+
"main": "./dist/index.cjs",
|
|
115
|
+
"module": "./dist/index.js",
|
|
116
|
+
"types": "./dist/index.d.ts",
|
|
117
|
+
"exports": {
|
|
118
|
+
".": {
|
|
119
|
+
"import": "./dist/index.js",
|
|
120
|
+
"require": "./dist/index.cjs",
|
|
121
|
+
"types": "./dist/index.d.ts"
|
|
122
|
+
},
|
|
123
|
+
"./styled-system": {
|
|
124
|
+
"import": "./styled-system/index.mjs",
|
|
125
|
+
"require": "./styled-system/index.js"
|
|
126
|
+
},
|
|
127
|
+
"./styled-system/css": {
|
|
128
|
+
"import": "./styled-system/css/index.mjs",
|
|
129
|
+
"require": "./styled-system/css/index.js"
|
|
130
|
+
},
|
|
131
|
+
"./styled-system/tokens": {
|
|
132
|
+
"import": "./styled-system/tokens/index.mjs",
|
|
133
|
+
"require": "./styled-system/tokens/index.js"
|
|
134
|
+
},
|
|
135
|
+
"./styled-system/recipes": {
|
|
136
|
+
"import": "./styled-system/recipes/index.mjs",
|
|
137
|
+
"require": "./styled-system/recipes/index.js"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"files": [
|
|
141
|
+
"dist",
|
|
142
|
+
"styled-system"
|
|
143
|
+
],
|
|
144
|
+
"scripts": {
|
|
145
|
+
"dev": "storybook dev -p 6006",
|
|
146
|
+
"build": "pnpm build:panda && pnpm build:lib",
|
|
147
|
+
"build:panda": "panda codegen",
|
|
148
|
+
"build:lib": "tsup",
|
|
149
|
+
"build:storybook": "storybook build",
|
|
150
|
+
"prepare": "panda codegen",
|
|
151
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
152
|
+
"test": "vitest",
|
|
153
|
+
"test:ui": "vitest --ui",
|
|
154
|
+
"tokens:generate": "tsx scripts/generate-palette.ts",
|
|
155
|
+
"tokens:sync": "tsx scripts/sync-tokens.ts",
|
|
156
|
+
"tokens:full": "pnpm tokens:generate && pnpm build:panda",
|
|
157
|
+
"typecheck": "tsc --noEmit"
|
|
158
|
+
},
|
|
159
|
+
"peerDependencies": {
|
|
160
|
+
"react": ">=19.0.0",
|
|
161
|
+
"react-dom": ">=19.0.0"
|
|
162
|
+
},
|
|
163
|
+
"dependencies": {
|
|
164
|
+
"@ark-ui/react": "^4.4.0",
|
|
165
|
+
"clsx": "^2.1.1"
|
|
166
|
+
},
|
|
167
|
+
"devDependencies": {
|
|
168
|
+
"@material/material-color-utilities": "^0.3.0",
|
|
169
|
+
"@pandacss/dev": "^0.52.0",
|
|
170
|
+
"@storybook/addon-a11y": "^8.5.0",
|
|
171
|
+
"@storybook/addon-essentials": "^8.5.0",
|
|
172
|
+
"@storybook/react": "^8.5.0",
|
|
173
|
+
"@storybook/react-vite": "^8.5.0",
|
|
174
|
+
"@types/node": "^22.0.0",
|
|
175
|
+
"@types/react": "^19.0.0",
|
|
176
|
+
"@types/react-dom": "^19.0.0",
|
|
177
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
178
|
+
"eslint": "^9.0.0",
|
|
179
|
+
"react": "^19.0.0",
|
|
180
|
+
"react-dom": "^19.0.0",
|
|
181
|
+
"storybook": "^8.5.0",
|
|
182
|
+
"tsup": "^8.3.0",
|
|
183
|
+
"tsx": "^4.19.0",
|
|
184
|
+
"typescript": "^5.7.0",
|
|
185
|
+
"vite": "^6.0.0",
|
|
186
|
+
"vitest": "^2.1.0"
|
|
187
|
+
},
|
|
188
|
+
"keywords": [
|
|
189
|
+
"design-system",
|
|
190
|
+
"panda-css",
|
|
191
|
+
"ark-ui",
|
|
192
|
+
"material-design-3",
|
|
193
|
+
"react",
|
|
194
|
+
"components"
|
|
195
|
+
],
|
|
196
|
+
"license": "MIT",
|
|
197
|
+
"repository": {
|
|
198
|
+
"type": "git",
|
|
199
|
+
"url": "https://github.com/Tasty-Maker-Studio/Discourser-Design-System.git"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### tsconfig.json
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"compilerOptions": {
|
|
209
|
+
"target": "ES2020",
|
|
210
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
211
|
+
"module": "ESNext",
|
|
212
|
+
"moduleResolution": "bundler",
|
|
213
|
+
"jsx": "react-jsx",
|
|
214
|
+
"strict": true,
|
|
215
|
+
"declaration": true,
|
|
216
|
+
"declarationMap": true,
|
|
217
|
+
"sourceMap": true,
|
|
218
|
+
"outDir": "dist",
|
|
219
|
+
"baseUrl": ".",
|
|
220
|
+
"paths": {
|
|
221
|
+
"@/*": ["./src/*"],
|
|
222
|
+
"styled-system/*": ["./styled-system/*"]
|
|
223
|
+
},
|
|
224
|
+
"skipLibCheck": true,
|
|
225
|
+
"esModuleInterop": true,
|
|
226
|
+
"allowSyntheticDefaultImports": true,
|
|
227
|
+
"forceConsistentCasingInFileNames": true,
|
|
228
|
+
"resolveJsonModule": true,
|
|
229
|
+
"isolatedModules": true,
|
|
230
|
+
"noEmit": true
|
|
231
|
+
},
|
|
232
|
+
"include": ["src/**/*", "scripts/**/*", "panda.config.ts"],
|
|
233
|
+
"exclude": ["node_modules", "dist", "styled-system", "storybook-static"]
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### tsup.config.ts
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { defineConfig } from 'tsup';
|
|
241
|
+
|
|
242
|
+
export default defineConfig({
|
|
243
|
+
entry: ['src/index.ts'],
|
|
244
|
+
format: ['esm', 'cjs'],
|
|
245
|
+
dts: true,
|
|
246
|
+
splitting: true,
|
|
247
|
+
sourcemap: true,
|
|
248
|
+
clean: true,
|
|
249
|
+
external: ['react', 'react-dom'],
|
|
250
|
+
esbuildOptions(options) {
|
|
251
|
+
options.banner = {
|
|
252
|
+
js: '"use client"',
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### panda.config.ts
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { defineConfig } from '@pandacss/dev';
|
|
262
|
+
import { activeLanguage, transformToPandaTheme } from './src/languages';
|
|
263
|
+
import { buttonRecipe, cardRecipe, iconButtonRecipe } from './src/recipes';
|
|
264
|
+
|
|
265
|
+
const theme = transformToPandaTheme(activeLanguage);
|
|
266
|
+
|
|
267
|
+
export default defineConfig({
|
|
268
|
+
preflight: true,
|
|
269
|
+
|
|
270
|
+
include: [
|
|
271
|
+
'./src/**/*.{js,jsx,ts,tsx}',
|
|
272
|
+
'./stories/**/*.{js,jsx,ts,tsx}'
|
|
273
|
+
],
|
|
274
|
+
|
|
275
|
+
exclude: [],
|
|
276
|
+
|
|
277
|
+
outdir: 'styled-system',
|
|
278
|
+
|
|
279
|
+
jsxFramework: 'react',
|
|
280
|
+
|
|
281
|
+
layers: {
|
|
282
|
+
reset: 'reset',
|
|
283
|
+
base: 'base',
|
|
284
|
+
tokens: 'tokens',
|
|
285
|
+
recipes: 'recipes',
|
|
286
|
+
utilities: 'utilities'
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
theme: {
|
|
290
|
+
tokens: theme.tokens,
|
|
291
|
+
semanticTokens: theme.semanticTokens,
|
|
292
|
+
textStyles: theme.textStyles,
|
|
293
|
+
extend: {
|
|
294
|
+
recipes: {
|
|
295
|
+
button: buttonRecipe,
|
|
296
|
+
card: cardRecipe,
|
|
297
|
+
iconButton: iconButtonRecipe,
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
conditions: {
|
|
303
|
+
light: '[data-theme=light] &, .light &',
|
|
304
|
+
dark: '[data-theme=dark] &, .dark &'
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
globalCss: {
|
|
308
|
+
html: {
|
|
309
|
+
colorScheme: 'light dark'
|
|
310
|
+
},
|
|
311
|
+
body: {
|
|
312
|
+
fontFamily: 'body',
|
|
313
|
+
bg: 'surface',
|
|
314
|
+
color: 'onSurface',
|
|
315
|
+
textStyle: 'bodyMedium'
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### src/contracts/design-language.contract.ts
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
/**
|
|
325
|
+
* Design Language Contract
|
|
326
|
+
*
|
|
327
|
+
* Any aesthetic (M3, Carbon, Fluent, custom) must implement this interface.
|
|
328
|
+
* This enables swapping aesthetics by changing one import.
|
|
329
|
+
*/
|
|
330
|
+
|
|
331
|
+
export interface DesignLanguageContract {
|
|
332
|
+
name: string;
|
|
333
|
+
version: string;
|
|
334
|
+
colors: ColorPalettes;
|
|
335
|
+
semantic: SemanticColors;
|
|
336
|
+
semanticDark?: SemanticColors; // Optional dark theme overrides
|
|
337
|
+
typography: TypographyConfig;
|
|
338
|
+
spacing: SpacingScale;
|
|
339
|
+
shape: ShapeConfig;
|
|
340
|
+
elevation: ElevationConfig;
|
|
341
|
+
motion: MotionConfig;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Color Types
|
|
345
|
+
export interface ColorPalettes {
|
|
346
|
+
primary: TonalPalette;
|
|
347
|
+
secondary: TonalPalette;
|
|
348
|
+
tertiary: TonalPalette;
|
|
349
|
+
neutral: TonalPalette;
|
|
350
|
+
neutralVariant: TonalPalette;
|
|
351
|
+
error: TonalPalette;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface TonalPalette {
|
|
355
|
+
0: string;
|
|
356
|
+
10: string;
|
|
357
|
+
20: string;
|
|
358
|
+
30: string;
|
|
359
|
+
40: string;
|
|
360
|
+
50: string;
|
|
361
|
+
60: string;
|
|
362
|
+
70: string;
|
|
363
|
+
80: string;
|
|
364
|
+
90: string;
|
|
365
|
+
95: string;
|
|
366
|
+
99: string;
|
|
367
|
+
100: string;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export interface SemanticColors {
|
|
371
|
+
primary: string;
|
|
372
|
+
onPrimary: string;
|
|
373
|
+
primaryContainer: string;
|
|
374
|
+
onPrimaryContainer: string;
|
|
375
|
+
secondary: string;
|
|
376
|
+
onSecondary: string;
|
|
377
|
+
secondaryContainer: string;
|
|
378
|
+
onSecondaryContainer: string;
|
|
379
|
+
tertiary: string;
|
|
380
|
+
onTertiary: string;
|
|
381
|
+
tertiaryContainer: string;
|
|
382
|
+
onTertiaryContainer: string;
|
|
383
|
+
error: string;
|
|
384
|
+
onError: string;
|
|
385
|
+
errorContainer: string;
|
|
386
|
+
onErrorContainer: string;
|
|
387
|
+
surface: string;
|
|
388
|
+
onSurface: string;
|
|
389
|
+
surfaceVariant: string;
|
|
390
|
+
onSurfaceVariant: string;
|
|
391
|
+
surfaceContainerLowest: string;
|
|
392
|
+
surfaceContainerLow: string;
|
|
393
|
+
surfaceContainer: string;
|
|
394
|
+
surfaceContainerHigh: string;
|
|
395
|
+
surfaceContainerHighest: string;
|
|
396
|
+
outline: string;
|
|
397
|
+
outlineVariant: string;
|
|
398
|
+
inverseSurface: string;
|
|
399
|
+
inverseOnSurface: string;
|
|
400
|
+
inversePrimary: string;
|
|
401
|
+
background: string;
|
|
402
|
+
onBackground: string;
|
|
403
|
+
scrim: string;
|
|
404
|
+
shadow: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Typography Types
|
|
408
|
+
export interface TypographyConfig {
|
|
409
|
+
fonts: {
|
|
410
|
+
display: string;
|
|
411
|
+
body: string;
|
|
412
|
+
mono: string;
|
|
413
|
+
};
|
|
414
|
+
scale: TypographyScale;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export interface TypographyScale {
|
|
418
|
+
displayLarge: TypeStyle;
|
|
419
|
+
displayMedium: TypeStyle;
|
|
420
|
+
displaySmall: TypeStyle;
|
|
421
|
+
headlineLarge: TypeStyle;
|
|
422
|
+
headlineMedium: TypeStyle;
|
|
423
|
+
headlineSmall: TypeStyle;
|
|
424
|
+
titleLarge: TypeStyle;
|
|
425
|
+
titleMedium: TypeStyle;
|
|
426
|
+
titleSmall: TypeStyle;
|
|
427
|
+
bodyLarge: TypeStyle;
|
|
428
|
+
bodyMedium: TypeStyle;
|
|
429
|
+
bodySmall: TypeStyle;
|
|
430
|
+
labelLarge: TypeStyle;
|
|
431
|
+
labelMedium: TypeStyle;
|
|
432
|
+
labelSmall: TypeStyle;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export interface TypeStyle {
|
|
436
|
+
fontSize: string;
|
|
437
|
+
lineHeight: string;
|
|
438
|
+
fontWeight: string;
|
|
439
|
+
letterSpacing: string;
|
|
440
|
+
fontFamily?: 'display' | 'body' | 'mono';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Spacing Types
|
|
444
|
+
export interface SpacingScale {
|
|
445
|
+
none: string;
|
|
446
|
+
xxs: string;
|
|
447
|
+
xs: string;
|
|
448
|
+
sm: string;
|
|
449
|
+
md: string;
|
|
450
|
+
lg: string;
|
|
451
|
+
xl: string;
|
|
452
|
+
xxl: string;
|
|
453
|
+
xxxl: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Shape Types
|
|
457
|
+
export interface ShapeConfig {
|
|
458
|
+
radii: RadiiScale;
|
|
459
|
+
style: 'sharp' | 'rounded' | 'soft' | 'organic';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export interface RadiiScale {
|
|
463
|
+
none: string;
|
|
464
|
+
extraSmall: string;
|
|
465
|
+
small: string;
|
|
466
|
+
medium: string;
|
|
467
|
+
large: string;
|
|
468
|
+
extraLarge: string;
|
|
469
|
+
full: string;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Elevation Types
|
|
473
|
+
export interface ElevationConfig {
|
|
474
|
+
levels: ElevationScale;
|
|
475
|
+
style: 'shadow' | 'border' | 'blur' | 'flat';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export interface ElevationScale {
|
|
479
|
+
level0: string;
|
|
480
|
+
level1: string;
|
|
481
|
+
level2: string;
|
|
482
|
+
level3: string;
|
|
483
|
+
level4: string;
|
|
484
|
+
level5: string;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Motion Types
|
|
488
|
+
export interface MotionConfig {
|
|
489
|
+
durations: DurationScale;
|
|
490
|
+
easings: EasingScale;
|
|
491
|
+
style: 'expressive' | 'productive' | 'minimal';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export interface DurationScale {
|
|
495
|
+
instant: string;
|
|
496
|
+
fast: string;
|
|
497
|
+
normal: string;
|
|
498
|
+
slow: string;
|
|
499
|
+
slower: string;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export interface EasingScale {
|
|
503
|
+
standard: string;
|
|
504
|
+
standardDecelerate: string;
|
|
505
|
+
standardAccelerate: string;
|
|
506
|
+
emphasized: string;
|
|
507
|
+
emphasizedDecelerate: string;
|
|
508
|
+
emphasizedAccelerate: string;
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### src/languages/transform.ts
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import type { DesignLanguageContract, TonalPalette, SemanticColors } from '../contracts/design-language.contract';
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Transforms a DesignLanguageContract into Panda CSS theme configuration
|
|
519
|
+
*/
|
|
520
|
+
export function transformToPandaTheme(language: DesignLanguageContract) {
|
|
521
|
+
return {
|
|
522
|
+
tokens: transformTokens(language),
|
|
523
|
+
semanticTokens: transformSemanticTokens(language),
|
|
524
|
+
textStyles: transformTextStyles(language)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function transformTokens(language: DesignLanguageContract) {
|
|
529
|
+
return {
|
|
530
|
+
colors: transformColorPalettes(language.colors),
|
|
531
|
+
fonts: {
|
|
532
|
+
display: { value: language.typography.fonts.display },
|
|
533
|
+
body: { value: language.typography.fonts.body },
|
|
534
|
+
mono: { value: language.typography.fonts.mono }
|
|
535
|
+
},
|
|
536
|
+
fontSizes: extractFontSizes(language.typography.scale),
|
|
537
|
+
lineHeights: extractLineHeights(language.typography.scale),
|
|
538
|
+
fontWeights: extractFontWeights(language.typography.scale),
|
|
539
|
+
letterSpacings: extractLetterSpacings(language.typography.scale),
|
|
540
|
+
spacing: objectToTokens(language.spacing),
|
|
541
|
+
radii: objectToTokens(language.shape.radii),
|
|
542
|
+
shadows: objectToTokens(language.elevation.levels),
|
|
543
|
+
durations: objectToTokens(language.motion.durations),
|
|
544
|
+
easings: objectToTokens(language.motion.easings)
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function transformSemanticTokens(language: DesignLanguageContract) {
|
|
549
|
+
const light = language.semantic;
|
|
550
|
+
const dark = language.semanticDark || light; // Fallback to light if no dark
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
colors: Object.fromEntries(
|
|
554
|
+
Object.entries(light).map(([key, lightValue]) => [
|
|
555
|
+
key,
|
|
556
|
+
{
|
|
557
|
+
value: {
|
|
558
|
+
base: lightValue,
|
|
559
|
+
_dark: dark[key as keyof SemanticColors] || lightValue
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
])
|
|
563
|
+
)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function transformTextStyles(language: DesignLanguageContract) {
|
|
568
|
+
const scale = language.typography.scale;
|
|
569
|
+
|
|
570
|
+
return Object.fromEntries(
|
|
571
|
+
Object.entries(scale).map(([name, style]) => [
|
|
572
|
+
name,
|
|
573
|
+
{
|
|
574
|
+
value: {
|
|
575
|
+
fontFamily: style.fontFamily || 'body',
|
|
576
|
+
fontSize: style.fontSize,
|
|
577
|
+
lineHeight: style.lineHeight,
|
|
578
|
+
fontWeight: style.fontWeight,
|
|
579
|
+
letterSpacing: style.letterSpacing
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
])
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function transformColorPalettes(palettes: Record<string, TonalPalette>) {
|
|
587
|
+
return Object.fromEntries(
|
|
588
|
+
Object.entries(palettes).map(([name, palette]) => [
|
|
589
|
+
name,
|
|
590
|
+
Object.fromEntries(
|
|
591
|
+
Object.entries(palette).map(([tone, value]) => [
|
|
592
|
+
tone,
|
|
593
|
+
{ value }
|
|
594
|
+
])
|
|
595
|
+
)
|
|
596
|
+
])
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function objectToTokens<T extends Record<string, string>>(obj: T) {
|
|
601
|
+
return Object.fromEntries(
|
|
602
|
+
Object.entries(obj).map(([key, value]) => [key, { value }])
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function extractFontSizes(scale: Record<string, { fontSize: string }>) {
|
|
607
|
+
return Object.fromEntries(
|
|
608
|
+
Object.entries(scale).map(([name, style]) => [
|
|
609
|
+
name,
|
|
610
|
+
{ value: style.fontSize }
|
|
611
|
+
])
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function extractLineHeights(scale: Record<string, { lineHeight: string }>) {
|
|
616
|
+
return Object.fromEntries(
|
|
617
|
+
Object.entries(scale).map(([name, style]) => [
|
|
618
|
+
name,
|
|
619
|
+
{ value: style.lineHeight }
|
|
620
|
+
])
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function extractFontWeights(scale: Record<string, { fontWeight: string }>) {
|
|
625
|
+
const weights = new Map<string, string>();
|
|
626
|
+
Object.values(scale).forEach(style => {
|
|
627
|
+
weights.set(style.fontWeight, style.fontWeight);
|
|
628
|
+
});
|
|
629
|
+
return Object.fromEntries(
|
|
630
|
+
Array.from(weights.entries()).map(([key, value]) => [
|
|
631
|
+
key,
|
|
632
|
+
{ value }
|
|
633
|
+
])
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function extractLetterSpacings(scale: Record<string, { letterSpacing: string }>) {
|
|
638
|
+
return Object.fromEntries(
|
|
639
|
+
Object.entries(scale).map(([name, style]) => [
|
|
640
|
+
name,
|
|
641
|
+
{ value: style.letterSpacing }
|
|
642
|
+
])
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### src/languages/material3.language.ts
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import type { DesignLanguageContract } from '../contracts/design-language.contract';
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Material Design 3 Language Implementation
|
|
654
|
+
*
|
|
655
|
+
* Source color: #63A002 (TastyMakers green)
|
|
656
|
+
* Generated via Material Theme Builder plugin 2024-12-24
|
|
657
|
+
*/
|
|
658
|
+
export const material3Language: DesignLanguageContract = {
|
|
659
|
+
name: 'material3',
|
|
660
|
+
version: '1.0.0',
|
|
661
|
+
|
|
662
|
+
colors: {
|
|
663
|
+
// From Material Theme Builder export
|
|
664
|
+
primary: {
|
|
665
|
+
0: '#000000',
|
|
666
|
+
10: '#102000',
|
|
667
|
+
20: '#1F3700',
|
|
668
|
+
30: '#2F4F00',
|
|
669
|
+
40: '#3F6900',
|
|
670
|
+
50: '#518500',
|
|
671
|
+
60: '#64A104',
|
|
672
|
+
70: '#7DBD2A',
|
|
673
|
+
80: '#97D945',
|
|
674
|
+
90: '#B2F65F',
|
|
675
|
+
95: '#D2FF9B',
|
|
676
|
+
99: '#F9FFE9',
|
|
677
|
+
100: '#FFFFFF'
|
|
678
|
+
},
|
|
679
|
+
secondary: {
|
|
680
|
+
0: '#000000',
|
|
681
|
+
10: '#121F04',
|
|
682
|
+
20: '#263515',
|
|
683
|
+
30: '#3C4C2A',
|
|
684
|
+
40: '#54643F',
|
|
685
|
+
50: '#6C7D56',
|
|
686
|
+
60: '#85976E',
|
|
687
|
+
70: '#A0B187',
|
|
688
|
+
80: '#BBCDA1',
|
|
689
|
+
90: '#D7E9BB',
|
|
690
|
+
95: '#E5F7C9',
|
|
691
|
+
99: '#F9FFE9',
|
|
692
|
+
100: '#FFFFFF'
|
|
693
|
+
},
|
|
694
|
+
tertiary: {
|
|
695
|
+
0: '#000000',
|
|
696
|
+
10: '#00201E',
|
|
697
|
+
20: '#003735',
|
|
698
|
+
30: '#00504C',
|
|
699
|
+
40: '#046A66',
|
|
700
|
+
50: '#30837F',
|
|
701
|
+
60: '#4D9D98',
|
|
702
|
+
70: '#69B8B3',
|
|
703
|
+
80: '#85D4CF',
|
|
704
|
+
90: '#A1F1EB',
|
|
705
|
+
95: '#B0FFF9',
|
|
706
|
+
99: '#F2FFFD',
|
|
707
|
+
100: '#FFFFFF'
|
|
708
|
+
},
|
|
709
|
+
neutral: {
|
|
710
|
+
0: '#000000',
|
|
711
|
+
10: '#1B1C18',
|
|
712
|
+
20: '#30312C',
|
|
713
|
+
30: '#464742',
|
|
714
|
+
40: '#5E5F59',
|
|
715
|
+
50: '#777771',
|
|
716
|
+
60: '#91918B',
|
|
717
|
+
70: '#ABACA5',
|
|
718
|
+
80: '#C7C7C0',
|
|
719
|
+
90: '#E3E3DB',
|
|
720
|
+
95: '#F2F1E9',
|
|
721
|
+
99: '#FDFCF5',
|
|
722
|
+
100: '#FFFFFF'
|
|
723
|
+
},
|
|
724
|
+
neutralVariant: {
|
|
725
|
+
0: '#000000',
|
|
726
|
+
10: '#191D14',
|
|
727
|
+
20: '#2E3228',
|
|
728
|
+
30: '#44483D',
|
|
729
|
+
40: '#5C6054',
|
|
730
|
+
50: '#75796C',
|
|
731
|
+
60: '#8F9285',
|
|
732
|
+
70: '#A9AD9F',
|
|
733
|
+
80: '#C5C8BA',
|
|
734
|
+
90: '#E1E4D5',
|
|
735
|
+
95: '#EFF2E3',
|
|
736
|
+
99: '#FBFEEE',
|
|
737
|
+
100: '#FFFFFF'
|
|
738
|
+
},
|
|
739
|
+
error: {
|
|
740
|
+
0: '#000000',
|
|
741
|
+
10: '#410E0B',
|
|
742
|
+
20: '#601410',
|
|
743
|
+
30: '#8C1D18',
|
|
744
|
+
40: '#B3261E',
|
|
745
|
+
50: '#DC362E',
|
|
746
|
+
60: '#E46962',
|
|
747
|
+
70: '#EC928E',
|
|
748
|
+
80: '#F2B8B5',
|
|
749
|
+
90: '#F9DEDC',
|
|
750
|
+
95: '#FCEEEE',
|
|
751
|
+
99: '#FFFBF9',
|
|
752
|
+
100: '#FFFFFF'
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
semantic: {
|
|
757
|
+
// Light theme from Material Theme Builder
|
|
758
|
+
primary: '#4C662B',
|
|
759
|
+
onPrimary: '#FFFFFF',
|
|
760
|
+
primaryContainer: '#CDEDA3',
|
|
761
|
+
onPrimaryContainer: '#354E16',
|
|
762
|
+
|
|
763
|
+
secondary: '#586249',
|
|
764
|
+
onSecondary: '#FFFFFF',
|
|
765
|
+
secondaryContainer: '#DCE7C8',
|
|
766
|
+
onSecondaryContainer: '#404A33',
|
|
767
|
+
|
|
768
|
+
tertiary: '#386663',
|
|
769
|
+
onTertiary: '#FFFFFF',
|
|
770
|
+
tertiaryContainer: '#BCECE7',
|
|
771
|
+
onTertiaryContainer: '#1F4E4B',
|
|
772
|
+
|
|
773
|
+
error: '#BA1A1A',
|
|
774
|
+
onError: '#FFFFFF',
|
|
775
|
+
errorContainer: '#FFDAD6',
|
|
776
|
+
onErrorContainer: '#93000A',
|
|
777
|
+
|
|
778
|
+
surface: '#F9FAEF',
|
|
779
|
+
onSurface: '#1A1C16',
|
|
780
|
+
surfaceVariant: '#E1E4D5',
|
|
781
|
+
onSurfaceVariant: '#44483D',
|
|
782
|
+
|
|
783
|
+
surfaceContainerLowest: '#FFFFFF',
|
|
784
|
+
surfaceContainerLow: '#F3F4E9',
|
|
785
|
+
surfaceContainer: '#EEEFE3',
|
|
786
|
+
surfaceContainerHigh: '#E8E9DE',
|
|
787
|
+
surfaceContainerHighest: '#E2E3D8',
|
|
788
|
+
|
|
789
|
+
outline: '#75796C',
|
|
790
|
+
outlineVariant: '#C5C8BA',
|
|
791
|
+
|
|
792
|
+
inverseSurface: '#2F312A',
|
|
793
|
+
inverseOnSurface: '#F1F2E6',
|
|
794
|
+
inversePrimary: '#B1D18A',
|
|
795
|
+
|
|
796
|
+
background: '#F9FAEF',
|
|
797
|
+
onBackground: '#1A1C16',
|
|
798
|
+
|
|
799
|
+
scrim: '#000000',
|
|
800
|
+
shadow: '#000000'
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
// Dark theme semantic colors (for reference/dark mode implementation)
|
|
804
|
+
semanticDark: {
|
|
805
|
+
primary: '#B1D18A',
|
|
806
|
+
onPrimary: '#1F3701',
|
|
807
|
+
primaryContainer: '#354E16',
|
|
808
|
+
onPrimaryContainer: '#CDEDA3',
|
|
809
|
+
|
|
810
|
+
secondary: '#BFCBAD',
|
|
811
|
+
onSecondary: '#2A331E',
|
|
812
|
+
secondaryContainer: '#404A33',
|
|
813
|
+
onSecondaryContainer: '#DCE7C8',
|
|
814
|
+
|
|
815
|
+
tertiary: '#A0D0CB',
|
|
816
|
+
onTertiary: '#003735',
|
|
817
|
+
tertiaryContainer: '#1F4E4B',
|
|
818
|
+
onTertiaryContainer: '#BCECE7',
|
|
819
|
+
|
|
820
|
+
error: '#FFB4AB',
|
|
821
|
+
onError: '#690005',
|
|
822
|
+
errorContainer: '#93000A',
|
|
823
|
+
onErrorContainer: '#FFDAD6',
|
|
824
|
+
|
|
825
|
+
surface: '#12140E',
|
|
826
|
+
onSurface: '#E2E3D8',
|
|
827
|
+
surfaceVariant: '#44483D',
|
|
828
|
+
onSurfaceVariant: '#C5C8BA',
|
|
829
|
+
|
|
830
|
+
surfaceContainerLowest: '#0C0F09',
|
|
831
|
+
surfaceContainerLow: '#1A1C16',
|
|
832
|
+
surfaceContainer: '#1E201A',
|
|
833
|
+
surfaceContainerHigh: '#282B24',
|
|
834
|
+
surfaceContainerHighest: '#33362E',
|
|
835
|
+
|
|
836
|
+
outline: '#8F9285',
|
|
837
|
+
outlineVariant: '#44483D',
|
|
838
|
+
|
|
839
|
+
inverseSurface: '#E2E3D8',
|
|
840
|
+
inverseOnSurface: '#2F312A',
|
|
841
|
+
inversePrimary: '#4C662B',
|
|
842
|
+
|
|
843
|
+
background: '#12140E',
|
|
844
|
+
onBackground: '#E2E3D8',
|
|
845
|
+
|
|
846
|
+
scrim: '#000000',
|
|
847
|
+
shadow: '#000000'
|
|
848
|
+
},
|
|
849
|
+
|
|
850
|
+
typography: {
|
|
851
|
+
fonts: {
|
|
852
|
+
display: 'Georgia, "Times New Roman", serif',
|
|
853
|
+
body: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
854
|
+
mono: '"JetBrains Mono", "Fira Code", Consolas, monospace'
|
|
855
|
+
},
|
|
856
|
+
scale: {
|
|
857
|
+
displayLarge: {
|
|
858
|
+
fontSize: '57px',
|
|
859
|
+
lineHeight: '64px',
|
|
860
|
+
fontWeight: '400',
|
|
861
|
+
letterSpacing: '-0.25px',
|
|
862
|
+
fontFamily: 'display'
|
|
863
|
+
},
|
|
864
|
+
displayMedium: {
|
|
865
|
+
fontSize: '45px',
|
|
866
|
+
lineHeight: '52px',
|
|
867
|
+
fontWeight: '400',
|
|
868
|
+
letterSpacing: '0px',
|
|
869
|
+
fontFamily: 'display'
|
|
870
|
+
},
|
|
871
|
+
displaySmall: {
|
|
872
|
+
fontSize: '36px',
|
|
873
|
+
lineHeight: '44px',
|
|
874
|
+
fontWeight: '400',
|
|
875
|
+
letterSpacing: '0px',
|
|
876
|
+
fontFamily: 'display'
|
|
877
|
+
},
|
|
878
|
+
headlineLarge: {
|
|
879
|
+
fontSize: '32px',
|
|
880
|
+
lineHeight: '40px',
|
|
881
|
+
fontWeight: '400',
|
|
882
|
+
letterSpacing: '0px',
|
|
883
|
+
fontFamily: 'display'
|
|
884
|
+
},
|
|
885
|
+
headlineMedium: {
|
|
886
|
+
fontSize: '28px',
|
|
887
|
+
lineHeight: '36px',
|
|
888
|
+
fontWeight: '400',
|
|
889
|
+
letterSpacing: '0px',
|
|
890
|
+
fontFamily: 'display'
|
|
891
|
+
},
|
|
892
|
+
headlineSmall: {
|
|
893
|
+
fontSize: '24px',
|
|
894
|
+
lineHeight: '32px',
|
|
895
|
+
fontWeight: '400',
|
|
896
|
+
letterSpacing: '0px',
|
|
897
|
+
fontFamily: 'display'
|
|
898
|
+
},
|
|
899
|
+
titleLarge: {
|
|
900
|
+
fontSize: '22px',
|
|
901
|
+
lineHeight: '28px',
|
|
902
|
+
fontWeight: '500',
|
|
903
|
+
letterSpacing: '0px',
|
|
904
|
+
fontFamily: 'body'
|
|
905
|
+
},
|
|
906
|
+
titleMedium: {
|
|
907
|
+
fontSize: '16px',
|
|
908
|
+
lineHeight: '24px',
|
|
909
|
+
fontWeight: '500',
|
|
910
|
+
letterSpacing: '0.15px',
|
|
911
|
+
fontFamily: 'body'
|
|
912
|
+
},
|
|
913
|
+
titleSmall: {
|
|
914
|
+
fontSize: '14px',
|
|
915
|
+
lineHeight: '20px',
|
|
916
|
+
fontWeight: '500',
|
|
917
|
+
letterSpacing: '0.1px',
|
|
918
|
+
fontFamily: 'body'
|
|
919
|
+
},
|
|
920
|
+
bodyLarge: {
|
|
921
|
+
fontSize: '16px',
|
|
922
|
+
lineHeight: '24px',
|
|
923
|
+
fontWeight: '400',
|
|
924
|
+
letterSpacing: '0.5px',
|
|
925
|
+
fontFamily: 'body'
|
|
926
|
+
},
|
|
927
|
+
bodyMedium: {
|
|
928
|
+
fontSize: '14px',
|
|
929
|
+
lineHeight: '20px',
|
|
930
|
+
fontWeight: '400',
|
|
931
|
+
letterSpacing: '0.25px',
|
|
932
|
+
fontFamily: 'body'
|
|
933
|
+
},
|
|
934
|
+
bodySmall: {
|
|
935
|
+
fontSize: '12px',
|
|
936
|
+
lineHeight: '16px',
|
|
937
|
+
fontWeight: '400',
|
|
938
|
+
letterSpacing: '0.4px',
|
|
939
|
+
fontFamily: 'body'
|
|
940
|
+
},
|
|
941
|
+
labelLarge: {
|
|
942
|
+
fontSize: '14px',
|
|
943
|
+
lineHeight: '20px',
|
|
944
|
+
fontWeight: '500',
|
|
945
|
+
letterSpacing: '0.1px',
|
|
946
|
+
fontFamily: 'body'
|
|
947
|
+
},
|
|
948
|
+
labelMedium: {
|
|
949
|
+
fontSize: '12px',
|
|
950
|
+
lineHeight: '16px',
|
|
951
|
+
fontWeight: '500',
|
|
952
|
+
letterSpacing: '0.5px',
|
|
953
|
+
fontFamily: 'body'
|
|
954
|
+
},
|
|
955
|
+
labelSmall: {
|
|
956
|
+
fontSize: '11px',
|
|
957
|
+
lineHeight: '16px',
|
|
958
|
+
fontWeight: '500',
|
|
959
|
+
letterSpacing: '0.5px',
|
|
960
|
+
fontFamily: 'body'
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
|
|
965
|
+
spacing: {
|
|
966
|
+
none: '0px',
|
|
967
|
+
xxs: '2px',
|
|
968
|
+
xs: '4px',
|
|
969
|
+
sm: '8px',
|
|
970
|
+
md: '16px',
|
|
971
|
+
lg: '24px',
|
|
972
|
+
xl: '32px',
|
|
973
|
+
xxl: '48px',
|
|
974
|
+
xxxl: '64px'
|
|
975
|
+
},
|
|
976
|
+
|
|
977
|
+
shape: {
|
|
978
|
+
radii: {
|
|
979
|
+
none: '0px',
|
|
980
|
+
extraSmall: '4px',
|
|
981
|
+
small: '8px',
|
|
982
|
+
medium: '12px',
|
|
983
|
+
large: '16px',
|
|
984
|
+
extraLarge: '28px',
|
|
985
|
+
full: '9999px'
|
|
986
|
+
},
|
|
987
|
+
style: 'rounded'
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
elevation: {
|
|
991
|
+
levels: {
|
|
992
|
+
level0: 'none',
|
|
993
|
+
level1: '0px 1px 2px rgba(0, 0, 0, 0.3), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)',
|
|
994
|
+
level2: '0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15)',
|
|
995
|
+
level3: '0px 4px 8px 3px rgba(0, 0, 0, 0.15), 0px 1px 3px rgba(0, 0, 0, 0.3)',
|
|
996
|
+
level4: '0px 6px 10px 4px rgba(0, 0, 0, 0.15), 0px 2px 3px rgba(0, 0, 0, 0.3)',
|
|
997
|
+
level5: '0px 8px 12px 6px rgba(0, 0, 0, 0.15), 0px 4px 4px rgba(0, 0, 0, 0.3)'
|
|
998
|
+
},
|
|
999
|
+
style: 'shadow'
|
|
1000
|
+
},
|
|
1001
|
+
|
|
1002
|
+
motion: {
|
|
1003
|
+
durations: {
|
|
1004
|
+
instant: '0ms',
|
|
1005
|
+
fast: '100ms',
|
|
1006
|
+
normal: '200ms',
|
|
1007
|
+
slow: '300ms',
|
|
1008
|
+
slower: '500ms'
|
|
1009
|
+
},
|
|
1010
|
+
easings: {
|
|
1011
|
+
standard: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
1012
|
+
standardDecelerate: 'cubic-bezier(0, 0, 0, 1)',
|
|
1013
|
+
standardAccelerate: 'cubic-bezier(0.3, 0, 1, 1)',
|
|
1014
|
+
emphasized: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
1015
|
+
emphasizedDecelerate: 'cubic-bezier(0.05, 0.7, 0.1, 1)',
|
|
1016
|
+
emphasizedAccelerate: 'cubic-bezier(0.3, 0, 0.8, 0.15)'
|
|
1017
|
+
},
|
|
1018
|
+
style: 'expressive'
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### src/languages/index.ts
|
|
1024
|
+
|
|
1025
|
+
```typescript
|
|
1026
|
+
// Export the active language
|
|
1027
|
+
// Change this import to switch aesthetics
|
|
1028
|
+
export { material3Language as activeLanguage } from './material3.language';
|
|
1029
|
+
|
|
1030
|
+
// Re-export transformer
|
|
1031
|
+
export { transformToPandaTheme } from './transform';
|
|
1032
|
+
|
|
1033
|
+
// Re-export types
|
|
1034
|
+
export type { DesignLanguageContract } from '../contracts/design-language.contract';
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
### src/recipes/button.recipe.ts
|
|
1038
|
+
|
|
1039
|
+
```typescript
|
|
1040
|
+
import { defineRecipe } from '@pandacss/dev';
|
|
1041
|
+
|
|
1042
|
+
export const buttonRecipe = defineRecipe({
|
|
1043
|
+
className: 'button',
|
|
1044
|
+
description: 'Material Design 3 button component',
|
|
1045
|
+
base: {
|
|
1046
|
+
display: 'inline-flex',
|
|
1047
|
+
alignItems: 'center',
|
|
1048
|
+
justifyContent: 'center',
|
|
1049
|
+
gap: 'sm',
|
|
1050
|
+
fontFamily: 'body',
|
|
1051
|
+
fontWeight: '500',
|
|
1052
|
+
borderRadius: 'full',
|
|
1053
|
+
cursor: 'pointer',
|
|
1054
|
+
transition: 'all',
|
|
1055
|
+
transitionDuration: 'fast',
|
|
1056
|
+
transitionTimingFunction: 'standard',
|
|
1057
|
+
outline: 'none',
|
|
1058
|
+
border: 'none',
|
|
1059
|
+
textDecoration: 'none',
|
|
1060
|
+
_disabled: {
|
|
1061
|
+
opacity: 0.38,
|
|
1062
|
+
cursor: 'not-allowed',
|
|
1063
|
+
pointerEvents: 'none'
|
|
1064
|
+
},
|
|
1065
|
+
_focusVisible: {
|
|
1066
|
+
outline: '2px solid',
|
|
1067
|
+
outlineColor: 'primary',
|
|
1068
|
+
outlineOffset: '2px'
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
variants: {
|
|
1072
|
+
variant: {
|
|
1073
|
+
filled: {
|
|
1074
|
+
bg: 'primary',
|
|
1075
|
+
color: 'onPrimary',
|
|
1076
|
+
_hover: {
|
|
1077
|
+
opacity: 0.92,
|
|
1078
|
+
shadow: 'level1'
|
|
1079
|
+
},
|
|
1080
|
+
_active: {
|
|
1081
|
+
opacity: 0.88
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
outlined: {
|
|
1085
|
+
bg: 'transparent',
|
|
1086
|
+
color: 'primary',
|
|
1087
|
+
borderWidth: '1px',
|
|
1088
|
+
borderStyle: 'solid',
|
|
1089
|
+
borderColor: 'outline',
|
|
1090
|
+
_hover: {
|
|
1091
|
+
bg: 'primary',
|
|
1092
|
+
bgOpacity: 0.08
|
|
1093
|
+
},
|
|
1094
|
+
_active: {
|
|
1095
|
+
bg: 'primary',
|
|
1096
|
+
bgOpacity: 0.12
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
text: {
|
|
1100
|
+
bg: 'transparent',
|
|
1101
|
+
color: 'primary',
|
|
1102
|
+
_hover: {
|
|
1103
|
+
bg: 'primary',
|
|
1104
|
+
bgOpacity: 0.08
|
|
1105
|
+
},
|
|
1106
|
+
_active: {
|
|
1107
|
+
bg: 'primary',
|
|
1108
|
+
bgOpacity: 0.12
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
elevated: {
|
|
1112
|
+
bg: 'surfaceContainerLow',
|
|
1113
|
+
color: 'primary',
|
|
1114
|
+
shadow: 'level1',
|
|
1115
|
+
_hover: {
|
|
1116
|
+
shadow: 'level2',
|
|
1117
|
+
bg: 'surfaceContainerLow'
|
|
1118
|
+
},
|
|
1119
|
+
_active: {
|
|
1120
|
+
shadow: 'level1'
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
tonal: {
|
|
1124
|
+
bg: 'secondaryContainer',
|
|
1125
|
+
color: 'onSecondaryContainer',
|
|
1126
|
+
_hover: {
|
|
1127
|
+
shadow: 'level1'
|
|
1128
|
+
},
|
|
1129
|
+
_active: {
|
|
1130
|
+
shadow: 'none'
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
size: {
|
|
1135
|
+
sm: {
|
|
1136
|
+
height: '32px',
|
|
1137
|
+
px: 'md',
|
|
1138
|
+
fontSize: 'labelMedium',
|
|
1139
|
+
lineHeight: 'labelMedium'
|
|
1140
|
+
},
|
|
1141
|
+
md: {
|
|
1142
|
+
height: '40px',
|
|
1143
|
+
px: 'lg',
|
|
1144
|
+
fontSize: 'labelLarge',
|
|
1145
|
+
lineHeight: 'labelLarge'
|
|
1146
|
+
},
|
|
1147
|
+
lg: {
|
|
1148
|
+
height: '48px',
|
|
1149
|
+
px: 'xl',
|
|
1150
|
+
fontSize: 'labelLarge',
|
|
1151
|
+
lineHeight: 'labelLarge'
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
defaultVariants: {
|
|
1156
|
+
variant: 'filled',
|
|
1157
|
+
size: 'md'
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
### src/recipes/card.recipe.ts
|
|
1163
|
+
|
|
1164
|
+
```typescript
|
|
1165
|
+
import { defineRecipe } from '@pandacss/dev';
|
|
1166
|
+
|
|
1167
|
+
export const cardRecipe = defineRecipe({
|
|
1168
|
+
className: 'card',
|
|
1169
|
+
description: 'Material Design 3 card component',
|
|
1170
|
+
base: {
|
|
1171
|
+
display: 'flex',
|
|
1172
|
+
flexDirection: 'column',
|
|
1173
|
+
borderRadius: 'medium',
|
|
1174
|
+
overflow: 'hidden',
|
|
1175
|
+
transition: 'all',
|
|
1176
|
+
transitionDuration: 'fast',
|
|
1177
|
+
transitionTimingFunction: 'standard'
|
|
1178
|
+
},
|
|
1179
|
+
variants: {
|
|
1180
|
+
variant: {
|
|
1181
|
+
elevated: {
|
|
1182
|
+
bg: 'surfaceContainerLow',
|
|
1183
|
+
shadow: 'level1',
|
|
1184
|
+
_hover: {
|
|
1185
|
+
shadow: 'level2'
|
|
1186
|
+
}
|
|
1187
|
+
},
|
|
1188
|
+
filled: {
|
|
1189
|
+
bg: 'surfaceContainerHighest'
|
|
1190
|
+
},
|
|
1191
|
+
outlined: {
|
|
1192
|
+
bg: 'surface',
|
|
1193
|
+
borderWidth: '1px',
|
|
1194
|
+
borderStyle: 'solid',
|
|
1195
|
+
borderColor: 'outlineVariant'
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
interactive: {
|
|
1199
|
+
true: {
|
|
1200
|
+
cursor: 'pointer',
|
|
1201
|
+
_hover: {
|
|
1202
|
+
opacity: 0.96
|
|
1203
|
+
},
|
|
1204
|
+
_active: {
|
|
1205
|
+
opacity: 0.92
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
defaultVariants: {
|
|
1211
|
+
variant: 'elevated',
|
|
1212
|
+
interactive: false
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
### src/recipes/icon-button.recipe.ts
|
|
1218
|
+
|
|
1219
|
+
```typescript
|
|
1220
|
+
import { defineRecipe } from '@pandacss/dev';
|
|
1221
|
+
|
|
1222
|
+
export const iconButtonRecipe = defineRecipe({
|
|
1223
|
+
className: 'icon-button',
|
|
1224
|
+
description: 'Material Design 3 icon button component',
|
|
1225
|
+
base: {
|
|
1226
|
+
display: 'inline-flex',
|
|
1227
|
+
alignItems: 'center',
|
|
1228
|
+
justifyContent: 'center',
|
|
1229
|
+
borderRadius: 'full',
|
|
1230
|
+
cursor: 'pointer',
|
|
1231
|
+
transition: 'all',
|
|
1232
|
+
transitionDuration: 'fast',
|
|
1233
|
+
transitionTimingFunction: 'standard',
|
|
1234
|
+
outline: 'none',
|
|
1235
|
+
border: 'none',
|
|
1236
|
+
p: '0',
|
|
1237
|
+
_disabled: {
|
|
1238
|
+
opacity: 0.38,
|
|
1239
|
+
cursor: 'not-allowed',
|
|
1240
|
+
pointerEvents: 'none'
|
|
1241
|
+
},
|
|
1242
|
+
_focusVisible: {
|
|
1243
|
+
outline: '2px solid',
|
|
1244
|
+
outlineColor: 'primary',
|
|
1245
|
+
outlineOffset: '2px'
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
variants: {
|
|
1249
|
+
variant: {
|
|
1250
|
+
standard: {
|
|
1251
|
+
bg: 'transparent',
|
|
1252
|
+
color: 'onSurfaceVariant',
|
|
1253
|
+
_hover: {
|
|
1254
|
+
bg: 'onSurfaceVariant',
|
|
1255
|
+
bgOpacity: 0.08
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
filled: {
|
|
1259
|
+
bg: 'primary',
|
|
1260
|
+
color: 'onPrimary',
|
|
1261
|
+
_hover: {
|
|
1262
|
+
opacity: 0.92
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
tonal: {
|
|
1266
|
+
bg: 'secondaryContainer',
|
|
1267
|
+
color: 'onSecondaryContainer',
|
|
1268
|
+
_hover: {
|
|
1269
|
+
opacity: 0.92
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
outlined: {
|
|
1273
|
+
bg: 'transparent',
|
|
1274
|
+
color: 'onSurfaceVariant',
|
|
1275
|
+
borderWidth: '1px',
|
|
1276
|
+
borderStyle: 'solid',
|
|
1277
|
+
borderColor: 'outline',
|
|
1278
|
+
_hover: {
|
|
1279
|
+
bg: 'onSurfaceVariant',
|
|
1280
|
+
bgOpacity: 0.08
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
size: {
|
|
1285
|
+
sm: {
|
|
1286
|
+
width: '32px',
|
|
1287
|
+
height: '32px',
|
|
1288
|
+
'& svg': {
|
|
1289
|
+
width: '18px',
|
|
1290
|
+
height: '18px'
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
md: {
|
|
1294
|
+
width: '40px',
|
|
1295
|
+
height: '40px',
|
|
1296
|
+
'& svg': {
|
|
1297
|
+
width: '24px',
|
|
1298
|
+
height: '24px'
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
lg: {
|
|
1302
|
+
width: '48px',
|
|
1303
|
+
height: '48px',
|
|
1304
|
+
'& svg': {
|
|
1305
|
+
width: '24px',
|
|
1306
|
+
height: '24px'
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
defaultVariants: {
|
|
1312
|
+
variant: 'standard',
|
|
1313
|
+
size: 'md'
|
|
1314
|
+
}
|
|
1315
|
+
});
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
### src/recipes/index.ts
|
|
1319
|
+
|
|
1320
|
+
```typescript
|
|
1321
|
+
export { buttonRecipe } from './button.recipe';
|
|
1322
|
+
export { cardRecipe } from './card.recipe';
|
|
1323
|
+
export { iconButtonRecipe } from './icon-button.recipe';
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
### src/utils/cn.ts
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
import { clsx, type ClassValue } from 'clsx';
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Utility function to merge class names
|
|
1333
|
+
*/
|
|
1334
|
+
export function cn(...inputs: ClassValue[]) {
|
|
1335
|
+
return clsx(inputs);
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
### src/components/Button/Button.tsx
|
|
1340
|
+
|
|
1341
|
+
```typescript
|
|
1342
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
|
|
1343
|
+
import { button, type ButtonVariantProps } from 'styled-system/recipes';
|
|
1344
|
+
import { cn } from '../../utils/cn';
|
|
1345
|
+
|
|
1346
|
+
export interface ButtonProps
|
|
1347
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
1348
|
+
ButtonVariantProps {
|
|
1349
|
+
children: ReactNode;
|
|
1350
|
+
leftIcon?: ReactNode;
|
|
1351
|
+
rightIcon?: ReactNode;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
1355
|
+
(
|
|
1356
|
+
{ children, variant, size, leftIcon, rightIcon, className, ...props },
|
|
1357
|
+
ref
|
|
1358
|
+
) => {
|
|
1359
|
+
return (
|
|
1360
|
+
<button
|
|
1361
|
+
ref={ref}
|
|
1362
|
+
className={cn(button({ variant, size }), className)}
|
|
1363
|
+
{...props}
|
|
1364
|
+
>
|
|
1365
|
+
{leftIcon}
|
|
1366
|
+
{children}
|
|
1367
|
+
{rightIcon}
|
|
1368
|
+
</button>
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
);
|
|
1372
|
+
|
|
1373
|
+
Button.displayName = 'Button';
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
### src/components/Button/Button.stories.tsx
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
1380
|
+
import { Button } from './Button';
|
|
1381
|
+
|
|
1382
|
+
const meta: Meta<typeof Button> = {
|
|
1383
|
+
title: 'Components/Button',
|
|
1384
|
+
component: Button,
|
|
1385
|
+
parameters: {
|
|
1386
|
+
layout: 'centered',
|
|
1387
|
+
},
|
|
1388
|
+
tags: ['autodocs'],
|
|
1389
|
+
argTypes: {
|
|
1390
|
+
variant: {
|
|
1391
|
+
control: 'select',
|
|
1392
|
+
options: ['filled', 'outlined', 'text', 'elevated', 'tonal'],
|
|
1393
|
+
},
|
|
1394
|
+
size: {
|
|
1395
|
+
control: 'select',
|
|
1396
|
+
options: ['sm', 'md', 'lg'],
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
export default meta;
|
|
1402
|
+
type Story = StoryObj<typeof meta>;
|
|
1403
|
+
|
|
1404
|
+
export const Filled: Story = {
|
|
1405
|
+
args: {
|
|
1406
|
+
children: 'Filled Button',
|
|
1407
|
+
variant: 'filled',
|
|
1408
|
+
},
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
export const Outlined: Story = {
|
|
1412
|
+
args: {
|
|
1413
|
+
children: 'Outlined Button',
|
|
1414
|
+
variant: 'outlined',
|
|
1415
|
+
},
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
export const Text: Story = {
|
|
1419
|
+
args: {
|
|
1420
|
+
children: 'Text Button',
|
|
1421
|
+
variant: 'text',
|
|
1422
|
+
},
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
export const Elevated: Story = {
|
|
1426
|
+
args: {
|
|
1427
|
+
children: 'Elevated Button',
|
|
1428
|
+
variant: 'elevated',
|
|
1429
|
+
},
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
export const Tonal: Story = {
|
|
1433
|
+
args: {
|
|
1434
|
+
children: 'Tonal Button',
|
|
1435
|
+
variant: 'tonal',
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
export const Sizes: Story = {
|
|
1440
|
+
render: () => (
|
|
1441
|
+
<div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
|
|
1442
|
+
<Button size="sm">Small</Button>
|
|
1443
|
+
<Button size="md">Medium</Button>
|
|
1444
|
+
<Button size="lg">Large</Button>
|
|
1445
|
+
</div>
|
|
1446
|
+
),
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
export const Disabled: Story = {
|
|
1450
|
+
args: {
|
|
1451
|
+
children: 'Disabled Button',
|
|
1452
|
+
disabled: true,
|
|
1453
|
+
},
|
|
1454
|
+
};
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### src/components/Button/index.ts
|
|
1458
|
+
|
|
1459
|
+
```typescript
|
|
1460
|
+
export { Button, type ButtonProps } from './Button';
|
|
1461
|
+
```
|
|
1462
|
+
|
|
1463
|
+
### src/components/index.ts
|
|
1464
|
+
|
|
1465
|
+
```typescript
|
|
1466
|
+
export * from './Button';
|
|
1467
|
+
// export * from './Card';
|
|
1468
|
+
// export * from './Dialog';
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
### src/index.ts
|
|
1472
|
+
|
|
1473
|
+
```typescript
|
|
1474
|
+
// Components
|
|
1475
|
+
export * from './components';
|
|
1476
|
+
|
|
1477
|
+
// Recipes (for direct usage)
|
|
1478
|
+
export * from './recipes';
|
|
1479
|
+
|
|
1480
|
+
// Language system
|
|
1481
|
+
export * from './languages';
|
|
1482
|
+
|
|
1483
|
+
// Contracts
|
|
1484
|
+
export type * from './contracts/design-language.contract';
|
|
1485
|
+
|
|
1486
|
+
// Utilities
|
|
1487
|
+
export { cn } from './utils/cn';
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
### scripts/generate-palette.ts
|
|
1491
|
+
|
|
1492
|
+
```typescript
|
|
1493
|
+
import {
|
|
1494
|
+
argbFromHex,
|
|
1495
|
+
hexFromArgb,
|
|
1496
|
+
TonalPalette,
|
|
1497
|
+
Hct,
|
|
1498
|
+
themeFromSourceColor
|
|
1499
|
+
} from '@material/material-color-utilities';
|
|
1500
|
+
import fs from 'fs';
|
|
1501
|
+
import path from 'path';
|
|
1502
|
+
|
|
1503
|
+
// TastyMakers source color from Material Theme Builder
|
|
1504
|
+
const SOURCE_COLOR = '#63A002';
|
|
1505
|
+
|
|
1506
|
+
function generatePalette() {
|
|
1507
|
+
const theme = themeFromSourceColor(argbFromHex(SOURCE_COLOR));
|
|
1508
|
+
|
|
1509
|
+
const tones = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99, 100];
|
|
1510
|
+
|
|
1511
|
+
const palettes = {
|
|
1512
|
+
primary: extractTones(theme.palettes.primary, tones),
|
|
1513
|
+
secondary: extractTones(theme.palettes.secondary, tones),
|
|
1514
|
+
tertiary: extractTones(theme.palettes.tertiary, tones),
|
|
1515
|
+
neutral: extractTones(theme.palettes.neutral, tones),
|
|
1516
|
+
neutralVariant: extractTones(theme.palettes.neutralVariant, tones),
|
|
1517
|
+
error: extractTones(theme.palettes.error, tones),
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const schemes = {
|
|
1521
|
+
light: formatScheme(theme.schemes.light),
|
|
1522
|
+
dark: formatScheme(theme.schemes.dark),
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const output = {
|
|
1526
|
+
sourceColor: SOURCE_COLOR,
|
|
1527
|
+
palettes,
|
|
1528
|
+
schemes,
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
const outputPath = path.join(process.cwd(), 'tokens/generated-palette.json');
|
|
1532
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
|
|
1533
|
+
|
|
1534
|
+
console.log('✅ Palette generated!');
|
|
1535
|
+
console.log(` Source: ${SOURCE_COLOR}`);
|
|
1536
|
+
console.log(` Output: ${outputPath}`);
|
|
1537
|
+
console.log('');
|
|
1538
|
+
console.log('🎨 Primary tones:');
|
|
1539
|
+
Object.entries(palettes.primary).forEach(([tone, hex]) => {
|
|
1540
|
+
console.log(` ${tone}: ${hex}`);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function extractTones(palette: TonalPalette, tones: number[]) {
|
|
1545
|
+
return Object.fromEntries(
|
|
1546
|
+
tones.map(tone => [tone.toString(), hexFromArgb(palette.tone(tone))])
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function formatScheme(scheme: any) {
|
|
1551
|
+
const result: Record<string, string> = {};
|
|
1552
|
+
for (const [key, value] of Object.entries(scheme)) {
|
|
1553
|
+
if (typeof value === 'number') {
|
|
1554
|
+
result[key] = hexFromArgb(value);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return result;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
generatePalette();
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
### .storybook/main.ts
|
|
1564
|
+
|
|
1565
|
+
```typescript
|
|
1566
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
1567
|
+
|
|
1568
|
+
const config: StorybookConfig = {
|
|
1569
|
+
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
1570
|
+
addons: [
|
|
1571
|
+
'@storybook/addon-essentials',
|
|
1572
|
+
'@storybook/addon-a11y',
|
|
1573
|
+
],
|
|
1574
|
+
framework: {
|
|
1575
|
+
name: '@storybook/react-vite',
|
|
1576
|
+
options: {},
|
|
1577
|
+
},
|
|
1578
|
+
docs: {
|
|
1579
|
+
autodocs: 'tag',
|
|
1580
|
+
},
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1583
|
+
export default config;
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
### .storybook/preview.ts
|
|
1587
|
+
|
|
1588
|
+
```typescript
|
|
1589
|
+
import type { Preview } from '@storybook/react';
|
|
1590
|
+
import '../styled-system/styles.css';
|
|
1591
|
+
|
|
1592
|
+
const preview: Preview = {
|
|
1593
|
+
parameters: {
|
|
1594
|
+
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
1595
|
+
controls: {
|
|
1596
|
+
matchers: {
|
|
1597
|
+
color: /(background|color)$/i,
|
|
1598
|
+
date: /Date$/i,
|
|
1599
|
+
},
|
|
1600
|
+
},
|
|
1601
|
+
backgrounds: {
|
|
1602
|
+
default: 'surface',
|
|
1603
|
+
values: [
|
|
1604
|
+
{ name: 'surface', value: '#F9FAEF' },
|
|
1605
|
+
{ name: 'dark', value: '#12140E' },
|
|
1606
|
+
],
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
export default preview;
|
|
1612
|
+
```
|
|
1613
|
+
|
|
1614
|
+
### .npmrc
|
|
1615
|
+
|
|
1616
|
+
```
|
|
1617
|
+
auto-install-peers=true
|
|
1618
|
+
strict-peer-dependencies=false
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
### .gitignore
|
|
1622
|
+
|
|
1623
|
+
```
|
|
1624
|
+
# Dependencies
|
|
1625
|
+
node_modules/
|
|
1626
|
+
|
|
1627
|
+
# Build outputs
|
|
1628
|
+
dist/
|
|
1629
|
+
styled-system/
|
|
1630
|
+
storybook-static/
|
|
1631
|
+
|
|
1632
|
+
# IDE
|
|
1633
|
+
.vscode/
|
|
1634
|
+
.idea/
|
|
1635
|
+
*.swp
|
|
1636
|
+
*.swo
|
|
1637
|
+
|
|
1638
|
+
# OS
|
|
1639
|
+
.DS_Store
|
|
1640
|
+
Thumbs.db
|
|
1641
|
+
|
|
1642
|
+
# Logs
|
|
1643
|
+
*.log
|
|
1644
|
+
npm-debug.log*
|
|
1645
|
+
pnpm-debug.log*
|
|
1646
|
+
|
|
1647
|
+
# Environment
|
|
1648
|
+
.env
|
|
1649
|
+
.env.local
|
|
1650
|
+
.env.*.local
|
|
1651
|
+
|
|
1652
|
+
# Testing
|
|
1653
|
+
coverage/
|
|
1654
|
+
|
|
1655
|
+
# Misc
|
|
1656
|
+
*.tgz
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
### vitest.config.ts
|
|
1660
|
+
|
|
1661
|
+
```typescript
|
|
1662
|
+
import { defineConfig } from 'vitest/config';
|
|
1663
|
+
import react from '@vitejs/plugin-react';
|
|
1664
|
+
|
|
1665
|
+
export default defineConfig({
|
|
1666
|
+
plugins: [react()],
|
|
1667
|
+
test: {
|
|
1668
|
+
environment: 'jsdom',
|
|
1669
|
+
globals: true,
|
|
1670
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
1671
|
+
},
|
|
1672
|
+
resolve: {
|
|
1673
|
+
alias: {
|
|
1674
|
+
'@': './src',
|
|
1675
|
+
'styled-system': './styled-system',
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
### README.md
|
|
1682
|
+
|
|
1683
|
+
```markdown
|
|
1684
|
+
# @discourser/design-system
|
|
1685
|
+
|
|
1686
|
+
An aesthetic-agnostic design system built with Panda CSS and Ark UI.
|
|
1687
|
+
|
|
1688
|
+
## Features
|
|
1689
|
+
|
|
1690
|
+
- 🎨 **Swappable aesthetics** - Change the entire look by swapping one import
|
|
1691
|
+
- 🎯 **Zero runtime CSS** - SSR-safe with Panda CSS
|
|
1692
|
+
- ♿ **Accessible** - WAI-ARIA compliant via Ark UI
|
|
1693
|
+
- 📦 **Tree-shakeable** - Only import what you need
|
|
1694
|
+
- 🌙 **Dark mode** - Built-in light/dark theme support
|
|
1695
|
+
- 🎭 **Material Design 3** - M3 Expressive as default aesthetic
|
|
1696
|
+
|
|
1697
|
+
## Installation
|
|
1698
|
+
|
|
1699
|
+
\`\`\`bash
|
|
1700
|
+
pnpm add @discourser/design-system
|
|
1701
|
+
\`\`\`
|
|
1702
|
+
|
|
1703
|
+
## Quick Start
|
|
1704
|
+
|
|
1705
|
+
\`\`\`tsx
|
|
1706
|
+
import { Button } from '@discourser/design-system';
|
|
1707
|
+
import '@discourser/design-system/styled-system/styles.css';
|
|
1708
|
+
|
|
1709
|
+
function App() {
|
|
1710
|
+
return (
|
|
1711
|
+
<Button variant="filled" size="md">
|
|
1712
|
+
Click me
|
|
1713
|
+
</Button>
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
\`\`\`
|
|
1717
|
+
|
|
1718
|
+
## Development
|
|
1719
|
+
|
|
1720
|
+
\`\`\`bash
|
|
1721
|
+
# Install dependencies
|
|
1722
|
+
pnpm install
|
|
1723
|
+
|
|
1724
|
+
# Start Storybook
|
|
1725
|
+
pnpm dev
|
|
1726
|
+
|
|
1727
|
+
# Build
|
|
1728
|
+
pnpm build
|
|
1729
|
+
|
|
1730
|
+
# Generate color palette from source color
|
|
1731
|
+
pnpm tokens:generate
|
|
1732
|
+
|
|
1733
|
+
# Run tests
|
|
1734
|
+
pnpm test
|
|
1735
|
+
\`\`\`
|
|
1736
|
+
|
|
1737
|
+
## Architecture
|
|
1738
|
+
|
|
1739
|
+
This design system uses a three-layer architecture:
|
|
1740
|
+
|
|
1741
|
+
1. **Infrastructure** - Token pipeline, build system, component logic
|
|
1742
|
+
2. **Design Language** - Swappable aesthetic (colors, typography, spacing)
|
|
1743
|
+
3. **Component Recipes** - Visual styling derived from the language
|
|
1744
|
+
|
|
1745
|
+
To change the aesthetic, edit `src/languages/index.ts` and point to a different language file.
|
|
1746
|
+
|
|
1747
|
+
## License
|
|
1748
|
+
|
|
1749
|
+
MIT
|
|
1750
|
+
```
|
|
1751
|
+
|
|
1752
|
+
## Post-Setup Commands
|
|
1753
|
+
|
|
1754
|
+
After Copilot creates all files, run:
|
|
1755
|
+
|
|
1756
|
+
```bash
|
|
1757
|
+
# Install dependencies
|
|
1758
|
+
pnpm install
|
|
1759
|
+
|
|
1760
|
+
# Generate Panda CSS output
|
|
1761
|
+
pnpm build:panda
|
|
1762
|
+
|
|
1763
|
+
# Generate M3 palette from source color (optional - values already in language file)
|
|
1764
|
+
pnpm tokens:generate
|
|
1765
|
+
|
|
1766
|
+
# Start Storybook to verify everything works
|
|
1767
|
+
pnpm dev
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
## Success Criteria
|
|
1771
|
+
|
|
1772
|
+
1. ✅ `pnpm install` completes without errors
|
|
1773
|
+
2. ✅ `pnpm build:panda` generates `styled-system/` folder
|
|
1774
|
+
3. ✅ `pnpm dev` launches Storybook
|
|
1775
|
+
4. ✅ Button component renders with M3 styling
|
|
1776
|
+
5. ✅ All 5 button variants visible in Storybook
|
|
1777
|
+
6. ✅ `pnpm build` creates `dist/` with ESM/CJS outputs
|
|
1778
|
+
|
|
1779
|
+
## Notes
|
|
1780
|
+
|
|
1781
|
+
- The color values in `material3.language.ts` are placeholders based on chartreuse (#CDDC39)
|
|
1782
|
+
- Run `pnpm tokens:generate` to create accurate M3 tonal palettes
|
|
1783
|
+
- Typography uses Georgia (display) and Inter (body) - ensure fonts are loaded in consuming apps
|
|
1784
|
+
|
|
1785
|
+
---
|
|
1786
|
+
|
|
1787
|
+
## Implementation Phases
|
|
1788
|
+
|
|
1789
|
+
This section outlines the phased approach to implementing the design system. Each phase builds on the previous one and includes validation gates.
|
|
1790
|
+
|
|
1791
|
+
### Phase 0: Context Engineering Foundation
|
|
1792
|
+
|
|
1793
|
+
**Goal:** Set up Claude Code Skills and CLAUDE.md for efficient AI-assisted development.
|
|
1794
|
+
|
|
1795
|
+
**Deliverables:**
|
|
1796
|
+
```
|
|
1797
|
+
.claude/
|
|
1798
|
+
├── commands/
|
|
1799
|
+
│ ├── fix-foundation.md # Phase 1 automation
|
|
1800
|
+
│ ├── implement-architecture.md # Phase 2 automation
|
|
1801
|
+
│ └── new-component.md # Component scaffolding
|
|
1802
|
+
└── skills/
|
|
1803
|
+
├── design-language/
|
|
1804
|
+
│ └── SKILL.md # Contract architecture knowledge
|
|
1805
|
+
├── panda-recipes/
|
|
1806
|
+
│ └── SKILL.md # Recipe patterns with M3
|
|
1807
|
+
├── m3-tokens/
|
|
1808
|
+
│ └── SKILL.md # M3 color values reference
|
|
1809
|
+
└── component-patterns/
|
|
1810
|
+
└── SKILL.md # forwardRef, Ark UI patterns
|
|
1811
|
+
|
|
1812
|
+
CLAUDE.md # Slim (<300 lines), pointer-based project context
|
|
1813
|
+
```
|
|
1814
|
+
|
|
1815
|
+
**Validation:**
|
|
1816
|
+
- [ ] `/fix-foundation` command recognized in Claude Code
|
|
1817
|
+
- [ ] Skills auto-load when relevant tasks are requested
|
|
1818
|
+
|
|
1819
|
+
---
|
|
1820
|
+
|
|
1821
|
+
### Phase 1: Fix Foundation
|
|
1822
|
+
|
|
1823
|
+
**Goal:** Establish correct dependencies, configuration, and build pipeline.
|
|
1824
|
+
|
|
1825
|
+
**Tasks:**
|
|
1826
|
+
1. Update `package.json` with correct versions:
|
|
1827
|
+
- `@ark-ui/react` ^4.4.0
|
|
1828
|
+
- `@pandacss/dev` ^0.52.0
|
|
1829
|
+
- `@material/material-color-utilities` ^0.3.0
|
|
1830
|
+
- React 19, Storybook 8.5, Vitest 2.x
|
|
1831
|
+
2. Update `tsconfig.json` with path aliases
|
|
1832
|
+
3. Update `.gitignore` (add `styled-system/`, `storybook-static/`)
|
|
1833
|
+
4. Update `.npmrc` for peer dependency handling
|
|
1834
|
+
|
|
1835
|
+
**Validation:**
|
|
1836
|
+
```bash
|
|
1837
|
+
pnpm install # ✅ No errors
|
|
1838
|
+
pnpm build:panda # ✅ Generates styled-system/
|
|
1839
|
+
pnpm dev # ✅ Storybook starts
|
|
1840
|
+
```
|
|
1841
|
+
|
|
1842
|
+
---
|
|
1843
|
+
|
|
1844
|
+
### Phase 2: Architecture Implementation
|
|
1845
|
+
|
|
1846
|
+
**Goal:** Implement the three-layer Contract → Language → Transform architecture.
|
|
1847
|
+
|
|
1848
|
+
**Tasks:**
|
|
1849
|
+
1. Create `src/contracts/design-language.contract.ts`
|
|
1850
|
+
- Full `DesignLanguageContract` interface
|
|
1851
|
+
- All supporting types (ColorPalettes, SemanticColors, Typography, etc.)
|
|
1852
|
+
|
|
1853
|
+
2. Create `src/languages/material3.language.ts`
|
|
1854
|
+
- Implement `DesignLanguageContract`
|
|
1855
|
+
- Use values from `docs/material-theme.json`
|
|
1856
|
+
- Include `semantic` (light) and `semanticDark` (dark)
|
|
1857
|
+
|
|
1858
|
+
3. Create `src/languages/transform.ts`
|
|
1859
|
+
- `transformToPandaTheme(language)` function
|
|
1860
|
+
- Returns `{ tokens, semanticTokens, textStyles }`
|
|
1861
|
+
|
|
1862
|
+
4. Create `src/languages/index.ts`
|
|
1863
|
+
- Export `material3Language as activeLanguage`
|
|
1864
|
+
- Re-export transformer
|
|
1865
|
+
|
|
1866
|
+
5. Update `panda.config.ts` to use the transform
|
|
1867
|
+
|
|
1868
|
+
**Validation:**
|
|
1869
|
+
```bash
|
|
1870
|
+
pnpm build:panda # ✅ No errors
|
|
1871
|
+
# Check styled-system/tokens/ contains M3 semantic colors
|
|
1872
|
+
```
|
|
1873
|
+
|
|
1874
|
+
---
|
|
1875
|
+
|
|
1876
|
+
### Phase 3: M3 Token Integration
|
|
1877
|
+
|
|
1878
|
+
**Goal:** Integrate full M3 token set from Material Theme Builder export.
|
|
1879
|
+
|
|
1880
|
+
**Tasks:**
|
|
1881
|
+
1. Verify `docs/material-theme.json` contains complete export
|
|
1882
|
+
2. Ensure all tonal palettes (primary, secondary, tertiary, neutral, neutralVariant, error) are in language file
|
|
1883
|
+
3. Ensure all semantic tokens are mapped for both light and dark
|
|
1884
|
+
4. Add typography scale (display, headline, title, body, label)
|
|
1885
|
+
5. Add spacing, shape (radii), elevation (shadows), motion (durations, easings)
|
|
1886
|
+
|
|
1887
|
+
**Validation:**
|
|
1888
|
+
```bash
|
|
1889
|
+
pnpm build:panda
|
|
1890
|
+
# Verify in Storybook:
|
|
1891
|
+
# - Colors render correctly
|
|
1892
|
+
# - Typography scale works
|
|
1893
|
+
# - Dark mode toggles properly (data-theme="dark")
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
---
|
|
1897
|
+
|
|
1898
|
+
### Phase 4: Complete Recipe Coverage + Testing
|
|
1899
|
+
|
|
1900
|
+
**Goal:** Ensure all components use the recipe pattern with full M3 variant support and comprehensive test coverage.
|
|
1901
|
+
|
|
1902
|
+
**Tasks:**
|
|
1903
|
+
|
|
1904
|
+
#### 4.1 Input Recipe & Component
|
|
1905
|
+
```typescript
|
|
1906
|
+
// src/recipes/input.recipe.ts
|
|
1907
|
+
import { defineRecipe } from '@pandacss/dev';
|
|
1908
|
+
|
|
1909
|
+
export const inputRecipe = defineRecipe({
|
|
1910
|
+
className: 'input',
|
|
1911
|
+
description: 'Material Design 3 text field component',
|
|
1912
|
+
base: {
|
|
1913
|
+
display: 'flex',
|
|
1914
|
+
flexDirection: 'column',
|
|
1915
|
+
gap: 'xs',
|
|
1916
|
+
},
|
|
1917
|
+
variants: {
|
|
1918
|
+
variant: {
|
|
1919
|
+
filled: {
|
|
1920
|
+
// M3 filled text field styling
|
|
1921
|
+
},
|
|
1922
|
+
outlined: {
|
|
1923
|
+
// M3 outlined text field styling
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1926
|
+
size: {
|
|
1927
|
+
sm: { /* ... */ },
|
|
1928
|
+
md: { /* ... */ },
|
|
1929
|
+
},
|
|
1930
|
+
state: {
|
|
1931
|
+
error: { /* ... */ },
|
|
1932
|
+
disabled: { /* ... */ },
|
|
1933
|
+
},
|
|
1934
|
+
},
|
|
1935
|
+
defaultVariants: {
|
|
1936
|
+
variant: 'outlined',
|
|
1937
|
+
size: 'md',
|
|
1938
|
+
},
|
|
1939
|
+
});
|
|
1940
|
+
```
|
|
1941
|
+
|
|
1942
|
+
```typescript
|
|
1943
|
+
// src/components/Input/Input.tsx
|
|
1944
|
+
import { forwardRef } from 'react';
|
|
1945
|
+
import { Field } from '@ark-ui/react';
|
|
1946
|
+
import { input, type InputVariantProps } from 'styled-system/recipes';
|
|
1947
|
+
import { cn } from '../../utils/cn';
|
|
1948
|
+
|
|
1949
|
+
export interface InputProps
|
|
1950
|
+
extends React.InputHTMLAttributes<HTMLInputElement>,
|
|
1951
|
+
InputVariantProps {
|
|
1952
|
+
label?: string;
|
|
1953
|
+
helperText?: string;
|
|
1954
|
+
errorText?: string;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
1958
|
+
({ label, helperText, errorText, variant, size, state, className, ...props }, ref) => {
|
|
1959
|
+
const hasError = !!errorText || state === 'error';
|
|
1960
|
+
|
|
1961
|
+
return (
|
|
1962
|
+
<Field.Root invalid={hasError}>
|
|
1963
|
+
{label && <Field.Label>{label}</Field.Label>}
|
|
1964
|
+
<Field.Input
|
|
1965
|
+
ref={ref}
|
|
1966
|
+
className={cn(input({ variant, size, state: hasError ? 'error' : state }), className)}
|
|
1967
|
+
{...props}
|
|
1968
|
+
/>
|
|
1969
|
+
{helperText && !hasError && <Field.HelperText>{helperText}</Field.HelperText>}
|
|
1970
|
+
{errorText && <Field.ErrorText>{errorText}</Field.ErrorText>}
|
|
1971
|
+
</Field.Root>
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
);
|
|
1975
|
+
|
|
1976
|
+
Input.displayName = 'Input';
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
#### 4.2 Dialog Recipe & Component (Compound Pattern)
|
|
1980
|
+
```typescript
|
|
1981
|
+
// src/recipes/dialog.recipe.ts
|
|
1982
|
+
import { defineSlotRecipe } from '@pandacss/dev';
|
|
1983
|
+
|
|
1984
|
+
export const dialogRecipe = defineSlotRecipe({
|
|
1985
|
+
className: 'dialog',
|
|
1986
|
+
description: 'Material Design 3 dialog component',
|
|
1987
|
+
slots: ['backdrop', 'positioner', 'content', 'title', 'description', 'closeTrigger'],
|
|
1988
|
+
base: {
|
|
1989
|
+
backdrop: {
|
|
1990
|
+
position: 'fixed',
|
|
1991
|
+
inset: 0,
|
|
1992
|
+
bg: 'scrim',
|
|
1993
|
+
opacity: 0.32,
|
|
1994
|
+
zIndex: 'modal',
|
|
1995
|
+
},
|
|
1996
|
+
positioner: {
|
|
1997
|
+
position: 'fixed',
|
|
1998
|
+
inset: 0,
|
|
1999
|
+
display: 'flex',
|
|
2000
|
+
alignItems: 'center',
|
|
2001
|
+
justifyContent: 'center',
|
|
2002
|
+
zIndex: 'modal',
|
|
2003
|
+
},
|
|
2004
|
+
content: {
|
|
2005
|
+
bg: 'surfaceContainerHigh',
|
|
2006
|
+
borderRadius: 'extraLarge',
|
|
2007
|
+
p: 'lg',
|
|
2008
|
+
shadow: 'level3',
|
|
2009
|
+
maxWidth: '560px',
|
|
2010
|
+
minWidth: '280px',
|
|
2011
|
+
},
|
|
2012
|
+
title: {
|
|
2013
|
+
textStyle: 'headlineSmall',
|
|
2014
|
+
color: 'onSurface',
|
|
2015
|
+
mb: 'md',
|
|
2016
|
+
},
|
|
2017
|
+
description: {
|
|
2018
|
+
textStyle: 'bodyMedium',
|
|
2019
|
+
color: 'onSurfaceVariant',
|
|
2020
|
+
},
|
|
2021
|
+
closeTrigger: {
|
|
2022
|
+
position: 'absolute',
|
|
2023
|
+
top: 'md',
|
|
2024
|
+
right: 'md',
|
|
2025
|
+
},
|
|
2026
|
+
},
|
|
2027
|
+
variants: {
|
|
2028
|
+
size: {
|
|
2029
|
+
sm: {
|
|
2030
|
+
content: { maxWidth: '400px' },
|
|
2031
|
+
},
|
|
2032
|
+
md: {
|
|
2033
|
+
content: { maxWidth: '560px' },
|
|
2034
|
+
},
|
|
2035
|
+
lg: {
|
|
2036
|
+
content: { maxWidth: '720px' },
|
|
2037
|
+
},
|
|
2038
|
+
fullscreen: {
|
|
2039
|
+
content: {
|
|
2040
|
+
maxWidth: '100vw',
|
|
2041
|
+
maxHeight: '100vh',
|
|
2042
|
+
borderRadius: 'none',
|
|
2043
|
+
m: 0,
|
|
2044
|
+
},
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2048
|
+
defaultVariants: {
|
|
2049
|
+
size: 'md',
|
|
2050
|
+
},
|
|
2051
|
+
});
|
|
2052
|
+
```
|
|
2053
|
+
|
|
2054
|
+
```typescript
|
|
2055
|
+
// src/components/Dialog/Dialog.tsx
|
|
2056
|
+
import { forwardRef } from 'react';
|
|
2057
|
+
import { Dialog as ArkDialog } from '@ark-ui/react';
|
|
2058
|
+
import { dialog, type DialogVariantProps } from 'styled-system/recipes';
|
|
2059
|
+
import { cn } from '../../utils/cn';
|
|
2060
|
+
|
|
2061
|
+
const styles = dialog();
|
|
2062
|
+
|
|
2063
|
+
export interface DialogProps extends ArkDialog.RootProps, DialogVariantProps {}
|
|
2064
|
+
|
|
2065
|
+
const DialogRoot = (props: DialogProps) => <ArkDialog.Root {...props} />;
|
|
2066
|
+
|
|
2067
|
+
const DialogTrigger = forwardRef<HTMLButtonElement, ArkDialog.TriggerProps>(
|
|
2068
|
+
({ className, ...props }, ref) => (
|
|
2069
|
+
<ArkDialog.Trigger ref={ref} className={className} {...props} />
|
|
2070
|
+
)
|
|
2071
|
+
);
|
|
2072
|
+
DialogTrigger.displayName = 'DialogTrigger';
|
|
2073
|
+
|
|
2074
|
+
const DialogBackdrop = forwardRef<HTMLDivElement, ArkDialog.BackdropProps>(
|
|
2075
|
+
({ className, ...props }, ref) => (
|
|
2076
|
+
<ArkDialog.Backdrop ref={ref} className={cn(styles.backdrop, className)} {...props} />
|
|
2077
|
+
)
|
|
2078
|
+
);
|
|
2079
|
+
DialogBackdrop.displayName = 'DialogBackdrop';
|
|
2080
|
+
|
|
2081
|
+
const DialogPositioner = forwardRef<HTMLDivElement, ArkDialog.PositionerProps>(
|
|
2082
|
+
({ className, ...props }, ref) => (
|
|
2083
|
+
<ArkDialog.Positioner ref={ref} className={cn(styles.positioner, className)} {...props} />
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
DialogPositioner.displayName = 'DialogPositioner';
|
|
2087
|
+
|
|
2088
|
+
const DialogContent = forwardRef<HTMLDivElement, ArkDialog.ContentProps>(
|
|
2089
|
+
({ className, ...props }, ref) => (
|
|
2090
|
+
<ArkDialog.Content ref={ref} className={cn(styles.content, className)} {...props} />
|
|
2091
|
+
)
|
|
2092
|
+
);
|
|
2093
|
+
DialogContent.displayName = 'DialogContent';
|
|
2094
|
+
|
|
2095
|
+
const DialogTitle = forwardRef<HTMLHeadingElement, ArkDialog.TitleProps>(
|
|
2096
|
+
({ className, ...props }, ref) => (
|
|
2097
|
+
<ArkDialog.Title ref={ref} className={cn(styles.title, className)} {...props} />
|
|
2098
|
+
)
|
|
2099
|
+
);
|
|
2100
|
+
DialogTitle.displayName = 'DialogTitle';
|
|
2101
|
+
|
|
2102
|
+
const DialogDescription = forwardRef<HTMLParagraphElement, ArkDialog.DescriptionProps>(
|
|
2103
|
+
({ className, ...props }, ref) => (
|
|
2104
|
+
<ArkDialog.Description ref={ref} className={cn(styles.description, className)} {...props} />
|
|
2105
|
+
)
|
|
2106
|
+
);
|
|
2107
|
+
DialogDescription.displayName = 'DialogDescription';
|
|
2108
|
+
|
|
2109
|
+
const DialogCloseTrigger = forwardRef<HTMLButtonElement, ArkDialog.CloseTriggerProps>(
|
|
2110
|
+
({ className, ...props }, ref) => (
|
|
2111
|
+
<ArkDialog.CloseTrigger ref={ref} className={cn(styles.closeTrigger, className)} {...props} />
|
|
2112
|
+
)
|
|
2113
|
+
);
|
|
2114
|
+
DialogCloseTrigger.displayName = 'DialogCloseTrigger';
|
|
2115
|
+
|
|
2116
|
+
export const Dialog = {
|
|
2117
|
+
Root: DialogRoot,
|
|
2118
|
+
Trigger: DialogTrigger,
|
|
2119
|
+
Backdrop: DialogBackdrop,
|
|
2120
|
+
Positioner: DialogPositioner,
|
|
2121
|
+
Content: DialogContent,
|
|
2122
|
+
Title: DialogTitle,
|
|
2123
|
+
Description: DialogDescription,
|
|
2124
|
+
CloseTrigger: DialogCloseTrigger,
|
|
2125
|
+
};
|
|
2126
|
+
```
|
|
2127
|
+
|
|
2128
|
+
#### 4.3 Register All Recipes
|
|
2129
|
+
```typescript
|
|
2130
|
+
// panda.config.ts - update theme.extend.recipes
|
|
2131
|
+
recipes: {
|
|
2132
|
+
button: buttonRecipe,
|
|
2133
|
+
card: cardRecipe,
|
|
2134
|
+
iconButton: iconButtonRecipe,
|
|
2135
|
+
input: inputRecipe,
|
|
2136
|
+
},
|
|
2137
|
+
slotRecipes: {
|
|
2138
|
+
dialog: dialogRecipe,
|
|
2139
|
+
},
|
|
2140
|
+
```
|
|
2141
|
+
|
|
2142
|
+
#### 4.4 Component Testing Requirements
|
|
2143
|
+
|
|
2144
|
+
Every component MUST have accompanying tests. Create test files alongside components.
|
|
2145
|
+
|
|
2146
|
+
**Install test dependencies:**
|
|
2147
|
+
```bash
|
|
2148
|
+
pnpm add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-axe
|
|
2149
|
+
```
|
|
2150
|
+
|
|
2151
|
+
**Test Setup:**
|
|
2152
|
+
```typescript
|
|
2153
|
+
// src/test/setup.ts
|
|
2154
|
+
import '@testing-library/jest-dom';
|
|
2155
|
+
import { vi } from 'vitest';
|
|
2156
|
+
|
|
2157
|
+
// Mock matchMedia for components that use media queries
|
|
2158
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
2159
|
+
writable: true,
|
|
2160
|
+
value: vi.fn().mockImplementation(query => ({
|
|
2161
|
+
matches: false,
|
|
2162
|
+
media: query,
|
|
2163
|
+
onchange: null,
|
|
2164
|
+
addListener: vi.fn(),
|
|
2165
|
+
removeListener: vi.fn(),
|
|
2166
|
+
addEventListener: vi.fn(),
|
|
2167
|
+
removeEventListener: vi.fn(),
|
|
2168
|
+
dispatchEvent: vi.fn(),
|
|
2169
|
+
})),
|
|
2170
|
+
});
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
**Input Component Tests:**
|
|
2174
|
+
```typescript
|
|
2175
|
+
// src/components/Input/Input.test.tsx
|
|
2176
|
+
import { render, screen } from '@testing-library/react';
|
|
2177
|
+
import userEvent from '@testing-library/user-event';
|
|
2178
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2179
|
+
import { Input } from './Input';
|
|
2180
|
+
|
|
2181
|
+
describe('Input', () => {
|
|
2182
|
+
it('renders with label', () => {
|
|
2183
|
+
render(<Input label="Email" />);
|
|
2184
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
it('shows error state', () => {
|
|
2188
|
+
render(<Input label="Email" errorText="Invalid email" />);
|
|
2189
|
+
expect(screen.getByText('Invalid email')).toBeInTheDocument();
|
|
2190
|
+
});
|
|
2191
|
+
|
|
2192
|
+
it('calls onChange when typing', async () => {
|
|
2193
|
+
const user = userEvent.setup();
|
|
2194
|
+
const handleChange = vi.fn();
|
|
2195
|
+
render(<Input label="Email" onChange={handleChange} />);
|
|
2196
|
+
|
|
2197
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com');
|
|
2198
|
+
expect(handleChange).toHaveBeenCalled();
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
it('renders all variants', () => {
|
|
2202
|
+
const { rerender } = render(<Input variant="outlined" label="Test" />);
|
|
2203
|
+
expect(screen.getByLabelText('Test')).toBeInTheDocument();
|
|
2204
|
+
|
|
2205
|
+
rerender(<Input variant="filled" label="Test" />);
|
|
2206
|
+
expect(screen.getByLabelText('Test')).toBeInTheDocument();
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
it('is disabled when disabled prop is true', () => {
|
|
2210
|
+
render(<Input label="Email" disabled />);
|
|
2211
|
+
expect(screen.getByLabelText('Email')).toBeDisabled();
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
```
|
|
2215
|
+
|
|
2216
|
+
**Dialog Component Tests:**
|
|
2217
|
+
```typescript
|
|
2218
|
+
// src/components/Dialog/Dialog.test.tsx
|
|
2219
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2220
|
+
import userEvent from '@testing-library/user-event';
|
|
2221
|
+
import { describe, it, expect } from 'vitest';
|
|
2222
|
+
import { Dialog } from './Dialog';
|
|
2223
|
+
import { Button } from '../Button';
|
|
2224
|
+
|
|
2225
|
+
describe('Dialog', () => {
|
|
2226
|
+
it('opens when trigger is clicked', async () => {
|
|
2227
|
+
const user = userEvent.setup();
|
|
2228
|
+
render(
|
|
2229
|
+
<Dialog.Root>
|
|
2230
|
+
<Dialog.Trigger asChild>
|
|
2231
|
+
<Button>Open</Button>
|
|
2232
|
+
</Dialog.Trigger>
|
|
2233
|
+
<Dialog.Backdrop />
|
|
2234
|
+
<Dialog.Positioner>
|
|
2235
|
+
<Dialog.Content>
|
|
2236
|
+
<Dialog.Title>Test Dialog</Dialog.Title>
|
|
2237
|
+
<Dialog.Description>Test content</Dialog.Description>
|
|
2238
|
+
</Dialog.Content>
|
|
2239
|
+
</Dialog.Positioner>
|
|
2240
|
+
</Dialog.Root>
|
|
2241
|
+
);
|
|
2242
|
+
|
|
2243
|
+
await user.click(screen.getByText('Open'));
|
|
2244
|
+
await waitFor(() => {
|
|
2245
|
+
expect(screen.getByText('Test Dialog')).toBeInTheDocument();
|
|
2246
|
+
});
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
it('closes when close trigger is clicked', async () => {
|
|
2250
|
+
const user = userEvent.setup();
|
|
2251
|
+
render(
|
|
2252
|
+
<Dialog.Root defaultOpen>
|
|
2253
|
+
<Dialog.Backdrop />
|
|
2254
|
+
<Dialog.Positioner>
|
|
2255
|
+
<Dialog.Content>
|
|
2256
|
+
<Dialog.Title>Test Dialog</Dialog.Title>
|
|
2257
|
+
<Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
|
|
2258
|
+
</Dialog.Content>
|
|
2259
|
+
</Dialog.Positioner>
|
|
2260
|
+
</Dialog.Root>
|
|
2261
|
+
);
|
|
2262
|
+
|
|
2263
|
+
await user.click(screen.getByText('Close'));
|
|
2264
|
+
await waitFor(() => {
|
|
2265
|
+
expect(screen.queryByText('Test Dialog')).not.toBeInTheDocument();
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
it('traps focus within dialog', async () => {
|
|
2270
|
+
const user = userEvent.setup();
|
|
2271
|
+
render(
|
|
2272
|
+
<Dialog.Root defaultOpen>
|
|
2273
|
+
<Dialog.Backdrop />
|
|
2274
|
+
<Dialog.Positioner>
|
|
2275
|
+
<Dialog.Content>
|
|
2276
|
+
<Dialog.Title>Test Dialog</Dialog.Title>
|
|
2277
|
+
<Button>First</Button>
|
|
2278
|
+
<Button>Second</Button>
|
|
2279
|
+
<Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
|
|
2280
|
+
</Dialog.Content>
|
|
2281
|
+
</Dialog.Positioner>
|
|
2282
|
+
</Dialog.Root>
|
|
2283
|
+
);
|
|
2284
|
+
|
|
2285
|
+
await user.tab();
|
|
2286
|
+
expect(screen.getByText('First')).toHaveFocus();
|
|
2287
|
+
|
|
2288
|
+
await user.tab();
|
|
2289
|
+
expect(screen.getByText('Second')).toHaveFocus();
|
|
2290
|
+
});
|
|
2291
|
+
});
|
|
2292
|
+
```
|
|
2293
|
+
|
|
2294
|
+
**Validation:**
|
|
2295
|
+
```bash
|
|
2296
|
+
pnpm build:panda # ✅ All recipes generate
|
|
2297
|
+
pnpm dev # ✅ All components render in Storybook
|
|
2298
|
+
pnpm test # ✅ All component tests pass
|
|
2299
|
+
```
|
|
2300
|
+
|
|
2301
|
+
---
|
|
2302
|
+
|
|
2303
|
+
### Phase 5: Ark UI Integration + Accessibility Testing
|
|
2304
|
+
|
|
2305
|
+
**Goal:** Ensure all components properly leverage Ark UI for accessibility and state management, with comprehensive accessibility tests.
|
|
2306
|
+
|
|
2307
|
+
**Tasks:**
|
|
2308
|
+
|
|
2309
|
+
#### 5.1 Audit Current Components
|
|
2310
|
+
Review each component and ensure it uses Ark UI primitives where applicable:
|
|
2311
|
+
|
|
2312
|
+
| Component | Current | Target |
|
|
2313
|
+
|-----------|---------|--------|
|
|
2314
|
+
| Button | `<button>` | `<Ark.Button>` or keep native (simple) |
|
|
2315
|
+
| Card | `<div>` | Keep native (no interaction state) |
|
|
2316
|
+
| IconButton | `<button>` | `<Ark.Button>` |
|
|
2317
|
+
| Input | Native | `<Field.Root>`, `<Field.Input>`, etc. |
|
|
2318
|
+
| Dialog | N/A | Full Ark Dialog compound |
|
|
2319
|
+
|
|
2320
|
+
#### 5.2 Implement Missing Ark UI Patterns
|
|
2321
|
+
|
|
2322
|
+
**Button with Ark UI:**
|
|
2323
|
+
```typescript
|
|
2324
|
+
// src/components/Button/Button.tsx
|
|
2325
|
+
import { Button as ArkButton } from '@ark-ui/react';
|
|
2326
|
+
|
|
2327
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
2328
|
+
({ children, variant, size, leftIcon, rightIcon, className, ...props }, ref) => {
|
|
2329
|
+
return (
|
|
2330
|
+
<ArkButton
|
|
2331
|
+
ref={ref}
|
|
2332
|
+
className={cn(button({ variant, size }), className)}
|
|
2333
|
+
{...props}
|
|
2334
|
+
>
|
|
2335
|
+
{leftIcon}
|
|
2336
|
+
{children}
|
|
2337
|
+
{rightIcon}
|
|
2338
|
+
</ArkButton>
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
);
|
|
2342
|
+
```
|
|
2343
|
+
|
|
2344
|
+
#### 5.3 Add Additional Ark UI Components
|
|
2345
|
+
|
|
2346
|
+
Consider adding these M3 components using Ark UI:
|
|
2347
|
+
- **Menu** - `@ark-ui/react` Menu
|
|
2348
|
+
- **Tabs** - `@ark-ui/react` Tabs
|
|
2349
|
+
- **Tooltip** - `@ark-ui/react` Tooltip
|
|
2350
|
+
- **Switch** - `@ark-ui/react` Switch
|
|
2351
|
+
- **Checkbox** - `@ark-ui/react` Checkbox
|
|
2352
|
+
- **Select** - `@ark-ui/react` Select
|
|
2353
|
+
|
|
2354
|
+
#### 5.4 Accessibility Testing
|
|
2355
|
+
|
|
2356
|
+
Every component MUST include accessibility tests using jest-axe:
|
|
2357
|
+
|
|
2358
|
+
```typescript
|
|
2359
|
+
// src/components/Button/Button.a11y.test.tsx
|
|
2360
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
2361
|
+
import { render } from '@testing-library/react';
|
|
2362
|
+
import { Button } from './Button';
|
|
2363
|
+
|
|
2364
|
+
expect.extend(toHaveNoViolations);
|
|
2365
|
+
|
|
2366
|
+
describe('Button accessibility', () => {
|
|
2367
|
+
it('has no accessibility violations', async () => {
|
|
2368
|
+
const { container } = render(<Button>Click me</Button>);
|
|
2369
|
+
const results = await axe(container);
|
|
2370
|
+
expect(results).toHaveNoViolations();
|
|
2371
|
+
});
|
|
2372
|
+
|
|
2373
|
+
it('has no violations when disabled', async () => {
|
|
2374
|
+
const { container } = render(<Button disabled>Disabled</Button>);
|
|
2375
|
+
const results = await axe(container);
|
|
2376
|
+
expect(results).toHaveNoViolations();
|
|
2377
|
+
});
|
|
2378
|
+
|
|
2379
|
+
it('has no violations for all variants', async () => {
|
|
2380
|
+
const variants = ['filled', 'outlined', 'text', 'elevated', 'tonal'] as const;
|
|
2381
|
+
|
|
2382
|
+
for (const variant of variants) {
|
|
2383
|
+
const { container } = render(<Button variant={variant}>Test</Button>);
|
|
2384
|
+
const results = await axe(container);
|
|
2385
|
+
expect(results).toHaveNoViolations();
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
});
|
|
2389
|
+
```
|
|
2390
|
+
|
|
2391
|
+
```typescript
|
|
2392
|
+
// src/components/Dialog/Dialog.a11y.test.tsx
|
|
2393
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
2394
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2395
|
+
import userEvent from '@testing-library/user-event';
|
|
2396
|
+
import { Dialog } from './Dialog';
|
|
2397
|
+
import { Button } from '../Button';
|
|
2398
|
+
|
|
2399
|
+
expect.extend(toHaveNoViolations);
|
|
2400
|
+
|
|
2401
|
+
describe('Dialog accessibility', () => {
|
|
2402
|
+
it('has no accessibility violations when open', async () => {
|
|
2403
|
+
const { container } = render(
|
|
2404
|
+
<Dialog.Root defaultOpen>
|
|
2405
|
+
<Dialog.Backdrop />
|
|
2406
|
+
<Dialog.Positioner>
|
|
2407
|
+
<Dialog.Content>
|
|
2408
|
+
<Dialog.Title>Accessible Dialog</Dialog.Title>
|
|
2409
|
+
<Dialog.Description>This dialog should be accessible.</Dialog.Description>
|
|
2410
|
+
<Dialog.CloseTrigger>Close</Dialog.CloseTrigger>
|
|
2411
|
+
</Dialog.Content>
|
|
2412
|
+
</Dialog.Positioner>
|
|
2413
|
+
</Dialog.Root>
|
|
2414
|
+
);
|
|
2415
|
+
|
|
2416
|
+
const results = await axe(container);
|
|
2417
|
+
expect(results).toHaveNoViolations();
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
it('has proper aria attributes', async () => {
|
|
2421
|
+
render(
|
|
2422
|
+
<Dialog.Root defaultOpen>
|
|
2423
|
+
<Dialog.Backdrop />
|
|
2424
|
+
<Dialog.Positioner>
|
|
2425
|
+
<Dialog.Content>
|
|
2426
|
+
<Dialog.Title>Test Title</Dialog.Title>
|
|
2427
|
+
<Dialog.Description>Test Description</Dialog.Description>
|
|
2428
|
+
</Dialog.Content>
|
|
2429
|
+
</Dialog.Positioner>
|
|
2430
|
+
</Dialog.Root>
|
|
2431
|
+
);
|
|
2432
|
+
|
|
2433
|
+
const dialog = screen.getByRole('dialog');
|
|
2434
|
+
expect(dialog).toHaveAttribute('aria-labelledby');
|
|
2435
|
+
expect(dialog).toHaveAttribute('aria-describedby');
|
|
2436
|
+
});
|
|
2437
|
+
});
|
|
2438
|
+
```
|
|
2439
|
+
|
|
2440
|
+
**Validation:**
|
|
2441
|
+
```bash
|
|
2442
|
+
pnpm dev # ✅ Components work correctly
|
|
2443
|
+
# Test keyboard navigation in Storybook
|
|
2444
|
+
# Test screen reader announcements
|
|
2445
|
+
pnpm test # ✅ All tests pass (unit + a11y)
|
|
2446
|
+
```
|
|
2447
|
+
|
|
2448
|
+
---
|
|
2449
|
+
|
|
2450
|
+
### Phase 6: Build & Package (Figma Make Compatible)
|
|
2451
|
+
|
|
2452
|
+
**Goal:** Create a publishable npm package compatible with Figma Make (Vite-based) with proper exports, types, and documentation.
|
|
2453
|
+
|
|
2454
|
+
**Requirements from Figma Make (https://developers.figma.com/docs/code/bring-your-design-system-package/):**
|
|
2455
|
+
- ✅ React 18+ (we use React 19)
|
|
2456
|
+
- ✅ Compatible with Vite
|
|
2457
|
+
- ✅ Published as npm package (public or private)
|
|
2458
|
+
|
|
2459
|
+
**Tasks:**
|
|
2460
|
+
|
|
2461
|
+
#### 6.1 Verify Vite Compatibility
|
|
2462
|
+
|
|
2463
|
+
Figma Make uses Vite as its build system. Test package compatibility:
|
|
2464
|
+
|
|
2465
|
+
```bash
|
|
2466
|
+
# Create a test Vite project
|
|
2467
|
+
npm create vite@latest make-test-app -- --template react-ts
|
|
2468
|
+
cd make-test-app
|
|
2469
|
+
|
|
2470
|
+
# Install your local package
|
|
2471
|
+
pnpm add ../path/to/tastymakers-design-system-0.1.0.tgz
|
|
2472
|
+
|
|
2473
|
+
# Test the import
|
|
2474
|
+
cat > src/App.tsx << 'EOF'
|
|
2475
|
+
import { Button, Card } from '@discourser/design-system';
|
|
2476
|
+
import '@discourser/design-system/styles.css';
|
|
2477
|
+
|
|
2478
|
+
function App() {
|
|
2479
|
+
return (
|
|
2480
|
+
<Card>
|
|
2481
|
+
<Button variant="filled">Test Button</Button>
|
|
2482
|
+
</Card>
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
export default App;
|
|
2486
|
+
EOF
|
|
2487
|
+
|
|
2488
|
+
# Build - must succeed for Figma Make compatibility
|
|
2489
|
+
pnpm build
|
|
2490
|
+
```
|
|
2491
|
+
|
|
2492
|
+
#### 6.2 Verify tsup Configuration
|
|
2493
|
+
```typescript
|
|
2494
|
+
// tsup.config.ts
|
|
2495
|
+
import { defineConfig } from 'tsup';
|
|
2496
|
+
|
|
2497
|
+
export default defineConfig({
|
|
2498
|
+
entry: ['src/index.ts'],
|
|
2499
|
+
format: ['esm', 'cjs'],
|
|
2500
|
+
dts: true,
|
|
2501
|
+
splitting: true,
|
|
2502
|
+
sourcemap: true,
|
|
2503
|
+
clean: true,
|
|
2504
|
+
external: ['react', 'react-dom'],
|
|
2505
|
+
esbuildOptions(options) {
|
|
2506
|
+
options.banner = {
|
|
2507
|
+
js: '"use client"',
|
|
2508
|
+
};
|
|
2509
|
+
},
|
|
2510
|
+
});
|
|
2511
|
+
```
|
|
2512
|
+
|
|
2513
|
+
#### 6.3 Verify Package Exports
|
|
2514
|
+
```json
|
|
2515
|
+
// package.json exports field
|
|
2516
|
+
{
|
|
2517
|
+
"exports": {
|
|
2518
|
+
".": {
|
|
2519
|
+
"import": "./dist/index.js",
|
|
2520
|
+
"require": "./dist/index.cjs",
|
|
2521
|
+
"types": "./dist/index.d.ts"
|
|
2522
|
+
},
|
|
2523
|
+
"./styles.css": "./styled-system/styles.css",
|
|
2524
|
+
"./styled-system": {
|
|
2525
|
+
"import": "./styled-system/index.mjs",
|
|
2526
|
+
"require": "./styled-system/index.js"
|
|
2527
|
+
},
|
|
2528
|
+
"./styled-system/css": {
|
|
2529
|
+
"import": "./styled-system/css/index.mjs",
|
|
2530
|
+
"require": "./styled-system/css/index.js"
|
|
2531
|
+
},
|
|
2532
|
+
"./styled-system/tokens": {
|
|
2533
|
+
"import": "./styled-system/tokens/index.mjs",
|
|
2534
|
+
"require": "./styled-system/tokens/index.js"
|
|
2535
|
+
},
|
|
2536
|
+
"./styled-system/recipes": {
|
|
2537
|
+
"import": "./styled-system/recipes/index.mjs",
|
|
2538
|
+
"require": "./styled-system/recipes/index.js"
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
```
|
|
2543
|
+
|
|
2544
|
+
#### 6.4 Add CI/CD Workflows
|
|
2545
|
+
|
|
2546
|
+
```yaml
|
|
2547
|
+
# .github/workflows/ci.yml
|
|
2548
|
+
name: CI
|
|
2549
|
+
|
|
2550
|
+
on:
|
|
2551
|
+
push:
|
|
2552
|
+
branches: [main]
|
|
2553
|
+
pull_request:
|
|
2554
|
+
branches: [main]
|
|
2555
|
+
|
|
2556
|
+
jobs:
|
|
2557
|
+
build:
|
|
2558
|
+
runs-on: ubuntu-latest
|
|
2559
|
+
steps:
|
|
2560
|
+
- uses: actions/checkout@v4
|
|
2561
|
+
- uses: pnpm/action-setup@v2
|
|
2562
|
+
with:
|
|
2563
|
+
version: 8
|
|
2564
|
+
- uses: actions/setup-node@v4
|
|
2565
|
+
with:
|
|
2566
|
+
node-version: 20
|
|
2567
|
+
cache: 'pnpm'
|
|
2568
|
+
- run: pnpm install
|
|
2569
|
+
- run: pnpm typecheck
|
|
2570
|
+
- run: pnpm build:panda
|
|
2571
|
+
- run: pnpm build
|
|
2572
|
+
- run: pnpm test
|
|
2573
|
+
|
|
2574
|
+
vite-compatibility:
|
|
2575
|
+
runs-on: ubuntu-latest
|
|
2576
|
+
needs: build
|
|
2577
|
+
steps:
|
|
2578
|
+
- uses: actions/checkout@v4
|
|
2579
|
+
- uses: pnpm/action-setup@v2
|
|
2580
|
+
with:
|
|
2581
|
+
version: 8
|
|
2582
|
+
- uses: actions/setup-node@v4
|
|
2583
|
+
with:
|
|
2584
|
+
node-version: 20
|
|
2585
|
+
cache: 'pnpm'
|
|
2586
|
+
- run: pnpm install
|
|
2587
|
+
- run: pnpm build
|
|
2588
|
+
- run: pnpm pack
|
|
2589
|
+
- name: Test Vite Compatibility
|
|
2590
|
+
run: |
|
|
2591
|
+
npm create vite@latest test-app -- --template react-ts
|
|
2592
|
+
cd test-app
|
|
2593
|
+
npm install ../tastymakers-design-system-*.tgz
|
|
2594
|
+
npm run build
|
|
2595
|
+
```
|
|
2596
|
+
|
|
2597
|
+
```yaml
|
|
2598
|
+
# .github/workflows/publish.yml
|
|
2599
|
+
name: Publish
|
|
2600
|
+
|
|
2601
|
+
on:
|
|
2602
|
+
release:
|
|
2603
|
+
types: [created]
|
|
2604
|
+
|
|
2605
|
+
jobs:
|
|
2606
|
+
publish:
|
|
2607
|
+
runs-on: ubuntu-latest
|
|
2608
|
+
steps:
|
|
2609
|
+
- uses: actions/checkout@v4
|
|
2610
|
+
- uses: pnpm/action-setup@v2
|
|
2611
|
+
with:
|
|
2612
|
+
version: 8
|
|
2613
|
+
- uses: actions/setup-node@v4
|
|
2614
|
+
with:
|
|
2615
|
+
node-version: 20
|
|
2616
|
+
registry-url: 'https://registry.npmjs.org'
|
|
2617
|
+
- run: pnpm install
|
|
2618
|
+
- run: pnpm build
|
|
2619
|
+
- run: pnpm publish --access public
|
|
2620
|
+
env:
|
|
2621
|
+
NODE_AUTH_TOKEN: ${{ secrets.
|
|
2622
|
+
}}
|
|
2623
|
+
```
|
|
2624
|
+
|
|
2625
|
+
#### 6.5 Create npm Organization
|
|
2626
|
+
|
|
2627
|
+
Before publishing, create the `@discourser` npm organization:
|
|
2628
|
+
|
|
2629
|
+
1. Go to https://www.npmjs.com/org/create
|
|
2630
|
+
2. Create organization with name `discourser`
|
|
2631
|
+
3. Choose "Unlimited public packages" (free tier)
|
|
2632
|
+
4. Verify organization exists at https://www.npmjs.com/org/discourser
|
|
2633
|
+
|
|
2634
|
+
**Note:** npm allows multiple organizations per user account.
|
|
2635
|
+
|
|
2636
|
+
#### 6.6 Configure npm Authentication Locally
|
|
2637
|
+
|
|
2638
|
+
```bash
|
|
2639
|
+
# Login to npm (if not already)
|
|
2640
|
+
npm login
|
|
2641
|
+
|
|
2642
|
+
# Verify you can publish to the org
|
|
2643
|
+
npm access ls-packages @discourser
|
|
2644
|
+
```
|
|
2645
|
+
|
|
2646
|
+
#### 6.7 Install & Configure Changesets
|
|
2647
|
+
|
|
2648
|
+
Changesets provides automated versioning and changelog generation.
|
|
2649
|
+
|
|
2650
|
+
**Install dependencies:**
|
|
2651
|
+
```bash
|
|
2652
|
+
pnpm add -D @changesets/cli @changesets/changelog-github
|
|
2653
|
+
pnpm changeset init
|
|
2654
|
+
```
|
|
2655
|
+
|
|
2656
|
+
**Configure `.changeset/config.json`:**
|
|
2657
|
+
```json
|
|
2658
|
+
{
|
|
2659
|
+
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
|
2660
|
+
"changelog": [
|
|
2661
|
+
"@changesets/changelog-github",
|
|
2662
|
+
{ "repo": "Tasty-Maker-Studio/Discourser-Design-System" }
|
|
2663
|
+
],
|
|
2664
|
+
"commit": false,
|
|
2665
|
+
"fixed": [],
|
|
2666
|
+
"linked": [],
|
|
2667
|
+
"access": "public",
|
|
2668
|
+
"baseBranch": "main",
|
|
2669
|
+
"updateInternalDependencies": "patch",
|
|
2670
|
+
"ignore": []
|
|
2671
|
+
}
|
|
2672
|
+
```
|
|
2673
|
+
|
|
2674
|
+
**Create `.changeset/README.md`:**
|
|
2675
|
+
```markdown
|
|
2676
|
+
# Changesets
|
|
2677
|
+
|
|
2678
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`.
|
|
2679
|
+
|
|
2680
|
+
## How to add a changeset
|
|
2681
|
+
|
|
2682
|
+
1. Run `pnpm changeset`
|
|
2683
|
+
2. Select the type of change (patch/minor/major)
|
|
2684
|
+
3. Write a summary of the change
|
|
2685
|
+
4. Commit the generated changeset file
|
|
2686
|
+
|
|
2687
|
+
Changesets are automatically consumed when the "Version Packages" PR is merged.
|
|
2688
|
+
```
|
|
2689
|
+
|
|
2690
|
+
**Create `CHANGELOG.md` (root):**
|
|
2691
|
+
```markdown
|
|
2692
|
+
# @discourser/design-system
|
|
2693
|
+
|
|
2694
|
+
## Changelog
|
|
2695
|
+
|
|
2696
|
+
All notable changes to this project will be documented in this file.
|
|
2697
|
+
|
|
2698
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
2699
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
2700
|
+
```
|
|
2701
|
+
|
|
2702
|
+
**Update `package.json`:**
|
|
2703
|
+
```json
|
|
2704
|
+
{
|
|
2705
|
+
"name": "@discourser/design-system",
|
|
2706
|
+
"version": "0.0.0",
|
|
2707
|
+
"publishConfig": {
|
|
2708
|
+
"access": "public",
|
|
2709
|
+
"registry": "https://registry.npmjs.org"
|
|
2710
|
+
},
|
|
2711
|
+
"repository": {
|
|
2712
|
+
"type": "git",
|
|
2713
|
+
"url": "https://github.com/Tasty-Maker-Studio/Discourser-Design-System.git"
|
|
2714
|
+
},
|
|
2715
|
+
"scripts": {
|
|
2716
|
+
"changeset": "changeset",
|
|
2717
|
+
"version": "changeset version",
|
|
2718
|
+
"release": "pnpm build && changeset publish"
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
```
|
|
2722
|
+
|
|
2723
|
+
**Validation:**
|
|
2724
|
+
```bash
|
|
2725
|
+
pnpm changeset # ✅ Interactive prompt works
|
|
2726
|
+
pnpm changeset status # ✅ Shows no changesets (or lists pending)
|
|
2727
|
+
```
|
|
2728
|
+
|
|
2729
|
+
#### 6.8 Create CI Workflow
|
|
2730
|
+
|
|
2731
|
+
Create `.github/workflows/ci.yml`:
|
|
2732
|
+
|
|
2733
|
+
```yaml
|
|
2734
|
+
name: CI
|
|
2735
|
+
|
|
2736
|
+
on:
|
|
2737
|
+
push:
|
|
2738
|
+
branches: [main]
|
|
2739
|
+
pull_request:
|
|
2740
|
+
branches: [main]
|
|
2741
|
+
|
|
2742
|
+
concurrency:
|
|
2743
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
2744
|
+
cancel-in-progress: true
|
|
2745
|
+
|
|
2746
|
+
jobs:
|
|
2747
|
+
build:
|
|
2748
|
+
name: Build & Test
|
|
2749
|
+
runs-on: ubuntu-latest
|
|
2750
|
+
steps:
|
|
2751
|
+
- name: Checkout
|
|
2752
|
+
uses: actions/checkout@v4
|
|
2753
|
+
|
|
2754
|
+
- name: Setup pnpm
|
|
2755
|
+
uses: pnpm/action-setup@v2
|
|
2756
|
+
with:
|
|
2757
|
+
version: 9
|
|
2758
|
+
|
|
2759
|
+
- name: Setup Node.js
|
|
2760
|
+
uses: actions/setup-node@v4
|
|
2761
|
+
with:
|
|
2762
|
+
node-version: 20
|
|
2763
|
+
cache: 'pnpm'
|
|
2764
|
+
|
|
2765
|
+
- name: Install dependencies
|
|
2766
|
+
run: pnpm install --frozen-lockfile
|
|
2767
|
+
|
|
2768
|
+
- name: Typecheck
|
|
2769
|
+
run: pnpm typecheck
|
|
2770
|
+
|
|
2771
|
+
- name: Build Panda CSS
|
|
2772
|
+
run: pnpm build:panda
|
|
2773
|
+
|
|
2774
|
+
- name: Build library
|
|
2775
|
+
run: pnpm build:lib
|
|
2776
|
+
|
|
2777
|
+
- name: Run tests
|
|
2778
|
+
run: pnpm test
|
|
2779
|
+
|
|
2780
|
+
- name: Upload build artifacts
|
|
2781
|
+
uses: actions/upload-artifact@v4
|
|
2782
|
+
with:
|
|
2783
|
+
name: build-output
|
|
2784
|
+
path: |
|
|
2785
|
+
dist/
|
|
2786
|
+
styled-system/
|
|
2787
|
+
retention-days: 1
|
|
2788
|
+
|
|
2789
|
+
vite-compatibility:
|
|
2790
|
+
name: Vite Compatibility
|
|
2791
|
+
runs-on: ubuntu-latest
|
|
2792
|
+
needs: build
|
|
2793
|
+
steps:
|
|
2794
|
+
- name: Checkout
|
|
2795
|
+
uses: actions/checkout@v4
|
|
2796
|
+
|
|
2797
|
+
- name: Setup pnpm
|
|
2798
|
+
uses: pnpm/action-setup@v2
|
|
2799
|
+
with:
|
|
2800
|
+
version: 9
|
|
2801
|
+
|
|
2802
|
+
- name: Setup Node.js
|
|
2803
|
+
uses: actions/setup-node@v4
|
|
2804
|
+
with:
|
|
2805
|
+
node-version: 20
|
|
2806
|
+
cache: 'pnpm'
|
|
2807
|
+
|
|
2808
|
+
- name: Install dependencies
|
|
2809
|
+
run: pnpm install --frozen-lockfile
|
|
2810
|
+
|
|
2811
|
+
- name: Build package
|
|
2812
|
+
run: pnpm build
|
|
2813
|
+
|
|
2814
|
+
- name: Create tarball
|
|
2815
|
+
run: pnpm pack
|
|
2816
|
+
|
|
2817
|
+
- name: Test Vite compatibility
|
|
2818
|
+
run: |
|
|
2819
|
+
# Create test Vite app
|
|
2820
|
+
npm create vite@latest test-app -- --template react-ts
|
|
2821
|
+
cd test-app
|
|
2822
|
+
|
|
2823
|
+
# Install the local package
|
|
2824
|
+
npm install ../discourser-design-system-*.tgz
|
|
2825
|
+
|
|
2826
|
+
# Create test file
|
|
2827
|
+
cat > src/App.tsx << 'EOF'
|
|
2828
|
+
import { Button } from '@discourser/design-system';
|
|
2829
|
+
import '@discourser/design-system/styles.css';
|
|
2830
|
+
|
|
2831
|
+
function App() {
|
|
2832
|
+
return <Button variant="filled">Test</Button>;
|
|
2833
|
+
}
|
|
2834
|
+
export default App;
|
|
2835
|
+
EOF
|
|
2836
|
+
|
|
2837
|
+
# Build must succeed
|
|
2838
|
+
npm run build
|
|
2839
|
+
```
|
|
2840
|
+
|
|
2841
|
+
#### 6.9 Create Release Workflow
|
|
2842
|
+
|
|
2843
|
+
Create `.github/workflows/release.yml`:
|
|
2844
|
+
|
|
2845
|
+
```yaml
|
|
2846
|
+
name: Release
|
|
2847
|
+
|
|
2848
|
+
on:
|
|
2849
|
+
push:
|
|
2850
|
+
branches: [main]
|
|
2851
|
+
|
|
2852
|
+
concurrency:
|
|
2853
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
2854
|
+
cancel-in-progress: false
|
|
2855
|
+
|
|
2856
|
+
permissions:
|
|
2857
|
+
contents: write
|
|
2858
|
+
pull-requests: write
|
|
2859
|
+
|
|
2860
|
+
jobs:
|
|
2861
|
+
release:
|
|
2862
|
+
name: Release
|
|
2863
|
+
runs-on: ubuntu-latest
|
|
2864
|
+
steps:
|
|
2865
|
+
- name: Checkout
|
|
2866
|
+
uses: actions/checkout@v4
|
|
2867
|
+
with:
|
|
2868
|
+
fetch-depth: 0
|
|
2869
|
+
|
|
2870
|
+
- name: Setup pnpm
|
|
2871
|
+
uses: pnpm/action-setup@v2
|
|
2872
|
+
with:
|
|
2873
|
+
version: 9
|
|
2874
|
+
|
|
2875
|
+
- name: Setup Node.js
|
|
2876
|
+
uses: actions/setup-node@v4
|
|
2877
|
+
with:
|
|
2878
|
+
node-version: 20
|
|
2879
|
+
cache: 'pnpm'
|
|
2880
|
+
registry-url: 'https://registry.npmjs.org'
|
|
2881
|
+
|
|
2882
|
+
- name: Install dependencies
|
|
2883
|
+
run: pnpm install --frozen-lockfile
|
|
2884
|
+
|
|
2885
|
+
- name: Build
|
|
2886
|
+
run: pnpm build
|
|
2887
|
+
|
|
2888
|
+
- name: Create Release Pull Request or Publish
|
|
2889
|
+
id: changesets
|
|
2890
|
+
uses: changesets/action@v1
|
|
2891
|
+
with:
|
|
2892
|
+
version: pnpm changeset version
|
|
2893
|
+
publish: pnpm release
|
|
2894
|
+
commit: "chore: version packages"
|
|
2895
|
+
title: "chore: version packages"
|
|
2896
|
+
env:
|
|
2897
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
2898
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
2899
|
+
|
|
2900
|
+
- name: Create Git Tag
|
|
2901
|
+
if: steps.changesets.outputs.published == 'true'
|
|
2902
|
+
run: |
|
|
2903
|
+
git config user.name "github-actions[bot]"
|
|
2904
|
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
2905
|
+
|
|
2906
|
+
# Get the version from package.json
|
|
2907
|
+
VERSION=$(node -p "require('./package.json').version")
|
|
2908
|
+
|
|
2909
|
+
# Create and push tag
|
|
2910
|
+
git tag -a "v$VERSION" -m "Release v$VERSION"
|
|
2911
|
+
git push origin "v$VERSION"
|
|
2912
|
+
```
|
|
2913
|
+
|
|
2914
|
+
#### 6.10 Configure GitHub Secrets & Permissions (MANUAL)
|
|
2915
|
+
|
|
2916
|
+
This step requires manual configuration in the GitHub repository settings.
|
|
2917
|
+
|
|
2918
|
+
**Step 1: Generate npm Token**
|
|
2919
|
+
|
|
2920
|
+
1. Go to https://www.npmjs.com/settings/YOUR_USERNAME/tokens
|
|
2921
|
+
2. Click "Generate New Token" → "Classic Token"
|
|
2922
|
+
3. Select "Automation" type
|
|
2923
|
+
4. Copy the generated token (starts with `npm_`)
|
|
2924
|
+
|
|
2925
|
+
**Step 2: Add Secret to GitHub**
|
|
2926
|
+
|
|
2927
|
+
1. Go to repository: https://github.com/Tasty-Maker-Studio/Discourser-Design-System
|
|
2928
|
+
2. Navigate to Settings → Secrets and variables → Actions
|
|
2929
|
+
3. Click "New repository secret"
|
|
2930
|
+
4. Name: `NPM_TOKEN`
|
|
2931
|
+
5. Value: Paste the npm token
|
|
2932
|
+
6. Click "Add secret"
|
|
2933
|
+
|
|
2934
|
+
**Step 3: Configure Workflow Permissions**
|
|
2935
|
+
|
|
2936
|
+
1. Go to Settings → Actions → General
|
|
2937
|
+
2. Scroll to "Workflow permissions"
|
|
2938
|
+
3. Select "Read and write permissions"
|
|
2939
|
+
4. Check "Allow GitHub Actions to create and approve pull requests"
|
|
2940
|
+
5. Click "Save"
|
|
2941
|
+
|
|
2942
|
+
**Validation Checklist:**
|
|
2943
|
+
- [ ] NPM_TOKEN secret is configured
|
|
2944
|
+
- [ ] Workflow permissions allow write access
|
|
2945
|
+
- [ ] Workflow permissions allow PR creation
|
|
2946
|
+
|
|
2947
|
+
#### 6.11 Validate Full Release Pipeline
|
|
2948
|
+
|
|
2949
|
+
Test the complete release workflow end-to-end.
|
|
2950
|
+
|
|
2951
|
+
**Step 1: Create a Test Changeset**
|
|
2952
|
+
```bash
|
|
2953
|
+
pnpm changeset
|
|
2954
|
+
|
|
2955
|
+
# Select: patch
|
|
2956
|
+
# Summary: "Initial release - Button, Card, IconButton components"
|
|
2957
|
+
# This creates a file in .changeset/ like `funny-dogs-dance.md`
|
|
2958
|
+
```
|
|
2959
|
+
|
|
2960
|
+
**Step 2: Commit and Push**
|
|
2961
|
+
```bash
|
|
2962
|
+
git add .
|
|
2963
|
+
git commit -m "chore: add changeset for initial release"
|
|
2964
|
+
git push origin main
|
|
2965
|
+
```
|
|
2966
|
+
|
|
2967
|
+
**Step 3: Verify CI Workflow**
|
|
2968
|
+
1. Go to https://github.com/Tasty-Maker-Studio/Discourser-Design-System/actions
|
|
2969
|
+
2. Verify "CI" workflow passes (build, test, vite-compatibility)
|
|
2970
|
+
|
|
2971
|
+
**Step 4: Verify Release Workflow**
|
|
2972
|
+
1. After CI passes, "Release" workflow should run
|
|
2973
|
+
2. It should create a PR titled "chore: version packages"
|
|
2974
|
+
|
|
2975
|
+
**Step 5: Review Version Packages PR**
|
|
2976
|
+
The PR should contain:
|
|
2977
|
+
- `package.json`: version bumped from `0.0.0` to `0.0.1`
|
|
2978
|
+
- `CHANGELOG.md`: Updated with changeset content
|
|
2979
|
+
- Changeset file deleted from `.changeset/`
|
|
2980
|
+
|
|
2981
|
+
**Step 6: Merge and Publish**
|
|
2982
|
+
1. Review the PR
|
|
2983
|
+
2. Merge to main
|
|
2984
|
+
3. Release workflow runs again and publishes to npm
|
|
2985
|
+
|
|
2986
|
+
**Step 7: Verify npm Publication**
|
|
2987
|
+
```bash
|
|
2988
|
+
# Check package exists
|
|
2989
|
+
npm view @discourser/design-system
|
|
2990
|
+
|
|
2991
|
+
# Or visit
|
|
2992
|
+
# https://www.npmjs.com/package/@discourser/design-system
|
|
2993
|
+
```
|
|
2994
|
+
|
|
2995
|
+
**Complete Release Flow Diagram:**
|
|
2996
|
+
```
|
|
2997
|
+
Developer: pnpm changeset → Creates .changeset/*.md
|
|
2998
|
+
↓
|
|
2999
|
+
Git push to main
|
|
3000
|
+
↓
|
|
3001
|
+
CI Workflow: Build, Test, Vite Compatibility
|
|
3002
|
+
↓
|
|
3003
|
+
Release Workflow: Detects changesets → Creates "Version Packages" PR
|
|
3004
|
+
↓
|
|
3005
|
+
Human: Reviews and merges PR
|
|
3006
|
+
↓
|
|
3007
|
+
Release Workflow: Runs again → Publishes to npm + Creates git tag
|
|
3008
|
+
↓
|
|
3009
|
+
Package available at npmjs.com
|
|
3010
|
+
```
|
|
3011
|
+
|
|
3012
|
+
**Validation:**
|
|
3013
|
+
```bash
|
|
3014
|
+
# After full pipeline test:
|
|
3015
|
+
npm view @discourser/design-system # ✅ Package exists
|
|
3016
|
+
npm view @discourser/design-system versions # ✅ Shows 0.0.1
|
|
3017
|
+
```
|
|
3018
|
+
|
|
3019
|
+
#### 6.12 Test Local Package (Manual Verification)
|
|
3020
|
+
|
|
3021
|
+
For local testing before committing:
|
|
3022
|
+
|
|
3023
|
+
```bash
|
|
3024
|
+
# Build the package
|
|
3025
|
+
pnpm build
|
|
3026
|
+
|
|
3027
|
+
# Create a tarball
|
|
3028
|
+
pnpm pack
|
|
3029
|
+
|
|
3030
|
+
# In a separate test Vite project
|
|
3031
|
+
npm create vite@latest make-test-app -- --template react-ts
|
|
3032
|
+
cd make-test-app
|
|
3033
|
+
pnpm add ../path/to/discourser-design-system-0.0.1.tgz
|
|
3034
|
+
pnpm build # Must succeed for Figma Make compatibility
|
|
3035
|
+
```
|
|
3036
|
+
|
|
3037
|
+
**Phase 6 Validation Summary:**
|
|
3038
|
+
```bash
|
|
3039
|
+
pnpm build # ✅ Creates dist/ with ESM, CJS, and .d.ts
|
|
3040
|
+
pnpm pack # ✅ Creates tarball
|
|
3041
|
+
pnpm test # ✅ All tests pass
|
|
3042
|
+
pnpm changeset # ✅ Interactive prompt works
|
|
3043
|
+
# CI workflow passes # ✅ Build + Test + Vite compatibility
|
|
3044
|
+
# Release workflow # ✅ Creates Version Packages PR
|
|
3045
|
+
# npm publish # ✅ Package available on npmjs.com
|
|
3046
|
+
```
|
|
3047
|
+
|
|
3048
|
+
---
|
|
3049
|
+
|
|
3050
|
+
### Phase 7: Figma Make Guidelines
|
|
3051
|
+
|
|
3052
|
+
**Goal:** Create guidelines that teach Figma Make how to use the design system package, following the [Figma documentation](https://developers.figma.com/docs/code/write-design-system-guidelines/).
|
|
3053
|
+
|
|
3054
|
+
**Background:** Figma Make's AI can inspect your package but works best with explicit guidelines. This is similar to documentation you'd give a new engineer.
|
|
3055
|
+
|
|
3056
|
+
**Tasks:**
|
|
3057
|
+
|
|
3058
|
+
#### 7.1 Create Guidelines Structure
|
|
3059
|
+
|
|
3060
|
+
```
|
|
3061
|
+
guidelines/
|
|
3062
|
+
├── Guidelines.md # Top-level entry point (ALWAYS read first)
|
|
3063
|
+
├── overview-components.md # Component catalog and usage patterns
|
|
3064
|
+
├── overview-icons.md # Icon usage (if applicable)
|
|
3065
|
+
├── design-tokens/
|
|
3066
|
+
│ ├── colors.md # Semantic color tokens
|
|
3067
|
+
│ ├── typography.md # Typography scale
|
|
3068
|
+
│ ├── spacing.md # Spacing tokens
|
|
3069
|
+
│ └── elevation.md # Shadow/elevation tokens
|
|
3070
|
+
└── components/
|
|
3071
|
+
├── button.md
|
|
3072
|
+
├── card.md
|
|
3073
|
+
├── icon-button.md
|
|
3074
|
+
├── input.md
|
|
3075
|
+
├── dialog.md
|
|
3076
|
+
└── [additional components].md
|
|
3077
|
+
```
|
|
3078
|
+
|
|
3079
|
+
#### 7.2 Create Guidelines.md (Entry Point)
|
|
3080
|
+
|
|
3081
|
+
```markdown
|
|
3082
|
+
<!-- guidelines/Guidelines.md -->
|
|
3083
|
+
# TastyMakers Design System Guidelines
|
|
3084
|
+
|
|
3085
|
+
This project uses the `@discourser/design-system` package, a Material Design 3 implementation built with Panda CSS and Ark UI.
|
|
3086
|
+
|
|
3087
|
+
## IMPORTANT: Always Read These First
|
|
3088
|
+
|
|
3089
|
+
Before writing any code, follow these steps IN ORDER:
|
|
3090
|
+
|
|
3091
|
+
### Step 1: Read Overview Files (REQUIRED)
|
|
3092
|
+
Read ALL files with a name that starts with "overview-":
|
|
3093
|
+
- `overview-components.md` - Available components and usage patterns
|
|
3094
|
+
- `overview-icons.md` - Icon usage (if applicable)
|
|
3095
|
+
|
|
3096
|
+
### Step 2: Read Design Token Files (REQUIRED)
|
|
3097
|
+
Read ALL files in the `design-tokens/` folder:
|
|
3098
|
+
- `design-tokens/colors.md`
|
|
3099
|
+
- `design-tokens/typography.md`
|
|
3100
|
+
- `design-tokens/spacing.md`
|
|
3101
|
+
- `design-tokens/elevation.md`
|
|
3102
|
+
|
|
3103
|
+
### Step 3: Plan Components Needed (REQUIRED)
|
|
3104
|
+
Identify which components you need to use.
|
|
3105
|
+
|
|
3106
|
+
### Step 4: Read Component Guidelines BEFORE Using Components (REQUIRED)
|
|
3107
|
+
BEFORE using ANY component, you MUST read its guidelines file first:
|
|
3108
|
+
- Using Button? → Read `components/button.md` FIRST
|
|
3109
|
+
- Using Dialog? → Read `components/dialog.md` FIRST
|
|
3110
|
+
- Using Input? → Read `components/input.md` FIRST
|
|
3111
|
+
- Using Card? → Read `components/card.md` FIRST
|
|
3112
|
+
|
|
3113
|
+
DO NOT write code using a component until you have read its specific guidelines.
|
|
3114
|
+
|
|
3115
|
+
## Core Principles
|
|
3116
|
+
|
|
3117
|
+
- **Always prefer design system components** over native HTML elements
|
|
3118
|
+
- **Use semantic tokens** (e.g., `primary`, `onPrimary`) not raw colors
|
|
3119
|
+
- **Follow M3 patterns** for variants, sizing, and state layers
|
|
3120
|
+
- **Do not override styles** unless absolutely necessary
|
|
3121
|
+
|
|
3122
|
+
## Package Imports
|
|
3123
|
+
|
|
3124
|
+
\`\`\`typescript
|
|
3125
|
+
// Components
|
|
3126
|
+
import { Button, Card, Dialog, Input, IconButton } from '@discourser/design-system';
|
|
3127
|
+
|
|
3128
|
+
// Styles (REQUIRED - must be imported)
|
|
3129
|
+
import '@discourser/design-system/styles.css';
|
|
3130
|
+
\`\`\`
|
|
3131
|
+
|
|
3132
|
+
## Quick Reference
|
|
3133
|
+
|
|
3134
|
+
| Component | Variants | Sizes | Guidelines |
|
|
3135
|
+
|-----------|----------|-------|------------|
|
|
3136
|
+
| Button | filled, outlined, text, elevated, tonal | sm, md, lg | `components/button.md` |
|
|
3137
|
+
| Card | elevated, filled, outlined | - | `components/card.md` |
|
|
3138
|
+
| IconButton | standard, filled, tonal, outlined | sm, md, lg | `components/icon-button.md` |
|
|
3139
|
+
| Input | filled, outlined | sm, md | `components/input.md` |
|
|
3140
|
+
| Dialog | - | sm, md, lg, fullscreen | `components/dialog.md` |
|
|
3141
|
+
```
|
|
3142
|
+
|
|
3143
|
+
#### 7.3 Create overview-components.md
|
|
3144
|
+
|
|
3145
|
+
```markdown
|
|
3146
|
+
<!-- guidelines/overview-components.md -->
|
|
3147
|
+
# Components Overview
|
|
3148
|
+
|
|
3149
|
+
Always prefer components from `@discourser/design-system` if available. Do not use native HTML elements when a design system component exists.
|
|
3150
|
+
|
|
3151
|
+
## Available Components
|
|
3152
|
+
|
|
3153
|
+
| Component | Purpose | Guidelines |
|
|
3154
|
+
|-----------|---------|------------|
|
|
3155
|
+
| Button | Primary interactive element for actions | [button.md](components/button.md) |
|
|
3156
|
+
| Card | Container for related content | [card.md](components/card.md) |
|
|
3157
|
+
| IconButton | Icon-only interactive element | [icon-button.md](components/icon-button.md) |
|
|
3158
|
+
| Input | Text input with label and validation | [input.md](components/input.md) |
|
|
3159
|
+
| Dialog | Modal overlay for focused tasks | [dialog.md](components/dialog.md) |
|
|
3160
|
+
|
|
3161
|
+
## Common Props
|
|
3162
|
+
|
|
3163
|
+
Most components accept:
|
|
3164
|
+
- `variant` - Visual style variant (e.g., filled, outlined)
|
|
3165
|
+
- `size` - Size variant (sm, md, lg)
|
|
3166
|
+
- `disabled` - Disable interaction
|
|
3167
|
+
- `className` - Additional CSS classes (use sparingly)
|
|
3168
|
+
|
|
3169
|
+
## Styling Guidelines
|
|
3170
|
+
|
|
3171
|
+
**✅ DO:**
|
|
3172
|
+
\`\`\`typescript
|
|
3173
|
+
<Button variant="filled" size="md">Submit</Button>
|
|
3174
|
+
<Card variant="elevated">Content</Card>
|
|
3175
|
+
<Input variant="outlined" label="Email" />
|
|
3176
|
+
\`\`\`
|
|
3177
|
+
|
|
3178
|
+
**❌ DO NOT:**
|
|
3179
|
+
\`\`\`typescript
|
|
3180
|
+
// Don't override styles with inline styles
|
|
3181
|
+
<Button style={{ backgroundColor: 'blue' }}>Submit</Button>
|
|
3182
|
+
|
|
3183
|
+
// Don't use raw HTML when components exist
|
|
3184
|
+
<button className="...">Submit</button>
|
|
3185
|
+
<input type="text" />
|
|
3186
|
+
|
|
3187
|
+
// Don't use raw color values
|
|
3188
|
+
<div style={{ backgroundColor: '#4C662B' }}>...</div>
|
|
3189
|
+
\`\`\`
|
|
3190
|
+
|
|
3191
|
+
## Controlled vs Uncontrolled
|
|
3192
|
+
|
|
3193
|
+
- **Controlled**: Component receives `value` and `onChange` props
|
|
3194
|
+
- **Uncontrolled**: Component manages own state, use `defaultValue`
|
|
3195
|
+
- Prefer controlled for form components
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
#### 7.4 Create Design Token Guidelines
|
|
3199
|
+
|
|
3200
|
+
```markdown
|
|
3201
|
+
<!-- guidelines/design-tokens/colors.md -->
|
|
3202
|
+
# Color Tokens
|
|
3203
|
+
|
|
3204
|
+
The design system uses Material Design 3 semantic color tokens. Always use semantic tokens, never raw hex values.
|
|
3205
|
+
|
|
3206
|
+
## Semantic Colors (Light Theme)
|
|
3207
|
+
|
|
3208
|
+
### Primary
|
|
3209
|
+
| Token | Usage | Example Value |
|
|
3210
|
+
|-------|-------|---------------|
|
|
3211
|
+
| `primary` | Primary actions, buttons, links | #4C662B |
|
|
3212
|
+
| `onPrimary` | Text/icons on primary backgrounds | #FFFFFF |
|
|
3213
|
+
| `primaryContainer` | Primary container backgrounds | #CDEDA3 |
|
|
3214
|
+
| `onPrimaryContainer` | Text/icons on primary container | #354E16 |
|
|
3215
|
+
|
|
3216
|
+
### Secondary
|
|
3217
|
+
| Token | Usage |
|
|
3218
|
+
|-------|-------|
|
|
3219
|
+
| `secondary` | Secondary actions |
|
|
3220
|
+
| `onSecondary` | Text/icons on secondary |
|
|
3221
|
+
| `secondaryContainer` | Secondary container backgrounds |
|
|
3222
|
+
| `onSecondaryContainer` | Text/icons on secondary container |
|
|
3223
|
+
|
|
3224
|
+
### Surface
|
|
3225
|
+
| Token | Usage |
|
|
3226
|
+
|-------|-------|
|
|
3227
|
+
| `surface` | Default background |
|
|
3228
|
+
| `onSurface` | Default text color |
|
|
3229
|
+
| `surfaceVariant` | Alternate surface |
|
|
3230
|
+
| `onSurfaceVariant` | Text on variant surfaces |
|
|
3231
|
+
| `surfaceContainerLowest` | Lowest elevation |
|
|
3232
|
+
| `surfaceContainerLow` | Low elevation (cards) |
|
|
3233
|
+
| `surfaceContainer` | Default containers |
|
|
3234
|
+
| `surfaceContainerHigh` | Dialogs, elevated content |
|
|
3235
|
+
| `surfaceContainerHighest` | Highest elevation |
|
|
3236
|
+
|
|
3237
|
+
### Error
|
|
3238
|
+
| Token | Usage |
|
|
3239
|
+
|-------|-------|
|
|
3240
|
+
| `error` | Error states |
|
|
3241
|
+
| `onError` | Text/icons on error |
|
|
3242
|
+
| `errorContainer` | Error container backgrounds |
|
|
3243
|
+
| `onErrorContainer` | Text/icons on error container |
|
|
3244
|
+
|
|
3245
|
+
### Other
|
|
3246
|
+
| Token | Usage |
|
|
3247
|
+
|-------|-------|
|
|
3248
|
+
| `outline` | Borders, dividers |
|
|
3249
|
+
| `outlineVariant` | Subtle borders |
|
|
3250
|
+
| `scrim` | Modal overlays |
|
|
3251
|
+
| `shadow` | Shadow color |
|
|
3252
|
+
|
|
3253
|
+
## Usage in Components
|
|
3254
|
+
|
|
3255
|
+
Colors are applied automatically through component variants:
|
|
3256
|
+
|
|
3257
|
+
\`\`\`typescript
|
|
3258
|
+
// ✅ Correct - uses semantic tokens internally
|
|
3259
|
+
<Button variant="filled">Primary Action</Button>
|
|
3260
|
+
|
|
3261
|
+
// ❌ Wrong - don't apply colors directly
|
|
3262
|
+
<Button style={{ backgroundColor: '#4C662B' }}>Primary</Button>
|
|
3263
|
+
\`\`\`
|
|
3264
|
+
|
|
3265
|
+
## Dark Mode
|
|
3266
|
+
|
|
3267
|
+
The design system supports dark mode via `data-theme="dark"` on a parent element. Semantic tokens automatically adjust.
|
|
3268
|
+
```
|
|
3269
|
+
|
|
3270
|
+
```markdown
|
|
3271
|
+
<!-- guidelines/design-tokens/typography.md -->
|
|
3272
|
+
# Typography Tokens
|
|
3273
|
+
|
|
3274
|
+
The design system uses M3 typography scale with semantic naming.
|
|
3275
|
+
|
|
3276
|
+
## Font Families
|
|
3277
|
+
|
|
3278
|
+
| Token | Font | Usage |
|
|
3279
|
+
|-------|------|-------|
|
|
3280
|
+
| `display` | Georgia, serif | Display text, headings |
|
|
3281
|
+
| `body` | Inter, sans-serif | Body text, UI elements |
|
|
3282
|
+
| `mono` | JetBrains Mono | Code snippets |
|
|
3283
|
+
|
|
3284
|
+
## Type Scale
|
|
3285
|
+
|
|
3286
|
+
### Display (for hero sections, large text)
|
|
3287
|
+
- `displayLarge` - 57px
|
|
3288
|
+
- `displayMedium` - 45px
|
|
3289
|
+
- `displaySmall` - 36px
|
|
3290
|
+
|
|
3291
|
+
### Headline (for page/section headers)
|
|
3292
|
+
- `headlineLarge` - 32px
|
|
3293
|
+
- `headlineMedium` - 28px
|
|
3294
|
+
- `headlineSmall` - 24px
|
|
3295
|
+
|
|
3296
|
+
### Title (for card titles, dialogs)
|
|
3297
|
+
- `titleLarge` - 22px
|
|
3298
|
+
- `titleMedium` - 16px
|
|
3299
|
+
- `titleSmall` - 14px
|
|
3300
|
+
|
|
3301
|
+
### Body (for content)
|
|
3302
|
+
- `bodyLarge` - 16px (primary body text)
|
|
3303
|
+
- `bodyMedium` - 14px (default body text)
|
|
3304
|
+
- `bodySmall` - 12px (secondary text)
|
|
3305
|
+
|
|
3306
|
+
### Label (for buttons, form labels)
|
|
3307
|
+
- `labelLarge` - 14px (button text)
|
|
3308
|
+
- `labelMedium` - 12px (form labels)
|
|
3309
|
+
- `labelSmall` - 11px (badges)
|
|
3310
|
+
|
|
3311
|
+
## Usage
|
|
3312
|
+
|
|
3313
|
+
Typography is applied through `textStyle` in Panda CSS:
|
|
3314
|
+
|
|
3315
|
+
\`\`\`typescript
|
|
3316
|
+
import { css } from 'styled-system/css';
|
|
3317
|
+
|
|
3318
|
+
const heading = css({ textStyle: 'headlineMedium' });
|
|
3319
|
+
const body = css({ textStyle: 'bodyMedium' });
|
|
3320
|
+
\`\`\`
|
|
3321
|
+
```
|
|
3322
|
+
|
|
3323
|
+
#### 7.5 Create Component Guidelines
|
|
3324
|
+
|
|
3325
|
+
```markdown
|
|
3326
|
+
<!-- guidelines/components/button.md -->
|
|
3327
|
+
# Button
|
|
3328
|
+
|
|
3329
|
+
**Purpose:** Primary interactive element for user actions.
|
|
3330
|
+
|
|
3331
|
+
## Import
|
|
3332
|
+
|
|
3333
|
+
\`\`\`typescript
|
|
3334
|
+
import { Button } from '@discourser/design-system';
|
|
3335
|
+
\`\`\`
|
|
3336
|
+
|
|
3337
|
+
## Variants
|
|
3338
|
+
|
|
3339
|
+
| Variant | Usage | When to Use |
|
|
3340
|
+
|---------|-------|-------------|
|
|
3341
|
+
| `filled` | Primary actions | Submit, Confirm, Main CTA |
|
|
3342
|
+
| `outlined` | Secondary actions | Cancel, Back, Alternative options |
|
|
3343
|
+
| `text` | Tertiary actions | Links, Less prominent actions |
|
|
3344
|
+
| `elevated` | Floating actions | FAB-like buttons |
|
|
3345
|
+
| `tonal` | Medium emphasis | Secondary CTA, Soft highlight |
|
|
3346
|
+
|
|
3347
|
+
## Sizes
|
|
3348
|
+
|
|
3349
|
+
| Size | Height | Usage |
|
|
3350
|
+
|------|--------|-------|
|
|
3351
|
+
| `sm` | 32px | Compact UI, dense layouts |
|
|
3352
|
+
| `md` | 40px | Default, most use cases |
|
|
3353
|
+
| `lg` | 48px | Touch targets, mobile emphasis |
|
|
3354
|
+
|
|
3355
|
+
## Props
|
|
3356
|
+
|
|
3357
|
+
| Prop | Type | Default | Description |
|
|
3358
|
+
|------|------|---------|-------------|
|
|
3359
|
+
| `variant` | `'filled' \| 'outlined' \| 'text' \| 'elevated' \| 'tonal'` | `'filled'` | Visual style |
|
|
3360
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size |
|
|
3361
|
+
| `disabled` | `boolean` | `false` | Disable button |
|
|
3362
|
+
| `leftIcon` | `ReactNode` | - | Icon before text |
|
|
3363
|
+
| `rightIcon` | `ReactNode` | - | Icon after text |
|
|
3364
|
+
|
|
3365
|
+
## Examples
|
|
3366
|
+
|
|
3367
|
+
\`\`\`typescript
|
|
3368
|
+
// Primary action
|
|
3369
|
+
<Button variant="filled">Submit</Button>
|
|
3370
|
+
|
|
3371
|
+
// Secondary action
|
|
3372
|
+
<Button variant="outlined">Cancel</Button>
|
|
3373
|
+
|
|
3374
|
+
// With icon
|
|
3375
|
+
<Button variant="filled" leftIcon={<PlusIcon />}>Add Item</Button>
|
|
3376
|
+
|
|
3377
|
+
// Disabled
|
|
3378
|
+
<Button variant="filled" disabled>Unavailable</Button>
|
|
3379
|
+
|
|
3380
|
+
// Different sizes
|
|
3381
|
+
<Button size="sm">Small</Button>
|
|
3382
|
+
<Button size="md">Medium</Button>
|
|
3383
|
+
<Button size="lg">Large</Button>
|
|
3384
|
+
\`\`\`
|
|
3385
|
+
|
|
3386
|
+
## DO NOT
|
|
3387
|
+
|
|
3388
|
+
\`\`\`typescript
|
|
3389
|
+
// ❌ Don't use native button when Button component exists
|
|
3390
|
+
<button>Submit</button>
|
|
3391
|
+
|
|
3392
|
+
// ❌ Don't override button colors with inline styles
|
|
3393
|
+
<Button style={{ backgroundColor: 'red' }}>Delete</Button>
|
|
3394
|
+
|
|
3395
|
+
// ❌ Don't combine conflicting sizes
|
|
3396
|
+
<Button size="lg" style={{ height: '24px' }}>Small</Button>
|
|
3397
|
+
|
|
3398
|
+
// ❌ Don't add className to override core styles
|
|
3399
|
+
<Button className="bg-blue-500">Custom</Button>
|
|
3400
|
+
\`\`\`
|
|
3401
|
+
```
|
|
3402
|
+
|
|
3403
|
+
```markdown
|
|
3404
|
+
<!-- guidelines/components/dialog.md -->
|
|
3405
|
+
# Dialog
|
|
3406
|
+
|
|
3407
|
+
**Purpose:** Modal overlay for focused tasks requiring user attention.
|
|
3408
|
+
|
|
3409
|
+
## Import
|
|
3410
|
+
|
|
3411
|
+
\`\`\`typescript
|
|
3412
|
+
import { Dialog } from '@discourser/design-system';
|
|
3413
|
+
\`\`\`
|
|
3414
|
+
|
|
3415
|
+
## Structure
|
|
3416
|
+
|
|
3417
|
+
Dialog uses a compound component pattern. All parts are required for proper accessibility:
|
|
3418
|
+
|
|
3419
|
+
\`\`\`typescript
|
|
3420
|
+
<Dialog.Root>
|
|
3421
|
+
<Dialog.Trigger asChild>
|
|
3422
|
+
<Button>Open Dialog</Button>
|
|
3423
|
+
</Dialog.Trigger>
|
|
3424
|
+
<Dialog.Backdrop />
|
|
3425
|
+
<Dialog.Positioner>
|
|
3426
|
+
<Dialog.Content>
|
|
3427
|
+
<Dialog.Title>Dialog Title</Dialog.Title>
|
|
3428
|
+
<Dialog.Description>Dialog content goes here.</Dialog.Description>
|
|
3429
|
+
<Dialog.CloseTrigger asChild>
|
|
3430
|
+
<Button variant="text">Close</Button>
|
|
3431
|
+
</Dialog.CloseTrigger>
|
|
3432
|
+
</Dialog.Content>
|
|
3433
|
+
</Dialog.Positioner>
|
|
3434
|
+
</Dialog.Root>
|
|
3435
|
+
\`\`\`
|
|
3436
|
+
|
|
3437
|
+
## Parts
|
|
3438
|
+
|
|
3439
|
+
| Part | Required | Description |
|
|
3440
|
+
|------|----------|-------------|
|
|
3441
|
+
| `Dialog.Root` | Yes | Container, manages open/close state |
|
|
3442
|
+
| `Dialog.Trigger` | No | Opens dialog when clicked |
|
|
3443
|
+
| `Dialog.Backdrop` | Yes | Semi-transparent overlay behind dialog |
|
|
3444
|
+
| `Dialog.Positioner` | Yes | Centers the content |
|
|
3445
|
+
| `Dialog.Content` | Yes | The dialog panel |
|
|
3446
|
+
| `Dialog.Title` | Yes | Accessible title (required for a11y) |
|
|
3447
|
+
| `Dialog.Description` | No | Supporting text |
|
|
3448
|
+
| `Dialog.CloseTrigger` | No | Closes dialog when clicked |
|
|
3449
|
+
|
|
3450
|
+
## Sizes
|
|
3451
|
+
|
|
3452
|
+
| Size | Max Width | Usage |
|
|
3453
|
+
|------|-----------|-------|
|
|
3454
|
+
| `sm` | 400px | Confirmations, simple prompts |
|
|
3455
|
+
| `md` | 560px | Default, forms |
|
|
3456
|
+
| `lg` | 720px | Complex content, tables |
|
|
3457
|
+
| `fullscreen` | 100vw | Mobile, immersive experiences |
|
|
3458
|
+
|
|
3459
|
+
## Accessibility
|
|
3460
|
+
|
|
3461
|
+
- Focus is automatically trapped within dialog when open
|
|
3462
|
+
- ESC key closes dialog
|
|
3463
|
+
- Title is announced to screen readers
|
|
3464
|
+
- Background content is marked as inert
|
|
3465
|
+
|
|
3466
|
+
## Examples
|
|
3467
|
+
|
|
3468
|
+
\`\`\`typescript
|
|
3469
|
+
// Confirmation dialog
|
|
3470
|
+
<Dialog.Root>
|
|
3471
|
+
<Dialog.Trigger asChild>
|
|
3472
|
+
<Button variant="outlined">Delete Item</Button>
|
|
3473
|
+
</Dialog.Trigger>
|
|
3474
|
+
<Dialog.Backdrop />
|
|
3475
|
+
<Dialog.Positioner>
|
|
3476
|
+
<Dialog.Content size="sm">
|
|
3477
|
+
<Dialog.Title>Delete Item?</Dialog.Title>
|
|
3478
|
+
<Dialog.Description>
|
|
3479
|
+
This action cannot be undone. The item will be permanently deleted.
|
|
3480
|
+
</Dialog.Description>
|
|
3481
|
+
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
|
3482
|
+
<Dialog.CloseTrigger asChild>
|
|
3483
|
+
<Button variant="text">Cancel</Button>
|
|
3484
|
+
</Dialog.CloseTrigger>
|
|
3485
|
+
<Button variant="filled">Delete</Button>
|
|
3486
|
+
</div>
|
|
3487
|
+
</Dialog.Content>
|
|
3488
|
+
</Dialog.Positioner>
|
|
3489
|
+
</Dialog.Root>
|
|
3490
|
+
|
|
3491
|
+
// Controlled dialog
|
|
3492
|
+
function ControlledDialog() {
|
|
3493
|
+
const [open, setOpen] = useState(false);
|
|
3494
|
+
|
|
3495
|
+
return (
|
|
3496
|
+
<Dialog.Root open={open} onOpenChange={({ open }) => setOpen(open)}>
|
|
3497
|
+
<Dialog.Trigger asChild>
|
|
3498
|
+
<Button>Open</Button>
|
|
3499
|
+
</Dialog.Trigger>
|
|
3500
|
+
<Dialog.Backdrop />
|
|
3501
|
+
<Dialog.Positioner>
|
|
3502
|
+
<Dialog.Content>
|
|
3503
|
+
<Dialog.Title>Controlled Dialog</Dialog.Title>
|
|
3504
|
+
<Dialog.Description>State is managed externally.</Dialog.Description>
|
|
3505
|
+
</Dialog.Content>
|
|
3506
|
+
</Dialog.Positioner>
|
|
3507
|
+
</Dialog.Root>
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
\`\`\`
|
|
3511
|
+
|
|
3512
|
+
## DO NOT
|
|
3513
|
+
|
|
3514
|
+
\`\`\`typescript
|
|
3515
|
+
// ❌ Don't omit required parts
|
|
3516
|
+
<Dialog.Root>
|
|
3517
|
+
<Dialog.Content> {/* Missing Backdrop, Positioner, Title */}
|
|
3518
|
+
Content here
|
|
3519
|
+
</Dialog.Content>
|
|
3520
|
+
</Dialog.Root>
|
|
3521
|
+
|
|
3522
|
+
// ❌ Don't omit Title (accessibility violation)
|
|
3523
|
+
<Dialog.Root>
|
|
3524
|
+
<Dialog.Backdrop />
|
|
3525
|
+
<Dialog.Positioner>
|
|
3526
|
+
<Dialog.Content>
|
|
3527
|
+
<Dialog.Description>No title!</Dialog.Description>
|
|
3528
|
+
</Dialog.Content>
|
|
3529
|
+
</Dialog.Positioner>
|
|
3530
|
+
</Dialog.Root>
|
|
3531
|
+
|
|
3532
|
+
// ❌ Don't use native modal elements
|
|
3533
|
+
<dialog open>
|
|
3534
|
+
<p>Native dialog</p>
|
|
3535
|
+
</dialog>
|
|
3536
|
+
\`\`\`
|
|
3537
|
+
```
|
|
3538
|
+
|
|
3539
|
+
```markdown
|
|
3540
|
+
<!-- guidelines/components/input.md -->
|
|
3541
|
+
# Input
|
|
3542
|
+
|
|
3543
|
+
**Purpose:** Text input field with label, helper text, and validation.
|
|
3544
|
+
|
|
3545
|
+
## Import
|
|
3546
|
+
|
|
3547
|
+
\`\`\`typescript
|
|
3548
|
+
import { Input } from '@discourser/design-system';
|
|
3549
|
+
\`\`\`
|
|
3550
|
+
|
|
3551
|
+
## Variants
|
|
3552
|
+
|
|
3553
|
+
| Variant | Usage |
|
|
3554
|
+
|---------|-------|
|
|
3555
|
+
| `outlined` | Default, most use cases |
|
|
3556
|
+
| `filled` | Alternative style, denser layouts |
|
|
3557
|
+
|
|
3558
|
+
## Sizes
|
|
3559
|
+
|
|
3560
|
+
| Size | Height | Usage |
|
|
3561
|
+
|------|--------|-------|
|
|
3562
|
+
| `sm` | 40px | Compact forms |
|
|
3563
|
+
| `md` | 56px | Default |
|
|
3564
|
+
|
|
3565
|
+
## Props
|
|
3566
|
+
|
|
3567
|
+
| Prop | Type | Default | Description |
|
|
3568
|
+
|------|------|---------|-------------|
|
|
3569
|
+
| `variant` | `'outlined' \| 'filled'` | `'outlined'` | Visual style |
|
|
3570
|
+
| `size` | `'sm' \| 'md'` | `'md'` | Input size |
|
|
3571
|
+
| `label` | `string` | - | Field label |
|
|
3572
|
+
| `helperText` | `string` | - | Helper text below input |
|
|
3573
|
+
| `errorText` | `string` | - | Error message (shows error state) |
|
|
3574
|
+
| `disabled` | `boolean` | `false` | Disable input |
|
|
3575
|
+
|
|
3576
|
+
## Examples
|
|
3577
|
+
|
|
3578
|
+
\`\`\`typescript
|
|
3579
|
+
// Basic input with label
|
|
3580
|
+
<Input label="Email" placeholder="you@example.com" />
|
|
3581
|
+
|
|
3582
|
+
// With helper text
|
|
3583
|
+
<Input
|
|
3584
|
+
label="Password"
|
|
3585
|
+
type="password"
|
|
3586
|
+
helperText="Must be at least 8 characters"
|
|
3587
|
+
/>
|
|
3588
|
+
|
|
3589
|
+
// Error state
|
|
3590
|
+
<Input
|
|
3591
|
+
label="Email"
|
|
3592
|
+
errorText="Please enter a valid email address"
|
|
3593
|
+
/>
|
|
3594
|
+
|
|
3595
|
+
// Disabled
|
|
3596
|
+
<Input label="Username" disabled value="johndoe" />
|
|
3597
|
+
|
|
3598
|
+
// Controlled
|
|
3599
|
+
const [email, setEmail] = useState('');
|
|
3600
|
+
<Input
|
|
3601
|
+
label="Email"
|
|
3602
|
+
value={email}
|
|
3603
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
3604
|
+
/>
|
|
3605
|
+
\`\`\`
|
|
3606
|
+
|
|
3607
|
+
## DO NOT
|
|
3608
|
+
|
|
3609
|
+
\`\`\`typescript
|
|
3610
|
+
// ❌ Don't use native input without the component
|
|
3611
|
+
<input type="text" />
|
|
3612
|
+
|
|
3613
|
+
// ❌ Don't omit label (accessibility)
|
|
3614
|
+
<Input placeholder="Enter email" /> // Missing label!
|
|
3615
|
+
|
|
3616
|
+
// ❌ Don't override input styles
|
|
3617
|
+
<Input label="Email" style={{ border: '2px solid red' }} />
|
|
3618
|
+
\`\`\`
|
|
3619
|
+
```
|
|
3620
|
+
|
|
3621
|
+
#### 7.6 Publish Package to npm
|
|
3622
|
+
|
|
3623
|
+
**Public Package:**
|
|
3624
|
+
```bash
|
|
3625
|
+
# Ensure logged into npm
|
|
3626
|
+
npm login
|
|
3627
|
+
|
|
3628
|
+
# Publish
|
|
3629
|
+
pnpm publish --access public
|
|
3630
|
+
```
|
|
3631
|
+
|
|
3632
|
+
**Private Package (for Figma organization):**
|
|
3633
|
+
1. In Figma Make, go to ⚙️ Make settings → Figma npm registry
|
|
3634
|
+
2. Click "Get started" and enter organization scope (e.g., `@discourser`)
|
|
3635
|
+
3. Click "Generate key" (requires org admin)
|
|
3636
|
+
4. Add to `.npmrc`:
|
|
3637
|
+
```
|
|
3638
|
+
@discourser:registry=https://npm.figma.com/
|
|
3639
|
+
//npm.figma.com/:_authToken=YOUR_TOKEN
|
|
3640
|
+
```
|
|
3641
|
+
5. Publish: `npm publish`
|
|
3642
|
+
|
|
3643
|
+
#### 7.7 Create Figma Make Template (Optional)
|
|
3644
|
+
|
|
3645
|
+
After publishing, create a Make template for your team:
|
|
3646
|
+
1. Create new Figma Make file
|
|
3647
|
+
2. Install package: "Install @discourser/design-system"
|
|
3648
|
+
3. Add guidelines folder with all markdown files from 7.1-7.5
|
|
3649
|
+
4. Publish as template for team use
|
|
3650
|
+
|
|
3651
|
+
**Validation:**
|
|
3652
|
+
```bash
|
|
3653
|
+
# In Figma Make file:
|
|
3654
|
+
# 1. Install package
|
|
3655
|
+
# 2. Ask: "Create a form with Button and Input components"
|
|
3656
|
+
# 3. Verify Make uses design system components correctly
|
|
3657
|
+
# 4. Verify semantic tokens are applied
|
|
3658
|
+
# 5. Ask: "Create a confirmation dialog"
|
|
3659
|
+
# 6. Verify Dialog compound pattern is used correctly
|
|
3660
|
+
```
|
|
3661
|
+
|
|
3662
|
+
---
|
|
3663
|
+
|
|
3664
|
+
### Phase Summary
|
|
3665
|
+
|
|
3666
|
+
| Phase | Description | Status |
|
|
3667
|
+
|-------|-------------|--------|
|
|
3668
|
+
| 0 | Context Engineering (Skills, CLAUDE.md) | ✅ Complete |
|
|
3669
|
+
| 1 | Fix Foundation (deps, config) | ✅ Complete |
|
|
3670
|
+
| 2 | Architecture (Contract/Language/Transform) | ✅ Complete |
|
|
3671
|
+
| 3 | M3 Token Integration | ✅ Complete |
|
|
3672
|
+
| 4 | Complete Recipe Coverage + Testing | 🔄 In Progress |
|
|
3673
|
+
| 5 | Ark UI Integration + A11y Testing | ⬜ Not Started |
|
|
3674
|
+
| 6 | Build & Package (Figma Make compatible) | ⬜ Not Started |
|
|
3675
|
+
| 7 | Figma Make Guidelines | ⬜ Not Started |
|
|
3676
|
+
|
|
3677
|
+
---
|
|
3678
|
+
|
|
3679
|
+
### Slash Commands Reference
|
|
3680
|
+
|
|
3681
|
+
These commands are available in Claude Code when working in this repository:
|
|
3682
|
+
|
|
3683
|
+
```bash
|
|
3684
|
+
/fix-foundation # Run Phase 1 tasks
|
|
3685
|
+
/implement-architecture # Run Phase 2 tasks
|
|
3686
|
+
/new-component <name> # Scaffold a new component with recipe + stories + tests
|
|
3687
|
+
```
|
|
3688
|
+
|
|
3689
|
+
### Skills Reference
|
|
3690
|
+
|
|
3691
|
+
These skills auto-load when relevant:
|
|
3692
|
+
|
|
3693
|
+
| Skill | Triggers On |
|
|
3694
|
+
|-------|-------------|
|
|
3695
|
+
| `design-language` | Contract, language, transform work |
|
|
3696
|
+
| `panda-recipes` | Recipe creation, variant patterns |
|
|
3697
|
+
| `m3-tokens` | Color values, semantic tokens |
|
|
3698
|
+
| `component-patterns` | Component creation, forwardRef, Ark UI |
|