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/README.md +4 -3
- package/dist/index.js +1000 -222
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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
|
|
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(
|
|
857
|
-
|
|
858
|
-
|
|
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
|
|
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"
|
|
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 })
|
|
876
|
-
3. get-design-system-status({ projectId })
|
|
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
|
|
886
|
-
Default is create-design-system (above). For a named
|
|
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 =
|
|
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">© {{ '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:
|
|
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
|
|
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
|
-
${
|
|
6353
|
+
${fence3}html
|
|
5567
6354
|
${ds.metadata.html}
|
|
5568
|
-
${
|
|
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
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
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:
|
|
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
|
-
"
|
|
7571
|
+
"review-illustration",
|
|
6687
7572
|
{
|
|
6688
|
-
title: "
|
|
6689
|
-
description: `
|
|
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
|
-
|
|
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
|
-
|
|
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. \`
|
|
6697
|
-
2.
|
|
6698
|
-
3.
|
|
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
|
-
|
|
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
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
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:
|
|
7593
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
7594
|
+
annotations: { readOnlyHint: true }
|
|
6708
7595
|
},
|
|
6709
|
-
async ({
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
const
|
|
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:
|
|
7629
|
+
text: layers.length ? `${message}
|
|
7630
|
+
|
|
7631
|
+
Layers, in paint order: ${layers.join(", ")}.` : message
|
|
6725
7632
|
}
|
|
6726
7633
|
],
|
|
6727
7634
|
structuredContent: {
|
|
6728
|
-
|
|
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
|
-
|
|
6833
|
-
|
|
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
|
|
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
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
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
|
-
|
|
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
|
}
|