app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit c0b60da261eab5ae956fa1e4c3d2a62318e1670d
parent 4a751f6dcb695283dd73a22a4e1a4f39592d6ca3
Author: triesap <tyson@radroots.org>
Date:   Fri,  6 Feb 2026 18:48:54 +0000

app: add individual summary view

- add summary cards for profile and products in step 3
- route summary taps back to profile or products steps
- replace notification checkboxes with ios switch styling
- add ios switch styles to shared ui css

Diffstat:
Mapp/src/app.rs | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mapp/stylesheets/apps-ui.css | 34++++++++++++++++++++++++++++++++++
2 files changed, 141 insertions(+), 23 deletions(-)

diff --git a/app/src/app.rs b/app/src/app.rs @@ -1888,8 +1888,8 @@ fn ConfigPage() -> impl IntoView { > <textarea id="app-config-individual-products-input" - class="textarea-base min-h-[7.5rem]" - rows="4" + class="textarea-base min-h-[15rem]" + rows="8" placeholder="Apples, tomatoes, fresh herbs".to_string() prop:value=move || individual_products_input.get() on:input=move |ev| { @@ -1965,39 +1965,123 @@ fn ConfigPage() -> impl IntoView { id="app-config-preferences" class="app-view app-view-enter flex flex-col w-full gap-5" > + {move || { + if role.get() == Some(RadrootsAppRole::Individual) { + view! { + <div + id="app-config-summary" + class="flex flex-col gap-3" + > + <p class="text-xs font-semibold uppercase tracking-[0.18em] text-ly1-gl-label/70"> + {"Summary"} + </p> + <div class="flex flex-col rounded-touch border border-ly1-edge/60 bg-ly1 overflow-hidden"> + <button + id="app-config-summary-profile" + type="button" + class="flex items-center justify-between gap-4 px-4 py-3 text-left" + on:click=move |_| { + config_step.set(RadrootsAppConfigStep::Profile); + } + > + <div class="flex flex-col gap-1"> + <span class="text-sm font-semibold text-ly1-gl"> + {"Profile"} + </span> + <span class="text-xs text-ly1-gl-label/80 line-clamp-2"> + {move || { + let name = profile_name.get(); + let location = profile_location.get(); + let name = name.trim().to_string(); + let location = location.trim().to_string(); + if name.is_empty() && location.is_empty() { + "Add name and location".to_string() + } else if location.is_empty() { + name + } else if name.is_empty() { + location + } else { + format!("{name} • {location}") + } + }} + </span> + </div> + <RadrootsAppUiIcon key=RadrootsAppUiIconKey::CaretRight size=18 /> + </button> + <button + id="app-config-summary-products" + type="button" + class="flex items-center justify-between gap-4 border-t border-ly1-edge/50 px-4 py-3 text-left" + on:click=move |_| { + config_step.set(RadrootsAppConfigStep::Role); + } + > + <div class="flex flex-col gap-1"> + <span class="text-sm font-semibold text-ly1-gl"> + {"Products interested in"} + </span> + <span class="text-xs text-ly1-gl-label/80 line-clamp-2"> + {move || { + let items = individual_products.get(); + if items.is_empty() { + "Add products".to_string() + } else { + items.join(", ") + } + }} + </span> + </div> + <RadrootsAppUiIcon key=RadrootsAppUiIconKey::CaretRight size=18 /> + </button> + </div> + </div> + } + .into_any() + } else { + view! { <></> }.into_any() + } + }} <RadrootsAppUiFormField label="Notifications".to_string() id="app-config-preferences-notifications".to_string() > - <div class="flex flex-col gap-3"> - <label + <div class="flex flex-col rounded-touch border border-ly1-edge/60 bg-ly1 overflow-hidden"> + <div id="app-config-notifications-orders" - class="flex items-center justify-between gap-4 rounded-touch border border-ly1-edge/60 bg-ly1 px-3 py-2 text-sm text-ly1-gl" + class="flex items-center justify-between gap-4 px-4 py-3 text-sm text-ly1-gl" > <span>{"Order updates"}</span> - <input - type="checkbox" - class="h-4 w-4 accent-[hsl(var(--ly1-gl))]" - prop:checked=move || notifications_orders.get() - on:change=move |ev| { - notifications_orders.set(event_target_checked(&ev)); + <button + type="button" + class="ios-switch" + role="switch" + aria-checked=move || if notifications_orders.get() { "true" } else { "false" } + attr:data-checked=move || if notifications_orders.get() { "true" } else { "false" } + on:click=move |_| { + notifications_orders.update(|value| *value = !*value); } - /> - </label> - <label + > + <span class="ios-switch__thumb"></span> + </button> + </div> + <div id="app-config-notifications-messages" - class="flex items-center justify-between gap-4 rounded-touch border border-ly1-edge/60 bg-ly1 px-3 py-2 text-sm text-ly1-gl" + class="flex items-center justify-between gap-4 border-t border-ly1-edge/50 px-4 py-3 text-sm text-ly1-gl" > <span>{"Messages"}</span> - <input - type="checkbox" - class="h-4 w-4 accent-[hsl(var(--ly1-gl))]" - prop:checked=move || notifications_messages.get() - on:change=move |ev| { - notifications_messages.set(event_target_checked(&ev)); + <button + type="button" + class="ios-switch" + role="switch" + aria-checked=move || if notifications_messages.get() { "true" } else { "false" } + attr:data-checked=move || if notifications_messages.get() { "true" } else { "false" } + on:click=move |_| { + notifications_messages.update(|value| *value = !*value); } - /> - </label> + > + <span class="ios-switch__thumb"></span> + </button> + </div> </div> </RadrootsAppUiFormField> <RadrootsAppUiFormField diff --git a/app/stylesheets/apps-ui.css b/app/stylesheets/apps-ui.css @@ -138,6 +138,40 @@ @apply active:opacity-80 group-active:opacity-80; } +@layer components { + .ios-switch { + display: inline-flex; + align-items: center; + justify-content: flex-start; + width: 44px; + height: 26px; + padding: 2px; + border-radius: 999px; + border: 1px solid hsl(var(--ly1-edge) / 0.6); + background: hsl(var(--ly1-edge) / 0.3); + transition: background 160ms ease, border-color 160ms ease; + } + + .ios-switch__thumb { + width: 22px; + height: 22px; + border-radius: 999px; + background: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); + transition: transform 160ms ease; + transform: translateX(0); + } + + .ios-switch[data-checked="true"] { + background: hsl(var(--ly0-gl-hl) / 1); + border-color: hsl(var(--ly0-gl-hl) / 0.9); + } + + .ios-switch[data-checked="true"] .ios-switch__thumb { + transform: translateX(18px); + } +} + @utility ly1-apply-active { @apply bg-ly1-focus; }