From fd2e613aa5545051730d8f24dcff4db1e5a92acc Mon Sep 17 00:00:00 2001 From: BOHA Date: Fri, 12 Jun 2026 23:37:44 +0200 Subject: [PATCH] feat(ui): sidebar icon refresh + unified detail-page headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar (user-approved design, docs/superpowers/specs/2026-06-12-sidebar- icons-design.md): all 31 menu items get unique contextual stroke glyphs in rounded-square tiles tinted per section (Docházka blue, Kniha jízd teal, Administrativa amber, Sklad violet, Systém slate; new teal/violet/slate palette entries with channel tokens in both schemes). Replaces the old set where 13 items shared a glyph (3x people, 3x sliders, ...) and Faktury was a dollar sign. Odin stays the only solid + animated tile. Uniqueness pinned by navdata-icons.test.ts (tsconfig.test.json gains jsx for the .tsx import). Headers: ProjectDetail and OrderDetail now match the Offer/Invoice shell — number rendered in secondary color at regular weight next to the bold entity word, and flexWrap on both left header boxes so the title drops below Zpět on phones instead of overflowing the viewport (the reported mobile bug on received orders). Co-Authored-By: Claude Fable 5 --- .../specs/2026-06-12-sidebar-icons-design.md | 52 ++ package-lock.json | 4 +- package.json | 2 +- src/__tests__/navdata-icons.test.ts | 51 ++ src/admin/pages/OrderDetail.tsx | 29 +- src/admin/pages/ProjectDetail.tsx | 29 +- src/admin/theme.ts | 64 +- src/admin/ui/SidebarNav.tsx | 28 +- src/admin/ui/navData.tsx | 587 ++++++++---------- tsconfig.test.json | 1 + 10 files changed, 489 insertions(+), 358 deletions(-) create mode 100644 docs/superpowers/specs/2026-06-12-sidebar-icons-design.md create mode 100644 src/__tests__/navdata-icons.test.ts diff --git a/docs/superpowers/specs/2026-06-12-sidebar-icons-design.md b/docs/superpowers/specs/2026-06-12-sidebar-icons-design.md new file mode 100644 index 0000000..aa7e787 --- /dev/null +++ b/docs/superpowers/specs/2026-06-12-sidebar-icons-design.md @@ -0,0 +1,52 @@ +# Sidebar icon refresh — design + +**Date:** 2026-06-12 · **Status:** approved (visual companion session, 3 screens) + +## Problem + +All 31 sidebar items have icons, but 13 share a glyph with another item +(3× "people", 3× "sliders", 2× grid/bar-chart/box/history-clock) and Faktury +uses a dollar sign in an app that bills in Kč/EUR. Duplicated glyphs defeat +the purpose of icons as unique visual anchors. + +## Decisions (user-approved) + +1. **Full refresh** of all items (option B over targeted fix). +2. **Tile language (option B of 3 rendered directions):** every item's glyph + sits in a rounded-square tile (border-radius 32%, echoing OdinMark), as a + **soft color wash — one muted hue per menu section**: + Přehled `primary` (red) · Docházka `info` (blue) · Kniha jízd `teal` · + Administrativa `warning` (amber) · Sklad `violet` · Systém `slate`. +3. **Odin stays unique:** the only **solid** (gradient) tile and the only + animated icon. No animation anywhere else. Red stays reserved for + Odin/primary. +4. **Glyph set** approved on the full-set screen: receipt for Faktury, + business card for Zákazníci, truck for Dodavatelé, stamp for Schvalování, + ledger ± for Správa bilancí, route/pin/car for Kniha jízd, barcode for + Inventura, shelf rack for Lokace, arrows into/out of a box for + Příjmy/Výdeje, warehouse hall for Sklad přehled, tag/cart/folder for + Nabídky/Objednávky/Projekty, gauge for Přehled, paper plane for Žádosti, + timesheet for Moje historie (docházka), shapes for Kategorie, chart frame + for Reporty, magnifier-over-list for Audit log. Kept concepts (now unique + in the set): clock (Záznam docházky), calendar (Plán prací), people + (Uživatelé), gear (Nastavení), box (Položky). + +## Implementation + +- `theme.ts`: add `teal`, `violet`, `slate` palette entries (light + dark + mains, full main/light/dark/contrastText so cssVariables emits `*Channel` + tokens) + TS module augmentation. +- `navData.tsx`: `MenuSection.hue` (palette key), new glyph SVGs + (stroke, 24-viewBox, width 2, round caps), `MenuItem.rawIcon` flag for + Odin (its icon ships its own tile). +- `SidebarNav.tsx`: renders the tile (26 px, radius 32%, `bgcolor: +rgba(.mainChannel / 0.12)`, glyph color `.main`, svg 16 px) + around every non-`rawIcon` item. No hardcoded hex; washes are + channel-alpha so both schemes work. +- Pinning test `navdata-icons.test.ts`: every non-raw icon renders to unique + static markup (no two items may ever share a glyph again). + +## Out of scope + +Animation for non-Odin icons (explicitly rejected), page-level icons, +mobile drawer changes (it reuses SidebarNav). diff --git a/package-lock.json b/package-lock.json index 973a93a..a4470d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "app-ts", - "version": "2.4.31", + "version": "2.4.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "app-ts", - "version": "2.4.31", + "version": "2.4.32", "license": "ISC", "dependencies": { "@anthropic-ai/sdk": "^0.102.0", diff --git a/package.json b/package.json index 4a14e93..ea2f5d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "app-ts", - "version": "2.4.31", + "version": "2.4.32", "description": "", "main": "dist/server.js", "scripts": { diff --git a/src/__tests__/navdata-icons.test.ts b/src/__tests__/navdata-icons.test.ts new file mode 100644 index 0000000..6b96f09 --- /dev/null +++ b/src/__tests__/navdata-icons.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { isValidElement, type ReactElement } from "react"; +import { menuSections, type MenuItem } from "../admin/ui/navData"; + +/** + * Pins the sidebar icon-refresh contract (2026-06-12 design): every menu + * item has a glyph, and no two items share one — duplicated glyphs were the + * whole problem the refresh fixed (3× "people", 3× "sliders", …). Odin's + * rawIcon (OdinMark, MUI-styled + animated) is exempt from rendering here + * but still must exist and stay the ONLY rawIcon. + */ + +const allItems: Array = menuSections.flatMap( + (s) => s.items.map((item) => ({ section: s.label, ...item })), +); + +describe("sidebar nav icons", () => { + it("every item has an icon and a section hue", () => { + for (const section of menuSections) { + expect(section.hue, `section ${section.label}`).toBeTruthy(); + } + for (const item of allItems) { + expect(isValidElement(item.icon), `${item.section}/${item.label}`).toBe( + true, + ); + } + }); + + it("Odin is the only rawIcon (its own tile + animation stay unique)", () => { + const raw = allItems.filter((i) => i.rawIcon); + expect(raw.map((i) => i.label)).toEqual(["Odin"]); + }); + + it("no two items share a glyph", () => { + const rendered = allItems + .filter((i) => !i.rawIcon) + .map((item) => ({ + key: `${item.section}/${item.label}`, + markup: renderToStaticMarkup(item.icon as ReactElement), + })); + + const seen = new Map(); + for (const { key, markup } of rendered) { + const dupe = seen.get(markup); + expect(dupe, `${key} shares its glyph with ${dupe}`).toBeUndefined(); + seen.set(markup, key); + } + expect(seen.size).toBe(rendered.length); + }); +}); diff --git a/src/admin/pages/OrderDetail.tsx b/src/admin/pages/OrderDetail.tsx index 00f9911..72e03b1 100644 --- a/src/admin/pages/OrderDetail.tsx +++ b/src/admin/pages/OrderDetail.tsx @@ -389,7 +389,17 @@ export default function OrderDetail() { mb: 3, }} > - + {/* flexWrap: the title must drop below the Zpět button on phones + instead of overflowing the viewport (same as Offer/Invoice + detail headers). */} + - + - Objednávka {order.order_number} + Objednávka + + {order.order_number} + - + {/* flexWrap: the title must drop below the Zpět button on phones + instead of being squeezed beside it (mirrors Offer/Invoice + detail headers). */} + - + - Projekt {project.project_number} + Projekt + + {project.project_number} + .mainChannel is how +// the nav tiles build their alpha washes. +type NavPaletteColor = PaletteColor & PaletteColorChannel; + +declare module "@mui/material/styles" { + interface Palette { + teal: NavPaletteColor; + violet: NavPaletteColor; + slate: NavPaletteColor; + } + interface PaletteOptions { + teal?: PaletteOptions["primary"]; + violet?: PaletteOptions["primary"]; + slate?: PaletteOptions["primary"]; + } +} + export type IconBadgeColor = | "default" | "primary" diff --git a/src/admin/ui/SidebarNav.tsx b/src/admin/ui/SidebarNav.tsx index 0f75263..1b869fd 100644 --- a/src/admin/ui/SidebarNav.tsx +++ b/src/admin/ui/SidebarNav.tsx @@ -88,9 +88,8 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) { sx={{ borderRadius: 2, mb: 0.25, - py: 0.6, + py: 0.45, color: "text.secondary", - "& svg": { width: 18, height: 18 }, "&.Mui-selected, &.Mui-selected:hover": { bgcolor: "background.paper", color: "primary.main", @@ -99,8 +98,29 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) { }, }} > - - {item.icon} + + {item.rawIcon ? ( + item.icon + ) : ( + ({ + width: 26, + height: 26, + // Echoes OdinMark's rounded-square tile; only Odin + // is solid + animated, these are quiet washes. + borderRadius: "32%", + flexShrink: 0, + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + bgcolor: `rgba(${t.vars!.palette[section.hue].mainChannel} / 0.12)`, + color: t.vars!.palette[section.hue].main, + "& svg": { width: 16, height: 16 }, + })} + > + {item.icon} + + )} + {children} + + ); +} + export const menuSections: MenuSection[] = [ { label: "Přehled", + hue: "primary", items: [ { path: "/", label: "Přehled", end: true, - icon: ( - - - - - - + // tachometr — dashboard + icon: glyph( + <> + + + , ), }, { @@ -44,78 +71,66 @@ export const menuSections: MenuSection[] = [ label: "Odin", permission: "ai.use", end: true, - icon: , + icon: , + rawIcon: true, }, ], }, { label: "Docházka", + hue: "info", items: [ { path: "/attendance", label: "Záznam", permission: "attendance.record", end: true, - icon: ( - + // píchačky — hodiny + icon: glyph( + <> - + , ), }, { path: "/attendance/history", label: "Moje historie", permission: "attendance.history", - icon: ( - - - - + // výkaz s hodinami — timesheet + icon: glyph( + <> + + + + + + , ), }, { path: "/attendance/requests", label: "Žádosti", permission: "attendance.record", - icon: ( - - - - - - + // odeslaná žádost — vlaštovka + icon: glyph( + <> + + + , ), }, { path: "/attendance/approval", label: "Schvalování", permission: "attendance.approve", - icon: ( - - - - + // razítko + icon: glyph( + <> + + + + , ), }, { @@ -124,147 +139,108 @@ export const menuSections: MenuSection[] = [ permission: "attendance.manage", matchPrefix: "/attendance/admin", matchAlso: ["/attendance/create", "/attendance/location"], - icon: ( - - - - - - - - - - - + // osoba + fajfka + icon: glyph( + <> + + + + , ), }, { path: "/attendance/balances", label: "Správa bilancí", permission: "attendance.balances", - icon: ( - - - - - + // kniha plus/mínus — ruční korekce bilancí + icon: glyph( + <> + + + + + , ), }, { path: "/plan-work", label: "Plán prací", permission: ["attendance.manage", "attendance.record"], - icon: ( - - + // kalendář s plánovacím blokem + icon: glyph( + <> + - - + + + , ), }, ], }, { label: "Kniha jízd", + hue: "teal", items: [ { path: "/trips", label: "Záznam", permission: "trips.record", end: true, - icon: ( - - - - - - + // trasa A→B + icon: glyph( + <> + + + + , ), }, { path: "/trips/history", label: "Moje historie", permission: "trips.history", - icon: ( - - - - + // navštívená místa — pin + icon: glyph( + <> + + + , ), }, { path: "/trips/admin", label: "Správa", permission: "trips.manage", - icon: ( - - - - - - - - - - - + // schránka s fajfkou — kniha jízd + icon: glyph( + <> + + + + , ), }, { path: "/vehicles", label: "Vozidla", permission: "vehicles.manage", - icon: ( - - - - - - + // auto + icon: glyph( + <> + + + + + , ), }, ], }, { label: "Administrativa", + hue: "warning", items: [ { path: "/offers", @@ -272,16 +248,12 @@ export const menuSections: MenuSection[] = [ permission: "offers.view", matchPrefix: "/offers", matchExclude: ["/offers/customers", "/offers/templates"], - icon: ( - - - - + // cenovka + icon: glyph( + <> + + + , ), }, { @@ -289,17 +261,13 @@ export const menuSections: MenuSection[] = [ label: "Objednávky", permission: "orders.view", matchPrefix: "/orders", - icon: ( - - - - - + // košík + icon: glyph( + <> + + + + , ), }, { @@ -307,16 +275,13 @@ export const menuSections: MenuSection[] = [ label: "Faktury", permission: "invoices.view", matchPrefix: "/invoices", - icon: ( - - - - + // účtenka + icon: glyph( + <> + + + + , ), }, { @@ -324,60 +289,44 @@ export const menuSections: MenuSection[] = [ label: "Projekty", permission: "projects.view", matchPrefix: "/projects", - icon: ( - - - - - + // složka (NAS projektové složky) + icon: glyph( + , ), }, { path: "/offers/customers", label: "Zákazníci", permission: "customers.view", - icon: ( - - - - - - + // vizitka + icon: glyph( + <> + + + + + + , ), }, ], }, { label: "Sklad", + hue: "violet", items: [ { path: "/warehouse", label: "Přehled", permission: "warehouse.view", end: true, - icon: ( - - - - - + // skladová hala + icon: glyph( + <> + + + + , ), }, { @@ -385,17 +334,13 @@ export const menuSections: MenuSection[] = [ label: "Položky", permission: "warehouse.view", matchPrefix: "/warehouse/items", - icon: ( - + // krabice + icon: glyph( + <> - + , ), }, { @@ -403,17 +348,13 @@ export const menuSections: MenuSection[] = [ label: "Příjmy", permission: "warehouse.operate", matchPrefix: "/warehouse/receipts", - icon: ( - - - - - + // šipka do krabice + icon: glyph( + <> + + + + , ), }, { @@ -421,17 +362,13 @@ export const menuSections: MenuSection[] = [ label: "Výdeje", permission: "warehouse.operate", matchPrefix: "/warehouse/issues", - icon: ( - - - - - + // šipka z krabice ven + icon: glyph( + <> + + + + , ), }, { @@ -439,18 +376,12 @@ export const menuSections: MenuSection[] = [ label: "Rezervace", permission: "warehouse.operate", matchPrefix: "/warehouse/reservations", - icon: ( - - - - - - + // krabice se záložkou + icon: glyph( + <> + + + , ), }, { @@ -458,16 +389,15 @@ export const menuSections: MenuSection[] = [ label: "Inventura", permission: "warehouse.inventory", matchPrefix: "/warehouse/inventory", - icon: ( - - - - + // čárový kód — fyzický sken zásob + icon: glyph( + <> + + + + + + , ), }, { @@ -475,17 +405,12 @@ export const menuSections: MenuSection[] = [ label: "Reporty", permission: "warehouse.view", end: true, - icon: ( - - - - - + // graf v rámu + icon: glyph( + <> + + + , ), }, { @@ -493,23 +418,14 @@ export const menuSections: MenuSection[] = [ label: "Kategorie", permission: "warehouse.manage", matchPrefix: "/warehouse/categories", - icon: ( - - - - - - - - - - - + // tvary — třídění do kategorií + icon: glyph( + <> + + + + + , ), }, { @@ -517,18 +433,16 @@ export const menuSections: MenuSection[] = [ label: "Lokace", permission: "warehouse.manage", matchPrefix: "/warehouse/locations", - icon: ( - - - - - - + // regál s krabicemi + icon: glyph( + <> + + + + + + + , ), }, { @@ -536,41 +450,34 @@ export const menuSections: MenuSection[] = [ label: "Dodavatelé", permission: "warehouse.manage", matchPrefix: "/warehouse/suppliers", - icon: ( - - - - - - + // kamion + icon: glyph( + <> + + + + + , ), }, ], }, { label: "Systém", + hue: "slate", items: [ { path: "/users", label: "Uživatelé", permission: "users.view", - icon: ( - + // lidé — uživatelské účty + icon: glyph( + <> - + , ), }, { @@ -583,35 +490,27 @@ export const menuSections: MenuSection[] = [ "settings.system", "settings.templates", ], - icon: ( - + // ozubené kolo + icon: glyph( + <> - + , ), }, { path: "/audit-log", label: "Audit log", permission: "settings.audit", - icon: ( - - - - - - - + // lupa nad záznamy + icon: glyph( + <> + + + + + + , ), }, ], diff --git a/tsconfig.test.json b/tsconfig.test.json index 894715f..2d1ebf8 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,6 +4,7 @@ "module": "commonjs", "lib": ["ES2022"], "types": ["node", "vitest/globals"], + "jsx": "react-jsx", "moduleResolution": "node", "noEmit": true, "strict": true,