@willa-ui/shared 0.0.3-alpha.f3c50c9 → 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.cjs CHANGED
@@ -1,10 +1,40 @@
1
1
  /*!
2
- * @willa-ui/shared.js v0.0.3-alpha.f3c50c9
2
+ * @willa-ui/shared.js v0.0.4
3
3
  * (c) 2026 chentao.arthur
4
4
  */
5
5
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
+ //#region \0rolldown/runtime.js
7
+ var __create = Object.create;
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __getProtoOf = Object.getPrototypeOf;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
+ key = keys[i];
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
17
+ get: ((k) => from[k]).bind(null, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+ //#endregion
6
28
  let react = require("react");
29
+ let highlight_js = require("highlight.js");
30
+ highlight_js = __toESM(highlight_js);
7
31
  let aidly = require("aidly");
32
+ //#region src/css.ts
33
+ function formatCssSize(value) {
34
+ if (typeof value === "number") return Number.isFinite(value) ? `${value}px` : void 0;
35
+ if (typeof value === "string") return value.trim() || void 0;
36
+ }
37
+ //#endregion
8
38
  //#region src/clipboard.ts
9
39
  async function copyToClipboard(text) {
10
40
  if (!text) return false;
@@ -30,6 +60,390 @@ async function copyToClipboard(text) {
30
60
  }
31
61
  }
32
62
  //#endregion
63
+ //#region src/controllableState.ts
64
+ function useControllableState(options) {
65
+ const { value, defaultValue, onChange } = options;
66
+ const [uncontrolledValue, setUncontrolledValue] = (0, react.useState)(defaultValue);
67
+ const controlled = value !== void 0;
68
+ const currentValue = controlled ? value : uncontrolledValue;
69
+ return [
70
+ currentValue,
71
+ (0, react.useCallback)((nextValue) => {
72
+ const resolvedValue = resolveStateAction(nextValue, currentValue);
73
+ if (!controlled) setUncontrolledValue(resolvedValue);
74
+ onChange?.(resolvedValue);
75
+ }, [
76
+ controlled,
77
+ currentValue,
78
+ onChange
79
+ ]),
80
+ controlled
81
+ ];
82
+ }
83
+ const resolveStateAction = (value, previousValue) => {
84
+ if (typeof value === "function") return value(previousValue);
85
+ return value;
86
+ };
87
+ //#endregion
88
+ //#region src/viewport.ts
89
+ const MOBILE_BREAKPOINT = 640;
90
+ function isMobileViewport(viewportWidth) {
91
+ return viewportWidth <= 640;
92
+ }
93
+ function isMobile() {
94
+ if (typeof window === "undefined") return false;
95
+ return isMobileViewport(window.innerWidth);
96
+ }
97
+ //#endregion
98
+ //#region src/copy.ts
99
+ function useCopyToClipboard(options = {}) {
100
+ const { resetDuration = 1200, onCopy } = options;
101
+ const [status, setStatus] = (0, react.useState)("idle");
102
+ const timerRef = (0, react.useRef)(null);
103
+ const clearTimer = (0, react.useCallback)(() => {
104
+ if (timerRef.current === null) return;
105
+ window.clearTimeout(timerRef.current);
106
+ timerRef.current = null;
107
+ }, []);
108
+ const startFeedback = (0, react.useCallback)((nextStatus, duration = resetDuration) => {
109
+ clearTimer();
110
+ setStatus(nextStatus);
111
+ if (nextStatus === "idle" || duration <= 0) return;
112
+ timerRef.current = window.setTimeout(() => {
113
+ setStatus("idle");
114
+ timerRef.current = null;
115
+ }, duration);
116
+ }, [clearTimer, resetDuration]);
117
+ const reset = (0, react.useCallback)(() => {
118
+ startFeedback("idle", 0);
119
+ }, [startFeedback]);
120
+ const copy = (0, react.useCallback)(async (text, actionOptions = {}) => {
121
+ startFeedback("idle", 0);
122
+ const ok = await copyToClipboard(text);
123
+ startFeedback(ok ? "copied" : "failed", actionOptions.resetDuration);
124
+ if (ok) {
125
+ onCopy?.(text);
126
+ actionOptions.onCopy?.(text);
127
+ }
128
+ return ok;
129
+ }, [onCopy, startFeedback]);
130
+ (0, react.useEffect)(() => {
131
+ return () => {
132
+ clearTimer();
133
+ };
134
+ }, [clearTimer]);
135
+ return {
136
+ status,
137
+ copied: status === "copied",
138
+ failed: status === "failed",
139
+ copy,
140
+ reset
141
+ };
142
+ }
143
+ //#endregion
144
+ //#region src/codeHighlight.ts
145
+ const parseCodeMeta = (className) => {
146
+ const [rawLanguage = "text", rawMeta = ""] = (/language-([^\s]+)/.exec(className ?? "")?.[1] ?? "text").split("--meta-");
147
+ const highlightLines = /* @__PURE__ */ new Set();
148
+ const normalizedMeta = rawMeta.replace(/_/g, " ");
149
+ const showLineNumbers = /(?:^|[^A-Za-z0-9-])ln(?:$|[^A-Za-z0-9-])/.test(normalizedMeta);
150
+ for (const match of rawMeta.matchAll(/\{([^}]+)\}/g)) addCodeHighlightRange(highlightLines, match[1]);
151
+ return {
152
+ rawLanguage,
153
+ highlightLines,
154
+ showLineNumbers
155
+ };
156
+ };
157
+ const normalizeHljsLanguage = (language) => {
158
+ const key = language.toLowerCase();
159
+ return {
160
+ c: "c",
161
+ cc: "cpp",
162
+ "c++": "cpp",
163
+ cpp: "cpp",
164
+ cxx: "cpp",
165
+ css: "css",
166
+ go: "go",
167
+ golang: "go",
168
+ html: "xml",
169
+ js: "javascript",
170
+ jsx: "javascript",
171
+ rs: "rust",
172
+ rust: "rust",
173
+ ts: "typescript",
174
+ tsx: "typescript",
175
+ sh: "bash",
176
+ bash: "bash",
177
+ shell: "bash",
178
+ yml: "yaml",
179
+ md: "markdown"
180
+ }[key] ?? key;
181
+ };
182
+ const highlightCodeToHtml = (code, rawLanguage) => {
183
+ const language = normalizeHljsLanguage(rawLanguage);
184
+ if (highlight_js.default.getLanguage(language)) return {
185
+ html: highlight_js.default.highlight(code, { language }).value,
186
+ display: rawLanguage
187
+ };
188
+ const result = highlight_js.default.highlightAuto(code);
189
+ return {
190
+ html: result.value,
191
+ display: result.language ?? rawLanguage
192
+ };
193
+ };
194
+ const createCodeHighlightLines = (ranges) => {
195
+ const highlightLines = /* @__PURE__ */ new Set();
196
+ for (const range of ranges ?? []) if (typeof range === "number") {
197
+ if (!Number.isInteger(range) || range <= 0) continue;
198
+ highlightLines.add(range);
199
+ } else addCodeHighlightRange(highlightLines, range.join("-"));
200
+ return highlightLines;
201
+ };
202
+ const addCodeHighlightRange = (highlightLines, value) => {
203
+ for (const part of value.split(",")) {
204
+ const rangeMatch = /^\s*(\d+)(?:-(\d+))?\s*$/.exec(part);
205
+ if (!rangeMatch) continue;
206
+ const start = Number(rangeMatch[1]);
207
+ const end = Number(rangeMatch[2] ?? rangeMatch[1]);
208
+ if (!Number.isInteger(start) || !Number.isInteger(end)) continue;
209
+ for (let line = start; line <= end; line += 1) if (line > 0) highlightLines.add(line);
210
+ }
211
+ };
212
+ //#endregion
213
+ //#region src/dom.ts
214
+ const focusableSelector = [
215
+ "a[href]",
216
+ "button:not([disabled])",
217
+ "textarea:not([disabled])",
218
+ "input:not([disabled])",
219
+ "select:not([disabled])",
220
+ "[tabindex]:not([tabindex='-1'])"
221
+ ].join(",");
222
+ function getFocusableElements(container) {
223
+ return Array.from(container.querySelectorAll(focusableSelector));
224
+ }
225
+ //#endregion
226
+ //#region src/number.ts
227
+ function clampNumber(value, min, max) {
228
+ if (max < min) return min;
229
+ return Math.min(Math.max(value, min), max);
230
+ }
231
+ function createNumberRange(start, end) {
232
+ if (end < start) return [];
233
+ return Array.from({ length: end - start + 1 }, (_, index) => start + index);
234
+ }
235
+ //#endregion
236
+ //#region src/floating.ts
237
+ function getFloatingPanelPosition(options) {
238
+ const { anchorRect, anchorPoint, floatingRect, side = "bottom", align = "start", offset = 0, viewportWidth, viewportHeight, viewportPadding = 8, matchAnchorMinWidth = false } = options;
239
+ const resolvedViewportWidth = viewportWidth ?? (typeof window === "undefined" ? 0 : window.innerWidth);
240
+ const resolvedViewportHeight = viewportHeight ?? (typeof window === "undefined" ? 0 : window.innerHeight);
241
+ const floatingWidth = floatingRect.width;
242
+ const floatingHeight = floatingRect.height;
243
+ const basePosition = anchorPoint ? {
244
+ top: anchorPoint.y,
245
+ left: anchorPoint.x
246
+ } : getAnchoredPanelPosition({
247
+ anchorRect: anchorRect ?? createPointRect({
248
+ x: 0,
249
+ y: 0
250
+ }),
251
+ floatingRect,
252
+ side,
253
+ align,
254
+ offset
255
+ });
256
+ const maxLeft = Math.max(viewportPadding, resolvedViewportWidth - floatingWidth - viewportPadding);
257
+ const maxTop = Math.max(viewportPadding, resolvedViewportHeight - floatingHeight - viewportPadding);
258
+ const position = {
259
+ top: clampNumber(basePosition.top, viewportPadding, maxTop),
260
+ left: clampNumber(basePosition.left, viewportPadding, maxLeft)
261
+ };
262
+ if (anchorRect && matchAnchorMinWidth) position.minWidth = Math.min(anchorRect.width, resolvedViewportWidth - viewportPadding * 2);
263
+ return position;
264
+ }
265
+ const getAnchoredPanelPosition = (options) => {
266
+ const { anchorRect, floatingRect, side, align, offset } = options;
267
+ const centerLeft = anchorRect.left + anchorRect.width / 2 - floatingRect.width / 2;
268
+ const centerTop = anchorRect.top + anchorRect.height / 2 - floatingRect.height / 2;
269
+ if (side === "top" || side === "bottom") return {
270
+ top: side === "top" ? anchorRect.top - floatingRect.height - offset : anchorRect.bottom + offset,
271
+ left: getAlignedPosition({
272
+ start: anchorRect.left,
273
+ end: anchorRect.right,
274
+ size: floatingRect.width,
275
+ center: centerLeft,
276
+ align
277
+ })
278
+ };
279
+ return {
280
+ top: getAlignedPosition({
281
+ start: anchorRect.top,
282
+ end: anchorRect.bottom,
283
+ size: floatingRect.height,
284
+ center: centerTop,
285
+ align
286
+ }),
287
+ left: side === "left" ? anchorRect.left - floatingRect.width - offset : anchorRect.right + offset
288
+ };
289
+ };
290
+ const getAlignedPosition = (options) => {
291
+ const { start, end, size, center, align } = options;
292
+ if (align === "start") return start;
293
+ if (align === "end") return end - size;
294
+ return center;
295
+ };
296
+ const createPointRect = (point) => ({
297
+ top: point.y,
298
+ right: point.x,
299
+ bottom: point.y,
300
+ left: point.x,
301
+ width: 0,
302
+ height: 0
303
+ });
304
+ //#endregion
305
+ //#region src/floatingLayer.ts
306
+ function useFloatingLayer(options) {
307
+ const { open, triggerRef, floatingRef, anchorRect, anchorPoint, getAnchorRect, getAnchorPoint, side = "bottom", align = "start", offset = 0, viewportPadding = 8, minWidth, fallbackWidth, fallbackHeight = 0, matchAnchorWidth = false, matchAnchorMinWidth = false, fullWidthBelow, applyResolvedWidth = false, flipToFit = false, outsideRefs, closeOnOutsidePointerDown = true, restoreFocus = false, onClose, onOpenAutoFocus } = options;
308
+ const [position, setPosition] = (0, react.useState)();
309
+ const previousFocusRef = (0, react.useRef)(null);
310
+ const updatePosition = (0, react.useCallback)(() => {
311
+ const triggerElement = triggerRef?.current;
312
+ const floatingElement = floatingRef.current;
313
+ if (!floatingElement || typeof window === "undefined") return;
314
+ const resolvedAnchorRect = getAnchorRect?.() ?? anchorRect ?? triggerElement?.getBoundingClientRect();
315
+ const resolvedAnchorPoint = getAnchorPoint?.() ?? anchorPoint ?? null;
316
+ const viewportWidth = window.innerWidth;
317
+ const maxWidth = Math.max(0, viewportWidth - viewportPadding * 2);
318
+ const rawFloatingWidth = floatingElement.offsetWidth || resolvedAnchorRect?.width || fallbackWidth || 0;
319
+ const floatingHeight = floatingElement.offsetHeight || fallbackHeight;
320
+ const resolvedSide = flipToFit && resolvedAnchorRect && !resolvedAnchorPoint ? getFloatingLayerFitSide({
321
+ anchorRect: resolvedAnchorRect,
322
+ floatingHeight,
323
+ side,
324
+ viewportHeight: window.innerHeight,
325
+ viewportPadding
326
+ }) : side;
327
+ const floatingWidth = resolveFloatingLayerWidth({
328
+ anchorWidth: resolvedAnchorRect?.width,
329
+ floatingWidth: rawFloatingWidth,
330
+ fullWidthBelow,
331
+ matchAnchorWidth,
332
+ maxWidth,
333
+ minWidth,
334
+ viewportWidth
335
+ });
336
+ setPosition({
337
+ ...getFloatingPanelPosition({
338
+ anchorPoint: resolvedAnchorPoint ?? void 0,
339
+ anchorRect: resolvedAnchorPoint ? void 0 : resolvedAnchorRect,
340
+ floatingRect: {
341
+ width: floatingWidth,
342
+ height: floatingHeight
343
+ },
344
+ side: resolvedSide,
345
+ align,
346
+ offset,
347
+ viewportPadding,
348
+ matchAnchorMinWidth: matchAnchorMinWidth && !matchAnchorWidth && !resolvedAnchorPoint
349
+ }),
350
+ anchorRect: resolvedAnchorRect ?? void 0,
351
+ width: applyResolvedWidth || matchAnchorWidth || minWidth !== void 0 ? floatingWidth : void 0
352
+ });
353
+ }, [
354
+ align,
355
+ anchorRect,
356
+ anchorPoint,
357
+ fallbackHeight,
358
+ fallbackWidth,
359
+ flipToFit,
360
+ floatingRef,
361
+ fullWidthBelow,
362
+ getAnchorRect,
363
+ getAnchorPoint,
364
+ matchAnchorMinWidth,
365
+ matchAnchorWidth,
366
+ minWidth,
367
+ offset,
368
+ applyResolvedWidth,
369
+ side,
370
+ triggerRef,
371
+ viewportPadding
372
+ ]);
373
+ (0, react.useEffect)(() => {
374
+ if (!open || typeof window === "undefined") return;
375
+ const ownerDocument = floatingRef.current?.ownerDocument ?? triggerRef?.current?.ownerDocument ?? document;
376
+ if (restoreFocus) previousFocusRef.current = ownerDocument.activeElement instanceof HTMLElement ? ownerDocument.activeElement : null;
377
+ updatePosition();
378
+ const frame = window.requestAnimationFrame(updatePosition);
379
+ const focusTimer = onOpenAutoFocus === void 0 ? void 0 : window.setTimeout(onOpenAutoFocus, 0);
380
+ const handlePointerDown = (event) => {
381
+ if (!closeOnOutsidePointerDown) return;
382
+ const target = event.target;
383
+ if (!(target instanceof Node)) return;
384
+ if (containsPointerTarget(target, [
385
+ triggerRef,
386
+ floatingRef,
387
+ ...outsideRefs ?? []
388
+ ])) return;
389
+ onClose?.();
390
+ };
391
+ const handleWindowUpdate = () => {
392
+ updatePosition();
393
+ };
394
+ ownerDocument.addEventListener("pointerdown", handlePointerDown);
395
+ window.addEventListener("resize", handleWindowUpdate);
396
+ window.addEventListener("scroll", handleWindowUpdate, true);
397
+ return () => {
398
+ window.cancelAnimationFrame(frame);
399
+ if (focusTimer !== void 0) window.clearTimeout(focusTimer);
400
+ ownerDocument.removeEventListener("pointerdown", handlePointerDown);
401
+ window.removeEventListener("resize", handleWindowUpdate);
402
+ window.removeEventListener("scroll", handleWindowUpdate, true);
403
+ if (restoreFocus) {
404
+ previousFocusRef.current?.focus();
405
+ previousFocusRef.current = null;
406
+ }
407
+ };
408
+ }, [
409
+ closeOnOutsidePointerDown,
410
+ floatingRef,
411
+ onClose,
412
+ onOpenAutoFocus,
413
+ open,
414
+ outsideRefs,
415
+ restoreFocus,
416
+ triggerRef,
417
+ updatePosition
418
+ ]);
419
+ (0, react.useEffect)(() => {
420
+ if (open) return;
421
+ setPosition(void 0);
422
+ }, [open]);
423
+ return {
424
+ position,
425
+ updatePosition
426
+ };
427
+ }
428
+ const getFloatingLayerFitSide = (options) => {
429
+ const { anchorRect, floatingHeight, side, viewportHeight, viewportPadding } = options;
430
+ if (side !== "top" && side !== "bottom") return side;
431
+ const topSpace = anchorRect.top - viewportPadding;
432
+ const bottomSpace = viewportHeight - anchorRect.bottom - viewportPadding;
433
+ if (side === "top") return topSpace >= floatingHeight || topSpace >= bottomSpace ? "top" : "bottom";
434
+ return bottomSpace >= floatingHeight || bottomSpace >= topSpace ? "bottom" : "top";
435
+ };
436
+ const resolveFloatingLayerWidth = (options) => {
437
+ const { anchorWidth, floatingWidth, fullWidthBelow, matchAnchorWidth, maxWidth, minWidth, viewportWidth } = options;
438
+ if (fullWidthBelow !== void 0 && viewportWidth <= fullWidthBelow) return maxWidth;
439
+ if (matchAnchorWidth && anchorWidth !== void 0) return Math.min(Math.max(anchorWidth, minWidth ?? anchorWidth), maxWidth);
440
+ if (minWidth !== void 0) return Math.min(Math.max(floatingWidth, minWidth), maxWidth);
441
+ return floatingWidth;
442
+ };
443
+ const containsPointerTarget = (target, refs) => {
444
+ return refs.some((ref) => ref?.current?.contains(target));
445
+ };
446
+ //#endregion
33
447
  //#region src/nodes.ts
34
448
  const toNodeArray = (children) => {
35
449
  return ((0, aidly.isArray)(children) ? children : [children]).filter((item) => {
@@ -47,6 +461,184 @@ function isMediaOnlyParagraph(children) {
47
461
  return Boolean(node.type.__willaMediaElement);
48
462
  }
49
463
  //#endregion
464
+ //#region src/refs.ts
465
+ function assignRef(ref, value) {
466
+ if (!ref) return;
467
+ if (typeof ref === "function") {
468
+ ref(value);
469
+ return;
470
+ }
471
+ ref.current = value;
472
+ }
473
+ function composeRefs(...refs) {
474
+ return (value) => {
475
+ refs.forEach((ref) => assignRef(ref, value));
476
+ };
477
+ }
478
+ //#endregion
479
+ //#region src/media.ts
480
+ function resolveMediaAsset(props, assetPath) {
481
+ if (!assetPath) return void 0;
482
+ return props.resolveAssetUrl ? props.resolveAssetUrl(props.articleSourcePath ?? "", assetPath) : assetPath;
483
+ }
484
+ function resolveMediaVolume(volume) {
485
+ if (volume === void 0) return void 0;
486
+ if (!Number.isFinite(volume)) return void 0;
487
+ return clampNumber(volume, 0, 1);
488
+ }
489
+ //#endregion
490
+ //#region src/file.ts
491
+ const createObjectFileItem = (file) => {
492
+ return {
493
+ id: `${file.name}-${file.size}-${file.lastModified}-${Math.random().toString(36).slice(2)}`,
494
+ file,
495
+ url: URL.createObjectURL(file),
496
+ kind: resolveFileKind(file)
497
+ };
498
+ };
499
+ const resolveFileKind = (file) => {
500
+ if (file.type.startsWith("image/")) return "image";
501
+ if (file.type.startsWith("audio/")) return "audio";
502
+ if (file.type.startsWith("video/")) return "video";
503
+ return "file";
504
+ };
505
+ const resolveFilePreviewType = (options) => {
506
+ if (options.type !== "auto") return options.type;
507
+ const mimeType = options.mimeType?.toLowerCase() ?? "";
508
+ const extension = getFileExtension(options.name);
509
+ if (mimeType.startsWith("image/") || imageExtensions.has(extension)) return "image";
510
+ if (mimeType.startsWith("video/") || videoExtensions.has(extension)) return "video";
511
+ if (mimeType.startsWith("audio/") || audioExtensions.has(extension)) return "audio";
512
+ if (mimeType === "application/pdf" || extension === "pdf") return "pdf";
513
+ if (mimeType === "text/csv" || extension === "csv") return "csv";
514
+ if (codeExtensions.has(extension)) return "code";
515
+ if (mimeType.startsWith("text/") || textExtensions.has(extension)) return "text";
516
+ return "download";
517
+ };
518
+ const getFileExtension = (name) => {
519
+ return name.trim().toLowerCase().split(".").filter(Boolean).pop() ?? "";
520
+ };
521
+ const getFileCodeLanguage = (name) => {
522
+ const extension = getFileExtension(name);
523
+ return codeLanguageByExtension[extension] ?? extension;
524
+ };
525
+ const resolveFileKindLabel = (kind) => {
526
+ if (kind === "image") return "图片";
527
+ if (kind === "audio") return "音频";
528
+ if (kind === "video") return "视频";
529
+ return "文件";
530
+ };
531
+ const formatFileSize = (size) => {
532
+ if (size <= 0) return "0 B";
533
+ const units = [
534
+ "B",
535
+ "KB",
536
+ "MB",
537
+ "GB"
538
+ ];
539
+ const unitIndex = Math.min(Math.floor(Math.log(size) / Math.log(1024)), units.length - 1);
540
+ const value = size / 1024 ** unitIndex;
541
+ const fractionDigits = value >= 10 || unitIndex === 0 ? 0 : 1;
542
+ return `${value.toFixed(fractionDigits)} ${units[unitIndex]}`;
543
+ };
544
+ const normalizeFileProgress = (progress) => {
545
+ if (typeof progress !== "number" || Number.isNaN(progress)) return;
546
+ return Math.min(100, Math.max(0, progress));
547
+ };
548
+ const resolveFilePreviewMode = (itemMode, fallbackMode) => itemMode ?? fallbackMode;
549
+ const canOpenFilePreviewDialog = (options) => {
550
+ const { disabled, href, previewMode, status = "ready" } = options;
551
+ return previewMode === "dialog" && Boolean(href) && status === "ready" && !disabled;
552
+ };
553
+ const imageExtensions = new Set([
554
+ "avif",
555
+ "gif",
556
+ "jpeg",
557
+ "jpg",
558
+ "png",
559
+ "webp"
560
+ ]);
561
+ const videoExtensions = new Set([
562
+ "mov",
563
+ "mp4",
564
+ "webm"
565
+ ]);
566
+ const audioExtensions = new Set([
567
+ "aac",
568
+ "flac",
569
+ "m4a",
570
+ "mp3",
571
+ "ogg",
572
+ "wav"
573
+ ]);
574
+ const codeExtensions = new Set([
575
+ "bash",
576
+ "c",
577
+ "cpp",
578
+ "css",
579
+ "diff",
580
+ "go",
581
+ "html",
582
+ "java",
583
+ "js",
584
+ "json",
585
+ "jsx",
586
+ "md",
587
+ "py",
588
+ "rs",
589
+ "sh",
590
+ "sql",
591
+ "ts",
592
+ "tsx",
593
+ "xml",
594
+ "yaml",
595
+ "yml"
596
+ ]);
597
+ const textExtensions = new Set(["log", "txt"]);
598
+ const codeLanguageByExtension = {
599
+ bash: "bash",
600
+ c: "c",
601
+ cpp: "cpp",
602
+ css: "css",
603
+ diff: "diff",
604
+ go: "go",
605
+ html: "html",
606
+ java: "java",
607
+ js: "javascript",
608
+ json: "json",
609
+ jsx: "jsx",
610
+ md: "markdown",
611
+ py: "python",
612
+ rs: "rust",
613
+ sh: "bash",
614
+ sql: "sql",
615
+ ts: "typescript",
616
+ tsx: "tsx",
617
+ xml: "xml",
618
+ yaml: "yaml",
619
+ yml: "yaml"
620
+ };
621
+ //#endregion
622
+ //#region src/request.ts
623
+ const pendingJsonRequests = /* @__PURE__ */ new Map();
624
+ function isAbortError(error) {
625
+ return error !== null && typeof error === "object" && "name" in error && error.name === "AbortError";
626
+ }
627
+ async function requestJson(input, options = {}) {
628
+ const { createError, dedupeKey, ...requestInit } = options;
629
+ const cachedRequest = dedupeKey ? pendingJsonRequests.get(dedupeKey) : null;
630
+ if (cachedRequest) return cachedRequest;
631
+ const request = fetch(input, requestInit).then(async (response) => {
632
+ if (!response.ok) throw createError ? await createError(response) : /* @__PURE__ */ new Error(`Request failed with status ${response.status}.`);
633
+ return response.json();
634
+ });
635
+ if (dedupeKey) {
636
+ pendingJsonRequests.set(dedupeKey, request);
637
+ request.then(() => pendingJsonRequests.delete(dedupeKey), () => pendingJsonRequests.delete(dedupeKey));
638
+ }
639
+ return request;
640
+ }
641
+ //#endregion
50
642
  //#region src/heading.ts
51
643
  const toHeadingSlug = (value) => {
52
644
  return value.trim().replace(/\[[^\]]*\]\([^)]*\)/g, (m) => {
@@ -94,8 +686,120 @@ function extractHeadings(source) {
94
686
  return headings;
95
687
  }
96
688
  //#endregion
689
+ //#region src/virtualScroll.ts
690
+ const getVirtualScrollWindow = (options) => {
691
+ const { itemCount, itemHeight, overscan, scrollTop, viewportHeight } = options;
692
+ if (itemCount <= 0) return {
693
+ startIndex: 0,
694
+ endIndex: 0,
695
+ paddingTop: 0,
696
+ paddingBottom: 0,
697
+ totalHeight: 0
698
+ };
699
+ const safeItemHeight = Math.max(1, itemHeight);
700
+ const visibleCount = Math.ceil(Math.max(0, viewportHeight) / safeItemHeight);
701
+ const startIndex = clampNumber(Math.floor(scrollTop / safeItemHeight) - overscan, 0, Math.max(itemCount - 1, 0));
702
+ const endIndex = clampNumber(startIndex + visibleCount + overscan * 2, 0, itemCount);
703
+ return {
704
+ startIndex,
705
+ endIndex,
706
+ paddingTop: startIndex * safeItemHeight,
707
+ paddingBottom: Math.max(0, itemCount - endIndex) * safeItemHeight,
708
+ totalHeight: itemCount * safeItemHeight
709
+ };
710
+ };
711
+ const useVirtualScrollWindow = (options) => {
712
+ const { enabled = false, itemCount, itemHeight, overscan = 4, container } = options;
713
+ const [metrics, setMetrics] = (0, react.useState)({
714
+ scrollTop: 0,
715
+ viewportHeight: 0
716
+ });
717
+ (0, react.useEffect)(() => {
718
+ if (!enabled || !container) return;
719
+ let frame = 0;
720
+ const update = () => {
721
+ frame = 0;
722
+ setMetrics({
723
+ scrollTop: container.scrollTop,
724
+ viewportHeight: container.clientHeight
725
+ });
726
+ };
727
+ const schedule = () => {
728
+ if (frame) return;
729
+ frame = window.requestAnimationFrame(update);
730
+ };
731
+ update();
732
+ container.addEventListener("scroll", schedule, { passive: true });
733
+ const resizeObserver = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule);
734
+ resizeObserver?.observe(container);
735
+ window.addEventListener("resize", schedule);
736
+ return () => {
737
+ if (frame) window.cancelAnimationFrame(frame);
738
+ container.removeEventListener("scroll", schedule);
739
+ resizeObserver?.disconnect();
740
+ window.removeEventListener("resize", schedule);
741
+ };
742
+ }, [container, enabled]);
743
+ return (0, react.useMemo)(() => {
744
+ if (!enabled) return {
745
+ startIndex: 0,
746
+ endIndex: itemCount,
747
+ paddingTop: 0,
748
+ paddingBottom: 0,
749
+ totalHeight: itemCount * itemHeight
750
+ };
751
+ return getVirtualScrollWindow({
752
+ itemCount,
753
+ itemHeight,
754
+ overscan,
755
+ scrollTop: metrics.scrollTop,
756
+ viewportHeight: metrics.viewportHeight
757
+ });
758
+ }, [
759
+ enabled,
760
+ itemCount,
761
+ itemHeight,
762
+ metrics.scrollTop,
763
+ metrics.viewportHeight,
764
+ overscan
765
+ ]);
766
+ };
767
+ //#endregion
768
+ exports.MOBILE_BREAKPOINT = MOBILE_BREAKPOINT;
769
+ exports.assignRef = assignRef;
770
+ exports.canOpenFilePreviewDialog = canOpenFilePreviewDialog;
771
+ exports.clampNumber = clampNumber;
772
+ exports.composeRefs = composeRefs;
97
773
  exports.copyToClipboard = copyToClipboard;
774
+ exports.createCodeHighlightLines = createCodeHighlightLines;
98
775
  exports.createHeadingIdFactory = createHeadingIdFactory;
776
+ exports.createNumberRange = createNumberRange;
777
+ exports.createObjectFileItem = createObjectFileItem;
99
778
  exports.extractHeadings = extractHeadings;
100
779
  exports.flattenText = flattenText;
780
+ exports.formatCssSize = formatCssSize;
781
+ exports.formatFileSize = formatFileSize;
782
+ exports.getFileCodeLanguage = getFileCodeLanguage;
783
+ exports.getFileExtension = getFileExtension;
784
+ exports.getFloatingPanelPosition = getFloatingPanelPosition;
785
+ exports.getFocusableElements = getFocusableElements;
786
+ exports.getVirtualScrollWindow = getVirtualScrollWindow;
787
+ exports.highlightCodeToHtml = highlightCodeToHtml;
788
+ exports.isAbortError = isAbortError;
101
789
  exports.isMediaOnlyParagraph = isMediaOnlyParagraph;
790
+ exports.isMobile = isMobile;
791
+ exports.isMobileViewport = isMobileViewport;
792
+ exports.normalizeFileProgress = normalizeFileProgress;
793
+ exports.normalizeHljsLanguage = normalizeHljsLanguage;
794
+ exports.parseCodeMeta = parseCodeMeta;
795
+ exports.requestJson = requestJson;
796
+ exports.resolveFileKind = resolveFileKind;
797
+ exports.resolveFileKindLabel = resolveFileKindLabel;
798
+ exports.resolveFilePreviewMode = resolveFilePreviewMode;
799
+ exports.resolveFilePreviewType = resolveFilePreviewType;
800
+ exports.resolveMediaAsset = resolveMediaAsset;
801
+ exports.resolveMediaVolume = resolveMediaVolume;
802
+ exports.useControllableState = useControllableState;
803
+ exports.useCopyToClipboard = useCopyToClipboard;
804
+ exports.useFloatingLayer = useFloatingLayer;
805
+ exports.useVirtualScrollWindow = useVirtualScrollWindow;