@epilot/spark-ui 1.0.0-alpha.4 → 1.0.0-alpha.5
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/accent-colors-Ch2J0EEY.js +13 -0
- package/dist/accent-colors-Ch2J0EEY.js.map +1 -0
- package/dist/accent-colors-CjHCf8fH.cjs +18 -0
- package/dist/accent-colors-CjHCf8fH.cjs.map +1 -0
- package/dist/components/accordion.cjs +1 -1
- package/dist/components/accordion.d.ts +1 -1
- package/dist/components/accordion.js +1 -1
- package/dist/components/attachment.d.cts +2 -2
- package/dist/components/attachment.d.ts +2 -2
- package/dist/components/badge.cjs +12 -7
- package/dist/components/badge.cjs.map +1 -1
- package/dist/components/badge.d.cts +5 -1
- package/dist/components/badge.d.ts +5 -1
- package/dist/components/badge.js +12 -7
- package/dist/components/badge.js.map +1 -1
- package/dist/components/bar-chart.cjs +12 -9
- package/dist/components/bar-chart.cjs.map +1 -1
- package/dist/components/bar-chart.js +12 -9
- package/dist/components/bar-chart.js.map +1 -1
- package/dist/components/battery-progress-row.cjs +10 -16
- package/dist/components/battery-progress-row.cjs.map +1 -1
- package/dist/components/battery-progress-row.d.cts +1 -1
- package/dist/components/battery-progress-row.d.ts +2 -2
- package/dist/components/battery-progress-row.js +10 -16
- package/dist/components/battery-progress-row.js.map +1 -1
- package/dist/components/bubble.d.cts +1 -1
- package/dist/components/bubble.d.ts +1 -1
- package/dist/components/button.cjs +5 -2
- package/dist/components/button.cjs.map +1 -1
- package/dist/components/button.d.cts +8 -7
- package/dist/components/button.d.ts +8 -7
- package/dist/components/button.js +5 -2
- package/dist/components/button.js.map +1 -1
- package/dist/components/card.d.cts +1 -1
- package/dist/components/card.d.ts +1 -1
- package/dist/components/chart.cjs +1 -1
- package/dist/components/chart.cjs.map +1 -1
- package/dist/components/chart.js +1 -1
- package/dist/components/chart.js.map +1 -1
- package/dist/components/checkbox.cjs +30 -23
- package/dist/components/checkbox.cjs.map +1 -1
- package/dist/components/checkbox.d.cts +5 -1
- package/dist/components/checkbox.d.ts +5 -1
- package/dist/components/checkbox.js +30 -23
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/circular-gauge.d.ts +1 -1
- package/dist/components/day-of-week-picker.cjs +2 -2
- package/dist/components/day-of-week-picker.cjs.map +1 -1
- package/dist/components/day-of-week-picker.js +2 -2
- package/dist/components/day-of-week-picker.js.map +1 -1
- package/dist/components/device-card.d.ts +1 -1
- package/dist/components/donut-chart.cjs +3 -3
- package/dist/components/donut-chart.cjs.map +1 -1
- package/dist/components/donut-chart.js +3 -3
- package/dist/components/donut-chart.js.map +1 -1
- package/dist/components/energy-bar-chart.d.ts +1 -1
- package/dist/components/energy-breakdown.cjs +1 -1
- package/dist/components/energy-breakdown.cjs.map +1 -1
- package/dist/components/energy-breakdown.js +1 -1
- package/dist/components/energy-breakdown.js.map +1 -1
- package/dist/components/feed-in-hero.d.ts +1 -1
- package/dist/components/field.cjs +1 -1
- package/dist/components/field.d.cts +1 -1
- package/dist/components/field.d.ts +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/info-block.d.cts +1 -1
- package/dist/components/info-block.d.ts +1 -1
- package/dist/components/input-group.cjs +1 -1
- package/dist/components/input-group.js +1 -1
- package/dist/components/input-otp.cjs +1 -1
- package/dist/components/input-otp.js +1 -1
- package/dist/components/input.cjs +1 -1
- package/dist/components/input.d.cts +1 -1
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/line-chart.cjs +11 -9
- package/dist/components/line-chart.cjs.map +1 -1
- package/dist/components/line-chart.js +11 -9
- package/dist/components/line-chart.js.map +1 -1
- package/dist/components/metering-row.d.ts +1 -1
- package/dist/components/money-saved-block.cjs +1 -1
- package/dist/components/money-saved-block.cjs.map +1 -1
- package/dist/components/money-saved-block.js +1 -1
- package/dist/components/money-saved-block.js.map +1 -1
- package/dist/components/number-flow.d.ts +1 -1
- package/dist/components/power-history-chart.cjs +9 -7
- package/dist/components/power-history-chart.cjs.map +1 -1
- package/dist/components/power-history-chart.js +9 -7
- package/dist/components/power-history-chart.js.map +1 -1
- package/dist/components/power-history.cjs +1 -1
- package/dist/components/power-history.cjs.map +1 -1
- package/dist/components/power-history.js +1 -1
- package/dist/components/power-history.js.map +1 -1
- package/dist/components/power-network.cjs +36 -11
- package/dist/components/power-network.cjs.map +1 -1
- package/dist/components/power-network.js +36 -11
- package/dist/components/power-network.js.map +1 -1
- package/dist/components/progress.cjs +1 -1
- package/dist/components/progress.cjs.map +1 -1
- package/dist/components/progress.js +1 -1
- package/dist/components/progress.js.map +1 -1
- package/dist/components/radio-card-group.cjs +26 -1
- package/dist/components/radio-card-group.cjs.map +1 -1
- package/dist/components/radio-card-group.js +26 -1
- package/dist/components/radio-card-group.js.map +1 -1
- package/dist/components/radio.cjs +11 -4
- package/dist/components/radio.cjs.map +1 -1
- package/dist/components/radio.d.cts +15 -3
- package/dist/components/radio.d.ts +15 -3
- package/dist/components/radio.js +11 -4
- package/dist/components/radio.js.map +1 -1
- package/dist/components/sankey-chart.cjs +4 -4
- package/dist/components/sankey-chart.cjs.map +1 -1
- package/dist/components/sankey-chart.d.ts +1 -1
- package/dist/components/sankey-chart.js +4 -4
- package/dist/components/sankey-chart.js.map +1 -1
- package/dist/components/schedule-list.cjs +3 -3
- package/dist/components/schedule-list.cjs.map +1 -1
- package/dist/components/schedule-list.js +3 -3
- package/dist/components/schedule-list.js.map +1 -1
- package/dist/components/segmented-control.cjs +1 -1
- package/dist/components/segmented-control.cjs.map +1 -1
- package/dist/components/segmented-control.d.cts +1 -1
- package/dist/components/segmented-control.d.ts +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/segmented-control.js.map +1 -1
- package/dist/components/select.cjs +1 -1
- package/dist/components/select.d.cts +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/series-legend.cjs +1 -1
- package/dist/components/series-legend.cjs.map +1 -1
- package/dist/components/series-legend.js +1 -1
- package/dist/components/series-legend.js.map +1 -1
- package/dist/components/slider.cjs +6 -19
- package/dist/components/slider.cjs.map +1 -1
- package/dist/components/slider.d.cts +0 -13
- package/dist/components/slider.d.ts +0 -13
- package/dist/components/slider.js +6 -19
- package/dist/components/slider.js.map +1 -1
- package/dist/components/stepper.cjs +2 -2
- package/dist/components/stepper.cjs.map +1 -1
- package/dist/components/stepper.js +2 -2
- package/dist/components/stepper.js.map +1 -1
- package/dist/components/switch.cjs +8 -3
- package/dist/components/switch.cjs.map +1 -1
- package/dist/components/switch.d.cts +9 -4
- package/dist/components/switch.d.ts +9 -4
- package/dist/components/switch.js +8 -3
- package/dist/components/switch.js.map +1 -1
- package/dist/components/tabs.cjs +4 -4
- package/dist/components/tabs.cjs.map +1 -1
- package/dist/components/tabs.js +4 -4
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/theme.cjs +1 -1
- package/dist/components/theme.d.cts +1 -1
- package/dist/components/theme.d.ts +1 -1
- package/dist/components/theme.js +1 -1
- package/dist/components/time-picker.cjs +1 -1
- package/dist/components/time-picker.cjs.map +1 -1
- package/dist/components/time-picker.js +1 -1
- package/dist/components/time-picker.js.map +1 -1
- package/dist/hooks/use-reduced-motion.cjs +22 -0
- package/dist/hooks/use-reduced-motion.cjs.map +1 -0
- package/dist/hooks/use-reduced-motion.d.cts +7 -0
- package/dist/hooks/use-reduced-motion.d.ts +7 -0
- package/dist/hooks/use-reduced-motion.js +20 -0
- package/dist/hooks/use-reduced-motion.js.map +1 -0
- package/dist/{input-box-variants-Dr1K7uYE.js → input-box-variants-DcajYXvo.js} +1 -1
- package/dist/{input-box-variants-Dr1K7uYE.js.map → input-box-variants-DcajYXvo.js.map} +1 -1
- package/dist/{input-box-variants-BMDdwvqg.cjs → input-box-variants-ZCtgs-1S.cjs} +1 -1
- package/dist/{input-box-variants-BMDdwvqg.cjs.map → input-box-variants-ZCtgs-1S.cjs.map} +1 -1
- package/dist/{number-flow-DSbucQ_K.d.ts → number-flow-D9zsxH0-.d.ts} +1 -1
- package/dist/styles/index.css +4 -2
- package/dist/styles/spark-default.css +78 -0
- package/dist/{theme-C19BbKfO.js → theme-8PDO9E0S.js} +1 -1
- package/dist/{theme-C19BbKfO.js.map → theme-8PDO9E0S.js.map} +1 -1
- package/dist/{theme-CfFWrMEF.cjs → theme-ClRJhexF.cjs} +1 -1
- package/dist/{theme-CfFWrMEF.cjs.map → theme-ClRJhexF.cjs.map} +1 -1
- package/dist/{theme-types-DoROIXpi.d.ts → theme-types-BDdoRxXa.d.ts} +2 -3
- package/dist/{theme-types-r20EJfEC.d.cts → theme-types-BwSJKdsB.d.cts} +2 -3
- package/dist/{tones-DyCkL71h.d.ts → tones-PXJGlSpe.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slider.js","names":["React","BaseSlider"],"sources":["../../src/components/slider.tsx"],"sourcesContent":["import { Slider as BaseSlider } from '@base-ui/react/slider'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\n\ntype SliderValue = number | number[]\n\ntype SliderVariant = 'default' | 'ticks'\n\n// Above this many steps the tick dots become a picket fence, so `variant=\"ticks\"`\n// silently stops drawing them (the continuous rail stays).\nconst MAX_TICKS = 24\n\n// Clamp to [min, max] then snap to the step grid (origin = min), trimming float\n// noise to the step's own decimal precision. `max` is always valid even if it\n// isn't a step multiple (matches Base UI).\nfunction snapToStep(\n value: number,\n min: number,\n max: number,\n step: number,\n): number {\n const clamped = Math.min(max, Math.max(min, value))\n\n if (!(step > 0)) {\n return clamped\n }\n\n const snapped = min + Math.round((clamped - min) / step) * step\n const bounded = Math.min(max, Math.max(min, snapped))\n const decimals = (String(step).split('.')[1] ?? '').length\n\n return decimals > 0 ? Number(bounded.toFixed(decimals)) : bounded\n}\n\n/**\n * Base UI's slider context is internal-only (not re-exported from\n * `@base-ui/react/slider`), so the editable `<SliderInput>` can't read the\n * current value from it. Instead `<Slider>` owns the value (controllable) and\n * shares it through this Spark context — keeping direct-input robust against\n * Base UI internal changes and letting inputs live anywhere in the subtree.\n */\ntype SliderContextValue = {\n values: readonly number[]\n min: number\n max: number\n step: number\n disabled: boolean\n /**\n * Write one thumb's value (from a `SliderInput`). Snaps to the step grid and\n * clamps; range values are kept sorted. Fires `onValueChange` only when the\n * value actually changes. When `commit` is set it also fires `onValueCommitted`\n * — but only if the committed value differs from `commitBaseline` (the value\n * when editing began), so a plain focus→blur never emits a spurious commit.\n */\n setThumbValue: (\n index: number,\n next: number,\n options?: { commit?: boolean; commitBaseline?: number },\n ) => void\n}\n\nconst SliderContext = React.createContext<SliderContextValue | null>(null)\n\nfunction useSliderContext(part: string): SliderContextValue {\n const ctx = React.useContext(SliderContext)\n\n if (!ctx) {\n throw new Error(`<${part}> must be rendered inside <Slider>`)\n }\n\n return ctx\n}\n\ntype SliderProps<Value extends SliderValue = SliderValue> = Omit<\n React.ComponentProps<typeof BaseSlider.Root>,\n 'value' | 'defaultValue' | 'onValueChange' | 'onValueCommitted' | 'render'\n> & {\n value?: Value\n defaultValue?: Value\n // Generic over the value shape: a single `number` keeps single-thumb handlers\n // number-typed, an array keeps range handlers array-typed — so callers don't\n // widen/narrow at every call site.\n onValueChange?: (\n value: Value,\n eventDetails: BaseSlider.Root.ChangeEventDetails,\n ) => void\n onValueCommitted?: (\n value: Value,\n eventDetails: BaseSlider.Root.CommitEventDetails,\n ) => void\n /**\n * `ticks` marks each step with a small dot on the rail — light on the accent\n * fill, neutral on the unfilled rail. Needs a sensible `step`; the dots are\n * hidden when the step count is < 2 or exceeds {@link MAX_TICKS}, and (in the\n * default render) for vertical sliders. Default `default`.\n */\n variant?: SliderVariant\n}\n\n/**\n * Root. Owns the value (controlled via `value` or uncontrolled via\n * `defaultValue`) and provides the Spark context. With no children it renders\n * the default single/range track (a childless-default, matching the library's\n * `*Close` idiom); pass children to compose `SliderInput` / `SliderValue` /\n * `SliderControl` yourself.\n */\nfunction Slider<Value extends SliderValue = SliderValue>({\n className,\n children,\n value,\n defaultValue,\n min = 0,\n max = 100,\n step = 1,\n disabled = false,\n orientation = 'horizontal',\n variant = 'default',\n onValueChange,\n onValueCommitted,\n // Radix positioned thumbs edge-aligned (inset within the track at min/max);\n // Base UI defaults to center-aligned (half the thumb overflows). `edge` keeps\n // the classic look and the thumb inside the rail.\n thumbAlignment = 'edge',\n // The accessible name belongs on the thumb's nested range input, not just the\n // Root — so keep it on the Root (names the group) AND forward it to the\n // childless-default thumb(s). When you compose your own thumbs, label them.\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n ...props\n}: SliderProps<Value>) {\n const isControlled = value !== undefined\n const [uncontrolled, setUncontrolled] = React.useState<SliderValue>(\n defaultValue ?? min,\n )\n const current = (isControlled ? value : uncontrolled) as SliderValue\n const values = React.useMemo(\n () => (Array.isArray(current) ? current : [current]),\n [current],\n )\n\n // Base UI hands back `number | readonly number[]`; we forward it as the\n // caller's `Value` (the runtime shape matches what they passed in).\n const emitChange = (\n next: number | readonly number[],\n eventDetails: BaseSlider.Root.ChangeEventDetails,\n ) => {\n if (!isControlled) {\n setUncontrolled(next as SliderValue)\n }\n\n onValueChange?.(next as Value, eventDetails)\n }\n\n // The direct-input write path. Snaps + clamps; for a range it swaps one thumb\n // and re-sorts (Base UI renders from a sorted copy, so an unsorted write would\n // desync the inputs from the thumbs). Fires onValueChange only on a real\n // change; fires onValueCommitted only when the committed value moved off the\n // pre-edit baseline.\n const setThumbValue = (\n index: number,\n next: number,\n options?: { commit?: boolean; commitBaseline?: number },\n ) => {\n const snapped = snapToStep(next, min, max, step)\n const wasArray = Array.isArray(current)\n let nextValue: SliderValue\n\n if (wasArray) {\n const copy = [...(current as number[])]\n copy[index] = snapped\n copy.sort((a, b) => a - b)\n nextValue = copy\n } else {\n nextValue = snapped\n }\n\n const changed = wasArray\n ? (nextValue as number[]).some((v, i) => v !== (current as number[])[i])\n : nextValue !== current\n\n // input-driven changes have no pointer/keyboard event; Base UI's own detail\n // object isn't constructible here, so we hand the consumer a minimal one.\n const details = {\n reason: 'input-change',\n } as unknown as BaseSlider.Root.ChangeEventDetails\n\n if (changed) {\n emitChange(nextValue, details)\n }\n\n if (options?.commit) {\n const committedChanged =\n options.commitBaseline === undefined\n ? changed\n : snapped !== options.commitBaseline\n\n if (committedChanged) {\n onValueCommitted?.(\n nextValue as Value,\n details as unknown as BaseSlider.Root.CommitEventDetails,\n )\n }\n }\n }\n\n // Rebuilt each render (cheap — <Slider> only re-renders on a value change) so\n // setThumbValue always closes over the current value + latest callbacks.\n const context: SliderContextValue = {\n values,\n min,\n max,\n step,\n disabled,\n setThumbValue,\n }\n\n // ticks are horizontal-only in the default render; SliderTicks self-hides when\n // the step count isn't countable, so no density check is needed here.\n const showTicks = variant === 'ticks' && orientation === 'horizontal'\n\n return (\n <SliderContext.Provider value={context}>\n <BaseSlider.Root\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n data-slot=\"slider\"\n value={current}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n orientation={orientation}\n thumbAlignment={thumbAlignment}\n onValueChange={emitChange}\n onValueCommitted={(next, eventDetails) =>\n onValueCommitted?.(next as Value, eventDetails)\n }\n className={cn(\n 'relative flex w-full flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-row',\n className,\n )}\n {...props}\n >\n {children ?? (\n <SliderControl>\n <SliderTrack>\n <SliderIndicator />\n {showTicks ? <SliderTicks /> : null}\n {values.map((_, index) => (\n <SliderThumb\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n index={values.length > 1 ? index : undefined}\n key={index}\n />\n ))}\n </SliderTrack>\n </SliderControl>\n )}\n </BaseSlider.Root>\n </SliderContext.Provider>\n )\n}\n\nSlider.displayName = 'Slider'\n\n/**\n * The interactive, draggable surface. A pointer-press anywhere on it moves the\n * nearest thumb (Base UI's track-press), so its full band is the target — kept\n * at ≥24px tall (WCAG 2.5.8) with the thin rail centered.\n */\nfunction SliderControl({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Control>) {\n return (\n <BaseSlider.Control\n data-slot=\"slider-control\"\n className={cn(\n 'relative flex min-h-6 w-full touch-none items-center select-none',\n 'data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-6 data-[orientation=vertical]:justify-center',\n 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderControl.displayName = 'SliderControl'\n\n/** The rail — the full extent, neutral at rest. Holds the Indicator + Thumb(s). */\nfunction SliderTrack({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Track>) {\n return (\n <BaseSlider.Track\n data-slot=\"slider-track\"\n className={cn(\n // 12px rail so the corner radius is legible; follows the [data-radius] knob\n // (square at none → pill at large/full). no overflow-hidden: the thumb is a\n // child and would be clipped.\n 'relative h-3 w-full rounded-[var(--spark-rounded-sm)] bg-gray-a4',\n 'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-3',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderTrack.displayName = 'SliderTrack'\n\n/** The filled portion (min → thumb). Accent, deepens in high-contrast. */\nfunction SliderIndicator({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Indicator>) {\n return (\n <BaseSlider.Indicator\n data-slot=\"slider-indicator\"\n className={cn(\n // matches the rail's radius so the fill's corners track the knob too\n 'rounded-[var(--spark-rounded-sm)] bg-[var(--spark-slider-range,var(--spark-9))]',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderIndicator.displayName = 'SliderIndicator'\n\n/**\n * Step tick dots overlaid on the rail (shown when `variant=\"ticks\"`). A dot at\n * each interior step — light (`--spark-slider-tick-on`, on the accent fill) for\n * steps up to the value, neutral gray beyond it. The endpoints are omitted (they\n * sit under the rounded rail ends / the thumb). Self-guards: draws nothing unless\n * the step count is in `[2, MAX_TICKS]`, so it's safe to compose directly.\n * Decorative + pointer-transparent so it never blocks a track-press. (Horizontal\n * orientation.)\n */\nfunction SliderTicks({ className, ...props }: React.ComponentProps<'div'>) {\n const { min, max, step, values } = useSliderContext('SliderTicks')\n\n if (!(step > 0) || max <= min) {\n return null\n }\n\n // count the reachable step intervals (floor, so a non-dividing step still gets\n // a dot on its last reachable step); drop the final one when it lands on max.\n const intervals = Math.floor((max - min) / step)\n\n if (intervals < 2 || intervals > MAX_TICKS) {\n return null\n }\n\n const lastIsEndpoint = min + intervals * step === max\n const dotCount = lastIsEndpoint ? intervals - 1 : intervals\n\n // Active band: [min, value] for a single thumb, [low, high] for a range.\n const bandLow = values.length > 1 ? Math.min(...values) : min\n const bandHigh = values.length > 1 ? Math.max(...values) : (values[0] ?? min)\n\n return (\n <div\n aria-hidden=\"true\"\n className={cn('pointer-events-none absolute inset-0', className)}\n data-slot=\"slider-ticks\"\n {...props}\n >\n {Array.from({ length: dotCount }, (_, k) => {\n const index = k + 1 // interior steps only (skip the two endpoints)\n const position = min + index * step\n const pct = ((position - min) / (max - min)) * 100\n const filled = position >= bandLow && position <= bandHigh\n\n return (\n <span\n className={cn(\n 'absolute top-1/2 size-1 -translate-x-1/2 -translate-y-1/2 rounded-full',\n // light dot on the accent fill (flips to step-1 in HC via the token);\n // neutral gray dot on the unfilled rail.\n filled\n ? 'bg-[var(--spark-slider-tick-on,var(--spark-on-solid,#fff))]'\n : 'bg-gray-a8',\n )}\n key={index}\n style={{ left: `${pct}%` }}\n />\n )\n })}\n </div>\n )\n}\n\nSliderTicks.displayName = 'SliderTicks'\n\n/**\n * The grab handle — a 16px accent knob. Corners follow the [data-radius] knob\n * (`--spark-rounded-lg`, matching the switch thumb): a circle at medium+, a\n * rounded square at small, a square at none. A background-colored separator ring\n * lifts it off the accent-filled range and a soft drop shadow adds depth; both\n * ride `--tw-shadow` so `focus-visible:ring` stays free. The invisible 24px hit\n * area meets the WCAG 2.5.8 target minimum.\n */\nfunction SliderThumb({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Thumb>) {\n return (\n <BaseSlider.Thumb\n data-slot=\"slider-thumb\"\n className={cn(\n 'block size-4 shrink-0 rounded-[var(--spark-rounded-lg)] bg-[var(--spark-slider-range,var(--spark-9))]',\n 'shadow-[0_0_0_2px_var(--spark-background,#fff),0_1px_2px_0_rgb(0_0_0/0.2)]',\n \"before:absolute before:top-1/2 before:left-1/2 before:size-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']\",\n 'focus-visible:ring focus-visible:ring-offset-2 focus-visible:outline-hidden',\n 'data-[dragging]:ring data-[dragging]:ring-offset-2',\n 'disabled:pointer-events-none',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderThumb.displayName = 'SliderThumb'\n\n/**\n * Read-only value readout (`<output>`). Reads Base UI's own slider context, so\n * it stays formatted per the Root's `format`/`locale`. Pass a render function\n * for custom output, e.g. `{(f) => `${f[0]} €`}`.\n */\nfunction SliderValue({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Value>) {\n return (\n <BaseSlider.Value\n data-slot=\"slider-value\"\n className={cn('text-sm text-gray tabular-nums', className)}\n {...props}\n />\n )\n}\n\nSliderValue.displayName = 'SliderValue'\n\ntype SliderInputProps = Omit<\n React.ComponentProps<'input'>,\n 'value' | 'defaultValue' | 'type' | 'onChange' | 'min' | 'max' | 'step'\n> & {\n /** Which thumb this input drives on a range slider. Defaults to 0. */\n index?: number\n /** Unit rendered after the field (e.g. \"€/month\"). Consumer-supplied — no default. */\n unit?: React.ReactNode\n}\n\n/**\n * Accessible direct-input — a native number field synced to a thumb's value, so\n * a user can type an exact value instead of dragging (or in addition to it).\n * Needs an accessible name: pass `aria-label` (no built-in default — portals are\n * multi-language). While typing it live-updates the thumb only for values already\n * in range (no mid-keystroke jump to the min/max); it snaps + clamps on\n * blur/Enter. `onFocus`/`onBlur`/`onKeyDown` you pass are composed with the\n * internal handlers, not replaced.\n */\nfunction SliderInput({\n className,\n index = 0,\n unit,\n disabled,\n onFocus,\n onBlur,\n onKeyDown,\n ...props\n}: SliderInputProps) {\n const ctx = useSliderContext('SliderInput')\n const currentValue = ctx.values[index] ?? ctx.min\n const [draft, setDraft] = React.useState<string>(String(currentValue))\n const [editing, setEditing] = React.useState(false)\n // the thumb value when editing began — commit fires only if it moved off this\n const baselineRef = React.useRef(currentValue)\n\n // Keep the field in sync when the value changes elsewhere (drag/keyboard),\n // but never fight the user's in-progress typing.\n React.useEffect(() => {\n if (!editing) {\n setDraft(String(currentValue))\n }\n }, [currentValue, editing])\n\n const parseInput = (raw: string): number | null => {\n if (raw.trim() === '') {\n return null\n }\n\n const parsed = Number(raw)\n\n return Number.isNaN(parsed) ? null : parsed\n }\n\n const commit = () => {\n const parsed = parseInput(draft)\n\n if (parsed === null) {\n setDraft(String(currentValue)) // reset an empty/invalid entry\n return\n }\n\n ctx.setThumbValue(index, parsed, {\n commit: true,\n commitBaseline: baselineRef.current,\n })\n }\n\n return (\n <span\n data-slot=\"slider-input\"\n className={cn('inline-flex items-baseline gap-1', className)}\n >\n <input\n type=\"number\"\n inputMode=\"decimal\"\n min={ctx.min}\n max={ctx.max}\n step={ctx.step}\n disabled={disabled ?? ctx.disabled}\n value={editing ? draft : String(currentValue)}\n onChange={(event) => {\n const raw = event.target.value\n setDraft(raw)\n const parsed = parseInput(raw)\n\n // live-update the thumb only when the typed value is already in range,\n // so a partial entry (e.g. \"7\" en route to \"75\") doesn't snap the thumb\n // to the min mid-keystroke. Out-of-range values clamp on blur/Enter.\n if (parsed !== null && parsed >= ctx.min && parsed <= ctx.max) {\n ctx.setThumbValue(index, parsed)\n }\n }}\n onFocus={(event) => {\n baselineRef.current = ctx.values[index] ?? ctx.min\n setEditing(true)\n onFocus?.(event)\n }}\n onBlur={(event) => {\n setEditing(false)\n commit()\n onBlur?.(event)\n }}\n onKeyDown={(event) => {\n // Enter blurs, and the single onBlur commit runs — no double-commit.\n if (event.key === 'Enter') {\n event.currentTarget.blur()\n }\n\n onKeyDown?.(event)\n }}\n className={cn(\n 'w-16 bg-transparent tabular-nums outline-hidden',\n 'rounded-[var(--spark-rounded-sm)] focus-visible:ring focus-visible:ring-offset-2',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n // hide the native spinners — the slider is the primary affordance\n '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n )}\n {...props}\n />\n {unit ? (\n <span className=\"text-sm text-gray\" data-slot=\"slider-unit\">\n {unit}\n </span>\n ) : null}\n </span>\n )\n}\n\nSliderInput.displayName = 'SliderInput'\n\nexport {\n Slider,\n SliderControl,\n SliderIndicator,\n SliderInput,\n SliderThumb,\n SliderTicks,\n SliderTrack,\n SliderValue,\n}\nexport type { SliderInputProps, SliderProps, SliderVariant }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"slider.js","names":["React","BaseSlider"],"sources":["../../src/components/slider.tsx"],"sourcesContent":["import { Slider as BaseSlider } from '@base-ui/react/slider'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\n\ntype SliderValue = number | number[]\n\ntype SliderVariant = 'default' | 'ticks'\n\n// Above this many steps the tick dots become a picket fence, so `variant=\"ticks\"`\n// silently stops drawing them (the continuous rail stays).\nconst MAX_TICKS = 24\n\n// Clamp to [min, max] then snap to the step grid (origin = min), trimming float\n// noise to the step's own decimal precision. `max` is always valid even if it\n// isn't a step multiple (matches Base UI).\nfunction snapToStep(\n value: number,\n min: number,\n max: number,\n step: number,\n): number {\n const clamped = Math.min(max, Math.max(min, value))\n\n if (!(step > 0)) {\n return clamped\n }\n\n const snapped = min + Math.round((clamped - min) / step) * step\n const bounded = Math.min(max, Math.max(min, snapped))\n const decimals = (String(step).split('.')[1] ?? '').length\n\n return decimals > 0 ? Number(bounded.toFixed(decimals)) : bounded\n}\n\n/**\n * Base UI's slider context is internal-only (not re-exported from\n * `@base-ui/react/slider`), so the editable `<SliderInput>` can't read the\n * current value from it. Instead `<Slider>` owns the value (controllable) and\n * shares it through this Spark context — keeping direct-input robust against\n * Base UI internal changes and letting inputs live anywhere in the subtree.\n */\ntype SliderContextValue = {\n values: readonly number[]\n min: number\n max: number\n step: number\n disabled: boolean\n /**\n * Write one thumb's value (from a `SliderInput`). Snaps to the step grid and\n * clamps; range values are kept sorted. Fires `onValueChange` only when the\n * value actually changes. When `commit` is set it also fires `onValueCommitted`\n * — but only if the committed value differs from `commitBaseline` (the value\n * when editing began), so a plain focus→blur never emits a spurious commit.\n */\n setThumbValue: (\n index: number,\n next: number,\n options?: { commit?: boolean; commitBaseline?: number },\n ) => void\n}\n\nconst SliderContext = React.createContext<SliderContextValue | null>(null)\n\nfunction useSliderContext(part: string): SliderContextValue {\n const ctx = React.useContext(SliderContext)\n\n if (!ctx) {\n throw new Error(`<${part}> must be rendered inside <Slider>`)\n }\n\n return ctx\n}\n\ntype SliderProps<Value extends SliderValue = SliderValue> = Omit<\n React.ComponentProps<typeof BaseSlider.Root>,\n 'value' | 'defaultValue' | 'onValueChange' | 'onValueCommitted' | 'render'\n> & {\n value?: Value\n defaultValue?: Value\n // Generic over the value shape: a single `number` keeps single-thumb handlers\n // number-typed, an array keeps range handlers array-typed — so callers don't\n // widen/narrow at every call site.\n onValueChange?: (\n value: Value,\n eventDetails: BaseSlider.Root.ChangeEventDetails,\n ) => void\n onValueCommitted?: (\n value: Value,\n eventDetails: BaseSlider.Root.CommitEventDetails,\n ) => void\n /**\n * `ticks` marks each step with a small dot on the rail — light on the accent\n * fill, neutral on the unfilled rail. Needs a sensible `step`; the dots are\n * hidden when the step count is < 2 or exceeds {@link MAX_TICKS}, and (in the\n * default render) for vertical sliders. Default `default`.\n */\n variant?: SliderVariant\n}\n\n/**\n * Root. Owns the value (controlled via `value` or uncontrolled via\n * `defaultValue`) and provides the Spark context. With no children it renders\n * the default single/range track (a childless-default, matching the library's\n * `*Close` idiom); pass children to compose `SliderInput` / `SliderValue` /\n * `SliderControl` yourself.\n */\nfunction Slider<Value extends SliderValue = SliderValue>({\n className,\n children,\n value,\n defaultValue,\n min = 0,\n max = 100,\n step = 1,\n disabled = false,\n orientation = 'horizontal',\n variant = 'default',\n onValueChange,\n onValueCommitted,\n // Radix positioned thumbs edge-aligned (inset within the track at min/max);\n // Base UI defaults to center-aligned (half the thumb overflows). `edge` keeps\n // the classic look and the thumb inside the rail.\n thumbAlignment = 'edge',\n // The accessible name belongs on the thumb's nested range input, not just the\n // Root — so keep it on the Root (names the group) AND forward it to the\n // childless-default thumb(s). When you compose your own thumbs, label them.\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledby,\n ...props\n}: SliderProps<Value>) {\n const isControlled = value !== undefined\n const [uncontrolled, setUncontrolled] = React.useState<SliderValue>(\n defaultValue ?? min,\n )\n const current = (isControlled ? value : uncontrolled) as SliderValue\n const values = React.useMemo(\n () => (Array.isArray(current) ? current : [current]),\n [current],\n )\n\n // Base UI hands back `number | readonly number[]`; we forward it as the\n // caller's `Value` (the runtime shape matches what they passed in).\n const emitChange = (\n next: number | readonly number[],\n eventDetails: BaseSlider.Root.ChangeEventDetails,\n ) => {\n if (!isControlled) {\n setUncontrolled(next as SliderValue)\n }\n\n onValueChange?.(next as Value, eventDetails)\n }\n\n // The direct-input write path. Snaps + clamps; for a range it swaps one thumb\n // and re-sorts (Base UI renders from a sorted copy, so an unsorted write would\n // desync the inputs from the thumbs). Fires onValueChange only on a real\n // change; fires onValueCommitted only when the committed value moved off the\n // pre-edit baseline.\n const setThumbValue = (\n index: number,\n next: number,\n options?: { commit?: boolean; commitBaseline?: number },\n ) => {\n const snapped = snapToStep(next, min, max, step)\n const wasArray = Array.isArray(current)\n let nextValue: SliderValue\n\n if (wasArray) {\n const copy = [...(current as number[])]\n copy[index] = snapped\n copy.sort((a, b) => a - b)\n nextValue = copy\n } else {\n nextValue = snapped\n }\n\n const changed = wasArray\n ? (nextValue as number[]).some((v, i) => v !== (current as number[])[i])\n : nextValue !== current\n\n // input-driven changes have no pointer/keyboard event; Base UI's own detail\n // object isn't constructible here, so we hand the consumer a minimal one.\n const details = {\n reason: 'input-change',\n } as unknown as BaseSlider.Root.ChangeEventDetails\n\n if (changed) {\n emitChange(nextValue, details)\n }\n\n if (options?.commit) {\n const committedChanged =\n options.commitBaseline === undefined\n ? changed\n : snapped !== options.commitBaseline\n\n if (committedChanged) {\n onValueCommitted?.(\n nextValue as Value,\n details as unknown as BaseSlider.Root.CommitEventDetails,\n )\n }\n }\n }\n\n // Rebuilt each render (cheap — <Slider> only re-renders on a value change) so\n // setThumbValue always closes over the current value + latest callbacks.\n const context: SliderContextValue = {\n values,\n min,\n max,\n step,\n disabled,\n setThumbValue,\n }\n\n // ticks are horizontal-only in the default render; SliderTicks self-hides when\n // the step count isn't countable, so no density check is needed here.\n const showTicks = variant === 'ticks' && orientation === 'horizontal'\n\n return (\n <SliderContext.Provider value={context}>\n <BaseSlider.Root\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n data-slot=\"slider\"\n value={current}\n min={min}\n max={max}\n step={step}\n disabled={disabled}\n orientation={orientation}\n thumbAlignment={thumbAlignment}\n onValueChange={emitChange}\n onValueCommitted={(next, eventDetails) =>\n onValueCommitted?.(next as Value, eventDetails)\n }\n className={cn(\n 'relative flex w-full flex-col gap-2 data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-row',\n className,\n )}\n {...props}\n >\n {children ?? (\n <SliderControl>\n <SliderTrack>\n <SliderIndicator />\n {showTicks ? <SliderTicks /> : null}\n {values.map((_, index) => (\n <SliderThumb\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledby}\n index={values.length > 1 ? index : undefined}\n key={index}\n />\n ))}\n </SliderTrack>\n </SliderControl>\n )}\n </BaseSlider.Root>\n </SliderContext.Provider>\n )\n}\n\nSlider.displayName = 'Slider'\n\n/**\n * The interactive, draggable surface. A pointer-press anywhere on it moves the\n * nearest thumb (Base UI's track-press), so its full band is the target — kept\n * at ≥24px tall (WCAG 2.5.8) with the thin rail centered.\n */\nfunction SliderControl({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Control>) {\n return (\n <BaseSlider.Control\n data-slot=\"slider-control\"\n className={cn(\n 'relative flex min-h-6 w-full touch-none items-center select-none',\n 'data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-6 data-[orientation=vertical]:justify-center',\n 'data-disabled:pointer-events-none data-disabled:opacity-50',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderControl.displayName = 'SliderControl'\n\n/** The rail — the full extent, neutral at rest. Holds the Indicator + Thumb(s). */\nfunction SliderTrack({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Track>) {\n return (\n <BaseSlider.Track\n data-slot=\"slider-track\"\n className={cn(\n // 12px rail so the corner radius is legible; follows the [data-radius] knob\n // (square at none → pill at large/full). no overflow-hidden: the thumb is a\n // child and would be clipped.\n 'relative h-3 w-full rounded-(--spark-rounded-sm) bg-gray-a4',\n 'data-[orientation=vertical]:h-full data-[orientation=vertical]:w-3',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderTrack.displayName = 'SliderTrack'\n\n/** The filled portion (min → thumb). Accent, deepens in high-contrast. */\nfunction SliderIndicator({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Indicator>) {\n return (\n <BaseSlider.Indicator\n data-slot=\"slider-indicator\"\n className={cn(\n // matches the rail's radius so the fill's corners track the knob too\n 'rounded-(--spark-rounded-sm) bg-(--spark-slider-range,var(--spark-9))',\n 'transition-[width,height,inset-inline-start] duration-150 data-dragging:transition-none motion-reduce:transition-none',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderIndicator.displayName = 'SliderIndicator'\n\n/**\n * Step tick dots overlaid on the rail (shown when `variant=\"ticks\"`). A dot at\n * each interior step — light (`--spark-slider-tick-on`, on the accent fill) for\n * steps up to the value, neutral gray beyond it. The endpoints are omitted (they\n * sit under the rounded rail ends / the thumb). Self-guards: draws nothing unless\n * the step count is in `[2, MAX_TICKS]`, so it's safe to compose directly.\n * Decorative + pointer-transparent so it never blocks a track-press. (Horizontal\n * orientation.)\n */\nfunction SliderTicks({ className, ...props }: React.ComponentProps<'div'>) {\n const { min, max, step, values } = useSliderContext('SliderTicks')\n\n if (!(step > 0) || max <= min) {\n return null\n }\n\n // count the reachable step intervals (floor, so a non-dividing step still gets\n // a dot on its last reachable step); drop the final one when it lands on max.\n const intervals = Math.floor((max - min) / step)\n\n if (intervals < 2 || intervals > MAX_TICKS) {\n return null\n }\n\n const lastIsEndpoint = min + intervals * step === max\n const dotCount = lastIsEndpoint ? intervals - 1 : intervals\n\n // Active band: [min, value] for a single thumb, [low, high] for a range.\n const bandLow = values.length > 1 ? Math.min(...values) : min\n const bandHigh = values.length > 1 ? Math.max(...values) : (values[0] ?? min)\n\n return (\n <div\n aria-hidden=\"true\"\n className={cn('pointer-events-none absolute inset-0', className)}\n data-slot=\"slider-ticks\"\n {...props}\n >\n {Array.from({ length: dotCount }, (_, k) => {\n const index = k + 1 // interior steps only (skip the two endpoints)\n const position = min + index * step\n const pct = ((position - min) / (max - min)) * 100\n const filled = position >= bandLow && position <= bandHigh\n\n return (\n <span\n className={cn(\n 'absolute top-1/2 size-1 -translate-x-1/2 -translate-y-1/2 rounded-full',\n // light dot on the accent fill (flips to step-1 in HC via the token);\n // neutral gray dot on the unfilled rail.\n filled\n ? 'bg-(--spark-slider-tick-on,var(--spark-on-solid,#fff))'\n : 'bg-gray-a8',\n )}\n key={index}\n style={{ left: `${pct}%` }}\n />\n )\n })}\n </div>\n )\n}\n\nSliderTicks.displayName = 'SliderTicks'\n\nfunction SliderThumb({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Thumb>) {\n return (\n <BaseSlider.Thumb\n data-slot=\"slider-thumb\"\n className={cn(\n 'block size-4 shrink-0 rounded-(--spark-rounded-lg) bg-(--spark-slider-range,var(--spark-9))',\n 'shadow-[0_0_0_2px_var(--spark-background,#fff),0_1px_2px_0_rgb(0_0_0/0.2)]',\n \"before:absolute before:top-1/2 before:left-1/2 before:size-6 before:-translate-x-1/2 before:-translate-y-1/2 before:content-['']\",\n 'outline-hidden has-[input:focus-visible]:ring-2 has-[input:focus-visible]:ring-offset-2',\n 'data-dragging:ring-2 data-dragging:ring-offset-2',\n 'transition-[inset-inline-start,top,box-shadow] duration-150 data-dragging:transition-none motion-reduce:transition-none',\n 'disabled:pointer-events-none',\n className,\n )}\n {...props}\n />\n )\n}\n\nSliderThumb.displayName = 'SliderThumb'\n\nfunction SliderValue({\n className,\n ...props\n}: React.ComponentProps<typeof BaseSlider.Value>) {\n return (\n <BaseSlider.Value\n data-slot=\"slider-value\"\n className={cn('text-sm text-gray tabular-nums', className)}\n {...props}\n />\n )\n}\n\nSliderValue.displayName = 'SliderValue'\n\ntype SliderInputProps = Omit<\n React.ComponentProps<'input'>,\n 'value' | 'defaultValue' | 'type' | 'onChange' | 'min' | 'max' | 'step'\n> & {\n /** Which thumb this input drives on a range slider. Defaults to 0. */\n index?: number\n /** Unit rendered after the field (e.g. \"€/month\"). Consumer-supplied — no default. */\n unit?: React.ReactNode\n}\n\n/**\n * Accessible direct-input — a native number field synced to a thumb's value, so\n * a user can type an exact value instead of dragging (or in addition to it).\n * Needs an accessible name: pass `aria-label` (no built-in default — portals are\n * multi-language). While typing it live-updates the thumb only for values already\n * in range (no mid-keystroke jump to the min/max); it snaps + clamps on\n * blur/Enter. `onFocus`/`onBlur`/`onKeyDown` you pass are composed with the\n * internal handlers, not replaced.\n */\nfunction SliderInput({\n className,\n index = 0,\n unit,\n disabled,\n onFocus,\n onBlur,\n onKeyDown,\n ...props\n}: SliderInputProps) {\n const ctx = useSliderContext('SliderInput')\n const currentValue = ctx.values[index] ?? ctx.min\n const [draft, setDraft] = React.useState<string>(String(currentValue))\n const [editing, setEditing] = React.useState(false)\n // the thumb value when editing began — commit fires only if it moved off this\n const baselineRef = React.useRef(currentValue)\n\n // Keep the field in sync when the value changes elsewhere (drag/keyboard),\n // but never fight the user's in-progress typing.\n React.useEffect(() => {\n if (!editing) {\n setDraft(String(currentValue))\n }\n }, [currentValue, editing])\n\n const parseInput = (raw: string): number | null => {\n if (raw.trim() === '') {\n return null\n }\n\n const parsed = Number(raw)\n\n return Number.isNaN(parsed) ? null : parsed\n }\n\n const commit = () => {\n const parsed = parseInput(draft)\n\n if (parsed === null) {\n setDraft(String(currentValue)) // reset an empty/invalid entry\n return\n }\n\n ctx.setThumbValue(index, parsed, {\n commit: true,\n commitBaseline: baselineRef.current,\n })\n }\n\n return (\n <span\n data-slot=\"slider-input\"\n className={cn('inline-flex items-baseline gap-1', className)}\n >\n <input\n type=\"number\"\n inputMode=\"decimal\"\n min={ctx.min}\n max={ctx.max}\n step={ctx.step}\n disabled={disabled ?? ctx.disabled}\n value={editing ? draft : String(currentValue)}\n onChange={(event) => {\n const raw = event.target.value\n setDraft(raw)\n const parsed = parseInput(raw)\n\n // live-update the thumb only when the typed value is already in range,\n // so a partial entry (e.g. \"7\" en route to \"75\") doesn't snap the thumb\n // to the min mid-keystroke. Out-of-range values clamp on blur/Enter.\n if (parsed !== null && parsed >= ctx.min && parsed <= ctx.max) {\n ctx.setThumbValue(index, parsed)\n }\n }}\n onFocus={(event) => {\n baselineRef.current = ctx.values[index] ?? ctx.min\n setEditing(true)\n onFocus?.(event)\n }}\n onBlur={(event) => {\n setEditing(false)\n commit()\n onBlur?.(event)\n }}\n onKeyDown={(event) => {\n // Enter blurs, and the single onBlur commit runs — no double-commit.\n if (event.key === 'Enter') {\n event.currentTarget.blur()\n }\n\n onKeyDown?.(event)\n }}\n className={cn(\n 'w-16 bg-transparent tabular-nums outline-hidden',\n 'rounded-(--spark-rounded-sm) focus-visible:ring-2 focus-visible:ring-offset-2',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n // hide the native spinners — the slider is the primary affordance\n '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',\n )}\n {...props}\n />\n {unit ? (\n <span className=\"text-sm text-gray\" data-slot=\"slider-unit\">\n {unit}\n </span>\n ) : null}\n </span>\n )\n}\n\nSliderInput.displayName = 'SliderInput'\n\nexport {\n Slider,\n SliderControl,\n SliderIndicator,\n SliderInput,\n SliderThumb,\n SliderTicks,\n SliderTrack,\n SliderValue,\n}\nexport type { SliderInputProps, SliderProps, SliderVariant }\n"],"mappings":";;;;;;AAUA,MAAM,YAAY;AAKlB,SAAS,WACP,OACA,KACA,KACA,MACQ;CACR,MAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,MAAM,CAAC;AAEnD,KAAI,EAAE,OAAO,GACX,QAAO;CAGT,MAAM,UAAU,MAAM,KAAK,OAAO,UAAU,OAAO,KAAK,GAAG;CAC3D,MAAM,UAAU,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,QAAQ,CAAC;CACrD,MAAM,YAAY,OAAO,KAAK,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI;AAEpD,QAAO,WAAW,IAAI,OAAO,QAAQ,QAAQ,SAAS,CAAC,GAAG;;AA8B5D,MAAM,gBAAgBA,QAAM,cAAyC,KAAK;AAE1E,SAAS,iBAAiB,MAAkC;CAC1D,MAAM,MAAMA,QAAM,WAAW,cAAc;AAE3C,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,IAAI,KAAK,oCAAoC;AAG/D,QAAO;;;;;;;;;AAoCT,SAAS,OAAgD,EACvD,WACA,UACA,OACA,cACA,MAAM,GACN,MAAM,KACN,OAAO,GACP,WAAW,OACX,cAAc,cACd,UAAU,WACV,eACA,kBAIA,iBAAiB,QAIjB,cAAc,WACd,mBAAmB,gBACnB,GAAG,SACkB;CACrB,MAAM,eAAe,UAAU,KAAA;CAC/B,MAAM,CAAC,cAAc,mBAAmBA,QAAM,SAC5C,gBAAgB,IACjB;CACD,MAAM,UAAW,eAAe,QAAQ;CACxC,MAAM,SAASA,QAAM,cACZ,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ,EACnD,CAAC,QAAQ,CACV;CAID,MAAM,cACJ,MACA,iBACG;AACH,MAAI,CAAC,aACH,iBAAgB,KAAoB;AAGtC,kBAAgB,MAAe,aAAa;;CAQ9C,MAAM,iBACJ,OACA,MACA,YACG;EACH,MAAM,UAAU,WAAW,MAAM,KAAK,KAAK,KAAK;EAChD,MAAM,WAAW,MAAM,QAAQ,QAAQ;EACvC,IAAI;AAEJ,MAAI,UAAU;GACZ,MAAM,OAAO,CAAC,GAAI,QAAqB;AACvC,QAAK,SAAS;AACd,QAAK,MAAM,GAAG,MAAM,IAAI,EAAE;AAC1B,eAAY;QAEZ,aAAY;EAGd,MAAM,UAAU,WACX,UAAuB,MAAM,GAAG,MAAM,MAAO,QAAqB,GAAG,GACtE,cAAc;EAIlB,MAAM,UAAU,EACd,QAAQ,gBACT;AAED,MAAI,QACF,YAAW,WAAW,QAAQ;AAGhC,MAAI,SAAS;OAET,QAAQ,mBAAmB,KAAA,IACvB,UACA,YAAY,QAAQ,eAGxB,oBACE,WACA,QACD;;;CAOP,MAAM,UAA8B;EAClC;EACA;EACA;EACA;EACA;EACA;EACD;CAID,MAAM,YAAY,YAAY,WAAW,gBAAgB;AAEzD,QACE,oBAAC,cAAc,UAAf;EAAwB,OAAO;YAC7B,oBAACC,SAAW,MAAZ;GACE,cAAY;GACZ,mBAAiB;GACjB,aAAU;GACV,OAAO;GACF;GACA;GACC;GACI;GACG;GACG;GAChB,eAAe;GACf,mBAAmB,MAAM,iBACvB,mBAAmB,MAAe,aAAa;GAEjD,WAAW,GACT,kJACA,UACD;GACD,GAAI;aAEH,YACC,oBAAC,eAAD,EAAA,UACE,qBAAC,aAAD,EAAA,UAAA;IACE,oBAAC,iBAAD,EAAmB,CAAA;IAClB,YAAY,oBAAC,aAAD,EAAe,CAAA,GAAG;IAC9B,OAAO,KAAK,GAAG,UACd,oBAAC,aAAD;KACE,cAAY;KACZ,mBAAiB;KACjB,OAAO,OAAO,SAAS,IAAI,QAAQ,KAAA;KAEnC,EADK,MACL,CACF;IACU,EAAA,CAAA,EACA,CAAA;GAEF,CAAA;EACK,CAAA;;AAI7B,OAAO,cAAc;;;;;;AAOrB,SAAS,cAAc,EACrB,WACA,GAAG,SAC+C;AAClD,QACE,oBAACA,SAAW,SAAZ;EACE,aAAU;EACV,WAAW,GACT,oEACA,sJACA,8DACA,UACD;EACD,GAAI;EACJ,CAAA;;AAIN,cAAc,cAAc;;AAG5B,SAAS,YAAY,EACnB,WACA,GAAG,SAC6C;AAChD,QACE,oBAACA,SAAW,OAAZ;EACE,aAAU;EACV,WAAW,GAIT,+DACA,sEACA,UACD;EACD,GAAI;EACJ,CAAA;;AAIN,YAAY,cAAc;;AAG1B,SAAS,gBAAgB,EACvB,WACA,GAAG,SACiD;AACpD,QACE,oBAACA,SAAW,WAAZ;EACE,aAAU;EACV,WAAW,GAET,yEACA,yHACA,UACD;EACD,GAAI;EACJ,CAAA;;AAIN,gBAAgB,cAAc;;;;;;;;;;AAW9B,SAAS,YAAY,EAAE,WAAW,GAAG,SAAsC;CACzE,MAAM,EAAE,KAAK,KAAK,MAAM,WAAW,iBAAiB,cAAc;AAElE,KAAI,EAAE,OAAO,MAAM,OAAO,IACxB,QAAO;CAKT,MAAM,YAAY,KAAK,OAAO,MAAM,OAAO,KAAK;AAEhD,KAAI,YAAY,KAAK,YAAY,UAC/B,QAAO;CAIT,MAAM,WADiB,MAAM,YAAY,SAAS,MAChB,YAAY,IAAI;CAGlD,MAAM,UAAU,OAAO,SAAS,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG;CAC1D,MAAM,WAAW,OAAO,SAAS,IAAI,KAAK,IAAI,GAAG,OAAO,GAAI,OAAO,MAAM;AAEzE,QACE,oBAAC,OAAD;EACE,eAAY;EACZ,WAAW,GAAG,wCAAwC,UAAU;EAChE,aAAU;EACV,GAAI;YAEH,MAAM,KAAK,EAAE,QAAQ,UAAU,GAAG,GAAG,MAAM;GAC1C,MAAM,QAAQ,IAAI;GAClB,MAAM,WAAW,MAAM,QAAQ;GAC/B,MAAM,OAAQ,WAAW,QAAQ,MAAM,OAAQ;AAG/C,UACE,oBAAC,QAAD;IACE,WAAW,GACT,0EALS,YAAY,WAAW,YAAY,WASxC,2DACA,aACL;IAED,OAAO,EAAE,MAAM,GAAG,IAAI,IAAI;IAC1B,EAFK,MAEL;IAEJ;EACE,CAAA;;AAIV,YAAY,cAAc;AAE1B,SAAS,YAAY,EACnB,WACA,GAAG,SAC6C;AAChD,QACE,oBAACA,SAAW,OAAZ;EACE,aAAU;EACV,WAAW,GACT,+FACA,8EACA,oIACA,2FACA,oDACA,2HACA,gCACA,UACD;EACD,GAAI;EACJ,CAAA;;AAIN,YAAY,cAAc;AAE1B,SAAS,YAAY,EACnB,WACA,GAAG,SAC6C;AAChD,QACE,oBAACA,SAAW,OAAZ;EACE,aAAU;EACV,WAAW,GAAG,kCAAkC,UAAU;EAC1D,GAAI;EACJ,CAAA;;AAIN,YAAY,cAAc;;;;;;;;;;AAqB1B,SAAS,YAAY,EACnB,WACA,QAAQ,GACR,MACA,UACA,SACA,QACA,WACA,GAAG,SACgB;CACnB,MAAM,MAAM,iBAAiB,cAAc;CAC3C,MAAM,eAAe,IAAI,OAAO,UAAU,IAAI;CAC9C,MAAM,CAAC,OAAO,YAAYD,QAAM,SAAiB,OAAO,aAAa,CAAC;CACtE,MAAM,CAAC,SAAS,cAAcA,QAAM,SAAS,MAAM;CAEnD,MAAM,cAAcA,QAAM,OAAO,aAAa;AAI9C,SAAM,gBAAgB;AACpB,MAAI,CAAC,QACH,UAAS,OAAO,aAAa,CAAC;IAE/B,CAAC,cAAc,QAAQ,CAAC;CAE3B,MAAM,cAAc,QAA+B;AACjD,MAAI,IAAI,MAAM,KAAK,GACjB,QAAO;EAGT,MAAM,SAAS,OAAO,IAAI;AAE1B,SAAO,OAAO,MAAM,OAAO,GAAG,OAAO;;CAGvC,MAAM,eAAe;EACnB,MAAM,SAAS,WAAW,MAAM;AAEhC,MAAI,WAAW,MAAM;AACnB,YAAS,OAAO,aAAa,CAAC;AAC9B;;AAGF,MAAI,cAAc,OAAO,QAAQ;GAC/B,QAAQ;GACR,gBAAgB,YAAY;GAC7B,CAAC;;AAGJ,QACE,qBAAC,QAAD;EACE,aAAU;EACV,WAAW,GAAG,oCAAoC,UAAU;YAF9D,CAIE,oBAAC,SAAD;GACE,MAAK;GACL,WAAU;GACV,KAAK,IAAI;GACT,KAAK,IAAI;GACT,MAAM,IAAI;GACV,UAAU,YAAY,IAAI;GAC1B,OAAO,UAAU,QAAQ,OAAO,aAAa;GAC7C,WAAW,UAAU;IACnB,MAAM,MAAM,MAAM,OAAO;AACzB,aAAS,IAAI;IACb,MAAM,SAAS,WAAW,IAAI;AAK9B,QAAI,WAAW,QAAQ,UAAU,IAAI,OAAO,UAAU,IAAI,IACxD,KAAI,cAAc,OAAO,OAAO;;GAGpC,UAAU,UAAU;AAClB,gBAAY,UAAU,IAAI,OAAO,UAAU,IAAI;AAC/C,eAAW,KAAK;AAChB,cAAU,MAAM;;GAElB,SAAS,UAAU;AACjB,eAAW,MAAM;AACjB,YAAQ;AACR,aAAS,MAAM;;GAEjB,YAAY,UAAU;AAEpB,QAAI,MAAM,QAAQ,QAChB,OAAM,cAAc,MAAM;AAG5B,gBAAY,MAAM;;GAEpB,WAAW,GACT,mDACA,iFACA,mDAEA,uHACD;GACD,GAAI;GACJ,CAAA,EACD,OACC,oBAAC,QAAD;GAAM,WAAU;GAAoB,aAAU;aAC3C;GACI,CAAA,GACL,KACC;;;AAIX,YAAY,cAAc"}
|
|
@@ -47,7 +47,7 @@ const Stepper = react.forwardRef(({ value, onValueChange, onValueCommit, min = 0
|
|
|
47
47
|
"aria-label": decreaseLabel,
|
|
48
48
|
onClick: decrement,
|
|
49
49
|
disabled: disabled || atMin,
|
|
50
|
-
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
50
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent",
|
|
51
51
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
|
|
52
52
|
className: "text-[16px]",
|
|
53
53
|
name: "chevron_left"
|
|
@@ -63,7 +63,7 @@ const Stepper = react.forwardRef(({ value, onValueChange, onValueCommit, min = 0
|
|
|
63
63
|
"aria-label": increaseLabel,
|
|
64
64
|
onClick: increment,
|
|
65
65
|
disabled: disabled || atMax,
|
|
66
|
-
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
66
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent",
|
|
67
67
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
|
|
68
68
|
className: "text-[16px]",
|
|
69
69
|
name: "chevron_right"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stepper.cjs","names":["React","MaterialSymbol"],"sources":["../../src/components/stepper.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\ninterface StepperProps {\n value: number\n onValueChange: (value: number) => void\n /**\n * Fires once after the user stops interacting (default: 400ms after the last\n * click). Use this for side effects like persisting the value, so rapid\n * clicks don't fan out into a request per click.\n */\n onValueCommit?: (value: number) => void\n min?: number\n max?: number\n step?: number\n formatValue?: (value: number) => string\n disabled?: boolean\n className?: string\n /** Delay (ms) after the last click before `onValueCommit` fires. */\n commitDelayMs?: number\n /** Accessible label, e.g. \"Sunshare percentage\". */\n 'aria-label'?: string\n /** Accessible label for the decrement button. Defaults to `\"Decrease\"`. */\n decreaseLabel?: string\n /** Accessible label for the increment button. Defaults to `\"Increase\"`. */\n increaseLabel?: string\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n (\n {\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n formatValue = (v) => String(v),\n disabled,\n className,\n commitDelayMs = 400,\n 'aria-label': ariaLabel,\n decreaseLabel = 'Decrease',\n increaseLabel = 'Increase',\n },\n ref,\n ) => {\n const commitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n React.useEffect(\n () => () => {\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n },\n [],\n )\n\n const scheduleCommit = (next: number) => {\n if (!onValueCommit) return\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n commitTimerRef.current = setTimeout(() => {\n commitTimerRef.current = null\n onValueCommit(next)\n }, commitDelayMs)\n }\n\n const decrement = () => {\n const next = Math.max(min, value - step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n const increment = () => {\n const next = Math.min(max, value + step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n\n const atMin = value <= min\n const atMax = value >= max\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex h-12 w-full items-center justify-between rounded-xl bg-base-gray-soft px-2',\n className,\n )}\n >\n <button\n type=\"button\"\n aria-label={decreaseLabel}\n onClick={decrement}\n disabled={disabled || atMin}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
1
|
+
{"version":3,"file":"stepper.cjs","names":["React","MaterialSymbol"],"sources":["../../src/components/stepper.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\ninterface StepperProps {\n value: number\n onValueChange: (value: number) => void\n /**\n * Fires once after the user stops interacting (default: 400ms after the last\n * click). Use this for side effects like persisting the value, so rapid\n * clicks don't fan out into a request per click.\n */\n onValueCommit?: (value: number) => void\n min?: number\n max?: number\n step?: number\n formatValue?: (value: number) => string\n disabled?: boolean\n className?: string\n /** Delay (ms) after the last click before `onValueCommit` fires. */\n commitDelayMs?: number\n /** Accessible label, e.g. \"Sunshare percentage\". */\n 'aria-label'?: string\n /** Accessible label for the decrement button. Defaults to `\"Decrease\"`. */\n decreaseLabel?: string\n /** Accessible label for the increment button. Defaults to `\"Increase\"`. */\n increaseLabel?: string\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n (\n {\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n formatValue = (v) => String(v),\n disabled,\n className,\n commitDelayMs = 400,\n 'aria-label': ariaLabel,\n decreaseLabel = 'Decrease',\n increaseLabel = 'Increase',\n },\n ref,\n ) => {\n const commitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n React.useEffect(\n () => () => {\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n },\n [],\n )\n\n const scheduleCommit = (next: number) => {\n if (!onValueCommit) return\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n commitTimerRef.current = setTimeout(() => {\n commitTimerRef.current = null\n onValueCommit(next)\n }, commitDelayMs)\n }\n\n const decrement = () => {\n const next = Math.max(min, value - step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n const increment = () => {\n const next = Math.min(max, value + step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n\n const atMin = value <= min\n const atMax = value >= max\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex h-12 w-full items-center justify-between rounded-xl bg-base-gray-soft px-2',\n className,\n )}\n >\n <button\n type=\"button\"\n aria-label={decreaseLabel}\n onClick={decrement}\n disabled={disabled || atMin}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent\"\n >\n <MaterialSymbol className=\"text-[16px]\" name=\"chevron_left\" />\n </button>\n <span\n className=\"text-base font-semibold text-gray tabular-nums select-none\"\n aria-live=\"polite\"\n >\n {formatValue(value)}\n </span>\n <button\n type=\"button\"\n aria-label={increaseLabel}\n onClick={increment}\n disabled={disabled || atMax}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent\"\n >\n <MaterialSymbol className=\"text-[16px]\" name=\"chevron_right\" />\n </button>\n </div>\n )\n },\n)\n\nStepper.displayName = 'Stepper'\n\nexport { Stepper }\nexport type { StepperProps }\n"],"mappings":";;;;;;;;;AA6BA,MAAM,UAAUA,MAAM,YAElB,EACE,OACA,eACA,eACA,MAAM,GACN,MAAM,KACN,OAAO,GACP,eAAe,MAAM,OAAO,EAAE,EAC9B,UACA,WACA,gBAAgB,KAChB,cAAc,WACd,gBAAgB,YAChB,gBAAgB,cAElB,QACG;CACH,MAAM,iBAAiBA,MAAM,OAC3B,KACD;AAED,OAAM,sBACQ;AACV,MAAI,eAAe,QAAS,cAAa,eAAe,QAAQ;IAElE,EAAE,CACH;CAED,MAAM,kBAAkB,SAAiB;AACvC,MAAI,CAAC,cAAe;AACpB,MAAI,eAAe,QAAS,cAAa,eAAe,QAAQ;AAChE,iBAAe,UAAU,iBAAiB;AACxC,kBAAe,UAAU;AACzB,iBAAc,KAAK;KAClB,cAAc;;CAGnB,MAAM,kBAAkB;EACtB,MAAM,OAAO,KAAK,IAAI,KAAK,QAAQ,KAAK;AACxC,MAAI,SAAS,OAAO;AAClB,iBAAc,KAAK;AACnB,kBAAe,KAAK;;;CAGxB,MAAM,kBAAkB;EACtB,MAAM,OAAO,KAAK,IAAI,KAAK,QAAQ,KAAK;AACxC,MAAI,SAAS,OAAO;AAClB,iBAAc,KAAK;AACnB,kBAAe,KAAK;;;CAIxB,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,SAAS;AAEvB,QACE,iBAAA,GAAA,kBAAA,MAAC,OAAD;EACO;EACL,MAAK;EACL,cAAY;EACZ,YAAA,GAAA,OAAA,IACE,0FACA,UACD;YAPH;GASE,iBAAA,GAAA,kBAAA,KAAC,UAAD;IACE,MAAK;IACL,cAAY;IACZ,SAAS;IACT,UAAU,YAAY;IACtB,WAAU;cAEV,iBAAA,GAAA,kBAAA,KAACC,8BAAAA,gBAAD;KAAgB,WAAU;KAAc,MAAK;KAAiB,CAAA;IACvD,CAAA;GACT,iBAAA,GAAA,kBAAA,KAAC,QAAD;IACE,WAAU;IACV,aAAU;cAET,YAAY,MAAM;IACd,CAAA;GACP,iBAAA,GAAA,kBAAA,KAAC,UAAD;IACE,MAAK;IACL,cAAY;IACZ,SAAS;IACT,UAAU,YAAY;IACtB,WAAU;cAEV,iBAAA,GAAA,kBAAA,KAACA,8BAAAA,gBAAD;KAAgB,WAAU;KAAc,MAAK;KAAkB,CAAA;IACxD,CAAA;GACL;;EAGX;AAED,QAAQ,cAAc"}
|
|
@@ -44,7 +44,7 @@ const Stepper = React$1.forwardRef(({ value, onValueChange, onValueCommit, min =
|
|
|
44
44
|
"aria-label": decreaseLabel,
|
|
45
45
|
onClick: decrement,
|
|
46
46
|
disabled: disabled || atMin,
|
|
47
|
-
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
47
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent",
|
|
48
48
|
children: /* @__PURE__ */ jsx(MaterialSymbol, {
|
|
49
49
|
className: "text-[16px]",
|
|
50
50
|
name: "chevron_left"
|
|
@@ -60,7 +60,7 @@ const Stepper = React$1.forwardRef(({ value, onValueChange, onValueCommit, min =
|
|
|
60
60
|
"aria-label": increaseLabel,
|
|
61
61
|
onClick: increment,
|
|
62
62
|
disabled: disabled || atMax,
|
|
63
|
-
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
63
|
+
className: "inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent",
|
|
64
64
|
children: /* @__PURE__ */ jsx(MaterialSymbol, {
|
|
65
65
|
className: "text-[16px]",
|
|
66
66
|
name: "chevron_right"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stepper.js","names":["React"],"sources":["../../src/components/stepper.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\ninterface StepperProps {\n value: number\n onValueChange: (value: number) => void\n /**\n * Fires once after the user stops interacting (default: 400ms after the last\n * click). Use this for side effects like persisting the value, so rapid\n * clicks don't fan out into a request per click.\n */\n onValueCommit?: (value: number) => void\n min?: number\n max?: number\n step?: number\n formatValue?: (value: number) => string\n disabled?: boolean\n className?: string\n /** Delay (ms) after the last click before `onValueCommit` fires. */\n commitDelayMs?: number\n /** Accessible label, e.g. \"Sunshare percentage\". */\n 'aria-label'?: string\n /** Accessible label for the decrement button. Defaults to `\"Decrease\"`. */\n decreaseLabel?: string\n /** Accessible label for the increment button. Defaults to `\"Increase\"`. */\n increaseLabel?: string\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n (\n {\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n formatValue = (v) => String(v),\n disabled,\n className,\n commitDelayMs = 400,\n 'aria-label': ariaLabel,\n decreaseLabel = 'Decrease',\n increaseLabel = 'Increase',\n },\n ref,\n ) => {\n const commitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n React.useEffect(\n () => () => {\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n },\n [],\n )\n\n const scheduleCommit = (next: number) => {\n if (!onValueCommit) return\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n commitTimerRef.current = setTimeout(() => {\n commitTimerRef.current = null\n onValueCommit(next)\n }, commitDelayMs)\n }\n\n const decrement = () => {\n const next = Math.max(min, value - step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n const increment = () => {\n const next = Math.min(max, value + step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n\n const atMin = value <= min\n const atMax = value >= max\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex h-12 w-full items-center justify-between rounded-xl bg-base-gray-soft px-2',\n className,\n )}\n >\n <button\n type=\"button\"\n aria-label={decreaseLabel}\n onClick={decrement}\n disabled={disabled || atMin}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-
|
|
1
|
+
{"version":3,"file":"stepper.js","names":["React"],"sources":["../../src/components/stepper.tsx"],"sourcesContent":["import * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\ninterface StepperProps {\n value: number\n onValueChange: (value: number) => void\n /**\n * Fires once after the user stops interacting (default: 400ms after the last\n * click). Use this for side effects like persisting the value, so rapid\n * clicks don't fan out into a request per click.\n */\n onValueCommit?: (value: number) => void\n min?: number\n max?: number\n step?: number\n formatValue?: (value: number) => string\n disabled?: boolean\n className?: string\n /** Delay (ms) after the last click before `onValueCommit` fires. */\n commitDelayMs?: number\n /** Accessible label, e.g. \"Sunshare percentage\". */\n 'aria-label'?: string\n /** Accessible label for the decrement button. Defaults to `\"Decrease\"`. */\n decreaseLabel?: string\n /** Accessible label for the increment button. Defaults to `\"Increase\"`. */\n increaseLabel?: string\n}\n\nconst Stepper = React.forwardRef<HTMLDivElement, StepperProps>(\n (\n {\n value,\n onValueChange,\n onValueCommit,\n min = 0,\n max = 100,\n step = 1,\n formatValue = (v) => String(v),\n disabled,\n className,\n commitDelayMs = 400,\n 'aria-label': ariaLabel,\n decreaseLabel = 'Decrease',\n increaseLabel = 'Increase',\n },\n ref,\n ) => {\n const commitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(\n null,\n )\n\n React.useEffect(\n () => () => {\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n },\n [],\n )\n\n const scheduleCommit = (next: number) => {\n if (!onValueCommit) return\n if (commitTimerRef.current) clearTimeout(commitTimerRef.current)\n commitTimerRef.current = setTimeout(() => {\n commitTimerRef.current = null\n onValueCommit(next)\n }, commitDelayMs)\n }\n\n const decrement = () => {\n const next = Math.max(min, value - step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n const increment = () => {\n const next = Math.min(max, value + step)\n if (next !== value) {\n onValueChange(next)\n scheduleCommit(next)\n }\n }\n\n const atMin = value <= min\n const atMax = value >= max\n\n return (\n <div\n ref={ref}\n role=\"group\"\n aria-label={ariaLabel}\n className={cn(\n 'inline-flex h-12 w-full items-center justify-between rounded-xl bg-base-gray-soft px-2',\n className,\n )}\n >\n <button\n type=\"button\"\n aria-label={decreaseLabel}\n onClick={decrement}\n disabled={disabled || atMin}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent\"\n >\n <MaterialSymbol className=\"text-[16px]\" name=\"chevron_left\" />\n </button>\n <span\n className=\"text-base font-semibold text-gray tabular-nums select-none\"\n aria-live=\"polite\"\n >\n {formatValue(value)}\n </span>\n <button\n type=\"button\"\n aria-label={increaseLabel}\n onClick={increment}\n disabled={disabled || atMax}\n className=\"inline-flex size-8 items-center justify-center rounded-md text-gray transition-colors hover:bg-gray-a3 disabled:opacity-40 disabled:hover:bg-transparent\"\n >\n <MaterialSymbol className=\"text-[16px]\" name=\"chevron_right\" />\n </button>\n </div>\n )\n },\n)\n\nStepper.displayName = 'Stepper'\n\nexport { Stepper }\nexport type { StepperProps }\n"],"mappings":";;;;;;AA6BA,MAAM,UAAUA,QAAM,YAElB,EACE,OACA,eACA,eACA,MAAM,GACN,MAAM,KACN,OAAO,GACP,eAAe,MAAM,OAAO,EAAE,EAC9B,UACA,WACA,gBAAgB,KAChB,cAAc,WACd,gBAAgB,YAChB,gBAAgB,cAElB,QACG;CACH,MAAM,iBAAiBA,QAAM,OAC3B,KACD;AAED,SAAM,sBACQ;AACV,MAAI,eAAe,QAAS,cAAa,eAAe,QAAQ;IAElE,EAAE,CACH;CAED,MAAM,kBAAkB,SAAiB;AACvC,MAAI,CAAC,cAAe;AACpB,MAAI,eAAe,QAAS,cAAa,eAAe,QAAQ;AAChE,iBAAe,UAAU,iBAAiB;AACxC,kBAAe,UAAU;AACzB,iBAAc,KAAK;KAClB,cAAc;;CAGnB,MAAM,kBAAkB;EACtB,MAAM,OAAO,KAAK,IAAI,KAAK,QAAQ,KAAK;AACxC,MAAI,SAAS,OAAO;AAClB,iBAAc,KAAK;AACnB,kBAAe,KAAK;;;CAGxB,MAAM,kBAAkB;EACtB,MAAM,OAAO,KAAK,IAAI,KAAK,QAAQ,KAAK;AACxC,MAAI,SAAS,OAAO;AAClB,iBAAc,KAAK;AACnB,kBAAe,KAAK;;;CAIxB,MAAM,QAAQ,SAAS;CACvB,MAAM,QAAQ,SAAS;AAEvB,QACE,qBAAC,OAAD;EACO;EACL,MAAK;EACL,cAAY;EACZ,WAAW,GACT,0FACA,UACD;YAPH;GASE,oBAAC,UAAD;IACE,MAAK;IACL,cAAY;IACZ,SAAS;IACT,UAAU,YAAY;IACtB,WAAU;cAEV,oBAAC,gBAAD;KAAgB,WAAU;KAAc,MAAK;KAAiB,CAAA;IACvD,CAAA;GACT,oBAAC,QAAD;IACE,WAAU;IACV,aAAU;cAET,YAAY,MAAM;IACd,CAAA;GACP,oBAAC,UAAD;IACE,MAAK;IACL,cAAY;IACZ,SAAS;IACT,UAAU,YAAY;IACtB,WAAU;cAEV,oBAAC,gBAAD;KAAgB,WAAU;KAAc,MAAK;KAAkB,CAAA;IACxD,CAAA;GACL;;EAGX;AAED,QAAQ,cAAc"}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
require("../chunk-aJGBMT86.cjs");
|
|
4
|
+
const require_accent_colors = require("../accent-colors-CjHCf8fH.cjs");
|
|
4
5
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
5
6
|
let _base_ui_react_switch = require("@base-ui/react/switch");
|
|
6
7
|
let cnfast = require("cnfast");
|
|
7
8
|
//#region src/components/switch.tsx
|
|
8
|
-
function Switch({ className, size: _size, variant: _variant, ...props }) {
|
|
9
|
+
function Switch({ className, styleVariant, color, size: _size, variant: _variant, ...props }) {
|
|
10
|
+
const dataStyle = styleVariant !== void 0 || color !== void 0 ? styleVariant ?? "solid" : void 0;
|
|
11
|
+
const dataAccentColor = color ? require_accent_colors.accentByColor[color] : void 0;
|
|
9
12
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_switch.Switch.Root, {
|
|
10
13
|
"data-slot": "switch",
|
|
11
|
-
|
|
14
|
+
"data-style": dataStyle,
|
|
15
|
+
"data-accent-color": dataAccentColor,
|
|
16
|
+
className: (0, cnfast.cn)("peer relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center p-[2px] transition-colors", "before:absolute before:inset-x-0 before:top-1/2 before:h-10 before:-translate-y-1/2 before:content-['']", "rounded-(--spark-rounded-xl)", "shadow-[0_1px_2px_0_rgb(0_0_0/0.05),inset_0_0_0_1px_var(--spark-switch-ring,transparent)]", "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", "data-disabled:pointer-events-none data-disabled:opacity-50", "data-unchecked:bg-gray-a5 data-unchecked:[--spark-switch-ring:var(--spark-switch-idle-border,transparent)]", "data-checked:bg-(--spark-switch-track,var(--spark-9)) data-checked:[--spark-switch-ring:var(--spark-style-border,transparent)]", className),
|
|
12
17
|
...props,
|
|
13
18
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_switch.Switch.Thumb, {
|
|
14
19
|
"data-slot": "switch-thumb",
|
|
15
|
-
className: (0, cnfast.cn)("pointer-events-none block size-5 bg-
|
|
20
|
+
className: (0, cnfast.cn)("pointer-events-none block size-5 bg-(--spark-switch-thumb,var(--spark-on-solid,#fff)) shadow-xs transition-transform", "rounded-(--spark-rounded-lg)", "data-checked:translate-x-4 data-unchecked:translate-x-0")
|
|
16
21
|
})
|
|
17
22
|
});
|
|
18
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switch.cjs","names":["SwitchPrimitive"],"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { Switch as SwitchPrimitive } from '@base-ui/react/switch'\nimport { cn } from '../utils/cn'\n\ntype SwitchProps = SwitchPrimitive.Root.Props & {\n /**\n * @deprecated The switch now renders a single size. Accepted for back-compat\n * but ignored.\n */\n size?: 'sm' | 'base' | 'lg'\n /**\n * @deprecated Use
|
|
1
|
+
{"version":3,"file":"switch.cjs","names":["accentByColor","SwitchPrimitive"],"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { Switch as SwitchPrimitive } from '@base-ui/react/switch'\nimport { accentByColor } from '../lib/accent-colors'\nimport type { SparkStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type SparkSwitchColor = 'accent' | 'neutral' | 'danger'\n\ntype SwitchProps = SwitchPrimitive.Root.Props & {\n styleVariant?: SparkStyle\n color?: SparkSwitchColor\n /**\n * @deprecated The switch now renders a single size. Accepted for back-compat\n * but ignored.\n */\n size?: 'sm' | 'base' | 'lg'\n /**\n * @deprecated Use `color=\"danger\"` for a destructive/error switch. Accepted\n * for back-compat but ignored.\n */\n variant?: 'primary' | 'destructive'\n}\n\nfunction Switch({\n className,\n styleVariant,\n color,\n size: _size,\n variant: _variant,\n ...props\n}: SwitchProps) {\n const hasLook = styleVariant !== undefined || color !== undefined\n const dataStyle = hasLook ? (styleVariant ?? 'solid') : undefined\n const dataAccentColor = color ? accentByColor[color] : undefined\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n data-style={dataStyle}\n data-accent-color={dataAccentColor}\n className={cn(\n 'peer relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center p-[2px] transition-colors',\n \"before:absolute before:inset-x-0 before:top-1/2 before:h-10 before:-translate-y-1/2 before:content-['']\",\n 'rounded-(--spark-rounded-xl)',\n 'shadow-[0_1px_2px_0_rgb(0_0_0/0.05),inset_0_0_0_1px_var(--spark-switch-ring,transparent)]',\n 'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n 'data-disabled:pointer-events-none data-disabled:opacity-50',\n 'data-unchecked:bg-gray-a5 data-unchecked:[--spark-switch-ring:var(--spark-switch-idle-border,transparent)]',\n 'data-checked:bg-(--spark-switch-track,var(--spark-9)) data-checked:[--spark-switch-ring:var(--spark-style-border,transparent)]',\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n 'pointer-events-none block size-5 bg-(--spark-switch-thumb,var(--spark-on-solid,#fff)) shadow-xs transition-transform',\n 'rounded-(--spark-rounded-lg)',\n 'data-checked:translate-x-4 data-unchecked:translate-x-0',\n )}\n />\n </SwitchPrimitive.Root>\n )\n}\n\nexport { Switch }\nexport type { SwitchProps }\n"],"mappings":";;;;;;;;AAsBA,SAAS,OAAO,EACd,WACA,cACA,OACA,MAAM,OACN,SAAS,UACT,GAAG,SACW;CAEd,MAAM,YADU,iBAAiB,KAAA,KAAa,UAAU,KAAA,IAC3B,gBAAgB,UAAW,KAAA;CACxD,MAAM,kBAAkB,QAAQA,sBAAAA,cAAc,SAAS,KAAA;AACvD,QACE,iBAAA,GAAA,kBAAA,KAACC,sBAAAA,OAAgB,MAAjB;EACE,aAAU;EACV,cAAY;EACZ,qBAAmB;EACnB,YAAA,GAAA,OAAA,IACE,qGACA,2GACA,gCACA,6FACA,iFACA,8DACA,8GACA,kIACA,UACD;EACD,GAAI;YAEJ,iBAAA,GAAA,kBAAA,KAACA,sBAAAA,OAAgB,OAAjB;GACE,aAAU;GACV,YAAA,GAAA,OAAA,IACE,wHACA,gCACA,0DACD;GACD,CAAA;EACmB,CAAA"}
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { u as SparkStyle } from "../theme-types-BwSJKdsB.cjs";
|
|
3
4
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
5
|
import { Switch as Switch$1 } from "@base-ui/react/switch";
|
|
5
6
|
|
|
6
7
|
//#region src/components/switch.d.ts
|
|
8
|
+
type SparkSwitchColor = 'accent' | 'neutral' | 'danger';
|
|
7
9
|
type SwitchProps = Switch$1.Root.Props & {
|
|
10
|
+
styleVariant?: SparkStyle;
|
|
11
|
+
color?: SparkSwitchColor;
|
|
8
12
|
/**
|
|
9
13
|
* @deprecated The switch now renders a single size. Accepted for back-compat
|
|
10
14
|
* but ignored.
|
|
11
15
|
*/
|
|
12
16
|
size?: 'sm' | 'base' | 'lg';
|
|
13
17
|
/**
|
|
14
|
-
* @deprecated Use
|
|
15
|
-
*
|
|
16
|
-
* back-compat but ignored.
|
|
18
|
+
* @deprecated Use `color="danger"` for a destructive/error switch. Accepted
|
|
19
|
+
* for back-compat but ignored.
|
|
17
20
|
*/
|
|
18
21
|
variant?: 'primary' | 'destructive';
|
|
19
22
|
};
|
|
20
23
|
declare function Switch({
|
|
21
24
|
className,
|
|
25
|
+
styleVariant,
|
|
26
|
+
color,
|
|
22
27
|
size: _size,
|
|
23
28
|
variant: _variant,
|
|
24
29
|
...props
|
|
25
30
|
}: SwitchProps): _$react_jsx_runtime0.JSX.Element;
|
|
26
31
|
//#endregion
|
|
27
|
-
export { Switch, type SwitchProps };
|
|
32
|
+
export { SparkSwitchColor, Switch, type SwitchProps };
|
|
28
33
|
//# sourceMappingURL=switch.d.cts.map
|
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { u as SparkStyle } from "../theme-types-BDdoRxXa.js";
|
|
3
4
|
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
5
|
import { Switch as Switch$1 } from "@base-ui/react/switch";
|
|
5
6
|
|
|
6
7
|
//#region src/components/switch.d.ts
|
|
8
|
+
type SparkSwitchColor = 'accent' | 'neutral' | 'danger';
|
|
7
9
|
type SwitchProps = Switch$1.Root.Props & {
|
|
10
|
+
styleVariant?: SparkStyle;
|
|
11
|
+
color?: SparkSwitchColor;
|
|
8
12
|
/**
|
|
9
13
|
* @deprecated The switch now renders a single size. Accepted for back-compat
|
|
10
14
|
* but ignored.
|
|
11
15
|
*/
|
|
12
16
|
size?: 'sm' | 'base' | 'lg';
|
|
13
17
|
/**
|
|
14
|
-
* @deprecated Use
|
|
15
|
-
*
|
|
16
|
-
* back-compat but ignored.
|
|
18
|
+
* @deprecated Use `color="danger"` for a destructive/error switch. Accepted
|
|
19
|
+
* for back-compat but ignored.
|
|
17
20
|
*/
|
|
18
21
|
variant?: 'primary' | 'destructive';
|
|
19
22
|
};
|
|
20
23
|
declare function Switch({
|
|
21
24
|
className,
|
|
25
|
+
styleVariant,
|
|
26
|
+
color,
|
|
22
27
|
size: _size,
|
|
23
28
|
variant: _variant,
|
|
24
29
|
...props
|
|
25
30
|
}: SwitchProps): _$react_jsx_runtime0.JSX.Element;
|
|
26
31
|
//#endregion
|
|
27
|
-
export { Switch, type SwitchProps };
|
|
32
|
+
export { SparkSwitchColor, Switch, type SwitchProps };
|
|
28
33
|
//# sourceMappingURL=switch.d.ts.map
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../utils/cn.js";
|
|
3
|
+
import { t as accentByColor } from "../accent-colors-Ch2J0EEY.js";
|
|
3
4
|
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
import { Switch as Switch$1 } from "@base-ui/react/switch";
|
|
5
6
|
//#region src/components/switch.tsx
|
|
6
|
-
function Switch({ className, size: _size, variant: _variant, ...props }) {
|
|
7
|
+
function Switch({ className, styleVariant, color, size: _size, variant: _variant, ...props }) {
|
|
8
|
+
const dataStyle = styleVariant !== void 0 || color !== void 0 ? styleVariant ?? "solid" : void 0;
|
|
9
|
+
const dataAccentColor = color ? accentByColor[color] : void 0;
|
|
7
10
|
return /* @__PURE__ */ jsx(Switch$1.Root, {
|
|
8
11
|
"data-slot": "switch",
|
|
9
|
-
|
|
12
|
+
"data-style": dataStyle,
|
|
13
|
+
"data-accent-color": dataAccentColor,
|
|
14
|
+
className: cn("peer relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center p-[2px] transition-colors", "before:absolute before:inset-x-0 before:top-1/2 before:h-10 before:-translate-y-1/2 before:content-['']", "rounded-(--spark-rounded-xl)", "shadow-[0_1px_2px_0_rgb(0_0_0/0.05),inset_0_0_0_1px_var(--spark-switch-ring,transparent)]", "focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden", "data-disabled:pointer-events-none data-disabled:opacity-50", "data-unchecked:bg-gray-a5 data-unchecked:[--spark-switch-ring:var(--spark-switch-idle-border,transparent)]", "data-checked:bg-(--spark-switch-track,var(--spark-9)) data-checked:[--spark-switch-ring:var(--spark-style-border,transparent)]", className),
|
|
10
15
|
...props,
|
|
11
16
|
children: /* @__PURE__ */ jsx(Switch$1.Thumb, {
|
|
12
17
|
"data-slot": "switch-thumb",
|
|
13
|
-
className: cn("pointer-events-none block size-5 bg-
|
|
18
|
+
className: cn("pointer-events-none block size-5 bg-(--spark-switch-thumb,var(--spark-on-solid,#fff)) shadow-xs transition-transform", "rounded-(--spark-rounded-lg)", "data-checked:translate-x-4 data-unchecked:translate-x-0")
|
|
14
19
|
})
|
|
15
20
|
});
|
|
16
21
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"switch.js","names":["SwitchPrimitive"],"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { Switch as SwitchPrimitive } from '@base-ui/react/switch'\nimport { cn } from '../utils/cn'\n\ntype SwitchProps = SwitchPrimitive.Root.Props & {\n /**\n * @deprecated The switch now renders a single size. Accepted for back-compat\n * but ignored.\n */\n size?: 'sm' | 'base' | 'lg'\n /**\n * @deprecated Use
|
|
1
|
+
{"version":3,"file":"switch.js","names":["SwitchPrimitive"],"sources":["../../src/components/switch.tsx"],"sourcesContent":["import { Switch as SwitchPrimitive } from '@base-ui/react/switch'\nimport { accentByColor } from '../lib/accent-colors'\nimport type { SparkStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type SparkSwitchColor = 'accent' | 'neutral' | 'danger'\n\ntype SwitchProps = SwitchPrimitive.Root.Props & {\n styleVariant?: SparkStyle\n color?: SparkSwitchColor\n /**\n * @deprecated The switch now renders a single size. Accepted for back-compat\n * but ignored.\n */\n size?: 'sm' | 'base' | 'lg'\n /**\n * @deprecated Use `color=\"danger\"` for a destructive/error switch. Accepted\n * for back-compat but ignored.\n */\n variant?: 'primary' | 'destructive'\n}\n\nfunction Switch({\n className,\n styleVariant,\n color,\n size: _size,\n variant: _variant,\n ...props\n}: SwitchProps) {\n const hasLook = styleVariant !== undefined || color !== undefined\n const dataStyle = hasLook ? (styleVariant ?? 'solid') : undefined\n const dataAccentColor = color ? accentByColor[color] : undefined\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n data-style={dataStyle}\n data-accent-color={dataAccentColor}\n className={cn(\n 'peer relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center p-[2px] transition-colors',\n \"before:absolute before:inset-x-0 before:top-1/2 before:h-10 before:-translate-y-1/2 before:content-['']\",\n 'rounded-(--spark-rounded-xl)',\n 'shadow-[0_1px_2px_0_rgb(0_0_0/0.05),inset_0_0_0_1px_var(--spark-switch-ring,transparent)]',\n 'focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden',\n 'data-disabled:pointer-events-none data-disabled:opacity-50',\n 'data-unchecked:bg-gray-a5 data-unchecked:[--spark-switch-ring:var(--spark-switch-idle-border,transparent)]',\n 'data-checked:bg-(--spark-switch-track,var(--spark-9)) data-checked:[--spark-switch-ring:var(--spark-style-border,transparent)]',\n className,\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n 'pointer-events-none block size-5 bg-(--spark-switch-thumb,var(--spark-on-solid,#fff)) shadow-xs transition-transform',\n 'rounded-(--spark-rounded-lg)',\n 'data-checked:translate-x-4 data-unchecked:translate-x-0',\n )}\n />\n </SwitchPrimitive.Root>\n )\n}\n\nexport { Switch }\nexport type { SwitchProps }\n"],"mappings":";;;;;;AAsBA,SAAS,OAAO,EACd,WACA,cACA,OACA,MAAM,OACN,SAAS,UACT,GAAG,SACW;CAEd,MAAM,YADU,iBAAiB,KAAA,KAAa,UAAU,KAAA,IAC3B,gBAAgB,UAAW,KAAA;CACxD,MAAM,kBAAkB,QAAQ,cAAc,SAAS,KAAA;AACvD,QACE,oBAACA,SAAgB,MAAjB;EACE,aAAU;EACV,cAAY;EACZ,qBAAmB;EACnB,WAAW,GACT,qGACA,2GACA,gCACA,6FACA,iFACA,8DACA,8GACA,kIACA,UACD;EACD,GAAI;YAEJ,oBAACA,SAAgB,OAAjB;GACE,aAAU;GACV,WAAW,GACT,wHACA,gCACA,0DACD;GACD,CAAA;EACmB,CAAA"}
|
package/dist/components/tabs.cjs
CHANGED
|
@@ -120,7 +120,7 @@ function TabsList({ className, style, variant = "underline", ref, ...props }) {
|
|
|
120
120
|
onClick: scrollByPage(dir),
|
|
121
121
|
tabIndex: -1,
|
|
122
122
|
type: "button",
|
|
123
|
-
className: (0, cnfast.cn)("absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-
|
|
123
|
+
className: (0, cnfast.cn)("absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12", dir === 1 ? "right-0 group-data-overflow-end/tabs:flex" : "left-0 group-data-overflow-start/tabs:flex"),
|
|
124
124
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, { name: dir === 1 ? "chevron_right" : "chevron_left" })
|
|
125
125
|
});
|
|
126
126
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -134,7 +134,7 @@ function TabsList({ className, style, variant = "underline", ref, ...props }) {
|
|
|
134
134
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_tabs.Tabs.List, {
|
|
135
135
|
"data-slot": "tabs-scroller",
|
|
136
136
|
ref: scrollerCallbackRef,
|
|
137
|
-
className: (0, cnfast.cn)("relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto", resolved === "underline" && "gap-2", resolved === "underline" && "after:pointer-events-none after:absolute after:bottom-0 after:left-
|
|
137
|
+
className: (0, cnfast.cn)("relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto", resolved === "underline" && "gap-2", resolved === "underline" && "after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-[left,width] after:duration-200 after:ease-out after:content-[''] motion-reduce:after:transition-none", resolved === "pill" && "gap-0 rounded-md bg-gray-a3 p-1"),
|
|
138
138
|
...props
|
|
139
139
|
}),
|
|
140
140
|
chevron(1)
|
|
@@ -144,11 +144,11 @@ function TabsList({ className, style, variant = "underline", ref, ...props }) {
|
|
|
144
144
|
function TabsTrigger({ className, children, ...props }) {
|
|
145
145
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_tabs.Tabs.Tab, {
|
|
146
146
|
"data-slot": "tabs-trigger",
|
|
147
|
-
className: (0, cnfast.cn)("group/trigger inline-flex min-w-
|
|
147
|
+
className: (0, cnfast.cn)("group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors", "data-active:text-(--spark-tabs-ink,var(--spark-a11))", "focus-visible:outline-hidden", "group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1", "data-disabled:pointer-events-none data-disabled:text-gray-a8", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4", "group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]", "group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5", "group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium", "group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3", "group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6", className),
|
|
148
148
|
...props,
|
|
149
149
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
150
150
|
"data-slot": "tabs-trigger-inner",
|
|
151
|
-
className: (0, cnfast.cn)("inline-flex items-center gap-1", "group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors", "group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3"),
|
|
151
|
+
className: (0, cnfast.cn)("inline-flex items-center gap-1", "group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors", "group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3", "group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1"),
|
|
152
152
|
children
|
|
153
153
|
})
|
|
154
154
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabs.cjs","names":["React","TabsPrimitive","MaterialSymbol"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n // Pointer-only affordance: keyboard users page through tabs with arrow keys\n // (focus auto-scrolls), so the chevrons are aria-hidden and out of the tab\n // order — they exist to make hidden overflow visible and clickable. They also\n // can't live INSIDE the list: role=tablist allows only tab children (ARIA),\n // and an absolute child of the scroll container would scroll away with it.\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n // frosted-glass overflow indicator: a 40x40 icon button (button-aligned\n // radius) floating at the overflowing edge, vertically centered. The\n // backdrop-blur reads the tabs sliding under it, so the chevron stays\n // legible over any tab colour — no shadow needed.\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-[var(--color-gray-1)]/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-[overflow-end]/tabs:flex'\n : 'left-0 group-data-[overflow-start]/tabs:flex',\n )}\n >\n <MaterialSymbol name={dir === 1 ? 'chevron_right' : 'chevron_left'} />\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn('group/tabs relative inline-block max-w-full', className)}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n // mobile-first: scrolls horizontally when the tabs don't fit (inert\n // otherwise). `relative` makes THIS the containing block + offsetParent\n // for the sliding bar: the ::after is laid out in the scroll content, so\n // it travels with its tab, and triggers measure content-relative\n // offsetLeft. scroll-smooth makes every scroll travel — clicks/focus\n // (which honor the triggers' scroll-mx peek), the chevrons' scrollBy,\n // and the observer's reveal.\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n // underline: no continuous track — each trigger carries its OWN bottom\n // rail (below), so the rails read as per-tab segments with gaps between\n // them. items-stretch keeps every trigger full height so their rails (and\n // the accent bar) line up on one baseline.\n resolved === 'underline' && 'gap-2',\n // underline: the sliding accent bar — driven by --tabs-indicator-x/-w\n // (measured above; internal vars, not theme knobs). It rides ON TOP of the\n // active tab's own rail (4px accent over the 2px neutral rail), and worms\n // between tabs. An absolute child of the scroll container scrolls with the\n // content, so it tracks its tab mid-overflow. Color = the parity token\n // --spark-indicator (step 9), HC deepens to 12.\n resolved === 'underline' &&\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-[var(--tabs-indicator-x,0px)] after:h-1 after:w-[var(--tabs-indicator-w,0px)] after:rounded-full after:bg-[var(--spark-tabs-indicator,var(--spark-indicator))] after:transition-[left,width] after:duration-200 after:ease-out after:content-[''] motion-reduce:after:transition-none\",\n // pill: soft neutral track, radius follows the knob (never a full circle)\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n // shared: state-driven ink — rest = full neutral (per Figma, not dimmed),\n // active = accent (HC deepens via --spark-tabs-ink). Hover is Radix-style\n // NEUTRAL in both variants (soft fill only, ink stays gray-12 until\n // selected) — berni 2026-07-06, over the Figma no-counter accent hover.\n // scroll-mx = the peek margin every scroll-into-view honors (native focus\n // scrolling included): a revealed tab always drags 40px of its neighbor in\n // group/trigger lets the inner label box react to hover ANYWHERE on the\n // full hit target (the paint shrinks, the target doesn't). min-w reads a var\n // (set per-variant below) via a PLAIN utility, so a consumer's className\n // min-w-* wins by tailwind-merge — a group-data:min-w-* would outrank it on\n // specificity (e.g. icon-only tabs that want to be narrow).\n 'group/trigger inline-flex min-w-[var(--tabs-trigger-minw,0px)] shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n 'data-active:text-[color:var(--spark-tabs-ink,var(--spark-a11))]',\n // inset ring: the list is a scroll container, so an outside ring would\n // be clipped at the list edge — inset keeps it fully visible (Radix idiom)\n 'focus-visible:ring-2 focus-visible:outline-hidden focus-visible:ring-inset',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n // underline (Figma 20-419): bold 16/24 label, 48px min height; hover fill\n // + rounding live on the INNER label box (Radix anatomy), so the trigger\n // itself carries only layout. The active bar lives on the list (it slides).\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n // each tab owns its bottom rail (neutral, 2px) — no continuous track. The\n // sliding accent bar (4px) rides on top of the active tab's rail.\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n // pill: compact segmented thumb — neutral card raised off the track; the\n // thumb IS the full trigger, so its hover fill stays full-size (unlike\n // underline, where the hover is an inset label box per the Radix ref).\n // Radius is concentric with the track: rounded-md (track) − p-1 ≈ rounded-sm.\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabs.cjs","names":["React","TabsPrimitive","MaterialSymbol"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-overflow-end/tabs:flex'\n : 'left-0 group-data-overflow-start/tabs:flex',\n )}\n >\n <MaterialSymbol name={dir === 1 ? 'chevron_right' : 'chevron_left'} />\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn('group/tabs relative inline-block max-w-full', className)}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n resolved === 'underline' && 'gap-2',\n resolved === 'underline' &&\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-[left,width] after:duration-200 after:ease-out after:content-[''] motion-reduce:after:transition-none\",\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n 'group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n 'data-active:text-(--spark-tabs-ink,var(--spark-a11))',\n 'focus-visible:outline-hidden',\n 'group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n 'group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
|