@caelo-cms/shared 0.10.22 → 0.10.24
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/dist/ai-tools.d.ts +289 -273
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +342 -323
- package/dist/ai-tools.js.map +1 -1
- package/dist/auth-forms.d.ts.map +1 -1
- package/dist/auth-forms.js +4 -1
- package/dist/auth-forms.js.map +1 -1
- package/dist/base-css.d.ts +35 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +40 -0
- package/dist/base-css.js.map +1 -0
- package/dist/build-page.d.ts +330 -0
- package/dist/build-page.d.ts.map +1 -0
- package/dist/build-page.js +282 -0
- package/dist/build-page.js.map +1 -0
- package/dist/content.d.ts +322 -9
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +354 -11
- package/dist/content.js.map +1 -1
- package/dist/css-gradient-scan.d.ts +14 -0
- package/dist/css-gradient-scan.d.ts.map +1 -0
- package/dist/css-gradient-scan.js +81 -0
- package/dist/css-gradient-scan.js.map +1 -0
- package/dist/css-var-scan.d.ts +56 -0
- package/dist/css-var-scan.d.ts.map +1 -0
- package/dist/css-var-scan.js +97 -0
- package/dist/css-var-scan.js.map +1 -0
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/design-manifest.d.ts +36 -0
- package/dist/design-manifest.d.ts.map +1 -0
- package/dist/design-manifest.js +90 -0
- package/dist/design-manifest.js.map +1 -0
- package/dist/fonts.d.ts +89 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +241 -0
- package/dist/fonts.js.map +1 -0
- package/dist/genesis-inventory.d.ts +32 -0
- package/dist/genesis-inventory.d.ts.map +1 -0
- package/dist/genesis-inventory.js +186 -0
- package/dist/genesis-inventory.js.map +1 -0
- package/dist/genesis.d.ts +102 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +145 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +29 -36
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +53 -128
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.d.ts +23 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +44 -0
- package/dist/interactions.js.map +1 -0
- package/dist/media.d.ts +101 -16
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +126 -15
- package/dist/media.js.map +1 -1
- package/dist/page-log.d.ts +94 -0
- package/dist/page-log.d.ts.map +1 -0
- package/dist/page-log.js +111 -0
- package/dist/page-log.js.map +1 -0
- package/dist/preview-compose.d.ts +85 -12
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +157 -67
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +40 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +34 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/responsive-images.d.ts +64 -0
- package/dist/responsive-images.d.ts.map +1 -0
- package/dist/responsive-images.js +98 -0
- package/dist/responsive-images.js.map +1 -0
- package/dist/safe-keys.d.ts +9 -0
- package/dist/safe-keys.d.ts.map +1 -0
- package/dist/safe-keys.js +20 -0
- package/dist/safe-keys.js.map +1 -0
- package/dist/seo.d.ts +11 -16
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +7 -23
- package/dist/seo.js.map +1 -1
- package/dist/skills.d.ts +14 -68
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +19 -113
- package/dist/skills.js.map +1 -1
- package/dist/strip-cdata.d.ts +7 -0
- package/dist/strip-cdata.d.ts.map +1 -0
- package/dist/strip-cdata.js +48 -0
- package/dist/strip-cdata.js.map +1 -0
- package/dist/structured-sets.d.ts +6 -52
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +6 -68
- package/dist/structured-sets.js.map +1 -1
- package/dist/subagents.d.ts +105 -3
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +224 -41
- package/dist/subagents.js.map +1 -1
- package/dist/template-engine.d.ts +85 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +403 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/theme-importers/auto-detect.d.ts +26 -0
- package/dist/theme-importers/auto-detect.d.ts.map +1 -0
- package/dist/theme-importers/auto-detect.js +42 -0
- package/dist/theme-importers/auto-detect.js.map +1 -0
- package/dist/theme-importers/css-comments.d.ts +12 -0
- package/dist/theme-importers/css-comments.d.ts.map +1 -0
- package/dist/theme-importers/css-comments.js +15 -0
- package/dist/theme-importers/css-comments.js.map +1 -0
- package/dist/theme-importers/dtcg.d.ts +46 -0
- package/dist/theme-importers/dtcg.d.ts.map +1 -0
- package/dist/theme-importers/dtcg.js +111 -0
- package/dist/theme-importers/dtcg.js.map +1 -0
- package/dist/theme-importers/loose.d.ts +3 -0
- package/dist/theme-importers/loose.d.ts.map +1 -0
- package/dist/theme-importers/loose.js +76 -0
- package/dist/theme-importers/loose.js.map +1 -0
- package/dist/theme-importers/shadcn.d.ts +24 -0
- package/dist/theme-importers/shadcn.d.ts.map +1 -0
- package/dist/theme-importers/shadcn.js +135 -0
- package/dist/theme-importers/shadcn.js.map +1 -0
- package/dist/theme-importers/style-dictionary.d.ts +17 -0
- package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
- package/dist/theme-importers/style-dictionary.js +125 -0
- package/dist/theme-importers/style-dictionary.js.map +1 -0
- package/dist/theme-importers/tailwind.d.ts +3 -0
- package/dist/theme-importers/tailwind.d.ts.map +1 -0
- package/dist/theme-importers/tailwind.js +218 -0
- package/dist/theme-importers/tailwind.js.map +1 -0
- package/dist/theme-literal-binding.d.ts +37 -0
- package/dist/theme-literal-binding.d.ts.map +1 -0
- package/dist/theme-literal-binding.js +138 -0
- package/dist/theme-literal-binding.js.map +1 -0
- package/dist/theme-normalize.d.ts +31 -0
- package/dist/theme-normalize.d.ts.map +1 -0
- package/dist/theme-normalize.js +587 -0
- package/dist/theme-normalize.js.map +1 -0
- package/dist/theme-ramp.d.ts +55 -0
- package/dist/theme-ramp.d.ts.map +1 -0
- package/dist/theme-ramp.js +149 -0
- package/dist/theme-ramp.js.map +1 -0
- package/dist/theme-render.d.ts +105 -0
- package/dist/theme-render.d.ts.map +1 -0
- package/dist/theme-render.js +441 -0
- package/dist/theme-render.js.map +1 -0
- package/dist/themes-errors.d.ts +109 -0
- package/dist/themes-errors.d.ts.map +1 -0
- package/dist/themes-errors.js +170 -0
- package/dist/themes-errors.js.map +1 -0
- package/dist/themes.d.ts +343 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +697 -0
- package/dist/themes.js.map +1 -0
- package/dist/version.d.ts +7 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -2
- package/src/__tests__/redos-hardening.test.ts +160 -0
- package/src/ai-tools-add-module-modes.test.ts +106 -0
- package/src/ai-tools-position.test.ts +134 -0
- package/src/ai-tools.test.ts +81 -0
- package/src/ai-tools.ts +1105 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +42 -0
- package/src/build-page.test.ts +228 -0
- package/src/build-page.ts +319 -0
- package/src/cap-failures.ts +67 -0
- package/src/content.test.ts +170 -0
- package/src/content.ts +620 -0
- package/src/context.ts +43 -0
- package/src/css-gradient-scan.ts +88 -0
- package/src/css-var-scan.test.ts +96 -0
- package/src/css-var-scan.ts +144 -0
- package/src/derive-module-type.test.ts +80 -0
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/design-manifest.ts +93 -0
- package/src/fonts.test.ts +157 -0
- package/src/fonts.ts +296 -0
- package/src/genesis-inventory.test.ts +86 -0
- package/src/genesis-inventory.ts +215 -0
- package/src/genesis-sanitize.test.ts +35 -0
- package/src/genesis.ts +158 -0
- package/src/i18n.test.ts +58 -0
- package/src/i18n.ts +91 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +59 -0
- package/src/interactions.ts +48 -0
- package/src/logger.ts +147 -0
- package/src/media.test.ts +160 -0
- package/src/media.ts +355 -0
- package/src/page-log.test.ts +163 -0
- package/src/page-log.ts +124 -0
- package/src/preview-compose.test.ts +637 -0
- package/src/preview-compose.ts +656 -0
- package/src/preview-scanner.test.ts +96 -0
- package/src/preview-scanner.ts +214 -0
- package/src/proposal-status.test.ts +69 -0
- package/src/proposal-status.ts +40 -0
- package/src/responsive-images.test.ts +104 -0
- package/src/responsive-images.ts +151 -0
- package/src/result.ts +29 -0
- package/src/safe-keys.ts +21 -0
- package/src/seo.test.ts +194 -0
- package/src/seo.ts +233 -0
- package/src/skills.ts +48 -0
- package/src/snapshots.test.ts +80 -0
- package/src/snapshots.ts +81 -0
- package/src/strip-cdata.test.ts +41 -0
- package/src/strip-cdata.ts +50 -0
- package/src/structured-sets.ts +114 -0
- package/src/subagents.test.ts +262 -0
- package/src/subagents.ts +432 -0
- package/src/template-engine.test.ts +379 -0
- package/src/template-engine.ts +520 -0
- package/src/theme-gradient.test.ts +92 -0
- package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
- package/src/theme-importers/auto-detect.ts +84 -0
- package/src/theme-importers/css-comments.ts +15 -0
- package/src/theme-importers/dtcg.ts +106 -0
- package/src/theme-importers/loose.ts +76 -0
- package/src/theme-importers/shadcn.ts +133 -0
- package/src/theme-importers/style-dictionary.ts +125 -0
- package/src/theme-importers/tailwind.ts +217 -0
- package/src/theme-literal-binding.test.ts +71 -0
- package/src/theme-literal-binding.ts +159 -0
- package/src/theme-motion.test.ts +115 -0
- package/src/theme-normalize-envelope.test.ts +43 -0
- package/src/theme-normalize-gradient.test.ts +135 -0
- package/src/theme-normalize.ts +661 -0
- package/src/theme-ramp.ts +187 -0
- package/src/theme-render-sanitize.test.ts +45 -0
- package/src/theme-render.test.ts +119 -0
- package/src/theme-render.ts +487 -0
- package/src/theme-shadow.test.ts +56 -0
- package/src/themes-errors.ts +199 -0
- package/src/themes.ts +842 -0
- package/src/version.ts +66 -0
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* issue #164 — design-compiler stage 1: the deterministic INVENTORY.
|
|
5
|
+
*
|
|
6
|
+
* Compiler contract (epic #149): the AI decides, code executes and
|
|
7
|
+
* verifies. This module is the "code" half of the first stage — it
|
|
8
|
+
* parses a Genesis draft (complete single-file HTML) into a fact base
|
|
9
|
+
* of design values (colors with usage contexts, font families, size /
|
|
10
|
+
* spacing / radius histograms, gradients, shadows) plus a section
|
|
11
|
+
* outline. The AI reads the inventory and makes the DECISIONS (which
|
|
12
|
+
* literal becomes which token, what things are called); it never has
|
|
13
|
+
* to re-derive facts by eyeballing raw CSS, and the facts can't be
|
|
14
|
+
* hallucinated.
|
|
15
|
+
*
|
|
16
|
+
* Pure string analysis, linear scans, unambiguous regex classes
|
|
17
|
+
* (ReDoS-aware per issue #113 — drafts are AI-authored but the parser
|
|
18
|
+
* budget must hold on adversarial input too).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { scanCssGradients } from "./css-gradient-scan.js";
|
|
22
|
+
|
|
23
|
+
/** One distinct color literal with where/how often the draft uses it. */
|
|
24
|
+
export interface ColorUsage {
|
|
25
|
+
readonly value: string;
|
|
26
|
+
readonly count: number;
|
|
27
|
+
/** Distinct declaration properties using it (background, color, …). */
|
|
28
|
+
readonly properties: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GenesisDraftInventory {
|
|
32
|
+
readonly colors: readonly ColorUsage[];
|
|
33
|
+
readonly gradients: readonly string[];
|
|
34
|
+
readonly fontFamilies: readonly string[];
|
|
35
|
+
readonly fontSizes: readonly string[];
|
|
36
|
+
readonly spacingValues: readonly string[];
|
|
37
|
+
readonly radiusValues: readonly string[];
|
|
38
|
+
readonly shadows: readonly string[];
|
|
39
|
+
/** Ordered structural outline of the body (tag + text snippet). */
|
|
40
|
+
readonly outline: readonly { tag: string; text: string }[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Linear extraction of every <style> block's contents. */
|
|
44
|
+
export function extractDraftCss(html: string): string {
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
const lower = html.toLowerCase();
|
|
47
|
+
let from = 0;
|
|
48
|
+
while (true) {
|
|
49
|
+
const open = lower.indexOf("<style", from);
|
|
50
|
+
if (open === -1) break;
|
|
51
|
+
const openEnd = lower.indexOf(">", open);
|
|
52
|
+
if (openEnd === -1) break;
|
|
53
|
+
const close = lower.indexOf("</style", openEnd);
|
|
54
|
+
if (close === -1) break;
|
|
55
|
+
parts.push(html.slice(openEnd + 1, close));
|
|
56
|
+
from = close + 8;
|
|
57
|
+
}
|
|
58
|
+
return parts.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const DECLARATION_RE = /([a-zA-Z-]+)\s*:\s*([^;{}]+)/g;
|
|
62
|
+
const COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\b|(?:oklch|rgba?|hsla?|lab|lch|hwb)\([^()]*\)/g;
|
|
63
|
+
const LENGTH_RE = /-?\d+(?:\.\d+)?(?:rem|em|px|vh|vw|ch|%)\b/g;
|
|
64
|
+
|
|
65
|
+
const SPACING_PROPS = new Set([
|
|
66
|
+
"margin",
|
|
67
|
+
"margin-top",
|
|
68
|
+
"margin-bottom",
|
|
69
|
+
"margin-left",
|
|
70
|
+
"margin-right",
|
|
71
|
+
"margin-block",
|
|
72
|
+
"margin-inline",
|
|
73
|
+
"padding",
|
|
74
|
+
"padding-top",
|
|
75
|
+
"padding-bottom",
|
|
76
|
+
"padding-left",
|
|
77
|
+
"padding-right",
|
|
78
|
+
"padding-block",
|
|
79
|
+
"padding-inline",
|
|
80
|
+
"gap",
|
|
81
|
+
"row-gap",
|
|
82
|
+
"column-gap",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const GENERIC_FONT_KEYWORDS: ReadonlySet<string> = new Set([
|
|
86
|
+
"serif",
|
|
87
|
+
"sans-serif",
|
|
88
|
+
"monospace",
|
|
89
|
+
"cursive",
|
|
90
|
+
"fantasy",
|
|
91
|
+
"system-ui",
|
|
92
|
+
"ui-sans-serif",
|
|
93
|
+
"ui-serif",
|
|
94
|
+
"ui-monospace",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
function pushCount(map: Map<string, number>, key: string): void {
|
|
98
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sortedByCount(map: Map<string, number>): string[] {
|
|
102
|
+
return [...map.entries()].sort((a, z) => z[1] - a[1]).map(([k]) => k);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const OUTLINE_TAG_RE = /<(section|header|footer|main|nav|h1|h2|h3)\b[^>]*>/gi;
|
|
106
|
+
const TAG_STRIP_RE = /<[^>]*>/g;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build the inventory. Caps guard pathological drafts: the boundary
|
|
110
|
+
* already limits draft size (GENESIS_DRAFT_MAX_HTML_BYTES), and each
|
|
111
|
+
* result list is truncated to what a decision-maker can actually use.
|
|
112
|
+
*/
|
|
113
|
+
export function inventoryGenesisDraft(html: string): GenesisDraftInventory {
|
|
114
|
+
const css = extractDraftCss(html);
|
|
115
|
+
|
|
116
|
+
const colorMeta = new Map<string, { count: number; properties: Set<string> }>();
|
|
117
|
+
const gradients = new Map<string, number>();
|
|
118
|
+
const fontFamilies = new Map<string, number>();
|
|
119
|
+
const fontSizes = new Map<string, number>();
|
|
120
|
+
const spacing = new Map<string, number>();
|
|
121
|
+
const radii = new Map<string, number>();
|
|
122
|
+
const shadows = new Map<string, number>();
|
|
123
|
+
|
|
124
|
+
for (const decl of css.matchAll(DECLARATION_RE)) {
|
|
125
|
+
const prop = (decl[1] ?? "").toLowerCase();
|
|
126
|
+
const value = (decl[2] ?? "").trim();
|
|
127
|
+
|
|
128
|
+
for (const g of scanCssGradients(value)) {
|
|
129
|
+
pushCount(gradients, g.literal);
|
|
130
|
+
}
|
|
131
|
+
// Colors inside gradients still count — a gradient stop color is a
|
|
132
|
+
// palette member the token map must name.
|
|
133
|
+
for (const c of value.matchAll(COLOR_LITERAL_RE)) {
|
|
134
|
+
const key = c[0].toLowerCase();
|
|
135
|
+
const meta = colorMeta.get(key) ?? { count: 0, properties: new Set<string>() };
|
|
136
|
+
meta.count += 1;
|
|
137
|
+
meta.properties.add(prop);
|
|
138
|
+
colorMeta.set(key, meta);
|
|
139
|
+
}
|
|
140
|
+
if (prop === "font-family") {
|
|
141
|
+
for (const fam of value.split(",")) {
|
|
142
|
+
const clean = fam.trim().replace(/^["']|["']$/g, "");
|
|
143
|
+
// Generic keywords are fallback-chain noise, not palette
|
|
144
|
+
// members — the token map names real typefaces.
|
|
145
|
+
if (
|
|
146
|
+
clean.length > 0 &&
|
|
147
|
+
clean.length < 80 &&
|
|
148
|
+
!GENERIC_FONT_KEYWORDS.has(clean.toLowerCase())
|
|
149
|
+
) {
|
|
150
|
+
pushCount(fontFamilies, clean);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (prop === "font-size") {
|
|
154
|
+
pushCount(fontSizes, value);
|
|
155
|
+
} else if (SPACING_PROPS.has(prop)) {
|
|
156
|
+
for (const len of value.matchAll(LENGTH_RE)) pushCount(spacing, len[0]);
|
|
157
|
+
} else if (prop === "border-radius") {
|
|
158
|
+
pushCount(radii, value);
|
|
159
|
+
} else if (prop === "box-shadow" || prop === "text-shadow") {
|
|
160
|
+
pushCount(shadows, value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const outline: { tag: string; text: string }[] = [];
|
|
165
|
+
for (const m of html.matchAll(OUTLINE_TAG_RE)) {
|
|
166
|
+
if (outline.length >= 60) break;
|
|
167
|
+
const tag = (m[1] ?? "").toLowerCase();
|
|
168
|
+
const start = (m.index ?? 0) + m[0].length;
|
|
169
|
+
const text = html
|
|
170
|
+
.slice(start, start + 400)
|
|
171
|
+
.replace(TAG_STRIP_RE, " ")
|
|
172
|
+
.replace(/\s+/g, " ")
|
|
173
|
+
.trim()
|
|
174
|
+
.slice(0, 80);
|
|
175
|
+
outline.push({ tag, text });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
colors: [...colorMeta.entries()]
|
|
180
|
+
.sort((a, z) => z[1].count - a[1].count)
|
|
181
|
+
.slice(0, 40)
|
|
182
|
+
.map(([value, meta]) => ({
|
|
183
|
+
value,
|
|
184
|
+
count: meta.count,
|
|
185
|
+
properties: [...meta.properties].sort(),
|
|
186
|
+
})),
|
|
187
|
+
gradients: sortedByCount(gradients).slice(0, 12),
|
|
188
|
+
fontFamilies: sortedByCount(fontFamilies).slice(0, 12),
|
|
189
|
+
fontSizes: sortedByCount(fontSizes).slice(0, 24),
|
|
190
|
+
spacingValues: sortedByCount(spacing).slice(0, 24),
|
|
191
|
+
radiusValues: sortedByCount(radii).slice(0, 12),
|
|
192
|
+
shadows: sortedByCount(shadows).slice(0, 12),
|
|
193
|
+
outline,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Compact, prompt-friendly rendering of the inventory. */
|
|
198
|
+
export function formatGenesisInventory(inv: GenesisDraftInventory): string {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
lines.push(
|
|
201
|
+
`Colors (${inv.colors.length}): ${inv.colors
|
|
202
|
+
.map((c) => `${c.value}×${c.count}[${c.properties.join("/")}]`)
|
|
203
|
+
.join(", ")}`,
|
|
204
|
+
);
|
|
205
|
+
if (inv.gradients.length > 0) lines.push(`Gradients: ${inv.gradients.join(" | ")}`);
|
|
206
|
+
lines.push(`Font families: ${inv.fontFamilies.join(", ") || "(none declared)"}`);
|
|
207
|
+
lines.push(`Font sizes: ${inv.fontSizes.join(", ")}`);
|
|
208
|
+
lines.push(`Spacing values: ${inv.spacingValues.join(", ")}`);
|
|
209
|
+
if (inv.radiusValues.length > 0) lines.push(`Radii: ${inv.radiusValues.join(", ")}`);
|
|
210
|
+
if (inv.shadows.length > 0) lines.push(`Shadows: ${inv.shadows.join(" | ")}`);
|
|
211
|
+
lines.push(
|
|
212
|
+
`Outline: ${inv.outline.map((o) => `<${o.tag}>${o.text ? ` ${o.text}` : ""}`).join(" → ")}`,
|
|
213
|
+
);
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* issue #199 — sanitizeDraftHtml. Adversarial per CLAUDE.md §6:
|
|
5
|
+
* operator-provided HTML becomes a stored draft that flows into
|
|
6
|
+
* materialisation, so script removal must survive casing tricks,
|
|
7
|
+
* multiple blocks, and unterminated tags.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { sanitizeDraftHtml } from "./genesis.js";
|
|
12
|
+
|
|
13
|
+
describe("sanitizeDraftHtml (#199)", () => {
|
|
14
|
+
it("strips script blocks, keeps everything else byte-identical", () => {
|
|
15
|
+
const html = `<html><head><style>.a{color:red}</style><script>alert(1)</script></head><body><h1>Hi</h1></body></html>`;
|
|
16
|
+
expect(sanitizeDraftHtml(html)).toBe(
|
|
17
|
+
`<html><head><style>.a{color:red}</style></head><body><h1>Hi</h1></body></html>`,
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("survives casing + attributes + multiple blocks", () => {
|
|
22
|
+
const html = `<SCRIPT src="evil.js"></SCRIPT><p>a</p><ScRiPt type="module">fetch('/x')</ScRiPt><p>b</p>`;
|
|
23
|
+
expect(sanitizeDraftHtml(html)).toBe(`<p>a</p><p>b</p>`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("an unterminated script drops to EOF rather than leaking half a payload", () => {
|
|
27
|
+
const html = `<p>keep</p><script>document.cookie`;
|
|
28
|
+
expect(sanitizeDraftHtml(html)).toBe(`<p>keep</p>`);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("leaves script-free documents untouched", () => {
|
|
32
|
+
const html = `<body><section class="description">javascript is a word, <script> is an entity</section></body>`;
|
|
33
|
+
expect(sanitizeDraftHtml(html)).toBe(html);
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/genesis.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* issue #163 — Site Genesis shared shapes; issue #375 generalises the
|
|
5
|
+
* same draft loop to growth-time.
|
|
6
|
+
*
|
|
7
|
+
* Design-time is divergent: the AI drafts complete freeform HTML pages
|
|
8
|
+
* (one per design direction) and the operator picks one; the CMS
|
|
9
|
+
* structure is derived FROM that choice (#164's compiler). Growth-time
|
|
10
|
+
* reuses the identical draft → present → select → materialise loop at
|
|
11
|
+
* smaller scope: `page` and `module` drafts are FRAGMENTS bound to the
|
|
12
|
+
* site's `var(--…)` theme tokens, composed into the real theme shell
|
|
13
|
+
* only at view time (never an AI approximation of the theme — #375).
|
|
14
|
+
* These are the boundary schemas for the brief + the draft rows.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The structured Design Brief the discovery dialog produces. Every
|
|
21
|
+
* field optional — the AI fills what the conversation answered; the
|
|
22
|
+
* draft prompts degrade gracefully on gaps. Stored on
|
|
23
|
+
* `site_defaults.design_brief`.
|
|
24
|
+
*/
|
|
25
|
+
export const designBriefSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
audience: z.string().min(1).max(500).optional(),
|
|
28
|
+
/** 3–5 adjectives the operator wants the design to feel like. */
|
|
29
|
+
moodWords: z.array(z.string().min(1).max(40)).max(12).optional(),
|
|
30
|
+
tone: z.string().min(1).max(300).optional(),
|
|
31
|
+
industry: z.string().min(1).max(200).optional(),
|
|
32
|
+
differentiators: z.string().min(1).max(1000).optional(),
|
|
33
|
+
imageryDirection: z.string().min(1).max(500).optional(),
|
|
34
|
+
avoid: z.string().min(1).max(500).optional(),
|
|
35
|
+
})
|
|
36
|
+
.strict();
|
|
37
|
+
export type DesignBrief = z.infer<typeof designBriefSchema>;
|
|
38
|
+
|
|
39
|
+
/** Complete single-file drafts stay well under this (inline CSS only). */
|
|
40
|
+
export const GENESIS_DRAFT_MAX_HTML_BYTES = 300_000;
|
|
41
|
+
|
|
42
|
+
export const genesisDraftStatus = z.enum(["candidate", "selected", "discarded"]);
|
|
43
|
+
export type GenesisDraftStatus = z.infer<typeof genesisDraftStatus>;
|
|
44
|
+
|
|
45
|
+
/** issue #199 — where a draft came from. */
|
|
46
|
+
export const genesisDraftSourceKind = z.enum(["genesis", "byod_image", "byod_html"]);
|
|
47
|
+
export type GenesisDraftSourceKind = z.infer<typeof genesisDraftSourceKind>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* issue #375 — what a draft covers. `site` is the Genesis case (a whole
|
|
51
|
+
* new design); `page` / `module` are growth-time variants of something
|
|
52
|
+
* that already exists.
|
|
53
|
+
*/
|
|
54
|
+
export const designDraftScope = z.enum(["site", "page", "module"]);
|
|
55
|
+
export type DesignDraftScope = z.infer<typeof designDraftScope>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* issue #375 — how a draft's HTML is stored. `site` drafts are complete
|
|
59
|
+
* standalone documents (Genesis, unchanged); `page`/`module` drafts are
|
|
60
|
+
* fragments that only become a renderable document when composed with
|
|
61
|
+
* the site's real theme shell at view time — so a stored variant never
|
|
62
|
+
* freezes a stale copy of the theme.
|
|
63
|
+
*/
|
|
64
|
+
export const designDraftFormat = z.enum(["document", "fragment"]);
|
|
65
|
+
export type DesignDraftFormat = z.infer<typeof designDraftFormat>;
|
|
66
|
+
|
|
67
|
+
/** The stored format is fully determined by the scope (see the paired
|
|
68
|
+
* DB CHECK constraint): `site` → standalone document, else fragment. */
|
|
69
|
+
export function draftFormatForScope(scope: DesignDraftScope): DesignDraftFormat {
|
|
70
|
+
return scope === "site" ? "document" : "fragment";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const addDraftBase = z
|
|
74
|
+
.object({
|
|
75
|
+
/** Human-readable design direction ("bold editorial"). */
|
|
76
|
+
direction: z.string().min(3).max(120),
|
|
77
|
+
/** Why this direction fits the brief — shown beside the preview. */
|
|
78
|
+
rationale: z.string().max(1000).default(""),
|
|
79
|
+
/** Site scope: complete self-contained single-file HTML. Page/module
|
|
80
|
+
* scope: an HTML fragment bound to the site's `var(--…)` tokens. */
|
|
81
|
+
html: z.string().min(200).max(GENESIS_DRAFT_MAX_HTML_BYTES),
|
|
82
|
+
/** issue #375 — what this draft covers; `site` is the Genesis case. */
|
|
83
|
+
scope: designDraftScope.default("site"),
|
|
84
|
+
/** issue #375 — page scope: the page this variant redesigns. Module
|
|
85
|
+
* scope: optional page context for "this module as used on …". */
|
|
86
|
+
targetPageId: z.string().uuid().optional(),
|
|
87
|
+
/** issue #375 — module scope: the module this variant restyles. */
|
|
88
|
+
targetModuleId: z.string().uuid().optional(),
|
|
89
|
+
/** issue #375 — groups one request's variants into one comparison.
|
|
90
|
+
* Omit on the first save (the op mints and returns it); pass it on
|
|
91
|
+
* every sibling save of the same round. */
|
|
92
|
+
variantSetId: z.string().uuid().optional(),
|
|
93
|
+
/** issue #199 — 'byod_image' (AI reproduction of an operator
|
|
94
|
+
* mockup) / 'byod_html' (operator HTML, sanitised at the op). */
|
|
95
|
+
sourceKind: genesisDraftSourceKind.default("genesis"),
|
|
96
|
+
/** issue #199 — byod_image only: the uploaded mockup's media
|
|
97
|
+
* asset. The parity gate uses THIS image as the reference. */
|
|
98
|
+
referenceAssetId: z.string().uuid().optional(),
|
|
99
|
+
})
|
|
100
|
+
.strict();
|
|
101
|
+
|
|
102
|
+
/** Scope/target pairing rules shared by the op and the AI tool. */
|
|
103
|
+
export const genesisAddDraftInput = addDraftBase.superRefine((v, ctx) => {
|
|
104
|
+
if (v.scope === "site") {
|
|
105
|
+
if (v.targetPageId !== undefined || v.targetModuleId !== undefined) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: z.ZodIssueCode.custom,
|
|
108
|
+
message: "site-scope drafts take no target — drop targetPageId/targetModuleId",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} else if (v.sourceKind !== "genesis") {
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
message:
|
|
115
|
+
"byod drafts are site-scope only (issue #199); growth-time variants use sourceKind 'genesis'",
|
|
116
|
+
});
|
|
117
|
+
} else if (v.scope === "page") {
|
|
118
|
+
if (v.targetPageId === undefined || v.targetModuleId !== undefined) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: z.ZodIssueCode.custom,
|
|
121
|
+
message: "page-scope drafts need targetPageId (and no targetModuleId)",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
} else if (v.targetModuleId === undefined) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: z.ZodIssueCode.custom,
|
|
127
|
+
message: "module-scope drafts need targetModuleId (targetPageId is optional page context)",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
export type GenesisAddDraftInput = z.infer<typeof genesisAddDraftInput>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* issue #199 — strip <script> blocks from operator-provided draft
|
|
135
|
+
* HTML. Defense in depth: drafts render inside sandbox="" iframes
|
|
136
|
+
* (scripts never execute there), but stored HTML flows onward into
|
|
137
|
+
* materialisation, so the boundary removes them outright. Linear
|
|
138
|
+
* scan per the #113 discipline; case-insensitive; unterminated
|
|
139
|
+
* script blocks drop everything to EOF (never leak half a script).
|
|
140
|
+
*/
|
|
141
|
+
export function sanitizeDraftHtml(html: string): string {
|
|
142
|
+
const lower = html.toLowerCase();
|
|
143
|
+
let out = "";
|
|
144
|
+
let from = 0;
|
|
145
|
+
while (true) {
|
|
146
|
+
const open = lower.indexOf("<script", from);
|
|
147
|
+
if (open === -1) {
|
|
148
|
+
out += html.slice(from);
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
out += html.slice(from, open);
|
|
152
|
+
const close = lower.indexOf("</script", open);
|
|
153
|
+
if (close === -1) return out;
|
|
154
|
+
const closeEnd = lower.indexOf(">", close);
|
|
155
|
+
if (closeEnd === -1) return out;
|
|
156
|
+
from = closeEnd + 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/i18n.test.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { computeContentHash, isHomeSlug, pageIsLocaleHome } from "./i18n.js";
|
|
5
|
+
|
|
6
|
+
describe("isHomeSlug", () => {
|
|
7
|
+
it("recognises the magic-slug sentinels regardless of surrounding slashes", () => {
|
|
8
|
+
for (const s of ["", "home", "index", "/", "/home/", "/index"]) {
|
|
9
|
+
expect(isHomeSlug(s)).toBe(true);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("treats any other slug as non-home", () => {
|
|
14
|
+
for (const s of ["about", "en", "blog/post", "homepage"]) {
|
|
15
|
+
expect(isHomeSlug(s)).toBe(false);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("pageIsLocaleHome (0184 shared predicate)", () => {
|
|
21
|
+
const PAGE = "11111111-1111-4111-8111-111111111111";
|
|
22
|
+
const OTHER = "22222222-2222-4222-8222-222222222222";
|
|
23
|
+
|
|
24
|
+
it("is true when the page IS the locale's designated home_page_id", () => {
|
|
25
|
+
expect(pageIsLocaleHome(PAGE, "en", PAGE)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("is true for a magic slug even with no designation", () => {
|
|
29
|
+
expect(pageIsLocaleHome(PAGE, "home", null)).toBe(true);
|
|
30
|
+
expect(pageIsLocaleHome(PAGE, "index", undefined)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is false for a non-magic slug that isn't the designated page", () => {
|
|
34
|
+
expect(pageIsLocaleHome(PAGE, "en", OTHER)).toBe(false);
|
|
35
|
+
expect(pageIsLocaleHome(PAGE, "about", null)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("computeContentHash", () => {
|
|
40
|
+
it("is stable across runs for the same input", async () => {
|
|
41
|
+
const a = await computeContentHash({ x: 1, y: [1, 2] });
|
|
42
|
+
const b = await computeContentHash({ x: 1, y: [1, 2] });
|
|
43
|
+
expect(a).toBe(b);
|
|
44
|
+
expect(a).toMatch(/^[0-9a-f]{64}$/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("is order-independent for object keys", async () => {
|
|
48
|
+
const a = await computeContentHash({ x: 1, y: 2 });
|
|
49
|
+
const b = await computeContentHash({ y: 2, x: 1 });
|
|
50
|
+
expect(a).toBe(b);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("is order-dependent for arrays", async () => {
|
|
54
|
+
const a = await computeContentHash({ x: [1, 2] });
|
|
55
|
+
const b = await computeContentHash({ x: [2, 1] });
|
|
56
|
+
expect(a).not.toBe(b);
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Slug/URL primitives shared between the static generator, the preview
|
|
5
|
+
* op, and the admin UI. Epic #380 Phase A (#383): locale URL shaping
|
|
6
|
+
* (resolveLocaleUrl, buildHreflangLinks) is deleted — URL shape beyond
|
|
7
|
+
* base + slug becomes a plugin contribution on the URL composition
|
|
8
|
+
* point (#390). What survives until the page-identity cut (#384):
|
|
9
|
+
*
|
|
10
|
+
* isHomeSlug / pageIsLocaleHome — the home-decision predicate.
|
|
11
|
+
* computeContentHash — drives `pages.content_hash` (removed in #384).
|
|
12
|
+
* trimSlashes / trimTrailingSlashes — linear-scan slug hygiene.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Strip leading and trailing `/` characters with a single linear scan.
|
|
19
|
+
*
|
|
20
|
+
* Replaces the regex `/^\/+|\/+$/g`, whose trailing-slash branch
|
|
21
|
+
* backtracks O(n²) on a long run of slashes that is not at end-of-string
|
|
22
|
+
* (CodeQL js/polynomial-redos). This char-index walk is unconditionally
|
|
23
|
+
* linear and produces byte-identical output for every input.
|
|
24
|
+
*/
|
|
25
|
+
export function trimSlashes(s: string): string {
|
|
26
|
+
let start = 0;
|
|
27
|
+
let end = s.length;
|
|
28
|
+
while (start < end && s.charCodeAt(start) === 47 /* '/' */) start += 1;
|
|
29
|
+
while (end > start && s.charCodeAt(end - 1) === 47 /* '/' */) end -= 1;
|
|
30
|
+
return s.slice(start, end);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Strip only trailing `/` characters with a single linear scan — the
|
|
35
|
+
* non-backtracking replacement for `/\/+$/` (same ReDoS class as above).
|
|
36
|
+
* Used for base URLs, which carry a scheme and must keep their leading
|
|
37
|
+
* characters.
|
|
38
|
+
*/
|
|
39
|
+
export function trimTrailingSlashes(s: string): string {
|
|
40
|
+
let end = s.length;
|
|
41
|
+
while (end > 0 && s.charCodeAt(end - 1) === 47 /* '/' */) end -= 1;
|
|
42
|
+
return s.slice(0, end);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The "magic slug" sentinel test: a page whose slug is empty, `home`,
|
|
47
|
+
* or `index` is the locale root by convention even without an explicit
|
|
48
|
+
* `locales.home_page_id` designation. Leading/trailing slashes are
|
|
49
|
+
* ignored so `/home/` reads the same as `home`.
|
|
50
|
+
*/
|
|
51
|
+
export function isHomeSlug(slug: string): boolean {
|
|
52
|
+
const s = trimSlashes(slug);
|
|
53
|
+
return s === "" || s === "home" || s === "index";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The single home-decision predicate every URL/output-path site must
|
|
58
|
+
* use so canonical, hreflang, redirects, and the emitted file agree
|
|
59
|
+
* (0184 explicit-homepage feature). A page is the locale root when it
|
|
60
|
+
* IS the locale's designated `home_page_id` OR carries a magic slug.
|
|
61
|
+
*
|
|
62
|
+
* @param pageId The page being resolved.
|
|
63
|
+
* @param slug That page's slug.
|
|
64
|
+
* @param localeHomePageId `locales.home_page_id` for the page's locale
|
|
65
|
+
* (null/undefined when no page is designated).
|
|
66
|
+
*/
|
|
67
|
+
export function pageIsLocaleHome(
|
|
68
|
+
pageId: string,
|
|
69
|
+
slug: string,
|
|
70
|
+
localeHomePageId: string | null | undefined,
|
|
71
|
+
): boolean {
|
|
72
|
+
return (localeHomePageId != null && pageId === localeHomePageId) || isHomeSlug(slug);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Canonical-JSON serializer + sha256 → hex. Stable across runs because
|
|
77
|
+
* keys are sorted. The output is used for `pages.content_hash` so that
|
|
78
|
+
* a Mode-2 translation can detect whether its source has changed.
|
|
79
|
+
*/
|
|
80
|
+
export async function computeContentHash(value: unknown): Promise<string> {
|
|
81
|
+
const canonical = JSON.stringify(value, (_key, val) => {
|
|
82
|
+
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
83
|
+
const sorted: Record<string, unknown> = {};
|
|
84
|
+
for (const k of Object.keys(val).sort()) sorted[k] = (val as Record<string, unknown>)[k];
|
|
85
|
+
return sorted;
|
|
86
|
+
}
|
|
87
|
+
return val;
|
|
88
|
+
});
|
|
89
|
+
const digest = await crypto.subtle.digest("SHA-256", TEXT_ENCODER.encode(canonical));
|
|
90
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
91
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { PROJECT_NAME } from "./index.js";
|
|
5
|
+
|
|
6
|
+
describe("shared scaffold", () => {
|
|
7
|
+
it("exports the project name", () => {
|
|
8
|
+
expect(PROJECT_NAME).toBe("caelo-cms");
|
|
9
|
+
});
|
|
10
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
export const PROJECT_NAME = "caelo-cms";
|
|
4
|
+
|
|
5
|
+
export * from "./ai-tools.js";
|
|
6
|
+
export * from "./auth-forms.js";
|
|
7
|
+
export * from "./base-css.js";
|
|
8
|
+
export * from "./build-page.js";
|
|
9
|
+
export * from "./cap-failures.js";
|
|
10
|
+
export * from "./content.js";
|
|
11
|
+
export * from "./context.js";
|
|
12
|
+
export * from "./css-gradient-scan.js";
|
|
13
|
+
export * from "./css-var-scan.js";
|
|
14
|
+
export * from "./design-draft-shell.js";
|
|
15
|
+
export * from "./design-manifest.js";
|
|
16
|
+
export * from "./fonts.js";
|
|
17
|
+
export * from "./genesis.js";
|
|
18
|
+
export * from "./genesis-inventory.js";
|
|
19
|
+
export * from "./i18n.js";
|
|
20
|
+
export * from "./interactions.js";
|
|
21
|
+
export {
|
|
22
|
+
type LogContext,
|
|
23
|
+
type Logger,
|
|
24
|
+
type LoggerOptions,
|
|
25
|
+
type LogLevel,
|
|
26
|
+
makeLogger,
|
|
27
|
+
mintRequestId,
|
|
28
|
+
redact,
|
|
29
|
+
type ServiceName,
|
|
30
|
+
type StructuredLogEntry,
|
|
31
|
+
} from "./logger.js";
|
|
32
|
+
export * from "./media.js";
|
|
33
|
+
export * from "./page-log.js";
|
|
34
|
+
export * from "./preview-compose.js";
|
|
35
|
+
export * from "./preview-scanner.js";
|
|
36
|
+
export * from "./proposal-status.js";
|
|
37
|
+
export * from "./responsive-images.js";
|
|
38
|
+
export * from "./result.js";
|
|
39
|
+
export * from "./safe-keys.js";
|
|
40
|
+
export * from "./seo.js";
|
|
41
|
+
export * from "./skills.js";
|
|
42
|
+
export * from "./snapshots.js";
|
|
43
|
+
export * from "./strip-cdata.js";
|
|
44
|
+
export * from "./structured-sets.js";
|
|
45
|
+
export * from "./subagents.js";
|
|
46
|
+
export * from "./template-engine.js";
|
|
47
|
+
export * from "./theme-importers/auto-detect.js";
|
|
48
|
+
export * from "./theme-importers/dtcg.js";
|
|
49
|
+
export * from "./theme-importers/loose.js";
|
|
50
|
+
export * from "./theme-importers/shadcn.js";
|
|
51
|
+
export * from "./theme-importers/style-dictionary.js";
|
|
52
|
+
export * from "./theme-importers/tailwind.js";
|
|
53
|
+
export * from "./theme-literal-binding.js";
|
|
54
|
+
export * from "./theme-normalize.js";
|
|
55
|
+
export * from "./theme-ramp.js";
|
|
56
|
+
export * from "./theme-render.js";
|
|
57
|
+
export * from "./themes.js";
|
|
58
|
+
export * from "./themes-errors.js";
|
|
59
|
+
export { CAELO_VERSION, CALEO_VERSION, type CaeloVersion, parseVersion } from "./version.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* issue #160 — the vetted interaction baseline (epic #149).
|
|
5
|
+
*
|
|
6
|
+
* A header without a working mobile menu reads as broken, not minimal
|
|
7
|
+
* — and hand-rolled per-module JS gets focus/aria handling wrong more
|
|
8
|
+
* often than right. This module carries the FUNCTIONAL (non-aesthetic)
|
|
9
|
+
* CSS + JS for the built-in nav renderer: hamburger toggle under the
|
|
10
|
+
* mobile breakpoint, `aria-expanded` bookkeeping, escape-to-close.
|
|
11
|
+
* All colors/spacing stay the module/theme's business — these rules
|
|
12
|
+
* only govern show/hide behaviour, so they don't violate the
|
|
13
|
+
* no-global-look re-scope (#151).
|
|
14
|
+
*
|
|
15
|
+
* The composer includes each asset ONCE per page when at least one
|
|
16
|
+
* nav-menu module rendered (natural dedup, same rule as #158).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Functional-only rules: collapse/expand mechanics, zero aesthetics. */
|
|
20
|
+
export const NAV_FUNCTIONAL_CSS =
|
|
21
|
+
".caelo-nav-menu .caelo-nav-toggle{display:none;background:none;border:0;cursor:pointer;padding:0.5rem;color:inherit;font:inherit}" +
|
|
22
|
+
".caelo-nav-menu .caelo-nav-toggle .caelo-nav-bar{display:block;width:1.25em;height:2px;background:currentColor;margin:0.25em 0}" +
|
|
23
|
+
"@media (max-width: 720px){" +
|
|
24
|
+
".caelo-nav-menu .caelo-nav-toggle{display:inline-block}" +
|
|
25
|
+
".caelo-nav-menu > ul{display:none}" +
|
|
26
|
+
'.caelo-nav-menu[data-nav-open="true"] > ul{display:block}' +
|
|
27
|
+
"}";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Toggle behaviour. Runs deferred; idempotent per element via the
|
|
31
|
+
* data attribute; closes on Escape and returns focus to the toggle.
|
|
32
|
+
*/
|
|
33
|
+
export const NAV_TOGGLE_JS =
|
|
34
|
+
"(function(){" +
|
|
35
|
+
"document.querySelectorAll('.caelo-nav-menu').forEach(function(nav){" +
|
|
36
|
+
"var btn=nav.querySelector('.caelo-nav-toggle');if(!btn)return;" +
|
|
37
|
+
"btn.addEventListener('click',function(){" +
|
|
38
|
+
"var open=nav.getAttribute('data-nav-open')==='true';" +
|
|
39
|
+
"nav.setAttribute('data-nav-open',String(!open));" +
|
|
40
|
+
"btn.setAttribute('aria-expanded',String(!open));" +
|
|
41
|
+
"});" +
|
|
42
|
+
"nav.addEventListener('keydown',function(e){" +
|
|
43
|
+
"if(e.key==='Escape'&&nav.getAttribute('data-nav-open')==='true'){" +
|
|
44
|
+
"nav.setAttribute('data-nav-open','false');" +
|
|
45
|
+
"btn.setAttribute('aria-expanded','false');btn.focus();" +
|
|
46
|
+
"}});" +
|
|
47
|
+
"});" +
|
|
48
|
+
"})();";
|