commit b8bf987d0e5271ad2569129daad36c5500934164 Author: DeeCee03 Date: Sat Aug 29 17:25:20 2026 +0200 feat(dashboard): Analytics-Dashboard als Gestaltungsvorlage zur Übergabe UI/UX-Prototyp der Analytics-Dashboards in HiveGrid, fachlich auf einen SHK-/Handwerksbetrieb zugeschnitten. Ziel ist die Übergabe an die Entwicklung, nicht der Produktivbetrieb. Inhalt: sechs Seiten (Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten), 26 KPIs, neun Chart-Bausteine, Filtersystem mit seitenabhängiger Relevanz und ein deterministischer Demo-Datensatz (420 Kunden, 1.900 Aufträge, Stichtag 30.07.2026). Drei Quellen sind zusammengeführt: die fachliche Planung mit dem Branchenpartner (Januar 2026), die Subframe-Komponentenbibliothek aus dem HiveGrid-Pitch und IBM Carbon Data Visualization für die Diagrammregeln. Herkunft und bewusste Abweichungen stehen in docs/HERKUNFT.md. Dokumentation: README, HANDOVER (was echte Logik braucht), docs/DESIGN-SYSTEM, KPI-KATALOG, DATENMODELL, NAVIGATION, HERKUNFT, ZIELSYSTEM (Einbauort in HiveGrid Cloud) und LEITPLANKEN (welche Cloud-Screens als Vorbild taugen). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2687b56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules +.next +out +build +.DS_Store +*.pem +npm-debug.log* +.env* +next-env.d.ts +tsconfig.tsbuildinfo diff --git a/HANDOVER.md b/HANDOVER.md new file mode 100644 index 0000000..50418b8 --- /dev/null +++ b/HANDOVER.md @@ -0,0 +1,119 @@ +# Übergabe an die Entwicklung + +Dieser Prototyp liefert **Gestaltung und Interaktion**, nicht die Anbindung. +Was echt ist, was Attrappe ist und was beim Einbau zu tun bleibt: + +--- + +## 1. Was fertig ist und übernommen werden kann + +| Bereich | Datei(en) | Zustand | +|---|---|---| +| Kennzahlen-Berechnung | `features/dashboard/data/calculations.ts` | **Reine Funktionen** über typisierten Eingangsdaten — direkt übernehmbar | +| Chart-Theme | `features/dashboard/utils/chartTheme.ts` | Carbon-Tokens, Palette, Achsen-Props, Margins | +| Formatierung | `features/dashboard/utils/formatters.ts` | Deutsche Zahlen-, Währungs- und Datumsformate | +| Chart-Komponenten | `features/dashboard/components/charts/` | Vollständig, mit Leerzuständen und Tooltips | +| Karten & Tabellen | `features/dashboard/components/cards/`, `tables/` | Vollständig | +| Filtersystem | `features/dashboard/context/FilterContext.tsx`, `data/filtering.ts` | State und Relevanzlogik fertig | +| Seitenaufbau | `features/dashboard/pages/` | Raster und Reihenfolge stehen | + +--- + +## 2. Was echte Logik braucht + +### 2.1 Datenquelle austauschen + +Alles hängt an **einer** Stelle: `features/dashboard/data/seed.ts` exportiert +`DATENSATZ` vom Typ `Datensatz`. Diesen Export durch echte Daten ersetzen — +Loader, Server-Query oder Service, je nach Architektur. Alle Berechnungen und +Komponenten arbeiten unverändert weiter. + +Feldzuordnung zu den HiveGrid-Entities steht in [`docs/DATENMODELL.md`](docs/DATENMODELL.md). + +**Achtung Zeitbezug:** `features/dashboard/utils/dateUtils.ts` definiert +`HEUTE` als festen Stichtag (30.07.2026), damit der Prototyp reproduzierbar +bleibt. Beim Einbau durch `new Date()` ersetzen — die Konstante wird an +elf Stellen verwendet, alle über den Import. + +### 2.2 Filter serverseitig anwenden + +Aktuell wird komplett clientseitig gefiltert (`data/filtering.ts`). Bei echten +Datenmengen gehört das in die Query. Der State im `FilterContext` lässt sich +dafür in URL-Parameter serialisieren; die Relevanzlogik (`FILTER_RELEVANZ`) +bleibt clientseitig, weil sie nur die Anzeige steuert. + +### 2.3 Aktionen ohne Wirkung + +Diese Elemente lösen bewusst nur einen Hinweis-Toast aus: + +| Element | Wo | Erwartetes Verhalten | +|---|---|---| +| Export (Seitenkopf) | `components/layout/PageHeader.tsx` | PDF-/CSV-Export der Ansicht | +| Teilen | `components/layout/PageHeader.tsx` | Link mit eingefrorenen Filtern | +| Aktualisieren | `components/layout/PageHeader.tsx` | Daten neu laden | +| „Als Bild exportieren" | `components/cards/ChartCard.tsx` | PNG-Export des Charts | +| Einträge in „Handlungsbedarf" | `components/cards/AlertList.tsx` | Absprung in die jeweilige Arbeitsliste — die Handlung je Zeile (`aktion`) beschreibt, was dort passieren soll | +| Tabellenzeilen | `components/tables/DataTable.tsx` | Absprung auf den Datensatz | +| Suche und Glocke (Kopfzeile) | `components/layout/AppShell.tsx` | Attrappe, gehört zur bestehenden Shell | + +### 2.4 Was funktioniert und erhalten bleiben sollte + +- **Zeitraum-Umschaltung** rechnet alle Kennzahlen und Charts neu, inklusive Trendvergleich zur Vorperiode +- **Mehrfachfilter** (Gewerk, Status, Quelle, Region, Mitarbeiter) mit entfernbaren Chips +- **Seitenabhängige Filterrelevanz** — irrelevante Filter werden gar nicht erst angeboten +- **Widgets ausblenden** über das ⋯-Menü jeder Karte, Wiedereinblenden über die Filterleiste, persistiert +- **Tabellen** sortieren, durchsuchen, blättern +- **Hover** auf Balken, Ringsegmenten, Trichterstufen und Verlaufspunkten +- **Legenden im Ring** heben beim Klick das jeweilige Segment hervor +- **Tastaturbedienung**: sichtbarer Fokus auf allen interaktiven Elementen, Escape schließt Dropdown und Karten-Menü + +Persistiert wird in `localStorage` unter `hivegrid.analytics.*`. In der echten +Software gehört das an das Benutzerprofil. + +--- + +## 3. Bewusste Entscheidungen + +**Recharts statt der Subframe-Chart-Komponenten.** Die Subframe-Wrapper +(`ui/components/AreaChart` etc.) geben zu wenig Kontrolle über Achsen, Tooltips +und Ringproportionen, um die Carbon-Regeln umzusetzen. Die übrige Oberfläche +nutzt weiterhin die Subframe-Bibliothek und deren Design-Tokens. + +**Deterministische Dummy-Daten.** Fester Seed statt `Math.random()`: Die Zahlen +bleiben über Reloads und Screenshots identisch, sonst ist keine Abstimmung über +konkrete Werte möglich. + +**Team-Seite enthalten,** obwohl die Planung sie für v2 vorgesehen hatte. +Auslastung ist im Handwerk die Kennzahl, nach der disponiert wird — ohne sie +fehlt dem Dashboard der Bezug zum Tagesgeschäft. + +**Kein Dark Mode.** Die Carbon-Palette ist hier im Light Theme hinterlegt. +Ein Dark Theme braucht die zweite Palettenvariante (in `@carbon/charts` +enthalten) und eigene Kontrastprüfung — bewusst nicht angefangen, statt es +halb zu machen. + +--- + +## 4. Empfohlene Reihenfolge beim Einbau + +1. `data/types.ts` gegen die echten Entity-Typen abgleichen +2. `data/seed.ts` durch die echte Datenquelle ersetzen +3. `HEUTE` in `utils/dateUtils.ts` auf `new Date()` umstellen +4. Eine Seite vollständig durchziehen (Übersicht) — dabei zeigt sich, welche Felder fehlen +5. Filter serverseitig nachziehen, sobald die Datenmengen es erfordern +6. Export, Absprünge und Persistenz am Benutzerprofil ergänzen + +--- + +## 5. Offene Punkte für die Fachabstimmung + +- **Auslastung** rechnet produktive Stunden gegen Vertragsstunden. Ob Rüst- und + Fahrzeiten als produktiv zählen, ist eine betriebliche Festlegung — die + Schwellen (70 % / 95 %) stehen in `components/charts/AuslastungsBalken.tsx`. +- **Umsatz** ist hier Zahlungseingang, nicht Rechnungsstellung. Für die + betriebswirtschaftliche Auswertung kann die andere Definition gewünscht sein; + beide Varianten sind aus denselben Daten berechenbar. +- **Deckungsbeitrag** basiert auf einer geschätzten Marge je Auftrag. Ob echte + Kostendaten vorliegen, ist offen. +- **Förderquote BAFA/KfW** ist als eigener Block angelegt, weil sie im + Wärmepumpengeschäft entscheidend ist. Sie braucht ein Feld am Auftrag. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe07985 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# HiveGrid Analytics Dashboard — UI/UX-Prototyp + +Gestaltungsvorlage für die Analytics-Dashboards in HiveGrid, fachlich auf einen +SHK-/Handwerksbetrieb zugeschnitten. **Ziel dieses Repos ist die Übergabe an die +Entwicklung** — nicht der Produktivbetrieb. + +```bash +npm install +npm run dev # http://localhost:4300 +``` + +--- + +## Was hier drin ist + +| Bereich | Inhalt | +|---|---| +| **6 Seiten** | Übersicht, Finanzen, Aufträge, Kunden, Team, Komponenten | +| **26 KPIs** | vollständig definiert in [`docs/KPI-KATALOG.md`](docs/KPI-KATALOG.md) | +| **9 Chart-Bausteine** | Fläche, Säulen, liegende Balken, Ring, Trichter, Auslastung, Sparkline, Legende, Leerzustand | +| **Filtersystem** | Zeitraum + 5 Dimensionen, seitenabhängig relevant, in localStorage persistiert | +| **Demo-Datensatz** | deterministisch erzeugt: 420 Kunden, 1.900 Aufträge, ~1.260 Rechnungen, 2,4 Mio. € Jahresumsatz | + +Alles ist deutschsprachig, weil die Oberfläche deutschsprachig ist — inklusive +Bezeichner im Code. Das erspart beim Einbau die ständige Übersetzung zwischen +`revenue` und „Umsatz". + +--- + +## Wo was liegt + +``` +app/ Next.js App Router — je Seite nur ein Wrapper +features/dashboard/ + components/ + layout/ AppShell, PageHeader, PageContainer, Navigation + cards/ KPICard, ChartCard, AlertList, StatZeile + charts/ alle Visualisierungen + gemeinsamer Tooltip + filters/ Filterleiste und Mehrfachauswahl + tables/ DataTable (sortieren, suchen, blättern) + Badges + context/ Filter-, Widget- und Toast-State + data/ types · seed (Dummy-Daten) · calculations · filtering + hooks/ useAnalyseDaten + pages/ die sechs Seitenkompositionen + utils/ chartTheme (Carbon-Tokens), formatters, dateUtils +ui/ Subframe-Komponentenbibliothek (unverändert übernommen) +docs/ Design-System, KPI-Katalog, Navigation, Datenmodell, Herkunft, Zielsystem, Leitplanken +``` + +**Wichtigste Datei für den Einbau:** `features/dashboard/data/calculations.ts`. +Dort steht jede Kennzahl als reine Funktion — Datenquelle austauschen, Rest bleibt. + +--- + +## Grundlagen + +Drei Quellen sind hier zusammengeführt: + +1. **Fachliche Planung** (Januar 2026, mit dem Branchenpartner erarbeitet) — + Seitenschnitt, KPI-Auswahl, Filtersystem, Zahlen- und Spacing-Regeln. +2. **Bestehender Pitch-Prototyp** — die Subframe-Komponentenbibliothek unter `ui/` + und die Gestaltungssprache der Oberfläche. +3. **IBM Carbon Data Visualization** — Chart-Regeln, Farbpalette, Achsen, Tooltips. + Siehe [`docs/DESIGN-SYSTEM.md`](docs/DESIGN-SYSTEM.md). + +Welche Quelle was beigesteuert hat — und an welchen Stellen bewusst von der +Januar-Planung abgewichen wurde — steht in [`docs/HERKUNFT.md`](docs/HERKUNFT.md). + +--- + +## Für die Entwicklung + +[`HANDOVER.md`](HANDOVER.md) listet auf, was echte Logik braucht, was bewusst +Attrappe ist und in welcher Reihenfolge der Einbau sinnvoll ist. + +Die Seite **/komponenten** zeigt jeden Baustein isoliert mit Dateipfad — der +schnellste Weg, sich zu orientieren. + +--- + +## Zielsystem + +Eingebaut wird das hier in **HiveGrid Cloud** (`HiveGrid-Cloud`, Gitea +`toolctrl-labs`) — die eigentliche Software: pnpm-Monorepo, React Router 7, +Prisma/Postgres, mandantenfähig. Zielort ist der Tenant-Dashboard-Screen unter +`…//dashboard`. + +Dort steht bereits eine Implementierung — bewusst als **Interim-UI**: gebaut aus +derselben Subframe-Bibliothek gegen ein stabiles View-Model, damit die +Backend-Tickets nicht auf den finalen Screen warten müssen. Die finale Optik ist +dort als Nachzug vorgemerkt. Genau die Lücke füllt dieser Prototyp. + +Die Ausgangslage ist günstiger als üblich: **dasselbe Subframe-Projekt** +(82 der 86 Komponenten identisch benannt) und **dieselbe Chart-Bibliothek** +(Recharts). Der Einbau ist ein Port, keine Neuimplementierung. + +Anzupassen sind vor allem vier Dinge — Daten kommen als View-Model-Prop mit fünf +Zuständen statt als Import, Texte laufen über i18n, Diagramme brauchen eine +Textalternative für Screenreader, und das App-Chrome dieses Prototyps entfällt, +weil die Zielanwendung Shell und Sidebar mitbringt. + +**Nicht alles in der Zielanwendung sieht heute final aus.** HiveGrid Cloud +arbeitet designer-first: Für jede Phase gibt es einen Brief mit den benötigten +UI-Elementen, den finalen Screen entwirft der Designer in Subframe — Layout und +Anordnung sind ausdrücklich dessen Entscheidung. Ein großer Teil dieser Screens +ist noch offen und läuft bis dahin auf Interim-UI. Für diesen Prototyp heißt +das: Verbindlich zu übergeben sind **Kennzahl-Definitionen, Chart-Regeln und +Verhalten** — die Pixelanordnung ist ein Vorschlag. + +Details, Dateipfade und die konkrete Einbau-Liste stehen in +[`docs/ZIELSYSTEM.md`](docs/ZIELSYSTEM.md). Welche Bereiche der Zielanwendung +bereits finale Subframe-Screens tragen — und damit als Leitplanke taugen — steht +in [`docs/LEITPLANKEN.md`](docs/LEITPLANKEN.md). diff --git a/app/auftraege/page.tsx b/app/auftraege/page.tsx new file mode 100644 index 0000000..29bf9f2 --- /dev/null +++ b/app/auftraege/page.tsx @@ -0,0 +1,5 @@ +import { AuftraegeSeite } from "@/features/dashboard/pages/AuftraegeSeite"; + +export default function Page() { + return ; +} diff --git a/app/finanzen/page.tsx b/app/finanzen/page.tsx new file mode 100644 index 0000000..0fc5bf8 --- /dev/null +++ b/app/finanzen/page.tsx @@ -0,0 +1,5 @@ +import { FinanzenSeite } from "@/features/dashboard/pages/FinanzenSeite"; + +export default function Page() { + return ; +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..cb2c866 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,79 @@ +@import "tailwindcss"; +@import "../ui/theme.css"; + +/* + * HiveGrid Analytics Dashboard + * --------------------------------------------------------------- + * Chrome (Sidebar, Karten, Buttons, Tabellen) nutzt die Subframe- + * Design-Tokens aus ui/theme.css. + * Die Datenvisualisierung folgt IBM Carbon (siehe docs/DESIGN-SYSTEM.md) + * und definiert eigene Tokens, damit Charts unabhaengig vom Brand-Theme + * lesbar und barrierearm bleiben. + */ + +:root { + /* --- Carbon Data-Viz Tokens (Light Theme) --- */ + --dv-grid: #e0e0e0; /* cds-layer-accent-01 - Gridlines */ + --dv-axis: #8d8d8d; /* cds-border-strong-01 - Achsenlinie */ + --dv-text: #161616; /* cds-text-primary - Chart-Titel */ + --dv-text-secondary: #525252;/* cds-text-secondary - Achsenlabels */ + --dv-tooltip-bg: #393939; /* cds-background-inverse */ + --dv-tooltip-fg: #ffffff; /* cds-text-inverse */ + + /* Vertikaler Rhythmus fuer Dashboard-Raster (8px-System) */ + --dv-card-padding: 16px; + --dv-card-gap: 16px; +} + +body { + background: var(--color-neutral-50, #fafafa); + color: var(--color-text-neutral-primary); + font-family: "Inter", system-ui, -apple-system, sans-serif; + font-variant-numeric: tabular-nums; +} + +/* + * Fokus-Sichtbarkeit. + * Eine Regel fuer alles Interaktive statt focus:-Klassen an jedem Element. + * :focus-visible zeigt den Ring nur bei Tastaturbedienung, nicht beim Klick. + */ +:where(a, button, [role="button"], input, select, textarea, [tabindex]):focus-visible { + outline: none; + box-shadow: var(--shadow-focus, 0 0 0 3px rgb(7 145 240 / 0.4)); + border-radius: var(--radius-sm, 4px); +} + +/* Radix-Portale (Dropdowns, Tooltips) ueber den Sticky-Header heben. */ +[data-radix-popper-content-wrapper] { + z-index: 1000 !important; +} + +/* Recharts: Fokus-Outline der SVG-Wrapper unterdruecken, Cursor vereinheitlichen. */ +.recharts-wrapper:focus, +.recharts-surface:focus { + outline: none; +} +.recharts-sector, +.recharts-rectangle, +.recharts-dot { + transition: opacity 140ms ease, filter 140ms ease; +} + +/* Scrollbars in Karten dezent halten. */ +.dv-scroll::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.dv-scroll::-webkit-scrollbar-thumb { + background: #d4d4d4; + border-radius: 999px; +} +.dv-scroll::-webkit-scrollbar-track { + background: transparent; +} + +@media print { + .dv-no-print { + display: none !important; + } +} diff --git a/app/komponenten/page.tsx b/app/komponenten/page.tsx new file mode 100644 index 0000000..cfe5ccc --- /dev/null +++ b/app/komponenten/page.tsx @@ -0,0 +1,5 @@ +import { KomponentenSeite } from "@/features/dashboard/pages/KomponentenSeite"; + +export default function Page() { + return ; +} diff --git a/app/kunden/page.tsx b/app/kunden/page.tsx new file mode 100644 index 0000000..94aadac --- /dev/null +++ b/app/kunden/page.tsx @@ -0,0 +1,5 @@ +import { KundenSeite } from "@/features/dashboard/pages/KundenSeite"; + +export default function Page() { + return ; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..b0709b1 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import { AppShell } from "@/features/dashboard/components/layout/AppShell"; +import { DashboardProviders } from "@/features/dashboard/context/DashboardProviders"; +import "./globals.css"; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], + weight: ["400", "500", "600", "700"], +}); + +export const metadata: Metadata = { + title: "HiveGrid · Analytics Dashboard", + description: + "UI/UX-Prototyp der Analytics-Dashboards für Handwerksbetriebe (SHK) — Übergabe an die Entwicklung", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..267a6d7 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,5 @@ +import { UebersichtSeite } from "@/features/dashboard/pages/UebersichtSeite"; + +export default function Page() { + return ; +} diff --git a/app/team/page.tsx b/app/team/page.tsx new file mode 100644 index 0000000..7618476 --- /dev/null +++ b/app/team/page.tsx @@ -0,0 +1,5 @@ +import { TeamSeite } from "@/features/dashboard/pages/TeamSeite"; + +export default function Page() { + return ; +} diff --git a/docs/DATENMODELL.md b/docs/DATENMODELL.md new file mode 100644 index 0000000..636c726 --- /dev/null +++ b/docs/DATENMODELL.md @@ -0,0 +1,99 @@ +# Datenmodell + +Definiert in `features/dashboard/data/types.ts`, erzeugt in `data/seed.ts`. +Die Struktur ist bewusst nah an den HiveGrid-Entities gehalten, damit beim +Einbau nur die Datenquelle getauscht werden muss. + +--- + +## Zuordnung zu HiveGrid + +| Prototyp | HiveGrid-Entity | Verwendete Felder | +|---|---|---| +| `Rechnung` | `invoice` | `amount`, `status`, `issuedAt`, `dueDate`, bezahlt-am | +| `Auftrag` | `order` | `totalValue`, `status`, `startDate`, `endDate`, Gewerk, Marge, Förderung | +| `Kunde` | `company` | `name`, `source`, `createdAt`, Typ, Region | +| `Mitarbeiter` | `TenantUser` + `User` | Name, Rolle, Gewerk, Status, Wochenstunden | +| `Zeiteintrag` | `timeEntry` | `hours`, `date`, Auftragsbezug, Abwesenheitsgrund | + +**Felder, die es so noch nicht gibt** und beim Einbau ergänzt werden müssen: + +| Feld | Wo | Wofür | +|---|---|---| +| `gewerk` | Auftrag, Mitarbeiter | Alle Gewerke-Auswertungen und der Gewerk-Filter | +| `margeProzent` | Auftrag | Deckungsbeitrag | +| `foerderung` / `foerderbetrag` | Auftrag | Förderquote BAFA/KfW | +| `verlorenAb` | Auftrag | Ehrlicher Vertriebstrichter (siehe unten) | +| `ueberAngebot` | Auftrag | Trennt Angebotsgeschäft von Direktaufträgen | +| `wochenstunden` | Mitarbeiter | Auslastungsberechnung | +| `zahlungsmoralTage` | Kunde | Auffällige Zahlungsmoral (auch aus Rechnungen ableitbar) | + +--- + +## Statuswerte + +**Auftrag:** `anfrage` → `aufmass` → `angebot` → `beauftragt` → `in_arbeit` → +`abgeschlossen`, dazu `verloren` als Endzustand. + +`verlorenAb` hält fest, welche Stufe ein verlorener Vorgang zuletzt erreicht +hat. Ohne dieses Feld müsste der Trichter raten und würde die oberen Stufen +systematisch zu klein zeigen. + +`ueberAngebot` trennt das Angebotsgeschäft (ab etwa 6.000 €) von Aufträgen, die +direkt beauftragt werden — Wartung, Notdienst, E-Check. Trichter und +Abschlussquote beziehen sich ausschließlich auf das Angebotsgeschäft. + +**Rechnung:** `entwurf` · `gestellt` · `bezahlt` · `ueberfaellig`. + +--- + +## Gewerke und Leistungsarten + +| Gewerk | Leistungsarten | +|---|---| +| Heizung | Wärmepumpe, Gastherme, Heizkörper, Wartung | +| Sanitär | Bad-Sanierung, Rohrleitungen, Armaturen, Notdienst | +| Klima & Lüftung | Splitgerät, Lüftungsanlage, Kanalreinigung, Wartung | +| Photovoltaik | PV-Anlage, Batteriespeicher, Wallbox, Wartung | +| Elektro | Elektroinstallation, Zählerschrank, Smart Home, E-Check | + +Förderfähig (BAFA/KfW) sind Wärmepumpe, PV-Anlage, Batteriespeicher und +Lüftungsanlage. + +--- + +## Der Demo-Datensatz + +Erzeugt mit festem Seed (`mulberry32`, Seed `20260730`) — die Zahlen bleiben +über Reloads und Screenshots identisch. Bezugsdatum ist der **30.07.2026** +(`HEUTE` in `utils/dateUtils.ts`). + +Fiktiver Betrieb: **Meister Krause GmbH**, Raum Rhein-Main, 24 Beschäftigte +(3 Meister, 14 Monteure, 3 Auszubildende, 4 Büro). + +| Größe | Wert | +|---|---| +| Kunden | 420 | +| Aufträge (24 Monate) | 1.900 | +| Rechnungen | ~1.270 | +| Zeiteinträge (12 Wochen) | ~1.500 | +| Jahresumsatz | ~2,4 Mio. € | +| Offene Forderungen | ~230 T€ | +| Abschlussquote (Angebotsgeschäft) | ~38 % | +| Ø Auslastung | ~73 % | + +**Eingebaute Effekte**, damit die Charts nicht künstlich flach aussehen: + +- **Saisonalität** — Heizung und Lüftung ziehen von September bis Februar an, + der Hochsommer ist schwächer (`SAISON_INDEX`) +- **Wachstum** — rund 12 % gegenüber dem Vorjahr +- **Größenabhängige Abschlussquote** — Wartung und Notdienst werden fast immer + ausgeführt, große Anlagen dagegen verglichen und häufiger verloren +- **Zahlungsverhalten** — die Mehrheit zahlt nahe am Zahlungsziel (21 Tage), + ein kleiner Rest wird zum Mahnfall; Forderungen älter als sieben Monate sind + eingebracht oder ausgebucht und stehen nicht mehr in der offenen Liste +- **Förderanträge** — nur bei Vorgängen der letzten rund 100 Tage offen; ein + Bescheid dauert Wochen, nicht Jahre + +Diese Effekte sind fachlich begründet, nicht kosmetisch: Ein gleichverteilter +Zufallsdatensatz erzeugt Charts, an denen sich keine Gestaltung prüfen lässt. diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md new file mode 100644 index 0000000..204a7a5 --- /dev/null +++ b/docs/DESIGN-SYSTEM.md @@ -0,0 +1,248 @@ +# Design-System: Datenvisualisierung + +Grundlage ist **IBM Carbon Data Visualization**, ergänzt um die Spacing- und +Zahlenregeln aus der eigenen Planung (`UI_UX_GUIDELINES.md`, Januar 2026). + +Die konkreten Farb- und Linienwerte sind aus dem ausgelieferten Stylesheet von +`@carbon/charts` (`dist/styles.css`, Light Theme) übernommen — nicht aus der +Dokumentation abgeschrieben. Alles Folgende ist in +`features/dashboard/utils/chartTheme.ts` als Code hinterlegt. + +--- + +## 1. Farben + +### Kategoriale Palette (14 Farben, Reihenfolge verbindlich) + +``` +1 #6929c4 2 #1192e8 3 #005d5d 4 #9f1853 5 #fa4d56 +6 #520408 7 #198038 8 #002d9c 9 #ee5396 10 #b28600 +11 #009d9a 12 #012749 13 #8a3800 14 #a56eff +``` + +Die Reihenfolge ist auf Unterscheidbarkeit optimiert — auch bei +Farbfehlsichtigkeit und in Graustufen. Farben deshalb **immer von vorne +vergeben**, nie nach Geschmack auswählen und nie umsortieren. + +**Maximal fünf Kategorien je Chart.** Alles darüber fasst +`gruppiereRest()` zu „Sonstige" zusammen. Ein Ring mit zwölf Segmenten ist +keine Information mehr, sondern Dekoration. + +### Statusfarben (nur für Aussagen, nie für Kategorien) + +| Bedeutung | Wert | +|---|---| +| positiv | `#198038` | +| Warnung | `#b28600` | +| negativ | `#da1e28` | +| neutral | `#8d8d8d` | +| Information | `#1192e8` | + +Kategorie- und Statusfarben nie im selben Chart mischen — eine grüne Kategorie +neben einem grünen Positiv-Status ist nicht mehr eindeutig lesbar. + +**Rangfolgen einfarbig.** Top-Kunden oder Aufträge je Mitarbeiter sind keine +Kategorien: Dort trägt die Farbe keine Information, und eine bunte Palette +erzeugt nur Unruhe. Palette nur, wenn die Farbe etwas bedeutet. + +### Feste Farbzuordnung + +Wiederkehrende Kategorien behalten ihre Farbe über alle Charts und Seiten +hinweg (`GEWERK_FARBEN`, `STATUS_KATEGORIE_FARBEN` in `chartTheme.ts`). Sonst +ist Heizung im Ring lila und im Balkendiagramm blau, und jede Legende muss neu +gelesen werden. + +| Gewerk | Farbe | | Status | Farbe | +|---|---|---|---|---| +| Heizung | `#6929c4` | | Verloren / Überfällig | `#fa4d56` / `#da1e28` | +| Sanitär | `#1192e8` | | In Arbeit / Bezahlt | `#198038` | +| Klima & Lüftung | `#005d5d` | | Beauftragt | `#009d9a` | +| Photovoltaik | `#9f1853` | | Angebot | `#6929c4` | +| Elektro | `#198038` | | Entwurf / Sonstige | `#8d8d8d` | + +Bei Statuswerten trägt die Farbe Bedeutung: „Verloren" und „Überfällig" sind +rot, „Bezahlt" grün. Eine zufällige Palettenfarbe für „Verloren" würde die +Aussage der Grafik verwässern. + +### Geordnete Dimensionen + +Altersklassen, Risikostufen und Ähnliches sind **keine Kategorien**, sondern eine +Reihenfolge mit wachsendem Risiko. Dafür gilt die vierstufige Risikorampe +(`RISIKO_RAMPE` in `chartTheme.ts`) — durchgehend Statusfarben, keine +Palettenfarbe dazwischen: + +| Stufe | Wert | Bedeutung | +|---|---|---| +| 1 | `#198038` | im Zahlungsziel | +| 2 | `#b28600` | beobachten | +| 3 | `#ba4e00` | mahnen | +| 4 | `#da1e28` | Abschreibungsrisiko | + +### Einzelserien + +| Rolle | Wert | +|---|---| +| Hauptreihe (Umsatzverlauf o. Ä.) | `#1192e8` | +| Vergleichsreihe (Vorjahr) | `#8d8d8d`, gestrichelt | + +Die Vergleichsreihe ist bewusst neutral: Sie soll einordnen, nicht konkurrieren. + +--- + +## 2. Achsen und Gitter + +| Element | Wert | Begründung | +|---|---|---| +| Gitterlinien | `#e0e0e0`, **nur horizontal** | Vertikale Linien zerschneiden den Verlauf | +| Achsenlinie | `#8d8d8d`, 1 px | Nur an der X-Achse; die Y-Achse braucht keine | +| Tick-Marks | keine | Die Beschriftung reicht als Orientierung | +| Beschriftung | 12 px, `#525252` | Carbon-Standard | +| Y-Achsen-Breite | 58 px | Platz für „245 T€" ohne Umbruch | + +Bei mehr als 12 Datenpunkten wird nur jede n-te Beschriftung gezeigt, sonst +überlappen sie (`MAX_ACHSENLABELS` in `chartTheme.ts` — eine Konstante für +alle Diagrammtypen). + +**Achsenwerte sind rund.** Recharts leitet Ticks aus `max/(n-1)` ab und rundet +den Rohschritt auf; bei 219 T€ ergibt das 0/55/110/165/220. Krumme Achsenwerte +kosten den Leser bei jedem Blick eine Rechenoperation. `nettesRaster()` erzwingt +deshalb Schrittweiten aus der 1-2-2,5-5-Reihe. + +**Die Einheit gehört zur Achse, nicht zum Wert.** `achsenFormat(max)` wählt sie +einmal aus dem Achsenmaximum. Sonst steht „0 €" neben „10 T€" und die Skala +wechselt mitten im Lesen die Einheit; die Nulllinie trägt gar keine Einheit. + +--- + +## 3. Tooltip + +Einheitlich für alle Charts (`components/charts/ChartTooltip.tsx`): + +- Fläche `#393939`, Text `#ffffff` +- Label 12 px mit 80 % Deckkraft, Wert 13 px fett in Tabellenziffern +- kein Rahmen, kein Doppelpunkt zwischen Name und Wert +- Farbmarker je Reihe, 10 × 10 px + +Ein gemeinsamer Tooltip verhindert, dass jedes Chart eigene Regeln entwickelt. + +--- + +## 4. Balken, Flächen, Ringe + +| Element | Regel | +|---|---| +| Balkenbreite | maximal 48 px | +| Balkenabstand | mindestens halbe Balkenbreite (`barCategoryGap: 28 %`) | +| Balkenecken | 2 px, nur an der Werteseite | +| Linienstärke Fläche | 2 px, Verlaufsfüllung 24 % → 2 % | +| Interpolation | **linear** — eine Monotone-Kurve erfindet Zwischenwerte zwischen zwei Messpunkten | +| Punkte | nur beim Hovern, Radius 4, weißer Rand | +| Ring innen/außen | 62 % / 88 %, 2° Segmentabstand | +| Ringmitte | Gesamtwert — sonst muss man die Segmente addieren | +| Ringlegende | rechts, einzeilig, mit Wert und Prozentanteil | + +Ringsegmente starten oben (12 Uhr) mit dem größten Wert und laufen im +Uhrzeigersinn absteigend. Auch „Sonstige" wird nach Größe einsortiert, nicht +ans Ende gehängt. + +**Ein Ring braucht ein echtes Ganzes.** Beträgt ein Segment über 80 %, trägt die +Grafik nichts mehr bei — dann ist die Bezugsgröße falsch gewählt. Beispiel: Der +Rechnungsbestand aus zwei Jahren gegen den heutigen Forderungsstand ergibt +92 % „Bezahlt". Richtig ist der offene Bestand allein; dann zeigt der Ring, wie +viel davon bereits überfällig ist. + +### Werte in einem engen Band: Punkt statt Balken + +Auslastung liegt betrieblich zwischen etwa 70 und 100 %. Ein Balken **muss** bei +Null beginnen, sonst verzerrt seine Länge — und dann sind 74 % und 96 % optisch +fast gleich lang. Ein Punkt kodiert über die **Position**, nicht über die Länge, +und darf deshalb einen ausgeschnittenen Bereich zeigen. + +Der Zielkorridor liegt als graues Band hinter den Punkten, die 100-%-Marke als +beschriftete Linie, darunter eine Achse. Damit erklärt sich die Grafik ohne +Ampel-Legende — und tote Legendeneinträge („Luft frei", wenn niemand darunter +liegt) entfallen. + +--- + +## 5. Abstände (8-px-System) + +| Bereich | Wert | +|---|---| +| Seitenrand | 24 px | +| Abstand zwischen Karten | 16 px | +| Innenabstand Karte | 16 px | +| Kopf zu Inhalt in der Karte | 12 px | +| Label zu Wert in der KPI-Karte | 4 px | +| Icon zu Text | 8 px | + +**Grundregel: innen ≤ außen.** Ist der Innenabstand größer als der Abstand +zwischen den Karten, zerfällt das Raster optisch. + +--- + +## 6. Typografie + +| Rolle | Größe | Gewicht | +|---|---|---| +| Seitentitel | 22 px | 600 | +| KPI-Wert | 28 px | 600 | +| Kartentitel | 14 px | 600 | +| Fließtext / Tabelle | 13 px | 400 | +| Label, Achsen, Legende | 12 px | 400–500 | +| Kleinstschrift (Meta) | 11 px | 400 | + +Eine Schriftfamilie (Inter). Zahlen immer in Tabellenziffern +(`font-variant-numeric: tabular-nums`, global gesetzt), damit Spalten +untereinander stehen. + +--- + +## 7. Zahlenformate (deutsch) + +| Fall | Ausgabe | +|---|---| +| Währung, KPI | `238.412 €` | +| Währung, kompakt (Achsen, Balken) | `8,4 T€` · `245 T€` · `1,25 Mio. €` | +| Prozent | `58,2 %` | +| Tage / Stunden | `34,2 Tage` · `7,5 h` | +| Datum | `30.07.2026`, kurz `30.07.`, Monat `Jul 26` | + +Punkt als Tausendertrenner, Komma als Dezimaltrenner. Innerhalb einer Ansicht +immer dasselbe Format — nicht in einer Zeile `245.000 €` und in der nächsten +`245 T€`. + +--- + +## 8. Zustände + +Jedes Chart braucht einen **Leerzustand** — Filter können jede Ansicht leer +laufen lassen. `ChartLeer` zeigt dann eine gestrichelte Fläche mit Hinweistext +statt eines leeren Rechtecks. + +Hover-Zustände: Karten erhalten einen stärkeren Schatten, Balken und +Ringsegmente werden hervorgehoben (die übrigen auf 35–45 % Deckkraft). +Tabellenzeilen bekommen **keinen** Hover, solange sie nicht anklickbar sind — +ein Hover ohne Ziel verspricht eine Interaktion, die es nicht gibt. + +## 9. Fokus und Kontrast + +**Fokus** ist eine einzige Regel in `app/globals.css` statt `focus:`-Klassen an +jedem Element: + +```css +:where(a, button, [role="button"], input, select, textarea, [tabindex]):focus-visible { + outline: none; + box-shadow: var(--shadow-focus); + border-radius: var(--radius-sm); +} +``` + +`:focus-visible` zeigt den Ring nur bei Tastaturbedienung. Overlays (Filter- +Dropdown, Karten-Menü) schließen mit **Escape** und geben den Fokus an den +auslösenden Knopf zurück. + +**Kontrast:** `--color-text-neutral-tertiary` liegt auf `neutral-700` +(5,15:1 auf Weiß), nicht auf `neutral-600` (3,68:1 — unter WCAG AA). Das +entspricht dem a11y-Override im Zielsystem. `text-neutral-subtle` ist ein +Placeholder-Token und für lesbaren Text nicht geeignet. diff --git a/docs/HERKUNFT.md b/docs/HERKUNFT.md new file mode 100644 index 0000000..e3355cf --- /dev/null +++ b/docs/HERKUNFT.md @@ -0,0 +1,173 @@ +# Herkunft + +Woher die Gestaltung dieses Prototyps stammt, was aus welcher Quelle übernommen +wurde und wo bewusst abgewichen wurde. + +Der Zweck: Wer beim Einbau eine Entscheidung anzweifelt, soll nachlesen können, +ob sie begründet ist oder nur mitgeschleppt wurde. Vieles hier sieht auf den +ersten Blick wie eine Abweichung von der Januar-Planung aus und ist eine +absichtliche Korrektur. + +--- + +## Die drei Quellen + +| Quelle | Was daraus stammt | Stand | +|---|---|---| +| **Fachliche Planung** mit dem Branchenpartner | Seitenschnitt, KPI-Auswahl, Filtersystem, Spacing- und Zahlenregeln | Januar 2026 | +| **HiveGrid-Pitch-Prototyp** | Komponentenbibliothek `ui/`, Design-Tokens, Icon-Set, Gestaltungssprache der Oberfläche | Juli/August 2026 | +| **IBM Carbon Data Visualization** | Chart-Regeln, Farbpalette, Achsen, Tooltips, Kategorien-Obergrenze | laufend | + +Die ersten beiden liegen als lokale Ordner außerhalb dieses Repos und sind +**nicht Teil der Übergabe** — alles fachlich Relevante daraus ist in `docs/` +überführt. Die Zuordnung unten ist die Brücke zurück, falls jemand die +Originale doch braucht. + +--- + +## 1. Fachliche Planung (Januar 2026) + +Ordner `dashboard-planung/`, entstanden mit dem Branchenpartner. Drei Teile +sind hier eingeflossen: + +| Teil der Planung | Wirkung in diesem Repo | +|---|---| +| `01_CRM-Analytics-Clickdummy/` — React/Vite-Click-Dummy | Seitenschnitt, Komponentenzuschnitt, Filterarchitektur | +| `UI_UX_GUIDELINES.md` | 8-px-Raster, Regel „innen ≤ außen", Zahlenformate, KPI-Karten-Aufbau | +| `03_Planung-und-Konzept/Analytics/REQUIREMENTS-analytics.md` | Anforderungs-IDs (INFRA, DASH, FIN, ORD, CUST, COMP, FILT) | + +Die Anforderungs-IDs stehen an mehreren Stellen als Kommentar im Code +(`FilterContext.tsx` → INFRA-01/02, `filtering.ts` → FILT-05, +`AnalyticsToolbar.tsx` → FILT-06). Sie sind der Faden zurück in die Planung. + +### Übernommen + +- **Fünf Analyse-Seiten** in derselben Reihenfolge: Übersicht, Finanzen, + Aufträge, Kunden, Team +- **Komponentenzuschnitt**: KPICard, ChartCard, AlertList, DataTable, + Filterleiste, Mehrfachauswahl mit Suche — dieselbe Aufteilung, neu geschrieben +- **Filterarchitektur**: globaler Filter-State über alle Seiten, in + `localStorage` persistiert, aktive Einschränkungen als entfernbare Chips +- **Seitenabhängige Filterrelevanz** (FILT-05) als Konzept +- **Recharts** als Chart-Bibliothek — dieselbe Wahl wie im Click-Dummy +- **8-px-Raster und deutsche Zahlenformate** aus `UI_UX_GUIDELINES.md`, + siehe [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 5 und 7 + +### Bewusst anders + +| Thema | Click-Dummy (Januar) | Hier | Grund | +|---|---|---|---| +| **Chart-Farben** | 12 kräftige Farben, Regel „keine Grautöne, wirken traurig" | Carbon-Palette, Grau ausdrücklich für Vergleichsreihen und Neutrales | Die Januar-Palette ist auf Freundlichkeit optimiert, nicht auf Unterscheidbarkeit. Carbon ist auf Farbfehlsichtigkeit und Graustufen geprüft. Grau ist kein Makel, sondern das Mittel, mit dem eine Vergleichsreihe zurücktritt | +| **Irrelevante Filter** | rechts ausgegraut angezeigt | werden gar nicht erst angeboten | Ein sichtbarer Filter, der nichts bewirkt, kostet Vertrauen in die Zahlen | +| **Schriftgrößen** | zur Laufzeit verstellbar (`TypographyContext` + Dev-Panel), KPI-Wert 37 px | feste Werte, KPI-Wert 28 px | Das Panel war ein Abstimmungswerkzeug. Die Abstimmung ist erfolgt; die Werte gehören jetzt fest ins Design-System | +| **Chart-Elemente** | Drop-Shadow statt Rahmen | flach, Trennung über Weißraum und Segmentabstand | Schatten auf Datenflächen erzeugen eine Tiefe, die keine Bedeutung trägt | +| **Fachlichkeit** | generisches CRM: Abteilungen, Bewertungen, Kundenkontakte | SHK-Handwerk: Gewerke, Förderung, Auslastung, Angebotsgeschäft | Auftrag dieses Prototyps | +| **Bezeichner** | englisch (`revenue`, `orders`) | deutsch (`umsatz`, `auftraege`) | Die Oberfläche ist deutsch. Ein Bruch in der Mitte erzwingt beim Einbau ständiges Übersetzen | +| **Datensatz** | 25 Mitarbeiter, 200 Kunden, 800 Aufträge, gleichverteilt | 24, 420, 1.900 — mit Saisonalität, Wachstum, Zahlungsverhalten | An einem gleichverteilten Datensatz lässt sich keine Gestaltung prüfen, siehe [DATENMODELL.md](DATENMODELL.md) | + +### Anforderungen, die hier nicht umgesetzt sind + +Die Planung führt 47 v1-Anforderungen als vollständig. In diesem Prototyp +fehlen vier davon bewusst: + +| ID | Anforderung | Warum nicht | +|---|---|---| +| INFRA-03 | Filter-Presets speichern und laden | Bedienkomfort ohne Gestaltungsfrage. Gehört in die echte Software ans Benutzerprofil, nicht in die Vorlage | +| INFRA-04 (teilweise) | Zeitraum mit benutzerdefiniertem Bereich | Die sechs Presets decken die Abstimmung ab; ein Datumsbereichs-Picker ist eine Umsetzungs-, keine Gestaltungsfrage | +| FILT-02 / FILT-04 | Filter für Rechnungsstatus und Betragsbereich | Rechnungsstatus ist auf der Finanzseite bereits als Chart-Dimension sichtbar; ein Betragsfilter verzerrt Summen-KPIs, ohne dass es an der Karte steht | +| CUST-03 | KPI „Kontakte gesamt" | Der Prototyp führt keine Kontakt-Entität. In HiveGrid existiert sie — die Karte lässt sich beim Einbau ergänzen | + +Umgekehrt ist **Team** enthalten, obwohl die Planung es unter v2 führt +(TEAM-01 bis TEAM-03): Auslastung ist im Handwerk die Kennzahl, nach der +disponiert wird. + +--- + +## 2. HiveGrid-Pitch-Prototyp + +Der Next.js-Prototyp aus dem Kundentermin (Juli 2026). Von dort stammt die +gesamte Oberfläche außerhalb der Diagramme. + +| Übernommen | Wo | +|---|---| +| Komponentenbibliothek — 86 Komponenten, 4 Layouts | `ui/components/`, `ui/layouts/` | +| Design-Tokens (Farben, Radien, Schatten, Typo-Skala) | `ui/theme.css`, eingebunden in `app/globals.css` | +| Icon-Set (Feather über `@subframe/core`) | überall, aufgelöst in `components/layout/FeatherIcon.tsx` | +| Stack | Next.js 16 · React 19 · Tailwind v4 — gleiche Versionen | + +`ui/` ist **byteidentisch** mit dem Stand im Pitch-Repo und wurde nicht +angefasst. Die Bibliothek wird dort über die Subframe-CLI aus dem Projekt +*Basedesign* synchronisiert: + +```bash +npx @subframe/cli@latest sync --all +``` + +Wer die Bibliothek aktualisiert, sollte das im Pitch-Repo tun und den Ordner +hierher kopieren — sonst laufen die beiden Stände auseinander. + +### Was aus der Bibliothek nicht verwendet wird + +`ui/components/` enthält eigene Chart-Wrapper (`AreaChart`, `BarChart`, +`LineChart`, `PieChart`). Sie sind hier **nicht** im Einsatz. Sie geben zu +wenig Kontrolle über Achsen, Tooltips und Ringproportionen, um die +Carbon-Regeln umzusetzen. Die Diagramme unter +`features/dashboard/components/charts/` setzen deshalb direkt auf Recharts auf. + +Das ist die einzige Stelle, an der dieser Prototyp von der Pitch-Bibliothek +abweicht — überall sonst gilt sie. + +--- + +## 3. IBM Carbon Data Visualization + +Grundlage aller Diagramm-Entscheidungen. Ausformuliert in +[DESIGN-SYSTEM.md](DESIGN-SYSTEM.md), als Code hinterlegt in +`features/dashboard/utils/chartTheme.ts`. + +Die Farb- und Linienwerte sind aus dem ausgelieferten Stylesheet von +`@carbon/charts` (`dist/styles.css`, Light Theme) übernommen — nicht aus der +Dokumentation abgeschrieben. Das Paket selbst ist **keine Abhängigkeit** dieses +Repos; gezogen wurden nur die Werte. + +| Von Carbon | Konkret | +|---|---| +| Kategoriale Palette | 14 Farben in verbindlicher Reihenfolge, immer von vorne vergeben | +| Obergrenze | maximal 5 Kategorien je Chart, Rest über `gruppiereRest()` zu „Sonstige" | +| Achsen | nur horizontale Gitterlinien, keine Tick-Marks, Achsenlinie nur an X | +| Tooltip | dunkle Fläche `#393939`, weißer Text, kein Rahmen | +| Neutrale Werte | Grid `#e0e0e0`, Achse `#8d8d8d`, Text `#161616` / `#525252` | + +**Ergänzt, nicht aus Carbon:** die festen Farbzuordnungen je Gewerk und je +Status (`GEWERK_FARBEN`, `STATUS_KATEGORIE_FARBEN`). Carbon regelt die Palette, +nicht welche Kategorie welche Farbe bekommt. Eine Kategorie behält ihre Farbe +über alle Seiten hinweg, damit man Legenden nicht ständig neu liest. + +--- + +## 4. Was aus keiner der drei Quellen stammt + +Die SHK-Fachlogik ist für diesen Prototyp entstanden und beim Einbau der Teil, +der mit dem Betrieb abzustimmen ist: + +| Thema | Wo | Offene Fachfrage | +|---|---|---| +| Gewerke und Leistungsarten | `data/types.ts` | Schnitt deckungsgleich mit dem Betrieb? | +| Förderquote BAFA/KfW | `calculations.ts` → `foerderQuote()` | Braucht ein Feld am Auftrag | +| Vertriebstrichter nur über Angebotsgeschäft | `vertriebsTrichter()`, Feld `ueberAngebot` | Ab welchem Wert wird schriftlich angeboten? | +| Absprungstufe verlorener Vorgänge | Feld `verlorenAb` | Wird das heute erfasst? | +| Auslastung und ihre Schwellen | `AuslastungsBalken.tsx` | Zählen Rüst- und Fahrzeiten als produktiv? | + +Diese Punkte stehen mit Begründung in [HANDOVER.md](../HANDOVER.md) Abschnitt 5 +und in [KPI-KATALOG.md](KPI-KATALOG.md). + +--- + +## Zeitleiste + +| Wann | Was | +|---|---| +| 18.–22.01.2026 | CRM-Analytics-Click-Dummy mit dem Branchenpartner, UI/UX-Regeln, Anforderungskatalog | +| 22.01.2026 | Dashboard- und Analytics-Bausteine ins HiveGrid-Projekt übernommen | +| Juli 2026 | Pitch-Prototyp mit der Subframe-Bibliothek *Basedesign* | +| 27.08.2026 | Dieses Repo: Planung, Pitch-Bibliothek und Carbon zusammengeführt, fachlich auf SHK zugeschnitten | diff --git a/docs/KPI-KATALOG.md b/docs/KPI-KATALOG.md new file mode 100644 index 0000000..043c459 --- /dev/null +++ b/docs/KPI-KATALOG.md @@ -0,0 +1,147 @@ +# KPI-Katalog + +Jede Kennzahl mit Definition, Berechnung und Fundstelle im Code. +Alle Funktionen liegen in `features/dashboard/data/calculations.ts`. + +**Zwei Zeitbezüge sind zu unterscheiden:** + +- **zeitraumbezogen** — reagiert auf die Zeitraum-Umschaltung (Umsatz, neue Kunden) +- **stichtagsbezogen** — gilt zum heutigen Tag, unabhängig vom Zeitraum + (offene Forderungen, aktive Aufträge, Auslastung des Bestands) + +Das ist Absicht: „Offene Forderungen der letzten 7 Tage" wäre keine sinnvolle +Zahl. + +In der Oberfläche steht der Zeitbezug in der **Fußzeile jeder KPI-Karte** +(„im gewählten Zeitraum" / „zum Stichtag" / „über alle Zeiträume"). Karten mit +Vorperiodenvergleich zeigen dort stattdessen den Trend — die Anatomie bleibt +gleich, damit die Reihe eine gemeinsame Grundlinie hat. Diagrammkarten, die +den Zeitraumfilter ignorieren, sagen es im Untertitel. + +--- + +## Übersicht + +| KPI | Definition | Bezug | Funktion | +|---|---|---|---| +| Umsatz (Zahlungseingang) | Summe bezahlter Ausgangsrechnungen, gezählt am Zahlungsdatum | Zeitraum | `umsatz()` | +| Trend Umsatz | Vergleich mit dem gleich langen Zeitraum davor | Zeitraum | `umsatzVorperiode()` + `berechneTrend()` | +| Offene Forderungen | Summe aller nicht bezahlten Ausgangsrechnungen | Stichtag | `offenePosten().gesamt` | +| Aktive Aufträge | Vorgänge im Status „beauftragt" oder „in Arbeit" | Stichtag | `aktiveAuftraege()` | +| Ø Auslastung Monteure | Mittel der produktiven Stunden je verfügbarer Stunde | Zeitraum | `teamKennzahlen()` | +| Neue Kunden | Kundendatensätze mit Anlagedatum im Zeitraum | Zeitraum | `neueKunden()` | +| Abschlussquote | Gewonnene Aufträge im Angebotsgeschäft, bezogen auf alle Angebote | Stichtag | `abschlussquote()` | +| Deckungsbeitrag | Nach Auftragswert gewichtete Marge abgeschlossener Aufträge | Zeitraum | `marge()` | + +**Umsatzentwicklung** (`umsatzVerlauf()`) bildet zusätzlich den +Vorjahreszeitraum ab: Jede Zahlung wird um zwölf Monate verschoben in denselben +Zeitraster einsortiert. Die Auflösung wechselt automatisch — bis 30 Tage +tageweise, darüber monatsweise. + +--- + +## Finanzen + +| KPI | Definition | Bezug | Funktion | +|---|---|---|---| +| Umsatz (bezahlt) | wie oben | Zeitraum | `umsatz()` | +| Offene Forderungen | Status „gestellt" oder „überfällig" | Stichtag | `offenePosten()` | +| Überfällig | Nur Status „überfällig" (Zahlungsziel deutlich überschritten) | Stichtag | `offenePosten().ueberfaellig*` | +| Ø Zahlungsdauer | Mittel aus Rechnungsdatum bis Zahlungseingang | Zeitraum | `zahlungsdauer()` | +| Deckungsbeitrag | siehe oben | Zeitraum | `marge()` | +| Forderungen nach Alter | Buckets ab Fälligkeit: 0–30 / 31–60 / 61–90 / > 90 Tage | Stichtag | `offenePosten().nachAlter` | +| Rechnungsvolumen nach Status | Betragssumme je Rechnungsstatus | Stichtag | `rechnungenNachStatus()` | +| Umsatz nach Gewerk | Bezahlte Rechnungen, zugeordnet über den Auftrag | Zeitraum | `umsatzNachGewerk()` | +| Top-Kunden | Kunden nach bezahltem Gesamtumsatz | gesamt | `topKunden()` | + +Die Altersstruktur ist die wichtigste Karte der Seite: Ab etwa 60 Tagen sinkt +die Einbringungsquote erfahrungsgemäß deutlich. Die Balken sind deshalb von +grün nach rot eingefärbt. + +--- + +## Aufträge + +| KPI | Definition | Bezug | Funktion | +|---|---|---|---| +| Neue Aufträge | Vorgänge mit Anlagedatum im Zeitraum | Zeitraum | `auftraegeImZeitraum()` | +| Aktive Aufträge | „beauftragt" oder „in Arbeit" | Stichtag | `aktiveAuftraege()` | +| Auftragsbestand | Summe der Werte aktiver Aufträge | Stichtag | abgeleitet | +| Über Termin | Beauftragt oder in Arbeit und Fälligkeit überschritten | Stichtag | `ueberfaelligeAuftraege()` | +| Abschlussquote | siehe Übersicht | Stichtag | `abschlussquote()` | +| Ø Durchlaufzeit | Anlagedatum bis Abschluss, nur abgeschlossene Vorgänge | Stichtag | `durchlaufzeit()` | +| Vertriebstrichter | Vorgänge je erreichter Stufe | Stichtag | `vertriebsTrichter()` | +| Förderquote BAFA/KfW | Anteil **förderfähiger** Aufträge mit beantragter oder bewilligter Förderung | Stichtag | `foerderQuote()` | + +**Zum Trichter:** Er zeigt ausschließlich das **Angebotsgeschäft** — Vorhaben +ab etwa 6.000 €, für die schriftlich angeboten wird (Feld `ueberAngebot`). +Wartung, Notdienst und E-Check gehen ohne Angebot direkt in die Ausführung; +zählte man sie mit, würde der Trichter unten breiter als oben. + +Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind +(Feld `verlorenAb`). Ohne diese Unterscheidung wären die oberen Stufen +systematisch zu klein und die Übergangsquoten wertlos. + +**Zur Abschlussquote:** gleiche Bezugsgröße wie der Trichter. Sie entspricht +deshalb exakt der Übergangsquote von „Angebot" auf „Beauftragt" — im +Demo-Datensatz rund 38 %. + +**Zur Förderquote:** Bezugsgröße sind nur förderfähige Leistungen (Wärmepumpe, +PV-Anlage, Batteriespeicher, Lüftungsanlage). Über alle Aufträge gerechnet läge +sie im einstelligen Bereich und wäre wertlos — eine Badsanierung war nie +förderfähig. + +--- + +## Kunden + +| KPI | Definition | Bezug | Funktion | +|---|---|---|---| +| Kunden gesamt | Alle Kundendatensätze | Stichtag | — | +| Neue Kunden | Anlagedatum im Zeitraum | Zeitraum | `neueKunden()` | +| Wiederkehrende Kunden | Anteil mit mehr als einem Auftrag | Stichtag | `wiederkehrquote()` | +| Ø Kundenwert | Bezahlter Umsatz je Kunde mit mindestens einem Auftrag | gesamt | abgeleitet | +| Reaktivierbar | Kunden mit Historie, über 12 Monate ohne Auftrag | Stichtag | `inaktiveKunden()` | +| Kunden nach Quelle / Typ / Region | Verteilungen | Stichtag | `kundenNach()` | +| Auffällige Zahlungsmoral | Ø Zahlungsdauer über 30 Tage | Stichtag | `zahlungsauffaellig()` | + +Empfehlung und Bestandskunden zusammen zeigen, wie stark der Betrieb ohne +Werbebudget wächst — im Handwerk die aussagekräftigste Quellenkennzahl. + +--- + +## Team + +| KPI | Definition | Bezug | Funktion | +|---|---|---|---| +| Im Einsatz | Gewerblich Beschäftigte mit Status „aktiv" | Stichtag | — | +| Ø Auslastung | Produktive Stunden / verfügbare Stunden (aus Wochenstunden hochgerechnet) | Zeitraum | `teamKennzahlen()` | +| Produktivquote | Auftragsbezogene Stunden / erfasste Stunden | Zeitraum | abgeleitet | +| Erfasste Stunden | Summe aller Zeiteinträge | Zeitraum | abgeleitet | +| Abwesenheit | Stunden für Urlaub, Krankheit, Fortbildung | Zeitraum | abgeleitet | +| Abgeschlossene Aufträge je Mitarbeiter | Anzahl im Zeitraum abgeschlossener Aufträge | Zeitraum | `teamKennzahlen()` | +| Auftragswert je Mitarbeiter | Summe der Werte dieser Aufträge | Zeitraum | `teamKennzahlen()` | + +**Auslastungsschwellen** (in `AuslastungsBalken.tsx`, betrieblich festzulegen): + +| Bereich | Bedeutung | +|---|---| +| < 70 % | Luft frei — Kapazität ungenutzt | +| 70–95 % | Zielkorridor | +| > 95 % | Ausgebucht — Terminrisiko | + +Der Auftragswert je Mitarbeiter ist ausdrücklich **kein Leistungsvergleich**: +Ein Wärmepumpen-Auftrag ist um ein Vielfaches größer als eine Wartung. Der +Hinweis steht als Tooltip an der Karte. + +--- + +## Bewusst nicht enthalten + +| Kennzahl | Grund | +|---|---| +| Live-Aktualisierung | Für die Gestaltungsabstimmung ohne Nutzen | +| CSV-/Excel-Export | Reine Umsetzungsfrage, kein Gestaltungsthema | +| Drilldown auf Einzeldatensätze | Der Absprung in die bestehenden Listen genügt | +| E-Mail-Marketing-Kennzahlen | Eigenes Modul (siehe Planungsordner 04) | +| Workflow-Kennzahlen | Eigenes Modul | diff --git a/docs/LEITPLANKEN.md b/docs/LEITPLANKEN.md new file mode 100644 index 0000000..f7735b4 --- /dev/null +++ b/docs/LEITPLANKEN.md @@ -0,0 +1,211 @@ +# Leitplanken aus HiveGrid Cloud + +Welche Bereiche der Zielanwendung bereits Davids finale Subframe-Screens tragen +und welche noch auf Interim-UI laufen — und was daraus für dieses Dashboard +verbindlich ist. + +Warum die Unterscheidung zählt: Ein nachgezogener Screen ist eine +**Design-Entscheidung**, an der man sich ausrichten kann. Eine Interim-UI ist +eine **Notlösung des Devs** — sie kopieren hieße, einen Platzhalter zum Standard +zu erklären. + +Stand der Auswertung: 2026-08-29 gegen den lokalen Repo-Stand. + +--- + +## 1. Woran man es im Code erkennt + +Der Repo pflegt dafür bewusste Marker. In absteigender Verlässlichkeit: + +| Muster | Bedeutung | +|---|---| +| `Interim-UI (AD-28)` im Kopfkommentar | Optik ist ausdrücklich vorläufig | +| `design-nachzug` | offener Punkt — steht auch in sonst fertigen Screens für einzelne Reste | +| Kopfkommentar mit Subframe-Screen-ID (`WF-P8 S03`, `S02/S02L/S03/S05`), Page-UUID oder „gegen Davids Export vom …" | nachgezogen, Optik ist abgestimmt | +| `@subframe/sync-disable` in `apps/web/app/ui/**` | lokal geänderte Komponente, die der Sync nicht überschreiben darf | + +Ein typischer Interim-Kopf (`projects/PipelineKanbanPage.tsx`): + +> `// R-P8b (P8 configurable Kanban) Interim-UI (AD-28) — pipeline board screen` +> `// (D-KANBAN), composed from ~/ui …` + +Ein typischer nachgezogener Kopf (`projects/ProjektListPage.tsx`): + +> `// WF-P8 S03 "Projekt-Liste" — rebuilt against Davids Subframe export of` +> `// 2026-07-28 (page 5c5bdb0a-995e-4c22-aa8b-e48333c60297) …` + +**Autoritativ ist aber die Liste, nicht der Kommentar:** +`.planning/SUBFRAME-FLOW-INVENTAR.md` trackt jeden Subframe-Flow mit Seitenzahl, +Owner und Stand. Der dortige Hinweis ist wichtig: Der Subframe-Sync pflegt nur +Components, Layouts und Theme — **Pages werden nie automatisch integriert**, +jeder Screen wird einzeln hinter der Loader/Action-Grenze verdrahtet. + +**Grenze der Methode:** Ein fehlender Marker sagt nichts. Von 214 +`*Page.tsx`-Dateien tragen 92 einen Interim-Marker, 36 einen Subframe-Bezug und +**86 gar keinen** — überwiegend Plattform-Admin (31) und Integrationen (13), +also interne Flächen mit niedrigem Politur-Druck. + +--- + +## 2. Die Abgrenzung + +### Nachgezogen — als Leitplanke brauchbar + +| Bereich | Stand laut Inventar | Im Code | +|---|---|---| +| **Team-Chat** | 🟢 24 von 24 Screens integriert (Handoff 2026-08-08) | `screens/chat/` — kein einziger Interim-Marker | +| **Form-Builder** | ✅ integriert (PR #71) | `screens/forms/` — Kopf nennt „WF-P16, screens S02/S02L/S03/S05" | +| **Automation / Workflow-Editor** | 🟡 voller Delta-Nachzug (Davids „V1 Vollbild minimal") | `screens/automation/` — 6 Screens mit Subframe-Bezug, keiner interim | +| **Projekt-Pipelines** | 🟡 Kernstrecke S01–S07 integriert | `screens/projects/` — 7 nachgezogen, 4 noch interim (darunter das Kanban selbst) | +| **Aufgaben** | 🟡 Stufen 0–6 fertig | `screens/tasks/` — 2 nachgezogen, 1 interim | +| **Lead-Liste** | 🟡 Kernstrecke (`/leads` = S01, `/leads/inbox` = S02) | `screens/leads/` — 2 nachgezogen, 6 noch interim | + +### Vollständig Interim — nicht als Vorbild nehmen + +Kein einziger nachgezogener Screen: **HR** (21 Seiten), **Mobile-PWA** (19), +**CRM** (6), **Booking** (5), **DSGVO/Privacy** (5), **E-Mail-Postfach** (4), +**Files** (3), **Kalender** (2), **Audit** (2), Signatur, Plugins, Auth — +**und das Dashboard** (1). + +### Noch gar nicht entworfen + +Die zehn Auth-Flows (Login/2FA, Passwort-Reset, Set-Password, Admin-RBAC, +Signup, Konto löschen, 2FA verwalten, Passwort ändern, Workspace-Auswahl, +Profil & Sicherheit) stehen im Inventar auf ❌ offen — zusammen 44 Seiten. + +Ergänzend: Die 30 Designer-Briefs unter `.planning/designer-briefs/` haben ihren +Block „Davids Delta" durchgehend **leer**. Die tatsächlichen Handoffs werden +über das Flow-Inventar und die Ticket-Docs geführt, nicht über die Briefs — +102 Ticket-Docs führen einen offenen `design-nachzug`. + +--- + +## 3. Was daraus für dieses Dashboard folgt + +### 3.1 KPI-Kachel: `StatsCard` ist gesetzt + +Die nachgezogenen Screens benutzen durchgehend `~/ui/components/StatsCard` +(Billing-Übersicht, Zahlungsseite, Admin-Health, Admin-Jobs, Admin-Billing). +Die API ist knapp: + +```tsx +} + label={t("…")} + value={formatters.number(totalSeats)} + percentage={t("…")} // "" um den Slot abzuschalten +/> +``` + +Unsere `KPICard` kann mehr: Sparkline, Akzentfarbe, `richtungGut` (steigender +Wert ist je nach Kennzahl gut oder schlecht), Trendlabel, ⋯-Menü zum Ausblenden. +Das ist fachlich begründet — **aber es ist eine Abweichung vom gesetzten +Baustein.** Beim Einbau gibt es genau zwei saubere Wege: + +1. `StatsCard` erweitern und die Datei mit `@subframe/sync-disable` schützen + (so wurde es dort schon einmal gemacht, siehe Kopf der Komponente), oder +2. die eigene Karte bewusst als lokale Komposition begründen. + +Was nicht geht: eine zweite KPI-Kachel kommentarlos danebenstellen. + +### 3.2 Raster: 16 px, mobil einspaltig + +Das durchgängige Muster der nachgezogenen Screens: + +``` +mobile:grid-cols-1 grid w-full grid-cols-4 gap-4 +``` + +`gap-4` = 16 px = unser Kartenabstand ✓. Das `mobile:`-Präfix ist der +hauseigene Breakpoint und in 44 Stellen belegt — unser Prototyp hat kein +Mobil-Verhalten und muss es beim Einbau ergänzen. + +### 3.3 Filterleiste: `FilterBar` statt Eigenbau + +`ProjektListPage`, `AutomationListPage`, `AutomationExecutionsPage`, +`FormsAdminPage` und die Betriebsmittel-Übersicht nutzen alle +`~/ui/components/FilterBar`. Unsere `AnalyticsToolbar` ist handgebautes +Tailwind — funktional gleichwertig, aber am gesetzten Baustein vorbei. Das ist +die auffälligste Stelle, an der der Prototyp beim Einbau umgestellt gehört. + +Bemerkenswert: Die Betriebsmittel-Übersicht wurde 2026-08-19 genau in diese +Richtung überarbeitet — „auf kompakte Kennzahlen plus echte `FilterBar` +umgestellt". Kennzahlenreihe über Filterleiste über Liste ist dort also bereits +das etablierte Muster. Unser Seitenaufbau folgt derselben Logik. + +### 3.4 Weitere Konventionen der fertigen Screens + +- Seitenkopf über `PageShell` / `PageHeader` / `PageBody`, nie eigenes Chrome +- i18n mit Namespace: `t("billing:subscriptionOverview.summary.plan")` +- Screen-Komponente ist rein: `({ vm, actions })`, kein `useLoaderData`, + kein `useFetcher` — Navigation und Schreibpfade sind Callbacks +- Tabellen über `~/ui/components/Table`, Dichte über die Komponente wählen + +--- + +## 4. Wo es keine Leitplanke gibt: die Diagramme + +Recharts kommt im gesamten Repo an **drei** Stellen vor: die beiden +Dashboard-Diagramme und `leads/LeadAnalyticsPage.tsx`. Es gibt keine geteilte +Palette, kein Chart-Theme, keine Kategorienregel — die vorhandenen Diagramme +setzen ihre Farben direkt im JSX. + +Auch Davids Standards decken das nicht ab: +`.planning/INTERIM-UI-DESIGN-STANDARDS.md` regelt Spacing, Control-Höhen, +Dialoggrößen und Zustände sehr genau, **Datenvisualisierung gar nicht**. + +Für dieses Dashboard heißt das beides: + +- Für Kacheln, Filter, Tabellen und Seitenaufbau **gibt es** einen Standard — + daran halten wir uns, statt ihn neu zu erfinden. +- Für Diagramme gibt es keinen. Was wir in + [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) und `chartTheme.ts` festlegen, füllt eine + echte Lücke und wird im Zweifel zur Referenz für alle späteren Charts. Das ist + der wertvollste, aber auch der begründungspflichtigste Teil der Übergabe — + deshalb steht zu jeder Regel dort, warum sie so ist. + +--- + +## 5. Selbst nachprüfen + +```sh +cd + +# Interim-Screens +grep -rl "Interim-UI\|AD-28" apps/web/app/components/screens --include='*Page.tsx' + +# nachgezogene Screens +grep -rl "Davids\|WF-P[0-9]" apps/web/app/components/screens --include='*Page.tsx' + +# autoritative Flow-Liste +cat .planning/SUBFRAME-FLOW-INVENTAR.md +``` + +Die Zahlen in diesem Dokument sind eine Momentaufnahme. Das Flow-Inventar wird +fortgeschrieben (Stand dort: 2026-08-25, 12. Update) — vor einer Entscheidung +lieber neu zählen. + +--- + +## 6. Wo dieser Prototyp bewusst abweicht + +Ein Abgleich mit den nachgezogenen Cloud-Screens fördert drei Stellen zutage, an +denen dieser Prototyp anders arbeitet. Sie stehen hier, damit sie beim Einbau +als **Entscheidung** erkennbar sind und nicht als Versehen wegkorrigiert werden. + +| Thema | Cloud | Hier | Begründung | +|---|---|---|---| +| **Zahlen in Tabellen** | linksbündig; `align-right` nur für die Aktionsspalte | rechtsbündig mit `tabular-nums` | IBM Carbon und die eigene Planung (`UI_UX_GUIDELINES.md` §2) verlangen es: Nur rechtsbündig stehen Tausenderstellen untereinander, und genau darum geht es in einem Analytics-Dashboard. Die Cloud-Tabellen zeigen überwiegend Text, nicht Beträge | +| **Filterleiste** | `~/ui` `FilterBar` mit Facetten-Slots | eigene `AnalyticsToolbar` | Der Prototyp braucht ein Zeitraum-Segment-Control, das die FilterBar nicht kennt. Beim Einbau ist das der erste Umbau — die Logik passt fast 1:1 auf `FacetFilterBar` | +| **Formatierung** | `locale/format.ts`, Prozent erwartet 0–1 | eigene `formatters.ts`, Prozent erwartet 0–100 | Der Prototyp hat keine i18n-Schicht. Beim Einbau ersetzen, aber die **Achsenformatierung** (`achsenFormat`, `nettesRaster`) hat dort kein Gegenstück und sollte mitkommen | + +Umgekehrt übernommen wurden: keine Versalien in Spaltenköpfen, Raster mit 16 px +Abstand, Kennzahlenreihe über Filterleiste über Liste, Leerzustände an jeder +Visualisierung. + +**Offen für den Einbau** (aus dem Cloud-Abgleich, hier bewusst nicht gebaut): +Screens als reine `({ vm, actions })`-Komponenten, Texte über i18n-Keys, +`mobile:`-Verhalten, `data-ui-id` auf allen Flächen, Textalternative je Diagramm. +Das ist Portierungsarbeit, keine Gestaltung — Details in +[ZIELSYSTEM.md](ZIELSYSTEM.md) Abschnitt 3. diff --git a/docs/NAVIGATION.md b/docs/NAVIGATION.md new file mode 100644 index 0000000..2b37bfc --- /dev/null +++ b/docs/NAVIGATION.md @@ -0,0 +1,96 @@ +# Navigation und Seitenaufbau + +## Struktur + +``` +Seitenleiste (fest, einklappbar auf Icon-Breite) +├── Analyse +│ ├── Übersicht / Kernkennzahlen, Handlungsbedarf +│ ├── Finanzen /finanzen Umsatz, Forderungen, Zahlungsverhalten +│ ├── Aufträge /auftraege Pipeline, Termine, Förderung +│ ├── Kunden /kunden Akquise, Quellen, Reaktivierung +│ └── Team /team Auslastung, Stunden, Abschlüsse +└── Entwicklung + └── Komponenten /komponenten Bausteine und Regeln (nur für die Umsetzung) +``` + +**Warum Seitenleiste statt Tab-Leiste:** Die bestehende HiveGrid-Oberfläche +navigiert links. Der Prototyp lässt sich so ohne Umbau der +Informationsarchitektur einsetzen. Einklappen schafft Platz für breite Tabellen +und das fünfspaltige KPI-Raster. + +--- + +## Aufbau jeder Analyse-Seite + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Kopfzeile: Breadcrumb · Suche · Benachrichtigungen · Profil │ fest +├──────────────────────────────────────────────────────────────┤ +│ Seitenkopf: Titel · Einordnung Aktualisieren Export ⇗ │ +├──────────────────────────────────────────────────────────────┤ +│ Filterleiste: Zeitraum │ Dimensionen │ zurücksetzen │ klebt oben +│ Aktive Filter als entfernbare Chips │ +│ Ausgeblendete Widgets (nur wenn welche versteckt sind) │ +├──────────────────────────────────────────────────────────────┤ +│ KPI-Reihe (5 Karten, gleiche Höhe) │ +│ [nur Übersicht] Handlungsbedarf über volle Breite │ +│ Hauptdiagramm (2/3) │ Nebendiagramm (1/3) │ +│ Detaildiagramme (2–3, oben bündig) │ +│ Arbeitsliste (Tabelle) │ +└──────────────────────────────────────────────────────────────┘ +``` + +Die Reihenfolge folgt der Lesefrage: **Wie steht es? → Was ist zu tun? → Wie hat +es sich entwickelt? → Woran liegt es?** + +Auf der **Übersicht** steht der Handlungsbedarf direkt unter der KPI-Reihe und +über die volle Breite. Sie ist die Seite der Geschäftsführung; dort ist „was ist +zu tun" die zweite Frage, nicht die letzte. Jede Zeile trägt Betrag und Handlung +(„Mahnlauf starten", „Nachfassen") — eine Liste ohne Verb ist ein Bericht. + +Auf den vier Detailseiten bleibt die Arbeitsliste unten: Dort ist sie die +Konsequenz aus dem, was darüber steht. + +**Gleiche Kartenhöhe nur bei gleicher Struktur.** Die KPI-Reihe wird gestreckt, +damit die fünf Karten als ein Objekt lesbar sind. Gemischte Reihen (Diagramm +mit fester Höhe neben Tabelle) stehen auf `items-start` — Strecken erzeugt dort +nur Weißraum. + +--- + +## Interaktionen + +| Element | Verhalten | +|---|---| +| **Zeitraum** | Heute · 7 Tage · 30 Tage · Quartal · Jahr · Gesamt. Rechnet alle zeitraumbezogenen Kennzahlen und Charts neu, inklusive Vorperiodenvergleich | +| **Dimensionsfilter** | Mehrfachauswahl mit Suche; wirkt sofort. Nur Filter, die auf der Seite etwas bewirken, werden angeboten | +| **Filter-Chips** | Jede Einschränkung erscheint als Chip und lässt sich einzeln entfernen | +| **Widget ausblenden** | ⋯-Menü an jeder Karte. Ausgeblendete Karten erscheinen in der Filterleiste und lassen sich einzeln oder gesammelt zurückholen | +| **Hover** | Balken, Ringsegmente und Trichterstufen heben sich hervor, die übrigen treten zurück; Verläufe zeigen einen Tooltip mit allen Reihen des Datenpunkts | +| **Handlungsbedarf** | Jede Zeile ist ein Absprung in die zugehörige Arbeitsliste (im Prototyp Attrappe mit Hinweis) | +| **Tastatur** | Fokus ist auf allen interaktiven Elementen sichtbar; Filter-Dropdown und Karten-Menü schließen mit Escape und geben den Fokus zurück | +| **Ringlegende** | Klick hebt ein Segment dauerhaft hervor, erneuter Klick löst es wieder | +| **Tabellen** | Spaltenkopf sortiert, Suchfeld filtert, Blättern über Pfeile | +| **Seitenleiste** | Einklappbar; die Auswahl bleibt beim Seitenwechsel erhalten | + +Filterauswahl und ausgeblendete Widgets überleben einen Reload +(`localStorage`, Schlüssel `hivegrid.analytics.*`). + +--- + +## Seitenabhängige Filterrelevanz + +Definiert in `features/dashboard/data/filtering.ts`: + +| Seite | Zeitraum | Gewerk | Auftragsstatus | Kundenquelle | Region | Mitarbeiter | +|---|:--:|:--:|:--:|:--:|:--:|:--:| +| Übersicht | ✓ | ✓ | | | ✓ | | +| Finanzen | ✓ | ✓ | | ✓ | ✓ | | +| Aufträge | ✓ | ✓ | ✓ | | ✓ | ✓ | +| Kunden | ✓ | ✓ | | ✓ | ✓ | | +| Team | ✓ | ✓ | | | | ✓ | + +Ein Filter, der sichtbar ist aber nichts bewirkt, kostet Vertrauen in die +Zahlen. Deshalb werden irrelevante Filter gar nicht erst angeboten — die +Auswahl bleibt aber im Hintergrund erhalten und wirkt beim Seitenwechsel wieder. diff --git a/docs/ZIELSYSTEM.md b/docs/ZIELSYSTEM.md new file mode 100644 index 0000000..f1e649e --- /dev/null +++ b/docs/ZIELSYSTEM.md @@ -0,0 +1,215 @@ +# Zielsystem: HiveGrid Cloud + +Wohin dieser Prototyp eingebaut wird, was dort heute schon steht und was beim +Einbau konkret anzupassen ist. + +Repo: `HiveGrid-Cloud` (Gitea `git.tool-ctrl.de/toolctrl-labs/HiveGrid-Cloud`). +pnpm-Monorepo, React Router 7, Prisma/Postgres, mandantenfähig mit RLS. +`apps/web` (Tenant- und Admin-App), dazu `api`, `worker`, `desktop`, +`hocuspocus`, `portal-domain-controller` und rund 40 Pakete unter `packages/`. +Größenordnung: 678 Route-Dateien, davon 575 im Core-Manifest. + +--- + +## 1. Der Platz dieses Prototyps + +Das Ziel ist der Tenant-Dashboard-Screen unter `…//dashboard`. +Dort liegt bereits eine Implementierung: + +| Datei | Was drin steht | +|---|---| +| `apps/web/app/components/screens/dashboard/DashboardPage.tsx` | Widget-Grid, 495 Zeilen | +| `…/DashboardEditPanel.tsx` | Edit-Modus (Widget-Filter/Zeitraum) | +| `…/DashboardDrilldownDialog.tsx` | Drilldown je Kachel | +| `…/DashboardMonthlySeriesChart.tsx`, `…/DashboardStageDistributionChart.tsx` | die zwei vorhandenen Diagramme | +| `apps/web/app/routes/app.$tenant/dashboard.tsx` | Loader, SSE-Stream, Export | + +Fachlich ist das die Phase **P11** (Modul M20): drei feste Rollen-Layouts +(**GF · Büro · Monteur**) aus einer Widget-Registry, live über SSE, plus das +Komfort-Delta aus **P30** (Edit-Modus, Drilldown, CSV-/Report-Export, +Realtime-Opt-out). + +**Wichtig:** Dieser Stand ist ausdrücklich **Interim-UI nach AD-28** — der Dev +baut aus `~/ui` gegen ein stabiles View-Model, damit das Ticket nicht auf den +finalen Screen wartet. Der Kommentarkopf von `DashboardPage.tsx` sagt es +wörtlich: *„Final Subframe optics = tracked design-nachzug (D-DASH / VD-P30)."* + +Genau diese Lücke füllt dieser Prototyp. Der Einbau ist der geplante Nachzug, +kein Umbau gegen den Strom. + +--- + +## 2. Gemeinsame Basis — und wo sie auseinanderläuft + +| | Prototyp | HiveGrid Cloud | +|---|---|---| +| Subframe-Projekt | `f023064a5ca0` | `f023064a5ca0` — **dasselbe** | +| Bibliothek | `ui/`, 86 Komponenten | `apps/web/app/ui/`, Alias `~/ui/*`, 92 Komponenten | +| Überschneidung | 82 Komponenten gleich benannt | dieselben 82 | +| Diagramme | Recharts ^2.15.4 | Recharts ^2.15.3 — **dieselbe Bibliothek** | +| Framework | Next.js 16 | React Router 7 | + +Die vier Namensunterschiede: `DialogFullscreen` → `FullscreenDialog`, +`DrawerRight` → `Drawer`, `AddressDialog` und `BannerOffline` existieren dort +nicht. Cloud hat zusätzlich `BulkActionBar`, `ConfirmDialog`, `DropdownMenu`, +`ModalBlank`, `ResultDialog`, `BannerPermanent`, `ChatSprechblase`. + +Dass Komponentenbibliothek **und** Chart-Bibliothek übereinstimmen, ist der +Grund, warum der Einbau realistisch ein Port ist und keine Neuimplementierung. + +--- + +## 3. Was beim Einbau konkret anders wird + +### 3.1 Daten kommen als View-Model-Prop + +Der Prototyp importiert `DATENSATZ` direkt. In der Cloud gilt das Muster +``: Die Route besitzt Loader, SSE und Actions, die +Screen-Komponente ist rein und bekommt alles als Prop. Kein `useLoaderData` +in der Komponente. + +```ts +export type DashboardPageVM = VMState; + +type VMState = + | { status: "idle" } + | { status: "loading"; data?: T } + | { status: "ready"; data: T } + | { status: "empty" } + | { status: "error"; data?: T; error: { messageKey: string; code?: string } }; +``` + +Fünf Zustände statt einem. Der Prototyp kennt nur „Daten da"; `loading`, +`empty` und `error` sind je Seite zu ergänzen. Die Chart-Leerzustände +(`ChartLeer`) decken davon nur einen Teil ab. + +`DashboardPageData` führt außerdem `activeRole`, `layout`, `generatedAt`, +`streamUrl`, `canCustomize`, `liveUpdatesEnabled`, `layoutSource` und +`catalog` — das Dashboard ist dort rollen- und berechtigungsabhängig, was der +Prototyp nicht abbildet. + +### 3.2 Kein eigenes App-Chrome + +`AppShell.tsx` aus diesem Prototyp fällt beim Einbau **weg**. Die Cloud-Routen +laufen bereits in der echten App-Shell mit Sidebar, Tenant-Layout und +Breadcrumb (`SubframeAppShell.tsx`, `AppSidebar.tsx`). Screen-Komponenten +dürfen dort ausdrücklich **kein** `~/ui/layouts/DefaultPageLayout` rendern — +das bringt eine eigene Beispiel-Sidebar mit und erzeugt eine doppelte +Navigation. Stattdessen gilt `PageShell` / `PageHeader` / `PageBody` aus +`components/screens/shared/PageShell.tsx`. + +Unsere Seitenköpfe und die Filterleiste bleiben, die Rahmen-Navigation nicht. + +### 3.3 Texte über i18n + +Alle statischen Texte laufen über `react-i18next` (`t("app.dashboard.…")`). +Dieser Prototyp hat deutsche Strings fest im JSX. Das ist beim Einbau +mechanische Arbeit, aber flächendeckend — es betrifft jede Karte, jede +Achsenbeschriftung und jeden Leerzustand. + +Die deutschen **Bezeichner im Code** (`umsatz`, `auftraege`) bleiben davon +unberührt und passen zur Cloud, deren UI ebenfalls deutsch ist (EN-Locale erst +in P30). + +### 3.4 Barrierefreiheit der Diagramme + +In der Cloud sind Charts konsequent `aria-hidden="true"`, und die umgebende +Karte liefert eine Textzusammenfassung plus Datentabelle für Screenreader. +Dieser Prototyp hat das **nicht**. Beim Einbau ist es Pflicht — das Repo fährt +ein a11y-Lint-Baseline-Gate und axe-Tests je Primitive. + +### 3.5 Stabile Oberflächen-IDs + +Jede relevante Fläche trägt ein `data-ui-id` (`ui.dashboard.widget.…`), erfasst +in `docs/ui-inventory/surfaces.json`. Tests und Agenten adressieren darüber. +Unsere Widget-IDs aus dem `WidgetContext` sind der natürliche Anknüpfungspunkt, +müssen aber auf das dortige Namensschema umgestellt werden. + +### 3.6 Spacing passt bereits + +`.planning/INTERIM-UI-DESIGN-STANDARDS.md` schreibt fest: Page-Padding **24**, +Card **16**, Grid-Gap **16**, Section-Gap **24**, Field-Gap **16**, +Control-Höhe Medium **36**, Input-Radius **4**. Das deckt sich mit dem +8-px-System in [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) Abschnitt 5 — hier ist +nichts anzupassen. + +--- + +## 4. Was dieser Prototyp dem Zielsystem voraushat + +Nicht alles fließt nur in eine Richtung. Drei Dinge fehlen dort heute: + +**Ein gemeinsames Chart-Theme.** Die beiden vorhandenen Diagramme setzen +Farben direkt im JSX (`fill="#334155"`, `fill="#cbd5e1"`), jedes mit eigenem +Gitter (`strokeDasharray="3 3"`), eigener Achsenbreite und eigenem +Balkenradius. Es gibt keine geteilte Palette, keine Kategorien-Obergrenze und +keine feste Farbzuordnung. `features/dashboard/utils/chartTheme.ts` ist genau +das, was dort fehlt — und der Teil, der sich am ehesten unabhängig vom Rest +übernehmen lässt. + +**Ausformulierte Diagramm-Regeln.** [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md) hat in +der Cloud keine Entsprechung. Die Standards dort regeln Layout, Controls und +Zustände sehr genau, Datenvisualisierung dagegen gar nicht. + +**Fachliche Kennzahl-Definitionen.** [KPI-KATALOG.md](KPI-KATALOG.md) mit +Zeitbezug und Berechnung je KPI ist im Zielsystem so nicht vorhanden; die +Widget-Registry beschreibt Kacheln, nicht deren betriebswirtschaftliche +Definition. + +--- + +## 5. Wer entwirft was — der Rest ist noch nicht fertig designt + +HiveGrid Cloud ist nach dem **Designer-First-Prinzip** organisiert +(`.planning/DESIGNER-FIRST-PLAYBOOK.md`). Der Ablauf ist verbindlich: + +1. Dev/Planung stellt je Phase einen **Designer-Brief** bereit — + *was* das Modul tut und *welche UI-Elemente* vorkommen müssen, **kein Layout**. +2. Der Designer entwirft die Screens in Subframe und darf Spec und UI/UX + **überschreiben** — Anordnung und Layout sind eine Design-Entscheidung. +3. Das **Delta** (geänderte Spec-Punkte, neue UX-Entscheidungen, Subframe-Link) + wird in den Brief eingetragen. +4. Der Dev setzt um und zieht Spec und Doku nach. + +Es gibt 31 solcher Briefs (`.planning/designer-briefs/P01…P34.md`) — Auth, CRM, +Lead-Inbox, Projekt-Pipelines, Automation-Flow-Editor, Form-Builder, Plantafel, +Kalender, Telefonie, HR, Billing, Customer-Portal, Help-Center. Für einen +großen Teil davon steht der Delta-Block **noch leer**: Der finale Screen ist +noch nicht entworfen, die Route läuft bis dahin auf Interim-UI. + +**Für die Einordnung dieses Prototyps heißt das dreierlei:** + +- Das Dashboard ist **ein Baustein unter vielen**, nicht die Leitgestaltung der + Anwendung. Es fügt sich in eine App-Shell und ein Design-System ein, die + bereits stehen. +- Was hier an Anordnung und Raster festgelegt ist, ist ein **Vorschlag**. Die + Layout-Hoheit liegt beim Designer — der Brief zu P11 sagt ausdrücklich + „Anordnung = Davids freie Hand". Verbindlich zu übergeben sind die + **Kennzahl-Definitionen, die Chart-Regeln und das Verhalten**, nicht die + Pixelanordnung. +- Andere Bereiche der Anwendung sehen heute noch nicht final aus, weil sie + entweder auf Interim-UI laufen oder noch beim Designer liegen. Ein Vergleich + „unser Dashboard gegen den aktuellen Screen X" misst deshalb nicht + zwangsläufig gegen den Zielzustand. + +--- + +## 6. Wenn du dir das Zielsystem selbst ansehen willst + +```sh +corepack enable && pnpm install +pnpm dev:services # Postgres + Redis +pnpm dev:services:reset # Wegwerf-DB inkl. Migrationen und Demo-Daten +pnpm dev:local +``` + +Tenant-App auf `http://app.localhost:3000`. Die lokalen Demo-Zugänge (Tenant +„HiveGrid Demo SHK") stehen im README von HiveGrid Cloud unter „App starten" — +sie werden hier bewusst nicht wiederholt. Die Demo-Daten sind zunächst +ausgeblendet und erscheinen über **„Demo-Daten anzeigen"** oder `?demo=show`. + +Der Weg zum Dashboard ist nach dem Login die erste Seite der Tenant-App. + +Für einen schnellen Überblick ohne Setup: +`docs/ui-inventory/surface-map.md` listet alle stabilen Oberflächen mit ihren +Quelldateien, `docs/route-inventory.md` die Größenordnung des Routenbestands. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..3265855 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,13 @@ +import next from "eslint-config-next"; +import nextTypescript from "eslint-config-next/typescript"; + +const eslintConfig = [ + ...next, + ...nextTypescript, + { + // Die Subframe-Bibliothek wird unverändert übernommen und nicht geprüft. + ignores: ["ui/**", ".next/**", "node_modules/**"], + }, +]; + +export default eslintConfig; diff --git a/features/dashboard/components/cards/AlertList.tsx b/features/dashboard/components/cards/AlertList.tsx new file mode 100644 index 0000000..32f5d5d --- /dev/null +++ b/features/dashboard/components/cards/AlertList.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React from "react"; +import { + FeatherAlertTriangle, + FeatherArrowRight, + FeatherCheckCircle, + FeatherInfo, +} from "@subframe/core"; +import { STATUS_FARBEN } from "../../utils/chartTheme"; +import { formatEuro } from "../../utils/formatters"; +import { useToast } from "../../context/ToastContext"; +import type { AlertEintrag } from "../../data/calculations"; + +/** + * Handlungsbedarf-Liste (COMP-08). + * + * Jede Zeile nennt drei Dinge: was los ist, um wie viel Geld es geht und was + * zu tun ist. Ohne den Betrag lassen sich die Zeilen nicht gegeneinander + * abwägen, ohne die Handlung ist die Liste ein Bericht statt einer Arbeitsliste. + * Der Absprung in die jeweilige Arbeitsliste ist im Prototyp eine Attrappe. + */ +export function AlertList({ + eintraege, + leerText = "Keine offenen Punkte", + /** Zwei Spalten, wenn die Karte über die volle Seitenbreite läuft. */ + spalten = 1, +}: { + eintraege: AlertEintrag[]; + leerText?: string; + spalten?: 1 | 2; +}) { + const { zeigeToast } = useToast(); + const rang = { kritisch: 0, warnung: 1, hinweis: 2 } as const; + const sortiert = [...eintraege].sort((a, b) => rang[a.art] - rang[b.art]); + + if (!sortiert.length) { + return ( +
+ + {leerText} +
+ ); + } + + return ( + // Container-Query statt Viewport: dieselbe Liste steht mal über die volle + // Seitenbreite und mal in einer Drittelkarte. Der Viewport weiß davon nichts. +
    + {sortiert.map((eintrag) => { + const farbe = + eintrag.art === "kritisch" + ? STATUS_FARBEN.negativ + : eintrag.art === "warnung" + ? STATUS_FARBEN.warnung + : STATUS_FARBEN.info; + + return ( +
  • + +
  • + ); + })} +
+ ); +} diff --git a/features/dashboard/components/cards/ChartCard.tsx b/features/dashboard/components/cards/ChartCard.tsx new file mode 100644 index 0000000..bbfb536 --- /dev/null +++ b/features/dashboard/components/cards/ChartCard.tsx @@ -0,0 +1,144 @@ +"use client"; + +import React from "react"; +import { + FeatherDownload, + FeatherEyeOff, + FeatherInfo, + FeatherMoreHorizontal, +} from "@subframe/core"; +import { useWidgets } from "../../context/WidgetContext"; +import { useToast } from "../../context/ToastContext"; + +/** + * Rahmen für jede Visualisierung (COMP-09). + * + * Enthält die einzige "echte" Interaktionslogik des Prototyps: + * Über das ⋯-Menü lässt sich die Karte ausblenden; sie taucht dann in der + * Filterleiste zum Wiedereinblenden auf. Der Zustand ist persistiert. + */ +export function ChartCard({ + id, + titel, + untertitel, + hinweis, + aktion, + legende, + children, + className = "", +}: { + /** Stabile ID — Schlüssel für Ausblenden/Einblenden. */ + id: string; + titel: string; + untertitel?: string; + /** Erklärt die Kennzahl; erscheint als Tooltip am (i). */ + hinweis?: string; + aktion?: React.ReactNode; + legende?: React.ReactNode; + children: React.ReactNode; + className?: string; +}) { + const { istSichtbar, verstecke } = useWidgets(); + const { zeigeToast } = useToast(); + const [menuOffen, setMenuOffen] = React.useState(false); + const menu = React.useRef(null); + const ausloeser = React.useRef(null); + + React.useEffect(() => { + if (!menuOffen) return; + const schliesse = (event: MouseEvent) => { + if (!menu.current?.contains(event.target as Node)) setMenuOffen(false); + }; + // Escape schliesst und gibt den Fokus an den Ausloeser zurueck — sonst + // landet die Tastaturbedienung nach dem Schliessen am Seitenanfang. + const taste = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setMenuOffen(false); + ausloeser.current?.focus(); + } + }; + document.addEventListener("mousedown", schliesse); + document.addEventListener("keydown", taste); + return () => { + document.removeEventListener("mousedown", schliesse); + document.removeEventListener("keydown", taste); + }; + }, [menuOffen]); + + if (!istSichtbar(id)) return null; + + return ( +
+
+
+
+

+ {titel} +

+ {hinweis ? ( + + + + ) : null} +
+ {untertitel ? ( +

+ {untertitel} +

+ ) : null} +
+ +
+ {aktion} + + + {menuOffen ? ( +
+ + +
+ ) : null} +
+
+ + {legende ?
{legende}
: null} + +
{children}
+
+ ); +} diff --git a/features/dashboard/components/cards/KPICard.tsx b/features/dashboard/components/cards/KPICard.tsx new file mode 100644 index 0000000..8732527 --- /dev/null +++ b/features/dashboard/components/cards/KPICard.tsx @@ -0,0 +1,139 @@ +"use client"; + +import React from "react"; +import { FeatherArrowDownRight, FeatherArrowUpRight, FeatherMinus } from "@subframe/core"; +import { FeatherIcon } from "../layout/FeatherIcon"; +import { Sparkline } from "../charts/Sparkline"; +import { CHART, STATUS_FARBEN } from "../../utils/chartTheme"; + +/** Zeitbezug einer Kennzahl — siehe docs/KPI-KATALOG.md. */ +export type Bezug = "zeitraum" | "stichtag" | "gesamt"; + +const BEZUG_LABEL: Record = { + zeitraum: "im gewählten Zeitraum", + stichtag: "zum Stichtag", + gesamt: "über alle Zeiträume", +}; + +/** + * KPI-Karte nach UI_UX_GUIDELINES.md, Abschnitt 4: + * Label oben links · Icon oben rechts · Wert · Zusatz · Fußzeile. + * + * Alle Karten einer Reihe haben dieselbe Anatomie — auch die ohne Trend. + * Wo kein Vorperiodenvergleich möglich ist (stichtagsbezogene Kennzahlen), + * trägt die Fußzeile den Zeitbezug. Ohne das steht in einer Reihe eine + * Karte mit Trendzeile neben vier Karten mit leerem Fuß, und das Auge + * sucht eine Regel, die es nicht gibt. + * + * Der Trend vergleicht immer mit der gleich langen Vorperiode + * (30 Tage gegen die 30 Tage davor) — nicht mit dem Vorjahr. + */ +export function KPICard({ + label, + wert, + zusatz, + trend, + trendLabel = "Vorperiode", + bezug = "zeitraum", + icon, + verlauf, + akzent = "neutral", + /** Bei "positiv" ist ein steigender Wert gut, bei "negativ" schlecht. */ + richtungGut = "positiv", +}: { + label: string; + wert: string; + zusatz?: string; + trend?: { wert: number; richtung: "up" | "down" | "flat" }; + trendLabel?: string; + bezug?: Bezug; + icon: string; + verlauf?: number[]; + /** + * Färbt das Icon. Nur setzen, wenn die Farbe aus einem Schwellenvergleich + * kommt — ein dekorativ grünes Icon behauptet "gut", ohne es zu belegen + * (docs/DESIGN-SYSTEM.md §1: Statusfarben nur für Aussagen). + */ + akzent?: "neutral" | "positiv" | "warnung" | "negativ" | "info"; + richtungGut?: "positiv" | "negativ"; +}) { + const gut = + trend?.richtung === "flat" + ? null + : richtungGut === "positiv" + ? trend?.richtung === "up" + : trend?.richtung === "down"; + + const trendFarbe = + gut === null ? STATUS_FARBEN.neutral : gut ? STATUS_FARBEN.positiv : STATUS_FARBEN.negativ; + + const akzentFarbe = { + neutral: CHART.textSekundaer, + positiv: STATUS_FARBEN.positiv, + warnung: STATUS_FARBEN.warnung, + negativ: STATUS_FARBEN.negativ, + info: STATUS_FARBEN.info, + }[akzent]; + + return ( +
+
+ + {label} + + + + +
+ +
+ + {wert} + + {zusatz ? ( + {zusatz} + ) : null} +
+ + {/* Fußzeile: immer vorhanden, damit die Reihe eine gemeinsame Grundlinie hat. */} +
+ {trend ? ( + + {trend.richtung === "up" ? ( + + ) : trend.richtung === "down" ? ( + + ) : ( + + )} + {trend.richtung === "flat" + ? "unverändert" + : `${trend.wert.toFixed(1).replace(".", ",")} %`} + + {trendLabel} + + + ) : ( + + {BEZUG_LABEL[bezug]} + + )} + + {verlauf && verlauf.length > 1 ? ( + + {/* Die Kurve trägt dieselbe Aussage wie der Pfeil daneben und + deshalb auch dieselbe Farbe — sonst zeigt die Karte zwei + widersprüchliche Signale. */} + + + ) : null} +
+
+ ); +} diff --git a/features/dashboard/components/cards/StatZeile.tsx b/features/dashboard/components/cards/StatZeile.tsx new file mode 100644 index 0000000..6ea7c48 --- /dev/null +++ b/features/dashboard/components/cards/StatZeile.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +/** + * Kompakte Zusatzkennzahlen unter einem Chart — + * beantwortet "und was heißt das jetzt?" ohne eigene Karte. + */ +export function StatZeile({ + eintraege, + /** + * Spalten auf breiten Schirmen. In einer Drittelkarte brechen vier Spalten + * mitten im Betrag um ("1,22 Mio." / "€") — dort gehoeren zwei hin. + */ + spalten = 4, +}: { + eintraege: { label: string; wert: string; hinweis?: string }[]; + spalten?: 2 | 3 | 4; +}) { + const raster = { 2: "md:grid-cols-2", 3: "md:grid-cols-3", 4: "md:grid-cols-4" }[spalten]; + return ( +
+ {eintraege.map((eintrag) => ( +
+ + {eintrag.label} + + + {eintrag.wert} + + {eintrag.hinweis ? ( + {eintrag.hinweis} + ) : null} +
+ ))} +
+ ); +} diff --git a/features/dashboard/components/charts/AreaVerlauf.tsx b/features/dashboard/components/charts/AreaVerlauf.tsx new file mode 100644 index 0000000..b70a114 --- /dev/null +++ b/features/dashboard/components/charts/AreaVerlauf.tsx @@ -0,0 +1,131 @@ +"use client"; + +import React from "react"; +import { + Area, + CartesianGrid, + ComposedChart, + Line, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { ChartTooltip } from "./ChartTooltip"; +import { ChartLeer } from "./ChartLeer"; +import { + CHART, + MARGIN, + MAX_ACHSENLABELS, + PRIMAER_FARBE, + VERGLEICH_FARBE, + achseX, + achseY, + nettesRaster, +} from "../../utils/chartTheme"; +import type { ZeitreihePunkt } from "../../data/types"; + +/** + * Zeitverlauf als Fläche mit optionaler Vergleichslinie (Vorjahr). + * + * Bewusst `ComposedChart`, nicht `AreaChart`: Recharts sammelt in einem + * AreaChart ausschließlich `Area`-Kinder ein (generateCategoricalChart mit + * GraphicalChild = Area). Ein `` darin wird stillschweigend verworfen — + * die Vergleichsreihe stand monatelang in der Legende, ohne je gezeichnet zu + * werden. ComposedChart nimmt beide Typen. + * + * Carbon-Regeln, die hier umgesetzt sind: + * - nur horizontale Gridlines (#e0e0e0), vertikale lenken ab + * - keine Tick-Marks, Achsenbeschriftung 12 px in #525252 + * - lineare Interpolation: eine Monotone-Kurve erfindet Zwischenwerte + * zwischen zwei Messpunkten, die es nicht gibt + * - Vergleichsreihe gestrichelt und neutral, damit sie die Hauptreihe + * nicht um Aufmerksamkeit bringt + */ +export function AreaVerlauf({ + daten, + formatWert, + /** Kürzere Schreibweise für die Y-Achse; ohne Angabe wie formatWert. */ + formatAchse, + hoehe = 260, + zeigeVergleich = false, + farbe = PRIMAER_FARBE, + labelPrefix, +}: { + daten: ZeitreihePunkt[]; + formatWert: (wert: number) => string; + formatAchse?: (wert: number) => string; + hoehe?: number; + zeigeVergleich?: boolean; + farbe?: string; + labelPrefix?: string; +}) { + // Zwei Verläufe auf einer Seite dürfen sich keinen Gradienten teilen. + const gradientId = `verlauf-${React.useId().replace(/:/g, "")}`; + + const hatWerte = daten.some((d) => d.value > 0 || (d.vergleich ?? 0) > 0); + if (!daten.length || !hatWerte) return ; + + // Bei vielen Punkten nur jede n-te Beschriftung zeigen, sonst überlappen sie. + const intervall = + daten.length > MAX_ACHSENLABELS ? Math.ceil(daten.length / MAX_ACHSENLABELS) - 1 : 0; + + const maximum = Math.max( + ...daten.map((d) => Math.max(d.value, zeigeVergleich ? (d.vergleich ?? 0) : 0)) + ); + const raster = nettesRaster(maximum); + + return ( +
+ + + + + + + + + + + + + ( + + )} + /> + + + + {zeigeVergleich ? ( + + ) : null} + + +
+ ); +} diff --git a/features/dashboard/components/charts/AuslastungsBalken.tsx b/features/dashboard/components/charts/AuslastungsBalken.tsx new file mode 100644 index 0000000..61dc975 --- /dev/null +++ b/features/dashboard/components/charts/AuslastungsBalken.tsx @@ -0,0 +1,135 @@ +"use client"; + +import React from "react"; +import { CHART, STATUS_FARBEN } from "../../utils/chartTheme"; +import { ChartLeer } from "./ChartLeer"; + +/** + * Auslastung je Person als Punktdiagramm mit Zielkorridor. + * + * Warum kein Balken: Auslastungswerte liegen im Betrieb zwischen etwa 70 und + * 100 %. Ein Balken muss bei 0 beginnen (sonst verzerrt die Länge), und dann + * sind 74 % und 96 % optisch fast gleich lang — das Diagramm zeigt nichts. + * Ein Punkt kodiert über die Position, nicht über die Länge, und darf deshalb + * einen ausgeschnittenen Bereich zeigen. Carbon nennt das Dot-Plot. + * + * Die Schwellen sind betrieblich gedacht, nicht mathematisch: + * < 70 % Luft frei — Kapazität ungenutzt + * 70–95 % Zielkorridor + * > 95 % ausgebucht — Überlastrisiko, Termine gefährdet + * Der Korridor liegt als graues Band hinter den Punkten, die 100-%-Marke als + * beschriftete Linie. Damit erklärt sich die Grafik ohne Ampel-Legende. + */ +const SCHWELLEN = { frei: 70, ausgebucht: 95 }; + +export function AuslastungsBalken({ + zeilen, + maxZeilen = 8, +}: { + zeilen: { name: string; wert: number; zusatz?: string }[]; + maxZeilen?: number; +}) { + const [aktiv, setAktiv] = React.useState(null); + const sichtbar = zeilen.slice(0, maxZeilen); + if (!sichtbar.length) return ; + + // Skala umschließt die Daten, bleibt aber mindestens 60–110 %, damit der + // Zielkorridor immer als Fläche erkennbar ist. + const min = Math.min(60, Math.floor(Math.min(...sichtbar.map((z) => z.wert)) / 10) * 10); + const max = Math.max(110, Math.ceil(Math.max(...sichtbar.map((z) => z.wert)) / 10) * 10); + const pos = (wert: number) => ((Math.min(Math.max(wert, min), max) - min) / (max - min)) * 100; + + const ticks = []; + for (let t = min; t <= max; t += 10) ticks.push(t); + + return ( +
+ {sichtbar.map((zeile) => { + const farbe = + zeile.wert < SCHWELLEN.frei + ? STATUS_FARBEN.info + : zeile.wert <= SCHWELLEN.ausgebucht + ? STATUS_FARBEN.positiv + : STATUS_FARBEN.warnung; + const hervorgehoben = aktiv === zeile.name; + + return ( +
setAktiv(zeile.name)} + onMouseLeave={() => setAktiv(null)} + className="grid grid-cols-[minmax(96px,24%)_minmax(0,1fr)_48px] items-center gap-3" + > + + {zeile.name} + {zeile.zusatz ? ( + + {zeile.zusatz} + + ) : null} + + + + {/* Zielkorridor als ruhiges Band hinter allen Zeilen */} +
+ ); + })} + + {/* Achse statt Farblegende — die Zonen erklären sich über das Band. */} +
+ + + {ticks.map((t) => ( + + {t} + + ))} + + % +
+ +

+ Graues Band = Zielkorridor {SCHWELLEN.frei}–{SCHWELLEN.ausgebucht} %, Linie = 100 % der + vertraglichen Stunden. +

+
+ ); +} diff --git a/features/dashboard/components/charts/BalkenHorizontal.tsx b/features/dashboard/components/charts/BalkenHorizontal.tsx new file mode 100644 index 0000000..81ba407 --- /dev/null +++ b/features/dashboard/components/charts/BalkenHorizontal.tsx @@ -0,0 +1,96 @@ +"use client"; + +import React from "react"; +import { farbeFuer, farbe as paletteFarbe } from "../../utils/chartTheme"; +import { ChartLeer } from "./ChartLeer"; + +/** + * Horizontale Balken für Rangfolgen (Top-Kunden, Aufträge je Gewerk). + * + * Bewusst als HTML/CSS statt Recharts: Kategorienamen sind hier lang und + * unterschiedlich breit; im SVG müsste man sie hart abschneiden. In HTML + * bleiben sie umbruchfähig, selektierbar und barrierefrei lesbar. + * Hover hebt den Balken hervor und zeigt den vollen Namen als Titel. + */ +export function BalkenHorizontal({ + daten, + formatWert, + maxBalken = 6, + /** + * Einfarbig für Rangfolgen (Top-Kunden, Aufträge je Mitarbeiter): + * Dort trägt die Farbe keine Information und würde nur Unruhe erzeugen. + * Palette nur, wenn die Balken echte Kategorien sind. + */ + einfarbig = true, + farbeStart = 1, + /** + * Zweitwert rechts neben dem Hauptwert — z. B. Auftragswert neben der + * Anzahl Abschlüsse. Erspart eine zweite Karte mit derselben Kodierung + * auf derselben Personenachse. + */ + formatZweitwert, +}: { + daten: { name: string; value: number; zweitwert?: number }[]; + formatWert: (wert: number) => string; + maxBalken?: number; + einfarbig?: boolean; + farbeStart?: number; + formatZweitwert?: (wert: number) => string; +}) { + const [aktiv, setAktiv] = React.useState(null); + + const sichtbar = [...daten] + .filter((d) => d.value > 0) + .sort((a, b) => b.value - a.value) + .slice(0, maxBalken); + + if (!sichtbar.length) return ; + + const max = Math.max(...sichtbar.map((d) => d.value)); + + return ( +
    + {sichtbar.map((eintrag, index) => { + const anteil = max > 0 ? (eintrag.value / max) * 100 : 0; + const balkenFarbe = einfarbig + ? paletteFarbe(1) + : farbeFuer(eintrag.name, farbeStart + index); + const hervorgehoben = aktiv === eintrag.name; + + return ( +
  • setAktiv(eintrag.name)} + onMouseLeave={() => setAktiv(null)} + className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1" + > + + {eintrag.name} + + + + {formatWert(eintrag.value)} + + {formatZweitwert && eintrag.zweitwert !== undefined ? ( + + {formatZweitwert(eintrag.zweitwert)} + + ) : null} + + + + +
  • + ); + })} +
+ ); +} diff --git a/features/dashboard/components/charts/BalkenVertikal.tsx b/features/dashboard/components/charts/BalkenVertikal.tsx new file mode 100644 index 0000000..c09738a --- /dev/null +++ b/features/dashboard/components/charts/BalkenVertikal.tsx @@ -0,0 +1,106 @@ +"use client"; + +import React from "react"; +import { Bar, BarChart, CartesianGrid, Cell, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; +import { ChartTooltip } from "./ChartTooltip"; +import { ChartLeer } from "./ChartLeer"; +import { + BALKEN, + CHART, + MARGIN, + MAX_ACHSENLABELS, + PRIMAER_FARBE, + achseX, + achseY, + nettesRaster, +} from "../../utils/chartTheme"; + +/** + * Vertikale Balken für Mengen über Zeit oder Kategorien mit Nulllinie. + * Balkenbreite ist gedeckelt (Carbon: Abstand ≥ halbe Balkenbreite), + * damit vier Balken nicht wie Flächen wirken. + */ +export function BalkenVertikal({ + daten, + formatWert, + /** Kürzere Schreibweise für die Y-Achse; ohne Angabe wie formatWert. */ + formatAchse, + hoehe = 240, + farbe = PRIMAER_FARBE, + /** Einzelne Balken einfärben, z. B. Alters-Buckets von grün nach rot. */ + farbenProBalken, + /** Bei Stückzahlen: keine Zwischenwerte auf der Achse ("1, 1, 2, 2"). */ + nurGanzzahlen = false, + labelPrefix, +}: { + daten: { name?: string; label?: string; value: number }[]; + formatWert: (wert: number) => string; + formatAchse?: (wert: number) => string; + hoehe?: number; + farbe?: string; + farbenProBalken?: string[]; + nurGanzzahlen?: boolean; + labelPrefix?: string; +}) { + const [aktiv, setAktiv] = React.useState(null); + + const punkte = daten.map((d) => ({ label: d.label ?? d.name ?? "", value: d.value })); + if (!punkte.length || !punkte.some((p) => p.value > 0)) return ; + + const intervall = + punkte.length > MAX_ACHSENLABELS ? Math.ceil(punkte.length / MAX_ACHSENLABELS) - 1 : 0; + + // Ganzzahlreihen brauchen ganzzahlige Ticks — sonst steht "1,5 Vorgänge". + const maximum = Math.max(...punkte.map((p) => p.value)); + const raster = nurGanzzahlen + ? undefined + : nettesRaster(maximum); + + return ( +
+ + + setAktiv(typeof zustand?.activeTooltipIndex === "number" ? zustand.activeTooltipIndex : null) + } + onMouseLeave={() => setAktiv(null)} + > + + + + ( + + )} + /> + + {punkte.map((_, index) => ( + + ))} + + + +
+ ); +} diff --git a/features/dashboard/components/charts/ChartLeer.tsx b/features/dashboard/components/charts/ChartLeer.tsx new file mode 100644 index 0000000..d773567 --- /dev/null +++ b/features/dashboard/components/charts/ChartLeer.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { FeatherBarChart2 } from "@subframe/core"; + +/** Leerzustand — wichtiger als er aussieht: Filter können jedes Chart leeren. */ +export function ChartLeer({ + text = "Keine Daten im gewählten Zeitraum", + hoehe = 220, +}: { + text?: string; + hoehe?: number; +}) { + return ( +
+ + {text} +
+ ); +} diff --git a/features/dashboard/components/charts/ChartLegende.tsx b/features/dashboard/components/charts/ChartLegende.tsx new file mode 100644 index 0000000..5c0c49d --- /dev/null +++ b/features/dashboard/components/charts/ChartLegende.tsx @@ -0,0 +1,64 @@ +"use client"; + +import React from "react"; + +/** + * Legende als eigene Komponente statt Recharts-Legend: + * Recharts platziert die Legende im SVG und kostet damit Chartfläche. + * Carbon setzt sie über den Chart, linksbündig, 12 px, 12 px Marker. + */ +export function ChartLegende({ + eintraege, + /** Ausgeblendete Reihen erscheinen ausgegraut — Klick schaltet um. */ + inaktiv = [], + onToggle, +}: { + eintraege: { name: string; farbe: string; wert?: string; gestrichelt?: boolean }[]; + inaktiv?: string[]; + onToggle?: (name: string) => void; +}) { + return ( +
+ {eintraege.map((eintrag) => { + const aus = inaktiv.includes(eintrag.name); + const Inhalt = ( + <> + + + {eintrag.name} + + {eintrag.wert ? ( + + {eintrag.wert} + + ) : null} + + ); + + return onToggle ? ( + + ) : ( + + {Inhalt} + + ); + })} +
+ ); +} diff --git a/features/dashboard/components/charts/ChartTooltip.tsx b/features/dashboard/components/charts/ChartTooltip.tsx new file mode 100644 index 0000000..31f5d44 --- /dev/null +++ b/features/dashboard/components/charts/ChartTooltip.tsx @@ -0,0 +1,77 @@ +"use client"; + +import React from "react"; +import { CHART } from "../../utils/chartTheme"; + +/** + * Einheitlicher Tooltip für alle Charts. + * + * Carbon-Vorgabe: dunkle Fläche (#393939), heller Text, 12 px Label, + * kein Rahmen, kein Doppelpunkt zwischen Name und Wert. + * Ein gemeinsamer Tooltip verhindert, dass jedes Chart eigene Regeln erfindet. + * + * Die Props sind bewusst weit typisiert: Recharts reicht sein internes + * Payload-Format durch, das je nach Chart-Typ leicht abweicht. + */ +interface TooltipPayload { + name?: unknown; + value?: unknown; + color?: string; + dataKey?: unknown; +} + +export function ChartTooltip({ + active, + label, + payload, + formatWert, + labelPrefix, +}: { + active?: boolean; + label?: unknown; + payload?: readonly TooltipPayload[]; + formatWert: (wert: number) => string; + labelPrefix?: string; +}) { + if (!active || !payload?.length) return null; + + const reihen = payload + .filter((eintrag) => typeof eintrag.value === "number") + .map((eintrag) => ({ + name: String(eintrag.name ?? eintrag.dataKey ?? ""), + value: eintrag.value as number, + color: eintrag.color, + })); + + if (!reihen.length) return null; + + const labelText = label === undefined || label === null ? "" : String(label); + + return ( +
+ {labelText ? ( +
+ {labelPrefix ? `${labelPrefix} ` : ""} + {labelText} +
+ ) : null} +
+ {reihen.map((reihe) => ( +
+ {reihe.color ? ( + + ) : null} + {reihe.name} + {formatWert(reihe.value)} +
+ ))} +
+
+ ); +} diff --git a/features/dashboard/components/charts/Donut.tsx b/features/dashboard/components/charts/Donut.tsx new file mode 100644 index 0000000..dfe7417 --- /dev/null +++ b/features/dashboard/components/charts/Donut.tsx @@ -0,0 +1,118 @@ +"use client"; + +import React from "react"; +import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; +import { ChartTooltip } from "./ChartTooltip"; +import { ChartLeer } from "./ChartLeer"; +import { DONUT, farbeFuer, gruppiereRest } from "../../utils/chartTheme"; + +/** + * Donut für Anteile. + * + * Regeln aus der Planung + Carbon: + * - maximal 5 Segmente, Rest als "Sonstige" (gruppiereRest) + * - größtes Segment startet oben, dann im Uhrzeigersinn absteigend + * - Gesamtwert steht in der Mitte, sonst muss man die Segmente addieren + * - Legende rechts mit Wert, einzeilig — nicht im Chart + * - Klick auf einen Legendeneintrag hebt das Segment hervor + */ +export function Donut({ + daten, + formatWert, + hoehe = 220, + mitteLabel = "Gesamt", + maxSegmente, + /** + * Format der Ringmitte. Getrennt vom Legendenformat, weil die Mitte eine + * Schlagzahl ist: Steht dieselbe Summe als Kennzahl auf der Seite, muss sie + * gleich geschrieben sein — "285,3 T€" neben "285.318 €" liest sich wie + * zwei verschiedene Zahlen. + */ + formatMitte, +}: { + daten: { name: string; value: number }[]; + formatWert: (wert: number) => string; + hoehe?: number; + mitteLabel?: string; + maxSegmente?: number; + formatMitte?: (wert: number) => string; +}) { + const [aktiv, setAktiv] = React.useState(null); + + const segmente = gruppiereRest(daten.filter((d) => d.value > 0), maxSegmente); + if (!segmente.length) return ; + + const gesamt = segmente.reduce((summe, s) => summe + s.value, 0); + + return ( +
+
+ + + + {segmente.map((segment, index) => ( + + ))} + + } /> + + + +
+ + {mitteLabel} + + + {(formatMitte ?? formatWert)(gesamt)} + +
+
+ +
    + {segmente.map((segment, index) => { + const anteil = gesamt > 0 ? (segment.value / gesamt) * 100 : 0; + return ( +
  • + +
  • + ); + })} +
+
+ ); +} diff --git a/features/dashboard/components/charts/Sparkline.tsx b/features/dashboard/components/charts/Sparkline.tsx new file mode 100644 index 0000000..9c7f6aa --- /dev/null +++ b/features/dashboard/components/charts/Sparkline.tsx @@ -0,0 +1,47 @@ +"use client"; + +import React from "react"; + +/** + * Miniaturverlauf in KPI-Karten. + * Bewusst als handgeschriebenes SVG: Recharts würde für 8 Punkte + * einen ResponsiveContainer samt Resize-Beobachter aufziehen. + */ +export function Sparkline({ werte, farbe }: { werte: number[]; farbe: string }) { + if (werte.length < 2) return null; + + const max = Math.max(...werte); + const min = Math.min(...werte); + const spanne = Math.max(1, max - min); + + const punkte = werte + .map((wert, index) => { + const x = (index / (werte.length - 1)) * 100; + const y = 28 - ((wert - min) / spanne) * 24; + return `${x.toFixed(2)},${y.toFixed(2)}`; + }) + .join(" "); + + const id = `spark-${farbe.replace("#", "")}`; + + return ( + + ); +} diff --git a/features/dashboard/components/charts/Trichter.tsx b/features/dashboard/components/charts/Trichter.tsx new file mode 100644 index 0000000..5dc58e2 --- /dev/null +++ b/features/dashboard/components/charts/Trichter.tsx @@ -0,0 +1,85 @@ +"use client"; + +import React from "react"; +import { KATEGORIE_FARBEN } from "../../utils/chartTheme"; +import { ChartLeer } from "./ChartLeer"; + +/** + * Vertriebstrichter. + * + * Zeigt je Stufe die absolute Zahl, die Balkenlänge als Anteil an der ersten + * Stufe und die Übergangsquote von der vorherigen Stufe. Genau die + * Übergangsquote ist die Zahl, nach der Betriebe ihren Vertrieb steuern: + * Sie zeigt, wo Vorgänge verloren gehen. + * + * Einfarbig: Die Stufen sind eine Reihenfolge, keine unabhängigen Kategorien — + * DESIGN-SYSTEM.md §1 „Rangfolgen einfarbig". Eine Farbabstufung würde nur + * kodieren, was die Reihenfolge ohnehin sagt. + * + * Die Balken sind zentriert, nicht linksbündig: So liest man die Verjüngung als + * Trichterform, ohne die gemeinsame Skala aufzugeben. Ein echtes Funnel-Chart + * mit breitenkodierten Segmenten kennt Carbon bewusst nicht — es verzerrt. + */ +export function Trichter({ + stufen, + formatWert, +}: { + stufen: { name: string; value: number }[]; + formatWert: (wert: number) => string; +}) { + const [aktiv, setAktiv] = React.useState(null); + + if (!stufen.length || stufen[0].value === 0) return ; + + const start = stufen[0].value; + + return ( +
    + {stufen.map((stufe, index) => { + const anteilGesamt = (stufe.value / start) * 100; + const vorher = index > 0 ? stufen[index - 1].value : null; + const uebergang = vorher && vorher > 0 ? (stufe.value / vorher) * 100 : null; + const hervorgehoben = aktiv === index; + + return ( +
  1. setAktiv(index)} + onMouseLeave={() => setAktiv(null)} + className="grid grid-cols-[96px_minmax(0,1fr)_52px_44px] items-center gap-2" + > + {stufe.name} + + + + + + + {formatWert(stufe.value)} + + + + {uebergang !== null ? `${uebergang.toFixed(0)} %` : "—"} + +
  2. + ); + })} + +
  3. + Rechte Spalte: Übergang von der vorherigen Stufe +
  4. +
+ ); +} diff --git a/features/dashboard/components/filters/AnalyticsToolbar.tsx b/features/dashboard/components/filters/AnalyticsToolbar.tsx new file mode 100644 index 0000000..5b1a6bf --- /dev/null +++ b/features/dashboard/components/filters/AnalyticsToolbar.tsx @@ -0,0 +1,203 @@ +"use client"; + +import React from "react"; +import { FeatherEye, FeatherRotateCcw, FeatherX } from "@subframe/core"; +import { MehrfachAuswahl } from "./MehrfachAuswahl"; +import { useFilter, type MehrfachFeld } from "../../context/FilterContext"; +import { useWidgets } from "../../context/WidgetContext"; +import { DATENSATZ } from "../../data/seed"; +import { FILTER_RELEVANZ, type SeitenId } from "../../data/filtering"; +import { + AUFTRAG_STATUS_LABEL, + GEWERKE, + ZEITRAUM_LABEL, + type AuftragStatus, + type ZeitraumPreset, +} from "../../data/types"; + +const ZEITRAEUME: ZeitraumPreset[] = ["heute", "7_tage", "30_tage", "quartal", "jahr", "alles"]; + +/** + * Filterleiste über dem Seiteninhalt. + * + * Zeigt nur die Filter, die auf der jeweiligen Seite etwas bewirken + * (FILTER_RELEVANZ) — ein Auftragsstatus-Filter auf der Finanzseite würde + * sonst wirkungslos wirken und Vertrauen kosten. + * Aktive Einschränkungen erscheinen darunter als entfernbare Chips (FILT-06). + */ +export function AnalyticsToolbar({ seite }: { seite: SeitenId }) { + const { filter, setFilter, toggleWert, leereFeld, zuruecksetzen } = useFilter(); + const { versteckt, titel, zeige, zeigeAlle } = useWidgets(); + + const erlaubt = new Set(FILTER_RELEVANZ[seite]); + + const quellen = React.useMemo( + () => [...new Set(DATENSATZ.kunden.map((k) => k.quelle))].sort(), + [] + ); + const regionen = React.useMemo( + () => [...new Set(DATENSATZ.kunden.map((k) => k.region))].sort(), + [] + ); + const monteure = React.useMemo( + () => + DATENSATZ.mitarbeiter + .filter((m) => m.rolle === "Monteur" || m.rolle === "Meister") + .map((m) => ({ wert: m.id, label: m.name, zusatz: m.gewerk })), + [] + ); + + const chips: { feld: MehrfachFeld; wert: string; label: string }[] = []; + if (erlaubt.has("gewerke")) + chips.push(...filter.gewerke.map((g) => ({ feld: "gewerke" as const, wert: g, label: g }))); + if (erlaubt.has("auftragStatus")) + chips.push( + ...filter.auftragStatus.map((s) => ({ + feld: "auftragStatus" as const, + wert: s, + label: AUFTRAG_STATUS_LABEL[s], + })) + ); + if (erlaubt.has("kundenQuellen")) + chips.push( + ...filter.kundenQuellen.map((q) => ({ feld: "kundenQuellen" as const, wert: q, label: q })) + ); + if (erlaubt.has("regionen")) + chips.push(...filter.regionen.map((r) => ({ feld: "regionen" as const, wert: r, label: r }))); + if (erlaubt.has("mitarbeiterIds")) + chips.push( + ...filter.mitarbeiterIds.map((id) => ({ + feld: "mitarbeiterIds" as const, + wert: id, + label: DATENSATZ.mitarbeiter.find((m) => m.id === id)?.name ?? id, + })) + ); + + return ( +
+
+ {/* Zeitraum als Segment-Control: eine Auswahl, immer sichtbar. */} +
+ {ZEITRAEUME.map((preset) => ( + + ))} +
+ + + + {erlaubt.has("gewerke") ? ( + ({ wert: g, label: g }))} + ausgewaehlt={filter.gewerke} + onToggle={(wert) => toggleWert("gewerke", wert)} + onLeeren={() => leereFeld("gewerke")} + /> + ) : null} + + {erlaubt.has("auftragStatus") ? ( + ({ + wert: s, + label: AUFTRAG_STATUS_LABEL[s], + }))} + ausgewaehlt={filter.auftragStatus} + onToggle={(wert) => toggleWert("auftragStatus", wert)} + onLeeren={() => leereFeld("auftragStatus")} + /> + ) : null} + + {erlaubt.has("kundenQuellen") ? ( + ({ wert: q, label: q }))} + ausgewaehlt={filter.kundenQuellen} + onToggle={(wert) => toggleWert("kundenQuellen", wert)} + onLeeren={() => leereFeld("kundenQuellen")} + /> + ) : null} + + {erlaubt.has("regionen") ? ( + ({ wert: r, label: r }))} + ausgewaehlt={filter.regionen} + onToggle={(wert) => toggleWert("regionen", wert)} + onLeeren={() => leereFeld("regionen")} + /> + ) : null} + + {erlaubt.has("mitarbeiterIds") ? ( + toggleWert("mitarbeiterIds", wert)} + onLeeren={() => leereFeld("mitarbeiterIds")} + /> + ) : null} + + {chips.length ? ( + + ) : null} +
+ + {chips.length ? ( +
+ {chips.map((chip) => ( + + ))} +
+ ) : null} + + {versteckt.length ? ( +
+ + + {versteckt.length} ausgeblendet: + + {versteckt.map((id) => ( + + ))} + +
+ ) : null} +
+ ); +} diff --git a/features/dashboard/components/filters/MehrfachAuswahl.tsx b/features/dashboard/components/filters/MehrfachAuswahl.tsx new file mode 100644 index 0000000..f1823d1 --- /dev/null +++ b/features/dashboard/components/filters/MehrfachAuswahl.tsx @@ -0,0 +1,141 @@ +"use client"; + +import React from "react"; +import { FeatherCheck, FeatherChevronDown, FeatherSearch } from "@subframe/core"; + +/** + * Mehrfachauswahl mit Suche (COMP-06). + * Bewusst schlank gehalten: ein Button, ein Panel, Klick außerhalb schließt. + */ +export function MehrfachAuswahl({ + label, + optionen, + ausgewaehlt, + onToggle, + onLeeren, + suchbarAb = 8, +}: { + label: string; + optionen: { wert: string; label: string; zusatz?: string }[]; + ausgewaehlt: string[]; + onToggle: (wert: string) => void; + onLeeren: () => void; + /** Ab wie vielen Optionen ein Suchfeld eingeblendet wird. */ + suchbarAb?: number; +}) { + const [offen, setOffen] = React.useState(false); + const [suche, setSuche] = React.useState(""); + const wrapper = React.useRef(null); + const ausloeser = React.useRef(null); + + React.useEffect(() => { + if (!offen) return; + const schliesse = (event: MouseEvent) => { + if (!wrapper.current?.contains(event.target as Node)) setOffen(false); + }; + const taste = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setOffen(false); + ausloeser.current?.focus(); + } + }; + document.addEventListener("mousedown", schliesse); + document.addEventListener("keydown", taste); + return () => { + document.removeEventListener("mousedown", schliesse); + document.removeEventListener("keydown", taste); + }; + }, [offen]); + + const gefiltert = optionen.filter((o) => + o.label.toLowerCase().includes(suche.toLowerCase()) + ); + + return ( +
+ + + {offen ? ( +
+ {optionen.length >= suchbarAb ? ( +
+ + setSuche(event.target.value)} + placeholder="Suchen …" + className="w-full bg-transparent text-[13px] outline-none placeholder:text-text-neutral-subtle" + /> +
+ ) : null} + +
+ {gefiltert.map((option) => { + const aktiv = ausgewaehlt.includes(option.wert); + return ( + + ); + })} + {!gefiltert.length ? ( +
+ Kein Treffer +
+ ) : null} +
+ +
+ + +
+
+ ) : null} +
+ ); +} diff --git a/features/dashboard/components/layout/AppShell.tsx b/features/dashboard/components/layout/AppShell.tsx new file mode 100644 index 0000000..53b3e88 --- /dev/null +++ b/features/dashboard/components/layout/AppShell.tsx @@ -0,0 +1,159 @@ +"use client"; + +import React from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { + FeatherBell, + FeatherHelpCircle, + FeatherPanelLeft, + FeatherSearch, +} from "@subframe/core"; +import { ALLE_SEITEN, NAV_GRUPPEN } from "./navigation"; +import { FeatherIcon } from "./FeatherIcon"; +import { DATENSATZ_INFO } from "../../data/seed"; +import { formatDatum, formatZahl } from "../../utils/formatters"; +import { HEUTE } from "../../utils/dateUtils"; + +/** + * Rahmen der Anwendung: feste Seitenleiste links, Inhalt rechts. + * + * Warum Seitenleiste statt Tab-Leiste: Die echte HiveGrid-Oberfläche + * navigiert links; so passt der Prototyp beim Einbau ohne Umbau der + * Informationsarchitektur. Die Leiste lässt sich auf Icon-Breite einklappen, + * damit breite Tabellen und 12-Spalten-Raster Platz bekommen. + */ +export function AppShell({ children }: { children: React.ReactNode }) { + const pathname = usePathname(); + const [eingeklappt, setEingeklappt] = React.useState(false); + + const aktiveSeite = + ALLE_SEITEN.find((s) => s.href !== "/" && pathname.startsWith(s.href)) ?? + ALLE_SEITEN.find((s) => s.href === "/"); + + return ( +
+ + +
+
+
+ Statistiken + / + + {aktiveSeite?.label ?? "Übersicht"} + +
+ +
+
+ + Suchen + + ⌘K + +
+ + + + MK + +
+
+ +
{children}
+
+
+ ); +} diff --git a/features/dashboard/components/layout/FeatherIcon.tsx b/features/dashboard/components/layout/FeatherIcon.tsx new file mode 100644 index 0000000..0250b03 --- /dev/null +++ b/features/dashboard/components/layout/FeatherIcon.tsx @@ -0,0 +1,18 @@ +"use client"; + +import React from "react"; +import * as SubframeCore from "@subframe/core"; + +type IconKomponente = React.ComponentType<{ className?: string }>; + +/** + * Löst einen Icon-Namen ("Euro") gegen das Feather-Set von @subframe/core auf. + * So können Konfigurationsdateien (navigation.ts) Icons als String führen, + * ohne React importieren zu müssen. + */ +export function FeatherIcon({ name, className }: { name: string; className?: string }) { + const registry = SubframeCore as unknown as Record; + const Komponente = registry[`Feather${name}`]; + if (!Komponente) return null; + return ; +} diff --git a/features/dashboard/components/layout/PageContainer.tsx b/features/dashboard/components/layout/PageContainer.tsx new file mode 100644 index 0000000..22289b9 --- /dev/null +++ b/features/dashboard/components/layout/PageContainer.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +/** + * Inhaltsspalte einer Seite. + * 24 px Außenabstand, 16 px Rasterabstand — entspricht dem 8-px-System + * aus den UI-Guidelines (internal ≤ external). + */ +export function PageContainer({ children }: { children: React.ReactNode }) { + return
{children}
; +} + +/** Standard-Raster für eine Kartenreihe. */ +export function Raster({ + spalten, + /** + * Karten oben ausrichten statt auf gleiche Hoehe strecken. + * Gleiche Hoehe ist richtig, wenn alle Karten dieselbe Struktur haben + * (KPI-Reihe). Sobald ein Chart mit fester Hoehe neben einer Tabelle steht, + * erzeugt Strecken nur Weissraum. + */ + obenBuendig = false, + children, +}: { + /** Spaltenzahl auf großen Bildschirmen. */ + spalten: 2 | 3 | 4 | 5; + obenBuendig?: boolean; + children: React.ReactNode; +}) { + const klassen: Record = { + 2: "grid grid-cols-1 gap-4 md:grid-cols-2", + 3: "grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3", + 4: "grid grid-cols-2 gap-4 lg:grid-cols-4", + 5: "grid grid-cols-2 gap-4 lg:grid-cols-3 xl:grid-cols-5", + }; + return ( +
{children}
+ ); +} diff --git a/features/dashboard/components/layout/PageHeader.tsx b/features/dashboard/components/layout/PageHeader.tsx new file mode 100644 index 0000000..74eba73 --- /dev/null +++ b/features/dashboard/components/layout/PageHeader.tsx @@ -0,0 +1,58 @@ +"use client"; + +import React from "react"; +import { FeatherDownload, FeatherRefreshCw, FeatherShare2 } from "@subframe/core"; +import { useToast } from "../../context/ToastContext"; + +/** + * Kopf einer Analyse-Seite: Titel, Einordnung, Seitenaktionen. + * Die Aktionen sind im Prototyp bewusst folgenlos (Toast statt Export) — + * siehe HANDOVER.md, Abschnitt "Was noch Logik braucht". + */ +export function PageHeader({ + titel, + beschreibung, + aktionen, +}: { + titel: string; + beschreibung: string; + aktionen?: React.ReactNode; +}) { + const { zeigeToast } = useToast(); + + return ( +
+
+

+ {titel} +

+

{beschreibung}

+
+ +
+ {aktionen} + + + +
+
+ ); +} diff --git a/features/dashboard/components/layout/navigation.ts b/features/dashboard/components/layout/navigation.ts new file mode 100644 index 0000000..be2c40e --- /dev/null +++ b/features/dashboard/components/layout/navigation.ts @@ -0,0 +1,75 @@ +import type { SeitenId } from "../../data/filtering"; + +export interface NavEintrag { + id: SeitenId | "komponenten"; + href: string; + label: string; + /** Icon-Name aus @subframe/core (Feather-Set). */ + icon: string; + beschreibung: string; +} + +/** + * Navigationsstruktur. + * + * Vier Analyse-Seiten kommen direkt aus REQUIREMENTS-analytics.md + * (Übersicht, Finanzen, Aufträge, Kunden). "Team" war dort als v2 markiert, + * ist hier aber enthalten, weil Auslastung im Handwerk die Kennzahl ist, + * nach der Betriebe steuern. "Komponenten" ist reine Entwickler-Doku. + */ +export const NAV_GRUPPEN: { titel: string; eintraege: NavEintrag[] }[] = [ + { + titel: "Analyse", + eintraege: [ + { + id: "uebersicht", + href: "/", + label: "Übersicht", + icon: "LayoutDashboard", + beschreibung: "Alle Kernkennzahlen auf einen Blick", + }, + { + id: "finanzen", + href: "/finanzen", + label: "Finanzen", + icon: "Euro", + beschreibung: "Umsatz, Rechnungen, Zahlungseingang", + }, + { + id: "auftraege", + href: "/auftraege", + label: "Aufträge", + icon: "ClipboardList", + beschreibung: "Pipeline, Status, Termine", + }, + { + id: "kunden", + href: "/kunden", + label: "Kunden", + icon: "Users", + beschreibung: "Akquise, Quellen, Bestand", + }, + { + id: "team", + href: "/team", + label: "Team", + icon: "HardHat", + beschreibung: "Auslastung und Produktivität der Monteure", + }, + ], + }, + { + titel: "Entwicklung", + eintraege: [ + { + id: "komponenten", + href: "/komponenten", + label: "Komponenten", + icon: "Component", + beschreibung: "Bausteine, Regeln und Übergabehinweise", + }, + ], + }, +]; + +export const ALLE_SEITEN = NAV_GRUPPEN.flatMap((g) => g.eintraege); diff --git a/features/dashboard/components/tables/DataTable.tsx b/features/dashboard/components/tables/DataTable.tsx new file mode 100644 index 0000000..d836c5c --- /dev/null +++ b/features/dashboard/components/tables/DataTable.tsx @@ -0,0 +1,215 @@ +"use client"; + +import React from "react"; +import { + FeatherArrowDown, + FeatherArrowUp, + FeatherChevronLeft, + FeatherChevronRight, + FeatherSearch, +} from "@subframe/core"; + +export interface Spalte { + key: string; + titel: string; + /** Zellinhalt; darf beliebige Knoten liefern (Badges, Links). */ + zelle: (zeile: T) => React.ReactNode; + /** Sortierwert — ohne diesen ist die Spalte nicht sortierbar. */ + sortWert?: (zeile: T) => number | string; + /** Zahlen gehören rechtsbündig und in Tabellenziffern. */ + numerisch?: boolean; + breite?: string; +} + +/** + * Tabelle für Arbeitslisten (offene Rechnungen, Aufträge, Kunden). + * + * Enthält die Interaktionen, die man in einer Demo wirklich braucht: + * Sortieren per Klick auf den Spaltenkopf, Volltextsuche, Blättern. + * Alles clientseitig — bei echten Datenmengen gehört das auf den Server. + */ +export function DataTable({ + zeilen, + spalten, + proSeite = 8, + suchbar = true, + suchFelder, + leerText = "Keine Einträge", +}: { + zeilen: T[]; + spalten: Spalte[]; + proSeite?: number; + suchbar?: boolean; + /** Felder, die die Suche durchsucht. */ + suchFelder?: (zeile: T) => string; + leerText?: string; +}) { + const [suche, setSuche] = React.useState(""); + const [seite, setSeite] = React.useState(0); + const [sortierung, setSortierung] = React.useState<{ key: string; richtung: "asc" | "desc" } | null>( + null + ); + + const gefiltert = React.useMemo(() => { + if (!suche.trim() || !suchFelder) return zeilen; + const begriff = suche.toLowerCase(); + return zeilen.filter((zeile) => suchFelder(zeile).toLowerCase().includes(begriff)); + }, [zeilen, suche, suchFelder]); + + const sortiert = React.useMemo(() => { + if (!sortierung) return gefiltert; + const spalte = spalten.find((s) => s.key === sortierung.key); + if (!spalte?.sortWert) return gefiltert; + + return [...gefiltert].sort((a, b) => { + const wertA = spalte.sortWert!(a); + const wertB = spalte.sortWert!(b); + const vergleich = + typeof wertA === "number" && typeof wertB === "number" + ? wertA - wertB + : String(wertA).localeCompare(String(wertB), "de"); + return sortierung.richtung === "asc" ? vergleich : -vergleich; + }); + }, [gefiltert, sortierung, spalten]); + + const seiten = Math.max(1, Math.ceil(sortiert.length / proSeite)); + const aktuelleSeite = Math.min(seite, seiten - 1); + const sichtbar = sortiert.slice(aktuelleSeite * proSeite, (aktuelleSeite + 1) * proSeite); + + const sortiereNach = (key: string) => { + setSeite(0); + setSortierung((aktuell) => + aktuell?.key === key + ? { key, richtung: aktuell.richtung === "asc" ? "desc" : "asc" } + : { key, richtung: "desc" } + ); + }; + + return ( +
+ {suchbar && suchFelder ? ( +
+ + { + setSuche(event.target.value); + setSeite(0); + }} + placeholder="In Liste suchen …" + className="w-full bg-transparent text-[13px] outline-none placeholder:text-text-neutral-subtle" + /> +
+ ) : null} + +
+ + + + {spalten.map((spalte) => { + const aktiv = sortierung?.key === spalte.key; + return ( + + ); + })} + + + + {sichtbar.map((zeile) => ( + + {spalten.map((spalte) => ( + + ))} + + ))} + {!sichtbar.length ? ( + + + + ) : null} + +
+ {spalte.sortWert ? ( + + ) : ( + spalte.titel + )} +
+ {spalte.zelle(zeile)} +
+ {leerText} +
+
+ + {sortiert.length > proSeite ? ( +
+ + {aktuelleSeite * proSeite + 1}–{Math.min((aktuelleSeite + 1) * proSeite, sortiert.length)} von{" "} + {sortiert.length} + +
+ + + {aktuelleSeite + 1} / {seiten} + + +
+
+ ) : null} +
+ ); +} diff --git a/features/dashboard/components/tables/StatusBadge.tsx b/features/dashboard/components/tables/StatusBadge.tsx new file mode 100644 index 0000000..deeb251 --- /dev/null +++ b/features/dashboard/components/tables/StatusBadge.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import type { AuftragStatus, RechnungStatus } from "../../data/types"; +import { AUFTRAG_STATUS_LABEL, RECHNUNG_STATUS_LABEL } from "../../data/types"; +import { BADGE_TON } from "../../utils/chartTheme"; + +/** Farbzuordnung: Endzustände neutral, Problemzustände rot, Laufendes blau. */ +const AUFTRAG_TON: Record = { + anfrage: BADGE_TON.neutral, + aufmass: BADGE_TON.information, + angebot: BADGE_TON.lila, + beauftragt: BADGE_TON.teal, + in_arbeit: BADGE_TON.positiv, + abgeschlossen: BADGE_TON.neutral, + verloren: BADGE_TON.negativ, +}; + +const RECHNUNG_TON: Record = { + entwurf: BADGE_TON.neutral, + gestellt: BADGE_TON.information, + bezahlt: BADGE_TON.positiv, + ueberfaellig: BADGE_TON.negativ, +}; + +export function AuftragBadge({ status }: { status: AuftragStatus }) { + return ( + + {AUFTRAG_STATUS_LABEL[status]} + + ); +} + +export function RechnungBadge({ status }: { status: RechnungStatus }) { + return ( + + {RECHNUNG_STATUS_LABEL[status]} + + ); +} diff --git a/features/dashboard/context/DashboardProviders.tsx b/features/dashboard/context/DashboardProviders.tsx new file mode 100644 index 0000000..f10ba7b --- /dev/null +++ b/features/dashboard/context/DashboardProviders.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; +import { FilterProvider } from "./FilterContext"; +import { WidgetProvider } from "./WidgetContext"; +import { ToastProvider } from "./ToastContext"; + +/** Sammelt alle Client-Provider, damit das Root-Layout schlank bleibt. */ +export function DashboardProviders({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/features/dashboard/context/FilterContext.tsx b/features/dashboard/context/FilterContext.tsx new file mode 100644 index 0000000..5fa2339 --- /dev/null +++ b/features/dashboard/context/FilterContext.tsx @@ -0,0 +1,70 @@ +"use client"; + +import React from "react"; +import { LEERER_FILTER, type FilterState } from "../data/types"; +import { erstellePersistentenStore } from "./persistenterStore"; + +/** + * Globaler Filter-State über alle Analytics-Seiten hinweg (INFRA-01), + * persistiert in localStorage (INFRA-02) — ein Reload verwirft die Auswahl + * nicht, im Kundentermin ein spürbarer Unterschied. + */ + +export type MehrfachFeld = + | "gewerke" + | "auftragStatus" + | "kundenQuellen" + | "regionen" + | "mitarbeiterIds"; + +const store = erstellePersistentenStore( + "hivegrid.analytics.filter.v1", + LEERER_FILTER +); + +interface FilterContextValue { + filter: FilterState; + setFilter: (naechster: FilterState | ((vorher: FilterState) => FilterState)) => void; + /** Einzelnen Mehrfachwert an- oder abwählen. */ + toggleWert: (feld: MehrfachFeld, wert: string) => void; + /** Ein Feld komplett leeren. */ + leereFeld: (feld: MehrfachFeld) => void; + zuruecksetzen: () => void; +} + +const FilterContext = React.createContext(null); + +export function FilterProvider({ children }: { children: React.ReactNode }) { + const filter = React.useSyncExternalStore( + store.subscribe, + store.getSnapshot, + store.getServerSnapshot + ); + + const wert = React.useMemo( + () => ({ + filter, + setFilter: store.setzen, + toggleWert: (feld, eintrag) => + store.setzen((aktuell) => { + const liste = aktuell[feld] as string[]; + const neu = liste.includes(eintrag) + ? liste.filter((w) => w !== eintrag) + : [...liste, eintrag]; + return { ...aktuell, [feld]: neu } as FilterState; + }), + leereFeld: (feld) => + store.setzen((aktuell) => ({ ...aktuell, [feld]: [] }) as FilterState), + zuruecksetzen: () => store.setzen(LEERER_FILTER), + }), + [filter] + ); + + return {children}; +} + +export function useFilter(): FilterContextValue { + const ctx = React.useContext(FilterContext); + if (!ctx) throw new Error("useFilter muss innerhalb von verwendet werden"); + return ctx; +} diff --git a/features/dashboard/context/ToastContext.tsx b/features/dashboard/context/ToastContext.tsx new file mode 100644 index 0000000..f19a5a0 --- /dev/null +++ b/features/dashboard/context/ToastContext.tsx @@ -0,0 +1,65 @@ +"use client"; + +import React from "react"; +import { FeatherCheck, FeatherInfo } from "@subframe/core"; + +/** + * Minimale Rückmeldung für Demo-Aktionen (Export, Speichern, Teilen). + * + * Im Prototyp passiert dahinter nichts — der Toast macht aber sichtbar, + * dass der Klick registriert wurde, statt dass der Button "tot" wirkt. + * In der echten Software durch die vorhandene Toast-Komponente ersetzen. + */ + +interface ToastContextValue { + zeigeToast: (text: string, detail?: string) => void; +} + +const ToastContext = React.createContext(null); + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toast, setToast] = React.useState<{ text: string; detail?: string } | null>(null); + const timer = React.useRef | null>(null); + + const zeigeToast = React.useCallback((text: string, detail?: string) => { + setToast({ text, detail }); + if (timer.current) clearTimeout(timer.current); + timer.current = setTimeout(() => setToast(null), 2600); + }, []); + + React.useEffect(() => () => { + if (timer.current) clearTimeout(timer.current); + }, []); + + return ( + + {children} + {toast ? ( +
+ + + +
+ {toast.text} + {toast.detail ? ( + + + {toast.detail} + + ) : null} +
+
+ ) : null} +
+ ); +} + +export function useToast(): ToastContextValue { + const ctx = React.useContext(ToastContext); + if (!ctx) throw new Error("useToast muss innerhalb von verwendet werden"); + return ctx; +} diff --git a/features/dashboard/context/WidgetContext.tsx b/features/dashboard/context/WidgetContext.tsx new file mode 100644 index 0000000..e042cb9 --- /dev/null +++ b/features/dashboard/context/WidgetContext.tsx @@ -0,0 +1,69 @@ +"use client"; + +import React from "react"; +import { erstellePersistentenStore } from "./persistenterStore"; + +/** + * Sichtbarkeit einzelner Widgets. + * + * Jede Karte hat eine stabile ID; über das ⋯-Menü lässt sie sich ausblenden. + * Ausgeblendete Karten sammelt die Filterleiste und macht sie einzeln oder + * gesammelt wieder sichtbar. Der Zustand überlebt einen Reload. + */ + +interface WidgetState { + versteckt: string[]; + /** Titel für die "wieder einblenden"-Liste. */ + titel: Record; +} + +const STANDARD: WidgetState = { versteckt: [], titel: {} }; + +const store = erstellePersistentenStore("hivegrid.analytics.widgets.v1", STANDARD); + +interface WidgetContextValue extends WidgetState { + istSichtbar: (id: string) => boolean; + verstecke: (id: string, titel: string) => void; + zeige: (id: string) => void; + zeigeAlle: () => void; +} + +const WidgetContext = React.createContext(null); + +export function WidgetProvider({ children }: { children: React.ReactNode }) { + const zustand = React.useSyncExternalStore( + store.subscribe, + store.getSnapshot, + store.getServerSnapshot + ); + + const wert = React.useMemo( + () => ({ + versteckt: zustand.versteckt, + titel: zustand.titel, + istSichtbar: (id) => !zustand.versteckt.includes(id), + verstecke: (id, kartenTitel) => + store.setzen((aktuell) => ({ + versteckt: aktuell.versteckt.includes(id) + ? aktuell.versteckt + : [...aktuell.versteckt, id], + titel: { ...aktuell.titel, [id]: kartenTitel }, + })), + zeige: (id) => + store.setzen((aktuell) => ({ + ...aktuell, + versteckt: aktuell.versteckt.filter((eintrag) => eintrag !== id), + })), + zeigeAlle: () => store.setzen((aktuell) => ({ ...aktuell, versteckt: [] })), + }), + [zustand] + ); + + return {children}; +} + +export function useWidgets(): WidgetContextValue { + const ctx = React.useContext(WidgetContext); + if (!ctx) throw new Error("useWidgets muss innerhalb von verwendet werden"); + return ctx; +} diff --git a/features/dashboard/context/persistenterStore.ts b/features/dashboard/context/persistenterStore.ts new file mode 100644 index 0000000..c7c1ed8 --- /dev/null +++ b/features/dashboard/context/persistenterStore.ts @@ -0,0 +1,60 @@ +/** + * Kleiner Store für Werte, die einen Reload überleben sollen. + * + * Warum kein `useState` + `useEffect`: Beim Serverrendering gibt es keinen + * localStorage. Lädt man den gespeicherten Wert nachträglich im Effekt, löst + * das eine zweite Renderrunde aus (und React beanstandet das zu Recht). + * `useSyncExternalStore` ist genau für diesen Fall gebaut: Es rendert + * serverseitig den Standardwert und wechselt nach der Hydration auf den + * gespeicherten — ohne Mismatch und ohne Zusatzrender. + */ + +export interface PersistenterStore { + subscribe: (hoerer: () => void) => () => void; + getSnapshot: () => T; + getServerSnapshot: () => T; + setzen: (naechster: T | ((vorher: T) => T)) => void; +} + +export function erstellePersistentenStore( + schluessel: string, + standard: T, + /** Gelesene Daten mit dem Standard verschmelzen — fängt alte Formate ab. */ + zusammenfuehren: (gespeichert: unknown, standard: T) => T = (g, s) => + ({ ...s, ...(g as object) }) as T +): PersistenterStore { + let wert = standard; + let gelesen = false; + const hoerer = new Set<() => void>(); + + function lade(): T { + if (gelesen || typeof window === "undefined") return wert; + gelesen = true; + try { + const roh = window.localStorage.getItem(schluessel); + if (roh) wert = zusammenfuehren(JSON.parse(roh), standard); + } catch { + // Defektes JSON oder gesperrter Storage: beim Standard bleiben. + } + return wert; + } + + return { + subscribe(neuerHoerer) { + hoerer.add(neuerHoerer); + return () => hoerer.delete(neuerHoerer); + }, + getSnapshot: () => lade(), + getServerSnapshot: () => standard, + setzen(naechster) { + lade(); + wert = typeof naechster === "function" ? (naechster as (v: T) => T)(wert) : naechster; + try { + window.localStorage.setItem(schluessel, JSON.stringify(wert)); + } catch { + // Privater Modus o. Ä. — Persistenz ist optional. + } + hoerer.forEach((h) => h()); + }, + }; +} diff --git a/features/dashboard/data/calculations.ts b/features/dashboard/data/calculations.ts new file mode 100644 index 0000000..3861273 --- /dev/null +++ b/features/dashboard/data/calculations.ts @@ -0,0 +1,568 @@ +/** + * Kennzahlen-Berechnungen. + * + * Alle Funktionen sind pure functions über dem gefilterten Datensatz + * (Anforderung INFRA-06) — die Entwicklung kann sie unverändert übernehmen + * und lediglich die Datenquelle austauschen. + * + * Die Definition jeder Kennzahl steht in docs/KPI-KATALOG.md. + */ + +import { + AUFTRAG_STATUS_LABEL, + FUNNEL_STUFEN, + RECHNUNG_STATUS_LABEL, + type Auftrag, + type AuftragStatus, + type ChartPunkt, + type Datensatz, + type Kunde, + type Rechnung, + FOERDERFAEHIGE_KATEGORIEN, + type ZeitraumPreset, + type ZeitreihePunkt, + type Zeiteintrag, + type Mitarbeiter, +} from "./types"; +import { + HEUTE, + bucketKey, + bucketsFuer, + formatWoche, + imFenster, + vorperiode, + zeitfenster, + addMonate, +} from "../utils/dateUtils"; +import { formatDatumKurz, formatMonatKurz, tageSeit } from "../utils/formatters"; + +/* ------------------------------------------------------------- Umsatz */ + +/** Umsatz = Summe bezahlter Ausgangsrechnungen im Zeitraum (Zahlungseingang). */ +export function umsatz(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number { + const fenster = zeitfenster(zeitraum); + return rechnungen + .filter((r) => r.status === "bezahlt" && imFenster(r.bezahltAm, fenster)) + .reduce((summe, r) => summe + r.betrag, 0); +} + +/** Gleiche Berechnung für die Vorperiode — Basis der Trend-Badges. */ +export function umsatzVorperiode(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number { + const fenster = vorperiode(zeitraum); + return rechnungen + .filter((r) => r.status === "bezahlt" && imFenster(r.bezahltAm, fenster)) + .reduce((summe, r) => summe + r.betrag, 0); +} + +/** Beschriftung eines Buckets — eine Stelle fuer alle drei Zeitreihen. */ +function bucketLabel(key: string, art: "tag" | "woche" | "monat"): string { + if (art === "monat") return formatMonatKurz(key); + if (art === "woche") return formatWoche(key); + return formatDatumKurz(key); +} + +/** + * Umsatzverlauf mit Vorjahresvergleich. + * Buckets richten sich nach dem Zeitraum (Tage bei ≤ 30 Tagen, sonst Monate). + */ +export function umsatzVerlauf( + rechnungen: Rechnung[], + zeitraum: ZeitraumPreset +): ZeitreihePunkt[] { + const { art, schluessel } = bucketsFuer(zeitraum); + + const aktuell = new Map(); + const vorjahr = new Map(); + + for (const r of rechnungen) { + if (r.status !== "bezahlt" || !r.bezahltAm) continue; + aktuell.set( + bucketKey(r.bezahltAm, art), + (aktuell.get(bucketKey(r.bezahltAm, art)) ?? 0) + r.betrag + ); + + // Vorjahreswert: dasselbe Datum plus 12 Monate landet im aktuellen Bucket. + const verschoben = addMonate(new Date(r.bezahltAm), 12); + const key = bucketKey(verschoben.toISOString().slice(0, 10), art); + vorjahr.set(key, (vorjahr.get(key) ?? 0) + r.betrag); + } + + return schluessel.map((key) => ({ + datum: key, + label: bucketLabel(key, art), + value: aktuell.get(key) ?? 0, + vergleich: vorjahr.get(key) ?? 0, + })); +} + +/** Deckungsbeitrag: gewichtete Marge über abgeschlossene Aufträge im Zeitraum. */ +export function marge(auftraege: Auftrag[], zeitraum: ZeitraumPreset): number { + const fenster = zeitfenster(zeitraum); + const relevant = auftraege.filter( + (a) => a.status === "abgeschlossen" && imFenster(a.abgeschlossenAm, fenster) + ); + const gesamtwert = relevant.reduce((s, a) => s + a.wert, 0); + if (gesamtwert === 0) return 0; + const db = relevant.reduce((s, a) => s + (a.wert * a.margeProzent) / 100, 0); + return (db / gesamtwert) * 100; +} + +/* --------------------------------------------------------- Rechnungen */ + +export interface OffenePosten { + gesamt: number; + anzahl: number; + ueberfaelligBetrag: number; + ueberfaelligAnzahl: number; + nachAlter: ChartPunkt[]; + liste: Rechnung[]; +} + +/** + * Offene Posten sind stichtagsbezogen (nicht zeitraumgefiltert): + * Was ist heute noch nicht bezahlt? + */ +export function offenePosten(rechnungen: Rechnung[]): OffenePosten { + const offen = rechnungen.filter((r) => r.status === "gestellt" || r.status === "ueberfaellig"); + + const buckets: Record = { + "0–30 Tage": { betrag: 0, anzahl: 0 }, + "31–60 Tage": { betrag: 0, anzahl: 0 }, + "61–90 Tage": { betrag: 0, anzahl: 0 }, + "> 90 Tage": { betrag: 0, anzahl: 0 }, + }; + + for (const r of offen) { + const tage = tageSeit(r.faelligAm, HEUTE); + const bucket = + tage <= 30 ? "0–30 Tage" : tage <= 60 ? "31–60 Tage" : tage <= 90 ? "61–90 Tage" : "> 90 Tage"; + buckets[bucket].betrag += r.betrag; + buckets[bucket].anzahl += 1; + } + + const ueberfaellig = offen.filter((r) => r.status === "ueberfaellig"); + + return { + gesamt: offen.reduce((s, r) => s + r.betrag, 0), + anzahl: offen.length, + ueberfaelligBetrag: ueberfaellig.reduce((s, r) => s + r.betrag, 0), + ueberfaelligAnzahl: ueberfaellig.length, + nachAlter: Object.entries(buckets).map(([name, v]) => ({ name, value: v.betrag })), + liste: offen.sort((a, b) => (a.faelligAm < b.faelligAm ? -1 : 1)), + }; +} + +/** Durchschnittliche Zahlungsdauer in Tagen (gestellt → bezahlt). */ +export function zahlungsdauer(rechnungen: Rechnung[], zeitraum: ZeitraumPreset): number { + const fenster = zeitfenster(zeitraum); + const bezahlt = rechnungen.filter( + (r) => r.status === "bezahlt" && r.bezahltAm && imFenster(r.bezahltAm, fenster) + ); + if (!bezahlt.length) return 0; + const summe = bezahlt.reduce( + (s, r) => + s + (new Date(r.bezahltAm!).getTime() - new Date(r.gestelltAm).getTime()) / 86_400_000, + 0 + ); + return summe / bezahlt.length; +} + +/** + * Zusammensetzung des OFFENEN Bestands. + * + * Nicht zu verwechseln mit rechnungenNachStatus(): dort steht der kumulierte + * Umsatz aus zwei Jahren gegen den heutigen Forderungsbestand — dabei entfallen + * über 90 % auf "Bezahlt", und der Ring zeigt nichts mehr. Der offene Bestand + * ist ein echtes Ganzes: alles, was heute noch aussteht. + */ +export function offeneNachStatus(rechnungen: Rechnung[]): ChartPunkt[] { + // Entwuerfe sind noch nicht gestellt und damit keine Forderung. Wuerden sie + // mitzaehlen, waeche die Ringsumme von der KPI "Offene Forderungen" ab. + const offen = rechnungen.filter( + (r) => r.richtung === "ausgang" && (r.status === "gestellt" || r.status === "ueberfaellig") + ); + const summen = new Map(); + for (const r of offen) { + const label = RECHNUNG_STATUS_LABEL[r.status]; + summen.set(label, (summen.get(label) ?? 0) + r.betrag); + } + return [...summen.entries()].map(([name, value]) => ({ name, value })); +} + +export function rechnungenNachStatus(rechnungen: Rechnung[]): ChartPunkt[] { + const zaehler = new Map(); + for (const r of rechnungen) { + const label = RECHNUNG_STATUS_LABEL[r.status]; + zaehler.set(label, (zaehler.get(label) ?? 0) + r.betrag); + } + return [...zaehler.entries()].map(([name, value]) => ({ name, value })); +} + +/* ------------------------------------------------------------ Aufträge */ + +export function auftraegeImZeitraum(auftraege: Auftrag[], zeitraum: ZeitraumPreset): Auftrag[] { + const fenster = zeitfenster(zeitraum); + return auftraege.filter((a) => imFenster(a.angelegtAm, fenster)); +} + +/** "Aktiv" = beauftragt oder in Arbeit — das, was das Team gerade bindet. */ +export function aktiveAuftraege(auftraege: Auftrag[]): Auftrag[] { + return auftraege.filter((a) => a.status === "beauftragt" || a.status === "in_arbeit"); +} + +export function ueberfaelligeAuftraege(auftraege: Auftrag[]): Auftrag[] { + // Einen Termin gibt es erst ab der Beauftragung — eine offene Anfrage kann + // nicht "über Termin" sein. + return auftraege + .filter( + (a) => + (a.status === "beauftragt" || a.status === "in_arbeit") && + new Date(a.faelligAm) < HEUTE + ) + .sort((a, b) => (a.faelligAm < b.faelligAm ? -1 : 1)); +} + +export function auftraegeNachStatus(auftraege: Auftrag[]): ChartPunkt[] { + const zaehler = new Map(); + for (const a of auftraege) zaehler.set(a.status, (zaehler.get(a.status) ?? 0) + 1); + return (Object.keys(AUFTRAG_STATUS_LABEL) as AuftragStatus[]) + .map((status) => ({ name: AUFTRAG_STATUS_LABEL[status], value: zaehler.get(status) ?? 0 })) + .filter((p) => p.value > 0); +} + +export function auftraegeNachGewerk(auftraege: Auftrag[]): ChartPunkt[] { + const zaehler = new Map(); + for (const a of auftraege) zaehler.set(a.gewerk, (zaehler.get(a.gewerk) ?? 0) + 1); + return [...zaehler.entries()] + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} + +export function umsatzNachGewerk( + auftraege: Auftrag[], + rechnungen: Rechnung[], + zeitraum: ZeitraumPreset +): ChartPunkt[] { + const fenster = zeitfenster(zeitraum); + const gewerkProAuftrag = new Map(auftraege.map((a) => [a.id, a.gewerk])); + const summen = new Map(); + + for (const r of rechnungen) { + if (r.status !== "bezahlt" || !imFenster(r.bezahltAm, fenster)) continue; + const gewerk = gewerkProAuftrag.get(r.auftragId); + if (!gewerk) continue; + summen.set(gewerk, (summen.get(gewerk) ?? 0) + r.betrag); + } + + return [...summen.entries()] + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} + +/** + * Vertriebstrichter: Wie viele Vorgänge haben eine Stufe mindestens erreicht? + * Verlorene Aufträge zählen nur bis zu ihrer letzten erreichten Stufe. + */ +/** + * Vertriebstrichter — bewusst nur über das Angebotsgeschäft. + * + * Wartung, Notdienst und E-Check laufen ohne Angebot direkt in die Ausführung. + * Zählte man sie mit, würde der Trichter unten breiter als oben und die + * Übergangsquoten wären wertlos. + */ +export function vertriebsTrichter(auftraege: Auftrag[]): ChartPunkt[] { + const rang = (status: AuftragStatus) => FUNNEL_STUFEN.indexOf(status); + const angebotsgeschaeft = auftraege.filter((a) => a.ueberAngebot); + + return FUNNEL_STUFEN.map((stufe, index) => { + const anzahl = angebotsgeschaeft.filter((a) => { + // Verlorene Vorgänge zählen bis zu der Stufe, auf der sie abgesprungen sind. + if (a.status === "verloren") { + return a.verlorenAb ? rang(a.verlorenAb) >= index : false; + } + return rang(a.status) >= index; + }).length; + return { name: AUFTRAG_STATUS_LABEL[stufe], value: anzahl }; + }); +} + +export function abschlussquote(auftraege: Auftrag[]): number { + // Bezugsgröße ist ausschließlich das Angebotsgeschäft. Wartungs- und + // Notdienstaufträge laufen ohne Angebot und würden die Quote schönrechnen. + const mitAngebot = auftraege.filter( + (a) => + a.ueberAngebot && + (["angebot", "beauftragt", "in_arbeit", "abgeschlossen"].includes(a.status) || + (a.status === "verloren" && a.verlorenAb === "angebot")) + ); + if (!mitAngebot.length) return 0; + const gewonnen = mitAngebot.filter((a) => + ["beauftragt", "in_arbeit", "abgeschlossen"].includes(a.status) + ).length; + return (gewonnen / mitAngebot.length) * 100; +} + +/** Ø Durchlaufzeit abgeschlossener Aufträge in Tagen. */ +export function durchlaufzeit(auftraege: Auftrag[]): number { + const fertig = auftraege.filter((a) => a.status === "abgeschlossen" && a.abgeschlossenAm); + if (!fertig.length) return 0; + const summe = fertig.reduce( + (s, a) => + s + (new Date(a.abgeschlossenAm!).getTime() - new Date(a.angelegtAm).getTime()) / 86_400_000, + 0 + ); + return summe / fertig.length; +} + +export function auftragseingang(auftraege: Auftrag[], zeitraum: ZeitraumPreset): ZeitreihePunkt[] { + const { art, schluessel } = bucketsFuer(zeitraum); + const zaehler = new Map(); + for (const a of auftraege) { + const key = bucketKey(a.angelegtAm, art); + zaehler.set(key, (zaehler.get(key) ?? 0) + 1); + } + return schluessel.map((key) => ({ + datum: key, + label: bucketLabel(key, art), + value: zaehler.get(key) ?? 0, + })); +} + +/* -------------------------------------------------------------- Kunden */ + +export function neueKunden(kunden: Kunde[], zeitraum: ZeitraumPreset): number { + const fenster = zeitfenster(zeitraum); + return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length; +} + +/** Gleiches Fenster davor — Basis fuer den Trend an der Neukunden-Karte. */ +export function neueKundenVorperiode(kunden: Kunde[], zeitraum: ZeitraumPreset): number { + const fenster = vorperiode(zeitraum); + return kunden.filter((k) => imFenster(k.angelegtAm, fenster)).length; +} + +export function kundenEntwicklung(kunden: Kunde[], zeitraum: ZeitraumPreset): ZeitreihePunkt[] { + const { art, schluessel } = bucketsFuer(zeitraum); + const zaehler = new Map(); + for (const k of kunden) { + const key = bucketKey(k.angelegtAm, art); + zaehler.set(key, (zaehler.get(key) ?? 0) + 1); + } + return schluessel.map((key) => ({ + datum: key, + label: bucketLabel(key, art), + value: zaehler.get(key) ?? 0, + })); +} + +export function kundenNach(kunden: Kunde[], feld: "quelle" | "typ" | "region"): ChartPunkt[] { + const zaehler = new Map(); + for (const k of kunden) zaehler.set(k[feld], (zaehler.get(k[feld]) ?? 0) + 1); + return [...zaehler.entries()] + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); +} + +export function topKunden(kunden: Kunde[], anzahl = 5): ChartPunkt[] { + return [...kunden] + .filter((k) => k.umsatzGesamt > 0) + .sort((a, b) => b.umsatzGesamt - a.umsatzGesamt) + .slice(0, anzahl) + .map((k) => ({ name: k.name, value: k.umsatzGesamt })); +} + +/** Anteil Kunden mit mehr als einem Auftrag. */ +export function wiederkehrquote(kunden: Kunde[]): number { + const mitAuftrag = kunden.filter((k) => k.auftraegeGesamt > 0); + if (!mitAuftrag.length) return 0; + return (mitAuftrag.filter((k) => k.auftraegeGesamt > 1).length / mitAuftrag.length) * 100; +} + +/** Kunden, die seit über 12 Monaten keinen Auftrag hatten. */ +export function inaktiveKunden(kunden: Kunde[]): Kunde[] { + const grenze = addMonate(HEUTE, -12); + return kunden + .filter((k) => k.auftraegeGesamt > 0 && k.letzterAuftragAm && new Date(k.letzterAuftragAm) < grenze) + .sort((a, b) => b.umsatzGesamt - a.umsatzGesamt); +} + +/** Kunden mit auffälliger Zahlungsmoral (> 30 Tage im Schnitt). */ +export function zahlungsauffaellig(kunden: Kunde[]): Kunde[] { + return kunden + .filter((k) => k.zahlungsmoralTage > 30 && k.auftraegeGesamt > 0) + .sort((a, b) => b.zahlungsmoralTage - a.zahlungsmoralTage); +} + +/* ---------------------------------------------------------------- Team */ + +export interface MitarbeiterKennzahl { + mitarbeiter: Mitarbeiter; + stunden: number; + produktivStunden: number; + auslastungProzent: number; + abgeschlossen: number; + umsatz: number; +} + +/** + * Auslastung = erfasste Arbeitsstunden / verfügbare Stunden im Zeitraum. + * Verfügbare Stunden werden aus den Wochenstunden hochgerechnet. + */ +export function teamKennzahlen( + mitarbeiter: Mitarbeiter[], + zeiten: Zeiteintrag[], + auftraege: Auftrag[], + zeitraum: ZeitraumPreset +): MitarbeiterKennzahl[] { + const fenster = zeitfenster(zeitraum); + const tageImFenster = Math.max( + 1, + Math.round((fenster.bis.getTime() - fenster.von.getTime()) / 86_400_000) + ); + const wochen = Math.max(1, tageImFenster / 7); + + return mitarbeiter + .filter((m) => m.rolle !== "Büro") + .map((m) => { + const eigene = zeiten.filter((z) => z.mitarbeiterId === m.id && imFenster(z.datum, fenster)); + const stunden = eigene.reduce((s, z) => s + z.stunden, 0); + const produktiv = eigene + .filter((z) => z.art === "arbeit" && z.auftragId) + .reduce((s, z) => s + z.stunden, 0); + const verfuegbar = m.wochenstunden * wochen; + + const eigeneAuftraege = auftraege.filter((a) => a.mitarbeiterId === m.id); + const abgeschlossen = eigeneAuftraege.filter( + (a) => a.status === "abgeschlossen" && imFenster(a.abgeschlossenAm, fenster) + ); + + return { + mitarbeiter: m, + stunden, + produktivStunden: produktiv, + auslastungProzent: verfuegbar > 0 ? Math.min(135, (produktiv / verfuegbar) * 100) : 0, + abgeschlossen: abgeschlossen.length, + umsatz: abgeschlossen.reduce((s, a) => s + a.wert, 0), + }; + }) + .sort((a, b) => b.auslastungProzent - a.auslastungProzent); +} + +/* -------------------------------------------------------------- Alerts */ + +export interface AlertEintrag { + id: string; + text: string; + detail: string; + art: "kritisch" | "warnung" | "hinweis"; + /** Betrag, um den es geht — macht die Zeilen untereinander vergleichbar. */ + betrag?: number; + /** + * Die Handlung, nicht die Beobachtung. Eine Liste ohne Verb ist ein Bericht; + * "Was ist zu tun" braucht ein Verb je Zeile. + */ + aktion: string; +} + +/** Handlungsbedarf für die Übersichtsseite. */ +export function handlungsbedarf(daten: Datensatz): AlertEintrag[] { + const eintraege: AlertEintrag[] = []; + const posten = offenePosten(daten.rechnungen); + const ueberfaellig = ueberfaelligeAuftraege(daten.auftraege); + const kritischAlt = posten.nachAlter.find((b) => b.name === "> 90 Tage"); + + if (posten.ueberfaelligAnzahl > 0) { + eintraege.push({ + id: "rechnungen-ueberfaellig", + text: `${posten.ueberfaelligAnzahl} überfällige Rechnungen`, + detail: "Zahlungsziel überschritten", + betrag: posten.ueberfaelligBetrag, + aktion: "Mahnlauf starten", + art: "kritisch", + }); + } + if (kritischAlt && kritischAlt.value > 0) { + eintraege.push({ + id: "rechnungen-90", + text: "Forderungen älter als 90 Tage", + detail: "Einbringungsquote sinkt deutlich", + betrag: kritischAlt.value, + aktion: "Inkasso prüfen", + art: "kritisch", + }); + } + if (ueberfaellig.length > 0) { + eintraege.push({ + id: "auftraege-ueberfaellig", + text: `${ueberfaellig.length} Aufträge über Termin`, + detail: `Ältester seit ${tageSeit(ueberfaellig[0].faelligAm, HEUTE)} Tagen fällig`, + betrag: ueberfaellig.reduce((s, a) => s + a.wert, 0), + aktion: "Termine neu planen", + art: "warnung", + }); + } + + const offeneAngebote = daten.auftraege.filter( + (a) => a.status === "angebot" && tageSeit(a.angelegtAm, HEUTE) > 21 + ); + if (offeneAngebote.length > 0) { + eintraege.push({ + id: "angebote-alt", + text: `${offeneAngebote.length} Angebote ohne Rückmeldung`, + detail: "Seit über 21 Tagen offen", + betrag: offeneAngebote.reduce((s, a) => s + a.wert, 0), + aktion: "Nachfassen", + art: "warnung", + }); + } + + const foerderOffen = daten.auftraege.filter((a) => a.foerderung === "beantragt"); + if (foerderOffen.length > 0) { + eintraege.push({ + id: "foerderung", + text: `${foerderOffen.length} Förderanträge in Bearbeitung`, + detail: "Bescheid ausstehend", + betrag: foerderOffen.reduce((s, a) => s + a.foerderbetrag, 0), + aktion: "Status prüfen", + art: "hinweis", + }); + } + + return eintraege; +} + +/* --------------------------------------------------------- Förderung */ + +/** + * Förderquote BAFA/KfW. + * + * Bezugsgröße sind ausschließlich förderfähige Leistungen (Wärmepumpe, PV, + * Speicher, Lüftung). Über alle Aufträge gerechnet läge die Quote im + * einstelligen Bereich und wäre wertlos — eine Badwartung war nie förderfähig. + */ +export function foerderQuote(auftraege: Auftrag[]): { + quote: number; + foerderfaehig: number; + bewilligt: number; + beantragt: number; + summe: number; + verteilung: ChartPunkt[]; +} { + const foerderfaehig = auftraege.filter((a) => FOERDERFAEHIGE_KATEGORIEN.has(a.kategorie)); + const bewilligt = foerderfaehig.filter((a) => a.foerderung === "bewilligt"); + const beantragt = foerderfaehig.filter((a) => a.foerderung === "beantragt"); + const mitFoerderung = bewilligt.length + beantragt.length; + + return { + quote: foerderfaehig.length ? (mitFoerderung / foerderfaehig.length) * 100 : 0, + foerderfaehig: foerderfaehig.length, + bewilligt: bewilligt.length, + beantragt: beantragt.length, + summe: bewilligt.reduce((s, a) => s + a.foerderbetrag, 0), + verteilung: [ + { name: "Bewilligt", value: bewilligt.length }, + { name: "Beantragt", value: beantragt.length }, + { name: "Nicht beantragt", value: foerderfaehig.length - mitFoerderung }, + ], + }; +} diff --git a/features/dashboard/data/filtering.ts b/features/dashboard/data/filtering.ts new file mode 100644 index 0000000..5d9c7ee --- /dev/null +++ b/features/dashboard/data/filtering.ts @@ -0,0 +1,95 @@ +/** + * Filterlogik. + * + * Zwei Dinge passieren hier: + * 1. `wendeFilterAn` schneidet den Datensatz auf die aktive Auswahl zu. + * 2. `relevanteFilter` steuert, welche Filter auf welcher Seite überhaupt + * angeboten werden (Page-aware Relevanz aus der Planung, FILT-05). + * Ein Auftragsstatus-Filter auf der Finanzseite verwirrt mehr, als er hilft. + */ + +import type { Datensatz, FilterState } from "./types"; +import { imFenster, zeitfenster } from "../utils/dateUtils"; + +export type SeitenId = "uebersicht" | "finanzen" | "auftraege" | "kunden" | "team"; + +export type FilterFeld = "zeitraum" | "gewerke" | "auftragStatus" | "kundenQuellen" | "regionen" | "mitarbeiterIds"; + +/** Welche Filter sind auf welcher Seite wirksam? */ +export const FILTER_RELEVANZ: Record = { + uebersicht: ["zeitraum", "gewerke", "regionen"], + finanzen: ["zeitraum", "gewerke", "kundenQuellen", "regionen"], + auftraege: ["zeitraum", "gewerke", "auftragStatus", "mitarbeiterIds", "regionen"], + kunden: ["zeitraum", "gewerke", "kundenQuellen", "regionen"], + team: ["zeitraum", "gewerke", "mitarbeiterIds"], +}; + +/** + * Setzt alle für die Seite irrelevanten Filter zurück, ohne sie im + * globalen State zu löschen — wechselt man die Seite, sind sie wieder da. + */ +export function wirksameFilter(filter: FilterState, seite: SeitenId): FilterState { + const erlaubt = new Set(FILTER_RELEVANZ[seite]); + return { + zeitraum: erlaubt.has("zeitraum") ? filter.zeitraum : "alles", + gewerke: erlaubt.has("gewerke") ? filter.gewerke : [], + auftragStatus: erlaubt.has("auftragStatus") ? filter.auftragStatus : [], + kundenQuellen: erlaubt.has("kundenQuellen") ? filter.kundenQuellen : [], + regionen: erlaubt.has("regionen") ? filter.regionen : [], + mitarbeiterIds: erlaubt.has("mitarbeiterIds") ? filter.mitarbeiterIds : [], + }; +} + +/** + * Wendet die Dimensionsfilter an — bewusst OHNE Zeitfilter. + * + * Grund: Kennzahlen brauchen unterschiedliche Zeitbezüge (Umsatz im Zeitraum, + * offene Rechnungen dagegen stichtagsbezogen). Der Zeitraum wird deshalb + * gezielt in den einzelnen Berechnungen angewendet, nicht pauschal vorab. + */ +export function wendeFilterAn(daten: Datensatz, filter: FilterState): Datensatz { + const kunden = daten.kunden.filter((k) => { + if (filter.kundenQuellen.length && !filter.kundenQuellen.includes(k.quelle)) return false; + if (filter.regionen.length && !filter.regionen.includes(k.region)) return false; + return true; + }); + const kundenIds = new Set(kunden.map((k) => k.id)); + + const mitarbeiter = daten.mitarbeiter.filter((m) => { + if (filter.mitarbeiterIds.length && !filter.mitarbeiterIds.includes(m.id)) return false; + if (filter.gewerke.length && !filter.gewerke.includes(m.gewerk)) return false; + return true; + }); + const mitarbeiterIds = new Set(mitarbeiter.map((m) => m.id)); + + const auftraege = daten.auftraege.filter((a) => { + if (!kundenIds.has(a.kundeId)) return false; + if (filter.gewerke.length && !filter.gewerke.includes(a.gewerk)) return false; + if (filter.auftragStatus.length && !filter.auftragStatus.includes(a.status)) return false; + if (filter.mitarbeiterIds.length && !mitarbeiterIds.has(a.mitarbeiterId)) return false; + return true; + }); + const auftragIds = new Set(auftraege.map((a) => a.id)); + + const rechnungen = daten.rechnungen.filter((r) => auftragIds.has(r.auftragId)); + const zeiten = daten.zeiten.filter((z) => mitarbeiterIds.has(z.mitarbeiterId)); + + return { mitarbeiter, kunden, auftraege, rechnungen, zeiten }; +} + +/** Anzahl aktiver Einschränkungen — für das Badge am Filter-Button. */ +export function anzahlAktiverFilter(filter: FilterState, seite: SeitenId): number { + const wirksam = wirksameFilter(filter, seite); + return ( + wirksam.gewerke.length + + wirksam.auftragStatus.length + + wirksam.kundenQuellen.length + + wirksam.regionen.length + + wirksam.mitarbeiterIds.length + ); +} + +/** Hilfsfunktion: liegt ein Datum im aktiven Zeitraum? */ +export function imZeitraum(iso: string | null, filter: FilterState): boolean { + return imFenster(iso, zeitfenster(filter.zeitraum)); +} diff --git a/features/dashboard/data/seed.ts b/features/dashboard/data/seed.ts new file mode 100644 index 0000000..0e12259 --- /dev/null +++ b/features/dashboard/data/seed.ts @@ -0,0 +1,629 @@ +/** + * Deterministischer Dummy-Datensatz für einen SHK-/Handwerksbetrieb. + * + * Bewusst deterministisch (fester Seed): Die Zahlen bleiben über Reloads, + * Screenshots und Abstimmungsrunden identisch. Für die echte Software wird + * diese Datei durch Entity-Builder-Queries ersetzt — das Interface `Datensatz` + * bleibt gleich. + * + * Betriebsprofil (fiktiv): + * Meister Krause GmbH · Heizung · Sanitär · Klima · PV · Elektro + * 24 Beschäftigte, ca. 3,4 Mio. € Jahresumsatz, Raum Rhein-Main + */ + +import { + AUFTRAG_STATUS_LABEL, + GEWERKE, + FOERDERFAEHIGE_KATEGORIEN, + KATEGORIEN, + type Auftrag, + type AuftragStatus, + type Datensatz, + type Gewerk, + type Kunde, + type KundenQuelle, + type KundenTyp, + type Mitarbeiter, + type MitarbeiterRolle, + type Rechnung, + type Zeiteintrag, +} from "./types"; +import { HEUTE, addTage, toISO } from "../utils/dateUtils"; + +/* ------------------------------------------------------ Zufall mit Seed */ + +/** mulberry32 — kleiner, schneller PRNG mit reproduzierbarer Sequenz. */ +function prng(seed: number) { + let a = seed; + return function random(): number { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const rnd = prng(20260730); + +const zahl = (min: number, max: number) => Math.floor(rnd() * (max - min + 1)) + min; +const kommazahl = (min: number, max: number) => rnd() * (max - min) + min; +const aus = (liste: readonly T[]): T => liste[Math.floor(rnd() * liste.length)]; + +function gewichtet(optionen: { wert: T; gewicht: number }[]): T { + const gesamt = optionen.reduce((s, o) => s + o.gewicht, 0); + let r = rnd() * gesamt; + for (const o of optionen) { + r -= o.gewicht; + if (r <= 0) return o.wert; + } + return optionen[optionen.length - 1].wert; +} + +/** Zufälliges Datum zwischen zwei Zeitpunkten. */ +function datumZwischen(von: Date, bis: Date): Date { + return new Date(von.getTime() + rnd() * (bis.getTime() - von.getTime())); +} + +/* ---------------------------------------------------------- Mitarbeiter */ + +const VORNAMEN = [ + "Thomas", "Michael", "Andreas", "Stefan", "Christian", "Markus", "Peter", "Martin", + "Daniel", "Kevin", "Sebastian", "Tobias", "Jens", "Sandra", "Maike", "Nicole", + "Julia", "Patrick", "Dennis", "Marco", "Sven", "Lukas", "Fabian", "Mehmet", +]; +const NACHNAMEN = [ + "Krause", "Müller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", + "Becker", "Schulz", "Hoffmann", "Schäfer", "Koch", "Richter", "Klein", "Wolf", + "Neumann", "Zimmermann", "Braun", "Hartmann", "Lange", "Werner", "Yilmaz", "Peters", +]; + +function kuerzelAus(name: string): string { + const [v, n] = name.split(" "); + return `${v[0]}${n[0]}`.toUpperCase(); +} + +function baueMitarbeiter(): Mitarbeiter[] { + const rollen: MitarbeiterRolle[] = [ + "Meister", "Meister", "Meister", + "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", + "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", "Monteur", + "Auszubildender", "Auszubildender", "Auszubildender", + "Büro", "Büro", "Büro", "Büro", + ]; + + const benutzt = new Set(); + return rollen.map((rolle, i) => { + let name = `${VORNAMEN[i % VORNAMEN.length]} ${NACHNAMEN[(i * 7) % NACHNAMEN.length]}`; + while (benutzt.has(name)) name = `${aus(VORNAMEN)} ${aus(NACHNAMEN)}`; + benutzt.add(name); + + return { + id: `ma-${i + 1}`, + name, + kuerzel: kuerzelAus(name), + rolle, + gewerk: GEWERKE[i % GEWERKE.length], + // Zwei Langzeitabwesende, damit "aktiv" vs. "gesamt" sichtbar wird. + status: i === 12 || i === 19 ? "inaktiv" : "aktiv", + wochenstunden: rolle === "Auszubildender" ? 35 : rolle === "Büro" ? 38 : 40, + eintrittsdatum: toISO(addTage(HEUTE, -zahl(90, 4200))), + } satisfies Mitarbeiter; + }); +} + +/* ---------------------------------------------------------------- Kunden */ + +const ORTE: { ort: string; plz: string; region: string }[] = [ + { ort: "Wiesbaden", plz: "65189", region: "Hessen" }, + { ort: "Mainz", plz: "55116", region: "Rheinland-Pfalz" }, + { ort: "Frankfurt am Main", plz: "60313", region: "Hessen" }, + { ort: "Rüsselsheim", plz: "65428", region: "Hessen" }, + { ort: "Hofheim am Taunus", plz: "65719", region: "Hessen" }, + { ort: "Bad Homburg", plz: "61348", region: "Hessen" }, + { ort: "Darmstadt", plz: "64283", region: "Hessen" }, + { ort: "Ingelheim", plz: "55218", region: "Rheinland-Pfalz" }, + { ort: "Bingen", plz: "55411", region: "Rheinland-Pfalz" }, + { ort: "Aschaffenburg", plz: "63739", region: "Bayern" }, +]; + +const GEWERBE_NAMEN = [ + "Autohaus Reinhardt", "Bäckerei Lindner", "Hotel Rheinblick", "Praxis Dr. Alber", + "Logistik Weber KG", "Metallbau Süd GmbH", "Kanzlei Hoffmann & Partner", + "Sporthalle Taunusstein", "Kita Sonnenschein", "Getränke Hauser", + "Elektro Nord GmbH", "Weingut Steinberg", "Fahrschule Brandt", "Pflegeheim St. Anna", +]; +const VERWALTUNG_NAMEN = [ + "Rhein-Main Immobilien GmbH", "Hausverwaltung Kessler", "WEG Parkstraße 12", + "Wohnbau Wiesbaden eG", "Immobilien Service Taunus", +]; + +const QUELLEN: { wert: KundenQuelle; gewicht: number }[] = [ + { wert: "Empfehlung", gewicht: 30 }, + { wert: "Bestandskunde", gewicht: 24 }, + { wert: "Google", gewicht: 18 }, + { wert: "Website", gewicht: 14 }, + { wert: "Partnerbetrieb", gewicht: 8 }, + { wert: "Werbung", gewicht: 6 }, +]; + +function baueKunden(anzahl: number): Kunde[] { + const kunden: Kunde[] = []; + for (let i = 0; i < anzahl; i++) { + const typ = gewichtet([ + { wert: "Privat", gewicht: 68 }, + { wert: "Gewerbe", gewicht: 22 }, + { wert: "Hausverwaltung", gewicht: 10 }, + ]); + const ort = aus(ORTE); + + const name = + typ === "Privat" + ? `Familie ${NACHNAMEN[(i * 5) % NACHNAMEN.length]}` + : typ === "Gewerbe" + ? GEWERBE_NAMEN[i % GEWERBE_NAMEN.length] + : VERWALTUNG_NAMEN[i % VERWALTUNG_NAMEN.length]; + + kunden.push({ + id: `kd-${i + 1}`, + name: typ === "Privat" ? `${name} · ${ort.ort}` : name, + typ, + quelle: gewichtet(QUELLEN), + region: ort.region, + plz: ort.plz, + ort: ort.ort, + angelegtAm: toISO(datumZwischen(addTage(HEUTE, -900), addTage(HEUTE, -2))), + letzterAuftragAm: null, + umsatzGesamt: 0, + auftraegeGesamt: 0, + // Die meisten zahlen nahe am Zahlungsziel; ein kleiner Teil schleppt. + // Gleichverteilt wäre jeder zweite Kunde auffällig — das wäre kein Betrieb, + // sondern ein Inkassofall. + zahlungsmoralTage: gewichtet([ + { wert: zahl(5, 14), gewicht: 42 }, + { wert: zahl(15, 24), gewicht: 33 }, + { wert: zahl(25, 34), gewicht: 15 }, + { wert: zahl(35, 52), gewicht: 7 }, + { wert: zahl(53, 78), gewicht: 3 }, + ]), + }); + } + return kunden; +} + +/* -------------------------------------------------------------- Aufträge */ + +/** Realistische Auftragswerte je Leistungsart (netto, in €). */ +const WERT_SPANNEN: Record = { + "Wärmepumpe": [17_500, 34_000], + "Gastherme": [5_800, 12_400], + "Heizkörper": [1_200, 6_500], + "Wartung Heizung": [150, 420], + "Bad-Sanierung": [9_500, 27_000], + "Rohrleitungen": [900, 7_800], + "Armaturen": [250, 2_400], + "Notdienst": [180, 950], + "Splitgerät": [2_400, 8_900], + "Lüftungsanlage": [6_500, 21_000], + "Kanalreinigung": [420, 1_800], + "Wartung Klima": [180, 480], + "PV-Anlage": [11_500, 29_500], + "Batteriespeicher": [6_800, 14_500], + "Wallbox": [1_150, 3_400], + "Wartung PV": [190, 460], + "Elektroinstallation": [1_800, 12_000], + "Zählerschrank": [1_400, 4_200], + "Smart Home": [2_200, 9_800], + "E-Check": [140, 390], +}; + +/** Bezugsgröße der Förderquote — zentral in types.ts definiert. */ +const FOERDERFAEHIG = FOERDERFAEHIGE_KATEGORIEN; + +/** + * Ergebnis eines Vorgangs, abhängig von seinem Alter. + * + * Die Verteilung bildet einen realistischen Trichter ab: + * von 100 Anfragen werden rund 55 zum Angebot und rund 24 zum Auftrag. + * Verlorene Vorgänge merken sich, auf welcher Stufe sie abgesprungen sind — + * sonst zeigt der Trichter oben zu wenig und unten zu viel. + */ +function ergebnisFuerAlter( + tageAlt: number, + /** Große Vorhaben werden angeboten und oft verglichen — kleine kaum. */ + grossauftrag: boolean +): { + status: AuftragStatus; + verlorenAb: AuftragStatus | null; +} { + type Ergebnis = { status: AuftragStatus; verlorenAb: AuftragStatus | null }; + const verloren = (stufe: AuftragStatus): Ergebnis => ({ status: "verloren", verlorenAb: stufe }); + const offen = (stufe: AuftragStatus): Ergebnis => ({ status: stufe, verlorenAb: null }); + + /** + * Verlustneigung: Wartungs- und Notdienstaufträge werden praktisch immer + * ausgeführt, große Anlagen dagegen ausgeschrieben und verglichen. + * Ein pauschaler Wert über alle Auftragsarten würde die Angebotsquote + * unrealistisch schönrechnen. + */ + const verlustFaktor = grossauftrag ? 2.1 : 0.28; + const v = (gewicht: number) => gewicht * verlustFaktor; + + // Frisch: steckt noch vorne im Trichter. + if (tageAlt < 12) { + return gewichtet([ + { wert: offen("anfrage"), gewicht: 40 }, + { wert: offen("aufmass"), gewicht: 24 }, + { wert: offen("angebot"), gewicht: 20 }, + { wert: offen("beauftragt"), gewicht: 9 }, + { wert: verloren("anfrage"), gewicht: v(7) }, + ]); + } + + // In Bearbeitung: Entscheidungen fallen. + if (tageAlt < 45) { + return gewichtet([ + { wert: offen("aufmass"), gewicht: 7 }, + { wert: offen("angebot"), gewicht: 20 }, + { wert: offen("beauftragt"), gewicht: 13 }, + { wert: offen("in_arbeit"), gewicht: 17 }, + { wert: offen("abgeschlossen"), gewicht: 8 }, + { wert: verloren("anfrage"), gewicht: v(9) }, + { wert: verloren("aufmass"), gewicht: v(7) }, + { wert: verloren("angebot"), gewicht: v(34) }, + ]); + } + + // Älter als ein Quartal: fast alles ist entschieden. + if (tageAlt < 120) { + return gewichtet([ + { wert: offen("in_arbeit"), gewicht: 9 }, + { wert: offen("abgeschlossen"), gewicht: 50 }, + { wert: verloren("anfrage"), gewicht: v(8) }, + { wert: verloren("aufmass"), gewicht: v(7) }, + { wert: verloren("angebot"), gewicht: v(46) }, + ]); + } + + // Über vier Monate alt: entweder fertig oder verloren. Ein Vorgang, der + // seit zwei Jahren "in Arbeit" ist, wäre ein Datenfehler, kein Auftrag. + return gewichtet([ + { wert: offen("abgeschlossen"), gewicht: 60 }, + { wert: verloren("anfrage"), gewicht: v(9) }, + { wert: verloren("aufmass"), gewicht: v(8) }, + { wert: verloren("angebot"), gewicht: v(54) }, + ]); +} + +/** + * Leistungsart innerhalb eines Gewerks. + * Wartung, Notdienst und E-Check sind im Alltag die häufigsten Aufträge — + * gleichverteilt zu ziehen würde den Durchschnittswert massiv verzerren. + */ +function kategorieFuer(gewerk: Gewerk): string { + const liste = KATEGORIEN[gewerk]; + const kleinteilig = liste.filter((k) => /Wartung|Notdienst|E-Check|Armaturen/.test(k)); + const gross = liste.filter((k) => !kleinteilig.includes(k)); + if (!kleinteilig.length) return aus(liste); + return rnd() < 0.42 ? aus(kleinteilig) : aus(gross); +} + +/** + * Verteilt Auftragsdaten über die letzten 24 Monate. + * + * Zwei Effekte sind absichtlich eingebaut, weil ein gleichverteilter + * Datensatz in jedem Zeitreihen-Chart unnatürlich flach aussieht: + * - moderates Wachstum von rund 12 % gegenüber dem Vorjahr + * - Saisonalität: Heizung, Wärmepumpe und Lüftung ziehen von September + * bis Februar an, der Hochsommer ist schwächer + * Umgesetzt über Verwerfungsmethode gegen eine Dichtefunktion. + */ +const SAISON_INDEX = [ + // Jan Feb Mär Apr Mai Jun Jul Aug Sep Okt Nov Dez + 1.12, 1.06, 1.0, 0.94, 0.88, 0.84, 0.82, 0.9, 1.1, 1.22, 1.18, 0.98, +]; + +function ziehAuftragsalter(): number { + const maxDichte = 1.12 * 1.22; + for (let versuch = 0; versuch < 40; versuch++) { + const tage = zahl(0, 719); + const datum = addTage(HEUTE, -tage); + const wachstum = 1 + 0.12 * (1 - tage / 720); + const dichte = wachstum * SAISON_INDEX[datum.getMonth()]; + if (rnd() <= dichte / maxDichte) return tage; + } + return zahl(0, 719); +} + +function baueAuftraege(kunden: Kunde[], mitarbeiter: Mitarbeiter[], anzahl: number): Auftrag[] { + const ausfuehrende = mitarbeiter.filter((m) => m.rolle === "Monteur" || m.rolle === "Meister"); + const auftraege: Auftrag[] = []; + + for (let i = 0; i < anzahl; i++) { + const tageAlt = ziehAuftragsalter(); + const angelegt = addTage(HEUTE, -tageAlt); + + const gewerk = gewichtet([ + { wert: "Heizung", gewicht: 34 }, + { wert: "Sanitär", gewicht: 24 }, + { wert: "Photovoltaik", gewicht: 20 }, + { wert: "Klima & Lüftung", gewicht: 12 }, + { wert: "Elektro", gewicht: 10 }, + ]); + const kategorie = kategorieFuer(gewerk); + const [minWert, maxWert] = WERT_SPANNEN[kategorie] ?? [500, 5_000]; + const wert = Math.round(kommazahl(minWert, maxWert) / 10) * 10; + + // Ab etwa 6.000 € wird im Handwerk regelmäßig schriftlich angeboten und + // verglichen; darunter läuft der Auftrag meist direkt. + const ueberAngebot = wert >= 6_000; + const { status, verlorenAb } = ergebnisFuerAlter(tageAlt, ueberAngebot); + const kunde = aus(kunden); + const mitarbeiterId = aus(ausfuehrende).id; + + const laufzeitTage = wert > 12_000 ? zahl(21, 75) : zahl(3, 28); + const startAm = + status === "in_arbeit" || status === "abgeschlossen" + ? toISO(addTage(angelegt, zahl(5, 30))) + : null; + const abgeschlossenAm = + status === "abgeschlossen" ? toISO(addTage(angelegt, laufzeitTage + zahl(0, 20))) : null; + + const foerderfaehig = FOERDERFAEHIG.has(kategorie); + // Ein Förderbescheid dauert Wochen, nicht Jahre: Nur bei jungen Vorgängen + // ist "beantragt" plausibel, ältere sind längst entschieden. + const foerderung: Auftrag["foerderung"] = !foerderfaehig + ? "keine" + : tageAlt <= 100 + ? gewichtet([ + { wert: "beantragt" as const, gewicht: 46 }, + { wert: "bewilligt" as const, gewicht: 39 }, + { wert: "keine" as const, gewicht: 15 }, + ]) + : gewichtet([ + { wert: "bewilligt" as const, gewicht: 84 }, + { wert: "keine" as const, gewicht: 16 }, + ]); + + auftraege.push({ + id: `au-${i + 1}`, + nummer: `A-2026-${String(1000 + i).slice(-4)}`, + kundeId: kunde.id, + mitarbeiterId, + gewerk, + kategorie, + titel: `${kategorie} · ${kunde.ort}`, + status, + prioritaet: gewichtet([ + { wert: "normal" as const, gewicht: 62 }, + { wert: "hoch" as const, gewicht: 22 }, + { wert: "niedrig" as const, gewicht: 16 }, + ]), + wert, + margeProzent: Math.round(kommazahl(16, 38) * 10) / 10, + angelegtAm: toISO(angelegt), + startAm, + abgeschlossenAm, + faelligAm: toISO(addTage(angelegt, laufzeitTage)), + foerderung, + foerderbetrag: + foerderung === "keine" ? 0 : Math.round((wert * kommazahl(0.2, 0.4)) / 50) * 50, + verlorenAb, + ueberAngebot, + }); + } + + return auftraege.sort((a, b) => (a.angelegtAm < b.angelegtAm ? 1 : -1)); +} + +/* ------------------------------------------------------------ Rechnungen */ + +function baueRechnungen(auftraege: Auftrag[]): Rechnung[] { + const rechnungen: Rechnung[] = []; + let nr = 0; + + for (const auftrag of auftraege) { + // Rechnung entsteht erst ab Beauftragung (Anzahlung) bzw. Abschluss. + if (!["beauftragt", "in_arbeit", "abgeschlossen"].includes(auftrag.status)) continue; + + const positionen: { betrag: number; basis: string }[] = []; + + if (auftrag.wert > 8_000 && auftrag.startAm) { + // Große Vorhaben: 30 % Anzahlung bei Auftragserteilung. + positionen.push({ betrag: Math.round(auftrag.wert * 0.3), basis: auftrag.startAm }); + } + if (auftrag.status === "abgeschlossen" && auftrag.abgeschlossenAm) { + const bereits = positionen.reduce((s, p) => s + p.betrag, 0); + positionen.push({ betrag: auftrag.wert - bereits, basis: auftrag.abgeschlossenAm }); + } + if (positionen.length === 0 && auftrag.status !== "beauftragt" && auftrag.startAm) { + positionen.push({ betrag: Math.round(auftrag.wert * 0.5), basis: auftrag.startAm }); + } + + for (const position of positionen) { + nr += 1; + const gestellt = new Date(position.basis); + const zahlungsziel = 21; + const faellig = addTage(gestellt, zahlungsziel); + + const ueberfaelligTage = Math.floor((HEUTE.getTime() - faellig.getTime()) / 86_400_000); + let status: Rechnung["status"]; + let bezahltAm: string | null = null; + + if (gestellt > HEUTE) { + // Rechnung existiert noch nicht — z. B. Schlussrechnung eines laufenden Auftrags. + status = "entwurf"; + } else if (ueberfaelligTage < 0) { + // Noch innerhalb des Zahlungsziels; ein Teil zahlt sofort. + status = rnd() < 0.08 ? "bezahlt" : "gestellt"; + if (status === "bezahlt") bezahltAm = toISO(addTage(gestellt, zahl(2, zahlungsziel))); + } else if (ueberfaelligTage <= 18) { + // Kurz über dem Ziel: das ist Alltag, noch kein Mahnfall. + if (rnd() < 0.52) { + status = "bezahlt"; + bezahltAm = toISO(addTage(faellig, zahl(-6, ueberfaelligTage))); + } else { + status = "gestellt"; + } + } else if (ueberfaelligTage <= 60) { + if (rnd() < 0.84) { + status = "bezahlt"; + bezahltAm = toISO(addTage(faellig, zahl(-8, Math.min(ueberfaelligTage, 34)))); + } else { + status = "ueberfaellig"; + } + } else if (ueberfaelligTage <= 120) { + // Mahnstufe erreicht — der Großteil zahlt spätestens jetzt. + if (rnd() < 0.94) { + status = "bezahlt"; + bezahltAm = toISO(addTage(faellig, zahl(-8, 70))); + } else { + status = "ueberfaellig"; + } + } else if (ueberfaelligTage <= 210) { + // Letzte Mahnstufe: hier entscheidet sich Zahlung oder Ausfall. + if (rnd() < 0.992) { + status = "bezahlt"; + bezahltAm = toISO(addTage(faellig, zahl(-8, 120))); + } else { + status = "ueberfaellig"; + } + } else { + // Älter als sieben Monate: eingebracht oder ausgebucht — in beiden + // Fällen steht die Forderung nicht mehr in der offenen Liste. + status = "bezahlt"; + bezahltAm = toISO(addTage(faellig, zahl(-8, 150))); + } + + rechnungen.push({ + id: `re-${nr}`, + nummer: `R-2026-${String(2000 + nr).slice(-4)}`, + auftragId: auftrag.id, + kundeId: auftrag.kundeId, + richtung: "ausgang", + betrag: position.betrag, + status, + gestelltAm: toISO(gestellt), + faelligAm: toISO(faellig), + bezahltAm, + }); + } + } + + return rechnungen; +} + +/* ---------------------------------------------------------- Zeiterfassung */ + +function baueZeiten(mitarbeiter: Mitarbeiter[], auftraege: Auftrag[]): Zeiteintrag[] { + const zeiten: Zeiteintrag[] = []; + const laufend = auftraege.filter((a) => a.status === "in_arbeit" || a.status === "abgeschlossen"); + const gewerblich = mitarbeiter.filter( + (m) => m.status === "aktiv" && m.rolle !== "Büro" + ); + let nr = 0; + + // Letzte 12 Wochen, Montag bis Freitag. + for (let tagOffset = 84; tagOffset >= 0; tagOffset--) { + const tag = addTage(HEUTE, -tagOffset); + const wochentag = tag.getDay(); + if (wochentag === 0 || wochentag === 6) continue; + + for (const person of gewerblich) { + const abwesend = rnd() < 0.09; + if (abwesend) { + nr += 1; + zeiten.push({ + id: `ze-${nr}`, + mitarbeiterId: person.id, + auftragId: null, + datum: toISO(tag), + stunden: 8, + art: "abwesenheit", + abwesenheitsgrund: gewichtet([ + { wert: "urlaub" as const, gewicht: 55 }, + { wert: "krank" as const, gewicht: 33 }, + { wert: "fortbildung" as const, gewicht: 12 }, + ]), + }); + continue; + } + + nr += 1; + const auftrag = laufend.length ? aus(laufend) : null; + zeiten.push({ + id: `ze-${nr}`, + mitarbeiterId: person.id, + auftragId: auftrag?.id ?? null, + datum: toISO(tag), + // Produktive Stunden schwanken; nicht jede Stunde ist verrechenbar. + stunden: Math.round(kommazahl(4.5, 9.5) * 2) / 2, + art: "arbeit", + }); + } + } + + return zeiten; +} + +/* -------------------------------------------------------- Aggregation */ + +function ergaenzeKundenkennzahlen( + kunden: Kunde[], + auftraege: Auftrag[], + rechnungen: Rechnung[] +): Kunde[] { + const umsatzProKunde = new Map(); + for (const r of rechnungen) { + if (r.status !== "bezahlt") continue; + umsatzProKunde.set(r.kundeId, (umsatzProKunde.get(r.kundeId) ?? 0) + r.betrag); + } + + const auftraegeProKunde = new Map(); + for (const a of auftraege) { + const liste = auftraegeProKunde.get(a.kundeId) ?? []; + liste.push(a); + auftraegeProKunde.set(a.kundeId, liste); + } + + return kunden.map((kunde) => { + const eigene = auftraegeProKunde.get(kunde.id) ?? []; + const letzter = eigene + .map((a) => a.angelegtAm) + .sort() + .at(-1); + + return { + ...kunde, + umsatzGesamt: umsatzProKunde.get(kunde.id) ?? 0, + auftraegeGesamt: eigene.filter((a) => a.status !== "verloren").length, + letzterAuftragAm: letzter ?? null, + }; + }); +} + +/* --------------------------------------------------------------- Export */ + +function baueDatensatz(): Datensatz { + const mitarbeiter = baueMitarbeiter(); + const rohkunden = baueKunden(420); + const auftraege = baueAuftraege(rohkunden, mitarbeiter, 1900); + const rechnungen = baueRechnungen(auftraege); + const kunden = ergaenzeKundenkennzahlen(rohkunden, auftraege, rechnungen); + const zeiten = baueZeiten(mitarbeiter, auftraege); + + return { mitarbeiter, kunden, auftraege, rechnungen, zeiten }; +} + +/** Einmal erzeugter, unveränderlicher Datensatz für die gesamte App. */ +export const DATENSATZ: Datensatz = baueDatensatz(); + +/** Nur für Debug-Ausgaben / die Komponenten-Galerie. */ +export const DATENSATZ_INFO = { + mitarbeiter: DATENSATZ.mitarbeiter.length, + kunden: DATENSATZ.kunden.length, + auftraege: DATENSATZ.auftraege.length, + rechnungen: DATENSATZ.rechnungen.length, + zeiteintraege: DATENSATZ.zeiten.length, + statusLabels: Object.values(AUFTRAG_STATUS_LABEL), +}; diff --git a/features/dashboard/data/types.ts b/features/dashboard/data/types.ts new file mode 100644 index 0000000..20b39a1 --- /dev/null +++ b/features/dashboard/data/types.ts @@ -0,0 +1,249 @@ +/** + * Datenmodell des Analytics-Prototyps. + * + * Die Felder sind bewusst nah an den HiveGrid-Entities (invoice, order, + * company, contact, timeEntry) gehalten, damit die Entwicklung die + * Dummy-Daten 1:1 gegen echte Entity-Builder-Daten tauschen kann. + * Siehe docs/DATENMODELL.md. + */ + +/* ---------------------------------------------------------------- Gewerke */ + +export const GEWERKE = [ + "Heizung", + "Sanitär", + "Klima & Lüftung", + "Photovoltaik", + "Elektro", +] as const; +export type Gewerk = (typeof GEWERKE)[number]; + +/** Leistungsarten je Gewerk — Grundlage für "Umsatz nach Kategorie". */ +export const KATEGORIEN: Record = { + Heizung: ["Wärmepumpe", "Gastherme", "Heizkörper", "Wartung Heizung"], + Sanitär: ["Bad-Sanierung", "Rohrleitungen", "Armaturen", "Notdienst"], + "Klima & Lüftung": ["Splitgerät", "Lüftungsanlage", "Kanalreinigung", "Wartung Klima"], + Photovoltaik: ["PV-Anlage", "Batteriespeicher", "Wallbox", "Wartung PV"], + Elektro: ["Elektroinstallation", "Zählerschrank", "Smart Home", "E-Check"], +}; + +/* -------------------------------------------------------------- Mitarbeiter */ + +export type MitarbeiterRolle = "Meister" | "Monteur" | "Auszubildender" | "Büro"; + +export interface Mitarbeiter { + id: string; + name: string; + kuerzel: string; + rolle: MitarbeiterRolle; + gewerk: Gewerk; + status: "aktiv" | "inaktiv"; + /** Vertraglich verfügbare Stunden pro Woche. */ + wochenstunden: number; + eintrittsdatum: string; +} + +/* ------------------------------------------------------------------ Kunden */ + +export type KundenQuelle = + | "Empfehlung" + | "Google" + | "Website" + | "Werbung" + | "Bestandskunde" + | "Partnerbetrieb"; + +export type KundenTyp = "Privat" | "Gewerbe" | "Hausverwaltung"; + +export interface Kunde { + id: string; + name: string; + typ: KundenTyp; + quelle: KundenQuelle; + region: string; + plz: string; + ort: string; + angelegtAm: string; + letzterAuftragAm: string | null; + /** Aggregiert aus Rechnungen — wird in seed.ts nachberechnet. */ + umsatzGesamt: number; + auftraegeGesamt: number; + zahlungsmoralTage: number; +} + +/* ---------------------------------------------------------------- Aufträge */ + +export type AuftragStatus = + | "anfrage" + | "aufmass" + | "angebot" + | "beauftragt" + | "in_arbeit" + | "abgeschlossen" + | "verloren"; + +export const AUFTRAG_STATUS_LABEL: Record = { + anfrage: "Anfrage", + aufmass: "Vor-Aufmaß", + angebot: "Angebot", + beauftragt: "Beauftragt", + in_arbeit: "In Arbeit", + abgeschlossen: "Abgeschlossen", + verloren: "Verloren", +}; + +/** + * Leistungsarten, für die BAFA-/KfW-Förderung in Frage kommt. + * Bezugsgröße der Förderquote — sonst rechnet man Wartungsaufträge + * in eine Quote hinein, die für sie gar nicht gilt. + */ +export const FOERDERFAEHIGE_KATEGORIEN = new Set([ + "Wärmepumpe", + "PV-Anlage", + "Batteriespeicher", + "Lüftungsanlage", +]); + +/** Reihenfolge im Vertriebstrichter (ohne Endzustände). */ +export const FUNNEL_STUFEN: AuftragStatus[] = [ + "anfrage", + "aufmass", + "angebot", + "beauftragt", + "in_arbeit", + "abgeschlossen", +]; + +export interface Auftrag { + id: string; + nummer: string; + kundeId: string; + mitarbeiterId: string; + gewerk: Gewerk; + kategorie: string; + titel: string; + status: AuftragStatus; + prioritaet: "niedrig" | "normal" | "hoch"; + wert: number; + /** Geschätzter Deckungsbeitrag in Prozent. */ + margeProzent: number; + angelegtAm: string; + startAm: string | null; + abgeschlossenAm: string | null; + faelligAm: string; + /** BAFA/KfW-Förderung beantragt? */ + foerderung: "keine" | "beantragt" | "bewilligt"; + foerderbetrag: number; + /** + * Bei verlorenen Vorgängen: die zuletzt erreichte Trichterstufe. + * Ohne dieses Feld müsste der Trichter raten, wo ein Vorgang abgesprungen + * ist — und würde die oberen Stufen systematisch zu klein zeigen. + */ + verlorenAb: AuftragStatus | null; + /** + * Lief der Vorgang über ein schriftliches Angebot? + * Wartung, Notdienst und E-Check werden direkt beauftragt. Zählt man sie + * in die Angebotsquote hinein, sieht der Vertrieb doppelt so gut aus, + * wie er ist. + */ + ueberAngebot: boolean; +} + +/* --------------------------------------------------------------- Rechnungen */ + +export type RechnungStatus = "entwurf" | "gestellt" | "bezahlt" | "ueberfaellig"; + +export const RECHNUNG_STATUS_LABEL: Record = { + entwurf: "Entwurf", + gestellt: "Gestellt", + bezahlt: "Bezahlt", + ueberfaellig: "Überfällig", +}; + +export interface Rechnung { + id: string; + nummer: string; + auftragId: string; + kundeId: string; + richtung: "ausgang" | "eingang"; + betrag: number; + status: RechnungStatus; + gestelltAm: string; + faelligAm: string; + bezahltAm: string | null; +} + +/* ------------------------------------------------------------ Zeiterfassung */ + +export interface Zeiteintrag { + id: string; + mitarbeiterId: string; + auftragId: string | null; + datum: string; + stunden: number; + art: "arbeit" | "abwesenheit"; + abwesenheitsgrund?: "urlaub" | "krank" | "fortbildung"; +} + +/* ---------------------------------------------------------------- Datensatz */ + +export interface Datensatz { + mitarbeiter: Mitarbeiter[]; + kunden: Kunde[]; + auftraege: Auftrag[]; + rechnungen: Rechnung[]; + zeiten: Zeiteintrag[]; +} + +/* ------------------------------------------------------------------ Filter */ + +export type ZeitraumPreset = + | "heute" + | "7_tage" + | "30_tage" + | "quartal" + | "jahr" + | "alles"; + +export const ZEITRAUM_LABEL: Record = { + heute: "Heute", + "7_tage": "7 Tage", + "30_tage": "30 Tage", + quartal: "Quartal", + jahr: "Jahr", + alles: "Gesamt", +}; + +export interface FilterState { + zeitraum: ZeitraumPreset; + gewerke: Gewerk[]; + auftragStatus: AuftragStatus[]; + kundenQuellen: KundenQuelle[]; + regionen: string[]; + mitarbeiterIds: string[]; +} + +export const LEERER_FILTER: FilterState = { + zeitraum: "30_tage", + gewerke: [], + auftragStatus: [], + kundenQuellen: [], + regionen: [], + mitarbeiterIds: [], +}; + +/* ------------------------------------------------------------- Chart-Typen */ + +export interface ChartPunkt { + name: string; + value: number; + /** Optionale Zweitreihe, z. B. Vorjahr. */ + vergleich?: number; +} + +export interface ZeitreihePunkt { + datum: string; + label: string; + value: number; + vergleich?: number; +} diff --git a/features/dashboard/hooks/useAnalyseDaten.ts b/features/dashboard/hooks/useAnalyseDaten.ts new file mode 100644 index 0000000..7e57825 --- /dev/null +++ b/features/dashboard/hooks/useAnalyseDaten.ts @@ -0,0 +1,30 @@ +"use client"; + +import React from "react"; +import { DATENSATZ } from "../data/seed"; +import { wendeFilterAn, wirksameFilter, type SeitenId } from "../data/filtering"; +import { useFilter } from "../context/FilterContext"; +import type { Datensatz, FilterState } from "../data/types"; + +/** + * Liefert den auf die aktive Filterauswahl zugeschnittenen Datensatz. + * + * `daten` — gefiltert nach den auf dieser Seite wirksamen Dimensionen + * `filter` — die tatsächlich wirksamen Filter (irrelevante sind neutralisiert) + * + * Der Zeitraum steckt bewusst nur im Filter, nicht im Datensatz: Manche + * Kennzahlen sind zeitraumbezogen (Umsatz), andere stichtagsbezogen + * (offene Posten). Die Berechnungen entscheiden das jeweils selbst. + */ +export function useAnalyseDaten(seite: SeitenId): { + daten: Datensatz; + filter: FilterState; + gesamt: Datensatz; +} { + const { filter } = useFilter(); + + const wirksam = React.useMemo(() => wirksameFilter(filter, seite), [filter, seite]); + const daten = React.useMemo(() => wendeFilterAn(DATENSATZ, wirksam), [wirksam]); + + return { daten, filter: wirksam, gesamt: DATENSATZ }; +} diff --git a/features/dashboard/pages/AuftraegeSeite.tsx b/features/dashboard/pages/AuftraegeSeite.tsx new file mode 100644 index 0000000..2b54621 --- /dev/null +++ b/features/dashboard/pages/AuftraegeSeite.tsx @@ -0,0 +1,287 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer, Raster } from "../components/layout/PageContainer"; +import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { StatZeile } from "../components/cards/StatZeile"; +import { BalkenVertikal } from "../components/charts/BalkenVertikal"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { Donut } from "../components/charts/Donut"; +import { Trichter } from "../components/charts/Trichter"; +import { DataTable, type Spalte } from "../components/tables/DataTable"; +import { AuftragBadge } from "../components/tables/StatusBadge"; +import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; +import { + abschlussquote, + aktiveAuftraege, + auftraegeImZeitraum, + auftraegeNachGewerk, + auftraegeNachStatus, + auftragseingang, + durchlaufzeit, + foerderQuote, + ueberfaelligeAuftraege, + vertriebsTrichter, +} from "../data/calculations"; +import { + berechneTrend, + formatDatum, + formatEuro, + formatEuroKompakt, + formatProzent, + formatTage, + formatZahl, + tageSeit, + tageLabel, + tageLabelDativ, +} from "../utils/formatters"; +import { STATUS_TEXT } from "../utils/chartTheme"; +import { HEUTE, vorperiode, imFenster } from "../utils/dateUtils"; +import type { Auftrag } from "../data/types"; + +export function AuftraegeSeite() { + const { daten, filter } = useAnalyseDaten("auftraege"); + + const imZeitraum = auftraegeImZeitraum(daten.auftraege, filter.zeitraum); + const vorher = daten.auftraege.filter((a) => imFenster(a.angelegtAm, vorperiode(filter.zeitraum))); + const aktiv = aktiveAuftraege(daten.auftraege); + const ueberfaellig = ueberfaelligeAuftraege(daten.auftraege); + const quote = abschlussquote(daten.auftraege); + const laufzeit = durchlaufzeit(daten.auftraege); + const foerderung = foerderQuote(daten.auftraege); + + const eingang = auftragseingang(daten.auftraege, filter.zeitraum); + const bestand = aktiv.reduce((s, a) => s + a.wert, 0); + + const mitarbeiterName = React.useMemo( + () => new Map(daten.mitarbeiter.map((m) => [m.id, m.name])), + [daten.mitarbeiter] + ); + const kundeName = React.useMemo( + () => new Map(daten.kunden.map((k) => [k.id, k.name])), + [daten.kunden] + ); + + const spalten: Spalte[] = [ + { + key: "titel", + titel: "Auftrag", + breite: "28%", + zelle: (a) => ( +
+ {a.titel} + + {a.nummer} · {kundeName.get(a.kundeId) ?? "Unbekannt"} + +
+ ), + sortWert: (a) => a.titel, + }, + { + key: "gewerk", + titel: "Gewerk", + zelle: (a) => {a.gewerk}, + sortWert: (a) => a.gewerk, + }, + { key: "status", titel: "Status", zelle: (a) => }, + { + key: "monteur", + titel: "Verantwortlich", + zelle: (a) => ( + + {mitarbeiterName.get(a.mitarbeiterId) ?? "—"} + + ), + sortWert: (a) => mitarbeiterName.get(a.mitarbeiterId) ?? "", + }, + { + key: "faellig", + titel: "Termin", + numerisch: true, + zelle: (a) => { + const tage = tageSeit(a.faelligAm, HEUTE); + return tage > 0 ? ( + + {tageLabel(tage, "über")} + + ) : ( + {formatDatum(a.faelligAm)} + ); + }, + sortWert: (a) => tageSeit(a.faelligAm, HEUTE), + }, + { + key: "wert", + titel: "Wert", + numerisch: true, + zelle: (a) => {formatEuro(a.wert)}, + sortWert: (a) => a.wert, + }, + ]; + + return ( + <> + + + + + + p.value)} + /> + + + + + + +
+ + formatZahl(w)} hoehe={250} nurGanzzahlen /> + s + a.wert, 0) / imZeitraum.length + : 0 + ), + }, + ]} + /> + + + + formatZahl(w)} /> + +
+ +
+ + formatZahl(w)} + hoehe={220} + mitteLabel="Vorgänge" + /> + + + + formatZahl(w)} + einfarbig={false} + maxBalken={5} + /> + + + + formatZahl(w)} + hoehe={200} + mitteLabel="Förderfähig" + /> + + +
+ + + + `${a.titel} ${a.nummer} ${a.gewerk} ${mitarbeiterName.get(a.mitarbeiterId) ?? ""}` + } + leerText="Keine aktiven Aufträge" + /> + +
+ + ); +} diff --git a/features/dashboard/pages/FinanzenSeite.tsx b/features/dashboard/pages/FinanzenSeite.tsx new file mode 100644 index 0000000..1617694 --- /dev/null +++ b/features/dashboard/pages/FinanzenSeite.tsx @@ -0,0 +1,278 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer, Raster } from "../components/layout/PageContainer"; +import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { StatZeile } from "../components/cards/StatZeile"; +import { AreaVerlauf } from "../components/charts/AreaVerlauf"; +import { BalkenVertikal } from "../components/charts/BalkenVertikal"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { Donut } from "../components/charts/Donut"; +import { ChartLegende } from "../components/charts/ChartLegende"; +import { DataTable, type Spalte } from "../components/tables/DataTable"; +import { RechnungBadge } from "../components/tables/StatusBadge"; +import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; +import { + marge, + offenePosten, + offeneNachStatus, + umsatz, + umsatzNachGewerk, + umsatzVerlauf, + umsatzVorperiode, + zahlungsdauer, +} from "../data/calculations"; +import { + berechneTrend, + formatDatum, + formatEuro, + formatEuroAchse, + formatEuroKompakt, + formatProzent, + formatTage, + tageSeit, + tageLabel, +} from "../utils/formatters"; +import { HEUTE } from "../utils/dateUtils"; +import { + PRIMAER_FARBE, + RISIKO_RAMPE, + STATUS_TEXT, + VERGLEICH_FARBE, +} from "../utils/chartTheme"; +import type { Rechnung } from "../data/types"; + +/** Ampelfarben für die Altersstruktur: je älter die Forderung, desto kritischer. */ +const ALTER_FARBEN = [...RISIKO_RAMPE]; + +export function FinanzenSeite() { + const { daten, filter } = useAnalyseDaten("finanzen"); + + const umsatzAktuell = umsatz(daten.rechnungen, filter.zeitraum); + const umsatzVorher = umsatzVorperiode(daten.rechnungen, filter.zeitraum); + const verlauf = umsatzVerlauf(daten.rechnungen, filter.zeitraum); + const posten = offenePosten(daten.rechnungen); + const dauer = zahlungsdauer(daten.rechnungen, filter.zeitraum); + const margeProzent = marge(daten.auftraege, filter.zeitraum); + + const kundeName = React.useMemo( + () => new Map(daten.kunden.map((k) => [k.id, k.name])), + [daten.kunden] + ); + + const spalten: Spalte[] = [ + { + key: "nummer", + titel: "Rechnung", + zelle: (r) => ( +
+ {r.nummer} + + {kundeName.get(r.kundeId) ?? "Unbekannt"} + +
+ ), + sortWert: (r) => r.nummer, + breite: "30%", + }, + { key: "status", titel: "Status", zelle: (r) => }, + { + key: "gestellt", + titel: "Gestellt", + zelle: (r) => formatDatum(r.gestelltAm), + sortWert: (r) => r.gestelltAm, + }, + { + key: "faellig", + titel: "Überfällig", + numerisch: true, + zelle: (r) => { + const tage = tageSeit(r.faelligAm, HEUTE); + return tage > 0 ? ( + 30 ? "font-medium" : ""} + style={{ color: tage > 30 ? STATUS_TEXT.negativ : STATUS_TEXT.warnung }} + > + {tageLabel(tage)} + + ) : ( + + in {Math.abs(tage) === 1 ? "1 Tag" : `${Math.abs(tage)} Tagen`} + + ); + }, + sortWert: (r) => tageSeit(r.faelligAm, HEUTE), + }, + { + key: "betrag", + titel: "Betrag", + numerisch: true, + zelle: (r) => {formatEuro(r.betrag)}, + sortWert: (r) => r.betrag, + }, + ]; + + return ( + <> + + + + + + p.value)} + /> + + 0 ? "negativ" : "positiv"} + /> + 30 ? "warnung" : "positiv"} + richtungGut="negativ" + /> + + + +
+ + } + > + + = umsatzVorher ? "+" : "−"}${formatEuro(Math.abs(umsatzAktuell - umsatzVorher))}`, + }, + ]} + /> + + + + + +
+ +
+ + + b.name === "> 90 Tage")?.value ?? 0), + hinweis: "Abschreibungsrisiko", + }, + { label: "Überfällig gesamt", wert: formatEuro(posten.ueberfaelligBetrag) }, + ]} + /> + + + + + + +
+ + + `${r.nummer} ${kundeName.get(r.kundeId) ?? ""}`} + leerText="Keine offenen Rechnungen" + /> + + +
+ + ); +} diff --git a/features/dashboard/pages/KomponentenSeite.tsx b/features/dashboard/pages/KomponentenSeite.tsx new file mode 100644 index 0000000..290c3cb --- /dev/null +++ b/features/dashboard/pages/KomponentenSeite.tsx @@ -0,0 +1,352 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer } from "../components/layout/PageContainer"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { AlertList } from "../components/cards/AlertList"; +import { StatZeile } from "../components/cards/StatZeile"; +import { AreaVerlauf } from "../components/charts/AreaVerlauf"; +import { BalkenVertikal } from "../components/charts/BalkenVertikal"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { Donut } from "../components/charts/Donut"; +import { Trichter } from "../components/charts/Trichter"; +import { AuslastungsBalken } from "../components/charts/AuslastungsBalken"; +import { ChartLeer } from "../components/charts/ChartLeer"; +import { ChartLegende } from "../components/charts/ChartLegende"; +import { AuftragBadge, RechnungBadge } from "../components/tables/StatusBadge"; +import { + KATEGORIE_FARBEN, + MAX_KATEGORIEN, + PRIMAER_FARBE, + STATUS_FARBEN, + VERGLEICH_FARBE, +} from "../utils/chartTheme"; +import { + formatEuro, + formatEuroAchse, + formatEuroKompakt, + formatProzent, + formatZahl, +} from "../utils/formatters"; +import { DATENSATZ_INFO } from "../data/seed"; + +/** Beispieldaten, damit die Galerie unabhängig von Filtern immer etwas zeigt. */ +const BEISPIEL_REIHE = Array.from({ length: 12 }, (_, i) => ({ + datum: `2026-${String(i + 1).padStart(2, "0")}`, + label: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"][i], + value: [148, 162, 171, 158, 166, 149, 152, 161, 189, 205, 198, 176][i] * 1000, + vergleich: [131, 140, 152, 146, 151, 138, 141, 149, 168, 181, 176, 158][i] * 1000, +})); + +const BEISPIEL_KATEGORIEN = [ + { name: "Heizung", value: 842_000 }, + { name: "Sanitär", value: 486_000 }, + { name: "Photovoltaik", value: 421_000 }, + { name: "Klima & Lüftung", value: 268_000 }, + { name: "Elektro", value: 194_000 }, + { name: "Sonstiges", value: 61_000 }, +]; + +export function KomponentenSeite() { + return ( + <> + + + + + + +
+
+ {KATEGORIE_FARBEN.map((farbe, index) => ( +
+ + + {index + 1}. {farbe} + +
+ ))} +
+
+ {Object.entries(STATUS_FARBEN).map(([name, farbe]) => ( + + + Status „{name}“ · {farbe} + + ))} +
+

+ Kategorien nutzen die Carbon-Palette, Statusaussagen die Ampelfarben. Beides nie + mischen: eine grüne Kategorie neben einem grünen Positiv-Status ist nicht mehr lesbar. + Maximal {MAX_KATEGORIEN} Segmente je Chart, der Rest wird zu „Sonstige“ + zusammengefasst. +

+
+
+ + +
+ + + + +
+ + +
+ + } + > + + + + + + +
+ +
+ + ({ label: p.label, value: p.value / 1000 }))} + formatWert={(w) => formatZahl(w)} + hoehe={200} + nurGanzzahlen + /> + + + + + + + + formatZahl(w)} + /> + +
+ +
+ + + + + + + + + + + +
+ + +
+
+ Auftrag + + + + + + + +
+
+ Rechnung + + + + +
+ +
+
+ + +
    + {[ + "Nur horizontale Gridlines in #e0e0e0. Vertikale Linien lenken vom Verlauf ab.", + "Achsenbeschriftungen 12 px in #525252, keine Tick-Marks, Achsenlinie #8d8d8d.", + "Ein Tooltip für alle Charts: dunkle Fläche #393939, weißer Text, kein Doppelpunkt.", + "Maximal fünf Kategorien je Chart, alles darüber wird zu „Sonstige“ zusammengefasst.", + "Balkenabstand mindestens halbe Balkenbreite; Balken nie breiter als 48 px.", + "Zahlen rechtsbündig und in Tabellenziffern, Text linksbündig.", + "Karten: 16 px Innenabstand, 16 px Abstand zwischen Karten — innen nie größer als außen.", + "Jede Karte braucht einen Leerzustand. Filter können jedes Chart leer laufen lassen.", + ].map((regel) => ( +
  • + + {regel} +
  • + ))} +
+
+
+ + ); +} + +function Abschnitt({ titel, text }: { titel: string; text: string }) { + return ( +
+

{titel}

+

{text}

+
+ ); +} + +function Hinweis({ text }: { text: string }) { + return ( +

+ {text} +

+ ); +} diff --git a/features/dashboard/pages/KundenSeite.tsx b/features/dashboard/pages/KundenSeite.tsx new file mode 100644 index 0000000..fc5483e --- /dev/null +++ b/features/dashboard/pages/KundenSeite.tsx @@ -0,0 +1,241 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer, Raster } from "../components/layout/PageContainer"; +import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { BalkenVertikal } from "../components/charts/BalkenVertikal"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { Donut } from "../components/charts/Donut"; +import { DataTable, type Spalte } from "../components/tables/DataTable"; +import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; +import { + inaktiveKunden, + kundenEntwicklung, + kundenNach, + neueKunden, + topKunden, + wiederkehrquote, + zahlungsauffaellig, +} from "../data/calculations"; +import { + berechneTrend, + formatDatum, + formatEuro, + formatEuroKompakt, + formatProzent, + formatZahl, + tageLabel, +} from "../utils/formatters"; +import { STATUS_TEXT } from "../utils/chartTheme"; +import { imFenster, vorperiode } from "../utils/dateUtils"; +import type { Kunde } from "../data/types"; + +export function KundenSeite() { + const { daten, filter } = useAnalyseDaten("kunden"); + + const neu = neueKunden(daten.kunden, filter.zeitraum); + const neuVorher = daten.kunden.filter((k) => + imFenster(k.angelegtAm, vorperiode(filter.zeitraum)) + ).length; + + const mitAuftrag = daten.kunden.filter((k) => k.auftraegeGesamt > 0); + const wiederkehrend = wiederkehrquote(daten.kunden); + const kundenwert = + mitAuftrag.length > 0 + ? mitAuftrag.reduce((s, k) => s + k.umsatzGesamt, 0) / mitAuftrag.length + : 0; + const inaktiv = inaktiveKunden(daten.kunden); + const auffaellig = zahlungsauffaellig(daten.kunden); + const entwicklung = kundenEntwicklung(daten.kunden, filter.zeitraum); + + const spalten: Spalte[] = [ + { + key: "name", + titel: "Kunde", + breite: "26%", + zelle: (k) => ( +
+ {k.name} + {k.typ} +
+ ), + sortWert: (k) => k.name, + }, + { + key: "quelle", + titel: "Quelle", + zelle: (k) => {k.quelle}, + sortWert: (k) => k.quelle, + }, + { + key: "auftraege", + titel: "Aufträge", + numerisch: true, + zelle: (k) => formatZahl(k.auftraegeGesamt), + sortWert: (k) => k.auftraegeGesamt, + }, + { + key: "umsatz", + titel: "Umsatz", + numerisch: true, + zelle: (k) => {formatEuro(k.umsatzGesamt)}, + sortWert: (k) => k.umsatzGesamt, + }, + { + key: "zahlung", + titel: "Ø Zahlung", + numerisch: true, + zelle: (k) => ( + 30 ? "font-medium" : ""} + style={k.zahlungsmoralTage > 30 ? { color: STATUS_TEXT.warnung } : undefined} + > + {tageLabel(k.zahlungsmoralTage)} + + ), + sortWert: (k) => k.zahlungsmoralTage, + }, + { + key: "letzter", + titel: "Letzter Auftrag", + zelle: (k) => + k.letzterAuftragAm ? ( + formatDatum(k.letzterAuftragAm) + ) : ( + — + ), + sortWert: (k) => k.letzterAuftragAm ?? "", + }, + ]; + + return ( + <> + + + + + + + p.value)} + /> + + + 0 ? "warnung" : "neutral"} + /> + + +
+ + formatZahl(w)} hoehe={250} nurGanzzahlen /> + + + + formatZahl(w)} + hoehe={220} + mitteLabel="Kunden" + /> + +
+ +
+ + formatZahl(w)} + hoehe={200} + mitteLabel="Kunden" + /> + + + + + + +
+ +
+ + s.key !== "zahlung")} + proSeite={6} + suchFelder={(k) => `${k.name} ${k.ort} ${k.quelle}`} + leerText="Alle Kunden waren zuletzt aktiv" + /> + + + + s.key !== "letzter")} + proSeite={6} + suchFelder={(k) => `${k.name} ${k.ort}`} + leerText="Keine Auffälligkeiten" + /> + +
+ +
+ + ); +} diff --git a/features/dashboard/pages/TeamSeite.tsx b/features/dashboard/pages/TeamSeite.tsx new file mode 100644 index 0000000..6b7d5eb --- /dev/null +++ b/features/dashboard/pages/TeamSeite.tsx @@ -0,0 +1,248 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer, Raster } from "../components/layout/PageContainer"; +import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { AuslastungsBalken } from "../components/charts/AuslastungsBalken"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { Donut } from "../components/charts/Donut"; +import { DataTable, type Spalte } from "../components/tables/DataTable"; +import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; +import { teamKennzahlen, type MitarbeiterKennzahl } from "../data/calculations"; +import { formatEuro, formatEuroKompakt, formatProzent, formatStunden, formatZahl } from "../utils/formatters"; +import { imFenster, zeitfenster } from "../utils/dateUtils"; + +export function TeamSeite() { + const { daten, filter } = useAnalyseDaten("team"); + + const kennzahlen = teamKennzahlen( + daten.mitarbeiter, + daten.zeiten, + daten.auftraege, + filter.zeitraum + ); + const aktive = kennzahlen.filter((k) => k.mitarbeiter.status === "aktiv"); + + const durchschnitt = + aktive.length > 0 ? aktive.reduce((s, k) => s + k.auslastungProzent, 0) / aktive.length : 0; + const stundenGesamt = aktive.reduce((s, k) => s + k.stunden, 0); + const produktivGesamt = aktive.reduce((s, k) => s + k.produktivStunden, 0); + const produktivQuote = stundenGesamt > 0 ? (produktivGesamt / stundenGesamt) * 100 : 0; + + const fenster = zeitfenster(filter.zeitraum); + const abwesenheiten = daten.zeiten.filter( + (z) => z.art === "abwesenheit" && imFenster(z.datum, fenster) + ); + const abwesenheitNach = React.useMemo(() => { + const zaehler = new Map(); + const label: Record = { + urlaub: "Urlaub", + krank: "Krank", + fortbildung: "Fortbildung", + }; + for (const eintrag of abwesenheiten) { + const name = label[eintrag.abwesenheitsgrund ?? "sonstige"] ?? "Sonstige"; + zaehler.set(name, (zaehler.get(name) ?? 0) + eintrag.stunden); + } + return [...zaehler.entries()].map(([name, value]) => ({ name, value })); + }, [abwesenheiten]); + + const spalten: Spalte[] = [ + { + key: "name", + titel: "Mitarbeiter", + breite: "26%", + zelle: (k) => ( +
+ + {k.mitarbeiter.kuerzel} + +
+ {k.mitarbeiter.name} + + {k.mitarbeiter.rolle} · {k.mitarbeiter.gewerk} + +
+
+ ), + sortWert: (k) => k.mitarbeiter.name, + }, + { + key: "status", + titel: "Status", + zelle: (k) => + k.mitarbeiter.status === "aktiv" ? ( + + aktiv + + ) : ( + + inaktiv + + ), + sortWert: (k) => k.mitarbeiter.status, + }, + { + key: "stunden", + titel: "Stunden", + numerisch: true, + zelle: (k) => formatStunden(k.stunden), + sortWert: (k) => k.stunden, + }, + { + key: "produktiv", + titel: "davon produktiv", + numerisch: true, + zelle: (k) => formatStunden(k.produktivStunden), + sortWert: (k) => k.produktivStunden, + }, + { + key: "auslastung", + titel: "Auslastung", + numerisch: true, + zelle: (k) => ( + 95 + ? "font-medium text-[#8a3800]" + : k.auslastungProzent < 70 + ? "text-[#00539a]" + : "font-medium text-[#0e6027]" + } + > + {formatProzent(k.auslastungProzent, 0)} + + ), + sortWert: (k) => k.auslastungProzent, + }, + { + key: "abgeschlossen", + titel: "Abgeschlossen", + numerisch: true, + zelle: (k) => formatZahl(k.abgeschlossen), + sortWert: (k) => k.abgeschlossen, + }, + { + key: "umsatz", + titel: "Auftragswert", + numerisch: true, + zelle: (k) => {formatEuro(k.umsatz)}, + sortWert: (k) => k.umsatz, + }, + ]; + + const tabellenZeilen = kennzahlen.map((k) => ({ ...k, id: k.mitarbeiter.id })); + + return ( + <> + + + + + + + 95 ? "warnung" : "positiv"} + /> + + + s + a.stunden, 0))} + zusatz="Urlaub, Krankheit, Fortbildung" + icon="CalendarOff" + richtungGut="negativ" + /> + + + + ({ + name: k.mitarbeiter.name, + wert: k.auslastungProzent, + zusatz: `${k.mitarbeiter.rolle} · ${k.mitarbeiter.gewerk}`, + }))} + maxZeilen={12} + /> + + +
+ + ({ + name: k.mitarbeiter.name, + value: k.abgeschlossen, + zweitwert: k.umsatz, + }))} + formatWert={(w) => formatZahl(w)} + formatZweitwert={formatEuroKompakt} + maxBalken={8} + /> + + + + formatStunden(w)} + hoehe={210} + mitteLabel="Stunden" + /> + +
+ + + `${k.mitarbeiter.name} ${k.mitarbeiter.rolle} ${k.mitarbeiter.gewerk}`} + /> + +
+ + ); +} diff --git a/features/dashboard/pages/UebersichtSeite.tsx b/features/dashboard/pages/UebersichtSeite.tsx new file mode 100644 index 0000000..ef967cc --- /dev/null +++ b/features/dashboard/pages/UebersichtSeite.tsx @@ -0,0 +1,273 @@ +"use client"; + +import React from "react"; +import { PageHeader } from "../components/layout/PageHeader"; +import { PageContainer, Raster } from "../components/layout/PageContainer"; +import { AnalyticsToolbar } from "../components/filters/AnalyticsToolbar"; +import { KPICard } from "../components/cards/KPICard"; +import { ChartCard } from "../components/cards/ChartCard"; +import { AlertList } from "../components/cards/AlertList"; +import { StatZeile } from "../components/cards/StatZeile"; +import { AreaVerlauf } from "../components/charts/AreaVerlauf"; +import { BalkenHorizontal } from "../components/charts/BalkenHorizontal"; +import { AuslastungsBalken } from "../components/charts/AuslastungsBalken"; +import { Trichter } from "../components/charts/Trichter"; +import { ChartLegende } from "../components/charts/ChartLegende"; +import { DataTable, type Spalte } from "../components/tables/DataTable"; +import { AuftragBadge } from "../components/tables/StatusBadge"; +import { useAnalyseDaten } from "../hooks/useAnalyseDaten"; +import { + aktiveAuftraege, + handlungsbedarf, + marge, + neueKunden, + neueKundenVorperiode, + zahlungsdauer, + offenePosten, + teamKennzahlen, + ueberfaelligeAuftraege, + umsatz, + umsatzNachGewerk, + umsatzVerlauf, + umsatzVorperiode, + vertriebsTrichter, +} from "../data/calculations"; +import { + berechneTrend, + formatEuro, + formatEuroAchse, + formatEuroKompakt, + formatProzent, + formatZahl, + tageSeit, + tageLabel, + formatTage, +} from "../utils/formatters"; +import { HEUTE } from "../utils/dateUtils"; +import { PRIMAER_FARBE, VERGLEICH_FARBE, + STATUS_TEXT, +} from "../utils/chartTheme"; +import type { Auftrag } from "../data/types"; + +export function UebersichtSeite() { + const { daten, filter } = useAnalyseDaten("uebersicht"); + + const umsatzAktuell = umsatz(daten.rechnungen, filter.zeitraum); + const umsatzVorher = umsatzVorperiode(daten.rechnungen, filter.zeitraum); + const verlauf = umsatzVerlauf(daten.rechnungen, filter.zeitraum); + const posten = offenePosten(daten.rechnungen); + const aktiv = aktiveAuftraege(daten.auftraege); + const ueberfaellig = ueberfaelligeAuftraege(daten.auftraege); + const team = teamKennzahlen(daten.mitarbeiter, daten.zeiten, daten.auftraege, filter.zeitraum); + const alerts = handlungsbedarf(daten); + + const monteureAktiv = team.filter((t) => t.mitarbeiter.status === "aktiv"); + const durchschnittAuslastung = + monteureAktiv.length > 0 + ? monteureAktiv.reduce((s, t) => s + t.auslastungProzent, 0) / monteureAktiv.length + : 0; + + const sparkline = verlauf.slice(-10).map((p) => p.value); + + const gewerkUmsatz = umsatzNachGewerk(daten.auftraege, daten.rechnungen, filter.zeitraum); + const trichter = vertriebsTrichter(daten.auftraege); + + const auftragSpalten: Spalte[] = [ + { + key: "nummer", + titel: "Auftrag", + breite: "26%", + zelle: (a) => ( +
+ {a.titel} + {a.nummer} +
+ ), + sortWert: (a) => a.nummer, + }, + { + key: "gewerk", + titel: "Gewerk", + zelle: (a) => {a.gewerk}, + sortWert: (a) => a.gewerk, + }, + { key: "status", titel: "Status", zelle: (a) => }, + { + key: "faellig", + titel: "Fällig seit", + numerisch: true, + zelle: (a) => ( + + {tageLabel(tageSeit(a.faelligAm, HEUTE))} + + ), + sortWert: (a) => tageSeit(a.faelligAm, HEUTE), + }, + { + key: "wert", + titel: "Wert", + numerisch: true, + zelle: (a) => formatEuro(a.wert), + sortWert: (a) => a.wert, + }, + ]; + + return ( + <> + + + + + + + 0 ? "warnung" : "neutral"} + /> + s + a.wert, 0))} Auftragsbestand`} + icon="ClipboardList" + bezug="stichtag" + /> + 95 ? "warnung" : durchschnittAuslastung < 70 ? "info" : "positiv"} + /> + k.auftraegeGesamt > 0).length)} Kunden mit Auftrag insgesamt`} + trend={berechneTrend( + neueKunden(daten.kunden, filter.zeitraum), + neueKundenVorperiode(daten.kunden, filter.zeitraum) + )} + icon="Users" + /> + + + + + + +
+ + } + > + + s + a.wert, 0) / daten.auftraege.length + : 0 + ), + }, + { label: "Ø Zahlungsdauer", wert: formatTage(zahlungsdauer(daten.rechnungen, filter.zeitraum)) }, + ]} + /> + + + + + +
+ +
+ + formatZahl(w)} /> + + + + ({ + name: t.mitarbeiter.name, + wert: t.auslastungProzent, + zusatz: t.mitarbeiter.gewerk, + }))} + /> + + +
+ + + `${a.titel} ${a.nummer} ${a.gewerk}`} + leerText="Kein Auftrag ist über Termin — sehr gut." + /> + + +
+ + ); +} diff --git a/features/dashboard/utils/chartTheme.ts b/features/dashboard/utils/chartTheme.ts new file mode 100644 index 0000000..5c5a226 --- /dev/null +++ b/features/dashboard/utils/chartTheme.ts @@ -0,0 +1,285 @@ +/** + * Zentrales Chart-Theme — abgeleitet aus IBM Carbon Data Visualization. + * + * Die Werte stammen aus dem ausgelieferten Stylesheet von @carbon/charts + * (dist/styles.css, Light Theme): + * Gridlines #e0e0e0 (--cds-layer-accent-01) + * Achse #8d8d8d (--cds-border-strong-01) + * Text primär #161616 (--cds-text-primary) + * Text sek. #525252 (--cds-text-secondary) + * Tooltip #393939 (--cds-background-inverse) auf #ffffff Text + * + * Warum kein bunter "Marketing"-Farbsatz: Die Carbon-Palette ist auf + * Unterscheidbarkeit und Kontrast optimiert und in dieser Reihenfolge + * getestet. Reihenfolge deshalb nie umsortieren, nur von vorne nehmen. + * + * Siehe docs/DESIGN-SYSTEM.md. + */ + +/** Carbon "14-color" categorical palette, Light Theme, in Originalreihenfolge. */ +export const KATEGORIE_FARBEN = [ + "#6929c4", // purple 70 + "#1192e8", // cyan 50 + "#005d5d", // teal 70 + "#9f1853", // magenta 70 + "#fa4d56", // red 50 + "#520408", // red 100 + "#198038", // green 60 + "#002d9c", // blue 80 + "#ee5396", // magenta 50 + "#b28600", // yellow 50 + "#009d9a", // teal 50 + "#012749", // cyan 90 + "#8a3800", // orange 70 + "#a56eff", // purple 50 +] as const; + +/** + * Carbon empfiehlt maximal 5 gleichzeitig unterscheidbare Kategorien. + * Alles darüber wird in "Sonstige" zusammengefasst (siehe gruppiereRest()). + */ +export const MAX_KATEGORIEN = 5; + +/** Ampelfarben für Status-Aussagen (nicht für Kategorien verwenden). */ +export const STATUS_FARBEN = { + positiv: "#198038", + warnung: "#b28600", + negativ: "#da1e28", + neutral: "#8d8d8d", + info: "#1192e8", +} as const; + +/** + * Feste Farbe je Gewerk. + * + * Eine Kategorie behält ihre Farbe über alle Charts und Seiten hinweg — + * sonst ist Heizung im Ring lila und im Balkendiagramm blau, und man muss + * jede Legende neu lesen. Die Werte stammen aus der Carbon-Palette. + */ +export const GEWERK_FARBEN: Record = { + Heizung: "#6929c4", + Sanitär: "#1192e8", + "Klima & Lüftung": "#005d5d", + Photovoltaik: "#9f1853", + Elektro: "#198038", +}; + +/** + * Feste Farben für Statuswerte. + * + * Hier trägt die Farbe Bedeutung: „Verloren" und „Überfällig" sind rot, + * „Bezahlt" und „In Arbeit" grün, Neutrales grau. Eine zufällige + * Palettenfarbe für „Verloren" würde die Aussage der Grafik verwässern. + */ +export const STATUS_KATEGORIE_FARBEN: Record = { + // Auftragsstatus + Anfrage: "#a56eff", + "Vor-Aufmaß": "#1192e8", + Angebot: "#6929c4", + Beauftragt: "#009d9a", + "In Arbeit": "#198038", + Abgeschlossen: "#005d5d", + Verloren: "#fa4d56", + // Rechnungsstatus + Bezahlt: "#198038", + Gestellt: "#1192e8", + Überfällig: "#da1e28", + Entwurf: "#8d8d8d", + // Förderung + Bewilligt: "#198038", + Beantragt: "#b28600", + "Nicht beantragt": "#8d8d8d", + // Abwesenheit + Urlaub: "#1192e8", + Krank: "#fa4d56", + Fortbildung: "#6929c4", + // Sammelposten + Sonstige: "#8d8d8d", +}; + +/** + * Textfarben für Statusaussagen in Tabellen und Listen. + * + * Nicht dieselben Werte wie STATUS_FARBEN: Flächen dürfen heller sein, Text + * braucht auf Weiß mehr Kontrast (WCAG AA, 4,5:1 bei 12–13 px). Deshalb je + * eine Stufe dunkler aus derselben Carbon-Familie. + */ +export const STATUS_TEXT = { + negativ: "#a2191f", // red 70 + warnung: "#8a3800", // orange 70 + positiv: "#0e6027", // green 70 +} as const; + +/** + * Vierstufige Risikorampe für geordnete Dimensionen (Forderungsalter). + * + * Durchgehend Statusfarben aus der Carbon-Skala, keine Palettenfarbe + * dazwischen: Altersklassen sind eine Reihenfolge mit wachsendem Risiko, + * keine unabhängigen Kategorien. + */ +export const RISIKO_RAMPE = [ + "#198038", // green 60 — im Zahlungsziel + "#b28600", // yellow 50 — beobachten + "#ba4e00", // orange 60 — mahnen + "#da1e28", // red 60 — Abschreibungsrisiko +] as const; + +/** + * Badge-Töne: helle Fläche mit dunklem Text derselben Carbon-Familie. + * Alle Paare erreichen auf ihrer Fläche mindestens 7:1 — Badges sind klein + * (11 px) und brauchen mehr Reserve als Fließtext. + */ +export const BADGE_TON = { + neutral: "bg-neutral-100 text-neutral-700", + information: "bg-[#e5f6ff] text-[#00539a]", + lila: "bg-[#e8daff] text-[#491d8b]", + teal: "bg-[#d9fbfb] text-[#005d5d]", + positiv: "bg-[#defbe6] text-[#0e6027]", + negativ: "bg-[#fff1f1] text-[#a2191f]", +} as const; + +/** Einzelserie (Umsatzverlauf o. Ä.) — Carbon Cyan 50. */ +export const PRIMAER_FARBE = "#1192e8"; +/** Vergleichsserie (Vorjahr) — bewusst neutral, damit sie zurücktritt. */ +export const VERGLEICH_FARBE = "#8d8d8d"; + +export const CHART = { + grid: "#e0e0e0", + achse: "#8d8d8d", + text: "#161616", + textSekundaer: "#525252", + tooltipBg: "#393939", + tooltipFg: "#ffffff", +} as const; + +/** + * Chart-Innenabstände. + * Carbon rechnet Achsenbeschriftungen in den Chart-Rahmen ein; die Karte selbst + * liefert bereits 16 px Padding, deshalb hier nur der Platz für Achsen. + */ +export const MARGIN = { + /** Vertikale Balken / Flächen mit Y-Achse links. + * Rechts genug Platz, damit die letzte Achsenbeschriftung nicht abschneidet. */ + standard: { top: 8, right: 24, bottom: 4, left: 4 }, + /** Horizontale Balken: links Platz für Kategorienamen. */ + horizontal: { top: 4, right: 56, bottom: 4, left: 4 }, + /** Sparkline / Mini-Chart ohne Achsen. */ + kompakt: { top: 2, right: 2, bottom: 2, left: 2 }, +} as const; + +/** Typografie im Chart (Carbon: 12 px Labels, 14 px Titel). */ +export const CHART_FONT = { + achse: 12, + label: 12, + legende: 12, + titel: 14, +} as const; + +/** Gemeinsame Achsen-Props für Recharts (Carbon-Look: keine Ticks, dünne Achse). */ +export const achseX = { + tickLine: false, + axisLine: { stroke: CHART.achse, strokeWidth: 1 }, + tick: { fill: CHART.textSekundaer, fontSize: CHART_FONT.achse }, + dy: 4, +} as const; + +export const achseY = { + tickLine: false, + axisLine: false, + tick: { fill: CHART.textSekundaer, fontSize: CHART_FONT.achse }, + // 58 px laut docs/DESIGN-SYSTEM.md §2 — reicht für "245 T€". + // Werte im Mio.-Bereich bekommen über achsenFormat() eine kurze Einheit. + width: 58, +} as const; + +/** Balkenbreiten nach Carbon: Abstand zwischen Balken ≥ halbe Balkenbreite. */ +export const BALKEN = { + maxBreite: 48, + /** Recharts-Kategorieabstand in Prozent der Kategoriebreite. */ + kategorieAbstand: "28%", + radius: 2, + hoeheHorizontal: 20, +} as const; + +/** Donut-Proportionen (Carbon: kräftiger Ring, Gesamtwert in der Mitte). */ +export const DONUT = { + innenRadius: "62%", + aussenRadius: "88%", + segmentAbstand: 2, +} as const; + +/** + * Reduziert eine Kategorienliste auf MAX_KATEGORIEN und fasst den Rest + * zu "Sonstige" zusammen — verhindert unlesbare Donuts mit 12 Segmenten. + */ +export function gruppiereRest( + daten: T[], + max: number = MAX_KATEGORIEN +): { name: string; value: number }[] { + const sortiert = [...daten].sort((a, b) => b.value - a.value); + if (sortiert.length <= max) return sortiert.map((d) => ({ name: d.name, value: d.value })); + + const kopf = sortiert.slice(0, max - 1).map((d) => ({ name: d.name, value: d.value })); + const rest = sortiert.slice(max - 1).reduce((summe, d) => summe + d.value, 0); + // "Sonstige" wird einsortiert, nicht angehaengt: der Ring laeuft sonst nicht + // mehr absteigend, obwohl DESIGN-SYSTEM.md §4 genau das verlangt. + return [...kopf, { name: "Sonstige", value: rest }].sort((a, b) => b.value - a.value); +} + +/** + * Rundes Achsenraster. + * + * Recharts leitet Ticks aus max/(n-1) ab und rundet den Rohschritt auf — bei + * 219 T€ ergibt das 0/55/110/165/220. Krumme Achsenwerte kosten den Leser + * bei jedem Blick eine Rechenoperation. Deshalb Schrittweiten aus der + * 1-2-2,5-5-Reihe erzwingen. + */ +export function nettesRaster( + max: number, + maxIntervalle = 5 +): { domain: [number, number]; ticks: number[] } { + if (!Number.isFinite(max) || max <= 0) return { domain: [0, 1], ticks: [0, 1] }; + const groessenordnung = 10 ** Math.floor(Math.log10(max)); + const kandidaten = [1, 2, 2.5, 5, 10] + .flatMap((k) => [(k * groessenordnung) / 10, k * groessenordnung, k * groessenordnung * 10]) + .sort((a, b) => a - b); + for (const schritt of kandidaten) { + const anzahl = Math.ceil(max / schritt); + if (anzahl <= maxIntervalle) { + return { + domain: [0, schritt * anzahl], + ticks: Array.from({ length: anzahl + 1 }, (_, i) => i * schritt), + }; + } + } + return { domain: [0, max], ticks: [0, max] }; +} + +/** + * Ab wie vielen Datenpunkten nur noch jede n-te Beschriftung gezeigt wird. + * Eine Konstante, damit Flaeche und Balken nicht unterschiedlich ausduennen. + */ +export const MAX_ACHSENLABELS = 12; + +/** Farbe nach Index, rotiert sicher innerhalb der Palette. */ +export function farbe(index: number): string { + return KATEGORIE_FARBEN[index % KATEGORIE_FARBEN.length]; +} + +/** + * Farbe für einen Kategorienamen: feste Zuordnung wenn vorhanden, + * sonst Position in der Palette. + */ +export function farbeFuer(name: string, index: number): string { + return GEWERK_FARBEN[name] ?? STATUS_KATEGORIE_FARBEN[name] ?? farbe(index); +} + +/** Abgestufte Einfärbung für Trichterstufen — eine Farbe, abnehmende Sättigung. */ +export function trichterFarbe(index: number, gesamt: number): string { + const anteil = gesamt > 1 ? index / (gesamt - 1) : 0; + // Von Carbon Purple 70 nach Purple 40: gleiche Farbfamilie, klare Reihenfolge. + const start = { r: 0x69, g: 0x29, b: 0xc4 }; + const ende = { r: 0xbe, g: 0x95, b: 0xff }; + const misch = (a: number, b: number) => Math.round(a + (b - a) * anteil); + return `rgb(${misch(start.r, ende.r)} ${misch(start.g, ende.g)} ${misch(start.b, ende.b)})`; +} diff --git a/features/dashboard/utils/dateUtils.ts b/features/dashboard/utils/dateUtils.ts new file mode 100644 index 0000000..624aec9 --- /dev/null +++ b/features/dashboard/utils/dateUtils.ts @@ -0,0 +1,132 @@ +import type { ZeitraumPreset } from "../data/types"; + +/** + * Fester "Heute"-Bezugspunkt des Prototyps. + * + * Wichtig: Die Dummy-Daten sind deterministisch relativ zu diesem Datum erzeugt. + * Dadurch bleiben alle Zahlen ueber Reloads und Screenshots hinweg identisch. + * In der echten Software wird das durch `new Date()` ersetzt. + */ +export const HEUTE = new Date("2026-07-30T12:00:00"); + +export interface Zeitfenster { + von: Date; + bis: Date; +} + +export function addTage(datum: Date, tage: number): Date { + const d = new Date(datum); + d.setDate(d.getDate() + tage); + return d; +} + +export function addMonate(datum: Date, monate: number): Date { + const d = new Date(datum); + d.setMonth(d.getMonth() + monate); + return d; +} + +export function toISO(datum: Date): string { + return datum.toISOString().slice(0, 10); +} + +/** Zeitfenster eines Presets, bezogen auf HEUTE. */ +export function zeitfenster(preset: ZeitraumPreset): Zeitfenster { + const bis = new Date(HEUTE); + bis.setHours(23, 59, 59, 999); + + switch (preset) { + case "heute": { + const von = new Date(HEUTE); + von.setHours(0, 0, 0, 0); + return { von, bis }; + } + case "7_tage": + return { von: addTage(bis, -7), bis }; + case "30_tage": + return { von: addTage(bis, -30), bis }; + case "quartal": + return { von: addMonate(bis, -3), bis }; + case "jahr": + return { von: addMonate(bis, -12), bis }; + case "alles": + default: + return { von: new Date("2000-01-01"), bis }; + } +} + +/** Gleich langes Fenster direkt davor — Basis fuer Trend-Vergleiche. */ +export function vorperiode(preset: ZeitraumPreset): Zeitfenster { + const { von, bis } = zeitfenster(preset); + const dauer = bis.getTime() - von.getTime(); + return { + von: new Date(von.getTime() - dauer), + bis: new Date(von.getTime() - 1), + }; +} + +export function imFenster(iso: string | null, fenster: Zeitfenster): boolean { + if (!iso) return false; + const t = new Date(iso).getTime(); + return t >= fenster.von.getTime() && t <= fenster.bis.getTime(); +} + +/** ISO-Kalenderwoche als sortierbarer Schluessel: "2026-W31". */ +export function isoWoche(datum: Date): string { + const d = new Date(Date.UTC(datum.getFullYear(), datum.getMonth(), datum.getDate())); + // Donnerstag derselben Woche bestimmt nach ISO 8601 Jahr und Wochennummer. + d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7)); + const jahresstart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const woche = Math.ceil(((d.getTime() - jahresstart.getTime()) / 86_400_000 + 1) / 7); + return `${d.getUTCFullYear()}-W${String(woche).padStart(2, "0")}`; +} + +/** "2026-W31" -> "KW 31" */ +export function formatWoche(schluessel: string): string { + return `KW ${schluessel.slice(6)}`; +} + +/** + * Passende Buckets fuer eine Zeitreihe. + * + * Die Aufloesung folgt der Frage, nicht dem Kalender: Bei rund zwei Vorgaengen + * pro Tag ist eine Tagesreihe ueber 30 Tage Rauschen, kein Verlauf. Deshalb + * Tage nur bis eine Woche, danach Kalenderwochen, ab einem Jahr Monate. + */ +export function bucketsFuer(preset: ZeitraumPreset): { + art: "tag" | "woche" | "monat"; + schluessel: string[]; +} { + const { von, bis } = zeitfenster(preset); + + if (preset === "heute" || preset === "7_tage") { + const schluessel: string[] = []; + for (let d = new Date(von); d <= bis; d = addTage(d, 1)) schluessel.push(toISO(d)); + return { art: "tag", schluessel }; + } + + if (preset === "30_tage" || preset === "quartal") { + const schluessel: string[] = []; + for (let d = new Date(von); d <= bis; d = addTage(d, 7)) { + const key = isoWoche(d); + if (!schluessel.includes(key)) schluessel.push(key); + } + const letzte = isoWoche(bis); + if (!schluessel.includes(letzte)) schluessel.push(letzte); + return { art: "woche", schluessel }; + } + + const monate: string[] = []; + const start = preset === "alles" ? addMonate(bis, -23) : von; + for (let d = new Date(start.getFullYear(), start.getMonth(), 1); d <= bis; d = addMonate(d, 1)) { + monate.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`); + } + return { art: "monat", schluessel: monate }; +} + +/** Bucket-Schluessel eines Datums fuer die gewaehlte Aufloesung. */ +export function bucketKey(iso: string, art: "tag" | "woche" | "monat"): string { + if (art === "monat") return iso.slice(0, 7); + if (art === "woche") return isoWoche(new Date(iso)); + return iso.slice(0, 10); +} diff --git a/features/dashboard/utils/formatters.ts b/features/dashboard/utils/formatters.ts new file mode 100644 index 0000000..fa8d196 --- /dev/null +++ b/features/dashboard/utils/formatters.ts @@ -0,0 +1,158 @@ +/** + * Deutsche Zahlen-, Waehrungs- und Datumsformatierung. + * Regeln aus UI_UX_GUIDELINES.md (Abschnitt 3, Zahlenformatierung): + * Punkt als Tausender-Trenner, Komma als Dezimaltrenner, grosse Zahlen abkuerzen. + */ + +const NF = new Intl.NumberFormat("de-DE"); + +/** 12450 -> "12.450" · 1250000 -> "1,3 Mio." */ +export function formatZahl(value: number, dezimalstellen = 0): string { + if (Math.abs(value) >= 1_000_000) { + return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio.`; + } + return new Intl.NumberFormat("de-DE", { + minimumFractionDigits: dezimalstellen, + maximumFractionDigits: dezimalstellen, + }).format(value); +} + +/** 8420 -> "8.420 €" · 245000 -> "245.000 €" (immer voll, fuer KPI-Werte) */ +export function formatEuro(value: number): string { + return `${NF.format(Math.round(value))} €`; +} + +/** + * Kompakte Waehrung fuer Achsen und Balken-Labels. + * 8420 -> "8,4 T€" · 245000 -> "245 T€" · 1250000 -> "1,25 Mio. €" + */ +export function formatEuroKompakt(value: number): string { + const abs = Math.abs(value); + if (abs >= 1_000_000) { + return `${(value / 1_000_000).toFixed(2).replace(".", ",")} Mio. €`; + } + if (abs >= 1_000) { + return `${(value / 1_000).toFixed(1).replace(".", ",")} T€`; + } + return `${NF.format(Math.round(value))} €`; +} + +/** + * Noch kompakter für Achsenbeschriftungen: dort zählt jede Stelle, + * und Nachkommastellen sind bei einer Skala ohnehin überflüssig. + * 34000 -> "34 T€" · 1250000 -> "1,3 Mio. €" + */ +export function formatEuroAchse(value: number): string { + // Die Nulllinie traegt keine Einheit: "0 €" neben "10 T€" liest sich wie ein + // Einheitenwechsel mitten in der Skala. + if (value === 0) return "0"; + const abs = Math.abs(value); + if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`; + if (abs >= 1_000) return `${Math.round(value / 1_000)} T€`; + return `${NF.format(Math.round(value))} €`; +} + +/** + * Achsenformat, einmal aus dem Maximum gewaehlt. + * + * formatEuroAchse entscheidet die Einheit pro Wert — dadurch steht "0 €" + * neben "10 T€". Eine Achse traegt aber genau eine Einheit. + */ +export function achsenFormat(max: number): (wert: number) => string { + if (max >= 1_000_000) { + return (wert) => + wert === 0 ? "0" : `${(wert / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`; + } + if (max >= 1_000) { + return (wert) => (wert === 0 ? "0" : `${NF.format(Math.round(wert / 1_000))} T€`); + } + return (wert) => (wert === 0 ? "0" : formatEuro(wert)); +} + +/** 45.32 -> "45,3 %" */ +export function formatProzent(value: number, dezimalstellen = 1): string { + return `${value.toFixed(dezimalstellen).replace(".", ",")} %`; +} + +const NF1 = new Intl.NumberFormat("de-DE", { + minimumFractionDigits: 1, + maximumFractionDigits: 1, +}); + +/** 2.4 -> "2,4 Tage" · 1234.5 -> "1.234,5 Tage" */ +export function formatTage(value: number): string { + return `${NF1.format(value)} Tage`; +} + +/** 2812.5 -> "2.812,5 h" */ +export function formatStunden(value: number): string { + return `${NF1.format(value)} h`; +} + +/** + * Ganze Tage mit korrektem Numerus — "1 Tag", "3 Tage". + * Ohne das steht in der Terminspalte "1 Tage ueber". + */ +export function tageLabel(anzahl: number, suffix = ""): string { + const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tage"; + return `${NF.format(anzahl)} ${wort}${suffix ? ` ${suffix}` : ""}`; +} + +/** Dativform fuer "seit …": "seit 1 Tag" / "seit 99 Tagen". */ +export function tageLabelDativ(anzahl: number): string { + const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tagen"; + return `${NF.format(anzahl)} ${wort}`; +} + +/** "2026-07-30" -> "30.07.2026" */ +export function formatDatum(iso: string): string { + const d = new Date(iso); + return d.toLocaleDateString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); +} + +/** "2026-07-30" -> "30.07." */ +export function formatDatumKurz(iso: string): string { + const d = new Date(iso); + return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" }); +} + +/** "2026-07" -> "Jul 26" */ +export function formatMonatKurz(iso: string): string { + const [jahr, monat] = iso.split("-"); + const namen = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; + return `${namen[Number(monat) - 1]} ${jahr.slice(2)}`; +} + +/** Tage zwischen heute und einem ISO-Datum (positiv = in der Vergangenheit). */ +export function tageSeit(iso: string, referenz: Date): number { + const diff = referenz.getTime() - new Date(iso).getTime(); + return Math.floor(diff / 86_400_000); +} + +/** + * Trend zwischen aktueller und vorheriger Periode. + * Gibt Prozentpunkte und Richtung zurueck; < 0,5 % gilt als unveraendert. + */ +export function berechneTrend( + aktuell: number, + vorher: number +): { wert: number; richtung: "up" | "down" | "flat" } { + if (vorher === 0) { + return aktuell > 0 ? { wert: 100, richtung: "up" } : { wert: 0, richtung: "flat" }; + } + const veraenderung = ((aktuell - vorher) / vorher) * 100; + if (Math.abs(veraenderung) < 0.5) return { wert: 0, richtung: "flat" }; + return { + wert: Math.abs(veraenderung), + richtung: veraenderung > 0 ? "up" : "down", + }; +} + +/** Text auf maxLaenge kuerzen, Ellipse anhaengen. */ +export function kuerze(text: string, maxLaenge: number): string { + return text.length <= maxLaenge ? text : `${text.slice(0, maxLaenge - 1)}…`; +} diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..e35fb43 --- /dev/null +++ b/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + // Das Entwicklungs-Abzeichen unten links überlagert die Seitenleiste und + // stört bei Screenshots für die Abstimmung. + devIndicators: false, +}; + +export default nextConfig; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a3a2179 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8465 @@ +{ + "name": "hivegrid-dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hivegrid-dashboard", + "version": "0.1.0", + "dependencies": { + "@subframe/core": "^1.155.0", + "clsx": "^2.1.1", + "next": "16.2.6", + "react": "19.2.4", + "react-dom": "19.2.4", + "recharts": "^2.15.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "peer": true + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@next/env": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT", + "peer": true + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.7.tgz", + "integrity": "sha512-CtXP35dxaB5T3zXSd+E3uHe/QpXcpYnZmxp6OaIbfthtfW4wyb77M23BG+bwIJDtsMwEP/YssdsmNyZu7jhWew==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.23.tgz", + "integrity": "sha512-H8qONfZd3ltrU3+jHCIgITbWo6e1iTKvP9DHdrvYbX48ooRM5FjEDTn16AMwdfuOGkWdZEhpl3PLL/Wk/AnHDQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.7.tgz", + "integrity": "sha512-cgYFEkntCxppHZgtSZ+7vh0wbZQ+IC7PPMw8DSnRG27B6kDd32/Zw0OJt7dGDigCoprMuWHjg2PvUn3PYvPFoQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.7.tgz", + "integrity": "sha512-mTSLf1GC/C0moWjTbvCM6Qn/gBjvlFt1azuWF2v7MN5C3Zq2U2J2lN3ZEYkpujuOU5Ro7A28wkviSxaKnG0BYg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT", + "peer": true + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@subframe/core": { + "version": "1.155.0", + "resolved": "https://registry.npmjs.org/@subframe/core/-/core-1.155.0.tgz", + "integrity": "sha512-6c0Un5VB2zLp/6mDRokLCDzV86Z6XElV0JSnJ0TXTfZYg7EqLSYwuPnYU47ittTcgMF8SyONSTGVaCKj0o6q8A==", + "license": "ISC", + "dependencies": { + "react-day-picker": "^9.0.4", + "react-virtualized-auto-sizer": "^1.0.25", + "recharts": "^2.15.1", + "tailwind-merge": "^2.3.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.13.0" + }, + "peerDependencies": { + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.2", + "@radix-ui/react-context-menu": "^2.2.10", + "@radix-ui/react-dialog": "^1.1.10", + "@radix-ui/react-dropdown-menu": "^2.1.10", + "@radix-ui/react-hover-card": "^1.1.10", + "@radix-ui/react-popover": "^1.1.10", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-slider": "^1.2.1", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.1", + "@radix-ui/react-toggle-group": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@radix-ui/react-visually-hidden": "^1.1.1", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "sonner": "^1.7.2 || ^2.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.3.tgz", + "integrity": "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "postcss": "^8.5.16", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", + "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT", + "peer": true + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "peer": true, + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT", + "peer": true + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.415", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", + "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.4.0.tgz", + "integrity": "sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.2.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz", + "integrity": "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", + "integrity": "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", + "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", + "date-fns": "^4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", + "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.2", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonner": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.8.tgz", + "integrity": "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT", + "peer": true + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee7c366 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "hivegrid-dashboard", + "version": "0.1.0", + "private": true, + "description": "HiveGrid Analytics Dashboard - UI/UX Prototyp (SHK/Handwerk) zur Uebergabe an die Entwicklung", + "scripts": { + "dev": "next dev -p 4300", + "build": "next build", + "start": "next start -p 4300", + "lint": "eslint" + }, + "dependencies": { + "@subframe/core": "^1.155.0", + "clsx": "^2.1.1", + "next": "16.2.6", + "react": "19.2.4", + "react-dom": "19.2.4", + "recharts": "^2.15.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b713c25 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/ui/components/Accordion/Accordion.tsx b/ui/components/Accordion/Accordion.tsx new file mode 100644 index 0000000..51fdd2f --- /dev/null +++ b/ui/components/Accordion/Accordion.tsx @@ -0,0 +1,157 @@ +"use client"; +/* + * Documentation: + * Accordion — https://app.subframe.com/f023064a5ca0/library?component=Accordion_d2e81e20-863a-4027-826a-991d8910efd9 + */ + +import React from "react"; +import { FeatherChevronRight } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ChevronProps + extends React.ComponentProps { + className?: string; +} + +const Chevron = React.forwardRef< + React.ElementRef, + ChevronProps +>(function Chevron({ className, ...otherProps }: ChevronProps, ref) { + return ( + + + + ); +}); + +export interface ContentProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Content = React.forwardRef(function Content( + { children, className, ...otherProps }: ContentProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface TriggerProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Trigger = React.forwardRef(function Trigger( + { children, className, ...otherProps }: TriggerProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface AccordionRootProps + extends React.ComponentProps { + disabled?: boolean; + error?: boolean; + open?: boolean; + variant?: "card" | "ghost"; + trigger?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const AccordionRoot = React.forwardRef( + function AccordionRoot( + { + disabled = false, + error = false, + open, + variant = "card", + trigger, + children, + className, + defaultOpen = false, + ...otherProps + }: AccordionRootProps, + ref + ) { + return ( + +
+ + {trigger ? ( +
+ {trigger} +
+ ) : null} +
+ + {children ? ( +
+ {children} +
+ ) : null} +
+
+
+ ); + } +); + +export const Accordion = Object.assign(AccordionRoot, { + Chevron, + Content, + Trigger, +}); diff --git a/ui/components/Accordion/index.tsx b/ui/components/Accordion/index.tsx new file mode 100644 index 0000000..2c3435e --- /dev/null +++ b/ui/components/Accordion/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Accordion as AccordionComponent } from "./Accordion"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Accordion = AccordionComponent; diff --git a/ui/components/AccordionNested/AccordionNested.tsx b/ui/components/AccordionNested/AccordionNested.tsx new file mode 100644 index 0000000..4aca169 --- /dev/null +++ b/ui/components/AccordionNested/AccordionNested.tsx @@ -0,0 +1,158 @@ +"use client"; +/* + * Documentation: + * Accordion Nested — https://app.subframe.com/f023064a5ca0/library?component=Accordion+Nested_91a645f4-5a9c-4c61-be00-d85b9d554e91 + */ + +import React from "react"; +import { FeatherChevronRight } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ChevronProps + extends React.ComponentProps { + className?: string; +} + +const Chevron = React.forwardRef< + React.ElementRef, + ChevronProps +>(function Chevron({ className, ...otherProps }: ChevronProps, ref) { + return ( + + + + ); +}); + +export interface ContentProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Content = React.forwardRef(function Content( + { children, className, ...otherProps }: ContentProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface TriggerProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Trigger = React.forwardRef(function Trigger( + { children, className, ...otherProps }: TriggerProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface AccordionNestedRootProps + extends React.ComponentProps { + disabled?: boolean; + error?: boolean; + open?: boolean; + variant?: "card" | "ghost"; + trigger?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const AccordionNestedRoot = React.forwardRef< + HTMLDivElement, + AccordionNestedRootProps +>(function AccordionNestedRoot( + { + disabled = false, + error = false, + open, + variant = "card", + trigger, + children, + className, + defaultOpen = false, + ...otherProps + }: AccordionNestedRootProps, + ref +) { + return ( + +
+ + {trigger ? ( +
+ {trigger} +
+ ) : null} +
+ + {children ? ( +
+ {children} +
+ ) : null} +
+
+
+ ); +}); + +export const AccordionNested = Object.assign(AccordionNestedRoot, { + Chevron, + Content, + Trigger, +}); diff --git a/ui/components/AccordionNested/index.tsx b/ui/components/AccordionNested/index.tsx new file mode 100644 index 0000000..528dd8b --- /dev/null +++ b/ui/components/AccordionNested/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { AccordionNested as AccordionNestedComponent } from "./AccordionNested"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const AccordionNested = AccordionNestedComponent; diff --git a/ui/components/AddressDialog/AddressDialog.md b/ui/components/AddressDialog/AddressDialog.md new file mode 100644 index 0000000..44a0df4 --- /dev/null +++ b/ui/components/AddressDialog/AddressDialog.md @@ -0,0 +1,28 @@ +## Wann verwenden + +Überall, wo eine postalische Adresse erfasst oder bearbeitet wird (Kontakt, Projekt, Lieferadresse, Baustelle, Rechnungsadresse). Immer diese Komponente statt eines eigenen Adressformulars — nur so bleibt Feldreihenfolge und Beschriftung im ganzen Produkt identisch. + +## Feldreihenfolge ist nicht verhandelbar + +Die Reihenfolge folgt DIN 5008 / DHL-Adresslogik, also der Reihenfolge, in der eine Adresse auf ein Label gedruckt wird: Firma → Name → Straße + Nr. → Adresszusatz → PLZ + Ort → Land. Nutzer tippen Adressen als zusammenhängenden Block ab; jede Umsortierung erzeugt Lesefehler und Zahlendreher. **Land steht immer zuletzt**, nie oben. + +PLZ steht links vor dem Ort (DE/AT/CH-Konvention), Hausnummer rechts nach der Straße. Beide Paare bleiben in einer Zeile, damit sie als eine Information gelesen werden. + +## Pflicht vs. optional + +- Fachlich nötig für Versand und Routing: Name (oder Firma), Straße, Nr., PLZ, Ort, Land. +- Optional und im Platzhalter als „(optional)" markiert: Firma, Adresszusatz. Adresszusatz nie für Straßendaten missbrauchen — er ist für Etage, Gebäude, c/o. + +## Notiz zur Adresse + +Interne Zustellinformation, kein Adressbestandteil: Zufahrt, Schlüsselübergabe, Parksituation, Torcode. Deshalb sitzt sie **unter der Trennlinie**, außerhalb des Adressblocks, und der Hilfetext sagt explizit, dass sie nicht auf Versandlabels gedruckt wird. Diese Trennung nicht auflösen und die Notiz nicht nach oben ziehen. + +## Props im Einsatz + +- `hideAddressType`: einschalten, wenn der Kontext den Typ schon festlegt (z. B. Dialog heißt „Rechnungsadresse ändern"). Doppelte Angabe derselben Information vermeiden. +- `hideNote`: nur einschalten, wo keine Zustellung stattfindet (z. B. reine Rechnungsadresse). +- `title` / `subtitle`: Titel benennt die Aktion („Adresse hinzufügen", „Lieferadresse bearbeiten"), der Untertitel sagt, wofür die Daten verwendet werden — nicht mit Bedienhinweisen füllen. + +## Beschriftung + +Labels bleiben kurz und in Nutzersprache („Nr.", nicht „Hausnummer/Zusatz"), Platzhalter zeigen ein echtes Beispiel („10115", „12a") statt einer Formatanweisung. Keine Sternchen für Pflichtfelder — optionale Felder werden markiert, nicht umgekehrt. \ No newline at end of file diff --git a/ui/components/AddressDialog/AddressDialog.tsx b/ui/components/AddressDialog/AddressDialog.tsx new file mode 100644 index 0000000..2b8591c --- /dev/null +++ b/ui/components/AddressDialog/AddressDialog.tsx @@ -0,0 +1,478 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Address Dialog — https://app.subframe.com/f023064a5ca0/library?component=Address+Dialog_b3a6c4d9-a908-4acc-881c-132a867b5ef6 + * Avatar — https://app.subframe.com/f023064a5ca0/library?component=Avatar_bec25ae6-5010-4485-b46b-cf79e3943ab2 + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Checkbox — https://app.subframe.com/f023064a5ca0/library?component=Checkbox_3816e3b5-c48c-499b-b45e-0777c6972523 + * Dialog — https://app.subframe.com/f023064a5ca0/library?component=Dialog_ca59db17-43fb-4247-8094-3c55162e902d + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + * Icon with Background — https://app.subframe.com/f023064a5ca0/library?component=Icon+with+Background_4a18e9fc-cea3-46ca-8c53-2412462342ad + * Search Input — https://app.subframe.com/f023064a5ca0/library?component=Search+Input_75a35ed3-52c0-4435-8586-e390eb77d723 + * Select — https://app.subframe.com/f023064a5ca0/library?component=Select_bb88f90b-8c43-4b73-9c2f-3558ce7838f3 + * Text Area — https://app.subframe.com/f023064a5ca0/library?component=Text+Area_4ec05ee8-8f1c-46b2-b863-5419aa7f5cce + * Text Input — https://app.subframe.com/f023064a5ca0/library?component=Text+Input_be48ca43-f8e7-4c0e-8870-d219ea11abfe + */ + +import React from "react"; +import { FeatherArrowRight } from "@subframe/core"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherMapPin } from "@subframe/core"; +import { FeatherUserSearch } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Avatar } from "../Avatar"; +import { Button } from "../Button"; +import { Checkbox } from "../Checkbox"; +import { Dialog } from "../Dialog"; +import { IconButton } from "../IconButton"; +import { IconWithBackground } from "../IconWithBackground"; +import { SearchInput } from "../SearchInput"; +import { Select } from "../Select"; +import { TextArea } from "../TextArea"; +import { TextInput } from "../TextInput"; + +export interface AddressDialogRootProps + extends React.ComponentProps { + hideAddressType?: boolean; + hideNote?: boolean; + hideCompany?: boolean; + hideDefaultAddress?: boolean; + hideImport?: boolean; + title?: React.ReactNode; + subtitle?: React.ReactNode; + className?: string; +} + +const AddressDialogRoot = React.forwardRef< + React.ElementRef, + AddressDialogRootProps +>(function AddressDialogRoot( + { + hideAddressType = false, + hideNote = false, + hideCompany = false, + hideDefaultAddress = false, + hideImport = false, + title, + subtitle, + className, + ...otherProps + }: AddressDialogRootProps, + ref +) { + return ( + + +
+
+
+ } + /> +
+ {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+
+ } /> +
+
+
+
+
+ + Aus Kontakt übernehmen + + + Adressdaten aus einem bestehenden Kontakt vorbefüllen + +
+ + + + + + +
+
+ + + +
+
+
+
+ MB +
+ + Markus Bauer + + + Bauer Elektrotechnik GmbH · Berlin + +
+
+
+ SK +
+ + Sandra Keller + + + Keller Immobilien · Potsdam + +
+
+
+ JR +
+ + Jonas Richter + + + Richter Bau AG · Hamburg + +
+
+
+ EV +
+ + Elena Vogt + + + — · München + +
+
+
+
+
+ + Alle Kontakte durchsuchen + + +
+
+ + + +
+
+
+ + + + + + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+ +
+ +
+
+ + +
+
+ +
+ ); +}); + +export const AddressDialog = AddressDialogRoot; diff --git a/ui/components/AddressDialog/index.tsx b/ui/components/AddressDialog/index.tsx new file mode 100644 index 0000000..adec4c6 --- /dev/null +++ b/ui/components/AddressDialog/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { AddressDialog as AddressDialogComponent } from "./AddressDialog"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const AddressDialog = AddressDialogComponent; diff --git a/ui/components/Alert/Alert.tsx b/ui/components/Alert/Alert.tsx new file mode 100644 index 0000000..633a90f --- /dev/null +++ b/ui/components/Alert/Alert.tsx @@ -0,0 +1,153 @@ +"use client"; +/* + * Documentation: + * Alert — https://app.subframe.com/f023064a5ca0/library?component=Alert_3a65613d-d546-467c-80f4-aaba6a7edcd5 + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + */ + +import React from "react"; +import { FeatherAlertCircle } from "@subframe/core"; +import { FeatherAlertTriangle } from "@subframe/core"; +import { FeatherCheckCircle } from "@subframe/core"; +import { FeatherInbox } from "@subframe/core"; +import { FeatherInfo } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { IconButton } from "../IconButton"; + +export interface AlertRootProps + extends Omit, "title"> { + variant?: "information" | "success" | "warning" | "error" | "neutral"; + notDismissible?: boolean; + title?: React.ReactNode; + description?: React.ReactNode; + textSize?: "small" | "medium"; + className?: string; +} + +const AlertRoot = React.forwardRef( + function AlertRoot( + { + variant = "information", + notDismissible = false, + title, + description, + textSize = "medium", + className, + ...otherProps + }: AlertRootProps, + ref + ) { + return ( +
+
+ + + + + +
+ {title ? ( + + {title} + + ) : null} + {description ? ( + + {description} + + ) : null} +
+
+ } /> +
+
+
+ +
+
+ ); + } +); + +export const Alert = AlertRoot; diff --git a/ui/components/Alert/index.tsx b/ui/components/Alert/index.tsx new file mode 100644 index 0000000..724d3c3 --- /dev/null +++ b/ui/components/Alert/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Alert as AlertComponent } from "./Alert"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Alert = AlertComponent; diff --git a/ui/components/AreaChart/AreaChart.tsx b/ui/components/AreaChart/AreaChart.tsx new file mode 100644 index 0000000..b44643a --- /dev/null +++ b/ui/components/AreaChart/AreaChart.tsx @@ -0,0 +1,42 @@ +"use client"; +/* + * Documentation: + * Area Chart — https://app.subframe.com/f023064a5ca0/library?component=Area+Chart_8aa1e7b3-5db6-4a62-aa49-137ced21a231 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface AreaChartRootProps + extends React.ComponentProps { + stacked?: boolean; + className?: string; +} + +const AreaChartRoot = React.forwardRef< + React.ElementRef, + AreaChartRootProps +>(function AreaChartRoot( + { stacked = false, className, ...otherProps }: AreaChartRootProps, + ref +) { + return ( + + ); +}); + +export const AreaChart = AreaChartRoot; diff --git a/ui/components/AreaChart/index.tsx b/ui/components/AreaChart/index.tsx new file mode 100644 index 0000000..676b497 --- /dev/null +++ b/ui/components/AreaChart/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { AreaChart as AreaChartComponent } from "./AreaChart"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const AreaChart = AreaChartComponent; diff --git a/ui/components/Avatar/Avatar.tsx b/ui/components/Avatar/Avatar.tsx new file mode 100644 index 0000000..2c0c017 --- /dev/null +++ b/ui/components/Avatar/Avatar.tsx @@ -0,0 +1,108 @@ +"use client"; +/* + * Documentation: + * Avatar — https://app.subframe.com/f023064a5ca0/library?component=Avatar_bec25ae6-5010-4485-b46b-cf79e3943ab2 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface AvatarRootProps + extends Omit, "color"> { + color?: "neutral" | "blue" | "green" | "red" | "yellow" | "purple"; + size?: + | "extra-small" + | "small" + | "medium" + | "large" + | "extra-large" + | "2-x-large" + | "3-x-large"; + border?: boolean; + children?: React.ReactNode; + image?: string; + className?: string; +} + +const AvatarRoot = React.forwardRef( + function AvatarRoot( + { + color = "neutral", + size = "medium", + border = false, + children, + image, + className, + ...otherProps + }: AvatarRootProps, + ref + ) { + return ( +
+ {children ? ( + + {children} + + ) : null} + {image ? ( + + ) : null} +
+ ); + } +); + +export const Avatar = AvatarRoot; diff --git a/ui/components/Avatar/index.tsx b/ui/components/Avatar/index.tsx new file mode 100644 index 0000000..d3dabb8 --- /dev/null +++ b/ui/components/Avatar/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Avatar as AvatarComponent } from "./Avatar"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Avatar = AvatarComponent; diff --git a/ui/components/AvatarGroup/AvatarGroup.tsx b/ui/components/AvatarGroup/AvatarGroup.tsx new file mode 100644 index 0000000..2bbd3a2 --- /dev/null +++ b/ui/components/AvatarGroup/AvatarGroup.tsx @@ -0,0 +1,110 @@ +"use client"; +/* + * Documentation: + * Avatar Group — https://app.subframe.com/f023064a5ca0/library?component=Avatar+Group_6e7444ab-1d67-47f5-af23-1a2d3ecf1016 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface AvatarOverflowProps + extends React.HTMLAttributes { + border?: boolean; + overflowLabel?: React.ReactNode; + className?: string; +} + +const AvatarOverflow = React.forwardRef( + function AvatarOverflow( + { + border = false, + overflowLabel, + className, + ...otherProps + }: AvatarOverflowProps, + ref + ) { + return ( +
+ {overflowLabel ? ( + + {overflowLabel} + + ) : null} +
+ ); + } +); + +export interface AvatarGroupRootProps + extends React.HTMLAttributes { + hoverEffect?: "on" | "off"; + border?: boolean; + showOverflow?: boolean; + children?: React.ReactNode; + overflowLabel?: React.ReactNode; + className?: string; +} + +const AvatarGroupRoot = React.forwardRef( + function AvatarGroupRoot( + { + hoverEffect = "off", + border = false, + showOverflow = false, + children, + overflowLabel, + className, + ...otherProps + }: AvatarGroupRootProps, + ref + ) { + return ( +
*:not(:first-child)]:-ml-2", + className + )} + ref={ref} + {...otherProps} + > + {children ? ( +
*:not(:first-child)]:-ml-2 [&>*]:relative relative [&>*]:z-0 z-0 [&>*:nth-child(1)]:z-[1] [&>*:nth-child(2)]:z-[2] [&>*:nth-child(3)]:z-[3] [&>*:nth-child(4)]:z-[4] [&>*:nth-child(5)]:z-[5]", + { + "[&>*]:border [&>*]:border-solid [&>*]:border-border-on-primary": + border, + "[&>*]:transition-transform [&>*]:duration-200 [&>*:hover]:scale-110 [&>*:hover]:rotate-6 [&>*:hover]:z-20": + hoverEffect === "on", + } + )} + > + {children} +
+ ) : null} + +
+ ); + } +); + +export const AvatarGroup = Object.assign(AvatarGroupRoot, { + AvatarOverflow, +}); diff --git a/ui/components/AvatarGroup/index.tsx b/ui/components/AvatarGroup/index.tsx new file mode 100644 index 0000000..132a7e4 --- /dev/null +++ b/ui/components/AvatarGroup/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { AvatarGroup as AvatarGroupComponent } from "./AvatarGroup"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const AvatarGroup = AvatarGroupComponent; diff --git a/ui/components/Badge/Badge.tsx b/ui/components/Badge/Badge.tsx new file mode 100644 index 0000000..8153a4d --- /dev/null +++ b/ui/components/Badge/Badge.tsx @@ -0,0 +1,317 @@ +"use client"; +/* + * Documentation: + * Badge — https://app.subframe.com/f023064a5ca0/library?component=Badge_7d75e221-355a-4695-acb7-3902a9f6ffde + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface BadgeRootProps extends React.HTMLAttributes { + tone?: + | "red-subtle" + | "orange-subtle" + | "yellow-subtle" + | "pear-subtle" + | "lime-subtle" + | "green-subtle" + | "teal-subtle" + | "ocean-subtle" + | "sky-subtle" + | "blue-subtle" + | "indigo-subtle" + | "violet-subtle" + | "fuchsia-subtle" + | "pink-subtle" + | "sand-subtle" + | "brown-subtle" + | "red-solid" + | "orange-solid" + | "yellow-solid" + | "pear-solid" + | "lime-solid" + | "green-solid" + | "teal-solid" + | "ocean-solid" + | "sky-solid" + | "blue-solid" + | "indigo-solid" + | "violet-solid" + | "fuchsia-solid" + | "pink-solid" + | "sand-solid" + | "brown-solid" + | "red-outline" + | "orange-outline" + | "yellow-outline" + | "pear-outline" + | "lime-outline" + | "green-outline" + | "teal-outline" + | "ocean-outline" + | "sky-outline" + | "blue-outline" + | "indigo-outline" + | "violet-outline" + | "fuchsia-outline" + | "pink-outline" + | "sand-outline" + | "brown-outline" + | "brand-primary-solid" + | "brand-secondary-solid" + | "neutral-solid" + | "brand-primary-subtle" + | "brand-secondary-subtle" + | "neutral-subtle" + | "white"; + children?: React.ReactNode; + size?: "small" | "medium" | "large"; + hideDot?: boolean; + className?: string; +} + +const BadgeRoot = React.forwardRef( + function BadgeRoot( + { + tone = "neutral-subtle", + children, + size = "medium", + hideDot = false, + className, + ...otherProps + }: BadgeRootProps, + ref + ) { + return ( +
+
+ {children ? ( + + {children} + + ) : null} +
+ ); + } +); + +export const Badge = BadgeRoot; diff --git a/ui/components/Badge/index.tsx b/ui/components/Badge/index.tsx new file mode 100644 index 0000000..bc7f68d --- /dev/null +++ b/ui/components/Badge/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Badge as BadgeComponent } from "./Badge"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Badge = BadgeComponent; diff --git a/ui/components/BadgeFeedback/BadgeFeedback.tsx b/ui/components/BadgeFeedback/BadgeFeedback.tsx new file mode 100644 index 0000000..6aacc14 --- /dev/null +++ b/ui/components/BadgeFeedback/BadgeFeedback.tsx @@ -0,0 +1,169 @@ +"use client"; +/* + * Documentation: + * Badge Feedback — https://app.subframe.com/f023064a5ca0/library?component=Badge+Feedback_896531e4-54ea-4e5a-8710-c30fc2ce10fb + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface BadgeFeedbackRootProps + extends React.HTMLAttributes { + farbe?: + | "solid-neutral" + | "solid-positive" + | "solid-negative" + | "solid-warning" + | "solid-information" + | "solid-purple" + | "solid-blue" + | "solid-brand-primary" + | "solid-brand-secondary" + | "subtle-neutral" + | "subtle-positive" + | "subtle-negative" + | "subtle-warning" + | "subtle-information" + | "subtle-purple" + | "subtle-blue" + | "subtle-brand-primary" + | "subtle-brand-secondary" + | "ghost-neutral" + | "ghost-positive" + | "ghost-negative" + | "ghost-warning" + | "ghost-information" + | "ghost-purple" + | "ghost-blue" + | "ghost-brand-primary" + | "ghost-brand-secondary"; + children?: React.ReactNode; + size?: "medium" | "small" | "large"; + className?: string; +} + +const BadgeFeedbackRoot = React.forwardRef< + HTMLDivElement, + BadgeFeedbackRootProps +>(function BadgeFeedbackRoot( + { + farbe = "solid-neutral", + children, + size = "medium", + className, + ...otherProps + }: BadgeFeedbackRootProps, + ref +) { + return ( +
+
+ {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export const BadgeFeedback = BadgeFeedbackRoot; diff --git a/ui/components/BadgeFeedback/index.tsx b/ui/components/BadgeFeedback/index.tsx new file mode 100644 index 0000000..9b26f93 --- /dev/null +++ b/ui/components/BadgeFeedback/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { BadgeFeedback as BadgeFeedbackComponent } from "./BadgeFeedback"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const BadgeFeedback = BadgeFeedbackComponent; diff --git a/ui/components/BadgeShortcut/BadgeShortcut.tsx b/ui/components/BadgeShortcut/BadgeShortcut.tsx new file mode 100644 index 0000000..da177db --- /dev/null +++ b/ui/components/BadgeShortcut/BadgeShortcut.tsx @@ -0,0 +1,121 @@ +"use client"; +/* + * Documentation: + * Badge Shortcut — https://app.subframe.com/f023064a5ca0/library?component=Badge+Shortcut_97bdb082-1124-4dd7-a335-b14b822d0157 + */ + +import React from "react"; +import { FeatherCommand } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface BadgeShortcutRootProps + extends React.HTMLAttributes { + variant?: + | "neutral" + | "brand-primary" + | "brand-secondary" + | "error" + | "warning" + | "success"; + iconLeft1?: React.ReactNode; + iconLeft2?: React.ReactNode; + children?: React.ReactNode; + iconRight?: React.ReactNode; + size?: "small" | "medium" | "large"; + className?: string; +} + +const BadgeShortcutRoot = React.forwardRef< + HTMLDivElement, + BadgeShortcutRootProps +>(function BadgeShortcutRoot( + { + variant = "neutral", + iconLeft1 = , + iconLeft2 = null, + children, + iconRight = null, + size = "medium", + className, + ...otherProps + }: BadgeShortcutRootProps, + ref +) { + return ( +
+ {iconLeft1 ? ( + + {iconLeft1} + + ) : null} + {iconLeft2 ? ( + + {iconLeft2} + + ) : null} + {children ? ( + + {children} + + ) : null} + {iconRight ? ( + + {iconRight} + + ) : null} +
+ ); +}); + +export const BadgeShortcut = BadgeShortcutRoot; diff --git a/ui/components/BadgeShortcut/index.tsx b/ui/components/BadgeShortcut/index.tsx new file mode 100644 index 0000000..1887ad7 --- /dev/null +++ b/ui/components/BadgeShortcut/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { BadgeShortcut as BadgeShortcutComponent } from "./BadgeShortcut"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const BadgeShortcut = BadgeShortcutComponent; diff --git a/ui/components/BannerAlert/BannerAlert.tsx b/ui/components/BannerAlert/BannerAlert.tsx new file mode 100644 index 0000000..4873d1b --- /dev/null +++ b/ui/components/BannerAlert/BannerAlert.tsx @@ -0,0 +1,129 @@ +"use client"; +/* + * Documentation: + * Banner Alert — https://app.subframe.com/f023064a5ca0/library?component=Banner+Alert_a3e8dcff-3c57-42da-880f-432bb8ce4100 + * Link Button — https://app.subframe.com/f023064a5ca0/library?component=Link+Button_a4ee726a-774c-4091-8c49-55b659356024 + */ + +import React from "react"; +import { FeatherAlertCircle } from "@subframe/core"; +import { FeatherAlertTriangle } from "@subframe/core"; +import { FeatherCheckCircle } from "@subframe/core"; +import { FeatherInfo } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { LinkButton } from "../LinkButton"; + +export interface BannerAlertRootProps + extends React.HTMLAttributes { + dismissing?: boolean; + type?: "information" | "success" | "warning" | "error"; + fillContainer?: boolean; + hideAction?: boolean; + dismissible?: boolean; + children?: React.ReactNode; + actionLabel?: React.ReactNode; + className?: string; +} + +const BannerAlertRoot = React.forwardRef( + function BannerAlertRoot( + { + dismissing = false, + type = "information", + fillContainer = false, + hideAction = false, + dismissible = false, + children, + actionLabel, + className, + ...otherProps + }: BannerAlertRootProps, + ref + ) { + return ( +
+ + + + + {children ? ( + + {children} + + ) : null} + + Aktion + +
+ +
+
+ ); + } +); + +export const BannerAlert = BannerAlertRoot; diff --git a/ui/components/BannerAlert/index.tsx b/ui/components/BannerAlert/index.tsx new file mode 100644 index 0000000..ada9514 --- /dev/null +++ b/ui/components/BannerAlert/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { BannerAlert as BannerAlertComponent } from "./BannerAlert"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const BannerAlert = BannerAlertComponent; diff --git a/ui/components/BannerOffline/BannerOffline.tsx b/ui/components/BannerOffline/BannerOffline.tsx new file mode 100644 index 0000000..d6fbfc6 --- /dev/null +++ b/ui/components/BannerOffline/BannerOffline.tsx @@ -0,0 +1,51 @@ +"use client"; +/* + * Documentation: + * Banner Offline — https://app.subframe.com/f023064a5ca0/library?component=Banner+Offline_2582cb6e-0176-4a5c-b70f-c3bef3c7001d + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface BannerOfflineRootProps + extends React.HTMLAttributes { + variant?: "default"; + text?: React.ReactNode; + className?: string; +} + +const BannerOfflineRoot = React.forwardRef< + HTMLDivElement, + BannerOfflineRootProps +>(function BannerOfflineRoot( + { + variant = "default", + text, + className, + ...otherProps + }: BannerOfflineRootProps, + ref +) { + return ( +
+ + {text ? ( + + {text} + + ) : null} +
+ ); +}); + +export const BannerOffline = BannerOfflineRoot; diff --git a/ui/components/BannerOffline/index.tsx b/ui/components/BannerOffline/index.tsx new file mode 100644 index 0000000..30afa81 --- /dev/null +++ b/ui/components/BannerOffline/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { BannerOffline as BannerOfflineComponent } from "./BannerOffline"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const BannerOffline = BannerOfflineComponent; diff --git a/ui/components/BarChart/BarChart.tsx b/ui/components/BarChart/BarChart.tsx new file mode 100644 index 0000000..0f44a3c --- /dev/null +++ b/ui/components/BarChart/BarChart.tsx @@ -0,0 +1,42 @@ +"use client"; +/* + * Documentation: + * Bar Chart — https://app.subframe.com/f023064a5ca0/library?component=Bar+Chart_4d4f30e7-1869-4980-8b96-617df3b37912 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface BarChartRootProps + extends React.ComponentProps { + stacked?: boolean; + className?: string; +} + +const BarChartRoot = React.forwardRef< + React.ElementRef, + BarChartRootProps +>(function BarChartRoot( + { stacked = false, className, ...otherProps }: BarChartRootProps, + ref +) { + return ( + + ); +}); + +export const BarChart = BarChartRoot; diff --git a/ui/components/BarChart/index.tsx b/ui/components/BarChart/index.tsx new file mode 100644 index 0000000..aa3cf8f --- /dev/null +++ b/ui/components/BarChart/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { BarChart as BarChartComponent } from "./BarChart"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const BarChart = BarChartComponent; diff --git a/ui/components/Breadcrumbs/Breadcrumbs.tsx b/ui/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000..39d1731 --- /dev/null +++ b/ui/components/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,86 @@ +"use client"; +/* + * Documentation: + * Breadcrumbs — https://app.subframe.com/f023064a5ca0/library?component=Breadcrumbs_8898334b-a66f-4ee8-8bd1-afcfa8e37cc0 + */ + +import React from "react"; +import { FeatherChevronRight } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ItemProps extends React.HTMLAttributes { + children?: React.ReactNode; + active?: boolean; + className?: string; +} + +const Item = React.forwardRef(function Item( + { children, active = false, className, ...otherProps }: ItemProps, + ref +) { + return children ? ( + + {children} + + ) : null; +}); + +export interface DividerProps + extends React.ComponentProps { + className?: string; +} + +const Divider = React.forwardRef< + React.ElementRef, + DividerProps +>(function Divider({ className, ...otherProps }: DividerProps, ref) { + return ( + + ); +}); + +export interface BreadcrumbsRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const BreadcrumbsRoot = React.forwardRef( + function BreadcrumbsRoot( + { children, className, ...otherProps }: BreadcrumbsRootProps, + ref + ) { + return children ? ( +
+ {children} +
+ ) : null; + } +); + +export const Breadcrumbs = Object.assign(BreadcrumbsRoot, { + Item, + Divider, +}); diff --git a/ui/components/Breadcrumbs/index.tsx b/ui/components/Breadcrumbs/index.tsx new file mode 100644 index 0000000..aeb6996 --- /dev/null +++ b/ui/components/Breadcrumbs/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Breadcrumbs as BreadcrumbsComponent } from "./Breadcrumbs"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Breadcrumbs = BreadcrumbsComponent; diff --git a/ui/components/Button/Button.tsx b/ui/components/Button/Button.tsx new file mode 100644 index 0000000..bc05393 --- /dev/null +++ b/ui/components/Button/Button.tsx @@ -0,0 +1,152 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ButtonRootProps + extends React.ButtonHTMLAttributes { + disabled?: boolean; + loading?: boolean; + variant?: + | "primary" + | "secondary" + | "tertiary" + | "ghost" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + size?: "large" | "medium" | "small"; + children?: React.ReactNode; + icon?: React.ReactNode; + iconRight?: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; + className?: string; +} + +const ButtonRoot = React.forwardRef( + function ButtonRoot( + { + disabled = false, + loading = false, + variant = "primary", + size = "medium", + children, + icon = null, + iconRight = null, + className, + type = "button", + ...otherProps + }: ButtonRootProps, + ref + ) { + return ( + + ); + } +); + +export const Button = ButtonRoot; diff --git a/ui/components/Button/index.tsx b/ui/components/Button/index.tsx new file mode 100644 index 0000000..6a88cf6 --- /dev/null +++ b/ui/components/Button/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Button as ButtonComponent } from "./Button"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Button = ButtonComponent; diff --git a/ui/components/ButtonGroup/ButtonGroup.tsx b/ui/components/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 0000000..f356e76 --- /dev/null +++ b/ui/components/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,51 @@ +"use client"; +/* + * Documentation: + * Button Group — https://app.subframe.com/f023064a5ca0/library?component=Button+Group_e0379327-50f0-4852-9ae1-ad09b4bf01d3 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface ButtonGroupRootProps + extends React.HTMLAttributes { + vertical?: boolean; + align?: "start" | "center" | "end" | "stretch"; + children?: React.ReactNode; + className?: string; +} + +const ButtonGroupRoot = React.forwardRef( + function ButtonGroupRoot( + { + vertical = false, + align = "start", + children, + className, + ...otherProps + }: ButtonGroupRootProps, + ref + ) { + return children ? ( +
*]:grow [&>*]:basis-0": + align === "stretch", + "justify-end": align === "end", + "justify-center": align === "center", + "flex-col items-start": vertical, + }, + className + )} + ref={ref} + {...otherProps} + > + {children} +
+ ) : null; + } +); + +export const ButtonGroup = ButtonGroupRoot; diff --git a/ui/components/ButtonGroup/index.tsx b/ui/components/ButtonGroup/index.tsx new file mode 100644 index 0000000..ee02b32 --- /dev/null +++ b/ui/components/ButtonGroup/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ButtonGroup as ButtonGroupComponent } from "./ButtonGroup"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const ButtonGroup = ButtonGroupComponent; diff --git a/ui/components/Calendar/Calendar.tsx b/ui/components/Calendar/Calendar.tsx new file mode 100644 index 0000000..6fac933 --- /dev/null +++ b/ui/components/Calendar/Calendar.tsx @@ -0,0 +1,53 @@ +"use client"; +/* + * Documentation: + * Calendar — https://app.subframe.com/f023064a5ca0/library?component=Calendar_5a87e517-ace2-49af-adcf-076c97ec3921 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export type CalendarRootProps = React.ComponentProps< + typeof SubframeCore.Calendar +> & { className?: string }; + +const CalendarRoot = React.forwardRef< + React.ElementRef, + CalendarRootProps +>(function CalendarRoot({ className, ...otherProps }: CalendarRootProps, ref) { + return ( + + ); +}); + +export const Calendar = CalendarRoot; diff --git a/ui/components/Calendar/index.tsx b/ui/components/Calendar/index.tsx new file mode 100644 index 0000000..bfd4d7d --- /dev/null +++ b/ui/components/Calendar/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Calendar as CalendarComponent } from "./Calendar"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Calendar = CalendarComponent; diff --git a/ui/components/Card/Card.tsx b/ui/components/Card/Card.tsx new file mode 100644 index 0000000..59c5205 --- /dev/null +++ b/ui/components/Card/Card.tsx @@ -0,0 +1,65 @@ +"use client"; +/* + * Documentation: + * Card — https://app.subframe.com/f023064a5ca0/library?component=Card_56067c50-02ce-4c7c-8866-9db30e23ecc0 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface CardRootProps extends React.HTMLAttributes { + children?: React.ReactNode; + size?: "extra-small" | "small" | "medium" | "large" | "extra-large"; + variant?: + | "default" + | "neutral" + | "information" + | "positive" + | "warning" + | "negative"; + className?: string; +} + +const CardRoot = React.forwardRef( + function CardRoot( + { + children, + size = "medium", + variant = "default", + className, + ...otherProps + }: CardRootProps, + ref + ) { + return children ? ( +
+ {children} +
+ ) : null; + } +); + +export const Card = CardRoot; diff --git a/ui/components/Card/index.tsx b/ui/components/Card/index.tsx new file mode 100644 index 0000000..99f789a --- /dev/null +++ b/ui/components/Card/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Card as CardComponent } from "./Card"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Card = CardComponent; diff --git a/ui/components/CardIcon/CardIcon.tsx b/ui/components/CardIcon/CardIcon.tsx new file mode 100644 index 0000000..34fb1dc --- /dev/null +++ b/ui/components/CardIcon/CardIcon.tsx @@ -0,0 +1,84 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Card Icon — https://app.subframe.com/f023064a5ca0/library?component=Card+Icon_41867f0d-7a6a-4e51-9b08-6cce5a3070b1 + */ + +import React from "react"; +import { FeatherInfo } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; + +export interface CardIconRootProps + extends Omit, "title"> { + outlined?: boolean; + layout?: "horizontal" | "vertical"; + hideIcon?: boolean; + title?: React.ReactNode; + body?: React.ReactNode; + className?: string; +} + +const CardIconRoot = React.forwardRef( + function CardIconRoot( + { + outlined = false, + layout = "horizontal", + hideIcon = false, + title, + body, + className, + ...otherProps + }: CardIconRootProps, + ref + ) { + return ( +
+
+ +
+
+
+ {title ? ( + + {title} + + ) : null} +
+ {body ? ( + + {body} + + ) : null} + +
+
+
+
+ ); + } +); + +export const CardIcon = CardIconRoot; diff --git a/ui/components/CardIcon/index.tsx b/ui/components/CardIcon/index.tsx new file mode 100644 index 0000000..0785ffb --- /dev/null +++ b/ui/components/CardIcon/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { CardIcon as CardIconComponent } from "./CardIcon"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const CardIcon = CardIconComponent; diff --git a/ui/components/CardImage/CardImage.tsx b/ui/components/CardImage/CardImage.tsx new file mode 100644 index 0000000..7d22ef4 --- /dev/null +++ b/ui/components/CardImage/CardImage.tsx @@ -0,0 +1,83 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Card Image — https://app.subframe.com/f023064a5ca0/library?component=Card+Image_1f735087-2381-42e1-8ceb-491a8bc63948 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; + +export interface CardImageRootProps + extends Omit, "title"> { + outlined?: boolean; + layout?: "horizontal" | "vertical"; + hideImage?: boolean; + title?: React.ReactNode; + body?: React.ReactNode; + className?: string; +} + +const CardImageRoot = React.forwardRef( + function CardImageRoot( + { + outlined = false, + layout = "horizontal", + hideImage = false, + title, + body, + className, + ...otherProps + }: CardImageRootProps, + ref + ) { + return ( +
+ +
+
+ {title ? ( + + {title} + + ) : null} +
+ {body ? ( + + {body} + + ) : null} + +
+
+
+
+ ); + } +); + +export const CardImage = CardImageRoot; diff --git a/ui/components/CardImage/index.tsx b/ui/components/CardImage/index.tsx new file mode 100644 index 0000000..7ee6925 --- /dev/null +++ b/ui/components/CardImage/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { CardImage as CardImageComponent } from "./CardImage"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const CardImage = CardImageComponent; diff --git a/ui/components/Checkbox/Checkbox.tsx b/ui/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000..3e62c45 --- /dev/null +++ b/ui/components/Checkbox/Checkbox.tsx @@ -0,0 +1,145 @@ +"use client"; +/* + * Documentation: + * Checkbox — https://app.subframe.com/f023064a5ca0/library?component=Checkbox_3816e3b5-c48c-499b-b45e-0777c6972523 + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherMinus } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface CheckboxRootProps + extends React.ComponentProps { + label?: React.ReactNode; + disabled?: boolean; + checked?: boolean; + error?: boolean; + description?: React.ReactNode; + helpText?: React.ReactNode; + indeterminate?: boolean; + hideLabel?: boolean; + onCheckedChange?: (checked: boolean) => void; + className?: string; +} + +const CheckboxRoot = React.forwardRef( + function CheckboxRoot( + { + label, + disabled = false, + checked = false, + error = false, + description, + helpText, + indeterminate = false, + hideLabel = false, + className, + ...otherProps + }: CheckboxRootProps, + ref + ) { + return ( + + + + ); + } +); + +export const Checkbox = CheckboxRoot; diff --git a/ui/components/Checkbox/index.tsx b/ui/components/Checkbox/index.tsx new file mode 100644 index 0000000..6fc47d5 --- /dev/null +++ b/ui/components/Checkbox/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Checkbox as CheckboxComponent } from "./Checkbox"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Checkbox = CheckboxComponent; diff --git a/ui/components/CheckboxCard/CheckboxCard.tsx b/ui/components/CheckboxCard/CheckboxCard.tsx new file mode 100644 index 0000000..ab50b1c --- /dev/null +++ b/ui/components/CheckboxCard/CheckboxCard.tsx @@ -0,0 +1,68 @@ +"use client"; +/* + * Documentation: + * Checkbox Card — https://app.subframe.com/f023064a5ca0/library?component=Checkbox+Card_de0b4dfb-3946-4702-be52-5678dd71925a + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface CheckboxCardRootProps + extends React.ComponentProps { + disabled?: boolean; + checked?: boolean; + showCheckbox?: boolean; + children?: React.ReactNode; + onCheckedChange?: (checked: boolean) => void; + className?: string; +} + +const CheckboxCardRoot = React.forwardRef< + HTMLButtonElement, + CheckboxCardRootProps +>(function CheckboxCardRoot( + { + disabled = false, + checked = false, + showCheckbox = false, + children, + className, + ...otherProps + }: CheckboxCardRootProps, + ref +) { + return ( + + + + ); +}); + +export const CheckboxCard = CheckboxCardRoot; diff --git a/ui/components/CheckboxCard/index.tsx b/ui/components/CheckboxCard/index.tsx new file mode 100644 index 0000000..4e2390e --- /dev/null +++ b/ui/components/CheckboxCard/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { CheckboxCard as CheckboxCardComponent } from "./CheckboxCard"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const CheckboxCard = CheckboxCardComponent; diff --git a/ui/components/CheckboxGroup/CheckboxGroup.tsx b/ui/components/CheckboxGroup/CheckboxGroup.tsx new file mode 100644 index 0000000..f1ddfdb --- /dev/null +++ b/ui/components/CheckboxGroup/CheckboxGroup.tsx @@ -0,0 +1,73 @@ +"use client"; +/* + * Documentation: + * Checkbox Group — https://app.subframe.com/f023064a5ca0/library?component=Checkbox+Group_f9f1b596-c6b3-4d60-aa9a-f34b353f8aa5 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface CheckboxGroupRootProps + extends React.HTMLAttributes { + label?: React.ReactNode; + helpText?: React.ReactNode; + error?: boolean; + horizontal?: boolean; + children?: React.ReactNode; + className?: string; +} + +const CheckboxGroupRoot = React.forwardRef< + HTMLDivElement, + CheckboxGroupRootProps +>(function CheckboxGroupRoot( + { + label, + helpText, + error = false, + horizontal = false, + children, + className, + ...otherProps + }: CheckboxGroupRootProps, + ref +) { + return ( +
+ {label ? ( + + {label} + + ) : null} + {children ? ( +
+ {children} +
+ ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+ ); +}); + +export const CheckboxGroup = CheckboxGroupRoot; diff --git a/ui/components/CheckboxGroup/index.tsx b/ui/components/CheckboxGroup/index.tsx new file mode 100644 index 0000000..2f17b44 --- /dev/null +++ b/ui/components/CheckboxGroup/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { CheckboxGroup as CheckboxGroupComponent } from "./CheckboxGroup"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const CheckboxGroup = CheckboxGroupComponent; diff --git a/ui/components/Combobox/Combobox.tsx b/ui/components/Combobox/Combobox.tsx new file mode 100644 index 0000000..329f9ea --- /dev/null +++ b/ui/components/Combobox/Combobox.tsx @@ -0,0 +1,386 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Combobox — https://app.subframe.com/f023064a5ca0/library?component=Combobox_8a1be2ee-c1b7-4407-9c21-6448306d2700 + * Loader — https://app.subframe.com/f023064a5ca0/library?component=Loader_f2e570c8-e463-45c2-aae9-a960146bc5d5 + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherLoader } from "@subframe/core"; +import { FeatherPlus } from "@subframe/core"; +import { FeatherSearch } from "@subframe/core"; +import { FeatherSearchX } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Loader } from "../Loader"; + +export interface ChipProps extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const Chip = React.forwardRef(function Chip( + { children, className, ...otherProps }: ChipProps, + ref +) { + return ( +
+ {children ? ( + + {children} + + ) : null} +
+ +
+
+ ); +}); + +export interface ItemProps extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const Item = React.forwardRef(function Item( + { + children, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: ItemProps, + ref +) { + return ( +
+
+ +
+ {children ? ( + + {children} + + ) : null} + +
+ ); +}); + +export interface AddItemProps extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const AddItem = React.forwardRef(function AddItem( + { children, className, ...otherProps }: AddItemProps, + ref +) { + return ( +
+ + {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export interface ComboboxRootProps + extends Omit, "placeholder"> { + disabled?: boolean; + error?: boolean; + state?: "default" | "typing" | "no-results" | "loading" | "create-new"; + hasValue?: boolean; + open?: boolean; + showLabel?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + searchValue?: React.ReactNode; + placeholder?: React.ReactNode; + createLabel?: React.ReactNode; + emptyText?: React.ReactNode; + loadingText?: React.ReactNode; + message?: React.ReactNode; + icon?: React.ReactNode; + chipChildren?: React.ReactNode; + children?: React.ReactNode; + size?: "small" | "medium" | "large"; + className?: string; +} + +const ComboboxRoot = React.forwardRef( + function ComboboxRoot( + { + disabled = false, + error = false, + state = "default", + hasValue = false, + open = false, + showLabel = false, + label, + helpText, + searchValue, + placeholder, + createLabel, + emptyText, + loadingText, + message, + icon = , + chipChildren, + children, + size = "medium", + className, + ...otherProps + }: ComboboxRootProps, + ref + ) { + return ( +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+
+
+ {icon ? ( + + {icon} + + ) : null} + {chipChildren ? ( +
+ {chipChildren} +
+ ) : null} +
+ +
+ + +
+
+ {message ? ( + + {message} + + ) : null} +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ + {emptyText ? ( + + {emptyText} + + ) : null} +
+
+ + {loadingText ? ( + + {loadingText} + + ) : null} +
+
+ + {createLabel ? ( + + {createLabel} + + ) : null} +
+
+
+
+ ); + } +); + +export const Combobox = Object.assign(ComboboxRoot, { + Chip, + Item, + AddItem, +}); diff --git a/ui/components/Combobox/index.tsx b/ui/components/Combobox/index.tsx new file mode 100644 index 0000000..f1e7180 --- /dev/null +++ b/ui/components/Combobox/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Combobox as ComboboxComponent } from "./Combobox"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Combobox = ComboboxComponent; diff --git a/ui/components/ContextMenu/ContextMenu.tsx b/ui/components/ContextMenu/ContextMenu.tsx new file mode 100644 index 0000000..a2a2760 --- /dev/null +++ b/ui/components/ContextMenu/ContextMenu.tsx @@ -0,0 +1,117 @@ +"use client"; +/* + * Documentation: + * Context Menu — https://app.subframe.com/f023064a5ca0/library?component=Context+Menu_f8a49f07-fa5b-46c8-9399-cbbf0930cc62 + */ + +import React from "react"; +import { FeatherStar } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ContextDividerProps + extends React.HTMLAttributes { + className?: string; +} + +const ContextDivider = React.forwardRef( + function ContextDivider( + { className, ...otherProps }: ContextDividerProps, + ref + ) { + return ( +
+
+
+ ); + } +); + +export interface ContextItemProps + extends React.ComponentProps { + children?: React.ReactNode; + icon?: React.ReactNode; + rightSlot?: React.ReactNode; + className?: string; +} + +const ContextItem = React.forwardRef( + function ContextItem( + { + children, + icon = , + rightSlot, + className, + ...otherProps + }: ContextItemProps, + ref + ) { + return ( + +
+
+ {icon ? ( + + {icon} + + ) : null} +
+ {children ? ( + + {children} + + ) : null} + {rightSlot ? ( +
+ {rightSlot} +
+ ) : null} +
+
+ ); + } +); + +export interface ContextMenuRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const ContextMenuRoot = React.forwardRef( + function ContextMenuRoot( + { children, className, ...otherProps }: ContextMenuRootProps, + ref + ) { + return children ? ( +
+ {children} +
+ ) : null; + } +); + +export const ContextMenu = Object.assign(ContextMenuRoot, { + ContextDivider, + ContextItem, +}); diff --git a/ui/components/ContextMenu/index.tsx b/ui/components/ContextMenu/index.tsx new file mode 100644 index 0000000..11ba1eb --- /dev/null +++ b/ui/components/ContextMenu/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ContextMenu as ContextMenuComponent } from "./ContextMenu"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const ContextMenu = ContextMenuComponent; diff --git a/ui/components/CopyToClipboardButton/CopyToClipboardButton.tsx b/ui/components/CopyToClipboardButton/CopyToClipboardButton.tsx new file mode 100644 index 0000000..3206e3b --- /dev/null +++ b/ui/components/CopyToClipboardButton/CopyToClipboardButton.tsx @@ -0,0 +1,77 @@ +"use client"; +/* + * Documentation: + * Copy to clipboard button — https://app.subframe.com/f023064a5ca0/library?component=Copy+to+clipboard+button_e8c76626-6462-4f2f-b595-38d530d427e8 + * Tooltip — https://app.subframe.com/f023064a5ca0/library?component=Tooltip_ccebd1e9-f6ac-4737-8376-0dfacd90c9f3 + */ + +import React from "react"; +import { FeatherClipboard } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Tooltip } from "../Tooltip"; + +export interface CopyToClipboardButtonRootProps + extends Omit< + React.ComponentProps, + "clipboardText" + > { + clipboardText?: React.ReactNode; + tooltipText?: React.ReactNode; + icon?: React.ReactNode; + onCopy?: () => void; + className?: string; +} + +const CopyToClipboardButtonRoot = React.forwardRef< + HTMLDivElement, + CopyToClipboardButtonRootProps +>(function CopyToClipboardButtonRoot( + { + clipboardText, + tooltipText, + icon = , + className, + ...otherProps + }: CopyToClipboardButtonRootProps, + ref +) { + return ( + + + + +
+ {icon ? ( + + {icon} + + ) : null} +
+
+
+ + + {tooltipText} + + +
+
+ ); +}); + +export const CopyToClipboardButton = CopyToClipboardButtonRoot; diff --git a/ui/components/CopyToClipboardButton/index.tsx b/ui/components/CopyToClipboardButton/index.tsx new file mode 100644 index 0000000..ed60a7c --- /dev/null +++ b/ui/components/CopyToClipboardButton/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { CopyToClipboardButton as CopyToClipboardButtonComponent } from "./CopyToClipboardButton"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const CopyToClipboardButton = CopyToClipboardButtonComponent; diff --git a/ui/components/DatePicker/DatePicker.tsx b/ui/components/DatePicker/DatePicker.tsx new file mode 100644 index 0000000..213e3bb --- /dev/null +++ b/ui/components/DatePicker/DatePicker.tsx @@ -0,0 +1,679 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Date Picker — https://app.subframe.com/f023064a5ca0/library?component=Date+Picker_e0461776-caeb-4bb3-a9a1-7df3bcfe9afe + * Dropdown Menu select — https://app.subframe.com/f023064a5ca0/library?component=Dropdown+Menu+select_99951515-459b-4286-919e-a89e7549b43b + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + */ + +import React from "react"; +import { FeatherCalendar } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherChevronLeft } from "@subframe/core"; +import { FeatherChevronRight } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { DropdownMenuSelect } from "../DropdownMenuSelect"; +import { IconButton } from "../IconButton"; + +export interface WeekdayProps extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const Weekday = React.forwardRef(function Weekday( + { children, className, ...otherProps }: WeekdayProps, + ref +) { + return ( +
+ {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export interface SelectProps extends React.HTMLAttributes { + children?: React.ReactNode; + grow?: boolean; + className?: string; +} + +const Select = React.forwardRef(function Select( + { children, grow = false, className, ...otherProps }: SelectProps, + ref +) { + return ( +
+
+ {children ? ( + + {children} + + ) : null} +
+ +
+ ); +}); + +export interface CalendarPanelProps + extends React.HTMLAttributes { + month?: React.ReactNode; + year?: React.ReactNode; + className?: string; +} + +const CalendarPanel = React.forwardRef( + function CalendarPanel( + { month, year, className, ...otherProps }: CalendarPanelProps, + ref + ) { + return ( +
+
+
+
+
+ } + /> + + } + /> + } + /> + + } + /> +
+
+
+ Mo + Di + Mi + Do + Fr + Sa + So +
+
+
+
+
+ 27 + 28 + 29 + 30 + 31 + 1 + 2 +
+
+ 3 + 4 + 5 + 6 + 7 + 8 + 9 +
+
+ 10 + 11 + 12 + 13 + 14 + 15 + 16 +
+
+ 17 + 18 + 19 + 20 + 21 + 22 + 23 +
+
+ 24 + 25 + 26 + 27 + 28 + 1 + 2 +
+
+
+
+
+
+
+ } + /> + + } + /> + } + /> + + } + /> +
+
+
+ Mo + Di + Mi + Do + Fr + Sa + So +
+
+
+
+
+ 27 + 28 + 29 + 30 + 31 + 1 + 2 +
+
+ 3 + 4 + 5 + 6 + 7 + 8 + 9 +
+
+ 10 + 11 + 12 + 13 + 14 + 15 + 16 +
+
+ 17 + 18 + 19 + 20 + 21 + 22 + 23 +
+
+ 24 + 25 + 26 + 27 + 28 + 1 + 2 +
+
+
+
+ ); + } +); + +export interface MonthSelectProps extends React.HTMLAttributes { + className?: string; +} + +const MonthSelect = React.forwardRef( + function MonthSelect({ className, ...otherProps }: MonthSelectProps, ref) { + return ( + + +
+ September +
+
+ + + +
+ Januar + + Februar + + März + April + Mai + Juni + Juli + August + September + Oktober + November + Dezember +
+
+
+
+
+ ); + } +); + +export interface YearSelectProps + extends React.ComponentProps { + className?: string; +} + +const YearSelect = React.forwardRef< + React.ElementRef, + YearSelectProps +>(function YearSelect({ className, ...otherProps }: YearSelectProps, ref) { + return ( + + + + 2026 + + + + + +
+ 2021 + 2022 + 2023 + 2024 + 2025 + 2026 + 2027 + 2028 + 2029 + 2030 + 2031 +
+
+
+
+
+ ); +}); + +export interface DayProps extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + today?: boolean; + outside?: boolean; + className?: string; +} + +const Day = React.forwardRef(function Day( + { + children, + selected = false, + today = false, + outside = false, + className, + ...otherProps + }: DayProps, + ref +) { + return ( +
+ {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export interface YearCellProps extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + className?: string; +} + +const YearCell = React.forwardRef( + function YearCell( + { children, selected = false, className, ...otherProps }: YearCellProps, + ref + ) { + return ( +
+ {children ? ( + + {children} + + ) : null} +
+ ); + } +); + +export interface DatePickerRootProps + extends Omit, "placeholder"> { + disabled?: boolean; + error?: boolean; + hasValue?: boolean; + size?: "small" | "medium" | "large"; + fillContainer?: boolean; + borderless?: boolean; + noPadding?: boolean; + showLabel?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + message?: React.ReactNode; + placeholder?: React.ReactNode; + value?: React.ReactNode; + month?: React.ReactNode; + year?: React.ReactNode; + boolean?: boolean; + className?: string; +} + +const DatePickerRoot = React.forwardRef( + function DatePickerRoot( + { + disabled = false, + error = false, + hasValue = false, + size = "medium", + fillContainer = false, + borderless = false, + noPadding = false, + showLabel = false, + label, + helpText, + message, + placeholder, + value, + month, + year, + boolean = false, + className, + ...otherProps + }: DatePickerRootProps, + ref + ) { + return ( + + +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+
+
+ {value ? ( + + {value} + + ) : null} + {placeholder ? ( + + {placeholder} + + ) : null} + +
+
+ {message ? ( + + {message} + + ) : null} +
+
+
+ + + + + + + +
+ ); + } +); + +export const DatePicker = Object.assign(DatePickerRoot, { + Weekday, + Select, + CalendarPanel, + MonthSelect, + YearSelect, + Day, + YearCell, +}); diff --git a/ui/components/DatePicker/index.tsx b/ui/components/DatePicker/index.tsx new file mode 100644 index 0000000..6fd82a4 --- /dev/null +++ b/ui/components/DatePicker/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { DatePicker as DatePickerComponent } from "./DatePicker"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const DatePicker = DatePickerComponent; diff --git a/ui/components/Dialog/Dialog.tsx b/ui/components/Dialog/Dialog.tsx new file mode 100644 index 0000000..02b3d86 --- /dev/null +++ b/ui/components/Dialog/Dialog.tsx @@ -0,0 +1,85 @@ +"use client"; +/* + * Documentation: + * Dialog — https://app.subframe.com/f023064a5ca0/library?component=Dialog_ca59db17-43fb-4247-8094-3c55162e902d + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ContentProps + extends React.ComponentProps { + children?: React.ReactNode; + size?: "xs" | "small" | "medium" | "large" | "media"; + padding?: "default" | "spacious"; + hideCloseIcon?: boolean; + className?: string; +} + +const Content = React.forwardRef(function Content( + { + children, + size = "small", + padding = "default", + hideCloseIcon = false, + className, + ...otherProps + }: ContentProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface DialogRootProps + extends React.ComponentProps { + children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + className?: string; +} + +const DialogRoot = React.forwardRef( + function DialogRoot( + { children, className, ...otherProps }: DialogRootProps, + ref + ) { + return children ? ( + +
+ {children} +
+
+ ) : null; + } +); + +export const Dialog = Object.assign(DialogRoot, { + Content, +}); diff --git a/ui/components/Dialog/index.tsx b/ui/components/Dialog/index.tsx new file mode 100644 index 0000000..f22277b --- /dev/null +++ b/ui/components/Dialog/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Dialog as DialogComponent } from "./Dialog"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Dialog = DialogComponent; diff --git a/ui/components/DialogFullscreen/DialogFullscreen.tsx b/ui/components/DialogFullscreen/DialogFullscreen.tsx new file mode 100644 index 0000000..22d6800 --- /dev/null +++ b/ui/components/DialogFullscreen/DialogFullscreen.tsx @@ -0,0 +1,74 @@ +"use client"; +/* + * Documentation: + * Dialog Fullscreen — https://app.subframe.com/f023064a5ca0/library?component=Dialog+Fullscreen_3f094173-71de-4378-a09a-05c482f7a137 + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + */ + +import React from "react"; +import { FeatherX } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { IconButton } from "../IconButton"; + +export interface DialogFullscreenRootProps + extends React.ComponentProps { + children?: React.ReactNode; + padding?: "default" | "spacious"; + hideCloseIcon?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + className?: string; +} + +const DialogFullscreenRoot = React.forwardRef< + HTMLDivElement, + DialogFullscreenRootProps +>(function DialogFullscreenRoot( + { + children, + padding = "default", + hideCloseIcon = false, + className, + ...otherProps + }: DialogFullscreenRootProps, + ref +) { + return ( + +
+
+ {children ? ( +
+ {children} +
+ ) : null} + } + /> +
+
+
+ ); +}); + +export const DialogFullscreen = DialogFullscreenRoot; diff --git a/ui/components/DialogFullscreen/index.tsx b/ui/components/DialogFullscreen/index.tsx new file mode 100644 index 0000000..7227a7e --- /dev/null +++ b/ui/components/DialogFullscreen/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { DialogFullscreen as DialogFullscreenComponent } from "./DialogFullscreen"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const DialogFullscreen = DialogFullscreenComponent; diff --git a/ui/components/DrawerRight/DrawerRight.tsx b/ui/components/DrawerRight/DrawerRight.tsx new file mode 100644 index 0000000..87b415a --- /dev/null +++ b/ui/components/DrawerRight/DrawerRight.tsx @@ -0,0 +1,67 @@ +"use client"; +/* + * Documentation: + * Drawer Right — https://app.subframe.com/f023064a5ca0/library?component=Drawer+Right_1e71b2cb-8d72-4e67-b368-8805179e9444 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ContentProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Content = React.forwardRef(function Content( + { children, className, ...otherProps }: ContentProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface DrawerRightRootProps + extends React.ComponentProps { + children?: React.ReactNode; + open?: boolean; + onOpenChange?: (open: boolean) => void; + className?: string; +} + +const DrawerRightRoot = React.forwardRef( + function DrawerRightRoot( + { children, className, ...otherProps }: DrawerRightRootProps, + ref + ) { + return children ? ( + +
+ {children} +
+
+ ) : null; + } +); + +export const DrawerRight = Object.assign(DrawerRightRoot, { + Content, +}); diff --git a/ui/components/DrawerRight/index.tsx b/ui/components/DrawerRight/index.tsx new file mode 100644 index 0000000..1bae108 --- /dev/null +++ b/ui/components/DrawerRight/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { DrawerRight as DrawerRightComponent } from "./DrawerRight"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const DrawerRight = DrawerRightComponent; diff --git a/ui/components/DropdownMenuMultiSelect/DropdownMenuMultiSelect.tsx b/ui/components/DropdownMenuMultiSelect/DropdownMenuMultiSelect.tsx new file mode 100644 index 0000000..926f00f --- /dev/null +++ b/ui/components/DropdownMenuMultiSelect/DropdownMenuMultiSelect.tsx @@ -0,0 +1,379 @@ +// @subframe/sync-disable +"use client"; +/* + * Documentation: + * Accordion — https://app.subframe.com/f023064a5ca0/library?component=Accordion_d2e81e20-863a-4027-826a-991d8910efd9 + * Avatar — https://app.subframe.com/f023064a5ca0/library?component=Avatar_bec25ae6-5010-4485-b46b-cf79e3943ab2 + * Checkbox — https://app.subframe.com/f023064a5ca0/library?component=Checkbox_3816e3b5-c48c-499b-b45e-0777c6972523 + * Dropdown Menu multi select — https://app.subframe.com/f023064a5ca0/library?component=Dropdown+Menu+multi+select_8c9fd450-3912-4c8a-abae-4f24e3022a64 + */ + +import React from "react"; +import { FeatherLayers } from "@subframe/core"; +import { FeatherSearch } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Accordion } from "../Accordion"; +import { Avatar } from "../Avatar"; +import { Checkbox } from "../Checkbox"; + +export interface DropdownDividerProps + extends React.HTMLAttributes { + className?: string; +} + +const DropdownDivider = React.forwardRef( + function DropdownDivider( + { className, ...otherProps }: DropdownDividerProps, + ref + ) { + return ( +
+
+
+ ); + } +); + +export interface DropdownItemProps + extends React.ComponentProps { + children?: React.ReactNode; + icon?: React.ReactNode; + iconRight?: React.ReactNode; + image?: string; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const DropdownItem = React.forwardRef( + function DropdownItem( + { + children, + icon = null, + iconRight = null, + image, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: DropdownItemProps, + ref + ) { + return ( + +
+
+ +
+ {image ? ( + + ) : null} + {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} + {iconRight ? ( + + {iconRight} + + ) : null} +
+
+ ); + } +); + +export interface DropdownUserItemProps + extends Omit, "title"> { + title?: React.ReactNode; + subtitle?: React.ReactNode; + image?: string; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const DropdownUserItem = React.forwardRef< + HTMLDivElement, + DropdownUserItemProps +>(function DropdownUserItem( + { + title, + subtitle, + image, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: DropdownUserItemProps, + ref +) { + return ( +
+
+ +
+ + OR + +
+ {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+
+ ); +}); + +export interface DropdownCustomItemProps + extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + showCheckbox?: boolean; + size?: "medium" | "small" | "large"; + className?: string; +} + +const DropdownCustomItem = React.forwardRef< + HTMLDivElement, + DropdownCustomItemProps +>(function DropdownCustomItem( + { + children, + selected = false, + showCheckbox = false, + size = "medium", + className, + ...otherProps + }: DropdownCustomItemProps, + ref +) { + return ( +
+ + {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); + +export interface DropdownNestedItemProps + extends React.ComponentProps { + children?: React.ReactNode; + label?: React.ReactNode; + icon?: React.ReactNode; + className?: string; +} + +const DropdownNestedItem = React.forwardRef< + React.ElementRef, + DropdownNestedItemProps +>(function DropdownNestedItem( + { + children, + label, + icon = , + className, + ...otherProps + }: DropdownNestedItemProps, + ref +) { + return ( + + {icon ? ( + + {icon} + + ) : null} + {label ? ( + + {label} + + ) : null} + +
+ } + ref={ref} + {...otherProps} + > + {children ? ( +
{children}
+ ) : null} + + ); +}); + +export interface DropdownMenuMultiSelectRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + showSearch?: boolean; + className?: string; +} + +const DropdownMenuMultiSelectRoot = React.forwardRef< + HTMLDivElement, + DropdownMenuMultiSelectRootProps +>(function DropdownMenuMultiSelectRoot( + { + children, + showSearch = false, + className, + ...otherProps + }: DropdownMenuMultiSelectRootProps, + ref +) { + return ( +
+
+
+ + +
+
+
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); + +export const DropdownMenuMultiSelect = Object.assign( + DropdownMenuMultiSelectRoot, + { + DropdownDivider, + DropdownItem, + DropdownUserItem, + DropdownCustomItem, + DropdownNestedItem, + } +); diff --git a/ui/components/DropdownMenuMultiSelect/index.tsx b/ui/components/DropdownMenuMultiSelect/index.tsx new file mode 100644 index 0000000..7c78704 --- /dev/null +++ b/ui/components/DropdownMenuMultiSelect/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { DropdownMenuMultiSelect as DropdownMenuMultiSelectComponent } from "./DropdownMenuMultiSelect"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const DropdownMenuMultiSelect = DropdownMenuMultiSelectComponent; diff --git a/ui/components/DropdownMenuSelect/DropdownMenuSelect.tsx b/ui/components/DropdownMenuSelect/DropdownMenuSelect.tsx new file mode 100644 index 0000000..208d271 --- /dev/null +++ b/ui/components/DropdownMenuSelect/DropdownMenuSelect.tsx @@ -0,0 +1,373 @@ +"use client"; +/* + * Documentation: + * Accordion — https://app.subframe.com/f023064a5ca0/library?component=Accordion_d2e81e20-863a-4027-826a-991d8910efd9 + * Avatar — https://app.subframe.com/f023064a5ca0/library?component=Avatar_bec25ae6-5010-4485-b46b-cf79e3943ab2 + * Checkbox — https://app.subframe.com/f023064a5ca0/library?component=Checkbox_3816e3b5-c48c-499b-b45e-0777c6972523 + * Dropdown Menu select — https://app.subframe.com/f023064a5ca0/library?component=Dropdown+Menu+select_99951515-459b-4286-919e-a89e7549b43b + */ + +import React from "react"; +import { FeatherLayers } from "@subframe/core"; +import { FeatherSearch } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Accordion } from "../Accordion"; +import { Avatar } from "../Avatar"; +import { Checkbox } from "../Checkbox"; + +export interface DropdownDividerProps + extends React.HTMLAttributes { + className?: string; +} + +const DropdownDivider = React.forwardRef( + function DropdownDivider( + { className, ...otherProps }: DropdownDividerProps, + ref + ) { + return ( +
+
+
+ ); + } +); + +export interface DropdownItemProps + extends React.ComponentProps { + children?: React.ReactNode; + icon?: React.ReactNode; + iconRight?: React.ReactNode; + image?: string; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const DropdownItem = React.forwardRef( + function DropdownItem( + { + children, + icon = null, + iconRight = null, + image, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: DropdownItemProps, + ref + ) { + return ( + +
+
+ +
+ {image ? ( + + ) : null} + {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} + {iconRight ? ( + + {iconRight} + + ) : null} +
+
+ ); + } +); + +export interface DropdownUserItemProps + extends Omit, "title"> { + title?: React.ReactNode; + subtitle?: React.ReactNode; + image?: string; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const DropdownUserItem = React.forwardRef< + HTMLDivElement, + DropdownUserItemProps +>(function DropdownUserItem( + { + title, + subtitle, + image, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: DropdownUserItemProps, + ref +) { + return ( +
+
+ +
+ + OR + +
+ {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+
+ ); +}); + +export interface DropdownCustomItemProps + extends React.HTMLAttributes { + children?: React.ReactNode; + selected?: boolean; + showCheckbox?: boolean; + size?: "medium" | "small" | "large"; + className?: string; +} + +const DropdownCustomItem = React.forwardRef< + HTMLDivElement, + DropdownCustomItemProps +>(function DropdownCustomItem( + { + children, + selected = false, + showCheckbox = false, + size = "medium", + className, + ...otherProps + }: DropdownCustomItemProps, + ref +) { + return ( +
+ + {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); + +export interface DropdownNestedItemProps + extends React.ComponentProps { + children?: React.ReactNode; + label?: React.ReactNode; + icon?: React.ReactNode; + className?: string; +} + +const DropdownNestedItem = React.forwardRef< + React.ElementRef, + DropdownNestedItemProps +>(function DropdownNestedItem( + { + children, + label, + icon = , + className, + ...otherProps + }: DropdownNestedItemProps, + ref +) { + return ( + + {icon ? ( + + {icon} + + ) : null} + {label ? ( + + {label} + + ) : null} + +
+ } + ref={ref} + {...otherProps} + > + {children ? ( +
{children}
+ ) : null} + + ); +}); + +export interface DropdownMenuSelectRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + showSearch?: boolean; + className?: string; +} + +const DropdownMenuSelectRoot = React.forwardRef< + HTMLDivElement, + DropdownMenuSelectRootProps +>(function DropdownMenuSelectRoot( + { + children, + showSearch = false, + className, + ...otherProps + }: DropdownMenuSelectRootProps, + ref +) { + return ( +
+
+
+ + +
+
+
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}); + +export const DropdownMenuSelect = Object.assign(DropdownMenuSelectRoot, { + DropdownDivider, + DropdownItem, + DropdownUserItem, + DropdownCustomItem, + DropdownNestedItem, +}); diff --git a/ui/components/DropdownMenuSelect/index.tsx b/ui/components/DropdownMenuSelect/index.tsx new file mode 100644 index 0000000..c6ad768 --- /dev/null +++ b/ui/components/DropdownMenuSelect/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { DropdownMenuSelect as DropdownMenuSelectComponent } from "./DropdownMenuSelect"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const DropdownMenuSelect = DropdownMenuSelectComponent; diff --git a/ui/components/EmptyState/EmptyState.tsx b/ui/components/EmptyState/EmptyState.tsx new file mode 100644 index 0000000..4ba5566 --- /dev/null +++ b/ui/components/EmptyState/EmptyState.tsx @@ -0,0 +1,109 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Empty State — https://app.subframe.com/f023064a5ca0/library?component=Empty+State_9fcf03a9-ada9-4a3d-80bc-87ff6ce78023 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; + +export interface EmptyStateRootProps + extends Omit, "title"> { + variant?: "default" | "no-results" | "error"; + outlined?: boolean; + fillContainer?: boolean; + title?: React.ReactNode; + body?: React.ReactNode; + className?: string; +} + +const EmptyStateRoot = React.forwardRef( + function EmptyStateRoot( + { + variant = "default", + outlined = false, + fillContainer = false, + title, + body, + className, + ...otherProps + }: EmptyStateRootProps, + ref + ) { + return ( +
+
+
+ + + +
+ {title ? ( + + {title} + + ) : null} + {body ? ( + + {body} + + ) : null} +
+ +
+
+
+ ); + } +); + +export const EmptyState = EmptyStateRoot; diff --git a/ui/components/EmptyState/index.tsx b/ui/components/EmptyState/index.tsx new file mode 100644 index 0000000..5d5aa8a --- /dev/null +++ b/ui/components/EmptyState/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { EmptyState as EmptyStateComponent } from "./EmptyState"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const EmptyState = EmptyStateComponent; diff --git a/ui/components/ErrorState/ErrorState.tsx b/ui/components/ErrorState/ErrorState.tsx new file mode 100644 index 0000000..881642c --- /dev/null +++ b/ui/components/ErrorState/ErrorState.tsx @@ -0,0 +1,81 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Error State — https://app.subframe.com/f023064a5ca0/library?component=Error+State_07f75db5-d9ee-406e-8c83-cb9e3bf687ee + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; + +export interface ErrorStateRootProps + extends Omit, "title"> { + variant?: "inline" | "full-width"; + hideCancel?: boolean; + title?: React.ReactNode; + description?: React.ReactNode; + retryLabel?: React.ReactNode; + cancelLabel?: React.ReactNode; + className?: string; +} + +const ErrorStateRoot = React.forwardRef( + function ErrorStateRoot( + { + variant = "inline", + hideCancel = false, + title, + description, + retryLabel, + cancelLabel, + className, + ...otherProps + }: ErrorStateRootProps, + ref + ) { + return ( +
+
+
+ + {title ? ( + + {title} + + ) : null} +
+
+ {description ? ( + + {description} + + ) : null} +
+ + +
+
+
+
+ ); + } +); + +export const ErrorState = ErrorStateRoot; diff --git a/ui/components/ErrorState/index.tsx b/ui/components/ErrorState/index.tsx new file mode 100644 index 0000000..2e2004f --- /dev/null +++ b/ui/components/ErrorState/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ErrorState as ErrorStateComponent } from "./ErrorState"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const ErrorState = ErrorStateComponent; diff --git a/ui/components/FileUpload/FileUpload.tsx b/ui/components/FileUpload/FileUpload.tsx new file mode 100644 index 0000000..2b720b0 --- /dev/null +++ b/ui/components/FileUpload/FileUpload.tsx @@ -0,0 +1,200 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * File Upload — https://app.subframe.com/f023064a5ca0/library?component=File+Upload_28a3336a-2bdd-4d1c-8dc1-75c9d6eef452 + * Progress — https://app.subframe.com/f023064a5ca0/library?component=Progress_60964db0-a1bf-428b-b9d5-f34cdf58ea77 + */ + +import React from "react"; +import { FeatherFile } from "@subframe/core"; +import { FeatherUpload } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; +import { Progress } from "../Progress"; + +export interface FileUploadRootProps + extends React.HTMLAttributes { + fullwith?: boolean; + state?: "default" | "hover" | "uploading" | "error" | "uploaded"; + filename?: React.ReactNode; + fileSize?: React.ReactNode; + uploadProgress?: number; + progressText?: React.ReactNode; + errorMessage?: React.ReactNode; + browseLabel?: React.ReactNode; + cancelLabel?: React.ReactNode; + className?: string; +} + +const FileUploadRoot = React.forwardRef( + function FileUploadRoot( + { + fullwith = false, + state = "default", + filename, + fileSize, + uploadProgress = 60, + progressText, + errorMessage, + browseLabel, + cancelLabel, + className, + ...otherProps + }: FileUploadRootProps, + ref + ) { + return ( +
+
+
+
+ + + Drop your invoice file here + +
+ + Drop your invoice file here or + +
+
+
+ +
+ {errorMessage ? ( + + {errorMessage} + + ) : null} +
+
+
+ {filename ? ( + + {filename} + + ) : null} + +
+ {progressText ? ( + + {progressText} + + ) : null} +
+ +
+
+
+
+ +
+ {filename ? ( + + {filename} + + ) : null} + {fileSize ? ( + + {fileSize} + + ) : null} +
+
+ +
+
+
+ ); + } +); + +export const FileUpload = FileUploadRoot; diff --git a/ui/components/FileUpload/index.tsx b/ui/components/FileUpload/index.tsx new file mode 100644 index 0000000..267705a --- /dev/null +++ b/ui/components/FileUpload/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { FileUpload as FileUploadComponent } from "./FileUpload"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const FileUpload = FileUploadComponent; diff --git a/ui/components/FilterBar/FilterBar.tsx b/ui/components/FilterBar/FilterBar.tsx new file mode 100644 index 0000000..f9a2b67 --- /dev/null +++ b/ui/components/FilterBar/FilterBar.tsx @@ -0,0 +1,212 @@ +// @subframe/sync-disable +"use client"; +/* + * Documentation: + * Filter Bar — https://app.subframe.com/f023064a5ca0/library?component=Filter+Bar_f2cf6974-3297-4da8-8613-cd00dc1c228e + */ + +import React from "react"; +import { FeatherBriefcase } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherRotateCcw } from "@subframe/core"; +import { FeatherSearch } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface StatusFilterButtonProps + extends React.HTMLAttributes { + icon?: React.ReactNode; + label?: React.ReactNode; + showCount?: boolean; + count?: React.ReactNode; + dropdown?: React.ReactNode; + className?: string; +} + +const StatusFilterButton = React.forwardRef< + HTMLDivElement, + StatusFilterButtonProps +>(function StatusFilterButton( + { + icon = , + label, + showCount = false, + count, + dropdown, + className, + ...otherProps + }: StatusFilterButtonProps, + ref +) { + return ( + + +
+
+
+
+ {count ? ( + + {count} + + ) : null} +
+ {icon ? ( + + {icon} + + ) : null} + {label ? ( + + {label} + + ) : null} +
+
+ +
+
+ + + + {dropdown ? ( +
{dropdown}
+ ) : null} +
+
+ + ); +}); + +export interface FilterBarRootProps + extends React.HTMLAttributes { + hideSearch?: boolean; + showActiveFilters?: boolean; + searchValue?: string; + onSearchChange?: (value: string) => void; + onClearFilters?: () => void; + children?: React.ReactNode; + activeFilters?: React.ReactNode; + className?: string; +} + +const FilterBarRoot = React.forwardRef( + function FilterBarRoot( + { + hideSearch = false, + showActiveFilters = false, + searchValue, + onSearchChange = () => {}, + onClearFilters = () => {}, + children, + activeFilters, + className, + ...otherProps + }: FilterBarRootProps, + ref + ) { + return ( +
+
+
+ + onSearchChange(event.target.value)} + type="text" + /> +
+
+ {children ? ( +
{children}
+ ) : null} +
+
+ {activeFilters ? ( +
{activeFilters}
+ ) : null} +
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onClearFilters(); + } + }} + > + + + Zurücksetzen + +
+
+
+ ); + } +); + +export const FilterBar = Object.assign(FilterBarRoot, { + StatusFilterButton, +}); diff --git a/ui/components/FilterBar/index.tsx b/ui/components/FilterBar/index.tsx new file mode 100644 index 0000000..ff19c61 --- /dev/null +++ b/ui/components/FilterBar/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { FilterBar as FilterBarComponent } from "./FilterBar"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const FilterBar = FilterBarComponent; diff --git a/ui/components/FormSaveRetry/FormSaveRetry.tsx b/ui/components/FormSaveRetry/FormSaveRetry.tsx new file mode 100644 index 0000000..818a66b --- /dev/null +++ b/ui/components/FormSaveRetry/FormSaveRetry.tsx @@ -0,0 +1,55 @@ +"use client"; +/* + * Documentation: + * Form save Retry — https://app.subframe.com/f023064a5ca0/library?component=Form+save+Retry_db7627dd-19cb-457b-bbfd-6fe16d620fe0 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface FormSaveRetryRootProps + extends Omit, "title"> { + variant?: "default"; + title?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const FormSaveRetryRoot = React.forwardRef< + HTMLDivElement, + FormSaveRetryRootProps +>(function FormSaveRetryRoot( + { + variant = "default", + title, + children, + className, + ...otherProps + }: FormSaveRetryRootProps, + ref +) { + return ( +
+
+ + {children ? ( +
+ {children} +
+ ) : null} +
+
+ ); +}); + +export const FormSaveRetry = FormSaveRetryRoot; diff --git a/ui/components/FormSaveRetry/index.tsx b/ui/components/FormSaveRetry/index.tsx new file mode 100644 index 0000000..5764544 --- /dev/null +++ b/ui/components/FormSaveRetry/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { FormSaveRetry as FormSaveRetryComponent } from "./FormSaveRetry"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const FormSaveRetry = FormSaveRetryComponent; diff --git a/ui/components/HeaderNavigation/HeaderNavigation.tsx b/ui/components/HeaderNavigation/HeaderNavigation.tsx new file mode 100644 index 0000000..791b010 --- /dev/null +++ b/ui/components/HeaderNavigation/HeaderNavigation.tsx @@ -0,0 +1,134 @@ +"use client"; +/* + * Documentation: + * Breadcrumbs — https://app.subframe.com/f023064a5ca0/library?component=Breadcrumbs_8898334b-a66f-4ee8-8bd1-afcfa8e37cc0 + * Header Navigation — https://app.subframe.com/f023064a5ca0/library?component=Header+Navigation_9abee202-1896-4bb8-b899-63d76af2825e + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + * Search Input — https://app.subframe.com/f023064a5ca0/library?component=Search+Input_75a35ed3-52c0-4435-8586-e390eb77d723 + */ + +import React from "react"; +import { FeatherBell } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Breadcrumbs } from "../Breadcrumbs"; +import { IconButton } from "../IconButton"; +import { SearchInput } from "../SearchInput"; + +export interface NavItemProps extends React.HTMLAttributes { + selected?: boolean; + children?: React.ReactNode; + icon?: React.ReactNode; + className?: string; +} + +const NavItem = React.forwardRef(function NavItem( + { + selected = false, + children, + icon = null, + className, + ...otherProps + }: NavItemProps, + ref +) { + return ( +
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export interface HeaderNavigationRootProps + extends React.HTMLAttributes { + leftSlot?: React.ReactNode; + breadcrumbs?: React.ReactNode[]; + className?: string; +} + +const HeaderNavigationRoot = React.forwardRef< + HTMLDivElement, + HeaderNavigationRootProps +>(function HeaderNavigationRoot( + { + leftSlot, + breadcrumbs = ["Übersicht"], + className, + ...otherProps + }: HeaderNavigationRootProps, + ref +) { + return ( +
+
+
+ + {breadcrumbs.map((breadcrumb, index) => ( + + {index > 0 ? : null} + + {breadcrumb} + + + ))} + +
+ {leftSlot ? ( +
+ {leftSlot} +
+ ) : null} +
+ + + + } /> +
+
+
+ ); +}); + +export const HeaderNavigation = Object.assign(HeaderNavigationRoot, { + NavItem, +}); diff --git a/ui/components/HeaderNavigation/index.tsx b/ui/components/HeaderNavigation/index.tsx new file mode 100644 index 0000000..1112b6d --- /dev/null +++ b/ui/components/HeaderNavigation/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { HeaderNavigation as HeaderNavigationComponent } from "./HeaderNavigation"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const HeaderNavigation = HeaderNavigationComponent; diff --git a/ui/components/HelpCircle/HelpCircle.tsx b/ui/components/HelpCircle/HelpCircle.tsx new file mode 100644 index 0000000..aa90c51 --- /dev/null +++ b/ui/components/HelpCircle/HelpCircle.tsx @@ -0,0 +1,87 @@ +"use client"; +/* + * Documentation: + * Help Circle — https://app.subframe.com/f023064a5ca0/library?component=Help+Circle_bdf8292f-ad00-49d7-9ca6-98c24f58f842 + */ + +import React from "react"; +import { FeatherHelpCircle } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface HelpCircleRootProps + extends React.HTMLAttributes { + icon?: React.ReactNode; + size?: "small" | "medium" | "large"; + shape?: "default" | "background"; + tooltipText?: React.ReactNode; + className?: string; +} + +const HelpCircleRoot = React.forwardRef( + function HelpCircleRoot( + { + icon = , + size = "medium", + shape = "default", + tooltipText, + className, + ...otherProps + }: HelpCircleRootProps, + ref + ) { + return ( + + + +
+ {icon ? ( + + {icon} + + ) : null} +
+
+ + +
+ {tooltipText ? ( + + {tooltipText} + + ) : null} +
+
+
+
+
+ ); + } +); + +export const HelpCircle = HelpCircleRoot; diff --git a/ui/components/HelpCircle/index.tsx b/ui/components/HelpCircle/index.tsx new file mode 100644 index 0000000..774b9b6 --- /dev/null +++ b/ui/components/HelpCircle/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { HelpCircle as HelpCircleComponent } from "./HelpCircle"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const HelpCircle = HelpCircleComponent; diff --git a/ui/components/Icon/Icon.tsx b/ui/components/Icon/Icon.tsx new file mode 100644 index 0000000..5509750 --- /dev/null +++ b/ui/components/Icon/Icon.tsx @@ -0,0 +1,132 @@ +"use client"; +/* + * Documentation: + * Icon — https://app.subframe.com/f023064a5ca0/library?component=Icon_e24ec5dc-9821-430d-8254-1d72a7d46218 + */ + +import React from "react"; +import { FeatherCircle } from "@subframe/core"; +import { FeatherCircleDashed } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface IconFeedbackIconProps + extends React.ComponentProps { + variant?: + | "primary" + | "secondary" + | "tertiary" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + size?: "medium" | "small" | "large"; + icon?: React.ReactNode; + className?: string; +} + +const IconFeedbackIcon = React.forwardRef< + React.ElementRef, + IconFeedbackIconProps +>(function IconFeedbackIcon( + { + variant = "primary", + size = "medium", + icon = , + className, + ...otherProps + }: IconFeedbackIconProps, + ref +) { + return icon ? ( + + {icon} + + ) : null; +}); + +export interface IconRootProps extends React.HTMLAttributes { + variant?: + | "primary" + | "secondary" + | "tertiary" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + size?: "extra-small" | "small" | "medium" | "large" | "extra-large"; + icon?: React.ReactNode; + className?: string; +} + +const IconRoot = React.forwardRef( + function IconRoot( + { + variant = "primary", + size = "medium", + icon = , + className, + ...otherProps + }: IconRootProps, + ref + ) { + return ( +
+ {icon ? ( + + {icon} + + ) : null} +
+ ); + } +); + +export const Icon = Object.assign(IconRoot, { + IconFeedbackIcon, +}); diff --git a/ui/components/Icon/index.tsx b/ui/components/Icon/index.tsx new file mode 100644 index 0000000..d1620da --- /dev/null +++ b/ui/components/Icon/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Icon as IconComponent } from "./Icon"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Icon = IconComponent; diff --git a/ui/components/IconButton/IconButton.tsx b/ui/components/IconButton/IconButton.tsx new file mode 100644 index 0000000..50ff7a7 --- /dev/null +++ b/ui/components/IconButton/IconButton.tsx @@ -0,0 +1,124 @@ +"use client"; +/* + * Documentation: + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + */ + +import React from "react"; +import { FeatherPlus } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface IconButtonRootProps + extends React.ButtonHTMLAttributes { + disabled?: boolean; + loading?: boolean; + variant?: + | "primary" + | "secondary" + | "tertiary" + | "ghost" + | "neutral" + | "destructive" + | "success" + | "warning" + | "information"; + size?: "large" | "medium" | "small" | "extra-small" | "2-x-small"; + square?: boolean; + icon?: React.ReactNode; + onClick?: (event: React.MouseEvent) => void; + className?: string; +} + +const IconButtonRoot = React.forwardRef( + function IconButtonRoot( + { + disabled = false, + loading = false, + variant = "primary", + size = "medium", + square = false, + icon = , + className, + type = "button", + ...otherProps + }: IconButtonRootProps, + ref + ) { + return ( + + ); + } +); + +export const IconButton = IconButtonRoot; diff --git a/ui/components/IconButton/index.tsx b/ui/components/IconButton/index.tsx new file mode 100644 index 0000000..0f1bbef --- /dev/null +++ b/ui/components/IconButton/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { IconButton as IconButtonComponent } from "./IconButton"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const IconButton = IconButtonComponent; diff --git a/ui/components/IconWithBackground/IconWithBackground.tsx b/ui/components/IconWithBackground/IconWithBackground.tsx new file mode 100644 index 0000000..b768f7a --- /dev/null +++ b/ui/components/IconWithBackground/IconWithBackground.tsx @@ -0,0 +1,160 @@ +"use client"; +/* + * Documentation: + * Icon with Background — https://app.subframe.com/f023064a5ca0/library?component=Icon+with+Background_4a18e9fc-cea3-46ca-8c53-2412462342ad + */ + +import React from "react"; +import { FeatherFlag } from "@subframe/core"; +import { FeatherMail } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface IconFeedbackIconProps + extends React.ComponentProps { + variant?: + | "primary" + | "secondary" + | "tertiary" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + size?: "medium" | "small" | "large"; + icon?: React.ReactNode; + className?: string; +} + +const IconFeedbackIcon = React.forwardRef< + React.ElementRef, + IconFeedbackIconProps +>(function IconFeedbackIcon( + { + variant = "primary", + size = "medium", + icon = , + className, + ...otherProps + }: IconFeedbackIconProps, + ref +) { + return icon ? ( + + {icon} + + ) : null; +}); + +export interface IconWithBackgroundRootProps + extends React.HTMLAttributes { + variant?: + | "primary" + | "secondary" + | "tertiary" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + size?: + | "extra-small" + | "small" + | "medium" + | "large" + | "extra-large" + | "2-x-large" + | "3-x-large"; + round?: boolean; + icon?: React.ReactNode; + className?: string; +} + +const IconWithBackgroundRoot = React.forwardRef< + HTMLDivElement, + IconWithBackgroundRootProps +>(function IconWithBackgroundRoot( + { + variant = "primary", + size = "medium", + round = false, + icon = , + className, + ...otherProps + }: IconWithBackgroundRootProps, + ref +) { + return ( +
+ {icon ? ( + + {icon} + + ) : null} +
+ ); +}); + +export const IconWithBackground = Object.assign(IconWithBackgroundRoot, { + IconFeedbackIcon, +}); diff --git a/ui/components/IconWithBackground/index.tsx b/ui/components/IconWithBackground/index.tsx new file mode 100644 index 0000000..083a9f6 --- /dev/null +++ b/ui/components/IconWithBackground/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { IconWithBackground as IconWithBackgroundComponent } from "./IconWithBackground"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const IconWithBackground = IconWithBackgroundComponent; diff --git a/ui/components/InputOtp/InputOtp.tsx b/ui/components/InputOtp/InputOtp.tsx new file mode 100644 index 0000000..eff10f5 --- /dev/null +++ b/ui/components/InputOtp/InputOtp.tsx @@ -0,0 +1,139 @@ +"use client"; +/* + * Documentation: + * Input OTP — https://app.subframe.com/f023064a5ca0/library?component=Input+OTP_1a223fbe-dca0-4e44-a383-44bbc7db975d + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface InputProps + extends Omit, "placeholder"> { + placeholder?: React.ReactNode; + className?: string; +} + +const Input = React.forwardRef(function Input( + { placeholder, className, ...otherProps }: InputProps, + ref +) { + return ( + + ); +}); + +export interface OtpInputItemProps + extends React.HTMLAttributes { + success?: boolean; + error?: boolean; + disabled?: boolean; + children?: React.ReactNode; + className?: string; +} + +const OtpInputItem = React.forwardRef( + function OtpInputItem( + { + success = false, + error = false, + disabled = false, + children, + className, + ...otherProps + }: OtpInputItemProps, + ref + ) { + return ( +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); + } +); + +export interface InputOtpRootProps + extends Omit, "title"> { + state?: "default" | "error" | "disabled"; + title?: React.ReactNode; + helperText?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const InputOtpRoot = React.forwardRef( + function InputOtpRoot( + { + state = "default", + title, + helperText, + children, + className, + ...otherProps + }: InputOtpRootProps, + ref + ) { + return ( +
+ {title ? ( + + {title} + + ) : null} + {children ? ( +
{children}
+ ) : null} + {helperText ? ( + + {helperText} + + ) : null} +
+ ); + } +); + +export const InputOtp = Object.assign(InputOtpRoot, { + Input, + OtpInputItem, +}); diff --git a/ui/components/InputOtp/index.tsx b/ui/components/InputOtp/index.tsx new file mode 100644 index 0000000..06b2231 --- /dev/null +++ b/ui/components/InputOtp/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { InputOtp as InputOtpComponent } from "./InputOtp"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const InputOtp = InputOtpComponent; diff --git a/ui/components/KanbanCard/KanbanCard.tsx b/ui/components/KanbanCard/KanbanCard.tsx new file mode 100644 index 0000000..8965fac --- /dev/null +++ b/ui/components/KanbanCard/KanbanCard.tsx @@ -0,0 +1,143 @@ +"use client"; +/* + * Documentation: + * Icon — https://app.subframe.com/f023064a5ca0/library?component=Icon_e24ec5dc-9821-430d-8254-1d72a7d46218 + * KanbanCard — https://app.subframe.com/f023064a5ca0/library?component=KanbanCard_27235bcb-e61b-4880-9ae6-3f12d290a434 + */ + +import React from "react"; +import { FeatherFlag } from "@subframe/core"; +import { FeatherLock } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Icon } from "../Icon"; + +export interface KanbanCardRootProps + extends React.HTMLAttributes { + tagsSlot?: React.ReactNode; + hideTags?: boolean; + titleSlot?: React.ReactNode; + valueSlot?: React.ReactNode; + priceLocked?: boolean; + footerLeftSlot?: React.ReactNode; + footerRightSlot?: React.ReactNode; + flagVariant?: + | "primary" + | "secondary" + | "tertiary" + | "destructive" + | "neutral" + | "success" + | "warning" + | "information"; + keepTagRowSpace?: boolean; + className?: string; +} + +const KanbanCardRoot = React.forwardRef( + function KanbanCardRoot( + { + tagsSlot, + hideTags = false, + titleSlot, + valueSlot, + priceLocked = false, + footerLeftSlot, + footerRightSlot, + flagVariant = "primary", + keepTagRowSpace = false, + className, + ...otherProps + }: KanbanCardRootProps, + ref + ) { + return ( +
+
+ } + /> + {tagsSlot ? ( +
+ {tagsSlot} +
+ ) : null} +
+ {titleSlot ? ( +
+ {titleSlot} +
+ ) : null} +
+ {valueSlot ? ( +
+ {valueSlot} +
+ ) : null} +
+ + + Umsatz gesperrt + +
+
+
+
+ {footerLeftSlot ? ( +
{footerLeftSlot}
+ ) : null} + {footerRightSlot ? ( +
{footerRightSlot}
+ ) : null} +
+
+
+ ); + } +); + +export const KanbanCard = KanbanCardRoot; diff --git a/ui/components/KanbanCard/index.tsx b/ui/components/KanbanCard/index.tsx new file mode 100644 index 0000000..518d61b --- /dev/null +++ b/ui/components/KanbanCard/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { KanbanCard as KanbanCardComponent } from "./KanbanCard"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const KanbanCard = KanbanCardComponent; diff --git a/ui/components/KanbanColumn/KanbanColumn.tsx b/ui/components/KanbanColumn/KanbanColumn.tsx new file mode 100644 index 0000000..c0b6f23 --- /dev/null +++ b/ui/components/KanbanColumn/KanbanColumn.tsx @@ -0,0 +1,171 @@ +"use client"; +/* + * Documentation: + * KanbanColumn — https://app.subframe.com/f023064a5ca0/library?component=KanbanColumn_bbc08458-14aa-4512-a807-e673a64315ab + */ + +import React from "react"; +import { FeatherChevronsLeft } from "@subframe/core"; +import { FeatherChevronsRight } from "@subframe/core"; +import { FeatherLock } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface KanbanColumnRootProps + extends React.HTMLAttributes { + collapsed?: boolean; + name?: React.ReactNode; + price?: React.ReactNode; + icon?: React.ReactNode; + badge?: React.ReactNode; + cards?: React.ReactNode; + sumLocked?: boolean; + className?: string; +} + +const KanbanColumnRoot = React.forwardRef< + HTMLDivElement, + KanbanColumnRootProps +>(function KanbanColumnRoot( + { + collapsed = false, + name, + price, + icon = , + badge, + cards, + sumLocked = false, + className, + ...otherProps + }: KanbanColumnRootProps, + ref +) { + return ( +
+
+
+ +
+
+
+ {name ? ( + + {name} + + ) : null} +
+ {name ? ( + + {name} + + ) : null} + {badge ? ( +
+ {badge} +
+ ) : null} +
+
+
+ + + Summe gesperrt + +
+ {price ? ( + + {price} + + ) : null} + {icon ? ( + + {icon} + + ) : null} +
+
+
+
+ {cards ? ( +
+ {cards} +
+ ) : null} +
+ ); +}); + +export const KanbanColumn = KanbanColumnRoot; diff --git a/ui/components/KanbanColumn/index.tsx b/ui/components/KanbanColumn/index.tsx new file mode 100644 index 0000000..bc88967 --- /dev/null +++ b/ui/components/KanbanColumn/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { KanbanColumn as KanbanColumnComponent } from "./KanbanColumn"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const KanbanColumn = KanbanColumnComponent; diff --git a/ui/components/KanbanSubbar/KanbanSubbar.tsx b/ui/components/KanbanSubbar/KanbanSubbar.tsx new file mode 100644 index 0000000..8c0bd33 --- /dev/null +++ b/ui/components/KanbanSubbar/KanbanSubbar.tsx @@ -0,0 +1,49 @@ +"use client"; +/* + * Documentation: + * Kanban Subbar — https://app.subframe.com/f023064a5ca0/library?component=Kanban+Subbar_75a855a6-3419-4c84-97bd-540dd2f180a4 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface KanbanSubbarRootProps + extends React.HTMLAttributes { + header?: React.ReactNode; + badge?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const KanbanSubbarRoot = React.forwardRef< + HTMLDivElement, + KanbanSubbarRootProps +>(function KanbanSubbarRoot( + { header, badge, children, className, ...otherProps }: KanbanSubbarRootProps, + ref +) { + return ( +
+
+ {header ? ( + + {header} + + ) : null} + {badge ?
{badge}
: null} +
+ {children ? ( +
{children}
+ ) : null} +
+ ); +}); + +export const KanbanSubbar = KanbanSubbarRoot; diff --git a/ui/components/KanbanSubbar/index.tsx b/ui/components/KanbanSubbar/index.tsx new file mode 100644 index 0000000..b5b91e2 --- /dev/null +++ b/ui/components/KanbanSubbar/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { KanbanSubbar as KanbanSubbarComponent } from "./KanbanSubbar"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const KanbanSubbar = KanbanSubbarComponent; diff --git a/ui/components/LineChart/LineChart.tsx b/ui/components/LineChart/LineChart.tsx new file mode 100644 index 0000000..07fcb35 --- /dev/null +++ b/ui/components/LineChart/LineChart.tsx @@ -0,0 +1,40 @@ +"use client"; +/* + * Documentation: + * Line Chart — https://app.subframe.com/f023064a5ca0/library?component=Line+Chart_22944dd2-3cdd-42fd-913a-1b11a3c1d16d + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface LineChartRootProps + extends React.ComponentProps { + className?: string; +} + +const LineChartRoot = React.forwardRef< + React.ElementRef, + LineChartRootProps +>(function LineChartRoot( + { className, ...otherProps }: LineChartRootProps, + ref +) { + return ( + + ); +}); + +export const LineChart = LineChartRoot; diff --git a/ui/components/LineChart/index.tsx b/ui/components/LineChart/index.tsx new file mode 100644 index 0000000..e339fde --- /dev/null +++ b/ui/components/LineChart/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { LineChart as LineChartComponent } from "./LineChart"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const LineChart = LineChartComponent; diff --git a/ui/components/LinkButton/LinkButton.tsx b/ui/components/LinkButton/LinkButton.tsx new file mode 100644 index 0000000..0a905fb --- /dev/null +++ b/ui/components/LinkButton/LinkButton.tsx @@ -0,0 +1,107 @@ +"use client"; +/* + * Documentation: + * Link Button — https://app.subframe.com/f023064a5ca0/library?component=Link+Button_a4ee726a-774c-4091-8c49-55b659356024 + */ + +import React from "react"; +import { FeatherArrowRight } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface LinkButtonRootProps + extends React.ButtonHTMLAttributes { + disabled?: boolean; + variant?: "primary" | "subtle"; + size?: "large" | "medium" | "small"; + icon?: React.ReactNode; + children?: React.ReactNode; + iconRight?: React.ReactNode; + hideIcon?: boolean; + underline?: boolean; + onClick?: (event: React.MouseEvent) => void; + className?: string; +} + +const LinkButtonRoot = React.forwardRef( + function LinkButtonRoot( + { + disabled = false, + variant = "primary", + size = "medium", + icon = null, + children, + iconRight = , + hideIcon = false, + underline = false, + className, + type = "button", + ...otherProps + }: LinkButtonRootProps, + ref + ) { + return ( + + ); + } +); + +export const LinkButton = LinkButtonRoot; diff --git a/ui/components/LinkButton/index.tsx b/ui/components/LinkButton/index.tsx new file mode 100644 index 0000000..8d7e0ee --- /dev/null +++ b/ui/components/LinkButton/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { LinkButton as LinkButtonComponent } from "./LinkButton"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const LinkButton = LinkButtonComponent; diff --git a/ui/components/Loader/Loader.tsx b/ui/components/Loader/Loader.tsx new file mode 100644 index 0000000..d594016 --- /dev/null +++ b/ui/components/Loader/Loader.tsx @@ -0,0 +1,41 @@ +"use client"; +/* + * Documentation: + * Loader — https://app.subframe.com/f023064a5ca0/library?component=Loader_f2e570c8-e463-45c2-aae9-a960146bc5d5 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface LoaderRootProps + extends React.ComponentProps { + size?: "small" | "medium" | "large"; + className?: string; +} + +const LoaderRoot = React.forwardRef< + React.ElementRef, + LoaderRootProps +>(function LoaderRoot( + { size = "medium", className, ...otherProps }: LoaderRootProps, + ref +) { + return ( + + ); +}); + +export const Loader = LoaderRoot; diff --git a/ui/components/Loader/index.tsx b/ui/components/Loader/index.tsx new file mode 100644 index 0000000..e150ae1 --- /dev/null +++ b/ui/components/Loader/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Loader as LoaderComponent } from "./Loader"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Loader = LoaderComponent; diff --git a/ui/components/LoadingIndicatorSkeleton/LoadingIndicatorSkeleton.tsx b/ui/components/LoadingIndicatorSkeleton/LoadingIndicatorSkeleton.tsx new file mode 100644 index 0000000..72f17ba --- /dev/null +++ b/ui/components/LoadingIndicatorSkeleton/LoadingIndicatorSkeleton.tsx @@ -0,0 +1,79 @@ +"use client"; +/* + * Documentation: + * Loading Indicator Skeleton — https://app.subframe.com/f023064a5ca0/library?component=Loading+Indicator+Skeleton_08dee637-2ba5-4c5d-86d4-a531fd11481f + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface LoadingIndicatorSkeletonRootProps + extends React.HTMLAttributes { + variant?: "card" | "list"; + className?: string; +} + +const LoadingIndicatorSkeletonRoot = React.forwardRef< + HTMLDivElement, + LoadingIndicatorSkeletonRootProps +>(function LoadingIndicatorSkeletonRoot( + { + variant = "card", + className, + ...otherProps + }: LoadingIndicatorSkeletonRootProps, + ref +) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +}); + +export const LoadingIndicatorSkeleton = LoadingIndicatorSkeletonRoot; diff --git a/ui/components/LoadingIndicatorSkeleton/index.tsx b/ui/components/LoadingIndicatorSkeleton/index.tsx new file mode 100644 index 0000000..dfca183 --- /dev/null +++ b/ui/components/LoadingIndicatorSkeleton/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { LoadingIndicatorSkeleton as LoadingIndicatorSkeletonComponent } from "./LoadingIndicatorSkeleton"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const LoadingIndicatorSkeleton = LoadingIndicatorSkeletonComponent; diff --git a/ui/components/Logo/Logo.tsx b/ui/components/Logo/Logo.tsx new file mode 100644 index 0000000..d165849 --- /dev/null +++ b/ui/components/Logo/Logo.tsx @@ -0,0 +1,78 @@ +"use client"; +/* + * Documentation: + * Logo — https://app.subframe.com/f023064a5ca0/library?component=Logo_609493cb-7054-4018-bcb4-26d43de9ef41 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface LogoRootProps extends React.HTMLAttributes { + size?: "8" | "16" | "24" | "28" | "32" | "64" | "128"; + showIcon?: boolean; + hideText?: boolean; + className?: string; +} + +const LogoRoot = React.forwardRef( + function LogoRoot( + { + size = "28", + showIcon = false, + hideText = false, + className, + ...otherProps + }: LogoRootProps, + ref + ) { + return ( +
+ + +
+ ); + } +); + +export const Logo = LogoRoot; diff --git a/ui/components/Logo/index.tsx b/ui/components/Logo/index.tsx new file mode 100644 index 0000000..24a5c14 --- /dev/null +++ b/ui/components/Logo/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Logo as LogoComponent } from "./Logo"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Logo = LogoComponent; diff --git a/ui/components/MultiSelect/MultiSelect.tsx b/ui/components/MultiSelect/MultiSelect.tsx new file mode 100644 index 0000000..8f75d65 --- /dev/null +++ b/ui/components/MultiSelect/MultiSelect.tsx @@ -0,0 +1,322 @@ +"use client"; +/* + * Documentation: + * Multi Select — https://app.subframe.com/f023064a5ca0/library?component=Multi+Select_457d3920-2559-46a2-9dfe-c5b809028265 + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherUsers } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ItemProps extends React.HTMLAttributes { + icon?: React.ReactNode; + iconRight?: React.ReactNode; + image?: string; + children?: React.ReactNode; + selected?: boolean; + showCheckbox?: boolean; + className?: string; +} + +const Item = React.forwardRef(function Item( + { + icon = null, + iconRight = null, + image, + children, + selected = false, + showCheckbox = false, + className, + ...otherProps + }: ItemProps, + ref +) { + return ( +
+
+ +
+ {image ? ( + + ) : null} + {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} + {iconRight ? ( + + {iconRight} + + ) : null} +
+ ); +}); + +export interface MultiSelectRootProps + extends Omit, "placeholder"> { + disabled?: boolean; + error?: boolean; + hasValue?: boolean; + variant?: "outline" | "filled"; + borderless?: boolean; + noPadding?: boolean; + fillContainer?: boolean; + size?: "medium" | "small" | "large"; + showLabel?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + value?: React.ReactNode; + placeholder?: React.ReactNode; + icon?: React.ReactNode; + children?: React.ReactNode; + message?: React.ReactNode; + dropdown?: React.ReactNode; + className?: string; +} + +const MultiSelectRoot = React.forwardRef( + function MultiSelectRoot( + { + disabled = false, + error = false, + hasValue = false, + variant = "outline", + borderless = false, + noPadding = false, + fillContainer = false, + size = "medium", + showLabel = false, + label, + helpText, + value, + placeholder, + icon = , + children, + message, + dropdown, + className, + ...otherProps + }: MultiSelectRootProps, + ref + ) { + return ( + + +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+
+
+ {icon ? ( + + {icon} + + ) : null} + {value ? ( + + {value} + + ) : null} + {placeholder ? ( + + {placeholder} + + ) : null} + +
+
+ {message ? ( + + {message} + + ) : null} + {children ? ( +
+ {children} +
+ ) : null} +
+
+
+ + + {dropdown ? ( +
{dropdown}
+ ) : null} +
+
+
+ ); + } +); + +export const MultiSelect = Object.assign(MultiSelectRoot, { + Item, +}); diff --git a/ui/components/MultiSelect/index.tsx b/ui/components/MultiSelect/index.tsx new file mode 100644 index 0000000..c7ad74c --- /dev/null +++ b/ui/components/MultiSelect/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { MultiSelect as MultiSelectComponent } from "./MultiSelect"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const MultiSelect = MultiSelectComponent; diff --git a/ui/components/MultiSelectTags/MultiSelectTags.tsx b/ui/components/MultiSelectTags/MultiSelectTags.tsx new file mode 100644 index 0000000..c1d631b --- /dev/null +++ b/ui/components/MultiSelectTags/MultiSelectTags.tsx @@ -0,0 +1,214 @@ +"use client"; +/* + * Documentation: + * Multi Select Tags — https://app.subframe.com/f023064a5ca0/library?component=Multi+Select+Tags_8e4472a6-e1eb-47a4-8c95-c911e02208bf + */ + +import React from "react"; +import { FeatherChevronDown } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface MultiSelectTagsRootProps + extends Omit, "placeholder"> { + disabled?: boolean; + error?: boolean; + hasValue?: boolean; + variant?: "outline" | "filled"; + borderless?: boolean; + noPadding?: boolean; + fillContainer?: boolean; + size?: "medium" | "small" | "large"; + showLabel?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + value?: React.ReactNode; + placeholder?: React.ReactNode; + icon?: React.ReactNode; + tagChildren?: React.ReactNode; + message?: React.ReactNode; + dropdown?: React.ReactNode; + className?: string; +} + +const MultiSelectTagsRoot = React.forwardRef< + HTMLDivElement, + MultiSelectTagsRootProps +>(function MultiSelectTagsRoot( + { + disabled = false, + error = false, + hasValue = false, + variant = "outline", + borderless = false, + noPadding = false, + fillContainer = false, + size = "medium", + showLabel = false, + label, + helpText, + value, + placeholder, + icon = null, + tagChildren, + message, + dropdown, + className, + ...otherProps + }: MultiSelectTagsRootProps, + ref +) { + return ( + + +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+
+
+ {icon ? ( + + {icon} + + ) : null} + {tagChildren ? ( +
+ {tagChildren} +
+ ) : null} + {placeholder ? ( + + {placeholder} + + ) : null} + +
+
+ {message ? ( + + {message} + + ) : null} +
+
+
+ + + {dropdown ? ( +
{dropdown}
+ ) : null} +
+
+
+ ); +}); + +export const MultiSelectTags = MultiSelectTagsRoot; diff --git a/ui/components/MultiSelectTags/index.tsx b/ui/components/MultiSelectTags/index.tsx new file mode 100644 index 0000000..4972a41 --- /dev/null +++ b/ui/components/MultiSelectTags/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { MultiSelectTags as MultiSelectTagsComponent } from "./MultiSelectTags"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const MultiSelectTags = MultiSelectTagsComponent; diff --git a/ui/components/NodeWorkflowEngine/NodeWorkflowEngine.tsx b/ui/components/NodeWorkflowEngine/NodeWorkflowEngine.tsx new file mode 100644 index 0000000..dee28bd --- /dev/null +++ b/ui/components/NodeWorkflowEngine/NodeWorkflowEngine.tsx @@ -0,0 +1,901 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Node Workflow Engine — https://app.subframe.com/f023064a5ca0/library?component=Node+Workflow+Engine_e75d1b96-38eb-4380-aef0-a320a2d99e5c + */ + +import React from "react"; +import { FeatherAlertCircle } from "@subframe/core"; +import { FeatherArrowRightLeft } from "@subframe/core"; +import { FeatherBellRing } from "@subframe/core"; +import { FeatherCalendar } from "@subframe/core"; +import { FeatherCalendarClock } from "@subframe/core"; +import { FeatherCalendarPlus } from "@subframe/core"; +import { FeatherCalendarX } from "@subframe/core"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherClipboardCheck } from "@subframe/core"; +import { FeatherClipboardList } from "@subframe/core"; +import { FeatherClock } from "@subframe/core"; +import { FeatherFilter } from "@subframe/core"; +import { FeatherFolderPlus } from "@subframe/core"; +import { FeatherGitBranch } from "@subframe/core"; +import { FeatherGitMerge } from "@subframe/core"; +import { FeatherGitPullRequest } from "@subframe/core"; +import { FeatherGlobe } from "@subframe/core"; +import { FeatherInbox } from "@subframe/core"; +import { FeatherMail } from "@subframe/core"; +import { FeatherMailX } from "@subframe/core"; +import { FeatherPlus } from "@subframe/core"; +import { FeatherRepeat } from "@subframe/core"; +import { FeatherSignpost } from "@subframe/core"; +import { FeatherSquareCheck } from "@subframe/core"; +import { FeatherStickyNote } from "@subframe/core"; +import { FeatherTimer } from "@subframe/core"; +import { FeatherTrophy } from "@subframe/core"; +import { FeatherUserCheck } from "@subframe/core"; +import { FeatherUserCog } from "@subframe/core"; +import { FeatherUserPen } from "@subframe/core"; +import { FeatherUserPlus } from "@subframe/core"; +import { FeatherVariable } from "@subframe/core"; +import { FeatherWebhook } from "@subframe/core"; +import { FeatherZap } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface KatalogProps extends React.HTMLAttributes { + className?: string; +} + +const Katalog = React.forwardRef(function Katalog( + { className, ...otherProps }: KatalogProps, + ref +) { + return ( +
+
+ + Auslöser + +
+ + + + + + + + + + + + + + + + +
+
+
+
+ + Aktionen: Daten & CRM + +
+ + +
+
+
+
+ + Aktionen: Kommunikation + +
+ +
+
+
+
+ + Aktionen: Aufgaben + +
+ + +
+
+
+
+ + Logik + +
+ + + + + + + + +
+
+
+
+ + Integration + +
+ +
+ +
+
+
+
+ ); +}); + +export interface NodeItemProps + extends Omit, "title"> { + art?: + | "ausloeser" + | "aktion" + | "logik" + | "multi-logik-3" + | "multi-logik-4" + | "ereignis"; + farbe?: "gruen" | "violett" | "blau" | "rot" | "amber" | "grau"; + status?: "standard" | "laeuft" | "fehler" | "erfolgreich"; + icon?: React.ReactNode; + title?: React.ReactNode; + beschreibung?: React.ReactNode; + ausgewaehlt?: boolean; + inaktiv?: boolean; + portLabel1?: React.ReactNode; + portLabel2?: React.ReactNode; + connectedOutputPorts?: number[]; + onInputPortClick?: () => void; + onOutputPortClick?: (port: number) => void; + onAddAfter?: (port: number) => void; + className?: string; +} + +const NodeItem = React.forwardRef( + function NodeItem( + { + art = "aktion", + farbe = "blau", + status = "standard", + icon = , + title, + beschreibung, + ausgewaehlt = false, + inaktiv = false, + portLabel1, + portLabel2, + connectedOutputPorts = [], + onInputPortClick, + onOutputPortClick, + onAddAfter, + className, + ...otherProps + }: NodeItemProps, + ref + ) { + return ( +
+
+ onInputPortClick?.()} + /> +
+
+
+ onInputPortClick?.()} + /> +
+
+ {icon ? ( + + {icon} + + ) : null} +
+ +
+
+
+ onOutputPortClick?.(1)} + onAddClick={() => onAddAfter?.(1)} + /> +
+ onOutputPortClick?.(2)} + onAddClick={() => onAddAfter?.(2)} + /> +
+
+ onOutputPortClick?.(3)} + onAddClick={() => onAddAfter?.(3)} + /> +
+
+ onOutputPortClick?.(4)} + onAddClick={() => onAddAfter?.(4)} + /> +
+
+
+
+ + {title} + + + {beschreibung} + +
+
+ ); + } +); + +export interface PortProps extends React.HTMLAttributes { + label?: React.ReactNode; + richtung?: "keine" | "rechts" | "oben" | "unten"; + portType?: "input" | "output"; + portIndex?: number; + connected?: boolean; + onPortClick?: React.MouseEventHandler; + onAddClick?: React.MouseEventHandler; + className?: string; +} + +const Port = React.forwardRef(function Port( + { + label, + richtung = "keine", + portType, + portIndex, + connected = false, + onPortClick, + onAddClick, + className, + ...otherProps + }: PortProps, + ref +) { + const handlePortClick: React.MouseEventHandler = (event) => { + event.stopPropagation(); + onPortClick?.(event); + }; + + const handleAddClick: React.MouseEventHandler = (event) => { + event.stopPropagation(); + onAddClick?.(event); + }; + + return ( +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+ ); +}); + +export interface StatusBadgeProps extends React.HTMLAttributes { + status?: "fehler" | "laeuft" | "erfolgreich"; + className?: string; +} + +const StatusBadge = React.forwardRef( + function StatusBadge( + { status = "erfolgreich", className, ...otherProps }: StatusBadgeProps, + ref + ) { + return ( +
+ {status === "fehler" ? ( + + ) : ( + + )} + +
+ ); + } +); + +export interface NodeWorkflowEngineRootProps + extends Omit, "title"> { + art?: + | "neuer-kontakt" + | "kontakt-geaendert" + | "neue-anfrage" + | "lead-qualifiziert" + | "projekt-gewonnen" + | "status-gewechselt" + | "projekt-angelegt" + | "deadline-naht" + | "email-eingegangen" + | "email-unzustellbar" + | "formular-abgeschickt" + | "termin-gebucht" + | "termin-storniert" + | "ereignis-erstellt" + | "zeitplan" + | "webhook-empfangen" + | "kontakt-aendern" + | "notiz-anlegen" + | "benachrichtigung-senden" + | "aufgabe-anlegen" + | "aufgabe-zuweisen" + | "wenn-dann" + | "warten" + | "schleife" + | "oder" + | "und" + | "switch" + | "wert-setzen" + | "filter" + | "http-anfrage" + | "pds-synchronisieren"; + onInputPortClick?: () => void; + onOutputPortClick?: (port: number) => void; + onAddAfter?: (port: number) => void; + connectedOutputPorts?: number[]; + title?: React.ReactNode; + beschreibung?: React.ReactNode; + className?: string; +} + +const NodeWorkflowEngineRoot = React.forwardRef< + HTMLDivElement, + NodeWorkflowEngineRootProps +>(function NodeWorkflowEngineRoot( + { + art = "neuer-kontakt", + onInputPortClick, + onOutputPortClick, + onAddAfter, + connectedOutputPorts = [], + title, + beschreibung, + className, + ...otherProps + }: NodeWorkflowEngineRootProps, + ref +) { + return ( +
+ + ) : art === "http-anfrage" ? ( + + ) : art === "filter" ? ( + + ) : art === "wert-setzen" ? ( + + ) : art === "switch" ? ( + + ) : art === "und" ? ( + + ) : art === "oder" ? ( + + ) : art === "schleife" ? ( + + ) : art === "warten" ? ( + + ) : art === "wenn-dann" ? ( + + ) : art === "aufgabe-zuweisen" ? ( + + ) : art === "aufgabe-anlegen" ? ( + + ) : art === "benachrichtigung-senden" ? ( + + ) : art === "notiz-anlegen" ? ( + + ) : art === "kontakt-aendern" ? ( + + ) : art === "webhook-empfangen" ? ( + + ) : art === "zeitplan" ? ( + + ) : art === "ereignis-erstellt" ? ( + + ) : art === "termin-storniert" ? ( + + ) : art === "termin-gebucht" ? ( + + ) : art === "formular-abgeschickt" ? ( + + ) : art === "email-unzustellbar" ? ( + + ) : art === "email-eingegangen" ? ( + + ) : art === "deadline-naht" ? ( + + ) : art === "projekt-angelegt" ? ( + + ) : art === "status-gewechselt" ? ( + + ) : art === "projekt-gewonnen" ? ( + + ) : art === "lead-qualifiziert" ? ( + + ) : art === "neue-anfrage" ? ( + + ) : art === "kontakt-geaendert" ? ( + + ) : ( + + ) + } + title={ + title ?? + (art === "pds-synchronisieren" + ? "PDS synchronisieren" + : art === "http-anfrage" + ? "HTTP-Anfrage" + : art === "filter" + ? "Filter" + : art === "wert-setzen" + ? "Wert setzen" + : art === "switch" + ? "Switch" + : art === "und" + ? "Und" + : art === "oder" + ? "Oder" + : art === "schleife" + ? "Schleife" + : art === "warten" + ? "Warten" + : art === "wenn-dann" + ? "Wenn-Dann" + : art === "aufgabe-zuweisen" + ? "Aufgabe zuweisen" + : art === "aufgabe-anlegen" + ? "Aufgabe anlegen" + : art === "benachrichtigung-senden" + ? "Benachrichtigung senden" + : art === "notiz-anlegen" + ? "Notiz anlegen" + : art === "kontakt-aendern" + ? "Kontakt ändern" + : art === "webhook-empfangen" + ? "Webhook empfangen" + : art === "zeitplan" + ? "Zeitplan" + : art === "ereignis-erstellt" + ? "Ereignis erstellt" + : art === "termin-storniert" + ? "Termin storniert" + : art === "termin-gebucht" + ? "Termin gebucht" + : art === "formular-abgeschickt" + ? "Formular abgeschickt" + : art === "email-unzustellbar" + ? "E-Mail unzustellbar" + : art === "email-eingegangen" + ? "E-Mail eingegangen" + : art === "deadline-naht" + ? "Deadline naht" + : art === "projekt-angelegt" + ? "Projekt angelegt" + : art === "status-gewechselt" + ? "Status gewechselt" + : art === "projekt-gewonnen" + ? "Projekt gewonnen" + : art === "lead-qualifiziert" + ? "Lead qualifiziert" + : art === "neue-anfrage" + ? "Neue Anfrage" + : art === "kontakt-geaendert" + ? "Kontakt geändert" + : "Neuer Kontakt") + } + beschreibung={ + beschreibung ?? + (art === "pds-synchronisieren" + ? "Daten mit PDS austauschen" + : art === "http-anfrage" + ? "Externen Dienst aufrufen" + : art === "filter" + ? "Datensätze filtern" + : art === "wert-setzen" + ? "Variable setzen oder lesen" + : art === "switch" + ? "Mehrere Pfade verzweigen" + : art === "und" + ? "Wenn alle Bedingungen zutreffen" + : art === "oder" + ? "Wenn mind. eine Bedingung zutrifft" + : art === "schleife" + ? "Für jedes Element wiederholen" + : art === "warten" + ? "Pausiert, z. B. 3 Tage" + : art === "wenn-dann" + ? "Verzweigt nach Bedingung" + : art === "aufgabe-zuweisen" + ? "Person oder Team zuordnen" + : art === "aufgabe-anlegen" + ? "Neue Aufgabe erstellen" + : art === "benachrichtigung-senden" + ? "E-Mail, Push oder intern" + : art === "notiz-anlegen" + ? "Notiz am Datensatz speichern" + : art === "kontakt-aendern" + ? "Datensatz aktualisieren" + : art === "webhook-empfangen" + ? "Wenn ein externes System ruft" + : art === "zeitplan" + ? "Zu festen Zeiten, z. B. montags 8 Uhr" + : art === "ereignis-erstellt" + ? "Wenn ein Kalender-Eintrag entsteht" + : art === "termin-storniert" + ? "Wenn ein Termin abgesagt wird" + : art === "termin-gebucht" + ? "Wenn ein Termin gebucht wird" + : art === "formular-abgeschickt" + ? "Wenn ein Formular eingeht" + : art === "email-unzustellbar" + ? "Wenn eine Kampagnen-Mail bounct" + : art === "email-eingegangen" + ? "Wenn eine E-Mail ankommt" + : art === "deadline-naht" + ? "Wenn eine Frist bald abläuft" + : art === "projekt-angelegt" + ? "Wenn ein Projekt erstellt wird" + : art === "status-gewechselt" + ? "Wenn eine Karte die Spalte wechselt" + : art === "projekt-gewonnen" + ? "Wenn ein Projekt gewonnen wird" + : art === "lead-qualifiziert" + ? "Wenn ein Lead qualifiziert wird" + : art === "neue-anfrage" + ? "Wenn eine Anfrage eingeht" + : art === "kontakt-geaendert" + ? "Wenn sich Kontaktdaten ändern" + : "Wenn ein Kontakt angelegt wird") + } + inaktiv={false} + portLabel1={ + art === "und" + ? "Ja" + : art === "oder" + ? "Ja" + : art === "schleife" + ? "Weiter" + : art === "wenn-dann" + ? "Ja" + : "1" + } + portLabel2={ + art === "und" + ? "Nein" + : art === "oder" + ? "Nein" + : art === "schleife" + ? "Fertig" + : art === "wenn-dann" + ? "Nein" + : "2" + } + onInputPortClick={onInputPortClick} + onOutputPortClick={onOutputPortClick} + onAddAfter={onAddAfter} + connectedOutputPorts={connectedOutputPorts} + /> +
+ ); +}); + +export const NodeWorkflowEngine = Object.assign(NodeWorkflowEngineRoot, { + Katalog, + NodeItem, + Port, + StatusBadge, +}); diff --git a/ui/components/NodeWorkflowEngine/index.tsx b/ui/components/NodeWorkflowEngine/index.tsx new file mode 100644 index 0000000..815a6e6 --- /dev/null +++ b/ui/components/NodeWorkflowEngine/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { NodeWorkflowEngine as NodeWorkflowEngineComponent } from "./NodeWorkflowEngine"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const NodeWorkflowEngine = NodeWorkflowEngineComponent; diff --git a/ui/components/NumberInput/NumberInput.tsx b/ui/components/NumberInput/NumberInput.tsx new file mode 100644 index 0000000..9e4d3c5 --- /dev/null +++ b/ui/components/NumberInput/NumberInput.tsx @@ -0,0 +1,237 @@ +"use client"; +/* + * Documentation: + * Number Input — https://app.subframe.com/f023064a5ca0/library?component=Number+Input_06ebc0c5-994e-4eac-b855-f3c6d7f1bbbf + */ + +import React from "react"; +import { FeatherChevronDown } from "@subframe/core"; +import { FeatherChevronUp } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface StepperProps extends React.HTMLAttributes { + down?: boolean; + disabled?: boolean; + className?: string; +} + +const Stepper = React.forwardRef(function Stepper( + { down = false, disabled = false, className, ...otherProps }: StepperProps, + ref +) { + return ( +
+ {down ? ( + + ) : ( + + )} +
+ ); +}); + +export interface InputProps + extends Omit, "placeholder"> { + placeholder?: React.ReactNode; + className?: string; +} + +const Input = React.forwardRef(function Input( + { placeholder, className, ...otherProps }: InputProps, + ref +) { + return ( + + ); +}); + +export interface NumberInputRootProps + extends React.HTMLAttributes { + disabled?: boolean; + error?: boolean; + variant?: "outline" | "filled"; + borderless?: boolean; + noPadding?: boolean; + size?: "small" | "medium" | "large"; + fillContainer?: boolean; + showLabel?: boolean; + label?: React.ReactNode; + description?: React.ReactNode; + message?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const NumberInputRoot = React.forwardRef( + function NumberInputRoot( + { + disabled = false, + error = false, + variant = "outline", + borderless = false, + noPadding = false, + size = "medium", + fillContainer = false, + showLabel = false, + label, + description, + message, + children, + className, + ...otherProps + }: NumberInputRootProps, + ref + ) { + return ( +
+
+ {label ? ( + + {label} + + ) : null} + {description ? ( + + {description} + + ) : null} +
+
+
+ {children ? ( +
+ {children} +
+ ) : null} +
+ +
+ +
+
+
+ {message ? ( + + {message} + + ) : null} +
+
+
+ ); + } +); + +export const NumberInput = Object.assign(NumberInputRoot, { + Stepper, + Input, +}); diff --git a/ui/components/NumberInput/index.tsx b/ui/components/NumberInput/index.tsx new file mode 100644 index 0000000..fb20ca4 --- /dev/null +++ b/ui/components/NumberInput/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { NumberInput as NumberInputComponent } from "./NumberInput"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const NumberInput = NumberInputComponent; diff --git a/ui/components/PartialPageFailure/PartialPageFailure.tsx b/ui/components/PartialPageFailure/PartialPageFailure.tsx new file mode 100644 index 0000000..21cb493 --- /dev/null +++ b/ui/components/PartialPageFailure/PartialPageFailure.tsx @@ -0,0 +1,56 @@ +"use client"; +/* + * Documentation: + * Button — https://app.subframe.com/f023064a5ca0/library?component=Button_3b777358-b86b-40af-9327-891efc6826fe + * Partial page failure — https://app.subframe.com/f023064a5ca0/library?component=Partial+page+failure_db7ae858-338b-4089-8177-2dea8e3b97fe + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; +import { Button } from "../Button"; + +export interface PartialPageFailureRootProps + extends React.HTMLAttributes { + variant?: "inline" | "full-width"; + className?: string; +} + +const PartialPageFailureRoot = React.forwardRef< + HTMLDivElement, + PartialPageFailureRootProps +>(function PartialPageFailureRoot( + { variant = "inline", className, ...otherProps }: PartialPageFailureRootProps, + ref +) { + return ( +
+
+
+ + + {"Failed to load"} + +
+
+ + This section couldn't be loaded. Please try again. + + +
+
+
+ ); +}); + +export const PartialPageFailure = PartialPageFailureRoot; diff --git a/ui/components/PartialPageFailure/index.tsx b/ui/components/PartialPageFailure/index.tsx new file mode 100644 index 0000000..909acf9 --- /dev/null +++ b/ui/components/PartialPageFailure/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { PartialPageFailure as PartialPageFailureComponent } from "./PartialPageFailure"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const PartialPageFailure = PartialPageFailureComponent; diff --git a/ui/components/PasswordStrengthIndicator/PasswordStrengthIndicator.tsx b/ui/components/PasswordStrengthIndicator/PasswordStrengthIndicator.tsx new file mode 100644 index 0000000..3d05b51 --- /dev/null +++ b/ui/components/PasswordStrengthIndicator/PasswordStrengthIndicator.tsx @@ -0,0 +1,119 @@ +"use client"; +/* + * Documentation: + * Password Strength Indicator — https://app.subframe.com/f023064a5ca0/library?component=Password+Strength+Indicator_7dffbe18-7e06-4e2a-bc30-1d608ceca7d8 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface PasswordStrengthIndicatorRootProps + extends React.HTMLAttributes { + label?: React.ReactNode; + strengthText?: React.ReactNode; + helpText?: React.ReactNode; + strength?: "weak" | "fair" | "good" | "strong"; + className?: string; +} + +const PasswordStrengthIndicatorRoot = React.forwardRef< + HTMLDivElement, + PasswordStrengthIndicatorRootProps +>(function PasswordStrengthIndicatorRoot( + { + label, + strengthText, + helpText, + strength = "weak", + className, + ...otherProps + }: PasswordStrengthIndicatorRootProps, + ref +) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ {label ? ( + + {label} + + ) : null} + {strengthText ? ( + + {strengthText} + + ) : null} +
+ {helpText ? ( + + {helpText} + + ) : null} +
+
+ ); +}); + +export const PasswordStrengthIndicator = PasswordStrengthIndicatorRoot; diff --git a/ui/components/PasswordStrengthIndicator/index.tsx b/ui/components/PasswordStrengthIndicator/index.tsx new file mode 100644 index 0000000..7588c38 --- /dev/null +++ b/ui/components/PasswordStrengthIndicator/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { PasswordStrengthIndicator as PasswordStrengthIndicatorComponent } from "./PasswordStrengthIndicator"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const PasswordStrengthIndicator = PasswordStrengthIndicatorComponent; diff --git a/ui/components/PieChart/PieChart.tsx b/ui/components/PieChart/PieChart.tsx new file mode 100644 index 0000000..4b159f4 --- /dev/null +++ b/ui/components/PieChart/PieChart.tsx @@ -0,0 +1,37 @@ +"use client"; +/* + * Documentation: + * Pie Chart — https://app.subframe.com/f023064a5ca0/library?component=Pie+Chart_0654ccc7-054c-4f3a-8e9a-b7c81dd3963c + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface PieChartRootProps + extends React.ComponentProps { + className?: string; +} + +const PieChartRoot = React.forwardRef< + React.ElementRef, + PieChartRootProps +>(function PieChartRoot({ className, ...otherProps }: PieChartRootProps, ref) { + return ( + + ); +}); + +export const PieChart = PieChartRoot; diff --git a/ui/components/PieChart/index.tsx b/ui/components/PieChart/index.tsx new file mode 100644 index 0000000..47ae19f --- /dev/null +++ b/ui/components/PieChart/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { PieChart as PieChartComponent } from "./PieChart"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const PieChart = PieChartComponent; diff --git a/ui/components/Progress/Progress.tsx b/ui/components/Progress/Progress.tsx new file mode 100644 index 0000000..0b55b02 --- /dev/null +++ b/ui/components/Progress/Progress.tsx @@ -0,0 +1,133 @@ +"use client"; +/* + * Documentation: + * Progress — https://app.subframe.com/f023064a5ca0/library?component=Progress_60964db0-a1bf-428b-b9d5-f34cdf58ea77 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface IndicatorProps + extends React.ComponentProps { + state?: "default" | "success" | "error"; + className?: string; +} + +const Indicator = React.forwardRef( + function Indicator( + { state = "default", className, ...otherProps }: IndicatorProps, + ref + ) { + return ( + +
+ + ); + } +); + +export interface ProgressRootProps + extends React.ComponentProps { + state?: "uploading" | "success" | "error"; + cardProgress?: boolean; + hideLabel?: boolean; + value?: number; + label?: React.ReactNode; + statusText?: React.ReactNode; + className?: string; +} + +const ProgressRoot = React.forwardRef( + function ProgressRoot( + { + state = "uploading", + cardProgress = false, + hideLabel = false, + value = 30, + label, + statusText, + className, + ...otherProps + }: ProgressRootProps, + ref + ) { + return ( + +
+
+ {label ? ( + + {label} + + ) : null} + {statusText ? ( + + {statusText} + + ) : null} +
+
+ +
+ {statusText ? ( + + {statusText} + + ) : null} +
+
+ ); + } +); + +export const Progress = Object.assign(ProgressRoot, { + Indicator, +}); diff --git a/ui/components/Progress/index.tsx b/ui/components/Progress/index.tsx new file mode 100644 index 0000000..ca63292 --- /dev/null +++ b/ui/components/Progress/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Progress as ProgressComponent } from "./Progress"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Progress = ProgressComponent; diff --git a/ui/components/ProgressCircle/ProgressCircle.tsx b/ui/components/ProgressCircle/ProgressCircle.tsx new file mode 100644 index 0000000..6a98ec4 --- /dev/null +++ b/ui/components/ProgressCircle/ProgressCircle.tsx @@ -0,0 +1,76 @@ +"use client"; +/* + * Documentation: + * Progress Circle — https://app.subframe.com/f023064a5ca0/library?component=Progress+Circle_91a72105-500a-46e2-997b-7e8cd29b5f81 + */ + +import React from "react"; +import { FeatherAlertTriangle } from "@subframe/core"; +import { FeatherCheck } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ProgressCircleRootProps + extends React.HTMLAttributes { + state?: "default" | "uploading" | "success" | "error"; + value?: number; + statusText?: React.ReactNode; + className?: string; +} + +const ProgressCircleRoot = React.forwardRef< + HTMLDivElement, + ProgressCircleRootProps +>(function ProgressCircleRoot( + { + state = "default", + value = 30, + statusText, + className, + ...otherProps + }: ProgressCircleRootProps, + ref +) { + return ( +
+ {statusText ? ( +
+ {statusText} +
+ ) : null} + + +
+ ); +}); + +export const ProgressCircle = ProgressCircleRoot; diff --git a/ui/components/ProgressCircle/index.tsx b/ui/components/ProgressCircle/index.tsx new file mode 100644 index 0000000..cacbd3d --- /dev/null +++ b/ui/components/ProgressCircle/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ProgressCircle as ProgressCircleComponent } from "./ProgressCircle"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const ProgressCircle = ProgressCircleComponent; diff --git a/ui/components/ProjectCard/ProjectCard.tsx b/ui/components/ProjectCard/ProjectCard.tsx new file mode 100644 index 0000000..02a7385 --- /dev/null +++ b/ui/components/ProjectCard/ProjectCard.tsx @@ -0,0 +1,357 @@ +// @subframe/sync-disable +"use client"; +/* + * Documentation: + * ProjectCard — https://app.subframe.com/f023064a5ca0/library?component=ProjectCard_f82a72d5-da31-4d61-b47b-6b6ebd18ff67 + */ + +import React from "react"; +import { FeatherCalendarClock } from "@subframe/core"; +import { FeatherFlag } from "@subframe/core"; +import { FeatherPlayCircle } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface PriorityIndicatorProps + extends React.HTMLAttributes { + variant?: "none" | "low" | "medium" | "high" | "critical"; + className?: string; +} + +const PriorityIndicator = React.forwardRef< + HTMLDivElement, + PriorityIndicatorProps +>(function PriorityIndicator( + { variant = "none", className, ...otherProps }: PriorityIndicatorProps, + ref +) { + return ( +
+
+
+
+
+
+
+ ); +}); + +export interface PriorityBadgeProps + extends React.HTMLAttributes { + variant?: "none" | "low" | "medium" | "high" | "critical"; + className?: string; +} + +const PriorityBadge = React.forwardRef( + function PriorityBadge( + { variant = "none", className, ...otherProps }: PriorityBadgeProps, + ref + ) { + return ( + + ); + } +); + +export interface ProjectCardRootProps + extends Omit, "title"> { + title?: React.ReactNode; + subtitle?: React.ReactNode; + tags?: React.ReactNode; + price?: React.ReactNode; + priorityLabel?: React.ReactNode; + avatar?: React.ReactNode; + startDate?: React.ReactNode; + endDate?: React.ReactNode; + appointmentLabel?: React.ReactNode; + secondAppointmentLabel?: React.ReactNode; + variant?: "none" | "low" | "medium" | "high" | "critical"; + hideBadge?: boolean; + hideIndicator?: boolean; + hideAvatar?: boolean; + hideDateRange?: boolean; + hideAppointment?: boolean; + hideSecondAppointment?: boolean; + className?: string; +} + +const ProjectCardRoot = React.forwardRef( + function ProjectCardRoot( + { + title, + subtitle, + tags, + price, + priorityLabel, + avatar, + startDate, + endDate, + appointmentLabel, + secondAppointmentLabel, + variant = "none", + hideBadge = false, + hideIndicator = false, + hideAvatar = false, + hideDateRange = false, + hideAppointment = false, + hideSecondAppointment = false, + className, + ...otherProps + }: ProjectCardRootProps, + ref + ) { + return ( +
+
+ {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+ {tags ? ( +
+
+ {tags} +
+
+ ) : null} +
+ {price ? ( + + {price} + + ) : null} +
+
+ + +
+ {avatar ? ( +
+ {avatar} +
+ ) : null} +
+
+
+
+ + {startDate ? ( + + {startDate} + + ) : null} +
+ {endDate ? ( + + {endDate} + + ) : null} + +
+
+ + {appointmentLabel ? ( + + {appointmentLabel} + + ) : null} +
+ {secondAppointmentLabel ? ( +
+ + + {secondAppointmentLabel} + +
+ ) : null} +
+
+ ); + } +); + +export const ProjectCard = Object.assign(ProjectCardRoot, { + PriorityIndicator, + PriorityBadge, +}); diff --git a/ui/components/ProjectCard/index.tsx b/ui/components/ProjectCard/index.tsx new file mode 100644 index 0000000..5218dcd --- /dev/null +++ b/ui/components/ProjectCard/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ProjectCard as ProjectCardComponent } from "./ProjectCard"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const ProjectCard = ProjectCardComponent; diff --git a/ui/components/RadioCardGroup/RadioCardGroup.tsx b/ui/components/RadioCardGroup/RadioCardGroup.tsx new file mode 100644 index 0000000..6f3a013 --- /dev/null +++ b/ui/components/RadioCardGroup/RadioCardGroup.tsx @@ -0,0 +1,96 @@ +"use client"; +/* + * Documentation: + * Radio Card Group — https://app.subframe.com/f023064a5ca0/library?component=Radio+Card+Group_6d5193b8-6043-4dc1-aad5-7f902ef872df + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface RadioCardProps + extends React.ComponentProps { + disabled?: boolean; + hideRadio?: boolean; + children?: React.ReactNode; + className?: string; +} + +const RadioCard = React.forwardRef( + function RadioCard( + { + disabled = false, + hideRadio = false, + children, + className, + ...otherProps + }: RadioCardProps, + ref + ) { + return ( + + + + ); + } +); + +export interface RadioCardGroupRootProps + extends React.ComponentProps { + children?: React.ReactNode; + value?: string; + onValueChange?: (value: string) => void; + className?: string; +} + +const RadioCardGroupRoot = React.forwardRef< + HTMLDivElement, + RadioCardGroupRootProps +>(function RadioCardGroupRoot( + { children, className, ...otherProps }: RadioCardGroupRootProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export const RadioCardGroup = Object.assign(RadioCardGroupRoot, { + RadioCard, +}); diff --git a/ui/components/RadioCardGroup/index.tsx b/ui/components/RadioCardGroup/index.tsx new file mode 100644 index 0000000..874bcbd --- /dev/null +++ b/ui/components/RadioCardGroup/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { RadioCardGroup as RadioCardGroupComponent } from "./RadioCardGroup"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const RadioCardGroup = RadioCardGroupComponent; diff --git a/ui/components/RadioGroup/RadioGroup.tsx b/ui/components/RadioGroup/RadioGroup.tsx new file mode 100644 index 0000000..2e1ffa8 --- /dev/null +++ b/ui/components/RadioGroup/RadioGroup.tsx @@ -0,0 +1,117 @@ +"use client"; +/* + * Documentation: + * Radio Group — https://app.subframe.com/f023064a5ca0/library?component=Radio+Group_c4b6300e-20b4-4f3e-8b9f-379a046674ca + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface OptionProps + extends React.ComponentProps { + label?: React.ReactNode; + disabled?: boolean; + className?: string; +} + +const Option = React.forwardRef(function Option( + { label, disabled = false, className, ...otherProps }: OptionProps, + ref +) { + return ( + + + + ); +}); + +export interface RadioGroupRootProps + extends React.ComponentProps { + error?: boolean; + horizontal?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + children?: React.ReactNode; + value?: string; + onValueChange?: (value: string) => void; + className?: string; +} + +const RadioGroupRoot = React.forwardRef( + function RadioGroupRoot( + { + error = false, + horizontal = false, + label, + helpText, + children, + className, + ...otherProps + }: RadioGroupRootProps, + ref + ) { + return ( + +
+ {label ? ( + + {label} + + ) : null} + {children ? ( +
+ {children} +
+ ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+ ); + } +); + +export const RadioGroup = Object.assign(RadioGroupRoot, { + Option, +}); diff --git a/ui/components/RadioGroup/index.tsx b/ui/components/RadioGroup/index.tsx new file mode 100644 index 0000000..1969b2b --- /dev/null +++ b/ui/components/RadioGroup/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { RadioGroup as RadioGroupComponent } from "./RadioGroup"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const RadioGroup = RadioGroupComponent; diff --git a/ui/components/SearchInput/SearchInput.tsx b/ui/components/SearchInput/SearchInput.tsx new file mode 100644 index 0000000..e099279 --- /dev/null +++ b/ui/components/SearchInput/SearchInput.tsx @@ -0,0 +1,218 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Badge Shortcut — https://app.subframe.com/f023064a5ca0/library?component=Badge+Shortcut_97bdb082-1124-4dd7-a335-b14b822d0157 + * Search Input — https://app.subframe.com/f023064a5ca0/library?component=Search+Input_75a35ed3-52c0-4435-8586-e390eb77d723 + */ + +import React from "react"; +import { FeatherLoader } from "@subframe/core"; +import { FeatherSearch } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { BadgeShortcut } from "../BadgeShortcut"; + +export interface InputProps + extends Omit, "placeholder"> { + placeholder?: React.ReactNode; + className?: string; +} + +const Input = React.forwardRef(function Input( + { placeholder, className, ...otherProps }: InputProps, + ref +) { + return ( + + ); +}); + +export interface SearchInputRootProps + extends React.HTMLAttributes { + disabled?: boolean; + error?: boolean; + state?: "default" | "loading" | "filled"; + variant?: "outline" | "filled"; + size?: "small" | "medium" | "large"; + fillContainer?: boolean; + borderless?: boolean; + noPadding?: boolean; + showLabel?: boolean; + label?: React.ReactNode; + helpText?: React.ReactNode; + message?: React.ReactNode; + hideShortcut?: boolean; + children?: React.ReactNode; + className?: string; +} + +const SearchInputRoot = React.forwardRef( + function SearchInputRoot( + { + disabled = false, + error = false, + state = "default", + variant = "outline", + size = "medium", + fillContainer = false, + borderless = false, + noPadding = false, + showLabel = false, + label, + helpText, + message, + hideShortcut = false, + children, + className, + ...otherProps + }: SearchInputRootProps, + ref + ) { + return ( +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+ + {children ? ( +
+ {children} +
+ ) : null} +
+ + K + +
+ +
+ +
+
+
+ {message ? ( + + {message} + + ) : null} +
+
+ ); + } +); + +export const SearchInput = Object.assign(SearchInputRoot, { + Input, +}); diff --git a/ui/components/SearchInput/index.tsx b/ui/components/SearchInput/index.tsx new file mode 100644 index 0000000..1fd8d09 --- /dev/null +++ b/ui/components/SearchInput/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SearchInput as SearchInputComponent } from "./SearchInput"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SearchInput = SearchInputComponent; diff --git a/ui/components/Select/Select.tsx b/ui/components/Select/Select.tsx new file mode 100644 index 0000000..599af76 --- /dev/null +++ b/ui/components/Select/Select.tsx @@ -0,0 +1,508 @@ +// @subframe/sync-disable +// @ts-nocheck +"use client"; +/* + * Documentation: + * Avatar — https://app.subframe.com/f023064a5ca0/library?component=Avatar_bec25ae6-5010-4485-b46b-cf79e3943ab2 + * Select — https://app.subframe.com/f023064a5ca0/library?component=Select_bb88f90b-8c43-4b73-9c2f-3558ce7838f3 + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import { FeatherChevronDown } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { Avatar } from "../Avatar"; + +export interface ContentProps + extends React.ComponentProps { + children?: React.ReactNode; + className?: string; +} + +const Content = React.forwardRef(function Content( + { children, className, ...otherProps }: ContentProps, + ref +) { + return children ? ( + +
+ {children} +
+
+ ) : null; +}); + +export interface ItemProps + extends Omit, "value"> { + value: React.ReactNode; + children?: React.ReactNode; + showItemText?: boolean; + content?: React.ReactNode; + className?: string; +} + +const Item = React.forwardRef(function Item( + { + value, + children, + showItemText = false, + content, + className, + ...otherProps + }: ItemProps, + ref +) { + return ( + +
+ + {children || value} + + {content ? ( +
+ {content} +
+ ) : null} + +
+
+ ); +}); + +export interface ItemTextProps + extends React.ComponentProps { + children?: React.ReactNode; + variant?: "option-1" | "selected"; + className?: string; +} + +const ItemText = React.forwardRef( + function ItemText( + { children, variant = "option-1", className, ...otherProps }: ItemTextProps, + ref + ) { + return children ? ( + + + {children} + + + ) : null; + } +); + +export interface TriggerProps + extends Omit< + React.ComponentProps, + "placeholder" + > { + placeholder?: React.ReactNode; + icon?: React.ReactNode; + variant?: "not-filled" | "disabled" | "filled"; + noPadding?: boolean; + showContent?: boolean; + selectedContent?: React.ReactNode; + className?: string; +} + +const Trigger = React.forwardRef( + function Trigger( + { + placeholder, + icon = null, + variant = "not-filled", + noPadding = false, + showContent = false, + selectedContent, + className, + ...otherProps + }: TriggerProps, + ref + ) { + return ( + + + + ); + } +); + +export interface TriggerValueProps + extends React.ComponentProps { + placeholder?: React.ReactNode; + variant?: "not-filled" | "disabled" | "filled"; + size?: "m" | "s" | "l"; + className?: string; +} + +const TriggerValue = React.forwardRef< + React.ElementRef, + TriggerValueProps +>(function TriggerValue( + { + placeholder, + variant = "not-filled", + size = "m", + className, + ...otherProps + }: TriggerValueProps, + ref +) { + return ( + + Value + + ); +}); + +export interface DividerProps extends React.HTMLAttributes { + className?: string; +} + +const Divider = React.forwardRef(function Divider( + { className, ...otherProps }: DividerProps, + ref +) { + return ( +
+
+
+ ); +}); + +export interface UserItemProps + extends Omit, "title"> { + value?: React.ReactNode; + title?: React.ReactNode; + subtitle?: React.ReactNode; + image?: string; + selected?: boolean; + className?: string; +} + +const UserItem = React.forwardRef( + function UserItem( + { + value, + title, + subtitle, + image, + selected = false, + className, + ...otherProps + }: UserItemProps, + ref + ) { + return ( +
+ + OR + +
+ {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+
+ ); + } +); + +export interface SelectRootProps + extends React.ComponentProps { + disabled?: boolean; + error?: boolean; + variant?: "outline" | "filled"; + borderless?: boolean; + noPadding?: boolean; + fillContainer?: boolean; + size?: "medium" | "small" | "large"; + showLabel?: boolean; + label?: React.ReactNode; + placeholder?: React.ReactNode; + helpText?: React.ReactNode; + icon?: React.ReactNode; + selectedContent?: React.ReactNode; + showContent?: boolean; + children?: React.ReactNode; + errorText?: React.ReactNode; + message?: React.ReactNode; + value?: string; + onValueChange?: (value: string) => void; + className?: string; +} + +const SelectRoot = React.forwardRef( + function SelectRoot( + { + disabled = false, + error = false, + variant = "outline", + borderless = false, + noPadding = false, + fillContainer = false, + size = "medium", + showLabel = false, + label, + placeholder, + helpText, + icon = null, + selectedContent, + showContent = false, + children, + errorText, + message, + className, + value, + defaultValue, + onValueChange, + open, + defaultOpen, + onOpenChange, + dir, + name, + autoComplete, + required, + form, + ...otherProps + }: SelectRootProps, + ref + ) { + return ( + +
+
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+
+
+
+ {selectedContent ? ( +
+ {selectedContent} +
+ ) : null} + +
+
+ {message ? ( + + {message} + + ) : null} +
+
+ + {children ? ( +
+ {children} +
+ ) : null} +
+
+
+ ); + } +); + +export const Select = Object.assign(SelectRoot, { + Content, + Item, + ItemText, + Trigger, + TriggerValue, + Divider, + UserItem, +}); diff --git a/ui/components/Select/index.tsx b/ui/components/Select/index.tsx new file mode 100644 index 0000000..8813d35 --- /dev/null +++ b/ui/components/Select/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Select as SelectComponent } from "./Select"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Select = SelectComponent; diff --git a/ui/components/SettingsPage/SettingsPage.tsx b/ui/components/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000..35aa3f0 --- /dev/null +++ b/ui/components/SettingsPage/SettingsPage.tsx @@ -0,0 +1,288 @@ +"use client"; +/* + * Documentation: + * Settings Page — https://app.subframe.com/f023064a5ca0/library?component=Settings+Page_53f07300-26e0-4199-9638-0ee2178f4587 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface SettingRowProps extends React.HTMLAttributes { + label?: React.ReactNode; + helpText?: React.ReactNode; + select?: React.ReactNode; + className?: string; +} + +const SettingRow = React.forwardRef( + function SettingRow( + { label, helpText, select, className, ...otherProps }: SettingRowProps, + ref + ) { + return ( +
+
+ {label ? ( + + {label} + + ) : null} + {helpText ? ( + + {helpText} + + ) : null} +
+ {select ? ( +
+ {select} +
+ ) : null} +
+ ); + } +); + +export interface SwitchSettingProps + extends React.HTMLAttributes { + label?: React.ReactNode; + description?: React.ReactNode; + _switch?: React.ReactNode; + className?: string; +} + +const SwitchSetting = React.forwardRef( + function SwitchSetting( + { + label, + description, + _switch, + className, + ...otherProps + }: SwitchSettingProps, + ref + ) { + return ( +
+
+ {label ? ( + + {label} + + ) : null} +
+ {description ? ( + + {description} + + ) : null} +
+
+
+ {_switch ? ( +
{_switch}
+ ) : null} +
+
+ ); + } +); + +export interface SectionHeaderProps + extends Omit, "title"> { + title?: React.ReactNode; + description?: React.ReactNode; + className?: string; +} + +const SectionHeader = React.forwardRef( + function SectionHeader( + { title, description, className, ...otherProps }: SectionHeaderProps, + ref + ) { + return ( +
+ {title ? ( + + {title} + + ) : null} + {description ? ( + + {description} + + ) : null} +
+ ); + } +); + +export interface NeutralCardProps extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const NeutralCard = React.forwardRef( + function NeutralCard( + { children, className, ...otherProps }: NeutralCardProps, + ref + ) { + return ( +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); + } +); + +export interface DividerProps extends React.HTMLAttributes { + className?: string; +} + +const Divider = React.forwardRef(function Divider( + { className, ...otherProps }: DividerProps, + ref +) { + return ( +
+ ); +}); + +export interface MultiColumnLayoutProps + extends React.HTMLAttributes { + colums?: React.ReactNode; + className?: string; +} + +const MultiColumnLayout = React.forwardRef< + HTMLDivElement, + MultiColumnLayoutProps +>(function MultiColumnLayout( + { colums, className, ...otherProps }: MultiColumnLayoutProps, + ref +) { + return ( +
+ {colums ? ( +
+ {colums} +
+ ) : null} +
+ ); +}); + +export interface SettingsPageRootProps + extends React.HTMLAttributes { + pageTitle?: React.ReactNode; + pageDescription?: React.ReactNode; + tabs?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const SettingsPageRoot = React.forwardRef< + HTMLDivElement, + SettingsPageRootProps +>(function SettingsPageRoot( + { + pageTitle, + pageDescription, + tabs, + children, + className, + ...otherProps + }: SettingsPageRootProps, + ref +) { + return ( +
+ {tabs ? ( +
{tabs}
+ ) : null} +
+
+ {pageTitle ? ( + + {pageTitle} + + ) : null} + {pageDescription ? ( + + {pageDescription} + + ) : null} +
+
+ {children ? ( +
+ {children} +
+ ) : null} +
+
+
+ ); +}); + +export const SettingsPage = Object.assign(SettingsPageRoot, { + SettingRow, + SwitchSetting, + SectionHeader, + NeutralCard, + Divider, + MultiColumnLayout, +}); diff --git a/ui/components/SettingsPage/index.tsx b/ui/components/SettingsPage/index.tsx new file mode 100644 index 0000000..3f74b89 --- /dev/null +++ b/ui/components/SettingsPage/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SettingsPage as SettingsPageComponent } from "./SettingsPage"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SettingsPage = SettingsPageComponent; diff --git a/ui/components/SidebarInside/SidebarInside.tsx b/ui/components/SidebarInside/SidebarInside.tsx new file mode 100644 index 0000000..485d1b0 --- /dev/null +++ b/ui/components/SidebarInside/SidebarInside.tsx @@ -0,0 +1,299 @@ +"use client"; +/* + * Documentation: + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + * Sidebar Inside — https://app.subframe.com/f023064a5ca0/library?component=Sidebar+Inside_f6538807-e4e9-4314-9b2f-aaed19760450 + */ + +import React from "react"; +import { FeatherCircleDashed } from "@subframe/core"; +import { FeatherSidebarClose } from "@subframe/core"; +import { FeatherSidebarOpen } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { IconButton } from "../IconButton"; + +export interface NavItemProps extends React.HTMLAttributes { + icon?: React.ReactNode; + children?: React.ReactNode; + selected?: boolean; + variant?: "icon-and-text" | "icon-only"; + className?: string; +} + +const NavItem = React.forwardRef(function NavItem( + { + icon = , + children, + selected = false, + variant = "icon-and-text", + className, + ...otherProps + }: NavItemProps, + ref +) { + return ( +
+
+
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+
+
+ ); +}); + +export interface NavSectionProps extends React.HTMLAttributes { + children?: React.ReactNode; + label?: React.ReactNode; + variant?: "default" | "subsection"; + showLabel?: boolean; + showDivider?: boolean; + className?: string; +} + +const NavSection = React.forwardRef( + function NavSection( + { + children, + label, + variant = "default", + showLabel = false, + showDivider = false, + className, + ...otherProps + }: NavSectionProps, + ref + ) { + return ( +
+
+ +
+
+ +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); + } +); + +export interface NavLabelProps extends React.HTMLAttributes { + label?: React.ReactNode; + className?: string; +} + +const NavLabel = React.forwardRef( + function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { + return ( +
+ {label ? ( + + {label} + + ) : null} +
+ ); + } +); + +export interface NavDividerProps extends React.HTMLAttributes { + className?: string; +} + +const NavDivider = React.forwardRef( + function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { + return ( +
+
+
+ ); + } +); + +export interface SubcomponentProps + extends React.HTMLAttributes { + sidebarButton?: "close" | "open"; + className?: string; +} + +const Subcomponent = React.forwardRef( + function Subcomponent( + { sidebarButton = "close", className, ...otherProps }: SubcomponentProps, + ref + ) { + return ( +
+ + ) : ( + + ) + } + /> +
+ ); + } +); + +export interface SidebarInsideRootProps + extends React.HTMLAttributes { + variant?: "open" | "closed"; + footer?: React.ReactNode; + children?: React.ReactNode; + visible?: boolean; + className?: string; +} + +const SidebarInsideRoot = React.forwardRef( + function SidebarInsideRoot( + { + variant = "open", + footer, + children, + visible = false, + className, + ...otherProps + }: SidebarInsideRootProps, + ref + ) { + return ( + + ); + } +); + +export const SidebarInside = Object.assign(SidebarInsideRoot, { + NavItem, + NavSection, + NavLabel, + NavDivider, + Subcomponent, +}); diff --git a/ui/components/SidebarInside/index.tsx b/ui/components/SidebarInside/index.tsx new file mode 100644 index 0000000..eccd6d6 --- /dev/null +++ b/ui/components/SidebarInside/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SidebarInside as SidebarInsideComponent } from "./SidebarInside"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SidebarInside = SidebarInsideComponent; diff --git a/ui/components/SidebarWithSections/SidebarWithSections.tsx b/ui/components/SidebarWithSections/SidebarWithSections.tsx new file mode 100644 index 0000000..901e705 --- /dev/null +++ b/ui/components/SidebarWithSections/SidebarWithSections.tsx @@ -0,0 +1,316 @@ +"use client"; +/* + * Documentation: + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + * Logo — https://app.subframe.com/f023064a5ca0/library?component=Logo_609493cb-7054-4018-bcb4-26d43de9ef41 + * Sidebar with sections — https://app.subframe.com/f023064a5ca0/library?component=Sidebar+with+sections_f4047c8b-cfb4-4761-b9cf-fbcae8a9b9b5 + */ + +import React from "react"; +import { FeatherCircleDashed } from "@subframe/core"; +import { FeatherSidebarClose } from "@subframe/core"; +import { FeatherSidebarOpen } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { IconButton } from "../IconButton"; +import { Logo } from "../Logo"; + +export interface NavItemProps extends React.HTMLAttributes { + icon?: React.ReactNode; + children?: React.ReactNode; + selected?: boolean; + variant?: "icon-and-text" | "icon-only" | "subitem"; + className?: string; +} + +const NavItem = React.forwardRef(function NavItem( + { + icon = , + children, + selected = false, + variant = "icon-and-text", + className, + ...otherProps + }: NavItemProps, + ref +) { + return ( +
+
+
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+
+
+ ); +}); + +export interface NavSectionProps extends React.HTMLAttributes { + children?: React.ReactNode; + label?: React.ReactNode; + variant?: "default" | "subsection"; + showLabel?: boolean; + showDivider?: boolean; + className?: string; +} + +const NavSection = React.forwardRef( + function NavSection( + { + children, + label, + variant = "default", + showLabel = false, + showDivider = false, + className, + ...otherProps + }: NavSectionProps, + ref + ) { + return ( +
+
+ +
+
+ +
+ {children ? ( +
+ {children} +
+ ) : null} +
+ ); + } +); + +export interface NavLabelProps extends React.HTMLAttributes { + label?: React.ReactNode; + className?: string; +} + +const NavLabel = React.forwardRef( + function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) { + return ( +
+ {label ? ( + + {label} + + ) : null} +
+ ); + } +); + +export interface NavDividerProps extends React.HTMLAttributes { + className?: string; +} + +const NavDivider = React.forwardRef( + function NavDivider({ className, ...otherProps }: NavDividerProps, ref) { + return ( +
+
+
+ ); + } +); + +export interface SubcomponentProps + extends React.HTMLAttributes { + sidebarButton?: "close" | "open"; + className?: string; +} + +const Subcomponent = React.forwardRef( + function Subcomponent( + { sidebarButton = "close", className, ...otherProps }: SubcomponentProps, + ref + ) { + return ( +
+ + ) : ( + + ) + } + /> +
+ ); + } +); + +export interface SidebarWithSectionsRootProps + extends React.HTMLAttributes { + variant?: "open" | "closed"; + footer?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const SidebarWithSectionsRoot = React.forwardRef< + HTMLElement, + SidebarWithSectionsRootProps +>(function SidebarWithSectionsRoot( + { + variant = "open", + footer, + children, + className, + ...otherProps + }: SidebarWithSectionsRootProps, + ref +) { + return ( + + ); +}); + +export const SidebarWithSections = Object.assign(SidebarWithSectionsRoot, { + NavItem, + NavSection, + NavLabel, + NavDivider, + Subcomponent, +}); diff --git a/ui/components/SidebarWithSections/index.tsx b/ui/components/SidebarWithSections/index.tsx new file mode 100644 index 0000000..baaa8c8 --- /dev/null +++ b/ui/components/SidebarWithSections/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SidebarWithSections as SidebarWithSectionsComponent } from "./SidebarWithSections"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SidebarWithSections = SidebarWithSectionsComponent; diff --git a/ui/components/SkeletonCircle/SkeletonCircle.tsx b/ui/components/SkeletonCircle/SkeletonCircle.tsx new file mode 100644 index 0000000..9270945 --- /dev/null +++ b/ui/components/SkeletonCircle/SkeletonCircle.tsx @@ -0,0 +1,37 @@ +"use client"; +/* + * Documentation: + * Skeleton Circle — https://app.subframe.com/f023064a5ca0/library?component=Skeleton+Circle_8b6e7a84-358f-4dc5-9de8-792a18fa9955 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface SkeletonCircleRootProps + extends React.ComponentProps { + size?: "default" | "small" | "x-small"; + className?: string; +} + +const SkeletonCircleRoot = React.forwardRef< + React.ElementRef, + SkeletonCircleRootProps +>(function SkeletonCircleRoot( + { size = "default", className, ...otherProps }: SkeletonCircleRootProps, + ref +) { + return ( + + ); +}); + +export const SkeletonCircle = SkeletonCircleRoot; diff --git a/ui/components/SkeletonCircle/index.tsx b/ui/components/SkeletonCircle/index.tsx new file mode 100644 index 0000000..d458757 --- /dev/null +++ b/ui/components/SkeletonCircle/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SkeletonCircle as SkeletonCircleComponent } from "./SkeletonCircle"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SkeletonCircle = SkeletonCircleComponent; diff --git a/ui/components/SkeletonText/SkeletonText.tsx b/ui/components/SkeletonText/SkeletonText.tsx new file mode 100644 index 0000000..5c93b12 --- /dev/null +++ b/ui/components/SkeletonText/SkeletonText.tsx @@ -0,0 +1,42 @@ +"use client"; +/* + * Documentation: + * Skeleton Text — https://app.subframe.com/f023064a5ca0/library?component=Skeleton+Text_a9aae3f0-955e-4607-a272-374f1dc18f4b + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface SkeletonTextRootProps + extends React.ComponentProps { + size?: "default" | "label" | "subheader" | "section-header" | "header"; + className?: string; +} + +const SkeletonTextRoot = React.forwardRef< + React.ElementRef, + SkeletonTextRootProps +>(function SkeletonTextRoot( + { size = "default", className, ...otherProps }: SkeletonTextRootProps, + ref +) { + return ( + + ); +}); + +export const SkeletonText = SkeletonTextRoot; diff --git a/ui/components/SkeletonText/index.tsx b/ui/components/SkeletonText/index.tsx new file mode 100644 index 0000000..16a858c --- /dev/null +++ b/ui/components/SkeletonText/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { SkeletonText as SkeletonTextComponent } from "./SkeletonText"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const SkeletonText = SkeletonTextComponent; diff --git a/ui/components/Slider/Slider.tsx b/ui/components/Slider/Slider.tsx new file mode 100644 index 0000000..e744be5 --- /dev/null +++ b/ui/components/Slider/Slider.tsx @@ -0,0 +1,185 @@ +"use client"; +/* + * Documentation: + * Slider — https://app.subframe.com/f023064a5ca0/library?component=Slider_f4092874-0320-449e-a0c5-b435987c4cfb + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface RangeProps + extends React.ComponentProps { + variant?: "default" | "disabled"; + className?: string; +} + +const Range = React.forwardRef(function Range( + { variant = "default", className, ...otherProps }: RangeProps, + ref +) { + return ( + +
+ + ); +}); + +export interface ThumbProps + extends React.ComponentProps { + variant?: "default" | "disabled"; + className?: string; +} + +const Thumb = React.forwardRef(function Thumb( + { variant = "default", className, ...otherProps }: ThumbProps, + ref +) { + return ( + +
+ + ); +}); + +export interface TrackProps + extends React.ComponentProps { + variant?: "default" | "disabled"; + className?: string; +} + +const Track = React.forwardRef(function Track( + { variant = "default", className, ...otherProps }: TrackProps, + ref +) { + return ( + +
+ +
+
+ ); +}); + +export interface SliderRootProps + extends React.ComponentProps { + disabled?: boolean; + range?: boolean; + label?: React.ReactNode; + valueText?: React.ReactNode; + description?: React.ReactNode; + value?: number[]; + onValueChange?: (value: number[]) => void; + onValueCommit?: (value: number[]) => void; + className?: string; +} + +const SliderRoot = React.forwardRef( + function SliderRoot( + { + disabled = false, + range = false, + label, + valueText, + description, + className, + ...otherProps + }: SliderRootProps, + ref + ) { + return ( + +
+
+ {label ? ( + + {label} + + ) : null} + {valueText ? ( + + {valueText} + + ) : null} +
+
+ + + +
+ {description ? ( + + {description} + + ) : null} +
+
+ ); + } +); + +export const Slider = Object.assign(SliderRoot, { + Range, + Thumb, + Track, +}); diff --git a/ui/components/Slider/index.tsx b/ui/components/Slider/index.tsx new file mode 100644 index 0000000..b6e598f --- /dev/null +++ b/ui/components/Slider/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Slider as SliderComponent } from "./Slider"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Slider = SliderComponent; diff --git a/ui/components/StatsCard/StatsCard.tsx b/ui/components/StatsCard/StatsCard.tsx new file mode 100644 index 0000000..567fd7e --- /dev/null +++ b/ui/components/StatsCard/StatsCard.tsx @@ -0,0 +1,113 @@ +"use client"; +/* + * Documentation: + * Stats Card — https://app.subframe.com/f023064a5ca0/library?component=Stats+Card_63f809c7-22ef-47f8-9fb9-0f33b252be3f + */ + +import React from "react"; +import { FeatherArrowRight } from "@subframe/core"; +import { FeatherDollarSign } from "@subframe/core"; +import { FeatherTrendingDown } from "@subframe/core"; +import { FeatherTrendingUp } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface StatsCardRootProps + extends React.HTMLAttributes { + trend?: "positive" | "negative" | "neutral"; + label?: React.ReactNode; + value?: React.ReactNode; + percentage?: React.ReactNode; + comparisonText?: React.ReactNode; + icon?: React.ReactNode; + className?: string; +} + +const StatsCardRoot = React.forwardRef( + function StatsCardRoot( + { + trend = "positive", + label, + value, + percentage, + comparisonText, + icon = , + className, + ...otherProps + }: StatsCardRootProps, + ref + ) { + return ( +
+
+
+ {label ? ( + + {label} + + ) : null} + {icon ? ( + + {icon} + + ) : null} +
+ {value ? ( + + {value} + + ) : null} +
+
+ + + + {percentage ? ( + + {percentage} + + ) : null} +
+ {comparisonText ? ( + + {comparisonText} + + ) : null} +
+
+
+ ); + } +); + +export const StatsCard = StatsCardRoot; diff --git a/ui/components/StatsCard/index.tsx b/ui/components/StatsCard/index.tsx new file mode 100644 index 0000000..c616133 --- /dev/null +++ b/ui/components/StatsCard/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { StatsCard as StatsCardComponent } from "./StatsCard"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const StatsCard = StatsCardComponent; diff --git a/ui/components/StepperHorizontal/StepperHorizontal.tsx b/ui/components/StepperHorizontal/StepperHorizontal.tsx new file mode 100644 index 0000000..fbde6c4 --- /dev/null +++ b/ui/components/StepperHorizontal/StepperHorizontal.tsx @@ -0,0 +1,119 @@ +"use client"; +/* + * Documentation: + * Stepper Horizontal — https://app.subframe.com/f023064a5ca0/library?component=Stepper+Horizontal_3c5d47dc-1b1a-45d9-b244-18422d7bfb56 + */ + +import React from "react"; +import * as SubframeUtils from "../../utils"; + +export interface StepProps extends React.HTMLAttributes { + variant?: "default" | "completed" | "active"; + firstStep?: boolean; + lastStep?: boolean; + stepNumber?: React.ReactNode; + label?: React.ReactNode; + className?: string; +} + +const Step = React.forwardRef(function Step( + { + variant = "default", + firstStep = false, + lastStep = false, + stepNumber, + label, + className, + ...otherProps + }: StepProps, + ref +) { + return ( +
+
+
+
+ {stepNumber ? ( + + {stepNumber} + + ) : null} +
+
+
+ {label ? ( + + {label} + + ) : null} +
+ ); +}); + +export interface StepperHorizontalRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const StepperHorizontalRoot = React.forwardRef< + HTMLDivElement, + StepperHorizontalRootProps +>(function StepperHorizontalRoot( + { children, className, ...otherProps }: StepperHorizontalRootProps, + ref +) { + return children ? ( +
+ {children} +
+ ) : null; +}); + +export const StepperHorizontal = Object.assign(StepperHorizontalRoot, { + Step, +}); diff --git a/ui/components/StepperHorizontal/index.tsx b/ui/components/StepperHorizontal/index.tsx new file mode 100644 index 0000000..609abd3 --- /dev/null +++ b/ui/components/StepperHorizontal/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { StepperHorizontal as StepperHorizontalComponent } from "./StepperHorizontal"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const StepperHorizontal = StepperHorizontalComponent; diff --git a/ui/components/StepperVertical/StepperVertical.tsx b/ui/components/StepperVertical/StepperVertical.tsx new file mode 100644 index 0000000..7988d4b --- /dev/null +++ b/ui/components/StepperVertical/StepperVertical.tsx @@ -0,0 +1,147 @@ +"use client"; +/* + * Documentation: + * Stepper Vertical — https://app.subframe.com/f023064a5ca0/library?component=Stepper+Vertical_bdc0291d-b5be-40c5-ae2f-527a868488b2 + */ + +import React from "react"; +import { FeatherCheck } from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface StepProps extends React.HTMLAttributes { + variant?: "default" | "completed" | "active"; + stepNumber?: React.ReactNode; + label?: React.ReactNode; + firstStep?: boolean; + lastStep?: boolean; + children?: React.ReactNode; + className?: string; +} + +const Step = React.forwardRef(function Step( + { + variant = "default", + stepNumber, + label, + firstStep = false, + lastStep = false, + children, + className, + ...otherProps + }: StepProps, + ref +) { + return ( +
+
+
+
+ {stepNumber ? ( + + {stepNumber} + + ) : null} + +
+
+
+
+ {label ? ( + + {label} + + ) : null} + {children ? ( +
+ {children} +
+ ) : null} +
+
+ ); +}); + +export interface StepperVerticalRootProps + extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const StepperVerticalRoot = React.forwardRef< + HTMLDivElement, + StepperVerticalRootProps +>(function StepperVerticalRoot( + { children, className, ...otherProps }: StepperVerticalRootProps, + ref +) { + return children ? ( +
+ {children} +
+ ) : null; +}); + +export const StepperVertical = Object.assign(StepperVerticalRoot, { + Step, +}); diff --git a/ui/components/StepperVertical/index.tsx b/ui/components/StepperVertical/index.tsx new file mode 100644 index 0000000..6f68b0e --- /dev/null +++ b/ui/components/StepperVertical/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { StepperVertical as StepperVerticalComponent } from "./StepperVertical"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const StepperVertical = StepperVerticalComponent; diff --git a/ui/components/StickyNote/StickyNote.tsx b/ui/components/StickyNote/StickyNote.tsx new file mode 100644 index 0000000..26fd945 --- /dev/null +++ b/ui/components/StickyNote/StickyNote.tsx @@ -0,0 +1,134 @@ +"use client"; +/* + * Documentation: + * Dropdown Menu select — https://app.subframe.com/f023064a5ca0/library?component=Dropdown+Menu+select_99951515-459b-4286-919e-a89e7549b43b + * Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6 + * Sticky Note — https://app.subframe.com/f023064a5ca0/library?component=Sticky+Note_bf633727-22a3-4c3a-9db4-a53efd508fc5 + */ + +import React from "react"; +import { FeatherCopy } from "@subframe/core"; +import { FeatherEdit2 } from "@subframe/core"; +import { FeatherMoreVertical } from "@subframe/core"; +import { FeatherTrash2 } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; +import { DropdownMenuSelect } from "../DropdownMenuSelect"; +import { IconButton } from "../IconButton"; + +export interface StickyNoteRootProps + extends Omit, "color"> { + text?: React.ReactNode; + color?: "yellow" | "pink" | "blue" | "green" | "purple" | "orange"; + className?: string; +} + +const StickyNoteRoot = React.forwardRef( + function StickyNoteRoot( + { text, color = "yellow", className, ...otherProps }: StickyNoteRootProps, + ref + ) { + return ( +
+
+
+ + + } + /> + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + Kopieren + +
+
+ + + Duplizieren + +
+
+ + + Text bearbeiten + +
+
+
+
+
+ + + Löschen + +
+ + + + +
+
+ {text ? ( + + {text} + + ) : null} +
+
+ ); + } +); + +export const StickyNote = StickyNoteRoot; diff --git a/ui/components/StickyNote/index.tsx b/ui/components/StickyNote/index.tsx new file mode 100644 index 0000000..e8d8e7d --- /dev/null +++ b/ui/components/StickyNote/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { StickyNote as StickyNoteComponent } from "./StickyNote"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const StickyNote = StickyNoteComponent; diff --git a/ui/components/Switch/Switch.tsx b/ui/components/Switch/Switch.tsx new file mode 100644 index 0000000..18c4248 --- /dev/null +++ b/ui/components/Switch/Switch.tsx @@ -0,0 +1,84 @@ +"use client"; +/* + * Documentation: + * Switch — https://app.subframe.com/f023064a5ca0/library?component=Switch_7a464794-9ea9-4040-b1de-5bfb2ce599d9 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ThumbProps + extends React.ComponentProps { + variant?: "off" | "on" | "disabled"; + className?: string; +} + +const Thumb = React.forwardRef(function Thumb( + { variant = "off", className, ...otherProps }: ThumbProps, + ref +) { + return ( + +
+ + ); +}); + +export interface SwitchRootProps + extends React.ComponentProps { + disabled?: boolean; + checked?: boolean; + onCheckedChange?: (checked: boolean) => void; + className?: string; +} + +const SwitchRoot = React.forwardRef( + function SwitchRoot( + { + disabled = false, + checked = false, + className, + ...otherProps + }: SwitchRootProps, + ref + ) { + return ( + +
+ +
+
+ ); + } +); + +export const Switch = Object.assign(SwitchRoot, { + Thumb, +}); diff --git a/ui/components/Switch/index.tsx b/ui/components/Switch/index.tsx new file mode 100644 index 0000000..addbb39 --- /dev/null +++ b/ui/components/Switch/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Switch as SwitchComponent } from "./Switch"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Switch = SwitchComponent; diff --git a/ui/components/Table/Table.tsx b/ui/components/Table/Table.tsx new file mode 100644 index 0000000..1d487ed --- /dev/null +++ b/ui/components/Table/Table.tsx @@ -0,0 +1,169 @@ +"use client"; +/* + * Documentation: + * Table — https://app.subframe.com/f023064a5ca0/library?component=Table_142dfde7-d0cc-48a1-a04c-a08ab2252633 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface CellProps + extends React.TdHTMLAttributes { + children?: React.ReactNode; + variant?: "align-left" | "align-center" | "align-right"; + className?: string; +} + +const Cell = React.forwardRef( + function Cell( + { children, variant = "align-left", className, ...otherProps }: CellProps, + ref + ) { + return ( + +
+ {children} +
+ + ); + } +); + +export interface HeaderCellProps + extends React.ThHTMLAttributes { + children?: React.ReactNode; + icon?: React.ReactNode; + className?: string; +} + +const HeaderCell = React.forwardRef< + HTMLTableHeaderCellElement, + HeaderCellProps +>(function HeaderCell( + { children, icon = null, className, ...otherProps }: HeaderCellProps, + ref +) { + return ( + +
+ {children ? ( + + {children} + + ) : null} + {icon ? ( + + {icon} + + ) : null} +
+ + ); +}); + +export interface HeaderRowProps + extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const HeaderRow = React.forwardRef( + function HeaderRow( + { children, className, ...otherProps }: HeaderRowProps, + ref + ) { + return ( + + {children} + + ); + } +); + +export interface RowProps extends React.HTMLAttributes { + children?: React.ReactNode; + clickable?: boolean; + className?: string; +} + +const Row = React.forwardRef(function Row( + { children, clickable = false, className, ...otherProps }: RowProps, + ref +) { + return ( + + {children} + + ); +}); + +export interface TableRootProps + extends React.TableHTMLAttributes { + header?: React.ReactNode; + children?: React.ReactNode; + emptyText?: React.ReactNode; + variant?: "default" | "disabled"; + className?: string; +} + +const TableRoot = React.forwardRef( + function TableRoot( + { + header, + children, + emptyText, + variant = "default", + className, + ...otherProps + }: TableRootProps, + ref + ) { + return ( + + {header} + + {children} + +
+ ); + } +); + +export const Table = Object.assign(TableRoot, { + Cell, + HeaderCell, + HeaderRow, + Row, +}); diff --git a/ui/components/Table/index.tsx b/ui/components/Table/index.tsx new file mode 100644 index 0000000..a593563 --- /dev/null +++ b/ui/components/Table/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Table as TableComponent } from "./Table"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Table = TableComponent; diff --git a/ui/components/Tabs/Tabs.tsx b/ui/components/Tabs/Tabs.tsx new file mode 100644 index 0000000..27e28c5 --- /dev/null +++ b/ui/components/Tabs/Tabs.tsx @@ -0,0 +1,179 @@ +"use client"; +/* + * Documentation: + * Tabs — https://app.subframe.com/f023064a5ca0/library?component=Tabs_e1ad5091-8ad8-4319-b1f7-3e47f0256c20 + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface TabBottomProps extends React.HTMLAttributes { + disabled?: boolean; + active?: boolean; + icon?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const TabBottom = React.forwardRef( + function TabBottom( + { + disabled = false, + active = false, + icon = null, + children, + className, + ...otherProps + }: TabBottomProps, + ref + ) { + return ( +
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+ ); + } +); + +export interface TabTopProps extends React.HTMLAttributes { + disabled?: boolean; + active?: boolean; + icon?: React.ReactNode; + children?: React.ReactNode; + className?: string; +} + +const TabTop = React.forwardRef(function TabTop( + { + disabled = false, + active = false, + icon = null, + children, + className, + ...otherProps + }: TabTopProps, + ref +) { + return ( +
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+ ); +}); + +export interface TabsRootProps extends React.HTMLAttributes { + children?: React.ReactNode; + className?: string; +} + +const TabsRoot = React.forwardRef( + function TabsRoot( + { children, className, ...otherProps }: TabsRootProps, + ref + ) { + return ( +
+
+ {children ? ( +
+ {children} +
+ ) : null} +
+
+ ); + } +); + +export const Tabs = Object.assign(TabsRoot, { + TabBottom, + TabTop, +}); diff --git a/ui/components/Tabs/index.tsx b/ui/components/Tabs/index.tsx new file mode 100644 index 0000000..a6d02d4 --- /dev/null +++ b/ui/components/Tabs/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Tabs as TabsComponent } from "./Tabs"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Tabs = TabsComponent; diff --git a/ui/components/Tag_Label/Tag_Label.tsx b/ui/components/Tag_Label/Tag_Label.tsx new file mode 100644 index 0000000..08507f2 --- /dev/null +++ b/ui/components/Tag_Label/Tag_Label.tsx @@ -0,0 +1,436 @@ +"use client"; +/* + * Documentation: + * Tag _ Label — https://app.subframe.com/f023064a5ca0/library?component=Tag+_+Label_bb114597-3b27-4c24-a34b-737c67238809 + */ + +import React from "react"; +import { FeatherCircle } from "@subframe/core"; +import { FeatherX } from "@subframe/core"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface ActionProps extends React.HTMLAttributes { + variant?: + | "on-light" + | "on-solid" + | "red" + | "orange" + | "yellow" + | "pear" + | "lime" + | "green" + | "teal" + | "ocean" + | "sky" + | "blue" + | "indigo" + | "violet" + | "fuchsia" + | "pink" + | "sand" + | "brown"; + icon?: React.ReactNode; + className?: string; +} + +const Action = React.forwardRef(function Action( + { + variant = "on-light", + icon = , + className, + ...otherProps + }: ActionProps, + ref +) { + return ( +
+
+
+ {icon ? ( + + {icon} + + ) : null} +
+
+ ); +}); + +export interface Tag_LabelRootProps + extends React.HTMLAttributes { + tone?: + | "red-subtle" + | "orange-subtle" + | "yellow-subtle" + | "pear-subtle" + | "lime-subtle" + | "green-subtle" + | "teal-subtle" + | "ocean-subtle" + | "sky-subtle" + | "blue-subtle" + | "indigo-subtle" + | "violet-subtle" + | "fuchsia-subtle" + | "pink-subtle" + | "sand-subtle" + | "brown-subtle" + | "red-solid" + | "orange-solid" + | "yellow-solid" + | "pear-solid" + | "lime-solid" + | "green-solid" + | "teal-solid" + | "ocean-solid" + | "sky-solid" + | "blue-solid" + | "indigo-solid" + | "violet-solid" + | "fuchsia-solid" + | "pink-solid" + | "sand-solid" + | "brown-solid" + | "red-outline" + | "orange-outline" + | "yellow-outline" + | "pear-outline" + | "lime-outline" + | "green-outline" + | "teal-outline" + | "ocean-outline" + | "sky-outline" + | "blue-outline" + | "indigo-outline" + | "violet-outline" + | "fuchsia-outline" + | "pink-outline" + | "sand-outline" + | "brown-outline" + | "brand-primary-solid" + | "brand-secondary-solid" + | "neutral-solid" + | "brand-primary-subtle" + | "brand-secondary-subtle" + | "neutral-subtle" + | "white"; + icon?: React.ReactNode; + children?: React.ReactNode; + actionChildren?: React.ReactNode; + noAction?: boolean; + size?: "small" | "medium" | "large"; + regularWeight?: boolean; + showIcon?: boolean; + className?: string; +} + +const Tag_LabelRoot = React.forwardRef( + function Tag_LabelRoot( + { + tone = "neutral-subtle", + icon = , + children, + actionChildren, + noAction = false, + size = "medium", + regularWeight = false, + showIcon = false, + className, + ...otherProps + }: Tag_LabelRootProps, + ref + ) { + return ( +
+
+ {icon ? ( + + {icon} + + ) : null} + {children ? ( + + {children} + + ) : null} +
+ {actionChildren ? ( +
+ {actionChildren} +
+ ) : null} +
+ ); + } +); + +export const Tag_Label = Object.assign(Tag_LabelRoot, { + Action, +}); diff --git a/ui/components/Tag_Label/index.tsx b/ui/components/Tag_Label/index.tsx new file mode 100644 index 0000000..36efdfa --- /dev/null +++ b/ui/components/Tag_Label/index.tsx @@ -0,0 +1,10 @@ +"use client"; +import { Tag_Label as Tag_LabelComponent } from "./Tag_Label"; + +/** + * Add wrapper components and business logic here. + * If you modify this file, disable Subframe sync for it to prevent overwrites. + * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components + */ + +export const Tag_Label = Tag_LabelComponent; diff --git a/ui/components/TextArea/TextArea.tsx b/ui/components/TextArea/TextArea.tsx new file mode 100644 index 0000000..e1b2b9a --- /dev/null +++ b/ui/components/TextArea/TextArea.tsx @@ -0,0 +1,154 @@ +"use client"; +/* + * Documentation: + * Text Area — https://app.subframe.com/f023064a5ca0/library?component=Text+Area_4ec05ee8-8f1c-46b2-b863-5419aa7f5cce + */ + +import React from "react"; +import * as SubframeCore from "@subframe/core"; +import * as SubframeUtils from "../../utils"; + +export interface InputProps + extends Omit< + React.TextareaHTMLAttributes, + "placeholder" | "value" + > { + placeholder?: React.ReactNode; + value?: React.ReactNode; + onChange?: (event: React.ChangeEvent) => void; + className?: string; +} + +const Input = React.forwardRef(function Input( + { placeholder, value, className, ...otherProps }: InputProps, + ref +) { + return ( +