Save Bar
Shopify-style contextual save bar, shown when a form has unsaved changes. Renders nothing when not visible.
Selector: tip-save-bar
API
| Name | Type | Default | Description |
|---|---|---|---|
visible (input) |
boolean |
false |
Typically bound to a page's dirty() computed signal; renders nothing when false. |
message (input) |
string |
'Unsaved changes' |
Left-aligned status text. |
saving (input) |
boolean |
false |
Disables both buttons and swaps the Save label to "Saving…". |
save (output) |
void |
— | Emitted on Save click. |
discard (output) |
void |
— | Emitted on Discard click. |
Usage
<tip-save-bar [visible]="dirty()" [saving]="saving()" (save)="onSave()" (discard)="onDiscard()" />
readonly dirty = computed(() => JSON.stringify(this.currentValues()) !== JSON.stringify(this.baseline()));
onSave(): void { this.baseline.set(this.currentValues()); this.toast.show('Settings saved', { tone: 'success' }); }
onDiscard(): void { this.applyValues(this.baseline()); }
Notes
- Top-pinning gotcha: the bar is
position: sticky; top: 0within its container — place it as the first element inside the scrollable content area of the screen that owns the form (see everysettings/pages/*.html, which puts it as the first child of.stg-page), not inside the fixed app-shell chrome, or the sticky offset won't behave as expected. - Each Settings page owns its own dirty-tracking: a
baselinesignal snapshotting the last-saved values, acurrentValuescomputed from the live field signals, anddirty = computed(() => JSON.stringify(currentValues()) !== JSON.stringify(baseline())). There's no shared "form state" service — this pattern is duplicated per page intentionally (see conventions.md and patterns/settings.md). - Slide-down entrance animation (
0.16s ease) is skipped underprefers-reduced-motion.