feat: Add theme switch

This commit is contained in:
deytenit committed 2026-02-15 15:36:31 +03:00
1 parent 891027cab0
commit 2d1bc9d119
38 files changed
+1724 -218

No files matched your search

+33 -30
View File
@@ -1,42 +1,45 @@
import { Container, Header, Link, Separator, Article } from '@ermnvldmr/ui';
import { Header, Link, Separator, Article , VStack } from '@ermnvldmr/ui';
import React from 'react';
import { sortedArticles } from '../../../content/registry';
import { IndexLayout } from '../../components/layouts/IndexLayout/IndexLayout';
import { createPage } from '../../lib/core/createPage';
import { SITE_TITLE } from '../../lib/shared/constants';
createPage(
function ArticlesList(): React.JSX.Element {
return (
<Container as="main" className="mx-auto max-w-5xl px-6 py-20">
<Header className="mb-12 text-center" level={1}>
Articles
</Header>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{sortedArticles.map((article, index) => (
<React.Fragment key={article.slug}>
{index > 0 && index % 2 === 0 && (
<div className="col-span-1 py-8 md:col-span-2">
<Separator className="w-full" type="double" />
</div>
)}
<Link className="group block h-full no-underline!" href={`/articles/${article.slug}`}>
<Article
additionalText={article.tags?.join(' • ')}
headline={article.title}
subHeadline={article.createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
>
{article.description}
</Article>
</Link>
</React.Fragment>
))}
</div>
</Container>
<IndexLayout>
<VStack className="w-full" gap={8}>
<Header level={1}>
Articles
</Header>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
{sortedArticles.map((article, index) => (
<React.Fragment key={article.slug}>
{index > 0 && index % 2 === 0 && (
<div className="col-span-1 py-8 md:col-span-2">
<Separator className="w-full" type="double" />
</div>
)}
<Link className="group block h-full no-underline!" href={`/articles/${article.slug}`}>
<Article
additionalText={article.tags?.join(' • ')}
headline={article.title}
subHeadline={article.createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
>
{article.description}
</Article>
</Link>
</React.Fragment>
))}
</div>
</VStack>
</IndexLayout>
);
},
{
+4 -3
View File
@@ -1,13 +1,14 @@
import { Container, Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
import { Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
import React from 'react';
import { IndexLayout } from '../components/layouts/IndexLayout/IndexLayout';
import { createPage } from '../lib/core/createPage';
import { SITE_DESCRIPTION, SITE_TITLE } from '../lib/shared/constants';
createPage(
function HomePage(): React.JSX.Element {
return (
<Container as="main" className="flex h-full w-full justify-center p-4">
<IndexLayout>
<VStack gap={4}>
<Header level={2}>Welcome to my page!</Header>
<Paragraph>
@@ -27,7 +28,7 @@ createPage(
</Paragraph>
<Paragraph italic>Thank you for visiting!</Paragraph>
</VStack>
</Container>
</IndexLayout>
);
},
{ title: SITE_TITLE, description: SITE_DESCRIPTION }
@@ -2,6 +2,8 @@ import { cn } from '@ermnvldmr/stl';
import { VStack, HStack, Markdown, Container, Time, Header, Paragraph, Text } from '@ermnvldmr/ui';
import React from 'react';
import { IndexLayout } from '../IndexLayout/IndexLayout';
/**
* Props for the ArticleLayout component.
*/
@@ -65,40 +67,41 @@ export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
);
return (
<Container as="article" className={cn('mx-auto max-w-3xl px-6 py-12 md:py-20', className)}>
<VStack as="header" className="pb-12" gap={4}>
<HStack align="center" className="flex-wrap" gap={4}>
{displayDate}
{tags && tags.length > 0 && (
<HStack className="flex-wrap" gap={2}>
{tags.map((tag) => (
<Text
key={tag}
className="before:text-muted-foreground/50 before:mr-0.5 before:content-['#']"
color="muted"
size="s"
>
{tag}
</Text>
))}
<IndexLayout>
<Container as="article" className={cn('min-w-[720px]', className)}>
<VStack className="w-full" gap={8}>
<VStack as="header" gap={4}>
<HStack align="center" gap={2} wrap="wrap">
{displayDate}
{tags && tags.length > 0 && (
<HStack gap={1} wrap="wrap">
{tags.map((tag) => (
<Text
key={tag}
className="before:content-['#']"
color="muted"
size="s"
>
{tag}
</Text>
))}
</HStack>
)}
</HStack>
)}
</HStack>
<Header className="leading-tight" level={1}>
{title}
</Header>
{description && (
<Paragraph italic className="leading-relaxed" color="muted" size="l">
{description}
</Paragraph>
)}
</VStack>
<Container as="section" className="article-content">
<Markdown>{children}</Markdown>
<Header level={1}>
{title}
</Header>
{description && (
<Paragraph italic color="muted" size="l">
{description}
</Paragraph>
)}
</VStack>
<Container as="section" className='w-full'>
<Markdown>{children}</Markdown>
</Container>
</VStack>
</Container>
</Container>
</IndexLayout>
);
};
@@ -0,0 +1,53 @@
import { Container, HStack, Link, ThemeSwitch, useTheme, VStack, Text, Separator } from '@ermnvldmr/ui';
import React from 'react';
/**
* Props for the IndexLayout component.
*/
export interface IndexLayoutProps {
/** Main content slot */
children: React.ReactNode;
}
/**
* The root layout for the www service.
* Handles global theme state and provides common structure.
*
* @param props - The component props.
* @param props.children - Main content slot.
* @returns A React element representing the index layout.
* @example
* ```tsx
* <IndexLayout>Content</IndexLayout>
* ```
*/
export function IndexLayout({ children }: IndexLayoutProps): React.JSX.Element {
// Initialize theme side-effects
useTheme();
const currentYear = new Date().getFullYear();
return (
<VStack align="center" className="min-h-screen bg-[var(--rb-background)] transition-colors duration-200" justify="between">
<VStack align="stretch" as="main" className="min-h-[105vh] py-8 mx-auto" justify="start">
{children}
</VStack>
<VStack as="footer" className="w-full" gap={0}>
<Separator thinned={false} />
<Container className="w-full" padding={12}>
<HStack align="center" className="w-full" justify="between">
<VStack align="start" gap={1}>
<Text color="muted" size="s">
© {currentYear} Vladimir Eremin
</Text>
<Link href="https://creativecommons.org/licenses/by/4.0/" size="s">
CC-BY 4.0
</Link>
</VStack>
<ThemeSwitch />
</HStack>
</Container>
</VStack>
</VStack>
);
}
+1 -21
View File
@@ -1,27 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<script>
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const isDark = getThemePreference() === 'dark';
document.documentElement.classList[isDark ? 'add' : 'remove']('dark');
if (typeof localStorage !== 'undefined') {
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class'],
});
}
</script>
<script>(function(){try{var k='ermnvldmr/ui/lib/theme';var p=JSON.parse(localStorage.getItem(k)||'"system"');var d=p==='dark'||(p==='system'&&window.matchMedia('(prefers-color-scheme:dark)').matches);document.documentElement.classList.toggle('dark',d)}catch(e){}})();</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />