@teamscala/service-boot 0.23.416 → 0.23.418

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,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
+ }