portosaurus 3.0.2 → 4.0.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 (60) hide show
  1. package/README.md +26 -126
  2. package/bin/portosaurus.mjs +8 -0
  3. package/package.json +6 -3
  4. package/src/assets/img/icon.png +0 -0
  5. package/src/assets/img/{icon.svg → svg/icon.svg} +35 -37
  6. package/src/assets/img/svg/project-blank.svg +140 -0
  7. package/src/assets/sample-resume.pdf +0 -0
  8. package/src/cli/build.mjs +2 -5
  9. package/src/cli/dev.mjs +27 -5
  10. package/src/cli/init.mjs +6 -12
  11. package/src/cli/schema.mjs +211 -0
  12. package/src/core/buildDocuConfig.mjs +305 -188
  13. package/src/core/constants.mjs +7 -1
  14. package/src/template/config.yml +150 -0
  15. package/src/template/notes/welcome.mdx +6 -0
  16. package/src/template/package.json +3 -3
  17. package/src/theme/MDXComponents.js +0 -1
  18. package/src/theme/components/AboutSection/index.js +32 -17
  19. package/src/theme/components/AboutSection/styles.module.css +151 -344
  20. package/src/theme/components/ContactSection/index.js +23 -14
  21. package/src/theme/components/ContactSection/styles.module.css +19 -8
  22. package/src/theme/components/ExperienceSection/index.js +12 -5
  23. package/src/theme/components/HeroSection/index.js +4 -3
  24. package/src/theme/components/HeroSection/styles.module.css +17 -16
  25. package/src/theme/components/NavArrow/index.js +114 -0
  26. package/src/theme/components/NavArrow/styles.module.css +107 -0
  27. package/src/theme/components/NoteIndex/index.js +66 -95
  28. package/src/theme/components/NoteIndex/styles.module.css +85 -89
  29. package/src/theme/components/Preview/components/FeedbackStates.js +3 -1
  30. package/src/theme/components/Preview/components/PreviewContent.js +91 -0
  31. package/src/theme/components/Preview/components/PreviewHeader.js +41 -33
  32. package/src/theme/components/Preview/components/Triggers/Pv.js +129 -72
  33. package/src/theme/components/Preview/components/ViewerWindow.js +198 -234
  34. package/src/theme/components/Preview/hooks/useAdaptiveSizing.js +115 -0
  35. package/src/theme/components/Preview/hooks/useDeepLinkHash.js +18 -23
  36. package/src/theme/components/Preview/hooks/useDockLayout.js +48 -8
  37. package/src/theme/components/Preview/hooks/useTouchZoom.js +118 -0
  38. package/src/theme/components/Preview/renderers/CodeRenderer.js +64 -25
  39. package/src/theme/components/Preview/state/index.js +70 -17
  40. package/src/theme/components/Preview/styles.module.css +181 -45
  41. package/src/theme/components/Preview/utils/index.js +11 -10
  42. package/src/theme/components/ProjectsSection/index.js +138 -148
  43. package/src/theme/components/ProjectsSection/styles.module.css +178 -112
  44. package/src/theme/components/SocialLinks/index.js +9 -7
  45. package/src/theme/components/Tooltip/index.js +31 -20
  46. package/src/theme/components/Tooltip/styles.module.css +101 -38
  47. package/src/theme/config/iconMappings.js +2 -0
  48. package/src/theme/css/custom.css +72 -0
  49. package/src/theme/hooks/useScrollReveal.js +30 -0
  50. package/src/theme/pages/index.js +7 -27
  51. package/src/theme/pages/notes.js +2 -2
  52. package/src/theme/pages/tasks.js +12 -11
  53. package/src/utils/cliUtils.mjs +23 -51
  54. package/src/utils/configUtils.mjs +95 -84
  55. package/src/utils/systemUtils.mjs +171 -0
  56. package/src/template/config.js +0 -68
  57. package/src/theme/components/ScrollToTop/index.js +0 -95
  58. package/src/theme/components/ScrollToTop/styles.module.css +0 -97
  59. package/src/theme/config/metaTags.js +0 -21
  60. /package/src/template/{.nojekyll → static/.nojekyll} +0 -0
@@ -0,0 +1,115 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Hook for managing responsive sizing, positioning, and adaptive anchoring.
5
+ * Handles the 3-tier layout logic (Phone, Tablet, Desktop) and window management.
6
+ */
7
+ export function useAdaptiveSizing({
8
+ mode,
9
+ windowWidth,
10
+ floatingState,
11
+ dockWidth,
12
+ peekHeight,
13
+ setFloatingState,
14
+ }) {
15
+ // --- Layout calculations (3-Tier Adaptive) ---
16
+ const isPhone = windowWidth <= 480;
17
+ const isMobile = windowWidth <= 768;
18
+ const isTablet = windowWidth > 480 && windowWidth <= 996;
19
+ const isDesktop = windowWidth > 996;
20
+
21
+ const isPopupMode = mode === "popup";
22
+ const isDockMode = mode === "dock" && isDesktop;
23
+ const showAsPeek = mode === "dock" && !isDesktop;
24
+ const isPipMode = mode === "pip";
25
+
26
+ // --- Adaptive Positioning (stick to right on resize) ---
27
+ const prevWidthRef = useRef(windowWidth);
28
+ useEffect(() => {
29
+ if (floatingState.x !== null && !isDockMode && !isMobile) {
30
+ const wasOnRight = floatingState.x > prevWidthRef.current / 2;
31
+ if (wasOnRight) {
32
+ const delta = windowWidth - prevWidthRef.current;
33
+ setFloatingState((prev) => ({ ...prev, x: prev.x + delta }));
34
+ }
35
+ }
36
+ prevWidthRef.current = windowWidth;
37
+ }, [windowWidth, isDockMode, isMobile, floatingState.x, setFloatingState]);
38
+
39
+ // --- Sizing math ---
40
+ const vh =
41
+ typeof window !== "undefined" ? document.documentElement.clientHeight : 800;
42
+
43
+ // PiP sizing logic
44
+ const pipWidth = isPhone
45
+ ? windowWidth
46
+ : isTablet
47
+ ? Math.min(600, windowWidth - 60)
48
+ : floatingState.width;
49
+
50
+ const pipHeight = isPhone
51
+ ? Math.min(floatingState.height, vh * 0.7)
52
+ : isTablet
53
+ ? Math.min(450, vh * 0.6)
54
+ : floatingState.height;
55
+
56
+ // Positioning logic
57
+ const marginX = isPhone ? 0 : 20;
58
+ const marginY = isPhone ? 0 : 20;
59
+
60
+ const defaultPipX = isTablet
61
+ ? (windowWidth - pipWidth) / 2
62
+ : isPhone
63
+ ? 0
64
+ : Math.max(16, windowWidth - pipWidth - marginX);
65
+
66
+ const defaultPipY = isPhone
67
+ ? vh - pipHeight
68
+ : Math.max(16, vh - pipHeight - marginY);
69
+
70
+ let rndX = floatingState.x ?? defaultPipX;
71
+ let rndY = floatingState.y ?? defaultPipY;
72
+
73
+ // Safety clamping for floating windows (PiP)
74
+ if (!isDockMode && !isPhone && floatingState.x !== null) {
75
+ rndX = Math.min(rndX, windowWidth - pipWidth - 10);
76
+ rndX = Math.max(10, rndX);
77
+ rndY = Math.min(rndY, vh - pipHeight - 10);
78
+ rndY = Math.max(10, rndY);
79
+ }
80
+
81
+ const rndPosition = isDockMode
82
+ ? { x: windowWidth - dockWidth, y: 0 }
83
+ : showAsPeek
84
+ ? { x: 0, y: Math.max(0, vh - peekHeight) }
85
+ : { x: rndX, y: rndY };
86
+
87
+ const rndSize = isDockMode
88
+ ? { width: dockWidth, height: vh }
89
+ : showAsPeek
90
+ ? { width: windowWidth, height: peekHeight }
91
+ : { width: pipWidth, height: pipHeight };
92
+
93
+ const rndBounds = isPhone
94
+ ? { left: 0, top: 0, right: windowWidth, bottom: vh }
95
+ : isDockMode
96
+ ? { left: 0, top: 0, right: windowWidth, bottom: vh }
97
+ : "parent";
98
+
99
+ return {
100
+ isPhone,
101
+ isMobile,
102
+ isTablet,
103
+ isDesktop,
104
+ isPopupMode,
105
+ isDockMode,
106
+ showAsPeek,
107
+ isPipMode,
108
+ rndPosition,
109
+ rndSize,
110
+ rndBounds,
111
+ pipWidth,
112
+ pipHeight,
113
+ vh,
114
+ };
115
+ }
@@ -1,38 +1,33 @@
1
- import { useEffect, useRef } from "react";
1
+ import { useEffect } from "react";
2
2
  import { generatePvSlug, generatePvHash } from "../utils";
3
3
 
4
4
  /**
5
5
  * Syncs the URL hash with the active preview tab.
6
6
  * Also scrolls the active tab element into view.
7
7
  */
8
- export function useDeepLinkHash(
9
- isOpen,
10
- sources,
11
- activeIndex,
12
- tabRefs,
13
- isDocked,
14
- ) {
8
+ export function useDeepLinkHash(isOpen, sources, activeIndex, mode, baseSlug) {
15
9
  useEffect(() => {
16
10
  if (!isOpen) return;
17
11
 
18
- const src = sources[activeIndex];
19
- if (src) {
20
- const slug = generatePvSlug(src.label, src.path);
21
- const newHash = generatePvHash(slug, isDocked);
12
+ let slug = baseSlug;
13
+ if (sources && sources.length > 1) {
14
+ const src = sources[activeIndex];
15
+ if (src) {
16
+ // For multi-tab previews, we append a slugified version of the tab's
17
+ // specific label or filename so each tab has a unique deep link
18
+ const rawLabel =
19
+ src.label ||
20
+ (src.path ? src.path.split(/[?#]/)[0].split("/").pop() : "tab");
21
+ const tabSlug = generatePvSlug(rawLabel);
22
+ slug = baseSlug ? `${baseSlug}-${tabSlug}` : tabSlug;
23
+ }
24
+ }
22
25
 
26
+ if (slug) {
27
+ const newHash = generatePvHash(slug, mode);
23
28
  if (window.location.hash !== `#${newHash}`) {
24
29
  window.history.replaceState(null, "", `#${newHash}`);
25
30
  }
26
31
  }
27
-
28
- // Scroll active tab into view
29
- const activeTabEl = tabRefs.current?.[activeIndex];
30
- if (activeTabEl) {
31
- activeTabEl.scrollIntoView({
32
- behavior: "smooth",
33
- block: "nearest",
34
- inline: "center",
35
- });
36
- }
37
- }, [activeIndex, isOpen, sources, isDocked]);
32
+ }, [activeIndex, isOpen, sources, mode, baseSlug]);
38
33
  }
@@ -7,17 +7,28 @@ import { useEffect, useRef } from "react";
7
7
  * - Collapses/expands the Docusaurus sidebar
8
8
  * - Tracks navbar height for dock top offset
9
9
  */
10
- export function useDockLayout(isOpen, isDocked, dockWidth) {
10
+ export function useDockLayout({
11
+ isOpen,
12
+ isPopupMode,
13
+ isSidebarDock,
14
+ isPeekDock,
15
+ dockWidth,
16
+ peekHeight,
17
+ }) {
11
18
  const weCollapsedSidebar = useRef(false);
12
19
 
13
20
  useEffect(() => {
14
21
  if (typeof document === "undefined") return;
15
22
 
16
- const isMobile = window.innerWidth <= 768;
17
- const desktopDockActive = isOpen && isDocked && !isMobile;
23
+ // --- Popup scroll lock ---
24
+ if (isOpen && isPopupMode) {
25
+ document.body.style.overflow = "hidden";
26
+ } else {
27
+ document.body.style.overflow = "";
28
+ }
18
29
 
19
30
  // --- Body classes & CSS vars ---
20
- if (desktopDockActive) {
31
+ if (isSidebarDock) {
21
32
  document.body.classList.add("pv-dock-active");
22
33
  document.body.style.setProperty("--pv-dock-width", `${dockWidth}px`);
23
34
  } else {
@@ -25,6 +36,17 @@ export function useDockLayout(isOpen, isDocked, dockWidth) {
25
36
  document.body.style.setProperty("--pv-dock-width", "0px");
26
37
  }
27
38
 
39
+ if (isPeekDock) {
40
+ document.body.classList.add("pv-peek-active");
41
+ document.body.style.setProperty(
42
+ "--mobile-peek-height",
43
+ `${peekHeight}px`,
44
+ );
45
+ } else {
46
+ document.body.classList.remove("pv-peek-active");
47
+ document.body.style.setProperty("--mobile-peek-height", "0px");
48
+ }
49
+
28
50
  // --- Docusaurus sidebar auto-collapse ---
29
51
  const sidebarToggleBtn = document.querySelector(
30
52
  '[class*="collapseSidebarButton"]',
@@ -33,7 +55,7 @@ export function useDockLayout(isOpen, isDocked, dockWidth) {
33
55
  '[aria-label="Expand sidebar"]',
34
56
  );
35
57
 
36
- if (desktopDockActive) {
58
+ if (isSidebarDock) {
37
59
  if (sidebarToggleBtn && !isCollapsed) {
38
60
  weCollapsedSidebar.current = true;
39
61
  sidebarToggleBtn.click();
@@ -56,15 +78,33 @@ export function useDockLayout(isOpen, isDocked, dockWidth) {
56
78
  }
57
79
  };
58
80
 
59
- if (desktopDockActive) {
81
+ if (isSidebarDock) {
60
82
  updateNavOffset();
61
83
  window.addEventListener("resize", updateNavOffset, { passive: true });
62
84
  }
63
85
 
64
86
  return () => {
65
87
  document.body.classList.remove("pv-dock-active");
66
- document.documentElement.style.removeProperty("--dock-top-offset");
88
+ document.body.classList.remove("pv-peek-active");
89
+ document.body.style.overflow = "";
90
+ document.body.style.removeProperty("--pv-dock-width");
91
+ document.body.style.removeProperty("--mobile-peek-height");
92
+
93
+ // Explicitly restore sidebar if we were the ones who collapsed it
94
+ // This prevents Docusaurus from "remembering" the collapsed state after reload
95
+ if (weCollapsedSidebar.current) {
96
+ const sidebarToggleBtn = document.querySelector(
97
+ '[class*="collapseSidebarButton"]',
98
+ );
99
+ const isCollapsed = !!document.querySelector(
100
+ '[aria-label="Expand sidebar"]',
101
+ );
102
+ if (sidebarToggleBtn && isCollapsed) {
103
+ sidebarToggleBtn.click();
104
+ }
105
+ weCollapsedSidebar.current = false;
106
+ }
67
107
  window.removeEventListener("resize", updateNavOffset);
68
108
  };
69
- }, [isOpen, isDocked, dockWidth]);
109
+ }, [isOpen, isPopupMode, isSidebarDock, isPeekDock, dockWidth, peekHeight]);
70
110
  }
@@ -0,0 +1,118 @@
1
+ import { useEffect } from "react";
2
+ import styles from "../styles.module.css";
3
+
4
+ /**
5
+ * Hook for managing touch interactions:
6
+ * - Pinch-to-zoom (touch)
7
+ * - Trackpad zooming (ctrl + wheel)
8
+ * - Click-and-drag panning
9
+ */
10
+ export function useTouchZoom({
11
+ containerRef,
12
+ isOpen,
13
+ zoomLevel,
14
+ setZoomLevel,
15
+ }) {
16
+ useEffect(() => {
17
+ const el = containerRef.current;
18
+ if (!el || !isOpen) return;
19
+
20
+ let initialDistance = null;
21
+ let initialZoom = zoomLevel;
22
+
23
+ const getDistance = (touches) => {
24
+ return Math.hypot(
25
+ touches[0].clientX - touches[1].clientX,
26
+ touches[0].clientY - touches[1].clientY,
27
+ );
28
+ };
29
+
30
+ const handleWheel = (e) => {
31
+ if (e.ctrlKey) {
32
+ e.preventDefault();
33
+ const delta = -e.deltaY * 0.01;
34
+ setZoomLevel((prev) => Math.min(Math.max(0.5, prev + delta), 3.0));
35
+ }
36
+ };
37
+
38
+ const handleTouchStart = (e) => {
39
+ if (e.touches.length === 2) {
40
+ initialDistance = getDistance(e.touches);
41
+ setZoomLevel((prev) => {
42
+ initialZoom = prev;
43
+ return prev;
44
+ });
45
+ }
46
+ };
47
+
48
+ const handleTouchMove = (e) => {
49
+ if (e.touches.length === 2 && initialDistance) {
50
+ e.preventDefault();
51
+ const currentDistance = getDistance(e.touches);
52
+ const ratio = currentDistance / initialDistance;
53
+ setZoomLevel(Math.min(Math.max(0.5, initialZoom * ratio), 3.0));
54
+ }
55
+ };
56
+
57
+ const handleTouchEnd = (e) => {
58
+ if (e.touches.length < 2) {
59
+ initialDistance = null;
60
+ }
61
+ };
62
+
63
+ // --- Mouse Click-and-Drag Panning ---
64
+ let isPanning = false;
65
+ let startX = 0;
66
+ let startY = 0;
67
+ let initialScrollLeft = 0;
68
+ let initialScrollTop = 0;
69
+
70
+ const handleMouseDown = (e) => {
71
+ if (e.button !== 0) return;
72
+ isPanning = true;
73
+ startX = e.pageX;
74
+ startY = e.pageY;
75
+ initialScrollLeft = el.scrollLeft;
76
+ initialScrollTop = el.scrollTop;
77
+ el.classList.add(styles.isPanning);
78
+ document.body.classList.add(styles.isPanning);
79
+ };
80
+
81
+ const handleMouseMove = (e) => {
82
+ if (!isPanning) return;
83
+ e.preventDefault();
84
+ const x = e.pageX;
85
+ const y = e.pageY;
86
+ const walkX = (x - startX) * 1;
87
+ const walkY = (y - startY) * 1;
88
+ el.scrollLeft = initialScrollLeft - walkX;
89
+ el.scrollTop = initialScrollTop - walkY;
90
+ };
91
+
92
+ const handleMouseUpOrLeave = () => {
93
+ isPanning = false;
94
+ el.classList.remove(styles.isPanning);
95
+ document.body.classList.remove(styles.isPanning);
96
+ };
97
+
98
+ el.addEventListener("wheel", handleWheel, { passive: false });
99
+ el.addEventListener("touchstart", handleTouchStart, { passive: false });
100
+ el.addEventListener("touchmove", handleTouchMove, { passive: false });
101
+ el.addEventListener("touchend", handleTouchEnd);
102
+ el.addEventListener("mousedown", handleMouseDown);
103
+ el.addEventListener("mousemove", handleMouseMove);
104
+ el.addEventListener("mouseup", handleMouseUpOrLeave);
105
+ el.addEventListener("mouseleave", handleMouseUpOrLeave);
106
+
107
+ return () => {
108
+ el.removeEventListener("wheel", handleWheel);
109
+ el.removeEventListener("touchstart", handleTouchStart);
110
+ el.removeEventListener("touchmove", handleTouchMove);
111
+ el.removeEventListener("touchend", handleTouchEnd);
112
+ el.removeEventListener("mousedown", handleMouseDown);
113
+ el.removeEventListener("mousemove", handleMouseMove);
114
+ el.removeEventListener("mouseup", handleMouseUpOrLeave);
115
+ el.removeEventListener("mouseleave", handleMouseUpOrLeave);
116
+ };
117
+ }, [isOpen, setZoomLevel, containerRef, zoomLevel]);
118
+ }
@@ -5,8 +5,9 @@ import { Highlight } from "prism-react-renderer";
5
5
  /**
6
6
  * Self-contained code highlighter using prism-react-renderer.
7
7
  * Reads the Prism theme directly from Docusaurus site config.
8
+ * Safe to use in Root.js because it doesn't depend on Docusaurus context providers.
8
9
  */
9
- export default function CodeRenderer({ code, language }) {
10
+ export default function CodeRenderer({ code, language, zoomLevel = 1.0 }) {
10
11
  const { siteConfig } = useDocusaurusContext();
11
12
  const isDark =
12
13
  typeof document !== "undefined" &&
@@ -15,8 +16,18 @@ export default function CodeRenderer({ code, language }) {
15
16
  const prismConfig = siteConfig?.themeConfig?.prism || {};
16
17
  const prismTheme = isDark ? prismConfig.darkTheme : prismConfig.theme;
17
18
 
19
+ // Normalize language (Prism uses 'diff' for both .diff and .patch)
20
+ const normalizedLanguage = language === "patch" ? "diff" : language || "text";
21
+
18
22
  return (
19
- <Highlight code={code} language={language || "text"} theme={prismTheme}>
23
+ <Highlight
24
+ code={code}
25
+ language={normalizedLanguage}
26
+ theme={prismTheme}
27
+ {...(typeof window !== "undefined" && window.Prism
28
+ ? { prism: window.Prism }
29
+ : {})}
30
+ >
20
31
  {({ className, style, tokens, getLineProps, getTokenProps }) => (
21
32
  <pre
22
33
  className={className}
@@ -25,37 +36,65 @@ export default function CodeRenderer({ code, language }) {
25
36
  margin: 0,
26
37
  borderRadius: 0,
27
38
  padding: "14px 0",
28
- fontSize: "0.85rem",
39
+ fontSize: `calc(0.85rem * ${zoomLevel})`,
29
40
  lineHeight: 1.6,
30
41
  minHeight: "100%",
31
42
  background: "var(--ifm-background-color)",
32
43
  }}
33
44
  >
34
- {tokens.map((line, i) => (
35
- <div
36
- key={i}
37
- {...getLineProps({ line })}
38
- style={{ display: "flex" }}
39
- >
40
- <span
45
+ {tokens.map((line, i) => {
46
+ const lineProps = getLineProps({ line });
47
+ const lineContent = line.map((t) => t.content).join("");
48
+
49
+ // --- Robust Diff Highlighting ---
50
+ let diffStyle = {};
51
+ if (normalizedLanguage === "diff") {
52
+ if (lineContent.startsWith("+")) {
53
+ diffStyle = {
54
+ background: "rgba(var(--ifm-color-success-rgb), 0.15)",
55
+ borderLeft: "3px solid var(--ifm-color-success)",
56
+ };
57
+ } else if (lineContent.startsWith("-")) {
58
+ diffStyle = {
59
+ background: "rgba(var(--ifm-color-danger-rgb), 0.15)",
60
+ borderLeft: "3px solid var(--ifm-color-danger)",
61
+ };
62
+ }
63
+ }
64
+
65
+ return (
66
+ <div
67
+ key={i}
68
+ {...lineProps}
41
69
  style={{
42
- display: "inline-block",
43
- width: "3em",
44
- paddingLeft: "14px",
45
- userSelect: "none",
46
- opacity: 0.4,
47
- flexShrink: 0,
70
+ ...lineProps.style,
71
+ ...diffStyle,
72
+ display: "flex",
73
+ paddingLeft: diffStyle.borderLeft ? "0px" : "3px",
48
74
  }}
49
75
  >
50
- {i + 1}
51
- </span>
52
- <span style={{ paddingRight: "14px" }}>
53
- {line.map((token, key) => (
54
- <span key={key} {...getTokenProps({ token })} />
55
- ))}
56
- </span>
57
- </div>
58
- ))}
76
+ <span
77
+ style={{
78
+ display: "inline-block",
79
+ width: "1.7em",
80
+ textAlign: "right",
81
+ marginRight: "12px",
82
+ userSelect: "none",
83
+ opacity: 0.35,
84
+ flexShrink: 0,
85
+ fontFamily: "var(--ifm-font-family-monospace)",
86
+ }}
87
+ >
88
+ {i + 1}
89
+ </span>
90
+ <span style={{ paddingRight: "14px", flex: 1 }}>
91
+ {line.map((token, key) => (
92
+ <span key={key} {...getTokenProps({ token })} />
93
+ ))}
94
+ </span>
95
+ </div>
96
+ );
97
+ })}
59
98
  </pre>
60
99
  )}
61
100
  </Highlight>
@@ -2,14 +2,14 @@ import { createContext, useContext, useReducer, useCallback } from "react";
2
2
 
3
3
  const PreviewContext = createContext(null);
4
4
 
5
- const DEFAULT_SIZE = { width: 720, height: 500 };
5
+ const DEFAULT_SIZE = { width: 720, height: 400 };
6
6
  const DEFAULT_DOCK_PERCENTAGE = 0.42; // 42% of viewport width
7
7
 
8
8
  function getInitialState() {
9
9
  if (typeof window === "undefined") {
10
10
  return {
11
11
  isOpen: false,
12
- isDocked: false,
12
+ mode: "popup",
13
13
  dockWidth: 420,
14
14
  };
15
15
  }
@@ -26,10 +26,13 @@ function getInitialState() {
26
26
 
27
27
  return {
28
28
  isOpen: false,
29
- isDocked: false,
29
+ mode: "popup",
30
30
  dockWidth: defaultDockWidth,
31
+ peekHeight: typeof window !== "undefined" ? window.innerHeight * 0.42 : 400,
31
32
  sources: [],
32
33
  activeIndex: 0,
34
+ baseSlug: null,
35
+ modeSwitch: true,
33
36
  floatingState: {
34
37
  width: DEFAULT_SIZE.width,
35
38
  height: DEFAULT_SIZE.height,
@@ -45,22 +48,40 @@ function reducer(state, action) {
45
48
  return {
46
49
  ...state,
47
50
  isOpen: true,
51
+ mode: action.mode || "popup",
48
52
  sources: action.sources,
49
53
  activeIndex: action.index ?? 0,
54
+ baseSlug: action.baseSlug,
55
+ modeSwitch: action.modeSwitch ?? true,
50
56
  };
51
57
  case "CLOSE":
52
- return { ...state, isOpen: false, isDocked: false };
53
- case "SET_DOCKED":
54
- return { ...state, isDocked: action.value };
58
+ return {
59
+ ...state,
60
+ isOpen: false,
61
+ mode: "popup",
62
+ baseSlug: null,
63
+ modeSwitch: true,
64
+ };
65
+ case "SET_MODE":
66
+ return { ...state, mode: action.mode };
55
67
  case "SET_ACTIVE_INDEX":
56
68
  return { ...state, activeIndex: action.index };
57
69
  case "SET_DOCK_WIDTH":
58
70
  return { ...state, dockWidth: action.width };
71
+ case "SET_PEEK_HEIGHT":
72
+ return { ...state, peekHeight: action.height };
59
73
  case "SET_FLOATING_STATE":
60
74
  return {
61
75
  ...state,
62
76
  floatingState: { ...state.floatingState, ...action.state },
63
77
  };
78
+ case "TOGGLE_MODE": {
79
+ let nextMode = "popup";
80
+ if (state.mode === "popup") nextMode = "dock";
81
+ else if (state.mode === "dock") nextMode = "pip";
82
+ else if (state.mode === "pip") nextMode = "dock";
83
+ return { ...state, mode: nextMode };
84
+ }
64
85
  default:
65
86
  return state;
66
87
  }
@@ -69,12 +90,29 @@ function reducer(state, action) {
69
90
  export function PreviewProvider({ children }) {
70
91
  const [state, dispatch] = useReducer(reducer, undefined, getInitialState);
71
92
 
72
- const openPreview = useCallback((sources, index = 0, hashId = null) => {
73
- if (hashId && typeof window !== "undefined") {
74
- window.history.replaceState(null, null, "#" + hashId);
75
- }
76
- dispatch({ type: "OPEN", sources, index });
77
- }, []);
93
+ const openPreview = useCallback(
94
+ (
95
+ sources,
96
+ index = 0,
97
+ hashId = null,
98
+ mode = "popup",
99
+ baseSlug = null,
100
+ modeSwitch = true,
101
+ ) => {
102
+ if (hashId && typeof window !== "undefined") {
103
+ window.history.replaceState(null, null, "#" + hashId);
104
+ }
105
+ dispatch({
106
+ type: "OPEN",
107
+ sources,
108
+ index,
109
+ mode,
110
+ baseSlug,
111
+ modeSwitch,
112
+ });
113
+ },
114
+ [],
115
+ );
78
116
 
79
117
  const closePreview = useCallback(() => {
80
118
  if (typeof window !== "undefined") {
@@ -87,8 +125,8 @@ export function PreviewProvider({ children }) {
87
125
  dispatch({ type: "CLOSE" });
88
126
  }, []);
89
127
 
90
- const setDocked = useCallback((val) => {
91
- dispatch({ type: "SET_DOCKED", value: val });
128
+ const setMode = useCallback((mode) => {
129
+ dispatch({ type: "SET_MODE", mode });
92
130
  }, []);
93
131
 
94
132
  const setActiveIndex = useCallback((index) => {
@@ -99,20 +137,30 @@ export function PreviewProvider({ children }) {
99
137
  dispatch({ type: "SET_DOCK_WIDTH", width });
100
138
  }, []);
101
139
 
140
+ const setPeekHeight = useCallback((height) => {
141
+ dispatch({ type: "SET_PEEK_HEIGHT", height });
142
+ }, []);
143
+
102
144
  const setFloatingState = useCallback((newState) => {
103
145
  dispatch({ type: "SET_FLOATING_STATE", state: newState });
104
146
  }, []);
105
147
 
148
+ const toggleMode = useCallback(() => {
149
+ dispatch({ type: "TOGGLE_MODE" });
150
+ }, []);
151
+
106
152
  return (
107
153
  <PreviewContext.Provider
108
154
  value={{
109
155
  ...state,
110
156
  openPreview,
111
157
  closePreview,
112
- setDocked,
158
+ setMode,
113
159
  setActiveIndex,
114
160
  setDockWidth,
161
+ setPeekHeight,
115
162
  setFloatingState,
163
+ toggleMode,
116
164
  }}
117
165
  >
118
166
  {children}
@@ -122,17 +170,22 @@ export function PreviewProvider({ children }) {
122
170
 
123
171
  const DEFAULT_CTX = {
124
172
  isOpen: false,
125
- isDocked: false,
173
+ mode: "popup",
126
174
  dockWidth: 420,
175
+ peekHeight: 400,
127
176
  sources: [],
128
177
  activeIndex: 0,
178
+ baseSlug: null,
179
+ modeSwitch: true,
129
180
  floatingState: { x: null, y: null, width: 800, height: 600 },
130
181
  openPreview: () => {},
131
182
  closePreview: () => {},
132
- setDocked: () => {},
183
+ setMode: () => {},
133
184
  setActiveIndex: () => {},
134
185
  setDockWidth: () => {},
186
+ setPeekHeight: () => {},
135
187
  setFloatingState: () => {},
188
+ toggleMode: () => {},
136
189
  };
137
190
 
138
191
  export function usePreview() {