From 15a375e389f8a0555894b28e167e6976c294ef9c Mon Sep 17 00:00:00 2001 From: BoraGkc Date: Mon, 28 Sep 2026 17:30:12 +0300 Subject: [PATCH] docs: move the design system's decisions into the repository Goals, ranked principles, scope, patterns, the content guide and the new-surface steps existed only in the design-system artifact, so coding agents reading AGENTS.md never saw them. They now live in docs/design-system/, with a one-source-per-question index, a changelog, and a list of decisions not made yet (ownership, communication, feedback, metrics, deprecation). No screen changes. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + UI_UX.md | 2 + docs/README.md | 1 + docs/design-system/BUILDING-A-SURFACE.md | 33 +++ docs/design-system/CHANGELOG.md | 13 ++ docs/design-system/CONTENT.md | 82 +++++++ docs/design-system/DESIGN.md | 281 +++++++++++++++++++++++ docs/design-system/DIRECTION.md | 50 ++++ docs/design-system/PATTERNS.md | 82 +++++++ docs/design-system/README.md | 29 +++ 10 files changed, 574 insertions(+) create mode 100644 docs/design-system/BUILDING-A-SURFACE.md create mode 100644 docs/design-system/CHANGELOG.md create mode 100644 docs/design-system/CONTENT.md create mode 100644 docs/design-system/DESIGN.md create mode 100644 docs/design-system/DIRECTION.md create mode 100644 docs/design-system/PATTERNS.md create mode 100644 docs/design-system/README.md diff --git a/AGENTS.md b/AGENTS.md index f6019ed..13cb7b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ BlogFactory is an agent control plane for multi-site content operations. MCP cli - `docs/mcp.md` is the current MCP catalog, OAuth, Review Card, and safety boundary. - `docs/operations.md` covers migrations, deployment, background work, and production verification. - `UI_UX.md` defines the white Device Console system and current information architecture. +- `docs/design-system/` holds the design system's decisions: `DIRECTION.md` (goals, principles in the order they win, scope) first, then `DESIGN.md`, `PATTERNS.md`, `CONTENT.md`. Its README says which file answers which question and lists decisions not made yet; flag those instead of assuming. - Historical plans are decision records, not current requirements. Prefer code and current docs when they disagree. - Prefer existing app patterns over new abstractions. Keep diffs small and preserve unrelated worktree changes. - When the user asks to push, ship, merge, or finish GitHub work, complete that chain directly. Stop only for missing credentials, failing checks that need product judgment, or unrequested destructive operations. diff --git a/UI_UX.md b/UI_UX.md index bc2cd02..252e590 100644 --- a/UI_UX.md +++ b/UI_UX.md @@ -1,5 +1,7 @@ # BlogFactory UI/UX Notes +The design system's goals, principles, patterns and content rules live in `docs/design-system/` (start with `DIRECTION.md`). This file owns the information architecture and workflow rules. + ## Direction BlogFactory uses a white Device Console theme inspired by technical music hardware and product-grid SaaS, adapted for content operations. The app should feel like a clean blog factory control surface: precise, dense, fast, and slightly mechanical without becoming decorative. diff --git a/docs/README.md b/docs/README.md index 100db39..10bb7bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ - [Research plan](research-plan.md): forward plan for per-article research, business profiles, topic plans, and mobile review; not shipped. - [RSS scheduler](rss-scheduler.md): protected scheduled feed processing. - [UI system](../UI_UX.md): Device Console rules, current information architecture, and responsive behavior. +- [Design system](design-system/README.md): goals, principles, scope, patterns, content guide, and which file answers which question. - [Agent context](../AGENTS.md): repository-specific implementation and release rules. New developers should read the README, feature plan, architecture map, and AGENTS context first. Unchecked roadmap items are not existing product capabilities. diff --git a/docs/design-system/BUILDING-A-SURFACE.md b/docs/design-system/BUILDING-A-SURFACE.md new file mode 100644 index 0000000..08e82e5 --- /dev/null +++ b/docs/design-system/BUILDING-A-SURFACE.md @@ -0,0 +1,33 @@ +# Building a new surface + +The order below is the shortest path to a screen that already looks like the rest of the console. + +1. **Reach for the primitive first.** shadcn/ui is the base and it is already installed. Never hand-roll a second component with the same job as a primitive — install the primitive and use it. Before writing a new page surface, check `WorkspaceBackground`, `BywordPageShell`, `BywordCard`, `SectionHeader`, `IconTile`, `OptionCard`, `SettingNavItem`, `FactoryMark` and `FactoryDivider`. +2. **Wrap the route** in `BywordPageShell` — `factory-grid-bg` on a `max-w-6xl` column with `space-4`/`space-6`/`space-10` gutters. +3. **Open with `PageHeader`.** One primary action; everything else goes in its `…` menu. +4. **Add `SectionTabs`** if the section has sibling routes. +5. **Build the body from cards.** A panel is `card` + `border` + `radius-md` + `shadow-panel` (`BywordCard`). Open a panel with `SectionHeader` when it needs a title and an action. +6. **Pick the loading state before the loaded state.** List and table routes load a shape-matched skeleton — `TableSkeleton`, `ListSkeleton`, `CardGridSkeleton`, `StatRowSkeleton`, `DetailSkeleton` — never a centred spinner. +7. **Pick all three empty states.** `EmptyState` with an explicit tone: `empty`, `filtered`, `error`. Each names the next action. +8. **Put state in a badge**, never in a row background. +9. **Register the route in `CommandPalette`.** + +## Where a change belongs + +- Broad style changes: `web/src/index.css`, `web/tailwind.config.ts`, the shadcn primitives, and `BywordSurface.tsx`. +- A cross-feature composition: `components/patterns/` — but only once a **second** feature needs it. +- Page-specific classes: touch them only when they bypass the shared system or cause a visible mismatch. + +Remap a token rather than rewriting call sites. The `byword-*` and `factory-*` names exist as compatibility aliases precisely so a theme change stays a token change. + +## Rules that are not negotiable + +- **Every visible control works.** No fake knobs, switches, sliders or decorative-only controls. If a search box is drawn, it searches. +- **Both themes are real.** Read `panel-*`, `grid-*` and `field-inset` for lighting; never hardcode a white inset or a dark drop shadow. +- **Colour comes from semantic tokens only.** Raw palette utilities are banned outside `components/ui/`, and `npm run lint:colors --workspace=web` enforces it. +- **Row-level destructive actions live in `RowActions`**, never as a bare icon button in the row. +- **Auth, onboarding and not-found** may carry the strongest branded treatment; the form inside them still stays plain. + +## Before you call it done + +Run `npm run build`, `npm run test --workspace=web`, and — for anything touching colour — `npm run lint:colors --workspace=web`. Smoke-check both themes whenever a change touches surfaces, borders or shadows, and check desktop and mobile on auth, onboarding, Overview, Create Content, Review Queue, Runs, Sources, Content, Search Growth, Control, the editor, the Review Card, and dialogs and dropdowns. diff --git a/docs/design-system/CHANGELOG.md b/docs/design-system/CHANGELOG.md new file mode 100644 index 0000000..7c3dd47 --- /dev/null +++ b/docs/design-system/CHANGELOG.md @@ -0,0 +1,13 @@ +# Design system changelog + +Each entry says what changed and what existing screens must do. + +| Class | Means | Existing screens | +|---|---|---| +| Decision | A goal, principle, scope or rule changed | Follow it in the next change that touches them | +| Addition | A new part, token or pattern | Nothing until they need it | +| Breaking | A part, token or rule is removed or renamed | Migrate before the removal date in the entry | + +## 2026-09-28 — decisions moved into the repository (Decision) + +Goals, principles in the order they win, and scope (`DIRECTION.md`); the visual language and the "one way per repeated thing" table (`DESIGN.md`); recurring-task patterns (`PATTERNS.md`); the content guide (`CONTENT.md`); and the new-surface steps (`BUILDING-A-SURFACE.md`) were written on 23–24 Sep for the design-system artifact and existed only there. They now live here, where coding agents read them through `AGENTS.md`. No screen changes; the text matches the code at `70aaba0`. diff --git a/docs/design-system/CONTENT.md b/docs/design-system/CONTENT.md new file mode 100644 index 0000000..97cd0f7 --- /dev/null +++ b/docs/design-system/CONTENT.md @@ -0,0 +1,82 @@ +# Content — how BlogFactory writes + +The words are part of the design system. The reader is an operator running content for one or more sites: busy, technical, and accountable for what reaches a CMS in their name. Write like an operator briefing another operator. + +## Voice + +- **Short, factual, present tense.** No marketing cadence, no exclamation marks, no emoji anywhere in the product. +- **Name the object, then its state.** "3 drafts waiting on review", not "You have some drafts!". +- **You is the operator; the system is BlogFactory.** "BlogFactory sent 4 drafts to WordPress" — never "we". +- **The operator's words, not the backend's.** "Destination", "run", "draft" — not `integration_id`, `job`, `result_post_ids`. +- **Say it once.** A title is not repeated by its tab; a button is not explained by the sentence beside it. + +## Capitalisation + +- **Sentence case everywhere**: "Generate draft", "Clear filters", "Brand voice note". +- **Product places keep their capitals** as proper names: Overview, Create Content, Review Queue, Runs, Search Growth, Sources, Content, Control, MCP Connections, Integrations, Sites, Brand Voice, Article Settings, Usage, Growth Plan, Image Gallery, Getting started. +- **Uppercase is a style**: kickers, table heads and badges are written in sentence case and set uppercase by CSS. +- **Acronyms stay acronyms**: MCP, CMS, RSS, SEO, URL, CSV, API. + +## Terms (one word per thing) + +| Use | Not | Meaning | +|---|---|---| +| Content | Library | the inventory of posts and images (`/library` is only a URL) | +| post | article (in UI), entry | one piece of content in BlogFactory | +| draft | pending post | a post not yet approved, or a CMS draft | +| CMS draft | publish | what an approved post becomes in the destination — never live | +| run | job, task | one generation, with its state and log | +| source | feed (generic), input | where runs come from: RSS, campaign, batch import, brief | +| destination | integration (as a target), endpoint | the CMS a draft is sent to | +| site | workspace (for a domain), property | a connected domain | +| workspace | account, org | everything an operator controls | +| Review Queue | inbox, approvals | the list of real decisions waiting | +| brand voice / persona | tone preset | how a site's posts should sound | +| preflight | checks, validation | what must pass before a draft is sent | +| RSS sources | Content Sources, feeds (as a title) | the RSS tab's list of sources | +| Reporting mode | News | feeds in a news editorial mode | +| Templates | Template Library | programmatic templates | + +## Patterns of sentences + +| Situation | Formula | Example | +|---|---|---| +| Button | Verb + object | "Generate draft", "Add feed", "Send to CMS draft" | +| Primary create | Verb + thing | "Start run", "Create campaign" | +| Search placeholder | "Search …" | "Search posts…" | +| Loading | a skeleton; words only for long work | "Writing · run 2f81c" | +| Load failed | "Could not load " + what is safe + Retry | "Could not load runs. Nothing was lost." | +| Empty (first time) | "No yet" + what creates one | "No sources yet — connect an RSS feed to start." | +| Empty (filtered) | "No match these filters" + the total | "218 posts exist in this workspace." | +| Blocked | the missing thing + where to fix it | "Pick a CMS destination before this draft can be sent." | +| Confirm destructive | question naming the thing + consequence; confirm repeats the verb | "Delete this source? … The 38 drafts it already made stay in Content." → "Delete source" | +| Success | past tense, once, in a toast | "Draft sent to WordPress" | +| Helper line | one sentence | "Paste any format — it is normalised before saving." | + +Titles, buttons, tabs and labels have no full stop; descriptions and errors do. + +## Source types + +Never show a raw `source_type`. `formatSourceType()` (`web/src/lib/source-labels.ts`) is the one mapping: rss/rss_feed → RSS, url → URL, pdf → PDF, paste/raw_text → Text, youtube → YouTube, reddit → Reddit, hackernews → Hacker News, github → GitHub, brief → Brief, campaign → Campaign, batch_import → Batch import, mcp_batch_import → MCP batch import; anything new is sentence-cased. So a line reads "RSS · In review · Revision 1", not "rss · in_review". + +## Numbers, dates, money + +- Counts with the noun: "7 items · 2 blockers". Middle dots separate facts in a line. +- Relative time in meta lines ("updated 4m ago"), absolute in details and charts ("Sep 23 18:49"). +- Numbers in tables are `type-data`, right-aligned, with thousands separators. +- Money is the AI provider's cost in USD ("$312 / $450"); it is never a BlogFactory plan price. +- A projection says it is one: "projected $374". + +## Agent (MCP) content + +- **Say who wrote it.** A draft carries its provenance: the run, the source, the client that made it. +- **Agents propose; people approve.** Copy never implies an agent published anything; the furthest an agent goes is "sent as a CMS draft". +- **State limits plainly.** "Read-only scope — this client cannot write drafts." + +## Checklist for a new screen's copy + +1. Every place name matches the sidebar and the tables above; "Library" appears nowhere. +2. Buttons are verbs naming their object; there is one primary. +3. Loading, empty, filtered, error and blocked states use the formulas above. +4. Nothing repeats the title, a tab or a button. +5. No exclamation marks, no emoji, no "we". diff --git a/docs/design-system/DESIGN.md b/docs/design-system/DESIGN.md new file mode 100644 index 0000000..73aa185 --- /dev/null +++ b/docs/design-system/DESIGN.md @@ -0,0 +1,281 @@ +# BlogFactory Device Console + +BlogFactory is an agent control plane for multi-site content operations, and its interface is a **device console**: technical music hardware and product-grid SaaS, adapted for running a blog factory. MCP clients do the work — generating, reading, editing, sending approved drafts to a CMS; the web app is where an operator summarises, reviews, controls and audits it. Build surfaces that feel precise, dense, fast and slightly mechanical, and stop short of decorative. An operator should be able to read a run's state from across the desk. + +Two themes are equally real. **Console** is the off-white default; **Graphite** is the same geometry and the same accents lit from a dark plate — surfaces go down, text goes up, hue stays. Never ship a change that only works in one. + +## How to use it + +- **Start with `DIRECTION.md`** — the goals, the principles in the order they win, and what this system covers. Then this file for the visual language, `PATTERNS.md` for recurring tasks, `CONTENT.md` for words, `BUILDING-A-SURFACE.md` for a new screen, and `web/src/components/README.md` for the component layers. `README.md` in this folder says which file answers which question. +- **Code is the design source.** Components live in `web/src/components`; import them from the app. The design-system artifact renders every component from the shipped code in both themes and is a view of this folder, not a second source. +- **Tokens.** `web/src/index.css` holds the variables as **bare HSL triplets** (`--primary: 13 100% 55%`) for Console and Graphite, and `web/tailwind.config.ts` maps them to utilities. Every call site wraps them: `hsl(var(--primary))`, or `hsl(var(--primary) / 0.14)` for a tint. The lighting and grid tokens (`panel-*`, `grid-*`, `field-inset`) are complete colours. +- **Where a change belongs.** Broad style: `web/src/index.css`, `web/tailwind.config.ts`, the shadcn primitives in `components/ui/`, and `components/layout/BywordSurface.tsx`. Remap a token rather than rewriting call sites — `byword-*` and `factory-*` exist as compatibility aliases precisely so a theme change stays a token change. +- **Checks before calling it done**: `npm run build`, `npm run test --workspace=web`, `npm run lint:colors --workspace=web` for anything touching colour, and a smoke check of both themes on desktop and phone. + +## Not synced + +- The **MCP Review Card** (`web/src/mcp-review/`) is a standalone MCP App with its own small stylesheet; it follows this language but is not rendered here. It never embeds the Router, AuthProvider or React Query. +- **Public marketing** lives in a separate Astro repository with its own system; it shares the mark, the colours and the two faces, not components. +- **Auth, onboarding and not-found** carry the strongest branded chassis (`factory-coal`, `device-perforation`); their forms are the ordinary Field parts documented here. + +## Overview + +The console is built from four ideas, and every rule below serves one of them: + +1. **Plates on a drawing grid.** The workspace is an off-white floor ruled with a faint 40px grid. Everything the operator works with sits on a white plate that is lit from above — a highlight along its top edge, a shade closing the bottom, a hairline and a soft drop underneath. +2. **Colour is a signal, not a mood.** Graphite ink and pale hairlines carry the page. Orange is the one action. Blue is where you are and where a link goes. Green, amber, red, grey and blue-running say what state a thing is in — and only that. +3. **Mono is the machine.** Space Grotesk is the operator's voice; IBM Plex Mono is the machine's: labels, metadata, ids, counts, table headers, statuses. +4. **Density is respect.** Small corners, 32px table heads, 36px controls, one-line help. The operator runs several sites; the screen should hold a working set, not a hero. + +## Colors + +Colour comes from semantic tokens only. Raw Tailwind palette utilities (`text-emerald-700`, `bg-amber-50`, `border-slate-300`) are banned outside `components/ui/`, and `npm run lint:colors --workspace=web` enforces it. + +### Named Rules + +- **The one-orange rule.** `primary` belongs to the single primary action on a surface. Two orange buttons means one of them is wrong. +- **The badge rule.** State lives in the badge, not the row. Rows, cards and pages never take a status fill; only hover (`byword-blue-soft`/45) and selection (`byword-blue-soft`) change a row. +- **The category rule.** A category that is not a state takes `factory-purple` (or `factory-amber`), never a status hue. Brand marks (YouTube, Reddit) render neutral so red, amber and green keep their operational meaning. +- **The lighting rule.** Read `panel-highlight`, `panel-shade`, `panel-edge`, `panel-drop`, `panel-lift`, `grid-line`, `grid-glow` and `field-inset`. Never hardcode a white inset or a dark drop — those tokens are what make Graphite work. + +### Primary — the record/action orange + +`primary` (13 100% 55%; 57% lightness in Graphite) is the record button on a device: the primary Button, a checked Checkbox, an on Switch, a Progress bar, the focus `ring`, text selection (at 35%). The primary Button's edge is a literal `#D43A14` and its hover `#F04416` — the only two hex values in the primitives, kept exact. + +### Navigation blue + +`byword-blue` (207 70% 52%) and its shadcn alias `accent` are navigation and link emphasis: the PageHeader kicker, the active sidebar item and section tab, link buttons, the IconTile glyph, the checklist ring. `byword-blue-soft` is the only blue allowed as a fill — hover, selection, the active tab's wash. Blue is never an action. + +### Neutrals + +| Token | Console | Graphite | Job | +|---|---|---|---| +| `background` | 60 14% 96% | 210 7% 9% | The workspace floor under the grid | +| `card` | 0 0% 100% | 210 7% 12% | Every plate, every field | +| `popover` | 0 0% 100% | 210 7% 13% | Menus, selects, tooltips, the palette — a step above `card` in Graphite | +| `muted` | 60 9% 91% | 210 7% 16% | Table header rail, tab rail, switch track, disabled fields, skeletons | +| `foreground` | 210 5% 20% | 60 9% 91% | Graphite ink | +| `muted-foreground` | 210 4% 42% | 210 6% 62% | Body copy under titles, metadata, headers | +| `border` | 80 5% 84% | 210 7% 21% | The hairline | +| `byword-border` | 75 5% 83% | 210 7% 21% | The shell hairline (BywordCard, PageHeader, SectionHeader) — a hair warmer | +| `input` | 75 4% 72% | 210 7% 28% | Field and outline-button hairline — darker, so a field reads as an opening | +| `secondary` | 210 5% 13% | 210 6% 22% | The black device key and the active Tabs trigger | + +### Status — reserved for operational state + +| Token | Console | Graphite | Means | +|---|---|---|---| +| `status-success` | 163 100% 26% | 163 55% 48% | Completed, active, approved, connected | +| `status-warning` | 37 100% 42% | 37 92% 57% | Needs review, paused, attention | +| `status-error` | 5 75% 49% | 5 78% 60% | Failed, blocker, rejected (`destructive` is its twin for actions) | +| `status-pending` | 210 4% 54% | 210 6% 55% | Pending, draft, idle | +| `status-running` | 207 72% 53% | 207 72% 62% | Running, in progress | + +Opacity suffixes carry the weight: `/14` for a badge fill, `/10` for an alert or error tile, `/35` (or `/30`) for the hairline, solid for a dot. + +### Factory aliases + +`factory-coal` (the darkest plate: auth and onboarding chassis), `factory-paper` (the warm editorial tint), `factory-amber` (priority and quota marks that are not warnings), `factory-purple` (the one categorical hue). `byword-ink` and `byword-blue-muted` are compatibility names for older call sites. + +### Sidebar + +The rail has its own set so it can sit a step off the workspace: `sidebar-background` (60 12% 94%), `sidebar-foreground`, `sidebar-muted`, `sidebar-border`, with `sidebar-primary` = blue and `sidebar-ring` = orange. + +### Graphite + +The `.dark` block (next-themes, class strategy, `blogfactory-theme` storage key) lowers surfaces, raises text, and keeps every hue. The highlight dims to 5% white; shades and drops become black at 35–50%; `byword-blue-soft` becomes a deep 207 45% 20%; status foregrounds flip to the dark ink so filled chips stay legible. In this system the same block answers to `[data-theme=dark]`. + +### Contrast + +Everything clears 4.5:1 on the grounds its token note names, in both themes, with two inherited exceptions kept exact and designed around: + +- `byword-blue` on `card` is 3.5:1 in Console — enough for the 2px tab borders, icons and dots it draws (3:1), short for small text. Keep blue text to the kicker and active navigation, where position and weight carry it too. Graphite passes at 6.7:1. +- `status-warning` as text on `card` is 2.9:1 in Console. It never carries meaning alone: StatusBadge always pairs it with an icon and a word. + +## Typography + +Two faces, both from Google Fonts: **Space Grotesk** (400–700) for the interface and **IBM Plex Mono** (400–700) for anything machine-produced. Body text sets with `ss01` and kerning on, no synthesised bold, zero letter-spacing everywhere (any `tracking-*` utility is reset to 0), `text-wrap: balance` on headings and `pretty` on paragraphs. + +### Hierarchy + +| Style | Face | Size / leading | Weight | Use | +|---|---|---|---|---| +| `type-page-title` | Sans | 26px / 1.12 (24px below sm) | 600 | One per route, in PageHeader | +| `type-panel-title` | Sans | 16px / 1.375 | 600 | Card, SectionHeader, dialog (18px) titles | +| `type-object-title` | Sans | 14px / 1.375 | 600 | The name of a row's subject: a post, a site, a source | +| `type-body` | Sans | 14px / 24px | 400 | Supporting prose, in `muted-foreground` | +| `type-editorial` | Sans | 15px / 28px | 400 | The article itself, in the editor and preview only | +| `type-control` | Sans | 13px / 1.2 | 600 | Button and tab labels (12px on `sm` buttons and SectionTabs) | +| `type-kicker` | Mono | 10px / 1.4 | 600, uppercase | Labels above a section, a stat, a page title; menu labels | +| `type-table-head` | Mono | 10px / 1.4 | 600, uppercase | Column headers on the header rail | +| `type-meta` | Mono | 11px / 16px | 400 | Timestamps, ids, counts, help lines | +| `type-status` | Mono | 11px / 1.4 | 600 | The word inside a StatusBadge | +| `type-data` | Mono | inherits (14px in cells) | 500, tabular | Numbers that must line up | + +Stat numbers are the one large figure: 30px/600 tabular sans in StatCard, 24px in StatStrip. + +### Named Rules + +- **Never scale type with the viewport.** The page title steps from 26px to 24px below `sm` and stops. +- **Uppercase is a style, not a spelling.** Write kickers and headers in sentence case in code; CSS sets them uppercase so screen readers read words. +- **Mono means machine.** If a person wrote it (a title, a description), it is sans. If the system produced it (an id, a time, a count, a state), it is mono. +- **Long names wrap or truncate on purpose.** Domains, titles and URLs never stretch a cell: `min-w-0` + `truncate`, or `break-words` for titles. + +## Layout + +### The page layer — one structure for every page + +1. `WorkspaceBackground` — `factory-grid-bg` at full height. +2. `BywordPageShell` — a 1152px (`max-w-6xl`) centred column; side gutters 16 / 24 / 40px at base / sm / lg; top and bottom 24px, 32px from lg. Wide single-table pages and SectionTabs use `max-w-7xl` (1280px). +3. `PageHeader` — blue kicker, title, one line; actions right; a `byword-border` rule 20px below, 24px above the first block. +4. `SectionTabs` — sticky at the top of the column when the place has sibling routes. +5. Panels — `BywordCard`, opened by `SectionHeader` when they need a title and an action. Gaps between panels: 16px in a grid, 24px between blocks. + +The shell sits right of the fixed `AppSidebar`: 224px wide (content offset 236px from lg), 60px collapsed and forced collapsed below 1024px (offset 64px). + +### Named Rules + +- **Overview owns summaries; Content owns inventory.** Don't repeat a large analytics panel above a table that already filters itself. +- **One primary per surface**, in the PageHeader or the panel that owns the task. Banners, row actions and secondary panels use outline or secondary. +- **The title is the place's name.** A page is titled with the sidebar or tab label that opened it — Runs, RSS sources, Usage, Users — never an internal name ("Job Queue", "Admin Users"). +- **Controls line up on 36px.** Buttons, inputs, selects, the tab rail. Dense rows use 32px. +- **No wasted card padding around tables.** A table sits flush in its BywordCard; the card's hairline is the table's frame. +- **Sticky toolbars float.** A page-level toolbar that sticks (Content's filters and bulk bar, the RSS toolbar) is `rounded-md`, `byword-border`, `background`/92 with a backdrop blur and a `panel-lift` drop, at `z-sticky-inner` (20). + +### Information architecture + +Operate: Overview `/`, Create Content `/create`, Review Queue `/review`, Runs `/runs`, Search Growth `/overview/growth`. Manage: Sources `/sources` (RSS, Campaigns, Batch Import), Content `/library` (Content, Image Gallery), Control `/control` (MCP Connections, Integrations, Sites, Brand Voice, Article Settings, Usage). Post edit and preview stay at `/library/posts/:id/edit` and `/preview`. The visible word is **Content**; "Library" never appears. Details in `UI_UX.md` › Information Architecture. + +### Data visualisation + +Charts are drawn, not decorated: mono axes and labels, `border` gridlines, hue by meaning — completed `status-success`, failed `status-error`, running `status-running`, spend and projections in `byword-blue`, the selected thing in `primary`. A projection is dashed and labelled as a projection. Every chart prints the numbers it draws (RunWaterfall, BudgetBurnChart). No gradients beyond a 20% area fill, no 3D, no pie charts for fewer than three parts. + +## Elevation & Depth + +Lighting, not depth. A plate is lit from its top edge and closed at its bottom; it does not float. There are only four heights: the floor, a plate, a lifted plate (hover on a link card), and the overlay layer. + +### Shadow Vocabulary + +| Token | Recipe | Where | +|---|---|---| +| `shadow-panel` | top highlight · bottom shade · 1px edge · 0 10px 28px drop | `.factory-panel`: cards, IconTile, the mark, table shells, dialogs, sheets, menus, popovers, tooltips | +| `shadow-field` | inset 0 1px 2px `field-inset` | Inputs, selects, textareas, the switch track, progress — recessed into the plate | +| `shadow-action` | white 32% top inset · 2px dark bottom inset · 1px edge | The primary and destructive keys — the same in both themes, a coloured key is lit by its own fill | +| `shadow-device` | white 12% top inset · 2px black bottom inset · edge | The black secondary key and the active Tabs trigger | +| `shadow-plate` | top highlight · 1px edge | The outline button — a plate, not a key | +| `shadow-rail` | top highlight only | TabsList, the table header rail, checkbox, radio, sidebar controls | +| `shadow-lift` | 0 12px 28px `panel-lift` | Added under an OptionCard or linked StatCard as it rises 2px | +| `shadow-toast` | edge + lift | Toasts | + +### Named Rules + +- **Plates don't stack.** Never a card in a card; use a hairline or a kicker inside. +- **Overlays share one layer** (`z-overlay`, 50) over a `foreground`/35 scrim with a 1px blur. +- **Graphite never paints white lines.** The highlight is 5% there; if a surface shows a bright top stripe in dark mode, it hardcoded a white inset. + +## Shapes + +`--radius` is 0.375rem and everything derives from it. `radius-lg` 6px is the largest corner in the system (feature panels, editor surfaces). `radius-md` 4px for the shared plates — Card, BywordCard, Dialog, Alert, OptionCard, StatCard, the palette. `radius-sm` 2px for every control — buttons, fields, tabs, badges, menus, tooltips, toasts, the checkbox, the (square) radio and the switch with its square thumb. `radius-full` only for what is genuinely round: status dots, the checklist marks and the scrollbar thumb. Nothing is pill-shaped. + +## Components + +The app is built on shadcn/ui primitives, and the rule is firm: never hand-roll a second component with the same job as a primitive — install the primitive and edit its variants. Components sit in four layers, and something earns the next layer up only when a **second** feature needs it: + +1. `components/ui/` — shadcn primitives: generic, no product knowledge, no data fetching. +2. `components/patterns/` — cross-feature compositions: EmptyState, ChecklistCard, RowActions, StatCard/StatStrip, TablePagination, PageSkeleton. +3. `components/layout/` — the shell: AppSidebar, AppLayout, PageHeader, SectionTabs, CommandPalette, ErrorBoundary, and the `BywordSurface` set (WorkspaceBackground, BywordPageShell, BywordCard, SectionHeader, IconTile, OptionCard, SettingNavItem, FactoryMark, FactoryDivider). +4. `components//` — product-specific (RunWaterfall, BudgetBurnChart, MarkdownEditor, the Review Card). + +### The catalogue + +| Group | Cards | +|---|---| +| Brand | FactoryMark · FactoryDivider · IconTile | +| Actions | Button · RowActions · DropdownMenu · CommandPalette | +| Forms | Field · Input · InputAffordance · Textarea · Select · Checkbox · RadioGroup · Switch · Slider · TagInput | +| Navigation | AppSidebar · SectionTabs · Tabs · Breadcrumb · Pagination · SettingNavItem | +| Surfaces | PageShell · PageHeader · SectionHeader · Card · OptionCard · Dialog · AlertDialog · Sheet · Popover · Tooltip · Accordion · Collapsible · ScrollArea · Separator | +| Status | StatusBadge · Badge · Alert · Toaster · Progress · Skeleton · EmptyState · ChecklistCard | +| Data | Table · StatCard · RunWaterfall · BudgetBurnChart | + +Every card in the design-system artifact states what it is for, when not to use it, its states, keyboard and accessibility behaviour, what the consumer supplies, and what not to do with it. + +### Repeated things look the same + +| Thing | The one way | +|---|---| +| Page top | PageHeader: kicker, title, one line, one primary + `…` | +| Sibling routes | SectionTabs | +| In-page views | Tabs (black active key) | +| A row's actions | RowActions `…`, destructive last | +| State | StatusBadge (icon + word) | +| Kind or count | Badge | +| Nothing here | EmptyState with the right tone | +| Loading | a shape-matched skeleton | +| Done elsewhere | a toast | +| Irreversible | AlertDialog | +| A URL or domain | InputAffordance + `url-validation.ts` | +| Finding anything | ⌘K CommandPalette | +| A source type (rss, youtube…) | `formatSourceType()` from `lib/source-labels` → RSS, YouTube, Brief | +| An editorial state | `editorialStateBadge()` from `lib/editorial-state` → StatusBadge | +| A metric tile | StatCard (tone = dot + hairline, never a fill) | +| A lasting notice | Alert, with an outline action | +| Setup | the Getting started checklist | + +## Iconography + +Icons are [lucide-react](https://lucide.dev), no exceptions and no second set, drawn at **1.8 stroke** where the call site sets it (IconTile, SettingNavItem, StatCard kickers) — lighter than lucide's 2, which keeps a dense table from looking furry. + +- 16px inside buttons, table rows, menu items and fields. +- 14px inside a StatusBadge or beside a `type-kicker`. +- 20px inside an IconTile or a settings rail row. + +An icon inherits `currentColor`; colour its parent with a token, not the icon. An icon never carries meaning alone: pair it with a word, or give it a tooltip and an accessible name. A spinning icon appears in exactly two places: the `running` StatusBadge, and a 16px `Loader2` inside a busy button in place of its leading icon. Never centred in a page body — that is a skeleton. + +The mark (`assets/Logos/blogfactory-mark.svg`) is a 64px plate with three unequal graphite bars and an orange lamp; it carries literal colours, so on dark grounds use the CSS-built `FactoryMark`. + +## Motion + +Motion confirms; it never performs. `transition-calm` (150ms ease-out) is the default on every control. `transition-gentle` (200ms ease-in-out) is for anything that changes size. + +| What | Motion | +|---|---| +| Press | Buttons drop 1px (`active:translate-y-px`); disabled buttons don't | +| Hover a link card | Rise 2px + `shadow-lift` (OptionCard, linked StatCard) | +| Menus, selects, popovers, tooltips | Fade + zoom from 95% + a 2px slide away from the trigger | +| Dialog, alert dialog | Fade + zoom 95%, 200ms | +| Sheet | Slide from its edge: 500ms in, 300ms out | +| Accordion | Height, 200ms | +| Content arriving | `fade-in` (4px rise, 300ms) and `slide-in-right` are defined in the Tailwind config but no component uses them yet — reach for them before writing new keyframes | +| Live | `pulse-gentle` (2s, opacity 1 → .7) on an open run's bar in RunWaterfall; `animate-pulse` on skeletons; `animate-spin` only on the running badge and a busy button's icon | + +### Character: the texture marks + +Factory identity comes from assembly labels, rails, small technical marks and textures — not from colour. `factory-grid-bg` (40px drawing grid + a top bloom) under every page; `factory-divider` (the hatched rule) on SectionHeaders and closing major blocks; `pixel-edge` on OptionCards; `device-hairline-bg` (18px) inside skeletons and behind diagrams; `device-perforation` (8px) and `factory-scanlines` (9px) on branded chassis. A few per page, never animated, always `aria-hidden`. + +## Do's and Don'ts + +### Do: + +- Put the one thing this screen exists for in orange, and everything else in outline, ghost or black. +- Say state in a StatusBadge with an icon and a word. +- Load lists and tables with a skeleton shaped like them. +- Give every empty, filtered and failed state its next action. +- Put the format in the field (`https://`), accept any paste, normalise before submit. +- Put row-level destructive actions in RowActions behind an AlertDialog. +- Register every new surface in the ⌘K palette. +- Read lighting from `panel-*`, `grid-*` and `field-inset`, and check both themes. +- Keep labels in task language: Overview, Create Content, Review Queue, Runs, Search Growth, Sources, Content, Control. + +### Don't: + +- Flood a page, card or row with a status colour. +- Add fake or decorative-only controls — a drawn switch, slider, search or button must work. +- Use soft SaaS gradients, oversized rounded cards, blue-purple washes or landing-page heroes inside the app. +- Show "Library" anywhere visible, or restore removed surfaces (News) and old routes. +- Put a centred spinner in a page body. +- Add a live-publish, delete-in-bulk or credential control to anything an agent can reach; the highest agent authority is a CMS **draft** after explicit approval. +- Hardcode a colour, a white inset or a dark drop outside the primitives. +- Add a letter-spacing utility (`tracking-*`); the console sets zero tracking everywhere. +- Put a coloured left border on a card, notice or heading; only an active navigation item carries the 2px blue rule. +- Use `window.confirm`, `window.prompt` or `window.alert`. diff --git a/docs/design-system/DIRECTION.md b/docs/design-system/DIRECTION.md new file mode 100644 index 0000000..9d1b266 --- /dev/null +++ b/docs/design-system/DIRECTION.md @@ -0,0 +1,50 @@ +# Direction — why the system exists, what it decides first, what it covers + +The Define layer of the Device Console: goals, principles, scope. Every other file here applies these decisions and none may contradict them. A person or an agent evaluating a change starts here: does it serve a goal, which principle decides it, is it in scope? + +Recorded 23 Sep 2026 from `AGENTS.md`, `UI_UX.md`, `web/src/components/README.md` and `docs/frontend-ui-system-plan-2026-09-21.md`. A change to this file is a product decision. + +## Goals + +| # | Goal | Problem and evidence | Success signal | +|---|---|---|---| +| G1 | **An operator reads the state of the factory at a glance.** | Operators run several sites and many runs; the product's value is knowing what needs a decision now (Overview digest, Review Queue priority: blocker → changes requested → in review → stale approval → warning). | State is always a StatusBadge; the queue holds only real work; no page floods colour; a run's state is legible from across the desk. | +| G2 | **Every page reads as the same console.** | The 21 Sep UI audit found hand-rolled cards, raw palette colours and per-page spinners. | Pages use the page layer (PageShell, PageHeader, SectionTabs, BywordCard); `npm run lint:colors` passes; no centred spinners. | +| G3 | **A shared decision changes in one place.** | Theme work proved that call sites must not hold colours or shadows. | Tokens and primitives only; `byword-*`/`factory-*` aliases remap instead of call-site rewrites; both themes pass from the token block. | +| G4 | **Agents building UI apply these decisions instead of guessing.** | Agents build most screens; MCP is the work layer and the web is the control layer. | An agent cites the right card, reuses the primitive, and flags a missing decision rather than inventing a component. | +| G5 | **Web and MCP never disagree.** | The action queue, review packet and preflight are shared services. | UI shows what the services return; no page re-derives queue classification, stale-draft rules or destination logic. | + +### Non-goals + +- A public component library. The system serves one app, `web/`, and the Review Card. +- A Figma library. Code is the design source; the design-system artifact (the rendered catalogue) is generated from it. +- Marketing. It lives in its own repository and system. +- Completeness for its own sake: a part enters `components/patterns/` when a **second** feature needs it. + +## Principles, in the order they win + +| # | Principle | What it decides | Kept | Broken | +|---|---|---|---|---| +| P1 | **A person approves; agents draft.** | The highest agent authority is CMS **draft** creation after explicit approval. No live publish, delete, bulk mutation or credential control reaches MCP. | "Send to CMS draft" behind current version, explicit destination and a valid preflight. | An "auto-publish" switch; a bulk delete on an agent-reachable surface. | +| P2 | **Show what the backend says.** | Missing data reads as missing, with its reason and next step; projections are labelled. | RunWaterfall claims no per-step timing; BudgetBurnChart labels its projection. | A sample chart "until the API is ready". | +| P3 | **Colour is semantic.** | Hue by meaning only: orange acts, blue navigates, status colours say state, purple categorises. | A campaign chip in `factory-purple`. | A green "YouTube" badge. | +| P4 | **One way per repeated thing.** | See `DESIGN.md` › Repeated things look the same. Beats a page's local preference. | A new list's row menu is RowActions. | A trash icon beside each row. | +| P5 | **Dense, calm, task-focused.** | Small corners, 36px controls, 32px table heads, one line of help; Overview summarises, Content inventories. | A table flush in its card. | An analytics hero above the Content table. | +| P6 | **Affordance before instruction.** | Shape the control so the input is obvious; words are the fallback. | `https://` in the field; paste anything. | A paragraph explaining the URL format. | +| P7 | **Every visible control works.** | No fake knobs, switches or searches. | A disabled control that says why. | A decorative slider. | + +A rule no principle supports is a proposal, not a rule. + +## Scope + +| Area | Status | Depth | +|---|---|---| +| `web/` authenticated app — every route, both themes, desktop and phone | **Included** | Full: tokens, primitives, patterns, layout, content, lint | +| MCP Review Card (`web/src/mcp-review/`) | **Included, standalone** | Follows the language; its own small stylesheet; never the Router, AuthProvider or React Query | +| Auth, onboarding, not-found | **Included** | Strongest branded chassis; ordinary Field parts inside | +| Docs and Help entries (`docs.html`, `help.html`) | **Included** | Same tokens and faces | +| Public marketing (Astro, separate repository) | **Excluded** | Shares the mark, colours and faces only | +| Admin routes | **Included** | Same parts; density over polish | +| BlogFactory Cloud billing surfaces | **Excluded** | Private repository | + +A request outside this table is not covered; say so rather than extending the system to it. diff --git a/docs/design-system/PATTERNS.md b/docs/design-system/PATTERNS.md new file mode 100644 index 0000000..d35297e --- /dev/null +++ b/docs/design-system/PATTERNS.md @@ -0,0 +1,82 @@ +# Patterns — recurring tasks, composed from the components + +A component is a part; a pattern is how parts answer a recurring task. Each pattern names the components it uses; none needs new code. + +## Feedback: which surface for which message + +| The operator needs to… | Use | Not | +|---|---|---| +| know something failed **here** and act on it | a `status-error` line under the field, or EmptyState `tone="error"` with Retry for a failed read | a toast (it leaves before it is read) | +| know something done **elsewhere on the screen** succeeded | a toast, past tense, once | a dialog | +| know a state that lasts (a broken connection, budget nearly spent) | an Alert | a toast | +| decide before something irreversible | AlertDialog naming the thing and the consequence | a toast with Undo | +| wait for a known layout | a shape-matched skeleton | a spinner in the page body | +| see a thing in progress | StatusBadge `running`, a busy button's own spinner for the action it started, Progress for a known fraction | a full-screen loader | + +## A list page (Content, Runs, Sources › RSS, Review Queue) + +0. **Data first.** Every column, filter and count maps to a field the API serves. What it does not serve is not drawn. + Summaries above a list are a row of StatCards and neutral panels with outline actions — never status-filled tiles. +1. PageShell › PageHeader (title, one line, one primary, `…` menu). +2. SectionTabs if the place has sibling routes. +3. The toolbar: search (Input with a search icon) · filters · a count. When it must stay visible, it is a sticky toolbar (`z-sticky-inner`). +4. The list in one BywordCard: a Table (flush) or rows divided by `byword-border`. +5. States: first load `TableSkeleton`/`ListSkeleton`; nothing yet EmptyState `empty` with the create action; nothing matches EmptyState `filtered` with Clear filters; failed EmptyState `error` with Retry. +6. More than one page: TablePagination under the table. + +## Filtering and search + +- The search box searches text; everything else is a filter. +- Filters apply at once and write to the URL, so a filtered list is shareable. +- Clear filters puts every filter back and keeps the search. +- A filter is named for what it matches. RSS sources' "Reporting mode" filter matches feeds in the news/sports-news editorial mode or with the Haber content type; there is no News surface. +- Overview owns summaries; the list owns its filters and bulk actions. + +## Bulk actions + +- Selecting rows (header Checkbox selects the page) shows one bar with the count ("3 posts selected") and the actions; it leaves when the selection clears. +- Bulk actions are editorial (assign, change state, send drafts after preflight). Bulk delete and live publish are not offered. + +## A record's detail + +- Beside its list in a Sheet (a run, a source): facts as rows — mono kicker left, value right, `type-data` for numbers. +- Its own route (a post) uses PageHeader with a Breadcrumb above, StatusBadge in the header, and panels below. + +## Review and delivery (Review Queue, the Review Card) + +- The queue shows only real work, in priority order: blocker → changes requested → in review → stale approval → warning → last update. +- A review packet shows provenance, editorial state, the revision summary, preflight, and the destination. +- **Send to CMS draft** is enabled only with the current version, an explicit destination and a passing preflight; otherwise it is disabled and the reason is on screen (a line or a Popover "Why is this blocked?"). +- A version conflict is an error state with the newer version named, never a silent overwrite. + +## Forms and dialogs + +- A create or edit task that returns to the page is a Dialog titled with the thing ("New campaign"). +- Fields: Label above; Input, Textarea, Select (three or more options), Checkbox, RadioGroup, Switch (takes effect at once), TagInput; errors under the field. +- URLs and domains: InputAffordance with the format chrome, `url-validation.ts` to normalise. +- Footer: Cancel (outline) left of the verb (primary). A disabled primary says why. +- Settings pages: SettingNavItem rail or SectionTabs, a StatStrip of current facts, then panels. + +## Destructive actions + +- In RowActions, last, after a separator, in the destructive item style. +- Always confirmed by an AlertDialog that names the thing and what happens to what it contains; the confirm repeats the verb. This covers API keys, sites, CMS integrations, writer-profile tools, templates, MCP tokens and admin access — nothing is deleted on a single click. +- Prefer reversible (pause, archive) to permanent. + +## Connecting things (Integrations, MCP Connections, Search Console) + +- A connection card shows its state as a StatusBadge (Active, Needs attention, Failed) and its last successful use in `type-meta`. +- Broken credentials surface as an Alert on the connection and an attention row in Getting started. +- MCP tokens are shown once, with a Copy button and a tooltip "Copy token · shown once"; scope and tool counts come from `/api/mcp/capabilities`, never hardcoded. + +## First run and setup + +- The sidebar's Getting started checklist is the one checklist; it hands each row to the existing setup step and leaves at 100%. +- A page with nothing yet explains what will be there and offers the one action that starts it (EmptyState). +- Create Content opens on OptionCards: From a feed, From a brief, Batch import. + +## Navigation and finding + +- ⌘K finds pages, sites, content, actions and help; register new surfaces there. +- The site switcher changes the active site for the whole workspace; every query is site-scoped. +- Docs and Help open through `lib/external-links.ts`. diff --git a/docs/design-system/README.md b/docs/design-system/README.md new file mode 100644 index 0000000..c1cb4e2 --- /dev/null +++ b/docs/design-system/README.md @@ -0,0 +1,29 @@ +# Design system + +The Device Console's decisions, in the repository where agents and people read them. The rendered catalogue (every component drawn from the shipped code, in both themes) is the design-system artifact; this folder is its source text. + +## One source per question + +| Question | Source | +|---|---| +| Why does the system exist, what wins a trade-off, what is in scope? | `DIRECTION.md` | +| What does it look like, and why? Which one way for a repeated thing? | `DESIGN.md` | +| How do parts answer a recurring task (list page, filters, bulk actions, forms)? | `PATTERNS.md` | +| What do we call it, how do we say it? | `CONTENT.md` | +| How do I build a new surface? | `BUILDING-A-SURFACE.md` | +| Where does a place live in the app (Operate / Manage)? | `../../UI_UX.md` › Information Architecture | +| Which component layer does a part belong to? | `../../web/src/components/README.md` | +| Which colour, shadow, radius? | `../../web/src/index.css`, `../../web/tailwind.config.ts` (enforced by `npm run lint:colors --workspace=web`) | +| What changed? | `CHANGELOG.md` | + +When two sources disagree, the code wins and the doc is fixed in the same change. + +## Not decided yet + +These are open product decisions. Until they are written here, an agent flags them instead of assuming an answer. + +- **Ownership:** who approves a change to this system, and who breaks a tie. +- **Communication:** where a design-system change is announced to the team. +- **Feedback:** where a problem with the system is reported, and how the reporter hears back. +- **Metrics:** which number shows the system is working (component reuse, screens rebuilt, drift found by `lint:colors`). +- **Deprecation:** how a part is retired: reason, replacement, affected screens, removal date.