Repository Public MIT
1"use client";2import { useActionState } from "react";3import { CopyButton } from "./copy-button";45export interface ActionState {6 error?: string;7 ok?: string;8 /** A credential shown exactly once, immediately after creation. */9 secret?: string;10}11export type FormAction = (prev: ActionState, data: FormData) => Promise<ActionState>;1213/** Progressive form wrapper: pending state, inline error/success, and one-time secret display. */14export function ActionForm({ action, children, submit, className = "", danger = false, confirm }: { action: FormAction; children?: React.ReactNode; submit: string; className?: string; danger?: boolean; confirm?: string }) {15 const [state, run, pending] = useActionState(action, {});16 return (17 <form18 action={run}19 className={className}20 onSubmit={(e) => {21 if (confirm && !window.confirm(confirm)) e.preventDefault();22 }}23 >24 {children}25 <div className="mt-4 flex flex-wrap items-center gap-3">26 <button type="submit" disabled={pending} className={`btn ${danger ? "btn-danger" : "btn-primary"}`}>27 {pending ? "Working…" : submit}28 </button>29 <p aria-live="polite" role={state.error ? "alert" : "status"} className={`min-w-0 text-sm ${state.error ? "text-danger" : "text-ok"}`}>30 {state.error ?? state.ok ?? ""}31 </p>32 </div>33 {state.secret && (34 <div className="mt-4 rounded-lg border border-warn/50 bg-warn/5 p-4" data-testid="one-time-secret">35 <p className="mb-2 text-sm font-medium text-text">Copy this token now. It will not be shown again.</p>36 <div className="flex items-center gap-2">37 <code className="min-w-0 flex-1 break-all rounded-md border border-line bg-code px-2.5 py-2 font-mono text-[12px]">{state.secret}</code>38 <CopyButton text={state.secret} />39 </div>40 <p className="mt-2 text-xs text-muted">41 Store it in your OS credential manager: <code className="font-mono">skills login</code> reads it from a hidden prompt and saves it to the Keychain. Never paste it into source files, prompts or chat.42 </p>43 </div>44 )}45 </form>46 );47}4849export function Field({ label, name, hint, children, ...rest }: { label: string; name: string; hint?: string; children?: React.ReactNode } & React.InputHTMLAttributes<HTMLInputElement>) {50 return (51 <div>52 <label htmlFor={`f-${name}`} className="label">{label}</label>53 {children ?? <input id={`f-${name}`} name={name} className="input" aria-describedby={hint ? `h-${name}` : undefined} {...rest} />}54 {hint && <p id={`h-${name}`} className="mt-1.5 text-xs text-muted">{hint}</p>}55 </div>56 );57}58