Storybook — Parent Dashboard
A calm, trustworthy parent dashboard for a kids' storybook app, built on the same soft rounded palette with grown-up accent tones. A per-child switcher swaps every figure live: an inline-SVG weekly reading-time bar chart that highlights the peak day, books-finished against a goal, and a current reading streak. A daily time-limit panel pairs a big pill slider with an enforce toggle and a plain-language status line, age-range content filters update a summary, and an easy-read font toggle keeps it accessible.
MCP
Code
:root {
--bg: #fff8ef;
--surface: #ffffff;
--ink: #2c2350;
--ink-soft: #6a6390;
--primary: #ff8a3d;
--secondary: #5ec5d6;
--accent: #ffd23f;
--pink: #ff6f9c;
--green: #7bd389;
--purple: #8a7bf0;
--line: #ece4f5;
--line-strong: #2c2350;
--r: 22px;
--r-sm: 14px;
--r-pill: 999px;
--shadow: 0 14px 30px -16px rgba(44, 35, 80, 0.35);
--shadow-soft: 0 8px 20px -14px rgba(44, 35, 80, 0.4);
--display: "Baloo 2", system-ui, sans-serif;
--body: "Nunito", system-ui, -apple-system, sans-serif;
--gutter: clamp(14px, 3vw, 28px);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--body);
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 520px at 8% -8%, #fff0e0 0%, transparent 60%),
radial-gradient(900px 460px at 102% 4%, #e8f7fa 0%, transparent 58%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
body.easy-read {
--body: "Nunito", Verdana, system-ui, sans-serif;
letter-spacing: 0.03em;
word-spacing: 0.12em;
line-height: 1.75;
}
h1, h2, h3 { font-family: var(--display); margin: 0; }
.skip-link {
position: absolute;
left: 14px;
top: -56px;
z-index: 30;
background: var(--ink);
color: #fff;
padding: 10px 18px;
border-radius: var(--r-pill);
font-weight: 700;
text-decoration: none;
transition: top 0.18s ease;
}
.skip-link:focus { top: 14px; }
.shell {
max-width: 1120px;
margin: 0 auto;
padding: var(--gutter);
}
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
padding: 4px 4px 18px;
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 54px;
height: 54px;
border-radius: 18px;
background: linear-gradient(150deg, #fff, #fff5e7);
border: 2.5px solid var(--line-strong);
box-shadow: var(--shadow-soft);
}
.brand-text { font-family: var(--display); font-weight: 800; font-size: 1.4rem; }
.brand-text em { font-style: normal; color: var(--primary); }
.topbar-controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 48px;
padding: 0 18px;
font-family: var(--body);
font-weight: 700;
font-size: 0.95rem;
color: var(--ink);
background: var(--surface);
border: 2.5px solid var(--line-strong);
border-radius: var(--r-pill);
cursor: pointer;
box-shadow: var(--shadow-soft);
transition: transform 0.12s ease, background 0.15s ease;
}
.chip span[aria-hidden] {
font-family: var(--display);
font-weight: 800;
background: var(--accent);
border-radius: 8px;
padding: 0 6px;
line-height: 1.4;
}
.chip:hover { transform: translateY(-2px); }
.chip:active { transform: translateY(1px); }
.chip[aria-pressed="true"] { background: var(--accent); }
.parent-badge {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 0 16px 0 6px;
background: var(--surface);
border: 2.5px solid var(--line-strong);
border-radius: var(--r-pill);
box-shadow: var(--shadow-soft);
}
.avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--purple);
color: #fff;
font-family: var(--display);
font-weight: 800;
}
.parent-name { font-weight: 700; font-size: 0.92rem; }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--gutter);
}
.panel {
background: var(--surface);
border: 2.5px solid var(--line-strong);
border-radius: var(--r);
padding: clamp(16px, 2.4vw, 24px);
box-shadow: var(--shadow);
}
.children-panel { grid-column: 1 / -1; }
.stats-panel { grid-column: 1 / -1; }
.activity-panel { grid-column: 1 / -1; }
.panel-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.panel-title { font-weight: 800; font-size: clamp(1.05rem, 2.4vw, 1.35rem); }
.panel-sub { margin: 6px 0 14px; color: var(--ink-soft); font-weight: 600; }
.panel-note {
font-weight: 700;
font-size: 0.82rem;
color: var(--ink-soft);
background: var(--bg);
border-radius: var(--r-pill);
padding: 4px 12px;
}
/* ---------- Child switcher ---------- */
.child-switcher {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin: 14px 0 18px;
}
.child-tab {
display: flex;
align-items: center;
gap: 10px;
min-height: 48px;
padding: 6px 18px 6px 8px;
font-family: var(--body);
font-weight: 700;
color: var(--ink);
background: var(--bg);
border: 2.5px solid var(--line-strong);
border-radius: var(--r-pill);
cursor: pointer;
transition: transform 0.12s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.child-tab .tab-face {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: #fff;
border: 2px solid var(--line-strong);
font-size: 1.15rem;
}
.child-tab:hover { transform: translateY(-2px); }
.child-tab[aria-selected="true"] {
background: var(--secondary);
color: var(--ink);
box-shadow: var(--shadow-soft);
}
.child-tab[aria-selected="true"] .tab-face { background: var(--accent); }
.child-hero {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-radius: var(--r-sm);
background: linear-gradient(120deg, #fff5e7, #eafaf3);
border: 2px dashed var(--line);
}
.child-face {
display: grid;
place-items: center;
width: 64px;
height: 64px;
border-radius: 22px;
background: #fff;
border: 2.5px solid var(--line-strong);
font-size: 2rem;
box-shadow: var(--shadow-soft);
}
.child-hero-name { margin: 0; font-family: var(--display); font-weight: 800; font-size: 1.3rem; }
.child-hero-sub { margin: 2px 0 0; color: var(--ink-soft); font-weight: 600; }
/* ---------- Stats ---------- */
.stat-grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: var(--gutter);
}
.stat-stack { display: grid; gap: var(--gutter); }
.stat-card {
position: relative;
border-radius: var(--r-sm);
padding: 18px;
border: 2px solid var(--line);
background: var(--bg);
}
.stat-time { background: linear-gradient(160deg, #fff, #f3effc); }
.stat-books { background: linear-gradient(160deg, #fff, #eafaf3); }
.stat-streak { background: linear-gradient(160deg, #fff, #fff0e8); }
.stat-emoji { font-size: 1.5rem; }
.stat-label { margin: 4px 0 0; font-weight: 700; color: var(--ink-soft); }
.stat-value {
margin: 2px 0 0;
font-family: var(--display);
font-weight: 800;
font-size: clamp(2rem, 6vw, 2.6rem);
line-height: 1;
letter-spacing: -0.02em;
}
.stat-unit { font-size: 0.95rem; color: var(--ink-soft); margin-left: 6px; font-family: var(--body); }
.stat-foot { margin: 8px 0 0; font-weight: 700; font-size: 0.84rem; color: var(--ink-soft); }
/* ---------- Bar chart ---------- */
.chart {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(7, 1fr);
align-items: end;
gap: 8px;
height: 132px;
}
.bar {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
height: 100%;
justify-content: flex-end;
}
.bar-fill {
width: 100%;
max-width: 30px;
border-radius: 10px 10px 6px 6px;
background: linear-gradient(180deg, var(--purple), var(--secondary));
border: 2px solid var(--line-strong);
min-height: 6px;
transition: height 0.5s cubic-bezier(.34,1.56,.64,1);
position: relative;
}
.bar-fill.is-peak { background: linear-gradient(180deg, var(--primary), var(--pink)); }
.bar-fill[data-min]::after {
content: attr(data-min);
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
font-size: 0.68rem;
font-weight: 800;
color: var(--ink-soft);
font-family: var(--body);
}
.bar-day { font-size: 0.72rem; font-weight: 800; color: var(--ink-soft); }
/* ---------- Limit control ---------- */
.limit-toggle-row { margin-bottom: 16px; }
.switch {
display: inline-flex;
align-items: center;
gap: 12px;
cursor: pointer;
font-weight: 700;
user-select: none;
}
.switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.switch-track {
position: relative;
width: 58px;
height: 34px;
border-radius: var(--r-pill);
background: #e3dcef;
border: 2.5px solid var(--line-strong);
transition: background 0.2s ease;
flex: none;
}
.switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 26px;
height: 26px;
border-radius: 50%;
background: #fff;
border: 2px solid var(--line-strong);
transition: transform 0.2s cubic-bezier(.34,1.56,.64,1);
}
.switch input:checked + .switch-track { background: var(--green); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(24px); }
.limit-control { transition: opacity 0.2s ease; }
.limit-control.is-off { opacity: 0.45; }
.limit-readout {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 12px;
}
.limit-value {
font-family: var(--display);
font-weight: 800;
font-size: 2.6rem;
line-height: 1;
color: var(--primary);
}
.limit-suffix { font-weight: 700; color: var(--ink-soft); }
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 16px;
border-radius: var(--r-pill);
background: linear-gradient(90deg, var(--accent), var(--primary));
border: 2.5px solid var(--line-strong);
cursor: pointer;
margin: 6px 0;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 30px;
height: 30px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--line-strong);
box-shadow: var(--shadow-soft);
cursor: grab;
margin-top: -1px;
}
input[type="range"]::-moz-range-thumb {
width: 30px;
height: 30px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--line-strong);
box-shadow: var(--shadow-soft);
cursor: grab;
}
input[type="range"]:active::-webkit-slider-thumb { cursor: grabbing; }
.limit-scale {
display: flex;
justify-content: space-between;
font-weight: 800;
font-size: 0.74rem;
color: var(--ink-soft);
margin-top: 2px;
}
.limit-status {
margin: 14px 0 0;
font-weight: 700;
font-size: 0.9rem;
color: var(--ink);
background: var(--bg);
border-radius: var(--r-sm);
padding: 10px 14px;
border: 2px solid var(--line);
}
/* ---------- Age filters ---------- */
.age-filters { display: flex; gap: 12px; flex-wrap: wrap; }
.age-chip {
min-height: 48px;
padding: 0 18px;
font-family: var(--body);
font-weight: 700;
font-size: 0.95rem;
color: var(--ink);
background: var(--bg);
border: 2.5px solid var(--line-strong);
border-radius: var(--r-pill);
cursor: pointer;
transition: transform 0.12s ease, background 0.15s ease;
}
.age-chip::before {
content: "○ ";
font-weight: 900;
}
.age-chip:hover { transform: translateY(-2px); }
.age-chip:active { transform: translateY(1px); }
.age-chip[aria-pressed="true"] { background: var(--green); }
.age-chip[aria-pressed="true"]::before { content: "● "; }
.filter-summary {
margin: 16px 0 0;
font-weight: 700;
font-size: 0.9rem;
color: var(--ink-soft);
}
/* ---------- Activity list ---------- */
.activity-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.activity-item {
display: flex;
align-items: center;
gap: 14px;
padding: 12px 14px;
border-radius: var(--r-sm);
background: var(--bg);
border: 2px solid var(--line);
animation: pop 0.3s ease;
}
.activity-icon {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 14px;
background: #fff;
border: 2.5px solid var(--line-strong);
font-size: 1.3rem;
flex: none;
}
.activity-body { flex: 1; min-width: 0; }
.activity-title { margin: 0; font-weight: 800; }
.activity-meta { margin: 2px 0 0; color: var(--ink-soft); font-weight: 600; font-size: 0.85rem; }
.activity-tag {
font-family: var(--display);
font-weight: 800;
font-size: 0.78rem;
padding: 4px 12px;
border-radius: var(--r-pill);
border: 2px solid var(--line-strong);
flex: none;
}
.tag-finished { background: var(--green); }
.tag-reading { background: var(--secondary); }
.tag-quiz { background: var(--accent); }
/* ---------- Footnote ---------- */
.footnote {
margin-top: var(--gutter);
text-align: center;
color: var(--ink-soft);
font-weight: 600;
font-size: 0.85rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-weight: 700;
padding: 12px 22px;
border-radius: var(--r-pill);
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: transform 0.25s ease, opacity 0.25s ease;
z-index: 40;
max-width: calc(100vw - 32px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Focus ---------- */
:focus-visible {
outline: 3px solid var(--purple);
outline-offset: 3px;
border-radius: 6px;
}
/* ---------- Motion ---------- */
@keyframes pop {
from { transform: scale(0.96); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes wiggle {
0%, 100% { transform: rotate(0); }
25% { transform: rotate(-8deg) scale(1.08); }
75% { transform: rotate(8deg) scale(1.08); }
}
.wiggle { animation: wiggle 0.45s ease; }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.panel:not(.children-panel):not(.stats-panel):not(.activity-panel) { grid-column: 1 / -1; }
}
@media (max-width: 620px) {
.stat-grid { grid-template-columns: 1fr; }
.stat-stack { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 420px) {
.stat-stack { grid-template-columns: 1fr; }
.child-tab .tab-face { width: 32px; height: 32px; }
.chart { height: 110px; gap: 5px; }
.topbar { gap: 10px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- Data (fictional) ---------- */
var DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
var CHILDREN = [
{
id: "mila",
name: "Mila",
face: "🦊",
age: "Age 6 · Ages 6–8 reader",
week: [22, 30, 15, 40, 28, 52, 35],
books: 5,
goal: 8,
streak: 6,
best: 9,
recommendedAge: "6-8",
activity: [
{ icon: "📖", title: "The Little Lantern Fox", meta: "Finished · 12 spreads", tag: "finished" },
{ icon: "🦉", title: "Owl School Adventures", meta: "Read 18 min today", tag: "reading" },
{ icon: "⭐", title: "Forest Friends quiz", meta: "Scored 4 of 5 stars", tag: "quiz" },
{ icon: "🌙", title: "Goodnight Moonbeam", meta: "Read 11 min · last night", tag: "reading" }
]
},
{
id: "theo",
name: "Theo",
face: "🐻",
age: "Age 4 · Ages 3–5 reader",
week: [12, 8, 18, 10, 20, 14, 9],
books: 9,
goal: 8,
streak: 11,
best: 11,
recommendedAge: "3-5",
activity: [
{ icon: "🚂", title: "Choo-Choo Counting", meta: "Finished · 8 pages", tag: "finished" },
{ icon: "🐶", title: "Puppy's Rainy Day", meta: "Read 9 min today", tag: "reading" },
{ icon: "🎨", title: "Colors with Coco", meta: "Finished · 10 pages", tag: "finished" },
{ icon: "⭐", title: "Shapes quiz", meta: "Scored 5 of 5 stars", tag: "quiz" }
]
},
{
id: "ada",
name: "Ada",
face: "🐱",
age: "Age 10 · Ages 9–12 reader",
week: [45, 38, 60, 50, 42, 70, 55],
books: 7,
goal: 8,
streak: 3,
best: 14,
recommendedAge: "9-12",
activity: [
{ icon: "🚀", title: "Comet Riders: Book 2", meta: "Read 40 min today", tag: "reading" },
{ icon: "🗺️", title: "Atlas of Tiny Kingdoms", meta: "Finished · 24 chapters", tag: "finished" },
{ icon: "⭐", title: "Mythical Beasts quiz", meta: "Scored 5 of 5 stars", tag: "quiz" },
{ icon: "🔍", title: "The Clockwork Mystery", meta: "Read 22 min · yesterday", tag: "reading" }
]
}
];
/* ---------- State ---------- */
var state = {
childId: CHILDREN[0].id,
limit: 45,
limitOn: true,
ages: { "3-5": true, "6-8": true, "9-12": false }
};
/* ---------- Helpers ---------- */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function currentChild() {
return CHILDREN.filter(function (c) { return c.id === state.childId; })[0];
}
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2400);
}
/* ---------- Build child switcher ---------- */
function buildSwitcher() {
var wrap = $("#childSwitcher");
wrap.innerHTML = "";
CHILDREN.forEach(function (c, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "child-tab";
btn.setAttribute("role", "tab");
btn.id = "tab-" + c.id;
btn.setAttribute("aria-selected", c.id === state.childId ? "true" : "false");
btn.tabIndex = c.id === state.childId ? 0 : -1;
btn.innerHTML = '<span class="tab-face" aria-hidden="true">' + c.face + "</span>" +
"<span>" + c.name + "</span>";
btn.addEventListener("click", function () { selectChild(c.id); });
btn.addEventListener("keydown", function (e) {
var idx = i;
if (e.key === "ArrowRight" || e.key === "ArrowDown") { idx = (i + 1) % CHILDREN.length; }
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { idx = (i - 1 + CHILDREN.length) % CHILDREN.length; }
else { return; }
e.preventDefault();
selectChild(CHILDREN[idx].id);
var next = $("#tab-" + CHILDREN[idx].id);
if (next) next.focus();
});
wrap.appendChild(btn);
});
}
function selectChild(id) {
if (id === state.childId) return;
state.childId = id;
var c = currentChild();
// auto-tune the recommended age filter for the chosen child
syncSwitcherSelection();
renderChild();
toast("Now viewing " + c.name + "'s reading");
}
function syncSwitcherSelection() {
var tabs = document.querySelectorAll(".child-tab");
Array.prototype.forEach.call(tabs, function (t) {
var on = t.id === "tab-" + state.childId;
t.setAttribute("aria-selected", on ? "true" : "false");
t.tabIndex = on ? 0 : -1;
});
}
/* ---------- Render child stats ---------- */
function renderChild() {
var c = currentChild();
$("#childFace").textContent = c.face;
$("#childName").textContent = c.name;
$("#childSub").textContent = c.age;
// weekly totals
var total = c.week.reduce(function (a, b) { return a + b; }, 0);
var avg = Math.round(total / c.week.length);
$("#weekTotal").textContent = total;
$("#weekAvg").textContent = "~" + avg + " min/day";
// books + streak
$("#booksFinished").textContent = c.books;
$("#booksGoal").textContent = "of " + c.goal + " goal";
$("#streak").textContent = c.streak;
$("#streakBest").textContent = "best " + c.best + " days";
renderChart(c);
renderActivity(c);
updateLimitStatus();
}
function renderChart(c) {
var chart = $("#chart");
chart.innerHTML = "";
var max = Math.max.apply(null, c.week);
var peak = c.week.indexOf(max);
c.week.forEach(function (min, i) {
var pct = max ? Math.round((min / max) * 100) : 0;
var cell = document.createElement("div");
cell.className = "bar";
var fill = document.createElement("div");
fill.className = "bar-fill" + (i === peak ? " is-peak" : "");
fill.style.height = Math.max(pct, 6) + "%";
fill.setAttribute("data-min", min);
var label = document.createElement("span");
label.className = "bar-day";
label.textContent = DAYS[i];
cell.appendChild(fill);
cell.appendChild(label);
chart.appendChild(cell);
});
chart.setAttribute(
"aria-label",
c.name + "'s daily reading minutes: " +
DAYS.map(function (d, i) { return d + " " + c.week[i]; }).join(", ")
);
}
function renderActivity(c) {
var list = $("#activityList");
list.innerHTML = "";
// only show activity that fits the enabled age filters' "vibe":
// we keep all items but flag the recommended-age match in the count line.
c.activity.forEach(function (a) {
var li = document.createElement("li");
li.className = "activity-item";
var tagClass = a.tag === "finished" ? "tag-finished" : a.tag === "quiz" ? "tag-quiz" : "tag-reading";
var tagText = a.tag === "finished" ? "Finished" : a.tag === "quiz" ? "Quiz" : "Reading";
li.innerHTML =
'<span class="activity-icon" aria-hidden="true">' + a.icon + "</span>" +
'<div class="activity-body">' +
'<p class="activity-title">' + a.title + "</p>" +
'<p class="activity-meta">' + a.meta + "</p>" +
"</div>" +
'<span class="activity-tag ' + tagClass + '">' + tagText + "</span>";
list.appendChild(li);
// playful tap wiggle on the icon
var icon = li.querySelector(".activity-icon");
icon.addEventListener("click", function () {
icon.classList.remove("wiggle");
void icon.offsetWidth;
icon.classList.add("wiggle");
});
});
$("#activityCount").textContent = c.activity.length + " entries";
}
/* ---------- Limit control ---------- */
var slider = $("#limitSlider");
var limitToggle = $("#limitToggle");
var limitControl = $("#limitControl");
function updateLimitStatus() {
var c = currentChild();
var todayMin = c.week[c.week.length - 1]; // Sun as "today"
var valEl = $("#limitValue");
valEl.textContent = state.limit;
$("#limitSlider").setAttribute("aria-valuetext", state.limit + " minutes");
var status = $("#limitStatus");
if (!state.limitOn) {
limitControl.classList.add("is-off");
status.textContent = "Limit is off — " + c.name + " can read with no daily cap.";
return;
}
limitControl.classList.remove("is-off");
var left = state.limit - todayMin;
if (left > 0) {
status.textContent = c.name + " has read " + todayMin + " of " + state.limit +
" min today — " + left + " min left.";
} else if (left === 0) {
status.textContent = c.name + " has reached today's " + state.limit + " min limit. 🎉";
} else {
status.textContent = "Heads up: " + c.name + " read " + todayMin +
" min, over the " + state.limit + " min limit.";
}
}
slider.addEventListener("input", function () {
state.limit = parseInt(slider.value, 10);
$("#limitValue").textContent = state.limit;
updateLimitStatus();
});
slider.addEventListener("change", function () {
toast("Daily limit set to " + state.limit + " min");
});
limitToggle.addEventListener("change", function () {
state.limitOn = limitToggle.checked;
slider.disabled = !state.limitOn;
updateLimitStatus();
toast(state.limitOn ? "Daily limit turned on" : "Daily limit turned off");
});
/* ---------- Age filters ---------- */
var ageButtons = document.querySelectorAll(".age-chip");
function updateFilterSummary() {
var on = [];
Array.prototype.forEach.call(ageButtons, function (b) {
var a = b.getAttribute("data-age");
state.ages[a] = b.getAttribute("aria-pressed") === "true";
if (state.ages[a]) on.push(b.textContent.replace("Ages ", ""));
});
var summary = $("#filterSummary");
if (on.length === 0) {
summary.textContent = "No age range selected — Storyleaf will hide all stories.";
} else {
summary.textContent = "Showing stories for ages " + on.join(", ") + ".";
}
}
Array.prototype.forEach.call(ageButtons, function (b) {
b.addEventListener("click", function () {
var pressed = b.getAttribute("aria-pressed") === "true";
b.setAttribute("aria-pressed", pressed ? "false" : "true");
updateFilterSummary();
var a = b.getAttribute("data-age");
toast((!pressed ? "Enabled" : "Disabled") + " ages " + a);
});
});
/* ---------- Easy-read font toggle ---------- */
var fontToggle = $("#fontToggle");
fontToggle.addEventListener("click", function () {
var on = document.body.classList.toggle("easy-read");
fontToggle.setAttribute("aria-pressed", on ? "true" : "false");
toast(on ? "Easy-read font on" : "Easy-read font off");
});
/* ---------- Init ---------- */
buildSwitcher();
renderChild();
state.limit = parseInt(slider.value, 10);
updateLimitStatus();
updateFilterSummary();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Storybook — Parent Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700;800&family=Nunito:ital,wght@0,400;0,600;0,700;0,800;1,600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to dashboard</a>
<div class="shell">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 48 48" width="34" height="34">
<path d="M6 10c6-3 12-3 18 0 6-3 12-3 18 0v28c-6-3-12-3-18 0-6-3-12-3-18 0Z" fill="#fff" stroke="#2c2350" stroke-width="2.5" stroke-linejoin="round"/>
<path d="M24 10v28" stroke="#2c2350" stroke-width="2.5"/>
<path d="M10 17h9M10 23h9M29 17h9M29 23h9" stroke="#5ec5d6" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="24" cy="6" r="3" fill="#ffd23f" stroke="#2c2350" stroke-width="2"/>
</svg>
</span>
<span class="brand-text">Storyleaf <em>Family</em></span>
</div>
<div class="topbar-controls">
<button id="fontToggle" class="chip" type="button" aria-pressed="false">
<span aria-hidden="true">Aa</span> Easy-read font
</button>
<div class="parent-badge">
<span class="avatar" aria-hidden="true">P</span>
<span class="parent-name">Parent view</span>
</div>
</div>
</header>
<main id="main" class="layout">
<section class="panel children-panel" aria-labelledby="children-h">
<h2 id="children-h" class="panel-title">Who are we checking on?</h2>
<div id="childSwitcher" class="child-switcher" role="tablist" aria-label="Choose a child">
<!-- child buttons injected by JS -->
</div>
<div class="child-hero">
<span id="childFace" class="child-face" aria-hidden="true">🦊</span>
<div class="child-hero-text">
<p class="child-hero-name" id="childName">—</p>
<p class="child-hero-sub" id="childSub">—</p>
</div>
</div>
</section>
<section class="panel stats-panel" aria-labelledby="stats-h">
<div class="panel-head">
<h2 id="stats-h" class="panel-title">This week at a glance</h2>
<span class="panel-note" id="weekRange">Mon 8 – Sun 14 Jun</span>
</div>
<div class="stat-grid">
<article class="stat-card stat-time">
<p class="stat-label">Reading time</p>
<p class="stat-value"><span id="weekTotal">0</span><span class="stat-unit">min</span></p>
<p class="stat-foot" id="weekAvg">~0 min/day</p>
<div class="chart" role="img" aria-label="Daily reading minutes for the week" id="chart">
<!-- bars injected by JS -->
</div>
</article>
<div class="stat-stack">
<article class="stat-card stat-books">
<span class="stat-emoji" aria-hidden="true">📚</span>
<p class="stat-value"><span id="booksFinished">0</span></p>
<p class="stat-label">Books finished</p>
<p class="stat-foot" id="booksGoal">of 8 goal</p>
</article>
<article class="stat-card stat-streak">
<span class="stat-emoji" aria-hidden="true">🔥</span>
<p class="stat-value"><span id="streak">0</span><span class="stat-unit">days</span></p>
<p class="stat-label">Current streak</p>
<p class="stat-foot" id="streakBest">best 0 days</p>
</article>
</div>
</div>
</section>
<section class="panel limit-panel" aria-labelledby="limit-h">
<h2 id="limit-h" class="panel-title">Daily reading time limit</h2>
<div class="limit-toggle-row">
<label class="switch" for="limitToggle">
<input type="checkbox" id="limitToggle" checked />
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
<span class="switch-text">Enforce a daily limit</span>
</label>
</div>
<div class="limit-control" id="limitControl">
<div class="limit-readout">
<span class="limit-value" id="limitValue">45</span>
<span class="limit-suffix">minutes / day</span>
</div>
<input
type="range"
id="limitSlider"
min="15" max="120" step="5" value="45"
aria-label="Daily reading time limit in minutes"
/>
<div class="limit-scale" aria-hidden="true">
<span>15</span><span>60</span><span>120</span>
</div>
<p class="limit-status" id="limitStatus">—</p>
</div>
</section>
<section class="panel filter-panel" aria-labelledby="filter-h">
<h2 id="filter-h" class="panel-title">Content filters</h2>
<p class="panel-sub">Pick the age range Storyleaf may show.</p>
<div class="age-filters" id="ageFilters" role="group" aria-label="Age range filters">
<button class="age-chip" type="button" data-age="3-5" aria-pressed="true">Ages 3–5</button>
<button class="age-chip" type="button" data-age="6-8" aria-pressed="true">Ages 6–8</button>
<button class="age-chip" type="button" data-age="9-12" aria-pressed="false">Ages 9–12</button>
</div>
<p class="filter-summary" id="filterSummary">—</p>
</section>
<section class="panel activity-panel" aria-labelledby="activity-h">
<div class="panel-head">
<h2 id="activity-h" class="panel-title">Recent activity</h2>
<span class="panel-note" id="activityCount">0 entries</span>
</div>
<ul class="activity-list" id="activityList">
<!-- activity injected by JS -->
</ul>
</section>
</main>
<footer class="footnote">
<p>Illustrative kids' UI only — fictional stories, characters & audio. Settings are not saved.</p>
</footer>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Parent Dashboard
A grown-up control panel for the Storyleaf kids’ reading app that keeps the friendly rounded look but dials the palette toward calmer, trustworthy tones. A per-child switcher (Mila, Theo and Ada) sits up top as a tab list; choosing a child swaps every number on the page — the weekly reading-time chart, books finished against a goal, the current streak and the recent-activity feed all update at once. The chart is drawn as a pure CSS bar grid (no images), labels each day’s minutes, and tints the busiest day in a warm accent.
The daily time-limit card pairs a large pill slider (15–120 min) with an enforce toggle. Dragging the slider updates the big readout live, and a plain-language status line does the math against today’s minutes — telling you how long is left, when the cap is reached, or if a child went over. Flipping the toggle off dims the control and switches the message to an unrestricted state, so the rule is always legible at a glance.
Content filters let you pick which age ranges Storyleaf may show (3–5, 6–8, 9–12); toggling a chip rewrites a one-line summary and warns when nothing is selected. Every control is keyboard-friendly with arrow-key navigation across the child tabs and visible focus rings, the recent-activity icons give a happy wiggle on tap, an easy-read font toggle loosens spacing for young or dyslexic readers, all motion respects prefers-reduced-motion, and the two-column layout collapses to a single stacked column down to 360px.
Illustrative kids’ UI only — fictional stories, characters, and audio.