From c4bf52dd97e6b674c4689de811686fb345df6832 Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Fri, 9 Oct 2026 15:23:06 +0200 Subject: [PATCH 1/5] =?UTF-8?q?Icons=20and=20colour=20roles:=20header=20ic?= =?UTF-8?q?ons=20with=20tooltips=20and=20a=20red=20log=20out;=20buttons=20?= =?UTF-8?q?coloured=20by=20what=20they=20do;=20=D9=85=D8=AD=D9=81=D9=88?= =?UTF-8?q?=D8=B8=20=D8=B4=D8=AF=DB=81=20and=20=D9=BE=D8=B3=D9=86=D8=AF?= =?UTF-8?q?=DB=8C=D8=AF=DB=81=20=D9=84=DA=A9=DA=BE=D8=A7=D8=B1=DB=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Icon component (Lucide shapes, ISC), currentColor, decorative (the control keeps its words in aria-label/title). - Colour roles: --action (primary, filled), --ok (approve, publish), --danger (log out, delete, reject); .btn variants and .icon-btn; light and dark. - Header: admin, moderation, saved, word book, account/sign in, font, theme as icons; log out in red. - Review: save/send filled, approve/publish green, return plain, reject red, with icons. Account: save, log out, password, delete. Reading pages: edit, history, arrange, tag. E-books: add, download, covers; the reader's controls as icons. - نشانات is now محفوظ شدہ; favourite writers have their own page, پسندیدہ لکھاری (/writers). Co-Authored-By: Claude Opus 5.5 --- web/src/components/Icon.astro | 35 +++++++++++++++++++++++++++ web/src/components/LibraryNav.astro | 7 +++--- web/src/components/Seg.astro | 2 +- web/src/layouts/Base.astro | 29 +++++++++++++--------- web/src/lib/pdfreader.ts | 2 +- web/src/pages/[...path].astro | 13 +++++----- web/src/pages/[poet]/ebooks.astro | 3 ++- web/src/pages/account.astro | 9 +++---- web/src/pages/ebook/[id].astro | 15 ++++++------ web/src/pages/library.astro | 29 ++++++++-------------- web/src/pages/mod/rev/[id].astro | 23 ++++++++++-------- web/src/pages/writers.astro | 22 +++++++++++++++++ web/src/styles/global.css | 37 +++++++++++++++++++++++------ 13 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 web/src/components/Icon.astro create mode 100644 web/src/pages/writers.astro diff --git a/web/src/components/Icon.astro b/web/src/components/Icon.astro new file mode 100644 index 00000000..005afb5c --- /dev/null +++ b/web/src/components/Icon.astro @@ -0,0 +1,35 @@ +--- +// Icons (Lucide, ISC licence; https://lucide.dev): drawn with currentColor, so they take the colour of their button +// or link. Decorative: the button or link carries the words (aria-label/title, or its text). +const { name, size = 20 } = Astro.props as { name: keyof typeof PATHS; size?: number }; +const PATHS = { + admin: '', + edit: '', + bookmark: '', + words: '', + user: '', + login: '', + logout: '', + theme: '', + font: '', + tag: '', + arrange: '', + history: '', + upload: '', + download: '', + check: '', + close: '', + undo: '', + send: '', + save: '', + full: '', + next: '', // points left: forward in a right-to-left page + prev: '', + trash: '', + key: '', + image: '', + plus: '', + external: '', +}; +--- + diff --git a/web/src/components/LibraryNav.astro b/web/src/components/LibraryNav.astro index 9afb2990..47f30db2 100644 --- a/web/src/components/LibraryNav.astro +++ b/web/src/components/LibraryNav.astro @@ -1,9 +1,10 @@ --- -// tabs between the reader's bookmarks and their word book (kept separate) -const { current } = Astro.props as { current: 'bookmarks' | 'words' }; +// tabs of the reader's own space: saved texts, favourite writers, the word book (kept separate), profile +const { current } = Astro.props as { current: 'bookmarks' | 'writers' | 'words' }; --- diff --git a/web/src/components/Seg.astro b/web/src/components/Seg.astro index 95275948..4576db74 100644 --- a/web/src/components/Seg.astro +++ b/web/src/components/Seg.astro @@ -3,4 +3,4 @@ const { s } = Astro.props as { s: [string, string, string?][] }; --- {s.map(([t, k, d]) => (k === 'mark' ? {t} : k === 'pen' ? {t} - : k === 'bmk' ? {t} : t))} + : k === 'bmk' ? {t} : t))} diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index 5fb512d6..3aafe70d 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -8,6 +8,7 @@ import '../styles/global.css'; interface Props { title?: string; description?: string; q?: string } const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props; const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر'; +import Icon from '../components/Icon.astro'; --- @@ -43,17 +44,23 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک -
- {Astro.locals.user?.role === 'admin' && } - {['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && } - {Astro.locals.user && } - {Astro.locals.user && } +
+ ? + : } + + + {Astro.locals.user && ( +
+ + +
+ )} + @@ -88,7 +95,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک @@ -52,10 +54,26 @@ import Icon from '../components/Icon.astro'; {Astro.locals.user ? : } - - +
+ +
+

خط

+
+

رنگ

+
+ {[['', 'سپیا', '#fbf6ea', '#bf3f2a'], ['teal', 'فیروزی', '#e2f0ee', '#0b4f6c'], ['lapis', 'نیلگوں', '#e6ebf8', '#1d3b8b'], + ['green', 'سبز', '#e5eede', '#1f5d3a'], ['rose', 'گلابی', '#f5e6ea', '#6b2c5e']].map(([v, name, a, b]) => ( + + ))} +
+

روشنی

+
+ + + +
+
+
{Astro.locals.user && (
@@ -96,13 +114,24 @@ import Icon from '../components/Icon.astro'; diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 99a935f5..55fde0b3 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -23,6 +23,20 @@ --gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark; --action: #7db3e8; --action-ink: #0d1520; --ok: #7cc47f; --danger: #f07a6e; } +/* colour palettes (owner request): sepia (the default above) and four calmer colourful ones, each light and dark; + chosen in the display settings (data-palette on ); dark follows the system or the reader's choice */ +:root[data-palette='teal'] { --bg: #f2f8f7; --paper: #fcfefe; --poem: #e2f0ee; --inner: #e9f3f1; --nested: #d4e8e4; --ink: #0f1f1d; --muted: #4a615e; --lapis: #0b4f6c; --gold: #2a9d8f; --gold-light: #b6dad4; --gold-subtle: #e4f2ef; --border: #8db8b1; --brand: #c8553d; --action: #0b4f6c; } +@media (prefers-color-scheme: dark) { :root[data-palette='teal']:not([data-theme='light']) { --bg: #0f1716; --paper: #121c1b; --poem: #152220; --inner: #1a2927; --nested: #20322f; --ink: #dcebe8; --muted: #8fa8a4; --lapis: #e6f4f2; --gold: #3fb5a5; --gold-light: #23413c; --gold-subtle: #172b28; --border: #2f7d72; --brand: #e07a5f; --action: #5cc2b3; --action-ink: #0c1514; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } } +:root[data-palette='teal'][data-theme='dark'] { --bg: #0f1716; --paper: #121c1b; --poem: #152220; --inner: #1a2927; --nested: #20322f; --ink: #dcebe8; --muted: #8fa8a4; --lapis: #e6f4f2; --gold: #3fb5a5; --gold-light: #23413c; --gold-subtle: #172b28; --border: #2f7d72; --brand: #e07a5f; --action: #5cc2b3; --action-ink: #0c1514; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } +:root[data-palette='lapis'] { --bg: #f3f5fb; --paper: #fdfdff; --poem: #e6ebf8; --inner: #ecf0f9; --nested: #dae1f3; --ink: #131a2e; --muted: #4e5772; --lapis: #1d3b8b; --gold: #c49a2c; --gold-light: #d8d0ad; --gold-subtle: #eef0f8; --border: #98a6c8; --brand: #b83254; --action: #1d3b8b; } +@media (prefers-color-scheme: dark) { :root[data-palette='lapis']:not([data-theme='light']) { --bg: #10131c; --paper: #131724; --poem: #171c2c; --inner: #1c2235; --nested: #232a40; --ink: #e1e5f2; --muted: #98a0bb; --lapis: #eef1fb; --gold: #d4b25a; --gold-light: #2c3350; --gold-subtle: #1b2034; --border: #4b5c9a; --brand: #e0607e; --action: #8ea9ee; --action-ink: #0e1220; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } } +:root[data-palette='lapis'][data-theme='dark'] { --bg: #10131c; --paper: #131724; --poem: #171c2c; --inner: #1c2235; --nested: #232a40; --ink: #e1e5f2; --muted: #98a0bb; --lapis: #eef1fb; --gold: #d4b25a; --gold-light: #2c3350; --gold-subtle: #1b2034; --border: #4b5c9a; --brand: #e0607e; --action: #8ea9ee; --action-ink: #0e1220; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } +:root[data-palette='green'] { --bg: #f3f7f0; --paper: #fdfefb; --poem: #e5eede; --inner: #ecf3e7; --nested: #d9e6cf; --ink: #151f0f; --muted: #54614a; --lapis: #1f5d3a; --gold: #a8872e; --gold-light: #d7cea2; --gold-subtle: #eff1e1; --border: #9db38c; --brand: #b5452f; --action: #1f5d3a; } +@media (prefers-color-scheme: dark) { :root[data-palette='green']:not([data-theme='light']) { --bg: #111611; --paper: #141a13; --poem: #182017; --inner: #1d271b; --nested: #243022; --ink: #e0e8d8; --muted: #9aa88e; --lapis: #eef5e8; --gold: #c9a94e; --gold-light: #2c3a27; --gold-subtle: #1c251a; --border: #4f7a45; --brand: #e0705a; --action: #84c79a; --action-ink: #0d150f; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } } +:root[data-palette='green'][data-theme='dark'] { --bg: #111611; --paper: #141a13; --poem: #182017; --inner: #1d271b; --nested: #243022; --ink: #e0e8d8; --muted: #9aa88e; --lapis: #eef5e8; --gold: #c9a94e; --gold-light: #2c3a27; --gold-subtle: #1c251a; --border: #4f7a45; --brand: #e0705a; --action: #84c79a; --action-ink: #0d150f; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } +:root[data-palette='rose'] { --bg: #fbf4f6; --paper: #fffcfd; --poem: #f5e6ea; --inner: #f8edf0; --nested: #eedbe0; --ink: #29141a; --muted: #6c5057; --lapis: #6b2c5e; --gold: #c27c88; --gold-light: #e7c8ce; --gold-subtle: #f7e8ec; --border: #c8a1a9; --brand: #b0304a; --action: #6b2c5e; } +@media (prefers-color-scheme: dark) { :root[data-palette='rose']:not([data-theme='light']) { --bg: #181214; --paper: #1c1518; --poem: #22191c; --inner: #291e22; --nested: #31252a; --ink: #efe0e4; --muted: #b39aa1; --lapis: #fbeef2; --gold: #d792a0; --gold-light: #3e2a30; --gold-subtle: #2a1d21; --border: #8a5563; --brand: #ef6f86; --action: #e3a3c3; --action-ink: #1d1216; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } } +:root[data-palette='rose'][data-theme='dark'] { --bg: #181214; --paper: #1c1518; --poem: #22191c; --inner: #291e22; --nested: #31252a; --ink: #efe0e4; --muted: #b39aa1; --lapis: #fbeef2; --gold: #d792a0; --gold-light: #3e2a30; --gold-subtle: #2a1d21; --border: #8a5563; --brand: #ef6f86; --action: #e3a3c3; --action-ink: #1d1216; --ok: #7cc47f; --danger: #f07a6e; color-scheme: dark; } :root.nastaliq { --font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --verse-lh: 2.7; } * { box-sizing: border-box; } @@ -481,13 +495,23 @@ h1 + .muted { text-align: center; margin-top: 0; } .icon { flex: none; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .icon-btn.small { width: 30px; height: 30px; } -/* the font picker in the header: the icon and a list */ -.font-pick { display: inline-flex; align-items: center; gap: 4px; height: 36px; padding: 0 4px 0 10px; border: 1.5px solid var(--border); border-radius: 999px; - background: var(--inner); color: var(--ink); cursor: pointer; } -.font-pick:hover, .font-pick:focus-within { border-color: var(--brand); color: var(--brand); } -.font-pick select { font: 400 .85rem/1.4 'Noto Naskh Arabic', serif; height: 28px; border: 0; background: transparent; color: inherit; - cursor: pointer; padding: 0 2px; } /* Naskh at a fixed height whatever the reading font (Nastaliq would spill out) */ -.font-pick select:focus { outline: none; } +/* display settings in the header: a button and its panel (font, palette swatches, light/dark/automatic) */ +.display-menu { position: relative; } +.display-menu > summary { list-style: none; } +.display-menu > summary::-webkit-details-marker { display: none; } +.display-menu[open] > summary { border-color: var(--brand); color: var(--brand); } +.display-panel { position: absolute; top: calc(100% + 8px); left: 0; z-index: 30; width: 260px; padding: 10px 14px; border: 1.5px solid var(--border); + border-radius: 14px; background: var(--paper); box-shadow: 0 8px 24px rgb(0 0 0 / .18); font: 400 .9rem/1.8 'Noto Naskh Arabic', serif; color: var(--ink); } +.display-panel p { margin: 6px 0 4px; color: var(--muted); font-size: .8rem; } +.display-panel .seg { display: flex; gap: 4px; } +.display-panel button { display: inline-flex; align-items: center; justify-content: center; gap: 4px; flex: 1; font: inherit; font-size: .85rem; padding: 2px 6px; + border: 1.5px solid var(--border); border-radius: 10px; background: var(--inner); color: var(--ink); cursor: pointer; } +.display-panel button[aria-pressed='true'] { border-color: var(--action); color: var(--action); font-weight: 700; } +.display-panel .f-nastaliq { font-family: 'Noto Nastaliq Urdu', serif; line-height: 2.2; } +.display-panel .swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 4px; } +.display-panel .swatches button { flex-direction: column; font-size: .7rem; padding: 4px 2px; } +.display-panel .swatches span { width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--a) 50%, var(--b) 50%); + box-shadow: 0 0 0 1px rgb(0 0 0 / .2); } .glyph-icon { position: relative; display: inline-flex; } .glyph-icon > span { position: absolute; inset: 0 0 4px 2px; display: flex; align-items: center; justify-content: center; font: 700 11px/1 'Noto Naskh Arabic', serif; } From 3305a514ba856a89ef1fe4c6267bd42482825437 Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Fri, 9 Oct 2026 15:34:38 +0200 Subject: [PATCH 5/5] =?UTF-8?q?Numerals:=20Eastern=20(=DB=B1=DB=B2=DB=B3)?= =?UTF-8?q?=20or=20Western=20(123)=20in=20the=20display=20settings;=20the?= =?UTF-8?q?=20server=20sends=20pages=20with=20the=20chosen=20digits=20(scr?= =?UTF-8?q?ipts=20and=20styles=20untouched)=20and=20script-drawn=20numbers?= =?UTF-8?q?=20follow.=20Settings=20button=20lines=20up=20with=20the=20othe?= =?UTF-8?q?r=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/layouts/Base.astro | 20 ++++++++++++++++++++ web/src/middleware.ts | 12 ++++++++++-- web/src/styles/global.css | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro index d857f6bc..ec139c2d 100644 --- a/web/src/layouts/Base.astro +++ b/web/src/layouts/Base.astro @@ -29,6 +29,7 @@ import Icon from '../components/Icon.astro'; if (t) document.documentElement.dataset.theme = t; 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 } catch (e) {} @@ -66,6 +67,8 @@ import Icon from '../components/Icon.astro'; ))} +

ہندسے

+

روشنی

@@ -122,15 +125,32 @@ import Icon from '../components/Icon.astro'; panel.querySelectorAll('[data-font]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.font === on.font))); panel.querySelectorAll('[data-palette]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.palette === on.palette))); panel.querySelectorAll('[data-theme-pick]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.themePick === on.theme))); + panel.querySelectorAll('[data-digits]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.digits === (root.dataset.digits ?? '')))); }; panel.addEventListener('click', (e) => { const b = e.target.closest('button'); if (!b) return; if (b.dataset.font) { root.classList.toggle('nastaliq', b.dataset.font === 'nastaliq'); store('divan-font', b.dataset.font); } if (b.dataset.palette != null) { if (b.dataset.palette) root.dataset.palette = b.dataset.palette; else delete root.dataset.palette; store('divan-palette', b.dataset.palette); } + if (b.dataset.digits != null) { + // numerals: a cookie, so the server sends pages with them; switching back to Eastern reloads the page + document.cookie = `divan-digits=${b.dataset.digits}; path=/; max-age=${b.dataset.digits ? 31536000 : 0}; samesite=lax`; + if (!b.dataset.digits) { delete root.dataset.digits; location.reload(); return; } + root.dataset.digits = 'latn'; westernDigits(document.body); + } if (b.dataset.themePick != null) { if (b.dataset.themePick) root.dataset.theme = b.dataset.themePick; else delete root.dataset.theme; store('divan-theme', b.dataset.themePick); } marks(); + }); document.addEventListener('click', (e) => { if (panel.open && !panel.contains(e.target)) panel.open = false; }); // close when clicking elsewhere + // Western numerals also in what scripts add later (word sidebar, page numbers, suggestions) + function westernDigits(node) { + const walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT); + for (let t = walk.nextNode(); t; t = walk.nextNode()) if (/[۰-۹]/.test(t.data)) t.data = t.data.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x6f0)); + } + new MutationObserver((list) => { + if (root.dataset.digits !== 'latn') return; + for (const m of list) for (const n of m.addedNodes) n.nodeType === 3 ? westernDigits(n.parentNode) : n.nodeType === 1 && westernDigits(n); + }).observe(document.body, { childList: true, subtree: true }); marks(); // Urdu on-screen keyboard, Gboard/Windows Urdu layout const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']]; diff --git a/web/src/middleware.ts b/web/src/middleware.ts index d2ff4d2d..d0d91be4 100644 --- a/web/src/middleware.ts +++ b/web/src/middleware.ts @@ -1,4 +1,6 @@ -// The signed-in reader (or null) for every page, from the session cookie +// The signed-in reader (or null) for every page, from the session cookie; and the reader's numerals: pages are +// written with Eastern Arabic (Urdu) digits, and a reader who chose Western digits (cookie divan-digits=latn, set in +// the display settings) gets them in the page as sent, so nothing flashes import { defineMiddleware } from 'astro:middleware'; import { auth, COOKIE } from './lib/auth'; @@ -10,5 +12,11 @@ export const onRequest = defineMiddleware(async (ctx, next) => { if (r.ok) ctx.locals.user = r.data.user; else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere } - return next(); + const res = await next(); + if (ctx.cookies.get('divan-digits')?.value !== 'latn' || !res.headers.get('content-type')?.includes('text/html')) return res; + // text only: scripts and styles are left alone (the page's own scripts look for Eastern digits) + const html = (await res.text()).split(/(|)/) + .map((part, i) => (i % 2 ? part : part.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x6f0)))).join(''); + const headers = new Headers(res.headers); headers.delete('content-length'); + return new Response(html, { status: res.status, statusText: res.statusText, headers }); }); diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 55fde0b3..7da92a46 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -496,7 +496,7 @@ h1 + .muted { text-align: center; margin-top: 0; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .icon-btn.small { width: 30px; height: 30px; } /* display settings in the header: a button and its panel (font, palette swatches, light/dark/automatic) */ -.display-menu { position: relative; } +.display-menu { position: relative; display: flex; } /* no line box: lines up with the other header icons */ .display-menu > summary { list-style: none; } .display-menu > summary::-webkit-details-marker { display: none; } .display-menu[open] > summary { border-color: var(--brand); color: var(--brand); }