@caelo-cms/shared 0.10.23 → 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.
Files changed (59) hide show
  1. package/dist/ai-tools.d.ts +0 -60
  2. package/dist/ai-tools.d.ts.map +1 -1
  3. package/dist/ai-tools.js +1 -59
  4. package/dist/ai-tools.js.map +1 -1
  5. package/dist/base-css.d.ts +11 -0
  6. package/dist/base-css.d.ts.map +1 -1
  7. package/dist/base-css.js +13 -1
  8. package/dist/base-css.js.map +1 -1
  9. package/dist/design-draft-shell.d.ts +21 -0
  10. package/dist/design-draft-shell.d.ts.map +1 -0
  11. package/dist/design-draft-shell.js +81 -0
  12. package/dist/design-draft-shell.js.map +1 -0
  13. package/dist/genesis.d.ts +43 -3
  14. package/dist/genesis.d.ts.map +1 -1
  15. package/dist/genesis.js +72 -5
  16. package/dist/genesis.js.map +1 -1
  17. package/dist/i18n.d.ts +0 -50
  18. package/dist/i18n.d.ts.map +1 -1
  19. package/dist/i18n.js +8 -149
  20. package/dist/i18n.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/preview-compose.d.ts +9 -15
  26. package/dist/preview-compose.d.ts.map +1 -1
  27. package/dist/preview-compose.js +3 -43
  28. package/dist/preview-compose.js.map +1 -1
  29. package/dist/seo.d.ts +8 -21
  30. package/dist/seo.d.ts.map +1 -1
  31. package/dist/seo.js +4 -22
  32. package/dist/seo.js.map +1 -1
  33. package/dist/structured-sets.d.ts +0 -36
  34. package/dist/structured-sets.d.ts.map +1 -1
  35. package/dist/structured-sets.js +1 -52
  36. package/dist/structured-sets.js.map +1 -1
  37. package/dist/version.d.ts +2 -2
  38. package/dist/version.js +1 -1
  39. package/package.json +2 -2
  40. package/src/ai-tools.ts +1 -75
  41. package/src/base-css.ts +13 -1
  42. package/src/design-draft-shell.test.ts +85 -0
  43. package/src/design-draft-shell.ts +109 -0
  44. package/src/genesis.ts +76 -5
  45. package/src/i18n.test.ts +1 -217
  46. package/src/i18n.ts +8 -186
  47. package/src/index.ts +1 -1
  48. package/src/preview-compose.test.ts +35 -0
  49. package/src/preview-compose.ts +3 -56
  50. package/src/seo.test.ts +0 -40
  51. package/src/seo.ts +8 -36
  52. package/src/structured-sets.ts +1 -67
  53. package/src/version.ts +1 -1
  54. package/dist/translation.d.ts +0 -127
  55. package/dist/translation.d.ts.map +0 -1
  56. package/dist/translation.js +0 -208
  57. package/dist/translation.js.map +0 -1
  58. package/src/translation.test.ts +0 -160
  59. package/src/translation.ts +0 -295
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./content.js";
11
11
  export * from "./context.js";
12
12
  export * from "./css-gradient-scan.js";
13
13
  export * from "./css-var-scan.js";
14
+ export * from "./design-draft-shell.js";
14
15
  export * from "./design-manifest.js";
15
16
  export * from "./fonts.js";
16
17
  export * from "./genesis.js";
@@ -55,5 +56,4 @@ export * from "./theme-ramp.js";
55
56
  export * from "./theme-render.js";
56
57
  export * from "./themes.js";
57
58
  export * from "./themes-errors.js";
58
- export * from "./translation.js";
59
59
  export { CAELO_VERSION, CALEO_VERSION, type CaeloVersion, parseVersion } from "./version.js";
@@ -107,6 +107,22 @@ describe("composePagePreview", () => {
107
107
  });
108
108
  expect(out.missingSlots).toEqual(["missing"]);
109
109
  });
110
+
111
+ it("applies the theme's --font-body to body via the technical baseline (issue #410)", () => {
112
+ // Regression: --font-body was declared by the theme renderer but no
113
+ // rule consumed it, so fresh sites rendered the UA serif. The base
114
+ // block must apply it (with a declared cascade default for
115
+ // themeless installs).
116
+ const out = composePagePreview({
117
+ templateHtml: `<head></head><body><caelo-slot name="x">_</caelo-slot></body>`,
118
+ templateCss: "",
119
+ blocks: [{ blockName: "x", modules: [{ ...blankModule, html: "<p>x</p>" }] }],
120
+ });
121
+ const base = out.html.indexOf('<style data-source="base">');
122
+ expect(base).toBeGreaterThan(-1);
123
+ const baseBlock = out.html.slice(base, out.html.indexOf("</style>", base));
124
+ expect(baseBlock).toContain("body{font-family:var(--font-body,system-ui,sans-serif)}");
125
+ });
110
126
  });
111
127
 
112
128
  describe("tagModuleId (P6.7 live-edit overlay support)", () => {
@@ -536,6 +552,25 @@ describe("composePageWithLayout", () => {
536
552
  expect(baseBlock).not.toMatch(/#[0-9a-f]{3}|font-size|color:|rem\b/i);
537
553
  });
538
554
 
555
+ it("applies the theme's --font-body to body via the technical baseline (issue #410)", () => {
556
+ // Same regression as the composePagePreview variant: both compose
557
+ // paths must ship a consumer for --font-body or the theme's chosen
558
+ // stack never reaches an element.
559
+ const out = composePageWithLayout({
560
+ templateHtml,
561
+ templateCss: "",
562
+ blocks: [],
563
+ layoutHtml,
564
+ layoutCss: "",
565
+ layoutBlocks: [],
566
+ layoutSlug: "test",
567
+ });
568
+ const base = out.html.indexOf('<style data-source="base">');
569
+ expect(base).toBeGreaterThan(-1);
570
+ const baseBlock = out.html.slice(base, out.html.indexOf("</style>", base));
571
+ expect(baseBlock).toContain("body{font-family:var(--font-body,system-ui,sans-serif)}");
572
+ });
573
+
539
574
  it("emits a twice-placed module's CSS/JS exactly once (issue #158)", () => {
540
575
  const shared = {
541
576
  ...blankModule,
@@ -34,7 +34,6 @@ import {
34
34
  listSlotNames,
35
35
  } from "./preview-scanner.js";
36
36
  import { stripCdataGuards } from "./strip-cdata.js";
37
- import { type LanguageSelectorOverride, renderLanguageSelector } from "./structured-sets.js";
38
37
  import { renderTemplate, type TemplateField } from "./template-engine.js";
39
38
  import { renderThemeCss as renderThemeCssFromTokens } from "./theme-render.js";
40
39
  import type { ThemeDocument } from "./themes.js";
@@ -90,21 +89,6 @@ export interface ComposeStructuredSets {
90
89
  readonly byKindSlug: Readonly<Record<string, readonly unknown[]>>;
91
90
  }
92
91
 
93
- /**
94
- * P9 — language-selector context. The caller (preview op + static
95
- * generator) resolves the current page's per-locale URLs once and
96
- * threads them in. The composer renders the selector when a module
97
- * slug starts with `language-selector-`.
98
- */
99
- export interface ComposeLanguageSelector {
100
- readonly availableLocales: ReadonlyArray<{
101
- code: string;
102
- displayName: string;
103
- href: string;
104
- isCurrent: boolean;
105
- }>;
106
- }
107
-
108
92
  /**
109
93
  * v0.11.0 — Theme context resolved by the preview op + static generator
110
94
  * (#45 Phase 3). Carries the active theme's DTCG tokens jsonb plus the
@@ -147,7 +131,6 @@ export interface ComposeInput {
147
131
  readonly templateCss: string;
148
132
  readonly blocks: readonly ComposeBlock[];
149
133
  readonly structuredSets?: ComposeStructuredSets;
150
- readonly languageSelector?: ComposeLanguageSelector;
151
134
  /**
152
135
  * v0.11.0 — active theme threaded through from the preview op + static
153
136
  * generator. Undefined when no theme row exists OR when the caller
@@ -180,8 +163,10 @@ function injectBefore(source: string, marker: RegExp, fragment: string): string
180
163
  * `crossorigin` attribute is REQUIRED for font preloads even same-origin,
181
164
  * per the fetch spec's font-destination CORS rule) + the @font-face
182
165
  * block. Empty css with no preloads → null (system-stack-only theme).
166
+ * Exported for the design-draft theme shell (#375) so draft previews
167
+ * load the identical fonts as the page preview.
183
168
  */
184
- function fontsHeadFragment(fonts: ComposeFonts | undefined): string | null {
169
+ export function fontsHeadFragment(fonts: ComposeFonts | undefined): string | null {
185
170
  if (fonts === undefined) return null;
186
171
  const links = fonts.preloads
187
172
  .map((href) => `<link rel="preload" as="font" type="font/woff2" crossorigin href="${href}">`)
@@ -214,13 +199,10 @@ export function composePagePreview(input: ComposeInput): ComposeOutput {
214
199
  // module HTML.
215
200
  const renderedModuleHtml = block.modules.map((m) => {
216
201
  const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
217
- const langSelector = lookupLanguageSelector(m.slug, input);
218
202
  let baseHtml: string;
219
203
  if (navMenuItems !== null) {
220
204
  navRendered = true;
221
205
  baseHtml = renderNavMenuHtml(navMenuItems);
222
- } else if (langSelector !== null) {
223
- baseHtml = langSelector;
224
206
  } else {
225
207
  baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
226
208
  }
@@ -314,35 +296,6 @@ function lookupNavMenuItems(
314
296
  return items ?? null;
315
297
  }
316
298
 
317
- /**
318
- * P9 — return rendered language-selector HTML when a module's slug
319
- * starts with `language-selector-`. The set's items act as overrides
320
- * (relabel a locale, hide one); the available-locale list comes from
321
- * the caller via `input.languageSelector`. Returns null when the
322
- * module is not a language selector.
323
- *
324
- * Convention: a module slug `language-selector-header` resolves to
325
- * structuredSets[`language-selector/header`] for overrides, and the
326
- * rendered HTML lists every locale that has a published variant of
327
- * the current page.
328
- */
329
- function lookupLanguageSelector(moduleSlug: string, input: ComposeInput): string | null {
330
- const prefix = "language-selector-";
331
- if (!moduleSlug.startsWith(prefix)) return null;
332
- if (!input.languageSelector || input.languageSelector.availableLocales.length === 0) {
333
- return null;
334
- }
335
- const setSlug = moduleSlug.slice(prefix.length);
336
- const overridesUnknown = input.structuredSets?.byKindSlug[`language-selector/${setSlug}`];
337
- const overrides = Array.isArray(overridesUnknown)
338
- ? (overridesUnknown as LanguageSelectorOverride[])
339
- : undefined;
340
- return renderLanguageSelector({
341
- availableLocales: input.languageSelector.availableLocales,
342
- overrides,
343
- });
344
- }
345
-
346
299
  interface NavMenuItem {
347
300
  label: string;
348
301
  href: string;
@@ -593,13 +546,10 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
593
546
  for (const block of input.blocks) {
594
547
  const renderedModuleHtml = block.modules.map((m) => {
595
548
  const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
596
- const langSelector = lookupLanguageSelector(m.slug, input);
597
549
  let baseHtml: string;
598
550
  if (navMenuItems !== null) {
599
551
  navRendered = true;
600
552
  baseHtml = renderNavMenuHtml(navMenuItems);
601
- } else if (langSelector !== null) {
602
- baseHtml = langSelector;
603
553
  } else {
604
554
  baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
605
555
  }
@@ -628,13 +578,10 @@ export function composePageWithLayout(input: ComposeWithLayoutInput): ComposeOut
628
578
  if (block.blockName === "content") continue; // reserved for the page body
629
579
  const renderedModuleHtml = block.modules.map((m) => {
630
580
  const navMenuItems = lookupNavMenuItems(m.slug, input.structuredSets);
631
- const langSelector = lookupLanguageSelector(m.slug, input);
632
581
  let baseHtml: string;
633
582
  if (navMenuItems !== null) {
634
583
  navRendered = true;
635
584
  baseHtml = renderNavMenuHtml(navMenuItems);
636
- } else if (langSelector !== null) {
637
- baseHtml = langSelector;
638
585
  } else {
639
586
  baseHtml = applyFieldSubstitution(m.html, m.fields, m.contentValues, input.theme);
640
587
  }
package/src/seo.test.ts CHANGED
@@ -17,7 +17,6 @@ describe("resolveCanonicalUrl", () => {
17
17
  resolveCanonicalUrl({
18
18
  siteBaseUrl: "https://example.com",
19
19
  pageSlug: "anything",
20
- pageLocale: "en",
21
20
  override: "https://canonical.example.com/x",
22
21
  }),
23
22
  ).toBe("https://canonical.example.com/x");
@@ -28,7 +27,6 @@ describe("resolveCanonicalUrl", () => {
28
27
  resolveCanonicalUrl({
29
28
  siteBaseUrl: "https://example.com",
30
29
  pageSlug: "home",
31
- pageLocale: "en",
32
30
  override: null,
33
31
  }),
34
32
  ).toBe("https://example.com/");
@@ -39,7 +37,6 @@ describe("resolveCanonicalUrl", () => {
39
37
  resolveCanonicalUrl({
40
38
  siteBaseUrl: "https://example.com/",
41
39
  pageSlug: "about",
42
- pageLocale: "en",
43
40
  override: null,
44
41
  }),
45
42
  ).toBe("https://example.com/about/");
@@ -51,7 +48,6 @@ describe("resolveCanonicalUrl", () => {
51
48
  resolveCanonicalUrl({
52
49
  siteBaseUrl: "https://example.com",
53
50
  pageSlug: "about",
54
- pageLocale: "en",
55
51
  override: null,
56
52
  pageUrlStyle: "no-extension",
57
53
  }),
@@ -63,37 +59,17 @@ describe("resolveCanonicalUrl", () => {
63
59
  resolveCanonicalUrl({
64
60
  siteBaseUrl: "https://example.com",
65
61
  pageSlug: "home",
66
- pageLocale: "en",
67
62
  override: null,
68
63
  pageUrlStyle: "no-extension",
69
64
  }),
70
65
  ).toBe("https://example.com/");
71
66
  });
72
67
 
73
- it("subdirectory locale strategy keeps the locale prefix, no trailing slash on slug", () => {
74
- expect(
75
- resolveCanonicalUrl({
76
- siteBaseUrl: "https://example.com",
77
- pageSlug: "about",
78
- pageLocale: "de",
79
- override: null,
80
- pageUrlStyle: "no-extension",
81
- localeConfig: {
82
- code: "de",
83
- urlStrategy: "subdirectory",
84
- urlHost: null,
85
- isDefault: false,
86
- },
87
- }),
88
- ).toBe("https://example.com/de/about");
89
- });
90
-
91
68
  it("default style preserves pre-v0.2.85 trailing-slash behavior", () => {
92
69
  expect(
93
70
  resolveCanonicalUrl({
94
71
  siteBaseUrl: "https://example.com",
95
72
  pageSlug: "about",
96
- pageLocale: "en",
97
73
  override: null,
98
74
  }),
99
75
  ).toBe("https://example.com/about/");
@@ -108,7 +84,6 @@ describe("renderSeoHead", () => {
108
84
  canonical: "https://example.com/",
109
85
  noindex: false,
110
86
  ogImageUrl: null,
111
- hreflang: [] as { locale: string; url: string }[],
112
87
  organization: {},
113
88
  };
114
89
 
@@ -136,21 +111,6 @@ describe("renderSeoHead", () => {
136
111
  expect(head).toContain('<meta name="twitter:card" content="summary_large_image" />');
137
112
  });
138
113
 
139
- it("emits hreflang per row + x-default when any rows are present", () => {
140
- const head = renderSeoHead({
141
- ...base,
142
- hreflang: [
143
- { locale: "de", url: "https://example.com/de/" },
144
- { locale: "fr", url: "https://example.com/fr/" },
145
- ],
146
- });
147
- expect(head).toContain('<link rel="alternate" hreflang="de" href="https://example.com/de/" />');
148
- expect(head).toContain('<link rel="alternate" hreflang="fr" href="https://example.com/fr/" />');
149
- expect(head).toContain(
150
- '<link rel="alternate" hreflang="x-default" href="https://example.com/" />',
151
- );
152
- });
153
-
154
114
  it("emits a JSON-LD WebPage block; encodes the < character to avoid script-tag breaks", () => {
155
115
  const head = renderSeoHead({
156
116
  ...base,
package/src/seo.ts CHANGED
@@ -108,22 +108,14 @@ export interface SiteSeoSettings {
108
108
 
109
109
  /**
110
110
  * Resolve the canonical URL for a page. If `pages_seo.canonical_url`
111
- * is set it wins; otherwise build `<siteBaseUrl>/<page-path>` from
112
- * the page's slug + locale. Locale strategy is single-locale (no
113
- * prefix) until P9 i18n.
111
+ * is set it wins; otherwise build `<siteBaseUrl>/<slug>` (home pages
112
+ * canonicalize to the bare base URL). URL shaping beyond base + slug
113
+ * becomes a plugin contribution on the URL composition point (#390).
114
114
  */
115
115
  export function resolveCanonicalUrl(args: {
116
116
  siteBaseUrl: string;
117
117
  pageSlug: string;
118
- pageLocale: string;
119
118
  override: string | null;
120
- /** P9 — when present, the locale's url_strategy decides the URL shape. */
121
- localeConfig?: {
122
- code: string;
123
- urlStrategy: "none" | "subdirectory" | "subdomain" | "domain";
124
- urlHost: string | null;
125
- isDefault: boolean;
126
- };
127
119
  /**
128
120
  * v0.2.85 — page emission style. 'directory' (default) → URLs end
129
121
  * in `/<slug>/`; 'no-extension' → URLs end in `/<slug>` (no
@@ -132,11 +124,10 @@ export function resolveCanonicalUrl(args: {
132
124
  */
133
125
  pageUrlStyle?: "directory" | "no-extension";
134
126
  /**
135
- * 0184 — explicit homepage designation (`locales.home_page_id`). When
136
- * true this page IS the locale root and canonicalizes to `<base>/`
137
- * regardless of its slug. Uses the same predicate (`isHomeSlug`) as
138
- * the magic-slug fallback so canonical agrees with hreflang + the
139
- * emitted output path.
127
+ * 0184 — explicit homepage designation. When true this page IS the
128
+ * site root and canonicalizes to `<base>/` regardless of its slug.
129
+ * Uses the same predicate (`isHomeSlug`) as the magic-slug fallback
130
+ * so canonical agrees with the emitted output path.
140
131
  */
141
132
  isHomePage?: boolean;
142
133
  }): string {
@@ -149,16 +140,6 @@ export function resolveCanonicalUrl(args: {
149
140
  // shape dictated by the page-emission style. Home pages always
150
141
  // resolve to the base URL (no tail) regardless of style.
151
142
  const tail = cleanSlug ? (style === "no-extension" ? cleanSlug : `${cleanSlug}/`) : "";
152
- const cfg = args.localeConfig;
153
- if (cfg && cfg.urlStrategy !== "none") {
154
- if (cfg.urlStrategy === "subdirectory") {
155
- return tail ? `${base}/${cfg.code}/${tail}` : `${base}/${cfg.code}/`;
156
- }
157
- if ((cfg.urlStrategy === "subdomain" || cfg.urlStrategy === "domain") && cfg.urlHost) {
158
- const protocol = base.startsWith("http://") ? "http://" : "https://";
159
- return tail ? `${protocol}${cfg.urlHost}/${tail}` : `${protocol}${cfg.urlHost}/`;
160
- }
161
- }
162
143
  return tail ? `${base}/${tail}` : `${base}/`;
163
144
  }
164
145
 
@@ -168,7 +149,6 @@ export interface SeoMetaInput {
168
149
  canonical: string;
169
150
  noindex: boolean;
170
151
  ogImageUrl: string | null;
171
- hreflang: { locale: string; url: string }[];
172
152
  organization: SiteSeoSettings["organization"];
173
153
  }
174
154
 
@@ -177,7 +157,7 @@ export interface SeoMetaInput {
177
157
  * the renderer injects just before </head>. Order is canonical
178
158
  * (W3C-recommended): charset / viewport stay in the layout; meta
179
159
  * description + canonical + robots + Open Graph + Twitter card +
180
- * hreflang + JSON-LD.
160
+ * JSON-LD.
181
161
  *
182
162
  * No raw HTML escape hatch — every variable is HTML-attribute-encoded.
183
163
  */
@@ -208,14 +188,6 @@ export function renderSeoHead(input: SeoMetaInput): string {
208
188
  lines.push(
209
189
  `<meta name="twitter:card" content="${input.ogImageUrl ? "summary_large_image" : "summary"}" />`,
210
190
  );
211
- // Hreflang per locale + x-default. P8 ships zero rows; P9 i18n
212
- // populates pages_hreflang and the existing renderer iterates.
213
- for (const h of input.hreflang) {
214
- lines.push(`<link rel="alternate" hreflang="${enc(h.locale)}" href="${enc(h.url)}" />`);
215
- }
216
- if (input.hreflang.length > 0) {
217
- lines.push(`<link rel="alternate" hreflang="x-default" href="${enc(input.canonical)}" />`);
218
- }
219
191
  // JSON-LD WebPage block referencing the Organization (when set).
220
192
  const ld: Record<string, unknown> = {
221
193
  "@context": "https://schema.org",
@@ -75,38 +75,13 @@ export const linkListItem = z
75
75
  })
76
76
  .strict();
77
77
 
78
- /**
79
- * P9 review-pass — `language-selector` kind. Items are user-supplied
80
- * overrides (e.g. force a specific display label per locale); empty
81
- * array means "auto-populate from the locale registry at render time."
82
- * The renderer (apps/static-generator + preview op) reads the locale
83
- * registry + the current page's slug + resolveLocaleUrl to emit
84
- * `<a hreflang lang href>` rows. Closes CMS_REQUIREMENTS §7.8.
85
- */
86
- export const languageSelectorOverride = z
87
- .object({
88
- /** BCP-47 locale code; must match a row in the locales registry. */
89
- locale: z.string().min(2).max(10),
90
- /** Override the locale's display_name when rendering this entry. */
91
- label: z.string().min(1).max(120).optional(),
92
- /** Hide this locale from the rendered selector even if it has a published page. */
93
- hidden: z.boolean().optional(),
94
- })
95
- .strict();
96
-
97
78
  /** Discriminated kind → items array. The Query API op picks the
98
79
  * validator at runtime by reading `kind` first.
99
80
  *
100
81
  * v0.11.0 (#45) — `theme` is no longer a structured-set kind. The
101
82
  * theme primitive moved to its own `themes` table with DTCG-shaped
102
83
  * jsonb tokens; see `themes.ts` + `theme-render.ts`. */
103
- export const structuredSetKind = z.enum([
104
- "nav-menu",
105
- "taxonomy",
106
- "tags",
107
- "link-list",
108
- "language-selector",
109
- ]);
84
+ export const structuredSetKind = z.enum(["nav-menu", "taxonomy", "tags", "link-list"]);
110
85
  export type StructuredSetKind = z.infer<typeof structuredSetKind>;
111
86
 
112
87
  /** Validate `items` against the right schema for `kind`. Throws a
@@ -130,8 +105,6 @@ export function validateStructuredSetItems(kind: StructuredSetKind, items: unkno
130
105
  return items.map((it) => tagItem.parse(it));
131
106
  case "link-list":
132
107
  return items.map((it) => linkListItem.parse(it));
133
- case "language-selector":
134
- return items.map((it) => languageSelectorOverride.parse(it));
135
108
  }
136
109
  }
137
110
 
@@ -139,42 +112,3 @@ export type NavMenuItem = z.infer<typeof navMenuItem>;
139
112
  export type TaxonomyItem = z.infer<typeof taxonomyItem>;
140
113
  export type TagItem = z.infer<typeof tagItem>;
141
114
  export type LinkListItem = z.infer<typeof linkListItem>;
142
- export type LanguageSelectorOverride = z.infer<typeof languageSelectorOverride>;
143
-
144
- /**
145
- * Render a `<nav class="caelo-language-selector">` with one `<a>`
146
- * per locale that has a published variant of the current page. Pure
147
- * function — caller threads the page's published-locale list and a
148
- * URL resolver. Used by `composePagePreview` (admin) and the static
149
- * generator (deploy) so byte-for-byte output parity holds.
150
- *
151
- * Per CMS_REQUIREMENTS §7.8: locales with no published variant for the
152
- * current page are excluded.
153
- */
154
- export function renderLanguageSelector(args: {
155
- /** Each locale that has a published variant of the current page. */
156
- readonly availableLocales: ReadonlyArray<{
157
- code: string;
158
- displayName: string;
159
- href: string;
160
- isCurrent: boolean;
161
- }>;
162
- /** Owner-supplied overrides (relabel a locale, hide one). */
163
- readonly overrides?: ReadonlyArray<LanguageSelectorOverride>;
164
- }): string {
165
- const overrideByLocale = new Map<string, LanguageSelectorOverride>(
166
- (args.overrides ?? []).map((o) => [o.locale, o]),
167
- );
168
- const enc = (s: string): string =>
169
- s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
170
- const items = args.availableLocales
171
- .filter((l) => !overrideByLocale.get(l.code)?.hidden)
172
- .map((l) => {
173
- const override = overrideByLocale.get(l.code);
174
- const label = override?.label ?? l.displayName;
175
- const aria = l.isCurrent ? ' aria-current="true"' : "";
176
- return `<a hreflang="${enc(l.code)}" lang="${enc(l.code)}" href="${enc(l.href)}"${aria}>${enc(label)}</a>`;
177
- });
178
- if (items.length === 0) return "";
179
- return `<nav class="caelo-language-selector" aria-label="Language">${items.join("")}</nav>`;
180
- }
package/src/version.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  * follow standard SemVer.
23
23
  */
24
24
 
25
- export const CAELO_VERSION = "0.10.23";
25
+ export const CAELO_VERSION = "0.10.24";
26
26
 
27
27
  /**
28
28
  * Deprecated alias for back-compat — early P17 work spelled this
@@ -1,127 +0,0 @@
1
- /**
2
- * P10 — translation primitives shared between the AI prompt builder,
3
- * the Mode 1 / Mode 2 op handlers, and the dashboard.
4
- *
5
- * computeBlockDiff(source, variant) — block-level diff per
6
- * CMS_REQUIREMENTS §7.6: matches modules by (block_name, position),
7
- * returns typed `changed` / `added` / `removed` operations. Pure
8
- * function; the AI receives this as Mode 2 prompt context.
9
- *
10
- * buildModeOnePrompt(...) / buildModeTwoPrompt(...) — assemble the
11
- * §7.6 sample-payload shapes with glossary + style guide injected.
12
- * Returns the prompt + a structured payload for unit-tests to
13
- * assert against.
14
- */
15
- import { z } from "zod";
16
- export interface ModuleBlockSlot {
17
- /** The block (e.g. "content", "header") this module sits in. */
18
- blockName: string;
19
- /** Position within the block. Blocks are ordered + 0-based. */
20
- position: number;
21
- /** Module id — locked across locales (translations share refs). */
22
- moduleId: string;
23
- /** Display name shown to the AI for context. */
24
- moduleSlug: string;
25
- /** The translatable fields of the module. P10 supports HTML body
26
- * and three SEO-style metadata fields. Extend as new typed fields
27
- * land (e.g. structured-content blocks in P12A). */
28
- html: string;
29
- altText: string | null;
30
- caption: string | null;
31
- }
32
- export type BlockDiffOp = {
33
- kind: "changed";
34
- blockName: string;
35
- position: number;
36
- moduleId: string;
37
- before: ModuleBlockSlot;
38
- after: ModuleBlockSlot;
39
- } | {
40
- kind: "added";
41
- blockName: string;
42
- position: number;
43
- module: ModuleBlockSlot;
44
- } | {
45
- kind: "removed";
46
- blockName: string;
47
- position: number;
48
- module: ModuleBlockSlot;
49
- };
50
- /**
51
- * Diff a source page's modules against a variant's modules. Match by
52
- * (blockName, position) — the §7.5 contract is that translations
53
- * share STRUCTURE with the source (block + position alignment) and
54
- * differ only in CONTENT (html / alt / caption per locale). Anything
55
- * mis-aligned is `added` (source has it, variant doesn't) or
56
- * `removed` (variant has it, source doesn't).
57
- *
58
- * `changed` rows contain BOTH before + after so the AI prompt can
59
- * show the structured diff per the §7.6 sample payload.
60
- */
61
- export declare function computeBlockDiff(source: readonly ModuleBlockSlot[], variant: readonly ModuleBlockSlot[]): BlockDiffOp[];
62
- export interface GlossaryEntry {
63
- sourceTerm: string;
64
- translation: string;
65
- context: string | null;
66
- }
67
- export interface ModeOnePromptInput {
68
- /** Source-locale code (e.g. 'en'). */
69
- sourceLocale: string;
70
- /** Target locale code (e.g. 'de-AT'). */
71
- targetLocale: string;
72
- /** Optional locale displayName for context (e.g. "German (Austria)"). */
73
- targetLocaleDisplayName?: string;
74
- /** Source-page modules. The AI translates each per (blockName, position). */
75
- sourceModules: readonly ModuleBlockSlot[];
76
- glossary: readonly GlossaryEntry[];
77
- styleGuide: string | null;
78
- }
79
- export interface ModeTwoPromptInput {
80
- sourceLocale: string;
81
- targetLocale: string;
82
- targetLocaleDisplayName?: string;
83
- sourceModules: readonly ModuleBlockSlot[];
84
- /** The CURRENT variant — passed in full so the AI keeps the prior
85
- * translation quality on unchanged blocks. */
86
- variantModules: readonly ModuleBlockSlot[];
87
- /** Pre-computed diff (call computeBlockDiff first). */
88
- diff: readonly BlockDiffOp[];
89
- glossary: readonly GlossaryEntry[];
90
- styleGuide: string | null;
91
- }
92
- /**
93
- * The AI returns this shape for both Mode 1 and Mode 2: one entry per
94
- * source module slot, with the translated content fields. The handler
95
- * matches entries back to the variant page's module rows by
96
- * (blockName, position).
97
- *
98
- * Mode 1 returns one entry per source module.
99
- * Mode 2 returns entries ONLY for changed blocks (preserves prior
100
- * translation on unchanged ones).
101
- */
102
- export declare const translationResultModule: z.ZodObject<{
103
- blockName: z.ZodString;
104
- position: z.ZodNumber;
105
- html: z.ZodString;
106
- altText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
107
- caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
108
- }, z.core.$strict>;
109
- export declare const translationResultPayload: z.ZodObject<{
110
- modules: z.ZodArray<z.ZodObject<{
111
- blockName: z.ZodString;
112
- position: z.ZodNumber;
113
- html: z.ZodString;
114
- altText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
115
- caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
116
- }, z.core.$strict>>;
117
- }, z.core.$strict>;
118
- export type TranslationResultPayload = z.infer<typeof translationResultPayload>;
119
- export declare function buildModeOnePrompt(input: ModeOnePromptInput): {
120
- system: string;
121
- user: string;
122
- };
123
- export declare function buildModeTwoPrompt(input: ModeTwoPromptInput): {
124
- system: string;
125
- user: string;
126
- };
127
- //# sourceMappingURL=translation.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"translation.d.ts","sourceRoot":"","sources":["../src/translation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;wDAEoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,MAAM,WAAW,GACnB;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;CACxB,GACD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC/E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC;AAEtF;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,OAAO,EAAE,SAAS,eAAe,EAAE,GAClC,WAAW,EAAE,CAgCf;AAMD,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,6EAA6E;IAC7E,aAAa,EAAE,SAAS,eAAe,EAAE,CAAC;IAC1C,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,aAAa,EAAE,SAAS,eAAe,EAAE,CAAC;IAC1C;kDAC8C;IAC9C,cAAc,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,uDAAuD;IACvD,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IAC7B,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IACnC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB;;;;;;kBASzB,CAAC;AAEZ,eAAO,MAAM,wBAAwB;;;;;;;;kBAI1B,CAAC;AACZ,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAiChF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAuBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAuEA"}