better-design 0.11.0 → 0.15.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
@@ -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, or Expo work, without me having to explicitly ask. For React Native or Expo, load task guidance with \`get-react-native-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. 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, 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. 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.`;
288
288
  RULE_BLOCK = `${RULE_START}
289
289
  ${RULE_BODY}
290
290
  ${RULE_END}`;
@@ -853,9 +853,20 @@ var init_setup = __esm({
853
853
  });
854
854
 
855
855
  // ../shared/src/mcp/instructions.ts
856
- function getMcpInstructions(enableReactNativeGuide = true) {
857
- if (enableReactNativeGuide) return INSTRUCTIONS;
858
- return INSTRUCTIONS.replace(", styling, React Native, or Expo", ", or styling").replace(", get-react-native-guide", "").replace(/\n\nRN\/Expo:.*not both\./, "");
856
+ function getMcpInstructions(options = {}) {
857
+ const { enableReactNativeGuide = true, enableThreeJsGuide = true } = options;
858
+ let instructions = INSTRUCTIONS;
859
+ if (!enableReactNativeGuide && !enableThreeJsGuide) {
860
+ instructions = instructions.replace(", styling, React Native, Expo, or Three.js", ", or styling").replace(", get-react-native-guide", "").replace(", get-three-js-guide", "").replace(/\nRN\/Expo:.*not both\./, "");
861
+ } else if (!enableReactNativeGuide) {
862
+ instructions = instructions.replace("React Native, Expo, or Three.js", "or Three.js").replace(", get-react-native-guide", "").replace(
863
+ "RN/Expo: get-react-native-guide; Three.js/R3F: get-three-js-guide.",
864
+ "Three.js/R3F: get-three-js-guide."
865
+ );
866
+ } else if (!enableThreeJsGuide) {
867
+ instructions = instructions.replace(", Expo, or Three.js work", ", or Expo work").replace(", get-three-js-guide", "").replace("; Three.js/R3F: get-three-js-guide", "");
868
+ }
869
+ return instructions.replace(/\n{3,}/g, "\n\n");
859
870
  }
860
871
  var INSTRUCTIONS;
861
872
  var init_instructions = __esm({
@@ -863,17 +874,17 @@ var init_instructions = __esm({
863
874
  "use strict";
864
875
  INSTRUCTIONS = `# Better Design
865
876
 
866
- Use these tools for ANY frontend, UI, component, styling, React Native, or Expo work.
877
+ Use these tools for ANY frontend, UI, component, styling, React Native, Expo, or Three.js work.
867
878
 
868
- Tools: find-design-system, create-design-system, get-design-system-status, get-design-system-kit, preview-design-system, extract-design-system, get-ui-principle, get-ux-principle, get-react-native-guide, get-review-rules, check-comprehension, find-icon-library, find-icons, install-icons, detect-icon-library.
879
+ Tools: find-design-system, create-design-system, get-design-system-status, get-design-system-kit, preview-design-system, extract-design-system, get-ui-principle, get-ux-principle, get-react-native-guide, get-three-js-guide, get-review-rules, check-comprehension, find-icon-library, find-icons, install-icons, detect-icon-library.
869
880
 
870
- RN/Expo: get-react-native-guide({ topic }) for lookup; ({ query }) for search; not both.
881
+ RN/Expo: get-react-native-guide; Three.js/R3F: get-three-js-guide. ({ topic }) for lookup; ({ query }) for search; not both.
871
882
 
872
- ## Build an app (even if the user never says "design" or "brand")
883
+ ## Build an app (even if the user never says "design")
873
884
  On any "build/make me <app>" request, settle the brand FIRST, then build with it:
874
885
  1. find-design-system({ query: the request }) BEFORE any generic coding/site builder. SHOW ~4 matches and ASK which to use, or fully custom. Don't auto-pick.
875
- 2. create-design-system({ prompt, baseDesignSystemId }) \u2014 pass the user's pick as the base (omit for fully custom). Share the project URL to watch it build.
876
- 3. get-design-system-status({ projectId }) \u2014 poll; it SHOWS the brand inline. When ready, find-icon-library and SHOW the user the closest icon libraries (no scores), install it, then offer to build. Don't stop silently.
886
+ 2. create-design-system({ prompt, baseDesignSystemId }): pass the user's pick as the base (omit for fully custom). Share the project URL.
887
+ 3. get-design-system-status({ projectId }): poll; it SHOWS the brand inline. When ready, find-icon-library and SHOW the closest icon libraries (no scores), install it, then offer to build. Don't stop silently.
877
888
  4. Build the app with that brand. Brand first, not raw UI.
878
889
 
879
890
  ## Building / modifying UI
@@ -882,8 +893,8 @@ On any "build/make me <app>" request, settle the brand FIRST, then build with it
882
893
  3. Use the project's tokens/components. Semantic HTML, aria, keyboard nav, WCAG AA.
883
894
  4. After: get-review-rules (copy/AI-tell rules) + check-comprehension; fix every critical/serious issue before presenting.
884
895
 
885
- ## If the user names an existing catalog DS
886
- Default is create-design-system (above). For a named catalog system: find-design-system(query) -> get-design-system-kit({ target: "react", designSystemId }) in coding hosts, or target "html" in inline canvases. Icons: detect-icon-library / find-icon-library -> find-icons -> install-icons. Never runtime @iconify/react.
896
+ ## If the user names a catalog DS
897
+ Default is create-design-system (above). For a named system: find-design-system(query) -> get-design-system-kit({ target: "react", designSystemId }) in coding hosts, or target "html" in inline canvases. Icons: detect-icon-library / find-icon-library -> find-icons -> install-icons. Never runtime @iconify/react.
887
898
  `.trim();
888
899
  }
889
900
  });
@@ -899,6 +910,28 @@ function codingHostName(server) {
899
910
  return void 0;
900
911
  }
901
912
  }
913
+ function buildKitOutputSchema(enableShopifyKit = true) {
914
+ const shared = enableShopifyKit ? "HTML and Shopify targets." : "HTML target only.";
915
+ const base = PREVIEW_OUTPUT_SCHEMA.extend({
916
+ buildInstructions: z.string().min(1).optional().describe("Mandatory build and review contract for clients that read only structuredContent."),
917
+ requiredWrapperClass: z.literal("ds").optional().describe(`${shared} The artifact or snippet root must retain this class exactly.`),
918
+ requiredComponentClassPrefix: z.literal("ds-").optional().describe(`${shared} Interactive components must retain classes with this prefix; never rename it.`),
919
+ customCssScope: z.literal("layout-only").optional().describe(`${shared} Custom classes may arrange layout but must not replace kit component styling.`),
920
+ hoverPolicy: z.string().min(1).optional().describe(`${shared} Mandatory pointer-safe hover contract to verify before presenting.`),
921
+ hostIsolationCssTemplate: z.string().min(1).optional().describe("HTML target only. Replace APP_ROOT with the artifact root selector and paste after host/base CSS so semantic text cannot inherit the host's light/dark colors."),
922
+ minimumTextContrast: z.literal("4.5:1").optional().describe(`${shared} Minimum contrast for normal visible text against its computed background.`),
923
+ textColorPolicy: z.string().min(1).optional().describe(`${shared} Mandatory semantic foreground-token contract; surface tokens and opacity must not be used to mute text.`),
924
+ hierarchyPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory first-view hierarchy, progressive-disclosure, and information-budget contract."),
925
+ compositionPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory layout-density and container-restraint policy."),
926
+ componentCss: z.string().min(1).optional().describe("HTML target only. Paste this token-driven ds-* component stylesheet verbatim."),
927
+ componentMaterialCss: z.string().optional().describe("HTML target only. Paste this design-system-specific material override after componentCss."),
928
+ componentExampleHtml: z.string().min(1).optional().describe("HTML target only. Starter markup that demonstrates the required .ds wrapper and ds-* component classes.")
929
+ });
930
+ if (!enableShopifyKit) return base;
931
+ return base.extend({
932
+ themeFiles: z.array(z.object({ path: z.string().min(1), content: z.string().min(1) })).optional().describe("Shopify target only. Theme files to write verbatim at each path: the per-system CSS asset, the Liquid snippets, and the theme editor sections.")
933
+ });
934
+ }
902
935
  function extractOutputPreview(result) {
903
936
  const content = result?.content;
904
937
  if (!Array.isArray(content)) return void 0;
@@ -1049,21 +1082,7 @@ The UI copy inside the screen follows the same restraint: short headlines (about
1049
1082
  step: z.string().optional().describe("Current generation step, while generating."),
1050
1083
  tokensCss: z.string().describe("CSS custom-property stylesheet for the system's tokens.")
1051
1084
  });
1052
- BUILD_KIT_OUTPUT_SCHEMA = PREVIEW_OUTPUT_SCHEMA.extend({
1053
- buildInstructions: z.string().min(1).optional().describe("Mandatory build and review contract for clients that read only structuredContent."),
1054
- requiredWrapperClass: z.literal("ds").optional().describe("HTML target only. The artifact root must retain this class exactly."),
1055
- requiredComponentClassPrefix: z.literal("ds-").optional().describe("HTML target only. Interactive components must retain classes with this prefix; never rename it."),
1056
- customCssScope: z.literal("layout-only").optional().describe("HTML target only. Custom classes may arrange layout but must not replace kit component styling."),
1057
- hoverPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory pointer-safe hover contract to verify before presenting."),
1058
- hostIsolationCssTemplate: z.string().min(1).optional().describe("HTML target only. Replace APP_ROOT with the artifact root selector and paste after host/base CSS so semantic text cannot inherit the host's light/dark colors."),
1059
- minimumTextContrast: z.literal("4.5:1").optional().describe("HTML target only. Minimum contrast for normal visible text against its computed background."),
1060
- textColorPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory semantic foreground-token contract; surface tokens and opacity must not be used to mute text."),
1061
- hierarchyPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory first-view hierarchy, progressive-disclosure, and information-budget contract."),
1062
- compositionPolicy: z.string().min(1).optional().describe("HTML target only. Mandatory layout-density and container-restraint policy."),
1063
- componentCss: z.string().min(1).optional().describe("HTML target only. Paste this token-driven ds-* component stylesheet verbatim."),
1064
- componentMaterialCss: z.string().optional().describe("HTML target only. Paste this design-system-specific material override after componentCss."),
1065
- componentExampleHtml: z.string().min(1).optional().describe("HTML target only. Starter markup that demonstrates the required .ds wrapper and ds-* component classes.")
1066
- });
1085
+ BUILD_KIT_OUTPUT_SCHEMA = buildKitOutputSchema();
1067
1086
  CONTEXT_ARG = z.string().max(280).optional().describe(
1068
1087
  "Optional. One sentence on WHY you are calling this and what the user asked for (e.g. 'adding nav icons for the fintech dashboard'). Used only for usage analytics; does not change the result."
1069
1088
  );
@@ -1072,6 +1091,8 @@ The UI copy inside the screen follows the same restraint: short headlines (about
1072
1091
  "get-ui-principle",
1073
1092
  "get-ux-principle",
1074
1093
  "get-react-native-guide",
1094
+ "get-three-js-guide",
1095
+ "get-motion-guide",
1075
1096
  "get-review-rules",
1076
1097
  "find-icon-library",
1077
1098
  "find-icons",
@@ -1161,7 +1182,7 @@ function formatTopicNotFound(topic, principles, toolName, resourceLabel) {
1161
1182
  }
1162
1183
  async function runPrincipleTool(provider, area, label, toolName, args) {
1163
1184
  const { topic, query, maxTokens } = args;
1164
- const resourceLabel = area === "react-native" ? "Guides" : "Principles";
1185
+ const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" ? "Guides" : "Principles";
1165
1186
  if (topic && query) {
1166
1187
  return {
1167
1188
  response: {
@@ -1236,7 +1257,7 @@ ${formatted}`;
1236
1257
  capture: { topResults: [] }
1237
1258
  };
1238
1259
  }
1239
- function registerPrincipleTools({ server, provider, track }, { enableReactNativeGuide = true } = {}) {
1260
+ function registerPrincipleTools({ server, provider, track }, { enableReactNativeGuide = true, enableThreeJsGuide = true } = {}) {
1240
1261
  server.registerTool(
1241
1262
  "get-ui-principle",
1242
1263
  {
@@ -1377,6 +1398,116 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
1377
1398
  }
1378
1399
  );
1379
1400
  }
1401
+ if (enableThreeJsGuide) {
1402
+ server.registerTool(
1403
+ "get-three-js-guide",
1404
+ {
1405
+ title: "Get Three.js Guide",
1406
+ description: `Load implementation guidance for Three.js and React Three Fiber projects.
1407
+
1408
+ Use this while planning, building, debugging, or optimizing 3D web scenes. It covers Three.js fundamentals, geometries, materials, textures, lights and shadows, models and 3D text, particles, physics with Rapier, performance, postprocessing, React Three Fiber and its drei helper catalog, WebGPU and TSL node materials, shader fundamentals, and copyable shader effect recipes. Preserve the examples when adapting them so the surrounding invariants remain visible to the coding agent.
1409
+
1410
+ This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
1411
+
1412
+ You can either:
1413
+ 1. Request a known topic: get-three-js-guide({ topic: "fundamentals" })
1414
+ 2. Use an alias: get-three-js-guide({ topic: "webgpu and tsl" })
1415
+ 3. Search for the current task or code example: get-three-js-guide({ query: "code example for a dissolve shader effect" })
1416
+ 4. Get the full topic index: get-three-js-guide({})
1417
+
1418
+ Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
1419
+ inputSchema: z2.object({
1420
+ topic: z2.string().trim().min(1).max(120).optional().describe("Specific Three.js topic or alias, e.g. fundamentals, shader-effects-recipes, drei-catalog, webgpu-and-tsl."),
1421
+ query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the Three.js or React Three Fiber task or problem."),
1422
+ maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1423
+ context: CONTEXT_ARG
1424
+ }),
1425
+ annotations: { readOnlyHint: true }
1426
+ },
1427
+ async ({ topic, query, maxTokens, context }) => {
1428
+ const start = Date.now();
1429
+ try {
1430
+ const { response, capture } = await runPrincipleTool(
1431
+ provider,
1432
+ "three-js",
1433
+ "Three.js",
1434
+ "get-three-js-guide",
1435
+ { topic, query, maxTokens }
1436
+ );
1437
+ return trackSearch(
1438
+ track,
1439
+ "get-three-js-guide",
1440
+ { ...capture, context },
1441
+ start,
1442
+ response
1443
+ );
1444
+ } catch (error) {
1445
+ return trackSearch(
1446
+ track,
1447
+ "get-three-js-guide",
1448
+ { query, topic, topResults: [], context },
1449
+ start,
1450
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1451
+ true,
1452
+ error instanceof Error ? error.message : String(error)
1453
+ );
1454
+ }
1455
+ }
1456
+ );
1457
+ }
1458
+ server.registerTool(
1459
+ "get-motion-guide",
1460
+ {
1461
+ title: "Get Motion Guide",
1462
+ description: `Load guidance for building a product demo or promo video in code.
1463
+
1464
+ 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.
1465
+
1466
+ The guidance is distilled from building a real 55-second product film, so it names the failures in the order you will hit them.
1467
+
1468
+ This tool hands over guidance only. It does not render: that needs Remotion and a headless Chrome in the user's own project. Write the composition into their repo and let them render locally.
1469
+
1470
+ This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
1471
+
1472
+ You can either:
1473
+ 1. Request a known topic: get-motion-guide({ topic: "fundamentals" })
1474
+ 2. Use an alias: get-motion-guide({ topic: "cut on the beat" })
1475
+ 3. Search for the current task or problem: get-motion-guide({ query: "my animated word swap clips the longer word" })
1476
+ 4. Get the full topic index: get-motion-guide({})
1477
+
1478
+ Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
1479
+ inputSchema: z2.object({
1480
+ topic: z2.string().trim().min(1).max(120).optional().describe("Specific motion topic or alias, e.g. fundamentals, text-and-layout, story-and-rhythm, verifying-a-render."),
1481
+ query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the video or animation task or problem."),
1482
+ maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1483
+ context: CONTEXT_ARG
1484
+ }),
1485
+ annotations: { readOnlyHint: true }
1486
+ },
1487
+ async ({ topic, query, maxTokens, context }) => {
1488
+ const start = Date.now();
1489
+ try {
1490
+ const { response, capture } = await runPrincipleTool(
1491
+ provider,
1492
+ "motion",
1493
+ "Motion",
1494
+ "get-motion-guide",
1495
+ { topic, query, maxTokens }
1496
+ );
1497
+ return trackSearch(track, "get-motion-guide", { ...capture, context }, start, response);
1498
+ } catch (error) {
1499
+ return trackSearch(
1500
+ track,
1501
+ "get-motion-guide",
1502
+ { query, topic, topResults: [], context },
1503
+ start,
1504
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1505
+ true,
1506
+ error instanceof Error ? error.message : String(error)
1507
+ );
1508
+ }
1509
+ }
1510
+ );
1380
1511
  server.registerTool(
1381
1512
  "get-review-rules",
1382
1513
  {
@@ -2806,11 +2937,12 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
2806
2937
  Full live showcase: ${viewUrl}` : ""
2807
2938
  ].join("\n");
2808
2939
  }
2809
- var MINIMUM_TEXT_CONTRAST, INLINE_TEXT_COLOR_POLICY, HOST_ISOLATION_CSS_TEMPLATE, INLINE_COMPOSITION_POLICY, INLINE_HIERARCHY_POLICY, DS_KIT_CSS, DS_KIT_EXAMPLE_HTML, HTML_KIT_CONDUCT_MD;
2940
+ var MINIMUM_TEXT_CONTRAST, HOVER_POLICY, INLINE_TEXT_COLOR_POLICY, HOST_ISOLATION_CSS_TEMPLATE, INLINE_COMPOSITION_POLICY, INLINE_HIERARCHY_POLICY, DS_KIT_CSS, DS_KIT_EXAMPLE_HTML, HTML_KIT_CONDUCT_MD;
2810
2941
  var init_html_kit = __esm({
2811
2942
  "../shared/src/mcp/html-kit.ts"() {
2812
2943
  "use strict";
2813
2944
  MINIMUM_TEXT_CONTRAST = "4.5:1";
2945
+ HOVER_POLICY = "Put every :hover rule inside @media (hover: hover) and (pointer: fine); never move controls or override selected/pressed styling. For selectable controls, unselected hover stays neutral: only strengthen the neutral surface or edge, never changes text to the primary color, and never borrows the primary/accent fill. Do not add app-specific :hover rules to ds-* controls; use the kit states verbatim.";
2814
2946
  INLINE_TEXT_COLOR_POLICY = [
2815
2947
  "Use semantic foreground tokens for text: `--foreground` on the page, `--card-foreground` on cards, and `--muted-foreground` for secondary copy.",
2816
2948
  "The background, card, muted, secondary, accent, and input tokens are surfaces: never use surface tokens as text colors.",
@@ -3006,6 +3138,645 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
3006
3138
  }
3007
3139
  });
3008
3140
 
3141
+ // ../shared/src/mcp/shopify-kit.ts
3142
+ function fence2(lang, body) {
3143
+ return `\`\`\`${lang}
3144
+ ${body}
3145
+ \`\`\``;
3146
+ }
3147
+ function buildDsShopifyFiles(input) {
3148
+ const materialCss = input.componentKit?.css?.trim();
3149
+ const assetCss = [
3150
+ compactTokensCss(input.tokensCss),
3151
+ DS_KIT_CSS,
3152
+ DS_SHOPIFY_CSS,
3153
+ ...materialCss ? [materialCss] : []
3154
+ ].join("\n\n");
3155
+ return [{ path: "assets/ds-kit.css", content: assetCss }, ...SNIPPETS];
3156
+ }
3157
+ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
3158
+ const files = buildDsShopifyFiles({ tokensCss, componentKit });
3159
+ const sourceLine = source?.kind === "catalog" ? `Source designSystemId: \`${source.designSystemId}\`` : source?.kind === "project" ? `Source projectId: \`${source.projectId}\`` : null;
3160
+ const fileBlocks = files.flatMap((f) => [
3161
+ `### \`${f.path}\``,
3162
+ "",
3163
+ fence2(f.path.endsWith(".css") ? "css" : f.path.endsWith(".json") ? "json" : "liquid", f.content),
3164
+ ""
3165
+ ]);
3166
+ return [
3167
+ `# ${name}: Shopify theme kit`,
3168
+ "",
3169
+ "Full design system for an existing Online Store 2.0 theme: one CSS asset carries this system's real tokens and component material, and the Liquid snippets, sections, and alternate templates cover a complete storefront (header, footer, hero, product page, collection page, content sections) with no JavaScript. Write each file to the path in its heading.",
3170
+ ...sourceLine ? ["", sourceLine] : [],
3171
+ "",
3172
+ SHOPIFY_KIT_CONDUCT_MD,
3173
+ "",
3174
+ "## Theme files",
3175
+ "",
3176
+ ...fileBlocks,
3177
+ "## After installing",
3178
+ "- Preview an unpublished copy of the theme first; never edit the live theme directly.",
3179
+ "- The kit styles parts of a storefront; page composition, cart mechanics, and checkout stay with the theme.",
3180
+ ...viewUrl ? ["", `Full live showcase: ${viewUrl}`] : []
3181
+ ].join("\n");
3182
+ }
3183
+ var DS_SHOPIFY_CSS, SNIPPET_BADGE, SNIPPET_PRICE, SNIPPET_BUY_BUTTON, SNIPPET_VARIANT_SELECT, SNIPPET_PAGINATION, SNIPPET_PRODUCT_CARD, SNIPPET_ANNOUNCEMENT, SNIPPET_NEWSLETTER, SECTION_FAQ, SECTION_FEATURED_COLLECTION, SECTION_HEADER, SECTION_FOOTER, SECTION_HERO, SECTION_RICH_TEXT, SECTION_IMAGE_WITH_TEXT, SECTION_MAIN_PRODUCT, SECTION_MAIN_COLLECTION, SECTION_CONTACT, SECTION_MULTICOLUMN, TEMPLATE_PRODUCT_JSON, TEMPLATE_COLLECTION_JSON, SNIPPETS, SHOPIFY_KIT_CONDUCT_MD;
3184
+ var init_shopify_kit = __esm({
3185
+ "../shared/src/mcp/shopify-kit.ts"() {
3186
+ "use strict";
3187
+ init_html_kit();
3188
+ DS_SHOPIFY_CSS = `.ds.ds-embed{background:transparent}
3189
+ .ds-announce{background:var(--primary,oklch(0.25 0.01 260));color:var(--primary-foreground,oklch(1 0 0));text-align:center;padding:.5rem 1rem;font-size:.8125rem}
3190
+ .ds-announce p{margin:0}
3191
+ .ds-announce a{color:inherit;text-decoration:underline}
3192
+ .ds-price{display:inline-flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}
3193
+ .ds-price-current{font-weight:600;color:var(--foreground,oklch(0.2 0 0))}
3194
+ .ds-price-compare{color:var(--muted-foreground,oklch(0.55 0.01 260))}
3195
+ .ds-card-media{display:block}
3196
+ .ds-card-media img{display:block;width:100%;height:auto}
3197
+ .ds-product-card .ds-card-title a{color:inherit;text-decoration:none}
3198
+ .ds-faq-item{border-bottom:1px solid var(--border,oklch(0.9 0.01 260));padding:.25rem 0}
3199
+ .ds-faq-item summary{cursor:pointer;padding:.75rem 0;font-weight:600;color:var(--foreground,oklch(0.2 0 0))}
3200
+ .ds-faq-answer{padding:0 0 1rem}
3201
+ .ds-grid{display:grid;grid-template-columns:repeat(var(--ds-cols,4),1fr);gap:1rem}
3202
+ @media (max-width:749px){.ds-grid{grid-template-columns:repeat(2,1fr)}}
3203
+ .ds-newsletter .ds-control-row{margin-top:.75rem}
3204
+ .ds-buy-form{margin:0;display:grid;gap:.75rem;justify-items:start}
3205
+ .ds-featured{padding:2rem 0}
3206
+ .ds-qty{width:6rem}
3207
+ .ds-header{display:flex;align-items:center;gap:1.5rem;padding:.85rem 1.5rem;background:var(--background,oklch(1 0 0));border-bottom:1px solid var(--border,oklch(0.9 0.01 260))}
3208
+ .ds-header-brand{font-weight:700;font-size:1.05rem;color:var(--foreground,oklch(0.2 0 0));text-decoration:none}
3209
+ .ds-header-logo{display:block;max-height:2rem;width:auto}
3210
+ .ds-header-nav{display:flex;flex:1;gap:1.1rem;flex-wrap:wrap}
3211
+ .ds-header-nav a{color:var(--foreground,oklch(0.2 0 0));text-decoration:none;font-size:.9rem}
3212
+ .ds-footer{display:flex;gap:2rem;flex-wrap:wrap;justify-content:space-between;align-items:center;padding:2rem 1.5rem;background:var(--background,oklch(1 0 0));border-top:1px solid var(--border,oklch(0.9 0.01 260))}
3213
+ .ds-footer nav{display:flex;gap:1rem;flex-wrap:wrap}
3214
+ .ds-footer a{color:var(--muted-foreground,oklch(0.55 0.01 260));text-decoration:none;font-size:.875rem}
3215
+ .ds-hero{display:grid;gap:1.25rem;padding:2.5rem 0}
3216
+ .ds-hero img{display:block;width:100%;height:auto}
3217
+ .ds-hero-actions{display:flex;gap:.75rem;flex-wrap:wrap}
3218
+ .ds-image-text{display:grid;gap:1.5rem;align-items:center;padding:2rem 0}
3219
+ .ds-product-layout{display:grid;gap:2rem;padding:2rem 0}
3220
+ .ds-product-media img{display:block;width:100%;height:auto}
3221
+ .ds-product-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin-top:.5rem}
3222
+ .ds-product-info{display:grid;gap:1rem;align-content:start}
3223
+ .ds-product-page{padding:2rem 0}
3224
+ .ds-collection-page{padding:2rem 0}
3225
+ .ds-pagination{display:block;text-align:center;margin:1.75rem 0 0}
3226
+ .ds-pagination .page,.ds-pagination .next,.ds-pagination .prev{display:inline-block;padding:.35rem .7rem}
3227
+ .ds-pagination a{color:var(--foreground,oklch(0.2 0 0))}
3228
+ .ds-pagination .current{font-weight:700;color:var(--primary,oklch(0.25 0.01 260))}
3229
+ @media (min-width:750px){.ds-product-layout{grid-template-columns:1fr 1fr}.ds-image-text{grid-template-columns:1fr 1fr}}`;
3230
+ SNIPPET_BADGE = `{% comment %}
3231
+ Status label. Usage: {% render 'ds-badge', label: 'New' %}
3232
+ Optional tone: primary | outline | safe | warn | danger | unknown | verified
3233
+ {% endcomment %}
3234
+ {%- assign ds_badge_tone = tone | default: '' -%}
3235
+ <span class="ds ds-embed ds-badge{% if ds_badge_tone != '' %} ds-badge-{{ ds_badge_tone }}{% endif %}">{{ label }}</span>`;
3236
+ SNIPPET_PRICE = `{% comment %}
3237
+ Price with compare-at strike-through and a Sale badge.
3238
+ Usage: {% render 'ds-price', product: product %}
3239
+ Or: {% render 'ds-price', variant: product.selected_or_first_available_variant %}
3240
+ {% endcomment %}
3241
+ {%- liquid
3242
+ if variant
3243
+ assign ds_price = variant.price
3244
+ assign ds_compare_at = variant.compare_at_price
3245
+ else
3246
+ assign ds_price = product.price
3247
+ assign ds_compare_at = product.compare_at_price
3248
+ endif
3249
+ -%}
3250
+ <span class="ds ds-embed ds-price">
3251
+ <span class="ds-price-current">{{ ds_price | money }}</span>
3252
+ {%- if ds_compare_at > ds_price -%}
3253
+ <s class="ds-price-compare">{{ ds_compare_at | money }}</s>
3254
+ {% render 'ds-badge', label: 'Sale', tone: 'primary' %}
3255
+ {%- endif -%}
3256
+ </span>`;
3257
+ SNIPPET_BUY_BUTTON = `{% comment %}
3258
+ Add-to-cart form that works without JavaScript.
3259
+ Usage: {% render 'ds-buy-button', product: product %}
3260
+ Multi-variant products automatically get a native variant dropdown; pass
3261
+ variant_id to pin one variant instead. Optional label overrides the button
3262
+ text; pass show_quantity: true for a quantity field.
3263
+ {% endcomment %}
3264
+ {%- assign ds_label = label | default: 'Add to cart' -%}
3265
+ {%- if product.available -%}
3266
+ {% form 'product', product, class: 'ds ds-embed ds-buy-form' %}
3267
+ {%- if variant_id -%}
3268
+ <input type="hidden" name="id" value="{{ variant_id }}">
3269
+ {%- elsif product.has_only_default_variant -%}
3270
+ <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
3271
+ {%- else -%}
3272
+ {% render 'ds-variant-select', product: product %}
3273
+ {%- endif -%}
3274
+ {%- if show_quantity -%}
3275
+ <label class="ds-label" for="ds-qty-{{ product.id }}">Quantity</label>
3276
+ <input id="ds-qty-{{ product.id }}" class="ds-input ds-qty" type="number" name="quantity" value="1" min="1">
3277
+ {%- endif -%}
3278
+ <button type="submit" class="ds-btn ds-btn-primary">{{ ds_label }}</button>
3279
+ {% endform %}
3280
+ {%- else -%}
3281
+ <span class="ds ds-embed"><button type="button" class="ds-btn ds-btn-outline" disabled>Sold out</button></span>
3282
+ {%- endif -%}`;
3283
+ SNIPPET_VARIANT_SELECT = `{% comment %}
3284
+ Native variant dropdown for a product form; no JavaScript. Renders inside a
3285
+ {% form 'product' %} and posts the chosen variant as name="id".
3286
+ Usage: {% render 'ds-variant-select', product: product %}
3287
+ {% endcomment %}
3288
+ <label class="ds-label" for="ds-variant-{{ product.id }}">Options</label>
3289
+ <select id="ds-variant-{{ product.id }}" name="id" class="ds-select">
3290
+ {%- for variant in product.variants -%}
3291
+ <option value="{{ variant.id }}"{% if variant == product.selected_or_first_available_variant %} selected{% endif %}{% unless variant.available %} disabled{% endunless %}>
3292
+ {{ variant.title }} - {{ variant.price | money }}{% unless variant.available %} (sold out){% endunless %}
3293
+ </option>
3294
+ {%- endfor -%}
3295
+ </select>`;
3296
+ SNIPPET_PAGINATION = `{% comment %}
3297
+ Styled wrapper for Shopify's default pagination. Render inside a
3298
+ {% paginate %} block: {% render 'ds-pagination', paginate: paginate %}
3299
+ {% endcomment %}
3300
+ {%- if paginate.pages > 1 -%}
3301
+ <nav class="ds ds-embed ds-pagination" aria-label="Pagination">
3302
+ {{ paginate | default_pagination }}
3303
+ </nav>
3304
+ {%- endif -%}`;
3305
+ SNIPPET_PRODUCT_CARD = `{% comment %}
3306
+ Product tile for grids. Usage: {% render 'ds-product-card', product: product %}
3307
+ Optional: show_vendor: true, enable_quick_add: true
3308
+ {% endcomment %}
3309
+ <div class="ds ds-embed ds-card ds-product-card">
3310
+ <a href="{{ product.url }}" class="ds-card-media">
3311
+ {%- if product.featured_image -%}
3312
+ {%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
3313
+ {{ product.featured_image | image_url: width: 900 | image_tag: widths: '360, 540, 720, 900', sizes: '(min-width: 990px) 25vw, 50vw', alt: ds_image_alt }}
3314
+ {%- endif -%}
3315
+ </a>
3316
+ <div class="ds-card-body">
3317
+ {%- if show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
3318
+ <h3 class="ds-card-title"><a href="{{ product.url }}">{{ product.title }}</a></h3>
3319
+ {% render 'ds-price', product: product %}
3320
+ </div>
3321
+ {%- if enable_quick_add -%}
3322
+ <div class="ds-card-footer">
3323
+ {%- if product.has_only_default_variant -%}
3324
+ {% render 'ds-buy-button', product: product %}
3325
+ {%- else -%}
3326
+ <a href="{{ product.url }}" class="ds-btn ds-btn-outline">Choose options</a>
3327
+ {%- endif -%}
3328
+ </div>
3329
+ {%- endif -%}
3330
+ </div>`;
3331
+ SNIPPET_ANNOUNCEMENT = `{% comment %}
3332
+ Announcement bar. Usage: {% render 'ds-announcement', text: 'Free shipping over $150' %}
3333
+ Optional: link: '/collections/all', link_label: 'Shop now'
3334
+ {% endcomment %}
3335
+ <div class="ds ds-embed ds-announce">
3336
+ <p>{{ text }}{%- if link and link_label %} <a href="{{ link }}">{{ link_label }}</a>{% endif -%}</p>
3337
+ </div>`;
3338
+ SNIPPET_NEWSLETTER = `{% comment %}
3339
+ Email signup wired to Shopify customers.
3340
+ Usage: {% render 'ds-newsletter', id: section.id %}
3341
+ Optional: id, heading, button_label, success_text
3342
+ Pass id when the snippet can appear more than once on a page, so each
3343
+ email input keeps a unique id and its label stays correctly associated.
3344
+ {% endcomment %}
3345
+ {%- liquid
3346
+ assign ds_heading = heading | default: 'Subscribe to our emails'
3347
+ assign ds_button = button_label | default: 'Subscribe'
3348
+ assign ds_success = success_text | default: 'Thanks for subscribing.'
3349
+ if id
3350
+ assign ds_input_id = 'ds-newsletter-email-' | append: id
3351
+ else
3352
+ assign ds_input_id = 'ds-newsletter-email'
3353
+ endif
3354
+ -%}
3355
+ <div class="ds ds-embed ds-newsletter">
3356
+ <h2 class="ds-section-title">{{ ds_heading }}</h2>
3357
+ {% form 'customer', class: 'ds-field' %}
3358
+ <input type="hidden" name="contact[tags]" value="newsletter">
3359
+ {%- if form.posted_successfully? -%}
3360
+ <p role="status">{{ ds_success }}</p>
3361
+ {%- else -%}
3362
+ <div class="ds-control-row">
3363
+ <div>
3364
+ <label class="ds-label" for="{{ ds_input_id }}">Email</label>
3365
+ <input id="{{ ds_input_id }}" type="email" name="contact[email]" class="ds-input" autocomplete="email" required>
3366
+ </div>
3367
+ <button type="submit" class="ds-btn ds-btn-primary">{{ ds_button }}</button>
3368
+ </div>
3369
+ {%- if form.errors -%}<p class="ds-alert" role="alert">{{ form.errors | default_errors }}</p>{%- endif -%}
3370
+ {%- endif -%}
3371
+ {% endform %}
3372
+ </div>`;
3373
+ SECTION_FAQ = `{% comment %}
3374
+ FAQ accordion using the browser's native details element. Merchants add it
3375
+ from the theme editor as "DS FAQ".
3376
+ {% endcomment %}
3377
+ <div class="ds ds-embed ds-container ds-faq">
3378
+ {%- if section.settings.heading != blank -%}
3379
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3380
+ {%- endif -%}
3381
+ {%- for block in section.blocks -%}
3382
+ <details class="ds-faq-item" {{ block.shopify_attributes }}>
3383
+ <summary>{{ block.settings.question }}</summary>
3384
+ <div class="ds-faq-answer">{{ block.settings.answer }}</div>
3385
+ </details>
3386
+ {%- endfor -%}
3387
+ </div>
3388
+ {% schema %}
3389
+ {
3390
+ "name": "DS FAQ",
3391
+ "settings": [
3392
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Frequently asked questions" }
3393
+ ],
3394
+ "blocks": [
3395
+ {
3396
+ "type": "item",
3397
+ "name": "Question",
3398
+ "settings": [
3399
+ { "type": "text", "id": "question", "label": "Question" },
3400
+ { "type": "richtext", "id": "answer", "label": "Answer" }
3401
+ ]
3402
+ }
3403
+ ],
3404
+ "max_blocks": 20,
3405
+ "presets": [{ "name": "DS FAQ", "blocks": [{ "type": "item" }, { "type": "item" }] }]
3406
+ }
3407
+ {% endschema %}`;
3408
+ SECTION_FEATURED_COLLECTION = `{% comment %}
3409
+ Product grid from a merchant-chosen collection. Merchants add it from the
3410
+ theme editor as "DS Featured collection".
3411
+ {% endcomment %}
3412
+ {%- assign ds_collection = section.settings.collection -%}
3413
+ <div class="ds ds-embed ds-container ds-featured">
3414
+ {%- if section.settings.heading != blank -%}
3415
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3416
+ {%- endif -%}
3417
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
3418
+ {%- for product in ds_collection.products limit: section.settings.products_to_show -%}
3419
+ {% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
3420
+ {%- else -%}
3421
+ <p class="ds-supporting">Choose a collection in the theme editor.</p>
3422
+ {%- endfor -%}
3423
+ </div>
3424
+ </div>
3425
+ {% schema %}
3426
+ {
3427
+ "name": "DS Featured collection",
3428
+ "settings": [
3429
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Featured products" },
3430
+ { "type": "collection", "id": "collection", "label": "Collection" },
3431
+ { "type": "range", "id": "products_to_show", "min": 2, "max": 12, "step": 1, "default": 4, "label": "Products to show" },
3432
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
3433
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
3434
+ { "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false }
3435
+ ],
3436
+ "presets": [{ "name": "DS Featured collection" }]
3437
+ }
3438
+ {% endschema %}`;
3439
+ SECTION_HEADER = `{% comment %}
3440
+ Site header. Merchants add it to the theme's header group in the editor.
3441
+ {% endcomment %}
3442
+ <header class="ds ds-embed ds-header">
3443
+ <a class="ds-header-brand" href="{{ routes.root_url }}">
3444
+ {%- if section.settings.logo -%}
3445
+ {{ section.settings.logo | image_url: width: 300 | image_tag: alt: shop.name, class: 'ds-header-logo' }}
3446
+ {%- else -%}
3447
+ {{ shop.name }}
3448
+ {%- endif -%}
3449
+ </a>
3450
+ <nav class="ds-header-nav" aria-label="Main">
3451
+ {%- for link in section.settings.menu.links -%}
3452
+ <a href="{{ link.url }}"{% if link.current %} aria-current="page"{% endif %}>{{ link.title }}</a>
3453
+ {%- endfor -%}
3454
+ </nav>
3455
+ <a class="ds-btn ds-btn-outline" href="{{ routes.cart_url }}">Cart ({{ cart.item_count }})</a>
3456
+ </header>
3457
+ {% schema %}
3458
+ {
3459
+ "name": "DS Header",
3460
+ "settings": [
3461
+ { "type": "image_picker", "id": "logo", "label": "Logo" },
3462
+ { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }
3463
+ ],
3464
+ "presets": [{ "name": "DS Header" }]
3465
+ }
3466
+ {% endschema %}`;
3467
+ SECTION_FOOTER = `{% comment %}
3468
+ Site footer. Merchants add it to the theme's footer group in the editor.
3469
+ {% endcomment %}
3470
+ <footer class="ds ds-embed ds-footer">
3471
+ <span class="ds-header-brand">{{ shop.name }}</span>
3472
+ <nav aria-label="Footer">
3473
+ {%- for link in section.settings.menu.links -%}
3474
+ <a href="{{ link.url }}">{{ link.title }}</a>
3475
+ {%- endfor -%}
3476
+ </nav>
3477
+ <span class="ds-supporting">&copy; {{ 'now' | date: '%Y' }} {{ shop.name }}</span>
3478
+ </footer>
3479
+ {% schema %}
3480
+ {
3481
+ "name": "DS Footer",
3482
+ "settings": [
3483
+ { "type": "link_list", "id": "menu", "label": "Menu", "default": "footer" }
3484
+ ],
3485
+ "presets": [{ "name": "DS Footer" }]
3486
+ }
3487
+ {% endschema %}`;
3488
+ SECTION_HERO = `{% comment %}
3489
+ Hero with image, heading, supporting line, and up to 2 actions.
3490
+ {% endcomment %}
3491
+ <div class="ds ds-embed ds-container ds-hero">
3492
+ {%- if section.settings.image -%}
3493
+ {%- assign ds_image_alt = section.settings.heading | strip_html -%}
3494
+ {{ section.settings.image | image_url: width: 1600 | image_tag: widths: '750, 1100, 1600', sizes: '100vw', alt: ds_image_alt }}
3495
+ {%- endif -%}
3496
+ <h2 class="ds-display">{{ section.settings.heading }}</h2>
3497
+ {%- if section.settings.text != blank -%}<p class="ds-supporting">{{ section.settings.text }}</p>{%- endif -%}
3498
+ <div class="ds-hero-actions">
3499
+ {%- if section.settings.button_label != blank -%}
3500
+ <a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a>
3501
+ {%- endif -%}
3502
+ {%- if section.settings.secondary_label != blank -%}
3503
+ <a class="ds-btn ds-btn-outline" href="{{ section.settings.secondary_link }}">{{ section.settings.secondary_label }}</a>
3504
+ {%- endif -%}
3505
+ </div>
3506
+ </div>
3507
+ {% schema %}
3508
+ {
3509
+ "name": "DS Hero",
3510
+ "settings": [
3511
+ { "type": "image_picker", "id": "image", "label": "Image" },
3512
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Tell your story" },
3513
+ { "type": "text", "id": "text", "label": "Supporting line" },
3514
+ { "type": "text", "id": "button_label", "label": "Button label", "default": "Shop now" },
3515
+ { "type": "url", "id": "button_link", "label": "Button link" },
3516
+ { "type": "text", "id": "secondary_label", "label": "Secondary button label" },
3517
+ { "type": "url", "id": "secondary_link", "label": "Secondary button link" }
3518
+ ],
3519
+ "presets": [{ "name": "DS Hero" }]
3520
+ }
3521
+ {% endschema %}`;
3522
+ SECTION_RICH_TEXT = `{% comment %}
3523
+ Heading plus rich text, with an optional button.
3524
+ {% endcomment %}
3525
+ <div class="ds ds-embed ds-container" style="padding:2rem 0">
3526
+ {%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
3527
+ <div>{{ section.settings.text }}</div>
3528
+ {%- if section.settings.button_label != blank -%}
3529
+ <p><a class="ds-btn ds-btn-secondary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
3530
+ {%- endif -%}
3531
+ </div>
3532
+ {% schema %}
3533
+ {
3534
+ "name": "DS Rich text",
3535
+ "settings": [
3536
+ { "type": "inline_richtext", "id": "heading", "label": "Heading" },
3537
+ { "type": "richtext", "id": "text", "label": "Text" },
3538
+ { "type": "text", "id": "button_label", "label": "Button label" },
3539
+ { "type": "url", "id": "button_link", "label": "Button link" }
3540
+ ],
3541
+ "presets": [{ "name": "DS Rich text" }]
3542
+ }
3543
+ {% endschema %}`;
3544
+ SECTION_IMAGE_WITH_TEXT = `{% comment %}
3545
+ Image beside heading, text, and an optional button.
3546
+ {% endcomment %}
3547
+ <div class="ds ds-embed ds-container ds-image-text">
3548
+ {%- if section.settings.image -%}
3549
+ {%- assign ds_image_alt = section.settings.heading | strip_html -%}
3550
+ {{ section.settings.image | image_url: width: 900 | image_tag: widths: '480, 720, 900', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
3551
+ {%- endif -%}
3552
+ <div class="ds-product-info">
3553
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3554
+ <div>{{ section.settings.text }}</div>
3555
+ {%- if section.settings.button_label != blank -%}
3556
+ <p><a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
3557
+ {%- endif -%}
3558
+ </div>
3559
+ </div>
3560
+ {% schema %}
3561
+ {
3562
+ "name": "DS Image with text",
3563
+ "settings": [
3564
+ { "type": "image_picker", "id": "image", "label": "Image" },
3565
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Pair text with an image" },
3566
+ { "type": "richtext", "id": "text", "label": "Text" },
3567
+ { "type": "text", "id": "button_label", "label": "Button label" },
3568
+ { "type": "url", "id": "button_link", "label": "Button link" }
3569
+ ],
3570
+ "presets": [{ "name": "DS Image with text" }]
3571
+ }
3572
+ {% endschema %}`;
3573
+ SECTION_MAIN_PRODUCT = `{% comment %}
3574
+ Product page main section. Assign the "product.ds" template to a product in
3575
+ the admin, or add this section to an existing product template.
3576
+ {% endcomment %}
3577
+ <div class="ds ds-embed ds-container ds-product-page">
3578
+ <div class="ds-product-layout">
3579
+ <div class="ds-product-media">
3580
+ {%- if product.featured_image -%}
3581
+ {%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
3582
+ {{ product.featured_image | image_url: width: 1200 | image_tag: widths: '480, 720, 960, 1200', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
3583
+ {%- endif -%}
3584
+ {%- if product.images.size > 1 -%}
3585
+ <div class="ds-product-thumbs">
3586
+ {%- for image in product.images offset: 1 limit: 4 -%}
3587
+ {%- assign ds_thumb_alt = image.alt | default: product.title -%}
3588
+ {{ image | image_url: width: 300 | image_tag: alt: ds_thumb_alt }}
3589
+ {%- endfor -%}
3590
+ </div>
3591
+ {%- endif -%}
3592
+ </div>
3593
+ <div class="ds-product-info">
3594
+ {%- if section.settings.show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
3595
+ <h1 class="ds-display">{{ product.title }}</h1>
3596
+ {% render 'ds-price', variant: product.selected_or_first_available_variant %}
3597
+ {% render 'ds-buy-button', product: product, show_quantity: section.settings.show_quantity %}
3598
+ <div>{{ product.description }}</div>
3599
+ </div>
3600
+ </div>
3601
+ </div>
3602
+ {% schema %}
3603
+ {
3604
+ "name": "DS Product",
3605
+ "settings": [
3606
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": true },
3607
+ { "type": "checkbox", "id": "show_quantity", "label": "Show quantity field", "default": true }
3608
+ ],
3609
+ "enabled_on": { "templates": ["product"] }
3610
+ }
3611
+ {% endschema %}`;
3612
+ SECTION_MAIN_COLLECTION = `{% comment %}
3613
+ Collection page main section with pagination. Assign the "collection.ds"
3614
+ template to a collection in the admin, or add to an existing template.
3615
+ {% endcomment %}
3616
+ {% paginate collection.products by section.settings.products_per_page %}
3617
+ <div class="ds ds-embed ds-container ds-collection-page">
3618
+ <h1 class="ds-display">{{ collection.title }}</h1>
3619
+ {%- if collection.description != blank -%}<div class="ds-supporting">{{ collection.description }}</div>{%- endif -%}
3620
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}; margin-top: 1.5rem">
3621
+ {%- for product in collection.products -%}
3622
+ {% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
3623
+ {%- else -%}
3624
+ <p class="ds-supporting">No products found.</p>
3625
+ {%- endfor -%}
3626
+ </div>
3627
+ {% render 'ds-pagination', paginate: paginate %}
3628
+ </div>
3629
+ {% endpaginate %}
3630
+ {% schema %}
3631
+ {
3632
+ "name": "DS Collection",
3633
+ "settings": [
3634
+ { "type": "range", "id": "products_per_page", "min": 4, "max": 24, "step": 4, "default": 12, "label": "Products per page" },
3635
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
3636
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
3637
+ { "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false }
3638
+ ],
3639
+ "enabled_on": { "templates": ["collection"] }
3640
+ }
3641
+ {% endschema %}`;
3642
+ SECTION_CONTACT = `{% comment %}
3643
+ Contact form posting to Shopify's contact endpoint. No JavaScript.
3644
+ {% endcomment %}
3645
+ <div class="ds ds-embed ds-container" style="max-width:36rem;padding:2rem 0">
3646
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3647
+ {% form 'contact' %}
3648
+ {%- if form.posted_successfully? -%}
3649
+ <p role="status">Thanks for your message. We reply within one business day.</p>
3650
+ {%- else -%}
3651
+ {%- if form.errors -%}<p class="ds-alert" role="alert">{{ form.errors | default_errors }}</p>{%- endif -%}
3652
+ <div class="ds-field">
3653
+ <label class="ds-label" for="ds-contact-name-{{ section.id }}">Name</label>
3654
+ <input id="ds-contact-name-{{ section.id }}" class="ds-input" type="text" name="contact[name]" autocomplete="name">
3655
+ </div>
3656
+ <div class="ds-field">
3657
+ <label class="ds-label" for="ds-contact-email-{{ section.id }}">Email</label>
3658
+ <input id="ds-contact-email-{{ section.id }}" class="ds-input" type="email" name="contact[email]" autocomplete="email" required>
3659
+ </div>
3660
+ <div class="ds-field">
3661
+ <label class="ds-label" for="ds-contact-body-{{ section.id }}">Message</label>
3662
+ <textarea id="ds-contact-body-{{ section.id }}" class="ds-textarea" name="contact[body]" required></textarea>
3663
+ </div>
3664
+ <button type="submit" class="ds-btn ds-btn-primary">Send message</button>
3665
+ {%- endif -%}
3666
+ {% endform %}
3667
+ </div>
3668
+ {% schema %}
3669
+ {
3670
+ "name": "DS Contact",
3671
+ "settings": [
3672
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Contact us" }
3673
+ ],
3674
+ "presets": [{ "name": "DS Contact" }]
3675
+ }
3676
+ {% endschema %}`;
3677
+ SECTION_MULTICOLUMN = `{% comment %}
3678
+ Row of text cards, for value props or feature callouts.
3679
+ {% endcomment %}
3680
+ <div class="ds ds-embed ds-container" style="padding:2rem 0">
3681
+ {%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
3682
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
3683
+ {%- for block in section.blocks -%}
3684
+ <div class="ds-card" {{ block.shopify_attributes }}>
3685
+ <div class="ds-card-body">
3686
+ <h3 class="ds-card-title">{{ block.settings.heading }}</h3>
3687
+ <p class="ds-card-desc">{{ block.settings.text }}</p>
3688
+ </div>
3689
+ </div>
3690
+ {%- endfor -%}
3691
+ </div>
3692
+ </div>
3693
+ {% schema %}
3694
+ {
3695
+ "name": "DS Multicolumn",
3696
+ "settings": [
3697
+ { "type": "inline_richtext", "id": "heading", "label": "Heading" },
3698
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 4, "step": 1, "default": 3, "label": "Columns on desktop" }
3699
+ ],
3700
+ "blocks": [
3701
+ {
3702
+ "type": "column",
3703
+ "name": "Column",
3704
+ "settings": [
3705
+ { "type": "text", "id": "heading", "label": "Heading" },
3706
+ { "type": "text", "id": "text", "label": "Text" }
3707
+ ]
3708
+ }
3709
+ ],
3710
+ "max_blocks": 8,
3711
+ "presets": [{ "name": "DS Multicolumn", "blocks": [{ "type": "column" }, { "type": "column" }, { "type": "column" }] }]
3712
+ }
3713
+ {% endschema %}`;
3714
+ TEMPLATE_PRODUCT_JSON = `{
3715
+ "sections": {
3716
+ "main": {
3717
+ "type": "ds-main-product",
3718
+ "settings": {}
3719
+ }
3720
+ },
3721
+ "order": ["main"]
3722
+ }`;
3723
+ TEMPLATE_COLLECTION_JSON = `{
3724
+ "sections": {
3725
+ "main": {
3726
+ "type": "ds-main-collection",
3727
+ "settings": {}
3728
+ }
3729
+ },
3730
+ "order": ["main"]
3731
+ }`;
3732
+ SNIPPETS = [
3733
+ { path: "snippets/ds-badge.liquid", content: SNIPPET_BADGE },
3734
+ { path: "snippets/ds-price.liquid", content: SNIPPET_PRICE },
3735
+ { path: "snippets/ds-variant-select.liquid", content: SNIPPET_VARIANT_SELECT },
3736
+ { path: "snippets/ds-buy-button.liquid", content: SNIPPET_BUY_BUTTON },
3737
+ { path: "snippets/ds-product-card.liquid", content: SNIPPET_PRODUCT_CARD },
3738
+ { path: "snippets/ds-announcement.liquid", content: SNIPPET_ANNOUNCEMENT },
3739
+ { path: "snippets/ds-newsletter.liquid", content: SNIPPET_NEWSLETTER },
3740
+ { path: "snippets/ds-pagination.liquid", content: SNIPPET_PAGINATION },
3741
+ { path: "sections/ds-header.liquid", content: SECTION_HEADER },
3742
+ { path: "sections/ds-footer.liquid", content: SECTION_FOOTER },
3743
+ { path: "sections/ds-hero.liquid", content: SECTION_HERO },
3744
+ { path: "sections/ds-rich-text.liquid", content: SECTION_RICH_TEXT },
3745
+ { path: "sections/ds-image-with-text.liquid", content: SECTION_IMAGE_WITH_TEXT },
3746
+ { path: "sections/ds-multicolumn.liquid", content: SECTION_MULTICOLUMN },
3747
+ { path: "sections/ds-contact.liquid", content: SECTION_CONTACT },
3748
+ { path: "sections/ds-faq.liquid", content: SECTION_FAQ },
3749
+ { path: "sections/ds-featured-collection.liquid", content: SECTION_FEATURED_COLLECTION },
3750
+ { path: "sections/ds-main-product.liquid", content: SECTION_MAIN_PRODUCT },
3751
+ { path: "sections/ds-main-collection.liquid", content: SECTION_MAIN_COLLECTION },
3752
+ { path: "templates/product.ds.json", content: TEMPLATE_PRODUCT_JSON },
3753
+ { path: "templates/collection.ds.json", content: TEMPLATE_COLLECTION_JSON }
3754
+ ];
3755
+ SHOPIFY_KIT_CONDUCT_MD = [
3756
+ "## Install into the merchant's theme (mandatory order)",
3757
+ "1. Write `assets/ds-kit.css` into the theme's `assets/` directory, every `snippets/ds-*.liquid` file into `snippets/`, every `sections/ds-*.liquid` file into `sections/`, and the `templates/*.ds.json` files into `templates/`. Copy file contents VERBATIM; retyped Liquid silently drops this system's material.",
3758
+ "2. Load the stylesheet once in `layout/theme.liquid`, before `</head>`: `{{ 'ds-kit.css' | asset_url | stylesheet_tag }}`.",
3759
+ '3. Merchants place everything from the theme editor: "DS Header" and "DS Footer" go into the theme\'s header and footer section groups; "DS Hero", "DS Featured collection", "DS Multicolumn", "DS Image with text", "DS Rich text", "DS Contact", and "DS FAQ" appear in the Add section list on any page. Any snippet also works from the built-in Custom Liquid section with one `{% render %}` line.',
3760
+ "4. For fully branded product and collection pages, assign the `product.ds` and `collection.ds` templates to products and collections in the admin. They render `ds-main-product` and `ds-main-collection`, which include a native no-JavaScript variant dropdown and pagination.",
3761
+ "",
3762
+ "## Class contract (pass/fail)",
3763
+ "- Do not rename `ds-*` classes and do not strip the `ds ds-embed` wrapper a snippet renders on its root; the wrapper scopes this system's tokens and keeps the background transparent inside existing theme sections.",
3764
+ "- Custom CSS is layout-only (position, spacing, grid). Never restyle a `ds-*` component's geometry or surface; those carry this system's identity.",
3765
+ `- ${HOVER_POLICY}`,
3766
+ `- Every visible text color keeps at least ${MINIMUM_TEXT_CONTRAST} contrast against its computed background. Colors come from the system's tokens, never invented values.`,
3767
+ "",
3768
+ "## Commerce correctness",
3769
+ "- `ds-buy-button` works without JavaScript on every product: multi-variant products get a native variant dropdown automatically, or pass `variant_id` to pin one. The product card's quick add links to the product page for multi-variant products.",
3770
+ "- Without JavaScript the displayed price and image do not update when a shopper changes the variant dropdown; the correct variant is still added to the cart, and cart and checkout show its real price. State this to merchants rather than papering over it.",
3771
+ "- Prices always render through the `money` filter, never hand-formatted.",
3772
+ "- Images always render through `image_url` with explicit widths; `img_url` is deprecated.",
3773
+ "",
3774
+ "## Not in this kit, on purpose",
3775
+ "Nothing here ships JavaScript. Sliding cart drawers, live variant image swapping, and instant search need scripts, and every theme already has its own; competing scripts are the least portable surface in Shopify. The kit's forms, dropdowns, quantity fields, accordions, and pagination all work as native HTML."
3776
+ ].join("\n");
3777
+ }
3778
+ });
3779
+
3009
3780
  // ../shared/src/mcp/preview-widget-sdk.generated.ts
3010
3781
  var PREVIEW_WIDGET_SDK_JS;
3011
3782
  var init_preview_widget_sdk_generated = __esm({
@@ -5329,7 +6100,7 @@ function completedHtmlKitStructuredContent(name, viewUrl, tokensCss, componentMa
5329
6100
  requiredWrapperClass: "ds",
5330
6101
  requiredComponentClassPrefix: "ds-",
5331
6102
  customCssScope: "layout-only",
5332
- hoverPolicy: "Put every :hover rule inside @media (hover: hover) and (pointer: fine); never move controls or override selected/pressed styling. For selectable controls, unselected hover stays neutral: only strengthen the neutral surface or edge, never changes text to the primary color, and never borrows the primary/accent fill. Do not add app-specific :hover rules to ds-* controls; use the kit states verbatim.",
6103
+ hoverPolicy: HOVER_POLICY,
5333
6104
  hostIsolationCssTemplate: HOST_ISOLATION_CSS_TEMPLATE,
5334
6105
  minimumTextContrast: MINIMUM_TEXT_CONTRAST,
5335
6106
  textColorPolicy: INLINE_TEXT_COLOR_POLICY,
@@ -5341,6 +6112,22 @@ function completedHtmlKitStructuredContent(name, viewUrl, tokensCss, componentMa
5341
6112
  tokensCss
5342
6113
  };
5343
6114
  }
6115
+ function completedShopifyKitStructuredContent(name, viewUrl, tokensCss, themeFiles) {
6116
+ return {
6117
+ status: "completed",
6118
+ name,
6119
+ viewUrl,
6120
+ buildInstructions: SHOPIFY_KIT_CONDUCT_MD,
6121
+ requiredWrapperClass: "ds",
6122
+ requiredComponentClassPrefix: "ds-",
6123
+ customCssScope: "layout-only",
6124
+ hoverPolicy: HOVER_POLICY,
6125
+ minimumTextContrast: MINIMUM_TEXT_CONTRAST,
6126
+ textColorPolicy: INLINE_TEXT_COLOR_POLICY,
6127
+ themeFiles,
6128
+ tokensCss
6129
+ };
6130
+ }
5344
6131
  async function fetchCatalog(designSystemId) {
5345
6132
  try {
5346
6133
  const [globalsRes, ...componentResults] = await Promise.all([
@@ -5385,7 +6172,7 @@ function registerDesignSystemTools({
5385
6172
  tracked,
5386
6173
  track,
5387
6174
  authContext
5388
- }) {
6175
+ }, { enableShopifyKit = true } = {}) {
5389
6176
  const searchInputSchema = z5.object({
5390
6177
  query: z5.string().describe(
5391
6178
  "The user's app, UI, industry, or visual direction. Examples: 'World Cup betting app for friends', 'warm editorial ecommerce', or 'professional fintech dashboard'."
@@ -5555,7 +6342,7 @@ ${BUILD_TARGET_RULE}` : ""}`
5555
6342
 
5556
6343
  `;
5557
6344
  const longestRun = Math.max(0, ...[...ds.metadata.html.matchAll(/`+/g)].map((m) => m[0].length));
5558
- const fence2 = "`".repeat(Math.max(3, longestRun + 1));
6345
+ const fence3 = "`".repeat(Math.max(3, longestRun + 1));
5559
6346
  const { tokensCss: tokensCss2, files: files2 } = await fetchCatalog(designSystemId);
5560
6347
  const componentMaterialCss = buildDsComponentCss(files2).css;
5561
6348
  return {
@@ -5563,9 +6350,9 @@ ${BUILD_TARGET_RULE}` : ""}`
5563
6350
  // document so hosted agents cannot truncate it away. [BD-240]
5564
6351
  content: [{ type: "text", text: `${steer}${intro}${HTML_KIT_CONDUCT_MD}
5565
6352
 
5566
- ${fence2}html
6353
+ ${fence3}html
5567
6354
  ${ds.metadata.html}
5568
- ${fence2}` }],
6355
+ ${fence3}` }],
5569
6356
  structuredContent: completedHtmlKitStructuredContent(
5570
6357
  ds.metadata.title,
5571
6358
  viewUrl,
@@ -5599,13 +6386,85 @@ ${fence2}` }],
5599
6386
  )
5600
6387
  };
5601
6388
  });
6389
+ const getShopifyKit = async ({
6390
+ designSystemId,
6391
+ projectId,
6392
+ context
6393
+ }) => tracked("get-design-system-kit", { target: "shopify", designSystemId, projectId, context }, async () => {
6394
+ if (projectId) {
6395
+ const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
6396
+ const result = await fetchProjectTokensCss(projectId, authContext);
6397
+ if (!result?.tokensCss) {
6398
+ return {
6399
+ content: [{ type: "text", text: `Could not load tokens for project "${projectId}". It may still be generating, or the id is wrong. Check ${viewUrl2}.` }],
6400
+ structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
6401
+ };
6402
+ }
6403
+ const componentKit2 = buildDsComponentCss(result.files);
6404
+ const name = result.name ?? `Project ${projectId.slice(0, 8)}`;
6405
+ const text2 = buildDsShopifyKit({
6406
+ name,
6407
+ tokensCss: result.tokensCss,
6408
+ viewUrl: viewUrl2,
6409
+ source: { kind: "project", projectId },
6410
+ componentKit: componentKit2
6411
+ });
6412
+ return {
6413
+ content: [{ type: "text", text: text2 }],
6414
+ structuredContent: completedShopifyKitStructuredContent(
6415
+ name,
6416
+ viewUrl2,
6417
+ result.tokensCss,
6418
+ buildDsShopifyFiles({ tokensCss: result.tokensCss, componentKit: componentKit2 })
6419
+ )
6420
+ };
6421
+ }
6422
+ if (!designSystemId) {
6423
+ return {
6424
+ content: [{ type: "text", text: "For target shopify, pass either a catalog designSystemId (from find-design-system) or a projectId (from create-design-system)." }],
6425
+ structuredContent: { status: "error", name: "Missing design system", tokensCss: "" }
6426
+ };
6427
+ }
6428
+ const ds = await provider.getDesignSystemById(designSystemId, { includeComponents: false });
6429
+ if (!ds) {
6430
+ return {
6431
+ content: [{ type: "text", text: `Design system "${designSystemId}" not found. Call find-design-system first to get a valid id, or pass a projectId for a generated system.` }],
6432
+ structuredContent: { status: "error", name: designSystemId, tokensCss: "" }
6433
+ };
6434
+ }
6435
+ const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
6436
+ const { tokensCss, files } = await fetchCatalog(designSystemId);
6437
+ if (!tokensCss) {
6438
+ return {
6439
+ content: [{ type: "text", text: `Could not load tokens for "${designSystemId}". The full live showcase is at ${viewUrl}.` }],
6440
+ structuredContent: { status: "error", name: ds.metadata.title, viewUrl, tokensCss: "" }
6441
+ };
6442
+ }
6443
+ const componentKit = buildDsComponentCss(files);
6444
+ const text = buildDsShopifyKit({
6445
+ name: ds.metadata.title,
6446
+ tokensCss,
6447
+ viewUrl,
6448
+ source: { kind: "catalog", designSystemId },
6449
+ componentKit
6450
+ });
6451
+ return {
6452
+ content: [{ type: "text", text }],
6453
+ structuredContent: completedShopifyKitStructuredContent(
6454
+ ds.metadata.title,
6455
+ viewUrl,
6456
+ tokensCss,
6457
+ buildDsShopifyFiles({ tokensCss, componentKit })
6458
+ )
6459
+ };
6460
+ });
5602
6461
  server.registerTool(
5603
6462
  "get-design-system-kit",
5604
6463
  {
5605
6464
  title: "Get a Design System Build Kit",
5606
6465
  description: `Return the correct build kit for a design system through one capability-based entry point.
5607
6466
 
5608
- 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.
6467
+ 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.` : ""}
5609
6468
 
5610
6469
  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.
5611
6470
 
@@ -5630,14 +6489,17 @@ same components/ui/ paths.
5630
6489
 
5631
6490
  ${BUILD_TARGET_RULE}`,
5632
6491
  inputSchema: z5.object({
5633
- target: z5.enum(["react", "html"]).describe('Required build target. Use "react" for any host with filesystem + terminal; use "html" only for inline chat canvases/artifacts.'),
5634
- designSystemId: z5.string().min(1).optional().describe("Catalog design system ID from find-design-system. Required for target react; for target html pass this or projectId."),
5635
- projectId: z5.string().min(1).optional().describe('Generated project ID from create-design-system / get-design-system-status. Supported only with target "html".'),
6492
+ // Off-pilot callers must not see the target at all, not merely be refused it.
6493
+ target: z5.enum(
6494
+ enableShopifyKit ? ["react", "html", "shopify"] : ["react", "html"]
6495
+ ).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.' : ""}`),
6496
+ designSystemId: z5.string().min(1).optional().describe(`Catalog design system ID from find-design-system. Required for target react; for target${enableShopifyKit ? "s html and shopify" : " html"} pass this or projectId.`),
6497
+ projectId: z5.string().min(1).optional().describe(`Generated project ID from create-design-system / get-design-system-status. Supported with target${enableShopifyKit ? 's "html" and "shopify"' : ' "html"'}.`),
5636
6498
  component: z5.union([z5.string(), z5.array(z5.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."),
5637
6499
  library: z5.enum(["radix", "base-ui"]).optional().describe('React target only, default "base-ui". Pass "radix" when the project already uses Radix primitives.'),
5638
6500
  context: CONTEXT_ARG
5639
6501
  }),
5640
- outputSchema: BUILD_KIT_OUTPUT_SCHEMA,
6502
+ outputSchema: buildKitOutputSchema(enableShopifyKit),
5641
6503
  annotations: { readOnlyHint: true }
5642
6504
  // This tool returns build instructions, not a visual preview. Attaching
5643
6505
  // the preview template makes ChatGPT render a nested showcase iframe;
@@ -5649,6 +6511,15 @@ ${BUILD_TARGET_RULE}`,
5649
6511
  if (target === "html") {
5650
6512
  return getHtmlKit({ designSystemId, projectId, context });
5651
6513
  }
6514
+ if (target === "shopify") {
6515
+ if (!enableShopifyKit) {
6516
+ return {
6517
+ content: [{ type: "text", text: 'The Shopify target is in a private pilot and is not enabled for this account. Use target "react" to build the storefront as components, or ask Better Design for pilot access.' }],
6518
+ structuredContent: { status: "error", name: "Shopify target unavailable", tokensCss: "" }
6519
+ };
6520
+ }
6521
+ return getShopifyKit({ designSystemId, projectId, context });
6522
+ }
5652
6523
  if (!designSystemId) {
5653
6524
  return {
5654
6525
  content: [{ type: "text", text: 'For target "react", pass a catalog designSystemId from find-design-system.' }],
@@ -6089,6 +6960,7 @@ var init_design_system_tools = __esm({
6089
6960
  "use strict";
6090
6961
  init_guardrails();
6091
6962
  init_html_kit();
6963
+ init_shopify_kit();
6092
6964
  init_preview_html();
6093
6965
  init_shared();
6094
6966
  REGISTRY_ITEM_SCHEMA = z5.object({
@@ -6673,6 +7545,19 @@ var init_extract_tools = __esm({
6673
7545
 
6674
7546
  // ../shared/src/mcp/tools/illustration-tools.ts
6675
7547
  import { z as z7 } from "zod";
7548
+ function fail(message) {
7549
+ return {
7550
+ content: [{ type: "text", text: message }],
7551
+ structuredContent: {
7552
+ faults: [message],
7553
+ layers: [],
7554
+ reviewed: false,
7555
+ unseen: true,
7556
+ reviewToken: "",
7557
+ message
7558
+ }
7559
+ };
7560
+ }
6676
7561
  function registerIllustrationTools({ server, track, tracked, authContext }) {
6677
7562
  const authHeaders2 = () => {
6678
7563
  const headers = { "Content-Type": "application/json" };
@@ -6683,215 +7568,104 @@ function registerIllustrationTools({ server, track, tracked, authContext }) {
6683
7568
  return headers;
6684
7569
  };
6685
7570
  server.registerTool(
6686
- "draw-illustration",
7571
+ "review-illustration",
6687
7572
  {
6688
- title: "Draw an Illustration",
6689
- description: `Draw an original SVG illustration for a UI surface: an empty state, a hero object, a container something drops into.
7573
+ title: "Review an Illustration",
7574
+ description: `Look at an SVG illustration you drew and get back what is visibly wrong with it.
6690
7575
 
6691
- Use this whenever a screen needs a picture and you would otherwise leave a placeholder box, an emoji, or a stock icon standing in for artwork.
7576
+ You write SVG blind. This renders it in a real browser and judges it against this project's illustration principles, so you find out that a shape does not close, a part floats unattached, the light comes from two directions, or contents sit outside the container meant to hold them.
6692
7577
 
6693
- The drawing is original, follows this project's illustration principles (light from one direction, seated shadows, containers split into back and front layers), and arrives in named layers so you can animate its parts.
7578
+ Use it whenever you have drawn an illustration, before you hand it to anyone. Drawing something worth shipping usually takes 2 or 3 rounds.
6694
7579
 
6695
7580
  How to call it:
6696
- 1. \`draw-illustration({ prompt: "something for the empty inbox screen" })\`. Write the prompt the way a person would ask; it does not need art direction.
6697
- 2. Pass \`projectId\` when the caller has a generated design system, and the drawing uses their brand colours. Without it the drawing uses neutral colours.
6698
- 3. Wait about a minute, then call \`get-illustration-status\` with the returned id to collect the finished svg.
7581
+ 1. Get the rules first with \`get-ui-principle({ topic: "svg-illustration" })\` and draw against them.
7582
+ 2. \`review-illustration({ svg, intent: "an open inbox tray with letters" })\`. Say what the drawing is meant to be, in your own words.
7583
+ 3. Fix what \`faults\` names, then call again passing \`reviewToken\` back. Reviewing the same drawing again costs nothing while that token is valid.
7584
+ 4. Stop when \`faults\` is empty.
6699
7585
 
6700
- Subjects: objects, tools, containers and scenes. People and faces are declined, because the principles this draws from do not cover them.`,
7586
+ 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.`,
6701
7587
  inputSchema: z7.object({
6702
- prompt: z7.string().min(1).describe('What the screen needs, in plain words. For example "something for the empty inbox screen".'),
6703
- projectId: z7.string().optional().describe("Generated project id from create-design-system. Supplies the brand palette."),
6704
- style: z7.enum(STYLES).optional().describe("flat, soft-depth or glossy. Defaults to the design system's character, or soft-depth."),
6705
- animated: z7.boolean().optional().describe("When true, the svg ships with entrance and ambient motion, and a reduced-motion off switch.")
7588
+ svg: z7.string().min(1).describe("The complete svg element you drew."),
7589
+ intent: z7.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
7590
+ animated: z7.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
7591
+ reviewToken: z7.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
6706
7592
  }),
6707
- outputSchema: DRAW_OUTPUT_SCHEMA
7593
+ outputSchema: REVIEW_OUTPUT_SCHEMA,
7594
+ annotations: { readOnlyHint: true }
6708
7595
  },
6709
- async ({ prompt, projectId, style, animated }) => tracked("draw-illustration", { query: prompt, hasProject: Boolean(projectId), style, animated }, async () => {
6710
- const startUrl = `${APP_BASE_URL}/api/illustrations`;
6711
- try {
6712
- const res = await fetch(startUrl, {
6713
- method: "POST",
6714
- headers: authHeaders2(),
6715
- signal: AbortSignal.timeout(15e3),
6716
- body: JSON.stringify({ prompt, projectId, style, animated: animated ?? false })
6717
- });
6718
- if (!res.ok) {
6719
- const detail = await res.text().catch(() => "");
7596
+ async ({
7597
+ svg,
7598
+ intent,
7599
+ animated,
7600
+ reviewToken
7601
+ }) => tracked(
7602
+ "review-illustration",
7603
+ { query: intent, animated, repeat: Boolean(reviewToken) },
7604
+ async () => {
7605
+ try {
7606
+ const res = await fetch(`${APP_BASE_URL}/api/illustrations/review`, {
7607
+ method: "POST",
7608
+ headers: authHeaders2(),
7609
+ signal: AbortSignal.timeout(6e4),
7610
+ body: JSON.stringify({ svg, intent, animated: animated ?? false, reviewToken })
7611
+ });
7612
+ if (!res.ok) {
7613
+ const detail = await res.text().catch(() => "");
7614
+ return fail(`Could not review that drawing (HTTP ${res.status}). ${detail.slice(0, 300)}`);
7615
+ }
7616
+ const body = await res.json();
7617
+ const faults = body.faults ?? [];
7618
+ const layers = body.layers ?? [];
7619
+ const message = !body.reviewed ? faults[0] ?? "That drawing was not reviewed." : faults.length === 0 ? "Nothing visibly wrong. The drawing is ready to use." : [
7620
+ `${faults.length} thing${faults.length === 1 ? "" : "s"} to fix:`,
7621
+ ...faults.map((f) => `- ${f}`),
7622
+ body.unseen ? "The drawing could not be rendered, so only its structure was checked." : "",
7623
+ body.reviewToken ? "Fix these and call again with the same reviewToken, which costs nothing." : "Fix these and call again."
7624
+ ].filter(Boolean).join("\n");
6720
7625
  return {
6721
7626
  content: [
6722
7627
  {
6723
7628
  type: "text",
6724
- text: `Could not start the drawing (HTTP ${res.status}). ${detail.slice(0, 400)}`
7629
+ text: layers.length ? `${message}
7630
+
7631
+ Layers, in paint order: ${layers.join(", ")}.` : message
6725
7632
  }
6726
7633
  ],
6727
7634
  structuredContent: {
6728
- status: "error",
6729
- illustrationId: "",
6730
- svg: "",
6731
- layers: [],
6732
- subject: "",
6733
- message: `Could not start the drawing (HTTP ${res.status}).`
6734
- }
6735
- };
6736
- }
6737
- const body = await res.json();
6738
- if (!body.id) {
6739
- return {
6740
- content: [{ type: "text", text: "The drawing service did not return an id." }],
6741
- structuredContent: {
6742
- status: "error",
6743
- illustrationId: "",
6744
- svg: "",
6745
- layers: [],
6746
- subject: "",
6747
- message: "The drawing service did not return an id."
6748
- }
6749
- };
6750
- }
6751
- const next = `Drawing started. Wait about a minute, then call get-illustration-status with id ${body.id}.`;
6752
- return {
6753
- content: [{ type: "text", text: next }],
6754
- structuredContent: {
6755
- status: "drawing",
6756
- illustrationId: body.id,
6757
- svg: "",
6758
- layers: [],
6759
- subject: "",
6760
- message: next
6761
- }
6762
- };
6763
- } catch (err) {
6764
- const message = `Could not reach the drawing service: ${err instanceof Error ? err.message : String(err)}`;
6765
- return {
6766
- content: [{ type: "text", text: message }],
6767
- structuredContent: {
6768
- status: "error",
6769
- illustrationId: "",
6770
- svg: "",
6771
- layers: [],
6772
- subject: "",
6773
- message
6774
- }
6775
- };
6776
- }
6777
- })
6778
- );
6779
- server.registerTool(
6780
- "get-illustration-status",
6781
- {
6782
- title: "Get Illustration Status",
6783
- description: `Collect a drawing started by draw-illustration.
6784
-
6785
- Call this about a minute after starting. While it is still drawing, wait and call again rather than starting a second drawing.
6786
-
6787
- When ready, \`svg\` holds the complete element: paste it into a component as it is. \`layers\` names the groups in paint order, so you know which part to animate. For a container, the back group renders behind whatever falls in and the front group in front of it, which is what makes something look like it landed inside.`,
6788
- inputSchema: z7.object({
6789
- illustrationId: z7.string().min(1).describe("The id returned by draw-illustration.")
6790
- }),
6791
- outputSchema: DRAW_OUTPUT_SCHEMA,
6792
- annotations: { readOnlyHint: true }
6793
- },
6794
- async ({ illustrationId }) => tracked("get-illustration-status", { illustrationId }, async () => {
6795
- try {
6796
- const res = await fetch(`${APP_BASE_URL}/api/illustrations/${illustrationId}`, {
6797
- headers: authHeaders2(),
6798
- signal: AbortSignal.timeout(15e3)
6799
- });
6800
- if (!res.ok) {
6801
- const message = `Could not read that drawing (HTTP ${res.status}).`;
6802
- return {
6803
- content: [{ type: "text", text: message }],
6804
- structuredContent: {
6805
- status: "error",
6806
- illustrationId,
6807
- svg: "",
6808
- layers: [],
6809
- subject: "",
6810
- message
6811
- }
6812
- };
6813
- }
6814
- const body = await res.json();
6815
- if ((body.status === "ready" || body.status === "partial") && body.svg) {
6816
- const layers = body.layers ?? [];
6817
- const partial = body.status === "partial";
6818
- const summary = [
6819
- partial ? `Drawing done, with reservations: ${body.subject ?? "illustration"}. It ran out of correction passes, so look it over before shipping it.` : `Drawing ready: ${body.subject ?? "illustration"}.`,
6820
- layers.length ? `Layers, in paint order: ${layers.join(", ")}.` : "",
6821
- "Paste the svg into a component. Keep the group ids so parts stay animatable."
6822
- ].filter(Boolean).join(" ");
6823
- return {
6824
- content: [{ type: "text", text: `${summary}
6825
-
6826
- ${body.svg}` }],
6827
- structuredContent: {
6828
- status: partial ? "partial" : "ready",
6829
- illustrationId,
6830
- svg: body.svg,
7635
+ faults,
6831
7636
  layers,
6832
- subject: body.subject ?? "",
6833
- message: summary
6834
- }
6835
- };
6836
- }
6837
- if (body.status === "error") {
6838
- const message = body.error ?? "The drawing failed.";
6839
- return {
6840
- content: [{ type: "text", text: message }],
6841
- structuredContent: {
6842
- status: "error",
6843
- illustrationId,
6844
- svg: "",
6845
- layers: [],
6846
- subject: "",
7637
+ reviewed: body.reviewed ?? false,
7638
+ unseen: body.unseen ?? false,
7639
+ reviewToken: body.reviewToken ?? "",
6847
7640
  message
6848
7641
  }
6849
7642
  };
7643
+ } catch (err) {
7644
+ return fail(
7645
+ `Could not reach the review service: ${err instanceof Error ? err.message : String(err)}`
7646
+ );
6850
7647
  }
6851
- const waiting = "Still drawing. Wait another thirty seconds and call this again.";
6852
- return {
6853
- content: [{ type: "text", text: waiting }],
6854
- structuredContent: {
6855
- status: "drawing",
6856
- illustrationId,
6857
- svg: "",
6858
- layers: [],
6859
- subject: "",
6860
- message: waiting
6861
- }
6862
- };
6863
- } catch (err) {
6864
- const message = `Could not reach the drawing service: ${err instanceof Error ? err.message : String(err)}`;
6865
- return {
6866
- content: [{ type: "text", text: message }],
6867
- structuredContent: {
6868
- status: "error",
6869
- illustrationId,
6870
- svg: "",
6871
- layers: [],
6872
- subject: "",
6873
- message
6874
- }
6875
- };
6876
7648
  }
6877
- })
7649
+ )
6878
7650
  );
6879
7651
  void track;
6880
7652
  }
6881
- var STYLES, DRAW_OUTPUT_SCHEMA;
7653
+ var REVIEW_OUTPUT_SCHEMA;
6882
7654
  var init_illustration_tools = __esm({
6883
7655
  "../shared/src/mcp/tools/illustration-tools.ts"() {
6884
7656
  "use strict";
6885
7657
  init_shared();
6886
- STYLES = ["flat", "soft-depth", "glossy"];
6887
- DRAW_OUTPUT_SCHEMA = {
6888
- status: z7.enum(["drawing", "ready", "partial", "error"]),
6889
- illustrationId: z7.string(),
6890
- /** Present once ready. The complete svg element, ready to paste. */
6891
- svg: z7.string(),
7658
+ REVIEW_OUTPUT_SCHEMA = {
7659
+ /** Empty when nothing is visibly wrong. */
7660
+ faults: z7.array(z7.string()),
6892
7661
  /** Group ids in paint order, the parts an animation can target. */
6893
7662
  layers: z7.array(z7.string()),
6894
- subject: z7.string(),
7663
+ /** False when the markup was refused; faults says why. */
7664
+ reviewed: z7.boolean(),
7665
+ /** True when the drawing could not be rendered, so only structure was checked. */
7666
+ unseen: z7.boolean(),
7667
+ /** Pass this back to review the same drawing again without spending another credit. */
7668
+ reviewToken: z7.string(),
6895
7669
  message: z7.string()
6896
7670
  };
6897
7671
  }
@@ -6951,7 +7725,7 @@ function registerAllTools(server, provider, onToolUsage, authContext, options =
6951
7725
  registerPrincipleTools(deps, options);
6952
7726
  registerComprehensionTools(deps);
6953
7727
  registerIconTools(deps);
6954
- registerDesignSystemTools(deps);
7728
+ registerDesignSystemTools(deps, options);
6955
7729
  registerExtractTools(deps);
6956
7730
  registerIllustrationTools(deps);
6957
7731
  registerPreviewResource(deps);
@@ -6987,14 +7761,18 @@ function createMcpServer(options) {
6987
7761
  version = "3.0.0",
6988
7762
  onToolUsage,
6989
7763
  authContext,
6990
- enableReactNativeGuide = true
7764
+ enableReactNativeGuide = true,
7765
+ enableThreeJsGuide = true,
7766
+ enableShopifyKit = true
6991
7767
  } = options;
6992
7768
  const server = new McpServer(
6993
7769
  { name, version },
6994
- { instructions: getMcpInstructions(enableReactNativeGuide) }
7770
+ { instructions: getMcpInstructions({ enableReactNativeGuide, enableThreeJsGuide }) }
6995
7771
  );
6996
7772
  registerAllTools(server, provider, onToolUsage, authContext, {
6997
- enableReactNativeGuide
7773
+ enableReactNativeGuide,
7774
+ enableThreeJsGuide,
7775
+ enableShopifyKit
6998
7776
  });
6999
7777
  return server;
7000
7778
  }