@willa-ui/shared 0.0.3 → 0.0.4

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/dist/index.js CHANGED
@@ -1,9 +1,24 @@
1
1
  /*!
2
- * @willa-ui/shared.js v0.0.3
2
+ * @willa-ui/shared.js v0.0.4
3
3
  * (c) 2026 chentao.arthur
4
4
  */
5
- import { isValidElement } from "react";
5
+ import {
6
+ isValidElement,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
13
+ import hljs from "highlight.js";
6
14
  import { isArray, isNil, isString } from "aidly";
15
+ //#region src/css.ts
16
+ function formatCssSize(value) {
17
+ if (typeof value === "number")
18
+ return Number.isFinite(value) ? `${value}px` : void 0;
19
+ if (typeof value === "string") return value.trim() || void 0;
20
+ }
21
+ //#endregion
7
22
  //#region src/clipboard.ts
8
23
  async function copyToClipboard(text) {
9
24
  if (!text) return false;
@@ -29,6 +44,510 @@ async function copyToClipboard(text) {
29
44
  }
30
45
  }
31
46
  //#endregion
47
+ //#region src/controllableState.ts
48
+ function useControllableState(options) {
49
+ const { value, defaultValue, onChange } = options;
50
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
51
+ const controlled = value !== void 0;
52
+ const currentValue = controlled ? value : uncontrolledValue;
53
+ return [
54
+ currentValue,
55
+ useCallback(
56
+ (nextValue) => {
57
+ const resolvedValue = resolveStateAction(nextValue, currentValue);
58
+ if (!controlled) setUncontrolledValue(resolvedValue);
59
+ onChange?.(resolvedValue);
60
+ },
61
+ [controlled, currentValue, onChange],
62
+ ),
63
+ controlled,
64
+ ];
65
+ }
66
+ const resolveStateAction = (value, previousValue) => {
67
+ if (typeof value === "function") return value(previousValue);
68
+ return value;
69
+ };
70
+ //#endregion
71
+ //#region src/viewport.ts
72
+ const MOBILE_BREAKPOINT = 640;
73
+ function isMobileViewport(viewportWidth) {
74
+ return viewportWidth <= 640;
75
+ }
76
+ function isMobile() {
77
+ if (typeof window === "undefined") return false;
78
+ return isMobileViewport(window.innerWidth);
79
+ }
80
+ //#endregion
81
+ //#region src/copy.ts
82
+ function useCopyToClipboard(options = {}) {
83
+ const { resetDuration = 1200, onCopy } = options;
84
+ const [status, setStatus] = useState("idle");
85
+ const timerRef = useRef(null);
86
+ const clearTimer = useCallback(() => {
87
+ if (timerRef.current === null) return;
88
+ window.clearTimeout(timerRef.current);
89
+ timerRef.current = null;
90
+ }, []);
91
+ const startFeedback = useCallback(
92
+ (nextStatus, duration = resetDuration) => {
93
+ clearTimer();
94
+ setStatus(nextStatus);
95
+ if (nextStatus === "idle" || duration <= 0) return;
96
+ timerRef.current = window.setTimeout(() => {
97
+ setStatus("idle");
98
+ timerRef.current = null;
99
+ }, duration);
100
+ },
101
+ [clearTimer, resetDuration],
102
+ );
103
+ const reset = useCallback(() => {
104
+ startFeedback("idle", 0);
105
+ }, [startFeedback]);
106
+ const copy = useCallback(
107
+ async (text, actionOptions = {}) => {
108
+ startFeedback("idle", 0);
109
+ const ok = await copyToClipboard(text);
110
+ startFeedback(ok ? "copied" : "failed", actionOptions.resetDuration);
111
+ if (ok) {
112
+ onCopy?.(text);
113
+ actionOptions.onCopy?.(text);
114
+ }
115
+ return ok;
116
+ },
117
+ [onCopy, startFeedback],
118
+ );
119
+ useEffect(() => {
120
+ return () => {
121
+ clearTimer();
122
+ };
123
+ }, [clearTimer]);
124
+ return {
125
+ status,
126
+ copied: status === "copied",
127
+ failed: status === "failed",
128
+ copy,
129
+ reset,
130
+ };
131
+ }
132
+ //#endregion
133
+ //#region src/codeHighlight.ts
134
+ const parseCodeMeta = (className) => {
135
+ const [rawLanguage = "text", rawMeta = ""] = (
136
+ /language-([^\s]+)/.exec(className ?? "")?.[1] ?? "text"
137
+ ).split("--meta-");
138
+ const highlightLines = /* @__PURE__ */ new Set();
139
+ const normalizedMeta = rawMeta.replace(/_/g, " ");
140
+ const showLineNumbers = /(?:^|[^A-Za-z0-9-])ln(?:$|[^A-Za-z0-9-])/.test(
141
+ normalizedMeta,
142
+ );
143
+ for (const match of rawMeta.matchAll(/\{([^}]+)\}/g))
144
+ addCodeHighlightRange(highlightLines, match[1]);
145
+ return {
146
+ rawLanguage,
147
+ highlightLines,
148
+ showLineNumbers,
149
+ };
150
+ };
151
+ const normalizeHljsLanguage = (language) => {
152
+ const key = language.toLowerCase();
153
+ return (
154
+ {
155
+ c: "c",
156
+ cc: "cpp",
157
+ "c++": "cpp",
158
+ cpp: "cpp",
159
+ cxx: "cpp",
160
+ css: "css",
161
+ go: "go",
162
+ golang: "go",
163
+ html: "xml",
164
+ js: "javascript",
165
+ jsx: "javascript",
166
+ rs: "rust",
167
+ rust: "rust",
168
+ ts: "typescript",
169
+ tsx: "typescript",
170
+ sh: "bash",
171
+ bash: "bash",
172
+ shell: "bash",
173
+ yml: "yaml",
174
+ md: "markdown",
175
+ }[key] ?? key
176
+ );
177
+ };
178
+ const highlightCodeToHtml = (code, rawLanguage) => {
179
+ const language = normalizeHljsLanguage(rawLanguage);
180
+ if (hljs.getLanguage(language))
181
+ return {
182
+ html: hljs.highlight(code, { language }).value,
183
+ display: rawLanguage,
184
+ };
185
+ const result = hljs.highlightAuto(code);
186
+ return {
187
+ html: result.value,
188
+ display: result.language ?? rawLanguage,
189
+ };
190
+ };
191
+ const createCodeHighlightLines = (ranges) => {
192
+ const highlightLines = /* @__PURE__ */ new Set();
193
+ for (const range of ranges ?? [])
194
+ if (typeof range === "number") {
195
+ if (!Number.isInteger(range) || range <= 0) continue;
196
+ highlightLines.add(range);
197
+ } else addCodeHighlightRange(highlightLines, range.join("-"));
198
+ return highlightLines;
199
+ };
200
+ const addCodeHighlightRange = (highlightLines, value) => {
201
+ for (const part of value.split(",")) {
202
+ const rangeMatch = /^\s*(\d+)(?:-(\d+))?\s*$/.exec(part);
203
+ if (!rangeMatch) continue;
204
+ const start = Number(rangeMatch[1]);
205
+ const end = Number(rangeMatch[2] ?? rangeMatch[1]);
206
+ if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
207
+ for (let line = start; line <= end; line += 1)
208
+ if (line > 0) highlightLines.add(line);
209
+ }
210
+ };
211
+ //#endregion
212
+ //#region src/dom.ts
213
+ const focusableSelector = [
214
+ "a[href]",
215
+ "button:not([disabled])",
216
+ "textarea:not([disabled])",
217
+ "input:not([disabled])",
218
+ "select:not([disabled])",
219
+ "[tabindex]:not([tabindex='-1'])",
220
+ ].join(",");
221
+ function getFocusableElements(container) {
222
+ return Array.from(container.querySelectorAll(focusableSelector));
223
+ }
224
+ //#endregion
225
+ //#region src/number.ts
226
+ function clampNumber(value, min, max) {
227
+ if (max < min) return min;
228
+ return Math.min(Math.max(value, min), max);
229
+ }
230
+ function createNumberRange(start, end) {
231
+ if (end < start) return [];
232
+ return Array.from({ length: end - start + 1 }, (_, index) => start + index);
233
+ }
234
+ //#endregion
235
+ //#region src/floating.ts
236
+ function getFloatingPanelPosition(options) {
237
+ const {
238
+ anchorRect,
239
+ anchorPoint,
240
+ floatingRect,
241
+ side = "bottom",
242
+ align = "start",
243
+ offset = 0,
244
+ viewportWidth,
245
+ viewportHeight,
246
+ viewportPadding = 8,
247
+ matchAnchorMinWidth = false,
248
+ } = options;
249
+ const resolvedViewportWidth =
250
+ viewportWidth ?? (typeof window === "undefined" ? 0 : window.innerWidth);
251
+ const resolvedViewportHeight =
252
+ viewportHeight ?? (typeof window === "undefined" ? 0 : window.innerHeight);
253
+ const floatingWidth = floatingRect.width;
254
+ const floatingHeight = floatingRect.height;
255
+ const basePosition = anchorPoint
256
+ ? {
257
+ top: anchorPoint.y,
258
+ left: anchorPoint.x,
259
+ }
260
+ : getAnchoredPanelPosition({
261
+ anchorRect:
262
+ anchorRect ??
263
+ createPointRect({
264
+ x: 0,
265
+ y: 0,
266
+ }),
267
+ floatingRect,
268
+ side,
269
+ align,
270
+ offset,
271
+ });
272
+ const maxLeft = Math.max(
273
+ viewportPadding,
274
+ resolvedViewportWidth - floatingWidth - viewportPadding,
275
+ );
276
+ const maxTop = Math.max(
277
+ viewportPadding,
278
+ resolvedViewportHeight - floatingHeight - viewportPadding,
279
+ );
280
+ const position = {
281
+ top: clampNumber(basePosition.top, viewportPadding, maxTop),
282
+ left: clampNumber(basePosition.left, viewportPadding, maxLeft),
283
+ };
284
+ if (anchorRect && matchAnchorMinWidth)
285
+ position.minWidth = Math.min(
286
+ anchorRect.width,
287
+ resolvedViewportWidth - viewportPadding * 2,
288
+ );
289
+ return position;
290
+ }
291
+ const getAnchoredPanelPosition = (options) => {
292
+ const { anchorRect, floatingRect, side, align, offset } = options;
293
+ const centerLeft =
294
+ anchorRect.left + anchorRect.width / 2 - floatingRect.width / 2;
295
+ const centerTop =
296
+ anchorRect.top + anchorRect.height / 2 - floatingRect.height / 2;
297
+ if (side === "top" || side === "bottom")
298
+ return {
299
+ top:
300
+ side === "top"
301
+ ? anchorRect.top - floatingRect.height - offset
302
+ : anchorRect.bottom + offset,
303
+ left: getAlignedPosition({
304
+ start: anchorRect.left,
305
+ end: anchorRect.right,
306
+ size: floatingRect.width,
307
+ center: centerLeft,
308
+ align,
309
+ }),
310
+ };
311
+ return {
312
+ top: getAlignedPosition({
313
+ start: anchorRect.top,
314
+ end: anchorRect.bottom,
315
+ size: floatingRect.height,
316
+ center: centerTop,
317
+ align,
318
+ }),
319
+ left:
320
+ side === "left"
321
+ ? anchorRect.left - floatingRect.width - offset
322
+ : anchorRect.right + offset,
323
+ };
324
+ };
325
+ const getAlignedPosition = (options) => {
326
+ const { start, end, size, center, align } = options;
327
+ if (align === "start") return start;
328
+ if (align === "end") return end - size;
329
+ return center;
330
+ };
331
+ const createPointRect = (point) => ({
332
+ top: point.y,
333
+ right: point.x,
334
+ bottom: point.y,
335
+ left: point.x,
336
+ width: 0,
337
+ height: 0,
338
+ });
339
+ //#endregion
340
+ //#region src/floatingLayer.ts
341
+ function useFloatingLayer(options) {
342
+ const {
343
+ open,
344
+ triggerRef,
345
+ floatingRef,
346
+ anchorRect,
347
+ anchorPoint,
348
+ getAnchorRect,
349
+ getAnchorPoint,
350
+ side = "bottom",
351
+ align = "start",
352
+ offset = 0,
353
+ viewportPadding = 8,
354
+ minWidth,
355
+ fallbackWidth,
356
+ fallbackHeight = 0,
357
+ matchAnchorWidth = false,
358
+ matchAnchorMinWidth = false,
359
+ fullWidthBelow,
360
+ applyResolvedWidth = false,
361
+ flipToFit = false,
362
+ outsideRefs,
363
+ closeOnOutsidePointerDown = true,
364
+ restoreFocus = false,
365
+ onClose,
366
+ onOpenAutoFocus,
367
+ } = options;
368
+ const [position, setPosition] = useState();
369
+ const previousFocusRef = useRef(null);
370
+ const updatePosition = useCallback(() => {
371
+ const triggerElement = triggerRef?.current;
372
+ const floatingElement = floatingRef.current;
373
+ if (!floatingElement || typeof window === "undefined") return;
374
+ const resolvedAnchorRect =
375
+ getAnchorRect?.() ??
376
+ anchorRect ??
377
+ triggerElement?.getBoundingClientRect();
378
+ const resolvedAnchorPoint = getAnchorPoint?.() ?? anchorPoint ?? null;
379
+ const viewportWidth = window.innerWidth;
380
+ const maxWidth = Math.max(0, viewportWidth - viewportPadding * 2);
381
+ const rawFloatingWidth =
382
+ floatingElement.offsetWidth ||
383
+ resolvedAnchorRect?.width ||
384
+ fallbackWidth ||
385
+ 0;
386
+ const floatingHeight = floatingElement.offsetHeight || fallbackHeight;
387
+ const resolvedSide =
388
+ flipToFit && resolvedAnchorRect && !resolvedAnchorPoint
389
+ ? getFloatingLayerFitSide({
390
+ anchorRect: resolvedAnchorRect,
391
+ floatingHeight,
392
+ side,
393
+ viewportHeight: window.innerHeight,
394
+ viewportPadding,
395
+ })
396
+ : side;
397
+ const floatingWidth = resolveFloatingLayerWidth({
398
+ anchorWidth: resolvedAnchorRect?.width,
399
+ floatingWidth: rawFloatingWidth,
400
+ fullWidthBelow,
401
+ matchAnchorWidth,
402
+ maxWidth,
403
+ minWidth,
404
+ viewportWidth,
405
+ });
406
+ setPosition({
407
+ ...getFloatingPanelPosition({
408
+ anchorPoint: resolvedAnchorPoint ?? void 0,
409
+ anchorRect: resolvedAnchorPoint ? void 0 : resolvedAnchorRect,
410
+ floatingRect: {
411
+ width: floatingWidth,
412
+ height: floatingHeight,
413
+ },
414
+ side: resolvedSide,
415
+ align,
416
+ offset,
417
+ viewportPadding,
418
+ matchAnchorMinWidth:
419
+ matchAnchorMinWidth && !matchAnchorWidth && !resolvedAnchorPoint,
420
+ }),
421
+ anchorRect: resolvedAnchorRect ?? void 0,
422
+ width:
423
+ applyResolvedWidth || matchAnchorWidth || minWidth !== void 0
424
+ ? floatingWidth
425
+ : void 0,
426
+ });
427
+ }, [
428
+ align,
429
+ anchorRect,
430
+ anchorPoint,
431
+ fallbackHeight,
432
+ fallbackWidth,
433
+ flipToFit,
434
+ floatingRef,
435
+ fullWidthBelow,
436
+ getAnchorRect,
437
+ getAnchorPoint,
438
+ matchAnchorMinWidth,
439
+ matchAnchorWidth,
440
+ minWidth,
441
+ offset,
442
+ applyResolvedWidth,
443
+ side,
444
+ triggerRef,
445
+ viewportPadding,
446
+ ]);
447
+ useEffect(() => {
448
+ if (!open || typeof window === "undefined") return;
449
+ const ownerDocument =
450
+ floatingRef.current?.ownerDocument ??
451
+ triggerRef?.current?.ownerDocument ??
452
+ document;
453
+ if (restoreFocus)
454
+ previousFocusRef.current =
455
+ ownerDocument.activeElement instanceof HTMLElement
456
+ ? ownerDocument.activeElement
457
+ : null;
458
+ updatePosition();
459
+ const frame = window.requestAnimationFrame(updatePosition);
460
+ const focusTimer =
461
+ onOpenAutoFocus === void 0
462
+ ? void 0
463
+ : window.setTimeout(onOpenAutoFocus, 0);
464
+ const handlePointerDown = (event) => {
465
+ if (!closeOnOutsidePointerDown) return;
466
+ const target = event.target;
467
+ if (!(target instanceof Node)) return;
468
+ if (
469
+ containsPointerTarget(target, [
470
+ triggerRef,
471
+ floatingRef,
472
+ ...(outsideRefs ?? []),
473
+ ])
474
+ )
475
+ return;
476
+ onClose?.();
477
+ };
478
+ const handleWindowUpdate = () => {
479
+ updatePosition();
480
+ };
481
+ ownerDocument.addEventListener("pointerdown", handlePointerDown);
482
+ window.addEventListener("resize", handleWindowUpdate);
483
+ window.addEventListener("scroll", handleWindowUpdate, true);
484
+ return () => {
485
+ window.cancelAnimationFrame(frame);
486
+ if (focusTimer !== void 0) window.clearTimeout(focusTimer);
487
+ ownerDocument.removeEventListener("pointerdown", handlePointerDown);
488
+ window.removeEventListener("resize", handleWindowUpdate);
489
+ window.removeEventListener("scroll", handleWindowUpdate, true);
490
+ if (restoreFocus) {
491
+ previousFocusRef.current?.focus();
492
+ previousFocusRef.current = null;
493
+ }
494
+ };
495
+ }, [
496
+ closeOnOutsidePointerDown,
497
+ floatingRef,
498
+ onClose,
499
+ onOpenAutoFocus,
500
+ open,
501
+ outsideRefs,
502
+ restoreFocus,
503
+ triggerRef,
504
+ updatePosition,
505
+ ]);
506
+ useEffect(() => {
507
+ if (open) return;
508
+ setPosition(void 0);
509
+ }, [open]);
510
+ return {
511
+ position,
512
+ updatePosition,
513
+ };
514
+ }
515
+ const getFloatingLayerFitSide = (options) => {
516
+ const { anchorRect, floatingHeight, side, viewportHeight, viewportPadding } =
517
+ options;
518
+ if (side !== "top" && side !== "bottom") return side;
519
+ const topSpace = anchorRect.top - viewportPadding;
520
+ const bottomSpace = viewportHeight - anchorRect.bottom - viewportPadding;
521
+ if (side === "top")
522
+ return topSpace >= floatingHeight || topSpace >= bottomSpace
523
+ ? "top"
524
+ : "bottom";
525
+ return bottomSpace >= floatingHeight || bottomSpace >= topSpace
526
+ ? "bottom"
527
+ : "top";
528
+ };
529
+ const resolveFloatingLayerWidth = (options) => {
530
+ const {
531
+ anchorWidth,
532
+ floatingWidth,
533
+ fullWidthBelow,
534
+ matchAnchorWidth,
535
+ maxWidth,
536
+ minWidth,
537
+ viewportWidth,
538
+ } = options;
539
+ if (fullWidthBelow !== void 0 && viewportWidth <= fullWidthBelow)
540
+ return maxWidth;
541
+ if (matchAnchorWidth && anchorWidth !== void 0)
542
+ return Math.min(Math.max(anchorWidth, minWidth ?? anchorWidth), maxWidth);
543
+ if (minWidth !== void 0)
544
+ return Math.min(Math.max(floatingWidth, minWidth), maxWidth);
545
+ return floatingWidth;
546
+ };
547
+ const containsPointerTarget = (target, refs) => {
548
+ return refs.some((ref) => ref?.current?.contains(target));
549
+ };
550
+ //#endregion
32
551
  //#region src/nodes.ts
33
552
  const toNodeArray = (children) => {
34
553
  return (isArray(children) ? children : [children]).filter((item) => {
@@ -46,6 +565,186 @@ function isMediaOnlyParagraph(children) {
46
565
  return Boolean(node.type.__willaMediaElement);
47
566
  }
48
567
  //#endregion
568
+ //#region src/refs.ts
569
+ function assignRef(ref, value) {
570
+ if (!ref) return;
571
+ if (typeof ref === "function") {
572
+ ref(value);
573
+ return;
574
+ }
575
+ ref.current = value;
576
+ }
577
+ function composeRefs(...refs) {
578
+ return (value) => {
579
+ refs.forEach((ref) => assignRef(ref, value));
580
+ };
581
+ }
582
+ //#endregion
583
+ //#region src/media.ts
584
+ function resolveMediaAsset(props, assetPath) {
585
+ if (!assetPath) return void 0;
586
+ return props.resolveAssetUrl
587
+ ? props.resolveAssetUrl(props.articleSourcePath ?? "", assetPath)
588
+ : assetPath;
589
+ }
590
+ function resolveMediaVolume(volume) {
591
+ if (volume === void 0) return void 0;
592
+ if (!Number.isFinite(volume)) return void 0;
593
+ return clampNumber(volume, 0, 1);
594
+ }
595
+ //#endregion
596
+ //#region src/file.ts
597
+ const createObjectFileItem = (file) => {
598
+ return {
599
+ id: `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(36).slice(2)}`,
600
+ file,
601
+ url: URL.createObjectURL(file),
602
+ kind: resolveFileKind(file),
603
+ };
604
+ };
605
+ const resolveFileKind = (file) => {
606
+ if (file.type.startsWith("image/")) return "image";
607
+ if (file.type.startsWith("audio/")) return "audio";
608
+ if (file.type.startsWith("video/")) return "video";
609
+ return "file";
610
+ };
611
+ const resolveFilePreviewType = (options) => {
612
+ if (options.type !== "auto") return options.type;
613
+ const mimeType = options.mimeType?.toLowerCase() ?? "";
614
+ const extension = getFileExtension(options.name);
615
+ if (mimeType.startsWith("image/") || imageExtensions.has(extension))
616
+ return "image";
617
+ if (mimeType.startsWith("video/") || videoExtensions.has(extension))
618
+ return "video";
619
+ if (mimeType.startsWith("audio/") || audioExtensions.has(extension))
620
+ return "audio";
621
+ if (mimeType === "application/pdf" || extension === "pdf") return "pdf";
622
+ if (mimeType === "text/csv" || extension === "csv") return "csv";
623
+ if (codeExtensions.has(extension)) return "code";
624
+ if (mimeType.startsWith("text/") || textExtensions.has(extension))
625
+ return "text";
626
+ return "download";
627
+ };
628
+ const getFileExtension = (name) => {
629
+ return name.trim().toLowerCase().split(".").filter(Boolean).pop() ?? "";
630
+ };
631
+ const getFileCodeLanguage = (name) => {
632
+ const extension = getFileExtension(name);
633
+ return codeLanguageByExtension[extension] ?? extension;
634
+ };
635
+ const resolveFileKindLabel = (kind) => {
636
+ if (kind === "image") return "图片";
637
+ if (kind === "audio") return "音频";
638
+ if (kind === "video") return "视频";
639
+ return "文件";
640
+ };
641
+ const formatFileSize = (size) => {
642
+ if (size <= 0) return "0 B";
643
+ const units = ["B", "KB", "MB", "GB"];
644
+ const unitIndex = Math.min(
645
+ Math.floor(Math.log(size) / Math.log(1024)),
646
+ units.length - 1,
647
+ );
648
+ const value = size / 1024 ** unitIndex;
649
+ const fractionDigits = value >= 10 || unitIndex === 0 ? 0 : 1;
650
+ return `${value.toFixed(fractionDigits)} ${units[unitIndex]}`;
651
+ };
652
+ const normalizeFileProgress = (progress) => {
653
+ if (typeof progress !== "number" || Number.isNaN(progress)) return;
654
+ return Math.min(100, Math.max(0, progress));
655
+ };
656
+ const resolveFilePreviewMode = (itemMode, fallbackMode) =>
657
+ itemMode ?? fallbackMode;
658
+ const canOpenFilePreviewDialog = (options) => {
659
+ const { disabled, href, previewMode, status = "ready" } = options;
660
+ return (
661
+ previewMode === "dialog" && Boolean(href) && status === "ready" && !disabled
662
+ );
663
+ };
664
+ const imageExtensions = new Set(["avif", "gif", "jpeg", "jpg", "png", "webp"]);
665
+ const videoExtensions = new Set(["mov", "mp4", "webm"]);
666
+ const audioExtensions = new Set(["aac", "flac", "m4a", "mp3", "ogg", "wav"]);
667
+ const codeExtensions = new Set([
668
+ "bash",
669
+ "c",
670
+ "cpp",
671
+ "css",
672
+ "diff",
673
+ "go",
674
+ "html",
675
+ "java",
676
+ "js",
677
+ "json",
678
+ "jsx",
679
+ "md",
680
+ "py",
681
+ "rs",
682
+ "sh",
683
+ "sql",
684
+ "ts",
685
+ "tsx",
686
+ "xml",
687
+ "yaml",
688
+ "yml",
689
+ ]);
690
+ const textExtensions = new Set(["log", "txt"]);
691
+ const codeLanguageByExtension = {
692
+ bash: "bash",
693
+ c: "c",
694
+ cpp: "cpp",
695
+ css: "css",
696
+ diff: "diff",
697
+ go: "go",
698
+ html: "html",
699
+ java: "java",
700
+ js: "javascript",
701
+ json: "json",
702
+ jsx: "jsx",
703
+ md: "markdown",
704
+ py: "python",
705
+ rs: "rust",
706
+ sh: "bash",
707
+ sql: "sql",
708
+ ts: "typescript",
709
+ tsx: "tsx",
710
+ xml: "xml",
711
+ yaml: "yaml",
712
+ yml: "yaml",
713
+ };
714
+ //#endregion
715
+ //#region src/request.ts
716
+ const pendingJsonRequests = /* @__PURE__ */ new Map();
717
+ function isAbortError(error) {
718
+ return (
719
+ error !== null &&
720
+ typeof error === "object" &&
721
+ "name" in error &&
722
+ error.name === "AbortError"
723
+ );
724
+ }
725
+ async function requestJson(input, options = {}) {
726
+ const { createError, dedupeKey, ...requestInit } = options;
727
+ const cachedRequest = dedupeKey ? pendingJsonRequests.get(dedupeKey) : null;
728
+ if (cachedRequest) return cachedRequest;
729
+ const request = fetch(input, requestInit).then(async (response) => {
730
+ if (!response.ok)
731
+ throw createError
732
+ ? await createError(response)
733
+ : /* @__PURE__ */ new Error(
734
+ `Request failed with status ${response.status}.`,
735
+ );
736
+ return response.json();
737
+ });
738
+ if (dedupeKey) {
739
+ pendingJsonRequests.set(dedupeKey, request);
740
+ request.then(
741
+ () => pendingJsonRequests.delete(dedupeKey),
742
+ () => pendingJsonRequests.delete(dedupeKey),
743
+ );
744
+ }
745
+ return request;
746
+ }
747
+ //#endregion
49
748
  //#region src/heading.ts
50
749
  const toHeadingSlug = (value) => {
51
750
  return value
@@ -104,10 +803,142 @@ function extractHeadings(source) {
104
803
  return headings;
105
804
  }
106
805
  //#endregion
806
+ //#region src/virtualScroll.ts
807
+ const getVirtualScrollWindow = (options) => {
808
+ const { itemCount, itemHeight, overscan, scrollTop, viewportHeight } =
809
+ options;
810
+ if (itemCount <= 0)
811
+ return {
812
+ startIndex: 0,
813
+ endIndex: 0,
814
+ paddingTop: 0,
815
+ paddingBottom: 0,
816
+ totalHeight: 0,
817
+ };
818
+ const safeItemHeight = Math.max(1, itemHeight);
819
+ const visibleCount = Math.ceil(Math.max(0, viewportHeight) / safeItemHeight);
820
+ const startIndex = clampNumber(
821
+ Math.floor(scrollTop / safeItemHeight) - overscan,
822
+ 0,
823
+ Math.max(itemCount - 1, 0),
824
+ );
825
+ const endIndex = clampNumber(
826
+ startIndex + visibleCount + overscan * 2,
827
+ 0,
828
+ itemCount,
829
+ );
830
+ return {
831
+ startIndex,
832
+ endIndex,
833
+ paddingTop: startIndex * safeItemHeight,
834
+ paddingBottom: Math.max(0, itemCount - endIndex) * safeItemHeight,
835
+ totalHeight: itemCount * safeItemHeight,
836
+ };
837
+ };
838
+ const useVirtualScrollWindow = (options) => {
839
+ const {
840
+ enabled = false,
841
+ itemCount,
842
+ itemHeight,
843
+ overscan = 4,
844
+ container,
845
+ } = options;
846
+ const [metrics, setMetrics] = useState({
847
+ scrollTop: 0,
848
+ viewportHeight: 0,
849
+ });
850
+ useEffect(() => {
851
+ if (!enabled || !container) return;
852
+ let frame = 0;
853
+ const update = () => {
854
+ frame = 0;
855
+ setMetrics({
856
+ scrollTop: container.scrollTop,
857
+ viewportHeight: container.clientHeight,
858
+ });
859
+ };
860
+ const schedule = () => {
861
+ if (frame) return;
862
+ frame = window.requestAnimationFrame(update);
863
+ };
864
+ update();
865
+ container.addEventListener("scroll", schedule, { passive: true });
866
+ const resizeObserver =
867
+ typeof ResizeObserver === "undefined"
868
+ ? void 0
869
+ : new ResizeObserver(schedule);
870
+ resizeObserver?.observe(container);
871
+ window.addEventListener("resize", schedule);
872
+ return () => {
873
+ if (frame) window.cancelAnimationFrame(frame);
874
+ container.removeEventListener("scroll", schedule);
875
+ resizeObserver?.disconnect();
876
+ window.removeEventListener("resize", schedule);
877
+ };
878
+ }, [container, enabled]);
879
+ return useMemo(() => {
880
+ if (!enabled)
881
+ return {
882
+ startIndex: 0,
883
+ endIndex: itemCount,
884
+ paddingTop: 0,
885
+ paddingBottom: 0,
886
+ totalHeight: itemCount * itemHeight,
887
+ };
888
+ return getVirtualScrollWindow({
889
+ itemCount,
890
+ itemHeight,
891
+ overscan,
892
+ scrollTop: metrics.scrollTop,
893
+ viewportHeight: metrics.viewportHeight,
894
+ });
895
+ }, [
896
+ enabled,
897
+ itemCount,
898
+ itemHeight,
899
+ metrics.scrollTop,
900
+ metrics.viewportHeight,
901
+ overscan,
902
+ ]);
903
+ };
904
+ //#endregion
107
905
  export {
906
+ MOBILE_BREAKPOINT,
907
+ assignRef,
908
+ canOpenFilePreviewDialog,
909
+ clampNumber,
910
+ composeRefs,
108
911
  copyToClipboard,
912
+ createCodeHighlightLines,
109
913
  createHeadingIdFactory,
914
+ createNumberRange,
915
+ createObjectFileItem,
110
916
  extractHeadings,
111
917
  flattenText,
918
+ formatCssSize,
919
+ formatFileSize,
920
+ getFileCodeLanguage,
921
+ getFileExtension,
922
+ getFloatingPanelPosition,
923
+ getFocusableElements,
924
+ getVirtualScrollWindow,
925
+ highlightCodeToHtml,
926
+ isAbortError,
112
927
  isMediaOnlyParagraph,
928
+ isMobile,
929
+ isMobileViewport,
930
+ normalizeFileProgress,
931
+ normalizeHljsLanguage,
932
+ parseCodeMeta,
933
+ requestJson,
934
+ resolveFileKind,
935
+ resolveFileKindLabel,
936
+ resolveFilePreviewMode,
937
+ resolveFilePreviewType,
938
+ resolveMediaAsset,
939
+ resolveMediaVolume,
940
+ useControllableState,
941
+ useCopyToClipboard,
942
+ useFloatingLayer,
943
+ useVirtualScrollWindow,
113
944
  };