From bba1b9332078b648ab4eac75be5c7a554c5ff646 Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Sat, 10 Oct 2026 00:02:56 +0200 Subject: [PATCH] =?UTF-8?q?Phones:=20one=20=E2=8B=AF=20per=20couplet=20ins?= =?UTF-8?q?tead=20of=20four=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On phones (up to 640px, or touch without a mouse) each couplet has one small ⋯ button, shown when the couplet is tapped; a set bookmark or note shows as a tiny glyph on it and keeps it visible. It opens one shared menu (a native popover, light-dismiss, arrow keys): bookmark or remove, note, share, download. Wider screens keep the four icons on hover. Couplets keep room under them on narrow screens, so the tools never sit on the text Co-Authored-By: Claude Opus 5.5 --- web/src/pages/[...path].astro | 44 +++++++++++++++++++++++++++++++++-- web/src/styles/global.css | 36 ++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/web/src/pages/[...path].astro b/web/src/pages/[...path].astro index 75388ced..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,36 @@ 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; diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 90ec1d8c..4e51b259 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -873,14 +873,42 @@ p, li { text-wrap: pretty; } /* 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) { - .c-tools > button { opacity: 0; pointer-events: none; } - .c-tools > button[aria-pressed='true'], .is-active > .c-tools > button { opacity: 1; pointer-events: auto; } .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); } - .poem .couplet:not(.is-active):not(:has(.c-tools [aria-pressed='true'])) { padding-bottom: 6px; } - .poem .para:not(.is-active):not(:has(.c-tools [aria-pressed='true'])), .poem .single:not(.is-active):not(:has(.c-tools [aria-pressed='true'])) { margin-bottom: 10px; } } /* 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); }