Rework app bar, notes layout, Settings/Admin/login restyle
Build and Push Docker Image / build-and-push (push) Failing after 20s
Build and Push Docker Image / build-and-push (push) Failing after 20s
App bar: brand, Studies / Read Bible / Reading Plans, and one account menu (theme, Settings, Admin, Log out). Study stage: notes get their own box above the resources in the right column. Settings, Admin and the login page use the reader themes; add a theme-aware danger colour. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31abee0695
commit
ecb1c701fa
22 files changed
+351
-289
No files matched your search
@@ -13,7 +13,7 @@ function RefChip({ label, onRemove, loadVerseText }) {
|
||||
<div className="rounded-xl border border-[var(--r-line)]">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-1.5">
|
||||
<button type="button" onClick={toggle} aria-expanded={open} className="flex-1 text-left text-sm">{label}</button>
|
||||
<button type="button" onClick={onRemove} aria-label={`Remove ${label}`} className="text-[var(--r-sub)] hover:text-red-600">×</button>
|
||||
<button type="button" onClick={onRemove} aria-label={`Remove ${label}`} className="text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]">×</button>
|
||||
</div>
|
||||
{open && <p className="border-t border-[var(--r-line)] px-3 py-2 font-serif text-[13px] leading-relaxed text-[var(--r-sub)]">{text ?? 'Loading…'}</p>}
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function NotesPane({ chunk, updateChunk, allTags }) {
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="px-4 pb-4">
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={8} placeholder={placeholder}
|
||||
<textarea aria-label={label} value={chunk[field] ?? ''} rows={7} placeholder={placeholder}
|
||||
onChange={(e) => updateChunk(chunk.id, { [field]: e.target.value })} className={areaCls} />
|
||||
</div>
|
||||
)}
|
||||
@@ -77,7 +77,7 @@ export default function NotesPane({ chunk, updateChunk, allTags }) {
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className="inline-flex items-center gap-1 rounded-full bg-[var(--r-bar)] px-2.5 py-1 text-xs">
|
||||
{tag}
|
||||
<button type="button" aria-label={`Remove tag ${tag}`} onClick={() => updateChunk(chunk.id, { tags: tags.filter((t) => t !== tag) })} className="text-[var(--r-sub)] hover:text-red-600">×</button>
|
||||
<button type="button" aria-label={`Remove tag ${tag}`} onClick={() => updateChunk(chunk.id, { tags: tags.filter((t) => t !== tag) })} className="text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]">×</button>
|
||||
</span>
|
||||
))}
|
||||
<input value={tagInput} onChange={(e) => setTagInput(e.target.value)} list="study-tag-suggestions"
|
||||
|
||||
@@ -1,29 +1,91 @@
|
||||
// Themed page frame for pages that aren't part of a study project (Home, Import):
|
||||
// slim top bar with a title, optional back button, and actions on the right.
|
||||
// Themed frame for pages outside a study project (Home, Import, Settings, Admin):
|
||||
// a calm app bar (brand, three destinations, account menu) and a page heading row.
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useApp } from '../../context/AppContext.js';
|
||||
import { themeVars, READER_THEMES } from '../reader/readerThemes.js';
|
||||
import { ChevronLeft } from '../reader/icons.jsx';
|
||||
import AuthStatus from '../AuthStatus.jsx';
|
||||
|
||||
const THEME_ORDER = ['paper', 'sepia', 'dark'];
|
||||
const NAV = [
|
||||
{ id: 'studies', label: 'Studies' },
|
||||
{ id: 'reader', label: 'Read Bible' },
|
||||
{ id: 'plans', label: 'Reading Plans' },
|
||||
];
|
||||
|
||||
export default function PageShell({ title, onBack, actions, maxWidth = 'max-w-6xl', children }) {
|
||||
const { readerTheme, setReaderTheme } = useApp();
|
||||
const cycle = () => setReaderTheme(THEME_ORDER[(THEME_ORDER.indexOf(readerTheme) + 1) % THEME_ORDER.length]);
|
||||
// Avatar button with a dropdown: who you are, the theme, Settings, Admin, Log out.
|
||||
function AccountMenu() {
|
||||
const { authUser, readerTheme, setReaderTheme, setCurrentPage, logout } = useApp();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const onDown = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); };
|
||||
const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('mousedown', onDown);
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => { document.removeEventListener('mousedown', onDown); document.removeEventListener('keydown', onKey); };
|
||||
}, [open]);
|
||||
|
||||
const go = (page) => { setOpen(false); setCurrentPage(page); };
|
||||
const item = 'block w-full rounded-lg px-3 py-2 text-left text-sm hover:bg-black/5';
|
||||
|
||||
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>
|
||||
{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">
|
||||
{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>
|
||||
<div className="flex overflow-hidden rounded-lg border border-[var(--r-line)]">
|
||||
{Object.entries(READER_THEMES).map(([id, t]) => (
|
||||
<button key={id} type="button" onClick={() => setReaderTheme(id)} aria-pressed={readerTheme === id}
|
||||
className={`flex-1 px-2 py-1.5 text-xs ${readerTheme === id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{t.label}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{authUser && (
|
||||
<div className="border-t border-[var(--r-line)] pt-1">
|
||||
<button type="button" role="menuitem" onClick={() => go('settings')} className={item}>Settings</button>
|
||||
{authUser.isAdmin && <button type="button" role="menuitem" onClick={() => go('admin')} className={item}>Admin</button>}
|
||||
<button type="button" role="menuitem" onClick={() => { setOpen(false); logout(); }} className={item}>Log out</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PageShell({ title, onBack, actions, active = null, maxWidth = 'max-w-6xl', children }) {
|
||||
const { readerTheme, goHome, openBibleReader, openReadingPlans } = useApp();
|
||||
const go = { studies: goHome, reader: openBibleReader, plans: openReadingPlans };
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--r-bg)] font-sans text-[var(--r-text)]" style={themeVars(readerTheme)}>
|
||||
<div className="sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)]">
|
||||
<div className={`mx-auto flex min-h-[3rem] ${maxWidth} flex-wrap items-center gap-2 px-3 py-1.5`}>
|
||||
{onBack && (
|
||||
<button type="button" onClick={onBack} aria-label="Back" className="flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /></button>
|
||||
)}
|
||||
<h1 className="text-base font-medium">{title}</h1>
|
||||
<div className="ml-auto flex flex-wrap items-center gap-1.5">
|
||||
{actions}
|
||||
<button type="button" onClick={cycle} className="rounded-lg px-2 py-1.5 text-xs text-[var(--r-sub)] hover:bg-black/5" title="Change theme">{READER_THEMES[readerTheme]?.label}</button>
|
||||
<AuthStatus themed />
|
||||
</div>
|
||||
<header className="sticky top-0 z-30 border-b border-[var(--r-line)] bg-[var(--r-bar)]">
|
||||
<div className={`mx-auto flex h-12 ${maxWidth} items-center gap-4 px-4`}>
|
||||
<button type="button" onClick={goHome} className="flex shrink-0 items-center gap-2" aria-label="Bible Study home">
|
||||
<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">
|
||||
{NAV.map((n) => (
|
||||
<button key={n.id} type="button" onClick={go[n.id]} aria-current={active === n.id ? 'page' : undefined}
|
||||
className={`rounded-full px-3 py-1.5 text-sm ${active === n.id ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`}>{n.label}</button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="ml-auto"><AccountMenu /></div>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`mx-auto flex ${maxWidth} flex-wrap items-center gap-3 px-4 pt-6`}>
|
||||
{onBack && (
|
||||
<button type="button" onClick={onBack} aria-label="Back" className="-ml-2 flex h-9 w-9 items-center justify-center rounded-lg text-[var(--r-sub)] hover:bg-black/5"><ChevronLeft /></button>
|
||||
)}
|
||||
<h1 className="text-2xl font-medium tracking-tight">{title}</h1>
|
||||
{actions && <div className="ml-auto flex flex-wrap items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const RESOURCE_TABS = [
|
||||
export default function ResourcePane({ tab, setTab, chunk, chapter, chapterIndex, project, updateChunk, notify, resources, wordStudy, crossRefs, openWordId, setOpenWordId }) {
|
||||
const counts = { words: chunk.greekWords.length, refs: (chunk.crossReferences ?? []).length };
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div role="tablist" aria-label="Study resources" className="flex shrink-0 flex-wrap gap-1.5 border-b border-[var(--r-line)] px-3 py-2.5">
|
||||
{RESOURCE_TABS.map((t) => (
|
||||
<button key={t.id} type="button" role="tab" aria-selected={tab === t.id} onClick={() => setTab(t.id)}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function StudioShell({ stage, loading = false, fixedHeight = fals
|
||||
: syncStatus === 'syncing' ? <span>Syncing…</span>
|
||||
: syncStatus === 'synced' ? <span>Saved</span>
|
||||
: syncStatus === 'error' ? (
|
||||
<span className="text-red-600">Sync failed <button type="button" onClick={retrySync} className="underline">Retry</button></span>
|
||||
<span className="text-[var(--r-danger,#b42318)]">Sync failed <button type="button" onClick={retrySync} className="underline">Retry</button></span>
|
||||
) : null}
|
||||
<button type="button" onClick={cycleTheme} className="rounded-lg px-2 py-1 hover:bg-black/5" title="Change theme">{READER_THEMES[readerTheme]?.label}</button>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ function WordCard({ word, open, onToggle, chunkId, wordStudy }) {
|
||||
<button type="button" onClick={() => lookupWord(chunkId, word.id, 'greek')} className={`${btn} bg-[var(--r-text)] text-[var(--r-bg)] hover:opacity-90`}>{word.loading ? 'Looking up...' : 'Look up Greek'}</button>
|
||||
<button type="button" onClick={() => lookupWord(chunkId, word.id, 'hebrew')} className={btn}>{word.loading ? 'Looking up...' : 'Look up Hebrew'}</button>
|
||||
{word.lexeme && <button type="button" onClick={() => speakOriginalWord(word.lexeme, word.strongNumber)} className={btn} title="Hear pronunciation">Speak</button>}
|
||||
<button type="button" onClick={() => removeGreekWord(chunkId, word.id)} className={`${btn} ml-auto text-red-700`}>Delete</button>
|
||||
<button type="button" onClick={() => removeGreekWord(chunkId, word.id)} className={`${btn} ml-auto text-[var(--r-danger,#b42318)]`}>Delete</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{field(word.strongNumber?.startsWith('H') ? 'Hebrew word' : 'Greek word', 'lexeme')}
|
||||
|
||||
Reference in new issue
Block a user