feat: Add generic components for UI
This commit is contained in:
38 files changed
+1562
-241
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# Design Plan: PaperArticle Component & Interactive Container
|
||||
|
||||
**Date:** 2026-01-31
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
This plan outlines the implementation of the `PaperArticle` component within the `paper` component set and the enhancement of the generic `Container` component to support interactivity.
|
||||
|
||||
## 1. Interactive Container Enhancement
|
||||
Enhance `packages/ui/src/components/generic/Container` to support press interactions.
|
||||
|
||||
### Changes
|
||||
- **Prop Expansion:** Add `onPress?: (e: PressEvent) => void` to `ContainerProps`.
|
||||
- **Interactivity:** Integrate `usePress` from `react-aria`.
|
||||
- **Ref Handling:** Use `useRef` for the underlying `div` to support `usePress`.
|
||||
- **Visual Feedback:**
|
||||
- Add `cursor-pointer` when `onPress` is present.
|
||||
- Add a subtle active state (e.g., `active:opacity-80`) for immediate haptic/visual feedback.
|
||||
- **Accessibility:** `usePress` handles keyboard interactions (Enter/Space) and ARIA attributes automatically.
|
||||
|
||||
## 2. PaperArticle Component
|
||||
A specialized layout component for newspaper-style articles.
|
||||
|
||||
### Location
|
||||
`packages/ui/src/components/paper/PaperArticle/PaperArticle.tsx`
|
||||
|
||||
### Props
|
||||
- `headline` (string): The main article title.
|
||||
- `subHeadline` (string, optional): Italicized label text between separators.
|
||||
- `additionalText` (string, optional): Small, muted, italicized text at the bottom.
|
||||
- `onPress` (handler, optional): Propagated to the wrapping `Container`.
|
||||
- `children` (ReactNode): The main body content.
|
||||
|
||||
### Layout (VStack)
|
||||
1. **Header:** `level={4}` (headline-m), centered.
|
||||
2. **Separator:** `PaperSeparator` (type: single).
|
||||
3. **Sub-headline (if present):** `Text` (type: label, size: m, italic: true), centered.
|
||||
4. **Separator (if sub-headline present):** `PaperSeparator` (type: single).
|
||||
5. **Main Body:** `Text` (type: body, size: m) wrapping `children`.
|
||||
6. **Additional Text (if present):** `Text` (type: label, size: s, italic: true, color: muted).
|
||||
|
||||
## 3. Implementation Steps
|
||||
1. **Modify `Container`**: Add `react-aria` hooks and `onPress` logic.
|
||||
2. **Create `PaperArticle`**: Implement the structured layout.
|
||||
3. **Export**: Update `packages/ui/src/components/paper/index.ts` and `packages/ui/src/index.ts`.
|
||||
4. **Verify**: Add Storybook stories for `PaperArticle` and interactive `Container` states.
|
||||
@@ -53,6 +53,12 @@ export const baseConfig = tseslint.config(
|
||||
'error',
|
||||
{ prefer: 'type-imports', disallowTypeAnnotations: false },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-assertions': [
|
||||
'error',
|
||||
{
|
||||
assertionStyle: 'never',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'error',
|
||||
{ allowExpressions: true, allowTypedFunctionExpressions: true },
|
||||
@@ -139,6 +145,7 @@ export const baseConfig = tseslint.config(
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
},
|
||||
},
|
||||
@@ -159,6 +166,7 @@ export const baseConfig = tseslint.config(
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/consistent-type-assertions': 'off',
|
||||
'jsdoc/require-jsdoc': 'off',
|
||||
'no-undef': 'off', // For CommonJS files
|
||||
},
|
||||
|
||||
@@ -31,12 +31,18 @@
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/jest-config": "workspace:*",
|
||||
"@ermnvldmr/vite-config": "workspace:*",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"jest": "^30.2.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@
|
||||
* Shared helper utilities for common functionality.
|
||||
*/
|
||||
|
||||
export { cn } from './cn/cn';
|
||||
export { cn } from './cn/cn';
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './functions';
|
||||
export * from './react';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { castRef } from './castRef';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
describe('react/castRef', () => {
|
||||
it('should cast a ref to a different type', () => {
|
||||
const ref = { current: null } as unknown as React.ForwardedRef<HTMLDivElement>;
|
||||
const casted = castRef<HTMLSpanElement>(ref);
|
||||
expect(casted).toBe(ref);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Safely casts a React ref to a specific type.
|
||||
* This is a controlled escape hatch for situations where TypeScript's
|
||||
* built-in Ref types are too restrictive (e.g., in generic forwardRef components).
|
||||
*
|
||||
* @param ref - The ref to cast
|
||||
* @returns The casted ref
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Component ref={castRef<HTMLDivElement>(ref)} />
|
||||
* ```
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function castRef<T>(ref: React.ForwardedRef<any>): React.ForwardedRef<T> {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return ref as React.ForwardedRef<T>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { genericMemo } from './genericMemo';
|
||||
|
||||
describe('react/genericMemo', () => {
|
||||
it('should memoize a component', () => {
|
||||
const Component = ({ name }: { name: string }) => <div>{name}</div>;
|
||||
const Memoized = genericMemo(Component);
|
||||
|
||||
expect(Memoized).toBeDefined();
|
||||
// In React, memo returns a special object with type: component
|
||||
expect((Memoized as any).type).toBe(Component);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* A type-safe version of React.memo that preserves generic types.
|
||||
*
|
||||
* @param component - The component to memoize
|
||||
* @returns The memoized component
|
||||
* @example
|
||||
* ```tsx
|
||||
* export const MyComponent = genericMemo(MyGenericComponent);
|
||||
* ```
|
||||
*/
|
||||
export function genericMemo<T>(component: T): T {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||
return React.memo(component as any) as any as T;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { castRef } from './castRef/castRef';
|
||||
export { genericMemo } from './genericMemo/genericMemo';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Common props for any React element.
|
||||
* Useful for forwarding props to dynamic components.
|
||||
*/
|
||||
export type ReactElementProps = React.HTMLAttributes<HTMLElement> & React.AllHTMLAttributes<HTMLElement>;
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
@@ -25,6 +26,7 @@ const config: StorybookConfig = {
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@ermnvldmr/stl': resolve(__dirname, '../../stl/src/index.ts'),
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { reactJestConfig } from '@ermnvldmr/jest-config/react';
|
||||
|
||||
export default reactJestConfig;
|
||||
export default {
|
||||
...reactJestConfig,
|
||||
moduleNameMapper: {
|
||||
...reactJestConfig.moduleNameMapper,
|
||||
'^@ermnvldmr/stl$': '<rootDir>/../stl/src/index.ts',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Breadcrumbs } from './Breadcrumbs';
|
||||
import { Link } from '../Link/Link';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof Breadcrumbs> = {
|
||||
title: 'components/generic/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof Breadcrumbs>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/">Home</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="slash" />
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/blog">Blog</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="slash" />
|
||||
<Breadcrumbs.Item>Modern Web Design</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithArrows: Story = {
|
||||
render: () => (
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/">Home</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="arrow" />
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/docs">Docs</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="arrow" />
|
||||
<Breadcrumbs.Item>Components</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithBullets: Story = {
|
||||
render: () => (
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/">Home</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="bull" />
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/about">About</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="bull" />
|
||||
<Breadcrumbs.Item>Team</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Breadcrumbs } from './Breadcrumbs';
|
||||
import { render, screen } from '../../../test-utils';
|
||||
import { Link } from '../Link/Link';
|
||||
|
||||
describe('components/generic/Breadcrumbs', () => {
|
||||
it('renders correctly with multiple items and separators', () => {
|
||||
render(
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item>
|
||||
<Link href="/">Home</Link>
|
||||
</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator variant="slash" />
|
||||
<Breadcrumbs.Item>Current</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('navigation')).toHaveAttribute('aria-label', 'Breadcrumb');
|
||||
expect(screen.getByRole('link', { name: 'Home' })).toBeInTheDocument();
|
||||
expect(screen.getByText('/')).toBeInTheDocument();
|
||||
expect(screen.getByText('Current')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports different separator variants', () => {
|
||||
const { rerender } = render(
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Separator data-testid="sep" variant="bull" />
|
||||
</Breadcrumbs>
|
||||
);
|
||||
expect(screen.getByTestId('sep')).toHaveTextContent('•');
|
||||
|
||||
rerender(
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Separator data-testid="sep" variant="slash" />
|
||||
</Breadcrumbs>
|
||||
);
|
||||
expect(screen.getByTestId('sep')).toHaveTextContent('/');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { HStack } from '../HStack/HStack';
|
||||
import { Text } from '../Text/Text';
|
||||
|
||||
import type { HStackProps } from '../HStack/HStack';
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
|
||||
/**
|
||||
* Props for the Breadcrumbs component.
|
||||
*/
|
||||
export interface BreadcrumbsProps extends HStackProps {
|
||||
/** Breadcrumb items and separators */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Root component for hierarchical navigation.
|
||||
*/
|
||||
const BreadcrumbsRoot = memo(function Breadcrumbs({ children, ...props }: BreadcrumbsProps) {
|
||||
return (
|
||||
<nav aria-label="Breadcrumb">
|
||||
<HStack align="center" as="ol" gap={2} {...props}>
|
||||
{children}
|
||||
</HStack>
|
||||
</nav>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Individual step in the breadcrumb trail.
|
||||
*/
|
||||
const BreadcrumbsItem = memo(function BreadcrumbsItem({
|
||||
children,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
} & TestIdProps) {
|
||||
return (
|
||||
<li className={className} data-testid={testId}>
|
||||
<Text color="muted" size="m" type="label">
|
||||
{children}
|
||||
</Text>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Visual divider between breadcrumb items.
|
||||
*/
|
||||
export interface BreadcrumbsSeparatorProps extends ClassNameProps, TestIdProps {
|
||||
/** Visual variant of the separator */
|
||||
variant?: 'slash' | 'bull' | 'arrow';
|
||||
}
|
||||
|
||||
const BreadcrumbsSeparator = memo(function BreadcrumbsSeparator({
|
||||
variant = 'slash',
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: BreadcrumbsSeparatorProps) {
|
||||
const content = {
|
||||
slash: '/',
|
||||
bull: '•',
|
||||
arrow: <ChevronRight className="size-4" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
className={cn('text-[var(--rb-muted-text)]', className)}
|
||||
data-testid={testId}
|
||||
>
|
||||
<Text color="muted" size="m" type="label">
|
||||
{content[variant]}
|
||||
</Text>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* A hierarchical navigation component.
|
||||
*
|
||||
* Breadcrumbs provides a flexible way to display the current location within
|
||||
* a site's hierarchy. It uses a compound component pattern for maximum flexibility.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Breadcrumbs>
|
||||
* <Breadcrumbs.Item><Link href="/">Home</Link></Breadcrumbs.Item>
|
||||
* <Breadcrumbs.Separator variant="arrow" />
|
||||
* <Breadcrumbs.Item>Current Page</Breadcrumbs.Item>
|
||||
* </Breadcrumbs>
|
||||
* ```
|
||||
*/
|
||||
export const Breadcrumbs = Object.assign(BreadcrumbsRoot, {
|
||||
Item: BreadcrumbsItem,
|
||||
Separator: BreadcrumbsSeparator,
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Container } from './Container';
|
||||
import { Text } from '../Text/Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
|
||||
const meta: Meta<typeof Container> = {
|
||||
title: 'components/generic/Container',
|
||||
component: Container,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
bg: {
|
||||
control: 'select',
|
||||
options: ['base', 'primary', 'secondary', 'tertiary', 'error', 'muted', 'transparent'],
|
||||
},
|
||||
padding: {
|
||||
control: 'number',
|
||||
},
|
||||
maxWidth: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg', 'xl', 'none'],
|
||||
},
|
||||
rounded: {
|
||||
control: 'select',
|
||||
options: ['none', 'sm', 'md', 'lg', 'full'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof Container>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'This is a container with some content.',
|
||||
padding: 4,
|
||||
bg: 'base',
|
||||
border: true,
|
||||
rounded: 'md',
|
||||
},
|
||||
};
|
||||
|
||||
export const SemanticColors: Story = {
|
||||
render: () => (
|
||||
<VStack gap={4}>
|
||||
<Container border bg="base" padding={4} rounded="md">Base Surface</Container>
|
||||
<Container bg="primary" padding={4} rounded="md">Primary Surface</Container>
|
||||
<Container bg="secondary" padding={4} rounded="md">Secondary Surface</Container>
|
||||
<Container bg="tertiary" padding={4} rounded="md">Tertiary Surface</Container>
|
||||
<Container bg="error" padding={4} rounded="md">Error Surface</Container>
|
||||
<Container bg="muted" padding={4} rounded="md">Muted Surface</Container>
|
||||
<Container border bg="transparent" padding={4} rounded="md">Transparent Surface</Container>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const PaddingScale: Story = {
|
||||
render: () => (
|
||||
<VStack gap={4}>
|
||||
<Container border bg="muted" padding={1}>Padding 1</Container>
|
||||
<Container border bg="muted" padding={2}>Padding 2</Container>
|
||||
<Container border bg="muted" padding={4}>Padding 4</Container>
|
||||
<Container border bg="muted" padding={8}>Padding 8</Container>
|
||||
<Container border bg="muted" padding={12}>Padding 12</Container>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const MaxWidths: Story = {
|
||||
render: () => (
|
||||
<VStack className="w-full" gap={4}>
|
||||
<Container bg="tertiary" maxWidth="sm" padding={4} rounded="md">Max Width SM (640px)</Container>
|
||||
<Container bg="tertiary" maxWidth="md" padding={4} rounded="md">Max Width MD (768px)</Container>
|
||||
<Container bg="tertiary" maxWidth="lg" padding={4} rounded="md">Max Width LG (1024px)</Container>
|
||||
<Container bg="tertiary" maxWidth="xl" padding={4} rounded="md">Max Width XL (1280px)</Container>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Rounding: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Container bg="secondary" padding={8} rounded="none">None</Container>
|
||||
<Container bg="secondary" padding={8} rounded="sm">Small</Container>
|
||||
<Container bg="secondary" padding={8} rounded="md">Medium</Container>
|
||||
<Container bg="secondary" padding={8} rounded="lg">Large</Container>
|
||||
<Container bg="secondary" padding={8} rounded="full">Full</Container>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const ElevationAndBorders: Story = {
|
||||
render: () => (
|
||||
<VStack gap={8}>
|
||||
<Container border bg="base" padding={6} rounded="lg">
|
||||
Border Only
|
||||
</Container>
|
||||
<Container shadow bg="base" padding={6} rounded="lg">
|
||||
Shadow Only
|
||||
</Container>
|
||||
<Container border shadow bg="base" padding={6} rounded="lg">
|
||||
Border + Shadow
|
||||
</Container>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Composition: Story = {
|
||||
render: () => (
|
||||
<Container border shadow bg="base" maxWidth="md" padding={0} rounded="lg">
|
||||
<Container bg="primary" padding={4} rounded="none">
|
||||
<Text bold color="inherit" size="l" type="title">Card Header</Text>
|
||||
</Container>
|
||||
<Container padding={6}>
|
||||
<VStack gap={4}>
|
||||
<Text size="m" type="body">
|
||||
This example demonstrates how containers can be nested and composed.
|
||||
The outer container has a shadow and border, while the header uses a primary background.
|
||||
</Text>
|
||||
<Container border bg="muted" padding={4} rounded="md">
|
||||
<Text size="m" type="label">Inside a nested muted container</Text>
|
||||
</Container>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Container>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { Container } from './Container';
|
||||
import { render, screen } from '../../../test-utils';
|
||||
|
||||
describe('components/generic/Container', () => {
|
||||
it('handles onPress event', async () => {
|
||||
const user = userEvent.setup();
|
||||
const handlePress = jest.fn();
|
||||
render(<Container onPress={handlePress}>Content</Container>);
|
||||
|
||||
const container = screen.getByText('Content');
|
||||
await user.click(container);
|
||||
expect(handlePress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('applies cursor-pointer and active styles when onPress is present', () => {
|
||||
const { rerender } = render(<Container>Content</Container>);
|
||||
let container = screen.getByText('Content');
|
||||
expect(container).not.toHaveClass('cursor-pointer');
|
||||
|
||||
rerender(<Container onPress={() => {}}>Content</Container>);
|
||||
container = screen.getByText('Content');
|
||||
expect(container).toHaveClass('cursor-pointer');
|
||||
expect(container).toHaveClass('active:opacity-80');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { memo } from 'react';
|
||||
import { usePress } from 'react-aria';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
import type { PressEvents } from 'react-aria';
|
||||
|
||||
/**
|
||||
* Tailwind gap spacing scale values.
|
||||
*/
|
||||
type GapScale = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
|
||||
|
||||
/**
|
||||
* Background variants for the Container.
|
||||
*/
|
||||
export type ContainerBackground = 'base' | 'primary' | 'secondary' | 'tertiary' | 'error' | 'muted' | 'transparent';
|
||||
|
||||
/**
|
||||
* Max width constraints for the Container.
|
||||
*/
|
||||
export type ContainerMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'none';
|
||||
|
||||
/**
|
||||
* Border radius variants for the Container.
|
||||
*/
|
||||
export type ContainerRounded = 'none' | 'sm' | 'md' | 'lg' | 'full';
|
||||
|
||||
/**
|
||||
* Props for the Container component.
|
||||
*/
|
||||
export interface ContainerProps extends ClassNameProps, TestIdProps {
|
||||
/** Content to be rendered inside the container */
|
||||
children: React.ReactNode;
|
||||
/** Background color variant */
|
||||
bg?: ContainerBackground;
|
||||
/** Padding using Tailwind spacing scale */
|
||||
padding?: GapScale;
|
||||
/** Maximum width constraint */
|
||||
maxWidth?: ContainerMaxWidth;
|
||||
/** Border radius variant */
|
||||
rounded?: ContainerRounded;
|
||||
/** Whether to show a shadow */
|
||||
shadow?: boolean;
|
||||
/** Whether to show a border */
|
||||
border?: boolean;
|
||||
/** Press interaction handler */
|
||||
onPress?: PressEvents['onPress'];
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic surface component for creating structural layouts.
|
||||
*
|
||||
* Container provides a consistent way to handle backgrounds, padding,
|
||||
* borders, and width constraints while maintaining theme consistency.
|
||||
*/
|
||||
export const Container = memo(function Container({
|
||||
children,
|
||||
bg = 'base',
|
||||
padding = 0,
|
||||
maxWidth = 'none',
|
||||
rounded = 'none',
|
||||
shadow = false,
|
||||
border = false,
|
||||
onPress,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: ContainerProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const { pressProps } = usePress({
|
||||
onPress,
|
||||
isDisabled: !onPress,
|
||||
});
|
||||
|
||||
const bgClasses: Record<ContainerBackground, string> = {
|
||||
base: 'bg-[var(--rb-container-base)] text-[var(--rb-container-text)]',
|
||||
primary: 'bg-[var(--rb-primary-base)] text-[var(--rb-primary-text)]',
|
||||
secondary: 'bg-[var(--rb-secondary-base)] text-[var(--rb-secondary-text)]',
|
||||
tertiary: 'bg-[var(--rb-tertiary-base)] text-[var(--rb-tertiary-text)]',
|
||||
error: 'bg-[var(--rb-error-base)] text-[var(--rb-error-text)]',
|
||||
muted: 'bg-[var(--rb-muted-base)] text-[var(--rb-muted-text)]',
|
||||
transparent: 'bg-transparent',
|
||||
};
|
||||
|
||||
const maxWidthClasses: Record<ContainerMaxWidth, string> = {
|
||||
sm: 'max-w-screen-sm',
|
||||
md: 'max-w-screen-md',
|
||||
lg: 'max-w-screen-lg',
|
||||
xl: 'max-w-screen-xl',
|
||||
none: 'max-w-none',
|
||||
};
|
||||
|
||||
const roundedClasses: Record<ContainerRounded, string> = {
|
||||
none: 'rounded-none',
|
||||
sm: 'rounded-sm',
|
||||
md: 'rounded-md',
|
||||
lg: 'rounded-lg',
|
||||
full: 'rounded-full',
|
||||
};
|
||||
|
||||
const paddingClasses: Record<GapScale, string> = {
|
||||
0: 'p-0',
|
||||
0.5: 'p-0.5',
|
||||
1: 'p-1',
|
||||
1.5: 'p-1.5',
|
||||
2: 'p-2',
|
||||
2.5: 'p-2.5',
|
||||
3: 'p-3',
|
||||
3.5: 'p-3.5',
|
||||
4: 'p-4',
|
||||
5: 'p-5',
|
||||
6: 'p-6',
|
||||
7: 'p-7',
|
||||
8: 'p-8',
|
||||
9: 'p-9',
|
||||
10: 'p-10',
|
||||
11: 'p-11',
|
||||
12: 'p-12',
|
||||
14: 'p-14',
|
||||
16: 'p-16',
|
||||
20: 'p-20',
|
||||
24: 'p-24',
|
||||
28: 'p-28',
|
||||
32: 'p-32',
|
||||
36: 'p-36',
|
||||
40: 'p-40',
|
||||
44: 'p-44',
|
||||
48: 'p-48',
|
||||
52: 'p-52',
|
||||
56: 'p-56',
|
||||
60: 'p-60',
|
||||
64: 'p-64',
|
||||
72: 'p-72',
|
||||
80: 'p-80',
|
||||
96: 'p-96',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
{...pressProps}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'overflow-hidden',
|
||||
bgClasses[bg],
|
||||
maxWidthClasses[maxWidth],
|
||||
roundedClasses[rounded],
|
||||
paddingClasses[padding],
|
||||
shadow && 'shadow-md',
|
||||
border && 'border border-[var(--rb-outline)]',
|
||||
onPress && 'cursor-pointer active:opacity-80 transition-opacity',
|
||||
className
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, forwardRef } from 'react';
|
||||
|
||||
import { Stack } from '../Stack/Stack';
|
||||
|
||||
@@ -37,6 +37,6 @@ export interface HStackProps extends Omit<StackProps, 'direction'> {
|
||||
* </HStack>
|
||||
* ```
|
||||
*/
|
||||
export const HStack = memo(function HStack(props: HStackProps) {
|
||||
return <Stack direction="row" {...props} />;
|
||||
});
|
||||
export const HStack = memo(forwardRef<HTMLElement, HStackProps>(function HStack(props, ref) {
|
||||
return <Stack direction="row" {...props} ref={ref} />;
|
||||
}));
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Header } from './Header';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
|
||||
const meta: Meta<typeof Header> = {
|
||||
title: 'Generic/Header',
|
||||
component: Header,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
level: {
|
||||
control: { type: 'select' },
|
||||
options: [1, 2, 3, 4, 5, 6],
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['default', 'primary', 'secondary', 'tertiary', 'error', 'muted', 'inherit'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof Header>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Header Level 1',
|
||||
level: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const AllLevels: Story = {
|
||||
render: () => (
|
||||
<VStack gap={4}>
|
||||
<Header level={1}>Header Level 1 (Display L)</Header>
|
||||
<Header level={2}>Header Level 2 (Display M)</Header>
|
||||
<Header level={3}>Header Level 3 (Headline L)</Header>
|
||||
<Header level={4}>Header Level 4 (Headline M)</Header>
|
||||
<Header level={5}>Header Level 5 (Title L)</Header>
|
||||
<Header level={6}>Header Level 6 (Title M)</Header>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Overrides: Story = {
|
||||
render: () => (
|
||||
<VStack gap={4}>
|
||||
<Header level={1} size="s" type="headline">H1 Tag with Headline Small Style</Header>
|
||||
<Header color="primary" level={3} size="l" type="body">H3 Tag with Body Large Style and Primary Color</Header>
|
||||
<Header italic underline level={6}>H6 with Modifiers</Header>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { Text } from '../Text/Text';
|
||||
|
||||
import type { TextProps, TextType, TextSize } from '../Text/Text';
|
||||
|
||||
/**
|
||||
* Props for the Header component.
|
||||
*/
|
||||
export interface HeaderProps extends Omit<TextProps, 'as'> {
|
||||
/** The semantic header level (1-6) */
|
||||
level: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* A semantic header component that wraps the Text component.
|
||||
*
|
||||
* Header automatically maps semantic levels (h1-h6) to the appropriate
|
||||
* typography variants in the design system, ensuring accessibility
|
||||
* and visual consistency.
|
||||
*/
|
||||
export const Header = memo(function Header({
|
||||
level,
|
||||
type,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: HeaderProps) {
|
||||
// Default mappings from level to type and size
|
||||
const levelMapping: Record<number, { type: TextType; size: TextSize }> = {
|
||||
1: { type: 'display', size: 'l' },
|
||||
2: { type: 'display', size: 'm' },
|
||||
3: { type: 'headline', size: 'l' },
|
||||
4: { type: 'headline', size: 'm' },
|
||||
5: { type: 'title', size: 'l' },
|
||||
6: { type: 'title', size: 'm' },
|
||||
};
|
||||
|
||||
const defaultStyles = levelMapping[level];
|
||||
const tags = {
|
||||
1: 'h1',
|
||||
2: 'h2',
|
||||
3: 'h3',
|
||||
4: 'h4',
|
||||
5: 'h5',
|
||||
6: 'h6',
|
||||
} as const;
|
||||
const Tag = tags[level];
|
||||
|
||||
return (
|
||||
<Text
|
||||
as={Tag}
|
||||
size={size ?? defaultStyles.size}
|
||||
type={type ?? defaultStyles.type}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Link } from './Link';
|
||||
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof Link> = {
|
||||
title: 'components/generic/Link',
|
||||
component: Link,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onPress: { action: 'pressed' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof Link>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
href: 'https://example.com',
|
||||
children: 'Standard Link',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithoutHref: Story = {
|
||||
args: {
|
||||
children: 'Action Link (no href)',
|
||||
onPress: () => alert('Link pressed!'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Link } from './Link';
|
||||
import { render, screen } from '../../../test-utils';
|
||||
|
||||
describe('components/generic/Link', () => {
|
||||
it('renders correctly as a link when href is provided', () => {
|
||||
render(<Link href="https://example.com">Click me</Link>);
|
||||
const link = screen.getByRole('link', { name: 'Click me' });
|
||||
expect(link).toHaveAttribute('href', 'https://example.com');
|
||||
expect(link).toHaveClass('text-[var(--rb-ring)]');
|
||||
});
|
||||
|
||||
it('renders as a button/span when no href is provided but has onPress', () => {
|
||||
render(<Link onPress={() => {}}>Click me</Link>);
|
||||
expect(screen.getByText('Click me')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { castRef, cn } from '@ermnvldmr/stl';
|
||||
import React, { memo, useRef } from 'react';
|
||||
import { useLink } from 'react-aria';
|
||||
|
||||
import { Text } from '../Text/Text';
|
||||
|
||||
import type { ClassNameProps } from '@ermnvldmr/stl';
|
||||
import type { AriaLinkOptions } from 'react-aria';
|
||||
|
||||
/**
|
||||
* Props for the Link component.
|
||||
*/
|
||||
export interface LinkProps extends AriaLinkOptions, ClassNameProps {
|
||||
/** The destination URL. Renders as an <a> tag if provided. */
|
||||
href?: string;
|
||||
/** Link content */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic navigation primitive for interactive text.
|
||||
*
|
||||
* Link ensures consistent styling and accessibility across all navigation elements.
|
||||
* It uses standard design system colors (--rb-ring) and provides responsive hover/focus states.
|
||||
*/
|
||||
export const Link = memo(function Link(props: LinkProps) {
|
||||
const { children, className, href, ...otherProps } = props;
|
||||
|
||||
// We use a specific ref type that useLink expects.
|
||||
// Since Link can be an 'a' or a 'span', we use HTMLElement as common denominator.
|
||||
const ref = useRef<HTMLAnchorElement>(null);
|
||||
const { linkProps } = useLink(otherProps, ref);
|
||||
|
||||
const sharedClasses = cn(
|
||||
'text-[var(--rb-ring)] cursor-pointer hover:underline underline-offset-4 transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Text
|
||||
{...linkProps}
|
||||
ref={ref}
|
||||
as="a"
|
||||
className={sharedClasses}
|
||||
href={href}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
{...linkProps}
|
||||
ref={castRef<HTMLSpanElement>(ref)}
|
||||
as="span"
|
||||
className={sharedClasses}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
||||
* through semantic prop names that map to Tailwind CSS utility classes.
|
||||
*/
|
||||
const meta: Meta<typeof Stack> = {
|
||||
title: 'components/shared/Stack',
|
||||
title: 'components/generic/Stack',
|
||||
component: Stack,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { memo } from 'react';
|
||||
import { cn, castRef, genericMemo } from '@ermnvldmr/stl';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
|
||||
@@ -44,6 +44,8 @@ export interface StackProps extends ClassNameProps, TestIdProps {
|
||||
wrap?: FlexWrap;
|
||||
/** Spacing between items using Tailwind's spacing scale */
|
||||
gap?: GapScale;
|
||||
/** The HTML element to use for rendering */
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,22 +71,23 @@ export interface StackProps extends ClassNameProps, TestIdProps {
|
||||
* </Stack>
|
||||
* ```
|
||||
*/
|
||||
export const Stack = memo(function Stack({
|
||||
const StackComponent = forwardRef<HTMLElement, StackProps>(function Stack({
|
||||
children,
|
||||
direction = 'row',
|
||||
justify = 'start',
|
||||
align = 'start',
|
||||
wrap = 'nowrap',
|
||||
gap = 0,
|
||||
as: Component = 'div',
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: StackProps) {
|
||||
}, ref) {
|
||||
const directionClasses = {
|
||||
row: 'flex-row',
|
||||
col: 'flex-col',
|
||||
'row-reverse': 'flex-row-reverse',
|
||||
'col-reverse': 'flex-col-reverse',
|
||||
};
|
||||
} as const;
|
||||
|
||||
const justifyClasses = {
|
||||
start: 'justify-start',
|
||||
@@ -93,7 +96,7 @@ export const Stack = memo(function Stack({
|
||||
between: 'justify-between',
|
||||
around: 'justify-around',
|
||||
evenly: 'justify-evenly',
|
||||
};
|
||||
} as const;
|
||||
|
||||
const alignClasses = {
|
||||
start: 'items-start',
|
||||
@@ -101,13 +104,13 @@ export const Stack = memo(function Stack({
|
||||
center: 'items-center',
|
||||
baseline: 'items-baseline',
|
||||
stretch: 'items-stretch',
|
||||
};
|
||||
} as const;
|
||||
|
||||
const wrapClasses = {
|
||||
nowrap: 'flex-nowrap',
|
||||
wrap: 'flex-wrap',
|
||||
'wrap-reverse': 'flex-wrap-reverse',
|
||||
};
|
||||
} as const;
|
||||
|
||||
// Convert gap scale to Tailwind class using explicit mapping
|
||||
// This ensures all gap classes are detected by Tailwind's static analysis
|
||||
@@ -127,7 +130,7 @@ export const Stack = memo(function Stack({
|
||||
8: 'gap-8',
|
||||
9: 'gap-9',
|
||||
10: 'gap-10',
|
||||
11: 'gap-11',
|
||||
11: 'gap-10',
|
||||
12: 'gap-12',
|
||||
14: 'gap-14',
|
||||
16: 'gap-16',
|
||||
@@ -161,11 +164,14 @@ export const Stack = memo(function Stack({
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
<Component
|
||||
ref={castRef<HTMLElement>(ref)}
|
||||
className={stackClasses}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
export const Stack = genericMemo(StackComponent);
|
||||
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Text } from './Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
|
||||
const meta: Meta<typeof Text> = {
|
||||
title: 'components/generic/Text',
|
||||
component: Text,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['display', 'headline', 'title', 'body', 'label'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['s', 'm', 'l'],
|
||||
},
|
||||
color: {
|
||||
control: 'select',
|
||||
options: ['default', 'primary', 'secondary', 'tertiary', 'error', 'muted', 'inherit'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof Text>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'The quick brown fox jumps over the lazy dog.',
|
||||
type: 'body',
|
||||
size: 'm',
|
||||
},
|
||||
};
|
||||
|
||||
export const TypeScale: Story = {
|
||||
render: () => (
|
||||
<VStack gap={8}>
|
||||
<div>
|
||||
<Text className="mb-4 block uppercase tracking-widest" color="muted" size="s" type="label">Display</Text>
|
||||
<VStack gap={2}>
|
||||
<Text size="l" type="display">Display Large</Text>
|
||||
<Text size="m" type="display">Display Medium</Text>
|
||||
<Text size="s" type="display">Display Small</Text>
|
||||
</VStack>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="mb-4 block uppercase tracking-widest" color="muted" size="s" type="label">Headline</Text>
|
||||
<VStack gap={2}>
|
||||
<Text size="l" type="headline">Headline Large</Text>
|
||||
<Text size="m" type="headline">Headline Medium</Text>
|
||||
<Text size="s" type="headline">Headline Small</Text>
|
||||
</VStack>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="mb-4 block uppercase tracking-widest" color="muted" size="s" type="label">Title</Text>
|
||||
<VStack gap={2}>
|
||||
<Text size="l" type="title">Title Large</Text>
|
||||
<Text size="m" type="title">Title Medium</Text>
|
||||
<Text size="s" type="title">Title Small</Text>
|
||||
</VStack>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="mb-4 block uppercase tracking-widest" color="muted" size="s" type="label">Body</Text>
|
||||
<VStack gap={2}>
|
||||
<Text size="l" type="body">Body Large</Text>
|
||||
<Text size="m" type="body">Body Medium</Text>
|
||||
<Text size="s" type="body">Body Small</Text>
|
||||
</VStack>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="mb-4 block uppercase tracking-widest" color="muted" size="s" type="label">Label</Text>
|
||||
<VStack gap={2}>
|
||||
<Text size="l" type="label">Label Large</Text>
|
||||
<Text size="m" type="label">Label Medium</Text>
|
||||
<Text size="s" type="label">Label Small</Text>
|
||||
</VStack>
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Colors: Story = {
|
||||
render: () => (
|
||||
<VStack gap={2}>
|
||||
<Text color="default">Default Text Color</Text>
|
||||
<Text color="primary">Primary Text Color</Text>
|
||||
<Text color="secondary">Secondary Text Color</Text>
|
||||
<Text color="tertiary">Tertiary Text Color</Text>
|
||||
<Text color="error">Error Text Color</Text>
|
||||
<Text color="muted">Muted Text Color</Text>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
|
||||
export const Modifiers: Story = {
|
||||
render: () => (
|
||||
<VStack gap={2}>
|
||||
<Text bold>Bold Text</Text>
|
||||
<Text italic>Italic Text</Text>
|
||||
<Text underline>Underlined Text</Text>
|
||||
<Text strike>Strikethrough Text</Text>
|
||||
<Text bold italic underline>Combined Modifiers</Text>
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { cn, castRef, genericMemo } from '@ermnvldmr/stl';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
|
||||
/**
|
||||
* Typography types.
|
||||
*/
|
||||
export type TextType = 'display' | 'headline' | 'title' | 'body' | 'label';
|
||||
|
||||
/**
|
||||
* Typography sizes.
|
||||
*/
|
||||
export type TextSize = 's' | 'm' | 'l';
|
||||
|
||||
/**
|
||||
* Text color variants.
|
||||
*/
|
||||
export type TextColor = 'default' | 'primary' | 'secondary' | 'tertiary' | 'error' | 'muted' | 'inherit';
|
||||
|
||||
/**
|
||||
* Props for the Text component.
|
||||
*/
|
||||
export interface TextProps extends ClassNameProps, TestIdProps, Omit<React.HTMLAttributes<HTMLElement>, 'as'> {
|
||||
/** Content to be rendered */
|
||||
children: React.ReactNode;
|
||||
/** Typography style type */
|
||||
type?: TextType;
|
||||
/** Typography size variant */
|
||||
size?: TextSize;
|
||||
/** Text color variant */
|
||||
color?: TextColor;
|
||||
/** Whether the text should be bold */
|
||||
bold?: boolean;
|
||||
/** Whether the text should be italic */
|
||||
italic?: boolean;
|
||||
/** Whether the text should be underlined */
|
||||
underline?: boolean;
|
||||
/** Whether the text should have a strike-through */
|
||||
strike?: boolean;
|
||||
/** The HTML element to use for rendering */
|
||||
as?: React.ElementType;
|
||||
/** Optional href for links */
|
||||
href?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A flexible typography component that implements the design system's type scale.
|
||||
*
|
||||
* Text centralizes all typography rules, including font families, sizes,
|
||||
* line heights, and letter spacing, reducing the need for custom CSS.
|
||||
*/
|
||||
const TextComponent = forwardRef<HTMLElement, TextProps>(function Text({
|
||||
children,
|
||||
type = 'body',
|
||||
size = 'm',
|
||||
color = 'default',
|
||||
bold = false,
|
||||
italic = false,
|
||||
underline = false,
|
||||
strike = false,
|
||||
as: Component = 'span',
|
||||
className,
|
||||
'data-testid': testId,
|
||||
...props
|
||||
}, ref) {
|
||||
|
||||
// Base typography styles mapping
|
||||
const typeStyles: Record<TextType, Record<TextSize, string>> = {
|
||||
display: {
|
||||
l: 'font-serif text-[57px] leading-[64px] tracking-[-0.25px]',
|
||||
m: 'font-serif text-[45px] leading-[52px] tracking-normal',
|
||||
s: 'font-serif text-[36px] leading-[44px] tracking-normal',
|
||||
},
|
||||
headline: {
|
||||
l: 'font-serif text-[32px] leading-[40px] tracking-normal',
|
||||
m: 'font-serif text-[28px] leading-[36px] tracking-normal',
|
||||
s: 'font-serif text-[24px] leading-[32px] tracking-normal',
|
||||
},
|
||||
title: {
|
||||
l: 'font-sans text-[22px] leading-[28px] tracking-normal',
|
||||
m: 'font-sans text-[16px] leading-[24px] tracking-[0.15px]',
|
||||
s: 'font-sans text-[14px] leading-[20px] tracking-[0.1px]',
|
||||
},
|
||||
body: {
|
||||
l: 'font-sans text-[16px] leading-[24px] tracking-[0.5px]',
|
||||
m: 'font-sans text-[14px] leading-[20px] tracking-[0.25px]',
|
||||
s: 'font-sans text-[12px] leading-[16px] tracking-[0.4px]',
|
||||
},
|
||||
label: {
|
||||
l: 'font-sans font-medium text-[14px] leading-[20px] tracking-[0.1px]',
|
||||
m: 'font-sans font-medium text-[12px] leading-[16px] tracking-[0.5px]',
|
||||
s: 'font-sans font-medium text-[11px] leading-[16px] tracking-[0.5px]',
|
||||
},
|
||||
};
|
||||
|
||||
const colorClasses: Record<TextColor, string> = {
|
||||
default: 'text-[var(--rb-text)]',
|
||||
primary: 'text-[var(--rb-primary-text)]',
|
||||
secondary: 'text-[var(--rb-secondary-text)]',
|
||||
tertiary: 'text-[var(--rb-tertiary-text)]',
|
||||
error: 'text-[var(--rb-error-text)]',
|
||||
muted: 'text-[var(--rb-muted-text)]',
|
||||
inherit: 'text-inherit',
|
||||
};
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
ref={castRef<HTMLElement>(ref)}
|
||||
className={cn(
|
||||
typeStyles[type][size],
|
||||
colorClasses[color],
|
||||
bold && 'font-bold',
|
||||
italic && 'italic',
|
||||
underline && 'underline',
|
||||
strike && 'line-through',
|
||||
className
|
||||
)}
|
||||
data-testid={testId}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
export const Text = genericMemo(TextComponent);
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, forwardRef } from 'react';
|
||||
|
||||
import { Stack } from '../Stack/Stack';
|
||||
|
||||
@@ -37,6 +37,6 @@ export interface VStackProps extends Omit<StackProps, 'direction'> {
|
||||
* </VStack>
|
||||
* ```
|
||||
*/
|
||||
export const VStack = memo(function VStack(props: VStackProps) {
|
||||
return <Stack direction="col" {...props} />;
|
||||
});
|
||||
export const VStack = memo(forwardRef<HTMLElement, VStackProps>(function VStack(props, ref) {
|
||||
return <Stack direction="col" {...props} ref={ref} />;
|
||||
}));
|
||||
@@ -1,2 +1,13 @@
|
||||
export { HStack } from './HStack/HStack';
|
||||
export { VStack } from './VStack/VStack';
|
||||
export * from './Container/Container';
|
||||
export * from './Header/Header';
|
||||
export * from './HStack/HStack';
|
||||
export * from './Stack/Stack';
|
||||
export * from './Text/Text';
|
||||
export * from './VStack/VStack';
|
||||
export * from './Link/Link';
|
||||
export * from './Breadcrumbs/Breadcrumbs';
|
||||
|
||||
export type { StackProps } from './Stack/Stack';
|
||||
export type { ContainerProps, ContainerBackground, ContainerMaxWidth, ContainerRounded } from './Container/Container';
|
||||
export type { TextProps, TextType, TextSize, TextColor } from './Text/Text';
|
||||
export type { HeaderProps } from './Header/Header';
|
||||
@@ -0,0 +1,42 @@
|
||||
import { PaperArticle } from './PaperArticle';
|
||||
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof PaperArticle> = {
|
||||
title: 'Components/Paper/PaperArticle',
|
||||
component: PaperArticle,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
onPress: { action: 'pressed' },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof PaperArticle>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
headline: 'AI Agent Learns to Code',
|
||||
children: 'In a shocking turn of events, a large language model has successfully implemented a UI component from scratch while maintaining design system consistency.',
|
||||
},
|
||||
};
|
||||
|
||||
export const Full: Story = {
|
||||
args: {
|
||||
headline: 'The Future of Web Development',
|
||||
subHeadline: 'Design Systems and AI Collaboration',
|
||||
additionalText: 'By AI Reporter #42',
|
||||
children: 'Newspaper-style layouts are making a comeback in modern web design. By combining classical typography principles with modern responsive frameworks, developers can create unique and engaging user experiences.',
|
||||
},
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
...Full.args,
|
||||
onPress: () => alert('Article clicked!'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PaperArticle } from './PaperArticle';
|
||||
import { render, screen } from '../../../test-utils';
|
||||
|
||||
describe('components/paper/PaperArticle', () => {
|
||||
const props = {
|
||||
headline: 'Breaking News',
|
||||
subHeadline: 'Local cat saves city',
|
||||
additionalText: 'Reporting by John Doe',
|
||||
};
|
||||
|
||||
it('renders all parts of the article', () => {
|
||||
render(<PaperArticle {...props}>Article body content</PaperArticle>);
|
||||
|
||||
expect(screen.getByText('Breaking News')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local cat saves city')).toBeInTheDocument();
|
||||
expect(screen.getByText('Article body content')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reporting by John Doe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders separators only when appropriate', () => {
|
||||
const { rerender } = render(<PaperArticle headline="Title">Body</PaperArticle>);
|
||||
// Should have 1 separator (between title and body)
|
||||
expect(screen.getAllByRole('separator')).toHaveLength(1);
|
||||
|
||||
rerender(<PaperArticle headline="Title" subHeadline="Sub">Body</PaperArticle>);
|
||||
// Should have 2 separators (Title-Sub and Sub-Body)
|
||||
expect(screen.getAllByRole('separator')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { Container } from '../../generic/Container/Container';
|
||||
import { Header } from '../../generic/Header/Header';
|
||||
import { Text } from '../../generic/Text/Text';
|
||||
import { VStack } from '../../generic/VStack/VStack';
|
||||
import { PaperSeparator } from '../PaperSeparator/PaperSeparator';
|
||||
|
||||
import type { ContainerProps } from '../../generic/Container/Container';
|
||||
|
||||
/**
|
||||
* Props for the newspaper-style PaperArticle component.
|
||||
*/
|
||||
export interface PaperArticleProps {
|
||||
/** The main article headline */
|
||||
headline: string;
|
||||
/** Optional sub-headline displayed between separators */
|
||||
subHeadline?: string;
|
||||
/** Optional additional text (e.g., author) displayed at the bottom in gray italic label */
|
||||
additionalText?: string;
|
||||
/** Press interaction handler propagated to the wrapping Container */
|
||||
onPress?: ContainerProps['onPress'];
|
||||
/** The main article body content */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A newspaper-style article component with a structured layout.
|
||||
*
|
||||
* Features:
|
||||
* - Centered medium headline (headline-m)
|
||||
* - Single-line separators
|
||||
* - Optional centered italic sub-headline (label-m)
|
||||
* - Main body text (body-m)
|
||||
* - Optional bottom gray italic label (label-s)
|
||||
* - Interactive wrapper via Container
|
||||
*/
|
||||
export const PaperArticle = memo(function PaperArticle({
|
||||
headline,
|
||||
subHeadline,
|
||||
additionalText,
|
||||
onPress,
|
||||
children,
|
||||
}: PaperArticleProps) {
|
||||
return (
|
||||
<Container padding={4} onPress={onPress}>
|
||||
<VStack align="stretch" gap={4}>
|
||||
<Header className="text-center" level={4}>
|
||||
{headline}
|
||||
</Header>
|
||||
|
||||
<VStack align="center" gap={2}>
|
||||
{subHeadline ? (
|
||||
<>
|
||||
<PaperSeparator className="w-3/4" type="single" />
|
||||
<Text italic className="text-center" size="m" type="label">
|
||||
{subHeadline}
|
||||
</Text>
|
||||
<PaperSeparator className="w-3/4" type="single" />
|
||||
</>
|
||||
) : <PaperSeparator className="w-3/4" type="single" />}
|
||||
</VStack>
|
||||
|
||||
<Text size="m" type="body">
|
||||
{children}
|
||||
</Text>
|
||||
|
||||
{additionalText && (
|
||||
<VStack align='end'>
|
||||
<Text italic color="muted" size="s" type="label">
|
||||
{additionalText}
|
||||
</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</VStack>
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
export { PaperButton } from './PaperButton/PaperButton';
|
||||
|
||||
export { PaperSeparator } from './PaperSeparator/PaperSeparator'
|
||||
export * from './PaperButton/PaperButton';
|
||||
export * from './PaperSeparator/PaperSeparator';
|
||||
export * from './PaperArticle/PaperArticle';
|
||||
+177
-182
@@ -1,3 +1,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap');
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@@ -63,200 +64,188 @@
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-rainby-red-50: #fef4f2;
|
||||
--color-rainby-red-100: #ffe7e1;
|
||||
--color-rainby-red-200: #ffd2c8;
|
||||
--color-rainby-red-300: #ffb4a3;
|
||||
--color-rainby-red-400: #fd876c;
|
||||
--color-rainby-red-500: #f5603e;
|
||||
--color-rainby-red-600: #e24420;
|
||||
--color-rainby-red-700: #be3617;
|
||||
--color-rainby-red-800: #9d3017;
|
||||
--color-rainby-red-900: #822d1a;
|
||||
--color-rainby-red-950: #471408;
|
||||
--rb-color-red-50: #fef4f2;
|
||||
--rb-color-red-100: #ffe7e1;
|
||||
--rb-color-red-200: #ffd2c8;
|
||||
--rb-color-red-300: #ffb4a3;
|
||||
--rb-color-red-400: #fd876c;
|
||||
--rb-color-red-500: #f5603e;
|
||||
--rb-color-red-600: #e24420;
|
||||
--rb-color-red-700: #be3617;
|
||||
--rb-color-red-800: #9d3017;
|
||||
--rb-color-red-900: #822d1a;
|
||||
--rb-color-red-950: #471408;
|
||||
|
||||
--color-rainby-orange-50: #fff8ed;
|
||||
--color-rainby-orange-100: #ffefd5;
|
||||
--color-rainby-orange-200: #fedbaa;
|
||||
--color-rainby-orange-300: #fdc175;
|
||||
--color-rainby-orange-400: #fb9b3c;
|
||||
--color-rainby-orange-500: #f97e16;
|
||||
--color-rainby-orange-600: #ea630c;
|
||||
--color-rainby-orange-700: #c24a0c;
|
||||
--color-rainby-orange-800: #9a3b12;
|
||||
--color-rainby-orange-900: #7c3212;
|
||||
--color-rainby-orange-950: #431707;
|
||||
--rb-color-orange-50: #fff8ed;
|
||||
--rb-color-orange-100: #ffefd5;
|
||||
--rb-color-orange-200: #fedbaa;
|
||||
--rb-color-orange-300: #fdc175;
|
||||
--rb-color-orange-400: #fb9b3c;
|
||||
--rb-color-orange-500: #f97e16;
|
||||
--rb-color-orange-600: #ea630c;
|
||||
--rb-color-orange-700: #c24a0c;
|
||||
--rb-color-orange-800: #9a3b12;
|
||||
--rb-color-orange-900: #7c3212;
|
||||
--rb-color-orange-950: #431707;
|
||||
|
||||
--color-rainby-yellow-50: #fefde8;
|
||||
--color-rainby-yellow-100: #fefdc3;
|
||||
--color-rainby-yellow-200: #fef9a3;
|
||||
--color-rainby-yellow-300: #fdec47;
|
||||
--color-rainby-yellow-400: #f9db16;
|
||||
--color-rainby-yellow-500: #e9c209;
|
||||
--color-rainby-yellow-600: #ca9704;
|
||||
--color-rainby-yellow-700: #a16c07;
|
||||
--color-rainby-yellow-800: #85550e;
|
||||
--color-rainby-yellow-900: #714512;
|
||||
--color-rainby-yellow-950: #422406;
|
||||
--rb-color-yellow-50: #fefde8;
|
||||
--rb-color-yellow-100: #fefdc3;
|
||||
--rb-color-yellow-200: #fef9a3;
|
||||
--rb-color-yellow-300: #fdec47;
|
||||
--rb-color-yellow-400: #f9db16;
|
||||
--rb-color-yellow-500: #e9c209;
|
||||
--rb-color-yellow-600: #ca9704;
|
||||
--rb-color-yellow-700: #a16c07;
|
||||
--rb-color-yellow-800: #85550e;
|
||||
--rb-color-yellow-900: #714512;
|
||||
--rb-color-yellow-950: #422406;
|
||||
|
||||
--color-rainby-green-50: #f8faeb;
|
||||
--color-rainby-green-100: #edf4d3;
|
||||
--color-rainby-green-200: #ddebab;
|
||||
--color-rainby-green-300: #c4dc7a;
|
||||
--color-rainby-green-400: #b6d167;
|
||||
--color-rainby-green-500: #8daf33;
|
||||
--color-rainby-green-600: #6e8b25;
|
||||
--color-rainby-green-700: #546b20;
|
||||
--color-rainby-green-800: #44551f;
|
||||
--color-rainby-green-900: #3a491e;
|
||||
--color-rainby-green-950: #1e280b;
|
||||
--rb-color-green-50: #f8faeb;
|
||||
--rb-color-green-100: #edf4d3;
|
||||
--rb-color-green-200: #ddebab;
|
||||
--rb-color-green-300: #c4dc7a;
|
||||
--rb-color-green-400: #b6d167;
|
||||
--rb-color-green-500: #8daf33;
|
||||
--rb-color-green-600: #6e8b25;
|
||||
--rb-color-green-700: #546b20;
|
||||
--rb-color-green-800: #44551f;
|
||||
--rb-color-green-900: #3a491e;
|
||||
--rb-color-green-950: #1e280b;
|
||||
|
||||
--color-rainby-cyan-50: #effbfc;
|
||||
--color-rainby-cyan-100: #d7f2f6;
|
||||
--color-rainby-cyan-200: #b4e6ed;
|
||||
--color-rainby-cyan-300: #81d3e0;
|
||||
--color-rainby-cyan-400: #45b6cb;
|
||||
--color-rainby-cyan-500: #2a9ab0;
|
||||
--color-rainby-cyan-600: #257d95;
|
||||
--color-rainby-cyan-700: #24657a;
|
||||
--color-rainby-cyan-800: #265464;
|
||||
--color-rainby-cyan-900: #234756;
|
||||
--color-rainby-cyan-950: #122e3a;
|
||||
--rb-color-cyan-50: #effbfc;
|
||||
--rb-color-cyan-100: #d7f2f6;
|
||||
--rb-color-cyan-200: #b4e6ed;
|
||||
--rb-color-cyan-300: #81d3e0;
|
||||
--rb-color-cyan-400: #45b6cb;
|
||||
--rb-color-cyan-500: #2a9ab0;
|
||||
--rb-color-cyan-600: #257d95;
|
||||
--rb-color-cyan-700: #24657a;
|
||||
--rb-color-cyan-800: #265464;
|
||||
--rb-color-cyan-900: #234756;
|
||||
--rb-color-cyan-950: #122e3a;
|
||||
|
||||
--color-rainby-blue-50: #eff5ff;
|
||||
--color-rainby-blue-100: #dbe8fe;
|
||||
--color-rainby-blue-200: #bfd8fe;
|
||||
--color-rainby-blue-300: #a2c9fe;
|
||||
--color-rainby-blue-400: #5f9efb;
|
||||
--color-rainby-blue-500: #3a79f7;
|
||||
--color-rainby-blue-600: #2358ed;
|
||||
--color-rainby-blue-700: #1c44d9;
|
||||
--color-rainby-blue-800: #1d38b0;
|
||||
--color-rainby-blue-900: #1d358b;
|
||||
--color-rainby-blue-950: #172254;
|
||||
--rb-color-blue-50: #eff5ff;
|
||||
--rb-color-blue-100: #dbe8fe;
|
||||
--rb-color-blue-200: #bfd8fe;
|
||||
--rb-color-blue-300: #a2c9fe;
|
||||
--rb-color-blue-400: #5f9efb;
|
||||
--rb-color-blue-500: #3a79f7;
|
||||
--rb-color-blue-600: #2358ed;
|
||||
--rb-color-blue-700: #1c44d9;
|
||||
--rb-color-blue-800: #1d38b0;
|
||||
--rb-color-blue-900: #1d358b;
|
||||
--rb-color-blue-950: #172254;
|
||||
|
||||
--color-rainby-violet-50: #eef0ff;
|
||||
--color-rainby-violet-100: #dfe3ff;
|
||||
--color-rainby-violet-200: #bdc2ff;
|
||||
--color-rainby-violet-300: #a3a8fe;
|
||||
--color-rainby-violet-400: #837ffa;
|
||||
--color-rainby-violet-500: #6e60f4;
|
||||
--color-rainby-violet-600: #6043e8;
|
||||
--color-rainby-violet-700: #5435cd;
|
||||
--color-rainby-violet-800: #442ea5;
|
||||
--color-rainby-violet-900: #3b2c83;
|
||||
--color-rainby-violet-950: #231a4c;
|
||||
--rb-color-violet-50: #eef0ff;
|
||||
--rb-color-violet-100: #dfe3ff;
|
||||
--rb-color-violet-200: #bdc2ff;
|
||||
--rb-color-violet-300: #a3a8fe;
|
||||
--rb-color-violet-400: #837ffa;
|
||||
--rb-color-violet-500: #6e60f4;
|
||||
--rb-color-violet-600: #6043e8;
|
||||
--rb-color-violet-700: #5435cd;
|
||||
--rb-color-violet-800: #442ea5;
|
||||
--rb-color-violet-900: #3b2c83;
|
||||
--rb-color-violet-950: #231a4c;
|
||||
|
||||
--color-rainby-neutral-50: #f6f6f6;
|
||||
--color-rainby-neutral-100: #e7e7e7;
|
||||
--color-rainby-neutral-200: #d1d1d1;
|
||||
--color-rainby-neutral-300: #b0b0b0;
|
||||
--color-rainby-neutral-400: #888888;
|
||||
--color-rainby-neutral-500: #737373;
|
||||
--color-rainby-neutral-600: #5d5d5d;
|
||||
--color-rainby-neutral-700: #4f4f4f;
|
||||
--color-rainby-neutral-800: #454545;
|
||||
--color-rainby-neutral-900: #3d3d3d;
|
||||
--color-rainby-neutral-950: #262626;
|
||||
--rb-color-neutral-50: #f6f6f6;
|
||||
--rb-color-neutral-100: #e7e7e7;
|
||||
--rb-color-neutral-200: #d1d1d1;
|
||||
--rb-color-neutral-300: #b0b0b0;
|
||||
--rb-color-neutral-400: #888888;
|
||||
--rb-color-neutral-500: #737373;
|
||||
--rb-color-neutral-600: #5d5d5d;
|
||||
--rb-color-neutral-700: #4f4f4f;
|
||||
--rb-color-neutral-800: #454545;
|
||||
--rb-color-neutral-900: #3d3d3d;
|
||||
--rb-color-neutral-950: #262626;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Core palette */
|
||||
--background: var(--color-rainby-neutral-50);
|
||||
--border: var(--color-rainby-neutral-500);
|
||||
--input: var(--color-rainby-neutral-800);
|
||||
--foreground: var(--color-rainby-neutral-950);
|
||||
--ring: var(--color-rainby-cyan-700);
|
||||
--rb-base: var(--rb-color-neutral-50);
|
||||
--rb-outline: var(--rb-color-neutral-500);
|
||||
--rb-input: var(--rb-color-neutral-800);
|
||||
--rb-text: var(--rb-color-neutral-950);
|
||||
--rb-ring: var(--rb-color-cyan-700);
|
||||
|
||||
/* Surfaces */
|
||||
--card: var(--color-rainby-neutral-50);
|
||||
--card-foreground: var(--color-rainby-neutral-950);
|
||||
--popover: var(--color-rainby-neutral-50);
|
||||
--popover-foreground: var(--color-rainby-neutral-950);
|
||||
/* Container */
|
||||
--rb-container-base: var(--rb-color-neutral-50);
|
||||
--rb-container-text: var(--rb-color-neutral-950);
|
||||
|
||||
/* Semantic */
|
||||
--primary: var(--color-rainby-cyan-200);
|
||||
--primary-foreground: var(--color-rainby-cyan-950);
|
||||
--secondary: var(--color-rainby-green-200);
|
||||
--secondary-foreground: var(--color-rainby-green-950);
|
||||
--muted: var(--color-rainby-neutral-300);
|
||||
--muted-foreground: var(--color-rainby-neutral-600);
|
||||
--accent: var(--color-rainby-violet-200);
|
||||
--accent-foreground: var(--color-rainby-violet-950);
|
||||
--destructive: var(--color-rainby-red-200);
|
||||
--destructive-foreground: var(--color-rainby-red-950);
|
||||
--rb-primary-base: var(--rb-color-cyan-200);
|
||||
--rb-primary-text: var(--rb-color-cyan-950);
|
||||
--rb-secondary-base: var(--rb-color-green-200);
|
||||
--rb-secondary-text: var(--rb-color-green-950);
|
||||
--rb-tertiary-base: var(--rb-color-violet-200);
|
||||
--rb-tertiary-text: var(--rb-color-violet-950);
|
||||
--rb-error-base: var(--rb-color-red-200);
|
||||
--rb-error-text: var(--rb-color-red-950);
|
||||
--rb-muted-base: var(--rb-color-neutral-300);
|
||||
--rb-muted-text: var(--rb-color-neutral-600);
|
||||
|
||||
/* Charts (e.g. Recharts in ShadCN examples) */
|
||||
--chart-1: var(--color-rainby-blue-500);
|
||||
--chart-2: var(--color-rainby-green-500);
|
||||
--chart-3: var(--color-rainby-yellow-500);
|
||||
--chart-4: var(--color-rainby-orange-500);
|
||||
--chart-5: var(--color-rainby-red-500);
|
||||
/* Charts */
|
||||
--chart-1: var(--rb-color-blue-500);
|
||||
--chart-2: var(--rb-color-green-500);
|
||||
--chart-3: var(--rb-color-yellow-500);
|
||||
--chart-4: var(--rb-color-orange-500);
|
||||
--chart-5: var(--rb-color-red-500);
|
||||
|
||||
/* Sidebar (if you use ShadCN’s sidebar components) */
|
||||
--sidebar: var(--color-rainby-neutral-50);
|
||||
--sidebar-foreground: var(--color-rainby-neutral-950);
|
||||
--sidebar-primary: var(--color-rainby-cyan-200);
|
||||
--sidebar-primary-foreground: var(--color-rainby-cyan-950);
|
||||
--sidebar-accent: var(--color-rainby-violet-200);
|
||||
--sidebar-accent-foreground: var(--color-rainby-violet-950);
|
||||
--sidebar-border: var(--color-rainby-neutral-500);
|
||||
--sidebar-ring: var(--color-rainby-cyan-700);
|
||||
/* Sidebar */
|
||||
--sidebar: var(--rb-color-neutral-50);
|
||||
--sidebar-foreground: var(--rb-color-neutral-950);
|
||||
--sidebar-primary: var(--rb-color-cyan-200);
|
||||
--sidebar-primary-foreground: var(--rb-color-cyan-950);
|
||||
--sidebar-accent: var(--rb-color-violet-200);
|
||||
--sidebar-accent-foreground: var(--rb-color-violet-950);
|
||||
--sidebar-border: var(--rb-color-neutral-500);
|
||||
--sidebar-ring: var(--rb-color-cyan-700);
|
||||
|
||||
/* Radii */
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
/* ──────────────── Dark Mode Override ──────────────── */
|
||||
.dark {
|
||||
/* Core palette flips */
|
||||
--background: var(--color-rainby-neutral-950);
|
||||
--ring: var(--color-rainby-cyan-300);
|
||||
--border: var(--color-rainby-neutral-500);
|
||||
--input: var(--color-rainby-neutral-300);
|
||||
--foreground: var(--color-rainby-neutral-50);
|
||||
--rb-base: var(--rb-color-neutral-950);
|
||||
--rb-text: var(--rb-color-neutral-50);
|
||||
--rb-outline: var(--rb-color-neutral-500);
|
||||
--rb-input: var(--rb-color-neutral-300);
|
||||
--rb-ring: var(--rb-color-cyan-300);
|
||||
|
||||
/* Surfaces */
|
||||
--card: var(--color-rainby-neutral-950);
|
||||
--card-foreground: var(--color-rainby-neutral-50);
|
||||
--popover: var(--color-rainby-neutral-950);
|
||||
--popover-foreground: var(--color-rainby-neutral-50);
|
||||
/* Container */
|
||||
--rb-container-base: var(--rb-color-neutral-950);
|
||||
--rb-container-text: var(--rb-color-neutral-50);
|
||||
|
||||
/* Semantic */
|
||||
--primary: var(--color-rainby-cyan-800);
|
||||
--primary-foreground: var(--color-rainby-cyan-50);
|
||||
--secondary: var(--color-rainby-green-800);
|
||||
--secondary-foreground: var(--color-rainby-green-50);
|
||||
--muted: var(--color-rainby-neutral-600);
|
||||
--muted-foreground: var(--color-rainby-neutral-100);
|
||||
--accent: var(--color-rainby-violet-800);
|
||||
--accent-foreground: var(--color-rainby-violet-50);
|
||||
--destructive: var(--color-rainby-red-800);
|
||||
--destructive-foreground: var(--color-rainby-red-50);
|
||||
--rb-primary-base: var(--rb-color-cyan-800);
|
||||
--rb-primary-text: var(--rb-color-cyan-50);
|
||||
--rb-secondary-base: var(--rb-color-green-800);
|
||||
--rb-secondary-text: var(--rb-color-green-50);
|
||||
--rb-tertiary-base: var(--rb-color-violet-800);
|
||||
--rb-tertiary-text: var(--rb-color-violet-50);
|
||||
--rb-error-base: var(--rb-color-red-800);
|
||||
--rb-error-text: var(--rb-color-red-50);
|
||||
--rb-muted-base: var(--rb-color-neutral-600);
|
||||
--rb-muted-text: var(--rb-color-neutral-100);
|
||||
|
||||
/* Charts */
|
||||
--chart-1: var(--color-rainby-blue-400);
|
||||
--chart-2: var(--color-rainby-green-400);
|
||||
--chart-3: var(--color-rainby-yellow-400);
|
||||
--chart-4: var(--color-rainby-orange-400);
|
||||
--chart-5: var(--color-rainby-red-400);
|
||||
--chart-1: var(--rb-color-blue-400);
|
||||
--chart-2: var(--rb-color-green-400);
|
||||
--chart-3: var(--rb-color-yellow-400);
|
||||
--chart-4: var(--rb-color-orange-400);
|
||||
--chart-5: var(--rb-color-red-400);
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: var(--color-rainby-neutral-950);
|
||||
--sidebar-foreground: var(--color-rainby-neutral-50);
|
||||
--sidebar-primary: var(--color-rainby-cyan-800);
|
||||
--sidebar-primary-foreground: var(--color-rainby-cyan-50);
|
||||
--sidebar-accent: var(--color-rainby-violet-800);
|
||||
--sidebar-accent-foreground: var(--color-rainby-violet-50);
|
||||
--sidebar-border: var(--color-rainby-neutral-500);
|
||||
--sidebar-ring: var(--color-rainby-cyan-700);
|
||||
|
||||
/* Radii stays same */
|
||||
--radius: 0.625rem;
|
||||
}
|
||||
|
||||
@utility frosted-glass {
|
||||
@apply bg-background/30 rounded-lg shadow-lg backdrop-blur-md backdrop-opacity-75 backdrop-filter;
|
||||
--sidebar: var(--rb-color-neutral-950);
|
||||
--sidebar-foreground: var(--rb-color-neutral-50);
|
||||
--sidebar-primary: var(--rb-color-cyan-800);
|
||||
--sidebar-primary-foreground: var(--rb-color-cyan-50);
|
||||
--sidebar-accent: var(--rb-color-violet-800);
|
||||
--sidebar-accent-foreground: var(--rb-color-violet-50);
|
||||
--sidebar-border: var(--rb-color-neutral-500);
|
||||
--sidebar-ring: var(--rb-color-cyan-700);
|
||||
}
|
||||
|
||||
@utility vstack {
|
||||
@@ -272,29 +261,35 @@
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
||||
--color-background: var(--rb-base);
|
||||
--color-foreground: var(--rb-text);
|
||||
--color-card: var(--rb-container-base);
|
||||
--color-card-foreground: var(--rb-container-text);
|
||||
--color-popover: var(--rb-container-base);
|
||||
--color-popover-foreground: var(--rb-container-text);
|
||||
|
||||
--color-primary: var(--rb-primary-base);
|
||||
--color-primary-foreground: var(--rb-primary-text);
|
||||
--color-secondary: var(--rb-secondary-base);
|
||||
--color-secondary-foreground: var(--rb-secondary-text);
|
||||
--color-accent: var(--rb-tertiary-base);
|
||||
--color-accent-foreground: var(--rb-tertiary-text);
|
||||
--color-destructive: var(--rb-error-base);
|
||||
--color-destructive-foreground: var(--rb-error-text);
|
||||
|
||||
--color-muted: var(--rb-muted-base);
|
||||
--color-muted-foreground: var(--rb-muted-text);
|
||||
--color-border: var(--rb-outline);
|
||||
--color-input: var(--rb-input);
|
||||
--color-ring: var(--rb-ring);
|
||||
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { createLibConfig } from '@ermnvldmr/vite-config/lib';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
|
||||
export default createLibConfig('ErmnvldmrUI', 'src/index.ts', ['@ermnvldmr/stl'], {
|
||||
const baseConfig = createLibConfig('ErmnvldmrUI', 'src/index.ts', ['@ermnvldmr/stl'], {
|
||||
dts: {
|
||||
exclude: ['**/*.test.ts', '**/*.test.tsx', '**/*.stories.tsx', 'src/setupTests.ts', 'src/test-utils.tsx'],
|
||||
},
|
||||
});
|
||||
|
||||
export default mergeConfig(baseConfig, {
|
||||
plugins: [tailwindcss()],
|
||||
});
|
||||
Generated
+44
-24
@@ -83,13 +83,13 @@ importers:
|
||||
version: 30.0.0
|
||||
jest:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
jest-environment-jsdom:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0
|
||||
ts-jest:
|
||||
specifier: ^29.4.6
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)))(typescript@5.8.3)
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
@@ -116,12 +116,24 @@ importers:
|
||||
'@ermnvldmr/vite-config':
|
||||
specifier: workspace:*
|
||||
version: link:../vite-config
|
||||
'@types/react':
|
||||
specifier: ^19.1.2
|
||||
version: 19.2.9
|
||||
'@types/react-dom':
|
||||
specifier: ^19.1.2
|
||||
version: 19.2.3(@types/react@19.2.9)
|
||||
jest:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.3
|
||||
react-dom:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.3(react@19.2.3)
|
||||
ts-jest:
|
||||
specifier: ^29.4.6
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)))(typescript@5.8.3)
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@@ -279,7 +291,7 @@ importers:
|
||||
version: 14.1.1
|
||||
jest:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
jest-environment-jsdom:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0
|
||||
@@ -291,7 +303,7 @@ importers:
|
||||
version: 8.6.15(prettier@3.5.3)
|
||||
ts-jest:
|
||||
specifier: ^29.4.6
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)))(typescript@5.8.3)
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@@ -356,7 +368,7 @@ importers:
|
||||
version: link:../../packages/eslint-config
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
version: 4.1.18(vite@6.4.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))
|
||||
eslint:
|
||||
specifier: ^9.25.1
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
@@ -480,7 +492,7 @@ importers:
|
||||
version: 14.1.1
|
||||
jest:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
version: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
jest-environment-jsdom:
|
||||
specifier: ^30.2.0
|
||||
version: 30.2.0
|
||||
@@ -501,7 +513,7 @@ importers:
|
||||
version: 8.6.15(prettier@3.5.3)
|
||||
ts-jest:
|
||||
specifier: ^29.4.6
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.25.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.25.12))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3)
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)
|
||||
@@ -8605,7 +8617,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))':
|
||||
'@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))':
|
||||
dependencies:
|
||||
'@jest/console': 30.2.0
|
||||
'@jest/pattern': 30.0.1
|
||||
@@ -8620,7 +8632,7 @@ snapshots:
|
||||
exit-x: 0.2.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 30.2.0
|
||||
jest-config: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
jest-config: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
jest-haste-map: 30.2.0
|
||||
jest-message-util: 30.2.0
|
||||
jest-regex-util: 30.0.1
|
||||
@@ -10754,10 +10766,10 @@ snapshots:
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-junit: 16.0.0
|
||||
jest-playwright-preset: 4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.0.10))
|
||||
jest-playwright-preset: 4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))
|
||||
jest-runner: 29.7.0
|
||||
jest-serializer-html: 7.1.0
|
||||
jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@25.0.10))
|
||||
jest-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))
|
||||
nyc: 15.1.0
|
||||
playwright: 1.58.0
|
||||
transitivePeerDependencies:
|
||||
@@ -10910,6 +10922,13 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.18
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.18
|
||||
|
||||
'@tailwindcss/vite@4.1.18(vite@6.4.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.18
|
||||
'@tailwindcss/oxide': 4.1.18
|
||||
tailwindcss: 4.1.18
|
||||
vite: 6.4.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)
|
||||
|
||||
'@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.18
|
||||
@@ -13672,15 +13691,15 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)):
|
||||
jest-cli@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
'@jest/test-result': 30.2.0
|
||||
'@jest/types': 30.2.0
|
||||
chalk: 4.1.2
|
||||
exit-x: 0.2.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
jest-config: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
jest-util: 30.2.0
|
||||
jest-validate: 30.2.0
|
||||
yargs: 17.7.2
|
||||
@@ -13756,7 +13775,7 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)):
|
||||
jest-config@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@jest/get-type': 30.1.0
|
||||
@@ -13785,6 +13804,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.10
|
||||
esbuild-register: 3.6.0(esbuild@0.27.2)
|
||||
ts-node: 10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
@@ -13958,7 +13978,7 @@ snapshots:
|
||||
'@types/node': 25.0.10
|
||||
jest-util: 30.2.0
|
||||
|
||||
jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.0.10)):
|
||||
jest-playwright-preset@4.0.0(jest-circus@29.7.0)(jest-environment-node@29.7.0)(jest-runner@29.7.0)(jest@29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))):
|
||||
dependencies:
|
||||
expect-playwright: 0.8.0
|
||||
jest: 29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
@@ -14237,7 +14257,7 @@ snapshots:
|
||||
leven: 3.1.0
|
||||
pretty-format: 30.2.0
|
||||
|
||||
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@25.0.10)):
|
||||
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@25.0.10)(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))):
|
||||
dependencies:
|
||||
ansi-escapes: 6.2.1
|
||||
chalk: 5.6.2
|
||||
@@ -14310,12 +14330,12 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)):
|
||||
jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
'@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
'@jest/types': 30.2.0
|
||||
import-local: 3.2.0
|
||||
jest-cli: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
jest-cli: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
@@ -16356,12 +16376,12 @@ snapshots:
|
||||
esbuild: 0.25.12
|
||||
jest-util: 30.2.0
|
||||
|
||||
ts-jest@29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2)))(typescript@5.8.3):
|
||||
ts-jest@29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(esbuild@0.27.2)(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3)))(typescript@5.8.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.8
|
||||
jest: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))
|
||||
jest: 30.2.0(@types/node@25.0.10)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.15.10(@swc/helpers@0.5.18))(@types/node@25.0.10)(typescript@5.8.3))
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
|
||||
@@ -42,13 +42,6 @@ const { title, description } = Astro.props;
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href="https://www.ermnvldmr.com/" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
Reference in new issue
Block a user