@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
package/dist/page-log.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* issue #264 — the per-page edit LOG: durable, append-only work history so a
|
|
4
|
+
* later chat or a fresh subagent that touches a page knows WHY it was edited,
|
|
5
|
+
* what decisions were taken, and which operator answers shaped it — without
|
|
6
|
+
* dragging the originating chat's full transcript through its context.
|
|
7
|
+
*
|
|
8
|
+
* This is FACT (what happened), not learned BEHAVIOUR — so, unlike
|
|
9
|
+
* `site_ai_memory`, it is ungated: any actor appends directly. The shared
|
|
10
|
+
* Zod schemas here are the single source of truth for the `page_log.append`
|
|
11
|
+
* op input, the `log_page_edit` tool schema, and the `page_log.list` output.
|
|
12
|
+
* `formatPageLogBlock` renders the `## Page log` context block the AI reads
|
|
13
|
+
* before touching a page.
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
/**
|
|
17
|
+
* The kinds of log entry, in the order the AI should reach for them:
|
|
18
|
+
* - `edited` — a substantive content/module change was made.
|
|
19
|
+
* - `decision` — a design/structure call the AI made and its rationale.
|
|
20
|
+
* - `operator_answer` — an answer the operator gave that shaped the page.
|
|
21
|
+
* - `open_question` — something still unresolved a future turn must settle.
|
|
22
|
+
* - `rebuilt` — the page was rebuilt from scratch (e.g. migration rebuild).
|
|
23
|
+
* - `note` — anything else worth preserving for a future editor.
|
|
24
|
+
*/
|
|
25
|
+
export const pageLogEntryKinds = [
|
|
26
|
+
"edited",
|
|
27
|
+
"decision",
|
|
28
|
+
"operator_answer",
|
|
29
|
+
"open_question",
|
|
30
|
+
"rebuilt",
|
|
31
|
+
"note",
|
|
32
|
+
];
|
|
33
|
+
export const pageLogEntryKindSchema = z.enum(pageLogEntryKinds);
|
|
34
|
+
/**
|
|
35
|
+
* Optional structured context beyond the one-line summary — an OBJECT, never
|
|
36
|
+
* a bare scalar or array, so the jsonb column stores queryable keys (chosen
|
|
37
|
+
* option, operator's exact words, affected module ids). Writes go through
|
|
38
|
+
* `jsonbParam()` in the op handler to avoid the double-encode trap (issue
|
|
39
|
+
* #68).
|
|
40
|
+
*/
|
|
41
|
+
export const pageLogDetailSchema = z.record(z.string(), z.unknown());
|
|
42
|
+
/** Input shape shared by `page_log.append` and the `log_page_edit` tool. */
|
|
43
|
+
export const pageLogAppendInputSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
pageId: z.string().uuid(),
|
|
46
|
+
entryKind: pageLogEntryKindSchema,
|
|
47
|
+
summary: z.string().min(1).max(2000),
|
|
48
|
+
detail: pageLogDetailSchema.optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
/** One row as returned by `page_log.list`. */
|
|
52
|
+
export const pageLogEntrySchema = z
|
|
53
|
+
.object({
|
|
54
|
+
id: z.string().uuid(),
|
|
55
|
+
pageId: z.string().uuid(),
|
|
56
|
+
chatSessionId: z.string().uuid().nullable(),
|
|
57
|
+
actorKind: z.enum(["human", "ai", "plugin", "system"]),
|
|
58
|
+
entryKind: pageLogEntryKindSchema,
|
|
59
|
+
summary: z.string(),
|
|
60
|
+
detail: pageLogDetailSchema.nullable(),
|
|
61
|
+
// ISO datetime, not just any string: the value is `created_at` sliced to a
|
|
62
|
+
// date and rendered into the AI context block — a real contract catches
|
|
63
|
+
// drift (a raw pg timestamp, a number) at the op boundary instead of
|
|
64
|
+
// producing a garbled `## Page log` line.
|
|
65
|
+
createdAt: z.string().datetime(),
|
|
66
|
+
})
|
|
67
|
+
.strict();
|
|
68
|
+
const ACTOR_LABEL = {
|
|
69
|
+
human: "operator",
|
|
70
|
+
ai: "AI",
|
|
71
|
+
plugin: "plugin",
|
|
72
|
+
system: "system",
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Render the `## Page log` context block for a single page's recent entries
|
|
76
|
+
* (newest first). Returns `null` when there is nothing to show, so the
|
|
77
|
+
* caller omits the header entirely (CLAUDE.md §11 — render only when
|
|
78
|
+
* non-empty). Kept well under 2 KB: entries are capped and each summary is
|
|
79
|
+
* clamped, because the block rides in the system prompt on every turn that
|
|
80
|
+
* touches the page.
|
|
81
|
+
*
|
|
82
|
+
* @param entries page log rows, expected newest-first (as `page_log.list`
|
|
83
|
+
* returns them). The renderer does not re-sort.
|
|
84
|
+
*/
|
|
85
|
+
export function formatPageLogBlock(entries) {
|
|
86
|
+
if (entries.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
// Capped + clamped to stay comfortably under 2 KB: the block rides in the
|
|
89
|
+
// system prompt on every turn that touches the page.
|
|
90
|
+
const MAX_ENTRIES = 8;
|
|
91
|
+
const MAX_SUMMARY = 160;
|
|
92
|
+
const shown = entries.slice(0, MAX_ENTRIES);
|
|
93
|
+
const lines = [
|
|
94
|
+
"## Page log",
|
|
95
|
+
"",
|
|
96
|
+
"Prior work on THIS page — read it before you change the page so you build on settled decisions instead of re-litigating them. After a meaningful change, append your own entry with `log_page_edit`.",
|
|
97
|
+
"",
|
|
98
|
+
];
|
|
99
|
+
for (const e of shown) {
|
|
100
|
+
const who = ACTOR_LABEL[e.actorKind] ?? e.actorKind;
|
|
101
|
+
const when = e.createdAt.slice(0, 10);
|
|
102
|
+
const summary = e.summary.length > MAX_SUMMARY ? `${e.summary.slice(0, MAX_SUMMARY - 3)}...` : e.summary;
|
|
103
|
+
lines.push(`- [${e.entryKind}] ${summary} (${who}, ${when})`);
|
|
104
|
+
}
|
|
105
|
+
if (entries.length > MAX_ENTRIES) {
|
|
106
|
+
const omitted = entries.length - MAX_ENTRIES;
|
|
107
|
+
lines.push(`- ${omitted} older ${omitted === 1 ? "entry" : "entries"} omitted.`);
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=page-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-log.js","sourceRoot":"","sources":["../src/page-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModuleFieldKind } from "./content.js";
|
|
2
|
+
import type { ThemeDocument } from "./themes.js";
|
|
1
3
|
export interface ComposeModule {
|
|
2
4
|
readonly moduleId: string;
|
|
3
5
|
readonly slug: string;
|
|
@@ -5,6 +7,40 @@ export interface ComposeModule {
|
|
|
5
7
|
readonly html: string;
|
|
6
8
|
readonly css: string;
|
|
7
9
|
readonly js: string;
|
|
10
|
+
/**
|
|
11
|
+
* v0.4.0 module-field schema, extended in #71 to carry `kind` so
|
|
12
|
+
* the shared template engine can dispatch text-list / link-list /
|
|
13
|
+
* module-list / module sections. `kind` is optional for back-compat
|
|
14
|
+
* with callers that haven't been updated; the engine treats absent
|
|
15
|
+
* kinds as primitives (the legacy compose-path behaviour).
|
|
16
|
+
*
|
|
17
|
+
* When present, the composer substitutes `{{name}}` placeholders
|
|
18
|
+
* in `html` with each field's `default` value before slot
|
|
19
|
+
* replacement — without this, modules created via the AI-authored
|
|
20
|
+
* extractor path (which mints `{{spantext}}` / `{{ctahref}}` etc.
|
|
21
|
+
* with declared defaults) ship raw placeholders to the browser,
|
|
22
|
+
* visible to visitors as literal `{{name}}` text.
|
|
23
|
+
*
|
|
24
|
+
* Per-placement overrides (content_instances.values) are applied
|
|
25
|
+
* here via the `contentValues` field below. The chat-branched
|
|
26
|
+
* preview path additionally walks nested-module refs in
|
|
27
|
+
* preview-render before reaching here.
|
|
28
|
+
*/
|
|
29
|
+
readonly fields?: readonly {
|
|
30
|
+
name: string;
|
|
31
|
+
kind?: ModuleFieldKind;
|
|
32
|
+
default?: unknown;
|
|
33
|
+
}[];
|
|
34
|
+
/**
|
|
35
|
+
* PR #61 follow-up — per-placement content values from the bound
|
|
36
|
+
* `content_instances.values` jsonb. When present, the composer
|
|
37
|
+
* substitutes `{{name}}` placeholders with `contentValues[name]`
|
|
38
|
+
* BEFORE falling back to `fields[].default`. Without this, AI-
|
|
39
|
+
* authored modules that declare explicit fields (without a `default`)
|
|
40
|
+
* but rely on per-placement values shipped raw `{{name}}` text to
|
|
41
|
+
* visitors — the bug e2e-livedit's second Stage caught.
|
|
42
|
+
*/
|
|
43
|
+
readonly contentValues?: Readonly<Record<string, unknown>>;
|
|
8
44
|
}
|
|
9
45
|
export interface ComposeBlock {
|
|
10
46
|
readonly blockName: string;
|
|
@@ -17,31 +53,68 @@ export interface ComposeStructuredSets {
|
|
|
17
53
|
readonly byKindSlug: Readonly<Record<string, readonly unknown[]>>;
|
|
18
54
|
}
|
|
19
55
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
56
|
+
* v0.11.0 — Theme context resolved by the preview op + static generator
|
|
57
|
+
* (#45 Phase 3). Carries the active theme's DTCG tokens jsonb plus the
|
|
58
|
+
* four asset URL resolutions (logo / logo-dark / favicon / social-share).
|
|
59
|
+
*
|
|
60
|
+
* The composer reads `tokens` to emit `<style data-source="theme">`;
|
|
61
|
+
* `assets` are surfaced for modules that want to reference theme-bound
|
|
62
|
+
* images (the dedicated `{{theme_logo_url}}` template binding lands in
|
|
63
|
+
* v0.11.x — see #45's "Out of scope"; v0.11.0 only surfaces the URLs).
|
|
64
|
+
*/
|
|
65
|
+
export interface ComposeThemeAsset {
|
|
66
|
+
readonly mediaId: string;
|
|
67
|
+
readonly url: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ComposeTheme {
|
|
70
|
+
readonly tokens: ThemeDocument;
|
|
71
|
+
readonly assets: {
|
|
72
|
+
readonly logo: ComposeThemeAsset | null;
|
|
73
|
+
readonly logoDark: ComposeThemeAsset | null;
|
|
74
|
+
readonly favicon: ComposeThemeAsset | null;
|
|
75
|
+
readonly socialShare: ComposeThemeAsset | null;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* issue #150 — self-hosted web fonts resolved by the caller (font
|
|
80
|
+
* resolver in admin-core; static generator + preview op thread the
|
|
81
|
+
* same shape so both surfaces load identical fonts). `css` is the
|
|
82
|
+
* @font-face block; `preloads` are woff2 URLs worth a
|
|
83
|
+
* `<link rel="preload">` (body/heading faces, capped upstream).
|
|
24
84
|
*/
|
|
25
|
-
export interface
|
|
26
|
-
readonly
|
|
27
|
-
|
|
28
|
-
displayName: string;
|
|
29
|
-
href: string;
|
|
30
|
-
isCurrent: boolean;
|
|
31
|
-
}>;
|
|
85
|
+
export interface ComposeFonts {
|
|
86
|
+
readonly css: string;
|
|
87
|
+
readonly preloads: readonly string[];
|
|
32
88
|
}
|
|
33
89
|
export interface ComposeInput {
|
|
34
90
|
readonly templateHtml: string;
|
|
35
91
|
readonly templateCss: string;
|
|
36
92
|
readonly blocks: readonly ComposeBlock[];
|
|
37
93
|
readonly structuredSets?: ComposeStructuredSets;
|
|
38
|
-
|
|
94
|
+
/**
|
|
95
|
+
* v0.11.0 — active theme threaded through from the preview op + static
|
|
96
|
+
* generator. Undefined when no theme row exists OR when the caller
|
|
97
|
+
* hasn't been migrated to the new primitive yet (renderer no-ops in
|
|
98
|
+
* both cases, preserving legacy parity).
|
|
99
|
+
*/
|
|
100
|
+
readonly theme?: ComposeTheme;
|
|
101
|
+
/** issue #150 — resolved web fonts; undefined = system stacks only. */
|
|
102
|
+
readonly fonts?: ComposeFonts;
|
|
39
103
|
}
|
|
40
104
|
export interface ComposeOutput {
|
|
41
105
|
readonly html: string;
|
|
42
106
|
readonly replacedSlots: readonly string[];
|
|
43
107
|
readonly missingSlots: readonly string[];
|
|
44
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* issue #150 — head fragment for resolved web fonts: preload links (the
|
|
111
|
+
* `crossorigin` attribute is REQUIRED for font preloads even same-origin,
|
|
112
|
+
* per the fetch spec's font-destination CORS rule) + the @font-face
|
|
113
|
+
* block. Empty css with no preloads → null (system-stack-only theme).
|
|
114
|
+
* Exported for the design-draft theme shell (#375) so draft previews
|
|
115
|
+
* load the identical fonts as the page preview.
|
|
116
|
+
*/
|
|
117
|
+
export declare function fontsHeadFragment(fonts: ComposeFonts | undefined): string | null;
|
|
45
118
|
export declare function composePagePreview(input: ComposeInput): ComposeOutput;
|
|
46
119
|
export declare function tagModuleId(html: string, moduleId: string): string;
|
|
47
120
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preview-compose.d.ts","sourceRoot":"","sources":["../src/preview-compose.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"preview-compose.d.ts","sourceRoot":"","sources":["../src/preview-compose.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAUpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,eAAe,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IACzF;;;;;;;;OAQG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5D;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED;yEACyE;AACzE,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,CAAC;CACnE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAC;QACxC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;QAC5C,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAAC;QAC3C,QAAQ,CAAC,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;KAChD,CAAC;CACH;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,CAAC;IACzC,QAAQ,CAAC,cAAc,CAAC,EAAE,qBAAqB,CAAC;IAChD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAYD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAShF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CAuFrE;AA2JD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUlE;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;gBAC5B,IAAI,EAAE,wBAAwB,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAMjF;AAkCD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,sBAAsB,GAAG,aAAa,CAkJlF"}
|
package/dist/preview-compose.js
CHANGED
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
* `<head>` skeleton lives. Sandboxing happens one layer up (preview iframe in
|
|
24
24
|
* the admin; in P11, plugin Web Components inside Shadow DOM).
|
|
25
25
|
*/
|
|
26
|
+
import { BASE_TECHNICAL_CSS } from "./base-css.js";
|
|
27
|
+
import { NAV_FUNCTIONAL_CSS, NAV_TOGGLE_JS } from "./interactions.js";
|
|
26
28
|
import { applySlotReplacements, extractInnerOfTopLevelContentSlot, listSlotNames, } from "./preview-scanner.js";
|
|
27
|
-
import {
|
|
29
|
+
import { stripCdataGuards } from "./strip-cdata.js";
|
|
30
|
+
import { renderTemplate } from "./template-engine.js";
|
|
31
|
+
import { renderThemeCss as renderThemeCssFromTokens } from "./theme-render.js";
|
|
28
32
|
const HEAD_CLOSE_RE = /<\/head\s*>/i;
|
|
29
33
|
const BODY_CLOSE_RE = /<\/body\s*>/i;
|
|
30
34
|
function injectBefore(source, marker, fragment) {
|
|
@@ -34,10 +38,32 @@ function injectBefore(source, marker, fragment) {
|
|
|
34
38
|
const idx = m.index;
|
|
35
39
|
return source.slice(0, idx) + fragment + source.slice(idx);
|
|
36
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* issue #150 — head fragment for resolved web fonts: preload links (the
|
|
43
|
+
* `crossorigin` attribute is REQUIRED for font preloads even same-origin,
|
|
44
|
+
* per the fetch spec's font-destination CORS rule) + the @font-face
|
|
45
|
+
* block. Empty css with no preloads → null (system-stack-only theme).
|
|
46
|
+
* Exported for the design-draft theme shell (#375) so draft previews
|
|
47
|
+
* load the identical fonts as the page preview.
|
|
48
|
+
*/
|
|
49
|
+
export function fontsHeadFragment(fonts) {
|
|
50
|
+
if (fonts === undefined)
|
|
51
|
+
return null;
|
|
52
|
+
const links = fonts.preloads
|
|
53
|
+
.map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`)
|
|
54
|
+
.join("");
|
|
55
|
+
const style = fonts.css.trim().length > 0 ? `<style data-source="fonts">${fonts.css}</style>` : "";
|
|
56
|
+
const fragment = links + style;
|
|
57
|
+
return fragment.length > 0 ? fragment : null;
|
|
58
|
+
}
|
|
37
59
|
export function composePagePreview(input) {
|
|
38
60
|
const contentByName = new Map();
|
|
39
61
|
const allCss = [];
|
|
40
62
|
const allJs = [];
|
|
63
|
+
// issue #160 — see composePageWithLayout.
|
|
64
|
+
let navRendered = false;
|
|
65
|
+
// issue #158 — same per-module dedup as composePageWithLayout.
|
|
66
|
+
const seenAssetModules = new Set();
|
|
41
67
|
// Template CSS first so module CSS can override it via source-order specificity.
|
|
42
68
|
if (input.templateCss.trim().length > 0)
|
|
43
69
|
allCss.push(input.templateCss);
|
|
@@ -52,37 +78,52 @@ export function composePagePreview(input) {
|
|
|
52
78
|
// module HTML.
|
|
53
79
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
54
80
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
55
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
56
81
|
let baseHtml;
|
|
57
82
|
if (navMenuItems !== null) {
|
|
83
|
+
navRendered = true;
|
|
58
84
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
59
85
|
}
|
|
60
|
-
else if (langSelector !== null) {
|
|
61
|
-
baseHtml = langSelector;
|
|
62
|
-
}
|
|
63
86
|
else {
|
|
64
|
-
baseHtml = m.html;
|
|
87
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
65
88
|
}
|
|
66
89
|
return tagModuleId(baseHtml, m.moduleId);
|
|
67
90
|
});
|
|
68
91
|
const html = renderedModuleHtml.join("\n");
|
|
69
92
|
contentByName.set(block.blockName, html);
|
|
70
93
|
for (const m of block.modules) {
|
|
94
|
+
if (seenAssetModules.has(m.moduleId))
|
|
95
|
+
continue;
|
|
96
|
+
seenAssetModules.add(m.moduleId);
|
|
71
97
|
if (m.css.trim().length > 0)
|
|
72
98
|
allCss.push(m.css);
|
|
73
99
|
if (m.js.trim().length > 0)
|
|
74
100
|
allJs.push(m.js);
|
|
75
101
|
}
|
|
76
102
|
}
|
|
103
|
+
if (navRendered) {
|
|
104
|
+
allCss.push(NAV_FUNCTIONAL_CSS);
|
|
105
|
+
allJs.push(NAV_TOGGLE_JS);
|
|
106
|
+
}
|
|
77
107
|
const replaced = applySlotReplacements(input.templateHtml, { contentByName });
|
|
78
108
|
let html = replaced.html;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
const
|
|
109
|
+
// issue #150 — @font-face + preloads before everything else so the
|
|
110
|
+
// browser discovers font URLs as early as possible.
|
|
111
|
+
const fontsFragment = fontsHeadFragment(input.fonts);
|
|
112
|
+
if (fontsFragment !== null) {
|
|
113
|
+
html = injectBefore(html, HEAD_CLOSE_RE, fontsFragment);
|
|
114
|
+
}
|
|
115
|
+
// v0.11.0 — theme tokens become CSS custom properties on :root + (when
|
|
116
|
+
// dark variants exist) :root.dark. Goes first so module CSS can
|
|
117
|
+
// `var(--color-primary)` and override. Pre-v0.11 read from
|
|
118
|
+
// structured_sets["theme/site"]; now reads the active themes row
|
|
119
|
+
// threaded through ComposeInput.theme.
|
|
120
|
+
const themeCss = renderThemeCss(input.theme);
|
|
82
121
|
if (themeCss !== null) {
|
|
83
122
|
const styleTag = `<style data-source="theme">${themeCss}</style>`;
|
|
84
123
|
html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
|
|
85
124
|
}
|
|
125
|
+
// issue #151 — invisible technical baseline (see composePageWithLayout).
|
|
126
|
+
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="base">${BASE_TECHNICAL_CSS}</style>`);
|
|
86
127
|
if (allCss.length > 0) {
|
|
87
128
|
const styleTag = `<style data-source="modules">\n${allCss.join("\n")}\n</style>`;
|
|
88
129
|
html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
|
|
@@ -125,35 +166,6 @@ function lookupNavMenuItems(moduleSlug, sets) {
|
|
|
125
166
|
const items = sets.byKindSlug[`nav-menu/${setSlug}`];
|
|
126
167
|
return items ?? null;
|
|
127
168
|
}
|
|
128
|
-
/**
|
|
129
|
-
* P9 — return rendered language-selector HTML when a module's slug
|
|
130
|
-
* starts with `language-selector-`. The set's items act as overrides
|
|
131
|
-
* (relabel a locale, hide one); the available-locale list comes from
|
|
132
|
-
* the caller via `input.languageSelector`. Returns null when the
|
|
133
|
-
* module is not a language selector.
|
|
134
|
-
*
|
|
135
|
-
* Convention: a module slug `language-selector-header` resolves to
|
|
136
|
-
* structuredSets[`language-selector/header`] for overrides, and the
|
|
137
|
-
* rendered HTML lists every locale that has a published variant of
|
|
138
|
-
* the current page.
|
|
139
|
-
*/
|
|
140
|
-
function lookupLanguageSelector(moduleSlug, input) {
|
|
141
|
-
const prefix = "language-selector-";
|
|
142
|
-
if (!moduleSlug.startsWith(prefix))
|
|
143
|
-
return null;
|
|
144
|
-
if (!input.languageSelector || input.languageSelector.availableLocales.length === 0) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
const setSlug = moduleSlug.slice(prefix.length);
|
|
148
|
-
const overridesUnknown = input.structuredSets?.byKindSlug[`language-selector/${setSlug}`];
|
|
149
|
-
const overrides = Array.isArray(overridesUnknown)
|
|
150
|
-
? overridesUnknown
|
|
151
|
-
: undefined;
|
|
152
|
-
return renderLanguageSelector({
|
|
153
|
-
availableLocales: input.languageSelector.availableLocales,
|
|
154
|
-
overrides,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
169
|
function escapeAttr(s) {
|
|
158
170
|
return s
|
|
159
171
|
.replace(/&/g, "&")
|
|
@@ -176,7 +188,13 @@ function renderNavMenuHtml(items) {
|
|
|
176
188
|
const o = it;
|
|
177
189
|
return typeof o.label === "string" && typeof o.href === "string";
|
|
178
190
|
});
|
|
179
|
-
|
|
191
|
+
// issue #160 — mobile-ready markup: toggle button (three functional
|
|
192
|
+
// bars via currentColor) + collapsible list. The functional CSS/JS
|
|
193
|
+
// ships once per page from ./interactions.ts when a nav rendered.
|
|
194
|
+
const toggle = '<button type="button" class="caelo-nav-toggle" aria-expanded="false" aria-label="Menu">' +
|
|
195
|
+
'<span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span>' +
|
|
196
|
+
"</button>";
|
|
197
|
+
return `<nav class="caelo-nav-menu" data-nav-open="false">${toggle}<ul>${safeItems.map(renderNavItem).join("")}</ul></nav>`;
|
|
180
198
|
}
|
|
181
199
|
function renderNavItem(item) {
|
|
182
200
|
const target = item.target === "_blank" ? ' target="_blank" rel="noopener"' : "";
|
|
@@ -186,25 +204,76 @@ function renderNavItem(item) {
|
|
|
186
204
|
return `<li><a href="${escapeAttr(item.href)}"${target}>${escapeText(item.label)}</a>${inner}</li>`;
|
|
187
205
|
}
|
|
188
206
|
/**
|
|
189
|
-
* Render the theme
|
|
190
|
-
*
|
|
191
|
-
* injecting an empty
|
|
207
|
+
* v0.11.0 — Render the active theme's DTCG tokens as `:root { … }` (+
|
|
208
|
+
* optional `:root.dark { … }`) CSS. Returns null when no active theme
|
|
209
|
+
* is threaded through, so the composer skips injecting an empty
|
|
210
|
+
* `<style>` tag. The renderer itself lives in `theme-render.ts`; this
|
|
211
|
+
* wrapper exists so the composer's call site stays readable + the
|
|
212
|
+
* empty-tokens case (active row, `tokens={}`) emits a deterministic
|
|
213
|
+
* empty shell rather than skipping the tag (observable absence in
|
|
214
|
+
* DevTools, per #45's test-strategy "empty-active-theme" assertion).
|
|
192
215
|
*/
|
|
193
|
-
function renderThemeCss(
|
|
194
|
-
if (!
|
|
216
|
+
function renderThemeCss(theme) {
|
|
217
|
+
if (!theme)
|
|
195
218
|
return null;
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
// Empty-tokens case: emit the shell so cascade ordering stays
|
|
220
|
+
// consistent (legacy "no theme/site row" case still returns null
|
|
221
|
+
// above so the tag is skipped entirely).
|
|
222
|
+
if (Object.keys(theme.tokens).length === 0)
|
|
223
|
+
return ":root{}";
|
|
224
|
+
return renderThemeCssFromTokens(theme.tokens);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Substitute `{{name}}` placeholders and `{{#name}}…{{/name}}`
|
|
228
|
+
* sections in module HTML. Thin wrapper around the shared template
|
|
229
|
+
* engine (#71); see `template-engine.ts` for the full substitution
|
|
230
|
+
* grammar and the loud-raw / failure-marker invariants.
|
|
231
|
+
*
|
|
232
|
+
* Compose is the no-DB path: nested-module field kinds (`module`,
|
|
233
|
+
* `module-list`) cannot be resolved here because the partial loader
|
|
234
|
+
* needs a DB walk to fetch nested content_instances. The engine
|
|
235
|
+
* emits a loud HTML comment for those refs so static-gen output is
|
|
236
|
+
* visible-broken (per CLAUDE.md §2) rather than silent-empty. The
|
|
237
|
+
* chat-branched preview path resolves nested refs via
|
|
238
|
+
* `preview-render.ts` BEFORE the substituted HTML reaches the
|
|
239
|
+
* composer, so this branch never trips for that path. Issue #70
|
|
240
|
+
* tracks pre-resolving in static-gen so the loud comment goes away
|
|
241
|
+
* there too.
|
|
242
|
+
*
|
|
243
|
+
* Both `fields` and `contentValues` are optional. Field `kind` is
|
|
244
|
+
* optional for back-compat with callers that haven't been updated;
|
|
245
|
+
* the engine treats absent kinds as primitives (the legacy
|
|
246
|
+
* compose-path behaviour).
|
|
247
|
+
*/
|
|
248
|
+
function applyFieldSubstitution(html, fields, contentValues, theme) {
|
|
249
|
+
// The substitution engine (renderTemplate) already unwraps CDATA
|
|
250
|
+
// guards; cover the no-op early-return path so a chrome module with no
|
|
251
|
+
// fields/values/theme is cleaned too.
|
|
252
|
+
if (!fields && !contentValues && !theme)
|
|
253
|
+
return stripCdataGuards(html);
|
|
254
|
+
const engineFields = (fields ?? []).map((f) => ({
|
|
255
|
+
name: f.name,
|
|
256
|
+
kind: f.kind ?? "text",
|
|
257
|
+
default: f.default,
|
|
258
|
+
}));
|
|
259
|
+
return renderTemplate({
|
|
260
|
+
html,
|
|
261
|
+
fields: engineFields,
|
|
262
|
+
contentValues,
|
|
263
|
+
// v0.11.1 (issue #76) — thread the active theme's asset URLs so
|
|
264
|
+
// module HTML carrying `{{theme_logo_url}}` etc. resolves. Unbound
|
|
265
|
+
// slots emit loud-raw + `theme-asset-unbound:<slot>` markers.
|
|
266
|
+
themeAssets: theme
|
|
267
|
+
? {
|
|
268
|
+
logo: theme.assets.logo?.url ?? null,
|
|
269
|
+
logoDark: theme.assets.logoDark?.url ?? null,
|
|
270
|
+
favicon: theme.assets.favicon?.url ?? null,
|
|
271
|
+
socialShare: theme.assets.socialShare?.url ?? null,
|
|
272
|
+
}
|
|
273
|
+
: undefined,
|
|
274
|
+
// Compose path has no DB; pass no partials so module/module-list
|
|
275
|
+
// refs emit the loud HTML comment per CLAUDE.md §2.
|
|
276
|
+
}).html;
|
|
208
277
|
}
|
|
209
278
|
export function tagModuleId(html, moduleId) {
|
|
210
279
|
if (!html)
|
|
@@ -284,6 +353,14 @@ export function composePageWithLayout(input) {
|
|
|
284
353
|
// brittle and reads as a bug).
|
|
285
354
|
const cssParts = [];
|
|
286
355
|
const jsParts = [];
|
|
356
|
+
// issue #160 — set when any nav-menu module rendered; pulls the
|
|
357
|
+
// functional nav CSS/JS in exactly once per page.
|
|
358
|
+
let navRendered = false;
|
|
359
|
+
// issue #158 — a module placed N times contributes its CSS/JS ONCE
|
|
360
|
+
// (first occurrence wins; source order is otherwise preserved).
|
|
361
|
+
// Duplicate rule blocks made the cascade order-dependent and bloated
|
|
362
|
+
// every page the same module appeared on twice.
|
|
363
|
+
const seenAssetModules = new Set();
|
|
287
364
|
if (input.layoutCss.trim().length > 0)
|
|
288
365
|
cssParts.push(input.layoutCss);
|
|
289
366
|
if (input.templateCss.trim().length > 0)
|
|
@@ -294,21 +371,21 @@ export function composePageWithLayout(input) {
|
|
|
294
371
|
for (const block of input.blocks) {
|
|
295
372
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
296
373
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
297
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
298
374
|
let baseHtml;
|
|
299
375
|
if (navMenuItems !== null) {
|
|
376
|
+
navRendered = true;
|
|
300
377
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
301
378
|
}
|
|
302
|
-
else if (langSelector !== null) {
|
|
303
|
-
baseHtml = langSelector;
|
|
304
|
-
}
|
|
305
379
|
else {
|
|
306
|
-
baseHtml = m.html;
|
|
380
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
307
381
|
}
|
|
308
382
|
return tagModuleId(baseHtml, m.moduleId);
|
|
309
383
|
});
|
|
310
384
|
templateContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
|
|
311
385
|
for (const m of block.modules) {
|
|
386
|
+
if (seenAssetModules.has(m.moduleId))
|
|
387
|
+
continue;
|
|
388
|
+
seenAssetModules.add(m.moduleId);
|
|
312
389
|
if (m.css.trim().length > 0)
|
|
313
390
|
cssParts.push(m.css);
|
|
314
391
|
if (m.js.trim().length > 0)
|
|
@@ -330,36 +407,49 @@ export function composePageWithLayout(input) {
|
|
|
330
407
|
continue; // reserved for the page body
|
|
331
408
|
const renderedModuleHtml = block.modules.map((m) => {
|
|
332
409
|
const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
|
|
333
|
-
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
334
410
|
let baseHtml;
|
|
335
411
|
if (navMenuItems !== null) {
|
|
412
|
+
navRendered = true;
|
|
336
413
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
337
414
|
}
|
|
338
|
-
else if (langSelector !== null) {
|
|
339
|
-
baseHtml = langSelector;
|
|
340
|
-
}
|
|
341
415
|
else {
|
|
342
|
-
baseHtml = m.html;
|
|
416
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
343
417
|
}
|
|
344
418
|
return tagModuleId(baseHtml, m.moduleId);
|
|
345
419
|
});
|
|
346
420
|
layoutContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
|
|
347
421
|
for (const m of block.modules) {
|
|
422
|
+
if (seenAssetModules.has(m.moduleId))
|
|
423
|
+
continue;
|
|
424
|
+
seenAssetModules.add(m.moduleId);
|
|
348
425
|
if (m.css.trim().length > 0)
|
|
349
426
|
cssParts.push(m.css);
|
|
350
427
|
if (m.js.trim().length > 0)
|
|
351
428
|
jsParts.push(m.js);
|
|
352
429
|
}
|
|
353
430
|
}
|
|
431
|
+
if (navRendered) {
|
|
432
|
+
cssParts.push(NAV_FUNCTIONAL_CSS);
|
|
433
|
+
jsParts.push(NAV_TOGGLE_JS);
|
|
434
|
+
}
|
|
354
435
|
// 3. Render the layout HTML, substituting all named slots.
|
|
355
436
|
const replaced = applySlotReplacements(input.layoutHtml, {
|
|
356
437
|
contentByName: layoutContentByName,
|
|
357
438
|
});
|
|
358
439
|
let html = replaced.html;
|
|
359
|
-
|
|
440
|
+
// issue #150 — fonts first (URL discovery), then theme vars, then
|
|
441
|
+
// aggregated CSS; source order in <head> mirrors injection order.
|
|
442
|
+
const fontsFragment = fontsHeadFragment(input.fonts);
|
|
443
|
+
if (fontsFragment !== null) {
|
|
444
|
+
html = injectBefore(html, HEAD_CLOSE_RE, fontsFragment);
|
|
445
|
+
}
|
|
446
|
+
const themeCss = renderThemeCss(input.theme);
|
|
360
447
|
if (themeCss !== null) {
|
|
361
448
|
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="theme">${themeCss}</style>`);
|
|
362
449
|
}
|
|
450
|
+
// issue #151 — invisible technical baseline (reset only, zero design
|
|
451
|
+
// opinion); module CSS follows and overrides trivially.
|
|
452
|
+
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="base">${BASE_TECHNICAL_CSS}</style>`);
|
|
363
453
|
if (cssParts.length > 0) {
|
|
364
454
|
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="modules">\n${cssParts.join("\n")}\n</style>`);
|
|
365
455
|
}
|