From ef6ca40c72404b0400c1b9f729acf5e6255868a2 Mon Sep 17 00:00:00 2001 From: Vladimir Eremin Date: Mon, 4 May 2026 18:58:46 +0300 Subject: [PATCH] feat (www): Add Wanted (wishlist) page (#15) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../Accordion/Accordion.stories.tsx | 112 ++++++ .../components/Accordion/Accordion.test.tsx | 221 +++++++++++ .../ui/src/components/Accordion/Accordion.tsx | 95 +++++ .../AccordionContent/AccordionContent.tsx | 37 ++ .../Accordion/AccordionItem/AccordionItem.tsx | 40 ++ .../AccordionTrigger/AccordionTrigger.tsx | 73 ++++ .../AccordionContext/AccordionContext.ts | 20 + .../AccordionItemContext.ts | 20 + packages/ui/src/components/Accordion/index.ts | 9 + packages/ui/src/components/Accordion/types.ts | 88 +++++ .../BentoGrid/BentoGrid.stories.tsx | 291 ++++++++++++++ .../components/BentoGrid/BentoGrid.test.tsx | 121 ++++++ .../ui/src/components/BentoGrid/BentoGrid.tsx | 56 +++ .../BentoGrid/BentoGridBaseCard.tsx | 53 +++ .../components/BentoGrid/BentoGridCTACard.tsx | 77 ++++ .../BentoGrid/BentoGridImageCard.tsx | 70 ++++ .../BentoGrid/BentoGridInfoCard.tsx | 47 +++ .../BentoGrid/BentoGridListCard.tsx | 126 ++++++ packages/ui/src/components/BentoGrid/index.ts | 7 + packages/ui/src/components/BentoGrid/types.ts | 39 ++ .../Page/PageHead/PageHead.test.tsx | 4 +- .../src/components/Page/PageHead/PageHead.tsx | 60 +-- packages/ui/src/components/index.ts | 2 + pnpm-lock.yaml | 361 ++++++++---------- services/www/package.json | 1 + services/www/src/app/wanted/index.i18n.ts | 192 ++++++++++ services/www/src/app/wanted/index.tsx | 233 +++++++++++ .../src/components/PageHead/PageHead.i18n.ts | 1 + .../www/src/components/PageHead/PageHead.tsx | 1 + 29 files changed, 2232 insertions(+), 225 deletions(-) create mode 100644 packages/ui/src/components/Accordion/Accordion.stories.tsx create mode 100644 packages/ui/src/components/Accordion/Accordion.test.tsx create mode 100644 packages/ui/src/components/Accordion/Accordion.tsx create mode 100644 packages/ui/src/components/Accordion/AccordionContent/AccordionContent.tsx create mode 100644 packages/ui/src/components/Accordion/AccordionItem/AccordionItem.tsx create mode 100644 packages/ui/src/components/Accordion/AccordionTrigger/AccordionTrigger.tsx create mode 100644 packages/ui/src/components/Accordion/contexts/AccordionContext/AccordionContext.ts create mode 100644 packages/ui/src/components/Accordion/contexts/AccordionItemContext/AccordionItemContext.ts create mode 100644 packages/ui/src/components/Accordion/index.ts create mode 100644 packages/ui/src/components/Accordion/types.ts create mode 100644 packages/ui/src/components/BentoGrid/BentoGrid.stories.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGrid.test.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGrid.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGridBaseCard.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGridCTACard.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGridImageCard.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGridInfoCard.tsx create mode 100644 packages/ui/src/components/BentoGrid/BentoGridListCard.tsx create mode 100644 packages/ui/src/components/BentoGrid/index.ts create mode 100644 packages/ui/src/components/BentoGrid/types.ts create mode 100644 services/www/src/app/wanted/index.i18n.ts create mode 100644 services/www/src/app/wanted/index.tsx diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx new file mode 100644 index 0000000..49ecab0 --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx @@ -0,0 +1,112 @@ +import React from 'react'; + +import { Accordion } from './Accordion'; + +import type { Meta, StoryObj } from '@storybook/react'; + +const meta: Meta = { + title: 'Components/Accordion', + component: Accordion, + tags: ['autodocs'], + argTypes: { + type: { + control: 'select', + options: ['single', 'multiple'], + }, + collapsible: { + control: 'boolean', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + type: 'single', + collapsible: true, + }, + render: (args) => ( + + + Is it accessible? + + Yes. It adheres to the WAI-ARIA design pattern. + + + + Is it styled? + + Yes. It comes with default styles that matches the design system. + + + + Is it animated? + + Yes. It uses CSS Grid transitions for smooth height animation. + + + + ), +}; + +export const Multiple: Story = { + args: { + type: 'multiple', + }, + render: (args) => ( + + + Section One + + This section can be open alongside others. + + + + Section Two + + You can open as many sections as you want in multiple mode. + + + + ), +}; + +export const DifferentHeaderLevels: Story = { + render: () => ( + + + Header Level 1 + Content for H1 + + + Header Level 2 + Content for H2 + + + Header Level 3 + Content for H3 + + + Header Level 4 + Content for H4 + + + ), +}; + +export const Disabled: Story = { + render: () => ( + + + Enabled Item + This one works fine. + + + Disabled Item + You should not see this. + + + ), +}; diff --git a/packages/ui/src/components/Accordion/Accordion.test.tsx b/packages/ui/src/components/Accordion/Accordion.test.tsx new file mode 100644 index 0000000..c339d9e --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.test.tsx @@ -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( + + + Trigger 1 + Content 1 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + Trigger 2 + Content 2 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + Trigger 2 + Content 2 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + ); + + 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(['item-1']); + return ( + + + Trigger 1 + Content 1 + + + Trigger 2 + Content 2 + + + ); + }; + + render(); + + 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( + + + Trigger 1 + Content 1 + + + Trigger 2 + Content 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('renders with valid HTML structure (heading containing button)', () => { + render( + + + Trigger 1 + Content 1 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + ); + + 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( + + + Trigger 1 + Content 1 + + + ); + + 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(Trigger)).toThrow( + 'Accordion sub-components must be used within an Accordion component.' + ); + + expect(() => render(Item)).toThrow( + 'Accordion sub-components must be used within an Accordion component.' + ); + + expect(() => render(Content)).toThrow( + 'AccordionItem sub-components must be used within an AccordionItem component.' + ); + + consoleSpy.mockRestore(); + }); +}); diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx new file mode 100644 index 0000000..46dab9a --- /dev/null +++ b/packages/ui/src/components/Accordion/Accordion.tsx @@ -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(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 ( + +
+ {children} +
+
+ ); +}; + +/** + * A collapsible accordion component system. + * + * @example + * ```tsx + * + * + * Is it accessible? + * Yes. It adheres to the WAI-ARIA design pattern. + * + * + * ``` + */ +export const Accordion = Object.assign(AccordionRoot, { + Item: AccordionItem, + Trigger: AccordionTrigger, + Content: AccordionContent, +}); diff --git a/packages/ui/src/components/Accordion/AccordionContent/AccordionContent.tsx b/packages/ui/src/components/Accordion/AccordionContent/AccordionContent.tsx new file mode 100644 index 0000000..51ba0f5 --- /dev/null +++ b/packages/ui/src/components/Accordion/AccordionContent/AccordionContent.tsx @@ -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 ( +
+
+
+ {children} +
+
+
+ ); +}; diff --git a/packages/ui/src/components/Accordion/AccordionItem/AccordionItem.tsx b/packages/ui/src/components/Accordion/AccordionItem/AccordionItem.tsx new file mode 100644 index 0000000..58d9588 --- /dev/null +++ b/packages/ui/src/components/Accordion/AccordionItem/AccordionItem.tsx @@ -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 ( + +
+ {children} +
+
+ ); +}; diff --git a/packages/ui/src/components/Accordion/AccordionTrigger/AccordionTrigger.tsx b/packages/ui/src/components/Accordion/AccordionTrigger/AccordionTrigger.tsx new file mode 100644 index 0000000..0034797 --- /dev/null +++ b/packages/ui/src/components/Accordion/AccordionTrigger/AccordionTrigger.tsx @@ -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 + * Click me + * ``` + */ +export const AccordionTrigger = ({ + children, + className, + level = 3, + ...headerProps +}: AccordionTriggerProps) => { + const { onValueChange } = useAccordionContext(); + const { value, isOpen, disabled: isDisabled } = useAccordionItemContext(); + const ref = useRef(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 ( +
+ +
+ ); +}; diff --git a/packages/ui/src/components/Accordion/contexts/AccordionContext/AccordionContext.ts b/packages/ui/src/components/Accordion/contexts/AccordionContext/AccordionContext.ts new file mode 100644 index 0000000..e15d302 --- /dev/null +++ b/packages/ui/src/components/Accordion/contexts/AccordionContext/AccordionContext.ts @@ -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(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; +} diff --git a/packages/ui/src/components/Accordion/contexts/AccordionItemContext/AccordionItemContext.ts b/packages/ui/src/components/Accordion/contexts/AccordionItemContext/AccordionItemContext.ts new file mode 100644 index 0000000..8f7d69c --- /dev/null +++ b/packages/ui/src/components/Accordion/contexts/AccordionItemContext/AccordionItemContext.ts @@ -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(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; +} diff --git a/packages/ui/src/components/Accordion/index.ts b/packages/ui/src/components/Accordion/index.ts new file mode 100644 index 0000000..49ba297 --- /dev/null +++ b/packages/ui/src/components/Accordion/index.ts @@ -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'; diff --git a/packages/ui/src/components/Accordion/types.ts b/packages/ui/src/components/Accordion/types.ts new file mode 100644 index 0000000..29191ba --- /dev/null +++ b/packages/ui/src/components/Accordion/types.ts @@ -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 { + /** 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; +} diff --git a/packages/ui/src/components/BentoGrid/BentoGrid.stories.tsx b/packages/ui/src/components/BentoGrid/BentoGrid.stories.tsx new file mode 100644 index 0000000..cbb71ad --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGrid.stories.tsx @@ -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 = { + title: 'Components/BentoGrid', + component: BentoGrid, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Showcase: Story = { + render: () => ( +
+ + + } + title="Lightning Fast" + /> + + Strict Mode Enabled
} + icon={} + title="Type Safe" + /> + + } + title="Modular Architecture" + /> + + +
+
+
+ +
+
+

Subscribe to Newsletter

+

Stay updated with the latest releases and technical articles.

+
+
+ +
+
+ + + ), +}; + +export const LayoutPatterns: Story = { + render: () => ( +
+
+

Standard 4-Column Grid

+ + 1x1 + 1x1 + 1x1 + 1x1 + +
+ +
+

Mixed Spans (Hero Pattern)

+ + 2x2 Hero + 2x1 Wide + 1x1 + 1x1 + +
+ +
+

Tall & Wide Pattern

+ + 1x1 + 2x1 Wide + 1x2 Tall + 1x1 + 2x1 + +
+
+ ), +}; + +export const AllCardTypes: Story = { + render: () => ( +
+ + v1.0.0
} + icon={} + title="Info Card" + /> + + + + } + sections={[ + { + value: 'features', + label: 'Features', + labelIcon: , + labelClassName: 'text-rb-green-600', + items: ['Responsive', 'Accessible', 'Themeable'], + } + ]} + title="List Card" + /> + +
+ +
+

Base Card (Custom)

+

You can put any children here to create custom card types while maintaining the bento border and padding.

+
+ + + ), +}; + +export const WishlistVariant: Story = { + render: () => ( +
+
+
My Wishlist
+ Things I want and things I already have, organized by category. +
+ + + + +
+ } + rowSpan={2} + sections={[ + { + value: 'wanted', + label: 'Wanted', + labelIcon: , + 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: , + labelClassName: 'text-rb-green-600', + items: [ + 'MacBook Pro M3 Max', + 'Sony WH-1000XM5 Headphones', + 'LG 27" 4K Monitor' + ] + } + ]} + title="Gadgets & Tech" + /> + + + + + } + sections={[ + { + value: 'wanted', + label: 'Bucket List', + labelIcon: , + labelClassName: 'text-rb-orange-600', + items: [ + 'Kyoto, Japan (Cherry Blossom Season)', + 'Reykjavik, Iceland', + 'Swiss Alps' + ] + }, + { + value: 'owned', + label: 'Visited', + labelIcon: , + labelClassName: 'text-rb-green-600', + items: [ + 'Paris, France', + 'Rome, Italy', + 'London, UK' + ] + } + ]} + title="Travel Destinations" + type="single" + /> + + + + + } + sections={[ + { + value: 'wanted', + label: 'Reading List', + labelIcon: , + labelClassName: 'text-rb-orange-600', + items: [ + 'Designing Data-Intensive Applications', + 'The Pragmatic Programmer', + 'Dune Messiah' + ] + } + ]} + title="Books to Read" + type="single" + /> + + + ), +}; diff --git a/packages/ui/src/components/BentoGrid/BentoGrid.test.tsx b/packages/ui/src/components/BentoGrid/BentoGrid.test.tsx new file mode 100644 index 0000000..ecd803d --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGrid.test.tsx @@ -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( + + Base Card Content + + Footer Content} + title="Info Title" + /> + + + + ); + + // 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( + + ); + + expect(screen.getByText('Stable Item 1')).toBeInTheDocument(); + expect(screen.getByText('Raw Item 2')).toBeInTheDocument(); + }); + + it('applies span classes correctly', () => { + render( + + + Spanning Card + + + ); + + 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( + + + + ); + + const title = screen.getByText('Primary CTA'); + const card = title.closest('.bg-rb-primary-base'); + expect(card).toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/BentoGrid/BentoGrid.tsx b/packages/ui/src/components/BentoGrid/BentoGrid.tsx new file mode 100644 index 0000000..a34ee4f --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGrid.tsx @@ -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(function BentoGrid( + { children, className, ...props }, + ref +) { + return ( +
+ {children} +
+ ); +}); + +/** + * A responsive Bento Grid system for creating structured, masonry-like layouts. + * + * @example + * ```tsx + * + * + * + * + * ``` + */ +export const BentoGrid = Object.assign(genericMemo(BentoGridComponent), { + BaseCard: BentoGridBaseCard, + ImageCard: BentoGridImageCard, + InfoCard: BentoGridInfoCard, + CTACard: BentoGridCTACard, + ListCard: BentoGridListCard, +}); diff --git a/packages/ui/src/components/BentoGrid/BentoGridBaseCard.tsx b/packages/ui/src/components/BentoGrid/BentoGridBaseCard.tsx new file mode 100644 index 0000000..a1beb22 --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGridBaseCard.tsx @@ -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 ( +
+ {children} +
+ ); +}; diff --git a/packages/ui/src/components/BentoGrid/BentoGridCTACard.tsx b/packages/ui/src/components/BentoGrid/BentoGridCTACard.tsx new file mode 100644 index 0000000..ba14f1d --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGridCTACard.tsx @@ -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 ( + + + +
+ {title} +
+ + {description} + +
+
+ +
+
+
+ ); +}; diff --git a/packages/ui/src/components/BentoGrid/BentoGridImageCard.tsx b/packages/ui/src/components/BentoGrid/BentoGridImageCard.tsx new file mode 100644 index 0000000..a2783ff --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGridImageCard.tsx @@ -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 ( + +
+ {alt} + + {hasOverlay && ( +
+ + {overlayTitle && ( +
+ {overlayTitle} +
+ )} + {overlayDescription && ( + + {overlayDescription} + + )} +
+
+ )} +
+
+ ); +}; diff --git a/packages/ui/src/components/BentoGrid/BentoGridInfoCard.tsx b/packages/ui/src/components/BentoGrid/BentoGridInfoCard.tsx new file mode 100644 index 0000000..deb65ac --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGridInfoCard.tsx @@ -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 ( + + + + {icon &&
{icon}
} +
{title}
+ {description && {description}} +
+ {footer &&
{footer}
} +
+
+ ); +}; diff --git a/packages/ui/src/components/BentoGrid/BentoGridListCard.tsx b/packages/ui/src/components/BentoGrid/BentoGridListCard.tsx new file mode 100644 index 0000000..0c452a6 --- /dev/null +++ b/packages/ui/src/components/BentoGrid/BentoGridListCard.tsx @@ -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 + * + * ``` + */ +export const BentoGridListCard = ({ + title, + description, + icon, + sections, + defaultValue, + type = 'multiple', + ...baseProps +}: BentoGridListCardProps) => { + return ( + + + + {icon &&
{icon}
} +
{title}
+
+ + {description && ( + + {description} + + )} + +
+ + {sections.map((section) => ( + + + + {section.labelIcon} + {section.label} + + + + + {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 ( + + {content} + + ); + })} + + + + ))} + +
+
+
+ ); +}; diff --git a/packages/ui/src/components/BentoGrid/index.ts b/packages/ui/src/components/BentoGrid/index.ts new file mode 100644 index 0000000..3c83a37 --- /dev/null +++ b/packages/ui/src/components/BentoGrid/index.ts @@ -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'; diff --git a/packages/ui/src/components/BentoGrid/types.ts b/packages/ui/src/components/BentoGrid/types.ts new file mode 100644 index 0000000..c2f0a75 --- /dev/null +++ b/packages/ui/src/components/BentoGrid/types.ts @@ -0,0 +1,39 @@ +import type React from 'react'; + +/** + * Common props for all BentoGrid card variations. + */ +export interface BentoCardBaseProps extends React.HTMLAttributes { + /** + * 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; +} diff --git a/packages/ui/src/components/Page/PageHead/PageHead.test.tsx b/packages/ui/src/components/Page/PageHead/PageHead.test.tsx index 636349b..3c36c55 100644 --- a/packages/ui/src/components/Page/PageHead/PageHead.test.tsx +++ b/packages/ui/src/components/Page/PageHead/PageHead.test.tsx @@ -32,7 +32,7 @@ describe('PageHead', () => { it('applies the correct classes for the strategy', () => { render(); const header = screen.getByRole('banner'); - expect(header).toHaveClass('sticky'); - expect(header).toHaveClass('shadow-md'); + expect(header).toHaveClass('fixed'); + expect(header).not.toHaveClass('sticky'); }); }); diff --git a/packages/ui/src/components/Page/PageHead/PageHead.tsx b/packages/ui/src/components/Page/PageHead/PageHead.tsx index abd50b8..dfdc14c 100644 --- a/packages/ui/src/components/Page/PageHead/PageHead.tsx +++ b/packages/ui/src/components/Page/PageHead/PageHead.tsx @@ -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 = ({ {addonRight &&
{addonRight}
} - +
{heading}
{typeof caption === 'string' ? ( @@ -123,23 +133,29 @@ export const PageHead = ({ return (
+ {/* Expanded Content: In-flow for collapsible, or sticky/fixed for others */} + {!isAlwaysCollapsed && ( +
+ {expandedContent} +
+ )} + + {/* Collapsed Content: Fixed overlay for collapsible, or absolute/relative within sticky/fixed root */}
-
{expandedContent}
-
-
-
{collapsedContent}
+ {collapsedContent}
); diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index e2b212d..d255f6e 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -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'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 005bf8a..f1c3132 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/services/www/package.json b/services/www/package.json index 753eafa..e69259e 100644 --- a/services/www/package.json +++ b/services/www/package.json @@ -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" diff --git a/services/www/src/app/wanted/index.i18n.ts b/services/www/src/app/wanted/index.i18n.ts new file mode 100644 index 0000000..dcb1329 --- /dev/null +++ b/services/www/src/app/wanted/index.i18n.ts @@ -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. Обычно я избегаю традиционных игровых наборов (friends, city, и другие подобные).', + }, + 'Architectural landmarks or skylines': { + en: 'Architectural landmarks or skylines', + ru: 'Архитектурные наборы', + }, + 'NASA and space exploration sets (like the #92176 Ideas NASA Apollo Saturn V)': { + en: 'NASA and space exploration sets (like the #92176 Ideas NASA Apollo Saturn V)', + ru: 'Наборы NASA и иные аэрокосмические (например, #92176 Ideas NASA Apollo 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 см (по приколу)', + }, +}); diff --git a/services/www/src/app/wanted/index.tsx b/services/www/src/app/wanted/index.tsx new file mode 100644 index 0000000..811c121 --- /dev/null +++ b/services/www/src/app/wanted/index.tsx @@ -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 = ( + + {t('Home')} + + {t('Wanted')} + + ); + + return ( + + + +
{t('How this works')}
+ + + {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.' + )} + + + {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.' + )} + + + {t( + 'And of course, if you are ever unsure about a specific edition or detail, we can always just talk about it.' + )} + + +
+ + + {/* Collectibles - Figurines */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + 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: , + items: [t('Genshin Impact Pop! Aether (#160)')], + }, + ]} + title={t('Figurines')} + /> + + {/* Collectibles - LEGO */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + items: [ + t('Architectural landmarks or skylines'), + t('NASA and space exploration sets (like the #92176 Ideas NASA Apollo Saturn V)'), + t('High-fidelity Technic vehicles'), + ], + }, + { + value: 'owned', + label: t('Already Owned'), + labelIcon: , + items: [t('Japanese Red Maple Bonsai (10348)'), t('The Pig House (21170)')], + }, + ]} + title={t('LEGO Sets')} + /> + + {/* Collectibles - Die-cast */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + 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: , + items: [t('Bburago – McLaren F1 MCL39 2025 #4 Norris')], + }, + ]} + title={t('Die-cast Models')} + /> + + {/* Coffee stuff */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + 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: , + items: [ + t('Espresso Machine'), + t('Hario V60'), + t('Manual Hand Grinder'), + t('French Press'), + ], + }, + ]} + title={t('Coffee gear')} + /> + + {/* Hobby */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + items: [ + t('Alientek T90 Soldering Iron (or a similar compact, smart iron)'), + ], + }, + ]} + title={t('Hobby')} + /> + + {/* Other stuff */} + } + sections={[ + { + value: 'wanted', + label: t('Looking for'), + labelIcon: , + 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')} + /> + +
+
+ ); +} + +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; diff --git a/services/www/src/components/PageHead/PageHead.i18n.ts b/services/www/src/components/PageHead/PageHead.i18n.ts index aeaece7..5eaab4a 100644 --- a/services/www/src/components/PageHead/PageHead.i18n.ts +++ b/services/www/src/components/PageHead/PageHead.i18n.ts @@ -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: 'ГитХаб' }, }); diff --git a/services/www/src/components/PageHead/PageHead.tsx b/services/www/src/components/PageHead/PageHead.tsx index cef1749..e6acdb4 100644 --- a/services/www/src/components/PageHead/PageHead.tsx +++ b/services/www/src/components/PageHead/PageHead.tsx @@ -37,6 +37,7 @@ export const PageHead = ({ heading, caption, breadcrumbs, metadata, strategy }: const nav = ( {t('Articles')} + {t('Wanted')} {t('GitHub')} );