viur-ui 0.0.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.
Files changed (114) hide show
  1. package/README.md +79 -0
  2. package/index.d.ts +351 -0
  3. package/index.js +46 -0
  4. package/package.json +84 -0
  5. package/src/components/Browser/Browser.vue +496 -0
  6. package/src/components/Browser/TreeNav.vue +408 -0
  7. package/src/components/Browser/ViurBrowser.d.vue.ts +64 -0
  8. package/src/components/Browser/ViurBrowser.vue +271 -0
  9. package/src/components/Browser/dragSession.ts +134 -0
  10. package/src/components/Browser/index.ts +14 -0
  11. package/src/components/Browser/types.ts +30 -0
  12. package/src/components/DataTable/ColumnFilterPanel.vue +225 -0
  13. package/src/components/DataTable/DataTable.vue +3155 -0
  14. package/src/components/DataTable/ViurDataTable.d.vue.ts +72 -0
  15. package/src/components/DataTable/ViurDataTable.vue +331 -0
  16. package/src/components/DataTable/columns.ts +400 -0
  17. package/src/components/DataTable/index.ts +60 -0
  18. package/src/components/DataTable/rows.ts +446 -0
  19. package/src/components/DataTable/types.ts +267 -0
  20. package/src/components/DataTable/useColumnLayout.ts +294 -0
  21. package/src/components/DataTable/useGridNavigation.ts +193 -0
  22. package/src/components/DataTable/useVirtualRows.ts +126 -0
  23. package/src/components/DataTable/viurColumns.ts +330 -0
  24. package/src/forms/ViForm.d.vue.ts +106 -0
  25. package/src/forms/ViForm.vue +592 -0
  26. package/src/forms/ViFormCategory.vue +57 -0
  27. package/src/forms/bones/Bone.vue +1023 -0
  28. package/src/forms/bones/BoneActions.vue +127 -0
  29. package/src/forms/bones/BoneLabel.vue +122 -0
  30. package/src/forms/bones/BoneMultipleList.vue +91 -0
  31. package/src/forms/bones/BoneMultipleWrapper.vue +105 -0
  32. package/src/forms/bones/BoneNestedWrapper.vue +155 -0
  33. package/src/forms/bones/BoneSingleEntry.vue +51 -0
  34. package/src/forms/bones/FilterBone.vue +694 -0
  35. package/src/forms/bones/actionbar/defaultBar.vue +98 -0
  36. package/src/forms/bones/actionbar/fileBar.vue +165 -0
  37. package/src/forms/bones/actionbar/relationalBar.vue +125 -0
  38. package/src/forms/bones/actions/ActionDialog.vue +70 -0
  39. package/src/forms/bones/actions/Debug.vue +106 -0
  40. package/src/forms/bones/actions/Delete.vue +13 -0
  41. package/src/forms/bones/actions/DescribeImage.vue +201 -0
  42. package/src/forms/bones/actions/Download.vue +27 -0
  43. package/src/forms/bones/actions/Placeholder.vue +14 -0
  44. package/src/forms/bones/actions/Translation.vue +238 -0
  45. package/src/forms/bones/actions/index.js +280 -0
  46. package/src/forms/bones/bone.d.ts +45 -0
  47. package/src/forms/bones/country_information.json +779 -0
  48. package/src/forms/bones/index.js +225 -0
  49. package/src/forms/bones/uploadAdapter.js +46 -0
  50. package/src/forms/bones/utils.js +284 -0
  51. package/src/forms/bones/view/base_item.vue +35 -0
  52. package/src/forms/bones/view/boneLogic.js +202 -0
  53. package/src/forms/bones/view/index.js +5 -0
  54. package/src/forms/bones/widgets/booleanBone.vue +83 -0
  55. package/src/forms/bones/widgets/booleanBoneChoose.vue +57 -0
  56. package/src/forms/bones/widgets/booleanBoneSelect.vue +58 -0
  57. package/src/forms/bones/widgets/codeBone.vue +214 -0
  58. package/src/forms/bones/widgets/colorBone.vue +75 -0
  59. package/src/forms/bones/widgets/dateBone.vue +114 -0
  60. package/src/forms/bones/widgets/emailBone.vue +100 -0
  61. package/src/forms/bones/widgets/fileBone.vue +273 -0
  62. package/src/forms/bones/widgets/jsonBone.vue +63 -0
  63. package/src/forms/bones/widgets/keyBone.vue +71 -0
  64. package/src/forms/bones/widgets/numericBone.vue +125 -0
  65. package/src/forms/bones/widgets/passwordBone.vue +215 -0
  66. package/src/forms/bones/widgets/phoneBone.vue +173 -0
  67. package/src/forms/bones/widgets/rawBone.vue +94 -0
  68. package/src/forms/bones/widgets/recordBone.vue +64 -0
  69. package/src/forms/bones/widgets/relationalBone.vue +164 -0
  70. package/src/forms/bones/widgets/relationalBoneSelect.vue +120 -0
  71. package/src/forms/bones/widgets/selectBone.vue +122 -0
  72. package/src/forms/bones/widgets/selectBoneChoose.vue +104 -0
  73. package/src/forms/bones/widgets/spatialBone.vue +111 -0
  74. package/src/forms/bones/widgets/stringBone.vue +139 -0
  75. package/src/forms/bones/widgets/textBone.vue +641 -0
  76. package/src/forms/install.d.ts +4 -0
  77. package/src/forms/install.js +43 -0
  78. package/src/forms/layouts/LayloutList2Columns.vue +57 -0
  79. package/src/forms/layouts/LayoutCategory.vue +38 -0
  80. package/src/forms/layouts/LayoutList.vue +31 -0
  81. package/src/forms/utils.js +601 -0
  82. package/src/public/assets/icons/fontawesome-manifest.json +8 -0
  83. package/src/public/assets/icons/regular/file.svg +1 -0
  84. package/src/public/assets/icons/solid/arrows-left-right.svg +1 -0
  85. package/src/public/assets/icons/solid/bug.svg +1 -0
  86. package/src/public/assets/icons/solid/check.svg +1 -0
  87. package/src/public/assets/icons/solid/circle-info.svg +1 -0
  88. package/src/public/assets/icons/solid/compress.svg +1 -0
  89. package/src/public/assets/icons/solid/download.svg +1 -0
  90. package/src/public/assets/icons/solid/expand.svg +1 -0
  91. package/src/public/assets/icons/solid/grip-vertical.svg +1 -0
  92. package/src/public/assets/icons/solid/plus.svg +1 -0
  93. package/src/public/assets/icons/solid/question.svg +1 -0
  94. package/src/public/assets/icons/solid/triangle-exclamation.svg +1 -0
  95. package/src/public/assets/icons/solid/upload.svg +1 -0
  96. package/src/public/assets/icons/solid/xmark.svg +1 -0
  97. package/src/request/cache.js +183 -0
  98. package/src/request/configStore.js +516 -0
  99. package/src/request/envelope.js +211 -0
  100. package/src/request/index.js +35 -0
  101. package/src/request/messageStore.js +201 -0
  102. package/src/request/request.js +752 -0
  103. package/src/request/skel.js +90 -0
  104. package/src/theme/colorStore.js +168 -0
  105. package/src/translations/de.js +178 -0
  106. package/src/translations/en.js +177 -0
  107. package/src/user/LoginScreen.d.vue.ts +54 -0
  108. package/src/user/LoginScreen.vue +627 -0
  109. package/src/user/google.js +75 -0
  110. package/src/user/index.js +3 -0
  111. package/src/user/store.d.ts +59 -0
  112. package/src/user/store.js +183 -0
  113. package/src/webawesomeCompat.css +72 -0
  114. package/src/webawesomeConfig.js +61 -0
@@ -0,0 +1,267 @@
1
+ // Typen des DataTable-Widgets — nachgebaut nach <wa-data-grid> (WebAwesome Pro
2
+ // v12), aber mit Vue-Mitteln: Zell-Inhalte kommen über Slots statt über
3
+ // Lit-Templates, Zustand über v-model statt über Properties am Element.
4
+
5
+ export type Row = Record<string, unknown>
6
+
7
+ export type Align = "start" | "center" | "end"
8
+ export type PinSide = "start" | "end"
9
+
10
+ export type SortMode =
11
+ | "auto" // Zahlen numerisch, Datum chronologisch, sonst alphanumerisch
12
+ | "text"
13
+ | "textCaseSensitive"
14
+ | "number"
15
+ | "datetime"
16
+ | "basic"
17
+
18
+ export type FilterType =
19
+ | "text" // Teilstring, case-insensitive (Default)
20
+ | "equals"
21
+ | "number-range" // [min, max]
22
+ | "date-range" // [von, bis], inklusive nach Kalendertag
23
+ | "set" // Wert ist einer der gewählten
24
+ | "includes-any" // Array-Zelle enthält einen der gewählten
25
+ | "includes-all" // Array-Zelle enthält alle gewählten
26
+
27
+ export type AggregationKind = "sum" | "min" | "max" | "extent" | "mean" | "median" | "count" | "unique" | "uniqueCount"
28
+
29
+ export interface FilterOption {
30
+ value: string
31
+ label?: string
32
+ count?: number
33
+ }
34
+
35
+ /** Eine Spaltendefinition. Reines Datenobjekt — reaktiv, ohne Render-Logik. */
36
+ export interface DataTableColumn<R extends Row = Row> {
37
+ /** Pfad in die Zeile, Punktpfade erlaubt ("user.name"). Dient als id, wenn `id` fehlt. */
38
+ field?: string
39
+ /** Explizite id. Pflicht für Spalten ohne `field` (Aktionen, berechnete Spalten). */
40
+ id?: string
41
+ /**
42
+ * Berechnet den Zellwert aus der Zeile. Anders als `formatter` (nur Darstellung)
43
+ * geht dieser Wert in Sortierung, Filter, Suche und Export ein.
44
+ */
45
+ value?: (row: R) => unknown
46
+ label?: string
47
+ align?: Align
48
+ headerAlign?: Align
49
+ /** Default: true, sobald `field` oder `value` vorhanden ist. */
50
+ sortable?: boolean
51
+ sortFn?: SortMode
52
+ /** Wohin leere Werte sortieren. */
53
+ sortUndefined?: false | "first" | "last"
54
+ /** Erste Sortierung dieser Spalte absteigend. */
55
+ sortDescFirst?: boolean
56
+ comparator?: (a: unknown, b: unknown, rowA: R, rowB: R) => number
57
+ /** Default: true, sobald `field` oder `value` vorhanden ist. */
58
+ searchable?: boolean
59
+ filterable?: boolean
60
+ filterType?: FilterType
61
+ filterFn?: (value: unknown, filterValue: unknown, row: R) => boolean
62
+ /** Ersetzt die berechneten Facetten im set/includes-Filter (z. B. im Server-Modus). */
63
+ filterOptions?: FilterOption[]
64
+ hidden?: boolean
65
+ hideable?: boolean
66
+ resizable?: boolean
67
+ movable?: boolean
68
+ pinnable?: boolean
69
+ pinned?: PinSide
70
+ width?: number
71
+ minWidth?: number
72
+ maxWidth?: number
73
+ /** Anteil am freien Platz; schlägt `width`. */
74
+ flex?: number
75
+ /** Zelltext. Für reichen Inhalt den Slot `cell:<id>` verwenden. */
76
+ formatter?: (value: unknown, row: R) => string
77
+ /**
78
+ * Wert(e) als farbige Plakette(n) statt als Text rendern. Positive Werte
79
+ * grün, negative rot, alles dazwischen regenbogen-abgestuft — siehe
80
+ * `badgesForBone` in viurColumns.ts, das diese Funktion für reine
81
+ * select- und bool-Bones erzeugt.
82
+ */
83
+ badge?: (value: unknown, row: R) => CellBadge[] | null
84
+ /** Fußzeile: Text oder Funktion über die gefilterten Zeilen (alle Seiten). */
85
+ footer?: string | ((rows: R[]) => string)
86
+ aggregation?: AggregationKind | ((values: unknown[], rows: R[]) => unknown)
87
+ cellClass?: string | ((value: unknown, row: R) => string)
88
+ }
89
+
90
+ /** Spalte mit aufgelösten Defaults — was die Komponente intern benutzt. */
91
+ /** Eine farbige Zell-Plakette (siehe DataTableColumn.badge). */
92
+ export interface CellBadge {
93
+ label: string
94
+ /** CSS-Farbe (Basiston); Hintergrund/Rand mischt die Tabelle selbst. */
95
+ color: string
96
+ }
97
+
98
+ export interface ResolvedColumn<R extends Row = Row> {
99
+ id: string
100
+ def: DataTableColumn<R>
101
+ field?: string
102
+ label: string
103
+ align: Align
104
+ headerAlign: Align
105
+ sortable: boolean
106
+ searchable: boolean
107
+ filterable: boolean
108
+ filterType: FilterType
109
+ hideable: boolean
110
+ /** Zellwert einer Zeile (value() oder Punktpfad). */
111
+ get: (row: R) => unknown
112
+ /** Zelltext für Anzeige, Suche als Text, Kopieren und CSV. */
113
+ text: (row: R) => string
114
+ }
115
+
116
+ export interface SortEntry {
117
+ id: string
118
+ desc: boolean
119
+ }
120
+
121
+ export interface FilterEntry {
122
+ id: string
123
+ value: unknown
124
+ }
125
+
126
+ export interface ColumnFacets {
127
+ /** Verschiedene Werte mit Trefferzahl, absteigend nach Häufigkeit. */
128
+ values: { value: string; count: number }[]
129
+ min?: number
130
+ max?: number
131
+ }
132
+
133
+ /** Anfrage an den Server (dataSource / data-request). */
134
+ export interface DataTableRequest {
135
+ sort: SortEntry[]
136
+ filters: FilterEntry[]
137
+ search: string
138
+ page: number
139
+ pageSize: number
140
+ signal: AbortSignal
141
+ }
142
+
143
+ export interface DataTableResponse<R extends Row = Row> {
144
+ rows: R[]
145
+ total: number
146
+ }
147
+
148
+ /** Serialisierbarer Schnappschuss des vom Nutzer verstellbaren Zustands. */
149
+ export interface DataTableState {
150
+ version: 1
151
+ columnOrder?: string[]
152
+ columnWidths?: Record<string, number>
153
+ columnVisibility?: Record<string, boolean>
154
+ columnPinning?: { start: string[]; end: string[] }
155
+ sort?: SortEntry[]
156
+ filters?: FilterEntry[]
157
+ search?: string
158
+ selectedKeys?: RowKey[]
159
+ expandedKeys?: RowKey[]
160
+ page?: number
161
+ pageSize?: number
162
+ }
163
+
164
+ export type RowKey = string | number
165
+
166
+ /** Knoten der (Baum-/Gruppen-)Hierarchie nach Filter und Sortierung. */
167
+ export interface RowNode<R extends Row = Row> {
168
+ kind: "data" | "group"
169
+ key: RowKey
170
+ depth: number
171
+ children: RowNode<R>[]
172
+ /** kind "data" */
173
+ row?: R
174
+ /** kind "group" */
175
+ groupColumnId?: string
176
+ groupValue?: unknown
177
+ groupCount?: number
178
+ groupRows?: R[]
179
+ aggregates?: Record<string, unknown>
180
+ }
181
+
182
+ /** Eine gerenderte Zeile (flach, inklusive Detail-Panels). */
183
+ export interface DisplayRow<R extends Row = Row> {
184
+ kind: "data" | "group" | "detail"
185
+ /** Eindeutig im Render-Durchlauf (Detailzeilen haben ein Suffix). */
186
+ key: string
187
+ /** Schlüssel der zugehörigen Zeile/Gruppe. */
188
+ nodeKey: RowKey
189
+ depth: number
190
+ expandable: boolean
191
+ expanded: boolean
192
+ /** Laufender Index der Datenzeilen (nur kind "data"). */
193
+ index: number
194
+ /**
195
+ * Die DURCHLAUF-Spalten links des eigenen Winkels (Länge = `depth - 1`).
196
+ * `true` heißt: dort läuft eine senkrechte Linie durch, weil der Vorfahre
197
+ * dieser Spalte noch ein Geschwister nach sich hat.
198
+ *
199
+ * Wichtig ist die Zuordnung: die Linie in einer Spalte verbindet nicht den
200
+ * Vorfahren DIESER Ebene mit seinem Geschwister, sondern den Vorfahren der
201
+ * NÄCHSTEN Ebene — genau wie in `tree`-Ausgaben:
202
+ *
203
+ * ```
204
+ * A
205
+ * ├── B
206
+ * │ └── C ← das "│" steht hier, weil nach B noch D kommt
207
+ * └── D
208
+ * ```
209
+ *
210
+ * Wurzeln erzeugen keine Durchlauf-Spalte: zwischen zwei Wurzeln wird keine
211
+ * Linie gezogen.
212
+ */
213
+ ancestorLines: boolean[]
214
+ /** Letztes Kind seines Elternteils → Winkel „└" statt „├". */
215
+ isLastChild: boolean
216
+ row?: R
217
+ node?: RowNode<R>
218
+ }
219
+
220
+ /**
221
+ * Die per `defineExpose` freigegebene API der Komponente. Als Typ für
222
+ * Template-Refs verwenden — `InstanceType<typeof DataTable>` funktioniert bei
223
+ * generischen Komponenten nicht.
224
+ */
225
+ export interface DataTableApi<R extends Row = Row> {
226
+ getVisibleRows: () => R[]
227
+ getProcessedRows: () => R[]
228
+ getSelectedRows: () => R[]
229
+ scrollToIndex: (index: number, align?: "start" | "center" | "end") => void
230
+ expandRow: (key: RowKey) => void
231
+ collapseRow: (key: RowKey) => void
232
+ expandAllRows: () => void
233
+ collapseAllRows: () => void
234
+ toggleColumn: (id: string, visible: boolean) => void
235
+ pinColumn: (id: string, side: PinSide | false) => void
236
+ getColumnPin: (id: string) => PinSide | false
237
+ autoSizeColumn: (id: string) => void
238
+ autoSizeColumns: () => void
239
+ sizeColumnsToFit: () => void
240
+ resetColumns: () => void
241
+ getColumnFacets: (id: string) => ColumnFacets
242
+ reload: () => void
243
+ /** Nachgeladene Kinder einer Zeile verwerfen und erneut holen. */
244
+ reloadChildren: (key: RowKey) => Promise<void>
245
+ /** Nachgeladene Kinder einer Zeile (undefined = noch nicht geladen). */
246
+ getLoadedChildren: (key: RowKey) => R[] | undefined
247
+ /** Nachgeladene Kinder einer Zeile ersetzen, ohne zu laden. */
248
+ setLoadedChildren: (key: RowKey, rows: R[]) => void
249
+ getDataAsCsv: (options?: {
250
+ columnIds?: string[]
251
+ includeHeaders?: boolean
252
+ delimiter?: string
253
+ escapeFormulas?: boolean
254
+ }) => string
255
+ exportDataAsCsv: (options?: {
256
+ fileName?: string
257
+ columnIds?: string[]
258
+ includeHeaders?: boolean
259
+ delimiter?: string
260
+ escapeFormulas?: boolean
261
+ }) => void
262
+ copySelectedRows: (options?: { format?: "tsv" | "csv" }) => Promise<number>
263
+ getState: () => DataTableState
264
+ setState: (state: Partial<DataTableState>) => void
265
+ resetState: () => void
266
+ focus: () => void
267
+ }
@@ -0,0 +1,294 @@
1
+ // Spalten-Layout: Reihenfolge, Breiten, Sichtbarkeit, Pinning — plus die
2
+ // Zeigergesten für Resize und Reorder. Alles in EINEM reaktiven State-Objekt
3
+ // (analog zur Komponenten-Regel), damit die Komponente nur noch rendert.
4
+
5
+ import { computed, reactive } from "vue"
6
+ import type { PinSide, ResolvedColumn, Row } from "./types"
7
+
8
+ const DEFAULT_WIDTH = 160
9
+ const DEFAULT_MIN_WIDTH = 64
10
+
11
+ export interface LayoutColumn<R extends Row = Row> {
12
+ column: ResolvedColumn<R>
13
+ /** Index in der sichtbaren Reihenfolge. */
14
+ index: number
15
+ /** Effektive Breite in px (gemessen oder gesetzt). */
16
+ width: number
17
+ /** Spaltenbreite für grid-template-columns. */
18
+ track: string
19
+ pinned: PinSide | false
20
+ /** Sticky-Abstand zur jeweiligen Kante (px). */
21
+ offset: number
22
+ /** Letzte am Start bzw. erste am Ende gepinnte Spalte (für die Trennlinie). */
23
+ pinEdge: boolean
24
+ resizable: boolean
25
+ movable: boolean
26
+ pinnable: boolean
27
+ }
28
+
29
+ export interface ColumnLayoutOptions<R extends Row = Row> {
30
+ columns: () => ResolvedColumn<R>[]
31
+ resizable?: () => boolean
32
+ reorderable?: () => boolean
33
+ pinnable?: () => boolean
34
+ onResize?: (id: string, width: number, finished: boolean) => void
35
+ onMove?: (order: string[], finished: boolean) => void
36
+ onPin?: (id: string, side: PinSide | false) => void
37
+ onVisibilityChange?: (id: string, visible: boolean) => void
38
+ }
39
+
40
+ function clampWidth<R extends Row>(column: ResolvedColumn<R>, width: number): number {
41
+ const min = column.def.minWidth ?? DEFAULT_MIN_WIDTH
42
+ const max = column.def.maxWidth ?? Number.POSITIVE_INFINITY
43
+ return Math.max(min, Math.min(max, width))
44
+ }
45
+
46
+ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOptions<R>) {
47
+ const state = reactive({
48
+ /** Vom Nutzer gesetzte Reihenfolge (ids). Leer = Reihenfolge der Definitionen. */
49
+ order: [] as string[],
50
+ /** Vom Nutzer gezogene Breiten. */
51
+ widths: {} as Record<string, number>,
52
+ /** Sichtbarkeit; fehlt ein Eintrag, gilt `hidden` aus der Definition. */
53
+ visibility: {} as Record<string, boolean>,
54
+ /** Pinning; fehlt ein Eintrag, gilt `pinned` aus der Definition. */
55
+ pinning: {} as Record<string, PinSide | false>,
56
+ /** Im DOM gemessene Breiten — Grundlage der Sticky-Abstände. */
57
+ measured: {} as Record<string, number>,
58
+ /** Laufende Resize-Geste. */
59
+ resizing: null as { id: string; startX: number; startWidth: number } | null,
60
+ /** Laufende Reorder-Geste. */
61
+ dragging: null as { id: string; overId: string | null; x: number; y: number } | null,
62
+
63
+ ordered: computed((): ResolvedColumn<R>[] => {
64
+ const columns = options.columns()
65
+ if (!state.order.length) return columns
66
+ const byId = new Map(columns.map((column) => [column.id, column]))
67
+ const sorted: ResolvedColumn<R>[] = []
68
+ for (const id of state.order) {
69
+ const column = byId.get(id)
70
+ if (column) {
71
+ sorted.push(column)
72
+ byId.delete(id)
73
+ }
74
+ }
75
+ // Neue Spalten, die die gespeicherte Reihenfolge nicht kennt, hinten dran.
76
+ return [...sorted, ...byId.values()]
77
+ }),
78
+
79
+ /** Sichtbare Spalten in Anzeigereihenfolge: gepinnt (Start) → frei → gepinnt (Ende). */
80
+ visibleColumns: computed((): LayoutColumn<R>[] => {
81
+ const visible = state.ordered.filter((column: ResolvedColumn<R>) => isVisible(column))
82
+ const pinnedStart = visible.filter((column: ResolvedColumn<R>) => pinOf(column) === "start")
83
+ const pinnedEnd = visible.filter((column: ResolvedColumn<R>) => pinOf(column) === "end")
84
+ const free = visible.filter((column: ResolvedColumn<R>) => !pinOf(column))
85
+ const arranged = [...pinnedStart, ...free, ...pinnedEnd]
86
+
87
+ let startOffset = 0
88
+ const offsets = new Map<string, number>()
89
+ for (const column of pinnedStart) {
90
+ offsets.set(column.id, startOffset)
91
+ startOffset += widthOf(column)
92
+ }
93
+ let endOffset = 0
94
+ for (const column of [...pinnedEnd].reverse()) {
95
+ offsets.set(column.id, endOffset)
96
+ endOffset += widthOf(column)
97
+ }
98
+
99
+ return arranged.map((column, index) => {
100
+ const pinned = pinOf(column)
101
+ return {
102
+ column,
103
+ index,
104
+ width: widthOf(column),
105
+ track: trackOf(column),
106
+ pinned,
107
+ offset: offsets.get(column.id) ?? 0,
108
+ pinEdge:
109
+ (pinned === "start" && column === pinnedStart[pinnedStart.length - 1]) ||
110
+ (pinned === "end" && column === pinnedEnd[0]),
111
+ resizable: column.def.resizable ?? options.resizable?.() ?? false,
112
+ movable: column.def.movable ?? options.reorderable?.() ?? false,
113
+ pinnable: column.def.pinnable ?? options.pinnable?.() ?? false,
114
+ }
115
+ })
116
+ }),
117
+
118
+ hiddenColumns: computed((): ResolvedColumn<R>[] =>
119
+ state.ordered.filter((column: ResolvedColumn<R>) => !isVisible(column))
120
+ ),
121
+ })
122
+
123
+ function isVisible(column: ResolvedColumn<R>): boolean {
124
+ return state.visibility[column.id] ?? !column.def.hidden
125
+ }
126
+
127
+ function pinOf(column: ResolvedColumn<R>): PinSide | false {
128
+ return state.pinning[column.id] ?? column.def.pinned ?? false
129
+ }
130
+
131
+ /** Effektive Breite: gezogen > gemessen > definiert > Default. */
132
+ function widthOf(column: ResolvedColumn<R>): number {
133
+ const width = state.widths[column.id] ?? state.measured[column.id] ?? column.def.width ?? DEFAULT_WIDTH
134
+ return clampWidth(column, width)
135
+ }
136
+
137
+ function trackOf(column: ResolvedColumn<R>): string {
138
+ // Eine vom Nutzer gezogene Breite gewinnt IMMER — auch gegen flex, sonst
139
+ // ließe sich eine flex-Spalte nicht von Hand skalieren.
140
+ const dragged = state.widths[column.id]
141
+ if (dragged !== undefined) return `${clampWidth(column, dragged)}px`
142
+ const fixed = column.def.width
143
+ if (fixed !== undefined && !column.def.flex) return `${clampWidth(column, fixed)}px`
144
+ const min = column.def.minWidth ?? DEFAULT_MIN_WIDTH
145
+ const flex = column.def.flex ?? 1
146
+ const max = column.def.maxWidth
147
+ const upper = max ? `${max}px` : `${flex}fr`
148
+ return `minmax(${min}px, ${upper})`
149
+ }
150
+
151
+ function setMeasured(id: string, width: number): void {
152
+ if (Math.abs((state.measured[id] ?? 0) - width) < 0.5) return
153
+ state.measured[id] = width
154
+ }
155
+
156
+ // ── Sichtbarkeit ────────────────────────────────────────────────────
157
+ function toggleColumn(id: string, visible?: boolean, silent = false): void {
158
+ const column = options.columns().find((item) => item.id === id)
159
+ if (!column) return
160
+ const next = visible ?? !isVisible(column)
161
+ state.visibility[id] = next
162
+ if (!silent) options.onVisibilityChange?.(id, next)
163
+ }
164
+
165
+ // ── Pinning ─────────────────────────────────────────────────────────
166
+ function pinColumn(id: string, side: PinSide | false, silent = false): void {
167
+ // Ohne feste Breite die aktuelle Breite einfrieren — sonst springt die
168
+ // Spalte beim Pinnen, weil der Sticky-Abstand eine px-Breite braucht.
169
+ const column = options.columns().find((item) => item.id === id)
170
+ if (column && side && state.widths[id] === undefined && column.def.width === undefined) {
171
+ state.widths[id] = widthOf(column)
172
+ }
173
+ state.pinning[id] = side
174
+ if (!silent) options.onPin?.(id, side)
175
+ }
176
+
177
+ function getColumnPin(id: string): PinSide | false {
178
+ const column = options.columns().find((item) => item.id === id)
179
+ return column ? pinOf(column) : false
180
+ }
181
+
182
+ // ── Reihenfolge ─────────────────────────────────────────────────────
183
+ function currentOrder(): string[] {
184
+ return state.ordered.map((column: ResolvedColumn<R>) => column.id)
185
+ }
186
+
187
+ function moveColumn(id: string, targetId: string, finished = true): void {
188
+ if (id === targetId) return
189
+ const order = currentOrder()
190
+ const from = order.indexOf(id)
191
+ const to = order.indexOf(targetId)
192
+ if (from === -1 || to === -1) return
193
+ order.splice(to, 0, ...order.splice(from, 1))
194
+ state.order = order
195
+ options.onMove?.(order, finished)
196
+ }
197
+
198
+ /** Spalte um `delta` Positionen verschieben (Tastatur: Shift + ←/→). */
199
+ function shiftColumn(id: string, delta: number): void {
200
+ const visible = state.visibleColumns.map((item: LayoutColumn<R>) => item.column.id)
201
+ const index = visible.indexOf(id)
202
+ const target = visible[index + delta]
203
+ if (target) moveColumn(id, target, true)
204
+ }
205
+
206
+ // ── Resize ──────────────────────────────────────────────────────────
207
+ function startResize(id: string, clientX: number): void {
208
+ const column = options.columns().find((item) => item.id === id)
209
+ if (!column) return
210
+ state.resizing = { id, startX: clientX, startWidth: widthOf(column) }
211
+ }
212
+
213
+ function moveResize(clientX: number): void {
214
+ const active = state.resizing
215
+ if (!active) return
216
+ const column = options.columns().find((item) => item.id === active.id)
217
+ if (!column) return
218
+ const width = clampWidth(column, active.startWidth + (clientX - active.startX))
219
+ state.widths[active.id] = width
220
+ options.onResize?.(active.id, width, false)
221
+ }
222
+
223
+ function endResize(): void {
224
+ const active = state.resizing
225
+ state.resizing = null
226
+ if (active) options.onResize?.(active.id, state.widths[active.id] ?? 0, true)
227
+ }
228
+
229
+ function setWidth(id: string, width: number): void {
230
+ const column = options.columns().find((item) => item.id === id)
231
+ if (!column) return
232
+ state.widths[id] = clampWidth(column, width)
233
+ }
234
+
235
+ // ── Drag & Drop (Reorder) ───────────────────────────────────────────
236
+ function startDrag(id: string, clientX: number, clientY: number): void {
237
+ state.dragging = { id, overId: null, x: clientX, y: clientY }
238
+ }
239
+
240
+ function dragOver(id: string, clientX: number, clientY: number): void {
241
+ if (!state.dragging) return
242
+ state.dragging.overId = id
243
+ state.dragging.x = clientX
244
+ state.dragging.y = clientY
245
+ }
246
+
247
+ function endDrag(): void {
248
+ const active = state.dragging
249
+ state.dragging = null
250
+ if (active?.overId && active.overId !== active.id) moveColumn(active.id, active.overId, true)
251
+ }
252
+
253
+ // ── Zurücksetzen / Zustand ──────────────────────────────────────────
254
+ function resetColumns(): void {
255
+ state.order = []
256
+ state.widths = {}
257
+ state.visibility = {}
258
+ state.pinning = {}
259
+ }
260
+
261
+ /** Spalten-ids, die es nicht mehr gibt, aus dem Zustand entfernen. */
262
+ function pruneUnknown(): void {
263
+ const ids = new Set(options.columns().map((column) => column.id))
264
+ state.order = state.order.filter((id) => ids.has(id))
265
+ for (const map of [state.widths, state.visibility, state.pinning, state.measured] as Record<string, unknown>[]) {
266
+ for (const id of Object.keys(map)) if (!ids.has(id)) delete map[id]
267
+ }
268
+ }
269
+
270
+ return {
271
+ state,
272
+ isVisible,
273
+ pinOf,
274
+ widthOf,
275
+ setMeasured,
276
+ toggleColumn,
277
+ pinColumn,
278
+ getColumnPin,
279
+ currentOrder,
280
+ moveColumn,
281
+ shiftColumn,
282
+ startResize,
283
+ moveResize,
284
+ endResize,
285
+ setWidth,
286
+ startDrag,
287
+ dragOver,
288
+ endDrag,
289
+ resetColumns,
290
+ pruneUnknown,
291
+ }
292
+ }
293
+
294
+ export type ColumnLayout<R extends Row = Row> = ReturnType<typeof useColumnLayout<R>>