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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
2
|
import Tooltip from "../../Tooltip";
|
|
3
3
|
import styles from "../styles.module.css";
|
|
4
4
|
|
|
@@ -9,19 +9,20 @@ import IconLink from "@porto/assets/img/svg/icon-link.svg";
|
|
|
9
9
|
import IconClose from "@porto/assets/img/svg/icon-close.svg";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Preview window header with title, zoom controls,
|
|
12
|
+
* Preview window header with title, zoom controls, mode toggle, and close button.
|
|
13
13
|
*/
|
|
14
14
|
export default function PreviewHeader({
|
|
15
15
|
displayTitle,
|
|
16
16
|
fileType,
|
|
17
17
|
fileUrl,
|
|
18
|
-
|
|
18
|
+
mode,
|
|
19
19
|
zoomLevel,
|
|
20
20
|
onZoomChange,
|
|
21
|
-
|
|
21
|
+
onToggleMode,
|
|
22
22
|
onClose,
|
|
23
23
|
onDownload,
|
|
24
24
|
isDownloading,
|
|
25
|
+
modeSwitch = true,
|
|
25
26
|
showDockLabel = true,
|
|
26
27
|
}) {
|
|
27
28
|
const [showZoomMenu, setShowZoomMenu] = useState(false);
|
|
@@ -31,21 +32,32 @@ export default function PreviewHeader({
|
|
|
31
32
|
const isMobileSize =
|
|
32
33
|
typeof window !== "undefined" && window.innerWidth <= 768;
|
|
33
34
|
|
|
35
|
+
// The Multitasking Toggle logic (Labels and Tooltips)
|
|
36
|
+
// The Multitasking Toggle logic (Labels and Tooltips)
|
|
37
|
+
const toggleLabel =
|
|
38
|
+
mode === "popup" ? "Dock" : mode === "dock" ? "PiP" : "Dock";
|
|
39
|
+
const toggleTooltip =
|
|
40
|
+
mode === "popup"
|
|
41
|
+
? "Dock to side"
|
|
42
|
+
: mode === "dock"
|
|
43
|
+
? "Open as PiP"
|
|
44
|
+
: "Dock to side";
|
|
45
|
+
|
|
34
46
|
return (
|
|
35
47
|
<>
|
|
36
48
|
{/* Dock-mode "PREVIEW" label pinned behind navbar */}
|
|
37
|
-
{
|
|
49
|
+
{mode === "dock" && !isMobileSize && (
|
|
38
50
|
<div className={styles.revealHeader}>
|
|
39
|
-
<h1 className={styles.
|
|
51
|
+
<h1 className={styles.popupTitle}>
|
|
40
52
|
<span className={styles.primaryText}>Preview </span>
|
|
41
53
|
</h1>
|
|
42
54
|
</div>
|
|
43
55
|
)}
|
|
44
56
|
|
|
45
|
-
<div className={styles.
|
|
57
|
+
<div className={styles.popupHeader}>
|
|
46
58
|
{/* Left: file title */}
|
|
47
59
|
<div className={styles.headerLeft}>
|
|
48
|
-
<h4 className={styles.
|
|
60
|
+
<h4 className={styles.popupTitle}>
|
|
49
61
|
<span className={styles.baseTitleText}>{displayTitle}</span>
|
|
50
62
|
</h4>
|
|
51
63
|
</div>
|
|
@@ -53,7 +65,7 @@ export default function PreviewHeader({
|
|
|
53
65
|
{/* Right: controls */}
|
|
54
66
|
<div className={styles.headerControls}>
|
|
55
67
|
{/* Zoom dropdown (desktop only) */}
|
|
56
|
-
{!isMobileSize && (
|
|
68
|
+
{!isMobileSize && fileType !== "web" && (
|
|
57
69
|
<div
|
|
58
70
|
className={styles.zoomDropdown}
|
|
59
71
|
ref={zoomMenuRef}
|
|
@@ -135,30 +147,26 @@ export default function PreviewHeader({
|
|
|
135
147
|
</Tooltip>
|
|
136
148
|
)}
|
|
137
149
|
|
|
138
|
-
{/* Dock / Popup toggle */}
|
|
139
|
-
|
|
140
|
-
msg={
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
</span>
|
|
159
|
-
)}
|
|
160
|
-
</button>
|
|
161
|
-
</Tooltip>
|
|
150
|
+
{/* Dock / PiP / Popup toggle */}
|
|
151
|
+
{modeSwitch && (
|
|
152
|
+
<Tooltip msg={toggleTooltip} position="bottom" underline={false}>
|
|
153
|
+
<button
|
|
154
|
+
onClick={onToggleMode}
|
|
155
|
+
className={`${styles.headerAction} ${styles.dockToggle}`}
|
|
156
|
+
>
|
|
157
|
+
{mode === "popup" || mode === "pip" ? (
|
|
158
|
+
<IconDock className={styles.headerIcon} />
|
|
159
|
+
) : (
|
|
160
|
+
<IconPopup
|
|
161
|
+
className={`${styles.headerIcon} ${styles.iconPopupTweak}`}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
{showDockLabel && (
|
|
165
|
+
<span className={styles.btnText}>{toggleLabel}</span>
|
|
166
|
+
)}
|
|
167
|
+
</button>
|
|
168
|
+
</Tooltip>
|
|
169
|
+
)}
|
|
162
170
|
|
|
163
171
|
{/* Close */}
|
|
164
172
|
<Tooltip msg="Close" position="bottom" underline={false}>
|
|
@@ -2,18 +2,24 @@ import React, { useEffect, useMemo } from "react";
|
|
|
2
2
|
import { useLocation } from "@docusaurus/router";
|
|
3
3
|
import { usePreview } from "../../state";
|
|
4
4
|
import Tooltip from "../../../Tooltip";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
generatePvSlug,
|
|
7
|
+
generatePvHash,
|
|
8
|
+
parsePvHash,
|
|
9
|
+
classify,
|
|
10
|
+
} from "../../utils";
|
|
6
11
|
import styles from "../../styles.module.css";
|
|
7
12
|
|
|
8
|
-
// Helper to normalize boolean props (handles "true", "false", and shorthand)
|
|
9
|
-
export function isTrue(val) {
|
|
10
|
-
if (typeof val === "boolean") return val;
|
|
11
|
-
if (typeof val === "string") return val.toLowerCase() === "true";
|
|
12
|
-
return !!val;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
13
|
// Normalize props into a sources array
|
|
16
|
-
export function normalizeSources({
|
|
14
|
+
export function normalizeSources({
|
|
15
|
+
href,
|
|
16
|
+
path,
|
|
17
|
+
sources,
|
|
18
|
+
children,
|
|
19
|
+
desc,
|
|
20
|
+
title,
|
|
21
|
+
id,
|
|
22
|
+
}) {
|
|
17
23
|
const rawSources =
|
|
18
24
|
sources && sources.length > 0
|
|
19
25
|
? sources
|
|
@@ -36,18 +42,12 @@ export function normalizeSources({ href, path, sources, children, desc }) {
|
|
|
36
42
|
// Smart fallback for filename
|
|
37
43
|
let urlLabel = "";
|
|
38
44
|
let domain = "";
|
|
39
|
-
let type = "
|
|
45
|
+
let type = "text";
|
|
40
46
|
|
|
41
47
|
if (sPath) {
|
|
48
|
+
type = classify(sPath);
|
|
42
49
|
const cleanPath = sPath.split(/[?#]/)[0].toLowerCase();
|
|
43
|
-
|
|
44
|
-
else if (cleanPath.match(/\.(png|jpe?g|gif|svg|webp)$/)) type = "Image";
|
|
45
|
-
else if (
|
|
46
|
-
sPath.includes("youtube.com") ||
|
|
47
|
-
sPath.includes("youtu.be") ||
|
|
48
|
-
sPath.includes("vimeo.com")
|
|
49
|
-
)
|
|
50
|
-
type = "Video";
|
|
50
|
+
urlLabel = cleanPath.split("/").filter(Boolean).pop();
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
53
|
if (sPath.startsWith("http") || sPath.startsWith("//")) {
|
|
@@ -57,16 +57,12 @@ export function normalizeSources({ href, path, sources, children, desc }) {
|
|
|
57
57
|
domain = url.hostname.replace("www.", "");
|
|
58
58
|
}
|
|
59
59
|
} catch (e) {}
|
|
60
|
-
urlLabel = cleanPath.split("/").filter(Boolean).pop();
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
const source = domain || urlLabel || "Local";
|
|
64
63
|
const displayLabel = label || source;
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
if (!tooltip) {
|
|
68
|
-
tooltip = `${type}: ${source}`;
|
|
69
|
-
}
|
|
65
|
+
const tooltip = sDesc || null;
|
|
70
66
|
|
|
71
67
|
return {
|
|
72
68
|
path: sPath,
|
|
@@ -75,13 +71,14 @@ export function normalizeSources({ href, path, sources, children, desc }) {
|
|
|
75
71
|
type,
|
|
76
72
|
source,
|
|
77
73
|
tooltip,
|
|
78
|
-
id: src.id,
|
|
74
|
+
id: src.id || id,
|
|
75
|
+
title: src.title || title,
|
|
79
76
|
};
|
|
80
77
|
});
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
/**
|
|
84
|
-
* --- Inline trigger: <Pv href="..."
|
|
81
|
+
* --- Inline trigger: <Pv href="..." mode="...">link text</Pv> ---
|
|
85
82
|
*/
|
|
86
83
|
export default function Pv(props) {
|
|
87
84
|
const {
|
|
@@ -89,52 +86,94 @@ export default function Pv(props) {
|
|
|
89
86
|
id: manualId,
|
|
90
87
|
activeIdx = 0,
|
|
91
88
|
sources: overrideSources,
|
|
89
|
+
title,
|
|
90
|
+
mode = "popup", // Default mode
|
|
91
|
+
modeSwitch = true,
|
|
92
|
+
underline = true,
|
|
92
93
|
} = props;
|
|
93
|
-
|
|
94
|
+
|
|
95
|
+
// Strict validation: Must have exactly one of href/path OR sources
|
|
96
|
+
const hasSingleSource = !!(props.href || props.path);
|
|
97
|
+
const hasMultiSource = !!(overrideSources && overrideSources.length > 0);
|
|
98
|
+
|
|
99
|
+
if (!hasSingleSource && !hasMultiSource) {
|
|
100
|
+
console.error(
|
|
101
|
+
"<Pv> component requires either 'href', 'path', or 'sources' prop.",
|
|
102
|
+
);
|
|
103
|
+
return <span style={{ color: "red" }}>[Preview Error: Missing href]</span>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (hasSingleSource && hasMultiSource) {
|
|
107
|
+
console.error(
|
|
108
|
+
"<Pv> component cannot accept both 'href' and 'sources'. Choose one.",
|
|
109
|
+
);
|
|
110
|
+
return <span style={{ color: "red" }}>[Preview Error: Conflict]</span>;
|
|
111
|
+
}
|
|
112
|
+
|
|
94
113
|
const {
|
|
95
114
|
isOpen,
|
|
96
|
-
|
|
115
|
+
mode: currentMode,
|
|
97
116
|
sources: activeSources,
|
|
98
117
|
activeIndex,
|
|
99
118
|
openPreview,
|
|
100
119
|
closePreview,
|
|
101
|
-
|
|
120
|
+
setMode,
|
|
102
121
|
} = usePreview();
|
|
103
122
|
const location = useLocation();
|
|
104
123
|
|
|
105
124
|
const srcList = useMemo(
|
|
106
125
|
() => overrideSources || normalizeSources(props),
|
|
107
|
-
[props, overrideSources],
|
|
126
|
+
[props, overrideSources, title],
|
|
108
127
|
);
|
|
109
128
|
|
|
110
|
-
// Unified Slug
|
|
111
|
-
const
|
|
112
|
-
if (manualId) return manualId;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
// Unified Slug Generation (id > title > filename > children > preview)
|
|
130
|
+
const baseSlug = useMemo(() => {
|
|
131
|
+
if (manualId) return generatePvSlug(manualId);
|
|
132
|
+
if (title) return generatePvSlug(title);
|
|
133
|
+
|
|
134
|
+
const pathOrHref = props.href || props.path || srcList[activeIdx]?.path;
|
|
135
|
+
if (pathOrHref) {
|
|
136
|
+
const filename = pathOrHref
|
|
137
|
+
.split(/[?#]/)[0]
|
|
138
|
+
.split("/")
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.pop();
|
|
141
|
+
if (filename) return generatePvSlug(filename);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const childrenText = typeof children === "string" ? children.trim() : null;
|
|
145
|
+
if (childrenText) return generatePvSlug(childrenText);
|
|
146
|
+
|
|
147
|
+
return "preview";
|
|
148
|
+
}, [manualId, title, props.href, props.path, srcList, activeIdx, children]);
|
|
119
149
|
|
|
120
150
|
// Deep Link Detection
|
|
121
151
|
useEffect(() => {
|
|
122
152
|
const timer = setTimeout(() => {
|
|
123
153
|
const parsed = parsePvHash(window.location.hash);
|
|
124
|
-
if (parsed && parsed.slug ===
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
if (parsed && parsed.slug === baseSlug) {
|
|
155
|
+
const hashMode = parsed.mode || mode;
|
|
156
|
+
setMode(hashMode);
|
|
157
|
+
openPreview(
|
|
158
|
+
srcList,
|
|
159
|
+
activeIdx,
|
|
160
|
+
generatePvHash(baseSlug, hashMode),
|
|
161
|
+
hashMode,
|
|
162
|
+
baseSlug,
|
|
163
|
+
modeSwitch,
|
|
164
|
+
);
|
|
127
165
|
}
|
|
128
166
|
}, 150);
|
|
129
167
|
return () => clearTimeout(timer);
|
|
130
168
|
}, [
|
|
131
169
|
location.hash,
|
|
132
|
-
|
|
170
|
+
baseSlug,
|
|
133
171
|
srcList,
|
|
134
172
|
openPreview,
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
setMode,
|
|
174
|
+
mode,
|
|
137
175
|
activeIdx,
|
|
176
|
+
modeSwitch,
|
|
138
177
|
]);
|
|
139
178
|
|
|
140
179
|
if (srcList.length === 0) return <span>{children}</span>;
|
|
@@ -150,40 +189,58 @@ export default function Pv(props) {
|
|
|
150
189
|
if (isCurrentlyActive) {
|
|
151
190
|
closePreview();
|
|
152
191
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
192
|
+
setMode(mode);
|
|
193
|
+
openPreview(
|
|
194
|
+
srcList,
|
|
195
|
+
activeIdx,
|
|
196
|
+
generatePvHash(baseSlug, mode),
|
|
197
|
+
mode,
|
|
198
|
+
baseSlug,
|
|
199
|
+
modeSwitch,
|
|
200
|
+
);
|
|
156
201
|
}
|
|
157
202
|
};
|
|
158
203
|
|
|
159
|
-
const targetHash = generatePvHash(
|
|
204
|
+
const targetHash = generatePvHash(baseSlug, mode);
|
|
205
|
+
|
|
206
|
+
const trigger = (
|
|
207
|
+
<a
|
|
208
|
+
href={`#${targetHash}`}
|
|
209
|
+
className={`${styles.previewTrigger} ${isCurrentlyActive ? styles.activeTrigger : ""} ${!underline ? styles.noUnderline : ""}`}
|
|
210
|
+
onClick={(e) => {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
handleClick();
|
|
213
|
+
}}
|
|
214
|
+
role="button"
|
|
215
|
+
tabIndex={0}
|
|
216
|
+
onKeyDown={(e) => {
|
|
217
|
+
if (e.key === "Enter") {
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
handleClick();
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
{children || srcList[activeIdx]?.label}
|
|
224
|
+
</a>
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const hasTooltip = !!srcList[activeIdx]?.tooltip;
|
|
228
|
+
|
|
229
|
+
if (!hasTooltip) {
|
|
230
|
+
return <span className={styles.previewContainer}>{trigger}</span>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tooltipMsg = srcList[activeIdx]?.tooltip;
|
|
160
234
|
|
|
161
235
|
return (
|
|
162
236
|
<span className={styles.previewContainer}>
|
|
163
|
-
|
|
164
|
-
msg={
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
className={`${styles.previewTrigger} ${isCurrentlyActive ? styles.activeTrigger : ""}`}
|
|
171
|
-
onClick={(e) => {
|
|
172
|
-
e.preventDefault();
|
|
173
|
-
handleClick();
|
|
174
|
-
}}
|
|
175
|
-
role="button"
|
|
176
|
-
tabIndex={0}
|
|
177
|
-
onKeyDown={(e) => {
|
|
178
|
-
if (e.key === "Enter") {
|
|
179
|
-
e.preventDefault();
|
|
180
|
-
handleClick();
|
|
181
|
-
}
|
|
182
|
-
}}
|
|
183
|
-
>
|
|
184
|
-
{children || srcList[activeIdx]?.label}
|
|
185
|
-
</a>
|
|
186
|
-
</Tooltip>
|
|
237
|
+
{tooltipMsg ? (
|
|
238
|
+
<Tooltip msg={tooltipMsg} position="top" underline={false}>
|
|
239
|
+
{trigger}
|
|
240
|
+
</Tooltip>
|
|
241
|
+
) : (
|
|
242
|
+
trigger
|
|
243
|
+
)}
|
|
187
244
|
</span>
|
|
188
245
|
);
|
|
189
246
|
}
|