@spark-ui/components 17.4.2 → 17.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accordion/index.js.map +1 -1
- package/dist/accordion/index.mjs.map +1 -1
- package/dist/alert-dialog/index.js.map +1 -1
- package/dist/alert-dialog/index.mjs.map +1 -1
- package/dist/avatar/index.js +1 -1
- package/dist/avatar/index.js.map +1 -1
- package/dist/avatar/index.mjs +8 -2
- package/dist/avatar/index.mjs.map +1 -1
- package/dist/badge/index.js.map +1 -1
- package/dist/badge/index.mjs.map +1 -1
- package/dist/breadcrumb/index.js.map +1 -1
- package/dist/breadcrumb/index.mjs.map +1 -1
- package/dist/button-B-sMnDc_.js.map +1 -1
- package/dist/button-C6nlNPdv.mjs.map +1 -1
- package/dist/card/index.js.map +1 -1
- package/dist/card/index.mjs.map +1 -1
- package/dist/carousel/index.js.map +1 -1
- package/dist/carousel/index.mjs.map +1 -1
- package/dist/checkbox-DjwbAH09.js.map +1 -1
- package/dist/checkbox-xsURzANi.mjs.map +1 -1
- package/dist/chip/index.js.map +1 -1
- package/dist/chip/index.mjs.map +1 -1
- package/dist/circular-meter/index.js.map +1 -1
- package/dist/circular-meter/index.mjs.map +1 -1
- package/dist/collapsible/index.js.map +1 -1
- package/dist/collapsible/index.mjs.map +1 -1
- package/dist/combobox/index.js.map +1 -1
- package/dist/combobox/index.mjs.map +1 -1
- package/dist/dialog/index.js.map +1 -1
- package/dist/dialog/index.mjs.map +1 -1
- package/dist/divider/index.js.map +1 -1
- package/dist/divider/index.mjs.map +1 -1
- package/dist/drawer/index.js.map +1 -1
- package/dist/drawer/index.mjs.map +1 -1
- package/dist/dropdown/index.js.map +1 -1
- package/dist/dropdown/index.mjs.map +1 -1
- package/dist/file-upload/index.js.map +1 -1
- package/dist/file-upload/index.mjs.map +1 -1
- package/dist/form-field-81wzFxM0.js.map +1 -1
- package/dist/form-field-GTAuK_nO.mjs.map +1 -1
- package/dist/icon-CRPcdgYp.js.map +1 -1
- package/dist/icon-D05Uqh8_.mjs.map +1 -1
- package/dist/icon-button-CYz_Fitz.js.map +1 -1
- package/dist/icon-button-DpucUC_L.mjs.map +1 -1
- package/dist/input-BUSYZ_VO.js.map +1 -1
- package/dist/input-CiWFuTs_.mjs.map +1 -1
- package/dist/input-otp/index.js.map +1 -1
- package/dist/input-otp/index.mjs.map +1 -1
- package/dist/kbd/index.js.map +1 -1
- package/dist/kbd/index.mjs.map +1 -1
- package/dist/label-BCSEss4U.js.map +1 -1
- package/dist/label-DDBRKLUX.mjs.map +1 -1
- package/dist/link-box/index.js.map +1 -1
- package/dist/link-box/index.mjs.map +1 -1
- package/dist/meter/index.js.map +1 -1
- package/dist/meter/index.mjs.map +1 -1
- package/dist/pagination/index.js.map +1 -1
- package/dist/pagination/index.mjs.map +1 -1
- package/dist/popover-CrKp_TKk.js.map +1 -1
- package/dist/popover-DsBY8eYl.mjs.map +1 -1
- package/dist/portal/index.js.map +1 -1
- package/dist/portal/index.mjs.map +1 -1
- package/dist/progress-BjqJSRnK.js.map +1 -1
- package/dist/progress-C3w4PmxY.mjs.map +1 -1
- package/dist/progress-tracker/index.js.map +1 -1
- package/dist/progress-tracker/index.mjs.map +1 -1
- package/dist/radio-group/index.js.map +1 -1
- package/dist/radio-group/index.mjs.map +1 -1
- package/dist/rating/index.js.map +1 -1
- package/dist/rating/index.mjs.map +1 -1
- package/dist/rating-display/index.js.map +1 -1
- package/dist/rating-display/index.mjs.map +1 -1
- package/dist/scrolling-list/index.js.map +1 -1
- package/dist/scrolling-list/index.mjs.map +1 -1
- package/dist/segmented-control/index.js.map +1 -1
- package/dist/segmented-control/index.mjs.map +1 -1
- package/dist/segmented-gauge/index.js.map +1 -1
- package/dist/segmented-gauge/index.mjs.map +1 -1
- package/dist/select/index.js.map +1 -1
- package/dist/select/index.mjs.map +1 -1
- package/dist/skeleton/index.js.map +1 -1
- package/dist/skeleton/index.mjs.map +1 -1
- package/dist/slider/index.js.map +1 -1
- package/dist/slider/index.mjs.map +1 -1
- package/dist/slot/index.js.map +1 -1
- package/dist/slot/index.mjs.map +1 -1
- package/dist/spinner-DFUoYvmm.js.map +1 -1
- package/dist/spinner-DULLiM6a.mjs.map +1 -1
- package/dist/src/accordion/index.d.mts +3 -0
- package/dist/src/accordion/index.d.ts +3 -0
- package/dist/src/alert-dialog/index.d.mts +3 -0
- package/dist/src/alert-dialog/index.d.ts +3 -0
- package/dist/src/avatar/index.d.mts +7 -5
- package/dist/src/avatar/index.d.ts +7 -5
- package/dist/src/badge/Badge.d.ts +3 -0
- package/dist/src/breadcrumb/index.d.mts +3 -0
- package/dist/src/breadcrumb/index.d.ts +3 -0
- package/dist/src/button/Button.d.ts +3 -0
- package/dist/src/card/index.d.mts +3 -0
- package/dist/src/card/index.d.ts +3 -0
- package/dist/src/carousel/index.d.mts +3 -0
- package/dist/src/carousel/index.d.ts +3 -0
- package/dist/src/checkbox/Checkbox.d.ts +3 -0
- package/dist/src/checkbox/CheckboxGroup.d.ts +4 -0
- package/dist/src/chip/index.d.mts +3 -0
- package/dist/src/chip/index.d.ts +3 -0
- package/dist/src/circular-meter/index.d.mts +3 -0
- package/dist/src/circular-meter/index.d.ts +3 -0
- package/dist/src/collapsible/index.d.mts +3 -0
- package/dist/src/collapsible/index.d.ts +3 -0
- package/dist/src/combobox/index.d.mts +3 -0
- package/dist/src/combobox/index.d.ts +3 -0
- package/dist/src/dialog/index.d.mts +3 -0
- package/dist/src/dialog/index.d.ts +3 -0
- package/dist/src/divider/index.d.mts +3 -0
- package/dist/src/divider/index.d.ts +3 -0
- package/dist/src/drawer/index.d.mts +3 -0
- package/dist/src/drawer/index.d.ts +3 -0
- package/dist/src/dropdown/index.d.mts +3 -0
- package/dist/src/dropdown/index.d.ts +3 -0
- package/dist/src/file-upload/index.d.mts +3 -0
- package/dist/src/file-upload/index.d.ts +3 -0
- package/dist/src/form-field/index.d.mts +3 -0
- package/dist/src/form-field/index.d.ts +3 -0
- package/dist/src/icon/Icon.d.ts +3 -0
- package/dist/src/icon-button/IconButton.d.ts +3 -0
- package/dist/src/input/Input.d.ts +3 -0
- package/dist/src/input/index.d.mts +4 -0
- package/dist/src/input/index.d.ts +4 -0
- package/dist/src/input-otp/index.d.mts +3 -0
- package/dist/src/input-otp/index.d.ts +3 -0
- package/dist/src/kbd/Kbd.d.ts +3 -0
- package/dist/src/label/index.d.mts +3 -0
- package/dist/src/label/index.d.ts +3 -0
- package/dist/src/link-box/index.d.mts +3 -0
- package/dist/src/link-box/index.d.ts +3 -0
- package/dist/src/meter/index.d.mts +3 -0
- package/dist/src/meter/index.d.ts +3 -0
- package/dist/src/pagination/index.d.mts +3 -0
- package/dist/src/pagination/index.d.ts +3 -0
- package/dist/src/popover/index.d.mts +3 -0
- package/dist/src/popover/index.d.ts +3 -0
- package/dist/src/portal/Portal.d.ts +3 -0
- package/dist/src/progress/index.d.mts +3 -0
- package/dist/src/progress/index.d.ts +3 -0
- package/dist/src/progress-tracker/index.d.mts +3 -0
- package/dist/src/progress-tracker/index.d.ts +3 -0
- package/dist/src/radio-group/index.d.mts +3 -0
- package/dist/src/radio-group/index.d.ts +3 -0
- package/dist/src/rating/Rating.d.ts +3 -0
- package/dist/src/rating-display/index.d.mts +3 -0
- package/dist/src/rating-display/index.d.ts +3 -0
- package/dist/src/scrolling-list/index.d.mts +3 -0
- package/dist/src/scrolling-list/index.d.ts +3 -0
- package/dist/src/segmented-control/index.d.mts +3 -0
- package/dist/src/segmented-control/index.d.ts +3 -0
- package/dist/src/segmented-gauge/index.d.mts +3 -0
- package/dist/src/segmented-gauge/index.d.ts +3 -0
- package/dist/src/select/index.d.mts +3 -0
- package/dist/src/select/index.d.ts +3 -0
- package/dist/src/skeleton/index.d.mts +3 -0
- package/dist/src/skeleton/index.d.ts +3 -0
- package/dist/src/slider/index.d.mts +3 -0
- package/dist/src/slider/index.d.ts +3 -0
- package/dist/src/slot/Slot.d.ts +4 -0
- package/dist/src/spinner/Spinner.d.ts +3 -0
- package/dist/src/stepper/index.d.mts +3 -0
- package/dist/src/stepper/index.d.ts +3 -0
- package/dist/src/switch/Switch.d.ts +3 -0
- package/dist/src/table/index.d.mts +6 -2
- package/dist/src/table/index.d.ts +6 -2
- package/dist/src/table/internal/TableRootWrapper.d.ts +3 -0
- package/dist/src/tabs/index.d.mts +3 -0
- package/dist/src/tabs/index.d.ts +3 -0
- package/dist/src/tag/Tag.d.ts +3 -0
- package/dist/src/text-link/TextLink.d.ts +3 -0
- package/dist/src/textarea/Textarea.d.ts +3 -0
- package/dist/src/textarea/index.d.mts +4 -0
- package/dist/src/textarea/index.d.ts +4 -0
- package/dist/src/toast/index.d.mts +3 -0
- package/dist/src/toast/index.d.ts +3 -0
- package/dist/src/visually-hidden/VisuallyHidden.d.ts +3 -0
- package/dist/stepper/index.js.map +1 -1
- package/dist/stepper/index.mjs.map +1 -1
- package/dist/switch/index.js.map +1 -1
- package/dist/switch/index.mjs.map +1 -1
- package/dist/table/index.js +1 -1
- package/dist/table/index.js.map +1 -1
- package/dist/table/index.mjs +1 -1
- package/dist/table/index.mjs.map +1 -1
- package/dist/tabs/index.js.map +1 -1
- package/dist/tabs/index.mjs.map +1 -1
- package/dist/tag/index.js.map +1 -1
- package/dist/tag/index.mjs.map +1 -1
- package/dist/text-link/index.js.map +1 -1
- package/dist/text-link/index.mjs.map +1 -1
- package/dist/textarea/index.js.map +1 -1
- package/dist/textarea/index.mjs.map +1 -1
- package/dist/toast/index.js.map +1 -1
- package/dist/toast/index.mjs.map +1 -1
- package/dist/visually-hidden/index.js.map +1 -1
- package/dist/visually-hidden/index.mjs.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/rating-display/RatingDisplayContext.tsx","../../src/rating-display/RatingDisplay.tsx","../../src/rating-display/RatingDisplayCount.tsx","../../src/rating-display/RatingDisplayStar.tsx","../../src/rating-display/utils.ts","../../src/rating-display/RatingDisplayStars.tsx","../../src/rating-display/RatingDisplayValue.tsx","../../src/rating-display/index.ts"],"sourcesContent":["import { createContext, type PropsWithChildren, useContext } from 'react'\n\nimport type { RatingDisplayStarProps } from './RatingDisplayStar'\n\ninterface RatingDisplayContextValue {\n value: number\n size: RatingDisplayStarProps['size']\n count?: number\n}\n\nconst RatingDisplayContext = createContext<RatingDisplayContextValue | null>(null)\n\ninterface RatingDisplayProviderProps extends PropsWithChildren<RatingDisplayContextValue> {}\n\nexport const RatingDisplayProvider = ({\n value,\n size,\n count,\n children,\n}: RatingDisplayProviderProps) => {\n return (\n <RatingDisplayContext.Provider value={{ value, size, count }}>\n {children}\n </RatingDisplayContext.Provider>\n )\n}\n\nexport const useRatingDisplay = () => {\n const context = useContext(RatingDisplayContext)\n if (!context) {\n throw new Error('RatingDisplay compound components must be used within RatingDisplay.')\n }\n\n return context\n}\n","import { type ComponentPropsWithRef, type PropsWithChildren } from 'react'\n\nimport { Slot } from '../slot'\nimport { RatingDisplayProvider } from './RatingDisplayContext'\nimport type { RatingDisplayStarProps } from './RatingDisplayStar'\n\nexport interface RatingDisplayProps extends PropsWithChildren<ComponentPropsWithRef<'div'>> {\n /**\n * When true, merges props onto the single child element instead of rendering a div.\n * Use to render the root as a link or another custom element.\n */\n asChild?: boolean\n /**\n * The rating value to display, on a scale from 0 to 5.\n */\n value?: number\n /**\n * Sets the size of the stars.\n * @default 'md'\n */\n size?: RatingDisplayStarProps['size']\n /**\n * Optional count value available to `RatingDisplay.Count`.\n */\n count?: number\n /**\n * Accessible description of the rating content.\n */\n 'aria-label': string\n}\n\nexport type RatingDisplayRootProps = RatingDisplayProps\n\nexport const RatingDisplay = ({\n value = 0,\n size = 'md',\n count,\n asChild = false,\n ref,\n children,\n ...rest\n}: RatingDisplayProps) => {\n const ratingValue = value ?? 0\n const Component = asChild ? Slot : 'div'\n\n return (\n <RatingDisplayProvider value={ratingValue} size={size} count={count}>\n <Component\n ref={ref}\n className=\"gap-x-sm relative inline-flex items-center\"\n data-spark-component=\"rating-display\"\n {...rest}\n >\n {children}\n </Component>\n </RatingDisplayProvider>\n )\n}\n\nRatingDisplay.displayName = 'RatingDisplay'\n","import { cva } from 'class-variance-authority'\nimport { type ComponentPropsWithRef, type ReactNode } from 'react'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\n\nexport interface RatingDisplayCountProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * Custom count content.\n * Pass a render function to receive the count value and return the content to render.\n */\n children?: ReactNode | ((count: number) => ReactNode)\n}\n\nconst ratingDisplayCountStyles = cva('text-on-surface/dim-1', {\n variants: {\n size: {\n sm: 'text-caption',\n md: 'text-body-2',\n lg: 'text-display-3',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n})\n\nexport const RatingDisplayCount = ({ className, children, ...rest }: RatingDisplayCountProps) => {\n const { count, size } = useRatingDisplay()\n if (count === undefined) return null\n const renderedCount = typeof children === 'function' ? children(count) : (children ?? count)\n\n return (\n <span className={ratingDisplayCountStyles({ size: size ?? 'md', className })} {...rest}>\n ({renderedCount})\n </span>\n )\n}\n\nRatingDisplayCount.displayName = 'RatingDisplay.Count'\n","import { StarFill } from '@spark-ui/icons/StarFill'\nimport { StarOutline } from '@spark-ui/icons/StarOutline'\nimport { cva, cx, type VariantProps } from 'class-variance-authority'\n\nimport { Icon } from '../icon'\nimport type { StarValue } from './types'\n\nconst ratingDisplayStarStyles = cva(['relative block after:absolute after:block after:inset-0'], {\n variants: {\n gap: {\n sm: ['after:w-[calc(100%+(var(--spacing-sm)))]', 'last-of-type:after:content-none'],\n md: ['after:w-[calc(100%+(var(--spacing-md)))]', 'last-of-type:after:content-none'],\n },\n },\n defaultVariants: {\n gap: 'sm',\n },\n})\n\nconst ratingDisplayStarIconStyles = cva('', {\n variants: {\n size: {\n sm: 'text-caption-link',\n md: 'text-body-1',\n lg: 'text-display-3',\n },\n design: {\n filled: ['text-main-variant'],\n outlined: ['text-on-surface/dim-3'],\n },\n },\n})\n\ntype RatingDisplayStarstylesProps = Omit<VariantProps<typeof ratingDisplayStarStyles>, never>\ntype RatingDisplayStarIconStylesProps = Omit<\n VariantProps<typeof ratingDisplayStarIconStyles>,\n 'design'\n>\n\nexport interface RatingDisplayStarProps\n extends RatingDisplayStarstylesProps, RatingDisplayStarIconStylesProps {\n value: StarValue\n}\n\nexport const RatingDisplayStar = ({ value, size }: RatingDisplayStarProps) => {\n return (\n <div\n data-spark-component=\"rating-display-star\"\n data-part=\"star\"\n className={ratingDisplayStarStyles({\n gap: size === 'lg' ? 'md' : 'sm',\n })}\n >\n <div className={cx('z-raised absolute overflow-hidden')} style={{ width: value * 100 + '%' }}>\n <Icon\n className={ratingDisplayStarIconStyles({\n size,\n design: 'filled',\n })}\n >\n <StarFill />\n </Icon>\n </div>\n\n <Icon className={ratingDisplayStarIconStyles({ size, design: 'outlined' })}>\n <StarOutline />\n </Icon>\n </div>\n )\n}\n","import { type StarValue } from './types'\n\nfunction getNearestHalfDecimal(num: number): number {\n return Math.round(num / 0.5) * 0.5\n}\n\nfunction formatRatingValue(value: number): string {\n const locale = Intl.DateTimeFormat().resolvedOptions().locale\n\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: 0,\n maximumFractionDigits: 1,\n }).format(value)\n}\n\nfunction getStarValue({ value, index }: { value?: number; index: number }): StarValue {\n if (value === undefined) return 0\n\n const starPosition = index + 1\n const formattedValue = getNearestHalfDecimal(value)\n\n if (Math.ceil(formattedValue) < starPosition) return 0\n\n return formattedValue >= starPosition ? 1 : 0.5\n}\n\nfunction getSingleStarValue(value?: number): StarValue {\n if (value === undefined) return 0\n if (value < 1) return 0\n if (value < 4) return 0.5\n return 1\n}\n\nfunction splitAt<T>(arr: T[], index: number): [T[], T[]] {\n const prev = arr.slice(0, index)\n const next = arr.slice(index)\n\n return [prev, next]\n}\n\nexport { formatRatingValue, getNearestHalfDecimal, getSingleStarValue, getStarValue, splitAt }\n","import { cx } from 'class-variance-authority'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\nimport { RatingDisplayStar, type RatingDisplayStarProps } from './RatingDisplayStar'\nimport type { StarValue } from './types'\nimport { getSingleStarValue, getStarValue } from './utils'\n\nexport interface RatingDisplayStarsProps {\n size?: RatingDisplayStarProps['size']\n /**\n * Sets the rendering mode for stars.\n * @default 'default'\n */\n variant?: 'default' | 'single-star'\n /**\n * Custom fill algorithm for each star.\n * By default, stars are rounded to the nearest 0.5.\n */\n getFillMode?: ({ value, index }: { value?: number; index: number }) => StarValue\n}\n\nexport const RatingDisplayStars = ({\n size,\n variant = 'default',\n getFillMode,\n}: RatingDisplayStarsProps) => {\n const { value, size: contextSize } = useRatingDisplay()\n const resolvedSize = size ?? contextSize\n const getDisplayValue = (index: number) => {\n if (getFillMode) {\n return getFillMode({ index, value })\n }\n\n return variant === 'single-star' ? getSingleStarValue(value) : getStarValue({ index, value })\n }\n\n const stars =\n variant === 'single-star'\n ? [getDisplayValue(0)]\n : Array.from({ length: 5 }, (_, index) => getDisplayValue(index))\n\n return (\n <div\n data-spark-component=\"rating-display-stars\"\n className={cx(resolvedSize === 'lg' ? 'gap-x-md' : 'gap-x-sm', 'flex')}\n >\n {stars.map((starValue, index) => (\n <RatingDisplayStar key={index} size={resolvedSize} value={starValue} />\n ))}\n </div>\n )\n}\n\nRatingDisplayStars.displayName = 'RatingDisplay.Stars'\n","import { cva } from 'class-variance-authority'\nimport { type ComponentPropsWithRef, type ReactNode } from 'react'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\nimport { formatRatingValue } from './utils'\n\nexport interface RatingDisplayValueProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * Custom value content.\n * Pass a render function to receive the formatted value (first arg) and raw value (second arg),\n * then return the content to render.\n */\n children?: ReactNode | ((formattedValue: string, value: number) => ReactNode)\n}\n\nconst ratingDisplayValueStyles = cva('text-on-surface font-bold', {\n variants: {\n size: {\n sm: 'text-caption',\n md: 'text-body-2',\n lg: 'text-display-3',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n})\n\nexport const RatingDisplayValue = ({ className, children, ...rest }: RatingDisplayValueProps) => {\n const { value, size } = useRatingDisplay()\n const formattedValue = formatRatingValue(value)\n const renderedValue =\n typeof children === 'function' ? children(formattedValue, value) : (children ?? formattedValue)\n\n return (\n <span\n data-spark-component=\"rating-display-value\"\n className={ratingDisplayValueStyles({ size: size ?? 'md', className })}\n {...rest}\n >\n {renderedValue}\n </span>\n )\n}\n\nRatingDisplayValue.displayName = 'RatingDisplay.Value'\n","import { RatingDisplay as Root } from './RatingDisplay'\nimport { RatingDisplayCount as Count } from './RatingDisplayCount'\nimport { RatingDisplayStars as Stars } from './RatingDisplayStars'\nimport { RatingDisplayValue as Value } from './RatingDisplayValue'\n\nexport const RatingDisplay: typeof Root & {\n Stars: typeof Stars\n Value: typeof Value\n Count: typeof Count\n} = Object.assign(Root, {\n Stars,\n Value,\n Count,\n})\n\nRatingDisplay.displayName = 'RatingDisplay'\nStars.displayName = 'RatingDisplay.Stars'\nValue.displayName = 'RatingDisplay.Value'\nCount.displayName = 'RatingDisplay.Count'\n\nexport { type RatingDisplayProps, type RatingDisplayRootProps } from './RatingDisplay'\nexport { type RatingDisplayStarsProps } from './RatingDisplayStars'\nexport { type RatingDisplayValueProps } from './RatingDisplayValue'\nexport { type RatingDisplayCountProps } from './RatingDisplayCount'\nexport type { StarValue } from './types'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/rating-display/RatingDisplayContext.tsx","../../src/rating-display/RatingDisplay.tsx","../../src/rating-display/RatingDisplayCount.tsx","../../src/rating-display/RatingDisplayStar.tsx","../../src/rating-display/utils.ts","../../src/rating-display/RatingDisplayStars.tsx","../../src/rating-display/RatingDisplayValue.tsx","../../src/rating-display/index.ts"],"sourcesContent":["import { createContext, type PropsWithChildren, useContext } from 'react'\n\nimport type { RatingDisplayStarProps } from './RatingDisplayStar'\n\ninterface RatingDisplayContextValue {\n value: number\n size: RatingDisplayStarProps['size']\n count?: number\n}\n\nconst RatingDisplayContext = createContext<RatingDisplayContextValue | null>(null)\n\ninterface RatingDisplayProviderProps extends PropsWithChildren<RatingDisplayContextValue> {}\n\nexport const RatingDisplayProvider = ({\n value,\n size,\n count,\n children,\n}: RatingDisplayProviderProps) => {\n return (\n <RatingDisplayContext.Provider value={{ value, size, count }}>\n {children}\n </RatingDisplayContext.Provider>\n )\n}\n\nexport const useRatingDisplay = () => {\n const context = useContext(RatingDisplayContext)\n if (!context) {\n throw new Error('RatingDisplay compound components must be used within RatingDisplay.')\n }\n\n return context\n}\n","import { type ComponentPropsWithRef, type PropsWithChildren } from 'react'\n\nimport { Slot } from '../slot'\nimport { RatingDisplayProvider } from './RatingDisplayContext'\nimport type { RatingDisplayStarProps } from './RatingDisplayStar'\n\nexport interface RatingDisplayProps extends PropsWithChildren<ComponentPropsWithRef<'div'>> {\n /**\n * When true, merges props onto the single child element instead of rendering a div.\n * Use to render the root as a link or another custom element.\n */\n asChild?: boolean\n /**\n * The rating value to display, on a scale from 0 to 5.\n */\n value?: number\n /**\n * Sets the size of the stars.\n * @default 'md'\n */\n size?: RatingDisplayStarProps['size']\n /**\n * Optional count value available to `RatingDisplay.Count`.\n */\n count?: number\n /**\n * Accessible description of the rating content.\n */\n 'aria-label': string\n}\n\nexport type RatingDisplayRootProps = RatingDisplayProps\n\nexport const RatingDisplay = ({\n value = 0,\n size = 'md',\n count,\n asChild = false,\n ref,\n children,\n ...rest\n}: RatingDisplayProps) => {\n const ratingValue = value ?? 0\n const Component = asChild ? Slot : 'div'\n\n return (\n <RatingDisplayProvider value={ratingValue} size={size} count={count}>\n <Component\n ref={ref}\n className=\"gap-x-sm relative inline-flex items-center\"\n data-spark-component=\"rating-display\"\n {...rest}\n >\n {children}\n </Component>\n </RatingDisplayProvider>\n )\n}\n\nRatingDisplay.displayName = 'RatingDisplay'\n","import { cva } from 'class-variance-authority'\nimport { type ComponentPropsWithRef, type ReactNode } from 'react'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\n\nexport interface RatingDisplayCountProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * Custom count content.\n * Pass a render function to receive the count value and return the content to render.\n */\n children?: ReactNode | ((count: number) => ReactNode)\n}\n\nconst ratingDisplayCountStyles = cva('text-on-surface/dim-1', {\n variants: {\n size: {\n sm: 'text-caption',\n md: 'text-body-2',\n lg: 'text-display-3',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n})\n\nexport const RatingDisplayCount = ({ className, children, ...rest }: RatingDisplayCountProps) => {\n const { count, size } = useRatingDisplay()\n if (count === undefined) return null\n const renderedCount = typeof children === 'function' ? children(count) : (children ?? count)\n\n return (\n <span className={ratingDisplayCountStyles({ size: size ?? 'md', className })} {...rest}>\n ({renderedCount})\n </span>\n )\n}\n\nRatingDisplayCount.displayName = 'RatingDisplay.Count'\n","import { StarFill } from '@spark-ui/icons/StarFill'\nimport { StarOutline } from '@spark-ui/icons/StarOutline'\nimport { cva, cx, type VariantProps } from 'class-variance-authority'\n\nimport { Icon } from '../icon'\nimport type { StarValue } from './types'\n\nconst ratingDisplayStarStyles = cva(['relative block after:absolute after:block after:inset-0'], {\n variants: {\n gap: {\n sm: ['after:w-[calc(100%+(var(--spacing-sm)))]', 'last-of-type:after:content-none'],\n md: ['after:w-[calc(100%+(var(--spacing-md)))]', 'last-of-type:after:content-none'],\n },\n },\n defaultVariants: {\n gap: 'sm',\n },\n})\n\nconst ratingDisplayStarIconStyles = cva('', {\n variants: {\n size: {\n sm: 'text-caption-link',\n md: 'text-body-1',\n lg: 'text-display-3',\n },\n design: {\n filled: ['text-main-variant'],\n outlined: ['text-on-surface/dim-3'],\n },\n },\n})\n\ntype RatingDisplayStarstylesProps = Omit<VariantProps<typeof ratingDisplayStarStyles>, never>\ntype RatingDisplayStarIconStylesProps = Omit<\n VariantProps<typeof ratingDisplayStarIconStyles>,\n 'design'\n>\n\nexport interface RatingDisplayStarProps\n extends RatingDisplayStarstylesProps, RatingDisplayStarIconStylesProps {\n value: StarValue\n}\n\nexport const RatingDisplayStar = ({ value, size }: RatingDisplayStarProps) => {\n return (\n <div\n data-spark-component=\"rating-display-star\"\n data-part=\"star\"\n className={ratingDisplayStarStyles({\n gap: size === 'lg' ? 'md' : 'sm',\n })}\n >\n <div className={cx('z-raised absolute overflow-hidden')} style={{ width: value * 100 + '%' }}>\n <Icon\n className={ratingDisplayStarIconStyles({\n size,\n design: 'filled',\n })}\n >\n <StarFill />\n </Icon>\n </div>\n\n <Icon className={ratingDisplayStarIconStyles({ size, design: 'outlined' })}>\n <StarOutline />\n </Icon>\n </div>\n )\n}\n","import { type StarValue } from './types'\n\nfunction getNearestHalfDecimal(num: number): number {\n return Math.round(num / 0.5) * 0.5\n}\n\nfunction formatRatingValue(value: number): string {\n const locale = Intl.DateTimeFormat().resolvedOptions().locale\n\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: 0,\n maximumFractionDigits: 1,\n }).format(value)\n}\n\nfunction getStarValue({ value, index }: { value?: number; index: number }): StarValue {\n if (value === undefined) return 0\n\n const starPosition = index + 1\n const formattedValue = getNearestHalfDecimal(value)\n\n if (Math.ceil(formattedValue) < starPosition) return 0\n\n return formattedValue >= starPosition ? 1 : 0.5\n}\n\nfunction getSingleStarValue(value?: number): StarValue {\n if (value === undefined) return 0\n if (value < 1) return 0\n if (value < 4) return 0.5\n return 1\n}\n\nfunction splitAt<T>(arr: T[], index: number): [T[], T[]] {\n const prev = arr.slice(0, index)\n const next = arr.slice(index)\n\n return [prev, next]\n}\n\nexport { formatRatingValue, getNearestHalfDecimal, getSingleStarValue, getStarValue, splitAt }\n","import { cx } from 'class-variance-authority'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\nimport { RatingDisplayStar, type RatingDisplayStarProps } from './RatingDisplayStar'\nimport type { StarValue } from './types'\nimport { getSingleStarValue, getStarValue } from './utils'\n\nexport interface RatingDisplayStarsProps {\n size?: RatingDisplayStarProps['size']\n /**\n * Sets the rendering mode for stars.\n * @default 'default'\n */\n variant?: 'default' | 'single-star'\n /**\n * Custom fill algorithm for each star.\n * By default, stars are rounded to the nearest 0.5.\n */\n getFillMode?: ({ value, index }: { value?: number; index: number }) => StarValue\n}\n\nexport const RatingDisplayStars = ({\n size,\n variant = 'default',\n getFillMode,\n}: RatingDisplayStarsProps) => {\n const { value, size: contextSize } = useRatingDisplay()\n const resolvedSize = size ?? contextSize\n const getDisplayValue = (index: number) => {\n if (getFillMode) {\n return getFillMode({ index, value })\n }\n\n return variant === 'single-star' ? getSingleStarValue(value) : getStarValue({ index, value })\n }\n\n const stars =\n variant === 'single-star'\n ? [getDisplayValue(0)]\n : Array.from({ length: 5 }, (_, index) => getDisplayValue(index))\n\n return (\n <div\n data-spark-component=\"rating-display-stars\"\n className={cx(resolvedSize === 'lg' ? 'gap-x-md' : 'gap-x-sm', 'flex')}\n >\n {stars.map((starValue, index) => (\n <RatingDisplayStar key={index} size={resolvedSize} value={starValue} />\n ))}\n </div>\n )\n}\n\nRatingDisplayStars.displayName = 'RatingDisplay.Stars'\n","import { cva } from 'class-variance-authority'\nimport { type ComponentPropsWithRef, type ReactNode } from 'react'\n\nimport { useRatingDisplay } from './RatingDisplayContext'\nimport { formatRatingValue } from './utils'\n\nexport interface RatingDisplayValueProps extends Omit<ComponentPropsWithRef<'span'>, 'children'> {\n /**\n * Custom value content.\n * Pass a render function to receive the formatted value (first arg) and raw value (second arg),\n * then return the content to render.\n */\n children?: ReactNode | ((formattedValue: string, value: number) => ReactNode)\n}\n\nconst ratingDisplayValueStyles = cva('text-on-surface font-bold', {\n variants: {\n size: {\n sm: 'text-caption',\n md: 'text-body-2',\n lg: 'text-display-3',\n },\n },\n defaultVariants: {\n size: 'md',\n },\n})\n\nexport const RatingDisplayValue = ({ className, children, ...rest }: RatingDisplayValueProps) => {\n const { value, size } = useRatingDisplay()\n const formattedValue = formatRatingValue(value)\n const renderedValue =\n typeof children === 'function' ? children(formattedValue, value) : (children ?? formattedValue)\n\n return (\n <span\n data-spark-component=\"rating-display-value\"\n className={ratingDisplayValueStyles({ size: size ?? 'md', className })}\n {...rest}\n >\n {renderedValue}\n </span>\n )\n}\n\nRatingDisplayValue.displayName = 'RatingDisplay.Value'\n","import { RatingDisplay as Root } from './RatingDisplay'\nimport { RatingDisplayCount as Count } from './RatingDisplayCount'\nimport { RatingDisplayStars as Stars } from './RatingDisplayStars'\nimport { RatingDisplayValue as Value } from './RatingDisplayValue'\n\n/**\n * A read-only component that displays a rating value using stars.\n */\nexport const RatingDisplay: typeof Root & {\n Stars: typeof Stars\n Value: typeof Value\n Count: typeof Count\n} = Object.assign(Root, {\n Stars,\n Value,\n Count,\n})\n\nRatingDisplay.displayName = 'RatingDisplay'\nStars.displayName = 'RatingDisplay.Stars'\nValue.displayName = 'RatingDisplay.Value'\nCount.displayName = 'RatingDisplay.Count'\n\nexport { type RatingDisplayProps, type RatingDisplayRootProps } from './RatingDisplay'\nexport { type RatingDisplayStarsProps } from './RatingDisplayStars'\nexport { type RatingDisplayValueProps } from './RatingDisplayValue'\nexport { type RatingDisplayCountProps } from './RatingDisplayCount'\nexport type { StarValue } from './types'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/scrolling-list/ScrollingList.tsx","../../src/scrolling-list/ScrollingListControls.tsx","../../src/scrolling-list/useFocusWithinScroll.tsx","../../src/scrolling-list/ScrollingListItem.tsx","../../src/scrolling-list/ScrollingListItems.tsx","../../src/scrolling-list/ScrollingListNextButton.tsx","../../src/scrolling-list/ScrollingListPrevButton.tsx","../../src/scrolling-list/ScrollingListSkipButton.tsx","../../src/scrolling-list/index.ts"],"sourcesContent":["import { ScrollOverflow, useScrollOverflow } from '@spark-ui/hooks/use-scroll-overflow'\nimport { cx } from 'class-variance-authority'\nimport {\n ComponentPropsWithRef,\n createContext,\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n} from 'react'\nimport { SnapCarouselResult, useSnapCarousel } from 'react-snap-carousel'\n\ntype SnapType = 'mandatory' | 'proximity' | 'none'\ntype ScrollBehavior = 'smooth' | 'instant'\ntype SnapStop = 'normal' | 'always'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n /**\n * CSS scroll snap behavior.\n * - `mandatory` to force snapping on each \"page\".\n * - `proximity` to force snapping only when scroll position is near the edge of a \"page\". Behavior can change depending on each browser.\n * - `none` to disabled scroll snapping.\n */\n snapType?: SnapType\n /**\n * Defines whether or not the scroll container is allowed to \"pass over\" possible snap positions.\n */\n snapStop?: SnapStop\n scrollBehavior?: ScrollBehavior\n /**\n * Add a fade effect to indicate content overflow.\n */\n withFade?: boolean\n children?: ReactNode\n /**\n * When `true`, allow previous and next buttons to be used when reaching the edges of the list.\n */\n loop?: boolean\n /**\n * Space (in pixels) between items.\n */\n gap?: number\n /**\n * Offset (in pixels) of the left of the optimal viewing region of the list.\n */\n scrollPadding?: number\n className?: string\n}\n\ninterface ScrollingListContextState extends SnapCarouselResult {\n snapType: SnapType\n snapStop: SnapStop\n scrollBehavior: ScrollBehavior\n visibleItemsRange: readonly [number, number]\n loop: boolean\n gap: number\n withFade: boolean\n scrollPadding: number\n scrollAreaRef: RefObject<HTMLDivElement | null>\n overflow: ScrollOverflow\n skipKeyboardNavigation: () => void\n}\n\nexport const ScrollingListContext = createContext<ScrollingListContextState>(\n null as unknown as ScrollingListContextState\n)\n\nexport const ScrollingList = ({\n snapType = 'none',\n snapStop = 'normal',\n scrollBehavior = 'smooth',\n loop = false,\n gap = 16,\n withFade = false,\n scrollPadding = 0,\n children,\n className,\n ...rest\n}: Props) => {\n const scrollAreaRef = useRef<HTMLDivElement>(null)\n const skipAnchorRef = useRef<HTMLButtonElement>(null)\n\n const snapCarouselAPI = useSnapCarousel()\n\n const { overflow, refresh: refreshOverflow } = useScrollOverflow(scrollAreaRef, {\n precisionTreshold: 1,\n })\n\n const { activePageIndex, pages, refresh } = snapCarouselAPI\n\n const visibleItems = pages[activePageIndex] as number[]\n\n const visibleItemsRange = visibleItems\n ? ([visibleItems[0]! + 1, visibleItems[visibleItems.length - 1]! + 1] as const)\n : ([0, 0] as const)\n\n // Force refresh of the carousel API when children change\n const forceRefresh = useCallback(() => {\n if (refresh && scrollAreaRef.current) {\n // Small delay to ensure DOM is updated\n setTimeout(() => {\n refresh()\n }, 0)\n }\n }, [refresh])\n\n useEffect(() => {\n forceRefresh()\n }, [children, forceRefresh])\n\n useLayoutEffect(() => {\n if (scrollAreaRef.current) {\n // Use requestAnimationFrame to ensure proper timing with the render cycle\n // This prevents race conditions that occur when the console is closed\n requestAnimationFrame(() => {\n refreshOverflow()\n })\n }\n }, [children, refreshOverflow])\n\n const skipKeyboardNavigation = () => {\n skipAnchorRef.current?.focus()\n }\n\n const ctxValue: ScrollingListContextState = {\n ...snapCarouselAPI,\n snapType,\n snapStop,\n skipKeyboardNavigation,\n scrollBehavior,\n visibleItemsRange,\n loop,\n gap,\n withFade,\n scrollPadding,\n scrollAreaRef,\n overflow,\n }\n\n return (\n <ScrollingListContext.Provider value={ctxValue}>\n <div\n data-spark-component=\"scrolling-list\"\n className={cx(\n 'gap-lg group/scrolling-list relative flex flex-col default:w-full',\n className\n )}\n {...rest}\n >\n {children}\n </div>\n <span ref={skipAnchorRef} className=\"size-0 overflow-hidden\" tabIndex={-1} />\n </ScrollingListContext.Provider>\n )\n}\n\nScrollingList.displayName = 'ScrollingList'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'\n\ninterface ScrollingListControls extends ComponentPropsWithoutRef<'div'> {\n /**\n * Visibility behavior of the control buttons:\n * - `always`: buttons are always visible.\n * - `hover`: buttons only appear on hover.\n *\n * a11y: `hover` is dangerous for accessibility as it disabled controls for touch screen users.\n * When using it, you must provide an alternative control outside of the list to replace them.\n */\n visibility?: 'hover' | 'always'\n children: ReactNode\n}\n\nexport const ScrollingListControls = ({\n children,\n visibility = 'always',\n className,\n ...rest\n}: ScrollingListControls) => {\n return (\n <div\n data-spark-component=\"scrolling-list-controls\"\n className={cx(\n 'default:px-md pointer-events-none absolute inset-0 flex flex-row items-center justify-between overflow-hidden',\n className\n )}\n style={\n {\n '--scrolling-list-controls-opacity': visibility === 'hover' ? '0' : '1',\n } as CSSProperties\n }\n data-orientation=\"horizontal\"\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nScrollingListControls.displayName = 'ScrollingList.Controls'\n","import { RefObject, useEffect, useState } from 'react'\n\nexport function useFocusWithinScroll<T extends HTMLElement | null>(\n ref: RefObject<T>, // The container to detect focus within\n scrollRef: RefObject<HTMLDivElement | null> // The scrollable container\n) {\n const [isFocusWithin, setIsFocusWithin] = useState(false)\n\n useEffect(() => {\n const handleFocusIn = (event: FocusEvent) => {\n setIsFocusWithin(true)\n\n const focusedElement = event.target as HTMLElement\n const scrollContainer = scrollRef.current\n\n if (focusedElement && scrollContainer) {\n const focusRect = focusedElement.getBoundingClientRect()\n const scrollRect = scrollContainer.getBoundingClientRect()\n\n // Check if the focused element is fully visible inside the scroll container\n const isFullyVisible =\n focusRect.left >= scrollRect.left &&\n focusRect.right <= scrollRect.right &&\n focusRect.top >= scrollRect.top &&\n focusRect.bottom <= scrollRect.bottom\n\n if (!isFullyVisible) {\n focusedElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n }\n }\n }\n\n const handleFocusOut = (event: FocusEvent) => {\n if (ref.current && !ref.current.contains(event.relatedTarget as Node)) {\n setIsFocusWithin(false)\n }\n }\n\n const node = ref.current\n if (node) {\n node.addEventListener('focusin', handleFocusIn)\n node.addEventListener('focusout', handleFocusOut)\n }\n\n return () => {\n if (node) {\n node.removeEventListener('focusin', handleFocusIn)\n node.removeEventListener('focusout', handleFocusOut)\n }\n }\n }, [ref, scrollRef])\n\n return isFocusWithin\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, ReactNode, useContext, useRef } from 'react'\n\nimport { Slot } from '../slot'\nimport { ScrollingListContext } from './ScrollingList'\nimport { useFocusWithinScroll } from './useFocusWithinScroll'\n\nexport interface ScrollingListItemProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Change the default rendered element for the one passed as a child, merging their props and behavior.\n */\n asChild?: boolean\n children?: ReactNode\n /**\n * DO NOT USE. This prop is automatically managed by the parent ScrollingList.ListItems\n */\n index?: number\n className?: string\n}\n\nexport const ScrollingListItem = ({\n asChild = false,\n children,\n index = 0,\n className = '',\n ...rest\n}: ScrollingListItemProps) => {\n const ctx = useContext(ScrollingListContext)\n const itemRef = useRef<HTMLDivElement>(null)\n\n const isSnapPoint = ctx.snapPointIndexes.has(index)\n\n useFocusWithinScroll(itemRef, ctx.scrollAreaRef)\n\n const Component = asChild ? Slot : 'div'\n\n return (\n <Component\n data-spark-component=\"scrolling-list-item\"\n role=\"listitem\"\n ref={itemRef}\n className={cx(\n 'default:w-auto default:shrink-0',\n {\n 'snap-start': isSnapPoint,\n 'snap-normal': isSnapPoint && ctx.snapStop === 'normal',\n 'snap-always': isSnapPoint && ctx.snapStop === 'always',\n },\n className\n )}\n {...rest}\n >\n {children}\n </Component>\n )\n}\n\nScrollingListItem.displayName = 'ScrollingList.Item'\n","import { cx } from 'class-variance-authority'\nimport {\n Children,\n cloneElement,\n ComponentPropsWithRef,\n CSSProperties,\n isValidElement,\n KeyboardEvent,\n ReactNode,\n Ref,\n RefObject,\n useContext,\n} from 'react'\n\nimport { ScrollingListContext } from './ScrollingList'\nimport { ScrollingListItemProps } from './ScrollingListItem'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n children?: ReactNode\n className?: string\n}\n\nexport function mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): Ref<T> {\n return (value: T | null) => {\n refs.forEach(ref => {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref && typeof ref === 'object' && 'current' in ref) {\n ;(ref as RefObject<T | null>).current = value\n }\n })\n }\n}\n\nexport const ScrollingListItems = ({ children, ref, className = '', ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n const snapConfig = {\n mandatory: 'x mandatory',\n proximity: 'x proximity',\n none: 'none',\n }\n\n const handleLeftArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasPrevPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasPrevPage ? ctx.activePageIndex - 1 : ctx.pages.length - 1, {\n behavior: ctx.scrollBehavior,\n })\n }\n\n const handleRightArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasNextPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasNextPage ? ctx.activePageIndex + 1 : 0, { behavior: ctx.scrollBehavior })\n }\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowLeft') {\n handleLeftArrow(event)\n }\n\n if (event.key === 'ArrowRight') {\n handleRightArrow(event)\n }\n }\n\n interface CustomCSSProperties extends CSSProperties {\n '--scrolling-list-gap'?: string\n '--scrolling-list-px'?: string\n }\n\n const inlineStyles: CustomCSSProperties = {\n scrollSnapType: snapConfig[ctx.snapType],\n scrollPaddingInline: 'var(--scrolling-list-px)',\n '--scrolling-list-px': `${ctx.scrollPadding}px`,\n '--scrolling-list-gap': `${ctx.gap}px`,\n ...(ctx.withFade && {\n maskImage:\n 'linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 44px, rgba(0, 0, 0, 1) calc(100% - 44px), rgba(0, 0, 0, 0))',\n maskSize: `calc(100% + ${ctx.overflow.left ? '0px' : '44px'} + ${ctx.overflow.right ? '0px' : '44px'}) 100%`,\n maskPosition: `${ctx.overflow.left ? '0px' : '-44px'} 0`,\n }),\n }\n\n return (\n <div\n data-spark-component=\"scrolling-list-items\"\n id=\"scrolling-list-items\"\n role=\"list\"\n className={cx(\n 'relative transition-all duration-300',\n 'u-no-scrollbar overflow-x-auto scroll-smooth',\n 'w-full gap-(--scrolling-list-gap) default:flex default:flex-row',\n 'focus-visible:u-outline',\n className\n )}\n ref={mergeRefs<HTMLDivElement>(ctx.scrollAreaRef, ctx.scrollRef, ref)}\n style={inlineStyles}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {Children.map(children, (child, index) =>\n isValidElement<ScrollingListItemProps>(child) ? cloneElement(child, { index }) : child\n )}\n </div>\n )\n}\n\nScrollingListItems.displayName = 'ScrollingList.Items'\n","import { ArrowVerticalRight } from '@spark-ui/icons/ArrowVerticalRight'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListNextButton = ({ 'aria-label': ariaLabel, ...rest }: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handleNextPage = () => {\n if (ctx.hasNextPage) {\n ctx.next({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.right)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-next-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handleNextPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalRight />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListNextButton.displayName = 'ScrollingList.NextButton'\n","import { ArrowVerticalLeft } from '@spark-ui/icons/ArrowVerticalLeft'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListPrevButton = ({\n 'aria-label': ariaLabel,\n\n ...rest\n}: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handlePrevPage = () => {\n const shouldSnapFirstPage =\n ctx.activePageIndex === 0 && (ctx.scrollAreaRef.current?.scrollLeft || 0) > 0\n\n if (shouldSnapFirstPage) {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n } else if (ctx.hasPrevPage) {\n ctx.prev({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(ctx.pages.length - 1, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.left)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-prev-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handlePrevPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalLeft />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListPrevButton.displayName = 'ScrollingList.PrevButton'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, useContext } from 'react'\n\nimport { Button } from '../button'\nimport { ScrollingListContext } from './ScrollingList'\n\ninterface Props extends ComponentPropsWithoutRef<'button'> {\n children: string\n}\n\nexport const ScrollingListSkipButton = ({ children, ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n return (\n <Button\n type=\"button\"\n design=\"tinted\"\n intent=\"surface\"\n tabIndex={0}\n className={cx(\n 'z-raised absolute top-1/2 left-0 -translate-y-1/2',\n 'not-focus-visible:pointer-events-none not-focus-visible:size-0 not-focus-visible:opacity-0'\n )}\n onClick={ctx.skipKeyboardNavigation}\n {...rest}\n >\n {children}\n </Button>\n )\n}\n\nScrollingListSkipButton.displayName = 'ScrollingList.SkipButton'\n","import { ScrollingList as Root } from './ScrollingList'\nimport { ScrollingListControls as Controls } from './ScrollingListControls'\nimport { ScrollingListItem as Item } from './ScrollingListItem'\nimport { ScrollingListItems as Items } from './ScrollingListItems'\nimport { ScrollingListNextButton as NextButton } from './ScrollingListNextButton'\nimport { ScrollingListPrevButton as PrevButton } from './ScrollingListPrevButton'\nimport { ScrollingListSkipButton as SkipButton } from './ScrollingListSkipButton'\n\nexport const ScrollingList: typeof Root & {\n Controls: typeof Controls\n NextButton: typeof NextButton\n PrevButton: typeof PrevButton\n Item: typeof Item\n Items: typeof Items\n SkipButton: typeof SkipButton\n} = Object.assign(Root, {\n Controls,\n NextButton,\n PrevButton,\n Item,\n Items,\n SkipButton,\n})\n\nScrollingList.displayName = 'ScrollingList'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/scrolling-list/ScrollingList.tsx","../../src/scrolling-list/ScrollingListControls.tsx","../../src/scrolling-list/useFocusWithinScroll.tsx","../../src/scrolling-list/ScrollingListItem.tsx","../../src/scrolling-list/ScrollingListItems.tsx","../../src/scrolling-list/ScrollingListNextButton.tsx","../../src/scrolling-list/ScrollingListPrevButton.tsx","../../src/scrolling-list/ScrollingListSkipButton.tsx","../../src/scrolling-list/index.ts"],"sourcesContent":["import { ScrollOverflow, useScrollOverflow } from '@spark-ui/hooks/use-scroll-overflow'\nimport { cx } from 'class-variance-authority'\nimport {\n ComponentPropsWithRef,\n createContext,\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n} from 'react'\nimport { SnapCarouselResult, useSnapCarousel } from 'react-snap-carousel'\n\ntype SnapType = 'mandatory' | 'proximity' | 'none'\ntype ScrollBehavior = 'smooth' | 'instant'\ntype SnapStop = 'normal' | 'always'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n /**\n * CSS scroll snap behavior.\n * - `mandatory` to force snapping on each \"page\".\n * - `proximity` to force snapping only when scroll position is near the edge of a \"page\". Behavior can change depending on each browser.\n * - `none` to disabled scroll snapping.\n */\n snapType?: SnapType\n /**\n * Defines whether or not the scroll container is allowed to \"pass over\" possible snap positions.\n */\n snapStop?: SnapStop\n scrollBehavior?: ScrollBehavior\n /**\n * Add a fade effect to indicate content overflow.\n */\n withFade?: boolean\n children?: ReactNode\n /**\n * When `true`, allow previous and next buttons to be used when reaching the edges of the list.\n */\n loop?: boolean\n /**\n * Space (in pixels) between items.\n */\n gap?: number\n /**\n * Offset (in pixels) of the left of the optimal viewing region of the list.\n */\n scrollPadding?: number\n className?: string\n}\n\ninterface ScrollingListContextState extends SnapCarouselResult {\n snapType: SnapType\n snapStop: SnapStop\n scrollBehavior: ScrollBehavior\n visibleItemsRange: readonly [number, number]\n loop: boolean\n gap: number\n withFade: boolean\n scrollPadding: number\n scrollAreaRef: RefObject<HTMLDivElement | null>\n overflow: ScrollOverflow\n skipKeyboardNavigation: () => void\n}\n\nexport const ScrollingListContext = createContext<ScrollingListContextState>(\n null as unknown as ScrollingListContextState\n)\n\nexport const ScrollingList = ({\n snapType = 'none',\n snapStop = 'normal',\n scrollBehavior = 'smooth',\n loop = false,\n gap = 16,\n withFade = false,\n scrollPadding = 0,\n children,\n className,\n ...rest\n}: Props) => {\n const scrollAreaRef = useRef<HTMLDivElement>(null)\n const skipAnchorRef = useRef<HTMLButtonElement>(null)\n\n const snapCarouselAPI = useSnapCarousel()\n\n const { overflow, refresh: refreshOverflow } = useScrollOverflow(scrollAreaRef, {\n precisionTreshold: 1,\n })\n\n const { activePageIndex, pages, refresh } = snapCarouselAPI\n\n const visibleItems = pages[activePageIndex] as number[]\n\n const visibleItemsRange = visibleItems\n ? ([visibleItems[0]! + 1, visibleItems[visibleItems.length - 1]! + 1] as const)\n : ([0, 0] as const)\n\n // Force refresh of the carousel API when children change\n const forceRefresh = useCallback(() => {\n if (refresh && scrollAreaRef.current) {\n // Small delay to ensure DOM is updated\n setTimeout(() => {\n refresh()\n }, 0)\n }\n }, [refresh])\n\n useEffect(() => {\n forceRefresh()\n }, [children, forceRefresh])\n\n useLayoutEffect(() => {\n if (scrollAreaRef.current) {\n // Use requestAnimationFrame to ensure proper timing with the render cycle\n // This prevents race conditions that occur when the console is closed\n requestAnimationFrame(() => {\n refreshOverflow()\n })\n }\n }, [children, refreshOverflow])\n\n const skipKeyboardNavigation = () => {\n skipAnchorRef.current?.focus()\n }\n\n const ctxValue: ScrollingListContextState = {\n ...snapCarouselAPI,\n snapType,\n snapStop,\n skipKeyboardNavigation,\n scrollBehavior,\n visibleItemsRange,\n loop,\n gap,\n withFade,\n scrollPadding,\n scrollAreaRef,\n overflow,\n }\n\n return (\n <ScrollingListContext.Provider value={ctxValue}>\n <div\n data-spark-component=\"scrolling-list\"\n className={cx(\n 'gap-lg group/scrolling-list relative flex flex-col default:w-full',\n className\n )}\n {...rest}\n >\n {children}\n </div>\n <span ref={skipAnchorRef} className=\"size-0 overflow-hidden\" tabIndex={-1} />\n </ScrollingListContext.Provider>\n )\n}\n\nScrollingList.displayName = 'ScrollingList'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'\n\ninterface ScrollingListControls extends ComponentPropsWithoutRef<'div'> {\n /**\n * Visibility behavior of the control buttons:\n * - `always`: buttons are always visible.\n * - `hover`: buttons only appear on hover.\n *\n * a11y: `hover` is dangerous for accessibility as it disabled controls for touch screen users.\n * When using it, you must provide an alternative control outside of the list to replace them.\n */\n visibility?: 'hover' | 'always'\n children: ReactNode\n}\n\nexport const ScrollingListControls = ({\n children,\n visibility = 'always',\n className,\n ...rest\n}: ScrollingListControls) => {\n return (\n <div\n data-spark-component=\"scrolling-list-controls\"\n className={cx(\n 'default:px-md pointer-events-none absolute inset-0 flex flex-row items-center justify-between overflow-hidden',\n className\n )}\n style={\n {\n '--scrolling-list-controls-opacity': visibility === 'hover' ? '0' : '1',\n } as CSSProperties\n }\n data-orientation=\"horizontal\"\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nScrollingListControls.displayName = 'ScrollingList.Controls'\n","import { RefObject, useEffect, useState } from 'react'\n\nexport function useFocusWithinScroll<T extends HTMLElement | null>(\n ref: RefObject<T>, // The container to detect focus within\n scrollRef: RefObject<HTMLDivElement | null> // The scrollable container\n) {\n const [isFocusWithin, setIsFocusWithin] = useState(false)\n\n useEffect(() => {\n const handleFocusIn = (event: FocusEvent) => {\n setIsFocusWithin(true)\n\n const focusedElement = event.target as HTMLElement\n const scrollContainer = scrollRef.current\n\n if (focusedElement && scrollContainer) {\n const focusRect = focusedElement.getBoundingClientRect()\n const scrollRect = scrollContainer.getBoundingClientRect()\n\n // Check if the focused element is fully visible inside the scroll container\n const isFullyVisible =\n focusRect.left >= scrollRect.left &&\n focusRect.right <= scrollRect.right &&\n focusRect.top >= scrollRect.top &&\n focusRect.bottom <= scrollRect.bottom\n\n if (!isFullyVisible) {\n focusedElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n }\n }\n }\n\n const handleFocusOut = (event: FocusEvent) => {\n if (ref.current && !ref.current.contains(event.relatedTarget as Node)) {\n setIsFocusWithin(false)\n }\n }\n\n const node = ref.current\n if (node) {\n node.addEventListener('focusin', handleFocusIn)\n node.addEventListener('focusout', handleFocusOut)\n }\n\n return () => {\n if (node) {\n node.removeEventListener('focusin', handleFocusIn)\n node.removeEventListener('focusout', handleFocusOut)\n }\n }\n }, [ref, scrollRef])\n\n return isFocusWithin\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, ReactNode, useContext, useRef } from 'react'\n\nimport { Slot } from '../slot'\nimport { ScrollingListContext } from './ScrollingList'\nimport { useFocusWithinScroll } from './useFocusWithinScroll'\n\nexport interface ScrollingListItemProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Change the default rendered element for the one passed as a child, merging their props and behavior.\n */\n asChild?: boolean\n children?: ReactNode\n /**\n * DO NOT USE. This prop is automatically managed by the parent ScrollingList.ListItems\n */\n index?: number\n className?: string\n}\n\nexport const ScrollingListItem = ({\n asChild = false,\n children,\n index = 0,\n className = '',\n ...rest\n}: ScrollingListItemProps) => {\n const ctx = useContext(ScrollingListContext)\n const itemRef = useRef<HTMLDivElement>(null)\n\n const isSnapPoint = ctx.snapPointIndexes.has(index)\n\n useFocusWithinScroll(itemRef, ctx.scrollAreaRef)\n\n const Component = asChild ? Slot : 'div'\n\n return (\n <Component\n data-spark-component=\"scrolling-list-item\"\n role=\"listitem\"\n ref={itemRef}\n className={cx(\n 'default:w-auto default:shrink-0',\n {\n 'snap-start': isSnapPoint,\n 'snap-normal': isSnapPoint && ctx.snapStop === 'normal',\n 'snap-always': isSnapPoint && ctx.snapStop === 'always',\n },\n className\n )}\n {...rest}\n >\n {children}\n </Component>\n )\n}\n\nScrollingListItem.displayName = 'ScrollingList.Item'\n","import { cx } from 'class-variance-authority'\nimport {\n Children,\n cloneElement,\n ComponentPropsWithRef,\n CSSProperties,\n isValidElement,\n KeyboardEvent,\n ReactNode,\n Ref,\n RefObject,\n useContext,\n} from 'react'\n\nimport { ScrollingListContext } from './ScrollingList'\nimport { ScrollingListItemProps } from './ScrollingListItem'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n children?: ReactNode\n className?: string\n}\n\nexport function mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): Ref<T> {\n return (value: T | null) => {\n refs.forEach(ref => {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref && typeof ref === 'object' && 'current' in ref) {\n ;(ref as RefObject<T | null>).current = value\n }\n })\n }\n}\n\nexport const ScrollingListItems = ({ children, ref, className = '', ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n const snapConfig = {\n mandatory: 'x mandatory',\n proximity: 'x proximity',\n none: 'none',\n }\n\n const handleLeftArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasPrevPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasPrevPage ? ctx.activePageIndex - 1 : ctx.pages.length - 1, {\n behavior: ctx.scrollBehavior,\n })\n }\n\n const handleRightArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasNextPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasNextPage ? ctx.activePageIndex + 1 : 0, { behavior: ctx.scrollBehavior })\n }\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowLeft') {\n handleLeftArrow(event)\n }\n\n if (event.key === 'ArrowRight') {\n handleRightArrow(event)\n }\n }\n\n interface CustomCSSProperties extends CSSProperties {\n '--scrolling-list-gap'?: string\n '--scrolling-list-px'?: string\n }\n\n const inlineStyles: CustomCSSProperties = {\n scrollSnapType: snapConfig[ctx.snapType],\n scrollPaddingInline: 'var(--scrolling-list-px)',\n '--scrolling-list-px': `${ctx.scrollPadding}px`,\n '--scrolling-list-gap': `${ctx.gap}px`,\n ...(ctx.withFade && {\n maskImage:\n 'linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 44px, rgba(0, 0, 0, 1) calc(100% - 44px), rgba(0, 0, 0, 0))',\n maskSize: `calc(100% + ${ctx.overflow.left ? '0px' : '44px'} + ${ctx.overflow.right ? '0px' : '44px'}) 100%`,\n maskPosition: `${ctx.overflow.left ? '0px' : '-44px'} 0`,\n }),\n }\n\n return (\n <div\n data-spark-component=\"scrolling-list-items\"\n id=\"scrolling-list-items\"\n role=\"list\"\n className={cx(\n 'relative transition-all duration-300',\n 'u-no-scrollbar overflow-x-auto scroll-smooth',\n 'w-full gap-(--scrolling-list-gap) default:flex default:flex-row',\n 'focus-visible:u-outline',\n className\n )}\n ref={mergeRefs<HTMLDivElement>(ctx.scrollAreaRef, ctx.scrollRef, ref)}\n style={inlineStyles}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {Children.map(children, (child, index) =>\n isValidElement<ScrollingListItemProps>(child) ? cloneElement(child, { index }) : child\n )}\n </div>\n )\n}\n\nScrollingListItems.displayName = 'ScrollingList.Items'\n","import { ArrowVerticalRight } from '@spark-ui/icons/ArrowVerticalRight'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListNextButton = ({ 'aria-label': ariaLabel, ...rest }: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handleNextPage = () => {\n if (ctx.hasNextPage) {\n ctx.next({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.right)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-next-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handleNextPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalRight />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListNextButton.displayName = 'ScrollingList.NextButton'\n","import { ArrowVerticalLeft } from '@spark-ui/icons/ArrowVerticalLeft'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListPrevButton = ({\n 'aria-label': ariaLabel,\n\n ...rest\n}: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handlePrevPage = () => {\n const shouldSnapFirstPage =\n ctx.activePageIndex === 0 && (ctx.scrollAreaRef.current?.scrollLeft || 0) > 0\n\n if (shouldSnapFirstPage) {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n } else if (ctx.hasPrevPage) {\n ctx.prev({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(ctx.pages.length - 1, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.left)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-prev-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handlePrevPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalLeft />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListPrevButton.displayName = 'ScrollingList.PrevButton'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, useContext } from 'react'\n\nimport { Button } from '../button'\nimport { ScrollingListContext } from './ScrollingList'\n\ninterface Props extends ComponentPropsWithoutRef<'button'> {\n children: string\n}\n\nexport const ScrollingListSkipButton = ({ children, ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n return (\n <Button\n type=\"button\"\n design=\"tinted\"\n intent=\"surface\"\n tabIndex={0}\n className={cx(\n 'z-raised absolute top-1/2 left-0 -translate-y-1/2',\n 'not-focus-visible:pointer-events-none not-focus-visible:size-0 not-focus-visible:opacity-0'\n )}\n onClick={ctx.skipKeyboardNavigation}\n {...rest}\n >\n {children}\n </Button>\n )\n}\n\nScrollingListSkipButton.displayName = 'ScrollingList.SkipButton'\n","import { ScrollingList as Root } from './ScrollingList'\nimport { ScrollingListControls as Controls } from './ScrollingListControls'\nimport { ScrollingListItem as Item } from './ScrollingListItem'\nimport { ScrollingListItems as Items } from './ScrollingListItems'\nimport { ScrollingListNextButton as NextButton } from './ScrollingListNextButton'\nimport { ScrollingListPrevButton as PrevButton } from './ScrollingListPrevButton'\nimport { ScrollingListSkipButton as SkipButton } from './ScrollingListSkipButton'\n\n/**\n * A horizontal scrollable list component with optional snap points and navigation controls.\n */\nexport const ScrollingList: typeof Root & {\n Controls: typeof Controls\n NextButton: typeof NextButton\n PrevButton: typeof PrevButton\n Item: typeof Item\n Items: typeof Items\n SkipButton: typeof SkipButton\n} = Object.assign(Root, {\n Controls,\n NextButton,\n PrevButton,\n Item,\n Items,\n SkipButton,\n})\n\nScrollingList.displayName = 'ScrollingList'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/scrolling-list/ScrollingList.tsx","../../src/scrolling-list/ScrollingListControls.tsx","../../src/scrolling-list/useFocusWithinScroll.tsx","../../src/scrolling-list/ScrollingListItem.tsx","../../src/scrolling-list/ScrollingListItems.tsx","../../src/scrolling-list/ScrollingListNextButton.tsx","../../src/scrolling-list/ScrollingListPrevButton.tsx","../../src/scrolling-list/ScrollingListSkipButton.tsx","../../src/scrolling-list/index.ts"],"sourcesContent":["import { ScrollOverflow, useScrollOverflow } from '@spark-ui/hooks/use-scroll-overflow'\nimport { cx } from 'class-variance-authority'\nimport {\n ComponentPropsWithRef,\n createContext,\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n} from 'react'\nimport { SnapCarouselResult, useSnapCarousel } from 'react-snap-carousel'\n\ntype SnapType = 'mandatory' | 'proximity' | 'none'\ntype ScrollBehavior = 'smooth' | 'instant'\ntype SnapStop = 'normal' | 'always'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n /**\n * CSS scroll snap behavior.\n * - `mandatory` to force snapping on each \"page\".\n * - `proximity` to force snapping only when scroll position is near the edge of a \"page\". Behavior can change depending on each browser.\n * - `none` to disabled scroll snapping.\n */\n snapType?: SnapType\n /**\n * Defines whether or not the scroll container is allowed to \"pass over\" possible snap positions.\n */\n snapStop?: SnapStop\n scrollBehavior?: ScrollBehavior\n /**\n * Add a fade effect to indicate content overflow.\n */\n withFade?: boolean\n children?: ReactNode\n /**\n * When `true`, allow previous and next buttons to be used when reaching the edges of the list.\n */\n loop?: boolean\n /**\n * Space (in pixels) between items.\n */\n gap?: number\n /**\n * Offset (in pixels) of the left of the optimal viewing region of the list.\n */\n scrollPadding?: number\n className?: string\n}\n\ninterface ScrollingListContextState extends SnapCarouselResult {\n snapType: SnapType\n snapStop: SnapStop\n scrollBehavior: ScrollBehavior\n visibleItemsRange: readonly [number, number]\n loop: boolean\n gap: number\n withFade: boolean\n scrollPadding: number\n scrollAreaRef: RefObject<HTMLDivElement | null>\n overflow: ScrollOverflow\n skipKeyboardNavigation: () => void\n}\n\nexport const ScrollingListContext = createContext<ScrollingListContextState>(\n null as unknown as ScrollingListContextState\n)\n\nexport const ScrollingList = ({\n snapType = 'none',\n snapStop = 'normal',\n scrollBehavior = 'smooth',\n loop = false,\n gap = 16,\n withFade = false,\n scrollPadding = 0,\n children,\n className,\n ...rest\n}: Props) => {\n const scrollAreaRef = useRef<HTMLDivElement>(null)\n const skipAnchorRef = useRef<HTMLButtonElement>(null)\n\n const snapCarouselAPI = useSnapCarousel()\n\n const { overflow, refresh: refreshOverflow } = useScrollOverflow(scrollAreaRef, {\n precisionTreshold: 1,\n })\n\n const { activePageIndex, pages, refresh } = snapCarouselAPI\n\n const visibleItems = pages[activePageIndex] as number[]\n\n const visibleItemsRange = visibleItems\n ? ([visibleItems[0]! + 1, visibleItems[visibleItems.length - 1]! + 1] as const)\n : ([0, 0] as const)\n\n // Force refresh of the carousel API when children change\n const forceRefresh = useCallback(() => {\n if (refresh && scrollAreaRef.current) {\n // Small delay to ensure DOM is updated\n setTimeout(() => {\n refresh()\n }, 0)\n }\n }, [refresh])\n\n useEffect(() => {\n forceRefresh()\n }, [children, forceRefresh])\n\n useLayoutEffect(() => {\n if (scrollAreaRef.current) {\n // Use requestAnimationFrame to ensure proper timing with the render cycle\n // This prevents race conditions that occur when the console is closed\n requestAnimationFrame(() => {\n refreshOverflow()\n })\n }\n }, [children, refreshOverflow])\n\n const skipKeyboardNavigation = () => {\n skipAnchorRef.current?.focus()\n }\n\n const ctxValue: ScrollingListContextState = {\n ...snapCarouselAPI,\n snapType,\n snapStop,\n skipKeyboardNavigation,\n scrollBehavior,\n visibleItemsRange,\n loop,\n gap,\n withFade,\n scrollPadding,\n scrollAreaRef,\n overflow,\n }\n\n return (\n <ScrollingListContext.Provider value={ctxValue}>\n <div\n data-spark-component=\"scrolling-list\"\n className={cx(\n 'gap-lg group/scrolling-list relative flex flex-col default:w-full',\n className\n )}\n {...rest}\n >\n {children}\n </div>\n <span ref={skipAnchorRef} className=\"size-0 overflow-hidden\" tabIndex={-1} />\n </ScrollingListContext.Provider>\n )\n}\n\nScrollingList.displayName = 'ScrollingList'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'\n\ninterface ScrollingListControls extends ComponentPropsWithoutRef<'div'> {\n /**\n * Visibility behavior of the control buttons:\n * - `always`: buttons are always visible.\n * - `hover`: buttons only appear on hover.\n *\n * a11y: `hover` is dangerous for accessibility as it disabled controls for touch screen users.\n * When using it, you must provide an alternative control outside of the list to replace them.\n */\n visibility?: 'hover' | 'always'\n children: ReactNode\n}\n\nexport const ScrollingListControls = ({\n children,\n visibility = 'always',\n className,\n ...rest\n}: ScrollingListControls) => {\n return (\n <div\n data-spark-component=\"scrolling-list-controls\"\n className={cx(\n 'default:px-md pointer-events-none absolute inset-0 flex flex-row items-center justify-between overflow-hidden',\n className\n )}\n style={\n {\n '--scrolling-list-controls-opacity': visibility === 'hover' ? '0' : '1',\n } as CSSProperties\n }\n data-orientation=\"horizontal\"\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nScrollingListControls.displayName = 'ScrollingList.Controls'\n","import { RefObject, useEffect, useState } from 'react'\n\nexport function useFocusWithinScroll<T extends HTMLElement | null>(\n ref: RefObject<T>, // The container to detect focus within\n scrollRef: RefObject<HTMLDivElement | null> // The scrollable container\n) {\n const [isFocusWithin, setIsFocusWithin] = useState(false)\n\n useEffect(() => {\n const handleFocusIn = (event: FocusEvent) => {\n setIsFocusWithin(true)\n\n const focusedElement = event.target as HTMLElement\n const scrollContainer = scrollRef.current\n\n if (focusedElement && scrollContainer) {\n const focusRect = focusedElement.getBoundingClientRect()\n const scrollRect = scrollContainer.getBoundingClientRect()\n\n // Check if the focused element is fully visible inside the scroll container\n const isFullyVisible =\n focusRect.left >= scrollRect.left &&\n focusRect.right <= scrollRect.right &&\n focusRect.top >= scrollRect.top &&\n focusRect.bottom <= scrollRect.bottom\n\n if (!isFullyVisible) {\n focusedElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n }\n }\n }\n\n const handleFocusOut = (event: FocusEvent) => {\n if (ref.current && !ref.current.contains(event.relatedTarget as Node)) {\n setIsFocusWithin(false)\n }\n }\n\n const node = ref.current\n if (node) {\n node.addEventListener('focusin', handleFocusIn)\n node.addEventListener('focusout', handleFocusOut)\n }\n\n return () => {\n if (node) {\n node.removeEventListener('focusin', handleFocusIn)\n node.removeEventListener('focusout', handleFocusOut)\n }\n }\n }, [ref, scrollRef])\n\n return isFocusWithin\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, ReactNode, useContext, useRef } from 'react'\n\nimport { Slot } from '../slot'\nimport { ScrollingListContext } from './ScrollingList'\nimport { useFocusWithinScroll } from './useFocusWithinScroll'\n\nexport interface ScrollingListItemProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Change the default rendered element for the one passed as a child, merging their props and behavior.\n */\n asChild?: boolean\n children?: ReactNode\n /**\n * DO NOT USE. This prop is automatically managed by the parent ScrollingList.ListItems\n */\n index?: number\n className?: string\n}\n\nexport const ScrollingListItem = ({\n asChild = false,\n children,\n index = 0,\n className = '',\n ...rest\n}: ScrollingListItemProps) => {\n const ctx = useContext(ScrollingListContext)\n const itemRef = useRef<HTMLDivElement>(null)\n\n const isSnapPoint = ctx.snapPointIndexes.has(index)\n\n useFocusWithinScroll(itemRef, ctx.scrollAreaRef)\n\n const Component = asChild ? Slot : 'div'\n\n return (\n <Component\n data-spark-component=\"scrolling-list-item\"\n role=\"listitem\"\n ref={itemRef}\n className={cx(\n 'default:w-auto default:shrink-0',\n {\n 'snap-start': isSnapPoint,\n 'snap-normal': isSnapPoint && ctx.snapStop === 'normal',\n 'snap-always': isSnapPoint && ctx.snapStop === 'always',\n },\n className\n )}\n {...rest}\n >\n {children}\n </Component>\n )\n}\n\nScrollingListItem.displayName = 'ScrollingList.Item'\n","import { cx } from 'class-variance-authority'\nimport {\n Children,\n cloneElement,\n ComponentPropsWithRef,\n CSSProperties,\n isValidElement,\n KeyboardEvent,\n ReactNode,\n Ref,\n RefObject,\n useContext,\n} from 'react'\n\nimport { ScrollingListContext } from './ScrollingList'\nimport { ScrollingListItemProps } from './ScrollingListItem'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n children?: ReactNode\n className?: string\n}\n\nexport function mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): Ref<T> {\n return (value: T | null) => {\n refs.forEach(ref => {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref && typeof ref === 'object' && 'current' in ref) {\n ;(ref as RefObject<T | null>).current = value\n }\n })\n }\n}\n\nexport const ScrollingListItems = ({ children, ref, className = '', ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n const snapConfig = {\n mandatory: 'x mandatory',\n proximity: 'x proximity',\n none: 'none',\n }\n\n const handleLeftArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasPrevPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasPrevPage ? ctx.activePageIndex - 1 : ctx.pages.length - 1, {\n behavior: ctx.scrollBehavior,\n })\n }\n\n const handleRightArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasNextPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasNextPage ? ctx.activePageIndex + 1 : 0, { behavior: ctx.scrollBehavior })\n }\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowLeft') {\n handleLeftArrow(event)\n }\n\n if (event.key === 'ArrowRight') {\n handleRightArrow(event)\n }\n }\n\n interface CustomCSSProperties extends CSSProperties {\n '--scrolling-list-gap'?: string\n '--scrolling-list-px'?: string\n }\n\n const inlineStyles: CustomCSSProperties = {\n scrollSnapType: snapConfig[ctx.snapType],\n scrollPaddingInline: 'var(--scrolling-list-px)',\n '--scrolling-list-px': `${ctx.scrollPadding}px`,\n '--scrolling-list-gap': `${ctx.gap}px`,\n ...(ctx.withFade && {\n maskImage:\n 'linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 44px, rgba(0, 0, 0, 1) calc(100% - 44px), rgba(0, 0, 0, 0))',\n maskSize: `calc(100% + ${ctx.overflow.left ? '0px' : '44px'} + ${ctx.overflow.right ? '0px' : '44px'}) 100%`,\n maskPosition: `${ctx.overflow.left ? '0px' : '-44px'} 0`,\n }),\n }\n\n return (\n <div\n data-spark-component=\"scrolling-list-items\"\n id=\"scrolling-list-items\"\n role=\"list\"\n className={cx(\n 'relative transition-all duration-300',\n 'u-no-scrollbar overflow-x-auto scroll-smooth',\n 'w-full gap-(--scrolling-list-gap) default:flex default:flex-row',\n 'focus-visible:u-outline',\n className\n )}\n ref={mergeRefs<HTMLDivElement>(ctx.scrollAreaRef, ctx.scrollRef, ref)}\n style={inlineStyles}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {Children.map(children, (child, index) =>\n isValidElement<ScrollingListItemProps>(child) ? cloneElement(child, { index }) : child\n )}\n </div>\n )\n}\n\nScrollingListItems.displayName = 'ScrollingList.Items'\n","import { ArrowVerticalRight } from '@spark-ui/icons/ArrowVerticalRight'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListNextButton = ({ 'aria-label': ariaLabel, ...rest }: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handleNextPage = () => {\n if (ctx.hasNextPage) {\n ctx.next({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.right)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-next-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handleNextPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalRight />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListNextButton.displayName = 'ScrollingList.NextButton'\n","import { ArrowVerticalLeft } from '@spark-ui/icons/ArrowVerticalLeft'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListPrevButton = ({\n 'aria-label': ariaLabel,\n\n ...rest\n}: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handlePrevPage = () => {\n const shouldSnapFirstPage =\n ctx.activePageIndex === 0 && (ctx.scrollAreaRef.current?.scrollLeft || 0) > 0\n\n if (shouldSnapFirstPage) {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n } else if (ctx.hasPrevPage) {\n ctx.prev({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(ctx.pages.length - 1, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.left)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-prev-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handlePrevPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalLeft />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListPrevButton.displayName = 'ScrollingList.PrevButton'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, useContext } from 'react'\n\nimport { Button } from '../button'\nimport { ScrollingListContext } from './ScrollingList'\n\ninterface Props extends ComponentPropsWithoutRef<'button'> {\n children: string\n}\n\nexport const ScrollingListSkipButton = ({ children, ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n return (\n <Button\n type=\"button\"\n design=\"tinted\"\n intent=\"surface\"\n tabIndex={0}\n className={cx(\n 'z-raised absolute top-1/2 left-0 -translate-y-1/2',\n 'not-focus-visible:pointer-events-none not-focus-visible:size-0 not-focus-visible:opacity-0'\n )}\n onClick={ctx.skipKeyboardNavigation}\n {...rest}\n >\n {children}\n </Button>\n )\n}\n\nScrollingListSkipButton.displayName = 'ScrollingList.SkipButton'\n","import { ScrollingList as Root } from './ScrollingList'\nimport { ScrollingListControls as Controls } from './ScrollingListControls'\nimport { ScrollingListItem as Item } from './ScrollingListItem'\nimport { ScrollingListItems as Items } from './ScrollingListItems'\nimport { ScrollingListNextButton as NextButton } from './ScrollingListNextButton'\nimport { ScrollingListPrevButton as PrevButton } from './ScrollingListPrevButton'\nimport { ScrollingListSkipButton as SkipButton } from './ScrollingListSkipButton'\n\nexport const ScrollingList: typeof Root & {\n Controls: typeof Controls\n NextButton: typeof NextButton\n PrevButton: typeof PrevButton\n Item: typeof Item\n Items: typeof Items\n SkipButton: typeof SkipButton\n} = Object.assign(Root, {\n Controls,\n NextButton,\n PrevButton,\n Item,\n Items,\n SkipButton,\n})\n\nScrollingList.displayName = 'ScrollingList'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/scrolling-list/ScrollingList.tsx","../../src/scrolling-list/ScrollingListControls.tsx","../../src/scrolling-list/useFocusWithinScroll.tsx","../../src/scrolling-list/ScrollingListItem.tsx","../../src/scrolling-list/ScrollingListItems.tsx","../../src/scrolling-list/ScrollingListNextButton.tsx","../../src/scrolling-list/ScrollingListPrevButton.tsx","../../src/scrolling-list/ScrollingListSkipButton.tsx","../../src/scrolling-list/index.ts"],"sourcesContent":["import { ScrollOverflow, useScrollOverflow } from '@spark-ui/hooks/use-scroll-overflow'\nimport { cx } from 'class-variance-authority'\nimport {\n ComponentPropsWithRef,\n createContext,\n ReactNode,\n RefObject,\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n} from 'react'\nimport { SnapCarouselResult, useSnapCarousel } from 'react-snap-carousel'\n\ntype SnapType = 'mandatory' | 'proximity' | 'none'\ntype ScrollBehavior = 'smooth' | 'instant'\ntype SnapStop = 'normal' | 'always'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n /**\n * CSS scroll snap behavior.\n * - `mandatory` to force snapping on each \"page\".\n * - `proximity` to force snapping only when scroll position is near the edge of a \"page\". Behavior can change depending on each browser.\n * - `none` to disabled scroll snapping.\n */\n snapType?: SnapType\n /**\n * Defines whether or not the scroll container is allowed to \"pass over\" possible snap positions.\n */\n snapStop?: SnapStop\n scrollBehavior?: ScrollBehavior\n /**\n * Add a fade effect to indicate content overflow.\n */\n withFade?: boolean\n children?: ReactNode\n /**\n * When `true`, allow previous and next buttons to be used when reaching the edges of the list.\n */\n loop?: boolean\n /**\n * Space (in pixels) between items.\n */\n gap?: number\n /**\n * Offset (in pixels) of the left of the optimal viewing region of the list.\n */\n scrollPadding?: number\n className?: string\n}\n\ninterface ScrollingListContextState extends SnapCarouselResult {\n snapType: SnapType\n snapStop: SnapStop\n scrollBehavior: ScrollBehavior\n visibleItemsRange: readonly [number, number]\n loop: boolean\n gap: number\n withFade: boolean\n scrollPadding: number\n scrollAreaRef: RefObject<HTMLDivElement | null>\n overflow: ScrollOverflow\n skipKeyboardNavigation: () => void\n}\n\nexport const ScrollingListContext = createContext<ScrollingListContextState>(\n null as unknown as ScrollingListContextState\n)\n\nexport const ScrollingList = ({\n snapType = 'none',\n snapStop = 'normal',\n scrollBehavior = 'smooth',\n loop = false,\n gap = 16,\n withFade = false,\n scrollPadding = 0,\n children,\n className,\n ...rest\n}: Props) => {\n const scrollAreaRef = useRef<HTMLDivElement>(null)\n const skipAnchorRef = useRef<HTMLButtonElement>(null)\n\n const snapCarouselAPI = useSnapCarousel()\n\n const { overflow, refresh: refreshOverflow } = useScrollOverflow(scrollAreaRef, {\n precisionTreshold: 1,\n })\n\n const { activePageIndex, pages, refresh } = snapCarouselAPI\n\n const visibleItems = pages[activePageIndex] as number[]\n\n const visibleItemsRange = visibleItems\n ? ([visibleItems[0]! + 1, visibleItems[visibleItems.length - 1]! + 1] as const)\n : ([0, 0] as const)\n\n // Force refresh of the carousel API when children change\n const forceRefresh = useCallback(() => {\n if (refresh && scrollAreaRef.current) {\n // Small delay to ensure DOM is updated\n setTimeout(() => {\n refresh()\n }, 0)\n }\n }, [refresh])\n\n useEffect(() => {\n forceRefresh()\n }, [children, forceRefresh])\n\n useLayoutEffect(() => {\n if (scrollAreaRef.current) {\n // Use requestAnimationFrame to ensure proper timing with the render cycle\n // This prevents race conditions that occur when the console is closed\n requestAnimationFrame(() => {\n refreshOverflow()\n })\n }\n }, [children, refreshOverflow])\n\n const skipKeyboardNavigation = () => {\n skipAnchorRef.current?.focus()\n }\n\n const ctxValue: ScrollingListContextState = {\n ...snapCarouselAPI,\n snapType,\n snapStop,\n skipKeyboardNavigation,\n scrollBehavior,\n visibleItemsRange,\n loop,\n gap,\n withFade,\n scrollPadding,\n scrollAreaRef,\n overflow,\n }\n\n return (\n <ScrollingListContext.Provider value={ctxValue}>\n <div\n data-spark-component=\"scrolling-list\"\n className={cx(\n 'gap-lg group/scrolling-list relative flex flex-col default:w-full',\n className\n )}\n {...rest}\n >\n {children}\n </div>\n <span ref={skipAnchorRef} className=\"size-0 overflow-hidden\" tabIndex={-1} />\n </ScrollingListContext.Provider>\n )\n}\n\nScrollingList.displayName = 'ScrollingList'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'\n\ninterface ScrollingListControls extends ComponentPropsWithoutRef<'div'> {\n /**\n * Visibility behavior of the control buttons:\n * - `always`: buttons are always visible.\n * - `hover`: buttons only appear on hover.\n *\n * a11y: `hover` is dangerous for accessibility as it disabled controls for touch screen users.\n * When using it, you must provide an alternative control outside of the list to replace them.\n */\n visibility?: 'hover' | 'always'\n children: ReactNode\n}\n\nexport const ScrollingListControls = ({\n children,\n visibility = 'always',\n className,\n ...rest\n}: ScrollingListControls) => {\n return (\n <div\n data-spark-component=\"scrolling-list-controls\"\n className={cx(\n 'default:px-md pointer-events-none absolute inset-0 flex flex-row items-center justify-between overflow-hidden',\n className\n )}\n style={\n {\n '--scrolling-list-controls-opacity': visibility === 'hover' ? '0' : '1',\n } as CSSProperties\n }\n data-orientation=\"horizontal\"\n {...rest}\n >\n {children}\n </div>\n )\n}\n\nScrollingListControls.displayName = 'ScrollingList.Controls'\n","import { RefObject, useEffect, useState } from 'react'\n\nexport function useFocusWithinScroll<T extends HTMLElement | null>(\n ref: RefObject<T>, // The container to detect focus within\n scrollRef: RefObject<HTMLDivElement | null> // The scrollable container\n) {\n const [isFocusWithin, setIsFocusWithin] = useState(false)\n\n useEffect(() => {\n const handleFocusIn = (event: FocusEvent) => {\n setIsFocusWithin(true)\n\n const focusedElement = event.target as HTMLElement\n const scrollContainer = scrollRef.current\n\n if (focusedElement && scrollContainer) {\n const focusRect = focusedElement.getBoundingClientRect()\n const scrollRect = scrollContainer.getBoundingClientRect()\n\n // Check if the focused element is fully visible inside the scroll container\n const isFullyVisible =\n focusRect.left >= scrollRect.left &&\n focusRect.right <= scrollRect.right &&\n focusRect.top >= scrollRect.top &&\n focusRect.bottom <= scrollRect.bottom\n\n if (!isFullyVisible) {\n focusedElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })\n }\n }\n }\n\n const handleFocusOut = (event: FocusEvent) => {\n if (ref.current && !ref.current.contains(event.relatedTarget as Node)) {\n setIsFocusWithin(false)\n }\n }\n\n const node = ref.current\n if (node) {\n node.addEventListener('focusin', handleFocusIn)\n node.addEventListener('focusout', handleFocusOut)\n }\n\n return () => {\n if (node) {\n node.removeEventListener('focusin', handleFocusIn)\n node.removeEventListener('focusout', handleFocusOut)\n }\n }\n }, [ref, scrollRef])\n\n return isFocusWithin\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, ReactNode, useContext, useRef } from 'react'\n\nimport { Slot } from '../slot'\nimport { ScrollingListContext } from './ScrollingList'\nimport { useFocusWithinScroll } from './useFocusWithinScroll'\n\nexport interface ScrollingListItemProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * Change the default rendered element for the one passed as a child, merging their props and behavior.\n */\n asChild?: boolean\n children?: ReactNode\n /**\n * DO NOT USE. This prop is automatically managed by the parent ScrollingList.ListItems\n */\n index?: number\n className?: string\n}\n\nexport const ScrollingListItem = ({\n asChild = false,\n children,\n index = 0,\n className = '',\n ...rest\n}: ScrollingListItemProps) => {\n const ctx = useContext(ScrollingListContext)\n const itemRef = useRef<HTMLDivElement>(null)\n\n const isSnapPoint = ctx.snapPointIndexes.has(index)\n\n useFocusWithinScroll(itemRef, ctx.scrollAreaRef)\n\n const Component = asChild ? Slot : 'div'\n\n return (\n <Component\n data-spark-component=\"scrolling-list-item\"\n role=\"listitem\"\n ref={itemRef}\n className={cx(\n 'default:w-auto default:shrink-0',\n {\n 'snap-start': isSnapPoint,\n 'snap-normal': isSnapPoint && ctx.snapStop === 'normal',\n 'snap-always': isSnapPoint && ctx.snapStop === 'always',\n },\n className\n )}\n {...rest}\n >\n {children}\n </Component>\n )\n}\n\nScrollingListItem.displayName = 'ScrollingList.Item'\n","import { cx } from 'class-variance-authority'\nimport {\n Children,\n cloneElement,\n ComponentPropsWithRef,\n CSSProperties,\n isValidElement,\n KeyboardEvent,\n ReactNode,\n Ref,\n RefObject,\n useContext,\n} from 'react'\n\nimport { ScrollingListContext } from './ScrollingList'\nimport { ScrollingListItemProps } from './ScrollingListItem'\n\ninterface Props extends ComponentPropsWithRef<'div'> {\n children?: ReactNode\n className?: string\n}\n\nexport function mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): Ref<T> {\n return (value: T | null) => {\n refs.forEach(ref => {\n if (typeof ref === 'function') {\n ref(value)\n } else if (ref && typeof ref === 'object' && 'current' in ref) {\n ;(ref as RefObject<T | null>).current = value\n }\n })\n }\n}\n\nexport const ScrollingListItems = ({ children, ref, className = '', ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n const snapConfig = {\n mandatory: 'x mandatory',\n proximity: 'x proximity',\n none: 'none',\n }\n\n const handleLeftArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasPrevPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasPrevPage ? ctx.activePageIndex - 1 : ctx.pages.length - 1, {\n behavior: ctx.scrollBehavior,\n })\n }\n\n const handleRightArrow = (event: KeyboardEvent<HTMLDivElement>) => {\n if (!ctx.loop && !ctx.hasNextPage) return\n\n event.preventDefault()\n ctx.goTo(ctx.hasNextPage ? ctx.activePageIndex + 1 : 0, { behavior: ctx.scrollBehavior })\n }\n\n const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowLeft') {\n handleLeftArrow(event)\n }\n\n if (event.key === 'ArrowRight') {\n handleRightArrow(event)\n }\n }\n\n interface CustomCSSProperties extends CSSProperties {\n '--scrolling-list-gap'?: string\n '--scrolling-list-px'?: string\n }\n\n const inlineStyles: CustomCSSProperties = {\n scrollSnapType: snapConfig[ctx.snapType],\n scrollPaddingInline: 'var(--scrolling-list-px)',\n '--scrolling-list-px': `${ctx.scrollPadding}px`,\n '--scrolling-list-gap': `${ctx.gap}px`,\n ...(ctx.withFade && {\n maskImage:\n 'linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 44px, rgba(0, 0, 0, 1) calc(100% - 44px), rgba(0, 0, 0, 0))',\n maskSize: `calc(100% + ${ctx.overflow.left ? '0px' : '44px'} + ${ctx.overflow.right ? '0px' : '44px'}) 100%`,\n maskPosition: `${ctx.overflow.left ? '0px' : '-44px'} 0`,\n }),\n }\n\n return (\n <div\n data-spark-component=\"scrolling-list-items\"\n id=\"scrolling-list-items\"\n role=\"list\"\n className={cx(\n 'relative transition-all duration-300',\n 'u-no-scrollbar overflow-x-auto scroll-smooth',\n 'w-full gap-(--scrolling-list-gap) default:flex default:flex-row',\n 'focus-visible:u-outline',\n className\n )}\n ref={mergeRefs<HTMLDivElement>(ctx.scrollAreaRef, ctx.scrollRef, ref)}\n style={inlineStyles}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {Children.map(children, (child, index) =>\n isValidElement<ScrollingListItemProps>(child) ? cloneElement(child, { index }) : child\n )}\n </div>\n )\n}\n\nScrollingListItems.displayName = 'ScrollingList.Items'\n","import { ArrowVerticalRight } from '@spark-ui/icons/ArrowVerticalRight'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListNextButton = ({ 'aria-label': ariaLabel, ...rest }: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handleNextPage = () => {\n if (ctx.hasNextPage) {\n ctx.next({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.right)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-next-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handleNextPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalRight />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListNextButton.displayName = 'ScrollingList.NextButton'\n","import { ArrowVerticalLeft } from '@spark-ui/icons/ArrowVerticalLeft'\nimport { cx } from 'class-variance-authority'\nimport { useContext } from 'react'\n\nimport { Icon } from '../icon'\nimport { IconButton, IconButtonProps } from '../icon-button'\nimport { ScrollingListContext } from './ScrollingList'\n\nexport const ScrollingListPrevButton = ({\n 'aria-label': ariaLabel,\n\n ...rest\n}: IconButtonProps) => {\n const ctx = useContext(ScrollingListContext)\n\n const handlePrevPage = () => {\n const shouldSnapFirstPage =\n ctx.activePageIndex === 0 && (ctx.scrollAreaRef.current?.scrollLeft || 0) > 0\n\n if (shouldSnapFirstPage) {\n ctx.goTo(0, { behavior: ctx.scrollBehavior })\n } else if (ctx.hasPrevPage) {\n ctx.prev({ behavior: ctx.scrollBehavior })\n } else {\n ctx.goTo(ctx.pages.length - 1, { behavior: ctx.scrollBehavior })\n }\n }\n\n const listHasOverflow = ctx.overflow.left || ctx.overflow.right\n const isDisabled = !listHasOverflow || (!ctx.loop && !ctx.overflow.left)\n\n return (\n <IconButton\n data-spark-component=\"scrolling-list-prev-button\"\n size=\"sm\"\n intent=\"surface\"\n design=\"filled\"\n className={cx(\n 'pointer-events-auto opacity-(--scrolling-list-controls-opacity) shadow-sm disabled:invisible',\n 'group-hover/scrolling-list:opacity-none focus-visible:opacity-none'\n )}\n onClick={handlePrevPage}\n disabled={isDisabled}\n aria-label={ariaLabel}\n aria-controls=\"scrolling-list-items\"\n {...rest}\n >\n <Icon>\n <ArrowVerticalLeft />\n </Icon>\n </IconButton>\n )\n}\n\nScrollingListPrevButton.displayName = 'ScrollingList.PrevButton'\n","import { cx } from 'class-variance-authority'\nimport { ComponentPropsWithoutRef, useContext } from 'react'\n\nimport { Button } from '../button'\nimport { ScrollingListContext } from './ScrollingList'\n\ninterface Props extends ComponentPropsWithoutRef<'button'> {\n children: string\n}\n\nexport const ScrollingListSkipButton = ({ children, ...rest }: Props) => {\n const ctx = useContext(ScrollingListContext)\n\n return (\n <Button\n type=\"button\"\n design=\"tinted\"\n intent=\"surface\"\n tabIndex={0}\n className={cx(\n 'z-raised absolute top-1/2 left-0 -translate-y-1/2',\n 'not-focus-visible:pointer-events-none not-focus-visible:size-0 not-focus-visible:opacity-0'\n )}\n onClick={ctx.skipKeyboardNavigation}\n {...rest}\n >\n {children}\n </Button>\n )\n}\n\nScrollingListSkipButton.displayName = 'ScrollingList.SkipButton'\n","import { ScrollingList as Root } from './ScrollingList'\nimport { ScrollingListControls as Controls } from './ScrollingListControls'\nimport { ScrollingListItem as Item } from './ScrollingListItem'\nimport { ScrollingListItems as Items } from './ScrollingListItems'\nimport { ScrollingListNextButton as NextButton } from './ScrollingListNextButton'\nimport { ScrollingListPrevButton as PrevButton } from './ScrollingListPrevButton'\nimport { ScrollingListSkipButton as SkipButton } from './ScrollingListSkipButton'\n\n/**\n * A horizontal scrollable list component with optional snap points and navigation controls.\n */\nexport const ScrollingList: typeof Root & {\n Controls: typeof Controls\n NextButton: typeof NextButton\n PrevButton: typeof PrevButton\n Item: typeof Item\n Items: typeof Items\n SkipButton: typeof SkipButton\n} = Object.assign(Root, {\n Controls,\n NextButton,\n PrevButton,\n Item,\n Items,\n SkipButton,\n})\n\nScrollingList.displayName = 'ScrollingList'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/SegmentedControl.tsx","../../src/segmented-control/SegmentedControlIndicator.tsx","../../src/segmented-control/SegmentedControlItem.tsx","../../src/segmented-control/index.ts"],"sourcesContent":["import { cva, VariantProps } from 'class-variance-authority'\n\nexport const rootStyles = cva([\n 'default:self-start',\n 'group inline-grid grid-flow-col auto-cols-fr',\n 'relative items-stretch min-w-max',\n 'rounded-xl p-sm',\n 'bg-surface border-sm border-outline',\n])\n\nexport const itemStyles = cva([\n 'relative z-raised min-h-sz-44 focus-visible:outline-none',\n 'flex flex-none items-center justify-center gap-md',\n 'default:px-lg default:py-md',\n 'rounded-[20px]',\n 'cursor-pointer select-none',\n 'font-medium',\n 'transition-colors duration-150',\n 'outline-none',\n 'focus-visible:u-outline',\n 'data-disabled:cursor-not-allowed data-disabled:opacity-dim-3',\n 'data-checked:text-on-support-container',\n // Avoid layout shift: simulate \"bold\" without changing font metrics.\n // Apply only to wrapped text nodes (not arbitrary nested JSX like Tag).\n 'data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]',\n])\n\nexport const indicatorStyles = cva([\n 'absolute z-base',\n 'rounded-[20px]',\n 'bg-support-container border-md border-support',\n 'group-has-focus-visible:border-focus',\n 'transition-[left,top,width,height] duration-200 ease-in-out',\n 'pointer-events-none',\n])\n\nexport type SegmentedControlStylesProps = VariantProps<typeof itemStyles>\n","import { createContext, RefObject, useContext } from 'react'\n\nexport interface SegmentedControlContextInterface {\n checkedValue: string | null\n containerRef: RefObject<HTMLDivElement | null>\n}\n\nexport const SegmentedControlContext = createContext<SegmentedControlContextInterface>(\n {} as SegmentedControlContextInterface\n)\n\nexport const useSegmentedControlContext = () => {\n const context = useContext(SegmentedControlContext)\n\n if (!context) {\n throw Error('useSegmentedControlContext must be used within a SegmentedControlContext Provider')\n }\n\n return context\n}\n","import { RadioGroup } from '@base-ui/react/radio-group'\nimport { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport { Children, type ComponentProps, isValidElement, Ref, useRef, useState } from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlProps\n extends\n Omit<ComponentProps<typeof RadioGroup>, 'value' | 'defaultValue' | 'onValueChange'>,\n SegmentedControlStylesProps {\n /**\n * The controlled selected value.\n */\n value?: string\n /**\n * The uncontrolled default selected value.\n */\n defaultValue?: string\n /**\n * Callback fired when the selected value changes.\n */\n onValueChange?: (value: string) => void\n ref?: Ref<HTMLDivElement>\n}\n\nconst getFirstItemValue = (children: React.ReactNode): string | null => {\n let firstValue: string | null = null\n\n Children.forEach(children, child => {\n if (firstValue !== null) return\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n firstValue = (child.props as { value: string }).value\n }\n })\n\n return firstValue\n}\n\nexport const SegmentedControl = ({\n value,\n defaultValue,\n onValueChange,\n className,\n children,\n ref,\n ...rest\n}: SegmentedControlProps) => {\n const containerRef = useRef<HTMLDivElement | null>(null)\n const mergedRef = useMergeRefs(containerRef, ref)\n\n const firstValue = getFirstItemValue(children)\n\n const isControlled = value !== undefined\n const [internalValue, setInternalValue] = useState<string | null>(\n () => defaultValue ?? firstValue\n )\n const checkedValue = isControlled ? (value ?? null) : internalValue\n\n const handleValueChange = (newValue: unknown) => {\n const next = newValue as string\n\n if (!isControlled) {\n setInternalValue(next)\n }\n\n onValueChange?.(next)\n }\n\n const { labelId, description, isRequired, isInvalid, name } = useFormFieldControl()\n\n return (\n <SegmentedControlContext.Provider\n value={{\n checkedValue,\n containerRef,\n }}\n >\n <RadioGroup\n ref={mergedRef}\n value={isControlled ? value : undefined}\n defaultValue={!isControlled ? (defaultValue ?? firstValue ?? undefined) : undefined}\n onValueChange={handleValueChange}\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n name={name}\n {...rest}\n >\n {children}\n </RadioGroup>\n </SegmentedControlContext.Provider>\n )\n}\n\nSegmentedControl.displayName = 'SegmentedControl'\n","import { type ComponentProps, type CSSProperties, Ref, useEffect, useMemo, useState } from 'react'\n\nimport { indicatorStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\ninterface IndicatorRect {\n left: number\n top: number\n width: number\n height: number\n}\n\nexport interface SegmentedControlIndicatorProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n}\n\nexport const SegmentedControlIndicator = ({\n className,\n ref,\n ...rest\n}: SegmentedControlIndicatorProps) => {\n const { checkedValue, containerRef } = useSegmentedControlContext()\n const [rect, setRect] = useState<IndicatorRect | null>(null)\n\n const selector = useMemo(\n () => (checkedValue ? `[data-value=\"${CSS.escape(checkedValue)}\"]` : null),\n [checkedValue]\n )\n\n useEffect(() => {\n const container = containerRef.current\n\n if (!container) {\n return\n }\n\n const selectedItem = selector ? container.querySelector<HTMLElement>(selector) : null\n\n const update = () => {\n const currentContainer = containerRef.current\n if (!currentContainer || !selector) {\n setRect(null)\n\n return\n }\n\n const currentSelected = currentContainer.querySelector<HTMLElement>(selector)\n if (!currentSelected) {\n setRect(null)\n\n return\n }\n\n const containerRect = currentContainer.getBoundingClientRect()\n const itemRect = currentSelected.getBoundingClientRect()\n\n // Storybook canvas \"zoom\" can be implemented via `transform: scale()`.\n // In that case, `getBoundingClientRect()` returns *scaled* values, but CSS positioning/sizing\n // expects unscaled layout pixels. We infer the scale factor from offset sizes and normalize.\n const scaleX =\n currentSelected.offsetWidth > 0 ? itemRect.width / currentSelected.offsetWidth : 1\n const scaleY =\n currentSelected.offsetHeight > 0 ? itemRect.height / currentSelected.offsetHeight : 1\n\n // `getBoundingClientRect()` is border-box; absolute positioning is relative to the padding box.\n setRect({\n left: (itemRect.left - containerRect.left) / scaleX - currentContainer.clientLeft,\n top: (itemRect.top - containerRect.top) / scaleY - currentContainer.clientTop,\n width: itemRect.width / scaleX,\n height: itemRect.height / scaleY,\n })\n }\n\n update()\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n update()\n })\n : null\n\n ro?.observe(container)\n if (selectedItem) ro?.observe(selectedItem)\n\n window.addEventListener('resize', update, { passive: true })\n window.visualViewport?.addEventListener('resize', update, { passive: true })\n\n return () => {\n ro?.disconnect()\n window.removeEventListener('resize', update)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [containerRef, selector])\n\n if (!rect) return null\n\n const style: CSSProperties = {\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n }\n\n return (\n <span\n ref={ref}\n data-spark-component=\"segmented-control-indicator\"\n aria-hidden\n className={indicatorStyles({ className })}\n style={style}\n {...rest}\n />\n )\n}\n\nSegmentedControlIndicator.displayName = 'SegmentedControl.Indicator'\n","import { Radio } from '@base-ui/react/radio'\nimport { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<typeof Radio.Root>,\n 'value'\n> {\n /**\n * A unique value that identifies this item within the segmented control.\n */\n value: string\n /**\n * When true, prevents the user from interacting with this item.\n * @default false\n */\n disabled?: boolean\n ref?: Ref<HTMLElement>\n}\n\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const content = Children.toArray(children).map((child, index) => {\n if (typeof child === 'string' || typeof child === 'number') {\n return (\n <span key={`text-${index}`} data-spark-segmented-control-text>\n {child}\n </span>\n )\n }\n\n return child\n })\n\n return (\n <Radio.Root\n ref={ref}\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n value={value}\n disabled={disabled}\n className={itemStyles({ className })}\n {...rest}\n >\n {content}\n </Radio.Root>\n )\n}\n\nSegmentedControlItem.displayName = 'SegmentedControl.Item'\n","import { SegmentedControl as Root } from './SegmentedControl'\nimport { SegmentedControlIndicator as Indicator } from './SegmentedControlIndicator'\nimport { SegmentedControlItem as Item } from './SegmentedControlItem'\n\nexport const SegmentedControl: typeof Root & {\n Item: typeof Item\n Indicator: typeof Indicator\n} = Object.assign(Root, {\n Item,\n Indicator,\n})\n\nSegmentedControl.displayName = 'SegmentedControl'\nItem.displayName = 'SegmentedControl.Item'\nIndicator.displayName = 'SegmentedControl.Indicator'\n\nexport type { SegmentedControlProps } from './SegmentedControl'\nexport type { SegmentedControlItemProps } from './SegmentedControlItem'\nexport type { SegmentedControlIndicatorProps } from './SegmentedControlIndicator'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/SegmentedControl.tsx","../../src/segmented-control/SegmentedControlIndicator.tsx","../../src/segmented-control/SegmentedControlItem.tsx","../../src/segmented-control/index.ts"],"sourcesContent":["import { cva, VariantProps } from 'class-variance-authority'\n\nexport const rootStyles = cva([\n 'default:self-start',\n 'group inline-grid grid-flow-col auto-cols-fr',\n 'relative items-stretch min-w-max',\n 'rounded-xl p-sm',\n 'bg-surface border-sm border-outline',\n])\n\nexport const itemStyles = cva([\n 'relative z-raised min-h-sz-44 focus-visible:outline-none',\n 'flex flex-none items-center justify-center gap-md',\n 'default:px-lg default:py-md',\n 'rounded-[20px]',\n 'cursor-pointer select-none',\n 'font-medium',\n 'transition-colors duration-150',\n 'outline-none',\n 'focus-visible:u-outline',\n 'data-disabled:cursor-not-allowed data-disabled:opacity-dim-3',\n 'data-checked:text-on-support-container',\n // Avoid layout shift: simulate \"bold\" without changing font metrics.\n // Apply only to wrapped text nodes (not arbitrary nested JSX like Tag).\n 'data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]',\n])\n\nexport const indicatorStyles = cva([\n 'absolute z-base',\n 'rounded-[20px]',\n 'bg-support-container border-md border-support',\n 'group-has-focus-visible:border-focus',\n 'transition-[left,top,width,height] duration-200 ease-in-out',\n 'pointer-events-none',\n])\n\nexport type SegmentedControlStylesProps = VariantProps<typeof itemStyles>\n","import { createContext, RefObject, useContext } from 'react'\n\nexport interface SegmentedControlContextInterface {\n checkedValue: string | null\n containerRef: RefObject<HTMLDivElement | null>\n}\n\nexport const SegmentedControlContext = createContext<SegmentedControlContextInterface>(\n {} as SegmentedControlContextInterface\n)\n\nexport const useSegmentedControlContext = () => {\n const context = useContext(SegmentedControlContext)\n\n if (!context) {\n throw Error('useSegmentedControlContext must be used within a SegmentedControlContext Provider')\n }\n\n return context\n}\n","import { RadioGroup } from '@base-ui/react/radio-group'\nimport { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport { Children, type ComponentProps, isValidElement, Ref, useRef, useState } from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlProps\n extends\n Omit<ComponentProps<typeof RadioGroup>, 'value' | 'defaultValue' | 'onValueChange'>,\n SegmentedControlStylesProps {\n /**\n * The controlled selected value.\n */\n value?: string\n /**\n * The uncontrolled default selected value.\n */\n defaultValue?: string\n /**\n * Callback fired when the selected value changes.\n */\n onValueChange?: (value: string) => void\n ref?: Ref<HTMLDivElement>\n}\n\nconst getFirstItemValue = (children: React.ReactNode): string | null => {\n let firstValue: string | null = null\n\n Children.forEach(children, child => {\n if (firstValue !== null) return\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n firstValue = (child.props as { value: string }).value\n }\n })\n\n return firstValue\n}\n\nexport const SegmentedControl = ({\n value,\n defaultValue,\n onValueChange,\n className,\n children,\n ref,\n ...rest\n}: SegmentedControlProps) => {\n const containerRef = useRef<HTMLDivElement | null>(null)\n const mergedRef = useMergeRefs(containerRef, ref)\n\n const firstValue = getFirstItemValue(children)\n\n const isControlled = value !== undefined\n const [internalValue, setInternalValue] = useState<string | null>(\n () => defaultValue ?? firstValue\n )\n const checkedValue = isControlled ? (value ?? null) : internalValue\n\n const handleValueChange = (newValue: unknown) => {\n const next = newValue as string\n\n if (!isControlled) {\n setInternalValue(next)\n }\n\n onValueChange?.(next)\n }\n\n const { labelId, description, isRequired, isInvalid, name } = useFormFieldControl()\n\n return (\n <SegmentedControlContext.Provider\n value={{\n checkedValue,\n containerRef,\n }}\n >\n <RadioGroup\n ref={mergedRef}\n value={isControlled ? value : undefined}\n defaultValue={!isControlled ? (defaultValue ?? firstValue ?? undefined) : undefined}\n onValueChange={handleValueChange}\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n name={name}\n {...rest}\n >\n {children}\n </RadioGroup>\n </SegmentedControlContext.Provider>\n )\n}\n\nSegmentedControl.displayName = 'SegmentedControl'\n","import { type ComponentProps, type CSSProperties, Ref, useEffect, useMemo, useState } from 'react'\n\nimport { indicatorStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\ninterface IndicatorRect {\n left: number\n top: number\n width: number\n height: number\n}\n\nexport interface SegmentedControlIndicatorProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n}\n\nexport const SegmentedControlIndicator = ({\n className,\n ref,\n ...rest\n}: SegmentedControlIndicatorProps) => {\n const { checkedValue, containerRef } = useSegmentedControlContext()\n const [rect, setRect] = useState<IndicatorRect | null>(null)\n\n const selector = useMemo(\n () => (checkedValue ? `[data-value=\"${CSS.escape(checkedValue)}\"]` : null),\n [checkedValue]\n )\n\n useEffect(() => {\n const container = containerRef.current\n\n if (!container) {\n return\n }\n\n const selectedItem = selector ? container.querySelector<HTMLElement>(selector) : null\n\n const update = () => {\n const currentContainer = containerRef.current\n if (!currentContainer || !selector) {\n setRect(null)\n\n return\n }\n\n const currentSelected = currentContainer.querySelector<HTMLElement>(selector)\n if (!currentSelected) {\n setRect(null)\n\n return\n }\n\n const containerRect = currentContainer.getBoundingClientRect()\n const itemRect = currentSelected.getBoundingClientRect()\n\n // Storybook canvas \"zoom\" can be implemented via `transform: scale()`.\n // In that case, `getBoundingClientRect()` returns *scaled* values, but CSS positioning/sizing\n // expects unscaled layout pixels. We infer the scale factor from offset sizes and normalize.\n const scaleX =\n currentSelected.offsetWidth > 0 ? itemRect.width / currentSelected.offsetWidth : 1\n const scaleY =\n currentSelected.offsetHeight > 0 ? itemRect.height / currentSelected.offsetHeight : 1\n\n // `getBoundingClientRect()` is border-box; absolute positioning is relative to the padding box.\n setRect({\n left: (itemRect.left - containerRect.left) / scaleX - currentContainer.clientLeft,\n top: (itemRect.top - containerRect.top) / scaleY - currentContainer.clientTop,\n width: itemRect.width / scaleX,\n height: itemRect.height / scaleY,\n })\n }\n\n update()\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n update()\n })\n : null\n\n ro?.observe(container)\n if (selectedItem) ro?.observe(selectedItem)\n\n window.addEventListener('resize', update, { passive: true })\n window.visualViewport?.addEventListener('resize', update, { passive: true })\n\n return () => {\n ro?.disconnect()\n window.removeEventListener('resize', update)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [containerRef, selector])\n\n if (!rect) return null\n\n const style: CSSProperties = {\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n }\n\n return (\n <span\n ref={ref}\n data-spark-component=\"segmented-control-indicator\"\n aria-hidden\n className={indicatorStyles({ className })}\n style={style}\n {...rest}\n />\n )\n}\n\nSegmentedControlIndicator.displayName = 'SegmentedControl.Indicator'\n","import { Radio } from '@base-ui/react/radio'\nimport { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<typeof Radio.Root>,\n 'value'\n> {\n /**\n * A unique value that identifies this item within the segmented control.\n */\n value: string\n /**\n * When true, prevents the user from interacting with this item.\n * @default false\n */\n disabled?: boolean\n ref?: Ref<HTMLElement>\n}\n\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const content = Children.toArray(children).map((child, index) => {\n if (typeof child === 'string' || typeof child === 'number') {\n return (\n <span key={`text-${index}`} data-spark-segmented-control-text>\n {child}\n </span>\n )\n }\n\n return child\n })\n\n return (\n <Radio.Root\n ref={ref}\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n value={value}\n disabled={disabled}\n className={itemStyles({ className })}\n {...rest}\n >\n {content}\n </Radio.Root>\n )\n}\n\nSegmentedControlItem.displayName = 'SegmentedControl.Item'\n","import { SegmentedControl as Root } from './SegmentedControl'\nimport { SegmentedControlIndicator as Indicator } from './SegmentedControlIndicator'\nimport { SegmentedControlItem as Item } from './SegmentedControlItem'\n\n/**\n * A set of toggle buttons that allows users to select a single option from a group of related choices.\n */\nexport const SegmentedControl: typeof Root & {\n Item: typeof Item\n Indicator: typeof Indicator\n} = Object.assign(Root, {\n Item,\n Indicator,\n})\n\nSegmentedControl.displayName = 'SegmentedControl'\nItem.displayName = 'SegmentedControl.Item'\nIndicator.displayName = 'SegmentedControl.Indicator'\n\nexport type { SegmentedControlProps } from './SegmentedControl'\nexport type { SegmentedControlItemProps } from './SegmentedControlItem'\nexport type { SegmentedControlIndicatorProps } from './SegmentedControlIndicator'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/SegmentedControl.tsx","../../src/segmented-control/SegmentedControlIndicator.tsx","../../src/segmented-control/SegmentedControlItem.tsx","../../src/segmented-control/index.ts"],"sourcesContent":["import { cva, VariantProps } from 'class-variance-authority'\n\nexport const rootStyles = cva([\n 'default:self-start',\n 'group inline-grid grid-flow-col auto-cols-fr',\n 'relative items-stretch min-w-max',\n 'rounded-xl p-sm',\n 'bg-surface border-sm border-outline',\n])\n\nexport const itemStyles = cva([\n 'relative z-raised min-h-sz-44 focus-visible:outline-none',\n 'flex flex-none items-center justify-center gap-md',\n 'default:px-lg default:py-md',\n 'rounded-[20px]',\n 'cursor-pointer select-none',\n 'font-medium',\n 'transition-colors duration-150',\n 'outline-none',\n 'focus-visible:u-outline',\n 'data-disabled:cursor-not-allowed data-disabled:opacity-dim-3',\n 'data-checked:text-on-support-container',\n // Avoid layout shift: simulate \"bold\" without changing font metrics.\n // Apply only to wrapped text nodes (not arbitrary nested JSX like Tag).\n 'data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]',\n])\n\nexport const indicatorStyles = cva([\n 'absolute z-base',\n 'rounded-[20px]',\n 'bg-support-container border-md border-support',\n 'group-has-focus-visible:border-focus',\n 'transition-[left,top,width,height] duration-200 ease-in-out',\n 'pointer-events-none',\n])\n\nexport type SegmentedControlStylesProps = VariantProps<typeof itemStyles>\n","import { createContext, RefObject, useContext } from 'react'\n\nexport interface SegmentedControlContextInterface {\n checkedValue: string | null\n containerRef: RefObject<HTMLDivElement | null>\n}\n\nexport const SegmentedControlContext = createContext<SegmentedControlContextInterface>(\n {} as SegmentedControlContextInterface\n)\n\nexport const useSegmentedControlContext = () => {\n const context = useContext(SegmentedControlContext)\n\n if (!context) {\n throw Error('useSegmentedControlContext must be used within a SegmentedControlContext Provider')\n }\n\n return context\n}\n","import { RadioGroup } from '@base-ui/react/radio-group'\nimport { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport { Children, type ComponentProps, isValidElement, Ref, useRef, useState } from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlProps\n extends\n Omit<ComponentProps<typeof RadioGroup>, 'value' | 'defaultValue' | 'onValueChange'>,\n SegmentedControlStylesProps {\n /**\n * The controlled selected value.\n */\n value?: string\n /**\n * The uncontrolled default selected value.\n */\n defaultValue?: string\n /**\n * Callback fired when the selected value changes.\n */\n onValueChange?: (value: string) => void\n ref?: Ref<HTMLDivElement>\n}\n\nconst getFirstItemValue = (children: React.ReactNode): string | null => {\n let firstValue: string | null = null\n\n Children.forEach(children, child => {\n if (firstValue !== null) return\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n firstValue = (child.props as { value: string }).value\n }\n })\n\n return firstValue\n}\n\nexport const SegmentedControl = ({\n value,\n defaultValue,\n onValueChange,\n className,\n children,\n ref,\n ...rest\n}: SegmentedControlProps) => {\n const containerRef = useRef<HTMLDivElement | null>(null)\n const mergedRef = useMergeRefs(containerRef, ref)\n\n const firstValue = getFirstItemValue(children)\n\n const isControlled = value !== undefined\n const [internalValue, setInternalValue] = useState<string | null>(\n () => defaultValue ?? firstValue\n )\n const checkedValue = isControlled ? (value ?? null) : internalValue\n\n const handleValueChange = (newValue: unknown) => {\n const next = newValue as string\n\n if (!isControlled) {\n setInternalValue(next)\n }\n\n onValueChange?.(next)\n }\n\n const { labelId, description, isRequired, isInvalid, name } = useFormFieldControl()\n\n return (\n <SegmentedControlContext.Provider\n value={{\n checkedValue,\n containerRef,\n }}\n >\n <RadioGroup\n ref={mergedRef}\n value={isControlled ? value : undefined}\n defaultValue={!isControlled ? (defaultValue ?? firstValue ?? undefined) : undefined}\n onValueChange={handleValueChange}\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n name={name}\n {...rest}\n >\n {children}\n </RadioGroup>\n </SegmentedControlContext.Provider>\n )\n}\n\nSegmentedControl.displayName = 'SegmentedControl'\n","import { type ComponentProps, type CSSProperties, Ref, useEffect, useMemo, useState } from 'react'\n\nimport { indicatorStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\ninterface IndicatorRect {\n left: number\n top: number\n width: number\n height: number\n}\n\nexport interface SegmentedControlIndicatorProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n}\n\nexport const SegmentedControlIndicator = ({\n className,\n ref,\n ...rest\n}: SegmentedControlIndicatorProps) => {\n const { checkedValue, containerRef } = useSegmentedControlContext()\n const [rect, setRect] = useState<IndicatorRect | null>(null)\n\n const selector = useMemo(\n () => (checkedValue ? `[data-value=\"${CSS.escape(checkedValue)}\"]` : null),\n [checkedValue]\n )\n\n useEffect(() => {\n const container = containerRef.current\n\n if (!container) {\n return\n }\n\n const selectedItem = selector ? container.querySelector<HTMLElement>(selector) : null\n\n const update = () => {\n const currentContainer = containerRef.current\n if (!currentContainer || !selector) {\n setRect(null)\n\n return\n }\n\n const currentSelected = currentContainer.querySelector<HTMLElement>(selector)\n if (!currentSelected) {\n setRect(null)\n\n return\n }\n\n const containerRect = currentContainer.getBoundingClientRect()\n const itemRect = currentSelected.getBoundingClientRect()\n\n // Storybook canvas \"zoom\" can be implemented via `transform: scale()`.\n // In that case, `getBoundingClientRect()` returns *scaled* values, but CSS positioning/sizing\n // expects unscaled layout pixels. We infer the scale factor from offset sizes and normalize.\n const scaleX =\n currentSelected.offsetWidth > 0 ? itemRect.width / currentSelected.offsetWidth : 1\n const scaleY =\n currentSelected.offsetHeight > 0 ? itemRect.height / currentSelected.offsetHeight : 1\n\n // `getBoundingClientRect()` is border-box; absolute positioning is relative to the padding box.\n setRect({\n left: (itemRect.left - containerRect.left) / scaleX - currentContainer.clientLeft,\n top: (itemRect.top - containerRect.top) / scaleY - currentContainer.clientTop,\n width: itemRect.width / scaleX,\n height: itemRect.height / scaleY,\n })\n }\n\n update()\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n update()\n })\n : null\n\n ro?.observe(container)\n if (selectedItem) ro?.observe(selectedItem)\n\n window.addEventListener('resize', update, { passive: true })\n window.visualViewport?.addEventListener('resize', update, { passive: true })\n\n return () => {\n ro?.disconnect()\n window.removeEventListener('resize', update)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [containerRef, selector])\n\n if (!rect) return null\n\n const style: CSSProperties = {\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n }\n\n return (\n <span\n ref={ref}\n data-spark-component=\"segmented-control-indicator\"\n aria-hidden\n className={indicatorStyles({ className })}\n style={style}\n {...rest}\n />\n )\n}\n\nSegmentedControlIndicator.displayName = 'SegmentedControl.Indicator'\n","import { Radio } from '@base-ui/react/radio'\nimport { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<typeof Radio.Root>,\n 'value'\n> {\n /**\n * A unique value that identifies this item within the segmented control.\n */\n value: string\n /**\n * When true, prevents the user from interacting with this item.\n * @default false\n */\n disabled?: boolean\n ref?: Ref<HTMLElement>\n}\n\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const content = Children.toArray(children).map((child, index) => {\n if (typeof child === 'string' || typeof child === 'number') {\n return (\n <span key={`text-${index}`} data-spark-segmented-control-text>\n {child}\n </span>\n )\n }\n\n return child\n })\n\n return (\n <Radio.Root\n ref={ref}\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n value={value}\n disabled={disabled}\n className={itemStyles({ className })}\n {...rest}\n >\n {content}\n </Radio.Root>\n )\n}\n\nSegmentedControlItem.displayName = 'SegmentedControl.Item'\n","import { SegmentedControl as Root } from './SegmentedControl'\nimport { SegmentedControlIndicator as Indicator } from './SegmentedControlIndicator'\nimport { SegmentedControlItem as Item } from './SegmentedControlItem'\n\nexport const SegmentedControl: typeof Root & {\n Item: typeof Item\n Indicator: typeof Indicator\n} = Object.assign(Root, {\n Item,\n Indicator,\n})\n\nSegmentedControl.displayName = 'SegmentedControl'\nItem.displayName = 'SegmentedControl.Item'\nIndicator.displayName = 'SegmentedControl.Indicator'\n\nexport type { SegmentedControlProps } from './SegmentedControl'\nexport type { SegmentedControlItemProps } from './SegmentedControlItem'\nexport type { SegmentedControlIndicatorProps } from './SegmentedControlIndicator'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/SegmentedControl.tsx","../../src/segmented-control/SegmentedControlIndicator.tsx","../../src/segmented-control/SegmentedControlItem.tsx","../../src/segmented-control/index.ts"],"sourcesContent":["import { cva, VariantProps } from 'class-variance-authority'\n\nexport const rootStyles = cva([\n 'default:self-start',\n 'group inline-grid grid-flow-col auto-cols-fr',\n 'relative items-stretch min-w-max',\n 'rounded-xl p-sm',\n 'bg-surface border-sm border-outline',\n])\n\nexport const itemStyles = cva([\n 'relative z-raised min-h-sz-44 focus-visible:outline-none',\n 'flex flex-none items-center justify-center gap-md',\n 'default:px-lg default:py-md',\n 'rounded-[20px]',\n 'cursor-pointer select-none',\n 'font-medium',\n 'transition-colors duration-150',\n 'outline-none',\n 'focus-visible:u-outline',\n 'data-disabled:cursor-not-allowed data-disabled:opacity-dim-3',\n 'data-checked:text-on-support-container',\n // Avoid layout shift: simulate \"bold\" without changing font metrics.\n // Apply only to wrapped text nodes (not arbitrary nested JSX like Tag).\n 'data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]',\n])\n\nexport const indicatorStyles = cva([\n 'absolute z-base',\n 'rounded-[20px]',\n 'bg-support-container border-md border-support',\n 'group-has-focus-visible:border-focus',\n 'transition-[left,top,width,height] duration-200 ease-in-out',\n 'pointer-events-none',\n])\n\nexport type SegmentedControlStylesProps = VariantProps<typeof itemStyles>\n","import { createContext, RefObject, useContext } from 'react'\n\nexport interface SegmentedControlContextInterface {\n checkedValue: string | null\n containerRef: RefObject<HTMLDivElement | null>\n}\n\nexport const SegmentedControlContext = createContext<SegmentedControlContextInterface>(\n {} as SegmentedControlContextInterface\n)\n\nexport const useSegmentedControlContext = () => {\n const context = useContext(SegmentedControlContext)\n\n if (!context) {\n throw Error('useSegmentedControlContext must be used within a SegmentedControlContext Provider')\n }\n\n return context\n}\n","import { RadioGroup } from '@base-ui/react/radio-group'\nimport { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport { Children, type ComponentProps, isValidElement, Ref, useRef, useState } from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlProps\n extends\n Omit<ComponentProps<typeof RadioGroup>, 'value' | 'defaultValue' | 'onValueChange'>,\n SegmentedControlStylesProps {\n /**\n * The controlled selected value.\n */\n value?: string\n /**\n * The uncontrolled default selected value.\n */\n defaultValue?: string\n /**\n * Callback fired when the selected value changes.\n */\n onValueChange?: (value: string) => void\n ref?: Ref<HTMLDivElement>\n}\n\nconst getFirstItemValue = (children: React.ReactNode): string | null => {\n let firstValue: string | null = null\n\n Children.forEach(children, child => {\n if (firstValue !== null) return\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n firstValue = (child.props as { value: string }).value\n }\n })\n\n return firstValue\n}\n\nexport const SegmentedControl = ({\n value,\n defaultValue,\n onValueChange,\n className,\n children,\n ref,\n ...rest\n}: SegmentedControlProps) => {\n const containerRef = useRef<HTMLDivElement | null>(null)\n const mergedRef = useMergeRefs(containerRef, ref)\n\n const firstValue = getFirstItemValue(children)\n\n const isControlled = value !== undefined\n const [internalValue, setInternalValue] = useState<string | null>(\n () => defaultValue ?? firstValue\n )\n const checkedValue = isControlled ? (value ?? null) : internalValue\n\n const handleValueChange = (newValue: unknown) => {\n const next = newValue as string\n\n if (!isControlled) {\n setInternalValue(next)\n }\n\n onValueChange?.(next)\n }\n\n const { labelId, description, isRequired, isInvalid, name } = useFormFieldControl()\n\n return (\n <SegmentedControlContext.Provider\n value={{\n checkedValue,\n containerRef,\n }}\n >\n <RadioGroup\n ref={mergedRef}\n value={isControlled ? value : undefined}\n defaultValue={!isControlled ? (defaultValue ?? firstValue ?? undefined) : undefined}\n onValueChange={handleValueChange}\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n name={name}\n {...rest}\n >\n {children}\n </RadioGroup>\n </SegmentedControlContext.Provider>\n )\n}\n\nSegmentedControl.displayName = 'SegmentedControl'\n","import { type ComponentProps, type CSSProperties, Ref, useEffect, useMemo, useState } from 'react'\n\nimport { indicatorStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\ninterface IndicatorRect {\n left: number\n top: number\n width: number\n height: number\n}\n\nexport interface SegmentedControlIndicatorProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n}\n\nexport const SegmentedControlIndicator = ({\n className,\n ref,\n ...rest\n}: SegmentedControlIndicatorProps) => {\n const { checkedValue, containerRef } = useSegmentedControlContext()\n const [rect, setRect] = useState<IndicatorRect | null>(null)\n\n const selector = useMemo(\n () => (checkedValue ? `[data-value=\"${CSS.escape(checkedValue)}\"]` : null),\n [checkedValue]\n )\n\n useEffect(() => {\n const container = containerRef.current\n\n if (!container) {\n return\n }\n\n const selectedItem = selector ? container.querySelector<HTMLElement>(selector) : null\n\n const update = () => {\n const currentContainer = containerRef.current\n if (!currentContainer || !selector) {\n setRect(null)\n\n return\n }\n\n const currentSelected = currentContainer.querySelector<HTMLElement>(selector)\n if (!currentSelected) {\n setRect(null)\n\n return\n }\n\n const containerRect = currentContainer.getBoundingClientRect()\n const itemRect = currentSelected.getBoundingClientRect()\n\n // Storybook canvas \"zoom\" can be implemented via `transform: scale()`.\n // In that case, `getBoundingClientRect()` returns *scaled* values, but CSS positioning/sizing\n // expects unscaled layout pixels. We infer the scale factor from offset sizes and normalize.\n const scaleX =\n currentSelected.offsetWidth > 0 ? itemRect.width / currentSelected.offsetWidth : 1\n const scaleY =\n currentSelected.offsetHeight > 0 ? itemRect.height / currentSelected.offsetHeight : 1\n\n // `getBoundingClientRect()` is border-box; absolute positioning is relative to the padding box.\n setRect({\n left: (itemRect.left - containerRect.left) / scaleX - currentContainer.clientLeft,\n top: (itemRect.top - containerRect.top) / scaleY - currentContainer.clientTop,\n width: itemRect.width / scaleX,\n height: itemRect.height / scaleY,\n })\n }\n\n update()\n\n const ro =\n typeof ResizeObserver !== 'undefined'\n ? new ResizeObserver(() => {\n update()\n })\n : null\n\n ro?.observe(container)\n if (selectedItem) ro?.observe(selectedItem)\n\n window.addEventListener('resize', update, { passive: true })\n window.visualViewport?.addEventListener('resize', update, { passive: true })\n\n return () => {\n ro?.disconnect()\n window.removeEventListener('resize', update)\n window.visualViewport?.removeEventListener('resize', update)\n }\n }, [containerRef, selector])\n\n if (!rect) return null\n\n const style: CSSProperties = {\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height,\n }\n\n return (\n <span\n ref={ref}\n data-spark-component=\"segmented-control-indicator\"\n aria-hidden\n className={indicatorStyles({ className })}\n style={style}\n {...rest}\n />\n )\n}\n\nSegmentedControlIndicator.displayName = 'SegmentedControl.Indicator'\n","import { Radio } from '@base-ui/react/radio'\nimport { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<typeof Radio.Root>,\n 'value'\n> {\n /**\n * A unique value that identifies this item within the segmented control.\n */\n value: string\n /**\n * When true, prevents the user from interacting with this item.\n * @default false\n */\n disabled?: boolean\n ref?: Ref<HTMLElement>\n}\n\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const content = Children.toArray(children).map((child, index) => {\n if (typeof child === 'string' || typeof child === 'number') {\n return (\n <span key={`text-${index}`} data-spark-segmented-control-text>\n {child}\n </span>\n )\n }\n\n return child\n })\n\n return (\n <Radio.Root\n ref={ref}\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n value={value}\n disabled={disabled}\n className={itemStyles({ className })}\n {...rest}\n >\n {content}\n </Radio.Root>\n )\n}\n\nSegmentedControlItem.displayName = 'SegmentedControl.Item'\n","import { SegmentedControl as Root } from './SegmentedControl'\nimport { SegmentedControlIndicator as Indicator } from './SegmentedControlIndicator'\nimport { SegmentedControlItem as Item } from './SegmentedControlItem'\n\n/**\n * A set of toggle buttons that allows users to select a single option from a group of related choices.\n */\nexport const SegmentedControl: typeof Root & {\n Item: typeof Item\n Indicator: typeof Indicator\n} = Object.assign(Root, {\n Item,\n Indicator,\n})\n\nSegmentedControl.displayName = 'SegmentedControl'\nItem.displayName = 'SegmentedControl.Item'\nIndicator.displayName = 'SegmentedControl.Indicator'\n\nexport type { SegmentedControlProps } from './SegmentedControl'\nexport type { SegmentedControlItemProps } from './SegmentedControlItem'\nexport type { SegmentedControlIndicatorProps } from './SegmentedControlIndicator'\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/segmented-gauge/SegmentedGaugeContext.tsx","../../src/segmented-gauge/SegmentedGaugeLabel.tsx","../../src/segmented-gauge/SegmentedGaugeSegment.tsx","../../src/segmented-gauge/SegmentedGaugeTrack.tsx","../../src/segmented-gauge/SegmentedGauge.tsx","../../src/segmented-gauge/index.ts"],"sourcesContent":["import { createContext, useContext } from 'react'\n\nexport interface SegmentedGaugeContextValue {\n value?: number\n min: number\n max: number\n segments: number\n currentIndex: number\n size: 'sm' | 'md'\n intent: 'main' | 'support' | 'accent' | 'success' | 'alert' | 'danger' | 'info' | 'neutral'\n customColor?: string\n labelId: string\n gaugeId: string\n}\n\nexport const SegmentedGaugeContext = createContext<SegmentedGaugeContextValue | null>(null)\n\nexport const useSegmentedGaugeContext = () => {\n const context = useContext(SegmentedGaugeContext)\n\n if (!context) {\n throw new Error('useSegmentedGaugeContext must be used within a SegmentedGauge provider')\n }\n\n return context\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref } from 'react'\n\nimport { useSegmentedGaugeContext } from './SegmentedGaugeContext'\n\nexport interface SegmentedGaugeLabelProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n id?: string\n}\n\nexport const SegmentedGaugeLabel = ({\n className,\n children,\n ref,\n id,\n ...props\n}: SegmentedGaugeLabelProps) => {\n const { labelId } = useSegmentedGaugeContext()\n\n return (\n <span\n data-spark-component=\"segmented-gauge-label\"\n data-testid=\"segmented-gauge-label\"\n ref={ref}\n id={id || labelId}\n className={cx('default:text-on-surface default:text-body-2', className)}\n {...props}\n >\n {children}\n </span>\n )\n}\n\nSegmentedGaugeLabel.displayName = 'SegmentedGauge.Label'\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref, useMemo } from 'react'\n\nimport { useSegmentedGaugeContext } from './SegmentedGaugeContext'\n\nexport interface SegmentedGaugeSegmentProps extends ComponentProps<'div'> {\n /**\n * Index of this segment (0-based)\n */\n index?: number\n ref?: Ref<HTMLDivElement>\n}\n\nexport const SegmentedGaugeSegment = ({\n index = 0,\n className,\n children,\n ref,\n ...props\n}: SegmentedGaugeSegmentProps) => {\n const { size, intent, customColor, currentIndex } = useSegmentedGaugeContext()\n\n // Calculate isActive and isCurrent from context and index\n const isActive = currentIndex !== -1 && index <= currentIndex\n const isCurrent = currentIndex !== -1 && index === currentIndex\n\n const gaugeColor = useMemo(() => {\n // If customColor is provided, use it\n if (customColor) {\n return customColor\n }\n\n // Handle predefined intents\n switch (intent) {\n case 'main':\n return 'var(--color-main)'\n case 'support':\n return 'var(--color-support)'\n case 'accent':\n return 'var(--color-accent)'\n case 'success':\n return 'var(--color-success)'\n case 'alert':\n return 'var(--color-alert)'\n case 'danger':\n return 'var(--color-error)'\n case 'info':\n return 'var(--color-info)'\n case 'neutral':\n return 'var(--color-neutral)'\n default:\n return 'var(--color-neutral)'\n }\n }, [intent, customColor])\n\n const segmentClasses = cx(\n 'border-outline relative rounded-full',\n {\n 'h-sz-8 w-sz-24': size === 'sm',\n 'h-sz-12 w-sz-36': size === 'md',\n 'default:bg-[var(--gauge-color)]': isActive,\n 'border-sm bg-surface': !isActive,\n },\n className\n )\n\n const indicatorClasses = cx(\n 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',\n 'default:bg-surface default:rounded-full',\n 'border-[var(--gauge-color)]',\n {\n 'size-sz-12 border-md': size === 'sm',\n 'size-sz-20 border-lg': size === 'md',\n }\n )\n\n return (\n <div\n data-spark-component=\"segmented-gauge-segment\"\n data-testid=\"segmented-gauge-segment\"\n data-active={isActive}\n data-current={isCurrent}\n ref={ref}\n style={\n {\n '--gauge-color': gaugeColor,\n } as React.CSSProperties\n }\n className={segmentClasses}\n {...props}\n >\n {children}\n {isCurrent && <div className={indicatorClasses} aria-hidden=\"true\" />}\n </div>\n )\n}\n\nSegmentedGaugeSegment.displayName = 'SegmentedGauge.Segment'\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref } from 'react'\n\nexport interface SegmentedGaugeTrackProps extends ComponentProps<'div'> {\n ref?: Ref<HTMLDivElement>\n}\n\nexport const SegmentedGaugeTrack = ({\n className,\n children,\n ref,\n ...props\n}: SegmentedGaugeTrackProps) => {\n return (\n <div\n data-spark-component=\"segmented-gauge-track\"\n ref={ref}\n className={cx('gap-sm relative flex rounded-full', className)}\n {...props}\n >\n {children}\n </div>\n )\n}\n\nSegmentedGaugeTrack.displayName = 'SegmentedGauge.Track'\n","import { cx } from 'class-variance-authority'\nimport { Ref, useId, useMemo } from 'react'\n\nimport { SegmentedGaugeContext } from './SegmentedGaugeContext'\nimport { SegmentedGaugeLabel } from './SegmentedGaugeLabel'\nimport { SegmentedGaugeSegment } from './SegmentedGaugeSegment'\nimport { SegmentedGaugeTrack } from './SegmentedGaugeTrack'\n\nconst calculateCurrentIndex = (\n value: number | undefined,\n min: number,\n max: number,\n segments: number\n) => {\n // If value is undefined or null, no segment is active\n if (value == null) {\n return -1\n }\n const normalizedValue = Math.max(min, Math.min(max, value))\n const range = max - min\n const segmentSize = range / (segments - 1)\n const rawIndex = (normalizedValue - min) / segmentSize\n\n // Clamp the index to valid range\n return Math.max(0, Math.min(segments - 1, Math.round(rawIndex)))\n}\n\nexport interface SegmentedGaugeProps {\n /**\n * The current value of the gauge\n */\n value?: number\n /**\n * Minimum value of the gauge (aria-valuemin)\n */\n min: number\n /**\n * Maximum value of the gauge (aria-valuemax)\n */\n max: number\n /**\n * Description text for the gauge (aria-describedby)\n */\n description?: string\n /**\n * Size of the gauge\n */\n size?: 'sm' | 'md'\n /**\n * Intent of the gauge - predefined color intent\n */\n intent?: 'main' | 'support' | 'accent' | 'success' | 'alert' | 'danger' | 'info' | 'neutral'\n /**\n * Custom color for the gauge (hex, CSS variable, etc.)\n */\n customColor?: string\n /**\n * ID of the gauge element\n */\n id?: string\n /**\n * Accessible label for the gauge (required if no id is provided)\n */\n 'aria-label'?: string\n /**\n * Textual representation of the current value (aria-valuetext)\n * By default, percentage is used (e.g. \"33%\")\n */\n 'aria-valuetext'?: string\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Ref to the root element\n */\n ref?: Ref<HTMLDivElement>\n /**\n * Children render prop for custom rendering\n */\n children?: (props: {\n segments: {\n isActive: boolean\n isCurrent: boolean\n }[]\n currentIndex: number\n }) => React.ReactNode\n}\n\nexport const SegmentedGauge = ({\n value,\n min,\n max,\n description,\n size = 'md',\n intent = 'neutral',\n customColor,\n id,\n 'aria-label': ariaLabel,\n className,\n ref,\n children,\n ...props\n}: SegmentedGaugeProps) => {\n // Calculate segments from min and max\n const segments = max - min + 1\n const currentIndex = useMemo(\n () => calculateCurrentIndex(value, min, max, segments),\n [value, min, max, segments]\n )\n\n // Generate unique IDs\n const internalLabelId = useId()\n const generatedId = useId()\n // Use provided id or generated one for the gauge element\n const gaugeId = id || generatedId\n\n const segmentsData = useMemo(() => {\n return Array.from({ length: segments }, (_, index) => ({\n isActive: currentIndex !== -1 && index <= currentIndex,\n isCurrent: currentIndex !== -1 && index === currentIndex,\n }))\n }, [segments, currentIndex])\n\n const contextValue = useMemo(\n () => ({\n value,\n min,\n max,\n segments,\n currentIndex,\n size,\n intent,\n customColor,\n labelId: internalLabelId,\n gaugeId,\n }),\n [value, min, max, segments, currentIndex, size, intent, customColor, internalLabelId, gaugeId]\n )\n\n /**\n * A `meter` role MUST have a value. If the value is not available, the component uses a `status` role instead.\n */\n const roleProps =\n value != null\n ? {\n role: 'meter',\n 'aria-valuenow': value,\n 'aria-valuemin': min,\n 'aria-valuemax': max,\n }\n : {\n role: 'status',\n }\n\n return (\n <SegmentedGaugeContext.Provider value={contextValue}>\n <div\n id={gaugeId}\n data-spark-component=\"segmented-gauge\"\n ref={ref}\n className={cx('gap-md flex flex-wrap items-center', className)}\n {...roleProps}\n aria-labelledby={id ? `${gaugeId}-label` : undefined}\n aria-label={!id ? ariaLabel : undefined}\n aria-describedby={internalLabelId}\n {...props}\n >\n {children ? (\n children({\n segments: segmentsData,\n currentIndex,\n })\n ) : (\n <>\n <SegmentedGaugeTrack>\n {segmentsData.map((_, index) => (\n <SegmentedGaugeSegment key={index} index={index} />\n ))}\n </SegmentedGaugeTrack>\n\n {description && (\n <SegmentedGaugeLabel id={internalLabelId}>{description}</SegmentedGaugeLabel>\n )}\n </>\n )}\n </div>\n </SegmentedGaugeContext.Provider>\n )\n}\n\nSegmentedGauge.displayName = 'SegmentedGauge'\n","import { SegmentedGauge as Root } from './SegmentedGauge'\nimport { SegmentedGaugeLabel } from './SegmentedGaugeLabel'\nimport { SegmentedGaugeSegment } from './SegmentedGaugeSegment'\nimport { SegmentedGaugeTrack } from './SegmentedGaugeTrack'\n\nexport const SegmentedGauge: typeof Root & {\n Track: typeof SegmentedGaugeTrack\n Segment: typeof SegmentedGaugeSegment\n Label: typeof SegmentedGaugeLabel\n} = Object.assign(Root, {\n Track: SegmentedGaugeTrack,\n Segment: SegmentedGaugeSegment,\n Label: SegmentedGaugeLabel,\n})\n\nSegmentedGauge.displayName = 'SegmentedGauge'\nSegmentedGaugeTrack.displayName = 'SegmentedGauge.Track'\nSegmentedGaugeSegment.displayName = 'SegmentedGauge.Segment'\nSegmentedGaugeLabel.displayName = 'SegmentedGauge.Label'\n\nexport { type SegmentedGaugeProps } from './SegmentedGauge'\nexport { type SegmentedGaugeTrackProps } from './SegmentedGaugeTrack'\nexport { type SegmentedGaugeSegmentProps } from './SegmentedGaugeSegment'\nexport { type SegmentedGaugeLabelProps } from './SegmentedGaugeLabel'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/segmented-gauge/SegmentedGaugeContext.tsx","../../src/segmented-gauge/SegmentedGaugeLabel.tsx","../../src/segmented-gauge/SegmentedGaugeSegment.tsx","../../src/segmented-gauge/SegmentedGaugeTrack.tsx","../../src/segmented-gauge/SegmentedGauge.tsx","../../src/segmented-gauge/index.ts"],"sourcesContent":["import { createContext, useContext } from 'react'\n\nexport interface SegmentedGaugeContextValue {\n value?: number\n min: number\n max: number\n segments: number\n currentIndex: number\n size: 'sm' | 'md'\n intent: 'main' | 'support' | 'accent' | 'success' | 'alert' | 'danger' | 'info' | 'neutral'\n customColor?: string\n labelId: string\n gaugeId: string\n}\n\nexport const SegmentedGaugeContext = createContext<SegmentedGaugeContextValue | null>(null)\n\nexport const useSegmentedGaugeContext = () => {\n const context = useContext(SegmentedGaugeContext)\n\n if (!context) {\n throw new Error('useSegmentedGaugeContext must be used within a SegmentedGauge provider')\n }\n\n return context\n}\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref } from 'react'\n\nimport { useSegmentedGaugeContext } from './SegmentedGaugeContext'\n\nexport interface SegmentedGaugeLabelProps extends ComponentProps<'span'> {\n ref?: Ref<HTMLSpanElement>\n id?: string\n}\n\nexport const SegmentedGaugeLabel = ({\n className,\n children,\n ref,\n id,\n ...props\n}: SegmentedGaugeLabelProps) => {\n const { labelId } = useSegmentedGaugeContext()\n\n return (\n <span\n data-spark-component=\"segmented-gauge-label\"\n data-testid=\"segmented-gauge-label\"\n ref={ref}\n id={id || labelId}\n className={cx('default:text-on-surface default:text-body-2', className)}\n {...props}\n >\n {children}\n </span>\n )\n}\n\nSegmentedGaugeLabel.displayName = 'SegmentedGauge.Label'\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref, useMemo } from 'react'\n\nimport { useSegmentedGaugeContext } from './SegmentedGaugeContext'\n\nexport interface SegmentedGaugeSegmentProps extends ComponentProps<'div'> {\n /**\n * Index of this segment (0-based)\n */\n index?: number\n ref?: Ref<HTMLDivElement>\n}\n\nexport const SegmentedGaugeSegment = ({\n index = 0,\n className,\n children,\n ref,\n ...props\n}: SegmentedGaugeSegmentProps) => {\n const { size, intent, customColor, currentIndex } = useSegmentedGaugeContext()\n\n // Calculate isActive and isCurrent from context and index\n const isActive = currentIndex !== -1 && index <= currentIndex\n const isCurrent = currentIndex !== -1 && index === currentIndex\n\n const gaugeColor = useMemo(() => {\n // If customColor is provided, use it\n if (customColor) {\n return customColor\n }\n\n // Handle predefined intents\n switch (intent) {\n case 'main':\n return 'var(--color-main)'\n case 'support':\n return 'var(--color-support)'\n case 'accent':\n return 'var(--color-accent)'\n case 'success':\n return 'var(--color-success)'\n case 'alert':\n return 'var(--color-alert)'\n case 'danger':\n return 'var(--color-error)'\n case 'info':\n return 'var(--color-info)'\n case 'neutral':\n return 'var(--color-neutral)'\n default:\n return 'var(--color-neutral)'\n }\n }, [intent, customColor])\n\n const segmentClasses = cx(\n 'border-outline relative rounded-full',\n {\n 'h-sz-8 w-sz-24': size === 'sm',\n 'h-sz-12 w-sz-36': size === 'md',\n 'default:bg-[var(--gauge-color)]': isActive,\n 'border-sm bg-surface': !isActive,\n },\n className\n )\n\n const indicatorClasses = cx(\n 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',\n 'default:bg-surface default:rounded-full',\n 'border-[var(--gauge-color)]',\n {\n 'size-sz-12 border-md': size === 'sm',\n 'size-sz-20 border-lg': size === 'md',\n }\n )\n\n return (\n <div\n data-spark-component=\"segmented-gauge-segment\"\n data-testid=\"segmented-gauge-segment\"\n data-active={isActive}\n data-current={isCurrent}\n ref={ref}\n style={\n {\n '--gauge-color': gaugeColor,\n } as React.CSSProperties\n }\n className={segmentClasses}\n {...props}\n >\n {children}\n {isCurrent && <div className={indicatorClasses} aria-hidden=\"true\" />}\n </div>\n )\n}\n\nSegmentedGaugeSegment.displayName = 'SegmentedGauge.Segment'\n","import { cx } from 'class-variance-authority'\nimport { ComponentProps, Ref } from 'react'\n\nexport interface SegmentedGaugeTrackProps extends ComponentProps<'div'> {\n ref?: Ref<HTMLDivElement>\n}\n\nexport const SegmentedGaugeTrack = ({\n className,\n children,\n ref,\n ...props\n}: SegmentedGaugeTrackProps) => {\n return (\n <div\n data-spark-component=\"segmented-gauge-track\"\n ref={ref}\n className={cx('gap-sm relative flex rounded-full', className)}\n {...props}\n >\n {children}\n </div>\n )\n}\n\nSegmentedGaugeTrack.displayName = 'SegmentedGauge.Track'\n","import { cx } from 'class-variance-authority'\nimport { Ref, useId, useMemo } from 'react'\n\nimport { SegmentedGaugeContext } from './SegmentedGaugeContext'\nimport { SegmentedGaugeLabel } from './SegmentedGaugeLabel'\nimport { SegmentedGaugeSegment } from './SegmentedGaugeSegment'\nimport { SegmentedGaugeTrack } from './SegmentedGaugeTrack'\n\nconst calculateCurrentIndex = (\n value: number | undefined,\n min: number,\n max: number,\n segments: number\n) => {\n // If value is undefined or null, no segment is active\n if (value == null) {\n return -1\n }\n const normalizedValue = Math.max(min, Math.min(max, value))\n const range = max - min\n const segmentSize = range / (segments - 1)\n const rawIndex = (normalizedValue - min) / segmentSize\n\n // Clamp the index to valid range\n return Math.max(0, Math.min(segments - 1, Math.round(rawIndex)))\n}\n\nexport interface SegmentedGaugeProps {\n /**\n * The current value of the gauge\n */\n value?: number\n /**\n * Minimum value of the gauge (aria-valuemin)\n */\n min: number\n /**\n * Maximum value of the gauge (aria-valuemax)\n */\n max: number\n /**\n * Description text for the gauge (aria-describedby)\n */\n description?: string\n /**\n * Size of the gauge\n */\n size?: 'sm' | 'md'\n /**\n * Intent of the gauge - predefined color intent\n */\n intent?: 'main' | 'support' | 'accent' | 'success' | 'alert' | 'danger' | 'info' | 'neutral'\n /**\n * Custom color for the gauge (hex, CSS variable, etc.)\n */\n customColor?: string\n /**\n * ID of the gauge element\n */\n id?: string\n /**\n * Accessible label for the gauge (required if no id is provided)\n */\n 'aria-label'?: string\n /**\n * Textual representation of the current value (aria-valuetext)\n * By default, percentage is used (e.g. \"33%\")\n */\n 'aria-valuetext'?: string\n /**\n * Additional CSS classes\n */\n className?: string\n /**\n * Ref to the root element\n */\n ref?: Ref<HTMLDivElement>\n /**\n * Children render prop for custom rendering\n */\n children?: (props: {\n segments: {\n isActive: boolean\n isCurrent: boolean\n }[]\n currentIndex: number\n }) => React.ReactNode\n}\n\nexport const SegmentedGauge = ({\n value,\n min,\n max,\n description,\n size = 'md',\n intent = 'neutral',\n customColor,\n id,\n 'aria-label': ariaLabel,\n className,\n ref,\n children,\n ...props\n}: SegmentedGaugeProps) => {\n // Calculate segments from min and max\n const segments = max - min + 1\n const currentIndex = useMemo(\n () => calculateCurrentIndex(value, min, max, segments),\n [value, min, max, segments]\n )\n\n // Generate unique IDs\n const internalLabelId = useId()\n const generatedId = useId()\n // Use provided id or generated one for the gauge element\n const gaugeId = id || generatedId\n\n const segmentsData = useMemo(() => {\n return Array.from({ length: segments }, (_, index) => ({\n isActive: currentIndex !== -1 && index <= currentIndex,\n isCurrent: currentIndex !== -1 && index === currentIndex,\n }))\n }, [segments, currentIndex])\n\n const contextValue = useMemo(\n () => ({\n value,\n min,\n max,\n segments,\n currentIndex,\n size,\n intent,\n customColor,\n labelId: internalLabelId,\n gaugeId,\n }),\n [value, min, max, segments, currentIndex, size, intent, customColor, internalLabelId, gaugeId]\n )\n\n /**\n * A `meter` role MUST have a value. If the value is not available, the component uses a `status` role instead.\n */\n const roleProps =\n value != null\n ? {\n role: 'meter',\n 'aria-valuenow': value,\n 'aria-valuemin': min,\n 'aria-valuemax': max,\n }\n : {\n role: 'status',\n }\n\n return (\n <SegmentedGaugeContext.Provider value={contextValue}>\n <div\n id={gaugeId}\n data-spark-component=\"segmented-gauge\"\n ref={ref}\n className={cx('gap-md flex flex-wrap items-center', className)}\n {...roleProps}\n aria-labelledby={id ? `${gaugeId}-label` : undefined}\n aria-label={!id ? ariaLabel : undefined}\n aria-describedby={internalLabelId}\n {...props}\n >\n {children ? (\n children({\n segments: segmentsData,\n currentIndex,\n })\n ) : (\n <>\n <SegmentedGaugeTrack>\n {segmentsData.map((_, index) => (\n <SegmentedGaugeSegment key={index} index={index} />\n ))}\n </SegmentedGaugeTrack>\n\n {description && (\n <SegmentedGaugeLabel id={internalLabelId}>{description}</SegmentedGaugeLabel>\n )}\n </>\n )}\n </div>\n </SegmentedGaugeContext.Provider>\n )\n}\n\nSegmentedGauge.displayName = 'SegmentedGauge'\n","import { SegmentedGauge as Root } from './SegmentedGauge'\nimport { SegmentedGaugeLabel } from './SegmentedGaugeLabel'\nimport { SegmentedGaugeSegment } from './SegmentedGaugeSegment'\nimport { SegmentedGaugeTrack } from './SegmentedGaugeTrack'\n\n/**\n * A visual indicator that displays a value using discrete segments within a defined range.\n */\nexport const SegmentedGauge: typeof Root & {\n Track: typeof SegmentedGaugeTrack\n Segment: typeof SegmentedGaugeSegment\n Label: typeof SegmentedGaugeLabel\n} = Object.assign(Root, {\n Track: SegmentedGaugeTrack,\n Segment: SegmentedGaugeSegment,\n Label: SegmentedGaugeLabel,\n})\n\nSegmentedGauge.displayName = 'SegmentedGauge'\nSegmentedGaugeTrack.displayName = 'SegmentedGauge.Track'\nSegmentedGaugeSegment.displayName = 'SegmentedGauge.Segment'\nSegmentedGaugeLabel.displayName = 'SegmentedGauge.Label'\n\nexport { type SegmentedGaugeProps } from './SegmentedGauge'\nexport { type SegmentedGaugeTrackProps } from './SegmentedGaugeTrack'\nexport { type SegmentedGaugeSegmentProps } from './SegmentedGaugeSegment'\nexport { type SegmentedGaugeLabelProps } from './SegmentedGaugeLabel'\n"],"mappings":"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"}
|