Outline: restyle the share page and the printed / Word outline

- Read-only share page: a calm paper-toned page rendered natively (no
  iframe), with outline tabs, Print / PDF, numbered points with accent
  numerals, type tags, notes and verse quotes; works on phones
- Printable / PDF page restyled to match (serif, hanging numerals, tidy
  spacing, footer)
- Word outline restyled: eyebrow + title block, bold main points, accent
  numerals, set-off verse quotes, tidy margins

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:10:42 -04:00
1 parent 34d04fd077
commit 57db2310dd
6 files changed
+184 -71

No files matched your search

+37 -29
View File
@@ -1,9 +1,17 @@
// What someone sees when they open a read-only outline link: the outline(s), nothing editable, and a
// way to print or save it.
// What someone sees when they open a read-only outline link: a calm page with the outline, a switcher
// when there is more than one, and Print / PDF. Nothing is editable.
import { useEffect, useState } from 'react';
import { getSharedOutline } from '../syncService.js';
import { buildOutlineHtml } from '../utils/bibleUtils.jsx';
import { themeVars } from './reader/readerThemes.js';
import OutlineDocument from './OutlineDocument.jsx';
import { printOutline } from '../utils/outlineExport.js';
import { flatten } from '../utils/outline.js';
const pill = (active) => `shrink-0 rounded-full px-3.5 py-1.5 text-sm transition ${active ? 'bg-[var(--r-text)] text-[var(--r-bg)]' : 'text-[var(--r-sub)] hover:bg-black/5'}`;
function Frame({ children }) {
return <div className="min-h-dvh bg-[var(--r-bg)] font-sans text-[var(--r-text)]" style={themeVars('paper')}>{children}</div>;
}
export default function SharedOutlineView({ token }) {
const [data, setData] = useState(null);
@@ -23,40 +31,40 @@ export default function SharedOutlineView({ token }) {
if (error) {
return (
<div className="flex min-h-dvh items-center justify-center bg-slate-100 px-4">
<div className="w-full max-w-sm rounded-3xl bg-white p-8 text-center shadow">
<p className="text-sm uppercase tracking-[0.24em] text-slate-400">Bible Study Project</p>
<p className="mt-4 text-sm text-rose-600">{error}</p>
<Frame>
<div className="flex min-h-dvh items-center justify-center px-5">
<div className="max-w-sm text-center">
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-[var(--r-accent)]">Bible Study Project</p>
<p className="mt-4 text-base">{error}</p>
</div>
</div>
</div>
</Frame>
);
}
if (!data) return <div className="flex min-h-dvh items-center justify-center bg-slate-100"><p className="text-sm text-slate-500">Loading outline…</p></div>;
if (!data) return <Frame><p className="flex min-h-dvh items-center justify-center text-sm text-[var(--r-sub)]">Loading outline…</p></Frame>;
const current = data.outlines.find((o) => o.scope === scope) ?? data.outlines[0];
const title = current && current.scope !== 'study' ? `${data.title} — ${current.label}` : data.title;
const minutes = current ? flatten(current.tree).reduce((sum, { node }) => sum + (Number.isFinite(node.minutes) ? node.minutes : 0), 0) : 0;
const meta = current ? [current.scope === 'study' ? data.translation : `${current.label} · ${data.translation}`, minutes > 0 ? `About ${minutes} minute${minutes === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ') : '';
const outline = current ? { title, subtitle: current.scope === 'study' ? data.translation : `${current.label} · ${data.translation}`, tree: current.tree } : null;
return (
<div className="min-h-dvh bg-slate-100">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-slate-900 px-4 py-3 text-xs text-slate-300">
<span>🔗 Read-only outline — <a href="/" className="underline hover:text-white">go to Bible Study Project</a></span>
<span className="flex items-center gap-3">
{data.outlines.length > 1 && (
<label className="flex items-center gap-2">Outline
<select value={current?.scope} onChange={(e) => setScope(e.target.value)} aria-label="Outline" className="rounded-md border border-slate-600 bg-slate-800 px-2 py-1 text-slate-100">
{data.outlines.map((o) => <option key={o.scope} value={o.scope}>{o.label}</option>)}
</select>
</label>
)}
{outline && <button type="button" onClick={() => printOutline(outline)} className="rounded-md border border-slate-500 px-3 py-1 text-slate-100 hover:bg-slate-700">Print / PDF</button>}
</span>
</div>
{outline ? (
<iframe title="Shared outline" srcDoc={buildOutlineHtml(outline)} sandbox="allow-popups" className="h-[calc(100dvh-3rem)] w-full border-0 bg-white" />
) : (
<p className="p-10 text-center text-sm text-slate-500">This study doesn’t have an outline yet.</p>
)}
</div>
<Frame>
<header className="sticky top-0 z-10 border-b border-[var(--r-line)] bg-[var(--r-bar)]/95 backdrop-blur">
<div className="mx-auto flex max-w-[44rem] flex-wrap items-center gap-x-3 gap-y-2 px-5 py-2.5 sm:px-8">
<span className="shrink-0 text-sm font-medium">Bible Study</span>
{data.outlines.length > 1 ? (
<nav aria-label="Outline" className="order-last flex w-full min-w-0 gap-1 overflow-x-auto sm:order-none sm:w-auto sm:flex-1">
{data.outlines.map((o) => (
<button key={o.scope} type="button" onClick={() => setScope(o.scope)} aria-current={current?.scope === o.scope ? 'true' : undefined} className={pill(current?.scope === o.scope)}>{o.label}</button>
))}
</nav>
) : <span className="flex-1" />}
{outline && <button type="button" onClick={() => printOutline(outline)} className="ml-auto shrink-0 sm:ml-0 rounded-full border border-[var(--r-line)] px-3.5 py-1.5 text-sm hover:bg-black/5">Print / PDF</button>}
</div>
</header>
{current ? <OutlineDocument title={title} meta={meta} tree={current.tree} /> : <p className="p-10 text-center text-sm text-[var(--r-sub)]">This study doesn’t have an outline yet.</p>}
</Frame>
);
}