Files
puppy-tracker/src/index.html
T
Alexander Heldt 09b9b62d06 Move Ate down to the dialog row
Pee and Poo take a row of their own and Ate joins Weigh-in and Note across the
bottom, which regroups the panel by what a tap does rather than by what the
event means: the bottom row is now exactly the three buttons that open a
dialog to type a value, and the row above it exactly the two that log on one
tap. Ate was the odd one out under the old split — it sat with the pees and
poos but behaved like a weigh-in.

That hands the longest label in the app, "⚖️ Weigh-in", a third-width button.
The narrow-phone padding trim already in place was written for "🍽️ Ate" in
that slot and now serves Weigh-in instead; on a mid-size phone the label is
close enough to the edge that it may wrap to two lines, which costs a little
row height and nothing else. Shortening it to "Weight" is the fix if it does.

The changelog entry describing the rows is corrected in place rather than
answered with a second entry. The update banner diffs by entry text, so an
edited entry is already surfaced as new to anyone coming from the previous
build — and one accurate description of the layout beats two entries whose
combined reading is only correct if you apply them in order.
2026-08-31 21:39:41 +00:00

538 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#7c5cff" />
<title>Puppy Tracker</title>
<link rel="manifest" href="manifest.json" />
<link rel="icon" href="icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="icon-180.png" />
<link rel="stylesheet" href="style.css" />
<script>
// Apply a saved theme before first paint so there's no light/dark flash.
// No saved choice → leave it to the prefers-color-scheme media query.
(function () {
try {
var t = localStorage.getItem("puppy-tracker:theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
} catch (e) {}
})();
</script>
</head>
<body>
<!-- Shown when a newer build's service worker is waiting. "Reload" activates
it and refreshes onto the new assets; "Later" dismisses until next time.
Suppressed on the very first install (see app.js). -->
<div id="update-banner" class="update-banner" hidden role="status" aria-live="polite">
<div class="update-banner-row">
<span class="update-banner-msg">A new version is available</span>
<span class="update-banner-actions">
<button type="button" id="update-reload" class="update-banner-btn">Reload</button>
<button type="button" id="update-later" class="update-banner-btn ghostish">Later</button>
</span>
</div>
<!-- What the waiting build adds over the running one; filled by app.js
from the diff between the cached and the fresh changelog.json. -->
<ul id="update-changelog" class="update-changelog" hidden></ul>
</div>
<!-- Login / register gate. Shown until the session check succeeds; the app
(#app) stays hidden behind it so no puppy data paints while logged out. -->
<div id="auth-screen" class="auth-screen" hidden>
<div class="auth-card">
<h1>🐶 Puppy Tracker</h1>
<p class="auth-sub" id="auth-sub">Sign in to continue</p>
<form id="auth-form">
<label>Email
<input type="email" id="auth-email" autocomplete="username" required />
</label>
<label>Password
<input type="password" id="auth-password" autocomplete="current-password" required minlength="8" />
</label>
<label id="auth-invite-field" hidden>Invite code
<input type="text" id="auth-invite" autocomplete="off" placeholder="Ask the owner for this" />
</label>
<p id="auth-error" class="auth-error" hidden></p>
<button type="submit" id="auth-submit">Sign in</button>
</form>
<p class="auth-toggle">
<span id="auth-toggle-text">No account yet?</span>
<button type="button" id="auth-toggle-btn" class="linklike">Create one</button>
</p>
</div>
</div>
<div id="app" hidden>
<header>
<div class="title">
<h1 id="app-title">🐶 Puppy Tracker</h1>
<div id="puppy-age" class="puppy-age" hidden></div>
</div>
<div class="header-actions">
<button type="button" id="pedigree-btn" class="ghost icon-btn" aria-label="Pedigree" title="Pedigree" hidden>🌳</button>
<button type="button" id="settings-btn" class="ghost icon-btn" aria-label="Settings" title="Settings">⚙️</button>
<button type="button" id="logout-btn" class="ghost icon-btn" aria-label="Log out" title="Log out">🚪</button>
<div id="online-status" class="status-pill"></div>
</div>
</header>
<main>
<section class="day-bar">
<!-- Compact twin of the big timer below: invisible (but keeping its
slot) while the big card is on screen, shown once it scrolls
away. Hidden entirely until a sleep event exists. Tapping it logs
the boundary that flips the current state (asleep → sleep end,
awake → sleep start). -->
<button type="button" id="bar-clock" class="bar-clock" hidden>
<span id="bar-clock-icon" aria-hidden="true"></span>
<span id="bar-clock-time"></span>
</button>
<button type="button" id="day-prev" class="ghost" aria-label="Previous day"></button>
<!-- A browser won't let us shorten the text a native date input shows,
so the face button carries a compact year-less date and the real
input stays (visually hidden) as the value + native picker. -->
<span class="day-date">
<button type="button" id="day-date-face" class="ghost"></button>
<input type="date" id="day-picker" tabindex="-1" aria-hidden="true" />
</span>
<button type="button" id="day-next" class="ghost" aria-label="Next day"></button>
<button type="button" id="day-today" class="ghost">Today</button>
</section>
<section id="big-clock" class="big-clock" hidden>
<div class="bc-label" id="bc-label"></div>
<div class="bc-time" id="bc-time">0:00</div>
<div class="bc-since" id="bc-since"></div>
</section>
<section class="quick-actions">
<h2>Log event</h2>
<!-- Explicit rows rather than one auto-fit grid: a start/end pair has
to stay side by side at every width (a flat grid split them apart
at some column counts, and orphaned the last button on a row of
its own). A row per timed pair, then the two one-tap moments, and
last the three that open a dialog to type a value. -->
<div class="actions">
<div class="action-row">
<button class="action sleep" data-type="sleep-start">😴 Sleep start</button>
<button class="action sleep" data-type="sleep-end">⏰ Sleep end</button>
</div>
<div class="action-row">
<button class="action walk" data-type="walk-start">🦮 Walk start</button>
<button class="action walk" data-type="walk-end">🏁 Walk end</button>
</div>
<div class="action-row">
<button class="action pee" data-type="pee">💧 Pee</button>
<button class="action poo" data-type="poo">💩 Poo</button>
</div>
<div class="action-row three-up">
<button class="action eat" data-type="eat">🍽️ Ate</button>
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
<button class="action note" data-type="note">📝 Note</button>
</div>
</div>
</section>
<section class="training" data-panel="training">
<h2>Training</h2>
<ul id="training-list" class="training-list"></ul>
<p id="training-empty" class="empty">No exercises yet. Add one to start tracking training.</p>
<button type="button" id="exercise-add" class="ghost training-add">Add exercise</button>
<div class="chart training-chart" id="training-chart-wrap" hidden>
<div class="chart-title">Consistency <span data-chart-days-label>(last 7 days)</span></div>
<svg id="chart-training" class="chart-svg" viewBox="0 0 320 60" role="img" aria-label="Training sessions per exercise per day"></svg>
<p class="muted-note">Darker = more sessions that day. Tap a cell to open that day.</p>
</div>
</section>
<section class="overview" data-panel="overview">
<h2 id="overview-title">Today's overview</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Sleep</div>
<div class="stat-value" id="stat-sleep">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Awake</div>
<div class="stat-value" id="stat-awake">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Walks</div>
<div class="stat-value" id="stat-walk">0m</div>
<div class="stat-sub" id="stat-walk-count" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div>
<div class="stat-sub" id="stat-meals-grams" hidden></div>
</div>
<div class="stat">
<div class="stat-label">Pees</div>
<div class="stat-value" id="stat-pees">0</div>
</div>
<div class="stat">
<div class="stat-label">Poos</div>
<div class="stat-value" id="stat-poos">0</div>
</div>
<div class="stat">
<div class="stat-label">Training</div>
<div class="stat-value" id="stat-training">0</div>
</div>
</div>
<div class="lasts">
<div class="last-row"><span>Last pee</span><span id="last-pee"></span></div>
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div>
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div>
<div class="last-row"><span>Last walk</span><span id="last-walk"></span></div>
</div>
</section>
<section class="timing" data-panel="timing">
<h2>Timing <span class="muted-note">(last 7 days)</span></h2>
<!-- One range chart per type, drawn by drawTimingChart: the band spans
the shortest to the typical gap and the marker is how long it has
been since the last one, so a marker past the band reads as due. -->
<div class="timing-charts">
<div class="timing-item">
<div class="timing-name">Pees</div>
<svg id="timing-chart-pee" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<p class="muted-note timing-note" id="timing-note-pee" hidden></p>
</div>
<div class="timing-item">
<div class="timing-name">Poos</div>
<svg id="timing-chart-poo" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<p class="muted-note timing-note" id="timing-note-poo" hidden></p>
</div>
<div class="timing-item">
<div class="timing-name">Meals</div>
<svg id="timing-chart-eat" class="timing-chart" viewBox="0 0 320 50" role="img"></svg>
<p class="muted-note timing-note" id="timing-note-eat" hidden></p>
</div>
</div>
<!-- The axis is stretched (see drawTimingChart), so say what the middle
of a bar means rather than leave it to be inferred. -->
<p class="muted-note">The middle of every bar is that type's typical gap: left of it is sooner than usual, right of it is longer, and the faded stretch runs out to the longest gap of the week.</p>
<p class="muted-note timing-hint" id="timing-hint"></p>
</section>
<section class="sleep" data-panel="sleep-windows">
<h2>Sleep windows</h2>
<ul id="sleep-list" class="wake-list"></ul>
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
</section>
<section class="wake" data-panel="wake-windows">
<h2>Wake windows</h2>
<ul id="wake-list" class="wake-list"></ul>
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
</section>
<section class="walks" data-panel="walks">
<h2>Walks <span class="muted-note" id="walk-total"></span></h2>
<ul id="walk-list" class="wake-list"></ul>
<p id="walk-empty" class="empty">No walks yet for this day. Use 🦮 Walk start / 🏁 Walk end to time one.</p>
<!-- Age-based guidance ("the five-minute rule"), only when a birthday
is set and the puppy is still growing. -->
<p id="walk-goal" class="muted-note" hidden></p>
</section>
<section class="weekly" data-panel="weekly">
<h2 id="daily-charts-title">Last 7 days</h2>
<div class="chart-days-picker" role="group" aria-label="How many days the charts cover">
<button type="button" class="ghost" data-days="7">7d</button>
<button type="button" class="ghost" data-days="14">14d</button>
<button type="button" class="ghost" data-days="30">30d</button>
</div>
<div class="chart">
<div class="chart-title">Sleep (hours)</div>
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day"></svg>
</div>
<div class="chart">
<div class="chart-title">Daily counts</div>
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day"></svg>
<div class="legend legend-toggle" id="counts-metrics" role="group" aria-label="Which counts to show">
<label class="lg pee"><input type="checkbox" data-metric="pees" checked /><span class="sw"></span>Pees</label>
<label class="lg poo"><input type="checkbox" data-metric="poos" checked /><span class="sw"></span>Poos</label>
<label class="lg eat"><input type="checkbox" data-metric="meals" checked /><span class="sw"></span>Meals</label>
</div>
</div>
<div class="chart" id="walk-chart-wrap" hidden>
<div class="chart-title">Walks (minutes)</div>
<svg id="chart-walk" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Minutes walked per day"></svg>
</div>
<div class="chart" id="grams-chart-wrap" hidden>
<div class="chart-title">Food (grams)</div>
<svg id="chart-grams" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Grams of food eaten per day"></svg>
</div>
</section>
<section class="patterns" data-panel="sleep-timeline">
<h2><span id="sleep-timeline-title">When sleeping</span> <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<svg id="chart-sleep-timeline" class="chart-svg" viewBox="0 0 320 125" role="img" aria-label="Sleep periods per day"></svg>
<p class="muted-note">Each row is a day, midnight to midnight; shaded = asleep. Tap a row to open that day.</p>
</section>
<section class="patterns" data-panel="sleep-trend">
<h2>Sleep trend</h2>
<svg id="chart-sleep-trend" class="chart-svg" viewBox="0 0 320 220" role="img" aria-label="Cumulative sleep hours through the selected day, the day before it, the recent average and (for today) the projected end-of-day total, with the age-based sleep goal band"></svg>
<div class="legend">
<span class="lg trend-today"><span class="sw"></span><span id="legend-trend-today-text">Today</span></span>
<span class="lg trend-projected" id="legend-trend-projected" hidden><span class="sw"></span><span id="legend-trend-projected-text">Projected</span></span>
<span class="lg trend-yesterday" id="legend-trend-yesterday"><span class="sw"></span><span id="legend-trend-yesterday-text">Yesterday</span></span>
<span class="lg trend-avg" id="legend-trend-avg"><span class="sw"></span><span id="legend-trend-avg-text">7-day avg</span></span>
<span class="lg trend-goal" id="legend-trend-goal" hidden><span class="sw"></span><span id="legend-trend-goal-text">Goal</span></span>
</div>
<p class="muted-note">Hours slept so far at each point of the day, against yesterday and the average over the picked chart window. The dashed tail continues today's line the way the average day usually plays out. The axis is stretched above 10h to give the hours around the goal more room.</p>
</section>
<section class="patterns" data-panel="hour-heatmap">
<h2>By hour of day <span class="muted-note" data-chart-days-label>(last 7 days)</span></h2>
<svg id="chart-hour-heatmap" class="chart-svg" viewBox="0 0 320 120" role="img" aria-label="Pee, poo and meal frequency by hour of day"></svg>
<p id="hour-heatmap-info" class="muted-note hour-point-info" aria-live="polite"></p>
<p class="muted-note">Darker = happens more often at that hour.</p>
</section>
<section class="weight" data-panel="weight">
<h2>Weight</h2>
<div class="weight-summary">
<div class="stat">
<div class="stat-label">Latest</div>
<div class="stat-value" id="weight-latest"></div>
</div>
<div class="stat">
<div class="stat-label">Since last</div>
<div class="stat-value" id="weight-change"></div>
</div>
</div>
<div class="chart">
<div class="chart-title">Weight (kg)</div>
<svg id="chart-weight" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Weight in kilograms over time"></svg>
<p id="weight-point-info" class="muted-note weight-point-info"></p>
</div>
<ul id="weight-list" class="wake-list"></ul>
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
</section>
<section class="notes-log" data-panel="notes">
<h2>Notes</h2>
<ul id="notes-list" class="event-list"></ul>
<p id="notes-empty" class="empty">No notes yet. Use the 📝 Note button to jot down things like vaccinations or vet visits — they'll be listed here across every day.</p>
</section>
<section class="history" data-panel="history">
<h2>History</h2>
<ul id="event-list" class="event-list"></ul>
<p id="empty-state" class="empty">No events logged for this day.</p>
</section>
</main>
<footer class="app-footer">
<button type="button" id="changelog-btn" class="linklike">Changelog</button>
</footer>
</div><!-- /#app -->
<!-- Pedigree lookup. A distinct full-screen view (hides #app while open)
that resolves a dog by chip / registration number / name against SKK
and renders its ancestry as a tree. Online-only. -->
<div id="pedigree-screen" class="pedigree-screen" hidden>
<header class="pedigree-header">
<button type="button" id="pedigree-back" class="ghost icon-btn" aria-label="Back to tracker" title="Back"></button>
<h1>🌳 Pedigree</h1>
<div class="ped-controls">
<button type="button" id="ped-view" class="ghost">Fan view</button>
<button type="button" id="ped-foldall" class="ghost">Collapse all</button>
<div class="ped-zoom" role="group" aria-label="Zoom">
<button type="button" id="ped-zoom-out" class="ghost icon-btn" aria-label="Zoom out" title="Zoom out"></button>
<button type="button" id="ped-zoom-reset" class="ghost" aria-label="Reset zoom" title="Reset zoom">100%</button>
<button type="button" id="ped-zoom-in" class="ghost icon-btn" aria-label="Zoom in" title="Zoom in">+</button>
</div>
</div>
</header>
<main class="pedigree-main">
<p class="muted-note pedigree-hint">
Your dog's ancestry from <strong>SKK Hunddata</strong>, traced from the
ID set in Settings. The first generations show at once, then the line
fills in further back.
<button type="button" id="pedigree-refresh" class="linklike">Refresh</button>
</p>
<p id="pedigree-status" class="pedigree-status" hidden></p>
<div id="pedigree-choose" class="pedigree-choose" hidden></div>
<div id="pedigree-subject" class="pedigree-subject" hidden></div>
<p id="pedigree-repeat-note" class="muted-note pedigree-repeat-note" hidden>Some ancestors appear in more than one place further back (pedigree collapse). Expand the tree to reveal their ×N badges, then tap one to highlight every spot that dog appears.</p>
<div id="pedigree-tree" class="pedigree-tree"></div>
<p id="pedigree-caption" class="pedigree-caption" hidden></p>
</main>
</div><!-- /#pedigree-screen -->
<dialog id="changelog-dialog">
<form method="dialog" id="changelog-form">
<h3>Changelog</h3>
<ul id="changelog-list" class="changelog-list"></ul>
<p id="changelog-empty" class="empty" hidden>No changelog available.</p>
<menu>
<button value="close">Close</button>
</menu>
</form>
</dialog>
<dialog id="settings-dialog">
<form method="dialog" id="settings-form">
<h3>Puppy settings</h3>
<label>Name
<input type="text" id="settings-name" placeholder="e.g. Rex" autocomplete="off" />
</label>
<label>Birthday
<input type="date" id="settings-birthday" />
</label>
<label>Pedigree ID
<input type="text" id="settings-pedigree" autocomplete="off" spellcheck="false"
placeholder="SKK chip or reg. number (optional)" />
</label>
<p class="settings-hint">Set your dog's SKK chip or registration number to unlock the 🌳 pedigree page.</p>
<label class="toggle-row">
<span>Dark mode</span>
<input type="checkbox" id="settings-theme" role="switch" class="switch" />
</label>
<label class="toggle-row">
<span>Pee/poo confetti 💩</span>
<input type="checkbox" id="settings-confetti" role="switch" class="switch" />
</label>
<hr class="settings-sep" />
<div id="reminders-section" hidden>
<h4 class="settings-subhead">Reminders</h4>
<label class="toggle-row">
<span>Push notifications</span>
<input type="checkbox" id="reminders-enabled" role="switch" class="switch" />
</label>
<p class="settings-hint" id="reminders-hint" hidden></p>
<div id="reminders-rules" hidden></div>
<button type="button" id="reminders-test" class="ghost" hidden>Send a test notification</button>
</div>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="settings-save">Save</button>
</menu>
<hr class="settings-sep" />
<button type="button" id="delete-account-btn" class="danger danger-block">Delete account…</button>
</form>
</dialog>
<dialog id="delete-account-dialog">
<form method="dialog" id="delete-account-form">
<h3>Delete account</h3>
<p class="danger-text">
This permanently deletes your account and <strong>all its data</strong>
every event, photo and your puppy profile. This can't be undone.
</p>
<label>Confirm your password
<input type="password" id="delete-account-password" autocomplete="current-password" />
</label>
<p id="delete-account-error" class="auth-error" hidden></p>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button type="button" id="delete-account-confirm" class="danger">Delete forever</button>
</menu>
</form>
</dialog>
<dialog id="exercise-dialog">
<form method="dialog" id="exercise-form">
<h3 id="exercise-title">Add exercise</h3>
<label>Name
<input type="text" id="exercise-name" placeholder="e.g. Sit" autocomplete="off" />
</label>
<label>How to do it
<textarea id="exercise-note" rows="5" placeholder="Reminder for how to train it, e.g. lure with a treat, mark the moment the butt touches the ground, reward"></textarea>
</label>
<menu>
<!-- type="button" keeps Save the form's default button, so Enter /
the iOS keyboard's "Go" saves instead of silently hitting the
(hidden) Delete button via implicit form submission. -->
<button type="button" value="delete" id="exercise-delete" class="danger" hidden>Delete</button>
<button type="button" value="cancel" class="ghost">Cancel</button>
<button value="save" id="exercise-save">Save</button>
</menu>
</form>
</dialog>
<dialog id="note-dialog">
<form method="dialog" id="note-form">
<h3 id="note-title">Add note</h3>
<label>Time
<div class="time-row">
<input type="date" id="note-date" />
<input type="time" id="note-time" lang="en-GB" />
<button type="button" id="note-time-now" class="ghost">Now</button>
</div>
</label>
<label id="note-weight-field" hidden>Weight (kg)
<input type="number" id="note-weight" inputmode="decimal" step="0.01" min="0" placeholder="e.g. 5.2" />
</label>
<label id="note-grams-field" hidden>Amount (g)
<input type="number" id="note-grams" inputmode="numeric" step="1" min="0" placeholder="e.g. 80 — leave empty if unknown" />
</label>
<label>Note
<textarea id="note-input" rows="4" placeholder="e.g. pee was instant, poo took 5min, ate 300g raw food"></textarea>
</label>
<div class="photo-field">
<input type="file" id="note-photo-input" accept="image/*" multiple hidden />
<button type="button" id="note-photo-btn" class="ghost">📷 Add photos</button>
<div id="note-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="note-save">Save</button>
</menu>
</form>
</dialog>
<dialog id="edit-dialog">
<form method="dialog" id="edit-form">
<h3>Edit event</h3>
<label>Time
<div class="time-row">
<input type="date" id="edit-date" />
<input type="time" id="edit-time" lang="en-GB" />
</div>
</label>
<label id="edit-weight-field" hidden>Weight (kg)
<input type="number" id="edit-weight" inputmode="decimal" step="0.01" min="0" />
</label>
<label id="edit-grams-field" hidden>Amount (g)
<input type="number" id="edit-grams" inputmode="numeric" step="1" min="0" />
</label>
<label>Note
<textarea id="edit-note" rows="4"></textarea>
</label>
<div class="photo-field">
<input type="file" id="edit-photo-input" accept="image/*" multiple hidden />
<button type="button" id="edit-photo-btn" class="ghost">📷 Add photos</button>
<div id="edit-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="delete" id="edit-delete" class="danger">Delete</button>
<button value="cancel" class="ghost">Cancel</button>
<button value="save">Save</button>
</menu>
</form>
</dialog>
<dialog id="lightbox">
<button type="button" id="lightbox-close" class="lightbox-close" aria-label="Close">×</button>
<img id="lightbox-img" alt="" />
</dialog>
<!-- Brief confirmation after a one-tap quick log, with Undo / Add note. -->
<div id="snackbar" class="snackbar" hidden role="status" aria-live="polite">
<span id="snackbar-msg" class="snackbar-msg"></span>
<button type="button" id="snackbar-note" class="snackbar-action">Add note</button>
<button type="button" id="snackbar-undo" class="snackbar-action">Undo</button>
</div>
<script src="app.js"></script>
</body>
</html>