viur-ui 0.0.1 → 0.0.3

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 (98) hide show
  1. package/README.md +67 -41
  2. package/index.d.ts +18 -9
  3. package/index.js +6 -3
  4. package/package.json +6 -6
  5. package/src/components/Browser/Browser.vue +58 -58
  6. package/src/components/Browser/TreeNav.vue +32 -32
  7. package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
  8. package/src/components/Browser/ViurBrowser.vue +47 -44
  9. package/src/components/Browser/dragSession.ts +27 -27
  10. package/src/components/Browser/index.ts +3 -3
  11. package/src/components/Browser/types.ts +9 -9
  12. package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
  13. package/src/components/DataTable/DataTable.vue +359 -351
  14. package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
  15. package/src/components/DataTable/ViurDataTable.vue +51 -50
  16. package/src/components/DataTable/columns.ts +30 -30
  17. package/src/components/DataTable/index.ts +12 -12
  18. package/src/components/DataTable/rows.ts +71 -72
  19. package/src/components/DataTable/types.ts +55 -55
  20. package/src/components/DataTable/useColumnLayout.ts +28 -28
  21. package/src/components/DataTable/useGridNavigation.ts +74 -15
  22. package/src/components/DataTable/useVirtualRows.ts +10 -10
  23. package/src/components/DataTable/viurColumns.ts +69 -69
  24. package/src/forms/ViForm.d.vue.ts +32 -32
  25. package/src/forms/ViForm.vue +17 -16
  26. package/src/forms/ViFormCategory.vue +3 -1
  27. package/src/forms/bones/Bone.vue +125 -108
  28. package/src/forms/bones/BoneActions.vue +21 -21
  29. package/src/forms/bones/BoneLabel.vue +1 -1
  30. package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
  31. package/src/forms/bones/BoneNestedWrapper.vue +3 -3
  32. package/src/forms/bones/BoneSingleEntry.vue +2 -2
  33. package/src/forms/bones/FilterBone.vue +3 -3
  34. package/src/forms/bones/actions/ActionDialog.vue +2 -2
  35. package/src/forms/bones/actions/Debug.vue +6 -6
  36. package/src/forms/bones/actions/Delete.vue +2 -2
  37. package/src/forms/bones/actions/DescribeImage.vue +4 -3
  38. package/src/forms/bones/actions/Download.vue +4 -4
  39. package/src/forms/bones/actions/Placeholder.vue +2 -2
  40. package/src/forms/bones/actions/index.js +72 -74
  41. package/src/forms/bones/bone.d.ts +19 -19
  42. package/src/forms/bones/uploadAdapter.js +3 -3
  43. package/src/forms/bones/utils.js +40 -41
  44. package/src/forms/bones/view/base_item.vue +1 -1
  45. package/src/forms/bones/view/boneLogic.js +1 -1
  46. package/src/forms/bones/widgets/booleanBone.vue +19 -19
  47. package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
  48. package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
  49. package/src/forms/bones/widgets/codeBone.vue +22 -22
  50. package/src/forms/bones/widgets/colorBone.vue +18 -18
  51. package/src/forms/bones/widgets/dateBone.vue +22 -22
  52. package/src/forms/bones/widgets/emailBone.vue +20 -20
  53. package/src/forms/bones/widgets/fileBone.vue +32 -31
  54. package/src/forms/bones/widgets/keyBone.vue +21 -21
  55. package/src/forms/bones/widgets/numericBone.vue +21 -21
  56. package/src/forms/bones/widgets/passwordBone.vue +34 -31
  57. package/src/forms/bones/widgets/phoneBone.vue +20 -20
  58. package/src/forms/bones/widgets/rawBone.vue +19 -19
  59. package/src/forms/bones/widgets/recordBone.vue +16 -16
  60. package/src/forms/bones/widgets/relationalBone.vue +26 -26
  61. package/src/forms/bones/widgets/selectBone.vue +23 -23
  62. package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
  63. package/src/forms/bones/widgets/spatialBone.vue +21 -21
  64. package/src/forms/bones/widgets/stringBone.vue +27 -27
  65. package/src/forms/bones/widgets/textBone.vue +28 -26
  66. package/src/forms/install.d.ts +1 -1
  67. package/src/forms/install.js +7 -0
  68. package/src/forms/layouts/LayloutList2Columns.vue +1 -1
  69. package/src/forms/layouts/LayoutCategory.vue +1 -1
  70. package/src/forms/layouts/LayoutList.vue +1 -1
  71. package/src/forms/utils.js +22 -22
  72. package/src/request/configStore.js +4 -4
  73. package/src/request/envelope.js +6 -6
  74. package/src/request/request.js +211 -121
  75. package/src/translations/de.js +31 -0
  76. package/src/translations/en.js +31 -0
  77. package/src/user/LoginScreen.d.vue.ts +24 -24
  78. package/src/user/LoginScreen.vue +6 -7
  79. package/src/user/store.d.ts +16 -16
  80. package/src/vite/safeHtml.js +73 -0
  81. package/src/vite/sanitizeVHtml.d.ts +4 -0
  82. package/src/vite/sanitizeVHtml.js +15 -0
  83. package/src/webawesomeCompat.css +0 -40
  84. package/src/public/assets/icons/fontawesome-manifest.json +0 -8
  85. package/src/public/assets/icons/regular/file.svg +0 -1
  86. package/src/public/assets/icons/solid/arrows-left-right.svg +0 -1
  87. package/src/public/assets/icons/solid/bug.svg +0 -1
  88. package/src/public/assets/icons/solid/check.svg +0 -1
  89. package/src/public/assets/icons/solid/circle-info.svg +0 -1
  90. package/src/public/assets/icons/solid/compress.svg +0 -1
  91. package/src/public/assets/icons/solid/download.svg +0 -1
  92. package/src/public/assets/icons/solid/expand.svg +0 -1
  93. package/src/public/assets/icons/solid/grip-vertical.svg +0 -1
  94. package/src/public/assets/icons/solid/plus.svg +0 -1
  95. package/src/public/assets/icons/solid/question.svg +0 -1
  96. package/src/public/assets/icons/solid/triangle-exclamation.svg +0 -1
  97. package/src/public/assets/icons/solid/upload.svg +0 -1
  98. package/src/public/assets/icons/solid/xmark.svg +0 -1
package/README.md CHANGED
@@ -1,79 +1,105 @@
1
1
  # viur-ui
2
2
 
3
- Official UI Components for Vue 3.
3
+ Vue 3 components and request helpers for [ViUR](https://www.viur.dev).
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
7
  ```bash
8
8
  npm install viur-ui
9
9
  ```
10
10
 
11
- **Requires:** Vue 3.3+, Vite with `@vitejs/plugin-vue`
11
+ Peers: `vue` 3.3+, `pinia`, `vue-i18n`, `@viur/webawesome`, `@vueuse/core`, `vue-router`; for code/text bones also `codemirror`, `@codemirror/language-data`, `ckeditor5`, `@ckeditor/ckeditor5-vue`.
12
12
 
13
- ## Development
14
-
15
- ### Histoire (Component Stories)
13
+ ## Vite
16
14
 
17
- ```bash
18
- # Neue Bones (WebAwesome) — http://localhost:6006
19
- npm run story:dev
15
+ ```js
16
+ import vue from "@vitejs/plugin-vue"
20
17
 
21
- # Legacy-Vergleich: Alt (Shoelace) vs. Neu (WebAwesome) — http://localhost:6007
22
- npm run histoire:legacy
18
+ export default {
19
+ plugins: [vue()],
20
+ optimizeDeps: {
21
+ exclude: ["viur-ui"], // ships raw SFCs; pre-bundling duplicates module state in dev
22
+ },
23
+ }
23
24
  ```
24
25
 
25
- ### Tests
26
+ Env: `VITE_API_URL`, `VITE_DEFAULT_RENDERER` (`json` | `vi`).
26
27
 
27
- ```bash
28
- # Unit-Tests (Vitest, watch mode)
29
- npm run test
28
+ ## Vitest
30
29
 
31
- # Unit-Tests einmalig ausführen
32
- npm run test:run
33
-
34
- # Contract Tests (Playwright gegen laufenden Histoire-Server auf :6007)
35
- npm run test:contract
30
+ ```js
31
+ test: {
32
+ server: { deps: { inline: ["viur-ui"] } }, // package .js files import .vue — Node can't load those
33
+ }
36
34
  ```
37
35
 
38
- > Die Contract Tests starten den Legacy-Histoire-Server automatisch, falls er noch nicht läuft.
36
+ ## Setup
39
37
 
40
- **Ersteinrichtung Playwright:** Nach `npm install` oder nach einem Playwright-Update müssen die Browser einmalig installiert werden:
38
+ ```js
39
+ import "viur-ui/src/webawesomeConfig.js"
40
+ import { deTranslations, enTranslations } from "viur-ui"
41
+ import { installViurForms } from "viur-ui/src/forms/install.js"
41
42
 
42
- ```bash
43
- npx playwright install chromium
43
+ app.use(createPinia())
44
+ app.use(i18n)
45
+ installViurForms(app) // also registers v-safe-html (DOMPurify) and $sanitizeHtml
44
46
  ```
45
47
 
46
- ## Vite Configuration
48
+ Translations: merge the package messages before your app's own (app keys win):
49
+
50
+ ```js
51
+ i18n.global.mergeLocaleMessage("de", deTranslations)
52
+ i18n.global.mergeLocaleMessage("en", enTranslations)
53
+ ```
47
54
 
48
- Add to your `vite.config.js` so Vite processes the `.vue` files from this package:
55
+ `installViurForms` registers these global names (async), skipping names that are already taken: `ViForm`, `vi-form`, `vi-form-category`, `Bone`, `bone`, `bone-label`, `bone-actions`, `bone-single-entry`, `bone-multiple-list`, `bone-multiple-wrapper`, `bone-nested-wrapper`. The package imports its wrappers locally, so running alongside `@viur/vue-utils` (e.g. a global `Bone` from there) doesn't affect viur-ui forms — but your own templates resolve whichever component owns the name.
56
+
57
+ ## Safe HTML (recommended)
58
+
59
+ Route every `v-html` through DOMPurify at compile time — your templates and viur-ui's, without changes:
49
60
 
50
61
  ```js
51
- import vue from '@vitejs/plugin-vue'
62
+ import vue from "@vitejs/plugin-vue"
63
+ import { sanitizeVHtml } from "viur-ui/src/vite/sanitizeVHtml.js"
52
64
 
53
65
  export default {
54
- plugins: [vue()],
55
- optimizeDeps: {
56
- exclude: ['viur-ui']
57
- }
66
+ plugins: [vue({ template: { compilerOptions: { directiveTransforms: { html: sanitizeVHtml } } } })],
58
67
  }
59
68
  ```
60
69
 
61
- ## Usage
70
+ `installViurForms(app)` provides the runtime sanitizer (`window.__sanitizeHtml`); call it before mounting. Without it, rendering throws rather than outputting unsanitized HTML. `v-safe-html` is then redundant.
62
71
 
63
- ### Named import via index
72
+ Not covered: SSR, `innerHTML` in JS, `h("div", { innerHTML })` — use `sanitizeHtml()` there. Don't decode HTML entities before sanitizing.
64
73
 
65
- ```js
66
- import { HelloWorld } from 'viur-ui'
74
+ ## ViForm
75
+
76
+ ```vue
77
+ <ViForm module="user" action="edit" :skelkey="key" />
67
78
  ```
68
79
 
69
- ### Direct path import
80
+ Props, state and exposed API: [`src/forms/ViForm.d.vue.ts`](./src/forms/ViForm.d.vue.ts).
81
+
82
+ ## API
70
83
 
71
84
  ```js
72
- import HelloWorld from 'viur-ui/src/components/HelloWorld.vue'
85
+ import { useListFetch, useViewFetch, useEditFetch, useConfigStore, notify } from "viur-ui"
86
+ ```
87
+
88
+ Full list: [`index.js`](./index.js), types: [`index.d.ts`](./index.d.ts).
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ npm install && npm install --prefix example
94
+ npm run story:dev # Histoire, :6006
95
+ npm run test:run # Vitest
96
+ npm run lint
73
97
  ```
74
98
 
75
- ## Components
99
+ ## Release
100
+
101
+ ```bash
102
+ npm version patch && git push --follow-tags
103
+ ```
76
104
 
77
- | Component | Description |
78
- |-----------|-------------|
79
- | `HelloWorld` | Renders "Hello World" |
105
+ Published by [`release.yml`](./.github/workflows/release.yml) via npm Trusted Publishing. `-alpha` / `-beta` / `-rc` → matching dist-tag, else `latest`.
package/index.d.ts CHANGED
@@ -31,8 +31,8 @@ export interface ListFetchOptions {
31
31
  immediate?: boolean
32
32
  cached?: boolean
33
33
  ttl?: number
34
- /** Zusatz-Header der LIST-Requests (nicht der Struktur) — Objekt oder
35
- * Funktion, pro Request ausgewertet (z.B. X-VIUR-BONELIST). */
34
+ /** Extra headers for the LIST requests (not the structure) — object or
35
+ * function, evaluated per request (e.g. X-VIUR-BONELIST). */
36
36
  headers?: Record<string, string> | (() => Record<string, string> | null) | null
37
37
  }
38
38
 
@@ -83,9 +83,9 @@ type ControllerFetchFn = (
83
83
  ) => UseFetchReturn<any>
84
84
 
85
85
  /**
86
- * Eigene Abfrage gegen einen Endpunkt eines Moduls bauen — die Grundlage der
87
- * `use*Fetch`-Helfer. `ctx` nimmt u. a. `renderer`, `version`, `query`
88
- * (zusätzlicher Query-String), `headers`, `immediate`.
86
+ * Build a custom query against an endpoint of a module — the basis of the
87
+ * `use*Fetch` helpers. `ctx` accepts, among others, `renderer`, `version`, `query`
88
+ * (additional query string), `headers`, `immediate`.
89
89
  */
90
90
  export declare function createControllerFetch(options?: {
91
91
  endpoint?: string
@@ -97,9 +97,9 @@ export declare function createControllerFetch(options?: {
97
97
 
98
98
  export declare const useViewFetch: ControllerFetchFn
99
99
  export declare const useStructureFetch: ControllerFetchFn
100
- /** Envelope v2 auspacken (v1-Antworten kommen unveraendert zurueck). */
100
+ /** Unwrap envelope v2 (v1 responses are returned unchanged). */
101
101
  export declare function normalizeEnvelope(body: unknown): unknown
102
- /** Skel-Struktur glaetten — akzeptiert Dict- wie Paar-Listen-Form. */
102
+ /** Flatten a skel structure — accepts both dict and pair-list form. */
103
103
  export declare function normalizeStructure(structure: unknown): Record<string, Record<string, unknown>>
104
104
  export declare const useAddFetch: ControllerFetchFn
105
105
  export declare const useEditFetch: ControllerFetchFn
@@ -345,7 +345,16 @@ export declare function skelDataToFormData(
345
345
 
346
346
  // ─── Translations ─────────────────────────────────────────────────────────────
347
347
 
348
- /** Message-Bäume der Paket-Komponenten — in die vue-i18n-Messages der App mergen
349
- * (vue-i18n ist Voraussetzung; die Komponenten übersetzen mit t() ohne Fallbacks). */
348
+ /** Message trees of the package components — merge into the app's vue-i18n messages
349
+ * (vue-i18n is required; the components translate with t() without fallbacks). */
350
350
  export declare const deTranslations: Record<string, any>
351
351
  export declare const enTranslations: Record<string, any>
352
+
353
+ // ─── Safe HTML ────────────────────────────────────────────────────────────────
354
+
355
+ /** Sanitizes an HTML string with DOMPurify (scripts, event handlers and javascript: URLs removed). */
356
+ export declare function sanitizeHtml(value: unknown): string
357
+ /** Vue plugin: registers the `v-safe-html` directive and `$sanitizeHtml`. */
358
+ export declare const safeHtml: import("vue").Plugin
359
+ /** The `v-safe-html` directive for local registration (`import { vSafeHtml }` in `<script setup>`). */
360
+ export declare const vSafeHtml: import("vue").ObjectDirective<HTMLElement, unknown>
package/index.js CHANGED
@@ -37,10 +37,13 @@ export {
37
37
  // Theme (brand color from the configuration)
38
38
  export { useColorStore, hexToHsl, BRAND_LIGHTNESS } from "./src/theme/colorStore.js"
39
39
 
40
- // Translations (Komponenten dieses Pakets; in die vue-i18n-Instanz der App mergen —
41
- // vue-i18n ist Voraussetzung, die Komponenten übersetzen mit t() ohne Fallbacks)
40
+ // Translations (components of this package; merge into the app's vue-i18n instance —
41
+ // vue-i18n is required, the components translate with t() without fallbacks)
42
42
  export { default as deTranslations } from "./src/translations/de.js"
43
43
  export { default as enTranslations } from "./src/translations/en.js"
44
44
 
45
- // Anzeige von Bone-Werten (vormals aus dem Legacy-Paket)
45
+ // Display of bone values
46
46
  export { default as boneLogic } from "./src/forms/bones/view/boneLogic.js"
47
+
48
+ // Safe HTML rendering (DOMPurify): v-safe-html directive + sanitizeHtml()
49
+ export { default as safeHtml, sanitizeHtml, vSafeHtml } from "./src/vite/safeHtml.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viur-ui",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/viur-framework/viur-ui"
@@ -14,7 +14,9 @@
14
14
  "index.d.ts"
15
15
  ],
16
16
  "dependencies": {
17
+ "@vue/compiler-dom": "^3.3.0",
17
18
  "destr": "^2.0.5",
19
+ "dompurify": "^3.4.16",
18
20
  "json-editor-vue": "^0.18.1",
19
21
  "libphonenumber-js": "^1.12.41",
20
22
  "logics-js": "^0.4.1",
@@ -24,7 +26,6 @@
24
26
  "peerDependencies": {
25
27
  "@ckeditor/ckeditor5-vue": "^7.3.0",
26
28
  "@codemirror/language-data": "^6.5.2",
27
- "@viur/shoelace": ">=1.0.0-0",
28
29
  "@viur/webawesome": ">=0.10.0",
29
30
  "@vueuse/core": "^12.4.0",
30
31
  "ckeditor5": "^45.2.0",
@@ -35,20 +36,17 @@
35
36
  "vue-router": "^4.5.0"
36
37
  },
37
38
  "scripts": {
38
- "icons": "npx wa-icons . ./src/public/assets/icons",
39
+ "icons": "npx wa-icons . ./stories/public/webawesome/assets/icons",
39
40
  "test": "vitest",
40
41
  "test:run": "vitest run",
41
42
  "story:dev": "histoire dev",
42
43
  "story:build": "histoire build",
43
- "histoire:legacy": "histoire dev --config histoire.legacy.config.js",
44
- "test:contract": "playwright test --config tests/contract/playwright.config.ts",
45
44
  "lint": "eslint .",
46
45
  "format:check": "prettier --check .",
47
46
  "prepare": "lefthook install"
48
47
  },
49
48
  "devDependencies": {
50
49
  "@histoire/plugin-vue": "^1.0.0-beta.1",
51
- "@playwright/test": "^1.59.1",
52
50
  "@vitejs/plugin-vue": "^6.0.6",
53
51
  "@vueuse/core": "^14.2.1",
54
52
  "eslint": "^10.2.1",
@@ -61,6 +59,8 @@
61
59
  "histoire": "^1.0.0-beta.1",
62
60
  "lefthook": "^2.1.6",
63
61
  "prettier": "^3.8.3",
62
+ "typescript": "^6.0.3",
63
+ "typescript-eslint": "^8.71.0",
64
64
  "vite": "^7.3.0",
65
65
  "vitest": "^4.1.4",
66
66
  "vue": "^3.5.32",
@@ -125,46 +125,46 @@
125
125
  <script setup>
126
126
  import { useI18n } from "vue-i18n"
127
127
  import { ref, computed, provide, watch } from "vue"
128
- import { DataTable } from "../DataTable"
128
+ import { DataTable } from "../DataTable/index.ts"
129
129
  import TreeNav from "./TreeNav.vue"
130
- import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession"
130
+ import { createDragSession, dropModeFor, DRAG_SESSION } from "./dragSession.ts"
131
131
 
132
132
  // --------------------------------------------------------------------------- //
133
- // Browser — Baum zur Navigation + DataTable als Liste //
133
+ // Browser — tree for navigation + DataTable as list //
134
134
  // --------------------------------------------------------------------------- //
135
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).
136
+ // GENERIC core: knows nothing about ViUR. The app (or ViurBrowser.vue) supplies
137
+ // rootnodes, the contents of the current location (`rows`, folders first) and a
138
+ // loader for the tree levels — and gets intents back as events:
139
+ // navigate — another location chosen (tree, breadcrumb, double-click on folder)
140
+ // open — double-click on an entry (leaf)
141
+ // move — drag & drop between list and tree, or into a folder row
142
+ // reorder — reordering within the list (DataTable row-reorder, 1:1)
143
+ // upload — files dragged from the OS into the list
144
+ // The browser loads nothing itself and only moves the VIEW — saving and
145
+ // reloading are up to the app (see ViurBrowser for the ViUR variant).
146
146
 
147
147
  const props = defineProps({
148
- /** Wurzeln/Repositories: { key, name, icon? } */
148
+ /** Roots/repositories: { key, name, icon? } */
149
149
  rootNodes: { type: Array, default: () => [] },
150
- /** Inhalt des aktuellen Ortes — Ordner zuerst (siehe rowKind). */
150
+ /** Contents of the current location — folders first (see rowKind). */
151
151
  rows: { type: Array, default: () => [] },
152
152
  columns: { type: Array, default: () => [] },
153
153
  rowKey: { type: String, default: "key" },
154
- /** Ordnet eine Zeile ein: "node" (Ordner) oder "leaf". */
154
+ /** Classifies a row: "node" (folder) or "leaf". */
155
155
  rowKind: { type: Function, default: (row) => (row?.skelType === "node" ? "node" : "leaf") },
156
- /** Kinder eines Baumknotens: (node) => Promise<BrowserNode[]> */
156
+ /** Children of a tree node: (node) => Promise<BrowserNode[]> */
157
157
  loadNodeChildren: { type: Function, default: null },
158
158
  orderField: { type: String, default: "sortindex" },
159
159
  selectable: { type: [String, Boolean], default: "multiple" },
160
160
  loading: { type: Boolean, default: false },
161
- /** OS-Dateien annehmen (nur Event `upload`, kein Upload-Code hier). */
161
+ /** Accept OS files (only the `upload` event, no upload code here). */
162
162
  uploads: { type: Boolean, default: false },
163
163
  uploadHint: { type: [String, null], default: null },
164
164
  label: { type: String, default: "Browser" },
165
- /** Cursor-Nachladen: null = aus, sonst „gibt es mehr?" (DataTable-Leiste). */
165
+ /** Cursor loading: null = off, otherwise "is there more?" (DataTable bar). */
166
166
  hasMore: { type: [Boolean, null], default: null },
167
- /** Weitere DataTable-Props (withSearch, size, …). */
167
+ /** Further DataTable props (withSearch, size, …). */
168
168
  tableProps: { type: Object, default: () => ({}) },
169
169
  })
170
170
 
@@ -178,9 +178,9 @@ const treeEl = ref(null)
178
178
  const tableEl = ref(null)
179
179
  const listEl = ref(null)
180
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.
181
+ // ── Rootnodes & location ──────────────────────────────────────────────────────
182
+ // The active root follows from the location (breadcrumb root); without a
183
+ // location the first rootnode applies. A change in the select navigates to its root.
184
184
  const nodeIndex = ref(new Map()) // key → { node, parent }
185
185
 
186
186
  watch(
@@ -237,7 +237,7 @@ function onRootChange(value) {
237
237
 
238
238
  function onRowDoubleClick({ row, key }) {
239
239
  if (props.rowKind(row) === "node") {
240
- // Ordner aus der Liste merken, damit der Breadcrumb seinen Namen kennt.
240
+ // Remember the folder from the list so the breadcrumb knows its name.
241
241
  rememberNodes([{ key, name: rowName(row), data: row }], location.value)
242
242
  navigateTo(key)
243
243
  } else {
@@ -250,11 +250,11 @@ function rowName(row) {
250
250
  return typeof value === "string" ? value : (value?.name ?? String(row?.[props.rowKey] ?? ""))
251
251
  }
252
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.
253
+ // ── Local view of the list ────────────────────────────────────────────────────
254
+ // A drag is purely client-side: rows that LEAVE the current location are
255
+ // hidden; nodes dragged HERE from the tree are inserted. `rows` (the app's
256
+ // data) stay untouched — new data or a change of location discards the
257
+ // overlay.
258
258
  const movedAway = ref(new Set())
259
259
  const movedIn = ref([])
260
260
 
@@ -277,9 +277,9 @@ function resetLocalMoves() {
277
277
  watch(() => props.rows, resetLocalMoves)
278
278
  watch(location, resetLocalMoves)
279
279
 
280
- // ── Drag-Session (Liste ↔ Baum) ───────────────────────────────────────────────
280
+ // ── Drag session (list ↔ tree) ────────────────────────────────────────────────
281
281
  const session = createDragSession((payload, target) => {
282
- // In sich selbst oder den eigenen Teilbaum fällt nichts: Ziel verwerfen.
282
+ // Nothing drops into itself or its own subtree: discard the target.
283
283
  if (payload.keys.some((key) => String(key) === String(target.key))) return
284
284
  let parent = target.key != null ? nodeIndex.value.get(String(target.key))?.parent : null
285
285
  const seen = new Set()
@@ -289,14 +289,14 @@ const session = createDragSession((payload, target) => {
289
289
  parent = nodeIndex.value.get(String(parent))?.parent ?? null
290
290
  }
291
291
 
292
- // Ansicht sofort nachziehen — ohne Reload.
292
+ // Update the view immediately — without reload.
293
293
  const targetIsCurrent = location.value != null && String(target.key) === String(location.value)
294
294
  if (targetIsCurrent) {
295
295
  const away = new Set(movedAway.value)
296
296
  for (const key of payload.keys) away.delete(String(key))
297
297
  movedAway.value = away
298
298
  if (payload.source === "tree") {
299
- // Baumknoten in die offene Ebene gezogen: als Ordnerzeile einfügen.
299
+ // Tree node dragged into the open level: insert as a folder row.
300
300
  const rows = payload.rows.map((row) => row?.data ?? row).filter(Boolean)
301
301
  movedIn.value = [...movedIn.value, ...rows]
302
302
  }
@@ -305,15 +305,15 @@ const session = createDragSession((payload, target) => {
305
305
  movedIn.value = movedIn.value.filter(
306
306
  (row) => !payload.keys.some((key) => String(key) === String(row[props.rowKey]))
307
307
  )
308
- // Was den Ort verlassen hat, ist auch nicht mehr ausgewählt.
308
+ // Whatever left the location is no longer selected either.
309
309
  selectedKeys.value = selectedKeys.value.filter(
310
310
  (selected) => !payload.keys.some((key) => String(key) === String(selected))
311
311
  )
312
312
  }
313
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.
314
+ // The TREE also updates locally: moved FOLDERS change (or leave) their
315
+ // loaded level — without reload. Leaves don't appear in the navigation
316
+ // tree and need nothing.
317
317
  for (const row of payload.rows) {
318
318
  const raw = row?.data ?? row
319
319
  const isNode = payload.source === "tree" || props.rowKind(raw) === "node"
@@ -321,7 +321,7 @@ const session = createDragSession((payload, target) => {
321
321
  const key = raw?.[props.rowKey] ?? row?.key
322
322
  const node = { key, name: nodeIndex.value.get(String(key))?.node?.name ?? rowName(raw), data: raw }
323
323
  treeEl.value?.moveNode?.(node, target.key)
324
- rememberNodes([node], target.key) // Breadcrumb-Elternkette aktuell halten
324
+ rememberNodes([node], target.key) // keep the breadcrumb parent chain up to date
325
325
  }
326
326
 
327
327
  emit("move", {
@@ -334,11 +334,11 @@ const session = createDragSession((payload, target) => {
334
334
  })
335
335
  provide(DRAG_SESSION, session)
336
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".
337
+ // The list itself is a drop target — in both directions:
338
+ // • The MIDDLE of a folder row means "into"; the edges (before/after) are
339
+ // left to the table's internal reordering.
340
+ // • If the drag comes from the TREE, the rest of the list (leaf rows,
341
+ // empty area) is a target too: "into the currently open folder".
342
342
  session.register({
343
343
  id: "list",
344
344
  el: () => listEl.value,
@@ -355,7 +355,7 @@ session.register({
355
355
  },
356
356
  })
357
357
 
358
- /** Zug aus dem Baum zielt auf die offene Ebene → die ganze Liste markieren. */
358
+ /** Drag from the tree targets the open level → highlight the whole list. */
359
359
  const listIsDropTarget = computed(
360
360
  () =>
361
361
  session.state.over?.container === "list" &&
@@ -363,7 +363,7 @@ const listIsDropTarget = computed(
363
363
  String(session.state.over?.key) === String(location.value)
364
364
  )
365
365
 
366
- // ── Upload-Dropzone (native, nur für OS-Dateien) ─────────────────────────────
366
+ // ── Upload dropzone (native, only for OS files) ──────────────────────────────
367
367
  const fileDragDepth = ref(0)
368
368
  const hasFiles = (event) => Array.from(event.dataTransfer?.types ?? []).includes("Files")
369
369
  function onFileDragEnter(event) {
@@ -396,15 +396,15 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
396
396
  flex-direction: column;
397
397
  min-block-size: 0;
398
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. */
399
+ /* Framed like the list widget (DataTable `appearance="outlined"`): ONE
400
+ box around tree and list. The table inside therefore stays frameless
401
+ (appearance="plain" below), otherwise there would be two nested frames. */
402
402
  background-color: var(--wa-color-surface-default, #fff);
403
403
  border: var(--wa-border-width-s, 1px) solid var(--wa-color-surface-border, #ddd);
404
404
  border-radius: var(--wa-border-radius-m, 0.5rem);
405
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. */
406
+ /* One text size for the whole box — the same as in the table
407
+ (.vi-dt), so breadcrumb and tree don't look larger than the list. */
408
408
  font-size: var(--wa-font-size-s, 0.875rem);
409
409
  }
410
410
  .vi-browser-head {
@@ -413,7 +413,7 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
413
413
  gap: var(--wa-space-s, 0.5rem);
414
414
  padding: var(--wa-space-2xs, 0.25rem) var(--wa-space-xs, 0.5rem);
415
415
  border-block-end: 1px solid var(--wa-color-surface-border, #ddd);
416
- /* Abgesetzt wie der Kopf der Tabelle (--dt-header-background). */
416
+ /* Set off like the table header (--dt-header-background). */
417
417
  background-color: var(--wa-color-surface-lowered, #f4f4f5);
418
418
  }
419
419
  .vi-browser-roots {
@@ -447,14 +447,14 @@ defineExpose({ table: tableEl, tree: treeEl, session, navigateTo })
447
447
  .vi-browser-body {
448
448
  flex: 1;
449
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. */
450
+ /* <wa-split-panel> IS the grid (display: grid on the host). Its row is
451
+ content-sized by itself and thus overflows the fixed height — the
452
+ table would then be as tall as its rows and lengthen the page instead
453
+ of scrolling within itself. A single full-height row binds the panels
454
+ to the area. Applies to the horizontal split, which is fixed
455
+ here. */
456
456
  grid-template-rows: 100%;
457
- /* Seitenleiste nicht kleiner als 10rem, Liste behält mindestens die Hälfte */
457
+ /* Sidebar no smaller than 10rem, list keeps at least half */
458
458
  --min: 10rem;
459
459
  --max: 50%;
460
460
  }