mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 20:19:51 +08:00
The canvas now fills the window and the chat floats on its right. draw.io is served from our own origin (public/drawio, downloaded at build time and checked against the release's SHA-256), so the app can drive the editor directly: - every AI change becomes a version card with a thumbnail; compare, restore or undo it, and Ctrl+Z on the canvas takes an AI change back in one step - shapes the AI just changed are highlighted on the canvas - selected shapes appear as a chip and are sent to the model - the app's own canvas toolbar, shape library and page tabs replace draw.io's chrome - new start screen, session rename and switcher, open .drawio files - settings split into Models & keys, Appearance, Advanced and About - phones switch between canvas and chat - dark mode keeps the diagram on a white sheet and no longer flashes With an external draw.io (NEXT_PUBLIC_DRAWIO_BASE_URL) the app falls back to draw.io's own toolbar and the features above that need same origin are off. The chat logic moved from the 1500-line chat panel into a chat engine plus small UI components; zustand holds canvas, versions, settings and UI state.
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import { ChevronDown, Plus } from "lucide-react"
|
|
import { useState } from "react"
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover"
|
|
import { IconButton } from "@/components/workspace/icon-button"
|
|
import { useDictionary } from "@/hooks/use-dictionary"
|
|
import {
|
|
addPage,
|
|
canManagePages,
|
|
duplicatePage,
|
|
keepFocusInCanvas,
|
|
removePage,
|
|
renamePage,
|
|
selectPage,
|
|
} from "@/lib/drawio/editor-bridge"
|
|
import { useCanvasStore } from "@/stores/canvas-store"
|
|
|
|
/** Page switcher at the bottom of the canvas (replaces draw.io's page tabs) */
|
|
export function PageTabs() {
|
|
const dict = useDictionary()
|
|
const t = dict.workspace
|
|
const pages = useCanvasStore((s) => s.pages)
|
|
const currentPageId = useCanvasStore((s) => s.currentPageId)
|
|
const [menuFor, setMenuFor] = useState<string | null>(null)
|
|
|
|
if (!canManagePages() || pages.length === 0) return null
|
|
|
|
return (
|
|
<div
|
|
className="pointer-events-auto flex max-w-[min(640px,100%)] items-center gap-0.5 overflow-x-auto rounded-xl bg-card p-1 shadow-float"
|
|
data-testid="page-tabs"
|
|
>
|
|
{pages.map((page) => {
|
|
const isCurrent = page.id === currentPageId
|
|
if (!isCurrent) {
|
|
return (
|
|
<button
|
|
key={page.id}
|
|
type="button"
|
|
onClick={() => selectPage(page.id)}
|
|
className="h-7 max-w-40 shrink-0 truncate rounded-lg px-2.5 text-[13px] text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
>
|
|
{page.name}
|
|
</button>
|
|
)
|
|
}
|
|
return (
|
|
<Popover
|
|
key={page.id}
|
|
open={menuFor === page.id}
|
|
onOpenChange={(open) =>
|
|
setMenuFor(open ? page.id : null)
|
|
}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="flex h-7 max-w-44 shrink-0 items-center gap-1 rounded-lg bg-accent px-2.5 text-[13px] font-medium text-foreground"
|
|
>
|
|
<span className="truncate">{page.name}</span>
|
|
<ChevronDown className="size-3.5 text-muted-foreground" />
|
|
</button>
|
|
</PopoverTrigger>
|
|
<PopoverContent
|
|
side="top"
|
|
align="start"
|
|
className="w-44 p-1"
|
|
onCloseAutoFocus={keepFocusInCanvas}
|
|
>
|
|
{[
|
|
{
|
|
label: t.renamePage,
|
|
run: () => renamePage(page.id),
|
|
},
|
|
{
|
|
label: t.duplicatePage,
|
|
run: () => duplicatePage(page.id),
|
|
},
|
|
...(pages.length > 1
|
|
? [
|
|
{
|
|
label: t.deletePage,
|
|
run: () => removePage(page.id),
|
|
},
|
|
]
|
|
: []),
|
|
].map((item) => (
|
|
<button
|
|
key={item.label}
|
|
type="button"
|
|
onClick={() => {
|
|
setMenuFor(null)
|
|
item.run()
|
|
}}
|
|
className="w-full rounded-md px-2.5 py-1.5 text-left text-[13px] hover:bg-accent"
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
})}
|
|
<IconButton
|
|
label={t.addPage}
|
|
size="sm"
|
|
tooltipSide="top"
|
|
onClick={addPage}
|
|
>
|
|
<Plus />
|
|
</IconButton>
|
|
</div>
|
|
)
|
|
}
|