From af50fc895f53cac3da808317b3128b3f654d2735 Mon Sep 17 00:00:00 2001 From: Alexis Guzman Date: Wed, 21 Oct 2026 17:27:43 +0000 Subject: [PATCH 1/2] feat(web): add Authored carousel --- apps/web/content/docs/zen/carousel.mdx | 78 ++++++ apps/web/content/docs/zen/meta.json | 1 + apps/web/lib/registry/examples.tsx | 2 + apps/web/lib/registry/pending.ts | 1 - apps/web/package.json | 1 + .../zen/examples/carousel-example.tsx | 66 +++++ apps/web/registry/zen/registry.json | 26 ++ apps/web/registry/zen/ui/carousel.tsx | 256 ++++++++++++++++++ bun.lock | 7 + 9 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 apps/web/content/docs/zen/carousel.mdx create mode 100644 apps/web/registry/zen/examples/carousel-example.tsx create mode 100644 apps/web/registry/zen/ui/carousel.tsx diff --git a/apps/web/content/docs/zen/carousel.mdx b/apps/web/content/docs/zen/carousel.mdx new file mode 100644 index 0000000..330c6c1 --- /dev/null +++ b/apps/web/content/docs/zen/carousel.mdx @@ -0,0 +1,78 @@ +--- +title: Carousel +description: A slide track with previous and next controls, built on Embla Carousel. +--- + +Carousel wraps [embla-carousel-react](https://www.embla-carousel.com/) in a +track, a slide and two icon controls. Each slide takes the full width of the +track, and the controls scroll the track one slide at a time. Arrow keys work +when the track has focus. + +It is an Authored component: shadcn/ui owns its structure and its design comes +from the nearest Port, because coss/ui ships no carousel. The controls inherit +their surface, focus ring and disabled treatment from `Button`, and a slide +inherits its sizing from `AspectRatio`. + +## Installation + +```bash +npx shadcn@latest add @kosori/carousel +``` + +## Example + + + +## Usage + +```tsx +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from '@/components/ui/carousel'; + +const CarouselDemo = () => ( + + + Slide one + Slide two + + + + +); +``` + +The previous and next controls sit outside the track, so leave room for them: +the Example pads its container by `px-12`. + +## Orientation + +`orientation` switches the scroll axis. The controls rotate and move above and +below the track, so a vertical carousel needs vertical room instead: + +```tsx + + + Slide one + Slide two + + + + +``` + +## API + +| Prop | Type | Default | Purpose | +| --- | --- | --- | --- | +| `opts` | Embla options | `undefined` | Forwarded to Embla Carousel | +| `orientation` | `horizontal`, `vertical` | `horizontal` | Scroll axis | +| `plugins` | Embla plugins | `undefined` | Forwarded to Embla Carousel | +| `setApi` | `(api: CarouselApi) => void` | `undefined` | Receives the Embla API | + +`useCarousel` reads the nearest carousel's API and scroll state, and throws +outside a `Carousel`. diff --git a/apps/web/content/docs/zen/meta.json b/apps/web/content/docs/zen/meta.json index 8954eba..0f6409b 100644 --- a/apps/web/content/docs/zen/meta.json +++ b/apps/web/content/docs/zen/meta.json @@ -18,6 +18,7 @@ "button-group", "calendar", "card", + "carousel", "checkbox", "collapsible", "combobox", diff --git a/apps/web/lib/registry/examples.tsx b/apps/web/lib/registry/examples.tsx index 9f5140e..589d2d6 100644 --- a/apps/web/lib/registry/examples.tsx +++ b/apps/web/lib/registry/examples.tsx @@ -19,6 +19,7 @@ import ButtonExample from '@/registry/zen/examples/button-example'; import ButtonGroupExample from '@/registry/zen/examples/button-group-example'; import CalendarExample from '@/registry/zen/examples/calendar-example'; import CardExample from '@/registry/zen/examples/card-example'; +import CarouselExample from '@/registry/zen/examples/carousel-example'; import CheckboxExample from '@/registry/zen/examples/checkbox-example'; import CollapsibleExample from '@/registry/zen/examples/collapsible-example'; import ComboboxExample from '@/registry/zen/examples/combobox-example'; @@ -90,6 +91,7 @@ const zenExampleComponents = { 'button-group': ButtonGroupExample, calendar: CalendarExample, card: CardExample, + carousel: CarouselExample, checkbox: CheckboxExample, collapsible: CollapsibleExample, combobox: ComboboxExample, diff --git a/apps/web/lib/registry/pending.ts b/apps/web/lib/registry/pending.ts index 01286ca..0bddbbc 100644 --- a/apps/web/lib/registry/pending.ts +++ b/apps/web/lib/registry/pending.ts @@ -6,7 +6,6 @@ export const PENDING_COMPONENTS = [ 'attachment', 'bubble', - 'carousel', 'chart', 'code-block', 'marker', diff --git a/apps/web/package.json b/apps/web/package.json index f693c8f..bbd8cba 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,7 @@ "@tailwindcss/postcss": "4.3.3", "class-variance-authority": "^0.7.1", "cn": "^0.5.0", + "embla-carousel-react": "^8.6.0", "fumadocs-core": "16.17.5", "fumadocs-mdx": "15.4.7", "fumadocs-ui": "npm:@fumadocs/base-ui@16.17.5", diff --git a/apps/web/registry/zen/examples/carousel-example.tsx b/apps/web/registry/zen/examples/carousel-example.tsx new file mode 100644 index 0000000..3cd1f67 --- /dev/null +++ b/apps/web/registry/zen/examples/carousel-example.tsx @@ -0,0 +1,66 @@ +import { AspectRatio } from '@/components/ui/aspect-ratio'; +import { + Carousel, + CarouselContent, + CarouselItem, + CarouselNext, + CarouselPrevious, +} from '@/components/ui/carousel'; + +const SLIDES = [ + { + className: + 'overflow-hidden rounded-xl border bg-linear-to-br from-primary/16 to-background', + description: 'The first slide of the default carousel.', + title: 'One', + }, + { + className: + 'overflow-hidden rounded-xl border bg-linear-to-br from-info/16 to-background', + description: 'Slides scroll with the previous and next controls.', + title: 'Two', + }, + { + className: + 'overflow-hidden rounded-xl border bg-linear-to-br from-success/16 to-background', + description: 'The track also responds to the arrow keys.', + title: 'Three', + }, + { + className: + 'overflow-hidden rounded-xl border bg-linear-to-br from-warning/16 to-background', + description: 'Every slide shares the same width.', + title: 'Four', + }, + { + className: + 'overflow-hidden rounded-xl border bg-linear-to-br from-destructive/16 to-background', + description: 'The last slide disables the next control.', + title: 'Five', + }, +]; + +const CarouselExample = () => ( +
+ + + {SLIDES.map((slide) => ( + + +
+ {slide.title} + + {slide.description} + +
+
+
+ ))} +
+ + +
+
+); + +export default CarouselExample; diff --git a/apps/web/registry/zen/registry.json b/apps/web/registry/zen/registry.json index b933074..65ede0d 100644 --- a/apps/web/registry/zen/registry.json +++ b/apps/web/registry/zen/registry.json @@ -333,6 +333,32 @@ } ] }, + { + "name": "carousel", + "type": "registry:ui", + "dependencies": ["@tabler/icons-react", "cn", "embla-carousel-react"], + "registryDependencies": ["@kosori/button"], + "files": [ + { + "path": "registry/zen/ui/carousel.tsx", + "type": "registry:ui" + } + ], + "meta": { + "authored": true + } + }, + { + "name": "carousel-example", + "type": "registry:example", + "registryDependencies": ["@kosori/aspect-ratio", "@kosori/carousel"], + "files": [ + { + "path": "registry/zen/examples/carousel-example.tsx", + "type": "registry:example" + } + ] + }, { "name": "checkbox", "type": "registry:ui", diff --git a/apps/web/registry/zen/ui/carousel.tsx b/apps/web/registry/zen/ui/carousel.tsx new file mode 100644 index 0000000..033f1cc --- /dev/null +++ b/apps/web/registry/zen/ui/carousel.tsx @@ -0,0 +1,256 @@ +'use client'; + +import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react'; +import { cn } from 'cn'; +import useEmblaCarousel, { + type UseEmblaCarouselType, +} from 'embla-carousel-react'; +import type React from 'react'; +import { + createContext, + useCallback, + useContext, + useEffect, + useState, +} from 'react'; + +import { Button } from '@/components/ui/button'; + +export type CarouselApi = UseEmblaCarouselType[1]; + +type CarouselOptions = Parameters[0]; +type CarouselPlugin = Parameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + orientation?: 'horizontal' | 'vertical'; + plugins?: CarouselPlugin; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + api: CarouselApi; + canScrollNext: boolean; + canScrollPrev: boolean; + carouselRef: ReturnType[0]; + scrollNext: () => void; + scrollPrev: () => void; +} & CarouselProps; + +const CarouselContext = createContext(null); + +export function useCarousel(): CarouselContextProps { + const context = useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +export function Carousel({ + orientation = 'horizontal', + opts, + setApi, + plugins, + className, + children, + ...props +}: React.ComponentProps<'div'> & CarouselProps): React.ReactElement { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins + ); + const [canScrollPrev, setCanScrollPrev] = useState(false); + const [canScrollNext, setCanScrollNext] = useState(false); + + const onSelect = useCallback((emblaApi: CarouselApi) => { + if (!emblaApi) { + return; + } + + setCanScrollPrev(emblaApi.canScrollPrev()); + setCanScrollNext(emblaApi.canScrollNext()); + }, []); + + const scrollPrev = useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext] + ); + + useEffect(() => { + if (!(api && setApi)) { + return; + } + + setApi(api); + }, [api, setApi]); + + useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api.off('reInit', onSelect); + api.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + + {/* biome-ignore lint/a11y/useSemanticElements: shadcn/ui gives the carousel role=region on a div, because a section would claim document structure it does not own */} +
+ {children} +
+
+ ); +} + +export function CarouselContent({ + className, + ...props +}: React.ComponentProps<'div'>): React.ReactElement { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); +} + +export function CarouselItem({ + className, + ...props +}: React.ComponentProps<'div'>): React.ReactElement { + const { orientation } = useCarousel(); + + return ( + // biome-ignore lint/a11y/useSemanticElements: shadcn/ui gives each slide role=group, because a fieldset would add form semantics +
+ ); +} + +export function CarouselPrevious({ + className, + variant = 'outline', + size = 'icon-sm', + ...props +}: React.ComponentProps): React.ReactElement { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); +} + +export function CarouselNext({ + className, + variant = 'outline', + size = 'icon-sm', + ...props +}: React.ComponentProps): React.ReactElement { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); +} diff --git a/bun.lock b/bun.lock index 7e531f1..8b20194 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "@tailwindcss/postcss": "4.3.3", "class-variance-authority": "^0.7.1", "cn": "^0.5.0", + "embla-carousel-react": "^8.6.0", "fumadocs-core": "16.17.5", "fumadocs-mdx": "15.4.7", "fumadocs-ui": "npm:@fumadocs/base-ui@16.17.5", @@ -625,6 +626,12 @@ "electron-to-chromium": ["electron-to-chromium@1.5.459", "", {}, "sha512-4SC9HHSFjO/liAhifpdOL5hG9b4BP7/5G3y3UPZVO8trlAXr1CaUEEJL0gWK3kOcYcZrOA5rkwG45Gra/lTIgg=="], + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "empathic": ["empathic@2.1.0", "", {}, "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw=="], From 49461799808cdab3c54b0e2111c00ca836251d98 Mon Sep 17 00:00:00 2001 From: Alexis Guzman Date: Wed, 21 Oct 2026 17:31:56 +0000 Subject: [PATCH 2/2] feat(web): add authored chart --- apps/web/content/docs/zen/chart.mdx | 63 +++ apps/web/content/docs/zen/meta.json | 1 + apps/web/lib/registry/examples.tsx | 2 + apps/web/lib/registry/pending.ts | 1 - apps/web/package.json | 2 + .../registry/zen/examples/chart-example.tsx | 54 +++ apps/web/registry/zen/registry.json | 25 ++ apps/web/registry/zen/ui/chart.tsx | 369 ++++++++++++++++++ apps/web/test/catalog.test.ts | 36 ++ apps/web/test/fixtures/snippets/chart.tsx | 39 ++ apps/web/test/install.test.ts | 10 + bun.lock | 76 +++- 12 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 apps/web/content/docs/zen/chart.mdx create mode 100644 apps/web/registry/zen/examples/chart-example.tsx create mode 100644 apps/web/registry/zen/ui/chart.tsx create mode 100644 apps/web/test/fixtures/snippets/chart.tsx diff --git a/apps/web/content/docs/zen/chart.mdx b/apps/web/content/docs/zen/chart.mdx new file mode 100644 index 0000000..c65bbb1 --- /dev/null +++ b/apps/web/content/docs/zen/chart.mdx @@ -0,0 +1,63 @@ +--- +title: Chart +description: Composes Recharts into a responsive container with themed tooltips and legends. +--- + +Chart is an Authored component: coss/ui has no counterpart, so it takes +shadcn/ui's Structure and wraps [Recharts](https://recharts.org) rather than +copying an upstream file. The container, tooltip and legend reuse kosori's +design vocabulary: the tooltip surface comes from Popover, muted labels use +`text-muted-foreground`, and series colours come from the `--chart-1` to +`--chart-5` Tokens through `ChartStyle`. + +## Installation + +```bash +npx shadcn@latest add @kosori/chart +``` + +## Example + + + +## Usage + +```tsx +'use client'; + +import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart'; + +const chartConfig = { + desktop: { label: 'Desktop', color: 'var(--chart-1)' }, +} satisfies ChartConfig; + +const data = [ + { desktop: 186, month: 'January' }, + { desktop: 305, month: 'February' }, +]; + +const ChartDemo = () => ( + + + + + } /> + } /> + + + +); +``` + +A `ChartConfig` entry labels a series and sets its colour. `ChartStyle` writes +those colours as custom properties on the container, so the same config works in +light and dark without a second definition. Pass `theme` instead of `color` when +a series needs a different value per scheme. diff --git a/apps/web/content/docs/zen/meta.json b/apps/web/content/docs/zen/meta.json index 8954eba..cf500a8 100644 --- a/apps/web/content/docs/zen/meta.json +++ b/apps/web/content/docs/zen/meta.json @@ -18,6 +18,7 @@ "button-group", "calendar", "card", + "chart", "checkbox", "collapsible", "combobox", diff --git a/apps/web/lib/registry/examples.tsx b/apps/web/lib/registry/examples.tsx index 9f5140e..3d9df02 100644 --- a/apps/web/lib/registry/examples.tsx +++ b/apps/web/lib/registry/examples.tsx @@ -19,6 +19,7 @@ import ButtonExample from '@/registry/zen/examples/button-example'; import ButtonGroupExample from '@/registry/zen/examples/button-group-example'; import CalendarExample from '@/registry/zen/examples/calendar-example'; import CardExample from '@/registry/zen/examples/card-example'; +import ChartExample from '@/registry/zen/examples/chart-example'; import CheckboxExample from '@/registry/zen/examples/checkbox-example'; import CollapsibleExample from '@/registry/zen/examples/collapsible-example'; import ComboboxExample from '@/registry/zen/examples/combobox-example'; @@ -90,6 +91,7 @@ const zenExampleComponents = { 'button-group': ButtonGroupExample, calendar: CalendarExample, card: CardExample, + chart: ChartExample, checkbox: CheckboxExample, collapsible: CollapsibleExample, combobox: ComboboxExample, diff --git a/apps/web/lib/registry/pending.ts b/apps/web/lib/registry/pending.ts index 01286ca..f9c0029 100644 --- a/apps/web/lib/registry/pending.ts +++ b/apps/web/lib/registry/pending.ts @@ -7,7 +7,6 @@ export const PENDING_COMPONENTS = [ 'attachment', 'bubble', 'carousel', - 'chart', 'code-block', 'marker', 'message', diff --git a/apps/web/package.json b/apps/web/package.json index f693c8f..82406d2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,7 +26,9 @@ "postcss": "8.5.30", "react": "19.2.8", "react-dom": "19.2.8", + "react-is": "^19.3.1", "react-resizable-panels": "4.14.8", + "recharts": "^3.10.1", "shadcn": "^4.22.0", "tailwindcss": "4.3.3", "tw-animate-css": "^1.4.0" diff --git a/apps/web/registry/zen/examples/chart-example.tsx b/apps/web/registry/zen/examples/chart-example.tsx new file mode 100644 index 0000000..519bbd0 --- /dev/null +++ b/apps/web/registry/zen/examples/chart-example.tsx @@ -0,0 +1,54 @@ +'use client'; + +import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart'; + +const chartData = [ + { desktop: 186, mobile: 80, month: 'January' }, + { desktop: 305, mobile: 200, month: 'February' }, + { desktop: 237, mobile: 120, month: 'March' }, + { desktop: 73, mobile: 190, month: 'April' }, + { desktop: 209, mobile: 130, month: 'May' }, + { desktop: 214, mobile: 140, month: 'June' }, +]; + +const formatMonth = (value: string) => value.slice(0, 3); + +const chartConfig = { + desktop: { + color: 'var(--chart-1)', + label: 'Desktop', + }, + mobile: { + color: 'var(--chart-2)', + label: 'Mobile', + }, +} satisfies ChartConfig; + +const ChartExample = () => ( + + + + + } /> + } /> + + + + +); + +export default ChartExample; diff --git a/apps/web/registry/zen/registry.json b/apps/web/registry/zen/registry.json index b933074..6f29e9c 100644 --- a/apps/web/registry/zen/registry.json +++ b/apps/web/registry/zen/registry.json @@ -333,6 +333,31 @@ } ] }, + { + "name": "chart", + "type": "registry:ui", + "dependencies": ["cn", "react-is", "recharts"], + "files": [ + { + "path": "registry/zen/ui/chart.tsx", + "type": "registry:ui" + } + ], + "meta": { + "authored": true + } + }, + { + "name": "chart-example", + "type": "registry:example", + "registryDependencies": ["@kosori/chart"], + "files": [ + { + "path": "registry/zen/examples/chart-example.tsx", + "type": "registry:example" + } + ] + }, { "name": "checkbox", "type": "registry:ui", diff --git a/apps/web/registry/zen/ui/chart.tsx b/apps/web/registry/zen/ui/chart.tsx new file mode 100644 index 0000000..ef01a9c --- /dev/null +++ b/apps/web/registry/zen/ui/chart.tsx @@ -0,0 +1,369 @@ +'use client'; + +import { cn } from 'cn'; +import type React from 'react'; +import { createContext, useContext, useId, useMemo } from 'react'; +import { + type DefaultLegendContentProps, + type DefaultTooltipContentProps, + Legend, + ResponsiveContainer, + Tooltip, + type TooltipValueType, +} from 'recharts'; + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { dark: '.dark', light: '' } as const; + +const INITIAL_DIMENSION = { height: 200, width: 320 } as const; +type TooltipNameType = number | string; + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode; + icon?: React.ComponentType; + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +>; + +type ChartContextProps = { + config: ChartConfig; +}; + +const ChartContext = createContext(null); + +function useChart() { + const context = useContext(ChartContext); + + if (!context) { + throw new Error('useChart must be used within a '); + } + + return context; +} + +export function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<'div'> & { + config: ChartConfig; + children: React.ComponentProps['children']; + initialDimension?: { + width: number; + height: number; + }; +}) { + const uniqueId = useId(); + const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`; + + return ( + +
+ + + {children} + +
+
+ ); +} + +export const ChartStyle = ({ + id, + config, +}: { + id: string; + config: ChartConfig; +}) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ); + + if (!colorConfig.length) { + return null; + } + + const css = Object.entries(THEMES) + .map( + ([theme, prefix]) => ` +${prefix} [data-chart=${id}] { +${colorConfig + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? + itemConfig.color; + return color ? ` --color-${key}: ${color};` : null; + }) + .join('\n')} +} +` + ) + .join('\n'); + + return ; +}; + +export const ChartTooltip = Tooltip; + +export function ChartTooltipContent({ + active, + payload, + className, + indicator = 'dot', + hideLabel = false, + hideIndicator = false, + label, + labelFormatter, + labelClassName, + formatter, + color, + nameKey, + labelKey, +}: React.ComponentProps & + React.ComponentProps<'div'> & { + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: 'line' | 'dot' | 'dashed'; + nameKey?: string; + labelKey?: string; + } & Omit< + DefaultTooltipContentProps, + 'accessibilityLayer' + >) { + const { config } = useChart(); + + const tooltipLabel = useMemo(() => { + if (hideLabel || !payload?.length) { + return null; + } + + const [item] = payload; + const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const value = + !labelKey && typeof label === 'string' + ? (config[label]?.label ?? label) + : itemConfig?.label; + + if (labelFormatter) { + return ( +
+ {labelFormatter(value, payload)} +
+ ); + } + + if (!value) { + return null; + } + + return ( +
+ {value} +
+ ); + }, [ + label, + labelFormatter, + payload, + hideLabel, + labelClassName, + config, + labelKey, + ]); + + if (!(active && payload?.length)) { + return null; + } + + const nestLabel = payload.length === 1 && indicator !== 'dot'; + + return ( +
+ {!nestLabel && tooltipLabel} +
+ {payload + .filter((item) => item.type !== 'none') + .map((item, index) => { + const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + const indicatorColor = color ?? item.payload?.fill ?? item.color; + + return ( +
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground', + indicator === 'dot' && 'items-center' + )} + key={index} + > + {formatter && item?.value !== undefined && item.name ? ( + formatter(item.value, item.name, item, index, item.payload) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + !hideIndicator && ( +
+ ) + )} +
+
+ {!!nestLabel && tooltipLabel} + + {itemConfig?.label ?? item.name} + +
+ {item.value !== null && item.value !== undefined && ( + + {typeof item.value === 'number' + ? item.value.toLocaleString() + : String(item.value)} + + )} +
+ + )} +
+ ); + })} +
+
+ ); +} + +export const ChartLegend = Legend; + +export function ChartLegendContent({ + className, + hideIcon = false, + payload, + verticalAlign = 'bottom', + nameKey, +}: React.ComponentProps<'div'> & { + hideIcon?: boolean; + nameKey?: string; +} & DefaultLegendContentProps) { + const { config } = useChart(); + + if (!payload?.length) { + return null; + } + + return ( +
+ {payload + .filter((item) => item.type !== 'none') + .map((item, index) => { + const key = `${nameKey ?? item.dataKey ?? 'value'}`; + const itemConfig = getPayloadConfigFromPayload(config, item, key); + + return ( +
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground' + )} + key={index} + > + {itemConfig?.icon && !hideIcon ? ( + + ) : ( +
+ )} + {itemConfig?.label} +
+ ); + })} +
+ ); +} + +function getPayloadConfigFromPayload( + config: ChartConfig, + payload: unknown, + key: string +) { + if (typeof payload !== 'object' || payload === null) { + return; + } + + const payloadPayload = + 'payload' in payload && + typeof payload.payload === 'object' && + payload.payload !== null + ? payload.payload + : undefined; + + let configLabelKey: string = key; + + if ( + key in payload && + typeof payload[key as keyof typeof payload] === 'string' + ) { + configLabelKey = payload[key as keyof typeof payload] as string; + } else if ( + payloadPayload && + key in payloadPayload && + typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' + ) { + configLabelKey = payloadPayload[ + key as keyof typeof payloadPayload + ] as string; + } + + return configLabelKey in config ? config[configLabelKey] : config[key]; +} diff --git a/apps/web/test/catalog.test.ts b/apps/web/test/catalog.test.ts index 265e10e..3b81d6a 100644 --- a/apps/web/test/catalog.test.ts +++ b/apps/web/test/catalog.test.ts @@ -3,6 +3,7 @@ import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { readCatalog } from '../lib/registry/catalog'; +import { parseExports } from '../lib/registry/compatibility'; import { COMPOSITIONS } from '../lib/registry/compositions'; import { PENDING_COMPONENTS } from '../lib/registry/pending'; import { findStyle, STYLES } from '../registry/preset'; @@ -176,6 +177,41 @@ test('the two trivial names are served with an Example and a page', async () => } }); +test('the authored chart serves the shadcn/ui surface with its runtime deps', async () => { + const items = await readCatalog('zen'); + const chart = items.find((item) => item.name === 'chart'); + const example = items.find((item) => item.name === 'chart-example'); + + expect(chart?.type).toBe('registry:ui'); + expect(chart?.meta?.authored).toBe(true); + expect(chart?.dependencies).toContain('recharts'); + expect(chart?.dependencies).toContain('react-is'); + expect(example?.type).toBe('registry:example'); + expect(example?.registryDependencies).toContain('@kosori/chart'); + + const source = await readFile(join(registryDir, 'zen/ui/chart.tsx'), 'utf8'); + const { names } = parseExports(source); + + for (const name of [ + 'ChartContainer', + 'ChartLegend', + 'ChartLegendContent', + 'ChartStyle', + 'ChartTooltip', + 'ChartTooltipContent', + ]) { + expect(names.has(name), `chart does not export ${name}`).toBe(true); + } + expect(source).toContain('export type ChartConfig'); + + const pages = await readPages('zen'); + expect(pages).toContain('chart'); + expect( + existsSync(join(docsDir, 'zen/chart.mdx')), + 'zen has no chart page' + ).toBe(true); +}); + test('an Incomplete Style reports its own scope, not the global Pending list', () => { expect(findStyle('kaze')?.incomplete).toBe(true); expect(findStyle('kaze')?.pending ?? []).toEqual([]); diff --git a/apps/web/test/fixtures/snippets/chart.tsx b/apps/web/test/fixtures/snippets/chart.tsx new file mode 100644 index 0000000..c359d11 --- /dev/null +++ b/apps/web/test/fixtures/snippets/chart.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'; +import { + type ChartConfig, + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, +} from '@/components/ui/chart'; + +const chartConfig = { + desktop: { + color: 'var(--chart-1)', + label: 'Desktop', + }, +} satisfies ChartConfig; + +const chartData = [{ desktop: 186, month: 'January' }]; + +const ChartPage = () => ( + + + + + } /> + } /> + + + +); + +export default ChartPage; diff --git a/apps/web/test/install.test.ts b/apps/web/test/install.test.ts index fe28a77..1d73a49 100644 --- a/apps/web/test/install.test.ts +++ b/apps/web/test/install.test.ts @@ -630,6 +630,16 @@ test('installing a preset and every item of every Style leaves Consumer projects ); } + // A Consumer renders a chart the way shadcn/ui documents it, so the + // Recharts runtime dependency is exercised through the installed source. + if (items.some((item) => item.name === 'chart')) { + await mkdir(join(scratch, 'app/chart'), { recursive: true }); + await cp( + join(webDir, 'test/fixtures/snippets/chart.tsx'), + join(scratch, 'app/chart/page.tsx') + ); + } + // ADR-0003: an install writes only the component directory, the CSS // entry and the dependency manifests. Record the tree before it runs. const before = await readTree(scratch); diff --git a/bun.lock b/bun.lock index 7e531f1..67b4c4f 100644 --- a/bun.lock +++ b/bun.lock @@ -28,7 +28,9 @@ "postcss": "8.5.30", "react": "19.2.8", "react-dom": "19.2.8", + "react-is": "^19.3.1", "react-resizable-panels": "4.14.8", + "recharts": "^3.10.1", "shadcn": "^4.22.0", "tailwindcss": "4.3.3", "tw-animate-css": "^1.4.0", @@ -309,6 +311,8 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.13.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA=="], + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@shadcn/registry": ["@shadcn/registry@0.2.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "cn": "^0.2.4", "code-block-writer": "^13.0.3", "deepmerge": "^4.3.1", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "ora": "^8.2.0", "postcss": "^8.5.23", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "tsconfig-paths": "^4.2.0", "type-fest": "^4.41.0", "undici": "^7.29.1", "zod": "^4.2.0" } }, "sha512-m3TeqU/+av2kgOLfESHkWzsmhuQuY4NXZd5Y1Rd4lzy4IX3JJ5+k2mYfkq6OT8oMuk2W4GQPAkO7+CC7KlpSbw=="], @@ -333,6 +337,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], "@tabler/icons": ["@tabler/icons@3.49.0", "", {}, "sha512-l+GnSkFETx0U26icxv1VUFnxxiTqXfCcVRiRAnyMwibupM2S2L/hSaZ9gmSgz01rkrsH/93NvLsxykqdanc6lw=="], @@ -383,6 +389,24 @@ "@turbo/windows-arm64": ["@turbo/windows-arm64@2.11.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-PgjBpLPW/WDsKzeBmNuw2QHtPxT/PtcloN3rHFTqVyMsqkLlyHVJbSusuhYoYFdi08lZ9Stryw66UOuPg/yW0w=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -405,6 +429,8 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], @@ -585,12 +611,36 @@ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], @@ -645,6 +695,8 @@ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + "es-toolkit": ["es-toolkit@1.52.0", "", {}, "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], @@ -677,6 +729,8 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], @@ -785,10 +839,14 @@ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "immer": ["immer@11.2.7", "", {}, "sha512-zGk7AJPx/dKSYCjdUG0jkY6qLKvCzHMtLjqIEm2LhQJOrpRubIvI71RwsYwQGBfRAaLcJuZU0i4dAF3rxwedOA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "ip-address": ["ip-address@10.7.4", "", {}, "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -1135,6 +1193,10 @@ "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + "react-is": ["react-is@19.3.1", "", {}, "sha512-RRieF6IyxQBkcK6pP8xOHprLTPW6QjRkbGdImtqNme+/hYVwCZkLQLdlZGOhUqXwkS9IEynaGvPJtv6XB/cz0Q=="], + + "react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="], + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], @@ -1147,6 +1209,8 @@ "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], + "recharts": ["recharts@3.10.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^11.1.8", "react-redux": "8.x.x || 9.x.x", "reselect": "5.2.0", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -1155,6 +1219,10 @@ "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + + "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="], @@ -1177,7 +1245,7 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], "resolve.exports": ["resolve.exports@2.0.4", "", {}, "sha512-zepuGL0ulf+nt+XaFrlKeaiTJeyPFPLLY62+ncq1/VTHBdt3HsfXOH60priZ6tnXQozdGcDeIF5JVTanvIcYWQ=="], @@ -1345,6 +1413,8 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], @@ -1385,12 +1455,16 @@ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@base-ui/utils/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + "@reduxjs/toolkit/reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "@shadcn/registry/cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="], "@shadcn/registry/execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],