@teamscala/service-boot 0.23.417 → 0.23.419

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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Branch 2 of the resolved-graph → pageData conversion: the public shell
7
+ * (header/body/footer regions from header/footer sources).
8
+ *
9
+ * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
10
+ * the closure variables became explicit parameters.
11
+ */
12
+
13
+ import { resolveThemeMode } from "../resolve-theme-mode.ts";
14
+ import { composePageTitle } from "../compose-page-title.ts";
15
+ import { resolveEntityTitle } from "./meta-title.ts";
16
+ import {
17
+ applyLayoutDefaults,
18
+ EMPTY_PUCK_DATA,
19
+ extractPuckData,
20
+ pickRootProp,
21
+ readBrandTokens,
22
+ type PuckData,
23
+ type RegionEntry,
24
+ } from "./page-data-shared.ts";
25
+
26
+ export function publicShellPageData(
27
+ resolved: Record<string, unknown>,
28
+ meta: Record<string, unknown>,
29
+ themeOverride: "light" | "dark" | undefined,
30
+ orgPrefix: boolean | undefined,
31
+ ): Record<string, unknown> {
32
+ const { brand, primaryColor, headingFont, bodyFont, logo, brandTheme } =
33
+ readBrandTokens(resolved);
34
+ const headerSource = resolved.header as Record<string, unknown> | undefined;
35
+ const footerSource = resolved.footer as Record<string, unknown> | undefined;
36
+
37
+ const page = (resolved.page as Record<string, unknown>) ?? {};
38
+ const website =
39
+ (resolved.website_by_host as Record<string, unknown>) ??
40
+ (resolved.website as Record<string, unknown>) ??
41
+ {};
42
+ const layoutDefaults =
43
+ (website.layout_defaults as Record<string, string>) ??
44
+ (website.layout_settings as Record<string, string>) ??
45
+ {};
46
+ const layoutSettings = website.layout_settings as
47
+ | { theme?: unknown }
48
+ | null
49
+ | undefined;
50
+ const websiteTheme =
51
+ layoutSettings && typeof layoutSettings.theme === "string"
52
+ ? layoutSettings.theme
53
+ : undefined;
54
+
55
+ const regions: RegionEntry[] = [];
56
+
57
+ if (headerSource) {
58
+ const headerPuck = extractPuckData(headerSource) ?? EMPTY_PUCK_DATA;
59
+ applyLayoutDefaults(headerPuck, layoutDefaults);
60
+ regions.push({
61
+ slot: "header",
62
+ puckData: headerPuck,
63
+ pageId: (headerSource.id as string) ?? "",
64
+ });
65
+ }
66
+
67
+ const bodyPuck = extractPuckData(page) ?? EMPTY_PUCK_DATA;
68
+ applyLayoutDefaults(bodyPuck, layoutDefaults);
69
+ regions.push({
70
+ slot: "body",
71
+ puckData: bodyPuck,
72
+ pageId: (page.id as string) ?? "",
73
+ metaTitle: (page.meta_title as string) ?? null,
74
+ metaDescription: (page.meta_description as string) ?? null,
75
+ });
76
+
77
+ if (footerSource) {
78
+ const footerPuck = extractPuckData(footerSource) ?? EMPTY_PUCK_DATA;
79
+ applyLayoutDefaults(footerPuck, layoutDefaults);
80
+ regions.push({
81
+ slot: "footer",
82
+ puckData: footerPuck,
83
+ pageId: (footerSource.id as string) ?? "",
84
+ });
85
+ }
86
+
87
+ const rootTheme = pickRootProp<string>(regions, "theme");
88
+ const { theme } = resolveThemeMode({
89
+ override: themeOverride,
90
+ candidates: [rootTheme, brandTheme, websiteTheme],
91
+ });
92
+
93
+ const overrideColor = pickRootProp<string>(regions, "primaryColor");
94
+ const overrideBg = pickRootProp<string>(regions, "backgroundColor");
95
+ const overrideHeading = pickRootProp<string>(regions, "headingFont");
96
+ const overrideBody = pickRootProp<string>(regions, "bodyFont");
97
+
98
+ const pageName = (page.name as string) || null;
99
+ const entityTitle = resolveEntityTitle(resolved);
100
+ const bodyMetaTitle = regions.find((r) => r.slot === "body")?.metaTitle;
101
+ const pageRootTitle = pickRootProp<string>(regions, "title");
102
+ const pageDescription =
103
+ regions.find((r) => r.slot === "body")?.metaDescription ??
104
+ pickRootProp<string>(regions, "description") ??
105
+ null;
106
+
107
+ return {
108
+ puckData: { root: { props: {} }, content: [] } as PuckData,
109
+ regions,
110
+ // Region-shell blocks (e.g. the header Navigation deriving the body's
111
+ // document sections) read the resolved entity graph from
112
+ // pageData.loaderData — the same surface Branch 1 exposes. Without it a
113
+ // header/footer block serverLoader gets ctx.loaderData=undefined and
114
+ // cannot see the body page, so a server-derived nav produced no items
115
+ // and the menu was client-only (vanished on stale tabs / narrow views).
116
+ loaderData: resolved,
117
+ website: {
118
+ slug: website.slug as string,
119
+ name: website.name as string,
120
+ organisation_id: website.organisation_id as string | null,
121
+ // header_settings carries the DB-driven header config (e.g.
122
+ // backdropBlur) the public shell applies — PublicTemplate reads
123
+ // pageData.website.header_settings.backdropBlur. Dropping it here
124
+ // silently kills the header blur even though the DB switch is set
125
+ // (the 2026-06-17 regression). Pass it through verbatim.
126
+ header_settings:
127
+ (website.header_settings as Record<string, unknown> | null) ?? null,
128
+ },
129
+ orgId:
130
+ (page.organisation_id as string) ??
131
+ null,
132
+ brand: {
133
+ primaryColor: overrideColor ?? primaryColor,
134
+ // Thread the org brand's dark surface (brand_background_color /
135
+ // brand_dark_color) when the page sets no explicit override — so
136
+ // dark-mode pages use the brand's real dark colour, not a
137
+ // primary-derived tint. Uses the org brand's real dark colour.
138
+ backgroundColor:
139
+ overrideBg ?? (brand.backgroundColor as string | null) ?? null,
140
+ lightSurface: (brand.lightSurface as string | null) ?? null,
141
+ darkSurface: (brand.darkSurface as string | null) ?? null,
142
+ accentColor: (brand.accentColor as string | null) ?? null,
143
+ secondaryColor: (brand.secondaryColor as string | null) ?? null,
144
+ mutedColor: (brand.mutedColor as string | null) ?? null,
145
+ headingFont: overrideHeading ?? headingFont,
146
+ bodyFont: overrideBody ?? bodyFont,
147
+ logo,
148
+ companyName: (brand.companyName as string) ?? null,
149
+ companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
150
+ contactEmail: (brand.contactEmail as string) ?? null,
151
+ favicon: (brand.favicon as string) ?? null,
152
+ },
153
+ theme,
154
+ meta: {
155
+ title: composePageTitle(
156
+ {
157
+ pageMetaTitle: bodyMetaTitle,
158
+ templateTitle: (meta.title as string) || undefined,
159
+ websiteName: (website.name as string) || undefined,
160
+ organisationName: (brand.companyName as string) || undefined,
161
+ pageName: pageName ?? pageRootTitle,
162
+ entityTitle: entityTitle || undefined,
163
+ },
164
+ { orgPrefix },
165
+ ),
166
+ ...(pageDescription ? { description: pageDescription } : {}),
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Branch 1 of the resolved-graph → pageData conversion: arbitrary region-slots
7
+ * (portal/agents/docs) driven by resource_loader.region_slots.
8
+ *
9
+ * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
10
+ * the closure variables became explicit parameters.
11
+ */
12
+
13
+ import { resolveThemeMode } from "../resolve-theme-mode.ts";
14
+ import { composePageTitle } from "../compose-page-title.ts";
15
+ import { resolveEntityTitle } from "./meta-title.ts";
16
+ import {
17
+ applyLayoutDefaults,
18
+ EMPTY_PUCK_DATA,
19
+ extractPuckData,
20
+ pickRootProp,
21
+ readBrandTokens,
22
+ warnUnresolvedRegionSlot,
23
+ type PuckData,
24
+ type RegionEntry,
25
+ } from "./page-data-shared.ts";
26
+
27
+ export function regionSlotsPageData(
28
+ resolved: Record<string, unknown>,
29
+ meta: Record<string, unknown>,
30
+ regionSlots: string[],
31
+ themeOverride: "light" | "dark" | undefined,
32
+ orgPrefix: boolean | undefined,
33
+ ): Record<string, unknown> {
34
+ const { brand, primaryColor, headingFont, bodyFont, logo, brandTheme } =
35
+ readBrandTokens(resolved);
36
+ const website = (resolved.website as Record<string, unknown>) ?? {};
37
+ const layoutDefaults =
38
+ (website.layout_defaults as Record<string, string>) ??
39
+ (website.layout_settings as Record<string, string>) ??
40
+ {};
41
+ const layoutSettings = website.layout_settings as
42
+ | { theme?: unknown }
43
+ | null
44
+ | undefined;
45
+ const websiteTheme =
46
+ layoutSettings && typeof layoutSettings.theme === "string"
47
+ ? layoutSettings.theme
48
+ : undefined;
49
+
50
+ // Title derives from the primary resolved entity (e.g. the GPT agent),
51
+ // matching the flat-page branch — so a utility-shell GPT shows the agent
52
+ // name in <title>/OG, not the generic "Portal". Falls back to the website
53
+ // name, then "Portal". Without this, the region-slots branch returned
54
+ // early with a hardcoded "Portal" title (gpt-test: SSR/DOM/OG title FAIL).
55
+ const entityTitle = resolveEntityTitle(resolved);
56
+
57
+ const regions: RegionEntry[] = [];
58
+ for (const slot of regionSlots) {
59
+ const source = resolved[slot] as Record<string, unknown> | undefined;
60
+ if (!source) {
61
+ warnUnresolvedRegionSlot(slot);
62
+ continue;
63
+ }
64
+ const puckData = extractPuckData(source) ?? EMPTY_PUCK_DATA;
65
+ applyLayoutDefaults(puckData, layoutDefaults);
66
+ regions.push({
67
+ slot,
68
+ puckData,
69
+ pageId: (source.id as string) ?? "",
70
+ });
71
+ }
72
+
73
+ if (regions.length === 0) {
74
+ return {
75
+ puckData: { root: { props: {} }, content: [] } as PuckData,
76
+ theme: "dark" as const,
77
+ brand: {
78
+ primaryColor,
79
+ backgroundColor: (brand.backgroundColor as string | null) ?? null,
80
+ lightSurface: (brand.lightSurface as string | null) ?? null,
81
+ darkSurface: (brand.darkSurface as string | null) ?? null,
82
+ headingFont,
83
+ bodyFont,
84
+ logo,
85
+ companyName: (brand.companyName as string) ?? null,
86
+ companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
87
+ contactEmail: (brand.contactEmail as string) ?? null,
88
+ favicon: (brand.favicon as string) ?? null,
89
+ },
90
+ meta: {
91
+ title: composePageTitle(
92
+ {
93
+ templateTitle: (meta.title as string) || undefined,
94
+ websiteName: (website.name as string) || undefined,
95
+ organisationName: (brand.companyName as string) || undefined,
96
+ entityTitle: entityTitle || undefined,
97
+ },
98
+ { orgPrefix },
99
+ ),
100
+ },
101
+ };
102
+ }
103
+
104
+ const rootTheme = pickRootProp<string>(regions, "theme");
105
+ const { theme } = resolveThemeMode({
106
+ override: themeOverride,
107
+ candidates: [rootTheme, brandTheme, websiteTheme],
108
+ });
109
+
110
+ const overrideColor = pickRootProp<string>(regions, "primaryColor");
111
+ const overrideHeading = pickRootProp<string>(regions, "headingFont");
112
+ const overrideBody = pickRootProp<string>(regions, "bodyFont");
113
+ // The page the user is viewing is the main content region (center for
114
+ // portal, body for utility). Its name ("Dashboard") is the title subject.
115
+ const contentSlot = regionSlots.find((s) => s === "center" || s === "body");
116
+ const contentPageName = contentSlot
117
+ ? ((resolved[contentSlot] as Record<string, unknown> | undefined)?.name as
118
+ | string
119
+ | undefined)
120
+ : undefined;
121
+
122
+ return {
123
+ puckData: { root: { props: {} }, content: [] } as PuckData,
124
+ regions,
125
+ // Region-shell blocks (e.g. the GPT chat in the center region) read
126
+ // their resolved entities from pageData.loaderData — same surface the
127
+ // flat single-page branch exposes. Without it the chat block has no
128
+ // agent and renders "Custom GPT not found".
129
+ loaderData: resolved,
130
+ website: {
131
+ slug: website.slug as string,
132
+ name: website.name as string,
133
+ organisation_id: website.organisation_id as string | null,
134
+ // header_settings carries the DB-driven header config (e.g.
135
+ // backdropBlur) the public shell applies — PublicTemplate reads
136
+ // pageData.website.header_settings.backdropBlur. Dropping it here
137
+ // silently kills the header blur even though the DB switch is set
138
+ // (the 2026-06-17 regression). Pass it through verbatim.
139
+ header_settings:
140
+ (website.header_settings as Record<string, unknown> | null) ?? null,
141
+ },
142
+ orgId:
143
+ ((resolved.center as Record<string, unknown> | undefined)
144
+ ?.organisation_id as string | null) ??
145
+ ((resolved.agent as Record<string, unknown> | undefined)
146
+ ?.organisation_id as string | null) ??
147
+ null,
148
+ brand: {
149
+ primaryColor: overrideColor ?? primaryColor,
150
+ backgroundColor: (brand.backgroundColor as string | null) ?? null,
151
+ lightSurface: (brand.lightSurface as string | null) ?? null,
152
+ darkSurface: (brand.darkSurface as string | null) ?? null,
153
+ headingFont: overrideHeading ?? headingFont,
154
+ bodyFont: overrideBody ?? bodyFont,
155
+ logo,
156
+ companyName: (brand.companyName as string) ?? null,
157
+ companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
158
+ contactEmail: (brand.contactEmail as string) ?? null,
159
+ favicon: (brand.favicon as string) ?? null,
160
+ },
161
+ theme,
162
+ meta: {
163
+ title: composePageTitle(
164
+ {
165
+ templateTitle: (meta.title as string) || undefined,
166
+ websiteName: (website.name as string) || undefined,
167
+ organisationName: (brand.companyName as string) || undefined,
168
+ pageName: contentPageName,
169
+ entityTitle: entityTitle || undefined,
170
+ },
171
+ { orgPrefix },
172
+ ),
173
+ },
174
+ };
175
+ }