@tenphi/docs 0.17.0 → 0.18.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.
@@ -0,0 +1,2005 @@
1
+ import { apcaContrast, contrastRatioFromLuminance, glaze, inferRoleFromName, normalizeRole, okhslToSrgb, oppositeRole, resolveContrastForLevel, variantToOkhsl } from "@tenphi/glaze";
2
+ import { mergeStyles } from "@tenphi/tasty/core";
3
+ //#region src/table-of-contents.ts
4
+ function validateTableOfContents(value) {
5
+ if (value === false) return [];
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) return ["tableOfContents must be false or an object."];
7
+ const config = value;
8
+ const errors = [];
9
+ for (const key of Object.keys(config)) if (![
10
+ "minHeadingLevel",
11
+ "maxHeadingLevel",
12
+ "mobile"
13
+ ].includes(key)) errors.push(`tableOfContents.${key} is not supported.`);
14
+ for (const key of ["minHeadingLevel", "maxHeadingLevel"]) if (config[key] !== void 0 && (!Number.isInteger(config[key]) || Number(config[key]) < 1 || Number(config[key]) > 6)) errors.push(`tableOfContents.${key} must be an integer from 1 to 6.`);
15
+ if (config.mobile !== void 0 && typeof config.mobile !== "boolean") errors.push("tableOfContents.mobile must be a boolean.");
16
+ if (typeof config.minHeadingLevel === "number" && typeof config.maxHeadingLevel === "number" && config.minHeadingLevel > config.maxHeadingLevel) errors.push("tableOfContents.minHeadingLevel cannot exceed maxHeadingLevel.");
17
+ return errors;
18
+ }
19
+ //#endregion
20
+ //#region src/publishing.ts
21
+ function routeUrl(route, base = "/", site) {
22
+ const path = `${base.replace(/\/$/, "")}${route === "/" ? "/" : `${route}/`}`;
23
+ return site ? new URL(path, site).href : path;
24
+ }
25
+ /** Version roots follow any locale prefix, e.g. /fr/v1/guide. */
26
+ function routeVersion(route, config) {
27
+ const first = route.split("/")[1];
28
+ const path = first && first !== "root" && config.locales?.[first] ? route.slice(first.length + 1) || "/" : route;
29
+ return [...config.site?.versions ?? []].sort((a, b) => b.routeBase.length - a.routeBase.length).find((v) => v.routeBase === "/" || path === v.routeBase || path.startsWith(`${v.routeBase}/`));
30
+ }
31
+ function pagePublishing(entry, config) {
32
+ const seo = config.site?.seo;
33
+ const page = entry.frontmatter.seo;
34
+ const version = routeVersion(entry.route, config);
35
+ const index = !entry.frontmatter.draft && seo?.index !== false && version?.index !== false && page?.index !== false;
36
+ const url = routeUrl(entry.route, config.build?.base, config.site?.url);
37
+ const canonical = page?.canonical ?? url;
38
+ const siteTitle = config.site?.title ?? "Documentation";
39
+ const title = page?.title ?? (seo?.titleTemplate ? seo.titleTemplate.replaceAll("{title}", entry.title).replaceAll("{site}", siteTitle) : entry.title === siteTitle ? entry.title : `${entry.title} | ${siteTitle}`);
40
+ return {
41
+ index,
42
+ sitemap: index && canonical === url,
43
+ canonical,
44
+ title,
45
+ version,
46
+ image: page?.image ?? seo?.image
47
+ };
48
+ }
49
+ function validateSeo(value, kind) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) return ["seo must be an object."];
51
+ const seo = value;
52
+ const errors = [];
53
+ const allowed = kind === "site" ? [
54
+ "titleTemplate",
55
+ "image",
56
+ "index",
57
+ "breadcrumbs",
58
+ "copyPage"
59
+ ] : [
60
+ "title",
61
+ "canonical",
62
+ "image",
63
+ "index",
64
+ "breadcrumbs"
65
+ ];
66
+ for (const key of Object.keys(seo)) if (!allowed.includes(key)) errors.push(`seo.${key} is not supported.`);
67
+ for (const key of ["index", "copyPage"]) if (seo[key] !== void 0 && typeof seo[key] !== "boolean") errors.push(`seo.${key} must be a boolean.`);
68
+ if (seo.title !== void 0 && (typeof seo.title !== "string" || !seo.title.trim())) errors.push("seo.title must be non-empty text.");
69
+ if (seo.titleTemplate !== void 0 && (typeof seo.titleTemplate !== "string" || !seo.titleTemplate.includes("{title}") || /\{(?!title\}|site\})[^}]*\}/.test(seo.titleTemplate))) errors.push("seo.titleTemplate must include {title} and may include {site}; other placeholders are not supported.");
70
+ if (seo.canonical !== void 0 && !absoluteUrl(seo.canonical)) errors.push("seo.canonical must be an absolute HTTP(S) URL without credentials or a fragment.");
71
+ if (seo.image !== void 0 && seo.image !== false) {
72
+ const image = seo.image;
73
+ if (!image || typeof image !== "object" || Array.isArray(image)) errors.push("seo.image must be false or {src, alt, width?, height?}.");
74
+ else {
75
+ const img = image;
76
+ for (const key of Object.keys(img)) if (![
77
+ "src",
78
+ "alt",
79
+ "width",
80
+ "height"
81
+ ].includes(key)) errors.push(`seo.image.${key} is not supported.`);
82
+ if (!absoluteUrl(img.src) && !(typeof img.src === "string" && /^\/(?!\/)[^\\\s?#]+$/.test(img.src) && !img.src.split("/").some((x) => x === ".." || x === "."))) errors.push("seo.image.src must be an HTTP(S) URL or root-relative public asset path.");
83
+ if (typeof img.alt !== "string" || !img.alt.trim()) errors.push("seo.image.alt must be non-empty text.");
84
+ for (const key of ["width", "height"]) if (img[key] !== void 0 && (!Number.isInteger(img[key]) || Number(img[key]) <= 0)) errors.push(`seo.image.${key} must be a positive integer.`);
85
+ if (img.width === void 0 !== (img.height === void 0)) errors.push("seo.image requires both width and height when dimensions are provided.");
86
+ }
87
+ }
88
+ if (seo.breadcrumbs !== void 0) {
89
+ if (kind === "site") {
90
+ if (typeof seo.breadcrumbs !== "boolean") errors.push("seo.breadcrumbs must be a boolean.");
91
+ } else if (seo.breadcrumbs !== false) {
92
+ if (!Array.isArray(seo.breadcrumbs) || seo.breadcrumbs.length < 2) errors.push("seo.breadcrumbs must be false or at least two {name, url} items.");
93
+ else for (const item of seo.breadcrumbs) if (!item || typeof item !== "object" || Object.keys(item).some((k) => k !== "name" && k !== "url") || typeof item.name !== "string" || !item.name.trim() || !absoluteUrl(item.url)) errors.push("Each seo.breadcrumbs item needs a name and absolute HTTP(S) url.");
94
+ }
95
+ }
96
+ return errors;
97
+ }
98
+ function absoluteUrl(value) {
99
+ if (typeof value !== "string" || /[\s\\]/.test(value)) return false;
100
+ try {
101
+ const u = new URL(value);
102
+ return /^https?:$/.test(u.protocol) && !u.hash && !u.username && !u.password;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ //#endregion
108
+ //#region src/theme/contrast.ts
109
+ const COLOR_MODES = [
110
+ "light",
111
+ "dark",
112
+ "lightContrast",
113
+ "darkContrast"
114
+ ];
115
+ function rgb(color) {
116
+ const { h, s, l } = variantToOkhsl(color);
117
+ return okhslToSrgb(h, s, l, color.pastel);
118
+ }
119
+ function composite(foreground, background) {
120
+ const alpha = foreground.alpha ?? 1;
121
+ return rgb(foreground).map((channel, i) => channel * alpha + background[i] * (1 - alpha));
122
+ }
123
+ function luminance(channels, metric) {
124
+ return channels.reduce((sum, channel, i) => sum + [
125
+ .2126,
126
+ .7152,
127
+ .0722
128
+ ][i] * (metric === "apca" ? channel ** 2.4 : channel <= .04045 ? channel / 12.92 : ((channel + .055) / 1.055) ** 2.4), 0);
129
+ }
130
+ function measureColorContrast(foreground, background, metric = "apca", underlay = background) {
131
+ const bg = composite(background, rgb(underlay));
132
+ const yForeground = luminance(composite(foreground, bg), metric);
133
+ const yBackground = luminance(bg, metric);
134
+ return metric === "apca" ? Math.abs(apcaContrast(yForeground, yBackground)) : contrastRatioFromLuminance(yForeground, yBackground);
135
+ }
136
+ function checkColorContrast(colors, definitions, level, brandTargets, inferRole = true) {
137
+ const pairs = /* @__PURE__ */ new Map();
138
+ const add = (foreground, background, spec) => {
139
+ const metric = resolveContrastForLevel(spec, 0).metric;
140
+ const key = `${foreground}/${background}/${metric}`;
141
+ const existing = pairs.get(key);
142
+ if (!existing) pairs.set(key, {
143
+ foreground,
144
+ background,
145
+ spec
146
+ });
147
+ else {
148
+ const targets = [0, 100].map((value) => Math.max(resolveContrastForLevel(spec, value).target, resolveContrastForLevel(existing.spec, value).target));
149
+ existing.spec = metric === "apca" ? { apca: targets } : { wcag: targets };
150
+ }
151
+ };
152
+ const roles = /* @__PURE__ */ new Map();
153
+ function role(name) {
154
+ const cached = roles.get(name);
155
+ if (cached) return cached;
156
+ const definition = definitions[name];
157
+ const explicit = "role" in definition ? normalizeRole(definition.role) : void 0;
158
+ const inferred = inferRole ? inferRoleFromName(name) : void 0;
159
+ const value = explicit ?? inferred ?? ("base" in definition && definition.base ? oppositeRole(role(definition.base)) : "text");
160
+ roles.set(name, value);
161
+ return value;
162
+ }
163
+ for (const [name, definition] of Object.entries(definitions)) {
164
+ if ("type" in definition || !definition.base || !definition.contrast) continue;
165
+ const isSurface = role(name) === "surface";
166
+ add(isSurface ? definition.base : name, isSurface ? name : definition.base, definition.contrast);
167
+ }
168
+ for (const role of [
169
+ "text",
170
+ "heading",
171
+ "text-soft"
172
+ ]) add(role, "surface", { wcag: [4.5, 7] });
173
+ add("accent-text", "surface", { apca: brandTargets });
174
+ for (const background of [
175
+ "surface",
176
+ "surface-2",
177
+ "surface-3",
178
+ "accent-surface-subtle",
179
+ "accent-surface-2-subtle"
180
+ ]) add("accent-text", background, { wcag: [4.5, 7] });
181
+ add("accent-surface-text", "accent-surface", { wcag: [4.5, 7] });
182
+ add("accent-surface-text", "accent-surface", { apca: [60, 75] });
183
+ add("logo-mark", "logo-surface", { wcag: [3, 4.5] });
184
+ for (const background of [
185
+ "surface",
186
+ "surface-2",
187
+ "surface-3"
188
+ ]) add("focus", background, { wcag: [3, 4.5] });
189
+ for (const role of [
190
+ "info",
191
+ "success",
192
+ "warning",
193
+ "danger",
194
+ "orange",
195
+ "green",
196
+ "blue",
197
+ "purple",
198
+ "red"
199
+ ]) {
200
+ add(`${role}-text`, `${role}-surface`, { apca: [60, 75] });
201
+ add(`${role}-text`, `${role}-surface`, { wcag: [4.5, 7] });
202
+ }
203
+ const checks = [];
204
+ const diagnostics = [];
205
+ for (const { foreground, background, spec } of pairs.values()) for (const mode of COLOR_MODES) {
206
+ const { metric, target } = resolveContrastForLevel(spec, mode.includes("Contrast") ? 100 : typeof level === "number" ? level : 0);
207
+ const measured = measureColorContrast(colors.get(foreground)[mode], colors.get(background)[mode], metric, colors.get("surface")[mode]);
208
+ const passed = measured + (metric === "apca" ? .05 : .01) >= target;
209
+ checks.push({
210
+ foreground,
211
+ background,
212
+ mode,
213
+ metric,
214
+ target,
215
+ measured,
216
+ passed
217
+ });
218
+ if (!passed) diagnostics.push({
219
+ code: foreground === "accent-text" && background === "surface" ? "DOCS_BRAND_CONTRAST_UNMET" : "DOCS_SEMANTIC_CONTRAST_UNMET",
220
+ severity: "error",
221
+ message: `${foreground} on ${background} in ${mode}: ${metric === "apca" ? "APCA Lc" : "WCAG ratio"} ${measured.toFixed(2)}; required ${target}.`,
222
+ hint: `Adjust theme.palette["${foreground}"] or theme.palette["${background}"] using tone, saturation, base, contrast, and autoFlip. Keep their contrast floor; a fixed mid-tone background may need to adapt. Also check theme.glaze tone boundaries and opacity.`
223
+ });
224
+ }
225
+ return {
226
+ checks,
227
+ diagnostics
228
+ };
229
+ }
230
+ //#endregion
231
+ //#region src/theme/palette.ts
232
+ function resolveColorTheme(theme = {}) {
233
+ const brand = normalizeBrand(theme.brand);
234
+ const authoredTarget = brand.contrast?.apca ?? 45;
235
+ const normalTarget = Array.isArray(authoredTarget) ? authoredTarget[0] : authoredTarget;
236
+ const highTarget = Array.isArray(authoredTarget) ? authoredTarget[1] : normalTarget + 15;
237
+ const glazeOptions = {
238
+ autoFlip: true,
239
+ ...theme.glaze,
240
+ ...theme.contrastLevel !== void 0 ? { contrastLevel: theme.contrastLevel } : {}
241
+ };
242
+ const surfaceInput = theme.palette?.surface;
243
+ const declaredSurface = isColorDeclaration(surfaceInput) && usesRelativeColor(surfaceInput);
244
+ const surfaceFrom = isColorDeclaration(surfaceInput) ? surfaceInput.from ?? brand.from : isSpecialDefinition(surfaceInput) ? "#ffffff" : surfaceInput ?? "#ffffff";
245
+ const resolvedSurfaceSeed = glaze.color({
246
+ from: surfaceFrom,
247
+ mode: "auto",
248
+ darkSaturation: .35
249
+ }).resolve();
250
+ const resolvedThemeSeed = glaze.color({
251
+ from: brand.from,
252
+ mode: "auto"
253
+ }, glazeOptions).resolve();
254
+ const lightThemeSeed = variantToOkhsl(resolvedThemeSeed.light);
255
+ const darkThemeSeed = variantToOkhsl(resolvedThemeSeed.dark);
256
+ const darkThemeSaturation = darkThemeSeed.s * 100;
257
+ const colorTheme = glaze({
258
+ hue: lightThemeSeed.h,
259
+ saturation: lightThemeSeed.s * 100,
260
+ darkHue: darkThemeSeed.h,
261
+ darkSaturation: darkThemeSaturation
262
+ }, void 0, glazeOptions);
263
+ const surfaceSaturation = declaredSurface ? surfaceInput.saturation ?? .05 : 1;
264
+ const darkSurfaceSaturation = declaredSurface ? surfaceInput.darkSaturation ?? surfaceSaturation * .7 : .35;
265
+ const darkSurfaceRampSaturation = (factor) => declaredSurface ? darkSurfaceSaturation * factor : saturationFactor(resolvedSurfaceSeed.dark.s * factor / .35, darkThemeSaturation / 100);
266
+ const surfaceRampHue = declaredSurface ? {
267
+ hue: surfaceInput.hue ?? lightThemeSeed.h,
268
+ darkHue: surfaceInput.darkHue ?? surfaceInput.hue ?? darkThemeSeed.h
269
+ } : {
270
+ hue: variantToOkhsl(resolvedSurfaceSeed.light).h,
271
+ darkHue: variantToOkhsl(resolvedSurfaceSeed.dark).h
272
+ };
273
+ const surfaceDefinition = paletteDefinition(surfaceInput, declaredSurface ? {
274
+ tone: 98,
275
+ saturation: surfaceSaturation,
276
+ darkSaturation: darkSurfaceSaturation,
277
+ mode: "auto"
278
+ } : {
279
+ from: surfaceFrom,
280
+ mode: "auto",
281
+ darkSaturation: saturationFactor(resolvedSurfaceSeed.dark.s, darkThemeSaturation / 100)
282
+ });
283
+ const definitions = {
284
+ surface: surfaceDefinition,
285
+ header: paletteDefinition(theme.palette?.header, declaredSurface ? {
286
+ ...surfaceDefinition,
287
+ opacity: .7
288
+ } : {
289
+ from: surfaceFrom,
290
+ mode: "auto",
291
+ darkSaturation: saturationFactor(resolvedSurfaceSeed.dark.s, darkThemeSaturation / 100),
292
+ opacity: .7
293
+ }),
294
+ "surface-2": {
295
+ base: "surface",
296
+ ...surfaceRampHue,
297
+ tone: "-2",
298
+ mode: "auto",
299
+ ...declaredSurface ? { saturation: surfaceSaturation * .75 } : { from: {
300
+ h: variantToOkhsl(resolvedSurfaceSeed.light).h,
301
+ s: resolvedSurfaceSeed.light.s * .75,
302
+ t: 1
303
+ } },
304
+ darkSaturation: darkSurfaceRampSaturation(declaredSurface ? .75 : .275)
305
+ },
306
+ "surface-3": {
307
+ base: "surface-2",
308
+ ...surfaceRampHue,
309
+ tone: "-2",
310
+ mode: "auto",
311
+ ...declaredSurface ? { saturation: surfaceSaturation * .65 } : { from: {
312
+ h: variantToOkhsl(resolvedSurfaceSeed.light).h,
313
+ s: resolvedSurfaceSeed.light.s * .65,
314
+ t: 1
315
+ } },
316
+ darkSaturation: darkSurfaceRampSaturation(declaredSurface ? .65 : .25)
317
+ },
318
+ text: paletteDefinition(theme.palette?.text, {
319
+ tone: 0,
320
+ saturation: 0,
321
+ base: "surface",
322
+ role: "text",
323
+ contrast: { apca: [75, 90] },
324
+ mode: "auto"
325
+ }),
326
+ heading: paletteDefinition(theme.palette?.heading ?? theme.palette?.text, {
327
+ tone: [4, 0],
328
+ saturation: 0,
329
+ base: "surface",
330
+ role: "text",
331
+ contrast: { apca: [75, 90] },
332
+ mode: "auto"
333
+ }),
334
+ "text-soft": paletteDefinition(theme.palette?.textSoft, {
335
+ from: "#626875",
336
+ base: "surface",
337
+ role: "text",
338
+ contrast: { apca: [60, 85] },
339
+ mode: "auto"
340
+ }),
341
+ "text-muted": mix("surface", "text", 66),
342
+ "surface-2-hover": mix("surface-2", "text", [3, 6]),
343
+ "surface-2-pressed": mix("surface-2", "text", [9, 14]),
344
+ "surface-3-hover": mix("surface-3", "text", [3, 6]),
345
+ "surface-3-pressed": mix("surface-3", "text", [9, 14]),
346
+ "accent-text": {
347
+ from: brand.from,
348
+ base: "surface-3",
349
+ role: "text",
350
+ contrast: { apca: [Math.max(75, normalTarget), Math.max(90, highTarget)] },
351
+ mode: "auto"
352
+ },
353
+ focus: {
354
+ from: brand.from,
355
+ base: "surface-3",
356
+ role: "border",
357
+ contrast: { wcag: [3, 4.5] },
358
+ mode: "auto"
359
+ },
360
+ "accent-surface": {
361
+ from: brand.from,
362
+ base: "surface",
363
+ role: "text",
364
+ contrast: { apca: [75, 90] },
365
+ mode: "auto"
366
+ },
367
+ "accent-surface-text": {
368
+ from: "#ffffff",
369
+ base: "accent-surface",
370
+ role: "text",
371
+ contrast: { apca: [75, 90] },
372
+ mode: "auto"
373
+ },
374
+ "logo-surface": {
375
+ from: brand.from,
376
+ base: "logo-mark",
377
+ role: "surface",
378
+ contrast: { wcag: [3, 4.5] },
379
+ mode: "fixed"
380
+ },
381
+ "logo-mark": {
382
+ tone: 100,
383
+ saturation: 0,
384
+ mode: "fixed"
385
+ },
386
+ "accent-surface-subtle": mix("surface", "accent-surface", [12, 18]),
387
+ "accent-surface-2-subtle": mix("surface-2", "accent-surface", [12, 18]),
388
+ shadow: {
389
+ type: "shadow",
390
+ bg: "surface",
391
+ fg: "text",
392
+ intensity: [12, 20],
393
+ tuning: { alphaMax: theme.glaze?.shadowTuning?.alphaMax ?? .28 }
394
+ },
395
+ clear: {
396
+ from: "#ffffff",
397
+ mode: "fixed",
398
+ opacity: 0
399
+ },
400
+ ...statusColors("info", theme.palette?.info, "#2563eb"),
401
+ ...statusColors("success", theme.palette?.success, "#16a34a"),
402
+ ...statusColors("warning", theme.palette?.warning, "#d97706"),
403
+ ...statusColors("danger", theme.palette?.danger, "#dc2626"),
404
+ ...statusColors("orange", void 0, "#d97706"),
405
+ ...statusColors("green", void 0, "#16a34a"),
406
+ ...statusColors("blue", void 0, "#2563eb"),
407
+ ...statusColors("purple", void 0, "#9333ea"),
408
+ ...statusColors("red", void 0, "#dc2626")
409
+ };
410
+ definitions.overlay = paletteDefinition(theme.palette?.overlay, {
411
+ from: "#000000",
412
+ mode: "static",
413
+ opacity: .5
414
+ });
415
+ definitions.border = {
416
+ base: "surface",
417
+ tone: ["-9", "-22"],
418
+ saturation: .205,
419
+ mode: "auto"
420
+ };
421
+ definitions["border-strong"] = {
422
+ base: "surface",
423
+ tone: ["-20", "-38"],
424
+ saturation: .205,
425
+ mode: "auto"
426
+ };
427
+ definitions["syntax-bg"] = { from: {
428
+ h: 210,
429
+ s: .09,
430
+ t: 1
431
+ } };
432
+ definitions["syntax-text"] = {
433
+ base: "syntax-bg",
434
+ tone: 0,
435
+ saturation: 0,
436
+ contrast: { wcag: ["AA", "AAA"] }
437
+ };
438
+ for (const [name, hue, saturation, contrast] of [
439
+ [
440
+ "comment",
441
+ 210,
442
+ .009,
443
+ 4.5
444
+ ],
445
+ [
446
+ "punctuation",
447
+ 210,
448
+ .009,
449
+ 6
450
+ ],
451
+ [
452
+ "keyword",
453
+ 210,
454
+ .9,
455
+ 4.5
456
+ ],
457
+ [
458
+ "string",
459
+ 40,
460
+ .9,
461
+ 4.5
462
+ ],
463
+ [
464
+ "token",
465
+ 125,
466
+ .9,
467
+ 4.5
468
+ ],
469
+ [
470
+ "property",
471
+ 155,
472
+ .9,
473
+ 4.5
474
+ ],
475
+ [
476
+ "number",
477
+ 70,
478
+ .9,
479
+ 4.5
480
+ ],
481
+ [
482
+ "function",
483
+ 210,
484
+ .9,
485
+ 4.5
486
+ ],
487
+ [
488
+ "value",
489
+ 210,
490
+ .9,
491
+ 4.5
492
+ ],
493
+ [
494
+ "operator",
495
+ 340,
496
+ .9,
497
+ 4.5
498
+ ]
499
+ ]) definitions[`syntax-${name}`] = {
500
+ from: {
501
+ h: hue,
502
+ s: saturation,
503
+ t: 1
504
+ },
505
+ base: "syntax-bg",
506
+ contrast: { wcag: [contrast, 7] }
507
+ };
508
+ const handled = /* @__PURE__ */ new Set([
509
+ "surface",
510
+ "header",
511
+ "overlay",
512
+ "text",
513
+ "heading",
514
+ "textSoft",
515
+ "info",
516
+ "success",
517
+ "warning",
518
+ "danger"
519
+ ]);
520
+ const names = /* @__PURE__ */ new Set();
521
+ for (const [name, input] of Object.entries(theme.palette ?? {})) {
522
+ if (input === void 0) continue;
523
+ const canonical = name === "textSoft" ? "text-soft" : name;
524
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(canonical) || [
525
+ "current",
526
+ "constructor",
527
+ "prototype"
528
+ ].includes(canonical)) throw new Error(`theme.palette.${name}: use a lowercase color name with hyphens; current, constructor, and prototype are reserved.`);
529
+ if (names.has(canonical)) throw new Error(`theme.palette.${name}: duplicates the ${canonical} color role (textSoft is an alias of text-soft).`);
530
+ names.add(canonical);
531
+ if (handled.has(name)) continue;
532
+ const defaults = definitions[canonical];
533
+ definitions[canonical] = paletteDefinition(input, defaults ?? { mode: "auto" });
534
+ }
535
+ for (const definition of Object.values(definitions)) {
536
+ if ("base" in definition && definition.base === "textSoft") definition.base = "text-soft";
537
+ if ("target" in definition && definition.target === "textSoft") definition.target = "text-soft";
538
+ if ("bg" in definition && definition.bg === "textSoft") definition.bg = "text-soft";
539
+ if ("fg" in definition && definition.fg === "textSoft") definition.fg = "text-soft";
540
+ }
541
+ colorTheme.colors(definitions);
542
+ const resolvedColors = colorTheme.resolve();
543
+ const resolvedSurface = requiredResolvedColor(resolvedColors, "surface");
544
+ const resolvedAccent = requiredResolvedColor(resolvedColors, "accent-text");
545
+ const scores = {
546
+ light: measureColorContrast(resolvedAccent.light, resolvedSurface.light),
547
+ dark: measureColorContrast(resolvedAccent.dark, resolvedSurface.dark),
548
+ lightContrast: measureColorContrast(resolvedAccent.lightContrast, resolvedSurface.lightContrast),
549
+ darkContrast: measureColorContrast(resolvedAccent.darkContrast, resolvedSurface.darkContrast)
550
+ };
551
+ const { checks: contrastChecks, diagnostics } = checkColorContrast(resolvedColors, definitions, theme.contrastLevel, [normalTarget, highTarget], theme.glaze?.inferRole);
552
+ const outputOptions = { modes: { highContrast: true } };
553
+ const tastyOptions = {
554
+ ...outputOptions,
555
+ states: {
556
+ dark: "theme=dark | (@media(prefers-color-scheme: dark) & :not([data-theme]))",
557
+ highContrast: "contrast=more | (@media(prefers-contrast: more) & :not([data-contrast]))"
558
+ }
559
+ };
560
+ const resolvedPalette = colorTheme.json(outputOptions);
561
+ const colorTokens = colorTheme.tasty(tastyOptions);
562
+ return {
563
+ colors: {
564
+ surface: requiredJsonColor(resolvedPalette, "surface"),
565
+ surface2: requiredJsonColor(resolvedPalette, "surface-2"),
566
+ surface3: requiredJsonColor(resolvedPalette, "surface-3"),
567
+ text: requiredJsonColor(resolvedPalette, "text"),
568
+ heading: requiredJsonColor(resolvedPalette, "heading"),
569
+ textSoft: requiredJsonColor(resolvedPalette, "text-soft"),
570
+ accentText: requiredJsonColor(resolvedPalette, "accent-text"),
571
+ accentSurface: requiredJsonColor(resolvedPalette, "accent-surface"),
572
+ accentSurfaceText: requiredJsonColor(resolvedPalette, "accent-surface-text"),
573
+ focus: requiredJsonColor(resolvedPalette, "focus"),
574
+ shadow: requiredJsonColor(resolvedPalette, "shadow")
575
+ },
576
+ colorTokens,
577
+ contrast: scores,
578
+ contrastChecks,
579
+ diagnostics
580
+ };
581
+ }
582
+ function mix(base, target, value, space = "okhsl") {
583
+ return {
584
+ type: "mix",
585
+ base,
586
+ target,
587
+ value,
588
+ space
589
+ };
590
+ }
591
+ function statusColors(name, input, fallback) {
592
+ const declaration = paletteDefinition(input, { from: fallback });
593
+ return {
594
+ [name]: isSpecialDefinition(declaration) ? declaration : {
595
+ base: "surface",
596
+ role: "border",
597
+ contrast: { apca: [30, 45] },
598
+ mode: "auto",
599
+ ...declaration
600
+ },
601
+ [`${name}-text`]: {
602
+ ...isSpecialDefinition(declaration) ? {
603
+ tone: 0,
604
+ saturation: 0
605
+ } : declaration,
606
+ base: `${name}-surface`,
607
+ role: "text",
608
+ contrast: { apca: [75, 90] },
609
+ mode: "auto"
610
+ },
611
+ [`${name}-surface`]: "type" in declaration && declaration.type === "shadow" ? {
612
+ base: "surface",
613
+ tone: "-2",
614
+ saturation: 0,
615
+ mode: "auto"
616
+ } : mix("surface", name, [12, 18], "srgb")
617
+ };
618
+ }
619
+ function isSpecialDefinition(value) {
620
+ return typeof value === "object" && value !== null && "type" in value;
621
+ }
622
+ function isColorDeclaration(value) {
623
+ return typeof value === "object" && value !== null && !("h" in value || "r" in value || "c" in value || "type" in value);
624
+ }
625
+ function paletteDefinition(value, baseDefaults) {
626
+ if (value === void 0) return baseDefaults;
627
+ if (isSpecialDefinition(value)) return { ...value };
628
+ const defaults = "type" in baseDefaults ? { mode: "auto" } : baseDefaults;
629
+ if (!isColorDeclaration(value) || value.from !== void 0) {
630
+ const { tone: _tone, saturation: _saturation, ...seedDefaults } = defaults;
631
+ return {
632
+ ...seedDefaults,
633
+ ...isColorDeclaration(value) ? value : { from: value }
634
+ };
635
+ }
636
+ if (!usesRelativeColor(value)) return {
637
+ ...defaults,
638
+ ...value
639
+ };
640
+ const { from: _ignored, ...relativeDefaults } = defaults;
641
+ return {
642
+ ...relativeDefaults,
643
+ ...value
644
+ };
645
+ }
646
+ function usesRelativeColor(value) {
647
+ return value.from === void 0 && (value.tone !== void 0 || value.hue !== void 0 || value.darkHue !== void 0 || value.saturation !== void 0 || value.darkSaturation !== void 0 || value.base !== void 0);
648
+ }
649
+ function saturationFactor(colorSaturation, themeSaturation) {
650
+ return themeSaturation > 0 ? Math.min(1, colorSaturation / themeSaturation) : 0;
651
+ }
652
+ function requiredResolvedColor(colors, name) {
653
+ const color = colors.get(name);
654
+ if (!color) throw new Error(`The Glaze ${name} color failed to resolve.`);
655
+ return color;
656
+ }
657
+ function requiredJsonColor(colors, name) {
658
+ const color = colors[name];
659
+ if (!color) throw new Error(`The Glaze ${name} token failed to export.`);
660
+ return color;
661
+ }
662
+ function normalizeBrand(brand) {
663
+ if (typeof brand === "object" && brand !== null && "from" in brand) return brand;
664
+ if (isBrandDeclaration$1(brand)) {
665
+ const seed = glaze.color({
666
+ hue: brand.hue,
667
+ saturation: brand.saturation,
668
+ tone: brand.tone,
669
+ mode: "fixed"
670
+ }).resolve().light;
671
+ return {
672
+ from: {
673
+ h: seed.h,
674
+ s: seed.s,
675
+ t: seed.t
676
+ },
677
+ ...brand.contrast && { contrast: brand.contrast },
678
+ ...brand.unsafeContrast && { unsafeContrast: true }
679
+ };
680
+ }
681
+ return { from: brand ?? "okhsl(266 68% 48%)" };
682
+ }
683
+ function isBrandDeclaration$1(brand) {
684
+ return typeof brand === "object" && brand !== null && "hue" in brand && "saturation" in brand && "tone" in brand;
685
+ }
686
+ //#endregion
687
+ //#region src/types.ts
688
+ const COOKBOOK_PALETTE_NAMES = [
689
+ "surface",
690
+ "header",
691
+ "surface-2",
692
+ "surface-3",
693
+ "text",
694
+ "heading",
695
+ "text-soft",
696
+ "text-muted",
697
+ "surface-2-hover",
698
+ "surface-2-pressed",
699
+ "surface-3-hover",
700
+ "surface-3-pressed",
701
+ "accent-text",
702
+ "focus",
703
+ "accent-surface",
704
+ "accent-surface-text",
705
+ "logo-surface",
706
+ "logo-mark",
707
+ "accent-surface-subtle",
708
+ "accent-surface-2-subtle",
709
+ "shadow",
710
+ "clear",
711
+ "info",
712
+ "info-text",
713
+ "info-surface",
714
+ "success",
715
+ "success-text",
716
+ "success-surface",
717
+ "warning",
718
+ "warning-text",
719
+ "warning-surface",
720
+ "danger",
721
+ "danger-text",
722
+ "danger-surface",
723
+ "orange",
724
+ "orange-text",
725
+ "orange-surface",
726
+ "green",
727
+ "green-text",
728
+ "green-surface",
729
+ "blue",
730
+ "blue-text",
731
+ "blue-surface",
732
+ "purple",
733
+ "purple-text",
734
+ "purple-surface",
735
+ "red",
736
+ "red-text",
737
+ "red-surface",
738
+ "overlay",
739
+ "border",
740
+ "border-strong",
741
+ "syntax-bg",
742
+ "syntax-text",
743
+ "syntax-comment",
744
+ "syntax-punctuation",
745
+ "syntax-keyword",
746
+ "syntax-string",
747
+ "syntax-token",
748
+ "syntax-property",
749
+ "syntax-number",
750
+ "syntax-function",
751
+ "syntax-value",
752
+ "syntax-operator"
753
+ ];
754
+ /** Cookbook UI surfaces whose default Tasty styles can be customized. */
755
+ const COOKBOOK_COMPONENT_NAMES = [
756
+ "Card",
757
+ "Callout",
758
+ "CodeGroup",
759
+ "Tab",
760
+ "Footer",
761
+ "PageActions",
762
+ "MobileTableOfContents",
763
+ "Hero",
764
+ "PageFrame",
765
+ "HeaderFrame",
766
+ "HeaderLinks",
767
+ "Heading",
768
+ "Layout",
769
+ "SearchButton",
770
+ "TableOfContentsLayout",
771
+ "LanguageSelect",
772
+ "Logo",
773
+ "SiteLogo",
774
+ "MarkdownCodeBlock",
775
+ "MarkdownHeading",
776
+ "MarkdownInlineCode",
777
+ "MarkdownTable",
778
+ "Mermaid",
779
+ "MobileMenuFooter",
780
+ "MobileMenuToggle",
781
+ "MobileNavigationTabs",
782
+ "PackageVersion",
783
+ "VersionSwitcher",
784
+ "Preview",
785
+ "Sidebar",
786
+ "SocialIcons",
787
+ "Steps",
788
+ "Tabs",
789
+ "TableOfContents",
790
+ "ThemeSelect",
791
+ "TopNavigation",
792
+ "StarlightHeader",
793
+ "Document",
794
+ "MainPane",
795
+ "MainContent",
796
+ "Banner",
797
+ "SkipLink",
798
+ "Search",
799
+ "Pagination",
800
+ "Markdown",
801
+ "MermaidSource",
802
+ "StarlightAside",
803
+ "StarlightCard",
804
+ "StarlightLinkCard",
805
+ "StarlightBadge",
806
+ "StarlightSteps",
807
+ "SearchResults",
808
+ "SyntaxHighlight"
809
+ ];
810
+ /** Named Tasty sub-elements available on each configurable Cookbook surface. */
811
+ const COOKBOOK_COMPONENT_SUB_ELEMENTS = {
812
+ SyntaxHighlight: [
813
+ "Scroll",
814
+ "Wrap",
815
+ "Marker",
816
+ "Comment",
817
+ "Punctuation",
818
+ "Keyword",
819
+ "String",
820
+ "Token",
821
+ "Property",
822
+ "Number",
823
+ "Function",
824
+ "Value",
825
+ "Operator",
826
+ "Text",
827
+ "Bg",
828
+ "Inserted",
829
+ "Deleted",
830
+ "Italic",
831
+ "Strong",
832
+ "Underline"
833
+ ],
834
+ SearchResults: [
835
+ "Form",
836
+ "Input",
837
+ "Clear",
838
+ "Results",
839
+ "Result",
840
+ "ResultLink",
841
+ "SearchIcon",
842
+ "ClearIcon",
843
+ "SuppressedClear",
844
+ "Message",
845
+ "List",
846
+ "Title",
847
+ "Excerpt",
848
+ "NestedResult",
849
+ "Match",
850
+ "More",
851
+ "HoverMore"
852
+ ],
853
+ StarlightSteps: [
854
+ "Item",
855
+ "Counter",
856
+ "Connector"
857
+ ],
858
+ StarlightBadge: [],
859
+ StarlightLinkCard: [
860
+ "Hover",
861
+ "Stack",
862
+ "Link",
863
+ "LinkOverlay",
864
+ "DescriptionAndIcon"
865
+ ],
866
+ StarlightCard: ["Title"],
867
+ StarlightAside: [
868
+ "Note",
869
+ "Tip",
870
+ "Caution",
871
+ "Danger",
872
+ "Title",
873
+ "Icon",
874
+ "FirstContent"
875
+ ],
876
+ MermaidSource: [],
877
+ Markdown: [
878
+ "Block",
879
+ "BlockSpacing",
880
+ "HeadingSpacing",
881
+ "List",
882
+ "CompactItem",
883
+ "ListItem",
884
+ "DefinitionTerm",
885
+ "DefinitionDescription",
886
+ "Link",
887
+ "HoverLink",
888
+ "Quote",
889
+ "Rule",
890
+ "Details",
891
+ "HoverDetails",
892
+ "Summary",
893
+ "OpenSummary",
894
+ "SummaryMarker",
895
+ "SummaryIcon",
896
+ "OpenSummaryIcon",
897
+ "Code"
898
+ ],
899
+ Pagination: [
900
+ "Link",
901
+ "NextLink",
902
+ "NextIcon",
903
+ "NextLabel",
904
+ "HoverLink",
905
+ "ActiveLink",
906
+ "Title",
907
+ "LoneNextLink",
908
+ "NativeIcon",
909
+ "Icon",
910
+ "PreviousIconRtl",
911
+ "NextIconRtl"
912
+ ],
913
+ Search: [
914
+ "Status",
915
+ "Dialog",
916
+ "CloseIcon",
917
+ "OpenDialog",
918
+ "Backdrop",
919
+ "Frame",
920
+ "Container",
921
+ "Close",
922
+ "HoverClose",
923
+ "ActiveClose"
924
+ ],
925
+ SkipLink: ["Focus"],
926
+ Banner: ["Link"],
927
+ MainContent: [
928
+ "ContentSpacing",
929
+ "Container",
930
+ "Panel",
931
+ "FirstPanel",
932
+ "BodyPanel"
933
+ ],
934
+ MainPane: ["WithSidebars"],
935
+ Document: [
936
+ "All",
937
+ "Body",
938
+ "Control",
939
+ "Pointer",
940
+ "ResponsiveWidth",
941
+ "ResponsiveHeight",
942
+ "Hidden",
943
+ "DesktopBlock",
944
+ "DesktopFlex",
945
+ "ScreenReaderOnly",
946
+ "Strong",
947
+ "Link",
948
+ "NarrowBlock",
949
+ "MobileBlock",
950
+ "Code",
951
+ "FocusRing",
952
+ "CurrentLink",
953
+ "SearchOpen"
954
+ ],
955
+ PageFrame: [
956
+ "MainFrame",
957
+ "SidebarFrame",
958
+ "Columns"
959
+ ],
960
+ HeaderFrame: [],
961
+ Heading: [
962
+ "Level1",
963
+ "Level2",
964
+ "Level3",
965
+ "Level4",
966
+ "Level5",
967
+ "Level6",
968
+ "PageTitle"
969
+ ],
970
+ HeaderLinks: [
971
+ "Desktop",
972
+ "DesktopLink",
973
+ "Link",
974
+ "HoverLink",
975
+ "PrimaryLink",
976
+ "HoverPrimaryLink",
977
+ "Trigger",
978
+ "HoverTrigger",
979
+ "Panel",
980
+ "PanelNavigation",
981
+ "PanelLink",
982
+ "FirstPanelLink",
983
+ "Close",
984
+ "HoverClose"
985
+ ],
986
+ Layout: [
987
+ "Islands",
988
+ "LockedPage",
989
+ "Light",
990
+ "Auto"
991
+ ],
992
+ SearchButton: [
993
+ "PendingShortcut",
994
+ "Label",
995
+ "Shortcut",
996
+ "Hover",
997
+ "Active",
998
+ "NativeIcon",
999
+ "Icon"
1000
+ ],
1001
+ TableOfContentsLayout: ["WithMobile", "Content"],
1002
+ Card: [
1003
+ "Heading2",
1004
+ "Heading3",
1005
+ "Paragraph"
1006
+ ],
1007
+ Callout: [
1008
+ "Title",
1009
+ "Body",
1010
+ "Tip",
1011
+ "Caution",
1012
+ "Danger"
1013
+ ],
1014
+ CodeGroup: [
1015
+ "Caption",
1016
+ "Pre",
1017
+ "Code"
1018
+ ],
1019
+ Tab: [
1020
+ "Heading",
1021
+ "Hidden",
1022
+ "HiddenHeading"
1023
+ ],
1024
+ MobileTableOfContents: [
1025
+ "Summary",
1026
+ "List",
1027
+ "NestedList",
1028
+ "Item",
1029
+ "Link",
1030
+ "HoverLink",
1031
+ "Focus"
1032
+ ],
1033
+ PageActions: [
1034
+ "Control",
1035
+ "Hover",
1036
+ "Focus",
1037
+ "Pending",
1038
+ "Status"
1039
+ ],
1040
+ Footer: [
1041
+ "Meta",
1042
+ "LoneMetaItem",
1043
+ "MetaLink",
1044
+ "HoverMetaLink",
1045
+ "Credit",
1046
+ "CreditLink",
1047
+ "HoverCreditLink"
1048
+ ],
1049
+ Hero: [
1050
+ "Visual",
1051
+ "Stack",
1052
+ "Copy",
1053
+ "Title",
1054
+ "Tagline",
1055
+ "Actions",
1056
+ "Action",
1057
+ "HoverAction",
1058
+ "PrimaryAction",
1059
+ "SecondaryAction",
1060
+ "MinimalAction",
1061
+ "ActionIcon"
1062
+ ],
1063
+ LanguageSelect: [
1064
+ "Label",
1065
+ "HoverLabel",
1066
+ "LabelIcon",
1067
+ "Select",
1068
+ "Caret",
1069
+ "Option"
1070
+ ],
1071
+ Logo: ["Svg", "Mark"],
1072
+ SiteLogo: [
1073
+ "Image",
1074
+ "Light",
1075
+ "Dark"
1076
+ ],
1077
+ MarkdownCodeBlock: [
1078
+ "Pre",
1079
+ "CopyButton",
1080
+ "HoverCopyButton",
1081
+ "CopiedButton",
1082
+ "CopyIcon",
1083
+ "CopiedIcon",
1084
+ "Code",
1085
+ "Diff",
1086
+ "DiffCode",
1087
+ "DiffLine",
1088
+ "EmptyDiffLine",
1089
+ "InsertedLine",
1090
+ "DeletedLine"
1091
+ ],
1092
+ MarkdownHeading: [
1093
+ "Heading",
1094
+ "Heading1",
1095
+ "Heading2",
1096
+ "Heading3",
1097
+ "Heading4",
1098
+ "Heading5",
1099
+ "Heading6",
1100
+ "Link",
1101
+ "RevealedLink",
1102
+ "HoverLink",
1103
+ "LinkIcon",
1104
+ "CopiedLink",
1105
+ "CopiedLinkIcon",
1106
+ "CopiedIcon"
1107
+ ],
1108
+ MarkdownTable: [
1109
+ "Table",
1110
+ "Cell",
1111
+ "LastBodyRowCell",
1112
+ "HeaderCell",
1113
+ "Scroll"
1114
+ ],
1115
+ MarkdownInlineCode: [],
1116
+ Mermaid: [
1117
+ "Diagram",
1118
+ "Text",
1119
+ "MonoText"
1120
+ ],
1121
+ MobileMenuFooter: ["Social"],
1122
+ MobileMenuToggle: [
1123
+ "Control",
1124
+ "Icon",
1125
+ "Section",
1126
+ "Page",
1127
+ "HoverControl",
1128
+ "ActiveControl"
1129
+ ],
1130
+ MobileNavigationTabs: [
1131
+ "Trigger",
1132
+ "Marker",
1133
+ "Caret",
1134
+ "ExpandedCaret",
1135
+ "Label",
1136
+ "List",
1137
+ "Item",
1138
+ "Link",
1139
+ "HoverLink",
1140
+ "CurrentLink"
1141
+ ],
1142
+ PackageVersion: [],
1143
+ VersionSwitcher: [
1144
+ "Trigger",
1145
+ "Panel",
1146
+ "OpenPanel",
1147
+ "Link",
1148
+ "CurrentLink"
1149
+ ],
1150
+ Preview: [
1151
+ "Caption",
1152
+ "Stage",
1153
+ "Frame",
1154
+ "Code",
1155
+ "Summary",
1156
+ "Pre"
1157
+ ],
1158
+ Sidebar: [
1159
+ "Backdrop",
1160
+ "OpenBackdrop",
1161
+ "MobileHeading",
1162
+ "HomeLink",
1163
+ "HomeLogo",
1164
+ "HomeLabel",
1165
+ "Close",
1166
+ "HoverClose",
1167
+ "CloseIcon",
1168
+ "CurrentLink",
1169
+ "OpenPane",
1170
+ "EnteredPane",
1171
+ "Content",
1172
+ "Tree",
1173
+ "List",
1174
+ "Item",
1175
+ "TopLevelSpacing",
1176
+ "GroupSpacing",
1177
+ "NestedItem",
1178
+ "SectionHeading",
1179
+ "Control",
1180
+ "Summary",
1181
+ "GroupLabel",
1182
+ "GroupLabelText",
1183
+ "Link",
1184
+ "LinkLabel",
1185
+ "InteractiveControl",
1186
+ "SummaryMarker",
1187
+ "Caret",
1188
+ "ExpandedCaret",
1189
+ "LinkedSummary",
1190
+ "GroupLink",
1191
+ "LinkedSectionHeading",
1192
+ "SectionLink",
1193
+ "Badge",
1194
+ "TopLevelLink"
1195
+ ],
1196
+ Steps: ["Item", "Marker"],
1197
+ SocialIcons: [
1198
+ "Link",
1199
+ "HoverLink",
1200
+ "Icon"
1201
+ ],
1202
+ Tabs: [
1203
+ "List",
1204
+ "Button",
1205
+ "SelectedButton",
1206
+ "FocusedButton"
1207
+ ],
1208
+ TableOfContents: [
1209
+ "Heading",
1210
+ "List",
1211
+ "Item",
1212
+ "Link",
1213
+ "LinkLabel",
1214
+ "HoverLink",
1215
+ "CurrentLink"
1216
+ ],
1217
+ ThemeSelect: [
1218
+ "Trigger",
1219
+ "HoverTrigger",
1220
+ "ActiveTrigger",
1221
+ "Icon",
1222
+ "Panel",
1223
+ "OpenPanel",
1224
+ "Section",
1225
+ "SectionSpacing",
1226
+ "SectionLabel",
1227
+ "Option",
1228
+ "HoverOption",
1229
+ "CheckedOption",
1230
+ "FocusedOption",
1231
+ "Input",
1232
+ "OptionIcon",
1233
+ "Checkmark",
1234
+ "SelectedCheckmark"
1235
+ ],
1236
+ TopNavigation: [
1237
+ "Scrollbar",
1238
+ "Link",
1239
+ "HoverLink",
1240
+ "CurrentLink",
1241
+ "ActiveIndicator"
1242
+ ],
1243
+ StarlightHeader: [
1244
+ "Primary",
1245
+ "TitleAndSearch",
1246
+ "Title",
1247
+ "LogoLink",
1248
+ "Logo",
1249
+ "SiteTitle",
1250
+ "Search",
1251
+ "SearchElement",
1252
+ "Tools",
1253
+ "ToolItem",
1254
+ "Social",
1255
+ "MobileTheme"
1256
+ ]
1257
+ };
1258
+ //#endregion
1259
+ //#region src/config/index.ts
1260
+ const DEFAULT_BRAND = "okhsl(266 68% 48%)";
1261
+ const HEAD_KEYS = /* @__PURE__ */ new Set([
1262
+ "tag",
1263
+ "attrs",
1264
+ "content"
1265
+ ]);
1266
+ const SITE_ICON_KEYS = /* @__PURE__ */ new Set(["source", "background"]);
1267
+ const COLOR_DECLARATION_KEYS = /* @__PURE__ */ new Set([
1268
+ "from",
1269
+ "tone",
1270
+ "saturation",
1271
+ "hue",
1272
+ "darkHue",
1273
+ "darkSaturation",
1274
+ "base",
1275
+ "contrast",
1276
+ "mode",
1277
+ "autoFlip",
1278
+ "opacity",
1279
+ "pastel",
1280
+ "role",
1281
+ "inherit"
1282
+ ]);
1283
+ const ROOT_KEYS = /* @__PURE__ */ new Set([
1284
+ "tableOfContents",
1285
+ "root",
1286
+ "redirects",
1287
+ "site",
1288
+ "head",
1289
+ "editLink",
1290
+ "lastUpdated",
1291
+ "locales",
1292
+ "translations",
1293
+ "defaultLocale",
1294
+ "content",
1295
+ "navigation",
1296
+ "theme",
1297
+ "markdown",
1298
+ "search",
1299
+ "components",
1300
+ "build"
1301
+ ]);
1302
+ const OBJECT_KEYS = {
1303
+ site: /* @__PURE__ */ new Set([
1304
+ "seo",
1305
+ "title",
1306
+ "version",
1307
+ "versions",
1308
+ "description",
1309
+ "url",
1310
+ "repository",
1311
+ "favicon",
1312
+ "logo",
1313
+ "headerLinks"
1314
+ ]),
1315
+ content: /* @__PURE__ */ new Set([
1316
+ "sources",
1317
+ "allowOutsideRoot",
1318
+ "localizeRepositoryLinks",
1319
+ "frontmatter"
1320
+ ]),
1321
+ markdown: /* @__PURE__ */ new Set(["stripLeadingBadges", "rawHtml"]),
1322
+ search: /* @__PURE__ */ new Set(["enabled"]),
1323
+ components: /* @__PURE__ */ new Set(["overrides"]),
1324
+ theme: /* @__PURE__ */ new Set([
1325
+ "brand",
1326
+ "palette",
1327
+ "glaze",
1328
+ "units",
1329
+ "recipes",
1330
+ "fonts",
1331
+ "fontLoading",
1332
+ "states",
1333
+ "tokens",
1334
+ "presets",
1335
+ "styles",
1336
+ "customStyles",
1337
+ "contrastLevel"
1338
+ ]),
1339
+ build: /* @__PURE__ */ new Set([
1340
+ "strict",
1341
+ "ci",
1342
+ "cacheDir",
1343
+ "maxArtifactBytes",
1344
+ "maxUnpackedBytes",
1345
+ "maxFiles",
1346
+ "maxPathDepth",
1347
+ "maxAssetBytes"
1348
+ ])
1349
+ };
1350
+ var DocsConfigError = class extends Error {
1351
+ diagnostics;
1352
+ constructor(diagnostics) {
1353
+ super(diagnostics.map((diagnostic) => diagnostic.message).join("\n"));
1354
+ this.name = "DocsConfigError";
1355
+ this.diagnostics = diagnostics;
1356
+ }
1357
+ };
1358
+ function validateConfig(config) {
1359
+ const diagnostics = [];
1360
+ if (config === null || typeof config !== "object" || Array.isArray(config)) {
1361
+ invalid(diagnostics, "Documentation configuration must be an object.");
1362
+ return diagnostics;
1363
+ }
1364
+ if (config.root !== void 0 && typeof config.root !== "string") invalid(diagnostics, "root must be a directory path.");
1365
+ for (const key of Object.keys(config)) if (!ROOT_KEYS.has(key)) unknown(diagnostics, key);
1366
+ for (const [section, keys] of Object.entries(OBJECT_KEYS)) {
1367
+ const value = config[section];
1368
+ if (value === void 0) continue;
1369
+ if (!isRecord(value)) {
1370
+ invalid(diagnostics, `${section} must be an object.`);
1371
+ continue;
1372
+ }
1373
+ for (const key of Object.keys(value)) if (!keys.has(key)) unknown(diagnostics, `${section}.${key}`);
1374
+ }
1375
+ if (isRecord(config.site) && config.site.versions !== void 0) {
1376
+ if (!Array.isArray(config.site.versions) || config.site.versions.length < 2) invalid(diagnostics, "site.versions must contain at least two versions.");
1377
+ else {
1378
+ const bases = /* @__PURE__ */ new Set();
1379
+ for (const [index, version] of config.site.versions.entries()) {
1380
+ const path = `site.versions[${index}]`;
1381
+ if (!isRecord(version)) {
1382
+ invalid(diagnostics, `${path} must be an object.`);
1383
+ continue;
1384
+ }
1385
+ for (const key of Object.keys(version)) if (key !== "label" && key !== "routeBase" && key !== "index") unknown(diagnostics, `${path}.${key}`);
1386
+ if (typeof version.label !== "string" || !version.label.trim()) invalid(diagnostics, `${path}.label must be a non-empty string.`);
1387
+ if (version.index !== void 0 && typeof version.index !== "boolean") invalid(diagnostics, `${path}.index must be a boolean.`);
1388
+ const base = version.routeBase;
1389
+ if (typeof base !== "string" || !base.startsWith("/") || base !== "/" && base.endsWith("/") || base.includes("//") || /[?#\\\s]/.test(base) || base.split("/").some((segment) => segment === "." || segment === "..")) invalid(diagnostics, `${path}.routeBase must be a root-relative route without a trailing slash.`);
1390
+ else if (bases.has(base)) invalid(diagnostics, `${path}.routeBase duplicates ${base}.`);
1391
+ else bases.add(base);
1392
+ }
1393
+ }
1394
+ }
1395
+ if (isRecord(config.site) && config.site.headerLinks !== void 0) {
1396
+ if (!Array.isArray(config.site.headerLinks)) invalid(diagnostics, "site.headerLinks must be an array.");
1397
+ else config.site.headerLinks.forEach((item, index) => {
1398
+ const path = `site.headerLinks[${index}]`;
1399
+ if (!isRecord(item)) {
1400
+ invalid(diagnostics, `${path} must be an object.`);
1401
+ return;
1402
+ }
1403
+ for (const key of Object.keys(item)) if (![
1404
+ "label",
1405
+ "link",
1406
+ "variant",
1407
+ "newTab"
1408
+ ].includes(key)) unknown(diagnostics, `${path}.${key}`);
1409
+ if (typeof item.label !== "string" || !item.label.trim()) invalid(diagnostics, `${path}.label must be a non-empty string.`);
1410
+ if (typeof item.link !== "string" || /[\\\s]/.test(item.link) || !(/^(?:\/(?!\/)|#)/.test(item.link) || isHttpUrl(item.link))) invalid(diagnostics, `${path}.link must be an HTTP(S) URL, root-relative route, or fragment.`);
1411
+ if (item.variant !== void 0 && !["default", "primary"].includes(item.variant)) invalid(diagnostics, `${path}.variant must be "default" or "primary".`);
1412
+ if (item.newTab !== void 0 && typeof item.newTab !== "boolean") invalid(diagnostics, `${path}.newTab must be a boolean.`);
1413
+ });
1414
+ }
1415
+ if (config.navigation !== void 0 && !Array.isArray(config.navigation)) {
1416
+ if (!isRecord(config.navigation)) invalid(diagnostics, "navigation must be an array or object.");
1417
+ else {
1418
+ for (const key of Object.keys(config.navigation)) if (!(/* @__PURE__ */ new Set(["items", "tabs"])).has(key)) unknown(diagnostics, `navigation.${key}`);
1419
+ if (config.navigation.items !== void 0 && !Array.isArray(config.navigation.items)) invalid(diagnostics, "navigation.items must be an array.");
1420
+ if (config.navigation.tabs !== void 0 && !Array.isArray(config.navigation.tabs)) invalid(diagnostics, "navigation.tabs must be an array.");
1421
+ }
1422
+ }
1423
+ if (Array.isArray(config.navigation)) validateNavigationItems(config.navigation, "navigation", diagnostics);
1424
+ else if (isRecord(config.navigation)) {
1425
+ if (Array.isArray(config.navigation.items)) validateNavigationItems(config.navigation.items, "navigation.items", diagnostics);
1426
+ if (Array.isArray(config.navigation.tabs)) for (const [index, tab] of config.navigation.tabs.entries()) {
1427
+ const path = `navigation.tabs[${index}]`;
1428
+ if (!isRecord(tab) || typeof tab.label !== "string" || typeof tab.link !== "string") invalid(diagnostics, `${path} must have a label and link.`);
1429
+ else if (tab.items !== void 0) validateNavigationItems(tab.items, `${path}.items`, diagnostics);
1430
+ }
1431
+ }
1432
+ if (config.redirects !== void 0 && (!isRecord(config.redirects) || Object.values(config.redirects).some((value) => typeof value !== "string"))) invalid(diagnostics, "redirects must map old routes to document routes.");
1433
+ const head = config.head;
1434
+ if (head !== void 0 && !Array.isArray(head)) invalid(diagnostics, "head must be an array.");
1435
+ else for (const [index, entry] of (head ?? []).entries()) {
1436
+ if (!isRecord(entry)) {
1437
+ invalid(diagnostics, `head[${index}] must be an object.`);
1438
+ continue;
1439
+ }
1440
+ for (const key of Object.keys(entry)) if (!HEAD_KEYS.has(key)) unknown(diagnostics, `head[${index}].${key}`);
1441
+ if (typeof entry.tag !== "string") invalid(diagnostics, `head[${index}].tag must be a string.`);
1442
+ if (entry.attrs !== void 0 && !isRecord(entry.attrs)) invalid(diagnostics, `head[${index}].attrs must be an object.`);
1443
+ else for (const [name, value] of Object.entries(entry.attrs ?? {})) if (value !== void 0 && typeof value !== "string" && typeof value !== "boolean") invalid(diagnostics, `head[${index}].attrs.${name} must be a string or boolean.`);
1444
+ if (entry.content !== void 0 && typeof entry.content !== "string") invalid(diagnostics, `head[${index}].content must be a string.`);
1445
+ }
1446
+ if (config.site?.seo !== void 0) for (const message of validateSeo(config.site.seo, "site")) invalid(diagnostics, `site.${message}`);
1447
+ if (config.tableOfContents !== void 0) for (const message of validateTableOfContents(config.tableOfContents)) invalid(diagnostics, message);
1448
+ const logo = config.site?.logo;
1449
+ if (logo !== void 0 && logo !== false) {
1450
+ const value = typeof logo === "string" ? { src: logo } : logo;
1451
+ if (!isRecord(value)) invalid(diagnostics, "site.logo must be false, a local image path, or a logo definition.");
1452
+ else {
1453
+ for (const key of Object.keys(value)) if (![
1454
+ "src",
1455
+ "light",
1456
+ "dark",
1457
+ "alt",
1458
+ "href",
1459
+ "width",
1460
+ "height",
1461
+ "decorative"
1462
+ ].includes(key)) unknown(diagnostics, `site.logo.${key}`);
1463
+ if (value.src !== void 0 && (value.light !== void 0 || value.dark !== void 0) || value.src === void 0 && (value.light === void 0 || value.dark === void 0)) invalid(diagnostics, "site.logo must provide src or both light and dark paths.");
1464
+ for (const key of [
1465
+ "src",
1466
+ "light",
1467
+ "dark"
1468
+ ]) if (value[key] !== void 0 && (typeof value[key] !== "string" || !value[key].trim() || /^[a-z][a-z\d+.-]*:|^\/\//i.test(value[key]))) invalid(diagnostics, `site.logo.${key} must be a non-empty local image path.`);
1469
+ if (value.href !== void 0 && (typeof value.href !== "string" || /[\\\s]/.test(value.href) || !(/^(?:\/(?!\/)|#)/.test(value.href) || isHttpUrl(value.href)))) invalid(diagnostics, "site.logo.href must be an HTTP(S) URL, root-relative route, or fragment.");
1470
+ if (value.alt !== void 0 && typeof value.alt !== "string") invalid(diagnostics, "site.logo.alt must be a string.");
1471
+ if (value.decorative !== void 0 && typeof value.decorative !== "boolean") invalid(diagnostics, "site.logo.decorative must be a boolean.");
1472
+ for (const key of ["width", "height"]) if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isFinite(value[key]) || value[key] <= 0)) invalid(diagnostics, `site.logo.${key} must be a positive dimension.`);
1473
+ }
1474
+ }
1475
+ const favicon = config.site?.favicon;
1476
+ if (favicon !== void 0) {
1477
+ if (typeof favicon === "string") {
1478
+ if (favicon.trim() === "") invalid(diagnostics, "site.favicon must be a non-empty local path.");
1479
+ } else if (!isRecord(favicon)) invalid(diagnostics, "site.favicon must be a local path or an object with a source path.");
1480
+ else {
1481
+ for (const key of Object.keys(favicon)) if (!SITE_ICON_KEYS.has(key)) unknown(diagnostics, `site.favicon.${key}`);
1482
+ if (typeof favicon.source !== "string" || favicon.source.trim() === "") invalid(diagnostics, "site.favicon.source must be a non-empty string.");
1483
+ if (favicon.background !== void 0 && (typeof favicon.background !== "string" || favicon.background.trim() === "")) invalid(diagnostics, "site.favicon.background must be a non-empty color string.");
1484
+ }
1485
+ }
1486
+ for (const field of ["url", "repository"]) {
1487
+ const value = config.site?.[field];
1488
+ if (value === void 0) continue;
1489
+ if (typeof value !== "string" || !isHttpUrl(value)) invalid(diagnostics, `site.${field} must be an absolute HTTP(S) URL.`);
1490
+ }
1491
+ if (config.editLink !== void 0 && (!isRecord(config.editLink) || typeof config.editLink.baseUrl !== "string")) invalid(diagnostics, "editLink must contain a string baseUrl.");
1492
+ else if (config.editLink !== void 0) {
1493
+ for (const key of Object.keys(config.editLink)) if (key !== "baseUrl") unknown(diagnostics, `editLink.${key}`);
1494
+ try {
1495
+ new URL(config.editLink.baseUrl);
1496
+ } catch {
1497
+ invalid(diagnostics, "editLink.baseUrl must be an absolute URL.");
1498
+ }
1499
+ }
1500
+ if (config.lastUpdated !== void 0 && typeof config.lastUpdated !== "boolean") invalid(diagnostics, "lastUpdated must be a boolean.");
1501
+ if (config.translations !== void 0 && (!isRecord(config.translations) || Object.values(config.translations).some((messages) => !isRecord(messages) || Object.values(messages).some((value) => typeof value !== "string")))) invalid(diagnostics, "translations must map language codes to string messages.");
1502
+ if (config.locales !== void 0 && !isRecord(config.locales)) invalid(diagnostics, "locales must be an object.");
1503
+ else for (const [key, locale] of Object.entries(config.locales ?? {})) {
1504
+ if (!isRecord(locale) || typeof locale.label !== "string") {
1505
+ invalid(diagnostics, `locales.${key} must contain a string label.`);
1506
+ continue;
1507
+ }
1508
+ for (const property of Object.keys(locale)) if (![
1509
+ "label",
1510
+ "lang",
1511
+ "dir"
1512
+ ].includes(property)) unknown(diagnostics, `locales.${key}.${property}`);
1513
+ if (locale.lang !== void 0 && typeof locale.lang !== "string") invalid(diagnostics, `locales.${key}.lang must be a string.`);
1514
+ if (locale.dir !== void 0 && locale.dir !== "ltr" && locale.dir !== "rtl") invalid(diagnostics, `locales.${key}.dir must be "ltr" or "rtl".`);
1515
+ }
1516
+ if (config.defaultLocale !== void 0 && typeof config.defaultLocale !== "string") invalid(diagnostics, "defaultLocale must be a string.");
1517
+ else if (config.defaultLocale !== void 0 && (config.locales === void 0 || !(config.defaultLocale in config.locales))) invalid(diagnostics, `defaultLocale must match a key in locales; received "${config.defaultLocale}".`);
1518
+ if (config.content?.allowOutsideRoot !== void 0 && typeof config.content.allowOutsideRoot !== "boolean") invalid(diagnostics, "content.allowOutsideRoot must be a boolean.");
1519
+ if (config.content?.localizeRepositoryLinks !== void 0 && typeof config.content.localizeRepositoryLinks !== "boolean") invalid(diagnostics, "content.localizeRepositoryLinks must be a boolean.");
1520
+ const configuredSources = config.content?.sources;
1521
+ if (configuredSources !== void 0 && !Array.isArray(configuredSources)) invalid(diagnostics, "content.sources must be an array.");
1522
+ const sources = Array.isArray(configuredSources) ? configuredSources : [];
1523
+ for (const [index, source] of sources.entries()) {
1524
+ if (!isRecord(source)) {
1525
+ invalid(diagnostics, `content.sources[${index}] must be an object.`);
1526
+ continue;
1527
+ }
1528
+ const sourceRecord = source;
1529
+ const variants = [
1530
+ "file",
1531
+ "glob",
1532
+ "package",
1533
+ "openapi"
1534
+ ].filter((key) => key in source);
1535
+ if (variants.length !== 1) {
1536
+ diagnostics.push({
1537
+ code: "DOCS_CONFIG_INVALID",
1538
+ severity: "error",
1539
+ message: `content.sources[${index}] must select exactly one of file, glob, package, or openapi.`
1540
+ });
1541
+ continue;
1542
+ }
1543
+ const variant = variants[0];
1544
+ const allowed = {
1545
+ file: /* @__PURE__ */ new Set([
1546
+ "id",
1547
+ "root",
1548
+ "routeBase",
1549
+ "file",
1550
+ "route",
1551
+ "title",
1552
+ "description",
1553
+ "navigation"
1554
+ ]),
1555
+ glob: /* @__PURE__ */ new Set([
1556
+ "id",
1557
+ "root",
1558
+ "glob",
1559
+ "base",
1560
+ "routeBase",
1561
+ "exclude",
1562
+ "navigation"
1563
+ ]),
1564
+ package: /* @__PURE__ */ new Set([
1565
+ "id",
1566
+ "package",
1567
+ "registry",
1568
+ "include",
1569
+ "exclude",
1570
+ "index",
1571
+ "routeBase",
1572
+ "trust"
1573
+ ]),
1574
+ openapi: /* @__PURE__ */ new Set([
1575
+ "id",
1576
+ "root",
1577
+ "routeBase",
1578
+ "openapi"
1579
+ ])
1580
+ }[variant];
1581
+ for (const key of Object.keys(source)) if (!allowed.has(key)) unknown(diagnostics, `content.sources[${index}].${key}`);
1582
+ for (const field of [
1583
+ "id",
1584
+ "root",
1585
+ "route",
1586
+ "routeBase",
1587
+ "base",
1588
+ "index",
1589
+ "title",
1590
+ "description",
1591
+ "registry"
1592
+ ]) if (sourceRecord[field] !== void 0 && typeof sourceRecord[field] !== "string") invalid(diagnostics, `content.sources[${index}].${field} must be a string.`);
1593
+ if (typeof sourceRecord.id === "string" && !/^[a-zA-Z0-9_-]+$/.test(sourceRecord.id)) invalid(diagnostics, `content.sources[${index}].id must contain only letters, numbers, underscores, and hyphens.`);
1594
+ if (sourceRecord.trust !== void 0 && !["markdown", "mdx"].includes(String(sourceRecord.trust))) invalid(diagnostics, `content.sources[${index}].trust must be "markdown" or "mdx".`);
1595
+ if (typeof sourceRecord.registry === "string" && !isHttpUrl(sourceRecord.registry)) invalid(diagnostics, `content.sources[${index}].registry must be an absolute HTTP(S) URL.`);
1596
+ const selector = sourceRecord[variant];
1597
+ if (variant === "glob" ? typeof selector !== "string" && (!Array.isArray(selector) || selector.some((value) => typeof value !== "string")) : typeof selector !== "string" || selector.trim() === "") invalid(diagnostics, `content.sources[${index}].${variant} must be ${variant === "glob" ? "a string or string array" : "a non-empty string"}.`);
1598
+ for (const list of ["include", "exclude"]) {
1599
+ const value = sourceRecord[list];
1600
+ if (value !== void 0 && (!Array.isArray(value) || value.some((entry) => typeof entry !== "string"))) invalid(diagnostics, `content.sources[${index}].${list} must be a string array.`);
1601
+ }
1602
+ }
1603
+ if (config.markdown?.stripLeadingBadges !== void 0 && typeof config.markdown.stripLeadingBadges !== "boolean") invalid(diagnostics, "markdown.stripLeadingBadges must be a boolean.");
1604
+ if (config.markdown?.rawHtml !== void 0 && ![
1605
+ "allow",
1606
+ "sanitize",
1607
+ "strip",
1608
+ "reject"
1609
+ ].includes(config.markdown.rawHtml)) invalid(diagnostics, "markdown.rawHtml must be \"allow\", \"sanitize\", \"strip\", or \"reject\".");
1610
+ if (config.search?.enabled !== void 0 && typeof config.search.enabled !== "boolean") invalid(diagnostics, "search.enabled must be a boolean.");
1611
+ for (const key of ["strict", "ci"]) {
1612
+ const value = config.build?.[key];
1613
+ if (value !== void 0 && typeof value !== "boolean") invalid(diagnostics, `build.${key} must be a boolean.`);
1614
+ }
1615
+ for (const key of [
1616
+ "maxArtifactBytes",
1617
+ "maxUnpackedBytes",
1618
+ "maxFiles",
1619
+ "maxPathDepth",
1620
+ "maxAssetBytes"
1621
+ ]) {
1622
+ const value = config.build?.[key];
1623
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) invalid(diagnostics, `build.${key} must be a positive integer.`);
1624
+ }
1625
+ const brand = config.theme?.brand;
1626
+ if (typeof brand === "object" && brand !== null && "contrast" in brand && !brand.unsafeContrast) {
1627
+ const target = brand.contrast?.apca;
1628
+ const normal = Array.isArray(target) ? target[0] : target;
1629
+ if (typeof normal === "number" && normal < 45) diagnostics.push({
1630
+ code: "DOCS_CONFIG_INVALID",
1631
+ severity: "error",
1632
+ message: "theme.brand.contrast.apca cannot be below 45 without unsafeContrast.",
1633
+ hint: "Remove the override, use 45 or higher, or explicitly set unsafeContrast: true."
1634
+ });
1635
+ }
1636
+ if (config.content?.frontmatter !== void 0 && !["preserve", "reject"].includes(config.content.frontmatter)) invalid(diagnostics, "content.frontmatter must be \"preserve\" or \"reject\".");
1637
+ for (const field of [
1638
+ "styles",
1639
+ "customStyles",
1640
+ "palette",
1641
+ "glaze",
1642
+ "units",
1643
+ "recipes",
1644
+ "fonts",
1645
+ "fontLoading",
1646
+ "tokens",
1647
+ "states",
1648
+ "presets",
1649
+ "glaze",
1650
+ "units",
1651
+ "recipes"
1652
+ ]) if (config.theme?.[field] !== void 0 && !isRecord(config.theme[field])) invalid(diagnostics, `theme.${field} must be an object.`);
1653
+ for (const [name, value] of Object.entries(config.theme?.units ?? {})) if (!/^[a-z][a-z0-9]*$/.test(name) || typeof value !== "string" || !value.trim()) invalid(diagnostics, `theme.units.${name} must be a named non-empty CSS length expression.`);
1654
+ for (const [name, recipe] of Object.entries(config.theme?.recipes ?? {})) if (!/^[a-z][a-z0-9-]*$/.test(name) || name === "none" || !isRecord(recipe)) invalid(diagnostics, `theme.recipes.${name} must be a named flat Tasty style object; none is reserved.`);
1655
+ else for (const key of Object.keys(recipe)) if (key === "recipe" || /^[A-Z]/.test(key) || /^[&.]/.test(key)) invalid(diagnostics, `theme.recipes.${name}.${key}: recipes are flat and cannot reference other recipes.`);
1656
+ if (isRecord(config.theme?.glaze)) {
1657
+ const settings = config.theme.glaze;
1658
+ for (const key of Object.keys(settings)) if (![
1659
+ "lightTone",
1660
+ "darkTone",
1661
+ "darkDesaturation",
1662
+ "autoFlip",
1663
+ "pastel",
1664
+ "inferRole",
1665
+ "shadowTuning"
1666
+ ].includes(key)) unknown(diagnostics, `theme.glaze.${key}`);
1667
+ for (const key of [
1668
+ "autoFlip",
1669
+ "pastel",
1670
+ "inferRole"
1671
+ ]) if (settings[key] !== void 0 && typeof settings[key] !== "boolean") invalid(diagnostics, `theme.glaze.${key} must be boolean.`);
1672
+ if (settings.darkDesaturation !== void 0 && (typeof settings.darkDesaturation !== "number" || !Number.isFinite(settings.darkDesaturation) || settings.darkDesaturation < 0 || settings.darkDesaturation > 1)) invalid(diagnostics, "theme.glaze.darkDesaturation must be between 0 and 1.");
1673
+ for (const key of ["lightTone", "darkTone"]) {
1674
+ const window = settings[key];
1675
+ if (window === void 0 || window === false) continue;
1676
+ const lo = Array.isArray(window) ? window[0] : window?.lo;
1677
+ const hi = Array.isArray(window) ? window[1] : window?.hi;
1678
+ if (typeof lo !== "number" || typeof hi !== "number" || !Number.isFinite(lo) || !Number.isFinite(hi) || lo < 0 || hi > 100 || lo >= hi || (Array.isArray(window) ? window.length !== 2 : !isRecord(window) || Object.keys(window).some((key) => ![
1679
+ "lo",
1680
+ "hi",
1681
+ "eps"
1682
+ ].includes(key)) || typeof window.eps !== "number" || !Number.isFinite(window.eps) || window.eps <= 0)) invalid(diagnostics, `theme.glaze.${key} must be false, [lo, hi], or {lo, hi, eps} with an ascending 0–100 range.`);
1683
+ }
1684
+ const tuning = settings.shadowTuning;
1685
+ if (tuning !== void 0) {
1686
+ if (!isRecord(tuning)) invalid(diagnostics, "theme.glaze.shadowTuning must be an object.");
1687
+ else for (const [key, value] of Object.entries(tuning)) {
1688
+ if (![
1689
+ "saturationFactor",
1690
+ "maxSaturation",
1691
+ "lightnessFactor",
1692
+ "lightnessBounds",
1693
+ "minGapTarget",
1694
+ "alphaMax",
1695
+ "bgHueBlend"
1696
+ ].includes(key)) unknown(diagnostics, `theme.glaze.shadowTuning.${key}`);
1697
+ if ((Array.isArray(value) ? value : [value]).some((item) => typeof item !== "number" || !Number.isFinite(item) || item < 0 || item > 1) || key === "lightnessBounds" && (!Array.isArray(value) || value.length !== 2 || value[0] > value[1])) invalid(diagnostics, `theme.glaze.shadowTuning.${key} must use values from 0 to 1.`);
1698
+ }
1699
+ }
1700
+ }
1701
+ if (isRecord(config.theme?.fontLoading)) {
1702
+ const choices = {
1703
+ google: ["self-hosted", "remote"],
1704
+ cache: [
1705
+ "reuse",
1706
+ "refresh",
1707
+ "offline"
1708
+ ],
1709
+ display: [
1710
+ "auto",
1711
+ "block",
1712
+ "swap",
1713
+ "fallback",
1714
+ "optional"
1715
+ ]
1716
+ };
1717
+ for (const [key, value] of Object.entries(config.theme.fontLoading)) {
1718
+ const allowed = choices[key];
1719
+ if (!allowed) unknown(diagnostics, `theme.fontLoading.${key}`);
1720
+ else if (value !== void 0 && (typeof value !== "string" || !allowed.includes(value))) invalid(diagnostics, `theme.fontLoading.${key} must be ${allowed.join(" or ")}.`);
1721
+ }
1722
+ }
1723
+ if (isRecord(config.theme?.fonts)) for (const [role, font] of Object.entries(config.theme.fonts)) {
1724
+ const path = `theme.fonts.${role}`;
1725
+ if (![
1726
+ "body",
1727
+ "heading",
1728
+ "code"
1729
+ ].includes(role)) {
1730
+ unknown(diagnostics, path);
1731
+ continue;
1732
+ }
1733
+ if (font === void 0) continue;
1734
+ if (typeof font === "string") {
1735
+ if (!validFontFamily(font)) invalid(diagnostics, `${path} must name a Google Fonts family.`);
1736
+ continue;
1737
+ }
1738
+ if (!isRecord(font)) {
1739
+ invalid(diagnostics, `${path} must be a Google Fonts name or font definition.`);
1740
+ continue;
1741
+ }
1742
+ if ("google" in font) {
1743
+ for (const key of Object.keys(font)) if (![
1744
+ "google",
1745
+ "weights",
1746
+ "styles"
1747
+ ].includes(key)) unknown(diagnostics, `${path}.${key}`);
1748
+ if (!validFontFamily(font.google)) invalid(diagnostics, `${path}.google must name a Google Fonts family.`);
1749
+ if (font.weights !== void 0 && (!Array.isArray(font.weights) || font.weights.length === 0 || font.weights.some((weight) => !validFontWeight(weight)))) invalid(diagnostics, `${path}.weights must be a non-empty array of weights from 1 to 1000 or ascending ranges such as "100 900".`);
1750
+ if (font.styles !== void 0 && (!Array.isArray(font.styles) || font.styles.length === 0 || font.styles.some((style) => !["normal", "italic"].includes(style)))) invalid(diagnostics, `${path}.styles must contain normal and/or italic.`);
1751
+ } else {
1752
+ for (const key of Object.keys(font)) if (!["family", "files"].includes(key)) unknown(diagnostics, `${path}.${key}`);
1753
+ if (!validFontFamily(font.family)) invalid(diagnostics, `${path}.family must be a font family name.`);
1754
+ if (!Array.isArray(font.files) || font.files.length === 0) {
1755
+ invalid(diagnostics, `${path}.files must contain at least one font file.`);
1756
+ continue;
1757
+ }
1758
+ font.files.forEach((file, index) => {
1759
+ const filePath = `${path}.files[${index}]`;
1760
+ if (!isRecord(file)) {
1761
+ invalid(diagnostics, `${filePath} must be an object.`);
1762
+ return;
1763
+ }
1764
+ for (const key of Object.keys(file)) if (![
1765
+ "src",
1766
+ "weight",
1767
+ "style"
1768
+ ].includes(key)) unknown(diagnostics, `${filePath}.${key}`);
1769
+ if (typeof file.src !== "string" || !/^\/(?!\/)[^\s"'()\\?#]+\.(?:woff2?|ttf|otf)$/.test(file.src) || file.src.split("/").some((segment) => segment === "." || segment === "..")) invalid(diagnostics, `${filePath}.src must be a root-relative .woff2, .woff, .ttf, or .otf public path.`);
1770
+ if (file.weight !== void 0 && !(typeof file.weight === "number" && Number.isInteger(file.weight) && file.weight >= 1 && file.weight <= 1e3) && !(typeof file.weight === "string" && /^(?:[1-9]\d{0,2}|1000) (?:[1-9]\d{0,2}|1000)$/.test(file.weight) && Number(file.weight.split(" ")[0]) < Number(file.weight.split(" ")[1]))) invalid(diagnostics, `${filePath}.weight must be a weight from 1 to 1000 or an ascending range.`);
1771
+ if (file.style !== void 0 && !["normal", "italic"].includes(file.style)) invalid(diagnostics, `${filePath}.style must be normal or italic.`);
1772
+ });
1773
+ }
1774
+ }
1775
+ for (const [name, entry] of Object.entries(config.theme?.styles ?? {})) {
1776
+ if (!COOKBOOK_COMPONENT_NAMES.includes(name)) {
1777
+ unknown(diagnostics, `theme.styles.${name}; register custom components under theme.customStyles`);
1778
+ continue;
1779
+ }
1780
+ if (!isRecord(entry)) invalid(diagnostics, `theme.styles.${name} must be a Tasty style object.`);
1781
+ else {
1782
+ if ("mode" in entry) invalid(diagnostics, `theme.styles.${name} must contain only Tasty properties, without a mode wrapper.`);
1783
+ const elements = COOKBOOK_COMPONENT_SUB_ELEMENTS[name];
1784
+ for (const key of Object.keys(entry)) if (/^[A-Z]/.test(key) && !elements.includes(key)) unknown(diagnostics, `theme.styles.${name}.${key}`);
1785
+ }
1786
+ }
1787
+ for (const [name, entry] of Object.entries(config.theme?.customStyles ?? {})) {
1788
+ if (COOKBOOK_COMPONENT_NAMES.includes(name)) invalid(diagnostics, `Use theme.styles.${name} for a built-in component.`);
1789
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name) || !isRecord(entry) || "mode" in entry) invalid(diagnostics, `theme.customStyles.${name} must be a named partial Tasty style object.`);
1790
+ }
1791
+ for (const [name, value] of Object.entries(config.theme?.palette ?? {})) if (value !== void 0) validatePaletteColor(name, value, diagnostics);
1792
+ if ((config.theme?.palette || config.theme?.glaze) && !diagnostics.some((item) => item.severity === "error")) try {
1793
+ resolveColorTheme(config.theme);
1794
+ } catch (error) {
1795
+ const message = error instanceof Error ? error.message : String(error);
1796
+ const name = /color "([^"]+)"/.exec(message)?.[1];
1797
+ invalid(diagnostics, `theme.palette${name ? `.${name}` : ""}: ${message}`);
1798
+ }
1799
+ if (brand !== void 0) try {
1800
+ if (isBrandDeclaration(brand)) glaze.color({
1801
+ hue: brand.hue,
1802
+ saturation: brand.saturation,
1803
+ tone: brand.tone
1804
+ }).resolve();
1805
+ else glaze.color({ from: typeof brand === "object" && brand !== null && "from" in brand ? brand.from : brand }).resolve();
1806
+ } catch (error) {
1807
+ invalid(diagnostics, `theme.brand: ${error instanceof Error ? error.message : String(error)}`);
1808
+ }
1809
+ const contrast = config.theme?.contrastLevel;
1810
+ if (contrast !== void 0 && contrast !== "auto" && (typeof contrast !== "number" || !Number.isFinite(contrast) || contrast < 0 || contrast > 100)) invalid(diagnostics, "theme.contrastLevel must be \"auto\" or a number from 0 to 100.");
1811
+ const componentOverrides = config.components?.overrides;
1812
+ if (componentOverrides !== void 0 && !isRecord(componentOverrides)) invalid(diagnostics, "components.overrides must be an object.");
1813
+ else for (const [name, override] of Object.entries(componentOverrides ?? {})) {
1814
+ if (typeof override === "string") continue;
1815
+ if (name === "Footer" && override === false) continue;
1816
+ invalid(diagnostics, `components.overrides.${name} must be an Astro component path${name === "Footer" ? " or false" : ""}.`);
1817
+ }
1818
+ return diagnostics;
1819
+ }
1820
+ function validatePaletteColor(name, value, diagnostics) {
1821
+ try {
1822
+ if (!isPaletteDeclaration(value)) {
1823
+ glaze.color({ from: value }).resolve();
1824
+ return;
1825
+ }
1826
+ const keys = "type" in value ? new Set(value.type === "mix" ? [
1827
+ "type",
1828
+ "base",
1829
+ "target",
1830
+ "value",
1831
+ "blend",
1832
+ "space",
1833
+ "contrast",
1834
+ "pastel",
1835
+ "role",
1836
+ "inherit"
1837
+ ] : value.type === "shadow" ? [
1838
+ "type",
1839
+ "bg",
1840
+ "fg",
1841
+ "intensity",
1842
+ "tuning",
1843
+ "pastel",
1844
+ "inherit"
1845
+ ] : []) : COLOR_DECLARATION_KEYS;
1846
+ for (const key of Object.keys(value)) if (!keys.has(key)) unknown(diagnostics, `theme.palette.${name}.${key}`);
1847
+ } catch (error) {
1848
+ invalid(diagnostics, `theme.palette.${name}: ${error instanceof Error ? error.message : String(error)}`);
1849
+ }
1850
+ }
1851
+ function isPaletteDeclaration(value) {
1852
+ return typeof value === "object" && value !== null && !("h" in value || "r" in value || "c" in value);
1853
+ }
1854
+ function isBrandDeclaration(value) {
1855
+ return isRecord(value) && "hue" in value && "saturation" in value && "tone" in value;
1856
+ }
1857
+ function unknown(diagnostics, path) {
1858
+ diagnostics.push({
1859
+ code: "DOCS_CONFIG_UNKNOWN_KEY",
1860
+ severity: "error",
1861
+ message: `Unknown configuration key "${path}".`
1862
+ });
1863
+ }
1864
+ function invalid(diagnostics, message) {
1865
+ diagnostics.push({
1866
+ code: "DOCS_CONFIG_INVALID",
1867
+ severity: "error",
1868
+ message
1869
+ });
1870
+ }
1871
+ function isRecord(value) {
1872
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1873
+ }
1874
+ function validFontFamily(value) {
1875
+ return typeof value === "string" && value.length <= 100 && /^[\p{L}\p{N} ._-]+$/u.test(value) && value.trim() === value && value.length > 0;
1876
+ }
1877
+ function isHttpUrl(value) {
1878
+ try {
1879
+ return ["http:", "https:"].includes(new URL(value).protocol);
1880
+ } catch {
1881
+ return false;
1882
+ }
1883
+ }
1884
+ function validateNavigationItems(items, path, diagnostics) {
1885
+ if (!Array.isArray(items)) {
1886
+ invalid(diagnostics, `${path} must be an array.`);
1887
+ return;
1888
+ }
1889
+ for (const [index, item] of items.entries()) {
1890
+ const itemPath = `${path}[${index}]`;
1891
+ if (typeof item === "string") continue;
1892
+ if (!isRecord(item) || typeof item.label !== "string") {
1893
+ invalid(diagnostics, `${itemPath} must be a route or a labeled navigation item.`);
1894
+ continue;
1895
+ }
1896
+ if (("items" in item || "autogenerate" in item) && item.link !== void 0 && (typeof item.link !== "string" || !/^\/(?!\/)[^?#\\\s]*$/.test(item.link) || item.link.includes("//") || item.link.split("/").some((segment) => segment === "." || segment === ".."))) invalid(diagnostics, `${itemPath}.link must be a root-relative page route without a query or fragment.`);
1897
+ if ("items" in item) validateNavigationItems(item.items, `${itemPath}.items`, diagnostics);
1898
+ else if ("autogenerate" in item) {
1899
+ if (!isRecord(item.autogenerate) || typeof item.autogenerate.directory !== "string") invalid(diagnostics, `${itemPath}.autogenerate.directory must be a directory path.`);
1900
+ } else if (typeof item.link !== "string") invalid(diagnostics, `${itemPath}.link must be a URL or route.`);
1901
+ }
1902
+ }
1903
+ function normalizeDocsConfig(config = {}) {
1904
+ const diagnostics = validateConfig(config);
1905
+ if (diagnostics.length > 0) throw new DocsConfigError(diagnostics);
1906
+ const navigation = Array.isArray(config.navigation) ? { items: config.navigation } : config.navigation ?? {};
1907
+ return {
1908
+ ...config.tableOfContents !== void 0 ? { tableOfContents: config.tableOfContents } : {},
1909
+ redirects: { ...config.redirects },
1910
+ site: { ...config.site },
1911
+ head: [...config.head ?? []],
1912
+ ...config.editLink ? { editLink: { ...config.editLink } } : {},
1913
+ lastUpdated: config.lastUpdated ?? false,
1914
+ ...config.translations ? { translations: config.translations } : {},
1915
+ ...config.locales ? { locales: { ...config.locales } } : {},
1916
+ ...config.defaultLocale ? { defaultLocale: config.defaultLocale } : {},
1917
+ content: {
1918
+ allowOutsideRoot: false,
1919
+ localizeRepositoryLinks: false,
1920
+ ...config.content
1921
+ },
1922
+ navigation,
1923
+ theme: {
1924
+ ...config.theme,
1925
+ brand: config.theme?.brand ?? DEFAULT_BRAND
1926
+ },
1927
+ markdown: {
1928
+ stripLeadingBadges: true,
1929
+ rawHtml: "sanitize",
1930
+ ...config.markdown
1931
+ },
1932
+ search: {
1933
+ enabled: true,
1934
+ ...config.search
1935
+ },
1936
+ components: { ...config.components },
1937
+ build: {
1938
+ strict: true,
1939
+ ci: process.env.CI === "true",
1940
+ base: "/",
1941
+ cacheDir: "",
1942
+ maxArtifactBytes: 26214400,
1943
+ maxUnpackedBytes: 104857600,
1944
+ maxFiles: 1e4,
1945
+ maxPathDepth: 24,
1946
+ maxAssetBytes: 20971520,
1947
+ ...config.build
1948
+ }
1949
+ };
1950
+ }
1951
+ function defineDocsConfig(config) {
1952
+ const diagnostics = validateConfig(config);
1953
+ if (diagnostics.length > 0) throw new DocsConfigError(diagnostics);
1954
+ return config;
1955
+ }
1956
+ /** Compose presets. Color roles and fonts replace, other objects merge, and styles use Tasty semantics. */
1957
+ function mergeDocsConfig(...configs) {
1958
+ const merge = (base, next) => {
1959
+ const result = { ...base };
1960
+ for (const [key, value] of Object.entries(next)) {
1961
+ if (value === void 0) continue;
1962
+ result[key] = isRecord(value) ? merge(isRecord(result[key]) ? result[key] : {}, value) : Array.isArray(value) ? [...value] : value;
1963
+ }
1964
+ return result;
1965
+ };
1966
+ return defineDocsConfig(configs.reduce((base, next) => {
1967
+ const { palette: _palette, ...themeWithoutPalette } = next.theme ?? {};
1968
+ const diagnostics = validateConfig({
1969
+ ...next,
1970
+ ...next.theme ? { theme: themeWithoutPalette } : {}
1971
+ });
1972
+ if (diagnostics.length) throw new DocsConfigError(diagnostics);
1973
+ const result = merge(base, next);
1974
+ if (next.site?.logo !== void 0) result.site.logo = next.site.logo;
1975
+ if (next.site?.seo?.image !== void 0) result.site.seo.image = next.site.seo.image;
1976
+ if (next.theme?.brand !== void 0) result.theme.brand = next.theme.brand;
1977
+ if (next.theme?.palette) result.theme.palette = {
1978
+ ...base.theme?.palette,
1979
+ ...Object.fromEntries(Object.entries(next.theme.palette).filter(([, color]) => color !== void 0))
1980
+ };
1981
+ if (next.theme?.fonts) result.theme.fonts = {
1982
+ ...base.theme?.fonts,
1983
+ ...Object.fromEntries(Object.entries(next.theme.fonts).filter(([, font]) => font !== void 0))
1984
+ };
1985
+ for (const field of [
1986
+ "styles",
1987
+ "customStyles",
1988
+ "recipes"
1989
+ ]) {
1990
+ if (!next.theme?.[field]) continue;
1991
+ const styles = { ...base.theme?.[field] };
1992
+ for (const [name, configured] of Object.entries(next.theme[field])) styles[name] = mergeStyles(styles[name] ?? {}, configured);
1993
+ if (field === "recipes") result.theme.recipes = styles;
1994
+ else result.theme[field] = styles;
1995
+ }
1996
+ return result;
1997
+ }, {}));
1998
+ }
1999
+ function validFontWeight(weight) {
2000
+ return typeof weight === "number" && Number.isInteger(weight) && weight >= 1 && weight <= 1e3 || typeof weight === "string" && /^(?:[1-9]\d{0,2}|1000) (?:[1-9]\d{0,2}|1000)$/.test(weight) && Number(weight.split(" ")[0]) < Number(weight.split(" ")[1]);
2001
+ }
2002
+ //#endregion
2003
+ export { validateConfig as a, COOKBOOK_PALETTE_NAMES as c, routeUrl as d, routeVersion as f, normalizeDocsConfig as i, resolveColorTheme as l, validateTableOfContents as m, defineDocsConfig as n, COOKBOOK_COMPONENT_NAMES as o, validateSeo as p, mergeDocsConfig as r, COOKBOOK_COMPONENT_SUB_ELEMENTS as s, DocsConfigError as t, pagePublishing as u };
2004
+
2005
+ //# sourceMappingURL=config-CD77r6rT.js.map