Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20e34a37c | ||
|
|
208f6406e6
|
||
|
|
82236be7d6
|
||
|
|
7ac7c1b405
|
||
|
|
77fa576ac6
|
No files matched your search
@@ -0,0 +1,112 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Accordion } from './Accordion';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof Accordion> = {
|
||||
title: 'Components/Accordion',
|
||||
component: Accordion,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['single', 'multiple'],
|
||||
},
|
||||
collapsible: {
|
||||
control: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Accordion>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
type: 'single',
|
||||
collapsible: true,
|
||||
},
|
||||
render: (args) => (
|
||||
<Accordion {...args}>
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger level={2}>Is it accessible?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
Yes. It adheres to the WAI-ARIA design pattern.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger level={2}>Is it styled?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
Yes. It comes with default styles that matches the design system.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-3">
|
||||
<Accordion.Trigger level={2}>Is it animated?</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
Yes. It uses CSS Grid transitions for smooth height animation.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
),
|
||||
};
|
||||
|
||||
export const Multiple: Story = {
|
||||
args: {
|
||||
type: 'multiple',
|
||||
},
|
||||
render: (args) => (
|
||||
<Accordion {...args}>
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger level={3}>Section One</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
This section can be open alongside others.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger level={3}>Section Two</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
You can open as many sections as you want in multiple mode.
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
),
|
||||
};
|
||||
|
||||
export const DifferentHeaderLevels: Story = {
|
||||
render: () => (
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="h1">
|
||||
<Accordion.Trigger level={1}>Header Level 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content for H1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="h2">
|
||||
<Accordion.Trigger level={2}>Header Level 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content for H2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="h3">
|
||||
<Accordion.Trigger level={3}>Header Level 3</Accordion.Trigger>
|
||||
<Accordion.Content>Content for H3</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="h4">
|
||||
<Accordion.Trigger level={4}>Header Level 4</Accordion.Trigger>
|
||||
<Accordion.Content>Content for H4</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
),
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger level={3}>Enabled Item</Accordion.Trigger>
|
||||
<Accordion.Content>This one works fine.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item disabled value="item-2">
|
||||
<Accordion.Trigger level={3}>Disabled Item</Accordion.Trigger>
|
||||
<Accordion.Content>You should not see this.</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,221 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
import { Accordion } from './Accordion';
|
||||
|
||||
describe('Accordion', () => {
|
||||
it('renders correctly with all sub-components', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger level={2}>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Trigger 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Content 1')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
it('toggles content on click in single mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Trigger 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content 2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const trigger1 = screen.getByText('Trigger 1');
|
||||
const trigger2 = screen.getByText('Trigger 2');
|
||||
|
||||
await user.click(trigger1);
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByRole('button', { name: /Trigger 2/i })).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
await user.click(trigger2);
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.getByRole('button', { name: /Trigger 2/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('allows multiple items to be open in multiple mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Accordion type="multiple">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Trigger 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content 2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Trigger 1'));
|
||||
await user.click(screen.getByText('Trigger 2'));
|
||||
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
expect(screen.getByRole('button', { name: /Trigger 2/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('respects the collapsible prop in single mode', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Accordion collapsible={false} type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const trigger = screen.getByText('Trigger 1');
|
||||
|
||||
// Open it
|
||||
await user.click(trigger);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
// Try to close it (should stay open because collapsible={false})
|
||||
await user.click(trigger);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('does not toggle when disabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item disabled value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const trigger = screen.getByText('Trigger 1');
|
||||
await user.click(trigger);
|
||||
expect(screen.getByRole('button')).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('works as a controlled component', async () => {
|
||||
const user = userEvent.setup();
|
||||
const TestComponent = () => {
|
||||
const [value, setValue] = React.useState<string[]>(['item-1']);
|
||||
return (
|
||||
<Accordion type="single" value={value} onValueChange={setValue}>
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Trigger 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content 2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
|
||||
await user.click(screen.getByText('Trigger 2'));
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.getByRole('button', { name: /Trigger 2/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('works as an uncontrolled component with defaultValue', () => {
|
||||
render(
|
||||
<Accordion defaultValue={['item-2']} type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value="item-2">
|
||||
<Accordion.Trigger>Trigger 2</Accordion.Trigger>
|
||||
<Accordion.Content>Content 2</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: /Trigger 1/i })).toHaveAttribute('aria-expanded', 'false');
|
||||
expect(screen.getByRole('button', { name: /Trigger 2/i })).toHaveAttribute('aria-expanded', 'true');
|
||||
});
|
||||
|
||||
it('renders with valid HTML structure (heading containing button)', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger level={3}>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const heading = screen.getByRole('heading', { level: 3 });
|
||||
const button = screen.getByRole('button');
|
||||
expect(heading).toContainElement(button);
|
||||
});
|
||||
|
||||
it('applies accessibility attributes to content when closed', () => {
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
const content = screen.getByText('Content 1').closest('[role="region"]');
|
||||
expect(content).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(content).toHaveAttribute('inert');
|
||||
});
|
||||
|
||||
it('removes accessibility hiding attributes when open', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Accordion type="single">
|
||||
<Accordion.Item value="item-1">
|
||||
<Accordion.Trigger>Trigger 1</Accordion.Trigger>
|
||||
<Accordion.Content>Content 1</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText('Trigger 1'));
|
||||
const content = screen.getByText('Content 1').closest('[role="region"]');
|
||||
expect(content).toHaveAttribute('aria-hidden', 'false');
|
||||
expect(content).not.toHaveAttribute('inert');
|
||||
});
|
||||
|
||||
it('throws error when sub-components are used outside of Accordion', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
expect(() => render(<Accordion.Trigger>Trigger</Accordion.Trigger>)).toThrow(
|
||||
'Accordion sub-components must be used within an Accordion component.'
|
||||
);
|
||||
|
||||
expect(() => render(<Accordion.Item value="1">Item</Accordion.Item>)).toThrow(
|
||||
'Accordion sub-components must be used within an Accordion component.'
|
||||
);
|
||||
|
||||
expect(() => render(<Accordion.Content>Content</Accordion.Content>)).toThrow(
|
||||
'AccordionItem sub-components must be used within an AccordionItem component.'
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
import { AccordionContent } from './AccordionContent/AccordionContent';
|
||||
import { AccordionItem } from './AccordionItem/AccordionItem';
|
||||
import { AccordionTrigger } from './AccordionTrigger/AccordionTrigger';
|
||||
import { AccordionContext } from './contexts/AccordionContext/AccordionContext';
|
||||
|
||||
import type { AccordionProps } from './types';
|
||||
|
||||
/**
|
||||
* An Accordion component that allows users to reveal and hide content sections.
|
||||
*
|
||||
* Built with a compound component pattern for maximum flexibility.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
const AccordionRoot = ({
|
||||
type = 'single',
|
||||
value: controlledValue,
|
||||
defaultValue = [],
|
||||
onValueChange,
|
||||
collapsible = true,
|
||||
children,
|
||||
className,
|
||||
'data-testid': testId,
|
||||
}: AccordionProps) => {
|
||||
const [internalValue, setInternalValue] = useState<string[]>(defaultValue);
|
||||
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const currentValues = isControlled ? controlledValue : internalValue;
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(itemValue: string) => {
|
||||
let nextValues: string[];
|
||||
|
||||
if (type === 'single') {
|
||||
const isCurrentOpen = currentValues.includes(itemValue);
|
||||
if (isCurrentOpen && collapsible) {
|
||||
nextValues = [];
|
||||
} else if (isCurrentOpen && !collapsible) {
|
||||
nextValues = [itemValue];
|
||||
} else {
|
||||
nextValues = [itemValue];
|
||||
}
|
||||
} else {
|
||||
nextValues = currentValues.includes(itemValue)
|
||||
? currentValues.filter((v) => v !== itemValue)
|
||||
: [...currentValues, itemValue];
|
||||
}
|
||||
|
||||
if (!isControlled) {
|
||||
setInternalValue(nextValues);
|
||||
}
|
||||
onValueChange?.(nextValues);
|
||||
},
|
||||
[type, currentValues, collapsible, isControlled, onValueChange]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
type,
|
||||
value: currentValues,
|
||||
onValueChange: handleValueChange,
|
||||
collapsible,
|
||||
}),
|
||||
[type, currentValues, handleValueChange, collapsible]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={contextValue}>
|
||||
<div className={cn('w-full', className)} data-testid={testId}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A collapsible accordion component system.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <Accordion type="single" collapsible>
|
||||
* <Accordion.Item value="item-1">
|
||||
* <Accordion.Trigger level={2}>Is it accessible?</Accordion.Trigger>
|
||||
* <Accordion.Content>Yes. It adheres to the WAI-ARIA design pattern.</Accordion.Content>
|
||||
* </Accordion.Item>
|
||||
* </Accordion>
|
||||
* ```
|
||||
*/
|
||||
export const Accordion = Object.assign(AccordionRoot, {
|
||||
Item: AccordionItem,
|
||||
Trigger: AccordionTrigger,
|
||||
Content: AccordionContent,
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React from 'react';
|
||||
|
||||
import { useAccordionItemContext } from '../contexts/AccordionItemContext/AccordionItemContext';
|
||||
|
||||
import type { AccordionContentProps } from '../types';
|
||||
|
||||
/**
|
||||
* The collapsible content of an Accordion item.
|
||||
*
|
||||
* Uses a CSS Grid transition for smooth height animation.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const AccordionContent = ({ children, className }: AccordionContentProps) => {
|
||||
const { value, isOpen } = useAccordionItemContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden={!isOpen}
|
||||
aria-labelledby={`accordion-trigger-${value}`}
|
||||
className={cn(
|
||||
'grid transition-[grid-template-rows] duration-200 ease-out',
|
||||
isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
||||
className
|
||||
)}
|
||||
id={`accordion-content-${value}`}
|
||||
inert={!isOpen ? true : undefined}
|
||||
role="region"
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="pb-4 pt-0 text-sm leading-relaxed">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { useAccordionContext } from '../contexts/AccordionContext/AccordionContext';
|
||||
import { AccordionItemContext } from '../contexts/AccordionItemContext/AccordionItemContext';
|
||||
|
||||
import type { AccordionItemProps } from '../types';
|
||||
|
||||
/**
|
||||
* An individual item within an Accordion.
|
||||
*
|
||||
* Provides context for the Trigger and Content sub-components.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const AccordionItem = ({
|
||||
value,
|
||||
children,
|
||||
disabled = false,
|
||||
className,
|
||||
}: AccordionItemProps) => {
|
||||
const { value: openValues } = useAccordionContext();
|
||||
const isOpen = openValues.includes(value);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
value,
|
||||
isOpen,
|
||||
disabled,
|
||||
}),
|
||||
[value, isOpen, disabled]
|
||||
);
|
||||
|
||||
return (
|
||||
<AccordionItemContext.Provider value={contextValue}>
|
||||
<div className={cn('border-b border-[var(--rb-outline)]/40', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionItemContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useButton } from 'react-aria';
|
||||
|
||||
import { Header } from '../../Header/Header';
|
||||
import { HStack } from '../../HStack/HStack';
|
||||
import { useAccordionContext } from '../contexts/AccordionContext/AccordionContext';
|
||||
import { useAccordionItemContext } from '../contexts/AccordionItemContext/AccordionItemContext';
|
||||
|
||||
import type { AccordionTriggerProps } from '../types';
|
||||
|
||||
/**
|
||||
* The interactive trigger for an Accordion item.
|
||||
*
|
||||
* Wraps the Header component and adds an expansion arrow.
|
||||
* @example Basic usage:
|
||||
* ```tsx
|
||||
* <Accordion.Trigger>Click me</Accordion.Trigger>
|
||||
* ```
|
||||
*/
|
||||
export const AccordionTrigger = ({
|
||||
children,
|
||||
className,
|
||||
level = 3,
|
||||
...headerProps
|
||||
}: AccordionTriggerProps) => {
|
||||
const { onValueChange } = useAccordionContext();
|
||||
const { value, isOpen, disabled: isDisabled } = useAccordionItemContext();
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!isDisabled) {
|
||||
onValueChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const { buttonProps } = useButton(
|
||||
{
|
||||
onPress: handleToggle,
|
||||
isDisabled,
|
||||
'aria-controls': `accordion-content-${value}`,
|
||||
'aria-expanded': isOpen,
|
||||
id: `accordion-trigger-${value}`,
|
||||
type: 'button',
|
||||
},
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<Header level={level} {...headerProps}>
|
||||
<button
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'group flex w-full items-center justify-between py-4 text-left transition-all outline-none focus-visible:ring-2 focus-visible:ring-[var(--rb-ring)] focus-visible:ring-offset-2',
|
||||
isDisabled && 'cursor-not-allowed opacity-50',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HStack align="center" className="w-full" gap={2}>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'size-5 shrink-0 transition-transform duration-200 text-[var(--rb-outline)]',
|
||||
isOpen && 'rotate-90'
|
||||
)}
|
||||
/>
|
||||
<span className="flex-1">{children}</span>
|
||||
</HStack>
|
||||
</button>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { AccordionContextValue } from '../../types';
|
||||
|
||||
/**
|
||||
* Context for sharing accordion state with items.
|
||||
*/
|
||||
export const AccordionContext = createContext<AccordionContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Hook to access the Accordion context.
|
||||
* @example Using the context:
|
||||
*/
|
||||
export function useAccordionContext(): AccordionContextValue {
|
||||
const context = useContext(AccordionContext);
|
||||
if (!context) {
|
||||
throw new Error('Accordion sub-components must be used within an Accordion component.');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import type { AccordionItemContextValue } from '../../types';
|
||||
|
||||
/**
|
||||
* Context for sharing specific accordion item state.
|
||||
*/
|
||||
export const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Hook to access the AccordionItem context.
|
||||
* @example Using the context:
|
||||
*/
|
||||
export function useAccordionItemContext(): AccordionItemContextValue {
|
||||
const context = useContext(AccordionItemContext);
|
||||
if (!context) {
|
||||
throw new Error('AccordionItem sub-components must be used within an AccordionItem component.');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './Accordion';
|
||||
export * from './types';
|
||||
export * from './contexts/AccordionContext/AccordionContext';
|
||||
export * from './contexts/AccordionItemContext/AccordionItemContext';
|
||||
|
||||
// Export sub-components individually for flexibility
|
||||
export * from './AccordionItem/AccordionItem';
|
||||
export * from './AccordionTrigger/AccordionTrigger';
|
||||
export * from './AccordionContent/AccordionContent';
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { HeaderProps } from '../Header/Header';
|
||||
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
|
||||
/**
|
||||
* Type of accordion behavior.
|
||||
* - 'single': Only one item can be open at a time.
|
||||
* - 'multiple': Multiple items can be open at a time.
|
||||
*/
|
||||
export type AccordionType = 'single' | 'multiple';
|
||||
|
||||
/**
|
||||
* Context value for the root Accordion component.
|
||||
*/
|
||||
export interface AccordionContextValue {
|
||||
/** Type of accordion behavior */
|
||||
type: AccordionType;
|
||||
/** Currently open values */
|
||||
value: string[];
|
||||
/** Callback when open values change */
|
||||
onValueChange: (value: string) => void;
|
||||
/** Whether the accordion is collapsible in single mode */
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context value for an individual Accordion item.
|
||||
*/
|
||||
export interface AccordionItemContextValue {
|
||||
/** Unique value for this item */
|
||||
value: string;
|
||||
/** Whether this item is currently open */
|
||||
isOpen: boolean;
|
||||
/** Whether this item is disabled */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the root Accordion component.
|
||||
*/
|
||||
export interface AccordionProps extends ClassNameProps, TestIdProps {
|
||||
/** Content of the accordion */
|
||||
children: ReactNode;
|
||||
/** Type of accordion behavior */
|
||||
type?: AccordionType;
|
||||
/** Controlled value of open items */
|
||||
value?: string[];
|
||||
/** Default value of open items (uncontrolled) */
|
||||
defaultValue?: string[];
|
||||
/** Callback when open values change */
|
||||
onValueChange?: (value: string[]) => void;
|
||||
/** Whether the accordion is collapsible in single mode */
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the AccordionItem component.
|
||||
*/
|
||||
export interface AccordionItemProps extends ClassNameProps {
|
||||
/** Unique value for this item */
|
||||
value: string;
|
||||
/** Content of the item (Trigger and Content) */
|
||||
children: ReactNode;
|
||||
/** Whether the item is disabled */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the AccordionTrigger component.
|
||||
*/
|
||||
export interface AccordionTriggerProps extends ClassNameProps, Omit<HeaderProps, 'children' | 'level'> {
|
||||
/** Content of the trigger (usually text) */
|
||||
children: ReactNode;
|
||||
/**
|
||||
* The semantic header level (1-6).
|
||||
* @default 3
|
||||
*/
|
||||
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the AccordionContent component.
|
||||
*/
|
||||
export interface AccordionContentProps extends ClassNameProps {
|
||||
/** Content to be revealed */
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import { Star, Code, Layout, Send, Zap, Shield, Cpu, Gamepad2, Plane, BookOpen, Check, Gift } from 'lucide-react';
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGrid } from './BentoGrid';
|
||||
import { Header } from '../Header/Header';
|
||||
import { Text } from '../Text/Text';
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
const meta: Meta<typeof BentoGrid> = {
|
||||
title: 'Components/BentoGrid',
|
||||
component: BentoGrid,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof BentoGrid>;
|
||||
|
||||
export const Showcase: Story = {
|
||||
render: () => (
|
||||
<div className="w-full max-w-6xl p-4">
|
||||
<BentoGrid>
|
||||
<BentoGrid.ImageCard
|
||||
alt="Developer workstation"
|
||||
colSpan={2}
|
||||
overlayDescription="A deep dive into modern web architecture and performance optimization."
|
||||
overlayTitle="Featured Project"
|
||||
rowSpan={2}
|
||||
src="https://images.unsplash.com/photo-1498050108023-c5249f4df085?auto=format&fit=crop&q=80&w=2072"
|
||||
/>
|
||||
<BentoGrid.InfoCard
|
||||
description="Built on top of Rsbuild and Rspack for near-instant compilation and ultra-fast HMR."
|
||||
icon={<Zap className="text-rb-orange-500" size={24} />}
|
||||
title="Lightning Fast"
|
||||
/>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="Get Started"
|
||||
description="Connect with other developers and share your projects."
|
||||
href="#"
|
||||
title="Join the Community"
|
||||
variant="primary"
|
||||
/>
|
||||
<BentoGrid.InfoCard
|
||||
description="Strict TypeScript integration across the entire monorepo for maximum reliability."
|
||||
footer={<div className="text-xs opacity-50">Strict Mode Enabled</div>}
|
||||
icon={<Shield className="text-rb-green-500" size={24} />}
|
||||
title="Type Safe"
|
||||
/>
|
||||
<BentoGrid.ImageCard
|
||||
alt="Code on screen"
|
||||
colSpan={2}
|
||||
overlayTitle="Clean Code"
|
||||
src="https://images.unsplash.com/photo-1461749280684-dccba630e2f6?auto=format&fit=crop&q=80&w=2069"
|
||||
/>
|
||||
<BentoGrid.InfoCard
|
||||
description="Easily extend and customize the design system with our modular component library."
|
||||
icon={<Cpu className="text-rb-blue-500" size={24} />}
|
||||
title="Modular Architecture"
|
||||
/>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="View Docs"
|
||||
description="Read the full documentation to learn more about the design system."
|
||||
href="#"
|
||||
title="Documentation"
|
||||
/>
|
||||
<BentoGrid.BaseCard className="bg-rb-tertiary-base/10 border-dashed" colSpan={4}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 bg-rb-tertiary-base rounded-full text-rb-tertiary-text">
|
||||
<Send size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-bold text-rb-text">Subscribe to Newsletter</h4>
|
||||
<p className="text-sm text-rb-muted-text">Stay updated with the latest releases and technical articles.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="px-6 py-2 bg-rb-text text-rb-base rounded-md font-medium">Subscribe</button>
|
||||
</div>
|
||||
</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const LayoutPatterns: Story = {
|
||||
render: () => (
|
||||
<div className="w-full max-w-6xl p-4 flex flex-col gap-12">
|
||||
<section>
|
||||
<h3 className="mb-4 text-xl font-bold">Standard 4-Column Grid</h3>
|
||||
<BentoGrid>
|
||||
<BentoGrid.BaseCard className="h-32 flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="h-32 flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="h-32 flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="h-32 flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-4 text-xl font-bold">Mixed Spans (Hero Pattern)</h3>
|
||||
<BentoGrid>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-primary-base/20 border-rb-primary-base" colSpan={2} rowSpan={2}>2x2 Hero</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100" colSpan={2}>2x1 Wide</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 className="mb-4 text-xl font-bold">Tall & Wide Pattern</h3>
|
||||
<BentoGrid>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-secondary-base/20 border-rb-secondary-base" colSpan={2}>2x1 Wide</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-tertiary-base/20 border-rb-tertiary-base" rowSpan={2}>1x2 Tall</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100">1x1</BentoGrid.BaseCard>
|
||||
<BentoGrid.BaseCard className="flex items-center justify-center bg-rb-color-neutral-100" colSpan={2}>2x1</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
</section>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const AllCardTypes: Story = {
|
||||
render: () => (
|
||||
<div className="w-full max-w-6xl p-4">
|
||||
<BentoGrid>
|
||||
<BentoGrid.InfoCard
|
||||
description="Used for features or information. Can have an icon and a footer."
|
||||
footer={<div className="text-xs font-mono">v1.0.0</div>}
|
||||
icon={<Layout size={24} />}
|
||||
title="Info Card"
|
||||
/>
|
||||
<BentoGrid.ImageCard
|
||||
alt="Abstract image"
|
||||
overlayDescription="Perfect for visual showcases with text overlays."
|
||||
overlayTitle="Image Card"
|
||||
src="https://images.unsplash.com/photo-1481487196290-c152efe083f5?auto=format&fit=crop&q=80&w=2048"
|
||||
/>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="Learn More"
|
||||
description="Drives action with an outline button."
|
||||
href="#"
|
||||
title="CTA Card (Default)"
|
||||
/>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="Sign Up Now"
|
||||
description="High prominence with a solid background."
|
||||
href="#"
|
||||
title="CTA Card (Primary)"
|
||||
variant="primary"
|
||||
/>
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['features']}
|
||||
description="A specialized card for categorized lists, ideal for wishlists or feature matrices."
|
||||
icon={<div className="p-2 bg-rb-blue-100 text-rb-blue-700 rounded-lg"><Star size={20} /></div>}
|
||||
sections={[
|
||||
{
|
||||
value: 'features',
|
||||
label: 'Features',
|
||||
labelIcon: <Check size={18} />,
|
||||
labelClassName: 'text-rb-green-600',
|
||||
items: ['Responsive', 'Accessible', 'Themeable'],
|
||||
}
|
||||
]}
|
||||
title="List Card"
|
||||
/>
|
||||
<BentoGrid.BaseCard className="flex flex-col gap-4">
|
||||
<div className="p-2 bg-rb-color-yellow-100 text-rb-color-yellow-900 rounded-md w-fit">
|
||||
<Code size={20} />
|
||||
</div>
|
||||
<h4 className="font-bold">Base Card (Custom)</h4>
|
||||
<p className="text-sm">You can put any children here to create custom card types while maintaining the bento border and padding.</p>
|
||||
</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const WishlistVariant: Story = {
|
||||
render: () => (
|
||||
<div className="w-full max-w-6xl p-4">
|
||||
<div className="mb-8">
|
||||
<Header level={2}>My Wishlist</Header>
|
||||
<Text className="text-rb-muted-text" size="l">Things I want and things I already have, organized by category.</Text>
|
||||
</div>
|
||||
|
||||
<BentoGrid>
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description="My essential list of gadgets and hardware I use for daily productivity and entertainment."
|
||||
icon={
|
||||
<div className="p-3 bg-rb-blue-100 text-rb-blue-700 rounded-xl">
|
||||
<Gamepad2 size={24} />
|
||||
</div>
|
||||
}
|
||||
rowSpan={2}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: 'Wanted',
|
||||
labelIcon: <Gift size={18} />,
|
||||
labelClassName: 'text-rb-orange-600',
|
||||
items: [
|
||||
'Steam Deck OLED 1TB',
|
||||
'Sony A7C II with 35mm lens',
|
||||
'Keychron Mechanical Keyboard'
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: 'Already Owned',
|
||||
labelIcon: <Check size={18} />,
|
||||
labelClassName: 'text-rb-green-600',
|
||||
items: [
|
||||
'MacBook Pro M3 Max',
|
||||
'Sony WH-1000XM5 Headphones',
|
||||
'LG 27" 4K Monitor'
|
||||
]
|
||||
}
|
||||
]}
|
||||
title="Gadgets & Tech"
|
||||
/>
|
||||
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
description="Places I want to visit and explore around the world."
|
||||
icon={
|
||||
<div className="p-3 bg-rb-green-100 text-rb-green-700 rounded-xl">
|
||||
<Plane size={24} />
|
||||
</div>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: 'Bucket List',
|
||||
labelIcon: <Gift size={18} />,
|
||||
labelClassName: 'text-rb-orange-600',
|
||||
items: [
|
||||
'Kyoto, Japan (Cherry Blossom Season)',
|
||||
'Reykjavik, Iceland',
|
||||
'Swiss Alps'
|
||||
]
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: 'Visited',
|
||||
labelIcon: <Check size={18} />,
|
||||
labelClassName: 'text-rb-green-600',
|
||||
items: [
|
||||
'Paris, France',
|
||||
'Rome, Italy',
|
||||
'London, UK'
|
||||
]
|
||||
}
|
||||
]}
|
||||
title="Travel Destinations"
|
||||
type="single"
|
||||
/>
|
||||
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
description="My reading backlog spanning technical books, sci-fi, and fantasy."
|
||||
icon={
|
||||
<div className="p-3 bg-rb-violet-100 text-rb-violet-700 rounded-xl">
|
||||
<BookOpen size={24} />
|
||||
</div>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: 'Reading List',
|
||||
labelIcon: <Gift size={18} />,
|
||||
labelClassName: 'text-rb-orange-600',
|
||||
items: [
|
||||
'Designing Data-Intensive Applications',
|
||||
'The Pragmatic Programmer',
|
||||
'Dune Messiah'
|
||||
]
|
||||
}
|
||||
]}
|
||||
title="Books to Read"
|
||||
type="single"
|
||||
/>
|
||||
</BentoGrid>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGrid } from './BentoGrid';
|
||||
|
||||
describe('BentoGrid', () => {
|
||||
it('renders all card types correctly', () => {
|
||||
render(
|
||||
<BentoGrid>
|
||||
<BentoGrid.BaseCard data-testid="base-card">Base Card Content</BentoGrid.BaseCard>
|
||||
<BentoGrid.ImageCard
|
||||
alt="Test Image"
|
||||
overlayDescription="Image Description"
|
||||
overlayTitle="Image Title"
|
||||
src="test-image.jpg"
|
||||
/>
|
||||
<BentoGrid.InfoCard
|
||||
description="Info Description"
|
||||
footer={<span>Footer Content</span>}
|
||||
title="Info Title"
|
||||
/>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="Click Me"
|
||||
description="CTA Description"
|
||||
href="/test"
|
||||
title="CTA Title"
|
||||
/>
|
||||
<BentoGrid.ListCard
|
||||
description="List Description"
|
||||
sections={[
|
||||
{
|
||||
value: 's1',
|
||||
label: 'Section 1',
|
||||
items: ['Item A', 'Item B']
|
||||
}
|
||||
]}
|
||||
title="List Title"
|
||||
/>
|
||||
</BentoGrid>
|
||||
);
|
||||
|
||||
// BaseCard
|
||||
expect(screen.getByTestId('base-card')).toBeInTheDocument();
|
||||
expect(screen.getByText('Base Card Content')).toBeInTheDocument();
|
||||
|
||||
// ImageCard
|
||||
expect(screen.getByAltText('Test Image')).toBeInTheDocument();
|
||||
expect(screen.getByText('Image Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Image Description')).toBeInTheDocument();
|
||||
|
||||
// InfoCard
|
||||
expect(screen.getByText('Info Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Info Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Footer Content')).toBeInTheDocument();
|
||||
|
||||
// CTACard
|
||||
expect(screen.getByText('CTA Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('CTA Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Click Me')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /Click Me/i })).toHaveAttribute('href', '/test');
|
||||
|
||||
// ListCard
|
||||
expect(screen.getByText('List Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('List Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Section 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Item A')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ListCard with object items (stable keys)', () => {
|
||||
render(
|
||||
<BentoGrid.ListCard
|
||||
sections={[
|
||||
{
|
||||
value: 's1',
|
||||
label: 'Section 1',
|
||||
items: [
|
||||
{ key: 'item-1', node: 'Stable Item 1' },
|
||||
'Raw Item 2'
|
||||
]
|
||||
}
|
||||
]}
|
||||
title="List Title"
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Stable Item 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Raw Item 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies span classes correctly', () => {
|
||||
render(
|
||||
<BentoGrid>
|
||||
<BentoGrid.BaseCard colSpan={2} rowSpan={2}>
|
||||
Spanning Card
|
||||
</BentoGrid.BaseCard>
|
||||
</BentoGrid>
|
||||
);
|
||||
|
||||
const card = screen.getByText('Spanning Card');
|
||||
expect(card).toHaveClass('md:col-span-2');
|
||||
expect(card).toHaveClass('md:row-span-2');
|
||||
});
|
||||
|
||||
it('applies primary variant to CTACard', () => {
|
||||
render(
|
||||
<BentoGrid>
|
||||
<BentoGrid.CTACard
|
||||
buttonText="Button"
|
||||
description="Description"
|
||||
href="/"
|
||||
title="Primary CTA"
|
||||
variant="primary"
|
||||
/>
|
||||
</BentoGrid>
|
||||
);
|
||||
|
||||
const title = screen.getByText('Primary CTA');
|
||||
const card = title.closest('.bg-rb-primary-base');
|
||||
expect(card).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { cn, genericMemo } from '@ermnvldmr/stl';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
import { BentoGridBaseCard } from './BentoGridBaseCard';
|
||||
import { BentoGridCTACard } from './BentoGridCTACard';
|
||||
import { BentoGridImageCard } from './BentoGridImageCard';
|
||||
import { BentoGridInfoCard } from './BentoGridInfoCard';
|
||||
import { BentoGridListCard } from './BentoGridListCard';
|
||||
|
||||
import type { BentoGridProps } from './types';
|
||||
|
||||
const BentoGridComponent = forwardRef<HTMLDivElement, BentoGridProps>(function BentoGrid(
|
||||
{ children, className, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'grid w-full grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-4',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* A responsive Bento Grid system for creating structured, masonry-like layouts.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BentoGrid>
|
||||
* <BentoGrid.ImageCard
|
||||
* colSpan={2}
|
||||
* rowSpan={2}
|
||||
* src="/image.jpg"
|
||||
* alt="Showcase"
|
||||
* overlayTitle="Featured"
|
||||
* />
|
||||
* <BentoGrid.InfoCard
|
||||
* title="Details"
|
||||
* description="Key features of the project."
|
||||
* />
|
||||
* </BentoGrid>
|
||||
* ```
|
||||
*/
|
||||
export const BentoGrid = Object.assign(genericMemo(BentoGridComponent), {
|
||||
BaseCard: BentoGridBaseCard,
|
||||
ImageCard: BentoGridImageCard,
|
||||
InfoCard: BentoGridInfoCard,
|
||||
CTACard: BentoGridCTACard,
|
||||
ListCard: BentoGridListCard,
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React from 'react';
|
||||
|
||||
import type { BentoCardBaseProps } from './types';
|
||||
|
||||
/**
|
||||
* Utility to generate Tailwind grid span classes based on props.
|
||||
* @example Generating classes:
|
||||
* ```ts
|
||||
* const classes = getSpanClasses({ colSpan: 2 });
|
||||
* ```
|
||||
*/
|
||||
const getSpanClasses = ({ colSpan = 1, rowSpan = 1 }: BentoCardBaseProps) => {
|
||||
const colSpans = {
|
||||
1: 'md:col-span-1',
|
||||
2: 'md:col-span-2',
|
||||
3: 'md:col-span-3',
|
||||
4: 'md:col-span-4',
|
||||
};
|
||||
|
||||
const rowSpans = {
|
||||
1: 'md:row-span-1',
|
||||
2: 'md:row-span-2',
|
||||
3: 'md:row-span-3',
|
||||
};
|
||||
|
||||
return cn(colSpans[colSpan], rowSpans[rowSpan]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Base card component that provides the foundational styling for all Bento cards.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const BentoGridBaseCard = ({
|
||||
children,
|
||||
className,
|
||||
colSpan,
|
||||
rowSpan,
|
||||
...props
|
||||
}: BentoCardBaseProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative overflow-hidden rounded-xl border border-rb-outline bg-rb-base p-6 shadow-sm transition-all hover:shadow-md',
|
||||
getSpanClasses({ colSpan, rowSpan }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGridBaseCard } from './BentoGridBaseCard';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Header } from '../Header/Header';
|
||||
import { Text } from '../Text/Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { BentoCardBaseProps } from './types';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface BentoGridCTACardProps extends BentoCardBaseProps {
|
||||
/** The title of the call to action */
|
||||
title: string;
|
||||
/** The description text */
|
||||
description: string;
|
||||
/** The text to display on the action button */
|
||||
buttonText: string;
|
||||
/** The destination URL for the action */
|
||||
href: string;
|
||||
/**
|
||||
* Visual variant for the card.
|
||||
* 'default' uses standard card styling.
|
||||
* 'primary' uses a more prominent background.
|
||||
*/
|
||||
variant?: 'default' | 'primary';
|
||||
}
|
||||
|
||||
/**
|
||||
* A Bento card designed specifically to drive user action.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const BentoGridCTACard = ({
|
||||
title,
|
||||
description,
|
||||
buttonText,
|
||||
href,
|
||||
variant = 'default',
|
||||
className,
|
||||
...baseProps
|
||||
}: BentoGridCTACardProps) => {
|
||||
return (
|
||||
<BentoGridBaseCard
|
||||
{...baseProps}
|
||||
className={cn(
|
||||
variant === 'primary' && 'bg-rb-primary-base border-rb-primary-base',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<VStack className="h-full justify-between" gap={4}>
|
||||
<VStack gap={2}>
|
||||
<Header className={cn(variant === 'primary' && 'text-rb-primary-text')} level={3}>
|
||||
{title}
|
||||
</Header>
|
||||
<Text
|
||||
className={cn(variant === 'primary' ? 'text-rb-primary-text/80' : 'text-rb-muted-text')}
|
||||
size="m"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</VStack>
|
||||
<div className="mt-auto">
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
href={href}
|
||||
variant={variant === 'primary' ? 'solid' : 'outline'}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
</BentoGridBaseCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGridBaseCard } from './BentoGridBaseCard';
|
||||
import { Header } from '../Header/Header';
|
||||
import { Image } from '../Image/Image';
|
||||
import { Text } from '../Text/Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { BentoCardBaseProps } from './types';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface BentoGridImageCardProps extends BentoCardBaseProps {
|
||||
/** Source URL of the image */
|
||||
src: string;
|
||||
/** Alt text for the image */
|
||||
alt: string;
|
||||
/** Optional title to overlay on the image */
|
||||
overlayTitle?: string;
|
||||
/** Optional description to overlay on the image */
|
||||
overlayDescription?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Bento card designed to showcase an image with optional textual overlays.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const BentoGridImageCard = ({
|
||||
src,
|
||||
alt,
|
||||
overlayTitle,
|
||||
overlayDescription,
|
||||
className,
|
||||
...baseProps
|
||||
}: BentoGridImageCardProps) => {
|
||||
const hasOverlay = !!overlayTitle || !!overlayDescription;
|
||||
|
||||
return (
|
||||
<BentoGridBaseCard {...baseProps} className={cn('p-0', className)}>
|
||||
<div className="relative h-full w-full">
|
||||
<Image
|
||||
alt={alt}
|
||||
className="h-full w-full object-cover"
|
||||
height="100%"
|
||||
ratio="auto"
|
||||
src={src}
|
||||
/>
|
||||
|
||||
{hasOverlay && (
|
||||
<div className="absolute inset-0 flex flex-col justify-end bg-linear-to-t from-black/80 via-black/40 to-transparent p-6 text-white">
|
||||
<VStack gap={1}>
|
||||
{overlayTitle && (
|
||||
<Header className="text-white" level={3}>
|
||||
{overlayTitle}
|
||||
</Header>
|
||||
)}
|
||||
{overlayDescription && (
|
||||
<Text className="text-white/90" size="s">
|
||||
{overlayDescription}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BentoGridBaseCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
import { BentoGridBaseCard } from './BentoGridBaseCard';
|
||||
import { Header } from '../Header/Header';
|
||||
import { Text } from '../Text/Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { BentoCardBaseProps } from './types';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface BentoGridInfoCardProps extends BentoCardBaseProps {
|
||||
/** The title of the card */
|
||||
title: string;
|
||||
/** The description text */
|
||||
description?: string;
|
||||
/** Optional icon or graphic to display at the top */
|
||||
icon?: React.ReactNode;
|
||||
/** Optional footer content */
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A versatile Bento card for text-heavy content or feature highlights.
|
||||
* @example Basic usage:
|
||||
*/
|
||||
export const BentoGridInfoCard = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
footer,
|
||||
...baseProps
|
||||
}: BentoGridInfoCardProps) => {
|
||||
return (
|
||||
<BentoGridBaseCard {...baseProps}>
|
||||
<VStack className="h-full justify-between" gap={4}>
|
||||
<VStack gap={2}>
|
||||
{icon && <div className="text-rb-primary-text">{icon}</div>}
|
||||
<Header level={3}>{title}</Header>
|
||||
{description && <Text className="text-rb-muted-text" size="m">{description}</Text>}
|
||||
</VStack>
|
||||
{footer && <div className="mt-auto">{footer}</div>}
|
||||
</VStack>
|
||||
</BentoGridBaseCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import { cn } from '@ermnvldmr/stl';
|
||||
import React from 'react';
|
||||
|
||||
import { Accordion } from '../Accordion';
|
||||
import { Header } from '../Header/Header';
|
||||
import { HStack } from '../HStack/HStack';
|
||||
import { List } from '../List';
|
||||
import { BentoGridBaseCard } from './BentoGridBaseCard';
|
||||
import { Text } from '../Text/Text';
|
||||
import { VStack } from '../VStack/VStack';
|
||||
|
||||
import type { BentoCardBaseProps } from './types';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface BentoGridListCardSection {
|
||||
/** Unique identifier for the accordion section */
|
||||
value: string;
|
||||
/** Label text for the accordion trigger */
|
||||
label: string;
|
||||
/** Optional icon next to the label */
|
||||
labelIcon?: React.ReactNode;
|
||||
/** Optional class name for the label text color (e.g. 'text-rb-orange-600') */
|
||||
labelClassName?: string;
|
||||
/** List of items to display when expanded. Can be raw nodes or objects with keys. */
|
||||
items: (React.ReactNode | { key: string; node: React.ReactNode })[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface BentoGridListCardProps extends BentoCardBaseProps {
|
||||
/** Title of the card */
|
||||
title: string;
|
||||
/** Description text */
|
||||
description?: string;
|
||||
/** Optional icon next to the title. For colored backgrounds, wrap the icon in a styled div. */
|
||||
icon?: React.ReactNode;
|
||||
/** Sections containing lists of items, rendered as Accordions */
|
||||
sections: BentoGridListCardSection[];
|
||||
/** Which sections are expanded by default (uses section value) */
|
||||
defaultValue?: string[];
|
||||
/** Whether multiple sections can be open at once */
|
||||
type?: 'single' | 'multiple';
|
||||
}
|
||||
|
||||
/**
|
||||
* A Bento card designed for categorized lists of items using Accordions.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <BentoGrid.ListCard
|
||||
* title="My List"
|
||||
* sections={[
|
||||
* { value: 's1', label: 'Section 1', items: ['Item 1', 'Item 2'] }
|
||||
* ]}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const BentoGridListCard = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
sections,
|
||||
defaultValue,
|
||||
type = 'multiple',
|
||||
...baseProps
|
||||
}: BentoGridListCardProps) => {
|
||||
return (
|
||||
<BentoGridBaseCard {...baseProps} className={cn('flex flex-col', baseProps.className)}>
|
||||
<VStack className="h-full" gap={4}>
|
||||
<HStack align="center" gap={3}>
|
||||
{icon && <div>{icon}</div>}
|
||||
<Header level={3}>{title}</Header>
|
||||
</HStack>
|
||||
|
||||
{description && (
|
||||
<Text className="text-rb-muted-text">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<div className="flex-grow w-full">
|
||||
<Accordion collapsible defaultValue={defaultValue} type={type}>
|
||||
{sections.map((section) => (
|
||||
<Accordion.Item key={section.value} value={section.value}>
|
||||
<Accordion.Trigger className={section.labelClassName}>
|
||||
<HStack align="center" gap={2}>
|
||||
{section.labelIcon}
|
||||
<Text size="l" type="body">{section.label}</Text>
|
||||
</HStack>
|
||||
</Accordion.Trigger>
|
||||
<Accordion.Content>
|
||||
<List>
|
||||
{section.items.map((item, index) => {
|
||||
const content = (() => {
|
||||
if (item && typeof item === 'object' && 'node' in item) {
|
||||
return item.node;
|
||||
}
|
||||
return item;
|
||||
})();
|
||||
|
||||
const itemKey =
|
||||
item && typeof item === 'object' && 'key' in item
|
||||
? item.key
|
||||
: typeof item === 'string'
|
||||
? item
|
||||
: index;
|
||||
|
||||
return (
|
||||
<List.Item key={itemKey}>
|
||||
<Text size="m">{content}</Text>
|
||||
</List.Item>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</VStack>
|
||||
</BentoGridBaseCard>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from './BentoGrid';
|
||||
export * from './types';
|
||||
export * from './BentoGridBaseCard';
|
||||
export * from './BentoGridImageCard';
|
||||
export * from './BentoGridInfoCard';
|
||||
export * from './BentoGridCTACard';
|
||||
export * from './BentoGridListCard';
|
||||
@@ -0,0 +1,39 @@
|
||||
import type React from 'react';
|
||||
|
||||
/**
|
||||
* Common props for all BentoGrid card variations.
|
||||
*/
|
||||
export interface BentoCardBaseProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Number of columns to span on medium screens and up.
|
||||
* @default 1
|
||||
*/
|
||||
colSpan?: 1 | 2 | 3 | 4;
|
||||
/**
|
||||
* Number of rows to span on medium screens and up.
|
||||
* @default 1
|
||||
*/
|
||||
rowSpan?: 1 | 2 | 3;
|
||||
/**
|
||||
* Additional CSS classes.
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The content of the card.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the root BentoGrid component.
|
||||
*/
|
||||
export interface BentoGridProps {
|
||||
/**
|
||||
* The cards to display in the grid.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Additional CSS classes.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ describe('PageHead', () => {
|
||||
it('applies the correct classes for the strategy', () => {
|
||||
render(<PageHead heading="Test" strategy="always-collapsed-fixed" />);
|
||||
const header = screen.getByRole('banner');
|
||||
expect(header).toHaveClass('sticky');
|
||||
expect(header).toHaveClass('shadow-md');
|
||||
expect(header).toHaveClass('fixed');
|
||||
expect(header).not.toHaveClass('sticky');
|
||||
});
|
||||
});
|
||||
@@ -67,19 +67,29 @@ export const PageHead = ({
|
||||
className,
|
||||
}: PageHeadProps) => {
|
||||
const { y: scrollY } = useScroll();
|
||||
const [isCollapsed, setIsCollapsed] = useState(strategy.includes('always-collapsed'));
|
||||
const isCollapsible = strategy === 'collapsible-sticky';
|
||||
const isAlwaysCollapsed = strategy.startsWith('always-collapsed');
|
||||
const [isCollapsed, setIsCollapsed] = useState(isAlwaysCollapsed);
|
||||
|
||||
useEffect(() => {
|
||||
if (strategy === 'collapsible-sticky') {
|
||||
setIsCollapsed(scrollY > 10);
|
||||
if (isCollapsible) {
|
||||
// Trigger collapse once we've scrolled past the expanded header's height.
|
||||
// Usually around 150-200px depending on caption length.
|
||||
// 200px ensures it won't snap until it's mostly gone.
|
||||
setIsCollapsed(scrollY > 200);
|
||||
}
|
||||
}, [scrollY, strategy]);
|
||||
}, [scrollY, isCollapsible]);
|
||||
|
||||
const rootClasses = cn(
|
||||
'page-head w-full transition-all duration-200 bg-[var(--rb-background)] sticky top-0 z-10 border-b border-[var(--rb-outline)]/10',
|
||||
{
|
||||
'shadow-md bg-[var(--rb-background)]/90 backdrop-blur-md': isCollapsed,
|
||||
},
|
||||
'page-head w-full bg-[var(--rb-background)] border-b border-[var(--rb-outline)]/10',
|
||||
// Always-collapsed or always-expanded strategies maintain their sticky/fixed behavior.
|
||||
// Collapsible-sticky strategy keeps the root in-flow so expanded content scrolls away.
|
||||
!isCollapsible &&
|
||||
(strategy.includes('sticky')
|
||||
? 'sticky top-0 z-10'
|
||||
: strategy.includes('fixed')
|
||||
? 'fixed top-0 z-10'
|
||||
: ''),
|
||||
className
|
||||
);
|
||||
|
||||
@@ -93,7 +103,7 @@ export const PageHead = ({
|
||||
</HStack>
|
||||
{addonRight && <div>{addonRight}</div>}
|
||||
</HStack>
|
||||
<VStack gap={2}>
|
||||
<VStack gap={4}>
|
||||
<Header level={1}>{heading}</Header>
|
||||
{typeof caption === 'string' ? (
|
||||
<Text color="muted" size="l" type="body">
|
||||
@@ -123,23 +133,29 @@ export const PageHead = ({
|
||||
|
||||
return (
|
||||
<header className={rootClasses}>
|
||||
{/* Expanded Content: In-flow for collapsible, or sticky/fixed for others */}
|
||||
{!isAlwaysCollapsed && (
|
||||
<div
|
||||
className={cn(
|
||||
'transition-opacity duration-300',
|
||||
isCollapsible && isCollapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'
|
||||
)}
|
||||
>
|
||||
{expandedContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Collapsed Content: Fixed overlay for collapsible, or absolute/relative within sticky/fixed root */}
|
||||
<div
|
||||
className={cn(
|
||||
'grid transition-[grid-template-rows] duration-300 ease-in-out',
|
||||
isCollapsed ? 'grid-rows-[0fr]' : 'grid-rows-[1fr]'
|
||||
'z-20 transition-all duration-300 transform bg-[var(--rb-background)]/90 backdrop-blur-md border-b border-[var(--rb-outline)]/10 shadow-md',
|
||||
isAlwaysCollapsed || (isCollapsible && isCollapsed)
|
||||
? 'translate-y-0 opacity-100'
|
||||
: '-translate-y-full opacity-0 pointer-events-none',
|
||||
isCollapsible ? 'fixed top-0 left-0 right-0' : 'absolute top-0 left-0 right-0'
|
||||
)}
|
||||
inert={isCollapsed || undefined}
|
||||
>
|
||||
<div className="overflow-hidden">{expandedContent}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'grid transition-[grid-template-rows] duration-300 ease-in-out',
|
||||
isCollapsed ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
|
||||
)}
|
||||
inert={!isCollapsed || undefined}
|
||||
>
|
||||
<div className="overflow-hidden">{collapsedContent}</div>
|
||||
{collapsedContent}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './Accordion/index';
|
||||
export * from './Article/Article';
|
||||
export * from './BentoGrid/index';
|
||||
export * from './Blockquote/Blockquote';
|
||||
export * from './Breadcrumbs/Breadcrumbs';
|
||||
export * from './Button/Button';
|
||||
|
||||
Generated
+160
-201
@@ -307,7 +307,7 @@ importers:
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.8.2))
|
||||
'@storybook/addon-coverage':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
version: 3.0.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(@types/react@19.2.9)(storybook@8.6.17(prettier@3.8.2))
|
||||
@@ -325,7 +325,7 @@ importers:
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.8.2))
|
||||
'@storybook/test-runner':
|
||||
specifier: ^0.19.1
|
||||
version: 0.19.1(@swc/helpers@0.5.18)(@types/node@25.0.10)(storybook@8.6.17(prettier@3.8.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))
|
||||
version: 0.19.1(@swc/helpers@0.5.18)(@types/node@25.6.0)(storybook@8.6.17(prettier@3.8.2))
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
@@ -349,7 +349,7 @@ importers:
|
||||
version: 19.2.3(@types/react@19.2.9)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(vitest@4.0.18(@types/node@25.0.10)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
version: 4.0.18(vitest@4.0.18(@types/node@25.6.0)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
babel-plugin-istanbul:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
@@ -373,13 +373,13 @@ importers:
|
||||
version: 8.6.17(prettier@3.8.2)
|
||||
storybook-react-rsbuild:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.8.2)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.8.2)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
vitest:
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18(@types/node@25.0.10)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
version: 4.0.18(@types/node@25.6.0)(jiti@2.6.1)(jsdom@28.0.0)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
wait-on:
|
||||
specifier: ^8.0.3
|
||||
version: 8.0.5
|
||||
@@ -456,6 +456,9 @@ importers:
|
||||
'@ermnvldmr/ui':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
lucide-react:
|
||||
specifier: ^0.503.0
|
||||
version: 0.503.0(react@19.2.3)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.2.3
|
||||
@@ -483,16 +486,16 @@ importers:
|
||||
version: 1.1.0(@rsbuild/core@1.7.3)
|
||||
'@rsbuild/plugin-mdx':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
version: 1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/rspack-plugin':
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
version: 1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@storybook/addon-a11y':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.5.3))
|
||||
'@storybook/addon-coverage':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
version: 3.0.0(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(@types/react@19.2.9)(storybook@8.6.17(prettier@3.5.3))
|
||||
@@ -507,7 +510,7 @@ importers:
|
||||
version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)
|
||||
'@storybook/react-vite':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
version: 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@storybook/test':
|
||||
specifier: ^8.6.15
|
||||
version: 8.6.15(storybook@8.6.17(prettier@3.5.3))
|
||||
@@ -576,7 +579,7 @@ importers:
|
||||
version: 8.6.17(prettier@3.5.3)
|
||||
storybook-react-rsbuild:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
version: 1.0.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
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)
|
||||
@@ -9572,12 +9575,12 @@ snapshots:
|
||||
'@types/yargs': 17.0.35
|
||||
chalk: 4.1.2
|
||||
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
magic-string: 0.27.0
|
||||
react-docgen-typescript: 2.4.0(typescript@5.8.3)
|
||||
vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
@@ -9756,12 +9759,12 @@ snapshots:
|
||||
'@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@mdx-js/loader@3.1.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@mdx-js/loader@3.1.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@mdx-js/mdx': 3.1.1
|
||||
source-map: 0.7.6
|
||||
optionalDependencies:
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11726,9 +11729,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@rsbuild/core': 1.7.3
|
||||
|
||||
'@rsbuild/plugin-mdx@1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsbuild/plugin-mdx@1.1.1(@rsbuild/core@1.7.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@mdx-js/loader': 3.1.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@mdx-js/loader': 3.1.1(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
optionalDependencies:
|
||||
'@rsbuild/core': 1.7.3
|
||||
transitivePeerDependencies:
|
||||
@@ -11771,13 +11774,13 @@ snapshots:
|
||||
|
||||
'@rsdoctor/client@1.5.3': {}
|
||||
|
||||
'@rsdoctor/core@1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/core@1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@rsbuild/plugin-check-syntax': 1.6.1(@rsbuild/core@1.7.3)
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/sdk': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/sdk': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rspack/resolver': 0.2.6
|
||||
browserslist-load-config: 1.0.1
|
||||
es-toolkit: 1.45.1
|
||||
@@ -11793,10 +11796,10 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- webpack
|
||||
|
||||
'@rsdoctor/graph@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/graph@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
es-toolkit: 1.45.1
|
||||
path-browserify: 1.0.1
|
||||
source-map: 0.7.6
|
||||
@@ -11804,13 +11807,13 @@ snapshots:
|
||||
- '@rspack/core'
|
||||
- webpack
|
||||
|
||||
'@rsdoctor/rspack-plugin@1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/rspack-plugin@1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@rsdoctor/core': 1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/sdk': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/core': 1.5.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/sdk': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
optionalDependencies:
|
||||
'@rspack/core': 1.7.6(@swc/helpers@0.5.18)
|
||||
transitivePeerDependencies:
|
||||
@@ -11820,12 +11823,12 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- webpack
|
||||
|
||||
'@rsdoctor/sdk@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/sdk@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@rsdoctor/client': 1.5.3
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/graph': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@rsdoctor/utils': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
launch-editor: 2.13.1
|
||||
safer-buffer: 2.1.2
|
||||
socket.io: 4.8.1
|
||||
@@ -11837,7 +11840,7 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- webpack
|
||||
|
||||
'@rsdoctor/types@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/types@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/estree': 1.0.5
|
||||
@@ -11845,12 +11848,12 @@ snapshots:
|
||||
source-map: 0.7.6
|
||||
optionalDependencies:
|
||||
'@rspack/core': 1.7.6(@swc/helpers@0.5.18)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)
|
||||
|
||||
'@rsdoctor/utils@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
'@rsdoctor/utils@1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.26.2
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@rsdoctor/types': 1.5.3(@rspack/core@1.7.6(@swc/helpers@0.5.18))(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@types/estree': 1.0.5
|
||||
acorn: 8.16.0
|
||||
acorn-import-attributes: 1.9.5(acorn@8.16.0)
|
||||
@@ -12142,7 +12145,7 @@ snapshots:
|
||||
storybook: 8.6.17(prettier@3.8.2)
|
||||
ts-dedent: 2.2.0
|
||||
|
||||
'@storybook/addon-coverage@3.0.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
'@storybook/addon-coverage@3.0.0(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
'@jsdevtools/coverage-istanbul-loader': 3.0.5
|
||||
@@ -12151,21 +12154,7 @@ snapshots:
|
||||
espree: 9.6.1
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
test-exclude: 6.0.0
|
||||
vite-plugin-istanbul: 6.0.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- vite
|
||||
|
||||
'@storybook/addon-coverage@3.0.0(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
'@jsdevtools/coverage-istanbul-loader': 3.0.5
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
convert-source-map: 2.0.0
|
||||
espree: 9.6.1
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
test-exclude: 6.0.0
|
||||
vite-plugin-istanbul: 6.0.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
vite-plugin-istanbul: 6.0.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- vite
|
||||
@@ -12340,13 +12329,13 @@ snapshots:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
'@storybook/builder-vite@8.6.15(storybook@8.6.17(prettier@3.5.3))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
'@storybook/builder-vite@8.6.15(storybook@8.6.17(prettier@3.5.3))(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 8.6.15(storybook@8.6.17(prettier@3.5.3))
|
||||
browser-assert: 1.2.1
|
||||
storybook: 8.6.17(prettier@3.5.3)
|
||||
ts-dedent: 2.2.0
|
||||
vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
|
||||
'@storybook/builder-vite@8.6.15(storybook@8.6.17(prettier@3.8.2))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
@@ -12495,20 +12484,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
endent: 2.1.0
|
||||
find-cache-dir: 3.3.2
|
||||
flat-cache: 3.2.0
|
||||
micromatch: 4.0.8
|
||||
react-docgen-typescript: 2.4.0(typescript@5.8.3)
|
||||
tslib: 2.8.1
|
||||
typescript: 5.8.3
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.1(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7))':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -12535,11 +12510,11 @@ snapshots:
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
storybook: 8.6.17(prettier@3.8.2)
|
||||
|
||||
'@storybook/react-vite@8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))':
|
||||
'@storybook/react-vite@8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@storybook/builder-vite': 8.6.15(storybook@8.6.17(prettier@3.5.3))(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@storybook/builder-vite': 8.6.15(storybook@8.6.17(prettier@3.5.3))(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2))
|
||||
'@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.21
|
||||
@@ -12549,7 +12524,7 @@ snapshots:
|
||||
resolve: 1.22.11
|
||||
storybook: 8.6.17(prettier@3.5.3)
|
||||
tsconfig-paths: 4.2.0
|
||||
vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
optionalDependencies:
|
||||
'@storybook/test': 8.6.15(storybook@8.6.17(prettier@3.5.3))
|
||||
transitivePeerDependencies:
|
||||
@@ -12643,7 +12618,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@storybook/test-runner@0.19.1(@swc/helpers@0.5.18)(@types/node@25.0.10)(storybook@8.6.17(prettier@3.8.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))':
|
||||
'@storybook/test-runner@0.19.1(@swc/helpers@0.5.18)(@types/node@25.6.0)(storybook@8.6.17(prettier@3.8.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@babel/generator': 7.28.6
|
||||
@@ -12657,14 +12632,14 @@ snapshots:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
'@swc/jest': 0.2.39(@swc/core@1.15.10(@swc/helpers@0.5.18))
|
||||
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))
|
||||
jest: 29.7.0(@types/node@25.6.0)
|
||||
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)(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-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.6.0))
|
||||
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)(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-watch-typeahead: 2.2.2(jest@29.7.0(@types/node@25.6.0))
|
||||
nyc: 15.1.0
|
||||
playwright: 1.58.0
|
||||
transitivePeerDependencies:
|
||||
@@ -13932,6 +13907,21 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@25.6.0):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@25.6.0)
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-require@1.1.1: {}
|
||||
|
||||
critters@0.0.25:
|
||||
@@ -15509,6 +15499,25 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@25.6.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(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': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@25.6.0)
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@25.6.0)
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-config@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:
|
||||
'@babel/core': 7.28.6
|
||||
@@ -15540,6 +15549,36 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@25.6.0):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.6
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.6)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 25.6.0
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-diff@29.7.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
@@ -15639,6 +15678,22 @@ snapshots:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
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.6.0)):
|
||||
dependencies:
|
||||
expect-playwright: 0.8.0
|
||||
jest: 29.7.0(@types/node@25.6.0)
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-process-manager: 0.4.0
|
||||
jest-runner: 29.7.0
|
||||
nyc: 15.1.0
|
||||
playwright-core: 1.58.0
|
||||
rimraf: 3.0.2
|
||||
uuid: 8.3.2
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
- supports-color
|
||||
|
||||
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
|
||||
optionalDependencies:
|
||||
jest-resolve: 29.7.0
|
||||
@@ -15793,6 +15848,17 @@ snapshots:
|
||||
string-length: 5.0.1
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@25.6.0)):
|
||||
dependencies:
|
||||
ansi-escapes: 6.2.1
|
||||
chalk: 5.6.2
|
||||
jest: 29.7.0(@types/node@25.6.0)
|
||||
jest-regex-util: 29.6.3
|
||||
jest-watcher: 29.7.0
|
||||
slash: 5.1.0
|
||||
string-length: 5.0.1
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
jest-watcher@29.7.0:
|
||||
dependencies:
|
||||
'@jest/test-result': 29.7.0
|
||||
@@ -15829,6 +15895,18 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@25.6.0):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0(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': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@25.6.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jju@1.4.0:
|
||||
@@ -17658,12 +17736,12 @@ snapshots:
|
||||
- '@types/react'
|
||||
- tslib
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)):
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.6(@swc/helpers@0.5.18))(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@rsbuild/core': 1.7.3
|
||||
'@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.5.3)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.5.3))(typescript@5.8.3)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@types/node': 18.19.130
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.21
|
||||
@@ -17686,34 +17764,6 @@ snapshots:
|
||||
- tslib
|
||||
- webpack
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.8.2)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@rsbuild/core': 1.7.3
|
||||
'@storybook/react': 8.6.15(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.8.2)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.8.3)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.25.12))
|
||||
'@types/node': 18.19.130
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.21
|
||||
react: 19.2.3
|
||||
react-docgen: 7.1.1
|
||||
react-docgen-typescript: 2.4.0(typescript@5.8.3)
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
resolve: 1.22.11
|
||||
storybook: 8.6.17(prettier@3.8.2)
|
||||
storybook-builder-rsbuild: 1.0.3(@rsbuild/core@1.7.3)(@types/react@19.2.9)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)
|
||||
tsconfig-paths: 4.2.0
|
||||
optionalDependencies:
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- '@rspack/core'
|
||||
- '@storybook/test'
|
||||
- '@types/react'
|
||||
- rollup
|
||||
- supports-color
|
||||
- tslib
|
||||
- webpack
|
||||
|
||||
storybook-react-rsbuild@1.0.3(@rsbuild/core@1.7.3)(@storybook/test@8.6.15(storybook@8.6.17(prettier@3.8.2)))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.60.1)(storybook@8.6.17(prettier@3.8.2))(typescript@5.8.3)(webpack@5.105.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
@@ -17918,17 +17968,6 @@ snapshots:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
esbuild: 0.25.12
|
||||
|
||||
terser-webpack-plugin@5.4.0(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
terser: 5.46.1
|
||||
webpack: 5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.10(@swc/helpers@0.5.18)
|
||||
esbuild: 0.27.7
|
||||
|
||||
terser-webpack-plugin@5.4.0(esbuild@0.27.7)(webpack@5.105.1(esbuild@0.27.7)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -18319,7 +18358,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-plugin-istanbul@6.0.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)):
|
||||
vite-plugin-istanbul@6.0.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
espree: 10.4.0
|
||||
@@ -18327,19 +18366,7 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map: 0.7.6
|
||||
test-exclude: 6.0.0
|
||||
vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-istanbul@6.0.2(vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
'@istanbuljs/load-nyc-config': 1.1.0
|
||||
espree: 10.4.0
|
||||
istanbul-lib-instrument: 6.0.3
|
||||
picocolors: 1.1.1
|
||||
source-map: 0.7.6
|
||||
test-exclude: 6.0.0
|
||||
vite: 8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -18397,42 +18424,6 @@ snapshots:
|
||||
terser: 5.46.1
|
||||
yaml: 2.8.2
|
||||
|
||||
vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.25.12)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.9
|
||||
rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.10
|
||||
esbuild: 0.25.12
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
terser: 5.46.1
|
||||
yaml: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.0.10)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.9
|
||||
rolldown: 1.0.0-rc.12(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)
|
||||
tinyglobby: 0.2.16
|
||||
optionalDependencies:
|
||||
'@types/node': 25.0.10
|
||||
esbuild: 0.27.7
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
terser: 5.46.1
|
||||
yaml: 2.8.2
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
|
||||
vite@8.0.5(@emnapi/core@1.8.1)(@emnapi/runtime@1.8.1)(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.2):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
@@ -18606,38 +18597,6 @@ snapshots:
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@types/json-schema': 7.0.15
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.16.0
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.20.1
|
||||
es-module-lexer: 2.0.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.3.1
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.2
|
||||
terser-webpack-plugin: 5.4.0(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7)(webpack@5.105.1(@swc/core@1.15.10(@swc/helpers@0.5.18))(esbuild@0.27.7))
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.3.4
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.105.1(esbuild@0.27.7):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@ermnvldmr/i18n": "workspace:*",
|
||||
"@ermnvldmr/stl": "workspace:*",
|
||||
"@ermnvldmr/ui": "workspace:*",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { createKeyset } from '@ermnvldmr/i18n';
|
||||
|
||||
export const { t } = createKeyset({
|
||||
Wanted: {
|
||||
en: 'Wanted',
|
||||
ru: 'Желаемое',
|
||||
},
|
||||
Home: {
|
||||
en: 'Home',
|
||||
ru: 'Главная'
|
||||
},
|
||||
'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.': {
|
||||
en: 'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.',
|
||||
ru: 'Спокойный список вещей, которые я планирую со временем приобрести. В основном это личная заметка, но также и ненавязчивая шпаргалка для друзей.',
|
||||
},
|
||||
'How this works': {
|
||||
en: 'How this works',
|
||||
ru: 'Как это работает',
|
||||
},
|
||||
'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.': {
|
||||
en: 'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.',
|
||||
ru: 'Считайте это живым списком вещей, которые я планирую приобрести. Если у вас когда-нибудь появится желание поучаствовать — я буду искренне рад, но, пожалуйста, знайте, что это абсолютно необязательно.',
|
||||
},
|
||||
'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.': {
|
||||
en: 'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.',
|
||||
ru: 'Когда дело доходит до самих вещей, подлинность и качество значат для меня очень много. Я всегда предпочту не иметь ничего, чем реплику или подделку.',
|
||||
},
|
||||
'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.': {
|
||||
en: 'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.',
|
||||
ru: 'И, конечно, если вы не уверены в каком-то конкретном издании или детали, мы всегда можем это обсудить.',
|
||||
},
|
||||
'Figurines': {
|
||||
en: 'Figurines',
|
||||
ru: 'Фигурки',
|
||||
},
|
||||
'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.': {
|
||||
en: 'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.',
|
||||
ru: 'Специфика издания и его подлинность здесь довольно важны. Обычно лучше знать точные детали, чтобы не ошибиться.',
|
||||
},
|
||||
'Looking for': {
|
||||
en: 'Looking for',
|
||||
ru: 'Ищу',
|
||||
},
|
||||
'NENDOROID 1970 Jill Stingray (VA-11 Hall-A)': {
|
||||
en: 'NENDOROID 1970 Jill Stingray (VA-11 Hall-A)',
|
||||
ru: 'NENDOROID 1970 Jill Stingray (VA-11 Hall-A)',
|
||||
},
|
||||
'Mirror’s Edge Catalyst (Collector’s Edition)': {
|
||||
en: 'Mirror’s Edge Catalyst (Collector’s Edition)',
|
||||
ru: 'Mirror’s Edge Catalyst (Collector’s Edition)',
|
||||
},
|
||||
"Deus Ex: Mankind Divided (Collector's Edition)": {
|
||||
en: "Deus Ex: Mankind Divided (Collector's Edition)",
|
||||
ru: "Deus Ex: Mankind Divided (Collector's Edition)",
|
||||
},
|
||||
'Genshin Impact Pop! Aether (#160)': {
|
||||
en: 'Genshin Impact Pop! Aether (#160)',
|
||||
ru: 'Genshin Impact Pop! Aether (#160)',
|
||||
},
|
||||
'LEGO Sets': {
|
||||
en: 'LEGO Sets',
|
||||
ru: 'Наборы LEGO',
|
||||
},
|
||||
'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.': {
|
||||
en: 'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.',
|
||||
ru: 'Я ценю наборы, ориентированные на инженерию, которые хорошо смотрятся на полке — например, Architecture, Technic или Botanical. Обычно я избегаю традиционных игровых наборов.',
|
||||
},
|
||||
'Architectural landmarks or skylines': {
|
||||
en: 'Architectural landmarks or skylines',
|
||||
ru: 'Архитектурные достопримечательности или панорамы',
|
||||
},
|
||||
'NASA and space exploration sets (like the Saturn V)': {
|
||||
en: 'NASA and space exploration sets (like the Saturn V)',
|
||||
ru: 'Наборы NASA и освоения космоса (например, Saturn V)',
|
||||
},
|
||||
'High-fidelity Technic vehicles': {
|
||||
en: 'High-fidelity Technic vehicles',
|
||||
ru: 'Высокодетализированная техника Technic',
|
||||
},
|
||||
'Already Owned': {
|
||||
en: 'Already Owned',
|
||||
ru: 'Уже есть',
|
||||
},
|
||||
'Japanese Red Maple Bonsai (10348)': {
|
||||
en: 'Japanese Red Maple Bonsai (10348)',
|
||||
ru: 'Japanese Red Maple Bonsai (10348)',
|
||||
},
|
||||
'The Pig House (21170)': {
|
||||
en: 'The Pig House (21170)',
|
||||
ru: 'The Pig House (21170)',
|
||||
},
|
||||
'Die-cast Models': {
|
||||
en: 'Die-cast Models',
|
||||
ru: 'Дайкаст модели',
|
||||
},
|
||||
'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.': {
|
||||
en: 'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.',
|
||||
ru: 'Мне нравятся качественные предварительно собранные дайкаст модели. Я не особо увлекаюсь пластиковыми наборами, которые нужно собирать с помощью клея и красок.',
|
||||
},
|
||||
'Spacecraft scale replicas (SpaceX, NASA)': {
|
||||
en: 'Spacecraft scale replicas (SpaceX, NASA)',
|
||||
ru: 'Масштабные модели космических аппаратов (SpaceX, NASA)',
|
||||
},
|
||||
'Motorsport cars (Modern F1, GT3, Endurance Legends)': {
|
||||
en: 'Motorsport cars (Modern F1, GT3, Endurance Legends)',
|
||||
ru: 'Автоспорт (современные болиды Формулы-1, GT3, легенды эндуранс)',
|
||||
},
|
||||
'Aviation models (SR-71 Blackbird, Concorde)': {
|
||||
en: 'Aviation models (SR-71 Blackbird, Concorde)',
|
||||
ru: 'Авиационные модели (SR-71 Blackbird, Concorde)',
|
||||
},
|
||||
'Bburago – McLaren F1 MCL39 2025 #4 Norris': {
|
||||
en: 'Bburago – McLaren F1 MCL39 2025 #4 Norris',
|
||||
ru: 'Bburago – McLaren F1 MCL39 2025 #4 Norris',
|
||||
},
|
||||
'Coffee gear': {
|
||||
en: 'Coffee gear',
|
||||
ru: 'Всё для кофе',
|
||||
},
|
||||
'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.': {
|
||||
en: 'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.',
|
||||
ru: 'Мой текущий сетап для эспрессо использует 51-миллиметровый портафильтр, поэтому любые аксессуары для него должны идеально подходить по размеру.',
|
||||
},
|
||||
'AeroPress': {
|
||||
en: 'AeroPress',
|
||||
ru: 'AeroPress',
|
||||
},
|
||||
'Dedicated cold brewer (Hario or Toddy)': {
|
||||
en: 'Dedicated cold brewer (Hario or Toddy)',
|
||||
ru: 'Специализированная колба для колд-брю (Hario или Toddy)',
|
||||
},
|
||||
'51mm WDT Tool': {
|
||||
en: '51mm WDT Tool',
|
||||
ru: 'WDT-инструмент на 51 мм',
|
||||
},
|
||||
'Freshly roasted specialty coffee beans': {
|
||||
en: 'Freshly roasted specialty coffee beans',
|
||||
ru: 'Свежеобжаренные спешелти зерна',
|
||||
},
|
||||
'Espresso Machine': {
|
||||
en: 'Espresso Machine',
|
||||
ru: 'Эспрессо-машина',
|
||||
},
|
||||
'Hario V60': {
|
||||
en: 'Hario V60',
|
||||
ru: 'Hario V60',
|
||||
},
|
||||
'Manual Hand Grinder': {
|
||||
en: 'Manual Hand Grinder',
|
||||
ru: 'Ручная кофемолка',
|
||||
},
|
||||
'French Press': {
|
||||
en: 'French Press',
|
||||
ru: 'Френч-пресс',
|
||||
},
|
||||
'Hobby': {
|
||||
en: 'Hobby',
|
||||
ru: 'Хобби',
|
||||
},
|
||||
'Various tools and equipment for electronics and other side projects.': {
|
||||
en: 'Various tools and equipment for electronics and other side projects.',
|
||||
ru: 'Различные инструменты и оборудование для электроники и других проектов.',
|
||||
},
|
||||
'Alientek T90 Soldering Iron (or a similar compact, smart iron)': {
|
||||
en: 'Alientek T90 Soldering Iron (or a similar compact, smart iron)',
|
||||
ru: 'Паяльник Alientek T90 (или аналогичный компактный умный паяльник)',
|
||||
},
|
||||
'Other items': {
|
||||
en: 'Other items',
|
||||
ru: 'Другое',
|
||||
},
|
||||
'A few standalone pieces that caught my eye.': {
|
||||
en: 'A few standalone pieces that caught my eye.',
|
||||
ru: 'Несколько отдельных вещей, которые привлекли моё внимание.',
|
||||
},
|
||||
'TimePiece® 3D tear-off calendar': {
|
||||
en: 'TimePiece® 3D tear-off calendar',
|
||||
ru: 'Отрывной 3D-календарь TimePiece®',
|
||||
},
|
||||
'Seiko SSC813 Prospex watch': {
|
||||
en: 'Seiko SSC813 Prospex watch',
|
||||
ru: 'Часы Seiko SSC813 Prospex',
|
||||
},
|
||||
'Tag Heuer Carrera watch': {
|
||||
en: 'Tag Heuer Carrera watch',
|
||||
ru: 'Часы Tag Heuer Carrera',
|
||||
},
|
||||
'Cast Iron Cube 10x10x10cm (just for the tactile density)': {
|
||||
en: 'Cast Iron Cube 10x10x10cm (just for the tactile density)',
|
||||
ru: 'Чугунный куб 10x10x10 см (просто ради тактильной плотности)',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,233 @@
|
||||
import { localePath } from '@ermnvldmr/i18n';
|
||||
import { BentoGrid, Header, Paragraph, VStack, Breadcrumbs } from '@ermnvldmr/ui';
|
||||
import { Ghost, Coffee, Plane, Check, Gift, Cuboid, BoxSelect, Cpu } from 'lucide-react';
|
||||
|
||||
import { t } from './index.i18n';
|
||||
import { DefaultLayout } from '../../components/Layout/DefaultLayout';
|
||||
import { createPage } from '../../lib/core/createPage';
|
||||
|
||||
/**
|
||||
* Wanted page component.
|
||||
* Serves as a personal log of things to acquire.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* createPage(WantedPage, { title: t('Wanted'), description: t('A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.') });
|
||||
* ```
|
||||
*/
|
||||
function WantedPage(): React.JSX.Element {
|
||||
const breadcrumbs = (
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.Item href={localePath('/')}>{t('Home')}</Breadcrumbs.Item>
|
||||
<Breadcrumbs.Separator />
|
||||
<Breadcrumbs.Item isCurrent>{t('Wanted')}</Breadcrumbs.Item>
|
||||
</Breadcrumbs>
|
||||
);
|
||||
|
||||
return (
|
||||
<DefaultLayout
|
||||
breadcrumbs={breadcrumbs}
|
||||
currentPath={localePath('/wanted')}
|
||||
description={t(
|
||||
'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.'
|
||||
)}
|
||||
title={t('Wanted')}
|
||||
>
|
||||
<VStack gap={8}>
|
||||
<VStack gap={4}>
|
||||
<Header level={3}>{t('How this works')}</Header>
|
||||
<VStack gap={4}>
|
||||
<Paragraph size="m">
|
||||
{t(
|
||||
'Think of this as a living list of things I plan to acquire over time. If you ever feel inspired to contribute, I would be genuinely thrilled, but please know it is never expected.'
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph size="m">
|
||||
{t(
|
||||
'When it comes to the items themselves, authenticity and quality matter a lot to me. I would always prefer to have nothing at all rather than a replica or a knock-off.'
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph size="m">
|
||||
{t(
|
||||
'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.'
|
||||
)}
|
||||
</Paragraph>
|
||||
</VStack>
|
||||
</VStack>
|
||||
|
||||
<BentoGrid>
|
||||
{/* Collectibles - Figurines */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t(
|
||||
'The specifics of the edition and its authenticity are quite important here. It is usually best to know the exact details to get it right.'
|
||||
)}
|
||||
icon={<Ghost size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('NENDOROID 1970 Jill Stingray (VA-11 Hall-A)'),
|
||||
t('Mirror’s Edge Catalyst (Collector’s Edition)'),
|
||||
t("Deus Ex: Mankind Divided (Collector's Edition)"),
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: t('Already Owned'),
|
||||
labelIcon: <Check size={18} />,
|
||||
items: [t('Genshin Impact Pop! Aether (#160)')],
|
||||
},
|
||||
]}
|
||||
title={t('Figurines')}
|
||||
/>
|
||||
|
||||
{/* Collectibles - LEGO */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t(
|
||||
'I appreciate sets that focus on engineering and make for good display pieces, like Architecture, Technic, or the Botanical collection. I generally lean away from traditional play-sets.'
|
||||
)}
|
||||
icon={<Cuboid size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('Architectural landmarks or skylines'),
|
||||
t('NASA and space exploration sets (like the Saturn V)'),
|
||||
t('High-fidelity Technic vehicles'),
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: t('Already Owned'),
|
||||
labelIcon: <Check size={18} />,
|
||||
items: [t('Japanese Red Maple Bonsai (10348)'), t('The Pig House (21170)')],
|
||||
},
|
||||
]}
|
||||
title={t('LEGO Sets')}
|
||||
/>
|
||||
|
||||
{/* Collectibles - Die-cast */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t(
|
||||
'I enjoy high-quality pre-assembled die-cast models. I am not much into plastic kits that require glue or paint to assemble.'
|
||||
)}
|
||||
icon={<Plane size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('Spacecraft scale replicas (SpaceX, NASA)'),
|
||||
t('Motorsport cars (Modern F1, GT3, Endurance Legends)'),
|
||||
t('Aviation models (SR-71 Blackbird, Concorde)'),
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: t('Already Owned'),
|
||||
labelIcon: <Check size={18} />,
|
||||
items: [t('Bburago – McLaren F1 MCL39 2025 #4 Norris')],
|
||||
},
|
||||
]}
|
||||
title={t('Die-cast Models')}
|
||||
/>
|
||||
|
||||
{/* Coffee stuff */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t(
|
||||
'My current espresso setup uses a 51mm portafilter, so any accessories for it need to match that size perfectly.'
|
||||
)}
|
||||
icon={<Coffee size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('AeroPress'),
|
||||
t('Dedicated cold brewer (Hario or Toddy)'),
|
||||
t('51mm WDT Tool'),
|
||||
t('Freshly roasted specialty coffee beans'),
|
||||
],
|
||||
},
|
||||
{
|
||||
value: 'owned',
|
||||
label: t('Already Owned'),
|
||||
labelIcon: <Check size={18} />,
|
||||
items: [
|
||||
t('Espresso Machine'),
|
||||
t('Hario V60'),
|
||||
t('Manual Hand Grinder'),
|
||||
t('French Press'),
|
||||
],
|
||||
},
|
||||
]}
|
||||
title={t('Coffee gear')}
|
||||
/>
|
||||
|
||||
{/* Hobby */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t('Various tools and equipment for electronics and other side projects.')}
|
||||
icon={<Cpu size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('Alientek T90 Soldering Iron (or a similar compact, smart iron)'),
|
||||
],
|
||||
},
|
||||
]}
|
||||
title={t('Hobby')}
|
||||
/>
|
||||
|
||||
{/* Other stuff */}
|
||||
<BentoGrid.ListCard
|
||||
colSpan={2}
|
||||
defaultValue={['wanted']}
|
||||
description={t('A few standalone pieces that caught my eye.')}
|
||||
icon={<BoxSelect size={24} />}
|
||||
sections={[
|
||||
{
|
||||
value: 'wanted',
|
||||
label: t('Looking for'),
|
||||
labelIcon: <Gift size={18} />,
|
||||
items: [
|
||||
t('TimePiece® 3D tear-off calendar'),
|
||||
t('Seiko SSC813 Prospex watch'),
|
||||
t('Tag Heuer Carrera watch'),
|
||||
t('Cast Iron Cube 10x10x10cm (just for the tactile density)'),
|
||||
],
|
||||
},
|
||||
]}
|
||||
title={t('Other items')}
|
||||
/>
|
||||
</BentoGrid>
|
||||
</VStack>
|
||||
</DefaultLayout>
|
||||
);
|
||||
}
|
||||
|
||||
createPage(WantedPage, {
|
||||
title: t('Wanted'),
|
||||
description: t(
|
||||
'A quiet log of things I am planning to pick up eventually. It serves mostly as a personal reference, but also as a zero-pressure guide for friends.'
|
||||
),
|
||||
});
|
||||
export default WantedPage;
|
||||
@@ -2,5 +2,6 @@ import { createKeyset } from '@ermnvldmr/i18n';
|
||||
|
||||
export const { t } = createKeyset({
|
||||
Articles: { en: 'Articles', ru: 'Статьи' },
|
||||
Wanted: { en: 'Wanted', ru: 'Разыскивается' },
|
||||
GitHub: { en: 'GitHub', ru: 'ГитХаб' },
|
||||
});
|
||||
@@ -37,6 +37,7 @@ export const PageHead = ({ heading, caption, breadcrumbs, metadata, strategy }:
|
||||
const nav = (
|
||||
<HStack gap={4}>
|
||||
<Link href="/articles">{t('Articles')}</Link>
|
||||
<Link href="/wanted">{t('Wanted')}</Link>
|
||||
<Link href="https://github.com/deytenit">{t('GitHub')}</Link>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user