better-design 0.15.0 → 0.19.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 +47 -56
- package/dist/index.js +2381 -537
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -899,9 +899,119 @@ Default is create-design-system (above). For a named system: find-design-system(
|
|
|
899
899
|
}
|
|
900
900
|
});
|
|
901
901
|
|
|
902
|
+
// ../shared/src/mcp/session-guidance.ts
|
|
903
|
+
function topicFromDocId(docId) {
|
|
904
|
+
return docId.replace(/^(principle|foundational|doc)-/, "").trim();
|
|
905
|
+
}
|
|
906
|
+
function scopeToClient(calls, clientName) {
|
|
907
|
+
if (!clientName) return calls;
|
|
908
|
+
const sameClient = calls.filter((call) => call.clientName === clientName);
|
|
909
|
+
return sameClient.length > 0 ? sameClient : calls;
|
|
910
|
+
}
|
|
911
|
+
function summarizeSessionGuidance(calls, clientName) {
|
|
912
|
+
const topics = [];
|
|
913
|
+
const seen = /* @__PURE__ */ new Set();
|
|
914
|
+
let guidanceCalls = 0;
|
|
915
|
+
let reviewed = false;
|
|
916
|
+
for (const call of scopeToClient(calls, clientName)) {
|
|
917
|
+
if (REVIEW_TOOLS.has(call.tool)) {
|
|
918
|
+
reviewed = true;
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!GUIDANCE_TOOLS.has(call.tool)) continue;
|
|
922
|
+
guidanceCalls += 1;
|
|
923
|
+
const raw = call.topic?.trim() || call.docIds.map(topicFromDocId).find(Boolean);
|
|
924
|
+
if (!raw) continue;
|
|
925
|
+
const topic = topicFromDocId(raw).toLowerCase();
|
|
926
|
+
if (!topic || seen.has(topic)) continue;
|
|
927
|
+
seen.add(topic);
|
|
928
|
+
topics.push(topic);
|
|
929
|
+
}
|
|
930
|
+
return { topics, reviewed, guidanceCalls };
|
|
931
|
+
}
|
|
932
|
+
function formatTopicList(topics) {
|
|
933
|
+
const named = topics.slice(0, MAX_NAMED_TOPICS);
|
|
934
|
+
if (named.length === 0) return "";
|
|
935
|
+
if (named.length === 1) return named[0];
|
|
936
|
+
return `${named.slice(0, -1).join(", ")} and ${named[named.length - 1]}`;
|
|
937
|
+
}
|
|
938
|
+
function reviewSectionForTopic(topic) {
|
|
939
|
+
const needle = topic.toLowerCase();
|
|
940
|
+
return REVIEW_SECTION_BY_TOPIC.find(({ match }) => match.test(needle))?.section ?? null;
|
|
941
|
+
}
|
|
942
|
+
function weightReviewRules(rules, topics) {
|
|
943
|
+
const sections = [...new Set(topics.map(reviewSectionForTopic).filter((s) => Boolean(s)))];
|
|
944
|
+
if (sections.length === 0) return { rules: [...rules], sections: [] };
|
|
945
|
+
const matches = (rule) => sections.some((section) => rule.id.toLowerCase().includes(section));
|
|
946
|
+
const first = rules.filter(matches);
|
|
947
|
+
if (first.length === 0 || first.length === rules.length) {
|
|
948
|
+
return { rules: [...rules], sections: [] };
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
rules: [...first, ...rules.filter((rule) => !matches(rule))],
|
|
952
|
+
sections: sections.filter(
|
|
953
|
+
(section) => first.some((rule) => rule.id.toLowerCase().includes(section))
|
|
954
|
+
)
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function guidancePreamble(topics, subject, orderedSections = []) {
|
|
958
|
+
if (topics.length === 0) return "";
|
|
959
|
+
const list = formatTopicList(topics);
|
|
960
|
+
const plural = topics.length > 1 ? "principles" : "principle";
|
|
961
|
+
const ordering = orderedSections.length ? ` The ${formatTopicList(orderedSections.map((s) => s.replace(/-/g, " ")))} rules are ordered first below.` : "";
|
|
962
|
+
return `Earlier this session you loaded the ${list} ${plural}. Check ${subject} against these first.${ordering}`;
|
|
963
|
+
}
|
|
964
|
+
function reviewNudge(session) {
|
|
965
|
+
if (session.reviewed || session.guidanceCalls === 0) return "";
|
|
966
|
+
return "You have loaded design guidance in this session and not reviewed the result yet. When the screen exists, call `get-review-rules` on the code, then `check-comprehension` on its copy, and fix every critical and serious finding before you present it.";
|
|
967
|
+
}
|
|
968
|
+
var MCP_SESSION_WINDOW_MS, GUIDANCE_TOOLS, REVIEW_TOOLS, SESSION_LOOP_TOOLS, NO_SESSION_GUIDANCE, MAX_NAMED_TOPICS, REVIEW_SECTION_BY_TOPIC;
|
|
969
|
+
var init_session_guidance = __esm({
|
|
970
|
+
"../shared/src/mcp/session-guidance.ts"() {
|
|
971
|
+
"use strict";
|
|
972
|
+
MCP_SESSION_WINDOW_MS = 30 * 6e4;
|
|
973
|
+
GUIDANCE_TOOLS = /* @__PURE__ */ new Set([
|
|
974
|
+
"get-ui-principle",
|
|
975
|
+
"get-ux-principle",
|
|
976
|
+
"get-react-native-guide",
|
|
977
|
+
"get-three-js-guide",
|
|
978
|
+
"get-motion-guide"
|
|
979
|
+
]);
|
|
980
|
+
REVIEW_TOOLS = /* @__PURE__ */ new Set([
|
|
981
|
+
"get-review-rules",
|
|
982
|
+
"check-comprehension"
|
|
983
|
+
]);
|
|
984
|
+
SESSION_LOOP_TOOLS = /* @__PURE__ */ new Set([
|
|
985
|
+
...GUIDANCE_TOOLS,
|
|
986
|
+
...REVIEW_TOOLS
|
|
987
|
+
]);
|
|
988
|
+
NO_SESSION_GUIDANCE = {
|
|
989
|
+
topics: [],
|
|
990
|
+
reviewed: false,
|
|
991
|
+
guidanceCalls: 0
|
|
992
|
+
};
|
|
993
|
+
MAX_NAMED_TOPICS = 4;
|
|
994
|
+
REVIEW_SECTION_BY_TOPIC = [
|
|
995
|
+
{ match: /accessib|a11y|contrast|keyboard|screen-reader/, section: "accessibility" },
|
|
996
|
+
{ match: /animation|motion|transition|spring|scroll|reveal|particle|canvas|hover|pointer|gesture|interaction/, section: "animation" },
|
|
997
|
+
// `\bform` on purpose: bare `form` also matches "performance", which the
|
|
998
|
+
// visual-design row below claims, and "information".
|
|
999
|
+
{ match: /microcopy|copy|content|marketing|onboarding|error|notification|dialog|\bform|search|navigation|psycholog|behavior/, section: "content" },
|
|
1000
|
+
{ match: /hierarchy|spacing|layout|typograph|color|depth|surface|polish|image|grid|component|distill|bolder|quieter|performance/, section: "visual-design" }
|
|
1001
|
+
];
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
902
1005
|
// ../shared/src/mcp/tools/shared.ts
|
|
903
1006
|
import { createHash } from "crypto";
|
|
904
1007
|
import { z } from "zod";
|
|
1008
|
+
function mcpClientName(server) {
|
|
1009
|
+
try {
|
|
1010
|
+
return server.server.getClientVersion?.()?.name || void 0;
|
|
1011
|
+
} catch {
|
|
1012
|
+
return void 0;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
905
1015
|
function codingHostName(server) {
|
|
906
1016
|
try {
|
|
907
1017
|
const name = server.server.getClientVersion?.()?.name;
|
|
@@ -932,6 +1042,24 @@ function buildKitOutputSchema(enableShopifyKit = true) {
|
|
|
932
1042
|
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
1043
|
});
|
|
934
1044
|
}
|
|
1045
|
+
async function loadSessionGuidance(provider, userId, clientName, now = Date.now()) {
|
|
1046
|
+
if (!provider.getRecentGuidance) return NO_SESSION_GUIDANCE;
|
|
1047
|
+
let timer;
|
|
1048
|
+
try {
|
|
1049
|
+
const calls = await Promise.race([
|
|
1050
|
+
provider.getRecentGuidance(userId, now - MCP_SESSION_WINDOW_MS),
|
|
1051
|
+
new Promise((resolve) => {
|
|
1052
|
+
timer = setTimeout(() => resolve(null), SESSION_LOOKUP_TIMEOUT_MS);
|
|
1053
|
+
})
|
|
1054
|
+
]);
|
|
1055
|
+
if (!Array.isArray(calls) || calls.length === 0) return NO_SESSION_GUIDANCE;
|
|
1056
|
+
return summarizeSessionGuidance(calls, clientName);
|
|
1057
|
+
} catch {
|
|
1058
|
+
return NO_SESSION_GUIDANCE;
|
|
1059
|
+
} finally {
|
|
1060
|
+
if (timer) clearTimeout(timer);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
935
1063
|
function extractOutputPreview(result) {
|
|
936
1064
|
const content = result?.content;
|
|
937
1065
|
if (!Array.isArray(content)) return void 0;
|
|
@@ -1037,10 +1165,11 @@ function truncateToTokens(text, maxTokens) {
|
|
|
1037
1165
|
|
|
1038
1166
|
...(truncated to ~${maxTokens} tokens)`;
|
|
1039
1167
|
}
|
|
1040
|
-
var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_DOMAINS,
|
|
1168
|
+
var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_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, SESSION_LOOKUP_TIMEOUT_MS, SEARCH_TOOLS;
|
|
1041
1169
|
var init_shared = __esm({
|
|
1042
1170
|
"../shared/src/mcp/tools/shared.ts"() {
|
|
1043
1171
|
"use strict";
|
|
1172
|
+
init_session_guidance();
|
|
1044
1173
|
MAX_OUTPUT_PREVIEW = 1e3;
|
|
1045
1174
|
ICONIFY_FETCH_TIMEOUT_MS = 1e4;
|
|
1046
1175
|
ICONIFY_API_BASE = "https://api.iconify.design";
|
|
@@ -1050,7 +1179,6 @@ var init_shared = __esm({
|
|
|
1050
1179
|
"https://fonts.googleapis.com",
|
|
1051
1180
|
"https://fonts.gstatic.com"
|
|
1052
1181
|
];
|
|
1053
|
-
WIDGET_FRAME_DOMAINS = [APP_BASE_URL];
|
|
1054
1182
|
WIDGET_APP_DOMAIN = `${createHash("sha256").update(`${APP_BASE_URL}/api/mcp`).digest("hex").slice(0, 32)}.claudemcpcontent.com`;
|
|
1055
1183
|
REQUIREMENTS_MD = `> **Requirements:** these components target **Tailwind v4**. The installed \`globals.css\` uses the v4 entry point (\`@import "tailwindcss"\`), so the project needs \`tailwindcss@^4\` with \`@tailwindcss/postcss\` in its PostCSS config (or the \`@tailwindcss/vite\` plugin). On a Tailwind v3 project, migrate to v4 first, otherwise the build throws the "trying to use tailwindcss directly as a PostCSS plugin" error.
|
|
1056
1184
|
`;
|
|
@@ -1074,8 +1202,8 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
1074
1202
|
status: z.string().describe("completed | error | pending | generating | selected"),
|
|
1075
1203
|
name: z.string().describe("Display name of the design system or project."),
|
|
1076
1204
|
viewUrl: z.string().optional().describe("Owner-facing Better Design page (the human 'Open' link). May require login."),
|
|
1077
|
-
previewUrl: z.string().optional().describe("Public, cookieless showcase URL to
|
|
1078
|
-
shotUrl: z.string().optional().describe("Real screenshot of the showcase page
|
|
1205
|
+
previewUrl: z.string().optional().describe("Public, cookieless showcase URL used to generate preview assets. Falls back to viewUrl."),
|
|
1206
|
+
shotUrl: z.string().optional().describe("Real screenshot of the showcase page for the self-contained preview widget."),
|
|
1079
1207
|
installCommand: z.string().optional().describe("shadcn install command, when complete."),
|
|
1080
1208
|
installLibrary: z.enum(["base-ui", "radix", "mixed"]).optional().describe("Actual component library flavor installed by installCommand."),
|
|
1081
1209
|
installNote: z.string().optional().describe("Important install flavor or fallback note for clients that read only structuredContent."),
|
|
@@ -1086,6 +1214,12 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
1086
1214
|
CONTEXT_ARG = z.string().max(280).optional().describe(
|
|
1087
1215
|
"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."
|
|
1088
1216
|
);
|
|
1217
|
+
READ_ONLY_TOOL_ANNOTATIONS = {
|
|
1218
|
+
readOnlyHint: true,
|
|
1219
|
+
destructiveHint: false,
|
|
1220
|
+
openWorldHint: false
|
|
1221
|
+
};
|
|
1222
|
+
SESSION_LOOKUP_TIMEOUT_MS = 2e3;
|
|
1089
1223
|
SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
1090
1224
|
"find-design-system",
|
|
1091
1225
|
"get-ui-principle",
|
|
@@ -1101,6 +1235,541 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
1101
1235
|
}
|
|
1102
1236
|
});
|
|
1103
1237
|
|
|
1238
|
+
// ../shared/src/mcp/scaffold/motion-scaffold.ts
|
|
1239
|
+
var MOTION_SCAFFOLD_FILES, MOTION_SCAFFOLD_STEPS;
|
|
1240
|
+
var init_motion_scaffold = __esm({
|
|
1241
|
+
"../shared/src/mcp/scaffold/motion-scaffold.ts"() {
|
|
1242
|
+
"use strict";
|
|
1243
|
+
MOTION_SCAFFOLD_FILES = [
|
|
1244
|
+
{
|
|
1245
|
+
path: "package.json",
|
|
1246
|
+
contents: `{
|
|
1247
|
+
"name": "motion",
|
|
1248
|
+
"private": true,
|
|
1249
|
+
"scripts": {
|
|
1250
|
+
"dev": "remotion studio",
|
|
1251
|
+
"music": "python3 scripts/make-music.py",
|
|
1252
|
+
"render": "remotion render Video out/video.mp4 --concurrency=6",
|
|
1253
|
+
"render:1080": "remotion render Video out/video-1080.mp4 --concurrency=6 --scale=1.5"
|
|
1254
|
+
},
|
|
1255
|
+
"dependencies": {
|
|
1256
|
+
"@remotion/cli": "4.0.499",
|
|
1257
|
+
"@remotion/google-fonts": "4.0.499",
|
|
1258
|
+
"@remotion/layout-utils": "4.0.499",
|
|
1259
|
+
"react": "19.0.0",
|
|
1260
|
+
"react-dom": "19.0.0",
|
|
1261
|
+
"remotion": "4.0.499"
|
|
1262
|
+
},
|
|
1263
|
+
"devDependencies": {
|
|
1264
|
+
"@types/react": "19.0.0",
|
|
1265
|
+
"typescript": "5.7.3"
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
`
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
path: "tsconfig.json",
|
|
1272
|
+
contents: `{
|
|
1273
|
+
"compilerOptions": {
|
|
1274
|
+
"target": "ES2022",
|
|
1275
|
+
"module": "ESNext",
|
|
1276
|
+
"moduleResolution": "bundler",
|
|
1277
|
+
"jsx": "react-jsx",
|
|
1278
|
+
"strict": true,
|
|
1279
|
+
"skipLibCheck": true,
|
|
1280
|
+
"esModuleInterop": true,
|
|
1281
|
+
"noEmit": true,
|
|
1282
|
+
"lib": [
|
|
1283
|
+
"ES2022",
|
|
1284
|
+
"DOM"
|
|
1285
|
+
]
|
|
1286
|
+
},
|
|
1287
|
+
"include": [
|
|
1288
|
+
"src"
|
|
1289
|
+
]
|
|
1290
|
+
}
|
|
1291
|
+
`
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
path: "remotion.config.ts",
|
|
1295
|
+
contents: `import { Config } from "@remotion/cli/config";
|
|
1296
|
+
|
|
1297
|
+
Config.setVideoImageFormat("jpeg");
|
|
1298
|
+
Config.setOverwriteOutput(true);
|
|
1299
|
+
`
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
path: ".gitignore",
|
|
1303
|
+
contents: `node_modules/\\nout/\\npublic/music/\\n`
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
path: "src/index.ts",
|
|
1307
|
+
contents: `import { registerRoot } from "remotion";
|
|
1308
|
+
import { RemotionRoot } from "./Root";
|
|
1309
|
+
|
|
1310
|
+
registerRoot(RemotionRoot);
|
|
1311
|
+
`
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
path: "src/Root.tsx",
|
|
1315
|
+
contents: `import React from "react";
|
|
1316
|
+
import { Composition } from "remotion";
|
|
1317
|
+
import { Video, TOTAL_FRAMES, FPS } from "./Video";
|
|
1318
|
+
|
|
1319
|
+
export const RemotionRoot: React.FC = () => (
|
|
1320
|
+
<Composition
|
|
1321
|
+
id="Video"
|
|
1322
|
+
component={Video}
|
|
1323
|
+
durationInFrames={TOTAL_FRAMES}
|
|
1324
|
+
fps={FPS}
|
|
1325
|
+
width={1280}
|
|
1326
|
+
height={720}
|
|
1327
|
+
/>
|
|
1328
|
+
);
|
|
1329
|
+
`
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
path: "src/Video.tsx",
|
|
1333
|
+
contents: `import React from "react";
|
|
1334
|
+
import { AbsoluteFill, Audio, Series, staticFile } from "remotion";
|
|
1335
|
+
|
|
1336
|
+
import { BeforeAfter } from "./scenes/BeforeAfter";
|
|
1337
|
+
import { DeviceFrame } from "./scenes/DeviceFrame";
|
|
1338
|
+
import { TitleCard } from "./scenes/TitleCard";
|
|
1339
|
+
import { color } from "./theme";
|
|
1340
|
+
|
|
1341
|
+
export const FPS = 60;
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* The running order, and the only place scene lengths live.
|
|
1345
|
+
*
|
|
1346
|
+
* Read the titles on their own. If they do not tell the story in order, no
|
|
1347
|
+
* amount of animation will rescue it.
|
|
1348
|
+
*
|
|
1349
|
+
* At 120 BPM and 60fps a beat is 30 frames, so every length here is a multiple
|
|
1350
|
+
* of 30 and the cuts land on the beat. scripts/make-music.py reads these
|
|
1351
|
+
* numbers back out of this file, so changing one keeps the music in step.
|
|
1352
|
+
*/
|
|
1353
|
+
export const SCENES = [
|
|
1354
|
+
{
|
|
1355
|
+
id: "hook",
|
|
1356
|
+
frames: 240,
|
|
1357
|
+
node: <TitleCard title="Your product, in sixty seconds." />,
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
id: "problem",
|
|
1361
|
+
frames: 270,
|
|
1362
|
+
node: <TitleCard title="Here is the part people struggle with." muted />,
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
id: "product",
|
|
1366
|
+
frames: 300,
|
|
1367
|
+
node: <DeviceFrame caption="So we built this." />,
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
id: "difference",
|
|
1371
|
+
frames: 330,
|
|
1372
|
+
node: <BeforeAfter beforeLabel="Before" afterLabel="After" />,
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
id: "cta",
|
|
1376
|
+
frames: 240,
|
|
1377
|
+
node: <TitleCard title="Try it today." />,
|
|
1378
|
+
},
|
|
1379
|
+
];
|
|
1380
|
+
|
|
1381
|
+
export const TOTAL_FRAMES = SCENES.reduce((sum, scene) => sum + scene.frames, 0);
|
|
1382
|
+
|
|
1383
|
+
export const Video: React.FC = () => (
|
|
1384
|
+
<AbsoluteFill style={{ backgroundColor: color.background }}>
|
|
1385
|
+
{/* Delete this line until you have run: bun run music */}
|
|
1386
|
+
<Audio src={staticFile("music/bed.wav")} />
|
|
1387
|
+
<Series>
|
|
1388
|
+
{SCENES.map((scene) => (
|
|
1389
|
+
<Series.Sequence key={scene.id} durationInFrames={scene.frames}>
|
|
1390
|
+
{scene.node}
|
|
1391
|
+
</Series.Sequence>
|
|
1392
|
+
))}
|
|
1393
|
+
</Series>
|
|
1394
|
+
</AbsoluteFill>
|
|
1395
|
+
);
|
|
1396
|
+
`
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
path: "src/theme.ts",
|
|
1400
|
+
contents: `/**
|
|
1401
|
+
* Replace these with your own design system's tokens so the film and the app
|
|
1402
|
+
* are the same brand. If the project uses Better Design, copy the values
|
|
1403
|
+
* straight out of globals.css.
|
|
1404
|
+
*/
|
|
1405
|
+
export const color = {
|
|
1406
|
+
background: "oklch(0.98 0.004 90)",
|
|
1407
|
+
foreground: "oklch(0.24 0.005 75)",
|
|
1408
|
+
card: "oklch(1 0 0)",
|
|
1409
|
+
muted: "oklch(0.94 0.006 86)",
|
|
1410
|
+
mutedFg: "oklch(0.52 0.008 80)",
|
|
1411
|
+
border: "oklch(0.9 0.008 85)",
|
|
1412
|
+
accent: "oklch(0.62 0.2 256)",
|
|
1413
|
+
accentFg: "oklch(1 0 0)",
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
export const radius = { sm: 8, md: 16, lg: 28 };
|
|
1417
|
+
|
|
1418
|
+
export const shadow = {
|
|
1419
|
+
card: "0 18px 40px oklch(0 0 0 / 0.10), 0 2px 6px oklch(0 0 0 / 0.06)",
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Load a webfont through @remotion/google-fonts rather than a CSS import, so
|
|
1424
|
+
* the render waits for it. A font that arrives late changes every text
|
|
1425
|
+
* measurement in the frame.
|
|
1426
|
+
*/
|
|
1427
|
+
export const sans =
|
|
1428
|
+
'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
1429
|
+
|
|
1430
|
+
export const type = {
|
|
1431
|
+
title: { fontSize: 76, fontWeight: 600, letterSpacing: "-0.04em" },
|
|
1432
|
+
caption: { fontSize: 28, fontWeight: 500, letterSpacing: "-0.01em" },
|
|
1433
|
+
label: { fontSize: 20, fontWeight: 500, letterSpacing: "0" },
|
|
1434
|
+
};
|
|
1435
|
+
`
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
path: "src/scenes/TitleCard.tsx",
|
|
1439
|
+
contents: `import React from "react";
|
|
1440
|
+
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
1441
|
+
|
|
1442
|
+
import { color, sans, type } from "../theme";
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* One sentence, held still. Note the two things every scene here does:
|
|
1446
|
+
* it starts visible enough to read on frame one, and it stops moving before
|
|
1447
|
+
* the scene ends.
|
|
1448
|
+
*/
|
|
1449
|
+
export const TitleCard: React.FC<{ title: string; muted?: boolean }> = ({
|
|
1450
|
+
title,
|
|
1451
|
+
muted = false,
|
|
1452
|
+
}) => {
|
|
1453
|
+
const frame = useCurrentFrame();
|
|
1454
|
+
const { fps } = useVideoConfig();
|
|
1455
|
+
|
|
1456
|
+
const enter = spring({ frame, fps, config: { damping: 26 } });
|
|
1457
|
+
const rise = interpolate(enter, [0, 1], [18, 0]);
|
|
1458
|
+
const fade = interpolate(frame, [0, 12], [0, 1], {
|
|
1459
|
+
extrapolateLeft: "clamp",
|
|
1460
|
+
extrapolateRight: "clamp",
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
return (
|
|
1464
|
+
<AbsoluteFill
|
|
1465
|
+
style={{
|
|
1466
|
+
alignItems: "center",
|
|
1467
|
+
justifyContent: "center",
|
|
1468
|
+
padding: 96,
|
|
1469
|
+
fontFamily: sans,
|
|
1470
|
+
}}
|
|
1471
|
+
>
|
|
1472
|
+
<h1
|
|
1473
|
+
style={{
|
|
1474
|
+
...type.title,
|
|
1475
|
+
margin: 0,
|
|
1476
|
+
width: "100%",
|
|
1477
|
+
textAlign: "center",
|
|
1478
|
+
color: muted ? color.mutedFg : color.foreground,
|
|
1479
|
+
opacity: fade,
|
|
1480
|
+
transform: "translateY(" + rise + "px)",
|
|
1481
|
+
}}
|
|
1482
|
+
>
|
|
1483
|
+
{title}
|
|
1484
|
+
</h1>
|
|
1485
|
+
</AbsoluteFill>
|
|
1486
|
+
);
|
|
1487
|
+
};
|
|
1488
|
+
`
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
path: "src/scenes/DeviceFrame.tsx",
|
|
1492
|
+
contents: `import React from "react";
|
|
1493
|
+
import { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from "remotion";
|
|
1494
|
+
|
|
1495
|
+
import { color, radius, sans, shadow, type } from "../theme";
|
|
1496
|
+
|
|
1497
|
+
/**
|
|
1498
|
+
* A screenshot inside a frame. Put your own UI where the placeholder is, and
|
|
1499
|
+
* keep any cursor or callout INSIDE this transform, or it drifts off target
|
|
1500
|
+
* the moment the scene scales.
|
|
1501
|
+
*/
|
|
1502
|
+
export const DeviceFrame: React.FC<{ caption: string }> = ({ caption }) => {
|
|
1503
|
+
const frame = useCurrentFrame();
|
|
1504
|
+
const { fps } = useVideoConfig();
|
|
1505
|
+
|
|
1506
|
+
const enter = spring({ frame, fps, config: { damping: 24 } });
|
|
1507
|
+
const scale = interpolate(enter, [0, 1], [0.94, 1]);
|
|
1508
|
+
const fade = interpolate(frame, [0, 10], [0, 1], {
|
|
1509
|
+
extrapolateLeft: "clamp",
|
|
1510
|
+
extrapolateRight: "clamp",
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
return (
|
|
1514
|
+
<AbsoluteFill
|
|
1515
|
+
style={{
|
|
1516
|
+
alignItems: "center",
|
|
1517
|
+
justifyContent: "center",
|
|
1518
|
+
gap: 40,
|
|
1519
|
+
fontFamily: sans,
|
|
1520
|
+
opacity: fade,
|
|
1521
|
+
}}
|
|
1522
|
+
>
|
|
1523
|
+
<div
|
|
1524
|
+
style={{
|
|
1525
|
+
width: 880,
|
|
1526
|
+
height: 460,
|
|
1527
|
+
borderRadius: radius.lg,
|
|
1528
|
+
background: color.card,
|
|
1529
|
+
border: "1px solid " + color.border,
|
|
1530
|
+
boxShadow: shadow.card,
|
|
1531
|
+
transform: "scale(" + scale + ")",
|
|
1532
|
+
display: "grid",
|
|
1533
|
+
placeItems: "center",
|
|
1534
|
+
color: color.mutedFg,
|
|
1535
|
+
...type.label,
|
|
1536
|
+
}}
|
|
1537
|
+
>
|
|
1538
|
+
Your screen here
|
|
1539
|
+
</div>
|
|
1540
|
+
<p style={{ ...type.caption, margin: 0, color: color.foreground }}>{caption}</p>
|
|
1541
|
+
</AbsoluteFill>
|
|
1542
|
+
);
|
|
1543
|
+
};
|
|
1544
|
+
`
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
path: "src/scenes/BeforeAfter.tsx",
|
|
1548
|
+
contents: `import React from "react";
|
|
1549
|
+
import { AbsoluteFill, interpolate, useCurrentFrame } from "remotion";
|
|
1550
|
+
|
|
1551
|
+
import { color, radius, sans, shadow, type } from "../theme";
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* The comparison scene. It only works if both sides are on screen together
|
|
1555
|
+
* long enough to read, so bring the second panel in early and hold both.
|
|
1556
|
+
*/
|
|
1557
|
+
export const BeforeAfter: React.FC<{
|
|
1558
|
+
beforeLabel: string;
|
|
1559
|
+
afterLabel: string;
|
|
1560
|
+
}> = ({ beforeLabel, afterLabel }) => {
|
|
1561
|
+
const frame = useCurrentFrame();
|
|
1562
|
+
|
|
1563
|
+
const left = interpolate(frame, [0, 20], [0, 1], {
|
|
1564
|
+
extrapolateLeft: "clamp",
|
|
1565
|
+
extrapolateRight: "clamp",
|
|
1566
|
+
});
|
|
1567
|
+
const right = interpolate(frame, [30, 55], [0, 1], {
|
|
1568
|
+
extrapolateLeft: "clamp",
|
|
1569
|
+
extrapolateRight: "clamp",
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
const panel = {
|
|
1573
|
+
width: 500,
|
|
1574
|
+
height: 380,
|
|
1575
|
+
borderRadius: radius.md,
|
|
1576
|
+
border: "1px solid " + color.border,
|
|
1577
|
+
background: color.card,
|
|
1578
|
+
boxShadow: shadow.card,
|
|
1579
|
+
display: "grid",
|
|
1580
|
+
placeItems: "center",
|
|
1581
|
+
color: color.mutedFg,
|
|
1582
|
+
...type.label,
|
|
1583
|
+
} as const;
|
|
1584
|
+
|
|
1585
|
+
return (
|
|
1586
|
+
<AbsoluteFill
|
|
1587
|
+
style={{
|
|
1588
|
+
alignItems: "center",
|
|
1589
|
+
justifyContent: "center",
|
|
1590
|
+
gap: 48,
|
|
1591
|
+
flexDirection: "row",
|
|
1592
|
+
fontFamily: sans,
|
|
1593
|
+
}}
|
|
1594
|
+
>
|
|
1595
|
+
<div style={{ opacity: left, display: "grid", gap: 16, justifyItems: "center" }}>
|
|
1596
|
+
<div style={panel}>Your old screen</div>
|
|
1597
|
+
<span style={{ ...type.label, color: color.mutedFg }}>{beforeLabel}</span>
|
|
1598
|
+
</div>
|
|
1599
|
+
<div style={{ opacity: right, display: "grid", gap: 16, justifyItems: "center" }}>
|
|
1600
|
+
<div style={{ ...panel, borderColor: color.accent }}>Your new screen</div>
|
|
1601
|
+
<span style={{ ...type.label, color: color.foreground }}>{afterLabel}</span>
|
|
1602
|
+
</div>
|
|
1603
|
+
</AbsoluteFill>
|
|
1604
|
+
);
|
|
1605
|
+
};
|
|
1606
|
+
`
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
path: "scripts/make-music.py",
|
|
1610
|
+
contents: `"""
|
|
1611
|
+
Synthesises the backing track, so nothing is licensed from anywhere.
|
|
1612
|
+
|
|
1613
|
+
120 BPM at 60fps is a beat every 30 frames. Scene lengths are read straight
|
|
1614
|
+
out of src/Video.tsx, so the swells cannot drift from the edit: change a scene
|
|
1615
|
+
length there, run this again, and the accents move with it.
|
|
1616
|
+
"""
|
|
1617
|
+
|
|
1618
|
+
import math
|
|
1619
|
+
import re
|
|
1620
|
+
import struct
|
|
1621
|
+
import wave
|
|
1622
|
+
from pathlib import Path
|
|
1623
|
+
|
|
1624
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
1625
|
+
SR = 44_100
|
|
1626
|
+
FPS = 60
|
|
1627
|
+
BPM = 120
|
|
1628
|
+
BEAT = 60 / BPM
|
|
1629
|
+
|
|
1630
|
+
SCENES = [
|
|
1631
|
+
int(n)
|
|
1632
|
+
for n in re.findall(r"frames: (\\d+)", (ROOT / "src" / "Video.tsx").read_text())
|
|
1633
|
+
]
|
|
1634
|
+
if not SCENES:
|
|
1635
|
+
raise SystemExit("no \`frames: <n>\` entries found in src/Video.tsx")
|
|
1636
|
+
|
|
1637
|
+
TOTAL = sum(SCENES) / FPS
|
|
1638
|
+
CUTS = []
|
|
1639
|
+
acc = 0
|
|
1640
|
+
for length in SCENES[:-1]:
|
|
1641
|
+
acc += length
|
|
1642
|
+
CUTS.append(acc / FPS)
|
|
1643
|
+
|
|
1644
|
+
# A minor pentatonic pad. Swap these for any scale you like.
|
|
1645
|
+
CHORD = [220.00, 261.63, 329.63, 392.00]
|
|
1646
|
+
|
|
1647
|
+
|
|
1648
|
+
def envelope(t: float, attack: float, release: float, length: float) -> float:
|
|
1649
|
+
if t < 0 or t > length:
|
|
1650
|
+
return 0.0
|
|
1651
|
+
if t < attack:
|
|
1652
|
+
return t / attack
|
|
1653
|
+
if t > length - release:
|
|
1654
|
+
return max(0.0, (length - t) / release)
|
|
1655
|
+
return 1.0
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
samples = []
|
|
1659
|
+
for i in range(int(SR * TOTAL)):
|
|
1660
|
+
t = i / SR
|
|
1661
|
+
value = 0.0
|
|
1662
|
+
|
|
1663
|
+
# Pad: quiet, always there, gives the film a floor.
|
|
1664
|
+
for n, freq in enumerate(CHORD):
|
|
1665
|
+
value += math.sin(2 * math.pi * freq * t) * (0.05 / (n + 1))
|
|
1666
|
+
|
|
1667
|
+
# A pluck on every beat, louder on the downbeat.
|
|
1668
|
+
beat_pos = t % BEAT
|
|
1669
|
+
beat_index = int(t / BEAT)
|
|
1670
|
+
accent = 1.0 if beat_index % 4 == 0 else 0.45
|
|
1671
|
+
value += (
|
|
1672
|
+
math.sin(2 * math.pi * CHORD[beat_index % len(CHORD)] * 2 * t)
|
|
1673
|
+
* envelope(beat_pos, 0.005, 0.18, BEAT)
|
|
1674
|
+
* 0.10
|
|
1675
|
+
* accent
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
# A swell into every cut, so the edit and the music agree.
|
|
1679
|
+
for cut in CUTS:
|
|
1680
|
+
since = t - (cut - 0.6)
|
|
1681
|
+
if 0 <= since <= 0.6:
|
|
1682
|
+
value += math.sin(2 * math.pi * 110 * t) * (since / 0.6) * 0.12
|
|
1683
|
+
|
|
1684
|
+
samples.append(value)
|
|
1685
|
+
|
|
1686
|
+
# Normalise, or it renders far too quiet under the picture.
|
|
1687
|
+
peak = max(abs(s) for s in samples) or 1.0
|
|
1688
|
+
gain = 0.89 / peak
|
|
1689
|
+
|
|
1690
|
+
out = ROOT / "public" / "music"
|
|
1691
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
1692
|
+
path = out / "bed.wav"
|
|
1693
|
+
|
|
1694
|
+
with wave.open(str(path), "w") as f:
|
|
1695
|
+
f.setnchannels(1)
|
|
1696
|
+
f.setsampwidth(2)
|
|
1697
|
+
f.setframerate(SR)
|
|
1698
|
+
f.writeframes(
|
|
1699
|
+
b"".join(struct.pack("<h", int(max(-1.0, min(1.0, s * gain)) * 32767)) for s in samples)
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
print(f"wrote {path.name}: {TOTAL:.2f}s, {len(CUTS)} cuts")
|
|
1703
|
+
`
|
|
1704
|
+
},
|
|
1705
|
+
{
|
|
1706
|
+
path: "README.md",
|
|
1707
|
+
contents: `# motion
|
|
1708
|
+
|
|
1709
|
+
The video for this project. Every frame is React, so a scene is reviewed and
|
|
1710
|
+
changed like any other component.
|
|
1711
|
+
|
|
1712
|
+
## Run it
|
|
1713
|
+
|
|
1714
|
+
\`\`\`bash
|
|
1715
|
+
bun install # or npm install. The first run pulls a headless Chrome
|
|
1716
|
+
bun run music # writes public/music/bed.wav
|
|
1717
|
+
bun run dev # opens the studio, scrub and edit with hot reload
|
|
1718
|
+
bun run render # writes out/video.mp4
|
|
1719
|
+
\`\`\`
|
|
1720
|
+
|
|
1721
|
+
Rendering happens on your machine. Nothing is uploaded.
|
|
1722
|
+
|
|
1723
|
+
## Where to start
|
|
1724
|
+
|
|
1725
|
+
\`src/Video.tsx\` is the running order and the only place scene lengths live.
|
|
1726
|
+
Read the scene ids on their own: if they do not tell your story in order, fix
|
|
1727
|
+
that before touching any animation.
|
|
1728
|
+
|
|
1729
|
+
\`src/theme.ts\` holds the colours and type. Replace them with your own design
|
|
1730
|
+
system's tokens so the film and the app are the same brand.
|
|
1731
|
+
|
|
1732
|
+
\`src/scenes/\` has three patterns to copy: a title card, a screenshot in a
|
|
1733
|
+
frame, and a before and after.
|
|
1734
|
+
|
|
1735
|
+
## Two rules worth keeping
|
|
1736
|
+
|
|
1737
|
+
Scene lengths are multiples of 30 frames, which at 60fps and 120 BPM puts every
|
|
1738
|
+
cut on a beat. \`scripts/make-music.py\` reads those lengths back out of
|
|
1739
|
+
\`src/Video.tsx\`, so the music follows the edit rather than the other way round.
|
|
1740
|
+
|
|
1741
|
+
A render that succeeds is not a render that is correct. Before you ship it,
|
|
1742
|
+
step through the frames:
|
|
1743
|
+
|
|
1744
|
+
\`\`\`bash
|
|
1745
|
+
ffmpeg -i out/video.mp4 -vf fps=3 frames/%04d.png
|
|
1746
|
+
\`\`\`
|
|
1747
|
+
|
|
1748
|
+
Sampling once a second is not enough. Check the first and last frame of every
|
|
1749
|
+
scene, which is where entry and exit animations fail.
|
|
1750
|
+
|
|
1751
|
+
## Shipping it to the web
|
|
1752
|
+
|
|
1753
|
+
\`\`\`bash
|
|
1754
|
+
ffmpeg -i out/video-1080.mp4 -vf scale=1280:-2 -c:v libx264 -crf 26 \\
|
|
1755
|
+
-preset slow -movflags +faststart -c:a aac -b:a 96k web.mp4
|
|
1756
|
+
\`\`\`
|
|
1757
|
+
|
|
1758
|
+
\`+faststart\` matters: without it the player waits for the whole file before it
|
|
1759
|
+
can begin.
|
|
1760
|
+
`
|
|
1761
|
+
}
|
|
1762
|
+
];
|
|
1763
|
+
MOTION_SCAFFOLD_STEPS = [
|
|
1764
|
+
"Write every file in MOTION_SCAFFOLD_FILES into a `motion/` directory at the repo root. Keep it out of the app's workspace: the first install pulls a headless Chrome and is around 600 MB.",
|
|
1765
|
+
"Replace the tokens in src/theme.ts with the project's own colours, radii and type, so the film and the app are the same brand.",
|
|
1766
|
+
"Rewrite the scene list in src/Video.tsx for this product. Keep every length a multiple of 30 frames so the cuts stay on the beat.",
|
|
1767
|
+
"Tell the user to run: bun install, then bun run music, then bun run render.",
|
|
1768
|
+
"Do not claim the video is finished until they have watched it. A render that succeeds is not a render that is correct."
|
|
1769
|
+
].join("\n");
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1104
1773
|
// ../shared/src/foundational-docs/topic-utils.ts
|
|
1105
1774
|
function normalizePrincipleTopic(value) {
|
|
1106
1775
|
return value.trim().toLowerCase().replace(/^principle-/, "").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1180,6 +1849,18 @@ function formatTopicNotFound(topic, principles, toolName, resourceLabel) {
|
|
|
1180
1849
|
`Call \`${toolName}({})\` for the full index.`
|
|
1181
1850
|
].filter(Boolean).join("\n");
|
|
1182
1851
|
}
|
|
1852
|
+
function appendReviewNudge(response, session) {
|
|
1853
|
+
const nudge = reviewNudge(session);
|
|
1854
|
+
if (!nudge) return response;
|
|
1855
|
+
const [first, ...rest] = response.content;
|
|
1856
|
+
if (!first) return response;
|
|
1857
|
+
return {
|
|
1858
|
+
...response,
|
|
1859
|
+
content: [{ ...first, text: `${first.text}
|
|
1860
|
+
|
|
1861
|
+
${nudge}` }, ...rest]
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1183
1864
|
async function runPrincipleTool(provider, area, label, toolName, args) {
|
|
1184
1865
|
const { topic, query, maxTokens } = args;
|
|
1185
1866
|
const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" ? "Guides" : "Principles";
|
|
@@ -1257,14 +1938,22 @@ ${formatted}`;
|
|
|
1257
1938
|
capture: { topResults: [] }
|
|
1258
1939
|
};
|
|
1259
1940
|
}
|
|
1260
|
-
function
|
|
1941
|
+
async function runGuidanceTool(provider, userId, clientName, area, label, toolName, args) {
|
|
1942
|
+
const [run, session] = await Promise.all([
|
|
1943
|
+
runPrincipleTool(provider, area, label, toolName, args),
|
|
1944
|
+
loadSessionGuidance(provider, userId, clientName)
|
|
1945
|
+
]);
|
|
1946
|
+
return { ...run, response: appendReviewNudge(run.response, session) };
|
|
1947
|
+
}
|
|
1948
|
+
function registerPrincipleTools({ server, provider, track, userId }, { enableReactNativeGuide = true, enableThreeJsGuide = true } = {}) {
|
|
1949
|
+
const clientName = () => mcpClientName(server);
|
|
1261
1950
|
server.registerTool(
|
|
1262
1951
|
"get-ui-principle",
|
|
1263
1952
|
{
|
|
1264
1953
|
title: "Get UI Design Principle",
|
|
1265
1954
|
description: `Load visual UI design principles into context on-demand.
|
|
1266
1955
|
|
|
1267
|
-
Use this for visual surface decisions: hierarchy and emphasis, spacing/layout and grouping, typography, color, depth, images, accessibility, polish, and animation. For behavior, flows, forms, navigation, notifications, errors, search, onboarding, microcopy, rewards, behavior change, design process, or psychology/cognitive laws, use get-ux-principle instead.
|
|
1956
|
+
Use this for visual surface decisions: hierarchy and emphasis, spacing/layout and grouping, typography, color, depth, images, accessibility, polish, and animation. Building a shop, product page, or product imagery: load product-pages and product-imagery. For behavior, flows, forms, navigation, notifications, errors, search, onboarding, microcopy, rewards, behavior change, design process, or psychology/cognitive laws, use get-ux-principle instead.
|
|
1268
1957
|
|
|
1269
1958
|
You can either:
|
|
1270
1959
|
1. Request a specific topic or alias: get-ui-principle({ topic: "spacing" })
|
|
@@ -1273,17 +1962,17 @@ You can either:
|
|
|
1273
1962
|
|
|
1274
1963
|
Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
1275
1964
|
inputSchema: z2.object({
|
|
1276
|
-
topic: z2.string().trim().min(1).max(120).optional().describe("Specific UI topic or alias to load, e.g. spacing, gestalt, hierarchy, blur test, accessibility, color, depth, animation."),
|
|
1965
|
+
topic: z2.string().trim().min(1).max(120).optional().describe("Specific UI topic or alias to load, e.g. spacing, gestalt, hierarchy, blur test, accessibility, color, depth, animation, product-pages, product-imagery."),
|
|
1277
1966
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language search query to find relevant principles."),
|
|
1278
1967
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1279
1968
|
context: CONTEXT_ARG
|
|
1280
1969
|
}),
|
|
1281
|
-
annotations:
|
|
1970
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1282
1971
|
},
|
|
1283
1972
|
async ({ topic, query, maxTokens, context }) => {
|
|
1284
1973
|
const start = Date.now();
|
|
1285
1974
|
try {
|
|
1286
|
-
const { response, capture } = await
|
|
1975
|
+
const { response, capture } = await runGuidanceTool(provider, userId, clientName(), "ui", "UI", "get-ui-principle", { topic, query, maxTokens });
|
|
1287
1976
|
return trackSearch(track, "get-ui-principle", { ...capture, context }, start, response);
|
|
1288
1977
|
} catch (error) {
|
|
1289
1978
|
return trackSearch(
|
|
@@ -1304,7 +1993,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1304
1993
|
title: "Get UX Design Principle",
|
|
1305
1994
|
description: `Load UX behavior and product-flow principles into context on-demand.
|
|
1306
1995
|
|
|
1307
|
-
Use this before implementing flows or interactive behavior: forms, navigation, notifications, errors, dialogs, search, onboarding, responsive behavior, microcopy, rewards and behavior change, design process and clarity audits, motion briefs, interaction patterns, mobile patterns, sound, anti-patterns, understanding users, and cognitive laws / psychology / behavioral science.
|
|
1996
|
+
Use this before implementing flows or interactive behavior: forms, navigation, notifications, errors, dialogs, search, onboarding, responsive behavior, microcopy, rewards and behavior change, design process and clarity audits, motion briefs, interaction patterns, mobile patterns, sound, anti-patterns, understanding users, and cognitive laws / psychology / behavioral science. Building a catalog, collection page, or product listing: load commerce-catalog. Building a cart, checkout, or payment step: load commerce-checkout.
|
|
1308
1997
|
|
|
1309
1998
|
You can either:
|
|
1310
1999
|
1. Request a specific topic: get-ux-principle({ topic: "behavior-change" })
|
|
@@ -1314,17 +2003,17 @@ You can either:
|
|
|
1314
2003
|
|
|
1315
2004
|
Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
1316
2005
|
inputSchema: z2.object({
|
|
1317
|
-
topic: z2.string().trim().min(1).max(120).optional().describe("Specific UX topic or alias to load, e.g. forms, navigation, microcopy, reward design, behavior-change, clarity audit, motion brief, design-process, cognitive-laws, psychology."),
|
|
2006
|
+
topic: z2.string().trim().min(1).max(120).optional().describe("Specific UX topic or alias to load, e.g. forms, navigation, microcopy, reward design, behavior-change, clarity audit, motion brief, design-process, cognitive-laws, psychology, commerce-catalog, commerce-checkout."),
|
|
1318
2007
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language search query to find relevant UX principles."),
|
|
1319
2008
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1320
2009
|
context: CONTEXT_ARG
|
|
1321
2010
|
}),
|
|
1322
|
-
annotations:
|
|
2011
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1323
2012
|
},
|
|
1324
2013
|
async ({ topic, query, maxTokens, context }) => {
|
|
1325
2014
|
const start = Date.now();
|
|
1326
2015
|
try {
|
|
1327
|
-
const { response, capture } = await
|
|
2016
|
+
const { response, capture } = await runGuidanceTool(provider, userId, clientName(), "ux", "UX", "get-ux-principle", { topic, query, maxTokens });
|
|
1328
2017
|
return trackSearch(track, "get-ux-principle", { ...capture, context }, start, response);
|
|
1329
2018
|
} catch (error) {
|
|
1330
2019
|
return trackSearch(
|
|
@@ -1365,13 +2054,15 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
|
|
|
1365
2054
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1366
2055
|
context: CONTEXT_ARG
|
|
1367
2056
|
}),
|
|
1368
|
-
annotations:
|
|
2057
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1369
2058
|
},
|
|
1370
2059
|
async ({ topic, query, maxTokens, context }) => {
|
|
1371
2060
|
const start = Date.now();
|
|
1372
2061
|
try {
|
|
1373
|
-
const { response, capture } = await
|
|
2062
|
+
const { response, capture } = await runGuidanceTool(
|
|
1374
2063
|
provider,
|
|
2064
|
+
userId,
|
|
2065
|
+
clientName(),
|
|
1375
2066
|
"react-native",
|
|
1376
2067
|
"React Native",
|
|
1377
2068
|
"get-react-native-guide",
|
|
@@ -1422,13 +2113,15 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
|
|
|
1422
2113
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1423
2114
|
context: CONTEXT_ARG
|
|
1424
2115
|
}),
|
|
1425
|
-
annotations:
|
|
2116
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1426
2117
|
},
|
|
1427
2118
|
async ({ topic, query, maxTokens, context }) => {
|
|
1428
2119
|
const start = Date.now();
|
|
1429
2120
|
try {
|
|
1430
|
-
const { response, capture } = await
|
|
2121
|
+
const { response, capture } = await runGuidanceTool(
|
|
1431
2122
|
provider,
|
|
2123
|
+
userId,
|
|
2124
|
+
clientName(),
|
|
1432
2125
|
"three-js",
|
|
1433
2126
|
"Three.js",
|
|
1434
2127
|
"get-three-js-guide",
|
|
@@ -1465,30 +2158,60 @@ Use this before writing a Remotion composition, and while debugging one. It cove
|
|
|
1465
2158
|
|
|
1466
2159
|
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
2160
|
|
|
1468
|
-
|
|
2161
|
+
Start with scaffold: true. It returns a complete Remotion project that already renders, so you adapt something working instead of inventing the layout, the render pipeline and the audio script.
|
|
2162
|
+
|
|
2163
|
+
This tool does not render. Rendering needs Remotion and a headless Chrome in the user's own project, around 600 MB on first install. Write the files into their repo and let them run it.
|
|
1469
2164
|
|
|
1470
2165
|
This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
|
|
1471
2166
|
|
|
1472
2167
|
You can either:
|
|
1473
|
-
1.
|
|
1474
|
-
2.
|
|
1475
|
-
3.
|
|
1476
|
-
4.
|
|
2168
|
+
1. Get the starter project: get-motion-guide({ scaffold: true })
|
|
2169
|
+
2. Request a known topic: get-motion-guide({ topic: "video-fundamentals" })
|
|
2170
|
+
3. Use an alias: get-motion-guide({ topic: "cut on the beat" })
|
|
2171
|
+
4. Search for the current task or problem: get-motion-guide({ query: "my animated word swap clips the longer word" })
|
|
2172
|
+
5. Get the full topic index: get-motion-guide({})
|
|
1477
2173
|
|
|
1478
2174
|
Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
|
|
1479
2175
|
inputSchema: z2.object({
|
|
1480
|
-
|
|
2176
|
+
scaffold: z2.boolean().optional().describe("Return a complete Remotion project that already renders, as files to write. Start here when building a video."),
|
|
2177
|
+
topic: z2.string().trim().min(1).max(120).optional().describe("Specific motion topic or alias, e.g. video-fundamentals, text-and-layout, story-and-rhythm, verifying-a-render."),
|
|
1481
2178
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the video or animation task or problem."),
|
|
1482
2179
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1483
2180
|
context: CONTEXT_ARG
|
|
1484
2181
|
}),
|
|
1485
|
-
annotations:
|
|
2182
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1486
2183
|
},
|
|
1487
|
-
async ({ topic, query, maxTokens, context }) => {
|
|
2184
|
+
async ({ scaffold, topic, query, maxTokens, context }) => {
|
|
1488
2185
|
const start = Date.now();
|
|
2186
|
+
if (scaffold) {
|
|
2187
|
+
const files = MOTION_SCAFFOLD_FILES.map(
|
|
2188
|
+
(file) => `--- ${file.path} ---
|
|
2189
|
+
${file.contents}`
|
|
2190
|
+
).join("\n");
|
|
2191
|
+
return trackSearch(
|
|
2192
|
+
track,
|
|
2193
|
+
"get-motion-guide",
|
|
2194
|
+
{ query, topic: "scaffold", topResults: [], context },
|
|
2195
|
+
start,
|
|
2196
|
+
{
|
|
2197
|
+
content: [
|
|
2198
|
+
{
|
|
2199
|
+
type: "text",
|
|
2200
|
+
text: `A Remotion project that already renders. Write these files, then follow the steps.
|
|
2201
|
+
|
|
2202
|
+
${MOTION_SCAFFOLD_STEPS}
|
|
2203
|
+
|
|
2204
|
+
${files}`
|
|
2205
|
+
}
|
|
2206
|
+
]
|
|
2207
|
+
}
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
1489
2210
|
try {
|
|
1490
|
-
const { response, capture } = await
|
|
2211
|
+
const { response, capture } = await runGuidanceTool(
|
|
1491
2212
|
provider,
|
|
2213
|
+
userId,
|
|
2214
|
+
clientName(),
|
|
1492
2215
|
"motion",
|
|
1493
2216
|
"Motion",
|
|
1494
2217
|
"get-motion-guide",
|
|
@@ -1537,6 +2260,7 @@ You MUST call this after writing UI code to self-review your output, then fix ev
|
|
|
1537
2260
|
- Microcopy details: pluralization, improvised unit glyphs, empty states without a next action
|
|
1538
2261
|
- Template layouts: gradient hero + eyebrow pill, left-border callout boxes
|
|
1539
2262
|
- Reward feedback: confirm the outcome, surface proportional user value, and avoid decorative over-reward
|
|
2263
|
+
- Prose tells in shipped paragraphs (hero, about, pricing, changelog, docs): banned words (delve, leverage, robust, cutting-edge, elevate), binary contrasts ("It's not X, it's Y"), throat-clearing and faux-insight openers, colon reveals, puffery and unsourced "experts agree", fake-profound kickers and recap endings, synonym cycling, emoji headings and mid-sentence bold
|
|
1540
2264
|
|
|
1541
2265
|
**Comprehension (would someone outside the team understand it?):**
|
|
1542
2266
|
- Mom test: insider vocabulary, unexplained acronyms, sentences too long to follow
|
|
@@ -1550,12 +2274,16 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1550
2274
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000\u201310000."),
|
|
1551
2275
|
context: CONTEXT_ARG
|
|
1552
2276
|
}),
|
|
1553
|
-
annotations:
|
|
2277
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1554
2278
|
},
|
|
1555
2279
|
async ({ category = "all", maxTokens, context }) => {
|
|
1556
2280
|
const start = Date.now();
|
|
1557
2281
|
try {
|
|
1558
|
-
const
|
|
2282
|
+
const [allRules, session] = await Promise.all([
|
|
2283
|
+
provider.getReviewRules(category),
|
|
2284
|
+
loadSessionGuidance(provider, userId, clientName())
|
|
2285
|
+
]);
|
|
2286
|
+
const { rules, sections } = weightReviewRules(allRules, session.topics);
|
|
1559
2287
|
const topResults = rules.map((rule) => ({ id: rule.id }));
|
|
1560
2288
|
if (rules.length === 0) {
|
|
1561
2289
|
return trackSearch(
|
|
@@ -1569,9 +2297,12 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1569
2297
|
const rulesOutput = rules.map((rule) => `# ${rule.title}
|
|
1570
2298
|
|
|
1571
2299
|
${rule.content}`).join("\n\n---\n\n");
|
|
2300
|
+
const preamble = guidancePreamble(session.topics, "the build", sections);
|
|
1572
2301
|
let text = `# Code Review Rules
|
|
1573
2302
|
|
|
1574
|
-
${
|
|
2303
|
+
${preamble ? `${preamble}
|
|
2304
|
+
|
|
2305
|
+
` : ""}${rulesOutput}`;
|
|
1575
2306
|
if (maxTokens) text = truncateToTokens(text, maxTokens);
|
|
1576
2307
|
return trackSearch(
|
|
1577
2308
|
track,
|
|
@@ -1598,8 +2329,10 @@ var SEARCH_RESULT_PREVIEW_CHARS;
|
|
|
1598
2329
|
var init_principle_tools = __esm({
|
|
1599
2330
|
"../shared/src/mcp/tools/principle-tools.ts"() {
|
|
1600
2331
|
"use strict";
|
|
2332
|
+
init_motion_scaffold();
|
|
1601
2333
|
init_topic_utils();
|
|
1602
2334
|
init_shared();
|
|
2335
|
+
init_session_guidance();
|
|
1603
2336
|
SEARCH_RESULT_PREVIEW_CHARS = 4e3;
|
|
1604
2337
|
}
|
|
1605
2338
|
});
|
|
@@ -2079,7 +2812,7 @@ var init_comprehension = __esm({
|
|
|
2079
2812
|
|
|
2080
2813
|
// ../shared/src/mcp/tools/comprehension-tools.ts
|
|
2081
2814
|
import { z as z3 } from "zod";
|
|
2082
|
-
function registerComprehensionTools({ server, track }) {
|
|
2815
|
+
function registerComprehensionTools({ server, provider, track, userId }) {
|
|
2083
2816
|
server.registerTool(
|
|
2084
2817
|
"check-comprehension",
|
|
2085
2818
|
{
|
|
@@ -2107,13 +2840,18 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
|
|
|
2107
2840
|
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."),
|
|
2108
2841
|
context: CONTEXT_ARG
|
|
2109
2842
|
}),
|
|
2110
|
-
annotations:
|
|
2843
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
2111
2844
|
},
|
|
2112
2845
|
async ({ context, ...args }) => {
|
|
2113
2846
|
const start = Date.now();
|
|
2114
2847
|
const report = analyzeComprehension(args);
|
|
2115
2848
|
const hasCopy = Boolean(report.copy.headline) || report.copy.body.length > 0 || report.copy.actions.length > 0;
|
|
2116
|
-
const
|
|
2849
|
+
const body = hasCopy ? formatComprehensionReport(report) : NOTHING_TO_CHECK;
|
|
2850
|
+
const session = await loadSessionGuidance(provider, userId, mcpClientName(server));
|
|
2851
|
+
const preamble = guidancePreamble(session.topics, "this copy");
|
|
2852
|
+
const text = preamble ? `${preamble}
|
|
2853
|
+
|
|
2854
|
+
${body}` : body;
|
|
2117
2855
|
track(
|
|
2118
2856
|
"check-comprehension",
|
|
2119
2857
|
{
|
|
@@ -2134,6 +2872,7 @@ var init_comprehension_tools = __esm({
|
|
|
2134
2872
|
"use strict";
|
|
2135
2873
|
init_comprehension();
|
|
2136
2874
|
init_shared();
|
|
2875
|
+
init_session_guidance();
|
|
2137
2876
|
NOTHING_TO_CHECK = `# Comprehension check
|
|
2138
2877
|
|
|
2139
2878
|
No user-facing copy was found. Pass the screen's copy directly:
|
|
@@ -2292,7 +3031,7 @@ Examples:
|
|
|
2292
3031
|
inputSchema: z4.object({
|
|
2293
3032
|
query: z4.string().describe("Style description or personality traits to match.")
|
|
2294
3033
|
}),
|
|
2295
|
-
annotations:
|
|
3034
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
2296
3035
|
};
|
|
2297
3036
|
const makeIconLibraryHandler = (toolName) => async ({ query }) => {
|
|
2298
3037
|
const start = Date.now();
|
|
@@ -2362,13 +3101,13 @@ Examples:
|
|
|
2362
3101
|
- find-icons({ query: "home", libraryId: "phosphor" })
|
|
2363
3102
|
- find-icons({ query: "settings", libraryId: "tabler" })
|
|
2364
3103
|
|
|
2365
|
-
|
|
3104
|
+
Every name returned exists in the library, so pass them to \`install-icons\` verbatim. A \`variant\` only re-orders the results, putting the names that carry that variant first; it never rewrites a name.`,
|
|
2366
3105
|
inputSchema: z4.object({
|
|
2367
3106
|
query: z4.string().describe("What icon you need (e.g., 'home', 'settings', 'user')"),
|
|
2368
3107
|
libraryId: z4.string().describe("The icon library ID from find-icon-library"),
|
|
2369
3108
|
variant: z4.string().optional().describe("Optional variant style (e.g., 'bold', 'outline', 'duotone').")
|
|
2370
3109
|
}),
|
|
2371
|
-
annotations:
|
|
3110
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
2372
3111
|
};
|
|
2373
3112
|
const makeIconSearchHandler = (toolName) => async ({ query, libraryId, variant }) => {
|
|
2374
3113
|
const start = Date.now();
|
|
@@ -2452,7 +3191,7 @@ If \`components/icons/index.ts\` already exists in the project, MERGE the new ex
|
|
|
2452
3191
|
variant: z4.string().optional().describe("Optional library variant (e.g. 'outline', 'filled')."),
|
|
2453
3192
|
context: CONTEXT_ARG
|
|
2454
3193
|
}),
|
|
2455
|
-
annotations:
|
|
3194
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
2456
3195
|
},
|
|
2457
3196
|
async ({ libraryId, names, targetDir, format, variant, context }) => tracked(
|
|
2458
3197
|
"install-icons",
|
|
@@ -2657,7 +3396,7 @@ Returns either:
|
|
|
2657
3396
|
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."),
|
|
2658
3397
|
context: CONTEXT_ARG
|
|
2659
3398
|
}),
|
|
2660
|
-
annotations:
|
|
3399
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
2661
3400
|
},
|
|
2662
3401
|
async ({ manifestContent, context }) => tracked("detect-icon-library", { hasManifest: Boolean(manifestContent), context }, async () => {
|
|
2663
3402
|
if (!manifestContent || !manifestContent.trim()) {
|
|
@@ -2735,6 +3474,21 @@ context in the supporting line or after the title. Skip the generated-template s
|
|
|
2735
3474
|
gradient hero with an uppercase eyebrow pill, centered three-card feature grid,
|
|
2736
3475
|
left-border callout boxes, walls of text.
|
|
2737
3476
|
|
|
3477
|
+
**Count the copy before you present.** This is a check to run, not advice to weigh.
|
|
3478
|
+
On the first view: at most 60 visible words, at most 25 separate text fragments, one
|
|
3479
|
+
headline of about 6 words, and at most one supporting line of about 15 words per
|
|
3480
|
+
section. Count them. If you are over, delete copy; do not shrink the type to fit and
|
|
3481
|
+
do not move the words into a tooltip. A section that needs three sentences to explain
|
|
3482
|
+
itself is a section whose layout is wrong.
|
|
3483
|
+
|
|
3484
|
+
**Labels are sentence case, never letter-spaced capitals.** Do not style section
|
|
3485
|
+
labels, stat captions, table column headers, card headers, or badges as tiny spaced
|
|
3486
|
+
capitals (\`text-xs uppercase tracking-widest\`, with or without \`font-mono\`). It is
|
|
3487
|
+
the single fastest generated-UI tell, and it costs legibility at exactly the size
|
|
3488
|
+
where legibility is scarcest. Use sentence case at the DS's own label size and muted
|
|
3489
|
+
color. Keep \`font-mono\` for code, IDs, keyboard shortcuts, and figures that must
|
|
3490
|
+
align in a column.
|
|
3491
|
+
|
|
2738
3492
|
**Hierarchy is pass/fail.** Give the first view one P0 task and at most one P1
|
|
2739
3493
|
summary; move P2 history, standings, detailed metadata, and empty-state chrome
|
|
2740
3494
|
below it or behind a tab/disclosure. Unless the user explicitly asks for a dense
|
|
@@ -2758,7 +3512,8 @@ After writing UI, self-review with
|
|
|
2758
3512
|
`;
|
|
2759
3513
|
DS_ESLINT_CONFIG = `// Better Design \u2014 design-system guardrail (flat ESLint config fragment).
|
|
2760
3514
|
// Flags raw HTML primitives in feature code when the installed DS ships an
|
|
2761
|
-
// equivalent in components/ui
|
|
3515
|
+
// equivalent in components/ui/*, plus the uppercase micro-label tell.
|
|
3516
|
+
// Wire it into eslint.config.mjs:
|
|
2762
3517
|
//
|
|
2763
3518
|
// import dsGuardrail from "./.better-design/eslint-design-system.mjs"; // better-design-guardrail
|
|
2764
3519
|
// export default [ ...dsGuardrail, /* ...your config */ ];
|
|
@@ -2771,6 +3526,7 @@ const banned = [
|
|
|
2771
3526
|
{ selector: "JSXOpeningElement[name.name='input']", message: "Use <Input> from @/components/ui/input instead of a raw <input>." },
|
|
2772
3527
|
{ selector: "JSXOpeningElement[name.name='select']", message: "Use <Select> from @/components/ui/select instead of a raw <select>." },
|
|
2773
3528
|
{ selector: "JSXOpeningElement[name.name='textarea']", message: "Use <Textarea> from @/components/ui/textarea instead of a raw <textarea>." },
|
|
3529
|
+
{ selector: "JSXAttribute[name.name='className'] Literal[value=/(^|[ ])uppercase([ ]|$)/][value=/(^|[ ])tracking-[^ ]+([ ]|$)/]", message: "Letter-spaced capitals are a generated-UI tell. Use sentence case at the design system's own label size; keep font-mono for code, IDs, and aligned figures." },
|
|
2774
3530
|
];
|
|
2775
3531
|
|
|
2776
3532
|
export default [
|
|
@@ -2922,12 +3678,14 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
2922
3678
|
'- Status colors: the `ds-badge-safe|warn|danger|unknown|verified` classes are colour modifiers, so add them ALONGSIDE the base `ds-badge`, for example `<span class="ds-badge ds-badge-safe">Sesame-free</span>`. They map to the system\'s own status tokens. Reach for these instead of hand-writing a colored badge from a raw token.',
|
|
2923
3679
|
"- Filters, toggles and any tappable pill use `ds-chip` (or `ds-btn`) rendered as a real `<button>`, never a `ds-badge` span. Badges are read-only status labels.",
|
|
2924
3680
|
'- Make the mock WORK, not just render: wire a few lines of vanilla JS so every control has a VISIBLE EFFECT. Filter chips toggle `aria-pressed` (the kit styles `ds-chip[aria-pressed="true"]` as selected) AND actually filter: tag each list item with `data-tags="..."` and hide items that do not match the pressed chips. Search inputs filter the list on `input`. A control that changes nothing reads as broken.',
|
|
2925
|
-
"- Custom classes are for LAYOUT only (position, spacing, grid). Never restyle a `ds-*` component's geometry or surface (border-radius, borders, shadows, fills) \u2014 those carry this system's identity. If a component looks wrong, use a different `ds-*` component, do not reshape it.",
|
|
3681
|
+
"- Custom classes are for LAYOUT only (position, spacing, grid). Never restyle a `ds-*` component's geometry or surface (border-radius, borders, shadows, fills) \u2014 those carry this system's identity. If a component looks wrong, use a different `ds-*` component, do not reshape it. An asset you draw yourself (an inline `<svg>` or an illustration block) is NOT a `ds-*` component: style it freely, as long as every colour comes from these tokens.",
|
|
2926
3682
|
"- Cards in a grid must stay aligned: the kit pins `ds-card-footer` to the card bottom so rows of cards keep equal footer lines even when a badge row wraps \u2014 do not override that. Keep wrap-prone rows (badges, chips) in the same slot on every card so nothing reads as misaligned.",
|
|
2927
3683
|
'- Build any layout you want with these tokens, do not invent new colors. For the full React component set in a real project, call `get-design-system-kit` with `target: "react"` instead.',
|
|
2928
3684
|
"",
|
|
2929
3685
|
"## Keep it tight",
|
|
2930
|
-
"Build the screens the user actually asked for (one, unless they asked for more) and compose the `ds-*` components above.
|
|
3686
|
+
"Build the screens the user actually asked for (one, unless they asked for more) and compose the `ds-*` components above.",
|
|
3687
|
+
BESPOKE_ASSET_POLICY,
|
|
3688
|
+
"Skip a long write-up: ship the artifact with a one-line note, no multi-paragraph explanation or trailing follow-up questions.",
|
|
2931
3689
|
...componentKit?.excluded?.length ? [
|
|
2932
3690
|
"",
|
|
2933
3691
|
"## Not in this kit (use the real components)",
|
|
@@ -2937,7 +3695,7 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
2937
3695
|
Full live showcase: ${viewUrl}` : ""
|
|
2938
3696
|
].join("\n");
|
|
2939
3697
|
}
|
|
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;
|
|
3698
|
+
var MINIMUM_TEXT_CONTRAST, HOVER_POLICY, INLINE_TEXT_COLOR_POLICY, HOST_ISOLATION_CSS_TEMPLATE, BESPOKE_ASSET_POLICY, INLINE_COMPOSITION_POLICY, INLINE_HIERARCHY_POLICY, DS_KIT_CSS, DS_KIT_EXAMPLE_HTML, HTML_KIT_CONDUCT_MD;
|
|
2941
3699
|
var init_html_kit = __esm({
|
|
2942
3700
|
"../shared/src/mcp/html-kit.ts"() {
|
|
2943
3701
|
"use strict";
|
|
@@ -2950,6 +3708,13 @@ var init_html_kit = __esm({
|
|
|
2950
3708
|
].join(" ");
|
|
2951
3709
|
HOST_ISOLATION_CSS_TEMPLATE = `APP_ROOT{color:var(--foreground,oklch(0.2 0 0));background:var(--background,oklch(1 0 0))}
|
|
2952
3710
|
APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-supporting):not(.ds-card-desc):not(.ds-muted){color:inherit!important}`;
|
|
3711
|
+
BESPOKE_ASSET_POLICY = [
|
|
3712
|
+
"Cut GENERIC decoration: faux maps, gradient washes, glassmorphism panels, radial-glow backgrounds. Those attach to no particular product, so they read as filler.",
|
|
3713
|
+
"A bespoke asset is the opposite and it is welcome. ONE illustration, section seam or piece of hero art, drawn from what this product actually does, is what makes a page look authored instead of generated.",
|
|
3714
|
+
"Apply this test before you keep it: could this asset sit unchanged on a competitor's page? If yes, cut it. If no, keep it.",
|
|
3715
|
+
"Draw it with these tokens, and reuse its visual language (its drawing style, palette, grid and line weight) for every later asset so the set reads as one family rather than clip art from different sources.",
|
|
3716
|
+
"An asset you draw yourself, an inline `<svg>` or an illustration block, is NOT a `ds-*` component: style it freely, as long as every colour comes from these tokens."
|
|
3717
|
+
].join(" ");
|
|
2953
3718
|
INLINE_COMPOSITION_POLICY = [
|
|
2954
3719
|
"Start with the primary user moment and make it the largest, highest-contrast composition in the first view; supporting data must read as secondary.",
|
|
2955
3720
|
"Keep one visible identity block per level. Do not stack an app logo and title directly above a second logo and title for the same experience; make a pool, league, or workspace name subordinate context inside the primary composition instead of another header.",
|
|
@@ -2982,6 +3747,11 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
|
|
|
2982
3747
|
.ds .ds-section-title{font-family:inherit;font-size:1.25rem;font-weight:600;line-height:1.2;letter-spacing:-.01em;margin:0;color:var(--foreground,oklch(0.2 0 0))}
|
|
2983
3748
|
.ds .ds-supporting{font-size:.875rem;font-weight:400;line-height:1.5;color:var(--muted-foreground,oklch(0.55 0 0));margin:0}
|
|
2984
3749
|
.ds a{color:var(--primary,oklch(0.45 0.15 264));text-decoration:none}
|
|
3750
|
+
/* A link styled as a button keeps the button's own label colour. The .ds a rule
|
|
3751
|
+
above is specificity (0,1,1) and beat .ds-btn-primary at (0,1,0), so every
|
|
3752
|
+
anchor call to action painted its label in the fill colour. [BD-449] */
|
|
3753
|
+
.ds a.ds-btn{color:inherit}
|
|
3754
|
+
.ds a.ds-btn:hover{text-decoration:none}
|
|
2985
3755
|
.ds p{margin:0 0 1em}
|
|
2986
3756
|
.ds-muted{color:var(--muted-foreground,oklch(0.55 0 0))}
|
|
2987
3757
|
|
|
@@ -3122,6 +3892,9 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
|
|
|
3122
3892
|
"## Hierarchy and information budget (pass/fail)",
|
|
3123
3893
|
INLINE_HIERARCHY_POLICY,
|
|
3124
3894
|
"",
|
|
3895
|
+
"## Draw one asset that only this product could carry",
|
|
3896
|
+
BESPOKE_ASSET_POLICY,
|
|
3897
|
+
"",
|
|
3125
3898
|
"## Compose the screen, do not decorate the canvas",
|
|
3126
3899
|
INLINE_COMPOSITION_POLICY,
|
|
3127
3900
|
"",
|
|
@@ -3144,18 +3917,40 @@ function fence2(lang, body) {
|
|
|
3144
3917
|
${body}
|
|
3145
3918
|
\`\`\``;
|
|
3146
3919
|
}
|
|
3920
|
+
function pointerSafeMaterialCss(css) {
|
|
3921
|
+
const lines = css?.trim().split("\n");
|
|
3922
|
+
if (!lines?.length) return void 0;
|
|
3923
|
+
const hoverRules = lines.filter((line) => line.includes(":hover"));
|
|
3924
|
+
const baseRules = lines.filter((line) => !line.includes(":hover"));
|
|
3925
|
+
if (hoverRules.length) {
|
|
3926
|
+
baseRules.push(`@media (hover:hover) and (pointer:fine){${hoverRules.join("\n")}}`);
|
|
3927
|
+
}
|
|
3928
|
+
return baseRules.join("\n").trim() || void 0;
|
|
3929
|
+
}
|
|
3930
|
+
function primaryButtonTextCss(css) {
|
|
3931
|
+
if (!css) return void 0;
|
|
3932
|
+
let foreground;
|
|
3933
|
+
for (const match of css.matchAll(/\.ds-btn(?:-primary)?\s*\{([^}]*)\}/g)) {
|
|
3934
|
+
for (const color of match[1]?.matchAll(/(?:^|;)\s*color\s*:\s*([^;}]+)/g) ?? []) {
|
|
3935
|
+
foreground = color[1]?.trim() ?? foreground;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
return foreground ? `.ds.ds-embed .ds-btn-primary{color:${foreground}!important;-webkit-text-fill-color:currentColor}` : void 0;
|
|
3939
|
+
}
|
|
3147
3940
|
function buildDsShopifyFiles(input) {
|
|
3148
|
-
const materialCss = input.componentKit?.css
|
|
3941
|
+
const materialCss = pointerSafeMaterialCss(input.componentKit?.css);
|
|
3942
|
+
const primaryTextCss = primaryButtonTextCss(materialCss);
|
|
3149
3943
|
const assetCss = [
|
|
3150
3944
|
compactTokensCss(input.tokensCss),
|
|
3151
3945
|
DS_KIT_CSS,
|
|
3152
3946
|
DS_SHOPIFY_CSS,
|
|
3153
|
-
...materialCss ? [materialCss] : []
|
|
3947
|
+
...materialCss ? [materialCss] : [],
|
|
3948
|
+
...primaryTextCss ? [primaryTextCss] : []
|
|
3154
3949
|
].join("\n\n");
|
|
3155
3950
|
return [{ path: "assets/ds-kit.css", content: assetCss }, ...SNIPPETS];
|
|
3156
3951
|
}
|
|
3157
|
-
function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
3158
|
-
const files = buildDsShopifyFiles({ tokensCss, componentKit });
|
|
3952
|
+
function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit, themeFiles }) {
|
|
3953
|
+
const files = themeFiles ?? buildDsShopifyFiles({ tokensCss, componentKit });
|
|
3159
3954
|
const sourceLine = source?.kind === "catalog" ? `Source designSystemId: \`${source.designSystemId}\`` : source?.kind === "project" ? `Source projectId: \`${source.projectId}\`` : null;
|
|
3160
3955
|
const fileBlocks = files.flatMap((f) => [
|
|
3161
3956
|
`### \`${f.path}\``,
|
|
@@ -3166,7 +3961,7 @@ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
3166
3961
|
return [
|
|
3167
3962
|
`# ${name}: Shopify theme kit`,
|
|
3168
3963
|
"",
|
|
3169
|
-
"Full design system for an
|
|
3964
|
+
"Full design system for an Online Store 2.0 theme: one CSS asset carries this system's real tokens and component material, while the Liquid snippets, global header/footer groups, and primary storefront templates compose the full store with no JavaScript. Write each file to the path in its heading.",
|
|
3170
3965
|
...sourceLine ? ["", sourceLine] : [],
|
|
3171
3966
|
"",
|
|
3172
3967
|
SHOPIFY_KIT_CONDUCT_MD,
|
|
@@ -3176,57 +3971,150 @@ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
3176
3971
|
...fileBlocks,
|
|
3177
3972
|
"## After installing",
|
|
3178
3973
|
"- Preview an unpublished copy of the theme first; never edit the live theme directly.",
|
|
3179
|
-
"- The kit
|
|
3974
|
+
"- The kit owns the storefront composition and cart page; Shopify continues to own checkout.",
|
|
3180
3975
|
...viewUrl ? ["", `Full live showcase: ${viewUrl}`] : []
|
|
3181
3976
|
].join("\n");
|
|
3182
3977
|
}
|
|
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;
|
|
3978
|
+
var DS_SHOPIFY_CSS, SNIPPET_BADGE, SNIPPET_PRICE, SNIPPET_BUY_BUTTON, SNIPPET_VARIANT_SELECT, SNIPPET_PAGINATION, SNIPPET_ICON, 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, SECTION_MAIN_PAGE, SECTION_MAIN_CART, SECTION_MAIN_SEARCH, SECTION_MAIN_404, TEMPLATE_INDEX_JSON, TEMPLATE_PRODUCT_JSON, TEMPLATE_PAGE_JSON, TEMPLATE_CART_JSON, TEMPLATE_SEARCH_JSON, TEMPLATE_404_JSON, HEADER_GROUP_JSON, FOOTER_GROUP_JSON, TEMPLATE_COLLECTION_JSON, SNIPPETS, SHOPIFY_KIT_CONDUCT_MD;
|
|
3184
3979
|
var init_shopify_kit = __esm({
|
|
3185
3980
|
"../shared/src/mcp/shopify-kit.ts"() {
|
|
3186
3981
|
"use strict";
|
|
3187
3982
|
init_html_kit();
|
|
3188
|
-
DS_SHOPIFY_CSS =
|
|
3189
|
-
|
|
3983
|
+
DS_SHOPIFY_CSS = `html:root{font-size:100%;background:var(--background,oklch(1 0 0));color:var(--foreground,oklch(0.2 0 0))}
|
|
3984
|
+
body{margin:0;background:var(--background,oklch(1 0 0));color:var(--foreground,oklch(0.2 0 0));font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:1rem;line-height:1.5;letter-spacing:normal;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased}
|
|
3985
|
+
body.gradient,body.color-background-1,main#MainContent,.content-for-layout{background:var(--background,oklch(1 0 0));color:var(--foreground,oklch(0.2 0 0))}
|
|
3986
|
+
body :where(button,input,select,textarea){font:inherit}
|
|
3987
|
+
body :where(.gradient,.color-background-1,.color-background-2){background:var(--background,oklch(1 0 0));color:var(--foreground,oklch(0.2 0 0))}
|
|
3988
|
+
body :where(.button,.shopify-payment-button__button){border-radius:var(--radius-md,var(--radius,0.5rem));background:var(--primary,oklch(0.25 0.01 260));color:var(--primary-foreground,oklch(0.98 0 0));box-shadow:var(--shadow-sm,none)}
|
|
3989
|
+
body :where(.field__input,.select__select,.customer .field input,.customer select){border-radius:var(--radius-md,var(--radius,0.5rem));background:var(--input,var(--background,oklch(1 0 0)));color:var(--foreground,oklch(0.2 0 0))}
|
|
3990
|
+
body :where(.card,.card__inner,.product__media,.cart-item__image-container){border-radius:var(--radius-lg,var(--radius,0.75rem))}
|
|
3991
|
+
.ds.ds-embed{background:transparent;font-family:var(--font-sans,ui-sans-serif,system-ui,sans-serif);font-size:1rem;letter-spacing:normal}
|
|
3992
|
+
.ds-container{width:min(calc(100% - 2.5rem),76rem);max-width:76rem;margin-inline:auto;padding:0}
|
|
3993
|
+
.ds-section{padding-block:clamp(3rem,7vw,6rem)}
|
|
3994
|
+
.ds-section-compact{padding-block:clamp(2rem,4vw,3.5rem)}
|
|
3995
|
+
.ds-section-head{display:flex;align-items:end;justify-content:space-between;gap:1.5rem;margin-bottom:clamp(1.5rem,3vw,2.5rem)}
|
|
3996
|
+
.ds-section-head .ds-section-title{font-size:clamp(1.65rem,3vw,2.5rem)}
|
|
3997
|
+
.ds-section-copy{max-width:42rem;color:var(--muted-foreground,oklch(0.45 0.01 260))}
|
|
3998
|
+
.ds-display,.ds-section-title,.ds-card-title{text-wrap:balance}
|
|
3999
|
+
.ds-section-copy,.ds-supporting,.ds-card-desc{text-wrap:pretty}
|
|
4000
|
+
.ds.ds-announce{background:var(--primary,oklch(0.25 0.01 260));color:var(--primary-foreground,oklch(0.98 0 0));text-align:center;padding:.55rem 1rem;font-size:.8125rem}
|
|
3190
4001
|
.ds-announce p{margin:0}
|
|
3191
4002
|
.ds-announce a{color:inherit;text-decoration:underline}
|
|
3192
|
-
.ds-price{display:inline-flex;align-items:baseline;gap:.
|
|
3193
|
-
.ds-price-current{font-weight:
|
|
3194
|
-
.ds-price-compare{color:var(--muted-foreground,oklch(0.
|
|
3195
|
-
.ds-
|
|
3196
|
-
.ds-
|
|
4003
|
+
.ds-price{display:inline-flex;align-items:baseline;gap:.6rem;flex-wrap:wrap}
|
|
4004
|
+
.ds-price-current{font-size:1.35rem;font-weight:700;letter-spacing:-.02em;color:var(--foreground,oklch(0.2 0 0))}
|
|
4005
|
+
.ds-price-compare{color:var(--muted-foreground,oklch(0.45 0.01 260))}
|
|
4006
|
+
.ds.ds-embed .ds-btn{min-height:2.75rem;white-space:nowrap;text-decoration:none}
|
|
4007
|
+
.ds.ds-embed .ds-btn-primary{color:var(--primary-foreground,oklch(0.98 0 0))!important;-webkit-text-fill-color:currentColor}
|
|
4008
|
+
.ds-card-media{display:block;overflow:hidden;background:var(--muted,oklch(0.95 0.01 90));aspect-ratio:4/5}
|
|
4009
|
+
.ds-card-media img,.ds-card-media svg{display:block;width:100%;height:100%;object-fit:cover}
|
|
4010
|
+
.ds-product-card{height:100%}
|
|
4011
|
+
.ds-product-card .ds-card-body{padding:1.1rem 1.25rem .65rem}
|
|
4012
|
+
.ds-product-card .ds-card-footer{padding:.75rem 1.25rem 1.25rem;border:0}
|
|
3197
4013
|
.ds-product-card .ds-card-title a{color:inherit;text-decoration:none}
|
|
3198
|
-
.ds-
|
|
3199
|
-
.ds-faq
|
|
3200
|
-
.ds-faq-
|
|
3201
|
-
.ds-
|
|
3202
|
-
|
|
3203
|
-
.ds-
|
|
4014
|
+
.ds-product-card .ds-supporting{margin-bottom:.25rem}
|
|
4015
|
+
.ds-faq{max-width:52rem}
|
|
4016
|
+
.ds-faq-item.ds-card{display:block;padding:.25rem 1.25rem;margin:.75rem 0}
|
|
4017
|
+
.ds-faq-item summary{cursor:pointer;padding:1.15rem 0;font-weight:600;color:var(--foreground,oklch(0.2 0 0))}
|
|
4018
|
+
.ds-faq-answer{padding:0 0 1.25rem;color:var(--muted-foreground,oklch(0.45 0.01 260))}
|
|
4019
|
+
.ds-grid{display:grid;grid-template-columns:repeat(var(--ds-cols,4),minmax(0,1fr));gap:clamp(1rem,2vw,1.75rem)}
|
|
4020
|
+
.ds-grid-empty{grid-column:1/-1}
|
|
4021
|
+
.ds-newsletter .ds-control-row{margin-top:1rem}
|
|
3204
4022
|
.ds-buy-form{margin:0;display:grid;gap:.75rem;justify-items:start}
|
|
3205
|
-
.ds-
|
|
4023
|
+
.ds-buy-form .ds-btn{min-width:min(100%,16rem)}
|
|
4024
|
+
.ds-featured{display:block}
|
|
3206
4025
|
.ds-qty{width:6rem}
|
|
3207
|
-
.ds-header{
|
|
3208
|
-
.ds-header-
|
|
3209
|
-
.ds-header-
|
|
3210
|
-
.ds-header-
|
|
3211
|
-
.ds-header-
|
|
3212
|
-
.ds-
|
|
3213
|
-
.ds-
|
|
3214
|
-
.ds-footer a{
|
|
3215
|
-
.ds-
|
|
3216
|
-
.ds-
|
|
3217
|
-
.ds-
|
|
3218
|
-
.ds-
|
|
3219
|
-
.ds-
|
|
3220
|
-
.ds-
|
|
3221
|
-
.ds-
|
|
3222
|
-
.ds-
|
|
3223
|
-
.ds-
|
|
3224
|
-
.ds-
|
|
3225
|
-
.ds-
|
|
3226
|
-
.ds-
|
|
4026
|
+
.ds.ds-header-shell{position:relative;z-index:20;background:color-mix(in oklab,var(--background,oklch(1 0 0)) 90%,transparent);padding:1rem 0}
|
|
4027
|
+
.ds-header-shell.is-sticky{position:sticky;top:0;backdrop-filter:blur(1rem)}
|
|
4028
|
+
.ds-header.ds-card{min-height:4.75rem;display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:2rem;padding:0 1.25rem;overflow:visible}
|
|
4029
|
+
.ds-header-brand{font-weight:700;font-size:1.1rem;letter-spacing:-.02em;color:var(--foreground,oklch(0.2 0 0));text-decoration:none;white-space:nowrap}
|
|
4030
|
+
.ds-header-logo{display:block;max-height:2.25rem;width:auto}
|
|
4031
|
+
.ds-header-nav{display:flex;justify-content:center;gap:clamp(1rem,2.5vw,2rem);min-width:0}
|
|
4032
|
+
.ds-header-link,.ds-header-nav a{color:var(--foreground,oklch(0.2 0 0));text-decoration:none;font-size:.9rem;font-weight:500}
|
|
4033
|
+
.ds-header-link:focus-visible,.ds-header-nav a:focus-visible,.ds-footer a:focus-visible{outline:2px solid var(--ring,var(--primary,oklch(0.45 0.15 264)));outline-offset:.3rem;border-radius:var(--radius-sm,.25rem)}
|
|
4034
|
+
.ds-header-actions{display:flex;align-items:center;justify-content:flex-end;gap:1rem}
|
|
4035
|
+
.ds-cart-link{display:inline-flex;align-items:center;gap:.35rem}
|
|
4036
|
+
.ds-cart-count{display:inline-grid;place-items:center;min-width:1.35rem;height:1.35rem;padding:0 .3rem;border-radius:999px;background:var(--primary,oklch(0.25 0.01 260));color:var(--primary-foreground,oklch(0.98 0 0));font-size:.7rem;font-weight:700}
|
|
4037
|
+
.ds-mobile-nav{display:none}
|
|
4038
|
+
.ds-mobile-nav summary{list-style:none;cursor:pointer}
|
|
4039
|
+
.ds-mobile-nav summary::-webkit-details-marker{display:none}
|
|
4040
|
+
.ds-mobile-nav-panel{position:absolute;inset-inline:0;top:calc(100% + .5rem);padding:1.25rem;display:grid;gap:1rem;background:var(--card,var(--background,oklch(1 0 0)));border:1px solid var(--border,oklch(0.88 0.01 260));border-radius:var(--radius-xl,var(--radius,1rem));box-shadow:var(--shadow-lg,var(--shadow-md,none))}
|
|
4041
|
+
.ds.ds-footer{background:var(--background,oklch(1 0 0));padding-block:1.25rem 2rem}
|
|
4042
|
+
.ds-footer-panel.ds-card{display:block;padding:clamp(2rem,5vw,4rem)}
|
|
4043
|
+
.ds-footer-grid{display:grid;grid-template-columns:minmax(14rem,1.4fr) repeat(2,minmax(10rem,1fr));gap:clamp(2rem,5vw,5rem)}
|
|
4044
|
+
.ds-footer-brand{max-width:28rem}
|
|
4045
|
+
.ds-footer-title{font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin:0 0 1rem}
|
|
4046
|
+
.ds-footer nav{display:grid;gap:.7rem}
|
|
4047
|
+
.ds-footer a{color:var(--muted-foreground,oklch(0.45 0.01 260));text-decoration:none;font-size:.9rem}
|
|
4048
|
+
.ds-footer-bottom{display:flex;align-items:center;justify-content:space-between;gap:1.5rem;margin-top:3rem;padding-top:1.5rem;border-top:1px solid var(--border,oklch(0.88 0.01 260))}
|
|
4049
|
+
.ds-policy-list,.ds-payment-list{list-style:none;padding:0;margin:0;display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
|
|
4050
|
+
.ds-payment-icon{display:block;width:2.25rem;height:auto}
|
|
4051
|
+
.ds.ds-hero-section{background:var(--background,oklch(1 0 0));overflow:hidden;padding-block:1.25rem clamp(3rem,7vw,6rem)}
|
|
4052
|
+
.ds-hero{display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);align-items:center;gap:clamp(2.5rem,7vw,7rem);padding-block:clamp(2rem,5vw,4.5rem)}
|
|
4053
|
+
.ds-hero.ds-hero-single{grid-template-columns:minmax(0,42rem);justify-content:start}
|
|
4054
|
+
.ds-hero-content{display:grid;gap:1.25rem;align-content:center;max-width:38rem}
|
|
4055
|
+
.ds-hero .ds-display{font-size:clamp(2.6rem,6vw,5.5rem);line-height:.98}
|
|
4056
|
+
.ds-hero .ds-supporting{font-size:clamp(1rem,1.5vw,1.2rem);max-width:35rem}
|
|
4057
|
+
.ds-hero-media{position:relative;order:2;min-width:0}
|
|
4058
|
+
.ds-hero-media-has-image:before,.ds-hero-media-has-image:after{content:"";position:absolute;z-index:-1;border-radius:var(--radius-3xl,var(--radius-xl,var(--radius,1rem)));pointer-events:none}
|
|
4059
|
+
.ds-hero-media-has-image:before{width:32%;height:28%;inset:-1rem auto auto -1rem;background:var(--pop-blue,transparent)}
|
|
4060
|
+
.ds-hero-media-has-image:after{width:28%;height:24%;inset:auto -1rem -1rem auto;background:var(--pop-orange,transparent)}
|
|
4061
|
+
.ds-hero-media img{display:block;width:100%;height:clamp(24rem,50vw,42rem);object-fit:cover;border-radius:var(--radius-3xl,var(--radius-xl,var(--radius,1rem)));background:var(--muted,oklch(0.95 0.01 90))}
|
|
4062
|
+
.ds-hero-media>.ds-media-empty{min-height:clamp(24rem,50vw,42rem)}
|
|
4063
|
+
.ds-hero-actions{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap}
|
|
4064
|
+
.ds-hero-trust{font-size:.8rem;color:var(--muted-foreground,oklch(0.45 0.01 260));margin:0}
|
|
4065
|
+
.ds-image-text{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:clamp(2rem,6vw,6rem);align-items:center}
|
|
4066
|
+
.ds-image-text.ds-image-text-single{grid-template-columns:minmax(0,42rem)}
|
|
4067
|
+
.ds-image-text-media.ds-card{display:block}
|
|
4068
|
+
.ds-image-text-media img{display:block;width:100%;height:clamp(20rem,45vw,38rem);object-fit:cover;background:var(--muted,oklch(0.95 0.01 90))}
|
|
4069
|
+
.ds-rich-text{max-width:52rem;text-align:center}
|
|
4070
|
+
.ds-rich-text .ds-section-copy{margin-inline:auto}
|
|
4071
|
+
.ds-rich-text .ds-btn{margin-top:1.25rem}
|
|
4072
|
+
.ds-value-panel.ds-card{display:grid;grid-template-columns:repeat(var(--ds-cols,3),minmax(0,1fr));overflow:hidden}
|
|
4073
|
+
.ds-value-item{padding:clamp(1.35rem,3vw,2rem)}
|
|
4074
|
+
.ds-value-item+.ds-value-item{border-left:1px solid var(--border,oklch(0.88 0.01 260))}
|
|
4075
|
+
.ds-value-title-row{display:flex;align-items:center;gap:.75rem}
|
|
4076
|
+
.ds-icon{display:block;width:1.25rem;height:1.25rem;flex:none}
|
|
4077
|
+
.ds-icon-tile{display:grid;place-items:center;flex:0 0 auto;width:2.5rem;height:2.5rem;border-radius:var(--radius-lg,var(--radius,1rem));background:var(--secondary,oklch(0.93 0.01 90));color:var(--secondary-foreground,var(--foreground,oklch(.31 .006 75)))}
|
|
4078
|
+
.ds-icon-tile-blue{background:var(--pop-blue,var(--secondary,oklch(0.93 0.01 90)));color:var(--pop-blue-foreground,var(--foreground,oklch(.31 .006 75)))}
|
|
4079
|
+
.ds-icon-tile-orange{background:var(--pop-orange,var(--secondary,oklch(0.93 0.01 90)));color:var(--pop-orange-foreground,var(--foreground,oklch(.31 .006 75)))}
|
|
4080
|
+
.ds-icon-tile-green{background:var(--pop-green,var(--secondary,oklch(0.93 0.01 90)));color:var(--pop-green-foreground,var(--foreground,oklch(.31 .006 75)))}
|
|
4081
|
+
.ds-product-layout{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(20rem,.9fr);gap:clamp(2rem,6vw,6rem);padding-block:clamp(3rem,7vw,6rem)}
|
|
4082
|
+
.ds-product-media-main.ds-card{display:block}
|
|
4083
|
+
.ds-product-media-main img{display:block;width:100%;height:auto;min-height:30rem;max-height:46rem;object-fit:cover;background:var(--muted,oklch(0.95 0.01 90))}
|
|
4084
|
+
.ds-product-media-main>.ds-media-empty{min-height:30rem}
|
|
4085
|
+
.ds-product-thumbs{display:grid;grid-template-columns:repeat(4,1fr);gap:.75rem;margin-top:.75rem}
|
|
4086
|
+
.ds-product-thumbs img{display:block;width:100%;aspect-ratio:1;object-fit:cover;border-radius:var(--radius-md,var(--radius,.5rem))}
|
|
4087
|
+
.ds-product-info{display:grid;gap:1.25rem;align-content:start}
|
|
4088
|
+
.ds-product-info.ds-card{padding:clamp(1.5rem,4vw,3rem)}
|
|
4089
|
+
.ds-product-info .ds-display{font-size:clamp(2rem,4vw,3.75rem)}
|
|
4090
|
+
.ds-product-description{color:var(--muted-foreground,oklch(0.42 0.01 260))}
|
|
4091
|
+
.ds-page-content.ds-card{display:block;padding:clamp(1.5rem,4vw,3rem)}
|
|
4092
|
+
.ds-contact-form.ds-card{display:grid;gap:1rem;max-width:42rem;margin-top:2rem;padding:clamp(1.5rem,4vw,3rem)}
|
|
4093
|
+
.ds-product-page,.ds-collection-page,.ds-page,.ds-cart-page,.ds-search-page{padding-block:clamp(3rem,7vw,6rem)}
|
|
4094
|
+
.ds-collection-head,.ds-page-head{display:grid;gap:1rem;margin-bottom:clamp(2rem,5vw,4rem);max-width:52rem}
|
|
4095
|
+
.ds-collection-head .ds-display,.ds-page-head .ds-display{font-size:clamp(2.4rem,5vw,4.75rem)}
|
|
4096
|
+
.ds-cart-list{display:grid;gap:1rem;margin:2rem 0;padding:0;list-style:none}
|
|
4097
|
+
.ds-cart-item.ds-card{display:grid;grid-template-columns:7rem minmax(0,1fr) auto;gap:1.25rem;align-items:center;padding:1rem}
|
|
4098
|
+
.ds-cart-item img{display:block;width:7rem;aspect-ratio:1;object-fit:cover;border-radius:var(--radius-md,var(--radius,.5rem))}
|
|
4099
|
+
.ds-cart-item .ds-media-empty{min-height:7rem;padding:.75rem}
|
|
4100
|
+
.ds-cart-summary.ds-card{display:grid;justify-items:end;gap:1rem;margin-left:auto;max-width:28rem;padding:1.5rem;text-align:right}
|
|
4101
|
+
.ds-search-form.ds-card{display:grid;grid-template-columns:minmax(0,32rem) auto;gap:.75rem;align-items:end;width:max-content;max-width:100%;margin:2rem 0 3rem;padding:1.25rem}
|
|
4102
|
+
.ds-empty{display:grid;justify-items:start;gap:1rem;padding-block:2rem}
|
|
4103
|
+
.ds-empty.ds-card{padding:1.5rem}
|
|
4104
|
+
.ds-media-empty{display:grid;place-items:center;align-content:center;gap:.75rem;width:100%;min-height:18rem;padding:2rem;text-align:center;background:var(--secondary,var(--muted,oklch(0.94 0.01 90)));color:var(--muted-foreground,oklch(.55 .009 80))}
|
|
4105
|
+
.ds-media-empty .ds-icon{width:1.5rem;height:1.5rem}
|
|
4106
|
+
.ds-media-empty-title{margin:0;color:var(--foreground,oklch(.31 .006 75));font-weight:600}
|
|
4107
|
+
.ds-media-empty-copy{max-width:22rem;margin:0;font-size:.875rem}
|
|
4108
|
+
.ds-pagination{display:block;text-align:center;margin:2.5rem 0 0}
|
|
4109
|
+
.ds-pagination .page,.ds-pagination .next,.ds-pagination .prev{display:inline-block;padding:.45rem .75rem}
|
|
3227
4110
|
.ds-pagination a{color:var(--foreground,oklch(0.2 0 0))}
|
|
3228
4111
|
.ds-pagination .current{font-weight:700;color:var(--primary,oklch(0.25 0.01 260))}
|
|
3229
|
-
@media (
|
|
4112
|
+
@media (max-width:989px){.ds-header.ds-card{grid-template-columns:auto 1fr auto;gap:1rem}.ds-mobile-nav{display:block}.ds-header-brand{justify-self:center}.ds-header-nav{display:none}.ds-header-actions .ds-header-link:not(.ds-cart-link){display:none}.ds-hero{grid-template-columns:1fr}.ds-hero-media{order:2}.ds-hero-media img{height:clamp(20rem,80vw,34rem)}.ds-value-panel.ds-card{grid-template-columns:1fr}.ds-value-item+.ds-value-item{border-left:0;border-top:1px solid var(--border,oklch(0.88 0.01 260))}.ds-footer-grid{grid-template-columns:1fr 1fr}.ds-footer-brand{grid-column:1/-1}.ds-product-layout{grid-template-columns:1fr}}
|
|
4113
|
+
@media (max-width:749px){.ds-section-head{display:grid}.ds-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.ds-header.ds-card{min-height:4rem}.ds-header-shell.is-sticky{position:relative}.ds-hero .ds-display{font-size:clamp(2.4rem,13vw,4rem)}.ds-image-text{grid-template-columns:1fr}.ds-footer-panel.ds-card{padding:2rem 1.25rem}.ds-footer-grid{grid-template-columns:1fr}.ds-footer-brand{grid-column:auto}.ds-footer-bottom{align-items:flex-start;flex-direction:column}.ds-cart-item.ds-card{grid-template-columns:5rem minmax(0,1fr)}.ds-cart-item img{width:5rem}.ds-cart-item .ds-media-empty{min-height:5rem}.ds-cart-item-total{grid-column:2}.ds-search-form.ds-card{grid-template-columns:1fr;width:100%}.ds-search-form .ds-btn{width:100%}}
|
|
4114
|
+
@media (max-width:479px){.ds-grid{grid-template-columns:1fr}.ds-header-actions{gap:.6rem}.ds-cart-link>span:first-child{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}}
|
|
4115
|
+
@media (pointer:coarse){.ds-mobile-nav summary,.ds-header-actions a{display:inline-flex;align-items:center;min-width:44px;min-height:44px}}
|
|
4116
|
+
@media (hover:hover) and (pointer:fine){.ds-header-link:hover,.ds-header-nav a:hover,.ds-footer a:hover{text-decoration:underline;text-underline-offset:.25rem}.ds-card-media img{transition:transform .3s ease}.ds-card-media:hover img{transform:scale(1.025)}}
|
|
4117
|
+
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}.ds-card-media img{transition:none}}`;
|
|
3230
4118
|
SNIPPET_BADGE = `{% comment %}
|
|
3231
4119
|
Status label. Usage: {% render 'ds-badge', label: 'New' %}
|
|
3232
4120
|
Optional tone: primary | outline | safe | warn | danger | unknown | verified
|
|
@@ -3251,7 +4139,7 @@ var init_shopify_kit = __esm({
|
|
|
3251
4139
|
<span class="ds-price-current">{{ ds_price | money }}</span>
|
|
3252
4140
|
{%- if ds_compare_at > ds_price -%}
|
|
3253
4141
|
<s class="ds-price-compare">{{ ds_compare_at | money }}</s>
|
|
3254
|
-
{% render 'ds-badge', label: 'Sale', tone: '
|
|
4142
|
+
{% render 'ds-badge', label: 'Sale', tone: 'warn' %}
|
|
3255
4143
|
{%- endif -%}
|
|
3256
4144
|
</span>`;
|
|
3257
4145
|
SNIPPET_BUY_BUTTON = `{% comment %}
|
|
@@ -3302,6 +4190,21 @@ var init_shopify_kit = __esm({
|
|
|
3302
4190
|
{{ paginate | default_pagination }}
|
|
3303
4191
|
</nav>
|
|
3304
4192
|
{%- endif -%}`;
|
|
4193
|
+
SNIPPET_ICON = `{% comment %}
|
|
4194
|
+
Iconoir storefront icons (MIT License), installed through Better Design.
|
|
4195
|
+
Usage: {% render 'ds-icon', icon: 'delivery' %}
|
|
4196
|
+
Available: quality | delivery | support | image.
|
|
4197
|
+
{% endcomment %}
|
|
4198
|
+
{%- case icon -%}
|
|
4199
|
+
{%- when 'quality' -%}
|
|
4200
|
+
<svg class="ds-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" aria-hidden="true" focusable="false"><path d="m8.5 11.5 3 3 5-5"/><path d="M5 18 3.13 4.913a.996.996 0 0 1 .774-1.114l7.662-1.703a2 2 0 0 1 .868 0L20.096 3.8c.51.113.848.596.774 1.114L19 18c-.07.495-.5 3.5-7 3.5S5.07 18.495 5 18"/></svg>
|
|
4201
|
+
{%- when 'delivery' -%}
|
|
4202
|
+
<svg class="ds-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5" aria-hidden="true" focusable="false"><path stroke-linejoin="round" stroke-miterlimit="1.5" d="M8 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4m10 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4"/><path d="M10.05 17H15V6.6a.6.6 0 0 0-.6-.6H1m4.65 11H3.6a.6.6 0 0 1-.6-.6v-4.9"/><path stroke-linejoin="round" d="M2 9h4"/><path d="M15 9h5.61a.6.6 0 0 1 .548.356l1.79 4.028a.6.6 0 0 1 .052.243V16.4a.6.6 0 0 1-.6.6h-1.9M15 17h1"/></svg>
|
|
4203
|
+
{%- when 'support' -%}
|
|
4204
|
+
<svg class="ds-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" aria-hidden="true" focusable="false"><path d="M8 10h8m-8 4h4m0 8c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12c0 1.821.487 3.53 1.338 5L2.5 21.5l4.5-.838A9.96 9.96 0 0 0 12 22"/></svg>
|
|
4205
|
+
{%- else -%}
|
|
4206
|
+
<svg class="ds-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" aria-hidden="true" focusable="false"><path d="M21 3.6v16.8a.6.6 0 0 1-.6.6H3.6a.6.6 0 0 1-.6-.6V3.6a.6.6 0 0 1 .6-.6h16.8a.6.6 0 0 1 .6.6"/><path d="m3 16 7-3 11 5m-5-8a2 2 0 1 1 0-4 2 2 0 0 1 0 4"/></svg>
|
|
4207
|
+
{%- endcase -%}`;
|
|
3305
4208
|
SNIPPET_PRODUCT_CARD = `{% comment %}
|
|
3306
4209
|
Product tile for grids. Usage: {% render 'ds-product-card', product: product %}
|
|
3307
4210
|
Optional: show_vendor: true, enable_quick_add: true
|
|
@@ -3311,11 +4214,14 @@ var init_shopify_kit = __esm({
|
|
|
3311
4214
|
{%- if product.featured_image -%}
|
|
3312
4215
|
{%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
|
|
3313
4216
|
{{ product.featured_image | image_url: width: 900 | image_tag: widths: '360, 540, 720, 900', sizes: '(min-width: 990px) 25vw, 50vw', alt: ds_image_alt }}
|
|
4217
|
+
{%- else -%}
|
|
4218
|
+
<span class="ds-media-empty" role="img" aria-label="Image unavailable">{% render 'ds-icon', icon: 'image' %}</span>
|
|
3314
4219
|
{%- endif -%}
|
|
3315
4220
|
</a>
|
|
3316
4221
|
<div class="ds-card-body">
|
|
3317
|
-
{%- if show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
3318
4222
|
<h3 class="ds-card-title"><a href="{{ product.url }}">{{ product.title }}</a></h3>
|
|
4223
|
+
{%- comment -%}Vendor sits under the title: a label above a heading is banned house style.{%- endcomment -%}
|
|
4224
|
+
{%- if show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
3319
4225
|
{% render 'ds-price', product: product %}
|
|
3320
4226
|
</div>
|
|
3321
4227
|
{%- if enable_quick_add -%}
|
|
@@ -3374,17 +4280,17 @@ var init_shopify_kit = __esm({
|
|
|
3374
4280
|
FAQ accordion using the browser's native details element. Merchants add it
|
|
3375
4281
|
from the theme editor as "DS FAQ".
|
|
3376
4282
|
{% endcomment %}
|
|
3377
|
-
<
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
{%- endif -%}
|
|
4283
|
+
<section class="ds ds-embed ds-section">
|
|
4284
|
+
<div class="ds-container ds-faq">
|
|
4285
|
+
{%- if section.settings.heading != blank -%}<div class="ds-section-head"><h2 class="ds-section-title">{{ section.settings.heading }}</h2></div>{%- endif -%}
|
|
3381
4286
|
{%- for block in section.blocks -%}
|
|
3382
|
-
<details class="ds-faq-item" {{ block.shopify_attributes }}>
|
|
4287
|
+
<details class="ds-card ds-faq-item" {{ block.shopify_attributes }}>
|
|
3383
4288
|
<summary>{{ block.settings.question }}</summary>
|
|
3384
4289
|
<div class="ds-faq-answer">{{ block.settings.answer }}</div>
|
|
3385
4290
|
</details>
|
|
3386
4291
|
{%- endfor -%}
|
|
3387
4292
|
</div>
|
|
4293
|
+
</section>
|
|
3388
4294
|
{% schema %}
|
|
3389
4295
|
{
|
|
3390
4296
|
"name": "DS FAQ",
|
|
@@ -3409,19 +4315,28 @@ var init_shopify_kit = __esm({
|
|
|
3409
4315
|
Product grid from a merchant-chosen collection. Merchants add it from the
|
|
3410
4316
|
theme editor as "DS Featured collection".
|
|
3411
4317
|
{% endcomment %}
|
|
3412
|
-
{%- assign ds_collection = section.settings.collection -%}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
4318
|
+
{%- assign ds_collection = section.settings.collection | default: collections.all -%}
|
|
4319
|
+
{%- if ds_collection.products_count > 0 or request.design_mode -%}
|
|
4320
|
+
<section class="ds ds-embed ds-section">
|
|
4321
|
+
<div class="ds-container ds-featured">
|
|
4322
|
+
<div class="ds-section-head">
|
|
4323
|
+
{%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
|
|
4324
|
+
{%- if section.settings.show_view_all -%}<a class="ds-header-link" href="{{ ds_collection.url | default: routes.all_products_collection_url }}">View all products</a>{%- endif -%}
|
|
4325
|
+
</div>
|
|
3417
4326
|
<div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
|
|
3418
4327
|
{%- for product in ds_collection.products limit: section.settings.products_to_show -%}
|
|
3419
4328
|
{% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
|
|
3420
4329
|
{%- else -%}
|
|
3421
|
-
<
|
|
4330
|
+
<div class="ds-card ds-empty ds-grid-empty">
|
|
4331
|
+
<span class="ds-icon-tile ds-icon-tile-blue">{% render 'ds-icon', icon: 'image' %}</span>
|
|
4332
|
+
<h3 class="ds-card-title">Add products to this collection</h3>
|
|
4333
|
+
<p class="ds-card-desc">Real product images, titles, and prices will appear here.</p>
|
|
4334
|
+
</div>
|
|
3422
4335
|
{%- endfor -%}
|
|
3423
4336
|
</div>
|
|
3424
4337
|
</div>
|
|
4338
|
+
</section>
|
|
4339
|
+
{%- endif -%}
|
|
3425
4340
|
{% schema %}
|
|
3426
4341
|
{
|
|
3427
4342
|
"name": "DS Featured collection",
|
|
@@ -3431,90 +4346,189 @@ var init_shopify_kit = __esm({
|
|
|
3431
4346
|
{ "type": "range", "id": "products_to_show", "min": 2, "max": 12, "step": 1, "default": 4, "label": "Products to show" },
|
|
3432
4347
|
{ "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
|
|
3433
4348
|
{ "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
|
|
3434
|
-
{ "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false }
|
|
4349
|
+
{ "type": "checkbox", "id": "enable_quick_add", "label": "Enable quick add", "default": false },
|
|
4350
|
+
{ "type": "checkbox", "id": "show_view_all", "label": "Show view all link", "default": true }
|
|
3435
4351
|
],
|
|
3436
4352
|
"presets": [{ "name": "DS Featured collection" }]
|
|
3437
4353
|
}
|
|
3438
4354
|
{% endschema %}`;
|
|
3439
4355
|
SECTION_HEADER = `{% comment %}
|
|
3440
|
-
|
|
4356
|
+
Complete storefront header, including announcement, responsive navigation,
|
|
4357
|
+
search, account, and cart. The generated header group installs it globally.
|
|
3441
4358
|
{% endcomment %}
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
</
|
|
4359
|
+
{%- if section.settings.announcement_text != blank -%}
|
|
4360
|
+
{% render 'ds-announcement', text: section.settings.announcement_text, link: section.settings.announcement_link, link_label: section.settings.announcement_link_label %}
|
|
4361
|
+
{%- endif -%}
|
|
4362
|
+
<div class="ds ds-embed ds-header-shell{% if section.settings.sticky %} is-sticky{% endif %}">
|
|
4363
|
+
<header class="ds-container ds-card ds-header">
|
|
4364
|
+
<details class="ds-mobile-nav">
|
|
4365
|
+
<summary class="ds-header-link" aria-label="Open navigation menu">Menu</summary>
|
|
4366
|
+
<nav class="ds-mobile-nav-panel" aria-label="Mobile">
|
|
4367
|
+
{%- for link in section.settings.menu.links -%}
|
|
4368
|
+
<a class="ds-header-link" href="{{ link.url }}"{% if link.current %} aria-current="page"{% endif %}>{{ link.title }}</a>
|
|
4369
|
+
{%- endfor -%}
|
|
4370
|
+
{%- if section.settings.show_search -%}<a class="ds-header-link" href="{{ routes.search_url }}">Search</a>{%- endif -%}
|
|
4371
|
+
{%- if shop.customer_accounts_enabled and section.settings.show_account -%}<a class="ds-header-link" href="{{ routes.account_url }}">Account</a>{%- endif -%}
|
|
4372
|
+
</nav>
|
|
4373
|
+
</details>
|
|
4374
|
+
<a class="ds-header-brand" href="{{ routes.root_url }}">
|
|
4375
|
+
{%- if section.settings.logo -%}
|
|
4376
|
+
{{ section.settings.logo | image_url: width: 360 | image_tag: widths: '180, 240, 360', alt: shop.name, class: 'ds-header-logo' }}
|
|
4377
|
+
{%- else -%}
|
|
4378
|
+
{{ shop.name }}
|
|
4379
|
+
{%- endif -%}
|
|
4380
|
+
</a>
|
|
4381
|
+
<nav class="ds-header-nav" aria-label="Main">
|
|
4382
|
+
{%- for link in section.settings.menu.links -%}
|
|
4383
|
+
<a href="{{ link.url }}"{% if link.current %} aria-current="page"{% endif %}>{{ link.title }}</a>
|
|
4384
|
+
{%- endfor -%}
|
|
4385
|
+
</nav>
|
|
4386
|
+
<div class="ds-header-actions">
|
|
4387
|
+
{%- if section.settings.show_search -%}<a class="ds-header-link" href="{{ routes.search_url }}">Search</a>{%- endif -%}
|
|
4388
|
+
{%- if shop.customer_accounts_enabled and section.settings.show_account -%}<a class="ds-header-link" href="{{ routes.account_url }}">Account</a>{%- endif -%}
|
|
4389
|
+
<a class="ds-header-link ds-cart-link" href="{{ routes.cart_url }}"><span>Cart</span><span class="ds-cart-count">{{ cart.item_count }}</span></a>
|
|
4390
|
+
</div>
|
|
4391
|
+
</header>
|
|
4392
|
+
</div>
|
|
3457
4393
|
{% schema %}
|
|
3458
4394
|
{
|
|
3459
4395
|
"name": "DS Header",
|
|
3460
4396
|
"settings": [
|
|
3461
4397
|
{ "type": "image_picker", "id": "logo", "label": "Logo" },
|
|
3462
|
-
{ "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" }
|
|
4398
|
+
{ "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" },
|
|
4399
|
+
{ "type": "text", "id": "announcement_text", "label": "Announcement", "default": "Free shipping and easy returns" },
|
|
4400
|
+
{ "type": "text", "id": "announcement_link_label", "label": "Announcement link label" },
|
|
4401
|
+
{ "type": "url", "id": "announcement_link", "label": "Announcement link" },
|
|
4402
|
+
{ "type": "checkbox", "id": "show_search", "label": "Show search", "default": true },
|
|
4403
|
+
{ "type": "checkbox", "id": "show_account", "label": "Show account", "default": true },
|
|
4404
|
+
{ "type": "checkbox", "id": "sticky", "label": "Sticky header", "default": true }
|
|
3463
4405
|
],
|
|
3464
4406
|
"presets": [{ "name": "DS Header" }]
|
|
3465
4407
|
}
|
|
3466
4408
|
{% endschema %}`;
|
|
3467
4409
|
SECTION_FOOTER = `{% comment %}
|
|
3468
|
-
|
|
4410
|
+
Complete storefront footer with navigation, newsletter, policies, and
|
|
4411
|
+
payment methods. The generated footer group installs it globally.
|
|
3469
4412
|
{% endcomment %}
|
|
3470
4413
|
<footer class="ds ds-embed ds-footer">
|
|
3471
|
-
<
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
4414
|
+
<div class="ds-container ds-card ds-footer-panel">
|
|
4415
|
+
<div class="ds-footer-grid">
|
|
4416
|
+
<div class="ds-footer-brand">
|
|
4417
|
+
<a class="ds-header-brand" href="{{ routes.root_url }}">{{ shop.name }}</a>
|
|
4418
|
+
{%- if section.settings.text != blank -%}<div class="ds-section-copy">{{ section.settings.text }}</div>{%- endif -%}
|
|
4419
|
+
{%- if section.settings.show_newsletter -%}
|
|
4420
|
+
{% render 'ds-newsletter', id: section.id, heading: section.settings.newsletter_heading, button_label: section.settings.newsletter_button_label %}
|
|
4421
|
+
{%- endif -%}
|
|
4422
|
+
</div>
|
|
4423
|
+
<div>
|
|
4424
|
+
<h2 class="ds-footer-title">{{ section.settings.menu_heading }}</h2>
|
|
4425
|
+
<nav aria-label="{{ section.settings.menu_heading | escape }}">
|
|
4426
|
+
{%- for link in section.settings.menu.links -%}<a href="{{ link.url }}">{{ link.title }}</a>{%- endfor -%}
|
|
4427
|
+
</nav>
|
|
4428
|
+
</div>
|
|
4429
|
+
<div>
|
|
4430
|
+
<h2 class="ds-footer-title">{{ section.settings.secondary_menu_heading }}</h2>
|
|
4431
|
+
<nav aria-label="{{ section.settings.secondary_menu_heading | escape }}">
|
|
4432
|
+
{%- for link in section.settings.secondary_menu.links -%}<a href="{{ link.url }}">{{ link.title }}</a>{%- endfor -%}
|
|
4433
|
+
</nav>
|
|
4434
|
+
</div>
|
|
4435
|
+
</div>
|
|
4436
|
+
<div class="ds-footer-bottom">
|
|
4437
|
+
<div>
|
|
4438
|
+
<p class="ds-supporting">© {{ 'now' | date: '%Y' }} {{ shop.name }}</p>
|
|
4439
|
+
{%- if section.settings.show_policies -%}
|
|
4440
|
+
<ul class="ds-policy-list" role="list">
|
|
4441
|
+
{%- for policy in shop.policies -%}{%- if policy != blank -%}<li><a href="{{ policy.url }}">{{ policy.title }}</a></li>{%- endif -%}{%- endfor -%}
|
|
4442
|
+
</ul>
|
|
4443
|
+
{%- endif -%}
|
|
4444
|
+
</div>
|
|
4445
|
+
{%- if section.settings.show_payment_icons -%}
|
|
4446
|
+
<ul class="ds-payment-list" role="list" aria-label="Accepted payment methods">
|
|
4447
|
+
{%- for type in shop.enabled_payment_types -%}<li>{{ type | payment_type_svg_tag: class: 'ds-payment-icon' }}</li>{%- endfor -%}
|
|
4448
|
+
</ul>
|
|
4449
|
+
{%- endif -%}
|
|
4450
|
+
</div>
|
|
4451
|
+
</div>
|
|
3478
4452
|
</footer>
|
|
3479
4453
|
{% schema %}
|
|
3480
4454
|
{
|
|
3481
4455
|
"name": "DS Footer",
|
|
3482
4456
|
"settings": [
|
|
3483
|
-
{ "type": "
|
|
4457
|
+
{ "type": "richtext", "id": "text", "label": "Store description", "default": "<p>Thoughtful products, clear service, and a storefront designed around what matters.</p>" },
|
|
4458
|
+
{ "type": "text", "id": "menu_heading", "label": "Menu heading", "default": "Shop" },
|
|
4459
|
+
{ "type": "link_list", "id": "menu", "label": "Menu", "default": "main-menu" },
|
|
4460
|
+
{ "type": "text", "id": "secondary_menu_heading", "label": "Secondary menu heading", "default": "Help" },
|
|
4461
|
+
{ "type": "link_list", "id": "secondary_menu", "label": "Secondary menu", "default": "footer" },
|
|
4462
|
+
{ "type": "checkbox", "id": "show_newsletter", "label": "Show newsletter", "default": true },
|
|
4463
|
+
{ "type": "text", "id": "newsletter_heading", "label": "Newsletter heading", "default": "Get product news and useful updates" },
|
|
4464
|
+
{ "type": "text", "id": "newsletter_button_label", "label": "Newsletter button label", "default": "Subscribe" },
|
|
4465
|
+
{ "type": "checkbox", "id": "show_policies", "label": "Show policies", "default": true },
|
|
4466
|
+
{ "type": "checkbox", "id": "show_payment_icons", "label": "Show payment icons", "default": true }
|
|
3484
4467
|
],
|
|
3485
4468
|
"presets": [{ "name": "DS Footer" }]
|
|
3486
4469
|
}
|
|
3487
4470
|
{% endschema %}`;
|
|
3488
4471
|
SECTION_HERO = `{% comment %}
|
|
3489
|
-
|
|
4472
|
+
Product-led hero. A selected product supplies its image, price, and link;
|
|
4473
|
+
otherwise the section falls back to the first all-products item.
|
|
3490
4474
|
{% endcomment %}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
4475
|
+
{%- liquid
|
|
4476
|
+
assign ds_product = section.settings.product
|
|
4477
|
+
if ds_product == blank
|
|
4478
|
+
assign ds_product = collections.all.products.first
|
|
4479
|
+
endif
|
|
4480
|
+
assign ds_has_hero_image = false
|
|
4481
|
+
if section.settings.image or ds_product.featured_image
|
|
4482
|
+
assign ds_has_hero_image = true
|
|
4483
|
+
endif
|
|
4484
|
+
assign ds_show_hero_media = ds_has_hero_image
|
|
4485
|
+
if request.design_mode
|
|
4486
|
+
assign ds_show_hero_media = true
|
|
4487
|
+
endif
|
|
4488
|
+
assign ds_button_link = section.settings.button_link | default: ds_product.url | default: routes.all_products_collection_url
|
|
4489
|
+
-%}
|
|
4490
|
+
<section class="ds ds-embed ds-hero-section">
|
|
4491
|
+
<div class="ds-container ds-hero{% unless ds_show_hero_media %} ds-hero-single{% endunless %}">
|
|
4492
|
+
<div class="ds-hero-content">
|
|
4493
|
+
{%- if request.page_type == 'index' -%}<h1 class="ds-display">{{ section.settings.heading }}</h1>{%- else -%}<h2 class="ds-display">{{ section.settings.heading }}</h2>{%- endif -%}
|
|
4494
|
+
{%- if section.settings.text != blank -%}<p class="ds-supporting">{{ section.settings.text }}</p>{%- endif -%}
|
|
4495
|
+
{%- if ds_product != blank -%}{% render 'ds-price', product: ds_product %}{%- endif -%}
|
|
4496
|
+
<div class="ds-hero-actions">
|
|
4497
|
+
{%- if section.settings.button_label != blank -%}<a class="ds-btn ds-btn-primary" href="{{ ds_button_link }}">{{ section.settings.button_label }}</a>{%- endif -%}
|
|
4498
|
+
{%- if section.settings.secondary_label != blank -%}<a class="ds-btn ds-btn-outline" href="{{ section.settings.secondary_link }}">{{ section.settings.secondary_label }}</a>{%- endif -%}
|
|
4499
|
+
</div>
|
|
4500
|
+
{%- if section.settings.trust_text != blank -%}<p class="ds-hero-trust">{{ section.settings.trust_text }}</p>{%- endif -%}
|
|
4501
|
+
</div>
|
|
4502
|
+
{%- if ds_show_hero_media -%}<div class="ds-hero-media{% if ds_has_hero_image %} ds-hero-media-has-image{% endif %}">
|
|
4503
|
+
{%- if section.settings.image -%}
|
|
4504
|
+
{%- assign ds_image_alt = section.settings.heading | strip_html -%}
|
|
4505
|
+
{{ section.settings.image | image_url: width: 1600 | image_tag: widths: '600, 900, 1200, 1600', sizes: '(min-width: 990px) 55vw, 100vw', alt: ds_image_alt }}
|
|
4506
|
+
{%- elsif ds_product.featured_image -%}
|
|
4507
|
+
{%- assign ds_image_alt = ds_product.featured_image.alt | default: ds_product.title -%}
|
|
4508
|
+
{{ ds_product.featured_image | image_url: width: 1600 | image_tag: widths: '600, 900, 1200, 1600', sizes: '(min-width: 990px) 55vw, 100vw', alt: ds_image_alt }}
|
|
4509
|
+
{%- else -%}
|
|
4510
|
+
<div class="ds-card ds-media-empty">
|
|
4511
|
+
<span class="ds-icon-tile ds-icon-tile-blue">{% render 'ds-icon', icon: 'image' %}</span>
|
|
4512
|
+
<p class="ds-media-empty-title">Choose a hero image</p>
|
|
4513
|
+
<p class="ds-media-empty-copy">Select an image or featured product in this section.</p>
|
|
4514
|
+
</div>
|
|
4515
|
+
{%- endif -%}
|
|
4516
|
+
</div>{%- endif -%}
|
|
3505
4517
|
</div>
|
|
3506
|
-
</
|
|
4518
|
+
</section>
|
|
3507
4519
|
{% schema %}
|
|
3508
4520
|
{
|
|
3509
4521
|
"name": "DS Hero",
|
|
3510
4522
|
"settings": [
|
|
4523
|
+
{ "type": "product", "id": "product", "label": "Featured product" },
|
|
3511
4524
|
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
3512
|
-
{ "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "
|
|
3513
|
-
{ "type": "text", "id": "text", "label": "Supporting line" },
|
|
3514
|
-
{ "type": "text", "id": "button_label", "label": "Button label", "default": "Shop
|
|
4525
|
+
{ "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Made for every day" },
|
|
4526
|
+
{ "type": "text", "id": "text", "label": "Supporting line", "default": "Useful products, thoughtful details, and clear service." },
|
|
4527
|
+
{ "type": "text", "id": "button_label", "label": "Button label", "default": "Shop the collection" },
|
|
3515
4528
|
{ "type": "url", "id": "button_link", "label": "Button link" },
|
|
3516
4529
|
{ "type": "text", "id": "secondary_label", "label": "Secondary button label" },
|
|
3517
|
-
{ "type": "url", "id": "secondary_link", "label": "Secondary button link" }
|
|
4530
|
+
{ "type": "url", "id": "secondary_link", "label": "Secondary button link" },
|
|
4531
|
+
{ "type": "text", "id": "trust_text", "label": "Trust line", "default": "Secure checkout. Easy returns. Human support." }
|
|
3518
4532
|
],
|
|
3519
4533
|
"presets": [{ "name": "DS Hero" }]
|
|
3520
4534
|
}
|
|
@@ -3522,13 +4536,15 @@ var init_shopify_kit = __esm({
|
|
|
3522
4536
|
SECTION_RICH_TEXT = `{% comment %}
|
|
3523
4537
|
Heading plus rich text, with an optional button.
|
|
3524
4538
|
{% endcomment %}
|
|
3525
|
-
<
|
|
4539
|
+
<section class="ds ds-embed ds-section">
|
|
4540
|
+
<div class="ds-container ds-rich-text">
|
|
3526
4541
|
{%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
|
|
3527
|
-
<div>{{ section.settings.text }}</div>
|
|
4542
|
+
<div class="ds-section-copy">{{ section.settings.text }}</div>
|
|
3528
4543
|
{%- if section.settings.button_label != blank -%}
|
|
3529
|
-
<
|
|
4544
|
+
<a class="ds-btn ds-btn-secondary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a>
|
|
3530
4545
|
{%- endif -%}
|
|
3531
4546
|
</div>
|
|
4547
|
+
</section>
|
|
3532
4548
|
{% schema %}
|
|
3533
4549
|
{
|
|
3534
4550
|
"name": "DS Rich text",
|
|
@@ -3544,23 +4560,38 @@ var init_shopify_kit = __esm({
|
|
|
3544
4560
|
SECTION_IMAGE_WITH_TEXT = `{% comment %}
|
|
3545
4561
|
Image beside heading, text, and an optional button.
|
|
3546
4562
|
{% endcomment %}
|
|
3547
|
-
|
|
4563
|
+
{%- assign ds_story_product = section.settings.product | default: collections.all.products[1] | default: collections.all.products.first -%}
|
|
4564
|
+
<section class="ds ds-embed ds-section">
|
|
4565
|
+
<div class="ds-container ds-image-text{% unless section.settings.image or ds_story_product.featured_image or request.design_mode %} ds-image-text-single{% endunless %}">
|
|
4566
|
+
{%- if section.settings.image or ds_story_product.featured_image or request.design_mode -%}<div class="ds-card ds-image-text-media">
|
|
3548
4567
|
{%- if section.settings.image -%}
|
|
3549
4568
|
{%- assign ds_image_alt = section.settings.heading | strip_html -%}
|
|
3550
4569
|
{{ section.settings.image | image_url: width: 900 | image_tag: widths: '480, 720, 900', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
|
|
4570
|
+
{%- elsif ds_story_product.featured_image -%}
|
|
4571
|
+
{%- assign ds_image_alt = ds_story_product.featured_image.alt | default: ds_story_product.title -%}
|
|
4572
|
+
{{ ds_story_product.featured_image | image_url: width: 1200 | image_tag: widths: '480, 720, 900, 1200', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
|
|
4573
|
+
{%- else -%}
|
|
4574
|
+
<div class="ds-media-empty">
|
|
4575
|
+
<span class="ds-icon-tile ds-icon-tile-orange">{% render 'ds-icon', icon: 'image' %}</span>
|
|
4576
|
+
<p class="ds-media-empty-title">Choose a story image</p>
|
|
4577
|
+
<p class="ds-media-empty-copy">Select an image or product for this section.</p>
|
|
4578
|
+
</div>
|
|
3551
4579
|
{%- endif -%}
|
|
4580
|
+
</div>{%- endif -%}
|
|
3552
4581
|
<div class="ds-product-info">
|
|
3553
4582
|
<h2 class="ds-section-title">{{ section.settings.heading }}</h2>
|
|
3554
|
-
<div>{{ section.settings.text }}</div>
|
|
4583
|
+
<div class="ds-section-copy">{{ section.settings.text }}</div>
|
|
3555
4584
|
{%- if section.settings.button_label != blank -%}
|
|
3556
4585
|
<p><a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
|
|
3557
4586
|
{%- endif -%}
|
|
3558
4587
|
</div>
|
|
3559
4588
|
</div>
|
|
4589
|
+
</section>
|
|
3560
4590
|
{% schema %}
|
|
3561
4591
|
{
|
|
3562
4592
|
"name": "DS Image with text",
|
|
3563
4593
|
"settings": [
|
|
4594
|
+
{ "type": "product", "id": "product", "label": "Product image fallback" },
|
|
3564
4595
|
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
3565
4596
|
{ "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Pair text with an image" },
|
|
3566
4597
|
{ "type": "richtext", "id": "text", "label": "Text" },
|
|
@@ -3577,10 +4608,14 @@ var init_shopify_kit = __esm({
|
|
|
3577
4608
|
<div class="ds ds-embed ds-container ds-product-page">
|
|
3578
4609
|
<div class="ds-product-layout">
|
|
3579
4610
|
<div class="ds-product-media">
|
|
4611
|
+
<div class="ds-card ds-product-media-main">
|
|
3580
4612
|
{%- if product.featured_image -%}
|
|
3581
4613
|
{%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
|
|
3582
4614
|
{{ product.featured_image | image_url: width: 1200 | image_tag: widths: '480, 720, 960, 1200', sizes: '(min-width: 750px) 50vw, 100vw', alt: ds_image_alt }}
|
|
4615
|
+
{%- else -%}
|
|
4616
|
+
<div class="ds-media-empty" role="img" aria-label="Product image unavailable">{% render 'ds-icon', icon: 'image' %}</div>
|
|
3583
4617
|
{%- endif -%}
|
|
4618
|
+
</div>
|
|
3584
4619
|
{%- if product.images.size > 1 -%}
|
|
3585
4620
|
<div class="ds-product-thumbs">
|
|
3586
4621
|
{%- for image in product.images offset: 1 limit: 4 -%}
|
|
@@ -3590,12 +4625,13 @@ var init_shopify_kit = __esm({
|
|
|
3590
4625
|
</div>
|
|
3591
4626
|
{%- endif -%}
|
|
3592
4627
|
</div>
|
|
3593
|
-
<div class="ds-product-info">
|
|
3594
|
-
{%- if section.settings.show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
4628
|
+
<div class="ds-card ds-product-info">
|
|
3595
4629
|
<h1 class="ds-display">{{ product.title }}</h1>
|
|
4630
|
+
{%- comment -%}Vendor sits under the title: a label above a heading is banned house style.{%- endcomment -%}
|
|
4631
|
+
{%- if section.settings.show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
3596
4632
|
{% render 'ds-price', variant: product.selected_or_first_available_variant %}
|
|
3597
4633
|
{% render 'ds-buy-button', product: product, show_quantity: section.settings.show_quantity %}
|
|
3598
|
-
<div>{{ product.description }}</div>
|
|
4634
|
+
<div class="ds-product-description">{{ product.description }}</div>
|
|
3599
4635
|
</div>
|
|
3600
4636
|
</div>
|
|
3601
4637
|
</div>
|
|
@@ -3615,9 +4651,11 @@ var init_shopify_kit = __esm({
|
|
|
3615
4651
|
{% endcomment %}
|
|
3616
4652
|
{% paginate collection.products by section.settings.products_per_page %}
|
|
3617
4653
|
<div class="ds ds-embed ds-container ds-collection-page">
|
|
3618
|
-
<
|
|
3619
|
-
|
|
3620
|
-
|
|
4654
|
+
<div class="ds-collection-head">
|
|
4655
|
+
<h1 class="ds-display">{{ collection.title }}</h1>
|
|
4656
|
+
{%- if collection.description != blank -%}<div class="ds-section-copy">{{ collection.description }}</div>{%- endif -%}
|
|
4657
|
+
</div>
|
|
4658
|
+
<div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
|
|
3621
4659
|
{%- for product in collection.products -%}
|
|
3622
4660
|
{% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
|
|
3623
4661
|
{%- else -%}
|
|
@@ -3642,9 +4680,10 @@ var init_shopify_kit = __esm({
|
|
|
3642
4680
|
SECTION_CONTACT = `{% comment %}
|
|
3643
4681
|
Contact form posting to Shopify's contact endpoint. No JavaScript.
|
|
3644
4682
|
{% endcomment %}
|
|
3645
|
-
<
|
|
4683
|
+
<section class="ds ds-embed ds-section">
|
|
4684
|
+
<div class="ds-container ds-page">
|
|
3646
4685
|
<h2 class="ds-section-title">{{ section.settings.heading }}</h2>
|
|
3647
|
-
{% form 'contact' %}
|
|
4686
|
+
{% form 'contact', class: 'ds-card ds-contact-form' %}
|
|
3648
4687
|
{%- if form.posted_successfully? -%}
|
|
3649
4688
|
<p role="status">Thanks for your message. We reply within one business day.</p>
|
|
3650
4689
|
{%- else -%}
|
|
@@ -3665,6 +4704,7 @@ var init_shopify_kit = __esm({
|
|
|
3665
4704
|
{%- endif -%}
|
|
3666
4705
|
{% endform %}
|
|
3667
4706
|
</div>
|
|
4707
|
+
</section>
|
|
3668
4708
|
{% schema %}
|
|
3669
4709
|
{
|
|
3670
4710
|
"name": "DS Contact",
|
|
@@ -3677,19 +4717,22 @@ var init_shopify_kit = __esm({
|
|
|
3677
4717
|
SECTION_MULTICOLUMN = `{% comment %}
|
|
3678
4718
|
Row of text cards, for value props or feature callouts.
|
|
3679
4719
|
{% endcomment %}
|
|
3680
|
-
<
|
|
3681
|
-
|
|
3682
|
-
<div class="ds-
|
|
4720
|
+
<section class="ds ds-embed ds-section-compact">
|
|
4721
|
+
<div class="ds-container">
|
|
4722
|
+
{%- if section.settings.heading != blank -%}<div class="ds-section-head"><h2 class="ds-section-title">{{ section.settings.heading }}</h2></div>{%- endif -%}
|
|
4723
|
+
<div class="ds-card ds-value-panel" style="--ds-cols: {{ section.settings.columns_desktop }}">
|
|
3683
4724
|
{%- for block in section.blocks -%}
|
|
3684
|
-
<div class="ds-
|
|
3685
|
-
|
|
3686
|
-
|
|
4725
|
+
<div class="ds-value-item" {{ block.shopify_attributes }}>
|
|
4726
|
+
<div class="ds-value-title-row">
|
|
4727
|
+
{%- if block.settings.icon != 'none' -%}<span class="ds-icon-tile {% cycle 'ds-icon-tile-blue', 'ds-icon-tile-orange', 'ds-icon-tile-green' %}">{% render 'ds-icon', icon: block.settings.icon %}</span>{%- endif -%}
|
|
4728
|
+
<h3 class="ds-card-title">{{ block.settings.heading }}</h3>
|
|
4729
|
+
</div>
|
|
3687
4730
|
<p class="ds-card-desc">{{ block.settings.text }}</p>
|
|
3688
|
-
</div>
|
|
3689
4731
|
</div>
|
|
3690
4732
|
{%- endfor -%}
|
|
3691
4733
|
</div>
|
|
3692
4734
|
</div>
|
|
4735
|
+
</section>
|
|
3693
4736
|
{% schema %}
|
|
3694
4737
|
{
|
|
3695
4738
|
"name": "DS Multicolumn",
|
|
@@ -3702,6 +4745,7 @@ var init_shopify_kit = __esm({
|
|
|
3702
4745
|
"type": "column",
|
|
3703
4746
|
"name": "Column",
|
|
3704
4747
|
"settings": [
|
|
4748
|
+
{ "type": "select", "id": "icon", "label": "Icon", "default": "quality", "options": [{ "value": "quality", "label": "Quality" }, { "value": "delivery", "label": "Delivery" }, { "value": "support", "label": "Support" }, { "value": "none", "label": "None" }] },
|
|
3705
4749
|
{ "type": "text", "id": "heading", "label": "Heading" },
|
|
3706
4750
|
{ "type": "text", "id": "text", "label": "Text" }
|
|
3707
4751
|
]
|
|
@@ -3711,27 +4755,231 @@ var init_shopify_kit = __esm({
|
|
|
3711
4755
|
"presets": [{ "name": "DS Multicolumn", "blocks": [{ "type": "column" }, { "type": "column" }, { "type": "column" }] }]
|
|
3712
4756
|
}
|
|
3713
4757
|
{% endschema %}`;
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
"
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
4758
|
+
SECTION_MAIN_PAGE = `{% comment %}
|
|
4759
|
+
Default content page, styled by the selected design system.
|
|
4760
|
+
{% endcomment %}
|
|
4761
|
+
<main class="ds ds-embed ds-container ds-page">
|
|
4762
|
+
<header class="ds-page-head"><h1 class="ds-display">{{ page.title }}</h1></header>
|
|
4763
|
+
<div class="ds-card ds-page-content ds-product-description">{{ page.content }}</div>
|
|
4764
|
+
</main>
|
|
4765
|
+
{% schema %}
|
|
4766
|
+
{
|
|
4767
|
+
"name": "DS Page",
|
|
4768
|
+
"enabled_on": { "templates": ["page"] }
|
|
4769
|
+
}
|
|
4770
|
+
{% endschema %}`;
|
|
4771
|
+
SECTION_MAIN_CART = `{% comment %}
|
|
4772
|
+
Native cart page. Quantity changes post through Shopify without JavaScript.
|
|
4773
|
+
{% endcomment %}
|
|
4774
|
+
<main class="ds ds-embed ds-container ds-cart-page">
|
|
4775
|
+
<header class="ds-page-head"><h1 class="ds-display">Your cart</h1></header>
|
|
4776
|
+
{%- if cart.item_count > 0 -%}
|
|
4777
|
+
<form action="{{ routes.cart_url }}" method="post">
|
|
4778
|
+
<ul class="ds-cart-list" role="list">
|
|
4779
|
+
{%- for item in cart.items -%}
|
|
4780
|
+
<li class="ds-card ds-cart-item">
|
|
4781
|
+
<a href="{{ item.url }}" aria-label="{{ item.product.title | escape }}">
|
|
4782
|
+
{%- if item.image -%}
|
|
4783
|
+
{%- assign ds_cart_alt = item.image.alt | default: item.product.title -%}
|
|
4784
|
+
{{ item.image | image_url: width: 280 | image_tag: widths: '140, 210, 280', alt: ds_cart_alt }}
|
|
4785
|
+
{%- else -%}
|
|
4786
|
+
<span class="ds-media-empty" role="img" aria-label="Image unavailable">{% render 'ds-icon', icon: 'image' %}</span>
|
|
4787
|
+
{%- endif -%}
|
|
4788
|
+
</a>
|
|
4789
|
+
<div>
|
|
4790
|
+
<h2 class="ds-card-title"><a href="{{ item.url }}">{{ item.product.title }}</a></h2>
|
|
4791
|
+
{%- unless item.product.has_only_default_variant -%}<p class="ds-supporting">{{ item.variant.title }}</p>{%- endunless -%}
|
|
4792
|
+
<label class="ds-label" for="ds-cart-quantity-{{ item.index | plus: 1 }}">Quantity</label>
|
|
4793
|
+
<input class="ds-input ds-qty" id="ds-cart-quantity-{{ item.index | plus: 1 }}" type="number" name="updates[]" value="{{ item.quantity }}" min="0">
|
|
4794
|
+
<a class="ds-header-link" href="{{ item.url_to_remove }}">Remove</a>
|
|
4795
|
+
</div>
|
|
4796
|
+
<strong class="ds-cart-item-total">{{ item.final_line_price | money }}</strong>
|
|
4797
|
+
</li>
|
|
4798
|
+
{%- endfor -%}
|
|
4799
|
+
</ul>
|
|
4800
|
+
<div class="ds-card ds-cart-summary">
|
|
4801
|
+
<p class="ds-supporting">Taxes and shipping calculated at checkout.</p>
|
|
4802
|
+
<div class="ds-price"><span>Subtotal</span><span class="ds-price-current">{{ cart.total_price | money }}</span></div>
|
|
4803
|
+
<div class="ds-hero-actions">
|
|
4804
|
+
<button class="ds-btn ds-btn-outline" type="submit" name="update">Update cart</button>
|
|
4805
|
+
<button class="ds-btn ds-btn-primary" type="submit" name="checkout">Checkout</button>
|
|
4806
|
+
</div>
|
|
4807
|
+
</div>
|
|
4808
|
+
</form>
|
|
4809
|
+
{%- else -%}
|
|
4810
|
+
<div class="ds-card ds-empty"><p class="ds-section-copy">Your cart is empty.</p><a class="ds-btn ds-btn-primary" href="{{ routes.all_products_collection_url }}">Continue shopping</a></div>
|
|
4811
|
+
{%- endif -%}
|
|
4812
|
+
</main>
|
|
4813
|
+
{% schema %}
|
|
4814
|
+
{
|
|
4815
|
+
"name": "DS Cart",
|
|
4816
|
+
"enabled_on": { "templates": ["cart"] }
|
|
4817
|
+
}
|
|
4818
|
+
{% endschema %}`;
|
|
4819
|
+
SECTION_MAIN_SEARCH = `{% comment %}
|
|
4820
|
+
Search page with native Shopify query handling and paginated results.
|
|
4821
|
+
{% endcomment %}
|
|
4822
|
+
<main class="ds ds-embed ds-container ds-search-page">
|
|
4823
|
+
<header class="ds-page-head"><h1 class="ds-display">Search</h1></header>
|
|
4824
|
+
<form class="ds-card ds-search-form" action="{{ routes.search_url }}" method="get" role="search">
|
|
4825
|
+
<div>
|
|
4826
|
+
<label class="ds-label" for="ds-search-query-{{ section.id }}">What are you looking for?</label>
|
|
4827
|
+
<input class="ds-input" id="ds-search-query-{{ section.id }}" type="search" name="q" value="{{ search.terms | escape }}" autocomplete="off">
|
|
4828
|
+
</div>
|
|
4829
|
+
<button class="ds-btn ds-btn-primary" type="submit">Search</button>
|
|
4830
|
+
</form>
|
|
4831
|
+
{%- if search.performed -%}
|
|
4832
|
+
{% paginate search.results by section.settings.results_per_page %}
|
|
4833
|
+
<div class="ds-section-head"><h2 class="ds-section-title">{{ search.results_count }} results</h2></div>
|
|
4834
|
+
<div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
|
|
4835
|
+
{%- for item in search.results -%}
|
|
4836
|
+
{%- if item.object_type == 'product' -%}
|
|
4837
|
+
{% render 'ds-product-card', product: item %}
|
|
4838
|
+
{%- else -%}
|
|
4839
|
+
<article class="ds-card"><div class="ds-card-body"><h3 class="ds-card-title"><a href="{{ item.url }}">{{ item.title }}</a></h3><p class="ds-card-desc">{{ item.content | strip_html | truncatewords: 24 }}</p></div></article>
|
|
4840
|
+
{%- endif -%}
|
|
4841
|
+
{%- else -%}
|
|
4842
|
+
<div class="ds-card ds-empty ds-grid-empty"><p class="ds-section-copy">No results found. Try a shorter or more general search.</p><a class="ds-btn ds-btn-outline" href="{{ routes.all_products_collection_url }}">Browse all products</a></div>
|
|
4843
|
+
{%- endfor -%}
|
|
4844
|
+
</div>
|
|
4845
|
+
{% render 'ds-pagination', paginate: paginate %}
|
|
4846
|
+
{% endpaginate %}
|
|
4847
|
+
{%- endif -%}
|
|
4848
|
+
</main>
|
|
4849
|
+
{% schema %}
|
|
4850
|
+
{
|
|
4851
|
+
"name": "DS Search",
|
|
4852
|
+
"settings": [
|
|
4853
|
+
{ "type": "range", "id": "results_per_page", "min": 4, "max": 24, "step": 4, "default": 12, "label": "Results per page" },
|
|
4854
|
+
{ "type": "range", "id": "columns_desktop", "min": 2, "max": 4, "step": 1, "default": 4, "label": "Columns on desktop" }
|
|
4855
|
+
],
|
|
4856
|
+
"enabled_on": { "templates": ["search"] }
|
|
4857
|
+
}
|
|
4858
|
+
{% endschema %}`;
|
|
4859
|
+
SECTION_MAIN_404 = `{% comment %}
|
|
4860
|
+
Compact not-found page with a clear recovery action.
|
|
4861
|
+
{% endcomment %}
|
|
4862
|
+
<main class="ds ds-embed ds-container ds-page">
|
|
4863
|
+
<div class="ds-card ds-empty">
|
|
4864
|
+
<h1 class="ds-display">Page not found</h1>
|
|
4865
|
+
<p class="ds-section-copy">The page may have moved or the link may be incorrect.</p>
|
|
4866
|
+
<a class="ds-btn ds-btn-primary" href="{{ routes.all_products_collection_url }}">Browse all products</a>
|
|
4867
|
+
</div>
|
|
4868
|
+
</main>
|
|
4869
|
+
{% schema %}
|
|
4870
|
+
{
|
|
4871
|
+
"name": "DS 404",
|
|
4872
|
+
"enabled_on": { "templates": ["404"] }
|
|
4873
|
+
}
|
|
4874
|
+
{% endschema %}`;
|
|
4875
|
+
TEMPLATE_INDEX_JSON = `{
|
|
4876
|
+
"sections": {
|
|
4877
|
+
"hero": {
|
|
4878
|
+
"type": "ds-hero",
|
|
4879
|
+
"settings": {
|
|
4880
|
+
"heading": "Made for every day",
|
|
4881
|
+
"text": "Useful products, thoughtful details, and clear service.",
|
|
4882
|
+
"button_label": "Shop the collection",
|
|
4883
|
+
"button_link": "shopify://collections/all",
|
|
4884
|
+
"trust_text": "Secure checkout. Easy returns. Human support."
|
|
4885
|
+
}
|
|
4886
|
+
},
|
|
4887
|
+
"featured": {
|
|
4888
|
+
"type": "ds-featured-collection",
|
|
4889
|
+
"settings": { "heading": "Featured products", "collection": "all", "products_to_show": 4, "columns_desktop": 4, "show_view_all": true }
|
|
4890
|
+
},
|
|
4891
|
+
"story": {
|
|
4892
|
+
"type": "ds-image-with-text",
|
|
4893
|
+
"settings": {
|
|
4894
|
+
"heading": "Useful by design",
|
|
4895
|
+
"text": "<p>Every product should earn its place. Use this section to explain the materials, choices, or details that make yours worth choosing.</p>",
|
|
4896
|
+
"button_label": "Explore the collection",
|
|
4897
|
+
"button_link": "shopify://collections/all"
|
|
4898
|
+
}
|
|
4899
|
+
},
|
|
4900
|
+
"values": {
|
|
4901
|
+
"type": "ds-multicolumn",
|
|
4902
|
+
"blocks": {
|
|
4903
|
+
"quality": { "type": "column", "settings": { "icon": "quality", "heading": "Made to last", "text": "Explain the quality customers can see and feel." } },
|
|
4904
|
+
"delivery": { "type": "column", "settings": { "icon": "delivery", "heading": "Clear delivery", "text": "Set useful expectations for shipping and returns." } },
|
|
4905
|
+
"support": { "type": "column", "settings": { "icon": "support", "heading": "Human support", "text": "Tell customers where to get a real answer." } }
|
|
4906
|
+
},
|
|
4907
|
+
"block_order": ["quality", "delivery", "support"],
|
|
4908
|
+
"settings": { "heading": "What you can expect", "columns_desktop": 3 }
|
|
4909
|
+
},
|
|
4910
|
+
"faq": {
|
|
4911
|
+
"type": "ds-faq",
|
|
4912
|
+
"blocks": {
|
|
4913
|
+
"shipping": { "type": "item", "settings": { "question": "When will my order arrive?", "answer": "<p>Add your delivery timing and tracking details here.</p>" } },
|
|
4914
|
+
"returns": { "type": "item", "settings": { "question": "What is your return policy?", "answer": "<p>Summarize the return window and the first step for starting a return.</p>" } },
|
|
4915
|
+
"support": { "type": "item", "settings": { "question": "How can I contact you?", "answer": "<p>Add the fastest support channel and expected reply time.</p>" } }
|
|
4916
|
+
},
|
|
4917
|
+
"block_order": ["shipping", "returns", "support"],
|
|
4918
|
+
"settings": { "heading": "Questions, answered" }
|
|
4919
|
+
}
|
|
4920
|
+
},
|
|
4921
|
+
"order": ["hero", "featured", "story", "values", "faq"]
|
|
4922
|
+
}`;
|
|
4923
|
+
TEMPLATE_PRODUCT_JSON = `{
|
|
4924
|
+
"sections": {
|
|
4925
|
+
"main": {
|
|
4926
|
+
"type": "ds-main-product",
|
|
4927
|
+
"settings": {}
|
|
4928
|
+
}
|
|
4929
|
+
},
|
|
4930
|
+
"order": ["main"]
|
|
4931
|
+
}`;
|
|
4932
|
+
TEMPLATE_PAGE_JSON = `{
|
|
4933
|
+
"sections": { "main": { "type": "ds-main-page", "settings": {} } },
|
|
4934
|
+
"order": ["main"]
|
|
4935
|
+
}`;
|
|
4936
|
+
TEMPLATE_CART_JSON = `{
|
|
4937
|
+
"sections": { "main": { "type": "ds-main-cart", "settings": {} } },
|
|
4938
|
+
"order": ["main"]
|
|
4939
|
+
}`;
|
|
4940
|
+
TEMPLATE_SEARCH_JSON = `{
|
|
4941
|
+
"sections": { "main": { "type": "ds-main-search", "settings": {} } },
|
|
4942
|
+
"order": ["main"]
|
|
4943
|
+
}`;
|
|
4944
|
+
TEMPLATE_404_JSON = `{
|
|
4945
|
+
"sections": { "main": { "type": "ds-main-404", "settings": {} } },
|
|
4946
|
+
"order": ["main"]
|
|
4947
|
+
}`;
|
|
4948
|
+
HEADER_GROUP_JSON = `{
|
|
4949
|
+
"name": "Header group",
|
|
4950
|
+
"type": "header",
|
|
4951
|
+
"sections": {
|
|
4952
|
+
"ds_header": {
|
|
4953
|
+
"type": "ds-header",
|
|
4954
|
+
"settings": { "menu": "main-menu", "announcement_text": "Free shipping and easy returns", "show_search": true, "show_account": true, "sticky": true }
|
|
4955
|
+
}
|
|
4956
|
+
},
|
|
4957
|
+
"order": ["ds_header"]
|
|
4958
|
+
}`;
|
|
4959
|
+
FOOTER_GROUP_JSON = `{
|
|
4960
|
+
"name": "Footer group",
|
|
4961
|
+
"type": "footer",
|
|
4962
|
+
"sections": {
|
|
4963
|
+
"ds_footer": {
|
|
4964
|
+
"type": "ds-footer",
|
|
4965
|
+
"settings": { "menu": "main-menu", "secondary_menu": "footer", "show_newsletter": true, "show_policies": true, "show_payment_icons": true }
|
|
4966
|
+
}
|
|
4967
|
+
},
|
|
4968
|
+
"order": ["ds_footer"]
|
|
4969
|
+
}`;
|
|
4970
|
+
TEMPLATE_COLLECTION_JSON = `{
|
|
4971
|
+
"sections": {
|
|
4972
|
+
"main": {
|
|
4973
|
+
"type": "ds-main-collection",
|
|
4974
|
+
"settings": {}
|
|
4975
|
+
}
|
|
3729
4976
|
},
|
|
3730
4977
|
"order": ["main"]
|
|
3731
4978
|
}`;
|
|
3732
4979
|
SNIPPETS = [
|
|
3733
4980
|
{ path: "snippets/ds-badge.liquid", content: SNIPPET_BADGE },
|
|
3734
4981
|
{ path: "snippets/ds-price.liquid", content: SNIPPET_PRICE },
|
|
4982
|
+
{ path: "snippets/ds-icon.liquid", content: SNIPPET_ICON },
|
|
3735
4983
|
{ path: "snippets/ds-variant-select.liquid", content: SNIPPET_VARIANT_SELECT },
|
|
3736
4984
|
{ path: "snippets/ds-buy-button.liquid", content: SNIPPET_BUY_BUTTON },
|
|
3737
4985
|
{ path: "snippets/ds-product-card.liquid", content: SNIPPET_PRODUCT_CARD },
|
|
@@ -3749,15 +4997,32 @@ var init_shopify_kit = __esm({
|
|
|
3749
4997
|
{ path: "sections/ds-featured-collection.liquid", content: SECTION_FEATURED_COLLECTION },
|
|
3750
4998
|
{ path: "sections/ds-main-product.liquid", content: SECTION_MAIN_PRODUCT },
|
|
3751
4999
|
{ path: "sections/ds-main-collection.liquid", content: SECTION_MAIN_COLLECTION },
|
|
5000
|
+
{ path: "sections/ds-main-page.liquid", content: SECTION_MAIN_PAGE },
|
|
5001
|
+
{ path: "sections/ds-main-cart.liquid", content: SECTION_MAIN_CART },
|
|
5002
|
+
{ path: "sections/ds-main-search.liquid", content: SECTION_MAIN_SEARCH },
|
|
5003
|
+
{ path: "sections/ds-main-404.liquid", content: SECTION_MAIN_404 },
|
|
5004
|
+
{ path: "sections/header-group.json", content: HEADER_GROUP_JSON },
|
|
5005
|
+
{ path: "sections/footer-group.json", content: FOOTER_GROUP_JSON },
|
|
5006
|
+
{ path: "templates/index.json", content: TEMPLATE_INDEX_JSON },
|
|
5007
|
+
{ path: "templates/product.json", content: TEMPLATE_PRODUCT_JSON },
|
|
5008
|
+
{ path: "templates/collection.json", content: TEMPLATE_COLLECTION_JSON },
|
|
5009
|
+
{ path: "templates/page.json", content: TEMPLATE_PAGE_JSON },
|
|
5010
|
+
{ path: "templates/cart.json", content: TEMPLATE_CART_JSON },
|
|
5011
|
+
{ path: "templates/search.json", content: TEMPLATE_SEARCH_JSON },
|
|
5012
|
+
{ path: "templates/404.json", content: TEMPLATE_404_JSON },
|
|
3752
5013
|
{ path: "templates/product.ds.json", content: TEMPLATE_PRODUCT_JSON },
|
|
3753
5014
|
{ path: "templates/collection.ds.json", content: TEMPLATE_COLLECTION_JSON }
|
|
3754
5015
|
];
|
|
3755
5016
|
SHOPIFY_KIT_CONDUCT_MD = [
|
|
3756
5017
|
"## Install into the merchant's theme (mandatory order)",
|
|
3757
|
-
"1.
|
|
3758
|
-
"2.
|
|
3759
|
-
|
|
3760
|
-
"4.
|
|
5018
|
+
"1. Duplicate the merchant's theme and work only in the unpublished copy. This full-store kit replaces the default header/footer groups and primary storefront templates; never install it directly on the live theme.",
|
|
5019
|
+
"2. Write every returned file to its exact path. That includes the CSS asset, snippets, Liquid sections, `sections/header-group.json`, `sections/footer-group.json`, and the primary `templates/*.json` files. Copy contents VERBATIM; retyped Liquid silently drops this system's material.",
|
|
5020
|
+
"3. Load the stylesheet once in `layout/theme.liquid`, before `</head>`: `{{ 'ds-kit.css' | asset_url | stylesheet_tag }}`.",
|
|
5021
|
+
"4. In `layout/theme.liquid`, confirm the theme renders `{% sections 'header-group' %}` and `{% sections 'footer-group' %}`; the generated group files become global only through those calls.",
|
|
5022
|
+
" - If the theme uses different group names, rename the generated group files and call them with `{% sections '<group-name>' %}`.",
|
|
5023
|
+
" - If it renders header/footer sections directly, replace those calls with the generated group calls, or use `{% section 'ds-header' %}` and `{% section 'ds-footer' %}` without group-editor support.",
|
|
5024
|
+
"5. The generated index, product, collection, page, cart, search, and 404 templates make the selected system the storefront default; `product.ds` and `collection.ds` remain available as alternate templates for compatibility.",
|
|
5025
|
+
"6. Open the unpublished theme editor. Select a featured product or hero image, replace the starter copy, verify menus and policies, then review desktop and mobile before publishing.",
|
|
3761
5026
|
"",
|
|
3762
5027
|
"## Class contract (pass/fail)",
|
|
3763
5028
|
"- 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.",
|
|
@@ -3777,6 +5042,43 @@ var init_shopify_kit = __esm({
|
|
|
3777
5042
|
}
|
|
3778
5043
|
});
|
|
3779
5044
|
|
|
5045
|
+
// ../shared/src/registry-css.ts
|
|
5046
|
+
function parseCssVars(css) {
|
|
5047
|
+
const out = { theme: {}, light: {}, dark: {} };
|
|
5048
|
+
const matchBlock = (selector) => {
|
|
5049
|
+
const re = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, "g");
|
|
5050
|
+
const blocks = [];
|
|
5051
|
+
let m;
|
|
5052
|
+
while (m = re.exec(css)) blocks.push(m[1]);
|
|
5053
|
+
return blocks.join("\n");
|
|
5054
|
+
};
|
|
5055
|
+
const extract = (body) => {
|
|
5056
|
+
const result = {};
|
|
5057
|
+
const clean = body.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
5058
|
+
const declRe = /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
|
|
5059
|
+
let m;
|
|
5060
|
+
while (m = declRe.exec(clean)) {
|
|
5061
|
+
result[m[1]] = m[2].replace(/\s+/g, " ").trim();
|
|
5062
|
+
}
|
|
5063
|
+
return result;
|
|
5064
|
+
};
|
|
5065
|
+
out.light = extract(matchBlock(":root"));
|
|
5066
|
+
out.dark = extract(matchBlock("\\.dark"));
|
|
5067
|
+
return out;
|
|
5068
|
+
}
|
|
5069
|
+
function mirrorSingleTheme(vars) {
|
|
5070
|
+
const hasLight = Object.keys(vars.light).length > 0;
|
|
5071
|
+
const hasDark = Object.keys(vars.dark).length > 0;
|
|
5072
|
+
if (hasLight && !hasDark) return { ...vars, dark: { ...vars.light } };
|
|
5073
|
+
if (hasDark && !hasLight) return { ...vars, light: { ...vars.dark } };
|
|
5074
|
+
return vars;
|
|
5075
|
+
}
|
|
5076
|
+
var init_registry_css = __esm({
|
|
5077
|
+
"../shared/src/registry-css.ts"() {
|
|
5078
|
+
"use strict";
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
5081
|
+
|
|
3780
5082
|
// ../shared/src/mcp/preview-widget-sdk.generated.ts
|
|
3781
5083
|
var PREVIEW_WIDGET_SDK_JS;
|
|
3782
5084
|
var init_preview_widget_sdk_generated = __esm({
|
|
@@ -4441,6 +5743,26 @@ function decodeArbitrary(raw) {
|
|
|
4441
5743
|
const v = raw.replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
4442
5744
|
return v && !/[{}<>;]/.test(v) ? v : null;
|
|
4443
5745
|
}
|
|
5746
|
+
function ringWidthRule(width) {
|
|
5747
|
+
return `--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(${width} + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:${RING_BOX_SHADOW}`;
|
|
5748
|
+
}
|
|
5749
|
+
function inferArbitraryKind(value) {
|
|
5750
|
+
if (IMAGE_FUNCTIONS.test(value)) return "image";
|
|
5751
|
+
if (COLOR_FUNCTIONS.test(value)) return "color";
|
|
5752
|
+
if (LENGTH_VALUE.test(value) || MATH_VALUE.test(value)) return "length";
|
|
5753
|
+
if (/^-?(?:\d*\.)?\d+$/.test(value)) return "number";
|
|
5754
|
+
return null;
|
|
5755
|
+
}
|
|
5756
|
+
function resolveArbitraryDeclaration(prefix, rawValue) {
|
|
5757
|
+
const spec = ARBITRARY_KINDS[prefix];
|
|
5758
|
+
if (!spec) return null;
|
|
5759
|
+
const hinted = rawValue.match(/^([a-z][a-z-]*):([\s\S]+)$/i);
|
|
5760
|
+
const hintKind = hinted ? HINT_KIND[hinted[1].toLowerCase()] : void 0;
|
|
5761
|
+
const value = hintKind ? hinted[2].trim() : rawValue;
|
|
5762
|
+
const kind = hintKind ?? inferArbitraryKind(value);
|
|
5763
|
+
const property = kind ? spec.props[kind] : void 0;
|
|
5764
|
+
return property ? { property, value, kind } : { property: spec.fallback, value, kind: spec.fallbackKind };
|
|
5765
|
+
}
|
|
4444
5766
|
function animateModifier(base) {
|
|
4445
5767
|
const spin = {
|
|
4446
5768
|
"animate-spin": "animation:spin 1s linear infinite",
|
|
@@ -4506,13 +5828,23 @@ function translateBase(base) {
|
|
|
4506
5828
|
if (arb) {
|
|
4507
5829
|
const val = decodeArbitrary(arb[2]);
|
|
4508
5830
|
if (val) {
|
|
5831
|
+
const withAlpha = (v) => arb[3] ? `color-mix(in oklab, ${v} ${arb[3]}%, transparent)` : v;
|
|
5832
|
+
const resolved = resolveArbitraryDeclaration(arb[1], val);
|
|
5833
|
+
if (resolved?.property === "--tw-ring-width") return ringWidthRule(resolved.value);
|
|
5834
|
+
if (resolved) {
|
|
5835
|
+
const value = resolved.kind === "color" ? withAlpha(resolved.value) : resolved.value;
|
|
5836
|
+
return `${resolved.property}:${value}`;
|
|
5837
|
+
}
|
|
5838
|
+
const composed = ARBITRARY_COMPOSED[arb[1]];
|
|
5839
|
+
if (composed) return composed(val);
|
|
5840
|
+
const multi = ARBITRARY_PROPS_MULTI[arb[1]];
|
|
5841
|
+
if (multi) return multi.map((p) => `${p}:${val}`).join(";");
|
|
4509
5842
|
const prop = ARBITRARY_PROP[arb[1]];
|
|
4510
5843
|
if (prop) return `${prop}:${val}`;
|
|
4511
5844
|
const colorProp = COLOR_PROP[arb[1]];
|
|
4512
|
-
if (colorProp) {
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
}
|
|
5845
|
+
if (colorProp) return `${colorProp}:${withAlpha(val)}`;
|
|
5846
|
+
const negated = arb[1].startsWith("-") ? ARBITRARY_PROP[arb[1].slice(1)] : void 0;
|
|
5847
|
+
if (negated) return `${negated}:calc(${val} * -1)`;
|
|
4516
5848
|
}
|
|
4517
5849
|
}
|
|
4518
5850
|
const rside = base.match(/^rounded-(t|r|b|l|tl|tr|br|bl)(?:-(.+))?$/);
|
|
@@ -4614,7 +5946,7 @@ function splitVariants(cls) {
|
|
|
4614
5946
|
out.push(cur);
|
|
4615
5947
|
return out;
|
|
4616
5948
|
}
|
|
4617
|
-
var BREAKPOINTS, KNOWN_EMPTY, PALETTE, FONT_FAMILY, FLEX_EXTRA, TW_DEFAULTS, ESCAPE_RE, STATE_VARIANTS, GROUP_PEER, PSEUDO_ELEMENTS, SPACING_PROP, SIZE_PROP, COLOR_PROP, SEMANTIC_TOKENS, RADIUS, RADIUS_SIDE, FONT_SIZE, FONT_WEIGHT, STATIC, ARBITRARY_PROP, MAX_W_SCALE;
|
|
5949
|
+
var BREAKPOINTS, KNOWN_EMPTY, PALETTE, FONT_FAMILY, FLEX_EXTRA, TW_DEFAULTS, ESCAPE_RE, STATE_VARIANTS, GROUP_PEER, PSEUDO_ELEMENTS, SPACING_PROP, SIZE_PROP, COLOR_PROP, SEMANTIC_TOKENS, RADIUS, RADIUS_SIDE, FONT_SIZE, FONT_WEIGHT, STATIC, ARBITRARY_PROP, ARBITRARY_PROPS_MULTI, FILTER_CHAIN, BACKDROP_CHAIN, RING_BOX_SHADOW, TRANSITION_TAIL, ARBITRARY_COMPOSED, HINT_KIND, ARBITRARY_KINDS, COLOR_FUNCTIONS, IMAGE_FUNCTIONS, LENGTH_VALUE, MATH_VALUE, MAX_W_SCALE;
|
|
4618
5950
|
var init_tw_translate = __esm({
|
|
4619
5951
|
"../shared/src/mcp/tw-translate.ts"() {
|
|
4620
5952
|
"use strict";
|
|
@@ -5042,15 +6374,12 @@ var init_tw_translate = __esm({
|
|
|
5042
6374
|
"rounded-xs": "border-radius:var(--radius-xs, 0.125rem)"
|
|
5043
6375
|
};
|
|
5044
6376
|
ARBITRARY_PROP = {
|
|
5045
|
-
bg: "background",
|
|
5046
|
-
text: "color",
|
|
5047
6377
|
w: "width",
|
|
5048
6378
|
h: "height",
|
|
5049
6379
|
"min-w": "min-width",
|
|
5050
6380
|
"max-w": "max-width",
|
|
5051
6381
|
"min-h": "min-height",
|
|
5052
6382
|
"max-h": "max-height",
|
|
5053
|
-
size: "width",
|
|
5054
6383
|
p: "padding",
|
|
5055
6384
|
px: "padding-inline",
|
|
5056
6385
|
py: "padding-block",
|
|
@@ -5067,8 +6396,6 @@ var init_tw_translate = __esm({
|
|
|
5067
6396
|
ml: "margin-left",
|
|
5068
6397
|
gap: "gap",
|
|
5069
6398
|
rounded: "border-radius",
|
|
5070
|
-
shadow: "box-shadow",
|
|
5071
|
-
border: "border-width",
|
|
5072
6399
|
top: "top",
|
|
5073
6400
|
left: "left",
|
|
5074
6401
|
right: "right",
|
|
@@ -5079,14 +6406,93 @@ var init_tw_translate = __esm({
|
|
|
5079
6406
|
opacity: "opacity",
|
|
5080
6407
|
z: "z-index",
|
|
5081
6408
|
grid: "grid-template-columns",
|
|
5082
|
-
"font": "font-size",
|
|
5083
6409
|
"aspect": "aspect-ratio",
|
|
5084
|
-
// transition props (transition-[background-color,box-shadow]) + timing
|
|
5085
|
-
transition: "transition-property",
|
|
5086
6410
|
duration: "transition-duration",
|
|
5087
6411
|
delay: "transition-delay",
|
|
5088
6412
|
ease: "transition-timing-function"
|
|
5089
6413
|
};
|
|
6414
|
+
ARBITRARY_PROPS_MULTI = {
|
|
6415
|
+
size: ["width", "height"]
|
|
6416
|
+
};
|
|
6417
|
+
FILTER_CHAIN = "var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)";
|
|
6418
|
+
BACKDROP_CHAIN = "var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)";
|
|
6419
|
+
RING_BOX_SHADOW = "var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)";
|
|
6420
|
+
TRANSITION_TAIL = "transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms";
|
|
6421
|
+
ARBITRARY_COMPOSED = {
|
|
6422
|
+
blur: (v) => `--tw-blur:blur(${v});filter:${FILTER_CHAIN}`,
|
|
6423
|
+
"backdrop-blur": (v) => `--tw-backdrop-blur:blur(${v});-webkit-backdrop-filter:${BACKDROP_CHAIN};backdrop-filter:${BACKDROP_CHAIN}`,
|
|
6424
|
+
"drop-shadow": (v) => `--tw-drop-shadow:drop-shadow(${v});filter:${FILTER_CHAIN}`,
|
|
6425
|
+
scale: (v) => `--tw-scale-x:${v};--tw-scale-y:${v};--tw-scale-z:${v};scale:var(--tw-scale-x) var(--tw-scale-y)`,
|
|
6426
|
+
origin: (v) => `transform-origin:${v}`,
|
|
6427
|
+
"underline-offset": (v) => `text-underline-offset:${v}`,
|
|
6428
|
+
// Tailwind's `transition-[a,b]` still applies the default duration and easing;
|
|
6429
|
+
// emitting only transition-property leaves every transition at 0s.
|
|
6430
|
+
transition: (v) => `transition-property:${v};${TRANSITION_TAIL}`
|
|
6431
|
+
};
|
|
6432
|
+
HINT_KIND = {
|
|
6433
|
+
color: "color",
|
|
6434
|
+
image: "image",
|
|
6435
|
+
url: "image",
|
|
6436
|
+
length: "length",
|
|
6437
|
+
"line-width": "length",
|
|
6438
|
+
percentage: "length",
|
|
6439
|
+
size: "length",
|
|
6440
|
+
"bg-size": "length",
|
|
6441
|
+
"absolute-size": "length",
|
|
6442
|
+
"relative-size": "length",
|
|
6443
|
+
"family-name": "family",
|
|
6444
|
+
"generic-name": "family",
|
|
6445
|
+
number: "number",
|
|
6446
|
+
integer: "number",
|
|
6447
|
+
ratio: "number",
|
|
6448
|
+
angle: "number",
|
|
6449
|
+
position: "position"
|
|
6450
|
+
};
|
|
6451
|
+
ARBITRARY_KINDS = {
|
|
6452
|
+
bg: {
|
|
6453
|
+
fallback: "background-color",
|
|
6454
|
+
fallbackKind: "color",
|
|
6455
|
+
props: {
|
|
6456
|
+
color: "background-color",
|
|
6457
|
+
image: "background-image",
|
|
6458
|
+
length: "background-size",
|
|
6459
|
+
position: "background-position"
|
|
6460
|
+
}
|
|
6461
|
+
},
|
|
6462
|
+
text: { fallback: "color", fallbackKind: "color", props: { color: "color", length: "font-size" } },
|
|
6463
|
+
font: {
|
|
6464
|
+
fallback: "font-weight",
|
|
6465
|
+
fallbackKind: "number",
|
|
6466
|
+
props: { family: "font-family", number: "font-weight", length: "font-size" }
|
|
6467
|
+
},
|
|
6468
|
+
border: { fallback: "border-color", fallbackKind: "color", props: { color: "border-color", length: "border-width" } },
|
|
6469
|
+
"border-x": { fallback: "border-inline-color", fallbackKind: "color", props: { color: "border-inline-color", length: "border-inline-width" } },
|
|
6470
|
+
"border-y": { fallback: "border-block-color", fallbackKind: "color", props: { color: "border-block-color", length: "border-block-width" } },
|
|
6471
|
+
"border-t": { fallback: "border-top-color", fallbackKind: "color", props: { color: "border-top-color", length: "border-top-width" } },
|
|
6472
|
+
"border-r": { fallback: "border-right-color", fallbackKind: "color", props: { color: "border-right-color", length: "border-right-width" } },
|
|
6473
|
+
"border-b": { fallback: "border-bottom-color", fallbackKind: "color", props: { color: "border-bottom-color", length: "border-bottom-width" } },
|
|
6474
|
+
"border-l": { fallback: "border-left-color", fallbackKind: "color", props: { color: "border-left-color", length: "border-left-width" } },
|
|
6475
|
+
divide: { fallback: "border-color", fallbackKind: "color", props: { color: "border-color", length: "border-width" } },
|
|
6476
|
+
outline: { fallback: "outline-color", fallbackKind: "color", props: { color: "outline-color", length: "outline-width" } },
|
|
6477
|
+
// `--tw-ring-width` is not a real Tailwind variable: it marks the ring-WIDTH
|
|
6478
|
+
// branch so the caller can emit the composed ring box-shadow instead of a
|
|
6479
|
+
// declaration. `ring-[<color>]` still sets the ring color.
|
|
6480
|
+
ring: { fallback: "--tw-ring-color", fallbackKind: "color", props: { color: "--tw-ring-color", length: "--tw-ring-width" } },
|
|
6481
|
+
// The kit has no Tailwind shadow-var machinery, so a shadow value goes straight
|
|
6482
|
+
// onto box-shadow; only the color hint targets the tint var.
|
|
6483
|
+
shadow: { fallback: "box-shadow", fallbackKind: "length", props: { color: "--tw-shadow-color" } },
|
|
6484
|
+
stroke: { fallback: "stroke", fallbackKind: "color", props: { color: "stroke", length: "stroke-width", number: "stroke-width" } },
|
|
6485
|
+
fill: { fallback: "fill", fallbackKind: "color", props: { color: "fill" } },
|
|
6486
|
+
decoration: {
|
|
6487
|
+
fallback: "text-decoration-color",
|
|
6488
|
+
fallbackKind: "color",
|
|
6489
|
+
props: { color: "text-decoration-color", length: "text-decoration-thickness" }
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
COLOR_FUNCTIONS = /^(?:#[0-9a-f]{3,8}$|(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix)\()/i;
|
|
6493
|
+
IMAGE_FUNCTIONS = /^(?:url\(|(?:repeating-)?(?:linear|radial|conic)-gradient\(|image-set\(|cross-fade\()/i;
|
|
6494
|
+
LENGTH_VALUE = /^-?(?:\d*\.)?\d+(?:px|rem|em|%|vh|vw|vmin|vmax|svh|lvh|dvh|svw|lvw|dvw|ch|ex|cap|ic|lh|pt|pc|cm|mm|q|in)$/i;
|
|
6495
|
+
MATH_VALUE = /^(?:calc|clamp|min|max)\(/i;
|
|
5090
6496
|
MAX_W_SCALE = {
|
|
5091
6497
|
xs: "20rem",
|
|
5092
6498
|
sm: "24rem",
|
|
@@ -5163,47 +6569,73 @@ function decodeArbitrary2(raw) {
|
|
|
5163
6569
|
const value = raw.replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
5164
6570
|
return value && !/[<>{};]/.test(value) ? value : null;
|
|
5165
6571
|
}
|
|
5166
|
-
function
|
|
6572
|
+
function restingDeclaration(scope, cssProp, utilPrefixes) {
|
|
5167
6573
|
for (const prefix of utilPrefixes) {
|
|
5168
6574
|
const m = scope.match(new RegExp(`(?<![\\w:.\\-])${prefix}-\\[([^\\]]+)\\]`));
|
|
5169
6575
|
if (m) {
|
|
5170
6576
|
const v = decodeArbitrary2(m[1]);
|
|
5171
|
-
if (v) return v;
|
|
6577
|
+
if (v) return resolveArbitraryDeclaration(prefix, v) ?? { property: cssProp, value: v };
|
|
5172
6578
|
}
|
|
5173
6579
|
}
|
|
5174
6580
|
const pm = scope.match(new RegExp(`(?<!:)\\[${cssProp}:([^\\]]+)\\]`));
|
|
5175
|
-
|
|
6581
|
+
const decoded = pm ? decodeArbitrary2(pm[1]) : null;
|
|
6582
|
+
return decoded ? { property: cssProp, value: decoded } : null;
|
|
6583
|
+
}
|
|
6584
|
+
function restingValue(scope, cssProp, utilPrefixes) {
|
|
6585
|
+
const declaration = restingDeclaration(scope, cssProp, utilPrefixes);
|
|
6586
|
+
return declaration?.property === cssProp ? declaration.value : null;
|
|
5176
6587
|
}
|
|
5177
6588
|
function variantBody(content, name) {
|
|
5178
6589
|
const vi = content.search(/variant\s*:\s*\{/);
|
|
5179
6590
|
const hay = vi !== -1 ? content.slice(vi) : content;
|
|
6591
|
+
const key = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5180
6592
|
const m = hay.match(
|
|
5181
|
-
new RegExp(
|
|
6593
|
+
new RegExp(
|
|
6594
|
+
`[\\s{,](?:${key}|["']${key}["'])\\s*:\\s*([\\s\\S]*?)(?:\\n\\s*(?:[A-Za-z_][\\w-]*|["'][^"']+["'])\\s*:|\\n\\s*\\},)`
|
|
6595
|
+
)
|
|
5182
6596
|
);
|
|
5183
6597
|
return m ? m[1] : null;
|
|
5184
6598
|
}
|
|
6599
|
+
function declaredDefaultVariantBody(content) {
|
|
6600
|
+
const defaults = content.match(/defaultVariants\s*:\s*\{([\s\S]*?)\}/)?.[1];
|
|
6601
|
+
const declaredDefault = defaults?.match(/\bvariant\s*:\s*["']([^"']+)["']/)?.[1];
|
|
6602
|
+
return (declaredDefault ? variantBody(content, declaredDefault) : null) ?? variantBody(content, "default");
|
|
6603
|
+
}
|
|
6604
|
+
function canonicalButtonVariantBody(content) {
|
|
6605
|
+
return declaredDefaultVariantBody(content) ?? variantBody(content, "primary");
|
|
6606
|
+
}
|
|
5185
6607
|
function componentDepthCss(files) {
|
|
5186
6608
|
const btn = files.find((f) => f.path && /(^|\/)button\.(tsx|jsx)$/.test(f.path));
|
|
5187
6609
|
if (!btn?.content) return "";
|
|
5188
6610
|
const content = btn.content;
|
|
5189
|
-
const scope =
|
|
6611
|
+
const scope = canonicalButtonVariantBody(content) ?? content;
|
|
5190
6612
|
const shadow = /(?:^|\s|"|')shadow-none(?=\s|"|'|$)/.test(scope) ? "none" : restingValue(scope, "box-shadow", ["shadow"]);
|
|
5191
|
-
const background =
|
|
6613
|
+
const background = restingDeclaration(scope, "background", ["bg"]) ?? restingDeclaration(scope, "background-image", []);
|
|
5192
6614
|
const sel = ".bd-btn-primary, .bd-view, .ds-btn-primary";
|
|
5193
6615
|
const rules = [];
|
|
5194
6616
|
if (shadow) rules.push(`${sel} { box-shadow: ${shadow}; }`);
|
|
5195
|
-
if (background) rules.push(`${sel} { background: ${background}; }`);
|
|
6617
|
+
if (background) rules.push(`${sel} { ${background.property}: ${background.value}; }`);
|
|
5196
6618
|
return rules.join("\n");
|
|
5197
6619
|
}
|
|
6620
|
+
function arbitrary(scope, re) {
|
|
6621
|
+
const m = scope.match(re);
|
|
6622
|
+
if (!m) return null;
|
|
6623
|
+
const value = m[1].replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
6624
|
+
return value && !/[<>{};]/.test(value) ? value : null;
|
|
6625
|
+
}
|
|
6626
|
+
function checkedTrackBackground(source) {
|
|
6627
|
+
const utility = arbitrary(source, /data-\[state=checked\]:bg-\[([^\]]+)\]/);
|
|
6628
|
+
if (utility) {
|
|
6629
|
+
return resolveArbitraryDeclaration("bg", utility) ?? { property: "background", value: utility };
|
|
6630
|
+
}
|
|
6631
|
+
const image = arbitrary(source, /data-\[state=checked\]:\[background-image:([^\]]+)\]/);
|
|
6632
|
+
if (image) return { property: "background-image", value: image };
|
|
6633
|
+
const shorthand = arbitrary(source, /data-\[state=checked\]:\[background:([^\]]+)\]/);
|
|
6634
|
+
return shorthand ? { property: "background", value: shorthand } : null;
|
|
6635
|
+
}
|
|
5198
6636
|
function buildDsComponentCss(files) {
|
|
5199
6637
|
const contentOf = (re) => stripComments2(files.find((f) => f.path && re.test(f.path))?.content ?? "");
|
|
5200
6638
|
const has = (name) => Boolean(contentOf(new RegExp(`(^|/)${name}\\.(tsx|jsx)$`)));
|
|
5201
|
-
const arbitrary = (scope, re) => {
|
|
5202
|
-
const m = scope.match(re);
|
|
5203
|
-
if (!m) return null;
|
|
5204
|
-
const value = m[1].replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
5205
|
-
return value && !/[<>{};]/.test(value) ? value : null;
|
|
5206
|
-
};
|
|
5207
6639
|
const rules = [componentRadiusCss(files), componentDepthCss(files)];
|
|
5208
6640
|
const covered = /* @__PURE__ */ new Set();
|
|
5209
6641
|
for (const name of ["button", "input", "alert", "avatar", "table", "tabs", "progress"]) {
|
|
@@ -5218,9 +6650,9 @@ function buildDsComponentCss(files) {
|
|
|
5218
6650
|
const sw = contentOf(/(^|\/)switch\.(tsx|jsx)$/);
|
|
5219
6651
|
if (sw) {
|
|
5220
6652
|
covered.add("switch");
|
|
5221
|
-
const bg =
|
|
6653
|
+
const bg = checkedTrackBackground(sw);
|
|
5222
6654
|
const shadow = arbitrary(sw, /data-\[state=checked\]:shadow-\[([^\]]+)\]/) ?? arbitrary(sw, /data-\[state=checked\]:\[box-shadow:([^\]]+)\]/);
|
|
5223
|
-
if (bg) rules.push(`.ds-switch-track.on {
|
|
6655
|
+
if (bg) rules.push(`.ds-switch-track.on { ${bg.property}: ${bg.value}; }`);
|
|
5224
6656
|
if (shadow) rules.push(`.ds-switch-track.on { box-shadow: ${shadow}; }`);
|
|
5225
6657
|
}
|
|
5226
6658
|
const badge = contentOf(/(^|\/)badge\.(tsx|jsx)$/);
|
|
@@ -5306,10 +6738,11 @@ function translatedKitComponentCss(contentOf, covered) {
|
|
|
5306
6738
|
const button = contentOf(/(^|\/)button\.(tsx|jsx)$/);
|
|
5307
6739
|
if (button) {
|
|
5308
6740
|
emit(".ds-btn", cvaBase(button), "button");
|
|
5309
|
-
|
|
6741
|
+
const canonical = canonicalButtonVariantBody(button);
|
|
6742
|
+
emit(".ds .ds-btn-primary", canonical ? quotedStrings(canonical) : null);
|
|
5310
6743
|
for (const v of ["secondary", "outline", "ghost"]) {
|
|
5311
6744
|
const body = variantBody(button, v);
|
|
5312
|
-
if (body) emit(`.ds-btn-${v}`, quotedStrings(body));
|
|
6745
|
+
if (body) emit(`.ds .ds-btn-${v}`, quotedStrings(body));
|
|
5313
6746
|
}
|
|
5314
6747
|
}
|
|
5315
6748
|
const shared = contentOf(/(^|\/)_shared\.(ts|tsx)$/);
|
|
@@ -5454,15 +6887,13 @@ ${PREVIEW_BASE_CSS}
|
|
|
5454
6887
|
.bd-app { padding: 16px; }
|
|
5455
6888
|
.bd-cta-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 16px; background: var(--card, var(--background, oklch(1 0 0))); border-top: 1px solid var(--border, oklch(0.92 0 0)); }
|
|
5456
6889
|
.bd-cta-name { font-weight: 600; font-size: 14px; color: var(--foreground, oklch(0.2 0 0)); font-family: var(--font-serif, var(--font-sans, ui-sans-serif)); }
|
|
5457
|
-
.bd-cta { font: inherit; font-size: 14px; font-weight: 700; text-decoration: none; color: var(--primary, oklch(0.45 0 0)); display: inline-flex; align-items: center; gap: 6px; }
|
|
5458
|
-
.bd-cta:hover { opacity: 0.7; }
|
|
6890
|
+
.bd-cta { min-height: 44px; padding-inline: 4px; font: inherit; font-size: 14px; font-weight: 700; text-decoration: none; color: var(--primary, oklch(0.45 0 0)); display: inline-flex; align-items: center; gap: 6px; }
|
|
5459
6891
|
.bd-cta-actions { display: inline-flex; align-items: center; gap: 14px; }
|
|
5460
|
-
.bd-expand { font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; background: none; border: 0; padding: 0; color: var(--muted-foreground, oklch(0.5 0 0)); display: inline-flex; align-items: center; gap: 5px; }
|
|
5461
|
-
.bd-expand:
|
|
6892
|
+
.bd-expand { min-height: 44px; font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; background: none; border: 0; padding: 0 4px; color: var(--muted-foreground, oklch(0.5 0 0)); display: inline-flex; align-items: center; gap: 5px; }
|
|
6893
|
+
.bd-cta:focus-visible, .bd-expand:focus-visible, .bd-nav-item:focus-visible { outline: 2px solid var(--ring, var(--primary, oklch(0.55 0.15 264))); outline-offset: 2px; }
|
|
5462
6894
|
.bd-progress { padding: 6px 16px 12px; }
|
|
5463
|
-
/* Fullscreen: the DS preview rendered
|
|
5464
|
-
|
|
5465
|
-
ext-apps model is in-widget rendering. requestDisplayMode just grants room. */
|
|
6895
|
+
/* Fullscreen: the DS preview rendered in the widget, full-bleed. The host's
|
|
6896
|
+
requestDisplayMode capability only grants more room. */
|
|
5466
6897
|
.bd-fs { position: fixed; inset: 0; display: flex; flex-direction: column; background: var(--background, oklch(1 0 0)); }
|
|
5467
6898
|
.bd-fs-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--border, oklch(0.92 0 0)); background: var(--card, var(--background, oklch(1 0 0))); flex: none; }
|
|
5468
6899
|
.bd-fs-body { flex: 1; overflow-y: auto; padding: 20px; max-width: 860px; width: 100%; margin: 0 auto; }
|
|
@@ -5509,14 +6940,14 @@ ${DS_KIT_CSS}
|
|
|
5509
6940
|
.bd-nav-card, .bd-pal-card { border: 1px solid var(--border, oklch(0.9 0 0)); border-radius: 8px; background: var(--card, var(--background, oklch(1 0 0))); overflow: clip; }
|
|
5510
6941
|
.bd-nav-h, .bd-pal-h { padding: 10px 12px; border-bottom: 1px solid var(--border, oklch(0.9 0 0)); font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted-foreground, oklch(0.5 0 0)); opacity: 0.6; }
|
|
5511
6942
|
.bd-nav-list { display: flex; flex-direction: column; gap: 1px; padding: 6px 6px 8px; }
|
|
5512
|
-
.bd-nav-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding:
|
|
5513
|
-
.bd-nav-item
|
|
6943
|
+
.bd-nav-item { min-height: 44px; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 8px; font: inherit; font-size: 12px; font-weight: 400; color: var(--muted-foreground, oklch(0.5 0 0)); background: transparent; border-radius: 5px; border: none; cursor: pointer; line-height: 1.4; }
|
|
6944
|
+
.bd-nav-item.active { font-weight: 500; color: var(--foreground, oklch(0.2 0 0)); background: var(--muted, oklch(0.95 0 0)); }
|
|
5514
6945
|
.bd-nav-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; background: var(--border, oklch(0.85 0 0)); }
|
|
5515
6946
|
.bd-nav-item.active .bd-nav-dot { background: var(--primary, oklch(0.45 0.15 264)); }
|
|
5516
6947
|
/* SectionHeader (component-showcase.tsx:1659-1680). */
|
|
5517
6948
|
.bd-sh { margin-bottom: 20px; margin-top: 64px; padding-top: 48px; border-top: 1px solid var(--border, oklch(0.9 0 0)); }
|
|
5518
6949
|
.bd-sh.first { margin-top: 28px; padding-top: 0; border-top: none; }
|
|
5519
|
-
.bd-sh-t { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-foreground, oklch(0.5 0 0)); }
|
|
6950
|
+
.bd-sh-t { margin: 0; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted-foreground, oklch(0.5 0 0)); }
|
|
5520
6951
|
/* Grid (component-showcase.tsx:1565-1571 \u2014 cols-1 / sm:2 / xl:3, gap-3 = 12px).
|
|
5521
6952
|
Own class (not the vignette's .bd-grid) + explicit responsive columns capped
|
|
5522
6953
|
at 3, so the fullscreen main never renders a 4th column. [BD-117] */
|
|
@@ -5536,26 +6967,18 @@ ${DS_KIT_CSS}
|
|
|
5536
6967
|
.bd-show-scroll { max-height: 420px; overflow: hidden; position: relative; -webkit-mask-image: linear-gradient(to bottom, #000 86%, transparent); mask-image: linear-gradient(to bottom, #000 86%, transparent); }
|
|
5537
6968
|
.bd-fs .bd-show-scroll { max-height: none; overflow: visible; -webkit-mask-image: none; mask-image: none; }
|
|
5538
6969
|
.bd-fs .bd-main { max-width: 920px; }
|
|
5539
|
-
/* Real-page
|
|
5540
|
-
full viewport + a Back bar. The iframe shows the actual /design-systems/<id>
|
|
5541
|
-
page (?embed=1, no app nav), so the components are real, not approximated. */
|
|
5542
|
-
.bd-frame-wrap { display: flex; flex-direction: column; background: var(--background, oklch(1 0 0)); }
|
|
5543
|
-
.bd-frame { width: 100%; border: 0; display: block; background: var(--background, oklch(1 0 0)); height: 460px; }
|
|
5544
|
-
.bd-frame-wrap.bd-fs { position: fixed; inset: 0; height: 100vh; }
|
|
5545
|
-
.bd-frame-wrap.bd-fs .bd-frame { flex: 1; height: auto; }
|
|
5546
|
-
/* Real-page screenshot (claude.ai, which can't iframe). Inline = capped + fade;
|
|
5547
|
-
fullscreen = scrollable. [BD-136] */
|
|
6970
|
+
/* Real-page screenshot. Inline = capped + fade; fullscreen = scrollable. */
|
|
5548
6971
|
.bd-shot-wrap { min-height: 240px; max-height: 540px; overflow: hidden; position: relative; background: var(--card, var(--background, oklch(0.97 0 0))); -webkit-mask-image: linear-gradient(to bottom, #000 90%, transparent); mask-image: linear-gradient(to bottom, #000 90%, transparent); }
|
|
5549
|
-
.bd-shot-wrap
|
|
6972
|
+
.bd-fs > .bd-shot-wrap { min-height: 0; max-height: none; flex: 1; overflow: auto; -webkit-mask-image: none; mask-image: none; }
|
|
5550
6973
|
.bd-shot { display: block; width: 100%; height: auto; }
|
|
5551
|
-
.bd-
|
|
5552
|
-
.bd-cta-name { font-weight: 600; font-size: 13px; color: var(--foreground, oklch(0.2 0 0)); }
|
|
6974
|
+
.bd-preview-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 9px 14px; border-top: 1px solid var(--border, oklch(0.92 0 0)); background: var(--card, var(--background, oklch(1 0 0))); }
|
|
6975
|
+
.bd-cta-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-size: 13px; color: var(--foreground, oklch(0.2 0 0)); }
|
|
5553
6976
|
.bd-cta-actions { display: inline-flex; align-items: center; gap: 14px; }
|
|
5554
|
-
|
|
5555
|
-
.bd-
|
|
5556
|
-
.bd-
|
|
5557
|
-
.bd-
|
|
5558
|
-
|
|
6977
|
+
@media (hover: hover) and (pointer: fine) {
|
|
6978
|
+
.bd-cta:hover { opacity: 0.7; }
|
|
6979
|
+
.bd-expand:hover { color: var(--foreground, oklch(0.2 0 0)); }
|
|
6980
|
+
.bd-nav-item:hover { font-weight: 500; color: var(--foreground, oklch(0.2 0 0)); background: var(--muted, oklch(0.95 0 0)); }
|
|
6981
|
+
}
|
|
5559
6982
|
</style>
|
|
5560
6983
|
<style id="bd-tokens"></style>
|
|
5561
6984
|
</head>
|
|
@@ -5577,7 +7000,7 @@ ${DS_KIT_CSS}
|
|
|
5577
7000
|
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5578
7001
|
}
|
|
5579
7002
|
|
|
5580
|
-
// Only http(s) URLs are safe to use as
|
|
7003
|
+
// Only http(s) URLs are safe to use as image or link targets; reject
|
|
5581
7004
|
// javascript:/data:/etc. so a hostile MCP payload can't inject a scheme that
|
|
5582
7005
|
// executes in the widget sandbox. Returns "" for anything else.
|
|
5583
7006
|
function safeUrl(u) {
|
|
@@ -5626,12 +7049,12 @@ ${DS_KIT_CSS}
|
|
|
5626
7049
|
return showcaseSections().map(function (s, i) {
|
|
5627
7050
|
var cards = s[2].map(function (it) {
|
|
5628
7051
|
return '<div class="bd-dc" data-name="' + esc(it[0].toLowerCase()) + '">' +
|
|
5629
|
-
'<div class="bd-dc-demo">' + it[1] + '</div>' +
|
|
7052
|
+
'<div class="bd-dc-demo" inert aria-hidden="true">' + it[1] + '</div>' +
|
|
5630
7053
|
'<div class="bd-dc-foot">' + esc(it[0]) + '</div>' +
|
|
5631
7054
|
'</div>';
|
|
5632
7055
|
}).join("");
|
|
5633
7056
|
return '<section class="bd-sec" data-sec="' + esc(s[0]) + '">' +
|
|
5634
|
-
'<div class="bd-sh' + (i === 0 ? " first" : "") + '"><
|
|
7057
|
+
'<div class="bd-sh' + (i === 0 ? " first" : "") + '"><h2 class="bd-sh-t">' + esc(s[1]) + '</h2></div>' +
|
|
5635
7058
|
'<div class="bd-show-grid">' + cards + '</div>' +
|
|
5636
7059
|
'</section>';
|
|
5637
7060
|
}).join("");
|
|
@@ -5650,7 +7073,7 @@ ${DS_KIT_CSS}
|
|
|
5650
7073
|
// for each semantic color token. Fullscreen only (CSS). [BD-117]
|
|
5651
7074
|
function paletteHtml() {
|
|
5652
7075
|
var TOK = [["Background", "background"], ["Foreground", "foreground"], ["Primary", "primary"], ["Secondary", "secondary"], ["Accent", "accent"], ["Muted", "muted"], ["Card", "card"], ["Border", "border"], ["Destructive", "destructive"], ["Ring", "ring"]];
|
|
5653
|
-
return '<aside class="bd-pal"><div class="bd-pal-card"><div class="bd-pal-h">
|
|
7076
|
+
return '<aside class="bd-pal"><div class="bd-pal-card"><div class="bd-pal-h">Colors</div><div class="bd-pal-list">' +
|
|
5654
7077
|
TOK.map(function (t) {
|
|
5655
7078
|
return '<div class="bd-tok"><span class="bd-tok-sw" style="background: var(--' + t[1] + ', transparent)"></span>' + esc(t[0]) + '</div>';
|
|
5656
7079
|
}).join("") + '</div></div></aside>';
|
|
@@ -5658,7 +7081,7 @@ ${DS_KIT_CSS}
|
|
|
5658
7081
|
|
|
5659
7082
|
// The full showcase. Mirrors the real page: a header (h1 + description), then a
|
|
5660
7083
|
// 3-column layout (left nav + main sections + right palette). Same render on
|
|
5661
|
-
//
|
|
7084
|
+
// every host. Inline caps the height + hides asides via CSS exactly
|
|
5662
7085
|
// like the real page below lg/xl; fullscreen shows everything. [BD-117]
|
|
5663
7086
|
function showcaseBody(out, full) {
|
|
5664
7087
|
var nm = esc(out.name || "Design system");
|
|
@@ -5716,7 +7139,7 @@ ${DS_KIT_CSS}
|
|
|
5716
7139
|
de.removeProperty("--font-serif");
|
|
5717
7140
|
de.removeProperty("--font-mono");
|
|
5718
7141
|
} else if (hostFontsPayload && applyHostFontsFn) {
|
|
5719
|
-
try { applyHostFontsFn(hostFontsPayload); } catch (e) {}
|
|
7142
|
+
try { applyHostFontsFn(hostFontsPayload); } catch (e) { /* best-effort: host font apply; fallback fonts stand */ }
|
|
5720
7143
|
}
|
|
5721
7144
|
}
|
|
5722
7145
|
|
|
@@ -5727,15 +7150,15 @@ ${DS_KIT_CSS}
|
|
|
5727
7150
|
// mode field = declined); only switch to what it actually granted.
|
|
5728
7151
|
var apply = function (m) { displayMode = m || displayMode; if (lastOut) render(lastOut); };
|
|
5729
7152
|
try {
|
|
5730
|
-
if (bdApp && bdApp.requestDisplayMode) {
|
|
5731
|
-
bdApp.requestDisplayMode({ mode: mode }).then(function (r) { apply(r && r.mode); }, function () {});
|
|
5732
|
-
return;
|
|
5733
|
-
}
|
|
5734
7153
|
if (window.openai && window.openai.requestDisplayMode) {
|
|
5735
7154
|
Promise.resolve(window.openai.requestDisplayMode({ mode: mode })).then(function (r) { apply(r && r.mode); }, function () {});
|
|
5736
7155
|
return;
|
|
5737
7156
|
}
|
|
5738
|
-
|
|
7157
|
+
if (bdApp && bdApp.requestDisplayMode) {
|
|
7158
|
+
bdApp.requestDisplayMode({ mode: mode }).then(function (r) { apply(r && r.mode); }, function () {});
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
} catch (e) { /* best-effort: bridge threw; apply the mode locally below */ }
|
|
5739
7162
|
apply(mode);
|
|
5740
7163
|
}
|
|
5741
7164
|
|
|
@@ -5766,91 +7189,32 @@ ${DS_KIT_CSS}
|
|
|
5766
7189
|
return !!(modes && modes.indexOf && modes.indexOf("fullscreen") !== -1);
|
|
5767
7190
|
}
|
|
5768
7191
|
|
|
5769
|
-
//
|
|
5770
|
-
|
|
5771
|
-
// -mcp#40), so it can never iframe \u2014 there we link out instead. [BD-117]
|
|
5772
|
-
//
|
|
5773
|
-
// Identify ChatGPT by window.openai ALONE. Do NOT also test !bdApp: bdApp is
|
|
5774
|
-
// our OWN inlined MCP-Apps client (window.__bdMcp), instantiated
|
|
5775
|
-
// unconditionally on every host, so it is always set after first paint. The
|
|
5776
|
-
// old check (window.openai AND not bdApp) therefore flipped to false on every
|
|
5777
|
-
// re-render (Fullscreen, set_globals), wrongly showing the claude.ai link card
|
|
5778
|
-
// on ChatGPT. window.openai is absent on claude.ai, so it is the right signal.
|
|
5779
|
-
function canIframe() {
|
|
5780
|
-
return !!window.openai;
|
|
5781
|
-
}
|
|
5782
|
-
|
|
5783
|
-
// The real page, with ?embed=1 so it renders the showcase WITHOUT the app nav
|
|
5784
|
-
// (clean component showcase to embed). [BD-117]
|
|
5785
|
-
function embedUrl(view) {
|
|
5786
|
-
if (!view) return "";
|
|
5787
|
-
return view + (view.indexOf("?") === -1 ? "?embed=1" : "&embed=1");
|
|
5788
|
-
}
|
|
5789
|
-
|
|
5790
|
-
// Embed the REAL /design-systems/<id> (or /projects/<id>) page \u2014 the actual
|
|
5791
|
-
// React components, not a token approximation. Inline = capped height + a
|
|
5792
|
-
// footer (Fullscreen + Open). Fullscreen = full-bleed iframe with NO header of
|
|
5793
|
-
// our own: both hosts render a native close button in fullscreen (ChatGPT's
|
|
5794
|
-
// top bar, claude.ai's native header), so our own Back bar was redundant and
|
|
5795
|
-
// visually broken (misaligned, disliked chevron). [BD-136]
|
|
5796
|
-
// embed = the PUBLIC, cookieless URL we iframe (the sandbox carries no
|
|
5797
|
-
// session cookie, so an owner-scoped page would 404 inside it). open = the
|
|
5798
|
-
// owner-facing page for the human "Open" link (they have the cookie). [BD-164]
|
|
5799
|
-
function iframeBody(out, embed, open, full) {
|
|
5800
|
-
var nm = esc(out.name || "Design system");
|
|
5801
|
-
var frame = '<iframe class="bd-frame" src="' + esc(embedUrl(embed)) + '" title="' + nm + ' showcase" referrerpolicy="no-referrer-when-downgrade"></iframe>';
|
|
5802
|
-
if (full) {
|
|
5803
|
-
return '<div class="bd-frame-wrap bd-fs">' + frame + '</div>';
|
|
5804
|
-
}
|
|
5805
|
-
var openHref = open || embed;
|
|
5806
|
-
return '<div class="bd-frame-wrap">' + frame +
|
|
5807
|
-
'<div class="bd-frame-foot"><span class="bd-cta-name">' + nm + '</span><span class="bd-cta-actions">' +
|
|
5808
|
-
(canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
|
|
5809
|
-
'<a class="bd-cta" data-bd-open href="' + esc(openHref) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a>' +
|
|
5810
|
-
'</span></div>' +
|
|
5811
|
-
'</div>';
|
|
5812
|
-
}
|
|
5813
|
-
|
|
5814
|
-
// claude.ai can't iframe \u2014 surface the REAL page as a browser link (no
|
|
5815
|
-
// approximation, just not embedded). [BD-117]
|
|
5816
|
-
function linkBody(out, view) {
|
|
5817
|
-
var nm = esc(out.name || "Design system");
|
|
5818
|
-
return '<div class="bd-link-card">' +
|
|
5819
|
-
'<div class="bd-link-title">' + nm + '</div>' +
|
|
5820
|
-
'<p class="bd-link-desc">Open the live showcase to see every component, styled by this system.</p>' +
|
|
5821
|
-
'<a class="bd-cta bd-cta-lg" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open live showcase' + ICON_EXTERNAL + '</a>' +
|
|
5822
|
-
'</div>';
|
|
5823
|
-
}
|
|
5824
|
-
|
|
5825
|
-
// The name + Fullscreen + Open footer, shared by the iframe / screenshot /
|
|
5826
|
-
// token previews. Name left, actions right (CSS handles the split). [BD-136]
|
|
5827
|
-
function footHtml(out) {
|
|
7192
|
+
// The name + Fullscreen + Open footer shared by screenshot and token previews.
|
|
7193
|
+
function footHtml(out, full) {
|
|
5828
7194
|
var view = safeUrl(out.viewUrl);
|
|
5829
|
-
return '<div class="bd-
|
|
5830
|
-
(canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
|
|
5831
|
-
(view ? '<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a>' : "") +
|
|
7195
|
+
return '<div class="bd-preview-foot"><span class="bd-cta-name">' + esc(out.name || "Design system") + '</span><span class="bd-cta-actions">' +
|
|
7196
|
+
(!full && canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
|
|
7197
|
+
(view ? '<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open showcase' + ICON_EXTERNAL + '</a>' : "") +
|
|
5832
7198
|
'</span></div>';
|
|
5833
7199
|
}
|
|
5834
7200
|
|
|
5835
|
-
// A
|
|
5836
|
-
// Browser Rendering
|
|
5837
|
-
//
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
return '<div class="bd-shot-wrap' + (full ? " bd-fs" : "") + '">' +
|
|
7201
|
+
// A real screenshot of the showcase page, server-rendered via Cloudflare
|
|
7202
|
+
// Browser Rendering. Inline is capped; fullscreen is scrollable. It falls
|
|
7203
|
+
// back to the token render if the image fails. [BD-316]
|
|
7204
|
+
function shotBody(out) {
|
|
7205
|
+
return '<div class="bd-shot-wrap">' +
|
|
5841
7206
|
'<img class="bd-shot" data-bd-shot src="' + esc(safeUrl(out.shotUrl)) + '" alt="' + esc(out.name || "Design system") + ' showcase" />' +
|
|
5842
7207
|
'</div>';
|
|
5843
7208
|
}
|
|
5844
7209
|
|
|
5845
|
-
// If the screenshot 404s
|
|
5846
|
-
// shows a preview rather than a broken image. [BD-136]
|
|
7210
|
+
// If the screenshot 404s or errors, show the token render instead.
|
|
5847
7211
|
function wireShotFallback() {
|
|
5848
7212
|
var img = root.querySelector("[data-bd-shot]");
|
|
5849
7213
|
if (!img) return;
|
|
5850
7214
|
img.addEventListener("error", function () { shotFailed = true; if (lastOut) render(lastOut); });
|
|
5851
7215
|
// The screenshot loads async; its real height only exists after it decodes.
|
|
5852
|
-
//
|
|
5853
|
-
//
|
|
7216
|
+
// Some hosts read documentElement height from the DOM, so re-pin the widget
|
|
7217
|
+
// height once the image has dimensions (a cached image may already
|
|
5854
7218
|
// be complete before this runs). [BD-136]
|
|
5855
7219
|
img.addEventListener("load", scheduleResize);
|
|
5856
7220
|
if (img.complete && img.naturalHeight) scheduleResize();
|
|
@@ -5863,43 +7227,30 @@ ${DS_KIT_CSS}
|
|
|
5863
7227
|
syncHostFonts();
|
|
5864
7228
|
// Only an http(s) URL is usable; reject any other scheme up front.
|
|
5865
7229
|
var view = safeUrl(out.viewUrl);
|
|
5866
|
-
// The iframe must load a PUBLIC, cookieless URL (the sandbox has no session
|
|
5867
|
-
// cookie). previewUrl points at /preview/<fragmentId>; fall back to view if
|
|
5868
|
-
// the host didn't supply one (e.g. catalog DSes, whose view is public). [BD-164]
|
|
5869
|
-
var embed = safeUrl(out.previewUrl) || view;
|
|
5870
7230
|
var isError = out.status === "error";
|
|
5871
7231
|
var generating = out.status && out.status !== "completed" && !isError;
|
|
5872
7232
|
var completed = !isError && !generating;
|
|
5873
|
-
//
|
|
5874
|
-
//
|
|
5875
|
-
var useShot = !shotFailed && !!safeUrl(out.shotUrl);
|
|
5876
|
-
|
|
5877
|
-
//
|
|
5878
|
-
//
|
|
5879
|
-
// owner's editable /projects/<id> (they have the session cookie on the web,
|
|
5880
|
-
// so it loads for them) so fullscreen Open still reaches their project. [BD-164]
|
|
7233
|
+
// Prefer the real screenshot on wide hosts. On compact hosts a desktop
|
|
7234
|
+
// screenshot scales into unreadable text, so use the responsive token UI.
|
|
7235
|
+
var useShot = window.innerWidth >= 640 && !shotFailed && !!safeUrl(out.shotUrl);
|
|
7236
|
+
|
|
7237
|
+
// Keep the host-provided "Open in <app>" action pointed at the owner-facing
|
|
7238
|
+
// project, where the user's browser session is available. [BD-164]
|
|
5881
7239
|
if (completed && view && window.openai && window.openai.setOpenInAppUrl) {
|
|
5882
|
-
try { window.openai.setOpenInAppUrl({ href: view }); } catch (e) {}
|
|
7240
|
+
try { window.openai.setOpenInAppUrl({ href: view }); } catch (e) { /* best-effort: optional host chrome; the in-card link still works */ }
|
|
5883
7241
|
}
|
|
5884
7242
|
|
|
5885
|
-
// Fullscreen:
|
|
5886
|
-
// screenshot (scrollable) or the token showcase. [BD-136]
|
|
7243
|
+
// Fullscreen remains self-contained: show the full screenshot or token UI.
|
|
5887
7244
|
if (displayMode === "fullscreen" && completed) {
|
|
5888
|
-
if (
|
|
5889
|
-
if (useShot) { root.innerHTML = '<div class="bd-card-shell bd-fs">' + shotBody(out, true) + footHtml(out) + '</div>'; wireShotFallback(); }
|
|
7245
|
+
if (useShot) { root.innerHTML = '<div class="bd-card-shell bd-fs">' + shotBody(out) + footHtml(out, true) + '</div>'; wireShotFallback(); }
|
|
5890
7246
|
else { root.innerHTML = showcaseBody(out, true); wireShowcase(); }
|
|
5891
7247
|
return;
|
|
5892
7248
|
}
|
|
5893
7249
|
|
|
5894
|
-
// Inline completed:
|
|
5895
|
-
// (capped) or token showcase, + a footer (Fullscreen + Open). [BD-136]
|
|
7250
|
+
// Inline completed: capped screenshot or token showcase, plus clear actions.
|
|
5896
7251
|
if (completed) {
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
return;
|
|
5900
|
-
}
|
|
5901
|
-
var body = useShot ? shotBody(out, false) : showcaseBody(out, false);
|
|
5902
|
-
root.innerHTML = '<div class="bd-card-shell">' + body + footHtml(out) + '</div>';
|
|
7252
|
+
var body = useShot ? shotBody(out) : showcaseBody(out, false);
|
|
7253
|
+
root.innerHTML = '<div class="bd-card-shell">' + body + footHtml(out, false) + '</div>';
|
|
5903
7254
|
if (useShot) wireShotFallback();
|
|
5904
7255
|
return;
|
|
5905
7256
|
}
|
|
@@ -5911,7 +7262,7 @@ ${DS_KIT_CSS}
|
|
|
5911
7262
|
var hero = errorBody(out);
|
|
5912
7263
|
var cta = view
|
|
5913
7264
|
? '<div class="bd-cta-row"><span class="bd-cta-name">' + esc(out.name || "Design system") + '</span>' +
|
|
5914
|
-
'<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a></div>'
|
|
7265
|
+
'<a class="bd-cta" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open showcase' + ICON_EXTERNAL + '</a></div>'
|
|
5915
7266
|
: "";
|
|
5916
7267
|
root.innerHTML = '<div class="bd-card-shell">' + hero + cta + "</div>";
|
|
5917
7268
|
}
|
|
@@ -5920,9 +7271,9 @@ ${DS_KIT_CSS}
|
|
|
5920
7271
|
// The MCP Apps client emits ui/notifications/size-changed (ResizeObserver on
|
|
5921
7272
|
// documentElement + body); ChatGPT honors that and auto-sizes its widget.
|
|
5922
7273
|
// claude.ai web does NOT honor size-changed \u2014 it reads the widget's
|
|
5923
|
-
// documentElement height straight from the DOM and pins the
|
|
5924
|
-
// inline height the document carries. With no explicit height the
|
|
5925
|
-
// at 0 (verified live: a single size-changed{0,0} then the
|
|
7274
|
+
// documentElement height straight from the DOM and pins the widget to whatever
|
|
7275
|
+
// inline height the document carries. With no explicit height the widget stays
|
|
7276
|
+
// at 0 (verified live: a single size-changed{0,0} then the host clips it;
|
|
5926
7277
|
// claude-ai-mcp#69). So on the DOM-reading hosts we set
|
|
5927
7278
|
// document.documentElement.style.height ourselves, AFTER content + the async
|
|
5928
7279
|
// screenshot have real height. [BD-136]
|
|
@@ -5960,14 +7311,14 @@ ${DS_KIT_CSS}
|
|
|
5960
7311
|
if (typeof MutationObserver === "function") {
|
|
5961
7312
|
new MutationObserver(scheduleResize).observe(root, { childList: true, subtree: true });
|
|
5962
7313
|
}
|
|
5963
|
-
} catch (e) {}
|
|
7314
|
+
} catch (e) { /* best-effort: sandbox without MutationObserver; the height pin just updates less often */ }
|
|
5964
7315
|
try {
|
|
5965
7316
|
if (typeof ResizeObserver === "function") {
|
|
5966
7317
|
new ResizeObserver(scheduleResize).observe(root);
|
|
5967
7318
|
}
|
|
5968
|
-
} catch (e) {}
|
|
7319
|
+
} catch (e) { /* best-effort: sandbox without ResizeObserver; the height pin just updates less often */ }
|
|
5969
7320
|
|
|
5970
|
-
// The widget runs in a sandboxed
|
|
7321
|
+
// The widget runs in a sandboxed host; a bare <a target=_blank>
|
|
5971
7322
|
// cannot open a tab or navigate the top frame, so the CTA looks dead. Route the
|
|
5972
7323
|
// click through the host: MCP Apps app.openLink({url}) or OpenAI Apps
|
|
5973
7324
|
// openai.openExternal({href}). Falls through to the native anchor for hosts
|
|
@@ -5977,19 +7328,18 @@ ${DS_KIT_CSS}
|
|
|
5977
7328
|
var t = e.target;
|
|
5978
7329
|
if (!t || !t.closest) return;
|
|
5979
7330
|
if (t.closest("[data-bd-fullscreen]")) { e.preventDefault(); setDisplayMode("fullscreen"); return; }
|
|
5980
|
-
if (t.closest("[data-bd-collapse]")) { e.preventDefault(); setDisplayMode("inline"); return; }
|
|
5981
7331
|
var a = t.closest("a[data-bd-open]");
|
|
5982
7332
|
if (!a) return;
|
|
5983
7333
|
var url = a.getAttribute("href");
|
|
5984
7334
|
if (!url) return;
|
|
5985
|
-
if (
|
|
7335
|
+
if (window.openai && window.openai.openExternal) {
|
|
5986
7336
|
e.preventDefault();
|
|
5987
|
-
try {
|
|
7337
|
+
try { window.openai.openExternal({ href: url }); } catch (err) { /* best-effort: host declined openExternal; nothing else can open the link */ }
|
|
5988
7338
|
return;
|
|
5989
7339
|
}
|
|
5990
|
-
if (
|
|
7340
|
+
if (bdApp && bdApp.openLink) {
|
|
5991
7341
|
e.preventDefault();
|
|
5992
|
-
try {
|
|
7342
|
+
try { bdApp.openLink({ url: url }); } catch (err) { /* best-effort: host declined openLink; nothing else can open the link */ }
|
|
5993
7343
|
}
|
|
5994
7344
|
});
|
|
5995
7345
|
|
|
@@ -6036,9 +7386,9 @@ ${DS_KIT_CSS}
|
|
|
6036
7386
|
// this every later host-context-changed (theme/font) would rebuild the
|
|
6037
7387
|
// DOM and lose scroll + active-section state in fullscreen. [BD-117]
|
|
6038
7388
|
else if (ctx && ctx.availableDisplayModes && !sawDisplayModes && lastOut) { sawDisplayModes = true; render(lastOut); }
|
|
6039
|
-
} catch (e) {}
|
|
7389
|
+
} catch (e) { /* best-effort: a malformed host context must not kill the widget; keep the last rendered state */ }
|
|
6040
7390
|
};
|
|
6041
|
-
if (app.connect) app.connect().catch(function () {});
|
|
7391
|
+
if (app.connect) app.connect().catch(function () { /* best-effort: a host that refuses the connect handshake still shows the static fallback */ });
|
|
6042
7392
|
}
|
|
6043
7393
|
} catch (e) { /* host without MCP Apps support; OpenAI bridge or static text covers it */ }
|
|
6044
7394
|
})();
|
|
@@ -6048,8 +7398,132 @@ ${DS_KIT_CSS}
|
|
|
6048
7398
|
}
|
|
6049
7399
|
});
|
|
6050
7400
|
|
|
6051
|
-
// ../shared/src/mcp/
|
|
7401
|
+
// ../shared/src/mcp/shopify-primitives.ts
|
|
6052
7402
|
import { z as z5 } from "zod";
|
|
7403
|
+
function exportedStringConstants(files) {
|
|
7404
|
+
const constants = /* @__PURE__ */ new Map();
|
|
7405
|
+
const declaration = /export\s+const\s+([A-Za-z_$][\w$]*)(?:\s*:[^=;]+)?\s*=\s*([\s\S]*?);/g;
|
|
7406
|
+
const literal = /"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'/g;
|
|
7407
|
+
for (const { content } of files) {
|
|
7408
|
+
for (const match of content.matchAll(declaration)) {
|
|
7409
|
+
const expression = match[2] ?? "";
|
|
7410
|
+
const values = [...expression.matchAll(literal)].map(
|
|
7411
|
+
(part) => (part[1] ?? part[2] ?? "").replace(/\\([\\"'])/g, "$1")
|
|
7412
|
+
);
|
|
7413
|
+
const residue = expression.replace(literal, "").replace(/\+/g, "").trim();
|
|
7414
|
+
if (values.length && !residue) constants.set(match[1], values.join(""));
|
|
7415
|
+
}
|
|
7416
|
+
}
|
|
7417
|
+
return constants;
|
|
7418
|
+
}
|
|
7419
|
+
function importedMaterialNames(content, constants) {
|
|
7420
|
+
const names = /* @__PURE__ */ new Set();
|
|
7421
|
+
for (const match of content.matchAll(localImport)) {
|
|
7422
|
+
for (const item of (match[3] ?? "").split(",")) {
|
|
7423
|
+
const imported = item.trim().split(/\s+as\s+/)[0];
|
|
7424
|
+
if (imported && constants.has(imported)) names.add(imported);
|
|
7425
|
+
}
|
|
7426
|
+
}
|
|
7427
|
+
return names;
|
|
7428
|
+
}
|
|
7429
|
+
function replaceMaterialNames(source, names, constants) {
|
|
7430
|
+
let result = source;
|
|
7431
|
+
for (const name of names) {
|
|
7432
|
+
result = result.replace(
|
|
7433
|
+
new RegExp(`\\b${name.replace(/[$]/g, "\\$")}\\b`, "g"),
|
|
7434
|
+
JSON.stringify(constants.get(name))
|
|
7435
|
+
);
|
|
7436
|
+
}
|
|
7437
|
+
return result;
|
|
7438
|
+
}
|
|
7439
|
+
function materializeFile(file, constants) {
|
|
7440
|
+
const names = importedMaterialNames(file.content, constants);
|
|
7441
|
+
if (!names.size) return file;
|
|
7442
|
+
let cursor = 0;
|
|
7443
|
+
let content = "";
|
|
7444
|
+
for (const match of file.content.matchAll(localImport)) {
|
|
7445
|
+
content += replaceMaterialNames(file.content.slice(cursor, match.index), names, constants);
|
|
7446
|
+
content += match[0];
|
|
7447
|
+
cursor = (match.index ?? 0) + match[0].length;
|
|
7448
|
+
}
|
|
7449
|
+
content += replaceMaterialNames(file.content.slice(cursor), names, constants);
|
|
7450
|
+
return { ...file, content };
|
|
7451
|
+
}
|
|
7452
|
+
function materializeImportedMaterial(files) {
|
|
7453
|
+
const constants = exportedStringConstants(files);
|
|
7454
|
+
if (!constants.size) return files;
|
|
7455
|
+
return files.map((file) => materializeFile(file, constants));
|
|
7456
|
+
}
|
|
7457
|
+
function globalsSource(files) {
|
|
7458
|
+
return files.find(({ path: path5 }) => /(^|\/)globals\.css$/.test(path5))?.content ?? "";
|
|
7459
|
+
}
|
|
7460
|
+
function staticKitSource(files) {
|
|
7461
|
+
return files.find(({ path: path5 }) => /(^|\/)static-kit\.css$/.test(path5))?.content.trim() ?? "";
|
|
7462
|
+
}
|
|
7463
|
+
function buildShopifyTokensCss(files) {
|
|
7464
|
+
const globals = globalsSource(files);
|
|
7465
|
+
if (!globals.trim()) {
|
|
7466
|
+
throw new Error("Shopify primitives require a globals.css token source");
|
|
7467
|
+
}
|
|
7468
|
+
const vars = mirrorSingleTheme(parseCssVars(globals));
|
|
7469
|
+
const { fontVars, fontImport } = extractFontCss(globals);
|
|
7470
|
+
return [
|
|
7471
|
+
fontImport,
|
|
7472
|
+
cssFromRegistryVars(vars),
|
|
7473
|
+
fontVars,
|
|
7474
|
+
extractRadiusScaleCss(globals),
|
|
7475
|
+
extractShadowScaleCss(globals),
|
|
7476
|
+
componentRadiusCss(files),
|
|
7477
|
+
componentDepthCss(files)
|
|
7478
|
+
].filter(Boolean).join("\n");
|
|
7479
|
+
}
|
|
7480
|
+
function buildShopifyPrimitiveSet(files) {
|
|
7481
|
+
const materializedFiles = materializeImportedMaterial(files);
|
|
7482
|
+
const tokensCss = buildShopifyTokensCss(materializedFiles);
|
|
7483
|
+
const componentKit = buildDsComponentCss(materializedFiles);
|
|
7484
|
+
const staticKitCss = staticKitSource(materializedFiles);
|
|
7485
|
+
const material = staticKitCss ? { ...componentKit, css: `${componentKit.css}
|
|
7486
|
+
${staticKitCss}` } : componentKit;
|
|
7487
|
+
const themeFiles = buildDsShopifyFiles({ tokensCss, componentKit: material });
|
|
7488
|
+
return shopifyPrimitiveSetSchema.parse({
|
|
7489
|
+
contractVersion: SHOPIFY_PRIMITIVE_CONTRACT_VERSION,
|
|
7490
|
+
tokensCss,
|
|
7491
|
+
componentMaterialCss: material.css,
|
|
7492
|
+
coveredSourcePrimitives: componentKit.covered,
|
|
7493
|
+
sourcePrimitivesWithShopifyAdapters: componentKit.excluded,
|
|
7494
|
+
themeFiles
|
|
7495
|
+
});
|
|
7496
|
+
}
|
|
7497
|
+
function parseShopifyPrimitiveSet(value) {
|
|
7498
|
+
const parsed = shopifyPrimitiveSetSchema.safeParse(value);
|
|
7499
|
+
return parsed.success ? parsed.data : null;
|
|
7500
|
+
}
|
|
7501
|
+
var SHOPIFY_PRIMITIVE_CONTRACT_VERSION, shopifyThemeFileSchema, shopifyPrimitiveSetSchema, localImport;
|
|
7502
|
+
var init_shopify_primitives = __esm({
|
|
7503
|
+
"../shared/src/mcp/shopify-primitives.ts"() {
|
|
7504
|
+
"use strict";
|
|
7505
|
+
init_registry_css();
|
|
7506
|
+
init_preview_html();
|
|
7507
|
+
init_shopify_kit();
|
|
7508
|
+
SHOPIFY_PRIMITIVE_CONTRACT_VERSION = 1;
|
|
7509
|
+
shopifyThemeFileSchema = z5.object({
|
|
7510
|
+
path: z5.string().min(1),
|
|
7511
|
+
content: z5.string().min(1)
|
|
7512
|
+
});
|
|
7513
|
+
shopifyPrimitiveSetSchema = z5.object({
|
|
7514
|
+
contractVersion: z5.literal(SHOPIFY_PRIMITIVE_CONTRACT_VERSION),
|
|
7515
|
+
tokensCss: z5.string().min(1),
|
|
7516
|
+
componentMaterialCss: z5.string(),
|
|
7517
|
+
coveredSourcePrimitives: z5.array(z5.string()),
|
|
7518
|
+
sourcePrimitivesWithShopifyAdapters: z5.array(z5.string()),
|
|
7519
|
+
themeFiles: z5.array(shopifyThemeFileSchema).min(1)
|
|
7520
|
+
});
|
|
7521
|
+
localImport = /(^|\n)(\s*import\s*\{([^}]*)\}\s*from\s*["']\.\.?\/[^"']+["'];?)/g;
|
|
7522
|
+
}
|
|
7523
|
+
});
|
|
7524
|
+
|
|
7525
|
+
// ../shared/src/mcp/tools/design-system-tools.ts
|
|
7526
|
+
import { z as z6 } from "zod";
|
|
6053
7527
|
function installCommandWithInit(installCommand) {
|
|
6054
7528
|
return `${SHADCN_INIT_IF_MISSING}
|
|
6055
7529
|
|
|
@@ -6079,15 +7553,42 @@ function inlineBuildMd(projectId) {
|
|
|
6079
7553
|
return `
|
|
6080
7554
|
**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.`;
|
|
6081
7555
|
}
|
|
6082
|
-
async function fetchProjectTokensCss(projectId, authContext) {
|
|
7556
|
+
async function fetchProjectTokensCss(projectId, apiFetch, authContext) {
|
|
6083
7557
|
try {
|
|
6084
|
-
const res = await
|
|
7558
|
+
const res = await apiFetch(`${APP_BASE_URL}/api/projects/${projectId}/registry/all.json`, {
|
|
6085
7559
|
headers: authHeaders(authContext)
|
|
6086
7560
|
});
|
|
6087
7561
|
if (!res.ok) return null;
|
|
6088
7562
|
const json = await res.json();
|
|
6089
7563
|
return { tokensCss: tokensCssFromRegistry(json), name: json.name, files: json.files ?? [] };
|
|
6090
|
-
} catch {
|
|
7564
|
+
} catch (err) {
|
|
7565
|
+
console.warn(`fetchProjectTokensCss(${projectId}) failed`, err);
|
|
7566
|
+
return null;
|
|
7567
|
+
}
|
|
7568
|
+
}
|
|
7569
|
+
function parseProjectShopifyPrimitives(projectId, name, value) {
|
|
7570
|
+
const primitives = parseShopifyPrimitiveSet(value);
|
|
7571
|
+
if (!primitives) return null;
|
|
7572
|
+
return {
|
|
7573
|
+
name: typeof name === "string" ? name : `Project ${projectId.slice(0, 8)}`,
|
|
7574
|
+
primitives
|
|
7575
|
+
};
|
|
7576
|
+
}
|
|
7577
|
+
async function fetchProjectShopifyPrimitives(projectId, apiFetch, authContext) {
|
|
7578
|
+
try {
|
|
7579
|
+
const res = await apiFetch(
|
|
7580
|
+
`${APP_BASE_URL}/api/projects/${projectId}/shopify/all.json`,
|
|
7581
|
+
{ headers: authHeaders(authContext) }
|
|
7582
|
+
);
|
|
7583
|
+
if (!res.ok) return null;
|
|
7584
|
+
const json = await res.json();
|
|
7585
|
+
return parseProjectShopifyPrimitives(
|
|
7586
|
+
projectId,
|
|
7587
|
+
json.name,
|
|
7588
|
+
json.shopifyPrimitives
|
|
7589
|
+
);
|
|
7590
|
+
} catch (err) {
|
|
7591
|
+
console.warn(`fetchProjectShopifyPrimitives(${projectId}) failed`, err);
|
|
6091
7592
|
return null;
|
|
6092
7593
|
}
|
|
6093
7594
|
}
|
|
@@ -6128,25 +7629,50 @@ function completedShopifyKitStructuredContent(name, viewUrl, tokensCss, themeFil
|
|
|
6128
7629
|
tokensCss
|
|
6129
7630
|
};
|
|
6130
7631
|
}
|
|
7632
|
+
function completedShopifyKitResult(name, viewUrl, source, primitives) {
|
|
7633
|
+
const { tokensCss, themeFiles } = primitives;
|
|
7634
|
+
return {
|
|
7635
|
+
content: [{
|
|
7636
|
+
type: "text",
|
|
7637
|
+
text: buildDsShopifyKit({ name, tokensCss, viewUrl, source, themeFiles })
|
|
7638
|
+
}],
|
|
7639
|
+
structuredContent: completedShopifyKitStructuredContent(
|
|
7640
|
+
name,
|
|
7641
|
+
viewUrl,
|
|
7642
|
+
tokensCss,
|
|
7643
|
+
themeFiles
|
|
7644
|
+
)
|
|
7645
|
+
};
|
|
7646
|
+
}
|
|
6131
7647
|
async function fetchCatalog(designSystemId) {
|
|
6132
7648
|
try {
|
|
6133
7649
|
const [globalsRes, ...componentResults] = await Promise.all([
|
|
6134
7650
|
fetch(`${REGISTRY_BASE_URL}/${designSystemId}/globals.json`),
|
|
6135
7651
|
...CATALOG_KIT_COMPONENTS.map(
|
|
6136
|
-
(c) => fetch(`${REGISTRY_BASE_URL}/${designSystemId}/${c}.json`).catch(() =>
|
|
7652
|
+
(c) => fetch(`${REGISTRY_BASE_URL}/${designSystemId}/${c}.json`).catch((err) => {
|
|
7653
|
+
console.warn(`catalog component fetch failed: ${designSystemId}/${c}`, err);
|
|
7654
|
+
return null;
|
|
7655
|
+
})
|
|
6137
7656
|
)
|
|
6138
7657
|
]);
|
|
6139
7658
|
if (!globalsRes.ok) return { tokensCss: "", files: [] };
|
|
6140
7659
|
const globals = REGISTRY_ITEM_SCHEMA.safeParse(await globalsRes.json());
|
|
6141
7660
|
if (!globals.success) return { tokensCss: "", files: [] };
|
|
6142
7661
|
const files = [...globals.data.files ?? []];
|
|
6143
|
-
for (const res of componentResults) {
|
|
6144
|
-
if (!res
|
|
7662
|
+
for (const [i, res] of componentResults.entries()) {
|
|
7663
|
+
if (!res) continue;
|
|
7664
|
+
if (!res.ok) {
|
|
7665
|
+
console.warn(
|
|
7666
|
+
`catalog component unavailable: ${designSystemId}/${CATALOG_KIT_COMPONENTS[i]} (HTTP ${res.status})`
|
|
7667
|
+
);
|
|
7668
|
+
continue;
|
|
7669
|
+
}
|
|
6145
7670
|
const item = REGISTRY_ITEM_SCHEMA.safeParse(await res.json().catch(() => null));
|
|
6146
7671
|
if (item.success && item.data.files) files.push(...item.data.files);
|
|
6147
7672
|
}
|
|
6148
7673
|
return { tokensCss: tokensCssFromRegistry({ cssVars: globals.data.cssVars, files }), files };
|
|
6149
|
-
} catch {
|
|
7674
|
+
} catch (error) {
|
|
7675
|
+
console.warn(`catalog registry fetch failed for ${designSystemId}`, error);
|
|
6150
7676
|
return { tokensCss: "", files: [] };
|
|
6151
7677
|
}
|
|
6152
7678
|
}
|
|
@@ -6171,10 +7697,11 @@ function registerDesignSystemTools({
|
|
|
6171
7697
|
provider,
|
|
6172
7698
|
tracked,
|
|
6173
7699
|
track,
|
|
6174
|
-
authContext
|
|
7700
|
+
authContext,
|
|
7701
|
+
apiFetch = globalThis.fetch
|
|
6175
7702
|
}, { enableShopifyKit = true } = {}) {
|
|
6176
|
-
const searchInputSchema =
|
|
6177
|
-
query:
|
|
7703
|
+
const searchInputSchema = z6.object({
|
|
7704
|
+
query: z6.string().describe(
|
|
6178
7705
|
"The user's app, UI, industry, or visual direction. Examples: 'World Cup betting app for friends', 'warm editorial ecommerce', or 'professional fintech dashboard'."
|
|
6179
7706
|
)
|
|
6180
7707
|
});
|
|
@@ -6269,7 +7796,7 @@ ${BUILD_TARGET_RULE}` : ""}`
|
|
|
6269
7796
|
title: "Find a Design System for a UI Build",
|
|
6270
7797
|
description: searchDescription(),
|
|
6271
7798
|
inputSchema: searchInputSchema,
|
|
6272
|
-
annotations:
|
|
7799
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
6273
7800
|
},
|
|
6274
7801
|
makeSearchHandler("find-design-system", true)
|
|
6275
7802
|
);
|
|
@@ -6290,7 +7817,7 @@ ${BUILD_TARGET_RULE}` : ""}`
|
|
|
6290
7817
|
structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
|
|
6291
7818
|
};
|
|
6292
7819
|
}
|
|
6293
|
-
const result = await fetchProjectTokensCss(projectId, authContext);
|
|
7820
|
+
const result = await fetchProjectTokensCss(projectId, apiFetch, authContext);
|
|
6294
7821
|
if (!result?.tokensCss) {
|
|
6295
7822
|
return {
|
|
6296
7823
|
content: [{ type: "text", text: `Could not load tokens for project "${projectId}". It may still be generating, or the id is wrong. Check ${viewUrl2}.` }],
|
|
@@ -6393,31 +7920,24 @@ ${fence3}` }],
|
|
|
6393
7920
|
}) => tracked("get-design-system-kit", { target: "shopify", designSystemId, projectId, context }, async () => {
|
|
6394
7921
|
if (projectId) {
|
|
6395
7922
|
const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
|
|
6396
|
-
const
|
|
6397
|
-
|
|
7923
|
+
const providerResult = await provider.getProjectShopifyPrimitives?.(projectId);
|
|
7924
|
+
const result = providerResult ? parseProjectShopifyPrimitives(
|
|
7925
|
+
projectId,
|
|
7926
|
+
providerResult.name,
|
|
7927
|
+
providerResult.shopifyPrimitives
|
|
7928
|
+
) : await fetchProjectShopifyPrimitives(projectId, apiFetch, authContext);
|
|
7929
|
+
if (!result) {
|
|
6398
7930
|
return {
|
|
6399
|
-
content: [{ type: "text", text: `
|
|
7931
|
+
content: [{ type: "text", text: `Shopify version unavailable. Project "${projectId}" has no maintained Shopify primitive set yet. It may still be generating, belong to another account, or predate Shopify persistence. Check ${viewUrl2}.` }],
|
|
6400
7932
|
structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
|
|
6401
7933
|
};
|
|
6402
7934
|
}
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
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
|
-
};
|
|
7935
|
+
return completedShopifyKitResult(
|
|
7936
|
+
result.name,
|
|
7937
|
+
viewUrl2,
|
|
7938
|
+
{ kind: "project", projectId },
|
|
7939
|
+
result.primitives
|
|
7940
|
+
);
|
|
6421
7941
|
}
|
|
6422
7942
|
if (!designSystemId) {
|
|
6423
7943
|
return {
|
|
@@ -6425,7 +7945,10 @@ ${fence3}` }],
|
|
|
6425
7945
|
structuredContent: { status: "error", name: "Missing design system", tokensCss: "" }
|
|
6426
7946
|
};
|
|
6427
7947
|
}
|
|
6428
|
-
const ds = await provider.getDesignSystemById(designSystemId, {
|
|
7948
|
+
const ds = await provider.getDesignSystemById(designSystemId, {
|
|
7949
|
+
includeComponents: false,
|
|
7950
|
+
includeShopifyPrimitives: true
|
|
7951
|
+
});
|
|
6429
7952
|
if (!ds) {
|
|
6430
7953
|
return {
|
|
6431
7954
|
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.` }],
|
|
@@ -6433,30 +7956,20 @@ ${fence3}` }],
|
|
|
6433
7956
|
};
|
|
6434
7957
|
}
|
|
6435
7958
|
const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
|
|
6436
|
-
const
|
|
6437
|
-
if (!
|
|
7959
|
+
const primitives = parseShopifyPrimitiveSet(ds.metadata.shopifyPrimitives);
|
|
7960
|
+
if (!primitives) {
|
|
7961
|
+
const text = ds.metadata.shopifyAccess === "denied" ? `Shopify target unavailable. The Shopify kit is a private pilot and this API key is not in it, so the stored theme for "${designSystemId}" was withheld. Nothing is wrong with the design system. Ask for pilot access, or use target "react" or "html". See ${viewUrl}.` : `Shopify version unavailable. Design system "${designSystemId}" has no maintained Shopify primitive set. Shopify output is unavailable until that system is seeded and certified; no generic fallback was returned. See ${viewUrl}.`;
|
|
6438
7962
|
return {
|
|
6439
|
-
content: [{ type: "text", text
|
|
7963
|
+
content: [{ type: "text", text }],
|
|
6440
7964
|
structuredContent: { status: "error", name: ds.metadata.title, viewUrl, tokensCss: "" }
|
|
6441
7965
|
};
|
|
6442
7966
|
}
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
name: ds.metadata.title,
|
|
6446
|
-
tokensCss,
|
|
7967
|
+
return completedShopifyKitResult(
|
|
7968
|
+
ds.metadata.title,
|
|
6447
7969
|
viewUrl,
|
|
6448
|
-
|
|
6449
|
-
|
|
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
|
-
};
|
|
7970
|
+
{ kind: "catalog", designSystemId },
|
|
7971
|
+
primitives
|
|
7972
|
+
);
|
|
6460
7973
|
});
|
|
6461
7974
|
server.registerTool(
|
|
6462
7975
|
"get-design-system-kit",
|
|
@@ -6488,22 +8001,22 @@ existing setup; otherwise use the default Base UI. Both flavors install to the
|
|
|
6488
8001
|
same components/ui/ paths.
|
|
6489
8002
|
|
|
6490
8003
|
${BUILD_TARGET_RULE}`,
|
|
6491
|
-
inputSchema:
|
|
8004
|
+
inputSchema: z6.object({
|
|
6492
8005
|
// Off-pilot callers must not see the target at all, not merely be refused it.
|
|
6493
|
-
target:
|
|
8006
|
+
target: z6.enum(
|
|
6494
8007
|
enableShopifyKit ? ["react", "html", "shopify"] : ["react", "html"]
|
|
6495
8008
|
).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:
|
|
6497
|
-
projectId:
|
|
6498
|
-
component:
|
|
6499
|
-
library:
|
|
8009
|
+
designSystemId: z6.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.`),
|
|
8010
|
+
projectId: z6.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"'}.`),
|
|
8011
|
+
component: z6.union([z6.string(), z6.array(z6.string())]).optional().describe("React target only. Pass only when the user asks to inspect specific component source code. Omit to scaffold the entire design system."),
|
|
8012
|
+
library: z6.enum(["radix", "base-ui"]).optional().describe('React target only, default "base-ui". Pass "radix" when the project already uses Radix primitives.'),
|
|
6500
8013
|
context: CONTEXT_ARG
|
|
6501
8014
|
}),
|
|
6502
8015
|
outputSchema: buildKitOutputSchema(enableShopifyKit),
|
|
6503
|
-
annotations:
|
|
8016
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
6504
8017
|
// This tool returns build instructions, not a visual preview. Attaching
|
|
6505
|
-
// the preview template makes ChatGPT render a
|
|
6506
|
-
// when that
|
|
8018
|
+
// the preview template makes ChatGPT render a large visual card;
|
|
8019
|
+
// when that preview is unavailable the user sees a large blank card even
|
|
6507
8020
|
// though the kit succeeded. preview-design-system remains the dedicated
|
|
6508
8021
|
// interactive preview path. [BD-240]
|
|
6509
8022
|
},
|
|
@@ -6717,15 +8230,18 @@ _${installNote}_`,
|
|
|
6717
8230
|
|
|
6718
8231
|
**Use this on any "build/make me <app>" request, even when the user never mentions design or branding** (a design system IS the app's brand). But DON'T jump straight here: first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via \`get-design-system-kit\`.
|
|
6719
8232
|
|
|
8233
|
+
**When the user has an existing brand** (their site, Figma file, or stylesheet): run \`extract-design-system\` or \`extract-from-url\` first and pass the resulting token sheet (the fenced \`\`\`css block, verbatim: the \`:root { ... }\` and optional \`.dark { ... }\` blocks) as \`brandCss\`. The generation is then pinned to that brand: no catalog aesthetic is retrieved, the sheet's colors, fonts and radii override everything, and later design-system switches are refused. Copy the block exactly; do not retype or trim it.
|
|
8234
|
+
|
|
6720
8235
|
How it works:
|
|
6721
8236
|
1. Sends the user's prompt to Better Design's generation pipeline (same flow the web chat input uses) and returns IMMEDIATELY with the project id + project URL.
|
|
6722
8237
|
2. Generation runs in the background and typically takes 1-3 minutes.
|
|
6723
8238
|
3. Call \`get-design-system-status\` with the returned project id (wait ~60 seconds first) to get the live preview URL and the final \`npx shadcn add <registry-url>\` install command once it completes.
|
|
6724
8239
|
|
|
6725
8240
|
Share the project URL with the user right away so they can watch the generation live.`,
|
|
6726
|
-
inputSchema:
|
|
6727
|
-
prompt:
|
|
6728
|
-
baseDesignSystemId:
|
|
8241
|
+
inputSchema: z6.object({
|
|
8242
|
+
prompt: z6.string().min(1).max(1e4).describe("Describe the design system the user wants. E.g. 'A warm, minimal design system for a personal finance app, soft pastels, rounded corners' or 'A brutalist developer tool aesthetic with sharp edges and monospace type'."),
|
|
8243
|
+
baseDesignSystemId: z6.string().optional().describe("Optional. The catalog id the USER picked from the `find-design-system` matches you showed them, used to bias generation toward that aesthetic (e.g. a theme tweak on top of Linear). Leave empty only when the user chose fully custom."),
|
|
8244
|
+
brandCss: z6.string().max(5e4).optional().describe("Optional. The user's extracted brand token sheet, verbatim: the fenced css block from extract-design-system / extract-from-url (`:root { --background: ...; }` plus optional `.dark { ... }`). When set, generation builds FROM this brand instead of a catalog look-alike, and baseDesignSystemId is ignored (a catalog base would leak its identity past the brand tokens).")
|
|
6729
8245
|
}),
|
|
6730
8246
|
annotations: {
|
|
6731
8247
|
readOnlyHint: false,
|
|
@@ -6733,7 +8249,7 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6733
8249
|
openWorldHint: false
|
|
6734
8250
|
}
|
|
6735
8251
|
},
|
|
6736
|
-
async ({ prompt, baseDesignSystemId }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId) }, async () => {
|
|
8252
|
+
async ({ prompt, baseDesignSystemId, brandCss }) => tracked("create-design-system", { query: prompt, hasBase: Boolean(baseDesignSystemId), hasBrand: Boolean(brandCss) }, async () => {
|
|
6737
8253
|
const startUrl = `${APP_BASE_URL}/api/projects/anonymous`;
|
|
6738
8254
|
let projectId;
|
|
6739
8255
|
const requestHeaders = { "Content-Type": "application/json" };
|
|
@@ -6742,12 +8258,13 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6742
8258
|
requestHeaders.Authorization = `Bearer ${bearer2}`;
|
|
6743
8259
|
}
|
|
6744
8260
|
try {
|
|
6745
|
-
const startRes = await
|
|
8261
|
+
const startRes = await apiFetch(startUrl, {
|
|
6746
8262
|
method: "POST",
|
|
6747
8263
|
headers: requestHeaders,
|
|
6748
8264
|
body: JSON.stringify({
|
|
6749
8265
|
value: prompt,
|
|
6750
|
-
...baseDesignSystemId ? { designSystemId: baseDesignSystemId } : {}
|
|
8266
|
+
...baseDesignSystemId ? { designSystemId: baseDesignSystemId } : {},
|
|
8267
|
+
...brandCss ? { brandCss } : {}
|
|
6751
8268
|
})
|
|
6752
8269
|
});
|
|
6753
8270
|
if (!startRes.ok) {
|
|
@@ -6768,10 +8285,10 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6768
8285
|
"",
|
|
6769
8286
|
`Project id: \`${projectId}\``,
|
|
6770
8287
|
"",
|
|
6771
|
-
...authContext ? [`Saved to your Better Design account. Watch it generate live:`] : [`Watch it generate live:`],
|
|
8288
|
+
...authContext ? [`Saved to your Better Design account. Watch it generate live (sign in with the account that created it, this link is private to you):`] : [`Watch it generate live:`],
|
|
6772
8289
|
projectUrl,
|
|
6773
8290
|
"",
|
|
6774
|
-
`Generation takes 1-3 minutes.
|
|
8291
|
+
`Generation takes 1-3 minutes. Wait ~60 seconds, then call \`get-design-system-status\` with the project id: it returns a shareable preview link that opens without signing in, plus the final install command. Give the user that link, not this one.`,
|
|
6775
8292
|
inlineBuildMd(projectId)
|
|
6776
8293
|
];
|
|
6777
8294
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -6786,7 +8303,7 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6786
8303
|
const statusUrl = `${APP_BASE_URL}/api/projects/${projectId}`;
|
|
6787
8304
|
let project;
|
|
6788
8305
|
try {
|
|
6789
|
-
const res = await
|
|
8306
|
+
const res = await apiFetch(statusUrl, { headers });
|
|
6790
8307
|
if (!res.ok) {
|
|
6791
8308
|
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}.` }] };
|
|
6792
8309
|
}
|
|
@@ -6803,7 +8320,7 @@ npx shadcn@latest add ${registryUrl}
|
|
|
6803
8320
|
let tokensCss = "";
|
|
6804
8321
|
let dsName = `Project ${projectId.slice(0, 8)}`;
|
|
6805
8322
|
try {
|
|
6806
|
-
const res = await
|
|
8323
|
+
const res = await apiFetch(registryUrl);
|
|
6807
8324
|
if (res.ok) {
|
|
6808
8325
|
const json = await res.json();
|
|
6809
8326
|
if (json.meta?.installInstructions) body = json.meta.installInstructions;
|
|
@@ -6813,10 +8330,17 @@ npx shadcn@latest add ${registryUrl}
|
|
|
6813
8330
|
}
|
|
6814
8331
|
} catch {
|
|
6815
8332
|
}
|
|
8333
|
+
const previewUrl = project.previewFragmentId ? `${APP_BASE_URL}/preview/${project.previewFragmentId}` : projectUrl;
|
|
8334
|
+
const shareLines = project.previewFragmentId ? [
|
|
8335
|
+
`Shareable preview, opens without signing in:`,
|
|
8336
|
+
previewUrl,
|
|
8337
|
+
""
|
|
8338
|
+
] : [];
|
|
6816
8339
|
const lines = [
|
|
6817
8340
|
`# Design system ready`,
|
|
6818
8341
|
"",
|
|
6819
|
-
...
|
|
8342
|
+
...shareLines,
|
|
8343
|
+
...authContext ? [`Saved to your Better Design account. View, edit, and re-grab the install command here (sign in with the account that created it, this link is private to you):`] : [`View and preview your design system anytime here:`],
|
|
6820
8344
|
projectUrl,
|
|
6821
8345
|
"",
|
|
6822
8346
|
`Project id: \`${projectId}\``,
|
|
@@ -6832,7 +8356,6 @@ npx shadcn@latest add ${registryUrl}
|
|
|
6832
8356
|
"",
|
|
6833
8357
|
`Now help the user finish, don't stop here. No icon library has been picked yet: call \`find-icon-library\` (query from the brand's personality), SHOW them the closest matches and let them pick one, then \`install-icons\`. After that, ask if they want to start building components.`
|
|
6834
8358
|
];
|
|
6835
|
-
const previewUrl = project.previewFragmentId ? `${APP_BASE_URL}/preview/${project.previewFragmentId}` : projectUrl;
|
|
6836
8359
|
const shotUrl = project.previewFragmentId ? `${APP_BASE_URL}/api/og/preview?fragment=${project.previewFragmentId}` : void 0;
|
|
6837
8360
|
return {
|
|
6838
8361
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -6870,8 +8393,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6870
8393
|
const statusConfig = {
|
|
6871
8394
|
title: "Get Design System Generation Status",
|
|
6872
8395
|
description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready. Once ready, ask the user what's next (icon library, install, or build) instead of stopping.`,
|
|
6873
|
-
inputSchema:
|
|
6874
|
-
projectId:
|
|
8396
|
+
inputSchema: z6.object({
|
|
8397
|
+
projectId: z6.string().min(1).describe("The project id returned by create-design-system."),
|
|
6875
8398
|
context: CONTEXT_ARG
|
|
6876
8399
|
}),
|
|
6877
8400
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -6894,8 +8417,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6894
8417
|
{
|
|
6895
8418
|
title: "Preview a Design System",
|
|
6896
8419
|
description: `Show the user a visual preview of a design system from the catalog (use the id from 'find-design-system'). Returns a token-true HTML preview: palette, typography, buttons, card, input, badge, plus a link to the full live showcase. Use this when the user wants to SEE a design system before installing it.`,
|
|
6897
|
-
inputSchema:
|
|
6898
|
-
designSystemId:
|
|
8420
|
+
inputSchema: z6.object({
|
|
8421
|
+
designSystemId: z6.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'."),
|
|
6899
8422
|
context: CONTEXT_ARG
|
|
6900
8423
|
}),
|
|
6901
8424
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -6945,8 +8468,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6945
8468
|
status: "completed",
|
|
6946
8469
|
name: ds.metadata.title,
|
|
6947
8470
|
viewUrl,
|
|
6948
|
-
// Real screenshot of the showcase
|
|
6949
|
-
// (claude.ai). The widget shows this as the inline image. [BD-136]
|
|
8471
|
+
// Real screenshot of the showcase for the self-contained widget.
|
|
6950
8472
|
shotUrl: `${APP_BASE_URL}/api/og/preview?id=${encodeURIComponent(designSystemId)}`,
|
|
6951
8473
|
tokensCss
|
|
6952
8474
|
}
|
|
@@ -6961,55 +8483,26 @@ var init_design_system_tools = __esm({
|
|
|
6961
8483
|
init_guardrails();
|
|
6962
8484
|
init_html_kit();
|
|
6963
8485
|
init_shopify_kit();
|
|
8486
|
+
init_shopify_primitives();
|
|
6964
8487
|
init_preview_html();
|
|
6965
8488
|
init_shared();
|
|
6966
|
-
REGISTRY_ITEM_SCHEMA =
|
|
6967
|
-
cssVars:
|
|
6968
|
-
light:
|
|
6969
|
-
dark:
|
|
6970
|
-
theme:
|
|
8489
|
+
REGISTRY_ITEM_SCHEMA = z6.object({
|
|
8490
|
+
cssVars: z6.object({
|
|
8491
|
+
light: z6.record(z6.string(), z6.string()).optional(),
|
|
8492
|
+
dark: z6.record(z6.string(), z6.string()).optional(),
|
|
8493
|
+
theme: z6.record(z6.string(), z6.string()).optional()
|
|
6971
8494
|
}).optional(),
|
|
6972
|
-
files:
|
|
8495
|
+
files: z6.array(z6.object({ path: z6.string().optional(), content: z6.string().optional() })).optional()
|
|
6973
8496
|
});
|
|
6974
8497
|
SHADCN_INIT_IF_MISSING = "[ -f components.json ] || npx shadcn@latest init -d -y";
|
|
6975
8498
|
SAFE_COMPONENT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
6976
8499
|
CATALOG_KIT_COMPONENTS = ["button", "input", "card", "badge", "switch"];
|
|
6977
|
-
DS_INDEX_SCHEMA =
|
|
6978
|
-
baseComponents:
|
|
8500
|
+
DS_INDEX_SCHEMA = z6.object({
|
|
8501
|
+
baseComponents: z6.array(z6.object({ name: z6.string() })).optional()
|
|
6979
8502
|
});
|
|
6980
8503
|
}
|
|
6981
8504
|
});
|
|
6982
8505
|
|
|
6983
|
-
// ../shared/src/registry-css.ts
|
|
6984
|
-
function parseCssVars(css) {
|
|
6985
|
-
const out = { theme: {}, light: {}, dark: {} };
|
|
6986
|
-
const matchBlock = (selector) => {
|
|
6987
|
-
const re = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, "g");
|
|
6988
|
-
const blocks = [];
|
|
6989
|
-
let m;
|
|
6990
|
-
while (m = re.exec(css)) blocks.push(m[1]);
|
|
6991
|
-
return blocks.join("\n");
|
|
6992
|
-
};
|
|
6993
|
-
const extract = (body) => {
|
|
6994
|
-
const result = {};
|
|
6995
|
-
const clean = body.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6996
|
-
const declRe = /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
|
|
6997
|
-
let m;
|
|
6998
|
-
while (m = declRe.exec(clean)) {
|
|
6999
|
-
result[m[1]] = m[2].replace(/\s+/g, " ").trim();
|
|
7000
|
-
}
|
|
7001
|
-
return result;
|
|
7002
|
-
};
|
|
7003
|
-
out.light = extract(matchBlock(":root"));
|
|
7004
|
-
out.dark = extract(matchBlock("\\.dark"));
|
|
7005
|
-
return out;
|
|
7006
|
-
}
|
|
7007
|
-
var init_registry_css = __esm({
|
|
7008
|
-
"../shared/src/registry-css.ts"() {
|
|
7009
|
-
"use strict";
|
|
7010
|
-
}
|
|
7011
|
-
});
|
|
7012
|
-
|
|
7013
8506
|
// ../shared/src/mcp/color-to-oklch.ts
|
|
7014
8507
|
function clamp01(n) {
|
|
7015
8508
|
return Math.min(1, Math.max(0, n));
|
|
@@ -7071,6 +8564,16 @@ function parseRgb(value) {
|
|
|
7071
8564
|
a: parseAlpha(alpha)
|
|
7072
8565
|
});
|
|
7073
8566
|
}
|
|
8567
|
+
function parseBareTriplet(value) {
|
|
8568
|
+
const parts = value.split(",").map((p) => p.trim());
|
|
8569
|
+
if (parts.length !== 3 && parts.length !== 4) return null;
|
|
8570
|
+
if (!parts.every((p) => /^\d{1,3}(\.\d+)?$/.test(p))) return null;
|
|
8571
|
+
const [r, g, b] = parts.map(Number);
|
|
8572
|
+
if (![r, g, b].every((n) => n >= 0 && n <= 255)) return null;
|
|
8573
|
+
const a = parts.length === 4 ? Number(parts[3]) : 1;
|
|
8574
|
+
if (a < 0 || a > 1) return null;
|
|
8575
|
+
return finiteRgba({ r: r / 255, g: g / 255, b: b / 255, a });
|
|
8576
|
+
}
|
|
7074
8577
|
function hueToRgb(p, q, t) {
|
|
7075
8578
|
let tt = t;
|
|
7076
8579
|
if (tt < 0) tt += 1;
|
|
@@ -7157,6 +8660,7 @@ function toOklch(value) {
|
|
|
7157
8660
|
if (v.startsWith("#")) rgba = parseHex(v);
|
|
7158
8661
|
else if (/^rgba?\(/i.test(v)) rgba = parseRgb(v);
|
|
7159
8662
|
else if (/^hsla?\(/i.test(v)) rgba = parseHsl(v);
|
|
8663
|
+
else if (v.includes(",")) rgba = parseBareTriplet(v);
|
|
7160
8664
|
return rgba ? rgbaToOklch(rgba) : null;
|
|
7161
8665
|
}
|
|
7162
8666
|
var NAMED_HEX, PASSTHROUGH_KEYWORDS, NON_COLOR_KEYWORDS;
|
|
@@ -7355,7 +8859,7 @@ function normalizeMap(raw) {
|
|
|
7355
8859
|
}
|
|
7356
8860
|
return { tokens, kinds };
|
|
7357
8861
|
}
|
|
7358
|
-
function
|
|
8862
|
+
function collectCssVars(css) {
|
|
7359
8863
|
const { light: rootLight, dark: rootDark } = parseCssVars(css);
|
|
7360
8864
|
const themeVars = extractThemeBlock(css);
|
|
7361
8865
|
const mergedLight = {};
|
|
@@ -7363,8 +8867,11 @@ function extractDesignSystem(css, name) {
|
|
|
7363
8867
|
mergedLight[denamespace(k)] = v;
|
|
7364
8868
|
}
|
|
7365
8869
|
Object.assign(mergedLight, rootLight);
|
|
7366
|
-
|
|
7367
|
-
|
|
8870
|
+
return { light: mergedLight, dark: rootDark };
|
|
8871
|
+
}
|
|
8872
|
+
function buildExtractionResult(raw, name) {
|
|
8873
|
+
const light = normalizeMap(raw.light);
|
|
8874
|
+
const dark = normalizeMap(raw.dark);
|
|
7368
8875
|
const hasLight = Object.keys(light.tokens).length > 0;
|
|
7369
8876
|
const hasDark = Object.keys(dark.tokens).length > 0;
|
|
7370
8877
|
const themeMode = hasLight && hasDark ? "dual" : hasDark ? "dark" : hasLight ? "light" : "none";
|
|
@@ -7429,6 +8936,8 @@ function formatExtraction(result) {
|
|
|
7429
8936
|
};
|
|
7430
8937
|
const meta = [
|
|
7431
8938
|
result.name ? `**Source:** ${result.name}` : null,
|
|
8939
|
+
result.sources && result.sources.length > 1 ? `**Merged from:** ${result.sources.join(", ")}` : null,
|
|
8940
|
+
result.aliased && result.aliased.length ? `**Vendor names mapped:** ${result.aliased.map(([f, t]) => `--${f} \u2192 --${t}`).join(", ")}` : null,
|
|
7432
8941
|
`**Theme:** ${modeLabel[result.themeMode]}`,
|
|
7433
8942
|
`**Tokens extracted:** ${result.tokenCount}`,
|
|
7434
8943
|
`**Colors normalized to oklch.**`
|
|
@@ -7455,7 +8964,7 @@ function formatExtraction(result) {
|
|
|
7455
8964
|
}
|
|
7456
8965
|
sections.push(
|
|
7457
8966
|
"## Token sheet (oklch-normalized, paste into globals.css)\n\n```css\n" + blocks.join("\n\n") + "\n```",
|
|
7458
|
-
"## How to use\n- Style new components against these tokens (`bg-background`, `text-foreground`, `bg-primary`, `border-border`, `ring-ring`) so they match the existing app.\n- To turn this into a full Better Design system, pass the
|
|
8967
|
+
"## How to use\n- Style new components against these tokens (`bg-background`, `text-foreground`, `bg-primary`, `border-border`, `ring-ring`) so they match the existing app.\n- To turn this into a full Better Design system, pass the token sheet above (the fenced css block, verbatim) to `create-design-system` as `brandCss`. Generation then builds FROM this brand instead of a catalog look-alike."
|
|
7459
8968
|
);
|
|
7460
8969
|
return sections.join("\n\n");
|
|
7461
8970
|
}
|
|
@@ -7506,9 +9015,198 @@ var init_extract_design_system = __esm({
|
|
|
7506
9015
|
}
|
|
7507
9016
|
});
|
|
7508
9017
|
|
|
9018
|
+
// ../shared/src/mcp/brand-sources.ts
|
|
9019
|
+
function inferSourceKind(content) {
|
|
9020
|
+
const t = content.trim();
|
|
9021
|
+
if (!t.startsWith("{") && !t.startsWith("[")) return "css";
|
|
9022
|
+
let parsed;
|
|
9023
|
+
try {
|
|
9024
|
+
parsed = JSON.parse(t);
|
|
9025
|
+
} catch {
|
|
9026
|
+
return "css";
|
|
9027
|
+
}
|
|
9028
|
+
if (t.includes('"$value"')) return "tokens";
|
|
9029
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
9030
|
+
const keys = Object.keys(parsed);
|
|
9031
|
+
if (keys.some((k) => COMPUTED_ROLES.has(k))) return "computed";
|
|
9032
|
+
}
|
|
9033
|
+
return "tokens";
|
|
9034
|
+
}
|
|
9035
|
+
function buildRoleSampleScript() {
|
|
9036
|
+
const entries = Object.keys(ROLE_MAP).map((role) => {
|
|
9037
|
+
const selector = ROLE_SELECTORS[role];
|
|
9038
|
+
const propList = JSON.stringify(Object.keys(ROLE_MAP[role]));
|
|
9039
|
+
return ` ${role}: pick(document.querySelector(${JSON.stringify(selector)}), ${propList}),`;
|
|
9040
|
+
}).join("\n");
|
|
9041
|
+
return `(() => {
|
|
9042
|
+
const pick = (el, props) => {
|
|
9043
|
+
if (!el) return null;
|
|
9044
|
+
const cs = getComputedStyle(el);
|
|
9045
|
+
return Object.fromEntries(props.map((p) => [p, cs[p]]));
|
|
9046
|
+
};
|
|
9047
|
+
return {
|
|
9048
|
+
${entries}
|
|
9049
|
+
};
|
|
9050
|
+
})()`;
|
|
9051
|
+
}
|
|
9052
|
+
function parseJsonOrNull(content) {
|
|
9053
|
+
try {
|
|
9054
|
+
return JSON.parse(content);
|
|
9055
|
+
} catch {
|
|
9056
|
+
return null;
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
function computedToVars(content) {
|
|
9060
|
+
const out = {};
|
|
9061
|
+
const roles = parseJsonOrNull(content);
|
|
9062
|
+
if (!roles || typeof roles !== "object") return out;
|
|
9063
|
+
for (const [role, styles] of Object.entries(roles)) {
|
|
9064
|
+
if (!COMPUTED_ROLES.has(role) || !styles || typeof styles !== "object") continue;
|
|
9065
|
+
const map = ROLE_MAP[role];
|
|
9066
|
+
for (const [prop, token] of Object.entries(map)) {
|
|
9067
|
+
const value = styles[prop];
|
|
9068
|
+
if (typeof value === "string" && value.trim()) out[token] = value.trim();
|
|
9069
|
+
}
|
|
9070
|
+
}
|
|
9071
|
+
if (out.border) out.input = out.border;
|
|
9072
|
+
return out;
|
|
9073
|
+
}
|
|
9074
|
+
function figmaColorToRgbString(v) {
|
|
9075
|
+
const { r, g, b, a } = v;
|
|
9076
|
+
if (typeof r !== "number" || typeof g !== "number" || typeof b !== "number") return null;
|
|
9077
|
+
const to255 = (n) => Math.round(Math.min(1, Math.max(0, n)) * 255);
|
|
9078
|
+
const alpha = typeof a === "number" && a < 1 ? `, ${a}` : "";
|
|
9079
|
+
return `rgb(${to255(r)}, ${to255(g)}, ${to255(b)}${alpha})`;
|
|
9080
|
+
}
|
|
9081
|
+
function kebab(part) {
|
|
9082
|
+
return part.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
9083
|
+
}
|
|
9084
|
+
function tokensToVars(content) {
|
|
9085
|
+
const out = {};
|
|
9086
|
+
const walk = (node, path5) => {
|
|
9087
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) return;
|
|
9088
|
+
const obj = node;
|
|
9089
|
+
const value = "$value" in obj ? obj.$value : "value" in obj && "type" in obj ? obj.value : void 0;
|
|
9090
|
+
if (value !== void 0) {
|
|
9091
|
+
const name = path5.map(kebab).join("-");
|
|
9092
|
+
if (!name) return;
|
|
9093
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
9094
|
+
out[name] = String(value);
|
|
9095
|
+
} else if (Array.isArray(value) && value.every((x) => typeof x === "string")) {
|
|
9096
|
+
out[name] = value.join(", ");
|
|
9097
|
+
} else if (value && typeof value === "object") {
|
|
9098
|
+
const rgb = figmaColorToRgbString(value);
|
|
9099
|
+
if (rgb) out[name] = rgb;
|
|
9100
|
+
}
|
|
9101
|
+
return;
|
|
9102
|
+
}
|
|
9103
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
9104
|
+
if (k.startsWith("$")) continue;
|
|
9105
|
+
walk(v, [...path5, k]);
|
|
9106
|
+
}
|
|
9107
|
+
};
|
|
9108
|
+
walk(parseJsonOrNull(content), []);
|
|
9109
|
+
return out;
|
|
9110
|
+
}
|
|
9111
|
+
function applyAliases(vars) {
|
|
9112
|
+
const applied = [];
|
|
9113
|
+
for (const [from, to] of SEMANTIC_ALIASES) {
|
|
9114
|
+
if (vars[from] !== void 0 && vars[to] === void 0) {
|
|
9115
|
+
vars[to] = vars[from];
|
|
9116
|
+
applied.push([from, to]);
|
|
9117
|
+
}
|
|
9118
|
+
}
|
|
9119
|
+
return applied;
|
|
9120
|
+
}
|
|
9121
|
+
function extractFromSources(sources, name) {
|
|
9122
|
+
const light = {};
|
|
9123
|
+
const dark = {};
|
|
9124
|
+
for (const source of sources) {
|
|
9125
|
+
const kind = source.kind ?? inferSourceKind(source.content);
|
|
9126
|
+
if (kind === "css") {
|
|
9127
|
+
const parsed = collectCssVars(source.content);
|
|
9128
|
+
Object.assign(light, parsed.light);
|
|
9129
|
+
Object.assign(dark, parsed.dark);
|
|
9130
|
+
} else if (kind === "computed") {
|
|
9131
|
+
Object.assign(light, computedToVars(source.content));
|
|
9132
|
+
} else {
|
|
9133
|
+
Object.assign(light, tokensToVars(source.content));
|
|
9134
|
+
}
|
|
9135
|
+
}
|
|
9136
|
+
const aliased = applyAliases(light);
|
|
9137
|
+
for (const pair of applyAliases(dark)) {
|
|
9138
|
+
if (!aliased.some(([f, t]) => f === pair[0] && t === pair[1])) aliased.push(pair);
|
|
9139
|
+
}
|
|
9140
|
+
const result = buildExtractionResult({ light, dark }, name);
|
|
9141
|
+
result.sources = sources.map((s) => s.name);
|
|
9142
|
+
if (aliased.length) result.aliased = aliased;
|
|
9143
|
+
return result;
|
|
9144
|
+
}
|
|
9145
|
+
var ROLE_MAP, COMPUTED_ROLES, ROLE_SELECTORS, SEMANTIC_ALIASES;
|
|
9146
|
+
var init_brand_sources = __esm({
|
|
9147
|
+
"../shared/src/mcp/brand-sources.ts"() {
|
|
9148
|
+
"use strict";
|
|
9149
|
+
init_extract_design_system();
|
|
9150
|
+
ROLE_MAP = {
|
|
9151
|
+
body: { backgroundColor: "background", color: "foreground", fontFamily: "font-sans" },
|
|
9152
|
+
heading: { fontFamily: "font-heading" },
|
|
9153
|
+
// The link color is the closest rendered stand-in for an accent. On sites
|
|
9154
|
+
// where links reuse the primary, the alias fill leaves primary untouched, so
|
|
9155
|
+
// this can only add information, never fight the button's own sample.
|
|
9156
|
+
link: { color: "accent" },
|
|
9157
|
+
button: {
|
|
9158
|
+
backgroundColor: "primary",
|
|
9159
|
+
color: "primary-foreground",
|
|
9160
|
+
borderRadius: "radius-button"
|
|
9161
|
+
},
|
|
9162
|
+
input: { borderColor: "border", borderRadius: "radius-input" },
|
|
9163
|
+
card: {
|
|
9164
|
+
backgroundColor: "card",
|
|
9165
|
+
color: "card-foreground",
|
|
9166
|
+
borderRadius: "radius-card",
|
|
9167
|
+
boxShadow: "shadow-card"
|
|
9168
|
+
}
|
|
9169
|
+
};
|
|
9170
|
+
COMPUTED_ROLES = new Set(Object.keys(ROLE_MAP));
|
|
9171
|
+
ROLE_SELECTORS = {
|
|
9172
|
+
body: "body",
|
|
9173
|
+
heading: "h1,h2",
|
|
9174
|
+
link: "a[href]",
|
|
9175
|
+
button: "button,[role=button],.btn,input[type=submit]",
|
|
9176
|
+
input: "input:not([type=hidden]),textarea,select",
|
|
9177
|
+
card: "[class*=card]"
|
|
9178
|
+
};
|
|
9179
|
+
SEMANTIC_ALIASES = [
|
|
9180
|
+
["color-background", "background"],
|
|
9181
|
+
["color-foreground", "foreground"],
|
|
9182
|
+
["color-button", "primary"],
|
|
9183
|
+
["color-button-text", "primary-foreground"],
|
|
9184
|
+
["color-secondary-button", "secondary"],
|
|
9185
|
+
["color-secondary-button-text", "secondary-foreground"],
|
|
9186
|
+
["color-link", "accent"],
|
|
9187
|
+
["font-body-family", "font-sans"],
|
|
9188
|
+
["button", "primary"],
|
|
9189
|
+
["button-text", "primary-foreground"]
|
|
9190
|
+
];
|
|
9191
|
+
}
|
|
9192
|
+
});
|
|
9193
|
+
|
|
7509
9194
|
// ../shared/src/mcp/tools/extract-tools.ts
|
|
7510
|
-
import { z as
|
|
7511
|
-
function
|
|
9195
|
+
import { z as z7 } from "zod";
|
|
9196
|
+
function authHeaders2(authContext) {
|
|
9197
|
+
if (!authContext) return {};
|
|
9198
|
+
const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
|
|
9199
|
+
return { Authorization: `Bearer ${bearer2}` };
|
|
9200
|
+
}
|
|
9201
|
+
function redactUrl(raw) {
|
|
9202
|
+
try {
|
|
9203
|
+
const u = new URL(raw);
|
|
9204
|
+
return `${u.origin}${u.pathname}`;
|
|
9205
|
+
} catch {
|
|
9206
|
+
return "[redacted]";
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
9209
|
+
function registerExtractTools({ server, tracked, authContext, apiFetch = globalThis.fetch }) {
|
|
7512
9210
|
server.registerTool(
|
|
7513
9211
|
"extract-design-system",
|
|
7514
9212
|
{
|
|
@@ -7519,32 +9217,100 @@ Use this when the user says "match our existing style", "use our design system",
|
|
|
7519
9217
|
|
|
7520
9218
|
How to use it: read the project's \`globals.css\` (or wherever the design tokens / CSS custom properties live, e.g. a Tailwind v4 \`@theme\` block) and pass its full contents as \`css\`. The MCP server cannot read your filesystem, so you must paste the text.
|
|
7521
9219
|
|
|
7522
|
-
Returns: the semantic palette (primary, background, foreground, accent, border, ring, etc.), radius, fonts, and shadow scale, plus a pasteable token sheet with every color normalized to \`oklch()\`. Handles hex, rgb()/rgba(), hsl()/hsla(), named colors, and existing oklch values, across \`:root\`, \`.dark\`, and \`@theme\` blocks
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
9220
|
+
Returns: the semantic palette (primary, background, foreground, accent, border, ring, etc.), radius, fonts, and shadow scale, plus a pasteable token sheet with every color normalized to \`oklch()\`. Handles hex, rgb()/rgba(), hsl()/hsla(), named colors, bare Shopify "r,g,b" triplets, and existing oklch values, across \`:root\`, \`.dark\`, and \`@theme\` blocks.
|
|
9221
|
+
|
|
9222
|
+
Beyond pasted CSS, \`sources\` accepts any mix of inputs and merges them (later sources win):
|
|
9223
|
+
- kind "css": any stylesheet text, same as the \`css\` param.
|
|
9224
|
+
- kind "computed": a JSON sample of rendered styles read off the LIVE site, which beats variable-name guessing because it records what elements actually look like. Gather it by running this in the site's browser console and passing the printed JSON:
|
|
9225
|
+
\`\`\`js
|
|
9226
|
+
JSON.stringify(${buildRoleSampleScript()})
|
|
9227
|
+
\`\`\`
|
|
9228
|
+
- kind "tokens": a design-tokens JSON export ($value entries, W3C format or the Figma variables API shape with 0..1 rgb floats).
|
|
9229
|
+
|
|
9230
|
+
Common vendor names (Shopify's --color-button family) are mapped into empty semantic slots automatically and the mapping is reported in the output.`,
|
|
9231
|
+
inputSchema: z7.object({
|
|
9232
|
+
css: z7.string().trim().min(1).max(2e5).optional().describe("Full contents of the project's globals.css (or any CSS holding the design tokens / CSS custom properties). Optional when `sources` is given."),
|
|
9233
|
+
sources: z7.array(z7.object({
|
|
9234
|
+
name: z7.string().trim().min(1).max(120).describe("Label for this source, e.g. 'globals.css', 'live site sample', 'figma variables'."),
|
|
9235
|
+
kind: z7.enum(["css", "computed", "tokens"]).optional().describe("Omit to infer: JSON with $value entries is tokens, JSON keyed by UI roles is computed, anything else is CSS."),
|
|
9236
|
+
content: z7.string().trim().min(1).max(2e5)
|
|
9237
|
+
})).max(10).optional().describe("Any mix of brand sources, merged in order (later sources win). See the tool description for how to gather each kind."),
|
|
9238
|
+
name: z7.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system, e.g. the app or repo name."),
|
|
9239
|
+
maxTokens: z7.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
|
|
7527
9240
|
}),
|
|
7528
|
-
annotations:
|
|
9241
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
7529
9242
|
},
|
|
7530
|
-
async ({ css, name, maxTokens }) => tracked("extract-design-system", { name: name ?? null }, async () => {
|
|
7531
|
-
const
|
|
9243
|
+
async ({ css, sources, name, maxTokens }) => tracked("extract-design-system", { name: name ?? null, sourceCount: (sources?.length ?? 0) + (css ? 1 : 0) }, async () => {
|
|
9244
|
+
const all = [
|
|
9245
|
+
...css ? [{ name: "pasted css", kind: "css", content: css }] : [],
|
|
9246
|
+
...sources ?? []
|
|
9247
|
+
];
|
|
9248
|
+
if (all.length === 0) {
|
|
9249
|
+
return {
|
|
9250
|
+
content: [{
|
|
9251
|
+
type: "text",
|
|
9252
|
+
text: "Nothing to extract: pass `css`, or one or more `sources`."
|
|
9253
|
+
}]
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9256
|
+
const result = extractFromSources(all, name);
|
|
7532
9257
|
let text = formatExtraction(result);
|
|
7533
9258
|
if (maxTokens) text = truncateToTokens(text, maxTokens);
|
|
7534
9259
|
return { content: [{ type: "text", text }] };
|
|
7535
9260
|
})
|
|
7536
9261
|
);
|
|
9262
|
+
server.registerTool(
|
|
9263
|
+
"extract-from-url",
|
|
9264
|
+
{
|
|
9265
|
+
title: "Extract Design System From a Live Site",
|
|
9266
|
+
description: `Point Better Design at a URL and get the site's brand back as an oklch token sheet, without pasting anything.
|
|
9267
|
+
|
|
9268
|
+
Use this when the user names their site ("match uclips.eu", "use our website's style") or hands you any public URL whose look should be reused. Server side, Better Design fetches the page's stylesheets AND renders the page in a headless browser to sample what role elements actually look like (body, heading, link, button, input, card), then merges both, rendered truth last. That reads the brand even when the site's variable names are unrecognizable.
|
|
9269
|
+
|
|
9270
|
+
Slow by nature (a real browser loads the page): expect 10 to 45 seconds. For CSS the user already has locally, prefer extract-design-system, which is instant.`,
|
|
9271
|
+
inputSchema: z7.object({
|
|
9272
|
+
url: z7.string().trim().min(8).max(2048).describe("Public http(s) URL of the page to read, usually the homepage."),
|
|
9273
|
+
name: z7.string().trim().min(1).max(120).optional().describe("Optional label for the extracted system. Defaults to the site's hostname."),
|
|
9274
|
+
maxTokens: z7.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates the token sheet to fit.")
|
|
9275
|
+
}),
|
|
9276
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
9277
|
+
},
|
|
9278
|
+
async ({ url, name, maxTokens }) => tracked("extract-from-url", { query: redactUrl(url) }, async () => {
|
|
9279
|
+
const params = new URLSearchParams({ url });
|
|
9280
|
+
if (name) params.set("name", name);
|
|
9281
|
+
const res = await apiFetch(`${APP_BASE_URL}/api/v1/brand-extract/from-url?${params}`, {
|
|
9282
|
+
headers: authHeaders2(authContext),
|
|
9283
|
+
signal: AbortSignal.timeout(9e4)
|
|
9284
|
+
});
|
|
9285
|
+
if (!res.ok) {
|
|
9286
|
+
const body2 = await res.json().catch(() => ({}));
|
|
9287
|
+
return {
|
|
9288
|
+
content: [{
|
|
9289
|
+
type: "text",
|
|
9290
|
+
text: `Could not extract from that URL (HTTP ${res.status}). ${body2.message ?? ""}`.trim()
|
|
9291
|
+
}]
|
|
9292
|
+
};
|
|
9293
|
+
}
|
|
9294
|
+
const body = await res.json();
|
|
9295
|
+
let text = body.notes?.length ? `> ${body.notes.join("\n> ")}
|
|
9296
|
+
|
|
9297
|
+
${body.markdown}` : body.markdown;
|
|
9298
|
+
if (maxTokens) text = truncateToTokens(text, maxTokens);
|
|
9299
|
+
return { content: [{ type: "text", text }] };
|
|
9300
|
+
})
|
|
9301
|
+
);
|
|
7537
9302
|
}
|
|
7538
9303
|
var init_extract_tools = __esm({
|
|
7539
9304
|
"../shared/src/mcp/tools/extract-tools.ts"() {
|
|
7540
9305
|
"use strict";
|
|
7541
9306
|
init_shared();
|
|
7542
9307
|
init_extract_design_system();
|
|
9308
|
+
init_brand_sources();
|
|
7543
9309
|
}
|
|
7544
9310
|
});
|
|
7545
9311
|
|
|
7546
9312
|
// ../shared/src/mcp/tools/illustration-tools.ts
|
|
7547
|
-
import { z as
|
|
9313
|
+
import { z as z8 } from "zod";
|
|
7548
9314
|
function fail(message) {
|
|
7549
9315
|
return {
|
|
7550
9316
|
content: [{ type: "text", text: message }],
|
|
@@ -7558,8 +9324,14 @@ function fail(message) {
|
|
|
7558
9324
|
}
|
|
7559
9325
|
};
|
|
7560
9326
|
}
|
|
7561
|
-
function registerIllustrationTools({
|
|
7562
|
-
|
|
9327
|
+
function registerIllustrationTools({
|
|
9328
|
+
server,
|
|
9329
|
+
track,
|
|
9330
|
+
tracked,
|
|
9331
|
+
authContext,
|
|
9332
|
+
apiFetch = globalThis.fetch
|
|
9333
|
+
}) {
|
|
9334
|
+
const authHeaders3 = () => {
|
|
7563
9335
|
const headers = { "Content-Type": "application/json" };
|
|
7564
9336
|
if (authContext) {
|
|
7565
9337
|
const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
|
|
@@ -7584,14 +9356,14 @@ How to call it:
|
|
|
7584
9356
|
4. Stop when \`faults\` is empty.
|
|
7585
9357
|
|
|
7586
9358
|
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.`,
|
|
7587
|
-
inputSchema:
|
|
7588
|
-
svg:
|
|
7589
|
-
intent:
|
|
7590
|
-
animated:
|
|
7591
|
-
reviewToken:
|
|
9359
|
+
inputSchema: z8.object({
|
|
9360
|
+
svg: z8.string().min(1).describe("The complete svg element you drew."),
|
|
9361
|
+
intent: z8.string().min(1).describe('What it is meant to be, in plain words. For example "an open inbox tray with letters".'),
|
|
9362
|
+
animated: z8.boolean().optional().describe("Set when the drawing was meant to move, so the motion checks apply."),
|
|
9363
|
+
reviewToken: z8.string().optional().describe("The token from a previous review of this drawing. Reviews it again free.")
|
|
7592
9364
|
}),
|
|
7593
9365
|
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
7594
|
-
annotations:
|
|
9366
|
+
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
7595
9367
|
},
|
|
7596
9368
|
async ({
|
|
7597
9369
|
svg,
|
|
@@ -7603,9 +9375,9 @@ Markup carrying script, event handlers, remote references, or styles that reach
|
|
|
7603
9375
|
{ query: intent, animated, repeat: Boolean(reviewToken) },
|
|
7604
9376
|
async () => {
|
|
7605
9377
|
try {
|
|
7606
|
-
const res = await
|
|
9378
|
+
const res = await apiFetch(`${APP_BASE_URL}/api/illustrations/review`, {
|
|
7607
9379
|
method: "POST",
|
|
7608
|
-
headers:
|
|
9380
|
+
headers: authHeaders3(),
|
|
7609
9381
|
signal: AbortSignal.timeout(6e4),
|
|
7610
9382
|
body: JSON.stringify({ svg, intent, animated: animated ?? false, reviewToken })
|
|
7611
9383
|
});
|
|
@@ -7657,16 +9429,16 @@ var init_illustration_tools = __esm({
|
|
|
7657
9429
|
init_shared();
|
|
7658
9430
|
REVIEW_OUTPUT_SCHEMA = {
|
|
7659
9431
|
/** Empty when nothing is visibly wrong. */
|
|
7660
|
-
faults:
|
|
9432
|
+
faults: z8.array(z8.string()),
|
|
7661
9433
|
/** Group ids in paint order, the parts an animation can target. */
|
|
7662
|
-
layers:
|
|
9434
|
+
layers: z8.array(z8.string()),
|
|
7663
9435
|
/** False when the markup was refused; faults says why. */
|
|
7664
|
-
reviewed:
|
|
9436
|
+
reviewed: z8.boolean(),
|
|
7665
9437
|
/** True when the drawing could not be rendered, so only structure was checked. */
|
|
7666
|
-
unseen:
|
|
9438
|
+
unseen: z8.boolean(),
|
|
7667
9439
|
/** Pass this back to review the same drawing again without spending another credit. */
|
|
7668
|
-
reviewToken:
|
|
7669
|
-
message:
|
|
9440
|
+
reviewToken: z8.string(),
|
|
9441
|
+
message: z8.string()
|
|
7670
9442
|
};
|
|
7671
9443
|
}
|
|
7672
9444
|
});
|
|
@@ -7703,15 +9475,13 @@ var init_preview_resource = __esm({
|
|
|
7703
9475
|
prefersBorder: true,
|
|
7704
9476
|
csp: {
|
|
7705
9477
|
connectDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7706
|
-
resourceDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS]
|
|
7707
|
-
frameDomains: WIDGET_FRAME_DOMAINS
|
|
9478
|
+
resourceDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS]
|
|
7708
9479
|
},
|
|
7709
9480
|
domain: WIDGET_APP_DOMAIN
|
|
7710
9481
|
},
|
|
7711
9482
|
"openai/widgetCSP": {
|
|
7712
9483
|
connect_domains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7713
9484
|
resource_domains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7714
|
-
frame_domains: WIDGET_FRAME_DOMAINS,
|
|
7715
9485
|
redirect_domains: [APP_BASE_URL]
|
|
7716
9486
|
}
|
|
7717
9487
|
};
|
|
@@ -7719,9 +9489,9 @@ var init_preview_resource = __esm({
|
|
|
7719
9489
|
});
|
|
7720
9490
|
|
|
7721
9491
|
// ../shared/src/mcp/tools.ts
|
|
7722
|
-
function registerAllTools(server, provider, onToolUsage, authContext, options = {}) {
|
|
9492
|
+
function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId) {
|
|
7723
9493
|
const { track, tracked } = createTracking(server, onToolUsage);
|
|
7724
|
-
const deps = { server, provider, track, tracked, authContext };
|
|
9494
|
+
const deps = { server, provider, track, tracked, authContext, apiFetch, userId };
|
|
7725
9495
|
registerPrincipleTools(deps, options);
|
|
7726
9496
|
registerComprehensionTools(deps);
|
|
7727
9497
|
registerIconTools(deps);
|
|
@@ -7749,9 +9519,22 @@ var init_tools = __esm({
|
|
|
7749
9519
|
var mcp_exports = {};
|
|
7750
9520
|
__export(mcp_exports, {
|
|
7751
9521
|
BUILD_TARGET_RULE: () => BUILD_TARGET_RULE,
|
|
9522
|
+
GUIDANCE_TOOLS: () => GUIDANCE_TOOLS,
|
|
7752
9523
|
INSTRUCTIONS: () => INSTRUCTIONS,
|
|
9524
|
+
MCP_SESSION_WINDOW_MS: () => MCP_SESSION_WINDOW_MS,
|
|
9525
|
+
REVIEW_TOOLS: () => REVIEW_TOOLS,
|
|
7753
9526
|
SEARCH_TOOLS: () => SEARCH_TOOLS,
|
|
7754
|
-
|
|
9527
|
+
SESSION_LOOP_TOOLS: () => SESSION_LOOP_TOOLS,
|
|
9528
|
+
SHOPIFY_PRIMITIVE_CONTRACT_VERSION: () => SHOPIFY_PRIMITIVE_CONTRACT_VERSION,
|
|
9529
|
+
buildShopifyPrimitiveSet: () => buildShopifyPrimitiveSet,
|
|
9530
|
+
createMcpServer: () => createMcpServer,
|
|
9531
|
+
formatTopicList: () => formatTopicList,
|
|
9532
|
+
guidancePreamble: () => guidancePreamble,
|
|
9533
|
+
parseShopifyPrimitiveSet: () => parseShopifyPrimitiveSet,
|
|
9534
|
+
reviewNudge: () => reviewNudge,
|
|
9535
|
+
reviewSectionForTopic: () => reviewSectionForTopic,
|
|
9536
|
+
summarizeSessionGuidance: () => summarizeSessionGuidance,
|
|
9537
|
+
weightReviewRules: () => weightReviewRules
|
|
7755
9538
|
});
|
|
7756
9539
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7757
9540
|
function createMcpServer(options) {
|
|
@@ -7763,17 +9546,27 @@ function createMcpServer(options) {
|
|
|
7763
9546
|
authContext,
|
|
7764
9547
|
enableReactNativeGuide = true,
|
|
7765
9548
|
enableThreeJsGuide = true,
|
|
7766
|
-
enableShopifyKit = true
|
|
9549
|
+
enableShopifyKit = true,
|
|
9550
|
+
apiFetch = globalThis.fetch,
|
|
9551
|
+
userId
|
|
7767
9552
|
} = options;
|
|
7768
9553
|
const server = new McpServer(
|
|
7769
9554
|
{ name, version },
|
|
7770
9555
|
{ instructions: getMcpInstructions({ enableReactNativeGuide, enableThreeJsGuide }) }
|
|
7771
9556
|
);
|
|
7772
|
-
registerAllTools(
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
9557
|
+
registerAllTools(
|
|
9558
|
+
server,
|
|
9559
|
+
provider,
|
|
9560
|
+
onToolUsage,
|
|
9561
|
+
authContext,
|
|
9562
|
+
{
|
|
9563
|
+
enableReactNativeGuide,
|
|
9564
|
+
enableThreeJsGuide,
|
|
9565
|
+
enableShopifyKit
|
|
9566
|
+
},
|
|
9567
|
+
apiFetch,
|
|
9568
|
+
userId
|
|
9569
|
+
);
|
|
7777
9570
|
return server;
|
|
7778
9571
|
}
|
|
7779
9572
|
var init_mcp = __esm({
|
|
@@ -7783,7 +9576,9 @@ var init_mcp = __esm({
|
|
|
7783
9576
|
init_tools();
|
|
7784
9577
|
init_tools();
|
|
7785
9578
|
init_instructions();
|
|
9579
|
+
init_session_guidance();
|
|
7786
9580
|
init_shared();
|
|
9581
|
+
init_shopify_primitives();
|
|
7787
9582
|
}
|
|
7788
9583
|
});
|
|
7789
9584
|
|
|
@@ -7793,7 +9588,9 @@ __export(api_client_exports, {
|
|
|
7793
9588
|
getDesignSystem: () => getDesignSystem,
|
|
7794
9589
|
getDesignSystemComponents: () => getDesignSystemComponents,
|
|
7795
9590
|
getPrinciple: () => getPrinciple,
|
|
9591
|
+
getProjectShopifyPrimitives: () => getProjectShopifyPrimitives,
|
|
7796
9592
|
getReviewRules: () => getReviewRules,
|
|
9593
|
+
getSessionGuidance: () => getSessionGuidance,
|
|
7797
9594
|
listPrinciples: () => listPrinciples,
|
|
7798
9595
|
searchDesignSystems: () => searchDesignSystems,
|
|
7799
9596
|
searchIconLibraries: () => searchIconLibraries,
|
|
@@ -7828,11 +9625,14 @@ async function searchDesignSystems(query, limit = 5) {
|
|
|
7828
9625
|
`/v1/design-systems/search?q=${encodeURIComponent(query)}&limit=${limit}`
|
|
7829
9626
|
);
|
|
7830
9627
|
}
|
|
7831
|
-
async function getDesignSystem(id,
|
|
7832
|
-
const params =
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
9628
|
+
async function getDesignSystem(id, options = {}) {
|
|
9629
|
+
const params = new URLSearchParams();
|
|
9630
|
+
if (options.includeHtml) params.set("includeHtml", "true");
|
|
9631
|
+
if (options.includeShopifyPrimitives) {
|
|
9632
|
+
params.set("includeShopifyPrimitives", "true");
|
|
9633
|
+
}
|
|
9634
|
+
const query = params.size ? `?${params}` : "";
|
|
9635
|
+
return request(`/v1/design-systems/${encodeURIComponent(id)}${query}`);
|
|
7836
9636
|
}
|
|
7837
9637
|
async function getDesignSystemComponents(id, names, includeCode = true) {
|
|
7838
9638
|
const params = new URLSearchParams();
|
|
@@ -7843,6 +9643,9 @@ async function getDesignSystemComponents(id, names, includeCode = true) {
|
|
|
7843
9643
|
`/v1/design-systems/${encodeURIComponent(id)}/components${query}`
|
|
7844
9644
|
);
|
|
7845
9645
|
}
|
|
9646
|
+
async function getProjectShopifyPrimitives(projectId) {
|
|
9647
|
+
return request(`/projects/${encodeURIComponent(projectId)}/shopify/all.json`);
|
|
9648
|
+
}
|
|
7846
9649
|
async function listPrinciples(area) {
|
|
7847
9650
|
const params = area ? `?area=${area}` : "";
|
|
7848
9651
|
return request(`/v1/principles${params}`);
|
|
@@ -7874,13 +9677,20 @@ async function getReviewRules(category = "all") {
|
|
|
7874
9677
|
const params = category !== "all" ? `?category=${encodeURIComponent(category)}` : "";
|
|
7875
9678
|
return request(`/v1/review-rules${params}`);
|
|
7876
9679
|
}
|
|
9680
|
+
async function getSessionGuidance(windowMs) {
|
|
9681
|
+
const params = new URLSearchParams({ windowMs: String(Math.round(windowMs)) });
|
|
9682
|
+
return request(`/v1/session-guidance?${params}`);
|
|
9683
|
+
}
|
|
7877
9684
|
function postAlert(body) {
|
|
7878
9685
|
fetch(`${API_URL2}/api/internal/alert`, {
|
|
7879
9686
|
method: "POST",
|
|
7880
9687
|
headers: { "Content-Type": "application/json" },
|
|
7881
9688
|
body: JSON.stringify(body),
|
|
7882
9689
|
signal: AbortSignal.timeout(5e3)
|
|
7883
|
-
}).
|
|
9690
|
+
}).then((res) => {
|
|
9691
|
+
if (!res.ok) console.warn(`postAlert failed: HTTP ${res.status}`);
|
|
9692
|
+
}).catch((err) => {
|
|
9693
|
+
console.warn("postAlert failed", err);
|
|
7884
9694
|
});
|
|
7885
9695
|
}
|
|
7886
9696
|
function trackToolUsage(tool, params, context = {}) {
|
|
@@ -7937,17 +9747,36 @@ function createHttpDataProvider() {
|
|
|
7937
9747
|
return results;
|
|
7938
9748
|
},
|
|
7939
9749
|
async getDesignSystemById(id, options = {}) {
|
|
7940
|
-
const systemRes = await getDesignSystem(id,
|
|
9750
|
+
const systemRes = await getDesignSystem(id, {
|
|
9751
|
+
includeHtml: options.includeHtml,
|
|
9752
|
+
includeShopifyPrimitives: options.includeShopifyPrimitives
|
|
9753
|
+
});
|
|
7941
9754
|
const componentsRes = options.includeComponents === false ? { components: [] } : await getDesignSystemComponents(
|
|
7942
9755
|
id,
|
|
7943
9756
|
options.componentNames,
|
|
7944
9757
|
options.includeComponentCode
|
|
7945
9758
|
);
|
|
7946
9759
|
return {
|
|
7947
|
-
|
|
9760
|
+
// Spread the field only when the API sent it. Assigning it
|
|
9761
|
+
// unconditionally would put `shopifyAccess: undefined` on every
|
|
9762
|
+
// response, so `"shopifyAccess" in metadata` reads true for callers who
|
|
9763
|
+
// never asked about Shopify, breaking the absent-unless-requested
|
|
9764
|
+
// contract the field documents. [BD-421]
|
|
9765
|
+
metadata: {
|
|
9766
|
+
...systemRes.metadata,
|
|
9767
|
+
...systemRes.shopifyAccess === void 0 ? {} : { shopifyAccess: systemRes.shopifyAccess }
|
|
9768
|
+
},
|
|
7948
9769
|
components: componentsRes.components
|
|
7949
9770
|
};
|
|
7950
9771
|
},
|
|
9772
|
+
async getProjectShopifyPrimitives(projectId) {
|
|
9773
|
+
try {
|
|
9774
|
+
return await getProjectShopifyPrimitives(projectId);
|
|
9775
|
+
} catch (err) {
|
|
9776
|
+
console.warn(`getProjectShopifyPrimitives(${projectId}) failed`, err);
|
|
9777
|
+
return null;
|
|
9778
|
+
}
|
|
9779
|
+
},
|
|
7951
9780
|
async listPrinciples(area) {
|
|
7952
9781
|
const { principles } = await listPrinciples(area);
|
|
7953
9782
|
return principles;
|
|
@@ -7955,7 +9784,8 @@ function createHttpDataProvider() {
|
|
|
7955
9784
|
async getPrincipleByTopic(topic, area) {
|
|
7956
9785
|
try {
|
|
7957
9786
|
return await getPrinciple(topic, area);
|
|
7958
|
-
} catch {
|
|
9787
|
+
} catch (err) {
|
|
9788
|
+
console.warn(`getPrincipleByTopic(${topic}) failed`, err);
|
|
7959
9789
|
return null;
|
|
7960
9790
|
}
|
|
7961
9791
|
},
|
|
@@ -7967,6 +9797,23 @@ function createHttpDataProvider() {
|
|
|
7967
9797
|
const { rules } = await getReviewRules(category);
|
|
7968
9798
|
return rules;
|
|
7969
9799
|
},
|
|
9800
|
+
/**
|
|
9801
|
+
* Session-aware review [BD-495]. The stdio client never learns its own user
|
|
9802
|
+
* id, so the argument is ignored and the API resolves the caller from the
|
|
9803
|
+
* API key. Any failure, including an older server without the endpoint,
|
|
9804
|
+
* resolves to an empty session so the review tools answer as they did
|
|
9805
|
+
* before.
|
|
9806
|
+
*/
|
|
9807
|
+
async getRecentGuidance(_userId, sinceMs) {
|
|
9808
|
+
try {
|
|
9809
|
+
const requested = Date.now() - sinceMs;
|
|
9810
|
+
const windowMs = Math.min(Math.max(requested, 1), MCP_SESSION_WINDOW_MS);
|
|
9811
|
+
const { calls } = await getSessionGuidance(windowMs);
|
|
9812
|
+
return calls;
|
|
9813
|
+
} catch {
|
|
9814
|
+
return [];
|
|
9815
|
+
}
|
|
9816
|
+
},
|
|
7970
9817
|
async searchIconLibraries(query, limit = 5) {
|
|
7971
9818
|
const { results } = await searchIconLibraries(query, limit);
|
|
7972
9819
|
return results;
|
|
@@ -7993,6 +9840,7 @@ function createHttpDataProvider() {
|
|
|
7993
9840
|
var init_http_data_provider = __esm({
|
|
7994
9841
|
"src/lib/http-data-provider.ts"() {
|
|
7995
9842
|
"use strict";
|
|
9843
|
+
init_mcp();
|
|
7996
9844
|
init_api_client();
|
|
7997
9845
|
}
|
|
7998
9846
|
});
|
|
@@ -8040,11 +9888,7 @@ __export(mcp_debug_log_exports, {
|
|
|
8040
9888
|
mcpDebugLogPath: () => mcpDebugLogPath
|
|
8041
9889
|
});
|
|
8042
9890
|
function mcpDebugLogPath() {
|
|
8043
|
-
|
|
8044
|
-
return typeof process !== "undefined" ? process.env?.MCP_DEBUG_LOG : void 0;
|
|
8045
|
-
} catch {
|
|
8046
|
-
return void 0;
|
|
8047
|
-
}
|
|
9891
|
+
return typeof process !== "undefined" ? process.env?.MCP_DEBUG_LOG : void 0;
|
|
8048
9892
|
}
|
|
8049
9893
|
function mcpDebugLog(entry) {
|
|
8050
9894
|
const path5 = mcpDebugLogPath();
|