feat: Refactor layouts in www
This commit is contained in:
66 files changed
+1805
-352
No files matched your search
@@ -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'];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
+1
-1
@@ -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'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="small".
|
||||
</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'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,2 +1,2 @@
|
||||
@import '../../../../packages/ui/src/static/styles.css';
|
||||
@source "../../../../packages/ui/src/components";
|
||||
@source '../../../../packages/ui/src/components';
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user