@epilot/spark-ui 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/components/bar-chart.cjs.map +1 -1
  2. package/dist/components/bar-chart.js.map +1 -1
  3. package/dist/components/bubble.d.cts +1 -1
  4. package/dist/components/bubble.d.ts +1 -1
  5. package/dist/components/button.d.cts +3 -3
  6. package/dist/components/button.d.ts +3 -3
  7. package/dist/components/callout.d.cts +1 -1
  8. package/dist/components/callout.d.ts +1 -1
  9. package/dist/components/donut-chart.cjs.map +1 -1
  10. package/dist/components/donut-chart.js.map +1 -1
  11. package/dist/components/field.cjs +1 -1
  12. package/dist/components/field.cjs.map +1 -1
  13. package/dist/components/field.js +1 -1
  14. package/dist/components/field.js.map +1 -1
  15. package/dist/components/icon.cjs +4 -4
  16. package/dist/components/icon.cjs.map +1 -1
  17. package/dist/components/icon.js +4 -4
  18. package/dist/components/icon.js.map +1 -1
  19. package/dist/components/input-date.cjs +2 -1
  20. package/dist/components/input-date.cjs.map +1 -1
  21. package/dist/components/input-date.d.cts +3 -1
  22. package/dist/components/input-date.d.ts +3 -1
  23. package/dist/components/input-date.js +2 -1
  24. package/dist/components/input-date.js.map +1 -1
  25. package/dist/components/input-group.d.cts +1 -1
  26. package/dist/components/input-group.d.ts +1 -1
  27. package/dist/components/line-chart.cjs +1 -1
  28. package/dist/components/line-chart.cjs.map +1 -1
  29. package/dist/components/line-chart.js +1 -1
  30. package/dist/components/line-chart.js.map +1 -1
  31. package/dist/components/power-history-chart.cjs +1 -1
  32. package/dist/components/power-history-chart.cjs.map +1 -1
  33. package/dist/components/power-history-chart.js +1 -1
  34. package/dist/components/power-history-chart.js.map +1 -1
  35. package/dist/components/sankey-chart.cjs +2 -2
  36. package/dist/components/sankey-chart.cjs.map +1 -1
  37. package/dist/components/sankey-chart.js +2 -2
  38. package/dist/components/sankey-chart.js.map +1 -1
  39. package/dist/components/slider.cjs.map +1 -1
  40. package/dist/components/slider.js.map +1 -1
  41. package/dist/components/toast.cjs +32 -7
  42. package/dist/components/toast.cjs.map +1 -1
  43. package/dist/components/toast.d.cts +15 -2
  44. package/dist/components/toast.d.ts +15 -2
  45. package/dist/components/toast.js +32 -7
  46. package/dist/components/toast.js.map +1 -1
  47. package/dist/lib/generate-radix-colors.cjs.map +1 -1
  48. package/dist/lib/generate-radix-colors.js.map +1 -1
  49. 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\nfunction matchValueShape(\n next: number | readonly number[],\n current: SliderValue,\n): SliderValue {\n const nextValues = typeof next === 'number' ? [next] : [...next]\n if (Array.isArray(current)) {\n return nextValues\n }\n return nextValues[0] ?? current\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 const shaped = matchValueShape(next, current) as Value\n\n if (!isControlled) {\n setUncontrolled(shaped as SliderValue)\n }\n\n onValueChange?.(shaped, 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?.(\n matchValueShape(next, current) as Value,\n eventDetails,\n )\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 'group/slider touch-target 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 // ease only while focused (keyboard/rail-click); programmatic and\n // initial value sets snap instantly instead of sweeping from 0.\n // transition-none base matters: bare duration-150 leaves property=all\n 'transition-none duration-150 group-focus-within/slider:transition-[width,height,inset-inline-start] 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-shadow duration-150 group-focus-within/slider:transition-[inset-inline-start,top,box-shadow] 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":"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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\nfunction matchValueShape(\n next: number | readonly number[],\n current: SliderValue,\n): SliderValue {\n const nextValues = typeof next === 'number' ? [next] : [...next]\n if (Array.isArray(current)) {\n return nextValues\n }\n return nextValues[0] ?? current\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 const shaped = matchValueShape(next, current) as Value\n\n if (!isControlled) {\n setUncontrolled(shaped as SliderValue)\n }\n\n onValueChange?.(shaped, 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?.(\n matchValueShape(next, current) as Value,\n eventDetails,\n )\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 // oxlint-disable-next-line react/no-array-index-key -- Base UI identifies thumbs by their index\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 'group/slider touch-target 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 // ease only while focused (keyboard/rail-click); programmatic and\n // initial value sets snap instantly instead of sweeping from 0.\n // transition-none base matters: bare duration-150 leaves property=all\n 'transition-none duration-150 group-focus-within/slider:transition-[width,height,inset-inline-start] 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-shadow duration-150 group-focus-within/slider:transition-[inset-inline-start,top,box-shadow] 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":"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"}
@@ -66,7 +66,12 @@ function toManagerOptions(id, title, type, options) {
66
66
  manager.close(id);
67
67
  }
68
68
  },
69
- data: { closeButton: options?.closeButton }
69
+ data: {
70
+ closeButton: options?.closeButton,
71
+ closeLabel: options?.closeLabel,
72
+ icon: options?.icon,
73
+ onCloseClick: options?.onCloseClick
74
+ }
70
75
  };
71
76
  }
72
77
  const emit = (type) => (message, options) => {
@@ -96,13 +101,23 @@ const toast = Object.assign(emit(), {
96
101
  error: phase(messages.error, "error")
97
102
  })
98
103
  });
99
- function ToastIcon({ type }) {
104
+ function ToastIcon({ custom, type }) {
100
105
  const icon = TOAST_ICONS[type];
106
+ if (custom !== void 0) {
107
+ if (custom === null || custom === false) return null;
108
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
109
+ "aria-hidden": "true",
110
+ className: "flex h-5 w-5 shrink-0 items-center justify-center [--spark-icon-size:20px]",
111
+ "data-slot": "toast-icon",
112
+ children: custom
113
+ });
114
+ }
101
115
  if (type !== "loading" && !icon) return null;
102
116
  const Glyph = icon?.icon;
103
117
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
104
118
  "aria-hidden": "true",
105
119
  className: "flex h-5 w-5 shrink-0 items-center justify-center",
120
+ "data-slot": "toast-icon",
106
121
  children: type === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_spinner.Spinner, {
107
122
  className: "text-gray-11",
108
123
  label: "",
@@ -111,9 +126,10 @@ function ToastIcon({ type }) {
111
126
  }) : Glyph ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { className: (0, cn.cn)("[--spark-icon-size:20px]", icon.className) }) : null
112
127
  });
113
128
  }
114
- function ToastItem({ closeButton, richColors, toast: toastObject }) {
129
+ function ToastItem({ closeButton, closeLabel, richColors, toast: toastObject }) {
115
130
  const tone = TONE_CLASSES[toastObject.type];
116
131
  const showClose = toastObject.data?.closeButton ?? closeButton;
132
+ const onCloseClick = toastObject.data?.onCloseClick;
117
133
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_base_ui_react_toast.Toast.Root, {
118
134
  "data-slot": "toast",
119
135
  className: (0, cn.cn)("flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none", richColors && tone),
@@ -124,7 +140,10 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
124
140
  ],
125
141
  toast: toastObject,
126
142
  children: [
127
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastIcon, { type: toastObject.type }),
143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastIcon, {
144
+ custom: toastObject.data?.icon,
145
+ type: toastObject.type
146
+ }),
128
147
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_base_ui_react_toast.Toast.Content, {
129
148
  className: "flex min-w-0 flex-1 flex-col gap-0.5",
130
149
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_toast.Toast.Title, {
@@ -143,12 +162,16 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
143
162
  "data-slot": "toast-action"
144
163
  }) : null,
145
164
  showClose ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_toast.Toast.Close, {
146
- "aria-label": "Close",
165
+ "aria-label": toastObject.data?.closeLabel ?? closeLabel,
147
166
  className: (0, cn.cn)(require_components_button.buttonVariants({
148
167
  size: "icon-small",
149
168
  variant: "tertiary"
150
169
  }), "-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11"),
151
170
  "data-slot": "toast-close",
171
+ onClick: onCloseClick && ((event) => {
172
+ onCloseClick(event);
173
+ if (event.defaultPrevented) event.preventBaseUIHandler();
174
+ }),
152
175
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_epilot_spark_icons.Close, {
153
176
  "aria-hidden": "true",
154
177
  className: "[--spark-icon-size:18px]"
@@ -157,15 +180,16 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
157
180
  ]
158
181
  });
159
182
  }
160
- function ToastList({ closeButton, richColors }) {
183
+ function ToastList({ closeButton, closeLabel, richColors }) {
161
184
  const { toasts } = _base_ui_react_toast.Toast.useToastManager();
162
185
  return toasts.map((toastObject) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastItem, {
163
186
  closeButton,
187
+ closeLabel,
164
188
  richColors,
165
189
  toast: toastObject
166
190
  }, toastObject.id));
167
191
  }
168
- function Toaster({ className, closeButton = true, duration = 4e3, limit, mobilePosition = "top", position = "bottom-right", richColors = true }) {
192
+ function Toaster({ className, closeButton = true, closeLabel = "Close", duration = 4e3, limit, mobilePosition = "top", position = "bottom-right", richColors = true }) {
169
193
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_base_ui_react_toast.Toast.Provider, {
170
194
  limit,
171
195
  timeout: duration,
@@ -177,6 +201,7 @@ function Toaster({ className, closeButton = true, duration = 4e3, limit, mobileP
177
201
  "data-slot": "toaster",
178
202
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastList, {
179
203
  closeButton,
204
+ closeLabel,
180
205
  richColors
181
206
  })
182
207
  }) })
@@ -1 +1 @@
1
- {"version":3,"file":"toast.cjs","names":["CheckCircle","Info","Warning","Cancel","ToastPrimitive","Spinner","buttonVariants","Close"],"sources":["../../src/components/toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toast as ToastPrimitive } from '@base-ui/react/toast'\nimport {\n Cancel,\n CheckCircle,\n Close,\n Info,\n Warning,\n type SparkIcon,\n} from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { buttonVariants } from './button'\nimport { Spinner } from './spinner'\n\ntype ToastType = 'success' | 'info' | 'warning' | 'error' | 'loading'\n\ntype ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right'\n\n// Per-toast overrides Base UI carries through untouched as opaque `data`.\ntype ToastData = { closeButton?: boolean }\n\n// `label` is the button's content; everything else a `<button>` takes passes\n// straight through, so a caller can set `className`, `disabled`, `type`, and\n// the rest without a new prop for each one.\ntype ToastAction = Omit<\n React.ComponentPropsWithoutRef<'button'>,\n 'children'\n> & {\n label: React.ReactNode\n}\n\ntype ToastOptions = {\n description?: React.ReactNode\n /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */\n duration?: number\n /** Reuse an id to update a toast in place instead of stacking a new one. */\n id?: string\n closeButton?: boolean\n action?: ToastAction\n onDismiss?: () => void\n /** `high` announces urgently (assertive); the default is polite. */\n priority?: 'low' | 'high'\n}\n\ntype ToastPromiseMessages<Value> = {\n loading: string\n success: string | ((value: Value) => string)\n error: string | ((error: unknown) => string)\n}\n\nconst TOAST_ICONS: Record<\n Exclude<ToastType, 'loading'>,\n { icon: SparkIcon; className: string }\n> = {\n success: { icon: CheckCircle, className: 'text-success-light' },\n info: { icon: Info, className: 'text-info-light' },\n warning: { icon: Warning, className: 'text-warning-light' },\n error: { icon: Cancel, className: 'text-error-light' },\n}\n\n// Opaque radix steps (bg 2, border 6, text 11) so a floating toast never lets\n// page content bleed through the way the alpha `-soft` tokens would. The border\n// carries the tone too, otherwise a tinted toast keeps a grey outline.\nconst TONE_CLASSES: Record<Exclude<ToastType, 'loading'>, string> = {\n success: 'border-green-6 bg-green-2 text-green-11',\n info: 'border-blue-6 bg-blue-2 text-blue-11',\n warning: 'border-orange-6 bg-orange-2 text-orange-11',\n error: 'border-red-6 bg-red-2 text-red-11',\n}\n\n// Mobile-first: every position drops to a full-width stack at the top, which\n// is what the end-customer portal ships. `position` only takes effect from the\n// `sm` breakpoint up.\nconst POSITION_CLASSES: Record<ToastPosition, string> = {\n 'top-left': 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-4',\n 'top-center':\n 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-1/2 sm:-translate-x-1/2',\n 'top-right': 'sm:top-4 sm:right-4 sm:bottom-auto sm:left-auto',\n 'bottom-left': 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-4',\n 'bottom-center':\n 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-1/2 sm:-translate-x-1/2',\n 'bottom-right': 'sm:top-auto sm:right-4 sm:bottom-4 sm:left-auto',\n}\n\nconst manager = ToastPrimitive.createToastManager<ToastData>()\n\nlet sequence = 0\n\n// `label` names the content and `onClick` is wrapped, so neither reaches the\n// DOM button as-is; everything else does.\nfunction actionRest({\n label: _label,\n onClick: _onClick,\n ...rest\n}: ToastAction) {\n return rest\n}\n\nfunction toManagerOptions(\n id: string,\n title: React.ReactNode,\n type: ToastType | undefined,\n options: ToastOptions | undefined,\n) {\n const action = options?.action\n return {\n id,\n title,\n type,\n description: options?.description,\n timeout: options?.duration,\n priority: options?.priority,\n onClose: options?.onDismiss,\n // Base UI leaves the toast open after an action; dismissing matches what\n // callers expect from an \"Undo\" button.\n actionProps: action && {\n ...actionRest(action),\n children: action.label,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n action.onClick?.(event)\n manager.close(id)\n },\n },\n data: { closeButton: options?.closeButton },\n }\n}\n\nconst emit =\n (type?: ToastType) =>\n (message: React.ReactNode, options?: ToastOptions): string => {\n // an id up front lets the action handler close the toast it belongs to\n const id = options?.id ?? `spark-toast-${(sequence += 1)}`\n return manager.add(toManagerOptions(id, message, type, options))\n }\n\n// A string phase becomes a titled toast of that type; an object phase keeps\n// whatever the caller set and only fills in the type.\nconst phase =\n <Value,>(\n value: ToastPromiseMessages<Value>[keyof ToastPromiseMessages<Value>],\n type: ToastType,\n ) =>\n (result: Value) => {\n const resolved = typeof value === 'function' ? value(result) : value\n return { title: resolved, type }\n }\n\nconst toast = Object.assign(emit(), {\n success: emit('success'),\n info: emit('info'),\n warning: emit('warning'),\n error: emit('error'),\n loading: emit('loading'),\n /** Dismiss one toast, or every toast when called without an id. */\n dismiss: (id?: string) => manager.close(id),\n promise: <Value,>(\n promiseValue: Promise<Value>,\n messages: ToastPromiseMessages<Value>,\n ) =>\n manager.promise(promiseValue, {\n loading: { title: messages.loading, type: 'loading' as const },\n success: phase(messages.success, 'success'),\n error: phase(messages.error as never, 'error'),\n }),\n})\n\nfunction ToastIcon({ type }: { type?: string }) {\n const icon = TOAST_ICONS[type as Exclude<ToastType, 'loading'>]\n if (type !== 'loading' && !icon) return null\n\n const Glyph = icon?.icon\n\n // Fixed box matching the title's line height, so the glyph and the spinner\n // both centre on the first line whether or not a description follows.\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center\"\n >\n {type === 'loading' ? (\n // Decorative: the toast already announces the loading message, so the\n // spinner must not add a second status live region.\n <Spinner\n className=\"text-gray-11\"\n label=\"\"\n role=\"presentation\"\n size=\"sm\"\n />\n ) : Glyph ? (\n <Glyph className={cn('[--spark-icon-size:20px]', icon.className)} />\n ) : null}\n </span>\n )\n}\n\nfunction ToastItem({\n closeButton,\n richColors,\n toast: toastObject,\n}: {\n closeButton: boolean\n richColors: boolean\n toast: ToastPrimitive.Root.ToastObject<ToastData>\n}) {\n const tone = TONE_CLASSES[toastObject.type as Exclude<ToastType, 'loading'>]\n const showClose = toastObject.data?.closeButton ?? closeButton\n\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(\n // Skin only. All geometry (position, stacking, transitions, the\n // pointer bridge) lives in styles/index.css, because Tailwind's\n // `utilities` layer outranks `components` and a stray `bottom-0` here\n // would beat the stylesheet's `bottom: auto` on top-anchored stacks.\n // The border is what makes the cards behind the front one legible.\n 'flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none',\n richColors && tone,\n )}\n // the stack is top-anchored on mobile and bottom-anchored above `sm`, so\n // both vertical directions have to dismiss\n swipeDirection={['up', 'down', 'right']}\n toast={toastObject}\n >\n <ToastIcon type={toastObject.type} />\n <ToastPrimitive.Content className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <ToastPrimitive.Title\n className=\"text-sm font-medium\"\n data-slot=\"toast-title\"\n />\n <ToastPrimitive.Description\n className={cn(\n 'text-sm',\n richColors && tone ? 'text-inherit' : 'text-gray-11',\n )}\n data-slot=\"toast-description\"\n />\n </ToastPrimitive.Content>\n {toastObject.actionProps ? (\n <ToastPrimitive.Action\n // -my-2 keeps the 36px control from stretching a single-line row past\n // the 20px text; self-center then lands it on the title when there is\n // no description, and mid-block when there is one\n className={cn(\n buttonVariants({ size: 'small', variant: 'primary' }),\n '-my-2 shrink-0 self-center rounded-full',\n )}\n data-slot=\"toast-action\"\n />\n ) : null}\n {showClose ? (\n <ToastPrimitive.Close\n aria-label=\"Close\"\n className={cn(\n buttonVariants({ size: 'icon-small', variant: 'tertiary' }),\n // -my-1: same reason as the action, for a 28px control\n '-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11',\n )}\n data-slot=\"toast-close\"\n >\n <Close aria-hidden=\"true\" className=\"[--spark-icon-size:18px]\" />\n </ToastPrimitive.Close>\n ) : null}\n </ToastPrimitive.Root>\n )\n}\n\nfunction ToastList({\n closeButton,\n richColors,\n}: {\n closeButton: boolean\n richColors: boolean\n}) {\n const { toasts } = ToastPrimitive.useToastManager<ToastData>()\n\n return toasts.map((toastObject) => (\n <ToastItem\n closeButton={closeButton}\n key={toastObject.id}\n richColors={richColors}\n toast={toastObject}\n />\n ))\n}\n\ntype ToasterProps = {\n className?: string\n /** Corner or edge the toasts stack in. */\n position?: ToastPosition\n /** Close button on every toast; a per-toast `closeButton` wins. */\n closeButton?: boolean\n /** Tinted surfaces for success/info/warning/error toasts. */\n richColors?: boolean\n /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */\n duration?: number\n /** Toasts visible at once before older ones are marked `data-limited`. */\n limit?: number\n /**\n * Vertical edge below the `sm` breakpoint, where the stack is always\n * full-width and `position` does not apply. Defaults to `top`, matching the\n * end-customer portal.\n */\n mobilePosition?: 'top' | 'bottom'\n}\n\nfunction Toaster({\n className,\n closeButton = true,\n duration = 4000,\n limit,\n mobilePosition = 'top',\n position = 'bottom-right',\n richColors = true,\n}: ToasterProps) {\n return (\n <ToastPrimitive.Provider\n limit={limit}\n timeout={duration}\n toastManager={manager}\n >\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n // block + zero height: the toasts are absolutely positioned against\n // this box's bottom edge, the way Base UI's stacked example works\n className={cn(\n 'fixed inset-x-4 z-50 block h-0 outline-none',\n mobilePosition === 'top' ? 'top-4' : 'bottom-4',\n 'sm:inset-x-auto sm:w-96 sm:max-w-[calc(100vw-2rem)]',\n POSITION_CLASSES[position],\n className,\n )}\n data-mobile-side={mobilePosition}\n data-side={position.startsWith('top') ? 'top' : 'bottom'}\n data-slot=\"toaster\"\n >\n <ToastList closeButton={closeButton} richColors={richColors} />\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n </ToastPrimitive.Provider>\n )\n}\n\nexport { Toaster, toast }\nexport type {\n ToastAction,\n ToasterProps,\n ToastOptions,\n ToastPosition,\n ToastPromiseMessages,\n ToastType,\n}\n"],"mappings":";;;;;;;;;;;;;AA0DA,MAAM,cAGF;CACF,SAAS;EAAE,MAAMA,oBAAAA;EAAa,WAAW;CAAqB;CAC9D,MAAM;EAAE,MAAMC,oBAAAA;EAAM,WAAW;CAAkB;CACjD,SAAS;EAAE,MAAMC,oBAAAA;EAAS,WAAW;CAAqB;CAC1D,OAAO;EAAE,MAAMC,oBAAAA;EAAQ,WAAW;CAAmB;AACvD;AAKA,MAAM,eAA8D;CAClE,SAAS;CACT,MAAM;CACN,SAAS;CACT,OAAO;AACT;AAKA,MAAM,mBAAkD;CACtD,YAAY;CACZ,cACE;CACF,aAAa;CACb,eAAe;CACf,iBACE;CACF,gBAAgB;AAClB;AAEA,MAAM,UAAUC,qBAAAA,MAAe,mBAA8B;AAE7D,IAAI,WAAW;AAIf,SAAS,WAAW,EAClB,OAAO,QACP,SAAS,UACT,GAAG,QACW;CACd,OAAO;AACT;AAEA,SAAS,iBACP,IACA,OACA,MACA,SACA;CACA,MAAM,SAAS,SAAS;CACxB,OAAO;EACL;EACA;EACA;EACA,aAAa,SAAS;EACtB,SAAS,SAAS;EAClB,UAAU,SAAS;EACnB,SAAS,SAAS;EAGlB,aAAa,UAAU;GACrB,GAAG,WAAW,MAAM;GACpB,UAAU,OAAO;GACjB,UAAU,UAA+C;IACvD,OAAO,UAAU,KAAK;IACtB,QAAQ,MAAM,EAAE;GAClB;EACF;EACA,MAAM,EAAE,aAAa,SAAS,YAAY;CAC5C;AACF;AAEA,MAAM,QACH,UACA,SAA0B,YAAmC;CAE5D,MAAM,KAAK,SAAS,MAAM,eAAgB,YAAY;CACtD,OAAO,QAAQ,IAAI,iBAAiB,IAAI,SAAS,MAAM,OAAO,CAAC;AACjE;AAIF,MAAM,SAEF,OACA,UAED,WAAkB;CAEjB,OAAO;EAAE,OADQ,OAAO,UAAU,aAAa,MAAM,MAAM,IAAI;EACrC;CAAK;AACjC;AAEF,MAAM,QAAQ,OAAO,OAAO,KAAK,GAAG;CAClC,SAAS,KAAK,SAAS;CACvB,MAAM,KAAK,MAAM;CACjB,SAAS,KAAK,SAAS;CACvB,OAAO,KAAK,OAAO;CACnB,SAAS,KAAK,SAAS;;CAEvB,UAAU,OAAgB,QAAQ,MAAM,EAAE;CAC1C,UACE,cACA,aAEA,QAAQ,QAAQ,cAAc;EAC5B,SAAS;GAAE,OAAO,SAAS;GAAS,MAAM;EAAmB;EAC7D,SAAS,MAAM,SAAS,SAAS,SAAS;EAC1C,OAAO,MAAM,SAAS,OAAgB,OAAO;CAC/C,CAAC;AACL,CAAC;AAED,SAAS,UAAU,EAAE,QAA2B;CAC9C,MAAM,OAAO,YAAY;CACzB,IAAI,SAAS,aAAa,CAAC,MAAM,OAAO;CAExC,MAAM,QAAQ,MAAM;CAIpB,OACE,iBAAA,GAAA,kBAAA,KAAC,QAAD;EACE,eAAY;EACZ,WAAU;YAET,SAAS,YAGR,iBAAA,GAAA,kBAAA,KAACC,2BAAAA,SAAD;GACE,WAAU;GACV,OAAM;GACN,MAAK;GACL,MAAK;EACN,CAAA,IACC,QACF,iBAAA,GAAA,kBAAA,KAAC,OAAD,EAAO,YAAA,GAAA,GAAA,IAAc,4BAA4B,KAAK,SAAS,EAAI,CAAA,IACjE;CACA,CAAA;AAEV;AAEA,SAAS,UAAU,EACjB,aACA,YACA,OAAO,eAKN;CACD,MAAM,OAAO,aAAa,YAAY;CACtC,MAAM,YAAY,YAAY,MAAM,eAAe;CAEnD,OACE,iBAAA,GAAA,kBAAA,MAACD,qBAAAA,MAAe,MAAhB;EACE,aAAU;EACV,YAAA,GAAA,GAAA,IAME,yGACA,cAAc,IAChB;EAGA,gBAAgB;GAAC;GAAM;GAAQ;EAAO;EACtC,OAAO;YAdT;GAgBE,iBAAA,GAAA,kBAAA,KAAC,WAAD,EAAW,MAAM,YAAY,KAAO,CAAA;GACpC,iBAAA,GAAA,kBAAA,MAACA,qBAAAA,MAAe,SAAhB;IAAwB,WAAU;cAAlC,CACE,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,OAAhB;KACE,WAAU;KACV,aAAU;IACX,CAAA,GACD,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,aAAhB;KACE,YAAA,GAAA,GAAA,IACE,WACA,cAAc,OAAO,iBAAiB,cACxC;KACA,aAAU;IACX,CAAA,CACqB;;GACvB,YAAY,cACX,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,QAAhB;IAIE,YAAA,GAAA,GAAA,IACEE,0BAAAA,eAAe;KAAE,MAAM;KAAS,SAAS;IAAU,CAAC,GACpD,yCACF;IACA,aAAU;GACX,CAAA,IACC;GACH,YACC,iBAAA,GAAA,kBAAA,KAACF,qBAAAA,MAAe,OAAhB;IACE,cAAW;IACX,YAAA,GAAA,GAAA,IACEE,0BAAAA,eAAe;KAAE,MAAM;KAAc,SAAS;IAAW,CAAC,GAE1D,sDACF;IACA,aAAU;cAEV,iBAAA,GAAA,kBAAA,KAACC,oBAAAA,OAAD;KAAO,eAAY;KAAO,WAAU;IAA4B,CAAA;GAC5C,CAAA,IACpB;EACe;;AAEzB;AAEA,SAAS,UAAU,EACjB,aACA,cAIC;CACD,MAAM,EAAE,WAAWH,qBAAAA,MAAe,gBAA2B;CAE7D,OAAO,OAAO,KAAK,gBACjB,iBAAA,GAAA,kBAAA,KAAC,WAAD;EACe;EAED;EACZ,OAAO;CACR,GAHM,YAAY,EAGlB,CACF;AACH;AAsBA,SAAS,QAAQ,EACf,WACA,cAAc,MACd,WAAW,KACX,OACA,iBAAiB,OACjB,WAAW,gBACX,aAAa,QACE;CACf,OACE,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,UAAhB;EACS;EACP,SAAS;EACT,cAAc;YAEd,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,QAAhB,EAAA,UACE,iBAAA,GAAA,kBAAA,KAACA,qBAAAA,MAAe,UAAhB;GAGE,YAAA,GAAA,GAAA,IACE,+CACA,mBAAmB,QAAQ,UAAU,YACrC,uDACA,iBAAiB,WACjB,SACF;GACA,oBAAkB;GAClB,aAAW,SAAS,WAAW,KAAK,IAAI,QAAQ;GAChD,aAAU;aAEV,iBAAA,GAAA,kBAAA,KAAC,WAAD;IAAwB;IAAyB;GAAa,CAAA;EACvC,CAAA,EACJ,CAAA;CACA,CAAA;AAE7B"}
1
+ {"version":3,"file":"toast.cjs","names":["CheckCircle","Info","Warning","Cancel","ToastPrimitive","Spinner","buttonVariants","Close"],"sources":["../../src/components/toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toast as ToastPrimitive } from '@base-ui/react/toast'\nimport type { BaseUIEvent } from '@base-ui/react/types'\nimport {\n Cancel,\n CheckCircle,\n Close,\n Info,\n Warning,\n type SparkIcon,\n} from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { buttonVariants } from './button'\nimport { Spinner } from './spinner'\n\ntype ToastType = 'success' | 'info' | 'warning' | 'error' | 'loading'\n\ntype ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right'\n\n// Per-toast overrides Base UI carries through untouched as opaque `data`.\ntype ToastData = {\n closeButton?: boolean\n closeLabel?: string\n icon?: React.ReactNode\n onCloseClick?: (event: React.MouseEvent<HTMLButtonElement>) => void\n}\n\n// `label` is the button's content; everything else a `<button>` takes passes\n// straight through, so a caller can set `className`, `disabled`, `type`, and\n// the rest without a new prop for each one.\ntype ToastAction = Omit<\n React.ComponentPropsWithoutRef<'button'>,\n 'children'\n> & {\n label: React.ReactNode\n}\n\ntype ToastOptions = {\n description?: React.ReactNode\n /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */\n duration?: number\n /** Reuse an id to update a toast in place instead of stacking a new one. */\n id?: string\n closeButton?: boolean\n /** Accessible name of the close button; overrides the Toaster's `closeLabel`. */\n closeLabel?: string\n /**\n * Replaces the type icon. `null` renders no icon. The icon is decorative,\n * so the title has to carry the meaning on its own.\n */\n icon?: React.ReactNode\n /**\n * Runs when the close button is clicked. Call `event.preventDefault()` to\n * keep the toast open, for example to cancel a running download first.\n */\n onCloseClick?: (event: React.MouseEvent<HTMLButtonElement>) => void\n action?: ToastAction\n onDismiss?: () => void\n /** `high` announces urgently (assertive); the default is polite. */\n priority?: 'low' | 'high'\n}\n\ntype ToastPromiseMessages<Value> = {\n loading: string\n success: string | ((value: Value) => string)\n error: string | ((error: unknown) => string)\n}\n\nconst TOAST_ICONS: Record<\n Exclude<ToastType, 'loading'>,\n { icon: SparkIcon; className: string }\n> = {\n success: { icon: CheckCircle, className: 'text-success-light' },\n info: { icon: Info, className: 'text-info-light' },\n warning: { icon: Warning, className: 'text-warning-light' },\n error: { icon: Cancel, className: 'text-error-light' },\n}\n\n// Opaque radix steps (bg 2, border 6, text 11) so a floating toast never lets\n// page content bleed through the way the alpha `-soft` tokens would. The border\n// carries the tone too, otherwise a tinted toast keeps a grey outline.\nconst TONE_CLASSES: Record<Exclude<ToastType, 'loading'>, string> = {\n success: 'border-green-6 bg-green-2 text-green-11',\n info: 'border-blue-6 bg-blue-2 text-blue-11',\n warning: 'border-orange-6 bg-orange-2 text-orange-11',\n error: 'border-red-6 bg-red-2 text-red-11',\n}\n\n// Mobile-first: every position drops to a full-width stack at the top, which\n// is what the end-customer portal ships. `position` only takes effect from the\n// `sm` breakpoint up.\nconst POSITION_CLASSES: Record<ToastPosition, string> = {\n 'top-left': 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-4',\n 'top-center':\n 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-1/2 sm:-translate-x-1/2',\n 'top-right': 'sm:top-4 sm:right-4 sm:bottom-auto sm:left-auto',\n 'bottom-left': 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-4',\n 'bottom-center':\n 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-1/2 sm:-translate-x-1/2',\n 'bottom-right': 'sm:top-auto sm:right-4 sm:bottom-4 sm:left-auto',\n}\n\nconst manager = ToastPrimitive.createToastManager<ToastData>()\n\nlet sequence = 0\n\n// `label` names the content and `onClick` is wrapped, so neither reaches the\n// DOM button as-is; everything else does.\nfunction actionRest({\n label: _label,\n onClick: _onClick,\n ...rest\n}: ToastAction) {\n return rest\n}\n\nfunction toManagerOptions(\n id: string,\n title: React.ReactNode,\n type: ToastType | undefined,\n options: ToastOptions | undefined,\n) {\n const action = options?.action\n return {\n id,\n title,\n type,\n description: options?.description,\n timeout: options?.duration,\n priority: options?.priority,\n onClose: options?.onDismiss,\n // Base UI leaves the toast open after an action; dismissing matches what\n // callers expect from an \"Undo\" button.\n actionProps: action && {\n ...actionRest(action),\n children: action.label,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n action.onClick?.(event)\n manager.close(id)\n },\n },\n data: {\n closeButton: options?.closeButton,\n closeLabel: options?.closeLabel,\n icon: options?.icon,\n onCloseClick: options?.onCloseClick,\n },\n }\n}\n\nconst emit =\n (type?: ToastType) =>\n (message: React.ReactNode, options?: ToastOptions): string => {\n // an id up front lets the action handler close the toast it belongs to\n const id = options?.id ?? `spark-toast-${(sequence += 1)}`\n return manager.add(toManagerOptions(id, message, type, options))\n }\n\n// A string phase becomes a titled toast of that type; an object phase keeps\n// whatever the caller set and only fills in the type.\nconst phase =\n <Value,>(\n value: ToastPromiseMessages<Value>[keyof ToastPromiseMessages<Value>],\n type: ToastType,\n ) =>\n (result: Value) => {\n const resolved = typeof value === 'function' ? value(result) : value\n return { title: resolved, type }\n }\n\nconst toast = Object.assign(emit(), {\n success: emit('success'),\n info: emit('info'),\n warning: emit('warning'),\n error: emit('error'),\n loading: emit('loading'),\n /** Dismiss one toast, or every toast when called without an id. */\n dismiss: (id?: string) => manager.close(id),\n promise: <Value,>(\n promiseValue: Promise<Value>,\n messages: ToastPromiseMessages<Value>,\n ) =>\n manager.promise(promiseValue, {\n loading: { title: messages.loading, type: 'loading' as const },\n success: phase(messages.success, 'success'),\n error: phase(messages.error as never, 'error'),\n }),\n})\n\nfunction ToastIcon({\n custom,\n type,\n}: {\n custom: React.ReactNode | undefined\n type?: string\n}) {\n const icon = TOAST_ICONS[type as Exclude<ToastType, 'loading'>]\n if (custom !== undefined) {\n if (custom === null || custom === false) return null\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center [--spark-icon-size:20px]\"\n data-slot=\"toast-icon\"\n >\n {custom}\n </span>\n )\n }\n if (type !== 'loading' && !icon) return null\n\n const Glyph = icon?.icon\n\n // Fixed box matching the title's line height, so the glyph and the spinner\n // both centre on the first line whether or not a description follows.\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center\"\n data-slot=\"toast-icon\"\n >\n {type === 'loading' ? (\n // Decorative: the toast already announces the loading message, so the\n // spinner must not add a second status live region.\n <Spinner\n className=\"text-gray-11\"\n label=\"\"\n role=\"presentation\"\n size=\"sm\"\n />\n ) : Glyph ? (\n <Glyph className={cn('[--spark-icon-size:20px]', icon.className)} />\n ) : null}\n </span>\n )\n}\n\nfunction ToastItem({\n closeButton,\n closeLabel,\n richColors,\n toast: toastObject,\n}: {\n closeButton: boolean\n closeLabel: string\n richColors: boolean\n toast: ToastPrimitive.Root.ToastObject<ToastData>\n}) {\n const tone = TONE_CLASSES[toastObject.type as Exclude<ToastType, 'loading'>]\n const showClose = toastObject.data?.closeButton ?? closeButton\n const onCloseClick = toastObject.data?.onCloseClick\n\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(\n // Skin only. All geometry (position, stacking, transitions, the\n // pointer bridge) lives in styles/index.css, because Tailwind's\n // `utilities` layer outranks `components` and a stray `bottom-0` here\n // would beat the stylesheet's `bottom: auto` on top-anchored stacks.\n // The border is what makes the cards behind the front one legible.\n 'flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none',\n richColors && tone,\n )}\n // the stack is top-anchored on mobile and bottom-anchored above `sm`, so\n // both vertical directions have to dismiss\n swipeDirection={['up', 'down', 'right']}\n toast={toastObject}\n >\n <ToastIcon custom={toastObject.data?.icon} type={toastObject.type} />\n <ToastPrimitive.Content className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <ToastPrimitive.Title\n className=\"text-sm font-medium\"\n data-slot=\"toast-title\"\n />\n <ToastPrimitive.Description\n className={cn(\n 'text-sm',\n richColors && tone ? 'text-inherit' : 'text-gray-11',\n )}\n data-slot=\"toast-description\"\n />\n </ToastPrimitive.Content>\n {toastObject.actionProps ? (\n <ToastPrimitive.Action\n // -my-2 keeps the 36px control from stretching a single-line row past\n // the 20px text; self-center then lands it on the title when there is\n // no description, and mid-block when there is one\n className={cn(\n buttonVariants({ size: 'small', variant: 'primary' }),\n '-my-2 shrink-0 self-center rounded-full',\n )}\n data-slot=\"toast-action\"\n />\n ) : null}\n {showClose ? (\n <ToastPrimitive.Close\n aria-label={toastObject.data?.closeLabel ?? closeLabel}\n className={cn(\n buttonVariants({ size: 'icon-small', variant: 'tertiary' }),\n // -my-1: same reason as the action, for a 28px control\n '-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11',\n )}\n data-slot=\"toast-close\"\n onClick={\n onCloseClick &&\n ((event: BaseUIEvent<React.MouseEvent<HTMLButtonElement>>) => {\n onCloseClick(event)\n // Base UI's own close runs after this handler unless prevented.\n if (event.defaultPrevented) event.preventBaseUIHandler()\n })\n }\n >\n <Close aria-hidden=\"true\" className=\"[--spark-icon-size:18px]\" />\n </ToastPrimitive.Close>\n ) : null}\n </ToastPrimitive.Root>\n )\n}\n\nfunction ToastList({\n closeButton,\n closeLabel,\n richColors,\n}: {\n closeButton: boolean\n closeLabel: string\n richColors: boolean\n}) {\n const { toasts } = ToastPrimitive.useToastManager<ToastData>()\n\n return toasts.map((toastObject) => (\n <ToastItem\n closeButton={closeButton}\n closeLabel={closeLabel}\n key={toastObject.id}\n richColors={richColors}\n toast={toastObject}\n />\n ))\n}\n\ntype ToasterProps = {\n className?: string\n /** Corner or edge the toasts stack in. */\n position?: ToastPosition\n /** Close button on every toast; a per-toast `closeButton` wins. */\n closeButton?: boolean\n /** Accessible name of every close button; a per-toast `closeLabel` wins. */\n closeLabel?: string\n /** Tinted surfaces for success/info/warning/error toasts. */\n richColors?: boolean\n /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */\n duration?: number\n /** Toasts visible at once before older ones are marked `data-limited`. */\n limit?: number\n /**\n * Vertical edge below the `sm` breakpoint, where the stack is always\n * full-width and `position` does not apply. Defaults to `top`, matching the\n * end-customer portal.\n */\n mobilePosition?: 'top' | 'bottom'\n}\n\nfunction Toaster({\n className,\n closeButton = true,\n closeLabel = 'Close',\n duration = 4000,\n limit,\n mobilePosition = 'top',\n position = 'bottom-right',\n richColors = true,\n}: ToasterProps) {\n return (\n <ToastPrimitive.Provider\n limit={limit}\n timeout={duration}\n toastManager={manager}\n >\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n // block + zero height: the toasts are absolutely positioned against\n // this box's bottom edge, the way Base UI's stacked example works\n className={cn(\n 'fixed inset-x-4 z-50 block h-0 outline-none',\n mobilePosition === 'top' ? 'top-4' : 'bottom-4',\n 'sm:inset-x-auto sm:w-96 sm:max-w-[calc(100vw-2rem)]',\n POSITION_CLASSES[position],\n className,\n )}\n data-mobile-side={mobilePosition}\n data-side={position.startsWith('top') ? 'top' : 'bottom'}\n data-slot=\"toaster\"\n >\n <ToastList\n closeButton={closeButton}\n closeLabel={closeLabel}\n richColors={richColors}\n />\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n </ToastPrimitive.Provider>\n )\n}\n\nexport { Toaster, toast }\nexport type {\n ToastAction,\n ToasterProps,\n ToastOptions,\n ToastPosition,\n ToastPromiseMessages,\n ToastType,\n}\n"],"mappings":"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"}
@@ -12,7 +12,18 @@ type ToastOptions = {
12
12
  description?: React$1.ReactNode; /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */
13
13
  duration?: number; /** Reuse an id to update a toast in place instead of stacking a new one. */
14
14
  id?: string;
15
- closeButton?: boolean;
15
+ closeButton?: boolean; /** Accessible name of the close button; overrides the Toaster's `closeLabel`. */
16
+ closeLabel?: string;
17
+ /**
18
+ * Replaces the type icon. `null` renders no icon. The icon is decorative,
19
+ * so the title has to carry the meaning on its own.
20
+ */
21
+ icon?: React$1.ReactNode;
22
+ /**
23
+ * Runs when the close button is clicked. Call `event.preventDefault()` to
24
+ * keep the toast open, for example to cancel a running download first.
25
+ */
26
+ onCloseClick?: (event: React$1.MouseEvent<HTMLButtonElement>) => void;
16
27
  action?: ToastAction;
17
28
  onDismiss?: () => void; /** `high` announces urgently (assertive); the default is polite. */
18
29
  priority?: 'low' | 'high';
@@ -34,7 +45,8 @@ declare const toast: ((message: React$1.ReactNode, options?: ToastOptions) => st
34
45
  type ToasterProps = {
35
46
  className?: string; /** Corner or edge the toasts stack in. */
36
47
  position?: ToastPosition; /** Close button on every toast; a per-toast `closeButton` wins. */
37
- closeButton?: boolean; /** Tinted surfaces for success/info/warning/error toasts. */
48
+ closeButton?: boolean; /** Accessible name of every close button; a per-toast `closeLabel` wins. */
49
+ closeLabel?: string; /** Tinted surfaces for success/info/warning/error toasts. */
38
50
  richColors?: boolean; /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */
39
51
  duration?: number; /** Toasts visible at once before older ones are marked `data-limited`. */
40
52
  limit?: number;
@@ -48,6 +60,7 @@ type ToasterProps = {
48
60
  declare function Toaster({
49
61
  className,
50
62
  closeButton,
63
+ closeLabel,
51
64
  duration,
52
65
  limit,
53
66
  mobilePosition,
@@ -12,7 +12,18 @@ type ToastOptions = {
12
12
  description?: React$1.ReactNode; /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */
13
13
  duration?: number; /** Reuse an id to update a toast in place instead of stacking a new one. */
14
14
  id?: string;
15
- closeButton?: boolean;
15
+ closeButton?: boolean; /** Accessible name of the close button; overrides the Toaster's `closeLabel`. */
16
+ closeLabel?: string;
17
+ /**
18
+ * Replaces the type icon. `null` renders no icon. The icon is decorative,
19
+ * so the title has to carry the meaning on its own.
20
+ */
21
+ icon?: React$1.ReactNode;
22
+ /**
23
+ * Runs when the close button is clicked. Call `event.preventDefault()` to
24
+ * keep the toast open, for example to cancel a running download first.
25
+ */
26
+ onCloseClick?: (event: React$1.MouseEvent<HTMLButtonElement>) => void;
16
27
  action?: ToastAction;
17
28
  onDismiss?: () => void; /** `high` announces urgently (assertive); the default is polite. */
18
29
  priority?: 'low' | 'high';
@@ -34,7 +45,8 @@ declare const toast: ((message: React$1.ReactNode, options?: ToastOptions) => st
34
45
  type ToasterProps = {
35
46
  className?: string; /** Corner or edge the toasts stack in. */
36
47
  position?: ToastPosition; /** Close button on every toast; a per-toast `closeButton` wins. */
37
- closeButton?: boolean; /** Tinted surfaces for success/info/warning/error toasts. */
48
+ closeButton?: boolean; /** Accessible name of every close button; a per-toast `closeLabel` wins. */
49
+ closeLabel?: string; /** Tinted surfaces for success/info/warning/error toasts. */
38
50
  richColors?: boolean; /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */
39
51
  duration?: number; /** Toasts visible at once before older ones are marked `data-limited`. */
40
52
  limit?: number;
@@ -48,6 +60,7 @@ type ToasterProps = {
48
60
  declare function Toaster({
49
61
  className,
50
62
  closeButton,
63
+ closeLabel,
51
64
  duration,
52
65
  limit,
53
66
  mobilePosition,
@@ -63,7 +63,12 @@ function toManagerOptions(id, title, type, options) {
63
63
  manager.close(id);
64
64
  }
65
65
  },
66
- data: { closeButton: options?.closeButton }
66
+ data: {
67
+ closeButton: options?.closeButton,
68
+ closeLabel: options?.closeLabel,
69
+ icon: options?.icon,
70
+ onCloseClick: options?.onCloseClick
71
+ }
67
72
  };
68
73
  }
69
74
  const emit = (type) => (message, options) => {
@@ -93,13 +98,23 @@ const toast = Object.assign(emit(), {
93
98
  error: phase(messages.error, "error")
94
99
  })
95
100
  });
96
- function ToastIcon({ type }) {
101
+ function ToastIcon({ custom, type }) {
97
102
  const icon = TOAST_ICONS[type];
103
+ if (custom !== void 0) {
104
+ if (custom === null || custom === false) return null;
105
+ return /* @__PURE__ */ jsx("span", {
106
+ "aria-hidden": "true",
107
+ className: "flex h-5 w-5 shrink-0 items-center justify-center [--spark-icon-size:20px]",
108
+ "data-slot": "toast-icon",
109
+ children: custom
110
+ });
111
+ }
98
112
  if (type !== "loading" && !icon) return null;
99
113
  const Glyph = icon?.icon;
100
114
  return /* @__PURE__ */ jsx("span", {
101
115
  "aria-hidden": "true",
102
116
  className: "flex h-5 w-5 shrink-0 items-center justify-center",
117
+ "data-slot": "toast-icon",
103
118
  children: type === "loading" ? /* @__PURE__ */ jsx(Spinner, {
104
119
  className: "text-gray-11",
105
120
  label: "",
@@ -108,9 +123,10 @@ function ToastIcon({ type }) {
108
123
  }) : Glyph ? /* @__PURE__ */ jsx(Glyph, { className: cn("[--spark-icon-size:20px]", icon.className) }) : null
109
124
  });
110
125
  }
111
- function ToastItem({ closeButton, richColors, toast: toastObject }) {
126
+ function ToastItem({ closeButton, closeLabel, richColors, toast: toastObject }) {
112
127
  const tone = TONE_CLASSES[toastObject.type];
113
128
  const showClose = toastObject.data?.closeButton ?? closeButton;
129
+ const onCloseClick = toastObject.data?.onCloseClick;
114
130
  return /* @__PURE__ */ jsxs(Toast.Root, {
115
131
  "data-slot": "toast",
116
132
  className: cn("flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none", richColors && tone),
@@ -121,7 +137,10 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
121
137
  ],
122
138
  toast: toastObject,
123
139
  children: [
124
- /* @__PURE__ */ jsx(ToastIcon, { type: toastObject.type }),
140
+ /* @__PURE__ */ jsx(ToastIcon, {
141
+ custom: toastObject.data?.icon,
142
+ type: toastObject.type
143
+ }),
125
144
  /* @__PURE__ */ jsxs(Toast.Content, {
126
145
  className: "flex min-w-0 flex-1 flex-col gap-0.5",
127
146
  children: [/* @__PURE__ */ jsx(Toast.Title, {
@@ -140,12 +159,16 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
140
159
  "data-slot": "toast-action"
141
160
  }) : null,
142
161
  showClose ? /* @__PURE__ */ jsx(Toast.Close, {
143
- "aria-label": "Close",
162
+ "aria-label": toastObject.data?.closeLabel ?? closeLabel,
144
163
  className: cn(buttonVariants({
145
164
  size: "icon-small",
146
165
  variant: "tertiary"
147
166
  }), "-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11"),
148
167
  "data-slot": "toast-close",
168
+ onClick: onCloseClick && ((event) => {
169
+ onCloseClick(event);
170
+ if (event.defaultPrevented) event.preventBaseUIHandler();
171
+ }),
149
172
  children: /* @__PURE__ */ jsx(Close, {
150
173
  "aria-hidden": "true",
151
174
  className: "[--spark-icon-size:18px]"
@@ -154,15 +177,16 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
154
177
  ]
155
178
  });
156
179
  }
157
- function ToastList({ closeButton, richColors }) {
180
+ function ToastList({ closeButton, closeLabel, richColors }) {
158
181
  const { toasts } = Toast.useToastManager();
159
182
  return toasts.map((toastObject) => /* @__PURE__ */ jsx(ToastItem, {
160
183
  closeButton,
184
+ closeLabel,
161
185
  richColors,
162
186
  toast: toastObject
163
187
  }, toastObject.id));
164
188
  }
165
- function Toaster({ className, closeButton = true, duration = 4e3, limit, mobilePosition = "top", position = "bottom-right", richColors = true }) {
189
+ function Toaster({ className, closeButton = true, closeLabel = "Close", duration = 4e3, limit, mobilePosition = "top", position = "bottom-right", richColors = true }) {
166
190
  return /* @__PURE__ */ jsx(Toast.Provider, {
167
191
  limit,
168
192
  timeout: duration,
@@ -174,6 +198,7 @@ function Toaster({ className, closeButton = true, duration = 4e3, limit, mobileP
174
198
  "data-slot": "toaster",
175
199
  children: /* @__PURE__ */ jsx(ToastList, {
176
200
  closeButton,
201
+ closeLabel,
177
202
  richColors
178
203
  })
179
204
  }) })
@@ -1 +1 @@
1
- {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../../src/components/toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toast as ToastPrimitive } from '@base-ui/react/toast'\nimport {\n Cancel,\n CheckCircle,\n Close,\n Info,\n Warning,\n type SparkIcon,\n} from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { buttonVariants } from './button'\nimport { Spinner } from './spinner'\n\ntype ToastType = 'success' | 'info' | 'warning' | 'error' | 'loading'\n\ntype ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right'\n\n// Per-toast overrides Base UI carries through untouched as opaque `data`.\ntype ToastData = { closeButton?: boolean }\n\n// `label` is the button's content; everything else a `<button>` takes passes\n// straight through, so a caller can set `className`, `disabled`, `type`, and\n// the rest without a new prop for each one.\ntype ToastAction = Omit<\n React.ComponentPropsWithoutRef<'button'>,\n 'children'\n> & {\n label: React.ReactNode\n}\n\ntype ToastOptions = {\n description?: React.ReactNode\n /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */\n duration?: number\n /** Reuse an id to update a toast in place instead of stacking a new one. */\n id?: string\n closeButton?: boolean\n action?: ToastAction\n onDismiss?: () => void\n /** `high` announces urgently (assertive); the default is polite. */\n priority?: 'low' | 'high'\n}\n\ntype ToastPromiseMessages<Value> = {\n loading: string\n success: string | ((value: Value) => string)\n error: string | ((error: unknown) => string)\n}\n\nconst TOAST_ICONS: Record<\n Exclude<ToastType, 'loading'>,\n { icon: SparkIcon; className: string }\n> = {\n success: { icon: CheckCircle, className: 'text-success-light' },\n info: { icon: Info, className: 'text-info-light' },\n warning: { icon: Warning, className: 'text-warning-light' },\n error: { icon: Cancel, className: 'text-error-light' },\n}\n\n// Opaque radix steps (bg 2, border 6, text 11) so a floating toast never lets\n// page content bleed through the way the alpha `-soft` tokens would. The border\n// carries the tone too, otherwise a tinted toast keeps a grey outline.\nconst TONE_CLASSES: Record<Exclude<ToastType, 'loading'>, string> = {\n success: 'border-green-6 bg-green-2 text-green-11',\n info: 'border-blue-6 bg-blue-2 text-blue-11',\n warning: 'border-orange-6 bg-orange-2 text-orange-11',\n error: 'border-red-6 bg-red-2 text-red-11',\n}\n\n// Mobile-first: every position drops to a full-width stack at the top, which\n// is what the end-customer portal ships. `position` only takes effect from the\n// `sm` breakpoint up.\nconst POSITION_CLASSES: Record<ToastPosition, string> = {\n 'top-left': 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-4',\n 'top-center':\n 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-1/2 sm:-translate-x-1/2',\n 'top-right': 'sm:top-4 sm:right-4 sm:bottom-auto sm:left-auto',\n 'bottom-left': 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-4',\n 'bottom-center':\n 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-1/2 sm:-translate-x-1/2',\n 'bottom-right': 'sm:top-auto sm:right-4 sm:bottom-4 sm:left-auto',\n}\n\nconst manager = ToastPrimitive.createToastManager<ToastData>()\n\nlet sequence = 0\n\n// `label` names the content and `onClick` is wrapped, so neither reaches the\n// DOM button as-is; everything else does.\nfunction actionRest({\n label: _label,\n onClick: _onClick,\n ...rest\n}: ToastAction) {\n return rest\n}\n\nfunction toManagerOptions(\n id: string,\n title: React.ReactNode,\n type: ToastType | undefined,\n options: ToastOptions | undefined,\n) {\n const action = options?.action\n return {\n id,\n title,\n type,\n description: options?.description,\n timeout: options?.duration,\n priority: options?.priority,\n onClose: options?.onDismiss,\n // Base UI leaves the toast open after an action; dismissing matches what\n // callers expect from an \"Undo\" button.\n actionProps: action && {\n ...actionRest(action),\n children: action.label,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n action.onClick?.(event)\n manager.close(id)\n },\n },\n data: { closeButton: options?.closeButton },\n }\n}\n\nconst emit =\n (type?: ToastType) =>\n (message: React.ReactNode, options?: ToastOptions): string => {\n // an id up front lets the action handler close the toast it belongs to\n const id = options?.id ?? `spark-toast-${(sequence += 1)}`\n return manager.add(toManagerOptions(id, message, type, options))\n }\n\n// A string phase becomes a titled toast of that type; an object phase keeps\n// whatever the caller set and only fills in the type.\nconst phase =\n <Value,>(\n value: ToastPromiseMessages<Value>[keyof ToastPromiseMessages<Value>],\n type: ToastType,\n ) =>\n (result: Value) => {\n const resolved = typeof value === 'function' ? value(result) : value\n return { title: resolved, type }\n }\n\nconst toast = Object.assign(emit(), {\n success: emit('success'),\n info: emit('info'),\n warning: emit('warning'),\n error: emit('error'),\n loading: emit('loading'),\n /** Dismiss one toast, or every toast when called without an id. */\n dismiss: (id?: string) => manager.close(id),\n promise: <Value,>(\n promiseValue: Promise<Value>,\n messages: ToastPromiseMessages<Value>,\n ) =>\n manager.promise(promiseValue, {\n loading: { title: messages.loading, type: 'loading' as const },\n success: phase(messages.success, 'success'),\n error: phase(messages.error as never, 'error'),\n }),\n})\n\nfunction ToastIcon({ type }: { type?: string }) {\n const icon = TOAST_ICONS[type as Exclude<ToastType, 'loading'>]\n if (type !== 'loading' && !icon) return null\n\n const Glyph = icon?.icon\n\n // Fixed box matching the title's line height, so the glyph and the spinner\n // both centre on the first line whether or not a description follows.\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center\"\n >\n {type === 'loading' ? (\n // Decorative: the toast already announces the loading message, so the\n // spinner must not add a second status live region.\n <Spinner\n className=\"text-gray-11\"\n label=\"\"\n role=\"presentation\"\n size=\"sm\"\n />\n ) : Glyph ? (\n <Glyph className={cn('[--spark-icon-size:20px]', icon.className)} />\n ) : null}\n </span>\n )\n}\n\nfunction ToastItem({\n closeButton,\n richColors,\n toast: toastObject,\n}: {\n closeButton: boolean\n richColors: boolean\n toast: ToastPrimitive.Root.ToastObject<ToastData>\n}) {\n const tone = TONE_CLASSES[toastObject.type as Exclude<ToastType, 'loading'>]\n const showClose = toastObject.data?.closeButton ?? closeButton\n\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(\n // Skin only. All geometry (position, stacking, transitions, the\n // pointer bridge) lives in styles/index.css, because Tailwind's\n // `utilities` layer outranks `components` and a stray `bottom-0` here\n // would beat the stylesheet's `bottom: auto` on top-anchored stacks.\n // The border is what makes the cards behind the front one legible.\n 'flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none',\n richColors && tone,\n )}\n // the stack is top-anchored on mobile and bottom-anchored above `sm`, so\n // both vertical directions have to dismiss\n swipeDirection={['up', 'down', 'right']}\n toast={toastObject}\n >\n <ToastIcon type={toastObject.type} />\n <ToastPrimitive.Content className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <ToastPrimitive.Title\n className=\"text-sm font-medium\"\n data-slot=\"toast-title\"\n />\n <ToastPrimitive.Description\n className={cn(\n 'text-sm',\n richColors && tone ? 'text-inherit' : 'text-gray-11',\n )}\n data-slot=\"toast-description\"\n />\n </ToastPrimitive.Content>\n {toastObject.actionProps ? (\n <ToastPrimitive.Action\n // -my-2 keeps the 36px control from stretching a single-line row past\n // the 20px text; self-center then lands it on the title when there is\n // no description, and mid-block when there is one\n className={cn(\n buttonVariants({ size: 'small', variant: 'primary' }),\n '-my-2 shrink-0 self-center rounded-full',\n )}\n data-slot=\"toast-action\"\n />\n ) : null}\n {showClose ? (\n <ToastPrimitive.Close\n aria-label=\"Close\"\n className={cn(\n buttonVariants({ size: 'icon-small', variant: 'tertiary' }),\n // -my-1: same reason as the action, for a 28px control\n '-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11',\n )}\n data-slot=\"toast-close\"\n >\n <Close aria-hidden=\"true\" className=\"[--spark-icon-size:18px]\" />\n </ToastPrimitive.Close>\n ) : null}\n </ToastPrimitive.Root>\n )\n}\n\nfunction ToastList({\n closeButton,\n richColors,\n}: {\n closeButton: boolean\n richColors: boolean\n}) {\n const { toasts } = ToastPrimitive.useToastManager<ToastData>()\n\n return toasts.map((toastObject) => (\n <ToastItem\n closeButton={closeButton}\n key={toastObject.id}\n richColors={richColors}\n toast={toastObject}\n />\n ))\n}\n\ntype ToasterProps = {\n className?: string\n /** Corner or edge the toasts stack in. */\n position?: ToastPosition\n /** Close button on every toast; a per-toast `closeButton` wins. */\n closeButton?: boolean\n /** Tinted surfaces for success/info/warning/error toasts. */\n richColors?: boolean\n /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */\n duration?: number\n /** Toasts visible at once before older ones are marked `data-limited`. */\n limit?: number\n /**\n * Vertical edge below the `sm` breakpoint, where the stack is always\n * full-width and `position` does not apply. Defaults to `top`, matching the\n * end-customer portal.\n */\n mobilePosition?: 'top' | 'bottom'\n}\n\nfunction Toaster({\n className,\n closeButton = true,\n duration = 4000,\n limit,\n mobilePosition = 'top',\n position = 'bottom-right',\n richColors = true,\n}: ToasterProps) {\n return (\n <ToastPrimitive.Provider\n limit={limit}\n timeout={duration}\n toastManager={manager}\n >\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n // block + zero height: the toasts are absolutely positioned against\n // this box's bottom edge, the way Base UI's stacked example works\n className={cn(\n 'fixed inset-x-4 z-50 block h-0 outline-none',\n mobilePosition === 'top' ? 'top-4' : 'bottom-4',\n 'sm:inset-x-auto sm:w-96 sm:max-w-[calc(100vw-2rem)]',\n POSITION_CLASSES[position],\n className,\n )}\n data-mobile-side={mobilePosition}\n data-side={position.startsWith('top') ? 'top' : 'bottom'}\n data-slot=\"toaster\"\n >\n <ToastList closeButton={closeButton} richColors={richColors} />\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n </ToastPrimitive.Provider>\n )\n}\n\nexport { Toaster, toast }\nexport type {\n ToastAction,\n ToasterProps,\n ToastOptions,\n ToastPosition,\n ToastPromiseMessages,\n ToastType,\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"toast.js","names":["ToastPrimitive"],"sources":["../../src/components/toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toast as ToastPrimitive } from '@base-ui/react/toast'\nimport type { BaseUIEvent } from '@base-ui/react/types'\nimport {\n Cancel,\n CheckCircle,\n Close,\n Info,\n Warning,\n type SparkIcon,\n} from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\nimport { buttonVariants } from './button'\nimport { Spinner } from './spinner'\n\ntype ToastType = 'success' | 'info' | 'warning' | 'error' | 'loading'\n\ntype ToastPosition =\n | 'top-left'\n | 'top-center'\n | 'top-right'\n | 'bottom-left'\n | 'bottom-center'\n | 'bottom-right'\n\n// Per-toast overrides Base UI carries through untouched as opaque `data`.\ntype ToastData = {\n closeButton?: boolean\n closeLabel?: string\n icon?: React.ReactNode\n onCloseClick?: (event: React.MouseEvent<HTMLButtonElement>) => void\n}\n\n// `label` is the button's content; everything else a `<button>` takes passes\n// straight through, so a caller can set `className`, `disabled`, `type`, and\n// the rest without a new prop for each one.\ntype ToastAction = Omit<\n React.ComponentPropsWithoutRef<'button'>,\n 'children'\n> & {\n label: React.ReactNode\n}\n\ntype ToastOptions = {\n description?: React.ReactNode\n /** Auto-dismiss time in ms. `0` keeps the toast until dismissed. */\n duration?: number\n /** Reuse an id to update a toast in place instead of stacking a new one. */\n id?: string\n closeButton?: boolean\n /** Accessible name of the close button; overrides the Toaster's `closeLabel`. */\n closeLabel?: string\n /**\n * Replaces the type icon. `null` renders no icon. The icon is decorative,\n * so the title has to carry the meaning on its own.\n */\n icon?: React.ReactNode\n /**\n * Runs when the close button is clicked. Call `event.preventDefault()` to\n * keep the toast open, for example to cancel a running download first.\n */\n onCloseClick?: (event: React.MouseEvent<HTMLButtonElement>) => void\n action?: ToastAction\n onDismiss?: () => void\n /** `high` announces urgently (assertive); the default is polite. */\n priority?: 'low' | 'high'\n}\n\ntype ToastPromiseMessages<Value> = {\n loading: string\n success: string | ((value: Value) => string)\n error: string | ((error: unknown) => string)\n}\n\nconst TOAST_ICONS: Record<\n Exclude<ToastType, 'loading'>,\n { icon: SparkIcon; className: string }\n> = {\n success: { icon: CheckCircle, className: 'text-success-light' },\n info: { icon: Info, className: 'text-info-light' },\n warning: { icon: Warning, className: 'text-warning-light' },\n error: { icon: Cancel, className: 'text-error-light' },\n}\n\n// Opaque radix steps (bg 2, border 6, text 11) so a floating toast never lets\n// page content bleed through the way the alpha `-soft` tokens would. The border\n// carries the tone too, otherwise a tinted toast keeps a grey outline.\nconst TONE_CLASSES: Record<Exclude<ToastType, 'loading'>, string> = {\n success: 'border-green-6 bg-green-2 text-green-11',\n info: 'border-blue-6 bg-blue-2 text-blue-11',\n warning: 'border-orange-6 bg-orange-2 text-orange-11',\n error: 'border-red-6 bg-red-2 text-red-11',\n}\n\n// Mobile-first: every position drops to a full-width stack at the top, which\n// is what the end-customer portal ships. `position` only takes effect from the\n// `sm` breakpoint up.\nconst POSITION_CLASSES: Record<ToastPosition, string> = {\n 'top-left': 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-4',\n 'top-center':\n 'sm:top-4 sm:right-auto sm:bottom-auto sm:left-1/2 sm:-translate-x-1/2',\n 'top-right': 'sm:top-4 sm:right-4 sm:bottom-auto sm:left-auto',\n 'bottom-left': 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-4',\n 'bottom-center':\n 'sm:top-auto sm:right-auto sm:bottom-4 sm:left-1/2 sm:-translate-x-1/2',\n 'bottom-right': 'sm:top-auto sm:right-4 sm:bottom-4 sm:left-auto',\n}\n\nconst manager = ToastPrimitive.createToastManager<ToastData>()\n\nlet sequence = 0\n\n// `label` names the content and `onClick` is wrapped, so neither reaches the\n// DOM button as-is; everything else does.\nfunction actionRest({\n label: _label,\n onClick: _onClick,\n ...rest\n}: ToastAction) {\n return rest\n}\n\nfunction toManagerOptions(\n id: string,\n title: React.ReactNode,\n type: ToastType | undefined,\n options: ToastOptions | undefined,\n) {\n const action = options?.action\n return {\n id,\n title,\n type,\n description: options?.description,\n timeout: options?.duration,\n priority: options?.priority,\n onClose: options?.onDismiss,\n // Base UI leaves the toast open after an action; dismissing matches what\n // callers expect from an \"Undo\" button.\n actionProps: action && {\n ...actionRest(action),\n children: action.label,\n onClick: (event: React.MouseEvent<HTMLButtonElement>) => {\n action.onClick?.(event)\n manager.close(id)\n },\n },\n data: {\n closeButton: options?.closeButton,\n closeLabel: options?.closeLabel,\n icon: options?.icon,\n onCloseClick: options?.onCloseClick,\n },\n }\n}\n\nconst emit =\n (type?: ToastType) =>\n (message: React.ReactNode, options?: ToastOptions): string => {\n // an id up front lets the action handler close the toast it belongs to\n const id = options?.id ?? `spark-toast-${(sequence += 1)}`\n return manager.add(toManagerOptions(id, message, type, options))\n }\n\n// A string phase becomes a titled toast of that type; an object phase keeps\n// whatever the caller set and only fills in the type.\nconst phase =\n <Value,>(\n value: ToastPromiseMessages<Value>[keyof ToastPromiseMessages<Value>],\n type: ToastType,\n ) =>\n (result: Value) => {\n const resolved = typeof value === 'function' ? value(result) : value\n return { title: resolved, type }\n }\n\nconst toast = Object.assign(emit(), {\n success: emit('success'),\n info: emit('info'),\n warning: emit('warning'),\n error: emit('error'),\n loading: emit('loading'),\n /** Dismiss one toast, or every toast when called without an id. */\n dismiss: (id?: string) => manager.close(id),\n promise: <Value,>(\n promiseValue: Promise<Value>,\n messages: ToastPromiseMessages<Value>,\n ) =>\n manager.promise(promiseValue, {\n loading: { title: messages.loading, type: 'loading' as const },\n success: phase(messages.success, 'success'),\n error: phase(messages.error as never, 'error'),\n }),\n})\n\nfunction ToastIcon({\n custom,\n type,\n}: {\n custom: React.ReactNode | undefined\n type?: string\n}) {\n const icon = TOAST_ICONS[type as Exclude<ToastType, 'loading'>]\n if (custom !== undefined) {\n if (custom === null || custom === false) return null\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center [--spark-icon-size:20px]\"\n data-slot=\"toast-icon\"\n >\n {custom}\n </span>\n )\n }\n if (type !== 'loading' && !icon) return null\n\n const Glyph = icon?.icon\n\n // Fixed box matching the title's line height, so the glyph and the spinner\n // both centre on the first line whether or not a description follows.\n return (\n <span\n aria-hidden=\"true\"\n className=\"flex h-5 w-5 shrink-0 items-center justify-center\"\n data-slot=\"toast-icon\"\n >\n {type === 'loading' ? (\n // Decorative: the toast already announces the loading message, so the\n // spinner must not add a second status live region.\n <Spinner\n className=\"text-gray-11\"\n label=\"\"\n role=\"presentation\"\n size=\"sm\"\n />\n ) : Glyph ? (\n <Glyph className={cn('[--spark-icon-size:20px]', icon.className)} />\n ) : null}\n </span>\n )\n}\n\nfunction ToastItem({\n closeButton,\n closeLabel,\n richColors,\n toast: toastObject,\n}: {\n closeButton: boolean\n closeLabel: string\n richColors: boolean\n toast: ToastPrimitive.Root.ToastObject<ToastData>\n}) {\n const tone = TONE_CLASSES[toastObject.type as Exclude<ToastType, 'loading'>]\n const showClose = toastObject.data?.closeButton ?? closeButton\n const onCloseClick = toastObject.data?.onCloseClick\n\n return (\n <ToastPrimitive.Root\n data-slot=\"toast\"\n className={cn(\n // Skin only. All geometry (position, stacking, transitions, the\n // pointer bridge) lives in styles/index.css, because Tailwind's\n // `utilities` layer outranks `components` and a stray `bottom-0` here\n // would beat the stylesheet's `bottom: auto` on top-anchored stacks.\n // The border is what makes the cards behind the front one legible.\n 'flex items-start gap-2 rounded-lg border border-gray-a5 bg-gray-1 p-3 text-gray shadow-md select-none',\n richColors && tone,\n )}\n // the stack is top-anchored on mobile and bottom-anchored above `sm`, so\n // both vertical directions have to dismiss\n swipeDirection={['up', 'down', 'right']}\n toast={toastObject}\n >\n <ToastIcon custom={toastObject.data?.icon} type={toastObject.type} />\n <ToastPrimitive.Content className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n <ToastPrimitive.Title\n className=\"text-sm font-medium\"\n data-slot=\"toast-title\"\n />\n <ToastPrimitive.Description\n className={cn(\n 'text-sm',\n richColors && tone ? 'text-inherit' : 'text-gray-11',\n )}\n data-slot=\"toast-description\"\n />\n </ToastPrimitive.Content>\n {toastObject.actionProps ? (\n <ToastPrimitive.Action\n // -my-2 keeps the 36px control from stretching a single-line row past\n // the 20px text; self-center then lands it on the title when there is\n // no description, and mid-block when there is one\n className={cn(\n buttonVariants({ size: 'small', variant: 'primary' }),\n '-my-2 shrink-0 self-center rounded-full',\n )}\n data-slot=\"toast-action\"\n />\n ) : null}\n {showClose ? (\n <ToastPrimitive.Close\n aria-label={toastObject.data?.closeLabel ?? closeLabel}\n className={cn(\n buttonVariants({ size: 'icon-small', variant: 'tertiary' }),\n // -my-1: same reason as the action, for a 28px control\n '-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11',\n )}\n data-slot=\"toast-close\"\n onClick={\n onCloseClick &&\n ((event: BaseUIEvent<React.MouseEvent<HTMLButtonElement>>) => {\n onCloseClick(event)\n // Base UI's own close runs after this handler unless prevented.\n if (event.defaultPrevented) event.preventBaseUIHandler()\n })\n }\n >\n <Close aria-hidden=\"true\" className=\"[--spark-icon-size:18px]\" />\n </ToastPrimitive.Close>\n ) : null}\n </ToastPrimitive.Root>\n )\n}\n\nfunction ToastList({\n closeButton,\n closeLabel,\n richColors,\n}: {\n closeButton: boolean\n closeLabel: string\n richColors: boolean\n}) {\n const { toasts } = ToastPrimitive.useToastManager<ToastData>()\n\n return toasts.map((toastObject) => (\n <ToastItem\n closeButton={closeButton}\n closeLabel={closeLabel}\n key={toastObject.id}\n richColors={richColors}\n toast={toastObject}\n />\n ))\n}\n\ntype ToasterProps = {\n className?: string\n /** Corner or edge the toasts stack in. */\n position?: ToastPosition\n /** Close button on every toast; a per-toast `closeButton` wins. */\n closeButton?: boolean\n /** Accessible name of every close button; a per-toast `closeLabel` wins. */\n closeLabel?: string\n /** Tinted surfaces for success/info/warning/error toasts. */\n richColors?: boolean\n /** Default auto-dismiss time in ms. `0` disables auto-dismiss. */\n duration?: number\n /** Toasts visible at once before older ones are marked `data-limited`. */\n limit?: number\n /**\n * Vertical edge below the `sm` breakpoint, where the stack is always\n * full-width and `position` does not apply. Defaults to `top`, matching the\n * end-customer portal.\n */\n mobilePosition?: 'top' | 'bottom'\n}\n\nfunction Toaster({\n className,\n closeButton = true,\n closeLabel = 'Close',\n duration = 4000,\n limit,\n mobilePosition = 'top',\n position = 'bottom-right',\n richColors = true,\n}: ToasterProps) {\n return (\n <ToastPrimitive.Provider\n limit={limit}\n timeout={duration}\n toastManager={manager}\n >\n <ToastPrimitive.Portal>\n <ToastPrimitive.Viewport\n // block + zero height: the toasts are absolutely positioned against\n // this box's bottom edge, the way Base UI's stacked example works\n className={cn(\n 'fixed inset-x-4 z-50 block h-0 outline-none',\n mobilePosition === 'top' ? 'top-4' : 'bottom-4',\n 'sm:inset-x-auto sm:w-96 sm:max-w-[calc(100vw-2rem)]',\n POSITION_CLASSES[position],\n className,\n )}\n data-mobile-side={mobilePosition}\n data-side={position.startsWith('top') ? 'top' : 'bottom'}\n data-slot=\"toaster\"\n >\n <ToastList\n closeButton={closeButton}\n closeLabel={closeLabel}\n richColors={richColors}\n />\n </ToastPrimitive.Viewport>\n </ToastPrimitive.Portal>\n </ToastPrimitive.Provider>\n )\n}\n\nexport { Toaster, toast }\nexport type {\n ToastAction,\n ToasterProps,\n ToastOptions,\n ToastPosition,\n ToastPromiseMessages,\n ToastType,\n}\n"],"mappings":"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"}