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.
- package/README.md +26 -126
- package/bin/portosaurus.mjs +8 -0
- package/package.json +6 -3
- package/src/assets/img/icon.png +0 -0
- package/src/assets/img/{icon.svg → svg/icon.svg} +35 -37
- package/src/assets/img/svg/project-blank.svg +140 -0
- package/src/assets/sample-resume.pdf +0 -0
- package/src/cli/build.mjs +2 -5
- package/src/cli/dev.mjs +27 -5
- package/src/cli/init.mjs +6 -12
- package/src/cli/schema.mjs +211 -0
- package/src/core/buildDocuConfig.mjs +305 -188
- package/src/core/constants.mjs +7 -1
- package/src/template/config.yml +150 -0
- package/src/template/notes/welcome.mdx +6 -0
- package/src/template/package.json +3 -3
- package/src/theme/MDXComponents.js +0 -1
- package/src/theme/components/AboutSection/index.js +32 -17
- package/src/theme/components/AboutSection/styles.module.css +151 -344
- package/src/theme/components/ContactSection/index.js +23 -14
- package/src/theme/components/ContactSection/styles.module.css +19 -8
- package/src/theme/components/ExperienceSection/index.js +12 -5
- package/src/theme/components/HeroSection/index.js +4 -3
- package/src/theme/components/HeroSection/styles.module.css +17 -16
- package/src/theme/components/NavArrow/index.js +114 -0
- package/src/theme/components/NavArrow/styles.module.css +107 -0
- package/src/theme/components/NoteIndex/index.js +66 -95
- package/src/theme/components/NoteIndex/styles.module.css +85 -89
- package/src/theme/components/Preview/components/FeedbackStates.js +3 -1
- package/src/theme/components/Preview/components/PreviewContent.js +91 -0
- package/src/theme/components/Preview/components/PreviewHeader.js +41 -33
- package/src/theme/components/Preview/components/Triggers/Pv.js +129 -72
- package/src/theme/components/Preview/components/ViewerWindow.js +198 -234
- package/src/theme/components/Preview/hooks/useAdaptiveSizing.js +115 -0
- package/src/theme/components/Preview/hooks/useDeepLinkHash.js +18 -23
- package/src/theme/components/Preview/hooks/useDockLayout.js +48 -8
- package/src/theme/components/Preview/hooks/useTouchZoom.js +118 -0
- package/src/theme/components/Preview/renderers/CodeRenderer.js +64 -25
- package/src/theme/components/Preview/state/index.js +70 -17
- package/src/theme/components/Preview/styles.module.css +181 -45
- package/src/theme/components/Preview/utils/index.js +11 -10
- package/src/theme/components/ProjectsSection/index.js +138 -148
- package/src/theme/components/ProjectsSection/styles.module.css +178 -112
- package/src/theme/components/SocialLinks/index.js +9 -7
- package/src/theme/components/Tooltip/index.js +31 -20
- package/src/theme/components/Tooltip/styles.module.css +101 -38
- package/src/theme/config/iconMappings.js +2 -0
- package/src/theme/css/custom.css +72 -0
- package/src/theme/hooks/useScrollReveal.js +30 -0
- package/src/theme/pages/index.js +7 -27
- package/src/theme/pages/notes.js +2 -2
- package/src/theme/pages/tasks.js +12 -11
- package/src/utils/cliUtils.mjs +23 -51
- package/src/utils/configUtils.mjs +95 -84
- package/src/utils/systemUtils.mjs +171 -0
- package/src/template/config.js +0 -68
- package/src/theme/components/ScrollToTop/index.js +0 -95
- package/src/theme/components/ScrollToTop/styles.module.css +0 -97
- package/src/theme/config/metaTags.js +0 -21
- /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
|
|
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
|
-
|
|
19
|
-
if (
|
|
20
|
-
const
|
|
21
|
-
|
|
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(
|
|
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
|
-
|
|
17
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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.
|
|
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,
|
|
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
|
|
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:
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
91
|
-
dispatch({ type: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
setMode: () => {},
|
|
133
184
|
setActiveIndex: () => {},
|
|
134
185
|
setDockWidth: () => {},
|
|
186
|
+
setPeekHeight: () => {},
|
|
135
187
|
setFloatingState: () => {},
|
|
188
|
+
toggleMode: () => {},
|
|
136
189
|
};
|
|
137
190
|
|
|
138
191
|
export function usePreview() {
|