Files
summercms/.planning/phases/10-admin-vue-spa/design/README.md
2026-09-26 23:21:43 +02:00

388 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: SummerCMS Admin Panel — Direction C ("Generic modern dashboard")
## Overview
This is the admin panel for SummerCMS, a CMS framework. The mocks use data from one real app: **Płytarium**, a home catalogue of vinyl, CD and cassette releases. The panel is a **schema-driven Vue 3 SPA**. Every list and form is generated from YAML-defined `columns` and `fields`. Build generic components, not screens: one DataTable, one FormField row, one Tabs component, one RelationManager. The UI language is **Polish**, and all copy below is final.
Screens covered: Login, Navigation shell (multi-plugin: Płytarium + Użytkownicy), List (Albumy) with its selected/empty/loading states, Form (Edycja albumu) with tabs and 422 errors, and the relation manager (Kolekcje → Redaktorzy) with its "Dodaj redaktora" modal. Light and dark mode are both included, plus tablet width.
## About the Design Files
`Direction C v2.dc.html` is a **design reference built in HTML**, not production code. Recreate it in the target Vue 3 codebase using its existing patterns. If the codebase has no UI setup yet, use Vue 3 + Vite + Tailwind with headless primitives (e.g. Radix Vue / Reka UI) and lucide-vue-next icons.
To view it, open `Direction C v2.dc.html` in a browser. `support.js` must be in the same folder, and you need internet access for the fonts and icons. The component's props act as a state switcher:
- `plugin`: `plytarium | users` (use `users` with `screen=list` to see the Users plugin list)
- `flyout`: boolean; with `narrow`, shows the collapsed-rail flyout
- `screen`: `login | list | form | relation`
- `listState`: `default | selected | empty | loading`
- `tab`: `basic | details`
- `dark`, `narrow` (tablet), `errors` (422), `toast`, `modal`, `userMenu`: booleans
All sample data sits in the `ALBUMS`, `EDITORS` and `CANDS` constants in the logic class.
## Fidelity
**High-fidelity.** Colours, type, spacing, radii and copy are final. Match them closely.
---
## Design Tokens
### Colours
| Token | Light | Dark | Use |
|---|---|---|---|
| bg | `#f4f6f9` | `#111726` | App background behind cards |
| surface | `#ffffff` | `#182033` | Cards, header, inputs, footer bar |
| subtle | `#f3f5f8` | `#1f283d` | Table header, search input fill, chips, avatar bg |
| border | `#e6e9ef` | `#29334b` | Card borders, row dividers |
| borderStrong | `#d2d8e2` | `#3a4661` | Input borders, unchecked checkbox, switch off |
| text | `#141b2d` | `#eef1f6` | Primary text |
| muted | `#566175` | `#a9b3c6` | Secondary text, column headers, icons |
| placeholder | `#6b7588` | `#8a95ab` | Input placeholder |
| primary | `#22304d` (navy) | `#fcd34d` (yellow) | Primary buttons, checked checkbox |
| onPrimary | `#ffffff` | `#1b2540` | Text on primary |
| danger | `#c62828` | `#f58a8a` | Destructive, errors |
| dangerSoft | `#fdf0f0` | `#3a1d24` | Error banner bg, destructive hover |
| okBg / okText | `#e3f4e8` / `#1c6b35` | `#173826` / `#8fdfa8` | "Tak" badge |
| ring | `rgba(252,196,40,.55)` | `rgba(252,211,77,.45)` | Focus ring (3px) |
| hover | `#f1f3f7` | `#212b42` | Hover bg for ghost buttons/rows |
| sel | `#fdf3cf` | `#3a3622` | Selected row bg, current page button |
| skel | `#eceff4` | `#263049` | Skeleton bars |
| overlay | `rgba(20,27,45,.5)` | `rgba(5,8,16,.7)` | Modal backdrop |
| side | `#1d2740` | `#0d1320` | Sidebar bg (sidebar is dark in both modes; dark mode adds a `#222b40` right border) |
| accent | `#fcd34d` | same | Brand yellow: logo, active nav, avatar |
| onAccent | `#1b2540` | same | Text on accent |
| sidebar text | `#c3cbda` idle, `#9aa6bd` section label, `#ffffff` hover, `#fcd34d` active | | |
| card shadow | `0 1px 2px rgba(20,27,45,.04), 0 4px 16px rgba(20,27,45,.05)` | `none` | |
The brand is **dark navy + sunny yellow**. In light mode the primary is navy and yellow is only an accent. In dark mode yellow becomes the primary.
### Typography
- UI font: **DM Sans** (Google Fonts, opsz 9..40, weights 400/500/600/700). Base size 14px, line-height 1.5.
- Mono: **DM Mono** 400/500. Used for the Półka shelf code and the `colorpicker` token.
- Datetimes use DM Sans with `font-variant-numeric: tabular-nums`, 13px, muted colour.
| Role | Size / weight / tracking |
|---|---|
| Page title (list) | 26px / 700 / -0.02em |
| Record title (form) | 24px / 700 / -0.02em |
| Login title | 22px / 700 / -0.02em |
| Modal title | 20px / 700 / -0.01em |
| Section title (Redaktorzy) | 17px / 700 |
| Body / cell | 14px / 400 (Nazwa cell 600) |
| Field label | 14px / 600 |
| Column header | 12px / 600, muted (the sorted column uses the text colour) |
| Helper / meta / error text | 13px |
### Radii
Inputs, buttons and selects: 10px. Cards: 16px. Login card: 16px. Modal: 20px. Inner table container and toggle cards: 12px. Tabs track: 12px, tab pill: 9px. Checkbox: 5px (6px in the form and modal). Pagination buttons: 8px. Badges, chips and switch: 999px. Avatars: 50%.
### Spacing
Page padding is 28px top/bottom and 32px left/right. Gaps between cards and blocks are 20px. Form card padding is 28px. The form grid gap is 22px (rows) × 24px (columns). Table cells use 14px horizontal padding, with 20px on the first and last columns.
### Control heights
- Primary/secondary buttons and search: **42px**
- Form inputs, selects and login inputs: **44px**
- Table header row: 44px. Body row: **54px** (56px in the relation manager). Modal option row: 54px.
- Pagination buttons: 34px
- Top header: 64px. Sidebar nav item: 40px.
### Focus
Every interactive element shows a visible **3px yellow ring** (`box-shadow: 0 0 0 3px var(--ring)`). Inputs also switch their border to `primary`. Buttons use `outline: 3px solid var(--ring); outline-offset: 2px`. Style `:focus-visible` this way and never remove it.
---
## Screens
### 1. Login (`screen=login`)
- Full-viewport background: `radial-gradient(ellipse at 50% 35%, #2b3a5c 0%, #1d2740 60%)`.
- A centred column, max-width 400px, gap 24px:
- **Wordmark**: sun icon (lucide `sun`, 20px, yellow) in a 34px circle filled `rgba(252,211,77,.14)`, followed by "Summer**CMS**" at 18px/700 in white, with "CMS" in yellow. This is the only branding, because the admin URL is custom (e.g. `/plytadmin`).
- **Card**: white background, radius 16, padding 32, gap 18, shadow `0 24px 60px rgba(0,0,0,.35)`.
- Title: "Witaj ponownie"
- Field "Login lub e-mail" (input 44px)
- Field "Hasło" (password)
- Error state: the password input gets a danger border and `aria-invalid`. Below it sits an alert block (`role="alert"`, dangerSoft bg, danger text, radius 10, padding 12×14, `circle-alert` icon 18px) reading **"Nieprawidłowe dane logowania"**.
- Full-width primary button **"Zaloguj"** (44px).
### 2. Navigation shell — multi-plugin (rail + section panel)
SummerCMS replaces WinterCMS. It is a backend that hosts many plugins, and each plugin registers navigation the way Winter's `registerNavigation()` does: **one main-menu item** (label, icon, order, permissions) plus **its own side menu** (items, icons, permissions). Płytarium is just one of these plugins. The shell is built from that registry, and nothing in it is hardcoded per plugin.
**Plugin rail (level 1 = Winter main menu)**
- Always visible. Width 80px, colour `side` (`#1d2740` light / `#0d1320` dark), padding 14px 0 12px, column gap 6, centred.
- Top: brand badge, a 40px rounded square (radius 12) filled `rgba(252,211,77,.14)` holding a `sun` icon (22px, yellow). `title="SummerCMS"`, bottom margin 14.
- One item per registered main-menu entry, sorted by `order`:
- 68px wide, padding 8/6, radius 12, letter-spacing -0.01em.
- 20px icon, with the label below it at 11px (line-height 1.2, centred).
- Idle: `#c3cbda`. Hover: `rgba(255,255,255,.08)` with white text.
- Active: bg `rgba(252,211,77,.14)`, text/icon `#fcd34d`, weight 700, `aria-current="page"`.
- Focus: 3px ring.
- Mock entries: **Płytarium** (`disc-3`), **Użytkownicy** (`users`), **Media** (`image`).
- A flex spacer, then **Ustawienia** (`settings`) pinned to the bottom. Settings is a separate area where plugins register settings pages, the same as Winter.
- When the panel is collapsed, a 40px "Rozwiń menu" button (`panel-left-open`) appears above Ustawienia.
- Wrap the rail in `<nav aria-label="Wtyczki">`.
**Section panel (level 2 = the active plugin's side menu)**
- Width 224px, `surface` bg, 1px `border` on the right, padding 14×12, gap 2.
- Header: 40px tall, 14px bottom margin. It shows the plugin label at 16px/700 (e.g. "Płytarium") and a 30px "Zwiń menu" button (`panel-left-close`).
- Items: 40px tall, radius 10, 18px icon, gap 12.
- Idle: `muted`, weight 500. Hover: `hover` bg with `text` colour.
- Active: `sel` bg (`#fdf3cf` / `#3a3622`), `text` colour, weight 600, `aria-current="page"`.
- Płytarium items: Albumy (`disc-3`), Artyści (`mic-vocal`), Kolekcje (`library`), Gatunki (`tags`).
- **"Style" is absent** because the current admin lacks permission for it.
- Użytkownicy items: Użytkownicy (`users`), Grupy (`users-round`), Role (`shield-check`).
**Switching plugins**
- Clicking a rail item navigates to that plugin's first permitted side-menu item (or to its registered default URL).
- The panel content changes, and the first breadcrumb changes to the plugin label ("Płytarium › Albumy", "Użytkownicy › Użytkownicy").
- The active rail item is derived from the current route's owning plugin, e.g. `/plytadmin/plytarium/albums` → `plytarium`.
**Permissions**
- Filter both levels. An item the user can't access is not rendered at all; don't show it disabled.
- A plugin whose side menu ends up empty after filtering is not shown in the rail either.
**Collapsed / tablet**
- Below about 1100px, or when the user collapses it manually, the panel is hidden and only the rail remains. The manual choice persists in localStorage.
- In that state, hovering or focusing a rail item, or pressing Enter/↓ on it, opens a **flyout** with the plugin's side menu:
- Position: 8px right of the rail, aligned with the header area.
- Size and shape: 230px wide, `surface` bg, border, radius 14, padding 8.
- Shadow: `0 16px 40px rgba(20,27,45,.18)`.
- Contents: a bold plugin title, then items 38px tall with radius 9 and the same active and idle styling as the panel.
- It uses `role="menu"`, closes on Esc or mouse-leave with a ~200ms delay, and returns focus to the rail item.
**Suggested registry shape** (what the backend exposes to the SPA):
```json
[{ "code": "plytarium", "label": "Płytarium", "icon": "disc-3", "order": 100,
"url": "/plytarium/albums",
"sideMenu": [{ "code": "albums", "label": "Albumy", "icon": "disc-3", "url": "/plytarium/albums" }, …] }]
```
The server filters this by permissions before sending it.
Tip: when there are many plugins, add a Ctrl K command palette that searches every plugin's side-menu entries.
**Top header**
- 64px tall, surface bg, bottom border, padding `0 24px 0 32px`.
- Left: breadcrumbs "Płytarium › Albumy › Enigmatic". Parent crumbs are muted links; the current crumb uses the text colour at weight 600. Separator: `chevron-right` 14px.
- Right: user button, 44px tall, radius 12, containing:
- 34px yellow avatar with initials "TW"
- Name "Tomasz Wójcik" (600) above role "Administrator" (12px muted). The name and role hide on tablet.
- `chevron-down`
- **User menu** (dropdown, `role="menu"`): 280px wide, radius 14, padding 8, shadow `0 16px 40px rgba(20,27,45,.16)`. It holds a header (40px avatar, "Tomasz Wójcik" in bold, "tomasz.wojcik@plytarium.pl" in muted), a divider, and the menu item "Wyloguj" (`log-out` icon).
### 3. List — Albumy (`screen=list`)
**Heading row**
- "Albumy" (26/700) with the subtitle "348 pozycji w katalogu".
- Primary button **"Nowy album"** (`plus` icon), aligned right.
**Card** (radius 16, border, shadow), containing a toolbar, a table and a pagination footer.
**Toolbar** (padding 16×20)
- Search input, 320px wide, `subtle` fill, `search` icon at left 14px. Placeholder: "Szukaj albumów…".
- Right side:
- No rows selected: **"Usuń zaznaczone"** shows as a disabled outline button (opacity .6, `not-allowed`, native `disabled`).
- 1+ rows selected: a pill "Zaznaczono 3" (sel bg, `check` icon) appears next to an enabled danger-outline button "Usuń zaznaczone" (`trash-2`, hover dangerSoft).
**Table** — min-width 920px; scrolls horizontally inside the card on narrow screens. The header is sticky with `subtle` bg and borders top and bottom. Columns in order:
1. **Checkbox** (52px). The header checkbox is tri-state: empty, `mixed` (minus icon) or checked.
2. **Nazwa**: sortable, currently ascending. The header shows `arrow-up` 14px in the text colour with `aria-sort="ascending"`. Cells are weight 600 and link to the edit form.
3. **Artyści**: relation column, comma-joined names, muted.
4. **Format** (110px): a pill (26px tall, subtle bg) with an icon. Winyl uses `disc-3`, CD uses `disc`, Kaseta uses `cassette-tape`.
5. **Gatunek**: shows "—" (muted) when empty.
6. **Półka** (72px): DM Mono 13px.
7. **Utworzono** (150px): `YYYY-MM-DD HH:mm`, muted, 13px, tabular numbers.
8. **Wypożyczony** (128px): switch column rendered as a badge. "Tak" is a green pill with an `arrow-up-right` icon; "Nie" is a muted outline pill.
- Row: 54px tall with a bottom border. Selected rows use the `sel` bg.
- Column headers are 12px/600 and muted, except the sorted column, which uses the text colour.
**Footer** (padding 14×20, top border, 13px muted)
- "1–12 z 348"
- "Na stronę" followed by a select button showing "12"
- Right-aligned pager:
- "Poprzednia" (disabled on page 1)
- Page buttons 1, 2, 3, …, 29. The current page has `sel` bg, weight 700 and `aria-current`.
- "Następna"
**States**
- *Selected*: in the mock, rows 3, 6 and 7 are selected (Enigmatic, Nevermind, Nowe sytuacje).
- *Empty*:
- 80px vertical padding inside the table area.
- 56px circle containing a `search-x` icon.
- Title "Nic nie znaleziono" (17/700).
- Text: "Żaden album nie pasuje do frazy „kaseta 1983”. Spróbuj innej nazwy lub artysty."
- Outline button "Wyczyść wyszukiwanie".
- The footer shows "0 wyników" with no pager.
- *Loading*:
- 8 skeleton rows at the same row height.
- Bars are 12px tall with radius 6. Pill placeholders match the badge sizes. Bar widths vary per row.
- The footer text reads "Wczytywanie…".
**Sample rows** (sorted by Nazwa). Columns: Nazwa · Artyści · Format · Gatunek · Półka · Utworzono · Wypożyczony.
| Nazwa | Artyści | Format | Gatunek | Półka | Utworzono | Wypożyczony |
|---|---|---|---|---|---|---|
| Astigmatic | Krzysztof Komeda | Winyl | Jazz | J-02 | 2026-09-21 14:32 | Nie |
| Ella and Louis | Ella Fitzgerald, Louis Armstrong | Winyl | Jazz | J-04 | 2026-09-20 19:05 | Tak |
| Enigmatic | Czesław Niemen, Akwarium | Winyl | Rock progresywny | R-11 | 2026-09-18 10:47 | Nie |
| Kind of Blue | Miles Davis | Winyl | Jazz | J-01 | 2026-09-17 21:13 | Nie |
| Legenda | Armia | Kaseta | Punk | K-03 | 2026-09-15 08:26 | Nie |
| Nevermind | Nirvana | CD | Grunge | C-07 | 2026-09-14 16:58 | Tak |
| Nowe sytuacje | Republika | Kaseta | Nowa fala | K-01 | 2026-09-12 12:09 | Nie |
| OK Computer | Radiohead | CD | Rock alternatywny | C-12 | 2026-09-10 18:40 | Nie |
| Rumours | Fleetwood Mac | Winyl | — | R-04 | 2026-09-08 20:15 | Tak |
| Spokojnie | Kult | Winyl | Rock | R-02 | 2026-09-05 11:31 | Nie |
| Tutu | Miles Davis | CD | Jazz fusion | C-03 | 2026-09-03 09:52 | Nie |
| Unknown Pleasures | Joy Division | Winyl | Post-punk | R-09 | 2026-09-01 22:04 | Nie |
### 4. Form — Edycja albumu (`screen=form`)
Content column max-width 980px, gap 20.
**Header row**
- 40px back button (`arrow-left`, radius 10, border) labelled "Wróć do listy".
- Title "Enigmatic" (24/700) with the subtitle "Edycja albumu · Czesław Niemen, Akwarium".
- **Tabs** on the right, styled as a segmented control:
- Track: subtle bg, 1px border, radius 12, padding 4.
- Tab: 34px tall, radius 9, 600 weight.
- Active tab: surface bg with shadow `0 1px 3px rgba(20,27,45,.12)`.
- Tabs: **"Podstawowe"** and **"Szczegóły"**, with `role="tablist"/"tab"` and `aria-selected`.
**422 error banner** (errors state only): dangerSoft bg, radius 12, padding 14×18, `circle-alert` icon 20px. Text: "**Nie udało się zapisać.** Popraw 2 pola oznaczone poniżej."
**Form card**: radius 16, padding 28.
- Grid: `repeat(auto-fit, minmax(280px, 1fr))`, gap 22×24.
- In the real implementation, map the YAML `span` value to columns: `left` → column 1, `right` → column 2, `full` → `grid-column: 1 / -1`. Below about 600px both columns collapse to one.
- FormField row: label (600, with a red `*` for required fields that is `aria-hidden`; mark the input `required` / `aria-required`), then the control, then the error message.
- Error message: 13px, danger colour, `circle-alert` 14px icon, linked through `aria-describedby`.
- The input border turns danger and the input gets `aria-invalid="true"`.
**Podstawowe tab fields**
| Field | Type | Span | Notes |
|---|---|---|---|
| Nazwa * | text | left | Error: "Pole Nazwa jest wymagane." (value empty) |
| Artyści * | multi-select relation with search | right | Chips: 30px pill, subtle bg, 22px primary-colour initials avatar, name (600), and a remove button labelled "Usuń: {name}". An inline input "Dodaj artystę…" opens a searchable dropdown. |
| Format * | dropdown | left | Options: Winyl / CD / Kaseta. The selected value shows its format icon. |
| Gatunek | relation dropdown | right | First option is **"— brak —"**, shown muted when selected. |
| Półka | text | left | Error: "Pole Półka może mieć maksymalnie 12 znaków." (value "Regał w salonie, góra") |
| Opis | textarea, 4 rows | full | Resizes vertically. |
| Wypożyczony | switch | left | Rendered as a toggle card: border, radius 12, padding 14×16. Title "Wypożyczony", helper text "Płyta jest aktualnie poza domem", and a 44×26 switch (`role="switch"`, off state uses the borderStrong track). |
| Ulubiony | checkbox | right | Toggle card with helper text "Pokazuj na początku listy" and a 20px checkbox, checked in the mock. |
**Szczegóły tab fields**
- Rok wydania (1970)
- Wytwórnia (Polskie Nagrania Muza)
- Numer katalogowy (XL 0569)
- Stan nośnika (select: "Bardzo dobry (VG+)")
- Kolor okładki, which uses the **unknown field type** fallback:
- 1.5px dashed borderStrong border, subtle bg, radius 10, min-height 44.
- `puzzle` icon with the text "Nieobsługiwany typ pola: `colorpicker`" (13px muted; the type name in DM Mono, text colour).
- The FieldRenderer should fall back to this for any unregistered `type`.
(These Szczegóły fields are placeholder content; the real ones come from the YAML.)
**Sticky footer**: surface bg, top border, padding 14×32, always visible below the scrolling form.
- Left: **"Usuń"**, a danger outline button with `trash-2`.
- Right, in order:
- "Anuluj" (ghost, muted)
- "Zapisz i zamknij" (outline)
- **"Zapisz"** (primary)
**Toast after save**
- Bottom-centre, 88px from the bottom, so it clears the footer.
- Navy `#1d2740` bg, white text, radius 14, shadow `0 16px 40px rgba(0,0,0,.25)`.
- 26px yellow circle with a check icon.
- Text: "Album „Enigmatic” został zapisany".
- Close button with `aria-label="Zamknij"`.
- Use `role="status"` and auto-dismiss after about 4s.
### 5. Relation manager — Kolekcje → Redaktorzy (`screen=relation`)
- The record header shows "Kolekcja domowa" with the subtitle "Edycja kolekcji". Tabs: "Podstawowe" and **"Redaktorzy"** (active).
- **The "Redaktorzy" tab (span full) renders only when editing an existing record, never on create.**
**Section header**
- "Redaktorzy" (17/700) with the helper text "Osoby, które mogą edytować tę kolekcję".
- On the right:
- Search, 220px wide, 38px tall, placeholder "Szukaj…"
- "Usuń zaznaczonych" (danger outline, `user-minus`)
- **"Dodaj redaktora"** (primary, `user-plus`)
- These buttons are 38px tall, one size down from page level.
**Table**: the same DataTable, inside a bordered 12px-radius container.
- Columns: checkbox, **Imię i nazwisko** (30px initials avatar plus name, 600), **E-mail** (muted), **Dodano** (160px, datetime).
- Rows are 56px tall. Row selection works the same as the main list. In the mock, Marek Nowak is selected and the header checkbox is `mixed`.
Sample editors:
- Anna Kowalska · anna.kowalska@plytarium.pl · 2026-08-02 17:20
- Marek Nowak · marek.nowak@plytarium.pl · 2026-08-14 09:03
- Julia Wiśniewska · julia.wisniewska@plytarium.pl · 2026-09-19 20:41
**"Dodaj redaktora" modal**
- Container: `role="dialog"`, `aria-modal`, 560px wide, radius 20, padding 24, gap 16, over the backdrop overlay.
- Title "Dodaj redaktora" (20/700). Helper text: "Wybierz osoby spoza obecnych redaktorów. Właściciel kolekcji nie jest wyświetlany."
- 34px close button (`x`, subtle bg).
- Search input (44px, autofocused, so the focus ring shows), placeholder "Szukaj po nazwisku lub e-mailu…".
- Candidate list: `role="listbox"`, `aria-multiselectable`.
- Each option row: 54px tall, radius 12, border, 32px initials avatar, name (600) above email (13px muted), and a checkbox on the right.
- Selected option: sel bg and a `#e0b020` border.
- Candidates are users not yet linked to the collection, **with the owner excluded on the server side**. Results are paginated at 5 per page: "1–5 z 14", with prev/next buttons (34px, radius 9).
- Footer: two equal-width buttons, "Anuluj" (outline) and **"Dodaj (N)"** (primary). N is the selection count; the button is disabled when N = 0.
- Mock: Piotr Zieliński and Katarzyna Lewandowska are selected, so the button reads "Dodaj (2)". Other candidates: Michał Kamiński, Agnieszka Dąbrowska, Paweł Szymański.
- Keyboard: Esc closes, focus is trapped inside, and focus returns to "Dodaj redaktora" on close.
---
## Interactions & Behavior
- **Sorting**: clicking a sortable header cycles asc → desc → none. Show `arrow-up` / `arrow-down`, and reflect the state in `aria-sort` and the URL query.
- **Search**: debounce at 300ms, reset to page 1, and keep the term in the URL.
- **Selection**:
- The header checkbox selects the current page.
- Selection resets on page or search change.
- Bulk delete opens a confirm dialog: "Usunąć N albumów?".
- **Pagination**: per-page options 12 / 24 / 48 / 96. Range text uses the format "{from}–{to} z {total}" with an en dash.
- **Loading**: show skeleton rows in place of the table body. Keep the toolbar and footer mounted.
- **Validation**:
- On a 422 `{errors: {field: [msg]}}` response, show the messages under each field plus the top banner, and focus the first invalid field.
- Errors clear per field on change.
- When the invalid field sits on another tab, mark that tab with a count badge.
- **Save**:
- "Zapisz" stays on the form and shows the toast.
- "Zapisz i zamknij" returns to the list and shows the toast there.
- "Anuluj" returns without saving; confirm first if the form is dirty.
- "Usuń" opens a confirm dialog.
- **Relation manager**: "Dodaj redaktora" opens the modal, and confirming POSTs the selected IDs. The table then refreshes and a toast reads "Dodano 2 redaktorów". "Usuń zaznaczonych" asks for confirmation, then detaches.
- **Transitions**: use subtle ones only, 150ms ease-out for hover/background and 200ms for modal fade and scale (0.98 → 1). No decorative animation.
- **Responsive**:
- 1280px is the design width.
- Below about 1100px the sidebar collapses to 72px icons; users can also toggle this manually and the choice persists.
- At tablet width (≥768px) tables scroll horizontally inside their card and the form grid wraps to one column when needed. The header user button hides its name.
## State Management
- Global: auth user and permissions (these drive which menu items render), plus the sidebar collapsed flag.
- Per list: `{ search, sort: {column, dir}, page, perPage, selectedIds, loading, rows, total }`, synced to the URL query.
- Per form: `{ record, dirty, errors: Record<field, string[]>, activeTab, saving, mode: 'create'|'update' }`. Tabs and fields come from the YAML schema; relation tabs require `mode === 'update'`.
- Relation modal: `{ open, search, page, candidates, total, selectedIds }`.
- Toast queue: global.
## Components to build (generic)
- `PluginRail`, `SectionPanel`, `SectionFlyout` (all driven by the navigation registry)
- `AppShell` (sidebar + header)
- `DataTable` (columns from schema; column types `text`, `relation`, `datetime`, `switch`, `badge`/`format`; selection; sort; states)
- `ListToolbar`
- `Pagination`
- `FormTabs`
- `FormGrid`
- `FormField` (label/control/error row; `span`)
- `FieldRenderer` (type registry → `text`, `textarea`, `dropdown`, `relation`, `relation-multi`, `switch`, `checkbox`, fallback `UnsupportedField`)
- `RelationManager`
- `RelationPickerModal`
- `Toast`
- `UserMenu`
## Assets
- Icons: **lucide** (the mock uses the `lucide-static` icon font). Use `lucide-vue-next` in production. Icons used:
- Navigation and brand: `sun`, `disc-3`, `disc`, `cassette-tape`, `mic-vocal`, `library`, `tags`, `settings`, `panel-left-close`
- Chevrons and arrows: `chevron-right`, `chevron-down`, `chevron-left`, `arrow-up`, `arrow-left`, `arrow-right`, `arrow-up-right`
- Actions and status: `search`, `search-x`, `plus`, `trash-2`, `check`, `minus`, `x`, `circle-alert`, `user-plus`, `user-minus`, `log-out`, `puzzle`
- Fonts: DM Sans and DM Mono from Google Fonts.
- No images. The logo is the lucide `sun` icon plus the text wordmark.
## Files
- `Direction C v2.dc.html`: the full design reference, with every screen and state switchable via props.
- `support.js`: runtime needed to open the HTML file locally. It is not part of the design.