@portosaur/theme 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portosaur/theme",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Theme components for Portosaur - React components, static assets etc.",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "soymadip",
@@ -31,7 +31,9 @@
31
31
  "@docusaurus/plugin-pwa": "^3.10.1",
32
32
  "@docusaurus/preset-classic": "^3.10.1",
33
33
  "@easyops-cn/docusaurus-search-local": "^0.55.1",
34
- "@portosaur/core": "^0.2.0",
34
+ "@fontsource-variable/lexend": "^5.2.11",
35
+ "@fontsource/alata": "^5.2.8",
36
+ "@portosaur/core": "^0.3.0",
35
37
  "chalk": "^5.6.2",
36
38
  "clsx": "^2.1.1",
37
39
  "favicons": "^7.2.0",
@@ -1,5 +1,5 @@
1
1
  import DocCategoryGeneratedIndexPage from "@theme-original/DocCategoryGeneratedIndexPage";
2
- import NoteCards from "@portosaur/theme/theme/components/NoteIndex/index.jsx";
2
+ import NoteCards from "@portosaur/theme/theme/components/NoteCards/index.jsx";
3
3
 
4
4
  export default function DocCategoryGeneratedIndexPageWrapper(props) {
5
5
  if (props.categoryGeneratedIndex?.title === "Notes") {
@@ -4,7 +4,8 @@ import Tabs from "@theme/Tabs";
4
4
  import TabItem from "@theme/TabItem";
5
5
  import { Pv, SrcPv } from "./components/Preview/index.js";
6
6
  import Tooltip from "./components/Tooltip/index.js";
7
- import NoteCards, { TopicList } from "./components/NoteIndex/index.js";
7
+ import NoteCards from "./components/NoteCards/index.jsx";
8
+ import TopicList from "./components/TopicList/index.jsx";
8
9
  const components = {
9
10
  ...MDXComponents,
10
11
  Pv,
@@ -0,0 +1,10 @@
1
+ export default function Indent({ level = 1 }) {
2
+ return (
3
+ <span
4
+ style={{
5
+ display: "inline-block",
6
+ width: `${level * 2}em`,
7
+ }}
8
+ />
9
+ );
10
+ }
@@ -4,7 +4,6 @@ import Link from "@docusaurus/Link";
4
4
  import { FaBook, FaChevronRight } from "react-icons/fa";
5
5
  import Tooltip from "../Tooltip/index.jsx";
6
6
  import { iconMap } from "../../config/iconMappings.jsx";
7
- import DocCardList from "@theme/DocCardList";
8
7
  import styles from "./styles.module.css";
9
8
 
10
9
  function useNotes() {
@@ -13,7 +12,9 @@ function useNotes() {
13
12
  return context
14
13
  .keys()
15
14
  .filter((path) => {
16
- if (path === "./index.md" || path === "./index.mdx") return false;
15
+ if (path === "./index.md" || path === "./index.mdx") {
16
+ return false;
17
+ }
17
18
  const pathParts = path.split("/");
18
19
  const isTopLevelFile =
19
20
  pathParts.length === 2 && !path.match(/index\.mdx?$/);
@@ -87,14 +88,19 @@ export default function NoteCards() {
87
88
  "docusaurus-plugin-content-docs",
88
89
  );
89
90
 
90
- if (!notes.length) return null;
91
+ if (!notes.length) {
92
+ return null;
93
+ }
91
94
 
92
95
  return (
93
96
  <div className={styles.notesGrid} role="list">
94
97
  {notes.map((note, index) => (
95
98
  <NoteCard
96
99
  key={note.slug}
97
- {...note}
100
+ title={note.title}
101
+ language={note.language}
102
+ slug={note.slug}
103
+ desc={note.desc}
98
104
  index={index}
99
105
  docsBasePath={docsBasePath}
100
106
  />
@@ -102,15 +108,3 @@ export default function NoteCards() {
102
108
  </div>
103
109
  );
104
110
  }
105
-
106
- export function TopicList({
107
- desc = "Click on the links below to explore the topics.",
108
- style = { marginTop: "-2.5rem", marginBottom: "2.5rem", textAlign: "center" },
109
- }) {
110
- return (
111
- <div style={style}>
112
- <p dangerouslySetInnerHTML={{ __html: desc }} />
113
- <DocCardList />
114
- </div>
115
- );
116
- }
@@ -0,0 +1,93 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { IoIosArrowUp } from "react-icons/io";
3
+ import styles from "./styles.module.css";
4
+
5
+ export default function ScrollToTop({ hideDelay = 1500 }) {
6
+ const [isVisible, setIsVisible] = useState(false);
7
+ const [isHovering, setIsHovering] = useState(false);
8
+ const timeoutRef = useRef(null);
9
+ const lastScrollTopRef = useRef(0);
10
+
11
+ const startHideTimer = () => {
12
+ // Clear any existing timeout
13
+ if (timeoutRef.current) {
14
+ clearTimeout(timeoutRef.current);
15
+ }
16
+
17
+ // Only start timer if not hovering
18
+ if (!isHovering) {
19
+ timeoutRef.current = setTimeout(() => {
20
+ setIsVisible(false);
21
+ }, hideDelay);
22
+ }
23
+ };
24
+
25
+ useEffect(() => {
26
+ const handleScroll = () => {
27
+ const scrollTop =
28
+ window.pageYOffset || document.documentElement.scrollTop;
29
+ const isScrollingUp = scrollTop < lastScrollTopRef.current;
30
+
31
+ // Save the current scroll position
32
+ lastScrollTopRef.current = scrollTop;
33
+
34
+ // Show button when scrolling up past threshold
35
+ if (isScrollingUp && scrollTop > 300) {
36
+ setIsVisible(true);
37
+ startHideTimer();
38
+ } else {
39
+ setIsVisible(false);
40
+ if (timeoutRef.current) {
41
+ clearTimeout(timeoutRef.current);
42
+ }
43
+ }
44
+ };
45
+
46
+ // Set up event listener
47
+ window.addEventListener("scroll", handleScroll, { passive: true });
48
+
49
+ // Clean up
50
+ return () => {
51
+ window.removeEventListener("scroll", handleScroll);
52
+ if (timeoutRef.current) {
53
+ clearTimeout(timeoutRef.current);
54
+ }
55
+ };
56
+ }, [hideDelay, isHovering]);
57
+
58
+ const handleMouseEnter = () => {
59
+ setIsHovering(true);
60
+ if (timeoutRef.current) {
61
+ clearTimeout(timeoutRef.current);
62
+ }
63
+ };
64
+
65
+ const handleMouseLeave = () => {
66
+ setIsHovering(false);
67
+ startHideTimer();
68
+ };
69
+
70
+ const scrollToTop = () => {
71
+ const prefersReducedMotion = window.matchMedia(
72
+ "(prefers-reduced-motion: reduce)",
73
+ ).matches;
74
+
75
+ window.scrollTo({
76
+ top: 0,
77
+ behavior: prefersReducedMotion ? "auto" : "smooth",
78
+ });
79
+ };
80
+
81
+ return (
82
+ <button
83
+ className={`${styles.scrollToTop} ${isVisible ? styles.visible : ""}`}
84
+ onClick={scrollToTop}
85
+ onMouseEnter={handleMouseEnter}
86
+ onMouseLeave={handleMouseLeave}
87
+ aria-label="Scroll to top"
88
+ title="Scroll to top"
89
+ >
90
+ <IoIosArrowUp />
91
+ </button>
92
+ );
93
+ }
@@ -0,0 +1,97 @@
1
+ .scrollToTop {
2
+ position: fixed;
3
+ bottom: 30px;
4
+ right: 30px;
5
+ background-color: var(--ifm-color-primary);
6
+ color: var(--ifm-background-color);
7
+ width: 50px;
8
+ height: 50px;
9
+ border-radius: 50%;
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ cursor: pointer;
14
+ box-shadow: var(--ifm-global-shadow-md);
15
+ border: none;
16
+ z-index: 100;
17
+ opacity: 0;
18
+ transform: translateY(20px) scale(0.9);
19
+ pointer-events: none;
20
+ transition:
21
+ opacity 0.3s ease,
22
+ transform 0.3s ease,
23
+ background-color 0.3s ease;
24
+ }
25
+
26
+ .scrollToTop.visible {
27
+ opacity: 1;
28
+ transform: translateY(0) scale(1);
29
+ pointer-events: auto;
30
+ }
31
+
32
+ .scrollToTop:hover {
33
+ background-color: var(--ifm-color-primary-dark);
34
+ transform: translateY(-3px) scale(1.05);
35
+ }
36
+
37
+ .scrollToTop:active {
38
+ transform: translateY(-1px) scale(1.02);
39
+ }
40
+
41
+ .scrollToTop svg {
42
+ width: 28px;
43
+ height: 28px;
44
+ }
45
+
46
+ .scrollToTop:hover svg {
47
+ transform: scale(1.1);
48
+ }
49
+
50
+ /* Responsive adjustments */
51
+ @media (max-width: 768px) {
52
+ .scrollToTop {
53
+ width: 45px;
54
+ height: 45px;
55
+ bottom: 25px;
56
+ right: 25px;
57
+ }
58
+
59
+ .scrollToTop svg {
60
+ width: 24px;
61
+ height: 24px;
62
+ }
63
+ }
64
+
65
+ @media (max-width: 480px) {
66
+ .scrollToTop {
67
+ width: 40px;
68
+ height: 40px;
69
+ bottom: 20px;
70
+ right: 50%;
71
+ transform: translateX(50%) translateY(20px) scale(0.9);
72
+ }
73
+
74
+ .scrollToTop.visible {
75
+ transform: translateX(50%) scale(1);
76
+ }
77
+
78
+ .scrollToTop:hover {
79
+ transform: translateX(50%) translateY(-3px) scale(1.05);
80
+ }
81
+
82
+ .scrollToTop:active {
83
+ transform: translateX(50%) translateY(-1px) scale(1.02);
84
+ }
85
+
86
+ .scrollToTop svg {
87
+ width: 22px;
88
+ height: 22px;
89
+ }
90
+ }
91
+
92
+ /* Accessibility preferences */
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .scrollToTop {
95
+ transition: none;
96
+ }
97
+ }
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, useCallback } from "react";
1
+ import { useState, useRef, useCallback, useEffect } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import styles from "./styles.module.css";
4
4
 
@@ -20,8 +20,10 @@ export default function Tooltip({
20
20
  }
21
21
 
22
22
  const [isVisible, setIsVisible] = useState(false);
23
- const [coords, setCoords] = useState({ top: 0, left: 0 });
23
+ const [coords, setCoords] = useState({ top: 0, left: 0, originalLeft: 0 });
24
+ const [arrowOffset, setArrowOffset] = useState(0);
24
25
  const containerRef = useRef(null);
26
+ const tooltipRef = useRef(null);
25
27
 
26
28
  const tooltipStyle = {
27
29
  ...(bg && { "--tooltip-color": bg }),
@@ -55,18 +57,68 @@ export default function Tooltip({
55
57
  left = rect.left + rect.width / 2;
56
58
  break;
57
59
  }
58
- setCoords({ top, left });
60
+ setCoords({ top, left, originalLeft: left });
61
+ setArrowOffset(0);
59
62
  setIsVisible(true);
60
63
  }, [position, gap]);
61
64
 
62
- const hide = useCallback(() => setIsVisible(false), []);
65
+ const hide = useCallback(() => {
66
+ setIsVisible(false);
67
+ if (
68
+ typeof document !== "undefined" &&
69
+ containerRef.current &&
70
+ containerRef.current.contains(document.activeElement)
71
+ ) {
72
+ document.activeElement.blur();
73
+ }
74
+ }, []);
75
+
76
+ useEffect(() => {
77
+ if (isVisible) {
78
+ window.addEventListener("scroll", hide, { passive: true });
79
+ return () => window.removeEventListener("scroll", hide);
80
+ }
81
+ }, [isVisible, hide]);
82
+
83
+ useEffect(() => {
84
+ if (
85
+ isVisible &&
86
+ tooltipRef.current &&
87
+ (position === "top" || position === "bottom")
88
+ ) {
89
+ const rect = tooltipRef.current.getBoundingClientRect();
90
+ const padding = 12;
91
+ let newLeft = coords.originalLeft;
92
+ const halfWidth = rect.width / 2;
93
+
94
+ if (coords.originalLeft - halfWidth < padding) {
95
+ newLeft = halfWidth + padding;
96
+ } else if (
97
+ coords.originalLeft + halfWidth >
98
+ window.innerWidth - padding
99
+ ) {
100
+ newLeft = window.innerWidth - halfWidth - padding;
101
+ }
102
+
103
+ if (newLeft !== coords.left) {
104
+ setCoords((prev) => ({ ...prev, left: newLeft }));
105
+ setArrowOffset(newLeft - coords.originalLeft);
106
+ }
107
+ }
108
+ }, [isVisible, coords.originalLeft, coords.left, position]);
63
109
 
64
110
  const tooltip =
65
111
  isVisible && typeof document !== "undefined"
66
112
  ? createPortal(
67
113
  <span
114
+ ref={tooltipRef}
68
115
  className={`${styles.tooltip} ${styles[position]}`}
69
- style={{ ...tooltipStyle, top: coords.top, left: coords.left }}
116
+ style={{
117
+ ...tooltipStyle,
118
+ top: coords.top,
119
+ left: coords.left,
120
+ "--arrow-offset": `${arrowOffset}px`,
121
+ }}
70
122
  role="tooltip"
71
123
  >
72
124
  {msg}
@@ -79,11 +131,12 @@ export default function Tooltip({
79
131
  return (
80
132
  <div
81
133
  ref={containerRef}
82
- className={`${styles.tooltipContainer} ${underline ? styles.hasUnderline : ""} ${className}`}
134
+ className={`${styles.tooltipContainer} ${underline ? styles.hasUnderline : ""} ${className} ${isVisible ? styles.isActive : ""}`}
83
135
  onMouseEnter={show}
84
136
  onMouseLeave={hide}
85
137
  onFocus={show}
86
138
  onBlur={hide}
139
+ onClick={() => (isVisible ? hide() : show())}
87
140
  style={{ display: "contents" }}
88
141
  >
89
142
  {children}
@@ -21,7 +21,17 @@
21
21
  transition: background-image 0.2s ease;
22
22
  }
23
23
 
24
- .hasUnderline:hover {
24
+ @media (hover: hover) {
25
+ .hasUnderline:hover {
26
+ background-image: radial-gradient(
27
+ circle,
28
+ color-mix(in srgb, var(--ifm-color-primary-darker), transparent 10%) 1.2px,
29
+ transparent 1.5px
30
+ );
31
+ }
32
+ }
33
+
34
+ .hasUnderline.isActive {
25
35
  background-image: radial-gradient(
26
36
  circle,
27
37
  color-mix(in srgb, var(--ifm-color-primary-darker), transparent 10%) 1.2px,
@@ -83,7 +93,7 @@
83
93
 
84
94
  .top .arrow {
85
95
  top: 100%;
86
- left: 50%;
96
+ left: calc(50% - var(--arrow-offset, 0px));
87
97
  transform: translateX(-50%) rotate(45deg);
88
98
  margin-top: -5.5px;
89
99
  border-right: 1px solid var(--ifm-color-emphasis-300);
@@ -92,7 +102,7 @@
92
102
 
93
103
  .bottom .arrow {
94
104
  bottom: 100%;
95
- left: 50%;
105
+ left: calc(50% - var(--arrow-offset, 0px));
96
106
  transform: translateX(-50%) rotate(45deg);
97
107
  margin-bottom: -5.5px;
98
108
  border-left: 1px solid var(--ifm-color-emphasis-300);
@@ -0,0 +1,31 @@
1
+ import DocCardList from "@theme/DocCardList";
2
+ import { useCurrentSidebarCategory } from "@docusaurus/plugin-content-docs/client";
3
+
4
+ // List Topics inside Individual Notes
5
+ export default function TopicList({
6
+ children,
7
+ description = "Click on the links below to explore the topics.",
8
+ style = {
9
+ marginTop: "-2.5rem",
10
+ marginBottom: "2.5rem",
11
+ textAlign: "center",
12
+ },
13
+ }) {
14
+ let items;
15
+ try {
16
+ const category = useCurrentSidebarCategory();
17
+ items = category.items;
18
+ } catch (e) {
19
+ // Fallback if not on a category page
20
+ }
21
+
22
+ return (
23
+ <>
24
+ <br />
25
+ {(children || description) && (
26
+ <p style={style}>{children || description}</p>
27
+ )}
28
+ <DocCardList items={items} />
29
+ </>
30
+ );
31
+ }
@@ -0,0 +1,61 @@
1
+ /* === Global Entrance Animations === */
2
+
3
+ @keyframes fadeIn {
4
+ from {
5
+ opacity: 0;
6
+ }
7
+ to {
8
+ opacity: 1;
9
+ }
10
+ }
11
+
12
+ @keyframes slideUp {
13
+ from {
14
+ opacity: 0;
15
+ transform: translateY(30px);
16
+ }
17
+ to {
18
+ opacity: 1;
19
+ transform: translateY(0);
20
+ }
21
+ }
22
+
23
+ @keyframes slideInLeft {
24
+ from {
25
+ opacity: 0;
26
+ transform: translateX(-40px);
27
+ }
28
+ to {
29
+ opacity: 1;
30
+ transform: translateX(0);
31
+ }
32
+ }
33
+
34
+ @keyframes slideInRight {
35
+ from {
36
+ opacity: 0;
37
+ transform: translateX(40px);
38
+ }
39
+ to {
40
+ opacity: 1;
41
+ transform: translateX(0);
42
+ }
43
+ }
44
+
45
+ @keyframes scaleIn {
46
+ from {
47
+ opacity: 0;
48
+ transform: scale(0.95);
49
+ }
50
+ to {
51
+ opacity: 1;
52
+ transform: scale(1);
53
+ }
54
+ }
55
+
56
+ @keyframes skillAppear {
57
+ to {
58
+ opacity: 1;
59
+ transform: translateY(0);
60
+ }
61
+ }