summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Swithinbank2023-08-10 13:02:50 +0200
committerGitHub2023-08-10 13:02:50 +0200
commitd076aec856921c2fe8a5204a0c31580a846af180 (patch)
treec124f63fe4bd56f45861e38d8fb8a5e01b23a3f3
parentdf24060936c18c9973afde4edd347a191edb6242 (diff)
downloadIT.starlight-d076aec856921c2fe8a5204a0c31580a846af180.tar.gz
IT.starlight-d076aec856921c2fe8a5204a0c31580a846af180.tar.bz2
IT.starlight-d076aec856921c2fe8a5204a0c31580a846af180.zip
Add theme editor to styling guide (#470)
-rw-r--r--.changeset/red-rockets-turn.md26
-rw-r--r--docs/package.json2
-rw-r--r--docs/src/components/theme-designer.astro147
-rw-r--r--docs/src/components/theme-designer/atom.ts31
-rw-r--r--docs/src/components/theme-designer/color-editor.astro93
-rw-r--r--docs/src/components/theme-designer/color-lib.ts90
-rw-r--r--docs/src/components/theme-designer/palette.astro41
-rw-r--r--docs/src/components/theme-designer/presets.astro94
-rw-r--r--docs/src/components/theme-designer/preview.astro53
-rw-r--r--docs/src/components/theme-designer/store.ts47
-rw-r--r--docs/src/components/theme-designer/value-slider.astro62
-rw-r--r--docs/src/content/docs/guides/css-and-tailwind.mdx59
-rw-r--r--packages/starlight/components/Icons.ts2
-rw-r--r--packages/starlight/style/props.css43
-rw-r--r--pnpm-lock.yaml15
15 files changed, 781 insertions, 24 deletions
diff --git a/.changeset/red-rockets-turn.md b/.changeset/red-rockets-turn.md
new file mode 100644
index 00000000..de6bbe44
--- /dev/null
+++ b/.changeset/red-rockets-turn.md
@@ -0,0 +1,26 @@
+---
+'@astrojs/starlight': minor
+---
+
+Drop support for the `--sl-hue-accent` CSS custom property.
+
+⚠️ **BREAKING CHANGE** — In previous Starlight versions you could control the accent color by setting the `--sl-hue-accent` custom property. This could result in inaccessible color contrast and unpredictable results.
+
+You must now set accent colors directly. If you relied on setting `--sl-hue-accent`, migrate by setting light and dark mode colors in your custom CSS:
+
+```css
+:root {
+ --sl-hue-accent: 234;
+ --sl-color-accent-low: hsl(var(--sl-hue-accent), 54%, 20%);
+ --sl-color-accent: hsl(var(--sl-hue-accent), 100%, 60%);
+ --sl-color-accent-high: hsl(var(--sl-hue-accent), 100%, 87%);
+}
+
+:root[data-theme="light"] {
+ --sl-color-accent-high: hsl(var(--sl-hue-accent), 80%, 30%);
+ --sl-color-accent: hsl(var(--sl-hue-accent), 90%, 60%);
+ --sl-color-accent-low: hsl(var(--sl-hue-accent), 88%, 90%);
+}
+```
+
+The [new color theme editor](https://starlight.astro.build/guides/css-and-tailwind/#color-theme-editor) might help if you’d prefer to set a new color scheme. \ No newline at end of file
diff --git a/docs/package.json b/docs/package.json
index bcfc4904..bed55508 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -14,7 +14,9 @@
},
"dependencies": {
"@astrojs/starlight": "workspace:*",
+ "@types/culori": "^2.0.0",
"astro": "^2.10.4",
+ "culori": "^3.2.0",
"sharp": "^0.32.3"
},
"devDependencies": {
diff --git a/docs/src/components/theme-designer.astro b/docs/src/components/theme-designer.astro
new file mode 100644
index 00000000..b8bd3b31
--- /dev/null
+++ b/docs/src/components/theme-designer.astro
@@ -0,0 +1,147 @@
+---
+import { TabItem, Tabs } from '@astrojs/starlight/components';
+import ColorEditor, { Props as EditorProps } from './theme-designer/color-editor.astro';
+import Presets, { Props as PresetsProps } from './theme-designer/presets.astro';
+import Preview from './theme-designer/preview.astro';
+
+interface Props {
+ labels: {
+ presets: PresetsProps['labels'];
+ editor: EditorProps['labels'] & { accentColor: string; grayColor: string };
+ preview: Record<
+ 'darkMode' | 'lightMode' | 'bodyText' | 'linkText' | 'dimText' | 'inlineCode',
+ string
+ >;
+ };
+}
+const {
+ labels: { presets, editor, preview },
+} = Astro.props;
+---
+
+<Presets labels={presets} />
+
+<div>
+ <theme-designer>
+ <div class="flex controls not-content">
+ <ColorEditor key="accent" legend={editor.accentColor} labels={editor} />
+ <ColorEditor key="gray" legend={editor.grayColor} labels={editor} />
+ </div>
+
+ <div class="preview" data-accent-preview>
+ <Preview labels={preview} data-dark />
+ <Preview labels={preview} data-light />
+ </div>
+
+ <Tabs>
+ <TabItem label="CSS">
+ <slot name="css-docs" />
+ <pre class="generated-code" tabindex="0"><code style="background-color: var(--astro-code-color-background);color: var(--sl-color-text)" data-theme-css /></pre>
+ </TabItem>
+ <TabItem label="Tailwind">
+ <slot name="tailwind-docs" />
+ <pre class="generated-code" tabindex="0"><code style="background-color: var(--astro-code-color-background);color: var(--sl-color-text)" data-theme-tailwind /></pre>
+ </TabItem>
+ </Tabs>
+ </theme-designer>
+</div>
+
+<script>
+ import { getPalettes } from './theme-designer/color-lib';
+ import { store } from './theme-designer/store';
+
+ class ThemeDesigner extends HTMLElement {
+ #stylesheet = new CSSStyleSheet();
+
+ constructor() {
+ super();
+ // Add our stylesheet to the document.
+ document.adoptedStyleSheets = [...document.adoptedStyleSheets, this.#stylesheet];
+ // Update theme palettes on user input.
+ const onInput = () => this.#update();
+ store.accent.subscribe(onInput);
+ store.gray.subscribe(onInput);
+ }
+
+ #update() {
+ const palettes = getPalettes({ accent: store.accent.get(), gray: store.gray.get() });
+ this.#updatePreview(palettes);
+ this.#updateStylesheet(palettes);
+ this.#updateTailwindConfig(palettes);
+ }
+
+ /** Apply the generated palettes to the style attributes of the in-content preview panes. */
+ #updatePreview({ dark, light }: ReturnType<typeof getPalettes>) {
+ const previews = this.querySelectorAll<HTMLDivElement>('[data-accent-preview] > *');
+ previews.forEach((preview) => {
+ const theme = 'dark' in preview.dataset ? dark : light;
+ Object.entries(theme).forEach(([key, color]) => {
+ preview.style.setProperty(`--sl-color-${key}`, color);
+ });
+ });
+ }
+
+ /** Convert a color palette into a string of CSS rules. */
+ #paletteToRules(palette: ReturnType<typeof getPalettes>['dark' | 'light']) {
+ return Object.entries(palette)
+ .map(([key, color]) => `--sl-color-${key}: ${color};`)
+ .join('\n\t');
+ }
+
+ /** Update the CSS stylesheet applied to the current page and offered to users to copy. */
+ #updateStylesheet({ dark, light }: ReturnType<typeof getPalettes>) {
+ const styles = `/* Dark mode colors. */
+:root {\n\t${this.#paletteToRules(dark)}\n}
+/* Light mode colors. */
+:root[data-theme='light'] {\n\t${this.#paletteToRules(light)}\n}`;
+ this.#stylesheet.replaceSync(styles);
+ const codePreview = this.querySelector('[data-theme-css]');
+ if (codePreview) codePreview.innerHTML = styles;
+ }
+
+ #updateTailwindConfig({ dark, light }: ReturnType<typeof getPalettes>) {
+ const config = `const starlightPlugin = require('@astrojs/starlight-tailwind');
+
+// Generated color palettes
+const accent = { 200: '${dark['accent-high']}', 600: '${light.accent}', 900: '${light['accent-high']}', 950: '${dark['accent-low']}' };
+const gray = { 100: '${light['gray-7']}', 200: '${light['gray-6']}', 300: '${light['gray-5']}', 400: '${light['gray-4']}', 500: '${light['gray-3']}', 700: '${light['gray-2']}', 800: '${light['gray-1']}', 900: '${light.white}' };
+
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+ theme: {
+ extend: {
+ colors: { accent, gray },
+ },
+ },
+ plugins: [starlightPlugin()],
+};`;
+ const codePreview = this.querySelector('[data-theme-tailwind]');
+ if (codePreview) codePreview.innerHTML = config;
+ }
+ }
+
+ customElements.define('theme-designer', ThemeDesigner);
+</script>
+
+<style>
+ .controls {
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ }
+ .controls > :global(*) {
+ flex: 1 1;
+ }
+ .preview {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1.5rem;
+ }
+
+ .generated-code {
+ height: 16rem;
+ background-color: var(--astro-code-color-background);
+ overflow: auto scroll;
+ user-select: all;
+ }
+</style>
diff --git a/docs/src/components/theme-designer/atom.ts b/docs/src/components/theme-designer/atom.ts
new file mode 100644
index 00000000..e1f2518c
--- /dev/null
+++ b/docs/src/components/theme-designer/atom.ts
@@ -0,0 +1,31 @@
+class Atom<T> {
+ #v: T;
+ #subscribers = new Map<(v: T) => void, (v: T) => void>();
+ #notify = () => this.#subscribers.forEach((cb) => cb(this.#v));
+ constructor(init: T) {
+ this.#v = init;
+ }
+ get(): T {
+ return this.#v;
+ }
+ set(v: T): void {
+ this.#v = v;
+ this.#notify();
+ }
+ subscribe(cb: (v: T) => void): () => boolean {
+ cb(this.#v);
+ this.#subscribers.set(cb, cb);
+ return () => this.#subscribers.delete(cb);
+ }
+}
+
+type MapStore<T> = Atom<T> & { setKey: (key: keyof T, value: T[typeof key]) => void };
+
+export function map<T extends Record<string, unknown>>(value: T): MapStore<T> {
+ const atom = new Atom(value) as MapStore<T>;
+ atom.setKey = (key: keyof T, value: T[typeof key]) => {
+ const curr = atom.get();
+ if (curr[key] !== value) atom.set({ ...curr, [key]: value });
+ };
+ return atom;
+}
diff --git a/docs/src/components/theme-designer/color-editor.astro b/docs/src/components/theme-designer/color-editor.astro
new file mode 100644
index 00000000..e91557ed
--- /dev/null
+++ b/docs/src/components/theme-designer/color-editor.astro
@@ -0,0 +1,93 @@
+---
+import { oklchToHex } from './color-lib';
+import { store } from './store';
+import ValueSlider from './value-slider.astro';
+
+export interface Props {
+ key: keyof typeof store;
+ legend: string;
+ labels: Record<'pickColor' | 'hue' | 'chroma', string>;
+}
+const { key, legend, labels } = Astro.props;
+const { hue, chroma } = store[key].get();
+const initialColor = oklchToHex(52, chroma, hue);
+---
+
+<color-editor data-key={key}>
+ <fieldset>
+ <legend>{legend}</legend>
+ <label class="color-picker">
+ <span class="sr-only">{labels.pickColor}</span>
+ <input type="color" value={initialColor} />
+ </label>
+ <div class="sliders">
+ <ValueSlider label={labels.hue} storeKey={key} type="hue" />
+ <ValueSlider label={labels.chroma} storeKey={key} type="chroma" />
+ </div>
+ </fieldset>
+</color-editor>
+
+<script>
+ import { oklch, oklchToHex } from './color-lib';
+ import { store } from './store';
+
+ export class ColorEditor extends HTMLElement {
+ #store = store[this.dataset.key as keyof typeof store];
+ #colorInput = this.querySelector<HTMLInputElement>('input[type="color"]')!;
+
+ constructor() {
+ super();
+ // Update color on user input.
+ this.#store.subscribe(({ chroma, hue }) => {
+ this.#colorInput.value = oklchToHex(52, chroma, hue);
+ });
+ this.#colorInput.addEventListener('input', (e) => {
+ if (!(e.target instanceof HTMLInputElement)) return;
+ const old = this.#store.get();
+ const { c, h } = { ...oklch(e.target.value) };
+ this.#store.set({ hue: h ?? old.hue, chroma: c ?? old.chroma });
+ });
+ }
+ }
+ customElements.define('color-editor', ColorEditor);
+</script>
+
+<style>
+ fieldset {
+ border: 1px solid var(--sl-color-gray-5);
+ background-color: var(--sl-color-gray-7, var(--sl-color-gray-6));
+ padding: 1rem;
+ color: var(--sl-color-white);
+ }
+ legend {
+ float: left;
+ float: inline-start;
+ font-weight: 600;
+ }
+ .color-picker {
+ float: right;
+ float: inline-end;
+ }
+ .sliders {
+ clear: both;
+ }
+ input[type='color'] {
+ border: 0;
+ background: transparent;
+ height: 2em;
+ width: 3rem;
+ cursor: pointer;
+ --swatch-border: var(--sl-color-gray-3);
+ }
+ input[type='color']:hover {
+ --swatch-border: var(--sl-color-gray-1);
+ }
+ input[type='color']::-webkit-color-swatch {
+ border: 1px solid var(--swatch-border);
+ border-radius: 0.5rem;
+ }
+ input[type='color']::-moz-color-swatch {
+ border: 1px solid var(--swatch-border);
+ border-radius: 0.5rem;
+ }
+</style>
diff --git a/docs/src/components/theme-designer/color-lib.ts b/docs/src/components/theme-designer/color-lib.ts
new file mode 100644
index 00000000..27b52b52
--- /dev/null
+++ b/docs/src/components/theme-designer/color-lib.ts
@@ -0,0 +1,90 @@
+import { useMode, modeOklch, modeRgb, formatHex, clampChroma } from 'culori/fn';
+
+const rgb = useMode(modeRgb);
+export const oklch = useMode(modeOklch);
+
+/** Convert an OKLCH color to an RGB hex code. */
+export const oklchToHex = (l: number, c: number, h: number) => {
+ const okLchColor = oklch(`oklch(${l}% ${c} ${h})`)!;
+ const rgbColor = rgb(clampChroma(okLchColor, 'oklch'));
+ return formatHex(rgbColor);
+};
+
+/** Generate dark and light palettes based on user-selected hue and chroma values. */
+export function getPalettes(config: {
+ accent: { hue: number; chroma: number };
+ gray: { hue: number; chroma: number };
+}) {
+ const {
+ accent: { hue: ah, chroma: ac },
+ gray: { hue: gh, chroma: gc },
+ } = config;
+ return {
+ dark: {
+ // Accents
+ 'accent-low': oklchToHex(25.94, ac / 3, ah),
+ accent: oklchToHex(52.28, ac, ah),
+ 'accent-high': oklchToHex(83.38, ac / 3, ah),
+ // Grays
+ white: oklchToHex(100, 0, 0),
+ 'gray-1': oklchToHex(94.77, gc / 2.5, gh),
+ 'gray-2': oklchToHex(81.34, gc / 2, gh),
+ 'gray-3': oklchToHex(63.78, gc, gh),
+ 'gray-4': oklchToHex(46.01, gc, gh),
+ 'gray-5': oklchToHex(34.09, gc, gh),
+ 'gray-6': oklchToHex(27.14, gc, gh),
+ black: oklchToHex(20.94, gc / 2, gh),
+ },
+ light: {
+ // Accents
+ 'accent-low': oklchToHex(87.81, ac / 4, ah),
+ accent: oklchToHex(52.95, ac, ah),
+ 'accent-high': oklchToHex(31.77, ac / 2, ah),
+ // Grays
+ white: oklchToHex(20.94, gc / 2, gh),
+ 'gray-1': oklchToHex(27.14, gc, gh),
+ 'gray-2': oklchToHex(34.09, gc, gh),
+ 'gray-3': oklchToHex(46.01, gc, gh),
+ 'gray-4': oklchToHex(63.78, gc, gh),
+ 'gray-5': oklchToHex(81.34, gc / 2, gh),
+ 'gray-6': oklchToHex(94.77, gc / 2.5, gh),
+ 'gray-7': oklchToHex(97.35, gc / 5, gh),
+ black: oklchToHex(100, 0, 0),
+ },
+ };
+}
+
+/*
+This is the default Starlight color palette in OKLCH.
+Used as a reference when designing the algorithm for mapping
+user hue and chroma pairs to a similar palette.
+
+Dark mode:
+ accent lo oklch(25.94% 0.09 273.5)
+ accent oklch(52.28% 0.266 268.7)
+ accent hi oklch(83.38% 0.084 279.5)
+
+ white oklch(100% 0 0)
+ gray-1 oklch(94.77% 0.008 278.19)
+ gray-2 oklch(81.34% 0.011 274.87)
+ gray-3 oklch(63.78% 0.019 265.84)
+ gray-4 oklch(46.01% 0.021 270.93)
+ gray-5 oklch(34.09% 0.017 267.07)
+ gray-6 oklch(27.14% 0.015 267.03)
+ black oklch(20.94% 0.01 268.4)
+
+Light mode:
+ accent lo oklch(87.81% 0.056 280.2)
+ accent oklch(52.95% 0.243 270.2)
+ accent hi oklch(31.77% 0.177 267.2)
+
+ white oklch(20.94% 0.01 268.4)
+ gray-1 oklch(27.14% 0.015 267.03)
+ gray-2 oklch(34.09% 0.017 267.07)
+ gray-3 oklch(46.01% 0.021 270.93)
+ gray-4 oklch(63.78% 0.019 265.84)
+ gray-5 oklch(81.34% 0.011 274.87)
+ gray-6 oklch(94.77% 0.008 278.19)
+ gray-7 oklch(97.35% 0.004 286.32)
+ black oklch(100% 0 0)
+*/
diff --git a/docs/src/components/theme-designer/palette.astro b/docs/src/components/theme-designer/palette.astro
new file mode 100644
index 00000000..0b4e1d0b
--- /dev/null
+++ b/docs/src/components/theme-designer/palette.astro
@@ -0,0 +1,41 @@
+---
+interface Props {
+ light?: boolean;
+}
+---
+
+<div class="not-content">
+ <div class="palette">
+ {
+ Astro.props.light ? (
+ <div class="circle" style="--bg:var(--sl-color-accent)" />
+ ) : (
+ <div class="circle" style="--bg:var(--sl-color-accent-high)" />
+ )
+ }
+ <div style="--bg:var(--sl-color-black)"></div>
+ <div style="--bg:var(--sl-color-white)"></div>
+ <div style="--bg:var(--sl-color-gray-1)"></div>
+ <div style="--bg:var(--sl-color-gray-2)"></div>
+ <div style="--bg:var(--sl-color-gray-3)"></div>
+ <div style="--bg:var(--sl-color-gray-4)"></div>
+ <div style="--bg:var(--sl-color-gray-5)"></div>
+ <div style="--bg:var(--sl-color-gray-6)"></div>
+ {Astro.props.light && <div style="--bg:var(--sl-color-gray-7)" />}
+ </div>
+</div>
+
+<style>
+ .palette {
+ display: grid;
+ grid-template-columns: repeat(10, 1fr);
+ }
+ .palette > * {
+ aspect-ratio: 1;
+ background-color: var(--bg);
+ }
+
+ .circle {
+ border-radius: 50%;
+ }
+</style>
diff --git a/docs/src/components/theme-designer/presets.astro b/docs/src/components/theme-designer/presets.astro
new file mode 100644
index 00000000..5925df37
--- /dev/null
+++ b/docs/src/components/theme-designer/presets.astro
@@ -0,0 +1,94 @@
+---
+import { Icon } from '@astrojs/starlight/components';
+import { getPalettes } from './color-lib';
+import { presets } from './store';
+
+export interface Props {
+ labels: Record<keyof typeof presets, string> & {
+ label: string;
+ random: string;
+ };
+}
+const { labels } = Astro.props;
+
+const resolvedPresets = Object.entries(presets).map(([key, preset]) => {
+ const palette = getPalettes(preset);
+ const label = labels[key as keyof typeof presets];
+ return [key, { ...palette, label }] as const;
+});
+---
+
+<preset-picker class="flex not-content">
+ <span class="label">{labels.label}</span>
+ {
+ resolvedPresets.map(([key, { label, dark, light }]) => (
+ <button
+ data-preset={key}
+ style={{
+ '--dark-bg': dark['accent-high'],
+ '--light-bg': light.accent,
+ '--dark-text': dark['accent-low'],
+ '--light-text': light.black,
+ }}
+ >
+ {label}
+ </button>
+ ))
+ }
+ <button data-random>{labels.random}<Icon name="random" size="1rem" /></button>
+</preset-picker>
+
+<style>
+ preset-picker {
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+ }
+
+ .label {
+ color: var(--sl-color-white);
+ }
+
+ [data-preset],
+ [data-random] {
+ border: 0;
+ border-radius: 99rem;
+ min-width: 4.5rem;
+ display: flex;
+ gap: 0.25rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0.25rem 0.75rem;
+ background-color: var(--dark-bg, var(--sl-color-white));
+ color: var(--dark-text, var(--sl-color-black));
+ font-size: var(--sl-text-xs);
+ cursor: pointer;
+ }
+ :global([data-theme='light']) [data-preset] {
+ background-color: var(--light-bg);
+ color: var(--light-text);
+ }
+ button > :global(*) {
+ /* We want all clicks to go to the button, not child elements. */
+ pointer-events: none;
+ }
+</style>
+
+<script>
+ import { usePreset, useRandom } from './store';
+
+ class PresetPicker extends HTMLElement {
+ constructor() {
+ super();
+ this.addEventListener('click', (e) => {
+ if (e.target instanceof HTMLButtonElement) {
+ const name = e.target.dataset.preset;
+ if (name) usePreset(name);
+ if ('random' in e.target.dataset) useRandom();
+ }
+ });
+ }
+ }
+
+ customElements.define('preset-picker', PresetPicker);
+</script>
diff --git a/docs/src/components/theme-designer/preview.astro b/docs/src/components/theme-designer/preview.astro
new file mode 100644
index 00000000..7dbfb7d8
--- /dev/null
+++ b/docs/src/components/theme-designer/preview.astro
@@ -0,0 +1,53 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+import Palette from './palette.astro';
+
+interface Props extends HTMLAttributes<'div'> {
+ labels: {
+ lightMode: string;
+ darkMode: string;
+ bodyText: string;
+ linkText: string;
+ dimText: string;
+ inlineCode: string;
+ };
+}
+const { labels, ...attrs } = Astro.props;
+const light = 'data-light' in attrs;
+const linkColor = light ? '--sl-color-accent' : '--sl-color-accent-high';
+const codeBg = light ? '--sl-color-gray-6' : '--sl-color-gray-5';
+---
+
+<div class="preview-card" {...attrs}>
+ <h4>{light ? labels.lightMode : labels.darkMode}</h4>
+ <p>
+ {labels.bodyText}
+ <span class="link" style={`color:var(${linkColor})`}>{labels.linkText}</span>
+ <span class="dim">{labels.dimText}</span>
+ <code style={`--sl-color-bg-inline-code:var(${codeBg})`}>{labels.inlineCode}</code>
+ </p>
+ <Palette {light} />
+</div>
+
+<style>
+ .preview-card {
+ margin: 0 !important;
+ border: 1px solid var(--sl-color-gray-5);
+ padding: 1rem;
+ background-color: var(--sl-color-black);
+ }
+
+ p {
+ margin-top: 0.5em !important;
+ font-size: var(--sl-text-sm);
+ color: var(--sl-color-gray-2);
+ }
+
+ .link {
+ text-decoration: underline;
+ }
+
+ .dim {
+ color: var(--sl-color-gray-3);
+ }
+</style>
diff --git a/docs/src/components/theme-designer/store.ts b/docs/src/components/theme-designer/store.ts
new file mode 100644
index 00000000..6eb1aa82
--- /dev/null
+++ b/docs/src/components/theme-designer/store.ts
@@ -0,0 +1,47 @@
+import { map } from './atom';
+
+export const presets = {
+ ocean: {
+ accent: { hue: 240, chroma: 0.27 },
+ gray: { hue: 220, chroma: 0.025 },
+ },
+ forest: {
+ accent: { hue: 140, chroma: 0.27 },
+ gray: { hue: 140, chroma: 0.03 },
+ },
+ oxide: {
+ accent: { hue: 30, chroma: 0.27 },
+ gray: { hue: 30, chroma: 0.02 },
+ },
+ nebula: {
+ accent: { hue: 320, chroma: 0.27 },
+ gray: { hue: 305, chroma: 0.07 },
+ },
+ default: {
+ accent: { hue: 269, chroma: 0.27 },
+ gray: { hue: 270, chroma: 0.016 },
+ },
+};
+
+export const store = {
+ accent: map(presets.default.accent),
+ gray: map(presets.default.gray),
+};
+
+export const usePreset = (name: string) => {
+ if (name in presets) {
+ const { accent, gray } = presets[name as keyof typeof presets];
+ store.accent.set(accent);
+ store.gray.set(gray);
+ }
+};
+
+const MAX_CHROMA = 0.27;
+
+export const useRandom = () => {
+ store.accent.set({ hue: randomHue(), chroma: MAX_CHROMA - randomChroma() });
+ store.gray.set({ hue: randomHue(), chroma: randomChroma() });
+};
+
+const randomHue = () => Math.round(Math.random() * 360);
+const randomChroma = () => Math.pow(Math.random(), 3) * MAX_CHROMA;
diff --git a/docs/src/components/theme-designer/value-slider.astro b/docs/src/components/theme-designer/value-slider.astro
new file mode 100644
index 00000000..4202f029
--- /dev/null
+++ b/docs/src/components/theme-designer/value-slider.astro
@@ -0,0 +1,62 @@
+---
+import { store } from './store';
+
+interface Props {
+ label: string;
+ storeKey: keyof typeof store;
+ type: 'hue' | 'chroma';
+}
+const { label, storeKey, type } = Astro.props;
+
+const { max, step } = { hue: { max: 360, step: 1 }, chroma: { max: 0.27, step: 0.001 } }[type];
+const value = store[storeKey].get()[type];
+---
+
+<value-slider data-store={storeKey} data-type={type}>
+ <label>
+ <span>{label}</span>
+ <input type="range" {max} {step} {value} />
+ <span class="value">{value}</span>
+ </label>
+</value-slider>
+
+<script>
+ import { store } from './store';
+
+ class ValueSlider extends HTMLElement {
+ #input = this.querySelector('input')!;
+ #output = this.querySelector<HTMLSpanElement>('.value')!;
+ #store = store[this.dataset.store as keyof typeof store];
+ #type = this.dataset.type as 'hue' | 'chroma';
+
+ constructor() {
+ super();
+ this.#input.addEventListener('input', () =>
+ this.#store.setKey(this.#type, this.#input.valueAsNumber)
+ );
+ this.#store.subscribe((v) => {
+ const newValue = v[this.#type].toString();
+ this.#input.value = newValue;
+ // HACK: slice excess digits in case of rounding errors.
+ this.#output.innerText = newValue.slice(0, 5);
+ });
+ }
+ }
+ customElements.define('value-slider', ValueSlider);
+</script>
+
+<style>
+ label {
+ display: grid;
+ grid-template-columns: auto 1fr 2.5rem;
+ align-items: center;
+ gap: 0.75rem;
+ }
+ input {
+ min-width: 5rem;
+ }
+ .value {
+ font-size: var(--sl-text-sm);
+ color: var(--sl-color-gray-2);
+ }
+</style>
diff --git a/docs/src/content/docs/guides/css-and-tailwind.mdx b/docs/src/content/docs/guides/css-and-tailwind.mdx
index fc04b23e..d7096875 100644
--- a/docs/src/content/docs/guides/css-and-tailwind.mdx
+++ b/docs/src/content/docs/guides/css-and-tailwind.mdx
@@ -10,12 +10,13 @@ You can style your Starlight site with custom CSS files or use the Starlight Tai
Customize the styles applied to your Starlight site by providing additional CSS files to modify or extend Starlight’s default styles.
1. Add a CSS file to your `src/` directory.
- For example, you could override Starlight’s default blue accent hue to purple:
+ For example, you could set a wider default column width and larger text size for page titles:
```css
/* src/styles/custom.css */
:root {
- --sl-hue-accent: 270;
+ --sl-content-width: 50rem;
+ --sl-text-5xl: 3.5rem;
}
```
@@ -231,3 +232,57 @@ module.exports = {
plugins: [starlightPlugin()],
};
```
+
+## Theming
+
+Starlight’s color theme can be controlled by overriding its default custom properties.
+These variables are used throughout the UI with a range of gray shades used for text and background colors and an accent color used for links and to highlight current items in navigation.
+
+### Color theme editor
+
+Use the sliders below to modify Starlight’s accent and gray color palettes.
+The dark and light preview areas will show the resulting colors, and the whole page will also update to preview your changes.
+
+When you’re happy with your changes, copy the CSS or Tailwind code below and use it in your project.
+
+import ThemeDesigner from '../../../components/theme-designer.astro';
+
+<ThemeDesigner
+ labels={{
+ presets: {
+ label: 'Presets',
+ ocean: 'Ocean',
+ forest: 'Forest',
+ oxide: 'Oxide',
+ nebula: 'Nebula',
+ default: 'Default',
+ random: 'Random',
+ },
+ editor: {
+ accentColor: 'Accent',
+ grayColor: 'Gray',
+ hue: 'Hue',
+ chroma: 'Chroma',
+ pickColor: 'Pick color',
+ },
+ preview: {
+ darkMode: 'Dark mode',
+ lightMode: 'Light mode',
+ bodyText:
+ 'Body text is displayed in a gray shade with a high contrast with the background.',
+ linkText: 'Links are colored.',
+ dimText: 'Some text, like the table of contents, has a lower contrast.',
+ inlineCode: 'Inline code has a distinct background.',
+ },
+ }}
+>
+ <Fragment slot="css-docs">
+ Add the following CSS to your project in a [custom CSS
+ file](#custom-css-styles) to apply this theme to your site.
+ </Fragment>
+ <Fragment slot="tailwind-docs">
+ The example [Tailwind config file](#styling-starlight-with-tailwind) below
+ includes generated `accent` and `gray` color palettes to use in the
+ `theme.extend.colors` configuration object.
+ </Fragment>
+</ThemeDesigner>
diff --git a/packages/starlight/components/Icons.ts b/packages/starlight/components/Icons.ts
index 7cf21b4b..92d5cc61 100644
--- a/packages/starlight/components/Icons.ts
+++ b/packages/starlight/components/Icons.ts
@@ -52,6 +52,8 @@ export const Icons = {
'<path d="M17 22H5a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3h1a4 4 0 0 1 7.3-2.18c.448.64.692 1.4.7 2.18h3a1 1 0 0 1 1 1v3a4 4 0 0 1 2.18 7.3A3.86 3.86 0 0 1 18 18v3a1 1 0 0 1-1 1ZM5 8a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11v-3.18a1 1 0 0 1 1.33-.95 1.77 1.77 0 0 0 1.74-.23 2 2 0 0 0 .93-1.37 2 2 0 0 0-.48-1.59 1.89 1.89 0 0 0-2.17-.55 1 1 0 0 1-1.33-.95V8h-3.2a1 1 0 0 1-1-1.33 1.77 1.77 0 0 0-.23-1.74 1.939 1.939 0 0 0-3-.43A2 2 0 0 0 8 6c.002.23.046.456.13.67A1 1 0 0 1 7.18 8H5Z"/>',
'list-format':
'<path d="M3.71 16.29C3.6149 16.199 3.50276 16.1276 3.38 16.08C3.13654 15.98 2.86346 15.98 2.62 16.08C2.49725 16.1276 2.38511 16.199 2.29 16.29C2.19896 16.3851 2.1276 16.4972 2.08 16.62C2.00342 16.8021 1.9825 17.0028 2.01988 17.1968C2.05725 17.3908 2.15125 17.5694 2.29 17.71C2.3872 17.7983 2.49882 17.8694 2.62 17.92C2.7397 17.9729 2.86913 18.0002 3 18.0002C3.13087 18.0002 3.2603 17.9729 3.38 17.92C3.50119 17.8694 3.6128 17.7983 3.71 17.71C3.84876 17.5694 3.94276 17.3908 3.98013 17.1968C4.01751 17.0028 3.99658 16.8021 3.92 16.62C3.87241 16.4972 3.80104 16.3851 3.71 16.29ZM7 8H21C21.2652 8 21.5196 7.89464 21.7071 7.70711C21.8946 7.51957 22 7.26522 22 7C22 6.73478 21.8946 6.48043 21.7071 6.29289C21.5196 6.10536 21.2652 6 21 6H7C6.73479 6 6.48043 6.10536 6.2929 6.29289C6.10536 6.48043 6 6.73478 6 7C6 7.26522 6.10536 7.51957 6.2929 7.70711C6.48043 7.89464 6.73479 8 7 8ZM3.71 11.29C3.56938 11.1512 3.39081 11.0572 3.19682 11.0199C3.00283 10.9825 2.80211 11.0034 2.62 11.08C2.49882 11.1306 2.3872 11.2017 2.29 11.29C2.19896 11.3851 2.1276 11.4972 2.08 11.62C2.0271 11.7397 1.99977 11.8691 1.99977 12C1.99977 12.1309 2.0271 12.2603 2.08 12.38C2.13065 12.5012 2.20167 12.6128 2.29 12.71C2.3872 12.7983 2.49882 12.8694 2.62 12.92C2.7397 12.9729 2.86913 13.0002 3 13.0002C3.13087 13.0002 3.2603 12.9729 3.38 12.92C3.50119 12.8694 3.6128 12.7983 3.71 12.71C3.79833 12.6128 3.86936 12.5012 3.92 12.38C3.97291 12.2603 4.00024 12.1309 4.00024 12C4.00024 11.8691 3.97291 11.7397 3.92 11.62C3.87241 11.4972 3.80104 11.3851 3.71 11.29ZM21 11H7C6.73479 11 6.48043 11.1054 6.2929 11.2929C6.10536 11.4804 6 11.7348 6 12C6 12.2652 6.10536 12.5196 6.2929 12.7071C6.48043 12.8946 6.73479 13 7 13H21C21.2652 13 21.5196 12.8946 21.7071 12.7071C21.8946 12.5196 22 12.2652 22 12C22 11.7348 21.8946 11.4804 21.7071 11.2929C21.5196 11.1054 21.2652 11 21 11ZM3.71 6.29C3.6149 6.19896 3.50276 6.12759 3.38 6.08C3.19789 6.00342 2.99718 5.9825 2.80319 6.01987C2.6092 6.05725 2.43063 6.15124 2.29 6.29C2.20167 6.3872 2.13065 6.49882 2.08 6.62C2.0271 6.7397 1.99977 6.86913 1.99977 7C1.99977 7.13087 2.0271 7.2603 2.08 7.38C2.13065 7.50119 2.20167 7.6128 2.29 7.71C2.3872 7.79833 2.49882 7.86936 2.62 7.92C2.80211 7.99658 3.00283 8.0175 3.19682 7.98013C3.39081 7.94275 3.56938 7.84876 3.71 7.71C3.79833 7.6128 3.86936 7.50119 3.92 7.38C3.97291 7.2603 4.00024 7.13087 4.00024 7C4.00024 6.86913 3.97291 6.7397 3.92 6.62C3.86936 6.49882 3.79833 6.3872 3.71 6.29ZM21 16H7C6.73479 16 6.48043 16.1054 6.2929 16.2929C6.10536 16.4804 6 16.7348 6 17C6 17.2652 6.10536 17.5196 6.2929 17.7071C6.48043 17.8946 6.73479 18 7 18H21C21.2652 18 21.5196 17.8946 21.7071 17.7071C21.8946 17.5196 22 17.2652 22 17C22 16.7348 21.8946 16.4804 21.7071 16.2929C21.5196 16.1054 21.2652 16 21 16Z"></path>',
+ random:
+ '<path d="M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z"/>',
github:
'<path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.08 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3Z"/>',
gitlab:
diff --git a/packages/starlight/style/props.css b/packages/starlight/style/props.css
index aaba636c..fd04d7ec 100644
--- a/packages/starlight/style/props.css
+++ b/packages/starlight/style/props.css
@@ -2,13 +2,13 @@
::backdrop {
/* Colors (dark mode) */
--sl-color-white: hsl(0, 0%, 100%); /* “white” */
- --sl-color-gray-1: hsl(231, 23%, 94%);
- --sl-color-gray-2: hsl(228, 8%, 77%);
- --sl-color-gray-3: hsl(221, 8%, 56%);
- --sl-color-gray-4: hsl(225, 9%, 36%);
- --sl-color-gray-5: hsl(222, 11%, 23%);
- --sl-color-gray-6: hsl(222, 13%, 16%);
- --sl-color-black: hsl(223, 13%, 10%);
+ --sl-color-gray-1: hsl(224, 20%, 94%);
+ --sl-color-gray-2: hsl(224, 6%, 77%);
+ --sl-color-gray-3: hsl(224, 6%, 56%);
+ --sl-color-gray-4: hsl(224, 7%, 36%);
+ --sl-color-gray-5: hsl(224, 10%, 23%);
+ --sl-color-gray-6: hsl(224, 14%, 16%);
+ --sl-color-black: hsl(224, 10%, 10%);
--sl-hue-orange: 41;
--sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
@@ -31,10 +31,9 @@
--sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
--sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
- --sl-hue-accent: var(--sl-hue-blue);
- --sl-color-accent-low: hsl(var(--sl-hue-accent), 54%, 20%);
- --sl-color-accent: hsl(var(--sl-hue-accent), 100%, 60%);
- --sl-color-accent-high: hsl(var(--sl-hue-accent), 100%, 87%);
+ --sl-color-accent-low: hsl(224, 54%, 20%);
+ --sl-color-accent: hsl(224, 100%, 60%);
+ --sl-color-accent-high: hsl(224, 100%, 85%);
--sl-color-text: var(--sl-color-gray-2);
--sl-color-text-accent: var(--sl-color-accent-high);
@@ -113,14 +112,14 @@
:root[data-theme='light'],
[data-theme='light'] ::backdrop {
/* Colours (light mode) */
- --sl-color-white: hsl(223, 13%, 10%);
- --sl-color-gray-1: hsl(222, 13%, 16%);
- --sl-color-gray-2: hsl(222, 11%, 23%);
- --sl-color-gray-3: hsl(225, 9%, 36%);
- --sl-color-gray-4: hsl(221, 8%, 56%);
- --sl-color-gray-5: hsl(228, 8%, 77%);
- --sl-color-gray-6: hsl(231, 23%, 94%);
- --sl-color-gray-7: hsl(240, 20%, 97%);
+ --sl-color-white: hsl(224, 10%, 10%);
+ --sl-color-gray-1: hsl(224, 14%, 16%);
+ --sl-color-gray-2: hsl(224, 10%, 23%);
+ --sl-color-gray-3: hsl(224, 7%, 36%);
+ --sl-color-gray-4: hsl(224, 6%, 56%);
+ --sl-color-gray-5: hsl(224, 6%, 77%);
+ --sl-color-gray-6: hsl(224, 20%, 94%);
+ --sl-color-gray-7: hsl(224, 19%, 97%);
--sl-color-black: hsl(0, 0%, 100%);
--sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
@@ -139,9 +138,9 @@
--sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
--sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
- --sl-color-accent-high: hsl(var(--sl-hue-accent), 80%, 30%);
- --sl-color-accent: hsl(var(--sl-hue-accent), 90%, 60%);
- --sl-color-accent-low: hsl(var(--sl-hue-accent), 88%, 90%);
+ --sl-color-accent-high: hsl(234, 80%, 30%);
+ --sl-color-accent: hsl(234, 90%, 60%);
+ --sl-color-accent-low: hsl(234, 88%, 90%);
--sl-color-text-accent: var(--sl-color-accent);
--sl-color-text-invert: var(--sl-color-black);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 05ceabf7..649f165c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,9 +35,15 @@ importers:
'@astrojs/starlight':
specifier: workspace:*
version: link:../packages/starlight
+ '@types/culori':
+ specifier: ^2.0.0
+ version: 2.0.0
astro:
specifier: ^2.10.4
version: 2.10.4(sharp@0.32.3)
+ culori:
+ specifier: ^3.2.0
+ version: 3.2.0
sharp:
specifier: ^0.32.3
version: 0.32.3
@@ -1396,6 +1402,10 @@ packages:
resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==}
dev: true
+ /@types/culori@2.0.0:
+ resolution: {integrity: sha512-bKpEra39sQS9UZ+1JoWhuGJEzwKS0dUkNCohVYmn6CAEBkqyIXimKiPDRZWtiOB7sKgkWMaTUpHFimygRoGIlg==}
+ dev: false
+
/@types/debug@4.1.7:
resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
dependencies:
@@ -2374,6 +2384,11 @@ packages:
stream-transform: 2.1.3
dev: true
+ /culori@3.2.0:
+ resolution: {integrity: sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: false
+
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}