- Phones get a bottom tab bar (Studies, Bible, Plans, Prayer, Account); wider screens keep the header nav - The Account menu opens upward from the bar; toasts sit above it - Reader and study pages get a labelled Home button instead of a small arrow Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9893883b19
commit
a2185ba1dd
10 files changed
+125
-16
No files matched your search
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useApp } from '../../context/AppContext.js';
|
||||
import { themeVars, READER_THEMES } from '../reader/readerThemes.js';
|
||||
import { ChevronLeft, SearchIcon } from '../reader/icons.jsx';
|
||||
import { useIsPhone } from '../../hooks/useIsPhone.js';
|
||||
|
||||
// `short` is what a phone shows; the full label is still the button's accessible name.
|
||||
const NAV = [
|
||||
@@ -13,8 +14,18 @@ const NAV = [
|
||||
{ id: 'prayer', label: 'Prayer', short: 'Prayer' },
|
||||
];
|
||||
|
||||
const ICONS = {
|
||||
studies: 'M4 5.5A1.5 1.5 0 0 1 5.5 4H18a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5.5ZM8 4v15M11 8h5M11 11.5h5',
|
||||
reader: 'M12 6.5C10.5 5 8 4.5 4.5 4.5V18c3.5 0 6 .5 7.5 2 1.5-1.5 4-2 7.5-2V4.5c-3.5 0-6 .5-7.5 2ZM12 6.5V20',
|
||||
plans: 'M5 6.5h14M5 6.5v12A1.5 1.5 0 0 0 6.5 20h11a1.5 1.5 0 0 0 1.5-1.5v-12M5 6.5A1.5 1.5 0 0 1 6.5 5H8m11 1.5A1.5 1.5 0 0 0 17.5 5H16M8 3.5v3m8-3v3M9 12.5l2 2 4-4',
|
||||
prayer: 'M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z',
|
||||
};
|
||||
const TabIcon = ({ id }) => (
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={ICONS[id]} /></svg>
|
||||
);
|
||||
|
||||
// Avatar button with a dropdown: who you are, the theme, Settings, Admin, Log out.
|
||||
function AccountMenu() {
|
||||
function AccountMenu({ tab = false, active = false }) {
|
||||
const { authUser, readerTheme, setReaderTheme, setCurrentPage, logout } = useApp();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
@@ -33,12 +44,20 @@ function AccountMenu() {
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button type="button" onClick={() => setOpen((o) => !o)} aria-label="Account menu" aria-expanded={open}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[var(--r-line)] bg-[var(--r-bg)] text-xs font-medium uppercase hover:bg-black/5">
|
||||
{authUser?.email?.[0] ?? '·'}
|
||||
</button>
|
||||
{tab ? (
|
||||
<button type="button" onClick={() => setOpen((o) => !o)} aria-label="Account menu" aria-expanded={open}
|
||||
className={`flex min-h-[52px] w-full flex-col items-center justify-center gap-0.5 text-[11px] ${open || active ? 'text-[var(--r-text)]' : 'text-[var(--r-sub)]'}`}>
|
||||
<span className="flex h-[22px] w-[22px] items-center justify-center rounded-full border border-current text-[11px] font-medium uppercase">{authUser?.email?.[0] ?? '·'}</span>
|
||||
<span aria-hidden="true">Account</span>
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={() => setOpen((o) => !o)} aria-label="Account menu" aria-expanded={open}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[var(--r-line)] bg-[var(--r-bg)] text-xs font-medium uppercase hover:bg-black/5">
|
||||
{authUser?.email?.[0] ?? '·'}
|
||||
</button>
|
||||
)}
|
||||
{open && (
|
||||
<div role="menu" className="absolute right-0 top-full z-40 mt-2 w-60 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-2 shadow-xl">
|
||||
<div role="menu" className={`absolute z-40 w-60 ${tab ? 'bottom-full right-2 mb-2' : 'right-0 top-full mt-2'} rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-2 shadow-xl`}>
|
||||
{authUser && <p className="truncate px-3 pb-1 pt-2 text-xs text-[var(--r-sub)]">{authUser.email}</p>}
|
||||
<div className="px-3 pb-2 pt-1">
|
||||
<p className="mb-1.5 text-[11px] uppercase tracking-wide text-[var(--r-sub)]">Theme</p>
|
||||
@@ -65,6 +84,13 @@ function AccountMenu() {
|
||||
export default function PageShell({ title, onBack, actions, active = null, maxWidth = 'max-w-6xl', children }) {
|
||||
const { readerTheme, goHome, goStudies, openBibleReader, openReadingPlans, openPrayer, openPalette } = useApp();
|
||||
const go = { studies: goStudies, reader: openBibleReader, plans: openReadingPlans, prayer: openPrayer };
|
||||
const phone = useIsPhone();
|
||||
// On a phone the destinations sit in a bar at the bottom, where a thumb reaches; toasts lift above it.
|
||||
useEffect(() => {
|
||||
if (!phone) return undefined;
|
||||
document.documentElement.style.setProperty('--bottom-nav', '56px');
|
||||
return () => document.documentElement.style.removeProperty('--bottom-nav');
|
||||
}, [phone]);
|
||||
return (
|
||||
<div className="min-h-dvh bg-[var(--r-bg)] font-sans text-[var(--r-text)]" style={themeVars(readerTheme)}>
|
||||
<a href="#content" className="skip-link">Skip to content</a>
|
||||
@@ -74,14 +100,14 @@ export default function PageShell({ title, onBack, actions, active = null, maxWi
|
||||
<img src="/logo.svg" alt="" className="h-6 w-6" />
|
||||
<span className="hidden text-sm font-medium sm:inline">Bible Study</span>
|
||||
</button>
|
||||
<nav aria-label="Main" className="flex items-center gap-0.5">
|
||||
{!phone && <nav aria-label="Main" className="flex items-center gap-0.5">
|
||||
{NAV.map((n) => (
|
||||
<button key={n.id} type="button" onClick={go[n.id]} aria-current={active === n.id ? 'page' : undefined} aria-label={n.label}
|
||||
className={`whitespace-nowrap rounded-full px-2.5 py-1.5 text-sm sm:px-3 ${active === n.id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>
|
||||
<span className="sm:hidden" aria-hidden="true">{n.short}</span><span className="hidden sm:inline" aria-hidden="true">{n.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</nav>}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{openPalette && (
|
||||
<button type="button" onClick={openPalette} aria-label="Search and go to" data-tip="Search studies, notes and passages (⌘K)"
|
||||
@@ -89,7 +115,7 @@ export default function PageShell({ title, onBack, actions, active = null, maxWi
|
||||
<SearchIcon /><span className="hidden sm:inline">Search</span><kbd className="hidden rounded border border-[var(--r-line)] px-1 text-[10px] md:inline">⌘K</kbd>
|
||||
</button>
|
||||
)}
|
||||
<AccountMenu />
|
||||
{!phone && <AccountMenu />}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -103,6 +129,21 @@ export default function PageShell({ title, onBack, actions, active = null, maxWi
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
{phone && (
|
||||
<>
|
||||
<div aria-hidden="true" className="h-[calc(5rem+env(safe-area-inset-bottom,0px))]" />
|
||||
<nav aria-label="Main" className="fixed inset-x-0 bottom-0 z-30 grid grid-cols-5 border-t border-[var(--r-line)] bg-[var(--r-bar)] pb-[env(safe-area-inset-bottom)]">
|
||||
{NAV.map((n) => (
|
||||
<button key={n.id} type="button" onClick={go[n.id]} aria-current={active === n.id ? 'page' : undefined} aria-label={n.label}
|
||||
className={`flex min-h-[52px] flex-col items-center justify-center gap-0.5 text-[11px] ${active === n.id ? 'text-[var(--r-text)]' : 'text-[var(--r-sub)]'}`}>
|
||||
<TabIcon id={n.id} />
|
||||
<span aria-hidden="true" className={active === n.id ? 'font-medium' : ''}>{n.short}</span>
|
||||
</button>
|
||||
))}
|
||||
<AccountMenu tab active={['settings', 'admin'].includes(active)} />
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user