better-design 0.20.0 → 0.21.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/README.md +6 -1
- package/dist/index.js +1086 -661
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../shared/src/mcp/clients.ts
|
|
13
|
-
var MCP_NAME, API_BASE, MCP_URL, bearer,
|
|
13
|
+
var MCP_NAME, API_BASE, MCP_URL, bearer, MCP_CLIENT_CONFIGS;
|
|
14
14
|
var init_clients = __esm({
|
|
15
15
|
"../shared/src/mcp/clients.ts"() {
|
|
16
16
|
"use strict";
|
|
@@ -18,7 +18,7 @@ var init_clients = __esm({
|
|
|
18
18
|
API_BASE = process.env.NEXT_PUBLIC_APP_URL || process.env.BETTER_DESIGN_API_URL || "https://better-design.com";
|
|
19
19
|
MCP_URL = `${API_BASE}/api/mcp`;
|
|
20
20
|
bearer = (apiKey) => ({ Authorization: `Bearer ${apiKey}` });
|
|
21
|
-
|
|
21
|
+
MCP_CLIENT_CONFIGS = {
|
|
22
22
|
"claude-code": {
|
|
23
23
|
kind: "command",
|
|
24
24
|
buildCommand: (apiKey, scope) => ({
|
|
@@ -175,14 +175,14 @@ var init_clients2 = __esm({
|
|
|
175
175
|
home = os.homedir();
|
|
176
176
|
CLIENTS = [
|
|
177
177
|
{
|
|
178
|
-
...
|
|
178
|
+
...MCP_CLIENT_CONFIGS["claude-code"],
|
|
179
179
|
id: "claude-code",
|
|
180
180
|
label: "Claude Code",
|
|
181
181
|
scopes: ["global", "project"],
|
|
182
182
|
detect: () => onPath("claude")
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
|
-
...
|
|
185
|
+
...MCP_CLIENT_CONFIGS.cursor,
|
|
186
186
|
id: "cursor",
|
|
187
187
|
label: "Cursor",
|
|
188
188
|
scopes: ["global", "project"],
|
|
@@ -190,7 +190,7 @@ var init_clients2 = __esm({
|
|
|
190
190
|
detect: () => dirExists(home, ".cursor")
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
|
-
...
|
|
193
|
+
...MCP_CLIENT_CONFIGS.vscode,
|
|
194
194
|
id: "vscode",
|
|
195
195
|
label: "VS Code (Copilot)",
|
|
196
196
|
scopes: ["global", "project"],
|
|
@@ -198,7 +198,7 @@ var init_clients2 = __esm({
|
|
|
198
198
|
detect: () => dirExists(appSupport(), "Code") || onPath("code")
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
|
-
...
|
|
201
|
+
...MCP_CLIENT_CONFIGS.windsurf,
|
|
202
202
|
id: "windsurf",
|
|
203
203
|
label: "Windsurf",
|
|
204
204
|
scopes: ["global"],
|
|
@@ -206,7 +206,7 @@ var init_clients2 = __esm({
|
|
|
206
206
|
detect: () => dirExists(home, ".codeium", "windsurf")
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
|
-
...
|
|
209
|
+
...MCP_CLIENT_CONFIGS.zed,
|
|
210
210
|
id: "zed",
|
|
211
211
|
label: "Zed",
|
|
212
212
|
scopes: ["global"],
|
|
@@ -214,7 +214,7 @@ var init_clients2 = __esm({
|
|
|
214
214
|
detect: () => dirExists(configHome(), "zed") || dirExists(appSupport(), "Zed") || onPath("zed")
|
|
215
215
|
},
|
|
216
216
|
{
|
|
217
|
-
...
|
|
217
|
+
...MCP_CLIENT_CONFIGS.cline,
|
|
218
218
|
id: "cline",
|
|
219
219
|
label: "Cline (VS Code)",
|
|
220
220
|
scopes: ["global"],
|
|
@@ -230,7 +230,7 @@ var init_clients2 = __esm({
|
|
|
230
230
|
detect: () => dirExists(appSupport(), "Code", "User", "globalStorage", "saoudrizwan.claude-dev")
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
|
-
...
|
|
233
|
+
...MCP_CLIENT_CONFIGS["claude-desktop"],
|
|
234
234
|
id: "claude-desktop",
|
|
235
235
|
label: "Claude Desktop",
|
|
236
236
|
scopes: ["global"],
|
|
@@ -238,7 +238,7 @@ var init_clients2 = __esm({
|
|
|
238
238
|
detect: () => platform() === "linux" ? dirExists(configHome(), "Claude") : dirExists(appSupport(), "Claude")
|
|
239
239
|
},
|
|
240
240
|
{
|
|
241
|
-
...
|
|
241
|
+
...MCP_CLIENT_CONFIGS.opencode,
|
|
242
242
|
id: "opencode",
|
|
243
243
|
label: "OpenCode",
|
|
244
244
|
scopes: ["global", "project"],
|
|
@@ -246,7 +246,7 @@ var init_clients2 = __esm({
|
|
|
246
246
|
detect: () => dirExists(configHome(), "opencode") || onPath("opencode")
|
|
247
247
|
},
|
|
248
248
|
{
|
|
249
|
-
...
|
|
249
|
+
...MCP_CLIENT_CONFIGS.amp,
|
|
250
250
|
id: "amp",
|
|
251
251
|
label: "Amp (Sourcegraph)",
|
|
252
252
|
scopes: ["global"],
|
|
@@ -254,7 +254,7 @@ var init_clients2 = __esm({
|
|
|
254
254
|
detect: () => dirExists(configHome(), "amp") || onPath("amp")
|
|
255
255
|
},
|
|
256
256
|
{
|
|
257
|
-
...
|
|
257
|
+
...MCP_CLIENT_CONFIGS.codex,
|
|
258
258
|
id: "codex",
|
|
259
259
|
label: "Codex CLI",
|
|
260
260
|
scopes: ["global"],
|
|
@@ -262,7 +262,7 @@ var init_clients2 = __esm({
|
|
|
262
262
|
detect: () => dirExists(home, ".codex") || onPath("codex")
|
|
263
263
|
},
|
|
264
264
|
{
|
|
265
|
-
...
|
|
265
|
+
...MCP_CLIENT_CONFIGS.continue,
|
|
266
266
|
id: "continue",
|
|
267
267
|
label: "Continue.dev",
|
|
268
268
|
scopes: ["project"],
|
|
@@ -284,7 +284,7 @@ var init_constants = __esm({
|
|
|
284
284
|
RULE_END = "<!-- better-design:end -->";
|
|
285
285
|
RULE_BODY = `## Better Design
|
|
286
286
|
|
|
287
|
-
Always use the Better Design MCP tools for any frontend, UI, UX, interaction, flow, form, navigation, dialog, error, onboarding, component, styling, React Native, Expo,
|
|
287
|
+
Always use the Better Design MCP tools for any frontend, UI, UX, interaction, flow, form, navigation, dialog, error, onboarding, component, styling, React Native, Expo, Three.js, or Shopify work, without me having to explicitly ask. For React Native or Expo, load task guidance with \`get-react-native-guide\`; for Three.js or React Three Fiber, use \`get-three-js-guide\`; for Shopify Liquid or theme work, use \`get-shopify-guide\`. Load visual guidance with \`get-ui-principle\`, load flow/behavior guidance with \`get-ux-principle\`, and self-review against \`get-review-rules\` and \`check-comprehension\` after. When a rendered DOM is available, also run \`inspect-spacing\` for measured layout evidence. Start UI builds with \`find-design-system\`. For a custom brand, call \`create-design-system\`, poll \`get-design-system-status\`, and run its React install command in coding hosts; in inline canvases, call \`get-design-system-kit\` with target \`html\` and the generated projectId. For a catalog system, call \`get-design-system-kit\` with target \`react\` in coding hosts or \`html\` in inline canvases. For a Shopify store, load \`get-shopify-guide\`, then call \`get-design-system-kit\` with target \`shopify\`.`;
|
|
288
288
|
RULE_BLOCK = `${RULE_START}
|
|
289
289
|
${RULE_BODY}
|
|
290
290
|
${RULE_END}`;
|
|
@@ -1504,8 +1504,8 @@ var init_lint = __esm({
|
|
|
1504
1504
|
// src/review/config-loader.ts
|
|
1505
1505
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1506
1506
|
import { dirname, join, parse as parse2 } from "path";
|
|
1507
|
-
function isNotFound(
|
|
1508
|
-
return
|
|
1507
|
+
function isNotFound(cause) {
|
|
1508
|
+
return cause?.code === "ENOENT" || cause?.code === "ENOTDIR";
|
|
1509
1509
|
}
|
|
1510
1510
|
function loadReviewConfig(cwd) {
|
|
1511
1511
|
const { root } = parse2(cwd);
|
|
@@ -1815,7 +1815,11 @@ For any pitch deck, presentation, or slide system in a coding host with filesyst
|
|
|
1815
1815
|
|
|
1816
1816
|
// ../shared/src/mcp/instructions.ts
|
|
1817
1817
|
function getMcpInstructions(options = {}) {
|
|
1818
|
-
const {
|
|
1818
|
+
const {
|
|
1819
|
+
enableReactNativeGuide = true,
|
|
1820
|
+
enableThreeJsGuide = true,
|
|
1821
|
+
enableShopifyKit = true
|
|
1822
|
+
} = options;
|
|
1819
1823
|
let instructions = INSTRUCTIONS;
|
|
1820
1824
|
if (!enableReactNativeGuide && !enableThreeJsGuide) {
|
|
1821
1825
|
instructions = instructions.replace(", React Native, Expo, or Three.js", "").replace(/\nRN\/Expo:[^\n]+/, "");
|
|
@@ -1827,6 +1831,9 @@ function getMcpInstructions(options = {}) {
|
|
|
1827
1831
|
} else if (!enableThreeJsGuide) {
|
|
1828
1832
|
instructions = instructions.replace(", Expo, or Three.js work", ", or Expo work").replace(", get-three-js-guide", "").replace("; Three.js/R3F: get-three-js-guide", "");
|
|
1829
1833
|
}
|
|
1834
|
+
if (!enableShopifyKit) {
|
|
1835
|
+
instructions = instructions.replace(/\nShopify:[^\n]+/, "");
|
|
1836
|
+
}
|
|
1830
1837
|
return instructions.replace(/\n{3,}/g, "\n\n");
|
|
1831
1838
|
}
|
|
1832
1839
|
var INSTRUCTIONS;
|
|
@@ -1841,6 +1848,7 @@ Use for ANY frontend, UI, component, styling, presentation, slide, React Native,
|
|
|
1841
1848
|
Core tools: find-design-system, create-design-system, get-design-system-status, get-design-system-kit, get-ui-principle, get-ux-principle, get-review-rules, check-comprehension.
|
|
1842
1849
|
|
|
1843
1850
|
RN/Expo: get-react-native-guide; Three.js/R3F: get-three-js-guide. Use { topic } for lookup or { query } for search.
|
|
1851
|
+
Shopify: get-shopify-guide for implementation guidance; get-design-system-kit target "shopify" for theme files.
|
|
1844
1852
|
|
|
1845
1853
|
${PRESENTATION_RUNTIME_RULE}
|
|
1846
1854
|
|
|
@@ -1938,6 +1946,7 @@ var init_session_guidance = __esm({
|
|
|
1938
1946
|
"get-ux-principle",
|
|
1939
1947
|
"get-react-native-guide",
|
|
1940
1948
|
"get-three-js-guide",
|
|
1949
|
+
"get-shopify-guide",
|
|
1941
1950
|
"get-motion-guide"
|
|
1942
1951
|
]);
|
|
1943
1952
|
REVIEW_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -2194,6 +2203,7 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
2194
2203
|
"get-ux-principle",
|
|
2195
2204
|
"get-react-native-guide",
|
|
2196
2205
|
"get-three-js-guide",
|
|
2206
|
+
"get-shopify-guide",
|
|
2197
2207
|
"get-motion-guide",
|
|
2198
2208
|
"get-review-rules",
|
|
2199
2209
|
"find-icon-library",
|
|
@@ -2212,23 +2222,28 @@ var init_motion_scaffold = __esm({
|
|
|
2212
2222
|
{
|
|
2213
2223
|
path: "package.json",
|
|
2214
2224
|
contents: `{
|
|
2215
|
-
"name": "
|
|
2225
|
+
"name": "product-demo-video",
|
|
2216
2226
|
"private": true,
|
|
2217
2227
|
"scripts": {
|
|
2218
|
-
"dev": "remotion studio",
|
|
2219
|
-
"
|
|
2220
|
-
"
|
|
2221
|
-
"
|
|
2228
|
+
"dev": "remotion studio --no-open",
|
|
2229
|
+
"check": "tsc --noEmit && remotion compositions src/index.ts",
|
|
2230
|
+
"music": "bun scripts/make-music.ts",
|
|
2231
|
+
"captions": "bun scripts/write-captions.ts",
|
|
2232
|
+
"render": "remotion render ProductDemo-Landscape out/product-demo.mp4 --concurrency=6",
|
|
2233
|
+
"render:vertical": "remotion render ProductDemo-Vertical out/product-demo-vertical.mp4 --concurrency=6",
|
|
2234
|
+
"package": "bun scripts/package-output.ts",
|
|
2235
|
+
"review:frames": "mkdir -p out/frames && ffmpeg -i out/product-demo.mp4 -vf fps=3 out/frames/%04d.png"
|
|
2222
2236
|
},
|
|
2223
2237
|
"dependencies": {
|
|
2224
|
-
"@remotion/cli": "4.0.
|
|
2225
|
-
"@remotion/
|
|
2226
|
-
"@remotion/layout-utils": "4.0.499",
|
|
2238
|
+
"@remotion/cli": "4.0.513",
|
|
2239
|
+
"@remotion/media": "4.0.513",
|
|
2227
2240
|
"react": "19.0.0",
|
|
2228
2241
|
"react-dom": "19.0.0",
|
|
2229
|
-
"remotion": "4.0.
|
|
2242
|
+
"remotion": "4.0.513",
|
|
2243
|
+
"zod": "4.4.3"
|
|
2230
2244
|
},
|
|
2231
2245
|
"devDependencies": {
|
|
2246
|
+
"@types/node": "22.13.14",
|
|
2232
2247
|
"@types/react": "19.0.0",
|
|
2233
2248
|
"typescript": "5.7.3"
|
|
2234
2249
|
}
|
|
@@ -2247,20 +2262,15 @@ var init_motion_scaffold = __esm({
|
|
|
2247
2262
|
"skipLibCheck": true,
|
|
2248
2263
|
"esModuleInterop": true,
|
|
2249
2264
|
"noEmit": true,
|
|
2250
|
-
"lib": [
|
|
2251
|
-
"ES2022",
|
|
2252
|
-
"DOM"
|
|
2253
|
-
]
|
|
2265
|
+
"lib": ["ES2022", "DOM"]
|
|
2254
2266
|
},
|
|
2255
|
-
"include": [
|
|
2256
|
-
"src"
|
|
2257
|
-
]
|
|
2267
|
+
"include": ["src", "scripts"]
|
|
2258
2268
|
}
|
|
2259
2269
|
`
|
|
2260
2270
|
},
|
|
2261
2271
|
{
|
|
2262
2272
|
path: "remotion.config.ts",
|
|
2263
|
-
contents: `import {
|
|
2273
|
+
contents: `import {Config} from "@remotion/cli/config";
|
|
2264
2274
|
|
|
2265
2275
|
Config.setVideoImageFormat("jpeg");
|
|
2266
2276
|
Config.setOverwriteOutput(true);
|
|
@@ -2268,472 +2278,692 @@ Config.setOverwriteOutput(true);
|
|
|
2268
2278
|
},
|
|
2269
2279
|
{
|
|
2270
2280
|
path: ".gitignore",
|
|
2271
|
-
contents: `node_modules
|
|
2281
|
+
contents: `node_modules/
|
|
2282
|
+
out/
|
|
2283
|
+
public/music/
|
|
2284
|
+
public/captions/
|
|
2285
|
+
`
|
|
2272
2286
|
},
|
|
2273
2287
|
{
|
|
2274
2288
|
path: "src/index.ts",
|
|
2275
|
-
contents: `import {
|
|
2276
|
-
import {
|
|
2289
|
+
contents: `import {registerRoot} from "remotion";
|
|
2290
|
+
import {RemotionRoot} from "./Root";
|
|
2277
2291
|
|
|
2278
2292
|
registerRoot(RemotionRoot);
|
|
2279
2293
|
`
|
|
2280
2294
|
},
|
|
2281
2295
|
{
|
|
2282
|
-
path: "src/
|
|
2283
|
-
contents: `
|
|
2284
|
-
|
|
2285
|
-
|
|
2296
|
+
path: "src/design-system.ts",
|
|
2297
|
+
contents: `export type VideoFormat = "landscape" | "vertical";
|
|
2298
|
+
export type MotionTemperament = "calm" | "direct" | "expressive" | "technical";
|
|
2299
|
+
|
|
2300
|
+
export type VideoDesignSystem = {
|
|
2301
|
+
color: {
|
|
2302
|
+
stage: string;
|
|
2303
|
+
foreground: string;
|
|
2304
|
+
mutedForeground: string;
|
|
2305
|
+
surface: string;
|
|
2306
|
+
elevatedSurface: string;
|
|
2307
|
+
border: string;
|
|
2308
|
+
accent: string;
|
|
2309
|
+
accentForeground: string;
|
|
2310
|
+
};
|
|
2311
|
+
type: {
|
|
2312
|
+
family: string;
|
|
2313
|
+
displayWeight: number;
|
|
2314
|
+
bodyWeight: number;
|
|
2315
|
+
};
|
|
2316
|
+
frame: {
|
|
2317
|
+
radius: {small: number; medium: number; large: number};
|
|
2318
|
+
shadow: string;
|
|
2319
|
+
};
|
|
2320
|
+
motion: {
|
|
2321
|
+
temperament: MotionTemperament;
|
|
2322
|
+
duration: {fast: number; normal: number; slow: number};
|
|
2323
|
+
stagger: {tight: number; normal: number};
|
|
2324
|
+
spring: {settle: number; emphasis: number};
|
|
2325
|
+
distance: {short: number; medium: number};
|
|
2326
|
+
};
|
|
2327
|
+
camera: {
|
|
2328
|
+
subtlePushFrom: number;
|
|
2329
|
+
normalPushFrom: number;
|
|
2330
|
+
perspective: number;
|
|
2331
|
+
};
|
|
2332
|
+
transition: {
|
|
2333
|
+
neutral: "cut";
|
|
2334
|
+
depthFrames: number;
|
|
2335
|
+
emphasisFrames: number;
|
|
2336
|
+
};
|
|
2337
|
+
format: Record<VideoFormat, {
|
|
2338
|
+
width: number;
|
|
2339
|
+
height: number;
|
|
2340
|
+
safeX: number;
|
|
2341
|
+
safeY: number;
|
|
2342
|
+
}>;
|
|
2343
|
+
audio: {bpm: number; beatFrames: number};
|
|
2344
|
+
};
|
|
2286
2345
|
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
)
|
|
2346
|
+
/**
|
|
2347
|
+
* Replace these semantic values with the product's design-system tokens.
|
|
2348
|
+
* Keep the roles intact: the film and the app should use the same brand logic.
|
|
2349
|
+
*/
|
|
2350
|
+
export const videoDesignSystem: VideoDesignSystem = {
|
|
2351
|
+
color: {
|
|
2352
|
+
stage: "oklch(0.98 0.004 90)",
|
|
2353
|
+
foreground: "oklch(0.24 0.005 75)",
|
|
2354
|
+
mutedForeground: "oklch(0.52 0.008 80)",
|
|
2355
|
+
surface: "oklch(1 0 0)",
|
|
2356
|
+
elevatedSurface: "oklch(0.99 0.004 90)",
|
|
2357
|
+
border: "oklch(0.9 0.008 85)",
|
|
2358
|
+
accent: "oklch(0.62 0.2 256)",
|
|
2359
|
+
accentForeground: "oklch(1 0 0)",
|
|
2360
|
+
},
|
|
2361
|
+
type: {
|
|
2362
|
+
family: "ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif",
|
|
2363
|
+
displayWeight: 650,
|
|
2364
|
+
bodyWeight: 500,
|
|
2365
|
+
},
|
|
2366
|
+
frame: {
|
|
2367
|
+
radius: {small: 10, medium: 18, large: 30},
|
|
2368
|
+
shadow: "0 24px 70px oklch(0 0 0 / 0.12), 0 3px 12px oklch(0 0 0 / 0.08)",
|
|
2369
|
+
},
|
|
2370
|
+
motion: {
|
|
2371
|
+
temperament: "direct",
|
|
2372
|
+
duration: {fast: 12, normal: 24, slow: 36},
|
|
2373
|
+
stagger: {tight: 4, normal: 8},
|
|
2374
|
+
spring: {settle: 200, emphasis: 26},
|
|
2375
|
+
distance: {short: 18, medium: 48},
|
|
2376
|
+
},
|
|
2377
|
+
camera: {
|
|
2378
|
+
subtlePushFrom: 0.96,
|
|
2379
|
+
normalPushFrom: 0.92,
|
|
2380
|
+
perspective: 1200,
|
|
2381
|
+
},
|
|
2382
|
+
transition: {
|
|
2383
|
+
neutral: "cut",
|
|
2384
|
+
depthFrames: 18,
|
|
2385
|
+
emphasisFrames: 12,
|
|
2386
|
+
},
|
|
2387
|
+
format: {
|
|
2388
|
+
landscape: {width: 1920, height: 1080, safeX: 120, safeY: 100},
|
|
2389
|
+
vertical: {width: 1080, height: 1920, safeX: 84, safeY: 180},
|
|
2390
|
+
},
|
|
2391
|
+
audio: {bpm: 120, beatFrames: 15},
|
|
2392
|
+
};
|
|
2297
2393
|
`
|
|
2298
2394
|
},
|
|
2299
2395
|
{
|
|
2300
|
-
path: "src/
|
|
2301
|
-
contents: `import
|
|
2302
|
-
import { AbsoluteFill, Audio, Series, staticFile } from "remotion";
|
|
2396
|
+
path: "src/formats.ts",
|
|
2397
|
+
contents: `import {videoDesignSystem, type VideoFormat} from "./design-system";
|
|
2303
2398
|
|
|
2304
|
-
|
|
2305
|
-
import { DeviceFrame } from "./scenes/DeviceFrame";
|
|
2306
|
-
import { TitleCard } from "./scenes/TitleCard";
|
|
2307
|
-
import { color } from "./theme";
|
|
2399
|
+
export const FORMAT_PROFILES = videoDesignSystem.format;
|
|
2308
2400
|
|
|
2309
|
-
export const
|
|
2401
|
+
export const formatProfile = (format: VideoFormat) => FORMAT_PROFILES[format];
|
|
2310
2402
|
|
|
2311
|
-
|
|
2312
|
-
*
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
export const
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2403
|
+
export const scaleFromReference = (value: number, width: number, height: number) =>
|
|
2404
|
+
value * (Math.min(width, height) / 1080);
|
|
2405
|
+
|
|
2406
|
+
export const isVerticalFormat = (width: number, height: number) => height > width;
|
|
2407
|
+
`
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
path: "src/brief.ts",
|
|
2411
|
+
contents: `import {z} from "zod";
|
|
2412
|
+
|
|
2413
|
+
export const ProductDemoSchema = z.object({
|
|
2414
|
+
format: z.enum(["landscape", "vertical"]),
|
|
2415
|
+
duration: z.enum(["short", "standard"]),
|
|
2416
|
+
product: z.object({
|
|
2417
|
+
name: z.string(),
|
|
2418
|
+
oneLiner: z.string(),
|
|
2419
|
+
}),
|
|
2420
|
+
hook: z.string(),
|
|
2421
|
+
productMoment: z.object({
|
|
2422
|
+
action: z.string(),
|
|
2423
|
+
result: z.string(),
|
|
2424
|
+
}),
|
|
2425
|
+
features: z.array(z.object({
|
|
2426
|
+
title: z.string(),
|
|
2427
|
+
outcome: z.string(),
|
|
2428
|
+
})).min(2).max(3),
|
|
2429
|
+
proof: z.object({
|
|
2430
|
+
label: z.string(),
|
|
2431
|
+
value: z.string(),
|
|
2432
|
+
sourceUrl: z.string().url(),
|
|
2433
|
+
}).optional(),
|
|
2434
|
+
cta: z.object({
|
|
2435
|
+
line: z.string(),
|
|
2436
|
+
destination: z.string(),
|
|
2437
|
+
}),
|
|
2438
|
+
music: z.boolean(),
|
|
2439
|
+
reducedMotion: z.boolean(),
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
export type ProductDemoProps = z.infer<typeof ProductDemoSchema>;
|
|
2443
|
+
|
|
2444
|
+
export const DEFAULT_BRIEF: ProductDemoProps = {
|
|
2445
|
+
format: "landscape",
|
|
2446
|
+
duration: "standard",
|
|
2447
|
+
product: {
|
|
2448
|
+
name: "Your product",
|
|
2449
|
+
oneLiner: "A clear description of what it helps people do",
|
|
2331
2450
|
},
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2451
|
+
hook: "The costly problem your customer recognizes",
|
|
2452
|
+
productMoment: {
|
|
2453
|
+
action: "Show one real action in the product",
|
|
2454
|
+
result: "Show the useful result of that action",
|
|
2336
2455
|
},
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2456
|
+
features: [
|
|
2457
|
+
{title: "First concrete moment", outcome: "The result people get"},
|
|
2458
|
+
{title: "Second concrete moment", outcome: "The next useful result"},
|
|
2459
|
+
],
|
|
2460
|
+
proof: {
|
|
2461
|
+
label: "A sourced result",
|
|
2462
|
+
value: "Replace me",
|
|
2463
|
+
sourceUrl: "https://example.com/source",
|
|
2341
2464
|
},
|
|
2342
|
-
{
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
node: <TitleCard title="Try it today." />,
|
|
2465
|
+
cta: {
|
|
2466
|
+
line: "Try the product",
|
|
2467
|
+
destination: "example.com",
|
|
2346
2468
|
},
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
export const Video: React.FC = () => (
|
|
2352
|
-
<AbsoluteFill style={{ backgroundColor: color.background }}>
|
|
2353
|
-
{/* Delete this line until you have run: bun run music */}
|
|
2354
|
-
<Audio src={staticFile("music/bed.wav")} />
|
|
2355
|
-
<Series>
|
|
2356
|
-
{SCENES.map((scene) => (
|
|
2357
|
-
<Series.Sequence key={scene.id} durationInFrames={scene.frames}>
|
|
2358
|
-
{scene.node}
|
|
2359
|
-
</Series.Sequence>
|
|
2360
|
-
))}
|
|
2361
|
-
</Series>
|
|
2362
|
-
</AbsoluteFill>
|
|
2363
|
-
);
|
|
2469
|
+
music: false,
|
|
2470
|
+
reducedMotion: false,
|
|
2471
|
+
};
|
|
2364
2472
|
`
|
|
2365
2473
|
},
|
|
2366
2474
|
{
|
|
2367
|
-
path: "src/
|
|
2368
|
-
contents:
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
border: "oklch(0.9 0.008 85)",
|
|
2380
|
-
accent: "oklch(0.62 0.2 256)",
|
|
2381
|
-
accentFg: "oklch(1 0 0)",
|
|
2475
|
+
path: "src/timeline.ts",
|
|
2476
|
+
contents: `import type {ProductDemoProps} from "./brief";
|
|
2477
|
+
|
|
2478
|
+
export type SceneKind = "hook" | "positioning" | "product" | "feature" | "proof" | "cta";
|
|
2479
|
+
|
|
2480
|
+
export type SceneSpec = {
|
|
2481
|
+
id: string;
|
|
2482
|
+
kind: SceneKind;
|
|
2483
|
+
title: string;
|
|
2484
|
+
supporting?: string;
|
|
2485
|
+
sourceUrl?: string;
|
|
2486
|
+
durationInFrames: number;
|
|
2382
2487
|
};
|
|
2383
2488
|
|
|
2384
|
-
export
|
|
2489
|
+
export type TimelineScene = SceneSpec & {from: number};
|
|
2385
2490
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2491
|
+
const withOffsets = (scenes: SceneSpec[]): TimelineScene[] => {
|
|
2492
|
+
let from = 0;
|
|
2493
|
+
return scenes.map((scene) => {
|
|
2494
|
+
const result = {...scene, from};
|
|
2495
|
+
from += scene.durationInFrames;
|
|
2496
|
+
return result;
|
|
2497
|
+
});
|
|
2388
2498
|
};
|
|
2389
2499
|
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2500
|
+
const proofScenes = (props: ProductDemoProps, durationInFrames: number): SceneSpec[] => props.proof
|
|
2501
|
+
? [{
|
|
2502
|
+
id: "proof",
|
|
2503
|
+
kind: "proof",
|
|
2504
|
+
title: props.proof.value,
|
|
2505
|
+
supporting: props.proof.label,
|
|
2506
|
+
sourceUrl: props.proof.sourceUrl,
|
|
2507
|
+
durationInFrames,
|
|
2508
|
+
}]
|
|
2509
|
+
: [];
|
|
2510
|
+
|
|
2511
|
+
export const buildTimeline = (props: ProductDemoProps): TimelineScene[] => {
|
|
2512
|
+
if (props.duration === "short") {
|
|
2513
|
+
return withOffsets([
|
|
2514
|
+
{id: "hook", kind: "hook", title: props.hook, durationInFrames: 90},
|
|
2515
|
+
{
|
|
2516
|
+
id: "product",
|
|
2517
|
+
kind: "product",
|
|
2518
|
+
title: props.productMoment.action,
|
|
2519
|
+
supporting: props.productMoment.result,
|
|
2520
|
+
durationInFrames: 270,
|
|
2521
|
+
},
|
|
2522
|
+
...proofScenes(props, 120),
|
|
2523
|
+
{id: "cta", kind: "cta", title: props.cta.line, supporting: props.cta.destination, durationInFrames: 60},
|
|
2524
|
+
]);
|
|
2525
|
+
}
|
|
2397
2526
|
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2527
|
+
const featureBudget = 420;
|
|
2528
|
+
const featureLength = Math.floor(featureBudget / props.features.length);
|
|
2529
|
+
const featureScenes = props.features.map((feature, index) => ({
|
|
2530
|
+
id: "feature-" + (index + 1),
|
|
2531
|
+
kind: "feature" as const,
|
|
2532
|
+
title: feature.title,
|
|
2533
|
+
supporting: feature.outcome,
|
|
2534
|
+
durationInFrames: index === props.features.length - 1
|
|
2535
|
+
? featureBudget - featureLength * index
|
|
2536
|
+
: featureLength,
|
|
2537
|
+
}));
|
|
2538
|
+
|
|
2539
|
+
return withOffsets([
|
|
2540
|
+
{id: "hook", kind: "hook", title: props.hook, durationInFrames: 120},
|
|
2541
|
+
{id: "positioning", kind: "positioning", title: props.product.oneLiner, supporting: props.product.name, durationInFrames: 90},
|
|
2542
|
+
{
|
|
2543
|
+
id: "product",
|
|
2544
|
+
kind: "product",
|
|
2545
|
+
title: props.productMoment.action,
|
|
2546
|
+
supporting: props.productMoment.result,
|
|
2547
|
+
durationInFrames: 300,
|
|
2548
|
+
},
|
|
2549
|
+
...featureScenes,
|
|
2550
|
+
...proofScenes(props, 270),
|
|
2551
|
+
{id: "cta", kind: "cta", title: props.cta.line, supporting: props.cta.destination, durationInFrames: 150},
|
|
2552
|
+
]);
|
|
2402
2553
|
};
|
|
2554
|
+
|
|
2555
|
+
export const totalFrames = (props: ProductDemoProps) =>
|
|
2556
|
+
buildTimeline(props).reduce((total, scene) => total + scene.durationInFrames, 0);
|
|
2557
|
+
|
|
2558
|
+
export const captionCues = (props: ProductDemoProps) =>
|
|
2559
|
+
buildTimeline(props).map((scene) => ({
|
|
2560
|
+
startFrame: scene.from,
|
|
2561
|
+
endFrame: scene.from + scene.durationInFrames,
|
|
2562
|
+
text: [scene.title, scene.supporting].filter(Boolean).join(". "),
|
|
2563
|
+
}));
|
|
2403
2564
|
`
|
|
2404
2565
|
},
|
|
2405
2566
|
{
|
|
2406
|
-
path: "src/
|
|
2567
|
+
path: "src/Root.tsx",
|
|
2407
2568
|
contents: `import React from "react";
|
|
2408
|
-
import {
|
|
2409
|
-
|
|
2410
|
-
import {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2569
|
+
import {Composition, type CalculateMetadataFunction} from "remotion";
|
|
2570
|
+
import {DEFAULT_BRIEF, ProductDemoSchema, type ProductDemoProps} from "./brief";
|
|
2571
|
+
import {formatProfile} from "./formats";
|
|
2572
|
+
import {totalFrames} from "./timeline";
|
|
2573
|
+
import {ProductDemo} from "./Video";
|
|
2574
|
+
|
|
2575
|
+
const calculateMetadata: CalculateMetadataFunction<ProductDemoProps> = ({props}) => {
|
|
2576
|
+
const profile = formatProfile(props.format);
|
|
2577
|
+
return {
|
|
2578
|
+
width: profile.width,
|
|
2579
|
+
height: profile.height,
|
|
2580
|
+
durationInFrames: totalFrames(props),
|
|
2581
|
+
props,
|
|
2582
|
+
};
|
|
2583
|
+
};
|
|
2423
2584
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2585
|
+
export const RemotionRoot: React.FC = () => (
|
|
2586
|
+
<>
|
|
2587
|
+
<Composition
|
|
2588
|
+
id="ProductDemo-Landscape"
|
|
2589
|
+
component={ProductDemo}
|
|
2590
|
+
schema={ProductDemoSchema}
|
|
2591
|
+
defaultProps={{...DEFAULT_BRIEF, format: "landscape"}}
|
|
2592
|
+
calculateMetadata={calculateMetadata}
|
|
2593
|
+
durationInFrames={1350}
|
|
2594
|
+
fps={30}
|
|
2595
|
+
width={1920}
|
|
2596
|
+
height={1080}
|
|
2597
|
+
/>
|
|
2598
|
+
<Composition
|
|
2599
|
+
id="ProductDemo-Vertical"
|
|
2600
|
+
component={ProductDemo}
|
|
2601
|
+
schema={ProductDemoSchema}
|
|
2602
|
+
defaultProps={{...DEFAULT_BRIEF, format: "vertical"}}
|
|
2603
|
+
calculateMetadata={calculateMetadata}
|
|
2604
|
+
durationInFrames={1350}
|
|
2605
|
+
fps={30}
|
|
2606
|
+
width={1080}
|
|
2607
|
+
height={1920}
|
|
2608
|
+
/>
|
|
2609
|
+
</>
|
|
2610
|
+
);
|
|
2611
|
+
`
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
path: "src/Video.tsx",
|
|
2615
|
+
contents: `import React from "react";
|
|
2616
|
+
import {Audio} from "@remotion/media";
|
|
2617
|
+
import {
|
|
2618
|
+
AbsoluteFill,
|
|
2619
|
+
Easing,
|
|
2620
|
+
Series,
|
|
2621
|
+
interpolate,
|
|
2622
|
+
spring,
|
|
2623
|
+
staticFile,
|
|
2624
|
+
useCurrentFrame,
|
|
2625
|
+
useVideoConfig,
|
|
2626
|
+
} from "remotion";
|
|
2627
|
+
import type {ProductDemoProps} from "./brief";
|
|
2628
|
+
import {videoDesignSystem as system} from "./design-system";
|
|
2629
|
+
import {formatProfile, isVerticalFormat, scaleFromReference} from "./formats";
|
|
2630
|
+
import {buildTimeline, type TimelineScene} from "./timeline";
|
|
2631
|
+
|
|
2632
|
+
const enter = (frame: number, fps: number, reducedMotion: boolean) => reducedMotion
|
|
2633
|
+
? 1
|
|
2634
|
+
: spring({
|
|
2635
|
+
frame,
|
|
2636
|
+
fps,
|
|
2637
|
+
config: {damping: system.motion.spring.settle},
|
|
2429
2638
|
});
|
|
2430
2639
|
|
|
2640
|
+
const SceneFrame: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
2641
|
+
const {width, height} = useVideoConfig();
|
|
2642
|
+
const vertical = isVerticalFormat(width, height);
|
|
2643
|
+
const profile = formatProfile(vertical ? "vertical" : "landscape");
|
|
2431
2644
|
return (
|
|
2432
|
-
<AbsoluteFill
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
<h1
|
|
2441
|
-
style={{
|
|
2442
|
-
...type.title,
|
|
2443
|
-
margin: 0,
|
|
2444
|
-
width: "100%",
|
|
2445
|
-
textAlign: "center",
|
|
2446
|
-
color: muted ? color.mutedFg : color.foreground,
|
|
2447
|
-
opacity: fade,
|
|
2448
|
-
transform: "translateY(" + rise + "px)",
|
|
2449
|
-
}}
|
|
2450
|
-
>
|
|
2451
|
-
{title}
|
|
2452
|
-
</h1>
|
|
2645
|
+
<AbsoluteFill style={{
|
|
2646
|
+
padding: profile.safeY + "px " + profile.safeX + "px",
|
|
2647
|
+
backgroundColor: system.color.stage,
|
|
2648
|
+
color: system.color.foreground,
|
|
2649
|
+
fontFamily: system.type.family,
|
|
2650
|
+
overflow: "hidden",
|
|
2651
|
+
}}>
|
|
2652
|
+
{children}
|
|
2453
2653
|
</AbsoluteFill>
|
|
2454
2654
|
);
|
|
2455
2655
|
};
|
|
2456
|
-
`
|
|
2457
|
-
},
|
|
2458
|
-
{
|
|
2459
|
-
path: "src/scenes/DeviceFrame.tsx",
|
|
2460
|
-
contents: `import React from "react";
|
|
2461
|
-
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
2462
2656
|
|
|
2463
|
-
|
|
2657
|
+
type SceneProps = {scene: TimelineScene; reducedMotion: boolean};
|
|
2464
2658
|
|
|
2465
|
-
|
|
2466
|
-
* A screenshot inside a frame. Put your own UI where the placeholder is, and
|
|
2467
|
-
* keep any cursor or callout INSIDE this transform, or it drifts off target
|
|
2468
|
-
* the moment the scene scales.
|
|
2469
|
-
*/
|
|
2470
|
-
export const DeviceFrame: React.FC<{ caption: string }> = ({ caption }) => {
|
|
2659
|
+
const MessageScene: React.FC<SceneProps & {accent?: boolean}> = ({scene, reducedMotion, accent = false}) => {
|
|
2471
2660
|
const frame = useCurrentFrame();
|
|
2472
|
-
const {
|
|
2473
|
-
|
|
2474
|
-
const
|
|
2475
|
-
const
|
|
2476
|
-
const fade = interpolate(frame, [0, 10], [0, 1], {
|
|
2477
|
-
extrapolateLeft: "clamp",
|
|
2478
|
-
extrapolateRight: "clamp",
|
|
2479
|
-
});
|
|
2480
|
-
|
|
2661
|
+
const {fps, width, height} = useVideoConfig();
|
|
2662
|
+
const vertical = isVerticalFormat(width, height);
|
|
2663
|
+
const progress = enter(frame, fps, reducedMotion);
|
|
2664
|
+
const titleSize = scaleFromReference(vertical ? 92 : 84, width, height);
|
|
2481
2665
|
return (
|
|
2482
|
-
<
|
|
2483
|
-
style={{
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
}}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2666
|
+
<SceneFrame>
|
|
2667
|
+
<div style={{
|
|
2668
|
+
margin: "auto",
|
|
2669
|
+
width: "100%",
|
|
2670
|
+
maxWidth: vertical ? 850 : 1500,
|
|
2671
|
+
textAlign: "center",
|
|
2672
|
+
translate: reducedMotion ? "none" : "0 " + interpolate(progress, [0, 1], [system.motion.distance.short, 0]) + "px",
|
|
2673
|
+
}}>
|
|
2674
|
+
<h1 style={{
|
|
2675
|
+
margin: 0,
|
|
2676
|
+
fontSize: titleSize,
|
|
2677
|
+
lineHeight: 1.02,
|
|
2678
|
+
letterSpacing: "-0.045em",
|
|
2679
|
+
fontWeight: system.type.displayWeight,
|
|
2680
|
+
textWrap: "balance",
|
|
2681
|
+
color: accent ? system.color.accent : system.color.foreground,
|
|
2682
|
+
}}>
|
|
2683
|
+
{scene.title}
|
|
2684
|
+
</h1>
|
|
2685
|
+
{scene.supporting ? (
|
|
2686
|
+
<p style={{
|
|
2687
|
+
margin: vertical ? "38px 0 0" : "30px 0 0",
|
|
2688
|
+
fontSize: scaleFromReference(34, width, height),
|
|
2689
|
+
lineHeight: 1.25,
|
|
2690
|
+
color: system.color.mutedForeground,
|
|
2691
|
+
}}>
|
|
2692
|
+
{scene.supporting}
|
|
2693
|
+
</p>
|
|
2694
|
+
) : null}
|
|
2507
2695
|
</div>
|
|
2508
|
-
|
|
2509
|
-
</AbsoluteFill>
|
|
2696
|
+
</SceneFrame>
|
|
2510
2697
|
);
|
|
2511
2698
|
};
|
|
2512
|
-
`
|
|
2513
|
-
},
|
|
2514
|
-
{
|
|
2515
|
-
path: "src/scenes/BeforeAfter.tsx",
|
|
2516
|
-
contents: `import React from "react";
|
|
2517
|
-
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
|
2518
|
-
|
|
2519
|
-
import { color, radius, sans, shadow, type } from "../theme";
|
|
2520
2699
|
|
|
2521
|
-
|
|
2522
|
-
* The comparison scene. It only works if both sides are on screen together
|
|
2523
|
-
* long enough to read, so bring the second panel in early and hold both.
|
|
2524
|
-
*/
|
|
2525
|
-
export const BeforeAfter: React.FC<{
|
|
2526
|
-
beforeLabel: string;
|
|
2527
|
-
afterLabel: string;
|
|
2528
|
-
}> = ({ beforeLabel, afterLabel }) => {
|
|
2700
|
+
const ProductScene: React.FC<SceneProps> = ({scene, reducedMotion}) => {
|
|
2529
2701
|
const frame = useCurrentFrame();
|
|
2530
|
-
|
|
2531
|
-
const
|
|
2702
|
+
const {fps, width, height} = useVideoConfig();
|
|
2703
|
+
const vertical = isVerticalFormat(width, height);
|
|
2704
|
+
const profile = formatProfile(vertical ? "vertical" : "landscape");
|
|
2705
|
+
const progress = enter(frame, fps, reducedMotion);
|
|
2706
|
+
const clickFrame = Math.min(96, Math.floor(scene.durationInFrames * 0.42));
|
|
2707
|
+
const resultOpacity = reducedMotion ? 1 : interpolate(frame, [clickFrame + system.motion.duration.fast, clickFrame + system.motion.duration.normal], [0, 1], {
|
|
2532
2708
|
extrapolateLeft: "clamp",
|
|
2533
2709
|
extrapolateRight: "clamp",
|
|
2710
|
+
easing: Easing.bezier(0.16, 1, 0.3, 1),
|
|
2534
2711
|
});
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2712
|
+
const panelWidth = vertical ? width - profile.safeX * 2 : Math.min(width - profile.safeX * 2, 1320);
|
|
2713
|
+
const panelHeight = vertical ? Math.min(980, height * 0.52) : Math.min(680, height - 260);
|
|
2714
|
+
return (
|
|
2715
|
+
<SceneFrame>
|
|
2716
|
+
<div style={{display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", gap: vertical ? 54 : 34}}>
|
|
2717
|
+
<p style={{margin: 0, fontSize: scaleFromReference(32, width, height), fontWeight: 600, textAlign: "center"}}>{scene.title}</p>
|
|
2718
|
+
<div style={{
|
|
2719
|
+
position: "relative",
|
|
2720
|
+
width: panelWidth,
|
|
2721
|
+
height: panelHeight,
|
|
2722
|
+
borderRadius: system.frame.radius.large,
|
|
2723
|
+
border: "1px solid " + system.color.border,
|
|
2724
|
+
backgroundColor: system.color.surface,
|
|
2725
|
+
boxShadow: system.frame.shadow,
|
|
2726
|
+
overflow: "hidden",
|
|
2727
|
+
scale: reducedMotion ? 1 : interpolate(progress, [0, 1], [system.camera.subtlePushFrom, 1], {output: "perceptual-scale"}),
|
|
2728
|
+
}}>
|
|
2729
|
+
<div style={{height: 62, borderBottom: "1px solid " + system.color.border, display: "flex", alignItems: "center", gap: 12, padding: "0 24px"}}>
|
|
2730
|
+
{[0, 1, 2].map((dot) => <span key={dot} style={{width: 12, height: 12, borderRadius: 999, backgroundColor: system.color.border}} />)}
|
|
2731
|
+
</div>
|
|
2732
|
+
<div style={{padding: vertical ? 42 : 54, display: "grid", gap: 24}}>
|
|
2733
|
+
<div style={{height: 18, width: "38%", borderRadius: 999, backgroundColor: system.color.border}} />
|
|
2734
|
+
<div style={{height: 110, borderRadius: system.frame.radius.medium, border: "1px solid " + system.color.border, padding: 28, display: "flex", justifyContent: "space-between", alignItems: "center"}}>
|
|
2735
|
+
<span style={{fontSize: scaleFromReference(28, width, height), fontWeight: 600}}>{scene.title}</span>
|
|
2736
|
+
<span style={{position: "relative", padding: "14px 22px", borderRadius: system.frame.radius.small, backgroundColor: system.color.accent, color: system.color.accentForeground, fontSize: scaleFromReference(22, width, height), fontWeight: 650}}>
|
|
2737
|
+
Run
|
|
2738
|
+
<span style={{
|
|
2739
|
+
position: "absolute",
|
|
2740
|
+
display: reducedMotion ? "none" : "block",
|
|
2741
|
+
left: "50%",
|
|
2742
|
+
top: "50%",
|
|
2743
|
+
zIndex: 2,
|
|
2744
|
+
width: 28,
|
|
2745
|
+
height: 38,
|
|
2746
|
+
backgroundColor: system.color.foreground,
|
|
2747
|
+
clipPath: "polygon(0 0, 100% 70%, 58% 72%, 45% 100%)",
|
|
2748
|
+
translate: interpolate(frame, [12, clickFrame], ["-220px 160px", "4px 6px"], {extrapolateLeft: "clamp", extrapolateRight: "clamp", easing: Easing.bezier(0.16, 1, 0.3, 1)}),
|
|
2749
|
+
scale: interpolate(frame, [clickFrame, clickFrame + 6, clickFrame + 12], [1, 0.82, 1], {extrapolateLeft: "clamp", extrapolateRight: "clamp", output: "perceptual-scale"}),
|
|
2750
|
+
}} />
|
|
2751
|
+
</span>
|
|
2752
|
+
</div>
|
|
2753
|
+
<div style={{opacity: resultOpacity, borderRadius: system.frame.radius.medium, padding: 28, backgroundColor: system.color.elevatedSurface, border: "1px solid " + system.color.accent, fontSize: scaleFromReference(27, width, height)}}>
|
|
2754
|
+
{scene.supporting}
|
|
2755
|
+
</div>
|
|
2756
|
+
</div>
|
|
2757
|
+
</div>
|
|
2758
|
+
</div>
|
|
2759
|
+
</SceneFrame>
|
|
2760
|
+
);
|
|
2761
|
+
};
|
|
2552
2762
|
|
|
2763
|
+
const ProofScene: React.FC<SceneProps> = ({scene, reducedMotion}) => {
|
|
2764
|
+
const frame = useCurrentFrame();
|
|
2765
|
+
const {fps, width, height} = useVideoConfig();
|
|
2766
|
+
const vertical = isVerticalFormat(width, height);
|
|
2767
|
+
const profile = formatProfile(vertical ? "vertical" : "landscape");
|
|
2768
|
+
const progress = enter(frame, fps, reducedMotion);
|
|
2553
2769
|
return (
|
|
2554
|
-
<
|
|
2555
|
-
style={{
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
gap: 48,
|
|
2559
|
-
flexDirection: "row",
|
|
2560
|
-
fontFamily: sans,
|
|
2561
|
-
}}
|
|
2562
|
-
>
|
|
2563
|
-
<div style={{ opacity: left, display: "grid", gap: 16, justifyItems: "center" }}>
|
|
2564
|
-
<div style={panel}>Your old screen</div>
|
|
2565
|
-
<span style={{ ...type.label, color: color.mutedFg }}>{beforeLabel}</span>
|
|
2770
|
+
<SceneFrame>
|
|
2771
|
+
<div style={{margin: "auto", textAlign: "center", scale: reducedMotion ? 1 : interpolate(progress, [0, 1], [0.92, 1], {output: "perceptual-scale"})}}>
|
|
2772
|
+
<div style={{fontSize: scaleFromReference(150, width, height), lineHeight: 1, letterSpacing: "-0.06em", fontWeight: 700, color: system.color.accent}}>{scene.title}</div>
|
|
2773
|
+
<p style={{fontSize: scaleFromReference(36, width, height), color: system.color.foreground}}>{scene.supporting}</p>
|
|
2566
2774
|
</div>
|
|
2567
|
-
<div style={{
|
|
2568
|
-
|
|
2569
|
-
<span style={{ ...type.label, color: color.foreground }}>{afterLabel}</span>
|
|
2775
|
+
<div style={{position: "absolute", left: 0, right: 0, bottom: profile.safeY, textAlign: "center", fontSize: scaleFromReference(18, width, height), color: system.color.mutedForeground}}>
|
|
2776
|
+
Source: {scene.sourceUrl}
|
|
2570
2777
|
</div>
|
|
2778
|
+
</SceneFrame>
|
|
2779
|
+
);
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
const SceneRenderer: React.FC<SceneProps> = ({scene, reducedMotion}) => {
|
|
2783
|
+
if (scene.kind === "product") return <ProductScene scene={scene} reducedMotion={reducedMotion} />;
|
|
2784
|
+
if (scene.kind === "proof") return <ProofScene scene={scene} reducedMotion={reducedMotion} />;
|
|
2785
|
+
return <MessageScene scene={scene} reducedMotion={reducedMotion} accent={scene.kind === "cta"} />;
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
export const ProductDemo: React.FC<ProductDemoProps> = (props) => {
|
|
2789
|
+
const timeline = buildTimeline(props);
|
|
2790
|
+
return (
|
|
2791
|
+
<AbsoluteFill style={{backgroundColor: system.color.stage}}>
|
|
2792
|
+
{props.music ? <Audio src={staticFile("music/bed.wav")} /> : null}
|
|
2793
|
+
<Series>
|
|
2794
|
+
{timeline.map((scene) => (
|
|
2795
|
+
<Series.Sequence key={scene.id} name={scene.id} durationInFrames={scene.durationInFrames}>
|
|
2796
|
+
<SceneRenderer scene={scene} reducedMotion={props.reducedMotion} />
|
|
2797
|
+
</Series.Sequence>
|
|
2798
|
+
))}
|
|
2799
|
+
</Series>
|
|
2571
2800
|
</AbsoluteFill>
|
|
2572
2801
|
);
|
|
2573
2802
|
};
|
|
2574
2803
|
`
|
|
2575
2804
|
},
|
|
2576
2805
|
{
|
|
2577
|
-
path: "scripts/
|
|
2578
|
-
contents: `""
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
2593
|
-
SR = 44_100
|
|
2594
|
-
FPS = 60
|
|
2595
|
-
BPM = 120
|
|
2596
|
-
BEAT = 60 / BPM
|
|
2597
|
-
|
|
2598
|
-
SCENES = [
|
|
2599
|
-
int(n)
|
|
2600
|
-
for n in re.findall(r"frames: (\\d+)", (ROOT / "src" / "Video.tsx").read_text())
|
|
2601
|
-
]
|
|
2602
|
-
if not SCENES:
|
|
2603
|
-
raise SystemExit("no \`frames: <n>\` entries found in src/Video.tsx")
|
|
2604
|
-
|
|
2605
|
-
TOTAL = sum(SCENES) / FPS
|
|
2606
|
-
CUTS = []
|
|
2607
|
-
acc = 0
|
|
2608
|
-
for length in SCENES[:-1]:
|
|
2609
|
-
acc += length
|
|
2610
|
-
CUTS.append(acc / FPS)
|
|
2611
|
-
|
|
2612
|
-
# A minor pentatonic pad. Swap these for any scale you like.
|
|
2613
|
-
CHORD = [220.00, 261.63, 329.63, 392.00]
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
def envelope(t: float, attack: float, release: float, length: float) -> float:
|
|
2617
|
-
if t < 0 or t > length:
|
|
2618
|
-
return 0.0
|
|
2619
|
-
if t < attack:
|
|
2620
|
-
return t / attack
|
|
2621
|
-
if t > length - release:
|
|
2622
|
-
return max(0.0, (length - t) / release)
|
|
2623
|
-
return 1.0
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
samples = []
|
|
2627
|
-
for i in range(int(SR * TOTAL)):
|
|
2628
|
-
t = i / SR
|
|
2629
|
-
value = 0.0
|
|
2630
|
-
|
|
2631
|
-
# Pad: quiet, always there, gives the film a floor.
|
|
2632
|
-
for n, freq in enumerate(CHORD):
|
|
2633
|
-
value += math.sin(2 * math.pi * freq * t) * (0.05 / (n + 1))
|
|
2634
|
-
|
|
2635
|
-
# A pluck on every beat, louder on the downbeat.
|
|
2636
|
-
beat_pos = t % BEAT
|
|
2637
|
-
beat_index = int(t / BEAT)
|
|
2638
|
-
accent = 1.0 if beat_index % 4 == 0 else 0.45
|
|
2639
|
-
value += (
|
|
2640
|
-
math.sin(2 * math.pi * CHORD[beat_index % len(CHORD)] * 2 * t)
|
|
2641
|
-
* envelope(beat_pos, 0.005, 0.18, BEAT)
|
|
2642
|
-
* 0.10
|
|
2643
|
-
* accent
|
|
2644
|
-
)
|
|
2645
|
-
|
|
2646
|
-
# A swell into every cut, so the edit and the music agree.
|
|
2647
|
-
for cut in CUTS:
|
|
2648
|
-
since = t - (cut - 0.6)
|
|
2649
|
-
if 0 <= since <= 0.6:
|
|
2650
|
-
value += math.sin(2 * math.pi * 110 * t) * (since / 0.6) * 0.12
|
|
2806
|
+
path: "scripts/write-captions.ts",
|
|
2807
|
+
contents: `import {mkdir, writeFile} from "node:fs/promises";
|
|
2808
|
+
import {DEFAULT_BRIEF} from "../src/brief";
|
|
2809
|
+
import {captionCues} from "../src/timeline";
|
|
2810
|
+
|
|
2811
|
+
const fps = 30;
|
|
2812
|
+
const stamp = (frame: number) => {
|
|
2813
|
+
const milliseconds = Math.round((frame / fps) * 1000);
|
|
2814
|
+
const hours = Math.floor(milliseconds / 3_600_000);
|
|
2815
|
+
const minutes = Math.floor((milliseconds % 3_600_000) / 60_000);
|
|
2816
|
+
const seconds = Math.floor((milliseconds % 60_000) / 1000);
|
|
2817
|
+
const rest = milliseconds % 1000;
|
|
2818
|
+
return [hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":") + "." + String(rest).padStart(3, "0");
|
|
2819
|
+
};
|
|
2651
2820
|
|
|
2652
|
-
|
|
2821
|
+
const body = captionCues(DEFAULT_BRIEF).map((cue, index) => [
|
|
2822
|
+
String(index + 1),
|
|
2823
|
+
stamp(cue.startFrame) + " --> " + stamp(cue.endFrame),
|
|
2824
|
+
cue.text,
|
|
2825
|
+
].join("\\n")).join("\\n\\n");
|
|
2653
2826
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2827
|
+
await mkdir("public/captions", {recursive: true});
|
|
2828
|
+
await writeFile("public/captions/product-demo.vtt", "WEBVTT\\n\\n" + body + "\\n");
|
|
2829
|
+
console.log("Wrote public/captions/product-demo.vtt");
|
|
2830
|
+
`
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
path: "scripts/make-music.ts",
|
|
2834
|
+
contents: `import {mkdir, writeFile} from "node:fs/promises";
|
|
2835
|
+
import {DEFAULT_BRIEF} from "../src/brief";
|
|
2836
|
+
import {videoDesignSystem} from "../src/design-system";
|
|
2837
|
+
import {buildTimeline, totalFrames} from "../src/timeline";
|
|
2838
|
+
|
|
2839
|
+
const sampleRate = 44_100;
|
|
2840
|
+
const fps = 30;
|
|
2841
|
+
const beatSeconds = 60 / videoDesignSystem.audio.bpm;
|
|
2842
|
+
const notes = [220, 261.63, 329.63, 392];
|
|
2843
|
+
const timeline = buildTimeline(DEFAULT_BRIEF);
|
|
2844
|
+
const cutSeconds = timeline.slice(1).map((scene) => scene.from / fps);
|
|
2845
|
+
const sampleCount = Math.floor(sampleRate * totalFrames(DEFAULT_BRIEF) / fps);
|
|
2846
|
+
const samples = new Float32Array(sampleCount);
|
|
2847
|
+
|
|
2848
|
+
for (let index = 0; index < sampleCount; index++) {
|
|
2849
|
+
const time = index / sampleRate;
|
|
2850
|
+
let value = notes.reduce(
|
|
2851
|
+
(sum, note, position) => sum + Math.sin(2 * Math.PI * note * time) * (0.04 / (position + 1)),
|
|
2852
|
+
0,
|
|
2853
|
+
);
|
|
2854
|
+
const beatPosition = time % beatSeconds;
|
|
2855
|
+
const beatIndex = Math.floor(time / beatSeconds);
|
|
2856
|
+
if (beatPosition < 0.16) {
|
|
2857
|
+
const envelope = (1 - beatPosition / 0.16) * (beatIndex % 4 === 0 ? 1 : 0.45);
|
|
2858
|
+
value += Math.sin(2 * Math.PI * notes[beatIndex % notes.length] * 2 * time) * envelope * 0.1;
|
|
2859
|
+
}
|
|
2860
|
+
for (const cut of cutSeconds) {
|
|
2861
|
+
const distance = time - (cut - 0.5);
|
|
2862
|
+
if (distance >= 0 && distance <= 0.5) {
|
|
2863
|
+
value += Math.sin(2 * Math.PI * 110 * time) * (distance / 0.5) * 0.1;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
samples[index] = value;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
const peak = samples.reduce((largest, sample) => Math.max(largest, Math.abs(sample)), 0) || 1;
|
|
2870
|
+
const gain = 0.89 / peak;
|
|
2871
|
+
const bytesPerSample = 2;
|
|
2872
|
+
const dataBytes = sampleCount * bytesPerSample;
|
|
2873
|
+
const wav = Buffer.alloc(44 + dataBytes);
|
|
2874
|
+
wav.write("RIFF", 0);
|
|
2875
|
+
wav.writeUInt32LE(36 + dataBytes, 4);
|
|
2876
|
+
wav.write("WAVE", 8);
|
|
2877
|
+
wav.write("fmt ", 12);
|
|
2878
|
+
wav.writeUInt32LE(16, 16);
|
|
2879
|
+
wav.writeUInt16LE(1, 20);
|
|
2880
|
+
wav.writeUInt16LE(1, 22);
|
|
2881
|
+
wav.writeUInt32LE(sampleRate, 24);
|
|
2882
|
+
wav.writeUInt32LE(sampleRate * bytesPerSample, 28);
|
|
2883
|
+
wav.writeUInt16LE(bytesPerSample, 32);
|
|
2884
|
+
wav.writeUInt16LE(16, 34);
|
|
2885
|
+
wav.write("data", 36);
|
|
2886
|
+
wav.writeUInt32LE(dataBytes, 40);
|
|
2887
|
+
|
|
2888
|
+
for (let index = 0; index < sampleCount; index++) {
|
|
2889
|
+
const normalized = Math.max(-1, Math.min(1, samples[index] * gain));
|
|
2890
|
+
wav.writeInt16LE(Math.round(normalized * 32_767), 44 + index * bytesPerSample);
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
await mkdir("public/music", {recursive: true});
|
|
2894
|
+
await writeFile("public/music/bed.wav", wav);
|
|
2895
|
+
console.log("Wrote public/music/bed.wav");
|
|
2896
|
+
`
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
path: "scripts/package-output.ts",
|
|
2900
|
+
contents: `import {copyFile, mkdir} from "node:fs/promises";
|
|
2657
2901
|
|
|
2658
|
-
|
|
2659
|
-
out.
|
|
2660
|
-
|
|
2902
|
+
const artifacts = [
|
|
2903
|
+
["out/product-demo.mp4", "out/delivery/product-demo.mp4"],
|
|
2904
|
+
["out/product-demo-vertical.mp4", "out/delivery/product-demo-vertical.mp4"],
|
|
2905
|
+
["public/captions/product-demo.vtt", "out/delivery/product-demo.vtt"],
|
|
2906
|
+
] as const;
|
|
2661
2907
|
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
f.writeframes(
|
|
2667
|
-
b"".join(struct.pack("<h", int(max(-1.0, min(1.0, s * gain)) * 32767)) for s in samples)
|
|
2668
|
-
)
|
|
2908
|
+
await mkdir("out/delivery", {recursive: true});
|
|
2909
|
+
for (const [source, destination] of artifacts) {
|
|
2910
|
+
await copyFile(source, destination);
|
|
2911
|
+
}
|
|
2669
2912
|
|
|
2670
|
-
|
|
2913
|
+
console.log("Packaged both videos and the required VTT caption track in out/delivery");
|
|
2671
2914
|
`
|
|
2672
2915
|
},
|
|
2673
2916
|
{
|
|
2674
2917
|
path: "README.md",
|
|
2675
|
-
contents: `#
|
|
2918
|
+
contents: `# Product demo video
|
|
2676
2919
|
|
|
2677
|
-
|
|
2678
|
-
changed like any other component.
|
|
2920
|
+
This starter turns a product brief and an existing design system into an editable Remotion video. The product interaction is the visual proof.
|
|
2679
2921
|
|
|
2680
|
-
##
|
|
2922
|
+
## Start here
|
|
2681
2923
|
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
bun run dev
|
|
2686
|
-
|
|
2687
|
-
\`\`\`
|
|
2924
|
+
1. Install the official Remotion skills from https://www.remotion.dev/docs/ai/skills.
|
|
2925
|
+
2. Replace the semantic tokens in src/design-system.ts with the product's tokens.
|
|
2926
|
+
3. Replace every value in DEFAULT_BRIEF in src/brief.ts. The supplied values are explicit placeholders, not final copy or proof.
|
|
2927
|
+
4. Run bun install, bun run check and bun run dev. Open the exact Studio URL printed by the command.
|
|
2928
|
+
5. Check both ProductDemo-Landscape and ProductDemo-Vertical. Turn on reducedMotion and confirm that every scene still shows the same message and end state.
|
|
2688
2929
|
|
|
2689
|
-
|
|
2930
|
+
The default composition renders without an audio file. Run bun run music, then set music to true in the Studio props when the bed is wanted.
|
|
2690
2931
|
|
|
2691
|
-
|
|
2932
|
+
FFmpeg must be installed and available on PATH only for bun run review:frames.
|
|
2692
2933
|
|
|
2693
|
-
|
|
2694
|
-
Read the scene ids on their own: if they do not tell your story in order, fix
|
|
2695
|
-
that before touching any animation.
|
|
2934
|
+
## Render and review
|
|
2696
2935
|
|
|
2697
|
-
|
|
2698
|
-
system's tokens so the film and the app are the same brand.
|
|
2936
|
+
Run:
|
|
2699
2937
|
|
|
2700
|
-
|
|
2701
|
-
|
|
2938
|
+
bun run captions
|
|
2939
|
+
bun run render
|
|
2940
|
+
bun run render:vertical
|
|
2941
|
+
bun run review:frames
|
|
2942
|
+
bun run package
|
|
2702
2943
|
|
|
2703
|
-
|
|
2944
|
+
The frame command samples at 3fps. Also check the first and last frame of every scene. A successful render is not visual approval. The package command places both videos and the required VTT caption track in out/delivery. Upload the VTT beside the MP4 instead of treating it as an optional build artifact.
|
|
2704
2945
|
|
|
2705
|
-
|
|
2706
|
-
cut on a beat. \`scripts/make-music.py\` reads those lengths back out of
|
|
2707
|
-
\`src/Video.tsx\`, so the music follows the edit rather than the other way round.
|
|
2946
|
+
## Design-system contract
|
|
2708
2947
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2948
|
+
- src/design-system.ts is the source of visual, motion and audio tokens.
|
|
2949
|
+
- src/formats.ts owns dimensions and safe areas. Vertical reflows; it does not shrink the landscape frame.
|
|
2950
|
+
- src/brief.ts owns product content and sourced proof.
|
|
2951
|
+
- src/timeline.ts owns scene order, lengths and caption cues.
|
|
2952
|
+
- src/Video.tsx owns reusable scene rendering.
|
|
2711
2953
|
|
|
2712
|
-
|
|
2713
|
-
ffmpeg -i out/video.mp4 -vf fps=3 frames/%04d.png
|
|
2714
|
-
\`\`\`
|
|
2715
|
-
|
|
2716
|
-
Sampling once a second is not enough. Check the first and last frame of every
|
|
2717
|
-
scene, which is where entry and exit animations fail.
|
|
2718
|
-
|
|
2719
|
-
## Shipping it to the web
|
|
2720
|
-
|
|
2721
|
-
\`\`\`bash
|
|
2722
|
-
ffmpeg -i out/video-1080.mp4 -vf scale=1280:-2 -c:v libx264 -crf 26 \\
|
|
2723
|
-
-preset slow -movflags +faststart -c:a aac -b:a 96k web.mp4
|
|
2724
|
-
\`\`\`
|
|
2954
|
+
Use Better Design guidance for brand, story, hierarchy and review. Use the official Remotion skills for current APIs, Studio, media, captions and rendering.
|
|
2725
2955
|
|
|
2726
|
-
|
|
2727
|
-
can begin.
|
|
2956
|
+
Optional community libraries can supply a specialized pattern: RemotionUI, remocn and Remotion Bits. Ask before installing them, keep their provenance and license, and adapt their source to this video system instead of importing a demo style unchanged.
|
|
2728
2957
|
`
|
|
2729
2958
|
}
|
|
2730
2959
|
];
|
|
2731
2960
|
MOTION_SCAFFOLD_STEPS = [
|
|
2732
|
-
"Write every file in MOTION_SCAFFOLD_FILES into a `motion/` directory at the
|
|
2733
|
-
"
|
|
2734
|
-
"
|
|
2735
|
-
"
|
|
2736
|
-
"
|
|
2961
|
+
"Write every file in MOTION_SCAFFOLD_FILES into a `motion/` directory at the repository root. Keep it outside the app workspace because the first Remotion install downloads a large headless browser.",
|
|
2962
|
+
"Install and follow the official Remotion skills from https://www.remotion.dev/docs/ai/skills for current APIs and package versions.",
|
|
2963
|
+
"Replace the semantic tokens in src/design-system.ts with the product's existing design-system values.",
|
|
2964
|
+
"Replace every placeholder in src/brief.ts. When proof is present, keep its source URL and never invent a claim.",
|
|
2965
|
+
"Run bun install, bun run check, bun run dev, bun run captions, bun run render, bun run render:vertical and bun run package.",
|
|
2966
|
+
"Install FFmpeg before bun run review:frames. Inspect every scene boundary and ask the user to watch both final renders. A successful render is not visual approval."
|
|
2737
2967
|
].join("\n");
|
|
2738
2968
|
}
|
|
2739
2969
|
});
|
|
@@ -2831,7 +3061,7 @@ ${nudge}` }, ...rest]
|
|
|
2831
3061
|
}
|
|
2832
3062
|
async function runPrincipleTool(provider, area, label, toolName, args) {
|
|
2833
3063
|
const { topic, query, maxTokens } = args;
|
|
2834
|
-
const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" || area === "swiftui" ? "Guides" : "Principles";
|
|
3064
|
+
const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" || area === "swiftui" || area === "shopify" ? "Guides" : "Principles";
|
|
2835
3065
|
if (topic && query) {
|
|
2836
3066
|
return {
|
|
2837
3067
|
response: {
|
|
@@ -2913,7 +3143,11 @@ async function runGuidanceTool(provider, userId, clientName, area, label, toolNa
|
|
|
2913
3143
|
]);
|
|
2914
3144
|
return { ...run, response: appendReviewNudge(run.response, session2) };
|
|
2915
3145
|
}
|
|
2916
|
-
function registerPrincipleTools({ server, provider, track, userId }, {
|
|
3146
|
+
function registerPrincipleTools({ server, provider, track, userId }, {
|
|
3147
|
+
enableReactNativeGuide = true,
|
|
3148
|
+
enableThreeJsGuide = true,
|
|
3149
|
+
enableShopifyKit = true
|
|
3150
|
+
} = {}) {
|
|
2917
3151
|
const clientName = () => mcpClientName(server);
|
|
2918
3152
|
server.registerTool(
|
|
2919
3153
|
"get-ui-principle",
|
|
@@ -3116,33 +3350,90 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
|
|
|
3116
3350
|
}
|
|
3117
3351
|
);
|
|
3118
3352
|
}
|
|
3353
|
+
if (enableShopifyKit) {
|
|
3354
|
+
server.registerTool(
|
|
3355
|
+
"get-shopify-guide",
|
|
3356
|
+
{
|
|
3357
|
+
title: "Get Shopify Theme Guide",
|
|
3358
|
+
description: `Load Better Design's Shopify Liquid and Online Store 2.0 implementation guidance directly from the MCP. No separate agent skill is required.
|
|
3359
|
+
|
|
3360
|
+
Use this before creating or changing Liquid, JSON templates, sections, blocks, snippets, locale files, theme assets, product forms, cart behavior, or Shopify CLI workflows. The guides cover theme architecture, Liquid objects/tags/filters, merchant-editable schema, LiquidDoc, localization, accessibility, performance, and validation with Shopify Theme Check. They link to Shopify's primary documentation for version-sensitive API details.
|
|
3361
|
+
|
|
3362
|
+
You can either:
|
|
3363
|
+
1. Request a known topic: get-shopify-guide({ topic: "shopify-theme-architecture" })
|
|
3364
|
+
2. Use an alias: get-shopify-guide({ topic: "liquiddoc" })
|
|
3365
|
+
3. Search for the current task or error: get-shopify-guide({ query: "product form variant selector and money formatting" })
|
|
3366
|
+
4. Get the full topic index: get-shopify-guide({})
|
|
3367
|
+
|
|
3368
|
+
Prefer query search when you do not know the topic name. Run Shopify Theme Check after writing files. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
|
|
3369
|
+
inputSchema: z2.object({
|
|
3370
|
+
topic: z2.string().trim().min(1).max(120).optional().describe("Specific Shopify topic or alias, e.g. shopify-theme-architecture, shopify-liquid-language, section-schema, localization, or theme-check."),
|
|
3371
|
+
query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the Shopify theme task, API, or validation error."),
|
|
3372
|
+
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
3373
|
+
context: CONTEXT_ARG
|
|
3374
|
+
}),
|
|
3375
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
3376
|
+
},
|
|
3377
|
+
async ({ topic, query, maxTokens, context }) => {
|
|
3378
|
+
const start = Date.now();
|
|
3379
|
+
try {
|
|
3380
|
+
const { response, capture } = await runGuidanceTool(
|
|
3381
|
+
provider,
|
|
3382
|
+
userId,
|
|
3383
|
+
clientName(),
|
|
3384
|
+
"shopify",
|
|
3385
|
+
"Shopify",
|
|
3386
|
+
"get-shopify-guide",
|
|
3387
|
+
{ topic, query, maxTokens }
|
|
3388
|
+
);
|
|
3389
|
+
return trackSearch(
|
|
3390
|
+
track,
|
|
3391
|
+
"get-shopify-guide",
|
|
3392
|
+
{ ...capture, context },
|
|
3393
|
+
start,
|
|
3394
|
+
response
|
|
3395
|
+
);
|
|
3396
|
+
} catch (error) {
|
|
3397
|
+
return trackSearch(
|
|
3398
|
+
track,
|
|
3399
|
+
"get-shopify-guide",
|
|
3400
|
+
{ query, topic, topResults: [], context },
|
|
3401
|
+
start,
|
|
3402
|
+
{ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
|
|
3403
|
+
true,
|
|
3404
|
+
error instanceof Error ? error.message : String(error)
|
|
3405
|
+
);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3119
3410
|
server.registerTool(
|
|
3120
3411
|
"get-motion-guide",
|
|
3121
3412
|
{
|
|
3122
3413
|
title: "Get Motion Guide",
|
|
3123
|
-
description: `Load guidance for building a product demo or promo video in code.
|
|
3414
|
+
description: `Load Better Design's video design-system guidance for building a product demo or promo video in code.
|
|
3124
3415
|
|
|
3125
|
-
Use this before writing a Remotion composition, and while debugging one. It covers
|
|
3416
|
+
Use this before writing a Remotion composition, and while debugging one. It covers the product-demo brief, brand and motion tokens, responsive format profiles, product interactions, captions and accessibility, story and rhythm, layout traps, rendering, and visual review.
|
|
3126
3417
|
|
|
3127
|
-
|
|
3418
|
+
Start with scaffold: true. It returns a complete brand-aware Remotion project with landscape and vertical compositions, typed Studio props, a shared timeline, product-interaction scene, sourced proof, captions and an optional generated music bed.
|
|
3128
3419
|
|
|
3129
|
-
|
|
3420
|
+
Install and follow the twelve official Remotion skills from https://www.remotion.dev/docs/ai/skills for current APIs. Better Design owns brand, hierarchy, story, pacing and review. RemotionUI, remocn and Remotion Bits are optional, source-labelled pattern libraries; ask before installing community code and adapt it to the active video tokens.
|
|
3130
3421
|
|
|
3131
|
-
This tool does not render. Rendering needs Remotion and a headless
|
|
3422
|
+
This tool does not render. Rendering needs Remotion and a large headless-browser download in the user's own project. Write the files into their repo and let them run it.
|
|
3132
3423
|
|
|
3133
3424
|
This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior. For in-app animation whose timing the user should tune by eye (durations, springs, stagger), call get-jig-guide and mount its tuning panel instead of guessing values.
|
|
3134
3425
|
|
|
3135
3426
|
You can either:
|
|
3136
3427
|
1. Get the starter project: get-motion-guide({ scaffold: true })
|
|
3137
|
-
2. Request a known topic: get-motion-guide({ topic: "video-
|
|
3138
|
-
3. Use an alias: get-motion-guide({ topic: "
|
|
3139
|
-
4. Search for the current task or problem: get-motion-guide({ query: "my
|
|
3428
|
+
2. Request a known topic: get-motion-guide({ topic: "video-design-system" })
|
|
3429
|
+
3. Use an alias: get-motion-guide({ topic: "product demo structure" })
|
|
3430
|
+
4. Search for the current task or problem: get-motion-guide({ query: "reflow my product demo for vertical video" })
|
|
3140
3431
|
5. Get the full topic index: get-motion-guide({})
|
|
3141
3432
|
|
|
3142
3433
|
Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
|
|
3143
3434
|
inputSchema: z2.object({
|
|
3144
|
-
scaffold: z2.boolean().optional().describe("Return a complete Remotion project
|
|
3145
|
-
topic: z2.string().trim().min(1).max(120).optional().describe("Specific motion topic or alias, e.g. video-
|
|
3435
|
+
scaffold: z2.boolean().optional().describe("Return a complete brand-aware Remotion product-demo project, as files to write. Start here when building a video."),
|
|
3436
|
+
topic: z2.string().trim().min(1).max(120).optional().describe("Specific motion topic or alias, e.g. video-design-system, product-demo-brief, formats-and-safe-areas, product-interactions, captions-and-accessibility, remotion-resources, verifying-a-render."),
|
|
3146
3437
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the video or animation task or problem."),
|
|
3147
3438
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
3148
3439
|
context: CONTEXT_ARG
|
|
@@ -3165,7 +3456,7 @@ ${file.contents}`
|
|
|
3165
3456
|
content: [
|
|
3166
3457
|
{
|
|
3167
3458
|
type: "text",
|
|
3168
|
-
text: `A Remotion project
|
|
3459
|
+
text: `A brand-aware Remotion product-demo project. Write these files, then follow the steps.
|
|
3169
3460
|
|
|
3170
3461
|
${MOTION_SCAFFOLD_STEPS}
|
|
3171
3462
|
|
|
@@ -5467,7 +5758,7 @@ function insert(tree, path5, token) {
|
|
|
5467
5758
|
const promoted = newGroup({ [LEAF_KEY]: next });
|
|
5468
5759
|
node[segment] = promoted;
|
|
5469
5760
|
node = promoted;
|
|
5470
|
-
} else if (next
|
|
5761
|
+
} else if (!isObjectLike(next)) {
|
|
5471
5762
|
const created = newGroup();
|
|
5472
5763
|
node[segment] = created;
|
|
5473
5764
|
node = created;
|
|
@@ -5477,7 +5768,7 @@ function insert(tree, path5, token) {
|
|
|
5477
5768
|
}
|
|
5478
5769
|
const last = path5[path5.length - 1];
|
|
5479
5770
|
const existing = node[last];
|
|
5480
|
-
if (existing
|
|
5771
|
+
if (isObjectLike(existing) && !isToken(existing)) {
|
|
5481
5772
|
existing[LEAF_KEY] = token;
|
|
5482
5773
|
return;
|
|
5483
5774
|
}
|
|
@@ -5499,7 +5790,7 @@ function tokensCssToDtcg(css, name) {
|
|
|
5499
5790
|
insert(doc, [key, ...groupPath(rawName)], dtcgToken(rawName, value));
|
|
5500
5791
|
}
|
|
5501
5792
|
const darkGroup = doc[key];
|
|
5502
|
-
if (
|
|
5793
|
+
if (isObjectLike(darkGroup) && !isToken(darkGroup)) {
|
|
5503
5794
|
darkGroup.$description = "Dark mode values.";
|
|
5504
5795
|
darkGroup.$extensions = { [MODE_EXTENSION]: "dark" };
|
|
5505
5796
|
}
|
|
@@ -5514,6 +5805,7 @@ var init_dtcg = __esm({
|
|
|
5514
5805
|
"../shared/src/mcp/dtcg.ts"() {
|
|
5515
5806
|
"use strict";
|
|
5516
5807
|
init_color_to_oklch();
|
|
5808
|
+
init_guards();
|
|
5517
5809
|
MODE_EXTENSION = "com.better-design.mode";
|
|
5518
5810
|
ZERO_PX = { value: 0, unit: "px" };
|
|
5519
5811
|
LEAF_KEY = "DEFAULT";
|
|
@@ -6623,6 +6915,13 @@ body :where(.card,.card__inner,.product__media,.cart-item__image-container){bord
|
|
|
6623
6915
|
{ path: "templates/collection.ds.json", content: TEMPLATE_COLLECTION_JSON }
|
|
6624
6916
|
];
|
|
6625
6917
|
SHOPIFY_KIT_CONDUCT_MD = [
|
|
6918
|
+
"## Shopify implementation guidance (mandatory)",
|
|
6919
|
+
"Use Better Design's `get-shopify-guide` MCP tool for Liquid and Online Store 2.0 guidance. The guidance is served directly by this MCP; do not install a separate agent skill.",
|
|
6920
|
+
"- Before changing any returned `.liquid`, JSON template, schema, locale, or asset file, call `get-shopify-guide` with the exact task or unfamiliar Shopify object, tag, filter, or validation error. Do not rely on memory for Shopify APIs.",
|
|
6921
|
+
'- Start with `get-shopify-guide({ topic: "shopify-workflow-validation" })` for the safe theme workflow. Use a query for focused retrieval and follow the linked Shopify primary reference when an API is version-sensitive.',
|
|
6922
|
+
"- After writing files, run `shopify theme check --path <absolute-theme-path>`. Fix every reported error, query `get-shopify-guide` with unfamiliar errors, then run Theme Check again.",
|
|
6923
|
+
"- Install the Better Design files verbatim first. Use the guide to verify or make requested Shopify-specific changes; do not replace this kit's tokens, `ds-*` classes, or storefront composition.",
|
|
6924
|
+
"",
|
|
6626
6925
|
"## Install into the merchant's theme (mandatory order)",
|
|
6627
6926
|
"1. Duplicate the merchant's theme and work only in the unpublished copy. This full-store kit replaces the default header/footer groups and primary storefront templates; never install it directly on the live theme.",
|
|
6628
6927
|
"2. Write every returned file to its exact path. That includes the CSS asset, snippets, Liquid sections, `sections/header-group.json`, `sections/footer-group.json`, and the primary `templates/*.json` files. Copy contents VERBATIM; retyped Liquid silently drops this system's material.",
|
|
@@ -9131,12 +9430,16 @@ var init_shopify_primitives = __esm({
|
|
|
9131
9430
|
}
|
|
9132
9431
|
});
|
|
9133
9432
|
|
|
9134
|
-
// ../shared/src/mcp/
|
|
9433
|
+
// ../shared/src/mcp/catalog-tokens.ts
|
|
9135
9434
|
import { z as z6 } from "zod";
|
|
9136
|
-
function
|
|
9137
|
-
|
|
9138
|
-
|
|
9139
|
-
|
|
9435
|
+
function inventoryFromRegistryDependencies(dependencies) {
|
|
9436
|
+
const inventory = { components: [], foundations: [] };
|
|
9437
|
+
for (const dependency of dependencies) {
|
|
9438
|
+
const name = dependency.split("/").at(-1)?.replace(/\.json$/, "");
|
|
9439
|
+
if (!name) continue;
|
|
9440
|
+
(FOUNDATION_ENTRY.test(name) ? inventory.foundations : inventory.components).push(name);
|
|
9441
|
+
}
|
|
9442
|
+
return inventory;
|
|
9140
9443
|
}
|
|
9141
9444
|
function tokensCssFromRegistry(json2) {
|
|
9142
9445
|
const files = json2.files ?? [];
|
|
@@ -9153,6 +9456,76 @@ function tokensCssFromRegistry(json2) {
|
|
|
9153
9456
|
componentDepthCss(files)
|
|
9154
9457
|
].filter(Boolean).join("\n");
|
|
9155
9458
|
}
|
|
9459
|
+
async function fetchCatalog(designSystemId, options = {}) {
|
|
9460
|
+
try {
|
|
9461
|
+
const [globalsRes, allRes, ...componentResults] = await Promise.all([
|
|
9462
|
+
fetch(`${REGISTRY_BASE_URL}/${designSystemId}/globals.json`),
|
|
9463
|
+
options.includeInventory ? fetch(`${REGISTRY_BASE_URL}/${designSystemId}/all.json`).catch((err) => {
|
|
9464
|
+
console.warn(`catalog inventory fetch failed: ${designSystemId}`, err);
|
|
9465
|
+
return null;
|
|
9466
|
+
}) : null,
|
|
9467
|
+
...CATALOG_KIT_COMPONENTS.map(
|
|
9468
|
+
(c) => fetch(`${REGISTRY_BASE_URL}/${designSystemId}/${c}.json`).catch((err) => {
|
|
9469
|
+
console.warn(`catalog component fetch failed: ${designSystemId}/${c}`, err);
|
|
9470
|
+
return null;
|
|
9471
|
+
})
|
|
9472
|
+
)
|
|
9473
|
+
]);
|
|
9474
|
+
if (!globalsRes.ok) return { tokensCss: "", files: [], inventory: { components: [], foundations: [] } };
|
|
9475
|
+
const globals = REGISTRY_ITEM_SCHEMA.safeParse(await globalsRes.json());
|
|
9476
|
+
if (!globals.success) return { tokensCss: "", files: [], inventory: { components: [], foundations: [] } };
|
|
9477
|
+
const all = allRes?.ok ? REGISTRY_ALL_SCHEMA.safeParse(await allRes.json().catch(() => null)) : null;
|
|
9478
|
+
const inventory = all?.success ? inventoryFromRegistryDependencies(all.data.registryDependencies) : { components: [], foundations: [] };
|
|
9479
|
+
const files = [...globals.data.files ?? []];
|
|
9480
|
+
for (const [i, res] of componentResults.entries()) {
|
|
9481
|
+
if (!res) continue;
|
|
9482
|
+
if (!res.ok) {
|
|
9483
|
+
console.warn(
|
|
9484
|
+
`catalog component unavailable: ${designSystemId}/${CATALOG_KIT_COMPONENTS[i]} (HTTP ${res.status})`
|
|
9485
|
+
);
|
|
9486
|
+
continue;
|
|
9487
|
+
}
|
|
9488
|
+
const item = REGISTRY_ITEM_SCHEMA.safeParse(await res.json().catch(() => null));
|
|
9489
|
+
if (item.success && item.data.files) files.push(...item.data.files);
|
|
9490
|
+
}
|
|
9491
|
+
return { tokensCss: tokensCssFromRegistry({ cssVars: globals.data.cssVars, files }), files, inventory };
|
|
9492
|
+
} catch (error) {
|
|
9493
|
+
console.warn(`catalog registry fetch failed for ${designSystemId}`, error);
|
|
9494
|
+
return { tokensCss: "", files: [], inventory: { components: [], foundations: [] } };
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
async function fetchCatalogTokensCss(designSystemId) {
|
|
9498
|
+
return (await fetchCatalog(designSystemId)).tokensCss;
|
|
9499
|
+
}
|
|
9500
|
+
var REGISTRY_ITEM_SCHEMA, REGISTRY_ALL_SCHEMA, FOUNDATION_ENTRY, CATALOG_KIT_COMPONENTS;
|
|
9501
|
+
var init_catalog_tokens = __esm({
|
|
9502
|
+
"../shared/src/mcp/catalog-tokens.ts"() {
|
|
9503
|
+
"use strict";
|
|
9504
|
+
init_preview_html();
|
|
9505
|
+
init_shared();
|
|
9506
|
+
REGISTRY_ITEM_SCHEMA = z6.object({
|
|
9507
|
+
cssVars: z6.object({
|
|
9508
|
+
light: z6.record(z6.string(), z6.string()).optional(),
|
|
9509
|
+
dark: z6.record(z6.string(), z6.string()).optional(),
|
|
9510
|
+
theme: z6.record(z6.string(), z6.string()).optional()
|
|
9511
|
+
}).optional(),
|
|
9512
|
+
files: z6.array(z6.object({ path: z6.string().optional(), content: z6.string().optional() })).optional()
|
|
9513
|
+
});
|
|
9514
|
+
REGISTRY_ALL_SCHEMA = z6.object({
|
|
9515
|
+
registryDependencies: z6.array(z6.string())
|
|
9516
|
+
});
|
|
9517
|
+
FOUNDATION_ENTRY = /^(?:globals|utils|use-.+|.+-utils)$/;
|
|
9518
|
+
CATALOG_KIT_COMPONENTS = ["button", "input", "card", "badge", "switch"];
|
|
9519
|
+
}
|
|
9520
|
+
});
|
|
9521
|
+
|
|
9522
|
+
// ../shared/src/mcp/tools/design-system-tools.ts
|
|
9523
|
+
import { z as z7 } from "zod";
|
|
9524
|
+
function installCommandWithInit(installCommand) {
|
|
9525
|
+
return `${SHADCN_INIT_IF_MISSING}
|
|
9526
|
+
|
|
9527
|
+
${installCommand}`;
|
|
9528
|
+
}
|
|
9156
9529
|
function authHeaders(authContext) {
|
|
9157
9530
|
if (!authContext) return {};
|
|
9158
9531
|
const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
|
|
@@ -9253,41 +9626,6 @@ function completedShopifyKitResult(name, viewUrl, source, primitives) {
|
|
|
9253
9626
|
)
|
|
9254
9627
|
};
|
|
9255
9628
|
}
|
|
9256
|
-
async function fetchCatalog(designSystemId) {
|
|
9257
|
-
try {
|
|
9258
|
-
const [globalsRes, ...componentResults] = await Promise.all([
|
|
9259
|
-
fetch(`${REGISTRY_BASE_URL}/${designSystemId}/globals.json`),
|
|
9260
|
-
...CATALOG_KIT_COMPONENTS.map(
|
|
9261
|
-
(c) => fetch(`${REGISTRY_BASE_URL}/${designSystemId}/${c}.json`).catch((err) => {
|
|
9262
|
-
console.warn(`catalog component fetch failed: ${designSystemId}/${c}`, err);
|
|
9263
|
-
return null;
|
|
9264
|
-
})
|
|
9265
|
-
)
|
|
9266
|
-
]);
|
|
9267
|
-
if (!globalsRes.ok) return { tokensCss: "", files: [] };
|
|
9268
|
-
const globals = REGISTRY_ITEM_SCHEMA.safeParse(await globalsRes.json());
|
|
9269
|
-
if (!globals.success) return { tokensCss: "", files: [] };
|
|
9270
|
-
const files = [...globals.data.files ?? []];
|
|
9271
|
-
for (const [i, res] of componentResults.entries()) {
|
|
9272
|
-
if (!res) continue;
|
|
9273
|
-
if (!res.ok) {
|
|
9274
|
-
console.warn(
|
|
9275
|
-
`catalog component unavailable: ${designSystemId}/${CATALOG_KIT_COMPONENTS[i]} (HTTP ${res.status})`
|
|
9276
|
-
);
|
|
9277
|
-
continue;
|
|
9278
|
-
}
|
|
9279
|
-
const item = REGISTRY_ITEM_SCHEMA.safeParse(await res.json().catch(() => null));
|
|
9280
|
-
if (item.success && item.data.files) files.push(...item.data.files);
|
|
9281
|
-
}
|
|
9282
|
-
return { tokensCss: tokensCssFromRegistry({ cssVars: globals.data.cssVars, files }), files };
|
|
9283
|
-
} catch (error) {
|
|
9284
|
-
console.warn(`catalog registry fetch failed for ${designSystemId}`, error);
|
|
9285
|
-
return { tokensCss: "", files: [] };
|
|
9286
|
-
}
|
|
9287
|
-
}
|
|
9288
|
-
async function fetchCatalogTokensCss(designSystemId) {
|
|
9289
|
-
return (await fetchCatalog(designSystemId)).tokensCss;
|
|
9290
|
-
}
|
|
9291
9629
|
async function fetchBaseVariantNames(designSystemId) {
|
|
9292
9630
|
try {
|
|
9293
9631
|
const res = await fetch(`${REGISTRY_BASE_URL}/${designSystemId}/index.json`, {
|
|
@@ -9309,8 +9647,8 @@ function registerDesignSystemTools({
|
|
|
9309
9647
|
authContext,
|
|
9310
9648
|
apiFetch = globalThis.fetch
|
|
9311
9649
|
}, { enableShopifyKit = true } = {}) {
|
|
9312
|
-
const searchInputSchema =
|
|
9313
|
-
query:
|
|
9650
|
+
const searchInputSchema = z7.object({
|
|
9651
|
+
query: z7.string().describe(
|
|
9314
9652
|
"The user's app, UI, industry, or visual direction. Examples: 'World Cup betting app for friends', 'warm editorial ecommerce', or 'professional fintech dashboard'."
|
|
9315
9653
|
)
|
|
9316
9654
|
});
|
|
@@ -9640,7 +9978,7 @@ ${fence3}` }],
|
|
|
9640
9978
|
title: "Get a Design System Build Kit",
|
|
9641
9979
|
description: `Return the correct build kit for a design system through one capability-based entry point.
|
|
9642
9980
|
|
|
9643
|
-
Use \`target: "react"\` whenever the host can write files and run a terminal. It returns the shadcn install command and real React components. Use \`target: "html"\` only for pure inline canvases or artifacts where npm and a build step are unavailable; it returns the framework-free token and component CSS kit.${enableShopifyKit ? ` Use \`target: "shopify"\` when the user is styling a Shopify store or theme; it returns ready-to-write Liquid theme files (a CSS asset with this system's real tokens, snippets, and theme editor sections)
|
|
9981
|
+
Use \`target: "react"\` whenever the host can write files and run a terminal. It returns the shadcn install command and real React components. Use \`target: "html"\` only for pure inline canvases or artifacts where npm and a build step are unavailable; it returns the framework-free token and component CSS kit.${enableShopifyKit ? ` Use \`target: "shopify"\` when the user is styling a Shopify store or theme; it returns ready-to-write Liquid theme files (a CSS asset with this system's real tokens, snippets, and theme editor sections) plus the mandatory handoff to Better Design's \`get-shopify-guide\` MCP tool for documentation and validation.` : ""}
|
|
9644
9982
|
|
|
9645
9983
|
For \`target: "html"\`, the returned class contract is mandatory, including inside a host-provided visualization workflow: paste the kit CSS verbatim, keep \`class="ds"\` on the artifact root, and keep \`ds-*\` component classes on interactive controls. App-specific classes may sit alongside them for layout only; never rename or replace the \`ds-*\` classes. Before presenting, inspect the artifact source and revise it if either \`class="ds"\` or \`ds-\` is absent. Put hover-only CSS inside \`@media (hover: hover) and (pointer: fine)\` and never let hover move a control or override its selected/pressed styling.
|
|
9646
9984
|
|
|
@@ -9664,15 +10002,15 @@ existing setup; otherwise use the default Base UI. Both flavors install to the
|
|
|
9664
10002
|
same components/ui/ paths.
|
|
9665
10003
|
|
|
9666
10004
|
${BUILD_TARGET_RULE}`,
|
|
9667
|
-
inputSchema:
|
|
10005
|
+
inputSchema: z7.object({
|
|
9668
10006
|
// Off-pilot callers must not see the target at all, not merely be refused it.
|
|
9669
|
-
target:
|
|
10007
|
+
target: z7.enum(
|
|
9670
10008
|
enableShopifyKit ? ["react", "html", "shopify", "dtcg"] : ["react", "html", "dtcg"]
|
|
9671
10009
|
).describe(`Required build target. Use "react" for any host with filesystem + terminal; use "html" only for inline chat canvases/artifacts.${enableShopifyKit ? ' Use "shopify" for Shopify stores and themes.' : ""} Use "dtcg" to hand the tokens to a design tool: it returns a W3C Design Tokens document for Style Dictionary, Tokens Studio, Terrazzo, or a Figma plugin that creates variables.`),
|
|
9672
|
-
designSystemId:
|
|
9673
|
-
projectId:
|
|
9674
|
-
component:
|
|
9675
|
-
library:
|
|
10010
|
+
designSystemId: z7.string().min(1).optional().describe(`Catalog design system ID from find-design-system. Required for target react; for target${enableShopifyKit ? "s html, dtcg and shopify" : "s html and dtcg"} pass this or projectId.`),
|
|
10011
|
+
projectId: z7.string().min(1).optional().describe(`Generated project ID from create-design-system / get-design-system-status. Supported with target${enableShopifyKit ? 's "html", "dtcg" and "shopify"' : 's "html" and "dtcg"'}.`),
|
|
10012
|
+
component: z7.union([z7.string(), z7.array(z7.string())]).optional().describe("React target only. Pass only when the user asks to inspect specific component source code. Omit to scaffold the entire design system."),
|
|
10013
|
+
library: z7.enum(["radix", "base-ui"]).optional().describe('React target only, default "base-ui". Pass "radix" when the project already uses Radix primitives.'),
|
|
9676
10014
|
context: CONTEXT_ARG
|
|
9677
10015
|
}),
|
|
9678
10016
|
outputSchema: buildKitOutputSchema(enableShopifyKit),
|
|
@@ -9904,10 +10242,10 @@ How it works:
|
|
|
9904
10242
|
3. Call \`get-design-system-status\` with the returned project id (wait ~60 seconds first) to get the live preview URL and the final \`npx shadcn add <registry-url>\` install command once it completes.
|
|
9905
10243
|
|
|
9906
10244
|
Share the project URL with the user right away so they can watch the generation live.`,
|
|
9907
|
-
inputSchema:
|
|
9908
|
-
prompt:
|
|
9909
|
-
baseDesignSystemId:
|
|
9910
|
-
brandCss:
|
|
10245
|
+
inputSchema: z7.object({
|
|
10246
|
+
prompt: z7.string().min(1).max(1e4).describe("Describe the design system the user wants. E.g. 'A warm, minimal design system for a personal finance app, soft pastels, rounded corners' or 'A brutalist developer tool aesthetic with sharp edges and monospace type'."),
|
|
10247
|
+
baseDesignSystemId: z7.string().optional().describe("Optional. The catalog id the USER picked from the `find-design-system` matches you showed them, used to bias generation toward that aesthetic (e.g. a theme tweak on top of Linear). Leave empty only when the user chose fully custom."),
|
|
10248
|
+
brandCss: z7.string().max(5e4).optional().describe("Optional. The user's extracted brand token sheet, verbatim: the fenced css block from extract-design-system / extract-from-url (`:root { --background: ...; }` plus optional `.dark { ... }`). When set, generation builds FROM this brand instead of a catalog look-alike, and baseDesignSystemId is ignored (a catalog base would leak its identity past the brand tokens).")
|
|
9911
10249
|
}),
|
|
9912
10250
|
annotations: {
|
|
9913
10251
|
readOnlyHint: false,
|
|
@@ -9929,8 +10267,8 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
9929
10267
|
headers: requestHeaders,
|
|
9930
10268
|
body: JSON.stringify({
|
|
9931
10269
|
value: prompt,
|
|
9932
|
-
|
|
9933
|
-
...brandCss
|
|
10270
|
+
designSystemId: baseDesignSystemId ? baseDesignSystemId : void 0,
|
|
10271
|
+
...brandCss && { brandCss }
|
|
9934
10272
|
})
|
|
9935
10273
|
});
|
|
9936
10274
|
if (!startRes.ok) {
|
|
@@ -10030,7 +10368,7 @@ npx shadcn@latest add ${registryUrl}
|
|
|
10030
10368
|
name: dsName,
|
|
10031
10369
|
viewUrl: projectUrl,
|
|
10032
10370
|
previewUrl,
|
|
10033
|
-
...shotUrl
|
|
10371
|
+
...shotUrl && { shotUrl },
|
|
10034
10372
|
installCommand: `npx shadcn@latest add ${registryUrl}`,
|
|
10035
10373
|
tokensCss
|
|
10036
10374
|
}
|
|
@@ -10059,8 +10397,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
10059
10397
|
const statusConfig = {
|
|
10060
10398
|
title: "Get Design System Generation Status",
|
|
10061
10399
|
description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready. Once ready, ask the user what's next (icon library, install, or build) instead of stopping.`,
|
|
10062
|
-
inputSchema:
|
|
10063
|
-
projectId:
|
|
10400
|
+
inputSchema: z7.object({
|
|
10401
|
+
projectId: z7.string().min(1).describe("The project id returned by create-design-system."),
|
|
10064
10402
|
context: CONTEXT_ARG
|
|
10065
10403
|
}),
|
|
10066
10404
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -10083,8 +10421,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
10083
10421
|
{
|
|
10084
10422
|
title: "Preview a Design System",
|
|
10085
10423
|
description: `Show the user a visual preview of a design system from the catalog (use the id from 'find-design-system'). Returns a token-true HTML preview: palette, typography, buttons, card, input, badge, plus a link to the full live showcase. Use this when the user wants to SEE a design system before installing it.`,
|
|
10086
|
-
inputSchema:
|
|
10087
|
-
designSystemId:
|
|
10424
|
+
inputSchema: z7.object({
|
|
10425
|
+
designSystemId: z7.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'."),
|
|
10088
10426
|
context: CONTEXT_ARG
|
|
10089
10427
|
}),
|
|
10090
10428
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -10142,7 +10480,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
10142
10480
|
})
|
|
10143
10481
|
);
|
|
10144
10482
|
}
|
|
10145
|
-
var
|
|
10483
|
+
var SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, DS_INDEX_SCHEMA;
|
|
10146
10484
|
var init_design_system_tools = __esm({
|
|
10147
10485
|
"../shared/src/mcp/tools/design-system-tools.ts"() {
|
|
10148
10486
|
"use strict";
|
|
@@ -10155,19 +10493,11 @@ var init_design_system_tools = __esm({
|
|
|
10155
10493
|
init_preview_html();
|
|
10156
10494
|
init_shared();
|
|
10157
10495
|
init_guards();
|
|
10158
|
-
|
|
10159
|
-
cssVars: z6.object({
|
|
10160
|
-
light: z6.record(z6.string(), z6.string()).optional(),
|
|
10161
|
-
dark: z6.record(z6.string(), z6.string()).optional(),
|
|
10162
|
-
theme: z6.record(z6.string(), z6.string()).optional()
|
|
10163
|
-
}).optional(),
|
|
10164
|
-
files: z6.array(z6.object({ path: z6.string().optional(), content: z6.string().optional() })).optional()
|
|
10165
|
-
});
|
|
10496
|
+
init_catalog_tokens();
|
|
10166
10497
|
SHADCN_INIT_IF_MISSING = "[ -f components.json ] || npx shadcn@latest init -d -y";
|
|
10167
10498
|
SAFE_COMPONENT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
baseComponents: z6.array(z6.object({ name: z6.string() })).optional()
|
|
10499
|
+
DS_INDEX_SCHEMA = z7.object({
|
|
10500
|
+
baseComponents: z7.array(z7.object({ name: z7.string() })).optional()
|
|
10171
10501
|
});
|
|
10172
10502
|
}
|
|
10173
10503
|
});
|
|
@@ -18186,7 +18516,7 @@ var init_drizzle_orm = __esm({
|
|
|
18186
18516
|
});
|
|
18187
18517
|
|
|
18188
18518
|
// ../../node_modules/drizzle-zod/index.mjs
|
|
18189
|
-
import { z as
|
|
18519
|
+
import { z as z8 } from "zod";
|
|
18190
18520
|
function isColumnType(column, columnTypes) {
|
|
18191
18521
|
return columnTypes.includes(column.columnType);
|
|
18192
18522
|
}
|
|
@@ -18194,7 +18524,7 @@ function isWithEnum(column) {
|
|
|
18194
18524
|
return "enumValues" in column && Array.isArray(column.enumValues) && column.enumValues.length > 0;
|
|
18195
18525
|
}
|
|
18196
18526
|
function columnToSchema(column, factory) {
|
|
18197
|
-
const z$1 = factory?.zodInstance ??
|
|
18527
|
+
const z$1 = factory?.zodInstance ?? z8;
|
|
18198
18528
|
const coerce = factory?.coerce ?? {};
|
|
18199
18529
|
let schema;
|
|
18200
18530
|
if (isWithEnum(column)) {
|
|
@@ -18244,7 +18574,7 @@ function columnToSchema(column, factory) {
|
|
|
18244
18574
|
}
|
|
18245
18575
|
return schema;
|
|
18246
18576
|
}
|
|
18247
|
-
function numberColumnToSchema(column,
|
|
18577
|
+
function numberColumnToSchema(column, z16, coerce) {
|
|
18248
18578
|
let unsigned = column.getSQLType().includes("unsigned");
|
|
18249
18579
|
let min;
|
|
18250
18580
|
let max;
|
|
@@ -18312,20 +18642,20 @@ function numberColumnToSchema(column, z15, coerce) {
|
|
|
18312
18642
|
min = Number.MIN_SAFE_INTEGER;
|
|
18313
18643
|
max = Number.MAX_SAFE_INTEGER;
|
|
18314
18644
|
}
|
|
18315
|
-
let schema = coerce === true || coerce?.number ?
|
|
18645
|
+
let schema = coerce === true || coerce?.number ? z16.coerce.number() : z16.number();
|
|
18316
18646
|
schema = schema.min(min).max(max);
|
|
18317
18647
|
return integer2 ? schema.int() : schema;
|
|
18318
18648
|
}
|
|
18319
|
-
function bigintColumnToSchema(column,
|
|
18649
|
+
function bigintColumnToSchema(column, z16, coerce) {
|
|
18320
18650
|
const unsigned = column.getSQLType().includes("unsigned");
|
|
18321
18651
|
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
|
|
18322
18652
|
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
|
|
18323
|
-
const schema = coerce === true || coerce?.bigint ?
|
|
18653
|
+
const schema = coerce === true || coerce?.bigint ? z16.coerce.bigint() : z16.bigint();
|
|
18324
18654
|
return schema.min(min).max(max);
|
|
18325
18655
|
}
|
|
18326
|
-
function stringColumnToSchema(column,
|
|
18656
|
+
function stringColumnToSchema(column, z16, coerce) {
|
|
18327
18657
|
if (isColumnType(column, ["PgUUID"])) {
|
|
18328
|
-
return
|
|
18658
|
+
return z16.string().uuid();
|
|
18329
18659
|
}
|
|
18330
18660
|
let max;
|
|
18331
18661
|
let regex;
|
|
@@ -18357,7 +18687,7 @@ function stringColumnToSchema(column, z15, coerce) {
|
|
|
18357
18687
|
regex = /^[01]+$/;
|
|
18358
18688
|
max = column.dimensions;
|
|
18359
18689
|
}
|
|
18360
|
-
let schema = coerce === true || coerce?.string ?
|
|
18690
|
+
let schema = coerce === true || coerce?.string ? z16.coerce.string() : z16.string();
|
|
18361
18691
|
schema = regex ? schema.regex(regex) : schema;
|
|
18362
18692
|
return max && fixed ? schema.length(max) : max ? schema.max(max) : schema;
|
|
18363
18693
|
}
|
|
@@ -18378,7 +18708,7 @@ function handleColumns(columns, refinements, conditions, factory) {
|
|
|
18378
18708
|
continue;
|
|
18379
18709
|
}
|
|
18380
18710
|
const column = is(selected, Column) ? selected : void 0;
|
|
18381
|
-
const schema = column ? columnToSchema(column, factory) :
|
|
18711
|
+
const schema = column ? columnToSchema(column, factory) : z8.any();
|
|
18382
18712
|
const refined = typeof refinement === "function" ? refinement(schema) : schema;
|
|
18383
18713
|
if (conditions.never(column)) {
|
|
18384
18714
|
continue;
|
|
@@ -18394,10 +18724,10 @@ function handleColumns(columns, refinements, conditions, factory) {
|
|
|
18394
18724
|
}
|
|
18395
18725
|
}
|
|
18396
18726
|
}
|
|
18397
|
-
return
|
|
18727
|
+
return z8.object(columnSchemas);
|
|
18398
18728
|
}
|
|
18399
18729
|
function handleEnum(enum_, factory) {
|
|
18400
|
-
const zod = factory?.zodInstance ??
|
|
18730
|
+
const zod = factory?.zodInstance ?? z8;
|
|
18401
18731
|
return zod.enum(enum_.enumValues);
|
|
18402
18732
|
}
|
|
18403
18733
|
var CONSTANTS, isPgEnum2, literalSchema, jsonSchema, bufferSchema, selectConditions, insertConditions, createSelectSchema, createInsertSchema;
|
|
@@ -18426,9 +18756,9 @@ var init_drizzle_zod = __esm({
|
|
|
18426
18756
|
INT64_UNSIGNED_MAX: 18446744073709551615n
|
|
18427
18757
|
};
|
|
18428
18758
|
isPgEnum2 = isWithEnum;
|
|
18429
|
-
literalSchema =
|
|
18430
|
-
jsonSchema =
|
|
18431
|
-
bufferSchema =
|
|
18759
|
+
literalSchema = z8.union([z8.string(), z8.number(), z8.boolean(), z8.null()]);
|
|
18760
|
+
jsonSchema = z8.union([literalSchema, z8.record(z8.any()), z8.array(z8.any())]);
|
|
18761
|
+
bufferSchema = z8.custom((v) => v instanceof Buffer);
|
|
18432
18762
|
selectConditions = {
|
|
18433
18763
|
never: () => false,
|
|
18434
18764
|
optional: () => false,
|
|
@@ -18454,8 +18784,8 @@ var init_drizzle_zod = __esm({
|
|
|
18454
18784
|
});
|
|
18455
18785
|
|
|
18456
18786
|
// ../shared/src/schema.ts
|
|
18457
|
-
import { z as
|
|
18458
|
-
var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, billingSubscription, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, projectsRelations, projectMemoryRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema;
|
|
18787
|
+
import { z as z9 } from "zod";
|
|
18788
|
+
var vector2, messageRoleEnum, messageTypeEnum, designSystemStatusEnum, onboardingPathValues, onboardingPathSchema, onboardingPathSqlValues, designConfigSchema, projectMemoryDataSchema, projects, messages, projectMemory, fragments, usage, scrapeBlock, resourceAccess, user, billingSubscription, featureAccess, session, account, verification, jwks, oauthClient, oauthRefreshToken, oauthAccessToken, oauthConsent, designSystems, designSystemComponents, foundationalDocs, apiKeys, emailCampaignSends, emailCampaignSteps, projectEvents, iconLibraries, dsSearchQueries, mcpToolCalls, mcpIssues, projectsRelations, projectMemoryRelations, messagesRelations, fragmentsRelations, designSystemsRelations, designSystemComponentsRelations, insertProjectSchema, selectProjectSchema, insertMessageSchema, selectMessageSchema, insertFragmentSchema, selectFragmentSchema, insertProjectMemorySchema, selectProjectMemorySchema, insertDesignSystemSchema, selectDesignSystemSchema, insertDesignSystemComponentSchema, selectDesignSystemComponentSchema, insertFoundationalDocSchema, selectFoundationalDocSchema, insertIconLibrarySchema, selectIconLibrarySchema, insertApiKeySchema, selectApiKeySchema, insertUserSchema, selectUserSchema, insertEmailCampaignSendSchema, selectEmailCampaignSendSchema, insertEmailCampaignStepSchema, selectEmailCampaignStepSchema, pluginAuthNonces;
|
|
18459
18789
|
var init_schema2 = __esm({
|
|
18460
18790
|
"../shared/src/schema.ts"() {
|
|
18461
18791
|
"use strict";
|
|
@@ -18489,42 +18819,42 @@ var init_schema2 = __esm({
|
|
|
18489
18819
|
"skip",
|
|
18490
18820
|
"legacy"
|
|
18491
18821
|
];
|
|
18492
|
-
onboardingPathSchema =
|
|
18822
|
+
onboardingPathSchema = z9.enum(onboardingPathValues);
|
|
18493
18823
|
onboardingPathSqlValues = sql.raw(
|
|
18494
18824
|
onboardingPathValues.map((path5) => `'${path5}'`).join(", ")
|
|
18495
18825
|
);
|
|
18496
|
-
designConfigSchema =
|
|
18497
|
-
colors:
|
|
18498
|
-
primary:
|
|
18499
|
-
secondary:
|
|
18500
|
-
accent:
|
|
18501
|
-
muted:
|
|
18502
|
-
destructive:
|
|
18503
|
-
background:
|
|
18504
|
-
foreground:
|
|
18505
|
-
card:
|
|
18506
|
-
cardForeground:
|
|
18507
|
-
popover:
|
|
18508
|
-
popoverForeground:
|
|
18509
|
-
border:
|
|
18510
|
-
input:
|
|
18511
|
-
ring:
|
|
18826
|
+
designConfigSchema = z9.object({
|
|
18827
|
+
colors: z9.object({
|
|
18828
|
+
primary: z9.string(),
|
|
18829
|
+
secondary: z9.string(),
|
|
18830
|
+
accent: z9.string(),
|
|
18831
|
+
muted: z9.string(),
|
|
18832
|
+
destructive: z9.string(),
|
|
18833
|
+
background: z9.string(),
|
|
18834
|
+
foreground: z9.string(),
|
|
18835
|
+
card: z9.string(),
|
|
18836
|
+
cardForeground: z9.string(),
|
|
18837
|
+
popover: z9.string(),
|
|
18838
|
+
popoverForeground: z9.string(),
|
|
18839
|
+
border: z9.string(),
|
|
18840
|
+
input: z9.string(),
|
|
18841
|
+
ring: z9.string()
|
|
18512
18842
|
}).partial(),
|
|
18513
|
-
radius:
|
|
18514
|
-
font:
|
|
18515
|
-
sans:
|
|
18516
|
-
mono:
|
|
18843
|
+
radius: z9.string(),
|
|
18844
|
+
font: z9.object({
|
|
18845
|
+
sans: z9.string(),
|
|
18846
|
+
mono: z9.string()
|
|
18517
18847
|
}).partial(),
|
|
18518
|
-
icons:
|
|
18519
|
-
libraryId:
|
|
18520
|
-
variant:
|
|
18848
|
+
icons: z9.object({
|
|
18849
|
+
libraryId: z9.string(),
|
|
18850
|
+
variant: z9.string().nullable()
|
|
18521
18851
|
}).partial()
|
|
18522
18852
|
}).partial();
|
|
18523
|
-
projectMemoryDataSchema =
|
|
18524
|
-
lockedIn:
|
|
18525
|
-
rejected:
|
|
18526
|
-
tone:
|
|
18527
|
-
notes:
|
|
18853
|
+
projectMemoryDataSchema = z9.object({
|
|
18854
|
+
lockedIn: z9.array(z9.string()),
|
|
18855
|
+
rejected: z9.array(z9.string()),
|
|
18856
|
+
tone: z9.string().nullable(),
|
|
18857
|
+
notes: z9.string().nullable()
|
|
18528
18858
|
});
|
|
18529
18859
|
projects = pgTable("projects", {
|
|
18530
18860
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
@@ -19044,6 +19374,78 @@ var init_schema2 = __esm({
|
|
|
19044
19374
|
index("ds_search_queries_unjudged_idx").on(t.createdAt).where(sql`${t.evaluatedAt} is null and ${t.tool} = 'resolve-design-system'`)
|
|
19045
19375
|
]
|
|
19046
19376
|
);
|
|
19377
|
+
mcpToolCalls = pgTable(
|
|
19378
|
+
"mcp_tool_calls",
|
|
19379
|
+
{
|
|
19380
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
19381
|
+
// Trace id shared with the PostHog $ai_span for the same call, so a DB row
|
|
19382
|
+
// can be cross-referenced with its LLM-Analytics trace [BD-109].
|
|
19383
|
+
traceId: text("trace_id"),
|
|
19384
|
+
tool: text("tool").notNull(),
|
|
19385
|
+
status: text("status").$type().notNull(),
|
|
19386
|
+
errorMessage: text("error_message"),
|
|
19387
|
+
durationMs: integer("duration_ms"),
|
|
19388
|
+
// Sanitized serialized tool input (same string shape as the $ai_span
|
|
19389
|
+
// input_state), capped — enough for an analyst to reproduce the call.
|
|
19390
|
+
toolInput: text("tool_input"),
|
|
19391
|
+
outputPreview: text("output_preview"),
|
|
19392
|
+
// Agent-stated intent from the optional `context` tool arg [BD-188].
|
|
19393
|
+
intent: text("intent"),
|
|
19394
|
+
// Retrieval signal, present only for query-bearing tools: the query text and
|
|
19395
|
+
// how well the corpus answered it. topScore NULL + resultCount 0 on a
|
|
19396
|
+
// semantic tool = nothing matched (a coverage gap candidate).
|
|
19397
|
+
query: text("query"),
|
|
19398
|
+
topScore: integer("top_score"),
|
|
19399
|
+
resultCount: integer("result_count"),
|
|
19400
|
+
userId: text("user_id"),
|
|
19401
|
+
authKind: text("auth_kind").notNull(),
|
|
19402
|
+
// "mcp-remote" = hosted clients on /api/mcp; "stdio" = the npm client's
|
|
19403
|
+
// /api/internal/alert ping.
|
|
19404
|
+
source: text("source").notNull(),
|
|
19405
|
+
clientName: text("client_name"),
|
|
19406
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
19407
|
+
// Set once the issue-detection cron has considered this row. NULL = not yet
|
|
19408
|
+
// analyzed; the cron samples `WHERE analyzed_at IS NULL` so each row is
|
|
19409
|
+
// analyzed at most once (idempotent, bounded cost) [BD-485].
|
|
19410
|
+
analyzedAt: timestamp("analyzed_at", { withTimezone: true })
|
|
19411
|
+
},
|
|
19412
|
+
(t) => [
|
|
19413
|
+
index("mcp_tool_calls_created_at_idx").on(t.createdAt),
|
|
19414
|
+
index("mcp_tool_calls_tool_idx").on(t.tool),
|
|
19415
|
+
// Partial index serving the issue-detection cron's FIFO sample.
|
|
19416
|
+
index("mcp_tool_calls_unanalyzed_idx").on(t.createdAt).where(sql`${t.analyzedAt} is null`)
|
|
19417
|
+
]
|
|
19418
|
+
);
|
|
19419
|
+
mcpIssues = pgTable(
|
|
19420
|
+
"mcp_issues",
|
|
19421
|
+
{
|
|
19422
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
19423
|
+
// Deterministic dedupe key computed in code (category + tool + normalized
|
|
19424
|
+
// signal), never by the LLM, so the same failure maps to the same row across
|
|
19425
|
+
// cron runs.
|
|
19426
|
+
fingerprint: text("fingerprint").notNull().unique(),
|
|
19427
|
+
category: text("category").$type().notNull(),
|
|
19428
|
+
severity: text("severity").$type().notNull(),
|
|
19429
|
+
tool: text("tool").notNull(),
|
|
19430
|
+
title: text("title").notNull(),
|
|
19431
|
+
summary: text("summary").notNull(),
|
|
19432
|
+
// Ready-to-paste prompt for a coding agent: what failed, the evidence, and
|
|
19433
|
+
// where to start. The "fix is one prompt away" handoff.
|
|
19434
|
+
fixPrompt: text("fix_prompt").notNull(),
|
|
19435
|
+
// Sample of the trace rows behind the issue (capped), newest first.
|
|
19436
|
+
evidence: jsonb("evidence").$type().notNull(),
|
|
19437
|
+
occurrences: integer("occurrences").notNull().default(1),
|
|
19438
|
+
status: text("status").$type().notNull().default("open"),
|
|
19439
|
+
firstSeenAt: timestamp("first_seen_at", { withTimezone: true }).defaultNow().notNull(),
|
|
19440
|
+
lastSeenAt: timestamp("last_seen_at", { withTimezone: true }).defaultNow().notNull(),
|
|
19441
|
+
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
|
19442
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
|
|
19443
|
+
},
|
|
19444
|
+
(t) => [
|
|
19445
|
+
index("mcp_issues_status_idx").on(t.status),
|
|
19446
|
+
index("mcp_issues_last_seen_at_idx").on(t.lastSeenAt)
|
|
19447
|
+
]
|
|
19448
|
+
);
|
|
19047
19449
|
projectsRelations = relations(projects, ({ many, one }) => ({
|
|
19048
19450
|
messages: many(messages),
|
|
19049
19451
|
memory: one(projectMemory, {
|
|
@@ -19109,11 +19511,17 @@ var init_schema2 = __esm({
|
|
|
19109
19511
|
selectEmailCampaignSendSchema = createSelectSchema(emailCampaignSends);
|
|
19110
19512
|
insertEmailCampaignStepSchema = createInsertSchema(emailCampaignSteps);
|
|
19111
19513
|
selectEmailCampaignStepSchema = createSelectSchema(emailCampaignSteps);
|
|
19514
|
+
pluginAuthNonces = pgTable("plugin_auth_nonces", {
|
|
19515
|
+
nonce: text("nonce").primaryKey(),
|
|
19516
|
+
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
|
19517
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
19518
|
+
expiresAt: timestamp("expires_at").notNull()
|
|
19519
|
+
});
|
|
19112
19520
|
}
|
|
19113
19521
|
});
|
|
19114
19522
|
|
|
19115
19523
|
// ../shared/src/mcp/tools/design-config-tools.ts
|
|
19116
|
-
import { z as
|
|
19524
|
+
import { z as z10 } from "zod";
|
|
19117
19525
|
function registerDesignConfigTools({
|
|
19118
19526
|
server,
|
|
19119
19527
|
tracked,
|
|
@@ -19164,7 +19572,7 @@ function registerDesignConfigTools({
|
|
|
19164
19572
|
{
|
|
19165
19573
|
title: "Get a Project's Tuned Design Config",
|
|
19166
19574
|
description: `Read the user-tuned design config (exact color tokens, radius, fonts, icon library) of a generated project. Call this BEFORE building or restyling UI for the project, and again after the user tunes values on the project page, so you apply their exact numbers instead of guessing from adjectives.`,
|
|
19167
|
-
inputSchema:
|
|
19575
|
+
inputSchema: z10.object({
|
|
19168
19576
|
projectId: PROJECT_ID_ARG,
|
|
19169
19577
|
context: CONTEXT_ARG
|
|
19170
19578
|
}),
|
|
@@ -19193,7 +19601,7 @@ function registerDesignConfigTools({
|
|
|
19193
19601
|
{
|
|
19194
19602
|
title: "Set a Project's Design Config",
|
|
19195
19603
|
description: `Write exact design-token values (colors, radius, fonts, icon library) onto a generated project. Use this when the user states concrete values ("make primary oklch(0.62 0.11 45)", "radius 12px"). It merges over the current config and persists, so the change survives restarts and every later get-design-config returns it. For vague direction ("warmer", "more playful"), iterate with the user toward concrete values first; do not invent numbers on their behalf.`,
|
|
19196
|
-
inputSchema:
|
|
19604
|
+
inputSchema: z10.object({
|
|
19197
19605
|
projectId: PROJECT_ID_ARG,
|
|
19198
19606
|
config: CONFIG_ARG,
|
|
19199
19607
|
context: CONTEXT_ARG
|
|
@@ -19238,7 +19646,7 @@ var init_design_config_tools = __esm({
|
|
|
19238
19646
|
"use strict";
|
|
19239
19647
|
init_schema2();
|
|
19240
19648
|
init_shared();
|
|
19241
|
-
PROJECT_ID_ARG =
|
|
19649
|
+
PROJECT_ID_ARG = z10.string().min(1).describe("The project id returned by create-design-system.");
|
|
19242
19650
|
CONFIG_ARG = designConfigSchema.describe(
|
|
19243
19651
|
"Partial design config to merge over the current one. Only pass the fields the user changed. Colors are oklch() strings (e.g. 'oklch(0.62 0.11 45)'); radius is a CSS length (e.g. '0.75rem'); fonts are family names."
|
|
19244
19652
|
);
|
|
@@ -19246,15 +19654,15 @@ var init_design_config_tools = __esm({
|
|
|
19246
19654
|
});
|
|
19247
19655
|
|
|
19248
19656
|
// ../shared/src/mcp/tools/jig-tools.ts
|
|
19249
|
-
import { z as
|
|
19657
|
+
import { z as z11 } from "zod";
|
|
19250
19658
|
function registerJigTools({ server, tracked }) {
|
|
19251
19659
|
server.registerTool(
|
|
19252
19660
|
"get-jig-guide",
|
|
19253
19661
|
{
|
|
19254
19662
|
title: "Get a Parameter-Tuning Jig",
|
|
19255
19663
|
description: `Return a tuning-panel setup (a "jig") plus the workflow for using it. Call this whenever you are about to hard-code taste-based parameter values: animation duration/easing/springs/stagger, particle systems, canvas physics, blur/glow/grain intensity, gradient stops, shadow recipes, parallax speeds. Instead of guessing values, wire the jig to those parameters, let the USER tune by eye, then hard-code the values they copy back and remove the jig. React web recommends DialKit (MIT, no re-render lag) with a dependency-free fallback; 'vanilla' and 'react-native' get dependency-free scaffolds.`,
|
|
19256
|
-
inputSchema:
|
|
19257
|
-
framework:
|
|
19664
|
+
inputSchema: z11.object({
|
|
19665
|
+
framework: z11.enum(["react", "vanilla", "react-native"]).optional().describe("Which setup to return. 'react' (default) for React/Next web projects; 'vanilla' for plain DOM or canvas work; 'react-native' for RN/Expo apps."),
|
|
19258
19666
|
context: CONTEXT_ARG
|
|
19259
19667
|
}),
|
|
19260
19668
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
@@ -20030,7 +20438,7 @@ var init_brand_sources = __esm({
|
|
|
20030
20438
|
});
|
|
20031
20439
|
|
|
20032
20440
|
// ../shared/src/mcp/tools/extract-tools.ts
|
|
20033
|
-
import { z as
|
|
20441
|
+
import { z as z12 } from "zod";
|
|
20034
20442
|
function authHeaders2(authContext) {
|
|
20035
20443
|
if (!authContext) return {};
|
|
20036
20444
|
const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
|
|
@@ -20066,15 +20474,15 @@ JSON.stringify(${buildRoleSampleScript()})
|
|
|
20066
20474
|
- kind "tokens": a design-tokens JSON export ($value entries, W3C format or the Figma variables API shape with 0..1 rgb floats).
|
|
20067
20475
|
|
|
20068
20476
|
Common vendor names (Shopify's --color-button family) are mapped into empty semantic slots automatically and the mapping is reported in the output.`,
|
|
20069
|
-
inputSchema:
|
|
20070
|
-
css:
|
|
20071
|
-
sources:
|
|
20072
|
-
name:
|
|
20073
|
-
kind:
|
|
20074
|
-
content:
|
|
20477
|
+
inputSchema: z12.object({
|
|
20478
|
+
css: z12.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
|
|
20479
|
+
sources: z12.array(z12.object({
|
|
20480
|
+
name: z12.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
|
|
20481
|
+
kind: z12.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
|
|
20482
|
+
content: z12.string().trim().min(1).max(2e5)
|
|
20075
20483
|
})).max(10).optional().describe("Any mix of brand sources, merged in order (later sources win). See the tool description for how to gather each kind."),
|
|
20076
|
-
name:
|
|
20077
|
-
maxTokens:
|
|
20484
|
+
name: z12.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
|
|
20485
|
+
maxTokens: z12.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
|
|
20078
20486
|
}),
|
|
20079
20487
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
20080
20488
|
},
|
|
@@ -20106,10 +20514,10 @@ Common vendor names (Shopify's --color-button family) are mapped into empty sema
|
|
|
20106
20514
|
Use this when the user names their site ("match uclips.eu", "use our website's style") or hands you any public URL whose look should be reused. Server side, Better Design fetches the page's stylesheets AND renders the page in a headless browser to sample what role elements actually look like (body, heading, link, button, input, card), then merges both, rendered truth last. That reads the brand even when the site's variable names are unrecognizable.
|
|
20107
20515
|
|
|
20108
20516
|
Slow by nature (a real browser loads the page): expect 10 to 45 seconds. For CSS the user already has locally, prefer extract-design-system, which is instant.`,
|
|
20109
|
-
inputSchema:
|
|
20110
|
-
url:
|
|
20111
|
-
name:
|
|
20112
|
-
maxTokens:
|
|
20517
|
+
inputSchema: z12.object({
|
|
20518
|
+
url: z12.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
|
|
20519
|
+
name: z12.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
|
|
20520
|
+
maxTokens: z12.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
|
|
20113
20521
|
}),
|
|
20114
20522
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
20115
20523
|
},
|
|
@@ -20148,7 +20556,7 @@ var init_extract_tools = __esm({
|
|
|
20148
20556
|
});
|
|
20149
20557
|
|
|
20150
20558
|
// ../shared/src/mcp/tools/illustration-tools.ts
|
|
20151
|
-
import { z as
|
|
20559
|
+
import { z as z13 } from "zod";
|
|
20152
20560
|
function fail(message) {
|
|
20153
20561
|
return {
|
|
20154
20562
|
content: [{ type: "text", text: message }],
|
|
@@ -20194,11 +20602,11 @@ How to call it:
|
|
|
20194
20602
|
4. Stop when \`faults\` is empty.
|
|
20195
20603
|
|
|
20196
20604
|
Markup carrying script, event handlers, remote references, or styles that reach outside the drawing is refused rather than cleaned, so the drawing you fix is the drawing you sent.`,
|
|
20197
|
-
inputSchema:
|
|
20198
|
-
svg:
|
|
20199
|
-
intent:
|
|
20200
|
-
animated:
|
|
20201
|
-
reviewToken:
|
|
20605
|
+
inputSchema: z13.object({
|
|
20606
|
+
svg: z13.string().min(1).describe("The complete svg element you drew."),
|
|
20607
|
+
intent: z13.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
|
|
20608
|
+
animated: z13.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
|
|
20609
|
+
reviewToken: z13.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
|
|
20202
20610
|
}),
|
|
20203
20611
|
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
20204
20612
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
@@ -20267,16 +20675,16 @@ var init_illustration_tools = __esm({
|
|
|
20267
20675
|
init_shared();
|
|
20268
20676
|
REVIEW_OUTPUT_SCHEMA = {
|
|
20269
20677
|
/** Empty when nothing is visibly wrong. */
|
|
20270
|
-
faults:
|
|
20678
|
+
faults: z13.array(z13.string()),
|
|
20271
20679
|
/** Group ids in paint order, the parts an animation can target. */
|
|
20272
|
-
layers:
|
|
20680
|
+
layers: z13.array(z13.string()),
|
|
20273
20681
|
/** False when the markup was refused; faults says why. */
|
|
20274
|
-
reviewed:
|
|
20682
|
+
reviewed: z13.boolean(),
|
|
20275
20683
|
/** True when the drawing could not be rendered, so only structure was checked. */
|
|
20276
|
-
unseen:
|
|
20684
|
+
unseen: z13.boolean(),
|
|
20277
20685
|
/** Pass this back to review the same drawing again without spending another credit. */
|
|
20278
|
-
reviewToken:
|
|
20279
|
-
message:
|
|
20686
|
+
reviewToken: z13.string(),
|
|
20687
|
+
message: z13.string()
|
|
20280
20688
|
};
|
|
20281
20689
|
}
|
|
20282
20690
|
});
|
|
@@ -20644,7 +21052,7 @@ function summary(findings) {
|
|
|
20644
21052
|
serious: 0,
|
|
20645
21053
|
moderate: 0,
|
|
20646
21054
|
total: findings.length,
|
|
20647
|
-
|
|
21055
|
+
truncated: dropped > 0 ? dropped : void 0
|
|
20648
21056
|
};
|
|
20649
21057
|
for (const finding of findings) result[finding.severity] += 1;
|
|
20650
21058
|
return result;
|
|
@@ -20672,7 +21080,7 @@ function inspectSpacing(input) {
|
|
|
20672
21080
|
states: input.states.map((state) => ({
|
|
20673
21081
|
name: state.name,
|
|
20674
21082
|
viewport: state.viewport,
|
|
20675
|
-
|
|
21083
|
+
screenshotDataUrl: state.screenshotDataUrl ? state.screenshotDataUrl : void 0,
|
|
20676
21084
|
elements: state.elements.map((element) => ({ id: element.id, label: element.label, rect: element.rect }))
|
|
20677
21085
|
}))
|
|
20678
21086
|
};
|
|
@@ -20694,7 +21102,7 @@ function captureSpacingSnapshot() {
|
|
|
20694
21102
|
const maxElements = 1500;
|
|
20695
21103
|
const candidates = Array.from(document.querySelectorAll("body *"));
|
|
20696
21104
|
const hiddenByAncestor = (element) => {
|
|
20697
|
-
if (
|
|
21105
|
+
if (element.checkVisibility instanceof Function) {
|
|
20698
21106
|
return !element.checkVisibility({
|
|
20699
21107
|
opacityProperty: true,
|
|
20700
21108
|
visibilityProperty: true,
|
|
@@ -21057,7 +21465,7 @@ h1 { margin: 4px 0 0; font-size: 20px; line-height: 1.25; }
|
|
|
21057
21465
|
});
|
|
21058
21466
|
|
|
21059
21467
|
// ../shared/src/mcp/tools/inspection-tools.ts
|
|
21060
|
-
import { z as
|
|
21468
|
+
import { z as z14 } from "zod";
|
|
21061
21469
|
function formatFindings(findings) {
|
|
21062
21470
|
if (findings.length === 0) return "No deterministic spacing issues were found in the supplied geometry.";
|
|
21063
21471
|
return findings.map((finding) => {
|
|
@@ -21084,26 +21492,26 @@ Workflow:
|
|
|
21084
21492
|
4. Call again with the captured states and the project's spacing scale when known.
|
|
21085
21493
|
|
|
21086
21494
|
The capture reads visible layout geometry, computed spacing, and short accessible labels for interactive controls. It does not collect source code, cookies, form values, or network data. Add data-bd-inspect-label, data-bd-symmetry, data-bd-group, data-bd-compare-size, data-bd-align-group, and data-bd-align-edge attributes when the intended relationship cannot be inferred safely.`,
|
|
21087
|
-
inputSchema:
|
|
21088
|
-
states:
|
|
21089
|
-
spacingScale:
|
|
21495
|
+
inputSchema: z14.object({
|
|
21496
|
+
states: z14.array(stateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
|
|
21497
|
+
spacingScale: z14.array(finiteNumber.positive()).min(2).max(32).optional().describe("Project spacing tokens in CSS pixels. Defaults to 4, 8, 12, 16, 24, 32, 48, 64, 96, 128."),
|
|
21090
21498
|
tolerance: finiteNumber.min(0).max(4).optional().describe("Pixel tolerance for grouping measurements. Defaults to 1px."),
|
|
21091
21499
|
context: CONTEXT_ARG
|
|
21092
21500
|
}),
|
|
21093
|
-
outputSchema:
|
|
21094
|
-
status:
|
|
21095
|
-
score:
|
|
21096
|
-
summary:
|
|
21097
|
-
critical:
|
|
21098
|
-
serious:
|
|
21099
|
-
moderate:
|
|
21100
|
-
total:
|
|
21501
|
+
outputSchema: z14.object({
|
|
21502
|
+
status: z14.enum(["needs-capture", "completed"]),
|
|
21503
|
+
score: z14.number().optional(),
|
|
21504
|
+
summary: z14.object({
|
|
21505
|
+
critical: z14.number(),
|
|
21506
|
+
serious: z14.number(),
|
|
21507
|
+
moderate: z14.number(),
|
|
21508
|
+
total: z14.number()
|
|
21101
21509
|
}).optional(),
|
|
21102
|
-
findings:
|
|
21103
|
-
states:
|
|
21104
|
-
rubric:
|
|
21105
|
-
rubricStatus:
|
|
21106
|
-
captureScript:
|
|
21510
|
+
findings: z14.array(findingSchema).optional(),
|
|
21511
|
+
states: z14.array(overlayStateSchema).optional(),
|
|
21512
|
+
rubric: z14.array(z14.object({ id: z14.string(), title: z14.string() })).optional(),
|
|
21513
|
+
rubricStatus: z14.enum(["loaded", "unavailable"]).optional(),
|
|
21514
|
+
captureScript: z14.string().optional()
|
|
21107
21515
|
}),
|
|
21108
21516
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
21109
21517
|
_meta: {
|
|
@@ -21183,46 +21591,46 @@ var init_inspection_tools = __esm({
|
|
|
21183
21591
|
init_inspection();
|
|
21184
21592
|
init_spacing_inspection_html();
|
|
21185
21593
|
init_shared();
|
|
21186
|
-
finiteNumber =
|
|
21187
|
-
rectSchema =
|
|
21594
|
+
finiteNumber = z14.number().finite();
|
|
21595
|
+
rectSchema = z14.object({
|
|
21188
21596
|
x: finiteNumber,
|
|
21189
21597
|
y: finiteNumber,
|
|
21190
21598
|
width: finiteNumber.nonnegative(),
|
|
21191
21599
|
height: finiteNumber.nonnegative()
|
|
21192
21600
|
});
|
|
21193
|
-
edgesSchema =
|
|
21601
|
+
edgesSchema = z14.object({
|
|
21194
21602
|
top: finiteNumber,
|
|
21195
21603
|
right: finiteNumber,
|
|
21196
21604
|
bottom: finiteNumber,
|
|
21197
21605
|
left: finiteNumber
|
|
21198
21606
|
});
|
|
21199
|
-
styleSchema =
|
|
21200
|
-
display:
|
|
21201
|
-
position:
|
|
21202
|
-
flexDirection:
|
|
21203
|
-
flexWrap:
|
|
21204
|
-
alignItems:
|
|
21205
|
-
justifyContent:
|
|
21607
|
+
styleSchema = z14.object({
|
|
21608
|
+
display: z14.string().max(60).optional(),
|
|
21609
|
+
position: z14.string().max(60).optional(),
|
|
21610
|
+
flexDirection: z14.string().max(60).optional(),
|
|
21611
|
+
flexWrap: z14.string().max(60).optional(),
|
|
21612
|
+
alignItems: z14.string().max(60).optional(),
|
|
21613
|
+
justifyContent: z14.string().max(60).optional(),
|
|
21206
21614
|
rowGap: finiteNumber.optional(),
|
|
21207
21615
|
columnGap: finiteNumber.optional(),
|
|
21208
21616
|
padding: edgesSchema.optional(),
|
|
21209
21617
|
margin: edgesSchema.optional(),
|
|
21210
|
-
overflowX:
|
|
21211
|
-
overflowY:
|
|
21618
|
+
overflowX: z14.string().max(60).optional(),
|
|
21619
|
+
overflowY: z14.string().max(60).optional()
|
|
21212
21620
|
});
|
|
21213
|
-
elementSchema =
|
|
21214
|
-
id:
|
|
21215
|
-
selector:
|
|
21216
|
-
tag:
|
|
21217
|
-
role:
|
|
21218
|
-
label:
|
|
21219
|
-
parentId:
|
|
21220
|
-
groupId:
|
|
21221
|
-
alignmentGroup:
|
|
21222
|
-
alignmentEdge:
|
|
21223
|
-
expectedSymmetry:
|
|
21224
|
-
compareSize:
|
|
21225
|
-
interactive:
|
|
21621
|
+
elementSchema = z14.object({
|
|
21622
|
+
id: z14.string().min(1).max(300),
|
|
21623
|
+
selector: z14.string().max(500).optional(),
|
|
21624
|
+
tag: z14.string().max(80).optional(),
|
|
21625
|
+
role: z14.string().max(80).optional(),
|
|
21626
|
+
label: z14.string().max(160).optional(),
|
|
21627
|
+
parentId: z14.string().max(300).optional(),
|
|
21628
|
+
groupId: z14.string().max(300).optional(),
|
|
21629
|
+
alignmentGroup: z14.string().max(300).optional(),
|
|
21630
|
+
alignmentEdge: z14.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
|
|
21631
|
+
expectedSymmetry: z14.enum(["horizontal", "vertical", "both"]).optional(),
|
|
21632
|
+
compareSize: z14.enum(["width", "height", "both"]).optional(),
|
|
21633
|
+
interactive: z14.boolean().optional(),
|
|
21226
21634
|
rect: rectSchema,
|
|
21227
21635
|
style: styleSchema.optional(),
|
|
21228
21636
|
clientWidth: finiteNumber.nonnegative().optional(),
|
|
@@ -21230,41 +21638,41 @@ var init_inspection_tools = __esm({
|
|
|
21230
21638
|
scrollWidth: finiteNumber.nonnegative().optional(),
|
|
21231
21639
|
scrollHeight: finiteNumber.nonnegative().optional()
|
|
21232
21640
|
});
|
|
21233
|
-
screenshotSchema =
|
|
21234
|
-
stateSchema =
|
|
21235
|
-
name:
|
|
21236
|
-
viewport:
|
|
21641
|
+
screenshotSchema = z14.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
|
|
21642
|
+
stateSchema = z14.object({
|
|
21643
|
+
name: z14.string().min(1).max(100),
|
|
21644
|
+
viewport: z14.object({
|
|
21237
21645
|
width: finiteNumber.positive().max(2e4),
|
|
21238
21646
|
height: finiteNumber.positive().max(2e4),
|
|
21239
21647
|
devicePixelRatio: finiteNumber.positive().max(10).optional()
|
|
21240
21648
|
}),
|
|
21241
|
-
elements:
|
|
21649
|
+
elements: z14.array(elementSchema).max(1500),
|
|
21242
21650
|
screenshotDataUrl: screenshotSchema.optional().describe("Optional browser screenshot as a data URL. It is echoed to the inline overlay and is not persisted.")
|
|
21243
21651
|
});
|
|
21244
|
-
evidenceSchema =
|
|
21245
|
-
label:
|
|
21246
|
-
value:
|
|
21247
|
-
unit:
|
|
21652
|
+
evidenceSchema = z14.object({
|
|
21653
|
+
label: z14.string(),
|
|
21654
|
+
value: z14.union([z14.number(), z14.string()]),
|
|
21655
|
+
unit: z14.enum(["px", "count"]).optional()
|
|
21248
21656
|
});
|
|
21249
|
-
findingSchema =
|
|
21250
|
-
id:
|
|
21251
|
-
ruleId:
|
|
21252
|
-
state:
|
|
21253
|
-
severity:
|
|
21254
|
-
message:
|
|
21255
|
-
fix:
|
|
21256
|
-
elementIds:
|
|
21257
|
-
evidence:
|
|
21657
|
+
findingSchema = z14.object({
|
|
21658
|
+
id: z14.string(),
|
|
21659
|
+
ruleId: z14.string(),
|
|
21660
|
+
state: z14.string(),
|
|
21661
|
+
severity: z14.enum(["critical", "serious", "moderate"]),
|
|
21662
|
+
message: z14.string(),
|
|
21663
|
+
fix: z14.string(),
|
|
21664
|
+
elementIds: z14.array(z14.string()),
|
|
21665
|
+
evidence: z14.array(evidenceSchema)
|
|
21258
21666
|
});
|
|
21259
|
-
overlayStateSchema =
|
|
21260
|
-
name:
|
|
21261
|
-
viewport:
|
|
21262
|
-
width:
|
|
21263
|
-
height:
|
|
21264
|
-
devicePixelRatio:
|
|
21667
|
+
overlayStateSchema = z14.object({
|
|
21668
|
+
name: z14.string(),
|
|
21669
|
+
viewport: z14.object({
|
|
21670
|
+
width: z14.number(),
|
|
21671
|
+
height: z14.number(),
|
|
21672
|
+
devicePixelRatio: z14.number().optional()
|
|
21265
21673
|
}),
|
|
21266
|
-
elements:
|
|
21267
|
-
screenshotDataUrl:
|
|
21674
|
+
elements: z14.array(z14.object({ id: z14.string(), label: z14.string().optional(), rect: rectSchema })),
|
|
21675
|
+
screenshotDataUrl: z14.string().optional()
|
|
21268
21676
|
});
|
|
21269
21677
|
}
|
|
21270
21678
|
});
|
|
@@ -21338,12 +21746,22 @@ var init_tools = __esm({
|
|
|
21338
21746
|
}
|
|
21339
21747
|
});
|
|
21340
21748
|
|
|
21749
|
+
// ../shared/src/mcp/version.ts
|
|
21750
|
+
var MCP_IMPLEMENTATION_VERSION;
|
|
21751
|
+
var init_version2 = __esm({
|
|
21752
|
+
"../shared/src/mcp/version.ts"() {
|
|
21753
|
+
"use strict";
|
|
21754
|
+
MCP_IMPLEMENTATION_VERSION = "0.21.0";
|
|
21755
|
+
}
|
|
21756
|
+
});
|
|
21757
|
+
|
|
21341
21758
|
// ../shared/src/mcp/index.ts
|
|
21342
21759
|
var mcp_exports = {};
|
|
21343
21760
|
__export(mcp_exports, {
|
|
21344
21761
|
BUILD_TARGET_RULE: () => BUILD_TARGET_RULE,
|
|
21345
21762
|
GUIDANCE_TOOLS: () => GUIDANCE_TOOLS,
|
|
21346
21763
|
INSTRUCTIONS: () => INSTRUCTIONS,
|
|
21764
|
+
MCP_IMPLEMENTATION_VERSION: () => MCP_IMPLEMENTATION_VERSION,
|
|
21347
21765
|
MCP_SESSION_WINDOW_MS: () => MCP_SESSION_WINDOW_MS,
|
|
21348
21766
|
REVIEW_TOOLS: () => REVIEW_TOOLS,
|
|
21349
21767
|
SEARCH_TOOLS: () => SEARCH_TOOLS,
|
|
@@ -21364,7 +21782,7 @@ function createMcpServer(options) {
|
|
|
21364
21782
|
const {
|
|
21365
21783
|
provider,
|
|
21366
21784
|
name = "BetterDesign",
|
|
21367
|
-
version: version2 =
|
|
21785
|
+
version: version2 = MCP_IMPLEMENTATION_VERSION,
|
|
21368
21786
|
onToolUsage,
|
|
21369
21787
|
authContext,
|
|
21370
21788
|
enableReactNativeGuide = true,
|
|
@@ -21375,7 +21793,13 @@ function createMcpServer(options) {
|
|
|
21375
21793
|
} = options;
|
|
21376
21794
|
const server = new McpServer(
|
|
21377
21795
|
{ name, version: version2 },
|
|
21378
|
-
{
|
|
21796
|
+
{
|
|
21797
|
+
instructions: getMcpInstructions({
|
|
21798
|
+
enableReactNativeGuide,
|
|
21799
|
+
enableThreeJsGuide,
|
|
21800
|
+
enableShopifyKit
|
|
21801
|
+
})
|
|
21802
|
+
}
|
|
21379
21803
|
);
|
|
21380
21804
|
registerAllTools(
|
|
21381
21805
|
server,
|
|
@@ -21397,6 +21821,7 @@ var init_mcp = __esm({
|
|
|
21397
21821
|
"use strict";
|
|
21398
21822
|
init_instructions();
|
|
21399
21823
|
init_tools();
|
|
21824
|
+
init_version2();
|
|
21400
21825
|
init_tools();
|
|
21401
21826
|
init_instructions();
|
|
21402
21827
|
init_session_guidance();
|
|
@@ -21410,7 +21835,7 @@ var review_tool_exports = {};
|
|
|
21410
21835
|
__export(review_tool_exports, {
|
|
21411
21836
|
registerReviewTool: () => registerReviewTool
|
|
21412
21837
|
});
|
|
21413
|
-
import { z as
|
|
21838
|
+
import { z as z15 } from "zod";
|
|
21414
21839
|
function registerReviewTool(server, onToolUsage) {
|
|
21415
21840
|
const { track } = createTracking(server, onToolUsage);
|
|
21416
21841
|
server.registerTool(
|
|
@@ -21427,11 +21852,11 @@ get-review-rules gives you the judgment rules to apply yourself. This tool is th
|
|
|
21427
21852
|
- WCAG basics: images without alt, icon-only buttons without an accessible name, removed focus outlines.
|
|
21428
21853
|
|
|
21429
21854
|
Fix every critical and serious finding, then call it again on the fixed source. Findings feed the same loop as a human code review: read, fix, re-check.`,
|
|
21430
|
-
inputSchema:
|
|
21431
|
-
files:
|
|
21432
|
-
|
|
21433
|
-
path:
|
|
21434
|
-
content:
|
|
21855
|
+
inputSchema: z15.object({
|
|
21856
|
+
files: z15.array(
|
|
21857
|
+
z15.object({
|
|
21858
|
+
path: z15.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
|
|
21859
|
+
content: z15.string().max(MAX_FILE_CHARS).describe("The file's full source.")
|
|
21435
21860
|
})
|
|
21436
21861
|
).min(1).max(MAX_FILES).describe("The .tsx or .jsx files to review, as written."),
|
|
21437
21862
|
context: CONTEXT_ARG
|
|
@@ -21669,7 +22094,7 @@ function createHttpDataProvider() {
|
|
|
21669
22094
|
// contract the field documents. [BD-421]
|
|
21670
22095
|
metadata: {
|
|
21671
22096
|
...systemRes.metadata,
|
|
21672
|
-
|
|
22097
|
+
...!(systemRes.shopifyAccess === void 0) && { shopifyAccess: systemRes.shopifyAccess }
|
|
21673
22098
|
},
|
|
21674
22099
|
components: componentsRes.components
|
|
21675
22100
|
};
|
|
@@ -21832,7 +22257,7 @@ if (!command || command.startsWith("-") || command === "setup") {
|
|
|
21832
22257
|
}
|
|
21833
22258
|
async function startServer() {
|
|
21834
22259
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
21835
|
-
const { createMcpServer: createMcpServer2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
22260
|
+
const { createMcpServer: createMcpServer2, MCP_IMPLEMENTATION_VERSION: MCP_IMPLEMENTATION_VERSION2 } = await Promise.resolve().then(() => (init_mcp(), mcp_exports));
|
|
21836
22261
|
const { registerReviewTool: registerReviewTool2 } = await Promise.resolve().then(() => (init_review_tool(), review_tool_exports));
|
|
21837
22262
|
const { createHttpDataProvider: createHttpDataProvider2 } = await Promise.resolve().then(() => (init_http_data_provider(), http_data_provider_exports));
|
|
21838
22263
|
const { trackToolUsage: trackToolUsage2, trackMcpInstalled: trackMcpInstalled2 } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
|
|
@@ -21856,7 +22281,7 @@ async function startServer() {
|
|
|
21856
22281
|
const server = createMcpServer2({
|
|
21857
22282
|
provider: createHttpDataProvider2(),
|
|
21858
22283
|
name: "DesignSystems",
|
|
21859
|
-
version:
|
|
22284
|
+
version: MCP_IMPLEMENTATION_VERSION2,
|
|
21860
22285
|
onToolUsage
|
|
21861
22286
|
});
|
|
21862
22287
|
registerReviewTool2(server, onToolUsage);
|
|
@@ -21871,6 +22296,6 @@ async function startServer() {
|
|
|
21871
22296
|
};
|
|
21872
22297
|
const transport = new StdioServerTransport();
|
|
21873
22298
|
await server.connect(transport);
|
|
21874
|
-
console.error(
|
|
22299
|
+
console.error(`Better Design MCP Server v${MCP_IMPLEMENTATION_VERSION2} running on stdio`);
|
|
21875
22300
|
}
|
|
21876
22301
|
//# sourceMappingURL=index.js.map
|