Reconcile the two-theme documentation contract

0602a1f2a926 · Christopher David · · parent 558d89cd72c2

Reconcile the two-theme documentation contract

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified AGENTS.md
  • modified INVARIANTS.md
  • modified docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md
  • modified docs/component-library.md
  • modified ops/ci/docs-check.exs
  • modified test/openagents_web/live/components_live_test.exs

Diff

6 files changed, +75 -18

AGENTS.md modified +4 -4

@@ -42,17 +42,17 @@ custom classes must fully style the input

42 42
43 43
- **Always use and maintain this import syntax** in the app.css file for projects generated with `phx.new`
44 44
- **Never** use `@apply` when writing raw css
45
- **There is exactly one component system: vendored Basecoat plus OpenAgents style pack.** Basecoat lives in `assets/vendor/basecoat/components/` and carries structure (display, padding, min-height); `assets/css/openagents.css` carries OpenAgents' identity (motion tokens, radius scale, colour, the notched variant, corner frames) and must stay the last import so its declarations win.
45
- **There is exactly one component system: vendored Basecoat plus OpenAgents style pack.** Basecoat lives in `assets/vendor/basecoat/components/` and carries structure (display, padding, min-height); `assets/css/openagents.css` carries OpenAgents' identity (motion tokens, radius scale, color, the notched variant, corner frames) and must stay the last import so its declarations win.
46 46
  - **Reach for `OpenAgentsWeb.UI` first.** It wraps that CSS in 22 ready primitives — `button/1`, `card/1`, `badge/1`, `alert/1`, `input/1`, `textarea/1`, `label/1`, `field/1`, `header/1`, `table/1`, `list/1`, `avatar/1`, `menu/1`, `empty/1`, `kbd/1`, and the rest. Fall back to hand-written classes only when no primitive covers the shape.
47 47
  - **Variants are data attributes, not classes.** A control is `class="btn"` plus `data-variant="primary"` / `data-size="sm"` / `data-tone="danger"`. Never invent `btn-primary`-style variant classes; they defeat the whole point of the split.
48
  - **Never add a second component library — DaisyUI above all.** DaisyUI was removed deliberately. It emitted flat `.btn` rules (setting background, colour, border) from a cascade layer that outranked every `.btn[data-variant=…]` in `openagents.css`, so all eight UI button variants rendered identically on staging. Any library with the same shape will do the same thing again.
48
  - **Never add a second component library — DaisyUI above all.** DaisyUI was removed deliberately. It emitted flat `.btn` rules (setting background, color, border) from a cascade layer that outranked every `.btn[data-variant=…]` in `openagents.css`, so all eight UI button variants rendered identically on staging. Any library with the same shape will do the same thing again.
49 49
  - **Import Basecoat components individually**, one `@import "../vendor/basecoat/components/<name>.css"` per component a surface actually uses. Component CSS lands in `@layer components` and ships whether or not the class appears in markup, so the list is a budget. **Never** import `basecoat.css`, `basecoat-base.css`, or `basecoat-components.css` — each pulls in all 39 components at once.
50
  - The palette is the token ladder at the top of `app.css` (`--ink-void` / `--ink-surface` / `--ink-raised`, the text and line tiers, and the four semantic colours). It is dark-only. Use the Basecoat utility names built on it — `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, and `border-border`. The retired compatibility aliases no longer exist
50
  - The palette is the two owned token ladders in `assets/css/openagents.css`. The interface supports only light and dark palettes; the system preference selects between them and is not a third palette. Use the Basecoat utility names built on these tokens — `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, and `border-border`. The retired compatibility aliases no longer exist
51 51
- Add custom Tailwind only when the design is unique to OpenAgents and no primitive fits.
52 52
- Out of the box **only the app.js and app.css bundles are supported**
53 53
  - You cannot reference an external vendor'd script `src` or link `href` in the layouts
54 54
  - You must import the vendor deps into app.js and app.css to use them
55
  - **Never write inline <script>custom js</script> tags within templates**
55
  - **Never write inline `<script>` tags within templates.** The only exception is the synchronous, content-free theme bootstrap in `root.html.heex`, which must run before the first stylesheet paint. Put every other client behavior in `app.js` or a colocated LiveView hook
56 56
57 57
### UI and UX design guidelines
58 58
INVARIANTS.md modified +15 -10

@@ -1511,23 +1511,28 @@ Basecoat's JavaScript is never loaded and the account menu uses the native

1511 1511
popover API, so the identity control works without custom client-side script.
1512 1512
Where Basecoat has no equivalent, the primitive wraps the browser's own control
1513 1513
rather than reimplementing it: `audio_player/1` is a native `<audio controls>`
1514
in a OpenAgents-styled box, keyboard operable and announced by the user agent, and it
1515
requires an accessible name because a page of recordings is otherwise a page of
1516
identically announced players.
1517
The shared corner radius, the self-hosted Geist faces, the single dark theme,
1518
and the reserved semantic color meanings hold across every component. Depth is
1519
limited to the sanctioned lift, halo, and state-ring tokens.
1514
in an OpenAgents-styled box, keyboard operable and announced by the user agent,
1515
and it requires an accessible name because a page of recordings is otherwise a
1516
page of identically announced players.
1517
The shared corner radius, the self-hosted Geist faces, exactly two owned themes,
1518
and the reserved semantic color meanings hold across every component. The
1519
system preference selects light or dark through `prefers-color-scheme`; it does
1520
not introduce a third palette. Depth is limited to the sanctioned lift, halo,
1521
and state-ring tokens.
1520 1522
Adopting an additional Basecoat component requires a
1521 1523
`docs/component-library.md` change and an explicit per-component import. The
1522
application exposes no light/system theme control while the palette is
1523
dark-only. Apps SDK UI glyphs are preferred; the pinned Heroicons fallback has
1524
an explicit inventory and no current product call sites.
1524
application exposes one system, light, and dark preference control. A
1525
synchronous, content-free bootstrap applies the stored choice before first
1526
paint and synchronizes changes across tabs. Apps SDK UI glyphs are preferred;
1527
the pinned Heroicons fallback has an explicit inventory and no current product
1528
call sites.
1525 1529
1526 1530
Evidence: `assets/vendor/basecoat/README.md`, `assets/css/openagents.css`,
1527 1531
`priv/static/fonts`, `OpenAgentsWeb.UI`, `OpenAgentsWeb.ComponentCatalog`,
1528 1532
`OpenAgentsWeb.UITest`, `OpenAgentsWeb.UIGalleryLiveTest`,
1529 1533
`test/openagents_web/component_catalog_test.exs`,
1530
`test/openagents_web/icon_affordances_test.exs`, and
1534
`test/openagents_web/icon_affordances_test.exs`,
1535
`test/openagents_web/live/components_live_test.exs`, and
1531 1536
`assets/test/css_contract_test.mjs`.
1532 1537
1533 1538
### LEADERBOARD-001 — The public board publishes one bounded projection
docs/2026-08-20-integration-hardening-and-staging-readiness-recommendations.md modified +4

@@ -444,6 +444,10 @@ records.

444 444
  Tailwind and proves that exactly two owned themes and the system fallback
445 445
  survive, Basecoat geometry precedes the OpenAgents style pack, and all eight
446 446
  governed button variants survive the cascade.
447
- Reconciled `AGENTS.md`, UI-003, and the component guide with the later light
448
  palette. The documentation check now refuses the superseded dark-only claims
449
  in active contracts, and the component test enumerates the system, light, and
450
  dark controls without treating the system preference as a third palette.
447 451
- Migrated current glyph uses to the preferred vendored Apps SDK set. Retained
448 452
  Heroicons only as the owner-approved second tier, pinned it to immutable
449 453
  revision `0435d4ca364a608cc75e2f8683d374e55abbae26`, and recorded an empty
docs/component-library.md modified +8 -4

@@ -21,7 +21,7 @@ The current `OpenAgentsWeb.UI` inventory is:

21 21
| Component | Purpose |
22 22
| --- | --- |
23 23
| `button/1`, `text_button/1` | Boxed, link, chip, destructive, and primary actions |
24
| `input/1`, `textarea/1`, `label/1`, `field/1` | Form-aware controls and labelled groups |
24
| `input/1`, `textarea/1`, `label/1`, `field/1` | Form-aware controls and labeled groups |
25 25
| `header/1`, `table/1`, `list/1` | Page headings and structured data |
26 26
| `alert/1`, `badge/1`, `status_indicator/1` | Explicit feedback and semantic state |
27 27
| `card/1`, `frame/1` | Bounded content and decorative framing |

@@ -32,8 +32,11 @@ The current `OpenAgentsWeb.UI` inventory is:

32 32
33 33
`OpenAgentsWeb.Layouts` owns `app/1`, `flash_group/1`, `command_bar/1`,
34 34
and `account_control/1`. Product templates begin with `Layouts.app` and never
35
render `flash_group/1` directly. The staging palette is deliberately dark-only,
36
so the application exposes no theme control or browser theme state.
35
render `flash_group/1` directly. The command bar exposes one system, light, and
36
dark preference control over exactly two owned palettes. The system choice
37
stores no override and follows `prefers-color-scheme`. The synchronous theme
38
bootstrap in `root.html.heex` applies an explicit choice before the first paint
39
and synchronizes it across tabs.
37 40
38 41
## Specialized components
39 42

@@ -70,7 +73,8 @@ repository breadcrumbs, file rows, commit rows, and bounded diff panels.

70 73
71 74
The compiled CSS contract test proves that Basecoat geometry precedes the
72 75
OpenAgents style pack, every supported button variant survives compilation, no
73
retired palette alias survives, and no theme selector enters the bundle.
76
retired palette alias survives, both owned themes compile, the operating-system
77
fallback compiles, and no third theme selector enters the bundle.
74 78
75 79
See [the UI roadmap](issues-projects-ui-roadmap.md),
76 80
[ADR 0005](decisions/0005-use-basecoat-and-one-component-system.md), and the
ops/ci/docs-check.exs modified +14

@@ -27,11 +27,19 @@ defmodule OpenAgents.DocsCheck do

27 27
    {~r{/home/[A-Za-z0-9._-]+/(?:work|code)(?:/|\b)}, "Linux developer path"}
28 28
  ]
29 29
30
  @theme_contract_files ["AGENTS.md", "INVARIANTS.md", "docs/component-library.md"]
31
  @retired_theme_claims [
32
    {~r/\bdark-only\b/i, "retired dark-only theme claim"},
33
    {~r/\bsingle dark theme\b/i, "retired single-theme claim"},
34
    {~r/\bno theme selector enters the bundle\b/i, "retired selector-free theme claim"}
35
  ]
36
30 37
  def run do
31 38
    errors =
32 39
      []
33 40
      |> check_markdown_links()
34 41
      |> check_current_language()
42
      |> check_theme_contract()
35 43
      |> check_invariants()
36 44
37 45
    case Enum.reverse(errors) do

@@ -105,6 +113,12 @@ defmodule OpenAgents.DocsCheck do

105 113
    end)
106 114
  end
107 115
116
  defp check_theme_contract(errors) do
117
    Enum.reduce(@theme_contract_files, errors, fn file, acc ->
118
      scan_terms(acc, file, File.read!(file), @retired_theme_claims)
119
    end)
120
  end
121
108 122
  defp check_invariants(errors) do
109 123
    content = File.read!("INVARIANTS.md")
110 124
    sections = invariant_sections(content)
test/openagents_web/live/components_live_test.exs modified +30

@@ -78,4 +78,34 @@ defmodule OpenAgentsWeb.ComponentsLiveTest do

78 78
79 79
    refute has_element?(view, ~s{a[href="/components/theme-toggle"]})
80 80
  end
81
82
  test "the command bar exposes exactly the governed theme preferences" do
83
    document =
84
      OpenAgentsWeb.Layouts.theme_toggle(%{})
85
      |> rendered_to_string()
86
      |> LazyHTML.from_fragment()
87
88
    assert document
89
           |> LazyHTML.query(~s{.theme-toggle[role="group"][aria-label="Color theme"]})
90
           |> LazyHTML.to_tree() != []
91
92
    assert document
93
           |> LazyHTML.query(
94
             ~s{button[data-theme-option="system"][aria-label="Match system theme"]}
95
           )
96
           |> LazyHTML.to_tree() != []
97
98
    assert document
99
           |> LazyHTML.query(~s{button[data-theme-option="light"][aria-label="Light theme"]})
100
           |> LazyHTML.to_tree() != []
101
102
    assert document
103
           |> LazyHTML.query(~s{button[data-theme-option="dark"][aria-label="Dark theme"]})
104
           |> LazyHTML.to_tree() != []
105
106
    assert document
107
           |> LazyHTML.query(~s{button[data-theme-option]})
108
           |> LazyHTML.to_tree()
109
           |> length() == 3
110
  end
81 111
end

This page updates live while a promote is in flight · changelog