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 +4 -2
- package/src/components/DataTable/DataTable.vue +19 -11
- package/src/components/DataTable/useGridNavigation.ts +59 -0
- package/src/request/request.js +190 -100
- package/src/public/assets/icons/fontawesome-manifest.json +0 -8
- package/src/public/assets/icons/regular/file.svg +0 -1
- package/src/public/assets/icons/solid/arrows-left-right.svg +0 -1
- package/src/public/assets/icons/solid/bug.svg +0 -1
- package/src/public/assets/icons/solid/check.svg +0 -1
- package/src/public/assets/icons/solid/circle-info.svg +0 -1
- package/src/public/assets/icons/solid/compress.svg +0 -1
- package/src/public/assets/icons/solid/download.svg +0 -1
- package/src/public/assets/icons/solid/expand.svg +0 -1
- package/src/public/assets/icons/solid/grip-vertical.svg +0 -1
- package/src/public/assets/icons/solid/plus.svg +0 -1
- package/src/public/assets/icons/solid/question.svg +0 -1
- package/src/public/assets/icons/solid/triangle-exclamation.svg +0 -1
- package/src/public/assets/icons/solid/upload.svg +0 -1
- package/src/public/assets/icons/solid/xmark.svg +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viur-ui",
|
|
3
|
-
"version": "0.0.
|
|
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 . ./
|
|
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)
|
|
1786
|
-
|
|
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
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
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
|
package/src/request/request.js
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
78
|
-
if (
|
|
79
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
cursor.value
|
|
277
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
377
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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:
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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 +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>
|