Mobile ux fixes #1
No files matched your search
+2
-2
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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 ---------- */
|
||||
|
||||
@@ -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());
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
@@ -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); }
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user