Merge branch 'feature/icons-palette' into main: header icons, colour roles, palettes, display settings, Eastern/Western numerals
This commit is contained in:
commit
1e3fd3066e
39
web/src/components/Icon.astro
Normal file
39
web/src/components/Icon.astro
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
// 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: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
||||||
|
edit: '<path d="M12 20h9"/><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z"/>',
|
||||||
|
bookmark: '<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/>',
|
||||||
|
// the word book: a book; the letter ع is laid over it as text (.glyph-icon), which every browser draws the same
|
||||||
|
words: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>',
|
||||||
|
user: '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
|
||||||
|
login: '<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="m10 17 5-5-5-5"/><path d="M15 12H3"/>',
|
||||||
|
logout: '<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="m16 17 5-5-5-5"/><path d="M21 12H9"/>',
|
||||||
|
theme: '<circle cx="12" cy="12" r="10"/><path d="M12 18a6 6 0 0 0 0-12v12z"/>',
|
||||||
|
font: '<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>',
|
||||||
|
tag: '<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r="1" fill="currentColor"/>',
|
||||||
|
arrange: '<path d="M10 12h11"/><path d="M10 18h11"/><path d="M10 6h11"/><path d="M4 10h2"/><path d="M4 6h1v4"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/>',
|
||||||
|
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||||||
|
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
|
||||||
|
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
|
||||||
|
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||||
|
close: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||||
|
undo: '<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/>',
|
||||||
|
send: '<path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/>',
|
||||||
|
save: '<path d="M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/><path d="M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7"/><path d="M7 3v4a1 1 0 0 0 1 1h7"/>',
|
||||||
|
full: '<path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/>',
|
||||||
|
next: '<path d="m15 18-6-6 6-6"/>', // points left: forward in a right-to-left page
|
||||||
|
prev: '<path d="m9 18 6-6-6-6"/>',
|
||||||
|
trash: '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>',
|
||||||
|
key: '<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/>',
|
||||||
|
image: '<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.828 0L6 21"/>',
|
||||||
|
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||||
|
settings: '<path d="M21 4h-7"/><path d="M10 4H3"/><path d="M21 12h-9"/><path d="M8 12H3"/><path d="M21 20h-5"/><path d="M12 20H3"/><path d="M14 2v4"/><path d="M8 10v4"/><path d="M16 18v4"/>',
|
||||||
|
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
||||||
|
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
|
||||||
|
external: '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
<svg class="icon" viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" set:html={PATHS[name]} />
|
||||||
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
// tabs between the reader's bookmarks and their word book (kept separate)
|
// tabs of the reader's own space: saved texts, favourite writers, the word book (kept separate), profile
|
||||||
const { current } = Astro.props as { current: 'bookmarks' | 'words' };
|
const { current } = Astro.props as { current: 'bookmarks' | 'writers' | 'words' };
|
||||||
---
|
---
|
||||||
<nav class="admin-nav">
|
<nav class="admin-nav">
|
||||||
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> نشانات</a>
|
<a href="/library" aria-current={current === 'bookmarks' ? 'page' : undefined}><span class="bm-icon on" aria-hidden="true"></span> محفوظ شدہ</a>
|
||||||
|
<a href="/writers" aria-current={current === 'writers' ? 'page' : undefined}>پسندیدہ لکھاری</a>
|
||||||
<a href="/words" aria-current={current === 'words' ? 'page' : undefined}>ذخیرۂ الفاظ</a>
|
<a href="/words" aria-current={current === 'words' ? 'page' : undefined}>ذخیرۂ الفاظ</a>
|
||||||
<a href="/account">پروفائل</a>
|
<a href="/account">پروفائل</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -3,4 +3,4 @@
|
|||||||
const { s } = Astro.props as { s: [string, string, string?][] };
|
const { s } = Astro.props as { s: [string, string, string?][] };
|
||||||
---
|
---
|
||||||
{s.map(([t, k, d]) => (k === 'mark' ? <mark>{t}</mark> : k === 'pen' ? <span class="takhallus">{t}</span>
|
{s.map(([t, k, d]) => (k === 'mark' ? <mark>{t}</mark> : k === 'pen' ? <span class="takhallus">{t}</span>
|
||||||
: k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ نشان ہٹائیں؟" title="نشان زدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))}
|
: k === 'bmk' ? <span class="bmk" data-save={d} data-confirm="یہ عبارت محفوظ شدہ سے ہٹائیں؟" title="محفوظ شدہ عبارت (ہٹانے کے لیے دبائیں)">{t}</span> : t))}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import '../styles/global.css';
|
|||||||
interface Props { title?: string; description?: string; q?: string }
|
interface Props { title?: string; description?: string; q?: string }
|
||||||
const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props;
|
const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props;
|
||||||
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر';
|
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر';
|
||||||
|
import Icon from '../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="ur-PK" dir="rtl">
|
<html lang="ur-PK" dir="rtl">
|
||||||
@ -18,7 +19,7 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
|||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// font + theme choice applied before paint (Naskh and system theme by default)
|
// font, palette and light/dark applied before paint (Naskh, sepia and the system's light/dark by default)
|
||||||
try {
|
try {
|
||||||
// ?font=nastaliq|naskh in the URL sets the choice (shareable links)
|
// ?font=nastaliq|naskh in the URL sets the choice (shareable links)
|
||||||
const f = new URLSearchParams(location.search).get('font');
|
const f = new URLSearchParams(location.search).get('font');
|
||||||
@ -26,6 +27,9 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
|||||||
if (localStorage.getItem('divan-font') === 'nastaliq') document.documentElement.classList.add('nastaliq');
|
if (localStorage.getItem('divan-font') === 'nastaliq') document.documentElement.classList.add('nastaliq');
|
||||||
const t = localStorage.getItem('divan-theme');
|
const t = localStorage.getItem('divan-theme');
|
||||||
if (t) document.documentElement.dataset.theme = t;
|
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) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
@ -43,17 +47,43 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
|||||||
<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>
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="toggles">
|
<nav class="toggles" aria-label="سائٹ">
|
||||||
{Astro.locals.user?.role === 'admin' && <a class="account-link" href="/admin">ایڈمن</a>}
|
{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="account-link" href="/mod">موڈریشن</a>}
|
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="icon-btn" href="/mod" title="موڈریشن" aria-label="موڈریشن"><Icon name="edit" /></a>}
|
||||||
{Astro.locals.user && <a class="account-link" href="/library">نشانات</a>}
|
{Astro.locals.user && <a class="icon-btn" href="/library" title="محفوظ شدہ" aria-label="محفوظ شدہ"><Icon name="bookmark" /></a>}
|
||||||
{Astro.locals.user && <a class="account-link" href="/words">ذخیرۂ الفاظ</a>}
|
{Astro.locals.user && <a class="icon-btn" href="/words" title="ذخیرۂ الفاظ" aria-label="ذخیرۂ الفاظ"><span class="glyph-icon"><Icon name="words" /><span aria-hidden="true">ع</span></span></a>}
|
||||||
{Astro.locals.user
|
{Astro.locals.user
|
||||||
? <a class="account-link" href="/account" title={Astro.locals.user.email}>{Astro.locals.user.full_name.split(' ')[0] || 'اکاؤنٹ'}</a>
|
? <a class="icon-btn" href="/account" title={`اکاؤنٹ: ${Astro.locals.user.full_name || Astro.locals.user.email}`} aria-label="اکاؤنٹ"><Icon name="user" /></a>
|
||||||
: <a class="account-link" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`}>لاگ ان</a>}
|
: <a class="icon-btn" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`} title="لاگ ان" aria-label="لاگ ان"><Icon name="login" /></a>}
|
||||||
<button type="button" id="font-toggle" aria-label="خط بدلیں">نسخ</button>
|
<details class="display-menu">
|
||||||
<button type="button" id="theme-toggle" aria-label="روشن یا تاریک">◐</button>
|
<summary class="icon-btn" title="خط اور رنگ" aria-label="خط اور رنگ"><Icon name="settings" /></summary>
|
||||||
|
<div class="display-panel">
|
||||||
|
<p>خط</p>
|
||||||
|
<div class="seg"><button type="button" data-font="naskh">نسخ</button><button type="button" data-font="nastaliq" class="f-nastaliq">نستعلیق</button></div>
|
||||||
|
<p>رنگ</p>
|
||||||
|
<div class="swatches">
|
||||||
|
{[['', 'سپیا', '#fbf6ea', '#bf3f2a'], ['teal', 'فیروزی', '#e2f0ee', '#0b4f6c'], ['lapis', 'نیلگوں', '#e6ebf8', '#1d3b8b'],
|
||||||
|
['green', 'سبز', '#e5eede', '#1f5d3a'], ['rose', 'گلابی', '#f5e6ea', '#6b2c5e']].map(([v, name, a, b]) => (
|
||||||
|
<button type="button" data-palette={v} title={name} aria-label={name} style={`--a: ${a}; --b: ${b}`}><span></span>{name}</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<p>ہندسے</p>
|
||||||
|
<div class="seg"><button type="button" data-digits="">۱۲۳ مشرقی</button><button type="button" data-digits="latn">123 مغربی</button></div>
|
||||||
|
<p>روشنی</p>
|
||||||
|
<div class="seg">
|
||||||
|
<button type="button" data-theme-pick="light"><Icon name="sun" size={16} /> روشن</button>
|
||||||
|
<button type="button" data-theme-pick="dark"><Icon name="moon" size={16} /> تاریک</button>
|
||||||
|
<button type="button" data-theme-pick=""><Icon name="theme" size={16} /> خودکار</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{Astro.locals.user && (
|
||||||
|
<form method="post" action="/account" class="inline-form">
|
||||||
|
<input type="hidden" name="act" value="signout" />
|
||||||
|
<button class="icon-btn danger" title="لاگ آؤٹ" aria-label="لاگ آؤٹ"><Icon name="logout" /></button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div id="kb" class="wrap kb" hidden></div>
|
<div id="kb" class="wrap kb" hidden></div>
|
||||||
</header>
|
</header>
|
||||||
@ -87,14 +117,41 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
|||||||
</footer>
|
</footer>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const fontBtn = document.getElementById('font-toggle');
|
// display settings: reading font, colour palette, light/dark/automatic; each choice is remembered
|
||||||
const label = () => (fontBtn.textContent = root.classList.contains('nastaliq') ? 'نستعلیق' : 'نسخ');
|
const panel = document.querySelector('.display-menu');
|
||||||
label();
|
const store = (k, v) => { try { v ? localStorage.setItem(k, v) : localStorage.removeItem(k); } catch (e) {} };
|
||||||
fontBtn.onclick = () => {
|
const marks = () => {
|
||||||
root.classList.toggle('nastaliq');
|
const on = { font: root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh', palette: root.dataset.palette ?? '', theme: root.dataset.theme ?? '' };
|
||||||
try { localStorage.setItem('divan-font', root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh'); } catch (e) {}
|
panel.querySelectorAll('[data-font]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.font === on.font)));
|
||||||
label();
|
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
|
// Urdu on-screen keyboard, Gboard/Windows Urdu layout
|
||||||
const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']];
|
const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']];
|
||||||
const SHIFT = [['ظ','ض','ذ','ڈ','ث','ّ','ۃ','ـ','چ','خ'], ['ژ','ز','ڑ','ں','ۂ','ء','آ','گ',''], ['','','ۓ','','ؤ','ئ','']];
|
const SHIFT = [['ظ','ض','ذ','ڈ','ث','ّ','ۃ','ـ','چ','خ'], ['ژ','ز','ڑ','ں','ۂ','ء','آ','گ',''], ['','','ۓ','','ؤ','ئ','']];
|
||||||
@ -396,11 +453,6 @@ const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو ک
|
|||||||
if (!menu.hidden) hideMenu(); else if (!dict.hidden) closeDict();
|
if (!menu.hidden) hideMenu(); else if (!dict.hidden) closeDict();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('theme-toggle').onclick = () => {
|
|
||||||
const dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
root.dataset.theme = dark ? 'light' : 'dark';
|
|
||||||
try { localStorage.setItem('divan-theme', root.dataset.theme); } catch (e) {}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export async function pdfReader(root: HTMLElement) {
|
|||||||
const full = root.querySelector<HTMLButtonElement>('.pdf-full')!;
|
const full = root.querySelector<HTMLButtonElement>('.pdf-full')!;
|
||||||
const toggle = () => (document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen()).catch(() => {});
|
const toggle = () => (document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen()).catch(() => {});
|
||||||
full.onclick = toggle;
|
full.onclick = toggle;
|
||||||
document.addEventListener('fullscreenchange', () => { full.textContent = document.fullscreenElement === root ? 'واپس' : 'پوری اسکرین'; show(page); });
|
document.addEventListener('fullscreenchange', () => { full.title = document.fullscreenElement === root ? 'پوری اسکرین سے واپس (F)' : 'پوری اسکرین (F)'; show(page); });
|
||||||
// a click on the half the book turns towards goes forward
|
// a click on the half the book turns towards goes forward
|
||||||
pagesBox.onclick = (e) => { const left = e.clientX < pagesBox.getBoundingClientRect().left + pagesBox.clientWidth / 2; step(left === rtl ? 1 : -1); };
|
pagesBox.onclick = (e) => { const left = e.clientX < pagesBox.getBoundingClientRect().left + pagesBox.clientWidth / 2; step(left === rtl ? 1 : -1); };
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
|||||||
@ -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 { defineMiddleware } from 'astro:middleware';
|
||||||
import { auth, COOKIE } from './lib/auth';
|
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;
|
if (r.ok) ctx.locals.user = r.data.user;
|
||||||
else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere
|
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(/(<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>)/)
|
||||||
|
.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 });
|
||||||
});
|
});
|
||||||
|
|||||||
@ -82,6 +82,7 @@ const ebookCount = page.type === 'poet' ? ((await api<any[]>(`/api/ebooks?poet=$
|
|||||||
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
|
const tagTarget = page.type === 'poem' ? `work/${poemId}` : `category/${page.category?.id}`;
|
||||||
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
|
const bookmark = (c: number) => JSON.stringify({ kind: 'couplet', poemId, couplet: c });
|
||||||
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
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 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' && (
|
{page.type !== 'poet' && (
|
||||||
@ -95,8 +96,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
|
|||||||
{page.type === 'poet' && (
|
{page.type === 'poet' && (
|
||||||
<>
|
<>
|
||||||
<h1>{page.poet.name}</h1>
|
<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">ترتیب دیں</button></form>
|
<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>
|
||||||
<a class="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
|
<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>
|
<p class="muted">{years(page.poet)}</p>
|
||||||
<Tags tags={pageTags} />
|
<Tags tags={pageTags} />
|
||||||
{page.poet.description && <div class="intro">{page.poet.description}</div>}
|
{page.poet.description && <div class="intro">{page.poet.description}</div>}
|
||||||
@ -106,8 +107,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
|
|||||||
<>
|
<>
|
||||||
<h1>{page.category.title}</h1>
|
<h1>{page.category.title}</h1>
|
||||||
<Tags tags={pageTags} />
|
<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">ترتیب دیں</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>{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="mod-history" href={`/mod/order/${page.category.id}`}>ترتیب کا تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
|
<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,8 +136,8 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
|
|||||||
</p>
|
</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>
|
||||||
{modCan.edit && <> <form method="post" action={`/mod/work/${poemId}`} class="inline-form"><button class="mod-edit-btn">ترمیم کریں</button></form>
|
{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="mod-history" href={`/mod/work/${poemId}`}>تاریخچہ</a></>}{tagCan && <> <form method="post" action={`/mod/tags/${tagTarget}`} class="inline-form"><button class="mod-edit-btn">ٹیگ</button></form></>}</div>
|
<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} />
|
<Tags tags={pageTags} />
|
||||||
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
|
{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}>
|
<article class="poem" data-cite={`${page.poet.nickname}، ${page.poem.title}`} data-poem={poemId}>
|
||||||
|
|||||||
@ -18,11 +18,12 @@ const cover = (b: any) => (b.has_cover ? `/ebooks/cover/${b.id}` : b.kind === 'a
|
|||||||
const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
|
const token = Astro.locals.user ? Astro.cookies.get(COOKIE)?.value : undefined;
|
||||||
const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user!.role)
|
const mayAdd = token && ['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user!.role)
|
||||||
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
|
&& (await asUser(token, `/api/mod/can?category=${page.category.id}`)).data.ebooks;
|
||||||
|
import Icon from '../../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title={`ای بکس · ${page.poet.nickname}`}>
|
<Base title={`ای بکس · ${page.poet.nickname}`}>
|
||||||
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
|
<nav class="crumbs"><a href={page.poet.url}>{page.poet.nickname}</a></nav>
|
||||||
<h1>ای بکس</h1>
|
<h1>ای بکس</h1>
|
||||||
{mayAdd && <p class="center"><a class="mod-edit-btn" href={`/mod/ebook/new?poet=${page.poet.id}`}>ای بک شامل کریں</a></p>}
|
{mayAdd && <p class="center"><a class="btn primary" href={`/mod/ebook/new?poet=${page.poet.id}`}><Icon name="plus" size={18} /> ای بک شامل کریں</a></p>}
|
||||||
<nav class="admin-nav ebook-tabs" aria-label="کتابوں کی قسم">
|
<nav class="admin-nav ebook-tabs" aria-label="کتابوں کی قسم">
|
||||||
{tabs.map(([k, ur, en]) => {
|
{tabs.map(([k, ur, en]) => {
|
||||||
const n = books.filter((b) => b.kind === k).length;
|
const n = books.filter((b) => b.kind === k).length;
|
||||||
|
|||||||
@ -36,6 +36,7 @@ if (Astro.request.method === 'POST') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const since = new Date(user.created_at);
|
const since = new Date(user.created_at);
|
||||||
|
import Icon from '../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title="میرا اکاؤنٹ">
|
<Base title="میرا اکاؤنٹ">
|
||||||
<h1>{user.full_name || 'میرا اکاؤنٹ'}</h1>
|
<h1>{user.full_name || 'میرا اکاؤنٹ'}</h1>
|
||||||
@ -49,12 +50,12 @@ const since = new Date(user.created_at);
|
|||||||
<input type="hidden" name="act" value="profile" />
|
<input type="hidden" name="act" value="profile" />
|
||||||
<label>پورا نام<input name="full_name" value={user.full_name} maxlength="100" dir="auto" autocomplete="name" /></label>
|
<label>پورا نام<input name="full_name" value={user.full_name} maxlength="100" dir="auto" autocomplete="name" /></label>
|
||||||
<label>تعارف<textarea name="bio" maxlength="1000" rows="4" dir="auto">{user.bio}</textarea></label>
|
<label>تعارف<textarea name="bio" maxlength="1000" rows="4" dir="auto">{user.bio}</textarea></label>
|
||||||
<button type="submit">محفوظ کریں</button>
|
<button type="submit" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" class="account-form">
|
<form method="post" class="account-form">
|
||||||
<input type="hidden" name="act" value="signout" />
|
<input type="hidden" name="act" value="signout" />
|
||||||
<button type="submit">لاگ آؤٹ</button>
|
<button type="submit" class="btn danger"><Icon name="logout" size={18} /> لاگ آؤٹ</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" class="account-form">
|
<form method="post" class="account-form">
|
||||||
@ -63,7 +64,7 @@ const since = new Date(user.created_at);
|
|||||||
<label>موجودہ پاس ورڈ<input type="password" name="current" required autocomplete="current-password" dir="ltr" /></label>
|
<label>موجودہ پاس ورڈ<input type="password" name="current" required autocomplete="current-password" dir="ltr" /></label>
|
||||||
<label>نیا پاس ورڈ<input type="password" name="next" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
|
<label>نیا پاس ورڈ<input type="password" name="next" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
|
||||||
<label>نیا پاس ورڈ دوبارہ<input type="password" name="next2" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
|
<label>نیا پاس ورڈ دوبارہ<input type="password" name="next2" required minlength="8" autocomplete="new-password" dir="ltr" /></label>
|
||||||
<button type="submit">پاس ورڈ بدلیں</button>
|
<button type="submit" class="btn"><Icon name="key" size={18} /> پاس ورڈ بدلیں</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form method="post" class="account-form danger">
|
<form method="post" class="account-form danger">
|
||||||
@ -71,6 +72,6 @@ const since = new Date(user.created_at);
|
|||||||
<p class="muted">اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔</p>
|
<p class="muted">اکاؤنٹ اور اس کا تمام ڈیٹا مستقل طور پر حذف ہو جائے گا۔</p>
|
||||||
<input type="hidden" name="act" value="delete" />
|
<input type="hidden" name="act" value="delete" />
|
||||||
<label>پاس ورڈ<input type="password" name="password" required autocomplete="current-password" dir="ltr" /></label>
|
<label>پاس ورڈ<input type="password" name="password" required autocomplete="current-password" dir="ltr" /></label>
|
||||||
<button type="submit">اکاؤنٹ مستقل طور پر ختم کریں</button>
|
<button type="submit" class="btn danger"><Icon name="trash" size={18} /> اکاؤنٹ مستقل طور پر ختم کریں</button>
|
||||||
</form>
|
</form>
|
||||||
</Base>
|
</Base>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const mayCover = token && poetPage && b.kind !== 'archive' && ['mod-l2', 'mod-l1
|
|||||||
// the reader's bookmarked pages of this book
|
// the reader's bookmarked pages of this book
|
||||||
const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/api/library/state?ebook=${b.id}`)).data.pages ?? [] : [];
|
const marks = token && b.published && b.kind === 'pdf' ? (await asUser(token, `/api/library/state?ebook=${b.id}`)).data.pages ?? [] : [];
|
||||||
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
|
const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size / 1024)))} KB` : `${ud((b.size / 1048576).toFixed(1))} MB`;
|
||||||
|
import Icon from '../../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title={`${b.title} · ${b.poet}`}>
|
<Base title={`${b.title} · ${b.poet}`}>
|
||||||
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
|
<nav class="crumbs"><a href={b.poet_url}>{b.poet}</a> » <a href={`${b.poet_url}/ebooks?kind=${b.kind}`}>ای بکس</a></nav>
|
||||||
@ -25,7 +26,7 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
|
|||||||
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
|
{!b.published && <p class="form-error center">یہ کتاب ابھی جائزے میں ہے؛ صرف موڈریٹر دیکھ سکتے ہیں۔</p>}
|
||||||
<p class="center muted">
|
<p class="center muted">
|
||||||
{KIND[b.kind]}{mb && <> · {mb}</>}{b.source && <> · ماخذ: {b.source}</>}{b.licence && <> · {b.licence}</>}
|
{KIND[b.kind]}{mb && <> · {mb}</>}{b.source && <> · ماخذ: {b.source}</>}{b.licence && <> · {b.licence}</>}
|
||||||
{b.file && <> · <a href={file} download>ڈاؤن لوڈ</a></>}
|
{b.file && <> · <a class="btn" href={file} download><Icon name="download" size={17} /> ڈاؤن لوڈ</a></>}
|
||||||
{b.kind === 'archive' && <> · <a href={`https://archive.org/details/${b.archive_id}`}>archive.org پر</a></>}
|
{b.kind === 'archive' && <> · <a href={`https://archive.org/details/${b.archive_id}`}>archive.org پر</a></>}
|
||||||
</p>
|
</p>
|
||||||
<p class="center eb-people">
|
<p class="center eb-people">
|
||||||
@ -38,8 +39,8 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
|
|||||||
{mayCover && (
|
{mayCover && (
|
||||||
<p class="center cover-tools" data-id={b.id} data-kind={b.kind} data-src={file}>
|
<p class="center cover-tools" data-id={b.id} data-kind={b.kind} data-src={file}>
|
||||||
{b.cover && <img class="cover-small" src={`/ebooks/cover/${b.id}`} alt="سرورق" />}
|
{b.cover && <img class="cover-small" src={`/ebooks/cover/${b.id}`} alt="سرورق" />}
|
||||||
{(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make>{b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>}
|
{(b.kind === 'pdf' || b.kind === 'epub') && <button type="button" class="mod-edit-btn" data-make><Icon name="image" size={17} /> {b.kind === 'pdf' ? 'پہلے صفحے سے سرورق بنائیں' : 'کتاب کا سرورق لیں'}</button>}
|
||||||
<label class="mod-edit-btn">اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label>
|
<label class="mod-edit-btn"><Icon name="upload" size={17} /> اپنی تصویر سرورق بنائیں<input type="file" accept="image/jpeg,image/png,image/webp" hidden data-own /></label>
|
||||||
<span class="muted" data-cover-status></span>
|
<span class="muted" data-cover-status></span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@ -47,15 +48,15 @@ const mb = !b.size ? '' : b.size < 1048576 ? `${ud(Math.max(1, Math.round(b.size
|
|||||||
{b.kind === 'pdf' && (
|
{b.kind === 'pdf' && (
|
||||||
<div class="pdf-reader" data-src={file} data-ebook={b.id} data-marks={JSON.stringify(marks)} dir={b.direction === 'ltr' ? 'ltr' : 'rtl'}>
|
<div class="pdf-reader" data-src={file} data-ebook={b.id} data-marks={JSON.stringify(marks)} dir={b.direction === 'ltr' ? 'ltr' : 'rtl'}>
|
||||||
<nav class="pdf-bar">
|
<nav class="pdf-bar">
|
||||||
<button type="button" class="mod-edit-btn pdf-prev">‹ پچھلا</button>
|
<button type="button" class="icon-btn pdf-prev" title="پچھلا صفحہ" aria-label="پچھلا صفحہ"><Icon name={b.direction === 'ltr' ? 'next' : 'prev'} /></button>
|
||||||
<span>صفحہ <input class="pdf-page" type="number" min="1" value="1" aria-label="صفحہ نمبر" /> از <span class="pdf-total">…</span></span>
|
<span>صفحہ <input class="pdf-page" type="number" min="1" value="1" aria-label="صفحہ نمبر" /> از <span class="pdf-total">…</span></span>
|
||||||
<button type="button" class="mod-edit-btn pdf-next">اگلا ›</button>
|
<button type="button" class="icon-btn pdf-next" title="اگلا صفحہ" aria-label="اگلا صفحہ"><Icon name={b.direction === 'ltr' ? 'prev' : 'next'} /></button>
|
||||||
<button type="button" class="mod-edit-btn pdf-full" title="پوری اسکرین (F)">پوری اسکرین</button>
|
<button type="button" class="icon-btn pdf-full" title="پوری اسکرین (F)" aria-label="پوری اسکرین"><Icon name="full" /></button>
|
||||||
{b.published && <button type="button" class="save pdf-mark" aria-pressed="false">صفحہ محفوظ کریں</button>}
|
{b.published && <button type="button" class="save pdf-mark" aria-pressed="false">صفحہ محفوظ کریں</button>}
|
||||||
</nav>
|
</nav>
|
||||||
<p class="tag-chips pdf-marks" hidden></p>
|
<p class="tag-chips pdf-marks" hidden></p>
|
||||||
<div class="pdf-pages" title="بائیں طرف کلک: اگلا صفحہ"><p class="muted">کتاب کھل رہی ہے…</p></div>
|
<div class="pdf-pages" title="بائیں طرف کلک: اگلا صفحہ"><p class="muted">کتاب کھل رہی ہے…</p></div>
|
||||||
<p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener">نئی ٹیب میں کھولیں</a></p>
|
<p class="muted center small-line" dir="rtl">{b.direction === 'ltr' ? '→ اگلا صفحہ · ← پچھلا صفحہ' : '← اگلا صفحہ · → پچھلا صفحہ'} · <a href={file} target="_blank" rel="noopener"><Icon name="external" size={15} /> نئی ٹیب میں کھولیں</a></p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{b.kind === 'archive' && <iframe class="ebook-frame" src={`https://archive.org/embed/${b.archive_id}`} title={b.title} allowfullscreen></iframe>}
|
{b.kind === 'archive' && <iframe class="ebook-frame" src={`https://archive.org/embed/${b.archive_id}`} title={b.title} allowfullscreen></iframe>}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
// نشانات: the reader's bookmarks (poets, books/chapters, works, couplets, phrases), each with its full path in
|
// محفوظ شدہ: the reader's saved books/chapters, works, couplets, phrases and e-book pages, each with its full path in
|
||||||
// the site (poet » book » chapter » work) and a link to the couplet. The word book is separate: /words.
|
// the site (poet » book » chapter » work) and a link to the couplet. The word book is separate: /words.
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
import { asUser, COOKIE } from '../lib/auth';
|
import { asUser, COOKIE } from '../lib/auth';
|
||||||
@ -17,7 +17,7 @@ if (Astro.request.method === 'POST') {
|
|||||||
return Astro.redirect(`/library#i${id}`, 303);
|
return Astro.redirect(`/library#i${id}`, 303);
|
||||||
}
|
}
|
||||||
const lib = (await asUser(token, '/api/library')).data;
|
const lib = (await asUser(token, '/api/library')).data;
|
||||||
const total = lib.poets.length + lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0);
|
const total = lib.categories.length + lib.poems.length + lib.couplets.length + lib.phrases.length + (lib.pages?.length ?? 0);
|
||||||
// a bookmarked phrase shown inside its couplet: [before, phrase, after] per line
|
// a bookmarked phrase shown inside its couplet: [before, phrase, after] per line
|
||||||
const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).map((part, i) => {
|
const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).map((part, i) => {
|
||||||
if (!part) return [lines[i], '', ''];
|
if (!part) return [lines[i], '', ''];
|
||||||
@ -26,28 +26,19 @@ const marked = (lines: string[], phrase: string) => phraseParts(lines, phrase).m
|
|||||||
});
|
});
|
||||||
const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != null ? `#c${item.poem.couplet}` : ''}` : '#');
|
const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != null ? `#c${item.poem.couplet}` : ''}` : '#');
|
||||||
---
|
---
|
||||||
<Base title="نشانات">
|
<Base title="محفوظ شدہ">
|
||||||
<h1>{me.full_name || 'نشانات'}</h1>
|
<h1>{me.full_name || 'محفوظ شدہ'}</h1>
|
||||||
{me.bio && <p class="profile-bio">{me.bio}</p>}
|
{me.bio && <p class="profile-bio">{me.bio}</p>}
|
||||||
<LibraryNav current="bookmarks" />
|
<LibraryNav current="bookmarks" />
|
||||||
<p class="muted center">{ud(total)} نشانات</p>
|
<p class="muted center">{ud(total)} محفوظ شدہ</p>
|
||||||
|
|
||||||
{total === 0 && (
|
{total === 0 && (
|
||||||
<p class="muted center">ابھی کچھ محفوظ نہیں۔ کسی شاعر، کتاب، باب یا کلام پر "محفوظ کریں" دبائیں، کسی شعر کے ساتھ والا نشان دبائیں، یا کوئی عبارت منتخب کر کے "محفوظ کریں" دبائیں۔</p>
|
<p class="muted center">ابھی کچھ محفوظ نہیں۔ کسی کتاب، باب یا کلام پر "محفوظ کریں" دبائیں، کسی شعر کے ساتھ والا نشان دبائیں، یا کوئی عبارت منتخب کر کے "محفوظ کریں" دبائیں۔</p>
|
||||||
)}
|
|
||||||
|
|
||||||
{lib.poets.length > 0 && (
|
|
||||||
<section class="lib-section">
|
|
||||||
<h2>نشان زد شعرا</h2>
|
|
||||||
<div class="poets">
|
|
||||||
{lib.poets.map((p: any) => p.poet && <a class="poet-card" id={`i${p.id}`} href={p.poet.url}>{p.poet.name}</a>)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{lib.categories.length > 0 && (
|
{lib.categories.length > 0 && (
|
||||||
<section class="lib-section">
|
<section class="lib-section">
|
||||||
<h2>نشان زد کتابیں اور ابواب</h2>
|
<h2>محفوظ کتابیں اور ابواب</h2>
|
||||||
<ul class="lib-list">
|
<ul class="lib-list">
|
||||||
{lib.categories.map((c: any) => (
|
{lib.categories.map((c: any) => (
|
||||||
<li id={`i${c.id}`}>
|
<li id={`i${c.id}`}>
|
||||||
@ -64,7 +55,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
|
|||||||
|
|
||||||
{lib.poems.length > 0 && (
|
{lib.poems.length > 0 && (
|
||||||
<section class="lib-section">
|
<section class="lib-section">
|
||||||
<h2>نشان زد کلام</h2>
|
<h2>محفوظ کلام</h2>
|
||||||
<ul class="lib-list">
|
<ul class="lib-list">
|
||||||
{lib.poems.map((p: any) => (
|
{lib.poems.map((p: any) => (
|
||||||
<li id={`i${p.id}`}>
|
<li id={`i${p.id}`}>
|
||||||
@ -81,7 +72,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
|
|||||||
|
|
||||||
{lib.couplets.length > 0 && (
|
{lib.couplets.length > 0 && (
|
||||||
<section class="lib-section">
|
<section class="lib-section">
|
||||||
<h2>نشان زد اشعار</h2>
|
<h2>محفوظ اشعار</h2>
|
||||||
<ul class="lib-list">
|
<ul class="lib-list">
|
||||||
{lib.couplets.map((c: any) => (
|
{lib.couplets.map((c: any) => (
|
||||||
<li id={`i${c.id}`}>
|
<li id={`i${c.id}`}>
|
||||||
@ -103,7 +94,7 @@ const at = (item: any) => (item.poem ? `${item.poem.url}${item.poem.couplet != n
|
|||||||
|
|
||||||
{lib.phrases.length > 0 && (
|
{lib.phrases.length > 0 && (
|
||||||
<section class="lib-section">
|
<section class="lib-section">
|
||||||
<h2>نشان زد عبارتیں</h2>
|
<h2>محفوظ عبارتیں</h2>
|
||||||
<ul class="lib-list">
|
<ul class="lib-list">
|
||||||
{lib.phrases.map((p: any) => (
|
{lib.phrases.map((p: any) => (
|
||||||
<li id={`i${p.id}`}>
|
<li id={`i${p.id}`}>
|
||||||
|
|||||||
@ -42,7 +42,10 @@ const ebook = rev.entity === 'ebook';
|
|||||||
const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'],
|
const EB: [string, string][] = [['title', 'عنوان'], ['writer', 'مصنف'], ['coauthors', 'شریک مصنفین'], ['language', 'زبان'], ['direction', 'سمت'],
|
||||||
['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']];
|
['tags', 'ٹیگ'], ['source', 'ماخذ'], ['licence', 'اجازت'], ['note', 'تفصیل']];
|
||||||
const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {};
|
const ebMeta = ebook ? Object.fromEntries(EB.map(([k, label]) => [k, (rev.content.split('\n').find((l: string) => l.startsWith(label + ':')) ?? '').slice(label.length + 1).trim()])) : {};
|
||||||
const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیجیں'], ['reject', 'مسترد کریں'], ['publish', 'شائع کریں']] as const).filter(([a]) => may[a]);
|
// each action in its colour and with its icon: approve and publish confirm, return goes back, reject is destructive
|
||||||
|
const acts = ([['approve', 'منظور کریں', 'ok', 'check'], ['return', 'واپس بھیجیں', '', 'undo'], ['reject', 'مسترد کریں', 'danger', 'close'],
|
||||||
|
['publish', 'شائع کریں', 'ok', 'upload']] as const).filter(([a]) => may[a]);
|
||||||
|
import Icon from '../../../components/Icon.astro';
|
||||||
---
|
---
|
||||||
<Base title={`مسودہ: ${rev.work_title}`}>
|
<Base title={`مسودہ: ${rev.work_title}`}>
|
||||||
<h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1>
|
<h1>{order && 'ترتیب: '}{tagging && 'ٹیگ: '}{ebook && 'ای بک: '}{rev.work_title}</h1>
|
||||||
@ -62,8 +65,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
|
|||||||
<EbookFields v={ebMeta} />
|
<EbookFields v={ebMeta} />
|
||||||
<textarea name="content" id="eb-content" hidden>{rev.content}</textarea>
|
<textarea name="content" id="eb-content" hidden>{rev.content}</textarea>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<button name="act" value="save">محفوظ کریں</button>
|
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
|
||||||
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : may.save && tagging ? (
|
) : may.save && tagging ? (
|
||||||
@ -92,8 +95,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
|
|||||||
<p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p>
|
<p><button type="button" class="tool" id="tag-add">+ ٹیگ</button></p>
|
||||||
<textarea name="content" id="tag-content" hidden>{rev.content}</textarea>
|
<textarea name="content" id="tag-content" hidden>{rev.content}</textarea>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<button name="act" value="save">محفوظ کریں</button>
|
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
|
||||||
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : may.save && order ? (
|
) : may.save && order ? (
|
||||||
@ -115,8 +118,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
|
|||||||
</ol>
|
</ol>
|
||||||
<textarea name="content" id="order-content" hidden>{rev.content}</textarea>
|
<textarea name="content" id="order-content" hidden>{rev.content}</textarea>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<button name="act" value="save">محفوظ کریں</button>
|
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
|
||||||
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : may.save ? (
|
) : may.save ? (
|
||||||
@ -160,8 +163,8 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
|
|||||||
<textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea>
|
<textarea name="content" id="mod-content" data-urdu rows="22" dir="rtl" spellcheck="false">{rev.content}</textarea>
|
||||||
</label>
|
</label>
|
||||||
<div class="filter-actions">
|
<div class="filter-actions">
|
||||||
<button name="act" value="save">محفوظ کریں</button>
|
<button name="act" value="save" class="btn primary"><Icon name="save" size={18} /> محفوظ کریں</button>
|
||||||
<button name="act" value="submit">{me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
<button name="act" value="submit" class="btn primary"><Icon name={me.role === 'admin' ? 'upload' : 'send'} size={18} /> {me.role === 'admin' ? 'محفوظ کر کے شائع کریں' : me.role === 'mod-l1' ? 'محفوظ کر کے اشاعت کے لیے بھیجیں' : 'محفوظ کر کے جائزے کے لیے بھیجیں'}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>}
|
) : rev.summary && <p class="center">خلاصہ: {rev.summary}</p>}
|
||||||
@ -186,7 +189,7 @@ const acts = ([['approve', 'منظور کریں'], ['return', 'واپس بھیج
|
|||||||
{acts.length > 0 && (
|
{acts.length > 0 && (
|
||||||
<form method="post" class="account-form mod-actions">
|
<form method="post" class="account-form mod-actions">
|
||||||
<label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label>
|
<label>تبصرہ (واپس بھیجنے یا مسترد کرنے کے لیے ضروری)<textarea name="comment" rows="2" data-urdu maxlength="2000"></textarea></label>
|
||||||
<div class="filter-actions">{acts.map(([a, label]) => <button name="act" value={a} class={`act-${a}`}>{label}</button>)}</div>
|
<div class="filter-actions">{acts.map(([a, label, role, icon]) => <button name="act" value={a} class={`btn ${role} act-${a}`}><Icon name={icon} size={18} /> {label}</button>)}</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
22
web/src/pages/writers.astro
Normal file
22
web/src/pages/writers.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
// پسندیدہ لکھاری: the writers (poets and prose writers) the reader saved, apart from their saved texts
|
||||||
|
import Base from '../layouts/Base.astro';
|
||||||
|
import LibraryNav from '../components/LibraryNav.astro';
|
||||||
|
import { asUser, COOKIE } from '../lib/auth';
|
||||||
|
import { ud } from '../lib/urdu';
|
||||||
|
|
||||||
|
const me = Astro.locals.user;
|
||||||
|
if (!me) return Astro.redirect('/signin?next=/writers');
|
||||||
|
const lib = (await asUser(Astro.cookies.get(COOKIE)!.value, '/api/library')).data;
|
||||||
|
const writers = lib.poets.filter((p: any) => p.poet);
|
||||||
|
---
|
||||||
|
<Base title="پسندیدہ لکھاری">
|
||||||
|
<h1>پسندیدہ لکھاری</h1>
|
||||||
|
<LibraryNav current="writers" />
|
||||||
|
{writers.length === 0
|
||||||
|
? <p class="muted center">ابھی کوئی لکھاری محفوظ نہیں۔ کسی شاعر یا ادیب کے صفحے پر "محفوظ کریں" دبائیں۔</p>
|
||||||
|
: <>
|
||||||
|
<p class="muted center">{ud(writers.length)} لکھاری</p>
|
||||||
|
<div class="poets">{writers.map((p: any) => <a class="poet-card" href={p.poet.url} title={p.poet.name}><span class="name">{p.poet.name}</span></a>)}</div>
|
||||||
|
</>}
|
||||||
|
</Base>
|
||||||
@ -4,6 +4,8 @@
|
|||||||
--bg: #fbf6ea; --paper: #fffdf7; --poem: #f6edd6; --inner: #f6efe3; --nested: #eee3d0;
|
--bg: #fbf6ea; --paper: #fffdf7; --poem: #f6edd6; --inner: #f6efe3; --nested: #eee3d0;
|
||||||
--ink: #17130f; --muted: #635543; --lapis: #0d1d30; --gold: #b88828; --gold-light: #dfcd9f;
|
--ink: #17130f; --muted: #635543; --lapis: #0d1d30; --gold: #b88828; --gold-light: #dfcd9f;
|
||||||
--gold-subtle: #f5edd8; --border: #b8a78c; --brand: #bf3f2a;
|
--gold-subtle: #f5edd8; --border: #b8a78c; --brand: #bf3f2a;
|
||||||
|
/* roles: primary actions (filled), confirming (approve, publish), destructive (log out, delete, reject) */
|
||||||
|
--action: #1f4e79; --action-ink: #fff; --ok: #2e7d32; --danger: #b3261e;
|
||||||
--font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --verse-lh: 2.1;
|
--font: 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', serif; --verse-lh: 2.1;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
@ -12,13 +14,29 @@
|
|||||||
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
|
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
|
||||||
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
|
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
|
||||||
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
|
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
|
||||||
|
--action: #7db3e8; --action-ink: #0d1520; --ok: #7cc47f; --danger: #f07a6e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
:root[data-theme='dark'] {
|
:root[data-theme='dark'] {
|
||||||
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
|
--bg: #14120f; --paper: #181510; --poem: #1c1916; --inner: #242019; --nested: #2d2620;
|
||||||
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
|
--ink: #e7ded0; --muted: #9e9185; --lapis: #f0f6fc; --gold: #c9a050; --gold-light: #3a322b;
|
||||||
--gold-subtle: #2a2418; --border: #9b7332; --brand: #d4553a; color-scheme: dark;
|
--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 <html>); 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; }
|
:root.nastaliq { --font: 'Noto Nastaliq Urdu', 'Noto Naskh Arabic', serif; --verse-lh: 2.7; }
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
@ -32,16 +50,15 @@ a:hover { color: var(--brand); }
|
|||||||
.site-header .wrap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 12px; }
|
.site-header .wrap { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding-top: 12px; }
|
||||||
.wordmark { font-family: 'Noto Nastaliq Urdu', serif; font-size: 2rem; line-height: 1.5; color: var(--brand); font-weight: 700; }
|
.wordmark { font-family: 'Noto Nastaliq Urdu', serif; font-size: 2rem; line-height: 1.5; color: var(--brand); font-weight: 700; }
|
||||||
.search-form { flex: 1; min-width: 200px; display: flex; position: relative; }
|
.search-form { flex: 1; min-width: 200px; display: flex; position: relative; }
|
||||||
.search-form input, .toggles button {
|
.search-form input {
|
||||||
font: inherit; border: 1.5px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 10px;
|
font: inherit; border: 1.5px solid var(--border); background: var(--inner); color: var(--ink); border-radius: 10px;
|
||||||
}
|
}
|
||||||
.search-form input { flex: 1; padding: 4px 14px; padding-inline-end: 106px; }
|
.search-form input { flex: 1; padding: 4px 14px; padding-inline-end: 106px; }
|
||||||
.search-form input:focus { outline: none; border-color: var(--gold); }
|
.search-form input:focus { outline: none; border-color: var(--gold); }
|
||||||
.toggles { display: flex; gap: 6px; }
|
.toggles { display: flex; gap: 6px; align-items: center; } /* icon buttons with tooltips */
|
||||||
.toggles button { padding: 2px 12px; cursor: pointer; font-size: .85rem; }
|
.toggles form { display: flex; margin: 0; } /* the log-out button sits in a form: no line box of its own */
|
||||||
/* controls keep Naskh at normal height whatever reading font is chosen */
|
/* controls keep Naskh at normal height whatever reading font is chosen */
|
||||||
.search-form input, .toggles button { font-family: 'Noto Naskh Arabic', serif; line-height: 1.8; }
|
.search-form input { font-family: 'Noto Naskh Arabic', serif; line-height: 1.8; }
|
||||||
.toggles button:hover { border-color: var(--brand); }
|
|
||||||
|
|
||||||
main { padding: 16px 0 48px; }
|
main { padding: 16px 0 48px; }
|
||||||
h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--lapis); }
|
h1, h2 { line-height: 1.6; margin: .2em 0 .4em; font-weight: 700; text-align: center; color: var(--lapis); }
|
||||||
@ -329,8 +346,6 @@ h1 + .muted { text-align: center; margin-top: 0; }
|
|||||||
.mod-edit textarea { font-size: 1.05rem; line-height: 2; min-height: 26em; resize: vertical; }
|
.mod-edit textarea { font-size: 1.05rem; line-height: 2; min-height: 26em; resize: vertical; }
|
||||||
.mod-edit textarea:focus, .mod-edit input:focus { outline: none; border-color: var(--gold); }
|
.mod-edit textarea:focus, .mod-edit input:focus { outline: none; border-color: var(--gold); }
|
||||||
.mod-actions { max-width: 700px; }
|
.mod-actions { max-width: 700px; }
|
||||||
.mod-actions .act-publish { border-color: #3d8b4f; color: #3d8b4f; }
|
|
||||||
.mod-actions .act-reject, .mod-actions .act-return { border-color: var(--brand); color: var(--brand); }
|
|
||||||
.diff { max-width: 900px; margin: 0 auto; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
.diff { max-width: 900px; margin: 0 auto; border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||||
.diff div { padding: 2px 12px; white-space: pre-wrap; line-height: 1.9; }
|
.diff div { padding: 2px 12px; white-space: pre-wrap; line-height: 1.9; }
|
||||||
.diff div span { display: inline-block; width: 1.2em; font-weight: 700; }
|
.diff div span { display: inline-block; width: 1.2em; font-weight: 700; }
|
||||||
@ -457,3 +472,46 @@ h1 + .muted { text-align: center; margin-top: 0; }
|
|||||||
.eb-co .author-find input { width: 100%; }
|
.eb-co .author-find input { width: 100%; }
|
||||||
.eb-people { color: var(--muted); }
|
.eb-people { color: var(--muted); }
|
||||||
.account-form.ebook-form, .mod-edit.account-form { max-width: 560px; }
|
.account-form.ebook-form, .mod-edit.account-form { max-width: 560px; }
|
||||||
|
|
||||||
|
/* buttons by role (owner request): one shape, colour says what it does
|
||||||
|
.btn secondary (gold outline) .btn.primary main action (filled)
|
||||||
|
.btn.ok confirms (approve, publish) .btn.danger destructive (log out, delete, reject)
|
||||||
|
.icon-btn icon only, round, with a tooltip (title) and aria-label */
|
||||||
|
.btn, .mod-edit-btn, .save-like { display: inline-flex; align-items: center; gap: 6px; font: inherit; font-family: 'Noto Naskh Arabic', serif;
|
||||||
|
font-size: .85rem; line-height: 1.8; padding: 2px 14px; border: 1.5px solid var(--gold); border-radius: 999px; background: var(--inner);
|
||||||
|
color: var(--ink); cursor: pointer; text-decoration: none; }
|
||||||
|
.btn:hover, .mod-edit-btn:hover, .save-like:hover { border-color: var(--brand); color: var(--brand); }
|
||||||
|
.btn.primary { background: var(--action); border-color: var(--action); color: var(--action-ink); }
|
||||||
|
.btn.primary:hover { filter: brightness(1.12); color: var(--action-ink); }
|
||||||
|
.btn.ok { border-color: var(--ok); color: var(--ok); }
|
||||||
|
.btn.ok:hover { background: var(--ok); color: var(--bg); }
|
||||||
|
.btn.danger { border-color: var(--danger); color: var(--danger); }
|
||||||
|
.btn.danger:hover { background: var(--danger); color: #fff; }
|
||||||
|
.icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border: 1.5px solid var(--border);
|
||||||
|
border-radius: 50%; background: var(--inner); color: var(--ink); cursor: pointer; text-decoration: none; }
|
||||||
|
.icon-btn:hover, .icon-btn[aria-current='page'] { border-color: var(--brand); color: var(--brand); }
|
||||||
|
.icon-btn.danger { color: var(--danger); border-color: var(--danger); }
|
||||||
|
.icon-btn.danger:hover { background: var(--danger); color: #fff; }
|
||||||
|
.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; }
|
||||||
|
/* display settings in the header: a button and its panel (font, palette swatches, light/dark/automatic) */
|
||||||
|
.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); }
|
||||||
|
.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; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user