better-design 0.12.0 → 0.15.1

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;
@@ -1004,7 +1037,7 @@ function truncateToTokens(text, maxTokens) {
1004
1037
 
1005
1038
  ...(truncated to ~${maxTokens} tokens)`;
1006
1039
  }
1007
- var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_DOMAINS, WIDGET_FRAME_DOMAINS, WIDGET_APP_DOMAIN, REQUIREMENTS_MD, ANTI_MOCKUP_MD, BUILD_TARGET_RULE, CODING_HOST_RE, PREVIEW_OUTPUT_SCHEMA, BUILD_KIT_OUTPUT_SCHEMA, CONTEXT_ARG, SEARCH_TOOLS;
1040
+ var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_DOMAINS, WIDGET_FRAME_DOMAINS, WIDGET_APP_DOMAIN, REQUIREMENTS_MD, ANTI_MOCKUP_MD, BUILD_TARGET_RULE, CODING_HOST_RE, PREVIEW_OUTPUT_SCHEMA, BUILD_KIT_OUTPUT_SCHEMA, CONTEXT_ARG, READ_ONLY_TOOL_ANNOTATIONS, SEARCH_TOOLS;
1008
1041
  var init_shared = __esm({
1009
1042
  "../shared/src/mcp/tools/shared.ts"() {
1010
1043
  "use strict";
@@ -1049,29 +1082,22 @@ 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
  );
1089
+ READ_ONLY_TOOL_ANNOTATIONS = {
1090
+ readOnlyHint: true,
1091
+ destructiveHint: false,
1092
+ openWorldHint: false
1093
+ };
1070
1094
  SEARCH_TOOLS = /* @__PURE__ */ new Set([
1071
1095
  "find-design-system",
1072
1096
  "get-ui-principle",
1073
1097
  "get-ux-principle",
1074
1098
  "get-react-native-guide",
1099
+ "get-three-js-guide",
1100
+ "get-motion-guide",
1075
1101
  "get-review-rules",
1076
1102
  "find-icon-library",
1077
1103
  "find-icons",
@@ -1161,7 +1187,7 @@ function formatTopicNotFound(topic, principles, toolName, resourceLabel) {
1161
1187
  }
1162
1188
  async function runPrincipleTool(provider, area, label, toolName, args) {
1163
1189
  const { topic, query, maxTokens } = args;
1164
- const resourceLabel = area === "react-native" ? "Guides" : "Principles";
1190
+ const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" ? "Guides" : "Principles";
1165
1191
  if (topic && query) {
1166
1192
  return {
1167
1193
  response: {
@@ -1236,7 +1262,7 @@ ${formatted}`;
1236
1262
  capture: { topResults: [] }
1237
1263
  };
1238
1264
  }
1239
- function registerPrincipleTools({ server, provider, track }, { enableReactNativeGuide = true } = {}) {
1265
+ function registerPrincipleTools({ server, provider, track }, { enableReactNativeGuide = true, enableThreeJsGuide = true } = {}) {
1240
1266
  server.registerTool(
1241
1267
  "get-ui-principle",
1242
1268
  {
@@ -1257,7 +1283,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
1257
1283
  maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1258
1284
  context: CONTEXT_ARG
1259
1285
  }),
1260
- annotations: { readOnlyHint: true }
1286
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1261
1287
  },
1262
1288
  async ({ topic, query, maxTokens, context }) => {
1263
1289
  const start = Date.now();
@@ -1298,7 +1324,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
1298
1324
  maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1299
1325
  context: CONTEXT_ARG
1300
1326
  }),
1301
- annotations: { readOnlyHint: true }
1327
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1302
1328
  },
1303
1329
  async ({ topic, query, maxTokens, context }) => {
1304
1330
  const start = Date.now();
@@ -1344,7 +1370,7 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
1344
1370
  maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1345
1371
  context: CONTEXT_ARG
1346
1372
  }),
1347
- annotations: { readOnlyHint: true }
1373
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1348
1374
  },
1349
1375
  async ({ topic, query, maxTokens, context }) => {
1350
1376
  const start = Date.now();
@@ -1377,6 +1403,116 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
1377
1403
  }
1378
1404
  );
1379
1405
  }
1406
+ if (enableThreeJsGuide) {
1407
+ server.registerTool(
1408
+ "get-three-js-guide",
1409
+ {
1410
+ title: "Get Three.js Guide",
1411
+ description: `Load implementation guidance for Three.js and React Three Fiber projects.
1412
+
1413
+ 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.
1414
+
1415
+ This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
1416
+
1417
+ You can either:
1418
+ 1. Request a known topic: get-three-js-guide({ topic: "fundamentals" })
1419
+ 2. Use an alias: get-three-js-guide({ topic: "webgpu and tsl" })
1420
+ 3. Search for the current task or code example: get-three-js-guide({ query: "code example for a dissolve shader effect" })
1421
+ 4. Get the full topic index: get-three-js-guide({})
1422
+
1423
+ Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
1424
+ inputSchema: z2.object({
1425
+ 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."),
1426
+ query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the Three.js or React Three Fiber task or problem."),
1427
+ maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1428
+ context: CONTEXT_ARG
1429
+ }),
1430
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1431
+ },
1432
+ async ({ topic, query, maxTokens, context }) => {
1433
+ const start = Date.now();
1434
+ try {
1435
+ const { response, capture } = await runPrincipleTool(
1436
+ provider,
1437
+ "three-js",
1438
+ "Three.js",
1439
+ "get-three-js-guide",
1440
+ { topic, query, maxTokens }
1441
+ );
1442
+ return trackSearch(
1443
+ track,
1444
+ "get-three-js-guide",
1445
+ { ...capture, context },
1446
+ start,
1447
+ response
1448
+ );
1449
+ } catch (error) {
1450
+ return trackSearch(
1451
+ track,
1452
+ "get-three-js-guide",
1453
+ { query, topic, topResults: [], context },
1454
+ start,
1455
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1456
+ true,
1457
+ error instanceof Error ? error.message : String(error)
1458
+ );
1459
+ }
1460
+ }
1461
+ );
1462
+ }
1463
+ server.registerTool(
1464
+ "get-motion-guide",
1465
+ {
1466
+ title: "Get Motion Guide",
1467
+ description: `Load guidance for building a product demo or promo video in code.
1468
+
1469
+ 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.
1470
+
1471
+ The guidance is distilled from building a real 55-second product film, so it names the failures in the order you will hit them.
1472
+
1473
+ 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.
1474
+
1475
+ This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
1476
+
1477
+ You can either:
1478
+ 1. Request a known topic: get-motion-guide({ topic: "fundamentals" })
1479
+ 2. Use an alias: get-motion-guide({ topic: "cut on the beat" })
1480
+ 3. Search for the current task or problem: get-motion-guide({ query: "my animated word swap clips the longer word" })
1481
+ 4. Get the full topic index: get-motion-guide({})
1482
+
1483
+ Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
1484
+ inputSchema: z2.object({
1485
+ 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."),
1486
+ query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the video or animation task or problem."),
1487
+ maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
1488
+ context: CONTEXT_ARG
1489
+ }),
1490
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1491
+ },
1492
+ async ({ topic, query, maxTokens, context }) => {
1493
+ const start = Date.now();
1494
+ try {
1495
+ const { response, capture } = await runPrincipleTool(
1496
+ provider,
1497
+ "motion",
1498
+ "Motion",
1499
+ "get-motion-guide",
1500
+ { topic, query, maxTokens }
1501
+ );
1502
+ return trackSearch(track, "get-motion-guide", { ...capture, context }, start, response);
1503
+ } catch (error) {
1504
+ return trackSearch(
1505
+ track,
1506
+ "get-motion-guide",
1507
+ { query, topic, topResults: [], context },
1508
+ start,
1509
+ { content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : "Unknown error"}` }] },
1510
+ true,
1511
+ error instanceof Error ? error.message : String(error)
1512
+ );
1513
+ }
1514
+ }
1515
+ );
1380
1516
  server.registerTool(
1381
1517
  "get-review-rules",
1382
1518
  {
@@ -1419,7 +1555,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
1419
1555
  maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000\u201310000."),
1420
1556
  context: CONTEXT_ARG
1421
1557
  }),
1422
- annotations: { readOnlyHint: true }
1558
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1423
1559
  },
1424
1560
  async ({ category = "all", maxTokens, context }) => {
1425
1561
  const start = Date.now();
@@ -1976,7 +2112,7 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
1976
2112
  knownTerms: z3.array(z3.string().trim().min(1).max(60)).max(50).optional().describe("Words this audience already knows, so they are not flagged as insider language."),
1977
2113
  context: CONTEXT_ARG
1978
2114
  }),
1979
- annotations: { readOnlyHint: true }
2115
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
1980
2116
  },
1981
2117
  async ({ context, ...args }) => {
1982
2118
  const start = Date.now();
@@ -2161,7 +2297,7 @@ Examples:
2161
2297
  inputSchema: z4.object({
2162
2298
  query: z4.string().describe("Style description or personality traits to match.")
2163
2299
  }),
2164
- annotations: { readOnlyHint: true }
2300
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
2165
2301
  };
2166
2302
  const makeIconLibraryHandler = (toolName) => async ({ query }) => {
2167
2303
  const start = Date.now();
@@ -2237,7 +2373,7 @@ Returns icons formatted for use with Iconify React. If you pass a variant, Bette
2237
2373
  libraryId: z4.string().describe("The icon library ID from find-icon-library"),
2238
2374
  variant: z4.string().optional().describe("Optional variant style (e.g., 'bold', 'outline', 'duotone').")
2239
2375
  }),
2240
- annotations: { readOnlyHint: true }
2376
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
2241
2377
  };
2242
2378
  const makeIconSearchHandler = (toolName) => async ({ query, libraryId, variant }) => {
2243
2379
  const start = Date.now();
@@ -2321,7 +2457,7 @@ If \`components/icons/index.ts\` already exists in the project, MERGE the new ex
2321
2457
  variant: z4.string().optional().describe("Optional library variant (e.g. 'outline', 'filled')."),
2322
2458
  context: CONTEXT_ARG
2323
2459
  }),
2324
- annotations: { readOnlyHint: true }
2460
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
2325
2461
  },
2326
2462
  async ({ libraryId, names, targetDir, format, variant, context }) => tracked(
2327
2463
  "install-icons",
@@ -2526,7 +2662,7 @@ Returns either:
2526
2662
  manifestContent: z4.string().optional().describe("Contents of the project's components/icons/.library.json file, if it exists. The agent should attempt to read this file from disk and pass it here. If the file doesn't exist or can't be read, omit this field."),
2527
2663
  context: CONTEXT_ARG
2528
2664
  }),
2529
- annotations: { readOnlyHint: true }
2665
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
2530
2666
  },
2531
2667
  async ({ manifestContent, context }) => tracked("detect-icon-library", { hasManifest: Boolean(manifestContent), context }, async () => {
2532
2668
  if (!manifestContent || !manifestContent.trim()) {
@@ -2806,11 +2942,12 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
2806
2942
  Full live showcase: ${viewUrl}` : ""
2807
2943
  ].join("\n");
2808
2944
  }
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;
2945
+ 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
2946
  var init_html_kit = __esm({
2811
2947
  "../shared/src/mcp/html-kit.ts"() {
2812
2948
  "use strict";
2813
2949
  MINIMUM_TEXT_CONTRAST = "4.5:1";
2950
+ 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
2951
  INLINE_TEXT_COLOR_POLICY = [
2815
2952
  "Use semantic foreground tokens for text: `--foreground` on the page, `--card-foreground` on cards, and `--muted-foreground` for secondary copy.",
2816
2953
  "The background, card, muted, secondary, accent, and input tokens are surfaces: never use surface tokens as text colors.",
@@ -3006,6 +3143,645 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
3006
3143
  }
3007
3144
  });
3008
3145
 
3146
+ // ../shared/src/mcp/shopify-kit.ts
3147
+ function fence2(lang, body) {
3148
+ return `\`\`\`${lang}
3149
+ ${body}
3150
+ \`\`\``;
3151
+ }
3152
+ function buildDsShopifyFiles(input) {
3153
+ const materialCss = input.componentKit?.css?.trim();
3154
+ const assetCss = [
3155
+ compactTokensCss(input.tokensCss),
3156
+ DS_KIT_CSS,
3157
+ DS_SHOPIFY_CSS,
3158
+ ...materialCss ? [materialCss] : []
3159
+ ].join("\n\n");
3160
+ return [{ path: "assets/ds-kit.css", content: assetCss }, ...SNIPPETS];
3161
+ }
3162
+ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
3163
+ const files = buildDsShopifyFiles({ tokensCss, componentKit });
3164
+ const sourceLine = source?.kind === "catalog" ? `Source designSystemId: \`${source.designSystemId}\`` : source?.kind === "project" ? `Source projectId: \`${source.projectId}\`` : null;
3165
+ const fileBlocks = files.flatMap((f) => [
3166
+ `### \`${f.path}\``,
3167
+ "",
3168
+ fence2(f.path.endsWith(".css") ? "css" : f.path.endsWith(".json") ? "json" : "liquid", f.content),
3169
+ ""
3170
+ ]);
3171
+ return [
3172
+ `# ${name}: Shopify theme kit`,
3173
+ "",
3174
+ "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.",
3175
+ ...sourceLine ? ["", sourceLine] : [],
3176
+ "",
3177
+ SHOPIFY_KIT_CONDUCT_MD,
3178
+ "",
3179
+ "## Theme files",
3180
+ "",
3181
+ ...fileBlocks,
3182
+ "## After installing",
3183
+ "- Preview an unpublished copy of the theme first; never edit the live theme directly.",
3184
+ "- The kit styles parts of a storefront; page composition, cart mechanics, and checkout stay with the theme.",
3185
+ ...viewUrl ? ["", `Full live showcase: ${viewUrl}`] : []
3186
+ ].join("\n");
3187
+ }
3188
+ 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;
3189
+ var init_shopify_kit = __esm({
3190
+ "../shared/src/mcp/shopify-kit.ts"() {
3191
+ "use strict";
3192
+ init_html_kit();
3193
+ DS_SHOPIFY_CSS = `.ds.ds-embed{background:transparent}
3194
+ .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}
3195
+ .ds-announce p{margin:0}
3196
+ .ds-announce a{color:inherit;text-decoration:underline}
3197
+ .ds-price{display:inline-flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}
3198
+ .ds-price-current{font-weight:600;color:var(--foreground,oklch(0.2 0 0))}
3199
+ .ds-price-compare{color:var(--muted-foreground,oklch(0.55 0.01 260))}
3200
+ .ds-card-media{display:block}
3201
+ .ds-card-media img{display:block;width:100%;height:auto}
3202
+ .ds-product-card .ds-card-title a{color:inherit;text-decoration:none}
3203
+ .ds-faq-item{border-bottom:1px solid var(--border,oklch(0.9 0.01 260));padding:.25rem 0}
3204
+ .ds-faq-item summary{cursor:pointer;padding:.75rem 0;font-weight:600;color:var(--foreground,oklch(0.2 0 0))}
3205
+ .ds-faq-answer{padding:0 0 1rem}
3206
+ .ds-grid{display:grid;grid-template-columns:repeat(var(--ds-cols,4),1fr);gap:1rem}
3207
+ @media (max-width:749px){.ds-grid{grid-template-columns:repeat(2,1fr)}}
3208
+ .ds-newsletter .ds-control-row{margin-top:.75rem}
3209
+ .ds-buy-form{margin:0;display:grid;gap:.75rem;justify-items:start}
3210
+ .ds-featured{padding:2rem 0}
3211
+ .ds-qty{width:6rem}
3212
+ .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))}
3213
+ .ds-header-brand{font-weight:700;font-size:1.05rem;color:var(--foreground,oklch(0.2 0 0));text-decoration:none}
3214
+ .ds-header-logo{display:block;max-height:2rem;width:auto}
3215
+ .ds-header-nav{display:flex;flex:1;gap:1.1rem;flex-wrap:wrap}
3216
+ .ds-header-nav a{color:var(--foreground,oklch(0.2 0 0));text-decoration:none;font-size:.9rem}
3217
+ .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))}
3218
+ .ds-footer nav{display:flex;gap:1rem;flex-wrap:wrap}
3219
+ .ds-footer a{color:var(--muted-foreground,oklch(0.55 0.01 260));text-decoration:none;font-size:.875rem}
3220
+ .ds-hero{display:grid;gap:1.25rem;padding:2.5rem 0}
3221
+ .ds-hero img{display:block;width:100%;height:auto}
3222
+ .ds-hero-actions{display:flex;gap:.75rem;flex-wrap:wrap}
3223
+ .ds-image-text{display:grid;gap:1.5rem;align-items:center;padding:2rem 0}
3224
+ .ds-product-layout{display:grid;gap:2rem;padding:2rem 0}
3225
+ .ds-product-media img{display:block;width:100%;height:auto}
3226
+ .ds-product-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin-top:.5rem}
3227
+ .ds-product-info{display:grid;gap:1rem;align-content:start}
3228
+ .ds-product-page{padding:2rem 0}
3229
+ .ds-collection-page{padding:2rem 0}
3230
+ .ds-pagination{display:block;text-align:center;margin:1.75rem 0 0}
3231
+ .ds-pagination .page,.ds-pagination .next,.ds-pagination .prev{display:inline-block;padding:.35rem .7rem}
3232
+ .ds-pagination a{color:var(--foreground,oklch(0.2 0 0))}
3233
+ .ds-pagination .current{font-weight:700;color:var(--primary,oklch(0.25 0.01 260))}
3234
+ @media (min-width:750px){.ds-product-layout{grid-template-columns:1fr 1fr}.ds-image-text{grid-template-columns:1fr 1fr}}`;
3235
+ SNIPPET_BADGE = `{% comment %}
3236
+ Status label. Usage: {% render 'ds-badge', label: 'New' %}
3237
+ Optional tone: primary | outline | safe | warn | danger | unknown | verified
3238
+ {% endcomment %}
3239
+ {%- assign ds_badge_tone = tone | default: '' -%}
3240
+ <span class="ds ds-embed ds-badge{% if ds_badge_tone != '' %} ds-badge-{{ ds_badge_tone }}{% endif %}">{{ label }}</span>`;
3241
+ SNIPPET_PRICE = `{% comment %}
3242
+ Price with compare-at strike-through and a Sale badge.
3243
+ Usage: {% render 'ds-price', product: product %}
3244
+ Or: {% render 'ds-price', variant: product.selected_or_first_available_variant %}
3245
+ {% endcomment %}
3246
+ {%- liquid
3247
+ if variant
3248
+ assign ds_price = variant.price
3249
+ assign ds_compare_at = variant.compare_at_price
3250
+ else
3251
+ assign ds_price = product.price
3252
+ assign ds_compare_at = product.compare_at_price
3253
+ endif
3254
+ -%}
3255
+ <span class="ds ds-embed ds-price">
3256
+ <span class="ds-price-current">{{ ds_price | money }}</span>
3257
+ {%- if ds_compare_at > ds_price -%}
3258
+ <s class="ds-price-compare">{{ ds_compare_at | money }}</s>
3259
+ {% render 'ds-badge', label: 'Sale', tone: 'primary' %}
3260
+ {%- endif -%}
3261
+ </span>`;
3262
+ SNIPPET_BUY_BUTTON = `{% comment %}
3263
+ Add-to-cart form that works without JavaScript.
3264
+ Usage: {% render 'ds-buy-button', product: product %}
3265
+ Multi-variant products automatically get a native variant dropdown; pass
3266
+ variant_id to pin one variant instead. Optional label overrides the button
3267
+ text; pass show_quantity: true for a quantity field.
3268
+ {% endcomment %}
3269
+ {%- assign ds_label = label | default: 'Add to cart' -%}
3270
+ {%- if product.available -%}
3271
+ {% form 'product', product, class: 'ds ds-embed ds-buy-form' %}
3272
+ {%- if variant_id -%}
3273
+ <input type="hidden" name="id" value="{{ variant_id }}">
3274
+ {%- elsif product.has_only_default_variant -%}
3275
+ <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
3276
+ {%- else -%}
3277
+ {% render 'ds-variant-select', product: product %}
3278
+ {%- endif -%}
3279
+ {%- if show_quantity -%}
3280
+ <label class="ds-label" for="ds-qty-{{ product.id }}">Quantity</label>
3281
+ <input id="ds-qty-{{ product.id }}" class="ds-input ds-qty" type="number" name="quantity" value="1" min="1">
3282
+ {%- endif -%}
3283
+ <button type="submit" class="ds-btn ds-btn-primary">{{ ds_label }}</button>
3284
+ {% endform %}
3285
+ {%- else -%}
3286
+ <span class="ds ds-embed"><button type="button" class="ds-btn ds-btn-outline" disabled>Sold out</button></span>
3287
+ {%- endif -%}`;
3288
+ SNIPPET_VARIANT_SELECT = `{% comment %}
3289
+ Native variant dropdown for a product form; no JavaScript. Renders inside a
3290
+ {% form 'product' %} and posts the chosen variant as name="id".
3291
+ Usage: {% render 'ds-variant-select', product: product %}
3292
+ {% endcomment %}
3293
+ <label class="ds-label" for="ds-variant-{{ product.id }}">Options</label>
3294
+ <select id="ds-variant-{{ product.id }}" name="id" class="ds-select">
3295
+ {%- for variant in product.variants -%}
3296
+ <option value="{{ variant.id }}"{% if variant == product.selected_or_first_available_variant %} selected{% endif %}{% unless variant.available %} disabled{% endunless %}>
3297
+ {{ variant.title }} - {{ variant.price | money }}{% unless variant.available %} (sold out){% endunless %}
3298
+ </option>
3299
+ {%- endfor -%}
3300
+ </select>`;
3301
+ SNIPPET_PAGINATION = `{% comment %}
3302
+ Styled wrapper for Shopify's default pagination. Render inside a
3303
+ {% paginate %} block: {% render 'ds-pagination', paginate: paginate %}
3304
+ {% endcomment %}
3305
+ {%- if paginate.pages > 1 -%}
3306
+ <nav class="ds ds-embed ds-pagination" aria-label="Pagination">
3307
+ {{ paginate | default_pagination }}
3308
+ </nav>
3309
+ {%- endif -%}`;
3310
+ SNIPPET_PRODUCT_CARD = `{% comment %}
3311
+ Product tile for grids. Usage: {% render 'ds-product-card', product: product %}
3312
+ Optional: show_vendor: true, enable_quick_add: true
3313
+ {% endcomment %}
3314
+ <div class="ds ds-embed ds-card ds-product-card">
3315
+ <a href="{{ product.url }}" class="ds-card-media">
3316
+ {%- if product.featured_image -%}
3317
+ {%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
3318
+ {{ product.featured_image | image_url: width: 900 | image_tag: widths: '360, 540, 720, 900', sizes: '(min-width: 990px) 25vw, 50vw', alt: ds_image_alt }}
3319
+ {%- endif -%}
3320
+ </a>
3321
+ <div class="ds-card-body">
3322
+ {%- if show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
3323
+ <h3 class="ds-card-title"><a href="{{ product.url }}">{{ product.title }}</a></h3>
3324
+ {% render 'ds-price', product: product %}
3325
+ </div>
3326
+ {%- if enable_quick_add -%}
3327
+ <div class="ds-card-footer">
3328
+ {%- if product.has_only_default_variant -%}
3329
+ {% render 'ds-buy-button', product: product %}
3330
+ {%- else -%}
3331
+ <a href="{{ product.url }}" class="ds-btn ds-btn-outline">Choose options</a>
3332
+ {%- endif -%}
3333
+ </div>
3334
+ {%- endif -%}
3335
+ </div>`;
3336
+ SNIPPET_ANNOUNCEMENT = `{% comment %}
3337
+ Announcement bar. Usage: {% render 'ds-announcement', text: 'Free shipping over $150' %}
3338
+ Optional: link: '/collections/all', link_label: 'Shop now'
3339
+ {% endcomment %}
3340
+ <div class="ds ds-embed ds-announce">
3341
+ <p>{{ text }}{%- if link and link_label %} <a href="{{ link }}">{{ link_label }}</a>{% endif -%}</p>
3342
+ </div>`;
3343
+ SNIPPET_NEWSLETTER = `{% comment %}
3344
+ Email signup wired to Shopify customers.
3345
+ Usage: {% render 'ds-newsletter', id: section.id %}
3346
+ Optional: id, heading, button_label, success_text
3347
+ Pass id when the snippet can appear more than once on a page, so each
3348
+ email input keeps a unique id and its label stays correctly associated.
3349
+ {% endcomment %}
3350
+ {%- liquid
3351
+ assign ds_heading = heading | default: 'Subscribe to our emails'
3352
+ assign ds_button = button_label | default: 'Subscribe'
3353
+ assign ds_success = success_text | default: 'Thanks for subscribing.'
3354
+ if id
3355
+ assign ds_input_id = 'ds-newsletter-email-' | append: id
3356
+ else
3357
+ assign ds_input_id = 'ds-newsletter-email'
3358
+ endif
3359
+ -%}
3360
+ <div class="ds ds-embed ds-newsletter">
3361
+ <h2 class="ds-section-title">{{ ds_heading }}</h2>
3362
+ {% form 'customer', class: 'ds-field' %}
3363
+ <input type="hidden" name="contact[tags]" value="newsletter">
3364
+ {%- if form.posted_successfully? -%}
3365
+ <p role="status">{{ ds_success }}</p>
3366
+ {%- else -%}
3367
+ <div class="ds-control-row">
3368
+ <div>
3369
+ <label class="ds-label" for="{{ ds_input_id }}">Email</label>
3370
+ <input id="{{ ds_input_id }}" type="email" name="contact[email]" class="ds-input" autocomplete="email" required>
3371
+ </div>
3372
+ <button type="submit" class="ds-btn ds-btn-primary">{{ ds_button }}</button>
3373
+ </div>
3374
+ {%- if form.errors -%}<p class="ds-alert" role="alert">{{ form.errors | default_errors }}</p>{%- endif -%}
3375
+ {%- endif -%}
3376
+ {% endform %}
3377
+ </div>`;
3378
+ SECTION_FAQ = `{% comment %}
3379
+ FAQ accordion using the browser's native details element. Merchants add it
3380
+ from the theme editor as "DS FAQ".
3381
+ {% endcomment %}
3382
+ <div class="ds ds-embed ds-container ds-faq">
3383
+ {%- if section.settings.heading != blank -%}
3384
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3385
+ {%- endif -%}
3386
+ {%- for block in section.blocks -%}
3387
+ <details class="ds-faq-item" {{ block.shopify_attributes }}>
3388
+ <summary>{{ block.settings.question }}</summary>
3389
+ <div class="ds-faq-answer">{{ block.settings.answer }}</div>
3390
+ </details>
3391
+ {%- endfor -%}
3392
+ </div>
3393
+ {% schema %}
3394
+ {
3395
+ "name": "DS FAQ",
3396
+ "settings": [
3397
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Frequently asked questions" }
3398
+ ],
3399
+ "blocks": [
3400
+ {
3401
+ "type": "item",
3402
+ "name": "Question",
3403
+ "settings": [
3404
+ { "type": "text", "id": "question", "label": "Question" },
3405
+ { "type": "richtext", "id": "answer", "label": "Answer" }
3406
+ ]
3407
+ }
3408
+ ],
3409
+ "max_blocks": 20,
3410
+ "presets": [{ "name": "DS FAQ", "blocks": [{ "type": "item" }, { "type": "item" }] }]
3411
+ }
3412
+ {% endschema %}`;
3413
+ SECTION_FEATURED_COLLECTION = `{% comment %}
3414
+ Product grid from a merchant-chosen collection. Merchants add it from the
3415
+ theme editor as "DS Featured collection".
3416
+ {% endcomment %}
3417
+ {%- assign ds_collection = section.settings.collection -%}
3418
+ <div class="ds ds-embed ds-container ds-featured">
3419
+ {%- if section.settings.heading != blank -%}
3420
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3421
+ {%- endif -%}
3422
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
3423
+ {%- for product in ds_collection.products limit: section.settings.products_to_show -%}
3424
+ {% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
3425
+ {%- else -%}
3426
+ <p class="ds-supporting">Choose a collection in the theme editor.</p>
3427
+ {%- endfor -%}
3428
+ </div>
3429
+ </div>
3430
+ {% schema %}
3431
+ {
3432
+ "name": "DS Featured collection",
3433
+ "settings": [
3434
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Featured products" },
3435
+ { "type": "collection", "id": "collection", "label": "Collection" },
3436
+ { "type": "range", "id": "products_to_show", "min": 2, "max": 12, "step": 1, "default": 4, "label": "Products to show" },
3437
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
3438
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
3439
+ { "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false }
3440
+ ],
3441
+ "presets": [{ "name": "DS Featured collection" }]
3442
+ }
3443
+ {% endschema %}`;
3444
+ SECTION_HEADER = `{% comment %}
3445
+ Site header. Merchants add it to the theme's header group in the editor.
3446
+ {% endcomment %}
3447
+ <header class="ds ds-embed ds-header">
3448
+ <a class="ds-header-brand" href="{{ routes.root_url }}">
3449
+ {%- if section.settings.logo -%}
3450
+ {{ section.settings.logo | image_url: width: 300 | image_tag: alt: shop.name, class: 'ds-header-logo' }}
3451
+ {%- else -%}
3452
+ {{ shop.name }}
3453
+ {%- endif -%}
3454
+ </a>
3455
+ <nav class="ds-header-nav" aria-label="Main">
3456
+ {%- for link in section.settings.menu.links -%}
3457
+ <a href="{{ link.url }}"{% if link.current %} aria-current="page"{% endif %}>{{ link.title }}</a>
3458
+ {%- endfor -%}
3459
+ </nav>
3460
+ <a class="ds-btn ds-btn-outline" href="{{ routes.cart_url }}">Cart ({{ cart.item_count }})</a>
3461
+ </header>
3462
+ {% schema %}
3463
+ {
3464
+ "name": "DS Header",
3465
+ "settings": [
3466
+ { "type": "image_picker", "id": "logo", "label": "Logo" },
3467
+ { "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }
3468
+ ],
3469
+ "presets": [{ "name": "DS Header" }]
3470
+ }
3471
+ {% endschema %}`;
3472
+ SECTION_FOOTER = `{% comment %}
3473
+ Site footer. Merchants add it to the theme's footer group in the editor.
3474
+ {% endcomment %}
3475
+ <footer class="ds ds-embed ds-footer">
3476
+ <span class="ds-header-brand">{{ shop.name }}</span>
3477
+ <nav aria-label="Footer">
3478
+ {%- for link in section.settings.menu.links -%}
3479
+ <a href="{{ link.url }}">{{ link.title }}</a>
3480
+ {%- endfor -%}
3481
+ </nav>
3482
+ <span class="ds-supporting">&copy; {{ 'now' | date: '%Y' }} {{ shop.name }}</span>
3483
+ </footer>
3484
+ {% schema %}
3485
+ {
3486
+ "name": "DS Footer",
3487
+ "settings": [
3488
+ { "type": "link_list", "id": "menu", "label": "Menu", "default": "footer" }
3489
+ ],
3490
+ "presets": [{ "name": "DS Footer" }]
3491
+ }
3492
+ {% endschema %}`;
3493
+ SECTION_HERO = `{% comment %}
3494
+ Hero with image, heading, supporting line, and up to 2 actions.
3495
+ {% endcomment %}
3496
+ <div class="ds ds-embed ds-container ds-hero">
3497
+ {%- if section.settings.image -%}
3498
+ {%- assign ds_image_alt = section.settings.heading | strip_html -%}
3499
+ {{ section.settings.image | image_url: width: 1600 | image_tag: widths: '750, 1100, 1600', sizes: '100vw', alt: ds_image_alt }}
3500
+ {%- endif -%}
3501
+ <h2 class="ds-display">{{ section.settings.heading }}</h2>
3502
+ {%- if section.settings.text != blank -%}<p class="ds-supporting">{{ section.settings.text }}</p>{%- endif -%}
3503
+ <div class="ds-hero-actions">
3504
+ {%- if section.settings.button_label != blank -%}
3505
+ <a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a>
3506
+ {%- endif -%}
3507
+ {%- if section.settings.secondary_label != blank -%}
3508
+ <a class="ds-btn ds-btn-outline" href="{{ section.settings.secondary_link }}">{{ section.settings.secondary_label }}</a>
3509
+ {%- endif -%}
3510
+ </div>
3511
+ </div>
3512
+ {% schema %}
3513
+ {
3514
+ "name": "DS Hero",
3515
+ "settings": [
3516
+ { "type": "image_picker", "id": "image", "label": "Image" },
3517
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Tell your story" },
3518
+ { "type": "text", "id": "text", "label": "Supporting line" },
3519
+ { "type": "text", "id": "button_label", "label": "Button label", "default": "Shop now" },
3520
+ { "type": "url", "id": "button_link", "label": "Button link" },
3521
+ { "type": "text", "id": "secondary_label", "label": "Secondary button label" },
3522
+ { "type": "url", "id": "secondary_link", "label": "Secondary button link" }
3523
+ ],
3524
+ "presets": [{ "name": "DS Hero" }]
3525
+ }
3526
+ {% endschema %}`;
3527
+ SECTION_RICH_TEXT = `{% comment %}
3528
+ Heading plus rich text, with an optional button.
3529
+ {% endcomment %}
3530
+ <div class="ds ds-embed ds-container" style="padding:2rem 0">
3531
+ {%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
3532
+ <div>{{ section.settings.text }}</div>
3533
+ {%- if section.settings.button_label != blank -%}
3534
+ <p><a class="ds-btn ds-btn-secondary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
3535
+ {%- endif -%}
3536
+ </div>
3537
+ {% schema %}
3538
+ {
3539
+ "name": "DS Rich text",
3540
+ "settings": [
3541
+ { "type": "inline_richtext", "id": "heading", "label": "Heading" },
3542
+ { "type": "richtext", "id": "text", "label": "Text" },
3543
+ { "type": "text", "id": "button_label", "label": "Button label" },
3544
+ { "type": "url", "id": "button_link", "label": "Button link" }
3545
+ ],
3546
+ "presets": [{ "name": "DS Rich text" }]
3547
+ }
3548
+ {% endschema %}`;
3549
+ SECTION_IMAGE_WITH_TEXT = `{% comment %}
3550
+ Image beside heading, text, and an optional button.
3551
+ {% endcomment %}
3552
+ <div class="ds ds-embed ds-container ds-image-text">
3553
+ {%- if section.settings.image -%}
3554
+ {%- assign ds_image_alt = section.settings.heading | strip_html -%}
3555
+ {{ section.settings.image | image_url: width: 900 | image_tag: widths: '480, 720, 900', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
3556
+ {%- endif -%}
3557
+ <div class="ds-product-info">
3558
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3559
+ <div>{{ section.settings.text }}</div>
3560
+ {%- if section.settings.button_label != blank -%}
3561
+ <p><a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
3562
+ {%- endif -%}
3563
+ </div>
3564
+ </div>
3565
+ {% schema %}
3566
+ {
3567
+ "name": "DS Image with text",
3568
+ "settings": [
3569
+ { "type": "image_picker", "id": "image", "label": "Image" },
3570
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Pair text with an image" },
3571
+ { "type": "richtext", "id": "text", "label": "Text" },
3572
+ { "type": "text", "id": "button_label", "label": "Button label" },
3573
+ { "type": "url", "id": "button_link", "label": "Button link" }
3574
+ ],
3575
+ "presets": [{ "name": "DS Image with text" }]
3576
+ }
3577
+ {% endschema %}`;
3578
+ SECTION_MAIN_PRODUCT = `{% comment %}
3579
+ Product page main section. Assign the "product.ds" template to a product in
3580
+ the admin, or add this section to an existing product template.
3581
+ {% endcomment %}
3582
+ <div class="ds ds-embed ds-container ds-product-page">
3583
+ <div class="ds-product-layout">
3584
+ <div class="ds-product-media">
3585
+ {%- if product.featured_image -%}
3586
+ {%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
3587
+ {{ product.featured_image | image_url: width: 1200 | image_tag: widths: '480, 720, 960, 1200', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
3588
+ {%- endif -%}
3589
+ {%- if product.images.size > 1 -%}
3590
+ <div class="ds-product-thumbs">
3591
+ {%- for image in product.images offset: 1 limit: 4 -%}
3592
+ {%- assign ds_thumb_alt = image.alt | default: product.title -%}
3593
+ {{ image | image_url: width: 300 | image_tag: alt: ds_thumb_alt }}
3594
+ {%- endfor -%}
3595
+ </div>
3596
+ {%- endif -%}
3597
+ </div>
3598
+ <div class="ds-product-info">
3599
+ {%- if section.settings.show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
3600
+ <h1 class="ds-display">{{ product.title }}</h1>
3601
+ {% render 'ds-price', variant: product.selected_or_first_available_variant %}
3602
+ {% render 'ds-buy-button', product: product, show_quantity: section.settings.show_quantity %}
3603
+ <div>{{ product.description }}</div>
3604
+ </div>
3605
+ </div>
3606
+ </div>
3607
+ {% schema %}
3608
+ {
3609
+ "name": "DS Product",
3610
+ "settings": [
3611
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": true },
3612
+ { "type": "checkbox", "id": "show_quantity", "label": "Show quantity field", "default": true }
3613
+ ],
3614
+ "enabled_on": { "templates": ["product"] }
3615
+ }
3616
+ {% endschema %}`;
3617
+ SECTION_MAIN_COLLECTION = `{% comment %}
3618
+ Collection page main section with pagination. Assign the "collection.ds"
3619
+ template to a collection in the admin, or add to an existing template.
3620
+ {% endcomment %}
3621
+ {% paginate collection.products by section.settings.products_per_page %}
3622
+ <div class="ds ds-embed ds-container ds-collection-page">
3623
+ <h1 class="ds-display">{{ collection.title }}</h1>
3624
+ {%- if collection.description != blank -%}<div class="ds-supporting">{{ collection.description }}</div>{%- endif -%}
3625
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}; margin-top: 1.5rem">
3626
+ {%- for product in collection.products -%}
3627
+ {% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
3628
+ {%- else -%}
3629
+ <p class="ds-supporting">No products found.</p>
3630
+ {%- endfor -%}
3631
+ </div>
3632
+ {% render 'ds-pagination', paginate: paginate %}
3633
+ </div>
3634
+ {% endpaginate %}
3635
+ {% schema %}
3636
+ {
3637
+ "name": "DS Collection",
3638
+ "settings": [
3639
+ { "type": "range", "id": "products_per_page", "min": 4, "max": 24, "step": 4, "default": 12, "label": "Products per page" },
3640
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
3641
+ { "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
3642
+ { "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false }
3643
+ ],
3644
+ "enabled_on": { "templates": ["collection"] }
3645
+ }
3646
+ {% endschema %}`;
3647
+ SECTION_CONTACT = `{% comment %}
3648
+ Contact form posting to Shopify's contact endpoint. No JavaScript.
3649
+ {% endcomment %}
3650
+ <div class="ds ds-embed ds-container" style="max-width:36rem;padding:2rem 0">
3651
+ <h2 class="ds-section-title">{{ section.settings.heading }}</h2>
3652
+ {% form 'contact' %}
3653
+ {%- if form.posted_successfully? -%}
3654
+ <p role="status">Thanks for your message. We reply within one business day.</p>
3655
+ {%- else -%}
3656
+ {%- if form.errors -%}<p class="ds-alert" role="alert">{{ form.errors | default_errors }}</p>{%- endif -%}
3657
+ <div class="ds-field">
3658
+ <label class="ds-label" for="ds-contact-name-{{ section.id }}">Name</label>
3659
+ <input id="ds-contact-name-{{ section.id }}" class="ds-input" type="text" name="contact[name]" autocomplete="name">
3660
+ </div>
3661
+ <div class="ds-field">
3662
+ <label class="ds-label" for="ds-contact-email-{{ section.id }}">Email</label>
3663
+ <input id="ds-contact-email-{{ section.id }}" class="ds-input" type="email" name="contact[email]" autocomplete="email" required>
3664
+ </div>
3665
+ <div class="ds-field">
3666
+ <label class="ds-label" for="ds-contact-body-{{ section.id }}">Message</label>
3667
+ <textarea id="ds-contact-body-{{ section.id }}" class="ds-textarea" name="contact[body]" required></textarea>
3668
+ </div>
3669
+ <button type="submit" class="ds-btn ds-btn-primary">Send message</button>
3670
+ {%- endif -%}
3671
+ {% endform %}
3672
+ </div>
3673
+ {% schema %}
3674
+ {
3675
+ "name": "DS Contact",
3676
+ "settings": [
3677
+ { "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Contact us" }
3678
+ ],
3679
+ "presets": [{ "name": "DS Contact" }]
3680
+ }
3681
+ {% endschema %}`;
3682
+ SECTION_MULTICOLUMN = `{% comment %}
3683
+ Row of text cards, for value props or feature callouts.
3684
+ {% endcomment %}
3685
+ <div class="ds ds-embed ds-container" style="padding:2rem 0">
3686
+ {%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
3687
+ <div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
3688
+ {%- for block in section.blocks -%}
3689
+ <div class="ds-card" {{ block.shopify_attributes }}>
3690
+ <div class="ds-card-body">
3691
+ <h3 class="ds-card-title">{{ block.settings.heading }}</h3>
3692
+ <p class="ds-card-desc">{{ block.settings.text }}</p>
3693
+ </div>
3694
+ </div>
3695
+ {%- endfor -%}
3696
+ </div>
3697
+ </div>
3698
+ {% schema %}
3699
+ {
3700
+ "name": "DS Multicolumn",
3701
+ "settings": [
3702
+ { "type": "inline_richtext", "id": "heading", "label": "Heading" },
3703
+ { "type": "range", "id": "columns_desktop", "min": 2, "max": 4, "step": 1, "default": 3, "label": "Columns on desktop" }
3704
+ ],
3705
+ "blocks": [
3706
+ {
3707
+ "type": "column",
3708
+ "name": "Column",
3709
+ "settings": [
3710
+ { "type": "text", "id": "heading", "label": "Heading" },
3711
+ { "type": "text", "id": "text", "label": "Text" }
3712
+ ]
3713
+ }
3714
+ ],
3715
+ "max_blocks": 8,
3716
+ "presets": [{ "name": "DS Multicolumn", "blocks": [{ "type": "column" }, { "type": "column" }, { "type": "column" }] }]
3717
+ }
3718
+ {% endschema %}`;
3719
+ TEMPLATE_PRODUCT_JSON = `{
3720
+ "sections": {
3721
+ "main": {
3722
+ "type": "ds-main-product",
3723
+ "settings": {}
3724
+ }
3725
+ },
3726
+ "order": ["main"]
3727
+ }`;
3728
+ TEMPLATE_COLLECTION_JSON = `{
3729
+ "sections": {
3730
+ "main": {
3731
+ "type": "ds-main-collection",
3732
+ "settings": {}
3733
+ }
3734
+ },
3735
+ "order": ["main"]
3736
+ }`;
3737
+ SNIPPETS = [
3738
+ { path: "snippets/ds-badge.liquid", content: SNIPPET_BADGE },
3739
+ { path: "snippets/ds-price.liquid", content: SNIPPET_PRICE },
3740
+ { path: "snippets/ds-variant-select.liquid", content: SNIPPET_VARIANT_SELECT },
3741
+ { path: "snippets/ds-buy-button.liquid", content: SNIPPET_BUY_BUTTON },
3742
+ { path: "snippets/ds-product-card.liquid", content: SNIPPET_PRODUCT_CARD },
3743
+ { path: "snippets/ds-announcement.liquid", content: SNIPPET_ANNOUNCEMENT },
3744
+ { path: "snippets/ds-newsletter.liquid", content: SNIPPET_NEWSLETTER },
3745
+ { path: "snippets/ds-pagination.liquid", content: SNIPPET_PAGINATION },
3746
+ { path: "sections/ds-header.liquid", content: SECTION_HEADER },
3747
+ { path: "sections/ds-footer.liquid", content: SECTION_FOOTER },
3748
+ { path: "sections/ds-hero.liquid", content: SECTION_HERO },
3749
+ { path: "sections/ds-rich-text.liquid", content: SECTION_RICH_TEXT },
3750
+ { path: "sections/ds-image-with-text.liquid", content: SECTION_IMAGE_WITH_TEXT },
3751
+ { path: "sections/ds-multicolumn.liquid", content: SECTION_MULTICOLUMN },
3752
+ { path: "sections/ds-contact.liquid", content: SECTION_CONTACT },
3753
+ { path: "sections/ds-faq.liquid", content: SECTION_FAQ },
3754
+ { path: "sections/ds-featured-collection.liquid", content: SECTION_FEATURED_COLLECTION },
3755
+ { path: "sections/ds-main-product.liquid", content: SECTION_MAIN_PRODUCT },
3756
+ { path: "sections/ds-main-collection.liquid", content: SECTION_MAIN_COLLECTION },
3757
+ { path: "templates/product.ds.json", content: TEMPLATE_PRODUCT_JSON },
3758
+ { path: "templates/collection.ds.json", content: TEMPLATE_COLLECTION_JSON }
3759
+ ];
3760
+ SHOPIFY_KIT_CONDUCT_MD = [
3761
+ "## Install into the merchant's theme (mandatory order)",
3762
+ "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.",
3763
+ "2. Load the stylesheet once in `layout/theme.liquid`, before `</head>`: `{{ 'ds-kit.css' | asset_url | stylesheet_tag }}`.",
3764
+ '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.',
3765
+ "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.",
3766
+ "",
3767
+ "## Class contract (pass/fail)",
3768
+ "- 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.",
3769
+ "- Custom CSS is layout-only (position, spacing, grid). Never restyle a `ds-*` component's geometry or surface; those carry this system's identity.",
3770
+ `- ${HOVER_POLICY}`,
3771
+ `- 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.`,
3772
+ "",
3773
+ "## Commerce correctness",
3774
+ "- `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.",
3775
+ "- 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.",
3776
+ "- Prices always render through the `money` filter, never hand-formatted.",
3777
+ "- Images always render through `image_url` with explicit widths; `img_url` is deprecated.",
3778
+ "",
3779
+ "## Not in this kit, on purpose",
3780
+ "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."
3781
+ ].join("\n");
3782
+ }
3783
+ });
3784
+
3009
3785
  // ../shared/src/mcp/preview-widget-sdk.generated.ts
3010
3786
  var PREVIEW_WIDGET_SDK_JS;
3011
3787
  var init_preview_widget_sdk_generated = __esm({
@@ -5308,9 +6084,9 @@ function inlineBuildMd(projectId) {
5308
6084
  return `
5309
6085
  **Building inline (ChatGPT canvas / Claude artifact)?** There is no npm or Tailwind there, so the install above can't run. Call \`get-design-system-kit\` with \`target: "html"\` and projectId \`${projectId}\` to get this system's real tokens + a framework-free CSS kit you paste into one \`<style>\` and build with immediately.`;
5310
6086
  }
5311
- async function fetchProjectTokensCss(projectId, authContext) {
6087
+ async function fetchProjectTokensCss(projectId, apiFetch, authContext) {
5312
6088
  try {
5313
- const res = await fetch(`${APP_BASE_URL}/api/projects/${projectId}/registry/all.json`, {
6089
+ const res = await apiFetch(`${APP_BASE_URL}/api/projects/${projectId}/registry/all.json`, {
5314
6090
  headers: authHeaders(authContext)
5315
6091
  });
5316
6092
  if (!res.ok) return null;
@@ -5329,7 +6105,7 @@ function completedHtmlKitStructuredContent(name, viewUrl, tokensCss, componentMa
5329
6105
  requiredWrapperClass: "ds",
5330
6106
  requiredComponentClassPrefix: "ds-",
5331
6107
  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.",
6108
+ hoverPolicy: HOVER_POLICY,
5333
6109
  hostIsolationCssTemplate: HOST_ISOLATION_CSS_TEMPLATE,
5334
6110
  minimumTextContrast: MINIMUM_TEXT_CONTRAST,
5335
6111
  textColorPolicy: INLINE_TEXT_COLOR_POLICY,
@@ -5341,6 +6117,22 @@ function completedHtmlKitStructuredContent(name, viewUrl, tokensCss, componentMa
5341
6117
  tokensCss
5342
6118
  };
5343
6119
  }
6120
+ function completedShopifyKitStructuredContent(name, viewUrl, tokensCss, themeFiles) {
6121
+ return {
6122
+ status: "completed",
6123
+ name,
6124
+ viewUrl,
6125
+ buildInstructions: SHOPIFY_KIT_CONDUCT_MD,
6126
+ requiredWrapperClass: "ds",
6127
+ requiredComponentClassPrefix: "ds-",
6128
+ customCssScope: "layout-only",
6129
+ hoverPolicy: HOVER_POLICY,
6130
+ minimumTextContrast: MINIMUM_TEXT_CONTRAST,
6131
+ textColorPolicy: INLINE_TEXT_COLOR_POLICY,
6132
+ themeFiles,
6133
+ tokensCss
6134
+ };
6135
+ }
5344
6136
  async function fetchCatalog(designSystemId) {
5345
6137
  try {
5346
6138
  const [globalsRes, ...componentResults] = await Promise.all([
@@ -5384,8 +6176,9 @@ function registerDesignSystemTools({
5384
6176
  provider,
5385
6177
  tracked,
5386
6178
  track,
5387
- authContext
5388
- }) {
6179
+ authContext,
6180
+ apiFetch = globalThis.fetch
6181
+ }, { enableShopifyKit = true } = {}) {
5389
6182
  const searchInputSchema = z5.object({
5390
6183
  query: z5.string().describe(
5391
6184
  "The user's app, UI, industry, or visual direction. Examples: 'World Cup betting app for friends', 'warm editorial ecommerce', or 'professional fintech dashboard'."
@@ -5482,7 +6275,7 @@ ${BUILD_TARGET_RULE}` : ""}`
5482
6275
  title: "Find a Design System for a UI Build",
5483
6276
  description: searchDescription(),
5484
6277
  inputSchema: searchInputSchema,
5485
- annotations: { readOnlyHint: true }
6278
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
5486
6279
  },
5487
6280
  makeSearchHandler("find-design-system", true)
5488
6281
  );
@@ -5503,7 +6296,7 @@ ${BUILD_TARGET_RULE}` : ""}`
5503
6296
  structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
5504
6297
  };
5505
6298
  }
5506
- const result = await fetchProjectTokensCss(projectId, authContext);
6299
+ const result = await fetchProjectTokensCss(projectId, apiFetch, authContext);
5507
6300
  if (!result?.tokensCss) {
5508
6301
  return {
5509
6302
  content: [{ type: "text", text: `Could not load tokens for project "${projectId}". It may still be generating, or the id is wrong. Check ${viewUrl2}.` }],
@@ -5555,7 +6348,7 @@ ${BUILD_TARGET_RULE}` : ""}`
5555
6348
 
5556
6349
  `;
5557
6350
  const longestRun = Math.max(0, ...[...ds.metadata.html.matchAll(/`+/g)].map((m) => m[0].length));
5558
- const fence2 = "`".repeat(Math.max(3, longestRun + 1));
6351
+ const fence3 = "`".repeat(Math.max(3, longestRun + 1));
5559
6352
  const { tokensCss: tokensCss2, files: files2 } = await fetchCatalog(designSystemId);
5560
6353
  const componentMaterialCss = buildDsComponentCss(files2).css;
5561
6354
  return {
@@ -5563,9 +6356,9 @@ ${BUILD_TARGET_RULE}` : ""}`
5563
6356
  // document so hosted agents cannot truncate it away. [BD-240]
5564
6357
  content: [{ type: "text", text: `${steer}${intro}${HTML_KIT_CONDUCT_MD}
5565
6358
 
5566
- ${fence2}html
6359
+ ${fence3}html
5567
6360
  ${ds.metadata.html}
5568
- ${fence2}` }],
6361
+ ${fence3}` }],
5569
6362
  structuredContent: completedHtmlKitStructuredContent(
5570
6363
  ds.metadata.title,
5571
6364
  viewUrl,
@@ -5599,13 +6392,85 @@ ${fence2}` }],
5599
6392
  )
5600
6393
  };
5601
6394
  });
6395
+ const getShopifyKit = async ({
6396
+ designSystemId,
6397
+ projectId,
6398
+ context
6399
+ }) => tracked("get-design-system-kit", { target: "shopify", designSystemId, projectId, context }, async () => {
6400
+ if (projectId) {
6401
+ const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
6402
+ const result = await fetchProjectTokensCss(projectId, apiFetch, authContext);
6403
+ if (!result?.tokensCss) {
6404
+ return {
6405
+ content: [{ type: "text", text: `Could not load tokens for project "${projectId}". It may still be generating, or the id is wrong. Check ${viewUrl2}.` }],
6406
+ structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
6407
+ };
6408
+ }
6409
+ const componentKit2 = buildDsComponentCss(result.files);
6410
+ const name = result.name ?? `Project ${projectId.slice(0, 8)}`;
6411
+ const text2 = buildDsShopifyKit({
6412
+ name,
6413
+ tokensCss: result.tokensCss,
6414
+ viewUrl: viewUrl2,
6415
+ source: { kind: "project", projectId },
6416
+ componentKit: componentKit2
6417
+ });
6418
+ return {
6419
+ content: [{ type: "text", text: text2 }],
6420
+ structuredContent: completedShopifyKitStructuredContent(
6421
+ name,
6422
+ viewUrl2,
6423
+ result.tokensCss,
6424
+ buildDsShopifyFiles({ tokensCss: result.tokensCss, componentKit: componentKit2 })
6425
+ )
6426
+ };
6427
+ }
6428
+ if (!designSystemId) {
6429
+ return {
6430
+ content: [{ type: "text", text: "For target shopify, pass either a catalog designSystemId (from find-design-system) or a projectId (from create-design-system)." }],
6431
+ structuredContent: { status: "error", name: "Missing design system", tokensCss: "" }
6432
+ };
6433
+ }
6434
+ const ds = await provider.getDesignSystemById(designSystemId, { includeComponents: false });
6435
+ if (!ds) {
6436
+ return {
6437
+ 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.` }],
6438
+ structuredContent: { status: "error", name: designSystemId, tokensCss: "" }
6439
+ };
6440
+ }
6441
+ const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
6442
+ const { tokensCss, files } = await fetchCatalog(designSystemId);
6443
+ if (!tokensCss) {
6444
+ return {
6445
+ content: [{ type: "text", text: `Could not load tokens for "${designSystemId}". The full live showcase is at ${viewUrl}.` }],
6446
+ structuredContent: { status: "error", name: ds.metadata.title, viewUrl, tokensCss: "" }
6447
+ };
6448
+ }
6449
+ const componentKit = buildDsComponentCss(files);
6450
+ const text = buildDsShopifyKit({
6451
+ name: ds.metadata.title,
6452
+ tokensCss,
6453
+ viewUrl,
6454
+ source: { kind: "catalog", designSystemId },
6455
+ componentKit
6456
+ });
6457
+ return {
6458
+ content: [{ type: "text", text }],
6459
+ structuredContent: completedShopifyKitStructuredContent(
6460
+ ds.metadata.title,
6461
+ viewUrl,
6462
+ tokensCss,
6463
+ buildDsShopifyFiles({ tokensCss, componentKit })
6464
+ )
6465
+ };
6466
+ });
5602
6467
  server.registerTool(
5603
6468
  "get-design-system-kit",
5604
6469
  {
5605
6470
  title: "Get a Design System Build Kit",
5606
6471
  description: `Return the correct build kit for a design system through one capability-based entry point.
5607
6472
 
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.
6473
+ 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
6474
 
5610
6475
  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
6476
 
@@ -5630,15 +6495,18 @@ same components/ui/ paths.
5630
6495
 
5631
6496
  ${BUILD_TARGET_RULE}`,
5632
6497
  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".'),
6498
+ // Off-pilot callers must not see the target at all, not merely be refused it.
6499
+ target: z5.enum(
6500
+ enableShopifyKit ? ["react", "html", "shopify"] : ["react", "html"]
6501
+ ).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.' : ""}`),
6502
+ 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.`),
6503
+ 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
6504
  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
6505
  library: z5.enum(["radix", "base-ui"]).optional().describe('React target only, default "base-ui". Pass "radix" when the project already uses Radix primitives.'),
5638
6506
  context: CONTEXT_ARG
5639
6507
  }),
5640
- outputSchema: BUILD_KIT_OUTPUT_SCHEMA,
5641
- annotations: { readOnlyHint: true }
6508
+ outputSchema: buildKitOutputSchema(enableShopifyKit),
6509
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
5642
6510
  // This tool returns build instructions, not a visual preview. Attaching
5643
6511
  // the preview template makes ChatGPT render a nested showcase iframe;
5644
6512
  // when that iframe is unavailable the user sees a large blank card even
@@ -5649,6 +6517,15 @@ ${BUILD_TARGET_RULE}`,
5649
6517
  if (target === "html") {
5650
6518
  return getHtmlKit({ designSystemId, projectId, context });
5651
6519
  }
6520
+ if (target === "shopify") {
6521
+ if (!enableShopifyKit) {
6522
+ return {
6523
+ 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.' }],
6524
+ structuredContent: { status: "error", name: "Shopify target unavailable", tokensCss: "" }
6525
+ };
6526
+ }
6527
+ return getShopifyKit({ designSystemId, projectId, context });
6528
+ }
5652
6529
  if (!designSystemId) {
5653
6530
  return {
5654
6531
  content: [{ type: "text", text: 'For target "react", pass a catalog designSystemId from find-design-system.' }],
@@ -5871,7 +6748,7 @@ Share the project URL with the user right away so they can watch the generation
5871
6748
  requestHeaders.Authorization = `Bearer ${bearer2}`;
5872
6749
  }
5873
6750
  try {
5874
- const startRes = await fetch(startUrl, {
6751
+ const startRes = await apiFetch(startUrl, {
5875
6752
  method: "POST",
5876
6753
  headers: requestHeaders,
5877
6754
  body: JSON.stringify({
@@ -5915,7 +6792,7 @@ Share the project URL with the user right away so they can watch the generation
5915
6792
  const statusUrl = `${APP_BASE_URL}/api/projects/${projectId}`;
5916
6793
  let project;
5917
6794
  try {
5918
- const res = await fetch(statusUrl, { headers });
6795
+ const res = await apiFetch(statusUrl, { headers });
5919
6796
  if (!res.ok) {
5920
6797
  return { content: [{ type: "text", text: `Could not read project ${projectId} (HTTP ${res.status}). Check the id, or view it at ${APP_BASE_URL}/projects/${projectId}.` }] };
5921
6798
  }
@@ -5932,7 +6809,7 @@ npx shadcn@latest add ${registryUrl}
5932
6809
  let tokensCss = "";
5933
6810
  let dsName = `Project ${projectId.slice(0, 8)}`;
5934
6811
  try {
5935
- const res = await fetch(registryUrl);
6812
+ const res = await apiFetch(registryUrl);
5936
6813
  if (res.ok) {
5937
6814
  const json = await res.json();
5938
6815
  if (json.meta?.installInstructions) body = json.meta.installInstructions;
@@ -6089,6 +6966,7 @@ var init_design_system_tools = __esm({
6089
6966
  "use strict";
6090
6967
  init_guardrails();
6091
6968
  init_html_kit();
6969
+ init_shopify_kit();
6092
6970
  init_preview_html();
6093
6971
  init_shared();
6094
6972
  REGISTRY_ITEM_SCHEMA = z5.object({
@@ -6653,7 +7531,7 @@ Returns: the semantic palette (primary, background, foreground, accent, border,
6653
7531
  name: z6.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
6654
7532
  maxTokens: z6.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
6655
7533
  }),
6656
- annotations: { readOnlyHint: true }
7534
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
6657
7535
  },
6658
7536
  async ({ css, name, maxTokens }) => tracked("extract-design-system", { name: name ?? null }, async () => {
6659
7537
  const result = extractDesignSystem(css, name);
@@ -6686,7 +7564,13 @@ function fail(message) {
6686
7564
  }
6687
7565
  };
6688
7566
  }
6689
- function registerIllustrationTools({ server, track, tracked, authContext }) {
7567
+ function registerIllustrationTools({
7568
+ server,
7569
+ track,
7570
+ tracked,
7571
+ authContext,
7572
+ apiFetch = globalThis.fetch
7573
+ }) {
6690
7574
  const authHeaders2 = () => {
6691
7575
  const headers = { "Content-Type": "application/json" };
6692
7576
  if (authContext) {
@@ -6719,7 +7603,7 @@ Markup carrying script, event handlers, remote references, or styles that reach
6719
7603
  reviewToken: z7.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
6720
7604
  }),
6721
7605
  outputSchema: REVIEW_OUTPUT_SCHEMA,
6722
- annotations: { readOnlyHint: true }
7606
+ annotations: READ_ONLY_TOOL_ANNOTATIONS
6723
7607
  },
6724
7608
  async ({
6725
7609
  svg,
@@ -6731,7 +7615,7 @@ Markup carrying script, event handlers, remote references, or styles that reach
6731
7615
  { query: intent, animated, repeat: Boolean(reviewToken) },
6732
7616
  async () => {
6733
7617
  try {
6734
- const res = await fetch(`${APP_BASE_URL}/api/illustrations/review`, {
7618
+ const res = await apiFetch(`${APP_BASE_URL}/api/illustrations/review`, {
6735
7619
  method: "POST",
6736
7620
  headers: authHeaders2(),
6737
7621
  signal: AbortSignal.timeout(6e4),
@@ -6847,13 +7731,13 @@ var init_preview_resource = __esm({
6847
7731
  });
6848
7732
 
6849
7733
  // ../shared/src/mcp/tools.ts
6850
- function registerAllTools(server, provider, onToolUsage, authContext, options = {}) {
7734
+ function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch) {
6851
7735
  const { track, tracked } = createTracking(server, onToolUsage);
6852
- const deps = { server, provider, track, tracked, authContext };
7736
+ const deps = { server, provider, track, tracked, authContext, apiFetch };
6853
7737
  registerPrincipleTools(deps, options);
6854
7738
  registerComprehensionTools(deps);
6855
7739
  registerIconTools(deps);
6856
- registerDesignSystemTools(deps);
7740
+ registerDesignSystemTools(deps, options);
6857
7741
  registerExtractTools(deps);
6858
7742
  registerIllustrationTools(deps);
6859
7743
  registerPreviewResource(deps);
@@ -6889,15 +7773,27 @@ function createMcpServer(options) {
6889
7773
  version = "3.0.0",
6890
7774
  onToolUsage,
6891
7775
  authContext,
6892
- enableReactNativeGuide = true
7776
+ enableReactNativeGuide = true,
7777
+ enableThreeJsGuide = true,
7778
+ enableShopifyKit = true,
7779
+ apiFetch = globalThis.fetch
6893
7780
  } = options;
6894
7781
  const server = new McpServer(
6895
7782
  { name, version },
6896
- { instructions: getMcpInstructions(enableReactNativeGuide) }
7783
+ { instructions: getMcpInstructions({ enableReactNativeGuide, enableThreeJsGuide }) }
7784
+ );
7785
+ registerAllTools(
7786
+ server,
7787
+ provider,
7788
+ onToolUsage,
7789
+ authContext,
7790
+ {
7791
+ enableReactNativeGuide,
7792
+ enableThreeJsGuide,
7793
+ enableShopifyKit
7794
+ },
7795
+ apiFetch
6897
7796
  );
6898
- registerAllTools(server, provider, onToolUsage, authContext, {
6899
- enableReactNativeGuide
6900
- });
6901
7797
  return server;
6902
7798
  }
6903
7799
  var init_mcp = __esm({