viur-ui 0.0.2 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viur-ui",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/viur-framework/viur-ui"
@@ -36,7 +36,7 @@
36
36
  "vue-router": "^4.5.0"
37
37
  },
38
38
  "scripts": {
39
- "icons": "npx wa-icons . ./src/public/assets/icons",
39
+ "icons": "npx wa-icons . ./stories/public/webawesome/assets/icons",
40
40
  "test": "vitest",
41
41
  "test:run": "vitest run",
42
42
  "story:dev": "histoire dev",
@@ -59,6 +59,8 @@
59
59
  "histoire": "^1.0.0-beta.1",
60
60
  "lefthook": "^2.1.6",
61
61
  "prettier": "^3.8.3",
62
+ "typescript": "^6.0.3",
63
+ "typescript-eslint": "^8.71.0",
62
64
  "vite": "^7.3.0",
63
65
  "vitest": "^4.1.4",
64
66
  "vue": "^3.5.32",
@@ -310,12 +310,12 @@
310
310
  role="row"
311
311
  :style="state.gridStyle"
312
312
  :data-key="displayRow.key"
313
- @dblclick="onRowDoubleClick(displayRow, $event)"
314
313
  :aria-selected="
315
314
  selectable !== false && displayRow.kind === 'data' ? state.selectedSet.has(displayRow.nodeKey) : undefined
316
315
  "
317
316
  :aria-level="state.isTree ? displayRow.depth + 1 : undefined"
318
317
  :aria-expanded="displayRow.expandable ? displayRow.expanded : undefined"
318
+ @dblclick="onRowDoubleClick(displayRow, $event)"
319
319
  >
320
320
  <div
321
321
  v-for="cell in state.cellColumns"
@@ -1782,9 +1782,11 @@ function toggleExpand(displayRow: DisplayRow<R>): void {
1782
1782
  if (props.loadChildren && displayRow.kind === "data" && displayRow.row) {
1783
1783
  const row = displayRow.row
1784
1784
  if (childState(row) === "unknown" && !displayRow.node?.children.length) {
1785
- void fetchChildren(row).then(() => {
1786
- if (childState(row) !== "empty") expand(displayRow.nodeKey, row)
1787
- })
1785
+ void fetchChildren(row)
1786
+ .then(() => {
1787
+ if (childState(row) !== "empty") expand(displayRow.nodeKey, row)
1788
+ })
1789
+ .catch((error) => console.warn("[DataTable] Ebene konnte nicht aufgeklappt werden", error))
1788
1790
  return
1789
1791
  }
1790
1792
  }
@@ -2537,7 +2539,7 @@ defineExpose({
2537
2539
  const row = findRowByKey(key)
2538
2540
  if (!row) return
2539
2541
  delete state.loadedChildren[String(key)]
2540
- await fetchChildren(row, true)
2542
+ return await fetchChildren(row, true)
2541
2543
  },
2542
2544
  /** A row's lazy-loaded children (undefined = not loaded yet). */
2543
2545
  getLoadedChildren: (key: RowKey): R[] | undefined => state.loadedChildren[String(key)] as R[] | undefined,
@@ -2718,16 +2720,17 @@ watch(
2718
2720
  z-index: 3;
2719
2721
  }
2720
2722
  /* The row only provides the tint as a variable and lays out the cells —
2721
- painting happens in the CELLS.
2722
- Reason: the row box is only as wide as the visible area, but its
2723
- column tracks can extend beyond it (horizontal scrolling).
2724
- A background or border on the row therefore breaks off in the middle of the grid
2725
- — visible as a missing divider under pinned columns as soon as you
2726
- scroll. The cells, in contrast, cover the full width. */
2723
+ painting happens in the CELLS, which always cover the full width of the grid.
2724
+ The row box itself grows to the full width of its column tracks
2725
+ (`min-inline-size: max-content`). Without that it is only as wide as the
2726
+ visible area while the tracks extend beyond it — and cells pinned to the
2727
+ start, being `position: sticky` inside the row, scroll away with the row
2728
+ once the table is scrolled further than one visible width. */
2727
2729
  .vi-dt-row {
2728
2730
  --dt-row-bg: var(--wa-color-surface-default);
2729
2731
  display: grid;
2730
2732
  align-items: stretch;
2733
+ min-inline-size: max-content;
2731
2734
  min-block-size: var(--dt-row-height, 2.75rem);
2732
2735
  background-color: transparent;
2733
2736
  }
@@ -2793,6 +2796,11 @@ watch(
2793
2796
  background-color: var(--dt-row-bg);
2794
2797
  border-block-end: var(--dt-border-width) solid var(--dt-border-color);
2795
2798
  overflow: hidden;
2799
+ /* Every row is its own grid, sized by `max-content` (see .vi-dt-row). If
2800
+ cell content counted towards that size, a row with a long text would
2801
+ size its flex columns differently from the others and the columns would
2802
+ no longer line up. Content still overflows and ellipsizes as before. */
2803
+ contain: inline-size;
2796
2804
  }
2797
2805
  .vi-dt-cell.is-lead {
2798
2806
  padding-inline: var(--wa-space-2xs);
@@ -35,6 +35,48 @@ export interface GridNavigationOptions {
35
35
 
36
36
  const RESIZE_STEP = 24
37
37
 
38
+ const TOGGLE_INPUT_TYPES = new Set(["checkbox", "radio", "button", "submit", "reset", "image"])
39
+
40
+ /**
41
+ * Classifies the control inside a cell that a keydown came from (shadow DOM
42
+ * included). "text" controls own every navigation key, "activate" controls
43
+ * (buttons, checkboxes, switches) only Enter and Space. The walk stops at the
44
+ * cell, so the cell itself never counts.
45
+ */
46
+ function cellControl(event: KeyboardEvent): { kind: "text" | "activate"; cell: HTMLElement | undefined } | undefined {
47
+ const path = event.composedPath()
48
+ let kind: "text" | "activate" | undefined
49
+ for (const node of path) {
50
+ if (!(node instanceof HTMLElement)) continue
51
+ if (node.hasAttribute("data-row")) return kind ? { kind, cell: node } : undefined
52
+ if (kind === "text") continue
53
+ const tag = node.tagName
54
+ const role = node.getAttribute("role")
55
+ if (
56
+ tag === "TEXTAREA" ||
57
+ tag === "SELECT" ||
58
+ node.isContentEditable ||
59
+ (tag === "INPUT" && !TOGGLE_INPUT_TYPES.has((node as HTMLInputElement).type)) ||
60
+ role === "textbox" ||
61
+ role === "combobox" ||
62
+ role === "listbox"
63
+ ) {
64
+ kind = "text"
65
+ } else if (
66
+ tag === "BUTTON" ||
67
+ tag === "INPUT" ||
68
+ (tag === "A" && node.hasAttribute("href")) ||
69
+ role === "button" ||
70
+ role === "checkbox" ||
71
+ role === "switch"
72
+ ) {
73
+ kind ??= "activate"
74
+ }
75
+ }
76
+ // Not inside a cell (e.g. a toolbar control) - leave it alone too.
77
+ return kind ? { kind, cell: undefined } : undefined
78
+ }
79
+
38
80
  export function useGridNavigation(scroller: Ref<HTMLElement | undefined>, options: GridNavigationOptions) {
39
81
  const state = reactive({
40
82
  /** -1 = header row. */
@@ -73,6 +115,23 @@ export function useGridNavigation(scroller: Ref<HTMLElement | undefined>, option
73
115
  }
74
116
 
75
117
  function handleKeydown(event: KeyboardEvent): void {
118
+ // ARIA grid: keys inside a cell's control belong to the control,
119
+ // Escape returns focus to the cell.
120
+ const control = cellControl(event)
121
+ if (control) {
122
+ // An open popup (wa-select) closes itself on Escape at document level.
123
+ // composedPath()[0], not event.target: the target is retargeted to the host.
124
+ const origin = event.composedPath()[0]
125
+ const popupOpen = origin instanceof HTMLElement && origin.getAttribute("aria-expanded") === "true"
126
+ if (event.key === "Escape" && !event.defaultPrevented && !popupOpen && control.cell) {
127
+ event.preventDefault()
128
+ control.cell.focus()
129
+ return
130
+ }
131
+ if (control.kind === "text") return
132
+ if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") return
133
+ }
134
+
76
135
  const rows = options.rowCount()
77
136
  const cols = options.colCount()
78
137
  const inHeader = state.row === -1
@@ -62,31 +62,43 @@ let _skeyPending = null
62
62
 
63
63
  async function _refillSkeys(renderer) {
64
64
  const base = import.meta.env.VITE_API_URL ?? window.location.origin
65
- let response = await fetch(`${base}/${renderer}/skey?amount=${SKEY_BATCH_SIZE}`, { credentials: "include" })
66
- if (!response.ok) {
67
- response = await fetch(`${base}/${renderer}/skey`, { credentials: "include" })
68
- if (!response.ok) throw new Error("Failed to fetch skey token")
65
+ try {
66
+ let response = await fetch(`${base}/${renderer}/skey?amount=${SKEY_BATCH_SIZE}`, { credentials: "include" })
67
+ if (!response.ok) {
68
+ response = await fetch(`${base}/${renderer}/skey`, { credentials: "include" })
69
+ if (!response.ok) throw new Error("Failed to fetch skey token")
70
+ }
71
+ let data = await response.json()
72
+ if (!Array.isArray(data)) data = [data]
73
+ _skeyCache.push(...data)
74
+ } finally {
75
+ // also on failure: otherwise every later caller awaits the same rejected promise
76
+ _skeyPending = null
69
77
  }
70
- let data = await response.json()
71
- if (!Array.isArray(data)) data = [data]
72
- _skeyCache.push(...data)
73
- _skeyPending = null
74
78
  }
75
79
 
76
80
  async function fetchSkeyToken(renderer) {
77
- if (_skeyCache.length === 0) {
78
- if (!_skeyPending) _skeyPending = _refillSkeys(renderer)
79
- await _skeyPending
81
+ try {
82
+ if (_skeyCache.length === 0) {
83
+ if (!_skeyPending) _skeyPending = _refillSkeys(renderer)
84
+ await _skeyPending
85
+ }
86
+ return _skeyCache.shift()
87
+ } catch (error) {
88
+ throw error
80
89
  }
81
- return _skeyCache.shift()
82
90
  }
83
91
 
84
92
  async function executeAndThrow(fetchResult, executeImpl) {
85
- await executeImpl()
86
- if (fetchResult.error.value) {
87
- throw fetchResult.error.value
93
+ try {
94
+ await executeImpl()
95
+ if (fetchResult.error.value) {
96
+ throw fetchResult.error.value
97
+ }
98
+ return fetchResult
99
+ } catch (error) {
100
+ throw error
88
101
  }
89
- return fetchResult
90
102
  }
91
103
 
92
104
  function createControllerListFetch({ all = false, secure = false } = {}) {
@@ -115,6 +127,11 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
115
127
  const orders = ref([])
116
128
  const params = ref({ ...initialParams })
117
129
  let _isNext = false
130
+ // Bumped by execute()/reset(). A request remembers the generation it started
131
+ // in; once a newer one exists it must not write anymore. Aborting alone is
132
+ // not enough: after the list response arrived, afterFetch still awaits the
133
+ // structure round trip and would then write into the already cleared state.
134
+ let _generation = 0
118
135
 
119
136
  function _buildUrl() {
120
137
  const base = url ?? create_url({ controller, endpoint: group ? `list/${group}` : "list", renderer, version })
@@ -135,7 +152,7 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
135
152
  * Additionally, the structure can be a PAIR LIST
136
153
  * (`json.bone.structure.keytuples`) — normalizeStructure flattens that.
137
154
  */
138
- async function _fetchStructure() {
155
+ async function _fetchStructure(generation) {
139
156
  const skeltype =
140
157
  params.value.skelType === "node"
141
158
  ? "viewNodeSkel"
@@ -146,11 +163,15 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
146
163
  if (params.value.skelType) {
147
164
  url += `${url.includes("?") ? "&" : "?"}skelType=${encodeURIComponent(params.value.skelType)}`
148
165
  }
149
- const resp = await fetch(url, { credentials: "include", headers: { Accept: "application/json" } })
150
- if (!resp.ok) return
151
- const body = normalizeEnvelope(await resp.json())
152
- const raw = body?.["structure"] ?? body?.[skeltype]
153
- if (raw) structure.value = normalizeStructure(raw)
166
+ try {
167
+ const resp = await fetch(url, { credentials: "include", headers: { Accept: "application/json" } })
168
+ if (!resp.ok) return
169
+ const body = normalizeEnvelope(await resp.json())
170
+ const raw = body?.["structure"] ?? body?.[skeltype]
171
+ if (raw && generation === _generation) structure.value = normalizeStructure(raw)
172
+ } catch {
173
+ // the structure is optional, the list stays usable without it
174
+ }
154
175
  }
155
176
 
156
177
  const listUrl = ref(_buildUrl())
@@ -166,7 +187,9 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
166
187
  const extraHeaders = typeof headers === "function" ? headers() : headers
167
188
  if (extraHeaders) options.headers = { ...options.headers, ...extraHeaders }
168
189
  if (cached && !_isNext) {
169
- const hit = await getCached(url, ttl)
190
+ const hit = await getCached(url, ttl).catch((error) => {
191
+ throw error
192
+ })
170
193
  if (hit) {
171
194
  data.value = hit.data ?? hit.skellist ?? hit
172
195
  cursor.value = hit.meta?.cursor ?? hit.cursor ?? ""
@@ -175,17 +198,29 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
175
198
  return { options, url }
176
199
  }
177
200
  }
178
- if (!secure) return { options, url }
179
- const skey = await fetchSkeyToken(renderer)
180
- const separator = url.includes("?") ? "&" : "?"
181
- return { url: `${url}${separator}skey=${skey}`, options }
201
+ const generation = _generation
202
+ if (!secure) return { options, url, generation }
203
+ try {
204
+ const skey = await fetchSkeyToken(renderer)
205
+ const separator = url.includes("?") ? "&" : "?"
206
+ return { url: `${url}${separator}skey=${skey}`, options, generation }
207
+ } catch (error) {
208
+ throw error
209
+ }
182
210
  },
183
211
  async afterFetch(ctx) {
184
212
  // Normalize v1/v2 up to the canonical v2 envelope, then read the list
185
213
  // payload from `data` and pagination from `meta`.
214
+ const { generation } = ctx.context
215
+ if (generation !== _generation) return ctx
186
216
  const responseData = normalizeEnvelope(ctx.data)
187
217
  if (!_isNext) {
188
- await _fetchStructure()
218
+ try {
219
+ await _fetchStructure(generation)
220
+ } catch (error) {
221
+ throw error
222
+ }
223
+ if (generation !== _generation) return ctx
189
224
  }
190
225
  const list = responseData.data ?? []
191
226
  // Some installations deliver the structure along with the list
@@ -199,12 +234,19 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
199
234
  data.value = _isNext ? data.value.concat(list) : list
200
235
  if (cached && !_isNext) {
201
236
  const entityKeys = list.map((s) => s.key).filter(Boolean)
202
- await setCached(ctx.url ?? listUrl.value, responseData, entityKeys)
237
+ await setCached(ctx.url ?? listUrl.value, responseData, entityKeys).catch((error) => {
238
+ throw error
239
+ })
203
240
  }
204
241
  ctx.data = data.value
205
242
  return ctx
206
243
  },
207
244
  onFetchError(ctx) {
245
+ // A superseded or aborted request is no failure: keep the current
246
+ // error as it is (null for the run that replaced this one).
247
+ if (ctx.context.generation !== _generation || ctx.error?.name === "AbortError") {
248
+ return { ...ctx, error: error.value }
249
+ }
208
250
  ctx.error = new HTTPError(ctx.response?.status, ctx.response?.statusText, ctx.error?.message, ctx.response)
209
251
  if (messages) {
210
252
  notify(
@@ -245,11 +287,16 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
245
287
  params.value = { ...params.value, cursor: cursor.value }
246
288
  _isNext = true
247
289
  listUrl.value = _buildUrl()
248
- await executeAndThrow(fetchResult, _execute)
290
+ try {
291
+ await executeAndThrow(fetchResult, _execute)
292
+ } catch {
293
+ // already in `error`; rethrowing here would only be an unhandled rejection
294
+ }
249
295
  }
250
296
  })
251
297
 
252
298
  function execute() {
299
+ _generation++
253
300
  _isNext = false
254
301
  abort()
255
302
  data.value = []
@@ -265,34 +312,46 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
265
312
  /** Load one more page; detects the ViUR end (empty page with the same
266
313
  * cursor marker) and then clears the cursor. Returns true on progress. */
267
314
  async function _advance() {
268
- if (!cursor.value) return false
269
- const before = data.value.length
270
- const cursorBefore = cursor.value
271
- params.value = { ...params.value, cursor: cursor.value }
272
- _isNext = true
273
- listUrl.value = _buildUrl()
274
- await executeAndThrow(fetchResult, _execute)
275
- if (data.value.length === before && cursor.value === cursorBefore) {
276
- cursor.value = ""
277
- return false
315
+ try {
316
+ if (!cursor.value) return false
317
+ const before = data.value.length
318
+ const cursorBefore = cursor.value
319
+ params.value = { ...params.value, cursor: cursor.value }
320
+ _isNext = true
321
+ listUrl.value = _buildUrl()
322
+ await executeAndThrow(fetchResult, _execute)
323
+ if (data.value.length === before && cursor.value === cursorBefore) {
324
+ cursor.value = ""
325
+ return false
326
+ }
327
+ return true
328
+ } catch (error) {
329
+ throw error
278
330
  }
279
- return true
280
331
  }
281
332
 
282
333
  /** Load the next `pages` pages (default: one) — e.g. directly as the
283
334
  * handler of the DataTable cursor bar: `@load-more="({ pages }) =>
284
335
  * pages === 'all' ? list.nextAll() : list.next(pages)"`. */
285
336
  async function next(pages = 1) {
286
- let remaining = pages
287
- while (remaining-- > 0) {
288
- if (!(await _advance())) return
337
+ try {
338
+ let remaining = pages
339
+ while (remaining-- > 0) {
340
+ if (!(await _advance())) return
341
+ }
342
+ } catch (error) {
343
+ throw error
289
344
  }
290
345
  }
291
346
 
292
347
  /** Load all remaining pages ("load everything"). */
293
348
  async function nextAll() {
294
- while (await _advance()) {
295
- /* until the end */
349
+ try {
350
+ while (await _advance()) {
351
+ /* until the end */
352
+ }
353
+ } catch (error) {
354
+ throw error
296
355
  }
297
356
  }
298
357
 
@@ -302,6 +361,7 @@ function createControllerListFetch({ all = false, secure = false } = {}) {
302
361
  }
303
362
 
304
363
  function reset() {
364
+ _generation++
305
365
  abort()
306
366
  data.value = []
307
367
  structure.value = {}
@@ -367,23 +427,35 @@ export function useTreeFetch(
367
427
  // detects the ViUR end (empty page with the same cursor marker) itself and
368
428
  // then clears its cursor — only progress counts here.
369
429
  async function _nextOf(list) {
370
- const before = list.data.value.length
371
- await list.next()
372
- return list.data.value.length > before
430
+ try {
431
+ const before = list.data.value.length
432
+ await list.next()
433
+ return list.data.value.length > before
434
+ } catch (error) {
435
+ throw error
436
+ }
373
437
  }
374
438
 
375
439
  async function next() {
376
- if (nodes.cursor.value && (await _nextOf(nodes))) return
377
- if (leaves.cursor.value) await _nextOf(leaves)
440
+ try {
441
+ if (nodes.cursor.value && (await _nextOf(nodes))) return
442
+ if (leaves.cursor.value) await _nextOf(leaves)
443
+ } catch (error) {
444
+ throw error
445
+ }
378
446
  }
379
447
 
380
448
  /** Load all remaining pages of both lists ("load everything"). */
381
449
  async function nextAll() {
382
- while (nodes.cursor.value) {
383
- if (!(await _nextOf(nodes))) break
384
- }
385
- while (leaves.cursor.value) {
386
- if (!(await _nextOf(leaves))) break
450
+ try {
451
+ while (nodes.cursor.value) {
452
+ if (!(await _nextOf(nodes))) break
453
+ }
454
+ while (leaves.cursor.value) {
455
+ if (!(await _nextOf(leaves))) break
456
+ }
457
+ } catch (error) {
458
+ throw error
387
459
  }
388
460
  }
389
461
 
@@ -459,7 +531,12 @@ export function createControllerFetch({
459
531
  },
460
532
  options: {
461
533
  async beforeFetch({ options, url, cancel }) {
462
- const skeyToken = secure ? ctxSkey || (await fetchSkeyToken(renderer)) : null
534
+ let skeyToken = null
535
+ try {
536
+ skeyToken = secure ? ctxSkey || (await fetchSkeyToken(renderer)) : null
537
+ } catch (error) {
538
+ throw error
539
+ }
463
540
  const base = create_url({ controller, endpoint, renderer, version, lang })
464
541
  const extraQs = query ? (typeof query === "string" ? query : new URLSearchParams(query).toString()) : ""
465
542
 
@@ -473,7 +550,9 @@ export function createControllerFetch({
473
550
  _cacheKey = qs ? `${base}?${qs}` : base
474
551
 
475
552
  if (cached) {
476
- const hit = await getCached(_cacheKey, ttl)
553
+ const hit = await getCached(_cacheKey, ttl).catch((error) => {
554
+ throw error
555
+ })
477
556
  if (hit) {
478
557
  _cachedHit = hit
479
558
  cancel()
@@ -525,18 +604,25 @@ export function createControllerFetch({
525
604
  }
526
605
  if (cached && method.toUpperCase() === "GET" && _cacheKey) {
527
606
  const entityKey = ctx.data?.data?.key
528
- await setCached(_cacheKey, ctx.data, entityKey ? [entityKey] : [])
607
+ await setCached(_cacheKey, ctx.data, entityKey ? [entityKey] : []).catch((error) => {
608
+ throw error
609
+ })
529
610
  }
530
611
  if (invalidates) {
531
612
  const keys = []
532
613
  if (data.key) keys.push(data.key)
533
614
  if (ctx.data?.data?.key) keys.push(ctx.data.data.key)
534
- if (keys.length) await invalidateByKey(keys)
615
+ if (keys.length)
616
+ await invalidateByKey(keys).catch((error) => {
617
+ throw error
618
+ })
535
619
  }
536
620
  if (invalidatesAll) {
537
621
  const base = import.meta.env.VITE_API_URL ?? window.location.origin
538
622
  const controllerPath = Array.isArray(controller) ? controller.join("/") : controller
539
- await clearCache(`${base}/${renderer}/${controllerPath}/`)
623
+ await clearCache(`${base}/${renderer}/${controllerPath}/`).catch((error) => {
624
+ throw error
625
+ })
540
626
  }
541
627
  return ctx
542
628
  },
@@ -586,47 +672,51 @@ export async function uploadFile(
586
672
  file,
587
673
  { target = undefined, publicUpload = false, renderer = null, version = null } = {}
588
674
  ) {
589
- const _renderer = renderer ?? import.meta.env.VITE_DEFAULT_RENDERER ?? "json"
590
- const fetchOpts = { credentials: "include", headers: { Accept: "application/json" }, method: "POST" }
591
-
592
- // Step 1: get upload URL
593
- const filedata = {
594
- fileName: file.name,
595
- mimeType: file.type || "application/octet-stream",
596
- size: file.size.toString(),
597
- }
598
- if (target) filedata.node = target
599
- if (publicUpload) filedata.public = true
600
- const fd1 = _toFormData(filedata)
601
- fd1.append("skey", await fetchSkeyToken(_renderer))
675
+ try {
676
+ const _renderer = renderer ?? import.meta.env.VITE_DEFAULT_RENDERER ?? "json"
677
+ const fetchOpts = { credentials: "include", headers: { Accept: "application/json" }, method: "POST" }
678
+
679
+ // Step 1: get upload URL
680
+ const filedata = {
681
+ fileName: file.name,
682
+ mimeType: file.type || "application/octet-stream",
683
+ size: file.size.toString(),
684
+ }
685
+ if (target) filedata.node = target
686
+ if (publicUpload) filedata.public = true
687
+ const fd1 = _toFormData(filedata)
688
+ fd1.append("skey", await fetchSkeyToken(_renderer))
689
+
690
+ const getUrlResp = await fetch(
691
+ create_url({ controller: "file", endpoint: "getUploadURL", renderer: _renderer, version }),
692
+ {
693
+ ...fetchOpts,
694
+ body: fd1,
695
+ }
696
+ )
697
+ if (!getUrlResp.ok) throw new Error(`getUploadURL failed: ${getUrlResp.status}`)
698
+ // Envelope-agnostic: v2 carries the payload in `data`, v1 in `values`.
699
+ const getUrlData = normalizeEnvelope(await getUrlResp.json())
700
+ const { uploadUrl, uploadKey } = getUrlData.data ?? getUrlData.values ?? {}
602
701
 
603
- const getUrlResp = await fetch(
604
- create_url({ controller: "file", endpoint: "getUploadURL", renderer: _renderer, version }),
605
- {
702
+ // Step 2: upload raw bytes to external storage (no-cors, no credentials)
703
+ await fetch(uploadUrl, { method: "POST", body: file, mode: "no-cors" })
704
+
705
+ // Step 3: register file in ViUR
706
+ const fd3 = _toFormData({ key: uploadKey, skelType: "leaf" })
707
+ fd3.append("skey", await fetchSkeyToken(_renderer))
708
+
709
+ const addResp = await fetch(create_url({ controller: "file", endpoint: "add", renderer: _renderer, version }), {
606
710
  ...fetchOpts,
607
- body: fd1,
608
- }
609
- )
610
- if (!getUrlResp.ok) throw new Error(`getUploadURL failed: ${getUrlResp.status}`)
611
- // Envelope-agnostic: v2 carries the payload in `data`, v1 in `values`.
612
- const getUrlData = normalizeEnvelope(await getUrlResp.json())
613
- const { uploadUrl, uploadKey } = getUrlData.data ?? getUrlData.values ?? {}
614
-
615
- // Step 2: upload raw bytes to external storage (no-cors, no credentials)
616
- await fetch(uploadUrl, { method: "POST", body: file, mode: "no-cors" })
617
-
618
- // Step 3: register file in ViUR
619
- const fd3 = _toFormData({ key: uploadKey, skelType: "leaf" })
620
- fd3.append("skey", await fetchSkeyToken(_renderer))
621
-
622
- const addResp = await fetch(create_url({ controller: "file", endpoint: "add", renderer: _renderer, version }), {
623
- ...fetchOpts,
624
- body: fd3,
625
- })
626
- if (!addResp.ok) throw new Error(`file/add failed: ${addResp.status}`)
627
- const addData = normalizeEnvelope(await addResp.json())
628
- if (addData.status !== "success") throw new Error(`Upload failed: ${JSON.stringify(addData)}`)
629
- return addData.data ?? addData.values
711
+ body: fd3,
712
+ })
713
+ if (!addResp.ok) throw new Error(`file/add failed: ${addResp.status}`)
714
+ const addData = normalizeEnvelope(await addResp.json())
715
+ if (addData.status !== "success") throw new Error(`Upload failed: ${JSON.stringify(addData)}`)
716
+ return addData.data ?? addData.values
717
+ } catch (error) {
718
+ throw error
719
+ }
630
720
  }
631
721
 
632
722
  function _absolute(url) {
@@ -1,8 +0,0 @@
1
- {
2
- "package": "@fortawesome/fontawesome-free",
3
- "version": "7.2.0",
4
- "totalRequested": 14,
5
- "copied": 14,
6
- "missing": [],
7
- "timestamp": "2026-03-26T08:36:14.058Z"
8
- }
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M176 48L64 48c-8.8 0-16 7.2-16 16l0 384c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-240-88 0c-39.8 0-72-32.2-72-72l0-88zM316.1 160L224 67.9 224 136c0 13.3 10.7 24 24 24l68.1 0zM0 64C0 28.7 28.7 0 64 0L197.5 0c17 0 33.3 6.7 45.3 18.7L365.3 141.3c12 12 18.7 28.3 18.7 45.3L384 448c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 64z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M470.6 374.6l96-96c12.5-12.5 12.5-32.8 0-45.3l-96-96c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l41.4 41.4-357.5 0 41.4-41.4c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-96 96c-6 6-9.4 14.1-9.4 22.6s3.4 16.6 9.4 22.6l96 96c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-41.4-41.4 357.5 0-41.4 41.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M192 96c0-53 43-96 96-96s96 43 96 96l0 3.6c0 15.7-12.7 28.4-28.4 28.4l-135.1 0c-15.7 0-28.4-12.7-28.4-28.4l0-3.6zm345.6 12.8c10.6 14.1 7.7 34.2-6.4 44.8l-97.8 73.3c5.3 8.9 9.3 18.7 11.8 29.1l98.8 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l-96 0 0 32c0 2.6-.1 5.3-.2 7.9l83.4 62.5c14.1 10.6 17 30.7 6.4 44.8s-30.7 17-44.8 6.4l-63.1-47.3c-23.2 44.2-66.5 76.2-117.7 83.9L312 280c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 230.2c-51.2-7.7-94.5-39.7-117.7-83.9L83.2 473.6c-14.1 10.6-34.2 7.7-44.8-6.4s-7.7-34.2 6.4-44.8l83.4-62.5c-.1-2.6-.2-5.2-.2-7.9l0-32-96 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l98.8 0c2.5-10.4 6.5-20.2 11.8-29.1L44.8 153.6c-14.1-10.6-17-30.7-6.4-44.8s30.7-17 44.8-6.4L192 184c12.3-5.1 25.8-8 40-8l112 0c14.2 0 27.7 2.8 40 8l108.8-81.6c14.1-10.6 34.2-7.7 44.8 6.4z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M434.8 70.1c14.3 10.4 17.5 30.4 7.1 44.7l-256 352c-5.5 7.6-14 12.3-23.4 13.1s-18.5-2.7-25.1-9.3l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l101.5 101.5 234-321.7c10.4-14.3 30.4-17.5 44.7-7.1z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 512a256 256 0 1 0 0-512 256 256 0 1 0 0 512zM224 160a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm-8 64l48 0c13.3 0 24 10.7 24 24l0 88 8 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-80 0c-13.3 0-24-10.7-24-24s10.7-24 24-24l24 0 0-64-24 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M160 64c0-17.7-14.3-32-32-32S96 46.3 96 64l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96zM32 320c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM352 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 320c-17.7 0-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-96 0z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 32c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 210.7-41.4-41.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l96 96c12.5 12.5 32.8 12.5 45.3 0l96-96c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 242.7 256 32zM64 320c-35.3 0-64 28.7-64 64l0 32c0 35.3 28.7 64 64 64l320 0c35.3 0 64-28.7 64-64l0-32c0-35.3-28.7-64-64-64l-46.9 0-56.6 56.6c-31.2 31.2-81.9 31.2-113.1 0L110.9 320 64 320zm304 56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M32 32C14.3 32 0 46.3 0 64l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-64 64 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L32 32zM64 352c0-17.7-14.3-32-32-32S0 334.3 0 352l0 96c0 17.7 14.3 32 32 32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-64 0 0-64zM320 32c-17.7 0-32 14.3-32 32s14.3 32 32 32l64 0 0 64c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32l-96 0zM448 352c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 64-64 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l96 0c17.7 0 32-14.3 32-32l0-96z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M128 40c0-22.1-17.9-40-40-40L40 0C17.9 0 0 17.9 0 40L0 88c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zm0 192c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM0 424l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 40c0-22.1-17.9-40-40-40L232 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48zM192 232l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40zM320 424c0-22.1-17.9-40-40-40l-48 0c-22.1 0-40 17.9-40 40l0 48c0 22.1 17.9 40 40 40l48 0c22.1 0 40-17.9 40-40l0-48z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 64c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 160-160 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l160 0 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32l0-160 160 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-160 0 0-160z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M64 160c0-53 43-96 96-96s96 43 96 96c0 42.7-27.9 78.9-66.5 91.4-28.4 9.2-61.5 35.3-61.5 76.6l0 24c0 17.7 14.3 32 32 32s32-14.3 32-32l0-24c0-1.7 .6-4.1 3.5-7.3 3-3.3 7.9-6.5 13.7-8.4 64.3-20.7 110.8-81 110.8-152.3 0-88.4-71.6-160-160-160S0 71.6 0 160c0 17.7 14.3 32 32 32s32-14.3 32-32zm96 352c22.1 0 40-17.9 40-40s-17.9-40-40-40-40 17.9-40 40 17.9 40 40 40z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 0c14.7 0 28.2 8.1 35.2 21l216 400c6.7 12.4 6.4 27.4-.8 39.5S486.1 480 472 480L40 480c-14.1 0-27.2-7.4-34.4-19.5s-7.5-27.1-.8-39.5l216-400c7-12.9 20.5-21 35.2-21zm0 352a32 32 0 1 0 0 64 32 32 0 1 0 0-64zm0-192c-18.2 0-32.7 15.5-31.4 33.7l7.4 104c.9 12.5 11.4 22.3 23.9 22.3 12.6 0 23-9.7 23.9-22.3l7.4-104c1.3-18.2-13.1-33.7-31.4-33.7z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M256 109.3L256 320c0 17.7-14.3 32-32 32s-32-14.3-32-32l0-210.7-41.4 41.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l96-96c12.5-12.5 32.8-12.5 45.3 0l96 96c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 109.3zM224 400c44.2 0 80-35.8 80-80l80 0c35.3 0 64 28.7 64 64l0 32c0 35.3-28.7 64-64 64L64 480c-35.3 0-64-28.7-64-64l0-32c0-35.3 28.7-64 64-64l80 0c0 44.2 35.8 80 80 80zm144 24a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2026 Fonticons, Inc. --><path fill="currentColor" d="M55.1 73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L147.2 256 9.9 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192.5 301.3 329.9 438.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.8 256 375.1 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192.5 210.7 55.1 73.4z"/></svg>