-
-
Notifications
You must be signed in to change notification settings - Fork 578
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add theme editor to styling guide (#470)
- Loading branch information
Showing
15 changed files
with
781 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
Oops, something went wrong.