diff --git a/web/src/pages/[...path].astro b/web/src/pages/[...path].astro
index 6bc76af5..863c27c7 100644
--- a/web/src/pages/[...path].astro
+++ b/web/src/pages/[...path].astro
@@ -151,6 +151,7 @@ import Icon from '../components/Icon.astro';
const bm = <>
+
>;
const lm = lineMarks(b.c, b.lines);
@@ -202,7 +203,8 @@ import Icon from '../components/Icon.astro';
let at: { couplet: number; phrase: string | null } = { couplet: 0, phrase: null };
const post = (body: object) => fetch('/api/library', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
.then((r) => (r.status === 401 ? (location.href = '/signin?next=' + encodeURIComponent(location.pathname), null) : r.ok ? r.json() : null));
- const lineOf = (c: number) => [...document.querySelectorAll(`[data-c="${c}"] p, p[data-c="${c}"]`)].map((p) => p.textContent!.trim()).join(' / ');
+ const linesOf = (c: number) => [...document.querySelectorAll(`[data-c="${c}"] > p:not(.tag-chips), p[data-c="${c}"]`)].map((p) => p.textContent!.replace(/\s+/g, ' ').trim());
+ const lineOf = (c: number) => linesOf(c).join(' / ');
const open = (couplet: number, phrase: string | null) => {
if (!signedIn) { location.href = '/signin?next=' + encodeURIComponent(location.pathname); return; }
at = { couplet, phrase };
@@ -246,6 +248,44 @@ import Icon from '../components/Icon.astro';
});
document.addEventListener('keydown', (e) => { const ph = (e.target as HTMLElement).closest?.('.bmk[data-phrase]') as HTMLElement | null;
if (ph && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); const p = JSON.parse(ph.dataset.phrase!); open(p.couplet, p.phrase); } });
+ // share a couplet as an image: a square card (frame, logo, the lines, the poet), drawn here and handed to the share sheet
+ const share = async (c: number) => {
+ const [poet, title] = (document.querySelector('[data-cite]') as HTMLElement).dataset.cite!.split('، ');
+ const prose = !!document.querySelector(`p.para[data-c="${c}"]`);
+ const lines = linesOf(c), S = 1080, cv = document.createElement('canvas'); cv.width = cv.height = S;
+ const g = cv.getContext('2d')!; // the card is always on paper, whatever the page's palette
+ await Promise.all(['700 60px "Noto Nastaliq Urdu"', '400 34px "Noto Naskh Arabic"'].map((f) => document.fonts.load(f, lines.join(' '))));
+ g.fillStyle = '#fbf6ea'; g.fillRect(0, 0, S, S);
+ g.strokeStyle = '#b88828'; g.lineWidth = 18; g.strokeRect(9, 9, S - 18, S - 18); g.lineWidth = 3; g.strokeRect(40, 40, S - 80, S - 80);
+ const logo = new Image(); logo.src = '/logo/turanj.svg'; await logo.decode().catch(() => {});
+ if (logo.complete) g.drawImage(logo, S / 2 - 70, 80, 140, 140);
+ g.direction = 'rtl'; g.textAlign = 'center'; g.textBaseline = 'middle';
+ // the lines: one per misra; prose wraps; the size shrinks until everything fits the card
+ let size = 72, rows: string[] = [];
+ const wrap = (t: string, w: number) => { const out: string[] = []; let row = '';
+ for (const word of t.split(' ')) { const next = row ? row + ' ' + word : word; if (g.measureText(next).width > w && row) { out.push(row); row = word; } else row = next; }
+ return row ? [...out, row] : out; };
+ for (; size >= 30; size -= 2) {
+ g.font = `700 ${size}px "Noto Nastaliq Urdu"`;
+ rows = lines.flatMap((l) => wrap(l, S - 200));
+ // verse keeps each misra on one line (prose wraps); Nastaliq is tall: lines 2.6 sizes apart leave room for the ✤ between misras
+ if ((prose || rows.length === lines.length) && rows.length * size * 2.6 <= 560) break;
+ }
+ const lh = size * 2.6, top = 250 + (560 - rows.length * lh) / 2;
+ g.fillStyle = '#17130f'; rows.forEach((r, i) => g.fillText(r, S / 2, top + lh * (i + .5)));
+ // the couplet line (as on the home page) between the verse and the poet: two tapering half-lines and a ✤
+ const y = S - 260, half = (from: number, to: number) => { g.beginPath(); g.moveTo(from, y); g.quadraticCurveTo((from + to) / 2, y - 6, to, y - 4); g.lineTo(to, y + 4); g.quadraticCurveTo((from + to) / 2, y + 6, from, y); g.fill(); };
+ g.fillStyle = '#1f4e79'; half(S / 2 - 30, S / 2 - 230); half(S / 2 + 30, S / 2 + 230);
+ g.fillStyle = '#bf3f2a'; g.font = '700 34px "Noto Naskh Arabic"'; g.fillText('✤', S / 2, y + 7);
+ g.fillStyle = '#bf3f2a'; g.font = '700 40px "Noto Nastaliq Urdu"'; g.fillText(poet, S / 2, S - 190);
+ g.fillStyle = '#635543'; g.font = '400 30px "Noto Naskh Arabic"'; g.fillText(`${title} · دیوان`, S / 2, S - 120);
+ const blob: Blob = await new Promise((done) => cv.toBlob((b) => done(b!), 'image/png'));
+ const file = new File([blob], `divan-${poemId}-${c + 1}.png`, { type: 'image/png' });
+ const url = `${location.origin}${location.pathname}#c${c}`;
+ if (navigator.canShare?.({ files: [file] })) await navigator.share({ files: [file], text: url }).catch(() => {});
+ else { const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = file.name; a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 2000); (window as any).toast('تصویر ڈاؤن لوڈ ہو گئی'); }
+ };
+ document.addEventListener('click', (e) => { const sh = (e.target as HTMLElement).closest('.sh') as HTMLElement | null; if (sh) share(Number(sh.dataset.c)); });
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 3d380d67..9811779f 100644
--- a/web/src/styles/global.css
+++ b/web/src/styles/global.css
@@ -734,3 +734,10 @@ p, li { text-wrap: pretty; }
.note-others button:hover { color: var(--active); }
.ask-actions { flex-wrap: wrap; }
.note-box .note-clear { margin-inline-start: auto; }
+/* share a couplet as an image: under the note glyph, shown on hover like the bookmark */
+.sh { position: absolute; inset-inline-end: -26px; top: calc(.5em + 52px); width: 20px; height: 20px; padding: 0; border: 0; background: none; cursor: pointer; opacity: 0; transition: opacity .15s; }
+.sh::before { content: ''; display: block; width: 16px; height: 16px; margin: auto; background-color: var(--text-muted); -webkit-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") center / contain no-repeat; 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") center / contain no-repeat; }
+.sh:hover::before { background-color: var(--active); }
+.couplet:hover .sh, .poem .para:hover .sh, .poem .single:hover .sh, .sh:focus-visible { opacity: 1; }
+@media (hover: none) { .sh { opacity: .55; } }
+@media (max-width: 700px) { .sh { inset-inline-end: -4px; top: 52px; } }