diff --git a/docs/config.md b/docs/config.md index 403683c44..693dcafad 100644 --- a/docs/config.md +++ b/docs/config.md @@ -27,16 +27,70 @@ The path to the source root; defaults to `docs`. The path to the output root; defaults to `dist`. +## theme + +The theme names, if any; defaults to `auto`. Themes affect the visual appearance of pages by specifying colors and fonts, and possibly by augmenting default styles. The theme option is a convenient shorthand alternative to specifying a [custom stylesheet](#style). + +The current built-in themes are: + +- *auto* (default) - *light* or *dark* depending on the user’s preferred color scheme +- *auto-alt* - *light-alt* or *dark* depending on the user’s preferred color scheme +- *light* - light mode +- *dark* - dark mode +- *wide* - allows the main column to go full width; to be used with one of the above + +You can combine themes like so: + +```js +theme: ["auto-alt", "wide"] +``` + +A theme can be also configured for individual pages via the [front matter](./markdown.md#front-matter): + +```yaml +--- +theme: [auto-alt, wide] +--- +``` + ## style -The path to the project’s stylesheet. This is typically set to `docs/style.css` to override or augment the default stylesheet, or to apply a theme. For example, to use the *light* theme: +The path to a custom stylesheet. This option takes precedence over [themes](#theme) (if any), providing more control by allowing you to remove or alter the default stylesheet and define a custom theme. + +The custom stylesheet should typically import the `"observablehq:default.css"` to build on the default styles. You can also import any of the built-in themes. For example, to create a stylesheet that builds up on the *light* theme, create a `custom-style.css` file in the `docs` folder, then set the **style** option to `"custom-style.css"`: ```css @import url("observablehq:theme-light.css"); @import url("observablehq:default.css"); + +:root { + --theme-foreground-focus: green; +} ``` -The current built-in themes are: *auto* (default), *light*, and *dark*. +If you build on the *auto* or *auto-alt* themes, make sure that colors are chosen according to the user’s [preferred color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). + +The default styles are implemented using CSS custom properties. These properties are designed to be defined by themes or custom stylesheets. The following custom properties are supported: + +- `--theme-foreground` - foreground color, _e.g._ black +- `--theme-background` - background color, _e.g._ white +- `--theme-background-alt` - alternative background color, _e.g._ light gray +- `--theme-foreground-alt` - alternative foreground color, used for titles and section titles, _e.g._ brown +- `--theme-foreground-muted` - muted foreground color, _e.g._ dark gray +- `--theme-foreground-faint` - faint foreground color, _e.g._ middle gray +- `--theme-foreground-fainter` - fainter foreground color, _e.g._ light gray +- `--theme-foreground-faintest` - fainter foreground color, _e.g._ almost white +- `--theme-foreground-focus` - focus color, _e.g._ blue + +The style property is also configurable for a single page by indicating its relative path in the front-matter: + +```yaml +--- +style: custom-style.css +--- +``` + +In this case, the path to the stylesheet is resolved relative to the page’s Markdown file rather than the config file. ## title @@ -62,7 +116,6 @@ export interface Section { } ``` - If a section’s **open** option is not set, it defaults to true. Projects can have “unlisted” pages that are not included in the pages list. These pages will still be accessible if linked from other pages or visited directly, but they won’t be listed in the sidebar or linked to via the previous & next footer. diff --git a/src/build.ts b/src/build.ts index 37b18068a..6a8b28470 100644 --- a/src/build.ts +++ b/src/build.ts @@ -83,7 +83,7 @@ export async function build( files.push(...render.files.map(resolveFile)); imports.push(...render.imports.filter((i) => i.type === "local").map(resolveFile)); await effects.writeFile(outputPath, render.html); - const style = mergeStyle(render.data?.style, render.data?.theme, config.style); + const style = mergeStyle(path, render.data?.style, render.data?.theme, config.style); if (style) { if ("path" in style) style.path = resolvePath(sourceFile, style.path); if (!styles.some((s) => styleEquals(s, style))) styles.push(style); diff --git a/src/config.ts b/src/config.ts index 795fc5b13..2c5171ec3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,7 @@ import {readFile} from "node:fs/promises"; import {basename, dirname, extname, join} from "node:path"; import {visitFiles} from "./files.js"; import {parseMarkdown} from "./markdown.js"; +import {resolvePath} from "./url.js"; export interface Page { name: string; @@ -118,12 +119,12 @@ export function mergeToc(spec: any, toc: TableOfContents): TableOfContents { return {label, show}; } -export function mergeStyle(style: any, theme: any, defaultStyle: null | Style): null | Style { +export function mergeStyle(path: string, style: any, theme: any, defaultStyle: null | Style): null | Style { return style === undefined && theme === undefined ? defaultStyle : style === null ? null // disable : style !== undefined - ? {path: String(style)} // TODO resolve path? + ? {path: resolvePath(path, style)} : {theme: normalizeTheme(theme)}; } diff --git a/src/preview.ts b/src/preview.ts index 9f56e1100..6f864a457 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -22,7 +22,7 @@ import type {ParseResult, ReadMarkdownResult} from "./markdown.js"; import {renderPreview} from "./render.js"; import {bundleStyles, getClientPath, rollupClient} from "./rollup.js"; import {bold, faint, green, underline} from "./tty.js"; -import {relativeUrl, resolvePath} from "./url.js"; +import {relativeUrl} from "./url.js"; const publicRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "public"); @@ -257,11 +257,11 @@ function getWatchPaths(parseResult: ParseResult): string[] { } export function getPreviewStylesheet(path: string, data: ParseResult["data"], style: Config["style"]): string | null { - style = mergeStyle(data?.style, data?.theme, style); + style = mergeStyle(path, data?.style, data?.theme, style); return !style ? null : "path" in style - ? relativeUrl(path, `/_import/${resolvePath(path, style.path)}`) + ? relativeUrl(path, `/_import/${style.path}`) : relativeUrl(path, `/_observablehq/theme-${style.theme.join(",")}.css`); }