@portosaur/theme 0.1.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.
Files changed (76) hide show
  1. package/README.md +13 -0
  2. package/assets/img/icon-old.png +0 -0
  3. package/assets/img/icon.png +0 -0
  4. package/assets/img/project-blank.png +0 -0
  5. package/assets/img/social-card.jpeg +0 -0
  6. package/assets/img/svg/icon-blog.svg +2 -0
  7. package/assets/img/svg/icon-close.svg +3 -0
  8. package/assets/img/svg/icon-dock.svg +4 -0
  9. package/assets/img/svg/icon-link.svg +5 -0
  10. package/assets/img/svg/icon-note.svg +2 -0
  11. package/assets/img/svg/icon-popup.svg +1 -0
  12. package/assets/img/svg/icon-save.svg +5 -0
  13. package/assets/img/svg/icon.svg +240 -0
  14. package/assets/img/svg/project-blank.svg +140 -0
  15. package/assets/sample-resume.pdf +0 -0
  16. package/package.json +41 -0
  17. package/plugins/README.md +8 -0
  18. package/src/index.d.ts +11 -0
  19. package/src/index.mjs +14 -0
  20. package/src/plugins/theme.mjs +13 -0
  21. package/theme/DocCategoryGeneratedIndexPage/index.jsx +15 -0
  22. package/theme/MDXComponents.jsx +19 -0
  23. package/theme/README.md +9 -0
  24. package/theme/Root.jsx +11 -0
  25. package/theme/components/AboutSection/index.jsx +264 -0
  26. package/theme/components/AboutSection/styles.module.css +309 -0
  27. package/theme/components/ContactSection/index.jsx +188 -0
  28. package/theme/components/ContactSection/styles.module.css +343 -0
  29. package/theme/components/ExperienceSection/index.jsx +119 -0
  30. package/theme/components/ExperienceSection/styles.module.css +183 -0
  31. package/theme/components/HeroSection/index.jsx +198 -0
  32. package/theme/components/HeroSection/styles.module.css +484 -0
  33. package/theme/components/NavArrow/index.jsx +124 -0
  34. package/theme/components/NavArrow/styles.module.css +107 -0
  35. package/theme/components/NoteIndex/index.jsx +182 -0
  36. package/theme/components/NoteIndex/styles.module.css +167 -0
  37. package/theme/components/Preview/components/FeedbackStates.jsx +200 -0
  38. package/theme/components/Preview/components/FileTabs.jsx +41 -0
  39. package/theme/components/Preview/components/PreviewContent.jsx +104 -0
  40. package/theme/components/Preview/components/PreviewHeader.jsx +411 -0
  41. package/theme/components/Preview/components/Triggers/Pv.jsx +253 -0
  42. package/theme/components/Preview/components/Triggers/SrcPv.jsx +55 -0
  43. package/theme/components/Preview/components/Triggers/index.jsx +2 -0
  44. package/theme/components/Preview/components/ViewerWindow.jsx +489 -0
  45. package/theme/components/Preview/hooks/useAdaptiveSizing.jsx +90 -0
  46. package/theme/components/Preview/hooks/useDeepLinkHash.jsx +24 -0
  47. package/theme/components/Preview/hooks/useDockLayout.jsx +86 -0
  48. package/theme/components/Preview/hooks/useFileFetch.jsx +38 -0
  49. package/theme/components/Preview/hooks/useTouchZoom.jsx +98 -0
  50. package/theme/components/Preview/index.jsx +3 -0
  51. package/theme/components/Preview/renderers/CodeRenderer.jsx +124 -0
  52. package/theme/components/Preview/renderers/ImageRenderer.jsx +74 -0
  53. package/theme/components/Preview/renderers/PdfRenderer.jsx +93 -0
  54. package/theme/components/Preview/renderers/WebRenderer.jsx +59 -0
  55. package/theme/components/Preview/state/index.jsx +177 -0
  56. package/theme/components/Preview/styles.module.css +776 -0
  57. package/theme/components/Preview/utils/index.jsx +62 -0
  58. package/theme/components/ProjectsSection/index.jsx +790 -0
  59. package/theme/components/ProjectsSection/styles.module.css +900 -0
  60. package/theme/components/SocialLinks/index.jsx +115 -0
  61. package/theme/components/SocialLinks/styles.module.css +57 -0
  62. package/theme/components/Tooltip/index.jsx +104 -0
  63. package/theme/components/Tooltip/styles.module.css +168 -0
  64. package/theme/config/iconMappings.jsx +427 -0
  65. package/theme/config/prism.jsx +72 -0
  66. package/theme/config/sidebar.jsx +11 -0
  67. package/theme/css/bootstrap.css +5 -0
  68. package/theme/css/catppuccin.css +618 -0
  69. package/theme/css/custom.css +253 -0
  70. package/theme/css/tasks.css +874 -0
  71. package/theme/hooks/useScrollReveal.jsx +20 -0
  72. package/theme/pages/index.jsx +104 -0
  73. package/theme/pages/notes.jsx +131 -0
  74. package/theme/pages/tasks.jsx +989 -0
  75. package/theme/utils/HashNavigation.jsx +185 -0
  76. package/theme/utils/updateTitle.jsx +65 -0
@@ -0,0 +1,55 @@
1
+ import React, { useMemo } from "react";
2
+ import Pv, { normalizeSources } from "./Pv";
3
+ import styles from "../../styles.module.css";
4
+ export default function SrcPv(props) {
5
+ const { prefixText = "Source file: " } = props;
6
+ const srcList = useMemo(() => normalizeSources(props), [props]);
7
+ if (srcList.length === 0) return null;
8
+ return jsxDEV_7x81h0kn(
9
+ "div",
10
+ {
11
+ className: styles.sourceFooter,
12
+ children: [
13
+ jsxDEV_7x81h0kn(
14
+ "span",
15
+ { className: styles.sourceFooterLabel, children: prefixText },
16
+ undefined,
17
+ false,
18
+ undefined,
19
+ this,
20
+ ),
21
+ srcList.map((src, idx) =>
22
+ jsxDEV_7x81h0kn(
23
+ React.Fragment,
24
+ {
25
+ children: [
26
+ jsxDEV_7x81h0kn(
27
+ Pv,
28
+ {
29
+ ...props,
30
+ sources: srcList,
31
+ activeIdx: idx,
32
+ children: src.label,
33
+ },
34
+ undefined,
35
+ false,
36
+ undefined,
37
+ this,
38
+ ),
39
+ idx < srcList.length - 1 ? ", " : "",
40
+ ],
41
+ },
42
+ idx,
43
+ true,
44
+ undefined,
45
+ this,
46
+ ),
47
+ ),
48
+ ],
49
+ },
50
+ undefined,
51
+ true,
52
+ undefined,
53
+ this,
54
+ );
55
+ }
@@ -0,0 +1,2 @@
1
+ export { default as Pv } from "./Pv";
2
+ export { default as SrcPv } from "./SrcPv";
@@ -0,0 +1,489 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { useLocation } from "@docusaurus/router";
4
+ import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
5
+ import { Rnd } from "react-rnd";
6
+ import { AnimatePresence, motion } from "framer-motion";
7
+ import { usePreview } from "../state/index.js";
8
+ import { classify, getExt, resolveUrl } from "../utils/index.js";
9
+ import { useFileFetch } from "../hooks/useFileFetch.js";
10
+ import { useDockLayout } from "../hooks/useDockLayout.js";
11
+ import { useDeepLinkHash } from "../hooks/useDeepLinkHash.js";
12
+ import { useAdaptiveSizing } from "../hooks/useAdaptiveSizing.js";
13
+ import { useTouchZoom } from "../hooks/useTouchZoom.js";
14
+ import PreviewHeader from "./PreviewHeader.js";
15
+ import FileTabs from "./FileTabs.js";
16
+ import PreviewContent from "./PreviewContent.js";
17
+ import styles from "../styles.module.css";
18
+ export default function PreviewViewer() {
19
+ const {
20
+ isOpen,
21
+ mode,
22
+ sources,
23
+ activeIndex,
24
+ baseSlug,
25
+ dockWidth,
26
+ peekHeight,
27
+ modeSwitch,
28
+ closePreview,
29
+ toggleMode,
30
+ setActiveIndex,
31
+ setDockWidth,
32
+ setPeekHeight,
33
+ floatingState,
34
+ setFloatingState,
35
+ } = usePreview();
36
+ const { siteConfig } = useDocusaurusContext();
37
+ const customFields = siteConfig?.customFields;
38
+ const corsProxyList = customFields?.corsProxyList || [];
39
+ const location = useLocation();
40
+ const [mounted, setMounted] = useState(typeof window !== "undefined");
41
+ const [zoomLevel, setZoomLevel] = useState(1);
42
+ const [isOnline, setIsOnline] = useState(
43
+ typeof window !== "undefined" ? window.navigator.onLine : true,
44
+ );
45
+ const [windowWidth, setWindowWidth] = useState(
46
+ typeof window !== "undefined" ? document.documentElement.clientWidth : 1200,
47
+ );
48
+ const [isInteracting, setIsInteracting] = useState(false);
49
+ const [isDownloading, setIsDownloading] = useState(false);
50
+ const popupBodyRef = useRef(null);
51
+ useEffect(() => {
52
+ setMounted(true);
53
+ const handleOnline = () => setIsOnline(true);
54
+ const handleOffline = () => setIsOnline(false);
55
+ const handleResize = () =>
56
+ setWindowWidth(document.documentElement.clientWidth);
57
+ window.addEventListener("online", handleOnline);
58
+ window.addEventListener("offline", handleOffline);
59
+ window.addEventListener("resize", handleResize);
60
+ return () => {
61
+ setMounted(false);
62
+ window.removeEventListener("online", handleOnline);
63
+ window.removeEventListener("offline", handleOffline);
64
+ window.removeEventListener("resize", handleResize);
65
+ };
66
+ }, []);
67
+ const layout = useAdaptiveSizing({
68
+ mode,
69
+ windowWidth,
70
+ floatingState,
71
+ dockWidth,
72
+ peekHeight,
73
+ setFloatingState,
74
+ });
75
+ const { isDockMode, showAsPeek, isPipMode, isPopupMode } = layout;
76
+ useTouchZoom({ containerRef: popupBodyRef, isOpen, zoomLevel, setZoomLevel });
77
+ useDockLayout({
78
+ isOpen,
79
+ isPopupMode,
80
+ isSidebarDock: isDockMode,
81
+ isPeekDock: showAsPeek,
82
+ dockWidth,
83
+ peekHeight,
84
+ });
85
+ useDeepLinkHash(isOpen, sources, activeIndex, mode, baseSlug);
86
+ const prevPathRef = useRef(location.pathname);
87
+ useEffect(() => {
88
+ if (prevPathRef.current !== location.pathname) {
89
+ prevPathRef.current = location.pathname;
90
+ if (isOpen) closePreview();
91
+ }
92
+ }, [location.pathname, isOpen, closePreview]);
93
+ useEffect(() => {
94
+ if (isOpen) setZoomLevel(1);
95
+ }, [mode, isOpen]);
96
+ useEffect(() => {
97
+ if (!isOpen) return;
98
+ const handler = (e) => {
99
+ if (e.key === "Escape") closePreview();
100
+ };
101
+ window.addEventListener("keydown", handler, { capture: true });
102
+ return () =>
103
+ window.removeEventListener("keydown", handler, { capture: true });
104
+ }, [isOpen, closePreview]);
105
+ const currentFile = sources[activeIndex] ?? sources[0] ?? null;
106
+ const fileType = currentFile ? classify(currentFile.url) : null;
107
+ const ext = currentFile ? getExt(currentFile.url) : "";
108
+ const fileUrl = currentFile ? resolveUrl(currentFile.url) : "";
109
+ const {
110
+ content: textContent,
111
+ loading: textLoading,
112
+ errors: fetchErrors,
113
+ retry: retryFetch,
114
+ setError,
115
+ } = useFileFetch(currentFile?.url, fileType, isOpen);
116
+ const handleDownload = useCallback(async () => {
117
+ if (!fileUrl) return;
118
+ setIsDownloading(true);
119
+ try {
120
+ const downloadName =
121
+ currentFile.title || currentFile.url.split("/").pop() || "download";
122
+ const triggerBlobDownload = async (url) => {
123
+ const resp = await fetch(url, { mode: "cors", cache: "no-cache" });
124
+ if (!resp.ok) throw new Error("Fetch failed");
125
+ const blob = await resp.blob();
126
+ const blobUrl = URL.createObjectURL(blob);
127
+ const a = document.createElement("a");
128
+ a.href = blobUrl;
129
+ a.download = downloadName;
130
+ document.body.appendChild(a);
131
+ a.click();
132
+ document.body.removeChild(a);
133
+ setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
134
+ };
135
+ try {
136
+ const bustUrl = fileUrl.includes("?")
137
+ ? `${fileUrl}&cb=${Date.now()}`
138
+ : `${fileUrl}?cb=${Date.now()}`;
139
+ await triggerBlobDownload(bustUrl);
140
+ } catch (e1) {
141
+ let proxySuccess = false;
142
+ for (const proxyBaseUrl of corsProxyList) {
143
+ try {
144
+ const proxyUrl = `${proxyBaseUrl}${encodeURIComponent(fileUrl)}`;
145
+ await triggerBlobDownload(proxyUrl);
146
+ proxySuccess = true;
147
+ break;
148
+ } catch (pE) {}
149
+ }
150
+ if (!proxySuccess) {
151
+ const link = document.createElement("a");
152
+ link.href = fileUrl;
153
+ link.target = "_blank";
154
+ link.setAttribute("download", downloadName);
155
+ document.body.appendChild(link);
156
+ link.click();
157
+ document.body.removeChild(link);
158
+ }
159
+ }
160
+ } finally {
161
+ setIsDownloading(false);
162
+ }
163
+ }, [fileUrl, currentFile, corsProxyList]);
164
+ if (!mounted || !currentFile) return null;
165
+ const displayTitle =
166
+ currentFile.title ||
167
+ (fileType === "web"
168
+ ? currentFile.url.replace(/^https?:\/\//, "").split("/")[0]
169
+ : currentFile.url.split("/").pop() || "File");
170
+ const header = jsxDEV_7x81h0kn(
171
+ "div",
172
+ {
173
+ className: styles.headerWrapper,
174
+ children: [
175
+ showAsPeek &&
176
+ jsxDEV_7x81h0kn(
177
+ "div",
178
+ { className: styles.peekHandle },
179
+ undefined,
180
+ false,
181
+ undefined,
182
+ this,
183
+ ),
184
+ jsxDEV_7x81h0kn(
185
+ PreviewHeader,
186
+ {
187
+ displayTitle,
188
+ fileType,
189
+ fileUrl,
190
+ mode,
191
+ zoomLevel,
192
+ onZoomChange: setZoomLevel,
193
+ onToggleMode: toggleMode,
194
+ onClose: closePreview,
195
+ onDownload: handleDownload,
196
+ isDownloading,
197
+ modeSwitch,
198
+ },
199
+ undefined,
200
+ false,
201
+ undefined,
202
+ this,
203
+ ),
204
+ ],
205
+ },
206
+ undefined,
207
+ true,
208
+ undefined,
209
+ this,
210
+ );
211
+ const innerContent = jsxDEV_7x81h0kn(
212
+ "div",
213
+ {
214
+ className: styles.windowContent,
215
+ children: [
216
+ jsxDEV_7x81h0kn(
217
+ FileTabs,
218
+ { sources, activeIndex, onSelect: setActiveIndex },
219
+ undefined,
220
+ false,
221
+ undefined,
222
+ this,
223
+ ),
224
+ jsxDEV_7x81h0kn(
225
+ "div",
226
+ {
227
+ className: `${styles.popupBody} ${fileType === "code" ? styles.isText : styles.isGrabbable}`,
228
+ ref: (el) => {
229
+ popupBodyRef.current = el;
230
+ if (el && isOpen) el.focus({ preventScroll: true });
231
+ },
232
+ tabIndex: -1,
233
+ children: jsxDEV_7x81h0kn(
234
+ PreviewContent,
235
+ {
236
+ currentFile,
237
+ fileType,
238
+ fileUrl,
239
+ isOnline,
240
+ fetchErrors,
241
+ textLoading,
242
+ textContent,
243
+ zoomLevel,
244
+ ext,
245
+ retryFetch,
246
+ setError,
247
+ },
248
+ undefined,
249
+ false,
250
+ undefined,
251
+ this,
252
+ ),
253
+ },
254
+ undefined,
255
+ false,
256
+ undefined,
257
+ this,
258
+ ),
259
+ ],
260
+ },
261
+ undefined,
262
+ true,
263
+ undefined,
264
+ this,
265
+ );
266
+ const rndEnableResizing = isDockMode
267
+ ? { left: true }
268
+ : showAsPeek
269
+ ? { top: true }
270
+ : true;
271
+ const rndResizeHandleStyles = showAsPeek
272
+ ? {
273
+ top: {
274
+ height: "24px",
275
+ top: "-12px",
276
+ cursor: "row-resize",
277
+ zIndex: 100,
278
+ },
279
+ }
280
+ : isDockMode
281
+ ? { left: { width: "20px", left: "-10px" } }
282
+ : isPipMode
283
+ ? {
284
+ bottom: { height: "20px", bottom: "-10px" },
285
+ right: { width: "20px", right: "-10px" },
286
+ left: { width: "20px", left: "-10px" },
287
+ top: { height: "20px", top: "-10px" },
288
+ bottomRight: {
289
+ width: "30px",
290
+ height: "30px",
291
+ bottom: "-15px",
292
+ right: "-15px",
293
+ },
294
+ bottomLeft: {
295
+ width: "30px",
296
+ height: "30px",
297
+ bottom: "-15px",
298
+ left: "-15px",
299
+ },
300
+ topRight: {
301
+ width: "30px",
302
+ height: "30px",
303
+ top: "-15px",
304
+ right: "-15px",
305
+ },
306
+ topLeft: {
307
+ width: "30px",
308
+ height: "30px",
309
+ top: "-15px",
310
+ left: "-15px",
311
+ },
312
+ }
313
+ : {};
314
+ const rndMinWidth = isDockMode ? 380 : showAsPeek ? windowWidth : 380;
315
+ const rndMinHeight = showAsPeek ? 150 : isDockMode ? undefined : 60;
316
+ const rndMaxWidth = isDockMode
317
+ ? windowWidth * 0.8
318
+ : showAsPeek
319
+ ? windowWidth
320
+ : undefined;
321
+ const rndMaxHeight = showAsPeek ? layout.vh * 0.85 : undefined;
322
+ return createPortal(
323
+ jsxDEV_7x81h0kn(
324
+ AnimatePresence,
325
+ {
326
+ children:
327
+ isOpen &&
328
+ jsxDEV_7x81h0kn(
329
+ motion.div,
330
+ {
331
+ id: "pv-viewer",
332
+ "data-mode": mode,
333
+ className: `${styles.previewSystem} ${showAsPeek ? styles.modePeek : ""} ${isDockMode ? styles.modeDock : ""} ${isPipMode ? styles.modePip : ""} ${isPopupMode ? styles.modePopup : ""}`,
334
+ initial: { opacity: 0 },
335
+ animate: { opacity: 1 },
336
+ exit: { opacity: 0 },
337
+ transition: { duration: 0.2 },
338
+ onWheel: (e) => e.stopPropagation(),
339
+ children: [
340
+ isPopupMode &&
341
+ jsxDEV_7x81h0kn(
342
+ "div",
343
+ {
344
+ className: styles.previewBackdrop,
345
+ onClick: closePreview,
346
+ },
347
+ undefined,
348
+ false,
349
+ undefined,
350
+ this,
351
+ ),
352
+ isPopupMode
353
+ ? jsxDEV_7x81h0kn(
354
+ motion.div,
355
+ {
356
+ className: styles.windowFrame,
357
+ initial: {
358
+ opacity: 0,
359
+ scale: 0.9,
360
+ y: "-45%",
361
+ x: "-50%",
362
+ },
363
+ animate: { opacity: 1, scale: 1, y: "-50%", x: "-50%" },
364
+ exit: { opacity: 0, scale: 0.9, y: "-45%", x: "-50%" },
365
+ transition: {
366
+ type: "spring",
367
+ damping: 25,
368
+ stiffness: 300,
369
+ },
370
+ onClick: (e) => e.stopPropagation(),
371
+ children: [
372
+ jsxDEV_7x81h0kn(
373
+ "div",
374
+ {
375
+ className: styles.dragHandleWrapper,
376
+ children: header,
377
+ },
378
+ undefined,
379
+ false,
380
+ undefined,
381
+ this,
382
+ ),
383
+ innerContent,
384
+ ],
385
+ },
386
+ "desktop-popup",
387
+ true,
388
+ undefined,
389
+ this,
390
+ )
391
+ : jsxDEV_7x81h0kn(
392
+ Rnd,
393
+ {
394
+ position: layout.rndPosition,
395
+ size: layout.rndSize,
396
+ disableDragging: isDockMode || showAsPeek,
397
+ enableResizing: rndEnableResizing,
398
+ dragHandleClassName: styles.dragHandleWrapper,
399
+ minWidth: rndMinWidth,
400
+ minHeight: rndMinHeight,
401
+ maxWidth: rndMaxWidth,
402
+ maxHeight: rndMaxHeight,
403
+ bounds: layout.rndBounds,
404
+ resizeHandleStyles: rndResizeHandleStyles,
405
+ onDragStart: () => setIsInteracting(true),
406
+ onDragStop: (_e, d) => {
407
+ setIsInteracting(false);
408
+ if (!isDockMode && !showAsPeek)
409
+ setFloatingState({ x: d.x, y: d.y });
410
+ },
411
+ onResizeStart: () => setIsInteracting(true),
412
+ onResizeStop: (
413
+ _e,
414
+ _direction,
415
+ ref,
416
+ _delta,
417
+ position,
418
+ ) => {
419
+ setIsInteracting(false);
420
+ const newWidth = parseInt(ref.style.width, 10);
421
+ const newHeight = parseInt(ref.style.height, 10);
422
+ if (isDockMode) setDockWidth(newWidth);
423
+ else if (showAsPeek) setPeekHeight(newHeight);
424
+ else
425
+ setFloatingState({
426
+ width: newWidth,
427
+ height: newHeight,
428
+ ...position,
429
+ });
430
+ },
431
+ className: styles.rndWrapper,
432
+ style: {
433
+ zIndex: 10,
434
+ transition: isInteracting
435
+ ? "none"
436
+ : "all 0.4s cubic-bezier(0.22, 1, 0.36, 1)",
437
+ },
438
+ children: jsxDEV_7x81h0kn(
439
+ "div",
440
+ {
441
+ className: `${styles.windowFrame} ${isInteracting ? styles.windowInteracting : ""}`,
442
+ style: {
443
+ width: "100%",
444
+ height: "100%",
445
+ position: "relative",
446
+ },
447
+ onClick: (e) => e.stopPropagation(),
448
+ children: [
449
+ jsxDEV_7x81h0kn(
450
+ "div",
451
+ {
452
+ className: styles.dragHandleWrapper,
453
+ children: header,
454
+ },
455
+ undefined,
456
+ false,
457
+ undefined,
458
+ this,
459
+ ),
460
+ innerContent,
461
+ ],
462
+ },
463
+ undefined,
464
+ true,
465
+ undefined,
466
+ this,
467
+ ),
468
+ },
469
+ `${mode}-${showAsPeek}`,
470
+ false,
471
+ undefined,
472
+ this,
473
+ ),
474
+ ],
475
+ },
476
+ undefined,
477
+ true,
478
+ undefined,
479
+ this,
480
+ ),
481
+ },
482
+ undefined,
483
+ false,
484
+ undefined,
485
+ this,
486
+ ),
487
+ document.body,
488
+ );
489
+ }
@@ -0,0 +1,90 @@
1
+ import { useEffect, useRef } from "react";
2
+ export function useAdaptiveSizing({
3
+ mode,
4
+ windowWidth,
5
+ floatingState,
6
+ dockWidth,
7
+ peekHeight,
8
+ setFloatingState,
9
+ }) {
10
+ const isPhone = windowWidth <= 480;
11
+ const isMobile = windowWidth <= 768;
12
+ const isTablet = windowWidth > 480 && windowWidth <= 996;
13
+ const isDesktop = windowWidth > 996;
14
+ const isPopupMode = mode === "popup";
15
+ const isDockMode = mode === "dock" && isDesktop;
16
+ const showAsPeek = mode === "dock" && !isDesktop;
17
+ const isPipMode = mode === "pip";
18
+ const prevWidthRef = useRef(windowWidth);
19
+ useEffect(() => {
20
+ if (floatingState.x !== null && !isDockMode && !isMobile) {
21
+ const wasOnRight = floatingState.x > prevWidthRef.current / 2;
22
+ if (wasOnRight) {
23
+ const delta = windowWidth - prevWidthRef.current;
24
+ setFloatingState((prev) => ({ ...prev, x: prev.x + delta }));
25
+ }
26
+ }
27
+ prevWidthRef.current = windowWidth;
28
+ }, [windowWidth, isDockMode, isMobile, floatingState.x, setFloatingState]);
29
+ const vh =
30
+ typeof window !== "undefined" ? document.documentElement.clientHeight : 800;
31
+ const pipWidth = isPhone
32
+ ? windowWidth
33
+ : isTablet
34
+ ? Math.min(600, windowWidth - 60)
35
+ : floatingState.width;
36
+ const pipHeight = isPhone
37
+ ? Math.min(floatingState.height, vh * 0.7)
38
+ : isTablet
39
+ ? Math.min(450, vh * 0.6)
40
+ : floatingState.height;
41
+ const marginX = isPhone ? 0 : 20;
42
+ const marginY = isPhone ? 0 : 20;
43
+ const defaultPipX = isTablet
44
+ ? (windowWidth - pipWidth) / 2
45
+ : isPhone
46
+ ? 0
47
+ : Math.max(16, windowWidth - pipWidth - marginX);
48
+ const defaultPipY = isPhone
49
+ ? vh - pipHeight
50
+ : Math.max(16, vh - pipHeight - marginY);
51
+ let rndX = floatingState.x ?? defaultPipX;
52
+ let rndY = floatingState.y ?? defaultPipY;
53
+ if (!isDockMode && !isPhone && floatingState.x !== null) {
54
+ rndX = Math.min(rndX, windowWidth - pipWidth - 10);
55
+ rndX = Math.max(10, rndX);
56
+ rndY = Math.min(rndY, vh - pipHeight - 10);
57
+ rndY = Math.max(10, rndY);
58
+ }
59
+ const rndPosition = isDockMode
60
+ ? { x: windowWidth - dockWidth, y: 0 }
61
+ : showAsPeek
62
+ ? { x: 0, y: Math.max(0, vh - peekHeight) }
63
+ : { x: rndX, y: rndY };
64
+ const rndSize = isDockMode
65
+ ? { width: dockWidth, height: vh }
66
+ : showAsPeek
67
+ ? { width: windowWidth, height: peekHeight }
68
+ : { width: pipWidth, height: pipHeight };
69
+ const rndBounds = isPhone
70
+ ? { left: 0, top: 0, right: windowWidth, bottom: vh }
71
+ : isDockMode
72
+ ? { left: 0, top: 0, right: windowWidth, bottom: vh }
73
+ : "parent";
74
+ return {
75
+ isPhone,
76
+ isMobile,
77
+ isTablet,
78
+ isDesktop,
79
+ isPopupMode,
80
+ isDockMode,
81
+ showAsPeek,
82
+ isPipMode,
83
+ rndPosition,
84
+ rndSize,
85
+ rndBounds,
86
+ pipWidth,
87
+ pipHeight,
88
+ vh,
89
+ };
90
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect } from "react";
2
+ import { generatePvSlug, generatePvHash } from "../utils";
3
+ export function useDeepLinkHash(isOpen, sources, activeIndex, mode, baseSlug) {
4
+ useEffect(() => {
5
+ if (!isOpen) return;
6
+ let slug = baseSlug;
7
+ if (sources && sources.length > 1) {
8
+ const src = sources[activeIndex];
9
+ if (src) {
10
+ const rawLabel =
11
+ src.label ||
12
+ (src.path ? src.path.split(/[?#]/)[0].split("/").pop() : "tab");
13
+ const tabSlug = generatePvSlug(rawLabel);
14
+ slug = baseSlug ? `${baseSlug}-${tabSlug}` : tabSlug;
15
+ }
16
+ }
17
+ if (slug) {
18
+ const newHash = generatePvHash(slug, mode);
19
+ if (window.location.hash !== `#${newHash}`) {
20
+ window.history.replaceState(null, "", `#${newHash}`);
21
+ }
22
+ }
23
+ }, [activeIndex, isOpen, sources, mode, baseSlug]);
24
+ }