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
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from "react";
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, dock/popup toggle, and close button.
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
- isDocked,
18
+ mode,
19
19
  zoomLevel,
20
20
  onZoomChange,
21
- onToggleDock,
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
- {isDocked && !isMobileSize && (
49
+ {mode === "dock" && !isMobileSize && (
38
50
  <div className={styles.revealHeader}>
39
- <h1 className={styles.modalTitle}>
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.modalHeader}>
57
+ <div className={styles.popupHeader}>
46
58
  {/* Left: file title */}
47
59
  <div className={styles.headerLeft}>
48
- <h4 className={styles.modalTitle}>
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
- <Tooltip
140
- msg={isDocked ? "Open as popup" : "Dock to side"}
141
- position="bottom"
142
- underline={false}
143
- >
144
- <button
145
- onClick={onToggleDock}
146
- className={`${styles.headerAction} ${styles.dockToggle}`}
147
- >
148
- {isDocked ? (
149
- <IconPopup
150
- className={`${styles.headerIcon} ${styles.iconPopupTweak}`}
151
- />
152
- ) : (
153
- <IconDock className={styles.headerIcon} />
154
- )}
155
- {showDockLabel && (
156
- <span className={styles.btnText}>
157
- {isDocked ? "Popup" : "Dock"}
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 { generatePvSlug, generatePvHash, parsePvHash } from "../../utils";
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({ href, path, sources, children, desc }) {
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 = "Web";
45
+ let type = "text";
40
46
 
41
47
  if (sPath) {
48
+ type = classify(sPath);
42
49
  const cleanPath = sPath.split(/[?#]/)[0].toLowerCase();
43
- if (cleanPath.endsWith(".pdf")) type = "PDF";
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
- let tooltip = sDesc;
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="..." id="...">link text</Pv> ---
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
- const initialDocked = isTrue(props.docked);
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
- isDocked,
115
+ mode: currentMode,
97
116
  sources: activeSources,
98
117
  activeIndex,
99
118
  openPreview,
100
119
  closePreview,
101
- setDocked,
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 & Hash Generation
111
- const slug = useMemo(() => {
112
- if (manualId) return manualId;
113
- const label = typeof children === "string" ? children : null;
114
- return generatePvSlug(
115
- label,
116
- props.href || props.path || srcList[activeIdx]?.path,
117
- );
118
- }, [manualId, children, props.href, props.path, srcList, activeIdx]);
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 === slug) {
125
- setDocked(parsed.isDocked || initialDocked);
126
- openPreview(srcList, activeIdx, generatePvHash(slug, parsed.isDocked));
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
- slug,
170
+ baseSlug,
133
171
  srcList,
134
172
  openPreview,
135
- setDocked,
136
- initialDocked,
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
- const targetDocked = initialDocked || isDocked;
154
- setDocked(targetDocked);
155
- openPreview(srcList, activeIdx, generatePvHash(slug, targetDocked));
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(slug, initialDocked || isDocked);
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
- <Tooltip
164
- msg={srcList[activeIdx]?.tooltip || "Preview"}
165
- position="top"
166
- underline={false}
167
- >
168
- <a
169
- href={`#${targetHash}`}
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
  }