feat: add accessibility features to UI

This commit is contained in:
gosangam
2026-04-18 12:25:36 +05:30
parent 5235f479b3
commit c865b59088
30 changed files with 557 additions and 233 deletions
+1 -1
View File
@@ -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 }}
/>
))}
+6 -6
View File
@@ -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">
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+10 -10
View File
@@ -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">
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+10 -10
View File
@@ -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">
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+1 -1
View File
@@ -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 }}
/>
))}
+160 -114
View File
@@ -5,126 +5,172 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--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);
--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);
--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);
--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.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.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);
--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);
--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(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 {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@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;
}
+10 -4
View File
@@ -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
View File
@@ -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">
+43
View File
@@ -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
/>
);
}
+3 -3
View File
@@ -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>
+289
View File
@@ -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>
);
}
-57
View File
@@ -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>
);
}
+7 -7
View File
@@ -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