tablo_core/schema/layouts/
wizard.rs1use topcoat::{
4 Result,
5 context::Cx,
6 view::{StaticClass, class, *},
7};
8
9use super::{Span, spanned, tab_id};
10use crate::schema::{
11 Schema,
12 condition::Condition,
13 fields::Field,
14 tree::{IntoSchema, Node, Retype, Source, render_nodes},
15};
16
17const STEP_LIST: StaticClass =
20 class!("flex flex-wrap items-center gap-x-6 gap-y-2 [counter-reset:tablo-step]");
21const STEP_TRIGGER: StaticClass = class!(
23 "group inline-flex items-center gap-2 rounded-md text-sm font-medium text-muted-foreground \
24 outline-none [counter-increment:tablo-step] focus-visible:ring-[3px] \
25 focus-visible:ring-ring/50 aria-[current=step]:text-foreground \
26 data-wizard-error:text-destructive"
27);
28const STEP_NUMBER: StaticClass = class!(
30 "flex size-7 shrink-0 items-center justify-center rounded-full border border-input text-xs \
31 before:content-[counter(tablo-step)] group-data-wizard-done:border-primary \
32 group-data-wizard-done:text-primary group-aria-[current=step]:border-primary \
33 group-aria-[current=step]:bg-primary group-aria-[current=step]:text-primary-foreground \
34 group-data-wizard-error:border-destructive group-data-wizard-error:text-destructive"
35);
36
37#[derive(Debug)]
60pub struct Wizard<C = Schema> {
61 pub(crate) children: C,
63 pub(crate) condition: Option<Condition>,
64 pub(super) span: Option<Span>,
65}
66
67impl Wizard<()> {
68 pub fn new<F>(steps: impl IntoIterator<Item = Step<Schema<F>>>) -> Wizard<Schema<F>> {
70 let mut children = Schema::<F>::default();
71 for step in steps {
72 let mut fields = Vec::new();
73 let step = step.map(|mut children| {
74 fields = std::mem::take(&mut children.fields);
75 children.retype()
76 });
77 children.append(Schema::from_parts(vec![Node::Step(Box::new(step))], fields));
78 }
79 Wizard {
80 children,
81 condition: None,
82 span: None,
83 }
84 }
85}
86
87impl<C> Wizard<C> {
88 pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Wizard<D> {
89 Wizard {
90 children: f(self.children),
91 condition: self.condition,
92 span: self.span,
93 }
94 }
95}
96
97impl<F> IntoSchema<F> for Wizard<Schema<F>> {
98 fn into_schema(self) -> Schema<F> {
99 let mut fields = Vec::new();
100 let block = self.map(|mut children| {
101 fields = std::mem::take(&mut children.fields);
102 children.retype()
103 });
104 Schema::from_parts(vec![Node::Wizard(Box::new(block))], fields)
105 }
106}
107
108#[derive(Debug)]
110pub struct Step<C = Schema> {
111 label: crate::Text,
112 pub(crate) children: C,
113 pub(crate) condition: Option<Condition>,
114}
115
116impl Step<()> {
117 pub fn new(label: impl Into<crate::Text>) -> Self {
118 Self {
119 label: label.into(),
120 children: (),
121 condition: None,
122 }
123 }
124
125 pub fn schema<F>(self, children: impl IntoSchema<F>) -> Step<Schema<F>> {
127 self.map(|_| children.into_schema())
128 }
129}
130
131impl<C> Step<C> {
132 pub(crate) fn map<D>(self, f: impl FnOnce(C) -> D) -> Step<D> {
133 Step {
134 label: self.label,
135 children: f(self.children),
136 condition: self.condition,
137 }
138 }
139}
140
141impl Wizard<Schema> {
142 pub(crate) async fn render<'a>(
143 &self,
144 cx: &'a Cx,
145 fields: &[Field],
146 source: &Source<'_>,
147 ) -> Result<BoxView<'a>> {
148 let steps: Vec<(&Node, &Step)> = self
149 .children
150 .nodes
151 .iter()
152 .filter_map(|node| match node {
153 Node::Step(step) if node.shows_any(fields, source) => Some((node, &**step)),
154 _ => None,
155 })
156 .collect();
157 let errored: Vec<bool> = steps
158 .iter()
159 .map(|(node, _)| node.has_error(fields, source))
160 .collect();
161 let shown: Vec<bool> = steps
163 .iter()
164 .map(|(_, step)| source.holds(step.condition.as_ref()))
165 .collect();
166 let active = errored
167 .iter()
168 .position(|e| *e)
169 .unwrap_or_else(|| shown.iter().position(|s| *s).unwrap_or(0));
170 let first = shown.iter().position(|s| *s);
171 let last = shown.iter().rposition(|s| *s);
172 let depth = source.tab_depth();
173 let mut triggers = Vec::with_capacity(steps.len());
174 let mut panels = Vec::with_capacity(steps.len());
175 for (index, (node, step)) in steps.iter().enumerate() {
176 let id = node
177 .first_field()
178 .map(|field| tab_id(&source.id(fields[field].name()), depth))
179 .unwrap_or_default();
180 source.set_tab_depth(depth + 1);
181 let body = render_nodes(cx, &step.children.nodes, fields, source).await;
182 source.set_tab_depth(depth);
183 let (trigger, panel) = step_view(
184 cx,
185 &id,
186 &step.label,
187 index.cmp(&active),
188 errored[index],
189 body?,
190 );
191 triggers.push(source.guard(cx, step.condition.as_ref(), trigger));
192 panels.push(source.guard(cx, step.condition.as_ref(), panel));
193 }
194 let view = view! {
195 <div data-wizard="" class="flex flex-col gap-6">
196 <div class=(STEP_LIST)>
197 for trigger in triggers {
198 (trigger)
199 }
200 </div>
201 for panel in panels {
202 (panel)
203 }
204 <div class="flex items-center justify-between gap-2">
205 tablo_ui::button(
206 variant: tablo_ui::ButtonVariant::Outline,
207 attrs: attributes! {
208 type="button"
209 data-wizard-back=""
210 hidden=(first.is_none_or(|first| active <= first))
211 },
212 (crate::t!(cx, "tablo-wizard-back"))
213 )
214 tablo_ui::button(
215 variant: tablo_ui::ButtonVariant::Secondary,
216 attrs: attributes! {
217 type="button"
218 data-wizard-next=""
219 class="ms-auto"
220 hidden=(last.is_none_or(|last| active >= last))
221 },
222 (crate::t!(cx, "tablo-wizard-next"))
223 )
224 </div>
225 </div>
226 }
227 .boxed();
228 Ok(spanned(cx, self.span, view))
229 }
230}
231
232fn step_view<'a>(
235 cx: &'a Cx,
236 id: &str,
237 label: &crate::Text,
238 place: std::cmp::Ordering,
239 errored: bool,
240 body: BoxView<'a>,
241) -> (BoxView<'a>, BoxView<'a>) {
242 let trigger_id = format!("{id}-step");
243 let panel_id = format!("{id}-steppanel");
244 let labelled_by = trigger_id.clone();
245 let controls = panel_id.clone();
246 let label = label.localize(cx);
247 let current = place.is_eq();
248 let trigger = view! {
249 <button
250 type="button"
251 id=(trigger_id)
252 aria-controls=(controls)
253 aria-current=(current.then_some("step"))
254 data-wizard-trigger=""
255 data-wizard-done=(place.is_lt())
256 data-wizard-error=(errored)
257 class=(STEP_TRIGGER)
258 >
259 <span aria-hidden="true" class=(STEP_NUMBER)></span>
260 (label)
261 </button>
262 }
263 .boxed();
264 let panel = view! {
265 <div
266 role="group"
267 id=(panel_id)
268 aria-labelledby=(labelled_by)
269 hidden=(!current)
270 data-wizard-step=""
271 class="flex flex-col gap-6"
272 >
273 (body)
274 </div>
275 }
276 .boxed();
277 (trigger, panel)
278}