@efa-one/sdk 1.10.2 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,43 +1,49 @@
1
1
  # @efa-one/sdk
2
2
 
3
- Offizielles SDK zum Bauen von **efa-one-Apps**. Es ist die verbindliche
4
- Integrationsschicht, die jede App einbettet, um ein First-Class-Bürger der
5
- Plattform zu sein: Auth gegen den Kernel, Clients zu den Plattform-Services und
6
- das Frontend-IPC-/i18n-Toolkit für die iframe-Einbettung.
3
+ Official SDK for building **efa-one apps**. It is the integration layer every app
4
+ embeds to become a first-class citizen of the platform: authentication against the
5
+ kernel, clients for the platform services, and the frontend IPC / i18n toolkit for
6
+ iframe embedding.
7
7
 
8
- ## Zwei Entry-Points
8
+ ## Two entry points
9
9
 
10
- Das SDK ist gemischt server-/browserseitig und daher in zwei Sub-Pfade getrennt,
11
- damit `express` nicht ins Frontend und `react` nicht ins Backend leakt:
10
+ The SDK is a mix of server- and browser-side code, so it is split into two
11
+ sub-paths — this keeps `express` out of the frontend and `react` out of the backend:
12
12
 
13
- | Import | Läuft in | Enthält |
13
+ | Import | Runs in | Contains |
14
14
  |---|---|---|
15
- | `@efa-one/sdk/backend` | Node/Express | Auth + Token-Exchange (`requireAuth`, `createExchangeRouter`, `requireInternalOrAuth` …), Health-Router, Service-Discovery + Gateway-Client (`serviceClient`, `resolveService`), Clients für Audit/Reporting/Mail/Notifications, Permission-Auflösung/-Registrierung (`getUserPermissions`, `registerPermissions`, `checkPermission`), Capability-Registry (`registerApiMetadata`), Backend-i18n |
16
- | `@efa-one/sdk/frontend` | Browser/React | postMessage-IPC (`sendDeclareAppInfo`, `sendAtStart`, `navigateToApp`, `notifyRouteChange` …), react-i18next-Factory (`initI18n`), `DevHeader` |
15
+ | `@efa-one/sdk/backend` | Node/Express | Auth + token exchange (`requireAuth`, `createExchangeRouter`, `requireInternalOrAuth` …), health router, service discovery + gateway client (`serviceClient`, `resolveService`), clients for audit/reporting/mail/notifications, permission resolution/registration (`getUserPermissions`, `registerPermissions`, `checkPermission`), capability registry (`registerApiMetadata`), backend i18n |
16
+ | `@efa-one/sdk/frontend` | Browser/React | postMessage IPC (`registerAppInfo`, `sendAtStart`, `navigateToApp`, `notifyRouteChange` …), react-i18next factory (`initI18n`), `DevHeader` |
17
+ | `@efa-one/sdk/frontend/ui` | Browser/React | The efa-one design-system kit: `Button`, `Input`, `Badge`, `Alert`, `Dialog`, `DropdownMenu`, `Tooltip`, `EmptyState`, `Skeleton`, `RecordDialog`, and the **`DataTable`** (sortable/filterable/groupable list with per-user persisted view). Ship its companion CSS once: `import '@efa-one/sdk/frontend/ui/styles.css'` |
18
+ | `@efa-one/sdk/frontend/viewPreferences` | Browser/React | Persistence seam for `DataTable` — `createViewPreferencesClient()` (standard `/api/view-preferences/:listId` adapter) + `useViewPreferences()` |
17
19
 
18
- Jedes Einzelmodul ist zusätzlich direkt erreichbar, z. B.
19
- `@efa-one/sdk/backend/auth` oder `@efa-one/sdk/frontend/ipc`.
20
+ Every individual module is also reachable directly, e.g.
21
+ `@efa-one/sdk/backend/auth` or `@efa-one/sdk/frontend/ipc`.
20
22
 
21
- > **Hinweis zu Legacy-Präfixen:** Einige plattform-interne Identifier (postMessage-
22
- > Nachrichtentypen, Env-Variablen-Namen, JWT-`iss`) tragen noch technische
23
- > Legacy-Präfixe aus einer früheren Namensgebung. Diese sind Teil des Draht-
24
- > Protokolls zwischen App und Kernel und werden gemeinsam mit dem Kernel in einem
25
- > koordinierten Schritt migriert — nicht einseitig im SDK.
23
+ The `frontend/ui` kit renders against the platform design tokens (`--color-*`,
24
+ `--border-radius-*`) and expects Tailwind in the consuming app; `lucide-react` and
25
+ the `@radix-ui/*` primitives it uses are optional peer dependencies.
26
+
27
+ > **Note on legacy prefixes:** Some platform-internal identifiers (postMessage
28
+ > message types, environment variable names, JWT `iss`) still carry technical
29
+ > legacy prefixes from an earlier naming. These are part of the wire protocol
30
+ > between app and kernel and are migrated together with the kernel in one
31
+ > coordinated step — not unilaterally in the SDK.
26
32
 
27
33
  ## Installation
28
34
 
29
35
  ```bash
30
- # im Backend-Projekt der App
36
+ # in the app's backend project
31
37
  npm install @efa-one/sdk express pg jsonwebtoken
32
38
 
33
- # im Frontend-Projekt der App
39
+ # in the app's frontend project
34
40
  npm install @efa-one/sdk react i18next i18next-http-backend react-i18next
35
41
  ```
36
42
 
37
- Die Runtime-Bibliotheken sind **optionale Peer-Dependencies** — installiere nur die
38
- zum genutzten Entry-Point passenden. Das SDK selbst bündelt sie nicht.
43
+ The runtime libraries are **optional peer dependencies** — install only the ones
44
+ matching the entry point you use. The SDK itself does not bundle them.
39
45
 
40
- ## Verwendung
46
+ ## Usage
41
47
 
42
48
  ```ts
43
49
  // Backend
@@ -49,71 +55,68 @@ app.get('/api/items', requireAuth, handler);
49
55
 
50
56
  ```tsx
51
57
  // Frontend
52
- import { initI18n, sendDeclareAppInfo, DevHeader } from '@efa-one/sdk/frontend';
58
+ import { initI18n, registerAppInfo, DevHeader } from '@efa-one/sdk/frontend';
53
59
 
54
- sendDeclareAppInfo({ appName: 'efa-chat', version: __APP_VERSION__ });
60
+ registerAppInfo({ appName: 'efa-chat', version: __APP_VERSION__ });
55
61
  ```
56
62
 
57
- ## Build
58
-
59
- Reines `tsc`, kein Bundler. Zwei Targets:
60
-
61
- ```bash
62
- npm install # devDeps + Peer-Libs zum Typecheck
63
- npm run build # backend/ (CJS) + frontend/ (ESM), je mit .d.ts
64
- npm run typecheck # nur Typprüfung, kein Emit
63
+ ```tsx
64
+ // Frontend — a fully-featured list in a few lines
65
+ import { DataTable, type ColumnDef } from '@efa-one/sdk/frontend/ui';
66
+ import { createViewPreferencesClient } from '@efa-one/sdk/frontend/viewPreferences';
67
+ import '@efa-one/sdk/frontend/ui/styles.css';
68
+
69
+ const viewPrefs = createViewPreferencesClient({ apiBase: getApiBase }); // create once
70
+ const columns: ColumnDef<Item>[] = [
71
+ { id: 'name', label: 'Name', accessor: (r) => r.name, filter: { type: 'text' } },
72
+ { id: 'status', label: 'Status', accessor: (r) => r.status },
73
+ ];
74
+
75
+ <DataTable listId="items.list" rows={items} columns={columns} rowKey={(r) => r.id} persistence={viewPrefs} />
65
76
  ```
66
77
 
67
- - `backend/` — CommonJS (`module: commonjs`)
68
- - `frontend/` — ESM (`module: esnext`, `moduleResolution: bundler`), via
69
- `frontend/package.json` als `{"type":"module"}` markiert; App-Bundler (Vite)
70
- konsumieren es direkt.
71
-
72
- Das flache Output-Layout (`backend/` + `frontend/` im Paket-Root) ist bewusst so
73
- gewählt, damit Consumer mit `moduleResolution: node` (node10) die Subpath-Importe
74
- physisch auflösen — ohne dass sie ihre tsconfig anpassen müssen.
75
-
76
- ## Publish
77
-
78
- Public auf npmjs unter dem Scope `@efa-one` (`publishConfig.access = "public"` ist
79
- gesetzt).
78
+ ## Build your first app
80
79
 
81
- **Automatisiert (Standardweg):** ein `v*`-Tag löst den Publish via GitHub Actions
82
- aus (`.github/workflows/publish.yml`) — analog zum GHCR-Flow der Apps. Der Workflow
83
- gleicht Tag ↔ `package.json`-Version ab, läuft Tests + Typecheck als Gate und
84
- publiziert dann.
80
+ This repo ships a ready-to-copy app scaffold under [`template/`](./template) — the fastest
81
+ path to a working efa-app. It already wires up auth, health, service discovery,
82
+ audit/reporting, IPC and i18n against this SDK, so you write business logic only. No extra
83
+ tooling (and no Claude Code) required — clone the repo, then:
85
84
 
86
85
  ```bash
87
- npm version patch # bumpt package.json + legt Commit + Tag an
88
- git push --follow-tags # Tag-Push triggert den Publish-Job
86
+ cp -R template efa-myapp
87
+ cd efa-myapp && rm -rf .git && git init
88
+ npm --prefix backend install && npm --prefix frontend install
89
+ docker compose up -d --build
89
90
  ```
90
91
 
91
- Einmalige Voraussetzung: Repo-Secret **`NPM_TOKEN`** (Automation-Token eines
92
- Accounts mit Publish-Recht auf `@efa-one`) unter *Settings → Secrets and variables →
93
- Actions*.
92
+ See [`template/README.md`](./template/README.md) for the full walkthrough — stack variants
93
+ (3-container/PostgreSQL vs. single-container/SQLite), the intake questions, permission
94
+ registration, and the README chapters every app must keep.
94
95
 
95
- **Manuell (Fallback):**
96
+ ## Build the SDK itself
97
+
98
+ Plain `tsc`, no bundler. Two targets:
96
99
 
97
100
  ```bash
98
- npm login # Account muss Publish-Recht auf @efa-one haben
99
- npm publish # baut via prepublishOnly automatisch neu
101
+ npm install # devDeps + peer libs for the typecheck
102
+ npm run build # backend/ (CJS) + frontend/ (ESM), each with .d.ts
103
+ npm run typecheck # type check only, no emit
100
104
  ```
101
105
 
102
- `npm install` durch Apps/CI/Docker braucht **keine** Credentials — nur der
103
- Publish-Schritt.
104
-
105
- ## Migration einer bestehenden App
106
+ - `backend/` — CommonJS (`module: commonjs`)
107
+ - `frontend/` — ESM (`module: esnext`, `moduleResolution: bundler`), marked via
108
+ `frontend/package.json` as `{"type":"module"}`; app bundlers (Vite) consume it
109
+ directly.
106
110
 
107
- Von kopiertem `template-core/` auf das Paket umstellen: siehe
108
- [MIGRATION.md](./MIGRATION.md) (Import-Mapping, tsconfig/vite-Entkopplung,
109
- Dockerfile-Bereinigung, Lockfile-Regen).
111
+ The flat output layout (`backend/` + `frontend/` at the package root) is a
112
+ deliberate choice so that consumers using `moduleResolution: node` (node10) can
113
+ resolve the subpath imports physically — without having to change their tsconfig.
110
114
 
111
- ## Versionierung
115
+ ## Versioning
112
116
 
113
- SemVer. Template-Migration = `npm version` + publish; App-Update =
114
- `npm update @efa-one/sdk`.
117
+ SemVer. Consuming apps pull new versions with `npm update @efa-one/sdk`.
115
118
 
116
- ## Lizenz
119
+ ## License
117
120
 
118
- [Apache-2.0](./LICENSE) — permissiv mit Patent-Grant, damit Kunden und Partner ohne
119
- Reibung efa-one-Apps bauen können.
121
+ [Apache-2.0](./LICENSE) — permissive with a patent grant, so customers and partners
122
+ can build efa-one apps without friction.
package/frontend/ipc.d.ts CHANGED
@@ -5,6 +5,21 @@ export declare function sendDeclareAppInfo(payload: {
5
5
  appName: string;
6
6
  version: string;
7
7
  }): void;
8
+ export declare function appInfoDeclareFromAuth(info: {
9
+ appName: string;
10
+ version: string;
11
+ }, data: unknown): {
12
+ type: 'CONVERGE_DECLARE_APP_INFO';
13
+ payload: {
14
+ appName: string;
15
+ version: string;
16
+ serviceKey: string;
17
+ };
18
+ } | null;
19
+ export declare function registerAppInfo(info: {
20
+ appName: string;
21
+ version: string;
22
+ }): () => void;
8
23
  export declare function navigateToApp(serviceKey: string, options?: {
9
24
  path?: string;
10
25
  }): void;
package/frontend/ipc.js CHANGED
@@ -27,6 +27,28 @@ export function sendDeclareAppInfo(payload) {
27
27
  return;
28
28
  window.parent.postMessage({ type: 'CONVERGE_DECLARE_APP_INFO', payload }, getParentOrigin());
29
29
  }
30
+ export function appInfoDeclareFromAuth(info, data) {
31
+ const d = data;
32
+ if (!d || d.type !== 'CONVERGE_AUTH' || typeof d.serviceKey !== 'string')
33
+ return null;
34
+ return {
35
+ type: 'CONVERGE_DECLARE_APP_INFO',
36
+ payload: { appName: info.appName, version: info.version, serviceKey: d.serviceKey },
37
+ };
38
+ }
39
+ export function registerAppInfo(info) {
40
+ if (!isEmbedded())
41
+ return () => { };
42
+ const handler = (event) => {
43
+ if (!isFromPlatformParent(event))
44
+ return;
45
+ const message = appInfoDeclareFromAuth(info, event.data);
46
+ if (message)
47
+ window.parent.postMessage(message, getParentOrigin());
48
+ };
49
+ window.addEventListener('message', handler);
50
+ return () => window.removeEventListener('message', handler);
51
+ }
30
52
  export function navigateToApp(serviceKey, options) {
31
53
  if (!isEmbedded())
32
54
  return;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export type AlertVariant = 'error' | 'success' | 'warning' | 'info';
3
+ interface AlertProps {
4
+ variant?: AlertVariant;
5
+ children: React.ReactNode;
6
+ onDismiss?: () => void;
7
+ className?: string;
8
+ }
9
+ export declare function Alert({ variant, children, onDismiss, className }: AlertProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { X } from 'lucide-react';
3
+ const VARIANT = {
4
+ error: 'bg-[color-mix(in_srgb,var(--color-danger)_10%,transparent)] border-[var(--color-danger)] text-[var(--color-danger)]',
5
+ success: 'bg-[color-mix(in_srgb,var(--color-success)_10%,transparent)] border-[var(--color-success)] text-[var(--color-success)]',
6
+ warning: 'bg-[color-mix(in_srgb,var(--color-warning)_10%,transparent)] border-[var(--color-warning)] text-[var(--color-warning)]',
7
+ info: 'bg-[color-mix(in_srgb,var(--color-primary)_10%,transparent)] border-[var(--color-primary)] text-[var(--color-primary)]',
8
+ };
9
+ export function Alert({ variant = 'error', children, onDismiss, className = '' }) {
10
+ return (_jsxs("div", { role: "alert", className: `shrink-0 flex items-start gap-2 border rounded-md px-4 py-3 text-sm font-medium ${VARIANT[variant]} ${className}`, children: [_jsx("span", { className: "flex-1", children: children }), onDismiss && (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Meldung schlie\u00DFen", className: "shrink-0 -mr-1 p-0.5 rounded hover:bg-current/10 transition-colors", children: _jsx(X, { className: "w-4 h-4" }) }))] }));
11
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ type BadgeVariant = 'success' | 'warning' | 'danger' | 'neutral';
3
+ interface BadgeProps {
4
+ variant?: BadgeVariant;
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+ export declare function Badge({ variant, children, className }: BadgeProps): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Badge({ variant = 'neutral', children, className = '' }) {
3
+ return (_jsx("span", { className: `badge badge-${variant} ${className}`, children: children }));
4
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'ghost';
3
+ type ButtonSize = 'sm' | 'md';
4
+ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5
+ variant?: ButtonVariant;
6
+ size?: ButtonSize;
7
+ loading?: boolean;
8
+ }
9
+ export declare function Button({ variant, size, loading, disabled, children, className, ...props }: ButtonProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Loader2 } from 'lucide-react';
3
+ const variantClasses = {
4
+ primary: 'bg-[var(--color-primary)] text-white border-transparent hover:bg-[var(--color-primary-hover)]',
5
+ secondary: 'bg-[var(--color-surface)] text-[var(--color-text-primary)] border-[var(--color-border)] hover:bg-[var(--color-surface-raised)]',
6
+ danger: 'bg-[var(--color-danger)] text-white border-transparent hover:opacity-90',
7
+ ghost: 'bg-transparent text-[var(--color-text-secondary)] border-transparent hover:bg-[var(--color-surface-raised)]',
8
+ };
9
+ const sizeClasses = {
10
+ sm: 'text-xs px-3 py-1.5 gap-1.5',
11
+ md: 'text-sm px-4 py-2 gap-2',
12
+ };
13
+ export function Button({ variant = 'primary', size = 'md', loading = false, disabled, children, className = '', ...props }) {
14
+ return (_jsxs("button", { disabled: disabled || loading, className: `inline-flex items-center justify-center font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${variantClasses[variant]} ${sizeClasses[size]} ${className}`, ...props, children: [loading && _jsx(Loader2, { className: "w-4 h-4 animate-spin" }), children] }));
15
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { type ViewPreferencesAdapter } from '../viewPreferences';
3
+ export type FilterDef = {
4
+ type: 'text';
5
+ } | {
6
+ type: 'multi-select';
7
+ options: Array<{
8
+ value: string;
9
+ label: string;
10
+ }>;
11
+ } | {
12
+ type: 'boolean';
13
+ labelTrue?: string;
14
+ labelFalse?: string;
15
+ };
16
+ export interface ColumnDef<T> {
17
+ id: string;
18
+ label: string;
19
+ accessor: (row: T) => unknown;
20
+ cell?: (row: T) => React.ReactNode;
21
+ filter?: FilterDef;
22
+ sortable?: boolean;
23
+ width?: string;
24
+ defaultVisible?: boolean;
25
+ }
26
+ interface FilterValue {
27
+ text?: string;
28
+ selected?: string[];
29
+ bool?: 'true' | 'false' | null;
30
+ }
31
+ export declare const DEFAULT_VIEW_VERSION = 2;
32
+ interface ViewPrefs {
33
+ columnVisibility: Record<string, boolean>;
34
+ columnOrder: string[];
35
+ sort: {
36
+ columnId: string;
37
+ direction: 'asc' | 'desc';
38
+ } | null;
39
+ filters: Record<string, FilterValue>;
40
+ groupBy: string[];
41
+ version?: number;
42
+ }
43
+ export interface DataTableSelectionProps<K> {
44
+ selected: Set<K>;
45
+ onChange: (next: Set<K>) => void;
46
+ bulkActions?: React.ReactNode;
47
+ }
48
+ interface DataTableProps<T, K extends string | number> {
49
+ listId: string;
50
+ rows: T[];
51
+ columns: ColumnDef<T>[];
52
+ rowKey: (row: T) => K;
53
+ onRowClick?: (row: T) => void;
54
+ selection?: DataTableSelectionProps<K>;
55
+ initialPrefs?: Partial<ViewPrefs>;
56
+ footer?: React.ReactNode;
57
+ persistence?: ViewPreferencesAdapter;
58
+ }
59
+ export declare function defaultPrefsFor<T>(columns: ColumnDef<T>[], overrides?: Partial<ViewPrefs>): ViewPrefs;
60
+ export declare function groupLabelOf(value: unknown): string;
61
+ interface RenderItem<T> {
62
+ kind: 'group' | 'row';
63
+ key?: string;
64
+ level?: number;
65
+ label?: string;
66
+ count?: number;
67
+ expanded?: boolean;
68
+ row?: T;
69
+ }
70
+ export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string>): RenderItem<T>[];
71
+ export declare function DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, persistence, }: DataTableProps<T, K>): React.ReactElement;
72
+ export {};
@@ -0,0 +1,321 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import * as DropdownMenu from './DropdownMenu';
4
+ import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X } from 'lucide-react';
5
+ import { Button } from './Button';
6
+ import { useViewPreferences } from '../viewPreferences';
7
+ const GROUP_KEY_SEP = '\x1f';
8
+ const EMPTY_GROUP_LABEL = '– ohne –';
9
+ export const DEFAULT_VIEW_VERSION = 2;
10
+ export function defaultPrefsFor(columns, overrides) {
11
+ const visibility = {};
12
+ columns.forEach((c) => { visibility[c.id] = c.defaultVisible !== false; });
13
+ return {
14
+ columnVisibility: visibility,
15
+ columnOrder: columns.map((c) => c.id),
16
+ sort: null,
17
+ filters: {},
18
+ groupBy: [],
19
+ version: DEFAULT_VIEW_VERSION,
20
+ ...(overrides ?? {}),
21
+ };
22
+ }
23
+ export function groupLabelOf(value) {
24
+ if (value == null)
25
+ return '';
26
+ if (Array.isArray(value))
27
+ return value.map(String).join(', ');
28
+ const s = String(value).trim();
29
+ return s;
30
+ }
31
+ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
32
+ if (groupBy.length === 0)
33
+ return rows.map((row) => ({ kind: 'row', row }));
34
+ const out = [];
35
+ const recurse = (subset, depth, parentKey) => {
36
+ if (depth >= groupBy.length) {
37
+ subset.forEach((row) => out.push({ kind: 'row', row }));
38
+ return;
39
+ }
40
+ const col = columnsById.get(groupBy[depth]);
41
+ if (!col) {
42
+ subset.forEach((row) => out.push({ kind: 'row', row }));
43
+ return;
44
+ }
45
+ const buckets = new Map();
46
+ for (const r of subset) {
47
+ const lbl = groupLabelOf(col.accessor(r));
48
+ const bucketLabel = lbl || EMPTY_GROUP_LABEL;
49
+ const arr = buckets.get(bucketLabel) ?? [];
50
+ arr.push(r);
51
+ buckets.set(bucketLabel, arr);
52
+ }
53
+ const labels = Array.from(buckets.keys()).sort((a, b) => {
54
+ if (a === EMPTY_GROUP_LABEL)
55
+ return 1;
56
+ if (b === EMPTY_GROUP_LABEL)
57
+ return -1;
58
+ return a.localeCompare(b, 'de');
59
+ });
60
+ for (const label of labels) {
61
+ const childRows = buckets.get(label);
62
+ const key = parentKey ? `${parentKey}${GROUP_KEY_SEP}${label}` : label;
63
+ const expanded = expandedGroups.has(key);
64
+ out.push({
65
+ kind: 'group',
66
+ key,
67
+ level: depth,
68
+ label,
69
+ count: childRows.length,
70
+ expanded,
71
+ });
72
+ if (expanded)
73
+ recurse(childRows, depth + 1, key);
74
+ }
75
+ };
76
+ recurse(rows, 0, '');
77
+ return out;
78
+ }
79
+ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, persistence, }) {
80
+ const defaultPrefs = useMemo(() => defaultPrefsFor(columns, initialPrefs), [columns, initialPrefs]);
81
+ const [prefs, setPrefs, reset] = useViewPreferences(listId, defaultPrefs, persistence);
82
+ const [expandedGroups, setExpandedGroups] = useState(new Set());
83
+ const toggleGroup = (key) => {
84
+ setExpandedGroups((prev) => {
85
+ const next = new Set(prev);
86
+ if (next.has(key))
87
+ next.delete(key);
88
+ else
89
+ next.add(key);
90
+ return next;
91
+ });
92
+ };
93
+ const columnsById = useMemo(() => new Map(columns.map((c) => [c.id, c])), [columns]);
94
+ const groupBy = prefs.groupBy ?? [];
95
+ const visibleColumns = useMemo(() => {
96
+ const byId = new Map(columns.map((c) => [c.id, c]));
97
+ return prefs.columnOrder
98
+ .map((id) => byId.get(id))
99
+ .filter((c) => Boolean(c) && prefs.columnVisibility[c.id] !== false);
100
+ }, [columns, prefs.columnOrder, prefs.columnVisibility]);
101
+ const sortedFilteredRows = useMemo(() => {
102
+ let out = rows;
103
+ for (const col of columns) {
104
+ const f = prefs.filters[col.id];
105
+ if (!f || !col.filter)
106
+ continue;
107
+ if (col.filter.type === 'text' && f.text) {
108
+ const q = f.text.toLowerCase();
109
+ out = out.filter((r) => {
110
+ const v = col.accessor(r);
111
+ return v != null && String(v).toLowerCase().includes(q);
112
+ });
113
+ }
114
+ else if (col.filter.type === 'multi-select' && f.selected && f.selected.length > 0) {
115
+ const set = new Set(f.selected);
116
+ out = out.filter((r) => {
117
+ const v = col.accessor(r);
118
+ if (Array.isArray(v))
119
+ return v.some((x) => set.has(String(x)));
120
+ return v != null && set.has(String(v));
121
+ });
122
+ }
123
+ else if (col.filter.type === 'boolean' && f.bool != null) {
124
+ out = out.filter((r) => String(Boolean(col.accessor(r))) === f.bool);
125
+ }
126
+ }
127
+ if (prefs.sort) {
128
+ const col = columns.find((c) => c.id === prefs.sort.columnId);
129
+ if (col) {
130
+ const dir = prefs.sort.direction === 'asc' ? 1 : -1;
131
+ out = [...out].sort((a, b) => {
132
+ const va = col.accessor(a);
133
+ const vb = col.accessor(b);
134
+ if (va == null && vb == null)
135
+ return 0;
136
+ if (va == null)
137
+ return 1 * dir;
138
+ if (vb == null)
139
+ return -1 * dir;
140
+ if (typeof va === 'number' && typeof vb === 'number')
141
+ return (va - vb) * dir;
142
+ return String(va).localeCompare(String(vb), 'de') * dir;
143
+ });
144
+ }
145
+ }
146
+ return out;
147
+ }, [rows, columns, prefs.filters, prefs.sort]);
148
+ const allSelected = !!selection && sortedFilteredRows.length > 0
149
+ && sortedFilteredRows.every((r) => selection.selected.has(rowKey(r)));
150
+ const toggleAll = () => {
151
+ if (!selection)
152
+ return;
153
+ const next = new Set(selection.selected);
154
+ if (allSelected)
155
+ sortedFilteredRows.forEach((r) => next.delete(rowKey(r)));
156
+ else
157
+ sortedFilteredRows.forEach((r) => next.add(rowKey(r)));
158
+ selection.onChange(next);
159
+ };
160
+ const toggleOne = (key) => {
161
+ if (!selection)
162
+ return;
163
+ const next = new Set(selection.selected);
164
+ if (next.has(key))
165
+ next.delete(key);
166
+ else
167
+ next.add(key);
168
+ selection.onChange(next);
169
+ };
170
+ const gridTemplate = useMemo(() => {
171
+ const cols = [];
172
+ if (selection)
173
+ cols.push('36px');
174
+ visibleColumns.forEach((c) => cols.push(c.width ?? '1fr'));
175
+ cols.push('40px');
176
+ return cols.join(' ');
177
+ }, [selection, visibleColumns]);
178
+ const setSort = (columnId, direction) => {
179
+ setPrefs({ ...prefs, sort: { columnId, direction } });
180
+ };
181
+ const clearSort = () => {
182
+ setPrefs({ ...prefs, sort: null });
183
+ };
184
+ const setColumnVisible = (columnId, visible) => {
185
+ setPrefs({
186
+ ...prefs,
187
+ columnVisibility: { ...prefs.columnVisibility, [columnId]: visible },
188
+ });
189
+ };
190
+ const moveColumn = (columnId, dir) => {
191
+ const order = [...prefs.columnOrder];
192
+ const idx = order.indexOf(columnId);
193
+ if (idx < 0)
194
+ return;
195
+ const newIdx = idx + dir;
196
+ if (newIdx < 0 || newIdx >= order.length)
197
+ return;
198
+ [order[idx], order[newIdx]] = [order[newIdx], order[idx]];
199
+ setPrefs({ ...prefs, columnOrder: order });
200
+ };
201
+ const addGroupBy = (columnId) => {
202
+ if (groupBy.includes(columnId))
203
+ return;
204
+ setPrefs({ ...prefs, groupBy: [...groupBy, columnId] });
205
+ setExpandedGroups(new Set());
206
+ };
207
+ const removeGroupBy = (columnId) => {
208
+ setPrefs({ ...prefs, groupBy: groupBy.filter((id) => id !== columnId) });
209
+ setExpandedGroups(new Set());
210
+ };
211
+ const moveGroupBy = (columnId, dir) => {
212
+ const order = [...groupBy];
213
+ const idx = order.indexOf(columnId);
214
+ if (idx < 0)
215
+ return;
216
+ const newIdx = idx + dir;
217
+ if (newIdx < 0 || newIdx >= order.length)
218
+ return;
219
+ [order[idx], order[newIdx]] = [order[newIdx], order[idx]];
220
+ setPrefs({ ...prefs, groupBy: order });
221
+ setExpandedGroups(new Set());
222
+ };
223
+ const clearGroupBy = () => {
224
+ setPrefs({ ...prefs, groupBy: [] });
225
+ setExpandedGroups(new Set());
226
+ };
227
+ const setFilter = (columnId, value) => {
228
+ const filters = { ...prefs.filters };
229
+ if (value == null || ((value.text == null || value.text === '') &&
230
+ (value.selected == null || value.selected.length === 0) &&
231
+ (value.bool == null))) {
232
+ delete filters[columnId];
233
+ }
234
+ else {
235
+ filters[columnId] = value;
236
+ }
237
+ setPrefs({ ...prefs, filters });
238
+ };
239
+ return (_jsxs("div", { className: "border border-[var(--color-border)]", children: [selection && selection.selected.size > 0 && (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-sm border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [_jsxs("span", { children: [selection.selected.size, " ausgew\u00E4hlt"] }), _jsx("button", { type: "button", className: "text-xs underline text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]", onClick: () => selection.onChange(new Set()), children: "Auswahl l\u00F6schen" }), _jsx("div", { className: "flex-1" }), selection.bulkActions] })), _jsxs("div", { className: "grid items-center px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { gridTemplateColumns: gridTemplate, background: 'var(--color-surface-raised)' }, children: [selection && (_jsx("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" })), visibleColumns.map((col) => {
240
+ const activeSort = prefs.sort?.columnId === col.id ? prefs.sort.direction : null;
241
+ const filterValue = prefs.filters[col.id];
242
+ const isFiltered = filterValue != null;
243
+ return (_jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex items-center gap-1 px-1 py-0.5 hover:bg-[var(--color-surface)] text-left", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("span", { children: col.label }), activeSort === 'asc' && _jsx(ChevronUp, { className: "w-3 h-3" }), activeSort === 'desc' && _jsx(ChevronDown, { className: "w-3 h-3" }), isFiltered && _jsx(Filter, { className: "w-3 h-3 text-[var(--color-primary)]" })] }) }), _jsxs(DropdownMenu.Content, { className: "min-w-[220px] p-1", children: [col.sortable !== false && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu.Item, { onSelect: () => setSort(col.id, 'asc'), children: [_jsx(ArrowUp, { className: "w-4 h-4" }), "Sortieren A\u2192Z"] }), _jsxs(DropdownMenu.Item, { onSelect: () => setSort(col.id, 'desc'), children: [_jsx(ArrowDown, { className: "w-4 h-4" }), "Sortieren Z\u2192A"] }), activeSort && (_jsx(DropdownMenu.Item, { onSelect: clearSort, children: "Sortierung zur\u00FCcksetzen" })), _jsx(DropdownMenu.Separator, {})] })), groupBy.includes(col.id) ? (_jsxs(DropdownMenu.Item, { onSelect: () => removeGroupBy(col.id), children: [_jsx(Layers, { className: "w-4 h-4" }), "Gruppierung aufheben"] })) : (_jsxs(DropdownMenu.Item, { onSelect: () => addGroupBy(col.id), children: [_jsx(Layers, { className: "w-4 h-4" }), "Nach dieser Spalte gruppieren"] })), _jsx(DropdownMenu.Separator, {}), col.filter && (_jsx(FilterEditor, { column: col, value: filterValue, onChange: (v) => setFilter(col.id, v) })), _jsxs(DropdownMenu.Item, { onSelect: () => setColumnVisible(col.id, false), variant: "danger", children: [_jsx(EyeOff, { className: "w-4 h-4" }), "Spalte ausblenden"] })] })] }, col.id));
244
+ }), _jsx(ColumnInventoryButton, { columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy })] }), sortedFilteredRows.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-sm text-[var(--color-text-muted)]", children: "Keine Eintr\u00E4ge" })) : (buildRenderItems(sortedFilteredRows, groupBy, columnsById, expandedGroups).map((item, idx) => {
245
+ if (item.kind === 'group') {
246
+ const level = item.level ?? 0;
247
+ const groupColLabel = columnsById.get(groupBy[level])?.label ?? '';
248
+ return (_jsx("div", { className: "px-3 py-1.5 text-xs border-b border-[var(--color-border)] cursor-pointer select-none hover:bg-[var(--color-surface-raised)]", style: {
249
+ background: 'var(--color-surface-raised)',
250
+ paddingLeft: 12 + level * 18,
251
+ }, onClick: () => toggleGroup(item.key), title: item.expanded ? 'Gruppe einklappen' : 'Gruppe ausklappen', children: _jsxs("div", { className: "flex items-center gap-2", children: [item.expanded
252
+ ? _jsx(ChevronDown, { className: "w-3.5 h-3.5 text-[var(--color-text-muted)]" })
253
+ : _jsx(ChevronRight, { className: "w-3.5 h-3.5 text-[var(--color-text-muted)]" }), _jsx("span", { className: "text-[var(--color-text-muted)] uppercase text-[10px]", children: groupColLabel }), _jsx("span", { className: "font-medium text-[var(--color-text-primary)]", children: item.label }), _jsxs("span", { className: "text-[var(--color-text-muted)]", children: ["(", item.count, ")"] })] }) }, `g:${item.key}`));
254
+ }
255
+ const row = item.row;
256
+ const key = rowKey(row);
257
+ const isChecked = selection?.selected.has(key) ?? false;
258
+ const rowIndent = groupBy.length > 0 ? 12 + groupBy.length * 18 : undefined;
259
+ return (_jsxs("div", { className: `grid items-center px-3 py-2 text-sm border-b border-[var(--color-border)] ${onRowClick ? 'cursor-pointer hover:bg-[var(--color-surface-raised)]' : ''}`, style: {
260
+ gridTemplateColumns: gridTemplate,
261
+ background: isChecked ? 'rgba(99,102,241,0.07)' : undefined,
262
+ paddingLeft: rowIndent,
263
+ }, onClick: () => onRowClick?.(row), children: [selection && (_jsx("input", { type: "checkbox", checked: isChecked, onChange: () => toggleOne(key), onClick: (e) => e.stopPropagation(), "aria-label": "Zeile ausw\u00E4hlen" })), visibleColumns.map((col) => (_jsx("div", { className: "min-w-0 truncate", children: col.cell ? col.cell(row) : String(col.accessor(row) ?? '') }, col.id))), _jsx("div", {})] }, `r:${String(key)}:${idx}`));
264
+ })), footer && (_jsx("div", { className: "px-3 py-2 text-xs text-[var(--color-text-muted)] border-t border-[var(--color-border)]", children: footer }))] }));
265
+ }
266
+ function FilterEditor({ column, value, onChange, }) {
267
+ if (!column.filter)
268
+ return null;
269
+ const f = column.filter;
270
+ if (f.type === 'text') {
271
+ return (_jsxs("div", { className: "px-2 py-1.5 space-y-1", children: [_jsx("label", { className: "block text-xs text-[var(--color-text-muted)]", children: "Enth\u00E4lt" }), _jsx("input", { type: "text", value: value?.text ?? '', onChange: (e) => onChange({ text: e.target.value }), onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), className: "w-full px-2 py-1 text-xs border border-[var(--color-border)] bg-[var(--color-surface)]", style: { borderRadius: 'var(--border-radius-md)' } }), value?.text && (_jsx("button", { type: "button", className: "text-xs underline text-[var(--color-text-muted)]", onClick: () => onChange(undefined), children: "Filter entfernen" }))] }));
272
+ }
273
+ if (f.type === 'multi-select') {
274
+ const selected = new Set(value?.selected ?? []);
275
+ return (_jsxs("div", { className: "px-2 py-1.5 space-y-1 max-h-[300px] overflow-auto", children: [_jsxs("div", { className: "flex items-center justify-between text-xs text-[var(--color-text-muted)]", children: [_jsx("button", { type: "button", onClick: (e) => { e.preventDefault(); onChange(undefined); }, className: "hover:text-[var(--color-text-primary)] underline", children: "Alle" }), _jsx("span", { children: selected.size > 0 ? `${selected.size} aktiv` : '' })] }), f.options.map((opt) => {
276
+ const checked = selected.has(opt.value);
277
+ return (_jsxs("button", { type: "button", onClick: (e) => {
278
+ e.preventDefault();
279
+ const next = new Set(selected);
280
+ if (checked)
281
+ next.delete(opt.value);
282
+ else
283
+ next.add(opt.value);
284
+ onChange(next.size === 0 ? undefined : { selected: Array.from(next) });
285
+ }, className: "flex items-center gap-2 w-full px-1 py-1 text-left text-xs hover:bg-[var(--color-surface-raised)]", children: [_jsx("span", { className: "inline-flex items-center justify-center w-4 h-4 border", style: {
286
+ borderColor: 'var(--color-border)',
287
+ background: checked ? 'var(--color-primary)' : 'var(--color-surface)',
288
+ color: 'white',
289
+ borderRadius: 'var(--border-radius-md)',
290
+ fontSize: 10,
291
+ }, children: checked ? '✓' : '' }), _jsx("span", { children: opt.label })] }, opt.value));
292
+ })] }));
293
+ }
294
+ if (f.type === 'boolean') {
295
+ return (_jsxs("div", { className: "px-2 py-1.5 space-y-1", children: [_jsx("label", { className: "block text-xs text-[var(--color-text-muted)]", children: "Filter" }), _jsxs("select", { value: value?.bool ?? '', onChange: (e) => {
296
+ const v = e.target.value;
297
+ onChange(v === '' ? undefined : { bool: v });
298
+ }, onClick: (e) => e.stopPropagation(), className: "w-full px-2 py-1 text-xs border border-[var(--color-border)] bg-[var(--color-surface)]", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("option", { value: "", children: "\u2013 alle \u2013" }), _jsx("option", { value: "true", children: f.labelTrue ?? 'Ja' }), _jsx("option", { value: "false", children: f.labelFalse ?? 'Nein' })] })] }));
299
+ }
300
+ return null;
301
+ }
302
+ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, reset, groupBy, addGroupBy, removeGroupBy, moveGroupBy, clearGroupBy, }) {
303
+ const groupableCandidates = columns.filter((c) => !groupBy.includes(c.id));
304
+ const [open, setOpen] = useState(false);
305
+ const orderedColumns = useMemo(() => {
306
+ const byId = new Map(columns.map((c) => [c.id, c]));
307
+ return prefs.columnOrder.map((id) => byId.get(id)).filter(Boolean);
308
+ }, [columns, prefs.columnOrder]);
309
+ return (_jsxs(DropdownMenu.Root, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx("button", { type: "button", className: "p-1 hover:bg-[var(--color-surface)] text-[var(--color-text-muted)] justify-self-end", title: "Spalten konfigurieren", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(Settings, { className: "w-4 h-4" }) }) }), _jsxs(DropdownMenu.Content, { className: "min-w-[260px] p-2", children: [_jsx("div", { className: "text-xs font-semibold mb-2 text-[var(--color-text-muted)] uppercase", children: "Gruppieren nach" }), groupBy.length === 0 ? (_jsx("div", { className: "text-xs text-[var(--color-text-muted)] italic px-1 mb-2", children: "Keine Gruppierung aktiv." })) : (_jsxs("div", { className: "mb-2 space-y-1", onClick: (e) => e.stopPropagation(), children: [groupBy.map((id, idx) => {
310
+ const col = columns.find((c) => c.id === id);
311
+ return (_jsxs("div", { className: "flex items-center gap-2 px-1 py-1 text-xs", children: [_jsxs("span", { className: "text-[var(--color-text-muted)] w-4", children: [idx + 1, "."] }), _jsx("span", { className: "flex-1 truncate", children: col?.label ?? id }), _jsx("button", { type: "button", disabled: idx === 0, onClick: () => moveGroupBy(id, -1), className: "p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Ebene hoch", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowUp, { className: "w-3 h-3" }) }), _jsx("button", { type: "button", disabled: idx === groupBy.length - 1, onClick: () => moveGroupBy(id, 1), className: "p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Ebene runter", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowDown, { className: "w-3 h-3" }) }), _jsx("button", { type: "button", onClick: () => removeGroupBy(id), className: "p-0.5 text-[var(--color-text-muted)] hover:text-[var(--color-danger)]", title: "Diese Ebene entfernen", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(X, { className: "w-3 h-3" }) })] }, id));
312
+ }), _jsx("button", { type: "button", onClick: clearGroupBy, className: "text-xs underline text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] px-1", children: "Alle aufheben" })] })), groupableCandidates.length > 0 && (_jsxs("div", { className: "mb-2 flex items-center gap-2 px-1", onClick: (e) => e.stopPropagation(), children: [_jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: "Ebene hinzuf\u00FCgen:" }), _jsxs("select", { defaultValue: "", onChange: (e) => {
313
+ const v = e.target.value;
314
+ if (v)
315
+ addGroupBy(v);
316
+ e.target.value = '';
317
+ }, className: "flex-1 px-1 py-0.5 text-xs border border-[var(--color-border)] bg-[var(--color-surface)]", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("option", { value: "", children: "\u2013 w\u00E4hlen \u2013" }), groupableCandidates.map((c) => (_jsx("option", { value: c.id, children: c.label }, c.id)))] })] })), _jsx("div", { className: "border-t border-[var(--color-border)] mb-2" }), _jsx("div", { className: "text-xs font-semibold mb-2 text-[var(--color-text-muted)] uppercase", children: "Spalten" }), orderedColumns.map((col, idx) => {
318
+ const visible = prefs.columnVisibility[col.id] !== false;
319
+ return (_jsxs("div", { className: "flex items-center gap-2 px-1 py-1 text-sm", onClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", checked: visible, onChange: () => setColumnVisible(col.id, !visible), "aria-label": `Spalte ${col.label} ${visible ? 'ausblenden' : 'einblenden'}` }), _jsx("span", { className: "flex-1 text-xs", children: col.label }), _jsx("button", { type: "button", disabled: idx === 0, onClick: () => moveColumn(col.id, -1), className: "p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach oben", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowUp, { className: "w-3 h-3" }) }), _jsx("button", { type: "button", disabled: idx === orderedColumns.length - 1, onClick: () => moveColumn(col.id, 1), className: "p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach unten", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowDown, { className: "w-3 h-3" }) })] }, col.id));
320
+ }), _jsx("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: _jsxs(Button, { variant: "ghost", onClick: () => { reset(); setOpen(false); }, children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Zur\u00FCcksetzen"] }) })] })] }));
321
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import * as RadixDialog from '@radix-ui/react-dialog';
3
+ interface DialogProps {
4
+ open?: boolean;
5
+ onOpenChange?: (open: boolean) => void;
6
+ trigger?: React.ReactNode;
7
+ title: string;
8
+ description?: string;
9
+ children: React.ReactNode;
10
+ footer?: React.ReactNode;
11
+ }
12
+ export declare function Dialog({ open, onOpenChange, trigger, title, description, children, footer }: DialogProps): React.JSX.Element;
13
+ export declare const DialogClose: React.ForwardRefExoticComponent<RadixDialog.DialogCloseProps & React.RefAttributes<HTMLButtonElement>>;
14
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as RadixDialog from '@radix-ui/react-dialog';
3
+ import { X } from 'lucide-react';
4
+ export function Dialog({ open, onOpenChange, trigger, title, description, children, footer }) {
5
+ return (_jsxs(RadixDialog.Root, { open: open, onOpenChange: onOpenChange, children: [trigger && _jsx(RadixDialog.Trigger, { asChild: true, children: trigger }), _jsxs(RadixDialog.Portal, { children: [_jsx(RadixDialog.Overlay, { className: "fixed inset-0 bg-black/40 z-40 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" }), _jsxs(RadixDialog.Content, { className: "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] shadow-lg focus:outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", children: [_jsxs("div", { className: "flex items-start justify-between p-5 border-b border-[var(--color-border)]", children: [_jsxs("div", { children: [_jsx(RadixDialog.Title, { className: "text-base font-semibold text-[var(--color-text-primary)]", children: title }), description && (_jsx(RadixDialog.Description, { className: "text-sm text-[var(--color-text-muted)] mt-0.5", children: description }))] }), _jsx(RadixDialog.Close, { className: "ml-4 p-1 rounded-md text-[var(--color-text-muted)] hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text-primary)] transition-colors", children: _jsx(X, { className: "w-4 h-4" }) })] }), _jsx("div", { className: "p-5", children: children }), footer && (_jsx("div", { className: "flex items-center justify-end gap-2 px-5 pb-5", children: footer }))] })] })] }));
6
+ }
7
+ export const DialogClose = RadixDialog.Close;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import * as Radix from '@radix-ui/react-dropdown-menu';
3
+ export declare const Root: React.FC<Radix.DropdownMenuProps>;
4
+ export declare const Trigger: React.ForwardRefExoticComponent<Radix.DropdownMenuTriggerProps & React.RefAttributes<HTMLButtonElement>>;
5
+ export declare const Portal: React.FC<Radix.DropdownMenuPortalProps>;
6
+ export declare const Content: React.ForwardRefExoticComponent<Omit<Radix.DropdownMenuContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
7
+ interface ItemProps extends React.ComponentPropsWithoutRef<typeof Radix.Item> {
8
+ variant?: 'default' | 'danger';
9
+ }
10
+ export declare const Item: React.ForwardRefExoticComponent<ItemProps & React.RefAttributes<HTMLDivElement>>;
11
+ export declare const Separator: React.ForwardRefExoticComponent<Omit<Radix.DropdownMenuSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
12
+ export declare const Label: React.ForwardRefExoticComponent<Omit<Radix.DropdownMenuLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
13
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import * as Radix from '@radix-ui/react-dropdown-menu';
4
+ export const Root = Radix.Root;
5
+ export const Trigger = Radix.Trigger;
6
+ export const Portal = Radix.Portal;
7
+ export const Content = React.forwardRef(({ className = '', sideOffset = 4, ...props }, ref) => (_jsx(Radix.Portal, { children: _jsx(Radix.Content, { ref: ref, sideOffset: sideOffset, className: `z-50 min-w-[160px] rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-1 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 ${className}`, ...props }) })));
8
+ Content.displayName = 'DropdownMenu.Content';
9
+ export const Item = React.forwardRef(({ className = '', variant = 'default', ...props }, ref) => (_jsx(Radix.Item, { ref: ref, className: `flex items-center gap-2 px-3 py-1.5 text-sm rounded-md cursor-pointer outline-none select-none transition-colors data-[disabled]:opacity-50 data-[disabled]:pointer-events-none ${variant === 'danger'
10
+ ? 'text-[var(--color-danger)] data-[highlighted]:bg-[var(--color-surface-raised)]'
11
+ : 'text-[var(--color-text-primary)] data-[highlighted]:bg-[var(--color-surface-raised)]'} ${className}`, ...props })));
12
+ Item.displayName = 'DropdownMenu.Item';
13
+ export const Separator = React.forwardRef(({ className = '', ...props }, ref) => (_jsx(Radix.Separator, { ref: ref, className: `my-1 h-px bg-[var(--color-border)] ${className}`, ...props })));
14
+ Separator.displayName = 'DropdownMenu.Separator';
15
+ export const Label = React.forwardRef(({ className = '', ...props }, ref) => (_jsx(Radix.Label, { ref: ref, className: `px-3 py-1 text-xs font-semibold text-[var(--color-text-muted)] uppercase tracking-wide ${className}`, ...props })));
16
+ Label.displayName = 'DropdownMenu.Label';
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ interface EmptyStateProps {
4
+ icon: LucideIcon;
5
+ title: string;
6
+ description?: string;
7
+ action?: React.ReactNode;
8
+ }
9
+ export declare function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps): React.JSX.Element;
10
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function EmptyState({ icon: Icon, title, description, action }) {
3
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center text-center py-12 px-6", children: [_jsx("div", { className: "text-[var(--color-text-muted)] mb-3", children: _jsx(Icon, { className: "w-10 h-10 mx-auto" }) }), _jsx("p", { className: "text-base font-semibold text-[var(--color-text-primary)]", children: title }), description && (_jsx("p", { className: "text-sm text-[var(--color-text-secondary)] mt-1 max-w-xs", children: description })), action && _jsx("div", { className: "mt-4", children: action })] }));
4
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
3
+ }
4
+ export declare function Input({ className, ...props }: InputProps): React.JSX.Element;
5
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Input({ className = '', ...props }) {
3
+ return (_jsx("input", { className: `w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:border-transparent bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-muted)] transition-colors ${className}`, ...props }));
4
+ }
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ export type RecordDialogMode = 'read' | 'edit' | 'create';
3
+ interface RecordDialogProps {
4
+ open: {
5
+ id: string;
6
+ item?: any;
7
+ } | null;
8
+ onOpenChange(open: {
9
+ id: string;
10
+ item?: any;
11
+ } | null): void;
12
+ mode?: RecordDialogMode;
13
+ onModeChange?(mode: RecordDialogMode): void;
14
+ title: string;
15
+ readContent: React.ReactNode;
16
+ editContent: React.ReactNode;
17
+ onSave(mode: 'edit' | 'create'): Promise<void> | void;
18
+ saving?: boolean;
19
+ saveDisabled?: boolean;
20
+ error?: string;
21
+ onErrorDismiss?: () => void;
22
+ labels?: {
23
+ close?: string;
24
+ cancel?: string;
25
+ save?: string;
26
+ create?: string;
27
+ edit?: string;
28
+ };
29
+ }
30
+ export declare function RecordDialog({ open, onOpenChange, mode: controlledMode, onModeChange, title, readContent, editContent, onSave, saving, saveDisabled, error, onErrorDismiss, labels, }: RecordDialogProps): React.JSX.Element;
31
+ export {};
@@ -0,0 +1,38 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Pencil } from 'lucide-react';
4
+ import { Dialog, DialogClose } from './Dialog';
5
+ import { Button } from './Button';
6
+ import { Alert } from './Alert';
7
+ export function RecordDialog({ open, onOpenChange, mode: controlledMode, onModeChange, title, readContent, editContent, onSave, saving, saveDisabled, error, onErrorDismiss, labels, }) {
8
+ const [internalMode, setInternalMode] = useState('read');
9
+ const mode = controlledMode ?? internalMode;
10
+ useEffect(() => {
11
+ if (!open)
12
+ return;
13
+ const initial = open.id === 'new' ? 'create' : 'read';
14
+ if (controlledMode === undefined)
15
+ setInternalMode(initial);
16
+ onModeChange?.(initial);
17
+ }, [open?.id]);
18
+ function setMode(next) {
19
+ if (controlledMode === undefined)
20
+ setInternalMode(next);
21
+ onModeChange?.(next);
22
+ }
23
+ function handleClose() {
24
+ onOpenChange(null);
25
+ }
26
+ async function handleSave() {
27
+ if (mode === 'read')
28
+ return;
29
+ await onSave(mode);
30
+ }
31
+ const closeLabel = labels?.close ?? 'Schließen';
32
+ const cancelLabel = labels?.cancel ?? 'Abbrechen';
33
+ const saveLabel = labels?.save ?? 'Speichern';
34
+ const createLabel = labels?.create ?? 'Erstellen';
35
+ const editLabel = labels?.edit ?? 'Bearbeiten';
36
+ return (_jsx(Dialog, { open: open !== null, onOpenChange: (o) => { if (!o)
37
+ handleClose(); }, title: title, footer: mode === 'read' ? (_jsxs(_Fragment, { children: [_jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "secondary", children: closeLabel }) }), _jsxs(Button, { onClick: () => setMode('edit'), children: [_jsx(Pencil, { className: "w-4 h-4" }), editLabel] })] })) : (_jsxs(_Fragment, { children: [_jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "secondary", children: cancelLabel }) }), _jsx(Button, { onClick: handleSave, loading: !!saving, disabled: !!saveDisabled, children: mode === 'create' ? createLabel : saveLabel })] })), children: mode === 'read' ? (_jsxs("div", { children: [_jsx("div", { className: "flex justify-end mb-2 -mt-1", children: _jsx("button", { onClick: () => setMode('edit'), className: "p-1.5 rounded-md text-[var(--color-text-muted)] hover:text-[var(--color-primary)] hover:bg-[var(--color-surface-raised)] transition-colors", title: editLabel, "aria-label": editLabel, children: _jsx(Pencil, { className: "w-4 h-4" }) }) }), readContent] })) : (_jsxs("div", { className: "flex flex-col gap-4", children: [error && (_jsx(Alert, { variant: "error", onDismiss: onErrorDismiss, children: error })), editContent] })) }));
38
+ }
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface SkeletonProps {
3
+ className?: string;
4
+ width?: string | number;
5
+ height?: string | number;
6
+ }
7
+ export declare function Skeleton({ className, width, height }: SkeletonProps): React.JSX.Element;
8
+ export declare function SkeletonRow(): React.JSX.Element;
9
+ export {};
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function Skeleton({ className = '', width, height }) {
3
+ return (_jsx("div", { className: `skeleton ${className}`, style: { width, height } }));
4
+ }
5
+ export function SkeletonRow() {
6
+ return (_jsxs("div", { className: "flex items-center gap-3 py-2", children: [_jsx(Skeleton, { width: 120, height: 14 }), _jsx(Skeleton, { width: 80, height: 14 })] }));
7
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ export declare function TooltipProvider({ children }: {
3
+ children: React.ReactNode;
4
+ }): React.JSX.Element;
5
+ interface TooltipProps {
6
+ content: React.ReactNode;
7
+ children: React.ReactNode;
8
+ side?: 'top' | 'right' | 'bottom' | 'left';
9
+ delayDuration?: number;
10
+ }
11
+ export declare function Tooltip({ content, children, side, delayDuration }: TooltipProps): React.JSX.Element;
12
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as RadixTooltip from '@radix-ui/react-tooltip';
3
+ export function TooltipProvider({ children }) {
4
+ return _jsx(RadixTooltip.Provider, { children: children });
5
+ }
6
+ export function Tooltip({ content, children, side = 'top', delayDuration = 300 }) {
7
+ return (_jsxs(RadixTooltip.Root, { delayDuration: delayDuration, children: [_jsx(RadixTooltip.Trigger, { asChild: true, children: children }), _jsx(RadixTooltip.Portal, { children: _jsxs(RadixTooltip.Content, { side: side, sideOffset: 6, className: "z-50 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5 text-xs text-[var(--color-text-primary)] shadow-md animate-in fade-in-0 zoom-in-95", children: [content, _jsx(RadixTooltip.Arrow, { className: "fill-[var(--color-border)]" })] }) })] }));
8
+ }
@@ -0,0 +1,11 @@
1
+ export { Button } from './Button';
2
+ export { Input } from './Input';
3
+ export { Badge } from './Badge';
4
+ export { Alert, type AlertVariant } from './Alert';
5
+ export { Dialog, DialogClose } from './Dialog';
6
+ export * as DropdownMenu from './DropdownMenu';
7
+ export { Tooltip, TooltipProvider } from './Tooltip';
8
+ export { EmptyState } from './EmptyState';
9
+ export { Skeleton, SkeletonRow } from './Skeleton';
10
+ export { RecordDialog, type RecordDialogMode } from './RecordDialog';
11
+ export { DataTable, DEFAULT_VIEW_VERSION, type ColumnDef, type FilterDef, type DataTableSelectionProps, } from './DataTable';
@@ -0,0 +1,11 @@
1
+ export { Button } from './Button';
2
+ export { Input } from './Input';
3
+ export { Badge } from './Badge';
4
+ export { Alert } from './Alert';
5
+ export { Dialog, DialogClose } from './Dialog';
6
+ export * as DropdownMenu from './DropdownMenu';
7
+ export { Tooltip, TooltipProvider } from './Tooltip';
8
+ export { EmptyState } from './EmptyState';
9
+ export { Skeleton, SkeletonRow } from './Skeleton';
10
+ export { RecordDialog } from './RecordDialog';
11
+ export { DataTable, DEFAULT_VIEW_VERSION, } from './DataTable';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @efa-one/sdk/frontend/ui/styles.css
3
+ *
4
+ * Globale Hilfsklassen, die `Badge` und `Skeleton` aus dem SDK-ui-Kit benötigen
5
+ * (sie sind CSS-Klassen-basiert, nicht Tailwind-Arbitrary). Einmal pro App
6
+ * importieren: import '@efa-one/sdk/frontend/ui/styles.css';
7
+ *
8
+ * Die Design-Tokens (`--color-*`, `--border-radius-*`) definiert die App selbst
9
+ * (Kernel-Theme-überschrieben zur Laufzeit) — hier bewusst NICHT dupliziert.
10
+ */
11
+
12
+ /* ── Badge utilities ─────────────────────────────────────────── */
13
+ .badge {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ padding: 2px 8px;
17
+ border-radius: var(--border-radius-sm);
18
+ font-size: 12px;
19
+ font-weight: 500;
20
+ }
21
+ .badge-success {
22
+ background: color-mix(in srgb, var(--color-success) 15%, transparent);
23
+ color: var(--color-success);
24
+ }
25
+ .badge-warning {
26
+ background: color-mix(in srgb, var(--color-warning) 15%, transparent);
27
+ color: var(--color-warning);
28
+ }
29
+ .badge-danger {
30
+ background: color-mix(in srgb, var(--color-danger) 15%, transparent);
31
+ color: var(--color-danger);
32
+ }
33
+ .badge-neutral {
34
+ background: var(--color-surface-raised);
35
+ color: var(--color-text-secondary);
36
+ }
37
+
38
+ /* ── Skeleton loading ────────────────────────────────────────── */
39
+ .skeleton {
40
+ background: linear-gradient(
41
+ 90deg,
42
+ var(--color-surface-raised) 25%,
43
+ var(--color-border) 50%,
44
+ var(--color-surface-raised) 75%
45
+ );
46
+ background-size: 200% 100%;
47
+ animation: shimmer 1.5s infinite;
48
+ border-radius: var(--border-radius-md);
49
+ }
50
+ @keyframes shimmer {
51
+ from { background-position: 200% 0; }
52
+ to { background-position: -200% 0; }
53
+ }
@@ -0,0 +1,11 @@
1
+ export interface ViewPreferencesAdapter {
2
+ get<T = unknown>(listId: string): Promise<T | null>;
3
+ put(listId: string, prefs: unknown): void | Promise<void>;
4
+ reset(listId: string): void | Promise<void>;
5
+ }
6
+ export interface ViewPreferencesClientOptions {
7
+ apiBase?: string | (() => string);
8
+ fetchImpl?: typeof fetch;
9
+ }
10
+ export declare function createViewPreferencesClient(opts?: ViewPreferencesClientOptions): ViewPreferencesAdapter;
11
+ export declare function useViewPreferences<T extends object>(listId: string, defaultPrefs: T, adapter?: ViewPreferencesAdapter): [T, (next: T) => void, () => Promise<void>, boolean];
@@ -0,0 +1,79 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ export function createViewPreferencesClient(opts = {}) {
3
+ const resolveBase = typeof opts.apiBase === 'function'
4
+ ? opts.apiBase
5
+ : () => opts.apiBase ?? '';
6
+ const f = opts.fetchImpl ?? fetch;
7
+ const url = (listId) => `${resolveBase()}/api/view-preferences/${encodeURIComponent(listId)}`;
8
+ return {
9
+ async get(listId) {
10
+ const res = await f(url(listId), { credentials: 'include' });
11
+ if (res.status === 204 || !res.ok)
12
+ return null;
13
+ return res.json();
14
+ },
15
+ async put(listId, prefs) {
16
+ await f(url(listId), {
17
+ method: 'PUT',
18
+ credentials: 'include',
19
+ headers: { 'Content-Type': 'application/json' },
20
+ body: JSON.stringify(prefs),
21
+ });
22
+ },
23
+ async reset(listId) {
24
+ await f(url(listId), { method: 'DELETE', credentials: 'include' });
25
+ },
26
+ };
27
+ }
28
+ export function useViewPreferences(listId, defaultPrefs, adapter) {
29
+ const [prefs, setPrefsState] = useState(defaultPrefs);
30
+ const [loaded, setLoaded] = useState(!adapter);
31
+ const saveTimer = useRef(null);
32
+ useEffect(() => {
33
+ if (!adapter)
34
+ return;
35
+ let cancelled = false;
36
+ Promise.resolve(adapter.get(listId))
37
+ .then((remote) => {
38
+ if (cancelled)
39
+ return;
40
+ const hasRemote = !!remote && typeof remote === 'object';
41
+ const dv = defaultPrefs.version;
42
+ const rv = hasRemote ? remote.version : undefined;
43
+ if (hasRemote && rv === dv) {
44
+ setPrefsState({ ...defaultPrefs, ...remote });
45
+ }
46
+ else {
47
+ setPrefsState(defaultPrefs);
48
+ if (hasRemote)
49
+ Promise.resolve(adapter.reset(listId)).catch(() => { });
50
+ }
51
+ setLoaded(true);
52
+ })
53
+ .catch(() => setLoaded(true));
54
+ return () => { cancelled = true; };
55
+ }, [listId]);
56
+ const setPrefs = useCallback((next) => {
57
+ setPrefsState(next);
58
+ if (!adapter || !loaded)
59
+ return;
60
+ if (saveTimer.current != null)
61
+ window.clearTimeout(saveTimer.current);
62
+ saveTimer.current = window.setTimeout(() => {
63
+ Promise.resolve(adapter.put(listId, next)).catch((err) => {
64
+ console.warn('view-preferences save failed', err);
65
+ });
66
+ }, 600);
67
+ }, [listId, loaded, adapter]);
68
+ const reset = useCallback(async () => {
69
+ setPrefsState(defaultPrefs);
70
+ if (!adapter)
71
+ return;
72
+ try {
73
+ await adapter.reset(listId);
74
+ }
75
+ catch {
76
+ }
77
+ }, [listId, defaultPrefs, adapter]);
78
+ return [prefs, setPrefs, reset, loaded];
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efa-one/sdk",
3
- "version": "1.10.2",
3
+ "version": "1.12.0",
4
4
  "description": "Offizielles SDK für efa-one-Apps: Auth/Session, Service-Discovery, Gateway-Client, Audit/Reporting/Mail/Notifications, Permission- und Capability-Registrierung sowie das Frontend-IPC-/i18n-Toolkit.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "commonjs",
@@ -20,6 +20,12 @@
20
20
  "import": "./frontend/index.js",
21
21
  "default": "./frontend/index.js"
22
22
  },
23
+ "./frontend/ui": {
24
+ "types": "./frontend/ui/index.d.ts",
25
+ "import": "./frontend/ui/index.js",
26
+ "default": "./frontend/ui/index.js"
27
+ },
28
+ "./frontend/ui/styles.css": "./frontend/ui/styles.css",
23
29
  "./frontend/*": {
24
30
  "types": "./frontend/*.d.ts",
25
31
  "import": "./frontend/*.js",
@@ -35,7 +41,7 @@
35
41
  "scripts": {
36
42
  "clean": "rm -rf backend frontend",
37
43
  "build:backend": "tsc -p tsconfig.backend.json",
38
- "build:frontend": "tsc -p tsconfig.frontend.json && node -e \"require('fs').writeFileSync('frontend/package.json', JSON.stringify({type:'module'}))\"",
44
+ "build:frontend": "tsc -p tsconfig.frontend.json && node -e \"const fs=require('fs');fs.writeFileSync('frontend/package.json', JSON.stringify({type:'module'}));fs.copyFileSync('src/frontend/ui/styles.css','frontend/ui/styles.css')\"",
39
45
  "build": "npm run clean && npm run build:backend && npm run build:frontend",
40
46
  "typecheck": "tsc -p tsconfig.backend.json --noEmit && tsc -p tsconfig.frontend.json --noEmit",
41
47
  "test": "vitest run",
@@ -45,12 +51,17 @@
45
51
  "access": "public"
46
52
  },
47
53
  "peerDependencies": {
54
+ "@radix-ui/react-dialog": ">=1",
55
+ "@radix-ui/react-dropdown-menu": ">=1",
56
+ "@radix-ui/react-tooltip": ">=1",
48
57
  "express": ">=4",
49
58
  "i18next": ">=23",
50
59
  "i18next-http-backend": ">=2",
51
60
  "jsonwebtoken": ">=9",
61
+ "lucide-react": ">=0.400.0",
52
62
  "pg": ">=8",
53
63
  "react": ">=18",
64
+ "react-dom": ">=18",
54
65
  "react-i18next": ">=13"
55
66
  },
56
67
  "peerDependenciesMeta": {
@@ -66,6 +77,9 @@
66
77
  "react": {
67
78
  "optional": true
68
79
  },
80
+ "react-dom": {
81
+ "optional": true
82
+ },
69
83
  "i18next": {
70
84
  "optional": true
71
85
  },
@@ -74,12 +88,28 @@
74
88
  },
75
89
  "react-i18next": {
76
90
  "optional": true
91
+ },
92
+ "@radix-ui/react-dialog": {
93
+ "optional": true
94
+ },
95
+ "@radix-ui/react-dropdown-menu": {
96
+ "optional": true
97
+ },
98
+ "@radix-ui/react-tooltip": {
99
+ "optional": true
100
+ },
101
+ "lucide-react": {
102
+ "optional": true
77
103
  }
78
104
  },
79
105
  "devDependencies": {
106
+ "@radix-ui/react-dialog": "^1.1.4",
107
+ "@radix-ui/react-dropdown-menu": "^2.1.4",
108
+ "@radix-ui/react-tooltip": "^1.1.6",
80
109
  "@types/express": "^4.17.21",
81
110
  "@types/jsonwebtoken": "^9.0.6",
82
111
  "cookie-parser": "^1.4.6",
112
+ "lucide-react": "^0.468.0",
83
113
  "@types/node": "^20.14.0",
84
114
  "@types/pg": "^8.11.0",
85
115
  "@types/react": "^18.2.46",
@@ -90,6 +120,8 @@
90
120
  "jsonwebtoken": "^9.0.2",
91
121
  "pg": "^8.12.0",
92
122
  "react": "^18.2.0",
123
+ "react-dom": "^18.2.0",
124
+ "@types/react-dom": "^18.2.0",
93
125
  "react-i18next": "^13.5.0",
94
126
  "supertest": "^7.2.2",
95
127
  "typescript": "7.0.2",