feat: Refactor layouts in www

This commit is contained in:
deytenit committed 2026-03-07 20:59:03 +03:00
1 parent 6aa3a03319
commit f3a3a52aa8
66 files changed
+1805 -352

No files matched your search

+15
View File
@@ -1,4 +1,5 @@
import { defineServiceConfig, discoverEntries } from '@ermnvldmr/rsbuild-config/dev';
import { THEME_INIT_SCRIPT, THEME_INIT_STYLES } from '@ermnvldmr/ui/dev';
import { pluginMdx } from '@rsbuild/plugin-mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
@@ -14,4 +15,18 @@ export default defineServiceConfig({
source: {
entry: discoverEntries(import.meta.dirname, './src/app/**/*.tsx'),
},
html: {
tags: [
{
tag: 'script',
children: THEME_INIT_SCRIPT,
head: true,
},
{
tag: 'style',
children: THEME_INIT_STYLES,
head: true,
},
],
},
});
@@ -2,7 +2,7 @@ import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/layouts/ArticleLayout/ArticleLayout';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['matrix-server-deployment-guide'];
@@ -2,7 +2,7 @@ import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
import React from 'react';
import { articles } from '../../../../content/registry';
import { ArticleLayout } from '../../../components/layouts/ArticleLayout/ArticleLayout';
import { ArticleLayout } from '../../../components/Layout/ArticleLayout';
import { createPage } from '../../../lib/core/createPage';
const article = articles['test-article'];
+19 -5
View File
@@ -1,17 +1,31 @@
import { Header, Separator, Article, VStack } from '@ermnvldmr/ui';
import { Separator, Article, VStack, Breadcrumbs } from '@ermnvldmr/ui';
import React from 'react';
import { sortedArticles } from '../../../content/registry';
import { IndexLayout } from '../../components/layouts/IndexLayout/IndexLayout';
import { DefaultLayout } from '../../components/Layout/DefaultLayout';
import { createPage } from '../../lib/core/createPage';
import { SITE_TITLE } from '../../lib/shared/constants';
createPage(
function ArticlesList(): React.JSX.Element {
const breadcrumbs = (
<Breadcrumbs>
<Breadcrumbs.Item>
<a href="/">Home</a>
</Breadcrumbs.Item>
<Breadcrumbs.Separator />
<Breadcrumbs.Item>Articles</Breadcrumbs.Item>
</Breadcrumbs>
);
return (
<IndexLayout>
<DefaultLayout
breadcrumbs={breadcrumbs}
description="Browse all articles and projects."
paddingY="medium"
title="Articles"
>
<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}>
@@ -37,7 +51,7 @@ createPage(
))}
</div>
</VStack>
</IndexLayout>
</DefaultLayout>
);
},
{
+4 -4
View File
@@ -1,14 +1,14 @@
import { Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
import { Paragraph, Link, VStack, Header } from '@ermnvldmr/ui';
import React from 'react';
import { IndexLayout } from '../components/layouts/IndexLayout/IndexLayout';
import { DefaultLayout } from '../components/Layout/DefaultLayout';
import { createPage } from '../lib/core/createPage';
import { SITE_DESCRIPTION, SITE_TITLE } from '../lib/shared/constants';
createPage(
function HomePage(): React.JSX.Element {
return (
<IndexLayout>
<DefaultLayout centerVertically headerAddonLeft={<Header level={3}>{SITE_TITLE}</Header>}>
<VStack gap={4}>
<Header level={2}>Welcome to my page!</Header>
<Paragraph>
@@ -28,7 +28,7 @@ createPage(
</Paragraph>
<Paragraph italic>Thank you for visiting!</Paragraph>
</VStack>
</IndexLayout>
</DefaultLayout>
);
},
{ title: SITE_TITLE, description: SITE_DESCRIPTION }
@@ -0,0 +1,42 @@
import { Container, HStack, Link, ThemeSwitch, VStack, Text } from '@ermnvldmr/ui';
import React from 'react';
import { RainbowSeparator } from '../RainbowSeparator';
/**
/**
* The standard footer for the www service.
* Displays copyright information, license, and theme switcher.
*
* @returns A Footer component.
* @example
* ```tsx
* <Footer />
* ```
*/
export const Footer = () => {
const currentYear = new Date().getFullYear();
return (
<VStack as="footer" className="w-full" gap={0}>
<RainbowSeparator />
<Container bg="transparent" className="w-full max-sm:p-8" padding={12}>
<HStack
align="center"
className="w-full max-sm:flex-col max-sm:items-start max-sm:gap-8"
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>
);
};
@@ -0,0 +1 @@
export * from './Footer';
@@ -18,7 +18,7 @@ import type { Meta, StoryObj } from '@storybook/react';
* Metadata for the ArticleLayout component stories.
*/
const meta: Meta<typeof ArticleLayout> = {
title: 'WWW/Layouts/ArticleLayout',
title: 'WWW/Layout/ArticleLayout',
component: ArticleLayout,
tags: ['autodocs'],
args: {
@@ -0,0 +1,101 @@
import { Markdown, Time, Text, PageColumns, PageColumn, Breadcrumbs, HStack } from '@ermnvldmr/ui';
import React from 'react';
import { DefaultLayout } from '../DefaultLayout/DefaultLayout';
/**
* Props for the ArticleLayout component.
*/
export interface ArticleLayoutProps {
/** The title of the article. */
title: string;
/** A short description or lead paragraph for the article. */
description?: string;
/** The date the article was created. */
createdDate: Date;
/** The date the article was last updated. */
updatedDate?: Date;
/** An array of tags associated with the article. */
tags?: string[];
/** The content of the article. */
children: React.ReactNode;
/** Optional additional class names for the article container. */
className?: string;
}
/**
* ArticleLayout provides a structured container for long-form content.
* It uses DefaultLayout and PageColumns to provide a clean reading experience.
*
* @param props - Component props.
* @returns A React element representing the article layout.
* @example
* ```tsx
* <ArticleLayout title="My Article" createdDate={new Date()}>
* Content
* </ArticleLayout>
* ```
*/
export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
title,
createdDate,
updatedDate,
tags,
children,
className,
}) => {
const displayDate = updatedDate ? (
<Text color="muted" size="s" type="label">
Updated <Time date={updatedDate} />
</Text>
) : (
<Time color="muted" date={createdDate} size="s" type="label" />
);
const metadata = (
<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>
);
const breadcrumbs = (
<Breadcrumbs>
<Breadcrumbs.Item>
<a href="/">Home</a>
</Breadcrumbs.Item>
<Breadcrumbs.Separator />
<Breadcrumbs.Item>
<a href="/articles">Articles</a>
</Breadcrumbs.Item>
<Breadcrumbs.Separator />
<Breadcrumbs.Item>{title}</Breadcrumbs.Item>
</Breadcrumbs>
);
return (
<DefaultLayout
breadcrumbs={breadcrumbs}
description={metadata}
paddingY="medium"
title={title}
width="slim"
>
<PageColumns className={className} gap={12}>
<PageColumn size="full">
<section className="w-full">
<Markdown>{children}</Markdown>
</section>
</PageColumn>
</PageColumns>
</DefaultLayout>
);
};
@@ -0,0 +1 @@
export * from './ArticleLayout';
@@ -0,0 +1,114 @@
import { Header, Paragraph, VStack, PageColumns, PageColumn } from '@ermnvldmr/ui';
import React from 'react';
import { DefaultLayout } from './DefaultLayout';
import type { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof DefaultLayout> = {
title: 'WWW/Layout/DefaultLayout',
component: DefaultLayout,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof DefaultLayout>;
const StoryPage = () => (
<VStack className="py-12" gap={8}>
<VStack gap={4}>
<Header level={1}>Default Layout Shell</Header>
<Paragraph size="l">
This is the standard site shell used for most pages in the www service. It includes the
common Header, a constrained PageContainer for content, and the persistent Footer with the
rainbow peek.
</Paragraph>
</VStack>
<PageColumns gap={8}>
<PageColumn size="full">
<VStack gap={4}>
<Header level={2}>Main Content Area</Header>
<Paragraph>
The main content area is automatically wrapped in a PageContainer, ensuring it stays
centered and follows the site&apos;s width constraints.
</Paragraph>
<Paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</Paragraph>
</VStack>
</PageColumn>
<PageColumn size="small">
<VStack className="p-4 bg-slate-50 dark:bg-slate-900 border rounded-lg" gap={4}>
<Header level={3}>Sidebar</Header>
<Paragraph size="s">
This sidebar is placed inside a PageColumn with size=&quot;small&quot;.
</Paragraph>
</VStack>
</PageColumn>
</PageColumns>
</VStack>
);
/**
* Standard width layout.
*/
export const Default: Story = {
args: {
children: <StoryPage />,
width: 'default',
},
};
/**
* Slim width layout, often used for reading-heavy pages.
*/
export const Slim: Story = {
args: {
children: <StoryPage />,
width: 'slim',
},
};
/**
* Vertically centered layout, perfect for landing page sections or simple welcome screens.
*/
export const Centered: Story = {
args: {
children: (
<VStack align="center" className="text-center" gap={6}>
<Header level={1}>Welcome Home</Header>
<Paragraph size="l">
This content is perfectly centered vertically in the viewport. The footer is still peeking
at the bottom.
</Paragraph>
</VStack>
),
centerVertically: true,
},
};
/**
* Demonstration of the "Long Page" behavior where the footer is pushed below the fold.
*/
export const LongPage: Story = {
args: {
children: (
<VStack className="py-12" gap={8}>
<Header level={1}>Scroll Down to see the Footer</Header>
{Array.from({ length: 10 }).map((_, i) => (
// eslint-disable-next-line react/no-array-index-key
<Paragraph key={i}>
Scrollable block {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</Paragraph>
))}
</VStack>
),
},
};
@@ -0,0 +1,81 @@
import { PageContainer, type PageWidth, type PagePadding } from '@ermnvldmr/ui';
import React from 'react';
import { Footer } from '../../Footer';
import { PageHead } from '../../PageHead';
import { IndexLayout } from '../IndexLayout/IndexLayout';
/**
* Props for the DefaultLayout component.
*/
export interface DefaultLayoutProps {
/** Main content slot */
children: React.ReactNode;
/** Title for the PageHead. If provided, PageHead is shown. */
title?: string;
/** Optional caption/description for the PageHead. */
description?: React.ReactNode;
/** Optional breadcrumbs for the PageHead. */
breadcrumbs?: React.ReactNode;
/** Optional metadata for the PageHead. */
headerAddonLeft?: React.ReactNode;
/**
* The maximum width of the content.
* @default 'default'
*/
width?: PageWidth;
/**
* Vertical padding scale.
* @default 'none'
*/
paddingY?: PagePadding;
/**
* Whether to center content vertically.
* @default false
*/
centerVertically?: boolean;
}
/**
* The standard layout shell for the www service.
* Enforces a strict header/content/footer structure.
*
* @param props - The component props.
* @returns A React element representing the default layout.
* @example
* ```tsx
* <DefaultLayout title="My Page">
* Content
* </DefaultLayout>
* ```
*/
export function DefaultLayout({
children,
title,
description,
breadcrumbs,
headerAddonLeft,
width = 'default',
paddingY = 'none',
centerVertically = false,
}: DefaultLayoutProps): React.JSX.Element {
return (
<IndexLayout footer={<Footer />}>
<PageHead
breadcrumbs={breadcrumbs}
caption={description}
heading={title ?? ''}
metadata={headerAddonLeft}
strategy={title ? 'collapsible-sticky' : 'always-collapsed-fixed'}
/>
<PageContainer
as="main"
centerVertically={centerVertically}
paddingY={paddingY}
width={width}
>
{children}
</PageContainer>
</IndexLayout>
);
}
@@ -0,0 +1 @@
export * from './DefaultLayout';
@@ -0,0 +1,59 @@
import React from 'react';
import { IndexLayout } from './IndexLayout';
import type { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof IndexLayout> = {
title: 'WWW/Layout/IndexLayout',
component: IndexLayout,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof IndexLayout>;
/**
* The base "Zero-UI" layout showing how it can be used for custom full-screen content.
*/
export const Default: Story = {
args: {
children: (
<div className="w-full h-full flex flex-col items-center justify-center p-8 text-center gap-8">
<h1 className="text-4xl font-bold">Custom Index View</h1>
<p className="max-w-md">
The IndexLayout is a barebones technical shell. It provides no Header or Footer by
default, and leaves it up to the child to decide how to handle its space.
</p>
<div className="w-full h-32 bg-slate-200 dark:bg-slate-800 rounded-xl flex items-center justify-center border-2 border-dashed border-slate-400">
Custom Dashboard Area
</div>
</div>
),
},
};
/**
* IndexLayout used with a manual footer passed to it.
*/
export const WithManualFooter: Story = {
args: {
children: (
<div className="p-8">
<h1 className="text-2xl font-bold">Page with custom Footer</h1>
<p className="mt-4">
IndexLayout supports an optional footer slot that can be used to manually orchestrate its
position.
</p>
</div>
),
footer: (
<div className="p-4 bg-slate-100 dark:bg-slate-900 border-t text-center font-bold">
I am a manual footer passed to IndexLayout.
</div>
),
},
};
@@ -0,0 +1,35 @@
import { PageRoot } from '@ermnvldmr/ui';
import React from 'react';
/**
* Props for the IndexLayout component.
*/
export interface IndexLayoutProps {
/** Main content slot */
children: React.ReactNode;
/** Footer content slot */
footer?: React.ReactNode;
}
/**
* The root layout for the www service.
* Handles the "peek" footer logic: the content is at least calc(100vh - 4px)
* so the 4px rainbow border of the footer peeks into the viewport.
*
* @param props - The component props.
* @returns A React element representing the index layout.
* @example
* ```tsx
* <IndexLayout footer={<Footer />}>
* Content
* </IndexLayout>
* ```
*/
export function IndexLayout({ children, footer }: IndexLayoutProps): React.JSX.Element {
return (
<PageRoot>
<div className="min-h-[calc(100vh-4px)] flex flex-col pb-24 max-sm:pb-12">{children}</div>
{footer}
</PageRoot>
);
}
@@ -0,0 +1 @@
export * from './IndexLayout';
@@ -0,0 +1,56 @@
import { Header, Paragraph, VStack } from '@ermnvldmr/ui';
import React from 'react';
import { MinimalLayout } from './MinimalLayout';
import type { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof MinimalLayout> = {
title: 'WWW/Layout/MinimalLayout',
component: MinimalLayout,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof MinimalLayout>;
/**
* Minimal layout for focused pages. No header, just content and footer.
*/
export const Default: Story = {
args: {
children: (
<VStack className="p-12" gap={6}>
<Header level={1}>Minimal Layout</Header>
<Paragraph size="l">
MinimalLayout is for focused experiences where you want to minimize distractions. It
includes only the main content area and the common site footer at the bottom.
</Paragraph>
<Paragraph>You&apos;ll notice there is no site header/navigation in this view.</Paragraph>
</VStack>
),
},
};
/**
* Minimal layout with a centered landing section.
*/
export const LandingStyle: Story = {
args: {
children: (
<VStack align="center" className="text-center px-4" gap={8}>
<div className="w-24 h-24 bg-slate-900 dark:bg-slate-100 rounded-full flex items-center justify-center text-white dark:text-black text-3xl font-bold">
VE
</div>
<VStack gap={4}>
<Header level={1}>Vladimir Eremin</Header>
<Paragraph size="l">Building software with focus and precision.</Paragraph>
</VStack>
</VStack>
),
centerVertically: true,
},
};
@@ -0,0 +1,62 @@
import { PageContainer, type PageWidth, type PagePadding } from '@ermnvldmr/ui';
import React from 'react';
import { Footer } from '../../Footer';
import { IndexLayout } from '../IndexLayout/IndexLayout';
/**
* Props for the MinimalLayout component.
*/
export interface MinimalLayoutProps {
/** Main content slot */
children: React.ReactNode;
/**
* The maximum width of the content.
* @default 'default'
*/
width?: PageWidth;
/**
* Vertical padding scale.
* @default 'none'
*/
paddingY?: PagePadding;
/**
* Whether to center content vertically.
* @default false
*/
centerVertically?: boolean;
}
/**
/**
* A minimal layout containing content and a footer.
* Useful for landing pages or high-focus content.
*
* @param props - The component props.
* @returns A React element representing the minimal layout.
* @example
* ```tsx
* <MinimalLayout>
* Content
* </MinimalLayout>
* ```
*/
export function MinimalLayout({
children,
width = 'default',
paddingY = 'none',
centerVertically = false,
}: MinimalLayoutProps): React.JSX.Element {
return (
<IndexLayout footer={<Footer />}>
<PageContainer
as="main"
centerVertically={centerVertically}
paddingY={paddingY}
width={width}
>
{children}
</PageContainer>
</IndexLayout>
);
}
@@ -0,0 +1 @@
export * from './MinimalLayout';
@@ -0,0 +1,52 @@
import { PageHead as PageHeadOriginal, HStack, Link } from '@ermnvldmr/ui';
import React from 'react';
import type { PageHeadStrategy } from '@ermnvldmr/ui';
/**
* Props for the WWW-specific PageHead.
* Restricts customization to content-related fields.
*/
export interface PageHeadProps {
/** The main heading text. */
heading: string;
/** The caption/description text. */
caption?: React.ReactNode;
/** Optional breadcrumbs. */
breadcrumbs?: React.ReactNode;
/** Optional metadata (like dates/tags) to show next to the logo. */
metadata?: React.ReactNode;
/** Header behavior strategy. */
strategy?: PageHeadStrategy;
}
/**
* The standard PageHead for the www service.
* Enforces the "Vladimir Eremin" identity and site navigation.
*
* @param props - Component props.
* @returns A PageHead component.
* @example
* ```tsx
* <PageHead heading="My Page" />
* ```
*/
export const PageHead = ({ heading, caption, breadcrumbs, metadata, strategy }: PageHeadProps) => {
const nav = (
<HStack gap={4}>
<Link href="/articles">Articles</Link>
<Link href="https://github.com/deytenit">GitHub</Link>
</HStack>
);
return (
<PageHeadOriginal
addonLeft={metadata}
addonRight={nav}
breadcrumbs={breadcrumbs}
caption={caption}
heading={heading}
strategy={strategy}
/>
);
};
@@ -0,0 +1 @@
export * from './PageHead';
@@ -0,0 +1,20 @@
import React from 'react';
/**
/**
* A separator that uses a rainbow gradient.
* Glances at the bottom of the viewport to invite scrolling.
*
* @example
* ```tsx
* <RainbowSeparator />
* ```
*/
export const RainbowSeparator = () => {
return (
<div
className="w-full h-[4px] bg-gradient-to-r from-red-500 via-yellow-500 via-green-500 via-blue-500 to-purple-500 animate-pulse opacity-70"
role="separator"
/>
);
};
@@ -0,0 +1 @@
export * from './RainbowSeparator';
@@ -1,100 +0,0 @@
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.
*/
export interface ArticleLayoutProps {
/** The title of the article. */
title: string;
/** A short description or lead paragraph for the article. */
description?: string;
/** The date the article was created. */
createdDate: Date;
/** The date the article was last updated. */
updatedDate?: Date;
/** An array of tags associated with the article. */
tags?: string[];
/** The content of the article. */
children: React.ReactNode;
/** Optional additional class names for the article container. */
className?: string;
}
/**
* ArticleLayout provides a structured container for long-form content.
* It uses the project's base typography styles and provides a hero section
* for metadata like title, date, and tags.
*
* @param props - The component props.
* @param props.title
* @param props.description
* @param props.createdDate
* @param props.updatedDate
* @param props.tags
* @param props.children
* @param props.className
* @returns A React element representing the article layout.
* @example
* ```tsx
* <ArticleLayout
* title="Hello World"
* createdDate={new Date()}
* tags={['hello', 'world']}
* >
* <p>This is my first article.</p>
* </ArticleLayout>
* ```
*/
export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
title,
description,
createdDate,
updatedDate,
tags,
children,
className,
}) => {
const displayDate = updatedDate ? (
<Text color="muted" size="s" type="label">
Updated <Time date={updatedDate} />
</Text>
) : (
<Time color="muted" date={createdDate} size="s" type="label" />
);
return (
<IndexLayout>
<Container as="article" bg="transparent" className={cn('w-full md:w-8/12', 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>
<Header level={1}>{title}</Header>
{description && (
<Paragraph italic color="muted" size="l">
{description}
</Paragraph>
)}
</VStack>
<Container as="section" bg="transparent" className="w-full">
<Markdown>{children}</Markdown>
</Container>
</VStack>
</Container>
</IndexLayout>
);
};
@@ -1,66 +0,0 @@
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 w-screen bg-[var(--rb-background)] transition-colors duration-200"
justify="between"
>
<VStack align="center" as="main" className="min-h-[105vh] py-8 mx-auto" justify="start">
{children}
</VStack>
<VStack as="footer" className="w-full" gap={0}>
<Separator thinned="none" />
<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,16 +0,0 @@
import { Welcome } from './Welcome';
import type { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof Welcome> = {
title: 'Widgets/Welcome',
component: Welcome,
parameters: {
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof Welcome>;
export const Default: Story = {};
@@ -1,19 +0,0 @@
import { Container, Header, Paragraph } from '@ermnvldmr/ui';
import React from 'react';
/**
* A welcome component for the WWW service.
*
* @example
* <Welcome />
*
* @returns {React.JSX.Element} The welcome message.
*/
export const Welcome: React.FC = (): React.JSX.Element => {
return (
<Container bg="muted" padding={4} rounded="lg">
<Header level={1}>Welcome to WWW Service</Header>
<Paragraph>This is a placeholder component for Storybook.</Paragraph>
</Container>
);
};
+1 -1
View File
@@ -1,2 +1,2 @@
@import '../../../../packages/ui/src/static/styles.css';
@source "../../../../packages/ui/src/components";
@source '../../../../packages/ui/src/components';
-12
View File
@@ -1,18 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<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" />