@dloizides/tenant-theme-web 1.0.1

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/dist/index.js ADDED
@@ -0,0 +1,295 @@
1
+ 'use strict';
2
+
3
+ var utils = require('@dloizides/utils');
4
+
5
+ // src/fetchTenantTheme.ts
6
+ function mapColorsToMode(dto, fallback) {
7
+ if (!utils.isValueDefined(dto)) {
8
+ return fallback;
9
+ }
10
+ return {
11
+ background: dto.background ?? fallback.background,
12
+ surface: dto.surface ?? fallback.surface,
13
+ surfaceElevated: fallback.surfaceElevated,
14
+ text: dto.onBackground ?? fallback.text,
15
+ textSecondary: dto.onSurface ?? fallback.textSecondary,
16
+ border: fallback.border,
17
+ divider: fallback.divider
18
+ };
19
+ }
20
+ function mapBrandColors(colors, defaults) {
21
+ return {
22
+ primary: colors?.primary ?? defaults.primary,
23
+ secondary: colors?.secondary ?? defaults.secondary,
24
+ accent: colors?.primaryLight ?? defaults.accent
25
+ };
26
+ }
27
+ function mapTypography(dto) {
28
+ if (!utils.isValueDefined(dto.typography)) {
29
+ return void 0;
30
+ }
31
+ return {
32
+ fontFamily: dto.typography.fontFamily ?? void 0,
33
+ headingScale: dto.typography.headerScale ?? void 0
34
+ };
35
+ }
36
+ function toTenantThemeConfig(dto, defaultThemeConfig) {
37
+ return {
38
+ ...mapBrandColors(dto.colors, defaultThemeConfig),
39
+ semantic: {
40
+ success: defaultThemeConfig.semantic?.success,
41
+ warning: defaultThemeConfig.semantic?.warning,
42
+ error: dto.colors?.error ?? defaultThemeConfig.semantic?.error,
43
+ info: defaultThemeConfig.semantic?.info
44
+ },
45
+ light: mapColorsToMode(dto.colors, defaultThemeConfig.light),
46
+ dark: mapColorsToMode(dto.darkColors, defaultThemeConfig.dark),
47
+ typography: mapTypography(dto),
48
+ branding: {
49
+ presetId: dto.presetId ?? null,
50
+ logoContentId: dto.logoContentId ?? null,
51
+ faviconContentId: dto.faviconContentId ?? null
52
+ }
53
+ };
54
+ }
55
+ function hasThemeData(dto) {
56
+ return utils.isValueDefined(dto.presetId) || utils.isValueDefined(dto.colors) || utils.isValueDefined(dto.darkColors) || utils.isValueDefined(dto.typography);
57
+ }
58
+
59
+ // src/fetchTenantTheme.ts
60
+ var HTTP_SUCCESS_MIN = 200;
61
+ var HTTP_SUCCESS_MAX = 299;
62
+ var HTTP_NOT_MODIFIED = 304;
63
+ var HTTP_NOT_FOUND = 404;
64
+ var ETAG_HEADER = "etag";
65
+ var IF_NONE_MATCH_HEADER = "If-None-Match";
66
+ function buildRequestHeaders(cachedEtag) {
67
+ const headers = {
68
+ "Accept": "application/json"
69
+ };
70
+ if (utils.isNotEmptyString(cachedEtag)) {
71
+ headers[IF_NONE_MATCH_HEADER] = cachedEtag;
72
+ }
73
+ return headers;
74
+ }
75
+ function extractEtag(response) {
76
+ const headerValue = response.headers[ETAG_HEADER];
77
+ if (typeof headerValue === "string" && headerValue.length > 0) {
78
+ return headerValue;
79
+ }
80
+ return null;
81
+ }
82
+ function isErrorWithResponse(value) {
83
+ if (typeof value !== "object" || !utils.isValueDefined(value)) {
84
+ return false;
85
+ }
86
+ return "response" in value;
87
+ }
88
+ function isNotModifiedResponse(error) {
89
+ if (!isErrorWithResponse(error)) {
90
+ return false;
91
+ }
92
+ return error.response?.status === HTTP_NOT_MODIFIED;
93
+ }
94
+ function isNotFoundResponse(error) {
95
+ if (!isErrorWithResponse(error)) {
96
+ return false;
97
+ }
98
+ return error.response?.status === HTTP_NOT_FOUND;
99
+ }
100
+ function isSuccessOrNotModified(status) {
101
+ return status >= HTTP_SUCCESS_MIN && status <= HTTP_SUCCESS_MAX || status === HTTP_NOT_MODIFIED;
102
+ }
103
+ function buildNotModifiedResult(cachedEtag) {
104
+ return { themeConfig: null, etag: cachedEtag ?? null, notModified: true };
105
+ }
106
+ async function fetchTenantTheme(tenantId, options) {
107
+ const { httpGet, defaultThemeConfig, baseURL, cachedEtag, signal } = options;
108
+ const headers = buildRequestHeaders(cachedEtag);
109
+ try {
110
+ const response = await httpGet({
111
+ url: `/api/tenants/${tenantId}/theme`,
112
+ baseURL,
113
+ headers,
114
+ signal,
115
+ validateStatus: isSuccessOrNotModified
116
+ });
117
+ if (response.status === HTTP_NOT_MODIFIED) {
118
+ return buildNotModifiedResult(cachedEtag);
119
+ }
120
+ const dto = response.data;
121
+ const etag = extractEtag(response);
122
+ const hasData = utils.isValueDefined(dto) && hasThemeData(dto);
123
+ return {
124
+ themeConfig: hasData ? toTenantThemeConfig(dto, defaultThemeConfig) : null,
125
+ etag,
126
+ notModified: false
127
+ };
128
+ } catch (error) {
129
+ if (isNotModifiedResponse(error)) {
130
+ return buildNotModifiedResult(cachedEtag);
131
+ }
132
+ if (isNotFoundResponse(error)) {
133
+ return { themeConfig: null, etag: null, notModified: false };
134
+ }
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ // src/saveTenantTheme.ts
140
+ function toApiThemeRequest(config) {
141
+ return {
142
+ presetId: config.branding.presetId ?? null,
143
+ colors: {
144
+ primary: config.primary,
145
+ secondary: config.secondary,
146
+ background: config.light.background,
147
+ surface: config.light.surface,
148
+ error: config.semantic?.error ?? null,
149
+ onBackground: config.light.text,
150
+ onSurface: config.light.textSecondary
151
+ },
152
+ darkColors: {
153
+ primary: config.primary,
154
+ secondary: config.secondary,
155
+ background: config.dark.background,
156
+ surface: config.dark.surface,
157
+ error: config.semantic?.error ?? null,
158
+ onBackground: config.dark.text,
159
+ onSurface: config.dark.textSecondary
160
+ },
161
+ typography: config.typography ? { fontFamily: config.typography.fontFamily ?? null, headerScale: config.typography.headingScale ?? null } : null,
162
+ logoContentId: config.branding.logoContentId ?? null,
163
+ faviconContentId: config.branding.faviconContentId ?? null
164
+ };
165
+ }
166
+ async function saveTenantTheme(tenantId, config, httpPut) {
167
+ return httpPut({
168
+ url: `/api/tenants/${tenantId}/theme`,
169
+ headers: { "Content-Type": "application/json" },
170
+ data: toApiThemeRequest(config)
171
+ });
172
+ }
173
+ var CACHE_KEY_PREFIX = "tenant-theme-";
174
+ var HOURS_PER_DAY = 24;
175
+ var MINUTES_PER_HOUR = 60;
176
+ var SECONDS_PER_MINUTE = 60;
177
+ var MS_PER_SECOND = 1e3;
178
+ var MAX_CACHE_AGE_MS = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
179
+ var warnLogger;
180
+ function configureThemeCacheLogger(logger) {
181
+ warnLogger = logger;
182
+ }
183
+ function warn(message, error) {
184
+ if (utils.isValueDefined(warnLogger)) {
185
+ warnLogger("themeCacheStorage", message, error);
186
+ }
187
+ }
188
+ function buildCacheKey(tenantId) {
189
+ return `${CACHE_KEY_PREFIX}${tenantId}`;
190
+ }
191
+ function isStorageAvailable() {
192
+ if (typeof window === "undefined") {
193
+ return false;
194
+ }
195
+ try {
196
+ return typeof window.localStorage !== "undefined";
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+ function isRecord(value) {
202
+ return utils.isValueDefined(value) && typeof value === "object";
203
+ }
204
+ function isCachedThemeData(value) {
205
+ if (!isRecord(value)) {
206
+ return false;
207
+ }
208
+ const hasConfig = utils.isValueDefined(value.config) && typeof value.config === "object";
209
+ const hasCachedAt = typeof value.cachedAt === "number";
210
+ return hasConfig && hasCachedAt;
211
+ }
212
+ function readThemeCache(tenantId) {
213
+ if (!isStorageAvailable()) {
214
+ return null;
215
+ }
216
+ try {
217
+ const raw = localStorage.getItem(buildCacheKey(tenantId));
218
+ if (!utils.isValueDefined(raw)) {
219
+ return null;
220
+ }
221
+ const parsed = JSON.parse(raw);
222
+ if (!isCachedThemeData(parsed)) {
223
+ return null;
224
+ }
225
+ const age = Date.now() - parsed.cachedAt;
226
+ if (age > MAX_CACHE_AGE_MS) {
227
+ return null;
228
+ }
229
+ return parsed;
230
+ } catch (error) {
231
+ warn("Failed to read theme cache", error);
232
+ return null;
233
+ }
234
+ }
235
+ function writeThemeCache(tenantId, config, etag) {
236
+ if (!isStorageAvailable()) {
237
+ return;
238
+ }
239
+ const data = {
240
+ config,
241
+ logoUrl: config.branding.logoContentId,
242
+ faviconUrl: config.branding.faviconContentId,
243
+ etag,
244
+ cachedAt: Date.now()
245
+ };
246
+ try {
247
+ localStorage.setItem(buildCacheKey(tenantId), JSON.stringify(data));
248
+ } catch (error) {
249
+ warn("Failed to write theme cache", error);
250
+ }
251
+ }
252
+ function clearThemeCache(tenantId) {
253
+ if (!isStorageAvailable()) {
254
+ return;
255
+ }
256
+ try {
257
+ localStorage.removeItem(buildCacheKey(tenantId));
258
+ } catch (error) {
259
+ warn("Failed to clear theme cache", error);
260
+ }
261
+ }
262
+ function clearAllThemeCaches() {
263
+ if (!isStorageAvailable()) {
264
+ return;
265
+ }
266
+ try {
267
+ const keysToRemove = [];
268
+ for (let i = 0; i < localStorage.length; i++) {
269
+ const key = localStorage.key(i);
270
+ if (utils.isValueDefined(key) && key.startsWith(CACHE_KEY_PREFIX)) {
271
+ keysToRemove.push(key);
272
+ }
273
+ }
274
+ for (const key of keysToRemove) {
275
+ localStorage.removeItem(key);
276
+ }
277
+ } catch (error) {
278
+ warn("Failed to clear all theme caches", error);
279
+ }
280
+ }
281
+
282
+ exports.CACHE_KEY_PREFIX = CACHE_KEY_PREFIX;
283
+ exports.MAX_CACHE_AGE_MS = MAX_CACHE_AGE_MS;
284
+ exports.clearAllThemeCaches = clearAllThemeCaches;
285
+ exports.clearThemeCache = clearThemeCache;
286
+ exports.configureThemeCacheLogger = configureThemeCacheLogger;
287
+ exports.fetchTenantTheme = fetchTenantTheme;
288
+ exports.hasThemeData = hasThemeData;
289
+ exports.readThemeCache = readThemeCache;
290
+ exports.saveTenantTheme = saveTenantTheme;
291
+ exports.toApiThemeRequest = toApiThemeRequest;
292
+ exports.toTenantThemeConfig = toTenantThemeConfig;
293
+ exports.writeThemeCache = writeThemeCache;
294
+ //# sourceMappingURL=index.js.map
295
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mapToThemeConfig.ts","../src/fetchTenantTheme.ts","../src/saveTenantTheme.ts","../src/themeCacheStorage.ts"],"names":["isValueDefined","isNotEmptyString"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Maps the flat API DTO returned by GET /api/tenants/{tenantId}/theme into the\n * frontend {@link TenantThemeConfig} shape.\n *\n * The fallback values (`defaultThemeConfig`) are **app-supplied** so the package\n * stays palette-agnostic: each product passes its own DEFAULT_THEME_CONFIG.\n */\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type {\n ApiThemeColorsDto,\n ApiThemeResponseDto,\n TenantThemeConfig,\n ThemeModeColors,\n} from './types';\n\nfunction mapColorsToMode(\n dto: ApiThemeColorsDto | null | undefined,\n fallback: ThemeModeColors,\n): ThemeModeColors {\n if (!isValueDefined(dto)) {\n return fallback;\n }\n return {\n background: dto.background ?? fallback.background,\n surface: dto.surface ?? fallback.surface,\n surfaceElevated: fallback.surfaceElevated,\n text: dto.onBackground ?? fallback.text,\n textSecondary: dto.onSurface ?? fallback.textSecondary,\n border: fallback.border,\n divider: fallback.divider,\n };\n}\n\nfunction mapBrandColors(\n colors: ApiThemeColorsDto | null | undefined,\n defaults: TenantThemeConfig,\n): Pick<TenantThemeConfig, 'primary' | 'secondary' | 'accent'> {\n return {\n primary: colors?.primary ?? defaults.primary,\n secondary: colors?.secondary ?? defaults.secondary,\n accent: colors?.primaryLight ?? defaults.accent,\n };\n}\n\nfunction mapTypography(dto: ApiThemeResponseDto): TenantThemeConfig['typography'] {\n if (!isValueDefined(dto.typography)) {\n return undefined;\n }\n return {\n fontFamily: dto.typography.fontFamily ?? undefined,\n headingScale: dto.typography.headerScale ?? undefined,\n };\n}\n\n/**\n * Transforms a raw API DTO into a {@link TenantThemeConfig}, filling any missing\n * field from the app-supplied `defaultThemeConfig`.\n */\nexport function toTenantThemeConfig(\n dto: ApiThemeResponseDto,\n defaultThemeConfig: TenantThemeConfig,\n): TenantThemeConfig {\n return {\n ...mapBrandColors(dto.colors, defaultThemeConfig),\n semantic: {\n success: defaultThemeConfig.semantic?.success,\n warning: defaultThemeConfig.semantic?.warning,\n error: dto.colors?.error ?? defaultThemeConfig.semantic?.error,\n info: defaultThemeConfig.semantic?.info,\n },\n light: mapColorsToMode(dto.colors, defaultThemeConfig.light),\n dark: mapColorsToMode(dto.darkColors, defaultThemeConfig.dark),\n typography: mapTypography(dto),\n branding: {\n presetId: dto.presetId ?? null,\n logoContentId: dto.logoContentId ?? null,\n faviconContentId: dto.faviconContentId ?? null,\n },\n };\n}\n\n/** True when the DTO carries any theme payload worth mapping. */\nexport function hasThemeData(dto: ApiThemeResponseDto): boolean {\n return (\n isValueDefined(dto.presetId) ||\n isValueDefined(dto.colors) ||\n isValueDefined(dto.darkColors) ||\n isValueDefined(dto.typography)\n );\n}\n","/**\n * ETag-conditional tenant-theme fetch.\n *\n * The API returns a flat DTO shape (presetId, colors, darkColors, typography);\n * this transforms it into the frontend {@link TenantThemeConfig} format.\n *\n * Supports ETag-based conditional requests:\n * - Sends `If-None-Match` when a cached ETag is available\n * - Returns the response ETag for caching\n * - Returns `notModified: true` on 304 so the caller keeps using cached data\n *\n * Endpoint: GET {baseURL}/api/tenants/{tenantId}/theme\n *\n * The HTTP transport (`httpGet`), the fallback palette (`defaultThemeConfig`),\n * and the API base URL (`baseURL`) are all app-supplied ports.\n */\nimport { isNotEmptyString, isValueDefined } from '@dloizides/utils';\n\nimport { hasThemeData, toTenantThemeConfig } from './mapToThemeConfig';\n\nimport type {\n ApiThemeResponseDto,\n HttpGet,\n HttpResponse,\n TenantThemeConfig,\n} from './types';\n\n// -- Constants ----------------------------------------------------------------\n\n/** HTTP status range for successful responses (inclusive). */\nconst HTTP_SUCCESS_MIN = 200;\nconst HTTP_SUCCESS_MAX = 299;\n\n/** HTTP 304 status code for Not Modified responses. */\nconst HTTP_NOT_MODIFIED = 304;\n\n/** HTTP 404 status code for Not Found responses. */\nconst HTTP_NOT_FOUND = 404;\n\nconst ETAG_HEADER = 'etag';\nconst IF_NONE_MATCH_HEADER = 'If-None-Match';\n\n// -- Options + result types ---------------------------------------------------\n\n/** Options for {@link fetchTenantTheme}. */\nexport interface FetchTenantThemeOptions {\n signal?: AbortSignal;\n /** Cached ETag to send as the If-None-Match header. */\n cachedEtag?: string;\n /** App-supplied GET transport (wire to the app's apiClient/bff-web-client). */\n httpGet: HttpGet;\n /** App-supplied fallback palette (the product's DEFAULT_THEME_CONFIG). */\n defaultThemeConfig: TenantThemeConfig;\n /** Optional API base URL. Omit to use the transport's own base. */\n baseURL?: string;\n}\n\n/** Frontend-facing response shape consumed by the theme hook. */\nexport interface TenantThemeResponse {\n themeConfig: TenantThemeConfig | null;\n etag: string | null;\n /** True when the server returned 304 Not Modified (use cached data). */\n notModified: boolean;\n}\n\n// -- HTTP helpers -------------------------------------------------------------\n\nfunction buildRequestHeaders(cachedEtag: string | undefined): Record<string, string> {\n const headers: Record<string, string> = {\n 'Accept': 'application/json',\n };\n if (isNotEmptyString(cachedEtag)) {\n headers[IF_NONE_MATCH_HEADER] = cachedEtag;\n }\n return headers;\n}\n\nfunction extractEtag(response: HttpResponse): string | null {\n const headerValue: unknown = response.headers[ETAG_HEADER];\n if (typeof headerValue === 'string' && headerValue.length > 0) {\n return headerValue;\n }\n return null;\n}\n\ninterface ErrorWithResponse {\n response?: { status?: unknown };\n}\n\nfunction isErrorWithResponse(value: unknown): value is ErrorWithResponse {\n if (typeof value !== 'object' || !isValueDefined(value)) {\n return false;\n }\n return 'response' in value;\n}\n\nfunction isNotModifiedResponse(error: unknown): boolean {\n if (!isErrorWithResponse(error)) {\n return false;\n }\n return error.response?.status === HTTP_NOT_MODIFIED;\n}\n\nfunction isNotFoundResponse(error: unknown): boolean {\n if (!isErrorWithResponse(error)) {\n return false;\n }\n return error.response?.status === HTTP_NOT_FOUND;\n}\n\nfunction isSuccessOrNotModified(status: number): boolean {\n return (status >= HTTP_SUCCESS_MIN && status <= HTTP_SUCCESS_MAX) || status === HTTP_NOT_MODIFIED;\n}\n\nfunction buildNotModifiedResult(cachedEtag: string | undefined): TenantThemeResponse {\n return { themeConfig: null, etag: cachedEtag ?? null, notModified: true };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * Fetch the tenant theme configuration via the app-supplied transport and\n * transform the API DTO into {@link TenantThemeConfig}. Returns a null config\n * when the tenant has no theme configured (200 empty DTO or 404).\n *\n * Supports ETag-based conditional requests:\n * - Pass `cachedEtag` to send the If-None-Match header\n * - On 304 Not Modified, returns `{ notModified: true }` so the caller uses\n * its cached data\n */\nexport async function fetchTenantTheme(\n tenantId: string,\n options: FetchTenantThemeOptions,\n): Promise<TenantThemeResponse> {\n const { httpGet, defaultThemeConfig, baseURL, cachedEtag, signal } = options;\n const headers = buildRequestHeaders(cachedEtag);\n\n try {\n const response = await httpGet<ApiThemeResponseDto>({\n url: `/api/tenants/${tenantId}/theme`,\n baseURL,\n headers,\n signal,\n validateStatus: isSuccessOrNotModified,\n });\n\n if (response.status === HTTP_NOT_MODIFIED) {\n return buildNotModifiedResult(cachedEtag);\n }\n\n const dto = response.data;\n const etag = extractEtag(response);\n const hasData = isValueDefined(dto) && hasThemeData(dto);\n\n return {\n themeConfig: hasData ? toTenantThemeConfig(dto, defaultThemeConfig) : null,\n etag,\n notModified: false,\n };\n } catch (error: unknown) {\n if (isNotModifiedResponse(error)) {\n return buildNotModifiedResult(cachedEtag);\n }\n if (isNotFoundResponse(error)) {\n return { themeConfig: null, etag: null, notModified: false };\n }\n throw error;\n }\n}\n","/**\n * Tenant-theme save (PUT) logic.\n *\n * Maps a {@link TenantThemeConfig} into the API's UpdateTenantThemeRequest body\n * and PUTs it via the app-supplied {@link HttpPut} transport.\n *\n * Endpoint: PUT /api/tenants/{tenantId}/theme\n */\nimport type {\n ApiThemeRequest,\n HttpPut,\n TenantThemeConfig,\n} from './types';\n\n/** Response shape from PUT /api/tenants/{tenantId}/theme */\nexport interface SaveThemeResponse {\n success: boolean;\n}\n\n/** Maps a frontend {@link TenantThemeConfig} to the API's request body. */\nexport function toApiThemeRequest(config: TenantThemeConfig): ApiThemeRequest {\n return {\n presetId: config.branding.presetId ?? null,\n colors: {\n primary: config.primary,\n secondary: config.secondary,\n background: config.light.background,\n surface: config.light.surface,\n error: config.semantic?.error ?? null,\n onBackground: config.light.text,\n onSurface: config.light.textSecondary,\n },\n darkColors: {\n primary: config.primary,\n secondary: config.secondary,\n background: config.dark.background,\n surface: config.dark.surface,\n error: config.semantic?.error ?? null,\n onBackground: config.dark.text,\n onSurface: config.dark.textSecondary,\n },\n typography: config.typography\n ? { fontFamily: config.typography.fontFamily ?? null, headerScale: config.typography.headingScale ?? null }\n : null,\n logoContentId: config.branding.logoContentId ?? null,\n faviconContentId: config.branding.faviconContentId ?? null,\n };\n}\n\n/**\n * Save the tenant theme via the app-supplied PUT transport.\n */\nexport async function saveTenantTheme(\n tenantId: string,\n config: TenantThemeConfig,\n httpPut: HttpPut,\n): Promise<SaveThemeResponse> {\n return httpPut<SaveThemeResponse, ApiThemeRequest>({\n url: `/api/tenants/${tenantId}/theme`,\n headers: { 'Content-Type': 'application/json' },\n data: toApiThemeRequest(config),\n });\n}\n","/**\n * localStorage cache utilities for tenant theme configuration.\n *\n * Provides read/write/clear operations with automatic expiry. All operations\n * are safe (no-throw) and return null on failure.\n *\n * The package emits no `console` calls; supply a logger via\n * {@link configureThemeCacheLogger} to capture cache-failure warnings (the app\n * wires its own logging service). When unset, failures are swallowed silently.\n */\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { CachedThemeData, TenantThemeConfig } from './types';\n\nconst CACHE_KEY_PREFIX = 'tenant-theme-';\n\n/** Maximum cache age: 24 hours in milliseconds */\nconst HOURS_PER_DAY = 24;\nconst MINUTES_PER_HOUR = 60;\nconst SECONDS_PER_MINUTE = 60;\nconst MS_PER_SECOND = 1000;\nconst MAX_CACHE_AGE_MS =\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;\n\n/** Minimal warn-only logger contract for cache failures. */\nexport type ThemeCacheWarn = (context: string, message: string, error?: unknown) => void;\n\nlet warnLogger: ThemeCacheWarn | undefined;\n\n/**\n * Supply a warn logger the cache calls when a localStorage operation fails.\n * Pass `undefined` to revert to silent (no-op) behavior.\n */\nexport function configureThemeCacheLogger(logger: ThemeCacheWarn | undefined): void {\n warnLogger = logger;\n}\n\nfunction warn(message: string, error: unknown): void {\n if (isValueDefined(warnLogger)) {\n warnLogger('themeCacheStorage', message, error);\n }\n}\n\nfunction buildCacheKey(tenantId: string): string {\n return `${CACHE_KEY_PREFIX}${tenantId}`;\n}\n\nfunction isStorageAvailable(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n try {\n return typeof window.localStorage !== 'undefined';\n } catch {\n return false;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isCachedThemeData(value: unknown): value is CachedThemeData {\n if (!isRecord(value)) {\n return false;\n }\n const hasConfig = isValueDefined(value.config) && typeof value.config === 'object';\n const hasCachedAt = typeof value.cachedAt === 'number';\n return hasConfig && hasCachedAt;\n}\n\n/**\n * Read cached theme data from localStorage.\n * Returns null if cache is missing, expired, or corrupt.\n */\nexport function readThemeCache(tenantId: string): CachedThemeData | null {\n if (!isStorageAvailable()) {\n return null;\n }\n\n try {\n const raw = localStorage.getItem(buildCacheKey(tenantId));\n if (!isValueDefined(raw)) {\n return null;\n }\n\n const parsed: unknown = JSON.parse(raw);\n if (!isCachedThemeData(parsed)) {\n return null;\n }\n\n const age = Date.now() - parsed.cachedAt;\n if (age > MAX_CACHE_AGE_MS) {\n return null;\n }\n\n return parsed;\n } catch (error) {\n warn('Failed to read theme cache', error);\n return null;\n }\n}\n\n/**\n * Write theme data to localStorage cache.\n */\nexport function writeThemeCache(tenantId: string, config: TenantThemeConfig, etag: string): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n const data: CachedThemeData = {\n config,\n logoUrl: config.branding.logoContentId,\n faviconUrl: config.branding.faviconContentId,\n etag,\n cachedAt: Date.now(),\n };\n\n try {\n localStorage.setItem(buildCacheKey(tenantId), JSON.stringify(data));\n } catch (error) {\n warn('Failed to write theme cache', error);\n }\n}\n\n/**\n * Clear cached theme data for a specific tenant.\n */\nexport function clearThemeCache(tenantId: string): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n try {\n localStorage.removeItem(buildCacheKey(tenantId));\n } catch (error) {\n warn('Failed to clear theme cache', error);\n }\n}\n\n/**\n * Clear all tenant theme caches from localStorage.\n * Used during logout to remove any tenant-specific data.\n */\nexport function clearAllThemeCaches(): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n try {\n const keysToRemove: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (isValueDefined(key) && key.startsWith(CACHE_KEY_PREFIX)) {\n keysToRemove.push(key);\n }\n }\n for (const key of keysToRemove) {\n localStorage.removeItem(key);\n }\n } catch (error) {\n warn('Failed to clear all theme caches', error);\n }\n}\n\nexport { MAX_CACHE_AGE_MS, CACHE_KEY_PREFIX };\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,282 @@
1
+ import { isValueDefined, isNotEmptyString } from '@dloizides/utils';
2
+
3
+ // src/fetchTenantTheme.ts
4
+ function mapColorsToMode(dto, fallback) {
5
+ if (!isValueDefined(dto)) {
6
+ return fallback;
7
+ }
8
+ return {
9
+ background: dto.background ?? fallback.background,
10
+ surface: dto.surface ?? fallback.surface,
11
+ surfaceElevated: fallback.surfaceElevated,
12
+ text: dto.onBackground ?? fallback.text,
13
+ textSecondary: dto.onSurface ?? fallback.textSecondary,
14
+ border: fallback.border,
15
+ divider: fallback.divider
16
+ };
17
+ }
18
+ function mapBrandColors(colors, defaults) {
19
+ return {
20
+ primary: colors?.primary ?? defaults.primary,
21
+ secondary: colors?.secondary ?? defaults.secondary,
22
+ accent: colors?.primaryLight ?? defaults.accent
23
+ };
24
+ }
25
+ function mapTypography(dto) {
26
+ if (!isValueDefined(dto.typography)) {
27
+ return void 0;
28
+ }
29
+ return {
30
+ fontFamily: dto.typography.fontFamily ?? void 0,
31
+ headingScale: dto.typography.headerScale ?? void 0
32
+ };
33
+ }
34
+ function toTenantThemeConfig(dto, defaultThemeConfig) {
35
+ return {
36
+ ...mapBrandColors(dto.colors, defaultThemeConfig),
37
+ semantic: {
38
+ success: defaultThemeConfig.semantic?.success,
39
+ warning: defaultThemeConfig.semantic?.warning,
40
+ error: dto.colors?.error ?? defaultThemeConfig.semantic?.error,
41
+ info: defaultThemeConfig.semantic?.info
42
+ },
43
+ light: mapColorsToMode(dto.colors, defaultThemeConfig.light),
44
+ dark: mapColorsToMode(dto.darkColors, defaultThemeConfig.dark),
45
+ typography: mapTypography(dto),
46
+ branding: {
47
+ presetId: dto.presetId ?? null,
48
+ logoContentId: dto.logoContentId ?? null,
49
+ faviconContentId: dto.faviconContentId ?? null
50
+ }
51
+ };
52
+ }
53
+ function hasThemeData(dto) {
54
+ return isValueDefined(dto.presetId) || isValueDefined(dto.colors) || isValueDefined(dto.darkColors) || isValueDefined(dto.typography);
55
+ }
56
+
57
+ // src/fetchTenantTheme.ts
58
+ var HTTP_SUCCESS_MIN = 200;
59
+ var HTTP_SUCCESS_MAX = 299;
60
+ var HTTP_NOT_MODIFIED = 304;
61
+ var HTTP_NOT_FOUND = 404;
62
+ var ETAG_HEADER = "etag";
63
+ var IF_NONE_MATCH_HEADER = "If-None-Match";
64
+ function buildRequestHeaders(cachedEtag) {
65
+ const headers = {
66
+ "Accept": "application/json"
67
+ };
68
+ if (isNotEmptyString(cachedEtag)) {
69
+ headers[IF_NONE_MATCH_HEADER] = cachedEtag;
70
+ }
71
+ return headers;
72
+ }
73
+ function extractEtag(response) {
74
+ const headerValue = response.headers[ETAG_HEADER];
75
+ if (typeof headerValue === "string" && headerValue.length > 0) {
76
+ return headerValue;
77
+ }
78
+ return null;
79
+ }
80
+ function isErrorWithResponse(value) {
81
+ if (typeof value !== "object" || !isValueDefined(value)) {
82
+ return false;
83
+ }
84
+ return "response" in value;
85
+ }
86
+ function isNotModifiedResponse(error) {
87
+ if (!isErrorWithResponse(error)) {
88
+ return false;
89
+ }
90
+ return error.response?.status === HTTP_NOT_MODIFIED;
91
+ }
92
+ function isNotFoundResponse(error) {
93
+ if (!isErrorWithResponse(error)) {
94
+ return false;
95
+ }
96
+ return error.response?.status === HTTP_NOT_FOUND;
97
+ }
98
+ function isSuccessOrNotModified(status) {
99
+ return status >= HTTP_SUCCESS_MIN && status <= HTTP_SUCCESS_MAX || status === HTTP_NOT_MODIFIED;
100
+ }
101
+ function buildNotModifiedResult(cachedEtag) {
102
+ return { themeConfig: null, etag: cachedEtag ?? null, notModified: true };
103
+ }
104
+ async function fetchTenantTheme(tenantId, options) {
105
+ const { httpGet, defaultThemeConfig, baseURL, cachedEtag, signal } = options;
106
+ const headers = buildRequestHeaders(cachedEtag);
107
+ try {
108
+ const response = await httpGet({
109
+ url: `/api/tenants/${tenantId}/theme`,
110
+ baseURL,
111
+ headers,
112
+ signal,
113
+ validateStatus: isSuccessOrNotModified
114
+ });
115
+ if (response.status === HTTP_NOT_MODIFIED) {
116
+ return buildNotModifiedResult(cachedEtag);
117
+ }
118
+ const dto = response.data;
119
+ const etag = extractEtag(response);
120
+ const hasData = isValueDefined(dto) && hasThemeData(dto);
121
+ return {
122
+ themeConfig: hasData ? toTenantThemeConfig(dto, defaultThemeConfig) : null,
123
+ etag,
124
+ notModified: false
125
+ };
126
+ } catch (error) {
127
+ if (isNotModifiedResponse(error)) {
128
+ return buildNotModifiedResult(cachedEtag);
129
+ }
130
+ if (isNotFoundResponse(error)) {
131
+ return { themeConfig: null, etag: null, notModified: false };
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ // src/saveTenantTheme.ts
138
+ function toApiThemeRequest(config) {
139
+ return {
140
+ presetId: config.branding.presetId ?? null,
141
+ colors: {
142
+ primary: config.primary,
143
+ secondary: config.secondary,
144
+ background: config.light.background,
145
+ surface: config.light.surface,
146
+ error: config.semantic?.error ?? null,
147
+ onBackground: config.light.text,
148
+ onSurface: config.light.textSecondary
149
+ },
150
+ darkColors: {
151
+ primary: config.primary,
152
+ secondary: config.secondary,
153
+ background: config.dark.background,
154
+ surface: config.dark.surface,
155
+ error: config.semantic?.error ?? null,
156
+ onBackground: config.dark.text,
157
+ onSurface: config.dark.textSecondary
158
+ },
159
+ typography: config.typography ? { fontFamily: config.typography.fontFamily ?? null, headerScale: config.typography.headingScale ?? null } : null,
160
+ logoContentId: config.branding.logoContentId ?? null,
161
+ faviconContentId: config.branding.faviconContentId ?? null
162
+ };
163
+ }
164
+ async function saveTenantTheme(tenantId, config, httpPut) {
165
+ return httpPut({
166
+ url: `/api/tenants/${tenantId}/theme`,
167
+ headers: { "Content-Type": "application/json" },
168
+ data: toApiThemeRequest(config)
169
+ });
170
+ }
171
+ var CACHE_KEY_PREFIX = "tenant-theme-";
172
+ var HOURS_PER_DAY = 24;
173
+ var MINUTES_PER_HOUR = 60;
174
+ var SECONDS_PER_MINUTE = 60;
175
+ var MS_PER_SECOND = 1e3;
176
+ var MAX_CACHE_AGE_MS = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
177
+ var warnLogger;
178
+ function configureThemeCacheLogger(logger) {
179
+ warnLogger = logger;
180
+ }
181
+ function warn(message, error) {
182
+ if (isValueDefined(warnLogger)) {
183
+ warnLogger("themeCacheStorage", message, error);
184
+ }
185
+ }
186
+ function buildCacheKey(tenantId) {
187
+ return `${CACHE_KEY_PREFIX}${tenantId}`;
188
+ }
189
+ function isStorageAvailable() {
190
+ if (typeof window === "undefined") {
191
+ return false;
192
+ }
193
+ try {
194
+ return typeof window.localStorage !== "undefined";
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ function isRecord(value) {
200
+ return isValueDefined(value) && typeof value === "object";
201
+ }
202
+ function isCachedThemeData(value) {
203
+ if (!isRecord(value)) {
204
+ return false;
205
+ }
206
+ const hasConfig = isValueDefined(value.config) && typeof value.config === "object";
207
+ const hasCachedAt = typeof value.cachedAt === "number";
208
+ return hasConfig && hasCachedAt;
209
+ }
210
+ function readThemeCache(tenantId) {
211
+ if (!isStorageAvailable()) {
212
+ return null;
213
+ }
214
+ try {
215
+ const raw = localStorage.getItem(buildCacheKey(tenantId));
216
+ if (!isValueDefined(raw)) {
217
+ return null;
218
+ }
219
+ const parsed = JSON.parse(raw);
220
+ if (!isCachedThemeData(parsed)) {
221
+ return null;
222
+ }
223
+ const age = Date.now() - parsed.cachedAt;
224
+ if (age > MAX_CACHE_AGE_MS) {
225
+ return null;
226
+ }
227
+ return parsed;
228
+ } catch (error) {
229
+ warn("Failed to read theme cache", error);
230
+ return null;
231
+ }
232
+ }
233
+ function writeThemeCache(tenantId, config, etag) {
234
+ if (!isStorageAvailable()) {
235
+ return;
236
+ }
237
+ const data = {
238
+ config,
239
+ logoUrl: config.branding.logoContentId,
240
+ faviconUrl: config.branding.faviconContentId,
241
+ etag,
242
+ cachedAt: Date.now()
243
+ };
244
+ try {
245
+ localStorage.setItem(buildCacheKey(tenantId), JSON.stringify(data));
246
+ } catch (error) {
247
+ warn("Failed to write theme cache", error);
248
+ }
249
+ }
250
+ function clearThemeCache(tenantId) {
251
+ if (!isStorageAvailable()) {
252
+ return;
253
+ }
254
+ try {
255
+ localStorage.removeItem(buildCacheKey(tenantId));
256
+ } catch (error) {
257
+ warn("Failed to clear theme cache", error);
258
+ }
259
+ }
260
+ function clearAllThemeCaches() {
261
+ if (!isStorageAvailable()) {
262
+ return;
263
+ }
264
+ try {
265
+ const keysToRemove = [];
266
+ for (let i = 0; i < localStorage.length; i++) {
267
+ const key = localStorage.key(i);
268
+ if (isValueDefined(key) && key.startsWith(CACHE_KEY_PREFIX)) {
269
+ keysToRemove.push(key);
270
+ }
271
+ }
272
+ for (const key of keysToRemove) {
273
+ localStorage.removeItem(key);
274
+ }
275
+ } catch (error) {
276
+ warn("Failed to clear all theme caches", error);
277
+ }
278
+ }
279
+
280
+ export { CACHE_KEY_PREFIX, MAX_CACHE_AGE_MS, clearAllThemeCaches, clearThemeCache, configureThemeCacheLogger, fetchTenantTheme, hasThemeData, readThemeCache, saveTenantTheme, toApiThemeRequest, toTenantThemeConfig, writeThemeCache };
281
+ //# sourceMappingURL=index.mjs.map
282
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mapToThemeConfig.ts","../src/fetchTenantTheme.ts","../src/saveTenantTheme.ts","../src/themeCacheStorage.ts"],"names":["isValueDefined"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Maps the flat API DTO returned by GET /api/tenants/{tenantId}/theme into the\n * frontend {@link TenantThemeConfig} shape.\n *\n * The fallback values (`defaultThemeConfig`) are **app-supplied** so the package\n * stays palette-agnostic: each product passes its own DEFAULT_THEME_CONFIG.\n */\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type {\n ApiThemeColorsDto,\n ApiThemeResponseDto,\n TenantThemeConfig,\n ThemeModeColors,\n} from './types';\n\nfunction mapColorsToMode(\n dto: ApiThemeColorsDto | null | undefined,\n fallback: ThemeModeColors,\n): ThemeModeColors {\n if (!isValueDefined(dto)) {\n return fallback;\n }\n return {\n background: dto.background ?? fallback.background,\n surface: dto.surface ?? fallback.surface,\n surfaceElevated: fallback.surfaceElevated,\n text: dto.onBackground ?? fallback.text,\n textSecondary: dto.onSurface ?? fallback.textSecondary,\n border: fallback.border,\n divider: fallback.divider,\n };\n}\n\nfunction mapBrandColors(\n colors: ApiThemeColorsDto | null | undefined,\n defaults: TenantThemeConfig,\n): Pick<TenantThemeConfig, 'primary' | 'secondary' | 'accent'> {\n return {\n primary: colors?.primary ?? defaults.primary,\n secondary: colors?.secondary ?? defaults.secondary,\n accent: colors?.primaryLight ?? defaults.accent,\n };\n}\n\nfunction mapTypography(dto: ApiThemeResponseDto): TenantThemeConfig['typography'] {\n if (!isValueDefined(dto.typography)) {\n return undefined;\n }\n return {\n fontFamily: dto.typography.fontFamily ?? undefined,\n headingScale: dto.typography.headerScale ?? undefined,\n };\n}\n\n/**\n * Transforms a raw API DTO into a {@link TenantThemeConfig}, filling any missing\n * field from the app-supplied `defaultThemeConfig`.\n */\nexport function toTenantThemeConfig(\n dto: ApiThemeResponseDto,\n defaultThemeConfig: TenantThemeConfig,\n): TenantThemeConfig {\n return {\n ...mapBrandColors(dto.colors, defaultThemeConfig),\n semantic: {\n success: defaultThemeConfig.semantic?.success,\n warning: defaultThemeConfig.semantic?.warning,\n error: dto.colors?.error ?? defaultThemeConfig.semantic?.error,\n info: defaultThemeConfig.semantic?.info,\n },\n light: mapColorsToMode(dto.colors, defaultThemeConfig.light),\n dark: mapColorsToMode(dto.darkColors, defaultThemeConfig.dark),\n typography: mapTypography(dto),\n branding: {\n presetId: dto.presetId ?? null,\n logoContentId: dto.logoContentId ?? null,\n faviconContentId: dto.faviconContentId ?? null,\n },\n };\n}\n\n/** True when the DTO carries any theme payload worth mapping. */\nexport function hasThemeData(dto: ApiThemeResponseDto): boolean {\n return (\n isValueDefined(dto.presetId) ||\n isValueDefined(dto.colors) ||\n isValueDefined(dto.darkColors) ||\n isValueDefined(dto.typography)\n );\n}\n","/**\n * ETag-conditional tenant-theme fetch.\n *\n * The API returns a flat DTO shape (presetId, colors, darkColors, typography);\n * this transforms it into the frontend {@link TenantThemeConfig} format.\n *\n * Supports ETag-based conditional requests:\n * - Sends `If-None-Match` when a cached ETag is available\n * - Returns the response ETag for caching\n * - Returns `notModified: true` on 304 so the caller keeps using cached data\n *\n * Endpoint: GET {baseURL}/api/tenants/{tenantId}/theme\n *\n * The HTTP transport (`httpGet`), the fallback palette (`defaultThemeConfig`),\n * and the API base URL (`baseURL`) are all app-supplied ports.\n */\nimport { isNotEmptyString, isValueDefined } from '@dloizides/utils';\n\nimport { hasThemeData, toTenantThemeConfig } from './mapToThemeConfig';\n\nimport type {\n ApiThemeResponseDto,\n HttpGet,\n HttpResponse,\n TenantThemeConfig,\n} from './types';\n\n// -- Constants ----------------------------------------------------------------\n\n/** HTTP status range for successful responses (inclusive). */\nconst HTTP_SUCCESS_MIN = 200;\nconst HTTP_SUCCESS_MAX = 299;\n\n/** HTTP 304 status code for Not Modified responses. */\nconst HTTP_NOT_MODIFIED = 304;\n\n/** HTTP 404 status code for Not Found responses. */\nconst HTTP_NOT_FOUND = 404;\n\nconst ETAG_HEADER = 'etag';\nconst IF_NONE_MATCH_HEADER = 'If-None-Match';\n\n// -- Options + result types ---------------------------------------------------\n\n/** Options for {@link fetchTenantTheme}. */\nexport interface FetchTenantThemeOptions {\n signal?: AbortSignal;\n /** Cached ETag to send as the If-None-Match header. */\n cachedEtag?: string;\n /** App-supplied GET transport (wire to the app's apiClient/bff-web-client). */\n httpGet: HttpGet;\n /** App-supplied fallback palette (the product's DEFAULT_THEME_CONFIG). */\n defaultThemeConfig: TenantThemeConfig;\n /** Optional API base URL. Omit to use the transport's own base. */\n baseURL?: string;\n}\n\n/** Frontend-facing response shape consumed by the theme hook. */\nexport interface TenantThemeResponse {\n themeConfig: TenantThemeConfig | null;\n etag: string | null;\n /** True when the server returned 304 Not Modified (use cached data). */\n notModified: boolean;\n}\n\n// -- HTTP helpers -------------------------------------------------------------\n\nfunction buildRequestHeaders(cachedEtag: string | undefined): Record<string, string> {\n const headers: Record<string, string> = {\n 'Accept': 'application/json',\n };\n if (isNotEmptyString(cachedEtag)) {\n headers[IF_NONE_MATCH_HEADER] = cachedEtag;\n }\n return headers;\n}\n\nfunction extractEtag(response: HttpResponse): string | null {\n const headerValue: unknown = response.headers[ETAG_HEADER];\n if (typeof headerValue === 'string' && headerValue.length > 0) {\n return headerValue;\n }\n return null;\n}\n\ninterface ErrorWithResponse {\n response?: { status?: unknown };\n}\n\nfunction isErrorWithResponse(value: unknown): value is ErrorWithResponse {\n if (typeof value !== 'object' || !isValueDefined(value)) {\n return false;\n }\n return 'response' in value;\n}\n\nfunction isNotModifiedResponse(error: unknown): boolean {\n if (!isErrorWithResponse(error)) {\n return false;\n }\n return error.response?.status === HTTP_NOT_MODIFIED;\n}\n\nfunction isNotFoundResponse(error: unknown): boolean {\n if (!isErrorWithResponse(error)) {\n return false;\n }\n return error.response?.status === HTTP_NOT_FOUND;\n}\n\nfunction isSuccessOrNotModified(status: number): boolean {\n return (status >= HTTP_SUCCESS_MIN && status <= HTTP_SUCCESS_MAX) || status === HTTP_NOT_MODIFIED;\n}\n\nfunction buildNotModifiedResult(cachedEtag: string | undefined): TenantThemeResponse {\n return { themeConfig: null, etag: cachedEtag ?? null, notModified: true };\n}\n\n// -- Public API ---------------------------------------------------------------\n\n/**\n * Fetch the tenant theme configuration via the app-supplied transport and\n * transform the API DTO into {@link TenantThemeConfig}. Returns a null config\n * when the tenant has no theme configured (200 empty DTO or 404).\n *\n * Supports ETag-based conditional requests:\n * - Pass `cachedEtag` to send the If-None-Match header\n * - On 304 Not Modified, returns `{ notModified: true }` so the caller uses\n * its cached data\n */\nexport async function fetchTenantTheme(\n tenantId: string,\n options: FetchTenantThemeOptions,\n): Promise<TenantThemeResponse> {\n const { httpGet, defaultThemeConfig, baseURL, cachedEtag, signal } = options;\n const headers = buildRequestHeaders(cachedEtag);\n\n try {\n const response = await httpGet<ApiThemeResponseDto>({\n url: `/api/tenants/${tenantId}/theme`,\n baseURL,\n headers,\n signal,\n validateStatus: isSuccessOrNotModified,\n });\n\n if (response.status === HTTP_NOT_MODIFIED) {\n return buildNotModifiedResult(cachedEtag);\n }\n\n const dto = response.data;\n const etag = extractEtag(response);\n const hasData = isValueDefined(dto) && hasThemeData(dto);\n\n return {\n themeConfig: hasData ? toTenantThemeConfig(dto, defaultThemeConfig) : null,\n etag,\n notModified: false,\n };\n } catch (error: unknown) {\n if (isNotModifiedResponse(error)) {\n return buildNotModifiedResult(cachedEtag);\n }\n if (isNotFoundResponse(error)) {\n return { themeConfig: null, etag: null, notModified: false };\n }\n throw error;\n }\n}\n","/**\n * Tenant-theme save (PUT) logic.\n *\n * Maps a {@link TenantThemeConfig} into the API's UpdateTenantThemeRequest body\n * and PUTs it via the app-supplied {@link HttpPut} transport.\n *\n * Endpoint: PUT /api/tenants/{tenantId}/theme\n */\nimport type {\n ApiThemeRequest,\n HttpPut,\n TenantThemeConfig,\n} from './types';\n\n/** Response shape from PUT /api/tenants/{tenantId}/theme */\nexport interface SaveThemeResponse {\n success: boolean;\n}\n\n/** Maps a frontend {@link TenantThemeConfig} to the API's request body. */\nexport function toApiThemeRequest(config: TenantThemeConfig): ApiThemeRequest {\n return {\n presetId: config.branding.presetId ?? null,\n colors: {\n primary: config.primary,\n secondary: config.secondary,\n background: config.light.background,\n surface: config.light.surface,\n error: config.semantic?.error ?? null,\n onBackground: config.light.text,\n onSurface: config.light.textSecondary,\n },\n darkColors: {\n primary: config.primary,\n secondary: config.secondary,\n background: config.dark.background,\n surface: config.dark.surface,\n error: config.semantic?.error ?? null,\n onBackground: config.dark.text,\n onSurface: config.dark.textSecondary,\n },\n typography: config.typography\n ? { fontFamily: config.typography.fontFamily ?? null, headerScale: config.typography.headingScale ?? null }\n : null,\n logoContentId: config.branding.logoContentId ?? null,\n faviconContentId: config.branding.faviconContentId ?? null,\n };\n}\n\n/**\n * Save the tenant theme via the app-supplied PUT transport.\n */\nexport async function saveTenantTheme(\n tenantId: string,\n config: TenantThemeConfig,\n httpPut: HttpPut,\n): Promise<SaveThemeResponse> {\n return httpPut<SaveThemeResponse, ApiThemeRequest>({\n url: `/api/tenants/${tenantId}/theme`,\n headers: { 'Content-Type': 'application/json' },\n data: toApiThemeRequest(config),\n });\n}\n","/**\n * localStorage cache utilities for tenant theme configuration.\n *\n * Provides read/write/clear operations with automatic expiry. All operations\n * are safe (no-throw) and return null on failure.\n *\n * The package emits no `console` calls; supply a logger via\n * {@link configureThemeCacheLogger} to capture cache-failure warnings (the app\n * wires its own logging service). When unset, failures are swallowed silently.\n */\nimport { isValueDefined } from '@dloizides/utils';\n\nimport type { CachedThemeData, TenantThemeConfig } from './types';\n\nconst CACHE_KEY_PREFIX = 'tenant-theme-';\n\n/** Maximum cache age: 24 hours in milliseconds */\nconst HOURS_PER_DAY = 24;\nconst MINUTES_PER_HOUR = 60;\nconst SECONDS_PER_MINUTE = 60;\nconst MS_PER_SECOND = 1000;\nconst MAX_CACHE_AGE_MS =\n HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;\n\n/** Minimal warn-only logger contract for cache failures. */\nexport type ThemeCacheWarn = (context: string, message: string, error?: unknown) => void;\n\nlet warnLogger: ThemeCacheWarn | undefined;\n\n/**\n * Supply a warn logger the cache calls when a localStorage operation fails.\n * Pass `undefined` to revert to silent (no-op) behavior.\n */\nexport function configureThemeCacheLogger(logger: ThemeCacheWarn | undefined): void {\n warnLogger = logger;\n}\n\nfunction warn(message: string, error: unknown): void {\n if (isValueDefined(warnLogger)) {\n warnLogger('themeCacheStorage', message, error);\n }\n}\n\nfunction buildCacheKey(tenantId: string): string {\n return `${CACHE_KEY_PREFIX}${tenantId}`;\n}\n\nfunction isStorageAvailable(): boolean {\n if (typeof window === 'undefined') {\n return false;\n }\n try {\n return typeof window.localStorage !== 'undefined';\n } catch {\n return false;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return isValueDefined(value) && typeof value === 'object';\n}\n\nfunction isCachedThemeData(value: unknown): value is CachedThemeData {\n if (!isRecord(value)) {\n return false;\n }\n const hasConfig = isValueDefined(value.config) && typeof value.config === 'object';\n const hasCachedAt = typeof value.cachedAt === 'number';\n return hasConfig && hasCachedAt;\n}\n\n/**\n * Read cached theme data from localStorage.\n * Returns null if cache is missing, expired, or corrupt.\n */\nexport function readThemeCache(tenantId: string): CachedThemeData | null {\n if (!isStorageAvailable()) {\n return null;\n }\n\n try {\n const raw = localStorage.getItem(buildCacheKey(tenantId));\n if (!isValueDefined(raw)) {\n return null;\n }\n\n const parsed: unknown = JSON.parse(raw);\n if (!isCachedThemeData(parsed)) {\n return null;\n }\n\n const age = Date.now() - parsed.cachedAt;\n if (age > MAX_CACHE_AGE_MS) {\n return null;\n }\n\n return parsed;\n } catch (error) {\n warn('Failed to read theme cache', error);\n return null;\n }\n}\n\n/**\n * Write theme data to localStorage cache.\n */\nexport function writeThemeCache(tenantId: string, config: TenantThemeConfig, etag: string): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n const data: CachedThemeData = {\n config,\n logoUrl: config.branding.logoContentId,\n faviconUrl: config.branding.faviconContentId,\n etag,\n cachedAt: Date.now(),\n };\n\n try {\n localStorage.setItem(buildCacheKey(tenantId), JSON.stringify(data));\n } catch (error) {\n warn('Failed to write theme cache', error);\n }\n}\n\n/**\n * Clear cached theme data for a specific tenant.\n */\nexport function clearThemeCache(tenantId: string): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n try {\n localStorage.removeItem(buildCacheKey(tenantId));\n } catch (error) {\n warn('Failed to clear theme cache', error);\n }\n}\n\n/**\n * Clear all tenant theme caches from localStorage.\n * Used during logout to remove any tenant-specific data.\n */\nexport function clearAllThemeCaches(): void {\n if (!isStorageAvailable()) {\n return;\n }\n\n try {\n const keysToRemove: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (isValueDefined(key) && key.startsWith(CACHE_KEY_PREFIX)) {\n keysToRemove.push(key);\n }\n }\n for (const key of keysToRemove) {\n localStorage.removeItem(key);\n }\n } catch (error) {\n warn('Failed to clear all theme caches', error);\n }\n}\n\nexport { MAX_CACHE_AGE_MS, CACHE_KEY_PREFIX };\n"]}