388 lines
25 KiB
Markdown
388 lines
25 KiB
Markdown
# 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.
|