@sntlr/registry-shell 1.0.0 → 1.1.1
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 +2 -5
- package/dist/adapter/custom.d.ts +1 -1
- package/dist/adapter/default.d.ts +3 -3
- package/dist/adapter/default.js +3 -3
- package/dist/cli/init.js +0 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/shared.js +0 -5
- package/dist/cli/shared.js.map +1 -1
- package/dist/config-loader.d.ts +0 -2
- package/dist/config-loader.js +0 -1
- package/dist/define-config.d.ts +2 -8
- package/package.json +1 -1
- package/src/adapter/custom.ts +1 -1
- package/src/adapter/default.ts +3 -3
- package/src/cli/init.ts +0 -3
- package/src/cli/shared.ts +0 -4
- package/src/config-loader.ts +0 -3
- package/src/define-config.ts +2 -9
- package/src/next-app/app/globals.css +329 -329
- package/src/next-app/app/page.tsx +1 -1
- package/src/next-app/components/component-icon.tsx +140 -140
- package/src/next-app/components/heading-anchor.tsx +52 -52
- package/src/next-app/{fallback → components}/homepage.tsx +2 -3
- package/src/next-app/components/navigation-progress.tsx +62 -62
- package/src/next-app/components/resizable-preview.tsx +101 -101
- package/src/next-app/components/sidebar-provider.tsx +75 -75
- package/src/next-app/hooks/use-controls.ts +72 -72
- package/src/next-app/lib/i18n.tsx +630 -630
- package/src/next-app/lib/registry-adapter.ts +9 -35
- package/src/next-app/lib/utils.ts +6 -6
- package/src/next-app/next-env.d.ts +6 -6
- package/src/next-app/next.config.ts +0 -5
- package/src/next-app/postcss.config.mjs +7 -7
- package/src/next-app/user-aliases.d.ts +0 -7
- package/src/next-app/app/_user-global.css +0 -6
- package/src/next-app/app/_user-sources.css +0 -9
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AlertTriangle,
|
|
5
|
-
ChevronsDown,
|
|
6
|
-
Terminal,
|
|
7
|
-
Hash,
|
|
8
|
-
BookMarked,
|
|
9
|
-
RectangleHorizontal,
|
|
10
|
-
BadgeCheck,
|
|
11
|
-
ChevronRight,
|
|
12
|
-
CalendarDays,
|
|
13
|
-
CreditCard,
|
|
14
|
-
CheckSquare,
|
|
15
|
-
ChevronsUpDown,
|
|
16
|
-
MousePointerClick,
|
|
17
|
-
Inbox,
|
|
18
|
-
Keyboard,
|
|
19
|
-
PanelTop,
|
|
20
|
-
ChevronDown,
|
|
21
|
-
TextCursorInput,
|
|
22
|
-
Tag,
|
|
23
|
-
Layers,
|
|
24
|
-
CircleDot,
|
|
25
|
-
ListFilter,
|
|
26
|
-
SeparatorHorizontal,
|
|
27
|
-
PanelRight,
|
|
28
|
-
LayoutDashboard,
|
|
29
|
-
Square,
|
|
30
|
-
Bell,
|
|
31
|
-
Table2,
|
|
32
|
-
Columns3,
|
|
33
|
-
AlignLeft,
|
|
34
|
-
ToggleLeft,
|
|
35
|
-
MessageSquare,
|
|
36
|
-
Pipette,
|
|
37
|
-
Palette,
|
|
38
|
-
TableProperties,
|
|
39
|
-
FunctionSquare,
|
|
40
|
-
Upload,
|
|
41
|
-
SplitSquareHorizontal,
|
|
42
|
-
TextCursor,
|
|
43
|
-
MousePointer2,
|
|
44
|
-
KeyRound,
|
|
45
|
-
ArrowLeftRight,
|
|
46
|
-
ShieldAlert,
|
|
47
|
-
SquarePen,
|
|
48
|
-
Filter,
|
|
49
|
-
Users,
|
|
50
|
-
Link2,
|
|
51
|
-
LogIn,
|
|
52
|
-
UserPlus,
|
|
53
|
-
Smartphone,
|
|
54
|
-
Mail,
|
|
55
|
-
Lock,
|
|
56
|
-
Shield,
|
|
57
|
-
UserCog,
|
|
58
|
-
Building2,
|
|
59
|
-
Users2,
|
|
60
|
-
ShieldCheck,
|
|
61
|
-
Component,
|
|
62
|
-
type LucideIcon,
|
|
63
|
-
} from "lucide-react"
|
|
64
|
-
|
|
65
|
-
const iconMap: Record<string, LucideIcon> = {
|
|
66
|
-
accordion: ChevronsDown,
|
|
67
|
-
alert: AlertTriangle,
|
|
68
|
-
avatar: CircleDot,
|
|
69
|
-
badge: BadgeCheck,
|
|
70
|
-
breadcrumb: ChevronRight,
|
|
71
|
-
button: RectangleHorizontal,
|
|
72
|
-
calendar: CalendarDays,
|
|
73
|
-
card: CreditCard,
|
|
74
|
-
checkbox: CheckSquare,
|
|
75
|
-
collapsible: ChevronsUpDown,
|
|
76
|
-
command: Terminal,
|
|
77
|
-
"confirm-dialog": ShieldAlert,
|
|
78
|
-
"context-menu": MousePointerClick,
|
|
79
|
-
"color-picker": Pipette,
|
|
80
|
-
"color-swatch": Palette,
|
|
81
|
-
"data-table": TableProperties,
|
|
82
|
-
"empty-state": Inbox,
|
|
83
|
-
"file-upload": Upload,
|
|
84
|
-
"form-section": SquarePen,
|
|
85
|
-
kbd: Keyboard,
|
|
86
|
-
"formula-editor": FunctionSquare,
|
|
87
|
-
"app-switcher": ArrowLeftRight,
|
|
88
|
-
dialog: PanelTop,
|
|
89
|
-
"dropdown-menu": ChevronDown,
|
|
90
|
-
input: TextCursorInput,
|
|
91
|
-
"input-otp": Hash,
|
|
92
|
-
label: Tag,
|
|
93
|
-
"live-caret": TextCursor,
|
|
94
|
-
"live-cursor": MousePointer2,
|
|
95
|
-
pagination: BookMarked,
|
|
96
|
-
popover: Layers,
|
|
97
|
-
"radio-group": CircleDot,
|
|
98
|
-
"search-filter-bar": Filter,
|
|
99
|
-
select: ListFilter,
|
|
100
|
-
separator: SeparatorHorizontal,
|
|
101
|
-
"password-input": KeyRound,
|
|
102
|
-
sheet: PanelRight,
|
|
103
|
-
sidebar: LayoutDashboard,
|
|
104
|
-
skeleton: Square,
|
|
105
|
-
"split-button": SplitSquareHorizontal,
|
|
106
|
-
sonner: Bell,
|
|
107
|
-
table: Table2,
|
|
108
|
-
tabs: Columns3,
|
|
109
|
-
textarea: AlignLeft,
|
|
110
|
-
toggle: ToggleLeft,
|
|
111
|
-
tooltip: MessageSquare,
|
|
112
|
-
"social-links": Link2,
|
|
113
|
-
"user-status": Users,
|
|
114
|
-
"auth-login": LogIn,
|
|
115
|
-
"auth-register": UserPlus,
|
|
116
|
-
"authorized-devices": Smartphone,
|
|
117
|
-
"email-update-form": Mail,
|
|
118
|
-
"password-reset-form": Lock,
|
|
119
|
-
"mfa-form": Shield,
|
|
120
|
-
"profile-form": UserCog,
|
|
121
|
-
"account-deletion-form": AlertTriangle,
|
|
122
|
-
"org-settings-form": Building2,
|
|
123
|
-
"org-roles-form": ShieldCheck,
|
|
124
|
-
"org-members-form": Users2,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function ComponentHeroIcon({ name }: { name: string }) {
|
|
128
|
-
const Icon = iconMap[name] ?? Component
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<Icon
|
|
132
|
-
className="absolute right-0 top-1/2 -translate-y-1/2 size-28 text-foreground/4 -rotate-12 pointer-events-none"
|
|
133
|
-
strokeWidth={1.5}
|
|
134
|
-
/>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function getComponentIcon(name: string): LucideIcon {
|
|
139
|
-
return iconMap[name] ?? Component
|
|
140
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AlertTriangle,
|
|
5
|
+
ChevronsDown,
|
|
6
|
+
Terminal,
|
|
7
|
+
Hash,
|
|
8
|
+
BookMarked,
|
|
9
|
+
RectangleHorizontal,
|
|
10
|
+
BadgeCheck,
|
|
11
|
+
ChevronRight,
|
|
12
|
+
CalendarDays,
|
|
13
|
+
CreditCard,
|
|
14
|
+
CheckSquare,
|
|
15
|
+
ChevronsUpDown,
|
|
16
|
+
MousePointerClick,
|
|
17
|
+
Inbox,
|
|
18
|
+
Keyboard,
|
|
19
|
+
PanelTop,
|
|
20
|
+
ChevronDown,
|
|
21
|
+
TextCursorInput,
|
|
22
|
+
Tag,
|
|
23
|
+
Layers,
|
|
24
|
+
CircleDot,
|
|
25
|
+
ListFilter,
|
|
26
|
+
SeparatorHorizontal,
|
|
27
|
+
PanelRight,
|
|
28
|
+
LayoutDashboard,
|
|
29
|
+
Square,
|
|
30
|
+
Bell,
|
|
31
|
+
Table2,
|
|
32
|
+
Columns3,
|
|
33
|
+
AlignLeft,
|
|
34
|
+
ToggleLeft,
|
|
35
|
+
MessageSquare,
|
|
36
|
+
Pipette,
|
|
37
|
+
Palette,
|
|
38
|
+
TableProperties,
|
|
39
|
+
FunctionSquare,
|
|
40
|
+
Upload,
|
|
41
|
+
SplitSquareHorizontal,
|
|
42
|
+
TextCursor,
|
|
43
|
+
MousePointer2,
|
|
44
|
+
KeyRound,
|
|
45
|
+
ArrowLeftRight,
|
|
46
|
+
ShieldAlert,
|
|
47
|
+
SquarePen,
|
|
48
|
+
Filter,
|
|
49
|
+
Users,
|
|
50
|
+
Link2,
|
|
51
|
+
LogIn,
|
|
52
|
+
UserPlus,
|
|
53
|
+
Smartphone,
|
|
54
|
+
Mail,
|
|
55
|
+
Lock,
|
|
56
|
+
Shield,
|
|
57
|
+
UserCog,
|
|
58
|
+
Building2,
|
|
59
|
+
Users2,
|
|
60
|
+
ShieldCheck,
|
|
61
|
+
Component,
|
|
62
|
+
type LucideIcon,
|
|
63
|
+
} from "lucide-react"
|
|
64
|
+
|
|
65
|
+
const iconMap: Record<string, LucideIcon> = {
|
|
66
|
+
accordion: ChevronsDown,
|
|
67
|
+
alert: AlertTriangle,
|
|
68
|
+
avatar: CircleDot,
|
|
69
|
+
badge: BadgeCheck,
|
|
70
|
+
breadcrumb: ChevronRight,
|
|
71
|
+
button: RectangleHorizontal,
|
|
72
|
+
calendar: CalendarDays,
|
|
73
|
+
card: CreditCard,
|
|
74
|
+
checkbox: CheckSquare,
|
|
75
|
+
collapsible: ChevronsUpDown,
|
|
76
|
+
command: Terminal,
|
|
77
|
+
"confirm-dialog": ShieldAlert,
|
|
78
|
+
"context-menu": MousePointerClick,
|
|
79
|
+
"color-picker": Pipette,
|
|
80
|
+
"color-swatch": Palette,
|
|
81
|
+
"data-table": TableProperties,
|
|
82
|
+
"empty-state": Inbox,
|
|
83
|
+
"file-upload": Upload,
|
|
84
|
+
"form-section": SquarePen,
|
|
85
|
+
kbd: Keyboard,
|
|
86
|
+
"formula-editor": FunctionSquare,
|
|
87
|
+
"app-switcher": ArrowLeftRight,
|
|
88
|
+
dialog: PanelTop,
|
|
89
|
+
"dropdown-menu": ChevronDown,
|
|
90
|
+
input: TextCursorInput,
|
|
91
|
+
"input-otp": Hash,
|
|
92
|
+
label: Tag,
|
|
93
|
+
"live-caret": TextCursor,
|
|
94
|
+
"live-cursor": MousePointer2,
|
|
95
|
+
pagination: BookMarked,
|
|
96
|
+
popover: Layers,
|
|
97
|
+
"radio-group": CircleDot,
|
|
98
|
+
"search-filter-bar": Filter,
|
|
99
|
+
select: ListFilter,
|
|
100
|
+
separator: SeparatorHorizontal,
|
|
101
|
+
"password-input": KeyRound,
|
|
102
|
+
sheet: PanelRight,
|
|
103
|
+
sidebar: LayoutDashboard,
|
|
104
|
+
skeleton: Square,
|
|
105
|
+
"split-button": SplitSquareHorizontal,
|
|
106
|
+
sonner: Bell,
|
|
107
|
+
table: Table2,
|
|
108
|
+
tabs: Columns3,
|
|
109
|
+
textarea: AlignLeft,
|
|
110
|
+
toggle: ToggleLeft,
|
|
111
|
+
tooltip: MessageSquare,
|
|
112
|
+
"social-links": Link2,
|
|
113
|
+
"user-status": Users,
|
|
114
|
+
"auth-login": LogIn,
|
|
115
|
+
"auth-register": UserPlus,
|
|
116
|
+
"authorized-devices": Smartphone,
|
|
117
|
+
"email-update-form": Mail,
|
|
118
|
+
"password-reset-form": Lock,
|
|
119
|
+
"mfa-form": Shield,
|
|
120
|
+
"profile-form": UserCog,
|
|
121
|
+
"account-deletion-form": AlertTriangle,
|
|
122
|
+
"org-settings-form": Building2,
|
|
123
|
+
"org-roles-form": ShieldCheck,
|
|
124
|
+
"org-members-form": Users2,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function ComponentHeroIcon({ name }: { name: string }) {
|
|
128
|
+
const Icon = iconMap[name] ?? Component
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Icon
|
|
132
|
+
className="absolute right-0 top-1/2 -translate-y-1/2 size-28 text-foreground/4 -rotate-12 pointer-events-none"
|
|
133
|
+
strokeWidth={1.5}
|
|
134
|
+
/>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getComponentIcon(name: string): LucideIcon {
|
|
139
|
+
return iconMap[name] ?? Component
|
|
140
|
+
}
|
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { Link as LinkIcon } from "lucide-react"
|
|
4
|
-
|
|
5
|
-
function slugify(text: string): string {
|
|
6
|
-
return text
|
|
7
|
-
.toLowerCase()
|
|
8
|
-
.replace(/[^\w\s-]/g, "")
|
|
9
|
-
.replace(/\s+/g, "-")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
13
|
-
children?: React.ReactNode
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function createHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
|
|
17
|
-
const Tag = `h${level}` as const
|
|
18
|
-
|
|
19
|
-
return function Heading({ children, id, ...props }: HeadingProps) {
|
|
20
|
-
const headingId = id || slugify(typeof children === "string" ? children : "")
|
|
21
|
-
if (!headingId) return <Tag {...props}>{children}</Tag>
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<Tag id={headingId} className="group relative scroll-mt-20" {...props}>
|
|
25
|
-
{children}
|
|
26
|
-
<a
|
|
27
|
-
href={`#${headingId}`}
|
|
28
|
-
onClick={(e) => {
|
|
29
|
-
e.preventDefault()
|
|
30
|
-
history.replaceState(null, "", `#${headingId}`)
|
|
31
|
-
document.getElementById(headingId)?.scrollIntoView({ behavior: "smooth", block: "start" })
|
|
32
|
-
// Copy link to clipboard
|
|
33
|
-
navigator.clipboard?.writeText(window.location.href)
|
|
34
|
-
}}
|
|
35
|
-
className="inline-flex items-center ml-2 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
|
36
|
-
aria-label={`Link to ${typeof children === "string" ? children : "section"}`}
|
|
37
|
-
>
|
|
38
|
-
<LinkIcon className="size-4" />
|
|
39
|
-
</a>
|
|
40
|
-
</Tag>
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const mdxHeadings = {
|
|
46
|
-
h1: createHeading(1),
|
|
47
|
-
h2: createHeading(2),
|
|
48
|
-
h3: createHeading(3),
|
|
49
|
-
h4: createHeading(4),
|
|
50
|
-
h5: createHeading(5),
|
|
51
|
-
h6: createHeading(6),
|
|
52
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Link as LinkIcon } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
function slugify(text: string): string {
|
|
6
|
+
return text
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.replace(/[^\w\s-]/g, "")
|
|
9
|
+
.replace(/\s+/g, "-")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
13
|
+
children?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createHeading(level: 1 | 2 | 3 | 4 | 5 | 6) {
|
|
17
|
+
const Tag = `h${level}` as const
|
|
18
|
+
|
|
19
|
+
return function Heading({ children, id, ...props }: HeadingProps) {
|
|
20
|
+
const headingId = id || slugify(typeof children === "string" ? children : "")
|
|
21
|
+
if (!headingId) return <Tag {...props}>{children}</Tag>
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Tag id={headingId} className="group relative scroll-mt-20" {...props}>
|
|
25
|
+
{children}
|
|
26
|
+
<a
|
|
27
|
+
href={`#${headingId}`}
|
|
28
|
+
onClick={(e) => {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
history.replaceState(null, "", `#${headingId}`)
|
|
31
|
+
document.getElementById(headingId)?.scrollIntoView({ behavior: "smooth", block: "start" })
|
|
32
|
+
// Copy link to clipboard
|
|
33
|
+
navigator.clipboard?.writeText(window.location.href)
|
|
34
|
+
}}
|
|
35
|
+
className="inline-flex items-center ml-2 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
|
|
36
|
+
aria-label={`Link to ${typeof children === "string" ? children : "section"}`}
|
|
37
|
+
>
|
|
38
|
+
<LinkIcon className="size-4" />
|
|
39
|
+
</a>
|
|
40
|
+
</Tag>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const mdxHeadings = {
|
|
46
|
+
h1: createHeading(1),
|
|
47
|
+
h2: createHeading(2),
|
|
48
|
+
h3: createHeading(3),
|
|
49
|
+
h4: createHeading(4),
|
|
50
|
+
h5: createHeading(5),
|
|
51
|
+
h6: createHeading(6),
|
|
52
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `homePage`. Two states:
|
|
2
|
+
* The shell's homepage rendered at `/`. Two states:
|
|
4
3
|
* - Registry has content → generic index listing components/blocks/docs.
|
|
5
4
|
* - Registry empty / absent → terse "no registry wired" placeholder
|
|
6
5
|
* pointing at the shell's documentation site.
|
|
@@ -10,7 +9,7 @@ import { getAllComponents } from "@shell/lib/components-nav"
|
|
|
10
9
|
import { getAllDocs } from "@shell/lib/docs"
|
|
11
10
|
import type { HomePageProps } from "@shell/lib/registry-adapter"
|
|
12
11
|
|
|
13
|
-
export default function
|
|
12
|
+
export default function HomePage({ firstDocSlug }: HomePageProps) {
|
|
14
13
|
const items = getAllComponents()
|
|
15
14
|
const docs = getAllDocs()
|
|
16
15
|
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useSyncExternalStore } from "react"
|
|
4
|
-
import { usePathname } from "next/navigation"
|
|
5
|
-
|
|
6
|
-
// Simple external store for navigation state
|
|
7
|
-
let listeners: Array<() => void> = []
|
|
8
|
-
let navProgress = { loading: false, progress: 0 }
|
|
9
|
-
|
|
10
|
-
function setNav(updates: Partial<typeof navProgress>) {
|
|
11
|
-
navProgress = { ...navProgress, ...updates }
|
|
12
|
-
listeners.forEach((l) => l())
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function subscribe(listener: () => void) {
|
|
16
|
-
listeners.push(listener)
|
|
17
|
-
return () => { listeners = listeners.filter((l) => l !== listener) }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getSnapshot() { return navProgress }
|
|
21
|
-
|
|
22
|
-
export function NavigationProgress() {
|
|
23
|
-
const pathname = usePathname()
|
|
24
|
-
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
25
|
-
const prevPathname = useRef(pathname)
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (prevPathname.current !== pathname) {
|
|
29
|
-
prevPathname.current = pathname
|
|
30
|
-
setNav({ progress: 100 })
|
|
31
|
-
const timer = setTimeout(() => setNav({ loading: false, progress: 0 }), 200)
|
|
32
|
-
return () => clearTimeout(timer)
|
|
33
|
-
}
|
|
34
|
-
}, [pathname])
|
|
35
|
-
|
|
36
|
-
// Intercept link clicks to show progress
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
function handleClick(e: MouseEvent) {
|
|
39
|
-
const link = (e.target as HTMLElement).closest("a")
|
|
40
|
-
if (!link) return
|
|
41
|
-
const href = link.getAttribute("href")
|
|
42
|
-
if (!href || href.startsWith("#") || href.startsWith("http") || href.startsWith("mailto:")) return
|
|
43
|
-
if (href === pathname) return
|
|
44
|
-
setNav({ loading: true, progress: 30 })
|
|
45
|
-
setTimeout(() => setNav({ progress: 70 }), 100)
|
|
46
|
-
}
|
|
47
|
-
document.addEventListener("click", handleClick)
|
|
48
|
-
return () => document.removeEventListener("click", handleClick)
|
|
49
|
-
}, [pathname])
|
|
50
|
-
|
|
51
|
-
if (!state.loading && state.progress === 0) return null
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div
|
|
55
|
-
className="fixed top-0 left-0 z-[100] h-0.5 bg-primary transition-all duration-300 ease-out"
|
|
56
|
-
style={{
|
|
57
|
-
width: `${state.progress}%`,
|
|
58
|
-
opacity: state.progress >= 100 ? 0 : 1,
|
|
59
|
-
}}
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useSyncExternalStore } from "react"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
|
|
6
|
+
// Simple external store for navigation state
|
|
7
|
+
let listeners: Array<() => void> = []
|
|
8
|
+
let navProgress = { loading: false, progress: 0 }
|
|
9
|
+
|
|
10
|
+
function setNav(updates: Partial<typeof navProgress>) {
|
|
11
|
+
navProgress = { ...navProgress, ...updates }
|
|
12
|
+
listeners.forEach((l) => l())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function subscribe(listener: () => void) {
|
|
16
|
+
listeners.push(listener)
|
|
17
|
+
return () => { listeners = listeners.filter((l) => l !== listener) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSnapshot() { return navProgress }
|
|
21
|
+
|
|
22
|
+
export function NavigationProgress() {
|
|
23
|
+
const pathname = usePathname()
|
|
24
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
25
|
+
const prevPathname = useRef(pathname)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (prevPathname.current !== pathname) {
|
|
29
|
+
prevPathname.current = pathname
|
|
30
|
+
setNav({ progress: 100 })
|
|
31
|
+
const timer = setTimeout(() => setNav({ loading: false, progress: 0 }), 200)
|
|
32
|
+
return () => clearTimeout(timer)
|
|
33
|
+
}
|
|
34
|
+
}, [pathname])
|
|
35
|
+
|
|
36
|
+
// Intercept link clicks to show progress
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
function handleClick(e: MouseEvent) {
|
|
39
|
+
const link = (e.target as HTMLElement).closest("a")
|
|
40
|
+
if (!link) return
|
|
41
|
+
const href = link.getAttribute("href")
|
|
42
|
+
if (!href || href.startsWith("#") || href.startsWith("http") || href.startsWith("mailto:")) return
|
|
43
|
+
if (href === pathname) return
|
|
44
|
+
setNav({ loading: true, progress: 30 })
|
|
45
|
+
setTimeout(() => setNav({ progress: 70 }), 100)
|
|
46
|
+
}
|
|
47
|
+
document.addEventListener("click", handleClick)
|
|
48
|
+
return () => document.removeEventListener("click", handleClick)
|
|
49
|
+
}, [pathname])
|
|
50
|
+
|
|
51
|
+
if (!state.loading && state.progress === 0) return null
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className="fixed top-0 left-0 z-[100] h-0.5 bg-primary transition-all duration-300 ease-out"
|
|
56
|
+
style={{
|
|
57
|
+
width: `${state.progress}%`,
|
|
58
|
+
opacity: state.progress >= 100 ? 0 : 1,
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|