Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion js/packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"type": "module",
"name": "@crayonai/react-ui",
"license": "MIT",
"version": "0.8.3",
"version": "0.8.4",
"description": "Component library for Generative UI SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -536,9 +536,7 @@ export const LargeDatasetDemo: Story = {
Complete breakdown of 15 compensation categories with carousel navigation
</p>
</div>
<Card style={{ width: "auto", height: "300px", padding: "24px" }}>
<RadialChart {...args} />
</Card>
<RadialChart {...args} />
<div
style={{
marginTop: "16px",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const formatPercentage = (value: number, total: number): string => {
const ITEM_HEIGHT = 36; // Height of each legend item
const ITEM_GAP = 2; // Gap between items
const LEGEND_ITEM_LIMIT = 6;
const SHOW_MORE_BREAKPOINT = 450;

export const StackedLegend = ({
items,
Expand All @@ -42,7 +43,9 @@ export const StackedLegend = ({
const [showAll, setShowAll] = useState(false);
const [isOverflowing, setIsOverflowing] = useState(false);

const isShowMoreLayout = containerWidth !== undefined && items.length > LEGEND_ITEM_LIMIT;
const isShowMoreLayout =
containerWidth !== undefined &&
(containerWidth < SHOW_MORE_BREAKPOINT || items.length > LEGEND_ITEM_LIMIT);

const handleMouseEnter = (key: string, index: number) => {
onItemHover?.(key);
Expand All @@ -62,7 +65,7 @@ export const StackedLegend = ({
const overflowing = scrollHeight > clientHeight;
setIsOverflowing(overflowing);
setShowUpButton(scrollTop > 0);
setShowDownButton(scrollTop < scrollHeight - clientHeight - 1); // -1 for rounding errors
setShowDownButton(scrollTop < scrollHeight - clientHeight - 1);
}
};

Expand Down Expand Up @@ -175,7 +178,7 @@ export const StackedLegend = ({
</div>
))}
</div>
{isShowMoreLayout && !showAll && (
{isShowMoreLayout && !showAll && items.length > LEGEND_ITEM_LIMIT && (
<Button
variant="secondary"
size="small"
Expand All @@ -185,7 +188,7 @@ export const StackedLegend = ({
Show more
</Button>
)}
{isShowMoreLayout && showAll && (
{isShowMoreLayout && showAll && items.length > LEGEND_ITEM_LIMIT && (
<Button
variant="secondary"
size="small"
Expand Down
84 changes: 43 additions & 41 deletions js/packages/react-ui/src/components/Slider/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import * as SliderPrimitive from "@radix-ui/react-slider";
import clsx from "clsx";
import { forwardRef, ReactNode, useMemo, useState } from "react";

export interface SliderProps extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
variant: "continuous" | "discrete" | "range";
export interface SliderProps
extends Omit<
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
"value" | "defaultValue"
> {
variant: "continuous" | "discrete";
min: number;
max: number;
step?: number;
disabled?: boolean;
value?: number[];
defaultValue?: number[];
className?: string;
style?: React.CSSProperties;
Expand All @@ -24,8 +29,8 @@ export const Slider = forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>
step,
disabled,
value,
onValueChange,
defaultValue,
onValueChange,
className,
style,
leftContent,
Expand All @@ -35,49 +40,47 @@ export const Slider = forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>
ref,
) => {
// used to show the correct value on thumb
const [internalValue, setInternalValue] = useState(value || defaultValue);
const valueToShow = value || internalValue;
const [internalValue, setInternalValue] = useState(
defaultValue && defaultValue.length > 0 ? defaultValue : [min],
);

const isControlled = value !== undefined;
const valueToShow = isControlled ? value : internalValue;

const isRange = valueToShow && valueToShow.length > 1;

const thumbs = useMemo(() => {
const thumbClass = "slider-thumb-handle";
const valueIndicatorClass = "slider-thumb-value";

return variant === "range" ? (
<>
<SliderPrimitive.Thumb className="slider-thumb">
<div className={thumbClass}>
<div className="slider-thumb-handle-inner">
<div className="slider-thumb-handle-inner-dot" />
</div>
{!disabled && (
<div className={valueIndicatorClass}>{valueToShow && valueToShow[0]}</div>
)}
</div>
</SliderPrimitive.Thumb>
<SliderPrimitive.Thumb className="slider-thumb">
<div className={thumbClass}>
<div className="slider-thumb-handle-inner">
<div className="slider-thumb-handle-inner-dot" />
</div>
{!disabled && (
<div className={valueIndicatorClass}>{valueToShow && valueToShow[1]}</div>
)}
</div>
</SliderPrimitive.Thumb>
</>
) : (
if (isRange) {
return (
<>
{valueToShow?.map((v, i) => (
<SliderPrimitive.Thumb key={i} className="slider-thumb">
<div className={thumbClass}>
<div className="slider-thumb-handle-inner">
<div className="slider-thumb-handle-inner-dot" />
</div>
{!disabled && <div className={valueIndicatorClass}>{v}</div>}
</div>
</SliderPrimitive.Thumb>
))}
</>
);
}

return (
<SliderPrimitive.Thumb className="slider-thumb">
<div className={thumbClass}>
<div className="slider-thumb-handle-inner">
<div className="slider-thumb-handle-inner-dot" />
</div>
{!disabled && (
<div className={valueIndicatorClass}>{valueToShow && valueToShow[0]}</div>
)}
{!disabled && <div className={valueIndicatorClass}>{valueToShow?.[0]}</div>}
</div>
</SliderPrimitive.Thumb>
);
}, [variant, defaultValue, disabled, valueToShow]);
}, [disabled, valueToShow, isRange]);

const renderDots = () => {
if (variant === "discrete" && step) {
Expand All @@ -102,23 +105,22 @@ export const Slider = forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>
min={min}
max={max}
step={step}
value={value}
onValueChange={(value) => {
setInternalValue(value);
onValueChange?.(value);
value={valueToShow}
onValueChange={(val) => {
if (!isControlled) {
setInternalValue(val);
}
onValueChange?.(val);
}}
minStepsBetweenThumbs={1}
disabled={disabled}
key={variant}
defaultValue={defaultValue}
style={style}
>
<SliderPrimitive.Track className="slider-track">
<SliderPrimitive.Range
className={clsx("slider-range", {
"slider-range--at-min":
(variant === "continuous" || variant === "discrete") &&
valueToShow?.[0] === min,
"slider-range--at-min": !isRange && valueToShow?.[0] === min,
})}
/>
{variant === "discrete" && renderDots()}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Volume1Icon, Volume2Icon } from "lucide-react";
import { useState } from "react";
import { Slider, SliderProps } from "../Slider";

const meta: Meta<SliderProps> = {
Expand All @@ -18,13 +19,13 @@ const meta: Meta<SliderProps> = {
argTypes: {
variant: {
control: false,
options: ["continuous", "discrete", "range"],
options: ["continuous", "discrete"],
description:
"The type of slider - continuous for smooth sliding, discrete for stepped values, or range for selecting a range",
"The type of slider - continuous for smooth sliding, or discrete for stepped values. Range functionality is enabled by passing an array with multiple values to `value` or `defaultValue`.",
defaultValue: "continuous",
table: {
category: "Appearance",
type: { summary: "'continuous' | 'discrete' | 'range'" },
type: { summary: "'continuous' | 'discrete'" },
required: true,
},
},
Expand Down Expand Up @@ -143,9 +144,7 @@ export const Continuous: Story = {
variant: "continuous",
min: 0,
max: 100,
step: 1,
defaultValue: [25],
disabled: false,
},
parameters: {
docs: {
Expand Down Expand Up @@ -176,11 +175,10 @@ export const Discrete: Story = {

export const Range: Story = {
args: {
variant: "range",
variant: "continuous",
min: 0,
max: 100,
step: 1,
defaultValue: [25, 75],
defaultValue: [20, 80],
},
parameters: {
docs: {
Expand All @@ -191,21 +189,83 @@ export const Range: Story = {
},
};

export const WithLeftAndRightContent: Story = {
export const DiscreteRange: Story = {
args: {
variant: "discrete",
min: 0,
max: 100,
step: 10,
defaultValue: [20, 80],
},
parameters: {
docs: {
description: {
story:
"A discrete range slider combines the features of a discrete slider and a range slider.",
},
},
},
};

export const Controlled: Story = {
render: () => {
const [value, setValue] = useState([50]);
return (
<Slider
variant="continuous"
min={0}
max={100}
step={1}
value={value}
onValueChange={setValue}
disabled={false}
/>
);
},
parameters: {
docs: {
description: {
story:
"A controlled slider where the value is managed by component state via the `value` prop.",
},
},
},
name: "Controlled (with value prop)",
};

export const UncontrolledWithoutDefault: Story = {
name: "Without Value or DefaultValue",
args: {
variant: "continuous",
min: 0,
max: 100,
step: 1,
defaultValue: [25],
disabled: false,
},
parameters: {
docs: {
description: {
story:
"A slider without an initial `value` or `defaultValue` prop. It defaults to the minimum value.",
},
},
},
};

export const WithIcons: Story = {
name: "With Icons",
args: {
variant: "continuous",
min: 0,
max: 100,
defaultValue: [40],
leftContent: <Volume1Icon />,
rightContent: <Volume2Icon />,
},
parameters: {
docs: {
description: {
story: "A slider with left and right content",
story: "A slider can have icons or other content on either side.",
},
},
},
Expand Down