mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-08 05:39:52 +02:00
feat: better layout rendering
This commit is contained in:
@@ -24,14 +24,14 @@ import { toast } from "sonner";
|
|||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const {
|
const {
|
||||||
state,
|
state,
|
||||||
updateLayout,
|
updateLayout,
|
||||||
removeWindow,
|
removeWindow,
|
||||||
switchToTemporary,
|
switchToTemporary,
|
||||||
applyTemporaryToPersistent,
|
applyTemporaryToPersistent,
|
||||||
discardTemporary,
|
discardTemporary,
|
||||||
isTemporary
|
isTemporary,
|
||||||
} = useGrimoire();
|
} = useGrimoire();
|
||||||
const [commandLauncherOpen, setCommandLauncherOpen] = useState(false);
|
const [commandLauncherOpen, setCommandLauncherOpen] = useState(false);
|
||||||
const { actor, identifier } = useParams();
|
const { actor, identifier } = useParams();
|
||||||
@@ -94,18 +94,15 @@ export default function Home() {
|
|||||||
if (spellbookEvent && !hasLoadedSpellbook) {
|
if (spellbookEvent && !hasLoadedSpellbook) {
|
||||||
try {
|
try {
|
||||||
const parsed = parseSpellbook(spellbookEvent as SpellbookEvent);
|
const parsed = parseSpellbook(spellbookEvent as SpellbookEvent);
|
||||||
|
|
||||||
// Use the new temporary state system
|
// Use the new temporary state system
|
||||||
switchToTemporary(parsed);
|
switchToTemporary(parsed);
|
||||||
setHasLoadedSpellbook(true);
|
setHasLoadedSpellbook(true);
|
||||||
|
|
||||||
if (isPreviewPath) {
|
if (isPreviewPath) {
|
||||||
toast.info(`Previewing layout: ${parsed.title}`, {
|
toast.info(`Previewing layout: ${parsed.title}`, {
|
||||||
description: "You are in a temporary session. Apply to keep this layout permanently.",
|
description:
|
||||||
});
|
"You are in a temporary session. Apply to keep this layout.",
|
||||||
} else if (isDirectPath) {
|
|
||||||
toast.success(`Loaded temporary layout: ${parsed.title}`, {
|
|
||||||
description: "Visit / to return to your permanent dashboard, or click Apply Layout.",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -113,12 +110,18 @@ export default function Home() {
|
|||||||
toast.error("Failed to load spellbook");
|
toast.error("Failed to load spellbook");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [spellbookEvent, hasLoadedSpellbook, isPreviewPath, isDirectPath, switchToTemporary]);
|
}, [
|
||||||
|
spellbookEvent,
|
||||||
|
hasLoadedSpellbook,
|
||||||
|
isPreviewPath,
|
||||||
|
isDirectPath,
|
||||||
|
switchToTemporary,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleApplyLayout = () => {
|
const handleApplyLayout = () => {
|
||||||
applyTemporaryToPersistent();
|
applyTemporaryToPersistent();
|
||||||
navigate("/", { replace: true });
|
navigate("/", { replace: true });
|
||||||
toast.success("Layout applied to your dashboard permanently");
|
toast.success("Layout applied to your dashboard");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDiscardPreview = () => {
|
const handleDiscardPreview = () => {
|
||||||
@@ -201,22 +204,24 @@ export default function Home() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BookHeart className="size-4" />
|
<BookHeart className="size-4" />
|
||||||
<span>
|
<span>
|
||||||
{isPreviewPath ? "Preview Mode" : "Temporary Layout"}: {spellbookEvent?.tags.find(t => t[0] === 'title')?.[1] || 'Spellbook'}
|
{isPreviewPath ? "Preview Mode" : "Temporary Layout"}:{" "}
|
||||||
|
{spellbookEvent?.tags.find((t) => t[0] === "title")?.[1] ||
|
||||||
|
"Spellbook"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 hover:bg-black/10 text-accent-foreground font-bold"
|
className="h-7 hover:bg-black/10 text-accent-foreground font-bold"
|
||||||
onClick={handleDiscardPreview}
|
onClick={handleDiscardPreview}
|
||||||
>
|
>
|
||||||
<X className="size-3.5 mr-1" />
|
<X className="size-3.5 mr-1" />
|
||||||
Discard
|
Discard
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 bg-white text-accent hover:bg-white/90 font-bold shadow-sm"
|
className="h-7 bg-white text-accent hover:bg-white/90 font-bold shadow-sm"
|
||||||
onClick={handleApplyLayout}
|
onClick={handleApplyLayout}
|
||||||
>
|
>
|
||||||
@@ -232,9 +237,8 @@ export default function Home() {
|
|||||||
className="p-1 text-muted-foreground hover:text-accent transition-colors cursor-crosshair"
|
className="p-1 text-muted-foreground hover:text-accent transition-colors cursor-crosshair"
|
||||||
title="Launch command (Cmd+K)"
|
title="Launch command (Cmd+K)"
|
||||||
aria-label="Launch command palette"
|
aria-label="Launch command palette"
|
||||||
>
|
></button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SpellbookDropdown />
|
<SpellbookDropdown />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
Sparkles,
|
Sparkles,
|
||||||
SplitSquareHorizontal,
|
SplitSquareHorizontal,
|
||||||
SplitSquareVertical,
|
SplitSquareVertical,
|
||||||
Save,
|
|
||||||
BookOpen,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Slider } from "./ui/slider";
|
import { Slider } from "./ui/slider";
|
||||||
@@ -23,17 +21,15 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { LayoutConfig } from "@/types/app";
|
import type { LayoutConfig } from "@/types/app";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SaveSpellbookDialog } from "./SaveSpellbookDialog";
|
|
||||||
|
|
||||||
export function LayoutControls() {
|
export function LayoutControls() {
|
||||||
const { state, applyPresetLayout, updateLayoutConfig, addWindow } = useGrimoire();
|
const { state, applyPresetLayout, updateLayoutConfig } = useGrimoire();
|
||||||
const { workspaces, activeWorkspaceId, layoutConfig } = state;
|
const { workspaces, activeWorkspaceId, layoutConfig } = state;
|
||||||
|
|
||||||
// Local state for immediate slider feedback (debounced persistence)
|
// Local state for immediate slider feedback (debounced persistence)
|
||||||
const [localSplitPercentage, setLocalSplitPercentage] = useState<
|
const [localSplitPercentage, setLocalSplitPercentage] = useState<
|
||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const activeWorkspace = workspaces[activeWorkspaceId];
|
const activeWorkspace = workspaces[activeWorkspaceId];
|
||||||
const windowCount = activeWorkspace?.windowIds.length || 0;
|
const windowCount = activeWorkspace?.windowIds.length || 0;
|
||||||
@@ -109,42 +105,19 @@ export function LayoutControls() {
|
|||||||
localSplitPercentage ?? layoutConfig.splitPercentage;
|
localSplitPercentage ?? layoutConfig.splitPercentage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DropdownMenu>
|
||||||
<SaveSpellbookDialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen} />
|
<DropdownMenuTrigger asChild>
|
||||||
<DropdownMenu>
|
<Button
|
||||||
<DropdownMenuTrigger asChild>
|
variant="ghost"
|
||||||
<Button
|
size="icon"
|
||||||
variant="ghost"
|
className="h-6 w-6"
|
||||||
size="icon"
|
aria-label="Layout settings"
|
||||||
className="h-6 w-6"
|
>
|
||||||
aria-label="Layout settings"
|
<SlidersHorizontal className="h-3 w-3 text-muted-foreground" />
|
||||||
>
|
</Button>
|
||||||
<SlidersHorizontal className="h-3 w-3 text-muted-foreground" />
|
</DropdownMenuTrigger>
|
||||||
</Button>
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
</DropdownMenuTrigger>
|
{/* Layouts Section */}
|
||||||
<DropdownMenuContent align="end" className="w-64">
|
|
||||||
{/* Spellbooks Section */}
|
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
|
||||||
Spellbooks
|
|
||||||
</div>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => setSaveDialogOpen(true)}
|
|
||||||
className="flex items-center gap-3 cursor-pointer"
|
|
||||||
>
|
|
||||||
<Save className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<div className="font-medium text-sm">Save Layout</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => addWindow("spellbooks", {})}
|
|
||||||
className="flex items-center gap-3 cursor-pointer"
|
|
||||||
>
|
|
||||||
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<div className="font-medium text-sm">Open Spellbooks</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
{/* Layouts Section */}
|
|
||||||
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
|
||||||
Layout Presets
|
Layout Presets
|
||||||
</div>
|
</div>
|
||||||
@@ -225,6 +198,5 @@ export function LayoutControls() {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function SaveSpellbookDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
existingSpellbook,
|
existingSpellbook,
|
||||||
}: SaveSpellbookDialogProps) {
|
}: SaveSpellbookDialogProps) {
|
||||||
const { state } = useGrimoire();
|
const { state, loadSpellbook } = useGrimoire();
|
||||||
const isUpdateMode = !!existingSpellbook;
|
const isUpdateMode = !!existingSpellbook;
|
||||||
|
|
||||||
const [title, setTitle] = useState(existingSpellbook?.title || "");
|
const [title, setTitle] = useState(existingSpellbook?.title || "");
|
||||||
@@ -123,6 +123,17 @@ export function SaveSpellbookDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Set as active spellbook
|
||||||
|
const parsedSpellbook = {
|
||||||
|
slug,
|
||||||
|
title,
|
||||||
|
description: description || undefined,
|
||||||
|
content: localSpellbook.content,
|
||||||
|
referencedSpells: [],
|
||||||
|
event: localSpellbook.event as any, // Event might not exist for locally-only spellbooks
|
||||||
|
};
|
||||||
|
loadSpellbook(parsedSpellbook);
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
// Reset form only if creating new
|
// Reset form only if creating new
|
||||||
if (!isUpdateMode) {
|
if (!isUpdateMode) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { BookHeart, ChevronDown, Plus, Save, Settings, X } from "lucide-react";
|
import { BookHeart, ChevronDown, Plus, Save, Settings, X } from "lucide-react";
|
||||||
import { useLiveQuery } from "dexie-react-hooks";
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
import db from "@/services/db";
|
import db from "@/services/db";
|
||||||
import { useGrimoire } from "@/core/state";
|
import { useGrimoire } from "@/core/state";
|
||||||
import { useReqTimeline } from "@/hooks/useReqTimeline";
|
import { useReqTimeline } from "@/hooks/useReqTimeline";
|
||||||
@@ -20,19 +21,32 @@ import { cn } from "@/lib/utils";
|
|||||||
import { SaveSpellbookDialog } from "./SaveSpellbookDialog";
|
import { SaveSpellbookDialog } from "./SaveSpellbookDialog";
|
||||||
|
|
||||||
export function SpellbookDropdown() {
|
export function SpellbookDropdown() {
|
||||||
const { state, loadSpellbook, addWindow, clearActiveSpellbook, applyTemporaryToPersistent, isTemporary } =
|
const {
|
||||||
useGrimoire();
|
state,
|
||||||
|
loadSpellbook,
|
||||||
|
addWindow,
|
||||||
|
clearActiveSpellbook,
|
||||||
|
applyTemporaryToPersistent,
|
||||||
|
isTemporary,
|
||||||
|
} = useGrimoire();
|
||||||
|
const location = useLocation();
|
||||||
const activeAccount = state.activeAccount;
|
const activeAccount = state.activeAccount;
|
||||||
const activeSpellbook = state.activeSpellbook;
|
const activeSpellbook = state.activeSpellbook;
|
||||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||||
const [dialogSpellbook, setDialogSpellbook] = useState<{
|
const [dialogSpellbook, setDialogSpellbook] = useState<
|
||||||
slug: string;
|
| {
|
||||||
title: string;
|
slug: string;
|
||||||
description?: string;
|
title: string;
|
||||||
workspaceIds?: string[];
|
description?: string;
|
||||||
localId?: string;
|
workspaceIds?: string[];
|
||||||
pubkey?: string;
|
localId?: string;
|
||||||
} | undefined>(undefined);
|
pubkey?: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
// Check if we're in preview mode
|
||||||
|
const isPreviewMode = location.pathname.startsWith("/preview/");
|
||||||
|
|
||||||
// 1. Load Local Data
|
// 1. Load Local Data
|
||||||
const localSpellbooks = useLiveQuery(() =>
|
const localSpellbooks = useLiveQuery(() =>
|
||||||
@@ -89,10 +103,11 @@ export function SpellbookDropdown() {
|
|||||||
// Check if active spellbook is in local library
|
// Check if active spellbook is in local library
|
||||||
const isActiveLocal = useMemo(() => {
|
const isActiveLocal = useMemo(() => {
|
||||||
if (!activeSpellbook) return false;
|
if (!activeSpellbook) return false;
|
||||||
return (localSpellbooks || []).some(s => s.slug === activeSpellbook.slug);
|
return (localSpellbooks || []).some((s) => s.slug === activeSpellbook.slug);
|
||||||
}, [activeSpellbook, localSpellbooks]);
|
}, [activeSpellbook, localSpellbooks]);
|
||||||
|
|
||||||
if (!activeAccount || (spellbooks.length === 0 && !activeSpellbook)) {
|
// Show dropdown if: in preview mode, has active account, or has active spellbook
|
||||||
|
if (!isPreviewMode && !activeAccount && !activeSpellbook) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +191,8 @@ export function SpellbookDropdown() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isActiveLocal && activeSpellbook.pubkey === activeAccount.pubkey ? (
|
{isActiveLocal && activeAccount &&
|
||||||
|
activeSpellbook.pubkey === activeAccount.pubkey ? (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={handleUpdateActive}
|
onClick={handleUpdateActive}
|
||||||
className={itemClass}
|
className={itemClass}
|
||||||
@@ -205,60 +221,71 @@ export function SpellbookDropdown() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Spellbooks Section */}
|
{/* Spellbooks Section - only show if user is logged in */}
|
||||||
<DropdownMenuLabel className="flex items-center justify-between py-1 px-2 text-[10px] uppercase tracking-wider text-muted-foreground font-bold">
|
{activeAccount && (
|
||||||
My Layouts
|
<>
|
||||||
</DropdownMenuLabel>
|
<DropdownMenuLabel className="flex items-center justify-between py-1 px-2 text-[10px] uppercase tracking-wider text-muted-foreground font-bold">
|
||||||
|
My Layouts
|
||||||
{spellbooks.length === 0 ? (
|
</DropdownMenuLabel>
|
||||||
<div className="px-2 py-4 text-center text-xs text-muted-foreground italic">
|
|
||||||
No layouts saved yet.
|
{spellbooks.length === 0 ? (
|
||||||
</div>
|
<div className="px-2 py-4 text-center text-xs text-muted-foreground italic">
|
||||||
) : (
|
No layouts saved yet.
|
||||||
spellbooks.map((sb) => {
|
</div>
|
||||||
const isActive = activeSpellbook?.slug === sb.slug;
|
) : (
|
||||||
return (
|
spellbooks.map((sb) => {
|
||||||
|
const isActive = activeSpellbook?.slug === sb.slug;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={sb.slug}
|
||||||
|
disabled={isActive}
|
||||||
|
onClick={() => handleApplySpellbook(sb)}
|
||||||
|
className={cn(itemClass, isActive && "bg-muted font-bold")}
|
||||||
|
>
|
||||||
|
<BookHeart
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 mr-2 text-muted-foreground",
|
||||||
|
isActive && "text-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row gap-0 min-w-0">
|
||||||
|
<span className="truncate font-medium text-sm">
|
||||||
|
{sb.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
{!activeSpellbook && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={sb.slug}
|
onClick={handleNewSpellbook}
|
||||||
disabled={isActive}
|
className={itemClass}
|
||||||
onClick={() => handleApplySpellbook(sb)}
|
|
||||||
className={cn(itemClass, isActive && "bg-muted font-bold")}
|
|
||||||
>
|
>
|
||||||
<BookHeart
|
<Save className="size-3.5 mr-2 text-muted-foreground" />
|
||||||
className={cn(
|
<span className="text-sm font-medium">Save Spellbook</span>
|
||||||
"size-3.5 mr-2 text-muted-foreground",
|
|
||||||
isActive && "text-foreground",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row gap-0 min-w-0">
|
|
||||||
<span className="truncate font-medium text-sm">
|
|
||||||
{sb.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
)}
|
||||||
})
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => addWindow("spellbooks", {})}
|
||||||
|
className={cn(itemClass, "text-xs opacity-70")}
|
||||||
|
>
|
||||||
|
<Settings className="size-3.5 mr-2 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium">Manage Library</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
{/* Show message for non-logged-in users in preview mode */}
|
||||||
|
{!activeAccount && isPreviewMode && (
|
||||||
{!activeSpellbook && (
|
<div className="px-2 py-4 text-center text-xs text-muted-foreground italic">
|
||||||
<DropdownMenuItem
|
Log in to save and manage layouts
|
||||||
onClick={handleNewSpellbook}
|
</div>
|
||||||
className={itemClass}
|
|
||||||
>
|
|
||||||
<Plus className="size-3.5 mr-2 text-muted-foreground" />
|
|
||||||
<span className="text-sm font-medium">Save current as Layout</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => addWindow("spellbooks", {})}
|
|
||||||
className={cn(itemClass, "text-xs opacity-70")}
|
|
||||||
>
|
|
||||||
<Settings className="size-3.5 mr-2 text-muted-foreground" />
|
|
||||||
Manage Library
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -83,6 +83,41 @@ function LayoutVisualizer({
|
|||||||
if (typeof node === "string") {
|
if (typeof node === "string") {
|
||||||
const window = windows[node];
|
const window = windows[node];
|
||||||
const appId = window?.appId || "unknown";
|
const appId = window?.appId || "unknown";
|
||||||
|
|
||||||
|
// For req windows, show kind badges if available
|
||||||
|
if (appId === "req" && window?.props?.filter?.kinds) {
|
||||||
|
const kinds = window.props.filter.kinds;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: "40px",
|
||||||
|
minWidth: "40px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "2px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1px solid hsl(var(--border))",
|
||||||
|
background: "hsl(var(--muted))",
|
||||||
|
padding: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{kinds.map((kind: number) => (
|
||||||
|
<KindBadge
|
||||||
|
key={kind}
|
||||||
|
kind={kind}
|
||||||
|
variant="compact"
|
||||||
|
className="text-[8px] h-4 px-1"
|
||||||
|
showName={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: show appId as text
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -112,6 +147,8 @@ function LayoutVisualizer({
|
|||||||
// Branch node - split
|
// Branch node - split
|
||||||
if (node && typeof node === "object" && "first" in node && "second" in node) {
|
if (node && typeof node === "object" && "first" in node && "second" in node) {
|
||||||
const isRow = node.direction === "row";
|
const isRow = node.direction === "row";
|
||||||
|
const splitPercentage = node.splitPercentage ?? 50; // Default to 50/50 if not specified
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -123,8 +160,12 @@ function LayoutVisualizer({
|
|||||||
minWidth: isRow ? "80px" : "40px",
|
minWidth: isRow ? "80px" : "40px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderLayout(node.first)}
|
<div style={{ flexGrow: splitPercentage, minHeight: "40px", minWidth: "40px", display: "flex" }}>
|
||||||
{renderLayout(node.second)}
|
{renderLayout(node.first)}
|
||||||
|
</div>
|
||||||
|
<div style={{ flexGrow: 100 - splitPercentage, minHeight: "40px", minWidth: "40px", display: "flex" }}>
|
||||||
|
{renderLayout(node.second)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -325,26 +366,16 @@ export function SpellbookDetailRenderer({ event }: { event: NostrEvent }) {
|
|||||||
|
|
||||||
<div className="grid gap-4 grid-cols-1">
|
<div className="grid gap-4 grid-cols-1">
|
||||||
{sortedWorkspaces.map((ws) => {
|
{sortedWorkspaces.map((ws) => {
|
||||||
const wsWindows = ws.windowIds.length;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={ws.id}
|
key={ws.id}
|
||||||
className="flex flex-col gap-3 p-4 rounded-xl border border-border bg-card/50"
|
className="flex flex-col gap-3 p-4 rounded-xl border border-border bg-card/50"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
{ws.label && (
|
||||||
<div className="flex flex-col gap-0.5">
|
<span className="font-bold text-sm">
|
||||||
<span className="text-sm font-mono text-muted-foreground">
|
{ws.label}
|
||||||
Tab {ws.number}
|
</span>
|
||||||
</span>
|
)}
|
||||||
<span className="font-bold">
|
|
||||||
{ws.label || "Untitled Tab"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 px-3 py-1 bg-muted rounded-full text-xs font-medium">
|
|
||||||
<ExternalLink className="size-3" />
|
|
||||||
{wsWindows} {wsWindows === 1 ? "window" : "windows"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ws.layout && (
|
{ws.layout && (
|
||||||
<LayoutVisualizer
|
<LayoutVisualizer
|
||||||
|
|||||||
Reference in New Issue
Block a user