Mobile ux fixes #1

Merged
iamdoubz merged 11 commits from mobile-ux-fixes into main 2026-11-05 22:08:11 -06:00
11 changed files with 53 additions and 21 deletions

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>ParentCalendar</title>
<meta name="description" content="Scan paper school calendars and lunch menus into your calendar." />
<meta name="theme-color" content="#f4f5f9" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0e1015" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#f4f5f9" media="(prefers-color-scheme: light)" data-for="light" />
<meta name="theme-color" content="#0e1015" media="(prefers-color-scheme: dark)" data-for="dark" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="ParentCalendar" />
<link rel="manifest" href="/manifest.webmanifest" />
+6 -2
View File
@@ -91,7 +91,8 @@
main {
max-width: 1120px;
margin: 0 auto;
padding: var(--s5) var(--gutter) calc(var(--nav-h) + var(--s6) + env(safe-area-inset-bottom));
padding: var(--s5) max(var(--gutter), env(safe-area-inset-right)) calc(var(--nav-h) + var(--s6) + env(safe-area-inset-bottom))
max(var(--gutter), env(safe-area-inset-left)); /* landscape notch */
outline: none;
}
@@ -116,7 +117,7 @@
display: grid; place-items: center; width: 56px; height: 30px; border-radius: var(--r-full);
transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
nav a:hover .pill { background: var(--surface-2); }
@media (hover: hover) { nav a:hover .pill { background: var(--surface-2); } }
nav a[aria-current='page'] { color: var(--primary-text); }
nav a[aria-current='page'] .pill { background: var(--primary-soft); }
nav a:focus-visible { outline: none; }
@@ -141,6 +142,9 @@
width: max-content; max-width: calc(100vw - 2 * var(--gutter));
}
@media (min-width: 1024px) { .toast-region { bottom: var(--s5); } }
/* On the review screen, sit above the sticky Publish bar instead of covering it. */
:global(body:has(.publish-bar)) .toast-region { bottom: calc(var(--nav-h) + 88px + env(safe-area-inset-bottom)); }
@media (min-width: 1024px) { :global(body:has(.publish-bar)) .toast-region { bottom: 96px; } }
.toast {
display: flex; align-items: center; gap: var(--s2);
padding: var(--s3) var(--s4); border-radius: var(--r-md);
+13 -3
View File
@@ -149,6 +149,11 @@ legend { padding: 0; }
clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
/* Phones: drop a button's text label to save room, but keep it for screen readers. */
@media (max-width: 420px) {
.hide-sm { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; }
}
/* ---------- Layout helpers ---------- */
.stack { display: flex; flex-direction: column; gap: var(--s3); }
.stack-lg { display: flex; flex-direction: column; gap: var(--s5); }
@@ -191,20 +196,25 @@ button, .btn {
transform var(--dur-out) var(--ease), box-shadow var(--dur) var(--ease);
-webkit-tap-highlight-color: transparent;
}
button:hover:not(:disabled), .btn:hover { background: var(--surface-2); }
/* Hover only where a pointer hovers; on phones it would stick after a tap. */
@media (hover: hover) {
button:hover:not(:disabled), .btn:hover { background: var(--surface-2); }
button.primary:hover:not(:disabled), .btn.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
button.ghost:hover:not(:disabled), .btn.ghost:hover { background: var(--surface-2); }
}
button:active:not(:disabled), .btn:active { transform: scale(0.97); }
button:disabled { opacity: 0.45; cursor: not-allowed; }
button.primary, .btn.primary { background: var(--primary); border-color: var(--primary); color: var(--on-primary); box-shadow: var(--shadow-1); }
button.primary:hover:not(:disabled), .btn.primary:hover { background: var(--primary-hover); border-color: var(--primary-hover); }
button.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
button.danger.solid { background: var(--danger); color: var(--surface); border-color: var(--danger); }
button.ghost, .btn.ghost { background: transparent; border-color: transparent; }
button.ghost:hover:not(:disabled), .btn.ghost:hover { background: var(--surface-2); }
/* .sm is visually compact but keeps the full 44px tap target */
button.sm, .btn.sm { min-height: 44px; padding: 0 var(--s3); font-size: var(--fs-sm); border-radius: var(--r-sm); }
button.lg, .btn.lg { min-height: 56px; padding: 0 var(--s5); font-size: var(--fs-md); border-radius: var(--r-lg); }
button.block, .btn.block { width: 100%; }
button.icon { padding: 0; width: 44px; }
/* Labels that can be long (user-named calendars, time zones) wrap instead of overflowing the screen. */
button.wrap, .btn.wrap { white-space: normal; text-align: center; padding-block: var(--s2); }
button[aria-pressed='true'] { background: var(--primary-soft); border-color: var(--primary); color: var(--primary-text); }
/* ---------- Forms ---------- */
+8
View File
@@ -52,4 +52,12 @@ export function setTheme(t: Theme) {
} catch {}
if (t === 'system') delete document.documentElement.dataset.theme;
else document.documentElement.dataset.theme = t;
syncThemeColor(t);
}
// The status bar and browser chrome follow a theme pinned in Settings, not only the device's mode.
function syncThemeColor(t: Theme) {
document.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]').forEach((m) => {
m.media = t === 'system' ? `(prefers-color-scheme: ${m.dataset.for})` : t === m.dataset.for ? 'all' : 'not all';
});
}
syncThemeColor(getTheme());
+1 -1
View File
@@ -117,7 +117,7 @@
<h2 class="month-name" aria-live="polite">{monthLabel(month)}</h2>
<button class="icon" aria-label="Next month" onclick={() => (month = shiftMonth(month, 1))}><ChevronRight size={22} aria-hidden="true" /></button>
<span class="grow"></span>
<button class="sm" onclick={() => addOn(`${month}-01`)}><CalendarPlus size={16} aria-hidden="true" />Add event</button>
<button class="sm" onclick={() => addOn(`${month}-01`)}><CalendarPlus size={16} aria-hidden="true" /><span class="hide-sm">Add event</span></button>
</div>
{#if editing === 'new'}
+6 -4
View File
@@ -94,9 +94,11 @@
<span class="kind"><Icon size={20} aria-hidden="true" /></span>
<span class="grow">
<span class="name">{s.source_name}</span>
<span class="meta">{monthLabel(s.hint_month)} · {s.event_count} {s.event_count === 1 ? 'event' : 'events'}</span>
<span class="meta">
<StatusBadge status={s.status} />
<span>{monthLabel(s.hint_month)} · {s.event_count} {s.event_count === 1 ? 'event' : 'events'}</span>
</span>
</span>
<StatusBadge status={s.status} />
<ChevronRight size={18} class="chev" aria-hidden="true" />
</a>
</li>
@@ -155,7 +157,7 @@
display: grid; place-items: center; width: 40px; height: 40px; flex: none;
border-radius: var(--r-md); background: var(--surface-2); color: var(--text-2);
}
.name { display: block; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.meta { display: block; font-size: var(--fs-sm); color: var(--text-2); }
.name { display: block; font-weight: 650; }
.meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s2); margin-top: 2px; font-size: var(--fs-sm); color: var(--text-2); }
.scan :global(.chev) { color: var(--text-3); flex: none; }
</style>
+3 -3
View File
@@ -82,9 +82,9 @@
</EmptyState>
{:else}
<div class="stack-lg">
<div class="kids" role="tablist" aria-label="Child">
<div class="kids" role="group" aria-label="Child">
{#each kids as c (c.id)}
<button role="tab" aria-selected={c.id === childId} onclick={() => (childId = c.id)} style:--c={c.color}>
<button aria-pressed={c.id === childId} onclick={() => (childId = c.id)} style:--c={c.color}>
<span class="dot" style:background={c.color} aria-hidden="true"></span>{c.display_name}
</button>
{/each}
@@ -158,7 +158,7 @@
<style>
.kids { display: flex; flex-wrap: wrap; gap: var(--s2); }
.kids button { border-radius: var(--r-full); padding: 0 var(--s4); }
.kids button[aria-selected='true'] { border-color: var(--c); background: color-mix(in srgb, var(--c) 14%, var(--surface)); box-shadow: inset 0 0 0 1px var(--c); }
.kids button[aria-pressed='true'] { border-color: var(--c); background: color-mix(in srgb, var(--c) 14%, var(--surface)); box-shadow: inset 0 0 0 1px var(--c); }
.kids .dot { width: 12px; height: 12px; }
.month-bar { display: flex; align-items: center; gap: var(--s1); }
+6 -2
View File
@@ -50,7 +50,7 @@
{#each list.slice(0, 3) as e (e.id)}
<span class="chip" style:--c={colorFor(e)} title={e.title}>{e.title}</span>
{/each}
{#if list.length > 3}<span class="more">+{list.length - 3} more</span>{/if}
{#if list.length > 3}<span class="more">+{list.length - 3}<span class="hide-sm"> more</span></span>{/if}
{/snippet}
{#if onday}
<button type="button" class="day" class:outside class:today={day === today} onclick={() => onday(day)}
@@ -76,7 +76,7 @@
text-align: left; font-size: var(--fs-xs); font-weight: 500; overflow: hidden; min-width: 0; white-space: normal;
}
button.day { cursor: pointer; }
button.day:hover:not(:disabled) { border-color: var(--primary); background: var(--surface-2); }
@media (hover: hover) { button.day:hover:not(:disabled) { border-color: var(--primary); background: var(--surface-2); } }
.outside { opacity: 0.4; }
.num { font-size: var(--fs-xs); font-weight: 700; color: var(--text-2); width: 22px; height: 22px; display: grid; place-items: center; border-radius: 50%; }
.today .num { background: var(--primary); color: var(--on-primary); }
@@ -86,4 +86,8 @@
}
.more { color: var(--text-2); font-weight: 600; }
@media (min-width: 768px) { .day { min-height: 96px; } }
/* Phones: a cell fits 3-4 letters of a title, so show each event as a colored bar; List view has the titles. */
@media (max-width: 599px) {
.chip { height: 6px; padding: 0; border: 0; border-radius: 3px; font-size: 0; background: var(--c); }
}
</style>
+6 -2
View File
@@ -334,6 +334,11 @@
.photo-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; box-shadow: var(--shadow-1); }
.photo-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); padding: var(--s1) var(--s1) var(--s1) var(--s3); border-bottom: 1px solid var(--border); min-height: 48px; }
.photo { max-height: 36dvh; overflow: auto; background: var(--surface-2); }
/* Short screens (phone landscape) and open edit forms need the room more than the pinned photo does. */
@media (max-width: 959px) {
.layout:has(:global(.editor)) .photo-col { position: static; }
}
@media (max-width: 959px) and (max-height: 500px) { .photo-col { position: static; } }
.frame { position: relative; }
.frame img { display: block; width: 100%; height: auto; }
.zoom .frame { width: 250%; }
@@ -343,7 +348,7 @@
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
.bulk { display: flex; flex-direction: column; gap: var(--s3); border-color: var(--primary); }
.kind-set { display: inline-flex; gap: var(--s2); }
.kind-set select { width: auto; min-height: 44px; padding: 0 var(--s2); font-size: var(--fs-sm); }
.kind-set select { width: auto; min-height: 44px; padding: 0 var(--s2); } /* 16px: smaller makes iOS zoom in */
.new-event { display: flex; flex-direction: column; gap: var(--s2); }
.section-title.warn { color: var(--warn); }
@@ -385,5 +390,4 @@
background: var(--surface); border: 1px solid var(--border); box-shadow: var(--shadow-3);
}
@media (min-width: 1024px) { .publish-bar { bottom: var(--s4); } }
@media (max-width: 420px) { .hide-sm { display: none; } }
</style>
+1 -1
View File
@@ -111,7 +111,7 @@
<a class="btn primary" href="#/lunch"><Sandwich size={18} aria-hidden="true" /> Pick lunch days</a>
{/if}
{#each calendars as c, i (c.id)}
<a class="btn" class:primary={i === 0 && scan.layout_type !== 'menu'} href={`/api/calendars/${c.id}/export.ics`} download>
<a class="btn wrap" class:primary={i === 0 && scan.layout_type !== 'menu'} href={`/api/calendars/${c.id}/export.ics`} download>
<Download size={18} aria-hidden="true" /> Download for {c.name}
</a>
{/each}
+1 -1
View File
@@ -232,7 +232,7 @@
<div class="row spread">
<span class="strong">{h.timezone}</span>
{#if deviceZone && deviceZone !== h.timezone}
<button class="sm" onclick={() => send('/user', 'PUT', { timezone: deviceZone })}>Use this device's ({deviceZone})</button>
<button class="sm wrap" onclick={() => send('/user', 'PUT', { timezone: deviceZone })}>Use this device's ({deviceZone})</button>
{/if}
</div>
</section>