diff --git a/README.md b/README.md index 223b759..ef4b118 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Below is a list of the components available in this library. Each component has - [BBBScrollArea](./src/components/ScrollArea/README.md) - [BBBSearch](./src/components/Search/README.md) - [BBBSelect](./src/components/Select/README.md) +- [BBBSlider](./src/components/Slider/README.md) - [BBBSpinner](./src/components/Spinner//README.md) - [BBBTextAreaInput](./src/components/TextAreaInput/README.md) - [BBBTextInput](./src/components/TextInput/README.md) diff --git a/package.json b/package.json index bea56d7..6d53f7a 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,13 @@ "require": "./dist/components/Select.js", "default": "./dist/components/Select.js" }, + "./Slider": { + "types": "./dist/types/components/Slider/index.d.ts", + "node": "./dist/components/Slider.js", + "import": "./dist/esm/components/Slider/index.js", + "require": "./dist/components/Slider.js", + "default": "./dist/components/Slider.js" + }, "./Spinner": { "types": "./dist/types/components/Spinner/index.d.ts", "node": "./dist/components/Spinner.js", @@ -211,6 +218,9 @@ "Select": [ "dist/types/components/Select/index.d.ts" ], + "Slider": [ + "dist/types/components/Slider/index.d.ts" + ], "Spinner": [ "dist/types/components/Spinner/index.d.ts" ], diff --git a/src/components/Slider/README.md b/src/components/Slider/README.md new file mode 100644 index 0000000..ae036d9 --- /dev/null +++ b/src/components/Slider/README.md @@ -0,0 +1,137 @@ +# BBBSlider + +The `BBBSlider` component provides a horizontal, single-thumb slider for picking a value inside a +range. It supports labels (`label` and `helperText`), decorative end icons, marks with or without +tick dots, and a value tooltip. + +![Demo](assets/example.png) + +## Usage Example + +### Slider with a label + +```jsx +import { BBBSlider } from 'bbb-ui-components-react'; + + +``` + +### Volume, where the number matters + +```jsx +import { BBBSlider } from 'bbb-ui-components-react'; + + `${value}%`} + getAriaValueText={(value) => `${value} percent`} + dataTest="volume-slider" + onChange={(event, value) => setVolume(value)} +/> +``` + +### Brightness, where the number does not matter + +```jsx +import { BBBSlider } from 'bbb-ui-components-react'; +import { MdBrightnessLow, MdBrightnessHigh } from 'react-icons/md'; + +} + iconEnd={} + valueLabelDisplay="off" + onChange={(event, value) => setBrightness(value)} +/> +``` + +### Discrete stages with labels only + +```jsx +import { BBBSlider } from 'bbb-ui-components-react'; + + setGridSize(value)} +/> +``` + +## Accessibility + +- Give every slider an accessible name: either `label` (which also renders visible text and is + wired to the input with `htmlFor`) or `ariaLabel` when the design has no visible label. +- `helperText` is referenced by the input's `aria-describedby` automatically; `ariaDescribedBy` + overrides it when the description lives elsewhere on the page. +- When `valueLabelFormat` shows something other than the raw value, pass the matching + `getAriaValueText` so screen readers announce what the user sees. +- Keyboard support comes from MUI: arrows move by `step`, Page Up/Page Down jump, Home/End go to + the ends. + +## Testing + +`dataTest` lands on the root element, and the focusable range input gets the same value suffixed +with `-input`. Drive the input, not the root: + +```js +await page.locator('[data-test="volume-slider-input"]').fill('80'); +``` + +## Notes + +The value tooltip is rendered inside the slider, so a container with `overflow: hidden` can clip it +when the slider sits at the very top of the container. Use `valueLabelDisplay="off"` or reserve +vertical space above the slider in that case. + +Mark labels are centred on their tick, so the labels at `min` and `max` extend half their width past +each end of the track. Reserve horizontal padding around the slider, or avoid labelling the extreme +marks, when the container clips. + +`iconStart`/`iconEnd` render at `1.5rem` by default, matching a circle `BBButton`; pass a `size` on +the icon itself to override it. + +## Props + +| Property | Type | Default | Description | +| ------------------- | ------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `label` | `string` | | The main label text displayed above the slider. | +| `helperText` | `string` | | The helper text displayed below the label. | +| `value` | `number` | | The current value; providing it makes the slider controlled. | +| `defaultValue` | `number` | | The initial value used while the slider is uncontrolled. | +| `min` | `number` | `0` | The minimum allowed value. | +| `max` | `number` | `100` | The maximum allowed value. | +| `step` | `number \| null` | `1` | The granularity the value moves by; `null` snaps to `marks`. | +| `marks` | `boolean \| { value: number, label?: ReactNode }[]` | `false` | The marks rendered on the track. | +| `showMarkTicks` | `boolean` | `true` | If `false`, renders the mark labels without their tick dots. | +| `animate` | `boolean` | `true` | If `false`, disables transitions and hover/drag visual effects so state changes render instantly. | +| `valueLabelDisplay` | `'auto' \| 'on' \| 'off'` | `'auto'` | When the value tooltip is shown. | +| `valueLabelFormat` | `string \| ((value: number, index: number) => ReactNode)` | | Formats the value shown in the tooltip. | +| `getAriaValueText` | `(value: number, index: number) => string` | | The text screen readers announce for the current value. | +| `iconStart` | `React.ReactNode` | | A decorative node rendered at the minimum end; it is hidden from screen readers. | +| `iconEnd` | `React.ReactNode` | | A decorative node rendered at the maximum end; it is hidden from screen readers. | +| `disabled` | `boolean` | `false` | If `true`, the slider is disabled and unresponsive. | +| `onChange` | `(event: Event, value: number) => void` | | Fired continuously while the user drags or presses a key. | +| `onChangeCommitted` | `(event: React.SyntheticEvent \| Event, value: number) => void` | | Fired once, when the interaction ends; prefer it for expensive updates. | +| `id` | `string` | | The HTML `id` of the underlying range input; auto-generated when omitted. | +| `dataTest` | `string` | | The `data-test` attribute for the root; the range input gets `${dataTest}-input`. | +| `ariaLabel` | `string` | | The accessible name for the slider. | +| `ariaLabelledBy` | `string` | | The ID of the element that labels the slider; ignored when `ariaLabel` is set, and falls back to the rendered label's id when omitted. | +| `ariaDescribedBy` | `string` | | The ID of the element that describes the slider; falls back to the rendered helper text's id when omitted. | +| `...props` | `MuiSliderProps` | | Any other props are passed down to the underlying Material-UI Slider component. | diff --git a/src/components/Slider/assets/example.png b/src/components/Slider/assets/example.png new file mode 100644 index 0000000..b606678 Binary files /dev/null and b/src/components/Slider/assets/example.png differ diff --git a/src/components/Slider/component.stories.tsx b/src/components/Slider/component.stories.tsx new file mode 100644 index 0000000..c329062 --- /dev/null +++ b/src/components/Slider/component.stories.tsx @@ -0,0 +1,232 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { MdBrightnessLow, MdBrightnessHigh } from 'react-icons/md'; +import BBBSlider from './component'; +import { VALUE_LABEL_DISPLAY_VALUES, DEFAULT_VALUE_LABEL_DISPLAY } from './constants'; + +const meta = { + title: 'BBBSlider', + component: BBBSlider, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text', + description: 'Main label text displayed above the slider.', + }, + helperText: { + control: 'text', + description: 'Helper text displayed below the label.', + }, + min: { + control: 'number', + description: 'Minimum allowed value.', + }, + max: { + control: 'number', + description: 'Maximum allowed value.', + }, + step: { + control: 'number', + description: 'Granularity the value can move by; use null with marks to snap to them.', + }, + marks: { + control: 'object', + description: 'Marks to render on the track; true spreads them by step, or pass an array with labels.', + }, + showMarkTicks: { + control: 'boolean', + description: 'Renders the tick dots of `marks`; set to false to keep the mark labels only.', + table: { defaultValue: { summary: 'true' } }, + }, + animate: { + control: 'boolean', + description: 'Enables transitions and hover/drag visual effects; set to false for instant state changes.', + table: { defaultValue: { summary: 'true' } }, + }, + valueLabelDisplay: { + control: 'select', + options: VALUE_LABEL_DISPLAY_VALUES, + description: 'Controls when the value tooltip is shown.', + table: { defaultValue: { summary: `${DEFAULT_VALUE_LABEL_DISPLAY}` } }, + }, + iconStart: { + control: false, + description: 'Decorative node rendered at the minimum end of the track.', + }, + iconEnd: { + control: false, + description: 'Decorative node rendered at the maximum end of the track.', + }, + valueLabelFormat: { + control: false, + description: 'Formats the value shown in the tooltip; receives the raw value.', + }, + getAriaValueText: { + control: false, + description: 'Returns the text screen readers announce for the current value.', + }, + disabled: { + control: 'boolean', + description: 'Disable the slider when true.', + }, + dataTest: { + control: 'text', + description: 'Value for the data-test attribute on the root; the range input gets `${dataTest}-input`.', + }, + ariaLabel: { + control: 'text', + description: 'Accessible name for the slider.', + }, + onChange: { + control: false, + description: 'Change handler fired continuously while the user drags or presses a key.', + }, + onChangeCommitted: { + control: false, + description: 'Change handler fired once, when the interaction ends.', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** Bare slider with no label, covering the default 0-100 range. */ +export const Default: Story = { + args: { defaultValue: 50, ariaLabel: 'Volume' }, +}; + +/** Visible label and helper text; clicking the label focuses the slider. */ +export const WithLabel: Story = { + args: { label: 'Brightness', helperText: 'Applies to the whole image', defaultValue: 50 }, +}; + +/** Disabled state, which keeps the label readable but blocks interaction. */ +export const Disabled: Story = { + args: { label: 'Brightness', defaultValue: 50, disabled: true }, +}; + +/** Decorative icons at both ends, the brightness pattern where the number itself is irrelevant. */ +export const WithIcons: Story = { + args: { + label: 'Brightness', + iconStart: , + iconEnd: , + valueLabelDisplay: 'off', + min: -100, + max: 100, + defaultValue: 0, + ariaLabel: 'Camera brightness', + }, +}; + +/** Discrete stages with labelled marks, snapping to each step. */ +export const WithMarks: Story = { + args: { + label: 'Webcam grid size', + min: 1, + max: 4, + step: 1, + marks: [ + { value: 1, label: '1x1' }, + { value: 2, label: '2x2' }, + { value: 3, label: '3x3' }, + { value: 4, label: '4x4' }, + ], + defaultValue: 2, + valueLabelDisplay: 'off', + ariaLabel: 'Webcam grid size', + }, + decorators: [ + (StoryComponent) => ( +
+ +
+ ), + ], +}; + +/** Mark labels without the tick dots, reproducing the camera modal's -100/0/100 scale. */ +export const MarksWithoutTicks: Story = { + args: { + label: 'Brightness', + min: -100, + max: 100, + showMarkTicks: false, + marks: [ + { value: -100, label: '-100' }, + { value: 0, label: '0' }, + { value: 100, label: '100' }, + ], + defaultValue: 0, + ariaLabel: 'Camera brightness', + }, + decorators: [ + (StoryComponent) => ( +
+ +
+ ), + ], +}; + +/** Tooltip pinned open, for cases where the number must stay visible while adjusting. */ +export const ValueLabelAlwaysOn: Story = { + args: { label: 'Volume', defaultValue: 70, valueLabelDisplay: 'on', ariaLabel: 'Volume' }, +}; + +/** Percentage formatting through valueLabelFormat, the volume pattern where the number matters. */ +export const PercentageFormat: Story = { + args: { + label: 'Volume', + defaultValue: 70, + valueLabelDisplay: 'on', + valueLabelFormat: (value: number) => `${value}%`, + getAriaValueText: (value: number) => `${value} percent`, + ariaLabel: 'Volume', + }, +}; + +/** Stored value and displayed value differ: the range is 0-200 but the user sees -100 to 100. */ +export const OffsetFormat: Story = { + args: { + label: 'Brightness', + min: 0, + max: 200, + defaultValue: 100, + valueLabelDisplay: 'on', + valueLabelFormat: (value: number) => `${value - 100}`, + getAriaValueText: (value: number) => `${value - 100}`, + ariaLabel: 'Camera brightness', + }, +}; + +/** Animation disabled: dragging the thumb and viewing the tooltip show no transitions or hover glow. */ +export const WithoutAnimation: Story = { + args: { + label: 'Volume', + defaultValue: 70, + valueLabelDisplay: 'on', + animate: false, + ariaLabel: 'Volume', + }, +}; + +/** Same slider under a right-to-left ancestor: the track fills from the right. */ +export const RightToLeft: Story = { + args: { + label: 'Brightness', + min: -100, + max: 100, + defaultValue: 50, + valueLabelDisplay: 'on', + ariaLabel: 'Camera brightness', + }, + decorators: [ + (StoryComponent) => ( +
+ +
+ ), + ], +}; diff --git a/src/components/Slider/component.tsx b/src/components/Slider/component.tsx new file mode 100644 index 0000000..6f21302 --- /dev/null +++ b/src/components/Slider/component.tsx @@ -0,0 +1,110 @@ +import React, { JSX, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'; +import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles'; +import { SliderProps } from './types'; +import * as Styled from './styles'; +import { DEFAULT_ANIMATE, DEFAULT_SHOW_MARK_TICKS, DEFAULT_VALUE_LABEL_DISPLAY } from './constants'; + +/** + * A horizontal, single-thumb slider for picking a value inside a range. + * + * It supports labels (label and helperText), decorative end icons, marks and a value tooltip, + * and keeps its accessible name and description on the underlying range input. + * + */ +function Slider ({ + label, + helperText, + iconStart, + iconEnd, + showMarkTicks = DEFAULT_SHOW_MARK_TICKS, + animate = DEFAULT_ANIMATE, + valueLabelDisplay = DEFAULT_VALUE_LABEL_DISPLAY, + onChange, + onChangeCommitted, + id: providedId, + dataTest, + ariaLabel, + ariaLabelledBy, + ariaDescribedBy, + slotProps, + ...rest +}: SliderProps): JSX.Element { + const generatedUniqueId = useId(); + const id = providedId ?? generatedUniqueId; + const labelId = label ? `${id}-label` : undefined; + const helperTextId = helperText ? `${id}-helper` : undefined; + + const wrapperRef = useRef(null); + const [isRtl, setIsRtl] = useState(false); + + useEffect(() => { + const wrapper = wrapperRef.current; + if (!wrapper) return undefined; + + const readDirection = () => { + setIsRtl(window.getComputedStyle(wrapper).direction === 'rtl'); + }; + + readDirection(); + const observer = new MutationObserver(readDirection); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] }); + + return () => observer.disconnect(); + }, []); + + const outerTheme = useTheme(); + const theme = useMemo( + () => (isRtl && outerTheme.direction !== 'rtl' + ? createTheme(outerTheme, { direction: 'rtl' }) + : outerTheme), + [outerTheme, isRtl], + ); + + const handleChange = useCallback((event: Event, value: number | number[]) => { + onChange?.(event, Array.isArray(value) ? value[0] : value); + }, [onChange]); + + const handleChangeCommitted = useCallback(( + event: React.SyntheticEvent | Event, + value: number | number[], + ) => { + onChangeCommitted?.(event, Array.isArray(value) ? value[0] : value); + }, [onChangeCommitted]); + + return ( + + {(label || helperText) && ( + + {label && {label}} + {helperText && {helperText}} + + )} + + {iconStart && {iconStart}} + + + + {iconEnd && {iconEnd}} + + + ); +} + +export default Slider; diff --git a/src/components/Slider/constants.ts b/src/components/Slider/constants.ts new file mode 100644 index 0000000..628d7fa --- /dev/null +++ b/src/components/Slider/constants.ts @@ -0,0 +1,10 @@ +export const VALUE_LABEL_DISPLAYS = { + AUTO: 'auto', + ON: 'on', + OFF: 'off', +} as const; + +export const VALUE_LABEL_DISPLAY_VALUES = Object.values(VALUE_LABEL_DISPLAYS); +export const DEFAULT_VALUE_LABEL_DISPLAY = VALUE_LABEL_DISPLAYS.AUTO; +export const DEFAULT_SHOW_MARK_TICKS = true; +export const DEFAULT_ANIMATE = true; diff --git a/src/components/Slider/index.ts b/src/components/Slider/index.ts new file mode 100644 index 0000000..58fd15e --- /dev/null +++ b/src/components/Slider/index.ts @@ -0,0 +1 @@ +export { default as BBBSlider } from './component'; diff --git a/src/components/Slider/styles.ts b/src/components/Slider/styles.ts new file mode 100644 index 0000000..edd3929 --- /dev/null +++ b/src/components/Slider/styles.ts @@ -0,0 +1,167 @@ +import { Slider } from '@mui/material'; +import { styled as materialStyled } from '@mui/material/styles'; +import styled from 'styled-components'; +import { + colorBrand1, + colorNeutral3, + colorWhite, + colorShadowDefault, + colorTextDefault, + colorTextLight, + colorIconDefault, + colorBorderFocusRing, +} from '../../stylesheets/palette'; +import { + fontSizeXSmall, + fontSizeDefault, + fontSizeBig, + fontWeightDefault, + fontWeightLight, +} from '../../stylesheets/typography'; +import { + spacingXSmall, + spacingSmall, + spacingXLarge, + borderRadiusSmall, +} from '../../stylesheets/sizing'; +import { StyledSliderRowProps, StyledSliderWrapperProps } from './types'; + +const THUMB_SIZE = '1rem'; +const TRACK_HEIGHT = '0.3125rem'; +const MARK_SIZE = '0.25rem'; +const ICON_SIZE = '1.5rem'; + +export const SliderWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + ${({ $valueLabelSpace }) => $valueLabelSpace && ` + gap: ${spacingXLarge}; + `} +`; + +export const TextWrapper = styled.label` + display: flex; + flex-direction: column; + cursor: pointer; +`; + +export const Title = styled.span` + color: ${colorTextDefault}; + font-size: ${fontSizeBig}; + font-weight: ${fontWeightLight}; +`; + +export const HelperText = styled.span` + color: ${colorTextLight}; + font-size: ${fontSizeDefault}; + font-weight: ${fontWeightLight}; +`; + +export const SliderRow = styled.div` + display: flex; + align-items: center; + gap: ${spacingSmall}; + width: 100%; + + ${({ $showMarkTicks }) => !$showMarkTicks && ` + & .MuiSlider-mark { + display: none; + } + `} + + ${({ $animate }) => !$animate && ` + & .MuiSlider-thumb, + & .MuiSlider-track, + & .MuiSlider-valueLabel { + transition: none; + } + + & .MuiSlider-thumb:hover:not(.Mui-focusVisible), + & .MuiSlider-thumb.Mui-active:not(.Mui-focusVisible) { + box-shadow: 0 2px 4px 0 ${colorShadowDefault}; + } + `} + + ${({ $isRtl }) => $isRtl && ` + /* MUI only mirrors right:-anchored positioning under RTL, not the LTR transforms paired + with it (that half normally comes from stylis-plugin-rtl, which this library can't add) */ + && .MuiSlider-thumb { + transform: translate(50%, -50%); + } + + && .MuiSlider-mark { + transform: translate(1px, -50%); + } + + && .MuiSlider-markLabel { + transform: translateX(50%); + } + `} +`; + +export const IconWrapper = styled.span` + display: flex; + align-items: center; + color: ${colorIconDefault}; + font-size: ${ICON_SIZE}; +`; + +export const MaterialSlider = materialStyled(Slider)({ + color: colorBrand1, + height: TRACK_HEIGHT, + '& .MuiSlider-rail': { + backgroundColor: 'currentColor', + opacity: 0.38, + }, + '& .MuiSlider-track': { + border: '1px solid currentColor', + backgroundColor: colorBrand1, + }, + '& .MuiSlider-thumb': { + height: THUMB_SIZE, + width: THUMB_SIZE, + backgroundColor: 'currentColor', + boxShadow: `0 2px 4px 0 ${colorShadowDefault}`, + '&:hover, &.Mui-focusVisible': { + boxShadow: `0 0 0 0.5rem ${colorBorderFocusRing}`, + }, + '&.Mui-active': { + boxShadow: `0 0 0 0.875rem ${colorBorderFocusRing}`, + }, + }, + '& .MuiSlider-mark': { + width: MARK_SIZE, + height: MARK_SIZE, + borderRadius: '50%', + backgroundColor: colorNeutral3, + '&.MuiSlider-markActive': { + backgroundColor: colorWhite, + }, + }, + '& .MuiSlider-markLabel': { + color: colorTextLight, + fontSize: fontSizeXSmall, + // RTL: resolve each label's own direction so a leading minus sign doesn't get reordered + unicodeBidi: 'plaintext', + }, + '& .MuiSlider-valueLabel': { + backgroundColor: colorTextDefault, + color: colorWhite, + fontSize: fontSizeXSmall, + fontWeight: fontWeightDefault, + borderRadius: borderRadiusSmall, + padding: `${spacingXSmall} ${spacingSmall}`, + '& .MuiSlider-valueLabelLabel': { + // RTL: resolve each label's own direction so a leading minus sign doesn't get reordered + unicodeBidi: 'plaintext', + }, + }, + '&.Mui-disabled': { + color: colorNeutral3, + '& .MuiSlider-track': { + backgroundColor: colorNeutral3, + }, + }, +}); diff --git a/src/components/Slider/types.ts b/src/components/Slider/types.ts new file mode 100644 index 0000000..6e1a6a7 --- /dev/null +++ b/src/components/Slider/types.ts @@ -0,0 +1,67 @@ +import React from 'react'; +import { SliderProps as MuiSliderProps } from '@mui/material'; +import { VALUE_LABEL_DISPLAY_VALUES } from './constants'; + +export type ValueLabelDisplay = typeof VALUE_LABEL_DISPLAY_VALUES[number]; + +export interface StyledSliderRowProps { + $showMarkTicks: boolean; + $animate: boolean; + $isRtl: boolean; +} + +export interface StyledSliderWrapperProps { + $valueLabelSpace: boolean; +} + +export interface SliderProps extends Omit { + /** Main label text displayed above the slider. */ + label?: string; + + /** Helper text displayed below the label. */ + helperText?: string; + + /** Current value; providing it makes the slider controlled. */ + value?: number; + + /** Initial value used while the slider is uncontrolled. */ + defaultValue?: number; + + /** Change handler fired continuously while the user drags or presses a key. */ + onChange?: (event: Event, value: number) => void; + + /** Change handler fired once, when the interaction ends; prefer it for expensive updates. */ + onChangeCommitted?: (event: React.SyntheticEvent | Event, value: number) => void; + + /** Decorative node rendered at the minimum end of the track. */ + iconStart?: React.ReactNode; + + /** Decorative node rendered at the maximum end of the track. */ + iconEnd?: React.ReactNode; + + /** Controls when the value tooltip is shown. @default 'auto' */ + valueLabelDisplay?: ValueLabelDisplay; + + /** Renders the tick dots of `marks`; set to false to keep the mark labels only. @default true */ + showMarkTicks?: boolean; + + /** Enables transitions and hover/drag visual effects; set to false for instant state changes. @default true */ + animate?: boolean; + + /** HTML `id` applied to the underlying range input; auto-generated via `useId` when omitted. */ + id?: string; + + /** Value for the `data-test` attribute on the root; the range input gets `${dataTest}-input`. */ + dataTest?: string; + + /** Accessible name for the slider. */ + ariaLabel?: string; + + /** ID of the element that labels the slider; ignored when `ariaLabel` is set, and falls back to the rendered label's id when omitted. */ + ariaLabelledBy?: string; + + /** ID of the element that describes the slider; falls back to the rendered helper text's id when omitted. */ + ariaDescribedBy?: string; +} diff --git a/src/components/index.ts b/src/components/index.ts index 230d706..6c16524 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -9,6 +9,7 @@ export { BBBNavigation } from './Navigation'; export { BBBScrollArea } from './ScrollArea'; export { BBBSearch } from './Search'; export { BBBSelect } from './Select'; +export { BBBSlider } from './Slider'; export { BBBSpinner } from './Spinner'; export { BBBTextAreaInput } from './TextAreaInput'; export { BBBTextInput } from './TextInput'; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 461ae7f..49b575f 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -13,6 +13,7 @@ export default { ScrollArea: './src/components/ScrollArea/index.ts', Search: './src/components/Search/index.ts', Select: './src/components/Select/index.ts', + Slider: './src/components/Slider/index.ts', Spinner: './src/components/Spinner/index.ts', TextAreaInput: './src/components/TextAreaInput/index.ts', TextInput: './src/components/TextInput/index.ts',