viur-ui 0.0.1 → 0.0.2

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 (83) 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 +3 -5
  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 +346 -346
  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 +15 -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 +22 -22
  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
@@ -64,7 +64,7 @@ export function v1SelectionFollow(body) {
64
64
  if (!String(bone?.["type"] ?? "").startsWith("select")) continue
65
65
  const chosen = values?.[name]
66
66
  if (typeof chosen !== "string" || !/^(\/|https?:\/\/)/.test(chosen)) continue
67
- // Nur wenn der Wert tatsächlich eine der angebotenen Ziel-URLs ist.
67
+ // Only if the value actually is one of the offered target URLs.
68
68
  if (bone?.["values"] && Object.prototype.hasOwnProperty.call(bone["values"], chosen)) return chosen
69
69
  }
70
70
  return null
@@ -190,11 +190,11 @@ export function normalizeEnvelope(data) {
190
190
  out["action"] = action
191
191
  out["status"] = status
192
192
  out["errors"] = normalizeErrors(data["errors"])
193
- // Ziel des Schritts. v1 legt es je nach Stufe anderswo ab:
194
- // next_url – vom Server gesetzt (z. B. Auswahl des zweiten Faktors)
195
- // params.action_url – bei einem gerenderten Schritt, der selbst ein Ziel
196
- // mitbringt (viur-core: der OTP-Schritt des zweiten
197
- // Faktors kommt als `edit` mit action_url)
193
+ // Target of the step. v1 puts it in different places depending on the stage:
194
+ // next_url – set by the server (e.g. choice of the second factor)
195
+ // params.action_url – for a rendered step that brings its own target
196
+ // (viur-core: the OTP step of the second factor
197
+ // comes as `edit` with action_url)
198
198
  out["follow"] = data["follow"] ?? data["next_url"] ?? data["params"]?.["action_url"] ?? null
199
199
  out["structure"] = data["structure"] ?? null
200
200
 
@@ -126,14 +126,14 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
126
126
  }
127
127
 
128
128
  /**
129
- * Struktur der Liste holen.
129
+ * Fetch the structure of the list.
130
130
  *
131
- * Zwei Antwortformen sind unterwegs:
132
- * • aktuell: `/<modul>/structure` rendert wie jede andere Aktion, also
131
+ * Two response shapes are in circulation:
132
+ * • current: `/<modul>/structure` renders like any other action, i.e.
133
133
  * `{ action: "structure.view", values, structure }`
134
- * • historisch (getStructure): `{ viewSkel: {…}, editSkel: {…} }`
135
- * Zusätzlich kann die Struktur eine PAAR-LISTE sein
136
- * (`json.bone.structure.keytuples`) — normalizeStructure glättet das.
134
+ * • historic (getStructure): `{ viewSkel: {…}, editSkel: {…} }`
135
+ * Additionally, the structure can be a PAIR LIST
136
+ * (`json.bone.structure.keytuples`) — normalizeStructure flattens that.
137
137
  */
138
138
  async function _fetchStructure() {
139
139
  const skeltype =
@@ -188,9 +188,9 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
188
188
  await _fetchStructure()
189
189
  }
190
190
  const list = responseData.data ?? []
191
- // Manche Installationen liefern die Struktur in der Liste mit
192
- // (Kompatibilitäts-Flag `json.bone.structure.inlists`) — dann ist der
193
- // separate Aufruf oben umsonst gewesen, aber diese hier ist aktueller.
191
+ // Some installations deliver the structure along with the list
192
+ // (compatibility flag `json.bone.structure.inlists`) — then the
193
+ // separate call above was in vain, but this one is more current.
194
194
  if (responseData.structure) {
195
195
  structure.value = normalizeStructure(responseData.structure)
196
196
  }
@@ -218,8 +218,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
218
218
 
219
219
  const { isFetching, isFinished, error, execute: _execute, abort } = useViurFetch(listUrl, { immediate }).json()
220
220
 
221
- /** Alles geladen: letzte Antwort da und kein Cursor mehr offen. Ab hier
222
- * dürfen Sortierung/Filter lokal auf dem Datensatz laufen. */
221
+ /** Everything loaded: last response is in and no cursor left open. From here
222
+ * on, sorting/filtering may run locally on the dataset. */
223
223
  const complete = computed(() => isFinished.value && !cursor.value)
224
224
 
225
225
  const fetchResult = {
@@ -262,8 +262,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
262
262
  return executeAndThrow(fetchResult, _execute)
263
263
  }
264
264
 
265
- /** Eine Seite nachladen; erkennt das ViUR-Ende (leere Seite mit derselben
266
- * Cursor-Marke) und löscht dann den Cursor. Liefert true bei Fortschritt. */
265
+ /** Load one more page; detects the ViUR end (empty page with the same
266
+ * cursor marker) and then clears the cursor. Returns true on progress. */
267
267
  async function _advance() {
268
268
  if (!cursor.value) return false
269
269
  const before = data.value.length
@@ -279,8 +279,8 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
279
279
  return true
280
280
  }
281
281
 
282
- /** Die nächsten `pages` Seiten laden (Standard: eine) — z. B. direkt als
283
- * Handler der DataTable-Cursor-Leiste: `@load-more="({ pages }) =>
282
+ /** Load the next `pages` pages (default: one) — e.g. directly as the
283
+ * handler of the DataTable cursor bar: `@load-more="({ pages }) =>
284
284
  * pages === 'all' ? list.nextAll() : list.next(pages)"`. */
285
285
  async function next(pages = 1) {
286
286
  let remaining = pages
@@ -289,10 +289,10 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
289
289
  }
290
290
  }
291
291
 
292
- /** Alle restlichen Seiten laden ("Alles laden"). */
292
+ /** Load all remaining pages ("load everything"). */
293
293
  async function nextAll() {
294
294
  while (await _advance()) {
295
- /* bis zum Ende */
295
+ /* until the end */
296
296
  }
297
297
  }
298
298
 
@@ -352,7 +352,7 @@ export function useTreeFetch(
352
352
  const isFinished = computed(() => nodes.isFinished.value && leaves.isFinished.value)
353
353
  const error = computed(() => nodes.error.value ?? leaves.error.value)
354
354
  const hasMore = computed(() => !!(nodes.cursor.value || leaves.cursor.value))
355
- /** Beide Listen vollständig geladen — Sortierung/Filter dürfen lokal laufen. */
355
+ /** Both lists fully loaded — sorting/filtering may run locally. */
356
356
  const complete = computed(() => isFinished.value && !hasMore.value)
357
357
 
358
358
  function _both(fn) {
@@ -363,9 +363,9 @@ export function useTreeFetch(
363
363
  return _both((l) => l.execute())
364
364
  }
365
365
 
366
- // Eine Seite nachladen: erst Nodes zu Ende, dann Leaves. Das ViUR-Ende
367
- // (leere Seite mit derselben Cursor-Marke) erkennt useListFetch.next()
368
- // selbst und löscht dann seinen Cursor — hier zählt nur der Fortschritt.
366
+ // Load one more page: nodes to the end first, then leaves. useListFetch.next()
367
+ // detects the ViUR end (empty page with the same cursor marker) itself and
368
+ // then clears its cursor — only progress counts here.
369
369
  async function _nextOf(list) {
370
370
  const before = list.data.value.length
371
371
  await list.next()
@@ -377,7 +377,7 @@ export function useTreeFetch(
377
377
  if (leaves.cursor.value) await _nextOf(leaves)
378
378
  }
379
379
 
380
- /** Alle restlichen Seiten beider Listen laden ("Alles laden"). */
380
+ /** Load all remaining pages of both lists ("load everything"). */
381
381
  async function nextAll() {
382
382
  while (nodes.cursor.value) {
383
383
  if (!(await _nextOf(nodes))) break
@@ -12,6 +12,7 @@ export default {
12
12
  list: "Auflisten",
13
13
  addEmpty: "Neuer Eintrag",
14
14
  no_entries: "Keine Einträge",
15
+ errors_summary: "Fehler",
15
16
  },
16
17
  lang: "Sprache",
17
18
  bones: {
@@ -27,6 +28,13 @@ export default {
27
28
  phone: {
28
29
  placeholder: "Telefon/Fax",
29
30
  },
31
+ password: {
32
+ mismatch: "Die eingegebenen Passwörter stimmen nicht überein.",
33
+ empty: "Das eingegebene Passwort ist leer.",
34
+ },
35
+ text: {
36
+ openInNewWindow: "Link in neuem Fenster öffnen",
37
+ },
30
38
  },
31
39
 
32
40
  errors: {
@@ -77,6 +85,10 @@ export default {
77
85
  collapseRow: "Zeile einklappen",
78
86
  expandRow: "Zeile ausklappen",
79
87
  noData: "Keine Daten",
88
+ noValues: "Keine Werte",
89
+ clear: "Leeren",
90
+ filterEquals: "genau …",
91
+ filterContains: "enthält …",
80
92
  noResults: "Keine Treffer",
81
93
  filterFrom: "von",
82
94
  filterTo: "bis",
@@ -108,6 +120,7 @@ export default {
108
120
  path: "Pfad",
109
121
  navigation: "Navigation",
110
122
  uploadHint: "Dateien hier ablegen",
123
+ name: "Name",
111
124
  },
112
125
  form: {
113
126
  next: "Weiter",
@@ -122,6 +135,7 @@ export default {
122
135
  original: "Original Sprache ({lang}):",
123
136
  target: "Zielsprache ({lang}):",
124
137
  run: "Übersetzen",
138
+ name: "Übersetzen",
125
139
  applyNext: "Übernehmen und nächste leere Sprache",
126
140
  apply: "Ziel für {lang} übernehmen",
127
141
  },
@@ -130,6 +144,23 @@ export default {
130
144
  instructions: "weitere Anweisungen",
131
145
  run: "Beschreibung erstellen",
132
146
  apply: "Beschreibung übernehmen",
147
+ name: "Bildbeschreibung erstellen",
148
+ title: "Bild beschreiben",
149
+ },
150
+ debug: {
151
+ name: "Debug",
152
+ title: "Bone Debug-Informationen anzeigen",
153
+ structure: "Bone-Struktur",
154
+ value: "Bone-Wert",
155
+ state: "Bone-Status",
156
+ context: "Kontext",
157
+ },
158
+ placeholder: {
159
+ name: "Platzhalter",
160
+ title: "Platzhalter (TODO: entfernen)",
161
+ },
162
+ delete: {
163
+ name: "Löschen",
133
164
  },
134
165
  },
135
166
  messages: {
@@ -12,6 +12,7 @@ export default {
12
12
  list: "List",
13
13
  addEmpty: "New entry",
14
14
  no_entries: "No entries",
15
+ errors_summary: "Errors",
15
16
  },
16
17
  lang: "Language",
17
18
  bones: {
@@ -27,6 +28,13 @@ export default {
27
28
  phone: {
28
29
  placeholder: "Phone/Fax",
29
30
  },
31
+ password: {
32
+ mismatch: "The passwords do not match.",
33
+ empty: "The password is empty.",
34
+ },
35
+ text: {
36
+ openInNewWindow: "Open link in new window",
37
+ },
30
38
  },
31
39
 
32
40
  errors: {
@@ -77,6 +85,10 @@ export default {
77
85
  collapseRow: "Collapse row",
78
86
  expandRow: "Expand row",
79
87
  noData: "No data",
88
+ noValues: "No values",
89
+ clear: "Clear",
90
+ filterEquals: "exactly …",
91
+ filterContains: "contains …",
80
92
  noResults: "No matches",
81
93
  filterFrom: "from",
82
94
  filterTo: "to",
@@ -108,6 +120,7 @@ export default {
108
120
  path: "Path",
109
121
  navigation: "Navigation",
110
122
  uploadHint: "Drop files here",
123
+ name: "Name",
111
124
  },
112
125
  form: {
113
126
  next: "Next",
@@ -121,6 +134,7 @@ export default {
121
134
  original: "Source language ({lang}):",
122
135
  target: "Target language ({lang}):",
123
136
  run: "Translate",
137
+ name: "Translate",
124
138
  applyNext: "Apply and go to next empty language",
125
139
  apply: "Apply target for {lang}",
126
140
  },
@@ -129,6 +143,23 @@ export default {
129
143
  instructions: "additional instructions",
130
144
  run: "Generate description",
131
145
  apply: "Apply description",
146
+ name: "Generate image description",
147
+ title: "Describe image",
148
+ },
149
+ debug: {
150
+ name: "Debug",
151
+ title: "Show bone debug information",
152
+ structure: "Bone structure",
153
+ value: "Bone value",
154
+ state: "Bone state",
155
+ context: "Context",
156
+ },
157
+ placeholder: {
158
+ name: "Placeholder",
159
+ title: "Placeholder (TODO: remove)",
160
+ },
161
+ delete: {
162
+ name: "Delete",
132
163
  },
133
164
  },
134
165
  messages: {
@@ -1,52 +1,52 @@
1
1
  import type { DefineComponent } from "vue"
2
2
 
3
- /** Props von LoginScreen.vue (siehe defineProps dort). */
3
+ /** Props of LoginScreen.vue (see defineProps there). */
4
4
  export interface LoginScreenProps {
5
- /** Überschrift der Karte; per `header`-Slot ersetzbar. */
5
+ /** Heading of the card; replaceable via the `header` slot. */
6
6
  title?: string
7
7
  /**
8
- * Endpunkt unterhalb des `user`-Moduls, der den Ablauf startet. Envelope v2
9
- * fährt die ganze Kette ab "login". viur-core 3.9 (klassisches v1) antwortet
10
- * auf "login" mit der Provider-Auswahl; die Kette führt der LoginScreen dann
11
- * selbst wie vi-vue-utils (Passwort, Passwort vergessen, zweiter Faktor).
8
+ * Endpoint below the `user` module that starts the flow. Envelope v2
9
+ * runs the whole chain from "login". viur-core 3.9 (classic v1) answers
10
+ * "login" with the provider choice; the LoginScreen then drives the chain
11
+ * itself like vi-vue-utils (password, lost password, second factor).
12
12
  */
13
13
  action?: string
14
- /** Renderer für die Requests; Standard: VITE_DEFAULT_RENDERER oder "json". */
14
+ /** Renderer for the requests; default: VITE_DEFAULT_RENDERER or "json". */
15
15
  renderer?: string | null
16
- /** Protokollversion festnageln; null → unversioniert (Server wählt v2). */
16
+ /** Pin the protocol version; null → unversioned (server picks v2). */
17
17
  version?: string | number | null
18
18
  /**
19
- * OAuth-Client-ID für „Sign in with Google". Ohne Angabe wird die vom Server
20
- * per `/vi/config` ("admin.user.google.clientID") gelieferte ID genutzt;
21
- * `false` blendet den Knopf immer aus.
19
+ * OAuth client ID for "Sign in with Google". If omitted, the ID supplied by
20
+ * the server via `/vi/config` ("admin.user.google.clientID") is used;
21
+ * `false` always hides the button.
22
22
  */
23
23
  googleClientId?: string | false | null
24
24
  /**
25
- * Hintergrundbild der App, falls die Konfiguration keines setzt
26
- * ("admin.login.background" leer). Wie dort: absolute URLs bleiben, "/…" ist
27
- * ein Pfad auf dem Backend, alles andere ein Asset der App (unter BASE_URL).
25
+ * Background image of the app, in case the configuration doesn't set one
26
+ * ("admin.login.background" empty). As there: absolute URLs stay, "/…" is
27
+ * a path on the backend, anything else an asset of the app (under BASE_URL).
28
28
  */
29
29
  backgroundImage?: string | null
30
- /** Logo über dem Login, falls "admin.login.logo" leer ist (Auflösung wie oben). */
30
+ /** Logo above the login, in case "admin.login.logo" is empty (resolved as above). */
31
31
  logo?: string | null
32
- /** Fortschrittsleiste des Multistep-Formulars zeigen. */
32
+ /** Show the progress bar of the multistep form. */
33
33
  showSteps?: boolean
34
- /** Beim Mounten eine bestehende Session prüfen und den Login überspringen. */
34
+ /** On mount, check for an existing session and skip the login. */
35
35
  autoStart?: boolean
36
36
  /**
37
- * Ziel nach erfolgreicher Anmeldung (auch wenn `autoStart` eine bestehende
38
- * Session findet). Läuft über vue-router, falls die Anwendung einen hat,
39
- * sonst über `location`. Ohne Angabe bleibt die Ansicht stehen und der
40
- * Aufrufer entscheidet in `@success`.
37
+ * Target after a successful login (also when `autoStart` finds an existing
38
+ * session). Goes through vue-router if the application has one,
39
+ * otherwise through `location`. If omitted, the view stays put and the
40
+ * caller decides in `@success`.
41
41
  */
42
42
  redirect?: string
43
43
  }
44
44
 
45
- /** Ereignisse als `on*`-Props, damit `@success` / `@error` im Template typen. */
45
+ /** Events as `on*` props so that `@success` / `@error` are typed in the template. */
46
46
  export interface LoginScreenEmitProps {
47
- /** Angemeldet — mit den Werten des UserSkel. */
47
+ /** Logged in — with the values of the UserSkel. */
48
48
  onSuccess?: (user: Record<string, any> | null) => void
49
- /** Login fehlgeschlagen (Formularfehler, Transport, "google"). */
49
+ /** Login failed (form error, transport, "google"). */
50
50
  onError?: (error: unknown) => void
51
51
  }
52
52
 
@@ -138,8 +138,7 @@
138
138
  @failed="onFailed"
139
139
  >
140
140
  <!-- Pass the form's own slots through so an app can supply its own
141
- chrome (the default action bar is Shoelace; a WebAwesome app
142
- replaces it here). Only forwarded when the consumer provides the
141
+ chrome (e.g. its own action bar). Only forwarded when the consumer provides the
143
142
  slot, otherwise ViForm keeps its default. -->
144
143
  <template
145
144
  v-if="$slots.actions"
@@ -244,8 +243,8 @@ const emit = defineEmits(["success", "error"])
244
243
  const store = useUserStore()
245
244
  const configStore = useConfigStore()
246
245
  const { t } = useI18n()
247
- // Optional: ohne installierten Router ist das undefined — dann wird per
248
- // location navigiert.
246
+ // Optional: without an installed router this is undefined — navigation then
247
+ // happens via location.
249
248
  const router = useRouter()
250
249
 
251
250
  const state = reactive({
@@ -306,9 +305,9 @@ function onFailed(error) {
306
305
  }
307
306
 
308
307
  /**
309
- * Nach dem Anmelden weiterleiten, wenn `redirect` gesetzt ist. Läuft NACH dem
310
- * success-Emit, damit der Aufrufer den Benutzer noch in seinen eigenen Zustand
311
- * übernehmen kann (Rechte, Persona-Sichtbarkeit), bevor das Ziel rendert.
308
+ * Redirect after login if `redirect` is set. Runs AFTER the success emit so
309
+ * the caller can still take the user into its own state (access rights,
310
+ * persona visibility) before the target renders.
312
311
  */
313
312
  async function goRedirect() {
314
313
  if (!props.redirect) return
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Werte des UserSkel, wie das Backend sie liefert. Nur die Felder, auf die sich
3
- * der Store selbst stützt, sind benannt — der Rest bleibt offen, weil das Skel
4
- * pro Projekt erweitert wird.
2
+ * Values of the UserSkel as the backend delivers them. Only the fields the
3
+ * store itself relies on are named — the rest stays open because the skel
4
+ * is extended per project.
5
5
  */
6
6
  export interface UserSkelValues {
7
7
  key?: string
@@ -16,43 +16,43 @@ export interface UserSkelValues {
16
16
  export type UserStatus = "unknown" | "loading" | "authenticated" | "anonymous"
17
17
 
18
18
  export interface UserStoreState {
19
- /** Angemeldeter Benutzer oder null. */
19
+ /** Logged-in user or null. */
20
20
  user: UserSkelValues | null
21
21
  status: UserStatus
22
- /** Renderer der Requests (Standard: VITE_DEFAULT_RENDERER oder "json"). */
22
+ /** Renderer for the requests (default: VITE_DEFAULT_RENDERER or "json"). */
23
23
  renderer: string
24
24
  googleClientId: string | null
25
25
  }
26
26
 
27
27
  /**
28
- * Der Store, wie ihn `useUserStore()` liefert. Die Getter stehen als Werte da,
29
- * weil Pinia die Refs eines Setup-Stores am Store-Objekt entpackt.
28
+ * The store as returned by `useUserStore()`. The getters appear as values
29
+ * because Pinia unwraps the refs of a setup store on the store object.
30
30
  */
31
31
  export interface UserStore {
32
32
  state: UserStoreState
33
33
  isLoggedIn: boolean
34
- /** Rechte des Benutzers (leer, wenn keiner angemeldet ist). */
34
+ /** Access rights of the user (empty if nobody is logged in). */
35
35
  access: string[]
36
36
  isRoot: boolean
37
37
  isAdmin: boolean
38
- /** Root gilt immer als berechtigt. */
38
+ /** Root always counts as authorized. */
39
39
  hasAccess: (right: string) => boolean
40
- /** Kurzform des Namens für Oberflächen, "-" ohne Benutzer. */
40
+ /** Short form of the name for UIs, "-" without a user. */
41
41
  shortName: string
42
42
  configure: (options?: { renderer?: string; googleClientId?: string | null }) => void
43
- /** Session laden. Ergibt null, wenn niemand angemeldet ist (401). */
43
+ /** Load the session. Yields null if nobody is logged in (401). */
44
44
  fetchSelf: () => Promise<UserSkelValues | null>
45
45
  logout: () => Promise<void>
46
46
  reset: () => void
47
47
  /**
48
- * Klassische Kette (viur-core 3.9): einen Schritt per GET rendern — Endpunkt
49
- * unterhalb von `user` (Standard "login") oder eine Server-URL. Liefert die
50
- * normalisierte Antwort; wirft bei einem Transportfehler.
48
+ * Classic chain (viur-core 3.9): render a step via GET — an endpoint
49
+ * below `user` (default "login") or a server URL. Returns the
50
+ * normalized response; throws on a transport error.
51
51
  */
52
52
  loadLoginStep: (target?: string) => Promise<Record<string, any>>
53
- /** Primären Provider ohne eigenes Widget wählen; `next_url` ist sein erster Schritt. */
53
+ /** Choose a primary provider without a widget of its own; `next_url` is its first step. */
54
54
  selectAuthenticationProvider: (provider: string) => Promise<Record<string, any>>
55
- /** Google-Identity-Token anmelden; liefert die normalisierte Antwort. */
55
+ /** Log in with a Google identity token; returns the normalized response. */
56
56
  loginWithGoogle: (token: string) => Promise<Record<string, any> | null>
57
57
  }
58
58
 
@@ -0,0 +1,73 @@
1
+ import DOMPurify from "dompurify"
2
+
3
+ // Central, safe HTML rendering for the app.
4
+ //
5
+ // Purpose: v-html renders raw HTML and is therefore a stored-XSS sink as soon as
6
+ // the value comes from the backend or user input. This file provides ONE sanitize
7
+ // function, used in two places:
8
+ //
9
+ // 1) window.__sanitizeHtml -> called by the compiled templates.
10
+ // The app's Vue Vite plugin can override the built-in v-html compiler
11
+ // transform (in the app's vite.config.js) so that EVERY v-html runs through
12
+ // it automatically. Existing `v-html` usages become safe without code changes.
13
+ // 2) the `v-safe-html` directive as an explicit, easy-to-spot alias.
14
+ //
15
+ // DOMPurify is a fail-closed allowlist: formatting tags (h1-h6, br, p, strong, em,
16
+ // ul/ol/li, a[href], table, ...) are kept and stay functional; <script>, event
17
+ // handlers (onerror/onclick/...) and javascript: URLs are removed.
18
+ //
19
+ // IMPORTANT: do NOT run values through helpers that decode HTML entities (e.g. a
20
+ // `cleanString()`) beforehand — that turns neutralized payloads live again. Plain
21
+ // text output needs no HTML at all -> use {{ text }} / v-text.
22
+
23
+ // Protect links that open in a new tab against reverse tabnabbing.
24
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
25
+ if (node.tagName === "A" && node.getAttribute("target") === "_blank") {
26
+ node.setAttribute("rel", "noopener noreferrer")
27
+ }
28
+ })
29
+
30
+ const ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }
31
+
32
+ export function sanitizeHtml(value) {
33
+ const html = value == null ? "" : String(value)
34
+ // DOMPurify returns its input UNCHANGED where it can't run (no DOM, e.g. SSR or
35
+ // some test environments). Stay fail-closed there: escape instead of passing through.
36
+ if (!DOMPurify.isSupported) return html.replace(/[&<>"']/g, (c) => ESCAPES[c])
37
+ // `target` is stripped by default; keep it so the hook above can secure new-tab links
38
+ return DOMPurify.sanitize(html, { ADD_ATTR: ["target"] })
39
+ }
40
+
41
+ // For the compiled templates (v-html compiler override in the app's vite.config.js).
42
+ // Set when this module is imported — i.e. before app.mount().
43
+ if (typeof window !== "undefined") {
44
+ window.__sanitizeHtml = sanitizeHtml
45
+ }
46
+
47
+ function render(el, value) {
48
+ el.innerHTML = sanitizeHtml(value)
49
+ }
50
+
51
+ // Directive object — also importable for local registration (`<script setup>`:
52
+ // `import { vSafeHtml } from ".../safeHtml.js"` makes `v-safe-html` available
53
+ // without a global plugin).
54
+ export const vSafeHtml = {
55
+ mounted(el, binding) {
56
+ render(el, binding.value)
57
+ },
58
+ updated(el, binding) {
59
+ if (binding.value !== binding.oldValue) {
60
+ render(el, binding.value)
61
+ }
62
+ },
63
+ }
64
+
65
+ export default {
66
+ install: (app) => {
67
+ // Also available globally as an instance property (e.g. for render functions).
68
+ app.config.globalProperties.$sanitizeHtml = sanitizeHtml
69
+
70
+ // Explicit alias for v-html.
71
+ app.directive("safe-html", vSafeHtml)
72
+ },
73
+ }
@@ -0,0 +1,4 @@
1
+ import type { DirectiveTransform } from "@vue/compiler-dom"
2
+
3
+ /** Compile-time v-html override: routes every v-html through `window.__sanitizeHtml` (DOMPurify). */
4
+ export declare const sanitizeVHtml: DirectiveTransform
@@ -0,0 +1,15 @@
1
+ import { DOMDirectiveTransforms, createCompoundExpression } from "@vue/compiler-dom"
2
+
3
+ // Compile-time v-html override for the app's vite.config.js: every v-html is routed
4
+ // through window.__sanitizeHtml (DOMPurify, set by src/vite/safeHtml.js /
5
+ // installViurForms). Wraps Vue's own transform, so its dev warnings and child
6
+ // handling stay intact.
7
+ //
8
+ // vue({ template: { compilerOptions: { directiveTransforms: { html: sanitizeVHtml } } } })
9
+ export function sanitizeVHtml(dir, node, context) {
10
+ const result = DOMDirectiveTransforms.html(dir, node, context)
11
+ if (dir.exp) {
12
+ result.props[0].value = createCompoundExpression(["window.__sanitizeHtml(", dir.exp, ")"])
13
+ }
14
+ return result
15
+ }
@@ -1,43 +1,3 @@
1
- :root {
2
- --sl-spacing-x-small: var(--wa-space-xs);
3
- --sl-spacing-small: var(--wa-space-s);
4
- --sl-spacing-medium: var(--wa-space-m);
5
- --sl-spacing-large: var(--wa-space-l);
6
-
7
- --sl-border-radius-medium: var(--wa-border-radius-m);
8
- --sl-input-border-radius-medium: var(--wa-form-control-border-radius);
9
- --sl-input-height-medium: var(--wa-form-control-height);
10
- --sl-input-font-size-medium: var(--wa-font-size-m);
11
-
12
- --sl-color-neutral-0: var(--wa-color-surface-default);
13
- --sl-color-neutral-50: var(--wa-color-neutral-95);
14
- --sl-color-neutral-100: var(--wa-color-neutral-90);
15
- --sl-color-neutral-200: var(--wa-color-neutral-fill-normal);
16
- --sl-color-neutral-300: var(--wa-color-surface-border);
17
- --sl-color-neutral-400: var(--wa-color-neutral-border-normal);
18
- --sl-color-neutral-500: var(--wa-color-neutral-60);
19
- --sl-color-neutral-600: var(--wa-color-neutral-50);
20
- --sl-color-neutral-700: var(--wa-color-neutral-40);
21
- --sl-color-neutral-800: var(--wa-color-neutral-20);
22
- --sl-color-neutral-900: var(--wa-color-text-normal);
23
-
24
- --sl-color-gray-200: var(--wa-color-neutral-95);
25
- --sl-color-gray-400: var(--wa-color-neutral-60);
26
- --sl-color-gray-500: var(--wa-color-neutral-border-normal);
27
-
28
- --sl-color-primary-500: var(--wa-color-brand-50);
29
- --sl-color-primary-600: var(--wa-color-brand-60);
30
- --sl-color-success-500: var(--wa-color-success-50);
31
- --sl-color-success-600: var(--wa-color-success-60);
32
- --sl-color-danger-500: var(--wa-color-danger-50);
33
- --sl-color-danger-700: var(--wa-color-danger-70);
34
-
35
- --sl-color-info-50: var(--wa-color-brand-95);
36
- --sl-color-info-300: var(--wa-color-brand-90);
37
- --sl-color-info-500: var(--wa-color-brand-60);
38
- --sl-color-info-900: var(--wa-color-brand-30);
39
- }
40
-
41
1
  wa-input,
42
2
  wa-textarea,
43
3
  wa-select,