standby-design-mcp 0.1.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/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/index.js +3458 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../core/src/math.ts
|
|
13
|
+
function round(n, decimals = 4) {
|
|
14
|
+
const f = Math.pow(10, decimals);
|
|
15
|
+
return Math.round(n * f) / f;
|
|
16
|
+
}
|
|
17
|
+
var init_math = __esm({
|
|
18
|
+
"../core/src/math.ts"() {
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ../core/src/clamp.ts
|
|
23
|
+
function generateClamp(minSize, maxSize, minVw = 375, maxVw = 1920) {
|
|
24
|
+
const lo = Math.min(minSize, maxSize);
|
|
25
|
+
const hi = Math.max(minSize, maxSize);
|
|
26
|
+
if (lo === hi) {
|
|
27
|
+
return `${round(lo)}rem`;
|
|
28
|
+
}
|
|
29
|
+
const minVwRem = minVw / 16;
|
|
30
|
+
const maxVwRem = maxVw / 16;
|
|
31
|
+
const slope = (hi - lo) / (maxVwRem - minVwRem);
|
|
32
|
+
const intercept = lo - slope * minVwRem;
|
|
33
|
+
const slopeVw = round(slope * 100);
|
|
34
|
+
const interceptRem = round(intercept);
|
|
35
|
+
const sign = interceptRem >= 0 ? "+" : "-";
|
|
36
|
+
const absIntercept = Math.abs(interceptRem);
|
|
37
|
+
const preferred = `${slopeVw}vw ${sign} ${absIntercept}rem`;
|
|
38
|
+
return `clamp(${round(lo)}rem, ${preferred}, ${round(hi)}rem)`;
|
|
39
|
+
}
|
|
40
|
+
var init_clamp = __esm({
|
|
41
|
+
"../core/src/clamp.ts"() {
|
|
42
|
+
init_math();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ../core/src/typography.ts
|
|
47
|
+
function applyTypography(levels, lineHeightOverrides, letterSpacingOverrides) {
|
|
48
|
+
return levels.map((l) => ({
|
|
49
|
+
...l,
|
|
50
|
+
lineHeight: lineHeightOverrides[l.level] ?? DEFAULT_LINE_HEIGHTS[l.level],
|
|
51
|
+
letterSpacing: letterSpacingOverrides[l.level] ?? DEFAULT_LETTER_SPACINGS[l.level]
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
var DEFAULT_LINE_HEIGHTS, DEFAULT_LETTER_SPACINGS;
|
|
55
|
+
var init_typography = __esm({
|
|
56
|
+
"../core/src/typography.ts"() {
|
|
57
|
+
DEFAULT_LINE_HEIGHTS = {
|
|
58
|
+
"display": 1.05,
|
|
59
|
+
"h1": 1.1,
|
|
60
|
+
"h2": 1.15,
|
|
61
|
+
"h3": 1.2,
|
|
62
|
+
"h4": 1.25,
|
|
63
|
+
"h5": 1.3,
|
|
64
|
+
"h6": 1.4,
|
|
65
|
+
"body-l": 1.5,
|
|
66
|
+
"body-m": 1.5,
|
|
67
|
+
"body-s": 1.5,
|
|
68
|
+
"caption": 1.4
|
|
69
|
+
};
|
|
70
|
+
DEFAULT_LETTER_SPACINGS = {
|
|
71
|
+
"display": -0.04,
|
|
72
|
+
"h1": -0.03,
|
|
73
|
+
"h2": -0.02,
|
|
74
|
+
"h3": -0.01,
|
|
75
|
+
"h4": 0,
|
|
76
|
+
"h5": 0,
|
|
77
|
+
"h6": 0,
|
|
78
|
+
"body-l": 0,
|
|
79
|
+
"body-m": 0,
|
|
80
|
+
"body-s": 0.01,
|
|
81
|
+
"caption": 0.01
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ../core/src/scale.ts
|
|
87
|
+
var scale_exports = {};
|
|
88
|
+
__export(scale_exports, {
|
|
89
|
+
DEFAULT_TRADITIONAL: () => DEFAULT_TRADITIONAL,
|
|
90
|
+
DEFAULT_TRADITIONAL_MOBILE: () => DEFAULT_TRADITIONAL_MOBILE,
|
|
91
|
+
LEVEL_LABELS: () => LEVEL_LABELS,
|
|
92
|
+
RATIO_PRESETS: () => RATIO_PRESETS,
|
|
93
|
+
TRADITIONAL_SIZES: () => TRADITIONAL_SIZES,
|
|
94
|
+
TYPE_LEVELS: () => TYPE_LEVELS,
|
|
95
|
+
customScale: () => customScale,
|
|
96
|
+
goldenScale: () => goldenScale,
|
|
97
|
+
stepDown: () => stepDown,
|
|
98
|
+
traditionalScale: () => traditionalScale
|
|
99
|
+
});
|
|
100
|
+
function goldenScale(baseSize) {
|
|
101
|
+
return TYPE_LEVELS.map((level) => {
|
|
102
|
+
const ref = GOLDEN_TABLE[level];
|
|
103
|
+
const min = round(ref.min * baseSize);
|
|
104
|
+
const max = round(ref.max * baseSize);
|
|
105
|
+
const isFluid = min !== max;
|
|
106
|
+
return {
|
|
107
|
+
level,
|
|
108
|
+
label: LEVEL_LABELS[level],
|
|
109
|
+
minRem: min,
|
|
110
|
+
maxRem: max,
|
|
111
|
+
clampValue: isFluid ? generateClamp(min, max) : `${min}rem`,
|
|
112
|
+
isFluid,
|
|
113
|
+
isHeading: level === "display" || level.startsWith("h"),
|
|
114
|
+
lineHeight: DEFAULT_LINE_HEIGHTS[level],
|
|
115
|
+
letterSpacing: DEFAULT_LETTER_SPACINGS[level]
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function stepDown(px) {
|
|
120
|
+
const pxList = TRADITIONAL_SIZES.map((s) => s.px);
|
|
121
|
+
const idx = pxList.indexOf(px);
|
|
122
|
+
if (idx <= 0) return pxList[0];
|
|
123
|
+
return pxList[idx - 1];
|
|
124
|
+
}
|
|
125
|
+
function traditionalScale(assignments, mobileAssignments) {
|
|
126
|
+
return TYPE_LEVELS.map((level) => {
|
|
127
|
+
const maxPx = assignments[level];
|
|
128
|
+
const minPx = mobileAssignments[level];
|
|
129
|
+
const maxRem = round(maxPx / 16);
|
|
130
|
+
const minRem = round(minPx / 16);
|
|
131
|
+
const isFluid = minRem !== maxRem;
|
|
132
|
+
return {
|
|
133
|
+
level,
|
|
134
|
+
label: LEVEL_LABELS[level],
|
|
135
|
+
minRem,
|
|
136
|
+
maxRem,
|
|
137
|
+
clampValue: isFluid ? generateClamp(minRem, maxRem) : `${maxRem}rem`,
|
|
138
|
+
isFluid,
|
|
139
|
+
isHeading: level === "display" || level.startsWith("h"),
|
|
140
|
+
lineHeight: DEFAULT_LINE_HEIGHTS[level],
|
|
141
|
+
letterSpacing: DEFAULT_LETTER_SPACINGS[level]
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function customScale(baseSize, ratio, mobileRatio = ratio, mobileBaseSize = baseSize) {
|
|
146
|
+
return TYPE_LEVELS.map((level) => {
|
|
147
|
+
const step = LEVEL_STEPS[level];
|
|
148
|
+
const max = round(baseSize * Math.pow(ratio, step));
|
|
149
|
+
const min = round(mobileBaseSize * Math.pow(mobileRatio, step));
|
|
150
|
+
const isFluid = min !== max;
|
|
151
|
+
return {
|
|
152
|
+
level,
|
|
153
|
+
label: LEVEL_LABELS[level],
|
|
154
|
+
minRem: min,
|
|
155
|
+
maxRem: max,
|
|
156
|
+
clampValue: isFluid ? generateClamp(min, max) : `${min}rem`,
|
|
157
|
+
isFluid,
|
|
158
|
+
isHeading: level === "display" || level.startsWith("h"),
|
|
159
|
+
lineHeight: DEFAULT_LINE_HEIGHTS[level],
|
|
160
|
+
letterSpacing: DEFAULT_LETTER_SPACINGS[level]
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
var TYPE_LEVELS, LEVEL_LABELS, GOLDEN_TABLE, TRADITIONAL_SIZES, DEFAULT_TRADITIONAL, DEFAULT_TRADITIONAL_MOBILE, RATIO_PRESETS, LEVEL_STEPS;
|
|
165
|
+
var init_scale = __esm({
|
|
166
|
+
"../core/src/scale.ts"() {
|
|
167
|
+
init_clamp();
|
|
168
|
+
init_math();
|
|
169
|
+
init_typography();
|
|
170
|
+
TYPE_LEVELS = [
|
|
171
|
+
"display",
|
|
172
|
+
"h1",
|
|
173
|
+
"h2",
|
|
174
|
+
"h3",
|
|
175
|
+
"h4",
|
|
176
|
+
"h5",
|
|
177
|
+
"h6",
|
|
178
|
+
"body-l",
|
|
179
|
+
"body-m",
|
|
180
|
+
"body-s",
|
|
181
|
+
"caption"
|
|
182
|
+
];
|
|
183
|
+
LEVEL_LABELS = {
|
|
184
|
+
"display": "Display",
|
|
185
|
+
"h1": "H1",
|
|
186
|
+
"h2": "H2",
|
|
187
|
+
"h3": "H3",
|
|
188
|
+
"h4": "H4",
|
|
189
|
+
"h5": "H5",
|
|
190
|
+
"h6": "H6",
|
|
191
|
+
"body-l": "Body L",
|
|
192
|
+
"body-m": "Body M",
|
|
193
|
+
"body-s": "Body S",
|
|
194
|
+
"caption": "Caption"
|
|
195
|
+
};
|
|
196
|
+
GOLDEN_TABLE = {
|
|
197
|
+
"display": { min: 3, max: 4.7 },
|
|
198
|
+
"h1": { min: 2.4, max: 3.7 },
|
|
199
|
+
"h2": { min: 2.2, max: 2.9 },
|
|
200
|
+
"h3": { min: 1.8, max: 2.2 },
|
|
201
|
+
"h4": { min: 1.5, max: 1.7 },
|
|
202
|
+
"h5": { min: 1.2, max: 1.3 },
|
|
203
|
+
"h6": { min: 1, max: 1 },
|
|
204
|
+
"body-l": { min: 1.5, max: 1.7 },
|
|
205
|
+
"body-m": { min: 1.1, max: 1.3 },
|
|
206
|
+
"body-s": { min: 1, max: 1 },
|
|
207
|
+
"caption": { min: 0.79, max: 0.79 }
|
|
208
|
+
};
|
|
209
|
+
TRADITIONAL_SIZES = [
|
|
210
|
+
{ px: 6, name: "Nonpareille" },
|
|
211
|
+
{ px: 7, name: "Mignon" },
|
|
212
|
+
{ px: 8, name: "Petit" },
|
|
213
|
+
{ px: 9, name: "Borgis" },
|
|
214
|
+
{ px: 10, name: "Korpus" },
|
|
215
|
+
{ px: 10.5, name: "Konkordanz" },
|
|
216
|
+
{ px: 11, name: "Rheinl\xE4nder" },
|
|
217
|
+
{ px: 12, name: "Cicero" },
|
|
218
|
+
{ px: 14, name: "Mittel" },
|
|
219
|
+
{ px: 16, name: "Tertia" },
|
|
220
|
+
{ px: 18, name: "Paragon" },
|
|
221
|
+
{ px: 20, name: "Text" },
|
|
222
|
+
{ px: 22, name: "Doppelcicero" },
|
|
223
|
+
{ px: 24, name: "Kleine Kanon" },
|
|
224
|
+
{ px: 26, name: "Gro\xDFe Kanon" },
|
|
225
|
+
{ px: 28, name: "Missal" },
|
|
226
|
+
{ px: 36, name: "Kleine Sabon" },
|
|
227
|
+
{ px: 48, name: "Gro\xDFe Sabon" },
|
|
228
|
+
{ px: 72, name: "Imperial" }
|
|
229
|
+
];
|
|
230
|
+
DEFAULT_TRADITIONAL = {
|
|
231
|
+
"display": 72,
|
|
232
|
+
"h1": 48,
|
|
233
|
+
"h2": 36,
|
|
234
|
+
"h3": 24,
|
|
235
|
+
"h4": 20,
|
|
236
|
+
"h5": 18,
|
|
237
|
+
"h6": 16,
|
|
238
|
+
"body-l": 20,
|
|
239
|
+
"body-m": 18,
|
|
240
|
+
"body-s": 16,
|
|
241
|
+
"caption": 14
|
|
242
|
+
};
|
|
243
|
+
DEFAULT_TRADITIONAL_MOBILE = Object.fromEntries(
|
|
244
|
+
TYPE_LEVELS.map((level) => [level, stepDown(DEFAULT_TRADITIONAL[level])])
|
|
245
|
+
);
|
|
246
|
+
RATIO_PRESETS = [
|
|
247
|
+
{ name: "Minor Second", value: 1.067 },
|
|
248
|
+
{ name: "Major Second", value: 1.125 },
|
|
249
|
+
{ name: "Minor Third", value: 1.2 },
|
|
250
|
+
{ name: "Major Third", value: 1.25 },
|
|
251
|
+
{ name: "Golden Ratio (area)", value: 1.272 },
|
|
252
|
+
{ name: "Perfect Fourth", value: 1.333 },
|
|
253
|
+
{ name: "Augmented Fourth", value: 1.414 },
|
|
254
|
+
{ name: "Perfect Fifth", value: 1.5 },
|
|
255
|
+
{ name: "Golden Ratio", value: 1.618 }
|
|
256
|
+
];
|
|
257
|
+
LEVEL_STEPS = {
|
|
258
|
+
"display": 6,
|
|
259
|
+
"h1": 5,
|
|
260
|
+
"h2": 4,
|
|
261
|
+
"h3": 3,
|
|
262
|
+
"h4": 2,
|
|
263
|
+
"h5": 1,
|
|
264
|
+
"h6": 0,
|
|
265
|
+
"body-l": 2,
|
|
266
|
+
"body-m": 1,
|
|
267
|
+
"body-s": 0,
|
|
268
|
+
"caption": -1
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/index.ts
|
|
274
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
275
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
276
|
+
|
|
277
|
+
// src/tools-generate.ts
|
|
278
|
+
import { z } from "zod";
|
|
279
|
+
|
|
280
|
+
// ../core/src/url-state/color.ts
|
|
281
|
+
function encodeState(s) {
|
|
282
|
+
const brand = s.brandHex.replace("#", "");
|
|
283
|
+
const bg = s.bgColorHex.replace("#", "");
|
|
284
|
+
const err = s.errorColorHex.replace("#", "");
|
|
285
|
+
const chroma = Math.round(s.chromaScale * 100);
|
|
286
|
+
let hash = `${brand},${bg},${s.bgAutoMatch ? 1 : 0},${err},${s.errorAutoMatch ? 1 : 0},${chroma},${s.currentMode},${s.brandPin ? 1 : 0},${s.errorPin ? 1 : 0},${s.fgContrastMode},${encodeURIComponent(s.themeName)},${s.brandInvert ? 1 : 0},${s.errorInvert ? 1 : 0}`;
|
|
287
|
+
s.extraAccents.forEach((a) => {
|
|
288
|
+
hash += `!${encodeURIComponent(a.name)}:${a.hex.replace("#", "")}:${a.pin ? 1 : 0}:${a.autoMatch ? 1 : 0}:${Math.round(a.autoHue)}:${a.invert ? 1 : 0}`;
|
|
289
|
+
});
|
|
290
|
+
return hash;
|
|
291
|
+
}
|
|
292
|
+
function decodeState(hash) {
|
|
293
|
+
if (!hash) return null;
|
|
294
|
+
const segments = hash.split("!");
|
|
295
|
+
const p = segments[0].split(",");
|
|
296
|
+
if (p.length < 7) return null;
|
|
297
|
+
const [brand, bg, bgAuto, err, errAuto, chroma, mode] = p;
|
|
298
|
+
if (!/^[0-9a-fA-F]{6}$/.test(brand)) return null;
|
|
299
|
+
const result = {
|
|
300
|
+
brandHex: "#" + brand.toUpperCase(),
|
|
301
|
+
bgColorHex: /^[0-9a-fA-F]{6}$/.test(bg) ? "#" + bg.toUpperCase() : "#" + brand.toUpperCase(),
|
|
302
|
+
bgAutoMatch: bgAuto === "1",
|
|
303
|
+
errorColorHex: /^[0-9a-fA-F]{6}$/.test(err) ? "#" + err.toUpperCase() : "#CC3333",
|
|
304
|
+
errorAutoMatch: errAuto === "1",
|
|
305
|
+
chromaScale: 0.25,
|
|
306
|
+
currentMode: mode === "balanced" || mode === "exact" ? mode : "balanced",
|
|
307
|
+
brandPin: p[7] === "1",
|
|
308
|
+
brandInvert: false,
|
|
309
|
+
errorPin: p[8] === "1",
|
|
310
|
+
errorInvert: false,
|
|
311
|
+
fgContrastMode: "best",
|
|
312
|
+
themeName: "Standby.Design",
|
|
313
|
+
extraAccents: []
|
|
314
|
+
};
|
|
315
|
+
const chromaVal = parseInt(chroma);
|
|
316
|
+
if (!isNaN(chromaVal) && chromaVal >= 0 && chromaVal <= 100) {
|
|
317
|
+
result.chromaScale = chromaVal / 100;
|
|
318
|
+
}
|
|
319
|
+
if (p.length > 9) {
|
|
320
|
+
const fgMode = p[9];
|
|
321
|
+
if (fgMode === "best" || fgMode === "preferLight" || fgMode === "preferDark") {
|
|
322
|
+
result.fgContrastMode = fgMode;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
result.themeName = p.length > 10 && p[10] ? decodeURIComponent(p[10]) : "Standby.Design";
|
|
326
|
+
result.brandInvert = p.length > 11 && p[11] === "1";
|
|
327
|
+
result.errorInvert = p.length > 12 && p[12] === "1";
|
|
328
|
+
for (let i = 1; i < segments.length; i++) {
|
|
329
|
+
const firstColon = segments[i].indexOf(":");
|
|
330
|
+
if (firstColon === -1) continue;
|
|
331
|
+
const rawName = segments[i].substring(0, firstColon);
|
|
332
|
+
const rest = segments[i].substring(firstColon + 1);
|
|
333
|
+
const restParts = rest.split(":");
|
|
334
|
+
const rawHex = restParts[0];
|
|
335
|
+
const pin = restParts[1] === "1";
|
|
336
|
+
const autoMatch = restParts[2] === "1";
|
|
337
|
+
const autoHue = restParts[3] ? parseInt(restParts[3]) || 0 : 0;
|
|
338
|
+
const accentInvert = restParts[4] === "1";
|
|
339
|
+
const name = decodeURIComponent(rawName) || "Extra " + i;
|
|
340
|
+
if (autoMatch || /^[0-9a-fA-F]{6}$/.test(rawHex)) {
|
|
341
|
+
result.extraAccents.push({ name, hex: "#" + rawHex.toUpperCase(), pin, invert: accentInvert, autoMatch, autoHue });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ../core/src/url-state/type.ts
|
|
348
|
+
init_scale();
|
|
349
|
+
function encodeOverrides(overrides) {
|
|
350
|
+
const entries = Object.entries(overrides).filter(([, v]) => v !== void 0);
|
|
351
|
+
if (entries.length === 0) return "";
|
|
352
|
+
return entries.map(([k, v]) => `${k}:${v}`).join(";");
|
|
353
|
+
}
|
|
354
|
+
function encodeState2(state) {
|
|
355
|
+
const parts = [
|
|
356
|
+
state.scaleMode,
|
|
357
|
+
state.baseSize,
|
|
358
|
+
state.customRatio,
|
|
359
|
+
state.mobileRatio,
|
|
360
|
+
state.headingFont,
|
|
361
|
+
state.bodyFont,
|
|
362
|
+
state.monoFont
|
|
363
|
+
];
|
|
364
|
+
if (state.scaleMode === "traditional" && state.traditionalAssignments) {
|
|
365
|
+
for (const level of TYPE_LEVELS) {
|
|
366
|
+
parts.push(state.traditionalAssignments[level]);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const ext = [];
|
|
370
|
+
if (state.headingWeight !== 500) ext.push(`hw=${state.headingWeight}`);
|
|
371
|
+
if (state.mobileBaseSize !== state.baseSize) ext.push(`mbs=${state.mobileBaseSize}`);
|
|
372
|
+
if (state.mobileRatioMode !== "auto") ext.push(`mrm=${state.mobileRatioMode}`);
|
|
373
|
+
if (state.autoShrink !== 25) ext.push(`as=${state.autoShrink}`);
|
|
374
|
+
const lh = encodeOverrides(state.lineHeightOverrides);
|
|
375
|
+
if (lh) ext.push(`lh=${lh}`);
|
|
376
|
+
const ls = encodeOverrides(state.letterSpacingOverrides);
|
|
377
|
+
if (ls) ext.push(`ls=${ls}`);
|
|
378
|
+
if (state.scaleMode === "traditional" && state.traditionalMobileAssignments) {
|
|
379
|
+
const tmParts = TYPE_LEVELS.map((l) => state.traditionalMobileAssignments[l]);
|
|
380
|
+
ext.push(`tm=${tmParts.join(",")}`);
|
|
381
|
+
}
|
|
382
|
+
const positional = parts.join(",");
|
|
383
|
+
return ext.length > 0 ? `${positional}|${ext.join("&")}` : positional;
|
|
384
|
+
}
|
|
385
|
+
function decodeOverrides(str) {
|
|
386
|
+
const result = {};
|
|
387
|
+
if (!str) return result;
|
|
388
|
+
for (const pair of str.split(";")) {
|
|
389
|
+
const [key, val] = pair.split(":");
|
|
390
|
+
if (key && val && TYPE_LEVELS.includes(key)) {
|
|
391
|
+
const n = parseFloat(val);
|
|
392
|
+
if (!isNaN(n)) result[key] = n;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
function decodeState2(hash) {
|
|
398
|
+
const raw = hash.replace(/^#/, "");
|
|
399
|
+
if (!raw) return null;
|
|
400
|
+
const [positional, extStr] = raw.split("|");
|
|
401
|
+
const parts = positional.split(",");
|
|
402
|
+
if (parts.length < 7) return null;
|
|
403
|
+
const mode = parts[0];
|
|
404
|
+
if (!["traditional", "custom"].includes(mode)) return null;
|
|
405
|
+
const baseSize = parseFloat(parts[1]);
|
|
406
|
+
const customRatio = parseFloat(parts[2]);
|
|
407
|
+
const mobileRatio = parseFloat(parts[3]);
|
|
408
|
+
if (isNaN(baseSize) || isNaN(customRatio) || isNaN(mobileRatio)) return null;
|
|
409
|
+
const state = {
|
|
410
|
+
scaleMode: mode,
|
|
411
|
+
baseSize,
|
|
412
|
+
customRatio,
|
|
413
|
+
mobileRatio,
|
|
414
|
+
headingFont: parts[4],
|
|
415
|
+
bodyFont: parts[5],
|
|
416
|
+
monoFont: parts[6],
|
|
417
|
+
headingWeight: 500,
|
|
418
|
+
mobileBaseSize: baseSize,
|
|
419
|
+
mobileRatioMode: "auto",
|
|
420
|
+
autoShrink: 25,
|
|
421
|
+
lineHeightOverrides: {},
|
|
422
|
+
letterSpacingOverrides: {}
|
|
423
|
+
};
|
|
424
|
+
if (mode === "traditional" && parts.length >= 16) {
|
|
425
|
+
const assignments = {};
|
|
426
|
+
const levelCount = Math.min(TYPE_LEVELS.length, parts.length - 7);
|
|
427
|
+
for (let i = 0; i < levelCount; i++) {
|
|
428
|
+
const val = parseFloat(parts[7 + i]);
|
|
429
|
+
if (isNaN(val)) return null;
|
|
430
|
+
assignments[TYPE_LEVELS[i]] = val;
|
|
431
|
+
}
|
|
432
|
+
for (const level of TYPE_LEVELS) {
|
|
433
|
+
if (!(level in assignments)) {
|
|
434
|
+
assignments[level] = DEFAULT_TRADITIONAL[level];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
state.traditionalAssignments = assignments;
|
|
438
|
+
}
|
|
439
|
+
if (extStr) {
|
|
440
|
+
const params = new URLSearchParams(extStr);
|
|
441
|
+
const hw = params.get("hw");
|
|
442
|
+
if (hw) {
|
|
443
|
+
const n = parseInt(hw);
|
|
444
|
+
if (!isNaN(n)) state.headingWeight = n;
|
|
445
|
+
}
|
|
446
|
+
const mbs = params.get("mbs");
|
|
447
|
+
if (mbs) {
|
|
448
|
+
const n = parseFloat(mbs);
|
|
449
|
+
if (!isNaN(n)) state.mobileBaseSize = n;
|
|
450
|
+
}
|
|
451
|
+
const mrm = params.get("mrm");
|
|
452
|
+
if (mrm === "custom") state.mobileRatioMode = "custom";
|
|
453
|
+
const as = params.get("as");
|
|
454
|
+
if (as) {
|
|
455
|
+
const n = parseFloat(as);
|
|
456
|
+
if (!isNaN(n)) state.autoShrink = n;
|
|
457
|
+
}
|
|
458
|
+
params.get("sbm");
|
|
459
|
+
const lh = params.get("lh");
|
|
460
|
+
if (lh) state.lineHeightOverrides = decodeOverrides(lh);
|
|
461
|
+
const ls = params.get("ls");
|
|
462
|
+
if (ls) state.letterSpacingOverrides = decodeOverrides(ls);
|
|
463
|
+
const tm = params.get("tm");
|
|
464
|
+
if (tm && mode === "traditional") {
|
|
465
|
+
const tmParts = tm.split(",");
|
|
466
|
+
const tmAssign = {};
|
|
467
|
+
for (let i = 0; i < Math.min(TYPE_LEVELS.length, tmParts.length); i++) {
|
|
468
|
+
const val = parseFloat(tmParts[i]);
|
|
469
|
+
if (!isNaN(val)) tmAssign[TYPE_LEVELS[i]] = val;
|
|
470
|
+
}
|
|
471
|
+
state.traditionalMobileAssignments = tmAssign;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return state;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ../core/src/url-state/shape.ts
|
|
478
|
+
function encodeState3(s) {
|
|
479
|
+
return [
|
|
480
|
+
s.shapeStyle,
|
|
481
|
+
s.shadowEnabled ? 1 : 0,
|
|
482
|
+
s.shadowType,
|
|
483
|
+
Math.round(s.shadowStrength * 100),
|
|
484
|
+
Math.round(s.shadowBlurScale * 100),
|
|
485
|
+
Math.round(s.shadowScale * 1e3),
|
|
486
|
+
s.shadowColorMode === "auto" ? "a" : "c",
|
|
487
|
+
s.shadowColorMode === "custom" ? s.shadowCustomColor.replace("#", "") : "",
|
|
488
|
+
s.borderEnabled ? 1 : 0,
|
|
489
|
+
Math.round(s.borderWidth * 10),
|
|
490
|
+
s.borderColorMode === "auto" ? "a" : "c",
|
|
491
|
+
s.borderColorMode === "custom" ? s.borderCustomColor.replace("#", "") : "",
|
|
492
|
+
s.borderRadius,
|
|
493
|
+
Math.round(s.glassDepth * 10),
|
|
494
|
+
Math.round(s.glassBlur * 10),
|
|
495
|
+
Math.round(s.glassDispersion * 10),
|
|
496
|
+
s.ringWidth,
|
|
497
|
+
s.ringOffset,
|
|
498
|
+
s.ringColorMode === "auto" ? "a" : "c",
|
|
499
|
+
s.ringColorMode === "custom" ? s.ringCustomColor.replace("#", "") : "",
|
|
500
|
+
s.separationMode,
|
|
501
|
+
s.shadowOffsetX,
|
|
502
|
+
s.shadowOffsetY,
|
|
503
|
+
s.brutalistVariant === "solid" ? "s" : "o"
|
|
504
|
+
].join(",");
|
|
505
|
+
}
|
|
506
|
+
var SHAPE_STYLES = /* @__PURE__ */ new Set(["paper", "glass", "neomorph", "neobrutalism"]);
|
|
507
|
+
var SHADOW_TYPES = /* @__PURE__ */ new Set(["normal", "flat"]);
|
|
508
|
+
var SEPARATION_MODES = /* @__PURE__ */ new Set(["shadow", "border", "contrast", "gap", "mixed"]);
|
|
509
|
+
function decodeState3(raw) {
|
|
510
|
+
const parts = raw.split(",");
|
|
511
|
+
if (parts.length < 20) return null;
|
|
512
|
+
const isLegacy = parts[0] === "0" || parts[0] === "1";
|
|
513
|
+
if (isLegacy) {
|
|
514
|
+
parts.unshift("paper");
|
|
515
|
+
}
|
|
516
|
+
if (parts.length < 21) return null;
|
|
517
|
+
const result = {};
|
|
518
|
+
if (SHAPE_STYLES.has(parts[0])) result.shapeStyle = parts[0];
|
|
519
|
+
if (parts[1] === "0" || parts[1] === "1") result.shadowEnabled = parts[1] === "1";
|
|
520
|
+
if (parts[2] === "neumorphic") {
|
|
521
|
+
result.shapeStyle = "neomorph";
|
|
522
|
+
result.shadowType = "normal";
|
|
523
|
+
} else if (SHADOW_TYPES.has(parts[2])) {
|
|
524
|
+
result.shadowType = parts[2];
|
|
525
|
+
}
|
|
526
|
+
const strength = parseInt(parts[3]);
|
|
527
|
+
if (!isNaN(strength)) result.shadowStrength = strength / 100;
|
|
528
|
+
const blur = parseInt(parts[4]);
|
|
529
|
+
if (!isNaN(blur)) result.shadowBlurScale = blur / 100;
|
|
530
|
+
const scale = parseInt(parts[5]);
|
|
531
|
+
if (!isNaN(scale)) result.shadowScale = scale / 1e3;
|
|
532
|
+
result.shadowColorMode = parts[6] === "c" ? "custom" : "auto";
|
|
533
|
+
if (parts[7] && /^[0-9a-fA-F]{6}$/.test(parts[7])) result.shadowCustomColor = "#" + parts[7];
|
|
534
|
+
if (parts[8] === "0" || parts[8] === "1") result.borderEnabled = parts[8] === "1";
|
|
535
|
+
const bw = parseInt(parts[9]);
|
|
536
|
+
if (!isNaN(bw)) result.borderWidth = bw / 10;
|
|
537
|
+
result.borderColorMode = parts[10] === "c" ? "custom" : "auto";
|
|
538
|
+
if (parts[11] && /^[0-9a-fA-F]{6}$/.test(parts[11])) result.borderCustomColor = "#" + parts[11];
|
|
539
|
+
const radius = parseInt(parts[12]);
|
|
540
|
+
if (!isNaN(radius)) result.borderRadius = radius;
|
|
541
|
+
const gDepth = parseInt(parts[13]);
|
|
542
|
+
if (!isNaN(gDepth)) result.glassDepth = gDepth / 10;
|
|
543
|
+
const gBlur = parseInt(parts[14]);
|
|
544
|
+
if (!isNaN(gBlur)) result.glassBlur = gBlur / 10;
|
|
545
|
+
const gDisp = parseInt(parts[15]);
|
|
546
|
+
if (!isNaN(gDisp)) result.glassDispersion = gDisp / 10;
|
|
547
|
+
const rw = parseInt(parts[16]);
|
|
548
|
+
if (!isNaN(rw)) result.ringWidth = rw;
|
|
549
|
+
const ro = parseInt(parts[17]);
|
|
550
|
+
if (!isNaN(ro)) result.ringOffset = ro;
|
|
551
|
+
result.ringColorMode = parts[18] === "c" ? "custom" : "auto";
|
|
552
|
+
if (parts[19] && /^[0-9a-fA-F]{6}$/.test(parts[19])) result.ringCustomColor = "#" + parts[19];
|
|
553
|
+
if (SEPARATION_MODES.has(parts[20])) result.separationMode = parts[20];
|
|
554
|
+
const oX = parseInt(parts[21]);
|
|
555
|
+
if (!isNaN(oX)) result.shadowOffsetX = oX;
|
|
556
|
+
const oY = parseInt(parts[22]);
|
|
557
|
+
if (!isNaN(oY)) result.shadowOffsetY = oY;
|
|
558
|
+
if (parts[23] === "s" || parts[23] === "o") {
|
|
559
|
+
result.brutalistVariant = parts[23] === "s" ? "solid" : "outlined";
|
|
560
|
+
}
|
|
561
|
+
return result;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ../core/src/url-state/symbol.ts
|
|
565
|
+
var STYLE_MAP = { a: "auto", o: "outlined", f: "filled", d: "duotone" };
|
|
566
|
+
var STYLE_REV = { auto: "a", outlined: "o", filled: "f", duotone: "d" };
|
|
567
|
+
var WEIGHT_MAP = { a: "auto", t: "thin", r: "regular", b: "bold" };
|
|
568
|
+
var WEIGHT_REV = { auto: "a", thin: "t", regular: "r", bold: "b" };
|
|
569
|
+
var CORNERS_MAP = { a: "auto", s: "sharp", n: "rounded" };
|
|
570
|
+
var CORNERS_REV = { auto: "a", sharp: "s", rounded: "n" };
|
|
571
|
+
function encodeState4(state) {
|
|
572
|
+
return [
|
|
573
|
+
STYLE_REV[state.preferredStyle],
|
|
574
|
+
WEIGHT_REV[state.preferredWeight],
|
|
575
|
+
CORNERS_REV[state.preferredCorners],
|
|
576
|
+
Math.round(state.iconBaseSize * 100),
|
|
577
|
+
Math.round(state.iconScale * 1e3),
|
|
578
|
+
state.snapTo4px ? "1" : "0",
|
|
579
|
+
state.selectedSet || ""
|
|
580
|
+
].join(",");
|
|
581
|
+
}
|
|
582
|
+
function decodeState4(raw) {
|
|
583
|
+
const parts = raw.split(",");
|
|
584
|
+
if (parts.length < 6) return null;
|
|
585
|
+
const style = STYLE_MAP[parts[0]];
|
|
586
|
+
const weight = WEIGHT_MAP[parts[1]];
|
|
587
|
+
const corners = CORNERS_MAP[parts[2]];
|
|
588
|
+
const baseSize = parseInt(parts[3], 10) / 100;
|
|
589
|
+
const scale = parseInt(parts[4], 10) / 1e3;
|
|
590
|
+
const snap = parts[5] !== "0";
|
|
591
|
+
const setId = parts[6] || null;
|
|
592
|
+
if (!style || !weight || !corners || isNaN(baseSize) || isNaN(scale)) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
preferredStyle: style,
|
|
597
|
+
preferredWeight: weight,
|
|
598
|
+
preferredCorners: corners,
|
|
599
|
+
iconBaseSize: Math.max(0.5, Math.min(3, baseSize)),
|
|
600
|
+
iconScale: Math.max(1, Math.min(2, scale)),
|
|
601
|
+
snapTo4px: snap,
|
|
602
|
+
selectedSet: setId
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ../core/src/layout.ts
|
|
607
|
+
var DEFAULT_BREAKPOINTS = [
|
|
608
|
+
{ name: "sm", minPx: 640 },
|
|
609
|
+
{ name: "md", minPx: 768 },
|
|
610
|
+
{ name: "lg", minPx: 1024 },
|
|
611
|
+
{ name: "xl", minPx: 1280 },
|
|
612
|
+
{ name: "2xl", minPx: 1536 }
|
|
613
|
+
];
|
|
614
|
+
var DEFAULT_CONTAINERS = [
|
|
615
|
+
{ name: "prose", maxPx: 680 },
|
|
616
|
+
{ name: "narrow", maxPx: 960 },
|
|
617
|
+
{ name: "default", maxPx: 1200 },
|
|
618
|
+
{ name: "wide", maxPx: 1440 },
|
|
619
|
+
{ name: "full", maxPx: 1920 }
|
|
620
|
+
];
|
|
621
|
+
var DEFAULT_FLUID_MIN_VW = 375;
|
|
622
|
+
var DEFAULT_FLUID_MAX_VW = 1920;
|
|
623
|
+
var DEFAULT_PROSE_MAX_CH = 65;
|
|
624
|
+
function sortBreakpoints(bps) {
|
|
625
|
+
return [...bps].sort((a, b) => a.minPx - b.minPx);
|
|
626
|
+
}
|
|
627
|
+
function sortContainers(cts) {
|
|
628
|
+
return [...cts].sort((a, b) => a.maxPx - b.maxPx);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// ../core/src/aspect.ts
|
|
632
|
+
var DEFAULT_ASPECT_RATIOS = [
|
|
633
|
+
{ name: "square", w: 1, h: 1 },
|
|
634
|
+
{ name: "landscape", w: 4, h: 3 },
|
|
635
|
+
{ name: "photo", w: 3, h: 2 },
|
|
636
|
+
{ name: "video", w: 16, h: 9 },
|
|
637
|
+
{ name: "ultrawide", w: 21, h: 9 },
|
|
638
|
+
{ name: "golden", w: 1.618, h: 1 },
|
|
639
|
+
{ name: "silver", w: 1.414, h: 1 },
|
|
640
|
+
{ name: "cinema", w: 2.39, h: 1 }
|
|
641
|
+
];
|
|
642
|
+
function formatAspect(a) {
|
|
643
|
+
const w = Number.isInteger(a.w) ? a.w.toString() : a.w.toFixed(3).replace(/\.?0+$/, "");
|
|
644
|
+
const h = Number.isInteger(a.h) ? a.h.toString() : a.h.toFixed(3).replace(/\.?0+$/, "");
|
|
645
|
+
return `${w} / ${h}`;
|
|
646
|
+
}
|
|
647
|
+
function aspectValue(a) {
|
|
648
|
+
return a.h === 0 ? 0 : a.w / a.h;
|
|
649
|
+
}
|
|
650
|
+
function reciprocal(a) {
|
|
651
|
+
return { name: `${a.name}-portrait`, w: a.h, h: a.w };
|
|
652
|
+
}
|
|
653
|
+
function expandAndSortAspects(ratios, includeReciprocals) {
|
|
654
|
+
const all = [];
|
|
655
|
+
for (const a of ratios) {
|
|
656
|
+
all.push(a);
|
|
657
|
+
if (includeReciprocals && a.w !== a.h) all.push(reciprocal(a));
|
|
658
|
+
}
|
|
659
|
+
return all.sort((a, b) => aspectValue(b) - aspectValue(a));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ../core/src/url-state/space.ts
|
|
663
|
+
var DEFAULT_SPACE_URL_STATE = {
|
|
664
|
+
spacingMode: "harmonic",
|
|
665
|
+
spacingBaseRem: 1,
|
|
666
|
+
spacingRatio: 1.272,
|
|
667
|
+
spacingMultiplier: 1,
|
|
668
|
+
spacingSnap: true,
|
|
669
|
+
breakpoints: DEFAULT_BREAKPOINTS,
|
|
670
|
+
fluidMinVw: DEFAULT_FLUID_MIN_VW,
|
|
671
|
+
fluidMaxVw: DEFAULT_FLUID_MAX_VW,
|
|
672
|
+
containers: DEFAULT_CONTAINERS,
|
|
673
|
+
proseMaxCh: DEFAULT_PROSE_MAX_CH,
|
|
674
|
+
aspectRatios: DEFAULT_ASPECT_RATIOS,
|
|
675
|
+
aspectIncludeReciprocals: true
|
|
676
|
+
};
|
|
677
|
+
function breakpointsEqualDefault(bps) {
|
|
678
|
+
if (bps.length !== DEFAULT_BREAKPOINTS.length) return false;
|
|
679
|
+
return bps.every((b, i) => b.name === DEFAULT_BREAKPOINTS[i].name && b.minPx === DEFAULT_BREAKPOINTS[i].minPx);
|
|
680
|
+
}
|
|
681
|
+
function containersEqualDefault(cts) {
|
|
682
|
+
if (cts.length !== DEFAULT_CONTAINERS.length) return false;
|
|
683
|
+
return cts.every((c, i) => c.name === DEFAULT_CONTAINERS[i].name && c.maxPx === DEFAULT_CONTAINERS[i].maxPx);
|
|
684
|
+
}
|
|
685
|
+
function aspectsEqualDefault(ars) {
|
|
686
|
+
if (ars.length !== DEFAULT_ASPECT_RATIOS.length) return false;
|
|
687
|
+
return ars.every((a, i) => a.name === DEFAULT_ASPECT_RATIOS[i].name && a.w === DEFAULT_ASPECT_RATIOS[i].w && a.h === DEFAULT_ASPECT_RATIOS[i].h);
|
|
688
|
+
}
|
|
689
|
+
function encodeAspect(a) {
|
|
690
|
+
const wInt = Number.isInteger(a.w);
|
|
691
|
+
const hInt = Number.isInteger(a.h);
|
|
692
|
+
if (wInt && hInt) return `${a.name}:${a.w}:${a.h}`;
|
|
693
|
+
return `${a.name}:${Math.round(a.w * 1e3)}:${Math.round(a.h * 1e3)}`;
|
|
694
|
+
}
|
|
695
|
+
function decodeAspectField(s, h) {
|
|
696
|
+
const n = parseFloat(s);
|
|
697
|
+
if (isNaN(n)) return 0;
|
|
698
|
+
return n >= 100 ? n / 1e3 : n;
|
|
699
|
+
}
|
|
700
|
+
function encodeState5(s) {
|
|
701
|
+
const head = [
|
|
702
|
+
s.spacingMode,
|
|
703
|
+
Math.round(s.spacingBaseRem * 1e3),
|
|
704
|
+
Math.round(s.spacingRatio * 1e3),
|
|
705
|
+
Math.round(s.spacingMultiplier * 100),
|
|
706
|
+
s.spacingSnap ? 1 : 0
|
|
707
|
+
].join(",");
|
|
708
|
+
const ext = [];
|
|
709
|
+
if (!breakpointsEqualDefault(s.breakpoints)) {
|
|
710
|
+
const bpStr = s.breakpoints.map((b) => `${b.name}:${b.minPx}`).join(";");
|
|
711
|
+
ext.push(`bp=${bpStr}`);
|
|
712
|
+
}
|
|
713
|
+
if (s.fluidMinVw !== DEFAULT_FLUID_MIN_VW || s.fluidMaxVw !== DEFAULT_FLUID_MAX_VW) {
|
|
714
|
+
ext.push(`fvw=${s.fluidMinVw},${s.fluidMaxVw}`);
|
|
715
|
+
}
|
|
716
|
+
if (!containersEqualDefault(s.containers)) {
|
|
717
|
+
const ctStr = s.containers.map((c) => `${c.name}:${c.maxPx}`).join(";");
|
|
718
|
+
ext.push(`ct=${ctStr}`);
|
|
719
|
+
}
|
|
720
|
+
if (s.proseMaxCh !== DEFAULT_PROSE_MAX_CH) {
|
|
721
|
+
ext.push(`pch=${s.proseMaxCh}`);
|
|
722
|
+
}
|
|
723
|
+
if (!aspectsEqualDefault(s.aspectRatios)) {
|
|
724
|
+
const arStr = s.aspectRatios.map(encodeAspect).join(";");
|
|
725
|
+
ext.push(`ar=${arStr}`);
|
|
726
|
+
}
|
|
727
|
+
if (!s.aspectIncludeReciprocals) {
|
|
728
|
+
ext.push(`arr=0`);
|
|
729
|
+
}
|
|
730
|
+
return ext.length > 0 ? `${head}|${ext.join("&")}` : head;
|
|
731
|
+
}
|
|
732
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["harmonic", "geometric"]);
|
|
733
|
+
function decodeState5(raw) {
|
|
734
|
+
if (!raw) return null;
|
|
735
|
+
const [head, extStr] = raw.split("|");
|
|
736
|
+
const parts = head.split(",");
|
|
737
|
+
if (parts.length < 5) return null;
|
|
738
|
+
const result = {};
|
|
739
|
+
if (VALID_MODES.has(parts[0])) {
|
|
740
|
+
result.spacingMode = parts[0];
|
|
741
|
+
}
|
|
742
|
+
const baseRem = parseInt(parts[1]);
|
|
743
|
+
if (!isNaN(baseRem)) result.spacingBaseRem = baseRem / 1e3;
|
|
744
|
+
const ratio = parseInt(parts[2]);
|
|
745
|
+
if (!isNaN(ratio)) result.spacingRatio = ratio / 1e3;
|
|
746
|
+
const mult = parseInt(parts[3]);
|
|
747
|
+
if (!isNaN(mult)) result.spacingMultiplier = mult / 100;
|
|
748
|
+
if (parts[4] === "0" || parts[4] === "1") result.spacingSnap = parts[4] === "1";
|
|
749
|
+
if (!extStr) return result;
|
|
750
|
+
const params = new URLSearchParams(extStr);
|
|
751
|
+
const bp = params.get("bp");
|
|
752
|
+
if (bp) {
|
|
753
|
+
const bps = [];
|
|
754
|
+
for (const entry of bp.split(";")) {
|
|
755
|
+
const [name, px] = entry.split(":");
|
|
756
|
+
const n = parseInt(px);
|
|
757
|
+
if (name && !isNaN(n)) bps.push({ name, minPx: n });
|
|
758
|
+
}
|
|
759
|
+
if (bps.length > 0) result.breakpoints = bps;
|
|
760
|
+
}
|
|
761
|
+
const fvw = params.get("fvw");
|
|
762
|
+
if (fvw) {
|
|
763
|
+
const [minS, maxS] = fvw.split(",");
|
|
764
|
+
const min = parseInt(minS);
|
|
765
|
+
const max = parseInt(maxS);
|
|
766
|
+
if (!isNaN(min)) result.fluidMinVw = min;
|
|
767
|
+
if (!isNaN(max)) result.fluidMaxVw = max;
|
|
768
|
+
}
|
|
769
|
+
const ct = params.get("ct");
|
|
770
|
+
if (ct) {
|
|
771
|
+
const cts = [];
|
|
772
|
+
for (const entry of ct.split(";")) {
|
|
773
|
+
const [name, px] = entry.split(":");
|
|
774
|
+
const n = parseInt(px);
|
|
775
|
+
if (name && !isNaN(n)) cts.push({ name, maxPx: n });
|
|
776
|
+
}
|
|
777
|
+
if (cts.length > 0) result.containers = cts;
|
|
778
|
+
}
|
|
779
|
+
const pch = params.get("pch");
|
|
780
|
+
if (pch) {
|
|
781
|
+
const n = parseInt(pch);
|
|
782
|
+
if (!isNaN(n)) result.proseMaxCh = n;
|
|
783
|
+
}
|
|
784
|
+
const ar = params.get("ar");
|
|
785
|
+
if (ar) {
|
|
786
|
+
const ars = [];
|
|
787
|
+
for (const entry of ar.split(";")) {
|
|
788
|
+
const [name, wS, hS] = entry.split(":");
|
|
789
|
+
if (!name || wS === void 0 || hS === void 0) continue;
|
|
790
|
+
const w = decodeAspectField(wS, hS);
|
|
791
|
+
const h = decodeAspectField(hS, wS);
|
|
792
|
+
if (w > 0 && h > 0) ars.push({ name, w, h });
|
|
793
|
+
}
|
|
794
|
+
if (ars.length > 0) result.aspectRatios = ars;
|
|
795
|
+
}
|
|
796
|
+
const arr = params.get("arr");
|
|
797
|
+
if (arr === "0") result.aspectIncludeReciprocals = false;
|
|
798
|
+
else if (arr === "1") result.aspectIncludeReciprocals = true;
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ../core/src/color-math.ts
|
|
803
|
+
function srgbToLinear(c) {
|
|
804
|
+
c /= 255;
|
|
805
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
806
|
+
}
|
|
807
|
+
function linearToSrgb(c) {
|
|
808
|
+
c = Math.max(0, Math.min(1, c));
|
|
809
|
+
return c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
810
|
+
}
|
|
811
|
+
function linearSrgbToOklab(r, g, b) {
|
|
812
|
+
let l_ = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
813
|
+
let m_ = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
814
|
+
let s_ = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
815
|
+
l_ = Math.cbrt(l_);
|
|
816
|
+
m_ = Math.cbrt(m_);
|
|
817
|
+
s_ = Math.cbrt(s_);
|
|
818
|
+
return [
|
|
819
|
+
0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
820
|
+
1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
821
|
+
0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_
|
|
822
|
+
];
|
|
823
|
+
}
|
|
824
|
+
function oklabToLinearSrgb(L, a, b) {
|
|
825
|
+
let l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
826
|
+
let m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
827
|
+
let s_ = L - 0.0894841775 * a - 1.291485548 * b;
|
|
828
|
+
l_ = l_ * l_ * l_;
|
|
829
|
+
m_ = m_ * m_ * m_;
|
|
830
|
+
s_ = s_ * s_ * s_;
|
|
831
|
+
return [
|
|
832
|
+
4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
|
|
833
|
+
-1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
|
|
834
|
+
-0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_
|
|
835
|
+
];
|
|
836
|
+
}
|
|
837
|
+
function oklabToOklch(L, a, b) {
|
|
838
|
+
return [L, Math.sqrt(a * a + b * b), (Math.atan2(b, a) * 180 / Math.PI % 360 + 360) % 360];
|
|
839
|
+
}
|
|
840
|
+
function oklchToOklab(L, C, H) {
|
|
841
|
+
const rad = H * Math.PI / 180;
|
|
842
|
+
return [L, C * Math.cos(rad), C * Math.sin(rad)];
|
|
843
|
+
}
|
|
844
|
+
function hexToRgb(hex) {
|
|
845
|
+
hex = hex.replace("#", "");
|
|
846
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
847
|
+
}
|
|
848
|
+
function rgbToHex(r, g, b) {
|
|
849
|
+
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
850
|
+
return "#" + [r, g, b].map((c) => clamp(c).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
851
|
+
}
|
|
852
|
+
function hexToOklch(hex) {
|
|
853
|
+
const [r, g, b] = hexToRgb(hex);
|
|
854
|
+
const [lr, lg, lb] = [srgbToLinear(r), srgbToLinear(g), srgbToLinear(b)];
|
|
855
|
+
const [L, a, ob] = linearSrgbToOklab(lr, lg, lb);
|
|
856
|
+
return oklabToOklch(L, a, ob);
|
|
857
|
+
}
|
|
858
|
+
function oklchToHex(L, C, H) {
|
|
859
|
+
const [oL, oa, ob] = oklchToOklab(L, C, H);
|
|
860
|
+
const [r, g, b] = oklabToLinearSrgb(oL, oa, ob);
|
|
861
|
+
return rgbToHex(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b));
|
|
862
|
+
}
|
|
863
|
+
function isInGamut(L, C, H) {
|
|
864
|
+
const [oL, oa, ob] = oklchToOklab(L, C, H);
|
|
865
|
+
const [r, g, b] = oklabToLinearSrgb(oL, oa, ob);
|
|
866
|
+
return r >= -1e-3 && r <= 1.001 && g >= -1e-3 && g <= 1.001 && b >= -1e-3 && b <= 1.001;
|
|
867
|
+
}
|
|
868
|
+
function maxChromaInGamut(L, H) {
|
|
869
|
+
let lo = 0, hi = 0.4;
|
|
870
|
+
for (let i = 0; i < 20; i++) {
|
|
871
|
+
const mid = (lo + hi) / 2;
|
|
872
|
+
if (isInGamut(L, mid, H)) lo = mid;
|
|
873
|
+
else hi = mid;
|
|
874
|
+
}
|
|
875
|
+
return lo;
|
|
876
|
+
}
|
|
877
|
+
function invertHex(hex) {
|
|
878
|
+
const [L, C, H] = hexToOklch(hex);
|
|
879
|
+
const invL = 1 - L;
|
|
880
|
+
const maxC = maxChromaInGamut(invL, H);
|
|
881
|
+
return oklchToHex(invL, Math.min(C, maxC * 0.95), H);
|
|
882
|
+
}
|
|
883
|
+
function relativeLuminance(hex) {
|
|
884
|
+
const [r, g, b] = hexToRgb(hex);
|
|
885
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
886
|
+
}
|
|
887
|
+
function contrastRatio(hex1, hex2) {
|
|
888
|
+
const l1 = relativeLuminance(hex1);
|
|
889
|
+
const l2 = relativeLuminance(hex2);
|
|
890
|
+
const lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
|
|
891
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ../core/src/palette.ts
|
|
895
|
+
var STEPS = [25, 50, 75, 100, 200, 300, 400, 500, 600, 700, 800, 825, 850, 875, 900, 925, 950, 975];
|
|
896
|
+
var L_WHITE = 0.98;
|
|
897
|
+
var L_BLACK = 0.1;
|
|
898
|
+
var ERROR_HUE = 25;
|
|
899
|
+
var SUCCESS_HUE = 145;
|
|
900
|
+
var WARNING_HUE = 85;
|
|
901
|
+
var INFO_HUE = 255;
|
|
902
|
+
function surfaceChromaCorrection(L) {
|
|
903
|
+
if (L >= 0.45) return 1;
|
|
904
|
+
return Math.pow(L / 0.45, 1.2);
|
|
905
|
+
}
|
|
906
|
+
function computeAutoAccentHex(primaryHex, hue) {
|
|
907
|
+
const [pL, pC, pH] = hexToOklch(primaryHex);
|
|
908
|
+
const primaryMaxC = maxChromaInGamut(pL, pH);
|
|
909
|
+
const saturation = primaryMaxC > 1e-3 ? pC / primaryMaxC : 0.5;
|
|
910
|
+
const accentMaxC = maxChromaInGamut(pL, hue);
|
|
911
|
+
return oklchToHex(pL, accentMaxC * saturation * 0.95, hue);
|
|
912
|
+
}
|
|
913
|
+
function computeAutoErrorHex(primaryHex) {
|
|
914
|
+
return computeAutoAccentHex(primaryHex, ERROR_HUE);
|
|
915
|
+
}
|
|
916
|
+
function generatePalette(hex, chromaScale = 1, mode = "balanced") {
|
|
917
|
+
const [bL, bC, bH] = hexToOklch(hex);
|
|
918
|
+
const inputMaxC = maxChromaInGamut(bL, bH);
|
|
919
|
+
const saturation = inputMaxC > 1e-3 ? bC / inputMaxC : 0;
|
|
920
|
+
const blend = Math.min(1, saturation / 0.3);
|
|
921
|
+
const safeH = bC > 5e-3 ? bH : 0;
|
|
922
|
+
const L_MID = mode === "exact" ? bL : 0.5;
|
|
923
|
+
function stepToL(step) {
|
|
924
|
+
if (step <= 500) return L_WHITE + step / 500 * (L_MID - L_WHITE);
|
|
925
|
+
return L_MID + (step - 500) / 500 * (L_BLACK - L_MID);
|
|
926
|
+
}
|
|
927
|
+
if (mode === "exact") {
|
|
928
|
+
return STEPS.map((step) => {
|
|
929
|
+
const L = stepToL(step);
|
|
930
|
+
const H = safeH;
|
|
931
|
+
let C;
|
|
932
|
+
if (step === 500) {
|
|
933
|
+
C = bC * chromaScale;
|
|
934
|
+
} else {
|
|
935
|
+
const stepNorm = step <= 500 ? step / 500 : (1e3 - step) / 500;
|
|
936
|
+
const envelope = 1 - Math.pow(1 - stepNorm, 2);
|
|
937
|
+
const correction = chromaScale < 1 ? surfaceChromaCorrection(L) : 1;
|
|
938
|
+
const desired = bC * chromaScale * envelope * correction;
|
|
939
|
+
const gamutMax = bC > 5e-3 ? maxChromaInGamut(L, safeH) : 0;
|
|
940
|
+
C = Math.min(desired, gamutMax * 0.95);
|
|
941
|
+
}
|
|
942
|
+
const hx = oklchToHex(L, C, H);
|
|
943
|
+
const css = `oklch(${L.toFixed(4)} ${C.toFixed(4)} ${H.toFixed(2)})`;
|
|
944
|
+
return { step, L, C, H, hex: hx, css };
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
const maxC500 = maxChromaInGamut(L_MID, safeH);
|
|
948
|
+
const targetC500 = maxC500 * 0.88 * blend;
|
|
949
|
+
return STEPS.map((step) => {
|
|
950
|
+
const L = stepToL(step);
|
|
951
|
+
const envelope = 1 - Math.pow(2 * L - 1, 2);
|
|
952
|
+
const correction = chromaScale < 1 ? surfaceChromaCorrection(L) : 1;
|
|
953
|
+
const desired = targetC500 * chromaScale * Math.max(0, envelope) * correction;
|
|
954
|
+
const gamutMax = bC > 5e-3 ? maxChromaInGamut(L, safeH) : 0;
|
|
955
|
+
const C = Math.min(desired, gamutMax * 0.95);
|
|
956
|
+
const H = safeH;
|
|
957
|
+
const hx = oklchToHex(L, C, H);
|
|
958
|
+
const css = `oklch(${L.toFixed(4)} ${C.toFixed(4)} ${H.toFixed(2)})`;
|
|
959
|
+
return { step, L, C, H, hex: hx, css };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/tools-generate.ts
|
|
964
|
+
init_scale();
|
|
965
|
+
|
|
966
|
+
// ../core/src/icon-sets.ts
|
|
967
|
+
var ICON_FAMILIES = [
|
|
968
|
+
{
|
|
969
|
+
id: "material",
|
|
970
|
+
name: "Material Symbols",
|
|
971
|
+
iconifyPrefix: "material-symbols",
|
|
972
|
+
cornerStyle: "sharp",
|
|
973
|
+
mood: 25,
|
|
974
|
+
description: "Geometric, authoritative, and systematic.",
|
|
975
|
+
url: "https://fonts.google.com/icons",
|
|
976
|
+
npmPackage: "@iconify-json/material-symbols",
|
|
977
|
+
license: "Apache 2.0",
|
|
978
|
+
defaultVariant: "material-outlined",
|
|
979
|
+
variants: [
|
|
980
|
+
{ id: "material-outlined", style: "outlined", strokeWeight: "regular", label: "Outlined" },
|
|
981
|
+
{ id: "material-filled", style: "filled", strokeWeight: "regular", label: "Filled" }
|
|
982
|
+
]
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "lucide",
|
|
986
|
+
name: "Lucide",
|
|
987
|
+
iconifyPrefix: "lucide",
|
|
988
|
+
cornerStyle: "rounded",
|
|
989
|
+
mood: 60,
|
|
990
|
+
description: "Friendly, consistent, and approachable.",
|
|
991
|
+
url: "https://lucide.dev",
|
|
992
|
+
npmPackage: "lucide-react",
|
|
993
|
+
license: "ISC",
|
|
994
|
+
defaultVariant: "lucide",
|
|
995
|
+
variants: [
|
|
996
|
+
{ id: "lucide", style: "outlined", strokeWeight: "regular", label: "Outlined" }
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
id: "phosphor",
|
|
1001
|
+
name: "Phosphor",
|
|
1002
|
+
iconifyPrefix: "ph",
|
|
1003
|
+
cornerStyle: "rounded",
|
|
1004
|
+
mood: 55,
|
|
1005
|
+
description: "Warm, versatile, and expressive.",
|
|
1006
|
+
url: "https://phosphoricons.com",
|
|
1007
|
+
npmPackage: "@phosphor-icons/react",
|
|
1008
|
+
license: "MIT",
|
|
1009
|
+
defaultVariant: "phosphor-regular",
|
|
1010
|
+
variants: [
|
|
1011
|
+
{ id: "phosphor-regular", style: "outlined", strokeWeight: "regular", label: "Regular" },
|
|
1012
|
+
{ id: "phosphor-bold", style: "outlined", strokeWeight: "bold", label: "Bold" },
|
|
1013
|
+
{ id: "phosphor-thin", style: "outlined", strokeWeight: "thin", label: "Thin" },
|
|
1014
|
+
{ id: "phosphor-fill", style: "filled", strokeWeight: "regular", label: "Fill" }
|
|
1015
|
+
]
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
id: "solar",
|
|
1019
|
+
name: "Solar",
|
|
1020
|
+
iconifyPrefix: "solar",
|
|
1021
|
+
cornerStyle: "rounded",
|
|
1022
|
+
mood: 75,
|
|
1023
|
+
description: "Expressive, modern, with a distinctive broken-line variant.",
|
|
1024
|
+
url: "https://solar-icons.com",
|
|
1025
|
+
npmPackage: "@iconify-json/solar",
|
|
1026
|
+
license: "CC BY 4.0",
|
|
1027
|
+
defaultVariant: "solar-linear",
|
|
1028
|
+
variants: [
|
|
1029
|
+
{ id: "solar-linear", style: "outlined", strokeWeight: "thin", label: "Linear" },
|
|
1030
|
+
{ id: "solar-bold", style: "outlined", strokeWeight: "bold", label: "Bold" },
|
|
1031
|
+
{ id: "solar-broken", style: "outlined", strokeWeight: "thin", label: "Broken" }
|
|
1032
|
+
]
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: "tabler",
|
|
1036
|
+
name: "Tabler Icons",
|
|
1037
|
+
iconifyPrefix: "tabler",
|
|
1038
|
+
cornerStyle: "rounded",
|
|
1039
|
+
mood: 50,
|
|
1040
|
+
description: "Clean, systematic, with outline and filled variants on a 24px grid.",
|
|
1041
|
+
url: "https://tabler.io/icons",
|
|
1042
|
+
npmPackage: "@tabler/icons-react",
|
|
1043
|
+
license: "MIT",
|
|
1044
|
+
defaultVariant: "tabler-outline",
|
|
1045
|
+
variants: [
|
|
1046
|
+
{ id: "tabler-outline", style: "outlined", strokeWeight: "regular", label: "Outline" },
|
|
1047
|
+
{ id: "tabler-filled", style: "filled", strokeWeight: "regular", label: "Filled" }
|
|
1048
|
+
]
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
id: "radix",
|
|
1052
|
+
name: "Radix Icons",
|
|
1053
|
+
iconifyPrefix: "radix-icons",
|
|
1054
|
+
cornerStyle: "sharp",
|
|
1055
|
+
mood: 40,
|
|
1056
|
+
description: "Precise, minimal, designed on a 15px grid.",
|
|
1057
|
+
url: "https://www.radix-ui.com/icons",
|
|
1058
|
+
npmPackage: "@radix-ui/react-icons",
|
|
1059
|
+
license: "MIT",
|
|
1060
|
+
defaultVariant: "radix",
|
|
1061
|
+
variants: [
|
|
1062
|
+
{ id: "radix", style: "outlined", strokeWeight: "thin", label: "Regular" }
|
|
1063
|
+
]
|
|
1064
|
+
}
|
|
1065
|
+
];
|
|
1066
|
+
var ICON_SETS = ICON_FAMILIES.flatMap(
|
|
1067
|
+
(f) => f.variants.map((v) => ({
|
|
1068
|
+
id: v.id,
|
|
1069
|
+
name: f.variants.length > 1 ? `${f.name} ${v.label}` : f.name,
|
|
1070
|
+
familyId: f.id,
|
|
1071
|
+
iconifyPrefix: f.iconifyPrefix,
|
|
1072
|
+
style: v.style,
|
|
1073
|
+
strokeWeight: v.strokeWeight,
|
|
1074
|
+
cornerStyle: f.cornerStyle,
|
|
1075
|
+
mood: f.mood,
|
|
1076
|
+
description: f.description,
|
|
1077
|
+
url: f.url,
|
|
1078
|
+
npmPackage: f.npmPackage,
|
|
1079
|
+
license: f.license
|
|
1080
|
+
}))
|
|
1081
|
+
);
|
|
1082
|
+
function getSetById(id) {
|
|
1083
|
+
return ICON_SETS.find((s) => s.id === id);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ../core/src/unified-hash.ts
|
|
1087
|
+
function isUnifiedHash(raw) {
|
|
1088
|
+
const str = raw.replace(/^#/, "");
|
|
1089
|
+
return /(?:^|&)[ctsyp]=/.test(str);
|
|
1090
|
+
}
|
|
1091
|
+
function parseUnifiedHash(raw) {
|
|
1092
|
+
const str = raw.replace(/^#/, "");
|
|
1093
|
+
const result = { c: null, t: null, s: null, y: null, p: null };
|
|
1094
|
+
if (!isUnifiedHash(str)) return result;
|
|
1095
|
+
for (const part of str.split("&")) {
|
|
1096
|
+
const eq = part.indexOf("=");
|
|
1097
|
+
if (eq === -1) continue;
|
|
1098
|
+
const key = part.slice(0, eq);
|
|
1099
|
+
const value = part.slice(eq + 1);
|
|
1100
|
+
if (key === "c" || key === "t" || key === "s" || key === "y" || key === "p") {
|
|
1101
|
+
result[key] = value || null;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
function buildUnifiedHash(segments) {
|
|
1107
|
+
const parts = [];
|
|
1108
|
+
if (segments.c) parts.push("c=" + segments.c);
|
|
1109
|
+
if (segments.t) parts.push("t=" + segments.t);
|
|
1110
|
+
if (segments.s) parts.push("s=" + segments.s);
|
|
1111
|
+
if (segments.y) parts.push("y=" + segments.y);
|
|
1112
|
+
if (segments.p) parts.push("p=" + segments.p);
|
|
1113
|
+
return parts.join("&");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/lib.ts
|
|
1117
|
+
init_scale();
|
|
1118
|
+
init_typography();
|
|
1119
|
+
|
|
1120
|
+
// ../core/src/spacing.ts
|
|
1121
|
+
var SPACING_STEPS = [
|
|
1122
|
+
{ name: "3xs", multiple: 0.25 },
|
|
1123
|
+
{ name: "2xs", multiple: 0.5 },
|
|
1124
|
+
{ name: "xs", multiple: 0.75 },
|
|
1125
|
+
{ name: "sm", multiple: 1 },
|
|
1126
|
+
{ name: "md", multiple: 1.5 },
|
|
1127
|
+
{ name: "lg", multiple: 2 },
|
|
1128
|
+
{ name: "xl", multiple: 3 },
|
|
1129
|
+
{ name: "2xl", multiple: 4 },
|
|
1130
|
+
{ name: "3xl", multiple: 6 }
|
|
1131
|
+
];
|
|
1132
|
+
var SM_INDEX = 3;
|
|
1133
|
+
function snapRem(raw) {
|
|
1134
|
+
if (raw < 1) return Math.round(raw * 4) / 4;
|
|
1135
|
+
if (raw < 4) return Math.round(raw * 2) / 2;
|
|
1136
|
+
return Math.round(raw);
|
|
1137
|
+
}
|
|
1138
|
+
function computeFromConfig(cfg) {
|
|
1139
|
+
const { baseRem, ratio, mode, multiplier, snap } = cfg;
|
|
1140
|
+
const unit = baseRem * multiplier;
|
|
1141
|
+
return SPACING_STEPS.map((step, i) => {
|
|
1142
|
+
const raw = mode === "harmonic" ? unit * step.multiple : unit * Math.pow(ratio, i - SM_INDEX);
|
|
1143
|
+
const rem = snap ? snapRem(raw) : Math.round(raw * 1e3) / 1e3;
|
|
1144
|
+
return {
|
|
1145
|
+
name: step.name,
|
|
1146
|
+
multiple: step.multiple,
|
|
1147
|
+
rem,
|
|
1148
|
+
px: Math.round(rem * 16)
|
|
1149
|
+
};
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
function computeFromLevels(levels, multiplier) {
|
|
1153
|
+
const bodyM = levels.find((l) => l.level === "body-m");
|
|
1154
|
+
if (!bodyM) return [];
|
|
1155
|
+
const baseRem = bodyM.maxRem * bodyM.lineHeight;
|
|
1156
|
+
return computeFromConfig({
|
|
1157
|
+
baseRem,
|
|
1158
|
+
ratio: 1.272,
|
|
1159
|
+
mode: "harmonic",
|
|
1160
|
+
multiplier,
|
|
1161
|
+
snap: true
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
function computeSpacingTokens(arg, multiplier = 1) {
|
|
1165
|
+
if (Array.isArray(arg)) return computeFromLevels(arg, multiplier);
|
|
1166
|
+
return computeFromConfig(arg);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/lib.ts
|
|
1170
|
+
var BASE_URL = "https://standby.design";
|
|
1171
|
+
function parseInput(input) {
|
|
1172
|
+
if (!input) return { c: null, t: null, s: null, y: null, p: null };
|
|
1173
|
+
let hash = input.trim();
|
|
1174
|
+
if (/^https?:\/\//i.test(hash)) {
|
|
1175
|
+
const idx = hash.indexOf("#");
|
|
1176
|
+
hash = idx === -1 ? "" : hash.slice(idx + 1);
|
|
1177
|
+
}
|
|
1178
|
+
hash = hash.replace(/^#/, "");
|
|
1179
|
+
return parseUnifiedHash(hash);
|
|
1180
|
+
}
|
|
1181
|
+
function buildHash(segs) {
|
|
1182
|
+
return buildUnifiedHash({
|
|
1183
|
+
c: segs.c ?? void 0,
|
|
1184
|
+
t: segs.t ?? void 0,
|
|
1185
|
+
s: segs.s ?? void 0,
|
|
1186
|
+
y: segs.y ?? void 0,
|
|
1187
|
+
p: segs.p ?? void 0
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
function systemUrl(segs) {
|
|
1191
|
+
const hash = buildHash(segs);
|
|
1192
|
+
const colorState = segs.c ? decodeState(segs.c) : null;
|
|
1193
|
+
const params = new URLSearchParams();
|
|
1194
|
+
const name = colorState?.themeName?.trim();
|
|
1195
|
+
if (name && name !== "Standby.Design") params.set("t", name);
|
|
1196
|
+
const hex = colorState?.brandHex?.replace("#", "");
|
|
1197
|
+
if (hex && /^[0-9a-fA-F]{6}$/.test(hex)) params.set("c", hex);
|
|
1198
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1199
|
+
return `${BASE_URL}/system/${query}#${hash}`;
|
|
1200
|
+
}
|
|
1201
|
+
function toolUrl(tool, segs) {
|
|
1202
|
+
return `${BASE_URL}/${tool}#${buildHash(segs)}`;
|
|
1203
|
+
}
|
|
1204
|
+
function normalizeHex(input) {
|
|
1205
|
+
const hex = input.trim().replace(/^#/, "");
|
|
1206
|
+
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
|
1207
|
+
return "#" + hex.toUpperCase();
|
|
1208
|
+
}
|
|
1209
|
+
var DEFAULT_COLOR_STATE = {
|
|
1210
|
+
brandHex: "#335A7F",
|
|
1211
|
+
bgColorHex: "#335A7F",
|
|
1212
|
+
bgAutoMatch: true,
|
|
1213
|
+
errorColorHex: "#CC3333",
|
|
1214
|
+
errorAutoMatch: true,
|
|
1215
|
+
chromaScale: 0.25,
|
|
1216
|
+
currentMode: "balanced",
|
|
1217
|
+
brandPin: false,
|
|
1218
|
+
brandInvert: false,
|
|
1219
|
+
errorPin: false,
|
|
1220
|
+
errorInvert: false,
|
|
1221
|
+
fgContrastMode: "best",
|
|
1222
|
+
themeName: "",
|
|
1223
|
+
extraAccents: [
|
|
1224
|
+
{ name: "Success", hex: "#33994D", pin: false, invert: false, autoMatch: true, autoHue: SUCCESS_HUE },
|
|
1225
|
+
{ name: "Warning", hex: "#998033", pin: false, invert: false, autoMatch: true, autoHue: WARNING_HUE },
|
|
1226
|
+
{ name: "Info", hex: "#3355CC", pin: false, invert: false, autoMatch: true, autoHue: INFO_HUE }
|
|
1227
|
+
]
|
|
1228
|
+
};
|
|
1229
|
+
var DEFAULT_TYPE_HASH = "custom,1,1.272,1.19,satoshi,satoshi,system-mono";
|
|
1230
|
+
function defaultTypeState() {
|
|
1231
|
+
return decodeState2(DEFAULT_TYPE_HASH);
|
|
1232
|
+
}
|
|
1233
|
+
var DEFAULT_SHAPE_STATE = {
|
|
1234
|
+
shapeStyle: "paper",
|
|
1235
|
+
shadowEnabled: true,
|
|
1236
|
+
shadowType: "normal",
|
|
1237
|
+
shadowStrength: 1,
|
|
1238
|
+
shadowBlurScale: 1,
|
|
1239
|
+
shadowScale: 1.272,
|
|
1240
|
+
shadowColorMode: "auto",
|
|
1241
|
+
shadowCustomColor: "#000000",
|
|
1242
|
+
borderEnabled: true,
|
|
1243
|
+
borderWidth: 1,
|
|
1244
|
+
borderColorMode: "auto",
|
|
1245
|
+
borderCustomColor: "#000000",
|
|
1246
|
+
borderRadius: 8,
|
|
1247
|
+
glassDepth: 0.2,
|
|
1248
|
+
glassBlur: 1,
|
|
1249
|
+
glassDispersion: 0.4,
|
|
1250
|
+
ringWidth: 2,
|
|
1251
|
+
ringOffset: 2,
|
|
1252
|
+
ringColorMode: "auto",
|
|
1253
|
+
ringCustomColor: "#000000",
|
|
1254
|
+
separationMode: "shadow",
|
|
1255
|
+
shadowOffsetX: 2,
|
|
1256
|
+
shadowOffsetY: 4,
|
|
1257
|
+
brutalistVariant: "outlined"
|
|
1258
|
+
};
|
|
1259
|
+
var DEFAULT_SYMBOL_STATE = {
|
|
1260
|
+
preferredStyle: "auto",
|
|
1261
|
+
preferredWeight: "auto",
|
|
1262
|
+
preferredCorners: "auto",
|
|
1263
|
+
iconBaseSize: 1.25,
|
|
1264
|
+
iconScale: 1.272,
|
|
1265
|
+
snapTo4px: true,
|
|
1266
|
+
selectedSet: null
|
|
1267
|
+
};
|
|
1268
|
+
function colorStateFrom(segs) {
|
|
1269
|
+
return (segs.c ? decodeState(segs.c) : null) ?? DEFAULT_COLOR_STATE;
|
|
1270
|
+
}
|
|
1271
|
+
function typeStateFrom(segs) {
|
|
1272
|
+
return (segs.t ? decodeState2(segs.t) : null) ?? defaultTypeState();
|
|
1273
|
+
}
|
|
1274
|
+
function shapeStateFrom(segs) {
|
|
1275
|
+
const decoded = segs.s ? decodeState3(segs.s) : null;
|
|
1276
|
+
return { ...DEFAULT_SHAPE_STATE, ...decoded ?? {} };
|
|
1277
|
+
}
|
|
1278
|
+
function symbolStateFrom(segs) {
|
|
1279
|
+
return segs.y ? decodeState4(segs.y) : null;
|
|
1280
|
+
}
|
|
1281
|
+
function spaceStateFrom(segs) {
|
|
1282
|
+
const decoded = segs.p ? decodeState5(segs.p) : null;
|
|
1283
|
+
return { ...DEFAULT_SPACE_URL_STATE, ...decoded ?? {} };
|
|
1284
|
+
}
|
|
1285
|
+
function accentCssName(name) {
|
|
1286
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "accent";
|
|
1287
|
+
}
|
|
1288
|
+
function buildPalette(colorState) {
|
|
1289
|
+
const { brandHex, bgColorHex, bgAutoMatch, errorColorHex, errorAutoMatch, chromaScale, currentMode, extraAccents, brandPin, errorPin } = colorState;
|
|
1290
|
+
const effectiveBgHex = bgAutoMatch ? brandHex : bgColorHex;
|
|
1291
|
+
const effectiveErrorHex = errorAutoMatch ? computeAutoErrorHex(brandHex) : errorColorHex;
|
|
1292
|
+
const brand = generatePalette(brandHex, 1, currentMode);
|
|
1293
|
+
const surface = generatePalette(effectiveBgHex, chromaScale, currentMode);
|
|
1294
|
+
const error = generatePalette(effectiveErrorHex, 1, currentMode);
|
|
1295
|
+
const errorSurface = generatePalette(effectiveErrorHex, chromaScale, currentMode);
|
|
1296
|
+
const neutral = generatePalette(effectiveBgHex, 0, currentMode);
|
|
1297
|
+
const accentPalettes = (extraAccents || []).filter((a) => a.autoMatch || /^#[0-9a-fA-F]{6}$/.test(a.hex)).map((a) => {
|
|
1298
|
+
const effectiveHex = a.autoMatch ? computeAutoAccentHex(brandHex, a.autoHue) : a.hex;
|
|
1299
|
+
return {
|
|
1300
|
+
name: a.name,
|
|
1301
|
+
hex: effectiveHex,
|
|
1302
|
+
cssName: accentCssName(a.name),
|
|
1303
|
+
palette: generatePalette(effectiveHex, 1, currentMode),
|
|
1304
|
+
slatedPalette: generatePalette(effectiveHex, chromaScale, currentMode),
|
|
1305
|
+
pin: a.pin,
|
|
1306
|
+
invert: a.invert
|
|
1307
|
+
};
|
|
1308
|
+
});
|
|
1309
|
+
const brandSwatchOverride = brandPin ? { hex: brandHex, L: hexToOklch(brandHex)[0] } : null;
|
|
1310
|
+
const errorSwatchOverride = errorPin ? { hex: effectiveErrorHex, L: hexToOklch(effectiveErrorHex)[0] } : null;
|
|
1311
|
+
const neutralExtended = [
|
|
1312
|
+
{ step: 0, L: 1, C: 0, H: 0, hex: "#FFFFFF", css: "oklch(1 0 0)" },
|
|
1313
|
+
...neutral,
|
|
1314
|
+
{ step: 1e3, L: 0, C: 0, H: 0, hex: "#000000", css: "oklch(0 0 0)" }
|
|
1315
|
+
];
|
|
1316
|
+
return { brand, surface, error, errorSurface, neutral, neutralExtended, accentPalettes, brandSwatchOverride, errorSwatchOverride, effectiveBgHex, effectiveErrorHex };
|
|
1317
|
+
}
|
|
1318
|
+
function buildScale(typeState) {
|
|
1319
|
+
let s;
|
|
1320
|
+
if (typeState.scaleMode === "traditional") {
|
|
1321
|
+
const desktop = typeState.traditionalAssignments ?? DEFAULT_TRADITIONAL;
|
|
1322
|
+
const mobile = typeState.traditionalMobileAssignments ?? DEFAULT_TRADITIONAL_MOBILE;
|
|
1323
|
+
s = traditionalScale(desktop, mobile);
|
|
1324
|
+
} else {
|
|
1325
|
+
s = customScale(typeState.baseSize, typeState.customRatio, typeState.mobileRatio, typeState.mobileBaseSize ?? typeState.baseSize);
|
|
1326
|
+
}
|
|
1327
|
+
return applyTypography(s, typeState.lineHeightOverrides ?? {}, typeState.letterSpacingOverrides ?? {});
|
|
1328
|
+
}
|
|
1329
|
+
function buildSpacing(spaceState) {
|
|
1330
|
+
return computeSpacingTokens({
|
|
1331
|
+
baseRem: spaceState.spacingBaseRem,
|
|
1332
|
+
ratio: spaceState.spacingRatio,
|
|
1333
|
+
mode: spaceState.spacingMode,
|
|
1334
|
+
multiplier: spaceState.spacingMultiplier,
|
|
1335
|
+
snap: spaceState.spacingSnap
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
function stepHex(entries, step) {
|
|
1339
|
+
return entries.find((e) => e.step === step)?.hex ?? "";
|
|
1340
|
+
}
|
|
1341
|
+
function textResult(text) {
|
|
1342
|
+
return { content: [{ type: "text", text }] };
|
|
1343
|
+
}
|
|
1344
|
+
function errorResult(text) {
|
|
1345
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// ../core/src/icon-tokens.ts
|
|
1349
|
+
function computeIconTokens(baseSize, scale, strokePx, snapTo4px = true) {
|
|
1350
|
+
const md = baseSize;
|
|
1351
|
+
const sm = md / scale;
|
|
1352
|
+
const xs = sm / scale;
|
|
1353
|
+
const lg = md * scale;
|
|
1354
|
+
const xl = lg * scale;
|
|
1355
|
+
const xxl = xl * scale;
|
|
1356
|
+
const toToken = (raw, name, useCase) => {
|
|
1357
|
+
const px = snapTo4px ? Math.round(raw * 16 / 4) * 4 : Math.round(raw * 16);
|
|
1358
|
+
const rem = snapTo4px ? px / 16 : Math.round(raw * 1e4) / 1e4;
|
|
1359
|
+
return { name, rem, px, useCase };
|
|
1360
|
+
};
|
|
1361
|
+
return {
|
|
1362
|
+
sizes: [
|
|
1363
|
+
toToken(xs, "xs", "Inline, caption-level"),
|
|
1364
|
+
toToken(sm, "sm", "Body text companion"),
|
|
1365
|
+
toToken(md, "md", "Standard UI elements"),
|
|
1366
|
+
toToken(lg, "lg", "Navigation, actions"),
|
|
1367
|
+
toToken(xl, "xl", "Feature, hero sections"),
|
|
1368
|
+
toToken(xxl, "2xl", "Display, illustration")
|
|
1369
|
+
],
|
|
1370
|
+
strokeWidth: strokePx
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function weightToStroke(weight) {
|
|
1374
|
+
switch (weight) {
|
|
1375
|
+
case "thin":
|
|
1376
|
+
return 1;
|
|
1377
|
+
case "regular":
|
|
1378
|
+
return 1.5;
|
|
1379
|
+
case "bold":
|
|
1380
|
+
return 2;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// ../core/src/recommend.ts
|
|
1385
|
+
var WEIGHT_ORDER = ["thin", "regular", "bold"];
|
|
1386
|
+
function styleScore(pref, actual) {
|
|
1387
|
+
if (pref === "auto") return { score: 0.7, reason: null };
|
|
1388
|
+
if (pref === actual) return { score: 1, reason: `${actual} style matches` };
|
|
1389
|
+
return { score: 0.2, reason: null };
|
|
1390
|
+
}
|
|
1391
|
+
function moodScore(prefMood, setMood) {
|
|
1392
|
+
const score = Math.exp(-Math.pow(setMood - prefMood, 2) / 2e3);
|
|
1393
|
+
const diff = Math.abs(setMood - prefMood);
|
|
1394
|
+
if (diff <= 15) return { score, reason: "mood closely matches" };
|
|
1395
|
+
return { score, reason: null };
|
|
1396
|
+
}
|
|
1397
|
+
function weightScore(pref, actual) {
|
|
1398
|
+
if (pref === "auto") return { score: 0.7, reason: null };
|
|
1399
|
+
if (pref === actual) return { score: 1, reason: `${actual} weight matches` };
|
|
1400
|
+
const prefIdx = WEIGHT_ORDER.indexOf(pref);
|
|
1401
|
+
const actIdx = WEIGHT_ORDER.indexOf(actual);
|
|
1402
|
+
if (Math.abs(prefIdx - actIdx) === 1) return { score: 0.5, reason: null };
|
|
1403
|
+
return { score: 0.1, reason: null };
|
|
1404
|
+
}
|
|
1405
|
+
function cornerScore(pref, actual) {
|
|
1406
|
+
if (pref === "auto") return { score: 0.7, reason: null };
|
|
1407
|
+
if (pref === actual) return { score: 1, reason: `${actual} corners match` };
|
|
1408
|
+
if (actual === "mixed") return { score: 0.6, reason: null };
|
|
1409
|
+
return { score: 0.2, reason: null };
|
|
1410
|
+
}
|
|
1411
|
+
function recommendSets(prefs) {
|
|
1412
|
+
return ICON_SETS.map((set) => {
|
|
1413
|
+
const style = styleScore(prefs.style, set.style);
|
|
1414
|
+
const mood = moodScore(prefs.mood, set.mood);
|
|
1415
|
+
const weight = weightScore(prefs.weight, set.strokeWeight);
|
|
1416
|
+
const corners = cornerScore(prefs.corners, set.cornerStyle);
|
|
1417
|
+
const score = style.score * 0.25 + mood.score * 0.3 + weight.score * 0.2 + corners.score * 0.25;
|
|
1418
|
+
const reasons = [style.reason, mood.reason, weight.reason, corners.reason].filter(Boolean);
|
|
1419
|
+
return { set, score, reasons };
|
|
1420
|
+
}).sort((a, b) => b.score - a.score);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ../core/src/fontshare.ts
|
|
1424
|
+
var MONO_FONTS = [
|
|
1425
|
+
{ slug: "system-mono", name: "System Default", category: "mono", fallback: "monospace", isSystem: true },
|
|
1426
|
+
{ slug: "consolas", name: "Consolas", category: "mono", fallback: "monospace", isSystem: true },
|
|
1427
|
+
{ slug: "sf-mono", name: "SF Mono", category: "mono", fallback: "monospace", isSystem: true }
|
|
1428
|
+
];
|
|
1429
|
+
var SYSTEM_MONO_STACKS = {
|
|
1430
|
+
"system-mono": "ui-monospace, 'Cascadia Code', Consolas, 'SF Mono', Menlo, 'DejaVu Sans Mono', monospace",
|
|
1431
|
+
"consolas": "Consolas, 'Cascadia Code', 'DejaVu Sans Mono', monospace",
|
|
1432
|
+
"sf-mono": "'SF Mono', Menlo, 'DejaVu Sans Mono', monospace"
|
|
1433
|
+
};
|
|
1434
|
+
var SEED_CATALOG = [
|
|
1435
|
+
{ slug: "satoshi", name: "Satoshi", category: "sans", fallback: "sans-serif" },
|
|
1436
|
+
{ slug: "general-sans", name: "General Sans", category: "sans", fallback: "sans-serif" },
|
|
1437
|
+
{ slug: "zodiak", name: "Zodiak", category: "serif", fallback: "serif" },
|
|
1438
|
+
{ slug: "clash-display", name: "Clash Display", category: "display", fallback: "sans-serif" },
|
|
1439
|
+
...MONO_FONTS
|
|
1440
|
+
];
|
|
1441
|
+
var catalog = [...SEED_CATALOG];
|
|
1442
|
+
var catalogMap = new Map(catalog.map((f) => [f.slug, f]));
|
|
1443
|
+
function getCatalog() {
|
|
1444
|
+
return catalog;
|
|
1445
|
+
}
|
|
1446
|
+
function fontFamily(slug) {
|
|
1447
|
+
const monoStack = SYSTEM_MONO_STACKS[slug];
|
|
1448
|
+
if (monoStack) return monoStack;
|
|
1449
|
+
const entry = catalogMap.get(slug);
|
|
1450
|
+
if (!entry) return "sans-serif";
|
|
1451
|
+
return `'${entry.name}', ${entry.fallback}`;
|
|
1452
|
+
}
|
|
1453
|
+
function buildFontshareUrl(slugs) {
|
|
1454
|
+
const unique = [...new Set(slugs.filter((s) => !SYSTEM_MONO_STACKS[s] && s))];
|
|
1455
|
+
if (unique.length === 0) return "";
|
|
1456
|
+
const params = unique.map((s) => `f[]=${s}@1,2`).join("&");
|
|
1457
|
+
return `https://api.fontshare.com/v2/css?${params}&display=swap`;
|
|
1458
|
+
}
|
|
1459
|
+
function buildFontshareEmbed(slugs) {
|
|
1460
|
+
const url = buildFontshareUrl(slugs);
|
|
1461
|
+
if (!url) return "";
|
|
1462
|
+
return `<link href="${url}" rel="stylesheet">`;
|
|
1463
|
+
}
|
|
1464
|
+
function fontsByCategory() {
|
|
1465
|
+
const groups = {};
|
|
1466
|
+
for (const font of catalog) {
|
|
1467
|
+
if (!groups[font.category]) groups[font.category] = [];
|
|
1468
|
+
groups[font.category].push(font);
|
|
1469
|
+
}
|
|
1470
|
+
return groups;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/summaries.ts
|
|
1474
|
+
function colorSummary(state, palette) {
|
|
1475
|
+
const lines = [];
|
|
1476
|
+
const name = state.themeName && state.themeName !== "Standby.Design" ? ` \xB7 theme "${state.themeName}"` : "";
|
|
1477
|
+
lines.push(`## Color \u2014 brand ${state.brandHex} \xB7 mode ${state.currentMode} \xB7 surface chroma ${Math.round(state.chromaScale * 100)}%${name}`);
|
|
1478
|
+
lines.push("");
|
|
1479
|
+
lines.push(`Brand scale: ${palette.brand.map((e) => `${e.step}:${e.hex}`).join(" ")}`);
|
|
1480
|
+
lines.push(`Surface scale: ${palette.surface.map((e) => `${e.step}:${e.hex}`).join(" ")}`);
|
|
1481
|
+
lines.push(`Error: ${palette.effectiveErrorHex}${state.errorAutoMatch ? " (auto-derived from brand hue)" : ""}`);
|
|
1482
|
+
if (palette.accentPalettes.length > 0) {
|
|
1483
|
+
lines.push(`Accents: ${palette.accentPalettes.map((a) => `${a.name} ${a.hex}${a.pin ? " (pinned)" : ""}`).join(" \xB7 ")}`);
|
|
1484
|
+
}
|
|
1485
|
+
lines.push("");
|
|
1486
|
+
lines.push("Key semantic tokens (light/dark):");
|
|
1487
|
+
lines.push(`- primary: ${state.brandPin ? state.brandHex + " (pinned)" : `${stepHex(palette.brand, 600)} / ${stepHex(palette.brand, 400)}`}`);
|
|
1488
|
+
lines.push(`- background: ${stepHex(palette.surface, 50)} / ${stepHex(palette.surface, 875)}`);
|
|
1489
|
+
lines.push(`- destructive: ${state.errorPin ? palette.effectiveErrorHex + " (pinned)" : `${stepHex(palette.error, 600)} / ${stepHex(palette.error, 400)}`}`);
|
|
1490
|
+
return lines.join("\n");
|
|
1491
|
+
}
|
|
1492
|
+
function typeSummary(state, scale) {
|
|
1493
|
+
const lines = [];
|
|
1494
|
+
const modeLabel = state.scaleMode === "traditional" ? "traditional scale" : `custom ratio ${state.customRatio} (mobile ${state.mobileRatio})`;
|
|
1495
|
+
lines.push(`## Typography \u2014 ${modeLabel} \xB7 base ${state.baseSize}rem`);
|
|
1496
|
+
lines.push(`Fonts: heading ${fontFamily(state.headingFont)} (${state.headingFont}, weight ${state.headingWeight}) \xB7 body ${fontFamily(state.bodyFont)} (${state.bodyFont}) \xB7 mono ${fontFamily(state.monoFont)} (${state.monoFont})`);
|
|
1497
|
+
lines.push("");
|
|
1498
|
+
lines.push("Level | Mobile\u2192Desktop | Line-height | Letter-spacing");
|
|
1499
|
+
for (const l of scale) {
|
|
1500
|
+
const size = l.isFluid ? `${l.minRem}rem \u2192 ${l.maxRem}rem` : `${l.maxRem}rem`;
|
|
1501
|
+
lines.push(`${l.label} | ${size} | ${l.lineHeight} | ${l.letterSpacing}em`);
|
|
1502
|
+
}
|
|
1503
|
+
return lines.join("\n");
|
|
1504
|
+
}
|
|
1505
|
+
function spaceSummary(state, spacing) {
|
|
1506
|
+
const lines = [];
|
|
1507
|
+
const mode = state.spacingMode === "geometric" ? `geometric \xD7${state.spacingRatio}` : "harmonic multiples";
|
|
1508
|
+
lines.push(`## Spacing & Layout \u2014 ${mode} \xB7 base ${state.spacingBaseRem}rem \xB7 multiplier ${state.spacingMultiplier}\xD7 \xB7 snap ${state.spacingSnap ? "on" : "off"}`);
|
|
1509
|
+
lines.push(`Spacing: ${spacing.map((t) => `${t.name}:${t.rem}rem`).join(" ")}`);
|
|
1510
|
+
lines.push(`Breakpoints: ${state.breakpoints.map((b) => `${b.name}:${b.minPx}px`).join(" ")}`);
|
|
1511
|
+
lines.push(`Containers: ${state.containers.map((c) => `${c.name}:${c.maxPx}px`).join(" ")} \xB7 prose ${state.proseMaxCh}ch`);
|
|
1512
|
+
lines.push(`Aspect ratios: ${state.aspectRatios.map((a) => `${a.name} ${a.w}:${a.h}`).join(" \xB7 ")}${state.aspectIncludeReciprocals ? " (+ reciprocals)" : ""}`);
|
|
1513
|
+
lines.push(`Fluid viewport anchors: ${state.fluidMinVw}px \u2192 ${state.fluidMaxVw}px`);
|
|
1514
|
+
return lines.join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
function shapeSummary(state) {
|
|
1517
|
+
const lines = [];
|
|
1518
|
+
lines.push(`## Shape \u2014 style ${state.shapeStyle} \xB7 radius base ${state.borderRadius}px`);
|
|
1519
|
+
switch (state.shapeStyle) {
|
|
1520
|
+
case "paper":
|
|
1521
|
+
lines.push(`Shadows: ${state.shadowEnabled ? `${state.shadowType}, strength ${state.shadowStrength}, blur \xD7${state.shadowBlurScale}, level spread \xD7${state.shadowScale}` : "off"} (color ${state.shadowColorMode === "custom" ? state.shadowCustomColor : "auto"})`);
|
|
1522
|
+
break;
|
|
1523
|
+
case "glass":
|
|
1524
|
+
lines.push(`Glass: depth ${state.glassDepth} \xB7 blur ${state.glassBlur} \xB7 dispersion ${state.glassDispersion}`);
|
|
1525
|
+
break;
|
|
1526
|
+
case "neomorph":
|
|
1527
|
+
lines.push(`Neumorphic shadows: strength ${state.shadowStrength}, blur \xD7${state.shadowBlurScale}`);
|
|
1528
|
+
break;
|
|
1529
|
+
case "neobrutalism":
|
|
1530
|
+
lines.push(`Brutalist: variant ${state.brutalistVariant} \xB7 shadow offset ${state.shadowOffsetX}px/${state.shadowOffsetY}px`);
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
lines.push(`Border: ${state.borderEnabled ? `${state.borderWidth}px (color ${state.borderColorMode === "custom" ? state.borderCustomColor : "auto"})` : "off"}`);
|
|
1534
|
+
lines.push(`Focus ring: ${state.ringWidth}px width, ${state.ringOffset}px offset (color ${state.ringColorMode === "custom" ? state.ringCustomColor : "auto"})`);
|
|
1535
|
+
lines.push(`Separation mode: ${state.separationMode}`);
|
|
1536
|
+
return lines.join("\n");
|
|
1537
|
+
}
|
|
1538
|
+
function resolveIconSet(state) {
|
|
1539
|
+
if (state.selectedSet) {
|
|
1540
|
+
const set = getSetById(state.selectedSet);
|
|
1541
|
+
if (set) return { set, selected: true };
|
|
1542
|
+
}
|
|
1543
|
+
const recommended = recommendSets({
|
|
1544
|
+
style: state.preferredStyle,
|
|
1545
|
+
mood: 50,
|
|
1546
|
+
weight: state.preferredWeight,
|
|
1547
|
+
corners: state.preferredCorners
|
|
1548
|
+
});
|
|
1549
|
+
return { set: recommended[0]?.set || ICON_SETS[0], selected: false };
|
|
1550
|
+
}
|
|
1551
|
+
function symbolSummary(state) {
|
|
1552
|
+
const { set, selected } = resolveIconSet(state);
|
|
1553
|
+
const tokens = computeIconTokens(state.iconBaseSize, state.iconScale, weightToStroke(set.strokeWeight), state.snapTo4px);
|
|
1554
|
+
const lines = [];
|
|
1555
|
+
lines.push(`## Icons \u2014 ${selected ? "selected" : "recommended"}: ${set.name} (${set.id})`);
|
|
1556
|
+
lines.push(`${set.description} \xB7 install: npm install ${set.npmPackage}`);
|
|
1557
|
+
lines.push(`Style ${set.style} \xB7 weight ${set.strokeWeight} \xB7 corners ${set.cornerStyle}`);
|
|
1558
|
+
lines.push(`Sizes (base ${state.iconBaseSize}rem, scale \xD7${state.iconScale}${state.snapTo4px ? ", 4px snap" : ""}): ${tokens.sizes.map((s) => `${s.name}:${s.px}px`).join(" ")} \xB7 stroke ${tokens.strokeWidth}px`);
|
|
1559
|
+
return lines.join("\n");
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// src/tools-generate.ts
|
|
1563
|
+
var URL_PARAM = z.string().optional().describe(
|
|
1564
|
+
"Existing standby.design URL (or raw hash) to modify. Only this tool's section is changed; color/type/shape/icon/spacing settings from other tools are preserved. Omit to start fresh from defaults."
|
|
1565
|
+
);
|
|
1566
|
+
function header(segs, editTool) {
|
|
1567
|
+
return [
|
|
1568
|
+
`Design system: ${systemUrl(segs)}`,
|
|
1569
|
+
`Fine-tune in UI: ${toolUrl(editTool, segs)}`
|
|
1570
|
+
].join("\n");
|
|
1571
|
+
}
|
|
1572
|
+
var READ_ONLY = { readOnlyHint: true, openWorldHint: false };
|
|
1573
|
+
var ACCENT_PRESET_HUES = {
|
|
1574
|
+
success: SUCCESS_HUE,
|
|
1575
|
+
warning: WARNING_HUE,
|
|
1576
|
+
info: INFO_HUE
|
|
1577
|
+
};
|
|
1578
|
+
function registerGenerateTools(server2) {
|
|
1579
|
+
server2.registerTool(
|
|
1580
|
+
"generate_color_palette",
|
|
1581
|
+
{
|
|
1582
|
+
title: "Generate color palette",
|
|
1583
|
+
description: "Generate a perceptually uniform OKLCH color palette from a brand color: 18-step scales for brand/surface/error/accents, semantic tokens (shadcn/ui compatible), light+dark modes. Returns a shareable standby.design/system URL and a compact summary. Use export_design_system for full CSS/Tailwind output.",
|
|
1584
|
+
inputSchema: {
|
|
1585
|
+
url: URL_PARAM,
|
|
1586
|
+
brandHex: z.string().optional().describe('Brand color as 6-digit hex, e.g. "#335A7F". The palette midpoint is derived from this.'),
|
|
1587
|
+
themeName: z.string().max(60).optional().describe("Name of the design system (appears in exports and page title)."),
|
|
1588
|
+
mode: z.enum(["balanced", "exact"]).optional().describe(`"balanced": step 500 is always the perceptual midpoint (even light/dark distribution). "exact": step 500 keeps the input color's lightness.`),
|
|
1589
|
+
chromaScale: z.number().min(0).max(1).optional().describe("Surface saturation 0\u20131 (0 = grey surfaces, 1 = vibrant). Default 0.25."),
|
|
1590
|
+
bgColorHex: z.string().optional().describe('Surface tint color as hex, or "auto" to derive from the brand color (default).'),
|
|
1591
|
+
errorColorHex: z.string().optional().describe('Error/destructive color as hex, or "auto" to derive from the brand hue (default).'),
|
|
1592
|
+
brandPin: z.boolean().optional().describe("Pin the primary token to the exact input hex instead of the palette step."),
|
|
1593
|
+
brandInvert: z.boolean().optional().describe("For pinned brand: mirror lightness in dark mode (e.g. black buttons in light mode, white in dark)."),
|
|
1594
|
+
errorPin: z.boolean().optional(),
|
|
1595
|
+
errorInvert: z.boolean().optional(),
|
|
1596
|
+
fgContrastMode: z.enum(["best", "preferLight", "preferDark"]).optional().describe("Text color strategy on colored backgrounds."),
|
|
1597
|
+
accents: z.array(z.object({
|
|
1598
|
+
name: z.string().min(1).max(30),
|
|
1599
|
+
hex: z.string().optional().describe("Hex color. Omit to auto-derive from the brand color (requires autoHue or a preset name: Success/Warning/Info)."),
|
|
1600
|
+
autoHue: z.number().min(0).max(360).optional().describe("OKLCH hue for auto-derived accents."),
|
|
1601
|
+
pin: z.boolean().optional(),
|
|
1602
|
+
invert: z.boolean().optional()
|
|
1603
|
+
})).max(3).optional().describe("Up to 3 additional named accent colors. Replaces the existing accent list when provided; pass [] to remove all accents. Default: Success/Warning/Info, auto-derived.")
|
|
1604
|
+
},
|
|
1605
|
+
annotations: READ_ONLY
|
|
1606
|
+
},
|
|
1607
|
+
async (args) => {
|
|
1608
|
+
const segs = parseInput(args.url);
|
|
1609
|
+
const state = { ...colorStateFrom(segs), extraAccents: [...colorStateFrom(segs).extraAccents] };
|
|
1610
|
+
if (args.brandHex !== void 0) {
|
|
1611
|
+
const hex = normalizeHex(args.brandHex);
|
|
1612
|
+
if (!hex) return errorResult(`Invalid brandHex "${args.brandHex}" \u2014 expected a 6-digit hex color like #335A7F.`);
|
|
1613
|
+
state.brandHex = hex;
|
|
1614
|
+
}
|
|
1615
|
+
if (args.themeName !== void 0) state.themeName = args.themeName;
|
|
1616
|
+
if (args.mode !== void 0) state.currentMode = args.mode;
|
|
1617
|
+
if (args.chromaScale !== void 0) state.chromaScale = args.chromaScale;
|
|
1618
|
+
if (args.bgColorHex !== void 0) {
|
|
1619
|
+
if (args.bgColorHex.toLowerCase() === "auto") {
|
|
1620
|
+
state.bgAutoMatch = true;
|
|
1621
|
+
state.bgColorHex = state.brandHex;
|
|
1622
|
+
} else {
|
|
1623
|
+
const hex = normalizeHex(args.bgColorHex);
|
|
1624
|
+
if (!hex) return errorResult(`Invalid bgColorHex "${args.bgColorHex}" \u2014 expected a 6-digit hex color or "auto".`);
|
|
1625
|
+
state.bgAutoMatch = false;
|
|
1626
|
+
state.bgColorHex = hex;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (args.errorColorHex !== void 0) {
|
|
1630
|
+
if (args.errorColorHex.toLowerCase() === "auto") {
|
|
1631
|
+
state.errorAutoMatch = true;
|
|
1632
|
+
} else {
|
|
1633
|
+
const hex = normalizeHex(args.errorColorHex);
|
|
1634
|
+
if (!hex) return errorResult(`Invalid errorColorHex "${args.errorColorHex}" \u2014 expected a 6-digit hex color or "auto".`);
|
|
1635
|
+
state.errorAutoMatch = false;
|
|
1636
|
+
state.errorColorHex = hex;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (args.brandPin !== void 0) state.brandPin = args.brandPin;
|
|
1640
|
+
if (args.brandInvert !== void 0) state.brandInvert = args.brandInvert;
|
|
1641
|
+
if (args.errorPin !== void 0) state.errorPin = args.errorPin;
|
|
1642
|
+
if (args.errorInvert !== void 0) state.errorInvert = args.errorInvert;
|
|
1643
|
+
if (args.fgContrastMode !== void 0) state.fgContrastMode = args.fgContrastMode;
|
|
1644
|
+
if (args.accents !== void 0) {
|
|
1645
|
+
const accents = [];
|
|
1646
|
+
for (const a of args.accents) {
|
|
1647
|
+
if (a.hex) {
|
|
1648
|
+
const hex = normalizeHex(a.hex);
|
|
1649
|
+
if (!hex) return errorResult(`Invalid hex "${a.hex}" for accent "${a.name}".`);
|
|
1650
|
+
accents.push({ name: a.name, hex, pin: a.pin ?? false, invert: a.invert ?? false, autoMatch: false, autoHue: 0 });
|
|
1651
|
+
} else {
|
|
1652
|
+
const hue = a.autoHue ?? ACCENT_PRESET_HUES[a.name.toLowerCase()];
|
|
1653
|
+
if (hue === void 0) {
|
|
1654
|
+
return errorResult(`Accent "${a.name}": provide either a hex color or an autoHue (0\u2013360) for auto-derivation. Presets with known hues: Success, Warning, Info.`);
|
|
1655
|
+
}
|
|
1656
|
+
accents.push({ name: a.name, hex: "#000000", pin: a.pin ?? false, invert: a.invert ?? false, autoMatch: true, autoHue: hue });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
state.extraAccents = accents;
|
|
1660
|
+
}
|
|
1661
|
+
const next = { ...segs, c: encodeState(state) };
|
|
1662
|
+
const palette = buildPalette(state);
|
|
1663
|
+
return textResult(`${header(next, "color")}
|
|
1664
|
+
|
|
1665
|
+
${colorSummary(state, palette)}`);
|
|
1666
|
+
}
|
|
1667
|
+
);
|
|
1668
|
+
const levelOverrides = z.record(z.string(), z.number()).optional();
|
|
1669
|
+
server2.registerTool(
|
|
1670
|
+
"generate_type_scale",
|
|
1671
|
+
{
|
|
1672
|
+
title: "Generate type scale",
|
|
1673
|
+
description: "Generate a fluid typographic scale (CSS clamp() between 375px and 1920px viewports) with 11 levels (Display, H1\u2013H6, Body L/M/S, Caption), Fontshare fonts, line heights and letter spacing. Returns a shareable standby.design/system URL and a summary table. Use list_fonts to discover font slugs.",
|
|
1674
|
+
inputSchema: {
|
|
1675
|
+
url: URL_PARAM,
|
|
1676
|
+
scaleMode: z.enum(["custom", "traditional"]).optional().describe('"custom": compound ratio scale (default). "traditional": classical Renaissance point sizes per level.'),
|
|
1677
|
+
baseSize: z.number().min(0.5).max(2).optional().describe("Desktop base size in rem (default 1.0)."),
|
|
1678
|
+
ratio: z.number().min(1.05).max(1.7).optional().describe("Scale ratio for custom mode. Presets: 1.2 minor third, 1.25 major third, 1.272 golden-ratio-area (default, \u221A\u03C6), 1.333 perfect fourth, 1.618 golden ratio. The empirical perceptual corridor is 1.20\u20131.50."),
|
|
1679
|
+
mobileBaseSize: z.number().min(0.5).max(2).optional().describe("Mobile base size in rem (default = baseSize)."),
|
|
1680
|
+
mobileRatio: z.number().min(1).max(1.7).optional().describe("Explicit mobile scale ratio. Omit to auto-derive from ratio and autoShrink."),
|
|
1681
|
+
autoShrink: z.number().min(0).max(100).optional().describe("Percent reduction for the auto-derived mobile ratio (default 25). Only used when mobileRatio is not set."),
|
|
1682
|
+
headingFont: z.string().optional().describe('Fontshare font slug for headings, e.g. "satoshi", "general-sans", "clash-display".'),
|
|
1683
|
+
bodyFont: z.string().optional().describe("Fontshare font slug for body text."),
|
|
1684
|
+
monoFont: z.string().optional().describe('Font slug for code. System stacks: "system-mono", "consolas", "sf-mono".'),
|
|
1685
|
+
headingWeight: z.number().int().min(100).max(900).optional().describe("Font weight for headings (default 500)."),
|
|
1686
|
+
lineHeightOverrides: levelOverrides.describe('Per-level line-height overrides, e.g. {"h1": 1.1, "body-m": 1.6}. Levels: display, h1\u2013h6, body-l, body-m, body-s, caption.'),
|
|
1687
|
+
letterSpacingOverrides: levelOverrides.describe('Per-level letter-spacing overrides in em, e.g. {"display": -0.05}.'),
|
|
1688
|
+
traditionalAssignments: z.record(z.string(), z.number()).optional().describe('For traditional mode: desktop px size per level, e.g. {"display": 48, "h1": 36, ...}.'),
|
|
1689
|
+
traditionalMobileAssignments: z.record(z.string(), z.number()).optional().describe("For traditional mode: mobile px size per level.")
|
|
1690
|
+
},
|
|
1691
|
+
annotations: READ_ONLY
|
|
1692
|
+
},
|
|
1693
|
+
async (args) => {
|
|
1694
|
+
const segs = parseInput(args.url);
|
|
1695
|
+
const state = typeStateFrom(segs);
|
|
1696
|
+
for (const key of ["lineHeightOverrides", "letterSpacingOverrides", "traditionalAssignments", "traditionalMobileAssignments"]) {
|
|
1697
|
+
const rec = args[key];
|
|
1698
|
+
if (!rec) continue;
|
|
1699
|
+
const bad = Object.keys(rec).filter((k) => !TYPE_LEVELS.includes(k));
|
|
1700
|
+
if (bad.length > 0) return errorResult(`Invalid type level(s) in ${key}: ${bad.join(", ")}. Valid levels: ${TYPE_LEVELS.join(", ")}.`);
|
|
1701
|
+
}
|
|
1702
|
+
if (args.scaleMode !== void 0) state.scaleMode = args.scaleMode;
|
|
1703
|
+
if (args.baseSize !== void 0) {
|
|
1704
|
+
if (state.mobileBaseSize === state.baseSize && args.mobileBaseSize === void 0) {
|
|
1705
|
+
state.mobileBaseSize = args.baseSize;
|
|
1706
|
+
}
|
|
1707
|
+
state.baseSize = args.baseSize;
|
|
1708
|
+
}
|
|
1709
|
+
if (args.ratio !== void 0) state.customRatio = args.ratio;
|
|
1710
|
+
if (args.mobileBaseSize !== void 0) state.mobileBaseSize = args.mobileBaseSize;
|
|
1711
|
+
if (args.autoShrink !== void 0) state.autoShrink = args.autoShrink;
|
|
1712
|
+
if (args.mobileRatio !== void 0) {
|
|
1713
|
+
state.mobileRatioMode = "custom";
|
|
1714
|
+
state.mobileRatio = args.mobileRatio;
|
|
1715
|
+
} else if (state.mobileRatioMode === "auto") {
|
|
1716
|
+
state.mobileRatio = Math.round((1 + (state.customRatio - 1) * (1 - state.autoShrink / 100)) * 1e3) / 1e3;
|
|
1717
|
+
}
|
|
1718
|
+
if (args.headingFont !== void 0) state.headingFont = args.headingFont;
|
|
1719
|
+
if (args.bodyFont !== void 0) state.bodyFont = args.bodyFont;
|
|
1720
|
+
if (args.monoFont !== void 0) state.monoFont = args.monoFont;
|
|
1721
|
+
if (args.headingWeight !== void 0) state.headingWeight = args.headingWeight;
|
|
1722
|
+
if (args.lineHeightOverrides !== void 0) state.lineHeightOverrides = args.lineHeightOverrides;
|
|
1723
|
+
if (args.letterSpacingOverrides !== void 0) state.letterSpacingOverrides = args.letterSpacingOverrides;
|
|
1724
|
+
if (state.scaleMode === "traditional") {
|
|
1725
|
+
const fill = (rec, base) => {
|
|
1726
|
+
const out = { ...base };
|
|
1727
|
+
for (const [k, v] of Object.entries(rec ?? {})) out[k] = v;
|
|
1728
|
+
return out;
|
|
1729
|
+
};
|
|
1730
|
+
const { DEFAULT_TRADITIONAL: DEFAULT_TRADITIONAL2, DEFAULT_TRADITIONAL_MOBILE: DEFAULT_TRADITIONAL_MOBILE2 } = await Promise.resolve().then(() => (init_scale(), scale_exports));
|
|
1731
|
+
state.traditionalAssignments = fill(args.traditionalAssignments, state.traditionalAssignments ?? DEFAULT_TRADITIONAL2);
|
|
1732
|
+
state.traditionalMobileAssignments = fill(args.traditionalMobileAssignments, state.traditionalMobileAssignments ?? DEFAULT_TRADITIONAL_MOBILE2);
|
|
1733
|
+
}
|
|
1734
|
+
const next = { ...segs, t: encodeState2(state) };
|
|
1735
|
+
const scale = buildScale(state);
|
|
1736
|
+
return textResult(`${header(next, "type")}
|
|
1737
|
+
|
|
1738
|
+
${typeSummary(state, scale)}`);
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1741
|
+
server2.registerTool(
|
|
1742
|
+
"generate_shape_tokens",
|
|
1743
|
+
{
|
|
1744
|
+
title: "Generate shape tokens",
|
|
1745
|
+
description: 'Generate shape tokens \u2014 radii, shadows, borders, focus rings \u2014 in one of four visual styles: "paper" (classic surfaces with layered shadows), "glass" (liquid glass), "neomorph" (soft neumorphism), "neobrutalism" (hard offset shadows). Returns a shareable standby.design/system URL and a summary.',
|
|
1746
|
+
inputSchema: {
|
|
1747
|
+
url: URL_PARAM,
|
|
1748
|
+
style: z.enum(["paper", "glass", "neomorph", "neobrutalism"]).optional().describe('Top-level visual style (default "paper").'),
|
|
1749
|
+
borderRadius: z.number().int().min(0).max(64).optional().describe("Base radius in px; scales to a xs\u2013xl radius set (default 8)."),
|
|
1750
|
+
shadowEnabled: z.boolean().optional(),
|
|
1751
|
+
shadowType: z.enum(["normal", "flat"]).optional().describe("Shadow rendering for paper style."),
|
|
1752
|
+
shadowStrength: z.number().min(0).max(3).optional().describe("Shadow alpha multiplier (default 1.0)."),
|
|
1753
|
+
shadowBlurScale: z.number().min(0).max(3).optional().describe("Shadow blur multiplier (default 1.0)."),
|
|
1754
|
+
shadowScale: z.number().min(1).max(2).optional().describe("Elevation level spread ratio (default 1.272)."),
|
|
1755
|
+
shadowColorHex: z.string().optional().describe('Custom shadow color as hex, or "auto" (default).'),
|
|
1756
|
+
shadowOffsetX: z.number().int().min(-16).max(16).optional().describe("Brutalist shadow X offset in px (default 2)."),
|
|
1757
|
+
shadowOffsetY: z.number().int().min(-16).max(16).optional().describe("Brutalist shadow Y offset in px (default 4)."),
|
|
1758
|
+
brutalistVariant: z.enum(["outlined", "solid"]).optional(),
|
|
1759
|
+
borderEnabled: z.boolean().optional(),
|
|
1760
|
+
borderWidth: z.number().min(0).max(10).optional().describe("Border width in px (default 1)."),
|
|
1761
|
+
borderColorHex: z.string().optional().describe('Custom border color as hex, or "auto" (default).'),
|
|
1762
|
+
glassDepth: z.number().min(-2).max(5).optional().describe("Glass displacement intensity (default 0.2)."),
|
|
1763
|
+
glassBlur: z.number().min(0).max(20).optional().describe("Glass backdrop blur (default 1.0)."),
|
|
1764
|
+
glassDispersion: z.number().min(0).max(2).optional().describe("Chromatic aberration intensity (default 0.4)."),
|
|
1765
|
+
ringWidth: z.number().int().min(0).max(8).optional().describe("Focus ring width in px (default 2)."),
|
|
1766
|
+
ringOffset: z.number().int().min(0).max(8).optional().describe("Focus ring offset in px (default 2)."),
|
|
1767
|
+
ringColorHex: z.string().optional().describe('Custom focus ring color as hex, or "auto" (default).'),
|
|
1768
|
+
separationMode: z.enum(["shadow", "border", "contrast", "gap", "mixed"]).optional().describe("How surfaces separate from the background.")
|
|
1769
|
+
},
|
|
1770
|
+
annotations: READ_ONLY
|
|
1771
|
+
},
|
|
1772
|
+
async (args) => {
|
|
1773
|
+
const segs = parseInput(args.url);
|
|
1774
|
+
const state = shapeStateFrom(segs);
|
|
1775
|
+
if (args.style !== void 0) state.shapeStyle = args.style;
|
|
1776
|
+
if (args.borderRadius !== void 0) state.borderRadius = args.borderRadius;
|
|
1777
|
+
if (args.shadowEnabled !== void 0) state.shadowEnabled = args.shadowEnabled;
|
|
1778
|
+
if (args.shadowType !== void 0) state.shadowType = args.shadowType;
|
|
1779
|
+
if (args.shadowStrength !== void 0) state.shadowStrength = args.shadowStrength;
|
|
1780
|
+
if (args.shadowBlurScale !== void 0) state.shadowBlurScale = args.shadowBlurScale;
|
|
1781
|
+
if (args.shadowScale !== void 0) state.shadowScale = args.shadowScale;
|
|
1782
|
+
if (args.shadowOffsetX !== void 0) state.shadowOffsetX = args.shadowOffsetX;
|
|
1783
|
+
if (args.shadowOffsetY !== void 0) state.shadowOffsetY = args.shadowOffsetY;
|
|
1784
|
+
if (args.brutalistVariant !== void 0) state.brutalistVariant = args.brutalistVariant;
|
|
1785
|
+
if (args.borderEnabled !== void 0) state.borderEnabled = args.borderEnabled;
|
|
1786
|
+
if (args.borderWidth !== void 0) state.borderWidth = args.borderWidth;
|
|
1787
|
+
if (args.glassDepth !== void 0) state.glassDepth = args.glassDepth;
|
|
1788
|
+
if (args.glassBlur !== void 0) state.glassBlur = args.glassBlur;
|
|
1789
|
+
if (args.glassDispersion !== void 0) state.glassDispersion = args.glassDispersion;
|
|
1790
|
+
if (args.ringWidth !== void 0) state.ringWidth = args.ringWidth;
|
|
1791
|
+
if (args.ringOffset !== void 0) state.ringOffset = args.ringOffset;
|
|
1792
|
+
if (args.separationMode !== void 0) state.separationMode = args.separationMode;
|
|
1793
|
+
for (const [argKey, modeKey, colorKey] of [
|
|
1794
|
+
["shadowColorHex", "shadowColorMode", "shadowCustomColor"],
|
|
1795
|
+
["borderColorHex", "borderColorMode", "borderCustomColor"],
|
|
1796
|
+
["ringColorHex", "ringColorMode", "ringCustomColor"]
|
|
1797
|
+
]) {
|
|
1798
|
+
const value = args[argKey];
|
|
1799
|
+
if (value === void 0) continue;
|
|
1800
|
+
if (value.toLowerCase() === "auto") {
|
|
1801
|
+
state[modeKey] = "auto";
|
|
1802
|
+
} else {
|
|
1803
|
+
const hex = normalizeHex(value);
|
|
1804
|
+
if (!hex) return errorResult(`Invalid ${argKey} "${value}" \u2014 expected a 6-digit hex color or "auto".`);
|
|
1805
|
+
state[modeKey] = "custom";
|
|
1806
|
+
state[colorKey] = hex;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
const next = { ...segs, s: encodeState3(state) };
|
|
1810
|
+
return textResult(`${header(next, "shape")}
|
|
1811
|
+
|
|
1812
|
+
${shapeSummary(state)}`);
|
|
1813
|
+
}
|
|
1814
|
+
);
|
|
1815
|
+
const SET_IDS = ICON_SETS.map((s) => s.id);
|
|
1816
|
+
server2.registerTool(
|
|
1817
|
+
"generate_icon_tokens",
|
|
1818
|
+
{
|
|
1819
|
+
title: "Generate icon tokens",
|
|
1820
|
+
description: `Pick an icon set and generate icon sizing tokens (xs\u20132xl + stroke width). Either select a specific set or state style preferences and get a recommendation. Available sets: ${SET_IDS.join(", ")}. Returns a shareable standby.design/system URL and a summary.`,
|
|
1821
|
+
inputSchema: {
|
|
1822
|
+
url: URL_PARAM,
|
|
1823
|
+
set: z.string().optional().describe(`Icon set variant id to select explicitly, e.g. "lucide-outlined". One of: ${SET_IDS.join(", ")}. Pass "auto" to clear the selection and use the recommendation instead.`),
|
|
1824
|
+
style: z.enum(["outlined", "filled", "duotone", "auto"]).optional().describe("Preferred icon style for the recommendation."),
|
|
1825
|
+
weight: z.enum(["thin", "regular", "bold", "auto"]).optional().describe("Preferred stroke weight."),
|
|
1826
|
+
corners: z.enum(["sharp", "rounded", "auto"]).optional().describe("Preferred corner style (sharp = corporate/precise, rounded = friendly)."),
|
|
1827
|
+
baseSize: z.number().min(0.5).max(3).optional().describe("Base icon size (md) in rem (default 1.25)."),
|
|
1828
|
+
scale: z.number().min(1).max(2).optional().describe("Size scale ratio (default 1.272)."),
|
|
1829
|
+
snapTo4px: z.boolean().optional().describe("Round sizes to the 4px grid (default true).")
|
|
1830
|
+
},
|
|
1831
|
+
annotations: READ_ONLY
|
|
1832
|
+
},
|
|
1833
|
+
async (args) => {
|
|
1834
|
+
const segs = parseInput(args.url);
|
|
1835
|
+
const state = symbolStateFrom(segs) ?? { ...DEFAULT_SYMBOL_STATE };
|
|
1836
|
+
if (args.set !== void 0) {
|
|
1837
|
+
if (args.set.toLowerCase() === "auto") {
|
|
1838
|
+
state.selectedSet = null;
|
|
1839
|
+
} else if (!SET_IDS.includes(args.set)) {
|
|
1840
|
+
return errorResult(`Unknown icon set "${args.set}". Valid ids: ${SET_IDS.join(", ")} \u2014 or "auto" for the recommendation.`);
|
|
1841
|
+
} else {
|
|
1842
|
+
state.selectedSet = args.set;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (args.style !== void 0) state.preferredStyle = args.style;
|
|
1846
|
+
if (args.weight !== void 0) state.preferredWeight = args.weight;
|
|
1847
|
+
if (args.corners !== void 0) state.preferredCorners = args.corners;
|
|
1848
|
+
if (args.baseSize !== void 0) state.iconBaseSize = args.baseSize;
|
|
1849
|
+
if (args.scale !== void 0) state.iconScale = args.scale;
|
|
1850
|
+
if (args.snapTo4px !== void 0) state.snapTo4px = args.snapTo4px;
|
|
1851
|
+
const next = { ...segs, y: encodeState4(state) };
|
|
1852
|
+
return textResult(`${header(next, "symbol")}
|
|
1853
|
+
|
|
1854
|
+
${symbolSummary(state)}`);
|
|
1855
|
+
}
|
|
1856
|
+
);
|
|
1857
|
+
server2.registerTool(
|
|
1858
|
+
"generate_space_tokens",
|
|
1859
|
+
{
|
|
1860
|
+
title: "Generate spacing & layout tokens",
|
|
1861
|
+
description: "Generate spacing tokens (3xs\u20133xl), breakpoints, container widths, prose measure, and aspect ratios. Returns a shareable standby.design/system URL and a summary.",
|
|
1862
|
+
inputSchema: {
|
|
1863
|
+
url: URL_PARAM,
|
|
1864
|
+
mode: z.enum(["harmonic", "geometric"]).optional().describe('"harmonic": multiples of the base (default). "geometric": compound ratio steps.'),
|
|
1865
|
+
baseRem: z.number().min(0.25).max(4).optional().describe("Base spacing in rem (default 1.0)."),
|
|
1866
|
+
ratio: z.number().min(1).max(2).optional().describe("Ratio for geometric mode (default 1.272)."),
|
|
1867
|
+
multiplier: z.number().min(0.25).max(4).optional().describe("Uniform scale factor on all tokens (default 1.0)."),
|
|
1868
|
+
snap: z.boolean().optional().describe("Snap token values to a sensible grid (default true)."),
|
|
1869
|
+
breakpoints: z.array(z.object({
|
|
1870
|
+
name: z.string().min(1),
|
|
1871
|
+
minPx: z.number().int().min(0)
|
|
1872
|
+
})).optional().describe('Replace the breakpoint list, e.g. [{"name":"sm","minPx":640},...]. Defaults: sm 640, md 768, lg 1024, xl 1280, 2xl 1536.'),
|
|
1873
|
+
fluidMinVw: z.number().int().min(200).max(1e3).optional().describe("Lower fluid viewport anchor in px (default 375)."),
|
|
1874
|
+
fluidMaxVw: z.number().int().min(1e3).max(3840).optional().describe("Upper fluid viewport anchor in px (default 1920)."),
|
|
1875
|
+
containers: z.array(z.object({
|
|
1876
|
+
name: z.string().min(1),
|
|
1877
|
+
maxPx: z.number().int().min(0)
|
|
1878
|
+
})).optional().describe("Replace the container list. Defaults: prose 680, narrow 880, default 1200, wide 1440."),
|
|
1879
|
+
proseMaxCh: z.number().int().min(20).max(120).optional().describe("Prose measure in ch (default 65)."),
|
|
1880
|
+
aspectRatios: z.array(z.object({
|
|
1881
|
+
name: z.string().min(1),
|
|
1882
|
+
w: z.number().positive(),
|
|
1883
|
+
h: z.number().positive()
|
|
1884
|
+
})).optional().describe('Replace the aspect ratio list, e.g. [{"name":"video","w":16,"h":9}].'),
|
|
1885
|
+
includeReciprocals: z.boolean().optional().describe("Also export flipped (portrait) variants of each ratio (default true).")
|
|
1886
|
+
},
|
|
1887
|
+
annotations: READ_ONLY
|
|
1888
|
+
},
|
|
1889
|
+
async (args) => {
|
|
1890
|
+
const segs = parseInput(args.url);
|
|
1891
|
+
const state = spaceStateFrom(segs);
|
|
1892
|
+
if (args.mode !== void 0) state.spacingMode = args.mode;
|
|
1893
|
+
if (args.baseRem !== void 0) state.spacingBaseRem = args.baseRem;
|
|
1894
|
+
if (args.ratio !== void 0) state.spacingRatio = args.ratio;
|
|
1895
|
+
if (args.multiplier !== void 0) state.spacingMultiplier = args.multiplier;
|
|
1896
|
+
if (args.snap !== void 0) state.spacingSnap = args.snap;
|
|
1897
|
+
if (args.breakpoints !== void 0) state.breakpoints = args.breakpoints;
|
|
1898
|
+
if (args.fluidMinVw !== void 0) state.fluidMinVw = args.fluidMinVw;
|
|
1899
|
+
if (args.fluidMaxVw !== void 0) state.fluidMaxVw = args.fluidMaxVw;
|
|
1900
|
+
if (args.containers !== void 0) state.containers = args.containers;
|
|
1901
|
+
if (args.proseMaxCh !== void 0) state.proseMaxCh = args.proseMaxCh;
|
|
1902
|
+
if (args.aspectRatios !== void 0) state.aspectRatios = args.aspectRatios;
|
|
1903
|
+
if (args.includeReciprocals !== void 0) state.aspectIncludeReciprocals = args.includeReciprocals;
|
|
1904
|
+
const next = { ...segs, p: encodeState5(state) };
|
|
1905
|
+
const spacing = buildSpacing(state);
|
|
1906
|
+
return textResult(`${header(next, "space")}
|
|
1907
|
+
|
|
1908
|
+
${spaceSummary(state, spacing)}`);
|
|
1909
|
+
}
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// src/tools-system.ts
|
|
1914
|
+
import { z as z2 } from "zod";
|
|
1915
|
+
|
|
1916
|
+
// ../../system-react/src/lib/color-code-export.ts
|
|
1917
|
+
function generateSeedComment(state, effectiveBgHex, effectiveErrorHex) {
|
|
1918
|
+
const title = state.themeName && state.themeName !== "Standby.Design" ? `${state.themeName} \u2014 ` : "";
|
|
1919
|
+
const accents = state.extraAccents.map(
|
|
1920
|
+
(a) => `${a.name} ${a.autoMatch ? `auto (hue ${Math.round(a.autoHue)}\xB0)` : a.hex}${a.pin ? ", pinned" : ""}`
|
|
1921
|
+
).join(" \xB7 ");
|
|
1922
|
+
const lines = [
|
|
1923
|
+
`/*`,
|
|
1924
|
+
` * ${title}generated with standby.design`,
|
|
1925
|
+
` * Seed: brand ${state.brandHex}${state.brandPin ? " (pinned)" : ""}${state.brandInvert ? " (dark-inverted)" : ""} \xB7 mode ${state.currentMode} \xB7 text contrast ${state.fgContrastMode}`,
|
|
1926
|
+
` * Surface: ${state.bgAutoMatch ? "auto from brand" : state.bgColorHex} \u2192 ${effectiveBgHex} \xB7 chroma ${Math.round(state.chromaScale * 100)}%`,
|
|
1927
|
+
` * Error: ${state.errorAutoMatch ? "auto from brand hue" : state.errorColorHex} \u2192 ${effectiveErrorHex}${state.errorPin ? " (pinned)" : ""}${state.errorInvert ? " (dark-inverted)" : ""}`,
|
|
1928
|
+
...accents ? [` * Accents: ${accents}`] : [],
|
|
1929
|
+
` * Restore: https://standby.design/color#c=${encodeState(state)}`,
|
|
1930
|
+
` */`
|
|
1931
|
+
];
|
|
1932
|
+
return lines.join("\n") + "\n\n";
|
|
1933
|
+
}
|
|
1934
|
+
function palMap(arr) {
|
|
1935
|
+
return Object.fromEntries(arr.map((r) => [r.step, r]));
|
|
1936
|
+
}
|
|
1937
|
+
function fgStep(bgHex, pMap, ls, ds, fgMode) {
|
|
1938
|
+
if (!bgHex) return ls;
|
|
1939
|
+
const lightHex = pMap[ls]?.hex;
|
|
1940
|
+
const darkHex = pMap[ds]?.hex;
|
|
1941
|
+
const lightCR = lightHex ? contrastRatio(lightHex, bgHex) : 0;
|
|
1942
|
+
const darkCR = darkHex ? contrastRatio(darkHex, bgHex) : 0;
|
|
1943
|
+
if (fgMode === "preferDark") {
|
|
1944
|
+
if (darkCR >= 4.5) return ds;
|
|
1945
|
+
if (lightCR >= 4.5) return ls;
|
|
1946
|
+
return ds;
|
|
1947
|
+
}
|
|
1948
|
+
if (fgMode === "preferLight") {
|
|
1949
|
+
if (lightCR >= 4.5) return ls;
|
|
1950
|
+
if (darkCR >= 4.5) return ds;
|
|
1951
|
+
return ls;
|
|
1952
|
+
}
|
|
1953
|
+
return lightCR >= darkCR ? ls : ds;
|
|
1954
|
+
}
|
|
1955
|
+
function fgDirect(bgHex, pMap, ls, ds, pfx, fgMode) {
|
|
1956
|
+
const step = fgStep(bgHex, pMap, ls, ds, fgMode);
|
|
1957
|
+
return pMap[step]?.css || `var(--color-${pfx}-${step})`;
|
|
1958
|
+
}
|
|
1959
|
+
function hexToCss(hex) {
|
|
1960
|
+
const [L, C, H] = hexToOklch(hex);
|
|
1961
|
+
return `oklch(${L.toFixed(4)} ${C.toFixed(4)} ${H.toFixed(2)})`;
|
|
1962
|
+
}
|
|
1963
|
+
function fmtSec(palette, prefix, mode) {
|
|
1964
|
+
const gs = [
|
|
1965
|
+
{ l: "Light Surfaces", f: (r) => r.step <= 100 },
|
|
1966
|
+
{ l: "Core", f: (r) => r.step >= 200 && r.step <= 800 },
|
|
1967
|
+
{ l: "Dark Surfaces (normal)", f: (r) => r.step >= 825 && r.step <= 875 },
|
|
1968
|
+
{ l: "Dark Surfaces (high contrast)", f: (r) => r.step >= 900 }
|
|
1969
|
+
];
|
|
1970
|
+
let out = "";
|
|
1971
|
+
gs.forEach((g, i) => {
|
|
1972
|
+
if (i > 0) out += "\n";
|
|
1973
|
+
out += ` /* ${g.l} */
|
|
1974
|
+
`;
|
|
1975
|
+
palette.filter(g.f).forEach((r) => {
|
|
1976
|
+
const v = mode === "css" ? r.css : r.hex;
|
|
1977
|
+
out += ` --color-${prefix}-${r.step}: ${v};
|
|
1978
|
+
`;
|
|
1979
|
+
});
|
|
1980
|
+
});
|
|
1981
|
+
return out;
|
|
1982
|
+
}
|
|
1983
|
+
function buildBlock(sel, rows) {
|
|
1984
|
+
let o = `${sel} {
|
|
1985
|
+
`;
|
|
1986
|
+
rows.forEach(([name, prefix, step]) => {
|
|
1987
|
+
if (name === "#comment") o += ` /* ${step} */
|
|
1988
|
+
`;
|
|
1989
|
+
else if (prefix === null) o += ` /* ${step} */
|
|
1990
|
+
`;
|
|
1991
|
+
else if (prefix === "#direct") o += ` --${name}: ${step};
|
|
1992
|
+
`;
|
|
1993
|
+
else o += ` --${name}: var(--color-${prefix}-${step});
|
|
1994
|
+
`;
|
|
1995
|
+
});
|
|
1996
|
+
return o + `}
|
|
1997
|
+
`;
|
|
1998
|
+
}
|
|
1999
|
+
function generatePrimitivesOklch(brand, surface, error, errorSurface, neutralExtended, accentPalettes, chromaScale, customBgHex, themeName) {
|
|
2000
|
+
const pct = Math.round(chromaScale * 100);
|
|
2001
|
+
const bgLabel = customBgHex ? `${pct}% chroma \u2014 base ${customBgHex}` : `${pct}% chroma`;
|
|
2002
|
+
const header2 = themeName ? `/* ${themeName} \u2014 Primitive Tokens */` : `/* Primitive Tokens */`;
|
|
2003
|
+
const cmt = (t) => `
|
|
2004
|
+
/* ${t} */
|
|
2005
|
+
`;
|
|
2006
|
+
let o = `${header2}
|
|
2007
|
+
:root {
|
|
2008
|
+
`;
|
|
2009
|
+
o += cmt("Brand") + fmtSec(brand, "brand", "css");
|
|
2010
|
+
o += cmt(`Surface \u2014 ${bgLabel}`) + fmtSec(surface, "surface", "css");
|
|
2011
|
+
o += cmt("Error") + fmtSec(error, "error", "css");
|
|
2012
|
+
o += cmt(`Error Surface \u2014 ${pct}% chroma`) + fmtSec(errorSurface, "error-surface", "css");
|
|
2013
|
+
o += cmt("Neutral") + fmtSec(neutralExtended, "neutral", "css");
|
|
2014
|
+
accentPalettes.forEach((entry) => {
|
|
2015
|
+
o += cmt(entry.name) + fmtSec(entry.palette, entry.cssName, "css");
|
|
2016
|
+
o += cmt(`${entry.name} Surface \u2014 ${pct}% chroma`) + fmtSec(entry.slatedPalette, entry.cssName + "-surface", "css");
|
|
2017
|
+
});
|
|
2018
|
+
o += `}`;
|
|
2019
|
+
return o;
|
|
2020
|
+
}
|
|
2021
|
+
function generateSemantic(accentPalettes, brandPal, errPal, errSurfPal, surfacePal, brandPin, pinnedBrandHex, brandInvert, errorPin, pinnedErrorHex, errorInvert, fgMode, themeName) {
|
|
2022
|
+
const brandMap = palMap(brandPal);
|
|
2023
|
+
const errMap = palMap(errPal);
|
|
2024
|
+
const errSurfMap = palMap(errSurfPal);
|
|
2025
|
+
const surfMap = palMap(surfacePal);
|
|
2026
|
+
const bPin = brandPin;
|
|
2027
|
+
const bHex = pinnedBrandHex;
|
|
2028
|
+
const ePin = errorPin;
|
|
2029
|
+
const eHex = pinnedErrorHex;
|
|
2030
|
+
const bCss = bPin && bHex ? hexToCss(bHex) : null;
|
|
2031
|
+
const bFgCss = bPin && bHex ? fgDirect(bHex, brandMap, 50, 975, "brand", fgMode) : null;
|
|
2032
|
+
const bInvHex = bPin && bHex && brandInvert ? invertHex(bHex) : null;
|
|
2033
|
+
const bInvCss = bInvHex ? hexToCss(bInvHex) : null;
|
|
2034
|
+
const bInvFgCss = bInvHex ? fgDirect(bInvHex, brandMap, 50, 975, "brand", fgMode) : null;
|
|
2035
|
+
const primaryLight = bPin && bHex ? ["primary", "#direct", bCss] : ["primary", "brand", 600];
|
|
2036
|
+
const primaryFgLight = bPin && bHex ? ["primary-foreground", "#direct", bFgCss] : ["primary-foreground", "brand", fgStep(brandMap[600]?.hex, brandMap, 50, 975, fgMode)];
|
|
2037
|
+
const primaryDark = bPin && bHex ? bInvCss ? ["primary", "#direct", bInvCss] : ["primary", "#direct", bCss] : ["primary", "brand", 400];
|
|
2038
|
+
const primaryFgDark = bPin && bHex ? bInvFgCss ? ["primary-foreground", "#direct", bInvFgCss] : ["primary-foreground", "#direct", bFgCss] : ["primary-foreground", "brand", fgStep(brandMap[400]?.hex, brandMap, 50, 975, fgMode)];
|
|
2039
|
+
const sbPrimLight = bPin && bHex ? ["sidebar-primary", "#direct", bCss] : ["sidebar-primary", "brand", 600];
|
|
2040
|
+
const sbPrimFgLight = bPin && bHex ? ["sidebar-primary-foreground", "#direct", bFgCss] : ["sidebar-primary-foreground", "brand", fgStep(brandMap[600]?.hex, brandMap, 50, 975, fgMode)];
|
|
2041
|
+
const sbPrimDark = bPin && bHex ? bInvCss ? ["sidebar-primary", "#direct", bInvCss] : ["sidebar-primary", "#direct", bCss] : ["sidebar-primary", "brand", 400];
|
|
2042
|
+
const sbPrimFgDark = bPin && bHex ? bInvFgCss ? ["sidebar-primary-foreground", "#direct", bInvFgCss] : ["sidebar-primary-foreground", "#direct", bFgCss] : ["sidebar-primary-foreground", "brand", fgStep(brandMap[400]?.hex, brandMap, 50, 975, fgMode)];
|
|
2043
|
+
const brandContrastWarnLight = [];
|
|
2044
|
+
const brandContrastWarnDark = [];
|
|
2045
|
+
if (bPin && bHex) {
|
|
2046
|
+
const lightBg = surfMap[50]?.hex;
|
|
2047
|
+
const darkBg = surfMap[875]?.hex;
|
|
2048
|
+
const darkCheckHex = bInvHex || bHex;
|
|
2049
|
+
const lightFail = lightBg ? contrastRatio(bHex, lightBg) < 4.5 : false;
|
|
2050
|
+
const darkFail = darkBg ? contrastRatio(darkCheckHex, darkBg) < 4.5 : false;
|
|
2051
|
+
if (lightFail) {
|
|
2052
|
+
brandContrastWarnLight.push([
|
|
2053
|
+
"#comment",
|
|
2054
|
+
null,
|
|
2055
|
+
`\u26A0 Pinned primary has low contrast on light surfaces \u2014 do not use as text color. Use --foreground for text on light backgrounds.`
|
|
2056
|
+
]);
|
|
2057
|
+
}
|
|
2058
|
+
if (darkFail) {
|
|
2059
|
+
brandContrastWarnDark.push([
|
|
2060
|
+
"#comment",
|
|
2061
|
+
null,
|
|
2062
|
+
`\u26A0 Pinned primary has low contrast on dark surfaces \u2014 do not use as text color. Use --foreground for text on dark backgrounds.`
|
|
2063
|
+
]);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
const eCss = ePin && eHex ? hexToCss(eHex) : null;
|
|
2067
|
+
const eFgCss = ePin && eHex ? fgDirect(eHex, errSurfMap, 100, 900, "error-surface", fgMode) : null;
|
|
2068
|
+
const eInvHex = ePin && eHex && errorInvert ? invertHex(eHex) : null;
|
|
2069
|
+
const eInvCss = eInvHex ? hexToCss(eInvHex) : null;
|
|
2070
|
+
const eInvFgCss = eInvHex ? fgDirect(eInvHex, errSurfMap, 100, 900, "error-surface", fgMode) : null;
|
|
2071
|
+
const destLight = ePin && eHex ? ["destructive", "#direct", eCss] : ["destructive", "error", 600];
|
|
2072
|
+
const destFgLight = ePin && eHex ? ["destructive-foreground", "#direct", eFgCss] : ["destructive-foreground", "error-surface", fgStep(errMap[600]?.hex, errSurfMap, 100, 900, fgMode)];
|
|
2073
|
+
const destDark = ePin && eHex ? eInvCss ? ["destructive", "#direct", eInvCss] : ["destructive", "#direct", eCss] : ["destructive", "error", 400];
|
|
2074
|
+
const destFgDark = ePin && eHex ? eInvFgCss ? ["destructive-foreground", "#direct", eInvFgCss] : ["destructive-foreground", "#direct", eFgCss] : ["destructive-foreground", "error-surface", fgStep(errMap[400]?.hex, errSurfMap, 100, 900, fgMode)];
|
|
2075
|
+
const root = buildBlock(":root", [
|
|
2076
|
+
[null, null, "Base"],
|
|
2077
|
+
["background", "surface", 50],
|
|
2078
|
+
["foreground", "surface", 975],
|
|
2079
|
+
[null, null, "Card"],
|
|
2080
|
+
["card", "surface", 25],
|
|
2081
|
+
["card-foreground", "surface", 975],
|
|
2082
|
+
[null, null, "Popover"],
|
|
2083
|
+
["popover", "surface", 25],
|
|
2084
|
+
["popover-foreground", "surface", 975],
|
|
2085
|
+
[null, null, "Primary"],
|
|
2086
|
+
primaryLight,
|
|
2087
|
+
primaryFgLight,
|
|
2088
|
+
...brandContrastWarnLight,
|
|
2089
|
+
["primary-subtle", "brand", 100],
|
|
2090
|
+
["primary-subtle-foreground", "brand", 950],
|
|
2091
|
+
[null, null, "Secondary \u2014 softened brand"],
|
|
2092
|
+
["secondary", "brand", 200],
|
|
2093
|
+
["secondary-foreground", "brand", fgStep(brandMap[200]?.hex, brandMap, 100, 900, fgMode)],
|
|
2094
|
+
[null, null, "Muted"],
|
|
2095
|
+
["muted", "surface", 75],
|
|
2096
|
+
["muted-foreground", "surface", 700],
|
|
2097
|
+
[null, null, "Accent"],
|
|
2098
|
+
["accent", "brand", 100],
|
|
2099
|
+
["accent-foreground", "brand", fgStep(brandMap[100]?.hex, brandMap, 50, 950, fgMode)],
|
|
2100
|
+
[null, null, "Destructive"],
|
|
2101
|
+
destLight,
|
|
2102
|
+
destFgLight,
|
|
2103
|
+
["destructive-subtle", "error", 100],
|
|
2104
|
+
["destructive-subtle-foreground", "error", 950],
|
|
2105
|
+
["destructive-border", "error-surface", 300],
|
|
2106
|
+
[null, null, "Border / Input / Ring"],
|
|
2107
|
+
["border", "surface", 300],
|
|
2108
|
+
["border-muted", "surface", 200],
|
|
2109
|
+
["input", "surface", 300],
|
|
2110
|
+
["ring", "surface", 400],
|
|
2111
|
+
[null, null, "Sidebar"],
|
|
2112
|
+
["sidebar", "surface", 25],
|
|
2113
|
+
["sidebar-foreground", "surface", 975],
|
|
2114
|
+
sbPrimLight,
|
|
2115
|
+
sbPrimFgLight,
|
|
2116
|
+
["sidebar-accent", "brand", 100],
|
|
2117
|
+
["sidebar-accent-foreground", "brand", fgStep(brandMap[100]?.hex, brandMap, 50, 950, fgMode)],
|
|
2118
|
+
["sidebar-border", "surface", 300],
|
|
2119
|
+
["sidebar-ring", "surface", 400]
|
|
2120
|
+
]);
|
|
2121
|
+
const dark = buildBlock(".dark", [
|
|
2122
|
+
[null, null, "Base"],
|
|
2123
|
+
["background", "surface", 875],
|
|
2124
|
+
["foreground", "surface", 25],
|
|
2125
|
+
[null, null, "Card"],
|
|
2126
|
+
["card", "surface", 825],
|
|
2127
|
+
["card-foreground", "surface", 25],
|
|
2128
|
+
[null, null, "Popover"],
|
|
2129
|
+
["popover", "surface", 800],
|
|
2130
|
+
["popover-foreground", "surface", 25],
|
|
2131
|
+
[null, null, "Primary"],
|
|
2132
|
+
primaryDark,
|
|
2133
|
+
primaryFgDark,
|
|
2134
|
+
...brandContrastWarnDark,
|
|
2135
|
+
["primary-subtle", "brand", 800],
|
|
2136
|
+
["primary-subtle-foreground", "brand", 50],
|
|
2137
|
+
[null, null, "Secondary \u2014 softened brand"],
|
|
2138
|
+
["secondary", "brand", 800],
|
|
2139
|
+
["secondary-foreground", "brand", fgStep(brandMap[800]?.hex, brandMap, 100, 900, fgMode)],
|
|
2140
|
+
[null, null, "Muted"],
|
|
2141
|
+
["muted", "surface", 850],
|
|
2142
|
+
["muted-foreground", "surface", 300],
|
|
2143
|
+
[null, null, "Accent"],
|
|
2144
|
+
["accent", "brand", 800],
|
|
2145
|
+
["accent-foreground", "brand", fgStep(brandMap[800]?.hex, brandMap, 50, 950, fgMode)],
|
|
2146
|
+
[null, null, "Destructive"],
|
|
2147
|
+
destDark,
|
|
2148
|
+
destFgDark,
|
|
2149
|
+
["destructive-subtle", "error", 800],
|
|
2150
|
+
["destructive-subtle-foreground", "error", 50],
|
|
2151
|
+
["destructive-border", "error-surface", 700],
|
|
2152
|
+
[null, null, "Border / Input / Ring"],
|
|
2153
|
+
["border", "surface", 600],
|
|
2154
|
+
["border-muted", "surface", 700],
|
|
2155
|
+
["input", "surface", 700],
|
|
2156
|
+
["ring", "surface", 500],
|
|
2157
|
+
[null, null, "Sidebar"],
|
|
2158
|
+
["sidebar", "surface", 875],
|
|
2159
|
+
["sidebar-foreground", "surface", 25],
|
|
2160
|
+
sbPrimDark,
|
|
2161
|
+
sbPrimFgDark,
|
|
2162
|
+
["sidebar-accent", "brand", 800],
|
|
2163
|
+
["sidebar-accent-foreground", "brand", fgStep(brandMap[800]?.hex, brandMap, 50, 950, fgMode)],
|
|
2164
|
+
["sidebar-border", "surface", 600],
|
|
2165
|
+
["sidebar-ring", "surface", 500]
|
|
2166
|
+
]);
|
|
2167
|
+
let accentBlocks = "";
|
|
2168
|
+
accentPalettes.forEach((entry) => {
|
|
2169
|
+
const n = entry.cssName;
|
|
2170
|
+
const aPin = !!entry.pin;
|
|
2171
|
+
const aInv = !!entry.invert;
|
|
2172
|
+
const aHex = entry.hex;
|
|
2173
|
+
const aCss = aPin ? hexToCss(aHex) : null;
|
|
2174
|
+
const aMap = palMap(entry.palette || []);
|
|
2175
|
+
const aFgCss = aPin ? fgDirect(aHex, aMap, 50, 975, n, fgMode) : null;
|
|
2176
|
+
const aInvHex = aPin && aInv ? invertHex(aHex) : null;
|
|
2177
|
+
const aInvCss = aInvHex ? hexToCss(aInvHex) : null;
|
|
2178
|
+
const aInvFgCss = aInvHex ? fgDirect(aInvHex, aMap, 50, 975, n, fgMode) : null;
|
|
2179
|
+
const aLight = aPin ? [n, "#direct", aCss] : [n, n, 600];
|
|
2180
|
+
const aFgL = aPin ? [`${n}-foreground`, "#direct", aFgCss] : [`${n}-foreground`, n, fgStep(aMap[600]?.hex, aMap, 50, 975, fgMode)];
|
|
2181
|
+
const aDark = aPin ? aInvCss ? [n, "#direct", aInvCss] : [n, "#direct", aCss] : [n, n, 400];
|
|
2182
|
+
const aFgD = aPin ? aInvFgCss ? [`${n}-foreground`, "#direct", aInvFgCss] : [`${n}-foreground`, "#direct", aFgCss] : [`${n}-foreground`, n, fgStep(aMap[400]?.hex, aMap, 50, 975, fgMode)];
|
|
2183
|
+
const accentRoot = buildBlock(":root", [
|
|
2184
|
+
[null, null, `${entry.name} \u2014 light`],
|
|
2185
|
+
aLight,
|
|
2186
|
+
aFgL,
|
|
2187
|
+
[null, null, "Background / Card / Popover"],
|
|
2188
|
+
[`${n}-background`, `${n}-surface`, 50],
|
|
2189
|
+
[`${n}-background-foreground`, `${n}-surface`, 975],
|
|
2190
|
+
[`${n}-card`, `${n}-surface`, 25],
|
|
2191
|
+
[`${n}-card-foreground`, `${n}-surface`, 975],
|
|
2192
|
+
[`${n}-popover`, `${n}-surface`, 25],
|
|
2193
|
+
[`${n}-popover-foreground`, `${n}-surface`, 975],
|
|
2194
|
+
[null, null, "Secondary"],
|
|
2195
|
+
[`${n}-secondary`, n, 200],
|
|
2196
|
+
[`${n}-secondary-foreground`, n, 900],
|
|
2197
|
+
[null, null, "Muted / Subtle / Accent"],
|
|
2198
|
+
[`${n}-muted`, `${n}-surface`, 75],
|
|
2199
|
+
[`${n}-muted-foreground`, `${n}-surface`, 700],
|
|
2200
|
+
[`${n}-accent`, n, 100],
|
|
2201
|
+
[`${n}-accent-foreground`, n, 950],
|
|
2202
|
+
[`${n}-subtle`, n, 100],
|
|
2203
|
+
[`${n}-subtle-foreground`, n, 950],
|
|
2204
|
+
[null, null, "Border / Input / Ring"],
|
|
2205
|
+
[`${n}-border`, `${n}-surface`, 300],
|
|
2206
|
+
[`${n}-border-muted`, `${n}-surface`, 200],
|
|
2207
|
+
[`${n}-input`, `${n}-surface`, 300],
|
|
2208
|
+
[`${n}-ring`, `${n}-surface`, 400]
|
|
2209
|
+
]);
|
|
2210
|
+
const accentDark = buildBlock(".dark", [
|
|
2211
|
+
[null, null, `${entry.name} \u2014 dark`],
|
|
2212
|
+
aDark,
|
|
2213
|
+
aFgD,
|
|
2214
|
+
[null, null, "Background / Card / Popover"],
|
|
2215
|
+
[`${n}-background`, `${n}-surface`, 875],
|
|
2216
|
+
[`${n}-background-foreground`, `${n}-surface`, 25],
|
|
2217
|
+
[`${n}-card`, `${n}-surface`, 825],
|
|
2218
|
+
[`${n}-card-foreground`, `${n}-surface`, 25],
|
|
2219
|
+
[`${n}-popover`, `${n}-surface`, 800],
|
|
2220
|
+
[`${n}-popover-foreground`, `${n}-surface`, 25],
|
|
2221
|
+
[null, null, "Secondary"],
|
|
2222
|
+
[`${n}-secondary`, n, 800],
|
|
2223
|
+
[`${n}-secondary-foreground`, n, 100],
|
|
2224
|
+
[null, null, "Muted / Subtle / Accent"],
|
|
2225
|
+
[`${n}-muted`, `${n}-surface`, 850],
|
|
2226
|
+
[`${n}-muted-foreground`, `${n}-surface`, 300],
|
|
2227
|
+
[`${n}-accent`, n, 800],
|
|
2228
|
+
[`${n}-accent-foreground`, n, 50],
|
|
2229
|
+
[`${n}-subtle`, n, 800],
|
|
2230
|
+
[`${n}-subtle-foreground`, n, 50],
|
|
2231
|
+
[null, null, "Border / Input / Ring"],
|
|
2232
|
+
[`${n}-border`, `${n}-surface`, 700],
|
|
2233
|
+
[`${n}-border-muted`, `${n}-surface`, 700],
|
|
2234
|
+
[`${n}-input`, `${n}-surface`, 700],
|
|
2235
|
+
[`${n}-ring`, `${n}-surface`, 500]
|
|
2236
|
+
]);
|
|
2237
|
+
accentBlocks += `
|
|
2238
|
+
/* ${entry.name} Accent \u2014 semantic tokens */
|
|
2239
|
+
` + accentRoot + "\n" + accentDark;
|
|
2240
|
+
});
|
|
2241
|
+
const header2 = themeName ? `/* ${themeName} \u2014 Semantic Tokens (shadcn/ui compatible) */
|
|
2242
|
+
` : `/* Semantic Tokens \u2014 shadcn/ui compatible */
|
|
2243
|
+
`;
|
|
2244
|
+
return header2 + root + "\n" + dark + accentBlocks;
|
|
2245
|
+
}
|
|
2246
|
+
function generateLlmBriefing(brandHex, effectiveBgHex, effectiveErrorHex, accentPalettes, chromaScale, mode, brandPin, errorPin, themeName, fgContrastMode) {
|
|
2247
|
+
const pct = Math.round(chromaScale * 100);
|
|
2248
|
+
const title = themeName || "Untitled Theme";
|
|
2249
|
+
const fgLabel = fgContrastMode === "best" ? "Best Contrast (auto)" : fgContrastMode === "preferLight" ? "Prefer Light" : "Prefer Dark";
|
|
2250
|
+
const colorRows = [
|
|
2251
|
+
`| Brand | \`${brandHex}\` | \`--color-brand-*\` | Primary interactive color${brandPin ? " (pinned)" : ""} |`,
|
|
2252
|
+
`| Surface | \`${effectiveBgHex}\` | \`--color-surface-*\` | Background/container tint at ${pct}% chroma |`,
|
|
2253
|
+
`| Error | \`${effectiveErrorHex}\` | \`--color-error-*\` | Destructive/alert color${errorPin ? " (pinned)" : ""} |`,
|
|
2254
|
+
`| Neutral | derived | \`--color-neutral-*\` | 0% chroma variant for high-contrast surfaces |`,
|
|
2255
|
+
...accentPalettes.map(
|
|
2256
|
+
(a) => `| ${a.name} | \`${a.hex}\` | \`--color-${a.cssName}-*\` | Additional accent${a.pin ? " (pinned)" : ""} |`
|
|
2257
|
+
)
|
|
2258
|
+
].join("\n");
|
|
2259
|
+
const pinnedNote = brandPin || errorPin || accentPalettes.some((a) => a.pin) ? `
|
|
2260
|
+
> **Pinned** means the exact input hex is used for primary/destructive button colors instead of the generated palette step (brand-600/400 or error-600/400).
|
|
2261
|
+
` : "";
|
|
2262
|
+
let pinnedContrastWarning = "";
|
|
2263
|
+
if (brandPin) {
|
|
2264
|
+
pinnedContrastWarning = `
|
|
2265
|
+
> \u26A0 **Contrast warning \u2014 pinned primary**: The pinned brand color (\`${brandHex}\`) may not have sufficient contrast (WCAG AA 4.5:1) against your surface backgrounds when used as a text color. **Do not use \`--primary\` for body text.** Use \`--foreground\` for general text. \`--primary-foreground\` is only for text on primary-colored backgrounds (e.g. buttons).
|
|
2266
|
+
`;
|
|
2267
|
+
}
|
|
2268
|
+
return `# ${title} \u2014 Theme Briefing
|
|
2269
|
+
|
|
2270
|
+
All colors are generated in the **OKLCH** color space (perceptually uniform). Gamut mapping to sRGB is handled automatically \u2014 you never need to worry about out-of-gamut values.
|
|
2271
|
+
|
|
2272
|
+
## Seed Colors
|
|
2273
|
+
|
|
2274
|
+
| Role | Hex | CSS prefix | Notes |
|
|
2275
|
+
|------|-----|------------|-------|
|
|
2276
|
+
${colorRows}
|
|
2277
|
+
${pinnedNote}${pinnedContrastWarning}
|
|
2278
|
+
## Semantic Token Mapping
|
|
2279
|
+
|
|
2280
|
+
| Token | Light | Dark | Usage |
|
|
2281
|
+
|-------|-------|------|-------|
|
|
2282
|
+
| \`--background\` | surface-50 | surface-875 | Page background |
|
|
2283
|
+
| \`--foreground\` | surface-975 | surface-25 | Primary text |
|
|
2284
|
+
| \`--card\` | surface-25 | surface-825 | Card backgrounds |
|
|
2285
|
+
| \`--popover\` | surface-25 | surface-800 | Popover/dropdown |
|
|
2286
|
+
| \`--primary\` | brand-600 | brand-400 | Primary buttons, links |
|
|
2287
|
+
| \`--primary-subtle\` | brand-100 | brand-800 | Tinted brand fills: callouts, selected items |
|
|
2288
|
+
| \`--secondary\` | brand-200 | brand-800 | Secondary buttons |
|
|
2289
|
+
| \`--muted\` | surface-75 | surface-850 | Muted backgrounds |
|
|
2290
|
+
| \`--accent\` | brand-100 | brand-800 | Interaction highlights (hover, selected) |
|
|
2291
|
+
| \`--destructive\` | error-600 | error-400 | Error/delete actions |
|
|
2292
|
+
| \`--destructive-subtle\` | error-100 | error-800 | Inline errors, alert backgrounds |
|
|
2293
|
+
| \`--destructive-border\` | error-surface-300 | error-surface-700 | Error borders |
|
|
2294
|
+
| \`--border\` | surface-300 | surface-600 | Default borders |
|
|
2295
|
+
| \`--border-muted\` | surface-200 | surface-700 | Subtle separators |
|
|
2296
|
+
| \`--input\` | surface-300 | surface-700 | Form-control borders (input, select, textarea) |
|
|
2297
|
+
| \`--ring\` | surface-400 | surface-500 | Focus rings |
|
|
2298
|
+
|
|
2299
|
+
### Sidebar
|
|
2300
|
+
|
|
2301
|
+
| Token | Light | Dark |
|
|
2302
|
+
|-------|-------|------|
|
|
2303
|
+
| \`--sidebar\` | surface-25 | surface-875 |
|
|
2304
|
+
| \`--sidebar-primary\` | brand-600 | brand-400 |
|
|
2305
|
+
| \`--sidebar-accent\` | brand-100 | brand-800 |
|
|
2306
|
+
| \`--sidebar-border\` | surface-300 | surface-600 |
|
|
2307
|
+
|
|
2308
|
+
Every background token has a matching \`*-foreground\` counterpart. Always pair them.
|
|
2309
|
+
${accentPalettes.length > 0 ? `
|
|
2310
|
+
### Accent Scopes
|
|
2311
|
+
|
|
2312
|
+
Each accent color provides a full semantic scope:
|
|
2313
|
+
${accentPalettes.map((a) => `- **${a.name}** (\`--${a.cssName}\`): \`-foreground\`, \`-background\`, \`-card\`, \`-popover\`, \`-secondary\`, \`-muted\`, \`-accent\`, \`-subtle\`, \`-border\`, \`-border-muted\`, \`-input\`, \`-ring\` \u2014 each with light/dark variants.`).join("\n")}
|
|
2314
|
+
` : ""}
|
|
2315
|
+
## How to Use
|
|
2316
|
+
|
|
2317
|
+
1. **Use semantic tokens** (\`--primary\`, \`--background\`, etc.) in component code \u2014 never reference primitive step numbers directly.
|
|
2318
|
+
2. **Tailwind**: All tokens are available as Tailwind utilities (\`bg-primary\`, \`text-foreground\`, \`border-border\`, etc.).
|
|
2319
|
+
3. **Dark mode**: Add \`.dark\` to \`<html>\` or a container. All semantic tokens remap automatically.
|
|
2320
|
+
4. **Borders**: Default to \`--border-muted\` for subtle separation (dividers, table rows). Use \`--border\` for visible borders (cards, panels). Form controls (\`input\`, \`select\`, \`textarea\`) always use \`--input\` \u2014 never \`--border\`.
|
|
2321
|
+
5. **Shadows and radii**: see standby.design/shape for hue-matched shadow tokens and border-radius scales.
|
|
2322
|
+
6. **Border-radius**: Base \`--radius: 0.625rem\`. Derived sizes: \`--radius-sm\` (0.6\xD7) through \`--radius-4xl\` (2.6\xD7).
|
|
2323
|
+
|
|
2324
|
+
## Primitive Scale Reference
|
|
2325
|
+
|
|
2326
|
+
Each color uses an 18-step scale: **25, 50, 75, 100, 200\u2013800, 825, 850, 875, 900, 925, 950, 975**
|
|
2327
|
+
|
|
2328
|
+
- **25\u2013100** \u2014 light surfaces (backgrounds, cards)
|
|
2329
|
+
- **200\u2013800** \u2014 core palette (buttons, text, accents)
|
|
2330
|
+
- **825\u2013875** \u2014 dark-mode surfaces
|
|
2331
|
+
- **900\u2013975** \u2014 high-contrast surfaces
|
|
2332
|
+
|
|
2333
|
+
Two variants per color:
|
|
2334
|
+
- \`--color-{name}-{step}\` \u2014 full chroma (interactive elements)
|
|
2335
|
+
- \`--color-{name}-surface-{step}\` \u2014 ${pct}% chroma (backgrounds, containers)
|
|
2336
|
+
|
|
2337
|
+
## Settings
|
|
2338
|
+
|
|
2339
|
+
- **Palette mode**: ${mode === "balanced" ? "Balanced midpoint" : "Brand Centered"}
|
|
2340
|
+
- **Foreground contrast**: ${fgLabel}
|
|
2341
|
+
- **Surface chroma**: ${pct}%
|
|
2342
|
+
`;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// ../../system-react/src/lib/type-code-export.ts
|
|
2346
|
+
function generateCssExport(opts) {
|
|
2347
|
+
const { levels, headingFont, bodyFont, monoFont, scaleLabel: scaleLabel2 } = opts;
|
|
2348
|
+
const headingFF = fontFamily(headingFont);
|
|
2349
|
+
const bodyFF = fontFamily(bodyFont);
|
|
2350
|
+
const monoFF = fontFamily(monoFont);
|
|
2351
|
+
const isGolden = scaleLabel2.includes("\u221A\u03C6");
|
|
2352
|
+
let css = `/* Type Scale \u2014 ${scaleLabel2} */
|
|
2353
|
+
`;
|
|
2354
|
+
if (isGolden) {
|
|
2355
|
+
css += `/* Scales the perceived area of letterforms by the golden ratio. */
|
|
2356
|
+
`;
|
|
2357
|
+
}
|
|
2358
|
+
css += `:root {
|
|
2359
|
+
`;
|
|
2360
|
+
css += ` /* Font Families */
|
|
2361
|
+
`;
|
|
2362
|
+
css += ` --font-heading: ${headingFF};
|
|
2363
|
+
`;
|
|
2364
|
+
css += ` --font-body: ${bodyFF};
|
|
2365
|
+
`;
|
|
2366
|
+
css += ` --font-mono: ${monoFF};
|
|
2367
|
+
`;
|
|
2368
|
+
css += `
|
|
2369
|
+
/* Type Scale */
|
|
2370
|
+
`;
|
|
2371
|
+
for (const l of levels) {
|
|
2372
|
+
css += ` --text-${l.level}: ${l.clampValue};
|
|
2373
|
+
`;
|
|
2374
|
+
}
|
|
2375
|
+
css += `
|
|
2376
|
+
/* Line Heights */
|
|
2377
|
+
`;
|
|
2378
|
+
for (const l of levels) {
|
|
2379
|
+
css += ` --leading-${l.level}: ${l.lineHeight};
|
|
2380
|
+
`;
|
|
2381
|
+
}
|
|
2382
|
+
css += `
|
|
2383
|
+
/* Letter Spacing */
|
|
2384
|
+
`;
|
|
2385
|
+
for (const l of levels) {
|
|
2386
|
+
css += ` --tracking-${l.level}: ${l.letterSpacing}em;
|
|
2387
|
+
`;
|
|
2388
|
+
}
|
|
2389
|
+
if (opts.spacingTokens.length > 0) {
|
|
2390
|
+
css += `
|
|
2391
|
+
/* Spacing Scale */
|
|
2392
|
+
`;
|
|
2393
|
+
for (const t of opts.spacingTokens) {
|
|
2394
|
+
css += ` --space-${t.name}: ${t.rem}rem;
|
|
2395
|
+
`;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
css += `}
|
|
2399
|
+
`;
|
|
2400
|
+
return css;
|
|
2401
|
+
}
|
|
2402
|
+
function generateTailwindV4Export(opts) {
|
|
2403
|
+
const { levels, headingFont, bodyFont, monoFont, scaleLabel: scaleLabel2 } = opts;
|
|
2404
|
+
const headingFF = fontFamily(headingFont);
|
|
2405
|
+
const bodyFF = fontFamily(bodyFont);
|
|
2406
|
+
const monoFF = fontFamily(monoFont);
|
|
2407
|
+
const isGolden = scaleLabel2.includes("\u221A\u03C6");
|
|
2408
|
+
let css = `/* Type Scale \u2014 ${scaleLabel2} */
|
|
2409
|
+
`;
|
|
2410
|
+
if (isGolden) {
|
|
2411
|
+
css += `/* Scales the perceived area of letterforms by the golden ratio. */
|
|
2412
|
+
`;
|
|
2413
|
+
}
|
|
2414
|
+
css += `@theme {
|
|
2415
|
+
`;
|
|
2416
|
+
css += ` --font-heading: ${headingFF};
|
|
2417
|
+
`;
|
|
2418
|
+
css += ` --font-body: ${bodyFF};
|
|
2419
|
+
`;
|
|
2420
|
+
css += ` --font-mono: ${monoFF};
|
|
2421
|
+
`;
|
|
2422
|
+
css += `
|
|
2423
|
+
`;
|
|
2424
|
+
for (const l of levels) {
|
|
2425
|
+
css += ` --text-${l.level}: ${l.clampValue};
|
|
2426
|
+
`;
|
|
2427
|
+
}
|
|
2428
|
+
css += `
|
|
2429
|
+
`;
|
|
2430
|
+
for (const l of levels) {
|
|
2431
|
+
css += ` --leading-${l.level}: ${l.lineHeight};
|
|
2432
|
+
`;
|
|
2433
|
+
}
|
|
2434
|
+
css += `
|
|
2435
|
+
`;
|
|
2436
|
+
for (const l of levels) {
|
|
2437
|
+
css += ` --tracking-${l.level}: ${l.letterSpacing}em;
|
|
2438
|
+
`;
|
|
2439
|
+
}
|
|
2440
|
+
if (opts.spacingTokens.length > 0) {
|
|
2441
|
+
css += `
|
|
2442
|
+
`;
|
|
2443
|
+
for (const t of opts.spacingTokens) {
|
|
2444
|
+
css += ` --space-${t.name}: ${t.rem}rem;
|
|
2445
|
+
`;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
css += `}
|
|
2449
|
+
`;
|
|
2450
|
+
return css;
|
|
2451
|
+
}
|
|
2452
|
+
function generateLlmBriefing2(opts) {
|
|
2453
|
+
const { levels, spacingTokens, headingFont, bodyFont, monoFont, scaleLabel: scaleLabel2 } = opts;
|
|
2454
|
+
const headingFF = fontFamily(headingFont);
|
|
2455
|
+
const bodyFF = fontFamily(bodyFont);
|
|
2456
|
+
const monoFF = fontFamily(monoFont);
|
|
2457
|
+
let md = `# Type Scale \u2014 ${scaleLabel2}
|
|
2458
|
+
|
|
2459
|
+
`;
|
|
2460
|
+
md += `## Fonts
|
|
2461
|
+
|
|
2462
|
+
`;
|
|
2463
|
+
md += `- **Heading:** ${headingFF}
|
|
2464
|
+
`;
|
|
2465
|
+
md += `- **Body:** ${bodyFF}
|
|
2466
|
+
`;
|
|
2467
|
+
md += `- **Mono:** ${monoFF}
|
|
2468
|
+
`;
|
|
2469
|
+
md += `
|
|
2470
|
+
## Type Scale
|
|
2471
|
+
|
|
2472
|
+
`;
|
|
2473
|
+
md += `| Level | Min | Max | clamp() |
|
|
2474
|
+
`;
|
|
2475
|
+
md += `|-------|-----|-----|---------|
|
|
2476
|
+
`;
|
|
2477
|
+
for (const l of levels) {
|
|
2478
|
+
if (l.isFluid) {
|
|
2479
|
+
md += `| ${l.label} | ${l.minRem}rem | ${l.maxRem}rem | \`${l.clampValue}\` |
|
|
2480
|
+
`;
|
|
2481
|
+
} else {
|
|
2482
|
+
md += `| ${l.label} | ${l.maxRem}rem | \u2014 | ${l.maxRem}rem |
|
|
2483
|
+
`;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
md += `
|
|
2487
|
+
## Line Heights
|
|
2488
|
+
|
|
2489
|
+
`;
|
|
2490
|
+
md += `| Level | Value |
|
|
2491
|
+
`;
|
|
2492
|
+
md += `|-------|-------|
|
|
2493
|
+
`;
|
|
2494
|
+
for (const l of levels) {
|
|
2495
|
+
md += `| ${l.label} | ${l.lineHeight} |
|
|
2496
|
+
`;
|
|
2497
|
+
}
|
|
2498
|
+
md += `
|
|
2499
|
+
## Letter Spacing
|
|
2500
|
+
|
|
2501
|
+
`;
|
|
2502
|
+
md += `| Level | Value |
|
|
2503
|
+
`;
|
|
2504
|
+
md += `|-------|-------|
|
|
2505
|
+
`;
|
|
2506
|
+
for (const l of levels) {
|
|
2507
|
+
md += `| ${l.label} | ${l.letterSpacing}em |
|
|
2508
|
+
`;
|
|
2509
|
+
}
|
|
2510
|
+
if (spacingTokens.length > 0) {
|
|
2511
|
+
md += `
|
|
2512
|
+
## Spacing Scale
|
|
2513
|
+
|
|
2514
|
+
`;
|
|
2515
|
+
md += `| Token | Value |
|
|
2516
|
+
`;
|
|
2517
|
+
md += `|-------|-------|
|
|
2518
|
+
`;
|
|
2519
|
+
for (const t of spacingTokens) {
|
|
2520
|
+
md += `| --space-${t.name} | ${t.rem}rem (${t.px}px) |
|
|
2521
|
+
`;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
md += `
|
|
2525
|
+
## Usage
|
|
2526
|
+
|
|
2527
|
+
`;
|
|
2528
|
+
md += `Use the CSS custom properties from the CSS or Tailwind export.
|
|
2529
|
+
`;
|
|
2530
|
+
md += `Font sizes use \`clamp()\` for fluid scaling between 375px and 1920px viewport.
|
|
2531
|
+
`;
|
|
2532
|
+
md += `Combine with color tokens from standby.design/color and shape tokens from standby.design/shape.
|
|
2533
|
+
`;
|
|
2534
|
+
return md;
|
|
2535
|
+
}
|
|
2536
|
+
function generateFontEmbed(headingFont, bodyFont, monoFont) {
|
|
2537
|
+
const slugs = [headingFont, bodyFont, monoFont].filter(Boolean);
|
|
2538
|
+
const embed = buildFontshareEmbed(slugs);
|
|
2539
|
+
if (!embed) return "<!-- No Fontshare fonts selected -->";
|
|
2540
|
+
return `<!-- Fontshare Fonts -->
|
|
2541
|
+
${embed}`;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// ../core/src/shadows.ts
|
|
2545
|
+
function buildLevels(scale) {
|
|
2546
|
+
return [
|
|
2547
|
+
{ name: "xs", factor: 1 / (scale * scale) },
|
|
2548
|
+
{ name: "sm", factor: 1 / scale },
|
|
2549
|
+
{ name: "md", factor: 1 },
|
|
2550
|
+
{ name: "lg", factor: scale },
|
|
2551
|
+
{ name: "xl", factor: scale * scale }
|
|
2552
|
+
];
|
|
2553
|
+
}
|
|
2554
|
+
function getShadowHue(bgHex, config) {
|
|
2555
|
+
if (config.colorMode === "custom" && config.customColor) {
|
|
2556
|
+
const [, , h2] = hexToOklch(config.customColor);
|
|
2557
|
+
return { L: 0.05, C: 0.01, H: h2.toFixed(2) };
|
|
2558
|
+
}
|
|
2559
|
+
const [, , h] = hexToOklch(bgHex);
|
|
2560
|
+
return { L: 0.05, C: 0.01, H: h.toFixed(2) };
|
|
2561
|
+
}
|
|
2562
|
+
function generateNormal(bgHex, isDark, config) {
|
|
2563
|
+
const { H } = getShadowHue(bgHex, config);
|
|
2564
|
+
const shadowL = isDark ? 0.02 : 0.05;
|
|
2565
|
+
const shadowC = isDark ? 5e-3 : 0.01;
|
|
2566
|
+
const alphaMultiplier = (isDark ? 1.4 : 0.7) * config.strength;
|
|
2567
|
+
return buildLevels(config.scale).map(({ name, factor }) => {
|
|
2568
|
+
const oY = factor;
|
|
2569
|
+
const a1 = Math.min(0.7, 0.12 * factor * alphaMultiplier).toFixed(3);
|
|
2570
|
+
const a2 = Math.min(0.5, 0.18 * factor * alphaMultiplier).toFixed(3);
|
|
2571
|
+
const b1 = (factor * 0.6 * config.blurScale).toFixed(3);
|
|
2572
|
+
const b2 = (factor * 2 * config.blurScale).toFixed(3);
|
|
2573
|
+
const shadow = `0 ${(oY * 0.4).toFixed(3)}rem ${b1}rem oklch(${shadowL} ${shadowC} ${H} / ${a1}), 0 ${oY.toFixed(3)}rem ${b2}rem oklch(${shadowL} ${shadowC} ${H} / ${a2})`;
|
|
2574
|
+
return { name, shadow };
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
function neumorphicColors(bgHex, isDark) {
|
|
2578
|
+
const [bgL, bgC, bgH] = hexToOklch(bgHex);
|
|
2579
|
+
const lightL = Math.min(1, bgL + (isDark ? 0.12 : 0.25));
|
|
2580
|
+
const darkL = Math.max(0, bgL - (isDark ? 0.15 : 0.12));
|
|
2581
|
+
return {
|
|
2582
|
+
lightHex: oklchToHex(lightL, bgC * 0.3, bgH),
|
|
2583
|
+
darkHex: oklchToHex(darkL, bgC * 0.3, bgH)
|
|
2584
|
+
};
|
|
2585
|
+
}
|
|
2586
|
+
function generateNeumorphic(bgHex, isDark, config) {
|
|
2587
|
+
const { lightHex, darkHex } = neumorphicColors(bgHex, isDark);
|
|
2588
|
+
return buildLevels(config.scale).map(({ name, factor }) => {
|
|
2589
|
+
const dist = (factor * 0.25).toFixed(2);
|
|
2590
|
+
const blur = (factor * 0.5 * config.blurScale).toFixed(2);
|
|
2591
|
+
const aLight = Math.min(1, 0.5 * factor * config.strength).toFixed(2);
|
|
2592
|
+
const aDark = Math.min(1, 0.6 * factor * config.strength).toFixed(2);
|
|
2593
|
+
const shadow = `-${dist}rem -${dist}rem ${blur}rem ${lightHex}${alphaHex(parseFloat(aLight))}, ${dist}rem ${dist}rem ${blur}rem ${darkHex}${alphaHex(parseFloat(aDark))}`;
|
|
2594
|
+
return { name, shadow };
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
function generateFlat(bgHex, isDark, config) {
|
|
2598
|
+
const [, , surfaceHue] = hexToOklch(bgHex);
|
|
2599
|
+
const shadowL = isDark ? 0.08 : 0.15;
|
|
2600
|
+
const shadowC = isDark ? 0.01 : 0.02;
|
|
2601
|
+
const H = surfaceHue.toFixed(2);
|
|
2602
|
+
const baseAlpha = (isDark ? 0.6 : 0.35) * config.strength;
|
|
2603
|
+
return buildLevels(config.scale).map(({ name, factor }) => {
|
|
2604
|
+
const offset = (factor * 0.25).toFixed(2);
|
|
2605
|
+
const a = Math.min(1, baseAlpha * factor).toFixed(2);
|
|
2606
|
+
const shadow = `0 ${offset}rem 0 oklch(${shadowL} ${shadowC} ${H} / ${a})`;
|
|
2607
|
+
return { name, shadow };
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
function generateBrutalist(bgHex, isDark, config) {
|
|
2611
|
+
const baseX = config.offsetX ?? 4;
|
|
2612
|
+
const baseY = config.offsetY ?? 4;
|
|
2613
|
+
const strokeWidth = config.borderWidth ?? 1;
|
|
2614
|
+
let strokeL;
|
|
2615
|
+
let strokeC;
|
|
2616
|
+
let strokeH;
|
|
2617
|
+
if (config.colorMode === "custom" && config.customColor) {
|
|
2618
|
+
const [l, c, h] = hexToOklch(config.customColor);
|
|
2619
|
+
strokeL = l;
|
|
2620
|
+
strokeC = c;
|
|
2621
|
+
strokeH = h.toFixed(2);
|
|
2622
|
+
} else {
|
|
2623
|
+
const [, , surfaceHue] = hexToOklch(bgHex);
|
|
2624
|
+
strokeL = isDark ? 0.98 : 0.05;
|
|
2625
|
+
strokeC = 5e-3;
|
|
2626
|
+
strokeH = surfaceHue.toFixed(2);
|
|
2627
|
+
}
|
|
2628
|
+
const [bgL, bgC, bgHue] = hexToOklch(bgHex);
|
|
2629
|
+
return buildLevels(config.scale).map(({ name, factor }) => {
|
|
2630
|
+
const oX = (baseX * factor).toFixed(2);
|
|
2631
|
+
const oY = (baseY * factor).toFixed(2);
|
|
2632
|
+
const a = Math.min(1, config.strength).toFixed(3);
|
|
2633
|
+
const shadow = `${oX}px ${oY}px 0 0 oklch(${bgL.toFixed(3)} ${bgC.toFixed(3)} ${bgHue.toFixed(2)}), ${oX}px ${oY}px 0 ${strokeWidth}px oklch(${strokeL} ${strokeC} ${strokeH} / ${a})`;
|
|
2634
|
+
return { name, shadow };
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
function alphaHex(a) {
|
|
2638
|
+
return Math.round(Math.min(1, Math.max(0, a)) * 255).toString(16).padStart(2, "0");
|
|
2639
|
+
}
|
|
2640
|
+
function generateShadows(bgHex, isDark, config) {
|
|
2641
|
+
switch (config.type) {
|
|
2642
|
+
case "neumorphic":
|
|
2643
|
+
return generateNeumorphic(bgHex, isDark, config);
|
|
2644
|
+
case "flat":
|
|
2645
|
+
return generateFlat(bgHex, isDark, config);
|
|
2646
|
+
case "brutalist":
|
|
2647
|
+
return generateBrutalist(bgHex, isDark, config);
|
|
2648
|
+
default:
|
|
2649
|
+
return generateNormal(bgHex, isDark, config);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
// ../../system-react/src/lib/shape-code-export.ts
|
|
2654
|
+
var SHAPE_DEFAULTS = {
|
|
2655
|
+
shapeStyle: "paper",
|
|
2656
|
+
shadowEnabled: true,
|
|
2657
|
+
shadowType: "normal",
|
|
2658
|
+
shadowStrength: 1,
|
|
2659
|
+
shadowBlurScale: 1,
|
|
2660
|
+
shadowScale: 1.272,
|
|
2661
|
+
shadowColorMode: "auto",
|
|
2662
|
+
shadowCustomColor: "#000000",
|
|
2663
|
+
shadowOffsetX: 2,
|
|
2664
|
+
shadowOffsetY: 4,
|
|
2665
|
+
brutalistVariant: "outlined",
|
|
2666
|
+
borderEnabled: true,
|
|
2667
|
+
borderWidth: 1,
|
|
2668
|
+
borderRadius: 8,
|
|
2669
|
+
glassDepth: 0,
|
|
2670
|
+
glassBlur: 0,
|
|
2671
|
+
glassDispersion: 0,
|
|
2672
|
+
ringWidth: 2,
|
|
2673
|
+
ringOffset: 2,
|
|
2674
|
+
separationMode: "shadow",
|
|
2675
|
+
surfaceHex: "#335A7F"
|
|
2676
|
+
};
|
|
2677
|
+
function optsFromState(state, surfaceHex) {
|
|
2678
|
+
return {
|
|
2679
|
+
...SHAPE_DEFAULTS,
|
|
2680
|
+
...state,
|
|
2681
|
+
surfaceHex: surfaceHex ?? state?.shadowCustomColor ?? SHAPE_DEFAULTS.surfaceHex
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function effectiveShadowType(opts) {
|
|
2685
|
+
if (opts.shapeStyle === "neomorph") return "neumorphic";
|
|
2686
|
+
if (opts.shapeStyle === "neobrutalism") return "brutalist";
|
|
2687
|
+
return opts.shadowType;
|
|
2688
|
+
}
|
|
2689
|
+
function buildShadowConfig(opts) {
|
|
2690
|
+
return {
|
|
2691
|
+
type: effectiveShadowType(opts),
|
|
2692
|
+
strength: opts.shadowStrength,
|
|
2693
|
+
blurScale: opts.shadowBlurScale,
|
|
2694
|
+
scale: opts.shadowScale,
|
|
2695
|
+
colorMode: opts.shadowColorMode,
|
|
2696
|
+
customColor: opts.shadowCustomColor,
|
|
2697
|
+
offsetX: opts.shadowOffsetX,
|
|
2698
|
+
offsetY: opts.shadowOffsetY,
|
|
2699
|
+
borderWidth: opts.borderEnabled ? opts.borderWidth : 1
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function deriveBgHex(surfaceHex, isDark) {
|
|
2703
|
+
const [, , H] = hexToOklch(surfaceHex);
|
|
2704
|
+
const C = 0.015;
|
|
2705
|
+
return isDark ? oklchToHex(0.15, C, H) : oklchToHex(0.96, C * 0.3, H);
|
|
2706
|
+
}
|
|
2707
|
+
function radiusScale(base) {
|
|
2708
|
+
return {
|
|
2709
|
+
xs: base / 4,
|
|
2710
|
+
sm: base / 2,
|
|
2711
|
+
md: base,
|
|
2712
|
+
lg: base * 1.5,
|
|
2713
|
+
xl: base * 2
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function pxToRem(px) {
|
|
2717
|
+
return `${+(px / 16).toFixed(4)}rem`;
|
|
2718
|
+
}
|
|
2719
|
+
function shadowTypeLabel(t) {
|
|
2720
|
+
return { normal: "Normal", neumorphic: "Neumorphic", flat: "Flat", brutalist: "Brutalist" }[t];
|
|
2721
|
+
}
|
|
2722
|
+
function scaleLabel(scale) {
|
|
2723
|
+
return Math.abs(scale - 1.272) < 1e-3 ? "\u221A\u03C6" : scale.toFixed(3);
|
|
2724
|
+
}
|
|
2725
|
+
function generateShapeCss(opts) {
|
|
2726
|
+
const config = buildShadowConfig(opts);
|
|
2727
|
+
const radii = radiusScale(opts.borderRadius);
|
|
2728
|
+
let css = `/* Shape Tokens \u2014 standby.design/shape */
|
|
2729
|
+
`;
|
|
2730
|
+
css += `:root {
|
|
2731
|
+
`;
|
|
2732
|
+
if (opts.shadowEnabled) {
|
|
2733
|
+
const lightBg = deriveBgHex(opts.surfaceHex, false);
|
|
2734
|
+
const lightShadows = generateShadows(lightBg, false, config);
|
|
2735
|
+
css += ` /* Shadows \u2014 ${shadowTypeLabel(effectiveShadowType(opts))}, scale ${scaleLabel(opts.shadowScale)} */
|
|
2736
|
+
`;
|
|
2737
|
+
for (const s of lightShadows) {
|
|
2738
|
+
css += ` --shadow-${s.name}: ${s.shadow};
|
|
2739
|
+
`;
|
|
2740
|
+
}
|
|
2741
|
+
} else {
|
|
2742
|
+
css += ` /* Shadows: disabled */
|
|
2743
|
+
`;
|
|
2744
|
+
}
|
|
2745
|
+
css += `
|
|
2746
|
+
/* Border Radius */
|
|
2747
|
+
`;
|
|
2748
|
+
for (const [name, px] of Object.entries(radii)) {
|
|
2749
|
+
const varName = name === "md" ? "--radius" : `--radius-${name}`;
|
|
2750
|
+
css += ` ${varName}: ${pxToRem(px)};
|
|
2751
|
+
`;
|
|
2752
|
+
}
|
|
2753
|
+
if (opts.borderEnabled && opts.borderWidth > 0) {
|
|
2754
|
+
css += `
|
|
2755
|
+
/* Border */
|
|
2756
|
+
`;
|
|
2757
|
+
css += ` --border-width: ${opts.borderWidth}px;
|
|
2758
|
+
`;
|
|
2759
|
+
}
|
|
2760
|
+
css += `
|
|
2761
|
+
/* Focus Ring */
|
|
2762
|
+
`;
|
|
2763
|
+
css += ` --ring-width: ${opts.ringWidth}px;
|
|
2764
|
+
`;
|
|
2765
|
+
css += ` --ring-offset: ${opts.ringOffset}px;
|
|
2766
|
+
`;
|
|
2767
|
+
if (opts.shapeStyle === "glass") {
|
|
2768
|
+
css += `
|
|
2769
|
+
/* Liquid Glass (vaso) */
|
|
2770
|
+
`;
|
|
2771
|
+
css += ` --glass-depth: ${opts.glassDepth};
|
|
2772
|
+
`;
|
|
2773
|
+
css += ` --glass-blur: ${opts.glassBlur};
|
|
2774
|
+
`;
|
|
2775
|
+
css += ` --glass-dispersion: ${opts.glassDispersion};
|
|
2776
|
+
`;
|
|
2777
|
+
}
|
|
2778
|
+
css += `}
|
|
2779
|
+
`;
|
|
2780
|
+
if (opts.shadowEnabled) {
|
|
2781
|
+
const darkBg = deriveBgHex(opts.surfaceHex, true);
|
|
2782
|
+
const darkShadows = generateShadows(darkBg, true, config);
|
|
2783
|
+
css += `
|
|
2784
|
+
.dark {
|
|
2785
|
+
`;
|
|
2786
|
+
for (const s of darkShadows) {
|
|
2787
|
+
css += ` --shadow-${s.name}: ${s.shadow};
|
|
2788
|
+
`;
|
|
2789
|
+
}
|
|
2790
|
+
css += `}
|
|
2791
|
+
`;
|
|
2792
|
+
}
|
|
2793
|
+
css += `
|
|
2794
|
+
/* Surface separation strategy: ${opts.separationMode} */
|
|
2795
|
+
`;
|
|
2796
|
+
return css;
|
|
2797
|
+
}
|
|
2798
|
+
function generateShapeTailwind(opts) {
|
|
2799
|
+
const config = buildShadowConfig(opts);
|
|
2800
|
+
const radii = radiusScale(opts.borderRadius);
|
|
2801
|
+
let css = `/* Shape Tokens \u2014 standby.design/shape */
|
|
2802
|
+
`;
|
|
2803
|
+
css += `@theme {
|
|
2804
|
+
`;
|
|
2805
|
+
for (const [name, px] of Object.entries(radii)) {
|
|
2806
|
+
const varName = name === "md" ? "--radius" : `--radius-${name}`;
|
|
2807
|
+
css += ` ${varName}: ${pxToRem(px)};
|
|
2808
|
+
`;
|
|
2809
|
+
}
|
|
2810
|
+
if (opts.borderEnabled && opts.borderWidth > 0) {
|
|
2811
|
+
css += `
|
|
2812
|
+
--border-width: ${opts.borderWidth}px;
|
|
2813
|
+
`;
|
|
2814
|
+
}
|
|
2815
|
+
css += `
|
|
2816
|
+
--ring-width: ${opts.ringWidth}px;
|
|
2817
|
+
`;
|
|
2818
|
+
css += ` --ring-offset: ${opts.ringOffset}px;
|
|
2819
|
+
`;
|
|
2820
|
+
if (opts.shapeStyle === "glass") {
|
|
2821
|
+
css += `
|
|
2822
|
+
--glass-depth: ${opts.glassDepth};
|
|
2823
|
+
`;
|
|
2824
|
+
css += ` --glass-blur: ${opts.glassBlur};
|
|
2825
|
+
`;
|
|
2826
|
+
css += ` --glass-dispersion: ${opts.glassDispersion};
|
|
2827
|
+
`;
|
|
2828
|
+
}
|
|
2829
|
+
css += `}
|
|
2830
|
+
`;
|
|
2831
|
+
if (opts.shadowEnabled) {
|
|
2832
|
+
const lightBg = deriveBgHex(opts.surfaceHex, false);
|
|
2833
|
+
const darkBg = deriveBgHex(opts.surfaceHex, true);
|
|
2834
|
+
const lightShadows = generateShadows(lightBg, false, config);
|
|
2835
|
+
const darkShadows = generateShadows(darkBg, true, config);
|
|
2836
|
+
css += `
|
|
2837
|
+
/* Shadows \u2014 ${shadowTypeLabel(effectiveShadowType(opts))}, scale ${scaleLabel(opts.shadowScale)} */
|
|
2838
|
+
`;
|
|
2839
|
+
css += `:root {
|
|
2840
|
+
`;
|
|
2841
|
+
for (const s of lightShadows) {
|
|
2842
|
+
css += ` --shadow-${s.name}: ${s.shadow};
|
|
2843
|
+
`;
|
|
2844
|
+
}
|
|
2845
|
+
css += `}
|
|
2846
|
+
`;
|
|
2847
|
+
css += `.dark {
|
|
2848
|
+
`;
|
|
2849
|
+
for (const s of darkShadows) {
|
|
2850
|
+
css += ` --shadow-${s.name}: ${s.shadow};
|
|
2851
|
+
`;
|
|
2852
|
+
}
|
|
2853
|
+
css += `}
|
|
2854
|
+
`;
|
|
2855
|
+
}
|
|
2856
|
+
css += `
|
|
2857
|
+
/* Surface separation strategy: ${opts.separationMode} */
|
|
2858
|
+
`;
|
|
2859
|
+
return css;
|
|
2860
|
+
}
|
|
2861
|
+
function generateLlmBriefing3(opts) {
|
|
2862
|
+
const config = buildShadowConfig(opts);
|
|
2863
|
+
const radii = radiusScale(opts.borderRadius);
|
|
2864
|
+
let md = `# Shape Tokens \u2014 standby.design/shape
|
|
2865
|
+
|
|
2866
|
+
`;
|
|
2867
|
+
md += `## Shadows
|
|
2868
|
+
|
|
2869
|
+
`;
|
|
2870
|
+
if (opts.shadowEnabled) {
|
|
2871
|
+
md += `- **Style:** ${shadowTypeLabel(effectiveShadowType(opts))}
|
|
2872
|
+
`;
|
|
2873
|
+
md += `- **Scale:** ${scaleLabel(opts.shadowScale)} (5 levels: xs, sm, md, lg, xl)
|
|
2874
|
+
`;
|
|
2875
|
+
md += `- **Strength:** ${opts.shadowStrength}
|
|
2876
|
+
`;
|
|
2877
|
+
md += `- **Blur scale:** ${opts.shadowBlurScale}
|
|
2878
|
+
`;
|
|
2879
|
+
md += `- **Color mode:** ${opts.shadowColorMode}${opts.shadowColorMode === "custom" ? ` (${opts.shadowCustomColor})` : ""}
|
|
2880
|
+
|
|
2881
|
+
`;
|
|
2882
|
+
const lightBg = deriveBgHex(opts.surfaceHex, false);
|
|
2883
|
+
const darkBg = deriveBgHex(opts.surfaceHex, true);
|
|
2884
|
+
const lightShadows = generateShadows(lightBg, false, config);
|
|
2885
|
+
const darkShadows = generateShadows(darkBg, true, config);
|
|
2886
|
+
md += `### Light mode
|
|
2887
|
+
|
|
2888
|
+
`;
|
|
2889
|
+
md += `| Level | box-shadow |
|
|
2890
|
+
|-------|------------|
|
|
2891
|
+
`;
|
|
2892
|
+
for (const s of lightShadows) md += `| ${s.name} | \`${s.shadow}\` |
|
|
2893
|
+
`;
|
|
2894
|
+
md += `
|
|
2895
|
+
### Dark mode
|
|
2896
|
+
|
|
2897
|
+
`;
|
|
2898
|
+
md += `| Level | box-shadow |
|
|
2899
|
+
|-------|------------|
|
|
2900
|
+
`;
|
|
2901
|
+
for (const s of darkShadows) md += `| ${s.name} | \`${s.shadow}\` |
|
|
2902
|
+
`;
|
|
2903
|
+
} else {
|
|
2904
|
+
md += `Shadows are **disabled**.
|
|
2905
|
+
`;
|
|
2906
|
+
}
|
|
2907
|
+
md += `
|
|
2908
|
+
## Border Radius
|
|
2909
|
+
|
|
2910
|
+
`;
|
|
2911
|
+
md += `Base: ${opts.borderRadius}px
|
|
2912
|
+
|
|
2913
|
+
`;
|
|
2914
|
+
md += `| Token | Value |
|
|
2915
|
+
|-------|-------|
|
|
2916
|
+
`;
|
|
2917
|
+
for (const [name, px] of Object.entries(radii)) {
|
|
2918
|
+
const token = name === "md" ? "--radius" : `--radius-${name}`;
|
|
2919
|
+
md += `| ${token} | ${pxToRem(px)} (${px}px) |
|
|
2920
|
+
`;
|
|
2921
|
+
}
|
|
2922
|
+
md += `
|
|
2923
|
+
## Border
|
|
2924
|
+
|
|
2925
|
+
`;
|
|
2926
|
+
if (opts.borderEnabled && opts.borderWidth > 0) {
|
|
2927
|
+
md += `- **Width:** ${opts.borderWidth}px
|
|
2928
|
+
`;
|
|
2929
|
+
} else {
|
|
2930
|
+
md += `Borders are **disabled**.
|
|
2931
|
+
`;
|
|
2932
|
+
}
|
|
2933
|
+
md += `
|
|
2934
|
+
## Focus Ring
|
|
2935
|
+
|
|
2936
|
+
`;
|
|
2937
|
+
md += `- **Width:** ${opts.ringWidth}px
|
|
2938
|
+
`;
|
|
2939
|
+
md += `- **Offset:** ${opts.ringOffset}px
|
|
2940
|
+
`;
|
|
2941
|
+
md += `
|
|
2942
|
+
## Liquid Glass
|
|
2943
|
+
|
|
2944
|
+
`;
|
|
2945
|
+
if (opts.shapeStyle === "glass") {
|
|
2946
|
+
md += `Uses the \`vaso\` library (liquid glass distortion effect).
|
|
2947
|
+
|
|
2948
|
+
`;
|
|
2949
|
+
md += `- **Depth:** ${opts.glassDepth} (displacement intensity)
|
|
2950
|
+
`;
|
|
2951
|
+
md += `- **Blur:** ${opts.glassBlur} (backdrop blur amount)
|
|
2952
|
+
`;
|
|
2953
|
+
md += `- **Dispersion:** ${opts.glassDispersion} (chromatic aberration)
|
|
2954
|
+
`;
|
|
2955
|
+
} else {
|
|
2956
|
+
md += `Liquid glass effect is **disabled**.
|
|
2957
|
+
`;
|
|
2958
|
+
}
|
|
2959
|
+
md += `
|
|
2960
|
+
## Surface Separation
|
|
2961
|
+
|
|
2962
|
+
`;
|
|
2963
|
+
md += `Strategy: **${opts.separationMode}**
|
|
2964
|
+
`;
|
|
2965
|
+
const descriptions = {
|
|
2966
|
+
shadow: "Surfaces are separated using box-shadow elevation.",
|
|
2967
|
+
border: "Surfaces are separated using visible borders.",
|
|
2968
|
+
contrast: "Surfaces are separated using background-color contrast.",
|
|
2969
|
+
gap: "Surfaces are separated using whitespace (gap/padding).",
|
|
2970
|
+
mixed: "Surfaces use a combination of shadow, border, and contrast."
|
|
2971
|
+
};
|
|
2972
|
+
md += `${descriptions[opts.separationMode]}
|
|
2973
|
+
`;
|
|
2974
|
+
md += `
|
|
2975
|
+
## Usage
|
|
2976
|
+
|
|
2977
|
+
`;
|
|
2978
|
+
md += `Use the CSS custom properties from the CSS or Tailwind export.
|
|
2979
|
+
`;
|
|
2980
|
+
md += `Shadows are mode-dependent \u2014 define both \`:root\` and \`.dark\` blocks.
|
|
2981
|
+
`;
|
|
2982
|
+
md += `Combine with color tokens from standby.design/color and type tokens from standby.design/type.
|
|
2983
|
+
`;
|
|
2984
|
+
return md;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
// ../../system-react/src/lib/design-token-export.ts
|
|
2988
|
+
function generateDesignTokens(opts) {
|
|
2989
|
+
const { levels, spacingTokens, headingFont, bodyFont, monoFont, headingWeight } = opts;
|
|
2990
|
+
const headingFF = fontFamily(headingFont);
|
|
2991
|
+
const bodyFF = fontFamily(bodyFont);
|
|
2992
|
+
const monoFF = fontFamily(monoFont);
|
|
2993
|
+
const tokens = {
|
|
2994
|
+
font: {
|
|
2995
|
+
heading: { $type: "fontFamily", $value: headingFF },
|
|
2996
|
+
body: { $type: "fontFamily", $value: bodyFF },
|
|
2997
|
+
mono: { $type: "fontFamily", $value: monoFF }
|
|
2998
|
+
},
|
|
2999
|
+
typography: {},
|
|
3000
|
+
spacing: {}
|
|
3001
|
+
};
|
|
3002
|
+
const typo = tokens.typography;
|
|
3003
|
+
for (const l of levels) {
|
|
3004
|
+
typo[l.level] = {
|
|
3005
|
+
$type: "typography",
|
|
3006
|
+
$value: {
|
|
3007
|
+
fontFamily: l.isHeading ? headingFF : bodyFF,
|
|
3008
|
+
fontSize: l.clampValue,
|
|
3009
|
+
fontWeight: l.isHeading ? headingWeight : 400,
|
|
3010
|
+
lineHeight: l.lineHeight,
|
|
3011
|
+
letterSpacing: `${l.letterSpacing}em`
|
|
3012
|
+
}
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
const space = tokens.spacing;
|
|
3016
|
+
for (const t of spacingTokens) {
|
|
3017
|
+
space[t.name] = {
|
|
3018
|
+
$type: "dimension",
|
|
3019
|
+
$value: `${t.rem}rem`
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
return JSON.stringify(tokens, null, 2);
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// ../core/src/space-code-export.ts
|
|
3026
|
+
var expandAspects = expandAndSortAspects;
|
|
3027
|
+
function generateSpaceCss(opts) {
|
|
3028
|
+
const { spacingTokens, breakpoints, fluidMinVw, fluidMaxVw, containers, proseMaxCh, aspectRatios, includeReciprocals, ratioLabel } = opts;
|
|
3029
|
+
const bps = sortBreakpoints(breakpoints);
|
|
3030
|
+
const cts = sortContainers(containers);
|
|
3031
|
+
const ars = expandAspects(aspectRatios, includeReciprocals);
|
|
3032
|
+
let css = `/* Space \u2014 ${ratioLabel} */
|
|
3033
|
+
:root {
|
|
3034
|
+
`;
|
|
3035
|
+
css += ` /* Spacing Scale */
|
|
3036
|
+
`;
|
|
3037
|
+
for (const t of spacingTokens) {
|
|
3038
|
+
css += ` --space-${t.name}: ${t.rem}rem;
|
|
3039
|
+
`;
|
|
3040
|
+
}
|
|
3041
|
+
css += `
|
|
3042
|
+
/* Breakpoints */
|
|
3043
|
+
`;
|
|
3044
|
+
for (const b of bps) {
|
|
3045
|
+
css += ` --breakpoint-${b.name}: ${b.minPx}px;
|
|
3046
|
+
`;
|
|
3047
|
+
}
|
|
3048
|
+
css += ` --fluid-min-vw: ${fluidMinVw}px;
|
|
3049
|
+
`;
|
|
3050
|
+
css += ` --fluid-max-vw: ${fluidMaxVw}px;
|
|
3051
|
+
`;
|
|
3052
|
+
css += `
|
|
3053
|
+
/* Containers */
|
|
3054
|
+
`;
|
|
3055
|
+
for (const c of cts) {
|
|
3056
|
+
css += ` --container-${c.name}: ${c.maxPx}px;
|
|
3057
|
+
`;
|
|
3058
|
+
}
|
|
3059
|
+
css += ` --prose-max: ${proseMaxCh}ch;
|
|
3060
|
+
`;
|
|
3061
|
+
css += `
|
|
3062
|
+
/* Aspect Ratios */
|
|
3063
|
+
`;
|
|
3064
|
+
for (const a of ars) {
|
|
3065
|
+
css += ` --aspect-${a.name}: ${formatAspect(a)};
|
|
3066
|
+
`;
|
|
3067
|
+
}
|
|
3068
|
+
css += `}
|
|
3069
|
+
`;
|
|
3070
|
+
return css;
|
|
3071
|
+
}
|
|
3072
|
+
function generateSpaceTailwind(opts) {
|
|
3073
|
+
const { spacingTokens, breakpoints, containers, aspectRatios, includeReciprocals, ratioLabel } = opts;
|
|
3074
|
+
const bps = sortBreakpoints(breakpoints);
|
|
3075
|
+
const cts = sortContainers(containers);
|
|
3076
|
+
const ars = expandAspects(aspectRatios, includeReciprocals);
|
|
3077
|
+
let css = `/* Space \u2014 ${ratioLabel} */
|
|
3078
|
+
`;
|
|
3079
|
+
css += `/* Tailwind v4: --spacing-*, --breakpoint-*, --container-* are native theme keys. */
|
|
3080
|
+
`;
|
|
3081
|
+
css += `@theme {
|
|
3082
|
+
`;
|
|
3083
|
+
for (const t of spacingTokens) {
|
|
3084
|
+
css += ` --spacing-${t.name}: ${t.rem}rem;
|
|
3085
|
+
`;
|
|
3086
|
+
}
|
|
3087
|
+
css += `
|
|
3088
|
+
`;
|
|
3089
|
+
for (const b of bps) {
|
|
3090
|
+
css += ` --breakpoint-${b.name}: ${b.minPx}px;
|
|
3091
|
+
`;
|
|
3092
|
+
}
|
|
3093
|
+
css += `
|
|
3094
|
+
`;
|
|
3095
|
+
for (const c of cts) {
|
|
3096
|
+
css += ` --container-${c.name}: ${c.maxPx}px;
|
|
3097
|
+
`;
|
|
3098
|
+
}
|
|
3099
|
+
css += `
|
|
3100
|
+
`;
|
|
3101
|
+
for (const a of ars) {
|
|
3102
|
+
css += ` --aspect-${a.name}: ${formatAspect(a)};
|
|
3103
|
+
`;
|
|
3104
|
+
}
|
|
3105
|
+
css += `}
|
|
3106
|
+
`;
|
|
3107
|
+
return css;
|
|
3108
|
+
}
|
|
3109
|
+
function generateSpaceLlmBriefing(opts) {
|
|
3110
|
+
const { spacingTokens, breakpoints, fluidMinVw, fluidMaxVw, containers, proseMaxCh, aspectRatios, includeReciprocals, ratioLabel } = opts;
|
|
3111
|
+
const bps = sortBreakpoints(breakpoints);
|
|
3112
|
+
const cts = sortContainers(containers);
|
|
3113
|
+
const ars = expandAspects(aspectRatios, includeReciprocals);
|
|
3114
|
+
let md = `# Spacing & Layout \u2014 ${ratioLabel}
|
|
3115
|
+
|
|
3116
|
+
`;
|
|
3117
|
+
md += `## Spacing Scale
|
|
3118
|
+
|
|
3119
|
+
`;
|
|
3120
|
+
md += `| Token | rem | px |
|
|
3121
|
+
`;
|
|
3122
|
+
md += `|-------|-----|----|
|
|
3123
|
+
`;
|
|
3124
|
+
for (const t of spacingTokens) {
|
|
3125
|
+
md += `| --space-${t.name} | ${t.rem}rem | ${t.px}px |
|
|
3126
|
+
`;
|
|
3127
|
+
}
|
|
3128
|
+
md += `
|
|
3129
|
+
## Breakpoints
|
|
3130
|
+
|
|
3131
|
+
`;
|
|
3132
|
+
md += `| Name | Min Width |
|
|
3133
|
+
`;
|
|
3134
|
+
md += `|------|-----------|
|
|
3135
|
+
`;
|
|
3136
|
+
for (const b of bps) md += `| ${b.name} | ${b.minPx}px |
|
|
3137
|
+
`;
|
|
3138
|
+
md += `
|
|
3139
|
+
Fluid viewport anchors: ${fluidMinVw}px (min) \u2192 ${fluidMaxVw}px (max).
|
|
3140
|
+
`;
|
|
3141
|
+
md += `
|
|
3142
|
+
## Containers
|
|
3143
|
+
|
|
3144
|
+
`;
|
|
3145
|
+
md += `| Name | Max Width |
|
|
3146
|
+
`;
|
|
3147
|
+
md += `|------|-----------|
|
|
3148
|
+
`;
|
|
3149
|
+
for (const c of cts) md += `| ${c.name} | ${c.maxPx}px |
|
|
3150
|
+
`;
|
|
3151
|
+
md += `
|
|
3152
|
+
Prose reading column: ${proseMaxCh}ch.
|
|
3153
|
+
`;
|
|
3154
|
+
md += `
|
|
3155
|
+
## Aspect Ratios
|
|
3156
|
+
|
|
3157
|
+
`;
|
|
3158
|
+
md += `| Name | Ratio |
|
|
3159
|
+
`;
|
|
3160
|
+
md += `|------|-------|
|
|
3161
|
+
`;
|
|
3162
|
+
for (const a of ars) md += `| ${a.name} | ${formatAspect(a)} |
|
|
3163
|
+
`;
|
|
3164
|
+
md += `
|
|
3165
|
+
## Usage
|
|
3166
|
+
|
|
3167
|
+
`;
|
|
3168
|
+
md += `Use the CSS custom properties from the CSS or Tailwind export.
|
|
3169
|
+
`;
|
|
3170
|
+
md += `Spacing tokens follow a ${opts.ratioLabel} scale suitable for margins, padding, and gap values.
|
|
3171
|
+
`;
|
|
3172
|
+
md += `Combine with color tokens from standby.design/color, type tokens from standby.design/type, and shape tokens from standby.design/shape.
|
|
3173
|
+
`;
|
|
3174
|
+
if (includeReciprocals) {
|
|
3175
|
+
md += `
|
|
3176
|
+
*Portrait variants (-portrait suffix) are included for non-square ratios.*
|
|
3177
|
+
`;
|
|
3178
|
+
}
|
|
3179
|
+
return md;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
// src/tools-system.ts
|
|
3183
|
+
var ALL_SECTIONS = ["color", "type", "space", "shape", "symbol"];
|
|
3184
|
+
function getScaleLabel(typeState) {
|
|
3185
|
+
if (typeState.scaleMode === "traditional") return "Traditional Scale";
|
|
3186
|
+
const r = typeState.customRatio;
|
|
3187
|
+
if (Math.abs(r - 1.272) < 1e-3) return "Custom Ratio (\u221A\u03C6 \u2248 1.272)";
|
|
3188
|
+
return `Custom Ratio (${r})`;
|
|
3189
|
+
}
|
|
3190
|
+
function getSpaceRatioLabel(s) {
|
|
3191
|
+
if (s.spacingMode === "geometric" && Math.abs(s.spacingRatio - 1.272) < 1e-3) {
|
|
3192
|
+
return "\u221A\u03C6 Golden Ratio";
|
|
3193
|
+
}
|
|
3194
|
+
if (s.spacingMode === "geometric") {
|
|
3195
|
+
return `Geometric \xD7${s.spacingRatio.toFixed(3)}`;
|
|
3196
|
+
}
|
|
3197
|
+
return "Harmonic multiples";
|
|
3198
|
+
}
|
|
3199
|
+
function symbolTokenLines(sym) {
|
|
3200
|
+
const { set } = resolveIconSet(sym);
|
|
3201
|
+
const tokens = computeIconTokens(sym.iconBaseSize, sym.iconScale, weightToStroke(set.strokeWeight), sym.snapTo4px);
|
|
3202
|
+
return {
|
|
3203
|
+
header: [
|
|
3204
|
+
`/* Icon Tokens \u2014 standby.design/symbol */`,
|
|
3205
|
+
`/* Recommended: ${set.name} (${set.id}) */`
|
|
3206
|
+
],
|
|
3207
|
+
tokens: [
|
|
3208
|
+
...tokens.sizes.map((s) => ` --icon-${s.name}: ${s.rem}rem;`),
|
|
3209
|
+
` --icon-stroke: ${tokens.strokeWidth}px;`
|
|
3210
|
+
]
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
function generateSymbolCss(sym) {
|
|
3214
|
+
const { header: header2, tokens } = symbolTokenLines(sym);
|
|
3215
|
+
return [...header2, ":root {", ...tokens, "}"].join("\n");
|
|
3216
|
+
}
|
|
3217
|
+
function generateSymbolTailwind(sym) {
|
|
3218
|
+
const { header: header2, tokens } = symbolTokenLines(sym);
|
|
3219
|
+
return [...header2, "@theme {", ...tokens, "}"].join("\n");
|
|
3220
|
+
}
|
|
3221
|
+
function generateSymbolLlmBriefing(sym) {
|
|
3222
|
+
const { set } = resolveIconSet(sym);
|
|
3223
|
+
const tokens = computeIconTokens(sym.iconBaseSize, sym.iconScale, weightToStroke(set.strokeWeight), sym.snapTo4px);
|
|
3224
|
+
return [
|
|
3225
|
+
`# Icon System \u2014 standby.design/symbol`,
|
|
3226
|
+
``,
|
|
3227
|
+
`**Recommended:** ${set.name} \u2014 ${set.description}`,
|
|
3228
|
+
`- Install: \`npm install ${set.npmPackage}\``,
|
|
3229
|
+
`- Style: ${set.style}, Weight: ${set.strokeWeight}, Corners: ${set.cornerStyle}`,
|
|
3230
|
+
``,
|
|
3231
|
+
`| Token | Value |`,
|
|
3232
|
+
`|-------|-------|`,
|
|
3233
|
+
...tokens.sizes.map((s) => `| --icon-${s.name} | ${s.rem}rem (${s.px}px) |`),
|
|
3234
|
+
`| --icon-stroke | ${tokens.strokeWidth}px |`
|
|
3235
|
+
].join("\n");
|
|
3236
|
+
}
|
|
3237
|
+
function registerSystemTools(server2) {
|
|
3238
|
+
server2.registerTool(
|
|
3239
|
+
"get_design_system",
|
|
3240
|
+
{
|
|
3241
|
+
title: "Inspect design system",
|
|
3242
|
+
description: "Decode a standby.design URL (or raw hash) and return an overview of the full design system: color palette, type scale, spacing & layout, shape tokens, and icons \u2014 plus per-tool edit links.",
|
|
3243
|
+
inputSchema: {
|
|
3244
|
+
url: z2.string().describe("A standby.design URL or raw unified hash (e.g. from a previous generate_* call or copied from the browser).")
|
|
3245
|
+
},
|
|
3246
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3247
|
+
},
|
|
3248
|
+
async (args) => {
|
|
3249
|
+
const segs = parseInput(args.url);
|
|
3250
|
+
if (!segs.c && !segs.t && !segs.s && !segs.y && !segs.p) {
|
|
3251
|
+
return errorResult("No design-system configuration found in that URL/hash. Expected a unified hash like #c=...&t=...&s=...&y=...&p=...");
|
|
3252
|
+
}
|
|
3253
|
+
const colorState = colorStateFrom(segs);
|
|
3254
|
+
const typeState = typeStateFrom(segs);
|
|
3255
|
+
const shapeState = shapeStateFrom(segs);
|
|
3256
|
+
const symbolState = symbolStateFrom(segs);
|
|
3257
|
+
const spaceState = spaceStateFrom(segs);
|
|
3258
|
+
const parts = [
|
|
3259
|
+
`Design system: ${systemUrl(segs)}`,
|
|
3260
|
+
"",
|
|
3261
|
+
`Configured sections: ${["c", "t", "s", "y", "p"].filter((k) => segs[k]).map((k) => ({ c: "color", t: "type", s: "shape", y: "symbol", p: "space" })[k]).join(", ")} (missing sections shown with defaults)`,
|
|
3262
|
+
"",
|
|
3263
|
+
colorSummary(colorState, buildPalette(colorState)),
|
|
3264
|
+
`Edit: ${toolUrl("color", segs)}`,
|
|
3265
|
+
"",
|
|
3266
|
+
typeSummary(typeState, buildScale(typeState)),
|
|
3267
|
+
`Edit: ${toolUrl("type", segs)}`,
|
|
3268
|
+
"",
|
|
3269
|
+
spaceSummary(spaceState, buildSpacing(spaceState)),
|
|
3270
|
+
`Edit: ${toolUrl("space", segs)}`,
|
|
3271
|
+
"",
|
|
3272
|
+
shapeSummary(shapeState),
|
|
3273
|
+
`Edit: ${toolUrl("shape", segs)}`,
|
|
3274
|
+
"",
|
|
3275
|
+
symbolState ? symbolSummary(symbolState) : "## Icons \u2014 not configured (use generate_icon_tokens)",
|
|
3276
|
+
symbolState ? `Edit: ${toolUrl("symbol", segs)}` : ""
|
|
3277
|
+
];
|
|
3278
|
+
return textResult(parts.filter((p) => p !== "").join("\n").replace(/\n{3,}/g, "\n\n"));
|
|
3279
|
+
}
|
|
3280
|
+
);
|
|
3281
|
+
server2.registerTool(
|
|
3282
|
+
"export_design_system",
|
|
3283
|
+
{
|
|
3284
|
+
title: "Export design system code",
|
|
3285
|
+
description: 'Generate the full token code for a design system URL in one format: "css" (CSS custom properties incl. semantic shadcn/ui-compatible tokens), "tailwind" (Tailwind v4 @theme), "design-tokens" (W3C DTCG JSON \u2014 typography & spacing), "llm-briefing" (Markdown brief for AI code generation), or "font-embed" (Fontshare <link> snippet). Optionally restrict to specific sections.',
|
|
3286
|
+
inputSchema: {
|
|
3287
|
+
url: z2.string().describe("A standby.design URL or raw unified hash."),
|
|
3288
|
+
format: z2.enum(["css", "tailwind", "design-tokens", "llm-briefing", "font-embed"]).describe("Output format."),
|
|
3289
|
+
sections: z2.array(z2.enum(["color", "type", "space", "shape", "symbol"])).optional().describe("Which sections to include. Default: all configured sections (symbol only when configured).")
|
|
3290
|
+
},
|
|
3291
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3292
|
+
},
|
|
3293
|
+
async (args) => {
|
|
3294
|
+
const segs = parseInput(args.url);
|
|
3295
|
+
const sections = args.sections ?? ALL_SECTIONS.filter((s) => s !== "symbol" || segs.y !== null);
|
|
3296
|
+
const colorState = colorStateFrom(segs);
|
|
3297
|
+
const typeState = typeStateFrom(segs);
|
|
3298
|
+
const shapeState = shapeStateFrom(segs);
|
|
3299
|
+
const symbolState = symbolStateFrom(segs) ?? (sections.includes("symbol") ? { ...DEFAULT_SYMBOL_STATE } : null);
|
|
3300
|
+
const spaceState = spaceStateFrom(segs);
|
|
3301
|
+
const palette = buildPalette(colorState);
|
|
3302
|
+
const scale = buildScale(typeState);
|
|
3303
|
+
const spacing = buildSpacing(spaceState);
|
|
3304
|
+
const spaceOpts = {
|
|
3305
|
+
spacingTokens: spacing,
|
|
3306
|
+
breakpoints: spaceState.breakpoints,
|
|
3307
|
+
fluidMinVw: spaceState.fluidMinVw,
|
|
3308
|
+
fluidMaxVw: spaceState.fluidMaxVw,
|
|
3309
|
+
containers: spaceState.containers,
|
|
3310
|
+
proseMaxCh: spaceState.proseMaxCh,
|
|
3311
|
+
aspectRatios: spaceState.aspectRatios,
|
|
3312
|
+
includeReciprocals: spaceState.aspectIncludeReciprocals,
|
|
3313
|
+
ratioLabel: getSpaceRatioLabel(spaceState)
|
|
3314
|
+
};
|
|
3315
|
+
const typeOpts = {
|
|
3316
|
+
levels: scale,
|
|
3317
|
+
spacingTokens: [],
|
|
3318
|
+
headingFont: typeState.headingFont,
|
|
3319
|
+
bodyFont: typeState.bodyFont,
|
|
3320
|
+
monoFont: typeState.monoFont,
|
|
3321
|
+
scaleLabel: getScaleLabel(typeState)
|
|
3322
|
+
};
|
|
3323
|
+
const colorCss = () => {
|
|
3324
|
+
let css = generateSeedComment(colorState, palette.effectiveBgHex, palette.effectiveErrorHex);
|
|
3325
|
+
css += generatePrimitivesOklch(
|
|
3326
|
+
palette.brand,
|
|
3327
|
+
palette.surface,
|
|
3328
|
+
palette.error,
|
|
3329
|
+
palette.errorSurface,
|
|
3330
|
+
palette.neutralExtended,
|
|
3331
|
+
palette.accentPalettes,
|
|
3332
|
+
colorState.chromaScale,
|
|
3333
|
+
colorState.bgAutoMatch ? null : colorState.bgColorHex,
|
|
3334
|
+
colorState.themeName
|
|
3335
|
+
);
|
|
3336
|
+
css += "\n";
|
|
3337
|
+
css += generateSemantic(
|
|
3338
|
+
palette.accentPalettes,
|
|
3339
|
+
palette.brand,
|
|
3340
|
+
palette.error,
|
|
3341
|
+
palette.errorSurface,
|
|
3342
|
+
palette.surface,
|
|
3343
|
+
colorState.brandPin,
|
|
3344
|
+
colorState.brandPin ? colorState.brandHex : null,
|
|
3345
|
+
colorState.brandInvert,
|
|
3346
|
+
colorState.errorPin,
|
|
3347
|
+
colorState.errorPin ? palette.effectiveErrorHex : null,
|
|
3348
|
+
colorState.errorInvert,
|
|
3349
|
+
colorState.fgContrastMode,
|
|
3350
|
+
colorState.themeName
|
|
3351
|
+
);
|
|
3352
|
+
return css;
|
|
3353
|
+
};
|
|
3354
|
+
const has = (s) => sections.includes(s);
|
|
3355
|
+
let output;
|
|
3356
|
+
switch (args.format) {
|
|
3357
|
+
case "css":
|
|
3358
|
+
output = [
|
|
3359
|
+
has("color") ? colorCss() : "",
|
|
3360
|
+
has("type") ? generateCssExport(typeOpts) : "",
|
|
3361
|
+
has("space") ? generateSpaceCss(spaceOpts) : "",
|
|
3362
|
+
has("shape") ? generateShapeCss(optsFromState(shapeState, palette.effectiveBgHex)) : "",
|
|
3363
|
+
has("symbol") && symbolState ? generateSymbolCss(symbolState) : ""
|
|
3364
|
+
].filter(Boolean).join("\n") || "/* No sections selected */";
|
|
3365
|
+
break;
|
|
3366
|
+
case "tailwind":
|
|
3367
|
+
output = [
|
|
3368
|
+
// Color primitives + semantic tokens are plain CSS variables and identical in both formats.
|
|
3369
|
+
has("color") ? colorCss() : "",
|
|
3370
|
+
has("type") ? generateTailwindV4Export(typeOpts) : "",
|
|
3371
|
+
has("space") ? generateSpaceTailwind(spaceOpts) : "",
|
|
3372
|
+
has("shape") ? generateShapeTailwind(optsFromState(shapeState, palette.effectiveBgHex)) : "",
|
|
3373
|
+
has("symbol") && symbolState ? generateSymbolTailwind(symbolState) : ""
|
|
3374
|
+
].filter(Boolean).join("\n") || "/* No sections selected */";
|
|
3375
|
+
break;
|
|
3376
|
+
case "design-tokens":
|
|
3377
|
+
output = generateDesignTokens({
|
|
3378
|
+
levels: scale,
|
|
3379
|
+
spacingTokens: spacing,
|
|
3380
|
+
headingFont: typeState.headingFont,
|
|
3381
|
+
bodyFont: typeState.bodyFont,
|
|
3382
|
+
monoFont: typeState.monoFont,
|
|
3383
|
+
headingWeight: typeState.headingWeight
|
|
3384
|
+
});
|
|
3385
|
+
output += '\n\n/* Note: the DTCG export covers typography & spacing (matching the web app). Use format "css" or "tailwind" for color and shape tokens. */';
|
|
3386
|
+
break;
|
|
3387
|
+
case "llm-briefing": {
|
|
3388
|
+
const parts = [];
|
|
3389
|
+
if (has("color")) {
|
|
3390
|
+
parts.push(generateLlmBriefing(
|
|
3391
|
+
colorState.brandHex,
|
|
3392
|
+
palette.effectiveBgHex,
|
|
3393
|
+
palette.effectiveErrorHex,
|
|
3394
|
+
palette.accentPalettes,
|
|
3395
|
+
colorState.chromaScale,
|
|
3396
|
+
colorState.currentMode,
|
|
3397
|
+
colorState.brandPin,
|
|
3398
|
+
colorState.errorPin,
|
|
3399
|
+
colorState.themeName,
|
|
3400
|
+
colorState.fgContrastMode
|
|
3401
|
+
));
|
|
3402
|
+
}
|
|
3403
|
+
if (has("type")) parts.push(generateLlmBriefing2(typeOpts));
|
|
3404
|
+
if (has("space")) parts.push(generateSpaceLlmBriefing(spaceOpts));
|
|
3405
|
+
if (has("shape")) parts.push(generateLlmBriefing3(optsFromState(shapeState, palette.effectiveBgHex)));
|
|
3406
|
+
if (has("symbol") && symbolState) parts.push(generateSymbolLlmBriefing(symbolState));
|
|
3407
|
+
output = parts.join("\n---\n\n") || "<!-- No sections selected -->";
|
|
3408
|
+
break;
|
|
3409
|
+
}
|
|
3410
|
+
case "font-embed":
|
|
3411
|
+
output = generateFontEmbed(typeState.headingFont, typeState.bodyFont, typeState.monoFont) || "<!-- No fonts selected -->";
|
|
3412
|
+
break;
|
|
3413
|
+
}
|
|
3414
|
+
return textResult(`Design system: ${systemUrl(segs)}
|
|
3415
|
+
|
|
3416
|
+
${output}`);
|
|
3417
|
+
}
|
|
3418
|
+
);
|
|
3419
|
+
server2.registerTool(
|
|
3420
|
+
"list_fonts",
|
|
3421
|
+
{
|
|
3422
|
+
title: "List available fonts",
|
|
3423
|
+
description: "List Fontshare font slugs usable in generate_type_scale (headingFont/bodyFont/monoFont), grouped by category. This is the bundled seed catalog \u2014 any valid Fontshare slug works even if not listed here.",
|
|
3424
|
+
inputSchema: {},
|
|
3425
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
3426
|
+
},
|
|
3427
|
+
async () => {
|
|
3428
|
+
void getCatalog();
|
|
3429
|
+
const byCategory = fontsByCategory();
|
|
3430
|
+
const lines = [];
|
|
3431
|
+
for (const [category, fonts] of Object.entries(byCategory)) {
|
|
3432
|
+
lines.push(`## ${category}`);
|
|
3433
|
+
for (const f of fonts) lines.push(`- ${f.slug} \u2014 ${f.name}`);
|
|
3434
|
+
lines.push("");
|
|
3435
|
+
}
|
|
3436
|
+
lines.push("Any other Fontshare slug (fontshare.com) is also accepted.");
|
|
3437
|
+
return textResult(lines.join("\n"));
|
|
3438
|
+
}
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// src/index.ts
|
|
3443
|
+
var server = new McpServer(
|
|
3444
|
+
{ name: "standby-design", version: "0.1.0" },
|
|
3445
|
+
{
|
|
3446
|
+
instructions: [
|
|
3447
|
+
"Tools for generating complete design systems with standby.design.",
|
|
3448
|
+
"Typical flow: generate_color_palette \u2192 generate_type_scale \u2192 generate_shape_tokens / generate_icon_tokens / generate_space_tokens, passing the returned URL into each subsequent call so the sections accumulate. Every result includes a shareable standby.design/system URL the user can open to view and fine-tune the system visually.",
|
|
3449
|
+
"Use export_design_system to get the full CSS / Tailwind v4 / W3C design tokens / LLM briefing for a URL, and get_design_system to inspect an existing URL.",
|
|
3450
|
+
"All tools are pure and deterministic \u2014 no network access, no side effects."
|
|
3451
|
+
].join("\n")
|
|
3452
|
+
}
|
|
3453
|
+
);
|
|
3454
|
+
registerGenerateTools(server);
|
|
3455
|
+
registerSystemTools(server);
|
|
3456
|
+
var transport = new StdioServerTransport();
|
|
3457
|
+
await server.connect(transport);
|
|
3458
|
+
console.error("standby-design MCP server running on stdio");
|