@caelo-cms/shared 0.10.22 → 0.10.23
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 +290 -214
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +341 -264
- 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 +24 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +28 -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-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 +62 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +78 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +44 -1
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +72 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -0
- 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 +79 -0
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +155 -25
- 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 +8 -0
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +3 -1
- 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 -16
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +5 -16
- 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 +1179 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +30 -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-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 +87 -0
- package/src/i18n.test.ts +274 -0
- package/src/i18n.ts +269 -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 +602 -0
- package/src/preview-compose.ts +709 -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 +234 -0
- package/src/seo.ts +261 -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 +180 -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/translation.test.ts +160 -0
- package/src/translation.ts +295 -0
- package/src/version.ts +66 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue #264 — the per-page edit LOG: durable, append-only work history so a
|
|
3
|
+
* later chat or a fresh subagent that touches a page knows WHY it was edited,
|
|
4
|
+
* what decisions were taken, and which operator answers shaped it — without
|
|
5
|
+
* dragging the originating chat's full transcript through its context.
|
|
6
|
+
*
|
|
7
|
+
* This is FACT (what happened), not learned BEHAVIOUR — so, unlike
|
|
8
|
+
* `site_ai_memory`, it is ungated: any actor appends directly. The shared
|
|
9
|
+
* Zod schemas here are the single source of truth for the `page_log.append`
|
|
10
|
+
* op input, the `log_page_edit` tool schema, and the `page_log.list` output.
|
|
11
|
+
* `formatPageLogBlock` renders the `## Page log` context block the AI reads
|
|
12
|
+
* before touching a page.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
/**
|
|
16
|
+
* The kinds of log entry, in the order the AI should reach for them:
|
|
17
|
+
* - `edited` — a substantive content/module change was made.
|
|
18
|
+
* - `decision` — a design/structure call the AI made and its rationale.
|
|
19
|
+
* - `operator_answer` — an answer the operator gave that shaped the page.
|
|
20
|
+
* - `open_question` — something still unresolved a future turn must settle.
|
|
21
|
+
* - `rebuilt` — the page was rebuilt from scratch (e.g. migration rebuild).
|
|
22
|
+
* - `note` — anything else worth preserving for a future editor.
|
|
23
|
+
*/
|
|
24
|
+
export declare const pageLogEntryKinds: readonly ["edited", "decision", "operator_answer", "open_question", "rebuilt", "note"];
|
|
25
|
+
export declare const pageLogEntryKindSchema: z.ZodEnum<{
|
|
26
|
+
decision: "decision";
|
|
27
|
+
edited: "edited";
|
|
28
|
+
operator_answer: "operator_answer";
|
|
29
|
+
open_question: "open_question";
|
|
30
|
+
rebuilt: "rebuilt";
|
|
31
|
+
note: "note";
|
|
32
|
+
}>;
|
|
33
|
+
export type PageLogEntryKind = (typeof pageLogEntryKinds)[number];
|
|
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 declare const pageLogDetailSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
42
|
+
export type PageLogDetail = z.infer<typeof pageLogDetailSchema>;
|
|
43
|
+
/** Input shape shared by `page_log.append` and the `log_page_edit` tool. */
|
|
44
|
+
export declare const pageLogAppendInputSchema: z.ZodObject<{
|
|
45
|
+
pageId: z.ZodString;
|
|
46
|
+
entryKind: z.ZodEnum<{
|
|
47
|
+
decision: "decision";
|
|
48
|
+
edited: "edited";
|
|
49
|
+
operator_answer: "operator_answer";
|
|
50
|
+
open_question: "open_question";
|
|
51
|
+
rebuilt: "rebuilt";
|
|
52
|
+
note: "note";
|
|
53
|
+
}>;
|
|
54
|
+
summary: z.ZodString;
|
|
55
|
+
detail: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
56
|
+
}, z.core.$strict>;
|
|
57
|
+
export type PageLogAppendInput = z.infer<typeof pageLogAppendInputSchema>;
|
|
58
|
+
/** One row as returned by `page_log.list`. */
|
|
59
|
+
export declare const pageLogEntrySchema: z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
pageId: z.ZodString;
|
|
62
|
+
chatSessionId: z.ZodNullable<z.ZodString>;
|
|
63
|
+
actorKind: z.ZodEnum<{
|
|
64
|
+
system: "system";
|
|
65
|
+
human: "human";
|
|
66
|
+
ai: "ai";
|
|
67
|
+
plugin: "plugin";
|
|
68
|
+
}>;
|
|
69
|
+
entryKind: z.ZodEnum<{
|
|
70
|
+
decision: "decision";
|
|
71
|
+
edited: "edited";
|
|
72
|
+
operator_answer: "operator_answer";
|
|
73
|
+
open_question: "open_question";
|
|
74
|
+
rebuilt: "rebuilt";
|
|
75
|
+
note: "note";
|
|
76
|
+
}>;
|
|
77
|
+
summary: z.ZodString;
|
|
78
|
+
detail: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
79
|
+
createdAt: z.ZodString;
|
|
80
|
+
}, z.core.$strict>;
|
|
81
|
+
export type PageLogEntry = z.infer<typeof pageLogEntrySchema>;
|
|
82
|
+
/**
|
|
83
|
+
* Render the `## Page log` context block for a single page's recent entries
|
|
84
|
+
* (newest first). Returns `null` when there is nothing to show, so the
|
|
85
|
+
* caller omits the header entirely (CLAUDE.md §11 — render only when
|
|
86
|
+
* non-empty). Kept well under 2 KB: entries are capped and each summary is
|
|
87
|
+
* clamped, because the block rides in the system prompt on every turn that
|
|
88
|
+
* touches the page.
|
|
89
|
+
*
|
|
90
|
+
* @param entries page log rows, expected newest-first (as `page_log.list`
|
|
91
|
+
* returns them). The renderer does not re-sort.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatPageLogBlock(entries: readonly PageLogEntry[]): string | null;
|
|
94
|
+
//# sourceMappingURL=page-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-log.d.ts","sourceRoot":"","sources":["../src/page-log.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,wFAOpB,CAAC;AAEX,eAAO,MAAM,sBAAsB;;;;;;;EAA4B,CAAC;AAChE,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,wCAAoC,CAAC;AACrE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB;;;;;;;;;;;;kBAO1B,CAAC;AACZ,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;kBAepB,CAAC;AACZ,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAS9D;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,GAAG,IAAI,CAyBlF"}
|
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;
|
|
@@ -30,12 +66,55 @@ export interface ComposeLanguageSelector {
|
|
|
30
66
|
isCurrent: boolean;
|
|
31
67
|
}>;
|
|
32
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* v0.11.0 — Theme context resolved by the preview op + static generator
|
|
71
|
+
* (#45 Phase 3). Carries the active theme's DTCG tokens jsonb plus the
|
|
72
|
+
* four asset URL resolutions (logo / logo-dark / favicon / social-share).
|
|
73
|
+
*
|
|
74
|
+
* The composer reads `tokens` to emit `<style data-source="theme">`;
|
|
75
|
+
* `assets` are surfaced for modules that want to reference theme-bound
|
|
76
|
+
* images (the dedicated `{{theme_logo_url}}` template binding lands in
|
|
77
|
+
* v0.11.x — see #45's "Out of scope"; v0.11.0 only surfaces the URLs).
|
|
78
|
+
*/
|
|
79
|
+
export interface ComposeThemeAsset {
|
|
80
|
+
readonly mediaId: string;
|
|
81
|
+
readonly url: string;
|
|
82
|
+
}
|
|
83
|
+
export interface ComposeTheme {
|
|
84
|
+
readonly tokens: ThemeDocument;
|
|
85
|
+
readonly assets: {
|
|
86
|
+
readonly logo: ComposeThemeAsset | null;
|
|
87
|
+
readonly logoDark: ComposeThemeAsset | null;
|
|
88
|
+
readonly favicon: ComposeThemeAsset | null;
|
|
89
|
+
readonly socialShare: ComposeThemeAsset | null;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* issue #150 — self-hosted web fonts resolved by the caller (font
|
|
94
|
+
* resolver in admin-core; static generator + preview op thread the
|
|
95
|
+
* same shape so both surfaces load identical fonts). `css` is the
|
|
96
|
+
* @font-face block; `preloads` are woff2 URLs worth a
|
|
97
|
+
* `<link rel="preload">` (body/heading faces, capped upstream).
|
|
98
|
+
*/
|
|
99
|
+
export interface ComposeFonts {
|
|
100
|
+
readonly css: string;
|
|
101
|
+
readonly preloads: readonly string[];
|
|
102
|
+
}
|
|
33
103
|
export interface ComposeInput {
|
|
34
104
|
readonly templateHtml: string;
|
|
35
105
|
readonly templateCss: string;
|
|
36
106
|
readonly blocks: readonly ComposeBlock[];
|
|
37
107
|
readonly structuredSets?: ComposeStructuredSets;
|
|
38
108
|
readonly languageSelector?: ComposeLanguageSelector;
|
|
109
|
+
/**
|
|
110
|
+
* v0.11.0 — active theme threaded through from the preview op + static
|
|
111
|
+
* generator. Undefined when no theme row exists OR when the caller
|
|
112
|
+
* hasn't been migrated to the new primitive yet (renderer no-ops in
|
|
113
|
+
* both cases, preserving legacy parity).
|
|
114
|
+
*/
|
|
115
|
+
readonly theme?: ComposeTheme;
|
|
116
|
+
/** issue #150 — resolved web fonts; undefined = system stacks only. */
|
|
117
|
+
readonly fonts?: ComposeFonts;
|
|
39
118
|
}
|
|
40
119
|
export interface ComposeOutput {
|
|
41
120
|
readonly html: string;
|
|
@@ -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":"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"}
|
package/dist/preview-compose.js
CHANGED
|
@@ -23,8 +23,13 @@
|
|
|
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";
|
|
29
|
+
import { stripCdataGuards } from "./strip-cdata.js";
|
|
27
30
|
import { renderLanguageSelector } from "./structured-sets.js";
|
|
31
|
+
import { renderTemplate } from "./template-engine.js";
|
|
32
|
+
import { renderThemeCss as renderThemeCssFromTokens } from "./theme-render.js";
|
|
28
33
|
const HEAD_CLOSE_RE = /<\/head\s*>/i;
|
|
29
34
|
const BODY_CLOSE_RE = /<\/body\s*>/i;
|
|
30
35
|
function injectBefore(source, marker, fragment) {
|
|
@@ -34,10 +39,30 @@ function injectBefore(source, marker, fragment) {
|
|
|
34
39
|
const idx = m.index;
|
|
35
40
|
return source.slice(0, idx) + fragment + source.slice(idx);
|
|
36
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* issue #150 — head fragment for resolved web fonts: preload links (the
|
|
44
|
+
* `crossorigin` attribute is REQUIRED for font preloads even same-origin,
|
|
45
|
+
* per the fetch spec's font-destination CORS rule) + the @font-face
|
|
46
|
+
* block. Empty css with no preloads → null (system-stack-only theme).
|
|
47
|
+
*/
|
|
48
|
+
function fontsHeadFragment(fonts) {
|
|
49
|
+
if (fonts === undefined)
|
|
50
|
+
return null;
|
|
51
|
+
const links = fonts.preloads
|
|
52
|
+
.map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`)
|
|
53
|
+
.join("");
|
|
54
|
+
const style = fonts.css.trim().length > 0 ? `<style data-source="fonts">${fonts.css}</style>` : "";
|
|
55
|
+
const fragment = links + style;
|
|
56
|
+
return fragment.length > 0 ? fragment : null;
|
|
57
|
+
}
|
|
37
58
|
export function composePagePreview(input) {
|
|
38
59
|
const contentByName = new Map();
|
|
39
60
|
const allCss = [];
|
|
40
61
|
const allJs = [];
|
|
62
|
+
// issue #160 — see composePageWithLayout.
|
|
63
|
+
let navRendered = false;
|
|
64
|
+
// issue #158 — same per-module dedup as composePageWithLayout.
|
|
65
|
+
const seenAssetModules = new Set();
|
|
41
66
|
// Template CSS first so module CSS can override it via source-order specificity.
|
|
42
67
|
if (input.templateCss.trim().length > 0)
|
|
43
68
|
allCss.push(input.templateCss);
|
|
@@ -55,34 +80,53 @@ export function composePagePreview(input) {
|
|
|
55
80
|
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
86
|
else if (langSelector !== null) {
|
|
61
87
|
baseHtml = langSelector;
|
|
62
88
|
}
|
|
63
89
|
else {
|
|
64
|
-
baseHtml = m.html;
|
|
90
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
65
91
|
}
|
|
66
92
|
return tagModuleId(baseHtml, m.moduleId);
|
|
67
93
|
});
|
|
68
94
|
const html = renderedModuleHtml.join("\n");
|
|
69
95
|
contentByName.set(block.blockName, html);
|
|
70
96
|
for (const m of block.modules) {
|
|
97
|
+
if (seenAssetModules.has(m.moduleId))
|
|
98
|
+
continue;
|
|
99
|
+
seenAssetModules.add(m.moduleId);
|
|
71
100
|
if (m.css.trim().length > 0)
|
|
72
101
|
allCss.push(m.css);
|
|
73
102
|
if (m.js.trim().length > 0)
|
|
74
103
|
allJs.push(m.js);
|
|
75
104
|
}
|
|
76
105
|
}
|
|
106
|
+
if (navRendered) {
|
|
107
|
+
allCss.push(NAV_FUNCTIONAL_CSS);
|
|
108
|
+
allJs.push(NAV_TOGGLE_JS);
|
|
109
|
+
}
|
|
77
110
|
const replaced = applySlotReplacements(input.templateHtml, { contentByName });
|
|
78
111
|
let html = replaced.html;
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
const
|
|
112
|
+
// issue #150 — @font-face + preloads before everything else so the
|
|
113
|
+
// browser discovers font URLs as early as possible.
|
|
114
|
+
const fontsFragment = fontsHeadFragment(input.fonts);
|
|
115
|
+
if (fontsFragment !== null) {
|
|
116
|
+
html = injectBefore(html, HEAD_CLOSE_RE, fontsFragment);
|
|
117
|
+
}
|
|
118
|
+
// v0.11.0 — theme tokens become CSS custom properties on :root + (when
|
|
119
|
+
// dark variants exist) :root.dark. Goes first so module CSS can
|
|
120
|
+
// `var(--color-primary)` and override. Pre-v0.11 read from
|
|
121
|
+
// structured_sets["theme/site"]; now reads the active themes row
|
|
122
|
+
// threaded through ComposeInput.theme.
|
|
123
|
+
const themeCss = renderThemeCss(input.theme);
|
|
82
124
|
if (themeCss !== null) {
|
|
83
125
|
const styleTag = `<style data-source="theme">${themeCss}</style>`;
|
|
84
126
|
html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
|
|
85
127
|
}
|
|
128
|
+
// issue #151 — invisible technical baseline (see composePageWithLayout).
|
|
129
|
+
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="base">${BASE_TECHNICAL_CSS}</style>`);
|
|
86
130
|
if (allCss.length > 0) {
|
|
87
131
|
const styleTag = `<style data-source="modules">\n${allCss.join("\n")}\n</style>`;
|
|
88
132
|
html = injectBefore(html, HEAD_CLOSE_RE, styleTag);
|
|
@@ -176,7 +220,13 @@ function renderNavMenuHtml(items) {
|
|
|
176
220
|
const o = it;
|
|
177
221
|
return typeof o.label === "string" && typeof o.href === "string";
|
|
178
222
|
});
|
|
179
|
-
|
|
223
|
+
// issue #160 — mobile-ready markup: toggle button (three functional
|
|
224
|
+
// bars via currentColor) + collapsible list. The functional CSS/JS
|
|
225
|
+
// ships once per page from ./interactions.ts when a nav rendered.
|
|
226
|
+
const toggle = '<button type="button" class="caelo-nav-toggle" aria-expanded="false" aria-label="Menu">' +
|
|
227
|
+
'<span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span><span class="caelo-nav-bar"></span>' +
|
|
228
|
+
"</button>";
|
|
229
|
+
return `<nav class="caelo-nav-menu" data-nav-open="false">${toggle}<ul>${safeItems.map(renderNavItem).join("")}</ul></nav>`;
|
|
180
230
|
}
|
|
181
231
|
function renderNavItem(item) {
|
|
182
232
|
const target = item.target === "_blank" ? ' target="_blank" rel="noopener"' : "";
|
|
@@ -186,25 +236,76 @@ function renderNavItem(item) {
|
|
|
186
236
|
return `<li><a href="${escapeAttr(item.href)}"${target}>${escapeText(item.label)}</a>${inner}</li>`;
|
|
187
237
|
}
|
|
188
238
|
/**
|
|
189
|
-
* Render the theme
|
|
190
|
-
*
|
|
191
|
-
* injecting an empty
|
|
239
|
+
* v0.11.0 — Render the active theme's DTCG tokens as `:root { … }` (+
|
|
240
|
+
* optional `:root.dark { … }`) CSS. Returns null when no active theme
|
|
241
|
+
* is threaded through, so the composer skips injecting an empty
|
|
242
|
+
* `<style>` tag. The renderer itself lives in `theme-render.ts`; this
|
|
243
|
+
* wrapper exists so the composer's call site stays readable + the
|
|
244
|
+
* empty-tokens case (active row, `tokens={}`) emits a deterministic
|
|
245
|
+
* empty shell rather than skipping the tag (observable absence in
|
|
246
|
+
* DevTools, per #45's test-strategy "empty-active-theme" assertion).
|
|
192
247
|
*/
|
|
193
|
-
function renderThemeCss(
|
|
194
|
-
if (!
|
|
195
|
-
return null;
|
|
196
|
-
const items = sets.byKindSlug["theme/site"];
|
|
197
|
-
if (!items || items.length === 0)
|
|
198
|
-
return null;
|
|
199
|
-
const tokens = items.filter((it) => {
|
|
200
|
-
if (!it || typeof it !== "object")
|
|
201
|
-
return false;
|
|
202
|
-
const o = it;
|
|
203
|
-
return typeof o.token === "string" && typeof o.value === "string";
|
|
204
|
-
});
|
|
205
|
-
if (tokens.length === 0)
|
|
248
|
+
function renderThemeCss(theme) {
|
|
249
|
+
if (!theme)
|
|
206
250
|
return null;
|
|
207
|
-
|
|
251
|
+
// Empty-tokens case: emit the shell so cascade ordering stays
|
|
252
|
+
// consistent (legacy "no theme/site row" case still returns null
|
|
253
|
+
// above so the tag is skipped entirely).
|
|
254
|
+
if (Object.keys(theme.tokens).length === 0)
|
|
255
|
+
return ":root{}";
|
|
256
|
+
return renderThemeCssFromTokens(theme.tokens);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Substitute `{{name}}` placeholders and `{{#name}}…{{/name}}`
|
|
260
|
+
* sections in module HTML. Thin wrapper around the shared template
|
|
261
|
+
* engine (#71); see `template-engine.ts` for the full substitution
|
|
262
|
+
* grammar and the loud-raw / failure-marker invariants.
|
|
263
|
+
*
|
|
264
|
+
* Compose is the no-DB path: nested-module field kinds (`module`,
|
|
265
|
+
* `module-list`) cannot be resolved here because the partial loader
|
|
266
|
+
* needs a DB walk to fetch nested content_instances. The engine
|
|
267
|
+
* emits a loud HTML comment for those refs so static-gen output is
|
|
268
|
+
* visible-broken (per CLAUDE.md §2) rather than silent-empty. The
|
|
269
|
+
* chat-branched preview path resolves nested refs via
|
|
270
|
+
* `preview-render.ts` BEFORE the substituted HTML reaches the
|
|
271
|
+
* composer, so this branch never trips for that path. Issue #70
|
|
272
|
+
* tracks pre-resolving in static-gen so the loud comment goes away
|
|
273
|
+
* there too.
|
|
274
|
+
*
|
|
275
|
+
* Both `fields` and `contentValues` are optional. Field `kind` is
|
|
276
|
+
* optional for back-compat with callers that haven't been updated;
|
|
277
|
+
* the engine treats absent kinds as primitives (the legacy
|
|
278
|
+
* compose-path behaviour).
|
|
279
|
+
*/
|
|
280
|
+
function applyFieldSubstitution(html, fields, contentValues, theme) {
|
|
281
|
+
// The substitution engine (renderTemplate) already unwraps CDATA
|
|
282
|
+
// guards; cover the no-op early-return path so a chrome module with no
|
|
283
|
+
// fields/values/theme is cleaned too.
|
|
284
|
+
if (!fields && !contentValues && !theme)
|
|
285
|
+
return stripCdataGuards(html);
|
|
286
|
+
const engineFields = (fields ?? []).map((f) => ({
|
|
287
|
+
name: f.name,
|
|
288
|
+
kind: f.kind ?? "text",
|
|
289
|
+
default: f.default,
|
|
290
|
+
}));
|
|
291
|
+
return renderTemplate({
|
|
292
|
+
html,
|
|
293
|
+
fields: engineFields,
|
|
294
|
+
contentValues,
|
|
295
|
+
// v0.11.1 (issue #76) — thread the active theme's asset URLs so
|
|
296
|
+
// module HTML carrying `{{theme_logo_url}}` etc. resolves. Unbound
|
|
297
|
+
// slots emit loud-raw + `theme-asset-unbound:<slot>` markers.
|
|
298
|
+
themeAssets: theme
|
|
299
|
+
? {
|
|
300
|
+
logo: theme.assets.logo?.url ?? null,
|
|
301
|
+
logoDark: theme.assets.logoDark?.url ?? null,
|
|
302
|
+
favicon: theme.assets.favicon?.url ?? null,
|
|
303
|
+
socialShare: theme.assets.socialShare?.url ?? null,
|
|
304
|
+
}
|
|
305
|
+
: undefined,
|
|
306
|
+
// Compose path has no DB; pass no partials so module/module-list
|
|
307
|
+
// refs emit the loud HTML comment per CLAUDE.md §2.
|
|
308
|
+
}).html;
|
|
208
309
|
}
|
|
209
310
|
export function tagModuleId(html, moduleId) {
|
|
210
311
|
if (!html)
|
|
@@ -284,6 +385,14 @@ export function composePageWithLayout(input) {
|
|
|
284
385
|
// brittle and reads as a bug).
|
|
285
386
|
const cssParts = [];
|
|
286
387
|
const jsParts = [];
|
|
388
|
+
// issue #160 — set when any nav-menu module rendered; pulls the
|
|
389
|
+
// functional nav CSS/JS in exactly once per page.
|
|
390
|
+
let navRendered = false;
|
|
391
|
+
// issue #158 — a module placed N times contributes its CSS/JS ONCE
|
|
392
|
+
// (first occurrence wins; source order is otherwise preserved).
|
|
393
|
+
// Duplicate rule blocks made the cascade order-dependent and bloated
|
|
394
|
+
// every page the same module appeared on twice.
|
|
395
|
+
const seenAssetModules = new Set();
|
|
287
396
|
if (input.layoutCss.trim().length > 0)
|
|
288
397
|
cssParts.push(input.layoutCss);
|
|
289
398
|
if (input.templateCss.trim().length > 0)
|
|
@@ -297,18 +406,22 @@ export function composePageWithLayout(input) {
|
|
|
297
406
|
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
298
407
|
let baseHtml;
|
|
299
408
|
if (navMenuItems !== null) {
|
|
409
|
+
navRendered = true;
|
|
300
410
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
301
411
|
}
|
|
302
412
|
else if (langSelector !== null) {
|
|
303
413
|
baseHtml = langSelector;
|
|
304
414
|
}
|
|
305
415
|
else {
|
|
306
|
-
baseHtml = m.html;
|
|
416
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
307
417
|
}
|
|
308
418
|
return tagModuleId(baseHtml, m.moduleId);
|
|
309
419
|
});
|
|
310
420
|
templateContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
|
|
311
421
|
for (const m of block.modules) {
|
|
422
|
+
if (seenAssetModules.has(m.moduleId))
|
|
423
|
+
continue;
|
|
424
|
+
seenAssetModules.add(m.moduleId);
|
|
312
425
|
if (m.css.trim().length > 0)
|
|
313
426
|
cssParts.push(m.css);
|
|
314
427
|
if (m.js.trim().length > 0)
|
|
@@ -333,33 +446,50 @@ export function composePageWithLayout(input) {
|
|
|
333
446
|
const langSelector = lookupLanguageSelector(m.slug, input);
|
|
334
447
|
let baseHtml;
|
|
335
448
|
if (navMenuItems !== null) {
|
|
449
|
+
navRendered = true;
|
|
336
450
|
baseHtml = renderNavMenuHtml(navMenuItems);
|
|
337
451
|
}
|
|
338
452
|
else if (langSelector !== null) {
|
|
339
453
|
baseHtml = langSelector;
|
|
340
454
|
}
|
|
341
455
|
else {
|
|
342
|
-
baseHtml = m.html;
|
|
456
|
+
baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
|
|
343
457
|
}
|
|
344
458
|
return tagModuleId(baseHtml, m.moduleId);
|
|
345
459
|
});
|
|
346
460
|
layoutContentByName.set(block.blockName, renderedModuleHtml.join("\n"));
|
|
347
461
|
for (const m of block.modules) {
|
|
462
|
+
if (seenAssetModules.has(m.moduleId))
|
|
463
|
+
continue;
|
|
464
|
+
seenAssetModules.add(m.moduleId);
|
|
348
465
|
if (m.css.trim().length > 0)
|
|
349
466
|
cssParts.push(m.css);
|
|
350
467
|
if (m.js.trim().length > 0)
|
|
351
468
|
jsParts.push(m.js);
|
|
352
469
|
}
|
|
353
470
|
}
|
|
471
|
+
if (navRendered) {
|
|
472
|
+
cssParts.push(NAV_FUNCTIONAL_CSS);
|
|
473
|
+
jsParts.push(NAV_TOGGLE_JS);
|
|
474
|
+
}
|
|
354
475
|
// 3. Render the layout HTML, substituting all named slots.
|
|
355
476
|
const replaced = applySlotReplacements(input.layoutHtml, {
|
|
356
477
|
contentByName: layoutContentByName,
|
|
357
478
|
});
|
|
358
479
|
let html = replaced.html;
|
|
359
|
-
|
|
480
|
+
// issue #150 — fonts first (URL discovery), then theme vars, then
|
|
481
|
+
// aggregated CSS; source order in <head> mirrors injection order.
|
|
482
|
+
const fontsFragment = fontsHeadFragment(input.fonts);
|
|
483
|
+
if (fontsFragment !== null) {
|
|
484
|
+
html = injectBefore(html, HEAD_CLOSE_RE, fontsFragment);
|
|
485
|
+
}
|
|
486
|
+
const themeCss = renderThemeCss(input.theme);
|
|
360
487
|
if (themeCss !== null) {
|
|
361
488
|
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="theme">${themeCss}</style>`);
|
|
362
489
|
}
|
|
490
|
+
// issue #151 — invisible technical baseline (reset only, zero design
|
|
491
|
+
// opinion); module CSS follows and overrides trivially.
|
|
492
|
+
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="base">${BASE_TECHNICAL_CSS}</style>`);
|
|
363
493
|
if (cssParts.length > 0) {
|
|
364
494
|
html = injectBefore(html, HEAD_CLOSE_RE, `<style data-source="modules">\n${cssParts.join("\n")}\n</style>`);
|
|
365
495
|
}
|