Reading mode, and pages the browser's reader understands

- Reading mode (the book button in a poem's, book's or poet's action row): the text, the breadcrumbs, the poet's name and the logo; no search, icons, buttons, couplet tools, tags or footer; larger verse. Kept across pages until the ✕ in the header; switched on before paint (no flash); only on readable pages, so search and tools are unaffected
- For Safari Reader, Firefox Reader View and Chrome's reading mode: a poem is one <article> (title, poet, text), with <meta name="author"> and schema.org CreativeWork data
- Printing shows the text and its source only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Anas Rashid 2026-10-09 17:36:18 +02:00
parent 9f401fe2af
commit 792b68979b
3 changed files with 51 additions and 9 deletions

View File

@ -5,8 +5,9 @@ import '@fontsource/noto-naskh-arabic/700.css';
import '@fontsource/noto-nastaliq-urdu/400.css';
import '@fontsource/noto-nastaliq-urdu/700.css';
import '../styles/global.css';
interface Props { title?: string; description?: string; q?: string }
const { title, description = 'اردو کا کلاسیکی ادب', q = '' } = Astro.props;
interface Props { title?: string; description?: string; q?: string; author?: string; jsonld?: object }
// author and jsonld describe a work for search engines and the browser's own reader view
const { title, description = 'اردو کا کلاسیکی ادب', q = '', author, jsonld } = Astro.props;
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کا کلاسیکی ادب';
import Icon from '../components/Icon.astro';
import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can turn its rays
@ -36,6 +37,8 @@ import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="دیوان: اردو کا کلاسیکی ادب" />
<meta name="twitter:card" content="summary_large_image" />
{author && <meta name="author" content={author} />}
{jsonld && <script type="application/ld+json" set:html={JSON.stringify(jsonld).replace(/</g, '\\u003c')} />}
<script is:inline>
// font, palette and light/dark applied before paint (Naskh, sepia and the system's light/dark by default)
try {
@ -48,6 +51,7 @@ import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can
const pal = localStorage.getItem('divan-palette'); // sepia when unset
if (pal) document.documentElement.dataset.palette = pal;
if (document.cookie.includes('divan-digits=latn')) document.documentElement.dataset.digits = 'latn'; // Western numerals
if (localStorage.getItem('divan-reading')) document.documentElement.dataset.reading = ''; // reading mode, kept across pages
} catch (e) {}
</script>
</head>
@ -65,6 +69,7 @@ import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></svg>
</button>
</form>
<button type="button" class="icon-btn reading-exit" data-reading-toggle title="مطالعے کے انداز سے باہر" aria-label="مطالعے کے انداز سے باہر"><Icon name="close" /></button>
<nav class="toggles" aria-label="سائٹ">
{Astro.locals.user?.role === 'admin' && <a class="icon-btn" href="/admin" title="ایڈمن" aria-label="ایڈمن"><Icon name="admin" /></a>}
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="icon-btn" href="/mod" title="موڈریشن" aria-label="موڈریشن"><Icon name="edit" /></a>}
@ -505,6 +510,16 @@ import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can
</form>
</dialog>
<div class="toasts" aria-live="polite"></div>
<script is:inline>
// reading mode: only the text, the breadcrumbs and the logo; remembered across pages until switched off
document.addEventListener('click', (e) => {
if (!e.target.closest('[data-reading-toggle]')) return;
const root = document.documentElement, on = !('reading' in root.dataset);
const flip = () => { on ? (root.dataset.reading = '') : delete root.dataset.reading; };
document.startViewTransition ? document.startViewTransition(flip) : flip();
try { on ? localStorage.setItem('divan-reading', '1') : localStorage.removeItem('divan-reading'); } catch (err) {}
});
</script>
<script is:inline>
(() => {
const dlg = document.getElementById('ask'), ok = dlg.querySelector('[value=ok]');

View File

@ -90,7 +90,7 @@ const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couple
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
import Icon from '../components/Icon.astro';
---
<Base title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={page.type === 'poet' ? page.poet.description?.slice(0, 160) : undefined}>
<Base author={page.poet?.nickname} jsonld={page.type === 'poem' ? { '@context': 'https://schema.org', '@type': 'CreativeWork', name: page.poem.title, inLanguage: 'ur', author: { '@type': 'Person', name: page.poet.nickname }, isPartOf: crumbs.at(-1)?.title, url: Astro.url.href.split('?')[0] } : undefined} title={page.type === 'poem' ? `${page.poem.title} · ${page.poet.nickname}` : title} description={page.type === 'poet' ? page.poet.description?.slice(0, 160) : undefined}>
{page.type !== 'poet' && (
<nav class="crumbs">
{crumbs.map((c: any, i: number) => (
@ -101,8 +101,8 @@ import Icon from '../components/Icon.astro';
{page.type === 'poet' && (
<>
<h1>{page.poet.name}</h1>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<h1 data-readable>{page.poet.name}</h1>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poet', poetId })} aria-pressed={String(!!saved.poet)} title="یہ شاعر محفوظ کریں">{label(saved.poet)}</button><button type="button" class="icon-btn small" data-reading-toggle title="مطالعے کا انداز: صرف متن" aria-label="مطالعے کا انداز"><Icon name="dict" size={17} /></button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
<p class="muted">{years(page.poet)}</p>
<Tags tags={pageTags} />
@ -111,9 +111,9 @@ import Icon from '../components/Icon.astro';
)}
{page.type === 'category' && (
<>
<h1>{page.category.title}</h1>
<h1 data-readable>{page.category.title}</h1>
<Tags tags={pageTags} />
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="یہ کتاب یا باب محفوظ کریں">{label(saved.category)}</button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'category', categoryId: page.category.id })} aria-pressed={String(!!saved.category)} title="یہ کتاب یا باب محفوظ کریں">{label(saved.category)}</button><button type="button" class="icon-btn small" data-reading-toggle title="مطالعے کا انداز: صرف متن" aria-label="مطالعے کا انداز"><Icon name="dict" size={17} /></button>{arrange && <> <form method="post" action={`/mod/order/${page.category.id}`} class="inline-form"><button class="mod-edit-btn"><Icon name="arrange" size={17} /> ترتیب دیں</button></form>
<a class="icon-btn small" href={`/mod/order/${page.category.id}`} title="ترتیب کا تاریخچہ" aria-label="ترتیب کا تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
</>
)}
@ -135,18 +135,20 @@ import Icon from '../components/Icon.astro';
{page.type === 'poem' && (
<>
<article class="work" data-readable>
<h1><Seg s={show(page.poem.title)} /></h1>
<p class="byline">{page.poet.nickname}</p>
{re && (
<p class="hl-note muted">
«{hlq}» نمایاں · <a href={`/search?q=${encodeURIComponent(hlq)}`}>تلاش کے نتائج</a> · <a href={url}>نشان ہٹائیں</a>
</p>
)}
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="یہ کلام محفوظ کریں">{label(saved.poem)}</button>
<div class="center"><button type="button" class="save" data-save={JSON.stringify({ kind: 'poem', poemId })} aria-pressed={String(!!saved.poem)} title="یہ کلام محفوظ کریں">{label(saved.poem)}</button><button type="button" class="icon-btn small" data-reading-toggle title="مطالعے کا انداز: صرف متن" aria-label="مطالعے کا انداز"><Icon name="dict" size={17} /></button>
{modCan.edit && <> <form method="post" action={`/mod/work/${poemId}`} class="inline-form"><button class="mod-edit-btn"><Icon name="edit" size={17} /> ترمیم کریں</button></form>
<a class="icon-btn small" href={`/mod/work/${poemId}`} title="تاریخچہ" aria-label="تاریخچہ"><Icon name="history" size={17} /></a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn"><Icon name="tag" size={17} /> ٹیگ</button></form></>}</div>
<Tags tags={pageTags} />
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
<article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
<div class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
{blocks.map((b) => {
const cn = noteOf(b.c), any = cn || notes.some((n) => n.couplet === b.c);
// the bookmark, and beside it the note glyph (shown when this couplet or a phrase in it has a note)
@ -161,6 +163,7 @@ import Icon from '../components/Icon.astro';
: b.kind === 'para' ? <p class="para" id={`c${b.c}`} data-c={b.c}>{bm}<Seg s={show(b.lines[0], lm[0])} /></p>
: <p class="single" id={`c${b.c}`} data-c={b.c}>{bm}<Seg s={show(b.lines[0], lm[0])} /></p>;
})}
</div>
</article>
<p class="muted hint">کسی لفظ کو منتخب کریں تو اس کے معنی اور تلفظ ساتھ نظر آئیں گے؛ کسی عبارت پر نوٹ بھی لکھ سکتے ہیں۔</p>
<dialog class="ask note-box" id="note-box" aria-labelledby="note-for">

View File

@ -805,3 +805,27 @@ p, li { text-wrap: pretty; }
.note-edit textarea { width: 100%; box-sizing: border-box; resize: vertical; line-height: 1.9; background: var(--surface-page); }
.note-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 6px; }
.note-delete { position: absolute; top: 10px; left: 12px; margin: 0; }
/* ── reading mode: the text, the breadcrumbs and the logo; nothing else. On readable pages only (poems, books, poets),
kept across pages until switched off from the ✕ in the header ── */
.byline, .reading-exit { display: none; }
:root[data-reading]:has([data-readable]) {
.site-header .search-form, .site-header .toggles, .site-header .kb, .site-footer, .center:has(> .save), .c-tools, .tag-chips,
.hint, .hl-note, .divan-edit, .source, .dict { display: none !important; }
.reading-exit { display: inline-flex; margin-inline-start: auto; }
.site-header .wrap { flex-wrap: nowrap; padding-top: 10px; }
.wordmark .logo { width: 44px; }
main { max-width: 760px; padding-top: 4px; }
.crumbs { font-size: var(--fs-sm); }
.byline { display: block; text-align: center; margin: -4px 0 14px; color: var(--brand); font-family: 'Noto Nastaliq Urdu', serif; line-height: 2; }
.poem { background: none; border: 0; box-shadow: none; padding-inline: 0; }
.couplet, .poem .single { font-size: 1.28rem; }
.poem .para { font-size: 1.15rem; }
.couplet:hover { background: none; }
.intro { background: none; border: 0; padding-inline: 0; }
}
@media print { /* printing is reading too: the text and its source */
.site-header .search-form, .site-header .toggles, .site-footer, .center:has(> .save), .c-tools, .tag-chips, .hint, .poem-nav, .dict, .toasts { display: none !important; }
.byline { display: block; text-align: center; }
.poem { background: none; border: 0; }
}