@spark-ui/components 17.12.0 → 17.13.0

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.
@@ -1,7 +1,6 @@
1
- import { RadioGroup } from '@base-ui/react/radio-group';
2
1
  import { ComponentProps, Ref } from 'react';
3
2
  import { SegmentedControlStylesProps } from './SegmentedControl.styles';
4
- export interface SegmentedControlProps extends Omit<ComponentProps<typeof RadioGroup>, 'value' | 'defaultValue' | 'onValueChange'>, SegmentedControlStylesProps {
3
+ export interface SegmentedControlProps extends Omit<ComponentProps<'div'>, 'onValueChange'>, SegmentedControlStylesProps {
5
4
  /**
6
5
  * The controlled selected value.
7
6
  */
@@ -14,9 +13,22 @@ export interface SegmentedControlProps extends Omit<ComponentProps<typeof RadioG
14
13
  * Callback fired when the selected value changes.
15
14
  */
16
15
  onValueChange?: (value: string) => void;
16
+ /**
17
+ * Number of items per row in multi-row layout.
18
+ * When undefined, items display in a single row (default behavior).
19
+ * @default undefined
20
+ * @example
21
+ * // Create 3-column grid with wrapping rows
22
+ * <SegmentedControl rowLength={3}>
23
+ */
24
+ rowLength?: number;
25
+ /**
26
+ * The name attribute for the radio group (used in form submissions).
27
+ */
28
+ name?: string;
17
29
  ref?: Ref<HTMLDivElement>;
18
30
  }
19
31
  export declare const SegmentedControl: {
20
- ({ value, defaultValue, onValueChange, className, children, ref, ...rest }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
32
+ ({ value, defaultValue, onValueChange, className, children, rowLength, name: nameProp, ref, ...rest }: SegmentedControlProps): import("react/jsx-runtime").JSX.Element;
21
33
  displayName: string;
22
34
  };
@@ -2,6 +2,10 @@ import { RefObject } from 'react';
2
2
  export interface SegmentedControlContextInterface {
3
3
  checkedValue: string | null;
4
4
  containerRef: RefObject<HTMLDivElement | null>;
5
+ onValueChange: (value: string) => void;
6
+ name?: string;
7
+ rowLength?: number;
8
+ itemValues: string[];
5
9
  }
6
10
  export declare const SegmentedControlContext: import('react').Context<SegmentedControlContextInterface>;
7
11
  export declare const useSegmentedControlContext: () => SegmentedControlContextInterface;
@@ -1,6 +1,5 @@
1
- import { Radio } from '@base-ui/react/radio';
2
1
  import { ComponentProps, Ref } from 'react';
3
- export interface SegmentedControlItemProps extends Omit<ComponentProps<typeof Radio.Root>, 'value'> {
2
+ export interface SegmentedControlItemProps extends Omit<ComponentProps<'button'>, 'value' | 'onClick'> {
4
3
  /**
5
4
  * A unique value that identifies this item within the segmented control.
6
5
  */
@@ -10,7 +9,7 @@ export interface SegmentedControlItemProps extends Omit<ComponentProps<typeof Ra
10
9
  * @default false
11
10
  */
12
11
  disabled?: boolean;
13
- ref?: Ref<HTMLElement>;
12
+ ref?: Ref<HTMLButtonElement>;
14
13
  }
15
14
  /** A selectable item in the segmented control. Renders a <button> element. */
16
15
  export declare const SegmentedControlItem: {
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`../chunk-C91j1N6u.js`);let e=require(`class-variance-authority`),t=require(`react`),n=require(`react/jsx-runtime`),r=require(`@spark-ui/hooks/use-merge-refs`),i=require(`@spark-ui/components/form-field`),a=require(`@base-ui/react/radio-group`),o=require(`@base-ui/react/radio`);var s=(0,e.cva)([`default:self-start`,`group inline-grid grid-flow-col auto-cols-fr`,`relative items-stretch min-w-max`,`rounded-xl p-sm`,`bg-surface border-sm border-outline`]),c=(0,e.cva)([`relative z-raised min-h-sz-44 focus-visible:outline-none`,`flex flex-none items-center justify-center gap-md`,`default:px-lg default:py-md`,`rounded-[20px]`,`cursor-pointer select-none`,`font-medium`,`transition-colors duration-150`,`outline-none`,`focus-visible:u-outline`,`data-disabled:cursor-not-allowed data-disabled:opacity-dim-3`,`data-checked:text-on-support-container`,`data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]`]),l=(0,e.cva)([`absolute z-base`,`rounded-[20px]`,`bg-support-container border-md border-support`,`group-has-focus-visible:border-focus`,`transition-[left,top,width,height] duration-200 ease-in-out`,`pointer-events-none`]),u=(0,t.createContext)({}),d=()=>{let e=(0,t.useContext)(u);if(!e)throw Error(`useSegmentedControlContext must be used within a SegmentedControlContext Provider`);return e},f=e=>{let n=null;return t.Children.forEach(e,e=>{n===null&&(0,t.isValidElement)(e)&&typeof e.props.value==`string`&&(n=e.props.value)}),n},p=({value:e,defaultValue:o,onValueChange:c,className:l,children:d,ref:p,...m})=>{let h=(0,t.useRef)(null),g=(0,r.useMergeRefs)(h,p),_=f(d),v=e!==void 0,[y,b]=(0,t.useState)(()=>o??_),x=v?e??null:y,S=e=>{let t=e;v||b(t),c?.(t)},{labelId:C,description:w,isRequired:T,isInvalid:E,name:D}=(0,i.useFormFieldControl)();return(0,n.jsx)(u,{value:{checkedValue:x,containerRef:h},children:(0,n.jsx)(a.RadioGroup,{ref:g,value:v?e:void 0,defaultValue:v?void 0:o??_??void 0,onValueChange:S,"data-spark-component":`segmented-control`,className:s({className:l}),"aria-labelledby":C,"aria-describedby":w,"aria-required":T||void 0,"aria-invalid":E||void 0,name:D,...m,children:d})})};p.displayName=`SegmentedControl`;var m=({className:e,ref:r,...i})=>{let{checkedValue:a,containerRef:o}=d(),[s,c]=(0,t.useState)(null),u=(0,t.useMemo)(()=>a?`[data-value="${CSS.escape(a)}"]`:null,[a]);if((0,t.useEffect)(()=>{let e=o.current;if(!e)return;let t=u?e.querySelector(u):null,n=()=>{let e=o.current;if(!e||!u){c(null);return}let t=e.querySelector(u);if(!t){c(null);return}let n=e.getBoundingClientRect(),r=t.getBoundingClientRect(),i=t.offsetWidth>0?r.width/t.offsetWidth:1,a=t.offsetHeight>0?r.height/t.offsetHeight:1;c({left:(r.left-n.left)/i-e.clientLeft,top:(r.top-n.top)/a-e.clientTop,width:r.width/i,height:r.height/a})};n();let r=typeof ResizeObserver<`u`?new ResizeObserver(()=>{n()}):null;return r?.observe(e),t&&r?.observe(t),window.addEventListener(`resize`,n,{passive:!0}),window.visualViewport?.addEventListener(`resize`,n,{passive:!0}),()=>{r?.disconnect(),window.removeEventListener(`resize`,n),window.visualViewport?.removeEventListener(`resize`,n)}},[o,u]),!s)return null;let f={left:s.left,top:s.top,width:s.width,height:s.height};return(0,n.jsx)(`span`,{ref:r,"data-spark-component":`segmented-control-indicator`,"aria-hidden":!0,className:l({className:e}),style:f,...i})};m.displayName=`SegmentedControl.Indicator`;var h=({value:e,disabled:r=!1,children:i,className:a,ref:s,...l})=>{let u=t.Children.toArray(i).map((e,t)=>typeof e==`string`||typeof e==`number`?(0,n.jsx)(`span`,{"data-spark-segmented-control-text":!0,children:e},`text-${t}`):e);return(0,n.jsx)(o.Radio.Root,{ref:s,"data-spark-component":`segmented-control-item`,"data-value":e,value:e,disabled:r,className:c({className:a}),...l,children:u})};h.displayName=`SegmentedControl.Item`;var g=Object.assign(p,{Item:h,Indicator:m});g.displayName=`SegmentedControl`,h.displayName=`SegmentedControl.Item`,m.displayName=`SegmentedControl.Indicator`,exports.SegmentedControl=g;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`}),require(`../chunk-C91j1N6u.js`);let e=require(`class-variance-authority`),t=require(`react`),n=require(`react/jsx-runtime`),r=require(`@spark-ui/hooks/use-merge-refs`),i=require(`@spark-ui/components/form-field`);var a=(0,e.cva)([`default:self-start`,`group inline-flex flex-wrap`,`relative items-stretch min-w-max`,`rounded-xl p-sm`,`bg-surface border-sm border-outline`]),o=(0,e.cva)([`relative z-raised min-h-sz-44 focus-visible:outline-none`,`flex flex-auto items-center justify-center gap-md`,`default:px-lg default:py-md`,`rounded-[20px]`,`cursor-pointer select-none`,`font-medium`,`transition-colors duration-150`,`outline-none`,`focus-visible:u-outline`,`data-disabled:cursor-not-allowed data-disabled:opacity-dim-3`,`data-checked:text-on-support-container`,`data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]`]),s=(0,e.cva)([`absolute z-base`,`rounded-[20px]`,`bg-support-container border-md border-support`,`group-has-focus-visible:border-focus`,`transition-[left,top,width,height] duration-200 ease-in-out`,`pointer-events-none`]),c=(0,t.createContext)({}),l=()=>{let e=(0,t.useContext)(c);if(!e)throw Error(`useSegmentedControlContext must be used within a SegmentedControlContext Provider`);return e},u=({itemValues:e,containerRef:t,onValueChange:n})=>({handleKeyDown:r=>{let i=r.target.getAttribute(`data-value`);if(!i)return;let a=e.indexOf(i);if(a===-1)return;let o=null;if(o=d(r.key,a,e.length),o!==null&&o!==a){r.preventDefault();let i=0,a=e.length;for(;i<a;){let a=e[o];if(!a)return;let s=t.current?.querySelector(`[data-value="${CSS.escape(a)}"]`);if(s&&!s.hasAttribute(`data-disabled`)){s.focus(),n(a);return}let c=r.key===`ArrowRight`||r.key===`ArrowDown`?1:-1;o=(o+c+e.length)%e.length,i++}}}});function d(e,t,n){switch(e){case`ArrowRight`:case`ArrowDown`:return(t+1)%n;case`ArrowLeft`:case`ArrowUp`:return(t-1+n)%n;default:return null}}var f=e=>{let n=null;return t.Children.forEach(e,e=>{n===null&&(0,t.isValidElement)(e)&&typeof e.props.value==`string`&&(n=e.props.value)}),n},p=({value:e,defaultValue:o,onValueChange:s,className:l,children:d,rowLength:p,name:m,ref:h,...g})=>{let _=(0,t.useRef)(null),v=(0,r.useMergeRefs)(_,h),y=f(d),b=e!==void 0,[x,S]=(0,t.useState)(()=>o??y),C=b?e??null:x,w=e=>{b||S(e),s?.(e)},{labelId:T,description:E,isRequired:D,isInvalid:O,name:k}=(0,i.useFormFieldControl)(),A=m??k,j=[];t.Children.forEach(d,e=>{(0,t.isValidElement)(e)&&typeof e.props.value==`string`&&j.push(e.props.value)});let{handleKeyDown:M}=u({itemValues:j,containerRef:_,onValueChange:w}),N=p?{"--segmented-control-cols":p,rowGap:`var(--spacing-md)`}:void 0;return(0,n.jsx)(c,{value:{checkedValue:C,containerRef:_,onValueChange:w,name:A,rowLength:p,itemValues:j},children:(0,n.jsx)(`div`,{ref:v,role:`radiogroup`,"data-spark-component":`segmented-control`,className:a({className:l}),style:N,"aria-labelledby":T,"aria-describedby":E,"aria-required":D||void 0,"aria-invalid":O||void 0,onKeyDown:M,...g,children:d})})};p.displayName=`SegmentedControl`;var m=({className:e,ref:r,...i})=>{let{checkedValue:a,containerRef:o}=l(),[c,u]=(0,t.useState)(null),d=(0,t.useMemo)(()=>a?`[data-value="${CSS.escape(a)}"]`:null,[a]);if((0,t.useEffect)(()=>{let e=o.current;if(!e)return;let t=d?e.querySelector(d):null,n=()=>{let e=o.current;if(!e||!d){u(null);return}let t=e.querySelector(d);if(!t){u(null);return}let n=e.getBoundingClientRect(),r=t.getBoundingClientRect(),i=t.offsetWidth>0?r.width/t.offsetWidth:1,a=t.offsetHeight>0?r.height/t.offsetHeight:1;u({left:(r.left-n.left)/i-e.clientLeft,top:(r.top-n.top)/a-e.clientTop,width:r.width/i,height:r.height/a})};n();let r=typeof ResizeObserver<`u`?new ResizeObserver(()=>{n()}):null;return r?.observe(e),t&&r?.observe(t),window.addEventListener(`resize`,n,{passive:!0}),window.visualViewport?.addEventListener(`resize`,n,{passive:!0}),()=>{r?.disconnect(),window.removeEventListener(`resize`,n),window.visualViewport?.removeEventListener(`resize`,n)}},[o,d]),!c)return null;let f={left:c.left,top:c.top,width:c.width,height:c.height};return(0,n.jsx)(`span`,{ref:r,"data-spark-component":`segmented-control-indicator`,"aria-hidden":!0,className:s({className:e}),style:f,...i})};m.displayName=`SegmentedControl.Indicator`;var h=({value:e,disabled:r=!1,children:i,className:a,ref:s,...c})=>{let{checkedValue:u,onValueChange:d,name:f,rowLength:p,itemValues:m}=l(),h=u===e,g=()=>{r||d(e)},_=t.Children.toArray(i).map((e,t)=>typeof e==`string`||typeof e==`number`?(0,n.jsx)(`span`,{"data-spark-segmented-control-text":!0,children:e},`text-${t}`):e),v=m.indexOf(e),y=p?{flexBasis:`calc(100% / var(--segmented-control-cols))`}:void 0,b=!1;if(p&&p>0&&v!==-1){let e=p,t=v%e;b=Math.floor(v/e)<Math.ceil(m.length/e)-1&&t===0}return(0,n.jsxs)(`button`,{ref:s,type:`button`,role:`radio`,"data-spark-component":`segmented-control-item`,"data-value":e,"aria-checked":h,"data-checked":h||void 0,"data-disabled":r||void 0,disabled:r,tabIndex:h?0:-1,className:o({className:a}),style:y,onClick:g,...c,children:[_,f&&h&&(0,n.jsx)(`input`,{type:`hidden`,name:f,value:e}),b&&p&&(0,n.jsx)(`div`,{className:`bg-outline/dim-3 -mx-sm absolute left-0 h-px`,style:{bottom:`calc(var(--spacing-md) / -2)`,width:`calc(${p*100}% + var(--spacing-sm) * 2)`},"aria-hidden":`true`})]})};h.displayName=`SegmentedControl.Item`;var g=Object.assign(p,{Item:h,Indicator:m});g.displayName=`SegmentedControl`,h.displayName=`SegmentedControl.Item`,m.displayName=`SegmentedControl.Indicator`,exports.SegmentedControl=g;
2
2
  //# sourceMappingURL=index.js.map
@@ -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\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>\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\n/** The visual indicator that highlights the selected item. Renders a <span> element. */\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\n/** A selectable item in the segmented control. Renders a <button> element. */\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
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/useSegmentedControlNavigation.ts","../../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-flex flex-wrap',\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-auto 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 onValueChange: (value: string) => void\n name?: string\n rowLength?: number\n itemValues: string[]\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 { KeyboardEvent, RefObject } from 'react'\n\ninterface UseSegmentedControlNavigationProps {\n itemValues: string[]\n containerRef: RefObject<HTMLDivElement | null>\n onValueChange: (value: string) => void\n}\n\n/**\n * Custom hook that handles keyboard navigation for SegmentedControl.\n * Uses sequential left/right navigation.\n */\nexport const useSegmentedControlNavigation = ({\n itemValues,\n containerRef,\n onValueChange,\n}: UseSegmentedControlNavigationProps) => {\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // Find the currently focused item (which may differ from checkedValue during keyboard navigation)\n const focusedElement = e.target as HTMLElement\n const focusedValue = focusedElement.getAttribute('data-value')\n\n if (!focusedValue) return\n\n const currentIndex = itemValues.indexOf(focusedValue)\n if (currentIndex === -1) return\n\n let nextIndex: number | null = null\n\n // Always use 1D sequential navigation (left/right only)\n nextIndex = calculate1DNavigation(e.key, currentIndex, itemValues.length)\n\n if (nextIndex !== null && nextIndex !== currentIndex) {\n e.preventDefault()\n\n // Skip disabled items\n let attempts = 0\n const maxAttempts = itemValues.length\n\n while (attempts < maxAttempts) {\n const nextValue = itemValues[nextIndex]\n if (!nextValue) return\n\n const nextItem = containerRef.current?.querySelector<HTMLElement>(\n `[data-value=\"${CSS.escape(nextValue)}\"]`\n )\n\n // If the item is not disabled, focus it and update the value\n if (nextItem && !nextItem.hasAttribute('data-disabled')) {\n nextItem.focus()\n onValueChange(nextValue)\n return\n }\n\n // If disabled, continue in the same direction\n const direction = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : -1\n nextIndex = (nextIndex + direction + itemValues.length) % itemValues.length\n attempts++\n }\n }\n }\n\n return { handleKeyDown }\n}\n\n/**\n * Calculate next index for 1D sequential navigation.\n * Navigation wraps around at the boundaries.\n */\nfunction calculate1DNavigation(\n key: string,\n currentIndex: number,\n totalItems: number\n): number | null {\n switch (key) {\n case 'ArrowRight':\n case 'ArrowDown':\n return (currentIndex + 1) % totalItems\n case 'ArrowLeft':\n case 'ArrowUp':\n return (currentIndex - 1 + totalItems) % totalItems\n default:\n return null\n }\n}\n","import { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport {\n Children,\n type ComponentProps,\n CSSProperties,\n isValidElement,\n Ref,\n useRef,\n useState,\n} from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\nimport { useSegmentedControlNavigation } from './useSegmentedControlNavigation'\n\nexport interface SegmentedControlProps\n extends Omit<ComponentProps<'div'>, 'onValueChange'>, 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 /**\n * Number of items per row in multi-row layout.\n * When undefined, items display in a single row (default behavior).\n * @default undefined\n * @example\n * // Create 3-column grid with wrapping rows\n * <SegmentedControl rowLength={3}>\n */\n rowLength?: number\n /**\n * The name attribute for the radio group (used in form submissions).\n */\n name?: string\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 rowLength,\n name: nameProp,\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: string) => {\n if (!isControlled) {\n setInternalValue(newValue)\n }\n\n onValueChange?.(newValue)\n }\n\n const { labelId, description, isRequired, isInvalid, name: nameFromField } = useFormFieldControl()\n const name = nameProp ?? nameFromField\n\n // Get all item values in order\n const itemValues: string[] = []\n Children.forEach(children, child => {\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n itemValues.push((child.props as { value: string }).value)\n }\n })\n\n // Keyboard navigation (sequential left/right)\n const { handleKeyDown } = useSegmentedControlNavigation({\n itemValues,\n containerRef,\n onValueChange: handleValueChange,\n })\n\n // Compute dynamic flex styles for multi-row layout\n const flexStyles = rowLength\n ? ({\n '--segmented-control-cols': rowLength,\n rowGap: 'var(--spacing-md)',\n } as CSSProperties)\n : undefined\n\n return (\n <SegmentedControlContext\n value={{\n checkedValue,\n containerRef,\n onValueChange: handleValueChange,\n name,\n rowLength,\n itemValues,\n }}\n >\n <div\n ref={mergedRef}\n role=\"radiogroup\"\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n style={flexStyles}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {children}\n </div>\n </SegmentedControlContext>\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\n/** The visual indicator that highlights the selected item. Renders a <span> element. */\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 { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<'button'>,\n 'value' | 'onClick'\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<HTMLButtonElement>\n}\n\n/** A selectable item in the segmented control. Renders a <button> element. */\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const { checkedValue, onValueChange, name, rowLength, itemValues } = useSegmentedControlContext()\n\n const isChecked = checkedValue === value\n\n const handleClick = () => {\n if (!disabled) {\n onValueChange(value)\n }\n }\n\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 // Calculate flex-basis for multi-row layout and horizontal separator visibility\n const itemIndex = itemValues.indexOf(value)\n const itemStyle = rowLength\n ? { flexBasis: `calc(100% / var(--segmented-control-cols))` }\n : undefined\n\n let showHorizontalSeparator = false\n\n if (rowLength && rowLength > 0 && itemIndex !== -1) {\n const cols = rowLength\n const currentCol = itemIndex % cols\n const currentRow = Math.floor(itemIndex / cols)\n const totalRows = Math.ceil(itemValues.length / cols)\n\n // Show horizontal separator only on the first column of each row (except last row)\n showHorizontalSeparator = currentRow < totalRows - 1 && currentCol === 0\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n aria-checked={isChecked}\n data-checked={isChecked || undefined}\n data-disabled={disabled || undefined}\n disabled={disabled}\n tabIndex={isChecked ? 0 : -1}\n className={itemStyles({ className })}\n style={itemStyle}\n onClick={handleClick}\n {...rest}\n >\n {content}\n {/* Hidden input for form submission */}\n {name && isChecked && <input type=\"hidden\" name={name} value={value} />}\n {/* Horizontal separator between rows (full width across all columns) */}\n {showHorizontalSeparator && rowLength && (\n <div\n className=\"bg-outline/dim-3 -mx-sm absolute left-0 h-px\"\n style={{\n bottom: 'calc(var(--spacing-md) / -2)',\n width: `calc(${rowLength * 100}% + var(--spacing-sm) * 2)`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </button>\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,20 +1,18 @@
1
1
  import { cva as e } from "class-variance-authority";
2
2
  import { Children as t, createContext as n, isValidElement as r, useContext as i, useEffect as a, useMemo as o, useRef as s, useState as c } from "react";
3
- import { jsx as l } from "react/jsx-runtime";
4
- import { useMergeRefs as u } from "@spark-ui/hooks/use-merge-refs";
5
- import { useFormFieldControl as d } from "@spark-ui/components/form-field";
6
- import { RadioGroup as f } from "@base-ui/react/radio-group";
7
- import { Radio as p } from "@base-ui/react/radio";
3
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
4
+ import { useMergeRefs as d } from "@spark-ui/hooks/use-merge-refs";
5
+ import { useFormFieldControl as f } from "@spark-ui/components/form-field";
8
6
  //#region src/segmented-control/SegmentedControl.styles.ts
9
- var m = e([
7
+ var p = e([
10
8
  "default:self-start",
11
- "group inline-grid grid-flow-col auto-cols-fr",
9
+ "group inline-flex flex-wrap",
12
10
  "relative items-stretch min-w-max",
13
11
  "rounded-xl p-sm",
14
12
  "bg-surface border-sm border-outline"
15
- ]), h = e([
13
+ ]), m = e([
16
14
  "relative z-raised min-h-sz-44 focus-visible:outline-none",
17
- "flex flex-none items-center justify-center gap-md",
15
+ "flex flex-auto items-center justify-center gap-md",
18
16
  "default:px-lg default:py-md",
19
17
  "rounded-[20px]",
20
18
  "cursor-pointer select-none",
@@ -25,54 +23,100 @@ var m = e([
25
23
  "data-disabled:cursor-not-allowed data-disabled:opacity-dim-3",
26
24
  "data-checked:text-on-support-container",
27
25
  "data-checked:[&>[data-spark-segmented-control-text]]:[text-shadow:0.35px_0_currentColor,-0.35px_0_currentColor]"
28
- ]), g = e([
26
+ ]), h = e([
29
27
  "absolute z-base",
30
28
  "rounded-[20px]",
31
29
  "bg-support-container border-md border-support",
32
30
  "group-has-focus-visible:border-focus",
33
31
  "transition-[left,top,width,height] duration-200 ease-in-out",
34
32
  "pointer-events-none"
35
- ]), _ = n({}), v = () => {
36
- let e = i(_);
33
+ ]), g = n({}), _ = () => {
34
+ let e = i(g);
37
35
  if (!e) throw Error("useSegmentedControlContext must be used within a SegmentedControlContext Provider");
38
36
  return e;
39
- }, y = (e) => {
37
+ }, v = ({ itemValues: e, containerRef: t, onValueChange: n }) => ({ handleKeyDown: (r) => {
38
+ let i = r.target.getAttribute("data-value");
39
+ if (!i) return;
40
+ let a = e.indexOf(i);
41
+ if (a === -1) return;
42
+ let o = null;
43
+ if (o = y(r.key, a, e.length), o !== null && o !== a) {
44
+ r.preventDefault();
45
+ let i = 0, a = e.length;
46
+ for (; i < a;) {
47
+ let a = e[o];
48
+ if (!a) return;
49
+ let s = t.current?.querySelector(`[data-value="${CSS.escape(a)}"]`);
50
+ if (s && !s.hasAttribute("data-disabled")) {
51
+ s.focus(), n(a);
52
+ return;
53
+ }
54
+ let c = r.key === "ArrowRight" || r.key === "ArrowDown" ? 1 : -1;
55
+ o = (o + c + e.length) % e.length, i++;
56
+ }
57
+ }
58
+ } });
59
+ function y(e, t, n) {
60
+ switch (e) {
61
+ case "ArrowRight":
62
+ case "ArrowDown": return (t + 1) % n;
63
+ case "ArrowLeft":
64
+ case "ArrowUp": return (t - 1 + n) % n;
65
+ default: return null;
66
+ }
67
+ }
68
+ //#endregion
69
+ //#region src/segmented-control/SegmentedControl.tsx
70
+ var b = (e) => {
40
71
  let n = null;
41
72
  return t.forEach(e, (e) => {
42
73
  n === null && r(e) && typeof e.props.value == "string" && (n = e.props.value);
43
74
  }), n;
44
- }, b = ({ value: e, defaultValue: t, onValueChange: n, className: r, children: i, ref: a, ...o }) => {
45
- let p = s(null), h = u(p, a), g = y(i), v = e !== void 0, [b, x] = c(() => t ?? g), S = v ? e ?? null : b, C = (e) => {
46
- let t = e;
47
- v || x(t), n?.(t);
48
- }, { labelId: w, description: T, isRequired: E, isInvalid: D, name: O } = d();
49
- return /* @__PURE__ */ l(_, {
75
+ }, x = ({ value: e, defaultValue: n, onValueChange: i, className: a, children: o, rowLength: u, name: m, ref: h, ..._ }) => {
76
+ let y = s(null), x = d(y, h), S = b(o), C = e !== void 0, [w, T] = c(() => n ?? S), E = C ? e ?? null : w, D = (e) => {
77
+ C || T(e), i?.(e);
78
+ }, { labelId: O, description: k, isRequired: A, isInvalid: j, name: M } = f(), N = m ?? M, P = [];
79
+ t.forEach(o, (e) => {
80
+ r(e) && typeof e.props.value == "string" && P.push(e.props.value);
81
+ });
82
+ let { handleKeyDown: F } = v({
83
+ itemValues: P,
84
+ containerRef: y,
85
+ onValueChange: D
86
+ }), I = u ? {
87
+ "--segmented-control-cols": u,
88
+ rowGap: "var(--spacing-md)"
89
+ } : void 0;
90
+ return /* @__PURE__ */ l(g, {
50
91
  value: {
51
- checkedValue: S,
52
- containerRef: p
92
+ checkedValue: E,
93
+ containerRef: y,
94
+ onValueChange: D,
95
+ name: N,
96
+ rowLength: u,
97
+ itemValues: P
53
98
  },
54
- children: /* @__PURE__ */ l(f, {
55
- ref: h,
56
- value: v ? e : void 0,
57
- defaultValue: v ? void 0 : t ?? g ?? void 0,
58
- onValueChange: C,
99
+ children: /* @__PURE__ */ l("div", {
100
+ ref: x,
101
+ role: "radiogroup",
59
102
  "data-spark-component": "segmented-control",
60
- className: m({ className: r }),
61
- "aria-labelledby": w,
62
- "aria-describedby": T,
63
- "aria-required": E || void 0,
64
- "aria-invalid": D || void 0,
65
- name: O,
66
- ...o,
67
- children: i
103
+ className: p({ className: a }),
104
+ style: I,
105
+ "aria-labelledby": O,
106
+ "aria-describedby": k,
107
+ "aria-required": A || void 0,
108
+ "aria-invalid": j || void 0,
109
+ onKeyDown: F,
110
+ ..._,
111
+ children: o
68
112
  })
69
113
  });
70
114
  };
71
- b.displayName = "SegmentedControl";
115
+ x.displayName = "SegmentedControl";
72
116
  //#endregion
73
117
  //#region src/segmented-control/SegmentedControlIndicator.tsx
74
- var x = ({ className: e, ref: t, ...n }) => {
75
- let { checkedValue: r, containerRef: i } = v(), [s, u] = c(null), d = o(() => r ? `[data-value="${CSS.escape(r)}"]` : null, [r]);
118
+ var S = ({ className: e, ref: t, ...n }) => {
119
+ let { checkedValue: r, containerRef: i } = _(), [s, u] = c(null), d = o(() => r ? `[data-value="${CSS.escape(r)}"]` : null, [r]);
76
120
  if (a(() => {
77
121
  let e = i.current;
78
122
  if (!e) return;
@@ -113,39 +157,67 @@ var x = ({ className: e, ref: t, ...n }) => {
113
157
  ref: t,
114
158
  "data-spark-component": "segmented-control-indicator",
115
159
  "aria-hidden": !0,
116
- className: g({ className: e }),
160
+ className: h({ className: e }),
117
161
  style: f,
118
162
  ...n
119
163
  });
120
164
  };
121
- x.displayName = "SegmentedControl.Indicator";
165
+ S.displayName = "SegmentedControl.Indicator";
122
166
  //#endregion
123
167
  //#region src/segmented-control/SegmentedControlItem.tsx
124
- var S = ({ value: e, disabled: n = !1, children: r, className: i, ref: a, ...o }) => {
125
- let s = t.toArray(r).map((e, t) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ l("span", {
168
+ var C = ({ value: e, disabled: n = !1, children: r, className: i, ref: a, ...o }) => {
169
+ let { checkedValue: s, onValueChange: c, name: d, rowLength: f, itemValues: p } = _(), h = s === e, g = () => {
170
+ n || c(e);
171
+ }, v = t.toArray(r).map((e, t) => typeof e == "string" || typeof e == "number" ? /* @__PURE__ */ l("span", {
126
172
  "data-spark-segmented-control-text": !0,
127
173
  children: e
128
- }, `text-${t}`) : e);
129
- return /* @__PURE__ */ l(p.Root, {
174
+ }, `text-${t}`) : e), y = p.indexOf(e), b = f ? { flexBasis: "calc(100% / var(--segmented-control-cols))" } : void 0, x = !1;
175
+ if (f && f > 0 && y !== -1) {
176
+ let e = f, t = y % e;
177
+ x = Math.floor(y / e) < Math.ceil(p.length / e) - 1 && t === 0;
178
+ }
179
+ return /* @__PURE__ */ u("button", {
130
180
  ref: a,
181
+ type: "button",
182
+ role: "radio",
131
183
  "data-spark-component": "segmented-control-item",
132
184
  "data-value": e,
133
- value: e,
185
+ "aria-checked": h,
186
+ "data-checked": h || void 0,
187
+ "data-disabled": n || void 0,
134
188
  disabled: n,
135
- className: h({ className: i }),
189
+ tabIndex: h ? 0 : -1,
190
+ className: m({ className: i }),
191
+ style: b,
192
+ onClick: g,
136
193
  ...o,
137
- children: s
194
+ children: [
195
+ v,
196
+ d && h && /* @__PURE__ */ l("input", {
197
+ type: "hidden",
198
+ name: d,
199
+ value: e
200
+ }),
201
+ x && f && /* @__PURE__ */ l("div", {
202
+ className: "bg-outline/dim-3 -mx-sm absolute left-0 h-px",
203
+ style: {
204
+ bottom: "calc(var(--spacing-md) / -2)",
205
+ width: `calc(${f * 100}% + var(--spacing-sm) * 2)`
206
+ },
207
+ "aria-hidden": "true"
208
+ })
209
+ ]
138
210
  });
139
211
  };
140
- S.displayName = "SegmentedControl.Item";
212
+ C.displayName = "SegmentedControl.Item";
141
213
  //#endregion
142
214
  //#region src/segmented-control/index.ts
143
- var C = Object.assign(b, {
144
- Item: S,
145
- Indicator: x
215
+ var w = Object.assign(x, {
216
+ Item: C,
217
+ Indicator: S
146
218
  });
147
- C.displayName = "SegmentedControl", S.displayName = "SegmentedControl.Item", x.displayName = "SegmentedControl.Indicator";
219
+ w.displayName = "SegmentedControl", C.displayName = "SegmentedControl.Item", S.displayName = "SegmentedControl.Indicator";
148
220
  //#endregion
149
- export { C as SegmentedControl };
221
+ export { w as SegmentedControl };
150
222
 
151
223
  //# sourceMappingURL=index.mjs.map
@@ -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\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>\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\n/** The visual indicator that highlights the selected item. Renders a <span> element. */\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\n/** A selectable item in the segmented control. Renders a <button> element. */\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
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/segmented-control/SegmentedControl.styles.ts","../../src/segmented-control/SegmentedControlContext.tsx","../../src/segmented-control/useSegmentedControlNavigation.ts","../../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-flex flex-wrap',\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-auto 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 onValueChange: (value: string) => void\n name?: string\n rowLength?: number\n itemValues: string[]\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 { KeyboardEvent, RefObject } from 'react'\n\ninterface UseSegmentedControlNavigationProps {\n itemValues: string[]\n containerRef: RefObject<HTMLDivElement | null>\n onValueChange: (value: string) => void\n}\n\n/**\n * Custom hook that handles keyboard navigation for SegmentedControl.\n * Uses sequential left/right navigation.\n */\nexport const useSegmentedControlNavigation = ({\n itemValues,\n containerRef,\n onValueChange,\n}: UseSegmentedControlNavigationProps) => {\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // Find the currently focused item (which may differ from checkedValue during keyboard navigation)\n const focusedElement = e.target as HTMLElement\n const focusedValue = focusedElement.getAttribute('data-value')\n\n if (!focusedValue) return\n\n const currentIndex = itemValues.indexOf(focusedValue)\n if (currentIndex === -1) return\n\n let nextIndex: number | null = null\n\n // Always use 1D sequential navigation (left/right only)\n nextIndex = calculate1DNavigation(e.key, currentIndex, itemValues.length)\n\n if (nextIndex !== null && nextIndex !== currentIndex) {\n e.preventDefault()\n\n // Skip disabled items\n let attempts = 0\n const maxAttempts = itemValues.length\n\n while (attempts < maxAttempts) {\n const nextValue = itemValues[nextIndex]\n if (!nextValue) return\n\n const nextItem = containerRef.current?.querySelector<HTMLElement>(\n `[data-value=\"${CSS.escape(nextValue)}\"]`\n )\n\n // If the item is not disabled, focus it and update the value\n if (nextItem && !nextItem.hasAttribute('data-disabled')) {\n nextItem.focus()\n onValueChange(nextValue)\n return\n }\n\n // If disabled, continue in the same direction\n const direction = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? 1 : -1\n nextIndex = (nextIndex + direction + itemValues.length) % itemValues.length\n attempts++\n }\n }\n }\n\n return { handleKeyDown }\n}\n\n/**\n * Calculate next index for 1D sequential navigation.\n * Navigation wraps around at the boundaries.\n */\nfunction calculate1DNavigation(\n key: string,\n currentIndex: number,\n totalItems: number\n): number | null {\n switch (key) {\n case 'ArrowRight':\n case 'ArrowDown':\n return (currentIndex + 1) % totalItems\n case 'ArrowLeft':\n case 'ArrowUp':\n return (currentIndex - 1 + totalItems) % totalItems\n default:\n return null\n }\n}\n","import { useFormFieldControl } from '@spark-ui/components/form-field'\nimport { useMergeRefs } from '@spark-ui/hooks/use-merge-refs'\nimport {\n Children,\n type ComponentProps,\n CSSProperties,\n isValidElement,\n Ref,\n useRef,\n useState,\n} from 'react'\n\nimport type { SegmentedControlStylesProps } from './SegmentedControl.styles'\nimport { rootStyles } from './SegmentedControl.styles'\nimport { SegmentedControlContext } from './SegmentedControlContext'\nimport { useSegmentedControlNavigation } from './useSegmentedControlNavigation'\n\nexport interface SegmentedControlProps\n extends Omit<ComponentProps<'div'>, 'onValueChange'>, 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 /**\n * Number of items per row in multi-row layout.\n * When undefined, items display in a single row (default behavior).\n * @default undefined\n * @example\n * // Create 3-column grid with wrapping rows\n * <SegmentedControl rowLength={3}>\n */\n rowLength?: number\n /**\n * The name attribute for the radio group (used in form submissions).\n */\n name?: string\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 rowLength,\n name: nameProp,\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: string) => {\n if (!isControlled) {\n setInternalValue(newValue)\n }\n\n onValueChange?.(newValue)\n }\n\n const { labelId, description, isRequired, isInvalid, name: nameFromField } = useFormFieldControl()\n const name = nameProp ?? nameFromField\n\n // Get all item values in order\n const itemValues: string[] = []\n Children.forEach(children, child => {\n if (isValidElement(child) && typeof (child.props as { value?: string }).value === 'string') {\n itemValues.push((child.props as { value: string }).value)\n }\n })\n\n // Keyboard navigation (sequential left/right)\n const { handleKeyDown } = useSegmentedControlNavigation({\n itemValues,\n containerRef,\n onValueChange: handleValueChange,\n })\n\n // Compute dynamic flex styles for multi-row layout\n const flexStyles = rowLength\n ? ({\n '--segmented-control-cols': rowLength,\n rowGap: 'var(--spacing-md)',\n } as CSSProperties)\n : undefined\n\n return (\n <SegmentedControlContext\n value={{\n checkedValue,\n containerRef,\n onValueChange: handleValueChange,\n name,\n rowLength,\n itemValues,\n }}\n >\n <div\n ref={mergedRef}\n role=\"radiogroup\"\n data-spark-component=\"segmented-control\"\n className={rootStyles({ className })}\n style={flexStyles}\n aria-labelledby={labelId}\n aria-describedby={description}\n aria-required={isRequired || undefined}\n aria-invalid={isInvalid || undefined}\n onKeyDown={handleKeyDown}\n {...rest}\n >\n {children}\n </div>\n </SegmentedControlContext>\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\n/** The visual indicator that highlights the selected item. Renders a <span> element. */\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 { Children, type ComponentProps, Ref } from 'react'\n\nimport { itemStyles } from './SegmentedControl.styles'\nimport { useSegmentedControlContext } from './SegmentedControlContext'\n\nexport interface SegmentedControlItemProps extends Omit<\n ComponentProps<'button'>,\n 'value' | 'onClick'\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<HTMLButtonElement>\n}\n\n/** A selectable item in the segmented control. Renders a <button> element. */\nexport const SegmentedControlItem = ({\n value,\n disabled = false,\n children,\n className,\n ref,\n ...rest\n}: SegmentedControlItemProps) => {\n const { checkedValue, onValueChange, name, rowLength, itemValues } = useSegmentedControlContext()\n\n const isChecked = checkedValue === value\n\n const handleClick = () => {\n if (!disabled) {\n onValueChange(value)\n }\n }\n\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 // Calculate flex-basis for multi-row layout and horizontal separator visibility\n const itemIndex = itemValues.indexOf(value)\n const itemStyle = rowLength\n ? { flexBasis: `calc(100% / var(--segmented-control-cols))` }\n : undefined\n\n let showHorizontalSeparator = false\n\n if (rowLength && rowLength > 0 && itemIndex !== -1) {\n const cols = rowLength\n const currentCol = itemIndex % cols\n const currentRow = Math.floor(itemIndex / cols)\n const totalRows = Math.ceil(itemValues.length / cols)\n\n // Show horizontal separator only on the first column of each row (except last row)\n showHorizontalSeparator = currentRow < totalRows - 1 && currentCol === 0\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n role=\"radio\"\n data-spark-component=\"segmented-control-item\"\n data-value={value}\n aria-checked={isChecked}\n data-checked={isChecked || undefined}\n data-disabled={disabled || undefined}\n disabled={disabled}\n tabIndex={isChecked ? 0 : -1}\n className={itemStyles({ className })}\n style={itemStyle}\n onClick={handleClick}\n {...rest}\n >\n {content}\n {/* Hidden input for form submission */}\n {name && isChecked && <input type=\"hidden\" name={name} value={value} />}\n {/* Horizontal separator between rows (full width across all columns) */}\n {showHorizontalSeparator && rowLength && (\n <div\n className=\"bg-outline/dim-3 -mx-sm absolute left-0 h-px\"\n style={{\n bottom: 'calc(var(--spacing-md) / -2)',\n width: `calc(${rowLength * 100}% + var(--spacing-sm) * 2)`,\n }}\n aria-hidden=\"true\"\n />\n )}\n </button>\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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"}
@@ -0,0 +1,14 @@
1
+ import { KeyboardEvent, RefObject } from 'react';
2
+ interface UseSegmentedControlNavigationProps {
3
+ itemValues: string[];
4
+ containerRef: RefObject<HTMLDivElement | null>;
5
+ onValueChange: (value: string) => void;
6
+ }
7
+ /**
8
+ * Custom hook that handles keyboard navigation for SegmentedControl.
9
+ * Uses sequential left/right navigation.
10
+ */
11
+ export declare const useSegmentedControlNavigation: ({ itemValues, containerRef, onValueChange, }: UseSegmentedControlNavigationProps) => {
12
+ handleKeyDown: (e: KeyboardEvent<HTMLDivElement>) => void;
13
+ };
14
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-ui/components",
3
- "version": "17.12.0",
3
+ "version": "17.13.0",
4
4
  "license": "MIT",
5
5
  "description": "Spark (Leboncoin design system) components.",
6
6
  "exports": {
@@ -48,9 +48,9 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@base-ui/react": "^1.5.0",
51
- "@spark-ui/hooks": "17.12.0",
52
- "@spark-ui/icons": "17.12.0",
53
- "@spark-ui/internal-utils": "17.12.0",
51
+ "@spark-ui/hooks": "17.13.0",
52
+ "@spark-ui/icons": "17.13.0",
53
+ "@spark-ui/internal-utils": "17.13.0",
54
54
  "@zag-js/pagination": "1.30.0",
55
55
  "@zag-js/react": "1.30.0",
56
56
  "class-variance-authority": "0.7.1",
@@ -62,7 +62,7 @@
62
62
  "react-snap-carousel": "0.5.1"
63
63
  },
64
64
  "peerDependencies": {
65
- "@spark-ui/theme-utils": "17.12.0",
65
+ "@spark-ui/theme-utils": "17.13.0",
66
66
  "react": "19.2.4",
67
67
  "react-dom": "19.2.4",
68
68
  "tailwindcss": "4.1.18"