feat: Migrate www to the ui components

This commit is contained in:
deytenit committed 2026-02-10 08:01:18 +03:00
1 parent cb956be216
commit 25044c75ff
18 files changed
+406 -122

No files matched your search

+6 -3
View File
@@ -129,9 +129,12 @@ All packages use the `@ermnvldmr/` namespace:
### TypeScript Guidelines ### TypeScript Guidelines
- **Strict Mode**: All TypeScript strict checks must pass - **Strict Mode**: All TypeScript strict checks must pass
- **Explicit Types**: Avoid `any` and prefer explicit type definitions - **No 'any'**: The use of `any` is strictly prohibited. Use `unknown` if the type is truly unknown, or define proper generic constraints.
- **Interface First**: Use interfaces for component props and public APIs - **No 'ts-ignore' or 'ts-expect-error'**: Never suppress compiler errors. If a third-party library has missing types, provide a local declaration file (.d.ts) instead.
- **Cross-Package Types**: Export types from packages for service consumption - **No Unsafe Assertions**: Avoid `as any` or `as T` assertions. Prefer type guards, `instanceof` checks, or proper schema validation (e.g., Zod) to narrow types.
- **Explicit Types**: Avoid type inference for public APIs; prefer explicit type definitions for exports.
- **Interface First**: Use interfaces for component props and public APIs.
- **Cross-Package Types**: Export types from packages for service consumption.
### TSDoc Documentation Requirements ### TSDoc Documentation Requirements
@@ -176,3 +176,25 @@ export const Composition: Story = {
</Container> </Container>
), ),
}; };
export const SemanticElements: Story = {
render: () => (
<VStack gap={4}>
<Container border as="main" bg="base" padding={4} rounded="md">
Rendered as &lt;main&gt;
</Container>
<Container border as="article" bg="base" padding={4} rounded="md">
Rendered as &lt;article&gt;
</Container>
<Container border as="section" bg="base" padding={4} rounded="md">
Rendered as &lt;section&gt;
</Container>
<Container border as="header" bg="base" padding={4} rounded="md">
Rendered as &lt;header&gt;
</Container>
<Container border as="footer" bg="base" padding={4} rounded="md">
Rendered as &lt;footer&gt;
</Container>
</VStack>
),
};
@@ -25,4 +25,15 @@ describe('components/Container', () => {
expect(container).toHaveClass('cursor-pointer'); expect(container).toHaveClass('cursor-pointer');
expect(container).toHaveClass('active:opacity-80'); expect(container).toHaveClass('active:opacity-80');
}); });
it('renders as different HTML elements using the "as" prop', () => {
const { rerender } = render(<Container as="main">Main Content</Container>);
expect(screen.getByText('Main Content').tagName).toBe('MAIN');
rerender(<Container as="article">Article Content</Container>);
expect(screen.getByText('Article Content').tagName).toBe('ARTICLE');
rerender(<Container as="section">Section Content</Container>);
expect(screen.getByText('Section Content').tagName).toBe('SECTION');
});
}); });
@@ -86,6 +86,8 @@ export interface ContainerProps extends ClassNameProps, TestIdProps {
border?: boolean; border?: boolean;
/** Press interaction handler */ /** Press interaction handler */
onPress?: PressEvents['onPress']; onPress?: PressEvents['onPress'];
/** The HTML element to use for rendering */
as?: React.ElementType;
} }
/** /**
@@ -103,10 +105,11 @@ export const Container = memo(function Container({
shadow = false, shadow = false,
border = false, border = false,
onPress, onPress,
as: Component = 'div',
className, className,
'data-testid': testId, 'data-testid': testId,
}: ContainerProps) { }: ContainerProps) {
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLElement>(null);
const { pressProps } = usePress({ const { pressProps } = usePress({
onPress, onPress,
isDisabled: !onPress, isDisabled: !onPress,
@@ -176,7 +179,7 @@ export const Container = memo(function Container({
}; };
return ( return (
<div <Component
{...pressProps} {...pressProps}
ref={ref} ref={ref}
className={cn( className={cn(
@@ -193,6 +196,6 @@ export const Container = memo(function Container({
data-testid={testId} data-testid={testId}
> >
{children} {children}
</div> </Component>
); );
}); });
@@ -20,7 +20,22 @@ type Story = StoryObj<typeof Link>;
export const Default: Story = { export const Default: Story = {
args: { args: {
href: 'https://example.com', href: 'https://example.com',
children: 'Standard Link', children: 'Standard External Link',
},
};
export const Internal: Story = {
args: {
href: '/internal-page',
children: 'Internal Link',
},
};
export const ForcedExternal: Story = {
args: {
href: '/forced-external',
isExternal: true,
children: 'Forced External Link',
}, },
}; };
@@ -11,6 +11,27 @@ describe('components/Link', () => {
expect(link).toHaveClass('text-[var(--rb-ring)]'); expect(link).toHaveClass('text-[var(--rb-ring)]');
}); });
it('automatically adds target="_blank" and rel for external links', () => {
render(<Link href="https://example.com">External</Link>);
const link = screen.getByRole('link');
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
it('does not add target="_blank" for internal links by default', () => {
render(<Link href="/articles">Internal</Link>);
const link = screen.getByRole('link');
expect(link).not.toHaveAttribute('target');
});
it('respects explicit isExternal prop', () => {
const { rerender } = render(<Link isExternal href="/internal">Force External</Link>);
expect(screen.getByRole('link')).toHaveAttribute('target', '_blank');
rerender(<Link href="https://external.com" isExternal={false}>Force Internal</Link>);
expect(screen.getByRole('link')).not.toHaveAttribute('target');
});
it('renders as a button/span when no href is provided but has onPress', () => { it('renders as a button/span when no href is provided but has onPress', () => {
render(<Link onPress={() => {}}>Click me</Link>); render(<Link onPress={() => {}}>Click me</Link>);
expect(screen.getByText('Click me')).toBeInTheDocument(); expect(screen.getByText('Click me')).toBeInTheDocument();
+16 -2
View File
@@ -15,6 +15,8 @@ export interface LinkProps extends AriaLinkOptions, ClassNameProps {
href?: string; href?: string;
/** Link content */ /** Link content */
children: React.ReactNode; children: React.ReactNode;
/** Whether the link is external (opens in new tab) */
isExternal?: boolean;
} }
/** /**
@@ -24,7 +26,7 @@ export interface LinkProps extends AriaLinkOptions, ClassNameProps {
* It uses standard design system colors (--rb-ring) and provides responsive hover/focus states. * It uses standard design system colors (--rb-ring) and provides responsive hover/focus states.
*/ */
export const Link = memo(function Link(props: LinkProps) { export const Link = memo(function Link(props: LinkProps) {
const { children, className, href, ...otherProps } = props; const { children, className, href, isExternal, ...otherProps } = props;
// We use a specific ref type that useLink expects. // We use a specific ref type that useLink expects.
// Since Link can be an 'a' or a 'span', we use HTMLElement as common denominator. // Since Link can be an 'a' or a 'span', we use HTMLElement as common denominator.
@@ -38,8 +40,20 @@ export const Link = memo(function Link(props: LinkProps) {
); );
if (href) { if (href) {
const isAutoExternal = isExternal ?? href.startsWith('http');
const externalProps = isAutoExternal
? { target: '_blank', rel: 'noopener noreferrer' }
: {};
return ( return (
<Text {...linkProps} ref={ref} as="a" className={sharedClasses} href={href}> <Text
{...linkProps}
{...externalProps}
ref={ref}
as="a"
className={sharedClasses}
href={href}
>
{children} {children}
</Text> </Text>
); );
+1 -1
View File
@@ -31,7 +31,7 @@ export type TextColor =
export interface TextProps export interface TextProps
extends ClassNameProps, extends ClassNameProps,
TestIdProps, TestIdProps,
Omit<React.HTMLAttributes<HTMLElement>, 'as'> { Omit<React.AllHTMLAttributes<HTMLElement>, 'as' | 'type' | 'size' | 'color'> {
/** Content to be rendered */ /** Content to be rendered */
children: React.ReactNode; children: React.ReactNode;
/** Typography style type */ /** Typography style type */
@@ -0,0 +1,63 @@
import React from 'react';
import { Time } from './Time';
import { VStack } from '../VStack/VStack';
import type { Meta, StoryObj } from '@storybook/react';
const meta: Meta<typeof Time> = {
title: 'components/Time',
component: Time,
tags: ['autodocs'],
argTypes: {
color: {
control: 'select',
options: ['default', 'primary', 'secondary', 'tertiary', 'error', 'muted', 'inherit'],
},
size: {
control: 'select',
options: ['s', 'm', 'l'],
},
type: {
control: 'select',
options: ['display', 'headline', 'title', 'body', 'label'],
},
},
};
export default meta;
type Story = StoryObj<typeof Time>;
export const Default: Story = {
args: {
date: new Date(),
},
};
export const CustomFormatting: Story = {
render: () => (
<VStack gap={4}>
<Time date={new Date()} formatOptions={{ dateStyle: 'full' }} />
<Time date={new Date()} formatOptions={{ dateStyle: 'long' }} />
<Time date={new Date()} formatOptions={{ dateStyle: 'medium' }} />
<Time date={new Date()} formatOptions={{ dateStyle: 'short' }} />
</VStack>
),
};
export const Styled: Story = {
render: () => (
<VStack gap={4}>
<Time bold color="primary" date={new Date()} size="l" type="headline" />
<Time italic color="muted" date={new Date()} size="s" type="label" />
</VStack>
),
};
export const CustomChildren: Story = {
args: {
date: new Date(),
children: 'Today',
},
};
@@ -0,0 +1,56 @@
import React from 'react';
import { Time } from './Time';
import { render, screen } from '../../test-utils';
describe('components/Time', () => {
it('renders correctly with a Date object', () => {
const date = new Date('2026-02-10T00:00:00Z');
render(<Time date={date} />);
const timeElement = screen.getByText(/February 10, 2026/i);
expect(timeElement.tagName).toBe('TIME');
expect(timeElement).toHaveAttribute('datetime', date.toISOString());
});
it('renders correctly with an ISO string', () => {
const dateString = '2026-02-10T00:00:00Z';
render(<Time date={dateString} />);
const timeElement = screen.getByText(/February 10, 2026/i);
expect(timeElement.tagName).toBe('TIME');
expect(timeElement).toHaveAttribute('datetime', new Date(dateString).toISOString());
});
it('respects custom children', () => {
const date = new Date('2026-02-10T00:00:00Z');
render(<Time date={date}>Custom Date Display</Time>);
const timeElement = screen.getByText('Custom Date Display');
expect(timeElement).toHaveAttribute('datetime', date.toISOString());
});
it('applies custom formatting options', () => {
const date = new Date('2026-02-10T00:00:00Z');
render(
<Time
date={date}
formatOptions={{ year: '2-digit', month: 'short', day: 'numeric' }}
/>
);
// Depending on locale, "Feb 10, 26" or similar
expect(screen.getByText(/Feb/i)).toBeInTheDocument();
expect(screen.getByText(/10/i)).toBeInTheDocument();
expect(screen.getByText(/26/i)).toBeInTheDocument();
});
it('passes text props correctly', () => {
const date = new Date('2026-02-10T00:00:00Z');
render(<Time bold color="primary" date={date} size="l" />);
const timeElement = screen.getByText(/February 10, 2026/i);
expect(timeElement).toHaveClass('font-bold');
expect(timeElement).toHaveClass('text-[var(--rb-primary-text)]');
});
});
+68
View File
@@ -0,0 +1,68 @@
import { castRef, genericMemo } from '@ermnvldmr/stl';
import React, { forwardRef, useMemo } from 'react';
import { Text } from '../Text/Text';
import type { TextProps } from '../Text/Text';
/**
* Props for the Time component.
*/
export interface TimeProps extends Omit<TextProps, 'as' | 'children'> {
/** The date to display */
date: Date | string;
/** Intl.DateTimeFormatOptions for formatting */
formatOptions?: Intl.DateTimeFormatOptions;
/** Locale for formatting (defaults to system/en-US) */
locale?: string;
/** Content to be rendered instead of formatted date */
children?: React.ReactNode;
/** Semantic ISO date string for the HTML time element */
dateTime?: string;
}
/**
* A semantic time component that wraps the Text component.
*
* Time provides a consistent way to display formatted dates while
* maintaining semantic integrity using the HTML <time> element.
*/
export const Time = genericMemo(
forwardRef<HTMLElement, TimeProps>(function Time(
{ date, formatOptions, locale = 'en-US', children, dateTime, ...props },
ref
) {
const dateObj = useMemo(() => (typeof date === 'string' ? new Date(date) : date), [date]);
const isoString = useMemo(() => dateTime ?? dateObj.toISOString(), [dateTime, dateObj]);
const formattedDate = useMemo(() => {
if (children) return children;
// If formatOptions contains dateStyle or timeStyle, we must not provide
// individual components like year/month/day as it will throw an error.
const hasStyle =
formatOptions && ('dateStyle' in formatOptions || 'timeStyle' in formatOptions);
const options: Intl.DateTimeFormatOptions = hasStyle
? { ...formatOptions }
: {
year: 'numeric',
month: 'long',
day: 'numeric',
...formatOptions,
};
return dateObj.toLocaleDateString(locale, options);
}, [children, dateObj, locale, formatOptions]);
return (
<Text
ref={castRef<HTMLElement>(ref)}
as="time"
dateTime={isoString}
{...props}
>
{formattedDate}
</Text>
);
})
);
+2
View File
@@ -14,6 +14,7 @@ export * from './Code';
export * from './Blockquote'; export * from './Blockquote';
export * from './Stub/Stub'; export * from './Stub/Stub';
export * from './Image/Image'; export * from './Image/Image';
export * from './Time/Time';
export * from './Table'; export * from './Table';
@@ -33,6 +34,7 @@ export type { CodeProps, CodeBlockProps } from './Code';
export type { BlockquoteProps, BlockquoteCitationProps } from './Blockquote'; export type { BlockquoteProps, BlockquoteCitationProps } from './Blockquote';
export type { StubProps } from './Stub/Stub'; export type { StubProps } from './Stub/Stub';
export type { ImageProps } from './Image/Image'; export type { ImageProps } from './Image/Image';
export type { TimeProps } from './Time/Time';
export type { export type {
TableProps, TableProps,
TableDensity, TableDensity,
@@ -5,10 +5,6 @@ createdDate: 2026-01-31
tags: ['test', 'general'] tags: ['test', 'general']
--- ---
# Hello World
This is a test article.
## Section 1 ## Section 1
Some content here. Some content here.
@@ -1,3 +1,5 @@
import { Header, Paragraph, List, Blockquote, Code, CodeBlock, Container, Text } from '@ermnvldmr/ui';
import { ArticleLayout } from './ArticleLayout'; import { ArticleLayout } from './ArticleLayout';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
@@ -26,29 +28,31 @@ export default meta;
type Story = StoryObj<typeof ArticleLayout>; type Story = StoryObj<typeof ArticleLayout>;
/** /**
* Default story showcasing various HTML elements to verify typography. * Default story showcasing various UI components to verify typography in layout.
*/ */
export const Default: Story = { export const Default: Story = {
render: (args) => ( render: (args) => (
<ArticleLayout {...args}> <ArticleLayout {...args}>
<h2>Introduction</h2> <Header level={2}>Introduction</Header>
<p> <Paragraph>
Astro 5 introduces the Content Layer API, a powerful way to fetch content from any source, Astro 5 introduces the Content Layer API, a powerful way to fetch content from any source,
including local files, remote APIs, and CMSs. In this article, we&apos;ll explore how to set including local files, remote APIs, and CMSs. In this article, we&apos;ll explore how to set
up a robust system for articles and projects. up a robust system for articles and projects.
</p> </Paragraph>
<blockquote> <Blockquote>
&quot;The best way to manage content is to not manage it at all.&quot; — Someone, probably. The best way to manage content is to not manage it at all.
</blockquote> <Blockquote.Citation>Someone, probably</Blockquote.Citation>
</Blockquote>
<p> <Paragraph>
Let&apos;s look at some code. Our new <code>content.config.ts</code> uses the{' '} Let&apos;s look at some code. Our new <Code>content.config.ts</Code> uses the{' '}
<code>glob</code> loader to pull in MDX files from outside the <code>src</code> directory: <Code>glob</Code> loader to pull in MDX files from outside the <Code>src</Code> directory:
</p> </Paragraph>
<pre> <CodeBlock label="content.config.ts">
<code>{`// services/www/src/content.config.ts <Code>
{`// services/www/src/content.config.ts
import { defineCollection, z } from 'astro:content'; import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders'; import { glob } from 'astro/loaders';
@@ -63,37 +67,39 @@ const articles = defineCollection({
}), }),
}); });
export const collections = { articles };`}</code> export const collections = { articles };`}
</pre> </Code>
</CodeBlock>
<h3>Why Astro 5?</h3> <Header level={3}>Why Astro 5?</Header>
<p> <Paragraph>
Astro&apos;s zero-JS by default approach is perfect for content-heavy sites. By moving our Astro&apos;s zero-JS by default approach is perfect for content-heavy sites. By moving our
content outside of the <code>src</code> directory, we keep our project clean and organized. content outside of the <Code>src</Code> directory, we keep our project clean and organized.
</p> </Paragraph>
<ul> <List spacing="s" variant="unordered">
<li> <List.Item>
<strong>Better performance:</strong> Only what you need is shipped. <Text bold>Better performance:</Text> Only what you need is shipped.
</li> </List.Item>
<li> <List.Item>
<strong>Improved developer experience:</strong> Fast HMR and great tooling. <Text bold>Improved developer experience:</Text> Fast HMR and great tooling.
</li> </List.Item>
<li> <List.Item>
<strong>Type safety:</strong> Zod schemas validate your frontmatter. <Text bold>Type safety:</Text> Zod schemas validate your frontmatter.
</li> </List.Item>
</ul> </List>
<h3>Visual Media</h3> <Header level={3}>Visual Media</Header>
<p> <Paragraph>
We can also include images and videos. The layout ensures they are responsive and fit the We can also include images and videos. The layout ensures they are responsive and fit the
grid. grid.
</p> </Paragraph>
<div className="bg-muted flex aspect-video items-center justify-center rounded-lg border-2 border-dashed"> <Container border bg="muted" className="flex aspect-video items-center justify-center" rounded="lg">
<span className="text-muted-foreground">Placeholder for Image/Video</span> <Text color="muted">Placeholder for Image/Video</Text>
</div> </Container>
<p>Conclusion: It&apos;s a great time to be a web developer.</p> <Paragraph>Conclusion: It&apos;s a great time to be a web developer.</Paragraph>
</ArticleLayout> </ArticleLayout>
), ),
}; };
@@ -1,5 +1,5 @@
import { cn } from '@ermnvldmr/stl'; import { cn } from '@ermnvldmr/stl';
import { VStack, HStack, Markdown } from '@ermnvldmr/ui'; import { VStack, HStack, Markdown, Container, Time, Header, Paragraph, Text } from '@ermnvldmr/ui';
import React from 'react'; import React from 'react';
/** /**
@@ -56,51 +56,49 @@ export const ArticleLayout: React.FC<ArticleLayoutProps> = ({
children, children,
className, className,
}) => { }) => {
const formattedDate = createdDate.toLocaleDateString('en-US', { const displayDate = updatedDate ? (
year: 'numeric', <Text color="muted" size="s" type="label">
month: 'long', Updated <Time date={updatedDate} />
day: 'numeric', </Text>
}); ) : (
<Time color="muted" date={createdDate} size="s" type="label" />
const displayDate = updatedDate );
? `Updated ${updatedDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}`
: formattedDate;
return ( return (
<article className={cn('mx-auto max-w-3xl px-6 py-12 md:py-20', className)}> <Container as="article" className={cn('mx-auto max-w-3xl px-6 py-12 md:py-20', className)}>
<header className="pb-12"> <VStack as="header" className="pb-12" gap={4}>
<VStack gap={4}> <HStack align="center" className="flex-wrap" gap={4}>
<HStack className="text-muted-foreground text-sm font-medium" gap={4}> {displayDate}
<time dateTime={createdDate.toISOString()}>{displayDate}</time>
{tags && tags.length > 0 && ( {tags && tags.length > 0 && (
<HStack className="flex-wrap" gap={2}> <HStack className="flex-wrap" gap={2}>
{tags.map((tag) => ( {tags.map((tag) => (
<span <Text
key={tag} key={tag}
className="before:text-muted-foreground/50 before:mr-0.5 before:content-['#']" className="before:text-muted-foreground/50 before:mr-0.5 before:content-['#']"
color="muted"
size="s"
> >
{tag} {tag}
</span> </Text>
))} ))}
</HStack> </HStack>
)} )}
</HStack> </HStack>
<h1 className="leading-tight">{title}</h1> <Header className="leading-tight" level={1}>
{title}
</Header>
{description && ( {description && (
<p className="text-muted-foreground text-l leading-relaxed italic">{description}</p> <Paragraph italic className="leading-relaxed" color="muted" size="l">
{description}
</Paragraph>
)} )}
</VStack> </VStack>
</header>
<section className="article-content"> <Container as="section" className="article-content">
<Markdown>{children}</Markdown> <Markdown>{children}</Markdown>
</section> </Container>
</article> </Container>
); );
}; };
+27 -24
View File
@@ -1,4 +1,5 @@
--- ---
import { Container, Header, Paragraph, Link, VStack, List, Time, Text } from '@ermnvldmr/ui';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro'; import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
@@ -11,43 +12,45 @@ const articles = (await getCollection('articles')).sort(
--- ---
<IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects."> <IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects.">
<main class="mx-auto max-w-3xl px-6 py-20"> <Container as="main" className="mx-auto max-w-3xl px-6 py-20">
<h1 class="mb-12">Articles</h1> <Header level={1} className="mb-12">Articles</Header>
<ul class="space-y-12"> <List variant="plain" spacing="l">
{ {
articles.map((article) => ( articles.map((article) => (
<li> <List.Item>
<a href={`/articles/${article.id}`} class="group block no-underline"> <Link href={`/articles/${article.id}`} className="group block no-underline!">
<article class="space-y-3"> <VStack gap={3} as="article">
<time class="text-muted-foreground text-sm font-medium"> <Time
{article.data.createdDate.toLocaleDateString('en-US', { date={article.data.createdDate}
year: 'numeric', color="muted"
month: 'long', size="s"
day: 'numeric', type="label"
})} />
</time> <Header
<h2 class="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors"> level={2}
className="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors"
>
{article.data.title} {article.data.title}
</h2> </Header>
{article.data.description && ( {article.data.description && (
<p class="text-muted-foreground line-clamp-3 text-lg leading-relaxed"> <Paragraph color="muted" size="l" maxLines={3}>
{article.data.description} {article.data.description}
</p> </Paragraph>
)} )}
{article.data.tags && ( {article.data.tags && (
<div class="flex gap-2"> <div class="flex gap-2">
{article.data.tags.map((tag) => ( {article.data.tags.map((tag) => (
<span class="text-muted-foreground text-xs before:content-['#']"> <Text color="muted" size="s" className="before:content-['#']">
{tag} {tag}
</span> </Text>
))} ))}
</div> </div>
)} )}
</article> </VStack>
</a> </Link>
</li> </List.Item>
)) ))
} }
</ul> </List>
</main> </Container>
</IndexLayout> </IndexLayout>
+18 -16
View File
@@ -1,30 +1,32 @@
--- ---
import { Container, Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro'; import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
import { SITE_TITLE, SITE_DESCRIPTION } from '#shared/constants'; import { SITE_TITLE, SITE_DESCRIPTION } from '#shared/constants';
import '#global.css'; import '#global.css';
--- ---
<IndexLayout title={SITE_TITLE} description={SITE_DESCRIPTION}> <IndexLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
<main class="flex h-full w-full justify-center p-4"> <Container as="main" className="flex h-full w-full justify-center p-4">
<div> <VStack gap={4}>
<h2>Welcome to my page!</h2> <Header level={2}>Welcome to my page!</Header>
<p> <Paragraph>
If you see this page, my website is successfuly installed and working. If you see this page, my website is successfuly installed and working.
<br /> <br />
It will be further configured. It will be further configured.
</p> </Paragraph>
<p> <Paragraph>
For the source code and support please refer to <a href="https://github.com/deytenit/ermnvldmr.com/" For the source code and support please refer to <Link href="https://github.com/deytenit/ermnvldmr.com/"
>ermnvldmr.com monorepo</a >ermnvldmr.com monorepo</Link
>. >.
<br /> <br />
Pictures of mine are available at <a href="https://ermnvldmr.com/instagram">@deytenit</a Pictures of mine are available at <Link href="https://ermnvldmr.com/instagram">@deytenit</Link
>. >.
</p> </Paragraph>
<p> <Paragraph>
Check out my <a href="/articles">thoughts and articles on various matters</a>. Check out my <Link href="/articles">thoughts and articles on various matters</Link>.
</p> </Paragraph>
<p><em>Thank you for visiting!</em></p> <Paragraph italic>Thank you for visiting!</Paragraph>
</div> </VStack>
</main> </Container>
</IndexLayout> </IndexLayout>
+5 -4
View File
@@ -1,3 +1,4 @@
import { Container, Header, Paragraph } from '@ermnvldmr/ui';
import React from 'react'; import React from 'react';
/** /**
@@ -10,9 +11,9 @@ import React from 'react';
*/ */
export const Welcome: React.FC = (): React.JSX.Element => { export const Welcome: React.FC = (): React.JSX.Element => {
return ( return (
<div className="p-4 bg-gray-100 rounded-lg"> <Container bg="muted" padding={4} rounded="lg">
<h1 className="text-2xl font-bold">Welcome to WWW Service</h1> <Header level={1}>Welcome to WWW Service</Header>
<p>This is a placeholder component for Storybook.</p> <Paragraph>This is a placeholder component for Storybook.</Paragraph>
</div> </Container>
); );
}; };