Field editor: palette, select/move/resize/rotate, inspector, optimistic sync (#11) (#30)
Build image / build-and-push (push) Successful in 13s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #30.
This commit is contained in:
2026-07-19 01:01:38 +00:00
committed by steve
parent 2119f1a376
commit b79bfcf7a9
13 changed files with 989 additions and 74 deletions
+3
View File
@@ -1,4 +1,5 @@
import { Link, Outlet, useNavigate } from '@tanstack/react-router'
import { Toaster } from '@/components/ui/toast'
import { useLogout, useMe } from '@/lib/auth'
const navLinks = [
@@ -81,6 +82,8 @@ export function AppShell() {
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-6">
<Outlet />
</main>
<Toaster />
</div>
)
}
+65
View File
@@ -0,0 +1,65 @@
import { useEffect } from 'react'
import { create } from 'zustand'
import { cn } from '@/lib/cn'
type ToastTone = 'info' | 'error'
interface Toast {
id: number
message: string
tone: ToastTone
}
interface ToastState {
toasts: Toast[]
push: (message: string, tone?: ToastTone) => void
dismiss: (id: number) => void
}
let nextId = 1
export const useToastStore = create<ToastState>((set) => ({
toasts: [],
push: (message, tone = 'info') => set((s) => ({ toasts: [...s.toasts, { id: nextId++, message, tone }] })),
dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
}))
/** Fire a toast from anywhere (including non-component code). */
export const toast = {
info: (m: string) => useToastStore.getState().push(m, 'info'),
error: (m: string) => useToastStore.getState().push(m, 'error'),
}
// Param is `item`, not `toast`, so it doesn't shadow the module's `toast` export.
function ToastItem({ item }: { item: Toast }) {
const dismiss = useToastStore((s) => s.dismiss)
useEffect(() => {
const t = setTimeout(() => dismiss(item.id), 4000)
return () => clearTimeout(t)
}, [item.id, dismiss])
return (
<div
role={item.tone === 'error' ? 'alert' : 'status'}
className={cn(
'pointer-events-auto rounded-md border px-3 py-2 text-sm shadow-md',
item.tone === 'error'
? 'border-red-500/40 bg-red-500/10 text-red-700 dark:text-red-300'
: 'border-border bg-surface text-fg',
)}
>
{item.message}
</div>
)
}
/** Fixed stack of active toasts. Mount once near the app root. */
export function Toaster() {
const toasts = useToastStore((s) => s.toasts)
if (toasts.length === 0) return null
return (
<div className="pointer-events-none fixed bottom-4 left-1/2 z-[60] flex -translate-x-1/2 flex-col items-center gap-2">
{toasts.map((t) => (
<ToastItem key={t.id} item={t} />
))}
</div>
)
}