docs: design-system ADR, contributor styling rule, refreshed README brand (#1223)

* docs: design-system decision record, contributor styling rule, refreshed brand assets

- ADR-008: visual identity as a token contract in globals.css, reviewed
  through /dev/design (laws, dark-mode resolution constraint, hex
  rationale, alternatives)
- frontend/AGENTS.md: hard rule — styling via semantic tokens only, no
  raw palette classes or hand-written dark: twins
- README: new Quiet Green hero mark and a current workspace screenshot
  replacing the pre-redesign UI; drop a dead image reference

* docs: renumber design-token record to ADR-009 and index it

ADR-008 was taken by the notebook-scoped-search record (#1331) after this
branch was opened. Renames the file, updates the frontend/AGENTS.md link
and adds the missing index row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnMHW3qrSc8Nhi7xTPfZuv

* docs: renumber the design-token ADR to 011 and index ADR-009

ADR-009 was taken by the PBKDF2 credential record (#1360), which also
never got its row in decisions/README.md; ADR-010 is reserved for the
source-chat checkpointer bridge (#1332). Update the frontend/AGENTS.md
link accordingly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScFn54FhBq3cvVbcbQJjBZ

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Luis Novo 2026-10-02 22:39:19 -03:00 • committed by GitHub
parent 553fdea554
commit 1b4443bf81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 41 additions and 59 deletions

View file

@ -383,7 +383,6 @@ Open Notebook is MIT licensed. See the [LICENSE](LICENSE) file for details.
[license-url]: https://github.com/lfnovo/open-notebook/blob/master/LICENSE.txt
[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555
[linkedin-url]: https://linkedin.com/in/lfnovo
[product-screenshot]: images/screenshot.png
[Next.js]: https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=next.js&logoColor=white
[Next-url]: https://nextjs.org/
[React]: https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black

View file

@ -0,0 +1,33 @@
# ADR-011: Visual identity is a token contract in globals.css, reviewed through /dev/design
- **Status**: Accepted
- **Date**: 2026-07
- **Related**: Discussion #1202 (community co-design), #1218 (foundation), #1220 (screen reskin), [frontend rules](../../7-DEVELOPMENT/frontend.md)
## Context
The 2026 redesign ("Quiet Green", co-designed with the community in Discussion #1202) replaced the stock shadcn theme with a product-specific visual identity. Before it, colors were scattered across components as raw Tailwind palette classes (`text-red-600`, `bg-amber-50`), each with hand-written `dark:` twins — visually inconsistent, semantically meaningless, and expensive to change. The redesign also pre-designed Stage-2 UX (citations, per-source context states, content-type identity) that needs colors with *meaning*, not decoration.
## Decision
**All visual identity lives as a layered custom-property token system in `frontend/src/app/globals.css`. Components consume semantic tokens only — raw palette classes are banned. `/dev/design` is the living reference.**
- **Layers**: raw palette (fern, sage, gold, teal, plum, mauve, slate, violet, clay) → semantic slots (surfaces, ink ramp, hairlines, action, danger, warn) → product vocabularies: content-type hues (`--type-*`), evidence/citation classes (`--cite-*`), context states (`--ctx-*`). The product vocabularies are canonized now even where nothing consumes them yet, so Stage-2 features and community PRs share one contract.
- **The laws** (enforced in review): fern acts · teal speaks (the AI/system voice) · red destroys, and only destroys · warn is clay, never an action hue · color never washes a reading surface · hairlines separate, popovers own the only real shadow · geometry is squared (4–6px) · mono is for data, not prose.
- **Dark mode** overrides only the raw layers on `.dark`; every alias re-resolves via `var()`. This works **only** because the `dark` class sits on the document root (`theme-store` behavior). CSS custom properties resolve where they are *declared*, so a nested `.dark` wrapper inherits already-resolved light values — never theme a subtree.
- **Values are hex**, kept 1:1 with the validated design spec (a pixel-diff re-application test against the design mockups reached 0.000% visible difference). Converting to oklch would reintroduce drift for zero benefit.
- **`/dev/design`** (dev-only route, 404 in production) renders every token and primitive in both themes. It is the acceptance reference for visual PRs and the regression detector: screen-level PRs must leave it byte-identical.
## Alternatives considered
- **Keep per-component palette classes** — rejected: the status quo that produced the inconsistency; every rebrand becomes a 38-file sweep (we did exactly one and never want another).
- **A theming library / CSS-in-JS tokens** — rejected: Tailwind v4 `@theme` already bridges custom properties to utilities; adding a runtime dependency contradicts the lean posture.
- **Full design-system tooling (Storybook, Figma library, published npm package)** — rejected as over-engineering for a single-app project; `/dev/design` gives the same review value at near-zero maintenance cost.
- **oklch values (shadcn convention)** — rejected: fidelity to the pixel-diff-validated spec wins over convention; the file documents this so nobody "fixes" it later.
## Consequences
- Contributors must use token utilities (`text-destructive`, `bg-warn-tint`, `text-type-video`…) — see the styling rule in [frontend/AGENTS.md](../../../frontend/AGENTS.md). A PR reintroducing `text-red-600` is a review reject.
- `slate` and `violet` are exposed to Tailwind as `slate-hue`/`violet-hue` to avoid colliding with Tailwind's built-in palettes.
- Rebrands and theme tweaks are token edits, verified in `/dev/design`, instead of app-wide sweeps.
- Stage-2 features consume `--cite-*`/`--ctx-*` without new color decisions; the vocabulary already exists.

View file

@ -50,5 +50,7 @@ What this makes easier, what it makes harder, what to watch. (bullets)
| [ADR-006](ADR-006-migration-granularity.md) | Migration granularity follows merge granularity, not release granularity | Accepted |
| [ADR-007](ADR-007-optin-runtimes.md) | Heavy extraction runtimes (Docling, Crawl4AI local) are opt-in, installed at startup | Accepted |
| [ADR-008](ADR-008-notebook-scoped-search.md) | Notebook scope is an optional filter on the existing search functions | Accepted |
| [ADR-009](ADR-009-pbkdf2-credential-encryption.md) | PBKDF2 credential key derivation with versioned ciphertext | Accepted |
| [ADR-011](ADR-011-design-token-system.md) | Visual identity is a token contract in globals.css, reviewed through /dev/design | Accepted |
| [PDR-001](PDR-001-single-user-first.md) | Single-user first; don't preclude multi-user | Accepted |
| [PDR-002](PDR-002-provider-agnostic-core.md) | Provider-agnostic core by default | Accepted |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Before After
Before After

View file

@ -1,60 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="240" height="240" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Gradient Definitions -->
<defs>
<linearGradient id="nodeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#bd34fe;stop-opacity:1" />
<stop offset="100%" style="stop-color:#41d1ff;stop-opacity:1" />
</linearGradient>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#bd34fe;stop-opacity:0.3" />
<stop offset="100%" style="stop-color:#41d1ff;stop-opacity:0.3" />
</linearGradient>
</defs>
<!-- Connection Lines -->
<g class="connections">
<path d="M120 80L80 120" stroke="url(#lineGradient)" stroke-width="2"/>
<path d="M120 80L160 120" stroke="url(#lineGradient)" stroke-width="2"/>
<path d="M80 120L120 160" stroke="url(#lineGradient)" stroke-width="2"/>
<path d="M160 120L120 160" stroke="url(#lineGradient)" stroke-width="2"/>
<path d="M80 120L160 120" stroke="url(#lineGradient)" stroke-width="2"/>
</g>
<!-- Nodes -->
<g class="nodes">
<!-- Central Node -->
<circle cx="120" cy="80" r="20" fill="url(#nodeGradient)"/>
<circle cx="120" cy="80" r="24" stroke="url(#nodeGradient)" stroke-width="1" fill="none"/>
<!-- Left Node -->
<circle cx="80" cy="120" r="16" fill="url(#nodeGradient)"/>
<circle cx="80" cy="120" r="20" stroke="url(#nodeGradient)" stroke-width="1" fill="none"/>
<!-- Right Node -->
<circle cx="160" cy="120" r="16" fill="url(#nodeGradient)"/>
<circle cx="160" cy="120" r="20" stroke="url(#nodeGradient)" stroke-width="1" fill="none"/>
<!-- Bottom Node -->
<circle cx="120" cy="160" r="18" fill="url(#nodeGradient)"/>
<circle cx="120" cy="160" r="22" stroke="url(#nodeGradient)" stroke-width="1" fill="none"/>
</g>
<!-- Pulse Animation -->
<style>
@keyframes pulse {
0% { transform: scale(1); opacity: 0.8; }
50% { transform: scale(1.1); opacity: 1; }
100% { transform: scale(1); opacity: 0.8; }
}
.nodes circle {
animation: pulse 3s infinite ease-in-out;
}
.nodes circle:nth-child(2n) {
animation-delay: 1s;
}
.nodes circle:nth-child(3n) {
animation-delay: 2s;
}
</style>
</svg>
<!-- Open Notebook — tri-hue pebble mark ("Quiet Green"): fern acts, gold notes, teal speaks -->
<rect x="24" y="90" width="56" height="70" rx="16" fill="#2E6B4F"/>
<rect x="92" y="90" width="56" height="70" rx="16" fill="#A97B12"/>
<rect x="160" y="90" width="56" height="70" rx="16" fill="#0E7268"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 460 B

Before After
Before After

View file

@ -12,6 +12,7 @@ Normative rules for working on the Next.js frontend. Architecture and flow walkt
## Hard rules
- **i18n is mandatory**: every UI string goes through `t('section.key')` and the key must exist in **all locales** under `src/lib/locales/` (currently 14; en-US is the reference). Missing keys fall back to en-US silently — keep locales in sync. Each non-en-US locale ends with `satisfies TranslationShape` (type derived from en-US), so a missing/extra key fails `tsc`; the parity test (`src/lib/locales/index.test.ts`) checks the same at runtime.
- **Styling goes through design tokens** ([ADR-011](../docs/7-DEVELOPMENT/decisions/ADR-011-design-token-system.md)): use semantic utilities (`text-destructive`, `bg-warn-tint`, `text-teal`, `text-type-video`, …) defined in `src/app/globals.css` — never raw Tailwind palette classes (`text-red-600`, `bg-amber-50`) and never hand-written `dark:` color twins (tokens re-resolve per theme). The laws: fern acts, teal is the AI voice, red only destroys, warn is clay, color never washes a reading surface. `/dev/design` (dev-only route) renders every token and primitive in both themes — it's the visual reference, and screen-level PRs must not change it.
- All requests go through `apiClient` (`src/lib/api/client.ts`); never create a second axios instance. Auth token is auto-added from localStorage key `auth-storage`.
- Data fetching uses TanStack Query hooks in `src/lib/hooks/` with `QUERY_KEYS`; mutations invalidate caches and show toasts (sonner). Follow the existing hook shape.
- FormData requests: nested objects/arrays must be `JSON.stringify`-ed before appending; the interceptor strips Content-Type so the browser sets the multipart boundary — don't re-add it.