jamdesk 1.1.49 → 1.1.50
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/dist/__tests__/unit/deps-sync.test.js +9 -5
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/package.json +1 -1
- package/vendored/app/(unlock)/layout.tsx +43 -0
- package/vendored/app/[[...slug]]/layout.tsx +119 -0
- package/vendored/app/[[...slug]]/page.tsx +56 -751
- package/vendored/app/layout.tsx +11 -606
- package/vendored/app/not-found.tsx +27 -43
- package/vendored/components/AIActionsMenu.tsx +1 -1
- package/vendored/components/FontAwesomeLoader.tsx +7 -11
- package/vendored/components/HtmlLangSync.tsx +1 -1
- package/vendored/components/errors/NotFoundContent.tsx +5 -1
- package/vendored/components/layout/LayoutWrapper.tsx +5 -21
- package/vendored/components/layout/PageColumns.tsx +24 -1
- package/vendored/components/navigation/Header.tsx +1 -1
- package/vendored/components/navigation/LanguageSelector.tsx +2 -2
- package/vendored/components/navigation/Sidebar.tsx +38 -9
- package/vendored/components/navigation/TabsNav.tsx +1 -1
- package/vendored/components/search/SearchModal.tsx +1 -1
- package/vendored/lib/layout-helpers.tsx +464 -0
- package/vendored/lib/middleware-helpers.ts +0 -78
- package/vendored/lib/page-isr-helpers.ts +16 -0
- package/vendored/lib/project-resolver.ts +28 -1
- package/vendored/lib/r2-content.ts +8 -0
- package/vendored/lib/render-doc-page.tsx +595 -0
- package/vendored/lib/seo.ts +21 -0
- package/vendored/workspace-package-lock.json +7 -7
|
@@ -1,47 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// Bare 404 for unresolved-project requests (wild hostname, deleted R2
|
|
2
|
+
// project). Renders WITHOUT docs chrome — no theme, no fonts, no
|
|
3
|
+
// providers — because there's no project context to read chrome from.
|
|
4
|
+
// In-project 404s (real project, missing slug) are returned by
|
|
5
|
+
// app/[[...slug]]/page.tsx via Next's notFound() and inherit the docs
|
|
6
|
+
// layout and chrome from the catch-all segment.
|
|
8
7
|
export const metadata = {
|
|
9
|
-
title: '
|
|
8
|
+
title: 'Site not found',
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
export default
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// No project slug - use minimal fallback
|
|
33
|
-
config = {
|
|
34
|
-
name: 'Documentation',
|
|
35
|
-
theme: 'jam',
|
|
36
|
-
colors: { primary: '#0ea5e9' },
|
|
37
|
-
navigation: { groups: [] },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
config = getDocsConfig();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Note: LayoutWrapper is already provided by the root layout (layout.tsx)
|
|
45
|
-
// so we only render the content here
|
|
46
|
-
return <NotFoundContent config={config} />;
|
|
11
|
+
export default function RootNotFound() {
|
|
12
|
+
return (
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<body
|
|
15
|
+
style={{
|
|
16
|
+
margin: 0,
|
|
17
|
+
fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
|
18
|
+
padding: '4rem 2rem',
|
|
19
|
+
maxWidth: '40rem',
|
|
20
|
+
marginInline: 'auto',
|
|
21
|
+
color: '#111',
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<h1 style={{ fontSize: '2rem', fontWeight: 600 }}>Site not found</h1>
|
|
25
|
+
<p style={{ marginTop: '1rem', color: '#666' }}>
|
|
26
|
+
This Jamdesk project does not exist or has been removed.
|
|
27
|
+
</p>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
47
31
|
}
|
|
@@ -298,7 +298,7 @@ export function AIActionsMenu({ options, projectName }: AIActionsMenuProps) {
|
|
|
298
298
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
299
299
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
300
300
|
const itemsRef = useRef<(HTMLButtonElement | HTMLAnchorElement | null)[]>([]);
|
|
301
|
-
const pathname = usePathname();
|
|
301
|
+
const pathname = usePathname() ?? '';
|
|
302
302
|
const linkPrefix = useLinkPrefix();
|
|
303
303
|
|
|
304
304
|
// Compute URLs client-side to survive SPA navigation
|
|
@@ -8,20 +8,15 @@ export { FA_CSS_HREF };
|
|
|
8
8
|
/**
|
|
9
9
|
* Ensures Font Awesome CSS is loaded in the document head.
|
|
10
10
|
*
|
|
11
|
-
* On normal pages,
|
|
12
|
-
*
|
|
13
|
-
* a minimal error shell (<html id="__next_error__">)
|
|
14
|
-
* layout's <head> content.
|
|
15
|
-
*
|
|
16
|
-
* don't execute. This component acts as a fallback, injecting the stylesheet
|
|
17
|
-
* link via useEffect so icons render on 404 pages too.
|
|
11
|
+
* On normal pages, layout-helpers calls preinit(FA_CSS_HREF, { as: 'style' })
|
|
12
|
+
* and React 19 hoists the <link rel="stylesheet"> into <head> at SSR time. But
|
|
13
|
+
* on 404 pages Next.js renders a minimal error shell (<html id="__next_error__">)
|
|
14
|
+
* that omits the root layout's <head> content. This component runs in
|
|
15
|
+
* useEffect as a client-side fallback so icons still render on 404 pages.
|
|
18
16
|
*/
|
|
19
17
|
export function FontAwesomeLoader() {
|
|
20
18
|
useEffect(() => {
|
|
21
|
-
//
|
|
22
|
-
if ((window as Window & { __FA_CSS_LOADED__?: boolean }).__FA_CSS_LOADED__) return;
|
|
23
|
-
|
|
24
|
-
// 404 error shell path: check if stylesheet exists (handles browser-normalized absolute URLs)
|
|
19
|
+
// querySelector handles browser-normalized absolute href values.
|
|
25
20
|
const existing = document.querySelector(
|
|
26
21
|
`link[rel="stylesheet"][href$="${FA_CSS_HREF}"]`
|
|
27
22
|
);
|
|
@@ -31,6 +26,7 @@ export function FontAwesomeLoader() {
|
|
|
31
26
|
link.rel = 'stylesheet';
|
|
32
27
|
link.href = FA_CSS_HREF;
|
|
33
28
|
document.head.appendChild(link);
|
|
29
|
+
return () => link.remove();
|
|
34
30
|
}, []);
|
|
35
31
|
|
|
36
32
|
return null;
|
|
@@ -20,7 +20,7 @@ interface HtmlLangSyncProps {
|
|
|
20
20
|
* `lang` pointing at the previous language until a hard refresh.
|
|
21
21
|
*/
|
|
22
22
|
export function HtmlLangSync({ defaultLanguage }: HtmlLangSyncProps) {
|
|
23
|
-
const pathname = usePathname();
|
|
23
|
+
const pathname = usePathname() ?? '';
|
|
24
24
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
const lang = extractLanguageFromPath(pathname || '/') ?? defaultLanguage;
|
|
@@ -81,7 +81,11 @@ export function NotFoundContent({ config }: NotFoundContentProps) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
// No matched page — go to the project home (`/docs` for hostAtDocs,
|
|
85
|
+
// `/` otherwise). FALLBACK_CONFIG with empty groups hits this branch
|
|
86
|
+
// and previously produced `/index` (404 on click) or `/docs/index`
|
|
87
|
+
// (also 404). The bare prefix is the reachable project root.
|
|
88
|
+
return linkPrefix || '/';
|
|
85
89
|
};
|
|
86
90
|
|
|
87
91
|
return (
|
|
@@ -47,27 +47,11 @@ export function LayoutWrapper({ config, children }: LayoutWrapperProps) {
|
|
|
47
47
|
// Handle hash navigation for three-column independent scroll layout
|
|
48
48
|
useHashNavigation();
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// The
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const scrollTarget = contentScroll || window;
|
|
56
|
-
|
|
57
|
-
const handleScroll = () => {
|
|
58
|
-
const scrollY = contentScroll ? contentScroll.scrollTop : window.scrollY;
|
|
59
|
-
if (scrollY > 500) {
|
|
60
|
-
document.body.classList.add('scrolled');
|
|
61
|
-
} else {
|
|
62
|
-
document.body.classList.remove('scrolled');
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
67
|
-
handleScroll(); // Check initial position
|
|
68
|
-
|
|
69
|
-
return () => scrollTarget.removeEventListener('scroll', handleScroll);
|
|
70
|
-
}, []);
|
|
50
|
+
// The body.scrolled toggle used to live here, but #content-scroll-container
|
|
51
|
+
// is rendered by PageColumns (in page.tsx) and remounts on every navigation.
|
|
52
|
+
// The listener attached once at LayoutWrapper mount referenced a detached
|
|
53
|
+
// node after the first nav, so .scrolled never updated again. The effect
|
|
54
|
+
// now lives in PageColumns so it re-binds on each container instance.
|
|
71
55
|
|
|
72
56
|
// Get theme config to determine layout
|
|
73
57
|
const themeConfig = getTheme(config.theme as ThemeName | undefined);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type ReactNode } from 'react';
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from 'react';
|
|
4
4
|
import { useChatPanel } from '@/hooks/useChatPanel';
|
|
5
5
|
|
|
6
6
|
interface PageColumnsProps {
|
|
@@ -16,11 +16,34 @@ interface PageColumnsProps {
|
|
|
16
16
|
*/
|
|
17
17
|
export function PageColumns({ children, toc, isWideMode }: PageColumnsProps) {
|
|
18
18
|
const { isChatOpen } = useChatPanel();
|
|
19
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
// Toggle body.scrolled when the user scrolls past 500px in the content
|
|
22
|
+
// column. Lives here (not in LayoutWrapper) because the scroll container is
|
|
23
|
+
// rendered by this component and is replaced on every page navigation —
|
|
24
|
+
// attaching the listener at the layout level left it bound to a detached
|
|
25
|
+
// node after the first nav. Used by the Jam theme to hide the top gradient
|
|
26
|
+
// and make the header solid (themes/jam/variables.css).
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const contentScroll = scrollRef.current;
|
|
29
|
+
const scrollTarget: HTMLElement | Window = contentScroll ?? window;
|
|
30
|
+
|
|
31
|
+
const handleScroll = () => {
|
|
32
|
+
const scrollY = contentScroll ? contentScroll.scrollTop : window.scrollY;
|
|
33
|
+
document.body.classList.toggle('scrolled', scrollY > 500);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
scrollTarget.addEventListener('scroll', handleScroll, { passive: true });
|
|
37
|
+
handleScroll();
|
|
38
|
+
|
|
39
|
+
return () => scrollTarget.removeEventListener('scroll', handleScroll);
|
|
40
|
+
}, []);
|
|
19
41
|
|
|
20
42
|
return (
|
|
21
43
|
<div className="flex h-full">
|
|
22
44
|
{/* Content column - scrolls independently */}
|
|
23
45
|
<div
|
|
46
|
+
ref={scrollRef}
|
|
24
47
|
id="content-scroll-container"
|
|
25
48
|
className="flex-1 min-w-0 lg:overflow-y-auto lg:h-full content-scroll"
|
|
26
49
|
>
|
|
@@ -44,7 +44,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
|
|
|
44
44
|
const [isDark, setIsDark] = useState(false);
|
|
45
45
|
const [isTabsDropdownOpen, setIsTabsDropdownOpen] = useState(false);
|
|
46
46
|
const tabsDropdownRef = useRef<HTMLDivElement>(null);
|
|
47
|
-
const pathname = usePathname();
|
|
47
|
+
const pathname = usePathname() ?? '';
|
|
48
48
|
const linkPrefix = useLinkPrefix();
|
|
49
49
|
const currentLang = extractLanguageFromPath(pathname || '/');
|
|
50
50
|
const ui = getUiStrings(currentLang);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
4
|
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
|
|
5
|
-
import {
|
|
5
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
6
6
|
import type { ResolvedLanguage } from '@/lib/navigation-resolver';
|
|
7
7
|
import type { LanguageCode } from '@/lib/docs-types';
|
|
8
8
|
import {
|
|
@@ -33,7 +33,7 @@ export function LanguageSelector({
|
|
|
33
33
|
}: LanguageSelectorProps) {
|
|
34
34
|
const router = useRouter();
|
|
35
35
|
const linkPrefix = useLinkPrefix();
|
|
36
|
-
const pathname = usePathname();
|
|
36
|
+
const pathname = usePathname() ?? '';
|
|
37
37
|
const ui = getUiStrings(extractLanguageFromPath(pathname || '/'));
|
|
38
38
|
const [isOpen, setIsOpen] = useState(false);
|
|
39
39
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
@@ -134,7 +134,7 @@ function findGroupsContainingPath(groups: ResolvedGroup[], currentPath: string,
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPositionProp, isOpen = false, onClose }: SidebarProps) {
|
|
137
|
-
const pathname = usePathname();
|
|
137
|
+
const pathname = usePathname() ?? '';
|
|
138
138
|
const router = useRouter();
|
|
139
139
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
|
140
140
|
const [logoError, setLogoError] = useState(false);
|
|
@@ -149,9 +149,19 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
149
149
|
// Two-stage feedback for nav clicks:
|
|
150
150
|
// pendingPathname → optimistic active highlight (instant)
|
|
151
151
|
// spinnerPathname → small spinner on the row (after 500ms, only if nav is slow)
|
|
152
|
-
const { pendingPathname, spinnerPathname, onNavigate:
|
|
152
|
+
const { pendingPathname, spinnerPathname, onNavigate: rawNavigate } =
|
|
153
153
|
useDelayedNavigationSpinner(pathname);
|
|
154
154
|
|
|
155
|
+
// Suppresses the auto-scroll-to-active-link effect for user-initiated nav.
|
|
156
|
+
// The user just clicked a link they could see — moving it under their cursor
|
|
157
|
+
// is jarring (see the navigation shift recordings from 2026-04-29). We still
|
|
158
|
+
// auto-scroll for back/forward, deep links, and initial mount.
|
|
159
|
+
const userInitiatedNavRef = useRef(false);
|
|
160
|
+
const handleNavigate = useCallback((url: string) => {
|
|
161
|
+
userInitiatedNavRef.current = true;
|
|
162
|
+
rawNavigate(url);
|
|
163
|
+
}, [rawNavigate]);
|
|
164
|
+
|
|
155
165
|
const effectivePathname = pendingPathname || pathname;
|
|
156
166
|
|
|
157
167
|
// Determine effective tabsPosition:
|
|
@@ -253,9 +263,22 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
253
263
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
264
|
}, [pathname]); // Intentionally excluding onClose - we only want to trigger on pathname changes
|
|
255
265
|
|
|
256
|
-
// Scroll the active navigation link into view when pathname changes
|
|
266
|
+
// Scroll the active navigation link into view when pathname changes.
|
|
267
|
+
//
|
|
268
|
+
// Skipped for user-initiated nav: the user just clicked a sidebar link they
|
|
269
|
+
// could see, so moving it under their cursor is a jarring shift. For
|
|
270
|
+
// back/forward, deep links, and initial mount we still auto-scroll, but only
|
|
271
|
+
// when the active link is meaningfully off-screen (<80% visible) — being
|
|
272
|
+
// partially clipped by 1px shouldn't trigger a centering jump.
|
|
273
|
+
//
|
|
274
|
+
// Uses scrollTop math instead of scrollIntoView() so it only moves the
|
|
275
|
+
// sidebar's own scroll container; scrollIntoView walks all scrollable
|
|
276
|
+
// ancestors and was also tugging the document.
|
|
257
277
|
useEffect(() => {
|
|
258
|
-
|
|
278
|
+
if (userInitiatedNavRef.current) {
|
|
279
|
+
userInitiatedNavRef.current = false;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
259
282
|
let rafId = requestAnimationFrame(() => {
|
|
260
283
|
rafId = requestAnimationFrame(() => {
|
|
261
284
|
const scrollContainer = sidebarRef.current?.querySelector('.sidebar-scroll');
|
|
@@ -264,11 +287,17 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
|
|
|
264
287
|
|
|
265
288
|
const containerRect = scrollContainer.getBoundingClientRect();
|
|
266
289
|
const linkRect = activeLink.getBoundingClientRect();
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
290
|
+
const linkHeight = linkRect.bottom - linkRect.top;
|
|
291
|
+
const visibleTop = Math.max(linkRect.top, containerRect.top);
|
|
292
|
+
const visibleBottom = Math.min(linkRect.bottom, containerRect.bottom);
|
|
293
|
+
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
|
|
294
|
+
const visibleFraction = linkHeight > 0 ? visibleHeight / linkHeight : 1;
|
|
295
|
+
if (visibleFraction >= 0.8) return;
|
|
296
|
+
|
|
297
|
+
const desiredCenter = containerRect.top + containerRect.height / 2;
|
|
298
|
+
const linkCenter = linkRect.top + linkHeight / 2;
|
|
299
|
+
const delta = linkCenter - desiredCenter;
|
|
300
|
+
(scrollContainer as HTMLElement).scrollTop += delta;
|
|
272
301
|
});
|
|
273
302
|
});
|
|
274
303
|
|
|
@@ -17,7 +17,7 @@ interface TabsNavProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function TabsNav({ config, className = '' }: TabsNavProps) {
|
|
20
|
-
const pathname = usePathname();
|
|
20
|
+
const pathname = usePathname() ?? '';
|
|
21
21
|
const linkPrefix = useLinkPrefix();
|
|
22
22
|
|
|
23
23
|
// Determine effective tabsPosition
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useRef, type ReactElement } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
5
5
|
import { getRecentSearches, addRecentSearch, clearRecentSearches } from '@/lib/recent-searches';
|
|
6
6
|
import { useFocusTrap } from '@/hooks/useFocusTrap';
|
|
7
7
|
import { useBodyScrollLock } from '@/hooks/useBodyScrollLock';
|