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
265 changes: 90 additions & 175 deletions apps/mapgen-studio/src/App.tsx

Large diffs are not rendered by default.

128 changes: 70 additions & 58 deletions apps/mapgen-studio/src/features/presets/PresetDialogs.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,69 @@
import { useEffect, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
} from "../../ui/components/ui";
} from "../../components/ui";

/**
* Preset dialogs — error / save / confirm flows, migrated from the legacy
* hand-rolled AlertDialog to the token-driven shadcn Dialog. Open/confirm/cancel
* semantics are preserved: `onOpenChange` keeps the same open-state contract,
* Cancel/Close use a DialogClose, and the confirm action invokes the caller's
* callback. The `lightMode` prop is retained for call-site compatibility but no
* longer drives styling (the Dialog is token-driven via the `.dark` class).
*/

export type PresetErrorDialogProps = Readonly<{
open: boolean;
title: string;
message: string;
details?: ReadonlyArray<string>;
lightMode: boolean;
lightMode?: boolean;
onOpenChange: (open: boolean) => void;
}>;

export function PresetErrorDialog(props: PresetErrorDialogProps) {
const { open, title, message, details, lightMode, onOpenChange } = props;
const { open, title, message, details, onOpenChange } = props;
return (
<AlertDialog open={open} onOpenChange={onOpenChange} lightMode={lightMode}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{message}</AlertDialogDescription>
</AlertDialogHeader>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{message}</DialogDescription>
</DialogHeader>
{details && details.length > 0 ? (
<pre className="mt-3 max-h-48 overflow-auto rounded bg-black/5 p-2 text-[10px] leading-relaxed">
<pre className="mt-1 max-h-48 overflow-auto rounded bg-surface-sunken p-2 text-label leading-relaxed text-muted-foreground">
{details.join("\n")}
</pre>
) : null}
<AlertDialogFooter>
<AlertDialogAction>Close</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<DialogFooter>
<DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

export type PresetSaveDialogProps = Readonly<{
open: boolean;
lightMode: boolean;
lightMode?: boolean;
initialLabel?: string;
initialDescription?: string;
onCancel: () => void;
onConfirm: (args: { label: string; description?: string }) => void;
}>;

export function PresetSaveDialog(props: PresetSaveDialogProps) {
const { open, lightMode, initialLabel, initialDescription, onCancel, onConfirm } = props;
const { open, initialLabel, initialDescription, onCancel, onConfirm } = props;
const [label, setLabel] = useState(initialLabel ?? "");
const [description, setDescription] = useState(initialDescription ?? "");

Expand All @@ -66,53 +77,52 @@ export function PresetSaveDialog(props: PresetSaveDialogProps) {
const canSave = label.trim().length > 0;

return (
<AlertDialog open={open} onOpenChange={(next) => (!next ? onCancel() : undefined)} lightMode={lightMode}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Save Config</AlertDialogTitle>
<AlertDialogDescription>Choose a name for this config.</AlertDialogDescription>
</AlertDialogHeader>
<div className="mt-3 space-y-2">
<Dialog open={open} onOpenChange={(next) => (!next ? onCancel() : undefined)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Save Config</DialogTitle>
<DialogDescription>Choose a name for this config.</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<div>
<div className="mb-1 text-[10px] uppercase tracking-wide">Label</div>
<div className="mb-1 text-label uppercase tracking-wide text-muted-foreground">Label</div>
<Input
lightMode={lightMode}
value={label}
onChange={(e) => setLabel(e.target.value)}
placeholder="Config name"
/>
</div>
<div>
<div className="mb-1 text-[10px] uppercase tracking-wide">Description (optional)</div>
<div className="mb-1 text-label uppercase tracking-wide text-muted-foreground">Description (optional)</div>
<Input
lightMode={lightMode}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Short description"
/>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
disabled={!canSave}
onClick={() => {
if (!canSave) return;
onConfirm({ label: label.trim(), description: description.trim() || undefined });
}}
>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}


export type PresetConfirmDialogProps = Readonly<{
open: boolean;
lightMode: boolean;
lightMode?: boolean;
title: string;
message: string;
confirmLabel: string;
Expand All @@ -121,19 +131,21 @@ export type PresetConfirmDialogProps = Readonly<{
}>;

export function PresetConfirmDialog(props: PresetConfirmDialogProps) {
const { open, lightMode, title, message, confirmLabel, onCancel, onConfirm } = props;
const { open, title, message, confirmLabel, onCancel, onConfirm } = props;
return (
<AlertDialog open={open} onOpenChange={(next) => (!next ? onCancel() : undefined)} lightMode={lightMode}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{message}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>{confirmLabel}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Dialog open={open} onOpenChange={(next) => (!next ? onCancel() : undefined)}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{message}</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline" onClick={onCancel}>Cancel</Button>
</DialogClose>
<Button onClick={onConfirm}>{confirmLabel}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
69 changes: 25 additions & 44 deletions apps/mapgen-studio/src/ui/components/AppBrand.tsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,76 @@
import React, { useState } from 'react';
import { ExternalLink, Github, User } from 'lucide-react';

/**
* AppBrand — the identity pill in the header, with a hover info card.
*
* Reskinned onto the design tokens: the pill and its hover card float over the
* deck.gl map, so they ride the `popover` tier with `backdrop-blur`; the theme
* follows the single `.dark` class rather than the legacy `isLightMode` hex
* ternaries. The `isLightMode` prop is retained for call-site compatibility
* during the shell migration but no longer drives styling.
*/
interface AppBrandProps {
isLightMode: boolean;
isLightMode?: boolean;
}
export const AppBrand: React.FC<AppBrandProps> = ({ isLightMode }) => {

export const AppBrand: React.FC<AppBrandProps> = () => {
const [isHovered, setIsHovered] = useState(false);
// ============================================================================
// Styles
// ============================================================================
const containerClass = isLightMode ?
'bg-white/90 border-gray-200' :
'bg-[#16161d]/90 border-[#26262e]';
const textClass = isLightMode ? 'text-[#1f2933]' : 'text-[#e2e2e9]';
const mutedClass = isLightMode ? 'text-[#6b7280]' : 'text-[#6a6a7c]';
const linkClass = isLightMode ?
'text-[#4b5563] hover:text-[#1f2933]' :
'text-[#7a7a8c] hover:text-[#e2e2e9]';
const dividerClass = isLightMode ? 'border-gray-200' : 'border-[#26262e]';
// ============================================================================
// Render
// ============================================================================
return (
<div
className="relative h-10"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}>

{/* Main Pill */}
<div
className={`
h-full inline-flex items-center gap-2 px-3 rounded-lg border
backdrop-blur-sm cursor-default
${containerClass}
`}>

<span
className={`font-semibold text-[13px] tracking-tight ${textClass}`}>

<div className="h-full inline-flex items-center gap-2 px-3 rounded-lg border border-border bg-popover/90 backdrop-blur-sm cursor-default">
<span className="font-semibold text-[13px] tracking-tight text-foreground">
MapGen Studio
</span>
<span className={`text-[10px] font-mono ${mutedClass}`}>v0.1</span>
<span className="text-label font-mono text-muted-foreground">v0.1</span>
</div>

{/* Hover Dropdown */}
{isHovered &&
<div
className={`
absolute top-full left-0 mt-2
min-w-[200px] p-3 rounded-lg border
backdrop-blur-sm shadow-lg z-50
${containerClass}
`}>

<div className="absolute top-full left-0 mt-2 min-w-[200px] p-3 rounded-lg border border-border bg-popover/95 backdrop-blur-sm shadow-lg z-50">
{/* Description */}
<p className={`text-[11px] leading-relaxed ${mutedClass} mb-3`}>
<p className="text-data leading-relaxed text-muted-foreground mb-3">
Procedural map generation toolkit for game developers.
</p>

<div className={`border-t ${dividerClass} mb-3`} />
<div className="border-t border-border-subtle mb-3" />

{/* Links */}
<div className="flex flex-col gap-2">
<a
href="#"
className={`flex items-center gap-2 text-[11px] font-medium transition-colors ${linkClass}`}>
className="flex items-center gap-2 text-data font-medium text-muted-foreground transition-colors hover:text-foreground">

<User className="w-3.5 h-3.5" />
<span>Author Name</span>
</a>
<a
href="#"
className={`flex items-center gap-2 text-[11px] font-medium transition-colors ${linkClass}`}>
className="flex items-center gap-2 text-data font-medium text-muted-foreground transition-colors hover:text-foreground">

<Github className="w-3.5 h-3.5" />
<span>View on GitHub</span>
</a>
<a
href="#"
className={`flex items-center gap-2 text-[11px] font-medium transition-colors ${linkClass}`}>
className="flex items-center gap-2 text-data font-medium text-muted-foreground transition-colors hover:text-foreground">

<ExternalLink className="w-3.5 h-3.5" />
<span>Documentation</span>
</a>
</div>

<div className={`border-t ${dividerClass} my-3`} />
<div className="border-t border-border-subtle my-3" />

{/* Footer */}
<p className={`text-[10px] ${mutedClass}`}>© 2024 • MIT License</p>
<p className="text-label text-muted-foreground">© 2024 • MIT License</p>
</div>
}
</div>);

};
};
Loading