init: arcadia-admin — admin webapp for arcadia-core, cloned from vibespace
Initial commit. Spun up via the docs/STARTER.md recipe: cp from vibespace, reset git, rename package, set brand to "Arcadia Admin" with Shield icon in app/lib/identity.ts. Inherits the full Crema sibling-lib wiring including @crema/arcadia-client (typed HTTP + Phoenix Channels realtime against arcadia-core) and @crema/arcadia-auth-ui (login/signup/password-reset/2FA forms). The /login route already renders <LoginForm>; <ArcadiaProvider> in app/root.tsx reads VITE_ARCADIA_URL (default localhost:4000) and VITE_ARCADIA_TENANT (default "default"). CLAUDE.md and README rewritten to frame this as the admin app for arcadia-core. docs/STARTER.md removed — arcadia-admin is a leaf consumer, not a downstream starter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
155
app/components/scripts-dialog.tsx
Normal file
155
app/components/scripts-dialog.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
// Run a saved script from public/scripts/ or paste DSL ad-hoc.
|
||||
// Triggered by the Play icon in the appbar or Cmd/Ctrl+Shift+P.
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Play } from "lucide-react"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog"
|
||||
import { runScript, runScriptText } from "@crema/action-bus"
|
||||
|
||||
const KNOWN_SCRIPTS = [
|
||||
{ name: "demo-tour", description: "Tour the rail" },
|
||||
{ name: "demo-search", description: "Focus and fill search" },
|
||||
]
|
||||
|
||||
export function useScriptsHotkey(open: () => void) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const mod = e.metaKey || e.ctrlKey
|
||||
if (mod && e.shiftKey && (e.key === "p" || e.key === "P")) {
|
||||
e.preventDefault()
|
||||
open()
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKey)
|
||||
return () => window.removeEventListener("keydown", onKey)
|
||||
}, [open])
|
||||
}
|
||||
|
||||
export function ScriptsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (v: boolean) => void
|
||||
}) {
|
||||
const [text, setText] = useState("")
|
||||
const [status, setStatus] = useState<string | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const runByName = async (name: string) => {
|
||||
setBusy(true)
|
||||
setStatus(`Running ${name}…`)
|
||||
try {
|
||||
onOpenChange(false)
|
||||
await runScript(name)
|
||||
setStatus(`Ran ${name}.`)
|
||||
} catch (e) {
|
||||
setStatus(`Error: ${e instanceof Error ? e.message : String(e)}`)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const runText = async () => {
|
||||
if (!text.trim()) return
|
||||
setBusy(true)
|
||||
setStatus("Running…")
|
||||
try {
|
||||
onOpenChange(false)
|
||||
await runScriptText(text)
|
||||
setStatus("Done.")
|
||||
} catch (e) {
|
||||
setStatus(`Error: ${e instanceof Error ? e.message : String(e)}`)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run a script</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a saved script or paste DSL. Cmd/Ctrl + Shift + P toggles this.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="mb-1.5 text-xs font-medium text-muted-foreground">
|
||||
Saved
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
{KNOWN_SCRIPTS.map((s) => (
|
||||
<button
|
||||
key={s.name}
|
||||
type="button"
|
||||
data-action={`run-script-${s.name}`}
|
||||
onClick={() => runByName(s.name)}
|
||||
disabled={busy}
|
||||
className="flex items-center justify-between rounded-md border px-3 py-2 text-left text-sm transition-colors hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
<span>
|
||||
<span className="font-mono text-xs">{s.name}</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{s.description}
|
||||
</span>
|
||||
</span>
|
||||
<Play className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-1.5 text-xs font-medium text-muted-foreground">
|
||||
Paste DSL
|
||||
</div>
|
||||
<textarea
|
||||
data-action="scripts-dsl"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder={"navigate /resources\nclick nav-resources"}
|
||||
spellCheck={false}
|
||||
rows={6}
|
||||
className="w-full rounded-md border bg-background p-2 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className="rounded-md border bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
data-action="scripts-cancel"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={busy}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
data-action="scripts-run"
|
||||
onClick={runText}
|
||||
disabled={busy || !text.trim()}
|
||||
>
|
||||
<Play className="size-4" /> Run
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user