Feedback

SaveStatus

Saving…, Saved, Unsaved changes or Could not save, with an icon, the time last saved and an optional action.

#feedback#status#time#no-js

Demo

Preview
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.qwe
main.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>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.