handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
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};