From ec6e702190ec7a102482afa6146dd2bffc6d78c6 Mon Sep 17 00:00:00 2001 From: jdluu Date: Sat, 29 Aug 2026 14:19:16 -0700 Subject: [PATCH] docs: finalize repository documentation layout Closes #117 --- AGENTS.md | 9 +- agent_docs/FEATURE_FREEZE.md | 47 ----- agent_docs/REFACTOR_PLAN.md | 102 ---------- agent_docs/android-hardening.md | 190 ------------------- agent_docs/BRAND.md => docs/design-system.md | 68 ++++--- src/App.css | 4 +- src/__tests__/design/tokens.test.ts | 2 +- src/design/tokens.ts | 2 +- 8 files changed, 39 insertions(+), 385 deletions(-) delete mode 100644 agent_docs/FEATURE_FREEZE.md delete mode 100644 agent_docs/REFACTOR_PLAN.md delete mode 100644 agent_docs/android-hardening.md rename agent_docs/BRAND.md => docs/design-system.md (54%) diff --git a/AGENTS.md b/AGENTS.md index b615e2e..6c55f0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,18 +38,15 @@ the division of responsibility between the two apps. ## Layout map ``` -agent_docs/ Agent/internal documentation: BRAND.md (design system v2), - FEATURE_FREEZE.md (scope boundary), android-hardening.md. - Not user-facing; do not link from README. -docs/ User-facing docs only: app-boundaries.md, screenshots. +docs/ User-facing docs: app-boundaries.md, design-system.md (canonical + design reference; mirrored by src/design/tokens.ts + src/App.css), + screenshots. ``` ## Workflow (enforced) - All changes land via PR from a `feat/`, `fix/`, `chore/`, or `docs/` branch referencing a GitHub Issue; squash-merge to `main`; delete branch. -- Scope is governed by `agent_docs/FEATURE_FREEZE.md` — no new features - without a tagged exception issue. - Never commit roadmap/planning docs, credentials, or `.env` files. ### Source layout diff --git a/agent_docs/FEATURE_FREEZE.md b/agent_docs/FEATURE_FREEZE.md deleted file mode 100644 index b0d5d56..0000000 --- a/agent_docs/FEATURE_FREEZE.md +++ /dev/null @@ -1,47 +0,0 @@ -# ShelfSync Feature Freeze - -Status: **ACTIVE** as of 2026-08-25. Owner: jdluu. - -## The freeze line - -ShelfSync is feature-complete for its core mission: - -> Connect to an OPDS catalog, authenticate, download publications with -> integrity verification, and manage an offline library on Windows, Linux, -> and Android. - -Everything below the line is **done**. No new features are accepted above -it except through a written exception (see below). - -## In scope (allowed work) - -1. **Refactoring** — decomposition of oversized modules, extraction of - layers, removal of dead/legacy code (Calibre compatibility, P2P remnants). -2. **Stability** — bug fixes, error-handling hardening, race fixes. -3. **Performance** — download throughput, catalog parse latency, render - virtualization, memory footprint. -4. **Modularity** — provider-adapter boundaries, IPC contract tightening, - test coverage for existing behavior only. -5. **Security** — credential handling, transport verification, CSP, - Android storage scoping. -6. **Tooling/CI/docs** — anything that keeps the validation suite honest. - -## Out of scope (frozen — do not build) - -- Any reading/rendering UI (permanent boundary; see docs/app-boundaries.md). -- New catalog providers beyond generic OPDS + the Grimmory adapter. -- Sync/progress push to readers (deferred to Leafline collaboration later). -- New platforms (iOS, macOS builds). -- New UI paradigms: rethemed navigation, new dashboards, social features. -- Calibre direct-sync features beyond the legacy read-only compat layer. - -## Exception process - -Open a GitHub issue tagged `feature-freeze-exception` with: the user need, -why it cannot wait until after stabilization, and the blast radius. -Requires explicit approval from the maintainer before any branch is cut. - -## Exit criteria for lifting the freeze - -Not defined yet. Revisit when: zero open stability issues for one release -cycle, refactoring backlog empty, and CI fully green including e2e. diff --git a/agent_docs/REFACTOR_PLAN.md b/agent_docs/REFACTOR_PLAN.md deleted file mode 100644 index df5d8a4..0000000 --- a/agent_docs/REFACTOR_PLAN.md +++ /dev/null @@ -1,102 +0,0 @@ -# ShelfSync Comprehensive Refactor Plan (post-feature-freeze) - -Status: ACTIVE. Owner: jdluu. Executor: OpenCode (`opencode-bws`, model -`openrouter/stealth/ox-alpha`) under Hermes pilot supervision. - -## Ground rules (every milestone, no exceptions) - -1. One branch per milestone off `main`: `refactor/mN-`. -2. Behavior-preserving. No feature changes, no UI changes, no API/IPC changes. -3. Validation gate before any commit: - - `cargo test --manifest-path src-tauri/Cargo.toml` (200 tests must pass) - - `pnpm vitest run` (170 tests must pass) - - `npx tsc -b` (0 errors) - - `biome check .` (clean) - - `cargo check` (0 warnings) -4. Android device verification (Pixel 7 via adb) at the end of each phase: - `pnpm tauri android build --apk` + install + manual/scripted smoke test. - Nothing is assumed working until verified on-device. -5. Commits: conventional, granular, per logical step. PR per milestone. - -## Milestones - -### M1 — Rust backend layering (SOLID: SRP + DIP) - -The OPDS domain is the core asset. Split by responsibility: - -- `src-tauri/src/opds/install.rs` (1201 lines) -> extract: - - `install/path_planner.rs` (destination resolution, filename derivation) - - `install/archive_validator.rs` (zip/epub structural validation) - - `install/file_installer.rs` (atomic rename, revision replacement) - - keep `install.rs` as a thin facade re-exporting the public API so all - call sites stay valid (open/closed). -- `src-tauri/src/opds/downloader.rs` (829) -> extract HTTP streaming and - hash-verification into `downloader/verify.rs`; progress-event emission into - `downloader/progress.rs`. -- Introduce a `ContentVerifier` trait in `opds/mod.rs` (hash strategy behind - an interface; sha256/md5 implementations injected). Downloader depends on - the trait, not concrete types (DIP). - -Acceptance: line counts of new modules < 400 each; public API unchanged; -all 200 cargo tests green without modification (tests may move with code). - -### M2 — Rust persistence + commands (SRP, repository pattern) - -- `src-tauri/src/persist/store.rs` + `queries.rs`: formalize a - `LibraryRepository` trait; SQLite impl stays, callers depend on trait. -- Deduplicate SQL row-mapping helpers repeated across queries.rs (DRY): - one `RowMapper` module. -- Error handling: replace remaining string-y errors in opds command layer - with typed variants of existing error enums (no behavior change). - -### M3 — Frontend state & services (DRY, single source of truth) - -- `OpdsCatalogScreenContainer.tsx` (258 lines, 12+ useState): extract - download-state machine into `useDownloadRegistry` hook; catalog connection - state into `useCatalogConnection`. Container becomes composition only. -- `PublicationDetailModal` + `OpdsPublicationCard`: shared format-menu logic - already extracted; deduplicate acquisition-link selection logic into - `types/opds.ts` helper (single source for "which link do I download"). -- Services: unify error-toast patterns across opdsClient/offlineLibrary via - one `notifyOpdsError` util. - -### M4 — Comprehensive test suites + CI - -Rust: -- Unit tests colocated for every new module from M1/M2 (trait mocks for - ContentVerifier / LibraryRepository). -- Integration test: end-to-end download pipeline against a local fixture - server (wiremock), covering .part -> verify -> atomic rename -> revision. - -Frontend: -- Vitest for every new hook (useDownloadRegistry, useCatalogConnection): - happy path, error path, race/cancel path. -- Component tests for PublicationDetailModal format menu incl. a11y. - -CI (.github/workflows/ci.yml): -- Add `cargo clippy -- -D warnings` job step (deny new warnings). -- Add `cargo fmt --check` step. -- Keep e2e compile gate; add job comment documenting adb-based smoke plan. - -Coverage expectation: every public function of every new module has at least -one test; every bug fixed during refactor gets a regression test. - -### M5 — Android on-device verification (gate for "done") - -Prereq: Infisical secrets fetched (`pnpm secrets:fetch`), NDK present. -- Build release APK, install to Pixel 7 (adb 28261FDH200F50). -- Scripted adb smoke: launch app, connect to local Grimmory catalog, - browse, download one publication, confirm file exists in app storage, - force-stop and relaunch (offline library restore path works). -- Screenshot/log evidence recorded in PR description. -- Any failure = reopen the responsible milestone; device verification is - the definition of "not broken". - -## Execution protocol per milestone - -1. Pilot writes bounded prompt; OpenCode implements on the milestone branch - in its own worktree (parallel-safe across milestones where files are - disjoint: M1+M2 backend vs M3 frontend can run concurrently). -2. Pilot independently runs the full validation gate. -3. PR opened; CI must pass; pilot reviews diff for scope creep. -4. Squash-merge after review; board item updated. diff --git a/agent_docs/android-hardening.md b/agent_docs/android-hardening.md deleted file mode 100644 index d7aa35e..0000000 --- a/agent_docs/android-hardening.md +++ /dev/null @@ -1,190 +0,0 @@ -# Android Hardening Notes - -Status: Milestone 7 implementation notes for the Grimmory client roadmap. -Scope: OPDS credential storage, legacy permission and service removal, -download lifecycle on Android, and restart safety guarantees. - -## Verified targets - -| Target | Command | Result | -|---|---|---| -| Desktop (x86_64-unknown-linux-gnu) | `cargo test --manifest-path src-tauri/Cargo.toml` | 200 passed | -| Desktop (x86_64-unknown-linux-gnu) | `cargo check --manifest-path src-tauri/Cargo.toml` | pass | -| Web frontend | `pnpm vitest run` | 147 passed | -| Android (aarch64-linux-android) | `cargo check --target aarch64-linux-android --lib` | blocked, see below | - -The Android Rust cross-check is blocked by this environment, not by the code. -`rustup target add aarch64-linux-android` succeeds and dependency resolution -for the target completes, but build scripts of native dependencies (bundled -SQLite, ring) require the NDK C toolchain (`aarch64-linux-android-clang`), -which is not installed here. With `ANDROID_NDK_HOME` configured the same -command should be run before release. The android-only JNI module was type -checked against `jni 0.21.1` on the host target to validate its API usage; -runtime verification still requires a device or emulator smoke test. - -## OPDS credential storage - -### Evaluation - -1. Official Tauri keystore plugin: none exists today. The Tauri plugins - ecosystem has no maintained Android Keystore plugin, so the "use a plugin" - option is unavailable. -2. tauri-plugin-stronghold: encrypted at-rest storage, but it is keyed by a - password that we would have to persist somewhere, which moves the problem - rather than solving it. -3. Encrypted storage keyed by the Android Keystore (chosen): an AES/GCM key is - generated inside the Android Keystore with - `setBlockModes(GCM)`, `setEncryptionPaddings(NONE)`, and no user gate. - Plaintext secrets are sealed and opened in Kotlin - (`SecureCredentials.kt`); Rust exchanges only opaque base64 blobs over JNI - (`src-tauri/src/credentials/android_keystore.rs`). Ciphertext lives in a - JSON file inside app-private storage (`opds-credentials.json`), so device - encryption plus the non-exportable key protect the data at rest. - -### Abstraction - -All of this sits behind two traits in `src-tauri/src/credentials/mod.rs`: - -- `CredentialCipher`: `seal(plaintext)` and `open(sealed)` with no key export. -- `CredentialStore`: `save`, `load`, `delete`, keyed by provider, origin, and - username. - -Implementations: - -- `InMemorySessionStore`: desktop default. Session scoped, nothing touches - disk, matching the pre-hardening flow where credentials live only in the UI - session and are passed per command call. -- `EncryptedFileStore`: persists sealed blobs atomically (temp file plus - rename). A corrupted file reports `CredentialStoreError::Corrupt`; a lost or - rotated keystore key surfaces as `CredentialStoreError::Cipher` on load so - callers can prompt for re-entry instead of failing silently. -- `MockKeystoreCipher`: deterministic XOR plus base64 cipher used by unit - tests to emulate keystore loss across instances. - -Tauri commands: `opds_save_credential`, `opds_load_credential`, -`opds_delete_credential`. On desktop these resolve to the session-only store; -on Android they resolve to the encrypted file store. If opening the encrypted -store fails at startup, the app falls back to the session-only store and logs -an error rather than ever writing plaintext to disk. - -The OPDS browse screen currently keeps credentials in component state only. -Automatic session restoration from the secure store is intentionally deferred; -no code path persists OPDS passwords outside this abstraction. - -### Leak prevention - -- `CatalogConfig` implements `Debug` manually: username and password print as - `***` in any diagnostic formatting. Serialization also skips both fields. -- `OpdsCredentials` redacts the password in `Debug`. It deliberately keeps a - plain `Serialize` implementation because loading a stored credential must - return the real secret to the caller; the type must therefore never be - passed to logging or tracing sinks. -- Credential store errors are static strings; they never embed account data. -- No `log!` call site receives a password; the transport error sanitizer in - `commands/opds.rs` maps auth failures to fixed messages. -- Unit tests assert that debug output, serialized output, and the encrypted - store file never contain the plaintext password. - -## Android permission and service audit - -Removed (legacy peer architecture only): - -- `BLUETOOTH`, `BLUETOOTH_ADMIN`, `BLUETOOTH_SCAN`, `BLUETOOTH_ADVERTISE`, - `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION`: BLE discovery. -- `CHANGE_WIFI_MULTICAST_STATE`, `ACCESS_WIFI_STATE`: mDNS multicast lock. -- `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE`, - `READ_MEDIA_IMAGES/VIDEO/AUDIO`, `requestLegacyExternalStorage`: old library - import paths. Downloads now write under app-private storage only, so no - runtime storage permission is requested. -- `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC`, `WAKE_LOCK`: served - `HostForegroundService`, which advertised the peer host role. -- `HostForegroundService.kt` and `SyncWorker.kt` deleted along with their - manifest entries and the `androidx.work` dependency. -- `MainActivity.kt` no longer acquires multicast locks, starts hosting - services, schedules auto-sync workers, or prompts for storage permissions. -- Capability audit: `http:default` removed from `capabilities/default.json`; - the frontend never imports `@tauri-apps/plugin-http` and the Rust side never - registers the plugin. The unused `tauri-plugin-http` dependency was dropped - from `Cargo.toml`. - -Kept: - -- `INTERNET` plus `ACCESS_NETWORK_STATE`: required for OPDS catalog access and - downloads. -- Leanback feature declarations: launcher visibility only. - -Desktop behavior is unchanged: the Axum host server, mDNS discovery, tray, -and BLE modules still compile and start on desktop targets only. In -`lib.rs` setup, the host server and mDNS spawn now sit behind `#[cfg(desktop)]` -so mobile builds do not run peer services that no longer have permissions. - -## Download lifecycle - -Current implementation: one streamed HTTPS transfer per publication inside -the app process (`download_opds_publication`), written to a unique `.part` -file, renamed after content-type and length checks. The newer verified -pipeline (`download_verified_epub`) adds hash verification, durable job rows, -retry classification, and old-revision retention but currently covers EPUB -only and is not yet wired to a command. - -### Backgrounding - -Downloads run while the process is scheduled. When Android backgrounds and -freezes the app mid-transfer, the socket dies and the command resolves with a -network error, which emits a `Failed` progress event and shows the failure in -the UI when the user returns. There is no silent half-complete state: the -`.part` fragment is either cleaned up by the downloader's error path or swept -at next startup. Users should keep the app foregrounded for large transfers -until the deferral below lands. - -WorkManager or a user-visible foreground service is therefore not wired up in -this milestone. Rationale: the exposed transfer is a single in-process stream -without resumable range support, so surviving backgrounding would require -either a foreground service notification surface or redesigning the pipeline -around WorkManager with HTTP range resume plus durable job rows. That work -belongs with wiring `download_verified_epub` into the command layer, which -already provides the persistence side (job states, interrupted marking). -Deferred deliberately rather than shipping a foreground service that would -reintroduce service permissions this milestone just removed. - -### Cancellation - -Cancellation now propagates end to end: - -- The backend registers a `CancellationToken` per active publication id. -- `opds_cancel_download(publication_id)` cancels the token; the transfer - future is raced against it, so the response stream stops immediately - instead of draining in the background, and the slot is released. -- The frontend cancel button invokes the command and resets to idle; a - cancelled transfer can never report completion. -- A cancelled transfer leaves its `.part` fragment behind by design; sweeping - it immediately could delete a concurrent transfer's fragment, so cleanup - waits for the startup sweep, which only runs when nothing is in flight. - Tests cover token firing mid-stream, registry release, and unknown ids. - -### Restart and process death safety - -On every startup `restore_library_on_startup` runs before the offline library -becomes available to commands: - -1. `recover_interrupted_jobs` marks every `queued` or `running` job row as - `interrupted` with a finished timestamp and the reason "interrupted by - application restart". Because the database commit happens before the final - rename in the verified pipeline, a killed process can never leave a - complete-looking record without a verified file. -2. `cleanup_stale_part_files` walks the content root and removes only files - carrying the `.part` marker, through the root containment helper, while no - download is in flight. -3. Library classification maps `Interrupted` jobs to the failed section of - the offline library view, so an interrupted download reappears as a visible - failed entry the user can retry explicitly. - -Practical consequences for Android: - -- Process death during a download loses only bytes already received; the next - start reports the job as interrupted and removes partial fragments. -- Complete revisions survive restarts untouched; replacement failures leave - the previous verified file intact. -- Rotation does not interrupt transfers because they run in the Rust runtime, - not in activity scope; the progress events are simply re-listened after the - webview reloads. diff --git a/agent_docs/BRAND.md b/docs/design-system.md similarity index 54% rename from agent_docs/BRAND.md rename to docs/design-system.md index c3780b3..cb59ad6 100644 --- a/agent_docs/BRAND.md +++ b/docs/design-system.md @@ -1,36 +1,31 @@ -# ShelfSync Brand & Design System v2 +# ShelfSync Design System -The app pivoted from P2P sync to a focused OPDS library client. The old Nord-based -DaisyUI theme and book-with-arrows logo belong to the previous product. This document -defines the new brand from scratch. +Canonical, maintained reference for the ShelfSync visual language. This is the +single source of truth for brand tokens; implementations should mirror it: -## Who uses this app +- `src/design/tokens.ts` — typed semantic tokens (paper, lamplight, e-ink) +- `src/App.css` — DaisyUI themes (`paper`, `lamplight`) plus `e-ink` fallback -ShelfSync serves one archetype: **the self-hoster reader**. Someone who runs -Grimmory/Calibre-Web/Kavita on their own hardware, reads on a phone or tablet in the -evening, downloads 2-3 books a week over their LAN or tailnet, and cares that: +## Brand direction + +ShelfSync serves the **self-hoster reader**: someone who runs +Grimmory/Calibre-Web/Kavita on their own hardware, reads on a phone or tablet, +downloads a few books a week over their LAN or tailnet, and cares that: 1. Their books are theirs (no cloud, no account, no tracking) 2. The download-and-read loop takes seconds, not clicks 3. The app feels calm. They open it tired, before bed. -Competitor research (BookLore, Kavita, Stump, Calibre-Web reviews) shows the #1 -praised quality in this space is "modern and clean, doesn't overwhelm." The #1 -complaint about older tools (Calibre desktop, Ubooquity) is "clunky, feels like an -afterthought." Users reward restraint. - -## Brand emotion - -**A well-lit reading room at night.** Quiet confidence, not excitement. The feeling -of a favorite chair, a warm lamp, and a shelf that's exactly where you left it. +**A well-lit reading room at night.** Quiet confidence, not excitement. A +favorite chair, a warm lamp, a shelf that's exactly where you left it. Three words: **calm, owned, warm.** - Calm: low-chroma surfaces, generous whitespace, no badges or gradients shouting -- Owned: your server, your library, your files. The UI should feel like furniture, - not a SaaS dashboard -- Warm: paper-toned light theme, lamplight amber dark theme. Reading is an evening - activity; the app should feel like it +- Owned: your server, your library, your files. The UI should feel like + furniture, not a SaaS dashboard +- Warm: paper-toned light theme, lamplight amber dark theme. Reading is an + evening activity; the app should feel like it ## Design tokens @@ -39,7 +34,7 @@ Three words: **calm, owned, warm.** | Role | Font | Notes | |------|------|-------| | Display / book titles | **Source Serif 4** | The only serif. Book titles are content, and books are set in serif. UI chrome never uses it | -| UI / body | **Outfit** | Already loaded; geometric, friendly, not Inter | +| UI / body | **Outfit** | Geometric, friendly, not Inter | Weights: Outfit 400/500/600/700. Source Serif 4 600/700 (titles only). @@ -64,35 +59,36 @@ interactive elements only. Everything else is paper and ink. | warning | `#b08a3e` | `#d4b06a` | | error | `#b0563f` | `#d98a72` | -All accent-adjacent colors stay under 45% saturation (taste-skill §4.2: max 1 accent, -saturation discipline). No purple, no gradients, no glow. +Keep accent-adjacent colors under 45% saturation: no more than one accent, +with strict saturation discipline. No purple, no gradients, no glow. ### Shape -**One corner system: soft.** 12px cards, 8px fields, 999px pills for status badges -only. No mixed square/pill buttons. +**One corner system: soft.** 12px cards, 8px fields, 999px pills for status +badges only. No mixed square/pill buttons. ### Spacing & density -Book grid is the hero: covers are large (min 140px wide), gutters generous (24px). -Chrome (header, footer, forms) is compact. Density lives in the content, not the UI. +The book grid is the hero: covers are large (min 140px wide), gutters generous +(24px). Chrome (header, footer, forms) is compact. Density lives in the +content, not the UI. ### Motion -Purpose-only animation: page transitions fade-slide 200ms, downloads show a progress -fill on the card cover, toasts slide up 150ms. Nothing decorative. E-ink mode keeps -its zero-motion guarantee. +Purpose-only animation: page transitions fade-slide 200ms, downloads show a +progress fill on the card cover, toasts slide up 150ms. Nothing decorative. +E-ink mode keeps its zero-motion guarantee. ## Logo -Concept: **an open book seen from the spine, forming the silhouette of a lit lamp** -(positive space). Alternatively: a book whose pages curve into a lamp shade. Flat, -two-tone: ink + amber. No gradients, no arrows (the old sync arrows are gone; this -app no longer "syncs", it shelves). +Concept: **an open book seen from the spine, forming the silhouette of a lit +lamp** (positive space). Alternatively: a book whose pages curve into a lamp +shade. Flat, two-tone: ink + amber. No gradients, no arrows. This app no +longer "syncs", it shelves. ## Iconography -Lucide, 1.5px stroke, rounded caps. Already in use; keep. +Lucide, 1.5px stroke, rounded caps. ## Anti-patterns (banned) diff --git a/src/App.css b/src/App.css index ce52097..c146e4e 100644 --- a/src/App.css +++ b/src/App.css @@ -7,7 +7,7 @@ --safe-area-left: env(safe-area-inset-left, 0px); --safe-area-right: env(safe-area-inset-right, 0px); - /* Brand fonts (agent_docs/BRAND.md) */ + /* Brand fonts (docs/design-system.md) */ --font-ui: "Outfit", system-ui, -apple-system, sans-serif; --font-display: "Source Serif 4", Georgia, serif; @@ -115,7 +115,7 @@ html.e-ink .shadow-2xl { /* * Theme: "paper" (light) — a warm reading room in daylight. - * Paper neutrals, ink text, lamplight-amber accent. See docs/design/BRAND.md. + * Paper neutrals, ink text, lamplight-amber accent. See docs/design-system.md. */ @plugin "daisyui/theme" { name: "paper"; diff --git a/src/__tests__/design/tokens.test.ts b/src/__tests__/design/tokens.test.ts index d948a2a..e3ec936 100644 --- a/src/__tests__/design/tokens.test.ts +++ b/src/__tests__/design/tokens.test.ts @@ -117,7 +117,7 @@ describe("brandTokens — Brand v2 semantic token contract", () => { } }); - it("documents the typography roles from BRAND.md", () => { + it("documents the typography roles from docs/design-system.md", () => { expect(brandTokens.typography.ui).toContain("Outfit"); expect(brandTokens.typography.display).toContain("Source Serif 4"); expect(brandTokens.typography.ui).not.toBe(brandTokens.typography.display); diff --git a/src/design/tokens.ts b/src/design/tokens.ts index cb095c9..02f3986 100644 --- a/src/design/tokens.ts +++ b/src/design/tokens.ts @@ -2,7 +2,7 @@ * Brand v2 semantic design tokens. * * Single, typed source of truth for the ShelfSync design language, derived from - * agent_docs/BRAND.md and mirrored by the DaisyUI themes in src/App.css + * docs/design-system.md and mirrored by the DaisyUI themes in src/App.css * (`paper`, `lamplight`) plus the `e-ink` fallback. Every value here must stay * in sync with src/App.css; the DaisyUI class names those themes power * (`bg-base-100`, `border-base-300`, `text-base-content`, ...) are the