5c03da2ccc
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 6377faf332 |
#306: instance branding — logo and favicon, cropped in the browser
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 7m28s
CI / Build container images (pull_request) Successful in 2m7s
CI / Auth e2e pack (pull_request) Successful in 9m37s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Build and push images (push) Successful in 23s
CD / Deploy to Test (push) Successful in 12s
CD / Smoke tests against Test (push) Successful in 1m47s
CD / Promote to Int (push) Successful in 16s
CI / Lint, typecheck, test (push) Successful in 7m25s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 9m41s
CI / Import/export fidelity gate (push) Successful in 1m12s
An instance had no way to look like itself: the top bar said "Dorfteich" whatever the operator called their instance, `instance.name` was never rendered in the running app at all, and there was no favicon anywhere — `index.html` had no `<link rel="icon">` and `public/` held only fonts and theme-init.js. Where the line is drawn, and why: - **The api never decodes an image.** Cropping, scaling and the conversion to PNG happen on a canvas in the browser; the api checks the PNG signature, reads the IHDR dimensions at their fixed offsets and enforces the caps. An image library would put a decoder in front of attacker-supplied bytes AND would have to be carried through the `--network none` offline build. Reading two big-endian integers is not decoding. - **SVG is refused**, with its own error message rather than a generic "not a PNG": it can carry script, and serving it from our own origin would be a cross-site-scripting vector. An operator who tried one should learn that it is deliberate. - **The crop is driven by number inputs, not by dragging.** A drag-only cropper excludes keyboard and switch users outright; a number input is arrow-key operable and screen-reader readable without any custom aria. The resulting pixel size is stated in text, not only drawn as a frame. - **The variant is chosen by CSS, not JavaScript.** `theme-init.js` has already resolved `data-theme` before first paint, so the correct logo is the one painted rather than the one that appears after a flash. Without a dark variant the LIGHT logo carries both themes — the operator's own asset shown unchanged beats one they did not choose (the rule #307 extends to ponds). The settings screen warns; it never blocks. - **The favicon link is static, its resource dynamic.** index.html stays a static file and the api answers with the uploaded icon or a shipped default — that route must never 404, or the browser keeps its generic icon for good. The default is generated by a script from Node's own zlib (`gen-default-favicon.mjs`), for the same offline-build reason. - Both favicon sizes are uploaded together: one source, one crop, so the tab icon and the home-screen icon can never disagree. - Branding is served WITHOUT a session, because the login screen carries it and the browser fetches the favicon before anyone signs in. The admin screen says so — an operator may not expect their logo to be public. - The metadata is not writable through the settings endpoint: it describes bytes on disk, and hand-writing it would claim an asset that is not there. `./data/branding` follows the three-step rule #303 paid for: env default + `data-dirs.ts` entry, compose volume (repo AND the stages on ONE), and the `mkdir`/`chown` line in the api Dockerfile. `data-dirs.test.ts` is new and closes the hole that made #303's variant invisible: the nightly archive skips a missing directory WORDLESSLY, so the fence now demands that every `*_DIR` the backup env declares actually travels in the archive. Verified against the real defect — removing the line fails it by name. Audit catalogue v1.7 (`branding.changed`), carrying `scope` from the start so #307 is the same event with a different scope, not a second id. Verified: api suite 103 files green (a lone `public-api` ECONNRESET under local parallel load, green in isolation — the documented local flake); branding suite 12 tests against a real directory; crop arithmetic unit tests; a11y pack 11/11 in both schemes; /admin measured at 320px with the new section (overflow 0); and the whole flow walked in the browser: upload → crop 780×180 → stored as 512×118 → logo in the sidebar linking home with the instance name as its accessible name → topbar wordmark following `instance.name` → light logo still shown under `data-theme="dark"`. |
|||
| 5a4a99196e |
#300: route icon-only controls through IconButton/IconLink
All checks were successful
CI / Auth e2e pack (pull_request) Successful in 8m36s
CI / Import/export fidelity gate (pull_request) Successful in 58s
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Build container images (pull_request) Successful in 3m51s
CD / Build and push images (push) Successful in 15s
CD / Deploy to Test (push) Successful in 16s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 13s
CI / Lint, typecheck, test (push) Successful in 6m32s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m25s
CI / Import/export fidelity gate (push) Successful in 58s
The notification bell sat higher and larger than search and the theme toggle next to it. The cause was not the glyph: `.notifications-bell__button` carried its own rules with neither flex centring nor an icon size, so the svg was laid out inline on the text baseline and rendered at lucide's 24px default instead of the 1.15rem the shared `.icon-button` enforces. Route every icon-only control through the shared components instead: - `IconLink` joins `IconButton`, sharing one class helper. Three controls navigate (pond settings, graph, trash) and are links, not buttons — without a link twin they would have stayed the one group gluing the class on by hand. - 17 hand-applied `className="icon-button …"` usages across nine files now go through the components, which is what enforces the accessible name on a control that shows only an icon. - The bell's unread count reaches assistive technology. The badge sits inside the control, so `aria-label` hid it and a screen reader announced "Notifications" without ever saying how many. An ESLint rule keeps it that way: `icon-button` on a raw button, anchor or Link is now an error, in both string and template-literal form. The plugin uninstall button keeps a title that differs from its name (it explains why a required plugin is locked); IconButton spreads rest last, so the explicit title still wins. Also drops the graphify block from CLAUDE.md — it duplicates the workspace-level instructions. |
|||
| 2077d92c09 |
#166: Skip-Link, verstecktes Seiten-h1, Resizer in die Nav-Landmarke
Skip-Link als erster Tab-Stopp springt auf #main; die angemeldete Seitenansicht bekommt ein visually-hidden h1 (der sichtbare Titel ist ein Input, der jetzt auch ein aria-label trägt); der Sidebar-Resizer wandert in die nav-Landmarke (absolut an der Kante positioniert), damit kein Inhalt außerhalb von Landmarken liegt. Zwei e2e-Locator auf das Sidebar-Formular gescoped — das Editor-Titelfeld matcht seit dem neuen Label ebenfalls auf /title|titel/i. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 057992faaf |
#164: ARIA-Semantik — Editorfläche, Autocomplete-Listboxen, Sidebar, Toolbar
Die Editorfläche bekommt einen lokalisierten zugänglichen Namen und ist im Lesemodus role=document statt eines unbenannten Textfelds (setOptions im selben Layout-Effekt wie setEditable). Eingeklappte Sidebar zusätzlich inert (aria-hidden allein ließ fokussierbare Kinder im Tab-Weg). Die li-Zwischenknoten der Listboxen (Wikilink-/Mention-Autocomplete, Suchergebnisse) sind role=presentation, damit listbox→option wieder eine gültige Eltern-Kind-Beziehung ist. Toolbar: Pfeiltasten-Navigation über die Controls (native Selects behalten ihre Pfeiltasten) und ein sprechendes Toolbar-Label statt des Absatz-Buttons-Labels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq |
|||
| 6c98a71d34 |
Favorites: personal page stars, golden icons, sidebar filter (#132)
Some checks failed
CD / Build and push images (push) Successful in 3m57s
CD / Deploy to Test (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m32s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m14s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Failing after 2m51s
CI / Import/export fidelity gate (push) Has been skipped
Semantics changed from the issue during planning (documented there, comment 1192): favorites are PERSONAL per user, not pond-wide — the sys-fav label approach is dropped entirely. Storage is a page_favorites table (userId+pageId, FK cascade); PUT/DELETE /pages/:id/favorite toggles idempotently and needs read access only (#60 404 semantics — a star is a note-to-self, not a page modification), GET /ponds/:id/favorites lists the account's stars sliced to still-readable pages. Trashed pages keep their rows, so restore keeps the star; purge cascades it away. Web: one shared ['favorites', pondId] query feeds the TopBar star (between labels and history, golden when set), the golden tree icons in the sidebar, and a latching "Favorites" filter button next to the view switch that narrows either view (combinable with the label filter). No public-API/MCP exposure — with the label approach gone, that parity is no longer free; favorites stay UI-only for now. New favorites e2e pack (star toggle, golden icon, filter, per-user isolation) wired into CI; DB suite covers the round-trip, read gating, and the trash/restore/purge lifecycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| 44a90a53ac |
Sidebar: pin the four actions as an icon row at the bottom
Graph, new page, import, and trash collapse from scattered text links into one icon row pinned to the sidebar's bottom edge, in that order, each with a hover hint (the trash reads "Papierkorb anzeigen"). The new-page button now toggles the inline form, which still renders above the footer with the same classes; the import trigger becomes an icon whose progress list floats above the row so the icons stay put. All e2e class hooks (.sidebar__new-page, .sidebar__graph-link, .sidebar__import-*) are unchanged. Fixes #124 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| d4fb2a3c51 |
Sidebar tree: stack parents above children, compact rows, icons
The tree reused .sidebar__page-item's flex ROW from the flat list, so the nested children <ul> sat BESIDE the parent row and align-items: center made the parent float vertically centered next to its subtree. Stack the two in tree view instead: parent first, children indented below (about two characters per level). Rows get compacter (smaller font, tighter padding) so a deep imported vault fits on screen, and folder pages now read differently from leaf pages at a glance: lucide Folder/FolderOpen vs FileText, with the caret glyph upgraded to a ChevronRight that keeps the existing CSS rotation. Caret button, classes, and aria-labels stay untouched for the e2e contract (#101 convention: no page title in the caret label). Fixes #122 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fb2VzvcoBPHkjh8bZ6PzQn |
|||
| dbbe229cb9 |
Pond knowledge graph view (#112)
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
/p/:pondSlug/graph (static segment ranked above :pageSlug, same documented reserved-slug gap as trash/settings) renders the pond's readable wikilink graph from GET /ponds/:id/links: pages as nodes colored by their first label (legend included, DEFAULT_LABEL_COLOR for unlabeled), resolved links as edges, phantom targets as dashed nodes — clicking one offers to create the page, which resolves its links. Rendering is a self-contained SVG force graph: only d3-force is bundled (no d3 DOM/zoom modules, zero external requests); the layout runs synchronously to rest, zoom/pan/node-drag are plain pointer math. SVG over canvas deliberately — every node carries a data-testid the e2e packs can click. Ponds beyond 500 pages get a capped-view notice. Sidebar footer links every member to the graph (trash stays owner-only). New i18n namespace graph (de+en). Verified live: nodes/edges/legend render, node click opens the page, phantom click creates it and the node turns solid. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 0308bc712d |
Drag-onto reparent, Move-to dialog, and the delete decision (#109)
Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CD / Build and push images (push) Successful in 3m58s
CD / Deploy to Test (push) Successful in 9s
CI / Lint, typecheck, test (push) Successful in 4m23s
CI / Build container images (push) Has been skipped
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Has been cancelled
Sidebar folder view: a row now has three drop bands — the edges keep the within-group reorder, the middle band nests the dragged page under the row (appended to its new sibling group, with a drop-into outline cue). Cycle/depth refusals surface as a translated banner; successful moves are announced for screen readers. The overflow menu gains 'Move to…': a modal parent picker over the page tree (top level first, the page's own subtree disabled) that works in every sort mode. Delete now decides per case: childless pages keep the plain confirm; pages with subpages open a dialog offering promote (default wording: move subpages up) or subtree delete. The children lookup reads the CACHED pages list on purpose: an async fetch before window.confirm broke the click→confirm→DELETE rhythm the content pack (and users) rely on, and a stale childless read errs toward promote — never toward a silent subtree delete. Sidebar caret labels deliberately exclude the page title: accessible names are matched by substring in the specs (#101), and a title like 'Editor…' collided with the edit-mode toggle. Verified live: move dialog (subtree option disabled), promote and subtree delete flows; content/trash/export packs green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 15184876bd |
Sidebar folder view, label view, and the view toggle (#108)
Some checks failed
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CD / Build and push images (push) Has been cancelled
CI / Lint, typecheck, test (push) Has been cancelled
The sidebar now presents pages as a collapsible tree built from parentId (folder view) or grouped under the hierarchical label tree (label view, read-only; multi-label pages appear under each label, untagged ones in an 'unlabeled' group). The pond owner sets the default via a new sidebarView pond setting (PATCH-merged like the other keys); every user can override it locally (ui.sidebar.view.<pondId>), and the toggle sits above the page list. Collapse state persists per pond. New pages created while a page is open become its children — the inline form says so and sends parentId. Reordering (buttons and drag-between) now operates within one sibling group; the label filter stays a folder-view feature and falls back to the flat list while active, so the filtered order is never mistaken for a partial tree. SidebarContent is keyed by pond id so the per-pond localStorage hooks mount with the right key. e2e hooks (.sidebar__pages, .sidebar__page, reorder buttons) kept; reorder/labels/content packs green locally, plus a live smoke of nesting, collapse persistence, and both views. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 33121cd73d |
Move pond settings to a TopBar gear, pin the trash link to the sidebar bottom (M10 follow-up)
All checks were successful
CD / Build and push images (push) Successful in 1m8s
CD / Deploy to Test (push) Successful in 11s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
CI / Lint, typecheck, test (push) Successful in 3m42s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 5m31s
CI / Import/export fidelity gate (push) Successful in 47s
- owners get a Settings icon next to the pond name while a pond route
is active (same lucide set, localized aria-label/tooltip via the
existing labels:link key); gone on non-pond routes and for non-owners
- the trash stays a text link but pins to the sidebar's bottom
(.sidebar is a flex column now; .sidebar__footer uses margin-top:auto)
- trash.spec: delete flows go through the #101 overflow menu (was
missed in
|
|||
| e2f942c0ff |
Add document import UI in the sidebar (#64)
All checks were successful
CD / Build and push images (push) Successful in 3m43s
CI / Lint, typecheck, test (push) Successful in 2m56s
CI / Auth e2e pack (push) Successful in 3m53s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 10s
CD / Smoke tests against Test (push) Successful in 1m20s
CD / Promote to Int (push) Successful in 11s
An "Import document" action in the pond sidebar: pick a .docx/.odt/.md file
(or several), upload with per-file progress, and open the new page. A
.docx/.odt polls the conversion job (queued → converting → done); a .md
imports directly and comes back already succeeded. Failures stay listed with
the localized error and a retry; concurrent imports all complete and appear.
- web apps/web/src/import/: useImport hook (upload via apiUploadFile → poll
GET /jobs/:id → resolve the page slug → navigate; first success of a batch
navigates, every success refreshes the sidebar) and ImportControl (hidden
file input, accept from shared IMPORT_EXTENSIONS, per-file status list).
Wired into Sidebar next to "new page"; `import` i18n namespace (de+en).
- api: ImportService accepts .md/.markdown and imports in-process (no job),
returning a succeeded ConversionJobView with the created resultPageId
("Markdown imports directly"); the media+parse+create tail is now shared
between the job path and the sync path (createPageFromMarkdown), and a
conversion error on the sync path maps to an HTTP status. shared
IMPORT_EXTENSIONS gains md/markdown.
- e2e apps/web/e2e/import.spec.ts + CI step: .docx corpus fixture opens the
converted page (self-skips without a reachable pandoc sidecar — CI's e2e
stack has none, same as #63; verified locally + on stage), .md opens
directly, an unsupported .txt shows the localized error with no page
created, and two concurrent .md imports both complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
|
|||
| 69b00fcf2f |
Add manual page ordering with drag-and-drop (#45)
All checks were successful
CD / Build and push images (push) Successful in 3m1s
CI / Lint, typecheck, test (push) Successful in 2m13s
CI / Auth e2e pack (push) Successful in 2m36s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Enable the third sidebar sort mode — a freely defined order. - api: `PATCH /pages/:id/position` (before/after neighbour) recomputes only the moved page's fractional `sort_key`. Pure `sort-key.ts` helpers (`nextKeyOrRebalance`, `evenlySpacedKeys`) decide between the cheap single-key path and a full pond rebalance to evenly-spaced keys when a key would exceed MAX_SORT_KEY_LENGTH or the client's neighbours are stale; rebalance runs in one transaction. Order is server-authoritative. - web: enable 'manual' in the sort-mode switch; in manual mode the owner can reorder via native drag-and-drop (drop above/below by pointer half) or the keyboard (per-row up/down buttons), each announced through an aria-live region. Reordering is hidden while a label filter narrows the list. New pages already append at the end (create uses generateKeyBetween(last, null)). Pure `reorder.ts` neighbour helpers, unit-tested. - i18n: manual sort mode + reorder strings (de + en). - tests: sort-key property test (10.000 adversarial reorders never collide or overflow — rebalance verified); reposition db test (persist, server-order, sort-mode switch keeps manual order); reorder e2e pack (keyboard reorder persists across reload + identical on a fresh read; aria-live announced). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY |
|||
| 03e72242d3 |
Add label UI: tree management, page assignment, and sidebar filter (#44)
All checks were successful
CD / Build and push images (push) Successful in 2m53s
CI / Lint, typecheck, test (push) Successful in 2m8s
CI / Auth e2e pack (push) Successful in 2m31s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Build the M4 label experience on top of the #43 label API. - shared: `flattenLabelTree` (tree → depth-first list) for chip lookup, filtering, and the picker; `PageListItemView` adds each page's `labelIds` to the sidebar list response. - api: `GET /ponds/:id/pages` now includes `labelIds` per page (one grouped query), so the sidebar can render chips and filter without extra calls. - web: - Pond settings page (`/p/:pondSlug/settings`) with a `LabelManager` tree: inline create, rename, recolour (`<input type=color>`), move via a parent picker that excludes the label's own subtree, and delete that confirms then force-detaches assigned pages. Every control is a native button/input/select — the tree is fully keyboard-operable. - `LabelPicker` panel on the page editor: searchable, hierarchy-indented multi-select that assigns/unassigns immediately and refreshes the page's labels and the sidebar. - Sidebar: colored label chips on page entries (readable text via a luminance-based contrast helper) and a descendant-inclusive label filter (selecting a parent matches pages tagged with its children, via the shared `collectSubtreeIds`). Owner link to pond settings. - i18n `labels` namespace (de + en). - e2e `labels.spec.ts` (new CI pack): full lifecycle from the settings UI and picker-assign + parent-filter-includes-child. Selectors are language-independent because the UI language follows the user's locale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY |
|||
| a645763679 |
Add page trash: soft delete, restore, and purge job (#31)
All checks were successful
CD / Build and push images (push) Successful in 2m5s
CI / Lint, typecheck, test (push) Successful in 1m45s
CI / Auth e2e pack (push) Successful in 1m50s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 10s
Backend: a generic maintenance-job scheduler (SchedulerService, `jobs` table) that any later maintenance job registers with instead of growing its own timer loop. Due-ness and the run-mutex both live in the DB row (`lastRunAt` survives a restart; claiming a due job is one atomic `UPDATE ... WHERE status != 'RUNNING'`), and an injectable ClockService lets tests simulate retention elapsing without waiting or faking the global clock. Trash endpoints: GET /ponds/:id/trash (list), POST /pages/:id/restore, DELETE /pages/:id/purge (manual, bypasses retention) — all sharing the same purge logic as the scheduled daily job (default 30-day retention, new trash.retentionDays instance setting). Purging deletes a page's content cache, update log, and attachment files/quota; page_versions is a placeholder until M3 exists. Direct navigation to a trashed page now 404s with a distinguishable `page_trashed` code for editors (a plain 404 for everyone else) instead of the generic not-found. Attachment.pageId — added in #27 but never wired up — now gets set on every page state save to whichever page's document currently embeds the file, which is what lets purge find a page's files. Frontend: a per-pond trash view (restore/purge), a "move to trash" action with confirmation in the page menu, and a trash link in the sidebar for pond owners. Also fixes react-query retrying 4xx responses for several seconds by default, which was masking the trash-hint 404 in the UI (and would have affected any other not-found/permission error the same way). Closes #31 |
|||
| 49beb45b3e |
Add pond sidebar with page list, sort modes, and pond switcher (#26)
All checks were successful
CD / Build and push images (push) Successful in 1m59s
CI / Lint, typecheck, test (push) Successful in 1m38s
CI / Auth e2e pack (push) Successful in 1m48s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Successful in 1m10s
CD / Promote to Int (push) Successful in 9s
GET /ponds/:id/pages lists a pond's pages ordered by the pond's persisted sidebarSort setting (alpha/created; manual arrives with #45). The sidebar consumes it to show the page list with an active-page highlight, an owner-only sort switch (persists via the existing PATCH /ponds/:id), and an inline "new page" flow. The top bar gains a pond switcher; a new /p/:pondSlug route gives it somewhere to land, redirecting to the pond's first page once loaded. Sidebar collapse gains a Ctrl/Cmd+\ shortcut and a slightly refined transition. Closes #26 |
|||
| e855192d23 |
Add i18n with i18next, German and English, and a key-parity check
Translation resources live in packages/shared/i18n/<lang>/<ns>.json (common, errors) and ship with de and en. The web app initializes react-i18next with bundled resources (?lng= wins, then the browser language); all shell components use useTranslation and the temporary t() stub is gone. The api localizes its uniform error bodies via a minimal i18next instance negotiated from Accept-Language. `pnpm i18n:check` fails CI when any key is missing in any language, backed by tested helpers in @dorfteich/shared. Closes #5 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|||
| 300a418e85 |
Add React SPA shell with routing, layout, and API status
apps/web becomes a Vite + React application: React Router with home and 404 routes, base layout (top bar, collapsible sidebar remembered per user via localStorage, main area), CSS design tokens including the three font slots from ADR 0016, TanStack Query, and a typed fetch helper showing live API health on the home page. All UI strings go through a t() stub that issue #5 replaces with i18next. The Vite dev server proxies /api to the api dev port (3001). Closes #4 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |