@meshmakers/octo-ui 3.3.910 → 3.3.930

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,1026 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Injectable, inject, signal, Injector, effect, makeEnvironmentProviders, DOCUMENT, RendererFactory2, input, computed, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { TitleStrategy } from '@angular/router';
4
+ import { Title } from '@angular/platform-browser';
5
+ import { firstValueFrom } from 'rxjs';
6
+ import * as i1 from 'apollo-angular';
7
+ import { gql } from 'apollo-angular';
8
+ import * as i1$1 from '@progress/kendo-angular-icons';
9
+ import { SVGIconModule } from '@progress/kendo-angular-icons';
10
+ import { lightbulbOutlineIcon, brightnessContrastIcon } from '@progress/kendo-svg-icons';
11
+
12
+ const OCTO_BRANDING_DEFAULTS = new InjectionToken('OCTO_BRANDING_DEFAULTS');
13
+ const OCTO_BRANDING_FALLBACK_ASSETS = new InjectionToken('OCTO_BRANDING_FALLBACK_ASSETS');
14
+ // Library defaults carry the Meshmakers brand mint (#65ceaf) as primary so
15
+ // an app that opts into `provideOctoBranding` but never persists a
16
+ // `SystemUIBranding` record renders with the publisher's identity rather
17
+ // than vanilla Kendo. Mint is also non-aggressive in mixed contexts (no
18
+ // alarm/error semantics like Kendo's tomato default), so tenant overrides
19
+ // almost always read as an intentional rebrand rather than a workaround.
20
+ // Apps override per-tenant via Settings page; the override completely
21
+ // replaces these baselines.
22
+ const NEUTRAL_LIGHT_THEME = {
23
+ primaryColor: '#65ceaf',
24
+ secondaryColor: '#5ac4be',
25
+ tertiaryColor: '#0b5c92',
26
+ neutralColor: '#6c757d',
27
+ backgroundColor: '#ffffff',
28
+ headerGradient: { startColor: '#ffffff', endColor: '#f8f9fa' },
29
+ footerGradient: { startColor: '#65ceaf', endColor: '#5ac4be' },
30
+ };
31
+ const NEUTRAL_DARK_THEME = {
32
+ primaryColor: '#65ceaf',
33
+ secondaryColor: '#5ac4be',
34
+ tertiaryColor: '#4a8eef',
35
+ neutralColor: '#94a3b8',
36
+ backgroundColor: '#1a1d20',
37
+ headerGradient: { startColor: '#1a1d20', endColor: '#2b2f33' },
38
+ footerGradient: { startColor: '#65ceaf', endColor: '#5ac4be' },
39
+ };
40
+ /**
41
+ * Library-shipped neutral fallback. Apps that don't supply their own defaults
42
+ * see a working but visually-neutral baseline ("configure me"), not a crash.
43
+ */
44
+ const NEUTRAL_BRANDING_DEFAULTS = {
45
+ rtId: null,
46
+ appName: 'App',
47
+ appTitle: 'App',
48
+ headerLogoUrl: null,
49
+ footerLogoUrl: null,
50
+ faviconUrl: null,
51
+ lightTheme: NEUTRAL_LIGHT_THEME,
52
+ darkTheme: NEUTRAL_DARK_THEME,
53
+ };
54
+ const NEUTRAL_FALLBACK_ASSETS = {};
55
+
56
+ const CreateBrandingDocumentDto = gql `
57
+ mutation createBranding($branding: SystemUIBrandingInput!) {
58
+ runtime {
59
+ systemUIBrandings {
60
+ create(entities: [$branding]) {
61
+ rtId
62
+ rtWellKnownName
63
+ appName
64
+ appTitle
65
+ headerLogo
66
+ footerLogo
67
+ favicon
68
+ lightTheme {
69
+ primaryColor
70
+ secondaryColor
71
+ tertiaryColor
72
+ neutralColor
73
+ backgroundColor
74
+ headerGradient {
75
+ startColor
76
+ endColor
77
+ }
78
+ footerGradient {
79
+ startColor
80
+ endColor
81
+ }
82
+ }
83
+ darkTheme {
84
+ primaryColor
85
+ secondaryColor
86
+ tertiaryColor
87
+ neutralColor
88
+ backgroundColor
89
+ headerGradient {
90
+ startColor
91
+ endColor
92
+ }
93
+ footerGradient {
94
+ startColor
95
+ endColor
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ `;
103
+ class CreateBrandingDtoGQL extends i1.Mutation {
104
+ document = CreateBrandingDocumentDto;
105
+ constructor(apollo) {
106
+ super(apollo);
107
+ }
108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CreateBrandingDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
109
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CreateBrandingDtoGQL, providedIn: 'root' });
110
+ }
111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: CreateBrandingDtoGQL, decorators: [{
112
+ type: Injectable,
113
+ args: [{
114
+ providedIn: 'root'
115
+ }]
116
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
117
+
118
+ const GetBrandingDocumentDto = gql `
119
+ query getBranding {
120
+ runtime {
121
+ systemUIBranding(
122
+ first: 1
123
+ fieldFilter: [{attributePath: "rtWellKnownName", operator: EQUALS, comparisonValue: "Branding"}]
124
+ ) {
125
+ totalCount
126
+ items {
127
+ rtId
128
+ rtWellKnownName
129
+ appName
130
+ appTitle
131
+ headerLogo
132
+ footerLogo
133
+ favicon
134
+ lightTheme {
135
+ primaryColor
136
+ secondaryColor
137
+ tertiaryColor
138
+ neutralColor
139
+ backgroundColor
140
+ headerGradient {
141
+ startColor
142
+ endColor
143
+ }
144
+ footerGradient {
145
+ startColor
146
+ endColor
147
+ }
148
+ }
149
+ darkTheme {
150
+ primaryColor
151
+ secondaryColor
152
+ tertiaryColor
153
+ neutralColor
154
+ backgroundColor
155
+ headerGradient {
156
+ startColor
157
+ endColor
158
+ }
159
+ footerGradient {
160
+ startColor
161
+ endColor
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
167
+ }
168
+ `;
169
+ class GetBrandingDtoGQL extends i1.Query {
170
+ document = GetBrandingDocumentDto;
171
+ constructor(apollo) {
172
+ super(apollo);
173
+ }
174
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetBrandingDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
175
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetBrandingDtoGQL, providedIn: 'root' });
176
+ }
177
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GetBrandingDtoGQL, decorators: [{
178
+ type: Injectable,
179
+ args: [{
180
+ providedIn: 'root'
181
+ }]
182
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
183
+
184
+ const UpdateBrandingDocumentDto = gql `
185
+ mutation updateBranding($branding: SystemUIBrandingInputUpdate!) {
186
+ runtime {
187
+ systemUIBrandings {
188
+ update(entities: [$branding]) {
189
+ rtId
190
+ rtWellKnownName
191
+ appName
192
+ appTitle
193
+ headerLogo
194
+ footerLogo
195
+ favicon
196
+ lightTheme {
197
+ primaryColor
198
+ secondaryColor
199
+ tertiaryColor
200
+ neutralColor
201
+ backgroundColor
202
+ headerGradient {
203
+ startColor
204
+ endColor
205
+ }
206
+ footerGradient {
207
+ startColor
208
+ endColor
209
+ }
210
+ }
211
+ darkTheme {
212
+ primaryColor
213
+ secondaryColor
214
+ tertiaryColor
215
+ neutralColor
216
+ backgroundColor
217
+ headerGradient {
218
+ startColor
219
+ endColor
220
+ }
221
+ footerGradient {
222
+ startColor
223
+ endColor
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ `;
231
+ class UpdateBrandingDtoGQL extends i1.Mutation {
232
+ document = UpdateBrandingDocumentDto;
233
+ constructor(apollo) {
234
+ super(apollo);
235
+ }
236
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UpdateBrandingDtoGQL, deps: [{ token: i1.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
237
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UpdateBrandingDtoGQL, providedIn: 'root' });
238
+ }
239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: UpdateBrandingDtoGQL, decorators: [{
240
+ type: Injectable,
241
+ args: [{
242
+ providedIn: 'root'
243
+ }]
244
+ }], ctorParameters: () => [{ type: i1.Apollo }] });
245
+
246
+ const WELL_KNOWN_NAME = 'Branding';
247
+ /**
248
+ * Per-tenant branding. Source of truth is the `SystemUIBranding`
249
+ * CK runtime entity (one record per tenant, `rtWellKnownName = 'Branding'`).
250
+ * Loaded via GraphQL, mirrored into a signal for consumers.
251
+ */
252
+ class BrandingDataSource {
253
+ getBrandingGQL = inject(GetBrandingDtoGQL);
254
+ createBrandingGQL = inject(CreateBrandingDtoGQL);
255
+ updateBrandingGQL = inject(UpdateBrandingDtoGQL);
256
+ defaults = inject(OCTO_BRANDING_DEFAULTS);
257
+ state = signal({ ...this.defaults }, ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
258
+ branding = this.state.asReadonly();
259
+ async load() {
260
+ // `no-cache` (not `network-only`): Apollo's `InMemoryCache` normalizes
261
+ // entities by `rtId`, but the singleton `RuntimeModelQuery` type has no
262
+ // id — cache writes for `runtime.systemUIBranding`
263
+ // clobber sibling fields (e.g. `runtime.basicTree` used by plant
264
+ // hierarchy + /location). `no-cache` fetches from network AND skips
265
+ // writing to cache, so /location keeps its hierarchy cache intact.
266
+ const result = await firstValueFrom(this.getBrandingGQL.fetch({ fetchPolicy: 'no-cache' }));
267
+ const item = result.data?.runtime?.systemUIBranding?.items?.[0];
268
+ if (!item) {
269
+ this.state.set({ ...this.defaults });
270
+ return;
271
+ }
272
+ this.state.set(await this.mapFromServer(item));
273
+ }
274
+ async save(update) {
275
+ const current = this.state();
276
+ const input = await this.buildInput(update);
277
+ const saved = current.rtId
278
+ ? await this.runUpdate(current.rtId, input)
279
+ : await this.runCreate(input);
280
+ const mapped = await this.mapFromServer(saved);
281
+ this.state.set(mapped);
282
+ return mapped;
283
+ }
284
+ async resetToDefaults() {
285
+ await this.save({
286
+ appName: this.defaults.appName,
287
+ appTitle: this.defaults.appTitle,
288
+ headerLogoFile: null,
289
+ footerLogoFile: null,
290
+ faviconFile: null,
291
+ lightTheme: this.defaults.lightTheme,
292
+ darkTheme: this.defaults.darkTheme,
293
+ });
294
+ }
295
+ async buildInput(update) {
296
+ const [headerLogo, footerLogo, favicon] = await Promise.all([
297
+ this.prepareBinary(update.headerLogoFile),
298
+ this.prepareBinary(update.footerLogoFile),
299
+ this.prepareBinary(update.faviconFile),
300
+ ]);
301
+ const input = {
302
+ rtWellKnownName: WELL_KNOWN_NAME,
303
+ appName: update.appName,
304
+ appTitle: update.appTitle,
305
+ lightTheme: this.paletteToInput(update.lightTheme),
306
+ darkTheme: update.darkTheme ? this.paletteToInput(update.darkTheme) : null,
307
+ };
308
+ // Tri-state: set the field only on replace (bytes) or clear (null);
309
+ // omit when undefined so the server keeps the current blob.
310
+ if (headerLogo !== undefined)
311
+ input.headerLogo = headerLogo;
312
+ if (footerLogo !== undefined)
313
+ input.footerLogo = footerLogo;
314
+ if (favicon !== undefined)
315
+ input.favicon = favicon;
316
+ return input;
317
+ }
318
+ async runCreate(input) {
319
+ const result = await firstValueFrom(this.createBrandingGQL.mutate({
320
+ variables: { branding: input },
321
+ fetchPolicy: 'no-cache',
322
+ }));
323
+ const item = result.data?.runtime?.systemUIBrandings?.create?.[0];
324
+ if (!item) {
325
+ throw new Error('createBranding returned no entity');
326
+ }
327
+ return item;
328
+ }
329
+ async runUpdate(rtId, input) {
330
+ const result = await firstValueFrom(this.updateBrandingGQL.mutate({
331
+ variables: { branding: { rtId, item: input } },
332
+ fetchPolicy: 'no-cache',
333
+ }));
334
+ const item = result.data?.runtime?.systemUIBrandings?.update?.[0];
335
+ if (!item) {
336
+ throw new Error('updateBranding returned no entity');
337
+ }
338
+ return item;
339
+ }
340
+ paletteToInput(palette) {
341
+ return {
342
+ primaryColor: palette.primaryColor,
343
+ secondaryColor: palette.secondaryColor,
344
+ tertiaryColor: palette.tertiaryColor,
345
+ neutralColor: palette.neutralColor,
346
+ backgroundColor: palette.backgroundColor,
347
+ headerGradient: {
348
+ startColor: palette.headerGradient.startColor,
349
+ endColor: palette.headerGradient.endColor,
350
+ },
351
+ footerGradient: {
352
+ startColor: palette.footerGradient.startColor,
353
+ endColor: palette.footerGradient.endColor,
354
+ },
355
+ };
356
+ }
357
+ async mapFromServer(item) {
358
+ // `Binary` attribute in the ckType surfaces as GraphQL `[Byte]` (raw byte
359
+ // array scalar). Client converts to a data URL locally for <img src>.
360
+ // No second HTTP round-trip like the `BinaryLinked`/`LargeBinaryInfo`
361
+ // pattern (which would return a downloadUri to fetch).
362
+ const [headerLogoUrl, footerLogoUrl, faviconUrl] = await Promise.all([
363
+ this.bytesToDataUrl(item.headerLogo),
364
+ this.bytesToDataUrl(item.footerLogo),
365
+ this.bytesToDataUrl(item.favicon),
366
+ ]);
367
+ return {
368
+ rtId: item.rtId,
369
+ appName: item.appName ?? this.defaults.appName,
370
+ appTitle: item.appTitle ?? this.defaults.appTitle,
371
+ headerLogoUrl: headerLogoUrl,
372
+ footerLogoUrl: footerLogoUrl,
373
+ faviconUrl: faviconUrl,
374
+ lightTheme: this.paletteFromServer(item.lightTheme, this.defaults.lightTheme) ??
375
+ this.defaults.lightTheme,
376
+ darkTheme: this.paletteFromServer(item.darkTheme, this.defaults.darkTheme ?? this.defaults.lightTheme),
377
+ };
378
+ }
379
+ // Defaults are passed in so light/dark palettes fall back to their own
380
+ // defaults instead of always using lightTheme defaults (matters when only
381
+ // partial dark palette comes from server).
382
+ paletteFromServer(palette, defaults) {
383
+ if (!palette)
384
+ return null;
385
+ const { headerGradient, footerGradient } = palette;
386
+ return {
387
+ primaryColor: palette.primaryColor ?? defaults.primaryColor,
388
+ secondaryColor: palette.secondaryColor ?? defaults.secondaryColor,
389
+ tertiaryColor: palette.tertiaryColor ?? defaults.tertiaryColor,
390
+ neutralColor: palette.neutralColor ?? defaults.neutralColor,
391
+ backgroundColor: palette.backgroundColor ?? defaults.backgroundColor,
392
+ headerGradient: headerGradient
393
+ ? { startColor: headerGradient.startColor, endColor: headerGradient.endColor }
394
+ : defaults.headerGradient,
395
+ footerGradient: footerGradient
396
+ ? { startColor: footerGradient.startColor, endColor: footerGradient.endColor }
397
+ : defaults.footerGradient,
398
+ };
399
+ }
400
+ /**
401
+ * Convert a byte array from a GraphQL `[Byte]` field into a data URL.
402
+ * Returns null for null/undefined/empty input. MIME type is detected from
403
+ * the first few bytes (magic numbers) so <img src> works without a
404
+ * server-provided content-type.
405
+ */
406
+ async bytesToDataUrl(bytes) {
407
+ if (!bytes || bytes.length === 0)
408
+ return null;
409
+ // Codegen types `[Byte]` entries as `number | null`; strip nulls.
410
+ const clean = bytes.filter((b) => typeof b === 'number');
411
+ if (clean.length === 0)
412
+ return null;
413
+ const uint8 = new Uint8Array(clean);
414
+ const mime = this.detectImageMime(uint8) ?? 'application/octet-stream';
415
+ try {
416
+ return await this.blobToDataUrl(new Blob([uint8], { type: mime }));
417
+ }
418
+ catch (error) {
419
+ console.warn('[BrandingDataSource] Failed to decode image bytes', error);
420
+ return null;
421
+ }
422
+ }
423
+ detectImageMime(bytes) {
424
+ if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)
425
+ return 'image/png';
426
+ if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff)
427
+ return 'image/jpeg';
428
+ if (bytes.length >= 6 && bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46)
429
+ return 'image/gif';
430
+ if (bytes.length >= 12 && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50)
431
+ return 'image/webp';
432
+ if (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00)
433
+ return 'image/vnd.microsoft.icon';
434
+ if (bytes.length >= 5 && bytes[0] === 0x3c && bytes[1] === 0x3f && bytes[2] === 0x78 && bytes[3] === 0x6d && bytes[4] === 0x6c)
435
+ return 'image/svg+xml';
436
+ // UTF-8 BOM-less SVG starts with '<svg'
437
+ if (bytes.length >= 4 && bytes[0] === 0x3c && bytes[1] === 0x73 && bytes[2] === 0x76 && bytes[3] === 0x67)
438
+ return 'image/svg+xml';
439
+ return null;
440
+ }
441
+ blobToDataUrl(blob) {
442
+ return new Promise((resolve, reject) => {
443
+ const reader = new FileReader();
444
+ reader.onload = () => {
445
+ const result = reader.result;
446
+ if (typeof result === 'string') {
447
+ resolve(result);
448
+ }
449
+ else {
450
+ reject(new Error('FileReader returned non-string result'));
451
+ }
452
+ };
453
+ reader.onerror = () => reject(reader.error ?? new Error('FileReader failed'));
454
+ reader.readAsDataURL(blob);
455
+ });
456
+ }
457
+ /**
458
+ * Three-value binary resolution for save():
459
+ * - `null` → caller wants the asset cleared; forward null to the server.
460
+ * - `undefined` → caller left the slot untouched; omit the field so the
461
+ * server keeps the existing blob.
462
+ * - `File` → caller uploaded a new asset; serialize to byte array.
463
+ */
464
+ async prepareBinary(file) {
465
+ if (file === null)
466
+ return null;
467
+ if (file === undefined)
468
+ return undefined;
469
+ const buffer = await file.arrayBuffer();
470
+ return Array.from(new Uint8Array(buffer));
471
+ }
472
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingDataSource, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
473
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingDataSource, providedIn: 'root' });
474
+ }
475
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingDataSource, decorators: [{
476
+ type: Injectable,
477
+ args: [{ providedIn: 'root' }]
478
+ }] });
479
+
480
+ const OCTO_TITLE_TRANSLATOR = new InjectionToken('OCTO_TITLE_TRANSLATOR');
481
+ /**
482
+ * `TitleStrategy` that composes `<branding.appTitle> | <route.breadcrumb>`
483
+ * into `document.title` on every navigation, and re-applies on
484
+ * `BrandingDataSource.branding()` changes (e.g. after Settings save).
485
+ *
486
+ * Wire as the application's `TitleStrategy`:
487
+ * ```ts
488
+ * { provide: TitleStrategy, useExisting: AppTitleService }
489
+ * ```
490
+ *
491
+ * Optional translation of the breadcrumb key is supplied through
492
+ * {@link OCTO_TITLE_TRANSLATOR}; without it the key from `route.data` is used
493
+ * as-is.
494
+ *
495
+ * `BrandingDataSource` is resolved lazily via `injector.get()` rather than
496
+ * field-injected. The router constructs `TitleStrategy` eagerly during
497
+ * `APP_INITIALIZER`, before the host's tenant config has loaded. A direct
498
+ * field-inject of `BrandingDataSource` here would chain into Apollo's
499
+ * factory, which reads `tenantId` while it is still `undefined` and bakes
500
+ * `/tenants/undefined/GraphQL` into the request URI. Lazy resolution defers
501
+ * the chain to the first navigation, by which time config is loaded.
502
+ */
503
+ class AppTitleService extends TitleStrategy {
504
+ title = inject(Title);
505
+ translator = inject(OCTO_TITLE_TRANSLATOR, {
506
+ optional: true,
507
+ });
508
+ injector = inject(Injector);
509
+ lastRouterState;
510
+ brandingEffectInitialized = false;
511
+ updateTitle(routerState) {
512
+ this.lastRouterState = routerState;
513
+ this.applyTitle();
514
+ this.ensureBrandingSubscription();
515
+ }
516
+ ensureBrandingSubscription() {
517
+ if (this.brandingEffectInitialized)
518
+ return;
519
+ this.brandingEffectInitialized = true;
520
+ effect(() => this.applyTitle(), { injector: this.injector });
521
+ }
522
+ applyTitle() {
523
+ if (!this.lastRouterState)
524
+ return;
525
+ const data = this.injector.get(BrandingDataSource).branding();
526
+ const baseTitle = data.appTitle || data.appName;
527
+ const breadcrumbKey = this.getDeepestBreadcrumb(this.lastRouterState.root);
528
+ if (breadcrumbKey) {
529
+ const resolved = this.translator
530
+ ? this.translator(breadcrumbKey)
531
+ : breadcrumbKey;
532
+ this.title.setTitle(`${baseTitle} | ${resolved}`);
533
+ }
534
+ else {
535
+ this.title.setTitle(baseTitle);
536
+ }
537
+ }
538
+ getDeepestBreadcrumb(route) {
539
+ let breadcrumb = typeof route.data['breadcrumb'] === 'string'
540
+ ? route.data['breadcrumb']
541
+ : undefined;
542
+ for (const child of route.children) {
543
+ const childBreadcrumb = this.getDeepestBreadcrumb(child);
544
+ if (childBreadcrumb) {
545
+ breadcrumb = childBreadcrumb;
546
+ }
547
+ }
548
+ return breadcrumb;
549
+ }
550
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AppTitleService, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
551
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AppTitleService, providedIn: 'root' });
552
+ }
553
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AppTitleService, decorators: [{
554
+ type: Injectable,
555
+ args: [{ providedIn: 'root' }]
556
+ }] });
557
+
558
+ /**
559
+ * Registers branding tokens, defaults, fallback assets, and (by default) the
560
+ * `TitleStrategy` that drives `document.title` from the branding signal.
561
+ *
562
+ * Does **not** load the tenant's branding record. Loading must happen after
563
+ * authentication is settled (Apollo's URI bakes in the tenant ID at the time
564
+ * the first request fires; loading too early targets
565
+ * `/tenants/undefined/GraphQL`). The host shell typically does:
566
+ *
567
+ * ```ts
568
+ * inject(BrandingApplicationService); // start the apply effect
569
+ * effect(() => {
570
+ * if (this.auth.isAuthenticated()) {
571
+ * this.brandingDataSource.load();
572
+ * }
573
+ * });
574
+ * ```
575
+ *
576
+ * `BrandingApplicationService` is `providedIn: 'root'`, so the host only has
577
+ * to inject it once to activate the apply effect.
578
+ */
579
+ function provideOctoBranding(config) {
580
+ const merged = {
581
+ ...NEUTRAL_BRANDING_DEFAULTS,
582
+ ...(config?.defaults ?? {}),
583
+ lightTheme: {
584
+ ...NEUTRAL_BRANDING_DEFAULTS.lightTheme,
585
+ ...(config?.defaults?.lightTheme ?? {}),
586
+ },
587
+ darkTheme: config?.defaults?.darkTheme === null
588
+ ? null
589
+ : {
590
+ ...(NEUTRAL_BRANDING_DEFAULTS.darkTheme ??
591
+ NEUTRAL_BRANDING_DEFAULTS.lightTheme),
592
+ ...(config?.defaults?.darkTheme ?? {}),
593
+ },
594
+ };
595
+ const providers = [
596
+ { provide: OCTO_BRANDING_DEFAULTS, useValue: merged },
597
+ {
598
+ provide: OCTO_BRANDING_FALLBACK_ASSETS,
599
+ useValue: config?.fallbackAssets ?? NEUTRAL_FALLBACK_ASSETS,
600
+ },
601
+ ];
602
+ if (config?.registerTitleStrategy !== false) {
603
+ providers.push({ provide: TitleStrategy, useExisting: AppTitleService });
604
+ }
605
+ return makeEnvironmentProviders(providers);
606
+ }
607
+
608
+ /** Convenience barrel re-exporting branding model interfaces from ./models/. */
609
+
610
+ /**
611
+ * Light/dark mode toggle backed by `localStorage` + `<html data-theme="...">`.
612
+ * Pure mode toggle; palette generation lives in `BrandingApplicationService`.
613
+ */
614
+ class ThemeService {
615
+ document = inject(DOCUMENT);
616
+ renderer;
617
+ isDarkSignal = signal(false, ...(ngDevMode ? [{ debugName: "isDarkSignal" }] : /* istanbul ignore next */ []));
618
+ isDark = this.isDarkSignal.asReadonly();
619
+ constructor() {
620
+ this.renderer = inject(RendererFactory2).createRenderer(null, null);
621
+ this.isDarkSignal.set(this.getInitialThemeIsDark());
622
+ this.applyTheme();
623
+ }
624
+ toggleTheme() {
625
+ this.setDark(!this.isDarkSignal());
626
+ }
627
+ setDark(isDark) {
628
+ this.isDarkSignal.set(isDark);
629
+ try {
630
+ this.document.defaultView?.localStorage.setItem('theme', isDark ? 'dark' : 'light');
631
+ }
632
+ catch {
633
+ // localStorage may be unavailable (private mode, SSR) — degrade silently.
634
+ }
635
+ this.applyTheme();
636
+ }
637
+ applyTheme() {
638
+ const html = this.document.documentElement;
639
+ this.renderer.setAttribute(html, 'data-theme', this.isDarkSignal() ? 'dark' : 'light');
640
+ }
641
+ getInitialThemeIsDark() {
642
+ let stored = null;
643
+ try {
644
+ stored = this.document.defaultView?.localStorage.getItem('theme') ?? null;
645
+ }
646
+ catch {
647
+ stored = null;
648
+ }
649
+ if (stored === 'dark')
650
+ return true;
651
+ if (stored === 'light')
652
+ return false;
653
+ return (this.document.defaultView?.matchMedia?.('(prefers-color-scheme: dark)')
654
+ .matches ?? false);
655
+ }
656
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
657
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeService, providedIn: 'root' });
658
+ }
659
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeService, decorators: [{
660
+ type: Injectable,
661
+ args: [{ providedIn: 'root' }]
662
+ }], ctorParameters: () => [] });
663
+
664
+ const DEFAULT_THEME_SWITCHER_MESSAGES = {
665
+ toggleToDark: 'Switch to dark mode',
666
+ toggleToLight: 'Switch to light mode',
667
+ unavailable: 'Theme switching is unavailable for this tenant',
668
+ };
669
+
670
+ class ThemeSwitcherComponent {
671
+ messages = input(DEFAULT_THEME_SWITCHER_MESSAGES, ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
672
+ themeService = inject(ThemeService);
673
+ branding = inject(BrandingDataSource);
674
+ isDark = this.themeService.isDark;
675
+ available = computed(() => this.branding.branding().darkTheme !== null, ...(ngDevMode ? [{ debugName: "available" }] : /* istanbul ignore next */ []));
676
+ ariaLabel = computed(() => {
677
+ if (!this.available())
678
+ return this.messages().unavailable;
679
+ return this.isDark()
680
+ ? this.messages().toggleToLight
681
+ : this.messages().toggleToDark;
682
+ }, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
683
+ lightIcon = lightbulbOutlineIcon;
684
+ darkIcon = brightnessContrastIcon;
685
+ onToggle() {
686
+ if (!this.available())
687
+ return;
688
+ this.themeService.setDark(!this.isDark());
689
+ }
690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeSwitcherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: ThemeSwitcherComponent, isStandalone: true, selector: "mm-theme-switcher", inputs: { messages: { classPropertyName: "messages", publicName: "messages", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<button\n type=\"button\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"isDark()\"\n [disabled]=\"!available()\"\n (click)=\"onToggle()\"\n>\n <kendo-svg-icon [icon]=\"isDark() ? lightIcon : darkIcon\" />\n</button>\n", styles: [":host{display:inline-flex}button{appearance:none;background:transparent;border:0;padding:var(--kendo-spacing-2);cursor:pointer;color:inherit}button[disabled]{cursor:not-allowed;opacity:.5}button:focus-visible{outline:2px solid currentColor;outline-offset:2px}\n"], dependencies: [{ kind: "ngmodule", type: SVGIconModule }, { kind: "component", type: i1$1.SVGIconComponent, selector: "kendo-svg-icon, kendo-svgicon", inputs: ["icon"], exportAs: ["kendoSVGIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
692
+ }
693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ThemeSwitcherComponent, decorators: [{
694
+ type: Component,
695
+ args: [{ selector: 'mm-theme-switcher', standalone: true, imports: [SVGIconModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<button\n type=\"button\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-pressed]=\"isDark()\"\n [disabled]=\"!available()\"\n (click)=\"onToggle()\"\n>\n <kendo-svg-icon [icon]=\"isDark() ? lightIcon : darkIcon\" />\n</button>\n", styles: [":host{display:inline-flex}button{appearance:none;background:transparent;border:0;padding:var(--kendo-spacing-2);cursor:pointer;color:inherit}button[disabled]{cursor:not-allowed;opacity:.5}button:focus-visible{outline:2px solid currentColor;outline-offset:2px}\n"] }]
696
+ }], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }] } });
697
+
698
+ /**
699
+ * Writes tenant branding (Kendo color vars, gradients, favicon, document
700
+ * title) inline on <html>. An effect re-applies the correct palette when
701
+ * either the theme mode or the branding data changes, so mode toggles after
702
+ * a save don't leave stale inline values behind.
703
+ */
704
+ class BrandingApplicationService {
705
+ document = inject(DOCUMENT);
706
+ themeService = inject(ThemeService);
707
+ brandingDataSource = inject(BrandingDataSource);
708
+ fallbackAssets = inject(OCTO_BRANDING_FALLBACK_ASSETS, {
709
+ optional: true,
710
+ });
711
+ // Cleared before every apply so unset palette fields fall through to the
712
+ // static layer-2 brand-default instead of sticking from a prior save.
713
+ static BRANDED_VARS = [
714
+ ...['primary', 'secondary', 'tertiary'].flatMap((c) => [
715
+ `--kendo-color-${c}`,
716
+ `--kendo-color-${c}-hover`,
717
+ `--kendo-color-${c}-active`,
718
+ `--kendo-color-on-${c}`,
719
+ `--kendo-color-${c}-subtle`,
720
+ `--kendo-color-${c}-subtle-hover`,
721
+ `--kendo-color-${c}-subtle-active`,
722
+ `--kendo-color-${c}-emphasis`,
723
+ `--kendo-color-${c}-on-subtle`,
724
+ `--kendo-color-${c}-on-surface`,
725
+ ]),
726
+ '--kendo-color-base',
727
+ '--kendo-color-base-hover',
728
+ '--kendo-color-base-active',
729
+ '--kendo-color-on-base',
730
+ '--kendo-color-base-subtle',
731
+ '--kendo-color-base-subtle-hover',
732
+ '--kendo-color-base-subtle-active',
733
+ '--kendo-color-base-emphasis',
734
+ '--kendo-color-base-on-subtle',
735
+ '--kendo-color-subtle',
736
+ '--kendo-color-border',
737
+ '--kendo-color-border-alt',
738
+ '--kendo-color-app-surface',
739
+ '--kendo-color-on-app-surface',
740
+ '--app-header-gradient-start',
741
+ '--app-header-gradient-end',
742
+ '--app-header-text',
743
+ '--app-footer-gradient-start',
744
+ '--app-footer-gradient-end',
745
+ '--app-footer-text',
746
+ ];
747
+ constructor() {
748
+ effect(() => {
749
+ const isDark = this.themeService.isDark();
750
+ const data = this.brandingDataSource.branding();
751
+ // Tenant disabled dark-theme override (single-theme app) — force light
752
+ // mode. Without this guard a stale 'dark' in localStorage (or system
753
+ // prefers-color-scheme) carries over and we'd render light palette
754
+ // values into html[data-theme='dark']: surface ladder collapses to
755
+ // white and text becomes unreadable. ThemeSwitcherComponent also
756
+ // disables the toggle when darkTheme is null so the user can't
757
+ // re-enter this state interactively.
758
+ if (data.darkTheme === null && isDark) {
759
+ this.themeService.setDark(false);
760
+ return;
761
+ }
762
+ const palette = isDark ? data.darkTheme : data.lightTheme;
763
+ this.apply(data, palette);
764
+ });
765
+ }
766
+ apply(data, palette) {
767
+ this.clearInlineBrandingVars();
768
+ this.applyThemeColors(palette);
769
+ this.applyFavicon(data.faviconUrl);
770
+ // document.title is owned by AppTitleService (TitleStrategy) — it composes
771
+ // `${branding().appName} | ${routeBreadcrumb}` per navigation. Setting
772
+ // title here would race AppTitleService and overwrite the route portion
773
+ // (e.g. /settings would flicker from "TecLink | Settings" to just "TecLink"
774
+ // when branding finishes loading). The tab updates on next navigation
775
+ // after a tenant edits appName/appTitle in Settings — acceptable trade-off.
776
+ }
777
+ applyThemeColors(palette) {
778
+ const root = this.document.documentElement;
779
+ if (palette.primaryColor) {
780
+ this.applyColorPalette(root, 'primary', palette.primaryColor);
781
+ }
782
+ if (palette.secondaryColor) {
783
+ this.applyColorPalette(root, 'secondary', palette.secondaryColor);
784
+ }
785
+ if (palette.tertiaryColor) {
786
+ this.applyColorPalette(root, 'tertiary', palette.tertiaryColor);
787
+ }
788
+ if (palette.neutralColor) {
789
+ this.applyNeutralColors(root, palette.neutralColor);
790
+ }
791
+ this.setIf(root, '--app-header-gradient-start', palette.headerGradient.startColor);
792
+ this.setIf(root, '--app-header-gradient-end', palette.headerGradient.endColor);
793
+ if (palette.headerGradient.startColor) {
794
+ root.style.setProperty('--app-header-text', this.getContrastColor(palette.headerGradient.startColor));
795
+ }
796
+ this.setIf(root, '--app-footer-gradient-start', palette.footerGradient.startColor);
797
+ this.setIf(root, '--app-footer-gradient-end', palette.footerGradient.endColor);
798
+ if (palette.footerGradient.startColor) {
799
+ root.style.setProperty('--app-footer-text', this.getContrastColor(palette.footerGradient.startColor));
800
+ }
801
+ if (palette.backgroundColor) {
802
+ // Only write the root surface — --kendo-color-surface / -surface-alt /
803
+ // -base / -border are aliased in styles.scss to --color-surface-* shades
804
+ // that derive from --color-surface (= app-surface) via mode-aware
805
+ // color-mix. Writing them directly here would collapse the shade ladder
806
+ // (all surfaces flat = same colour → transparent-looking components).
807
+ root.style.setProperty('--kendo-color-app-surface', palette.backgroundColor);
808
+ // Contrast text on the surface — without this, a dark backgroundColor in
809
+ // light mode (or vice versa) renders text against an inverted surface
810
+ // and becomes unreadable. --color-on-surface in styles.scss aliases this.
811
+ root.style.setProperty('--kendo-color-on-app-surface', this.getContrastColor(palette.backgroundColor));
812
+ }
813
+ }
814
+ applyFavicon(faviconUrl) {
815
+ const href = faviconUrl ?? this.fallbackAssets?.favicon;
816
+ if (!href)
817
+ return;
818
+ let link = this.document.querySelector("link[rel~='icon']");
819
+ if (!link) {
820
+ link = this.document.createElement('link');
821
+ link.rel = 'icon';
822
+ this.document.head.appendChild(link);
823
+ }
824
+ link.href = href;
825
+ }
826
+ clearInlineBrandingVars() {
827
+ const root = this.document.documentElement;
828
+ for (const name of BrandingApplicationService.BRANDED_VARS) {
829
+ root.style.removeProperty(name);
830
+ }
831
+ }
832
+ setIf(root, name, value) {
833
+ if (value)
834
+ root.style.setProperty(name, value);
835
+ }
836
+ applyColorPalette(root, colorName, baseColor) {
837
+ const palette = this.generatePalette(baseColor);
838
+ root.style.setProperty(`--kendo-color-${colorName}`, palette[40]);
839
+ root.style.setProperty(`--kendo-color-${colorName}-hover`, palette[50]);
840
+ root.style.setProperty(`--kendo-color-${colorName}-active`, palette[60]);
841
+ root.style.setProperty(`--kendo-color-on-${colorName}`, this.getContrastColor(palette[40]));
842
+ root.style.setProperty(`--kendo-color-${colorName}-subtle`, palette[90]);
843
+ root.style.setProperty(`--kendo-color-${colorName}-subtle-hover`, palette[80]);
844
+ root.style.setProperty(`--kendo-color-${colorName}-subtle-active`, palette[70]);
845
+ root.style.setProperty(`--kendo-color-${colorName}-emphasis`, palette[70]);
846
+ root.style.setProperty(`--kendo-color-${colorName}-on-subtle`, palette[20]);
847
+ root.style.setProperty(`--kendo-color-${colorName}-on-surface`, palette[40]);
848
+ }
849
+ applyNeutralColors(root, baseColor) {
850
+ const palette = this.generatePalette(baseColor);
851
+ root.style.setProperty('--kendo-color-base', palette[95]);
852
+ root.style.setProperty('--kendo-color-base-hover', palette[90]);
853
+ root.style.setProperty('--kendo-color-base-active', palette[80]);
854
+ root.style.setProperty('--kendo-color-on-base', palette[20]);
855
+ root.style.setProperty('--kendo-color-base-subtle', palette[90]);
856
+ root.style.setProperty('--kendo-color-base-subtle-hover', palette[80]);
857
+ root.style.setProperty('--kendo-color-base-subtle-active', palette[70]);
858
+ root.style.setProperty('--kendo-color-base-emphasis', palette[70]);
859
+ root.style.setProperty('--kendo-color-base-on-subtle', palette[20]);
860
+ root.style.setProperty('--kendo-color-subtle', palette[40]);
861
+ root.style.setProperty('--kendo-color-border', palette[80]);
862
+ root.style.setProperty('--kendo-color-border-alt', palette[70]);
863
+ }
864
+ // ---------------------------------------------------------------------------
865
+ // Palette generation (ported from energy-community theme.service.ts)
866
+ // ---------------------------------------------------------------------------
867
+ generatePalette(baseColor) {
868
+ const rgb = this.hexToRgb(baseColor);
869
+ const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b);
870
+ const palette = {
871
+ 0: '#000000',
872
+ 100: '#ffffff',
873
+ };
874
+ palette[40] = baseColor;
875
+ const baseL = hsl.l;
876
+ const baseS = hsl.s;
877
+ const baseH = hsl.h;
878
+ const lighten = (amount) => Math.min(1, baseL + amount);
879
+ const darken = (amount) => Math.max(0, baseL - amount);
880
+ palette[50] = this.hslToHex(baseH, Math.min(1, baseS * 1.15), darken(0.12));
881
+ palette[60] = this.hslToHex(baseH, Math.min(1, baseS * 1.08), darken(0.08));
882
+ palette[70] = this.hslToHex(baseH, Math.max(0.25, baseS * 0.85), lighten(0.08));
883
+ palette[80] = this.hslToHex(baseH, Math.max(0.15, baseS * 0.6), lighten(0.22));
884
+ palette[90] = this.hslToHex(baseH, Math.max(0.08, baseS * 0.35), lighten(0.38));
885
+ palette[95] = this.hslToHex(baseH, Math.max(0.04, baseS * 0.2), lighten(0.43));
886
+ palette[98] = this.hslToHex(baseH, Math.max(0.02, baseS * 0.12), lighten(0.46));
887
+ palette[99] = this.hslToHex(baseH, Math.max(0.01, baseS * 0.08), lighten(0.47));
888
+ palette[10] = this.hslToHex(baseH, Math.min(1, baseS * 1.4), darken(0.72));
889
+ palette[20] = this.hslToHex(baseH, Math.min(1, baseS * 1.25), darken(0.62));
890
+ palette[25] = this.hslToHex(baseH, Math.min(1, baseS * 1.2), darken(0.57));
891
+ palette[30] = this.hslToHex(baseH, Math.min(1, baseS * 1.15), darken(0.52));
892
+ palette[35] = this.hslToHex(baseH, Math.min(1, baseS * 1.1), darken(0.42));
893
+ return palette;
894
+ }
895
+ hexToRgb(color) {
896
+ const hexResult = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color);
897
+ if (hexResult) {
898
+ return {
899
+ r: parseInt(hexResult[1], 16),
900
+ g: parseInt(hexResult[2], 16),
901
+ b: parseInt(hexResult[3], 16),
902
+ };
903
+ }
904
+ const rgbResult = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/.exec(color);
905
+ if (rgbResult) {
906
+ return {
907
+ r: parseInt(rgbResult[1], 10),
908
+ g: parseInt(rgbResult[2], 10),
909
+ b: parseInt(rgbResult[3], 10),
910
+ };
911
+ }
912
+ return { r: 0, g: 0, b: 0 };
913
+ }
914
+ rgbToHsl(r, g, b) {
915
+ const rn = r / 255;
916
+ const gn = g / 255;
917
+ const bn = b / 255;
918
+ const max = Math.max(rn, gn, bn);
919
+ const min = Math.min(rn, gn, bn);
920
+ let h = 0;
921
+ let s = 0;
922
+ const l = (max + min) / 2;
923
+ if (max !== min) {
924
+ const d = max - min;
925
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
926
+ switch (max) {
927
+ case rn:
928
+ h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
929
+ break;
930
+ case gn:
931
+ h = ((bn - rn) / d + 2) / 6;
932
+ break;
933
+ case bn:
934
+ h = ((rn - gn) / d + 4) / 6;
935
+ break;
936
+ }
937
+ }
938
+ return { h, s, l };
939
+ }
940
+ hslToHex(h, s, l) {
941
+ let r;
942
+ let g;
943
+ let b;
944
+ if (s === 0) {
945
+ r = l;
946
+ g = l;
947
+ b = l;
948
+ }
949
+ else {
950
+ const hue2rgb = (p, q, t) => {
951
+ let tn = t;
952
+ if (tn < 0)
953
+ tn += 1;
954
+ if (tn > 1)
955
+ tn -= 1;
956
+ if (tn < 1 / 6)
957
+ return p + (q - p) * 6 * tn;
958
+ if (tn < 1 / 2)
959
+ return q;
960
+ if (tn < 2 / 3)
961
+ return p + (q - p) * (2 / 3 - tn) * 6;
962
+ return p;
963
+ };
964
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
965
+ const p = 2 * l - q;
966
+ r = hue2rgb(p, q, h + 1 / 3);
967
+ g = hue2rgb(p, q, h);
968
+ b = hue2rgb(p, q, h - 1 / 3);
969
+ }
970
+ return this.rgbToHex(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255));
971
+ }
972
+ rgbToHex(r, g, b) {
973
+ return ('#' +
974
+ [r, g, b]
975
+ .map((x) => {
976
+ const hex = Math.round(x).toString(16);
977
+ return hex.length === 1 ? '0' + hex : hex;
978
+ })
979
+ .join(''));
980
+ }
981
+ getContrastColor(hex) {
982
+ const rgb = this.hexToRgb(hex);
983
+ const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
984
+ return brightness > 128 ? '#000000' : '#ffffff';
985
+ }
986
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingApplicationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
987
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingApplicationService, providedIn: 'root' });
988
+ }
989
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: BrandingApplicationService, decorators: [{
990
+ type: Injectable,
991
+ args: [{ providedIn: 'root' }]
992
+ }], ctorParameters: () => [] });
993
+
994
+ function createBrandingStub(overrides) {
995
+ const branding = signal({
996
+ ...NEUTRAL_BRANDING_DEFAULTS,
997
+ ...(overrides ?? {}),
998
+ }, ...(ngDevMode ? [{ debugName: "branding" }] : /* istanbul ignore next */ []));
999
+ return {
1000
+ branding,
1001
+ load: () => Promise.resolve(),
1002
+ save: () => Promise.resolve(branding()),
1003
+ resetToDefaults: () => Promise.resolve(),
1004
+ };
1005
+ }
1006
+ /** Convenience: returns a Provider that injects the stub as BrandingDataSource. */
1007
+ function provideBrandingTesting(stub = createBrandingStub()) {
1008
+ return { provide: BrandingDataSource, useValue: stub };
1009
+ }
1010
+
1011
+ // Public API of the branding feature folder. Re-exported by
1012
+ // octo-ui/src/public-api.ts to expose under @meshmakers/octo-ui.
1013
+ //
1014
+ // Note: the heavy admin-only `SettingsPageComponent` (a.k.a.
1015
+ // `mm-branding-settings`) and `BRANDING_ROUTES` live in the dedicated
1016
+ // secondary entry point `@meshmakers/octo-ui/branding-settings` so apps that
1017
+ // only need the lightweight branding pieces (logo, theme switcher, services)
1018
+ // don't pull in form/Kendo modules they don't use.
1019
+ // Configuration & tokens
1020
+
1021
+ /**
1022
+ * Generated bundle index. Do not edit.
1023
+ */
1024
+
1025
+ export { AppTitleService, BrandingApplicationService, BrandingDataSource, DEFAULT_THEME_SWITCHER_MESSAGES, NEUTRAL_BRANDING_DEFAULTS, NEUTRAL_FALLBACK_ASSETS, OCTO_BRANDING_DEFAULTS, OCTO_BRANDING_FALLBACK_ASSETS, OCTO_TITLE_TRANSLATOR, ThemeService, ThemeSwitcherComponent, createBrandingStub, provideBrandingTesting, provideOctoBranding };
1026
+ //# sourceMappingURL=meshmakers-octo-ui-branding.mjs.map