Every task now has a duration (10 min default) and a child-initiated start: a play icon on the kiosk, alongside the checkbox, begins a server-anchored countdown for that specific task. Deliberately not a beep-every-minute alarm -- audio is milestone-only (start/halfway/almost-done/time's-up), since frequent interrupting alerts can backfire for ADHD kids rather than help. Countdown is drift-free (recomputed each tick from the fixed started_at anchor, never decremented), audio is synthesized client-side via Web Audio (zero external assets, matching this app's existing hand-rolled-PNG-icon philosophy), and the child's own start tap is what unlocks audio playback for the rest of the session on iOS. Two real bugs caught and fixed during design, before they shipped: - Marking a task done now clears started_at (both the kiosk and parent routes) -- without this, unchecking a finished task later would resurrect a stale timer and instantly show "time's up" on a task the child hasn't touched today. - The parent editor's poll-pause guard only covered contenteditable fields; a plain number input for duration would have been silently unprotected from being clobbered by an incoming 4s poll mid-edit. Extended the guard to cover it, verified live that the DOM and focus both survive a poll while the field is focused. Schema: calendar_tasks gains duration_minutes (NOT NULL DEFAULT 10) and started_at (nullable). Verified against a simulated copy of the real production schema/data that the static DEFAULT backfills every existing row automatically (no business-logic backfill needed, unlike is_admin), and that repeated boots stay idempotent. Duplicate-calendar carries durations forward but always resets started_at, verified end-to-end. Kiosk countdown/milestone-firing verified live in-browser end-to-end: real timer start, halfway/almost-done/done firing at the correct proportional thresholds (not fixed minutes, so it scales from ~1min to 20+min tasks), and a mid-countdown page reload resuming at the correct remaining time with already-passed milestones backfilled silently rather than replayed.
470 lines
10 KiB
CSS
470 lines
10 KiB
CSS
:root{
|
|
--bg: #F1F5FB;
|
|
--card: #FFFFFF;
|
|
--ink: #2E3446;
|
|
--ink-soft: #6B7280;
|
|
--line: #E3E8F0;
|
|
|
|
--morning: #FF8C64;
|
|
--morning-bg: #FFF0E7;
|
|
--school: #4A90D9;
|
|
--school-bg: #E9F2FC;
|
|
--after: #4FAE72;
|
|
--after-bg: #E9F7EE;
|
|
--evening: #8B7FD1;
|
|
--evening-bg: #F0EDFB;
|
|
|
|
--today-ring: #FFC94A;
|
|
--danger: #D14343;
|
|
}
|
|
|
|
*{ box-sizing: border-box; }
|
|
|
|
body{
|
|
margin:0;
|
|
background: var(--bg);
|
|
font-family: 'Nunito', sans-serif;
|
|
color: var(--ink);
|
|
padding: 24px 16px 60px;
|
|
}
|
|
|
|
a{ color: var(--school); }
|
|
|
|
h1{
|
|
font-family: 'Baloo 2', sans-serif;
|
|
font-size: 2rem;
|
|
margin: 0;
|
|
color: var(--ink);
|
|
}
|
|
h1 span{ color: var(--morning); }
|
|
|
|
header{
|
|
max-width: 1400px;
|
|
margin: 0 auto 20px;
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.subtitle{
|
|
font-size: 0.95rem;
|
|
color: var(--ink-soft);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.toolbar{
|
|
display:flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
button.tool, a.tool{
|
|
font-family: 'Nunito', sans-serif;
|
|
font-weight: 800;
|
|
font-size: 0.85rem;
|
|
border: none;
|
|
border-radius: 999px;
|
|
padding: 10px 18px;
|
|
cursor: pointer;
|
|
background: var(--card);
|
|
color: var(--ink);
|
|
border: 2px solid var(--line);
|
|
transition: transform .12s ease, background .12s ease;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
}
|
|
button.tool:hover, a.tool:hover{ transform: translateY(-1px); background: #F7F9FC; }
|
|
button.tool.primary, a.tool.primary{ background: var(--school); color: white; border-color: var(--school); }
|
|
button.tool.primary:hover, a.tool.primary:hover{ background: #3d7fc4; }
|
|
button.tool.danger{ background: var(--card); color: var(--danger); border-color: #F3D0D0; }
|
|
button.tool.danger:hover{ background: #FDF1F1; }
|
|
button.tool:disabled{ opacity: .5; cursor: default; transform:none; }
|
|
|
|
.board{
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 14px;
|
|
}
|
|
|
|
.weekend-toggle-wrap{
|
|
max-width: 1400px;
|
|
margin: 14px auto 0;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 14px;
|
|
}
|
|
|
|
.day-card{
|
|
background: var(--card);
|
|
border-radius: 20px;
|
|
padding: 14px;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap: 10px;
|
|
border: 3px solid transparent;
|
|
box-shadow: 0 2px 10px rgba(46,52,70,0.06);
|
|
}
|
|
|
|
.day-card.today{
|
|
border-color: var(--today-ring);
|
|
box-shadow: 0 4px 18px rgba(255,201,74,0.35);
|
|
}
|
|
|
|
.day-head{
|
|
display:flex;
|
|
align-items:baseline;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.day-name{
|
|
font-family: 'Baloo 2', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 1.15rem;
|
|
}
|
|
|
|
.today-badge{
|
|
font-size: 0.65rem;
|
|
font-weight: 800;
|
|
background: var(--today-ring);
|
|
color: #6B4A00;
|
|
padding: 3px 9px;
|
|
border-radius: 999px;
|
|
letter-spacing: .03em;
|
|
}
|
|
|
|
.block{
|
|
border-radius: 14px;
|
|
padding: 10px 10px 8px;
|
|
}
|
|
|
|
.block.morning{ background: var(--morning-bg); }
|
|
.block.school{ background: var(--school-bg); }
|
|
.block.after{ background: var(--after-bg); }
|
|
.block.evening{ background: var(--evening-bg); }
|
|
|
|
.block-title{
|
|
font-family: 'Baloo 2', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 0.85rem;
|
|
display:flex;
|
|
align-items:center;
|
|
gap: 6px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.morning .block-title{ color: #C0501F; }
|
|
.school .block-title{ color: #2C6BAF; }
|
|
.after .block-title{ color: #2D7B4C; }
|
|
.evening .block-title{ color: #5F51B0; }
|
|
|
|
.task{
|
|
display:flex;
|
|
align-items:flex-start;
|
|
gap: 8px;
|
|
padding: 5px 4px;
|
|
border-radius: 8px;
|
|
}
|
|
.task:hover{ background: rgba(255,255,255,0.6); }
|
|
.task:hover .del{ opacity: 1; }
|
|
|
|
.task input[type=checkbox]{
|
|
margin-top: 3px;
|
|
width: 18px;
|
|
height: 18px;
|
|
flex: none;
|
|
accent-color: var(--ink);
|
|
cursor: pointer;
|
|
}
|
|
.task.kiosk input[type=checkbox]{
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.task-text{
|
|
flex: 1;
|
|
font-size: 0.92rem;
|
|
font-weight: 700;
|
|
outline: none;
|
|
line-height: 1.25;
|
|
padding: 1px 2px;
|
|
border-radius: 4px;
|
|
}
|
|
.task-text:focus{ background: rgba(255,255,255,0.85); }
|
|
.task.done .task-text{
|
|
text-decoration: line-through;
|
|
opacity: 0.45;
|
|
}
|
|
|
|
.del{
|
|
opacity: 0;
|
|
border: none;
|
|
background: none;
|
|
color: var(--ink-soft);
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
font-size: 0.85rem;
|
|
padding: 0 3px;
|
|
transition: opacity .12s ease;
|
|
}
|
|
.del:hover{ color: var(--danger); }
|
|
|
|
.duration-input{
|
|
width: 34px;
|
|
border: none;
|
|
background: none;
|
|
color: var(--ink-soft);
|
|
font-family: 'Nunito', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 0.75rem;
|
|
text-align: right;
|
|
padding: 0 2px;
|
|
opacity: 0.55;
|
|
flex: none;
|
|
-moz-appearance: textfield;
|
|
}
|
|
.duration-input::-webkit-outer-spin-button,
|
|
.duration-input::-webkit-inner-spin-button{ -webkit-appearance: none; margin: 0; }
|
|
.duration-input:hover, .duration-input:focus{ opacity: 1; background: rgba(255,255,255,0.7); border-radius: 4px; }
|
|
|
|
.add-task{
|
|
background: none;
|
|
border: none;
|
|
color: var(--ink-soft);
|
|
font-size: 0.82rem;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
padding: 4px 4px 2px;
|
|
opacity: 0.75;
|
|
}
|
|
.add-task:hover{ opacity: 1; text-decoration: underline; }
|
|
|
|
.legend{
|
|
max-width: 1400px;
|
|
margin: 22px auto 0;
|
|
display:flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
font-size: 0.82rem;
|
|
color: var(--ink-soft);
|
|
align-items:center;
|
|
}
|
|
.legend .dot{
|
|
display:inline-block;
|
|
width: 10px; height:10px;
|
|
border-radius: 50%;
|
|
margin-right: 5px;
|
|
}
|
|
.legend .item{ display:flex; align-items:center; }
|
|
|
|
.hint{
|
|
max-width: 1400px;
|
|
margin: 6px auto 0;
|
|
font-size: 0.78rem;
|
|
color: var(--ink-soft);
|
|
}
|
|
|
|
@media (max-width: 1100px){
|
|
.board{ grid-template-columns: repeat(2, 1fr); }
|
|
.weekend-toggle-wrap{ grid-template-columns: 1fr; }
|
|
}
|
|
@media (max-width: 600px){
|
|
.board{ grid-template-columns: 1fr; }
|
|
}
|
|
|
|
@media print{
|
|
body{ background: white; padding: 0; }
|
|
.toolbar, .add-task, .del, .hint, .period-toggles, nav.crumbs{ display:none !important; }
|
|
.board{ grid-template-columns: repeat(5, 1fr); gap: 8px; }
|
|
.day-card{ box-shadow:none; border: 2px solid var(--line) !important; break-inside: avoid; }
|
|
.weekend-toggle-wrap{ margin-top: 8px; grid-template-columns: repeat(2,1fr); }
|
|
}
|
|
|
|
#weekendWrap.hidden{ display:none; }
|
|
|
|
body.hide-morning .block.morning{ display:none; }
|
|
body.hide-school .block.school{ display:none; }
|
|
body.hide-after .block.after{ display:none; }
|
|
body.hide-evening .block.evening{ display:none; }
|
|
|
|
[contenteditable="true"]{
|
|
outline: none;
|
|
border-radius: 4px;
|
|
padding: 0 2px;
|
|
}
|
|
[contenteditable="true"]:focus{ background: rgba(255,255,255,0.7); }
|
|
|
|
.period-toggles{
|
|
max-width: 1400px;
|
|
margin: 0 auto 18px;
|
|
display:flex;
|
|
gap: 18px;
|
|
flex-wrap: wrap;
|
|
align-items:center;
|
|
background: var(--card);
|
|
border: 2px solid var(--line);
|
|
border-radius: 14px;
|
|
padding: 10px 16px;
|
|
}
|
|
.period-toggles .label{
|
|
font-size: 0.8rem;
|
|
font-weight: 800;
|
|
color: var(--ink-soft);
|
|
}
|
|
.period-toggles label.opt{
|
|
display:flex;
|
|
align-items:center;
|
|
gap: 6px;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
}
|
|
.period-toggles input[type=checkbox]{
|
|
width: 16px; height:16px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.block-title .icon{ margin-right: 2px; }
|
|
.block-title .label{
|
|
outline: none;
|
|
border-radius: 4px;
|
|
padding: 0 2px;
|
|
}
|
|
.block-title .label:focus{ background: rgba(255,255,255,0.75); }
|
|
|
|
/* --- App chrome shared across auth/dashboard pages --- */
|
|
|
|
nav.crumbs{
|
|
max-width: 1400px;
|
|
margin: 0 auto 14px;
|
|
font-size: 0.85rem;
|
|
color: var(--ink-soft);
|
|
}
|
|
nav.crumbs a{ text-decoration: none; font-weight: 700; }
|
|
|
|
.auth-wrap{
|
|
max-width: 420px;
|
|
margin: 60px auto;
|
|
background: var(--card);
|
|
border-radius: 20px;
|
|
padding: 32px;
|
|
box-shadow: 0 2px 10px rgba(46,52,70,0.06);
|
|
}
|
|
.auth-wrap h1{ font-size: 1.5rem; margin-bottom: 4px; }
|
|
.auth-wrap p.lead{ color: var(--ink-soft); margin-top: 0; margin-bottom: 20px; font-size: 0.9rem; }
|
|
|
|
.field{ margin-bottom: 14px; display:flex; flex-direction:column; gap: 6px; }
|
|
.field label{ font-size: 0.82rem; font-weight: 800; color: var(--ink-soft); }
|
|
.field input{
|
|
font-family: 'Nunito', sans-serif;
|
|
font-size: 0.95rem;
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
border: 2px solid var(--line);
|
|
outline: none;
|
|
}
|
|
.field input:focus{ border-color: var(--school); }
|
|
|
|
.error-msg{
|
|
background: #FDF1F1;
|
|
border: 2px solid #F3D0D0;
|
|
color: var(--danger);
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.form-actions{ margin-top: 18px; display:flex; flex-direction:column; gap: 10px; }
|
|
.form-actions button.tool{ width: 100%; text-align:center; }
|
|
.switch-link{ font-size: 0.85rem; color: var(--ink-soft); text-align:center; }
|
|
|
|
.section{
|
|
max-width: 1400px;
|
|
margin: 0 auto 28px;
|
|
}
|
|
.section h2{
|
|
font-family: 'Baloo 2', sans-serif;
|
|
font-size: 1.3rem;
|
|
margin: 0 0 12px;
|
|
}
|
|
|
|
.card-grid{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
.entity-card{
|
|
background: var(--card);
|
|
border-radius: 16px;
|
|
padding: 16px;
|
|
border: 2px solid var(--line);
|
|
display:flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.entity-card h3{ margin: 0; font-family: 'Baloo 2', sans-serif; font-size: 1.1rem; }
|
|
.entity-card .meta{ font-size: 0.8rem; color: var(--ink-soft); }
|
|
.entity-card .row{ display:flex; gap: 8px; flex-wrap: wrap; }
|
|
.entity-card .row button.tool, .entity-card .row a.tool{ padding: 7px 12px; font-size: 0.78rem; }
|
|
|
|
.empty-state{
|
|
color: var(--ink-soft);
|
|
font-size: 0.9rem;
|
|
padding: 20px;
|
|
text-align: center;
|
|
background: var(--card);
|
|
border: 2px dashed var(--line);
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.kiosk-lock-note{
|
|
max-width: 1400px;
|
|
margin: 0 auto 14px;
|
|
font-size: 0.85rem;
|
|
color: var(--ink-soft);
|
|
display:flex;
|
|
align-items:center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.modal-backdrop{
|
|
position: fixed; inset: 0;
|
|
background: rgba(46,52,70,0.35);
|
|
display:flex; align-items:center; justify-content:center;
|
|
padding: 20px;
|
|
z-index: 50;
|
|
}
|
|
.modal-backdrop.hidden{ display:none; }
|
|
.modal{
|
|
background: var(--card);
|
|
border-radius: 20px;
|
|
padding: 24px;
|
|
max-width: 420px;
|
|
width: 100%;
|
|
}
|
|
.modal h2{ margin-top:0; font-family:'Baloo 2', sans-serif; }
|
|
.modal .form-actions{ flex-direction: row; }
|
|
.modal .form-actions button{ flex: 1; }
|
|
|
|
.toast{
|
|
position: fixed;
|
|
bottom: 20px; left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--ink);
|
|
color: white;
|
|
padding: 10px 18px;
|
|
border-radius: 999px;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity .2s ease;
|
|
z-index: 100;
|
|
}
|
|
.toast.show{ opacity: 1; }
|