Skip to content

Commit 70e7df7

Browse files
feature: dark brand
enable dark brand in brand sass bundles; do not exempt brand when creating dark sass bundles enable toggling when brand is multi and includes dark (as previously for theme)
1 parent c6994ae commit 70e7df7

File tree

20 files changed

+730
-92
lines changed

20 files changed

+730
-92
lines changed

src/command/render/pandoc-html.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,28 +88,45 @@ export async function resolveSassBundles(
8888
// the brand bundle itself doesn't have any 'brand' entries;
8989
// those are used to specify where the brand-specific layers should be inserted
9090
// in the final bundle.
91-
const brandLayersMaybeBrand = bundlesWithBrand.find((bundle) =>
91+
const maybeBrandBundle = bundlesWithBrand.find((bundle) =>
9292
bundle.key === "brand"
93-
)?.user || [];
94-
assert(!brandLayersMaybeBrand.find((v) => v === "brand"));
95-
const brandLayers = brandLayersMaybeBrand as SassLayer[];
96-
let foundBrand = false;
93+
);
94+
assert(!maybeBrandBundle ||
95+
!maybeBrandBundle.user?.find((v) => v === "brand") &&
96+
!maybeBrandBundle.dark?.user?.find((v) => v === "brand"));
97+
let foundBrand = {light: false, dark: false};
9798
const bundles: SassBundle[] = bundlesWithBrand.filter((bundle) =>
9899
bundle.key !== "brand"
99100
).map((bundle) => {
100101
const userBrand = bundle.user?.findIndex((layer) => layer === "brand");
102+
let cloned = false;
101103
if (userBrand && userBrand !== -1) {
104+
// console.log('light brand order specified', userBrand, cloned);
102105
bundle = cloneDeep(bundle);
103-
bundle.user!.splice(userBrand, 1, ...brandLayers);
104-
foundBrand = true;
106+
cloned = true;
107+
bundle.user!.splice(userBrand, 1, ...(maybeBrandBundle?.user || []));
108+
foundBrand.light = true;
109+
}
110+
const darkBrand = bundle.dark?.user?.findIndex((layer) => layer === "brand");
111+
if (darkBrand && darkBrand !== -1) {
112+
// console.log('dark brand order specified', darkBrand, cloned);
113+
if (!cloned) {
114+
bundle = cloneDeep(bundle);
115+
}
116+
bundle.dark!.user!.splice(darkBrand, 1, ...(maybeBrandBundle?.dark?.user || []))
117+
foundBrand.dark = true;
105118
}
106119
return bundle as SassBundle;
107120
});
108-
if (!foundBrand) {
121+
if (!foundBrand.light || !foundBrand.dark) {
109122
bundles.unshift({
110123
dependency,
111124
key: "brand",
112-
user: brandLayers,
125+
user: !foundBrand.light && maybeBrandBundle?.user as SassLayer[] || [],
126+
dark: !foundBrand.dark && maybeBrandBundle?.dark?.user && {
127+
user: maybeBrandBundle.dark.user as SassLayer[],
128+
default: maybeBrandBundle.dark.default
129+
} || undefined
113130
});
114131
}
115132

src/command/render/pandoc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,7 @@ async function resolveExtras(
14671467

14681468
// perform typst-specific merging
14691469
if (isTypstOutput(format.pandoc)) {
1470-
const brand = await project.resolveBrand(input);
1470+
const brand = (await project.resolveBrand(input))?.light;
14711471
const fontdirs: Set<string> = new Set();
14721472
const base_urls = {
14731473
google: "https://fonts.googleapis.com/css",

src/command/render/render-contexts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,11 +614,11 @@ async function resolveFormats(
614614

615615
// resolve brand in project and forward it to format
616616
const brand = await project.resolveBrand(target.source);
617-
mergedFormats[format].render.brand = brand;
617+
mergedFormats[format].render.brand = brand?.light;
618618

619619
// apply defaults from brand yaml under the metadata of the current format
620620
const brandFormatDefaults: Metadata =
621-
(brand?.data?.defaults?.quarto as unknown as Record<
621+
(brand?.light?.data?.defaults?.quarto as unknown as Record<
622622
string,
623623
Record<string, Metadata>
624624
>)?.format

src/core/brand/brand.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,11 @@ export class Brand {
284284
}
285285
}
286286

287+
export type LightDarkBrand = {
288+
light?: Brand;
289+
dark?: Brand;
290+
};
291+
287292
export const getFavicon = (brand: Brand): string | undefined => {
288293
const logoInfo = brand.getLogo("small");
289294
if (!logoInfo) {

src/core/sass/brand.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
BrandFontWeight,
2323
} from "../../resources/types/schema-types.ts";
2424
import { Brand } from "../brand/brand.ts";
25+
import {
26+
darkModeDefault,
27+
} from "../../format/html/format-html-info.ts";
28+
2529

2630
const defaultColorNameMap: Record<string, string> = {
2731
"link-color": "link",
@@ -75,7 +79,10 @@ export async function brandBootstrapSassBundles(
7579
return [{
7680
key,
7781
dependency: "bootstrap",
78-
user: layers,
82+
user: layers.light,
83+
dark: {
84+
user: layers.dark
85+
}
7986
}];
8087
}
8188

@@ -559,29 +566,42 @@ const brandTypographyLayer = (
559566
};
560567
};
561568

569+
export interface LightDarkSassLayers {
570+
light: SassLayer[];
571+
dark: SassLayer[];
572+
}
573+
562574
export async function brandSassLayers(
563575
fileName: string | undefined,
564576
project: ProjectContext,
565577
nameMap: Record<string, string> = {},
566-
): Promise<SassLayer[]> {
578+
): Promise<LightDarkSassLayers> {
567579
const brand = await project.resolveBrand(fileName);
568-
const sassLayers: SassLayer[] = [];
580+
const sassLayers: LightDarkSassLayers = {
581+
light: [],
582+
dark: []
583+
};
569584

570-
if (brand) {
571-
sassLayers.push({
585+
for (const layer of [sassLayers.light, sassLayers.dark]) {
586+
layer.push({
572587
defaults: '$theme: "brand" !default;',
573588
uses: "",
574589
functions: "",
575590
mixins: "",
576591
rules: "",
577592
});
578593
}
579-
if (brand?.data.color) {
580-
sassLayers.push(brandColorLayer(brand, nameMap));
594+
if (brand?.light?.data.color) {
595+
sassLayers.light.push(brandColorLayer(brand?.light, nameMap));
581596
}
582-
583-
if (brand?.data.typography) {
584-
sassLayers.push(brandTypographyLayer(brand));
597+
if (brand?.dark?.data.color) {
598+
sassLayers.dark.push(brandColorLayer(brand?.dark, nameMap));
599+
}
600+
if (brand?.light?.data.typography) {
601+
sassLayers.light.push(brandTypographyLayer(brand?.light));
602+
}
603+
if (brand?.dark?.data.typography) {
604+
sassLayers.dark.push(brandTypographyLayer(brand?.dark));
585605
}
586606

587607
return sassLayers;
@@ -591,16 +611,19 @@ export async function brandBootstrapSassLayers(
591611
fileName: string | undefined,
592612
project: ProjectContext,
593613
nameMap: Record<string, string> = {},
594-
): Promise<SassLayer[]> {
614+
): Promise<LightDarkSassLayers> {
595615
const layers = await brandSassLayers(
596616
fileName,
597617
project,
598618
nameMap,
599619
);
600620

601621
const brand = await project.resolveBrand(fileName);
602-
if (brand?.data?.defaults?.bootstrap) {
603-
layers.unshift(brandDefaultsBootstrapLayer(brand));
622+
if (brand?.light?.data?.defaults?.bootstrap) {
623+
layers.light.unshift(brandDefaultsBootstrapLayer(brand.light));
624+
}
625+
if (brand?.dark?.data?.defaults?.bootstrap) {
626+
layers.dark.unshift(brandDefaultsBootstrapLayer(brand.dark));
604627
}
605628

606629
return layers;
@@ -611,16 +634,16 @@ export async function brandRevealSassLayers(
611634
_format: Format,
612635
project: ProjectContext,
613636
): Promise<SassLayer[]> {
614-
return brandSassLayers(
637+
return (await brandSassLayers(
615638
input,
616639
project,
617640
defaultColorNameMap,
618-
);
641+
)).light;
619642
}
620643

621644
export async function brandSassFormatExtras(
622645
input: string | undefined,
623-
_format: Format,
646+
format: Format,
624647
project: ProjectContext,
625648
): Promise<FormatExtras> {
626649
const htmlSassBundleLayers = await brandBootstrapSassLayers(
@@ -634,7 +657,11 @@ export async function brandSassFormatExtras(
634657
{
635658
key: "brand",
636659
dependency: "bootstrap",
637-
user: htmlSassBundleLayers,
660+
user: htmlSassBundleLayers.light,
661+
dark: {
662+
user: htmlSassBundleLayers.dark,
663+
default: darkModeDefault(format.metadata)
664+
}
638665
},
639666
],
640667
},

src/format/html/format-html-bootstrap.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ import {
7878
documentTitleScssLayer,
7979
processDocumentTitle,
8080
} from "./format-html-title.ts";
81+
import {
82+
darkModeDefault,
83+
} from "./format-html-info.ts";
84+
8185
import { kTemplatePartials } from "../../command/render/template.ts";
8286
import { isHtmlOutput } from "../../config/format.ts";
8387
import { emplaceNotebookPreviews } from "./format-html-notebook.ts";
@@ -1062,13 +1066,8 @@ function bootstrapHtmlFinalizer(format: Format, flags: PandocFlags) {
10621066

10631067
// start body with light or dark class for proper display when JS is disabled
10641068
let initialLightDarkClass = "quarto-light";
1065-
// some logic duplicated from resolveThemeLayer
1066-
const theme = format.metadata.theme;
1067-
if (theme && !Array.isArray(theme) && typeof theme === "object") {
1068-
const keys = Object.keys(theme);
1069-
if(keys.length > 1 && keys[0] === "dark") {
1070-
initialLightDarkClass = "quarto-dark";
1071-
}
1069+
if (darkModeDefault(format.metadata)) {
1070+
initialLightDarkClass = "quarto-dark";
10721071
}
10731072
doc.body.classList.add(initialLightDarkClass);
10741073

src/format/html/format-html-info.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Copyright (C) 2020-2022 Posit Software, PBC
77
*/
88

9-
import { kTheme } from "../../config/constants.ts";
9+
import { kTheme, kBrand } from "../../config/constants.ts";
1010
import { isHtmlDashboardOutput, isHtmlOutput } from "../../config/format.ts";
1111
import { Format, Metadata } from "../../config/types.ts";
1212

@@ -40,14 +40,15 @@ export function formatDarkMode(format: Format): boolean | undefined {
4040

4141
export function darkModeDefault(metadata?: Metadata): boolean | undefined {
4242
if (metadata !== undefined) {
43-
const theme = metadata[kTheme];
44-
if (theme && typeof (theme) === "object") {
45-
const keys = Object.keys(theme);
46-
if (keys.includes("dark")) {
47-
if (keys[0] === "dark") {
48-
return true;
49-
} else {
50-
return false;
43+
for (const darkable of [metadata[kTheme], metadata[kBrand]]) {
44+
if (darkable && typeof (darkable) === "object") {
45+
const keys = Object.keys(darkable);
46+
if (keys.includes("dark")) {
47+
if (keys[0] === "dark") {
48+
return true;
49+
} else {
50+
return false;
51+
}
5152
}
5253
}
5354
}

src/project/project-shared.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { normalizeNewlines } from "../core/lib/text.ts";
4747
import { DirectiveCell } from "../core/lib/break-quarto-md-types.ts";
4848
import { QuartoJSONSchema, readYamlFromMarkdown } from "../core/yaml.ts";
4949
import { refSchema } from "../core/lib/yaml-schema/common.ts";
50-
import { Brand as BrandJson } from "../resources/types/schema-types.ts";
50+
import { Brand as BrandJson, BrandPathBoolLightDark } from "../resources/types/schema-types.ts";
5151
import { Brand } from "../core/brand/brand.ts";
5252
import { warnOnce } from "../core/log.ts";
5353
import { assert } from "testing/asserts";
@@ -512,7 +512,24 @@ export const ensureFileInformationCache = (
512512
export async function projectResolveBrand(
513513
project: ProjectContext,
514514
fileName?: string,
515-
) {
515+
) : Promise<{light?: Brand, dark?: Brand} | undefined> {
516+
async function loadBrand(brandPath: string) : Promise<Brand> {
517+
const brand = await readAndValidateYamlFromFile(
518+
brandPath,
519+
refSchema("brand", "Format-independent brand configuration."),
520+
"Brand validation failed for " + brandPath + ".",
521+
) as BrandJson;
522+
return new Brand(brand, dirname(brandPath), project.dir);
523+
}
524+
async function loadLocalBrand(brandPath: string) : Promise<Brand> {
525+
let resolved: string = "";
526+
if (brandPath.startsWith("/")) {
527+
resolved = join(project.dir, brandPath);
528+
} else {
529+
resolved = join(dirname(fileName!), brandPath);
530+
}
531+
return await loadBrand(resolved);
532+
}
516533
if (fileName === undefined) {
517534
if (project.brandCache) {
518535
return project.brandCache.brand;
@@ -538,46 +555,59 @@ export async function projectResolveBrand(
538555
refSchema("brand", "Format-independent brand configuration."),
539556
"Brand validation failed for " + brandPath + ".",
540557
) as BrandJson;
541-
project.brandCache.brand = new Brand(
558+
project.brandCache.brand = {light: new Brand(
542559
brand,
543560
dirname(brandPath),
544561
project.dir,
545-
);
562+
)};
546563
}
547564
return project.brandCache.brand;
548565
} else {
549566
const metadata = await project.fileMetadata(fileName);
550-
if (metadata.brand === false) {
567+
const brand = metadata.brand as BrandPathBoolLightDark;
568+
if (brand === false) {
551569
return undefined;
552570
}
553-
if (metadata.brand === true || metadata.brand === undefined) {
571+
if (brand === true || brand === undefined) {
554572
return project.resolveBrand();
555573
}
556574
const fileInformation = ensureFileInformationCache(project, fileName);
557575
if (fileInformation.brand) {
558576
return fileInformation.brand;
559577
}
560-
if (typeof metadata.brand === "string") {
561-
let brandPath: string = "";
562-
if (brandPath.startsWith("/")) {
563-
brandPath = join(project.dir, metadata.brand);
564-
} else {
565-
brandPath = join(dirname(fileName), metadata.brand);
566-
}
567-
const brand = await readAndValidateYamlFromFile(
568-
brandPath,
569-
refSchema("brand", "Format-independent brand configuration."),
570-
"Brand validation failed for " + brandPath + ".",
571-
) as BrandJson;
572-
fileInformation.brand = new Brand(brand, dirname(brandPath), project.dir);
578+
if (typeof brand === "string") {
579+
fileInformation.brand = {light: await loadLocalBrand(brand)};
573580
return fileInformation.brand;
574581
} else {
575-
assert(typeof metadata.brand === "object");
576-
fileInformation.brand = new Brand(
577-
metadata.brand as BrandJson,
578-
dirname(fileName),
579-
project.dir,
580-
);
582+
assert(typeof brand === "object");
583+
if ("light" in brand || "dark" in brand) {
584+
let light, dark;
585+
if (typeof brand.light === "string") {
586+
light = await loadLocalBrand(brand.light)
587+
} else {
588+
light = new Brand(
589+
brand.light!,
590+
dirname(fileName),
591+
project.dir
592+
);
593+
}
594+
if (typeof brand.dark === "string") {
595+
dark = await loadLocalBrand(brand.dark)
596+
} else {
597+
dark = new Brand(
598+
brand.dark!,
599+
dirname(fileName),
600+
project.dir
601+
);
602+
}
603+
fileInformation.brand = {light, dark};
604+
} else {
605+
fileInformation.brand = {light: new Brand(
606+
brand as BrandJson,
607+
dirname(fileName),
608+
project.dir,
609+
)};
610+
}
581611
return fileInformation.brand;
582612
}
583613
}

0 commit comments

Comments
 (0)