Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/api/docs-search-tree/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { getSideNav } from "@/services/docs/getSideNav";

/**
* Public tree for header docs quick search on pages that don't receive server-fetched `sideNavItems`.
* Same shape as `sideNav[0]?.dotcmsdocumentationchildren` from `getSideNav()`.
*/
export async function GET() {
try {
const sideNav = await getSideNav();
const items = Array.isArray(sideNav)
? sideNav[0]?.dotcmsdocumentationchildren ?? []
: [];
return NextResponse.json({ items });
} catch {
return NextResponse.json({ items: [] as unknown[] });
}
}
3 changes: 1 addition & 2 deletions app/docs/[slug]/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,12 +329,11 @@ export default async function Home({ searchParams, params }) {
<JsonLd pageData={data} path={path} hostname={hostname} />

<div className="flex-1">
<div className="flex flex-col lg:flex-row container mx-auto px-0">
<div className="flex flex-col lg:flex-row container mx-auto min-w-0 px-0">
{/* Left Navigation - Hide on mobile */}
<div className="hidden lg:block w-72 shrink-0">
<RedesignedNavTree
currentPath={slug}
items={sideNav[0]?.dotcmsdocumentationchildren || []}
initialSections={navSections}
/>
</div>
Expand Down
25 changes: 14 additions & 11 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SpeedInsights } from "@vercel/speed-insights/next"
import { LeadboxerScript } from '@/components/metrics/Leadboxer';
import { DotContentAnalytics } from "@dotcms/analytics/react";
import { AnalyticsConfig } from '@/util/config';
import { AssistantProvider } from '@/components/chat/AssistantProvider';

const inter = Inter({
subsets: ['latin'],
Expand Down Expand Up @@ -76,17 +77,19 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<AlertBanner
message={
<>
dotCMS is now licensed under the BSL 1.1 and is free to use in many cases. <a href="https://www.dotcms.com/bsl-faq" className="underline font-medium hover:text-blue-200">Learn more here</a>.
</>
}
/>
<InitialScroll />
<DotContentAnalytics config={AnalyticsConfig} />
{children}
<Toaster />
<AssistantProvider>
<AlertBanner
message={
<>
dotCMS is now licensed under the BSL 1.1 and is free to use in many cases. <a href="https://www.dotcms.com/bsl-faq" className="underline font-medium hover:text-blue-200">Learn more here</a>.
</>
}
/>
<InitialScroll />
<DotContentAnalytics config={AnalyticsConfig} />
{children}
<Toaster />
</AssistantProvider>
</ThemeProvider>
</ErrorBoundary>
<GoogleAnalytics gaId="G-CM1HLQW35G" />
Expand Down
17 changes: 15 additions & 2 deletions components/blogs/blog-detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ import React from 'react';
import { DetailHeader } from './blog-header';
import Authors from './authors';
import BlogComponent from "./blog-component";
import { useAssistant } from "@/components/chat/AssistantProvider";
import { useContentColumnWideLayout } from "@/hooks/useHeaderWideNav";
import { cn } from "@/util/utils";

export default function BlogDetailComponent({ post }) {

const customRenderers = {};
const { open: assistantOpen, expanded: assistantExpanded } = useAssistant();
const showWideColumn = useContentColumnWideLayout(
assistantOpen,
assistantExpanded
);

return (
<div className="container mx-auto w-full min-w-0 max-w-full">
{/* Main Content Grid */}
<div className="flex w-full min-w-0 flex-col gap-4 py-8 xl:flex-row xl:justify-center xl:gap-6">
<div className="flex w-full min-w-0 flex-col gap-4 py-8 xl:flex-row xl:justify-center xl:gap-6 overflow-x-hidden">


{/* Main Content */}
Expand All @@ -28,7 +36,12 @@ export default function BlogDetailComponent({ post }) {
</article>

{/* Right Sidebar */}
<div className="hidden w-64 shrink-0 xl:block">
<div
className={cn(
"w-64 shrink-0",
showWideColumn ? "block" : "hidden"
)}
>
<div className="sticky top-16">
<Authors authors={post.author} />
<OnThisPage />
Expand Down
20 changes: 17 additions & 3 deletions components/changelogs/ChangeLogList.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import Breadcrumbs from "../navigation/Breadcrumbs";
import PaginationBar from "../PaginationBar";
import Dropdown from "../shared/dropdown";
import { useEffect } from "react";
import { useAssistant } from "@/components/chat/AssistantProvider";
import { useContentColumnWideLayout } from "@/hooks/useHeaderWideNav";
import { cn } from "@/util/utils";

export default function ChangeLogContainer({ sideNav, slug }) {
//const router = useRouter(); // see removal of router in handleVersionChange
Expand All @@ -32,6 +35,12 @@ export default function ChangeLogContainer({ sideNav, slug }) {
vParam,
);

const { open: assistantOpen, expanded: assistantExpanded } = useAssistant();
const showWideColumn = useContentColumnWideLayout(
assistantOpen,
assistantExpanded
);

// Handle hash scrolling after data is loaded
useEffect(() => {
if (!loading && window.location.hash) {
Expand Down Expand Up @@ -89,7 +98,7 @@ export default function ChangeLogContainer({ sideNav, slug }) {
}
let thisMajorVersion;
return (
<div className="flex flex-col lg:flex-row w-full max-w-[1400px] mx-auto">
<div className="flex flex-col lg:flex-row w-full min-w-0 max-w-[1400px] mx-auto">
{/* Main Content Area */}
<main className="flex-1 min-w-0 py-8 lg:pb-12 px-0 sm:px-0 lg:px-8
[&::-webkit-scrollbar]:w-1.5
Expand Down Expand Up @@ -210,8 +219,13 @@ export default function ChangeLogContainer({ sideNav, slug }) {
</div>
</main>

{/* Right Sidebar - Hide on smaller screens */}
<div className="w-64 shrink-0 hidden xl:block">
{/* Right Sidebar — effective width (viewport − assistant rail) */}
<div
className={cn(
"w-64 shrink-0",
showWideColumn ? "block" : "hidden"
)}
>
<div className="sticky top-16 pl-8">
<div className="text-muted-foreground
overflow-y-auto p-4 px-2
Expand Down
217 changes: 217 additions & 0 deletions components/chat/AssistantProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"use client";

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
ChatComponent,
type ChatComponentHandle,
} from "@/components/chat/ChatComponent";
import { Button } from "@/components/ui/button";
import {
Sparkles,
Maximize2,
Minimize2,
RotateCcw,
X,
} from "lucide-react";
import { cn } from "@/util/utils";
import { useMediaQuery } from "@/hooks/useMediaQuery";

const WIDTH_NARROW = "min(26rem, 100vw)";
const WIDTH_EXPANDED = "min(42rem, 100vw)";
const LG_UP = "(min-width: 1024px)";

type AssistantContextValue = {
open: boolean;
setOpen: (open: boolean) => void;
toggleOpen: () => void;
expanded: boolean;
setExpanded: (expanded: boolean) => void;
};

const AssistantContext = createContext<AssistantContextValue | null>(null);

export function useAssistant() {
const ctx = useContext(AssistantContext);
if (!ctx) {
throw new Error("useAssistant must be used within AssistantProvider");
}
return ctx;
}

export function AssistantProvider({
children,
}: {
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const chatRef = useRef<ChatComponentHandle>(null);
const isDesktop = useMediaQuery(LG_UP);

useEffect(() => {
if (!open) setExpanded(false);
}, [open]);

useEffect(() => {
if (!isDesktop && open) {
const prev = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prev;
};
}
}, [isDesktop, open]);

const toggleOpen = useCallback(() => {
setOpen((o) => !o);
}, []);

const value = useMemo(
() => ({
open,
setOpen,
toggleOpen,
expanded,
setExpanded,
}),
[open, expanded, toggleOpen]
);

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey;
if (mod && (e.key === "k" || e.key === "K")) {
e.preventDefault();
setOpen((o) => !o);
}
if (mod && e.key === "/") {
e.preventDefault();
setOpen((o) => !o);
}
if (e.key === "Escape") {
setOpen(false);
}
};

document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);

const padRight =
!open || !isDesktop
? "0px"
: expanded
? WIDTH_EXPANDED
: WIDTH_NARROW;

return (
<AssistantContext.Provider value={value}>
<div
className="min-h-dvh min-w-0 transition-[padding-right] duration-300 ease-out"
style={{ paddingRight: padRight }}
>
{children}
</div>

{open && !isDesktop ? (
<button
type="button"
aria-label="Close assistant"
className="fixed inset-0 z-[99] bg-black/45 backdrop-blur-[2px] animate-in fade-in duration-200"
onClick={() => setOpen(false)}
/>
) : null}

<aside
className={cn(
"fixed flex flex-col bg-background",
"transition-[transform,width] duration-300 ease-out",
isDesktop
? [
"inset-y-0 right-0 z-40 border-l border-border shadow-none",
open
? "translate-x-0"
: "translate-x-full pointer-events-none",
]
: [
"inset-x-0 bottom-0 top-16 z-[100] overflow-hidden rounded-t-3xl border border-border/70 shadow-2xl",
"pb-[calc(0.75rem+env(safe-area-inset-bottom,0px))]",
open
? "translate-y-0"
: "translate-y-full pointer-events-none",
]
)}
style={
isDesktop
? {
width: expanded ? WIDTH_EXPANDED : WIDTH_NARROW,
}
: undefined
}
aria-hidden={!open}
>
<div className="flex items-center justify-between gap-3 border-b border-border/70 px-4 py-3 shrink-0">
<div className="flex items-center gap-2.5 min-w-0">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/12 text-primary">
<Sparkles className="h-5 w-5" aria-hidden />
</span>
<span className="font-semibold text-base tracking-tight truncate">
Assistant
</span>
</div>
<div className="flex items-center gap-0.5 shrink-0">
<Button
type="button"
variant="ghost"
size="icon"
className={cn(
"h-9 w-9 text-muted-foreground hover:text-foreground",
!isDesktop && "hidden"
)}
onClick={() => setExpanded((e) => !e)}
aria-label={expanded ? "Use narrow panel" : "Expand panel"}
>
{expanded ? (
<Minimize2 className="h-4 w-4" />
) : (
<Maximize2 className="h-4 w-4" />
)}
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-muted-foreground hover:text-foreground"
onClick={() => chatRef.current?.reset()}
aria-label="Clear conversation"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-9 w-9 text-muted-foreground hover:text-foreground"
onClick={() => setOpen(false)}
aria-label="Close assistant"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>

<div className="flex min-h-0 flex-1 flex-col px-3 pb-5 pt-1 overflow-hidden">
<ChatComponent ref={chatRef} isPanelOpen={open} />
</div>
</aside>
</AssistantContext.Provider>
);
}
Loading