@efa-one/sdk 1.13.1 → 1.14.1

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
@@ -14,7 +14,7 @@ sub-paths — this keeps `express` out of the frontend and `react` out of the ba
14
14
  |---|---|---|
15
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
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'` |
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; reflows to stacked cards below 640px). Also exports `useIsMobile` — the platform-wide 640px breakpoint hook. Ship its companion CSS once: `import '@efa-one/sdk/frontend/ui/styles.css'` |
18
18
  | `@efa-one/sdk/frontend/viewPreferences` | Browser/React | Persistence seam for `DataTable` — `createViewPreferencesClient()` (standard `/api/view-preferences/:listId` adapter) + `useViewPreferences()` |
19
19
 
20
20
  Every individual module is also reachable directly, e.g.
@@ -112,6 +112,33 @@ The flat output layout (`backend/` + `frontend/` at the package root) is a
112
112
  deliberate choice so that consumers using `moduleResolution: node` (node10) can
113
113
  resolve the subpath imports physically — without having to change their tsconfig.
114
114
 
115
+ ## Quality gates (CI)
116
+
117
+ Every push and pull request runs the full gate; the same gate also runs as a
118
+ prerequisite job of the release workflow, so a `v*` tag can never publish a state
119
+ that a PR would have rejected.
120
+
121
+ | Workflow | Runs on | What it enforces |
122
+ |---|---|---|
123
+ | `build-check.yml` | push (`main`, `development`), PR, called by `publish.yml` | `tsc` typecheck (sources **and** `test/` + `scripts/`), Vitest **with coverage thresholds**, ESLint, full `npm run build` (incl. the ESM specifier gate), plus `npm ci` **and** `npm run build` in both `template/` scaffolds |
124
+ | `security.yml` | push, PR, weekly (Mon 06:00 UTC) | `npm audit --audit-level=high` and a copyleft license gate (GPL/AGPL/LGPL) across all three npm directories, Trivy filesystem scan (HIGH/CRITICAL, **including devDependencies** — without that switch Trivy skips them and the scan would be structurally empty here), Trivy misconfig/secret scan (report-only) |
125
+ | `publish.yml` | `v*` tag | tag ↔ `package.json` version match, then npm publish via OIDC trusted publishing |
126
+ | `dependabot.yml` + `dependabot-automerge.yml` | weekly | grouped minor/patch dependency PRs for the SDK, both scaffolds, the scaffold's Docker base images and the GitHub Actions themselves. Auto-merge is present but dormant — see the header comment in the workflow for the three settings that arm it |
127
+
128
+ Run the blocking gates locally before pushing:
129
+
130
+ ```bash
131
+ ./scripts/ci-local.sh # mirrors build-check.yml + security.yml (Trivy via docker if not installed)
132
+ ```
133
+
134
+ Coverage uses a two-level ratchet (global floor + per-file gates on the modules that
135
+ are deliberately tested); see `vitest.config.ts`. When new tests raise coverage, raise
136
+ the thresholds — never lower them.
137
+
138
+ **TypeScript is pinned to exactly `6.0.3`.** That is the last JS-based line;
139
+ `typescript-eslint` does not support the native 7.0 compiler and aborts the entire lint
140
+ run under it. Dependabot is configured to never propose a TypeScript major.
141
+
115
142
  ## Versioning
116
143
 
117
144
  SemVer. Consuming apps pull new versions with `npm update @efa-one/sdk`.
@@ -1,3 +1,4 @@
1
1
  export * from './ipc.js';
2
+ export { useIsMobile, MOBILE_QUERY } from './useIsMobile.js';
2
3
  export * from './i18n.js';
3
4
  export { default as DevHeader } from './DevHeader.js';
package/frontend/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './ipc.js';
2
+ export { useIsMobile, MOBILE_QUERY } from './useIsMobile.js';
2
3
  export * from './i18n.js';
3
4
  export { default as DevHeader } from './DevHeader.js';
@@ -1,9 +1,10 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import * as DropdownMenu from './DropdownMenu.js';
4
4
  import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X } from 'lucide-react';
5
5
  import { Button } from './Button.js';
6
6
  import { useViewPreferences } from '../viewPreferences.js';
7
+ import { useIsMobile } from '../useIsMobile.js';
7
8
  const GROUP_KEY_SEP = '\x1f';
8
9
  const EMPTY_GROUP_LABEL = '– ohne –';
9
10
  export const DEFAULT_VIEW_VERSION = 2;
@@ -79,6 +80,7 @@ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
79
80
  export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, persistence, }) {
80
81
  const defaultPrefs = useMemo(() => defaultPrefsFor(columns, initialPrefs), [columns, initialPrefs]);
81
82
  const [prefs, setPrefs, reset] = useViewPreferences(listId, defaultPrefs, persistence);
83
+ const isMobile = useIsMobile();
82
84
  const [expandedGroups, setExpandedGroups] = useState(new Set());
83
85
  const toggleGroup = (key) => {
84
86
  setExpandedGroups((prev) => {
@@ -236,12 +238,13 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
236
238
  }
237
239
  setPrefs({ ...prefs, filters });
238
240
  };
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) => {
241
+ const gearButton = (_jsx(ColumnInventoryButton, { columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy }));
242
+ return (_jsxs("div", { className: "border border-[var(--color-border)] overflow-x-auto", 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] })), isMobile ? (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [selection && (_jsxs("label", { className: "flex items-center gap-2 py-2 pr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "w-5 h-5", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" }), _jsx("span", { className: "text-[var(--color-text-muted)]", children: "Alle" })] })), _jsx("div", { className: "flex-1" }), gearButton] })) : (_jsxs("div", { className: "grid items-center min-w-min 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
243
  const activeSort = prefs.sort?.columnId === col.id ? prefs.sort.direction : null;
241
244
  const filterValue = prefs.filters[col.id];
242
245
  const isFiltered = filterValue != null;
243
246
  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) => {
247
+ }), gearButton] })), 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
248
  if (item.kind === 'group') {
246
249
  const level = item.level ?? 0;
247
250
  const groupColLabel = columnsById.get(groupBy[level])?.label ?? '';
@@ -256,7 +259,10 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
256
259
  const key = rowKey(row);
257
260
  const isChecked = selection?.selected.has(key) ?? false;
258
261
  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: {
262
+ if (isMobile) {
263
+ return (_jsxs("div", { className: `flex flex-col gap-1.5 px-3 py-3 border-b border-[var(--color-border)] ${onRowClick ? 'cursor-pointer hover:bg-[var(--color-surface-raised)]' : ''}`, style: { background: isChecked ? 'rgba(99,102,241,0.07)' : undefined, paddingLeft: rowIndent }, onClick: () => onRowClick?.(row), children: [selection && (_jsxs("label", { className: "flex items-center gap-2 self-start py-1 pr-2 cursor-pointer", onClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", className: "w-5 h-5", checked: isChecked, onChange: () => toggleOne(key), "aria-label": "Zeile ausw\u00E4hlen" }), _jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: "Ausw\u00E4hlen" })] })), visibleColumns.map((col) => (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: col.label }), _jsx("div", { className: "min-w-0 break-words text-sm", children: col.cell ? col.cell(row) : String(col.accessor(row) ?? '') })] }, col.id)))] }, `r:${String(key)}:${idx}`));
264
+ }
265
+ return (_jsxs("div", { className: `grid items-center min-w-min px-3 py-2 text-sm border-b border-[var(--color-border)] ${onRowClick ? 'cursor-pointer hover:bg-[var(--color-surface-raised)]' : ''}`, style: {
260
266
  gridTemplateColumns: gridTemplate,
261
267
  background: isChecked ? 'rgba(99,102,241,0.07)' : undefined,
262
268
  paddingLeft: rowIndent,
@@ -306,9 +312,9 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
306
312
  const byId = new Map(columns.map((c) => [c.id, c]));
307
313
  return prefs.columnOrder.map((id) => byId.get(id)).filter(Boolean);
308
314
  }, [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) => {
315
+ return (_jsxs(DropdownMenu.Root, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx("button", { type: "button", className: "p-2.5 sm: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
316
  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));
317
+ 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-2 sm: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-2 sm: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-2 sm: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
318
  }), _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
319
  const v = e.target.value;
314
320
  if (v)
@@ -316,6 +322,6 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
316
322
  e.target.value = '';
317
323
  }, 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
324
  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));
325
+ 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-2 sm: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-2 sm: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
326
  }), _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
327
  }
@@ -1,4 +1,4 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Pencil } from 'lucide-react';
4
4
  import { Dialog, DialogClose } from './Dialog.js';
@@ -8,4 +8,5 @@ export { Tooltip, TooltipProvider } from './Tooltip.js';
8
8
  export { EmptyState } from './EmptyState.js';
9
9
  export { Skeleton, SkeletonRow } from './Skeleton.js';
10
10
  export { RecordDialog, type RecordDialogMode } from './RecordDialog.js';
11
+ export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
11
12
  export { DataTable, DEFAULT_VIEW_VERSION, type ColumnDef, type FilterDef, type DataTableSelectionProps, } from './DataTable.js';
@@ -8,4 +8,5 @@ export { Tooltip, TooltipProvider } from './Tooltip.js';
8
8
  export { EmptyState } from './EmptyState.js';
9
9
  export { Skeleton, SkeletonRow } from './Skeleton.js';
10
10
  export { RecordDialog } from './RecordDialog.js';
11
+ export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
11
12
  export { DataTable, DEFAULT_VIEW_VERSION, } from './DataTable.js';
@@ -0,0 +1,2 @@
1
+ export declare const MOBILE_QUERY = "(max-width: 640px)";
2
+ export declare function useIsMobile(): boolean;
@@ -0,0 +1,17 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ export const MOBILE_QUERY = '(max-width: 640px)';
3
+ function subscribe(callback) {
4
+ if (typeof window === 'undefined' || !window.matchMedia)
5
+ return () => { };
6
+ const mql = window.matchMedia(MOBILE_QUERY);
7
+ mql.addEventListener('change', callback);
8
+ return () => mql.removeEventListener('change', callback);
9
+ }
10
+ function getSnapshot() {
11
+ if (typeof window === 'undefined' || !window.matchMedia)
12
+ return false;
13
+ return window.matchMedia(MOBILE_QUERY).matches;
14
+ }
15
+ export function useIsMobile() {
16
+ return useSyncExternalStore(subscribe, getSnapshot, () => false);
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@efa-one/sdk",
3
- "version": "1.13.1",
3
+ "version": "1.14.1",
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",
@@ -43,8 +43,10 @@
43
43
  "build:backend": "tsc -p tsconfig.backend.json",
44
44
  "build:frontend": "tsc -p tsconfig.frontend.json && node scripts/postbuild-frontend.mjs",
45
45
  "build": "npm run clean && npm run build:backend && npm run build:frontend",
46
- "typecheck": "tsc -p tsconfig.backend.json --noEmit && tsc -p tsconfig.frontend.json --noEmit",
46
+ "typecheck": "tsc -p tsconfig.backend.json --noEmit && tsc -p tsconfig.frontend.json --noEmit && tsc -p tsconfig.test.json --noEmit",
47
+ "lint": "eslint src test scripts",
47
48
  "test": "vitest run",
49
+ "test:coverage": "vitest run --coverage",
48
50
  "prepublishOnly": "npm run build"
49
51
  },
50
52
  "publishConfig": {
@@ -103,28 +105,33 @@
103
105
  }
104
106
  },
105
107
  "devDependencies": {
108
+ "@eslint/js": "^9.39.5",
106
109
  "@radix-ui/react-dialog": "^1.1.4",
107
110
  "@radix-ui/react-dropdown-menu": "^2.1.4",
108
111
  "@radix-ui/react-tooltip": "^1.1.6",
109
112
  "@types/express": "^4.17.21",
110
113
  "@types/jsonwebtoken": "^9.0.6",
111
- "cookie-parser": "^1.4.6",
112
- "lucide-react": "^0.468.0",
113
114
  "@types/node": "^20.14.0",
114
115
  "@types/pg": "^8.11.0",
115
116
  "@types/react": "^18.2.46",
117
+ "@types/react-dom": "^18.2.0",
116
118
  "@types/supertest": "^7.2.0",
119
+ "@vitest/coverage-v8": "^4.1.11",
120
+ "cookie-parser": "^1.4.6",
121
+ "eslint": "^9.39.5",
122
+ "eslint-plugin-react-hooks": "^7.1.1",
117
123
  "express": "^4.19.2",
118
124
  "i18next": "^23.11.0",
119
125
  "i18next-http-backend": "^2.7.3",
120
126
  "jsonwebtoken": "^9.0.2",
127
+ "lucide-react": "^0.577.0",
121
128
  "pg": "^8.12.0",
122
129
  "react": "^18.2.0",
123
130
  "react-dom": "^18.2.0",
124
- "@types/react-dom": "^18.2.0",
125
131
  "react-i18next": "^13.5.0",
126
132
  "supertest": "^7.2.2",
127
- "typescript": "7.0.2",
133
+ "typescript": "6.0.3",
134
+ "typescript-eslint": "^8.67.0",
128
135
  "vitest": "^4.1.4"
129
136
  },
130
137
  "engines": {