Bootstrap the Scholarr rewrite
36
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Editors and operating systems
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Local configuration and secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.local.yaml
|
||||||
|
*.local.yml
|
||||||
|
|
||||||
|
# Go
|
||||||
|
/bin/
|
||||||
|
/dist/
|
||||||
|
coverage.out
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Vue and Node
|
||||||
|
node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
/data/
|
||||||
|
/backups/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.cache/
|
||||||
95
AGENTS.md
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# Scholarr agent brief
|
||||||
|
|
||||||
|
This file is the canonical brief for every coding agent working in this repository. Read it and
|
||||||
|
`TASKS.md` before making changes.
|
||||||
|
|
||||||
|
## Product
|
||||||
|
|
||||||
|
Scholarr is a public, self-hosted FOSS author watchlist and publication tracker. A user follows
|
||||||
|
authors, learns when they publish, and finds legal open-access PDFs. It must serve a real
|
||||||
|
multi-user household deployment while remaining straightforward for the wider self-hosting
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Immutable doctrine
|
||||||
|
|
||||||
|
There is zero Google Scholar network contact, ever.
|
||||||
|
|
||||||
|
- No application code, test, tool, CI job, or documentation example may request a Google
|
||||||
|
endpoint.
|
||||||
|
- No scraping, browser automation, CAPTCHA handling, anti-bot behavior, or evasion machinery.
|
||||||
|
- Scholar IDs and URLs may exist only as inert imported metadata and human-clickable links.
|
||||||
|
- Acquisition uses documented APIs: OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
|
||||||
|
- Semantic Scholar is not a v1 requirement.
|
||||||
|
- Provider clients must use a contact identity, conservative persisted rate limits, restart-safe
|
||||||
|
cooldowns, and user-visible health. OpenAlex must stay at or below 2 requests per second.
|
||||||
|
|
||||||
|
This doctrine outranks convenience and implementation shortcuts.
|
||||||
|
|
||||||
|
## Locked architecture
|
||||||
|
|
||||||
|
- Backend: Go.
|
||||||
|
- Frontend: Vue.
|
||||||
|
- Database: SQLite only. Design for single-writer discipline from the start.
|
||||||
|
- Root author entity: `FollowedAuthor`, with one or more `AuthorSourceIdentity` records.
|
||||||
|
- Names are labels, never identity keys. External source IDs anchor identity.
|
||||||
|
- Publications are globally deduplicated. Follows and read state are per user.
|
||||||
|
- Auth v1 includes internal username/password, generic OIDC, and trusted-header/forward-auth.
|
||||||
|
- The service is config-driven. One declarative config controls providers and behavior.
|
||||||
|
- GitHub `justinzeus/scholarr` is the intended public primary. Forgejo keeps a copy.
|
||||||
|
|
||||||
|
Do not silently change a locked decision. Record a proposed change as open and ask the owner.
|
||||||
|
|
||||||
|
## Decision record
|
||||||
|
|
||||||
|
On tank, the authoritative project runbook is:
|
||||||
|
|
||||||
|
`/opt/stacks/server-docs/runbooks/scholarr-rewrite/README.md`
|
||||||
|
|
||||||
|
It owns doctrine, locked decisions, empirical calibration results, and project state. Its
|
||||||
|
companion files own the identity model and testing strategy. This brief is self-contained for
|
||||||
|
agents without tank access, but any state or design decision reached on tank must also be written
|
||||||
|
to that runbook in the same session.
|
||||||
|
|
||||||
|
## Frozen UI contract
|
||||||
|
|
||||||
|
- `design/DESIGN.md` is the frozen Vue handoff contract.
|
||||||
|
- `design/tokens.css`, `design/component-reference.html`, and `design/assets/` are canonical UI
|
||||||
|
inputs.
|
||||||
|
- `design/reference/Scholarr.dc.html` is the reviewed proof implementation.
|
||||||
|
- `design/reference/support.js` belongs only to that design proof. Never ship it in production.
|
||||||
|
- Port the design faithfully. Do not redesign while implementing.
|
||||||
|
- Carry two cleanup items into the Vue port: avoid em dashes in bulk-import copy, and do not show
|
||||||
|
raw match percentages as user-facing confidence labels.
|
||||||
|
|
||||||
|
UI QA is manual and performed by the owner against the frozen contract. Do not add or run
|
||||||
|
Playwright, Selenium, or other browser automation. Unit, integration, build, and curl smoke tests
|
||||||
|
are expected.
|
||||||
|
|
||||||
|
## Work process
|
||||||
|
|
||||||
|
- Work through `TASKS.md` in order. Each task has a goal and acceptance criteria.
|
||||||
|
- Freeze one spec at a time. Decided items are labeled decided; open items remain open for owner
|
||||||
|
review.
|
||||||
|
- Do not start implementation for an area whose spec is not frozen.
|
||||||
|
- Keep commits focused and use plain imperative commit messages without trailers.
|
||||||
|
- Stage only files you changed.
|
||||||
|
- Do not use em dashes in prose, code comments, commit messages, or user-facing copy.
|
||||||
|
- Never commit secrets, real user data, database dumps, provider payloads containing private
|
||||||
|
data, API keys, tokens, or real email addresses.
|
||||||
|
- Keep configuration examples synthetic and safe to publish.
|
||||||
|
|
||||||
|
## Correctness obligations
|
||||||
|
|
||||||
|
- Pure normalization, fingerprinting, parsing, and scoring logic gets table tests, property tests
|
||||||
|
for invariants, and Go fuzz tests for external input parsers.
|
||||||
|
- Provider clients use deterministic `httptest` fixtures, including 429 responses, missing
|
||||||
|
`Retry-After`, truncated JSON, and empty pages. Rate and cooldown tests use a fake clock.
|
||||||
|
- Sync tests prove one active run per user, idempotent reruns, additive-only baseline import,
|
||||||
|
resumability after interruption, and SQLite concurrency behavior. Run the race detector.
|
||||||
|
- Every migration is tested up and down, including upgrade from a seeded prior-version database.
|
||||||
|
- End-to-end smoke boots a seeded SQLite database with stub providers and exercises onboarding,
|
||||||
|
import, sync, review, read state, and export through the API.
|
||||||
|
- Matching and dedup changes may not regress the recorded golden-corpus baseline.
|
||||||
|
|
||||||
|
Dry-run import previews and dry-run destructive admin actions are product features, not test-only
|
||||||
|
helpers.
|
||||||
1
CLAUDE.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Read AGENTS.md.
|
||||||
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# Scholarr
|
||||||
|
|
||||||
|
Scholarr is a self-hosted author watchlist and publication tracker. Follow researchers, learn
|
||||||
|
when they publish, and find legal open-access copies of their work.
|
||||||
|
|
||||||
|
The rewrite is pre-alpha. Its product direction and interface are settled, and the architecture
|
||||||
|
specs are being frozen before implementation begins.
|
||||||
|
|
||||||
|
## Non-negotiable rule
|
||||||
|
|
||||||
|
Scholarr never makes network requests to Google Scholar or any other Google endpoint. Imported
|
||||||
|
Scholar IDs and URLs are inert metadata and human-clickable links only. Publication data comes
|
||||||
|
from documented APIs such as OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
|
||||||
|
|
||||||
|
## Technical direction
|
||||||
|
|
||||||
|
- Go backend
|
||||||
|
- Vue frontend
|
||||||
|
- SQLite only
|
||||||
|
- Multi-user, with a globally deduplicated publication store and per-user follow/read state
|
||||||
|
- Internal auth, generic OIDC, and trusted-header auth in v1
|
||||||
|
- Config-driven operation and simple first-run onboarding
|
||||||
|
|
||||||
|
See [AGENTS.md](AGENTS.md) for the full project constraints and [TASKS.md](TASKS.md) for the
|
||||||
|
ordered work queue.
|
||||||
|
|
||||||
|
## Frozen interface contract
|
||||||
|
|
||||||
|
The reviewed interface contract is in [design/DESIGN.md](design/DESIGN.md). Its tokens, component
|
||||||
|
reference, icons, logos, favicons, and proof implementation are checked in beside it. These files
|
||||||
|
are the source for the Vue port, not an application implementation.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The license has not been selected yet. Until a LICENSE file is added, normal copyright rules
|
||||||
|
apply. License selection is an explicit project decision in [TASKS.md](TASKS.md).
|
||||||
89
TASKS.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Scholarr task cards
|
||||||
|
|
||||||
|
Work in order. A card is complete only when its acceptance criteria are met. Specs are reviewed
|
||||||
|
and frozen one at a time before implementation begins.
|
||||||
|
|
||||||
|
## Card 0: publish the clean repository **IN PROGRESS**
|
||||||
|
|
||||||
|
**Goal:** publish this clean-history bootstrap as the public project.
|
||||||
|
|
||||||
|
The local bootstrap is complete and the personal GitHub identity is authenticated on tank. Publish
|
||||||
|
this same history to GitHub first, then to the Forgejo copy.
|
||||||
|
|
||||||
|
**Acceptance:** public `justinzeus/scholarr` repositories exist on GitHub (primary) and Forgejo
|
||||||
|
(copy), both point at this clean history, no legacy history is present, and no secret or work
|
||||||
|
identity appears anywhere.
|
||||||
|
|
||||||
|
## Card 1: freeze the data model spec
|
||||||
|
|
||||||
|
**Goal:** specify global author/publication identity, per-user follows/read state, review records,
|
||||||
|
and merge/undo behavior.
|
||||||
|
|
||||||
|
Inputs: the project identity decision record, the legacy schema concepts worth salvaging, and the
|
||||||
|
global `FollowedAuthor` plus `AuthorSourceIdentity` direction.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/data-model.md` is owner-reviewed and marked frozen; constraints,
|
||||||
|
migrations, duplicate prevention, audit/undo behavior, and every remaining open question are
|
||||||
|
explicit.
|
||||||
|
|
||||||
|
## Card 2: freeze provider contracts and health
|
||||||
|
|
||||||
|
**Goal:** specify sanctioned API contracts, etiquette, persisted rate clocks/cooldowns, and
|
||||||
|
user-visible source health for OpenAlex, Crossref, arXiv, Unpaywall, and ORCID.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/providers.md` is owner-reviewed and marked frozen; keyed and anonymous
|
||||||
|
lanes, contact identity, retry semantics, restart behavior, fixture boundaries, and open questions
|
||||||
|
are explicit.
|
||||||
|
|
||||||
|
## Card 3: freeze the sync engine and run lifecycle
|
||||||
|
|
||||||
|
**Goal:** define safe, resumable acquisition within SQLite's concurrency model.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/sync-engine.md` is owner-reviewed and marked frozen; it defines one
|
||||||
|
active run per user, idempotent reruns, additive-only baseline import, resumability, writer
|
||||||
|
serialization, dry runs, and failure recovery.
|
||||||
|
|
||||||
|
## Card 4: freeze PDF resolution
|
||||||
|
|
||||||
|
**Goal:** define legal open-access discovery and provenance.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/pdf-resolution.md` is owner-reviewed and marked frozen; source order,
|
||||||
|
license/provenance handling, retry behavior, and the absence of all Google access are explicit.
|
||||||
|
|
||||||
|
## Card 5: freeze auth
|
||||||
|
|
||||||
|
**Goal:** define internal, generic OIDC, and trusted-header auth without provider-specific coupling.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/auth.md` is owner-reviewed and marked frozen; identity linking, roles,
|
||||||
|
session/security behavior, first-admin recovery, and mode-specific trust boundaries are explicit.
|
||||||
|
|
||||||
|
## Card 6: freeze the config schema
|
||||||
|
|
||||||
|
**Goal:** define one declarative configuration surface for the service.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/config.md` is owner-reviewed and marked frozen; defaults, validation,
|
||||||
|
secret references, provider settings, reload/restart behavior, and upgrade compatibility are
|
||||||
|
explicit.
|
||||||
|
|
||||||
|
## Card 7: freeze onboarding UX
|
||||||
|
|
||||||
|
**Goal:** make first run straightforward: search or paste Scholar URLs, review candidates, confirm,
|
||||||
|
and begin tracking.
|
||||||
|
|
||||||
|
**Acceptance:** `docs/specs/onboarding.md` is owner-reviewed and marked frozen; empty, ambiguous,
|
||||||
|
cooldown, partial-success, and unresolved-shell paths match the frozen UI and honest calibration
|
||||||
|
expectations.
|
||||||
|
|
||||||
|
## Card 8: choose the public license
|
||||||
|
|
||||||
|
**Goal:** make the repository genuinely open source under a consciously selected OSI license.
|
||||||
|
|
||||||
|
**Acceptance:** the owner selects the license, `LICENSE` is added with correct copyright identity,
|
||||||
|
and README/project metadata are updated.
|
||||||
|
|
||||||
|
## Card 9: plan build phases
|
||||||
|
|
||||||
|
**Goal:** split implementation into reviewable vertical slices against the frozen specs and UI.
|
||||||
|
|
||||||
|
**Acceptance:** build cards cover schema/migrations, providers, sync, auth, API, Vue port,
|
||||||
|
packaging, backup/restore, and release gates. Every card names its tests and demo proof.
|
||||||
375
design/DESIGN.md
Normal file
|
|
@ -0,0 +1,375 @@
|
||||||
|
# Scholarr — Design Contract (DESIGN.md)
|
||||||
|
|
||||||
|
This is the binding design specification for the Scholarr web app. It is written
|
||||||
|
for the engineering team building the Vue 3 SPA. Everything visual in the app is
|
||||||
|
expressed through the tokens in `tokens.css`; every screen is a composition of the
|
||||||
|
named components below. If an implementation choice is not covered here, prefer the
|
||||||
|
option that is calmest, most honest about data, and easiest for a non-technical
|
||||||
|
seventy-year-old to understand.
|
||||||
|
|
||||||
|
The working reference implementation is `Scholarr.dc.html` (all pages, all states,
|
||||||
|
light/dark, responsive). This document is the contract; the file is the proof.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Product principles that shape the UI
|
||||||
|
|
||||||
|
1. **Honesty about sources is a feature.** Never fake real-time. Show "last delivered"
|
||||||
|
facts and set the expectation that data lags reality by days to weeks. Every source
|
||||||
|
has a visible health state.
|
||||||
|
2. **Identity is probabilistic.** Matching a followed name to a database identity has a
|
||||||
|
confidence level. Ambiguous cases go to a human review queue — a first-class daily
|
||||||
|
surface, not an admin afterthought.
|
||||||
|
3. **Nothing destructive by default.** Bulk actions preview and count before they act;
|
||||||
|
every consequential decision is undoable.
|
||||||
|
4. **Two audiences, one app.** The founding user is a retired academic who wants to know
|
||||||
|
"anything new for me?" in one glance. The admin wants dense, clutter-free control.
|
||||||
|
The Dashboard belongs to the reader; Sources/Activity/Settings belong to the admin.
|
||||||
|
5. **Self-hostable and private.** System-font stacks only; no CDN, no externally loaded
|
||||||
|
fonts, no third-party assets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Signature & aesthetic
|
||||||
|
|
||||||
|
- **Concept: "the living bibliography."** The reading surface is typeset like a reference
|
||||||
|
list — serif titles, monospace identifiers (DOIs, arXiv IDs), a measured reading column.
|
||||||
|
- **Signature element:** the mortarboard-with-descending-binary logo (bundled as
|
||||||
|
`logo-light-tight.png` / `logo-dark-tight.png`), echoed by the honest **"as of" / "last
|
||||||
|
delivered" freshness stamps** that appear anywhere data is shown.
|
||||||
|
- **Type roles:** serif for titles & publication entries; sans for all chrome/UI; mono for
|
||||||
|
identifiers, timestamps, counts and uppercase micro-labels. Never set body UI in serif.
|
||||||
|
- **Restraint:** at most two background tones per screen. The binary motif appears only in
|
||||||
|
the logo — never as decoration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Color tokens
|
||||||
|
|
||||||
|
Authored in oklch (see `tokens.css`); sRGB hex below for tools that need it. Semantic
|
||||||
|
colors are shared by two vocabularies: **matching confidence** and **source health**.
|
||||||
|
|
||||||
|
| Token | Role | Light | Dark |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--paper` | app background | `#fcfaf6` | `#161719` |
|
||||||
|
| `--surface` | cards, panels | `#fffefc` | `#1f2123` |
|
||||||
|
| `--surface-2` | insets, hover, skeleton | `#f6f3ee` | `#2b2d30` |
|
||||||
|
| `--ink` | primary text | `#29231d` | `#e9ebee` |
|
||||||
|
| `--ink-soft` | secondary text | `#5d5751` | `#afb1b4` |
|
||||||
|
| `--ink-faint` | meta / disabled | `#8b8580` | `#7e8084` |
|
||||||
|
| `--line` | hairline borders | `#e1ddd9` | `#35373a` |
|
||||||
|
| `--line-strong` | control borders | `#cbc6c1` | `#505357` |
|
||||||
|
| `--accent` | primary action fill | `#2e6799` | `#6aaae6` |
|
||||||
|
| `--accent-soft` | tinted backgrounds | `#dceeff` | `#1f3654` |
|
||||||
|
| `--accent-ink` | accent text | `#055085` | `#98cdff` |
|
||||||
|
| `--hi` / `--hi-soft` | **healthy / confident** | `#519160` / `#d9f3dd` | `#6cc185` / `#193d2f` |
|
||||||
|
| `--mid` / `--mid-soft` | **cooling down / likely** | `#bd8630` / `#fce9c6` | `#deaf56` / `#46381a` |
|
||||||
|
| `--low` / `--low-soft` | **failing / uncertain** | `#c0453f` / `#ffe0d8` | `#f47c6e` / `#562e27` |
|
||||||
|
|
||||||
|
Rules: text on `--paper`/`--surface` uses `--ink`/`--ink-soft`. Solid `--accent`/`--hi`/
|
||||||
|
`--mid`/`--low` fills always pair with near-white text (`oklch(0.99 0 0)`) — verify AA.
|
||||||
|
`-soft` tokens are backgrounds only; pair with the matching solid token for text.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Typography
|
||||||
|
|
||||||
|
| Role | Font | Size | Weight | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Page title (H1) | serif | `clamp(21px, 4vw, 27px)` | 700 | letter-spacing −0.015em |
|
||||||
|
| Section / card title | serif | 15–17px | 700 | |
|
||||||
|
| Publication title | serif | 14.5–16.5px | 700 unread / 500 read | `text-wrap: pretty` |
|
||||||
|
| Body | sans | 13–14px | 400–500 | line-height 1.45 |
|
||||||
|
| Meta / secondary | sans | 11.5–12.5px | 400 | `--ink-soft` |
|
||||||
|
| Identifiers, timestamps, counts | mono | 10.5–11.5px | 400–700 | `--ink-faint` |
|
||||||
|
| Uppercase micro-label | mono | 10px | 700 | letter-spacing 0.08–0.12em |
|
||||||
|
|
||||||
|
Minimum body text 12px. Titles truncate to one line with ellipsis in dense lists; wrap in
|
||||||
|
detail/feed views.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Space, radius, elevation
|
||||||
|
|
||||||
|
- **Space scale:** 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40. Lay groups out with flex/grid + `gap`.
|
||||||
|
- **Radius:** `--r-sm` 6 (controls), `--r-md` 10 (option cards), `--r-lg` 16 (panels), `--r-pill` 20 (chips/badges).
|
||||||
|
- **Elevation:** `--sh-1` resting cards; `--sh-2` overlays (mobile bars, modals, toasts). Borders (`--line`) do most of the separation work; shadows are subtle.
|
||||||
|
- **Reading column:** dashboard ≤ 780px, publications/authors ≤ 1100px, detail/review/forms ≤ 820–900px, centered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. The two badge vocabularies
|
||||||
|
|
||||||
|
Both render as a pill: `--r-pill`, mono 10–11px 700, `-soft` background + solid-token text,
|
||||||
|
optional leading dot. **These words are the end-user language — do not show raw scores or
|
||||||
|
internal states in the UI.**
|
||||||
|
|
||||||
|
### Matching confidence (author identity)
|
||||||
|
| Internal | Badge label | Token | When |
|
||||||
|
|---|---|---|---|
|
||||||
|
| high (≥ ~90%) | **Confident** | `--hi` | Identifiers + works agree across sources |
|
||||||
|
| medium | **Likely** | `--mid` | Probable, but the name is shared — worth a look |
|
||||||
|
| low | **Uncertain** | `--low` | Weak evidence; review before trusting |
|
||||||
|
| shell | **Not yet identified** | `--ink-faint` | A name with no database identity |
|
||||||
|
|
||||||
|
### Source health
|
||||||
|
| Internal | Badge label | Token | Plain-language meaning |
|
||||||
|
|---|---|---|---|
|
||||||
|
| healthy | **Healthy** | `--hi` | Delivering normally |
|
||||||
|
| cooling | **Cooling down** | `--mid` | We asked too fast; the source told us to slow down. Resumes on its own. |
|
||||||
|
| failing | **Not connected** | `--low` | Sign-in/credentials failed; matching continues from other sources |
|
||||||
|
|
||||||
|
### Open-access availability (per publication)
|
||||||
|
`◈ OA` (`--hi`) available · `◇ ?` (`--ink-faint`) checking Unpaywall · `⊘` (`--low`) no free PDF found.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Component inventory (with states)
|
||||||
|
|
||||||
|
Every component below is component-shaped: repeated instances are structurally identical,
|
||||||
|
styled only through tokens, translatable 1:1 to a scoped Vue component.
|
||||||
|
|
||||||
|
- **Buttons** — *primary* (`--accent` fill, near-white text), *secondary* (`--surface` +
|
||||||
|
`--line-strong` border), *ghost* (transparent, `--ink-soft`), *destructive-lite*
|
||||||
|
(`--low` text on `--surface`). States: default / hover (`--surface-2` or darker accent) /
|
||||||
|
focus-visible (2px `--accent` ring) / disabled (`--ink-faint`, dashed border for
|
||||||
|
unavailable actions like a missing PDF) / active-pressed.
|
||||||
|
- **Inputs & selects** — `--surface-2` fill, `--line-strong` border, label in `--ink-soft`
|
||||||
|
12px. Focus ring as global. (Static in mockups; real fields in Vue.)
|
||||||
|
- **Filter chips / segmented control** — pill, active = `--accent-soft` bg + `--accent-ink`
|
||||||
|
text + `--accent` border; inactive = `--surface` + `--line`. Optional mono count sub-pill.
|
||||||
|
- **List row (publication)** — `[unread dot] [field tag · serif title (truncate)] [meta line:
|
||||||
|
authors · venue · year · id · source] [NEW marker slot] [OA badge] [PDF button] [read toggle]`.
|
||||||
|
Unread: filled `--unread` dot with `--accent-soft` halo, title 700. Read: hollow dot,
|
||||||
|
title 500, row opacity 0.66. Hover: `--surface-2`.
|
||||||
|
- **List row (author)** — `[checkbox] [avatar] [name (+ transliteration)] [field] [confidence
|
||||||
|
badge] [last sync] [new count]`. Selectable; header row labels the columns.
|
||||||
|
- **Table header** — mono uppercase micro-labels, `--line` bottom border.
|
||||||
|
- **Cards / panels** — `--surface`, `--line` border, `--r-lg`, `--sh-1`; header row with serif
|
||||||
|
title + optional mono meta, hairline-separated body.
|
||||||
|
- **Avatar** — circle, initials, `--surface-2` (neutral) or `--accent-soft` (self/active).
|
||||||
|
- **Badges** — confidence / source-health / OA / role, per §6.
|
||||||
|
- **Freshness stamp** — mono `--ink-faint`, always phrased "as of …" or "last delivered …".
|
||||||
|
- **Progress bar** — 6px track `--surface-2`, `--accent` fill (review queue, bulk import).
|
||||||
|
- **Modal / overlay** — `--surface`, `--sh-2`, `--r-lg`; dim scrim. (Pattern; compose as needed.)
|
||||||
|
- **Toast** — `-soft` bg + matching solid icon + `--sh-2`; auto-dismiss, carries an Undo action
|
||||||
|
for consequential changes.
|
||||||
|
- **Empty state** — centered glyph + serif headline + one sentence of `--ink-soft` guidance +
|
||||||
|
one primary action. Never a dead end.
|
||||||
|
- **Skeleton** — `--surface-2`→`--line` shimmer, respects reduced-motion (freezes). Mirrors the
|
||||||
|
real row's shape and count.
|
||||||
|
- **Bottom tab bar (mobile)** — fixed, `--surface`, `--sh-2`, 5 destinations, badge dots.
|
||||||
|
|
||||||
|
### Required states per page
|
||||||
|
Populated, **empty**, **loading (skeleton)**, and **degraded** are demonstrated in the
|
||||||
|
reference file. Degraded truths that must remain visible: a cooling-down source (Unpaywall),
|
||||||
|
a not-connected source (ORCID), an unresolvable PDF, an author shell, and a
|
||||||
|
three-plausible-candidates review case.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Interaction patterns
|
||||||
|
|
||||||
|
- **Navigation** — persistent collapsible left sidebar on desktop; a fixed bottom tab bar on
|
||||||
|
phone. The nav must never scroll horizontally (v1's bug). Below 760px the sidebar is replaced,
|
||||||
|
not shrunk.
|
||||||
|
- **Theme** — system / light / dark switch. System follows `prefers-color-scheme`; explicit
|
||||||
|
choice sets `data-theme` on the root.
|
||||||
|
- **Bulk selection** — row checkboxes reveal a selection bar with count + non-destructive actions
|
||||||
|
(re-sync, send to review) + Clear. Nothing acts without an explicit button.
|
||||||
|
- **Keyboard triage (Review queue)** — one decision at a time. `1`/`2`/`3` choose a candidate,
|
||||||
|
`S` skip, `U` undo, `J`/`K` (or ↑/↓) move. Visible `<kbd>` hints. Every decision is reversible.
|
||||||
|
- **Undo** — bulk imports and review decisions are undoable; imports can be reversed from Activity.
|
||||||
|
- **Freshness** — show relative times ("2h ago") plus absolute where precision matters. Never imply
|
||||||
|
live data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Copy rules & phrase glossary
|
||||||
|
|
||||||
|
- Address the reader plainly and warmly; avoid jargon and internal terms.
|
||||||
|
- **No em dashes.** Use periods, commas, or a middot (·) separator.
|
||||||
|
- Numbers and identifiers in mono; prose in sans.
|
||||||
|
- Never say "scraped", "crawler", "confidence score 0.71", "rate-limited (HTTP 429)". Say the
|
||||||
|
plain-language equivalent.
|
||||||
|
|
||||||
|
| Say this | Not this |
|
||||||
|
|---|---|
|
||||||
|
| Confident match / Likely / Uncertain | 0.98 / 0.71 / 0.48, high/med/low |
|
||||||
|
| Not yet identified (author shell) | null identity, orphan record |
|
||||||
|
| Cooling down — resumes on its own | rate-limited, HTTP 429, backoff |
|
||||||
|
| Not connected — sign-in was rejected | auth failure 401 |
|
||||||
|
| No free PDF found | Unpaywall miss |
|
||||||
|
| open-access PDF | OA green/gold, self-archived |
|
||||||
|
| last delivered 2h ago / as of … | live, real-time, up to the second |
|
||||||
|
| We will keep checking on schedule | polling, cron |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Accessibility
|
||||||
|
|
||||||
|
- **Contrast:** WCAG AA for all text and meaningful UI. Verify solid-fill + near-white text and
|
||||||
|
every `-soft`/solid pairing in both themes.
|
||||||
|
- **Focus:** visible 2px `--accent` ring on every interactive element (`:focus-visible`).
|
||||||
|
- **Reduced motion:** `prefers-reduced-motion` freezes skeleton shimmer and all transitions.
|
||||||
|
- **Color is never the only signal:** confidence and health always pair the color with a word
|
||||||
|
(and often a dot/icon); unread/read pair color with dot fill and weight.
|
||||||
|
- **Targets:** ≥ 44px hit targets on mobile controls.
|
||||||
|
- **Semantics:** real headings, buttons for actions, labels tied to inputs; transliterated names
|
||||||
|
carry the Latinized form alongside.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Vue 3 mapping notes
|
||||||
|
|
||||||
|
- Ship `tokens.css` once at the app root; never hard-code a color, font, radius, or shadow —
|
||||||
|
reference a `var(--token)`. Theme switching only toggles `data-theme` on the root.
|
||||||
|
- Each component in §7 becomes one scoped SFC. Keep markup identical across repeated instances
|
||||||
|
(the reference file already does this) so the port is mechanical.
|
||||||
|
- Derived styles that depend on data (confidence color, read/unread weight, active chip) should
|
||||||
|
be computed props returning token names, not literal colors.
|
||||||
|
- No technique in the reference relies on anything Vue-hostile: no global class cascade beyond
|
||||||
|
tokens + a few resets, no runtime style injection, no third-party fonts or assets.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3b. Contrast proof (WCAG AA)
|
||||||
|
|
||||||
|
Measured contrast ratios for every token pairing used for **text** (AA needs 4.5:1 for body, 3:1 for large). Ratios ≥ 4.5 unless noted.
|
||||||
|
|
||||||
|
New text-safe tokens added this round: `--hi-ink` / `--mid-ink` / `--low-ink` (AA text on the matching `-soft` background), and `--on-accent` (text on solid fills — flips near-white in light, near-black in dark).
|
||||||
|
|
||||||
|
| Pairing | Light | Dark |
|
||||||
|
|---|---|---|
|
||||||
|
| `--ink` on `--surface` | 15.4 | 13.5 |
|
||||||
|
| `--ink-soft` on `--surface` | 7.1 | 7.5 |
|
||||||
|
| `--ink-faint` on `--surface` | 4.8 | 5.2 |
|
||||||
|
| `--accent-ink` on `--surface` | 8.4 | 9.6 |
|
||||||
|
| `--accent-ink` on `--accent-soft` | 7.1 | 7.3 |
|
||||||
|
| `--hi-ink` on `--hi-soft` | 6.2 | 7.9 |
|
||||||
|
| `--mid-ink` on `--mid-soft` | 6.4 | 7.4 |
|
||||||
|
| `--low-ink` on `--low-soft` | 6.0 | 6.3 |
|
||||||
|
| `--on-accent` on `--accent` (primary btn) | 5.8 | 7.7 |
|
||||||
|
| `--on-accent` on `--low` (destructive btn) | 4.9 | 7.2 |
|
||||||
|
|
||||||
|
Rule that made the earlier "Likely / Cooling down" badge (2.66:1) pass: **solid semantic colours (`--hi`/`--mid`/`--low`) are never used as badge text.** Badge text is always the `-ink` variant on the `-soft` background. Solid fills only ever carry `--on-accent` text, and only for `--accent`/`--low` (never `--hi`/`--mid`, whose light-theme solids fail with white). `--ink-faint` was darkened to clear AA on `--surface`.
|
||||||
|
|
||||||
|
## 12. Motion & interaction spec
|
||||||
|
|
||||||
|
Tokens (in `tokens.css`): `--dur-fast` 120ms, `--dur-base` 180ms, `--dur-slow` 260ms; `--ease-out` cubic-bezier(.2,0,0,1), `--ease-in-out` cubic-bezier(.4,0,.2,1).
|
||||||
|
|
||||||
|
- **Animates:** hover/focus colour transitions (fast); toggle-switch knob (base); filter/theme chip selection (fast); skeleton shimmer (1.4s loop); toast slide-up + fade (base); modal scrim fade + card rise (base); tooltip fade+rise (fast).
|
||||||
|
- **Never animates:** content arriving from a sync (rows appear without shifting neighbours — reserve space with skeletons); route/screen changes (instant, no cross-fade — this is a data tool); numbers/counts (no count-up); the reading list on mark-read (opacity change only, no reflow/animation of position).
|
||||||
|
- **Reduced motion** (`prefers-reduced-motion: reduce`, handled globally in tokens.css): all transitions/animations collapse to ~0ms; skeleton shimmer freezes to a static tint; tooltips and toasts appear/disappear instantly; no scroll animation.
|
||||||
|
|
||||||
|
**Keyboard triage (Review queue)** — one decision on screen at a time:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| `1` `2` `3` | Choose candidate 1/2/3 |
|
||||||
|
| `S` | Skip (decide later) |
|
||||||
|
| `U` | Undo last decision |
|
||||||
|
| `J` / `↓` | Next item |
|
||||||
|
| `K` / `↑` | Previous item |
|
||||||
|
|
||||||
|
Every decision is reversible; the queue shows progress (`n of N`) and an always-available Undo. Nothing is committed to the shared identity graph until the user decides.
|
||||||
|
|
||||||
|
**Bulk selection** (Authors): row checkboxes reveal a selection bar with a live count and only non-destructive actions (Re-sync, Send to review) plus Clear. No action fires without an explicit button press; destructive operations route through the confirm modal (§17).
|
||||||
|
|
||||||
|
## 13. Responsive spec
|
||||||
|
|
||||||
|
Breakpoint: **phone < 760px**, **wide ≥ 760px** (single breakpoint by design — the tool is comfortable on a 13" laptop, usable on a phone). Documented as `--bp-phone: 760px`; because custom properties can't drive `@media`, the Vue app defines the query once in a composable and the value must match this token.
|
||||||
|
|
||||||
|
Per-surface reflow:
|
||||||
|
- **Navigation:** wide = persistent left sidebar (collapsible to 64px icon rail); phone = fixed **bottom tab bar** + a slim top bar. The nav never scrolls horizontally (the v1 bug). Below 760 the sidebar is *replaced*, not shrunk.
|
||||||
|
- **Dashboard:** hero + feed stack to one column; the "new work from" author chips wrap.
|
||||||
|
- **Publications:** rows keep their shape; the meta line truncates with ellipsis; filter chips scroll inside their own contained track (never the page).
|
||||||
|
- **Authors table:** columns drop progressively on phone — keep Author + Identity(confidence) + New; hide Field and Last-sync (available in author detail). The row stays the same component, columns toggle via the breakpoint.
|
||||||
|
- **Author detail / Add / Review / forms:** two-column layouts collapse to one; the identity panel moves below the publications list.
|
||||||
|
- **Modal:** full-width minus 20px margin on phone; **toast** spans near-full width, bottom, above the tab bar.
|
||||||
|
- Reading-column caps: dashboard 780, publications/authors 1100, detail/sources/activity 900, review/add/forms 820.
|
||||||
|
|
||||||
|
## 14. Form rules
|
||||||
|
|
||||||
|
- **Labels above inputs**, 12px `--ink-soft` 600. Required fields marked with `*` in `--low-ink` and stated once ("Fields marked * are required").
|
||||||
|
- **Validation on save**, not per keystroke (calm for the founding user). On failure: input gets `--low` border, an inline message below the field (`--low-ink`, 11.5px, leading `warning` icon), AA-legible; focus moves to the first invalid field.
|
||||||
|
- **Submit states:** default "Save changes" → disabled "Saving…" → success shows an inline "saved" banner (`--hi-soft`/`--hi-ink` + check) AND a toast. Cancel reverts to the read-only view with no change.
|
||||||
|
- Read-only view shows values as plain text (not disabled inputs); an explicit **Edit** button enters the form. This mirrors the Settings → Profile states (view / editing / error / saving / saved).
|
||||||
|
|
||||||
|
## 15. Copy glossary (complete — one term per concept)
|
||||||
|
|
||||||
|
Confidence: **Confident** · **Likely** · **Uncertain** · **Not yet identified** (shell).
|
||||||
|
Source health: **Healthy** · **Cooling down** · **Not connected**.
|
||||||
|
Open access: **open-access PDF** (available) · **checking** (Unpaywall resolving) · **no free PDF** (none found).
|
||||||
|
Freshness (always relative + honest, never "live"): **"last delivered {t}"**, **"as of {t}"**, **"last checked {t}"**, **"Up to date · {t}"**, **"arXiv can run a day or two behind. That is normal."**
|
||||||
|
Review actions: **Skip for now** · **Undo** · **None of these** / **Reject match** / **Keep separate** / **Keep both** / **Leave as shell** (reject label varies by case) · **Merge** (duplicates).
|
||||||
|
Bulk import: **"We checked your {n} lines before importing anything"**, **"look valid"**, **"could not be read"**, **"duplicates within your list"**, **"already in your followed list"**, **"Follow {n} confident matches"**, **"Send {n} to review"**, **"Safe by default: nothing is followed until you confirm."**
|
||||||
|
Empty states (one line each): Publications "Nothing to read yet." · Authors "You are not following anyone yet." · Author detail "No publications from this author are in your library yet." · Review "Queue is clear." · Sources "No sources connected yet." · Dashboard feed "You are all caught up."
|
||||||
|
Error patterns: **404** "That page is not on the shelf. … Nothing is broken." · Login fail "That username or password was not recognised. Please try again." · Field "Enter a valid email address, for example name@example.org." · Source fail "Not delivering. Sign-in or credentials failed; the other sources keep working."
|
||||||
|
Destructive confirm: **"Remove {name}?"** + what is and isn't affected + **"Remove user"** / **Cancel**; success toast **"{name} was removed. Undo?"**
|
||||||
|
Never shown to users: raw scores/percentages as the label, "scraped", "crawler", "rate-limited/429", "auth 401", "polling/cron", "real-time".
|
||||||
|
|
||||||
|
## 16. Icon set
|
||||||
|
|
||||||
|
All icons: 24×24 viewBox, `fill:none; stroke:currentColor; stroke-width:1.8; round caps/joins`. Shipped as `assets/icons/{name}.svg` and as an inline `<symbol id="ic-{name}">` sprite. Inherit colour from context; size via width/height.
|
||||||
|
|
||||||
|
`dashboard` (nav: home/overview) · `publications` (nav: reading list) · `authors` (nav: followed people) · `review` (nav: identity triage / balance) · `sources` (nav: data providers / database) · `activity` (nav: sync history) · `settings` (nav: gear) · `search` · `check` (read / confirmed / valid) · `circle` (unread-toggle empty / read-only) · `dot` (filled status dot) · `plus` (add) · `external` (outbound link / open PDF in new tab) · `chev-left` / `chev-right` (collapse / paginate) · `arrow-left` (back) · `undo` · `sun` / `moon` / `system` (theme) · `signout` · `warning` (caution / validation) · `x` (close / remove) · `oa` (open padlock = open-access available) · `lock` (closed padlock = no free PDF) · `pending` (dashed circle = checking) · `book` (empty reading state) · `upload` (bulk import) · `eye` (colour-blind toggle).
|
||||||
|
|
||||||
|
Typographic characters kept intentionally (NOT icons): `→` `←` `·` in prose/buttons. No em dashes anywhere.
|
||||||
|
|
||||||
|
## 17. Global patterns
|
||||||
|
|
||||||
|
- **Toast + undo:** bottom-centre, inverse surface (`--ink` bg / `--paper` text), one at a time, auto-dismiss 5s, always offers **Undo** for consequential changes. New toast replaces the current one.
|
||||||
|
- **Modal / confirm dialog:** scrim (`oklch(.2 .02 60/.5)`) + centred `--surface` card, `--sh-2`, title (serif) + body + footer actions right-aligned (Cancel secondary, destructive uses `--low` fill + `--on-accent`). Used for every destructive action (delete user, remove author with follow-data, reconnect source).
|
||||||
|
- **404:** full-bleed, in the product's calm voice, reassures nothing is broken, single primary action back to the dashboard.
|
||||||
|
- **Reader (non-admin) view:** admin-only surfaces are **hidden, not disabled** — Sources and Activity drop out of the nav; Users & roles, Sign-in and Source-credentials sections drop out of Settings. Toggle via "View as" (demo affordance; in production the role comes from the account).
|
||||||
|
- **Tooltips:** hover **and** keyboard-focus reveal (`.tip`/`.tip-pop`), used to explain status badges in place (replaces a static legend). Instant under reduced motion.
|
||||||
|
- **Colour-blind mode:** adds solid/dashed/dotted outlines to status badges so the three-stop scale is distinguishable without hue; toggle in the sidebar and in Settings → Accessibility; persisted in `localStorage` (`scholarr-cb`). Colour is never the only signal even with the mode off (every badge carries a word).
|
||||||
|
|
||||||
|
## 18. Build note — support.js is tool-only
|
||||||
|
|
||||||
|
The reference implementation `Scholarr.dc.html` loads a small runtime (`support.js`) that belongs to **the design tool only**. It is **not part of the production build and must not be shipped**. The Vue app reproduces the same markup and behaviour natively; every page must render correctly with no `support.js` present. The only runtime assets are `tokens.css`, the icon SVGs, the logos and the favicons.
|
||||||
|
|
||||||
|
## 19. Vue handoff appendix
|
||||||
|
|
||||||
|
**Theming strategy (pick one, this is it):** attribute strategy — `data-theme` on the root element, with `prefers-color-scheme` as the default when the attribute is absent (exactly as `tokens.css` is written). No component sets a raw colour; **all** colour/space/radius/shadow/motion flows through `var(--token)`. Scoped Vue `<style>` blocks reference tokens only.
|
||||||
|
|
||||||
|
**Component tree & responsibilities:**
|
||||||
|
|
||||||
|
- `AppShell` — CSS grid, owns theme + role + colour-blind state; slots `PrimaryNav`, the active view, `MobileTabBar`, `ToastHost`, `ModalHost`.
|
||||||
|
- `PrimaryNav` (sidebar; `collapsed` prop) → `NavItem`(icon, label, badge, active), `ThemeSwitch`(model system|light|dark), `ColorblindToggle`, `RoleSwitch`, `UserCard`.
|
||||||
|
- `MobileTabBar` → `TabItem`(icon, label, badge).
|
||||||
|
- Views (one per route): `DashboardView`, `PublicationsView`, `AuthorsView`, `AuthorDetailView`, `ReviewView`, `AddAuthorView`, `BulkImportView`, `SourcesView`, `ActivityView`, `SettingsView`, `LoginView`, `OnboardingView`, `NotFoundView`.
|
||||||
|
- Shared components (props → slots):
|
||||||
|
- `PageHeader`(title, meta, actions-slot)
|
||||||
|
- `FreshnessStamp`(kind: last-delivered|as-of|last-checked, time)
|
||||||
|
- `ConfidencePill`(level: high|medium|low|shell) — renders label + tooltip; reads colour-blind flag for outline.
|
||||||
|
- `SourceHealthBadge`(state: healthy|cooling|failing) and `SourceHealthCard`(source) with reconnect action.
|
||||||
|
- `OaBadge`(status: pdf|pending|none) — icon + label + tooltip.
|
||||||
|
- `PubRow`(pub, read, isNew; emits open-pdf, toggle-read) — the bibliography-style entry.
|
||||||
|
- `AuthorRow`(author, selected; emits toggle-select, open).
|
||||||
|
- `FilterChip`(label, count, active), `StatePreviewToggle` (dev-only, remove in prod).
|
||||||
|
- `Skeleton`(shape: row|card, count), `EmptyState`(icon, title, body, action-slot).
|
||||||
|
- `Tooltip` (`.tip`/`.tip-pop`; hover + focus).
|
||||||
|
- `Modal` / `ConfirmDialog`(title, body, confirmLabel, destructive) — emits confirm/cancel.
|
||||||
|
- `Toast`(message, undo) via `ToastHost`.
|
||||||
|
- `Card`, `Button`(variant: primary|secondary|ghost|danger; disabled), `Field`(label, required, error), `ToggleSwitch`(checked), `Avatar`(initials, tone).
|
||||||
|
- `TriageDeck` (Review) — current item + keyboard handling + progress + undo.
|
||||||
|
- `CandidateCard` (Add author disambiguation), `BulkStepper` (import phases).
|
||||||
|
|
||||||
|
Repeated markup is already identical instance-to-instance in the reference file, so each maps to one component with no structural surprises.
|
||||||
|
|
||||||
|
## 20. Asset finalization
|
||||||
|
|
||||||
|
- **Logos:** `assets/logo-light.png` (dark mark, for light backgrounds) and `assets/logo-dark.png` (light mark, for dark backgrounds). The app swaps by effective theme. Minimum display 24px; clear space ≥ 25% of the mark's height on all sides; never recolour, rotate, or stretch — the descending-binary tail is part of the mark and the signature.
|
||||||
|
- **Favicons / PWA:** `assets/favicon.png` and `assets/favicon-512.png` (maskable-safe square).
|
||||||
|
- **Icons:** `assets/icons/*.svg` (29 files) plus the inline sprite; `currentColor`, 24 viewBox, 1.8 stroke — see §16.
|
||||||
|
|
||||||
|
## 21. Deliverables in this package
|
||||||
|
|
||||||
|
- `Scholarr.dc.html` — the living reference implementation: all 13 screens, every state (populated / loading / empty / degraded), light + dark, responsive, colour-blind mode, working review triage, confirm modal, undo toast, 404. (Runs on the tool's support.js — see §18; not shipped.)
|
||||||
|
- `component-reference.html` — every component in its states, both themes, self-contained (links `tokens.css`, own stylesheet, vanilla-JS theme/colour-blind toggles). Open it directly.
|
||||||
|
- `tokens.css` — the frozen token layer (colour incl. `-ink`/`--on-accent`, type, space, radius, elevation, motion).
|
||||||
|
- `DESIGN.md` — this contract.
|
||||||
|
- `assets/` — icons, logos, favicons.
|
||||||
15
design/README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Frozen UI reference
|
||||||
|
|
||||||
|
This directory is the reviewed Scholarr UI contract frozen on 2026-07-22.
|
||||||
|
|
||||||
|
- `DESIGN.md` is the canonical Vue handoff.
|
||||||
|
- `tokens.css` is the canonical token layer.
|
||||||
|
- `component-reference.html` renders the component library from those tokens.
|
||||||
|
- `assets/` contains the production icon, logo, and favicon inputs.
|
||||||
|
- `reference/Scholarr.dc.html` is the proof implementation used during design review.
|
||||||
|
- `reference/support.js` and `reference/uploads/` exist only to render that proof. The support
|
||||||
|
runtime must never be included in the production application.
|
||||||
|
|
||||||
|
The original reviewed files came from the git-excluded legacy design workspace on tank at
|
||||||
|
`/opt/stacks/scholarr/design/round2/`. Change the product UI only through an explicit owner-approved
|
||||||
|
design revision. Implementation work should port this contract, not edit it in place.
|
||||||
BIN
design/assets/favicon-512.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
design/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
1
design/assets/icons/activity.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3 8 4-16 3 8h4"></path></svg>
|
||||||
|
After Width: | Height: | Size: 229 B |
1
design/assets/icons/arrow-left.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="20" y1="12" x2="5" y2="12"></line><path d="M11 6l-6 6 6 6"></path></svg>
|
||||||
|
After Width: | Height: | Size: 265 B |
1
design/assets/icons/authors.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3"></circle><path d="M3.5 20c0-3.3 2.5-5.5 5.5-5.5s5.5 2.2 5.5 5.5"></path><circle cx="17.5" cy="9.5" r="2.3"></circle><path d="M16 15c2.4.2 4 2.1 4 5"></path></svg>
|
||||||
|
After Width: | Height: | Size: 373 B |
1
design/assets/icons/book.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5.5C4 4.7 4.7 4 5.5 4H11v16H5.5C4.7 20 4 20.5 4 21.3z"></path><path d="M20 5.5C20 4.7 19.3 4 18.5 4H13v16h5.5c.8 0 1.5.5 1.5 1.3z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 339 B |
1
design/assets/icons/check.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12.5l5 5 10-11"></path></svg>
|
||||||
|
After Width: | Height: | Size: 226 B |
1
design/assets/icons/chev-left.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"></path></svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
1
design/assets/icons/chev-right.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"></path></svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
1
design/assets/icons/circle.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"></circle></svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
1
design/assets/icons/dashboard.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="18" rx="1.5"></rect><rect x="14" y="3" width="7" height="8" rx="1.5"></rect><rect x="14" y="15" width="7" height="6" rx="1.5"></rect></svg>
|
||||||
|
After Width: | Height: | Size: 358 B |
1
design/assets/icons/dot.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="7" fill="currentColor" stroke="none"></circle></svg>
|
||||||
|
After Width: | Height: | Size: 262 B |
1
design/assets/icons/external.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6"></path><line x1="20" y1="4" x2="10.5" y2="13.5"></line><path d="M18 13.5V19a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5.5"></path></svg>
|
||||||
|
After Width: | Height: | Size: 343 B |
1
design/assets/icons/eye.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
1
design/assets/icons/lock.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V7.5a4 4 0 0 1 8 0V11"></path></svg>
|
||||||
|
After Width: | Height: | Size: 288 B |
1
design/assets/icons/moon.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13.5A8 8 0 1 1 10.5 4a6.3 6.3 0 0 0 9.5 9.5z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 255 B |
1
design/assets/icons/oa.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="9" rx="2"></rect><path d="M8 11V7.5a4 4 0 0 1 7.3-2.3"></path></svg>
|
||||||
|
After Width: | Height: | Size: 289 B |
1
design/assets/icons/pending.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8" stroke-dasharray="2.6 3"></circle></svg>
|
||||||
|
After Width: | Height: | Size: 253 B |
1
design/assets/icons/plus.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
1
design/assets/icons/publications.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="6" x2="20" y2="6"></line><line x1="4" y1="12" x2="20" y2="12"></line><line x1="4" y1="18" x2="14" y2="18"></line></svg>
|
||||||
|
After Width: | Height: | Size: 319 B |
1
design/assets/icons/review.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18"></path><line x1="6" y1="7" x2="18" y2="7"></line><path d="M6 7l-3 6.5h6z"></path><path d="M18 7l-3 6.5h6z"></path><line x1="8" y1="21" x2="16" y2="21"></line></svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
1
design/assets/icons/search.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"></circle><line x1="16.5" y1="16.5" x2="21" y2="21"></line></svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
1
design/assets/icons/settings.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9L17 7M7 17l-2.1 2.1"></path></svg>
|
||||||
|
After Width: | Height: | Size: 334 B |
1
design/assets/icons/signout.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2"></path><line x1="19" y1="12" x2="9" y2="12"></line><path d="M16 9l3 3-3 3"></path></svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
1
design/assets/icons/sources.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="5.5" rx="7" ry="2.8"></ellipse><path d="M5 5.5v13c0 1.6 3.1 2.8 7 2.8s7-1.2 7-2.8v-13"></path><path d="M5 12c0 1.6 3.1 2.8 7 2.8s7-1.2 7-2.8"></path></svg>
|
||||||
|
After Width: | Height: | Size: 359 B |
1
design/assets/icons/sun.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"></circle><path d="M12 2v2.5M12 19.5V22M2 12h2.5M19.5 12H22M4.9 4.9l1.8 1.8M17.3 17.3l1.8 1.8M19.1 4.9l-1.8 1.8M6.7 17.3l-1.8 1.8"></path></svg>
|
||||||
|
After Width: | Height: | Size: 356 B |
1
design/assets/icons/system.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="1.5"></rect><line x1="8" y1="20" x2="16" y2="20"></line><line x1="12" y1="16" x2="12" y2="20"></line></svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
1
design/assets/icons/undo.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 7L4 12l5 5"></path><path d="M4 12h10a6 6 0 0 1 0 12h-1.5"></path></svg>
|
||||||
|
After Width: | Height: | Size: 266 B |
1
design/assets/icons/upload.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 15V4"></path><path d="M8 8l4-4 4 4"></path><path d="M5 15v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3"></path></svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
1
design/assets/icons/warning.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5l9 16.5H3z"></path><line x1="12" y1="10" x2="12" y2="14"></line><circle cx="12" cy="17" r="0.9" fill="currentColor" stroke="none"></circle></svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
1
design/assets/icons/x.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"></line><line x1="18" y1="6" x2="6" y2="18"></line></svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
BIN
design/assets/logo-dark.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
design/assets/logo-light.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
212
design/component-reference.html
Normal file
1078
design/reference/Scholarr.dc.html
Normal file
1841
design/reference/support.js
Normal file
BIN
design/reference/uploads/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
design/reference/uploads/logo-dark-tight.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
design/reference/uploads/logo-dark.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
design/reference/uploads/logo-light-tight.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
design/reference/uploads/scholar_logo.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
106
design/tokens.css
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
/* ============================================================
|
||||||
|
Scholarr — design tokens (FROZEN CONTRACT)
|
||||||
|
Single source of truth for color, type, space, radius,
|
||||||
|
elevation, and motion. Light is default; dark applies by OS
|
||||||
|
preference OR an explicit [data-theme="dark"] on a root
|
||||||
|
element. [data-theme="light"] forces light under a dark OS.
|
||||||
|
|
||||||
|
Colors authored in oklch; sRGB hex beside each is the
|
||||||
|
reference/fallback. See DESIGN.md §3 for the full contrast
|
||||||
|
proof table. Text colors are chosen so every pairing used
|
||||||
|
for text meets WCAG AA in both themes.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Type ---------------------------------------------------- */
|
||||||
|
--font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
|
||||||
|
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace;
|
||||||
|
|
||||||
|
/* Type scale (px) ---------------------------------------- */
|
||||||
|
--fs-display: 27px; --fs-title: 17px; --fs-body: 14px;
|
||||||
|
--fs-sm: 12.5px; --fs-xs: 11px; --fs-micro: 10px;
|
||||||
|
--lh-tight: 1.25; --lh-body: 1.45;
|
||||||
|
|
||||||
|
/* Space scale (px) --------------------------------------- */
|
||||||
|
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
|
||||||
|
--sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
|
||||||
|
|
||||||
|
/* Radius ------------------------------------------------- */
|
||||||
|
--r-sm: 6px; --r-md: 10px; --r-lg: 16px; --r-pill: 20px;
|
||||||
|
|
||||||
|
/* Elevation ---------------------------------------------- */
|
||||||
|
--sh-1: 0 1px 2px oklch(0.26 0.02 62 / 0.06), 0 2px 8px oklch(0.26 0.02 62 / 0.05);
|
||||||
|
--sh-2: 0 2px 6px oklch(0.26 0.02 62 / 0.08), 0 12px 32px oklch(0.26 0.02 62 / 0.09);
|
||||||
|
|
||||||
|
/* Motion (see DESIGN.md §Motion). Frozen at 0 by reduced-motion below. */
|
||||||
|
--dur-fast: 120ms; --dur-base: 180ms; --dur-slow: 260ms;
|
||||||
|
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
/* Breakpoint reference (custom props can't drive @media; documented here
|
||||||
|
and mirrored in DESIGN.md §Responsive): phone < 760px, wide >= 760px. */
|
||||||
|
|
||||||
|
/* Surfaces & ink (warm paper/ink) ------------------------ */
|
||||||
|
--paper: oklch(0.986 0.006 85); /* #fcfaf6 */
|
||||||
|
--surface: oklch(0.998 0.003 85); /* #fffefc */
|
||||||
|
--surface-2: oklch(0.965 0.008 84); /* #f6f3ee */
|
||||||
|
--ink: oklch(0.26 0.014 62); /* #29231d */
|
||||||
|
--ink-soft: oklch(0.46 0.012 62); /* #5d5751 */
|
||||||
|
--ink-faint: oklch(0.53 0.011 64); /* #76706b (AA on surface) */
|
||||||
|
--line: oklch(0.9 0.007 72); /* #e1ddd9 */
|
||||||
|
--line-strong: oklch(0.83 0.009 72); /* #cbc6c1 */
|
||||||
|
|
||||||
|
/* Accent (scholarly ink-blue) ---------------------------- */
|
||||||
|
--accent: oklch(0.5 0.1 248); /* #2e6799 */
|
||||||
|
--accent-soft: oklch(0.94 0.03 248); /* #dceeff */
|
||||||
|
--accent-ink: oklch(0.42 0.11 248); /* #055085 accent text on paper/soft */
|
||||||
|
--unread: oklch(0.5 0.1 248); /* #2e6799 */
|
||||||
|
--on-accent: oklch(0.99 0 0); /* #fcfcfc text ON solid fills */
|
||||||
|
|
||||||
|
/* Semantic — confidence AND source health share one scale.
|
||||||
|
-soft = background, -ink = AA-safe text on that -soft. */
|
||||||
|
--hi: oklch(0.6 0.1 150); /* #519160 healthy / confident */
|
||||||
|
--hi-soft: oklch(0.94 0.04 150); /* #d9f3dd */
|
||||||
|
--hi-ink: oklch(0.44 0.11 150); /* #15632f */
|
||||||
|
--mid: oklch(0.66 0.12 74); /* #bd8630 cooling / likely */
|
||||||
|
--mid-soft: oklch(0.94 0.05 82); /* #fce9c6 */
|
||||||
|
--mid-ink: oklch(0.45 0.09 74); /* #734c0c */
|
||||||
|
--low: oklch(0.56 0.16 26); /* #c0453f failing / uncertain */
|
||||||
|
--low-soft: oklch(0.94 0.05 30); /* #ffe0d8 */
|
||||||
|
--low-ink: oklch(0.47 0.17 26); /* #a51e21 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark tokens (shared by OS-preference and explicit opt-in) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
--paper: oklch(0.205 0.004 255); --surface: oklch(0.247 0.005 255); --surface-2: oklch(0.295 0.006 255);
|
||||||
|
--ink: oklch(0.94 0.004 255); --ink-soft: oklch(0.76 0.005 255); --ink-faint: oklch(0.68 0.006 255);
|
||||||
|
--line: oklch(0.335 0.006 255); --line-strong: oklch(0.44 0.008 255);
|
||||||
|
--accent: oklch(0.72 0.11 248); --accent-soft: oklch(0.33 0.06 255); --accent-ink: oklch(0.83 0.09 248);
|
||||||
|
--unread: oklch(0.74 0.11 248); --on-accent: oklch(0.17 0.02 255);
|
||||||
|
--hi: oklch(0.74 0.12 152); --hi-soft: oklch(0.33 0.05 165); --hi-ink: oklch(0.85 0.11 152);
|
||||||
|
--mid: oklch(0.78 0.12 82); --mid-soft: oklch(0.35 0.05 85); --mid-ink: oklch(0.86 0.11 85);
|
||||||
|
--low: oklch(0.72 0.15 28); --low-soft: oklch(0.35 0.06 30); --low-ink: oklch(0.84 0.13 28);
|
||||||
|
--sh-1: 0 1px 2px oklch(0 0 0 / 0.3), 0 2px 8px oklch(0 0 0 / 0.28);
|
||||||
|
--sh-2: 0 2px 8px oklch(0 0 0 / 0.4), 0 16px 40px oklch(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--paper: oklch(0.205 0.004 255); --surface: oklch(0.247 0.005 255); --surface-2: oklch(0.295 0.006 255);
|
||||||
|
--ink: oklch(0.94 0.004 255); --ink-soft: oklch(0.76 0.005 255); --ink-faint: oklch(0.68 0.006 255);
|
||||||
|
--line: oklch(0.335 0.006 255); --line-strong: oklch(0.44 0.008 255);
|
||||||
|
--accent: oklch(0.72 0.11 248); --accent-soft: oklch(0.33 0.06 255); --accent-ink: oklch(0.83 0.09 248);
|
||||||
|
--unread: oklch(0.74 0.11 248); --on-accent: oklch(0.17 0.02 255);
|
||||||
|
--hi: oklch(0.74 0.12 152); --hi-soft: oklch(0.33 0.05 165); --hi-ink: oklch(0.85 0.11 152);
|
||||||
|
--mid: oklch(0.78 0.12 82); --mid-soft: oklch(0.35 0.05 85); --mid-ink: oklch(0.86 0.11 85);
|
||||||
|
--low: oklch(0.72 0.15 28); --low-soft: oklch(0.35 0.06 30); --low-ink: oklch(0.84 0.13 28);
|
||||||
|
--sh-1: 0 1px 2px oklch(0 0 0 / 0.3), 0 2px 8px oklch(0 0 0 / 0.28);
|
||||||
|
--sh-2: 0 2px 8px oklch(0 0 0 / 0.4), 0 16px 40px oklch(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global accessibility defaults ---------------------------- */
|
||||||
|
*:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; scroll-behavior: auto !important; }
|
||||||
|
}
|
||||||
11
docs/specs/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Architecture specs
|
||||||
|
|
||||||
|
Specs are frozen in the order listed in `TASKS.md`. Each spec must:
|
||||||
|
|
||||||
|
- distinguish locked decisions from open questions;
|
||||||
|
- define data and trust boundaries;
|
||||||
|
- state failure, recovery, migration, and observability behavior;
|
||||||
|
- identify deterministic fixtures and acceptance tests;
|
||||||
|
- receive explicit owner review before being marked frozen.
|
||||||
|
|
||||||
|
No implementation for a subsystem starts before its spec is frozen.
|
||||||