@cosmicdrift/kumiko-bundled-features 0.122.4 → 0.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.122.4",
3
+ "version": "0.123.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -96,11 +96,11 @@
96
96
  "./step-dispatcher": "./src/step-dispatcher/index.ts"
97
97
  },
98
98
  "dependencies": {
99
- "@cosmicdrift/kumiko-dispatcher-live": "0.122.4",
100
- "@cosmicdrift/kumiko-framework": "0.122.4",
101
- "@cosmicdrift/kumiko-headless": "0.122.4",
102
- "@cosmicdrift/kumiko-renderer": "0.122.4",
103
- "@cosmicdrift/kumiko-renderer-web": "0.122.4",
99
+ "@cosmicdrift/kumiko-dispatcher-live": "0.123.0",
100
+ "@cosmicdrift/kumiko-framework": "0.123.0",
101
+ "@cosmicdrift/kumiko-headless": "0.123.0",
102
+ "@cosmicdrift/kumiko-renderer": "0.123.0",
103
+ "@cosmicdrift/kumiko-renderer-web": "0.123.0",
104
104
  "@mollie/api-client": "^4.5.0",
105
105
  "@node-rs/argon2": "^2.0.2",
106
106
  "@types/nodemailer": "^8.0.0",
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { validateBoot } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { createConfigFeature } from "../../config/feature";
4
+ import { createManagedPagesFeature } from "../feature";
5
+
6
+ // Completeness-Gate: managed-pages ruft jetzt r.translations → der Boot-Validator
7
+ // erzwingt den KOMPLETTEN Required-Surface-Key-Satz (screen:*.title,
8
+ // entity:*:field:*, Section-Header, rowAction-Labels/Confirms) in de+en. Fehlt
9
+ // EIN Key, wirft validateBoot. Beide allowCustomCss-Varianten müssen booten —
10
+ // bei true kommen die zwei customCss-Keys als required dazu.
11
+
12
+ const resolveApexTenant = () => null;
13
+
14
+ describe("managed-pages i18n surface completeness", () => {
15
+ test("boot-validates with allowCustomCss: false", () => {
16
+ const features = [
17
+ createConfigFeature(),
18
+ createManagedPagesFeature({ resolveApexTenant, allowCustomCss: false }),
19
+ ];
20
+ expect(() => validateBoot(features)).not.toThrow();
21
+ });
22
+
23
+ test("boot-validates with allowCustomCss: true (customCss keys required)", () => {
24
+ const features = [
25
+ createConfigFeature(),
26
+ createManagedPagesFeature({ resolveApexTenant, allowCustomCss: true }),
27
+ ];
28
+ expect(() => validateBoot(features)).not.toThrow();
29
+ });
30
+ });
@@ -19,6 +19,7 @@ import { BRANDING_KEYS, BRANDING_QUERY_QN, CUSTOM_CSS_KEY, coerceBranding } from
19
19
  import { createBrandingQuery } from "./handlers/branding.query";
20
20
  import { bySlugQuery } from "./handlers/by-slug.query";
21
21
  import { setWrite } from "./handlers/set.write";
22
+ import { MANAGED_PAGES_I18N } from "./i18n";
22
23
  import { createBrandingSettingsScreen } from "./screens/branding-screen";
23
24
  import { pageEditScreen, pageListScreen } from "./screens/page-screens";
24
25
  import { pageEntity } from "./table";
@@ -196,6 +197,8 @@ export function createManagedPagesFeature(opts: ManagedPagesOptions): FeatureDef
196
197
  r.screen(pageEditScreen);
197
198
  r.screen(createBrandingSettingsScreen({ allowCustomCss }));
198
199
 
200
+ r.translations({ keys: MANAGED_PAGES_I18N });
201
+
199
202
  r.httpRoute({
200
203
  method: "GET",
201
204
  path: `${basePath}/:slug`,
@@ -0,0 +1,56 @@
1
+ // Übersetzungs-Bundle für die managed-pages Admin-Screens. Ohne diese Keys
2
+ // rendert die UI die Roh-i18n-Keys (screen:*.title, entity:*:field:*, Section-
3
+ // Header). Der Boot-Validator (required-surface-keys) verlangt — sobald ein
4
+ // Feature r.translations ruft — den KOMPLETTEN Required-Surface-Satz in de+en;
5
+ // diese Datei deckt jeden Screen/Feld/Section-Key ab, den managed-pages
6
+ // registriert. Die zwei customCss-Keys werden unconditional geliefert (schaden
7
+ // bei allowCustomCss:false nicht, sind aber Pflicht für Consumer die es aktivieren).
8
+
9
+ type LocalizedString = { readonly de: string; readonly en: string };
10
+
11
+ export const MANAGED_PAGES_I18N: Readonly<Record<string, LocalizedString>> = {
12
+ // Screen-Titel (global/unpräfixiert — screenTitleKey = screen:<id>.title)
13
+ "screen:page-list.title": { de: "Seiten", en: "Pages" },
14
+ "screen:page-edit.title": { de: "Seite bearbeiten", en: "Edit page" },
15
+ "screen:branding-settings.title": { de: "Branding", en: "Branding" },
16
+
17
+ // Page-Entity Feld-Labels (managed-pages:entity:page:field:*)
18
+ "managed-pages:entity:page:field:slug": { de: "Slug", en: "Slug" },
19
+ "managed-pages:entity:page:field:lang": { de: "Sprache", en: "Language" },
20
+ "managed-pages:entity:page:field:title": { de: "Titel", en: "Title" },
21
+ "managed-pages:entity:page:field:published": { de: "Veröffentlicht", en: "Published" },
22
+ "managed-pages:entity:page:field:description": { de: "Beschreibung", en: "Description" },
23
+ "managed-pages:entity:page:field:ogImage": { de: "OG-Bild", en: "OG image" },
24
+ "managed-pages:entity:page:field:body": { de: "Inhalt", en: "Content" },
25
+
26
+ // Config-Edit Feld-Labels (managed-pages:entity:__config-edit__:field:*, camelCase Form-Keys)
27
+ "managed-pages:entity:__config-edit__:field:title": { de: "Website-Titel", en: "Site title" },
28
+ "managed-pages:entity:__config-edit__:field:description": {
29
+ de: "Beschreibung",
30
+ en: "Description",
31
+ },
32
+ "managed-pages:entity:__config-edit__:field:siteUrl": { de: "Website-URL", en: "Site URL" },
33
+ "managed-pages:entity:__config-edit__:field:accentColor": {
34
+ de: "Akzentfarbe",
35
+ en: "Accent color",
36
+ },
37
+ "managed-pages:entity:__config-edit__:field:logoUrl": { de: "Logo-URL", en: "Logo URL" },
38
+ "managed-pages:entity:__config-edit__:field:layoutPreset": { de: "Layout", en: "Layout" },
39
+
40
+ // Section-Header
41
+ "managed-pages:section.meta": { de: "Metadaten", en: "Metadata" },
42
+ "managed-pages:section.body": { de: "Inhalt", en: "Content" },
43
+ "managed-pages:branding.section.identity": { de: "Identität", en: "Identity" },
44
+
45
+ // Row-Actions + Confirm (pageListScreen)
46
+ "managed-pages:actions.edit": { de: "Bearbeiten", en: "Edit" },
47
+ "managed-pages:actions.delete": { de: "Löschen", en: "Delete" },
48
+ "managed-pages:confirms.page-delete": {
49
+ de: "Diese Seite wirklich löschen?",
50
+ en: "Delete this page?",
51
+ },
52
+
53
+ // Nur bei allowCustomCss:true im Screen — unconditional geliefert
54
+ "managed-pages:entity:__config-edit__:field:customCss": { de: "Eigenes CSS", en: "Custom CSS" },
55
+ "managed-pages:branding.section.custom-css": { de: "Eigenes CSS", en: "Custom CSS" },
56
+ };
@@ -33,6 +33,7 @@ const TENANT_CLIP = `${TENANT_SCOPE}{overflow:hidden}`;
33
33
  export function tenantStyleBlock(customCss: string): string {
34
34
  const sanitized = sanitizeTenantCss(customCss, TENANT_SCOPE);
35
35
  if (!sanitized) return "";
36
+ // html-ok: sanitizeTenantCss ist die Escaping-Boundary (Allowlist, strippt `<`).
36
37
  return `\n<style data-tenant-css>${TENANT_CONTAINMENT}\n${TENANT_CLIP}\n${sanitized}</style>`;
37
38
  }
38
39
 
@@ -52,7 +53,7 @@ export function wrapInLayout(opts: {
52
53
  description?: string | null;
53
54
  branding?: BrandingTokens;
54
55
  }): string {
55
- const themeStyle = opts.branding ? brandingStyleBlock(opts.branding) : "";
56
+ const themeStyleHtml = opts.branding ? brandingStyleBlock(opts.branding) : "";
56
57
  const header = opts.branding ? brandingHeaderHtml(opts.branding) : "";
57
58
  // Untrusted per-tenant CSS — scoped, allowlist-sanitized and host-contained
58
59
  // at the render boundary by tenantStyleBlock (same helper a custom wrapLayout
@@ -88,7 +89,7 @@ export function wrapInLayout(opts: {
88
89
  .brand-header a { display: flex; align-items: center; gap: 0.6rem; color: inherit; text-decoration: none; }
89
90
  .brand-logo { height: 2rem; width: auto; }
90
91
  .brand-title { font-weight: 600; font-size: 1.1rem; }
91
- </style>${themeStyle}${tenantStyle}
92
+ </style>${themeStyleHtml}${tenantStyle}
92
93
  </head>
93
94
  <body>
94
95
  ${header}