Dashboard

OnboardingChecklist

Steps to finish setting up, with a progress bar, a tick on each done and a link to where the rest get done.

#dashboard#progress#list#forms#user#interactive

Demo

Preview

Get started

1 of 3 done
  1. Fill in your profileA name and a picture.
  2. Invite your teamThree seats are free on this plan.Invite →
  3. Deploy a projectSet up →

Tick a step, or follow its link.

Billing

2 of 2 done
  1. Add a card
  2. Add a VAT number
All done. You are set up.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/onboarding_checklist.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/onboarding_checklist.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/onboarding_checklist.qwe
component OnboardingChecklistDemoLive() {    state last = null    state hidden = false    <div.demo-stack>        <OnboardingChecklist steps={[            { id: "profile", title: "Fill in your profile", text: "A name and a picture.", done: true },            { id: "team", title: "Invite your team", text: "Three seats are free on this plan.", href: "#onboarding_checklist", link: "Invite" },            { id: "deploy", title: "Deploy a project", href: "#onboarding_checklist" },        ]} on_change={(id, on) => { last = `${id} ${on ? "done" : "undone"}` }} on_dismiss={() => { hidden = true }}/>        <p.demo-note>{hidden ? "Hidden: on_dismiss was called." : last ?? "Tick a step, or follow its link."}</p>    </div>}view OnboardingChecklistDemo() =    <div.demo-stack.demo-narrow>        <OnboardingChecklistDemoLive/>        <OnboardingChecklist title="Billing" dismissable={false} steps={[            { id: "card", title: "Add a card", done: true }, { id: "vat", title: "Add a VAT number", done: true },        ]}/>    </div>

Source

ui/onboarding_checklist.qwe
// OnboardingChecklist: the steps to finish setting up, with how many are done,// a tick on each done one, and a link to where the rest get done. Each step's// tick is a real checkbox, so a step can also be marked done by hand.////   <OnboardingChecklist steps={[//       { id: "profile", title: "Fill in your profile", done: true },//       { id: "team", title: "Invite your team", text: "Three seats are free.", href: "/team", link: "Invite" },//       { id: "deploy", title: "Deploy a project", href: "/projects/new" },//   ]}/>//   <OnboardingChecklist title="Set up billing" steps={steps} on_change={(id, done) => { save(id, done) }}/>//// steps: a list of { id, title, text?, href?, link?, done? }: href and link make// a link to where it gets done ("Set up" by default); done marks it finished.// title: the heading ("Get started"). name: the checkboxes' name ("done"), each// with the step's id as its value, so a form sends the ones ticked. on_change(id,// done): inside one of your components, called when a step is ticked or unticked.// When every step is done it says so and offers a Hide button; dismissable false// drops that; on_dismiss(): called with nothing when it is hidden; remember: a// name under which the hiding is kept in this browser (localStorage).import ui/theme.qwe use ui_clslet onboarding_checklist_css = css`.ui-onboard-host { display: contents; }.ui-onboard { display: grid; gap: 14px; padding: 18px 20px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); font: 14px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-onboard-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px; }.ui-onboard-title { margin: 0; font-size: 16px; font-weight: 600; }.ui-onboard-count { color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-onboard-track { height: 6px; overflow: hidden; border-radius: 999px; background: var(--ui-subtle);  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-border) 60%, transparent); }.ui-onboard-fill { height: 100%; border-radius: inherit; background: var(--ui-success); transition: width .3s ease; }.ui-onboard-steps { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; }.ui-onboard-step { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 2px 12px; align-items: center; padding: 8px 6px; border-radius: var(--ui-radius-sm); }.ui-onboard-step:hover { background: var(--ui-subtle); }.ui-onboard-tick { position: relative; display: grid; place-items: center; width: 22px; height: 22px; }.ui-onboard-tick input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }.ui-onboard-box { display: grid; place-items: center; width: 22px; height: 22px; border: 2px solid var(--ui-strong); border-radius: 50%; color: transparent; transition: background .15s, border-color .15s; }.ui-onboard-box svg { width: 12px; height: 12px; }.ui-onboard-tick input:checked + .ui-onboard-box { border-color: var(--ui-success); background: var(--ui-success); color: #fff; }.ui-onboard-tick input:focus-visible + .ui-onboard-box { box-shadow: var(--ui-focus); }.ui-onboard-what { min-width: 0; }.ui-onboard-step-title { font-weight: 500; }.ui-onboard-done .ui-onboard-step-title { color: var(--ui-muted); text-decoration: line-through; }.ui-onboard-text { display: block; color: var(--ui-muted); font-size: 13px; }.ui-onboard-link { color: var(--ui-accent); font-weight: 500; font-size: 13px; text-decoration: none; white-space: nowrap; border-radius: 3px; }.ui-onboard-link:hover { text-decoration: underline; }.ui-onboard-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-onboard-done .ui-onboard-link { visibility: hidden; }.ui-onboard-all { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; padding: 10px 12px;  border-radius: var(--ui-radius-sm); background: var(--ui-success-soft); color: var(--ui-success); font-weight: 500; }.ui-onboard-hide { height: 30px; padding: 0 11px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-fg); font: 500 13px/1 var(--ui-font); cursor: pointer; }.ui-onboard-hide:hover { background: var(--ui-subtle); }.ui-onboard-hide:focus-visible { outline: none; box-shadow: var(--ui-focus); }@media (prefers-reduced-motion: reduce) { .ui-onboard-fill, .ui-onboard-box { transition: none; } }`component OnboardingChecklist(steps: list, title: str = "Get started", name: str = "done", dismissable: bool = true,                              remember: str? = null, on_change = null, on_dismiss = null, class: str? = null) {    style global onboarding_checklist_css    state done = steps | filter((s) => s.done == true) | map((s) => s.id)    state shown = true    let key = remember != null ? `ui-onboarding:${remember}` : null    let total = len(steps)    let n = len(done)    let pct = total > 0 ? round(n * 100 / total) : 0    <div class="ui-onboard-host" @mount={(_el) => {        if key != null && js("localStorage.getItem", key) == "1" { shown = false }    }}>        {shown && <section class={ui_cls(["ui-onboard", class])} aria-label={title}>            <div.ui-onboard-head>                <h3.ui-onboard-title>{title}</h3>                <span.ui-onboard-count role="status">{n} of {total} done</span>            </div>            <div.ui-onboard-track role="progressbar" aria-label={title} aria-valuemin="0" aria-valuemax={str(total)} aria-valuenow={str(n)}                 aria-valuetext={`${n} of ${total} done`}>                <div.ui-onboard-fill style={`width: ${pct}%`}></div>            </div>            <ol.ui-onboard-steps>                {for s in steps => {                    let is_done = contains(done, s.id)                    <li class={ui_cls(["ui-onboard-step", is_done && "ui-onboard-done"])} key={s.id}>                        <span.ui-onboard-tick>                            <input type="checkbox" name={name} value={str(s.id)} checked={is_done} aria-label={`Done: ${s.title}`}                                   @change={(e) => {                                       done = e.checked ? (done | push(s.id)) : (done | filter((d) => d != s.id))                                       on_change && on_change(s.id, e.checked)                                   }}/>                            <span.ui-onboard-box aria-hidden="true">                                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3 3 7-7"/></svg>                            </span>                        </span>                        <span.ui-onboard-what>                            <span.ui-onboard-step-title>{s.title}</span>                            {s.text != null && <span.ui-onboard-text>{s.text}</span>}                        </span>                        {s.href != null && <a.ui-onboard-link href={s.href} tabindex={is_done ? "-1" : null}>{s.link ?? "Set up"} →</a>}                    </li>                }}            </ol>            {total > 0 && n == total && <div.ui-onboard-all>                <span>All done. You are set up.</span>                {dismissable && <button.ui-onboard-hide type="button" @click={() => {                    shown = false                    if key != null { js("localStorage.setItem", key, "1") }                    on_dismiss && on_dismiss()                }}>Hide</button>}            </div>}        </section>}    </div>}
Thread
75Useful? Like it, or add to its thread below.

Thread 0 posts

On OnboardingChecklist: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.