docs(design): handoff package for implementing the design
Jason accepted the draft 1 direction on 2026-10-10; the refined Relay mark stays as a placeholder. Adds IMPLEMENTING.md (gap map against row 40 and the bus/CLI modules, strings, boundaries, acceptance, proposed rows for Sage), tokens.json and generated tokens.css using the webui's existing names, icons.svg, and tools/build-tokens.mjs and tools/check-design.mjs. Coverage statuses corrected against the code (digest implemented; mosaic stop is row 41; priority and agent state not in the bus reads). No packages/ change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -3993,3 +3993,42 @@ The manifest checked 3/3 in the canonical tree. The candidate landed as f7c2d5cf
|
||||
Correction to my first landing commit message: it named a07b3784 as the gate base. The worktree was created at 03:50:07 local, before a07b3784 (03:50:10) and after 560373c3 (03:50:00), so the base was 560373c3. I amended the unpushed commit before pushing; a07b3784 adds only Filbert's SESSIONS line.
|
||||
|
||||
The recall check's content stayed out of scope. Dewey's two follow-ups (the name check reads the newest run record, and "no agent identity on headless run" passes when `system-prompt.md` is absent) and Darkwing's note 2 (the guards ask `docker info`, but the runs use `docker compose run`) go to #1541.
|
||||
|
||||
### 2026-10-10 — Claude Code (direct session for Jason), design handoff package
|
||||
|
||||
Before: draft 1 (1e11100b) had a preview, a spec and the mark, but nothing an implementing agent could take values from. Jason accepted the direction on 2026-10-10 and kept the refined Relay as a placeholder mark ("Use the design's logo for now and we will revise").
|
||||
|
||||
After: `docs/design/` adds:
|
||||
- `IMPLEMENTING.md`: the brief, with a gap map per view against what row 40 shipped and the bus, CLI and business modules expose, plus the strings to keep, boundaries, acceptance and five proposed rows for Sage;
|
||||
- `tokens.json`, and `tokens.css` generated by `tools/build-tokens.mjs`. It uses the webui's existing names (`data-mode`, `--onAction`, `--font`, `--r-lg`, `--pad`, `--gap`), so it can replace the inline `setProperty` theming without renaming any `var()` use;
|
||||
- `icons.svg`, with the mark and 17 icons;
|
||||
- `tools/check-design.mjs`, the browser checks.
|
||||
|
||||
Nothing under `packages/` changed. No queue row was moved.
|
||||
|
||||
Evidence:
|
||||
- `tokens.json` colours equal `packages/webui/src/public/brand.js` `tokens()` for 10 palettes × 3 modes × 14 keys: 0 mismatches;
|
||||
- `build-tokens.mjs --check`: current;
|
||||
- `check-design.mjs` on the preview passes all 4 checks:
|
||||
- no script errors across 4 tabs, 13 views × 5 states and 13 terminal screens;
|
||||
- no sideways scroll at 400 px;
|
||||
- terminal screens at exactly 120 columns (80 for compact);
|
||||
- preview colours equal `tokens.json` for 10 palettes × 4 modes;
|
||||
- each check failed on a planted defect: a thrown error in one view and state, a 121-column frame, a 900 px element, and an altered `--action`.
|
||||
|
||||
My first two plants did nothing. A duplicate object key overrode the thrown error, and `frame()` re-wraps long lines. I re-planted both.
|
||||
|
||||
Coverage corrections, checked against the code. The table now lists 41 features: 20 implemented, 3 in progress, 1 planned, 17 proposed.
|
||||
- The 08:00 digest is implemented (`packages/cli/src/notifier.mjs`), not planned.
|
||||
- `mosaic stop` is row 41 work.
|
||||
- Task priority and requirement, and agent state per role, are not in the bus reads, so they are now separate proposed rows.
|
||||
|
||||
Correction within this session: I first marked the trail filter chips as not shipped, from a grep cut short by `head`. Row 40 did ship them (`bus.js:313`), so they stay implemented.
|
||||
|
||||
Open for Jason:
|
||||
- the default palette and mode;
|
||||
- the final mark;
|
||||
- no-argument `mosaic` opening the full-screen app;
|
||||
- decision Seen from the web, which would give the browser a human-authority write.
|
||||
|
||||
The private artifact was republished as version 2. No push.
|
||||
|
||||
@@ -592,3 +592,4 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-10T08:49:19Z | darkwing | row 48 (#1534) round 1 review | approve, #1534 comment 27099, queue rev 320 (85fac901), packet 1355592f; candidate 285a4ec1 on 5e87c41f, gate green (queue 148/0, all test-*.sh 0 failed, test-task 26/0 no-Docker with the recall-pair skip, test-release 14/0 with Docker); fake docker info with daemon unreachable: recall pair fails both, 67/31 rc 1; stderr delayed 0.5 s: exit wait 6/2, close wait 8/0, candidate 10 runs 8/0; no real-model run; notes 1-3 non-blocking; no push
|
||||
2026-10-10T08:50:10Z | filbert | row 48 (#1534) round 1 review | approve, #1534 comment 27100 (read back, matches), queue rev 321 (560373c3); candidate 285a4ec1 on 5e87c41f, gate green (queue 148/0, all test-*.sh 0 failed, test-task 26/0 no-Docker with the recall-pair skip; with-Docker 98/0 and fail-mutant 96/2 taken from Dewey's outputs); mutants: exit+late-stderr 6/2, close+late 8/0, guard invert/none 26/2, skip-as-fail 26/1, skip-as-pass 27/0 survives (N1); notes N1-N3 non-blocking; no push
|
||||
2026-10-10T08:56:08Z | sage | row 48 (#1534) landing | done; gate green on 560373c3 plus candidate 285a4ec1 (queue 148/0, conversation 182/0, webui 22/0, control-board 124/0, all test-*.sh 0 failed; test-task 98/0 with Docker, 26/0 without with the recall-pair skip); code f7c2d5cf verify-commit 3/3 (message amended before push: gate base 560373c3, not a07b3784); BUILD-LOG 99d9cff9; rev 322; follow-up #1541 (recall record, vacuous identity check, docker info vs compose); #1534 closed
|
||||
2026-10-10T16:21:55Z | Claude Code (direct session for Jason) | Mosaic Stack design handoff package | done; docs/design/ IMPLEMENTING.md (gap map, strings, boundaries, acceptance, 5 proposed rows for Sage), tokens.json/tokens.css (webui names, 0 colour mismatches with brand.js), icons.svg, tools/build-tokens.mjs and tools/check-design.mjs (4/4 pass, each shown to fail on a planted defect); coverage corrected to 41 features; mark stays a placeholder per Jason; no packages/ change, no queue move, no push
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
# Implementing the Mosaic Stack design
|
||||
|
||||
This brief is for the agents who build the design. Read it with
|
||||
`README.md` (the spec) and the preview `mosaic-stack-design.html`.
|
||||
|
||||
## Status
|
||||
|
||||
- **2026-10-10:** Jason accepted the direction of draft 1. The mark
|
||||
stays the refined Relay as a placeholder until he revises it. Do not
|
||||
spend work on logo variants.
|
||||
- **Provisional defaults**, carried in the files and reversible:
|
||||
- Harbor palette, following the system light or dark setting;
|
||||
- the web shows decisions and copies `mosaic decide`; it never
|
||||
resolves them (REQ-DEC-3);
|
||||
- `mosaic` with no arguments opens the full-screen app. Today it
|
||||
exits 4 (usage).
|
||||
- **Not authorized by this brief:** queue rows, dispatch, push, or
|
||||
any change to a security boundary. Sage owns the queue. Each
|
||||
implementation row needs its own brief and review.
|
||||
|
||||
## Sources of truth
|
||||
|
||||
When two sources disagree, the higher one wins:
|
||||
|
||||
1. The PRD (`docs/prd/mosaic-stack.md`) and the repository invariants
|
||||
(`AGENTS.md`).
|
||||
2. Shipped behaviour and strings in `packages/`. The design quotes
|
||||
them, and the code is the reference.
|
||||
3. This brief.
|
||||
4. `tokens.json`, and the generated `tokens.css`.
|
||||
5. The preview. It uses sample data and its own internal names. Copy
|
||||
layout and states from it, not data, ids or code.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Use |
|
||||
|---|---|
|
||||
| `tokens.json` | Every design value: 10 palettes × light, dim and dark (14 colours each), type scale, radii, spacing, layout widths, terminal colours, status vocabulary |
|
||||
| `tokens.css` | Generated from `tokens.json`. Do not edit by hand |
|
||||
| `icons.svg` | Sprite: `mark` (placeholder) and 17 line icons `i-board` … `i-flag` |
|
||||
| `brand/relay-refined.svg` | The placeholder mark as a standalone file |
|
||||
| `tools/build-tokens.mjs` | Regenerates `tokens.css`; `--check` exits 1 when it is stale |
|
||||
| `tools/check-design.mjs` | Browser checks for the preview (see Acceptance) |
|
||||
|
||||
## Tokens
|
||||
|
||||
- **Names match the shipped console.** `tokens.css` uses the property
|
||||
names `packages/webui` already uses: `--canvas` … `--danger`,
|
||||
`--onAction` (camelCase, as `brand.js` sets it), `--font`, `--mono`,
|
||||
`--r`, `--r-lg`, `--pad`, `--gap`. It adds two names:
|
||||
- `--r-table` (8 px);
|
||||
- `--strong` (a `color-mix` of `--text`).
|
||||
- **Selectors.** No attribute means Harbor, light or dark from
|
||||
`prefers-color-scheme`. `data-mode="light|dim|dark"` sets the mode.
|
||||
`data-palette="<id>"` picks any other palette. The webui already sets
|
||||
both attributes on `<html>`.
|
||||
- **Values are the shipped values.** For all 10 × 3 × 14 colours,
|
||||
`tokens.json` equals what `packages/webui/src/public/brand.js`
|
||||
`tokens()` computes. Checked 2026-10-10: 0 mismatches.
|
||||
- **Adopting it in the webui:**
|
||||
- serve `tokens.css` (add it to the `files` map in
|
||||
`packages/webui/src/serve.mjs`);
|
||||
- stop the inline `setProperty` loop in `app.js` `theme()`, which
|
||||
would override the stylesheet;
|
||||
- keep setting `data-palette` and `data-mode`;
|
||||
- add a System choice that removes `data-mode`;
|
||||
- keep the localStorage key `mosaic-console-appearance`.
|
||||
- **Changes the design makes to existing values:**
|
||||
- `--r` goes from 8 to 6 px;
|
||||
- `--r-lg` goes from 14 to 10 px;
|
||||
- `--gap` stays 12 px and `--pad` stays 16 px;
|
||||
- `--shadow` stays owned by `app.css`. Use it only for overlays: the
|
||||
inspector over the table, the command palette and the toast.
|
||||
- **Fonts.** The console's CSP is `font-src 'self'`, so every font is
|
||||
self-hosted. Manrope 400 to 700 is already served. Add JetBrains Mono
|
||||
400, 500 and 700. Manrope 800 is optional: titles may use 700.
|
||||
- **Type scale:**
|
||||
- page title 28/800;
|
||||
- section 20/800;
|
||||
- question 16/700;
|
||||
- text 14/400 at 1.5;
|
||||
- table cells 13;
|
||||
- labels 11/700 in capitals with 0.06em tracking;
|
||||
- mono 12.5.
|
||||
|
||||
Use `font-variant-numeric: tabular-nums` in columns.
|
||||
- **Regenerate** after editing `tokens.json`:
|
||||
`node docs/design/tools/build-tokens.mjs`. Copy the result to the
|
||||
webui; the design directory stays the source.
|
||||
|
||||
## Icons and the mark
|
||||
|
||||
- **Format.** 24-unit grid, `stroke="currentColor"`, width 1.75, round
|
||||
caps and joins, no fills. The webui inlines icons through
|
||||
`Brand.icon(id)` with the same stroke rules. Replace those paths with
|
||||
the sprite's paths so there is one icon set, or serve `icons.svg` and
|
||||
use `<svg><use href="/icons.svg#i-inbox"/></svg>`. CSP
|
||||
`default-src 'self'` allows a same-origin sprite.
|
||||
- **Labels.** An icon never carries state alone. Every icon in
|
||||
navigation sits next to its text label.
|
||||
- **The mark.** The `mark` symbol uses `fill="currentColor"` and
|
||||
`viewBox="0 0 16 17"`. Draw it at 16, 24 or 32 px. It is a
|
||||
placeholder: keep it in one component so a later swap touches one
|
||||
file.
|
||||
|
||||
## Components
|
||||
|
||||
Every component needs the listed states, keyboard behaviour and
|
||||
focus ring (`--focus`, 2 px, offset 2 px).
|
||||
|
||||
| Component | States and rules |
|
||||
|---|---|
|
||||
| Command bar | Business name, freshness line ("Vikunja last read 14:21:07 (12 s ago)"), Ctrl+K button. Warning colour plus words when stale |
|
||||
| Section list | Icon + label + count; `aria-current="page"` on the active view. Below 760 px it becomes a horizontal strip that scrolls inside itself |
|
||||
| Dense table | 13 px cells, padding 7/10, header labels in caps. Row hover, selected row, ↑/↓ moves, Enter opens the inspector. Wide tables scroll inside their own container |
|
||||
| Inspector | Right drawer, 300 to 370 px. Over the table below 1100 px; a full-screen sheet below 760 px. Esc closes and returns focus to the row |
|
||||
| Status mark | Glyph + word, never colour alone: ● working, ◐ waiting, ▲ error, ○ offline, – idle, ? unknown |
|
||||
| Class chip | routine, within-role, cross-role, gated; BLOCKING is a separate chip |
|
||||
| Copy command | Mono command + Copy button. Uses the shipped strings (below). Never runs anything |
|
||||
| Toast | One line, polite live region, auto-dismiss; errors stay until dismissed |
|
||||
| Command palette | Ctrl+K opens; type to filter views, decisions and tasks; Enter goes; Esc closes and restores focus |
|
||||
| View states | Five per view: normal; loading (skeleton rows, `aria-busy`); empty (says what would appear and where it comes from); error (banner, last good data stays on screen with its read time); refusal (fail closed: the refusal code in mono and the file or rule that is missing) |
|
||||
| Not found | Names the bad route, links to the Board, mentions Ctrl+K |
|
||||
|
||||
## Views and the gap map
|
||||
|
||||
Existing routes in `packages/webui` are hash routes: `#/` (the board),
|
||||
`#/inbox[/<id>]`, `#/tasks[/<ref>]`, `#/agents`,
|
||||
`#/trail/task/<ref>`, `#/trail/decision/<id>` (with `?kind=`). New
|
||||
views follow the same form: `#/talk`, `#/queue`, `#/runs`,
|
||||
`#/ledger`, `#/business`, `#/library`, `#/settings`, `#/setup`.
|
||||
|
||||
Kinds of work:
|
||||
- **restyle:** built; apply tokens, layout and states;
|
||||
- **extend:** built; add design pieces whose data the current read
|
||||
already returns;
|
||||
- **new view:** the data exists in a module; add a read-only endpoint
|
||||
to the webui server and a view;
|
||||
- **backend:** the data is not produced or exposed yet;
|
||||
- **blocked:** waits on another row or on Jason.
|
||||
|
||||
| View | Data today | Kind | Notes |
|
||||
|---|---|---|---|
|
||||
| Board `#/` | `GET /api/board`, `POST /api/seen` (control board) | restyle | Seat table with Input needed and Seen/Unsee already works |
|
||||
| Inbox `#/inbox` | `GET /api/bus/inbox`: open decisions routed to `human` | restyle | Detail, authorization sentence, copy commands and history already work |
|
||||
| Inbox tabs: Routed to roles, Recent | none: `inbox` returns open decisions for the caller's role only | backend | Needs a reader op for decisions routed to other roles and for closed ones |
|
||||
| Decision Seen | broker `decision.seen` needs a human session (`broker.mjs:614-633`) | blocked | The web holds a reader session. Writing Seen from the browser gives it human authority, a security-boundary change for Jason. The web may show existing `seen` events (bus.js already reads them). The TUI may write Seen once a CLI verb exists |
|
||||
| Tasks `#/tasks` | `GET /api/bus/tasks`: title, bucket, done, markers, freshness | restyle | |
|
||||
| Task priority and requirement columns | not in `task_current.fields`; requirement is in the `task.created` event body | backend | |
|
||||
| Field writers in the task inspector | prose in `docs/plans/2026-10-04_slice-1.md:62-72` | backend | Encode the table as data first |
|
||||
| Agents `#/agents` | `GET /api/bus/agents`: latest claim per role | restyle | |
|
||||
| Agent state and Input needed per role | the control board has state per seat; the bus has none per role | backend | Needs a seat-to-role mapping, or state on the bus |
|
||||
| Launch budget (Opus n of 4, Sonnet n of 4) | ceiling only (`vocabulary.mjs:85-86`); `session.launched` has no model family | blocked | Row 41 (S6) |
|
||||
| Stop launches (`mosaic stop`) | broker `launch.revoke` / `launch.restore`, no CLI verb | blocked | Row 41 (S6). Web shows the command only |
|
||||
| Credentials (service, account, role, expiry) | `Credentials.status()` in `packages/bus/src/credentials.mjs:118`; not on the broker | backend | Metadata only. Never a value, token or path |
|
||||
| Trail `#/trail/...` | `GET /api/bus/trail?subject=`; filter chips shipped (`bus.js:313`) | restyle | |
|
||||
| Talk `#/talk` | the conversation History view exists; the live PM session is row 41 | blocked | Row 41 (S6). Reuse the controller/observer rules of `packages/conversation` |
|
||||
| Queue `#/queue` | `packages/queue/src/store.mjs` `list`/`show`; no endpoint | new view | Read-only. Rows move only through `queue move` |
|
||||
| Ledger `#/ledger` | `packages/ledger` `summarize`, `queueChecks` | new view | Read-only |
|
||||
| Business `#/business` | `packages/business` `loadBusiness`, `loadRole`, `resolveInstance`, `vocabulary.mjs` | new view | Read-only. A missing or invalid file shows the refusal state. Credential refs as metadata only |
|
||||
| Runs and releases `#/runs` | readers live inside `scripts/mosaic-task.mjs` and `scripts/release.sh`, not exported | backend | Extract a read-only module first |
|
||||
| Library `#/library` | no meta-harness in committed code | blocked | Row 41 (S6) creates `packages/harness` |
|
||||
| Settings `#/settings` | appearance exists in the webui header; notifier config `readNotifyConfig` | extend | Appearance, notifications (binding names only), about (loopback, release). Read-only apart from appearance |
|
||||
| Set up `#/setup` | validation lives in `scripts/mosaic-config.mjs` (a script) | backend | First-run checklist; the installer is out of v1 scope |
|
||||
| Command palette, five states, not found | client-side | extend | |
|
||||
|
||||
The Board is the console home. The control board's own page on port
|
||||
7331 stays until the console's Board covers it.
|
||||
|
||||
## The full-screen terminal app
|
||||
|
||||
- **Where.** `packages/cli`. Node built-ins only; the CLI has no
|
||||
dependencies and must keep none.
|
||||
- **Talk.** Embed the conversation `Terminal` class
|
||||
(`packages/conversation/src/terminal.mjs:54`,
|
||||
`new Terminal({client, write, rows, onQuit})`). Its `main()` owns the
|
||||
process, so do not call it. The app owns the screen and draws the
|
||||
conversation inside the Talk pane.
|
||||
- **Size.** Read `process.stdout.columns` and `rows`; redraw on
|
||||
`resize`. Designed at 120×36; every screen must fit 80×24. Below
|
||||
100 columns:
|
||||
- the nav shows keys only;
|
||||
- Inbox drops RAISED BY and REACHES YOU;
|
||||
- detail moves below the list.
|
||||
- **Colour.**
|
||||
- Use the terminal's own 16 colours (`tokens.json` `terminal.ansi`),
|
||||
never 256-colour or RGB.
|
||||
- Honour `NO_COLOR` (any non-empty value): no colour codes; reverse
|
||||
video stays for the selected row, and every state still reads
|
||||
from its word and mark.
|
||||
- Not a TTY: refuse the full-screen app and point at the verbs.
|
||||
- **Refusal.** In an agent run, exit 3 with the transport's message
|
||||
(`packages/cli/src/transport.mjs:30`). That is the same check the
|
||||
verbs use; do not write a second one.
|
||||
- **Deciding.** In Inbox, a digit picks an option. Then print the
|
||||
CLI's lines and resolve only on `y`. Reuse the `decide` code path
|
||||
rather than a copy of it. "Outcome unknown" offers no retry key.
|
||||
- **Keys:**
|
||||
- `c` talk, `i` inbox, `t` tasks, `a` agents, `r` trail;
|
||||
- ↑↓ or j/k move, Enter opens, `v` opens the trail of the selection;
|
||||
- `:` command line, `?` help, `q` quit;
|
||||
- Tab moves between the nav and the composer;
|
||||
- the composer keeps the conversation keys.
|
||||
- **No-argument behaviour** is a CLI change from exit 4 to opening the
|
||||
app. It is provisional; land it behind its own review.
|
||||
|
||||
## Strings to keep exactly
|
||||
|
||||
These are shipped. Reuse them; do not reword.
|
||||
|
||||
- **Decide:**
|
||||
- `your choice: <key> (<text>); this authorizes the action`, or
|
||||
`…; this declines the action`;
|
||||
- `resolve <id8> with <key>? [y/N] `;
|
||||
- `resolved <full id>: <key>`;
|
||||
- `outcome unknown: the broker may have recorded it. Check mosaic inbox or mosaic trail <id> before trying again`.
|
||||
- **Refusal:** `refused: this runs only from a human shell, outside any agent run (found <VARS> in the environment)`.
|
||||
- **Web copy:**
|
||||
- `Copied: <command>`;
|
||||
- `The clipboard is unavailable. The command is selected; press Ctrl+C to copy it.`;
|
||||
- `Decisions are answered in a terminal with mosaic decide; Console only shows them.`
|
||||
- **CLI list formats** (`packages/cli/src/format.mjs`):
|
||||
- inbox `<id8> <action> (<class>[, blocking]) raised <at> by <role>`;
|
||||
- tasks `<ref> <state> <title>`;
|
||||
- agents `<role> <holder_run> <harness|-> since <at>`;
|
||||
- trail `<at> <table>#<seq> <summary>`.
|
||||
- **Trail sentences, empty states and not-found lines** are in
|
||||
`packages/webui/src/public/bus.js` (for example
|
||||
`No task <ref> in this business.`). The "feat" labels there mark
|
||||
the gaps this design fills: poller status, bucket names, role
|
||||
instances and launch limits.
|
||||
- **Bus wording stays in mono:** event kinds (`task.changed.external`),
|
||||
refusal codes (`decision-closed`, `launch-revoked`), task refs and
|
||||
ids.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- **No Resolve button on the web.** Copy the `mosaic decide` command
|
||||
only (REQ-DEC-3).
|
||||
- **Seen is not resolution.** Board Seen/Unsee exists. Decision Seen
|
||||
from the web is blocked (see the gap map).
|
||||
- **Read-only web**, apart from Board Seen/Unsee, the board's
|
||||
existing reply path, and appearance settings stored in the browser.
|
||||
Queue rows move only through `queue move`; the business file is
|
||||
written by the human only.
|
||||
- **Secrets.** Show service, account, role and expiry. Never show
|
||||
values, tokens or file paths. Never log them.
|
||||
- **Server.**
|
||||
- Loopback only, no sign-in;
|
||||
- keep the existing CSP, Host and Origin checks;
|
||||
- new endpoints are `GET` only and return JSON;
|
||||
- no new dependencies in `packages/webui` or `packages/cli`.
|
||||
|
||||
## Acceptance
|
||||
|
||||
For every implementation row:
|
||||
- the package's own suite passes: `node --test tests/` in
|
||||
`packages/webui` or `packages/cli`, plus the repository suites the row
|
||||
touches;
|
||||
- `node docs/design/tools/build-tokens.mjs --check` passes when tokens
|
||||
changed;
|
||||
- every changed view shows its five states. Seeded tests cover the
|
||||
empty, error-with-stale-data and refusal states;
|
||||
- browser tests (`packages/webui/tests/browser.mjs` style):
|
||||
- no script errors;
|
||||
- no sideways page scroll at 400 px;
|
||||
- keyboard path: ↑/↓, Enter, Esc returning focus to the row;
|
||||
- focus is visible;
|
||||
- terminal app tests:
|
||||
- every screen fits 120 columns and 80×24;
|
||||
- `NO_COLOR` output has no escape codes;
|
||||
- an agent-run environment exits 3;
|
||||
- strings in "Strings to keep exactly" are unchanged (a grep test is
|
||||
enough).
|
||||
|
||||
`node docs/design/tools/check-design.mjs` checks the preview itself:
|
||||
- no script errors across 4 tabs, 13 views × 5 states and 13 terminal
|
||||
screens;
|
||||
- no sideways scroll at 400 px;
|
||||
- terminal widths;
|
||||
- palette colours against `tokens.json`.
|
||||
|
||||
It needs a local Chromium (`$CHROMIUM` or `/usr/bin/chromium`). Run it
|
||||
after editing the preview.
|
||||
|
||||
## Proposed rows (for Sage)
|
||||
|
||||
Draft order. These go after rows 41 and 42. Sage decides whether to
|
||||
queue them, and in what order.
|
||||
|
||||
1. **Console tokens and shell restyle** (frontend; Dewey's area).
|
||||
- Scope:
|
||||
- adopt `tokens.css`;
|
||||
- command bar, section list, dense table, inspector, toast,
|
||||
command palette, not found;
|
||||
- five states on Board, Inbox, Tasks, Agents and Trail.
|
||||
- No new data.
|
||||
2. **Read-only views with existing data** (frontend plus webui
|
||||
server). Queue, Ledger, Business, Settings: `GET` endpoints over the
|
||||
existing modules.
|
||||
3. **Full-screen `mosaic` app** (CLI): Inbox, Tasks, Agents, Trail,
|
||||
Help, decide, NO_COLOR and 80×24. Talk joins after row 41.
|
||||
- Needs Jason's confirmation of no-argument `mosaic`.
|
||||
4. **Backend reads for the design** (bus):
|
||||
- decisions routed to roles and recent decisions;
|
||||
- task priority and requirement;
|
||||
- agent state per role;
|
||||
- credential status on the broker;
|
||||
- the field-writer table as data;
|
||||
- a runs and release reader module.
|
||||
|
||||
Each is a separate small row.
|
||||
5. **After row 41:** Talk view, launch budget, `mosaic stop` in both
|
||||
surfaces, Library.
|
||||
|
||||
## Open decisions for Jason
|
||||
|
||||
1. Default palette and mode (provisional: Harbor, following the system
|
||||
setting).
|
||||
2. The final mark (placeholder: refined Relay).
|
||||
3. No-argument `mosaic` opens the full-screen app (provisional).
|
||||
4. Decision Seen from the web. It needs a human-authority write path
|
||||
from the browser. The recommendation is to keep it in the terminal
|
||||
for v1.
|
||||
+39
-20
@@ -1,7 +1,10 @@
|
||||
# Mosaic Stack design, draft 1
|
||||
|
||||
- Status: draft 1, 2026-10-09. A design for review, not built software.
|
||||
Nothing under `packages/` changed for it.
|
||||
- Status: draft 1, 2026-10-09. A design, not built software. Nothing
|
||||
under `packages/` changed for it.
|
||||
- 2026-10-10: Jason accepted the direction. The mark stays the refined
|
||||
Relay as a placeholder until he revises it. The defaults below are
|
||||
provisional. Implementers start from `IMPLEMENTING.md`.
|
||||
- Asked for by Jason: branding, WebUI page designs, a TUI design, and
|
||||
every feature included.
|
||||
- Preview: `mosaic-stack-design.html` in this directory. It is one
|
||||
@@ -162,37 +165,53 @@ below).
|
||||
|
||||
## Coverage
|
||||
|
||||
The Coverage tab lists 39 features. Each row gives the requirement, the
|
||||
The Coverage tab lists 41 features. Each row gives the requirement, the
|
||||
web screen, the TUI screen and a status. The status describes the code
|
||||
today, not these screens:
|
||||
- **implemented**: exists in the code now (18 features);
|
||||
- **implemented**: exists in the code now (20 features);
|
||||
- **in progress**: has an open queue row (3);
|
||||
- **planned**: in the PRD or a brief, with no code yet (3);
|
||||
- **proposed**: new in this design (15).
|
||||
- **planned**: in the PRD or a brief, with no code yet (1);
|
||||
- **proposed**: new in this design (17).
|
||||
|
||||
- **In progress.** Talk to the stack-owned PM session (row 41), the
|
||||
launch budget (row 41), and the trail filter chips (row 40).
|
||||
- **Planned.** The 08:00 digest, `mosaic stop`, and the business file
|
||||
view.
|
||||
- **In progress.** All row 41 (S6): talk to the stack-owned PM
|
||||
session, the launch budget, and `mosaic stop`.
|
||||
- **Planned.** The business file view.
|
||||
- **Proposed** (the larger items). The Inbox tabs, Seen for decisions,
|
||||
the field-writer table on tasks, credential expiry, the command
|
||||
palette, and the business, library, queue, runs and ledger views.
|
||||
Also first-run setup and the five-state treatment on every view.
|
||||
task priority and requirement columns, agent state per role, the
|
||||
field-writer table on tasks, credential expiry, the command palette,
|
||||
and the business, library, queue, runs and ledger views. Also
|
||||
first-run setup and the five-state treatment on every view.
|
||||
|
||||
Corrections on 2026-10-10, checked against the code:
|
||||
- the 08:00 digest is implemented (`packages/cli/src/notifier.mjs`);
|
||||
- `mosaic stop` is row 41 work;
|
||||
- task priority and agent state are not in the bus reads, so they
|
||||
moved out of the implemented rows.
|
||||
|
||||
`IMPLEMENTING.md` has the gap map per view.
|
||||
|
||||
## Open decisions for Jason
|
||||
|
||||
1. **Default palette and mode.** Proposed: Harbor, following the system
|
||||
setting, with Dim available.
|
||||
2. **Final mark.** Proposed: refined Relay. The original stays usable
|
||||
from 48 px.
|
||||
1. **Default palette and mode.** Provisional: Harbor, following the
|
||||
system setting, with Dim available.
|
||||
2. **Final mark.** The refined Relay is the placeholder (Jason,
|
||||
2026-10-10: "Use the design's logo for now and we will revise.").
|
||||
3. **Resolving from the web.** The PRD rules it out (REQ-DEC-3).
|
||||
Proposed: keep the Copy command through v1.
|
||||
4. **One `mosaic` app.** Proposed: `mosaic` with no arguments opens the
|
||||
full-screen app, and the verbs remain for scripts.
|
||||
Provisional: keep the Copy command through v1.
|
||||
4. **One `mosaic` app.** Provisional: `mosaic` with no arguments opens
|
||||
the full-screen app, and the verbs remain for scripts.
|
||||
5. **Decision Seen from the web.** It would give the browser a
|
||||
human-authority write. Recommended: keep it in the terminal for
|
||||
v1.
|
||||
|
||||
## Files
|
||||
|
||||
- `IMPLEMENTING.md`: the brief for implementing agents.
|
||||
- `mosaic-stack-design.html`: the preview page. Its source is built from
|
||||
a scratch file. Edit the HTML directly for draft 2.
|
||||
- `tokens.json`, `tokens.css`: design values. `tokens.css` is
|
||||
generated by `tools/build-tokens.mjs`.
|
||||
- `icons.svg`: the icon sprite, plus the placeholder mark.
|
||||
- `brand/relay-refined.svg`, `brand/relay-original.svg`: the mark in
|
||||
Harbor action blue.
|
||||
- `tools/check-design.mjs`: browser checks for the preview.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Mosaic Stack icons, draft 1. 24-unit grid, 1.75 stroke, round caps and joins, no fill.
|
||||
Use: <svg class="i" aria-hidden="true"><use href="icons.svg#i-inbox"/></svg> with color set by CSS.
|
||||
"mark" is the placeholder Relay mark (fill, not stroke). -->
|
||||
<symbol id="mark" viewBox="0 0 16 17" fill="currentColor"><path fill-rule="evenodd" d="M1 7 5 3 9 3 12 6 9 9 6 6 4 8 4 9 7 12 5 14 1 10ZM15 10 11 14 7 14 4 11 7 8 10 11 12 9 12 8 9 5 11 3 15 7Z"/></symbol>
|
||||
<symbol id="i-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.5"/></symbol>
|
||||
<symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5 6 5.5h12l2.5 8v5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1z"/><path d="M3.5 13.5h5l1 2.5h5l1-2.5h5"/></symbol>
|
||||
<symbol id="i-talk" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 5h15a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H10l-4.5 3.5V16h-1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/><path d="M8 10.5h8"/></symbol>
|
||||
<symbol id="i-tasks" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 6.5h10M10 12h10M10 17.5h10"/><path d="m3.5 6.5 1.5 1.5 2.5-3"/><path d="m3.5 12 1.5 1.5 2.5-3"/><path d="M4 17.5h3"/></symbol>
|
||||
<symbol id="i-agents" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8.5" r="3"/><path d="M3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5"/><circle cx="17" cy="9.5" r="2.4"/><path d="M15.8 14.6c2.2.2 3.9 1.8 4.7 4.9"/></symbol>
|
||||
<symbol id="i-trail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5.5" r="2"/><circle cx="18" cy="18.5" r="2"/><path d="M8 5.5h7.5a3.25 3.25 0 0 1 0 6.5h-7a3.25 3.25 0 0 0 0 6.5H16"/></symbol>
|
||||
<symbol id="i-queue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6.5h16M4 12h16M4 17.5h9"/><path d="m16.5 15.5 2 2 2-2"/></symbol>
|
||||
<symbol id="i-runs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 20 8v8l-8 4.5L4 16V8z"/><path d="m4 8 8 4.5L20 8M12 12.5v8"/></symbol>
|
||||
<symbol id="i-ledger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20.5V10M10 20.5V4M15 20.5v-8M20 20.5v-5"/></symbol>
|
||||
<symbol id="i-business" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 20.5V8.5l6-4v16M10.5 20.5h9v-9l-9-3"/><path d="M3 20.5h18M14 13.5h2M14 16.5h2M6.5 10.5h2M6.5 13.5h2"/></symbol>
|
||||
<symbol id="i-library" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 4.5h4v15h-4zM10 4.5h4v15h-4z"/><path d="m15.5 5.3 3.4-.9 3.1 14.3-3.4.9z"/></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.5-4.5"/></symbol>
|
||||
<symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="11" height="11" rx="1.5"/><path d="M15.5 8.5v-3a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3"/></symbol>
|
||||
<symbol id="i-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
|
||||
<symbol id="i-term" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="1.5"/><path d="m7.5 9.5 3 2.5-3 2.5M12.5 15h4"/></symbol>
|
||||
<symbol id="i-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 20.5v-16M5.5 4.5h12l-2.5 4 2.5 4h-12"/></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -847,7 +847,7 @@ agents(){
|
||||
return vh("Agents","Role instances, who holds each one, and the launch budget.", `<span class="st implemented">implemented</span>`) + stateBanner() +
|
||||
`<div class="cards"><div class="card"><span class="label">Opus sessions</span><div class="big tnum">2 <span class="note">of 4</span></div><div class="meter" aria-hidden="true"><i class="on"></i><i class="on"></i><i></i><i></i></div></div>
|
||||
<div class="card"><span class="label">Sonnet sessions</span><div class="big tnum">1 <span class="note">of 4</span></div><div class="meter" aria-hidden="true"><i class="on"></i><i></i><i></i><i></i></div></div>
|
||||
<div class="card"><span class="label">Launching</span><div class="big" style="color:var(--success)">allowed</div><div class="note">Stop all launches with <code>mosaic stop</code> <span class="st planned">planned</span></div></div></div>
|
||||
<div class="card"><span class="label">Launching</span><div class="big" style="color:var(--success)">allowed</div><div class="note">Stop all launches with <code>mosaic stop</code> <span class="st progress">in progress</span></div></div></div>
|
||||
<div class="tbl"><table><thead><tr><th>Role</th><th>Holder</th><th>Harness</th><th>State</th><th>Attention</th><th>Held for</th><th>Run</th></tr></thead><tbody>${body(rows,7,"<b>No role is claimed</b>The PM launches role sessions within the business file's limits.")}</tbody></table></div>
|
||||
<div class="section"><h3>Role credentials</h3><p class="note" style="margin-bottom:8px">Metadata only. Values and file paths are never shown; the broker alone reads them.</p>
|
||||
<div class="tbl"><table><thead><tr><th>Service</th><th>Account</th><th>Role</th><th>Status</th></tr></thead><tbody>${body(creds.map(([s,a,r,x,c])=>`<tr><td>${s}</td><td class="mono">${a}</td><td>${r}</td><td>${c?`<span class="chip ${c==="bad"?"gated":"warn"}">${x}</span>`:x}</td></tr>`).join(""),4,"<b>No credentials declared</b>")}</tbody></table></div></div>`;
|
||||
@@ -907,7 +907,7 @@ settings(){
|
||||
<div class="seg" role="group" aria-label="Mode">${["system","light","dim","dark"].map(m=>`<button data-setmode="${m}" aria-pressed="${theme.mode===m}">${m[0].toUpperCase()+m.slice(1)}</button>`).join("")}</div></div>
|
||||
<div class="panel stack"><h3 class="h2" style="font-size:16px">Notifications</h3>
|
||||
<dl class="kv"><dt>Blocking gated</dt><dd>CLI inbox and a Discord DM, right away</dd><dt>Everything else</dt><dd>One digest at 08:00 America/Chicago</dd><dt>Discord</dt><dd>connector #1509 · DM to you only</dd></dl>
|
||||
<p class="note">Set in the business variables. <span class="st implemented">implemented</span> DM · <span class="st planned">planned</span> digest</p>
|
||||
<p class="note">Set in the business variables. <span class="st implemented">implemented</span> DM and digest</p>
|
||||
<h3 class="h2" style="font-size:16px;margin-top:8px">About</h3>
|
||||
<dl class="kv"><dt>Release</dt><dd class="mono">0.0.12</dd><dt>WebUI</dt><dd class="mono">127.0.0.1:7330 · loopback only</dd><dt>Control board</dt><dd class="mono">127.0.0.1:7331</dd><dt>Sign-in</dt><dd>None in v1; localhost only, no SSO</dd><dt>System config</dt><dd class="mono">~/.config/mosaic-dev/config.json</dd></dl></div></div>`;
|
||||
},
|
||||
@@ -1271,7 +1271,7 @@ agents(){
|
||||
" vikunja bot-mosaic-stack-sync sync, read-only {w|expires 2026-10-21 · 12 days}",
|
||||
" vikunja bot-mosaic-stack-coder coder {d|not configured}",
|
||||
"",
|
||||
" {m|Stop every launch with} {b|mosaic stop} {m|(planned). Restoring launches is logged as launch.restored.}"
|
||||
" {m|Stop every launch with} {b|mosaic stop} {m|(row 41). Restoring launches is logged as launch.restored.}"
|
||||
];
|
||||
return frame("Agents", body, [" {m|3 roles held · 1 free · 0 revoked}", " {m|> (Tab to type to the PM)}", HINT_NAV]);
|
||||
},
|
||||
@@ -1408,19 +1408,21 @@ const COV = [
|
||||
["Outcome unknown, no retry","bus","toast + trail","outcome-unknown box","implemented",""],
|
||||
["Refused inside an agent run (exit 3)","REQ-DEC-3","n/a","shell","implemented",""],
|
||||
["Blocking decisions: Discord DM","REQ-DEC-4, #1509","Settings › Notifications","Inbox footer","implemented",""],
|
||||
["Daily digest 08:00 Central","REQ-DEC-4","Board card, Settings","Inbox footer","planned",""],
|
||||
["Mark decision seen","bus decision ops","Inspector button","s in Inbox","proposed","Seen is not resolution"],
|
||||
["Tasks list, buckets, priority, requirement","REQ-TASK-1","Tasks","Tasks (t)","implemented",""],
|
||||
["Daily digest 08:00 Central","REQ-DEC-4","Board card, Settings","Inbox footer","implemented","packages/cli notifier; runs while the bus host runs"],
|
||||
["Mark decision seen","bus decision ops","Inspector button","s in Inbox","proposed","Broker op decision.seen exists (human session); no CLI or web caller. Seen is not resolution"],
|
||||
["Tasks list and buckets","REQ-TASK-1","Tasks","Tasks (t)","implemented",""],
|
||||
["Task priority and requirement columns","REQ-TASK-1","Tasks","Tasks (t)","proposed","Not in the bus tasks read; requirement is in the task.created event"],
|
||||
["Task markers (changed, not created, conflict, missing)","REQ-TASK-1, -2","Tasks + inspector","Tasks","implemented",""],
|
||||
["Freshness: Vikunja last read","REQ-TASK-2","command bar, Tasks","Tasks header","implemented",""],
|
||||
["Field writers per task","REQ-TASK-1","Task inspector","Tasks detail","proposed","From the slice 1 field table"],
|
||||
["Agents: role holders and state","REQ-ROLE-2, REQ-CLI-1","Agents","Agents (a)","implemented",""],
|
||||
["Agents: role holders","REQ-ROLE-2, REQ-CLI-1","Agents","Agents (a)","implemented",""],
|
||||
["Agent state and Input needed per role","REQ-ROLE-2","Agents","Agents (a)","proposed","The control board has state per seat; the bus agents read has none"],
|
||||
["Launch budget: Opus n of 4, Sonnet n of 4","REQ-LAUNCH-1","Agents, Board","Agents","progress","row 41"],
|
||||
["Revoke launches in one word","REQ-LAUNCH-1","Agents card","mosaic stop","planned",""],
|
||||
["Revoke launches in one word","REQ-LAUNCH-1","Agents card","mosaic stop","progress","row 41; broker launch.revoke exists, no CLI verb yet"],
|
||||
["Revoke a stuck holder (gated)","REQ-ROLE-2","Role inspector → Inbox","Agents → Inbox","implemented","role.revoke is always gated"],
|
||||
["Credential metadata and expiry","REQ-CRED-1","Agents, Settings","Agents","proposed","Names and dates only"],
|
||||
["Credential metadata and expiry","REQ-CRED-1","Agents, Settings","Agents","proposed","Names and dates only; Credentials.status() exists but the broker does not expose it"],
|
||||
["Trail of a task or decision","REQ-EVT-1, REQ-CLI-1","Trail","Trail (r)","implemented",""],
|
||||
["Trail filter chips","slice 1 S5","Trail","Trail","progress","row 40"],
|
||||
["Trail filter chips","slice 1 S5","Trail","Trail","implemented","Row 40: All, Requests, Decisions, Launches, Task changes, Review"],
|
||||
["Control board: seat states, Input needed, Seen/Unsee","control-board","Board (home)","—","implemented","Port 7331 today; joins the console"],
|
||||
["Command palette Ctrl+K","D-002","everywhere",": command line","proposed",""],
|
||||
["Business file: roles, arbiters, limits","REQ-ROLE-3, -4","Business","—","planned","Read-only view"],
|
||||
|
||||
@@ -0,0 +1,813 @@
|
||||
/* Generated from tokens.json by tools/build-tokens.mjs. Do not edit by hand. */
|
||||
|
||||
/* Fonts: Manrope and JetBrains Mono. Self-host the woff2 files: the console's CSP blocks external hosts. packages/webui already serves Manrope 400-700; weight 800 and JetBrains Mono 400/500/700 still need adding (or drop 800 and use 700). The design preview loads both from Google Fonts only because it is a standalone page. */
|
||||
|
||||
:root {
|
||||
--font: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
--r: 6px;
|
||||
--r-lg: 10px;
|
||||
--r-table: 8px;
|
||||
--pad: 16px;
|
||||
--gap: 12px;
|
||||
}
|
||||
|
||||
/* Harbor: the default palette */
|
||||
|
||||
:root {
|
||||
--canvas: #f3f5f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6eaef;
|
||||
--line: #ced5df;
|
||||
--border: #636e7e;
|
||||
--text: #434d5b;
|
||||
--muted: #5e6978;
|
||||
--action: #2266bf;
|
||||
--onAction: #ffffff;
|
||||
--accent: #8d5e20;
|
||||
--focus: #2266bf;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12161c;
|
||||
--surface: #1c232b;
|
||||
--raised: #28323e;
|
||||
--line: #384556;
|
||||
--border: #a4acb7;
|
||||
--text: #b9c1cb;
|
||||
--muted: #a4acb7;
|
||||
--action: #74a6e7;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #74a6e7;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-mode="dark"] {
|
||||
--canvas: #12161c;
|
||||
--surface: #1c232b;
|
||||
--raised: #28323e;
|
||||
--line: #384556;
|
||||
--border: #a4acb7;
|
||||
--text: #b9c1cb;
|
||||
--muted: #a4acb7;
|
||||
--action: #74a6e7;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #74a6e7;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-mode="dim"] {
|
||||
--canvas: #242d38;
|
||||
--surface: #2e3947;
|
||||
--raised: #384556;
|
||||
--line: #47576c;
|
||||
--border: #a4acb7;
|
||||
--text: #d6dbe1;
|
||||
--muted: #a9b1bc;
|
||||
--action: #8ab4ea;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #8ab4ea;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Carmine */
|
||||
|
||||
:root[data-palette="carmine"] {
|
||||
--canvas: #f7f3f3;
|
||||
--surface: #ffffff;
|
||||
--raised: #efe7e8;
|
||||
--line: #deced1;
|
||||
--border: #7e6367;
|
||||
--text: #5e4549;
|
||||
--muted: #7b6065;
|
||||
--action: #b92740;
|
||||
--onAction: #ffffff;
|
||||
--accent: #2e706c;
|
||||
--focus: #b92740;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="carmine"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1b1214;
|
||||
--surface: #2b1d1f;
|
||||
--raised: #3d292c;
|
||||
--line: #55393e;
|
||||
--border: #b7a4a7;
|
||||
--text: #c9b6b9;
|
||||
--muted: #b7a4a7;
|
||||
--action: #e2788a;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e2788a;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="carmine"][data-mode="dark"] {
|
||||
--canvas: #1b1214;
|
||||
--surface: #2b1d1f;
|
||||
--raised: #3d292c;
|
||||
--line: #55393e;
|
||||
--border: #b7a4a7;
|
||||
--text: #c9b6b9;
|
||||
--muted: #b7a4a7;
|
||||
--action: #e2788a;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e2788a;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="carmine"][data-mode="dim"] {
|
||||
--canvas: #372528;
|
||||
--surface: #462f33;
|
||||
--raised: #55393e;
|
||||
--line: #6b484e;
|
||||
--border: #b7a4a7;
|
||||
--text: #ded3d5;
|
||||
--muted: #bca9ac;
|
||||
--action: #e996a4;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e996a4;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e99995;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Atlantic */
|
||||
|
||||
:root[data-palette="atlantic"] {
|
||||
--canvas: #f3f6f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6ecef;
|
||||
--line: #cdd9df;
|
||||
--border: #63757e;
|
||||
--text: #3f4e55;
|
||||
--muted: #5c6d75;
|
||||
--action: #19719a;
|
||||
--onAction: #ffffff;
|
||||
--accent: #257467;
|
||||
--focus: #19719a;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="atlantic"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12191c;
|
||||
--surface: #1c272b;
|
||||
--raised: #28373e;
|
||||
--line: #384d57;
|
||||
--border: #a4b1b7;
|
||||
--text: #bcc8cd;
|
||||
--muted: #a4b1b7;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e4817c;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="atlantic"][data-mode="dark"] {
|
||||
--canvas: #12191c;
|
||||
--surface: #1c272b;
|
||||
--raised: #28373e;
|
||||
--line: #384d57;
|
||||
--border: #a4b1b7;
|
||||
--text: #bcc8cd;
|
||||
--muted: #a4b1b7;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e4817c;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="atlantic"][data-mode="dim"] {
|
||||
--canvas: #243238;
|
||||
--surface: #2e3f47;
|
||||
--raised: #384d57;
|
||||
--line: #46606d;
|
||||
--border: #a4b1b7;
|
||||
--text: #e2e7e9;
|
||||
--muted: #afbbc0;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #edaaa6;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Terracotta */
|
||||
|
||||
:root[data-palette="terracotta"] {
|
||||
--canvas: #f7f4f3;
|
||||
--surface: #ffffff;
|
||||
--raised: #eee9e7;
|
||||
--line: #ded3cf;
|
||||
--border: #7e6b63;
|
||||
--text: #584841;
|
||||
--muted: #78665e;
|
||||
--action: #a74f2a;
|
||||
--onAction: #ffffff;
|
||||
--accent: #356f88;
|
||||
--focus: #a74f2a;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="terracotta"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1b1513;
|
||||
--surface: #2a211d;
|
||||
--raised: #3c2f2a;
|
||||
--line: #54423b;
|
||||
--border: #b7a9a4;
|
||||
--text: #cbbeb9;
|
||||
--muted: #b7a9a4;
|
||||
--action: #de9a7c;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #de9a7c;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="terracotta"][data-mode="dark"] {
|
||||
--canvas: #1b1513;
|
||||
--surface: #2a211d;
|
||||
--raised: #3c2f2a;
|
||||
--line: #54423b;
|
||||
--border: #b7a9a4;
|
||||
--text: #cbbeb9;
|
||||
--muted: #b7a9a4;
|
||||
--action: #de9a7c;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #de9a7c;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="terracotta"][data-mode="dim"] {
|
||||
--canvas: #362b26;
|
||||
--surface: #453630;
|
||||
--raised: #54423b;
|
||||
--line: #695349;
|
||||
--border: #b7a9a4;
|
||||
--text: #e5dfdc;
|
||||
--muted: #beb2ac;
|
||||
--action: #e2a68d;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #e2a68d;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eba29e;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Aubergine */
|
||||
|
||||
:root[data-palette="aubergine"] {
|
||||
--canvas: #f5f4f6;
|
||||
--surface: #ffffff;
|
||||
--raised: #ece8ed;
|
||||
--line: #d9d2db;
|
||||
--border: #78637e;
|
||||
--text: #59455e;
|
||||
--muted: #75607b;
|
||||
--action: #88469b;
|
||||
--onAction: #ffffff;
|
||||
--accent: #77672c;
|
||||
--focus: #88469b;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="aubergine"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #18141a;
|
||||
--surface: #262028;
|
||||
--raised: #362d39;
|
||||
--line: #4c3f50;
|
||||
--border: #b3a4b7;
|
||||
--text: #c7b9cb;
|
||||
--muted: #b3a4b7;
|
||||
--action: #bf8ecc;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #bf8ecc;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="aubergine"][data-mode="dark"] {
|
||||
--canvas: #18141a;
|
||||
--surface: #262028;
|
||||
--raised: #362d39;
|
||||
--line: #4c3f50;
|
||||
--border: #b3a4b7;
|
||||
--text: #c7b9cb;
|
||||
--muted: #b3a4b7;
|
||||
--action: #bf8ecc;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #bf8ecc;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="aubergine"][data-mode="dim"] {
|
||||
--canvas: #312933;
|
||||
--surface: #3e3441;
|
||||
--raised: #4c3f50;
|
||||
--line: #5f4f63;
|
||||
--border: #b3a4b7;
|
||||
--text: #e1d9e3;
|
||||
--muted: #baacbe;
|
||||
--action: #cba4d6;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #cba4d6;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Mineral */
|
||||
|
||||
:root[data-palette="mineral"] {
|
||||
--canvas: #f3f6f6;
|
||||
--surface: #ffffff;
|
||||
--raised: #e8eded;
|
||||
--line: #d0dcda;
|
||||
--border: #637e7a;
|
||||
--text: #3d524f;
|
||||
--muted: #586f6c;
|
||||
--action: #2a7469;
|
||||
--onAction: #ffffff;
|
||||
--accent: #9f414f;
|
||||
--focus: #2a7469;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="mineral"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #141a19;
|
||||
--surface: #1f2927;
|
||||
--raised: #2c3a38;
|
||||
--line: #3d514e;
|
||||
--border: #a4b7b4;
|
||||
--text: #b9cbc8;
|
||||
--muted: #a4b7b4;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #d18f99;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="mineral"][data-mode="dark"] {
|
||||
--canvas: #141a19;
|
||||
--surface: #1f2927;
|
||||
--raised: #2c3a38;
|
||||
--line: #3d514e;
|
||||
--border: #a4b7b4;
|
||||
--text: #b9cbc8;
|
||||
--muted: #a4b7b4;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #d18f99;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="mineral"][data-mode="dim"] {
|
||||
--canvas: #273432;
|
||||
--surface: #324340;
|
||||
--raised: #3d514e;
|
||||
--line: #4d6662;
|
||||
--border: #a4b7b4;
|
||||
--text: #e5ebea;
|
||||
--muted: #b2c2c0;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #e0b3ba;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eeaeaa;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Cobalt */
|
||||
|
||||
:root[data-palette="cobalt"] {
|
||||
--canvas: #f3f3f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6e8ef;
|
||||
--line: #cdd1df;
|
||||
--border: #63687e;
|
||||
--text: #454a5e;
|
||||
--muted: #60657b;
|
||||
--action: #1c3bc4;
|
||||
--onAction: #ffffff;
|
||||
--accent: #79671b;
|
||||
--focus: #1c3bc4;
|
||||
--success: #277255;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="cobalt"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12141c;
|
||||
--surface: #1c1f2c;
|
||||
--raised: #282c3e;
|
||||
--line: #383d57;
|
||||
--border: #a4a7b7;
|
||||
--text: #b6b9c9;
|
||||
--muted: #a4a7b7;
|
||||
--action: #798eec;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #798eec;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="cobalt"][data-mode="dark"] {
|
||||
--canvas: #12141c;
|
||||
--surface: #1c1f2c;
|
||||
--raised: #282c3e;
|
||||
--line: #383d57;
|
||||
--border: #a4a7b7;
|
||||
--text: #b6b9c9;
|
||||
--muted: #a4a7b7;
|
||||
--action: #798eec;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #798eec;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="cobalt"][data-mode="dim"] {
|
||||
--canvas: #242838;
|
||||
--surface: #2e3248;
|
||||
--raised: #383d57;
|
||||
--line: #464d6d;
|
||||
--border: #a4a7b7;
|
||||
--text: #d0d2dc;
|
||||
--muted: #a6aab9;
|
||||
--action: #94a5f0;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #94a5f0;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e89591;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Rosewood */
|
||||
|
||||
:root[data-palette="rosewood"] {
|
||||
--canvas: #f6f3f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #ede8eb;
|
||||
--line: #dcd0d6;
|
||||
--border: #7e6371;
|
||||
--text: #5e4552;
|
||||
--muted: #7b606e;
|
||||
--action: #a53b72;
|
||||
--onAction: #ffffff;
|
||||
--accent: #416c95;
|
||||
--focus: #a53b72;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="rosewood"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1a1417;
|
||||
--surface: #291f24;
|
||||
--raised: #3a2c33;
|
||||
--line: #513d48;
|
||||
--border: #b7a4ae;
|
||||
--text: #cbb9c2;
|
||||
--muted: #b7a4ae;
|
||||
--action: #d487af;
|
||||
--onAction: #10141a;
|
||||
--accent: #8eaecd;
|
||||
--focus: #d487af;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="rosewood"][data-mode="dark"] {
|
||||
--canvas: #1a1417;
|
||||
--surface: #291f24;
|
||||
--raised: #3a2c33;
|
||||
--line: #513d48;
|
||||
--border: #b7a4ae;
|
||||
--text: #cbb9c2;
|
||||
--muted: #b7a4ae;
|
||||
--action: #d487af;
|
||||
--onAction: #10141a;
|
||||
--accent: #8eaecd;
|
||||
--focus: #d487af;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="rosewood"][data-mode="dim"] {
|
||||
--canvas: #34272e;
|
||||
--surface: #43323b;
|
||||
--raised: #513d48;
|
||||
--line: #664d5a;
|
||||
--border: #b7a4ae;
|
||||
--text: #e1d6db;
|
||||
--muted: #beacb5;
|
||||
--action: #dc9ebe;
|
||||
--onAction: #10141a;
|
||||
--accent: #95b3d0;
|
||||
--focus: #dc9ebe;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Graphite */
|
||||
|
||||
:root[data-palette="graphite"] {
|
||||
--canvas: #f5f5f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #eaeaeb;
|
||||
--line: #d5d6d7;
|
||||
--border: #636e7e;
|
||||
--text: #434d5b;
|
||||
--muted: #5e6978;
|
||||
--action: #616975;
|
||||
--onAction: #ffffff;
|
||||
--accent: #5e6978;
|
||||
--focus: #616975;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="graphite"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #161718;
|
||||
--surface: #232425;
|
||||
--raised: #323334;
|
||||
--line: #454749;
|
||||
--border: #a4abb7;
|
||||
--text: #bcc3cd;
|
||||
--muted: #a4abb7;
|
||||
--action: #a6acb5;
|
||||
--onAction: #10141a;
|
||||
--accent: #a4abb7;
|
||||
--focus: #a6acb5;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="graphite"][data-mode="dark"] {
|
||||
--canvas: #161718;
|
||||
--surface: #232425;
|
||||
--raised: #323334;
|
||||
--line: #454749;
|
||||
--border: #a4abb7;
|
||||
--text: #bcc3cd;
|
||||
--muted: #a4abb7;
|
||||
--action: #a6acb5;
|
||||
--onAction: #10141a;
|
||||
--accent: #a4abb7;
|
||||
--focus: #a6acb5;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="graphite"][data-mode="dim"] {
|
||||
--canvas: #2d2e2f;
|
||||
--surface: #393a3c;
|
||||
--raised: #454749;
|
||||
--line: #57595c;
|
||||
--border: #a4abb7;
|
||||
--text: #dfe2e7;
|
||||
--muted: #afb6c0;
|
||||
--action: #b1b6be;
|
||||
--onAction: #10141a;
|
||||
--accent: #afb6c0;
|
||||
--focus: #b1b6be;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eba29e;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Grove */
|
||||
|
||||
:root[data-palette="grove"] {
|
||||
--canvas: #f4f6f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #e8edea;
|
||||
--line: #d2dbd6;
|
||||
--border: #637e6e;
|
||||
--text: #3d5246;
|
||||
--muted: #586f62;
|
||||
--action: #377752;
|
||||
--onAction: #ffffff;
|
||||
--accent: #9c444a;
|
||||
--focus: #377752;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="grove"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #141917;
|
||||
--surface: #202823;
|
||||
--raised: #2d3932;
|
||||
--line: #3f4f46;
|
||||
--border: #a4b7ac;
|
||||
--text: #b9cbc1;
|
||||
--muted: #a4b7ac;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #cd8e92;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="grove"][data-mode="dark"] {
|
||||
--canvas: #141917;
|
||||
--surface: #202823;
|
||||
--raised: #2d3932;
|
||||
--line: #3f4f46;
|
||||
--border: #a4b7ac;
|
||||
--text: #b9cbc1;
|
||||
--muted: #a4b7ac;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #cd8e92;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="grove"][data-mode="dim"] {
|
||||
--canvas: #29332d;
|
||||
--surface: #34413a;
|
||||
--raised: #3f4f46;
|
||||
--line: #4f6358;
|
||||
--border: #a4b7ac;
|
||||
--text: #e2e9e5;
|
||||
--muted: #afc0b6;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #ddb1b4;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #edaaa6;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
{
|
||||
"$comment": "Mosaic Stack design tokens, draft 1. Source of truth for web and terminal colour, type and shape. Palette values equal agents/dewey/work/wui/assets/palettes.json. Regenerate tokens.css with: node docs/design/tools/build-tokens.mjs",
|
||||
"version": "draft-1",
|
||||
"default": {
|
||||
"palette": "harbor",
|
||||
"mode": "system",
|
||||
"$comment": "Provisional (Jason 2026-10-10 accepted the direction; the default is still open)."
|
||||
},
|
||||
"modes": [
|
||||
"light",
|
||||
"dim",
|
||||
"dark"
|
||||
],
|
||||
"modeRules": {
|
||||
"system": "light unless prefers-color-scheme is dark, then dark",
|
||||
"dim": "only by explicit choice"
|
||||
},
|
||||
"color": {
|
||||
"semantic": {
|
||||
"canvas": "page background",
|
||||
"surface": "panels, tables, inspector",
|
||||
"raised": "selected nav, hover, chips",
|
||||
"line": "hairlines and table rules",
|
||||
"border": "control borders that must meet 3:1",
|
||||
"text": "body text",
|
||||
"strong": "derived: text mixed 72% with black (light) or 70% with white (dim, dark); headings and emphasis",
|
||||
"muted": "secondary text, timestamps",
|
||||
"action": "links, primary buttons, selection",
|
||||
"onAction": "text on action fills",
|
||||
"accent": "the PM, decisions, the second voice",
|
||||
"focus": "focus rings",
|
||||
"success": "working, pass, fresh",
|
||||
"warning": "waiting, stale, expiring, outcome unknown",
|
||||
"danger": "blocking, refused, error, conflict"
|
||||
}
|
||||
},
|
||||
"palettes": [
|
||||
{
|
||||
"id": "harbor",
|
||||
"name": "Harbor",
|
||||
"theory": "Complementary",
|
||||
"note": "Clear blue with a restrained copper counterpoint. The warm accent balances cool navigation and gives selected details a second voice.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f5f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6eaef",
|
||||
"line": "#ced5df",
|
||||
"border": "#636e7e",
|
||||
"text": "#434d5b",
|
||||
"muted": "#5e6978",
|
||||
"action": "#2266bf",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#8d5e20",
|
||||
"focus": "#2266bf",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#242d38",
|
||||
"surface": "#2e3947",
|
||||
"raised": "#384556",
|
||||
"line": "#47576c",
|
||||
"border": "#a4acb7",
|
||||
"text": "#d6dbe1",
|
||||
"muted": "#a9b1bc",
|
||||
"action": "#8ab4ea",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#8ab4ea",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12161c",
|
||||
"surface": "#1c232b",
|
||||
"raised": "#28323e",
|
||||
"line": "#384556",
|
||||
"border": "#a4acb7",
|
||||
"text": "#b9c1cb",
|
||||
"muted": "#a4acb7",
|
||||
"action": "#74a6e7",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1b47a",
|
||||
"focus": "#74a6e7",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "carmine",
|
||||
"name": "Carmine",
|
||||
"theory": "Near-complementary",
|
||||
"note": "A red-led identity with a cool turquoise counterpoint. Rose-tinted neutrals keep the red from covering every surface.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f7f3f3",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#efe7e8",
|
||||
"line": "#deced1",
|
||||
"border": "#7e6367",
|
||||
"text": "#5e4549",
|
||||
"muted": "#7b6065",
|
||||
"action": "#b92740",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#2e706c",
|
||||
"focus": "#b92740",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#372528",
|
||||
"surface": "#462f33",
|
||||
"raised": "#55393e",
|
||||
"line": "#6b484e",
|
||||
"border": "#b7a4a7",
|
||||
"text": "#ded3d5",
|
||||
"muted": "#bca9ac",
|
||||
"action": "#e996a4",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8bd0cb",
|
||||
"focus": "#e996a4",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e99995"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1b1214",
|
||||
"surface": "#2b1d1f",
|
||||
"raised": "#3d292c",
|
||||
"line": "#55393e",
|
||||
"border": "#b7a4a7",
|
||||
"text": "#c9b6b9",
|
||||
"muted": "#b7a4a7",
|
||||
"action": "#e2788a",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8bd0cb",
|
||||
"focus": "#e2788a",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "atlantic",
|
||||
"name": "Atlantic",
|
||||
"theory": "Analogous",
|
||||
"note": "Ocean blue and teal sit close on the hue wheel. Their shared cool bias makes a quieter combination than an opposing accent.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f6f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6ecef",
|
||||
"line": "#cdd9df",
|
||||
"border": "#63757e",
|
||||
"text": "#3f4e55",
|
||||
"muted": "#5c6d75",
|
||||
"action": "#19719a",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#257467",
|
||||
"focus": "#19719a",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#243238",
|
||||
"surface": "#2e3f47",
|
||||
"raised": "#384d57",
|
||||
"line": "#46606d",
|
||||
"border": "#a4b1b7",
|
||||
"text": "#e2e7e9",
|
||||
"muted": "#afbbc0",
|
||||
"action": "#73c3e8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#83d8ca",
|
||||
"focus": "#73c3e8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#edaaa6"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12191c",
|
||||
"surface": "#1c272b",
|
||||
"raised": "#28373e",
|
||||
"line": "#384d57",
|
||||
"border": "#a4b1b7",
|
||||
"text": "#bcc8cd",
|
||||
"muted": "#a4b1b7",
|
||||
"action": "#73c3e8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#83d8ca",
|
||||
"focus": "#73c3e8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e4817c"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "terracotta",
|
||||
"name": "Terracotta",
|
||||
"theory": "Complementary",
|
||||
"note": "Fired-clay orange meets a desaturated steel blue. Warm neutral surfaces connect the palette without needing a beige wash everywhere.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f7f4f3",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#eee9e7",
|
||||
"line": "#ded3cf",
|
||||
"border": "#7e6b63",
|
||||
"text": "#584841",
|
||||
"muted": "#78665e",
|
||||
"action": "#a74f2a",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#356f88",
|
||||
"focus": "#a74f2a",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#362b26",
|
||||
"surface": "#453630",
|
||||
"raised": "#54423b",
|
||||
"line": "#695349",
|
||||
"border": "#b7a9a4",
|
||||
"text": "#e5dfdc",
|
||||
"muted": "#beb2ac",
|
||||
"action": "#e2a68d",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#89bcd1",
|
||||
"focus": "#e2a68d",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eba29e"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1b1513",
|
||||
"surface": "#2a211d",
|
||||
"raised": "#3c2f2a",
|
||||
"line": "#54423b",
|
||||
"border": "#b7a9a4",
|
||||
"text": "#cbbeb9",
|
||||
"muted": "#b7a9a4",
|
||||
"action": "#de9a7c",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#89bcd1",
|
||||
"focus": "#de9a7c",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "aubergine",
|
||||
"name": "Aubergine",
|
||||
"theory": "Triadic pair",
|
||||
"note": "Muted plum and ochre sit 120 degrees apart on the hue wheel. The third triadic hue, teal, is left out so the interface has two brand accents rather than three.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f5f4f6",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#ece8ed",
|
||||
"line": "#d9d2db",
|
||||
"border": "#78637e",
|
||||
"text": "#59455e",
|
||||
"muted": "#75607b",
|
||||
"action": "#88469b",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#77672c",
|
||||
"focus": "#88469b",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#312933",
|
||||
"surface": "#3e3441",
|
||||
"raised": "#4c3f50",
|
||||
"line": "#5f4f63",
|
||||
"border": "#b3a4b7",
|
||||
"text": "#e1d9e3",
|
||||
"muted": "#baacbe",
|
||||
"action": "#cba4d6",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d3c388",
|
||||
"focus": "#cba4d6",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#18141a",
|
||||
"surface": "#262028",
|
||||
"raised": "#362d39",
|
||||
"line": "#4c3f50",
|
||||
"border": "#b3a4b7",
|
||||
"text": "#c7b9cb",
|
||||
"muted": "#b3a4b7",
|
||||
"action": "#bf8ecc",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d3c388",
|
||||
"focus": "#bf8ecc",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mineral",
|
||||
"name": "Mineral",
|
||||
"theory": "Complementary",
|
||||
"note": "Deep teal and dusty rose oppose each other while keeping saturation restrained. Blue-green tinted layers carry the identity in dark mode.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f6f6",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e8eded",
|
||||
"line": "#d0dcda",
|
||||
"border": "#637e7a",
|
||||
"text": "#3d524f",
|
||||
"muted": "#586f6c",
|
||||
"action": "#2a7469",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#9f414f",
|
||||
"focus": "#2a7469",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#273432",
|
||||
"surface": "#324340",
|
||||
"raised": "#3d514e",
|
||||
"line": "#4d6662",
|
||||
"border": "#a4b7b4",
|
||||
"text": "#e5ebea",
|
||||
"muted": "#b2c2c0",
|
||||
"action": "#87d4c8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e0b3ba",
|
||||
"focus": "#87d4c8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eeaeaa"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#141a19",
|
||||
"surface": "#1f2927",
|
||||
"raised": "#2c3a38",
|
||||
"line": "#3d514e",
|
||||
"border": "#a4b7b4",
|
||||
"text": "#b9cbc8",
|
||||
"muted": "#a4b7b4",
|
||||
"action": "#87d4c8",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#d18f99",
|
||||
"focus": "#87d4c8",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e58580"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cobalt",
|
||||
"name": "Cobalt",
|
||||
"theory": "Complementary",
|
||||
"note": "A stronger royal blue meets a small amber accent. The high hue separation is controlled by using amber only in secondary details.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f3f3f7",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e6e8ef",
|
||||
"line": "#cdd1df",
|
||||
"border": "#63687e",
|
||||
"text": "#454a5e",
|
||||
"muted": "#60657b",
|
||||
"action": "#1c3bc4",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#79671b",
|
||||
"focus": "#1c3bc4",
|
||||
"success": "#277255",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#242838",
|
||||
"surface": "#2e3248",
|
||||
"raised": "#383d57",
|
||||
"line": "#464d6d",
|
||||
"border": "#a4a7b7",
|
||||
"text": "#d0d2dc",
|
||||
"muted": "#a6aab9",
|
||||
"action": "#94a5f0",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1ce7a",
|
||||
"focus": "#94a5f0",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e89591"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#12141c",
|
||||
"surface": "#1c1f2c",
|
||||
"raised": "#282c3e",
|
||||
"line": "#383d57",
|
||||
"border": "#a4a7b7",
|
||||
"text": "#b6b9c9",
|
||||
"muted": "#a4a7b7",
|
||||
"action": "#798eec",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#e1ce7a",
|
||||
"focus": "#798eec",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rosewood",
|
||||
"name": "Rosewood",
|
||||
"theory": "Triadic pair",
|
||||
"note": "Dusty pink and steel blue occupy two points of a triadic relationship. The third, yellow-green, is omitted to avoid an overly colorful interface.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f6f3f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#ede8eb",
|
||||
"line": "#dcd0d6",
|
||||
"border": "#7e6371",
|
||||
"text": "#5e4552",
|
||||
"muted": "#7b606e",
|
||||
"action": "#a53b72",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#416c95",
|
||||
"focus": "#a53b72",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#34272e",
|
||||
"surface": "#43323b",
|
||||
"raised": "#513d48",
|
||||
"line": "#664d5a",
|
||||
"border": "#b7a4ae",
|
||||
"text": "#e1d6db",
|
||||
"muted": "#beacb5",
|
||||
"action": "#dc9ebe",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#95b3d0",
|
||||
"focus": "#dc9ebe",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#ea9d99"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#1a1417",
|
||||
"surface": "#291f24",
|
||||
"raised": "#3a2c33",
|
||||
"line": "#513d48",
|
||||
"border": "#b7a4ae",
|
||||
"text": "#cbb9c2",
|
||||
"muted": "#b7a4ae",
|
||||
"action": "#d487af",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#8eaecd",
|
||||
"focus": "#d487af",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "graphite",
|
||||
"name": "Graphite",
|
||||
"theory": "Monochromatic",
|
||||
"note": "One blue-gray hue uses value and saturation changes instead of a second brand hue. Semantic status colors remain independent.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f5f5f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#eaeaeb",
|
||||
"line": "#d5d6d7",
|
||||
"border": "#636e7e",
|
||||
"text": "#434d5b",
|
||||
"muted": "#5e6978",
|
||||
"action": "#616975",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#5e6978",
|
||||
"focus": "#616975",
|
||||
"success": "#287658",
|
||||
"warning": "#8f5c19",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#2d2e2f",
|
||||
"surface": "#393a3c",
|
||||
"raised": "#454749",
|
||||
"line": "#57595c",
|
||||
"border": "#a4abb7",
|
||||
"text": "#dfe2e7",
|
||||
"muted": "#afb6c0",
|
||||
"action": "#b1b6be",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#afb6c0",
|
||||
"focus": "#b1b6be",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#eba29e"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#161718",
|
||||
"surface": "#232425",
|
||||
"raised": "#323334",
|
||||
"line": "#454749",
|
||||
"border": "#a4abb7",
|
||||
"text": "#bcc3cd",
|
||||
"muted": "#a4abb7",
|
||||
"action": "#a6acb5",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#a4abb7",
|
||||
"focus": "#a6acb5",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e37d78"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "grove",
|
||||
"name": "Grove",
|
||||
"theory": "Split-complementary pair",
|
||||
"note": "Muted forest green meets dusty rose, 30 degrees to one side of its magenta complement. The second split accent is omitted. Low-saturation surfaces keep green from dominating.",
|
||||
"modes": {
|
||||
"light": {
|
||||
"canvas": "#f4f6f5",
|
||||
"surface": "#ffffff",
|
||||
"raised": "#e8edea",
|
||||
"line": "#d2dbd6",
|
||||
"border": "#637e6e",
|
||||
"text": "#3d5246",
|
||||
"muted": "#586f62",
|
||||
"action": "#377752",
|
||||
"onAction": "#ffffff",
|
||||
"accent": "#9c444a",
|
||||
"focus": "#377752",
|
||||
"success": "#287658",
|
||||
"warning": "#935f1a",
|
||||
"danger": "#ba2e26"
|
||||
},
|
||||
"dim": {
|
||||
"canvas": "#29332d",
|
||||
"surface": "#34413a",
|
||||
"raised": "#3f4f46",
|
||||
"line": "#4f6358",
|
||||
"border": "#a4b7ac",
|
||||
"text": "#e2e9e5",
|
||||
"muted": "#afc0b6",
|
||||
"action": "#8fcca9",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#ddb1b4",
|
||||
"focus": "#8fcca9",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#edaaa6"
|
||||
},
|
||||
"dark": {
|
||||
"canvas": "#141917",
|
||||
"surface": "#202823",
|
||||
"raised": "#2d3932",
|
||||
"line": "#3f4f46",
|
||||
"border": "#a4b7ac",
|
||||
"text": "#b9cbc1",
|
||||
"muted": "#a4b7ac",
|
||||
"action": "#8fcca9",
|
||||
"onAction": "#10141a",
|
||||
"accent": "#cd8e92",
|
||||
"focus": "#8fcca9",
|
||||
"success": "#85d5b7",
|
||||
"warning": "#e7b574",
|
||||
"danger": "#e58580"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"font": {
|
||||
"sans": {
|
||||
"family": "Manrope",
|
||||
"stack": "\"Manrope\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
||||
"weights": [
|
||||
400,
|
||||
500,
|
||||
600,
|
||||
700,
|
||||
800
|
||||
]
|
||||
},
|
||||
"mono": {
|
||||
"family": "JetBrains Mono",
|
||||
"stack": "\"JetBrains Mono\", ui-monospace, \"SFMono-Regular\", Menlo, Consolas, monospace",
|
||||
"weights": [
|
||||
400,
|
||||
500,
|
||||
700
|
||||
],
|
||||
"use": "ids, refs, actions, refusal codes, commands: anything a person might type or paste"
|
||||
},
|
||||
"source": "Self-host the woff2 files: the console's CSP blocks external hosts. packages/webui already serves Manrope 400-700; weight 800 and JetBrains Mono 400/500/700 still need adding (or drop 800 and use 700). The design preview loads both from Google Fonts only because it is a standalone page."
|
||||
},
|
||||
"type": {
|
||||
"page": {
|
||||
"size": 28,
|
||||
"weight": 800
|
||||
},
|
||||
"section": {
|
||||
"size": 20,
|
||||
"weight": 800
|
||||
},
|
||||
"question": {
|
||||
"size": 16,
|
||||
"weight": 700
|
||||
},
|
||||
"body": {
|
||||
"size": 14,
|
||||
"weight": 400,
|
||||
"lineHeight": 1.5
|
||||
},
|
||||
"table": {
|
||||
"size": 13,
|
||||
"weight": 400,
|
||||
"cellPadding": [
|
||||
7,
|
||||
10
|
||||
]
|
||||
},
|
||||
"label": {
|
||||
"size": 11,
|
||||
"weight": 700,
|
||||
"case": "upper",
|
||||
"tracking": "0.06em"
|
||||
},
|
||||
"mono": {
|
||||
"size": 12.5,
|
||||
"weight": 400
|
||||
},
|
||||
"numerals": "tabular-nums in any column of digits"
|
||||
},
|
||||
"shape": {
|
||||
"radius": {
|
||||
"control": 6,
|
||||
"panel": 10,
|
||||
"pill": 999,
|
||||
"kbd": 4,
|
||||
"table": 8
|
||||
},
|
||||
"stroke": {
|
||||
"hairline": 1
|
||||
},
|
||||
"focusRing": {
|
||||
"width": 2,
|
||||
"offset": 2
|
||||
}
|
||||
},
|
||||
"space": {
|
||||
"gutter": 16,
|
||||
"scale": [
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
14,
|
||||
16,
|
||||
20
|
||||
],
|
||||
"$comment": "Values the preview uses; gap 8 is the common sibling gap.",
|
||||
"gap": 12
|
||||
},
|
||||
"layout": {
|
||||
"console": {
|
||||
"sideList": 220,
|
||||
"inspector": {
|
||||
"min": 300,
|
||||
"max": 370
|
||||
},
|
||||
"breakpoints": {
|
||||
"inspectorOverlay": 1100,
|
||||
"singleColumn": 760
|
||||
}
|
||||
},
|
||||
"maxWidth": 1320
|
||||
},
|
||||
"icon": {
|
||||
"grid": 24,
|
||||
"stroke": 1.75,
|
||||
"linecap": "round",
|
||||
"linejoin": "round",
|
||||
"fill": "none",
|
||||
"sprite": "docs/design/icons.svg"
|
||||
},
|
||||
"terminal": {
|
||||
"size": {
|
||||
"design": [
|
||||
120,
|
||||
36
|
||||
],
|
||||
"minimum": [
|
||||
80,
|
||||
24
|
||||
],
|
||||
"keysOnlyNavBelow": 100
|
||||
},
|
||||
"ansi": [
|
||||
{
|
||||
"role": "action",
|
||||
"ansi": "blue (4)",
|
||||
"use": "action, selection, links, your messages"
|
||||
},
|
||||
{
|
||||
"role": "accent",
|
||||
"ansi": "yellow (3)",
|
||||
"use": "the PM, decisions"
|
||||
},
|
||||
{
|
||||
"role": "success",
|
||||
"ansi": "green (2)",
|
||||
"use": "working, pass, fresh"
|
||||
},
|
||||
{
|
||||
"role": "warning",
|
||||
"ansi": "bright yellow (11)",
|
||||
"use": "waiting, stale, expiring, outcome unknown"
|
||||
},
|
||||
{
|
||||
"role": "danger",
|
||||
"ansi": "red (1)",
|
||||
"use": "blocking, refused, error, conflict"
|
||||
},
|
||||
{
|
||||
"role": "muted",
|
||||
"ansi": "bright black (8)",
|
||||
"use": "timestamps, hints, rules"
|
||||
},
|
||||
{
|
||||
"role": "selection",
|
||||
"ansi": "reverse video",
|
||||
"use": "selected row"
|
||||
}
|
||||
],
|
||||
"noColor": "NO_COLOR set: no SGR colour; every state still reads from its word and mark; selection keeps reverse video"
|
||||
},
|
||||
"status": {
|
||||
"agent": [
|
||||
[
|
||||
"working",
|
||||
"●"
|
||||
],
|
||||
[
|
||||
"waiting",
|
||||
"◐"
|
||||
],
|
||||
[
|
||||
"error",
|
||||
"▲"
|
||||
],
|
||||
[
|
||||
"offline",
|
||||
"○"
|
||||
],
|
||||
[
|
||||
"idle",
|
||||
"–"
|
||||
],
|
||||
[
|
||||
"unknown",
|
||||
"?"
|
||||
]
|
||||
],
|
||||
"decision": [
|
||||
"routine",
|
||||
"within-role",
|
||||
"cross-role",
|
||||
"gated",
|
||||
"BLOCKING"
|
||||
],
|
||||
"taskMarker": [
|
||||
"changed in Vikunja",
|
||||
"not created by the stack",
|
||||
"conflict",
|
||||
"missing: moved",
|
||||
"not-found",
|
||||
"no-access"
|
||||
]
|
||||
},
|
||||
"mark": {
|
||||
"status": "placeholder (Jason 2026-10-10: use it for now, revise later)",
|
||||
"file": "docs/design/brand/relay-refined.svg",
|
||||
"viewBox": "0 0 16 17",
|
||||
"fill": "action"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
// Generates docs/design/tokens.css from docs/design/tokens.json.
|
||||
// Usage: node docs/design/tools/build-tokens.mjs [--check]
|
||||
// --check exits 1 when tokens.css is out of date instead of writing it.
|
||||
//
|
||||
// Selector contract for consumers:
|
||||
// no attributes default palette, light or dark from prefers-color-scheme
|
||||
// data-mode="light|dim|dark" explicit mode (packages/webui already sets data-mode)
|
||||
// data-palette="<id>" any non-default palette
|
||||
// Property names match packages/webui/src/public/shared/app.css and brand.js
|
||||
// (--onAction stays camelCase there), so this file can replace the inline
|
||||
// setProperty theming without renaming any var() use.
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const src = join(here, '..', 'tokens.json');
|
||||
const out = join(here, '..', 'tokens.css');
|
||||
const t = JSON.parse(readFileSync(src, 'utf8'));
|
||||
|
||||
const strong = (mode) => mode === 'light'
|
||||
? 'color-mix(in srgb, var(--text) 72%, #000)'
|
||||
: 'color-mix(in srgb, var(--text) 70%, #fff)';
|
||||
const colors = (modes, mode) => Object.entries(modes[mode]).map(([k, v]) => ` --${k}: ${v};`)
|
||||
.concat(` --strong: ${strong(mode)};`, ` color-scheme: ${mode === 'light' ? 'light' : 'dark'};`).join('\n');
|
||||
const block = (sel, body) => `${sel} {\n${body}\n}`;
|
||||
const media = (sel, body) => `@media (prefers-color-scheme: dark) {\n ${sel} {\n${body.replace(/^/gm, ' ')}\n }\n}`;
|
||||
|
||||
const def = t.palettes.find((p) => p.id === t.default.palette);
|
||||
if (!def) { console.error(`default palette ${t.default.palette} is not in tokens.json`); process.exit(2); }
|
||||
|
||||
const shared = [
|
||||
` --font: ${t.font.sans.stack};`,
|
||||
` --mono: ${t.font.mono.stack};`,
|
||||
` --r: ${t.shape.radius.control}px;`,
|
||||
` --r-lg: ${t.shape.radius.panel}px;`,
|
||||
` --r-table: ${t.shape.radius.table}px;`,
|
||||
` --pad: ${t.space.gutter}px;`,
|
||||
` --gap: ${t.space.gap}px;`,
|
||||
].join('\n');
|
||||
|
||||
const parts = [
|
||||
`/* Generated from tokens.json by tools/build-tokens.mjs. Do not edit by hand. */`,
|
||||
`/* Fonts: ${t.font.sans.family} and ${t.font.mono.family}. ${t.font.source} */`,
|
||||
block(':root', shared),
|
||||
`/* ${def.name}: the default palette */`,
|
||||
block(':root', colors(def.modes, 'light')),
|
||||
media(':root:not([data-mode="light"]):not([data-mode="dim"])', colors(def.modes, 'dark')),
|
||||
block(':root[data-mode="dark"]', colors(def.modes, 'dark')),
|
||||
block(':root[data-mode="dim"]', colors(def.modes, 'dim')),
|
||||
];
|
||||
for (const p of t.palettes) {
|
||||
if (p.id === def.id) continue;
|
||||
const s = `:root[data-palette="${p.id}"]`;
|
||||
parts.push(`/* ${p.name} */`,
|
||||
block(s, colors(p.modes, 'light')),
|
||||
media(`${s}:not([data-mode="light"]):not([data-mode="dim"])`, colors(p.modes, 'dark')),
|
||||
block(`${s}[data-mode="dark"]`, colors(p.modes, 'dark')),
|
||||
block(`${s}[data-mode="dim"]`, colors(p.modes, 'dim')));
|
||||
}
|
||||
const css = parts.join('\n\n') + '\n';
|
||||
|
||||
if (process.argv.includes('--check')) {
|
||||
let cur = '';
|
||||
try { cur = readFileSync(out, 'utf8'); } catch {}
|
||||
if (cur !== css) { console.error('tokens.css is out of date; run node docs/design/tools/build-tokens.mjs'); process.exit(1); }
|
||||
console.log('tokens.css is current');
|
||||
} else {
|
||||
writeFileSync(out, css);
|
||||
console.log(`wrote ${out} (${t.palettes.length} palettes x ${t.modes.length} modes)`);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env node
|
||||
// Browser checks for the design preview (docs/design/mosaic-stack-design.html).
|
||||
// Usage: node docs/design/tools/check-design.mjs [page.html]
|
||||
// Needs a local Chromium: $CHROMIUM, else /usr/bin/chromium. No dependencies.
|
||||
// Exit 0 when every check passes, 1 when any fails, 2 when the browser cannot start.
|
||||
//
|
||||
// Checks:
|
||||
// 1. no script errors while visiting every tab, console view, preview state,
|
||||
// inspector, terminal screen, palette and mode;
|
||||
// 2. no sideways page scroll at 400 px on any tab or console view;
|
||||
// 3. terminal screens are exactly 120 columns (80 for the compact screen) and
|
||||
// no taller than 36 rows (24 for compact); shell screens stay within 120;
|
||||
// 4. the preview's palette colours equal tokens.json for every palette and mode.
|
||||
// The CDP client follows packages/webui/tests/browser.mjs.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const page = resolve(process.argv[2] || join(here, '..', 'mosaic-stack-design.html'));
|
||||
const tokens = JSON.parse(readFileSync(join(here, '..', 'tokens.json'), 'utf8'));
|
||||
|
||||
async function browser() {
|
||||
const profile = await mkdtemp(join(tmpdir(), 'mosaic-design-check-'));
|
||||
const child = spawn(process.env.CHROMIUM || '/usr/bin/chromium', [
|
||||
'--headless', '--no-first-run', '--no-default-browser-check',
|
||||
'--disable-dev-shm-usage', '--remote-debugging-pipe', `--user-data-dir=${profile}`,
|
||||
], { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'] });
|
||||
const started = new Promise((ok, fail) => { child.once('error', fail); child.once('spawn', ok); });
|
||||
await started;
|
||||
let id = 0, buffer = '';
|
||||
const pending = new Map(), errors = [];
|
||||
child.stdio[4].on('data', (chunk) => {
|
||||
buffer += chunk.toString();
|
||||
let end;
|
||||
while ((end = buffer.indexOf('\0')) !== -1) {
|
||||
const m = JSON.parse(buffer.slice(0, end));
|
||||
buffer = buffer.slice(end + 1);
|
||||
if (m.method === 'Runtime.exceptionThrown') errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
|
||||
if (m.method === 'Runtime.consoleAPICalled' && m.params.type === 'error') errors.push(m.params.args.map((a) => a.value ?? a.description).join(' '));
|
||||
if (pending.has(m.id)) {
|
||||
const { ok, fail, timer } = pending.get(m.id);
|
||||
clearTimeout(timer);
|
||||
pending.delete(m.id);
|
||||
m.error ? fail(new Error(JSON.stringify(m.error))) : ok(m.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
const send = (method, params = {}, sessionId) => new Promise((ok, fail) => {
|
||||
const key = ++id;
|
||||
const timer = setTimeout(() => { pending.delete(key); fail(new Error(`CDP timeout: ${method}`)); }, 25000);
|
||||
pending.set(key, { ok, fail, timer });
|
||||
child.stdio[3].write(JSON.stringify({ id: key, method, params, ...(sessionId ? { sessionId } : {}) }) + '\0');
|
||||
});
|
||||
const { targetId } = await send('Target.createTarget', { url: 'about:blank' });
|
||||
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten: true });
|
||||
const call = (method, params) => send(method, params, sessionId);
|
||||
await call('Page.enable');
|
||||
await call('Runtime.enable');
|
||||
const evaluate = async (expression) => {
|
||||
const r = await call('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error(r.exceptionDetails.text + ': ' + r.result.description);
|
||||
return r.result.value;
|
||||
};
|
||||
return {
|
||||
errors, call, evaluate,
|
||||
viewport: (width, height = 900) => call('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: 1, mobile: false }),
|
||||
scheme: (value) => call('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value }] }),
|
||||
async navigate(url) {
|
||||
const r = await call('Page.navigate', { url });
|
||||
if (r.errorText) throw new Error(r.errorText);
|
||||
for (let n = 0; n < 50 && !(await evaluate('document.readyState === "complete"')); n++) await new Promise((ok) => setTimeout(ok, 100));
|
||||
},
|
||||
async close() {
|
||||
await send('Browser.close').catch(() => {});
|
||||
if (child.exitCode === null) await new Promise((ok) => child.once('exit', ok));
|
||||
await rm(profile, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const check = (name, failures) => {
|
||||
results.push({ name, failures });
|
||||
console.log(`${failures.length ? 'FAIL' : 'pass'} ${name}`);
|
||||
for (const f of failures.slice(0, 12)) console.log(` ${f}`);
|
||||
if (failures.length > 12) console.log(` ... ${failures.length - 12} more`);
|
||||
};
|
||||
|
||||
let b;
|
||||
try { b = await browser(); } catch (err) {
|
||||
console.error(`cannot start Chromium (${process.env.CHROMIUM || '/usr/bin/chromium'}): ${err.message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
try {
|
||||
await b.viewport(1280);
|
||||
await b.scheme('light');
|
||||
await b.navigate(pathToFileURL(page).href);
|
||||
await b.evaluate('localStorage.clear(), true').catch(() => {});
|
||||
|
||||
const tabs = await b.evaluate('[...document.querySelectorAll("[id^=tab-]")].map(e => e.id.slice(4))');
|
||||
const views = await b.evaluate('(document.querySelector("#tab-console").click(), [...document.querySelectorAll("#side [data-go]")].map(e => e.dataset.go))');
|
||||
const states = await b.evaluate('[...document.querySelectorAll("#stateSel option")].map(o => o.value)');
|
||||
const screens = await b.evaluate('(document.querySelector("#tab-terminal").click(), [...document.querySelectorAll("#screenList [data-scr]")].map(e => e.dataset.scr))');
|
||||
const nav = (v) => `document.querySelector('#side [data-go="${v}"]').click()`;
|
||||
const setState = (s) => `(()=>{const e=document.querySelector("#stateSel");e.value=${JSON.stringify(s)};e.dispatchEvent(new Event("change",{bubbles:true}))})()`;
|
||||
|
||||
// 1. Script errors across every view, state, inspector and screen.
|
||||
const before = b.errors.length;
|
||||
for (const t of tabs) await b.evaluate(`document.querySelector("#tab-${t}").click(), true`);
|
||||
await b.evaluate('document.querySelector("#tab-console").click(), true');
|
||||
for (const s of states) {
|
||||
await b.evaluate(`${setState(s)}, true`);
|
||||
for (const v of views) {
|
||||
await b.evaluate(`${nav(v)}, true`);
|
||||
await b.evaluate('(()=>{const r=document.querySelector("#view tbody tr");if(r)r.click();document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape",bubbles:true}))})(), true');
|
||||
}
|
||||
}
|
||||
await b.evaluate(`${setState('normal')}, true`);
|
||||
await b.evaluate('document.querySelector("#tab-terminal").click(), true');
|
||||
for (const s of screens) await b.evaluate(`document.querySelector('#screenList [data-scr="${s}"]').click(), true`);
|
||||
check(`no script errors (${tabs.length} tabs, ${views.length} views x ${states.length} states, ${screens.length} terminal screens)`, b.errors.slice(before));
|
||||
|
||||
// 3. Terminal widths and heights.
|
||||
const termFails = [];
|
||||
for (const s of screens) {
|
||||
const lines = await b.evaluate(`(document.querySelector('#screenList [data-scr="${s}"]').click(), document.querySelector("#term").innerText.split("\\n"))`);
|
||||
const shell = await b.evaluate('document.querySelector("#termTitle").textContent.endsWith("shell")');
|
||||
const compact = await b.evaluate('document.querySelector("#termTitle").textContent.endsWith("80×24")');
|
||||
const [w, h] = compact ? [80, 24] : [120, 36];
|
||||
lines.forEach((l, i) => {
|
||||
const n = [...l].length;
|
||||
if (shell ? n > w : n !== w) termFails.push(`${s} line ${i + 1}: ${n} columns, expected ${shell ? 'at most ' : ''}${w}`);
|
||||
});
|
||||
if (!shell && lines.length > h) termFails.push(`${s}: ${lines.length} rows, expected at most ${h}`);
|
||||
}
|
||||
check(`terminal screens fit 120x36 and 80x24 (${screens.length} screens)`, termFails);
|
||||
|
||||
// 2. No sideways scroll at 400 px.
|
||||
await b.viewport(400, 860);
|
||||
const wide = [];
|
||||
const overflow = async (label) => {
|
||||
const r = await b.evaluate('({sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth})');
|
||||
if (r.sw > r.cw) wide.push(`${label}: page is ${r.sw}px wide in a ${r.cw}px viewport`);
|
||||
};
|
||||
for (const t of tabs) { await b.evaluate(`document.querySelector("#tab-${t}").click(), true`); await overflow(`tab ${t}`); }
|
||||
await b.evaluate('document.querySelector("#tab-console").click(), true');
|
||||
for (const v of views) {
|
||||
await b.evaluate(`${nav(v)}, true`);
|
||||
await overflow(`view ${v}`);
|
||||
await b.evaluate('(()=>{const r=document.querySelector("#view tbody tr");if(r)r.click()})(), true');
|
||||
await overflow(`view ${v} with inspector`);
|
||||
await b.evaluate('document.dispatchEvent(new KeyboardEvent("keydown",{key:"Escape",bubbles:true})), true');
|
||||
}
|
||||
check(`no sideways scroll at 400 px (${tabs.length} tabs, ${views.length} views)`, wide);
|
||||
await b.viewport(1280);
|
||||
|
||||
// 4. Palettes and modes match tokens.json.
|
||||
const colourFails = [];
|
||||
const before4 = b.errors.length;
|
||||
const modes = await b.evaluate('[...document.querySelectorAll("#modeSeg button")].map(e => e.dataset.mode)');
|
||||
for (const p of tokens.palettes) {
|
||||
await b.evaluate(`(()=>{const e=document.querySelector("#palSel");e.value=${JSON.stringify(p.id)};e.dispatchEvent(new Event("change",{bubbles:true}))})(), true`);
|
||||
for (const m of modes) {
|
||||
for (const scheme of m === 'system' ? ['light', 'dark'] : ['light']) {
|
||||
await b.scheme(scheme);
|
||||
await b.evaluate(`document.querySelector('#modeSeg [data-mode="${m}"]').click(), true`);
|
||||
const want = p.modes[m === 'system' ? scheme : m];
|
||||
const got = await b.evaluate('(()=>{const s=getComputedStyle(document.documentElement);return {canvas:s.getPropertyValue("--canvas").trim(),action:s.getPropertyValue("--action").trim(),text:s.getPropertyValue("--text").trim(),danger:s.getPropertyValue("--danger").trim()}})()');
|
||||
for (const k of Object.keys(got)) {
|
||||
if (got[k].toLowerCase() !== String(want[k]).toLowerCase()) colourFails.push(`${p.id} ${m}${m === 'system' ? `/${scheme}` : ''} --${k}: page ${got[k]}, tokens.json ${want[k]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await b.scheme('light');
|
||||
check(`palette colours match tokens.json (${tokens.palettes.length} palettes x ${modes.length} modes)`, colourFails.concat(b.errors.slice(before4)));
|
||||
} finally {
|
||||
await b.close();
|
||||
}
|
||||
const failed = results.filter((r) => r.failures.length).length;
|
||||
console.log(failed ? `${failed} of ${results.length} checks failed` : `all ${results.length} checks passed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
Reference in New Issue
Block a user