@dirwell/theme-crosswave 0.0.0-stage → 0.0.1-alpha

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.
@@ -0,0 +1,569 @@
1
+ <script setup>
2
+ import {
3
+ ref,
4
+ shallowRef,
5
+ computed,
6
+ onMounted,
7
+ onBeforeUnmount,
8
+ watchEffect,
9
+ watch,
10
+ nextTick,
11
+ } from "vue";
12
+ import { useGamepad } from "@vueuse/core";
13
+ import { loadListing } from "@dirwell/core/client";
14
+ import {
15
+ fetchThemePage,
16
+ applyThemeMetadata,
17
+ restoreThemePage,
18
+ } from "./shared/navigation.mjs";
19
+ import { entryIcon } from "./shared/icons.mjs";
20
+ import { directoryLike, size, modified } from "./shared/records.mjs";
21
+ import { createWave } from "./wave.mjs";
22
+ const props = defineProps({ model: { type: Object, required: true } }),
23
+ model = shallowRef(restoreThemePage(props.model, Boolean(import.meta.hot))),
24
+ entries = shallowRef(model.value.entries);
25
+ const query = ref(""),
26
+ category = ref("all"),
27
+ selected = ref(null),
28
+ ready = ref(false),
29
+ busy = ref(false),
30
+ error = ref(""),
31
+ color = ref(props.model.color),
32
+ paused = ref(!props.model.backgroundMotion),
33
+ clock = ref(""),
34
+ pad = ref(false),
35
+ waveReady = ref(false),
36
+ pendingUpdates = ref(null),
37
+ canvas = ref(),
38
+ search = ref();
39
+ const complete = computed(() => entries.value.length === model.value.total),
40
+ membership = (entry) =>
41
+ Array.isArray(entry.presentation?.categories) ? entry.presentation.categories : [];
42
+ const rows = computed(() =>
43
+ entries.value.filter(
44
+ (entry) =>
45
+ (category.value === "all" || membership(entry).includes(category.value)) &&
46
+ `${entry.name} ${entry.link?.target ?? ""}`
47
+ .toLocaleLowerCase()
48
+ .includes(query.value.trim().toLocaleLowerCase()),
49
+ ),
50
+ ),
51
+ active = computed(() => rows.value.find((entry) => entry.id === selected.value) ?? rows.value[0]),
52
+ heading = computed(
53
+ () => model.value.categories.find((item) => item.id === category.value)?.label ?? "All files",
54
+ );
55
+ const nodes = new Map();
56
+ let wave,
57
+ gamepad,
58
+ abort,
59
+ navigation = 0,
60
+ timer,
61
+ lastPad = 0,
62
+ pressed = new Set(),
63
+ disposed = false,
64
+ padIdentity = null,
65
+ motionQuery;
66
+ function safeHref(entry) {
67
+ return typeof location !== "undefined" &&
68
+ location.protocol === "file:" &&
69
+ directoryLike(entry) &&
70
+ !entry.exitsExplorer &&
71
+ entry.href?.endsWith("/")
72
+ ? entry.href + "index.html"
73
+ : entry.href;
74
+ }
75
+ async function loadComplete() {
76
+ if (complete.value || !model.value.manifestHref || location.protocol === "file:") return;
77
+ const local = abort,
78
+ token = navigation;
79
+ busy.value = true;
80
+ try {
81
+ const data = await loadListing(new URL(model.value.manifestHref, document.baseURI).href, {
82
+ signal: local.signal,
83
+ cache: model.value.updatesHref ? "no-store" : "default",
84
+ });
85
+ if (token === navigation && !local.signal.aborted && !disposed) entries.value = data.entries;
86
+ } catch (cause) {
87
+ if (token === navigation && !local.signal.aborted)
88
+ error.value = "Could not load complete directory data. Native page links remain available.";
89
+ } finally {
90
+ if (token === navigation) busy.value = false;
91
+ }
92
+ }
93
+ async function openFolder(dataHref, url, push, refresh = false) {
94
+ const token = ++navigation;
95
+ abort?.abort();
96
+ const local = (abort = new AbortController());
97
+ busy.value = true;
98
+ error.value = "";
99
+ try {
100
+ const next = await fetchThemePage(dataHref, url, "crosswave", local.signal, refresh);
101
+ let nextEntries = next.entries;
102
+ if (refresh && next.manifestHref && nextEntries.length !== next.total)
103
+ nextEntries = (
104
+ await loadListing(next.manifestHref, { signal: local.signal, cache: "no-store" })
105
+ ).entries;
106
+ if (token !== navigation || local.signal.aborted || disposed) return;
107
+ model.value = next;
108
+ entries.value = nextEntries;
109
+ if (!refresh) {
110
+ query.value = "";
111
+ selected.value = null;
112
+ category.value = "all";
113
+ }
114
+ if (push) history.pushState({ dataHref: next.dataHref }, "", url);
115
+ applyThemeMetadata(next, url);
116
+ await loadComplete();
117
+ } catch (cause) {
118
+ if (token === navigation && !local.signal.aborted)
119
+ error.value = "Could not open the folder. Its native link remains available.";
120
+ } finally {
121
+ if (token === navigation) busy.value = false;
122
+ }
123
+ }
124
+ function navigateLink(event, href, dataHref) {
125
+ if (
126
+ event?.altKey ||
127
+ event?.ctrlKey ||
128
+ event?.metaKey ||
129
+ event?.shiftKey ||
130
+ event?.button > 0 ||
131
+ location.protocol === "file:"
132
+ )
133
+ return;
134
+ event?.preventDefault();
135
+ openFolder(dataHref, new URL(href, document.baseURI).href, true);
136
+ }
137
+ async function openEntry(event, entry) {
138
+ const href = entry.presentation?.folderDataHref;
139
+ if (
140
+ entry.exitsExplorer ||
141
+ event?.altKey ||
142
+ event?.ctrlKey ||
143
+ event?.metaKey ||
144
+ event?.shiftKey ||
145
+ event?.button > 0 ||
146
+ typeof href !== "string" ||
147
+ location.protocol === "file:"
148
+ )
149
+ return;
150
+ event?.preventDefault();
151
+ await openFolder(href, new URL(entry.href, document.baseURI).href, true);
152
+ }
153
+ async function move(index) {
154
+ const entry = rows.value[Math.max(0, Math.min(rows.value.length - 1, index))];
155
+ if (!entry) return;
156
+ selected.value = entry.id;
157
+ await nextTick();
158
+ const node = nodes.get(entry.id);
159
+ node?.querySelector("a")?.focus({ preventScroll: true });
160
+ node?.scrollIntoView({ block: "nearest" });
161
+ }
162
+ function stepCategory(delta) {
163
+ const items = model.value.categories,
164
+ index = items.findIndex((item) => item.id === category.value);
165
+ category.value = items[(index + delta + items.length) % items.length].id;
166
+ selected.value = null;
167
+ }
168
+ function confirm() {
169
+ if (!active.value || busy.value) return;
170
+ const anchor = nodes.get(active.value.id)?.querySelector("a");
171
+ anchor?.click();
172
+ }
173
+ function parent() {
174
+ if (model.value.parentHref) {
175
+ if (location.protocol === "file:") location.href = model.value.parentHref;
176
+ else
177
+ openFolder(
178
+ model.value.parentDataHref,
179
+ new URL(model.value.parentHref, document.baseURI).href,
180
+ true,
181
+ );
182
+ }
183
+ }
184
+ function keyboard(event) {
185
+ if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey) return;
186
+ const editing = /INPUT|TEXTAREA|SELECT/.test(event.target?.tagName ?? "");
187
+ if (editing) {
188
+ if (event.key === "Escape") query.value = "";
189
+ return;
190
+ }
191
+ const index = rows.value.findIndex((entry) => entry.id === (selected.value ?? active.value?.id));
192
+ if (event.key === "/") {
193
+ event.preventDefault();
194
+ search.value?.focus();
195
+ } else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
196
+ event.preventDefault();
197
+ move(
198
+ event.key === "Home"
199
+ ? 0
200
+ : event.key === "End"
201
+ ? rows.value.length - 1
202
+ : index + (event.key === "ArrowDown" ? 1 : -1),
203
+ );
204
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
205
+ event.preventDefault();
206
+ stepCategory(event.key === "ArrowRight" ? 1 : -1);
207
+ } else if (event.key === "Enter") {
208
+ event.preventDefault();
209
+ confirm();
210
+ } else if (event.key === "Backspace") {
211
+ event.preventDefault();
212
+ parent();
213
+ }
214
+ }
215
+ function controller(value) {
216
+ pad.value = !!value;
217
+ if (!value) {
218
+ pressed.clear();
219
+ padIdentity = null;
220
+ return;
221
+ }
222
+ const buttons = value.buttons ?? [],
223
+ next = new Set(
224
+ buttons
225
+ .map((button, index) => (button.pressed || button.value > 0.5 ? index : -1))
226
+ .filter((index) => index >= 0),
227
+ ),
228
+ time = performance.now(),
229
+ axis = value.axes ?? [];
230
+ const identity = `${value.index}:${value.id}`;
231
+ if (padIdentity !== identity) {
232
+ padIdentity = identity;
233
+ pressed = next;
234
+ lastPad = time;
235
+ return;
236
+ }
237
+ if (
238
+ document.hidden ||
239
+ disposed ||
240
+ busy.value ||
241
+ !complete.value ||
242
+ /INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName ?? "")
243
+ ) {
244
+ pressed = next;
245
+ return;
246
+ }
247
+ if (time - lastPad > 180) {
248
+ const index = rows.value.findIndex(
249
+ (entry) => entry.id === (selected.value ?? active.value?.id),
250
+ );
251
+ if (next.has(13) || (axis[1] ?? 0) > 0.35) {
252
+ move(index + 1);
253
+ lastPad = time;
254
+ } else if (next.has(12) || (axis[1] ?? 0) < -0.35) {
255
+ move(index - 1);
256
+ lastPad = time;
257
+ } else if (next.has(15) || (axis[0] ?? 0) > 0.35) {
258
+ stepCategory(1);
259
+ lastPad = time;
260
+ } else if (next.has(14) || (axis[0] ?? 0) < -0.35) {
261
+ stepCategory(-1);
262
+ lastPad = time;
263
+ }
264
+ }
265
+ if (next.has(0) && !pressed.has(0)) confirm();
266
+ if (next.has(1) && !pressed.has(1)) parent();
267
+ pressed = next;
268
+ }
269
+ const colors = {
270
+ azure: [0.17, 0.31, 0.52],
271
+ violet: [0.36, 0.25, 0.49],
272
+ amber: [0.44, 0.3, 0.1],
273
+ rose: [0.44, 0.25, 0.33],
274
+ jade: [0.13, 0.37, 0.32],
275
+ graphite: [0.26, 0.3, 0.38],
276
+ };
277
+ const onPop = (event) =>
278
+ event.state?.dataHref
279
+ ? openFolder(event.state.dataHref, location.href, false)
280
+ : location.reload();
281
+ const suspend = () => {
282
+ gamepad?.pause();
283
+ controller(null);
284
+ wave?.stop();
285
+ };
286
+ const syncMotion = () => wave?.sync();
287
+ const visibility = () => {
288
+ if (document.hidden) {
289
+ gamepad?.pause();
290
+ controller(null);
291
+ wave?.stop();
292
+ } else {
293
+ gamepad?.resume();
294
+ wave?.sync();
295
+ }
296
+ };
297
+ onMounted(() => {
298
+ ready.value = true;
299
+ document.body.setAttribute("data-cw-enhanced", "");
300
+ abort = new AbortController();
301
+ history.replaceState({ dataHref: new URL(model.value.dataHref, document.baseURI).href }, "");
302
+ try {
303
+ const stored = localStorage.getItem("dirwell-crosswave-color");
304
+ if (Object.hasOwn(colors, stored)) color.value = stored;
305
+ } catch {}
306
+ wave = createWave(
307
+ canvas.value,
308
+ colors,
309
+ document.documentElement,
310
+ () =>
311
+ !paused.value && !document.hidden && !matchMedia("(prefers-reduced-motion: reduce)").matches,
312
+ (value) => (waveReady.value = value),
313
+ );
314
+ wave?.sync();
315
+ if (model.value.gamepad && typeof navigator.getGamepads === "function") {
316
+ const safeNavigator = {
317
+ getGamepads() {
318
+ try {
319
+ const pads = navigator.getGamepads();
320
+ if (gamepad) {
321
+ const connected = Array.from(pads).filter((value) => value?.connected);
322
+ gamepad.gamepads.value = gamepad.gamepads.value.filter((value) =>
323
+ connected.some((current) => current.index === value.index),
324
+ );
325
+ for (const value of connected)
326
+ if (!gamepad.gamepads.value.some((current) => current.index === value.index))
327
+ gamepad.gamepads.value.push(value);
328
+ }
329
+ return pads;
330
+ } catch {
331
+ if (gamepad) gamepad.gamepads.value = [];
332
+ controller(null);
333
+ return [];
334
+ }
335
+ },
336
+ };
337
+ gamepad = useGamepad({ navigator: safeNavigator });
338
+ safeNavigator.getGamepads();
339
+ gamepad.resume();
340
+ watchEffect(
341
+ () =>
342
+ controller(
343
+ gamepad.gamepads.value.find((value) => value.connected && value.mapping === "standard") ??
344
+ null,
345
+ ),
346
+ { flush: "sync" },
347
+ );
348
+ }
349
+ const updateClock = () =>
350
+ (clock.value = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }));
351
+ updateClock();
352
+ timer = setInterval(updateClock, 1000);
353
+ window.addEventListener("keydown", keyboard);
354
+ window.addEventListener("popstate", onPop);
355
+ document.addEventListener("visibilitychange", visibility);
356
+ window.addEventListener("pagehide", suspend);
357
+ window.addEventListener("pageshow", visibility);
358
+ motionQuery = matchMedia("(prefers-reduced-motion: reduce)");
359
+ motionQuery.addEventListener("change", syncMotion);
360
+ loadComplete();
361
+ });
362
+ watch(
363
+ () =>
364
+ ready.value && model.value.updatesHref
365
+ ? new URL(model.value.updatesHref, document.baseURI).href
366
+ : null,
367
+ (href, _old, onCleanup) => {
368
+ if (!href || typeof EventSource === "undefined") return;
369
+ const updates = new EventSource(href);
370
+ updates.onmessage = () => (pendingUpdates.value = href);
371
+ updates.onerror = () => (error.value = "Live updates disconnected. Refresh to retry.");
372
+ updates.onopen = () => {
373
+ if (error.value.startsWith("Live updates")) error.value = "";
374
+ };
375
+ onCleanup(() => updates.close());
376
+ },
377
+ );
378
+ watchEffect(() => {
379
+ if (ready.value && !busy.value && pendingUpdates.value) {
380
+ const href = pendingUpdates.value;
381
+ pendingUpdates.value = null;
382
+ if (href === new URL(model.value.updatesHref, document.baseURI).href && !disposed)
383
+ openFolder(model.value.dataHref, location.href, false, true);
384
+ }
385
+ });
386
+ watchEffect(() => {
387
+ if (ready.value) {
388
+ document.documentElement.dataset.cwColor = color.value;
389
+ document.documentElement.dataset.cwMotion = String(!paused.value);
390
+ try {
391
+ localStorage.setItem("dirwell-crosswave-color", color.value);
392
+ } catch {}
393
+ wave?.sync();
394
+ }
395
+ });
396
+ onBeforeUnmount(() => {
397
+ disposed = true;
398
+ document.body.removeAttribute("data-cw-enhanced");
399
+ abort?.abort();
400
+ gamepad?.pause();
401
+ wave?.dispose();
402
+ clearInterval(timer);
403
+ window.removeEventListener("keydown", keyboard);
404
+ window.removeEventListener("popstate", onPop);
405
+ document.removeEventListener("visibilitychange", visibility);
406
+ window.removeEventListener("pagehide", suspend);
407
+ window.removeEventListener("pageshow", visibility);
408
+ motionQuery?.removeEventListener("change", syncMotion);
409
+ });
410
+ </script>
411
+ <template>
412
+ <div class="cw-background" :data-ready="waveReady ? true : undefined">
413
+ <canvas ref="canvas" aria-hidden="true"></canvas
414
+ ><svg class="cw-fallback-wave" aria-hidden="true" viewBox="0 0 1200 600">
415
+ <path d="M-100 400 Q400 150 1300 340" />
416
+ </svg>
417
+ </div>
418
+ <header class="cw-top">
419
+ <div class="cw-brand">
420
+ <slot name="header" :model="model">
421
+ <a
422
+ :href="model.rootHref"
423
+ @click="navigateLink($event, model.rootHref, model.rootDataHref)"
424
+ >{{ model.title }}</a
425
+ ></slot
426
+ >
427
+ </div>
428
+ <nav class="cw-location" aria-label="Breadcrumb">
429
+ <a :href="model.rootHref" @click="navigateLink($event, model.rootHref, model.rootDataHref)"
430
+ >Home</a
431
+ ><template v-for="crumb in model.breadcrumbs" :key="crumb.href">
432
+ /
433
+ <a :href="crumb.href" @click="navigateLink($event, crumb.href, crumb.dataHref)">{{
434
+ crumb.name
435
+ }}</a></template
436
+ >
437
+ </nav>
438
+ <div class="cw-clock" v-if="ready">{{ clock }}</div>
439
+ </header>
440
+ <main class="cw-stage" data-framework="vue" :aria-busy="busy">
441
+ <h1 class="cw-sr">Files in {{ model.path || "/" }}</h1>
442
+ <nav class="cw-rail" aria-label="File categories" :hidden="!ready">
443
+ <div class="cw-categories" role="tablist">
444
+ <button
445
+ v-for="item in model.categories"
446
+ :key="item.id"
447
+ role="tab"
448
+ :id="'cw-tab-' + item.id"
449
+ aria-controls="cw-panel"
450
+ :aria-selected="category === item.id"
451
+ :tabindex="category === item.id ? 0 : -1"
452
+ :class="['cw-category', { 'cw-active': category === item.id }]"
453
+ @click="
454
+ category = item.id;
455
+ selected = null;
456
+ "
457
+ :disabled="!complete"
458
+ >
459
+ <span v-html="item.svg"></span><span>{{ item.label }}</span
460
+ ><small>{{
461
+ entries.filter((entry) => membership(entry).includes(item.id)).length
462
+ }}</small>
463
+ </button>
464
+ </div>
465
+ </nav>
466
+ <section
467
+ class="cw-browser"
468
+ id="cw-panel"
469
+ role="tabpanel"
470
+ :aria-labelledby="'cw-tab-' + category"
471
+ >
472
+ <div class="cw-list-head">
473
+ <div class="cw-directory">
474
+ <h2>{{ heading }}</h2>
475
+ <span data-cw-count>{{ complete ? rows.length : entries.length }} items</span>
476
+ </div>
477
+ <label class="cw-search"
478
+ >Search<input ref="search" type="search" v-model="query" :disabled="!ready || !complete"
479
+ /></label>
480
+ </div>
481
+ <div class="cw-list-scroll">
482
+ <ul class="cw-files">
483
+ <li v-if="model.parentHref" class="cw-row">
484
+ <a
485
+ :href="model.parentHref"
486
+ @click="navigateLink($event, model.parentHref, model.parentDataHref)"
487
+ class="cw-entry"
488
+ >../</a
489
+ >
490
+ </li>
491
+ <li
492
+ v-for="entry in rows"
493
+ :key="entry.id"
494
+ :ref="(node) => (node ? nodes.set(entry.id, node) : nodes.delete(entry.id))"
495
+ :class="['cw-row', { 'cw-selected': active?.id === entry.id }]"
496
+ data-cw-entry
497
+ :data-id="entry.id"
498
+ >
499
+ <a
500
+ v-if="entry.href"
501
+ class="cw-entry"
502
+ :href="safeHref(entry)"
503
+ :target="entry.exitsExplorer ? '_blank' : undefined"
504
+ :rel="entry.exitsExplorer ? 'noopener' : undefined"
505
+ @click="openEntry($event, entry)"
506
+ @focus="selected = entry.id"
507
+ ><slot name="row" :model="model" :entry="entry" :selected="selected === entry.id"
508
+ ><span
509
+ class="cw-entry-icon"
510
+ aria-hidden="true"
511
+ v-html="
512
+ entryIcon(
513
+ entry.kind === 'symlink'
514
+ ? 'link'
515
+ : directoryLike(entry)
516
+ ? 'directory'
517
+ : 'file',
518
+ )
519
+ "
520
+ ></span>
521
+ <span class="cw-entry-label">
522
+ <span class="cw-name">{{ entry.name }}{{ directoryLike(entry) ? "/" : "" }}</span>
523
+ <span v-if="entry.link" class="cw-target">{{ entry.link.target }}</span>
524
+ <span class="cw-row-meta"
525
+ >{{ entry.link?.unavailable ? "Target unavailable" : entry.kind }} ·
526
+ {{ directoryLike(entry) ? "Folder" : size(entry.size) }}</span
527
+ >
528
+ <span class="cw-row-date">Modified {{ modified(entry.modifiedAt) }}</span>
529
+ </span></slot
530
+ ></a
531
+ ><span v-else class="cw-entry cw-unavailable"
532
+ ><slot name="row" :model="model" :entry="entry" :selected="selected === entry.id">{{
533
+ entry.name
534
+ }}</slot></span
535
+ >
536
+ </li>
537
+ </ul>
538
+ <p class="cw-empty" v-if="!rows.length">No matching files.</p>
539
+ </div>
540
+ </section>
541
+ <aside class="cw-detail" v-if="ready && active">
542
+ <h2>{{ active.name }}</h2>
543
+ <p>{{ active.kind }} · {{ directoryLike(active) ? "Folder" : size(active.size) }}</p>
544
+ <p>{{ modified(active.modifiedAt) }}</p>
545
+ <p v-if="active.link">{{ active.link.target }}</p>
546
+ </aside>
547
+ <p role="status" aria-live="polite">{{ busy ? "Loading…" : error }}</p>
548
+ <nav v-if="model.page?.count > 1 && !complete" aria-label="Pagination">
549
+ <a v-if="model.page.previousHref" :href="model.page.previousHref" rel="prev">Previous</a> Page
550
+ {{ model.page.number }} of {{ model.page.count }}
551
+ <a v-if="model.page.nextHref" :href="model.page.nextHref" rel="next">Next</a>
552
+ </nav>
553
+ </main>
554
+ <footer class="cw-bottom">
555
+ <div class="cw-key-help">
556
+ <slot name="footer" :model="model"
557
+ >Arrow keys navigate · Enter confirms · Backspace returns</slot
558
+ >
559
+ </div>
560
+ <div class="cw-controller" v-if="pad">Controller · Confirm / Back</div>
561
+ <label class="cw-appearance"
562
+ >Color<select v-model="color" :disabled="!ready">
563
+ <option v-for="name in Object.keys(colors)" :key="name">{{ name }}</option>
564
+ </select></label
565
+ ><button class="cw-motion" :disabled="!ready" @click="paused = !paused">
566
+ {{ paused ? "Resume motion" : "Pause motion" }}
567
+ </button>
568
+ </footer>
569
+ </template>
@@ -0,0 +1,4 @@
1
+ import type {DefineComponent, SlotsType, VNode, ComponentOptionsMixin, PublicProps} from 'vue';
2
+ import type {ThemePage, RowContext} from '../model.mjs';
3
+ export interface CrosswaveSlots {header?: (props: {model: ThemePage}) => VNode[]; row?: (props: RowContext) => VNode[]; footer?: (props: {model: ThemePage}) => VNode[];}
4
+ declare const Crosswave: DefineComponent<{model: ThemePage}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, {model: ThemePage}, {}, SlotsType<CrosswaveSlots>>; export default Crosswave;
@@ -0,0 +1,12 @@
1
+ // ../shared-theme/src/icons.ts
2
+ var paths = {
3
+ directory: '<path d="M3 7V5h6l2 3h10v12H3Z"/><path d="M3 10h18"/>',
4
+ file: '<path d="M5 3h9l5 5v13H5ZM14 3v5h5M8 12h8M8 16h6"/>',
5
+ link: '<path d="m9 15 6-6M8 10l-3 3a4 4 0 0 0 6 6l3-3M10 8l3-3a4 4 0 0 1 6 6l-3 3"/>'
6
+ };
7
+ function entryIcon(kind) {
8
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[kind]}</svg>`;
9
+ }
10
+ export {
11
+ entryIcon
12
+ };