@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.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/solid/client/index.js +1354 -0
- package/dist/solid/server/index.js +1244 -0
- package/dist/solid/source/index.jsx +1166 -0
- package/dist/solid/types/index.d.ts +432 -0
- package/package.json +65 -0
|
@@ -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 };
|