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) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -183,10 +183,10 @@ function VerseCard({
|
|||||||
{/* Synonyms */}
|
{/* Synonyms */}
|
||||||
{verse.synonyms.length > 0 && (
|
{verse.synonyms.length > 0 && (
|
||||||
<div>
|
<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
|
Synonyms
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<p className="text-base text-foreground leading-relaxed">
|
||||||
{verse.synonyms.map((s, i) => (
|
{verse.synonyms.map((s, i) => (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
<span className="italic text-foreground">{s.word}</span>—
|
<span className="italic text-foreground">{s.word}</span>—
|
||||||
@@ -200,7 +200,7 @@ function VerseCard({
|
|||||||
|
|
||||||
{/* Translation */}
|
{/* Translation */}
|
||||||
<div>
|
<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
|
Translation
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
@@ -212,10 +212,10 @@ function VerseCard({
|
|||||||
{/* Purport */}
|
{/* Purport */}
|
||||||
{verse.purport.length > 0 && (
|
{verse.purport.length > 0 && (
|
||||||
<div>
|
<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
|
Purport
|
||||||
</h3>
|
</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) => (
|
{verse.purport.map((p, i) => (
|
||||||
<p key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
<p key={i} dangerouslySetInnerHTML={{ __html: p }} />
|
||||||
))}
|
))}
|
||||||
@@ -391,7 +391,7 @@ export function ChapterViewer({
|
|||||||
<>
|
<>
|
||||||
{/* ---- Sidebar: Verse Index ---- */}
|
{/* ---- 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">
|
<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
|
Verses
|
||||||
</h2>
|
</h2>
|
||||||
<nav className="grid grid-cols-3 gap-1">
|
<nav className="grid grid-cols-3 gap-1">
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default async function DsChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function EjChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function EkChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function HkhChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
|||||||
|
|
||||||
{/* Transliteration */}
|
{/* Transliteration */}
|
||||||
<section>
|
<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}
|
{verse.transliteration}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -129,10 +129,10 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
|||||||
{/* Synonyms */}
|
{/* Synonyms */}
|
||||||
{verse.synonyms.length > 0 && (
|
{verse.synonyms.length > 0 && (
|
||||||
<section>
|
<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
|
Synonyms
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm text-foreground/70 leading-relaxed">
|
<div className="text-base text-foreground leading-relaxed">
|
||||||
{verse.synonyms.map((s, i) => (
|
{verse.synonyms.map((s, i) => (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
{i > 0 && "; "}
|
{i > 0 && "; "}
|
||||||
@@ -149,11 +149,11 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
|||||||
|
|
||||||
{/* Translation */}
|
{/* Translation */}
|
||||||
<section>
|
<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
|
Translation
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
className="text-foreground font-medium leading-relaxed"
|
className="text-base text-foreground font-medium leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -161,14 +161,14 @@ function VerseContent({ verse }: { verse: IsoVerse }) {
|
|||||||
{/* Purport */}
|
{/* Purport */}
|
||||||
{verse.purport.length > 0 && (
|
{verse.purport.length > 0 && (
|
||||||
<section>
|
<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
|
Purport
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{verse.purport.map((p, i) => (
|
{verse.purport.map((p, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: p }}
|
dangerouslySetInnerHTML={{ __html: p }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -192,7 +192,7 @@ function IntroContent({ intro }: { intro: IsoIntroSegment }) {
|
|||||||
{intro.paragraphs.map((paragraph, i) => (
|
{intro.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -230,7 +230,7 @@ function IsoSidebar({
|
|||||||
{/* Front Matter */}
|
{/* Front Matter */}
|
||||||
{frontMatterItems.length > 0 && (
|
{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
|
Front Matter
|
||||||
</h2>
|
</h2>
|
||||||
<nav className="flex flex-col gap-0.5 mb-4">
|
<nav className="flex flex-col gap-0.5 mb-4">
|
||||||
@@ -248,7 +248,7 @@ function IsoSidebar({
|
|||||||
{/* Mantras */}
|
{/* Mantras */}
|
||||||
{mantraItems.length > 0 && (
|
{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
|
Mantras
|
||||||
</h2>
|
</h2>
|
||||||
<nav className="flex flex-col gap-0.5 mb-4">
|
<nav className="flex flex-col gap-0.5 mb-4">
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default async function KbChapterPage({
|
|||||||
{chapter.paragraphs.map((paragraph, i) => (
|
{chapter.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function MgChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function NodChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
|||||||
|
|
||||||
{/* Transliteration */}
|
{/* Transliteration */}
|
||||||
<section>
|
<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}
|
{verse.transliteration}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -129,10 +129,10 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
|||||||
{/* Synonyms */}
|
{/* Synonyms */}
|
||||||
{verse.synonyms.length > 0 && (
|
{verse.synonyms.length > 0 && (
|
||||||
<section>
|
<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
|
Synonyms
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm text-foreground/70 leading-relaxed">
|
<div className="text-base text-foreground leading-relaxed">
|
||||||
{verse.synonyms.map((s, i) => (
|
{verse.synonyms.map((s, i) => (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
{i > 0 && "; "}
|
{i > 0 && "; "}
|
||||||
@@ -149,11 +149,11 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
|||||||
|
|
||||||
{/* Translation */}
|
{/* Translation */}
|
||||||
<section>
|
<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
|
Translation
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
className="text-foreground font-medium leading-relaxed"
|
className="text-base text-foreground font-medium leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
dangerouslySetInnerHTML={{ __html: verse.translation }}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
@@ -161,14 +161,14 @@ function VerseContent({ verse }: { verse: NoiVerse }) {
|
|||||||
{/* Purport */}
|
{/* Purport */}
|
||||||
{verse.purport.length > 0 && (
|
{verse.purport.length > 0 && (
|
||||||
<section>
|
<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
|
Purport
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{verse.purport.map((p, i) => (
|
{verse.purport.map((p, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: p }}
|
dangerouslySetInnerHTML={{ __html: p }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -192,7 +192,7 @@ function IntroContent({ intro }: { intro: NoiIntroSegment }) {
|
|||||||
{intro.paragraphs.map((paragraph, i) => (
|
{intro.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -228,7 +228,7 @@ function NoiSidebar({
|
|||||||
{/* Front Matter */}
|
{/* Front Matter */}
|
||||||
{frontMatterItems.length > 0 && (
|
{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
|
Front Matter
|
||||||
</h2>
|
</h2>
|
||||||
<nav className="flex flex-col gap-0.5 mb-4">
|
<nav className="flex flex-col gap-0.5 mb-4">
|
||||||
@@ -246,7 +246,7 @@ function NoiSidebar({
|
|||||||
{/* Texts */}
|
{/* Texts */}
|
||||||
{textItems.length > 0 && (
|
{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
|
Texts
|
||||||
</h2>
|
</h2>
|
||||||
<nav className="flex flex-col gap-0.5 mb-4">
|
<nav className="flex flex-col gap-0.5 mb-4">
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function OchkmChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function OwkChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function PoyChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function PqpaChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function RvChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default async function SsrChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function TlcChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default async function TtpChapterPage({
|
|||||||
{content.paragraphs.map((paragraph, i) => (
|
{content.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default async function EssayPage({
|
|||||||
{essay.paragraphs.map((paragraph, i) => (
|
{essay.paragraphs.map((paragraph, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-foreground leading-relaxed"
|
className="text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{ __html: paragraph }}
|
dangerouslySetInnerHTML={{ __html: paragraph }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+160
-114
@@ -5,126 +5,172 @@
|
|||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
--font-heading: var(--font-sans);
|
--font-heading: var(--font-sans);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
--color-sidebar-primary: var(--sidebar-primary);
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
--color-sidebar: var(--sidebar);
|
--color-sidebar: var(--sidebar);
|
||||||
--color-chart-5: var(--chart-5);
|
--color-chart-5: var(--chart-5);
|
||||||
--color-chart-4: var(--chart-4);
|
--color-chart-4: var(--chart-4);
|
||||||
--color-chart-3: var(--chart-3);
|
--color-chart-3: var(--chart-3);
|
||||||
--color-chart-2: var(--chart-2);
|
--color-chart-2: var(--chart-2);
|
||||||
--color-chart-1: var(--chart-1);
|
--color-chart-1: var(--chart-1);
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
--color-muted: var(--muted);
|
--color-muted: var(--muted);
|
||||||
--color-secondary-foreground: var(--secondary-foreground);
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
--color-secondary: var(--secondary);
|
--color-secondary: var(--secondary);
|
||||||
--color-primary-foreground: var(--primary-foreground);
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
--color-primary: var(--primary);
|
--color-primary: var(--primary);
|
||||||
--color-popover-foreground: var(--popover-foreground);
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--radius-sm: calc(var(--radius) * 0.6);
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
--radius-md: calc(var(--radius) * 0.8);
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) * 1.4);
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
--radius-2xl: calc(var(--radius) * 1.8);
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
--radius-3xl: calc(var(--radius) * 2.2);
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
--radius-4xl: calc(var(--radius) * 2.6);
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Light mode — "Aged Parchment" ── */
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(0.957 0.012 79.8);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.307 0.034 54.8);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(0.974 0.01 81.8);
|
||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.307 0.034 54.8);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(0.974 0.01 81.8);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.307 0.034 54.8);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.652 0.132 81.6);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.994 0.008 91.5);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.899 0.03 80.7);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.307 0.034 54.8);
|
||||||
--muted: oklch(0.97 0 0);
|
--muted: oklch(0.922 0.023 82.1);
|
||||||
--muted-foreground: oklch(0.556 0 0);
|
--muted-foreground: oklch(0.571 0.053 72.7);
|
||||||
--accent: oklch(0.97 0 0);
|
--accent: oklch(0.922 0.023 82.1);
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.307 0.034 54.8);
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.862 0.035 82.9);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.862 0.035 82.9);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.729 0.089 81.2);
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: oklch(0.652 0.132 81.6);
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: oklch(0.571 0.053 72.7);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: oklch(0.729 0.089 81.2);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: oklch(0.44 0.04 60);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--chart-5: oklch(0.307 0.034 54.8);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.974 0.01 81.8);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.307 0.034 54.8);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.652 0.132 81.6);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.994 0.008 91.5);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.922 0.023 82.1);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.307 0.034 54.8);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.862 0.035 82.9);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--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 {
|
.dark {
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.197 0.013 55.5);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.899 0.03 80.7);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.246 0.021 86.3);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.899 0.03 80.7);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.246 0.021 86.3);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.899 0.03 80.7);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.753 0.127 84.9);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.251 0.032 69.4);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.288 0.021 82.6);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.899 0.03 80.7);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.288 0.021 82.6);
|
||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.646 0.047 78.2);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.288 0.021 82.6);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.899 0.03 80.7);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(0.777 0.059 84.4 / 15%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(0.777 0.059 84.4 / 18%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.636 0.08 86.9);
|
||||||
--chart-1: oklch(0.87 0 0);
|
--chart-1: oklch(0.753 0.127 84.9);
|
||||||
--chart-2: oklch(0.556 0 0);
|
--chart-2: oklch(0.646 0.047 78.2);
|
||||||
--chart-3: oklch(0.439 0 0);
|
--chart-3: oklch(0.636 0.08 86.9);
|
||||||
--chart-4: oklch(0.371 0 0);
|
--chart-4: oklch(0.45 0.04 75);
|
||||||
--chart-5: oklch(0.269 0 0);
|
--chart-5: oklch(0.288 0.021 82.6);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.246 0.021 86.3);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.899 0.03 80.7);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.753 0.127 84.9);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.899 0.03 80.7);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.288 0.021 82.6);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.899 0.03 80.7);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(0.777 0.059 84.4 / 15%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.636 0.08 86.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
html {
|
html {
|
||||||
@apply font-sans;
|
@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 filtered = entries.filter((e) => {
|
||||||
const normTerm = normalizeForSearch(e.term);
|
const normTerm = normalizeForSearch(e.term);
|
||||||
const normDef = normalizeForSearch(e.definition);
|
const normDef = normalizeForSearch(e.definition);
|
||||||
return normTerm.includes(normalizedQuery) || normDef.includes(normalizedQuery);
|
return (
|
||||||
|
normTerm.includes(normalizedQuery) ||
|
||||||
|
normDef.includes(normalizedQuery)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
if (filtered.length > 0) {
|
if (filtered.length > 0) {
|
||||||
result[letter] = filtered;
|
result[letter] = filtered;
|
||||||
@@ -210,7 +213,8 @@ export function GlossaryClient({ data }: { data: GlossaryData }) {
|
|||||||
{letter}
|
{letter}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="ml-2 text-sm text-muted-foreground">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -223,7 +227,7 @@ export function GlossaryClient({ data }: { data: GlossaryData }) {
|
|||||||
{highlightMatch(entry.term, query)}
|
{highlightMatch(entry.term, query)}
|
||||||
</dt>
|
</dt>
|
||||||
<dd
|
<dd
|
||||||
className="mt-1 text-sm text-muted-foreground leading-relaxed"
|
className="mt-1 text-base text-foreground leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: query
|
__html: query
|
||||||
? highlightHtml(entry.definition_html, query)
|
? highlightHtml(entry.definition_html, query)
|
||||||
@@ -261,7 +265,9 @@ function highlightMatch(text: string, query: string): React.ReactNode {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{before}
|
{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}
|
{after}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+2
-5
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { PageHeader } from "@/components/page-header";
|
import { PageHeader } from "@/components/page-header";
|
||||||
|
import { Logo } from "@/components/logo";
|
||||||
import { BookOpen, FileText, BookA, PenLine } from "lucide-react";
|
import { BookOpen, FileText, BookA, PenLine } from "lucide-react";
|
||||||
|
|
||||||
const allowRtf = process.env.ALLOW_RTF === "true";
|
const allowRtf = process.env.ALLOW_RTF === "true";
|
||||||
@@ -16,11 +17,7 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<h1 className="text-3xl font-bold text-foreground">Prabhupāda Says</h1>
|
<Logo height={56} className="mb-1" />
|
||||||
<p className="mt-1 text-muted-foreground">
|
|
||||||
Browse the books and teachings of His Divine Grace A.C. Bhaktivedanta
|
|
||||||
Swami Prabhupāda
|
|
||||||
</p>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<main className="max-w-3xl mx-auto px-6 py-8">
|
<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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function PageHeader({
|
export function PageHeader({
|
||||||
@@ -16,7 +16,7 @@ export function PageHeader({
|
|||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-b border-border bg-card px-6",
|
"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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -28,7 +28,7 @@ export function PageHeader({
|
|||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">{children}</div>
|
<div className="flex-1 min-w-0">{children}</div>
|
||||||
<div className="shrink-0 pt-1">
|
<div className="shrink-0 pt-1">
|
||||||
<ThemeToggle />
|
<ReadingSettings />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 * as React from "react";
|
||||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
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">) {
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
return (
|
return (
|
||||||
@@ -9,12 +9,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...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