handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs
at trunk 148 lines 3.9 kB view raw
1import { type Component, createMemo, createSignal, For, type JSX } from 'solid-js'; 2 3type EmptyObjectKeys<T> = { 4 [K in keyof T]: T[K] extends Record<string, never> ? K : never; 5}[keyof T]; 6 7export type WizardConstraints = Record<string, Record<string, any>>; 8 9export interface WizardStepProps<TConstraints extends WizardConstraints, TStep extends keyof TConstraints> { 10 data: TConstraints[TStep]; 11 isActive: () => boolean; 12 onNext: <TNext extends keyof TConstraints>(step: TNext, data: TConstraints[TNext]) => void; 13 onPrevious: () => void; 14} 15 16export interface WizardProps<TConstraints extends WizardConstraints> { 17 initialStep: EmptyObjectKeys<TConstraints>; 18 components: { 19 [TStep in keyof TConstraints]: Component<WizardStepProps<TConstraints, TStep>>; 20 }; 21 onStepChange?: (step: number) => void; 22} 23 24interface HistoryEntry<TConstraints extends WizardConstraints> { 25 step: keyof TConstraints; 26 data: TConstraints[keyof TConstraints]; 27} 28 29export const Wizard = <TConstraints extends WizardConstraints>(props: WizardProps<TConstraints>) => { 30 const components = props.components; 31 const onStepChange = props.onStepChange; 32 33 const [history, setHistory] = createSignal<HistoryEntry<TConstraints>[]>([ 34 // @ts-expect-error 35 { step: props.initialStep, data: {} }, 36 ]); 37 38 const current = createMemo(() => { 39 return history().length - 1; 40 }); 41 42 const handleNext = <TNext extends keyof TConstraints>(step: TNext, data: TConstraints[TNext]) => { 43 const entries = history(); 44 45 setHistory([...entries, { step, data }]); 46 onStepChange?.(entries.length + 1); 47 }; 48 49 const handleBack = () => { 50 const entries = history(); 51 52 if (entries.length > 1) { 53 setHistory(entries.slice(0, -1)); 54 onStepChange?.(entries.length - 1); 55 } 56 }; 57 58 return ( 59 <div class="pb-8"> 60 <For each={history()}> 61 {({ step, data }, index) => { 62 const Component = components[step]; 63 64 const isActive = createMemo(() => current() === index()); 65 66 return ( 67 <fieldset 68 disabled={!isActive()} 69 class={`flex min-w-0 gap-4 px-4` + (!isActive() ? ` opacity-50` : ``)} 70 > 71 <div class="flex flex-col items-center gap-1 pt-4"> 72 <div class="grid h-6 w-6 place-items-center rounded-full bg-gray-200 py-1 text-center text-sm font-medium leading-none text-black"> 73 {'' + (index() + 1)} 74 </div> 75 <div hidden={isActive()} class="-mb-3 grow border-l border-gray-400"></div> 76 </div> 77 78 <Component data={data} isActive={isActive} onNext={handleNext} onPrevious={handleBack} /> 79 </fieldset> 80 ); 81 }} 82 </For> 83 </div> 84 ); 85}; 86 87export interface StageProps { 88 title: string; 89 disabled?: boolean; 90 onSubmit?: JSX.EventHandler<HTMLFormElement, SubmitEvent>; 91 children: JSX.Element; 92} 93 94export const Stage = (props: StageProps) => { 95 const onSubmit = props.onSubmit; 96 97 return ( 98 <form 99 onSubmit={(ev) => { 100 ev.preventDefault(); 101 onSubmit?.(ev); 102 }} 103 class="flex min-w-0 grow flex-col py-4" 104 > 105 <h3 class="mb-[1.125rem] mt-0.5 text-sm font-semibold">{props.title}</h3> 106 <fieldset 107 disabled={props.disabled} 108 class={`flex min-w-0 flex-col gap-6` + (props.disabled ? ` opacity-50` : ``)} 109 > 110 {props.children} 111 </fieldset> 112 </form> 113 ); 114}; 115 116export interface StageActionsProps { 117 hidden?: boolean; 118 children: JSX.Element; 119} 120 121export interface StageActionsDividerProps {} 122 123export const StageActions = (props: StageActionsProps) => { 124 return ( 125 <div hidden={props.hidden} class="flex flex-wrap gap-4"> 126 {props.children} 127 </div> 128 ); 129}; 130 131StageActions.Divider = (_props: StageActionsDividerProps) => { 132 return <div class="grow"></div>; 133}; 134 135export interface StageErrorViewProps { 136 error: string | undefined; 137} 138 139export const StageErrorView = (props: StageErrorViewProps) => { 140 return ( 141 <div 142 hidden={!props.error} 143 class="whitespace-pre-wrap text-[0.8125rem] font-medium leading-5 text-red-800" 144 > 145 {'' + props.error} 146 </div> 147 ); 148};