From c158056580f51edff80b730841920d5a6e612ec9 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Thu, 02 Jul 2026 08:00:18 +0000 Subject: [PATCH] Merge pull request #337 from flo-bit/theming Theming --- THEMING_STATE.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ apps/web/src/app.css | 13 +++++++++++++ apps/web/src/lib/types.ts | 5 ++++- apps/web/src/lib/helpers/website.ts | 17 +++++++++++++++++ apps/web/src/lib/sections/SectionChrome.svelte | 2 +- apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte | 2 +- apps/web/src/lib/sections/GridSection/EditingGridSection.svelte | 2 +- apps/web/src/lib/sections/HeroSection/Decoration.svelte | 6 +++--- apps/web/src/lib/sections/RowsSection/EditingRowsSection.svelte | 2 +- apps/web/src/lib/sections/SingleCardSection/EditingSingleCardSection.svelte | 2 +- apps/web/src/lib/website/view/DevThemeSwitcher.svelte | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ apps/web/src/lib/website/view/Head.svelte | 6 ++++-- apps/web/src/lib/website/view/ThemeScript.svelte | 21 ++++++++++++++++----- apps/web/src/lib/website/view/Website.svelte | 10 +++++++++- apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte | 4 ++-- 15 file(s) changed, 310 insertion(s)(+), 19 deletion(s)(-) diff --git a/THEMING_STATE.md b/THEMING_STATE.md new file mode 100644 --- /dev/null +++ b/THEMING_STATE.md @@ -0,0 +1,121 @@ +# Theming (Phase 2 tokens) — state & handoff + +You are in the **`blento-theming` worktree** (branch `theming`) of the Blento monorepo (pnpm + +Turborepo; app in `apps/web`; SvelteKit 2 / Svelte 5 / Tailwind CSS v4). Goal: make the site's +appearance **token-driven and customizable**, KEEPING Tailwind. Colors were already customizable +(accent/base); this work generalizes that into a blento-owned semantic token layer. + +**A second agent may be building `@blento/sources` in a sibling worktree — stay in your lane** (see +Boundaries). Read `../blento-schema-design.md` (the `## Themes + tokens` and `## Renderers` sections) +for the overall design — this file is the scoped, in-progress reality. + +## How the token pipeline works (the core mechanism) + +``` +page.style (StyleTokens, per page) ← the store (design: app.blento.page.style) + → getStyleTokens(data) [helpers/website.ts] ← flatten to { 'radius-card': '2rem', ... } + → [website/view/ThemeScript.svelte] + sets --blento- CSS vars on (SSR inline script + client $effect) + → app.css maps --blento-* into Tailwind theme values / direct rules + → components use normal utilities (rounded-card, [box-shadow:var(...)], body font) that resolve + through the overridable vars. Default of every token == today's look → pixel-0px. +``` +Colors (accent/base) are a **separate, pre-existing** mechanism: `ThemeScript` adds a color class +(e.g. `.pink .stone`) to ``, and `@foxui/core/theme.css` maps that to `--accent-*`/`--base-*`. +That was not changed; the new pipeline is for the non-color tokens. + +## Done (branch `theming`, 4 commits on top of main) + +| Token | Definition | Default | Applied at | +|---|---|---|---| +| accent / base color | `@foxui` color class on `` (pre-existing) | pink / stone | everywhere via `bg-accent-*` etc. | +| `--blento-radius-card` | `app.css` `@theme inline { --radius-card: var(--blento-radius-card, 1.5rem) }` → `rounded-card` utility | 1.5rem (= old `rounded-3xl`) | `BaseCard` + hero decorations + section chrome + edit drop-zones | +| `--blento-shadow-card` | `BaseCard` class `[box-shadow:var(--blento-shadow-card,0_0_#0000)]` | invisible | grid cards | +| `--blento-font-body` | `app.css` `body { font-family: var(--blento-font-body, inherit) }` | inherit | whole page | + +- **`getStyleTokens`** (`helpers/website.ts`) flattens `publication.style` groups `radius/shadow/space/font` + → `--blento--` pairs (e.g. `style.radius.card` → `--blento-radius-card`). +- **`publication.style?: StyleTokens`** added to the type (`lib/types.ts`); `StyleTokens` comes from + `@blento/schema` (`records.ts`). +- **Dev switcher** `website/view/DevThemeSwitcher.svelte` (rendered `{#if dev}` in `Website.svelte`): + press **1–6** to flip presets (Default/Ocean/Sunset/Forest/Grape/Rose) varying color+radius+shadow+font. + Applies the same `--blento-*` vars live; reload restores the real theme. Never ships (dev-only). + +## The pattern to add a token (repeat this) + +1. **Define** the token in `app.css`: either a `@theme inline` value backed by `var(--blento-x, )` + (when you want a Tailwind utility like `rounded-card`), or a direct rule / arbitrary property on the + component `[prop:var(--blento-x,)]`. **Default must equal today's value.** +2. **Migrate** the hardcoded value in the component(s) to use it. +3. **Wire the store**: `getStyleTokens` already flattens `radius/shadow/space/font` groups — add a new + group there if needed. +4. **Add it to the switcher** presets (`DevThemeSwitcher.svelte`) + `tokenKeys`, so you can preview it. +5. **Gate**: default renders pixel-0px vs prod; then prove the override *does* something (temporarily + make `getStyleTokens` return a forced value, screenshot, confirm a non-zero diff, revert). See Gate. + +## What's left (roughly prioritized) + +1. **More token dimensions** (same pattern, each 0px-default): + - **Grid spacing** — `margin = 16` is a **JS constant** in `apps/web/src/lib/index.ts`, used by the + grid layout math (card translate/size), NOT CSS. Tokenizing it means threading a value through the + grid calc (`sections/GridSection` + wherever the math lives). Most visible spacing lever; also the + hardest. (Card *padding* is per-card-type, so there's no single clean token.) + - **Button radius** — `rounded-2xl` on buttons/CTAs (hero CTA `HeroSection.svelte`, etc.) → + `--blento-radius-button`. + - **Surface** — card border/ring, page background treatment. + - **Type scale** — font sizes/weights, not just family. + - **Profile** — avatar size, name size (currently hardcoded in `website/view/Profile.svelte`). + - Hunt for components that render their own container and bypass tokens (we already fixed the hero + that way — the fix is always: swap the hardcoded value for the token). +2. **Node-level `style.tokens`** (per-card overrides cascading over page tokens). The node envelope has + `style.tokens`; `nodesToItems` (in `@blento/schema`) currently only projects `style.tokens.color` → + `Item.color`. To support per-card radius/shadow/etc., carry `node.style.tokens` onto the `Item` + (extend the projection) and have `BaseCard` set them as inline `--blento-*` vars on the card element. + Coupled to the Item projection; cleaner after a render-from-nodes pass. +3. **Picker UI + storage** (make it user-facing, not just dev): + - A control in `website/settings/sections/PageSection.svelte` (next to `SelectTheme`) that writes + `data.publication.style` (radius/font/…), with live preview in the editor. + - **Save path**: `helpers/save.ts` must write `publication.style`; and the record lexicon must accept + a `style` field — add `style` to `apps/web/lexicons/custom/app/blento/page.json` (and regenerate + with `pnpm generate` + `pnpm exec lex-cli generate`). `site.standard.publication` (legacy main-page + store) is external/strict — prefer landing this after the node migration makes `app.blento.page` the + root, or gate to sub-pages first. +4. **`app.blento.theme` records** (portable token bundles): a page references an ordered `themes` list; + the token cascade host→site→page→node. This is the marketplace/forkable-theme tier — later. + +## The gate (how to validate every change) + +Harness is `packages/diff-harness`. Run the app, snapshot prod vs local, diff. +```bash +pnpm --filter @blento/web dev # or `pnpm web` from apps/web; note the port +# gate DID (a grid site): did:plc:s42iw2fbfmgsgh7hdtvvoaao (prod: https://blento.app/) +pnpm --filter @blento/diff-harness screenshot "http://localhost:/" /tmp/t/local.png +pnpm --filter @blento/diff-harness screenshot "https://blento.app/hoopinformatics.bsky.social" /tmp/t/prod.png +pnpm --filter @blento/diff-harness pixel /tmp/t/prod.png /tmp/t/local.png /tmp/t/diff.png +``` +- **Expected residual (NOT a regression):** ~120px for the dev switcher indicator (bottom-left), plus + (on a content diff) one `blento.app/` self-link and one avatar CDN transform per site — + those are env/profile, not your change. A token's *default* must not change the cards. +- **Override proof** (that the token actually flows): temporarily edit `getStyleTokens` to `return { '': '' }`, + screenshot, confirm a non-zero pixel diff, then revert. (radius `0px`, shadow, and font were proven + this way at 0.1–0.6%.) +- Also require `pnpm --filter @blento/web check` → **0 errors / 0 warnings**. + +## Boundaries (don't collide with the sources agent) + +- **DO touch:** `app.css`, `website/view/ThemeScript.svelte` + `DevThemeSwitcher.svelte`, component + class strings, `helpers/website.ts` (`getStyleTokens`), `website/settings/**` (picker), `lib/types.ts` + (publication.style), the page lexicon (for the picker's `style` field). +- **DO NOT touch:** `packages/sources/**`, the data-loading path in `website/data/load.ts` + (`loadAdditionalData` / sources), or `packages/schema/src/node.ts` (the envelope) except reading + `StyleTokens`. Editing `nodesToItems` for node-level tokens is fine but coordinate — it's shared code. + +## Key files +- `apps/web/src/app.css` — token definitions (`@theme` radius, body font). +- `apps/web/src/lib/website/view/ThemeScript.svelte` — applies `--blento-*` vars from `tokens`. +- `apps/web/src/lib/website/view/DevThemeSwitcher.svelte` — dev switcher + presets. +- `apps/web/src/lib/helpers/website.ts` — `getStyleTokens`. +- `apps/web/src/lib/types.ts` — `publication.style?: StyleTokens`. +- `apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte` — `rounded-card` + shadow token. +- `apps/web/src/lib/website/settings/sections/PageSection.svelte` — theme settings (color picker; add tokens here). +- `packages/schema/src/records.ts` (`StyleTokens`), `migrate.ts` `migratePage` (accent/base → page.style.colors; unused today). diff --git a/apps/web/src/app.css b/apps/web/src/app.css --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -9,6 +9,19 @@ @source '../node_modules/@atmo-dev/events-ui/dist'; @import '@foxui/core/theme.css'; +/* Blento semantic design tokens. Each Tailwind theme value reads an overridable `--blento-*` CSS + variable with a default that matches today's look, so components use normal utilities (e.g. + `rounded-card`) that resolve through page-overridable tokens (set on by ThemeScript from + page.style). Colors stay on the @foxui accent/base mechanism above. */ +@theme inline { + --radius-card: var(--blento-radius-card, 1.5rem); +} + +/* Body font token — defaults to `inherit` (the current stack), overridden per page/theme. */ +body { + font-family: var(--blento-font-body, inherit); +} + @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *))); @custom-variant accent (&:where(.accent, .accent *)); diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts --- a/apps/web/src/lib/types.ts +++ b/apps/web/src/lib/types.ts @@ -1,6 +1,6 @@ import type { Blob } from '@atcute/lexicons'; import type { AppBskyActorDefs } from '@atcute/bluesky'; -import type { Node } from '@blento/schema'; +import type { Node, StyleTokens } from '@blento/schema'; export type Item = { id: string; @@ -91,6 +91,9 @@ // explicit layout sync mode (overrides editedOn when set) layoutMode?: 'desktop-leads' | 'mobile-leads' | 'independent'; }; + + /** Semantic design tokens (radius/space/font) applied as --blento-* CSS vars by ThemeScript. */ + style?: StyleTokens; }; profile: AppBskyActorDefs.ProfileViewDetailed; pronouns?: string; diff --git a/apps/web/src/lib/helpers/website.ts b/apps/web/src/lib/helpers/website.ts --- a/apps/web/src/lib/helpers/website.ts +++ b/apps/web/src/lib/helpers/website.ts @@ -1,5 +1,22 @@ import type { WebsiteData } from '../types'; +/** + * Flatten the page's `style` tokens into `--blento-` CSS-var pairs for ThemeScript. Colors stay + * on the @foxui accent/base class mechanism; radius/space/font flow through the token vars (e.g. + * `style.radius.card` -> `radius-card` -> `--blento-radius-card`). Empty when no page tokens are set. + */ +export function getStyleTokens(data: WebsiteData): Record { + const style = data.publication?.style; + if (!style) return {}; + const out: Record = {}; + for (const group of ['radius', 'shadow', 'space', 'font'] as const) { + const g = style[group]; + if (!g) continue; + for (const [k, v] of Object.entries(g)) out[`${group}-${k}`] = String(v); + } + return out; +} + export function getName(data: WebsiteData): string { return data.publication?.name || data.profile.displayName || data.handle; } diff --git a/apps/web/src/lib/sections/SectionChrome.svelte b/apps/web/src/lib/sections/SectionChrome.svelte --- a/apps/web/src/lib/sections/SectionChrome.svelte +++ b/apps/web/src/lib/sections/SectionChrome.svelte @@ -23,7 +23,7 @@ {#if enabled && (hovered || isActive)}
diff --git a/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte b/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte --- a/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte +++ b/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte @@ -84,7 +84,7 @@
+ + {#if dev} + + {/if} diff --git a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte --- a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte +++ b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte @@ -39,7 +39,7 @@ bind:this={ref} draggable={false} class={[ - 'card group/card selection:bg-accent-600/50 @container/card relative isolate z-0 h-full w-full rounded-3xl outline-offset-2 transition-[outline] duration-200', + 'card group/card selection:bg-accent-600/50 rounded-card @container/card relative isolate z-0 h-full w-full [box-shadow:var(--blento-shadow-card,0_0_#0000)] outline-offset-2 transition-[outline] duration-200', isEditing ? 'transition-all' : '', color ? (colors[color] ?? colors.accent) : colors.base, color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '', @@ -50,7 +50,7 @@ >