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,496 @@
1
+ <template>
2
+ <div class="vi-browser">
3
+ <header class="vi-browser-head">
4
+ <wa-select
5
+ v-if="rootNodes.length > 1"
6
+ class="vi-browser-roots"
7
+ :value="String(activeRoot ?? '')"
8
+ size="small"
9
+ @change="onRootChange($event.target.value)"
10
+ >
11
+ <wa-option
12
+ v-for="root in rootNodes"
13
+ :key="root.key"
14
+ :value="String(root.key)"
15
+ >
16
+ {{ root.name }}
17
+ </wa-option>
18
+ </wa-select>
19
+
20
+ <nav
21
+ class="vi-browser-breadcrumb"
22
+ :aria-label="t('browser.path')"
23
+ >
24
+ <template
25
+ v-for="(entry, index) in breadcrumb"
26
+ :key="entry.key"
27
+ >
28
+ <span
29
+ v-if="index"
30
+ class="vi-browser-crumb-sep"
31
+ aria-hidden="true"
32
+ >
33
+ /
34
+ </span>
35
+ <button
36
+ class="vi-browser-crumb"
37
+ type="button"
38
+ :aria-current="index === breadcrumb.length - 1 ? 'location' : undefined"
39
+ @click="navigateTo(entry.key)"
40
+ >
41
+ {{ entry.name }}
42
+ </button>
43
+ </template>
44
+ </nav>
45
+
46
+ <div class="vi-browser-actions">
47
+ <slot name="actions"></slot>
48
+ </div>
49
+ </header>
50
+
51
+ <wa-split-panel
52
+ class="vi-browser-body"
53
+ primary="start"
54
+ position-in-pixels="260"
55
+ snap="0px 260px"
56
+ >
57
+ <aside
58
+ slot="start"
59
+ class="vi-browser-tree"
60
+ >
61
+ <TreeNav
62
+ ref="treeEl"
63
+ :model-value="location"
64
+ :roots="treeRoots"
65
+ :load-children="loadNodeChildren"
66
+ container-id="tree"
67
+ :label="label"
68
+ @update:model-value="navigateTo($event)"
69
+ @children-loaded="onTreeChildren"
70
+ />
71
+ </aside>
72
+
73
+ <main
74
+ slot="end"
75
+ ref="listEl"
76
+ class="vi-browser-list"
77
+ :class="{ 'is-drop-target': listIsDropTarget }"
78
+ @dragenter="onFileDragEnter"
79
+ @dragover="onFileDragOver"
80
+ @dragleave="onFileDragLeave"
81
+ @drop="onFileDrop"
82
+ >
83
+ <DataTable
84
+ ref="tableEl"
85
+ v-model:selected-keys="selectedKeys"
86
+ :data="visibleRows"
87
+ :columns="columns"
88
+ :row-key="rowKey"
89
+ :selectable="selectable"
90
+ :order-field="orderField"
91
+ :loading="loading"
92
+ drag-source="list"
93
+ :label="label"
94
+ :has-more="hasMore"
95
+ appearance="plain"
96
+ v-bind="tableProps"
97
+ @row-dblclick="onRowDoubleClick"
98
+ @row-reorder="$emit('reorder', $event)"
99
+ @load-more="$emit('load-more', $event)"
100
+ @page-change="$emit('page-change', $event)"
101
+ >
102
+ <template
103
+ v-for="(_, slotName) in $slots"
104
+ #[slotName]="scope"
105
+ >
106
+ <slot
107
+ :name="slotName"
108
+ v-bind="scope"
109
+ ></slot>
110
+ </template>
111
+ </DataTable>
112
+
113
+ <div
114
+ v-if="uploads && fileDragDepth > 0"
115
+ class="vi-browser-dropzone"
116
+ >
117
+ <wa-icon name="upload"></wa-icon>
118
+ <span>{{ uploadHint ?? t("browser.uploadHint") }}</span>
119
+ </div>
120
+ </main>
121
+ </wa-split-panel>
122
+ </div>
123
+ </template>
124
+
125
+ <script setup>
126
+ import { useI18n } from "vue-i18n"
127
+ import { ref, computed, provide, watch } from "vue"
128
+ import { DataTable } from "../DataTable"
129
+ import TreeNav from "./TreeNav.vue"
130
+ import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession"
131
+
132
+ // --------------------------------------------------------------------------- //
133
+ // Browser — Baum zur Navigation + DataTable als Liste //
134
+ // --------------------------------------------------------------------------- //
135
+ //
136
+ // GENERISCHER Kern: kennt kein ViUR. Die App (oder ViurBrowser.vue) liefert
137
+ // Rootnodes, den Inhalt des aktuellen Ortes (`rows`, Ordner zuerst) und einen
138
+ // Loader für die Baum-Ebenen — und bekommt Absichten als Events zurück:
139
+ // navigate — anderer Ort gewählt (Baum, Breadcrumb, Doppelklick auf Ordner)
140
+ // open — Doppelklick auf einen Eintrag (Leaf)
141
+ // move — Drag & Drop zwischen Liste und Baum bzw. in eine Ordnerzeile
142
+ // reorder — Umsortieren innerhalb der Liste (DataTable row-reorder, 1:1)
143
+ // upload — Dateien aus dem OS in die Liste gezogen
144
+ // Der Browser lädt selbst nichts und verschiebt nur die ANSICHT — Speichern
145
+ // und Nachladen entscheidet die App (siehe ViurBrowser für die ViUR-Variante).
146
+
147
+ const props = defineProps({
148
+ /** Wurzeln/Repositories: { key, name, icon? } */
149
+ rootNodes: { type: Array, default: () => [] },
150
+ /** Inhalt des aktuellen Ortes — Ordner zuerst (siehe rowKind). */
151
+ rows: { type: Array, default: () => [] },
152
+ columns: { type: Array, default: () => [] },
153
+ rowKey: { type: String, default: "key" },
154
+ /** Ordnet eine Zeile ein: "node" (Ordner) oder "leaf". */
155
+ rowKind: { type: Function, default: (row) => (row?.skelType === "node" ? "node" : "leaf") },
156
+ /** Kinder eines Baumknotens: (node) => Promise<BrowserNode[]> */
157
+ loadNodeChildren: { type: Function, default: null },
158
+ orderField: { type: String, default: "sortindex" },
159
+ selectable: { type: [String, Boolean], default: "multiple" },
160
+ loading: { type: Boolean, default: false },
161
+ /** OS-Dateien annehmen (nur Event `upload`, kein Upload-Code hier). */
162
+ uploads: { type: Boolean, default: false },
163
+ uploadHint: { type: [String, null], default: null },
164
+ label: { type: String, default: "Browser" },
165
+ /** Cursor-Nachladen: null = aus, sonst „gibt es mehr?" (DataTable-Leiste). */
166
+ hasMore: { type: [Boolean, null], default: null },
167
+ /** Weitere DataTable-Props (withSearch, size, …). */
168
+ tableProps: { type: Object, default: () => ({}) },
169
+ })
170
+
171
+ const location = defineModel("location", { type: [String, Number, null], default: null })
172
+ const selectedKeys = defineModel("selectedKeys", { type: Array, default: () => [] })
173
+ const emit = defineEmits(["navigate", "open", "move", "reorder", "upload", "load-more", "page-change"])
174
+
175
+ const { t } = useI18n()
176
+
177
+ const treeEl = ref(null)
178
+ const tableEl = ref(null)
179
+ const listEl = ref(null)
180
+
181
+ // ── Rootnodes & Ort ───────────────────────────────────────────────────────────
182
+ // Der aktive Root ergibt sich aus dem Ort (Breadcrumb-Wurzel); ohne Ort gilt
183
+ // der erste Rootnode. Ein Wechsel im Select navigiert auf dessen Wurzel.
184
+ const nodeIndex = ref(new Map()) // key → { node, parent }
185
+
186
+ watch(
187
+ () => props.rootNodes,
188
+ (roots) => {
189
+ const next = new Map(nodeIndex.value)
190
+ for (const root of roots ?? []) next.set(String(root.key), { node: root, parent: null })
191
+ nodeIndex.value = next
192
+ if (location.value == null && roots?.length) navigateTo(roots[0].key)
193
+ },
194
+ { immediate: true }
195
+ )
196
+
197
+ const breadcrumb = computed(() => {
198
+ const out = []
199
+ let key = location.value
200
+ const seen = new Set()
201
+ while (key != null && !seen.has(String(key))) {
202
+ seen.add(String(key))
203
+ const entry = nodeIndex.value.get(String(key))
204
+ out.unshift({ key, name: entry?.node?.name ?? String(key) })
205
+ key = entry?.parent ?? null
206
+ }
207
+ return out
208
+ })
209
+
210
+ const activeRoot = computed(() => breadcrumb.value[0]?.key ?? props.rootNodes[0]?.key ?? null)
211
+ const treeRoots = computed(() => {
212
+ if (props.rootNodes.length <= 1) return props.rootNodes
213
+ const active = props.rootNodes.find((root) => String(root.key) === String(activeRoot.value))
214
+ return active ? [active] : props.rootNodes
215
+ })
216
+
217
+ function rememberNodes(nodes, parent) {
218
+ const next = new Map(nodeIndex.value)
219
+ for (const node of nodes ?? []) next.set(String(node.key), { node, parent })
220
+ nodeIndex.value = next
221
+ }
222
+
223
+ function onTreeChildren({ node, children }) {
224
+ rememberNodes(children, node.key)
225
+ }
226
+
227
+ function navigateTo(key) {
228
+ if (key === location.value) return
229
+ location.value = key
230
+ emit("navigate", { key, node: nodeIndex.value.get(String(key))?.node ?? null })
231
+ }
232
+
233
+ function onRootChange(value) {
234
+ const root = props.rootNodes.find((candidate) => String(candidate.key) === value)
235
+ if (root) navigateTo(root.key)
236
+ }
237
+
238
+ function onRowDoubleClick({ row, key }) {
239
+ if (props.rowKind(row) === "node") {
240
+ // Ordner aus der Liste merken, damit der Breadcrumb seinen Namen kennt.
241
+ rememberNodes([{ key, name: rowName(row), data: row }], location.value)
242
+ navigateTo(key)
243
+ } else {
244
+ emit("open", { row, key })
245
+ }
246
+ }
247
+
248
+ function rowName(row) {
249
+ const value = row?.name
250
+ return typeof value === "string" ? value : (value?.name ?? String(row?.[props.rowKey] ?? ""))
251
+ }
252
+
253
+ // ── Lokale Sicht auf die Liste ────────────────────────────────────────────────
254
+ // Ein Zug ist rein clientseitig: Zeilen, die den aktuellen Ort VERLASSEN,
255
+ // werden ausgeblendet; Knoten, die aus dem Baum HIERHER gezogen wurden, werden
256
+ // eingefügt. `rows` (die Daten der App) bleiben unangetastet — neue Daten oder
257
+ // ein Ortswechsel verwerfen die Überlagerung.
258
+ const movedAway = ref(new Set())
259
+ const movedIn = ref([])
260
+
261
+ const visibleRows = computed(() => {
262
+ let out = props.rows
263
+ if (movedAway.value.size) {
264
+ out = out.filter((row) => !movedAway.value.has(String(row[props.rowKey])))
265
+ }
266
+ if (movedIn.value.length) {
267
+ const present = new Set(out.map((row) => String(row[props.rowKey])))
268
+ out = [...out, ...movedIn.value.filter((row) => !present.has(String(row[props.rowKey])))]
269
+ }
270
+ return out
271
+ })
272
+
273
+ function resetLocalMoves() {
274
+ if (movedAway.value.size) movedAway.value = new Set()
275
+ if (movedIn.value.length) movedIn.value = []
276
+ }
277
+ watch(() => props.rows, resetLocalMoves)
278
+ watch(location, resetLocalMoves)
279
+
280
+ // ── Drag-Session (Liste ↔ Baum) ───────────────────────────────────────────────
281
+ const session = createDragSession((payload, target) => {
282
+ // In sich selbst oder den eigenen Teilbaum fällt nichts: Ziel verwerfen.
283
+ if (payload.keys.some((key) => String(key) === String(target.key))) return
284
+ let parent = target.key != null ? nodeIndex.value.get(String(target.key))?.parent : null
285
+ const seen = new Set()
286
+ while (parent != null && !seen.has(String(parent))) {
287
+ if (payload.keys.some((key) => String(key) === String(parent))) return
288
+ seen.add(String(parent))
289
+ parent = nodeIndex.value.get(String(parent))?.parent ?? null
290
+ }
291
+
292
+ // Ansicht sofort nachziehen — ohne Reload.
293
+ const targetIsCurrent = location.value != null && String(target.key) === String(location.value)
294
+ if (targetIsCurrent) {
295
+ const away = new Set(movedAway.value)
296
+ for (const key of payload.keys) away.delete(String(key))
297
+ movedAway.value = away
298
+ if (payload.source === "tree") {
299
+ // Baumknoten in die offene Ebene gezogen: als Ordnerzeile einfügen.
300
+ const rows = payload.rows.map((row) => row?.data ?? row).filter(Boolean)
301
+ movedIn.value = [...movedIn.value, ...rows]
302
+ }
303
+ } else {
304
+ movedAway.value = new Set([...movedAway.value, ...payload.keys.map((key) => String(key))])
305
+ movedIn.value = movedIn.value.filter(
306
+ (row) => !payload.keys.some((key) => String(key) === String(row[props.rowKey]))
307
+ )
308
+ // Was den Ort verlassen hat, ist auch nicht mehr ausgewählt.
309
+ selectedKeys.value = selectedKeys.value.filter(
310
+ (selected) => !payload.keys.some((key) => String(key) === String(selected))
311
+ )
312
+ }
313
+
314
+ // Auch der BAUM zieht lokal nach: verschobene ORDNER wechseln (oder
315
+ // verlassen) ihre geladene Ebene — ohne Reload. Blätter tauchen im
316
+ // Navigations-Baum nicht auf und brauchen nichts.
317
+ for (const row of payload.rows) {
318
+ const raw = row?.data ?? row
319
+ const isNode = payload.source === "tree" || props.rowKind(raw) === "node"
320
+ if (!isNode) continue
321
+ const key = raw?.[props.rowKey] ?? row?.key
322
+ const node = { key, name: nodeIndex.value.get(String(key))?.node?.name ?? rowName(raw), data: raw }
323
+ treeEl.value?.moveNode?.(node, target.key)
324
+ rememberNodes([node], target.key) // Breadcrumb-Elternkette aktuell halten
325
+ }
326
+
327
+ emit("move", {
328
+ keys: payload.keys,
329
+ rows: payload.rows,
330
+ target: target.key,
331
+ mode: target.mode,
332
+ source: payload.source === "tree" ? "tree" : "list",
333
+ })
334
+ })
335
+ provide(DRAG_SESSION, session)
336
+
337
+ // Die Liste ist selbst Drop-Ziel — in beide Richtungen:
338
+ // • Die MITTE einer Ordnerzeile heißt „hinein"; die Ränder (davor/dahinter)
339
+ // überlässt sie der internen Umsortierung der Tabelle.
340
+ // • Kommt der Zug aus dem BAUM, ist auch der Rest der Liste (Leaf-Zeilen,
341
+ // freie Fläche) ein Ziel: „in den gerade geöffneten Ordner".
342
+ session.register({
343
+ id: "list",
344
+ el: () => listEl.value,
345
+ targetFor: (hit, _x, y) => {
346
+ const fromTree = session.state.payload?.source === "tree"
347
+ const intoCurrent =
348
+ fromTree && location.value != null ? { container: "list", key: location.value, mode: "inside" } : null
349
+ const rowEl = hit.closest?.("[data-key]")
350
+ if (!rowEl) return intoCurrent
351
+ const row = visibleRows.value.find((candidate) => String(candidate[props.rowKey]) === rowEl.dataset.key)
352
+ if (!row || props.rowKind(row) !== "node") return intoCurrent
353
+ if (dropModeFor(rowEl.getBoundingClientRect(), y) !== "inside") return intoCurrent
354
+ return { container: "list", key: row[props.rowKey], mode: "inside" }
355
+ },
356
+ })
357
+
358
+ /** Zug aus dem Baum zielt auf die offene Ebene → die ganze Liste markieren. */
359
+ const listIsDropTarget = computed(
360
+ () =>
361
+ session.state.over?.container === "list" &&
362
+ location.value != null &&
363
+ String(session.state.over?.key) === String(location.value)
364
+ )
365
+
366
+ // ── Upload-Dropzone (native, nur für OS-Dateien) ─────────────────────────────
367
+ const fileDragDepth = ref(0)
368
+ const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files")
369
+ function onFileDragEnter(event) {
370
+ if (!props.uploads || !hasFiles(event)) return
371
+ event.preventDefault()
372
+ fileDragDepth.value++
373
+ }
374
+ function onFileDragOver(event) {
375
+ if (!props.uploads || !hasFiles(event)) return
376
+ event.preventDefault()
377
+ }
378
+ function onFileDragLeave(event) {
379
+ if (!props.uploads || !hasFiles(event)) return
380
+ fileDragDepth.value = Math.max(0, fileDragDepth.value - 1)
381
+ }
382
+ function onFileDrop(event) {
383
+ if (!props.uploads || !hasFiles(event)) return
384
+ event.preventDefault()
385
+ fileDragDepth.value = 0
386
+ const files = Array.from(event.dataTransfer?.files ?? [])
387
+ if (files.length) emit("upload", { files, target: location.value })
388
+ }
389
+
390
+ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
391
+ </script>
392
+
393
+ <style scoped>
394
+ .vi-browser {
395
+ display: flex;
396
+ flex-direction: column;
397
+ min-block-size: 0;
398
+ block-size: 100%;
399
+ /* Gerahmt wie das Listen-Widget (DataTable `appearance="outlined"`): EIN
400
+ Kasten um Baum und Liste. Die Tabelle darin bleibt deshalb rahmenlos
401
+ (appearance="plain" unten), sonst stünden zwei Rahmen ineinander. */
402
+ background-color: var(--wa-color-surface-default, #fff);
403
+ border: var(--wa-border-width-s, 1px) solid var(--wa-color-surface-border, #ddd);
404
+ border-radius: var(--wa-border-radius-m, 0.5rem);
405
+ overflow: hidden;
406
+ /* Eine Textgröße für den ganzen Kasten — dieselbe wie in der Tabelle
407
+ (.vi-dt), damit Breadcrumb und Baum nicht größer wirken als die Liste. */
408
+ font-size: var(--wa-font-size-s, 0.875rem);
409
+ }
410
+ .vi-browser-head {
411
+ display: flex;
412
+ align-items: center;
413
+ gap: var(--wa-space-s, 0.5rem);
414
+ padding: var(--wa-space-2xs, 0.25rem) var(--wa-space-xs, 0.5rem);
415
+ border-block-end: 1px solid var(--wa-color-surface-border, #ddd);
416
+ /* Abgesetzt wie der Kopf der Tabelle (--dt-header-background). */
417
+ background-color: var(--wa-color-surface-lowered, #f4f4f5);
418
+ }
419
+ .vi-browser-roots {
420
+ min-inline-size: 10rem;
421
+ }
422
+ .vi-browser-breadcrumb {
423
+ display: flex;
424
+ align-items: center;
425
+ gap: var(--wa-space-2xs, 0.25rem);
426
+ overflow: hidden;
427
+ flex: 1;
428
+ }
429
+ .vi-browser-crumb {
430
+ border: none;
431
+ background: none;
432
+ padding: var(--wa-space-2xs, 0.25rem);
433
+ border-radius: var(--wa-border-radius-s, 0.25rem);
434
+ cursor: pointer;
435
+ color: inherit;
436
+ white-space: nowrap;
437
+ }
438
+ .vi-browser-crumb:hover {
439
+ background: var(--wa-color-neutral-fill-quiet, rgba(0 0 0 / 0.05));
440
+ }
441
+ .vi-browser-crumb[aria-current="location"] {
442
+ font-weight: 600;
443
+ }
444
+ .vi-browser-crumb-sep {
445
+ color: var(--wa-color-text-quiet, #888);
446
+ }
447
+ .vi-browser-body {
448
+ flex: 1;
449
+ min-block-size: 0;
450
+ /* <wa-split-panel> IST das Grid (display: grid am Host). Seine Zeile ist von
451
+ sich aus inhaltshoch und läuft damit über die feste Höhe hinaus — die
452
+ Tabelle wäre dann so hoch wie ihre Zeilen und würde die Seite verlängern
453
+ statt in sich zu scrollen. Eine Zeile über die volle Höhe bindet die
454
+ Panels an die Fläche. Gilt für die waagerechte Aufteilung, die hier fest
455
+ eingestellt ist. */
456
+ grid-template-rows: 100%;
457
+ /* Seitenleiste nicht kleiner als 10rem, Liste behält mindestens die Hälfte */
458
+ --min: 10rem;
459
+ --max: 50%;
460
+ }
461
+ .vi-browser-tree {
462
+ block-size: 100%;
463
+ overflow: auto;
464
+ padding: var(--wa-space-2xs, 0.25rem);
465
+ }
466
+ .vi-browser-list {
467
+ block-size: 100%;
468
+ position: relative;
469
+ min-inline-size: 0;
470
+ min-block-size: 0;
471
+ display: flex;
472
+ flex-direction: column;
473
+ }
474
+ .vi-browser-list > :deep(.vi-dt) {
475
+ flex: 1;
476
+ min-block-size: 0;
477
+ }
478
+ .vi-browser-list.is-drop-target {
479
+ outline: 2px dashed var(--wa-color-brand-50, #2563eb);
480
+ outline-offset: -2px;
481
+ }
482
+ .vi-browser-dropzone {
483
+ position: absolute;
484
+ inset: 0;
485
+ display: flex;
486
+ flex-direction: column;
487
+ align-items: center;
488
+ justify-content: center;
489
+ gap: var(--wa-space-s, 0.5rem);
490
+ background: color-mix(in oklab, var(--wa-color-surface-default, #fff) 80%, transparent);
491
+ border: 2px dashed var(--wa-color-brand-50, #2563eb);
492
+ border-radius: var(--wa-border-radius-m, 0.5rem);
493
+ pointer-events: none;
494
+ font-weight: 600;
495
+ }
496
+ </style>