Files
summercms/.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-UI-SPEC.md
2026-09-28 22:25:34 +02:00

40 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created, reviewed_at
phase slug status shadcn_initialized preset created reviewed_at
11.1 summercms-documentation-for-humans-and-ai-agents approved false none 2026-09-28 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
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

  • Dimension 1 Copywriting: PASS
  • Dimension 2 Visuals: PASS (FLAG resolved: Focal Points section added)
  • Dimension 3 Color: PASS (FLAG resolved: dark .tok-kw moved off the accent hue, dark Primary sharing the accent hue documented)
  • Dimension 4 Typography: PASS
  • Dimension 5 Spacing: PASS
  • Dimension 6 Registry Safety: PASS
  • Dimension 7 Inventory Provenance: PASS

Approval: approved 2026-09-28