Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/core/brand/brand.ts
12925 views
1
/*
2
* brand.ts
3
*
4
* Class that implements support for `_brand.yml` data in Quarto
5
*
6
* Copyright (C) 2024 Posit Software, PBC
7
*/
8
9
import {
10
BrandColorLightDark,
11
BrandFont,
12
BrandLogoExplicitResource,
13
BrandLogoResource,
14
BrandLogoSingle,
15
BrandLogoUnified,
16
BrandNamedLogo,
17
BrandNamedThemeColor,
18
BrandSingle,
19
BrandStringLightDark,
20
BrandTypographyOptionsBase,
21
BrandTypographyOptionsHeadingsSingle,
22
BrandTypographySingle,
23
BrandTypographyUnified,
24
BrandUnified,
25
LogoLightDarkSpecifier,
26
LogoOptions,
27
NormalizedLogoLightDarkSpecifier,
28
Zod,
29
} from "../../resources/types/zod/schema-types.ts";
30
import { InternalError } from "../lib/error.ts";
31
32
import { dirname, join, relative, resolve } from "../../deno_ral/path.ts";
33
import { warnOnce } from "../log.ts";
34
import { isCssColorName } from "../css/color-names.ts";
35
import { isExternalPath } from "../url.ts";
36
import {
37
LogoLightDarkSpecifierPathOptional,
38
LogoOptionsPathOptional,
39
LogoSpecifier,
40
LogoSpecifierPathOptional,
41
} from "../../resources/types/schema-types.ts";
42
import { ensureLeadingSlash } from "../path.ts";
43
44
type ProcessedBrandData = {
45
color: Record<string, string>;
46
typography: BrandTypographySingle;
47
logo: {
48
small?: BrandLogoExplicitResource;
49
medium?: BrandLogoExplicitResource;
50
large?: BrandLogoExplicitResource;
51
images: Record<string, BrandLogoExplicitResource>;
52
};
53
};
54
55
export class Brand {
56
data: BrandSingle;
57
brandDir: string;
58
projectDir: string;
59
processedData: ProcessedBrandData;
60
61
constructor(
62
readonly brand: unknown,
63
brandDir: string,
64
projectDir: string,
65
) {
66
this.data = Zod.BrandSingle.parse(brand);
67
this.brandDir = brandDir;
68
this.projectDir = projectDir;
69
this.processedData = this.processData(this.data);
70
}
71
72
processData(data: BrandSingle): ProcessedBrandData {
73
const color: Record<string, string> = {};
74
for (const colorName of Object.keys(data.color?.palette ?? {})) {
75
color[colorName] = this.getColor(colorName);
76
}
77
for (const colorName of Object.keys(data.color ?? {})) {
78
if (colorName === "palette") {
79
continue;
80
}
81
color[colorName] = this.getColor(colorName);
82
}
83
84
const typography: BrandTypographySingle = {};
85
const base = this.getFont("base");
86
if (base) {
87
typography.base = base;
88
}
89
const headings = this.getFont("headings");
90
if (headings) {
91
typography.headings = headings;
92
}
93
const link = data.typography?.link;
94
if (link) {
95
typography.link = link;
96
}
97
let monospace = this.getFont("monospace");
98
let monospaceInline = this.getFont("monospace-inline");
99
let monospaceBlock = this.getFont("monospace-block");
100
101
if (monospace) {
102
if (typeof monospace === "string") {
103
monospace = { family: monospace };
104
}
105
typography.monospace = monospace;
106
}
107
if (monospaceInline && typeof monospaceInline === "string") {
108
monospaceInline = { family: monospaceInline };
109
}
110
if (monospaceBlock && typeof monospaceBlock === "string") {
111
monospaceBlock = { family: monospaceBlock };
112
}
113
114
// cut off control flow here so the type checker knows these
115
// are not strings
116
if (typeof monospace === "string") {
117
throw new InternalError("should never happen");
118
}
119
if (typeof monospaceInline === "string") {
120
throw new InternalError("should never happen");
121
}
122
if (typeof monospaceBlock === "string") {
123
throw new InternalError("should never happen");
124
}
125
126
if (monospace || monospaceInline) {
127
typography["monospace-inline"] = {
128
...(monospace ?? {}),
129
...(monospaceInline ?? {}),
130
};
131
}
132
if (monospaceBlock) {
133
if (typeof monospaceBlock === "string") {
134
monospaceBlock = { family: monospaceBlock };
135
}
136
}
137
if (monospace || monospaceBlock) {
138
typography["monospace-block"] = {
139
...(monospace ?? {}),
140
...(monospaceBlock ?? {}),
141
};
142
}
143
144
const logo: ProcessedBrandData["logo"] = { images: {} };
145
for (
146
const size of Zod.BrandNamedLogo.options
147
) {
148
const v = this.getLogo(size);
149
if (v) {
150
logo[size] = v;
151
}
152
}
153
for (const [key, value] of Object.entries(data.logo?.images ?? {})) {
154
logo.images[key] = this.resolvePath(value);
155
}
156
157
return {
158
color,
159
typography,
160
logo,
161
};
162
}
163
164
// semantics of name resolution for colors:
165
// - if the name is in the "palette" key, use that value as they key for a recursive call (so color names can be aliased or redefined away from scss defaults)
166
// - if the name is a default color name, call getColor recursively (so defaults can use named values)
167
// - otherwise, assume it's a color value and return it
168
getColor(name: string, quiet = false): string {
169
const seenValues = new Set<string>();
170
171
do {
172
if (seenValues.has(name)) {
173
throw new Error(
174
`Circular reference in _brand.yml color definitions: ${
175
Array.from(seenValues).join(
176
" -> ",
177
)
178
}`,
179
);
180
}
181
seenValues.add(name);
182
if (this.data.color?.palette?.[name]) {
183
name = this.data.color.palette[name] as string;
184
} else if (
185
Zod.BrandNamedThemeColor.options.includes(
186
name as BrandNamedThemeColor,
187
) &&
188
this.data.color?.[name as BrandNamedThemeColor]
189
) {
190
name = this.data.color[name as BrandNamedThemeColor]!;
191
} else {
192
// if the name is not a default color name, assume it's a color value
193
if (!isCssColorName(name) && !quiet) {
194
warnOnce(
195
`"${name}" is not a valid CSS color name.\nThis might cause SCSS compilation to fail, or the color to have no effect.`,
196
);
197
}
198
return name;
199
}
200
} while (seenValues.size < 100); // 100 ought to be enough for anyone, with apologies to Bill Gates
201
throw new Error(
202
"Recursion depth exceeded 100 in _brand.yml color definitions",
203
);
204
}
205
206
getFont(
207
name: string,
208
):
209
| BrandTypographyOptionsBase
210
| BrandTypographyOptionsHeadingsSingle
211
| undefined {
212
if (!this.data.typography) {
213
return undefined;
214
}
215
const typography = this.data.typography;
216
switch (name) {
217
case "base":
218
return typography.base;
219
case "headings":
220
return typography.headings;
221
case "link":
222
return typography.link;
223
case "monospace":
224
return typography.monospace;
225
case "monospace-inline":
226
return typography["monospace-inline"];
227
case "monospace-block":
228
return typography["monospace-block"];
229
}
230
return undefined;
231
}
232
233
getFontResources(name: string): BrandFont[] {
234
if (name === "fonts") {
235
throw new Error(
236
"'fonts' is a reserved name in _brand.yml typography definitions",
237
);
238
}
239
if (!this.data.typography) {
240
return [];
241
}
242
const typography = this.data.typography;
243
const fonts = typography.fonts;
244
return fonts ?? [];
245
}
246
247
resolvePath(entry: BrandLogoResource) {
248
const pathPrefix = relative(this.projectDir, this.brandDir);
249
if (typeof entry === "string") {
250
return { path: isExternalPath(entry) ? entry : join(pathPrefix, entry) };
251
}
252
return {
253
...entry,
254
path: isExternalPath(entry.path)
255
? entry.path
256
: join(pathPrefix, entry.path),
257
};
258
}
259
260
getLogoResource(name: string): BrandLogoExplicitResource {
261
const entry = this.data.logo?.images?.[name];
262
if (!entry) {
263
return this.resolvePath(name);
264
}
265
return this.resolvePath(entry);
266
}
267
getLogo(name: BrandNamedLogo): BrandLogoExplicitResource | undefined {
268
const entry = this.data.logo?.[name];
269
if (!entry) {
270
return undefined;
271
}
272
return this.getLogoResource(entry);
273
}
274
}
275
276
export type LightDarkBrand = {
277
light?: Brand;
278
dark?: Brand;
279
};
280
281
export type LightDarkBrandDarkFlag = {
282
light?: Brand;
283
dark?: Brand;
284
enablesDarkMode: boolean;
285
};
286
287
export type LightDarkColor = {
288
light?: string;
289
dark?: string;
290
};
291
292
export const getFavicon = (brand: Brand): string | undefined => {
293
const logoInfo = brand.getLogo("small");
294
if (!logoInfo) {
295
return undefined;
296
}
297
return logoInfo.path;
298
};
299
300
export function resolveLogo(
301
brand: LightDarkBrand | undefined,
302
spec: LogoLightDarkSpecifier | undefined,
303
order: BrandNamedLogo[],
304
): NormalizedLogoLightDarkSpecifier | undefined {
305
const resolveBrandLogo = (
306
mode: "light" | "dark",
307
name: string,
308
): LogoOptions => {
309
const logo = brand?.[mode]?.processedData?.logo;
310
return logo &&
311
((Zod.BrandNamedLogo.options.includes(name as BrandNamedLogo) &&
312
logo[name as BrandNamedLogo]) || logo.images[name]) ||
313
{ path: name };
314
};
315
function findLogo(
316
mode: "light" | "dark",
317
order: BrandNamedLogo[],
318
): LogoOptions | undefined {
319
if (brand?.[mode]) {
320
for (const size of order) {
321
const logo = brand[mode].processedData.logo[size];
322
if (logo) {
323
return logo;
324
}
325
}
326
}
327
return undefined;
328
}
329
const resolveLogoOptions = (
330
mode: "light" | "dark",
331
logo: LogoOptions,
332
): LogoOptions => {
333
const logo2 = resolveBrandLogo(mode, logo.path);
334
if (logo2) {
335
const { path: _, ...rest } = logo;
336
return {
337
...logo2,
338
...rest,
339
};
340
}
341
return logo;
342
};
343
if (spec === false) {
344
return undefined;
345
}
346
if (!spec) {
347
const lightLogo = findLogo("light", order);
348
const darkLogo = findLogo("dark", order);
349
if (!lightLogo && !darkLogo) {
350
return undefined;
351
}
352
return {
353
light: lightLogo || darkLogo,
354
dark: darkLogo || lightLogo,
355
};
356
}
357
if (typeof spec === "string") {
358
return {
359
light: resolveBrandLogo("light", spec),
360
dark: resolveBrandLogo("light", spec),
361
};
362
}
363
if ("path" in spec) {
364
return {
365
light: resolveLogoOptions("light", spec),
366
dark: resolveLogoOptions("dark", spec),
367
};
368
}
369
let light, dark;
370
if (!spec.light) {
371
light = findLogo("light", order);
372
} else if (typeof spec.light === "string") {
373
light = resolveBrandLogo("light", spec.light);
374
} else {
375
light = resolveLogoOptions("light", spec.light);
376
}
377
if (!spec.dark) {
378
dark = findLogo("dark", order);
379
} else if (typeof spec.dark === "string") {
380
dark = resolveBrandLogo("dark", spec.dark);
381
} else {
382
dark = resolveLogoOptions("dark", spec.dark);
383
}
384
// light logo default to dark logo if no light logo specified
385
if (!light && dark) {
386
light = { ...dark };
387
}
388
// dark logo default to light logo if no dark logo specified
389
// and dark mode is enabled
390
if (!dark && light && brand && brand.dark) {
391
dark = { ...light };
392
}
393
return {
394
light,
395
dark,
396
};
397
}
398
399
const ensureLeadingSlashIfNotExternal = (path: string) =>
400
isExternalPath(path) ? path : ensureLeadingSlash(path);
401
402
export function logoAddLeadingSlashes(
403
spec: NormalizedLogoLightDarkSpecifier | undefined,
404
brand: LightDarkBrand | undefined,
405
input: string | undefined,
406
): NormalizedLogoLightDarkSpecifier | undefined {
407
if (!spec) {
408
return spec;
409
}
410
if (input) {
411
const inputDir = dirname(resolve(input));
412
if (!brand || inputDir === brand.light?.projectDir) {
413
return spec;
414
}
415
}
416
return {
417
light: spec.light && {
418
...spec.light,
419
path: ensureLeadingSlashIfNotExternal(spec.light.path),
420
},
421
dark: spec.dark && {
422
...spec.dark,
423
path: ensureLeadingSlashIfNotExternal(spec.dark.path),
424
},
425
};
426
}
427
428
// Return a copy of the brand with logo paths converted from project-relative
429
// to project-absolute (leading /). Typst resolves these via --root, which
430
// points to the project directory. Call this before resolveLogo so that
431
// brand-sourced paths get the / prefix while document-sourced paths are
432
// left untouched.
433
export function brandWithAbsoluteLogoPaths(
434
brand: LightDarkBrand | undefined,
435
): LightDarkBrand | undefined {
436
if (!brand) {
437
return brand;
438
}
439
const transformBrand = (b: Brand | undefined): Brand | undefined => {
440
if (!b) return b;
441
const oldLogo = b.processedData.logo;
442
const logo: ProcessedBrandData["logo"] = { images: {} };
443
for (const size of Zod.BrandNamedLogo.options) {
444
if (oldLogo[size]) {
445
logo[size] = {
446
...oldLogo[size],
447
path: ensureLeadingSlashIfNotExternal(oldLogo[size]!.path),
448
};
449
}
450
}
451
for (const [key, value] of Object.entries(oldLogo.images)) {
452
logo.images[key] = {
453
...value,
454
path: ensureLeadingSlashIfNotExternal(value.path),
455
};
456
}
457
const copy = Object.create(b) as Brand;
458
copy.processedData = { ...b.processedData, logo };
459
return copy;
460
};
461
return {
462
light: transformBrand(brand.light),
463
dark: transformBrand(brand.dark),
464
};
465
}
466
467
// this a typst workaround but might as well write it as a proper function
468
export function fillLogoPaths(
469
brand: LightDarkBrand | undefined,
470
spec: LogoLightDarkSpecifierPathOptional | undefined,
471
order: BrandNamedLogo[],
472
): LogoLightDarkSpecifier | undefined {
473
function findLogoSize(
474
mode: "light" | "dark",
475
): string | undefined {
476
if (brand?.[mode]) {
477
for (const size of order) {
478
if (brand[mode].processedData.logo[size]) {
479
return size;
480
}
481
}
482
}
483
return undefined;
484
}
485
function resolveMode(
486
mode: "light" | "dark",
487
spec: LogoSpecifierPathOptional | undefined,
488
): LogoSpecifier | undefined {
489
if (!spec) {
490
return undefined;
491
}
492
if (!spec || typeof spec === "string") {
493
return spec;
494
} else if (spec.path) {
495
return spec as LogoOptions;
496
} else {
497
const size = findLogoSize(mode) ||
498
findLogoSize(mode === "light" ? "dark" : "light");
499
if (size) {
500
return {
501
path: size,
502
...spec,
503
};
504
}
505
}
506
return undefined;
507
}
508
if (!spec) {
509
return undefined;
510
}
511
if (typeof spec === "string") {
512
return spec;
513
}
514
if ("light" in spec || "dark" in spec) {
515
return {
516
light: resolveMode("light", spec.light),
517
dark: resolveMode("dark", spec.dark),
518
};
519
}
520
return {
521
light: resolveMode("light", spec as LogoOptionsPathOptional),
522
dark: resolveMode("dark", spec as LogoOptionsPathOptional),
523
};
524
}
525
526
function splitColorLightDark(
527
bcld: BrandColorLightDark,
528
): LightDarkColor {
529
if (typeof bcld === "string") {
530
return { light: bcld, dark: bcld };
531
}
532
return bcld;
533
}
534
535
const enablesDarkMode = (x: BrandColorLightDark | BrandStringLightDark) =>
536
typeof x === "object" && x?.dark;
537
538
export function brandHasDarkMode(brand: BrandUnified): boolean {
539
if (brand.color) {
540
for (const colorName of Zod.BrandNamedThemeColor.options) {
541
if (!brand.color[colorName]) {
542
continue;
543
}
544
if (enablesDarkMode(brand.color![colorName])) {
545
return true;
546
}
547
}
548
}
549
if (brand.typography) {
550
for (const elementName of Zod.BrandNamedTypographyElements.options) {
551
const element = brand.typography[elementName];
552
if (!element || typeof element === "string") {
553
continue;
554
}
555
if (
556
"background-color" in element && element["background-color"] &&
557
enablesDarkMode(element["background-color"])
558
) {
559
return true;
560
}
561
if (
562
"color" in element && element["color"] &&
563
enablesDarkMode(element["color"])
564
) {
565
return true;
566
}
567
}
568
}
569
if (brand.logo) {
570
for (const logoName of Zod.BrandNamedLogo.options) {
571
const logo = brand.logo[logoName];
572
if (!logo || typeof logo === "string") {
573
continue;
574
}
575
if (enablesDarkMode(logo)) {
576
return true;
577
}
578
}
579
}
580
return false;
581
}
582
583
function sharedTypography(
584
unified: BrandTypographyUnified,
585
): BrandTypographySingle {
586
const ret: BrandTypographySingle = {
587
fonts: unified.fonts,
588
};
589
for (const elementName of Zod.BrandNamedTypographyElements.options) {
590
if (!unified[elementName]) {
591
continue;
592
}
593
if (typeof unified[elementName] === "string") {
594
ret[elementName] = unified[elementName];
595
continue;
596
}
597
ret[elementName] = Object.fromEntries(
598
Object.entries(unified[elementName]).filter(
599
([key, _]) => !["color", "background-color"].includes(key),
600
),
601
);
602
}
603
return ret;
604
}
605
606
function splitLogo(
607
unifiedLogo: BrandLogoUnified,
608
): { light: BrandLogoSingle; dark: BrandLogoSingle } {
609
const light: BrandLogoSingle = { images: unifiedLogo.images },
610
dark: BrandLogoSingle = { images: unifiedLogo.images };
611
for (const logoName of Zod.BrandNamedLogo.options) {
612
if (unifiedLogo[logoName]) {
613
if (typeof unifiedLogo[logoName] === "string") {
614
light[logoName] = dark[logoName] = unifiedLogo[logoName];
615
continue;
616
}
617
({ light: light[logoName], dark: dark[logoName] } =
618
unifiedLogo[logoName]);
619
}
620
}
621
return { light, dark };
622
}
623
624
export function splitUnifiedBrand(
625
unified: unknown,
626
brandDir: string,
627
projectDir: string,
628
): LightDarkBrandDarkFlag {
629
const unifiedBrand: BrandUnified = Zod.BrandUnified.parse(unified);
630
let typography: BrandTypographySingle | undefined = undefined;
631
let headingsColor: LightDarkColor | undefined = undefined;
632
let monospaceColor: LightDarkColor | undefined = undefined;
633
let monospaceBackgroundColor: LightDarkColor | undefined = undefined;
634
let monospaceInlineColor: LightDarkColor | undefined = undefined;
635
let monospaceInlineBackgroundColor: LightDarkColor | undefined = undefined;
636
let monospaceBlockColor: LightDarkColor | undefined = undefined;
637
let monospaceBlockBackgroundColor: LightDarkColor | undefined = undefined;
638
let linkColor: LightDarkColor | undefined = undefined;
639
let linkBackgroundColor: LightDarkColor | undefined = undefined;
640
if (unifiedBrand.typography) {
641
typography = sharedTypography(unifiedBrand.typography);
642
if (
643
unifiedBrand.typography.headings &&
644
typeof unifiedBrand.typography.headings !== "string" &&
645
unifiedBrand.typography.headings.color
646
) {
647
headingsColor = splitColorLightDark(
648
unifiedBrand.typography.headings.color,
649
);
650
}
651
if (
652
unifiedBrand.typography.monospace &&
653
typeof unifiedBrand.typography.monospace !== "string"
654
) {
655
if (unifiedBrand.typography.monospace.color) {
656
monospaceColor = splitColorLightDark(
657
unifiedBrand.typography.monospace.color,
658
);
659
}
660
if (unifiedBrand.typography.monospace["background-color"]) {
661
monospaceBackgroundColor = splitColorLightDark(
662
unifiedBrand.typography.monospace["background-color"],
663
);
664
}
665
}
666
if (
667
unifiedBrand.typography["monospace-inline"] &&
668
typeof unifiedBrand.typography["monospace-inline"] !== "string"
669
) {
670
if (unifiedBrand.typography["monospace-inline"].color) {
671
monospaceInlineColor = splitColorLightDark(
672
unifiedBrand.typography["monospace-inline"].color,
673
);
674
}
675
if (unifiedBrand.typography["monospace-inline"]["background-color"]) {
676
monospaceInlineBackgroundColor = splitColorLightDark(
677
unifiedBrand.typography["monospace-inline"]["background-color"],
678
);
679
}
680
}
681
if (
682
unifiedBrand.typography["monospace-block"] &&
683
typeof unifiedBrand.typography["monospace-block"] !== "string"
684
) {
685
if (unifiedBrand.typography["monospace-block"].color) {
686
monospaceBlockColor = splitColorLightDark(
687
unifiedBrand.typography["monospace-block"].color,
688
);
689
}
690
if (unifiedBrand.typography["monospace-block"]["background-color"]) {
691
monospaceBlockBackgroundColor = splitColorLightDark(
692
unifiedBrand.typography["monospace-block"]["background-color"],
693
);
694
}
695
}
696
if (
697
unifiedBrand.typography.link &&
698
typeof unifiedBrand.typography.link !== "string"
699
) {
700
if (unifiedBrand.typography.link.color) {
701
linkColor = splitColorLightDark(
702
unifiedBrand.typography.link.color,
703
);
704
}
705
if (unifiedBrand.typography.link["background-color"]) {
706
linkBackgroundColor = splitColorLightDark(
707
unifiedBrand.typography.link["background-color"],
708
);
709
}
710
}
711
}
712
const specializeTypography = (
713
typography: BrandTypographySingle,
714
mode: "light" | "dark",
715
) =>
716
typography && {
717
fonts: typography.fonts && [...typography.fonts],
718
base: !typography.base || typeof typography.base === "string"
719
? typography.base
720
: { ...typography.base },
721
headings: !typography.headings || typeof typography.headings === "string"
722
? typography.headings
723
: {
724
...typography.headings,
725
...(headingsColor?.[mode] && { color: headingsColor[mode] }),
726
},
727
monospace:
728
!typography.monospace || typeof typography.monospace === "string"
729
? typography.monospace
730
: {
731
...typography.monospace,
732
...(monospaceColor?.[mode] && { color: monospaceColor[mode] }),
733
...(monospaceBackgroundColor?.[mode] &&
734
{ "background-color": monospaceBackgroundColor[mode] }),
735
},
736
"monospace-inline": !typography["monospace-inline"] ||
737
typeof typography["monospace-inline"] === "string"
738
? typography["monospace-inline"]
739
: {
740
...typography["monospace-inline"],
741
...(monospaceInlineColor?.[mode] &&
742
{ color: monospaceInlineColor[mode] }),
743
...(monospaceInlineBackgroundColor?.[mode] &&
744
{ "background-color": monospaceInlineBackgroundColor[mode] }),
745
},
746
"monospace-block": !typography["monospace-block"] ||
747
typeof typography["monospace-block"] === "string"
748
? typography["monospace-block"]
749
: {
750
...typography["monospace-block"],
751
...(monospaceBlockColor?.[mode] &&
752
{ color: monospaceBlockColor[mode] }),
753
...(monospaceBlockBackgroundColor?.[mode] &&
754
{ "background-color": monospaceBlockBackgroundColor[mode] }),
755
},
756
link: !typography.link || typeof typography.link === "string"
757
? typography.link
758
: {
759
...typography.link,
760
...(linkColor?.[mode] && { color: linkColor[mode] }),
761
...(linkBackgroundColor?.[mode] &&
762
{ "background-color": linkBackgroundColor[mode] }),
763
},
764
};
765
const logos = unifiedBrand.logo && splitLogo(unifiedBrand.logo);
766
const lightBrand: BrandSingle = {
767
meta: unifiedBrand.meta,
768
color: { palette: unifiedBrand.color && { ...unifiedBrand.color.palette } },
769
typography: typography && specializeTypography(typography, "light"),
770
logo: logos && logos.light,
771
defaults: unifiedBrand.defaults,
772
};
773
const darkBrand: BrandSingle = {
774
meta: unifiedBrand.meta,
775
color: { palette: unifiedBrand.color && { ...unifiedBrand.color.palette } },
776
typography: typography && specializeTypography(typography, "dark"),
777
logo: logos && logos.dark,
778
defaults: unifiedBrand.defaults,
779
};
780
if (unifiedBrand.color) {
781
for (const colorName of Zod.BrandNamedThemeColor.options) {
782
if (!unifiedBrand.color[colorName]) {
783
continue;
784
}
785
const { light, dark } = splitColorLightDark(
786
unifiedBrand.color[colorName],
787
);
788
789
if (light !== undefined) lightBrand.color![colorName] = light;
790
if (dark !== undefined) darkBrand.color![colorName] = dark;
791
}
792
}
793
return {
794
light: new Brand(lightBrand, brandDir, projectDir),
795
dark: new Brand(darkBrand, brandDir, projectDir),
796
enablesDarkMode: brandHasDarkMode(unifiedBrand),
797
};
798
}
799
800