@andesine/ui 0.4.0

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,1244 @@
1
+ import { createComponent as _$createComponent } from "solid-js/web";
2
+ import { ssrElement as _$ssrElement } from "solid-js/web";
3
+ import { mergeProps as _$mergeProps } from "solid-js/web";
4
+ import { createContext, createEffect, createSignal, createUniqueId, on, onCleanup, onMount, splitProps, useContext } from "solid-js";
5
+ import { isServer } from "solid-js/web";
6
+ import { Tabs as Tabs$1 } from "@ark-ui/solid/tabs";
7
+ const readSession = key => {
8
+ try {
9
+ return JSON.parse(sessionStorage.getItem(key) ?? "null") ?? void 0;
10
+ } catch {
11
+ return;
12
+ }
13
+ };
14
+ const writeSession = (key, value) => {
15
+ try {
16
+ sessionStorage.setItem(key, JSON.stringify(value));
17
+ } catch {}
18
+ };
19
+ const syncEvent$1 = "andesine:navigation-tree";
20
+ const getActiveGroups = items => {
21
+ return items.flatMap(item => {
22
+ return item.children?.length && item.active ? [item.id, ...getActiveGroups(item.children)] : [];
23
+ });
24
+ };
25
+ const getDefaultGroups = (items, depth) => {
26
+ return depth > 0 ? items.flatMap(item => {
27
+ return item.children?.length ? [item.id, ...getDefaultGroups(item.children, depth - 1)] : [];
28
+ }) : [];
29
+ };
30
+ const getInitialGroups = options => {
31
+ const items = options.items();
32
+ const saved = !isServer && options.storageKey ? readSession(options.storageKey) : void 0;
33
+ const base = Array.isArray(saved) ? saved.map(String) : getDefaultGroups(items, options.defaultDepth ?? 0);
34
+ return new Set([...base, ...getActiveGroups(items)]);
35
+ };
36
+ const createNavigationTree = options => {
37
+ const [expanded, setExpanded] = createSignal(getInitialGroups(options));
38
+ const save = ids => {
39
+ if (!options.storageKey) return;
40
+ writeSession(options.storageKey, [...ids]);
41
+ window.dispatchEvent(new CustomEvent(syncEvent$1, {
42
+ detail: {
43
+ key: options.storageKey,
44
+ ids: [...ids]
45
+ }
46
+ }));
47
+ };
48
+ onMount(() => {
49
+ if (!options.storageKey) return;
50
+ const sync = event => {
51
+ const {
52
+ key,
53
+ ids
54
+ } = event.detail;
55
+ if (key === options.storageKey) setExpanded(new Set(ids));
56
+ };
57
+ writeSession(options.storageKey, [...expanded()]);
58
+ window.addEventListener(syncEvent$1, sync);
59
+ onCleanup(() => window.removeEventListener(syncEvent$1, sync));
60
+ });
61
+ return {
62
+ isExpanded: id => expanded().has(id),
63
+ setExpanded: (id, open) => {
64
+ const ids = new Set(expanded());
65
+ if (open) ids.add(id);else ids.delete(id);
66
+ setExpanded(ids);
67
+ save(ids);
68
+ }
69
+ };
70
+ };
71
+ const TreeContext = createContext();
72
+ const ItemContext = createContext();
73
+ const useTree = () => {
74
+ const tree = useContext(TreeContext);
75
+ if (!tree) throw new Error("NavigationTree parts must be inside NavigationTree.Root.");
76
+ return tree;
77
+ };
78
+ const useItem = () => {
79
+ const item = useContext(ItemContext);
80
+ if (!item) throw new Error("NavigationTree item parts must be inside NavigationTree.Item.");
81
+ return item;
82
+ };
83
+ const Root$5 = props => {
84
+ const [local, rest] = splitProps(props, ["items", "storageKey", "defaultDepth"]);
85
+ const tree = createNavigationTree({
86
+ items: () => local.items,
87
+ storageKey: local.storageKey,
88
+ defaultDepth: local.defaultDepth
89
+ });
90
+ return _$createComponent(TreeContext.Provider, {
91
+ value: tree,
92
+ get children() {
93
+ return _$ssrElement("nav", _$mergeProps({
94
+ "aria-label": "Documentation"
95
+ }, rest, {
96
+ "data-scope": "navigation-tree",
97
+ "data-part": "root",
98
+ get ["data-storage-key"]() {
99
+ return local.storageKey;
100
+ }
101
+ }), undefined, true);
102
+ }
103
+ });
104
+ };
105
+ const List$2 = props => {
106
+ return _$ssrElement("ul", _$mergeProps(props, {
107
+ "data-scope": "navigation-tree",
108
+ "data-part": "list"
109
+ }), undefined, true);
110
+ };
111
+ const Item$2 = props => {
112
+ const [local, rest] = splitProps(props, ["item"]);
113
+ const tree = useTree();
114
+ const contentID = createUniqueId();
115
+ const isGroup = () => Boolean(local.item.children?.length);
116
+ return _$createComponent(ItemContext.Provider, {
117
+ get value() {
118
+ return {
119
+ item: local.item,
120
+ contentID
121
+ };
122
+ },
123
+ get children() {
124
+ return _$ssrElement("li", _$mergeProps(rest, {
125
+ "data-scope": "navigation-tree",
126
+ "data-part": "item",
127
+ get ["data-id"]() {
128
+ return local.item.id;
129
+ },
130
+ get ["data-current"]() {
131
+ return local.item.current || void 0;
132
+ },
133
+ get ["data-active"]() {
134
+ return local.item.active || void 0;
135
+ },
136
+ get ["data-state"]() {
137
+ return isGroup() ? tree.isExpanded(local.item.id) ? "open" : "closed" : void 0;
138
+ }
139
+ }), undefined, true);
140
+ }
141
+ });
142
+ };
143
+ const Link$3 = props => {
144
+ const {
145
+ item
146
+ } = useItem();
147
+ return _$ssrElement("a", _$mergeProps({
148
+ get href() {
149
+ return item.href;
150
+ },
151
+ get ["aria-current"]() {
152
+ return item.current ? "page" : void 0;
153
+ }
154
+ }, props, {
155
+ "data-scope": "navigation-tree",
156
+ "data-part": "link"
157
+ }), undefined, true);
158
+ };
159
+ const GroupTrigger = props => {
160
+ const tree = useTree();
161
+ const {
162
+ item,
163
+ contentID
164
+ } = useItem();
165
+ const expanded = () => tree.isExpanded(item.id);
166
+ return _$ssrElement("button", _$mergeProps({
167
+ type: "button"
168
+ }, props, {
169
+ get ["aria-expanded"]() {
170
+ return expanded();
171
+ },
172
+ "aria-controls": contentID,
173
+ "data-scope": "navigation-tree",
174
+ "data-part": "group-trigger",
175
+ get ["data-state"]() {
176
+ return expanded() ? "open" : "closed";
177
+ }
178
+ }), undefined, true);
179
+ };
180
+ const GroupContent = props => {
181
+ const tree = useTree();
182
+ const {
183
+ item,
184
+ contentID
185
+ } = useItem();
186
+ const expanded = () => tree.isExpanded(item.id);
187
+ return _$ssrElement("div", _$mergeProps(props, {
188
+ id: contentID,
189
+ get hidden() {
190
+ return !expanded();
191
+ },
192
+ "data-scope": "navigation-tree",
193
+ "data-part": "group-content",
194
+ get ["data-state"]() {
195
+ return expanded() ? "open" : "closed";
196
+ }
197
+ }), undefined, true);
198
+ };
199
+ const NavigationTree = {
200
+ Root: Root$5,
201
+ List: List$2,
202
+ Item: Item$2,
203
+ Link: Link$3,
204
+ GroupTrigger,
205
+ GroupContent
206
+ };
207
+ const focusableSelector = "a[href], button, input, select, textarea, summary, [tabindex]";
208
+ const focusTarget = target => {
209
+ const temporary = !target.matches(focusableSelector);
210
+ if (temporary) target.setAttribute("tabindex", "-1");
211
+ target.focus({
212
+ preventScroll: true
213
+ });
214
+ if (temporary) target.addEventListener("blur", () => target.removeAttribute("tabindex"), {
215
+ once: true
216
+ });
217
+ };
218
+ const getFocusable = root => {
219
+ return Array.from(root.querySelectorAll(focusableSelector)).filter(element => {
220
+ return element.tabIndex >= 0 && !element.matches(":disabled, [aria-disabled='true']") && !element.closest("[inert], [hidden]") && element.getClientRects().length > 0;
221
+ });
222
+ };
223
+ const getScrollOffset = () => {
224
+ return parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;
225
+ };
226
+ const scrollIntoContainer = (container, element, margin = 8) => {
227
+ const bounds = container.getBoundingClientRect();
228
+ const target = element.getBoundingClientRect();
229
+ if (target.top < bounds.top + margin) container.scrollTop += target.top - bounds.top - margin;
230
+ if (target.bottom > bounds.bottom - margin) container.scrollTop += target.bottom - bounds.bottom + margin;
231
+ };
232
+ const scrollKeys = new Set(["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "]);
233
+ const intentEvents = ["wheel", "touchmove", "keydown", "pointerdown"];
234
+ const createTableOfContents = options => {
235
+ const [activeID, setActiveID] = createSignal(options.headingIDs()[0]);
236
+ const [visibleIDs, setVisibleIDs] = createSignal([]);
237
+ let locked = false;
238
+ let userScroll = false;
239
+ onMount(() => {
240
+ let headings = [];
241
+ let frame = 0;
242
+ const update = () => {
243
+ const offset = getScrollOffset() + 1;
244
+ const atBottom = window.scrollY > 0 && window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2;
245
+ const tops = headings.map(heading => heading.getBoundingClientRect().top);
246
+ const passed = headings.filter((_, index) => tops[index] <= offset);
247
+ const visible = headings.filter((heading, index) => {
248
+ const end = tops[index + 1] ?? heading.parentElement.getBoundingClientRect().bottom;
249
+ return tops[index] < window.innerHeight && end > offset;
250
+ });
251
+ frame = 0;
252
+ setVisibleIDs(visible.map(heading => heading.id));
253
+ if (locked) return;
254
+ setActiveID((atBottom ? headings.at(-1) : passed.at(-1) ?? headings[0])?.id);
255
+ };
256
+ const schedule = () => {
257
+ frame ||= requestAnimationFrame(update);
258
+ };
259
+ const onIntent = event => {
260
+ if (!(event instanceof KeyboardEvent) || scrollKeys.has(event.key)) userScroll = true;
261
+ };
262
+ const onScroll = () => {
263
+ if (userScroll) locked = false;
264
+ userScroll = false;
265
+ schedule();
266
+ };
267
+ const observer = new ResizeObserver(schedule);
268
+ createEffect(() => {
269
+ headings = options.headingIDs().flatMap(id => document.getElementById(id) ?? []);
270
+ schedule();
271
+ });
272
+ observer.observe(document.body);
273
+ intentEvents.forEach(name => window.addEventListener(name, onIntent, {
274
+ passive: true
275
+ }));
276
+ window.addEventListener("scroll", onScroll, {
277
+ passive: true
278
+ });
279
+ window.addEventListener("hashchange", schedule);
280
+ onCleanup(() => {
281
+ cancelAnimationFrame(frame);
282
+ observer.disconnect();
283
+ intentEvents.forEach(name => window.removeEventListener(name, onIntent));
284
+ window.removeEventListener("scroll", onScroll);
285
+ window.removeEventListener("hashchange", schedule);
286
+ });
287
+ });
288
+ return {
289
+ activeID,
290
+ visibleIDs,
291
+ select: id => {
292
+ locked = true;
293
+ userScroll = false;
294
+ setActiveID(id);
295
+ }
296
+ };
297
+ };
298
+ const Context = createContext();
299
+ const useTableOfContents = () => {
300
+ const state = useContext(Context);
301
+ if (!state) throw new Error("TableOfContents parts must be inside TableOfContents.Root.");
302
+ return state;
303
+ };
304
+ const Root$4 = props => {
305
+ const [local, rest] = splitProps(props, ["headingIDs"]);
306
+ const state = createTableOfContents({
307
+ headingIDs: () => local.headingIDs
308
+ });
309
+ return _$createComponent(Context.Provider, {
310
+ value: state,
311
+ get children() {
312
+ return _$ssrElement("nav", _$mergeProps({
313
+ "aria-label": "On this page"
314
+ }, rest, {
315
+ "data-scope": "table-of-contents",
316
+ "data-part": "root"
317
+ }), undefined, true);
318
+ }
319
+ });
320
+ };
321
+ const List$1 = props => {
322
+ return _$ssrElement("ul", _$mergeProps(props, {
323
+ "data-scope": "table-of-contents",
324
+ "data-part": "list"
325
+ }), undefined, true);
326
+ };
327
+ const Item$1 = props => {
328
+ return _$ssrElement("li", _$mergeProps(props, {
329
+ "data-scope": "table-of-contents",
330
+ "data-part": "item"
331
+ }), undefined, true);
332
+ };
333
+ const Link$2 = props => {
334
+ const [local, rest] = splitProps(props, ["headingID"]);
335
+ const state = useTableOfContents();
336
+ const onClick = event => {
337
+ if (!(event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)) return;
338
+ state.select(local.headingID);
339
+ requestAnimationFrame(() => {
340
+ const heading = document.getElementById(local.headingID);
341
+ if (heading) focusTarget(heading);
342
+ });
343
+ };
344
+ return _$ssrElement("a", _$mergeProps(rest, {
345
+ get href() {
346
+ return `#${encodeURIComponent(local.headingID)}`;
347
+ },
348
+ get ["aria-current"]() {
349
+ return state.activeID() === local.headingID ? "location" : void 0;
350
+ },
351
+ get ["data-visible"]() {
352
+ return state.visibleIDs().includes(local.headingID) || void 0;
353
+ },
354
+ "data-scope": "table-of-contents",
355
+ "data-part": "link"
356
+ }), undefined, true);
357
+ };
358
+ const TableOfContents = {
359
+ Root: Root$4,
360
+ List: List$1,
361
+ Item: Item$1,
362
+ Link: Link$2
363
+ };
364
+ const createTableOfContentsGuide = options => {
365
+ const [shape, setShape] = createSignal();
366
+ const [stroke, setStroke] = createSignal();
367
+ const links = new Map();
368
+ const inset = options.inset ?? 6;
369
+ const radius = options.radius ?? 8;
370
+ const edgePoints = options.edgePoints ?? 16;
371
+ let track;
372
+ let marker;
373
+ onMount(() => {
374
+ let frame = 0;
375
+ const measure = () => {
376
+ const bounds = track?.getBoundingClientRect();
377
+ const segments = options.ids().flatMap(id => {
378
+ const rect = links.get(id)?.getBoundingClientRect();
379
+ const x = options.position(id);
380
+ return rect && bounds ? [{
381
+ id,
382
+ x,
383
+ top: rect.top - bounds.top,
384
+ bottom: rect.bottom - bounds.top
385
+ }] : [];
386
+ });
387
+ const highlighted = segments.filter(segment => {
388
+ return options.highlightedIDs().includes(segment.id);
389
+ });
390
+ const first = highlighted[0];
391
+ const last = highlighted.at(-1);
392
+ const width = bounds?.width ?? 0;
393
+ const path = segments.map((segment, index) => {
394
+ const previous = segments[index - 1];
395
+ const line = `L ${segment.x} ${segment.bottom - inset}`;
396
+ if (!previous) return `M ${segment.x} ${segment.top + inset} ${line}`;
397
+ if (previous.x === segment.x) return line;
398
+ const middle = (previous.bottom + segment.top) / 2;
399
+ return `C ${previous.x} ${middle} ${segment.x} ${middle} ${segment.x} ${segment.top + inset} ${line}`;
400
+ }).join(" ");
401
+ frame = 0;
402
+ setShape(segments.length ? {
403
+ path,
404
+ width,
405
+ range: first && last && {
406
+ top: first.top,
407
+ height: last.bottom - first.top
408
+ }
409
+ } : void 0);
410
+ };
411
+ const schedule = () => {
412
+ frame ||= requestAnimationFrame(measure);
413
+ };
414
+ const observer = new ResizeObserver(schedule);
415
+ createEffect(() => {
416
+ options.highlightedIDs();
417
+ schedule();
418
+ });
419
+ if (track) observer.observe(track);
420
+ onCleanup(() => {
421
+ cancelAnimationFrame(frame);
422
+ observer.disconnect();
423
+ });
424
+ });
425
+ createEffect(() => {
426
+ const current = shape();
427
+ if (!current || !marker) return setStroke(void 0);
428
+ marker.setAttribute("d", current.path);
429
+ const total = marker.getTotalLength();
430
+ const lengthAt = y => {
431
+ let low = 0;
432
+ let high = total;
433
+ for (let step = 0; step < 20; step += 1) {
434
+ const middle = (low + high) / 2;
435
+ if (marker.getPointAtLength(middle).y < y) low = middle;else high = middle;
436
+ }
437
+ return (low + high) / 2;
438
+ };
439
+ if (!current.range) return setStroke({
440
+ ...current,
441
+ offset: 0,
442
+ length: 0,
443
+ total
444
+ });
445
+ const {
446
+ top,
447
+ height
448
+ } = current.range;
449
+ const bottom = top + height;
450
+ const r = Math.min(radius, height / 2);
451
+ const xAt = y => marker.getPointAtLength(lengthAt(y)).x;
452
+ const edge = Array.from({
453
+ length: edgePoints
454
+ }, (_, index) => {
455
+ const y = bottom - r - (height - 2 * r) * index / (edgePoints - 1);
456
+ return `L ${xAt(y)} ${y}`;
457
+ });
458
+ const [xTop, xBottom] = [xAt(top + r), xAt(bottom - r)];
459
+ const backdrop = [`M ${xTop + r} ${top} L ${current.width - r} ${top}`, `Q ${current.width} ${top} ${current.width} ${top + r} L ${current.width} ${bottom - r}`, `Q ${current.width} ${bottom} ${current.width - r} ${bottom} L ${xBottom + r} ${bottom}`, `Q ${xBottom} ${bottom} ${xBottom} ${bottom - r}`, ...edge, `Q ${xTop} ${top} ${xTop + r} ${top} Z`].join(" ");
460
+ const start = lengthAt(top + inset);
461
+ const end = lengthAt(bottom - inset);
462
+ setStroke({
463
+ ...current,
464
+ offset: -start,
465
+ length: end - start,
466
+ total,
467
+ backdrop
468
+ });
469
+ });
470
+ createEffect(() => {
471
+ const viewport = options.viewport?.();
472
+ const first = links.get(options.highlightedIDs()[0] ?? "");
473
+ if (viewport && first) scrollIntoContainer(viewport, first, 12);
474
+ });
475
+ return {
476
+ stroke,
477
+ trackRef: element => track = element,
478
+ linkRef: (id, element) => links.set(id, element),
479
+ pathRef: element => marker = element
480
+ };
481
+ };
482
+ const enhanceMobileNavigation = (root, options = {}) => {
483
+ const trigger = root.querySelector(":scope > summary");
484
+ const content = root.querySelector(":scope > :not(summary)");
485
+ const media = options.mediaQuery ? window.matchMedia(options.mediaQuery) : void 0;
486
+ const inertElements = new Map();
487
+ let opened = false;
488
+ let previousOverflow = "";
489
+ if (!trigger || !content) throw new Error("Mobile navigation needs a summary and content.");
490
+ const setModal = modal => {
491
+ if (!modal) {
492
+ inertElements.forEach((inert, element) => element.inert = inert);
493
+ inertElements.clear();
494
+ document.body.style.overflow = previousOverflow;
495
+ return;
496
+ }
497
+ for (let branch = root; branch.parentElement; branch = branch.parentElement) for (const sibling of branch.parentElement.children) {
498
+ if (sibling === branch || !(sibling instanceof HTMLElement)) continue;
499
+ inertElements.set(sibling, sibling.inert);
500
+ sibling.inert = true;
501
+ }
502
+ previousOverflow = document.body.style.overflow;
503
+ document.body.style.overflow = "hidden";
504
+ };
505
+ const sync = () => {
506
+ const open = root.open && (media?.matches ?? true);
507
+ if (root.open && !open) root.open = false;
508
+ if (opened === open) return;
509
+ opened = open;
510
+ setModal(open);
511
+ if (open) (getFocusable(content)[0] ?? trigger).focus({
512
+ preventScroll: true
513
+ });
514
+ options.onOpenChange?.(open);
515
+ };
516
+ const close = () => {
517
+ root.open = false;
518
+ sync();
519
+ };
520
+ const onToggle = event => {
521
+ if (event.target !== root) return;
522
+ const hadFocus = content.contains(document.activeElement);
523
+ sync();
524
+ if (!opened && hadFocus) trigger.focus({
525
+ preventScroll: true
526
+ });
527
+ };
528
+ const onKeyDown = event => {
529
+ if (!opened || event.defaultPrevented) return;
530
+ if (event.key === "Escape") {
531
+ event.preventDefault();
532
+ close();
533
+ trigger.focus({
534
+ preventScroll: true
535
+ });
536
+ }
537
+ if (event.key !== "Tab") return;
538
+ const items = [trigger, ...getFocusable(content)];
539
+ const edge = event.shiftKey ? items[0] : items.at(-1);
540
+ if (document.activeElement !== edge) return;
541
+ event.preventDefault();
542
+ (event.shiftKey ? items.at(-1) : items[0])?.focus();
543
+ };
544
+ const onClick = event => {
545
+ const link = event.target.closest("a[href]");
546
+ const isPlainClick = event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
547
+ if (!(opened && isPlainClick && link !== null && content.contains(link))) return;
548
+ const url = new URL(link.href);
549
+ const samePage = url.origin === location.origin && url.pathname === location.pathname;
550
+ close();
551
+ if (samePage) requestAnimationFrame(() => {
552
+ const target = url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1))) || options.navigationTarget?.();
553
+ if (target) focusTarget(target);else trigger.focus({
554
+ preventScroll: true
555
+ });
556
+ });
557
+ };
558
+ root.addEventListener("toggle", onToggle);
559
+ root.addEventListener("keydown", onKeyDown);
560
+ root.addEventListener("click", onClick);
561
+ media?.addEventListener("change", sync);
562
+ sync();
563
+ return () => {
564
+ root.removeEventListener("toggle", onToggle);
565
+ root.removeEventListener("keydown", onKeyDown);
566
+ root.removeEventListener("click", onClick);
567
+ media?.removeEventListener("change", sync);
568
+ if (opened) setModal(false);
569
+ };
570
+ };
571
+ const Root$3 = props => {
572
+ const [local, rest] = splitProps(props, ["mediaQuery", "navigationTarget", "onOpenChange"]);
573
+ let root;
574
+ onMount(() => onCleanup(enhanceMobileNavigation(root, local)));
575
+ return _$ssrElement("details", _$mergeProps(rest, {
576
+ "data-scope": "mobile-navigation",
577
+ "data-part": "root"
578
+ }), undefined, true);
579
+ };
580
+ const Trigger = props => {
581
+ return _$ssrElement("summary", _$mergeProps(props, {
582
+ "data-scope": "mobile-navigation",
583
+ "data-part": "trigger"
584
+ }), undefined, true);
585
+ };
586
+ const Content = props => {
587
+ return _$ssrElement("div", _$mergeProps(props, {
588
+ "data-scope": "mobile-navigation",
589
+ "data-part": "content"
590
+ }), undefined, true);
591
+ };
592
+ const MobileNavigation = {
593
+ Root: Root$3,
594
+ Trigger,
595
+ Content
596
+ };
597
+ const Root$2 = props => {
598
+ return _$ssrElement("nav", _$mergeProps({
599
+ "aria-label": "Breadcrumbs"
600
+ }, props, {
601
+ "data-scope": "breadcrumbs",
602
+ "data-part": "root"
603
+ }), undefined, true);
604
+ };
605
+ const List = props => {
606
+ return _$ssrElement("ol", _$mergeProps(props, {
607
+ "data-scope": "breadcrumbs",
608
+ "data-part": "list"
609
+ }), undefined, true);
610
+ };
611
+ const Item = props => {
612
+ return _$ssrElement("li", _$mergeProps(props, {
613
+ "data-scope": "breadcrumbs",
614
+ "data-part": "item"
615
+ }), undefined, true);
616
+ };
617
+ const Link$1 = props => {
618
+ return _$ssrElement("a", _$mergeProps(props, {
619
+ "data-scope": "breadcrumbs",
620
+ "data-part": "link"
621
+ }), undefined, true);
622
+ };
623
+ const Label = props => {
624
+ return _$ssrElement("span", _$mergeProps(props, {
625
+ "data-scope": "breadcrumbs",
626
+ "data-part": "label"
627
+ }), undefined, true);
628
+ };
629
+ const Separator = props => {
630
+ return _$ssrElement("span", _$mergeProps({
631
+ "aria-hidden": "true"
632
+ }, props, {
633
+ "data-scope": "breadcrumbs",
634
+ "data-part": "separator"
635
+ }), undefined, true);
636
+ };
637
+ const Breadcrumbs = {
638
+ Root: Root$2,
639
+ List,
640
+ Item,
641
+ Link: Link$1,
642
+ Label,
643
+ Separator
644
+ };
645
+ const Root$1 = props => {
646
+ return _$ssrElement("nav", _$mergeProps({
647
+ "aria-label": "Pages"
648
+ }, props, {
649
+ "data-scope": "pagination",
650
+ "data-part": "root"
651
+ }), undefined, true);
652
+ };
653
+ const Link = props => {
654
+ const [local, rest] = splitProps(props, ["direction"]);
655
+ return _$ssrElement("a", _$mergeProps(rest, {
656
+ get rel() {
657
+ return local.direction === "previous" ? "prev" : "next";
658
+ },
659
+ "data-scope": "pagination",
660
+ "data-part": "link",
661
+ get ["data-direction"]() {
662
+ return local.direction;
663
+ }
664
+ }), undefined, true);
665
+ };
666
+ const Pagination = {
667
+ Root: Root$1,
668
+ Link
669
+ };
670
+ const syncEvent = "andesine:tabs";
671
+ const Root = props => {
672
+ const [local, rest] = splitProps(props, ["values", "defaultValue", "syncKey", "onValueChange"]);
673
+ const [value, setValue] = createSignal(local.defaultValue ?? local.values[0] ?? "");
674
+ const storageKey = () => `andesine-tabs:${local.syncKey}`;
675
+ const select = next => {
676
+ if (typeof next === "string" && local.values.includes(next)) setValue(next);
677
+ };
678
+ onMount(() => {
679
+ if (!local.syncKey) return;
680
+ const sync = event => {
681
+ const {
682
+ key,
683
+ value
684
+ } = event.detail;
685
+ if (key === local.syncKey) select(value);
686
+ };
687
+ select(readSession(storageKey()));
688
+ window.addEventListener(syncEvent, sync);
689
+ onCleanup(() => window.removeEventListener(syncEvent, sync));
690
+ });
691
+ return _$createComponent(Tabs$1.Root, _$mergeProps(rest, {
692
+ get value() {
693
+ return value();
694
+ },
695
+ onValueChange: details => {
696
+ setValue(details.value);
697
+ local.onValueChange?.(details);
698
+ if (!local.syncKey) return;
699
+ writeSession(storageKey(), details.value);
700
+ window.dispatchEvent(new CustomEvent(syncEvent, {
701
+ detail: {
702
+ key: local.syncKey,
703
+ value: details.value
704
+ }
705
+ }));
706
+ }
707
+ }));
708
+ };
709
+ const Tabs = {
710
+ Root,
711
+ List: Tabs$1.List,
712
+ Trigger: Tabs$1.Trigger,
713
+ Content: Tabs$1.Content,
714
+ Indicator: Tabs$1.Indicator
715
+ };
716
+ const createClipboard = (timeout = 2e3) => {
717
+ const [status, setStatus] = createSignal();
718
+ let timer;
719
+ onCleanup(() => clearTimeout(timer));
720
+ return {
721
+ status,
722
+ copy: async text => {
723
+ clearTimeout(timer);
724
+ try {
725
+ if (typeof text !== "string" && typeof ClipboardItem !== "undefined") {
726
+ const blob = text.then(value => new Blob([value], {
727
+ type: "text/plain"
728
+ }));
729
+ await navigator.clipboard.write([new ClipboardItem({
730
+ "text/plain": blob
731
+ })]);
732
+ } else await navigator.clipboard.writeText(await text);
733
+ setStatus(true);
734
+ } catch {
735
+ setStatus(false);
736
+ }
737
+ timer = setTimeout(() => setStatus(void 0), timeout);
738
+ }
739
+ };
740
+ };
741
+ const createSearch = options => {
742
+ const [query, setQueryValue] = createSignal("");
743
+ const [results, setResults] = createSignal();
744
+ const [status, setStatus] = createSignal("idle");
745
+ const [error, setError] = createSignal();
746
+ let timer;
747
+ let controller;
748
+ const cancel = () => {
749
+ clearTimeout(timer);
750
+ controller?.abort();
751
+ controller = void 0;
752
+ if (status() === "loading") setStatus(results() ? "done" : "idle");
753
+ };
754
+ const run = async value => {
755
+ const current = new AbortController();
756
+ controller = current;
757
+ try {
758
+ const found = await options.search(value, current.signal);
759
+ if (controller !== current) return;
760
+ setResults(() => found);
761
+ setStatus("done");
762
+ } catch (reason) {
763
+ if (controller !== current || current.signal.aborted) return;
764
+ setError(reason);
765
+ setStatus("error");
766
+ }
767
+ };
768
+ onCleanup(cancel);
769
+ return {
770
+ query,
771
+ results,
772
+ status,
773
+ error,
774
+ cancel,
775
+ setQuery: value => {
776
+ cancel();
777
+ setQueryValue(value);
778
+ if (!value.trim()) {
779
+ setResults(void 0);
780
+ setStatus("idle");
781
+ return;
782
+ }
783
+ setStatus("loading");
784
+ timer = setTimeout(() => run(value.trim()), options.delay ?? 200);
785
+ }
786
+ };
787
+ };
788
+ const createListNavigation = options => {
789
+ const [active, setActive] = createSignal(0);
790
+ const moves = {
791
+ ArrowDown: (index, count) => (index + 1) % count,
792
+ ArrowUp: (index, count) => (index - 1 + count) % count,
793
+ Home: () => 0,
794
+ End: (_, count) => count - 1
795
+ };
796
+ return {
797
+ active,
798
+ setActive,
799
+ onKeyDown: event => {
800
+ const count = options.count();
801
+ const move = moves[event.key];
802
+ if (!count || event.isComposing) return;
803
+ if (event.key === "Enter") {
804
+ event.preventDefault();
805
+ options.onSelect(Math.min(active(), count - 1));
806
+ }
807
+ if (!move) return;
808
+ event.preventDefault();
809
+ setActive(move(Math.min(active(), count - 1), count));
810
+ }
811
+ };
812
+ };
813
+ const createAnswer = options => {
814
+ const [turns, setTurns] = createSignal([]);
815
+ let controller;
816
+ const pending = () => turns().at(-1)?.status === "loading";
817
+ const updateLast = (current, turn) => {
818
+ if (controller !== current) return;
819
+ setTurns(list => [...list.slice(0, -1), {
820
+ ...list.at(-1),
821
+ ...turn
822
+ }]);
823
+ };
824
+ const cancel = () => {
825
+ controller?.abort();
826
+ controller = void 0;
827
+ if (pending()) setTurns(list => list.slice(0, -1));
828
+ };
829
+ const run = async question => {
830
+ const current = new AbortController();
831
+ cancel();
832
+ controller = current;
833
+ const history = turns().filter(turn => turn.status === "done");
834
+ setTurns([...history, {
835
+ question,
836
+ status: "loading"
837
+ }]);
838
+ try {
839
+ const answer = await options.ask(question, history, current.signal, partial => {
840
+ return updateLast(current, {
841
+ answer: partial
842
+ });
843
+ });
844
+ updateLast(current, {
845
+ answer,
846
+ status: "done"
847
+ });
848
+ } catch (error) {
849
+ if (current.signal.aborted) return;
850
+ updateLast(current, {
851
+ status: "error",
852
+ error
853
+ });
854
+ } finally {
855
+ if (controller === current) controller = void 0;
856
+ }
857
+ };
858
+ onCleanup(cancel);
859
+ return {
860
+ turns,
861
+ pending,
862
+ cancel,
863
+ ask: question => {
864
+ run(question).catch(() => {});
865
+ },
866
+ reset: () => {
867
+ cancel();
868
+ setTurns([]);
869
+ }
870
+ };
871
+ };
872
+ const isApplePlatform = () => {
873
+ return typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.userAgent);
874
+ };
875
+ const createSearchPalette = options => {
876
+ const [open, setOpen] = createSignal(false);
877
+ const [isApple, setIsApple] = createSignal(true);
878
+ const [input, setInput] = createSignal();
879
+ const search = createSearch({
880
+ search: options.search,
881
+ delay: options.delay ?? 250
882
+ });
883
+ const answer = options.ask && createAnswer({
884
+ ask: options.ask
885
+ });
886
+ const items = () => {
887
+ const results = search.results();
888
+ return results === void 0 ? [] : options.items(results);
889
+ };
890
+ const canAsk = () => Boolean(answer && search.query().trim());
891
+ const showingAnswer = () => {
892
+ return !search.query().trim() && Boolean(answer && answer.turns().length);
893
+ };
894
+ const openItem = item => {
895
+ setOpen(false);
896
+ window.location.assign(item.href);
897
+ };
898
+ const askQuery = () => {
899
+ const question = search.query().trim();
900
+ if (!question || !answer) return;
901
+ follow = true;
902
+ answer.ask(question);
903
+ search.setQuery("");
904
+ };
905
+ const navigation = createListNavigation({
906
+ count: () => search.status() === "loading" ? 0 : items().length + (canAsk() ? 1 : 0),
907
+ onSelect: index => {
908
+ const item = items()[index];
909
+ if (item) openItem(item);else askQuery();
910
+ }
911
+ });
912
+ let results;
913
+ let resumeSearch = false;
914
+ let follow = true;
915
+ createEffect(() => {
916
+ search.results();
917
+ navigation.setActive(0);
918
+ });
919
+ createEffect(on(open, isOpen => {
920
+ if (isOpen) {
921
+ if (resumeSearch) search.setQuery(search.query());
922
+ resumeSearch = false;
923
+ return;
924
+ }
925
+ resumeSearch = search.status() === "loading";
926
+ search.cancel();
927
+ answer?.cancel();
928
+ }));
929
+ createEffect(() => {
930
+ answer?.turns();
931
+ if (follow && results) queueMicrotask(() => results.scrollTo({
932
+ top: results.scrollHeight
933
+ }));
934
+ });
935
+ onMount(() => {
936
+ const url = new URL(window.location.href);
937
+ const param = options.queryParam ?? "q";
938
+ const query = url.searchParams.get(param)?.trim();
939
+ const onShortcut = event => {
940
+ if (!(event.key.toLowerCase() === "k" && (event.metaKey || event.ctrlKey))) return;
941
+ event.preventDefault();
942
+ setOpen(true);
943
+ };
944
+ const onClick = event => {
945
+ if (event.target.closest(options.triggerSelector ?? "[data-search-trigger]")) setOpen(true);
946
+ };
947
+ setIsApple(isApplePlatform());
948
+ if (query) {
949
+ url.searchParams.delete(param);
950
+ window.history.replaceState(window.history.state, "", url);
951
+ search.setQuery(query.slice(0, options.maxLength ?? 500));
952
+ setOpen(true);
953
+ }
954
+ document.addEventListener("keydown", onShortcut);
955
+ document.addEventListener("click", onClick);
956
+ onCleanup(() => {
957
+ document.removeEventListener("keydown", onShortcut);
958
+ document.removeEventListener("click", onClick);
959
+ });
960
+ });
961
+ return {
962
+ open,
963
+ setOpen,
964
+ search,
965
+ answer,
966
+ items,
967
+ canAsk,
968
+ showingAnswer,
969
+ navigation,
970
+ isApple,
971
+ askQuery,
972
+ openItem,
973
+ input,
974
+ inputRef: setInput,
975
+ resultsRef: element => {
976
+ results = element;
977
+ element.addEventListener("scroll", () => {
978
+ follow = element.scrollHeight - element.scrollTop - element.clientHeight < 48;
979
+ });
980
+ },
981
+ onInputKeyDown: event => {
982
+ const isTouch = window.matchMedia("(hover: none) and (pointer: coarse)").matches;
983
+ if (event.key === "Enter" && isTouch) {
984
+ event.preventDefault();
985
+ input()?.blur();
986
+ return;
987
+ }
988
+ if (showingAnswer()) return;
989
+ navigation.onKeyDown(event);
990
+ queueMicrotask(() => {
991
+ results?.querySelector(`[data-search-result="${navigation.active()}"]`)?.scrollIntoView({
992
+ block: "nearest"
993
+ });
994
+ });
995
+ }
996
+ };
997
+ };
998
+ const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
999
+ const getMatchTerms = query => {
1000
+ const phrase = query.trim().toLowerCase();
1001
+ if (!phrase) return [];
1002
+ return [...new Set([phrase, ...phrase.split(/\s+/).filter(term => term.length >= 2)])].sort((a, b) => b.length - a.length);
1003
+ };
1004
+ const matchesQuery = (text, query) => {
1005
+ return getMatchTerms(query).some(term => text.toLowerCase().includes(term));
1006
+ };
1007
+ const splitMatches = (text, query) => {
1008
+ const terms = getMatchTerms(query);
1009
+ if (!terms.length) return [{
1010
+ text,
1011
+ match: false
1012
+ }];
1013
+ return text.split(new RegExp(`(${terms.map(escapeRegExp).join("|")})`, "giu")).filter(Boolean).map(part => ({
1014
+ text: part,
1015
+ match: terms.includes(part.toLowerCase())
1016
+ }));
1017
+ };
1018
+ const getMatchPreview = (text, query, options = {}) => {
1019
+ const lower = text.toLowerCase();
1020
+ const matches = getMatchTerms(query).map(term => lower.indexOf(term)).filter(index => index >= 0);
1021
+ const start = matches.length ? Math.max(Math.min(...matches) - (options.prefix ?? 24), 0) : 0;
1022
+ const end = Math.min(start + (options.length ?? 180), text.length);
1023
+ return `${start > 0 ? "…" : ""}${text.slice(start, end).trim()}${end < text.length ? "…" : ""}`;
1024
+ };
1025
+ const createPageActions = options => {
1026
+ const [item, setItem] = createSignal("link");
1027
+ const clipboard = createClipboard(options.timeout);
1028
+ let markdown;
1029
+ const loadMarkdown = () => {
1030
+ markdown ??= fetch(options.markdown).then(response => {
1031
+ if (!response.ok) throw new Error(`Markdown request failed (${response.status}).`);
1032
+ return response.text();
1033
+ });
1034
+ markdown.catch(() => markdown = void 0);
1035
+ return markdown;
1036
+ };
1037
+ const result = () => {
1038
+ const success = clipboard.status();
1039
+ return success === void 0 ? void 0 : {
1040
+ item: item(),
1041
+ success
1042
+ };
1043
+ };
1044
+ return {
1045
+ result,
1046
+ copyLink: () => {
1047
+ setItem("link");
1048
+ return clipboard.copy(window.location.href);
1049
+ },
1050
+ copyMarkdown: () => {
1051
+ setItem("markdown");
1052
+ return clipboard.copy(loadMarkdown());
1053
+ }
1054
+ };
1055
+ };
1056
+ const createScrollMemory = options => {
1057
+ onMount(() => {
1058
+ const container = options.container();
1059
+ const current = options.current?.();
1060
+ if (!container) return;
1061
+ const save = () => writeSession(options.key, container.scrollTop);
1062
+ container.scrollTop = Number(readSession(options.key)) || 0;
1063
+ if (current) scrollIntoContainer(container, current);
1064
+ container.addEventListener("scroll", save, {
1065
+ passive: true
1066
+ });
1067
+ onCleanup(() => container.removeEventListener("scroll", save));
1068
+ });
1069
+ };
1070
+ const createScrollEdges = () => {
1071
+ const [start, setStart] = createSignal(false);
1072
+ const [end, setEnd] = createSignal(false);
1073
+ onMount(() => {
1074
+ const root = document.documentElement;
1075
+ const update = () => {
1076
+ setStart(root.scrollTop > 0);
1077
+ setEnd(root.scrollHeight - root.clientHeight - root.scrollTop >= 10);
1078
+ };
1079
+ const observer = new ResizeObserver(update);
1080
+ observer.observe(document.body);
1081
+ window.addEventListener("scroll", update, {
1082
+ passive: true
1083
+ });
1084
+ update();
1085
+ onCleanup(() => {
1086
+ observer.disconnect();
1087
+ window.removeEventListener("scroll", update);
1088
+ });
1089
+ });
1090
+ return {
1091
+ start,
1092
+ end
1093
+ };
1094
+ };
1095
+ const createPageScrollbar = (options = {}) => {
1096
+ const [metrics, setMetrics] = createSignal();
1097
+ const [scrolling, setScrolling] = createSignal(false);
1098
+ const [near, setNear] = createSignal(false);
1099
+ const [dragging, setDragging] = createSignal(false);
1100
+ const inset = options.inset ?? 4;
1101
+ const interactive = () => near() || dragging();
1102
+ let dragStart = 0;
1103
+ let scrollStart = 0;
1104
+ const scrollTo = top => window.scrollTo({
1105
+ top,
1106
+ behavior: "instant"
1107
+ });
1108
+ const stopDragging = () => {
1109
+ setDragging(false);
1110
+ };
1111
+ onMount(() => {
1112
+ const edgeZone = options.edgeZone ?? 16;
1113
+ const minThumbHeight = options.minThumbHeight ?? 24;
1114
+ let frame = 0;
1115
+ let timer;
1116
+ const measure = () => {
1117
+ const root = document.documentElement;
1118
+ const viewport = window.innerHeight;
1119
+ const maximum = root.scrollHeight - viewport;
1120
+ const locked = getComputedStyle(document.body).overflow === "hidden";
1121
+ const track = viewport - inset * 2;
1122
+ const height = Math.max(minThumbHeight, track * viewport / root.scrollHeight);
1123
+ frame = 0;
1124
+ setMetrics(maximum > 0 && !locked ? {
1125
+ top: inset + window.scrollY / maximum * (track - height),
1126
+ height,
1127
+ ratio: maximum / (track - height)
1128
+ } : void 0);
1129
+ };
1130
+ const schedule = () => {
1131
+ frame ||= requestAnimationFrame(measure);
1132
+ };
1133
+ const onScroll = () => {
1134
+ schedule();
1135
+ setScrolling(true);
1136
+ clearTimeout(timer);
1137
+ timer = setTimeout(() => setScrolling(false), options.hideDelay ?? 1e3);
1138
+ };
1139
+ const onPointerMove = event => {
1140
+ setNear(event.pointerType === "mouse" && event.clientX >= window.innerWidth - edgeZone);
1141
+ };
1142
+ const observer = new ResizeObserver(schedule);
1143
+ const styleObserver = new MutationObserver(schedule);
1144
+ observer.observe(document.body);
1145
+ styleObserver.observe(document.body, {
1146
+ attributes: true,
1147
+ attributeFilter: ["style"]
1148
+ });
1149
+ window.addEventListener("scroll", onScroll, {
1150
+ passive: true
1151
+ });
1152
+ window.addEventListener("resize", schedule);
1153
+ window.addEventListener("pointermove", onPointerMove, {
1154
+ passive: true
1155
+ });
1156
+ measure();
1157
+ onCleanup(() => {
1158
+ cancelAnimationFrame(frame);
1159
+ clearTimeout(timer);
1160
+ observer.disconnect();
1161
+ styleObserver.disconnect();
1162
+ window.removeEventListener("scroll", onScroll);
1163
+ window.removeEventListener("resize", schedule);
1164
+ window.removeEventListener("pointermove", onPointerMove);
1165
+ });
1166
+ });
1167
+ return {
1168
+ thumb: () => {
1169
+ const current = metrics();
1170
+ return current && {
1171
+ top: current.top,
1172
+ height: current.height
1173
+ };
1174
+ },
1175
+ visible: () => Boolean(metrics() && (scrolling() || interactive())),
1176
+ interactive,
1177
+ onTrackPointerDown: event => {
1178
+ const current = metrics();
1179
+ if (event.button !== 0 || !current) return;
1180
+ scrollTo((event.clientY - inset - current.height / 2) * current.ratio);
1181
+ },
1182
+ thumbHandlers: {
1183
+ onPointerDown: event => {
1184
+ if (event.button !== 0) return;
1185
+ event.preventDefault();
1186
+ event.stopPropagation();
1187
+ event.currentTarget.setPointerCapture(event.pointerId);
1188
+ dragStart = event.clientY;
1189
+ scrollStart = window.scrollY;
1190
+ setDragging(true);
1191
+ },
1192
+ onPointerMove: event => {
1193
+ const current = metrics();
1194
+ if (dragging() && current) scrollTo(scrollStart + (event.clientY - dragStart) * current.ratio);
1195
+ },
1196
+ onPointerUp: stopDragging,
1197
+ onPointerCancel: stopDragging,
1198
+ onLostPointerCapture: stopDragging
1199
+ }
1200
+ };
1201
+ };
1202
+ const createTabIndicator = options => {
1203
+ const [position, setPosition] = createSignal();
1204
+ const tabs = new Map();
1205
+ let root;
1206
+ let scroll;
1207
+ onMount(() => {
1208
+ let frame = 0;
1209
+ const measure = () => {
1210
+ const rootBounds = root?.getBoundingClientRect();
1211
+ const tabBounds = tabs.get(options.current())?.getBoundingClientRect();
1212
+ frame = 0;
1213
+ setPosition(rootBounds && tabBounds && {
1214
+ left: tabBounds.left - rootBounds.left,
1215
+ width: tabBounds.width
1216
+ });
1217
+ };
1218
+ const schedule = () => {
1219
+ frame ||= requestAnimationFrame(measure);
1220
+ };
1221
+ const observer = new ResizeObserver(schedule);
1222
+ tabs.get(options.current())?.scrollIntoView({
1223
+ block: "nearest",
1224
+ inline: "nearest"
1225
+ });
1226
+ if (root) observer.observe(root);
1227
+ scroll?.addEventListener("scroll", schedule, {
1228
+ passive: true
1229
+ });
1230
+ schedule();
1231
+ onCleanup(() => {
1232
+ cancelAnimationFrame(frame);
1233
+ observer.disconnect();
1234
+ scroll?.removeEventListener("scroll", schedule);
1235
+ });
1236
+ });
1237
+ return {
1238
+ position,
1239
+ rootRef: element => root = element,
1240
+ scrollRef: element => scroll = element,
1241
+ tabRef: (index, element) => tabs.set(index, element)
1242
+ };
1243
+ };
1244
+ export { Breadcrumbs, MobileNavigation, NavigationTree, Pagination, TableOfContents, Tabs, createAnswer, createClipboard, createListNavigation, createNavigationTree, createPageActions, createPageScrollbar, createScrollEdges, createScrollMemory, createSearch, createSearchPalette, createTabIndicator, createTableOfContents, createTableOfContentsGuide, enhanceMobileNavigation, getMatchPreview, getMatchTerms, matchesQuery, splitMatches, useTableOfContents };