diff --git a/blog-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md b/blog-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md index e2289e1..9e26b8d 100644 --- a/blog-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md +++ b/blog-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md @@ -12,7 +12,7 @@ import { SettingsPage } from "./components/SettingsPage"; import { ReportsPage } from "./components/ReportsPage"; import { StatusWidget } from "./components/StatusWidget"; -// Pages keyed by path (must match admin.pages paths) +// Pages keyed by path (trailing slash optional, so /settings and /settings/ both resolve) export const pages = { "/settings": SettingsPage, "/reports": ReportsPage, diff --git a/blog-cloudflare/.agents/skills/creating-plugins/references/api-routes.md b/blog-cloudflare/.agents/skills/creating-plugins/references/api-routes.md index c4d2419..8ecf112 100644 --- a/blog-cloudflare/.agents/skills/creating-plugins/references/api-routes.md +++ b/blog-cloudflare/.agents/skills/creating-plugins/references/api-routes.md @@ -175,7 +175,9 @@ routes: { }, "settings/save": { handler: async (ctx) => { - const input = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input. + // Don't call ctx.request.json() — the body stream is already consumed. + const input = ctx.input as Record; for (const [key, value] of Object.entries(input)) { if (value !== undefined) await ctx.kv.set(`settings:${key}`, value); } diff --git a/blog-cloudflare/.agents/skills/creating-plugins/references/block-kit.md b/blog-cloudflare/.agents/skills/creating-plugins/references/block-kit.md index 397505b..5354e91 100644 --- a/blog-cloudflare/.agents/skills/creating-plugins/references/block-kit.md +++ b/blog-cloudflare/.agents/skills/creating-plugins/references/block-kit.md @@ -16,10 +16,16 @@ Block Kit elements are also used for [Portable Text block editing fields](./port 6. Plugin returns new blocks ```typescript +import type { BlockInteraction } from "@emdash-cms/blocks"; + routes: { admin: { handler: async (ctx) => { - const interaction = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input; + // read it directly rather than ctx.request.json() (the body is consumed). + // BlockInteraction is the discriminated union of page_load, + // block_action, and form_submit payloads. + const interaction = ctx.input as BlockInteraction; if (interaction.type === "page_load") { return { diff --git a/blog-cloudflare/AGENTS.md b/blog-cloudflare/AGENTS.md index 912334c..3a15e60 100644 --- a/blog-cloudflare/AGENTS.md +++ b/blog-cloudflare/AGENTS.md @@ -70,23 +70,26 @@ Site settings have `title` and `tagline` -- both render in the header / footer. ## Visual character -Single typeface: **Inter** on `--font-sans`, used for everything including headings (with tighter letter-spacing on h1/h2). **JetBrains Mono** on `--font-mono` for inline code and code blocks. Body and headings share the same family; weight and size carry the hierarchy. +Single typeface: **Inter** on `--font-body`, used for everything including headings (`--font-heading` defaults to the body face; tighter letter-spacing on h1/h2). **JetBrains Mono** on `--font-mono` for inline code and code blocks. Body and headings share the same family; weight and size carry the hierarchy (`--font-weight-heading` 600, `--font-weight-display` 700 for h1/page titles). -The accent is `#0066cc` -- used for links, the post-card title hover, and the search input focus ring. There's also a secondary text colour (`--color-text-secondary`) and a `--color-muted` for meta info. Don't add a second accent. +The brand colour is `#0066cc` (`--color-brand`) -- used for links, the post-card title hover, and the search input focus ring. There's also a secondary text colour (`--color-text-secondary`) and a `--color-muted` for meta info. Don't add a second accent. The article layout is the standout feature: a three-column reading view with a left meta column (author bylines, date), centred 680px body column, and a right gutter for search, table of contents, and categories. Don't flatten that into one column on desktop -- the layout signals "this is something to read". ## Customisation -`src/styles/theme.css` is the only file to edit for visual changes. Every CSS variable from `Base.astro` is listed there as a commented default -- uncomment and change to override. The dark mode palette is defined inside `Base.astro` itself; light-mode overrides in `theme.css` won't affect dark mode. To customise dark mode, add `@media (prefers-color-scheme: dark)` and `:root.dark` rules in `theme.css`. +Design tokens live in `src/styles/tokens.css` with their default values. To restyle the site, override tokens in `src/styles/theme.css` -- declarations there are unlayered, so they always beat the `@layer base` defaults. Don't edit `tokens.css` or `Base.astro` for visual changes. -Fonts are configured in `astro.config.mjs` under `fonts:`. To swap the body face, change the `name:` for the entry bound to `cssVariable: "--font-sans"`. Good alternatives: Geist, IBM Plex Sans, Söhne (if you have a licence), Public Sans. If you want a serif-bodied blog, swap to a humanist serif like Source Serif, Crimson Pro, or Lora -- but then also raise `--font-size-base` to `1.0625rem` for readability. +Colours are defined with `light-dark(, )`, so each token carries both modes. Overriding with a plain colour changes light and dark at once; use `light-dark()` in the override to keep them distinct. There is no separate dark palette to maintain. -CSS variables worth knowing: +Webfonts are configured in `astro.config.mjs` under `fonts:`. To swap the body face, change the `name:` for the entry bound to `cssVariable: "--font-body"`. Good alternatives: Geist, IBM Plex Sans, Söhne (if you have a licence), Public Sans. If you want a serif-bodied blog, swap to a humanist serif like Source Serif, Crimson Pro, or Lora -- but then also raise `--font-size-base` to `1.0625rem` for readability. To give headings their own face (or use a system font) without touching the font pipeline, override `--font-heading` or `--font-body` in `theme.css`. -- `--color-accent`, `--color-accent-hover`, `--color-on-accent`, `--color-accent-ring` +CSS variables worth knowing (see `tokens.css` for the full list): + +- `--color-brand`, `--color-brand-hover`, `--color-on-brand`, `--color-brand-ring` - `--color-bg`, `--color-bg-subtle`, `--color-surface`, `--color-text`, `--color-text-secondary`, `--color-muted`, `--color-border`, `--color-border-subtle` -- `--font-sans`, `--font-mono` +- `--font-body`, `--font-heading`, `--font-mono` +- `--font-weight-heading` (600) / `--font-weight-display` (700) -- heading weights; lower them if you switch to a serif - `--tracking-tight` / `--tracking-snug` / `--tracking-wide` / `--tracking-wider` -- letter-spacing tokens used across headings and meta labels - `--content-width` (680px) -- article body column - `--wide-width` (1200px) -- max container diff --git a/blog-cloudflare/astro.config.mjs b/blog-cloudflare/astro.config.mjs index 1b87b1d..67dc302 100644 --- a/blog-cloudflare/astro.config.mjs +++ b/blog-cloudflare/astro.config.mjs @@ -28,7 +28,7 @@ export default defineConfig({ { provider: fontProviders.google(), name: "Inter", - cssVariable: "--font-sans", + cssVariable: "--font-body", weights: [400, 500, 600, 700], fallbacks: ["sans-serif"], }, diff --git a/blog-cloudflare/package.json b/blog-cloudflare/package.json index 55b89dd..7932c9b 100644 --- a/blog-cloudflare/package.json +++ b/blog-cloudflare/package.json @@ -16,11 +16,11 @@ "dependencies": { "@astrojs/cloudflare": "^14.0.0", "@astrojs/react": "^6.0.0", - "@emdash-cms/cloudflare": "^0.27.0", + "@emdash-cms/cloudflare": "^0.28.1", "@emdash-cms/plugin-forms": "^0.2.4", "@emdash-cms/plugin-webhook-notifier": "^0.2.0", "astro": "^7.0.0", - "emdash": "^0.27.0", + "emdash": "^0.28.1", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/blog-cloudflare/src/components/PostCard.astro b/blog-cloudflare/src/components/PostCard.astro index 09d1d9d..80fa416 100644 --- a/blog-cloudflare/src/components/PostCard.astro +++ b/blog-cloudflare/src/components/PostCard.astro @@ -173,7 +173,7 @@ const formattedDate = date .card-title { font-size: var(--font-size-xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); margin-bottom: var(--spacing-2); @@ -181,7 +181,7 @@ const formattedDate = date } .card-link:hover .card-title { - color: var(--color-accent); + color: var(--color-brand); } .card-excerpt { @@ -256,7 +256,7 @@ const formattedDate = date } .byline-more:focus-visible { - outline: 2px solid var(--color-accent); + outline: 2px solid var(--color-brand); } .byline-more[data-tooltip]:hover::after, diff --git a/blog-cloudflare/src/layouts/Base.astro b/blog-cloudflare/src/layouts/Base.astro index 67c46e3..30520cd 100644 --- a/blog-cloudflare/src/layouts/Base.astro +++ b/blog-cloudflare/src/layouts/Base.astro @@ -10,6 +10,7 @@ import { createPublicPageContext } from "emdash/page"; import LiveSearch from "emdash/ui/search"; import { Font } from "astro:assets"; import { resolveBlogSiteIdentity } from "../utils/site-identity"; +import "../styles/tokens.css"; import "../styles/theme.css"; interface Props { @@ -81,20 +82,19 @@ const isLoggedIn = !!Astro.locals.user; - + {fullTitle} @@ -304,9 +304,6 @@ const isLoggedIn = !!Astro.locals.user; if (theme === "system") { setCookie("theme", ""); root.classList.remove("light", "dark"); - if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - root.classList.add("dark"); - } } else { setCookie("theme", theme); root.classList.remove("light", "dark"); @@ -335,15 +332,6 @@ const isLoggedIn = !!Astro.locals.user; setTheme(btn.dataset.theme || "system"); }); }); - - // Listen for system preference changes - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - if (getStoredTheme() === "system") { - root.classList.toggle("dark", e.matches); - } - }); @@ -606,7 +474,7 @@ const isLoggedIn = !!Astro.locals.user; } .site-title:hover { - color: var(--color-accent); + color: var(--color-brand); } .site-logo-img { @@ -636,7 +504,7 @@ const isLoggedIn = !!Astro.locals.user; } .nav-links a:hover { - color: var(--color-accent); + color: var(--color-brand); } .nav-admin { @@ -654,13 +522,13 @@ const isLoggedIn = !!Astro.locals.user; .site-search { position: relative; width: var(--search-input-width); - --emdash-search-border-focus: var(--color-accent); + --emdash-search-border-focus: var(--color-brand); } :global(.site-search-input) { width: var(--search-input-width); padding: var(--spacing-2) var(--spacing-3); - font-family: var(--font-sans); + font-family: var(--font-body); font-size: var(--font-size-sm); border: 1px solid var(--color-border); border-radius: var(--radius); @@ -678,8 +546,8 @@ const isLoggedIn = !!Astro.locals.user; :global(.site-search-input):focus, :global(.site-search-input):focus-visible { outline: none; - border-color: var(--color-accent) !important; - box-shadow: 0 0 0 3px var(--color-accent-ring); + border-color: var(--color-brand) !important; + box-shadow: 0 0 0 3px var(--color-brand-ring); } :global(.site-search-results) { @@ -691,7 +559,7 @@ const isLoggedIn = !!Astro.locals.user; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-lg); - box-shadow: var(--shadow-dropdown); + box-shadow: var(--shadow-lg); max-height: 400px; overflow-y: auto; z-index: 1000; @@ -902,7 +770,7 @@ const isLoggedIn = !!Astro.locals.user; .theme-btn.active { background: var(--color-bg); color: var(--color-text); - box-shadow: var(--shadow-btn-active); + box-shadow: var(--shadow-sm); } .theme-btn svg { diff --git a/blog-cloudflare/src/pages/category/[slug].astro b/blog-cloudflare/src/pages/category/[slug].astro index f0bbf70..64bd0aa 100644 --- a/blog-cloudflare/src/pages/category/[slug].astro +++ b/blog-cloudflare/src/pages/category/[slug].astro @@ -87,7 +87,7 @@ const filteredPosts = posts.map((post) => ({ display: block; font-size: var(--font-size-xs); font-weight: 500; - color: var(--color-accent); + color: var(--color-brand); text-transform: uppercase; letter-spacing: var(--tracking-wider); margin-bottom: var(--spacing-2); @@ -95,7 +95,7 @@ const filteredPosts = posts.map((post) => ({ .archive-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } diff --git a/blog-cloudflare/src/pages/index.astro b/blog-cloudflare/src/pages/index.astro index da6b71c..f49ef41 100644 --- a/blog-cloudflare/src/pages/index.astro +++ b/blog-cloudflare/src/pages/index.astro @@ -302,7 +302,7 @@ function formatDate(date: Date | null | undefined) { .featured-title { font-size: clamp(var(--font-size-2xl), 4vw, var(--font-size-4xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); transition: color var(--transition-fast); @@ -310,7 +310,7 @@ function formatDate(date: Date | null | undefined) { .featured-title-link:hover .featured-title, .featured-grid:has(.featured-image-link:hover) .featured-title { - color: var(--color-accent); + color: var(--color-brand); } .featured-excerpt { @@ -364,13 +364,13 @@ function formatDate(date: Date | null | undefined) { .section-link { font-size: var(--font-size-sm); - color: var(--color-accent); + color: var(--color-brand); text-decoration: none; transition: color var(--transition-fast); } .section-link:hover { - color: var(--color-accent-hover); + color: var(--color-brand-hover); } /* Posts Grid */ @@ -397,7 +397,7 @@ function formatDate(date: Date | null | undefined) { .empty-state h2 { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); } .empty-state p { @@ -408,8 +408,8 @@ function formatDate(date: Date | null | undefined) { display: inline-block; margin-top: var(--spacing-4); padding: var(--spacing-3) var(--spacing-6); - background: var(--color-accent); - color: var(--color-on-accent); + background: var(--color-brand); + color: var(--color-on-brand); text-decoration: none; border-radius: var(--radius); font-size: var(--font-size-sm); @@ -418,7 +418,7 @@ function formatDate(date: Date | null | undefined) { } .btn:hover { - background: var(--color-accent-hover); + background: var(--color-brand-hover); } /* Responsive */ diff --git a/blog-cloudflare/src/pages/pages/[slug].astro b/blog-cloudflare/src/pages/pages/[slug].astro index 3c15fea..db1753c 100644 --- a/blog-cloudflare/src/pages/pages/[slug].astro +++ b/blog-cloudflare/src/pages/pages/[slug].astro @@ -46,7 +46,7 @@ if (Astro.cache?.enabled) Astro.cache.set(cacheHint); .page-title { font-size: clamp(var(--font-size-2xl), 4vw, var(--font-size-4xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); } diff --git a/blog-cloudflare/src/pages/posts/[slug].astro b/blog-cloudflare/src/pages/posts/[slug].astro index 0cc892f..0d45757 100644 --- a/blog-cloudflare/src/pages/posts/[slug].astro +++ b/blog-cloudflare/src/pages/posts/[slug].astro @@ -495,7 +495,7 @@ const publishDate = .article-title { font-size: clamp(2rem, 5vw, var(--font-size-5xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-4); @@ -588,7 +588,7 @@ const publishDate = } .article-content :global(a) { - color: var(--color-accent); + color: var(--color-brand); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; @@ -687,7 +687,7 @@ const publishDate = .sidebar-widgets :global(.widget-search__input) { width: 100%; padding: var(--spacing-2) var(--spacing-3); - font-family: var(--font-sans); + font-family: var(--font-body); font-size: var(--font-size-sm); border: 1px solid var(--color-border); border-radius: var(--radius); @@ -705,8 +705,8 @@ const publishDate = .sidebar-widgets :global(.widget-search__input):focus, .sidebar-widgets :global(.widget-search__input):focus-visible { outline: none; - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-ring); + border-color: var(--color-brand); + box-shadow: 0 0 0 3px var(--color-brand-ring); } .sidebar-widgets :global(.widget-search__button) { @@ -863,7 +863,7 @@ const publishDate = .article-comments :global(.ec-comments-heading) { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); margin-bottom: var(--spacing-8); } @@ -893,8 +893,8 @@ const publishDate = } .article-comments :global(.ec-comment-form-submit) { - background: var(--color-accent) !important; - color: var(--color-on-accent) !important; + background: var(--color-brand) !important; + color: var(--color-on-brand) !important; } /* More posts section */ @@ -912,7 +912,7 @@ const publishDate = .more-title { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); margin-bottom: var(--spacing-10); } diff --git a/blog-cloudflare/src/pages/posts/index.astro b/blog-cloudflare/src/pages/posts/index.astro index 6c8c3b7..537ebb8 100644 --- a/blog-cloudflare/src/pages/posts/index.astro +++ b/blog-cloudflare/src/pages/posts/index.astro @@ -121,7 +121,7 @@ const formatDate = (date: Date) => .page-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } @@ -214,7 +214,7 @@ const formatDate = (date: Date) => .post-title { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); margin-bottom: var(--spacing-2); @@ -222,7 +222,7 @@ const formatDate = (date: Date) => } .post-link:hover .post-title { - color: var(--color-accent); + color: var(--color-brand); } .post-excerpt { diff --git a/blog-cloudflare/src/pages/search.astro b/blog-cloudflare/src/pages/search.astro index 98e8a4e..28a3656 100644 --- a/blog-cloudflare/src/pages/search.astro +++ b/blog-cloudflare/src/pages/search.astro @@ -100,14 +100,14 @@ const { items: results } = query .search-input:focus { outline: none; - border-color: var(--color-accent); + border-color: var(--color-brand); } .search-button { padding: var(--spacing-2) var(--spacing-6); font-size: var(--font-size-base); - background: var(--color-accent); - color: var(--color-on-accent); + background: var(--color-brand); + color: var(--color-on-brand); border: none; border-radius: var(--radius); cursor: pointer; @@ -156,14 +156,14 @@ const { items: results } = query .result-title { font-size: var(--font-size-xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); margin-bottom: var(--spacing-2); transition: color var(--transition-fast); } .result-link:hover .result-title { - color: var(--color-accent); + color: var(--color-brand); } .result-snippet { @@ -174,7 +174,7 @@ const { items: results } = query /* FTS returns wrapping the matched terms */ .result-snippet :global(mark) { - background: var(--color-accent-ring, rgba(99, 102, 241, 0.2)); + background: var(--color-brand-ring); color: inherit; padding: 0 0.1em; border-radius: 2px; diff --git a/blog-cloudflare/src/pages/tag/[slug].astro b/blog-cloudflare/src/pages/tag/[slug].astro index 85dd186..35d43b3 100644 --- a/blog-cloudflare/src/pages/tag/[slug].astro +++ b/blog-cloudflare/src/pages/tag/[slug].astro @@ -89,7 +89,7 @@ const filteredPosts = posts.map((post) => ({ display: block; font-size: var(--font-size-xs); font-weight: 500; - color: var(--color-accent); + color: var(--color-brand); text-transform: uppercase; letter-spacing: var(--tracking-wider); margin-bottom: var(--spacing-2); @@ -97,7 +97,7 @@ const filteredPosts = posts.map((post) => ({ .archive-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } diff --git a/blog-cloudflare/src/styles/theme.css b/blog-cloudflare/src/styles/theme.css index 2f7249b..0ed080e 100644 --- a/blog-cloudflare/src/styles/theme.css +++ b/blog-cloudflare/src/styles/theme.css @@ -1,108 +1,28 @@ /* - theme.css -- override any :root variable here to retheme the blog. + theme.css -- your theme overrides. This is the file to edit to + retheme the site. - This is the only file you need to edit to customize the site's visual - appearance. All defaults are listed below as comments. Uncomment and - change any value to override it. + Every design token lives in src/styles/tokens.css (colors, type + scale, spacing, layout, shadows) with its default value. Override any + of them here: declarations in this file are unlayered, so they always + beat the @layer base defaults in tokens.css. - Base.astro puts its defaults inside @layer base, so declarations here - (which are unlayered) always take priority -- no specificity tricks needed. + Colors are defined with light-dark(). Overriding with a plain color + changes light and dark mode at once; use light-dark(, ) + to set different values per mode. - Note: this template defines explicit dark mode colors in Base.astro. - Overriding light-mode --color-* variables here won't affect dark mode. - To customize dark mode, also override --color-* variables inside a - @media (prefers-color-scheme: dark) block and/or in the :root.dark rule. -*/ - -:root { - /* --- Colors --- - --color-bg: #ffffff; - --color-bg-subtle: #fafafa; - --color-text: #1a1a1a; - --color-text-secondary: #525252; - --color-muted: #8b8b8b; - --color-border: #e5e5e5; - --color-border-subtle: #f0f0f0; - --color-surface: #f7f7f7; - --color-accent: #0066cc; - --color-accent-hover: #0052a3; - --color-on-accent: white; - --color-accent-ring: color-mix(in srgb, var(--color-accent) 25%, transparent); - */ - - /* --- Type scale --- - --font-size-xs: 0.8125rem; - --font-size-sm: 0.875rem; - --font-size-base: 1rem; - --font-size-lg: 1.125rem; - --font-size-xl: 1.25rem; - --font-size-2xl: 1.5rem; - --font-size-3xl: 2rem; - --font-size-4xl: 2.5rem; - --font-size-5xl: 3.5rem; - */ - - /* --- Line heights --- - --leading-tight: 1.15; - --leading-snug: 1.3; - --leading-normal: 1.5; - --leading-relaxed: 1.7; - */ - - /* --- Letter spacing --- - --tracking-tight: -0.03em; used on h1 and large titles - --tracking-snug: -0.02em; used on h2–h6, site/card titles - --tracking-wide: 0.06em; used on meta labels, TOC/widget titles - --tracking-wider: 0.08em; used on footer headings, section labels - */ + Webfonts are loaded in astro.config.mjs (--font-body, --font-mono). + Swap the loaded font there; use overrides here for system fonts or a + separate heading face. - /* --- Spacing --- - --spacing-1: 0.25rem; - --spacing-2: 0.5rem; - --spacing-3: 0.75rem; - --spacing-4: 1rem; - --spacing-5: 1.25rem; - --spacing-6: 1.5rem; - --spacing-8: 2rem; - --spacing-10: 2.5rem; - --spacing-12: 3rem; - --spacing-16: 4rem; - --spacing-20: 5rem; - --spacing-24: 6rem; - */ + Example: - /* --- Layout --- - --content-width: 680px; article/page body column width - --wide-width: 1200px; max container width (home, archives) - --gutter-width: 200px; right sidebar column (TOC) on article pages - --meta-col-width: 180px; left meta column on article pages - --nav-height: 64px; sticky header height - --search-input-width: 180px; nav search box width - */ - - /* --- Borders & radius --- - --radius: 4px; - --radius-lg: 8px; - */ - - /* --- Transitions --- - --transition-fast: 120ms ease; - --transition-base: 180ms ease; - */ - - /* --- Avatars --- - --avatar-size-xs: 18px; card byline avatars - --avatar-size-sm: 20px; post list byline avatars - --avatar-size-md: 24px; featured post byline avatars - --avatar-size-lg: 32px; single post byline avatars - */ - - /* --- Shadows --- - --shadow-dropdown: 0 8px 30px rgba(0, 0, 0, 0.12); - --shadow-btn-active: 0 1px 2px rgba(0, 0, 0, 0.05); - */ + :root { + --color-brand: light-dark(#0f766e, #2dd4bf); + --font-heading: "Iowan Old Style", Georgia, serif; + --radius: 8px; + } +*/ - /* --- Misc --- - --tag-padding-y: 2px; vertical padding on tag pills - */ +:root { } diff --git a/blog-cloudflare/src/styles/tokens.css b/blog-cloudflare/src/styles/tokens.css new file mode 100644 index 0000000..baf58e8 --- /dev/null +++ b/blog-cloudflare/src/styles/tokens.css @@ -0,0 +1,146 @@ +/* + tokens.css -- the design tokens for this template. These are the + defaults; to customize the site, override any variable in + src/styles/theme.css rather than editing this file. Declarations in + theme.css are unlayered, so they always beat these @layer base + defaults -- no specificity tricks needed. + + Colors use light-dark(): the first value applies in light mode, the + second in dark mode. Overriding a color with a plain value changes + both modes at once; use light-dark(, ) in an override to + keep them distinct. + + --font-body is defined by the font pipeline in astro.config.mjs, not + here. To swap the loaded webfont, edit the fonts entry there. To use a + system font, or a different face for headings only, override + --font-body / --font-heading in theme.css. +*/ + +@layer base { + :root { + color-scheme: light dark; + + /* Colors */ + --color-bg: light-dark(#ffffff, #0d0d0d); + --color-bg-subtle: light-dark(#fafafa, #141414); + --color-text: light-dark(#1a1a1a, #ededed); + --color-text-secondary: light-dark(#525252, #a0a0a0); + --color-muted: light-dark(#8b8b8b, #6b6b6b); + --color-border: light-dark(#e5e5e5, #2a2a2a); + --color-border-subtle: light-dark(#f0f0f0, #1f1f1f); + --color-surface: light-dark(#f7f7f7, #181818); + --color-brand: light-dark(#0066cc, #4d9fff); + --color-brand-hover: light-dark(#0052a3, #6eb0ff); + --color-on-brand: white; + --color-brand-ring: color-mix(in srgb, var(--color-brand) 25%, transparent); + + /* EmDash search theming */ + --emdash-search-bg: var(--color-bg); + --emdash-search-text: var(--color-text); + --emdash-search-muted: var(--color-muted); + --emdash-search-border: var(--color-border); + --emdash-search-hover: var(--color-surface); + --emdash-search-highlight: var(--color-text); + + /* Typography */ + --font-heading: var(--font-body); + --font-weight-heading: 600; /* h2-h6, card and list titles */ + --font-weight-display: 700; /* h1 and large page titles */ + + /* Type scale */ + --font-size-xs: 0.8125rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 2rem; + --font-size-4xl: 2.5rem; + --font-size-5xl: 3.5rem; + + /* Line heights */ + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal: 1.5; + --leading-relaxed: 1.7; + + /* Letter spacing */ + --tracking-tight: -0.03em; /* h1 and large titles */ + --tracking-snug: -0.02em; /* h2-h6, site/card titles */ + --tracking-wide: 0.06em; /* meta labels, TOC/widget titles */ + --tracking-wider: 0.08em; /* footer headings, section labels */ + + /* Spacing */ + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-8: 2rem; + --spacing-10: 2.5rem; + --spacing-12: 3rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + + /* Legacy spacing aliases */ + --spacing-xs: var(--spacing-1); + --spacing-sm: var(--spacing-2); + --spacing-md: var(--spacing-4); + --spacing-lg: var(--spacing-6); + --spacing-xl: var(--spacing-8); + --spacing-2xl: var(--spacing-12); + --spacing-3xl: var(--spacing-16); + + /* Layout */ + --content-width: 680px; /* article/page body column */ + --wide-width: 1200px; /* max container (home, archives) */ + --max-width: var(--content-width); + --gutter-width: 200px; /* right sidebar column (TOC) on article pages */ + --meta-col-width: 180px; /* left meta column on article pages */ + --nav-height: 64px; /* sticky header height */ + --search-input-width: 180px; /* nav search box width */ + + /* Borders & radius */ + --radius: 4px; + --radius-lg: 8px; + + /* Transitions */ + --transition-fast: 120ms ease; + --transition-base: 180ms ease; + + /* Avatars */ + --avatar-size-xs: 18px; /* card byline avatars */ + --avatar-size-sm: 20px; /* post list byline avatars */ + --avatar-size-md: 24px; /* featured post byline avatars */ + --avatar-size-lg: 32px; /* single post byline avatars */ + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12); + + /* Misc */ + --tag-padding-y: 2px; /* vertical padding on tag pills */ + } + + /* + * Light-mode values for browsers without light-dark() (Safari < 17.5, + * Chrome < 123). Must mirror the light values above. + */ + @supports not (color: light-dark(#000, #fff)) { + :root { + color-scheme: light; + --color-bg: #ffffff; + --color-bg-subtle: #fafafa; + --color-text: #1a1a1a; + --color-text-secondary: #525252; + --color-muted: #8b8b8b; + --color-border: #e5e5e5; + --color-border-subtle: #f0f0f0; + --color-surface: #f7f7f7; + --color-brand: #0066cc; + --color-brand-hover: #0052a3; + } + } +} diff --git a/blog/.agents/skills/creating-plugins/references/admin-ui.md b/blog/.agents/skills/creating-plugins/references/admin-ui.md index e2289e1..9e26b8d 100644 --- a/blog/.agents/skills/creating-plugins/references/admin-ui.md +++ b/blog/.agents/skills/creating-plugins/references/admin-ui.md @@ -12,7 +12,7 @@ import { SettingsPage } from "./components/SettingsPage"; import { ReportsPage } from "./components/ReportsPage"; import { StatusWidget } from "./components/StatusWidget"; -// Pages keyed by path (must match admin.pages paths) +// Pages keyed by path (trailing slash optional, so /settings and /settings/ both resolve) export const pages = { "/settings": SettingsPage, "/reports": ReportsPage, diff --git a/blog/.agents/skills/creating-plugins/references/api-routes.md b/blog/.agents/skills/creating-plugins/references/api-routes.md index c4d2419..8ecf112 100644 --- a/blog/.agents/skills/creating-plugins/references/api-routes.md +++ b/blog/.agents/skills/creating-plugins/references/api-routes.md @@ -175,7 +175,9 @@ routes: { }, "settings/save": { handler: async (ctx) => { - const input = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input. + // Don't call ctx.request.json() — the body stream is already consumed. + const input = ctx.input as Record; for (const [key, value] of Object.entries(input)) { if (value !== undefined) await ctx.kv.set(`settings:${key}`, value); } diff --git a/blog/.agents/skills/creating-plugins/references/block-kit.md b/blog/.agents/skills/creating-plugins/references/block-kit.md index 397505b..5354e91 100644 --- a/blog/.agents/skills/creating-plugins/references/block-kit.md +++ b/blog/.agents/skills/creating-plugins/references/block-kit.md @@ -16,10 +16,16 @@ Block Kit elements are also used for [Portable Text block editing fields](./port 6. Plugin returns new blocks ```typescript +import type { BlockInteraction } from "@emdash-cms/blocks"; + routes: { admin: { handler: async (ctx) => { - const interaction = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input; + // read it directly rather than ctx.request.json() (the body is consumed). + // BlockInteraction is the discriminated union of page_load, + // block_action, and form_submit payloads. + const interaction = ctx.input as BlockInteraction; if (interaction.type === "page_load") { return { diff --git a/blog/AGENTS.md b/blog/AGENTS.md index 912334c..3a15e60 100644 --- a/blog/AGENTS.md +++ b/blog/AGENTS.md @@ -70,23 +70,26 @@ Site settings have `title` and `tagline` -- both render in the header / footer. ## Visual character -Single typeface: **Inter** on `--font-sans`, used for everything including headings (with tighter letter-spacing on h1/h2). **JetBrains Mono** on `--font-mono` for inline code and code blocks. Body and headings share the same family; weight and size carry the hierarchy. +Single typeface: **Inter** on `--font-body`, used for everything including headings (`--font-heading` defaults to the body face; tighter letter-spacing on h1/h2). **JetBrains Mono** on `--font-mono` for inline code and code blocks. Body and headings share the same family; weight and size carry the hierarchy (`--font-weight-heading` 600, `--font-weight-display` 700 for h1/page titles). -The accent is `#0066cc` -- used for links, the post-card title hover, and the search input focus ring. There's also a secondary text colour (`--color-text-secondary`) and a `--color-muted` for meta info. Don't add a second accent. +The brand colour is `#0066cc` (`--color-brand`) -- used for links, the post-card title hover, and the search input focus ring. There's also a secondary text colour (`--color-text-secondary`) and a `--color-muted` for meta info. Don't add a second accent. The article layout is the standout feature: a three-column reading view with a left meta column (author bylines, date), centred 680px body column, and a right gutter for search, table of contents, and categories. Don't flatten that into one column on desktop -- the layout signals "this is something to read". ## Customisation -`src/styles/theme.css` is the only file to edit for visual changes. Every CSS variable from `Base.astro` is listed there as a commented default -- uncomment and change to override. The dark mode palette is defined inside `Base.astro` itself; light-mode overrides in `theme.css` won't affect dark mode. To customise dark mode, add `@media (prefers-color-scheme: dark)` and `:root.dark` rules in `theme.css`. +Design tokens live in `src/styles/tokens.css` with their default values. To restyle the site, override tokens in `src/styles/theme.css` -- declarations there are unlayered, so they always beat the `@layer base` defaults. Don't edit `tokens.css` or `Base.astro` for visual changes. -Fonts are configured in `astro.config.mjs` under `fonts:`. To swap the body face, change the `name:` for the entry bound to `cssVariable: "--font-sans"`. Good alternatives: Geist, IBM Plex Sans, Söhne (if you have a licence), Public Sans. If you want a serif-bodied blog, swap to a humanist serif like Source Serif, Crimson Pro, or Lora -- but then also raise `--font-size-base` to `1.0625rem` for readability. +Colours are defined with `light-dark(, )`, so each token carries both modes. Overriding with a plain colour changes light and dark at once; use `light-dark()` in the override to keep them distinct. There is no separate dark palette to maintain. -CSS variables worth knowing: +Webfonts are configured in `astro.config.mjs` under `fonts:`. To swap the body face, change the `name:` for the entry bound to `cssVariable: "--font-body"`. Good alternatives: Geist, IBM Plex Sans, Söhne (if you have a licence), Public Sans. If you want a serif-bodied blog, swap to a humanist serif like Source Serif, Crimson Pro, or Lora -- but then also raise `--font-size-base` to `1.0625rem` for readability. To give headings their own face (or use a system font) without touching the font pipeline, override `--font-heading` or `--font-body` in `theme.css`. -- `--color-accent`, `--color-accent-hover`, `--color-on-accent`, `--color-accent-ring` +CSS variables worth knowing (see `tokens.css` for the full list): + +- `--color-brand`, `--color-brand-hover`, `--color-on-brand`, `--color-brand-ring` - `--color-bg`, `--color-bg-subtle`, `--color-surface`, `--color-text`, `--color-text-secondary`, `--color-muted`, `--color-border`, `--color-border-subtle` -- `--font-sans`, `--font-mono` +- `--font-body`, `--font-heading`, `--font-mono` +- `--font-weight-heading` (600) / `--font-weight-display` (700) -- heading weights; lower them if you switch to a serif - `--tracking-tight` / `--tracking-snug` / `--tracking-wide` / `--tracking-wider` -- letter-spacing tokens used across headings and meta labels - `--content-width` (680px) -- article body column - `--wide-width` (1200px) -- max container diff --git a/blog/astro.config.mjs b/blog/astro.config.mjs index 726b23c..19e1d9a 100644 --- a/blog/astro.config.mjs +++ b/blog/astro.config.mjs @@ -29,7 +29,7 @@ export default defineConfig({ { provider: fontProviders.google(), name: "Inter", - cssVariable: "--font-sans", + cssVariable: "--font-body", weights: [400, 500, 600, 700], fallbacks: ["sans-serif"], }, diff --git a/blog/emdash-env.d.ts b/blog/emdash-env.d.ts index e1a5a89..4c380e8 100644 --- a/blog/emdash-env.d.ts +++ b/blog/emdash-env.d.ts @@ -23,7 +23,7 @@ export interface Post { slug: string | null; status: string; title: string; - featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number }; + featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record }; content?: PortableTextBlock[]; excerpt?: string; createdAt: Date; diff --git a/blog/package.json b/blog/package.json index fda679f..ca22f24 100644 --- a/blog/package.json +++ b/blog/package.json @@ -19,7 +19,7 @@ "@emdash-cms/plugin-audit-log": "^0.2.0", "astro": "^7.0.0", "better-sqlite3": "^12.8.0", - "emdash": "^0.27.0", + "emdash": "^0.28.1", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/blog/src/components/PostCard.astro b/blog/src/components/PostCard.astro index 09d1d9d..80fa416 100644 --- a/blog/src/components/PostCard.astro +++ b/blog/src/components/PostCard.astro @@ -173,7 +173,7 @@ const formattedDate = date .card-title { font-size: var(--font-size-xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); margin-bottom: var(--spacing-2); @@ -181,7 +181,7 @@ const formattedDate = date } .card-link:hover .card-title { - color: var(--color-accent); + color: var(--color-brand); } .card-excerpt { @@ -256,7 +256,7 @@ const formattedDate = date } .byline-more:focus-visible { - outline: 2px solid var(--color-accent); + outline: 2px solid var(--color-brand); } .byline-more[data-tooltip]:hover::after, diff --git a/blog/src/layouts/Base.astro b/blog/src/layouts/Base.astro index 67c46e3..30520cd 100644 --- a/blog/src/layouts/Base.astro +++ b/blog/src/layouts/Base.astro @@ -10,6 +10,7 @@ import { createPublicPageContext } from "emdash/page"; import LiveSearch from "emdash/ui/search"; import { Font } from "astro:assets"; import { resolveBlogSiteIdentity } from "../utils/site-identity"; +import "../styles/tokens.css"; import "../styles/theme.css"; interface Props { @@ -81,20 +82,19 @@ const isLoggedIn = !!Astro.locals.user; - + {fullTitle} @@ -304,9 +304,6 @@ const isLoggedIn = !!Astro.locals.user; if (theme === "system") { setCookie("theme", ""); root.classList.remove("light", "dark"); - if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - root.classList.add("dark"); - } } else { setCookie("theme", theme); root.classList.remove("light", "dark"); @@ -335,15 +332,6 @@ const isLoggedIn = !!Astro.locals.user; setTheme(btn.dataset.theme || "system"); }); }); - - // Listen for system preference changes - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - if (getStoredTheme() === "system") { - root.classList.toggle("dark", e.matches); - } - }); @@ -606,7 +474,7 @@ const isLoggedIn = !!Astro.locals.user; } .site-title:hover { - color: var(--color-accent); + color: var(--color-brand); } .site-logo-img { @@ -636,7 +504,7 @@ const isLoggedIn = !!Astro.locals.user; } .nav-links a:hover { - color: var(--color-accent); + color: var(--color-brand); } .nav-admin { @@ -654,13 +522,13 @@ const isLoggedIn = !!Astro.locals.user; .site-search { position: relative; width: var(--search-input-width); - --emdash-search-border-focus: var(--color-accent); + --emdash-search-border-focus: var(--color-brand); } :global(.site-search-input) { width: var(--search-input-width); padding: var(--spacing-2) var(--spacing-3); - font-family: var(--font-sans); + font-family: var(--font-body); font-size: var(--font-size-sm); border: 1px solid var(--color-border); border-radius: var(--radius); @@ -678,8 +546,8 @@ const isLoggedIn = !!Astro.locals.user; :global(.site-search-input):focus, :global(.site-search-input):focus-visible { outline: none; - border-color: var(--color-accent) !important; - box-shadow: 0 0 0 3px var(--color-accent-ring); + border-color: var(--color-brand) !important; + box-shadow: 0 0 0 3px var(--color-brand-ring); } :global(.site-search-results) { @@ -691,7 +559,7 @@ const isLoggedIn = !!Astro.locals.user; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-lg); - box-shadow: var(--shadow-dropdown); + box-shadow: var(--shadow-lg); max-height: 400px; overflow-y: auto; z-index: 1000; @@ -902,7 +770,7 @@ const isLoggedIn = !!Astro.locals.user; .theme-btn.active { background: var(--color-bg); color: var(--color-text); - box-shadow: var(--shadow-btn-active); + box-shadow: var(--shadow-sm); } .theme-btn svg { diff --git a/blog/src/pages/category/[slug].astro b/blog/src/pages/category/[slug].astro index f0bbf70..64bd0aa 100644 --- a/blog/src/pages/category/[slug].astro +++ b/blog/src/pages/category/[slug].astro @@ -87,7 +87,7 @@ const filteredPosts = posts.map((post) => ({ display: block; font-size: var(--font-size-xs); font-weight: 500; - color: var(--color-accent); + color: var(--color-brand); text-transform: uppercase; letter-spacing: var(--tracking-wider); margin-bottom: var(--spacing-2); @@ -95,7 +95,7 @@ const filteredPosts = posts.map((post) => ({ .archive-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } diff --git a/blog/src/pages/index.astro b/blog/src/pages/index.astro index da6b71c..f49ef41 100644 --- a/blog/src/pages/index.astro +++ b/blog/src/pages/index.astro @@ -302,7 +302,7 @@ function formatDate(date: Date | null | undefined) { .featured-title { font-size: clamp(var(--font-size-2xl), 4vw, var(--font-size-4xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); transition: color var(--transition-fast); @@ -310,7 +310,7 @@ function formatDate(date: Date | null | undefined) { .featured-title-link:hover .featured-title, .featured-grid:has(.featured-image-link:hover) .featured-title { - color: var(--color-accent); + color: var(--color-brand); } .featured-excerpt { @@ -364,13 +364,13 @@ function formatDate(date: Date | null | undefined) { .section-link { font-size: var(--font-size-sm); - color: var(--color-accent); + color: var(--color-brand); text-decoration: none; transition: color var(--transition-fast); } .section-link:hover { - color: var(--color-accent-hover); + color: var(--color-brand-hover); } /* Posts Grid */ @@ -397,7 +397,7 @@ function formatDate(date: Date | null | undefined) { .empty-state h2 { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); } .empty-state p { @@ -408,8 +408,8 @@ function formatDate(date: Date | null | undefined) { display: inline-block; margin-top: var(--spacing-4); padding: var(--spacing-3) var(--spacing-6); - background: var(--color-accent); - color: var(--color-on-accent); + background: var(--color-brand); + color: var(--color-on-brand); text-decoration: none; border-radius: var(--radius); font-size: var(--font-size-sm); @@ -418,7 +418,7 @@ function formatDate(date: Date | null | undefined) { } .btn:hover { - background: var(--color-accent-hover); + background: var(--color-brand-hover); } /* Responsive */ diff --git a/blog/src/pages/pages/[slug].astro b/blog/src/pages/pages/[slug].astro index 3c15fea..db1753c 100644 --- a/blog/src/pages/pages/[slug].astro +++ b/blog/src/pages/pages/[slug].astro @@ -46,7 +46,7 @@ if (Astro.cache?.enabled) Astro.cache.set(cacheHint); .page-title { font-size: clamp(var(--font-size-2xl), 4vw, var(--font-size-4xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); } diff --git a/blog/src/pages/posts/[slug].astro b/blog/src/pages/posts/[slug].astro index 0cc892f..0d45757 100644 --- a/blog/src/pages/posts/[slug].astro +++ b/blog/src/pages/posts/[slug].astro @@ -495,7 +495,7 @@ const publishDate = .article-title { font-size: clamp(2rem, 5vw, var(--font-size-5xl)); - font-weight: 700; + font-weight: var(--font-weight-display); line-height: var(--leading-tight); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-4); @@ -588,7 +588,7 @@ const publishDate = } .article-content :global(a) { - color: var(--color-accent); + color: var(--color-brand); text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; @@ -687,7 +687,7 @@ const publishDate = .sidebar-widgets :global(.widget-search__input) { width: 100%; padding: var(--spacing-2) var(--spacing-3); - font-family: var(--font-sans); + font-family: var(--font-body); font-size: var(--font-size-sm); border: 1px solid var(--color-border); border-radius: var(--radius); @@ -705,8 +705,8 @@ const publishDate = .sidebar-widgets :global(.widget-search__input):focus, .sidebar-widgets :global(.widget-search__input):focus-visible { outline: none; - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-ring); + border-color: var(--color-brand); + box-shadow: 0 0 0 3px var(--color-brand-ring); } .sidebar-widgets :global(.widget-search__button) { @@ -863,7 +863,7 @@ const publishDate = .article-comments :global(.ec-comments-heading) { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); margin-bottom: var(--spacing-8); } @@ -893,8 +893,8 @@ const publishDate = } .article-comments :global(.ec-comment-form-submit) { - background: var(--color-accent) !important; - color: var(--color-on-accent) !important; + background: var(--color-brand) !important; + color: var(--color-on-brand) !important; } /* More posts section */ @@ -912,7 +912,7 @@ const publishDate = .more-title { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); margin-bottom: var(--spacing-10); } diff --git a/blog/src/pages/posts/index.astro b/blog/src/pages/posts/index.astro index 6c8c3b7..537ebb8 100644 --- a/blog/src/pages/posts/index.astro +++ b/blog/src/pages/posts/index.astro @@ -121,7 +121,7 @@ const formatDate = (date: Date) => .page-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } @@ -214,7 +214,7 @@ const formatDate = (date: Date) => .post-title { font-size: var(--font-size-2xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); letter-spacing: var(--tracking-snug); margin-bottom: var(--spacing-2); @@ -222,7 +222,7 @@ const formatDate = (date: Date) => } .post-link:hover .post-title { - color: var(--color-accent); + color: var(--color-brand); } .post-excerpt { diff --git a/blog/src/pages/search.astro b/blog/src/pages/search.astro index 98e8a4e..28a3656 100644 --- a/blog/src/pages/search.astro +++ b/blog/src/pages/search.astro @@ -100,14 +100,14 @@ const { items: results } = query .search-input:focus { outline: none; - border-color: var(--color-accent); + border-color: var(--color-brand); } .search-button { padding: var(--spacing-2) var(--spacing-6); font-size: var(--font-size-base); - background: var(--color-accent); - color: var(--color-on-accent); + background: var(--color-brand); + color: var(--color-on-brand); border: none; border-radius: var(--radius); cursor: pointer; @@ -156,14 +156,14 @@ const { items: results } = query .result-title { font-size: var(--font-size-xl); - font-weight: 600; + font-weight: var(--font-weight-heading); line-height: var(--leading-snug); margin-bottom: var(--spacing-2); transition: color var(--transition-fast); } .result-link:hover .result-title { - color: var(--color-accent); + color: var(--color-brand); } .result-snippet { @@ -174,7 +174,7 @@ const { items: results } = query /* FTS returns wrapping the matched terms */ .result-snippet :global(mark) { - background: var(--color-accent-ring, rgba(99, 102, 241, 0.2)); + background: var(--color-brand-ring); color: inherit; padding: 0 0.1em; border-radius: 2px; diff --git a/blog/src/pages/tag/[slug].astro b/blog/src/pages/tag/[slug].astro index 85dd186..35d43b3 100644 --- a/blog/src/pages/tag/[slug].astro +++ b/blog/src/pages/tag/[slug].astro @@ -89,7 +89,7 @@ const filteredPosts = posts.map((post) => ({ display: block; font-size: var(--font-size-xs); font-weight: 500; - color: var(--color-accent); + color: var(--color-brand); text-transform: uppercase; letter-spacing: var(--tracking-wider); margin-bottom: var(--spacing-2); @@ -97,7 +97,7 @@ const filteredPosts = posts.map((post) => ({ .archive-title { font-size: var(--font-size-4xl); - font-weight: 700; + font-weight: var(--font-weight-display); letter-spacing: var(--tracking-tight); margin-bottom: var(--spacing-2); } diff --git a/blog/src/styles/theme.css b/blog/src/styles/theme.css index 2f7249b..0ed080e 100644 --- a/blog/src/styles/theme.css +++ b/blog/src/styles/theme.css @@ -1,108 +1,28 @@ /* - theme.css -- override any :root variable here to retheme the blog. + theme.css -- your theme overrides. This is the file to edit to + retheme the site. - This is the only file you need to edit to customize the site's visual - appearance. All defaults are listed below as comments. Uncomment and - change any value to override it. + Every design token lives in src/styles/tokens.css (colors, type + scale, spacing, layout, shadows) with its default value. Override any + of them here: declarations in this file are unlayered, so they always + beat the @layer base defaults in tokens.css. - Base.astro puts its defaults inside @layer base, so declarations here - (which are unlayered) always take priority -- no specificity tricks needed. + Colors are defined with light-dark(). Overriding with a plain color + changes light and dark mode at once; use light-dark(, ) + to set different values per mode. - Note: this template defines explicit dark mode colors in Base.astro. - Overriding light-mode --color-* variables here won't affect dark mode. - To customize dark mode, also override --color-* variables inside a - @media (prefers-color-scheme: dark) block and/or in the :root.dark rule. -*/ - -:root { - /* --- Colors --- - --color-bg: #ffffff; - --color-bg-subtle: #fafafa; - --color-text: #1a1a1a; - --color-text-secondary: #525252; - --color-muted: #8b8b8b; - --color-border: #e5e5e5; - --color-border-subtle: #f0f0f0; - --color-surface: #f7f7f7; - --color-accent: #0066cc; - --color-accent-hover: #0052a3; - --color-on-accent: white; - --color-accent-ring: color-mix(in srgb, var(--color-accent) 25%, transparent); - */ - - /* --- Type scale --- - --font-size-xs: 0.8125rem; - --font-size-sm: 0.875rem; - --font-size-base: 1rem; - --font-size-lg: 1.125rem; - --font-size-xl: 1.25rem; - --font-size-2xl: 1.5rem; - --font-size-3xl: 2rem; - --font-size-4xl: 2.5rem; - --font-size-5xl: 3.5rem; - */ - - /* --- Line heights --- - --leading-tight: 1.15; - --leading-snug: 1.3; - --leading-normal: 1.5; - --leading-relaxed: 1.7; - */ - - /* --- Letter spacing --- - --tracking-tight: -0.03em; used on h1 and large titles - --tracking-snug: -0.02em; used on h2–h6, site/card titles - --tracking-wide: 0.06em; used on meta labels, TOC/widget titles - --tracking-wider: 0.08em; used on footer headings, section labels - */ + Webfonts are loaded in astro.config.mjs (--font-body, --font-mono). + Swap the loaded font there; use overrides here for system fonts or a + separate heading face. - /* --- Spacing --- - --spacing-1: 0.25rem; - --spacing-2: 0.5rem; - --spacing-3: 0.75rem; - --spacing-4: 1rem; - --spacing-5: 1.25rem; - --spacing-6: 1.5rem; - --spacing-8: 2rem; - --spacing-10: 2.5rem; - --spacing-12: 3rem; - --spacing-16: 4rem; - --spacing-20: 5rem; - --spacing-24: 6rem; - */ + Example: - /* --- Layout --- - --content-width: 680px; article/page body column width - --wide-width: 1200px; max container width (home, archives) - --gutter-width: 200px; right sidebar column (TOC) on article pages - --meta-col-width: 180px; left meta column on article pages - --nav-height: 64px; sticky header height - --search-input-width: 180px; nav search box width - */ - - /* --- Borders & radius --- - --radius: 4px; - --radius-lg: 8px; - */ - - /* --- Transitions --- - --transition-fast: 120ms ease; - --transition-base: 180ms ease; - */ - - /* --- Avatars --- - --avatar-size-xs: 18px; card byline avatars - --avatar-size-sm: 20px; post list byline avatars - --avatar-size-md: 24px; featured post byline avatars - --avatar-size-lg: 32px; single post byline avatars - */ - - /* --- Shadows --- - --shadow-dropdown: 0 8px 30px rgba(0, 0, 0, 0.12); - --shadow-btn-active: 0 1px 2px rgba(0, 0, 0, 0.05); - */ + :root { + --color-brand: light-dark(#0f766e, #2dd4bf); + --font-heading: "Iowan Old Style", Georgia, serif; + --radius: 8px; + } +*/ - /* --- Misc --- - --tag-padding-y: 2px; vertical padding on tag pills - */ +:root { } diff --git a/blog/src/styles/tokens.css b/blog/src/styles/tokens.css new file mode 100644 index 0000000..baf58e8 --- /dev/null +++ b/blog/src/styles/tokens.css @@ -0,0 +1,146 @@ +/* + tokens.css -- the design tokens for this template. These are the + defaults; to customize the site, override any variable in + src/styles/theme.css rather than editing this file. Declarations in + theme.css are unlayered, so they always beat these @layer base + defaults -- no specificity tricks needed. + + Colors use light-dark(): the first value applies in light mode, the + second in dark mode. Overriding a color with a plain value changes + both modes at once; use light-dark(, ) in an override to + keep them distinct. + + --font-body is defined by the font pipeline in astro.config.mjs, not + here. To swap the loaded webfont, edit the fonts entry there. To use a + system font, or a different face for headings only, override + --font-body / --font-heading in theme.css. +*/ + +@layer base { + :root { + color-scheme: light dark; + + /* Colors */ + --color-bg: light-dark(#ffffff, #0d0d0d); + --color-bg-subtle: light-dark(#fafafa, #141414); + --color-text: light-dark(#1a1a1a, #ededed); + --color-text-secondary: light-dark(#525252, #a0a0a0); + --color-muted: light-dark(#8b8b8b, #6b6b6b); + --color-border: light-dark(#e5e5e5, #2a2a2a); + --color-border-subtle: light-dark(#f0f0f0, #1f1f1f); + --color-surface: light-dark(#f7f7f7, #181818); + --color-brand: light-dark(#0066cc, #4d9fff); + --color-brand-hover: light-dark(#0052a3, #6eb0ff); + --color-on-brand: white; + --color-brand-ring: color-mix(in srgb, var(--color-brand) 25%, transparent); + + /* EmDash search theming */ + --emdash-search-bg: var(--color-bg); + --emdash-search-text: var(--color-text); + --emdash-search-muted: var(--color-muted); + --emdash-search-border: var(--color-border); + --emdash-search-hover: var(--color-surface); + --emdash-search-highlight: var(--color-text); + + /* Typography */ + --font-heading: var(--font-body); + --font-weight-heading: 600; /* h2-h6, card and list titles */ + --font-weight-display: 700; /* h1 and large page titles */ + + /* Type scale */ + --font-size-xs: 0.8125rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 2rem; + --font-size-4xl: 2.5rem; + --font-size-5xl: 3.5rem; + + /* Line heights */ + --leading-tight: 1.15; + --leading-snug: 1.3; + --leading-normal: 1.5; + --leading-relaxed: 1.7; + + /* Letter spacing */ + --tracking-tight: -0.03em; /* h1 and large titles */ + --tracking-snug: -0.02em; /* h2-h6, site/card titles */ + --tracking-wide: 0.06em; /* meta labels, TOC/widget titles */ + --tracking-wider: 0.08em; /* footer headings, section labels */ + + /* Spacing */ + --spacing-1: 0.25rem; + --spacing-2: 0.5rem; + --spacing-3: 0.75rem; + --spacing-4: 1rem; + --spacing-5: 1.25rem; + --spacing-6: 1.5rem; + --spacing-8: 2rem; + --spacing-10: 2.5rem; + --spacing-12: 3rem; + --spacing-16: 4rem; + --spacing-20: 5rem; + --spacing-24: 6rem; + + /* Legacy spacing aliases */ + --spacing-xs: var(--spacing-1); + --spacing-sm: var(--spacing-2); + --spacing-md: var(--spacing-4); + --spacing-lg: var(--spacing-6); + --spacing-xl: var(--spacing-8); + --spacing-2xl: var(--spacing-12); + --spacing-3xl: var(--spacing-16); + + /* Layout */ + --content-width: 680px; /* article/page body column */ + --wide-width: 1200px; /* max container (home, archives) */ + --max-width: var(--content-width); + --gutter-width: 200px; /* right sidebar column (TOC) on article pages */ + --meta-col-width: 180px; /* left meta column on article pages */ + --nav-height: 64px; /* sticky header height */ + --search-input-width: 180px; /* nav search box width */ + + /* Borders & radius */ + --radius: 4px; + --radius-lg: 8px; + + /* Transitions */ + --transition-fast: 120ms ease; + --transition-base: 180ms ease; + + /* Avatars */ + --avatar-size-xs: 18px; /* card byline avatars */ + --avatar-size-sm: 20px; /* post list byline avatars */ + --avatar-size-md: 24px; /* featured post byline avatars */ + --avatar-size-lg: 32px; /* single post byline avatars */ + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12); + + /* Misc */ + --tag-padding-y: 2px; /* vertical padding on tag pills */ + } + + /* + * Light-mode values for browsers without light-dark() (Safari < 17.5, + * Chrome < 123). Must mirror the light values above. + */ + @supports not (color: light-dark(#000, #fff)) { + :root { + color-scheme: light; + --color-bg: #ffffff; + --color-bg-subtle: #fafafa; + --color-text: #1a1a1a; + --color-text-secondary: #525252; + --color-muted: #8b8b8b; + --color-border: #e5e5e5; + --color-border-subtle: #f0f0f0; + --color-surface: #f7f7f7; + --color-brand: #0066cc; + --color-brand-hover: #0052a3; + } + } +} diff --git a/marketing-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md b/marketing-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md index e2289e1..9e26b8d 100644 --- a/marketing-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md +++ b/marketing-cloudflare/.agents/skills/creating-plugins/references/admin-ui.md @@ -12,7 +12,7 @@ import { SettingsPage } from "./components/SettingsPage"; import { ReportsPage } from "./components/ReportsPage"; import { StatusWidget } from "./components/StatusWidget"; -// Pages keyed by path (must match admin.pages paths) +// Pages keyed by path (trailing slash optional, so /settings and /settings/ both resolve) export const pages = { "/settings": SettingsPage, "/reports": ReportsPage, diff --git a/marketing-cloudflare/.agents/skills/creating-plugins/references/api-routes.md b/marketing-cloudflare/.agents/skills/creating-plugins/references/api-routes.md index c4d2419..8ecf112 100644 --- a/marketing-cloudflare/.agents/skills/creating-plugins/references/api-routes.md +++ b/marketing-cloudflare/.agents/skills/creating-plugins/references/api-routes.md @@ -175,7 +175,9 @@ routes: { }, "settings/save": { handler: async (ctx) => { - const input = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input. + // Don't call ctx.request.json() — the body stream is already consumed. + const input = ctx.input as Record; for (const [key, value] of Object.entries(input)) { if (value !== undefined) await ctx.kv.set(`settings:${key}`, value); } diff --git a/marketing-cloudflare/.agents/skills/creating-plugins/references/block-kit.md b/marketing-cloudflare/.agents/skills/creating-plugins/references/block-kit.md index 397505b..5354e91 100644 --- a/marketing-cloudflare/.agents/skills/creating-plugins/references/block-kit.md +++ b/marketing-cloudflare/.agents/skills/creating-plugins/references/block-kit.md @@ -16,10 +16,16 @@ Block Kit elements are also used for [Portable Text block editing fields](./port 6. Plugin returns new blocks ```typescript +import type { BlockInteraction } from "@emdash-cms/blocks"; + routes: { admin: { handler: async (ctx) => { - const interaction = await ctx.request.json(); + // EmDash parses the request body once and exposes it as ctx.input; + // read it directly rather than ctx.request.json() (the body is consumed). + // BlockInteraction is the discriminated union of page_load, + // block_action, and form_submit payloads. + const interaction = ctx.input as BlockInteraction; if (interaction.type === "page_load") { return { diff --git a/marketing-cloudflare/AGENTS.md b/marketing-cloudflare/AGENTS.md index cd7d0ec..6bea048 100644 --- a/marketing-cloudflare/AGENTS.md +++ b/marketing-cloudflare/AGENTS.md @@ -88,37 +88,43 @@ Constraints worth remembering: ## Visual character -Typography is **Inter** on `--font-sans` with weights up to 800 for headline emphasis. There is no mono font, no serif. Headline tracking is tight. +Typography is **Inter** on `--font-body` with weights up to 800 for headline emphasis (`--font-weight-display: 800` on hero and section headlines, `--font-weight-heading: 700` on other headings). There is no mono font, no serif. Headline tracking is tight (`--tracking-tight`). Colour is the loudest of any template here. The default palette is: -- `--color-primary: #6366f1` (indigo) -- main brand colour, used in buttons and links -- `--color-accent: #f472b6` (pink) -- paired with primary in gradients (CTA buttons, icon backgrounds) -- `--color-success`, `--color-warning` -- semantic colours for inline icons (pricing checkmarks) +- `--color-brand: #6366f1` (indigo) -- main brand colour, used in buttons and links, with `--color-brand-strong` / `--color-brand-soft` shades +- `--color-accent: #f472b6` (pink) -- the gradient partner to brand +- `--color-success`, `--color-warning`, `--color-danger` -- semantic colours (pricing checkmarks, form errors) -Gradients are part of the look (`--color-primary` -> `--color-accent` on the "Get Started" button, on contact-method icons, on the "Most popular" pricing badge). Don't strip them entirely -- the template will look generic without them. Do swap them for a different pair if the brand calls for it. +Gradients are part of the look and are tokens themselves: `--gradient-brand` (logo, icon tiles, pricing badge, CTA hover), `--gradient-brand-strong` (CTA resting state), `--gradient-brand-soft` (hero image glow), and `--gradient-headline` (hero headline text fill). They follow the brand/accent colours automatically, so a rebrand usually only needs new `--color-brand-*` / `--color-accent-*` values. Don't strip the gradients entirely -- the template will look generic without them -- but a flat brand can set the `--gradient-*` tokens to solid colours. + +Shared utility classes keep the blocks consistent: `.section-header` / `.section-headline` / `.section-subheadline` for centred block intros, and `.icon-tile` for the 48px gradient icon squares. Use them in new blocks rather than restyling per block. Roundness is generous: `--radius` is 10px, `--radius-lg` 16px, plus a `--radius-full` for pills. Shadows are layered (`--shadow-sm` through `--shadow-xl`). ## Customisation -`src/styles/theme.css` is the only file to edit for visual changes. Every CSS variable from `Base.astro` is listed there as a commented default. The dark mode palette is defined inside `Base.astro`; light-mode overrides in `theme.css` won't affect dark mode. To customise dark mode, add `@media (prefers-color-scheme: dark)` and `:root.dark` rules in `theme.css`. +Design tokens live in `src/styles/tokens.css` with their default values. To restyle the site, override tokens in `src/styles/theme.css` -- declarations there are unlayered, so they always beat the `@layer base` defaults. Don't edit `tokens.css` or `Base.astro` for visual changes. + +Colours are defined with `light-dark(, )`, so each token carries both modes. Overriding with a plain colour changes light and dark at once; use `light-dark()` in the override to keep them distinct. There is no separate dark palette to maintain. -Fonts are configured in `astro.config.mjs` under `fonts:`. To swap the typeface, change the `name:` for the entry bound to `cssVariable: "--font-sans"`. Inter has 5 weights loaded (400-800) for hero impact -- if you swap, ensure the replacement has comparable weight range. Geist, Plus Jakarta Sans, Manrope, and DM Sans all work well as replacements. +Webfonts are configured in `astro.config.mjs` under `fonts:`. To swap the typeface, change the `name:` for the entry bound to `cssVariable: "--font-body"`. Inter has 5 weights loaded (400-800) for hero impact -- if you swap, ensure the replacement has comparable weight range. Geist, Plus Jakarta Sans, Manrope, and DM Sans all work well as replacements. For a system font, or a separate heading face, override `--font-body` / `--font-heading` in `theme.css`. A softer voice (editorial, luxury) usually also wants `--font-weight-display: 700` or lower. -CSS variables worth knowing: +CSS variables worth knowing (see `tokens.css` for the full list): -- `--color-primary`, `--color-primary-dark`, `--color-primary-light` -- `--color-accent`, `--color-accent-light` +- `--color-brand`, `--color-brand-strong`, `--color-brand-soft`, `--color-on-brand`, `--color-brand-ring` +- `--color-accent`, `--color-accent-soft` +- `--gradient-brand`, `--gradient-brand-strong`, `--gradient-brand-soft`, `--gradient-headline` - `--color-bg`, `--color-surface`, `--color-text`, `--color-muted`, `--color-border` -- `--font-sans` +- `--color-success`, `--color-warning`, `--color-danger` +- `--font-body`, `--font-heading`, `--font-weight-heading` (700), `--font-weight-display` (800) - `--font-size-{xs,sm,base,lg,xl,2xl,3xl,4xl,5xl,6xl}` -- type scale up to 4.5rem for the largest hero - `--radius-sm` (6px), `--radius` (10px), `--radius-lg` (16px), `--radius-full` - `--shadow-sm`, `--shadow`, `--shadow-lg`, `--shadow-xl` To re-brand, the highest-leverage moves are: -1. Change `--color-primary` and `--color-accent` to the brand pair. +1. Change `--color-brand` (and its `-strong` / `-soft` shades) and `--color-accent` to the brand pair -- the gradients follow. 2. Update the site title (logo wordmark) and tagline. 3. Replace the hero illustration URL. 4. Edit hero `headline` and `subheadline` blocks to specific, concrete copy. diff --git a/marketing-cloudflare/astro.config.mjs b/marketing-cloudflare/astro.config.mjs index b42d55c..37bc5cd 100644 --- a/marketing-cloudflare/astro.config.mjs +++ b/marketing-cloudflare/astro.config.mjs @@ -68,7 +68,7 @@ export default defineConfig({ { provider: fontProviders.google(), name: "Inter", - cssVariable: "--font-sans", + cssVariable: "--font-body", weights: [400, 500, 600, 700, 800], fallbacks: ["sans-serif"], }, diff --git a/marketing-cloudflare/package.json b/marketing-cloudflare/package.json index 979cb30..8160293 100644 --- a/marketing-cloudflare/package.json +++ b/marketing-cloudflare/package.json @@ -16,11 +16,11 @@ "dependencies": { "@astrojs/cloudflare": "^14.0.0", "@astrojs/react": "^6.0.0", - "@emdash-cms/cloudflare": "^0.27.0", + "@emdash-cms/cloudflare": "^0.28.1", "@iconify-json/ph": "^1.2.2", "astro": "^7.0.0", "astro-iconset": "^0.0.4", - "emdash": "^0.27.0", + "emdash": "^0.28.1", "react": "19.2.4", "react-dom": "19.2.4" }, diff --git a/marketing-cloudflare/src/components/blocks/FAQ.astro b/marketing-cloudflare/src/components/blocks/FAQ.astro index d313455..d4b3603 100644 --- a/marketing-cloudflare/src/components/blocks/FAQ.astro +++ b/marketing-cloudflare/src/components/blocks/FAQ.astro @@ -17,8 +17,8 @@ const { _key, headline, items } = node;
{headline && ( -
-

{headline}

+
+

{headline}

)}
@@ -40,16 +40,6 @@ const { _key, headline, items } = node;
diff --git a/marketing-cloudflare/src/components/blocks/Features.astro b/marketing-cloudflare/src/components/blocks/Features.astro index 71a5133..33cbb10 100644 --- a/marketing-cloudflare/src/components/blocks/Features.astro +++ b/marketing-cloudflare/src/components/blocks/Features.astro @@ -36,15 +36,15 @@ const iconMap: Record = {
{(headline || subheadline) && ( -
- {headline &&

{headline}

} - {subheadline &&

{subheadline}

} +
+ {headline &&

{headline}

} + {subheadline &&

{subheadline}

}
)}
{features.map((feature) => (
-
+

{feature.title}

@@ -56,23 +56,6 @@ const iconMap: Record = {
diff --git a/marketing/src/components/blocks/Features.astro b/marketing/src/components/blocks/Features.astro index 71a5133..33cbb10 100644 --- a/marketing/src/components/blocks/Features.astro +++ b/marketing/src/components/blocks/Features.astro @@ -36,15 +36,15 @@ const iconMap: Record = {
{(headline || subheadline) && ( -
- {headline &&

{headline}

} - {subheadline &&

{subheadline}

} +
+ {headline &&

{headline}

} + {subheadline &&

{subheadline}

}
)}
{features.map((feature) => (
-
+

{feature.title}

@@ -56,23 +56,6 @@ const iconMap: Record = {
diff --git a/portfolio-cloudflare/src/layouts/Base.astro b/portfolio-cloudflare/src/layouts/Base.astro index 81d7c27..f8fd378 100644 --- a/portfolio-cloudflare/src/layouts/Base.astro +++ b/portfolio-cloudflare/src/layouts/Base.astro @@ -3,6 +3,7 @@ import { getMenu, getSiteSettings } from "emdash"; import { EmDashHead } from "emdash/ui"; import { createPublicPageContext } from "emdash/page"; import { Font } from "astro:assets"; +import "../styles/tokens.css"; import "../styles/theme.css"; interface Props { @@ -43,7 +44,7 @@ const pageCtx = createPublicPageContext({ {fullTitle} - + @@ -143,9 +143,6 @@ const pageCtx = createPublicPageContext({ if (theme === "system") { document.cookie = `theme=; path=/; max-age=0; SameSite=Lax${secure}`; root.classList.remove("light", "dark"); - if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - root.classList.add("dark"); - } } else { document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure}`; root.classList.remove("light", "dark"); @@ -174,15 +171,6 @@ const pageCtx = createPublicPageContext({ setTheme(btn.dataset.theme || "system"); }); }); - - // Listen for system preference changes - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - if (getStoredTheme() === "system") { - root.classList.toggle("dark", e.matches); - } - }); diff --git a/portfolio/src/layouts/Base.astro b/portfolio/src/layouts/Base.astro index 81d7c27..f8fd378 100644 --- a/portfolio/src/layouts/Base.astro +++ b/portfolio/src/layouts/Base.astro @@ -3,6 +3,7 @@ import { getMenu, getSiteSettings } from "emdash"; import { EmDashHead } from "emdash/ui"; import { createPublicPageContext } from "emdash/page"; import { Font } from "astro:assets"; +import "../styles/tokens.css"; import "../styles/theme.css"; interface Props { @@ -43,7 +44,7 @@ const pageCtx = createPublicPageContext({ {fullTitle} - + @@ -143,9 +143,6 @@ const pageCtx = createPublicPageContext({ if (theme === "system") { document.cookie = `theme=; path=/; max-age=0; SameSite=Lax${secure}`; root.classList.remove("light", "dark"); - if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - root.classList.add("dark"); - } } else { document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure}`; root.classList.remove("light", "dark"); @@ -174,15 +171,6 @@ const pageCtx = createPublicPageContext({ setTheme(btn.dataset.theme || "system"); }); }); - - // Listen for system preference changes - window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", (e) => { - if (getStoredTheme() === "system") { - root.classList.toggle("dark", e.matches); - } - });