@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,171 @@
1
+ /**
2
+ * @system ui-service-boot
3
+ * @status handwritten
4
+ * @edit edit directly
5
+ *
6
+ * Branch 3 of the resolved-graph → pageData conversion: the single-page
7
+ * fallback (form/quiz/proposal/gpt pages with no region slots and no
8
+ * header/footer sources).
9
+ *
10
+ * Split out of resolve-page-data.ts (547 lines) — byte-identical body; only
11
+ * the closure variables became explicit parameters.
12
+ */
13
+
14
+ import { resolveThemeMode } from "../resolve-theme-mode.ts";
15
+ import { composePageTitle } from "../compose-page-title.ts";
16
+ import { resolveEntityTitle } from "./meta-title.ts";
17
+ import {
18
+ EMPTY_PUCK_DATA,
19
+ extractPuckData,
20
+ readBrandTokens,
21
+ type PuckData,
22
+ } from "./page-data-shared.ts";
23
+
24
+ export function singlePageData(
25
+ resolved: Record<string, unknown>,
26
+ meta: Record<string, unknown>,
27
+ providers: string[] | undefined,
28
+ themeOverride: "light" | "dark" | undefined,
29
+ orgPrefix: boolean | undefined,
30
+ ): Record<string, unknown> {
31
+ const { brand, primaryColor, headingFont, bodyFont, logo, brandTheme } =
32
+ readBrandTokens(resolved);
33
+
34
+ const page = (resolved.page as Record<string, unknown>) ?? null;
35
+ const puckData = page
36
+ ? extractPuckData(page) ?? EMPTY_PUCK_DATA
37
+ : ({ root: { props: {} }, content: [] } as PuckData);
38
+
39
+ const website = (resolved.website as Record<string, unknown>) ?? {};
40
+ const layoutSettings = (website.layout_settings ??
41
+ resolved.websiteDefaults) as { theme?: unknown } | null | undefined;
42
+ const websiteTheme =
43
+ layoutSettings && typeof layoutSettings.theme === "string"
44
+ ? layoutSettings.theme
45
+ : undefined;
46
+
47
+ const pageRoot = (
48
+ puckData as { root?: { props?: Record<string, unknown> } } | null
49
+ )?.root?.props;
50
+ const rootTheme =
51
+ pageRoot && typeof pageRoot.theme === "string" ? pageRoot.theme : undefined;
52
+ const agent = (resolved.agent as Record<string, unknown>) ?? null;
53
+ // A GPT page's mode is the agent's dark_theme flag (the DB switch). It's the
54
+ // top candidate so the SSR :root matches the GPT shell's own scope (both
55
+ // derive from the same ai_agents.dark_theme) — flip the flag, the whole GPT
56
+ // + everything it frames flips. themeOverride (?theme=) still wins for an
57
+ // embedded render.
58
+ const agentTheme =
59
+ typeof agent?.dark_theme === "boolean"
60
+ ? agent.dark_theme
61
+ ? "dark"
62
+ : "light"
63
+ : undefined;
64
+ // themeOverride (?theme=) wins so a form/quiz framed in a GPT panel matches
65
+ // the host shell's light/dark scheme — this single-page branch serves
66
+ // form/quiz/proposal/gpt pages.
67
+ const { theme } = resolveThemeMode({
68
+ override: themeOverride,
69
+ candidates: [agentTheme, rootTheme, brandTheme, websiteTheme],
70
+ });
71
+ const orgId =
72
+ (page?.organisation_id as string) ??
73
+ (agent?.organisation_id as string) ??
74
+ null;
75
+
76
+ const overrideColor =
77
+ pageRoot &&
78
+ typeof pageRoot.primaryColor === "string" &&
79
+ pageRoot.primaryColor
80
+ ? pageRoot.primaryColor
81
+ : null;
82
+ const overrideHeading =
83
+ pageRoot && typeof pageRoot.headingFont === "string" && pageRoot.headingFont
84
+ ? pageRoot.headingFont
85
+ : null;
86
+ const overrideBody =
87
+ pageRoot && typeof pageRoot.bodyFont === "string" && pageRoot.bodyFont
88
+ ? pageRoot.bodyFont
89
+ : null;
90
+
91
+ const screenMode = pageRoot?.screenMode as string | undefined;
92
+
93
+ const pageName = (page?.name as string) || null;
94
+ const entityTitle = resolveEntityTitle(resolved);
95
+ const entityDescription =
96
+ ((resolved.form as Record<string, unknown>)?.description as string) ||
97
+ ((resolved.quiz as Record<string, unknown>)?.description as string) ||
98
+ ((resolved.proposal as Record<string, unknown>)?.description as string) ||
99
+ ((resolved.agent as Record<string, unknown>)?.description as string);
100
+ const pageDescription =
101
+ (page?.meta_description as string) ||
102
+ entityDescription ||
103
+ (pageRoot?.description as string) ||
104
+ null;
105
+ const loaderDataPageMetaTitle = (
106
+ resolved.loaderData as { page?: { meta_title?: unknown } } | undefined
107
+ )?.page?.meta_title;
108
+ const pageMetaTitle =
109
+ (typeof loaderDataPageMetaTitle === "string" &&
110
+ loaderDataPageMetaTitle.trim()
111
+ ? loaderDataPageMetaTitle.trim()
112
+ : typeof page?.meta_title === "string" && page.meta_title.trim()
113
+ ? page.meta_title.trim()
114
+ : null) || null;
115
+
116
+ // Title is DERIVED via the single composer: meta_title is an explicit
117
+ // per-page OVERRIDE (priority 1), a curated resource template wins for
118
+ // resource surfaces (priority 2), otherwise <website>: <subject>. The
119
+ // /sign-in special-case is gone — pageMetaTitle now wins uniformly, so a
120
+ // sign-in page with a meta_title is honoured on every path, not just /sign-in.
121
+ const mergedMeta: Record<string, unknown> = {
122
+ ...(pageDescription ? { description: pageDescription } : {}),
123
+ ...meta,
124
+ };
125
+ mergedMeta.title = composePageTitle(
126
+ {
127
+ pageMetaTitle,
128
+ templateTitle: (meta.title as string) || undefined,
129
+ websiteName: (website.name as string) || undefined,
130
+ organisationName: (brand.companyName as string) || undefined,
131
+ pageName: pageName ?? ((pageRoot?.title as string) || undefined),
132
+ entityTitle: entityTitle || undefined,
133
+ },
134
+ { orgPrefix },
135
+ );
136
+
137
+ return {
138
+ puckData,
139
+ website: {
140
+ slug: website.slug as string,
141
+ name: website.name as string,
142
+ organisation_id: website.organisation_id as string | null,
143
+ // Pass header_settings through (DB-driven header blur etc.) — see the
144
+ // other two website assignments above. Dropping it kills the blur.
145
+ header_settings:
146
+ (website.header_settings as Record<string, unknown> | null) ?? null,
147
+ },
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
+ orgId,
163
+ loaderData: resolved,
164
+ ...(screenMode ? { fullScreen: screenMode === "full-screen" } : {}),
165
+ ...(pageRoot?.standalone != null
166
+ ? { standalone: pageRoot.standalone === false ? false : undefined }
167
+ : {}),
168
+ ...(providers ? { providerKeys: providers } : {}),
169
+ meta: mergedMeta,
170
+ };
171
+ }