@willa-ui/shared 0.0.1-alpha.8d8fe96 → 0.0.3-alpha.2739b3e

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