feat: Add theme switch
This commit is contained in:
38 files changed
+1724
-218
No files matched your search
@@ -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>
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,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" />
|
||||
|
||||
Reference in new issue
Block a user