SPB Git

Repository Public MIT

action-form.tsx 2.6 KB 58 lines tsx

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
Files main
Menu

Theme