feat: add accessibility features to UI
This commit is contained in:
@@ -79,7 +79,7 @@ export default async function BbdChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -183,10 +183,10 @@ function VerseCard({
|
||||
{/* Synonyms */}
|
||||
{verse.synonyms.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
Synonyms
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
<p className="text-base text-foreground leading-relaxed">
|
||||
{verse.synonyms.map((s, i) => (
|
||||
<span key={i}>
|
||||
<span className="italic text-foreground">{s.word}</span>—
|
||||
@@ -200,7 +200,7 @@ function VerseCard({
|
||||
|
||||
{/* Translation */}
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
Translation
|
||||
</h3>
|
||||
<p
|
||||
@@ -212,10 +212,10 @@ function VerseCard({
|
||||
{/* Purport */}
|
||||
{verse.purport.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-2">
|
||||
Purport
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
|
||||
<div className="text-base text-foreground leading-relaxed space-y-3">
|
||||
{verse.purport.map((p, i) => (
|
||||
<p key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
||||
))}
|
||||
@@ -391,7 +391,7 @@ export function ChapterViewer({
|
||||
<>
|
||||
{/* ---- Sidebar: Verse Index ---- */}
|
||||
<aside className="hidden lg:block w-64 shrink-0 sticky top-[89px] self-start max-h-[calc(100vh-89px)] overflow-y-auto py-8 pr-4">
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Verses
|
||||
</h2>
|
||||
<nav className="grid grid-cols-3 gap-1">
|
||||
|
||||
@@ -85,7 +85,7 @@ export default async function DsChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function EjChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function EkChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function HkhChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -121,7 +121,7 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
|
||||
{/* Transliteration */}
|
||||
<section>
|
||||
<div className="text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
|
||||
<div className="text-base text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
|
||||
{verse.transliteration}
|
||||
</div>
|
||||
</section>
|
||||
@@ -129,10 +129,10 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
{/* Synonyms */}
|
||||
{verse.synonyms.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Synonyms
|
||||
</h2>
|
||||
<div className="text-sm text-foreground/70 leading-relaxed">
|
||||
<div className="text-base text-foreground leading-relaxed">
|
||||
{verse.synonyms.map((s, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && "; "}
|
||||
@@ -149,11 +149,11 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
|
||||
{/* Translation */}
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Translation
|
||||
</h2>
|
||||
<div
|
||||
className="text-foreground font-medium leading-relaxed"
|
||||
className="text-base text-foreground font-medium leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
||||
/>
|
||||
</section>
|
||||
@@ -161,14 +161,14 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
||||
{/* Purport */}
|
||||
{verse.purport.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Purport
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{verse.purport.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: p }}
|
||||
/>
|
||||
))}
|
||||
@@ -192,7 +192,7 @@ function IntroContent({ intro }: { intro: IsoIntroSegment }) {
|
||||
{intro.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
@@ -230,7 +230,7 @@ function IsoSidebar({
|
||||
{/* Front Matter */}
|
||||
{frontMatterItems.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Front Matter
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-0.5 mb-4">
|
||||
@@ -248,7 +248,7 @@ function IsoSidebar({
|
||||
{/* Mantras */}
|
||||
{mantraItems.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Mantras
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-0.5 mb-4">
|
||||
|
||||
@@ -76,7 +76,7 @@ export default async function KbChapterPage({
|
||||
{chapter.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function MgChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function NodChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -121,7 +121,7 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
|
||||
{/* Transliteration */}
|
||||
<section>
|
||||
<div className="text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
|
||||
<div className="text-base text-foreground/80 italic whitespace-pre-line leading-relaxed text-center">
|
||||
{verse.transliteration}
|
||||
</div>
|
||||
</section>
|
||||
@@ -129,10 +129,10 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
{/* Synonyms */}
|
||||
{verse.synonyms.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Synonyms
|
||||
</h2>
|
||||
<div className="text-sm text-foreground/70 leading-relaxed">
|
||||
<div className="text-base text-foreground leading-relaxed">
|
||||
{verse.synonyms.map((s, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && "; "}
|
||||
@@ -149,11 +149,11 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
|
||||
{/* Translation */}
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Translation
|
||||
</h2>
|
||||
<div
|
||||
className="text-foreground font-medium leading-relaxed"
|
||||
className="text-base text-foreground font-medium leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
||||
/>
|
||||
</section>
|
||||
@@ -161,14 +161,14 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
||||
{/* Purport */}
|
||||
{verse.purport.length > 0 && (
|
||||
<section>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3">
|
||||
Purport
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{verse.purport.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: p }}
|
||||
/>
|
||||
))}
|
||||
@@ -192,7 +192,7 @@ function IntroContent({ intro }: { intro: NoiIntroSegment }) {
|
||||
{intro.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
@@ -228,7 +228,7 @@ function NoiSidebar({
|
||||
{/* Front Matter */}
|
||||
{frontMatterItems.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Front Matter
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-0.5 mb-4">
|
||||
@@ -246,7 +246,7 @@ function NoiSidebar({
|
||||
{/* Texts */}
|
||||
{textItems.length > 0 && (
|
||||
<>
|
||||
<h2 className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wider text-muted-foreground mb-3 px-2">
|
||||
Texts
|
||||
</h2>
|
||||
<nav className="flex flex-col gap-0.5 mb-4">
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function OchkmChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function OwkChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function PoyChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function PqpaChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function RvChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default async function SsrChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function TlcChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function TtpChapterPage({
|
||||
{content.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -142,7 +142,7 @@ export default async function EssayPage({
|
||||
{essay.paragraphs.map((paragraph, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-foreground leading-relaxed"
|
||||
className="text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||
/>
|
||||
))}
|
||||
|
||||
+106
-60
@@ -48,73 +48,80 @@
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
/* ── Light mode — "Aged Parchment" ── */
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--background: oklch(0.957 0.012 79.8);
|
||||
--foreground: oklch(0.307 0.034 54.8);
|
||||
--card: oklch(0.974 0.01 81.8);
|
||||
--card-foreground: oklch(0.307 0.034 54.8);
|
||||
--popover: oklch(0.974 0.01 81.8);
|
||||
--popover-foreground: oklch(0.307 0.034 54.8);
|
||||
--primary: oklch(0.652 0.132 81.6);
|
||||
--primary-foreground: oklch(0.994 0.008 91.5);
|
||||
--secondary: oklch(0.899 0.03 80.7);
|
||||
--secondary-foreground: oklch(0.307 0.034 54.8);
|
||||
--muted: oklch(0.922 0.023 82.1);
|
||||
--muted-foreground: oklch(0.571 0.053 72.7);
|
||||
--accent: oklch(0.922 0.023 82.1);
|
||||
--accent-foreground: oklch(0.307 0.034 54.8);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--border: oklch(0.862 0.035 82.9);
|
||||
--input: oklch(0.862 0.035 82.9);
|
||||
--ring: oklch(0.729 0.089 81.2);
|
||||
--chart-1: oklch(0.652 0.132 81.6);
|
||||
--chart-2: oklch(0.571 0.053 72.7);
|
||||
--chart-3: oklch(0.729 0.089 81.2);
|
||||
--chart-4: oklch(0.44 0.04 60);
|
||||
--chart-5: oklch(0.307 0.034 54.8);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--sidebar: oklch(0.974 0.01 81.8);
|
||||
--sidebar-foreground: oklch(0.307 0.034 54.8);
|
||||
--sidebar-primary: oklch(0.652 0.132 81.6);
|
||||
--sidebar-primary-foreground: oklch(0.994 0.008 91.5);
|
||||
--sidebar-accent: oklch(0.922 0.023 82.1);
|
||||
--sidebar-accent-foreground: oklch(0.307 0.034 54.8);
|
||||
--sidebar-border: oklch(0.862 0.035 82.9);
|
||||
--sidebar-ring: oklch(0.729 0.089 81.2);
|
||||
|
||||
/* Reading settings defaults */
|
||||
--reading-font-size: 1rem;
|
||||
--reading-font-family: inherit;
|
||||
--reading-text-align: start;
|
||||
}
|
||||
|
||||
/* ── Dark mode — "Ancient Manuscript" ── */
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--background: oklch(0.197 0.013 55.5);
|
||||
--foreground: oklch(0.899 0.03 80.7);
|
||||
--card: oklch(0.246 0.021 86.3);
|
||||
--card-foreground: oklch(0.899 0.03 80.7);
|
||||
--popover: oklch(0.246 0.021 86.3);
|
||||
--popover-foreground: oklch(0.899 0.03 80.7);
|
||||
--primary: oklch(0.753 0.127 84.9);
|
||||
--primary-foreground: oklch(0.251 0.032 69.4);
|
||||
--secondary: oklch(0.288 0.021 82.6);
|
||||
--secondary-foreground: oklch(0.899 0.03 80.7);
|
||||
--muted: oklch(0.288 0.021 82.6);
|
||||
--muted-foreground: oklch(0.646 0.047 78.2);
|
||||
--accent: oklch(0.288 0.021 82.6);
|
||||
--accent-foreground: oklch(0.899 0.03 80.7);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--border: oklch(0.777 0.059 84.4 / 15%);
|
||||
--input: oklch(0.777 0.059 84.4 / 18%);
|
||||
--ring: oklch(0.636 0.08 86.9);
|
||||
--chart-1: oklch(0.753 0.127 84.9);
|
||||
--chart-2: oklch(0.646 0.047 78.2);
|
||||
--chart-3: oklch(0.636 0.08 86.9);
|
||||
--chart-4: oklch(0.45 0.04 75);
|
||||
--chart-5: oklch(0.288 0.021 82.6);
|
||||
--sidebar: oklch(0.246 0.021 86.3);
|
||||
--sidebar-foreground: oklch(0.899 0.03 80.7);
|
||||
--sidebar-primary: oklch(0.753 0.127 84.9);
|
||||
--sidebar-primary-foreground: oklch(0.899 0.03 80.7);
|
||||
--sidebar-accent: oklch(0.288 0.021 82.6);
|
||||
--sidebar-accent-foreground: oklch(0.899 0.03 80.7);
|
||||
--sidebar-border: oklch(0.777 0.059 84.4 / 15%);
|
||||
--sidebar-ring: oklch(0.636 0.08 86.9);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -128,3 +135,42 @@
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reading settings — classes applied to <html> ── */
|
||||
|
||||
/* Serif font — only main content + sidebar */
|
||||
.reading-serif main,
|
||||
.reading-serif aside {
|
||||
font-family: Georgia, "Times New Roman", "Noto Serif", serif;
|
||||
}
|
||||
|
||||
/* Font size scaling — zoom on main + aside so rem-based Tailwind classes scale */
|
||||
.reading-fs-xs main,
|
||||
.reading-fs-xs aside {
|
||||
zoom: 0.8;
|
||||
}
|
||||
.reading-fs-sm main,
|
||||
.reading-fs-sm aside {
|
||||
zoom: 0.9;
|
||||
}
|
||||
.reading-fs-base main,
|
||||
.reading-fs-base aside {
|
||||
zoom: 1;
|
||||
}
|
||||
.reading-fs-lg main,
|
||||
.reading-fs-lg aside {
|
||||
zoom: 1.12;
|
||||
}
|
||||
.reading-fs-xl main,
|
||||
.reading-fs-xl aside {
|
||||
zoom: 1.25;
|
||||
}
|
||||
.reading-fs-2xl main,
|
||||
.reading-fs-2xl aside {
|
||||
zoom: 1.4;
|
||||
}
|
||||
|
||||
/* Justified text — only main content */
|
||||
.reading-justified main {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,10 @@ export function GlossaryClient({ data }: { data: GlossaryData }) {
|
||||
const filtered = entries.filter((e) => {
|
||||
const normTerm = normalizeForSearch(e.term);
|
||||
const normDef = normalizeForSearch(e.definition);
|
||||
return normTerm.includes(normalizedQuery) || normDef.includes(normalizedQuery);
|
||||
return (
|
||||
normTerm.includes(normalizedQuery) ||
|
||||
normDef.includes(normalizedQuery)
|
||||
);
|
||||
});
|
||||
if (filtered.length > 0) {
|
||||
result[letter] = filtered;
|
||||
@@ -210,7 +213,8 @@ export function GlossaryClient({ data }: { data: GlossaryData }) {
|
||||
{letter}
|
||||
</Badge>
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
{filteredGroups[letter].length} entr{filteredGroups[letter].length === 1 ? "y" : "ies"}
|
||||
{filteredGroups[letter].length} entr
|
||||
{filteredGroups[letter].length === 1 ? "y" : "ies"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -223,7 +227,7 @@ export function GlossaryClient({ data }: { data: GlossaryData }) {
|
||||
{highlightMatch(entry.term, query)}
|
||||
</dt>
|
||||
<dd
|
||||
className="mt-1 text-sm text-muted-foreground leading-relaxed"
|
||||
className="mt-1 text-base text-foreground leading-relaxed"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: query
|
||||
? highlightHtml(entry.definition_html, query)
|
||||
@@ -261,7 +265,9 @@ function highlightMatch(text: string, query: string): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{before}
|
||||
<mark className="bg-primary/20 text-foreground rounded-sm px-0.5">{match}</mark>
|
||||
<mark className="bg-primary/20 text-foreground rounded-sm px-0.5">
|
||||
{match}
|
||||
</mark>
|
||||
{after}
|
||||
</>
|
||||
);
|
||||
|
||||
+2
-5
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { PageHeader } from "@/components/page-header";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { BookOpen, FileText, BookA, PenLine } from "lucide-react";
|
||||
|
||||
const allowRtf = process.env.ALLOW_RTF === "true";
|
||||
@@ -16,11 +17,7 @@ export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<PageHeader>
|
||||
<h1 className="text-3xl font-bold text-foreground">Prabhupāda Says</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Browse the books and teachings of His Divine Grace A.C. Bhaktivedanta
|
||||
Swami Prabhupāda
|
||||
</p>
|
||||
<Logo height={56} className="mb-1" />
|
||||
</PageHeader>
|
||||
|
||||
<main className="max-w-3xl mx-auto px-6 py-8">
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
const emptySubscribe = () => () => {};
|
||||
|
||||
function useMounted() {
|
||||
return useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
export function Logo({
|
||||
className,
|
||||
height = 48,
|
||||
}: {
|
||||
className?: string;
|
||||
height?: number;
|
||||
}) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const mounted = useMounted();
|
||||
|
||||
// Aspect ratio from the original images: 480x200 = 2.4:1
|
||||
const width = Math.round(height * 2.4);
|
||||
|
||||
// Before mount, show the dark logo (works on both themes as a fallback)
|
||||
const isDark = mounted ? resolvedTheme === "dark" : false;
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={isDark ? "/logo_light.png" : "/logo_dark.png"}
|
||||
alt="Prabhupāda Says"
|
||||
width={width}
|
||||
height={height}
|
||||
className={className}
|
||||
priority
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { ReadingSettings } from "@/components/reading-settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function PageHeader({
|
||||
@@ -16,7 +16,7 @@ export function PageHeader({
|
||||
<header
|
||||
className={cn(
|
||||
"border-b border-border bg-card px-6",
|
||||
sticky ? "py-6 sticky top-0 z-20" : "py-8",
|
||||
sticky ? "py-4 sticky top-0 z-20" : "py-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
@@ -28,7 +28,7 @@ export function PageHeader({
|
||||
>
|
||||
<div className="flex-1 min-w-0">{children}</div>
|
||||
<div className="shrink-0 pt-1">
|
||||
<ThemeToggle />
|
||||
<ReadingSettings />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Settings, Sun, Moon, Monitor } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
const emptySubscribe = () => () => {};
|
||||
|
||||
function useMounted() {
|
||||
return useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
type ReadingPrefs = {
|
||||
serifFont: boolean;
|
||||
fontSize: string;
|
||||
justified: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_PREFS: ReadingPrefs = {
|
||||
serifFont: false,
|
||||
fontSize: "base",
|
||||
justified: false,
|
||||
};
|
||||
|
||||
const FONT_SIZES = [
|
||||
{ value: "xs", label: "XS" },
|
||||
{ value: "sm", label: "S" },
|
||||
{ value: "base", label: "M" },
|
||||
{ value: "lg", label: "L" },
|
||||
{ value: "xl", label: "XL" },
|
||||
{ value: "2xl", label: "XXL" },
|
||||
] as const;
|
||||
|
||||
const STORAGE_KEY = "spsays_reading_prefs";
|
||||
|
||||
function loadPrefs(): ReadingPrefs {
|
||||
if (typeof window === "undefined") return DEFAULT_PREFS;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return DEFAULT_PREFS;
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
serifFont:
|
||||
typeof parsed.serifFont === "boolean"
|
||||
? parsed.serifFont
|
||||
: DEFAULT_PREFS.serifFont,
|
||||
fontSize: FONT_SIZES.some((f) => f.value === parsed.fontSize)
|
||||
? parsed.fontSize
|
||||
: DEFAULT_PREFS.fontSize,
|
||||
justified:
|
||||
typeof parsed.justified === "boolean"
|
||||
? parsed.justified
|
||||
: DEFAULT_PREFS.justified,
|
||||
};
|
||||
} catch {
|
||||
return DEFAULT_PREFS;
|
||||
}
|
||||
}
|
||||
|
||||
function savePrefs(prefs: ReadingPrefs) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
||||
} catch {
|
||||
// localStorage might be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function applyClasses(prefs: ReadingPrefs) {
|
||||
const root = document.documentElement;
|
||||
|
||||
// Serif
|
||||
root.classList.toggle("reading-serif", prefs.serifFont);
|
||||
|
||||
// Font size — remove all, add current
|
||||
for (const fs of FONT_SIZES) {
|
||||
root.classList.remove(`reading-fs-${fs.value}`);
|
||||
}
|
||||
root.classList.add(`reading-fs-${prefs.fontSize}`);
|
||||
|
||||
// Justified
|
||||
root.classList.toggle("reading-justified", prefs.justified);
|
||||
}
|
||||
|
||||
function Toggle({
|
||||
checked,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
className="flex items-center justify-between w-full py-2 cursor-pointer"
|
||||
>
|
||||
<span className="text-sm text-foreground">{label}</span>
|
||||
<div
|
||||
className={cn(
|
||||
"w-10 h-5 rounded-full transition-colors relative shrink-0 ml-3",
|
||||
checked ? "bg-primary" : "bg-muted-foreground/30",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform shadow-sm",
|
||||
checked ? "translate-x-5" : "translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReadingSettings() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [prefs, setPrefs] = useState<ReadingPrefs>(() => loadPrefs());
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const { theme, setTheme } = useTheme();
|
||||
const mounted = useMounted();
|
||||
|
||||
// Apply classes on mount (prefs already loaded via lazy initializer)
|
||||
useEffect(() => {
|
||||
applyClasses(prefs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Click outside to close
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (
|
||||
panelRef.current &&
|
||||
!panelRef.current.contains(e.target as Node) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const updatePrefs = useCallback((update: Partial<ReadingPrefs>) => {
|
||||
setPrefs((prev) => {
|
||||
const next = { ...prev, ...update };
|
||||
savePrefs(next);
|
||||
applyClasses(next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const fontSizeIndex = FONT_SIZES.findIndex((f) => f.value === prefs.fontSize);
|
||||
const currentFontSizeLabel = FONT_SIZES[fontSizeIndex]?.label ?? "M";
|
||||
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Reading settings">
|
||||
<Settings className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Button
|
||||
ref={buttonRef as React.Ref<HTMLButtonElement>}
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="Reading settings"
|
||||
aria-expanded={open}
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
</Button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className="absolute right-0 top-full mt-2 w-72 rounded-lg border border-border bg-card shadow-lg z-50 p-4"
|
||||
>
|
||||
{/* Theme selector */}
|
||||
<div className="mb-4">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Theme
|
||||
</span>
|
||||
<div className="flex gap-1 mt-2">
|
||||
{(
|
||||
[
|
||||
{ value: "light", icon: Sun, label: "Light" },
|
||||
{ value: "dark", icon: Moon, label: "Dark" },
|
||||
{ value: "system", icon: Monitor, label: "System" },
|
||||
] as const
|
||||
).map(({ value, icon: Icon, label }) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setTheme(value)}
|
||||
className={cn(
|
||||
"flex-1 flex items-center justify-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer",
|
||||
theme === value
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-3 space-y-1">
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Reading
|
||||
</span>
|
||||
|
||||
{/* Serif font toggle */}
|
||||
<Toggle
|
||||
checked={prefs.serifFont}
|
||||
onChange={(v) => updatePrefs({ serifFont: v })}
|
||||
label="Serif Font"
|
||||
/>
|
||||
|
||||
{/* Justify text toggle */}
|
||||
<Toggle
|
||||
checked={prefs.justified}
|
||||
onChange={(v) => updatePrefs({ justified: v })}
|
||||
label="Justify Text"
|
||||
/>
|
||||
|
||||
{/* Font size slider */}
|
||||
<div className="pt-1 pb-1">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-foreground">Font Size</span>
|
||||
<span className="text-xs font-medium text-muted-foreground bg-muted px-2 py-0.5 rounded">
|
||||
{currentFontSizeLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-muted-foreground">A</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={FONT_SIZES.length - 1}
|
||||
step={1}
|
||||
value={fontSizeIndex}
|
||||
onChange={(e) => {
|
||||
const idx = parseInt(e.target.value, 10);
|
||||
updatePrefs({ fontSize: FONT_SIZES[idx].value });
|
||||
}}
|
||||
className="flex-1 h-1.5 accent-primary cursor-pointer"
|
||||
/>
|
||||
<span className="text-base text-muted-foreground font-medium">
|
||||
A
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
const emptySubscribe = () => () => {};
|
||||
|
||||
function useMounted() {
|
||||
return useSyncExternalStore(
|
||||
emptySubscribe,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const mounted = useMounted();
|
||||
|
||||
// Avoid hydration mismatch — on the server and during hydration, render a placeholder
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Toggle theme">
|
||||
<Sun className="size-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const isDark = resolvedTheme === "dark";
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isDark ? <Sun className="size-4" /> : <Moon className="size-4" />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{isDark ? "Light mode" : "Dark mode"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
import * as React from "react";
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
@@ -9,12 +9,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className
|
||||
"h-10 w-full min-w-0 rounded-lg border border-input bg-card px-3 py-2 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-base file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:bg-card dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Reference in New Issue
Block a user