SaveStatus
Saving…, Saved, Unsaved changes or Could not save, with an icon, the time last saved and an optional action.
Demo
Saved14:02Saving…Unsaved changesCould not saveAll changes savedjust now
Saved14:02
Saved 0 times from the button.
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/save_status.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/save_status.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/save_status.qwe
component SaveStatusDemoLive() { state status = "saved" state saves = 0 <div.demo-stack> <div.demo-row> <TextArea name="notes" label="Notes" rows={2} value="Edit me to see the status change." on_input={(_v) => { status = "unsaved" }}/> </div> <div.demo-row> <SaveStatus id="save-status-demo-live" status={status} time="14:02" action={status == "unsaved" ? "Save now" : null} on_click={() => { status = "saving"; saves += 1 }}/> {status == "saving" && <Button size="sm" variant="outline" id="save-status-demo-finish" on_click={() => { status = "saved" }}>Pretend it finished</Button>} </div> <p.demo-note>Saved {saves} {saves == 1 ? "time" : "times"} from the button.</p> </div>}view SaveStatusDemo() = <div.demo-stack> <div.demo-row style="gap: 24px"> <SaveStatus id="save-status-demo-saved" status="saved" time="2026-10-27T14:02:00Z"/> <SaveStatus id="save-status-demo-saving" status="saving"/> <SaveStatus id="save-status-demo-unsaved" status="unsaved"/> <SaveStatus id="save-status-demo-error" status="error" action="Retry"/> <SaveStatus status="saved" time="just now" size="sm" words={{ saved: "All changes saved" }}/> </div> <SaveStatusDemoLive/> </div>Source
ui/save_status.qwe
// SaveStatus: a word on whether what is being edited is saved: "Saving…",// "Saved", "Unsaved changes" or "Could not save", with an icon and the time it// was last saved. For the corner of an editor or a settings page.//// <SaveStatus status="saved" time="2026-10-27T14:02:00Z"/>// <SaveStatus status="saving"/>// <SaveStatus status="unsaved" on_click={save} action="Save now"/>//// status: "saved" (default), "saving", "unsaved" or "error". time: when it was// last saved, an ISO date-time (shown as its clock time, as written: date.to_zone// it to the reader's zone first) or any short text ("just now", "14:02").// words: a map from status to what it says, to change any of them. action: the// text of a small button shown after the word, with on_click() put on it// (inside one of your components: "Save now", "Retry"). size: "sm" or "md"// (default). Screen readers hear each change (role="status"); an error is an alert.import ui/theme.qwe use ui_clslet save_status_css = css`.ui-savest { display: inline-flex; align-items: center; gap: 6px; font: 13px/1.4 var(--ui-font); color: var(--ui-muted); --t: var(--ui-muted); }.ui-savest-saved { --t: var(--ui-success); }.ui-savest-unsaved { --t: var(--ui-warning); }.ui-savest-error { --t: var(--ui-danger); color: var(--ui-danger); }.ui-savest-icon { flex: none; width: 16px; height: 16px; color: var(--t); }.ui-savest-saving .ui-savest-icon { animation: ui-savest-turn 1s linear infinite; }.ui-savest-time { color: var(--ui-muted); }.ui-savest-time::before { content: "·"; margin: 0 6px 0 0; }.ui-savest-action { height: 24px; padding: 0 8px; margin-left: 2px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 500 12px/1 var(--ui-font); cursor: pointer; }.ui-savest-action:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-savest-action:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-savest-sm { font-size: 12px; gap: 5px; }.ui-savest-sm .ui-savest-icon { width: 14px; height: 14px; }@keyframes ui-savest-turn { to { transform: rotate(360deg); } }@media (prefers-reduced-motion: reduce) { .ui-savest-saving .ui-savest-icon { animation: none; } }`let save_status_words = { saved: "Saved", saving: "Saving…", unsaved: "Unsaved changes", error: "Could not save" }// The icon for each status, drawn in currentColor.fn save_status_icon(status: str) { let parts = match status { "saving" => [<path d="M8 2.5a5.5 5.5 0 1 1-5.2 3.7"/>, <path d="M2.5 2.5v4h4"/>] "unsaved" => [<circle cx="8" cy="8" r="6"/>, <path d="M8 5v3.5l2 1.5"/>] "error" => [<circle cx="8" cy="8" r="6"/>, <path d="M8 5v3.5M8 11h.01"/>] _ => [<circle cx="8" cy="8" r="6"/>, <path d="M5.5 8.2l1.8 1.8 3.4-3.8"/>] } <svg class="ui-savest-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{parts}</svg>}// A time to show: the clock time of an ISO date-time, or the text as given.fn save_status_time(time: str) -> str = date.valid(time) && contains(time, "T") ? date.format(time, "HH:mm") : timeview SaveStatus(status: str = "saved", time: str? = null, words: map = {}, action: str? = null, on_click = null, size: str = "md", id: str? = null, class: str? = null) { style global save_status_css let said = words[status] ?? save_status_words[status] ?? status <span class={ui_cls(["ui-savest", `ui-savest-${status}`, size != "md" && `ui-savest-${size}`, class])} id={id} role={status == "error" ? "alert" : "status"}> {save_status_icon(status)} <span.ui-savest-word>{said}</span> {time != null && status == "saved" && <span.ui-savest-time>{save_status_time(time)}</span>} {action != null && <button.ui-savest-action type="button" @click={() => { on_click && on_click() }}>{action}</button>} </span>}Related
AlertA message in the page in six tones and three variants, with an icon, announced to screen readers, and dismissable.5 views · 9 downloadsBadgeA small pill for a status or a count: six tones, soft, solid or outline, a status dot, and 99+.8 views · 6 downloadsConnectionStatusSays when the browser goes offline, as a bar across the top or a pill, and that it is back online for a moment.6 views · 7 downloadsErrorPageA 404 or 500 page: the code large, what it means, a search box if wanted, and links to go on from.5 views · 6 downloadsInlineMessageA small one-line message with an icon for its tone (info, success, warning, error), to sit in a form or beside a control.5 views · 7 downloadsLoadingDotsThree dots bouncing in turn, inline, for something short that is on its way; screen readers hear the label.5 views · 6 downloads
Thread
Thread 0 posts
On SaveStatus: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.