mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
feat(builder): Added calssic menubar
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"@omnia/scenario": "workspace:*",
|
||||
"@omnia/spatial": "workspace:*",
|
||||
"@radix-ui/react-dialog": "^1.1.19",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.20",
|
||||
"@radix-ui/react-separator": "^1.1.11",
|
||||
"@radix-ui/react-slot": "^1.3.0",
|
||||
"@radix-ui/react-tooltip": "^1.2.12",
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
SidebarProvider,
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
} from "@/components/ui/menubar";
|
||||
import { getConfigStatus, loadScenarioJson, saveScenario } from "@/app/actions";
|
||||
import type { Scenario } from "@omnia/scenario";
|
||||
import {
|
||||
@@ -16,9 +28,9 @@ import {
|
||||
Globe,
|
||||
MapPin,
|
||||
Users,
|
||||
Sparkles,
|
||||
Info,
|
||||
Eye,
|
||||
Pencil,
|
||||
} from "lucide-react";
|
||||
|
||||
// Import refactored builder components
|
||||
@@ -54,7 +66,6 @@ export default function BuilderPage() {
|
||||
const [availableScenarios, setAvailableScenarios] = useState<
|
||||
{ path: string; name: string; description: string }[]
|
||||
>([]);
|
||||
const [selectedTemplatePath, setSelectedTemplatePath] = useState("");
|
||||
|
||||
// Tabs: "metadata", "locations", "entities", "json"
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
@@ -430,229 +441,326 @@ export default function BuilderPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetScenario = () => {
|
||||
setScenarioId("");
|
||||
setName("My Custom Scenario");
|
||||
setDescription("A custom scenario template created via builder.");
|
||||
setStartTime("2026-07-06T12:00:00.000Z");
|
||||
setWorldAttributes([]);
|
||||
setSelectedLocIndex(0);
|
||||
setSelectedEntIndex(0);
|
||||
setStatusMessage({ text: "Scenario reset successfully.", type: "success" });
|
||||
};
|
||||
|
||||
const handleAddLocation = () => {
|
||||
const newId = generateUUID();
|
||||
setLocations([
|
||||
...locations,
|
||||
{ id: newId, attributes: [], connections: [] },
|
||||
]);
|
||||
setSelectedLocIndex(locations.length);
|
||||
setActiveTab("locations");
|
||||
};
|
||||
|
||||
const handleAddEntity = () => {
|
||||
const newId = generateUUID();
|
||||
setEntities([
|
||||
...entities,
|
||||
{
|
||||
id: newId,
|
||||
locationId: locationIds[0] || "",
|
||||
attributes: [],
|
||||
aliases: {},
|
||||
initialMemories: [],
|
||||
isAgent: true,
|
||||
},
|
||||
]);
|
||||
setSelectedEntIndex(entities.length);
|
||||
setActiveTab("entities");
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-1 min-h-0 w-full overflow-hidden bg-background">
|
||||
{/* Save Status Banner */}
|
||||
{statusMessage && (
|
||||
<div
|
||||
className={`fixed top-4 right-4 z-50 max-w-sm border p-4 shadow-lg animate-fade-in ${
|
||||
statusMessage.type === "success"
|
||||
? "bg-emerald-950/80 border-emerald-500 text-emerald-300"
|
||||
: statusMessage.type === "error"
|
||||
? "bg-destructive/10 border-destructive text-destructive"
|
||||
: "bg-secondary/90 border-border text-foreground"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="size-4 shrink-0 mt-0.5" />
|
||||
<div className="text-xs">{statusMessage.text}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Viewport-level Vertical Sidebar on the Left Side */}
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="h-full border-r border-border/30 bg-card shrink-0"
|
||||
<div className="flex flex-col flex-1 min-h-0 w-full bg-background overflow-hidden">
|
||||
{/* Save Status Banner */}
|
||||
{statusMessage && (
|
||||
<div
|
||||
className={`fixed top-14 right-4 z-50 max-w-sm border p-4 shadow-lg animate-fade-in ${
|
||||
statusMessage.type === "success"
|
||||
? "bg-emerald-950/80 border-emerald-500 text-emerald-300"
|
||||
: statusMessage.type === "error"
|
||||
? "bg-destructive/10 border-destructive text-destructive"
|
||||
: "bg-secondary/90 border-border text-foreground"
|
||||
}`}
|
||||
>
|
||||
<SidebarContent className="flex flex-col justify-between h-full bg-card p-6">
|
||||
<div className="flex flex-col gap-2 font-head">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground block mb-2">
|
||||
Configuration
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("metadata")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "metadata"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Globe className="size-3.5" />
|
||||
<span>World Metadata</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("locations")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "locations"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<MapPin className="size-3.5" />
|
||||
<span>Locations</span>
|
||||
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
|
||||
{locations.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("entities")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "entities"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Users className="size-3.5" />
|
||||
<span>Entities</span>
|
||||
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
|
||||
{entities.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("json")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "json"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Eye className="size-3.5" />
|
||||
<span>Live JSON Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Footer link */}
|
||||
<div className="border-t border-border/10 pt-3 flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="w-full py-1 text-center hover:text-foreground text-primary font-bold uppercase transition-colors cursor-pointer"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
|
||||
{/* Main Centered Content Pane on the Right */}
|
||||
<main className="flex-1 overflow-y-auto px-10 py-8 min-h-0 flex flex-col">
|
||||
<div className="mx-auto max-w-[1200px] w-full flex-1 flex flex-col min-h-0 gap-6">
|
||||
{/* Header block with Page Name and Load/Save Actions */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-border/20 pb-4 shrink-0">
|
||||
<div>
|
||||
<h1 className="text-headline-md text-primary flex items-center gap-2 font-head">
|
||||
<Sparkles className="size-6 text-primary" /> Scenario Builder
|
||||
</h1>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Define entities, spatial layouts, and initial metadata for
|
||||
custom Omnia scenarios.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 self-end md:self-auto">
|
||||
{/* Load Template dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-mono text-muted-foreground whitespace-nowrap">
|
||||
Load:
|
||||
</span>
|
||||
<select
|
||||
className="bg-card border border-border/30 px-2 py-1 text-xs outline-none h-8 w-44 rounded"
|
||||
value={selectedTemplatePath}
|
||||
onChange={(e) => {
|
||||
setSelectedTemplatePath(e.target.value);
|
||||
handleLoadTemplate(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Choose existing --</option>
|
||||
{availableScenarios.map((sc) => (
|
||||
<option key={sc.path} value={sc.path}>
|
||||
{sc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDownloadJson}
|
||||
className="h-8 text-xs flex gap-1.5 items-center cursor-pointer"
|
||||
>
|
||||
<FileJson className="size-3.5" /> Export JSON
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleSaveToServer}
|
||||
disabled={isSubmitting}
|
||||
className="h-8 text-xs flex gap-1.5 items-center cursor-pointer"
|
||||
>
|
||||
<Save className="size-3.5" /> Save to Server
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active configuration tab form */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{/* TAB 1: World Metadata & Attributes */}
|
||||
{activeTab === "metadata" && (
|
||||
<MetadataTab
|
||||
scenarioId={scenarioId}
|
||||
setScenarioId={setScenarioId}
|
||||
name={name}
|
||||
setName={setName}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
startTime={startTime}
|
||||
setStartTime={setStartTime}
|
||||
worldAttributes={worldAttributes}
|
||||
setWorldAttributes={setWorldAttributes}
|
||||
entityIds={entityIds}
|
||||
entities={entities}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 2: Locations & Spatial connections */}
|
||||
{activeTab === "locations" && (
|
||||
<LocationsTab
|
||||
locations={locations}
|
||||
setLocations={setLocations}
|
||||
entities={entities}
|
||||
locationIds={locationIds}
|
||||
entityIds={entityIds}
|
||||
selectedLocIndex={selectedLocIndex}
|
||||
setSelectedLocIndex={setSelectedLocIndex}
|
||||
generateUUID={generateUUID}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 3: Entities */}
|
||||
{activeTab === "entities" && (
|
||||
<EntitiesTab
|
||||
entities={entities}
|
||||
setEntities={setEntities}
|
||||
locations={locations}
|
||||
locationIds={locationIds}
|
||||
entityIds={entityIds}
|
||||
selectedEntIndex={selectedEntIndex}
|
||||
setSelectedEntIndex={setSelectedEntIndex}
|
||||
startTime={startTime}
|
||||
generateUUID={generateUUID}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 4: Live JSON Preview */}
|
||||
{activeTab === "json" && (
|
||||
<JsonTab
|
||||
compiledScenario={compiledScenario}
|
||||
onCopySuccess={() =>
|
||||
setStatusMessage({
|
||||
text: "JSON copied to clipboard!",
|
||||
type: "success",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="size-4 shrink-0 mt-0.5" />
|
||||
<div className="text-xs">{statusMessage.text}</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Menubar spanning full page width right below navbar */}
|
||||
<div className="w-full border-b border-border/20 bg-card py-1.5 px-4 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs font-head text-primary tracking-wider font-bold flex items-center gap-1.5">
|
||||
<Pencil className="size-4 text-primary" />
|
||||
</span>
|
||||
<Menubar className="border-none shadow-none bg-transparent h-7 p-0">
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="cursor-pointer">File</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger className="cursor-pointer">
|
||||
Load Template
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
{availableScenarios.length === 0 ? (
|
||||
<MenubarItem disabled>No templates</MenubarItem>
|
||||
) : (
|
||||
availableScenarios.map((sc) => (
|
||||
<MenubarItem
|
||||
key={sc.path}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
handleLoadTemplate(sc.path);
|
||||
}}
|
||||
>
|
||||
{sc.name}
|
||||
</MenubarItem>
|
||||
))
|
||||
)}
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem
|
||||
className="cursor-pointer"
|
||||
onClick={handleSaveToServer}
|
||||
disabled={isSubmitting || !scenarioId.trim()}
|
||||
>
|
||||
<Save className="size-4 mr-2 inline" /> Save to Server
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
className="cursor-pointer"
|
||||
onClick={handleDownloadJson}
|
||||
>
|
||||
<FileJson className="size-4 mr-2 inline" /> Export JSON
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="cursor-pointer">Edit</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem
|
||||
className="cursor-pointer"
|
||||
onClick={handleResetScenario}
|
||||
>
|
||||
Reset Scenario
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem
|
||||
className="cursor-pointer"
|
||||
onClick={handleAddLocation}
|
||||
>
|
||||
Add New Location
|
||||
</MenubarItem>
|
||||
<MenubarItem
|
||||
className="cursor-pointer"
|
||||
onClick={handleAddEntity}
|
||||
>
|
||||
Add New Entity
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger className="cursor-pointer">View</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarRadioGroup
|
||||
value={activeTab}
|
||||
onValueChange={(val) => setActiveTab(val as typeof activeTab)}
|
||||
>
|
||||
<MenubarRadioItem value="metadata" className="cursor-pointer">
|
||||
World Metadata
|
||||
</MenubarRadioItem>
|
||||
<MenubarRadioItem
|
||||
value="locations"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Locations
|
||||
</MenubarRadioItem>
|
||||
<MenubarRadioItem value="entities" className="cursor-pointer">
|
||||
Entities
|
||||
</MenubarRadioItem>
|
||||
<MenubarRadioItem value="json" className="cursor-pointer">
|
||||
Live JSON Preview
|
||||
</MenubarRadioItem>
|
||||
</MenubarRadioGroup>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</Menubar>
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-muted-foreground">
|
||||
{scenarioId ? `ID: ${scenarioId}` : "Unsaved Scenario"}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
|
||||
<SidebarProvider className="flex-1 min-h-0">
|
||||
<div className="flex flex-1 min-h-0 w-full overflow-hidden">
|
||||
{/* Viewport-level Vertical Sidebar on the Left Side */}
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="h-full border-r border-border/30 bg-card shrink-0"
|
||||
>
|
||||
<SidebarContent className="flex flex-col justify-between h-full bg-card p-6">
|
||||
<div className="flex flex-col gap-2 font-head">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-muted-foreground block mb-2">
|
||||
Configuration
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("metadata")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "metadata"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Globe className="size-3.5" />
|
||||
<span>World Metadata</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("locations")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "locations"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<MapPin className="size-3.5" />
|
||||
<span>Locations</span>
|
||||
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
|
||||
{locations.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("entities")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "entities"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Users className="size-3.5" />
|
||||
<span>Entities</span>
|
||||
<span className="ml-auto text-[10px] font-mono border border-muted-foreground/20 bg-muted/10 px-1 rounded">
|
||||
{entities.length}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab("json")}
|
||||
className={`w-full text-left px-4 py-2.5 text-xs font-mono font-bold tracking-wide border transition-all flex items-center gap-2 cursor-pointer ${
|
||||
activeTab === "json"
|
||||
? "border-primary bg-primary/10 text-primary shadow-[2px_2px_0_0_var(--primary)] font-bold"
|
||||
: "border-border/30 hover:bg-secondary text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<Eye className="size-3.5" />
|
||||
<span>Live JSON Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Footer link */}
|
||||
<div className="border-t border-border/10 pt-3 flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="w-full py-1 text-center hover:text-foreground text-primary font-bold uppercase transition-colors cursor-pointer"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
|
||||
{/* Main Centered Content Pane on the Right */}
|
||||
<main className="flex-1 overflow-y-auto px-10 py-8 min-h-0 flex flex-col">
|
||||
<div className="mx-auto max-w-[1200px] w-full flex-1 flex flex-col min-h-0 gap-6">
|
||||
{/* Header block with Page Name */}
|
||||
<div className="shrink-0">
|
||||
<h1 className="text-headline-md text-primary flex items-center gap-2 font-head">
|
||||
Scenario Builder
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Active configuration tab form */}
|
||||
<div className="flex-1 min-h-0">
|
||||
{/* TAB 1: World Metadata & Attributes */}
|
||||
{activeTab === "metadata" && (
|
||||
<MetadataTab
|
||||
scenarioId={scenarioId}
|
||||
setScenarioId={setScenarioId}
|
||||
name={name}
|
||||
setName={setName}
|
||||
description={description}
|
||||
setDescription={setDescription}
|
||||
startTime={startTime}
|
||||
setStartTime={setStartTime}
|
||||
worldAttributes={worldAttributes}
|
||||
setWorldAttributes={setWorldAttributes}
|
||||
entityIds={entityIds}
|
||||
entities={entities}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 2: Locations & Spatial connections */}
|
||||
{activeTab === "locations" && (
|
||||
<LocationsTab
|
||||
locations={locations}
|
||||
setLocations={setLocations}
|
||||
entities={entities}
|
||||
locationIds={locationIds}
|
||||
entityIds={entityIds}
|
||||
selectedLocIndex={selectedLocIndex}
|
||||
setSelectedLocIndex={setSelectedLocIndex}
|
||||
generateUUID={generateUUID}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 3: Entities */}
|
||||
{activeTab === "entities" && (
|
||||
<EntitiesTab
|
||||
entities={entities}
|
||||
setEntities={setEntities}
|
||||
locations={locations}
|
||||
locationIds={locationIds}
|
||||
entityIds={entityIds}
|
||||
selectedEntIndex={selectedEntIndex}
|
||||
setSelectedEntIndex={setSelectedEntIndex}
|
||||
startTime={startTime}
|
||||
generateUUID={generateUUID}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TAB 4: Live JSON Preview */}
|
||||
{activeTab === "json" && (
|
||||
<JsonTab
|
||||
compiledScenario={compiledScenario}
|
||||
onCopySuccess={() =>
|
||||
setStatusMessage({
|
||||
text: "JSON copied to clipboard!",
|
||||
type: "success",
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@custom-variant data-horizontal (&[data-orientation="horizontal"]);
|
||||
@custom-variant data-vertical (&[data-orientation="vertical"]);
|
||||
@custom-variant data-popup-open (&[data-state="open"]);
|
||||
@custom-variant data-highlighted (&[data-highlighted]);
|
||||
|
||||
@theme inline {
|
||||
--font-head: var(--font-head);
|
||||
|
||||
@@ -24,7 +24,6 @@ interface MetadataTabProps {
|
||||
|
||||
export function MetadataTab({
|
||||
scenarioId,
|
||||
setScenarioId,
|
||||
name,
|
||||
setName,
|
||||
description,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useMemo, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ZoomIn, ZoomOut, RotateCcw, Map as MapIcon } from "lucide-react";
|
||||
import { ZoomIn, ZoomOut, RotateCcw } from "lucide-react";
|
||||
import type { LocationData } from "./types";
|
||||
|
||||
interface BoxNode {
|
||||
@@ -203,7 +203,20 @@ export function WorldMap({
|
||||
|
||||
// Connection lines computation
|
||||
const connectionLines = useMemo(() => {
|
||||
const lines: any[] = [];
|
||||
const lines: {
|
||||
id: string;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
portalName?: string;
|
||||
portalState?: string;
|
||||
vision?: number;
|
||||
sound?: number;
|
||||
bidirectional?: boolean;
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
}[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
locations.forEach((loc) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Combobox as ComboboxPrimitive } from "@base-ui/react";
|
||||
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -68,13 +67,16 @@ function ComboboxInput({
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
{showTrigger && (
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
render={<ComboboxTrigger />}
|
||||
data-slot="input-group-button"
|
||||
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
||||
disabled={disabled}
|
||||
<ComboboxTrigger
|
||||
render={
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
data-slot="input-group-button"
|
||||
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showClear && <ComboboxClear disabled={disabled} />}
|
||||
@@ -252,7 +254,7 @@ function ComboboxChip({
|
||||
{children}
|
||||
{showRemove && (
|
||||
<ComboboxPrimitive.ChipRemove
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
||||
className="-ml-1 opacity-50 hover:opacity-100"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
|
||||
200
apps/gui/src/components/ui/dropdown-menu.tsx
Normal file
200
apps/gui/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
292
apps/gui/src/components/ui/menubar.tsx
Normal file
292
apps/gui/src/components/ui/menubar.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
|
||||
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar";
|
||||
import { CheckIcon, ChevronRight } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Menubar({ className, ...props }: MenubarPrimitive.Props) {
|
||||
return (
|
||||
<MenubarPrimitive
|
||||
data-slot="menubar"
|
||||
className={cn(
|
||||
"flex h-8 items-center gap-0.5 rounded border-2 bg-background p-[3px] shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarMenu({ ...props }: MenuPrimitive.Root.Props) {
|
||||
return <MenuPrimitive.Root data-slot="menubar-menu" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarGroup({ ...props }: MenuPrimitive.Group.Props) {
|
||||
return <MenuPrimitive.Group data-slot="menubar-group" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarPortal({ ...props }: MenuPrimitive.Portal.Props) {
|
||||
return <MenuPrimitive.Portal data-slot="menubar-portal" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Trigger
|
||||
data-slot="menubar-trigger"
|
||||
className={cn(
|
||||
"flex items-center rounded-sm px-1.5 py-[2px] text-sm font-medium outline-none select-none hover:bg-primary/15 hover:text-primary aria-expanded:bg-primary/15 aria-expanded:text-primary cursor-pointer",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props & {
|
||||
align?: MenuPrimitive.Positioner.Props["align"];
|
||||
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
|
||||
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="menubar-content"
|
||||
className={cn(
|
||||
"min-w-36 rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: MenuPrimitive.Item.Props & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Item
|
||||
data-slot="menubar-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/menubar-item gap-1.5 rounded-sm px-2 py-1.5 text-sm focus:bg-primary/15 focus:text-primary data-highlighted:bg-primary/15 data-highlighted:text-primary not-data-[variant=destructive]:focus:**:text-primary not-data-[variant=destructive]:data-highlighted:**:text-primary data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 data-[variant=destructive]:data-highlighted:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.CheckboxItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="menubar-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-2 pl-7 text-sm outline-none select-none focus:bg-primary/15 focus:text-primary data-highlighted:bg-primary/15 data-highlighted:text-primary focus:**:text-primary data-highlighted:**:text-primary data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<MenuPrimitive.CheckboxItemIndicator>
|
||||
<CheckIcon />
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {
|
||||
return (
|
||||
<MenuPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.RadioItem.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="menubar-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-1.5 rounded-sm py-1.5 pr-2 pl-7 text-sm outline-none select-none focus:bg-primary/15 focus:text-primary data-highlighted:bg-primary/15 data-highlighted:text-primary focus:**:text-primary data-highlighted:**:text-primary data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-1.5 flex size-4 items-center justify-center [&_svg:not([class*='size-'])]:size-4">
|
||||
<MenuPrimitive.RadioItemIndicator>
|
||||
<CheckIcon />
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: MenuPrimitive.GroupLabel.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.GroupLabel
|
||||
data-slot="menubar-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-1.5 py-1 text-sm font-medium data-inset:pl-7",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: MenuPrimitive.Separator.Props) {
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="menubar-separator"
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="menubar-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/menubar-item:text-primary group-data-highlighted/menubar-item:text-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Submenu components using SubmenuRoot and SubmenuTrigger
|
||||
function MenubarSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {
|
||||
return <MenuPrimitive.SubmenuRoot data-slot="menubar-sub" {...props} />;
|
||||
}
|
||||
|
||||
function MenubarSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: MenuPrimitive.SubmenuTrigger.Props & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.SubmenuTrigger
|
||||
data-slot="menubar-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center gap-1.5 rounded-sm px-2 py-1.5 text-sm focus:bg-primary/15 focus:text-primary data-highlighted:bg-primary/15 data-highlighted:text-primary data-inset:pl-7 data-open:bg-primary/15 data-open:text-primary data-highlighted:text-primary [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto size-4" />
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function MenubarSubContent({
|
||||
className,
|
||||
align = "start",
|
||||
alignOffset = -4,
|
||||
sideOffset = 8,
|
||||
...props
|
||||
}: MenuPrimitive.Popup.Props & {
|
||||
align?: MenuPrimitive.Positioner.Props["align"];
|
||||
alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
|
||||
sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
|
||||
}) {
|
||||
return (
|
||||
<MenuPrimitive.Portal>
|
||||
<MenuPrimitive.Positioner
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
>
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="menubar-sub-content"
|
||||
className={cn(
|
||||
"min-w-32 rounded border-2 bg-popover p-1 text-popover-foreground shadow-md duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
</MenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarPortal,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarItem,
|
||||
MenubarShortcut,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSub,
|
||||
MenubarSubTrigger,
|
||||
MenubarSubContent,
|
||||
};
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -342,6 +342,9 @@ importers:
|
||||
"@radix-ui/react-dialog":
|
||||
specifier: ^1.1.19
|
||||
version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
"@radix-ui/react-dropdown-menu":
|
||||
specifier: ^2.1.20
|
||||
version: 2.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
"@radix-ui/react-separator":
|
||||
specifier: ^1.1.11
|
||||
version: 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
|
||||
Reference in New Issue
Block a user