Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start creating Slider component(s) #314

Merged
merged 25 commits into from
Aug 22, 2024
Merged
6 changes: 6 additions & 0 deletions .changeset/pink-dancers-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@spear-ai/storybook": minor
"@spear-ai/ui": minor
---

Created Slider component(s).
9 changes: 6 additions & 3 deletions packages/storybook/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { usePreviousDistinct } from "@react-hookz/web";
import { ThemeProvider } from "next-themes";
import { ReactNode, useEffect } from "react";
import { I18nProvider } from "react-aria-components";
import { IntlProvider, ReactIntlErrorCode } from "react-intl";
import { StorybookProvider } from "@/components/storybook-provider/storybook-provider";

Expand Down Expand Up @@ -38,9 +39,11 @@ export const AppProviders = (properties: {
}
}}
>
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>
<I18nProvider locale={direction === "ltr" ? "en" : "ar"}>
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>
</I18nProvider>
</IntlProvider>
</StorybookProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const PreviewIconButton = (properties: {
return (
<TooltipTrigger>
<LinkIconButton
aria-label={intl.formatMessage({ defaultMessage: "Fusce dignissim", id: "uEMDBb" })}
className={`${size} ${rounded}`}
color={color}
href="https://ui.spear.ai"
Expand Down Expand Up @@ -73,7 +72,6 @@ const PreviewIconButton = (properties: {
return (
<TooltipTrigger>
<IconButton
aria-label={intl.formatMessage({ defaultMessage: "Fusce dignissim", id: "uEMDBb" })}
className={`${size} ${rounded}`}
color={color}
isDisabled={isDisabled}
Expand Down
256 changes: 256 additions & 0 deletions packages/storybook/src/components/slider/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { SpeakerLoudIcon, SpeakerModerateIcon } from "@radix-ui/react-icons";
import { IconButton, IconButtonIcon } from "@spear-ai/ui/components/icon-button";
import {
Slider,
SliderDescription,
SliderFill,
SliderLabel,
SliderLabels,
SliderOutput,
SliderThumb,
SliderTrack,
SliderTrackGroup,
} from "@spear-ai/ui/components/slider";
import type { Meta, StoryObj } from "@storybook/react";
import { useCallback, useContext, useMemo } from "react";
import { Form, SliderStateContext } from "react-aria-components";
import { useIntl } from "react-intl";
import { getDefaultSliderValue } from "@/helpers/get-default-slider-value";
import { getUniqueSliderKey } from "@/helpers/get-unique-slider-key";

const PreviewSliderDecrementButton = () => {
const state = useContext(SliderStateContext);
// eslint-disable-next-line @typescript-eslint/unbound-method
const { decrementThumb } = state;

const handlePress = useCallback(() => {
decrementThumb(0);
decrementThumb(1);
}, [decrementThumb]);

return (
<IconButton
className="relative rounded-full group-data-[orientation=vertical]:order-last group-data-[orientation=horizontal]:-ms-2 group-data-[orientation=horizontal]:me-0.5"
onPress={handlePress}
variant="ghost"
>
<IconButtonIcon asChild>
<SpeakerModerateIcon />
</IconButtonIcon>
</IconButton>
);
};

const PreviewSliderIncrementButton = () => {
const state = useContext(SliderStateContext);
// eslint-disable-next-line @typescript-eslint/unbound-method
const { incrementThumb } = state;

const handlePress = useCallback(() => {
incrementThumb(0);
incrementThumb(1);
}, [incrementThumb]);

return (
<IconButton
className="relative rounded-full group-data-[orientation=vertical]:order-first group-data-[orientation=horizontal]:-me-2 group-data-[orientation=horizontal]:ms-0.5"
onPress={handlePress}
variant="ghost"
>
<IconButtonIcon asChild>
<SpeakerLoudIcon />
</IconButtonIcon>
</IconButton>
);
};

const PreviewSlider = (properties: {
color: "neutral" | "primary";
hasEndIcon: boolean;
hasFill: boolean;
hasLabel: boolean;
hasLabelDescription: boolean;
hasOrigin: boolean;
hasStartIcon: boolean;
hasValence: boolean;
isDisabled: boolean;
isRange: boolean;
isSquished: boolean;
maxValue: number;
minValue: number;
orientation: "horizontal" | "vertical";
originValue: number;
step: number;
thumbShape: "circle" | "pill" | "square";
variant: "soft" | "surface";
}) => {
const {
color,
hasEndIcon,
hasFill,
hasLabel,
hasLabelDescription,
hasOrigin,
hasStartIcon,
hasValence,
isDisabled,
isRange,
isSquished,
maxValue,
minValue,
orientation,
originValue,
step,
thumbShape,
variant,
} = properties;
const intl = useIntl();

let thumbShapeClassName = "";

switch (thumbShape) {
case "pill": {
thumbShapeClassName = "h-5 w-2.5 group-data-[orientation=vertical]:rotate-90";
break;
}
case "square": {
thumbShapeClassName = "rounded-sm";
break;
}
default: {
break;
}
}

const defaultValue = useMemo(
() =>
getDefaultSliderValue({
isRange,
maxValue,
minValue,
}),
[isRange, maxValue, minValue],
);

const key = useMemo(
() =>
getUniqueSliderKey({
isRange,
maxValue,
minValue,
}),
[isRange, maxValue, minValue],
);

return (
<div className={`w-full ${isSquished ? "max-w-36" : "max-w-xs"}`}>
<Form className="relative w-full">
<Slider
className="w-full"
color={color}
defaultValue={defaultValue}
hasValence={hasValence}
isDisabled={isDisabled}
key={key}
maxValue={maxValue}
minValue={minValue}
orientation={orientation}
originValue={hasOrigin ? originValue : minValue}
step={step}
variant={variant}
>
<SliderLabels>
{hasLabel ? (
<SliderLabel>
{intl.formatMessage({
defaultMessage: "Volume",
id: "y867Vs",
})}
</SliderLabel>
) : null}
{hasLabel && hasLabelDescription ? (
<SliderDescription>
{intl.formatMessage({
defaultMessage: "How loud until the neighbors complain.",
id: "bEWLDO",
})}
</SliderDescription>
) : null}
{orientation === "horizontal" ? (
<SliderOutput>
{({ state }) =>
state.values.length === 2
? `${state.getThumbValue(0)}–${state.getThumbValue(1)} dB`
: `${state.getThumbValue(0)} dB`
}
</SliderOutput>
) : null}
</SliderLabels>
<SliderTrackGroup className="group-data-[orientation=vertical]:h-56">
{hasStartIcon ? <PreviewSliderDecrementButton /> : null}
<SliderTrack className="flex-1">
{hasFill ? <SliderFill /> : null}
<SliderThumb className={thumbShapeClassName} index={0} />
{isRange ? <SliderThumb className={thumbShapeClassName} index={1} /> : null}
</SliderTrack>
{hasEndIcon ? <PreviewSliderIncrementButton /> : null}
</SliderTrackGroup>
</Slider>
</Form>
</div>
);
};

const meta = {
component: PreviewSlider,
} satisfies Meta<typeof PreviewSlider>;

type Story = StoryObj<typeof meta>;

export const Standard: Story = {
args: {
color: "neutral",
hasEndIcon: true,
hasFill: true,
hasLabel: true,
hasLabelDescription: true,
hasOrigin: false,
hasStartIcon: true,
hasValence: false,
isDisabled: false,
isRange: false,
isSquished: false,
maxValue: 100,
minValue: 0,
orientation: "horizontal",
originValue: 0,
step: 1,
thumbShape: "circle",
variant: "surface",
},
argTypes: {
color: {
control: {
type: "select",
},
options: ["neutral", "primary"],
},
orientation: {
control: { type: "select" },
options: ["horizontal", "vertical"],
},
thumbShape: {
control: { type: "select" },
options: ["circle", "pill", "square"],
},
variant: {
control: { type: "select" },
options: ["soft", "surface"],
},
},
parameters: {
layout: "centered",
},
};

export default meta;
Loading