Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b51ea2505e | ||
|
|
fd2e613aa5 |
52
docs/superpowers/specs/2026-06-12-sidebar-icons-design.md
Normal file
52
docs/superpowers/specs/2026-06-12-sidebar-icons-design.md
Normal file
@@ -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(<hue>.mainChannel / 0.12)`, glyph color `<hue>.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).
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "2.4.31",
|
"version": "2.4.33",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "2.4.31",
|
"version": "2.4.33",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.102.0",
|
"@anthropic-ai/sdk": "^0.102.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "app-ts",
|
"name": "app-ts",
|
||||||
"version": "2.4.31",
|
"version": "2.4.33",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "dist/server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `audit_logs` MODIFY `created_at` DATETIME(0) NULL DEFAULT CURRENT_TIMESTAMP(0);
|
||||||
|
|
||||||
|
|
||||||
|
-- Stock invariants enforced by the database itself: a future cumulative-
|
||||||
|
-- decrement bug (the C2 class — duplicate issue lines driving a batch
|
||||||
|
-- negative and silently vanishing from stock totals) becomes a loud error
|
||||||
|
-- instead of hidden corruption. All databases verified clean of negative
|
||||||
|
-- rows before this migration (2026-06-12).
|
||||||
|
ALTER TABLE `sklad_batches`
|
||||||
|
ADD CONSTRAINT `chk_sklad_batches_quantity_nonneg` CHECK (`quantity` >= 0);
|
||||||
|
|
||||||
|
ALTER TABLE `sklad_item_locations`
|
||||||
|
ADD CONSTRAINT `chk_sklad_item_locations_quantity_nonneg` CHECK (`quantity` >= 0);
|
||||||
@@ -65,7 +65,9 @@ model audit_logs {
|
|||||||
new_values String? @db.LongText
|
new_values String? @db.LongText
|
||||||
user_agent String? @db.Text
|
user_agent String? @db.Text
|
||||||
session_id String? @db.VarChar(128)
|
session_id String? @db.VarChar(128)
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
// DATETIME, not TIMESTAMP — TIMESTAMP dies in 2038 and converts through
|
||||||
|
// the session timezone (2026-06-12 hardening migration).
|
||||||
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
|
||||||
@@index([action], map: "idx_audit_logs_action")
|
@@index([action], map: "idx_audit_logs_action")
|
||||||
@@index([created_at], map: "idx_audit_logs_created")
|
@@index([created_at], map: "idx_audit_logs_created")
|
||||||
|
|||||||
136
src/__tests__/db-constraints.test.ts
Normal file
136
src/__tests__/db-constraints.test.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||||
|
import prisma from "../config/database";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pins the 2026-06-12 DB hardening migration:
|
||||||
|
* 1. CHECK (quantity >= 0) on sklad_batches + sklad_item_locations — the DB
|
||||||
|
* itself rejects negative stock, so a future C2-class bug (cumulative
|
||||||
|
* decrements driving a batch negative) becomes a loud error instead of
|
||||||
|
* silently hidden corruption.
|
||||||
|
* 2. audit_logs.created_at is DATETIME, not TIMESTAMP — no 2038 cap.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const N = "db_constraints_";
|
||||||
|
|
||||||
|
let itemId: number;
|
||||||
|
let locationId: number;
|
||||||
|
let receiptId: number;
|
||||||
|
let lineAId: number;
|
||||||
|
let lineBId: number;
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
await prisma.sklad_batches.deleteMany({
|
||||||
|
where: { item: { name: { contains: N } } },
|
||||||
|
});
|
||||||
|
await prisma.sklad_item_locations.deleteMany({
|
||||||
|
where: { item: { name: { contains: N } } },
|
||||||
|
});
|
||||||
|
await prisma.sklad_receipt_lines.deleteMany({
|
||||||
|
where: { item: { name: { contains: N } } },
|
||||||
|
});
|
||||||
|
await prisma.sklad_receipts.deleteMany({ where: { notes: { contains: N } } });
|
||||||
|
await prisma.sklad_items.deleteMany({ where: { name: { contains: N } } });
|
||||||
|
await prisma.sklad_locations.deleteMany({
|
||||||
|
where: { name: { contains: N } },
|
||||||
|
});
|
||||||
|
await prisma.audit_logs.deleteMany({
|
||||||
|
where: { description: { contains: N } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await cleanup();
|
||||||
|
const item = await prisma.sklad_items.create({
|
||||||
|
data: { name: `${N}item`, unit: "ks" },
|
||||||
|
});
|
||||||
|
itemId = item.id;
|
||||||
|
const location = await prisma.sklad_locations.create({
|
||||||
|
data: { code: `DBC${Date.now() % 100000}`, name: `${N}loc` },
|
||||||
|
});
|
||||||
|
locationId = location.id;
|
||||||
|
const receipt = await prisma.sklad_receipts.create({
|
||||||
|
data: { notes: `${N}receipt`, status: "CONFIRMED" },
|
||||||
|
});
|
||||||
|
receiptId = receipt.id;
|
||||||
|
const lineA = await prisma.sklad_receipt_lines.create({
|
||||||
|
data: {
|
||||||
|
receipt_id: receiptId,
|
||||||
|
item_id: itemId,
|
||||||
|
quantity: 5,
|
||||||
|
unit_price: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lineAId = lineA.id;
|
||||||
|
const lineB = await prisma.sklad_receipt_lines.create({
|
||||||
|
data: {
|
||||||
|
receipt_id: receiptId,
|
||||||
|
item_id: itemId,
|
||||||
|
quantity: 5,
|
||||||
|
unit_price: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lineBId = lineB.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanup();
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("stock CHECK constraints", () => {
|
||||||
|
it("rejects inserting a negative batch quantity", async () => {
|
||||||
|
await expect(
|
||||||
|
prisma.sklad_batches.create({
|
||||||
|
data: {
|
||||||
|
item_id: itemId,
|
||||||
|
receipt_line_id: lineAId,
|
||||||
|
quantity: -1,
|
||||||
|
original_qty: 5,
|
||||||
|
unit_price: 1,
|
||||||
|
is_consumed: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects updating a batch quantity below zero", async () => {
|
||||||
|
const batch = await prisma.sklad_batches.create({
|
||||||
|
data: {
|
||||||
|
item_id: itemId,
|
||||||
|
receipt_line_id: lineBId,
|
||||||
|
quantity: 5,
|
||||||
|
original_qty: 5,
|
||||||
|
unit_price: 1,
|
||||||
|
is_consumed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
prisma.$executeRaw`UPDATE sklad_batches SET quantity = -2 WHERE id = ${batch.id}`,
|
||||||
|
).rejects.toThrow();
|
||||||
|
const fresh = await prisma.sklad_batches.findUnique({
|
||||||
|
where: { id: batch.id },
|
||||||
|
});
|
||||||
|
expect(Number(fresh?.quantity)).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a negative item_location quantity", async () => {
|
||||||
|
await expect(
|
||||||
|
prisma.sklad_item_locations.create({
|
||||||
|
data: { item_id: itemId, location_id: locationId, quantity: -1 },
|
||||||
|
}),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("audit_logs.created_at has no 2038 cap", () => {
|
||||||
|
it("accepts a far-future timestamp (DATETIME, not TIMESTAMP)", async () => {
|
||||||
|
const row = await prisma.audit_logs.create({
|
||||||
|
data: {
|
||||||
|
action: "test",
|
||||||
|
description: `${N}future`,
|
||||||
|
created_at: new Date(Date.UTC(2098, 0, 15, 12, 0, 0)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(row.created_at?.getUTCFullYear()).toBe(2098);
|
||||||
|
});
|
||||||
|
});
|
||||||
51
src/__tests__/navdata-icons.test.ts
Normal file
51
src/__tests__/navdata-icons.test.ts
Normal file
@@ -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<MenuItem & { section: string }> = 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<string, string>();
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -389,7 +389,17 @@ export default function OrderDetail() {
|
|||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
{/* flexWrap: the title must drop below the Zpět button on phones
|
||||||
|
instead of overflowing the viewport (same as Offer/Invoice
|
||||||
|
detail headers). */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/orders"
|
to="/orders"
|
||||||
@@ -399,9 +409,22 @@ export default function OrderDetail() {
|
|||||||
>
|
>
|
||||||
Zpět
|
Zpět
|
||||||
</Button>
|
</Button>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1.5,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
Objednávka {order.order_number}
|
Objednávka
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ color: "text.secondary", ml: 1, fontWeight: 400 }}
|
||||||
|
>
|
||||||
|
{order.order_number}
|
||||||
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
<StatusChip
|
<StatusChip
|
||||||
label={statusLabel(ORDER_STATUS, order.status)}
|
label={statusLabel(ORDER_STATUS, order.status)}
|
||||||
|
|||||||
@@ -298,7 +298,17 @@ export default function ProjectDetail() {
|
|||||||
mb: 3,
|
mb: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
{/* flexWrap: the title must drop below the Zpět button on phones
|
||||||
|
instead of being squeezed beside it (mirrors Offer/Invoice
|
||||||
|
detail headers). */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 2,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
component={RouterLink}
|
component={RouterLink}
|
||||||
to="/projects"
|
to="/projects"
|
||||||
@@ -308,9 +318,22 @@ export default function ProjectDetail() {
|
|||||||
>
|
>
|
||||||
Zpět
|
Zpět
|
||||||
</Button>
|
</Button>
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1.5,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography variant="h4">
|
<Typography variant="h4">
|
||||||
Projekt {project.project_number}
|
Projekt
|
||||||
|
<Box
|
||||||
|
component="span"
|
||||||
|
sx={{ color: "text.secondary", ml: 1, fontWeight: 400 }}
|
||||||
|
>
|
||||||
|
{project.project_number}
|
||||||
|
</Box>
|
||||||
</Typography>
|
</Typography>
|
||||||
<StatusChip
|
<StatusChip
|
||||||
label={STATUS_LABELS[project.status] || project.status}
|
label={STATUS_LABELS[project.status] || project.status}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { createTheme, type Theme } from "@mui/material/styles";
|
import {
|
||||||
|
createTheme,
|
||||||
|
type Theme,
|
||||||
|
type PaletteColor,
|
||||||
|
type PaletteColorChannel,
|
||||||
|
} from "@mui/material/styles";
|
||||||
|
|
||||||
const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif";
|
const FONT_BODY = "'Plus Jakarta Sans', system-ui, sans-serif";
|
||||||
const FONT_HEADING = "'Urbanist', sans-serif";
|
const FONT_HEADING = "'Urbanist', sans-serif";
|
||||||
@@ -33,6 +38,26 @@ export const theme = createTheme({
|
|||||||
warning: { main: "#b45309", contrastText: "#fff" },
|
warning: { main: "#b45309", contrastText: "#fff" },
|
||||||
error: { main: "#b91c1c", contrastText: "#fff" },
|
error: { main: "#b91c1c", contrastText: "#fff" },
|
||||||
info: { main: "#1d4ed8", contrastText: "#fff" },
|
info: { main: "#1d4ed8", contrastText: "#fff" },
|
||||||
|
// Sidebar section hues (full objects so cssVariables emits *Channel
|
||||||
|
// tokens — the nav tiles use channel-alpha washes).
|
||||||
|
teal: {
|
||||||
|
main: "#0e8a7c",
|
||||||
|
light: "#3aa99c",
|
||||||
|
dark: "#0a675d",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
main: "#6a4cb4",
|
||||||
|
light: "#8a6fd0",
|
||||||
|
dark: "#54399a",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
main: "#5c6470",
|
||||||
|
light: "#7d8694",
|
||||||
|
dark: "#454c56",
|
||||||
|
contrastText: "#fff",
|
||||||
|
},
|
||||||
background: { default: "#f4f3f1", paper: "#ffffff" },
|
background: { default: "#f4f3f1", paper: "#ffffff" },
|
||||||
text: { primary: "#1a1a1a", secondary: "#555555" },
|
text: { primary: "#1a1a1a", secondary: "#555555" },
|
||||||
divider: "rgba(0,0,0,0.1)",
|
divider: "rgba(0,0,0,0.1)",
|
||||||
@@ -46,6 +71,25 @@ export const theme = createTheme({
|
|||||||
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
warning: { main: "#f59e0b", contrastText: "#1a1a1a" },
|
||||||
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
error: { main: "#ef4444", contrastText: "#1a1a1a" },
|
||||||
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
|
info: { main: "#3b82f6", contrastText: "#1a1a1a" },
|
||||||
|
// Sidebar section hues — brighter mains so the glyphs read on dark.
|
||||||
|
teal: {
|
||||||
|
main: "#2dd4bf",
|
||||||
|
light: "#5eead4",
|
||||||
|
dark: "#14b8a6",
|
||||||
|
contrastText: "#1a1a1a",
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
main: "#a78bfa",
|
||||||
|
light: "#c4b5fd",
|
||||||
|
dark: "#8b5cf6",
|
||||||
|
contrastText: "#1a1a1a",
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
main: "#94a3b8",
|
||||||
|
light: "#cbd5e1",
|
||||||
|
dark: "#64748b",
|
||||||
|
contrastText: "#1a1a1a",
|
||||||
|
},
|
||||||
background: { default: "#0f0f0f", paper: "#1a1a1a" },
|
background: { default: "#0f0f0f", paper: "#1a1a1a" },
|
||||||
text: { primary: "#ffffff", secondary: "#a0a0a0" },
|
text: { primary: "#ffffff", secondary: "#a0a0a0" },
|
||||||
divider: "rgba(255,255,255,0.08)",
|
divider: "rgba(255,255,255,0.08)",
|
||||||
@@ -239,6 +283,24 @@ export const fonts = {
|
|||||||
mono: FONT_MONO,
|
mono: FONT_MONO,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom sidebar-section palette entries (teal/violet/slate above). The
|
||||||
|
// channel intersection matters: theme.vars.palette.<hue>.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 =
|
export type IconBadgeColor =
|
||||||
| "default"
|
| "default"
|
||||||
| "primary"
|
| "primary"
|
||||||
|
|||||||
@@ -88,9 +88,8 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
|
|||||||
sx={{
|
sx={{
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
mb: 0.25,
|
mb: 0.25,
|
||||||
py: 0.6,
|
py: 0.45,
|
||||||
color: "text.secondary",
|
color: "text.secondary",
|
||||||
"& svg": { width: 18, height: 18 },
|
|
||||||
"&.Mui-selected, &.Mui-selected:hover": {
|
"&.Mui-selected, &.Mui-selected:hover": {
|
||||||
bgcolor: "background.paper",
|
bgcolor: "background.paper",
|
||||||
color: "primary.main",
|
color: "primary.main",
|
||||||
@@ -99,8 +98,29 @@ export default function SidebarNav({ onNavigate, onLogout }: SidebarNavProps) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ListItemIcon sx={{ minWidth: 32, color: "inherit" }}>
|
<ListItemIcon sx={{ minWidth: 36, color: "inherit" }}>
|
||||||
|
{item.rawIcon ? (
|
||||||
|
item.icon
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={(t) => ({
|
||||||
|
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}
|
{item.icon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item.label}
|
primary={item.label}
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import OdinMark from "../components/odin/OdinMark";
|
import OdinMark from "../components/odin/OdinMark";
|
||||||
|
|
||||||
|
/** Palette key driving a section's tile wash + glyph color (see theme.ts). */
|
||||||
|
export type NavHue =
|
||||||
|
| "primary"
|
||||||
|
| "info"
|
||||||
|
| "teal"
|
||||||
|
| "warning"
|
||||||
|
| "violet"
|
||||||
|
| "slate";
|
||||||
|
|
||||||
export interface MenuItem {
|
export interface MenuItem {
|
||||||
path: string;
|
path: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -10,33 +19,51 @@ export interface MenuItem {
|
|||||||
matchAlso?: string[];
|
matchAlso?: string[];
|
||||||
matchExclude?: string[];
|
matchExclude?: string[];
|
||||||
icon: ReactNode;
|
icon: ReactNode;
|
||||||
|
/** Icon ships its own container (OdinMark) — SidebarNav skips the tile. */
|
||||||
|
rawIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuSection {
|
export interface MenuSection {
|
||||||
label: string;
|
label: string;
|
||||||
|
hue: NavHue;
|
||||||
items: MenuItem[];
|
items: MenuItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const menuSections: MenuSection[] = [
|
/**
|
||||||
{
|
* One stroke glyph per item, every glyph unique within the set (pinned by
|
||||||
label: "Přehled",
|
* navdata-icons.test.ts). 24-viewBox, stroke 2, round caps — rendered at
|
||||||
items: [
|
* 16px inside SidebarNav's 26px section-hue tile.
|
||||||
{
|
*/
|
||||||
path: "/",
|
function glyph(children: ReactNode) {
|
||||||
label: "Přehled",
|
return (
|
||||||
end: true,
|
|
||||||
icon: (
|
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
>
|
>
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
{children}
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
||||||
<rect x="14" y="14" width="7" height="7" rx="1" />
|
|
||||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const menuSections: MenuSection[] = [
|
||||||
|
{
|
||||||
|
label: "Přehled",
|
||||||
|
hue: "primary",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
label: "Přehled",
|
||||||
|
end: true,
|
||||||
|
// tachometr — dashboard
|
||||||
|
icon: glyph(
|
||||||
|
<>
|
||||||
|
<circle cx="12" cy="13" r="8.5" />
|
||||||
|
<path d="M12 13l4-4.5" />
|
||||||
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -44,78 +71,66 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Odin",
|
label: "Odin",
|
||||||
permission: "ai.use",
|
permission: "ai.use",
|
||||||
end: true,
|
end: true,
|
||||||
icon: <OdinMark size={22} />,
|
icon: <OdinMark size={26} />,
|
||||||
|
rawIcon: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Docházka",
|
label: "Docházka",
|
||||||
|
hue: "info",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
path: "/attendance",
|
path: "/attendance",
|
||||||
label: "Záznam",
|
label: "Záznam",
|
||||||
permission: "attendance.record",
|
permission: "attendance.record",
|
||||||
end: true,
|
end: true,
|
||||||
icon: (
|
// píchačky — hodiny
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="9" />
|
<circle cx="12" cy="12" r="9" />
|
||||||
<polyline points="12 7 12 12 15 15" />
|
<polyline points="12 7 12 12 15 15" />
|
||||||
</svg>
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/attendance/history",
|
path: "/attendance/history",
|
||||||
label: "Moje historie",
|
label: "Moje historie",
|
||||||
permission: "attendance.history",
|
permission: "attendance.history",
|
||||||
icon: (
|
// výkaz s hodinami — timesheet
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M14 3H5a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h6" />
|
||||||
stroke="currentColor"
|
<line x1="7" y1="8" x2="13" y2="8" />
|
||||||
strokeWidth="2"
|
<line x1="7" y1="12" x2="11" y2="12" />
|
||||||
>
|
<circle cx="16.5" cy="16" r="4.5" />
|
||||||
<polyline points="12 8 12 12 14 14" />
|
<polyline points="16.5 14 16.5 16 18 17" />
|
||||||
<path d="M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5" />
|
</>,
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/attendance/requests",
|
path: "/attendance/requests",
|
||||||
label: "Žádosti",
|
label: "Žádosti",
|
||||||
permission: "attendance.record",
|
permission: "attendance.record",
|
||||||
icon: (
|
// odeslaná žádost — vlaštovka
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M22 2L11 13" />
|
||||||
stroke="currentColor"
|
<path d="M22 2l-7 20-4-9-9-4z" />
|
||||||
strokeWidth="2"
|
</>,
|
||||||
>
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
<line x1="12" y1="18" x2="12" y2="12" />
|
|
||||||
<line x1="9" y1="15" x2="15" y2="15" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/attendance/approval",
|
path: "/attendance/approval",
|
||||||
label: "Schvalování",
|
label: "Schvalování",
|
||||||
permission: "attendance.approve",
|
permission: "attendance.approve",
|
||||||
icon: (
|
// razítko
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M9.5 12V9.8C9.5 8.4 8 7.8 8 6a4 4 0 0 1 8 0c0 1.8-1.5 2.4-1.5 3.8V12" />
|
||||||
stroke="currentColor"
|
<path d="M5 16a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2H5z" />
|
||||||
strokeWidth="2"
|
<line x1="5" y1="21" x2="19" y2="21" />
|
||||||
>
|
</>,
|
||||||
<path d="M9 12l2 2 4-4" />
|
|
||||||
<circle cx="12" cy="12" r="10" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -124,147 +139,108 @@ export const menuSections: MenuSection[] = [
|
|||||||
permission: "attendance.manage",
|
permission: "attendance.manage",
|
||||||
matchPrefix: "/attendance/admin",
|
matchPrefix: "/attendance/admin",
|
||||||
matchAlso: ["/attendance/create", "/attendance/location"],
|
matchAlso: ["/attendance/create", "/attendance/location"],
|
||||||
icon: (
|
// osoba + fajfka
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<circle cx="9" cy="8" r="3.5" />
|
||||||
stroke="currentColor"
|
<path d="M3 20a6 6 0 0 1 12 0" />
|
||||||
strokeWidth="2"
|
<path d="M15.5 11.5l2 2 4-4" />
|
||||||
>
|
</>,
|
||||||
<line x1="4" y1="21" x2="4" y2="14" />
|
|
||||||
<line x1="4" y1="10" x2="4" y2="3" />
|
|
||||||
<line x1="12" y1="21" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="8" x2="12" y2="3" />
|
|
||||||
<line x1="20" y1="21" x2="20" y2="16" />
|
|
||||||
<line x1="20" y1="12" x2="20" y2="3" />
|
|
||||||
<line x1="1" y1="14" x2="7" y2="14" />
|
|
||||||
<line x1="9" y1="8" x2="15" y2="8" />
|
|
||||||
<line x1="17" y1="16" x2="23" y2="16" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/attendance/balances",
|
path: "/attendance/balances",
|
||||||
label: "Správa bilancí",
|
label: "Správa bilancí",
|
||||||
permission: "attendance.balances",
|
permission: "attendance.balances",
|
||||||
icon: (
|
// kniha plus/mínus — ruční korekce bilancí
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="4" y="3" width="16" height="18" rx="2" />
|
||||||
stroke="currentColor"
|
<line x1="9" y1="8.5" x2="15" y2="8.5" />
|
||||||
strokeWidth="2"
|
<line x1="12" y1="5.5" x2="12" y2="11.5" />
|
||||||
>
|
<line x1="9" y1="16" x2="15" y2="16" />
|
||||||
<line x1="18" y1="20" x2="18" y2="10" />
|
</>,
|
||||||
<line x1="12" y1="20" x2="12" y2="4" />
|
|
||||||
<line x1="6" y1="20" x2="6" y2="14" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/plan-work",
|
path: "/plan-work",
|
||||||
label: "Plán prací",
|
label: "Plán prací",
|
||||||
permission: ["attendance.manage", "attendance.record"],
|
permission: ["attendance.manage", "attendance.record"],
|
||||||
icon: (
|
// kalendář s plánovacím blokem
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="3" y="4" width="18" height="17" rx="2" />
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<rect x="3" y="4" width="18" height="18" rx="2" />
|
|
||||||
<line x1="16" y1="2" x2="16" y2="6" />
|
<line x1="16" y1="2" x2="16" y2="6" />
|
||||||
<line x1="8" y1="2" x2="8" y2="6" />
|
<line x1="8" y1="2" x2="8" y2="6" />
|
||||||
<line x1="3" y1="10" x2="21" y2="10" />
|
<line x1="3" y1="9.5" x2="21" y2="9.5" />
|
||||||
</svg>
|
<path d="M8 14h3v3H8z" />
|
||||||
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Kniha jízd",
|
label: "Kniha jízd",
|
||||||
|
hue: "teal",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
path: "/trips",
|
path: "/trips",
|
||||||
label: "Záznam",
|
label: "Záznam",
|
||||||
permission: "trips.record",
|
permission: "trips.record",
|
||||||
end: true,
|
end: true,
|
||||||
icon: (
|
// trasa A→B
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<circle cx="5" cy="19" r="2" />
|
||||||
stroke="currentColor"
|
<circle cx="19" cy="5" r="2" />
|
||||||
strokeWidth="2"
|
<path d="M5 17C5 11 19 13 19 7" />
|
||||||
>
|
</>,
|
||||||
<circle cx="5" cy="18" r="3" />
|
|
||||||
<circle cx="19" cy="18" r="3" />
|
|
||||||
<path d="M5 18V12L8 5h8l3 7v6" />
|
|
||||||
<path d="M10 18h4" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/trips/history",
|
path: "/trips/history",
|
||||||
label: "Moje historie",
|
label: "Moje historie",
|
||||||
permission: "trips.history",
|
permission: "trips.history",
|
||||||
icon: (
|
// navštívená místa — pin
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M12 21s-6-5.2-6-10a6 6 0 0 1 12 0c0 4.8-6 10-6 10z" />
|
||||||
stroke="currentColor"
|
<circle cx="12" cy="11" r="2.5" />
|
||||||
strokeWidth="2"
|
</>,
|
||||||
>
|
|
||||||
<polyline points="12 8 12 12 14 14" />
|
|
||||||
<path d="M3.05 11a9 9 0 1 1 .5 4m-.5 5v-5h5" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/trips/admin",
|
path: "/trips/admin",
|
||||||
label: "Správa",
|
label: "Správa",
|
||||||
permission: "trips.manage",
|
permission: "trips.manage",
|
||||||
icon: (
|
// schránka s fajfkou — kniha jízd
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="5" y="4" width="14" height="17" rx="2" />
|
||||||
stroke="currentColor"
|
<path d="M9 2h6v4H9z" />
|
||||||
strokeWidth="2"
|
<path d="M9 13l2 2 4-4" />
|
||||||
>
|
</>,
|
||||||
<line x1="4" y1="21" x2="4" y2="14" />
|
|
||||||
<line x1="4" y1="10" x2="4" y2="3" />
|
|
||||||
<line x1="12" y1="21" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="8" x2="12" y2="3" />
|
|
||||||
<line x1="20" y1="21" x2="20" y2="16" />
|
|
||||||
<line x1="20" y1="12" x2="20" y2="3" />
|
|
||||||
<line x1="1" y1="14" x2="7" y2="14" />
|
|
||||||
<line x1="9" y1="8" x2="15" y2="8" />
|
|
||||||
<line x1="17" y1="16" x2="23" y2="16" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/vehicles",
|
path: "/vehicles",
|
||||||
label: "Vozidla",
|
label: "Vozidla",
|
||||||
permission: "vehicles.manage",
|
permission: "vehicles.manage",
|
||||||
icon: (
|
// auto
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M5 17H3v-4l2-5h11l3 5h2v4h-2" />
|
||||||
stroke="currentColor"
|
<circle cx="7.5" cy="17" r="2" />
|
||||||
strokeWidth="2"
|
<circle cx="16.5" cy="17" r="2" />
|
||||||
>
|
<line x1="9.5" y1="17" x2="14.5" y2="17" />
|
||||||
<rect x="1" y="3" width="15" height="13" rx="2" />
|
</>,
|
||||||
<path d="M16 8h4l3 3v5h-7V8z" />
|
|
||||||
<circle cx="5.5" cy="18.5" r="2.5" />
|
|
||||||
<circle cx="18.5" cy="18.5" r="2.5" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Administrativa",
|
label: "Administrativa",
|
||||||
|
hue: "warning",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
path: "/offers",
|
path: "/offers",
|
||||||
@@ -272,16 +248,12 @@ export const menuSections: MenuSection[] = [
|
|||||||
permission: "offers.view",
|
permission: "offers.view",
|
||||||
matchPrefix: "/offers",
|
matchPrefix: "/offers",
|
||||||
matchExclude: ["/offers/customers", "/offers/templates"],
|
matchExclude: ["/offers/customers", "/offers/templates"],
|
||||||
icon: (
|
// cenovka
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M20.6 13.4L11 3H4v7l9.6 10.4a2 2 0 0 0 2.8 0l4.2-4.2a2 2 0 0 0 0-2.8z" />
|
||||||
stroke="currentColor"
|
<circle cx="8" cy="8" r="1.5" />
|
||||||
strokeWidth="2"
|
</>,
|
||||||
>
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -289,17 +261,13 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Objednávky",
|
label: "Objednávky",
|
||||||
permission: "orders.view",
|
permission: "orders.view",
|
||||||
matchPrefix: "/orders",
|
matchPrefix: "/orders",
|
||||||
icon: (
|
// košík
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<circle cx="9" cy="20" r="1.6" />
|
||||||
stroke="currentColor"
|
<circle cx="17" cy="20" r="1.6" />
|
||||||
strokeWidth="2"
|
<path d="M2 3h3l2.7 12h10.6l2.7-9H6" />
|
||||||
>
|
</>,
|
||||||
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
|
|
||||||
<line x1="3" y1="6" x2="21" y2="6" />
|
|
||||||
<path d="M16 10a4 4 0 0 1-8 0" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -307,16 +275,13 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Faktury",
|
label: "Faktury",
|
||||||
permission: "invoices.view",
|
permission: "invoices.view",
|
||||||
matchPrefix: "/invoices",
|
matchPrefix: "/invoices",
|
||||||
icon: (
|
// účtenka
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M6 2h12v20l-3-2-3 2-3-2-3 2z" />
|
||||||
stroke="currentColor"
|
<line x1="9" y1="7" x2="15" y2="7" />
|
||||||
strokeWidth="2"
|
<line x1="9" y1="11" x2="15" y2="11" />
|
||||||
>
|
</>,
|
||||||
<line x1="12" y1="1" x2="12" y2="23" />
|
|
||||||
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -324,60 +289,44 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Projekty",
|
label: "Projekty",
|
||||||
permission: "projects.view",
|
permission: "projects.view",
|
||||||
matchPrefix: "/projects",
|
matchPrefix: "/projects",
|
||||||
icon: (
|
// složka (NAS projektové složky)
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />,
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
|
||||||
<line x1="8" y1="21" x2="16" y2="21" />
|
|
||||||
<line x1="12" y1="17" x2="12" y2="21" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/offers/customers",
|
path: "/offers/customers",
|
||||||
label: "Zákazníci",
|
label: "Zákazníci",
|
||||||
permission: "customers.view",
|
permission: "customers.view",
|
||||||
icon: (
|
// vizitka
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
stroke="currentColor"
|
<circle cx="8.5" cy="10.5" r="2.2" />
|
||||||
strokeWidth="2"
|
<path d="M5 16.5a3.5 3.5 0 0 1 7 0" />
|
||||||
>
|
<line x1="14.5" y1="9.5" x2="19" y2="9.5" />
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<line x1="14.5" y1="13.5" x2="19" y2="13.5" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
</>,
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Sklad",
|
label: "Sklad",
|
||||||
|
hue: "violet",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
path: "/warehouse",
|
path: "/warehouse",
|
||||||
label: "Přehled",
|
label: "Přehled",
|
||||||
permission: "warehouse.view",
|
permission: "warehouse.view",
|
||||||
end: true,
|
end: true,
|
||||||
icon: (
|
// skladová hala
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M3 21V9l9-5 9 5v12" />
|
||||||
stroke="currentColor"
|
<path d="M7 21v-7h10v7" />
|
||||||
strokeWidth="2"
|
<line x1="7" y1="17.5" x2="17" y2="17.5" />
|
||||||
strokeLinecap="round"
|
</>,
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
||||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
|
||||||
<line x1="12" y1="22.08" x2="12" y2="12" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -385,17 +334,13 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Položky",
|
label: "Položky",
|
||||||
permission: "warehouse.view",
|
permission: "warehouse.view",
|
||||||
matchPrefix: "/warehouse/items",
|
matchPrefix: "/warehouse/items",
|
||||||
icon: (
|
// krabice
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
||||||
<line x1="12" y1="22.08" x2="12" y2="12" />
|
<line x1="12" y1="22.08" x2="12" y2="12" />
|
||||||
</svg>
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -403,17 +348,13 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Příjmy",
|
label: "Příjmy",
|
||||||
permission: "warehouse.operate",
|
permission: "warehouse.operate",
|
||||||
matchPrefix: "/warehouse/receipts",
|
matchPrefix: "/warehouse/receipts",
|
||||||
icon: (
|
// šipka do krabice
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="4" y="12" width="16" height="9" rx="1" />
|
||||||
stroke="currentColor"
|
<polyline points="8 6 12 10 16 6" />
|
||||||
strokeWidth="2"
|
<line x1="12" y1="2" x2="12" y2="10" />
|
||||||
>
|
</>,
|
||||||
<polyline points="17 11 12 6 7 11" />
|
|
||||||
<line x1="12" y1="6" x2="12" y2="18" />
|
|
||||||
<path d="M5 19h14" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -421,17 +362,13 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Výdeje",
|
label: "Výdeje",
|
||||||
permission: "warehouse.operate",
|
permission: "warehouse.operate",
|
||||||
matchPrefix: "/warehouse/issues",
|
matchPrefix: "/warehouse/issues",
|
||||||
icon: (
|
// šipka z krabice ven
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="4" y="13" width="16" height="8" rx="1" />
|
||||||
stroke="currentColor"
|
<polyline points="8 6 12 2 16 6" />
|
||||||
strokeWidth="2"
|
<line x1="12" y1="2" x2="12" y2="11" />
|
||||||
>
|
</>,
|
||||||
<polyline points="7 13 12 18 17 13" />
|
|
||||||
<line x1="12" y1="18" x2="12" y2="6" />
|
|
||||||
<path d="M5 5h14" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -439,18 +376,12 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Rezervace",
|
label: "Rezervace",
|
||||||
permission: "warehouse.operate",
|
permission: "warehouse.operate",
|
||||||
matchPrefix: "/warehouse/reservations",
|
matchPrefix: "/warehouse/reservations",
|
||||||
icon: (
|
// krabice se záložkou
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="3" y="8" width="13" height="12" rx="1" />
|
||||||
stroke="currentColor"
|
<path d="M16 3h5v9l-2.5-2L16 12z" />
|
||||||
strokeWidth="2"
|
</>,
|
||||||
>
|
|
||||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
|
||||||
<circle cx="9" cy="7" r="4" />
|
|
||||||
<line x1="19" y1="8" x2="19" y2="14" />
|
|
||||||
<line x1="22" y1="11" x2="16" y2="11" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -458,16 +389,15 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Inventura",
|
label: "Inventura",
|
||||||
permission: "warehouse.inventory",
|
permission: "warehouse.inventory",
|
||||||
matchPrefix: "/warehouse/inventory",
|
matchPrefix: "/warehouse/inventory",
|
||||||
icon: (
|
// čárový kód — fyzický sken zásob
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<line x1="4" y1="6" x2="4" y2="18" />
|
||||||
stroke="currentColor"
|
<line x1="8" y1="6" x2="8" y2="18" />
|
||||||
strokeWidth="2"
|
<line x1="12" y1="6" x2="12" y2="18" />
|
||||||
>
|
<line x1="17" y1="6" x2="17" y2="18" />
|
||||||
<path d="M9 11l3 3L22 4" />
|
<line x1="20" y1="6" x2="20" y2="18" />
|
||||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
</>,
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -475,17 +405,12 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Reporty",
|
label: "Reporty",
|
||||||
permission: "warehouse.view",
|
permission: "warehouse.view",
|
||||||
end: true,
|
end: true,
|
||||||
icon: (
|
// graf v rámu
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
stroke="currentColor"
|
<polyline points="7 14 11 10 14 13 17 8" />
|
||||||
strokeWidth="2"
|
</>,
|
||||||
>
|
|
||||||
<line x1="18" y1="20" x2="18" y2="10" />
|
|
||||||
<line x1="12" y1="20" x2="12" y2="4" />
|
|
||||||
<line x1="6" y1="20" x2="6" y2="14" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -493,23 +418,14 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Kategorie",
|
label: "Kategorie",
|
||||||
permission: "warehouse.manage",
|
permission: "warehouse.manage",
|
||||||
matchPrefix: "/warehouse/categories",
|
matchPrefix: "/warehouse/categories",
|
||||||
icon: (
|
// tvary — třídění do kategorií
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<circle cx="7.5" cy="7.5" r="3.5" />
|
||||||
stroke="currentColor"
|
<rect x="13.5" y="4" width="7" height="7" rx="1" />
|
||||||
strokeWidth="2"
|
<path d="M7.5 13.5l3.5 7h-7z" />
|
||||||
>
|
<rect x="13.5" y="13.5" width="7" height="7" rx="3.5" />
|
||||||
<line x1="4" y1="21" x2="4" y2="14" />
|
</>,
|
||||||
<line x1="4" y1="10" x2="4" y2="3" />
|
|
||||||
<line x1="12" y1="21" x2="12" y2="12" />
|
|
||||||
<line x1="12" y1="8" x2="12" y2="3" />
|
|
||||||
<line x1="20" y1="21" x2="20" y2="16" />
|
|
||||||
<line x1="20" y1="12" x2="20" y2="3" />
|
|
||||||
<line x1="1" y1="14" x2="7" y2="14" />
|
|
||||||
<line x1="9" y1="8" x2="15" y2="8" />
|
|
||||||
<line x1="17" y1="16" x2="23" y2="16" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -517,18 +433,16 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Lokace",
|
label: "Lokace",
|
||||||
permission: "warehouse.manage",
|
permission: "warehouse.manage",
|
||||||
matchPrefix: "/warehouse/locations",
|
matchPrefix: "/warehouse/locations",
|
||||||
icon: (
|
// regál s krabicemi
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M3 21V4" />
|
||||||
stroke="currentColor"
|
<path d="M21 21V4" />
|
||||||
strokeWidth="2"
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
>
|
<line x1="3" y1="21" x2="21" y2="21" />
|
||||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
<rect x="7" y="8" width="4" height="4" />
|
||||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
<rect x="13" y="17" width="4" height="4" />
|
||||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
</>,
|
||||||
<rect x="14" y="14" width="7" height="7" rx="1" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -536,41 +450,34 @@ export const menuSections: MenuSection[] = [
|
|||||||
label: "Dodavatelé",
|
label: "Dodavatelé",
|
||||||
permission: "warehouse.manage",
|
permission: "warehouse.manage",
|
||||||
matchPrefix: "/warehouse/suppliers",
|
matchPrefix: "/warehouse/suppliers",
|
||||||
icon: (
|
// kamion
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<rect x="1" y="6" width="13" height="10" rx="1" />
|
||||||
stroke="currentColor"
|
<path d="M14 9h4l3 4v3h-7" />
|
||||||
strokeWidth="2"
|
<circle cx="6" cy="18.5" r="2" />
|
||||||
>
|
<circle cx="17.5" cy="18.5" r="2" />
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
</>,
|
||||||
<circle cx="9" cy="7" r="4" />
|
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Systém",
|
label: "Systém",
|
||||||
|
hue: "slate",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
path: "/users",
|
path: "/users",
|
||||||
label: "Uživatelé",
|
label: "Uživatelé",
|
||||||
permission: "users.view",
|
permission: "users.view",
|
||||||
icon: (
|
// lidé — uživatelské účty
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
<circle cx="9" cy="7" r="4" />
|
<circle cx="9" cy="7" r="4" />
|
||||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
</svg>
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -583,35 +490,27 @@ export const menuSections: MenuSection[] = [
|
|||||||
"settings.system",
|
"settings.system",
|
||||||
"settings.templates",
|
"settings.templates",
|
||||||
],
|
],
|
||||||
icon: (
|
// ozubené kolo
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="3" />
|
<circle cx="12" cy="12" r="3" />
|
||||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
</svg>
|
</>,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/audit-log",
|
path: "/audit-log",
|
||||||
label: "Audit log",
|
label: "Audit log",
|
||||||
permission: "settings.audit",
|
permission: "settings.audit",
|
||||||
icon: (
|
// lupa nad záznamy
|
||||||
<svg
|
icon: glyph(
|
||||||
viewBox="0 0 24 24"
|
<>
|
||||||
fill="none"
|
<path d="M4 5h12" />
|
||||||
stroke="currentColor"
|
<path d="M4 10h6" />
|
||||||
strokeWidth="2"
|
<path d="M4 15h5" />
|
||||||
>
|
<circle cx="15" cy="14" r="4.5" />
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
<line x1="18.2" y1="17.2" x2="21.5" y2="20.5" />
|
||||||
<polyline points="14 2 14 8 20 8" />
|
</>,
|
||||||
<line x1="16" y1="13" x2="8" y2="13" />
|
|
||||||
<line x1="16" y1="17" x2="8" y2="17" />
|
|
||||||
<polyline points="10 9 9 9 8 9" />
|
|
||||||
</svg>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"types": ["node", "vitest/globals"],
|
"types": ["node", "vitest/globals"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user