feat: initial shared design system extracted from newerror
Foundation + chrome for the bchen.dev static sites: design tokens, Spectral / Helvetica / IBM Plex Mono type, base + .prose typography, data-driven nav + footer, and prefers-color-scheme + [data-theme] dark mode with a pre-paint init. Colors are overridable CSS custom properties (defaults = newerror's palette); each site sets only its own colors.css. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C96hwEU9cM7mJpqATiv7R5
This commit is contained in:
110
README.md
Normal file
110
README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# static-design-system
|
||||
|
||||
Shared visual foundation for the bchen.dev static (Jekyll) sites. The canonical
|
||||
design is **A New Error** (newerror.bchen.dev) — Spectral serif body, Helvetica
|
||||
sans UI, IBM Plex Mono. Every site shares fonts, spacing, typography,
|
||||
nav/footer chrome, and light/dark behavior, and keeps **only its own colors**.
|
||||
|
||||
## What's here
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `base.css` | Tokens, font stacks, base + `.prose` typography, `.nav`/`.foot` chrome, mobile menu, icon swaps. Colors are CSS custom properties with defaults; sites override them. |
|
||||
| `theme-init.js` | Pre-paint theme read (inlined in `<head>`, no flash). |
|
||||
| `theme-toggle.js` | Theme-toggle + mobile-menu behavior (served + loaded at end of body). |
|
||||
| `head.html` | Shared `<head>` contents (meta/OG, fonts, CSS links, inlined theme-init). |
|
||||
| `nav.html` | Data-driven nav + mobile menu. |
|
||||
| `footer.html` | Shared footer. |
|
||||
|
||||
Blog-specific components (ledger feed, archive, reader/prose-article chrome)
|
||||
are **not** here — they live in newerror's own repo.
|
||||
|
||||
## Consuming it in a Jekyll site
|
||||
|
||||
This is designed for **legacy GitHub Pages** (Jekyll in safe mode: no custom
|
||||
plugins, no symlinks). The trick is to mount this repo as a submodule *inside*
|
||||
`_includes/` and deliver CSS/JS through tiny served wrapper files.
|
||||
|
||||
1. **Add the submodule at `_includes/ds`** (use the read-only **https** URL — a
|
||||
legacy Pages build cannot fetch ssh submodules):
|
||||
|
||||
```sh
|
||||
git submodule add https://gitea.bchen.dev/brendan/static-design-system.git _includes/ds
|
||||
```
|
||||
|
||||
2. **Serve the CSS** — create `assets/ds.css`:
|
||||
|
||||
```liquid
|
||||
---
|
||||
---
|
||||
{% include ds/base.css %}
|
||||
```
|
||||
|
||||
3. **Serve the JS** — create `assets/ds.js`:
|
||||
|
||||
```liquid
|
||||
---
|
||||
---
|
||||
{% include ds/theme-toggle.js %}
|
||||
```
|
||||
|
||||
4. **Add your palette** — create `assets/colors.css` overriding only the color
|
||||
tokens (`--bg --ink --muted --faint --rule --rule-2 --accent --accent-hover
|
||||
--field`) for light, `:root[data-theme="dark"]`, and the
|
||||
`prefers-color-scheme: dark` block. This loads *after* `ds.css`.
|
||||
|
||||
5. **Nav links** — create `_data/nav.yml`:
|
||||
|
||||
```yaml
|
||||
- title: Guides
|
||||
url: /guides
|
||||
- title: FAQ
|
||||
url: /support
|
||||
```
|
||||
|
||||
6. **Layout** — in `_layouts/default.html`:
|
||||
|
||||
```liquid
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>{% include ds/head.html %}</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="shell">{% include ds/nav.html %}</div>
|
||||
<main><div class="shell">
|
||||
{% if page.prose %}<div class="prose">{{ content }}</div>{% else %}{{ content }}{% endif %}
|
||||
</div></main>
|
||||
<div class="shell">{% include ds/footer.html %}</div>
|
||||
</div>
|
||||
<script src="/assets/ds.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Add `prose: true` to the front matter of markdown content pages to get the
|
||||
shared `.prose` typography; leave it off for bespoke landing pages.
|
||||
|
||||
## Config keys the includes read
|
||||
|
||||
| Key | Used by | Notes |
|
||||
|---|---|---|
|
||||
| `site.title` | nav mast, head | |
|
||||
| `site.description`, `site.author`, `site.url` | head meta | |
|
||||
| `site.data.nav` | nav | list of `{ title, url }` |
|
||||
| `site.rss_url` | nav | optional; shows an RSS link when set |
|
||||
| `site.author_url` | footer | optional; links the author name |
|
||||
| `site.footer_years` | footer | optional; e.g. `2025–2026` |
|
||||
|
||||
## Notes / gotchas
|
||||
|
||||
- **Push over ssh, reference over https.** Consuming sites' `.gitmodules` must
|
||||
use the https URL for the Pages build to fetch it; you can still push here
|
||||
over ssh.
|
||||
- **Keep `base.css` / the `.js` files Liquid-safe** — no `{{` or `{%`
|
||||
sequences, since their contents pass through the Jekyll include tag.
|
||||
- **Build-time dependency:** a consuming site's build now depends on
|
||||
`gitea.bchen.dev` being reachable (public, valid TLS).
|
||||
- **Local dev:** clone consumers with `--recurse-submodules`, or run
|
||||
`git submodule update --init` after cloning.
|
||||
- **Versioning:** tags (`v1`, `v2`, …) mark known-good states; a consuming
|
||||
site pins a commit and bumps deliberately.
|
||||
316
base.css
Normal file
316
base.css
Normal file
@@ -0,0 +1,316 @@
|
||||
/* =====================================================================
|
||||
static-design-system — base.css
|
||||
Shared visual foundation for the bchen.dev static sites.
|
||||
Canonical design: "A New Error" (newerror.bchen.dev) — the "Ledger"
|
||||
direction: Spectral serif body, Helvetica sans UI, IBM Plex Mono.
|
||||
|
||||
HOW COLOR WORKS
|
||||
Every color is a CSS custom property. The values below are DEFAULTS
|
||||
(newerror's palette). Each consuming site overrides ONLY these color
|
||||
tokens in its own colors.css, which loads AFTER this file. Fonts,
|
||||
spacing, typography, nav/footer chrome, and dark-mode behavior are
|
||||
shared and should NOT be re-declared per site.
|
||||
|
||||
IMPORTANT: keep this file Liquid-safe. Its contents are passed through the
|
||||
Jekyll include tag by each site's served wrapper (assets/ds.css), so do NOT
|
||||
introduce Liquid delimiters (double-brace or brace-percent sequences).
|
||||
Plain CSS braces are fine.
|
||||
===================================================================== */
|
||||
|
||||
:root {
|
||||
/* Type */
|
||||
--serif: "Spectral", Georgia, "Times New Roman", serif;
|
||||
--sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||
|
||||
/* Layout */
|
||||
--measure: 760px; /* standard content column */
|
||||
--read: 680px; /* narrower reading column (articles) */
|
||||
--pad: 34px;
|
||||
--gutter: 104px; /* date column width (ledger layouts) */
|
||||
}
|
||||
|
||||
/* ---- Color tokens — DEFAULTS (newerror). Override per site in colors.css ---- */
|
||||
:root, :root[data-theme="light"] {
|
||||
--bg: #FAFBFC;
|
||||
--ink: #1B2027;
|
||||
--muted: #69717C;
|
||||
--faint: #99A1AC;
|
||||
--rule: #E6E9EF;
|
||||
--rule-2: #b6b9be;
|
||||
--accent: #1463C7;
|
||||
--accent-hover: #0F4FA3;
|
||||
--field: #F1F3F6;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #14171C;
|
||||
--ink: #E2E6EC;
|
||||
--muted: #9AA2AD;
|
||||
--faint: #69717C;
|
||||
--rule: #262B32;
|
||||
--rule-2: #5e6670;
|
||||
--accent: #6FA9F2;
|
||||
--accent-hover: #97C1F8;
|
||||
--field: #1D2128;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #14171C;
|
||||
--ink: #E2E6EC;
|
||||
--muted: #9AA2AD;
|
||||
--faint: #69717C;
|
||||
--rule: #262B32;
|
||||
--rule-2: #5e6670;
|
||||
--accent: #6FA9F2;
|
||||
--accent-hover: #97C1F8;
|
||||
--field: #1D2128;
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
Base
|
||||
===================================================================== */
|
||||
* { box-sizing: border-box; }
|
||||
html { -webkit-text-size-adjust: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: var(--serif);
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
::selection { background: color-mix(in oklab, var(--accent) 22%, transparent); }
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
/* ---- shell / responsive width ---- */
|
||||
.shell {
|
||||
width: 100%;
|
||||
max-width: var(--measure);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--pad);
|
||||
}
|
||||
.page { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
.page > main { flex: 1 0 auto; }
|
||||
|
||||
/* =====================================================================
|
||||
Top navigation
|
||||
===================================================================== */
|
||||
.nav {
|
||||
font-family: var(--sans);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 38px 0 18px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.nav.minimal { border-bottom: none; padding-bottom: 0; }
|
||||
|
||||
.mast {
|
||||
font-family: var(--serif);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.005em;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mast:hover { color: var(--accent); }
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 26px;
|
||||
}
|
||||
.nav-links a, .iconbtn {
|
||||
font-family: var(--sans);
|
||||
font-size: 13.5px;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.nav-links a:hover, .iconbtn:hover { color: var(--accent); }
|
||||
.nav-links a.active { color: var(--ink); }
|
||||
|
||||
.nav-tools { display: flex; align-items: center; gap: 18px; }
|
||||
|
||||
.iconbtn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 26px; height: 26px; border-radius: 6px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.iconbtn:hover { color: var(--accent); background: var(--field); }
|
||||
.iconbtn svg { width: 15px; height: 15px; display: block; }
|
||||
|
||||
/* hamburger — hidden on desktop */
|
||||
.hamburger { display: none; }
|
||||
.mobile-menu { display: none; }
|
||||
|
||||
/* =====================================================================
|
||||
Footer
|
||||
===================================================================== */
|
||||
.foot {
|
||||
font-family: var(--sans);
|
||||
border-top: 1px solid var(--rule);
|
||||
margin-top: 20px;
|
||||
padding: 22px 0 40px;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px;
|
||||
font-size: 12.5px;
|
||||
color: var(--faint);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.foot a { color: var(--muted); text-decoration: none; }
|
||||
.foot a:hover { color: var(--accent); }
|
||||
.foot .rss-link { display: inline-flex; align-items: center; gap: 7px; }
|
||||
|
||||
/* =====================================================================
|
||||
Prose — reusable content typography (markdown pages, articles)
|
||||
Wrap rendered content in <div class="prose"> to opt in.
|
||||
===================================================================== */
|
||||
.prose { font-size: 19.5px; line-height: 1.68; color: var(--ink); }
|
||||
.prose > :first-child { margin-top: 0; }
|
||||
.prose p { margin: 0 0 1.3em; text-wrap: pretty; }
|
||||
.prose h1 {
|
||||
font-family: var(--serif);
|
||||
font-size: 32px; line-height: 1.16; font-weight: 600;
|
||||
letter-spacing: -0.018em; color: var(--ink);
|
||||
margin: 0 0 0.5em; text-wrap: pretty;
|
||||
}
|
||||
.prose h2 {
|
||||
font-family: var(--serif);
|
||||
font-size: 25px; font-weight: 600; line-height: 1.25;
|
||||
letter-spacing: -0.01em;
|
||||
margin: 2em 0 0.6em;
|
||||
}
|
||||
.prose h3 {
|
||||
font-family: var(--serif);
|
||||
font-size: 21px; font-weight: 600;
|
||||
margin: 1.7em 0 0.5em;
|
||||
}
|
||||
.prose a {
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--rule-2);
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
.prose a:hover { color: var(--accent); text-decoration-color: var(--accent); }
|
||||
.prose strong { font-weight: 600; }
|
||||
.prose em { font-style: italic; }
|
||||
|
||||
.prose blockquote {
|
||||
margin: 1.6em 0;
|
||||
padding-left: 22px;
|
||||
border-left: 2px solid var(--rule-2);
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
.prose blockquote p:last-child { margin-bottom: 0; }
|
||||
|
||||
.prose ul, .prose ol { margin: 0 0 1.3em; padding-left: 1.3em; }
|
||||
.prose li { margin: 0.3em 0; padding-left: 0.2em; }
|
||||
.prose li::marker { color: var(--faint); }
|
||||
|
||||
.prose hr {
|
||||
border: none; height: 0;
|
||||
border-top: 1px solid var(--rule);
|
||||
margin: 2.4em auto;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.prose code {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82em;
|
||||
background: var(--field);
|
||||
padding: 0.12em 0.4em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.prose pre {
|
||||
font-family: var(--mono);
|
||||
font-size: 14px; line-height: 1.55;
|
||||
background: var(--field);
|
||||
padding: 18px 20px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
.prose pre code { background: none; padding: 0; font-size: inherit; }
|
||||
|
||||
.prose figure { margin: 2em 0; }
|
||||
.prose figure img { display: block; width: 100%; height: auto; border-radius: 3px; }
|
||||
.prose figcaption {
|
||||
font-family: var(--sans);
|
||||
font-size: 12.5px; color: var(--faint);
|
||||
margin-top: 10px; text-align: left;
|
||||
}
|
||||
.prose img { max-width: 100%; height: auto; }
|
||||
|
||||
/* =====================================================================
|
||||
Mobile
|
||||
===================================================================== */
|
||||
@media (max-width: 640px) {
|
||||
:root { --pad: 22px; --gutter: 74px; }
|
||||
body { font-size: 17px; }
|
||||
|
||||
.nav-links { display: none; }
|
||||
.hamburger { display: inline-flex; }
|
||||
|
||||
.mobile-menu {
|
||||
display: none;
|
||||
font-family: var(--sans);
|
||||
}
|
||||
.mobile-menu.open {
|
||||
display: flex; flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 0 14px;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
.mobile-menu a, .mobile-menu button {
|
||||
font-family: var(--sans);
|
||||
text-align: left;
|
||||
background: none; border: none; cursor: pointer;
|
||||
color: var(--ink);
|
||||
font-size: 16px;
|
||||
padding: 12px 4px;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.mobile-menu a:hover, .mobile-menu button:hover { color: var(--accent); }
|
||||
.mobile-menu .row { display: flex; align-items: center; gap: 10px; }
|
||||
|
||||
.prose { font-size: 18.5px; }
|
||||
.prose h1 { font-size: 30px; }
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
Icon swaps (theme toggle + hamburger), driven by [data-theme] / .is-open
|
||||
===================================================================== */
|
||||
.theme-toggle .ico-sun { display: none; }
|
||||
.theme-toggle .ico-moon { display: block; }
|
||||
:root[data-theme="dark"] .theme-toggle .ico-moon { display: none; }
|
||||
:root[data-theme="dark"] .theme-toggle .ico-sun { display: block; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) .theme-toggle .ico-moon { display: none; }
|
||||
:root:not([data-theme="light"]) .theme-toggle .ico-sun { display: block; }
|
||||
}
|
||||
|
||||
.hamburger .ico-close { display: none; }
|
||||
.hamburger.is-open .ico-bars { display: none; }
|
||||
.hamburger.is-open .ico-close { display: block; }
|
||||
|
||||
/* reduce motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* { transition: none !important; }
|
||||
}
|
||||
9
footer.html
Normal file
9
footer.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{%- comment -%}
|
||||
static-design-system — footer.html
|
||||
Shared footer (chrome). Place inside a .shell.
|
||||
Uses site.author, optional site.author_url (links the name), and optional
|
||||
site.footer_years (e.g. "2025–2026").
|
||||
{%- endcomment -%}
|
||||
<footer class="foot">
|
||||
<span>© {% if site.author_url %}<a href="{{ site.author_url }}">{{ site.author }}</a>{% else %}{{ site.author }}{% endif %}{% if site.footer_years %} {{ site.footer_years }}{% endif %}.</span>
|
||||
</footer>
|
||||
39
head.html
Normal file
39
head.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{%- comment -%}
|
||||
static-design-system — head.html
|
||||
Shared contents of <head>. The consuming site wraps it:
|
||||
<head>{% include ds/head.html %} ...site-specific tags... </head>
|
||||
Expects site.title, site.description, site.author, site.url.
|
||||
Loads the fonts, the shared CSS (/assets/ds.css), then the site's color
|
||||
overrides (/assets/colors.css) — order matters — and inlines the pre-paint
|
||||
theme script to avoid a flash.
|
||||
{%- endcomment -%}
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>{{ page.title | default: site.title }}</title>
|
||||
<meta name="description" content="{{ page.description | default: site.description }}" />
|
||||
<meta name="author" content="{{ site.author }}" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="{{ page.title | default: site.title }}" />
|
||||
<meta property="og:description" content="{{ page.description | default: site.description }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="{{ site.url }}{{ page.url | default: '/' }}" />
|
||||
<meta property="og:site_name" content="{{ site.title }}" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{{ page.title | default: site.title }}" />
|
||||
<meta name="twitter:description" content="{{ page.description | default: site.description }}" />
|
||||
|
||||
<!-- Fonts: Spectral (serif) + IBM Plex Mono. Helvetica sans is a system font. -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;0,600;1,400;1,500&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Shared design system, then site color overrides (order matters). -->
|
||||
<link rel="stylesheet" href="/assets/ds.css" />
|
||||
<link rel="stylesheet" href="/assets/colors.css" />
|
||||
|
||||
<!-- Pre-paint theme (avoids flash-of-wrong-theme) -->
|
||||
<script>{% include ds/theme-init.js %}</script>
|
||||
60
nav.html
Normal file
60
nav.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{%- comment -%}
|
||||
static-design-system — nav.html
|
||||
Data-driven top navigation + mobile menu (chrome). Place inside a .shell.
|
||||
Links come from site.data.nav (a list of { title, url }). An RSS link is
|
||||
shown only if site.rss_url is set. The theme toggle and hamburger are always
|
||||
present; behavior is wired by /assets/ds.js (ds/theme-toggle.js).
|
||||
{%- endcomment -%}
|
||||
<nav class="nav">
|
||||
<a class="mast" href="/">{{ site.title }}</a>
|
||||
|
||||
<div class="nav-tools">
|
||||
<div class="nav-links">
|
||||
{%- for item in site.data.nav %}
|
||||
<a href="{{ item.url }}"{% if page.url == item.url %} class="active"{% endif %}>{{ item.title }}</a>
|
||||
{%- endfor %}
|
||||
{%- if site.rss_url %}
|
||||
<a class="iconbtn" href="{{ site.rss_url }}" title="RSS feed" aria-label="RSS feed"><svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="3.4" cy="12.6" r="1.55" fill="currentColor" />
|
||||
<path d="M2.2 7.1a6.7 6.7 0 0 1 6.7 6.7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path d="M2.2 3.2a10.6 10.6 0 0 1 10.6 10.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
</svg></a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
||||
<button class="iconbtn theme-toggle" type="button" aria-label="Toggle color theme" title="Toggle theme">
|
||||
<svg class="ico-moon" width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M13 9.4A5.4 5.4 0 0 1 6.6 3 5.6 5.6 0 1 0 13 9.4Z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<svg class="ico-sun" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="8" cy="8" r="3.1" stroke="currentColor" stroke-width="1.3" />
|
||||
<line x1="8" y1="0.6" x2="8" y2="2.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="8" y1="13.6" x2="8" y2="15.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="0.6" y1="8" x2="2.4" y2="8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="13.6" y1="8" x2="15.4" y2="8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="2.7" y1="2.7" x2="4" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="12" y1="12" x2="13.3" y2="13.3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="13.3" y1="2.7" x2="12" y2="4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<line x1="4" y1="12" x2="2.7" y2="13.3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="iconbtn hamburger" type="button" aria-label="Menu" aria-expanded="false">
|
||||
<svg class="ico-bars" width="16" height="16" viewBox="0 0 16 16"><path d="M2 4.5h12M2 8h12M2 11.5h12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
<svg class="ico-close" width="16" height="16" viewBox="0 0 16 16"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-menu">
|
||||
{%- for item in site.data.nav %}
|
||||
<a href="{{ item.url }}">{{ item.title }}</a>
|
||||
{%- endfor %}
|
||||
{%- if site.rss_url %}
|
||||
<a class="row" href="{{ site.rss_url }}"><svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<circle cx="3.4" cy="12.6" r="1.55" fill="currentColor" />
|
||||
<path d="M2.2 7.1a6.7 6.7 0 0 1 6.7 6.7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
<path d="M2.2 3.2a10.6 10.6 0 0 1 10.6 10.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
|
||||
</svg> <span>RSS feed</span></a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
14
theme-init.js
Normal file
14
theme-init.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* static-design-system — theme-init.js
|
||||
Pre-paint: apply the stored [data-theme] before first paint so there is no
|
||||
flash. This file is inlined into <head> by ds/head.html (see head.html); it
|
||||
must run before the stylesheet paints, so keep it tiny and synchronous.
|
||||
NOTE: no Liquid delimiters allowed here (this content is passed through the
|
||||
Jekyll include tag). Storage key must match theme-toggle.js. */
|
||||
(function () {
|
||||
try {
|
||||
var t = localStorage.getItem("bchen-theme");
|
||||
if (t === "light" || t === "dark") {
|
||||
document.documentElement.setAttribute("data-theme", t);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
36
theme-toggle.js
Normal file
36
theme-toggle.js
Normal file
@@ -0,0 +1,36 @@
|
||||
/* static-design-system — theme-toggle.js
|
||||
Shared chrome behavior: color-theme toggle + mobile (hamburger) menu.
|
||||
Served to each site via a wrapper (assets/ds.js) and loaded at end of body.
|
||||
Storage key must match ds/theme-init.js. */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var THEME_KEY = "bchen-theme";
|
||||
|
||||
function effectiveTheme() {
|
||||
var set = document.documentElement.getAttribute("data-theme");
|
||||
if (set) return set;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
/* ---- theme toggle ---- */
|
||||
var toggleBtn = document.querySelector(".theme-toggle");
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener("click", function () {
|
||||
var next = effectiveTheme() === "dark" ? "light" : "dark";
|
||||
document.documentElement.setAttribute("data-theme", next);
|
||||
try { localStorage.setItem(THEME_KEY, next); } catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- mobile menu ---- */
|
||||
var hamburger = document.querySelector(".hamburger");
|
||||
var mobileMenu = document.querySelector(".mobile-menu");
|
||||
if (hamburger && mobileMenu) {
|
||||
hamburger.addEventListener("click", function () {
|
||||
var open = mobileMenu.classList.toggle("open");
|
||||
hamburger.classList.toggle("is-open", open);
|
||||
hamburger.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user