@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-to-code
|
|
3
|
+
description: Figma to code workflow for auto-layout mapping, design token extraction, component mapping, responsive export, and developer handoff.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Figma to Code Workflow
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Use this workflow when translating Figma designs into production code. It covers extracting design tokens from Figma variables, mapping auto-layout to CSS flexbox/grid, converting Figma components to React components, handling responsive breakpoints, and establishing a repeatable handoff process. Apply this workflow when starting a new design system implementation or when a design team delivers new components.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Extracting Design Tokens via Figma API
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// scripts/extract-figma-tokens.ts
|
|
17
|
+
import fetch from 'node-fetch';
|
|
18
|
+
|
|
19
|
+
const FIGMA_TOKEN = process.env.FIGMA_TOKEN!;
|
|
20
|
+
const FILE_KEY = process.env.FIGMA_FILE_KEY!;
|
|
21
|
+
|
|
22
|
+
interface FigmaColor { r: number; g: number; b: number; a: number }
|
|
23
|
+
|
|
24
|
+
async function extractTokens() {
|
|
25
|
+
const res = await fetch(
|
|
26
|
+
`https://api.figma.com/v1/files/${FILE_KEY}/variables/local`,
|
|
27
|
+
{ headers: { 'X-Figma-Token': FIGMA_TOKEN } }
|
|
28
|
+
);
|
|
29
|
+
const data = await res.json();
|
|
30
|
+
|
|
31
|
+
const tokens: Record<string, Record<string, string>> = { colors: {}, spacing: {}, radius: {} };
|
|
32
|
+
|
|
33
|
+
for (const variable of Object.values(data.meta.variables) as any[]) {
|
|
34
|
+
const name = variable.name.replace(/\//g, '-').toLowerCase();
|
|
35
|
+
const value = Object.values(variable.valuesByMode)[0] as any;
|
|
36
|
+
|
|
37
|
+
if (variable.resolvedType === 'COLOR') {
|
|
38
|
+
tokens.colors[name] = rgbaToHex(value as FigmaColor);
|
|
39
|
+
} else if (variable.resolvedType === 'FLOAT') {
|
|
40
|
+
if (name.includes('spacing')) tokens.spacing[name] = `${value}px`;
|
|
41
|
+
if (name.includes('radius')) tokens.radius[name] = `${value}px`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return tokens;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rgbaToHex({ r, g, b, a }: FigmaColor): string {
|
|
49
|
+
const hex = [r, g, b].map((c) => Math.round(c * 255).toString(16).padStart(2, '0')).join('');
|
|
50
|
+
return a < 1 ? `#${hex}${Math.round(a * 255).toString(16).padStart(2, '0')}` : `#${hex}`;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Auto-Layout to CSS Flexbox Mapping
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// Figma auto-layout properties → CSS equivalents
|
|
58
|
+
|
|
59
|
+
const autoLayoutToCSS: Record<string, Record<string, string>> = {
|
|
60
|
+
// Layout direction
|
|
61
|
+
layoutMode: {
|
|
62
|
+
HORIZONTAL: 'flex-direction: row',
|
|
63
|
+
VERTICAL: 'flex-direction: column',
|
|
64
|
+
},
|
|
65
|
+
// Primary axis alignment (main axis)
|
|
66
|
+
primaryAxisAlignItems: {
|
|
67
|
+
MIN: 'justify-content: flex-start',
|
|
68
|
+
CENTER: 'justify-content: center',
|
|
69
|
+
MAX: 'justify-content: flex-end',
|
|
70
|
+
SPACE_BETWEEN: 'justify-content: space-between',
|
|
71
|
+
},
|
|
72
|
+
// Counter axis alignment (cross axis)
|
|
73
|
+
counterAxisAlignItems: {
|
|
74
|
+
MIN: 'align-items: flex-start',
|
|
75
|
+
CENTER: 'align-items: center',
|
|
76
|
+
MAX: 'align-items: flex-end',
|
|
77
|
+
BASELINE: 'align-items: baseline',
|
|
78
|
+
},
|
|
79
|
+
// Sizing
|
|
80
|
+
primaryAxisSizingMode: {
|
|
81
|
+
FIXED: 'width/height: fixed value',
|
|
82
|
+
AUTO: 'width/height: auto (fit-content)',
|
|
83
|
+
},
|
|
84
|
+
counterAxisSizingMode: {
|
|
85
|
+
FIXED: 'cross-size: fixed value',
|
|
86
|
+
AUTO: 'cross-size: auto (fit-content)',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Example: Figma frame → Tailwind classes
|
|
91
|
+
function figmaFrameToTailwind(frame: FigmaNode): string {
|
|
92
|
+
const classes: string[] = ['flex'];
|
|
93
|
+
|
|
94
|
+
if (frame.layoutMode === 'VERTICAL') classes.push('flex-col');
|
|
95
|
+
if (frame.itemSpacing) classes.push(`gap-${pxToTailwind(frame.itemSpacing)}`);
|
|
96
|
+
if (frame.paddingTop) classes.push(`pt-${pxToTailwind(frame.paddingTop)}`);
|
|
97
|
+
if (frame.paddingRight) classes.push(`pr-${pxToTailwind(frame.paddingRight)}`);
|
|
98
|
+
if (frame.paddingBottom) classes.push(`pb-${pxToTailwind(frame.paddingBottom)}`);
|
|
99
|
+
if (frame.paddingLeft) classes.push(`pl-${pxToTailwind(frame.paddingLeft)}`);
|
|
100
|
+
|
|
101
|
+
const alignMap: Record<string, string> = {
|
|
102
|
+
MIN: 'items-start', CENTER: 'items-center', MAX: 'items-end',
|
|
103
|
+
};
|
|
104
|
+
if (frame.counterAxisAlignItems) {
|
|
105
|
+
classes.push(alignMap[frame.counterAxisAlignItems] ?? '');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const justifyMap: Record<string, string> = {
|
|
109
|
+
MIN: 'justify-start', CENTER: 'justify-center', MAX: 'justify-end',
|
|
110
|
+
SPACE_BETWEEN: 'justify-between',
|
|
111
|
+
};
|
|
112
|
+
if (frame.primaryAxisAlignItems) {
|
|
113
|
+
classes.push(justifyMap[frame.primaryAxisAlignItems] ?? '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return classes.filter(Boolean).join(' ');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pxToTailwind(px: number): string {
|
|
120
|
+
const map: Record<number, string> = { 4: '1', 8: '2', 12: '3', 16: '4', 20: '5', 24: '6', 32: '8' };
|
|
121
|
+
return map[px] ?? `[${px}px]`;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Component Mapping (Figma to React)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// Map Figma component properties to React props
|
|
129
|
+
interface FigmaComponentMapping {
|
|
130
|
+
figmaName: string;
|
|
131
|
+
reactComponent: string;
|
|
132
|
+
propMapping: Record<string, { figmaProperty: string; transform?: (v: string) => string }>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const componentMappings: FigmaComponentMapping[] = [
|
|
136
|
+
{
|
|
137
|
+
figmaName: 'Button',
|
|
138
|
+
reactComponent: 'Button',
|
|
139
|
+
propMapping: {
|
|
140
|
+
variant: { figmaProperty: 'Style', transform: (v) => v.toLowerCase() },
|
|
141
|
+
size: { figmaProperty: 'Size', transform: (v) => v.toLowerCase() },
|
|
142
|
+
disabled: { figmaProperty: 'State', transform: (v) => String(v === 'Disabled') },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
figmaName: 'Input',
|
|
147
|
+
reactComponent: 'Input',
|
|
148
|
+
propMapping: {
|
|
149
|
+
label: { figmaProperty: 'Label' },
|
|
150
|
+
error: { figmaProperty: 'Error text' },
|
|
151
|
+
placeholder: { figmaProperty: 'Placeholder' },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// Generate React component from Figma node
|
|
157
|
+
function generateComponent(node: FigmaComponentNode, mapping: FigmaComponentMapping): string {
|
|
158
|
+
const props: string[] = [];
|
|
159
|
+
for (const [reactProp, config] of Object.entries(mapping.propMapping)) {
|
|
160
|
+
const value = node.componentProperties[config.figmaProperty]?.value;
|
|
161
|
+
if (value !== undefined) {
|
|
162
|
+
const transformed = config.transform ? config.transform(String(value)) : String(value);
|
|
163
|
+
props.push(`${reactProp}="${transformed}"`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return `<${mapping.reactComponent} ${props.join(' ')} />`;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Responsive Breakpoint Strategy
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// Map Figma frames to responsive breakpoints
|
|
174
|
+
interface ResponsiveFrame {
|
|
175
|
+
name: string;
|
|
176
|
+
width: number;
|
|
177
|
+
breakpoint: string;
|
|
178
|
+
tailwindPrefix: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const responsiveFrames: ResponsiveFrame[] = [
|
|
182
|
+
{ name: 'Mobile', width: 375, breakpoint: '0px', tailwindPrefix: '' },
|
|
183
|
+
{ name: 'Tablet', width: 768, breakpoint: '768px', tailwindPrefix: 'md:' },
|
|
184
|
+
{ name: 'Desktop', width: 1280, breakpoint: '1024px', tailwindPrefix: 'lg:' },
|
|
185
|
+
{ name: 'Wide', width: 1440, breakpoint: '1280px', tailwindPrefix: 'xl:' },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// Generate responsive class string from multi-frame analysis
|
|
189
|
+
function mergeResponsiveClasses(
|
|
190
|
+
frames: Record<string, string[]>
|
|
191
|
+
): string {
|
|
192
|
+
const mobile = new Set(frames['Mobile'] ?? []);
|
|
193
|
+
const result = [...mobile];
|
|
194
|
+
|
|
195
|
+
for (const { name, tailwindPrefix } of responsiveFrames.slice(1)) {
|
|
196
|
+
const classes = frames[name] ?? [];
|
|
197
|
+
for (const cls of classes) {
|
|
198
|
+
if (!mobile.has(cls)) {
|
|
199
|
+
result.push(`${tailwindPrefix}${cls}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result.join(' ');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Example output: "flex flex-col gap-4 md:flex-row md:gap-8 lg:gap-12"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Export Automation Script
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
// scripts/figma-export.ts
|
|
214
|
+
import * as Figma from 'figma-api';
|
|
215
|
+
import fs from 'fs/promises';
|
|
216
|
+
import path from 'path';
|
|
217
|
+
|
|
218
|
+
const api = new Figma.Api({ personalAccessToken: process.env.FIGMA_TOKEN! });
|
|
219
|
+
|
|
220
|
+
async function exportIcons(fileKey: string, nodeIds: string[]) {
|
|
221
|
+
const images = await api.getImage(fileKey, {
|
|
222
|
+
ids: nodeIds.join(','),
|
|
223
|
+
format: 'svg',
|
|
224
|
+
scale: 1,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
for (const [nodeId, url] of Object.entries(images.images)) {
|
|
228
|
+
if (!url) continue;
|
|
229
|
+
const response = await fetch(url);
|
|
230
|
+
const svg = await response.text();
|
|
231
|
+
const name = nodeId.replace(/:/g, '-');
|
|
232
|
+
await fs.writeFile(
|
|
233
|
+
path.join('src/assets/icons', `${name}.svg`),
|
|
234
|
+
optimizeSvg(svg)
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function optimizeSvg(svg: string): string {
|
|
240
|
+
return svg
|
|
241
|
+
.replace(/fill="[^"]*"/g, 'fill="currentColor"')
|
|
242
|
+
.replace(/\s+/g, ' ')
|
|
243
|
+
.trim();
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Developer Handoff Checklist Generator
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// scripts/handoff-report.ts
|
|
251
|
+
interface HandoffReport {
|
|
252
|
+
component: string;
|
|
253
|
+
tokens: string[];
|
|
254
|
+
variants: string[];
|
|
255
|
+
states: string[];
|
|
256
|
+
responsive: string[];
|
|
257
|
+
a11y: string[];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function generateHandoffReport(component: FigmaComponent): HandoffReport {
|
|
261
|
+
return {
|
|
262
|
+
component: component.name,
|
|
263
|
+
tokens: extractUsedTokens(component),
|
|
264
|
+
variants: Object.keys(component.componentPropertyDefinitions ?? {}),
|
|
265
|
+
states: ['default', 'hover', 'focus', 'disabled', 'error'].filter(
|
|
266
|
+
(state) => component.children.some((c: any) => c.name.toLowerCase().includes(state))
|
|
267
|
+
),
|
|
268
|
+
responsive: responsiveFrames
|
|
269
|
+
.filter((f) => component.children.some((c: any) => c.name.includes(f.name)))
|
|
270
|
+
.map((f) => f.name),
|
|
271
|
+
a11y: [
|
|
272
|
+
component.children.some((c: any) => c.type === 'TEXT') ? 'Has visible label' : 'Needs aria-label',
|
|
273
|
+
'Check color contrast ratios',
|
|
274
|
+
'Verify keyboard navigation order',
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Examples
|
|
281
|
+
|
|
282
|
+
| Figma Property | CSS Equivalent | Tailwind Class |
|
|
283
|
+
|----------------|---------------|----------------|
|
|
284
|
+
| Auto-layout horizontal, gap 16 | `display: flex; gap: 16px` | `flex gap-4` |
|
|
285
|
+
| Auto-layout vertical, center | `flex-direction: column; align-items: center` | `flex flex-col items-center` |
|
|
286
|
+
| Fill container (horizontal) | `flex: 1 1 0%` | `flex-1` |
|
|
287
|
+
| Hug contents | `width: fit-content` | `w-fit` |
|
|
288
|
+
| Padding 16/24 | `padding: 16px 24px` | `py-4 px-6` |
|
|
289
|
+
|
|
290
|
+
## Checklist
|
|
291
|
+
- [ ] Design tokens extracted from Figma variables, not hardcoded from visual inspection
|
|
292
|
+
- [ ] Auto-layout properties mapped to flexbox (not absolute positioning)
|
|
293
|
+
- [ ] Component variants map to React prop types with TypeScript
|
|
294
|
+
- [ ] Responsive layouts checked across all Figma breakpoint frames
|
|
295
|
+
- [ ] Icons exported as SVG with `currentColor` fill for theming
|
|
296
|
+
- [ ] Spacing values use token scale, not arbitrary pixel values
|
|
297
|
+
- [ ] Interactive states (hover, focus, disabled) accounted for in component
|
|
298
|
+
- [ ] Accessibility requirements documented from Figma annotations
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: file-upload
|
|
3
|
+
description: File upload patterns with multipart, presigned URLs, chunked upload, progress tracking, and validation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# File Upload Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when users need to upload files -- profile pictures, documents, CSVs, or videos. Direct-to-cloud uploads via presigned URLs are the standard pattern. Never stream large files through your application server. Use chunked uploads for files over 100MB and always validate file type and size on both client and server.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Presigned URL Upload (Direct to S3/R2)
|
|
14
|
+
|
|
15
|
+
The client uploads directly to cloud storage, bypassing your server:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
19
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
20
|
+
|
|
21
|
+
const s3 = new S3Client({ region: "us-east-1" });
|
|
22
|
+
|
|
23
|
+
app.post("/api/upload/presign", async (req, res) => {
|
|
24
|
+
const { filename, contentType, size } = req.body;
|
|
25
|
+
|
|
26
|
+
const allowedTypes = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
|
|
27
|
+
if (!allowedTypes.includes(contentType)) {
|
|
28
|
+
return res.status(400).json({ error: "File type not allowed" });
|
|
29
|
+
}
|
|
30
|
+
if (size > 10 * 1024 * 1024) {
|
|
31
|
+
return res.status(400).json({ error: "File too large (max 10MB)" });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const key = `uploads/${req.user.id}/${crypto.randomUUID()}-${sanitize(filename)}`;
|
|
35
|
+
const command = new PutObjectCommand({
|
|
36
|
+
Bucket: process.env.S3_BUCKET,
|
|
37
|
+
Key: key,
|
|
38
|
+
ContentType: contentType,
|
|
39
|
+
Metadata: { userId: req.user.id },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const url = await getSignedUrl(s3, command, { expiresIn: 300 });
|
|
43
|
+
res.json({ uploadUrl: url, key });
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Client-side upload:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
async function uploadFile(file: File) {
|
|
51
|
+
// 1. Get presigned URL
|
|
52
|
+
const { uploadUrl, key } = await api.post("/upload/presign", {
|
|
53
|
+
filename: file.name,
|
|
54
|
+
contentType: file.type,
|
|
55
|
+
size: file.size,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 2. Upload directly to S3
|
|
59
|
+
await fetch(uploadUrl, {
|
|
60
|
+
method: "PUT",
|
|
61
|
+
headers: { "Content-Type": file.type },
|
|
62
|
+
body: file,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 3. Confirm upload with your API
|
|
66
|
+
await api.post("/upload/confirm", { key });
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Chunked Upload for Large Files
|
|
72
|
+
|
|
73
|
+
For files over 100MB, split into chunks with resume support:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
|
|
77
|
+
|
|
78
|
+
async function chunkedUpload(
|
|
79
|
+
file: File,
|
|
80
|
+
onProgress: (percent: number) => void
|
|
81
|
+
) {
|
|
82
|
+
const { uploadId, key } = await api.post("/upload/multipart/init", {
|
|
83
|
+
filename: file.name,
|
|
84
|
+
contentType: file.type,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
88
|
+
const parts: { ETag: string; PartNumber: number }[] = [];
|
|
89
|
+
let completed = 0;
|
|
90
|
+
|
|
91
|
+
for (let partNumber = 1; partNumber <= totalChunks; partNumber++) {
|
|
92
|
+
const start = (partNumber - 1) * CHUNK_SIZE;
|
|
93
|
+
const chunk = file.slice(start, start + CHUNK_SIZE);
|
|
94
|
+
|
|
95
|
+
const { presignedUrl } = await api.post("/upload/multipart/presign", {
|
|
96
|
+
key, uploadId, partNumber,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const res = await fetch(presignedUrl, { method: "PUT", body: chunk });
|
|
100
|
+
parts.push({ ETag: res.headers.get("etag")!, PartNumber: partNumber });
|
|
101
|
+
completed++;
|
|
102
|
+
onProgress(Math.round((completed / totalChunks) * 100));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await api.post("/upload/multipart/complete", {
|
|
106
|
+
key, uploadId,
|
|
107
|
+
parts: parts.sort((a, b) => a.PartNumber - b.PartNumber),
|
|
108
|
+
});
|
|
109
|
+
return key;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Progress Tracking with XMLHttpRequest
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
function uploadWithProgress(
|
|
117
|
+
url: string,
|
|
118
|
+
file: File,
|
|
119
|
+
onProgress: (percent: number) => void
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
const xhr = new XMLHttpRequest();
|
|
123
|
+
xhr.open("PUT", url);
|
|
124
|
+
xhr.setRequestHeader("Content-Type", file.type);
|
|
125
|
+
|
|
126
|
+
xhr.upload.onprogress = (e) => {
|
|
127
|
+
if (e.lengthComputable) {
|
|
128
|
+
onProgress(Math.round((e.loaded / e.total) * 100));
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
xhr.onload = () => (xhr.status < 400 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)));
|
|
132
|
+
xhr.onerror = () => reject(new Error("Network error"));
|
|
133
|
+
xhr.send(file);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Image Processing After Upload
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import sharp from "sharp";
|
|
142
|
+
|
|
143
|
+
const variants = [
|
|
144
|
+
{ suffix: "thumb", width: 150, height: 150, quality: 80 },
|
|
145
|
+
{ suffix: "medium", width: 600, height: 600, quality: 85 },
|
|
146
|
+
{ suffix: "large", width: 1200, height: 1200, quality: 90 },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
async function processImage(inputKey: string): Promise<Record<string, string>> {
|
|
150
|
+
const obj = await s3.getObject({ Bucket: BUCKET, Key: inputKey });
|
|
151
|
+
const buffer = Buffer.from(await obj.Body!.transformToByteArray());
|
|
152
|
+
const results: Record<string, string> = {};
|
|
153
|
+
|
|
154
|
+
for (const v of variants) {
|
|
155
|
+
const processed = await sharp(buffer)
|
|
156
|
+
.resize(v.width, v.height, { fit: "inside", withoutEnlargement: true })
|
|
157
|
+
.webp({ quality: v.quality })
|
|
158
|
+
.toBuffer();
|
|
159
|
+
|
|
160
|
+
const outputKey = inputKey.replace(/\.[^.]+$/, `-${v.suffix}.webp`);
|
|
161
|
+
await s3.putObject({
|
|
162
|
+
Bucket: BUCKET, Key: outputKey, Body: processed,
|
|
163
|
+
ContentType: "image/webp",
|
|
164
|
+
CacheControl: "public, max-age=31536000, immutable",
|
|
165
|
+
});
|
|
166
|
+
results[v.suffix] = outputKey;
|
|
167
|
+
}
|
|
168
|
+
return results;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### File Validation
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Server-side validation beyond content-type header
|
|
176
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
177
|
+
|
|
178
|
+
async function validateUpload(key: string): Promise<{ valid: boolean; reason?: string }> {
|
|
179
|
+
const obj = await s3.getObject({ Bucket: BUCKET, Key: key });
|
|
180
|
+
const buffer = Buffer.from(await obj.Body!.transformToByteArray());
|
|
181
|
+
|
|
182
|
+
// Check actual file type (not just extension or Content-Type header)
|
|
183
|
+
const type = await fileTypeFromBuffer(buffer);
|
|
184
|
+
const allowed = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
|
|
185
|
+
|
|
186
|
+
if (!type || !allowed.includes(type.mime)) {
|
|
187
|
+
return { valid: false, reason: `Disallowed file type: ${type?.mime ?? "unknown"}` };
|
|
188
|
+
}
|
|
189
|
+
return { valid: true };
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Storage Quotas
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const STORAGE_LIMITS: Record<string, number> = {
|
|
197
|
+
free: 100 * 1024 * 1024, // 100MB
|
|
198
|
+
pro: 10 * 1024 * 1024 * 1024, // 10GB
|
|
199
|
+
enterprise: 100 * 1024 * 1024 * 1024, // 100GB
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
async function checkStorageQuota(userId: string, fileSize: number): Promise<boolean> {
|
|
203
|
+
const user = await db.users.findById(userId);
|
|
204
|
+
const usedBytes = await db.files.sumSize({ userId });
|
|
205
|
+
const limit = STORAGE_LIMITS[user.plan] ?? STORAGE_LIMITS.free;
|
|
206
|
+
return usedBytes + fileSize <= limit;
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
| File Type | Upload Pattern | Processing |
|
|
213
|
+
|-----------|---------------|------------|
|
|
214
|
+
| Profile picture (< 5MB) | Single presigned PUT | Resize to 3 variants, WebP |
|
|
215
|
+
| CSV import (< 50MB) | Single presigned PUT | Parse in background job |
|
|
216
|
+
| Video (> 100MB) | Multipart chunked | Transcode in background |
|
|
217
|
+
| PDF invoice | Presigned PUT + signed URL | Virus scan, no transform |
|
|
218
|
+
| Bulk images (20 files) | Parallel presigned PUTs | Concurrent pipeline |
|
|
219
|
+
|
|
220
|
+
## Checklist
|
|
221
|
+
- [ ] Files upload directly to S3/R2 via presigned URLs
|
|
222
|
+
- [ ] Presigned URLs expire within 5-15 minutes
|
|
223
|
+
- [ ] File type validated against allowlist (magic bytes, not just extension)
|
|
224
|
+
- [ ] File size enforced both client-side and server-side
|
|
225
|
+
- [ ] Files over 100MB use multipart/chunked upload with resume
|
|
226
|
+
- [ ] Images processed asynchronously (resize, compress, WebP)
|
|
227
|
+
- [ ] Processed files set `Cache-Control: immutable` for CDN
|
|
228
|
+
- [ ] Per-user storage quotas prevent abuse
|