diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index 6caeb041..b9c395eb 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -566,7 +566,17 @@ import { brand } from '../lib/brand'; (() => { const root = document.documentElement, header = document.querySelector('.site-header'); new ResizeObserver(() => root.style.setProperty('--header-h', header.offsetHeight + 'px')).observe(header); - const mark = () => (scrollY > 8 ? (root.dataset.scrolled = '') : delete root.dataset.scrolled); + // phones: the header steps aside while scrolling down and returns on the way up (not while it is in use) + const phone = matchMedia('(max-width: 640px)'); + let last = scrollY; + const mark = () => { + scrollY > 8 ? (root.dataset.scrolled = '') : delete root.dataset.scrolled; + const busy = header.contains(document.activeElement) || header.querySelector('details[open]') || !document.getElementById('kb').hidden; + if (!phone.matches || busy || scrollY < header.offsetHeight) delete root.dataset.headerHidden; + else if (scrollY > last + 6) root.dataset.headerHidden = ''; + else if (scrollY < last - 6) delete root.dataset.headerHidden; + if (Math.abs(scrollY - last) > 6) last = scrollY; + }; addEventListener('scroll', mark, { passive: true }); mark(); })(); diff --git a/web/src/pages/[...path].astro b/web/src/pages/[...path].astro index 6cd69522..52d98b42 100644 --- a/web/src/pages/[...path].astro +++ b/web/src/pages/[...path].astro @@ -153,10 +153,13 @@ import Icon from '../components/Icon.astro'; 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) const bm = + {/* wide screens: four icons on hover; phones: one ⋯ that opens a menu (the CSS shows one or the other) */} ; const lm = lineMarks(b.c, b.lines); return b.kind === 'couplet' ?
{bm}{b.lines.map((l, i) =>

)}
@@ -166,6 +169,12 @@ import Icon from '../components/Icon.astro';

کسی لفظ کو منتخب کریں تو اس کے معنی اور تلفظ ساتھ نظر آئیں گے؛ کسی عبارت پر نوٹ بھی لکھ سکتے ہیں۔

+

@@ -233,9 +242,10 @@ import Icon from '../components/Icon.astro'; box.showModal(); text.focus(); }; const mark = (couplet: number) => { // the glyph follows the notes on this couplet - const nt = document.querySelector(`.nt[data-for="${couplet}"]`); if (!nt) return; const cn = notes.find((n) => n.couplet === couplet && !n.phrase)?.note, any = notes.some((n) => n.couplet === couplet); - nt.setAttribute('aria-pressed', String(any)); nt.setAttribute('title', cn ? `نوٹ: ${cn}` : any ? 'اس شعر کی عبارتوں پر نوٹ' : 'نوٹ لکھیں'); // the bookmark is separate + const nt = document.querySelector(`.nt[data-for="${couplet}"]`), more = document.querySelector(`.more[data-for="${couplet}"]`) as HTMLElement | null; + nt?.setAttribute('aria-pressed', String(any)); nt?.setAttribute('title', cn ? `نوٹ: ${cn}` : any ? 'اس شعر کی عبارتوں پر نوٹ' : 'نوٹ لکھیں'); + if (more) { any ? (more.dataset.nt = '') : delete more.dataset.nt; more.title = cn ? `نوٹ: ${cn}` : 'محفوظ، نوٹ، بھیجیں'; } // the bookmark is separate }; box.addEventListener('close', async () => { const act = box.returnValue, { couplet, phrase } = at; @@ -305,6 +315,45 @@ import Icon from '../components/Icon.astro'; else { try { await navigator.clipboard.writeText(`${text}\n${url}`); (window as any).toast('یہ براؤزر شیئر نہیں کر سکتا: شعر اور لنک کاپی ہو گئے'); } catch { (window as any).toast('شیئر نہیں ہو سکا', 'error'); } } }; document.addEventListener('click', (e) => { const b = (e.target as HTMLElement).closest('.sh, .dl') as HTMLElement | null; if (b) share(Number(b.dataset.for), b.classList.contains('dl')); }); + // phones: one ⋯ per couplet opens the shared menu beside it: bookmark, note, share, download + const menu = document.getElementById('c-menu') as HTMLElement & { showPopover(): void; hidePopover(): void }; + let forC = 0, from: HTMLElement | null = null; + document.addEventListener('click', (e) => { + const more = (e.target as HTMLElement).closest('.more') as HTMLElement | null; if (!more) return; + forC = Number(more.dataset.for); from = more; + const on = 'bm' in more.dataset; + menu.querySelector('[data-act=bm] span')!.textContent = on ? 'محفوظ شدہ سے ہٹائیں' : 'محفوظ کریں'; + menu.querySelector('[data-act=bm]')!.toggleAttribute('data-on', on); + menu.showPopover(); + const r = more.getBoundingClientRect(), m = menu.getBoundingClientRect(); // below the button, kept on screen + menu.style.left = `${Math.max(8, Math.min(innerWidth - m.width - 8, r.left))}px`; + menu.style.top = `${r.bottom + 6 + m.height > innerHeight ? r.top - m.height - 6 : r.bottom + 6}px`; + (menu.querySelector('[role=menuitem]') as HTMLElement).focus(); + }); + menu.addEventListener('click', async (e) => { + const act = (e.target as HTMLElement).closest('[data-act]')?.getAttribute('data-act'); if (!act) return; + menu.hidePopover(); + if (act === 'nt') return open(forC, null); + if (act === 'sh' || act === 'dl') return share(forC, act === 'dl'); + const d = await post({ kind: 'couplet', poemId, couplet: forC }); if (!d || !from) return; + d.saved ? (from.dataset.bm = '') : delete from.dataset.bm; + from.parentElement?.querySelector('.bm')?.setAttribute('aria-pressed', String(d.saved)); // the wide-screen icon too + (window as any).toast(d.saved ? 'شعر محفوظ ہو گیا' : 'محفوظ شدہ سے ہٹا دیا'); + }); + menu.addEventListener('keydown', (e) => { // arrows move through the menu + const items = [...menu.querySelectorAll('[role=menuitem]')], i = items.indexOf(document.activeElement as HTMLElement); + if (e.key === 'ArrowDown') { e.preventDefault(); items[(i + 1) % items.length].focus(); } + if (e.key === 'ArrowUp') { e.preventDefault(); items[(i - 1 + items.length) % items.length].focus(); } + }); + // touch screens: tapping a couplet shows its tools (and hides the last one's); links, buttons and selections are left alone + if (matchMedia('(hover: none)').matches) document.querySelector('.poem')?.addEventListener('click', (e) => { + const t = e.target as HTMLElement; + if (t.closest('a, button, .bmk') || getSelection()?.toString().trim()) return; + const block = t.closest('.couplet, .para, .single'); if (!block) return; + const was = block.classList.contains('is-active'); + document.querySelectorAll('.poem .is-active').forEach((x) => x.classList.remove('is-active')); + if (!was) block.classList.add('is-active'); + }); document.addEventListener('divan:note', (e) => { const { couplet, phrase } = (e as CustomEvent).detail; open(couplet, phrase); }); } diff --git a/web/src/styles/global.css b/web/src/styles/global.css index ae5ab322..4e51b259 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -862,3 +862,53 @@ p, li { text-wrap: pretty; } .site-footer .wrap { grid-template-columns: 1fr; } .footer-bar { justify-content: center; text-align: center; } } + +/* ── phones ── */ +/* e-book format tabs: one line each; the English in brackets only where there is room */ +.ebook-tabs a { white-space: nowrap; } +@media (max-width: 560px) { + .ebook-tabs { flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; } + .ebook-tabs a span[lang] { display: none; } +} +/* touch screens have no hover: a couplet's tools show when it is tapped (the couplet is lightly marked); a set bookmark + or note stays visible; couplets without either do not keep room for the row */ +@media (hover: none) { + .poem .couplet.is-active, .poem .para.is-active, .poem .single.is-active { background: color-mix(in srgb, var(--accent) 14%, transparent); border-radius: var(--r-sm); } +} +/* phones: the header slides away while scrolling down and comes back on the way up (a transform: nothing below jumps) */ +@media (max-width: 640px) { + .site-header { transition: transform .25s ease, box-shadow .2s, background .2s; } + :root[data-header-hidden] .site-header { transform: translateY(-100%); box-shadow: none; } +} + +/* ── a couplet's tools: one small button (⋯, with a filled bookmark and/or note glyph when set) that opens one shared + menu: bookmark, note, share, download. The button shows on hover (on touch screens when the couplet is tapped); a set + bookmark or note keeps it visible ── */ +:root { --dots: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='5' cy='12' r='2.2'/%3E%3Ccircle cx='12' cy='12' r='2.2'/%3E%3Ccircle cx='19' cy='12' r='2.2'/%3E%3C/svg%3E"); --sh-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='m16 6-4-4-4 4'/%3E%3Cpath d='M12 2v13'/%3E%3C/svg%3E"); --dl-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3Cpath d='M12 15V3'/%3E%3C/svg%3E"); } +.c-tools > .more { align-items: center; gap: 3px; height: 24px; padding: 0 6px; border: 0; border-radius: var(--r-pill); background: none; + color: var(--text-muted); cursor: pointer; opacity: 0; transition: opacity .15s, background .15s; } +.more i, .c-menu i { flex: none; width: 15px; height: 15px; background-color: currentColor; -webkit-mask: var(--m) center / contain no-repeat; mask: var(--m) center / contain no-repeat; } +.more i { display: none; } +.more .g-dots { display: block; --m: var(--dots); } +.more[data-bm] .g-bm { display: block; --m: var(--bm-filled); background-color: var(--active); } +.more[data-nt] .g-nt { display: block; --m: var(--nt-filled); background-color: var(--accent); } +.more[data-bm], .more[data-nt] { opacity: 1; } +.more[data-bm] .g-dots, .more[data-nt] .g-dots { opacity: .45; } +:is(.couplet, .para, .single):hover .more, .is-active .more, .more:focus-visible { opacity: 1; } +.more:hover { background: var(--accent-soft); color: var(--active); } +/* the ⋯ and its menu are for phones; wider screens keep the four icons */ +.more { display: none; } +@media (max-width: 640px), (hover: none) and (pointer: coarse) { + .c-tools > .bm, .c-tools > .nt, .c-tools > .sh, .c-tools > .dl { display: none; } + .c-tools > .more { display: inline-flex; } + .c-tools { bottom: 2px; } .poem .couplet { padding-bottom: 26px; } /* room for the button: it never sits on the text */ +} +.c-menu { position: fixed; inset: auto; margin: 0; min-width: 180px; padding: 6px; border: 1px solid var(--border); border-radius: var(--r-md); + background: var(--surface-raised); color: var(--text); box-shadow: var(--shadow-lg); } +.c-menu:popover-open { display: flex; flex-direction: column; animation: pop-in .14s ease-out; } +.c-menu button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 5px 10px; border: 0; border-radius: var(--r-sm); background: none; + color: var(--text); font: var(--fs-sm)/1.9 var(--font-ui); text-align: start; cursor: pointer; } +.c-menu button:hover, .c-menu button:focus-visible { background: var(--accent-soft); color: var(--active); outline: none; } +.c-menu i { width: 17px; height: 17px; background-color: var(--text-muted); } +.c-menu .g-bm { --m: var(--bm-outline); } .c-menu [data-on] .g-bm { --m: var(--bm-filled); background-color: var(--active); } +.c-menu .g-nt { --m: var(--nt-outline); } .c-menu .g-sh { --m: var(--sh-mask); } .c-menu .g-dl { --m: var(--dl-mask); }