docs(11.1): UI design contract
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
---
|
||||
phase: "11.1"
|
||||
slug: "summercms-documentation-for-humans-and-ai-agents"
|
||||
status: approved
|
||||
shadcn_initialized: false
|
||||
preset: none
|
||||
created: "2026-09-28"
|
||||
reviewed_at: "2026-09-28"
|
||||
---
|
||||
|
||||
# Phase 11.1 — UI Design Contract
|
||||
|
||||
> Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
|
||||
|
||||
**Scope of this contract.** Phase 11.1 adds one new visual surface: the **static documentation site** that `summer docs:build` renders from `docs/**/*.md` and the ingested `modules/*/README.md` files, plus the terminal output of `summer docs:build`, `docs:serve` and `docs:sync`. It does not touch the admin SPA.
|
||||
|
||||
**What the site is built from.** `internal/docsite` uses stdlib `html/template` with goldmark. The theme lives in `internal/docsite/theme/` and is embedded with `//go:embed`. It is hand-written CSS plus about 3 small vanilla JS files. There is no Tailwind, no npm and no Node at build time (D-02, D-03). The AI outputs (`llms.txt`, `llms-full.txt`, per-page `.md`) carry no visual design, and their format is fixed in RESEARCH §Q3.
|
||||
|
||||
**Design source of truth.** The brand and tokens come from the admin SPA: `admin/src/styles/main.css` (`:root` and `.dark` blocks, lines 83-125) and `.planning/phases/10-admin-vue-spa/design/README.md` (Direction C v2: dark navy with sunny yellow). CONTEXT.md lets us reuse the admin tokens ("reuse the admin SPA's design tokens if convenient"). This contract copies their **values** into plain CSS custom properties with the same `--c-*` names. It does not add a colour outside that palette, except the five syntax-highlight values in the Color section.
|
||||
|
||||
**Layout reference.** wintercms.com/docs anatomy (RESEARCH §Q1). It has a header with search, a left sidebar grouped by section, an "On this page" TOC, prev/next links, an edit link and a light/dark/system toggle. The version selector and the Docs/API/Markup/UI tabs are left out (RESEARCH §Q1 table).
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Tool | none. It is a stdlib `html/template` static site. shadcn is not applicable because there is no React and no npm (D-03). |
|
||||
| Preset | not applicable |
|
||||
| Component library | none. The theme parts are Go templates listed under "Theme Parts" below. |
|
||||
| Icon library | Lucide (ISC). 15 icons are inlined as SVG in a `theme/templates/icons.html` partial, with path data copied from `@lucide/vue@1.17.0` (`admin/node_modules/@lucide/vue/dist/esm/icons/<name>.mjs`, all 15 verified present on 2026-09-28): `sun`, `moon`, `monitor`, `search`, `menu`, `x`, `chevron-left`, `chevron-right`, `pencil`, `file-text`, `copy`, `check`, `info`, `lightbulb`, `triangle-alert`. Each is `stroke="currentColor"`, `stroke-width="2"`, `fill="none"`, `aria-hidden="true"`. They are 16px inside 14px text rows and 20px in header buttons. Ship `theme/assets/LICENSE-lucide.txt` (ISC). |
|
||||
| Font | DM Sans (UI and prose) and DM Mono (code), self-hosted as woff2. These are the same families as the admin (main.css:25-26). Vendor exactly 8 files from `@fontsource/dm-sans@5.3.0` and `@fontsource/dm-mono@5.3.0` (`license: OFL-1.1`, read from their package.json on 2026-09-28, which resolves RESEARCH assumption A4). The files are `dm-sans-latin-{400,600}-normal.woff2`, `dm-sans-latin-ext-{400,600}-normal.woff2`, `dm-sans-latin-400-italic.woff2`, `dm-sans-latin-ext-400-italic.woff2`, `dm-mono-latin-400-normal.woff2` and `dm-mono-latin-ext-400-normal.woff2`. They go into `internal/docsite/theme/assets/fonts/` with both `LICENSE` files. Copy the `@font-face` `unicode-range` values from the matching fontsource CSS and use `font-display: swap`. Stacks: `"DM Sans", ui-sans-serif, system-ui, sans-serif` and `"DM Mono", ui-monospace, monospace`. **Do not** reference the hashed files in `modules/boardwalk/dist/assets/`. |
|
||||
| New dependencies | **None.** `go.mod` is unchanged (RESEARCH §Standard Stack). Fonts and icon paths are copied once as static files. Nothing reads `node_modules` at build time. |
|
||||
| CSP posture | No inline `<script>`, no inline event handlers and no `style=` attributes in generated HTML. Every script is an external file under `/assets/`, so the site works under `script-src 'self'; style-src 'self'` (consistent with Phase 10.1 D-16). |
|
||||
|
||||
Component Inventory is omitted because Tool is `none`. The docs theme has no component library to enumerate. The parts this phase builds are specified under Theme Parts.
|
||||
|
||||
---
|
||||
|
||||
## Theme Parts (new in this phase)
|
||||
|
||||
Every part is an `html/template` partial or an asset under `internal/docsite/theme/`. `TestBuildSite` asserts the marker in the last column on a rendered page. The same markers let the checker and the auditor find each part.
|
||||
|
||||
| Part | File | Marker in built HTML |
|
||||
|------|------|----------------------|
|
||||
| Page shell (header, sidebar, main, TOC, footer) | `templates/page.html` | `<body class="docs">` |
|
||||
| Header | `templates/header.html` | `<header class="site-header">` |
|
||||
| Sidebar nav | `templates/sidebar.html` | `<nav class="sidebar" aria-label="Documentation">` |
|
||||
| On-page TOC (desktop column) | `templates/toc.html` | `<aside class="toc" aria-label="On this page">` |
|
||||
| On-page TOC (narrow, collapsible) | `templates/toc.html` | `<details class="toc-inline">` |
|
||||
| Page head block (eyebrow, H1, lead, page actions) | `templates/page.html` | `<div class="page-actions">` |
|
||||
| Prev/next pager | `templates/pager.html` | `<nav class="pager" aria-label="Previous and next page">` |
|
||||
| Footer | `templates/footer.html` | `<footer class="site-footer">` |
|
||||
| Search dialog | `templates/search.html` + `assets/search.js` | `<dialog class="search" id="search">` |
|
||||
| Theme toggle | `templates/header.html` + `assets/theme-init.js` + `assets/site.js` | `<button class="theme-toggle"` |
|
||||
| Code block (+ source caption, copy button) | goldmark `NodeRenderer` for `FencedCodeBlock` | `<figure class="code">` |
|
||||
| Callout (`> [!NOTE]`, `[!TIP]`, `[!WARNING]`) | goldmark AST transformer + renderer | `<aside class="callout callout-note|tip|warning">` |
|
||||
| Heading permalink | goldmark heading renderer | `<a class="heading-anchor"` |
|
||||
| Not-found page | `templates/404.html` → `site/404.html` | `<body class="docs docs-404">` |
|
||||
| Icons | `templates/icons.html` | `<svg class="icon icon-<name>"` |
|
||||
| Styles | `assets/site.css` (one file, no preprocessor) | `<link rel="stylesheet" href="/assets/site.css">` (prefixed by `base_url`) |
|
||||
|
||||
---
|
||||
|
||||
## Layout and Breakpoints
|
||||
|
||||
| Viewport | Columns | Sidebar | TOC |
|
||||
|----------|---------|---------|-----|
|
||||
| ≥ 1280px | sidebar 272px, then content max 768px, then TOC 224px. Gap between content and TOC is 48px | Sticky below the header (`top: 64px`, `height: calc(100vh - 64px)`, `overflow-y: auto`) | Sticky column (`top: 96px`) |
|
||||
| 1024–1279px | sidebar 272px, then content max 768px | Sticky, as above | The `<details class="toc-inline">` "On this page" block, closed by default, sits directly under the page actions |
|
||||
| < 1024px | content only, full width | Off-canvas drawer opened by the header **Menu** button (details under Interactions) | `toc-inline`, as above |
|
||||
|
||||
- The header is 64px tall and sticky at `top: 0`, with `z-index` above the content.
|
||||
- Main content padding is 48px top, 32px left/right and 64px bottom at ≥ 1024px. It is 24px top, 16px left/right and 48px bottom below 1024px.
|
||||
- The content column never exceeds 768px (about 75 characters at 16px), so prose line length stays readable.
|
||||
- **No-JS fallback.** `theme-init.js` adds `class="js"` to `<html>`. The drawer behaviour and the search and theme buttons are styled only under `html.js`. Without JS, the search button and theme toggle are hidden, and below 1024px the sidebar renders as a normal block **above** the content. Every page stays navigable.
|
||||
- All headings get `scroll-margin-top: 96px` (64px header plus 32px), so anchor jumps land below the sticky header.
|
||||
- **Print** (`@media print`): hide the header, sidebar, TOC, page actions, pager, copy buttons and heading anchors. Content goes full width with black text on white.
|
||||
|
||||
---
|
||||
|
||||
## Spacing Scale
|
||||
|
||||
Declared values (all multiples of 4):
|
||||
|
||||
| Token | Value | Usage |
|
||||
|-------|-------|-------|
|
||||
| xs | 4px | Icon-to-label gap inside buttons, inline code horizontal padding, sidebar item vertical padding |
|
||||
| sm | 8px | Sidebar item horizontal padding, gap between a section label and its items, H3 bottom margin, table cell vertical padding |
|
||||
| md | 16px | Paragraph and list bottom margin, code block padding, callout padding, pager card padding, header horizontal gap, table cell horizontal padding |
|
||||
| lg | 24px | Gap between sidebar section groups, sidebar padding (vertical), header horizontal padding (desktop), block gap after code/tables/callouts |
|
||||
| xl | 32px | H3 top margin, content horizontal padding (desktop), gap above the pager |
|
||||
| 2xl | 48px | H2 top margin, content top padding, content-to-TOC gap |
|
||||
| 3xl | 64px | Header height, content bottom padding (desktop) |
|
||||
|
||||
Exceptions (each a multiple of 4):
|
||||
|
||||
- Fixed widths: sidebar 272px, TOC 224px (the admin `--spacing-panel`), content max 768px, drawer 304px (max 85vw), search dialog 640px (max `calc(100vw - 32px)`), header search trigger 256px.
|
||||
- Control heights: header icon buttons 40px square (Menu, search-icon-only, theme toggle); header search trigger 40px; search input 44px (admin `--spacing-input`); sidebar item min-height 32px; copy button 32px square. The 40px header buttons sit in a 64px bar, and their hit area is extended to 44px with `::before` padding for touch.
|
||||
- The search dialog offset from the viewport top is 96px.
|
||||
|
||||
Borders (1px, 2px, 3px, 4px) and radii are not spacing. Radii reuse the admin values: control 10px (search trigger, search input, buttons), inner 12px (code blocks, callouts, pager cards, images), card 16px (search dialog), 6px for inline code, 4px for `<kbd>`.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
Exactly four sizes and two weights. The admin also uses 500 and 700. The docs deliberately use only 400 and 600, so only two DM Sans weights ship.
|
||||
|
||||
| Role | Size | Weight | Line Height | Used for |
|
||||
|------|------|--------|-------------|----------|
|
||||
| Body | 16px | 400 | 1.6 | Prose paragraphs, lists, pager titles (at 600), search result headings (at 600), H3 and deeper headings (at 600, line height 1.5) |
|
||||
| Label | 14px | 400 | 1.5 | Sidebar items, TOC items, section eyebrow, page actions, code (DM Mono), inline code, table cells, callout body, footer, search input and result excerpts, `<kbd>`. Sidebar section titles, the TOC heading, table headers and callout titles use 600 |
|
||||
| Heading | 20px | 600 | 1.3 | H2 headings, page lead (description) at **400** in the muted colour, the header wordmark |
|
||||
| Display | 32px | 600 | 1.2 | H1 page title, `letter-spacing: -0.02em` (admin title tracking) |
|
||||
|
||||
Rules:
|
||||
|
||||
- Headings: H1 32/600, H2 20/600, H3 16/600. H4 and deeper render like H3 (16/600). Content should not use them, and the TOC lists only H2 and H3.
|
||||
- Code is DM Mono **400 only**. DM Mono has no 600, and synthetic bold is disabled (`font-synthesis: none`). Syntax tokens differ by colour, not weight.
|
||||
- `<strong>` is 600. `<em>` uses the vendored DM Sans 400 italic.
|
||||
- Inline code is 14px (0.875em of body) with line height inherited from its parent.
|
||||
- `font-variant-numeric: tabular-nums` applies to table cells.
|
||||
- The root sets `-webkit-text-size-adjust: 100%` and `font-size: 16px` (the admin root is 14px, but a reading site uses 16px).
|
||||
|
||||
---
|
||||
|
||||
## Color
|
||||
|
||||
Tokens are copied by value from `admin/src/styles/main.css`, with the same names. Light values live on `:root`. Dark values apply under `:root.dark` **and** under `@media (prefers-color-scheme: dark) { :root:not(.light) { … } }`, so dark mode works without JS (see Interactions → Theme). `color-scheme` is `light` or `dark` to match, and pages carry `<meta name="color-scheme" content="light dark">`.
|
||||
|
||||
| Role | Light | Dark | Usage |
|
||||
|------|-------|------|-------|
|
||||
| Dominant (60%) | `#ffffff` (`--c-surface`) | `#111726` (`--c-bg`) | Page and content column background, TOC column, search dialog body. Declared as `--d-page` pointing at the listed admin token per mode |
|
||||
| Secondary (30%) | `#f4f6f9` (`--c-bg`) / `#f3f5f8` (`--c-subtle`) | `#182033` (`--c-surface`) / `#1f283d` (`--c-subtle`) | Sidebar background (`--d-panel`: `#f4f6f9` / `#182033`). Code blocks, inline code, table header rows and NOTE callouts use `--c-subtle` |
|
||||
| Brand bar | `#1d2740` (`--c-side`) | `#0d1320` (`--c-side`), 1px bottom border `#222b40` | Header bar only. It stays dark in both modes, as the admin sidebar does. Header text is `#c3cbda` idle and `#ffffff` on hover; the search trigger placeholder is `#9aa6bd` |
|
||||
| Text | `#141b2d` | `#eef1f6` | Body text, headings, active sidebar item |
|
||||
| Muted | `#566175` | `#a9b3c6` | Sidebar idle items, TOC items, eyebrow, lead, page actions, footer, search excerpts, code comments (light) |
|
||||
| Border | `#e6e9ef` | `#29334b` | Sidebar right border, table rules, code block border, pager card border, header of `toc-inline` |
|
||||
| Primary (links) | `#22304d` | `#fcd34d` | Content links, pager titles and the active TOC bar. Content links are always underlined (1px, `text-underline-offset: 3px`, decoration colour `--c-border-strong` `#d2d8e2` / `#3a4661`, which switches to the link colour on hover). Colour alone never marks a link |
|
||||
| Accent (10%) | `#fcd34d` | `#fcd34d` | Only the elements listed below |
|
||||
|
||||
**Dark-mode Primary shares the accent hue.** Dark Primary `#fcd34d` is copied by value from the admin `--c-primary` dark token. Content links, pager titles and the active TOC bar are therefore yellow in dark mode. They are **not** accent uses. The link underline, not the colour, marks them. To keep code-heavy pages from turning mostly yellow, syntax keywords in dark mode do not use this hue (see `.tok-kw` below).
|
||||
| Destructive | `#c62828` on `#fdf0f0` | `#f58a8a` on `#3a1d24` | The WARNING callout only. This phase has no destructive actions |
|
||||
|
||||
Accent reserved for (and nothing else):
|
||||
|
||||
1. The "CMS" half of the header wordmark and the 20px `sun` icon in its 32px circle, filled `rgba(252, 211, 77, 0.14)` (admin `--color-accent-soft`).
|
||||
2. The focus ring: `outline: 3px solid var(--c-ring)` with `outline-offset: 2px` on every focusable element (`--c-ring` is `rgba(252,196,40,.55)` light and `rgba(252,211,77,.45)` dark). The search input uses `box-shadow: 0 0 0 3px var(--c-ring)` and `border-color: var(--c-primary)`. The ring is never removed.
|
||||
3. Selection tint `--c-sel` (`#fdf3cf` / `#3a3622`). It is used for the active sidebar item background, the active search result row, and `<mark>` on matched search terms.
|
||||
|
||||
Semantic callout colours reuse admin tokens: NOTE uses bg `--c-subtle`, a 4px left border `--c-border-strong` and a `--c-muted` icon. TIP uses bg `--c-ok-bg` (`#e3f4e8` / `#173826`), a 4px left border and icon in `--c-ok-text` (`#1c6b35` / `#8fdfa8`). WARNING uses bg `--c-danger-soft`, with a 4px left border and icon in `--c-danger`. Callout body text is `--c-text` in all three.
|
||||
|
||||
**Syntax highlighting** (the only colours not already in the admin palette; code weight is always 400):
|
||||
|
||||
| Token class | Light (on `#f3f5f8`) | Dark (on `#1f283d`) | Covers |
|
||||
|-------------|----------------------|---------------------|--------|
|
||||
| plain | `#141b2d` (15.7:1) | `#eef1f6` (13.0:1) | Identifiers, operators, punctuation |
|
||||
| `.tok-kw` | `#22304d` (12.0:1) | `#93c5fd` (8.1:1) | Go keywords, YAML keys (`.tok-key` shares it), shell command word after the prompt |
|
||||
| `.tok-str` | `#1c6b35` (6.0:1) | `#8fdfa8` (9.3:1) | String and rune literals, YAML scalar strings |
|
||||
| `.tok-com` | `#566175` (5.7:1) | `#8a95ab` (4.9:1) | Comments, including `// Output:` lines, and YAML `#` comments |
|
||||
| `.tok-num` | `#9a3412` (6.7:1) | `#fdba74` (8.7:1) | Numeric literals, YAML booleans and numbers |
|
||||
| `.tok-prompt` | `#566175` | `#8a95ab` | The `$ ` prompt in `sh` fences (`user-select: none`, so copy skips it) |
|
||||
|
||||
**Measured contrast** (WCAG, computed 2026-09-28): body text 17.2:1 light and 15.8:1 dark. Muted on page 6.3:1 light and 8.5:1 dark. Muted on the sidebar 5.8:1 light and 7.7:1 dark. Links 13.1:1 light and 12.4:1 dark. Header idle text 9.1:1. Header placeholder 6.1:1. Active sidebar item 15.4:1 light and 10.7:1 dark. Callout body 15.0 to 15.4:1 light and 11.4 to 13.4:1 dark. Every pair clears AA (4.5:1) for 14px text.
|
||||
|
||||
---
|
||||
|
||||
## Focal Points
|
||||
|
||||
Each screen has one primary visual anchor. Everything else is secondary and uses muted or 14px styling.
|
||||
|
||||
| Screen | Focal point | Why |
|
||||
|--------|-------------|-----|
|
||||
| Content page (guide or API reference) | The H1 page title (32/600), then the prose column | It is the only 32px element on the page. The sidebar, TOC and page actions are 14px muted |
|
||||
| Index page | The H1 "SummerCMS documentation" | It uses the same template as a content page, with no hero |
|
||||
| Search dialog | The search input (44px, focused on open), then the results list | Focus lands in the input. The active result row carries the only `--c-sel` fill |
|
||||
| 404 page | The H1 "Page not found" | There is no TOC, pager or page actions to compete with it |
|
||||
|
||||
## Page Anatomy (content column, top to bottom)
|
||||
|
||||
1. **Eyebrow**: the section display title from `docs/site.yaml` at 14/600 in muted, bottom margin 8px. The index page has none.
|
||||
2. **H1**: the frontmatter `title` at 32/600. The Markdown `# Title` line is stripped (RESEARCH §Q5) and never rendered twice.
|
||||
3. **Lead**: the frontmatter `description` at 20/400 in muted, top margin 8px, bottom margin 16px.
|
||||
4. **Page actions**: a row at 14/400 muted with a 16px gap and 16px icons.
|
||||
- `pencil` **Edit this page** links to `edit_url` with `{path}` filled. It is omitted when `edit_url` is empty.
|
||||
- `file-text` **View as Markdown** links to the sibling `.md` URL.
|
||||
- Both are ordinary links (underline on hover). There is a 1px `--c-border` rule 24px below the row.
|
||||
5. **`toc-inline`** (below 1280px only, and only when the TOC renders): a `<details>` with the summary "On this page" at 14/600, listing the same items as the TOC column.
|
||||
6. **Prose**. Rules for each element:
|
||||
- H2 margin is 48px top and 16px bottom. H3 margin is 32px top and 8px bottom.
|
||||
- Paragraphs and lists have a 16px bottom margin. Nested lists indent 24px.
|
||||
- Tables render as `display: block; overflow-x: auto; max-width: 100%`. Headers are 14/600 on `--c-subtle`, cells 14/400, padding 8px by 16px, with a 1px `--c-border` bottom rule.
|
||||
- Images: `max-width: 100%`, radius 12px, 1px border.
|
||||
- A plain blockquote (not a callout) has a 4px left border in `--c-border-strong`, 16px left padding and muted text.
|
||||
- A horizontal rule is a 1px `--c-border` line with 48px vertical margin.
|
||||
7. **Pager**: 32px above it, a 2-column grid with a 16px gap.
|
||||
- Each card is a whole-card link with a 1px `--c-border`, radius 12px and 16px padding.
|
||||
- Line 1 is **Previous** or **Next** at 14/400 muted, with a 16px `chevron-left` or `chevron-right`.
|
||||
- Line 2 is the target page title at 16/600 in the primary colour.
|
||||
- A line 3 appears only when the target is in a different section: the section title at 14/400 muted.
|
||||
- The Next card is right-aligned in column 2. On the first page the Previous cell stays empty, so Next stays right. On the last page the Next cell is empty.
|
||||
- If a page has neither a previous nor a next page (a one-page site), the pager `<nav>` is not rendered at all.
|
||||
- Target page titles and section titles wrap onto more lines. They are never truncated or ellipsised. Both cards in a row stretch to the taller card's height.
|
||||
- Order is the sidebar order and crosses section boundaries.
|
||||
- Hover changes the border to `--c-border-strong` and the background to `--c-hover`.
|
||||
- Below 640px the cards stack into 1 column, Previous first.
|
||||
8. **Footer**: 1px `--c-border` top rule, 24px padding, 14/400 muted. The left side reads "SummerCMS documentation". The right side has links to `llms.txt` and `llms-full.txt`.
|
||||
|
||||
**Heading permalinks.** Every H2 and H3 gets an `<a class="heading-anchor" href="#<id>" aria-label="Link to section: <heading text>">#</a>` after the heading text. It is muted, sits 8px after the heading text, and becomes visible on heading hover or anchor focus. On touch devices it is always hidden. IDs come from the shared slug `parser.IDs` (RESEARCH Pitfall 3).
|
||||
|
||||
**Code blocks** (`<figure class="code">`):
|
||||
|
||||
- Container: `--c-subtle` bg, 1px `--c-border`, radius 12px, `overflow: hidden`.
|
||||
- Caption bar (only for fences with `src=`): a `<figcaption>` 32px tall with 16px horizontal padding, a 1px bottom border and 14px DM Mono muted text. It shows the repo-relative path plus `#Ident` or `#region`, for example `modules/bonfire/example_test.go#ExampleNewRoot`. It links to `source_url` with `{path}` filled (the fragment is not sent to the forge). Fences without `src=` have no caption.
|
||||
- `<pre>`: 16px padding, DM Mono 14/1.6, `tab-size: 4`, `overflow-x: auto`, and no wrapping. Horizontal scroll stays inside the block.
|
||||
- Copy button: 32px square, placed top-right 8px from the edges (inside the caption bar when one exists). Its icon is `copy` with `aria-label="Copy code"`. It is transparent, turns `--c-hover` on hover, and shows the focus ring. It appears only under `html.js` and only when `navigator.clipboard?.writeText` exists. It copies the `<code>` text with prompt spans excluded.
|
||||
|
||||
**Callouts** (`<aside class="callout callout-…" role="note">`): radius 12px, 16px padding, 4px left border and a 16px gap from the icon. The title row shows a 16px icon (`info` for NOTE, `lightbulb` for TIP, `triangle-alert` for WARNING) and the title at 14/600, then the body at 14/400. Only NOTE, TIP and WARNING are allowed. Any other `> [!TYPE]` marker is a build problem (see Copywriting → CLI).
|
||||
|
||||
---
|
||||
|
||||
## Interactions
|
||||
|
||||
**Sidebar**
|
||||
|
||||
- Sections appear in `docs/site.yaml` order, all expanded. There is no collapsing.
|
||||
- Section titles are 14/600 in `--c-text`, with 8px below the title and 24px between groups.
|
||||
- Items are 14/400 muted, min-height 32px, padding 4px by 8px, radius 8px. Titles wrap and are never truncated.
|
||||
- Items in the API reference section use DM Mono 14/400, because they are module names (`surf`, `lagoon`).
|
||||
- Hover: `--c-hover` bg (`#f1f3f7` / `#212b42`) and `--c-text`, with a 150ms ease-out colour transition.
|
||||
- Active item: `--c-sel` bg, `--c-text`, weight 600 and `aria-current="page"`.
|
||||
- On load, `site.js` scrolls the active item into view with `block: "nearest"`, with no animation.
|
||||
|
||||
**Mobile drawer (< 1024px, JS only)**
|
||||
|
||||
- The header **Menu** button (`menu` icon, 40px) carries `aria-label="Open navigation"`, `aria-expanded` and `aria-controls="sidebar"`.
|
||||
- The sidebar slides in from the left. It is 304px wide (max 85vw), full height below the header, with `--d-panel` bg. An `--c-overlay` scrim (`rgba(20,27,45,.5)` / `rgba(5,8,16,.7)`) covers the page.
|
||||
- While the drawer is open, the Menu button switches to the `x` icon with `aria-label="Close navigation"`.
|
||||
- The drawer closes on Esc, a scrim click, a link click or the close button, and focus returns to the Menu button. Focus is kept inside the drawer while it is open. Page scroll locks with `overflow: hidden` on `<body>`.
|
||||
- Transition: 200ms ease-out `transform`. With `prefers-reduced-motion: reduce` there is no transition.
|
||||
|
||||
**On-page TOC**
|
||||
|
||||
- It renders only when the page has **at least 2** H2 or H3 headings. Otherwise neither the column nor `toc-inline` renders.
|
||||
- The heading "On this page" is 14/600. H2 items are 14/400 muted. H3 items are indented 16px. Items wrap.
|
||||
- Scroll spy (JS): an `IntersectionObserver` marks the heading nearest the top as current, with `--c-text` colour, a 2px left border in the primary colour and `aria-current="location"`. Without JS there is no highlight. The links still work.
|
||||
- The TOC column scrolls on its own (`max-height: calc(100vh - 128px)`) when it is taller than the viewport.
|
||||
|
||||
**Theme toggle**
|
||||
|
||||
- It is one 40px button in the header that cycles **System → Light → Dark → System**.
|
||||
- The icon shows the current setting (`monitor`, `sun` or `moon`). `aria-label` and `title` read "Color theme: {Current}. Switch to {Next}."
|
||||
- The setting is persisted in `localStorage["summer-docs-theme"]` as `system`, `light` or `dark`. The default is `system`.
|
||||
- `assets/theme-init.js` is loaded **synchronously** in `<head>`, before the stylesheet. It reads the key and sets `html.js` plus `html.dark` or `html.light`. For `system` it sets neither and lets the media query decide. It also listens to `matchMedia('(prefers-color-scheme: dark)')` changes in system mode. There is no flash of the wrong theme.
|
||||
- If `localStorage` throws (a private mode, storage disabled), the site behaves as `system` and the toggle still works for the session. No error is shown.
|
||||
- This tri-state toggle is new behaviour. The admin follows the system only (admin/src/app/theme.ts:1-2). The admin is not changed.
|
||||
|
||||
**Search**
|
||||
|
||||
- Trigger:
|
||||
- At ≥ 1024px it is a 256px by 40px header button. It shows the `search` icon, the text "Search docs" and a right-aligned `<kbd>`. The `<kbd>` reads "Ctrl K", or "⌘ K" when `navigator.platform` or `userAgentData` reports macOS.
|
||||
- Below 1024px it is a 40px icon button with `aria-label="Search docs"`.
|
||||
- Shortcuts: ⌘K and Ctrl+K anywhere, and `/` when focus is not in a text field.
|
||||
- Dialog:
|
||||
- A native `<dialog>` opened with `showModal()`, which gives Esc to close, a focus trap and inert background. Width 640px, 96px from the top, radius 16px, `--d-page` bg, 1px `--c-border`, shadow `0 16px 40px rgba(20,27,45,.18)` (admin `--shadow-pop`).
|
||||
- Backdrop: `--c-overlay`. It fades in over 200ms. Closing is immediate.
|
||||
- Input:
|
||||
- 44px tall, radius 10px, 1px `--c-border-strong`, 16px padding, 16/400 text, with a `search` icon inside on the left.
|
||||
- Placeholder: "Search the docs". It has `role="combobox"`, `aria-expanded`, `aria-controls="search-results"` and `aria-activedescendant`.
|
||||
- Focus lands in the input when the dialog opens. The previous query is kept for the session.
|
||||
- Index loading: `search-index.json` is fetched lazily on the first open. The response is cached in memory.
|
||||
- Matching: lowercase AND-token matching, ranked title > heading > text (RESEARCH §Q5). It runs on every input event, with no debounce. The index is a few hundred KB.
|
||||
- Results:
|
||||
- A `role="listbox"` `<ul id="search-results">`, up to 20 results, `max-height: 60vh` with its own scroll.
|
||||
- Each `role="option"` row has 8px by 16px padding and radius 8px:
|
||||
- line 1: "{Section} › {Page title}" at 14/400 muted;
|
||||
- line 2: the section heading, or the page title for a page-level hit, at 16/600;
|
||||
- line 3: the excerpt at 14/400 muted, clamped to 2 lines.
|
||||
- Matched terms in lines 2 and 3 are wrapped in `<mark>` with `--c-sel` bg and inherited colour. The rows are built only with `createElement` and `textContent`, never `innerHTML` (T-11.1-02).
|
||||
- Keyboard and pointer: ↑/↓ moves the active row (`--c-sel` bg) and wraps at the ends. Enter or a click navigates to `<page>#<anchor>`. Esc closes and returns focus to the trigger.
|
||||
- Announcements: a visually hidden `aria-live="polite"` region reads the result count (copy below).
|
||||
- Without JS the trigger is hidden, as described in Layout.
|
||||
|
||||
**Copy button**
|
||||
|
||||
- On success, the icon swaps to `check` and the live region announces "Copied". It reverts after 2000ms.
|
||||
- On failure (the promise rejects), the icon stays `copy` and the live region announces "Copy failed. Select the code and copy it manually."
|
||||
|
||||
**Motion**
|
||||
|
||||
- Colour and background changes use a 150ms ease-out transition. The dialog backdrop fades in over 200ms. The drawer slides over 200ms.
|
||||
- There is no other animation and no smooth scrolling. `prefers-reduced-motion: reduce` removes every transition.
|
||||
|
||||
**Accessibility**
|
||||
|
||||
- The first focusable element is a **Skip to content** link, visually hidden until focused. It then shows at the top-left of the header in `--c-surface` bg, `--c-text`, with the ring.
|
||||
- Landmarks: `header`, the `nav` sidebar (labelled "Documentation"), `main#content`, the `aside` TOC (labelled "On this page"), the pager `nav`, and `footer`.
|
||||
- `<html lang="en">`. `<title>` is "{Page title} · SummerCMS docs". The index `<title>` is "SummerCMS documentation".
|
||||
- Every page has a `<meta name="description">` from the frontmatter, `og:title` and `og:description`, and `<link rel="alternate" type="text/markdown" href="<page>.md">` so agents can discover the raw page from the HTML.
|
||||
- Every icon-only control has an `aria-label`. Decorative SVGs are `aria-hidden`.
|
||||
|
||||
---
|
||||
|
||||
## Copywriting Contract
|
||||
|
||||
All copy is English, second person, present tense and imperative (Winter tone, RESEARCH §Q1). No copy anywhere in the theme, the CLI or the generated files names a consuming application (D-11). The forbidden-name test runs over built output (RESEARCH §Q6).
|
||||
|
||||
| Element | Copy |
|
||||
|---------|------|
|
||||
| Primary CTA | **Search docs** (header search trigger, and the `aria-label` of the icon-only trigger) |
|
||||
| Empty state heading | Search with an empty query: **Search the documentation** |
|
||||
| Empty state body | "Type a module name, a command or a topic, for example `surf`, `migrate` or `relations`." |
|
||||
| Search no results | Heading: **No results for "{query}"**. Body: "Check the spelling, or try a shorter term such as a module or command name." |
|
||||
| Search loading | "Loading the search index…" (shown only while the fetch is pending) |
|
||||
| Error state (search over `file://`, fetch rejected) | "Search needs a web server. Run `summer docs:serve` and open the address it prints." |
|
||||
| Error state (search index fetch returned non-200 or invalid JSON) | "The search index could not be loaded. Reload the page to try again." |
|
||||
| Search result count (live region) | "No results" / "1 result" / "{n} results" |
|
||||
| Destructive confirmation | None. This phase has no destructive user actions. The only irreversible operation, cleaning the `--out` directory, is guarded by the `.summer-docs` marker refusal in the CLI copy below (T-11.1-05), not by a prompt. |
|
||||
| Header wordmark | "Summer" + "CMS" (the "CMS" half in accent). It links to the index, with `aria-label="SummerCMS documentation home"` |
|
||||
| Search input placeholder | "Search the docs" |
|
||||
| Skip link | "Skip to content" |
|
||||
| Menu button | "Open navigation" / "Close navigation" (`aria-label`) |
|
||||
| Theme toggle | "Color theme: System. Switch to Light." / "Color theme: Light. Switch to Dark." / "Color theme: Dark. Switch to System." |
|
||||
| Page actions | "Edit this page" · "View as Markdown" |
|
||||
| TOC heading (column and `toc-inline` summary) | "On this page" |
|
||||
| Pager labels | "Previous" · "Next" |
|
||||
| Copy button | `aria-label` "Copy code". Live region: "Copied" / "Copy failed. Select the code and copy it manually." |
|
||||
| Heading permalink | `aria-label` "Link to section: {heading}" |
|
||||
| Callout titles | "Note" · "Tip" · "Warning" |
|
||||
| Footer | "SummerCMS documentation" · "llms.txt" · "llms-full.txt" |
|
||||
| 404 page (`site/404.html`, served with status 404 by `docs:serve`) | H1 **Page not found**. Lead: "This page does not exist or has moved." Body: "Use the sidebar or search to find what you need, or go back to the documentation home." The words "documentation home" link to the index page It uses the normal shell with no active sidebar item, no TOC, no pager and no page actions |
|
||||
| Index page | Title **SummerCMS documentation**. Description: "SummerCMS is a content management framework for Go, inspired by WinterCMS." The index uses the standard page template with no hero and no cards, as Winter's introduction does. The content plans write its body |
|
||||
|
||||
**CLI output** (terminal is the second surface of this phase). Every problem line uses `file:line: rule: message` (RESEARCH §Architecture) so agents can fix it mechanically. Rules are lowercase single words.
|
||||
|
||||
| Command / situation | Copy |
|
||||
|---------------------|------|
|
||||
| `docs:build` success | `docs:build: wrote {n} pages to {out}` |
|
||||
| `docs:build` with problems (nothing written) | One line per problem, then `docs:build: {n} problems, nothing written` |
|
||||
| Drifted snippet | `{file}:{line}: snippet: body differs from {src} (run: summer docs:sync)` |
|
||||
| Missing snippet source | `{file}:{line}: snippet: {src} not found` |
|
||||
| Go fence without `src=` | `{file}:{line}: snippet: go code block has no src= reference` |
|
||||
| Unknown identifier | `{file}:{line}: identifier: {pkg}.{Ident} does not exist in modules/{pkg}` |
|
||||
| Broken link / anchor | `{file}:{line}: link: {target} does not resolve` / `{file}:{line}: link: #{anchor} not found in {page}` |
|
||||
| Unknown command | `{file}:{line}: command: "{name}" is not a summer or application command` |
|
||||
| Forbidden name | `{file}:{line}: forbidden: consuming-application name in output` (the matched word is not echoed) |
|
||||
| Unknown callout type | `{file}:{line}: callout: unknown type {TYPE} (use NOTE, TIP or WARNING)` |
|
||||
| Frontmatter | `{file}:1: frontmatter: {detail}`, where the detail is one of: `missing field "{name}"`, `unknown field "{name}"`, `section "{s}" does not match directory "{d}"`, `order {n} already used by {other}`, `first line must be "# {title}"`, `description is longer than 160 characters` |
|
||||
| Section with no pages | `docs/site.yaml:{line}: section: "{name}" has no pages (add the section in the same change as its first page)` |
|
||||
| Module without README | `modules/{name}: readme: package has Go files but no README.md` |
|
||||
| Output dir guard | `docs:build: refusing to clean {out}: it has no .summer-docs marker. Remove the directory or choose another --out.` / `docs:build: --out must not be inside --src or equal to the repository root` |
|
||||
| `docs:sync` | `docs:sync: updated {n} snippets in {m} files` / `docs:sync: all snippets up to date` |
|
||||
| `docs:serve` start | `Serving docs at http://{addr} (press Ctrl+C to stop)` |
|
||||
| `docs:serve` non-loopback refusal | `docs:serve: refusing to listen on {addr}: not a loopback address. Pass --allow-remote to serve on the network.` |
|
||||
| `docs:serve` rebuild failure | Print the problem lines. Keep serving the last good build and print `docs:serve: build failed, still serving the previous version` |
|
||||
|
||||
Content rules the build enforces (so that the UI stays consistent):
|
||||
|
||||
- A frontmatter `description` is one sentence of at most 160 characters. It is used for the lead, the meta description, llms.txt notes and pager context.
|
||||
- Headings are ASCII and contain no links or inline code (RESEARCH §Q6 lint).
|
||||
|
||||
---
|
||||
|
||||
## UI Considerations
|
||||
|
||||
The probe ran on 2026-09-28 over 8 elements: search, sidebar, TOC, pager, content page, theme toggle, copy button and 404. It raised 48 considerations: 16 explicit, 4 backstop, 28 dismissed, 0 unresolved. The 404 element came back unclassified, and the not-found row below covers it by manual review.
|
||||
|
||||
| Category | Element(s) | Status | Resolution / Reason |
|
||||
|----------|------------|--------|---------------------|
|
||||
| empty | search results (list-collection) | ✅ covered | An empty query shows the "Search the documentation" empty-state copy from the Copywriting Contract, and no results list renders |
|
||||
| zero-one-many | search results (list-collection) | ✅ covered | 0 shows the no-results copy. 1 to 20 show rows. More than 20 are truncated to the top 20 by rank. The live region uses the singular and plural count copy |
|
||||
| loading | search results (list-collection) | ✅ covered | While `search-index.json` is being fetched on first open, the "Loading the search index…" copy shows in the results area |
|
||||
| error | search results (list-collection) | ✅ covered | A rejected fetch (`file://`) and a non-200 or invalid-JSON response each show their Copywriting Contract error copy. The dialog stays usable and closable |
|
||||
| populated | search results (list-collection) | 🧪 backstop | Result rows show section › title, heading and a 2-line excerpt with `<mark>` highlights, and arrow keys and Enter work. Verified in manual UAT via `summer docs:serve` (VALIDATION manual-only row: no JS runner without Node) |
|
||||
| overflow | search results (list-collection) | ✅ covered | The results list has `max-height: 60vh` and its own vertical scroll. The dialog never exceeds the viewport |
|
||||
| long-text | search results (list-collection) | ✅ covered | Excerpts clamp to 2 lines. Titles and headings wrap and are never truncated |
|
||||
| overflow | sidebar (nav) | ✅ covered | The sidebar scrolls on its own (`overflow-y: auto`, height `calc(100vh - 64px)`), and the active item is scrolled into view on load |
|
||||
| long-text | sidebar and TOC items (nav) | ✅ covered | Items wrap onto more lines with min-height 32px. There is no ellipsis |
|
||||
| zero-one-many | on-page TOC (nav) | ✅ covered | With fewer than 2 H2/H3 headings, neither the TOC column nor `toc-inline` renders. With 2 or more they render, and a tall TOC scrolls in its column |
|
||||
| partial | prev/next pager (nav) | ✅ covered | The first page renders only Next, kept in the right column. The last page renders only Previous. A cross-section target adds the section line |
|
||||
| empty | prev/next pager (nav) | ✅ covered | A page with no previous and no next page (a one-page site) renders no pager `<nav>` |
|
||||
| long-text | prev/next pager (nav) | ✅ covered | Target and section titles wrap and are never truncated. Both cards in a row stretch to equal height |
|
||||
| error | not-found (nav) | ✅ covered | The build writes `site/404.html` with the "Page not found" copy, and `docs:serve` returns it with status 404 |
|
||||
| overflow | code blocks and tables (static-content) | ✅ covered | `<pre>` and tables scroll horizontally inside their own box (`overflow-x: auto`), and the page body never scrolls horizontally |
|
||||
| long-text | prose, inline code (static-content) | ✅ covered | Inline code and bare URLs use `overflow-wrap: anywhere`, and prose wraps inside the 768px column |
|
||||
| error | theme toggle (interactive-control) | ✅ covered | If `localStorage` throws, the site falls back to `system` for the session, the toggle still cycles, and no error is shown |
|
||||
| error | copy button (interactive-control) | ✅ covered | The button is not rendered without `navigator.clipboard`. On a rejected write, the live region announces the "Copy failed" copy |
|
||||
| loading | theme first paint (interactive-control) | 🧪 backstop | The synchronous `theme-init.js` in `<head>` applies `dark`/`light` before first paint, so there is no flash of the wrong theme. Verified in manual UAT: reload in each of the 3 modes |
|
||||
| populated | full page render, both themes (static-content + nav) | 🧪 backstop | Measured contrast pairs (Color section) hold in the rendered site in light and dark. Manual UAT checks a guide page and an API reference page in both modes at 1280px, 1024px and 375px |
|
||||
| error | mobile drawer, no JS (nav) | 🧪 backstop | Without JS, the sidebar renders above the content below 1024px, and the search and theme buttons are hidden. Checked in manual UAT with JS disabled |
|
||||
|
||||
**Dismissed (28), with reasons:**
|
||||
|
||||
- **Runtime loading and error states on build-time parts** (sidebar, TOC, pager, content page: loading and error, 8 in total). These parts are static HTML written by `docs:build`. They never fetch data at runtime, so they have no loading state and no runtime error state. Build-time failures are CLI problem lines, and nothing is written (Copywriting Contract → CLI).
|
||||
- **Empty, partial and zero-one-many on the sidebar.** A section with no pages is a build problem (`section: … has no pages`). Sections with one or more pages render the same item list.
|
||||
- **Empty, partial, overflow and zero-one-many on the content page.** Page-level omissions are specified elsewhere: a missing `edit_url` omits the action, and fewer than 2 headings omits the TOC. Overflow and long text are covered by the static-content rows above.
|
||||
- **TOC empty and partial, pager populated, overflow and zero-one-many.** The TOC 2-heading threshold covers them, along with the pager partial row and the full-page backstop row.
|
||||
- **Sidebar, TOC and search populated.** The full-page backstop row and the search populated row cover them.
|
||||
- **Theme toggle long-text; copy button overflow, long-text and loading.** Both are fixed-size icon buttons with no text content. The clipboard write resolves immediately, and its only states are success and failure (covered above).
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| Registry | Blocks Used | Safety Gate |
|
||||
|----------|-------------|-------------|
|
||||
| shadcn official | none (not applicable, no React) | not required |
|
||||
| Third-party registries | none | not applicable |
|
||||
| Vendored static assets (not code) | 8 DM Sans / DM Mono woff2 files from `@fontsource/dm-sans@5.3.0` and `@fontsource/dm-mono@5.3.0`, plus Lucide 1.17.0 SVG path data for 15 icons | Licence verified from the installed package.json on 2026-09-28 (OFL-1.1, OFL-1.1, ISC). Licence files ship alongside the assets. They are fonts and SVG paths only, with no executable code, so no `shadcn view` gate applies |
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [x] Dimension 1 Copywriting: PASS
|
||||
- [x] Dimension 2 Visuals: PASS (FLAG resolved: Focal Points section added)
|
||||
- [x] Dimension 3 Color: PASS (FLAG resolved: dark `.tok-kw` moved off the accent hue, dark Primary sharing the accent hue documented)
|
||||
- [x] Dimension 4 Typography: PASS
|
||||
- [x] Dimension 5 Spacing: PASS
|
||||
- [x] Dimension 6 Registry Safety: PASS
|
||||
- [x] Dimension 7 Inventory Provenance: PASS
|
||||
|
||||
**Approval:** approved 2026-09-28
|
||||
Reference in New Issue
Block a user