diff --git a/.cursor/rules/ui-interaction-standards.mdc b/.cursor/rules/ui-interaction-standards.mdc new file mode 100644 index 0000000..4d9960f --- /dev/null +++ b/.cursor/rules/ui-interaction-standards.mdc @@ -0,0 +1,25 @@ +--- +description: Gallery UI interaction and component standards for client screens +globs: client/src/**/*.{tsx,css} +alwaysApply: false +--- + +# UI interaction standards + +Follow [Documentation/ui-interaction-and-component-standards.md](Documentation/ui-interaction-and-component-standards.md) (MUST / SHOULD / MAY). Do not invent a second nav, toast, or CSS framework. + +## MUST + +- Drill-down via `HomePage` `View` state. Nested screens get an explicit Back; do not rely on the browser Back button. +- **Back to Timeline** → `goToTimelineHome()` (clears hall, resets year range). **Back to Gallery** → same hall session (camera/wing kept). +- Hide actions the user cannot `can()`; do not leave buttons that 403. +- Visitor chrome strings in `locales/{en,ru}`. Movement colours via `utils/movementColor.ts`. Timeline zoom/pan via `utils/timelineView.ts`. +- Loading: `GalleryLoadingMarker`. Errors/empty states: visible copy, not a blank canvas. +- Modals: `role="dialog"`, visible close, Escape. No nested modals. Destructive actions confirm first. +- Colocated CSS; museum gold `#c9a96e` / navy `/ Georgia`. No MUI/Ant Design. + +## SHOULD + +- Reuse `CatalogSearchBar`, `CuratorLoginModal`, `ArtistFilterModal`, `PaintingLightbox`, `DebugSearchResultsModal`, `DebugUploadButton`. +- Curator filters above the table; expensive work behind an explicit button. +- Update the standards doc when changing Back, search, layout query, or loading behaviour. diff --git a/Documentation/basics.md b/Documentation/basics.md index 52a025c..e301693 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -67,7 +67,8 @@ Gallery/ │ │ ├── utils/debugMode.ts # Debug mode + “Show more” localStorage prefs │ │ ├── utils/timelineView.ts # Shared zoom/pan math for timeline + movements │ │ ├── utils/movementColor.ts # Shared vivid/shade hex helpers for all movement charts -│ │ └── utils/movementTree.ts # View-independent Tree of Art layout engine│ └── dist/ # Production build (served by API when present) +│ │ └── utils/movementTree.ts # View-independent Tree of Art layout engine +│ └── dist/ # Production build (served by API when present) ├── scripts/ # Seed, bios, catalog expansion, image fetch, checkup tools │ ├── seed-wikipedia.js │ ├── seed-catalog-data.js @@ -546,3 +547,4 @@ See [API.md](API.md#authentication) and [data-and-images.md](data-and-images.md# | [tours.md](tours.md) | Guided tours — editor, public popup, 3D tour hall | | [i18n-russian.md](i18n-russian.md) | Russian UI + entity_translations | | [data-and-images.md](data-and-images.md) | Image pipeline and seeding | +| [ui-interaction-and-component-standards.md](ui-interaction-and-component-standards.md) | UI interaction, navigation, and component standards | diff --git a/Documentation/ui-interaction-and-component-standards.md b/Documentation/ui-interaction-and-component-standards.md new file mode 100644 index 0000000..8e5aa89 --- /dev/null +++ b/Documentation/ui-interaction-and-component-standards.md @@ -0,0 +1,527 @@ +# UI Interaction and Component Standards + +**Subject:** Virtual Art Gallery — screen and interaction guidance +**Applies to:** Public visitor UI, 3D halls, curator tools +**Companion rule:** `.cursor/rules/ui-interaction-standards.mdc` + +--- + +## 0. Introduction + +The Gallery is a single React SPA (`HomePage.tsx` view union — no URL router except `?layout=`). Public visitors browse a museum-dark timeline and 3D halls; curators use the same shell for catalog tools (Checkup, Translations, Influences, Tours, Users, Activity). Work lands in the same header, palette, and back-stack, so a new screen that invents its own chrome, confirmations, or loading pattern fragments the experience. + +This document is the shared contract for **how the UI behaves**. Architecture, APIs, and 3D hall construction live in [basics.md](basics.md), [API.md](API.md), and [data-and-images.md](data-and-images.md). Tree geometry lives in [movement-tree.md](movement-tree.md). Locale strings live in [i18n-russian.md](i18n-russian.md). + +### 0.1 Purpose and audience + +- **Purpose:** Define common UI interaction, navigation, and component standards for the Gallery client. +- **Primary audience:** Developers (and agents) adding or changing `client/src` UI; anyone writing curator-tool screens. +- **Secondary audience:** QA, product, copy/i18n. + +### 0.2 Scope + +- **In scope:** + - Navigation and back-stack behaviour + - Timeline / search / 3D / detail / curator-tool interaction patterns + - Loading, empty, error, and confirmation behaviour + - Permission-based UI (`can()` / RBAC) + - Shared component and visual-language rules +- **Out of scope:** + - REST contracts, retries, and image pipeline internals + - Three.js hall architecture, textures, and lighting (see [basics.md](basics.md)) + - Pixel-perfect branding kit (no separate design-system package; follow existing CSS) + +### 0.3 Requirement levels + +- **MUST:** Mandatory for new work and for fixes that touch the same screen. +- **SHOULD:** Recommended; deviate only with a short note in the PR or screen section. +- **MAY:** Optional pattern when the screen specification calls for it. + +### 0.4 Surface map + +| Surface | Typical components | Visitors | Curators | +|---------|--------------------|----------|----------| +| Timeline home | `Timeline`, `VerticalTimeline`, `MovementBands`, `VerticalMovementBands`, `MovementTree`, `CatalogSearchBar` | yes | yes | +| 3D hall | `VirtualGallery`, wing navigator, exit overlays | yes | yes | +| Painting / bio | `PaintingDetail`, `PaintingLightbox`, `ArtistBio`, `PaintingAnnotations` | yes | + debug panel when `can('images')` | +| Overlays | `CuratorLoginModal`, `ArtistFilterModal`, `ToursPopup`, `DebugSearchResultsModal` | some | all | +| Curator tools | `CheckupPage`, `TranslationsPage`, `InfluencesPage`, `ToursPage`, `UsersPage`, `AuditPage` | no | permission-gated | + +--- + +## 1. Design Goals and Principles + +### 1.1 Problem statement + +- Statement: Visitors and curators share one shell. Inconsistent back labels, ad-hoc modals, English-only captions, and one-off loading/error treatment make the museum feel like several apps glued together. + - Scope: All `client/src` screens and overlays. + - Rationale: The product is a gallery, not an admin console with a visitor skin. Predictable chrome is part of the exhibit. + - Verification: Compare a new screen’s header, back control, loading marker, and locale keys against this document. + +### 1.2 Functional design principles + +- Principle 1: One museum, two roles + - Statement: Public browse chrome MUST stay museum-dark (navy / gold / Georgia). Curator tools MAY be denser but MUST reuse the same header back pattern, gold accent, and permission hiding — they MUST NOT look like a separate product. + - Rationale: Curators enter from the same timeline; a visual cliff breaks trust. + - Verification: Side-by-side with timeline header and Checkup / Tours editor. + +- Principle 2: Predictable drill-down and return + - Statement: Navigation MUST follow Timeline → (movement picker) → hall → painting/bio, with an explicit in-app Back that restores the intended session — not the browser history stack. + - Rationale: `HomePage` owns view state; the browser Back button is not wired. + - Verification: Walk the [basics.md navigation flow](basics.md#user-navigation-flow) and the Back table in §2.4. + +- Principle 3: Clarity of actions and feedback + - Statement: Every user-initiated load, save, delete, or failed request MUST show a loading, success, or error state the user can see without opening the console. + - Rationale: 3D and image work is slow; silent failure looks like a broken hall. + - Verification: Trigger catalog load, hall open, form save, and a failed API call. + +- Principle 4: Minimize cognitive load + - Statement: Timeline charts MUST keep zoom/pan/click hints visible. Curator tables MUST put filter/search above the grid. Destructive actions MUST be confirmed. + - Rationale: Dense history data and catalog tables are easy to mis-click. + - Verification: Hint captions present; filters above tables; delete paths show a confirm. + +- Principle 5: Locale and permission are first-class + - Statement: New visitor-facing copy MUST go through `react-i18next` (`locales/{en,ru}`). Actions the user cannot perform MUST be hidden, not disabled-without-explanation. + - Rationale: EN/RU is a product requirement; exposing forbidden tools invites errors. + - Verification: Toggle EN|RU; log in as a curator without the relevant `can()` and confirm the control is absent. + +--- + +## 2. Navigation Principles + +### 2.1 Application chrome (no left module rail) + +The Gallery has **no persistent left navigation menu**. Primary wayfinding is the **timeline header** on home, and an explicit **Back** control on every nested view. + +- Statement: The timeline home MUST keep layout switch, catalog search, locale switcher, and role-appropriate tools in the site header. + - Scope: `HomePage` when `view` is `timeline` / `timeline-vertical` / `timeline-tree`. + - Rationale: Visitors need search and layout without hunting; curators need tools without leaving the museum frame. + - Verification: Header remains usable at 100vh; search dropdown stacks above movement bands (`z-index` on `.site-header`). + +- Statement: Layout switch links MUST stay in the top-left (`.site-layout-switch`). The Tree of Art control SHOULD use `.site-layout-link-feature`. + - Scope: Timeline home. + - Rationale: Three layouts must stay discoverable; Tree is the featured alternative start page. + - Verification: Classic / Vertical / Tree links match [basics.md](basics.md#timeline-and-movement-flow). + +- Statement: Nested views (hall, painting, bio, curator pages) MUST NOT reintroduce a second global nav. They MUST show a single primary Back control in the page header. + - Scope: All non-home views. + - Rationale: Avoid competing menus; the drill-down is the nav. + - Verification: No duplicate “home” plus “modules” rails on curator pages. + +### 2.2 Page hierarchy + +- Statement: Screens MUST stay within this hierarchy (max four levels): + + 1. Timeline home (classic / vertical / tree) + 2. Overlay or picker (artist filter, tours popup, login) **or** curator tool page + 3. 3D hall (artist / movement / tour) + 4. Painting detail or artist bio (optional lightbox on top of detail) + + - Scope: All visitor and curator flows. + - Rationale: Matches the existing drill-down; deeper stacks become unrecoverable without a router. + - Verification: New views are added to the `View` union in `HomePage.tsx` with a defined parent and Back handler. + +- Statement: Curator tools MUST open as siblings of the timeline (replace the home canvas), not as a fifth level under a hall. + - Scope: Checkup, Translations, Influences, Tours editor, Users, Activity. + - Rationale: Tools operate on the catalog, not on a hall session. + - Verification: Opening Checkup from a hall is not required; from timeline header, Back returns to timeline home. + +### 2.3 Location awareness (no breadcrumbs) + +- Statement: The app MUST NOT add a breadcrumb trail unless a future router lands. Until then, the Back label MUST name the destination (`← Back to Timeline`, `← Back to Gallery`, `← Back`). + - Scope: All nested views. + - Rationale: There is no URL path to reflect; a fake breadcrumb would lie. + - Verification: Labels match §2.4; they are i18n keys (`backToTimeline`, `backToGallery`, …). + +- Statement: Timeline layout MUST be deep-linkable via `?layout=classic|vertical|tree` (omit param for classic). Other views MUST NOT pretend to be bookmarkable until a router exists. + - Scope: Timeline home. + - Rationale: Layout is the one shareable start-page choice; halls and tools are session state. + - Verification: Load `?layout=tree`, switch layouts, confirm `history.replaceState` updates the query. + +### 2.4 Back navigation + +| Control | MUST return to | +|---------|----------------| +| **← Back to Timeline** (hall header, movement Exit to Timeline, painting opened from search) | Home timeline via `goToTimelineHome()` — unmount hall, clear session, reset year window to full catalog bounds | +| **← Back to Gallery** (painting from a hall) | Same hall session (camera / wing preserved) | +| **← Back** (artist bio) | `returnTo` view (usually the hall that opened bio) | +| Curator page Back | Timeline home | + +- Statement: Back MUST be an in-app control. The browser Back button MUST NOT be relied on (it is not wired to `View` state). + - Scope: All nested views. + - Rationale: `setView` is the router. + - Verification: From painting detail, in-app Back restores the hall; browser Back does not need to. + +- Statement: `goToTimelineHome()` MUST be the single implementation for “leave everything and show the timeline.” New exits MUST call it rather than duplicating reset logic. + - Scope: Halls, search-opened paintings, curator Back. + - Rationale: Year-range reset and session clear must stay consistent. + - Verification: After Exit to Timeline, `viewStart`/`viewEnd` equal catalog bounds. + +### 2.5 State preservation + +- Statement: Timeline pan/zoom (`viewStart` / `viewEnd`) MUST persist while the user stays on a timeline layout. Switching classic ↔ vertical ↔ tree MUST keep the same year window. + - Scope: Timeline home. + - Rationale: Layout is a lens, not a new dataset. + - Verification: Zoom, switch to Tree, confirm the year rail range is unchanged. + +- Statement: Returning to timeline via `goToTimelineHome()` MUST reset the year window to full catalog bounds. + - Scope: Hall / search / curator exits that call `goToTimelineHome()`. + - Rationale: Documented in [basics.md](basics.md#back-navigation); visitors expect a fresh overview, not a leftover zoom. + - Verification: Zoom in, enter a hall, Back to Timeline → full span. + +- Statement: Hall camera and wing MUST be preserved across **Back to Gallery** from painting detail. They MUST be discarded on **Back to Timeline**. + - Scope: Artist, movement, and tour halls. + - Rationale: Inspecting a painting is a detour; leaving the museum is not. + - Verification: Move in the hall, open a painting, Back to Gallery → same viewpoint. + +- Statement: Catalog search input MAY clear when the dropdown closes. It MUST NOT change timeline zoom by itself. + - Scope: `CatalogSearchBar`. + - Rationale: Search is a jump, not a filter on the chart. + - Verification: Type a query, Escape; year window unchanged. + +--- + +## 3. Common UI Interaction Patterns + +### 3.1 Catalog search (visitor) + +- Statement: Timeline search MUST live in the header, require **2** trimmed characters, debounce **300 ms**, and group results into Artists, Movements, Paintings. + - Scope: `CatalogSearchBar.tsx`. + - Rationale: Documented product behaviour; keeps `/api/search` load reasonable. + - Verification: 1 character shows no fetch; 2+ after debounce shows groups. + +- Statement: Keyboard MUST support `↑`/`↓` highlight, `Enter` to open, `Escape` to close. + - Scope: Catalog search dropdown. + - Rationale: Timeline is pointer-heavy; search should still be keyboardable. + - Verification: Keyboard-only open of an artist, movement, and painting. + +- Statement: Opening a painting from search MUST set `returnTo` timeline so Back is **← Back to Timeline**, not Gallery. + - Scope: Search → painting detail. + - Rationale: There is no hall session. + - Verification: Search a title, open, Back → home timeline. + +### 3.2 Timeline charts (zoom, pan, click) + +- Statement: Scroll MUST zoom, drag MUST pan, click on a stream/limb/label MUST open the movement (artist filter → hall). All three layouts MUST use `zoomTimelineView` / `panTimelineView`. + - Scope: `MovementBands`, `VerticalMovementBands`, `MovementTree`, era rails. + - Rationale: Shared year window; one mental model. + - Verification: Same wheel/drag behaviour on classic, vertical, and tree. + +- Statement: Each chart MUST show a localised hint caption (`captionClassicTimeline`, `captionClassicFlow`, `captionVerticalTimeline`, `captionVerticalFlow`, `captionTreeFlow`). + - Scope: Timeline home. + - Rationale: First-time visitors cannot discover zoom/pan otherwise. + - Verification: EN and RU captions change with `LocaleSwitcher`. + +- Statement: Timeline layout shifts (lane packing, tree fit scale) SHOULD animate rather than snap. + - Scope: Movement charts. + - Rationale: Unexplained jumps look like bugs. + - Verification: Zoom/pan does not teleport streams. + +### 3.3 Filters and search (curator tables) + +- Statement: Filter/search controls MUST sit in a toolbar **above** the table, not in a column header hack or a page footer. + - Scope: Checkup, Translations, Influences, Tours editor, Users, Activity. + - Rationale: Matches Checkup (`checkup-toolbar`) and keeps the grid scannable. + - Verification: Filters remain visible while the table scrolls. + +- Statement: Simple text filters MAY apply as the user types. Expensive operations (image search, import, refetch) MUST require an explicit button (e.g. Checkup **Search visible**). + - Scope: Curator list screens. + - Rationale: Checkup search is rate-limited and slow; typing must not fire it. + - Verification: Typing in Checkup filter does not start image search. + +- Statement: When a filter hides rows, the toolbar SHOULD show how many rows are visible (e.g. `N shown`). + - Scope: Filtered tables. + - Rationale: Empty-looking tables need an explanation. + - Verification: Filter to zero rows → empty state plus count. + +### 3.4 Date selection + +- Statement: Year fields in the catalog and timeline MUST use numeric years (negative = BCE). They MUST NOT switch to locale-specific calendar widgets for historical BCE dates. + - Scope: Timeline bounds, artist lifespan, painting years, curator year filters. + - Rationale: The catalog spans −800 to the present; HTML date inputs cannot represent BCE. + - Verification: Ancient era still filters correctly. + +- Statement: If a future screen needs a civil date (e.g. audit log day), it SHOULD use ISO `YYYY-MM-DD` and validate start ≤ end for ranges. + - Scope: Activity / audit and any new timestamp filters. + - Rationale: Consistent with API timestamps; avoids DD/MM ambiguity. + - Verification: Invalid range shows a field-level message. + +### 3.5 Tables and lists (curator) + +- Statement: Structured curator datasets MUST use a labeled HTML table (or existing page table classes), one logical record per row. + - Scope: Checkup, Users, Influences worklists, Tours list, Translations worklist, Activity. + - Rationale: Comparison and row actions need columns, not cards. + - Verification: Column headers present; row click/action affects one record. + +- Statement: Visitor-facing catalog MUST NOT be presented as a spreadsheet. Timeline streams, tree limbs, and 3D hangs are the list metaphor. + - Scope: Public home and halls. + - Rationale: The product is a gallery, not a DAM table. + - Verification: No “all paintings” data grid on the public home. + +- Statement: Tables MAY omit pagination while the dataset is curator-sized and client-filtered. If a list grows past comfortable scrolling, it SHOULD paginate or virtualise rather than rendering thousands of DOM rows. + - Scope: Curator tools. + - Rationale: Checkup is already filter-then-scroll; unbounded paint is a future foot-gun. + - Verification: New tools with large lists have a documented paging or virtualisation plan. + +- Statement: Row actions MUST sit in a dedicated column or overflow control, not as random icons in every cell. + - Scope: Interactive curator tables. + - Rationale: Scanability. + - Verification: Action column or consistent button set per row. + +### 3.6 Multi-row and per-record actions + +- Statement: Bulk actions MUST use a leading checkbox column, Select All for **visible** rows only, and a confirmation that includes the affected count for destructive work. + - Scope: Any new bulk-enabled table. (Today: ArtistFilterModal multi-select is a picker, not a bulk delete.) + - Rationale: Same as the Logistics template; prevent silent mass edits. + - Verification: Select All does not imply “all matching in the database” unless explicitly labelled. + +- Statement: Artist filter before a movement hall MUST be a modal checklist with explicit proceed/cancel, not a bulk-edit of the catalog. + - Scope: `ArtistFilterModal`. + - Rationale: It only chooses who appears in the hall. + - Verification: Cancel leaves the user on the timeline; proceed opens the hall. + +- Statement: Primary row/object action SHOULD be the name/title (open painting, open user, open tour). Secondary actions SHOULD stay in the row’s action controls. + - Scope: Curator tables and search results. + - Rationale: Matches search-result click-to-open. + - Verification: Clicking a Checkup title opens the painting when that handler exists. + +- Statement: Unavailable-by-permission actions MUST be hidden. Unavailable-by-record-state SHOULD be disabled with a `title`/tooltip explaining why. + - Scope: All tools. + - Rationale: RBAC vs workflow are different signals. + - Verification: Non-admin does not see Users; a disabled Fix button states why. + +### 3.7 Forms + +- Statement: Short auth and picker flows MUST use a modal. Multi-section catalog editors (Users create/edit, Tours editor, Influences wizard, Translations worklist) MUST be full-page (or the existing page layout), not nested modals. + - Scope: All forms. + - Rationale: Halls and timeline need to stay the “place”; heavy edit needs space. + - Verification: Login is modal; Users is a page. + +- Statement: Forms SHOULD be a single column. Related fields MAY group under a heading when there are more than five inputs. + - Scope: Curator forms. + - Rationale: Scanning beats dense multi-column on museum-width pages. + - Verification: Users create form remains vertically grouped. + +- Statement: Required fields MUST use the native `required` attribute and/or a visible marker; validation errors MUST appear next to the field or as a form-level error the submit control does not obscure. + - Scope: Login, Users, Tours, Influences, Translations. + - Rationale: Silent submit-disable is not enough. + - Verification: Submit empty login → field or form error, not a blank modal. + +- Statement: After successful save, the system MUST show an inline success message (or equivalent) and keep the user on the tool unless the spec says to return to timeline. + - Scope: Curator mutations. + - Rationale: Users page already uses `message` / `error` banners. + - Verification: Save permissions → success text; failed save → error text. + +- Statement: Unsaved-change guards SHOULD be added when a form is long enough that accidental Back would lose work (Tours editor, Translations). Login and tiny pickers MAY skip this. + - Scope: Heavy editors. + - Rationale: `window.confirm` on delete already exists; abandon-edit is the remaining hole. + - Verification: Dirty Tours editor + Back prompts or discards explicitly. + +### 3.8 Edit interaction pattern selection + +| Pattern | Use when | +|---------|----------| +| **Modal** | Login, artist filter, tours list popup, debug image picker, lightbox, hall exit/wing overlays | +| **Inline** | Checkup flags, debug **Checked** / **Fix it** on painting detail — small, reversible | +| **Full page** | Curator tools, painting detail, artist bio, 3D hall | +| **Drawer** | MUST NOT be introduced unless a spec adds a shared drawer component | + +- Statement: Nested modals MUST NOT be used (no modal opened from another modal). The debug “More” picker MAY stack on painting detail because detail is a full page, not a modal. + - Scope: All overlays. + - Rationale: Focus traps and Back labels break. + - Verification: Login does not open another dialog. + +### 3.9 Modals and overlays + +- Statement: Modals MUST use `role="dialog"` and `aria-modal="true"`, a visible close/cancel, and **Escape** to dismiss unless a submit is in flight. + - Scope: `CuratorLoginModal`, `ArtistFilterModal`, `ToursPopup`, `DebugSearchResultsModal`, `PaintingLightbox`, hall exit overlays. + - Rationale: Accessibility and parity with search. + - Verification: Esc closes lightbox and debug picker; backdrop click matches existing login behaviour. + +- Statement: Backdrop click MAY close pickers and login. It MUST NOT close a modal that is applying a destructive or long-running action. + - Scope: Overlays. + - Rationale: Accidental dismiss during Fix/upload is costly. + - Verification: Click outside login closes; do not dismiss mid-upload. + +### 3.10 Loading, empty, and error states + +- Statement: Catalog, portrait, and hall loads MUST use `GalleryLoadingMarker` (overlay or banner), not an ad-hoc spinner per screen unless the marker cannot cover the region. + - Scope: Home, halls, painting/bio image work. + - Rationale: One recognisable “the museum is fetching” treatment. + - Verification: First visit shows “Loading art history…”; hall open uses the same marker family. + +- Statement: Loading SHOULD be scoped to the affected region. Full-viewport overlay MUST be used only when the user cannot usefully interact (first catalog load, hall WebGL init). + - Scope: All loads. + - Rationale: Portrait banner vs full-page overlay already follows this. + - Verification: Timeline remains visible while “Loading portraits…” banners. + +- Statement: Empty datasets MUST explain themselves (e.g. vertical flow: no movements in range; search: no matches; Checkup: no rows for filter). + - Scope: Charts, search, tables. + - Rationale: Blank gold-on-navy reads as a crash. + - Verification: Zoom to a year with no movements; search a nonsense string. + +- Statement: Failed loads MUST set a visible error string (`error-banner`, form error, or page error) — never `console.error` alone. + - Scope: All data-fetching views. + - Rationale: Visitors have no console. + - Verification: Stop the API and confirm home shows a load failure message. + +### 3.11 3D hall interaction + +- Statement: Halls MUST keep **← Back to Timeline**, pointer-lock / click-to-move as already implemented, and **E** (or documented key) for exit/wing navigation. New hall UI MUST not steal those keys without updating this section. + - Scope: `VirtualGallery`. + - Rationale: Muscle memory across artist, movement, and tour halls. + - Verification: Same Back label and exit overlay pattern in all three hall kinds. + +- Statement: Clicking a framed painting MUST open painting detail with `returnTo` the current hall. Missing images MUST show the draped-canvas placeholder, not a broken ``. + - Scope: Halls. + - Rationale: Documented in [basics.md](basics.md). + - Verification: Work without a file still shows a frame cover. + +--- + +## 4. Behavioral Standards (Screen UX) + +### 4.1 Permission-based rendering (RBAC) + +- Statement: Header tools and debug controls MUST render only when `can('')` (and login for curator). Anonymous visitors MUST see browse + Tours popup + locale, not Checkup/Users/etc. + - Scope: `HomePage` header, painting/bio debug panel. + - Rationale: Security UX: hide, don’t tease. + - Verification: Logged-out home; curator without `users` cannot open Users. + +- Statement: Route-like views that require a role MUST show the existing “Curator access required” panel with login and back-to-gallery actions — they MUST NOT render an empty privileged page. + - Scope: Checkup, Translations, Influences, Tours editor, Users, Activity. + - Rationale: Deep view state can still be set; the gate must hold. + - Verification: Set view to Users while logged out → curator required copy. + +- Statement: The same permission MUST hide the same action on every surface (header, painting debug, API). Do not leave a visible button that 403s. + - Scope: All mutations. + - Rationale: Predictable roles. + - Verification: `can('images')` off → no debug panel and no Fix buttons. + +### 4.2 Notifications and user feedback + +- Statement: Curator mutations MUST show success or error text in the page’s existing banner/message area. The app has no global toast system; new screens MUST NOT invent a third notification widget without replacing this standard. + - Scope: Curator pages. + - Rationale: Users/Influences already use inline `message` / `error`. + - Verification: One visual treatment per page, consistent placement under the header. + +- Statement: Visitor-facing destructive actions (painting **Remove entry** in debug) MUST confirm and then show failure inline if the API rejects. + - Scope: Debug painting tools. + - Rationale: Catalog deletes are irreversible. + - Verification: Cancel confirm → no delete. + +- Statement: Copy MUST be concise and, for errors, actionable (“Is the server running?”, “Sign in as a curator…”). + - Scope: All user-visible strings. + - Rationale: Support load. + - Verification: Home catalog failure string remains understandable. + +### 4.3 Confirmation and cancellation + +- Statement: Destructive curator actions (delete influence, delete tour, remove painting, deactivate user if offered) MUST confirm before the request. `window.confirm` with an i18n string is the current standard (`confirmDelete`); a shared modal MAY replace it later but MUST stay one pattern. + - Scope: Influences, Tours, painting remove, Users. + - Rationale: Accidental clicks on dense tables. + - Verification: Delete tour → confirm; Cancel → no API call. + +- Statement: Cancel on a confirm MUST leave filters, selection, and unsaved fields unchanged. + - Scope: All confirms. + - Rationale: Context preservation. + - Verification: Filtered Checkup, cancel a destructive action → filter still applied. + +--- + +## 5. Component Consistency Rules + +### 5.1 Shared interaction pattern usage + +- Statement: New UI MUST reuse existing components before creating parallels: `GalleryLoadingMarker`, `CatalogSearchBar`, `LocaleSwitcher`, `CuratorLoginModal`, `ArtistFilterModal`, `PaintingLightbox`, `DebugSearchResultsModal`, `DebugUploadButton`. + - Scope: `client/src`. + - Rationale: Duplicate spinners and dialogs already caused drift. + - Verification: PR does not add a second login modal or loading overlay. + +- Statement: Movement colours MUST go through `utils/movementColor.ts` (`vividMovementColor`, `shadeMovementColor`). Hex parsing MUST tolerate `#rgb` / `#rrggbb` and keep the first six digits of longer values. + - Scope: Timeline charts and any movement swatch. + - Rationale: Classic, vertical, and tree already share this util. + - Verification: No local `parseHexColor` copies in components. + +- Statement: Timeline zoom/pan MUST use `utils/timelineView.ts`. Tree horizontal layout MUST use `utils/movementTree.ts` (view-independent structure). + - Scope: Timeline surfaces. + - Rationale: Layouts share one year window. + - Verification: No one-off wheel handlers that bypass the util. + +### 5.2 Component and style selection + +- Statement: There is **no Ant Design / MUI**. UI MUST be React + colocated CSS (`ComponentName.tsx` + `ComponentName.css`). Global museum tokens SHOULD reuse existing values: + + | Token | Typical value | Use | + |-------|----------------|-----| + | Gold | `#c9a96e` / `rgba(201, 169, 110, …)` | Accents, borders, links | + | Ink / navy | `#0f0f1a`, `#1a1a2e`, `#16213e` | Page background | + | Type | Georgia, serif | Titles, captions, layout links | + | Viewport | `100vh`, `overflow: hidden` on home | No document scroll for the museum shell | + + - Scope: All client UI. + - Rationale: The look *is* the design system. + - Verification: New CSS does not introduce a bright Bootstrap theme. + +- Statement: Prefer semantic `