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,24 +1,23 @@
1
- import { useRef, useState, useEffect } from "react";
2
- import Link from "@docusaurus/Link";
3
1
  import useBaseUrl from "@docusaurus/useBaseUrl";
4
2
  import { usePluginData } from "@docusaurus/useGlobalData";
3
+ import Link from "@docusaurus/Link";
4
+ import { FaBook, FaChevronRight } from "react-icons/fa";
5
+ import Tooltip from "../Tooltip/index.js";
5
6
  import { iconMap } from "../../config/iconMappings.js";
6
7
  import DocCardList from "@theme/DocCardList";
7
- import Tooltip from "../Tooltip/index.js";
8
-
9
- import { FaBook } from "react-icons/fa";
10
8
  import styles from "./styles.module.css";
11
9
 
12
- function indexNotes() {
10
+ /**
11
+ * Extracts and prepares notes from the filesystem
12
+ */
13
+ function useNotes() {
13
14
  const context = require.context(`@site/notes`, true, /index\.mdx?$|\.mdx?$/);
14
15
 
15
16
  return context
16
17
  .keys()
17
18
  .filter((path) => {
18
19
  // Skip root index file
19
- if (path === "./index.md" || path === "./index.mdx") {
20
- return false;
21
- }
20
+ if (path === "./index.md" || path === "./index.mdx") return false;
22
21
 
23
22
  const pathParts = path.split("/");
24
23
  const isTopLevelFile =
@@ -26,136 +25,111 @@ function indexNotes() {
26
25
  const isTopLevelDir =
27
26
  pathParts.length === 3 && path.match(/index\.mdx?$/);
28
27
 
29
- // Keep only top-level files & dirs
30
28
  return isTopLevelFile || isTopLevelDir;
31
29
  })
32
30
  .map((path) => {
31
+ const { frontMatter } = context(path);
33
32
  const pathParts = path.split("/");
34
33
  const isTopLevelFile = pathParts.length === 2;
35
34
 
36
- // Extract directory or filename
37
- const slug = isTopLevelFile
35
+ // Extract filename-based slug as fallback
36
+ const fileSlug = isTopLevelFile
38
37
  ? path.replace("./", "").replace(/\.mdx?$/, "")
39
38
  : pathParts[1];
40
39
 
41
- const { frontMatter } = context(path);
40
+ // Routing logic: slug > id > filename
41
+ const slug = frontMatter.slug || frontMatter.id || fileSlug;
42
+
43
+ // Display logic: title > language > filename
44
+ const rawTitle = frontMatter.title || frontMatter.language || fileSlug;
45
+ const title = rawTitle.charAt(0).toUpperCase() + rawTitle.slice(1);
42
46
 
43
- const title =
44
- frontMatter.title || slug.charAt(0).toUpperCase() + slug.slice(1);
45
47
  const language = frontMatter.language
46
48
  ? frontMatter.language
47
49
  .toLowerCase()
48
50
  .replace(/ /g, "")
49
51
  .replace(/[\s-]/g, "")
50
52
  : slug.toLowerCase() || title.toLowerCase();
51
- const position = frontMatter.sidebar_position || 999;
52
- const description = frontMatter.description || "";
53
53
 
54
54
  return {
55
55
  title,
56
56
  language,
57
- link: slug,
58
- position,
59
- description,
57
+ slug,
58
+ desc: frontMatter.desc || "",
59
+ position: frontMatter.sidebar_position || 999,
60
60
  };
61
61
  })
62
62
  .sort((a, b) => a.position - b.position);
63
63
  }
64
- function NoteCard({
65
- title,
66
- language,
67
- link,
68
- description,
69
- index,
70
- docsBasePath,
71
- buttonText,
72
- }) {
73
- const noteUrl = useBaseUrl(`${docsBasePath}/${link}`);
64
+
65
+ /**
66
+ * Individual Note Card Component
67
+ */
68
+ function NoteCard({ title, language, slug, desc, index, docsBasePath }) {
69
+ const noteUrl = useBaseUrl(`${docsBasePath}/${slug}`);
74
70
  const { icon: Icon = FaBook, color = "var(--ifm-color-primary)" } =
75
71
  iconMap[language] || iconMap[title.toLowerCase()] || {};
76
72
 
77
- const tooltipContent = description ? (
78
- <span
79
- style={{
80
- textAlign: "center",
81
- minWidth: "150px",
82
- display: "flex",
83
- flexDirection: "column",
84
- gap: "6px",
85
- }}
86
- >
87
- <strong style={{ fontSize: "0.95em" }}>{title}</strong>
88
- <span
89
- style={{
90
- fontSize: "0.85em",
91
- opacity: 0.85,
92
- fontWeight: 400,
93
- textAlign: "left",
94
- }}
95
- >
96
- {description}
97
- </span>
98
- </span>
99
- ) : (
100
- title
101
- );
73
+ const tooltipContent = desc ? desc : null;
102
74
 
103
- return (
104
- <div
105
- key={title}
106
- className={`${styles.noteCard} note-card`}
107
- style={{ "--card-index": index }}
108
- role="listitem"
109
- id={`note-${link}`}
75
+ const cardInner = (
76
+ <Link
77
+ to={noteUrl}
78
+ className={styles.noteCard}
79
+ style={{ "--card-index": index, "--note-color": color }}
80
+ aria-label={`Read note: ${title}`}
110
81
  >
111
- <div className={styles.noteIcon} style={{ color }} aria-hidden="true">
112
- <Icon />
82
+ <div className={styles.iconWrapper}>
83
+ <Icon className={styles.noteIcon} />
113
84
  </div>
114
- <Tooltip
115
- msg={tooltipContent}
116
- position="top"
117
- underline={false}
118
- color="var(--ifm-color-primary-dark)"
119
- >
120
- <span className={styles.noteTitle} role="heading" aria-level="3">
121
- {title}
122
- </span>
123
- </Tooltip>
124
- <Link
125
- className="button button--primary"
126
- to={noteUrl}
127
- aria-label={`Open ${title} note`}
128
- >
129
- {buttonText}
130
- </Link>
131
- </div>
85
+
86
+ <div className={styles.cardContent}>
87
+ <h3 className={styles.noteTitle}>{title}</h3>
88
+ </div>
89
+
90
+ <FaChevronRight className={styles.mobileChevron} />
91
+ </Link>
92
+ );
93
+
94
+ return tooltipContent ? (
95
+ <Tooltip msg={tooltipContent} position="top" underline={false} gap={-8}>
96
+ {cardInner}
97
+ </Tooltip>
98
+ ) : (
99
+ cardInner
132
100
  );
133
101
  }
134
102
 
135
- export default function NoteCards({ buttonText = "Open Note" }) {
136
- const notes = indexNotes();
103
+ /**
104
+ * Main Notes Grid Component
105
+ */
106
+ export default function NoteCards() {
107
+ const notes = useNotes();
137
108
  const { path: docsBasePath } = usePluginData(
138
109
  "docusaurus-plugin-content-docs",
139
110
  );
140
111
 
112
+ if (!notes.length) return null;
113
+
141
114
  return (
142
- <div className={styles.notesGrid} role="list" aria-label="Notes collection">
115
+ <div className={styles.notesGrid} role="list">
143
116
  {notes.map((note, index) => (
144
117
  <NoteCard
145
- key={note.title}
118
+ key={note.slug}
146
119
  {...note}
147
120
  index={index}
148
121
  docsBasePath={docsBasePath}
149
- buttonText={buttonText}
150
122
  />
151
123
  ))}
152
124
  </div>
153
125
  );
154
126
  }
155
127
 
156
- // List Topics inside Individual Notes
128
+ /**
129
+ * Topic List Helper (for inside notes)
130
+ */
157
131
  export function TopicList({
158
- description = "Click on the links below to explore the topics.",
132
+ desc = "Click on the links below to explore the topics.",
159
133
  style = {
160
134
  marginTop: "-2.5rem",
161
135
  marginBottom: "2.5rem",
@@ -163,12 +137,9 @@ export function TopicList({
163
137
  },
164
138
  }) {
165
139
  return (
166
- <>
167
- <p
168
- style={style}
169
- dangerouslySetInnerHTML={{ __html: description }} // Well we are giving it only HTML :)
170
- />
140
+ <div style={style}>
141
+ <p dangerouslySetInnerHTML={{ __html: desc }} />
171
142
  <DocCardList />
172
- </>
143
+ </div>
173
144
  );
174
145
  }
@@ -1,7 +1,7 @@
1
- @keyframes fadeIn {
1
+ @keyframes noteFadeIn {
2
2
  from {
3
3
  opacity: 0;
4
- transform: translateY(20px);
4
+ transform: translateY(15px);
5
5
  }
6
6
  to {
7
7
  opacity: 1;
@@ -12,40 +12,47 @@
12
12
  .notesGrid {
13
13
  display: flex;
14
14
  flex-wrap: wrap;
15
- gap: 1.5rem;
15
+ gap: 1rem;
16
16
  justify-content: center;
17
17
  margin: 2rem 0;
18
18
  }
19
19
 
20
20
  .noteCard {
21
21
  width: 200px;
22
- border-radius: var(--ifm-card-border-radius);
23
- text-align: center;
24
- padding: 1.2rem;
22
+ min-height: 220px;
25
23
  background-color: var(--ifm-card-background-color);
26
- opacity: 0;
27
- transition:
28
- transform 0.3s ease,
29
- scale 0.3s ease,
30
- z-index 0s;
31
- animation: fadeIn 0.5s ease-out forwards;
32
- animation-delay: calc(0.2s + (var(--card-index, 0) * 0.1s));
24
+ border-radius: var(--ifm-card-border-radius);
25
+ padding: 1.5rem 1rem;
26
+ text-decoration: none !important;
27
+ color: var(--ifm-font-color-base) !important;
33
28
  display: flex;
34
29
  flex-direction: column;
30
+ align-items: center;
31
+ justify-content: center;
35
32
  position: relative;
36
- z-index: 1;
33
+ overflow: hidden;
34
+ transition: all 0.3s ease;
35
+ animation: noteFadeIn 0.5s ease-out forwards;
36
+ animation-delay: calc(0.1s + (var(--card-index, 0) * 0.05s));
37
+ opacity: 0;
37
38
  }
38
39
 
39
40
  .noteCard:hover {
40
- transform: translateY(-5px);
41
- scale: 1.05;
42
- z-index: 10;
41
+ transform: translateY(-8px) scale(1.03);
42
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
43
+ }
44
+
45
+ .iconWrapper {
46
+ display: flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ margin-bottom: 1.5rem;
43
50
  }
44
51
 
45
52
  .noteIcon {
46
- font-size: 69px;
47
- margin-bottom: -1rem;
48
- transition: transform 0.2s ease;
53
+ font-size: 3.5rem;
54
+ color: var(--note-color);
55
+ transition: transform 0.3s ease;
49
56
  }
50
57
 
51
58
  .noteCard:hover .noteIcon {
@@ -53,75 +60,62 @@
53
60
  }
54
61
 
55
62
  .noteTitle {
56
- margin-bottom: 1.8rem;
57
- color: var(--ifm-font-color-base);
58
- font-size: 1.8rem;
59
- font-weight: 600;
63
+ margin: 0;
64
+ font-size: 1.6rem;
65
+ font-weight: 700;
66
+ line-height: 1.3;
67
+ text-align: center;
60
68
  display: -webkit-box;
61
- line-clamp: 3;
62
- -webkit-line-clamp: 3;
69
+ -webkit-line-clamp: 2;
70
+ line-clamp: 2;
63
71
  -webkit-box-orient: vertical;
64
72
  overflow: hidden;
65
73
  text-overflow: ellipsis;
66
- word-break: break-word;
74
+ overflow-wrap: anywhere;
67
75
  }
68
76
 
69
- .noteCard > a {
70
- margin-top: auto;
71
- align-self: center;
72
- }
73
-
74
- @media (max-width: 768px) {
75
- .noteCard {
76
- width: 150px;
77
- padding: 0.8rem;
78
- }
79
-
80
- .noteIcon {
81
- font-size: 45px;
82
- margin-bottom: -0.8rem;
83
- }
84
-
85
- .noteTitle {
86
- font-size: 1.3rem;
87
- margin-bottom: 1.5rem;
88
- }
77
+ /* Mobile List Style */
78
+ .mobileChevron {
79
+ display: none;
89
80
  }
90
81
 
91
82
  @media (max-width: 480px) {
92
83
  .notesGrid {
93
- gap: 0.7rem;
94
- margin: 0.8rem auto;
95
- justify-content: center;
96
- padding: 0 0.05rem;
84
+ gap: 0.65rem;
85
+ margin: 0.2rem 0;
86
+ padding: 0 0.2rem;
87
+ display: flex;
88
+ flex-direction: column;
89
+ align-items: stretch;
90
+ justify-content: flex-start;
91
+ }
92
+
93
+ /* Force the Tooltip container to be full-width */
94
+ .notesGrid > span {
95
+ width: 100%;
96
+ display: flex !important;
97
+ justify-content: flex-start;
97
98
  }
98
99
 
99
100
  .noteCard {
100
101
  width: 100%;
101
- max-width: 400px;
102
- padding: 1rem;
103
- display: flex;
102
+ min-height: auto;
104
103
  flex-direction: row;
105
104
  align-items: center;
106
- gap: 1rem;
107
- text-align: left;
108
- border-radius: var(--ifm-card-border-radius);
105
+ padding: 0.95rem 1.2rem;
106
+ justify-content: flex-start;
107
+ background-color: var(--ifm-card-background-color);
108
+ border-radius: 12px;
109
109
  box-shadow: var(--ifm-global-shadow-md);
110
- transition: transform 0.2s ease;
110
+ gap: 1.2rem;
111
111
  }
112
112
 
113
113
  .noteCard:hover {
114
114
  transform: none;
115
- scale: 1;
116
- }
117
-
118
- .noteCard:active {
119
- transform: scale(0.98);
120
115
  background-color: var(--ifm-hover-overlay);
121
116
  }
122
117
 
123
- .noteIcon {
124
- font-size: 42px;
118
+ .iconWrapper {
125
119
  margin: 0;
126
120
  flex-shrink: 0;
127
121
  display: flex;
@@ -129,43 +123,45 @@
129
123
  justify-content: center;
130
124
  }
131
125
 
132
- /* Remove any absolute positioning or shifts that break centering */
133
- .noteIcon > svg {
134
- margin: 0;
126
+ .noteIcon {
127
+ font-size: 2.8rem;
135
128
  }
136
129
 
137
- .noteTitle {
138
- font-size: 1.1rem;
139
- margin: 0;
130
+ .cardContent {
131
+ text-align: left;
140
132
  flex: 1;
141
133
  min-width: 0;
142
- line-height: 1.3;
143
- display: -webkit-box;
134
+ }
135
+
136
+ .noteTitle {
137
+ font-size: 1.2rem;
138
+ font-weight: 600;
139
+ color: var(--ifm-font-color-base);
144
140
  -webkit-line-clamp: 2;
145
141
  line-clamp: 2;
146
- -webkit-box-orient: vertical;
147
- overflow: hidden;
148
- text-overflow: ellipsis;
142
+ overflow-wrap: anywhere;
143
+ text-align: left;
149
144
  }
150
145
 
151
- .noteCard > a {
152
- margin: 0 0 0 auto;
153
- padding: 0.5rem 0.8rem;
154
- font-size: 0.85rem;
155
- border-radius: var(--ifm-global-radius);
156
- flex-shrink: 0;
157
- white-space: nowrap;
146
+ .mobileChevron {
147
+ display: block;
148
+ margin-left: auto;
149
+ font-size: 1.1rem;
150
+ opacity: 0.7;
151
+ color: var(--ifm-color-primary);
152
+ }
153
+
154
+ .noteCard:active {
155
+ transform: scale(0.98);
156
+ background-color: var(--ifm-hover-overlay);
158
157
  }
159
158
  }
160
159
 
160
+ /* Accessibility */
161
161
  @media (prefers-reduced-motion: reduce) {
162
162
  .noteCard {
163
- animation: none !important;
163
+ animation: none;
164
164
  opacity: 1;
165
- transition: none !important;
166
- }
167
-
168
- .noteIcon {
169
- transition: none !important;
165
+ transition: none;
170
166
  }
171
167
  }
@@ -6,7 +6,9 @@ import styles from "../styles.module.css";
6
6
  export function LoadingState() {
7
7
  return (
8
8
  <div className={styles.loading}>
9
- <div className={styles.spinner} />
9
+ <div className={styles.loadingIcon}>
10
+ <div className={styles.spinner} />
11
+ </div>
10
12
  <div className={styles.loadingText}>
11
13
  <p>Preparing preview...</p>
12
14
  <span>Fetching content from source</span>
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import styles from "../styles.module.css";
3
+ import { LoadingState, ErrorState, OfflineState } from "./FeedbackStates";
4
+ import ImageRenderer from "../renderers/ImageRenderer";
5
+ import PdfRenderer from "../renderers/PdfRenderer";
6
+ import WebRenderer from "../renderers/WebRenderer";
7
+ import CodeRenderer from "../renderers/CodeRenderer";
8
+
9
+ /**
10
+ * Component that routes to the appropriate renderer based on file type.
11
+ * Also handles shared loading, error, and offline states.
12
+ */
13
+ export default function PreviewContent({
14
+ currentFile,
15
+ fileType,
16
+ fileUrl,
17
+ isOnline,
18
+ fetchErrors,
19
+ textLoading,
20
+ textContent,
21
+ zoomLevel,
22
+ ext,
23
+ retryFetch,
24
+ setError,
25
+ }) {
26
+ const path = currentFile?.path;
27
+ const isExternal = path?.startsWith("http") || path?.startsWith("//");
28
+
29
+ // 1. Offline guard for external resources
30
+ if (!isOnline && isExternal) {
31
+ return <OfflineState onRetry={retryFetch} />;
32
+ }
33
+
34
+ // 2. Error state
35
+ const errorMsg = fetchErrors?.[path];
36
+ if (errorMsg) {
37
+ return (
38
+ <ErrorState
39
+ path={path}
40
+ message={errorMsg}
41
+ fileType={fileType}
42
+ fileUrl={fileUrl}
43
+ onRetry={retryFetch}
44
+ />
45
+ );
46
+ }
47
+
48
+ // 3. Loading state (for text files only — specialized renderers handle their own loading)
49
+ if (textLoading && fileType === "text") {
50
+ return <LoadingState />;
51
+ }
52
+
53
+ // 4. Feature-specific renderers
54
+ switch (fileType) {
55
+ case "image":
56
+ return (
57
+ <ImageRenderer
58
+ key={fileUrl}
59
+ fileUrl={fileUrl}
60
+ label={currentFile.label}
61
+ zoomLevel={zoomLevel}
62
+ onError={(msg) => setError(path, msg)}
63
+ />
64
+ );
65
+ case "pdf":
66
+ return (
67
+ <PdfRenderer
68
+ key={fileUrl}
69
+ fileUrl={fileUrl}
70
+ zoomLevel={zoomLevel}
71
+ onError={(msg) => setError(path, msg)}
72
+ />
73
+ );
74
+ case "web":
75
+ return (
76
+ <WebRenderer
77
+ key={fileUrl}
78
+ fileUrl={fileUrl}
79
+ label={currentFile.label}
80
+ onError={(msg) => setError(path, msg)}
81
+ />
82
+ );
83
+ default: {
84
+ // Text / code
85
+ if (!textContent) return <LoadingState />;
86
+ return (
87
+ <CodeRenderer code={textContent} language={ext} zoomLevel={zoomLevel} />
88
+ );
89
+ }
90
+ }
91
+ }