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/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, MCP_CLIENT_SHAPES;
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
- MCP_CLIENT_SHAPES = {
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
- ...MCP_CLIENT_SHAPES["claude-code"],
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
- ...MCP_CLIENT_SHAPES.cursor,
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
- ...MCP_CLIENT_SHAPES.vscode,
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
- ...MCP_CLIENT_SHAPES.windsurf,
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
- ...MCP_CLIENT_SHAPES.zed,
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
- ...MCP_CLIENT_SHAPES.cline,
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
- ...MCP_CLIENT_SHAPES["claude-desktop"],
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
- ...MCP_CLIENT_SHAPES.opencode,
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
- ...MCP_CLIENT_SHAPES.amp,
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
- ...MCP_CLIENT_SHAPES.codex,
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
- ...MCP_CLIENT_SHAPES.continue,
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, or Three.js 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\`. 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.`;
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(error) {
1508
- return error?.code === "ENOENT" || error?.code === "ENOTDIR";
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 { enableReactNativeGuide = true, enableThreeJsGuide = true } = options;
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": "motion",
2225
+ "name": "product-demo-video",
2216
2226
  "private": true,
2217
2227
  "scripts": {
2218
- "dev": "remotion studio",
2219
- "music": "python3 scripts/make-music.py",
2220
- "render": "remotion render Video out/video.mp4 --concurrency=6",
2221
- "render:1080": "remotion render Video out/video-1080.mp4 --concurrency=6 --scale=1.5"
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.499",
2225
- "@remotion/google-fonts": "4.0.499",
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.499"
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 { Config } from "@remotion/cli/config";
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/\\nout/\\npublic/music/\\n`
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 { registerRoot } from "remotion";
2276
- import { RemotionRoot } from "./Root";
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/Root.tsx",
2283
- contents: `import React from "react";
2284
- import { Composition } from "remotion";
2285
- import { Video, TOTAL_FRAMES, FPS } from "./Video";
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
- export const RemotionRoot: React.FC = () => (
2288
- <Composition
2289
- id="Video"
2290
- component={Video}
2291
- durationInFrames={TOTAL_FRAMES}
2292
- fps={FPS}
2293
- width={1280}
2294
- height={720}
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/Video.tsx",
2301
- contents: `import React from "react";
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
- import { BeforeAfter } from "./scenes/BeforeAfter";
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 FPS = 60;
2401
+ export const formatProfile = (format: VideoFormat) => FORMAT_PROFILES[format];
2310
2402
 
2311
- /**
2312
- * The running order, and the only place scene lengths live.
2313
- *
2314
- * Read the titles on their own. If they do not tell the story in order, no
2315
- * amount of animation will rescue it.
2316
- *
2317
- * At 120 BPM and 60fps a beat is 30 frames, so every length here is a multiple
2318
- * of 30 and the cuts land on the beat. scripts/make-music.py reads these
2319
- * numbers back out of this file, so changing one keeps the music in step.
2320
- */
2321
- export const SCENES = [
2322
- {
2323
- id: "hook",
2324
- frames: 240,
2325
- node: <TitleCard title="Your product, in sixty seconds." />,
2326
- },
2327
- {
2328
- id: "problem",
2329
- frames: 270,
2330
- node: <TitleCard title="Here is the part people struggle with." muted />,
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
- id: "product",
2334
- frames: 300,
2335
- node: <DeviceFrame caption="So we built this." />,
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
- id: "difference",
2339
- frames: 330,
2340
- node: <BeforeAfter beforeLabel="Before" afterLabel="After" />,
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
- id: "cta",
2344
- frames: 240,
2345
- node: <TitleCard title="Try it today." />,
2465
+ cta: {
2466
+ line: "Try the product",
2467
+ destination: "example.com",
2346
2468
  },
2347
- ];
2348
-
2349
- export const TOTAL_FRAMES = SCENES.reduce((sum, scene) => sum + scene.frames, 0);
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/theme.ts",
2368
- contents: `/**
2369
- * Replace these with your own design system's tokens so the film and the app
2370
- * are the same brand. If the project uses Better Design, copy the values
2371
- * straight out of globals.css.
2372
- */
2373
- export const color = {
2374
- background: "oklch(0.98 0.004 90)",
2375
- foreground: "oklch(0.24 0.005 75)",
2376
- card: "oklch(1 0 0)",
2377
- muted: "oklch(0.94 0.006 86)",
2378
- mutedFg: "oklch(0.52 0.008 80)",
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 const radius = { sm: 8, md: 16, lg: 28 };
2489
+ export type TimelineScene = SceneSpec & {from: number};
2385
2490
 
2386
- export const shadow = {
2387
- card: "0 18px 40px oklch(0 0 0 / 0.10), 0 2px 6px oklch(0 0 0 / 0.06)",
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
- * Load a webfont through @remotion/google-fonts rather than a CSS import, so
2392
- * the render waits for it. A font that arrives late changes every text
2393
- * measurement in the frame.
2394
- */
2395
- export const sans =
2396
- 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
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
- export const type = {
2399
- title: { fontSize: 76, fontWeight: 600, letterSpacing: "-0.04em" },
2400
- caption: { fontSize: 28, fontWeight: 500, letterSpacing: "-0.01em" },
2401
- label: { fontSize: 20, fontWeight: 500, letterSpacing: "0" },
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/scenes/TitleCard.tsx",
2567
+ path: "src/Root.tsx",
2407
2568
  contents: `import React from "react";
2408
- import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
2409
-
2410
- import { color, sans, type } from "../theme";
2411
-
2412
- /**
2413
- * One sentence, held still. Note the two things every scene here does:
2414
- * it starts visible enough to read on frame one, and it stops moving before
2415
- * the scene ends.
2416
- */
2417
- export const TitleCard: React.FC<{ title: string; muted?: boolean }> = ({
2418
- title,
2419
- muted = false,
2420
- }) => {
2421
- const frame = useCurrentFrame();
2422
- const { fps } = useVideoConfig();
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
- const enter = spring({ frame, fps, config: { damping: 26 } });
2425
- const rise = interpolate(enter, [0, 1], [18, 0]);
2426
- const fade = interpolate(frame, [0, 12], [0, 1], {
2427
- extrapolateLeft: "clamp",
2428
- extrapolateRight: "clamp",
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
- style={{
2434
- alignItems: "center",
2435
- justifyContent: "center",
2436
- padding: 96,
2437
- fontFamily: sans,
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
- import { color, radius, sans, shadow, type } from "../theme";
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 { fps } = useVideoConfig();
2473
-
2474
- const enter = spring({ frame, fps, config: { damping: 24 } });
2475
- const scale = interpolate(enter, [0, 1], [0.94, 1]);
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
- <AbsoluteFill
2483
- style={{
2484
- alignItems: "center",
2485
- justifyContent: "center",
2486
- gap: 40,
2487
- fontFamily: sans,
2488
- opacity: fade,
2489
- }}
2490
- >
2491
- <div
2492
- style={{
2493
- width: 880,
2494
- height: 460,
2495
- borderRadius: radius.lg,
2496
- background: color.card,
2497
- border: "1px solid " + color.border,
2498
- boxShadow: shadow.card,
2499
- transform: "scale(" + scale + ")",
2500
- display: "grid",
2501
- placeItems: "center",
2502
- color: color.mutedFg,
2503
- ...type.label,
2504
- }}
2505
- >
2506
- Your screen here
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
- <p style={{ ...type.caption, margin: 0, color: color.foreground }}>{caption}</p>
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 left = interpolate(frame, [0, 20], [0, 1], {
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 right = interpolate(frame, [30, 55], [0, 1], {
2536
- extrapolateLeft: "clamp",
2537
- extrapolateRight: "clamp",
2538
- });
2539
-
2540
- const panel = {
2541
- width: 500,
2542
- height: 380,
2543
- borderRadius: radius.md,
2544
- border: "1px solid " + color.border,
2545
- background: color.card,
2546
- boxShadow: shadow.card,
2547
- display: "grid",
2548
- placeItems: "center",
2549
- color: color.mutedFg,
2550
- ...type.label,
2551
- } as const;
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
- <AbsoluteFill
2555
- style={{
2556
- alignItems: "center",
2557
- justifyContent: "center",
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={{ opacity: right, display: "grid", gap: 16, justifyItems: "center" }}>
2568
- <div style={{ ...panel, borderColor: color.accent }}>Your new screen</div>
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/make-music.py",
2578
- contents: `"""
2579
- Synthesises the backing track, so nothing is licensed from anywhere.
2580
-
2581
- 120 BPM at 60fps is a beat every 30 frames. Scene lengths are read straight
2582
- out of src/Video.tsx, so the swells cannot drift from the edit: change a scene
2583
- length there, run this again, and the accents move with it.
2584
- """
2585
-
2586
- import math
2587
- import re
2588
- import struct
2589
- import wave
2590
- from pathlib import Path
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
- samples.append(value)
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
- # Normalise, or it renders far too quiet under the picture.
2655
- peak = max(abs(s) for s in samples) or 1.0
2656
- gain = 0.89 / peak
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
- out = ROOT / "public" / "music"
2659
- out.mkdir(parents=True, exist_ok=True)
2660
- path = out / "bed.wav"
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
- with wave.open(str(path), "w") as f:
2663
- f.setnchannels(1)
2664
- f.setsampwidth(2)
2665
- f.setframerate(SR)
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
- print(f"wrote {path.name}: {TOTAL:.2f}s, {len(CUTS)} cuts")
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: `# motion
2918
+ contents: `# Product demo video
2676
2919
 
2677
- The video for this project. Every frame is React, so a scene is reviewed and
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
- ## Run it
2922
+ ## Start here
2681
2923
 
2682
- \`\`\`bash
2683
- bun install # or npm install. The first run pulls a headless Chrome
2684
- bun run music # writes public/music/bed.wav
2685
- bun run dev # opens the studio, scrub and edit with hot reload
2686
- bun run render # writes out/video.mp4
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
- Rendering happens on your machine. Nothing is uploaded.
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
- ## Where to start
2932
+ FFmpeg must be installed and available on PATH only for bun run review:frames.
2692
2933
 
2693
- \`src/Video.tsx\` is the running order and the only place scene lengths live.
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
- \`src/theme.ts\` holds the colours and type. Replace them with your own design
2698
- system's tokens so the film and the app are the same brand.
2936
+ Run:
2699
2937
 
2700
- \`src/scenes/\` has three patterns to copy: a title card, a screenshot in a
2701
- frame, and a before and after.
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
- ## Two rules worth keeping
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
- Scene lengths are multiples of 30 frames, which at 60fps and 120 BPM puts every
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
- A render that succeeds is not a render that is correct. Before you ship it,
2710
- step through the frames:
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
- \`\`\`bash
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
- \`+faststart\` matters: without it the player waits for the whole file before it
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 repo root. Keep it out of the app's workspace: the first install pulls a headless Chrome and is around 600 MB.",
2733
- "Replace the tokens in src/theme.ts with the project's own colours, radii and type, so the film and the app are the same brand.",
2734
- "Rewrite the scene list in src/Video.tsx for this product. Keep every length a multiple of 30 frames so the cuts stay on the beat.",
2735
- "Tell the user to run: bun install, then bun run music, then bun run render.",
2736
- "Do not claim the video is finished until they have watched it. A render that succeeds is not a render that is correct."
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 }, { enableReactNativeGuide = true, enableThreeJsGuide = true } = {}) {
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 composition setup and rendering, the text-measurement and layout traps that clip words or drag a headline sideways, how to order scenes so the titles alone tell the story, cutting to a tempo so every scene lands on a beat, and how to check a finished render instead of trusting that it built.
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
- The guidance is distilled from building a real 55-second product film, so it names the failures in the order you will hit them.
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
- Start with scaffold: true. It returns a complete Remotion project that already renders, so you adapt something working instead of inventing the layout, the render pipeline and the audio script.
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 Chrome in the user's own project, around 600 MB on first install. Write the files into their repo and let them run it.
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-fundamentals" })
3138
- 3. Use an alias: get-motion-guide({ topic: "cut on the beat" })
3139
- 4. Search for the current task or problem: get-motion-guide({ query: "my animated word swap clips the longer word" })
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 that already renders, as files to write. Start here when building a video."),
3145
- topic: z2.string().trim().min(1).max(120).optional().describe("Specific motion topic or alias, e.g. video-fundamentals, text-and-layout, story-and-rhythm, verifying-a-render."),
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 that already renders. Write these files, then follow the steps.
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 === void 0 || typeof next !== "object") {
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 !== void 0 && typeof existing === "object" && !isToken(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 (typeof darkGroup === "object" && darkGroup !== void 0 && !isToken(darkGroup)) {
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/tools/design-system-tools.ts
9433
+ // ../shared/src/mcp/catalog-tokens.ts
9135
9434
  import { z as z6 } from "zod";
9136
- function installCommandWithInit(installCommand) {
9137
- return `${SHADCN_INIT_IF_MISSING}
9138
-
9139
- ${installCommand}`;
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 = z6.object({
9313
- query: z6.string().describe(
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) that drop into any Online Store 2.0 theme.` : ""}
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: z6.object({
10005
+ inputSchema: z7.object({
9668
10006
  // Off-pilot callers must not see the target at all, not merely be refused it.
9669
- target: z6.enum(
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: z6.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.`),
9673
- projectId: z6.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"'}.`),
9674
- component: z6.union([z6.string(), z6.array(z6.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."),
9675
- library: z6.enum(["radix", "base-ui"]).optional().describe('React target only, default "base-ui". Pass "radix" when the project already uses Radix primitives.'),
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: z6.object({
9908
- prompt: z6.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'."),
9909
- baseDesignSystemId: z6.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."),
9910
- brandCss: z6.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).")
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
- ...baseDesignSystemId ? { designSystemId: baseDesignSystemId } : {},
9933
- ...brandCss ? { 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 ? { 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: z6.object({
10063
- projectId: z6.string().min(1).describe("The project id returned by create-design-system."),
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: z6.object({
10087
- designSystemId: z6.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'."),
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 REGISTRY_ITEM_SCHEMA, SHADCN_INIT_IF_MISSING, SAFE_COMPONENT_NAME_RE, CATALOG_KIT_COMPONENTS, DS_INDEX_SCHEMA;
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
- REGISTRY_ITEM_SCHEMA = z6.object({
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
- CATALOG_KIT_COMPONENTS = ["button", "input", "card", "badge", "switch"];
10169
- DS_INDEX_SCHEMA = z6.object({
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 z7 } from "zod";
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 ?? z7;
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, z15, coerce) {
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 ? z15.coerce.number() : z15.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, z15, coerce) {
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 ? z15.coerce.bigint() : z15.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, z15, coerce) {
18656
+ function stringColumnToSchema(column, z16, coerce) {
18327
18657
  if (isColumnType(column, ["PgUUID"])) {
18328
- return z15.string().uuid();
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 ? z15.coerce.string() : z15.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) : z7.any();
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 z7.object(columnSchemas);
18727
+ return z8.object(columnSchemas);
18398
18728
  }
18399
18729
  function handleEnum(enum_, factory) {
18400
- const zod = factory?.zodInstance ?? z7;
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 = z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()]);
18430
- jsonSchema = z7.union([literalSchema, z7.record(z7.any()), z7.array(z7.any())]);
18431
- bufferSchema = z7.custom((v) => v instanceof Buffer);
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 z8 } from "zod";
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 = z8.enum(onboardingPathValues);
18822
+ onboardingPathSchema = z9.enum(onboardingPathValues);
18493
18823
  onboardingPathSqlValues = sql.raw(
18494
18824
  onboardingPathValues.map((path5) => `'${path5}'`).join(", ")
18495
18825
  );
18496
- designConfigSchema = z8.object({
18497
- colors: z8.object({
18498
- primary: z8.string(),
18499
- secondary: z8.string(),
18500
- accent: z8.string(),
18501
- muted: z8.string(),
18502
- destructive: z8.string(),
18503
- background: z8.string(),
18504
- foreground: z8.string(),
18505
- card: z8.string(),
18506
- cardForeground: z8.string(),
18507
- popover: z8.string(),
18508
- popoverForeground: z8.string(),
18509
- border: z8.string(),
18510
- input: z8.string(),
18511
- ring: z8.string()
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: z8.string(),
18514
- font: z8.object({
18515
- sans: z8.string(),
18516
- mono: z8.string()
18843
+ radius: z9.string(),
18844
+ font: z9.object({
18845
+ sans: z9.string(),
18846
+ mono: z9.string()
18517
18847
  }).partial(),
18518
- icons: z8.object({
18519
- libraryId: z8.string(),
18520
- variant: z8.string().nullable()
18848
+ icons: z9.object({
18849
+ libraryId: z9.string(),
18850
+ variant: z9.string().nullable()
18521
18851
  }).partial()
18522
18852
  }).partial();
18523
- projectMemoryDataSchema = z8.object({
18524
- lockedIn: z8.array(z8.string()),
18525
- rejected: z8.array(z8.string()),
18526
- tone: z8.string().nullable(),
18527
- notes: z8.string().nullable()
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 z9 } from "zod";
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: z9.object({
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: z9.object({
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 = z9.string().min(1).describe("The project id returned by create-design-system.");
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 z10 } from "zod";
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: z10.object({
19257
- framework: z10.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."),
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 z11 } from "zod";
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: z11.object({
20070
- css: z11.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."),
20071
- sources: z11.array(z11.object({
20072
- name: z11.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
20073
- kind: z11.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."),
20074
- content: z11.string().trim().min(1).max(2e5)
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: z11.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
20077
- maxTokens: z11.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
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: z11.object({
20110
- url: z11.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
20111
- name: z11.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
20112
- maxTokens: z11.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
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 z12 } from "zod";
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: z12.object({
20198
- svg: z12.string().min(1).describe("The complete svg element you drew."),
20199
- intent: z12.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
20200
- animated: z12.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
20201
- reviewToken: z12.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
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: z12.array(z12.string()),
20678
+ faults: z13.array(z13.string()),
20271
20679
  /** Group ids in paint order, the parts an animation can target. */
20272
- layers: z12.array(z12.string()),
20680
+ layers: z13.array(z13.string()),
20273
20681
  /** False when the markup was refused; faults says why. */
20274
- reviewed: z12.boolean(),
20682
+ reviewed: z13.boolean(),
20275
20683
  /** True when the drawing could not be rendered, so only structure was checked. */
20276
- unseen: z12.boolean(),
20684
+ unseen: z13.boolean(),
20277
20685
  /** Pass this back to review the same drawing again without spending another credit. */
20278
- reviewToken: z12.string(),
20279
- message: z12.string()
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
- ...dropped > 0 ? { truncated: dropped } : {}
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
- ...state.screenshotDataUrl ? { screenshotDataUrl: state.screenshotDataUrl } : {},
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 (typeof element.checkVisibility === "function") {
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 z13 } from "zod";
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: z13.object({
21088
- states: z13.array(stateSchema).min(1).max(6).optional().describe("Rendered layout states from the capture script. Omit on the first call to receive the script."),
21089
- spacingScale: z13.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."),
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: z13.object({
21094
- status: z13.enum(["needs-capture", "completed"]),
21095
- score: z13.number().optional(),
21096
- summary: z13.object({
21097
- critical: z13.number(),
21098
- serious: z13.number(),
21099
- moderate: z13.number(),
21100
- total: z13.number()
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: z13.array(findingSchema).optional(),
21103
- states: z13.array(overlayStateSchema).optional(),
21104
- rubric: z13.array(z13.object({ id: z13.string(), title: z13.string() })).optional(),
21105
- rubricStatus: z13.enum(["loaded", "unavailable"]).optional(),
21106
- captureScript: z13.string().optional()
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 = z13.number().finite();
21187
- rectSchema = z13.object({
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 = z13.object({
21601
+ edgesSchema = z14.object({
21194
21602
  top: finiteNumber,
21195
21603
  right: finiteNumber,
21196
21604
  bottom: finiteNumber,
21197
21605
  left: finiteNumber
21198
21606
  });
21199
- styleSchema = z13.object({
21200
- display: z13.string().max(60).optional(),
21201
- position: z13.string().max(60).optional(),
21202
- flexDirection: z13.string().max(60).optional(),
21203
- flexWrap: z13.string().max(60).optional(),
21204
- alignItems: z13.string().max(60).optional(),
21205
- justifyContent: z13.string().max(60).optional(),
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: z13.string().max(60).optional(),
21211
- overflowY: z13.string().max(60).optional()
21618
+ overflowX: z14.string().max(60).optional(),
21619
+ overflowY: z14.string().max(60).optional()
21212
21620
  });
21213
- elementSchema = z13.object({
21214
- id: z13.string().min(1).max(300),
21215
- selector: z13.string().max(500).optional(),
21216
- tag: z13.string().max(80).optional(),
21217
- role: z13.string().max(80).optional(),
21218
- label: z13.string().max(160).optional(),
21219
- parentId: z13.string().max(300).optional(),
21220
- groupId: z13.string().max(300).optional(),
21221
- alignmentGroup: z13.string().max(300).optional(),
21222
- alignmentEdge: z13.enum(["left", "right", "top", "bottom", "center-x", "center-y"]).optional(),
21223
- expectedSymmetry: z13.enum(["horizontal", "vertical", "both"]).optional(),
21224
- compareSize: z13.enum(["width", "height", "both"]).optional(),
21225
- interactive: z13.boolean().optional(),
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 = z13.string().max(5e6).regex(/^data:image\/(?:png|jpeg|webp);base64,/, "Use a PNG, JPEG, or WebP data URL.");
21234
- stateSchema = z13.object({
21235
- name: z13.string().min(1).max(100),
21236
- viewport: z13.object({
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: z13.array(elementSchema).max(1500),
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 = z13.object({
21245
- label: z13.string(),
21246
- value: z13.union([z13.number(), z13.string()]),
21247
- unit: z13.enum(["px", "count"]).optional()
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 = z13.object({
21250
- id: z13.string(),
21251
- ruleId: z13.string(),
21252
- state: z13.string(),
21253
- severity: z13.enum(["critical", "serious", "moderate"]),
21254
- message: z13.string(),
21255
- fix: z13.string(),
21256
- elementIds: z13.array(z13.string()),
21257
- evidence: z13.array(evidenceSchema)
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 = z13.object({
21260
- name: z13.string(),
21261
- viewport: z13.object({
21262
- width: z13.number(),
21263
- height: z13.number(),
21264
- devicePixelRatio: z13.number().optional()
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: z13.array(z13.object({ id: z13.string(), label: z13.string().optional(), rect: rectSchema })),
21267
- screenshotDataUrl: z13.string().optional()
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 = "3.0.0",
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
- { instructions: getMcpInstructions({ enableReactNativeGuide, enableThreeJsGuide }) }
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 z14 } from "zod";
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: z14.object({
21431
- files: z14.array(
21432
- z14.object({
21433
- path: z14.string().trim().min(1).max(400).describe("Repo-relative file path, e.g. components/signup-form.tsx."),
21434
- content: z14.string().max(MAX_FILE_CHARS).describe("The file's full source.")
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
- ...systemRes.shopifyAccess === void 0 ? {} : { shopifyAccess: systemRes.shopifyAccess }
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: "3.0.0",
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("Better Design MCP Server v3.0 running on stdio");
22299
+ console.error(`Better Design MCP Server v${MCP_IMPLEMENTATION_VERSION2} running on stdio`);
21875
22300
  }
21876
22301
  //# sourceMappingURL=index.js.map