better-design 0.15.1 → 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 +48 -35
- package/dist/index.js +2334 -510
- 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."),
|
|
@@ -1091,6 +1219,7 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
1091
1219
|
destructiveHint: false,
|
|
1092
1220
|
openWorldHint: false
|
|
1093
1221
|
};
|
|
1222
|
+
SESSION_LOOKUP_TIMEOUT_MS = 2e3;
|
|
1094
1223
|
SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
1095
1224
|
"find-design-system",
|
|
1096
1225
|
"get-ui-principle",
|
|
@@ -1106,6 +1235,541 @@ The UI copy inside the screen follows the same restraint: short headlines (about
|
|
|
1106
1235
|
}
|
|
1107
1236
|
});
|
|
1108
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
|
+
|
|
1109
1773
|
// ../shared/src/foundational-docs/topic-utils.ts
|
|
1110
1774
|
function normalizePrincipleTopic(value) {
|
|
1111
1775
|
return value.trim().toLowerCase().replace(/^principle-/, "").replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1185,6 +1849,18 @@ function formatTopicNotFound(topic, principles, toolName, resourceLabel) {
|
|
|
1185
1849
|
`Call \`${toolName}({})\` for the full index.`
|
|
1186
1850
|
].filter(Boolean).join("\n");
|
|
1187
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
|
+
}
|
|
1188
1864
|
async function runPrincipleTool(provider, area, label, toolName, args) {
|
|
1189
1865
|
const { topic, query, maxTokens } = args;
|
|
1190
1866
|
const resourceLabel = area === "react-native" || area === "three-js" || area === "motion" ? "Guides" : "Principles";
|
|
@@ -1262,14 +1938,22 @@ ${formatted}`;
|
|
|
1262
1938
|
capture: { topResults: [] }
|
|
1263
1939
|
};
|
|
1264
1940
|
}
|
|
1265
|
-
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);
|
|
1266
1950
|
server.registerTool(
|
|
1267
1951
|
"get-ui-principle",
|
|
1268
1952
|
{
|
|
1269
1953
|
title: "Get UI Design Principle",
|
|
1270
1954
|
description: `Load visual UI design principles into context on-demand.
|
|
1271
1955
|
|
|
1272
|
-
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.
|
|
1273
1957
|
|
|
1274
1958
|
You can either:
|
|
1275
1959
|
1. Request a specific topic or alias: get-ui-principle({ topic: "spacing" })
|
|
@@ -1278,7 +1962,7 @@ You can either:
|
|
|
1278
1962
|
|
|
1279
1963
|
Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
1280
1964
|
inputSchema: z2.object({
|
|
1281
|
-
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."),
|
|
1282
1966
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language search query to find relevant principles."),
|
|
1283
1967
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1284
1968
|
context: CONTEXT_ARG
|
|
@@ -1288,7 +1972,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1288
1972
|
async ({ topic, query, maxTokens, context }) => {
|
|
1289
1973
|
const start = Date.now();
|
|
1290
1974
|
try {
|
|
1291
|
-
const { response, capture } = await
|
|
1975
|
+
const { response, capture } = await runGuidanceTool(provider, userId, clientName(), "ui", "UI", "get-ui-principle", { topic, query, maxTokens });
|
|
1292
1976
|
return trackSearch(track, "get-ui-principle", { ...capture, context }, start, response);
|
|
1293
1977
|
} catch (error) {
|
|
1294
1978
|
return trackSearch(
|
|
@@ -1309,7 +1993,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1309
1993
|
title: "Get UX Design Principle",
|
|
1310
1994
|
description: `Load UX behavior and product-flow principles into context on-demand.
|
|
1311
1995
|
|
|
1312
|
-
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.
|
|
1313
1997
|
|
|
1314
1998
|
You can either:
|
|
1315
1999
|
1. Request a specific topic: get-ux-principle({ topic: "behavior-change" })
|
|
@@ -1319,7 +2003,7 @@ You can either:
|
|
|
1319
2003
|
|
|
1320
2004
|
Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
1321
2005
|
inputSchema: z2.object({
|
|
1322
|
-
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."),
|
|
1323
2007
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language search query to find relevant UX principles."),
|
|
1324
2008
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1325
2009
|
context: CONTEXT_ARG
|
|
@@ -1329,7 +2013,7 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1329
2013
|
async ({ topic, query, maxTokens, context }) => {
|
|
1330
2014
|
const start = Date.now();
|
|
1331
2015
|
try {
|
|
1332
|
-
const { response, capture } = await
|
|
2016
|
+
const { response, capture } = await runGuidanceTool(provider, userId, clientName(), "ux", "UX", "get-ux-principle", { topic, query, maxTokens });
|
|
1333
2017
|
return trackSearch(track, "get-ux-principle", { ...capture, context }, start, response);
|
|
1334
2018
|
} catch (error) {
|
|
1335
2019
|
return trackSearch(
|
|
@@ -1375,8 +2059,10 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
|
|
|
1375
2059
|
async ({ topic, query, maxTokens, context }) => {
|
|
1376
2060
|
const start = Date.now();
|
|
1377
2061
|
try {
|
|
1378
|
-
const { response, capture } = await
|
|
2062
|
+
const { response, capture } = await runGuidanceTool(
|
|
1379
2063
|
provider,
|
|
2064
|
+
userId,
|
|
2065
|
+
clientName(),
|
|
1380
2066
|
"react-native",
|
|
1381
2067
|
"React Native",
|
|
1382
2068
|
"get-react-native-guide",
|
|
@@ -1432,8 +2118,10 @@ Prefer query search when you do not know the topic name. Use maxTokens to keep r
|
|
|
1432
2118
|
async ({ topic, query, maxTokens, context }) => {
|
|
1433
2119
|
const start = Date.now();
|
|
1434
2120
|
try {
|
|
1435
|
-
const { response, capture } = await
|
|
2121
|
+
const { response, capture } = await runGuidanceTool(
|
|
1436
2122
|
provider,
|
|
2123
|
+
userId,
|
|
2124
|
+
clientName(),
|
|
1437
2125
|
"three-js",
|
|
1438
2126
|
"Three.js",
|
|
1439
2127
|
"get-three-js-guide",
|
|
@@ -1470,30 +2158,60 @@ Use this before writing a Remotion composition, and while debugging one. It cove
|
|
|
1470
2158
|
|
|
1471
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.
|
|
1472
2160
|
|
|
1473
|
-
|
|
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.
|
|
1474
2164
|
|
|
1475
2165
|
This complements the design tools: use get-ui-principle for visual decisions and get-ux-principle for product behavior.
|
|
1476
2166
|
|
|
1477
2167
|
You can either:
|
|
1478
|
-
1.
|
|
1479
|
-
2.
|
|
1480
|
-
3.
|
|
1481
|
-
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({})
|
|
1482
2173
|
|
|
1483
2174
|
Prefer query search when you do not know the topic name. Use maxTokens to keep responses focused (recommended: 5000-10000).`,
|
|
1484
2175
|
inputSchema: z2.object({
|
|
1485
|
-
|
|
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."),
|
|
1486
2178
|
query: z2.string().trim().min(1).max(500).optional().describe("Natural language description of the video or animation task or problem."),
|
|
1487
2179
|
maxTokens: z2.number().min(500).max(5e4).optional().describe("Maximum tokens to return. Truncates content to fit. Recommended: 5000-10000."),
|
|
1488
2180
|
context: CONTEXT_ARG
|
|
1489
2181
|
}),
|
|
1490
2182
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
1491
2183
|
},
|
|
1492
|
-
async ({ topic, query, maxTokens, context }) => {
|
|
2184
|
+
async ({ scaffold, topic, query, maxTokens, context }) => {
|
|
1493
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
|
+
}
|
|
1494
2210
|
try {
|
|
1495
|
-
const { response, capture } = await
|
|
2211
|
+
const { response, capture } = await runGuidanceTool(
|
|
1496
2212
|
provider,
|
|
2213
|
+
userId,
|
|
2214
|
+
clientName(),
|
|
1497
2215
|
"motion",
|
|
1498
2216
|
"Motion",
|
|
1499
2217
|
"get-motion-guide",
|
|
@@ -1542,6 +2260,7 @@ You MUST call this after writing UI code to self-review your output, then fix ev
|
|
|
1542
2260
|
- Microcopy details: pluralization, improvised unit glyphs, empty states without a next action
|
|
1543
2261
|
- Template layouts: gradient hero + eyebrow pill, left-border callout boxes
|
|
1544
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
|
|
1545
2264
|
|
|
1546
2265
|
**Comprehension (would someone outside the team understand it?):**
|
|
1547
2266
|
- Mom test: insider vocabulary, unexplained acronyms, sentences too long to follow
|
|
@@ -1560,7 +2279,11 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1560
2279
|
async ({ category = "all", maxTokens, context }) => {
|
|
1561
2280
|
const start = Date.now();
|
|
1562
2281
|
try {
|
|
1563
|
-
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);
|
|
1564
2287
|
const topResults = rules.map((rule) => ({ id: rule.id }));
|
|
1565
2288
|
if (rules.length === 0) {
|
|
1566
2289
|
return trackSearch(
|
|
@@ -1574,9 +2297,12 @@ Use maxTokens to keep responses focused (recommended: 5000\u201310000).`,
|
|
|
1574
2297
|
const rulesOutput = rules.map((rule) => `# ${rule.title}
|
|
1575
2298
|
|
|
1576
2299
|
${rule.content}`).join("\n\n---\n\n");
|
|
2300
|
+
const preamble = guidancePreamble(session.topics, "the build", sections);
|
|
1577
2301
|
let text = `# Code Review Rules
|
|
1578
2302
|
|
|
1579
|
-
${
|
|
2303
|
+
${preamble ? `${preamble}
|
|
2304
|
+
|
|
2305
|
+
` : ""}${rulesOutput}`;
|
|
1580
2306
|
if (maxTokens) text = truncateToTokens(text, maxTokens);
|
|
1581
2307
|
return trackSearch(
|
|
1582
2308
|
track,
|
|
@@ -1603,8 +2329,10 @@ var SEARCH_RESULT_PREVIEW_CHARS;
|
|
|
1603
2329
|
var init_principle_tools = __esm({
|
|
1604
2330
|
"../shared/src/mcp/tools/principle-tools.ts"() {
|
|
1605
2331
|
"use strict";
|
|
2332
|
+
init_motion_scaffold();
|
|
1606
2333
|
init_topic_utils();
|
|
1607
2334
|
init_shared();
|
|
2335
|
+
init_session_guidance();
|
|
1608
2336
|
SEARCH_RESULT_PREVIEW_CHARS = 4e3;
|
|
1609
2337
|
}
|
|
1610
2338
|
});
|
|
@@ -2084,7 +2812,7 @@ var init_comprehension = __esm({
|
|
|
2084
2812
|
|
|
2085
2813
|
// ../shared/src/mcp/tools/comprehension-tools.ts
|
|
2086
2814
|
import { z as z3 } from "zod";
|
|
2087
|
-
function registerComprehensionTools({ server, track }) {
|
|
2815
|
+
function registerComprehensionTools({ server, provider, track, userId }) {
|
|
2088
2816
|
server.registerTool(
|
|
2089
2817
|
"check-comprehension",
|
|
2090
2818
|
{
|
|
@@ -2118,7 +2846,12 @@ Use knownTerms for words your audience genuinely knows, so the check stops flagg
|
|
|
2118
2846
|
const start = Date.now();
|
|
2119
2847
|
const report = analyzeComprehension(args);
|
|
2120
2848
|
const hasCopy = Boolean(report.copy.headline) || report.copy.body.length > 0 || report.copy.actions.length > 0;
|
|
2121
|
-
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;
|
|
2122
2855
|
track(
|
|
2123
2856
|
"check-comprehension",
|
|
2124
2857
|
{
|
|
@@ -2139,6 +2872,7 @@ var init_comprehension_tools = __esm({
|
|
|
2139
2872
|
"use strict";
|
|
2140
2873
|
init_comprehension();
|
|
2141
2874
|
init_shared();
|
|
2875
|
+
init_session_guidance();
|
|
2142
2876
|
NOTHING_TO_CHECK = `# Comprehension check
|
|
2143
2877
|
|
|
2144
2878
|
No user-facing copy was found. Pass the screen's copy directly:
|
|
@@ -2367,7 +3101,7 @@ Examples:
|
|
|
2367
3101
|
- find-icons({ query: "home", libraryId: "phosphor" })
|
|
2368
3102
|
- find-icons({ query: "settings", libraryId: "tabler" })
|
|
2369
3103
|
|
|
2370
|
-
|
|
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.`,
|
|
2371
3105
|
inputSchema: z4.object({
|
|
2372
3106
|
query: z4.string().describe("What icon you need (e.g., 'home', 'settings', 'user')"),
|
|
2373
3107
|
libraryId: z4.string().describe("The icon library ID from find-icon-library"),
|
|
@@ -2740,6 +3474,21 @@ context in the supporting line or after the title. Skip the generated-template s
|
|
|
2740
3474
|
gradient hero with an uppercase eyebrow pill, centered three-card feature grid,
|
|
2741
3475
|
left-border callout boxes, walls of text.
|
|
2742
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
|
+
|
|
2743
3492
|
**Hierarchy is pass/fail.** Give the first view one P0 task and at most one P1
|
|
2744
3493
|
summary; move P2 history, standings, detailed metadata, and empty-state chrome
|
|
2745
3494
|
below it or behind a tab/disclosure. Unless the user explicitly asks for a dense
|
|
@@ -2763,7 +3512,8 @@ After writing UI, self-review with
|
|
|
2763
3512
|
`;
|
|
2764
3513
|
DS_ESLINT_CONFIG = `// Better Design \u2014 design-system guardrail (flat ESLint config fragment).
|
|
2765
3514
|
// Flags raw HTML primitives in feature code when the installed DS ships an
|
|
2766
|
-
// equivalent in components/ui
|
|
3515
|
+
// equivalent in components/ui/*, plus the uppercase micro-label tell.
|
|
3516
|
+
// Wire it into eslint.config.mjs:
|
|
2767
3517
|
//
|
|
2768
3518
|
// import dsGuardrail from "./.better-design/eslint-design-system.mjs"; // better-design-guardrail
|
|
2769
3519
|
// export default [ ...dsGuardrail, /* ...your config */ ];
|
|
@@ -2776,6 +3526,7 @@ const banned = [
|
|
|
2776
3526
|
{ selector: "JSXOpeningElement[name.name='input']", message: "Use <Input> from @/components/ui/input instead of a raw <input>." },
|
|
2777
3527
|
{ selector: "JSXOpeningElement[name.name='select']", message: "Use <Select> from @/components/ui/select instead of a raw <select>." },
|
|
2778
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." },
|
|
2779
3530
|
];
|
|
2780
3531
|
|
|
2781
3532
|
export default [
|
|
@@ -2927,12 +3678,14 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
2927
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.',
|
|
2928
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.",
|
|
2929
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.',
|
|
2930
|
-
"- 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.",
|
|
2931
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.",
|
|
2932
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.',
|
|
2933
3684
|
"",
|
|
2934
3685
|
"## Keep it tight",
|
|
2935
|
-
"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.",
|
|
2936
3689
|
...componentKit?.excluded?.length ? [
|
|
2937
3690
|
"",
|
|
2938
3691
|
"## Not in this kit (use the real components)",
|
|
@@ -2942,7 +3695,7 @@ function buildDsHtmlKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
2942
3695
|
Full live showcase: ${viewUrl}` : ""
|
|
2943
3696
|
].join("\n");
|
|
2944
3697
|
}
|
|
2945
|
-
var MINIMUM_TEXT_CONTRAST, HOVER_POLICY, INLINE_TEXT_COLOR_POLICY, HOST_ISOLATION_CSS_TEMPLATE, INLINE_COMPOSITION_POLICY, INLINE_HIERARCHY_POLICY, DS_KIT_CSS, DS_KIT_EXAMPLE_HTML, HTML_KIT_CONDUCT_MD;
|
|
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;
|
|
2946
3699
|
var init_html_kit = __esm({
|
|
2947
3700
|
"../shared/src/mcp/html-kit.ts"() {
|
|
2948
3701
|
"use strict";
|
|
@@ -2955,6 +3708,13 @@ var init_html_kit = __esm({
|
|
|
2955
3708
|
].join(" ");
|
|
2956
3709
|
HOST_ISOLATION_CSS_TEMPLATE = `APP_ROOT{color:var(--foreground,oklch(0.2 0 0));background:var(--background,oklch(1 0 0))}
|
|
2957
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(" ");
|
|
2958
3718
|
INLINE_COMPOSITION_POLICY = [
|
|
2959
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.",
|
|
2960
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.",
|
|
@@ -2987,6 +3747,11 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
|
|
|
2987
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))}
|
|
2988
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}
|
|
2989
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}
|
|
2990
3755
|
.ds p{margin:0 0 1em}
|
|
2991
3756
|
.ds-muted{color:var(--muted-foreground,oklch(0.55 0 0))}
|
|
2992
3757
|
|
|
@@ -3127,6 +3892,9 @@ APP_ROOT :where(h1,h2,h3,h4,h5,h6,p,strong,label,legend,li,dt,dd,small):not(.ds-
|
|
|
3127
3892
|
"## Hierarchy and information budget (pass/fail)",
|
|
3128
3893
|
INLINE_HIERARCHY_POLICY,
|
|
3129
3894
|
"",
|
|
3895
|
+
"## Draw one asset that only this product could carry",
|
|
3896
|
+
BESPOKE_ASSET_POLICY,
|
|
3897
|
+
"",
|
|
3130
3898
|
"## Compose the screen, do not decorate the canvas",
|
|
3131
3899
|
INLINE_COMPOSITION_POLICY,
|
|
3132
3900
|
"",
|
|
@@ -3149,18 +3917,40 @@ function fence2(lang, body) {
|
|
|
3149
3917
|
${body}
|
|
3150
3918
|
\`\`\``;
|
|
3151
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
|
+
}
|
|
3152
3940
|
function buildDsShopifyFiles(input) {
|
|
3153
|
-
const materialCss = input.componentKit?.css
|
|
3941
|
+
const materialCss = pointerSafeMaterialCss(input.componentKit?.css);
|
|
3942
|
+
const primaryTextCss = primaryButtonTextCss(materialCss);
|
|
3154
3943
|
const assetCss = [
|
|
3155
3944
|
compactTokensCss(input.tokensCss),
|
|
3156
3945
|
DS_KIT_CSS,
|
|
3157
3946
|
DS_SHOPIFY_CSS,
|
|
3158
|
-
...materialCss ? [materialCss] : []
|
|
3947
|
+
...materialCss ? [materialCss] : [],
|
|
3948
|
+
...primaryTextCss ? [primaryTextCss] : []
|
|
3159
3949
|
].join("\n\n");
|
|
3160
3950
|
return [{ path: "assets/ds-kit.css", content: assetCss }, ...SNIPPETS];
|
|
3161
3951
|
}
|
|
3162
|
-
function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
3163
|
-
const files = buildDsShopifyFiles({ tokensCss, componentKit });
|
|
3952
|
+
function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit, themeFiles }) {
|
|
3953
|
+
const files = themeFiles ?? buildDsShopifyFiles({ tokensCss, componentKit });
|
|
3164
3954
|
const sourceLine = source?.kind === "catalog" ? `Source designSystemId: \`${source.designSystemId}\`` : source?.kind === "project" ? `Source projectId: \`${source.projectId}\`` : null;
|
|
3165
3955
|
const fileBlocks = files.flatMap((f) => [
|
|
3166
3956
|
`### \`${f.path}\``,
|
|
@@ -3171,7 +3961,7 @@ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
3171
3961
|
return [
|
|
3172
3962
|
`# ${name}: Shopify theme kit`,
|
|
3173
3963
|
"",
|
|
3174
|
-
"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.",
|
|
3175
3965
|
...sourceLine ? ["", sourceLine] : [],
|
|
3176
3966
|
"",
|
|
3177
3967
|
SHOPIFY_KIT_CONDUCT_MD,
|
|
@@ -3181,57 +3971,150 @@ function buildDsShopifyKit({ name, tokensCss, viewUrl, source, componentKit }) {
|
|
|
3181
3971
|
...fileBlocks,
|
|
3182
3972
|
"## After installing",
|
|
3183
3973
|
"- Preview an unpublished copy of the theme first; never edit the live theme directly.",
|
|
3184
|
-
"- The kit
|
|
3974
|
+
"- The kit owns the storefront composition and cart page; Shopify continues to own checkout.",
|
|
3185
3975
|
...viewUrl ? ["", `Full live showcase: ${viewUrl}`] : []
|
|
3186
3976
|
].join("\n");
|
|
3187
3977
|
}
|
|
3188
|
-
var DS_SHOPIFY_CSS, SNIPPET_BADGE, SNIPPET_PRICE, SNIPPET_BUY_BUTTON, SNIPPET_VARIANT_SELECT, SNIPPET_PAGINATION, SNIPPET_PRODUCT_CARD, SNIPPET_ANNOUNCEMENT, SNIPPET_NEWSLETTER, SECTION_FAQ, SECTION_FEATURED_COLLECTION, SECTION_HEADER, SECTION_FOOTER, SECTION_HERO, SECTION_RICH_TEXT, SECTION_IMAGE_WITH_TEXT, SECTION_MAIN_PRODUCT, SECTION_MAIN_COLLECTION, SECTION_CONTACT, SECTION_MULTICOLUMN, TEMPLATE_PRODUCT_JSON, TEMPLATE_COLLECTION_JSON, SNIPPETS, SHOPIFY_KIT_CONDUCT_MD;
|
|
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;
|
|
3189
3979
|
var init_shopify_kit = __esm({
|
|
3190
3980
|
"../shared/src/mcp/shopify-kit.ts"() {
|
|
3191
3981
|
"use strict";
|
|
3192
3982
|
init_html_kit();
|
|
3193
|
-
DS_SHOPIFY_CSS =
|
|
3194
|
-
|
|
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}
|
|
3195
4001
|
.ds-announce p{margin:0}
|
|
3196
4002
|
.ds-announce a{color:inherit;text-decoration:underline}
|
|
3197
|
-
.ds-price{display:inline-flex;align-items:baseline;gap:.
|
|
3198
|
-
.ds-price-current{font-weight:
|
|
3199
|
-
.ds-price-compare{color:var(--muted-foreground,oklch(0.
|
|
3200
|
-
.ds-
|
|
3201
|
-
.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}
|
|
3202
4013
|
.ds-product-card .ds-card-title a{color:inherit;text-decoration:none}
|
|
3203
|
-
.ds-
|
|
3204
|
-
.ds-faq
|
|
3205
|
-
.ds-faq-
|
|
3206
|
-
.ds-
|
|
3207
|
-
|
|
3208
|
-
.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}
|
|
3209
4022
|
.ds-buy-form{margin:0;display:grid;gap:.75rem;justify-items:start}
|
|
3210
|
-
.ds-
|
|
4023
|
+
.ds-buy-form .ds-btn{min-width:min(100%,16rem)}
|
|
4024
|
+
.ds-featured{display:block}
|
|
3211
4025
|
.ds-qty{width:6rem}
|
|
3212
|
-
.ds-header{
|
|
3213
|
-
.ds-header-
|
|
3214
|
-
.ds-header-
|
|
3215
|
-
.ds-header-
|
|
3216
|
-
.ds-header-
|
|
3217
|
-
.ds-
|
|
3218
|
-
.ds-
|
|
3219
|
-
.ds-footer a{
|
|
3220
|
-
.ds-
|
|
3221
|
-
.ds-
|
|
3222
|
-
.ds-
|
|
3223
|
-
.ds-
|
|
3224
|
-
.ds-
|
|
3225
|
-
.ds-
|
|
3226
|
-
.ds-
|
|
3227
|
-
.ds-
|
|
3228
|
-
.ds-
|
|
3229
|
-
.ds-
|
|
3230
|
-
.ds-
|
|
3231
|
-
.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}
|
|
3232
4110
|
.ds-pagination a{color:var(--foreground,oklch(0.2 0 0))}
|
|
3233
4111
|
.ds-pagination .current{font-weight:700;color:var(--primary,oklch(0.25 0.01 260))}
|
|
3234
|
-
@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}}`;
|
|
3235
4118
|
SNIPPET_BADGE = `{% comment %}
|
|
3236
4119
|
Status label. Usage: {% render 'ds-badge', label: 'New' %}
|
|
3237
4120
|
Optional tone: primary | outline | safe | warn | danger | unknown | verified
|
|
@@ -3256,7 +4139,7 @@ var init_shopify_kit = __esm({
|
|
|
3256
4139
|
<span class="ds-price-current">{{ ds_price | money }}</span>
|
|
3257
4140
|
{%- if ds_compare_at > ds_price -%}
|
|
3258
4141
|
<s class="ds-price-compare">{{ ds_compare_at | money }}</s>
|
|
3259
|
-
{% render 'ds-badge', label: 'Sale', tone: '
|
|
4142
|
+
{% render 'ds-badge', label: 'Sale', tone: 'warn' %}
|
|
3260
4143
|
{%- endif -%}
|
|
3261
4144
|
</span>`;
|
|
3262
4145
|
SNIPPET_BUY_BUTTON = `{% comment %}
|
|
@@ -3307,6 +4190,21 @@ var init_shopify_kit = __esm({
|
|
|
3307
4190
|
{{ paginate | default_pagination }}
|
|
3308
4191
|
</nav>
|
|
3309
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 -%}`;
|
|
3310
4208
|
SNIPPET_PRODUCT_CARD = `{% comment %}
|
|
3311
4209
|
Product tile for grids. Usage: {% render 'ds-product-card', product: product %}
|
|
3312
4210
|
Optional: show_vendor: true, enable_quick_add: true
|
|
@@ -3316,11 +4214,14 @@ var init_shopify_kit = __esm({
|
|
|
3316
4214
|
{%- if product.featured_image -%}
|
|
3317
4215
|
{%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
|
|
3318
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>
|
|
3319
4219
|
{%- endif -%}
|
|
3320
4220
|
</a>
|
|
3321
4221
|
<div class="ds-card-body">
|
|
3322
|
-
{%- if show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
3323
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 -%}
|
|
3324
4225
|
{% render 'ds-price', product: product %}
|
|
3325
4226
|
</div>
|
|
3326
4227
|
{%- if enable_quick_add -%}
|
|
@@ -3379,17 +4280,17 @@ var init_shopify_kit = __esm({
|
|
|
3379
4280
|
FAQ accordion using the browser's native details element. Merchants add it
|
|
3380
4281
|
from the theme editor as "DS FAQ".
|
|
3381
4282
|
{% endcomment %}
|
|
3382
|
-
<
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
{%- 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 -%}
|
|
3386
4286
|
{%- for block in section.blocks -%}
|
|
3387
|
-
<details class="ds-faq-item" {{ block.shopify_attributes }}>
|
|
4287
|
+
<details class="ds-card ds-faq-item" {{ block.shopify_attributes }}>
|
|
3388
4288
|
<summary>{{ block.settings.question }}</summary>
|
|
3389
4289
|
<div class="ds-faq-answer">{{ block.settings.answer }}</div>
|
|
3390
4290
|
</details>
|
|
3391
4291
|
{%- endfor -%}
|
|
3392
4292
|
</div>
|
|
4293
|
+
</section>
|
|
3393
4294
|
{% schema %}
|
|
3394
4295
|
{
|
|
3395
4296
|
"name": "DS FAQ",
|
|
@@ -3414,19 +4315,28 @@ var init_shopify_kit = __esm({
|
|
|
3414
4315
|
Product grid from a merchant-chosen collection. Merchants add it from the
|
|
3415
4316
|
theme editor as "DS Featured collection".
|
|
3416
4317
|
{% endcomment %}
|
|
3417
|
-
{%- assign ds_collection = section.settings.collection -%}
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
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>
|
|
3422
4326
|
<div class="ds-grid" style="--ds-cols: {{ section.settings.columns_desktop }}">
|
|
3423
4327
|
{%- for product in ds_collection.products limit: section.settings.products_to_show -%}
|
|
3424
4328
|
{% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
|
|
3425
4329
|
{%- else -%}
|
|
3426
|
-
<
|
|
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>
|
|
3427
4335
|
{%- endfor -%}
|
|
3428
4336
|
</div>
|
|
3429
4337
|
</div>
|
|
4338
|
+
</section>
|
|
4339
|
+
{%- endif -%}
|
|
3430
4340
|
{% schema %}
|
|
3431
4341
|
{
|
|
3432
4342
|
"name": "DS Featured collection",
|
|
@@ -3436,90 +4346,189 @@ var init_shopify_kit = __esm({
|
|
|
3436
4346
|
{ "type": "range", "id": "products_to_show", "min": 2, "max": 12, "step": 1, "default": 4, "label": "Products to show" },
|
|
3437
4347
|
{ "type": "range", "id": "columns_desktop", "min": 2, "max": 5, "step": 1, "default": 4, "label": "Columns on desktop" },
|
|
3438
4348
|
{ "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
|
|
3439
|
-
{ "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 }
|
|
3440
4351
|
],
|
|
3441
4352
|
"presets": [{ "name": "DS Featured collection" }]
|
|
3442
4353
|
}
|
|
3443
4354
|
{% endschema %}`;
|
|
3444
4355
|
SECTION_HEADER = `{% comment %}
|
|
3445
|
-
|
|
4356
|
+
Complete storefront header, including announcement, responsive navigation,
|
|
4357
|
+
search, account, and cart. The generated header group installs it globally.
|
|
3446
4358
|
{% endcomment %}
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
</
|
|
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>
|
|
3462
4393
|
{% schema %}
|
|
3463
4394
|
{
|
|
3464
4395
|
"name": "DS Header",
|
|
3465
4396
|
"settings": [
|
|
3466
4397
|
{ "type": "image_picker", "id": "logo", "label": "Logo" },
|
|
3467
|
-
{ "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 }
|
|
3468
4405
|
],
|
|
3469
4406
|
"presets": [{ "name": "DS Header" }]
|
|
3470
4407
|
}
|
|
3471
4408
|
{% endschema %}`;
|
|
3472
4409
|
SECTION_FOOTER = `{% comment %}
|
|
3473
|
-
|
|
4410
|
+
Complete storefront footer with navigation, newsletter, policies, and
|
|
4411
|
+
payment methods. The generated footer group installs it globally.
|
|
3474
4412
|
{% endcomment %}
|
|
3475
4413
|
<footer class="ds ds-embed ds-footer">
|
|
3476
|
-
<
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
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>
|
|
3483
4452
|
</footer>
|
|
3484
4453
|
{% schema %}
|
|
3485
4454
|
{
|
|
3486
4455
|
"name": "DS Footer",
|
|
3487
4456
|
"settings": [
|
|
3488
|
-
{ "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 }
|
|
3489
4467
|
],
|
|
3490
4468
|
"presets": [{ "name": "DS Footer" }]
|
|
3491
4469
|
}
|
|
3492
4470
|
{% endschema %}`;
|
|
3493
4471
|
SECTION_HERO = `{% comment %}
|
|
3494
|
-
|
|
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.
|
|
3495
4474
|
{% endcomment %}
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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 -%}
|
|
3510
4517
|
</div>
|
|
3511
|
-
</
|
|
4518
|
+
</section>
|
|
3512
4519
|
{% schema %}
|
|
3513
4520
|
{
|
|
3514
4521
|
"name": "DS Hero",
|
|
3515
4522
|
"settings": [
|
|
4523
|
+
{ "type": "product", "id": "product", "label": "Featured product" },
|
|
3516
4524
|
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
3517
|
-
{ "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "
|
|
3518
|
-
{ "type": "text", "id": "text", "label": "Supporting line" },
|
|
3519
|
-
{ "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" },
|
|
3520
4528
|
{ "type": "url", "id": "button_link", "label": "Button link" },
|
|
3521
4529
|
{ "type": "text", "id": "secondary_label", "label": "Secondary button label" },
|
|
3522
|
-
{ "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." }
|
|
3523
4532
|
],
|
|
3524
4533
|
"presets": [{ "name": "DS Hero" }]
|
|
3525
4534
|
}
|
|
@@ -3527,13 +4536,15 @@ var init_shopify_kit = __esm({
|
|
|
3527
4536
|
SECTION_RICH_TEXT = `{% comment %}
|
|
3528
4537
|
Heading plus rich text, with an optional button.
|
|
3529
4538
|
{% endcomment %}
|
|
3530
|
-
<
|
|
4539
|
+
<section class="ds ds-embed ds-section">
|
|
4540
|
+
<div class="ds-container ds-rich-text">
|
|
3531
4541
|
{%- if section.settings.heading != blank -%}<h2 class="ds-section-title">{{ section.settings.heading }}</h2>{%- endif -%}
|
|
3532
|
-
<div>{{ section.settings.text }}</div>
|
|
4542
|
+
<div class="ds-section-copy">{{ section.settings.text }}</div>
|
|
3533
4543
|
{%- if section.settings.button_label != blank -%}
|
|
3534
|
-
<
|
|
4544
|
+
<a class="ds-btn ds-btn-secondary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a>
|
|
3535
4545
|
{%- endif -%}
|
|
3536
4546
|
</div>
|
|
4547
|
+
</section>
|
|
3537
4548
|
{% schema %}
|
|
3538
4549
|
{
|
|
3539
4550
|
"name": "DS Rich text",
|
|
@@ -3549,23 +4560,38 @@ var init_shopify_kit = __esm({
|
|
|
3549
4560
|
SECTION_IMAGE_WITH_TEXT = `{% comment %}
|
|
3550
4561
|
Image beside heading, text, and an optional button.
|
|
3551
4562
|
{% endcomment %}
|
|
3552
|
-
|
|
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">
|
|
3553
4567
|
{%- if section.settings.image -%}
|
|
3554
4568
|
{%- assign ds_image_alt = section.settings.heading | strip_html -%}
|
|
3555
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>
|
|
3556
4579
|
{%- endif -%}
|
|
4580
|
+
</div>{%- endif -%}
|
|
3557
4581
|
<div class="ds-product-info">
|
|
3558
4582
|
<h2 class="ds-section-title">{{ section.settings.heading }}</h2>
|
|
3559
|
-
<div>{{ section.settings.text }}</div>
|
|
4583
|
+
<div class="ds-section-copy">{{ section.settings.text }}</div>
|
|
3560
4584
|
{%- if section.settings.button_label != blank -%}
|
|
3561
4585
|
<p><a class="ds-btn ds-btn-primary" href="{{ section.settings.button_link }}">{{ section.settings.button_label }}</a></p>
|
|
3562
4586
|
{%- endif -%}
|
|
3563
4587
|
</div>
|
|
3564
4588
|
</div>
|
|
4589
|
+
</section>
|
|
3565
4590
|
{% schema %}
|
|
3566
4591
|
{
|
|
3567
4592
|
"name": "DS Image with text",
|
|
3568
4593
|
"settings": [
|
|
4594
|
+
{ "type": "product", "id": "product", "label": "Product image fallback" },
|
|
3569
4595
|
{ "type": "image_picker", "id": "image", "label": "Image" },
|
|
3570
4596
|
{ "type": "inline_richtext", "id": "heading", "label": "Heading", "default": "Pair text with an image" },
|
|
3571
4597
|
{ "type": "richtext", "id": "text", "label": "Text" },
|
|
@@ -3582,10 +4608,14 @@ var init_shopify_kit = __esm({
|
|
|
3582
4608
|
<div class="ds ds-embed ds-container ds-product-page">
|
|
3583
4609
|
<div class="ds-product-layout">
|
|
3584
4610
|
<div class="ds-product-media">
|
|
4611
|
+
<div class="ds-card ds-product-media-main">
|
|
3585
4612
|
{%- if product.featured_image -%}
|
|
3586
4613
|
{%- assign ds_image_alt = product.featured_image.alt | default: product.title -%}
|
|
3587
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>
|
|
3588
4617
|
{%- endif -%}
|
|
4618
|
+
</div>
|
|
3589
4619
|
{%- if product.images.size > 1 -%}
|
|
3590
4620
|
<div class="ds-product-thumbs">
|
|
3591
4621
|
{%- for image in product.images offset: 1 limit: 4 -%}
|
|
@@ -3595,12 +4625,13 @@ var init_shopify_kit = __esm({
|
|
|
3595
4625
|
</div>
|
|
3596
4626
|
{%- endif -%}
|
|
3597
4627
|
</div>
|
|
3598
|
-
<div class="ds-product-info">
|
|
3599
|
-
{%- if section.settings.show_vendor -%}<p class="ds-supporting">{{ product.vendor }}</p>{%- endif -%}
|
|
4628
|
+
<div class="ds-card ds-product-info">
|
|
3600
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 -%}
|
|
3601
4632
|
{% render 'ds-price', variant: product.selected_or_first_available_variant %}
|
|
3602
4633
|
{% render 'ds-buy-button', product: product, show_quantity: section.settings.show_quantity %}
|
|
3603
|
-
<div>{{ product.description }}</div>
|
|
4634
|
+
<div class="ds-product-description">{{ product.description }}</div>
|
|
3604
4635
|
</div>
|
|
3605
4636
|
</div>
|
|
3606
4637
|
</div>
|
|
@@ -3620,9 +4651,11 @@ var init_shopify_kit = __esm({
|
|
|
3620
4651
|
{% endcomment %}
|
|
3621
4652
|
{% paginate collection.products by section.settings.products_per_page %}
|
|
3622
4653
|
<div class="ds ds-embed ds-container ds-collection-page">
|
|
3623
|
-
<
|
|
3624
|
-
|
|
3625
|
-
|
|
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 }}">
|
|
3626
4659
|
{%- for product in collection.products -%}
|
|
3627
4660
|
{% render 'ds-product-card', product: product, show_vendor: section.settings.show_vendor, enable_quick_add: section.settings.enable_quick_add %}
|
|
3628
4661
|
{%- else -%}
|
|
@@ -3647,9 +4680,10 @@ var init_shopify_kit = __esm({
|
|
|
3647
4680
|
SECTION_CONTACT = `{% comment %}
|
|
3648
4681
|
Contact form posting to Shopify's contact endpoint. No JavaScript.
|
|
3649
4682
|
{% endcomment %}
|
|
3650
|
-
<
|
|
4683
|
+
<section class="ds ds-embed ds-section">
|
|
4684
|
+
<div class="ds-container ds-page">
|
|
3651
4685
|
<h2 class="ds-section-title">{{ section.settings.heading }}</h2>
|
|
3652
|
-
{% form 'contact' %}
|
|
4686
|
+
{% form 'contact', class: 'ds-card ds-contact-form' %}
|
|
3653
4687
|
{%- if form.posted_successfully? -%}
|
|
3654
4688
|
<p role="status">Thanks for your message. We reply within one business day.</p>
|
|
3655
4689
|
{%- else -%}
|
|
@@ -3670,6 +4704,7 @@ var init_shopify_kit = __esm({
|
|
|
3670
4704
|
{%- endif -%}
|
|
3671
4705
|
{% endform %}
|
|
3672
4706
|
</div>
|
|
4707
|
+
</section>
|
|
3673
4708
|
{% schema %}
|
|
3674
4709
|
{
|
|
3675
4710
|
"name": "DS Contact",
|
|
@@ -3682,19 +4717,22 @@ var init_shopify_kit = __esm({
|
|
|
3682
4717
|
SECTION_MULTICOLUMN = `{% comment %}
|
|
3683
4718
|
Row of text cards, for value props or feature callouts.
|
|
3684
4719
|
{% endcomment %}
|
|
3685
|
-
<
|
|
3686
|
-
|
|
3687
|
-
<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 }}">
|
|
3688
4724
|
{%- for block in section.blocks -%}
|
|
3689
|
-
<div class="ds-
|
|
3690
|
-
|
|
3691
|
-
|
|
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>
|
|
3692
4730
|
<p class="ds-card-desc">{{ block.settings.text }}</p>
|
|
3693
|
-
</div>
|
|
3694
4731
|
</div>
|
|
3695
4732
|
{%- endfor -%}
|
|
3696
4733
|
</div>
|
|
3697
4734
|
</div>
|
|
4735
|
+
</section>
|
|
3698
4736
|
{% schema %}
|
|
3699
4737
|
{
|
|
3700
4738
|
"name": "DS Multicolumn",
|
|
@@ -3707,6 +4745,7 @@ var init_shopify_kit = __esm({
|
|
|
3707
4745
|
"type": "column",
|
|
3708
4746
|
"name": "Column",
|
|
3709
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" }] },
|
|
3710
4749
|
{ "type": "text", "id": "heading", "label": "Heading" },
|
|
3711
4750
|
{ "type": "text", "id": "text", "label": "Text" }
|
|
3712
4751
|
]
|
|
@@ -3716,27 +4755,231 @@ var init_shopify_kit = __esm({
|
|
|
3716
4755
|
"presets": [{ "name": "DS Multicolumn", "blocks": [{ "type": "column" }, { "type": "column" }, { "type": "column" }] }]
|
|
3717
4756
|
}
|
|
3718
4757
|
{% endschema %}`;
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
"
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
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
|
+
}
|
|
4976
|
+
},
|
|
3735
4977
|
"order": ["main"]
|
|
3736
4978
|
}`;
|
|
3737
4979
|
SNIPPETS = [
|
|
3738
4980
|
{ path: "snippets/ds-badge.liquid", content: SNIPPET_BADGE },
|
|
3739
4981
|
{ path: "snippets/ds-price.liquid", content: SNIPPET_PRICE },
|
|
4982
|
+
{ path: "snippets/ds-icon.liquid", content: SNIPPET_ICON },
|
|
3740
4983
|
{ path: "snippets/ds-variant-select.liquid", content: SNIPPET_VARIANT_SELECT },
|
|
3741
4984
|
{ path: "snippets/ds-buy-button.liquid", content: SNIPPET_BUY_BUTTON },
|
|
3742
4985
|
{ path: "snippets/ds-product-card.liquid", content: SNIPPET_PRODUCT_CARD },
|
|
@@ -3754,15 +4997,32 @@ var init_shopify_kit = __esm({
|
|
|
3754
4997
|
{ path: "sections/ds-featured-collection.liquid", content: SECTION_FEATURED_COLLECTION },
|
|
3755
4998
|
{ path: "sections/ds-main-product.liquid", content: SECTION_MAIN_PRODUCT },
|
|
3756
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 },
|
|
3757
5013
|
{ path: "templates/product.ds.json", content: TEMPLATE_PRODUCT_JSON },
|
|
3758
5014
|
{ path: "templates/collection.ds.json", content: TEMPLATE_COLLECTION_JSON }
|
|
3759
5015
|
];
|
|
3760
5016
|
SHOPIFY_KIT_CONDUCT_MD = [
|
|
3761
5017
|
"## Install into the merchant's theme (mandatory order)",
|
|
3762
|
-
"1.
|
|
3763
|
-
"2.
|
|
3764
|
-
|
|
3765
|
-
"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.",
|
|
3766
5026
|
"",
|
|
3767
5027
|
"## Class contract (pass/fail)",
|
|
3768
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.",
|
|
@@ -3782,6 +5042,43 @@ var init_shopify_kit = __esm({
|
|
|
3782
5042
|
}
|
|
3783
5043
|
});
|
|
3784
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
|
+
|
|
3785
5082
|
// ../shared/src/mcp/preview-widget-sdk.generated.ts
|
|
3786
5083
|
var PREVIEW_WIDGET_SDK_JS;
|
|
3787
5084
|
var init_preview_widget_sdk_generated = __esm({
|
|
@@ -4446,6 +5743,26 @@ function decodeArbitrary(raw) {
|
|
|
4446
5743
|
const v = raw.replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
4447
5744
|
return v && !/[{}<>;]/.test(v) ? v : null;
|
|
4448
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
|
+
}
|
|
4449
5766
|
function animateModifier(base) {
|
|
4450
5767
|
const spin = {
|
|
4451
5768
|
"animate-spin": "animation:spin 1s linear infinite",
|
|
@@ -4511,13 +5828,23 @@ function translateBase(base) {
|
|
|
4511
5828
|
if (arb) {
|
|
4512
5829
|
const val = decodeArbitrary(arb[2]);
|
|
4513
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(";");
|
|
4514
5842
|
const prop = ARBITRARY_PROP[arb[1]];
|
|
4515
5843
|
if (prop) return `${prop}:${val}`;
|
|
4516
5844
|
const colorProp = COLOR_PROP[arb[1]];
|
|
4517
|
-
if (colorProp) {
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
}
|
|
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)`;
|
|
4521
5848
|
}
|
|
4522
5849
|
}
|
|
4523
5850
|
const rside = base.match(/^rounded-(t|r|b|l|tl|tr|br|bl)(?:-(.+))?$/);
|
|
@@ -4619,7 +5946,7 @@ function splitVariants(cls) {
|
|
|
4619
5946
|
out.push(cur);
|
|
4620
5947
|
return out;
|
|
4621
5948
|
}
|
|
4622
|
-
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;
|
|
4623
5950
|
var init_tw_translate = __esm({
|
|
4624
5951
|
"../shared/src/mcp/tw-translate.ts"() {
|
|
4625
5952
|
"use strict";
|
|
@@ -5047,15 +6374,12 @@ var init_tw_translate = __esm({
|
|
|
5047
6374
|
"rounded-xs": "border-radius:var(--radius-xs, 0.125rem)"
|
|
5048
6375
|
};
|
|
5049
6376
|
ARBITRARY_PROP = {
|
|
5050
|
-
bg: "background",
|
|
5051
|
-
text: "color",
|
|
5052
6377
|
w: "width",
|
|
5053
6378
|
h: "height",
|
|
5054
6379
|
"min-w": "min-width",
|
|
5055
6380
|
"max-w": "max-width",
|
|
5056
6381
|
"min-h": "min-height",
|
|
5057
6382
|
"max-h": "max-height",
|
|
5058
|
-
size: "width",
|
|
5059
6383
|
p: "padding",
|
|
5060
6384
|
px: "padding-inline",
|
|
5061
6385
|
py: "padding-block",
|
|
@@ -5072,8 +6396,6 @@ var init_tw_translate = __esm({
|
|
|
5072
6396
|
ml: "margin-left",
|
|
5073
6397
|
gap: "gap",
|
|
5074
6398
|
rounded: "border-radius",
|
|
5075
|
-
shadow: "box-shadow",
|
|
5076
|
-
border: "border-width",
|
|
5077
6399
|
top: "top",
|
|
5078
6400
|
left: "left",
|
|
5079
6401
|
right: "right",
|
|
@@ -5084,14 +6406,93 @@ var init_tw_translate = __esm({
|
|
|
5084
6406
|
opacity: "opacity",
|
|
5085
6407
|
z: "z-index",
|
|
5086
6408
|
grid: "grid-template-columns",
|
|
5087
|
-
"font": "font-size",
|
|
5088
6409
|
"aspect": "aspect-ratio",
|
|
5089
|
-
// transition props (transition-[background-color,box-shadow]) + timing
|
|
5090
|
-
transition: "transition-property",
|
|
5091
6410
|
duration: "transition-duration",
|
|
5092
6411
|
delay: "transition-delay",
|
|
5093
6412
|
ease: "transition-timing-function"
|
|
5094
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;
|
|
5095
6496
|
MAX_W_SCALE = {
|
|
5096
6497
|
xs: "20rem",
|
|
5097
6498
|
sm: "24rem",
|
|
@@ -5168,47 +6569,73 @@ function decodeArbitrary2(raw) {
|
|
|
5168
6569
|
const value = raw.replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
5169
6570
|
return value && !/[<>{};]/.test(value) ? value : null;
|
|
5170
6571
|
}
|
|
5171
|
-
function
|
|
6572
|
+
function restingDeclaration(scope, cssProp, utilPrefixes) {
|
|
5172
6573
|
for (const prefix of utilPrefixes) {
|
|
5173
6574
|
const m = scope.match(new RegExp(`(?<![\\w:.\\-])${prefix}-\\[([^\\]]+)\\]`));
|
|
5174
6575
|
if (m) {
|
|
5175
6576
|
const v = decodeArbitrary2(m[1]);
|
|
5176
|
-
if (v) return v;
|
|
6577
|
+
if (v) return resolveArbitraryDeclaration(prefix, v) ?? { property: cssProp, value: v };
|
|
5177
6578
|
}
|
|
5178
6579
|
}
|
|
5179
6580
|
const pm = scope.match(new RegExp(`(?<!:)\\[${cssProp}:([^\\]]+)\\]`));
|
|
5180
|
-
|
|
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;
|
|
5181
6587
|
}
|
|
5182
6588
|
function variantBody(content, name) {
|
|
5183
6589
|
const vi = content.search(/variant\s*:\s*\{/);
|
|
5184
6590
|
const hay = vi !== -1 ? content.slice(vi) : content;
|
|
6591
|
+
const key = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5185
6592
|
const m = hay.match(
|
|
5186
|
-
new RegExp(
|
|
6593
|
+
new RegExp(
|
|
6594
|
+
`[\\s{,](?:${key}|["']${key}["'])\\s*:\\s*([\\s\\S]*?)(?:\\n\\s*(?:[A-Za-z_][\\w-]*|["'][^"']+["'])\\s*:|\\n\\s*\\},)`
|
|
6595
|
+
)
|
|
5187
6596
|
);
|
|
5188
6597
|
return m ? m[1] : null;
|
|
5189
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
|
+
}
|
|
5190
6607
|
function componentDepthCss(files) {
|
|
5191
6608
|
const btn = files.find((f) => f.path && /(^|\/)button\.(tsx|jsx)$/.test(f.path));
|
|
5192
6609
|
if (!btn?.content) return "";
|
|
5193
6610
|
const content = btn.content;
|
|
5194
|
-
const scope =
|
|
6611
|
+
const scope = canonicalButtonVariantBody(content) ?? content;
|
|
5195
6612
|
const shadow = /(?:^|\s|"|')shadow-none(?=\s|"|'|$)/.test(scope) ? "none" : restingValue(scope, "box-shadow", ["shadow"]);
|
|
5196
|
-
const background =
|
|
6613
|
+
const background = restingDeclaration(scope, "background", ["bg"]) ?? restingDeclaration(scope, "background-image", []);
|
|
5197
6614
|
const sel = ".bd-btn-primary, .bd-view, .ds-btn-primary";
|
|
5198
6615
|
const rules = [];
|
|
5199
6616
|
if (shadow) rules.push(`${sel} { box-shadow: ${shadow}; }`);
|
|
5200
|
-
if (background) rules.push(`${sel} { background: ${background}; }`);
|
|
6617
|
+
if (background) rules.push(`${sel} { ${background.property}: ${background.value}; }`);
|
|
5201
6618
|
return rules.join("\n");
|
|
5202
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
|
+
}
|
|
5203
6636
|
function buildDsComponentCss(files) {
|
|
5204
6637
|
const contentOf = (re) => stripComments2(files.find((f) => f.path && re.test(f.path))?.content ?? "");
|
|
5205
6638
|
const has = (name) => Boolean(contentOf(new RegExp(`(^|/)${name}\\.(tsx|jsx)$`)));
|
|
5206
|
-
const arbitrary = (scope, re) => {
|
|
5207
|
-
const m = scope.match(re);
|
|
5208
|
-
if (!m) return null;
|
|
5209
|
-
const value = m[1].replace(/(?<!\\)_/g, " ").replace(/\\_/g, "_").trim();
|
|
5210
|
-
return value && !/[<>{};]/.test(value) ? value : null;
|
|
5211
|
-
};
|
|
5212
6639
|
const rules = [componentRadiusCss(files), componentDepthCss(files)];
|
|
5213
6640
|
const covered = /* @__PURE__ */ new Set();
|
|
5214
6641
|
for (const name of ["button", "input", "alert", "avatar", "table", "tabs", "progress"]) {
|
|
@@ -5223,9 +6650,9 @@ function buildDsComponentCss(files) {
|
|
|
5223
6650
|
const sw = contentOf(/(^|\/)switch\.(tsx|jsx)$/);
|
|
5224
6651
|
if (sw) {
|
|
5225
6652
|
covered.add("switch");
|
|
5226
|
-
const bg =
|
|
6653
|
+
const bg = checkedTrackBackground(sw);
|
|
5227
6654
|
const shadow = arbitrary(sw, /data-\[state=checked\]:shadow-\[([^\]]+)\]/) ?? arbitrary(sw, /data-\[state=checked\]:\[box-shadow:([^\]]+)\]/);
|
|
5228
|
-
if (bg) rules.push(`.ds-switch-track.on {
|
|
6655
|
+
if (bg) rules.push(`.ds-switch-track.on { ${bg.property}: ${bg.value}; }`);
|
|
5229
6656
|
if (shadow) rules.push(`.ds-switch-track.on { box-shadow: ${shadow}; }`);
|
|
5230
6657
|
}
|
|
5231
6658
|
const badge = contentOf(/(^|\/)badge\.(tsx|jsx)$/);
|
|
@@ -5311,10 +6738,11 @@ function translatedKitComponentCss(contentOf, covered) {
|
|
|
5311
6738
|
const button = contentOf(/(^|\/)button\.(tsx|jsx)$/);
|
|
5312
6739
|
if (button) {
|
|
5313
6740
|
emit(".ds-btn", cvaBase(button), "button");
|
|
5314
|
-
|
|
6741
|
+
const canonical = canonicalButtonVariantBody(button);
|
|
6742
|
+
emit(".ds .ds-btn-primary", canonical ? quotedStrings(canonical) : null);
|
|
5315
6743
|
for (const v of ["secondary", "outline", "ghost"]) {
|
|
5316
6744
|
const body = variantBody(button, v);
|
|
5317
|
-
if (body) emit(`.ds-btn-${v}`, quotedStrings(body));
|
|
6745
|
+
if (body) emit(`.ds .ds-btn-${v}`, quotedStrings(body));
|
|
5318
6746
|
}
|
|
5319
6747
|
}
|
|
5320
6748
|
const shared = contentOf(/(^|\/)_shared\.(ts|tsx)$/);
|
|
@@ -5459,15 +6887,13 @@ ${PREVIEW_BASE_CSS}
|
|
|
5459
6887
|
.bd-app { padding: 16px; }
|
|
5460
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)); }
|
|
5461
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)); }
|
|
5462
|
-
.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; }
|
|
5463
|
-
.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; }
|
|
5464
6891
|
.bd-cta-actions { display: inline-flex; align-items: center; gap: 14px; }
|
|
5465
|
-
.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; }
|
|
5466
|
-
.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; }
|
|
5467
6894
|
.bd-progress { padding: 6px 16px 12px; }
|
|
5468
|
-
/* Fullscreen: the DS preview rendered
|
|
5469
|
-
|
|
5470
|
-
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. */
|
|
5471
6897
|
.bd-fs { position: fixed; inset: 0; display: flex; flex-direction: column; background: var(--background, oklch(1 0 0)); }
|
|
5472
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; }
|
|
5473
6899
|
.bd-fs-body { flex: 1; overflow-y: auto; padding: 20px; max-width: 860px; width: 100%; margin: 0 auto; }
|
|
@@ -5514,14 +6940,14 @@ ${DS_KIT_CSS}
|
|
|
5514
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; }
|
|
5515
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; }
|
|
5516
6942
|
.bd-nav-list { display: flex; flex-direction: column; gap: 1px; padding: 6px 6px 8px; }
|
|
5517
|
-
.bd-nav-item { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding:
|
|
5518
|
-
.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)); }
|
|
5519
6945
|
.bd-nav-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; background: var(--border, oklch(0.85 0 0)); }
|
|
5520
6946
|
.bd-nav-item.active .bd-nav-dot { background: var(--primary, oklch(0.45 0.15 264)); }
|
|
5521
6947
|
/* SectionHeader (component-showcase.tsx:1659-1680). */
|
|
5522
6948
|
.bd-sh { margin-bottom: 20px; margin-top: 64px; padding-top: 48px; border-top: 1px solid var(--border, oklch(0.9 0 0)); }
|
|
5523
6949
|
.bd-sh.first { margin-top: 28px; padding-top: 0; border-top: none; }
|
|
5524
|
-
.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)); }
|
|
5525
6951
|
/* Grid (component-showcase.tsx:1565-1571 \u2014 cols-1 / sm:2 / xl:3, gap-3 = 12px).
|
|
5526
6952
|
Own class (not the vignette's .bd-grid) + explicit responsive columns capped
|
|
5527
6953
|
at 3, so the fullscreen main never renders a 4th column. [BD-117] */
|
|
@@ -5541,26 +6967,18 @@ ${DS_KIT_CSS}
|
|
|
5541
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); }
|
|
5542
6968
|
.bd-fs .bd-show-scroll { max-height: none; overflow: visible; -webkit-mask-image: none; mask-image: none; }
|
|
5543
6969
|
.bd-fs .bd-main { max-width: 920px; }
|
|
5544
|
-
/* Real-page
|
|
5545
|
-
full viewport + a Back bar. The iframe shows the actual /design-systems/<id>
|
|
5546
|
-
page (?embed=1, no app nav), so the components are real, not approximated. */
|
|
5547
|
-
.bd-frame-wrap { display: flex; flex-direction: column; background: var(--background, oklch(1 0 0)); }
|
|
5548
|
-
.bd-frame { width: 100%; border: 0; display: block; background: var(--background, oklch(1 0 0)); height: 460px; }
|
|
5549
|
-
.bd-frame-wrap.bd-fs { position: fixed; inset: 0; height: 100vh; }
|
|
5550
|
-
.bd-frame-wrap.bd-fs .bd-frame { flex: 1; height: auto; }
|
|
5551
|
-
/* Real-page screenshot (claude.ai, which can't iframe). Inline = capped + fade;
|
|
5552
|
-
fullscreen = scrollable. [BD-136] */
|
|
6970
|
+
/* Real-page screenshot. Inline = capped + fade; fullscreen = scrollable. */
|
|
5553
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); }
|
|
5554
|
-
.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; }
|
|
5555
6973
|
.bd-shot { display: block; width: 100%; height: auto; }
|
|
5556
|
-
.bd-
|
|
5557
|
-
.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)); }
|
|
5558
6976
|
.bd-cta-actions { display: inline-flex; align-items: center; gap: 14px; }
|
|
5559
|
-
|
|
5560
|
-
.bd-
|
|
5561
|
-
.bd-
|
|
5562
|
-
.bd-
|
|
5563
|
-
|
|
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
|
+
}
|
|
5564
6982
|
</style>
|
|
5565
6983
|
<style id="bd-tokens"></style>
|
|
5566
6984
|
</head>
|
|
@@ -5582,7 +7000,7 @@ ${DS_KIT_CSS}
|
|
|
5582
7000
|
return String(s == null ? "" : s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5583
7001
|
}
|
|
5584
7002
|
|
|
5585
|
-
// Only http(s) URLs are safe to use as
|
|
7003
|
+
// Only http(s) URLs are safe to use as image or link targets; reject
|
|
5586
7004
|
// javascript:/data:/etc. so a hostile MCP payload can't inject a scheme that
|
|
5587
7005
|
// executes in the widget sandbox. Returns "" for anything else.
|
|
5588
7006
|
function safeUrl(u) {
|
|
@@ -5631,12 +7049,12 @@ ${DS_KIT_CSS}
|
|
|
5631
7049
|
return showcaseSections().map(function (s, i) {
|
|
5632
7050
|
var cards = s[2].map(function (it) {
|
|
5633
7051
|
return '<div class="bd-dc" data-name="' + esc(it[0].toLowerCase()) + '">' +
|
|
5634
|
-
'<div class="bd-dc-demo">' + it[1] + '</div>' +
|
|
7052
|
+
'<div class="bd-dc-demo" inert aria-hidden="true">' + it[1] + '</div>' +
|
|
5635
7053
|
'<div class="bd-dc-foot">' + esc(it[0]) + '</div>' +
|
|
5636
7054
|
'</div>';
|
|
5637
7055
|
}).join("");
|
|
5638
7056
|
return '<section class="bd-sec" data-sec="' + esc(s[0]) + '">' +
|
|
5639
|
-
'<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>' +
|
|
5640
7058
|
'<div class="bd-show-grid">' + cards + '</div>' +
|
|
5641
7059
|
'</section>';
|
|
5642
7060
|
}).join("");
|
|
@@ -5655,7 +7073,7 @@ ${DS_KIT_CSS}
|
|
|
5655
7073
|
// for each semantic color token. Fullscreen only (CSS). [BD-117]
|
|
5656
7074
|
function paletteHtml() {
|
|
5657
7075
|
var TOK = [["Background", "background"], ["Foreground", "foreground"], ["Primary", "primary"], ["Secondary", "secondary"], ["Accent", "accent"], ["Muted", "muted"], ["Card", "card"], ["Border", "border"], ["Destructive", "destructive"], ["Ring", "ring"]];
|
|
5658
|
-
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">' +
|
|
5659
7077
|
TOK.map(function (t) {
|
|
5660
7078
|
return '<div class="bd-tok"><span class="bd-tok-sw" style="background: var(--' + t[1] + ', transparent)"></span>' + esc(t[0]) + '</div>';
|
|
5661
7079
|
}).join("") + '</div></div></aside>';
|
|
@@ -5663,7 +7081,7 @@ ${DS_KIT_CSS}
|
|
|
5663
7081
|
|
|
5664
7082
|
// The full showcase. Mirrors the real page: a header (h1 + description), then a
|
|
5665
7083
|
// 3-column layout (left nav + main sections + right palette). Same render on
|
|
5666
|
-
//
|
|
7084
|
+
// every host. Inline caps the height + hides asides via CSS exactly
|
|
5667
7085
|
// like the real page below lg/xl; fullscreen shows everything. [BD-117]
|
|
5668
7086
|
function showcaseBody(out, full) {
|
|
5669
7087
|
var nm = esc(out.name || "Design system");
|
|
@@ -5721,7 +7139,7 @@ ${DS_KIT_CSS}
|
|
|
5721
7139
|
de.removeProperty("--font-serif");
|
|
5722
7140
|
de.removeProperty("--font-mono");
|
|
5723
7141
|
} else if (hostFontsPayload && applyHostFontsFn) {
|
|
5724
|
-
try { applyHostFontsFn(hostFontsPayload); } catch (e) {}
|
|
7142
|
+
try { applyHostFontsFn(hostFontsPayload); } catch (e) { /* best-effort: host font apply; fallback fonts stand */ }
|
|
5725
7143
|
}
|
|
5726
7144
|
}
|
|
5727
7145
|
|
|
@@ -5732,15 +7150,15 @@ ${DS_KIT_CSS}
|
|
|
5732
7150
|
// mode field = declined); only switch to what it actually granted.
|
|
5733
7151
|
var apply = function (m) { displayMode = m || displayMode; if (lastOut) render(lastOut); };
|
|
5734
7152
|
try {
|
|
5735
|
-
if (bdApp && bdApp.requestDisplayMode) {
|
|
5736
|
-
bdApp.requestDisplayMode({ mode: mode }).then(function (r) { apply(r && r.mode); }, function () {});
|
|
5737
|
-
return;
|
|
5738
|
-
}
|
|
5739
7153
|
if (window.openai && window.openai.requestDisplayMode) {
|
|
5740
7154
|
Promise.resolve(window.openai.requestDisplayMode({ mode: mode })).then(function (r) { apply(r && r.mode); }, function () {});
|
|
5741
7155
|
return;
|
|
5742
7156
|
}
|
|
5743
|
-
|
|
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 */ }
|
|
5744
7162
|
apply(mode);
|
|
5745
7163
|
}
|
|
5746
7164
|
|
|
@@ -5771,91 +7189,32 @@ ${DS_KIT_CSS}
|
|
|
5771
7189
|
return !!(modes && modes.indexOf && modes.indexOf("fullscreen") !== -1);
|
|
5772
7190
|
}
|
|
5773
7191
|
|
|
5774
|
-
//
|
|
5775
|
-
|
|
5776
|
-
// -mcp#40), so it can never iframe \u2014 there we link out instead. [BD-117]
|
|
5777
|
-
//
|
|
5778
|
-
// Identify ChatGPT by window.openai ALONE. Do NOT also test !bdApp: bdApp is
|
|
5779
|
-
// our OWN inlined MCP-Apps client (window.__bdMcp), instantiated
|
|
5780
|
-
// unconditionally on every host, so it is always set after first paint. The
|
|
5781
|
-
// old check (window.openai AND not bdApp) therefore flipped to false on every
|
|
5782
|
-
// re-render (Fullscreen, set_globals), wrongly showing the claude.ai link card
|
|
5783
|
-
// on ChatGPT. window.openai is absent on claude.ai, so it is the right signal.
|
|
5784
|
-
function canIframe() {
|
|
5785
|
-
return !!window.openai;
|
|
5786
|
-
}
|
|
5787
|
-
|
|
5788
|
-
// The real page, with ?embed=1 so it renders the showcase WITHOUT the app nav
|
|
5789
|
-
// (clean component showcase to embed). [BD-117]
|
|
5790
|
-
function embedUrl(view) {
|
|
5791
|
-
if (!view) return "";
|
|
5792
|
-
return view + (view.indexOf("?") === -1 ? "?embed=1" : "&embed=1");
|
|
5793
|
-
}
|
|
5794
|
-
|
|
5795
|
-
// Embed the REAL /design-systems/<id> (or /projects/<id>) page \u2014 the actual
|
|
5796
|
-
// React components, not a token approximation. Inline = capped height + a
|
|
5797
|
-
// footer (Fullscreen + Open). Fullscreen = full-bleed iframe with NO header of
|
|
5798
|
-
// our own: both hosts render a native close button in fullscreen (ChatGPT's
|
|
5799
|
-
// top bar, claude.ai's native header), so our own Back bar was redundant and
|
|
5800
|
-
// visually broken (misaligned, disliked chevron). [BD-136]
|
|
5801
|
-
// embed = the PUBLIC, cookieless URL we iframe (the sandbox carries no
|
|
5802
|
-
// session cookie, so an owner-scoped page would 404 inside it). open = the
|
|
5803
|
-
// owner-facing page for the human "Open" link (they have the cookie). [BD-164]
|
|
5804
|
-
function iframeBody(out, embed, open, full) {
|
|
5805
|
-
var nm = esc(out.name || "Design system");
|
|
5806
|
-
var frame = '<iframe class="bd-frame" src="' + esc(embedUrl(embed)) + '" title="' + nm + ' showcase" referrerpolicy="no-referrer-when-downgrade"></iframe>';
|
|
5807
|
-
if (full) {
|
|
5808
|
-
return '<div class="bd-frame-wrap bd-fs">' + frame + '</div>';
|
|
5809
|
-
}
|
|
5810
|
-
var openHref = open || embed;
|
|
5811
|
-
return '<div class="bd-frame-wrap">' + frame +
|
|
5812
|
-
'<div class="bd-frame-foot"><span class="bd-cta-name">' + nm + '</span><span class="bd-cta-actions">' +
|
|
5813
|
-
(canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
|
|
5814
|
-
'<a class="bd-cta" data-bd-open href="' + esc(openHref) + '" target="_blank" rel="noopener">Open' + ICON_EXTERNAL + '</a>' +
|
|
5815
|
-
'</span></div>' +
|
|
5816
|
-
'</div>';
|
|
5817
|
-
}
|
|
5818
|
-
|
|
5819
|
-
// claude.ai can't iframe \u2014 surface the REAL page as a browser link (no
|
|
5820
|
-
// approximation, just not embedded). [BD-117]
|
|
5821
|
-
function linkBody(out, view) {
|
|
5822
|
-
var nm = esc(out.name || "Design system");
|
|
5823
|
-
return '<div class="bd-link-card">' +
|
|
5824
|
-
'<div class="bd-link-title">' + nm + '</div>' +
|
|
5825
|
-
'<p class="bd-link-desc">Open the live showcase to see every component, styled by this system.</p>' +
|
|
5826
|
-
'<a class="bd-cta bd-cta-lg" data-bd-open href="' + esc(view) + '" target="_blank" rel="noopener">Open live showcase' + ICON_EXTERNAL + '</a>' +
|
|
5827
|
-
'</div>';
|
|
5828
|
-
}
|
|
5829
|
-
|
|
5830
|
-
// The name + Fullscreen + Open footer, shared by the iframe / screenshot /
|
|
5831
|
-
// token previews. Name left, actions right (CSS handles the split). [BD-136]
|
|
5832
|
-
function footHtml(out) {
|
|
7192
|
+
// The name + Fullscreen + Open footer shared by screenshot and token previews.
|
|
7193
|
+
function footHtml(out, full) {
|
|
5833
7194
|
var view = safeUrl(out.viewUrl);
|
|
5834
|
-
return '<div class="bd-
|
|
5835
|
-
(canFullscreen() ? '<button class="bd-expand" data-bd-fullscreen type="button">Fullscreen' + ICON_EXPAND + '</button>' : "") +
|
|
5836
|
-
(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>' : "") +
|
|
5837
7198
|
'</span></div>';
|
|
5838
7199
|
}
|
|
5839
7200
|
|
|
5840
|
-
// A
|
|
5841
|
-
// Browser Rendering
|
|
5842
|
-
//
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
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">' +
|
|
5846
7206
|
'<img class="bd-shot" data-bd-shot src="' + esc(safeUrl(out.shotUrl)) + '" alt="' + esc(out.name || "Design system") + ' showcase" />' +
|
|
5847
7207
|
'</div>';
|
|
5848
7208
|
}
|
|
5849
7209
|
|
|
5850
|
-
// If the screenshot 404s
|
|
5851
|
-
// shows a preview rather than a broken image. [BD-136]
|
|
7210
|
+
// If the screenshot 404s or errors, show the token render instead.
|
|
5852
7211
|
function wireShotFallback() {
|
|
5853
7212
|
var img = root.querySelector("[data-bd-shot]");
|
|
5854
7213
|
if (!img) return;
|
|
5855
7214
|
img.addEventListener("error", function () { shotFailed = true; if (lastOut) render(lastOut); });
|
|
5856
7215
|
// The screenshot loads async; its real height only exists after it decodes.
|
|
5857
|
-
//
|
|
5858
|
-
//
|
|
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
|
|
5859
7218
|
// be complete before this runs). [BD-136]
|
|
5860
7219
|
img.addEventListener("load", scheduleResize);
|
|
5861
7220
|
if (img.complete && img.naturalHeight) scheduleResize();
|
|
@@ -5868,43 +7227,30 @@ ${DS_KIT_CSS}
|
|
|
5868
7227
|
syncHostFonts();
|
|
5869
7228
|
// Only an http(s) URL is usable; reject any other scheme up front.
|
|
5870
7229
|
var view = safeUrl(out.viewUrl);
|
|
5871
|
-
// The iframe must load a PUBLIC, cookieless URL (the sandbox has no session
|
|
5872
|
-
// cookie). previewUrl points at /preview/<fragmentId>; fall back to view if
|
|
5873
|
-
// the host didn't supply one (e.g. catalog DSes, whose view is public). [BD-164]
|
|
5874
|
-
var embed = safeUrl(out.previewUrl) || view;
|
|
5875
7230
|
var isError = out.status === "error";
|
|
5876
7231
|
var generating = out.status && out.status !== "completed" && !isError;
|
|
5877
7232
|
var completed = !isError && !generating;
|
|
5878
|
-
//
|
|
5879
|
-
//
|
|
5880
|
-
var useShot = !shotFailed && !!safeUrl(out.shotUrl);
|
|
5881
|
-
|
|
5882
|
-
//
|
|
5883
|
-
//
|
|
5884
|
-
// owner's editable /projects/<id> (they have the session cookie on the web,
|
|
5885
|
-
// 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]
|
|
5886
7239
|
if (completed && view && window.openai && window.openai.setOpenInAppUrl) {
|
|
5887
|
-
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 */ }
|
|
5888
7241
|
}
|
|
5889
7242
|
|
|
5890
|
-
// Fullscreen:
|
|
5891
|
-
// screenshot (scrollable) or the token showcase. [BD-136]
|
|
7243
|
+
// Fullscreen remains self-contained: show the full screenshot or token UI.
|
|
5892
7244
|
if (displayMode === "fullscreen" && completed) {
|
|
5893
|
-
if (
|
|
5894
|
-
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(); }
|
|
5895
7246
|
else { root.innerHTML = showcaseBody(out, true); wireShowcase(); }
|
|
5896
7247
|
return;
|
|
5897
7248
|
}
|
|
5898
7249
|
|
|
5899
|
-
// Inline completed:
|
|
5900
|
-
// (capped) or token showcase, + a footer (Fullscreen + Open). [BD-136]
|
|
7250
|
+
// Inline completed: capped screenshot or token showcase, plus clear actions.
|
|
5901
7251
|
if (completed) {
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
return;
|
|
5905
|
-
}
|
|
5906
|
-
var body = useShot ? shotBody(out, false) : showcaseBody(out, false);
|
|
5907
|
-
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>';
|
|
5908
7254
|
if (useShot) wireShotFallback();
|
|
5909
7255
|
return;
|
|
5910
7256
|
}
|
|
@@ -5916,7 +7262,7 @@ ${DS_KIT_CSS}
|
|
|
5916
7262
|
var hero = errorBody(out);
|
|
5917
7263
|
var cta = view
|
|
5918
7264
|
? '<div class="bd-cta-row"><span class="bd-cta-name">' + esc(out.name || "Design system") + '</span>' +
|
|
5919
|
-
'<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>'
|
|
5920
7266
|
: "";
|
|
5921
7267
|
root.innerHTML = '<div class="bd-card-shell">' + hero + cta + "</div>";
|
|
5922
7268
|
}
|
|
@@ -5925,9 +7271,9 @@ ${DS_KIT_CSS}
|
|
|
5925
7271
|
// The MCP Apps client emits ui/notifications/size-changed (ResizeObserver on
|
|
5926
7272
|
// documentElement + body); ChatGPT honors that and auto-sizes its widget.
|
|
5927
7273
|
// claude.ai web does NOT honor size-changed \u2014 it reads the widget's
|
|
5928
|
-
// documentElement height straight from the DOM and pins the
|
|
5929
|
-
// inline height the document carries. With no explicit height the
|
|
5930
|
-
// 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;
|
|
5931
7277
|
// claude-ai-mcp#69). So on the DOM-reading hosts we set
|
|
5932
7278
|
// document.documentElement.style.height ourselves, AFTER content + the async
|
|
5933
7279
|
// screenshot have real height. [BD-136]
|
|
@@ -5965,14 +7311,14 @@ ${DS_KIT_CSS}
|
|
|
5965
7311
|
if (typeof MutationObserver === "function") {
|
|
5966
7312
|
new MutationObserver(scheduleResize).observe(root, { childList: true, subtree: true });
|
|
5967
7313
|
}
|
|
5968
|
-
} catch (e) {}
|
|
7314
|
+
} catch (e) { /* best-effort: sandbox without MutationObserver; the height pin just updates less often */ }
|
|
5969
7315
|
try {
|
|
5970
7316
|
if (typeof ResizeObserver === "function") {
|
|
5971
7317
|
new ResizeObserver(scheduleResize).observe(root);
|
|
5972
7318
|
}
|
|
5973
|
-
} catch (e) {}
|
|
7319
|
+
} catch (e) { /* best-effort: sandbox without ResizeObserver; the height pin just updates less often */ }
|
|
5974
7320
|
|
|
5975
|
-
// The widget runs in a sandboxed
|
|
7321
|
+
// The widget runs in a sandboxed host; a bare <a target=_blank>
|
|
5976
7322
|
// cannot open a tab or navigate the top frame, so the CTA looks dead. Route the
|
|
5977
7323
|
// click through the host: MCP Apps app.openLink({url}) or OpenAI Apps
|
|
5978
7324
|
// openai.openExternal({href}). Falls through to the native anchor for hosts
|
|
@@ -5982,19 +7328,18 @@ ${DS_KIT_CSS}
|
|
|
5982
7328
|
var t = e.target;
|
|
5983
7329
|
if (!t || !t.closest) return;
|
|
5984
7330
|
if (t.closest("[data-bd-fullscreen]")) { e.preventDefault(); setDisplayMode("fullscreen"); return; }
|
|
5985
|
-
if (t.closest("[data-bd-collapse]")) { e.preventDefault(); setDisplayMode("inline"); return; }
|
|
5986
7331
|
var a = t.closest("a[data-bd-open]");
|
|
5987
7332
|
if (!a) return;
|
|
5988
7333
|
var url = a.getAttribute("href");
|
|
5989
7334
|
if (!url) return;
|
|
5990
|
-
if (
|
|
7335
|
+
if (window.openai && window.openai.openExternal) {
|
|
5991
7336
|
e.preventDefault();
|
|
5992
|
-
try {
|
|
7337
|
+
try { window.openai.openExternal({ href: url }); } catch (err) { /* best-effort: host declined openExternal; nothing else can open the link */ }
|
|
5993
7338
|
return;
|
|
5994
7339
|
}
|
|
5995
|
-
if (
|
|
7340
|
+
if (bdApp && bdApp.openLink) {
|
|
5996
7341
|
e.preventDefault();
|
|
5997
|
-
try {
|
|
7342
|
+
try { bdApp.openLink({ url: url }); } catch (err) { /* best-effort: host declined openLink; nothing else can open the link */ }
|
|
5998
7343
|
}
|
|
5999
7344
|
});
|
|
6000
7345
|
|
|
@@ -6041,9 +7386,9 @@ ${DS_KIT_CSS}
|
|
|
6041
7386
|
// this every later host-context-changed (theme/font) would rebuild the
|
|
6042
7387
|
// DOM and lose scroll + active-section state in fullscreen. [BD-117]
|
|
6043
7388
|
else if (ctx && ctx.availableDisplayModes && !sawDisplayModes && lastOut) { sawDisplayModes = true; render(lastOut); }
|
|
6044
|
-
} catch (e) {}
|
|
7389
|
+
} catch (e) { /* best-effort: a malformed host context must not kill the widget; keep the last rendered state */ }
|
|
6045
7390
|
};
|
|
6046
|
-
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 */ });
|
|
6047
7392
|
}
|
|
6048
7393
|
} catch (e) { /* host without MCP Apps support; OpenAI bridge or static text covers it */ }
|
|
6049
7394
|
})();
|
|
@@ -6053,8 +7398,132 @@ ${DS_KIT_CSS}
|
|
|
6053
7398
|
}
|
|
6054
7399
|
});
|
|
6055
7400
|
|
|
6056
|
-
// ../shared/src/mcp/
|
|
7401
|
+
// ../shared/src/mcp/shopify-primitives.ts
|
|
6057
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";
|
|
6058
7527
|
function installCommandWithInit(installCommand) {
|
|
6059
7528
|
return `${SHADCN_INIT_IF_MISSING}
|
|
6060
7529
|
|
|
@@ -6092,7 +7561,34 @@ async function fetchProjectTokensCss(projectId, apiFetch, authContext) {
|
|
|
6092
7561
|
if (!res.ok) return null;
|
|
6093
7562
|
const json = await res.json();
|
|
6094
7563
|
return { tokensCss: tokensCssFromRegistry(json), name: json.name, files: json.files ?? [] };
|
|
6095
|
-
} 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);
|
|
6096
7592
|
return null;
|
|
6097
7593
|
}
|
|
6098
7594
|
}
|
|
@@ -6133,25 +7629,50 @@ function completedShopifyKitStructuredContent(name, viewUrl, tokensCss, themeFil
|
|
|
6133
7629
|
tokensCss
|
|
6134
7630
|
};
|
|
6135
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
|
+
}
|
|
6136
7647
|
async function fetchCatalog(designSystemId) {
|
|
6137
7648
|
try {
|
|
6138
7649
|
const [globalsRes, ...componentResults] = await Promise.all([
|
|
6139
7650
|
fetch(`${REGISTRY_BASE_URL}/${designSystemId}/globals.json`),
|
|
6140
7651
|
...CATALOG_KIT_COMPONENTS.map(
|
|
6141
|
-
(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
|
+
})
|
|
6142
7656
|
)
|
|
6143
7657
|
]);
|
|
6144
7658
|
if (!globalsRes.ok) return { tokensCss: "", files: [] };
|
|
6145
7659
|
const globals = REGISTRY_ITEM_SCHEMA.safeParse(await globalsRes.json());
|
|
6146
7660
|
if (!globals.success) return { tokensCss: "", files: [] };
|
|
6147
7661
|
const files = [...globals.data.files ?? []];
|
|
6148
|
-
for (const res of componentResults) {
|
|
6149
|
-
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
|
+
}
|
|
6150
7670
|
const item = REGISTRY_ITEM_SCHEMA.safeParse(await res.json().catch(() => null));
|
|
6151
7671
|
if (item.success && item.data.files) files.push(...item.data.files);
|
|
6152
7672
|
}
|
|
6153
7673
|
return { tokensCss: tokensCssFromRegistry({ cssVars: globals.data.cssVars, files }), files };
|
|
6154
|
-
} catch {
|
|
7674
|
+
} catch (error) {
|
|
7675
|
+
console.warn(`catalog registry fetch failed for ${designSystemId}`, error);
|
|
6155
7676
|
return { tokensCss: "", files: [] };
|
|
6156
7677
|
}
|
|
6157
7678
|
}
|
|
@@ -6179,8 +7700,8 @@ function registerDesignSystemTools({
|
|
|
6179
7700
|
authContext,
|
|
6180
7701
|
apiFetch = globalThis.fetch
|
|
6181
7702
|
}, { enableShopifyKit = true } = {}) {
|
|
6182
|
-
const searchInputSchema =
|
|
6183
|
-
query:
|
|
7703
|
+
const searchInputSchema = z6.object({
|
|
7704
|
+
query: z6.string().describe(
|
|
6184
7705
|
"The user's app, UI, industry, or visual direction. Examples: 'World Cup betting app for friends', 'warm editorial ecommerce', or 'professional fintech dashboard'."
|
|
6185
7706
|
)
|
|
6186
7707
|
});
|
|
@@ -6399,31 +7920,24 @@ ${fence3}` }],
|
|
|
6399
7920
|
}) => tracked("get-design-system-kit", { target: "shopify", designSystemId, projectId, context }, async () => {
|
|
6400
7921
|
if (projectId) {
|
|
6401
7922
|
const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
|
|
6402
|
-
const
|
|
6403
|
-
|
|
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) {
|
|
6404
7930
|
return {
|
|
6405
|
-
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}.` }],
|
|
6406
7932
|
structuredContent: { status: "error", name: `Project ${projectId.slice(0, 8)}`, viewUrl: viewUrl2, tokensCss: "" }
|
|
6407
7933
|
};
|
|
6408
7934
|
}
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
source: { kind: "project", projectId },
|
|
6416
|
-
componentKit: componentKit2
|
|
6417
|
-
});
|
|
6418
|
-
return {
|
|
6419
|
-
content: [{ type: "text", text: text2 }],
|
|
6420
|
-
structuredContent: completedShopifyKitStructuredContent(
|
|
6421
|
-
name,
|
|
6422
|
-
viewUrl2,
|
|
6423
|
-
result.tokensCss,
|
|
6424
|
-
buildDsShopifyFiles({ tokensCss: result.tokensCss, componentKit: componentKit2 })
|
|
6425
|
-
)
|
|
6426
|
-
};
|
|
7935
|
+
return completedShopifyKitResult(
|
|
7936
|
+
result.name,
|
|
7937
|
+
viewUrl2,
|
|
7938
|
+
{ kind: "project", projectId },
|
|
7939
|
+
result.primitives
|
|
7940
|
+
);
|
|
6427
7941
|
}
|
|
6428
7942
|
if (!designSystemId) {
|
|
6429
7943
|
return {
|
|
@@ -6431,7 +7945,10 @@ ${fence3}` }],
|
|
|
6431
7945
|
structuredContent: { status: "error", name: "Missing design system", tokensCss: "" }
|
|
6432
7946
|
};
|
|
6433
7947
|
}
|
|
6434
|
-
const ds = await provider.getDesignSystemById(designSystemId, {
|
|
7948
|
+
const ds = await provider.getDesignSystemById(designSystemId, {
|
|
7949
|
+
includeComponents: false,
|
|
7950
|
+
includeShopifyPrimitives: true
|
|
7951
|
+
});
|
|
6435
7952
|
if (!ds) {
|
|
6436
7953
|
return {
|
|
6437
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.` }],
|
|
@@ -6439,30 +7956,20 @@ ${fence3}` }],
|
|
|
6439
7956
|
};
|
|
6440
7957
|
}
|
|
6441
7958
|
const viewUrl = `${APP_BASE_URL}/design-systems/${designSystemId}`;
|
|
6442
|
-
const
|
|
6443
|
-
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}.`;
|
|
6444
7962
|
return {
|
|
6445
|
-
content: [{ type: "text", text
|
|
7963
|
+
content: [{ type: "text", text }],
|
|
6446
7964
|
structuredContent: { status: "error", name: ds.metadata.title, viewUrl, tokensCss: "" }
|
|
6447
7965
|
};
|
|
6448
7966
|
}
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
name: ds.metadata.title,
|
|
6452
|
-
tokensCss,
|
|
7967
|
+
return completedShopifyKitResult(
|
|
7968
|
+
ds.metadata.title,
|
|
6453
7969
|
viewUrl,
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
return {
|
|
6458
|
-
content: [{ type: "text", text }],
|
|
6459
|
-
structuredContent: completedShopifyKitStructuredContent(
|
|
6460
|
-
ds.metadata.title,
|
|
6461
|
-
viewUrl,
|
|
6462
|
-
tokensCss,
|
|
6463
|
-
buildDsShopifyFiles({ tokensCss, componentKit })
|
|
6464
|
-
)
|
|
6465
|
-
};
|
|
7970
|
+
{ kind: "catalog", designSystemId },
|
|
7971
|
+
primitives
|
|
7972
|
+
);
|
|
6466
7973
|
});
|
|
6467
7974
|
server.registerTool(
|
|
6468
7975
|
"get-design-system-kit",
|
|
@@ -6494,22 +8001,22 @@ existing setup; otherwise use the default Base UI. Both flavors install to the
|
|
|
6494
8001
|
same components/ui/ paths.
|
|
6495
8002
|
|
|
6496
8003
|
${BUILD_TARGET_RULE}`,
|
|
6497
|
-
inputSchema:
|
|
8004
|
+
inputSchema: z6.object({
|
|
6498
8005
|
// Off-pilot callers must not see the target at all, not merely be refused it.
|
|
6499
|
-
target:
|
|
8006
|
+
target: z6.enum(
|
|
6500
8007
|
enableShopifyKit ? ["react", "html", "shopify"] : ["react", "html"]
|
|
6501
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.' : ""}`),
|
|
6502
|
-
designSystemId:
|
|
6503
|
-
projectId:
|
|
6504
|
-
component:
|
|
6505
|
-
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.'),
|
|
6506
8013
|
context: CONTEXT_ARG
|
|
6507
8014
|
}),
|
|
6508
8015
|
outputSchema: buildKitOutputSchema(enableShopifyKit),
|
|
6509
8016
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
6510
8017
|
// This tool returns build instructions, not a visual preview. Attaching
|
|
6511
|
-
// the preview template makes ChatGPT render a
|
|
6512
|
-
// 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
|
|
6513
8020
|
// though the kit succeeded. preview-design-system remains the dedicated
|
|
6514
8021
|
// interactive preview path. [BD-240]
|
|
6515
8022
|
},
|
|
@@ -6723,15 +8230,18 @@ _${installNote}_`,
|
|
|
6723
8230
|
|
|
6724
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\`.
|
|
6725
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
|
+
|
|
6726
8235
|
How it works:
|
|
6727
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.
|
|
6728
8237
|
2. Generation runs in the background and typically takes 1-3 minutes.
|
|
6729
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.
|
|
6730
8239
|
|
|
6731
8240
|
Share the project URL with the user right away so they can watch the generation live.`,
|
|
6732
|
-
inputSchema:
|
|
6733
|
-
prompt:
|
|
6734
|
-
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).")
|
|
6735
8245
|
}),
|
|
6736
8246
|
annotations: {
|
|
6737
8247
|
readOnlyHint: false,
|
|
@@ -6739,7 +8249,7 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6739
8249
|
openWorldHint: false
|
|
6740
8250
|
}
|
|
6741
8251
|
},
|
|
6742
|
-
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 () => {
|
|
6743
8253
|
const startUrl = `${APP_BASE_URL}/api/projects/anonymous`;
|
|
6744
8254
|
let projectId;
|
|
6745
8255
|
const requestHeaders = { "Content-Type": "application/json" };
|
|
@@ -6753,7 +8263,8 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6753
8263
|
headers: requestHeaders,
|
|
6754
8264
|
body: JSON.stringify({
|
|
6755
8265
|
value: prompt,
|
|
6756
|
-
...baseDesignSystemId ? { designSystemId: baseDesignSystemId } : {}
|
|
8266
|
+
...baseDesignSystemId ? { designSystemId: baseDesignSystemId } : {},
|
|
8267
|
+
...brandCss ? { brandCss } : {}
|
|
6757
8268
|
})
|
|
6758
8269
|
});
|
|
6759
8270
|
if (!startRes.ok) {
|
|
@@ -6774,10 +8285,10 @@ Share the project URL with the user right away so they can watch the generation
|
|
|
6774
8285
|
"",
|
|
6775
8286
|
`Project id: \`${projectId}\``,
|
|
6776
8287
|
"",
|
|
6777
|
-
...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:`],
|
|
6778
8289
|
projectUrl,
|
|
6779
8290
|
"",
|
|
6780
|
-
`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.`,
|
|
6781
8292
|
inlineBuildMd(projectId)
|
|
6782
8293
|
];
|
|
6783
8294
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -6819,10 +8330,17 @@ npx shadcn@latest add ${registryUrl}
|
|
|
6819
8330
|
}
|
|
6820
8331
|
} catch {
|
|
6821
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
|
+
] : [];
|
|
6822
8339
|
const lines = [
|
|
6823
8340
|
`# Design system ready`,
|
|
6824
8341
|
"",
|
|
6825
|
-
...
|
|
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:`],
|
|
6826
8344
|
projectUrl,
|
|
6827
8345
|
"",
|
|
6828
8346
|
`Project id: \`${projectId}\``,
|
|
@@ -6838,7 +8356,6 @@ npx shadcn@latest add ${registryUrl}
|
|
|
6838
8356
|
"",
|
|
6839
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.`
|
|
6840
8358
|
];
|
|
6841
|
-
const previewUrl = project.previewFragmentId ? `${APP_BASE_URL}/preview/${project.previewFragmentId}` : projectUrl;
|
|
6842
8359
|
const shotUrl = project.previewFragmentId ? `${APP_BASE_URL}/api/og/preview?fragment=${project.previewFragmentId}` : void 0;
|
|
6843
8360
|
return {
|
|
6844
8361
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
@@ -6876,8 +8393,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6876
8393
|
const statusConfig = {
|
|
6877
8394
|
title: "Get Design System Generation Status",
|
|
6878
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.`,
|
|
6879
|
-
inputSchema:
|
|
6880
|
-
projectId:
|
|
8396
|
+
inputSchema: z6.object({
|
|
8397
|
+
projectId: z6.string().min(1).describe("The project id returned by create-design-system."),
|
|
6881
8398
|
context: CONTEXT_ARG
|
|
6882
8399
|
}),
|
|
6883
8400
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -6900,8 +8417,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6900
8417
|
{
|
|
6901
8418
|
title: "Preview a Design System",
|
|
6902
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.`,
|
|
6903
|
-
inputSchema:
|
|
6904
|
-
designSystemId:
|
|
8420
|
+
inputSchema: z6.object({
|
|
8421
|
+
designSystemId: z6.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'."),
|
|
6905
8422
|
context: CONTEXT_ARG
|
|
6906
8423
|
}),
|
|
6907
8424
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
@@ -6951,8 +8468,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
6951
8468
|
status: "completed",
|
|
6952
8469
|
name: ds.metadata.title,
|
|
6953
8470
|
viewUrl,
|
|
6954
|
-
// Real screenshot of the showcase
|
|
6955
|
-
// (claude.ai). The widget shows this as the inline image. [BD-136]
|
|
8471
|
+
// Real screenshot of the showcase for the self-contained widget.
|
|
6956
8472
|
shotUrl: `${APP_BASE_URL}/api/og/preview?id=${encodeURIComponent(designSystemId)}`,
|
|
6957
8473
|
tokensCss
|
|
6958
8474
|
}
|
|
@@ -6967,55 +8483,26 @@ var init_design_system_tools = __esm({
|
|
|
6967
8483
|
init_guardrails();
|
|
6968
8484
|
init_html_kit();
|
|
6969
8485
|
init_shopify_kit();
|
|
8486
|
+
init_shopify_primitives();
|
|
6970
8487
|
init_preview_html();
|
|
6971
8488
|
init_shared();
|
|
6972
|
-
REGISTRY_ITEM_SCHEMA =
|
|
6973
|
-
cssVars:
|
|
6974
|
-
light:
|
|
6975
|
-
dark:
|
|
6976
|
-
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()
|
|
6977
8494
|
}).optional(),
|
|
6978
|
-
files:
|
|
8495
|
+
files: z6.array(z6.object({ path: z6.string().optional(), content: z6.string().optional() })).optional()
|
|
6979
8496
|
});
|
|
6980
8497
|
SHADCN_INIT_IF_MISSING = "[ -f components.json ] || npx shadcn@latest init -d -y";
|
|
6981
8498
|
SAFE_COMPONENT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/i;
|
|
6982
8499
|
CATALOG_KIT_COMPONENTS = ["button", "input", "card", "badge", "switch"];
|
|
6983
|
-
DS_INDEX_SCHEMA =
|
|
6984
|
-
baseComponents:
|
|
8500
|
+
DS_INDEX_SCHEMA = z6.object({
|
|
8501
|
+
baseComponents: z6.array(z6.object({ name: z6.string() })).optional()
|
|
6985
8502
|
});
|
|
6986
8503
|
}
|
|
6987
8504
|
});
|
|
6988
8505
|
|
|
6989
|
-
// ../shared/src/registry-css.ts
|
|
6990
|
-
function parseCssVars(css) {
|
|
6991
|
-
const out = { theme: {}, light: {}, dark: {} };
|
|
6992
|
-
const matchBlock = (selector) => {
|
|
6993
|
-
const re = new RegExp(`${selector}\\s*\\{([^}]*)\\}`, "g");
|
|
6994
|
-
const blocks = [];
|
|
6995
|
-
let m;
|
|
6996
|
-
while (m = re.exec(css)) blocks.push(m[1]);
|
|
6997
|
-
return blocks.join("\n");
|
|
6998
|
-
};
|
|
6999
|
-
const extract = (body) => {
|
|
7000
|
-
const result = {};
|
|
7001
|
-
const clean = body.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
7002
|
-
const declRe = /--([a-zA-Z0-9_-]+)\s*:\s*([^;]+);/g;
|
|
7003
|
-
let m;
|
|
7004
|
-
while (m = declRe.exec(clean)) {
|
|
7005
|
-
result[m[1]] = m[2].replace(/\s+/g, " ").trim();
|
|
7006
|
-
}
|
|
7007
|
-
return result;
|
|
7008
|
-
};
|
|
7009
|
-
out.light = extract(matchBlock(":root"));
|
|
7010
|
-
out.dark = extract(matchBlock("\\.dark"));
|
|
7011
|
-
return out;
|
|
7012
|
-
}
|
|
7013
|
-
var init_registry_css = __esm({
|
|
7014
|
-
"../shared/src/registry-css.ts"() {
|
|
7015
|
-
"use strict";
|
|
7016
|
-
}
|
|
7017
|
-
});
|
|
7018
|
-
|
|
7019
8506
|
// ../shared/src/mcp/color-to-oklch.ts
|
|
7020
8507
|
function clamp01(n) {
|
|
7021
8508
|
return Math.min(1, Math.max(0, n));
|
|
@@ -7077,6 +8564,16 @@ function parseRgb(value) {
|
|
|
7077
8564
|
a: parseAlpha(alpha)
|
|
7078
8565
|
});
|
|
7079
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
|
+
}
|
|
7080
8577
|
function hueToRgb(p, q, t) {
|
|
7081
8578
|
let tt = t;
|
|
7082
8579
|
if (tt < 0) tt += 1;
|
|
@@ -7163,6 +8660,7 @@ function toOklch(value) {
|
|
|
7163
8660
|
if (v.startsWith("#")) rgba = parseHex(v);
|
|
7164
8661
|
else if (/^rgba?\(/i.test(v)) rgba = parseRgb(v);
|
|
7165
8662
|
else if (/^hsla?\(/i.test(v)) rgba = parseHsl(v);
|
|
8663
|
+
else if (v.includes(",")) rgba = parseBareTriplet(v);
|
|
7166
8664
|
return rgba ? rgbaToOklch(rgba) : null;
|
|
7167
8665
|
}
|
|
7168
8666
|
var NAMED_HEX, PASSTHROUGH_KEYWORDS, NON_COLOR_KEYWORDS;
|
|
@@ -7361,7 +8859,7 @@ function normalizeMap(raw) {
|
|
|
7361
8859
|
}
|
|
7362
8860
|
return { tokens, kinds };
|
|
7363
8861
|
}
|
|
7364
|
-
function
|
|
8862
|
+
function collectCssVars(css) {
|
|
7365
8863
|
const { light: rootLight, dark: rootDark } = parseCssVars(css);
|
|
7366
8864
|
const themeVars = extractThemeBlock(css);
|
|
7367
8865
|
const mergedLight = {};
|
|
@@ -7369,8 +8867,11 @@ function extractDesignSystem(css, name) {
|
|
|
7369
8867
|
mergedLight[denamespace(k)] = v;
|
|
7370
8868
|
}
|
|
7371
8869
|
Object.assign(mergedLight, rootLight);
|
|
7372
|
-
|
|
7373
|
-
|
|
8870
|
+
return { light: mergedLight, dark: rootDark };
|
|
8871
|
+
}
|
|
8872
|
+
function buildExtractionResult(raw, name) {
|
|
8873
|
+
const light = normalizeMap(raw.light);
|
|
8874
|
+
const dark = normalizeMap(raw.dark);
|
|
7374
8875
|
const hasLight = Object.keys(light.tokens).length > 0;
|
|
7375
8876
|
const hasDark = Object.keys(dark.tokens).length > 0;
|
|
7376
8877
|
const themeMode = hasLight && hasDark ? "dual" : hasDark ? "dark" : hasLight ? "light" : "none";
|
|
@@ -7435,6 +8936,8 @@ function formatExtraction(result) {
|
|
|
7435
8936
|
};
|
|
7436
8937
|
const meta = [
|
|
7437
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,
|
|
7438
8941
|
`**Theme:** ${modeLabel[result.themeMode]}`,
|
|
7439
8942
|
`**Tokens extracted:** ${result.tokenCount}`,
|
|
7440
8943
|
`**Colors normalized to oklch.**`
|
|
@@ -7461,7 +8964,7 @@ function formatExtraction(result) {
|
|
|
7461
8964
|
}
|
|
7462
8965
|
sections.push(
|
|
7463
8966
|
"## Token sheet (oklch-normalized, paste into globals.css)\n\n```css\n" + blocks.join("\n\n") + "\n```",
|
|
7464
|
-
"## 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."
|
|
7465
8968
|
);
|
|
7466
8969
|
return sections.join("\n\n");
|
|
7467
8970
|
}
|
|
@@ -7512,9 +9015,198 @@ var init_extract_design_system = __esm({
|
|
|
7512
9015
|
}
|
|
7513
9016
|
});
|
|
7514
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
|
+
|
|
7515
9194
|
// ../shared/src/mcp/tools/extract-tools.ts
|
|
7516
|
-
import { z as
|
|
7517
|
-
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 }) {
|
|
7518
9210
|
server.registerTool(
|
|
7519
9211
|
"extract-design-system",
|
|
7520
9212
|
{
|
|
@@ -7525,32 +9217,100 @@ Use this when the user says "match our existing style", "use our design system",
|
|
|
7525
9217
|
|
|
7526
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.
|
|
7527
9219
|
|
|
7528
|
-
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
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
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.")
|
|
7533
9240
|
}),
|
|
7534
9241
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
7535
9242
|
},
|
|
7536
|
-
async ({ css, name, maxTokens }) => tracked("extract-design-system", { name: name ?? null }, async () => {
|
|
7537
|
-
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);
|
|
7538
9257
|
let text = formatExtraction(result);
|
|
7539
9258
|
if (maxTokens) text = truncateToTokens(text, maxTokens);
|
|
7540
9259
|
return { content: [{ type: "text", text }] };
|
|
7541
9260
|
})
|
|
7542
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
|
+
);
|
|
7543
9302
|
}
|
|
7544
9303
|
var init_extract_tools = __esm({
|
|
7545
9304
|
"../shared/src/mcp/tools/extract-tools.ts"() {
|
|
7546
9305
|
"use strict";
|
|
7547
9306
|
init_shared();
|
|
7548
9307
|
init_extract_design_system();
|
|
9308
|
+
init_brand_sources();
|
|
7549
9309
|
}
|
|
7550
9310
|
});
|
|
7551
9311
|
|
|
7552
9312
|
// ../shared/src/mcp/tools/illustration-tools.ts
|
|
7553
|
-
import { z as
|
|
9313
|
+
import { z as z8 } from "zod";
|
|
7554
9314
|
function fail(message) {
|
|
7555
9315
|
return {
|
|
7556
9316
|
content: [{ type: "text", text: message }],
|
|
@@ -7571,7 +9331,7 @@ function registerIllustrationTools({
|
|
|
7571
9331
|
authContext,
|
|
7572
9332
|
apiFetch = globalThis.fetch
|
|
7573
9333
|
}) {
|
|
7574
|
-
const
|
|
9334
|
+
const authHeaders3 = () => {
|
|
7575
9335
|
const headers = { "Content-Type": "application/json" };
|
|
7576
9336
|
if (authContext) {
|
|
7577
9337
|
const bearer2 = authContext.kind === "api-key" ? authContext.apiKey : authContext.accessToken;
|
|
@@ -7596,11 +9356,11 @@ How to call it:
|
|
|
7596
9356
|
4. Stop when \`faults\` is empty.
|
|
7597
9357
|
|
|
7598
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.`,
|
|
7599
|
-
inputSchema:
|
|
7600
|
-
svg:
|
|
7601
|
-
intent:
|
|
7602
|
-
animated:
|
|
7603
|
-
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.")
|
|
7604
9364
|
}),
|
|
7605
9365
|
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
7606
9366
|
annotations: READ_ONLY_TOOL_ANNOTATIONS
|
|
@@ -7617,7 +9377,7 @@ Markup carrying script, event handlers, remote references, or styles that reach
|
|
|
7617
9377
|
try {
|
|
7618
9378
|
const res = await apiFetch(`${APP_BASE_URL}/api/illustrations/review`, {
|
|
7619
9379
|
method: "POST",
|
|
7620
|
-
headers:
|
|
9380
|
+
headers: authHeaders3(),
|
|
7621
9381
|
signal: AbortSignal.timeout(6e4),
|
|
7622
9382
|
body: JSON.stringify({ svg, intent, animated: animated ?? false, reviewToken })
|
|
7623
9383
|
});
|
|
@@ -7669,16 +9429,16 @@ var init_illustration_tools = __esm({
|
|
|
7669
9429
|
init_shared();
|
|
7670
9430
|
REVIEW_OUTPUT_SCHEMA = {
|
|
7671
9431
|
/** Empty when nothing is visibly wrong. */
|
|
7672
|
-
faults:
|
|
9432
|
+
faults: z8.array(z8.string()),
|
|
7673
9433
|
/** Group ids in paint order, the parts an animation can target. */
|
|
7674
|
-
layers:
|
|
9434
|
+
layers: z8.array(z8.string()),
|
|
7675
9435
|
/** False when the markup was refused; faults says why. */
|
|
7676
|
-
reviewed:
|
|
9436
|
+
reviewed: z8.boolean(),
|
|
7677
9437
|
/** True when the drawing could not be rendered, so only structure was checked. */
|
|
7678
|
-
unseen:
|
|
9438
|
+
unseen: z8.boolean(),
|
|
7679
9439
|
/** Pass this back to review the same drawing again without spending another credit. */
|
|
7680
|
-
reviewToken:
|
|
7681
|
-
message:
|
|
9440
|
+
reviewToken: z8.string(),
|
|
9441
|
+
message: z8.string()
|
|
7682
9442
|
};
|
|
7683
9443
|
}
|
|
7684
9444
|
});
|
|
@@ -7715,15 +9475,13 @@ var init_preview_resource = __esm({
|
|
|
7715
9475
|
prefersBorder: true,
|
|
7716
9476
|
csp: {
|
|
7717
9477
|
connectDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7718
|
-
resourceDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS]
|
|
7719
|
-
frameDomains: WIDGET_FRAME_DOMAINS
|
|
9478
|
+
resourceDomains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS]
|
|
7720
9479
|
},
|
|
7721
9480
|
domain: WIDGET_APP_DOMAIN
|
|
7722
9481
|
},
|
|
7723
9482
|
"openai/widgetCSP": {
|
|
7724
9483
|
connect_domains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7725
9484
|
resource_domains: [APP_BASE_URL, ...WIDGET_RESOURCE_DOMAINS],
|
|
7726
|
-
frame_domains: WIDGET_FRAME_DOMAINS,
|
|
7727
9485
|
redirect_domains: [APP_BASE_URL]
|
|
7728
9486
|
}
|
|
7729
9487
|
};
|
|
@@ -7731,9 +9489,9 @@ var init_preview_resource = __esm({
|
|
|
7731
9489
|
});
|
|
7732
9490
|
|
|
7733
9491
|
// ../shared/src/mcp/tools.ts
|
|
7734
|
-
function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch) {
|
|
9492
|
+
function registerAllTools(server, provider, onToolUsage, authContext, options = {}, apiFetch = globalThis.fetch, userId) {
|
|
7735
9493
|
const { track, tracked } = createTracking(server, onToolUsage);
|
|
7736
|
-
const deps = { server, provider, track, tracked, authContext, apiFetch };
|
|
9494
|
+
const deps = { server, provider, track, tracked, authContext, apiFetch, userId };
|
|
7737
9495
|
registerPrincipleTools(deps, options);
|
|
7738
9496
|
registerComprehensionTools(deps);
|
|
7739
9497
|
registerIconTools(deps);
|
|
@@ -7761,9 +9519,22 @@ var init_tools = __esm({
|
|
|
7761
9519
|
var mcp_exports = {};
|
|
7762
9520
|
__export(mcp_exports, {
|
|
7763
9521
|
BUILD_TARGET_RULE: () => BUILD_TARGET_RULE,
|
|
9522
|
+
GUIDANCE_TOOLS: () => GUIDANCE_TOOLS,
|
|
7764
9523
|
INSTRUCTIONS: () => INSTRUCTIONS,
|
|
9524
|
+
MCP_SESSION_WINDOW_MS: () => MCP_SESSION_WINDOW_MS,
|
|
9525
|
+
REVIEW_TOOLS: () => REVIEW_TOOLS,
|
|
7765
9526
|
SEARCH_TOOLS: () => SEARCH_TOOLS,
|
|
7766
|
-
|
|
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
|
|
7767
9538
|
});
|
|
7768
9539
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7769
9540
|
function createMcpServer(options) {
|
|
@@ -7776,7 +9547,8 @@ function createMcpServer(options) {
|
|
|
7776
9547
|
enableReactNativeGuide = true,
|
|
7777
9548
|
enableThreeJsGuide = true,
|
|
7778
9549
|
enableShopifyKit = true,
|
|
7779
|
-
apiFetch = globalThis.fetch
|
|
9550
|
+
apiFetch = globalThis.fetch,
|
|
9551
|
+
userId
|
|
7780
9552
|
} = options;
|
|
7781
9553
|
const server = new McpServer(
|
|
7782
9554
|
{ name, version },
|
|
@@ -7792,7 +9564,8 @@ function createMcpServer(options) {
|
|
|
7792
9564
|
enableThreeJsGuide,
|
|
7793
9565
|
enableShopifyKit
|
|
7794
9566
|
},
|
|
7795
|
-
apiFetch
|
|
9567
|
+
apiFetch,
|
|
9568
|
+
userId
|
|
7796
9569
|
);
|
|
7797
9570
|
return server;
|
|
7798
9571
|
}
|
|
@@ -7803,7 +9576,9 @@ var init_mcp = __esm({
|
|
|
7803
9576
|
init_tools();
|
|
7804
9577
|
init_tools();
|
|
7805
9578
|
init_instructions();
|
|
9579
|
+
init_session_guidance();
|
|
7806
9580
|
init_shared();
|
|
9581
|
+
init_shopify_primitives();
|
|
7807
9582
|
}
|
|
7808
9583
|
});
|
|
7809
9584
|
|
|
@@ -7813,7 +9588,9 @@ __export(api_client_exports, {
|
|
|
7813
9588
|
getDesignSystem: () => getDesignSystem,
|
|
7814
9589
|
getDesignSystemComponents: () => getDesignSystemComponents,
|
|
7815
9590
|
getPrinciple: () => getPrinciple,
|
|
9591
|
+
getProjectShopifyPrimitives: () => getProjectShopifyPrimitives,
|
|
7816
9592
|
getReviewRules: () => getReviewRules,
|
|
9593
|
+
getSessionGuidance: () => getSessionGuidance,
|
|
7817
9594
|
listPrinciples: () => listPrinciples,
|
|
7818
9595
|
searchDesignSystems: () => searchDesignSystems,
|
|
7819
9596
|
searchIconLibraries: () => searchIconLibraries,
|
|
@@ -7848,11 +9625,14 @@ async function searchDesignSystems(query, limit = 5) {
|
|
|
7848
9625
|
`/v1/design-systems/search?q=${encodeURIComponent(query)}&limit=${limit}`
|
|
7849
9626
|
);
|
|
7850
9627
|
}
|
|
7851
|
-
async function getDesignSystem(id,
|
|
7852
|
-
const params =
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
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}`);
|
|
7856
9636
|
}
|
|
7857
9637
|
async function getDesignSystemComponents(id, names, includeCode = true) {
|
|
7858
9638
|
const params = new URLSearchParams();
|
|
@@ -7863,6 +9643,9 @@ async function getDesignSystemComponents(id, names, includeCode = true) {
|
|
|
7863
9643
|
`/v1/design-systems/${encodeURIComponent(id)}/components${query}`
|
|
7864
9644
|
);
|
|
7865
9645
|
}
|
|
9646
|
+
async function getProjectShopifyPrimitives(projectId) {
|
|
9647
|
+
return request(`/projects/${encodeURIComponent(projectId)}/shopify/all.json`);
|
|
9648
|
+
}
|
|
7866
9649
|
async function listPrinciples(area) {
|
|
7867
9650
|
const params = area ? `?area=${area}` : "";
|
|
7868
9651
|
return request(`/v1/principles${params}`);
|
|
@@ -7894,13 +9677,20 @@ async function getReviewRules(category = "all") {
|
|
|
7894
9677
|
const params = category !== "all" ? `?category=${encodeURIComponent(category)}` : "";
|
|
7895
9678
|
return request(`/v1/review-rules${params}`);
|
|
7896
9679
|
}
|
|
9680
|
+
async function getSessionGuidance(windowMs) {
|
|
9681
|
+
const params = new URLSearchParams({ windowMs: String(Math.round(windowMs)) });
|
|
9682
|
+
return request(`/v1/session-guidance?${params}`);
|
|
9683
|
+
}
|
|
7897
9684
|
function postAlert(body) {
|
|
7898
9685
|
fetch(`${API_URL2}/api/internal/alert`, {
|
|
7899
9686
|
method: "POST",
|
|
7900
9687
|
headers: { "Content-Type": "application/json" },
|
|
7901
9688
|
body: JSON.stringify(body),
|
|
7902
9689
|
signal: AbortSignal.timeout(5e3)
|
|
7903
|
-
}).
|
|
9690
|
+
}).then((res) => {
|
|
9691
|
+
if (!res.ok) console.warn(`postAlert failed: HTTP ${res.status}`);
|
|
9692
|
+
}).catch((err) => {
|
|
9693
|
+
console.warn("postAlert failed", err);
|
|
7904
9694
|
});
|
|
7905
9695
|
}
|
|
7906
9696
|
function trackToolUsage(tool, params, context = {}) {
|
|
@@ -7957,17 +9747,36 @@ function createHttpDataProvider() {
|
|
|
7957
9747
|
return results;
|
|
7958
9748
|
},
|
|
7959
9749
|
async getDesignSystemById(id, options = {}) {
|
|
7960
|
-
const systemRes = await getDesignSystem(id,
|
|
9750
|
+
const systemRes = await getDesignSystem(id, {
|
|
9751
|
+
includeHtml: options.includeHtml,
|
|
9752
|
+
includeShopifyPrimitives: options.includeShopifyPrimitives
|
|
9753
|
+
});
|
|
7961
9754
|
const componentsRes = options.includeComponents === false ? { components: [] } : await getDesignSystemComponents(
|
|
7962
9755
|
id,
|
|
7963
9756
|
options.componentNames,
|
|
7964
9757
|
options.includeComponentCode
|
|
7965
9758
|
);
|
|
7966
9759
|
return {
|
|
7967
|
-
|
|
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
|
+
},
|
|
7968
9769
|
components: componentsRes.components
|
|
7969
9770
|
};
|
|
7970
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
|
+
},
|
|
7971
9780
|
async listPrinciples(area) {
|
|
7972
9781
|
const { principles } = await listPrinciples(area);
|
|
7973
9782
|
return principles;
|
|
@@ -7975,7 +9784,8 @@ function createHttpDataProvider() {
|
|
|
7975
9784
|
async getPrincipleByTopic(topic, area) {
|
|
7976
9785
|
try {
|
|
7977
9786
|
return await getPrinciple(topic, area);
|
|
7978
|
-
} catch {
|
|
9787
|
+
} catch (err) {
|
|
9788
|
+
console.warn(`getPrincipleByTopic(${topic}) failed`, err);
|
|
7979
9789
|
return null;
|
|
7980
9790
|
}
|
|
7981
9791
|
},
|
|
@@ -7987,6 +9797,23 @@ function createHttpDataProvider() {
|
|
|
7987
9797
|
const { rules } = await getReviewRules(category);
|
|
7988
9798
|
return rules;
|
|
7989
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
|
+
},
|
|
7990
9817
|
async searchIconLibraries(query, limit = 5) {
|
|
7991
9818
|
const { results } = await searchIconLibraries(query, limit);
|
|
7992
9819
|
return results;
|
|
@@ -8013,6 +9840,7 @@ function createHttpDataProvider() {
|
|
|
8013
9840
|
var init_http_data_provider = __esm({
|
|
8014
9841
|
"src/lib/http-data-provider.ts"() {
|
|
8015
9842
|
"use strict";
|
|
9843
|
+
init_mcp();
|
|
8016
9844
|
init_api_client();
|
|
8017
9845
|
}
|
|
8018
9846
|
});
|
|
@@ -8060,11 +9888,7 @@ __export(mcp_debug_log_exports, {
|
|
|
8060
9888
|
mcpDebugLogPath: () => mcpDebugLogPath
|
|
8061
9889
|
});
|
|
8062
9890
|
function mcpDebugLogPath() {
|
|
8063
|
-
|
|
8064
|
-
return typeof process !== "undefined" ? process.env?.MCP_DEBUG_LOG : void 0;
|
|
8065
|
-
} catch {
|
|
8066
|
-
return void 0;
|
|
8067
|
-
}
|
|
9891
|
+
return typeof process !== "undefined" ? process.env?.MCP_DEBUG_LOG : void 0;
|
|
8068
9892
|
}
|
|
8069
9893
|
function mcpDebugLog(entry) {
|
|
8070
9894
|
const path5 = mcpDebugLogPath();
|