@epilot/spark-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/components/accordion.cjs +3 -6
  2. package/dist/components/accordion.cjs.map +1 -1
  3. package/dist/components/accordion.d.ts +1 -1
  4. package/dist/components/accordion.js +3 -6
  5. package/dist/components/accordion.js.map +1 -1
  6. package/dist/components/alert-dialog.cjs +4 -5
  7. package/dist/components/alert-dialog.cjs.map +1 -1
  8. package/dist/components/alert-dialog.js +4 -5
  9. package/dist/components/alert-dialog.js.map +1 -1
  10. package/dist/components/autocomplete.cjs +161 -0
  11. package/dist/components/autocomplete.cjs.map +1 -0
  12. package/dist/components/autocomplete.d.cts +76 -0
  13. package/dist/components/autocomplete.d.ts +76 -0
  14. package/dist/components/autocomplete.js +143 -0
  15. package/dist/components/autocomplete.js.map +1 -0
  16. package/dist/components/avatar.js +1 -1
  17. package/dist/components/badge.js +1 -1
  18. package/dist/components/bar-chart.js +1 -1
  19. package/dist/components/battery-progress-row.cjs +14 -11
  20. package/dist/components/battery-progress-row.cjs.map +1 -1
  21. package/dist/components/battery-progress-row.d.cts +2 -10
  22. package/dist/components/battery-progress-row.d.ts +2 -10
  23. package/dist/components/battery-progress-row.js +14 -11
  24. package/dist/components/battery-progress-row.js.map +1 -1
  25. package/dist/components/box.js +1 -1
  26. package/dist/components/bubble.d.cts +1 -1
  27. package/dist/components/bubble.d.ts +1 -1
  28. package/dist/components/button-group.js +1 -1
  29. package/dist/components/button.cjs +6 -6
  30. package/dist/components/button.cjs.map +1 -1
  31. package/dist/components/button.d.cts +2 -2
  32. package/dist/components/button.d.ts +2 -2
  33. package/dist/components/button.js +7 -7
  34. package/dist/components/button.js.map +1 -1
  35. package/dist/components/calendar.cjs +4 -5
  36. package/dist/components/calendar.cjs.map +1 -1
  37. package/dist/components/calendar.js +4 -5
  38. package/dist/components/calendar.js.map +1 -1
  39. package/dist/components/callout.cjs +18 -14
  40. package/dist/components/callout.cjs.map +1 -1
  41. package/dist/components/callout.d.cts +10 -4
  42. package/dist/components/callout.d.ts +10 -4
  43. package/dist/components/callout.js +18 -15
  44. package/dist/components/callout.js.map +1 -1
  45. package/dist/components/card.js +1 -1
  46. package/dist/components/checkbox.js +1 -1
  47. package/dist/components/device-card.cjs +2 -6
  48. package/dist/components/device-card.cjs.map +1 -1
  49. package/dist/components/device-card.d.cts +3 -3
  50. package/dist/components/device-card.d.ts +3 -3
  51. package/dist/components/device-card.js +2 -6
  52. package/dist/components/device-card.js.map +1 -1
  53. package/dist/components/dialog.cjs +2 -5
  54. package/dist/components/dialog.cjs.map +1 -1
  55. package/dist/components/dialog.js +2 -5
  56. package/dist/components/dialog.js.map +1 -1
  57. package/dist/components/dropdown-menu.cjs +4 -13
  58. package/dist/components/dropdown-menu.cjs.map +1 -1
  59. package/dist/components/dropdown-menu.js +4 -13
  60. package/dist/components/dropdown-menu.js.map +1 -1
  61. package/dist/components/empty-state.cjs +5 -5
  62. package/dist/components/empty-state.cjs.map +1 -1
  63. package/dist/components/empty-state.d.cts +3 -3
  64. package/dist/components/empty-state.d.ts +3 -3
  65. package/dist/components/empty-state.js +5 -5
  66. package/dist/components/empty-state.js.map +1 -1
  67. package/dist/components/icon.cjs +2 -3
  68. package/dist/components/icon.cjs.map +1 -1
  69. package/dist/components/icon.d.cts +0 -3
  70. package/dist/components/icon.d.ts +0 -3
  71. package/dist/components/icon.js +3 -4
  72. package/dist/components/icon.js.map +1 -1
  73. package/dist/components/info-block.d.cts +1 -1
  74. package/dist/components/info-block.d.ts +1 -1
  75. package/dist/components/input-otp.cjs +1 -1
  76. package/dist/components/input-otp.js +1 -1
  77. package/dist/components/input.cjs +1 -1
  78. package/dist/components/input.js +1 -1
  79. package/dist/components/line-chart.cjs +1 -1
  80. package/dist/components/line-chart.js +2 -2
  81. package/dist/components/list.cjs.map +1 -1
  82. package/dist/components/list.js.map +1 -1
  83. package/dist/components/marker.d.cts +1 -1
  84. package/dist/components/marker.d.ts +1 -1
  85. package/dist/components/message-scroller.cjs +3 -3
  86. package/dist/components/message-scroller.cjs.map +1 -1
  87. package/dist/components/message-scroller.js +4 -4
  88. package/dist/components/message-scroller.js.map +1 -1
  89. package/dist/components/metering-row.cjs +3 -3
  90. package/dist/components/metering-row.cjs.map +1 -1
  91. package/dist/components/metering-row.d.cts +3 -3
  92. package/dist/components/metering-row.d.ts +3 -3
  93. package/dist/components/metering-row.js +3 -3
  94. package/dist/components/metering-row.js.map +1 -1
  95. package/dist/components/money-saved-block.cjs +2 -5
  96. package/dist/components/money-saved-block.cjs.map +1 -1
  97. package/dist/components/money-saved-block.js +2 -5
  98. package/dist/components/money-saved-block.js.map +1 -1
  99. package/dist/components/on-off-button.cjs +2 -5
  100. package/dist/components/on-off-button.cjs.map +1 -1
  101. package/dist/components/on-off-button.js +2 -5
  102. package/dist/components/on-off-button.js.map +1 -1
  103. package/dist/components/page-header.cjs +2 -2
  104. package/dist/components/page-header.cjs.map +1 -1
  105. package/dist/components/page-header.d.cts +1 -1
  106. package/dist/components/page-header.d.ts +1 -1
  107. package/dist/components/page-header.js +2 -2
  108. package/dist/components/page-header.js.map +1 -1
  109. package/dist/components/period-tabs.cjs +3 -6
  110. package/dist/components/period-tabs.cjs.map +1 -1
  111. package/dist/components/period-tabs.js +3 -6
  112. package/dist/components/period-tabs.js.map +1 -1
  113. package/dist/components/pill.cjs +2 -5
  114. package/dist/components/pill.cjs.map +1 -1
  115. package/dist/components/pill.js +3 -6
  116. package/dist/components/pill.js.map +1 -1
  117. package/dist/components/power-history-chart.cjs +1 -1
  118. package/dist/components/power-history-chart.js +2 -2
  119. package/dist/components/power-history.cjs +1 -1
  120. package/dist/components/power-history.js +1 -1
  121. package/dist/components/power-network.cjs +3 -6
  122. package/dist/components/power-network.cjs.map +1 -1
  123. package/dist/components/power-network.js +4 -7
  124. package/dist/components/power-network.js.map +1 -1
  125. package/dist/components/radio.js +1 -1
  126. package/dist/components/sankey-chart.cjs +56 -71
  127. package/dist/components/sankey-chart.cjs.map +1 -1
  128. package/dist/components/sankey-chart.js +56 -71
  129. package/dist/components/sankey-chart.js.map +1 -1
  130. package/dist/components/schedule-list.cjs +3 -9
  131. package/dist/components/schedule-list.cjs.map +1 -1
  132. package/dist/components/schedule-list.js +3 -9
  133. package/dist/components/schedule-list.js.map +1 -1
  134. package/dist/components/select.cjs +6 -18
  135. package/dist/components/select.cjs.map +1 -1
  136. package/dist/components/select.js +6 -18
  137. package/dist/components/select.js.map +1 -1
  138. package/dist/components/sheet.cjs +2 -5
  139. package/dist/components/sheet.cjs.map +1 -1
  140. package/dist/components/sheet.js +2 -5
  141. package/dist/components/sheet.js.map +1 -1
  142. package/dist/components/slider.cjs +9 -3
  143. package/dist/components/slider.cjs.map +1 -1
  144. package/dist/components/slider.js +9 -3
  145. package/dist/components/slider.js.map +1 -1
  146. package/dist/components/smart-toggle-button.cjs +2 -5
  147. package/dist/components/smart-toggle-button.cjs.map +1 -1
  148. package/dist/components/smart-toggle-button.js +2 -5
  149. package/dist/components/smart-toggle-button.js.map +1 -1
  150. package/dist/components/solar-forecast-chart.cjs +8 -9
  151. package/dist/components/solar-forecast-chart.cjs.map +1 -1
  152. package/dist/components/solar-forecast-chart.d.cts +2 -2
  153. package/dist/components/solar-forecast-chart.d.ts +2 -2
  154. package/dist/components/solar-forecast-chart.js +8 -9
  155. package/dist/components/solar-forecast-chart.js.map +1 -1
  156. package/dist/components/stepper.cjs +3 -9
  157. package/dist/components/stepper.cjs.map +1 -1
  158. package/dist/components/stepper.js +3 -9
  159. package/dist/components/stepper.js.map +1 -1
  160. package/dist/components/switch.js +1 -1
  161. package/dist/components/tabs.cjs +2 -2
  162. package/dist/components/tabs.cjs.map +1 -1
  163. package/dist/components/tabs.js +2 -2
  164. package/dist/components/tabs.js.map +1 -1
  165. package/dist/components/textarea.cjs +1 -1
  166. package/dist/components/textarea.js +1 -1
  167. package/dist/components/theme.js +1 -1
  168. package/dist/components/toast.cjs +9 -12
  169. package/dist/components/toast.cjs.map +1 -1
  170. package/dist/components/toast.js +9 -12
  171. package/dist/components/toast.js.map +1 -1
  172. package/dist/components/tooltip.cjs +2 -5
  173. package/dist/components/tooltip.cjs.map +1 -1
  174. package/dist/components/tooltip.js +2 -5
  175. package/dist/components/tooltip.js.map +1 -1
  176. package/dist/styles/index.css +0 -2
  177. package/package.json +3 -10
  178. package/dist/icons/index.cjs +0 -7
  179. package/dist/icons/index.d.cts +0 -4
  180. package/dist/icons/index.d.ts +0 -4
  181. package/dist/icons/index.js +0 -3
  182. package/dist/icons/material-symbol.cjs +0 -106
  183. package/dist/icons/material-symbol.cjs.map +0 -1
  184. package/dist/icons/material-symbol.d.cts +0 -21
  185. package/dist/icons/material-symbol.d.ts +0 -21
  186. package/dist/icons/material-symbol.js +0 -102
  187. package/dist/icons/material-symbol.js.map +0 -1
  188. package/dist/styles/material-symbol.css +0 -25
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.cjs","names":["React","TabsPrimitive","MaterialSymbol"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n /**\n * Stretch the strip to its container's full width and divide it evenly\n * among the triggers, instead of shrinking to fit their labels. Each\n * trigger's `--tabs-trigger-minw` floor still applies — once there's no\n * longer room to keep every trigger at least that wide, the strip hands\n * over to the scroller exactly like the non-`fill` case.\n */\n fill?: boolean\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n fill,\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n const readyFrame = requestAnimationFrame(() =>\n scroller.setAttribute('data-indicator-ready', ''),\n )\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n cancelAnimationFrame(readyFrame)\n scroller.removeAttribute('data-indicator-ready')\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-overflow-end/tabs:flex'\n : 'left-0 group-data-overflow-start/tabs:flex',\n )}\n >\n <MaterialSymbol name={dir === 1 ? 'chevron_right' : 'chevron_left'} />\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn(\n 'group/tabs relative max-w-full',\n fill ? 'block w-full' : 'inline-block',\n className,\n )}\n data-fill={fill ? true : undefined}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n resolved === 'underline' && 'gap-2',\n resolved === 'underline' &&\n // transition only once data-indicator-ready is set (a frame after the\n // first measure) so the bar appears in place instead of sweeping in\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-none after:duration-200 after:ease-out after:content-[''] data-indicator-ready:after:transition-[left,width] motion-reduce:after:transition-none!\",\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n 'group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n // fill: divide the strip evenly instead of sizing to each label — the\n // min-w floor above still applies, so a strip too narrow to keep every\n // trigger at least that wide still hands over to the scroller.\n 'group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0',\n 'data-active:text-(--spark-tabs-ink,var(--spark-a11))',\n 'focus-visible:outline-hidden',\n 'group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n 'group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
1
+ {"version":3,"file":"tabs.cjs","names":["React","TabsPrimitive","ChevronRight","ChevronLeft"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport { ChevronLeft, ChevronRight } from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n /**\n * Stretch the strip to its container's full width and divide it evenly\n * among the triggers, instead of shrinking to fit their labels. Each\n * trigger's `--tabs-trigger-minw` floor still applies — once there's no\n * longer room to keep every trigger at least that wide, the strip hands\n * over to the scroller exactly like the non-`fill` case.\n */\n fill?: boolean\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n fill,\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n const readyFrame = requestAnimationFrame(() =>\n scroller.setAttribute('data-indicator-ready', ''),\n )\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n cancelAnimationFrame(readyFrame)\n scroller.removeAttribute('data-indicator-ready')\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-overflow-end/tabs:flex'\n : 'left-0 group-data-overflow-start/tabs:flex',\n )}\n >\n {dir === 1 ? <ChevronRight /> : <ChevronLeft />}\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn(\n 'group/tabs relative max-w-full',\n fill ? 'block w-full' : 'inline-block',\n className,\n )}\n data-fill={fill ? true : undefined}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n resolved === 'underline' && 'gap-2',\n resolved === 'underline' &&\n // transition only once data-indicator-ready is set (a frame after the\n // first measure) so the bar appears in place instead of sweeping in\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-none after:duration-200 after:ease-out after:content-[''] data-indicator-ready:after:transition-[left,width] motion-reduce:after:transition-none!\",\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n 'group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n // fill: divide the strip evenly instead of sizing to each label — the\n // min-w floor above still applies, so a strip too narrow to keep every\n // trigger at least that wide still hands over to the scroller.\n 'group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0',\n 'data-active:text-(--spark-tabs-ink,var(--spark-a11))',\n 'focus-visible:outline-hidden',\n 'group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n 'group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../utils/cn.js";
3
- import { MaterialSymbol } from "../icons/material-symbol.js";
4
3
  import * as React$1 from "react";
4
+ import { ChevronLeft, ChevronRight } from "@epilot/spark-icons";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { Tabs as Tabs$1 } from "@base-ui/react/tabs";
7
7
  //#region src/components/tabs.tsx
@@ -121,7 +121,7 @@ function TabsList({ className, style, variant = "underline", fill, ref, ...props
121
121
  tabIndex: -1,
122
122
  type: "button",
123
123
  className: cn("absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12", dir === 1 ? "right-0 group-data-overflow-end/tabs:flex" : "left-0 group-data-overflow-start/tabs:flex"),
124
- children: /* @__PURE__ */ jsx(MaterialSymbol, { name: dir === 1 ? "chevron_right" : "chevron_left" })
124
+ children: dir === 1 ? /* @__PURE__ */ jsx(ChevronRight, {}) : /* @__PURE__ */ jsx(ChevronLeft, {})
125
125
  });
126
126
  return /* @__PURE__ */ jsxs("div", {
127
127
  className: cn("group/tabs relative max-w-full", fill ? "block w-full" : "inline-block", className),
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","names":["React","TabsPrimitive"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport * as React from 'react'\nimport { MaterialSymbol } from '../icons/material-symbol'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n /**\n * Stretch the strip to its container's full width and divide it evenly\n * among the triggers, instead of shrinking to fit their labels. Each\n * trigger's `--tabs-trigger-minw` floor still applies — once there's no\n * longer room to keep every trigger at least that wide, the strip hands\n * over to the scroller exactly like the non-`fill` case.\n */\n fill?: boolean\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n fill,\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n const readyFrame = requestAnimationFrame(() =>\n scroller.setAttribute('data-indicator-ready', ''),\n )\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n cancelAnimationFrame(readyFrame)\n scroller.removeAttribute('data-indicator-ready')\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-overflow-end/tabs:flex'\n : 'left-0 group-data-overflow-start/tabs:flex',\n )}\n >\n <MaterialSymbol name={dir === 1 ? 'chevron_right' : 'chevron_left'} />\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn(\n 'group/tabs relative max-w-full',\n fill ? 'block w-full' : 'inline-block',\n className,\n )}\n data-fill={fill ? true : undefined}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n resolved === 'underline' && 'gap-2',\n resolved === 'underline' &&\n // transition only once data-indicator-ready is set (a frame after the\n // first measure) so the bar appears in place instead of sweeping in\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-none after:duration-200 after:ease-out after:content-[''] data-indicator-ready:after:transition-[left,width] motion-reduce:after:transition-none!\",\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n 'group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n // fill: divide the strip evenly instead of sizing to each label — the\n // min-w floor above still applies, so a strip too narrow to keep every\n // trigger at least that wide still hands over to the scroller.\n 'group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0',\n 'data-active:text-(--spark-tabs-ink,var(--spark-a11))',\n 'focus-visible:outline-hidden',\n 'group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n 'group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
1
+ {"version":3,"file":"tabs.js","names":["React","TabsPrimitive"],"sources":["../../src/components/tabs.tsx"],"sourcesContent":["import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'\nimport { ChevronLeft, ChevronRight } from '@epilot/spark-icons'\nimport * as React from 'react'\nimport { cn } from '../utils/cn'\n\n// useLayoutEffect warns during SSR; the isomorphic swap is the standard Radix idiom.\nconst useIsomorphicLayoutEffect =\n typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect\n\n// Merge our internal scroller ref with a consumer-forwarded ref. Written locally\n// and React 19-correct: it propagates cleanup-returning callback refs and nulls\n// plain refs on unmount.\ntype PossibleRef<T> = React.Ref<T> | undefined\n\nfunction setRef<T>(ref: PossibleRef<T>, value: T | null) {\n if (typeof ref === 'function') return ref(value)\n if (ref) ref.current = value\n return undefined\n}\n\nfunction composeRefs<T>(...refs: PossibleRef<T>[]) {\n return (node: T) => {\n let hasCleanup = false\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node)\n if (typeof cleanup === 'function') hasCleanup = true\n return cleanup\n })\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < refs.length; i++) {\n const cleanup = cleanups[i]\n if (typeof cleanup === 'function') cleanup()\n else setRef(refs[i], null)\n }\n }\n }\n return undefined\n }\n}\n\ntype TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>\n\nfunction Tabs(props: TabsProps) {\n return <TabsPrimitive.Root data-slot=\"tabs\" {...props} />\n}\n\ntype TabsListVariant = 'underline'\n\n/**\n * @deprecated Not part of Spark's tab design — use `'underline'`. `'pill'` (a\n * segmented control) and the old text-only `'default'` still render for\n * back-compat but are undocumented and may be removed.\n */\ntype TabsListVariantLegacy = 'default' | 'pill'\n\ntype TabsListProps = Omit<\n React.ComponentProps<typeof TabsPrimitive.List>,\n 'style'\n> & {\n /** `underline` — the Spark tab: bold label + a sliding accent bar. */\n variant?: TabsListVariant | TabsListVariantLegacy\n /**\n * Stretch the strip to its container's full width and divide it evenly\n * among the triggers, instead of shrinking to fit their labels. Each\n * trigger's `--tabs-trigger-minw` floor still applies — once there's no\n * longer room to keep every trigger at least that wide, the strip hands\n * over to the scroller exactly like the non-`fill` case.\n */\n fill?: boolean\n // style lands on the plain wrapper div, so no Base UI state-callback form\n style?: React.CSSProperties\n}\n\n// how much of a neighboring tab a scroll-into-view reveals (matches scroll-mx-10)\nconst REVEAL_PEEK = 40\n// worm indicator: the accent bar stretches across old+new, then contracts to the\n// new tab. WORM_MS must be >= the bar's CSS transition (200ms) so the stretch fully\n// settles before the contract begins — then contract sets left+width from rest in\n// one synced transition, pinning the leading edge so the motion never jiggles (a\n// contract that starts mid-stretch desyncs the two edges → wobble on big jumps).\nconst WORM_MS = 210\n\nfunction TabsList({\n className,\n style,\n variant = 'underline',\n fill,\n ref,\n ...props\n}: TabsListProps) {\n // 'underline' is the only real variant. Deprecated 'pill' still renders (a\n // segmented control, kept for back-compat); anything else (incl. the old\n // text-only 'default') folds into underline.\n const resolved = variant === 'pill' ? 'pill' : 'underline'\n const wrapperRef = React.useRef<HTMLDivElement | null>(null)\n const scrollerRef = React.useRef<HTMLDivElement | null>(null)\n // stable identity (keyed on the forwarded ref) so React doesn't detach/re-attach\n // the scroller ref on every render — TabsList re-renders on each selection.\n const scrollerCallbackRef = React.useMemo(\n () => composeRefs<HTMLDivElement>(scrollerRef, ref),\n [ref],\n )\n\n // Sliding underline + overflow state + travel-into-view. The bar lives on the\n // SCROLLER (::after scrolls with the content) and follows the active trigger via\n // two measured vars; useLayoutEffect measures before paint (no flash); the\n // MutationObserver tracks selection + children + text changes; the ResizeObserver\n // re-measures on container/scroller reflow. Together they cover everything that\n // moves a tab: selection, add/remove, a counter Badge growing, and resize.\n useIsomorphicLayoutEffect(() => {\n const wrapper = wrapperRef.current\n const scroller = scrollerRef.current\n if (!wrapper || !scroller) return\n // the edge chevrons show per side while more content hides in that direction\n const syncOverflow = () => {\n // 2px epsilon absorbs sub-pixel rounding at fractional browser zoom\n // (scrollWidth/clientWidth round to int, scrollLeft is fractional). Assumes\n // LTR (scrollLeft >= 0) — Spark's tab strips are LTR.\n const max = scroller.scrollWidth - scroller.clientWidth\n wrapper.toggleAttribute('data-overflow-start', scroller.scrollLeft > 2)\n wrapper.toggleAttribute(\n 'data-overflow-end',\n scroller.scrollLeft < max - 2,\n )\n }\n // scroller-local reveal — deliberately NOT scrollIntoView, which also scrolls\n // ancestors and would yank the page when the list is half off-screen. The peek\n // keeps the neighboring tab partially visible (\"there's more here\").\n const reveal = (el: HTMLElement) => {\n // jsdom (unit tests) doesn't implement Element.scrollTo\n if (typeof scroller.scrollTo !== 'function') return\n const start = el.offsetLeft - REVEAL_PEEK\n const end = el.offsetLeft + el.offsetWidth + REVEAL_PEEK\n if (start < scroller.scrollLeft) {\n scroller.scrollTo({ left: Math.max(0, start) })\n } else if (end > scroller.scrollLeft + scroller.clientWidth) {\n scroller.scrollTo({ left: end - scroller.clientWidth })\n }\n }\n // matchMedia guarded for jsdom (unit tests); read .matches live so toggling the\n // OS reduce-motion setting mid-session takes effect on the next selection.\n const motionQuery =\n typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-reduced-motion: reduce)')\n : null\n const setBar = (x: number, w: number) => {\n scroller.style.setProperty('--tabs-indicator-x', `${x}px`)\n scroller.style.setProperty('--tabs-indicator-w', `${w}px`)\n }\n let lastActive: HTMLElement | null = null\n let mounted = false\n let wormTimer: ReturnType<typeof setTimeout> | undefined\n const measure = () => {\n const active = scroller.querySelector<HTMLElement>(\n '[data-slot=\"tabs-trigger\"][data-active]',\n )\n const changed = active !== lastActive\n if (resolved === 'underline') {\n if (!active) {\n // no selection → collapse the bar (width 0), forget the previous tab, and\n // cancel any pending contract, so a later selection never worms from a\n // stale/removed node\n clearTimeout(wormTimer)\n lastActive = null\n scroller.style.setProperty('--tabs-indicator-w', '0px')\n } else if (\n changed &&\n lastActive?.isConnected &&\n mounted &&\n !motionQuery?.matches\n ) {\n // worm: stretch across old + new (one edge holds, the other reaches),\n // then contract to the new tab (the trailing edge catches up). The union\n // makes it direction-agnostic. isConnected guards dynamic tab removal — a\n // detached old node reports offsets of 0 and would sweep the bar from x=0.\n const from = Math.min(lastActive.offsetLeft, active.offsetLeft)\n const to = Math.max(\n lastActive.offsetLeft + lastActive.offsetWidth,\n active.offsetLeft + active.offsetWidth,\n )\n setBar(from, to - from)\n clearTimeout(wormTimer)\n const el = active\n wormTimer = setTimeout(\n () => setBar(el.offsetLeft, el.offsetWidth),\n WORM_MS,\n )\n } else {\n // first paint, reduced motion, detached previous, or a resize (same tab)\n // → settle on the active tab, no worm\n clearTimeout(wormTimer)\n setBar(active.offsetLeft, active.offsetWidth)\n }\n }\n syncOverflow()\n // travel the active tab into view on selection change — and on mount, so a\n // defaulted off-screen tab is visible. reveal() is scroller-local, so it never\n // scrolls the page/ancestors.\n if (active && changed) {\n reveal(active)\n lastActive = active\n }\n mounted = true\n }\n measure()\n const readyFrame = requestAnimationFrame(() =>\n scroller.setAttribute('data-indicator-ready', ''),\n )\n scroller.addEventListener('scroll', syncOverflow, { passive: true })\n const mo = new MutationObserver(measure)\n mo.observe(scroller, {\n subtree: true,\n childList: true,\n // characterData catches a trigger's text growing (e.g. a counter Badge 9→120):\n // in overflow the scroller's border-box is pinned, so that only changes\n // scrollWidth (never reported by the ResizeObserver) yet shifts every tab.\n characterData: true,\n attributeFilter: ['data-active'],\n })\n const ro = new ResizeObserver(measure)\n ro.observe(scroller)\n return () => {\n cancelAnimationFrame(readyFrame)\n scroller.removeAttribute('data-indicator-ready')\n scroller.removeEventListener('scroll', syncOverflow)\n clearTimeout(wormTimer)\n mo.disconnect()\n ro.disconnect()\n }\n }, [resolved])\n\n const scrollByPage = (dir: 1 | -1) => () => {\n const scroller = scrollerRef.current\n if (typeof scroller?.scrollBy !== 'function') return\n // behavior omitted → follows the scroller's CSS scroll-behavior (smooth,\n // motion-reduce → auto)\n scroller.scrollBy({ left: dir * scroller.clientWidth * 0.6 })\n }\n const chevron = (dir: 1 | -1) => (\n <button\n aria-hidden=\"true\"\n data-slot={dir === 1 ? 'tabs-scroll-next' : 'tabs-scroll-prev'}\n onClick={scrollByPage(dir)}\n tabIndex={-1}\n type=\"button\"\n className={cn(\n 'absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12',\n dir === 1\n ? 'right-0 group-data-overflow-end/tabs:flex'\n : 'left-0 group-data-overflow-start/tabs:flex',\n )}\n >\n {dir === 1 ? <ChevronRight /> : <ChevronLeft />}\n </button>\n )\n\n return (\n // the wrapper is the public box (data-slot tabs-list, consumer className +\n // style) — group/tabs + data-variant here drive the trigger's group-data-[…]/tabs\n // styling, and data-overflow-start/end (set above) show/hide the chevrons.\n // className + style both land here so a consumer's layout (margin/width) moves\n // the whole control, not just the inner scroller (which would offset the chevrons).\n <div\n className={cn(\n 'group/tabs relative max-w-full',\n fill ? 'block w-full' : 'inline-block',\n className,\n )}\n data-fill={fill ? true : undefined}\n data-slot=\"tabs-list\"\n data-variant={resolved}\n ref={wrapperRef}\n style={style}\n >\n {chevron(-1)}\n <TabsPrimitive.List\n data-slot=\"tabs-scroller\"\n ref={scrollerCallbackRef}\n className={cn(\n 'relative no-scrollbar flex w-full items-stretch overflow-x-auto scroll-smooth motion-reduce:scroll-auto',\n resolved === 'underline' && 'gap-2',\n resolved === 'underline' &&\n // transition only once data-indicator-ready is set (a frame after the\n // first measure) so the bar appears in place instead of sweeping in\n \"after:pointer-events-none after:absolute after:bottom-0 after:left-(--tabs-indicator-x,0px) after:h-1 after:w-(--tabs-indicator-w,0px) after:rounded-full after:bg-(--spark-tabs-indicator,var(--spark-indicator)) after:transition-none after:duration-200 after:ease-out after:content-[''] data-indicator-ready:after:transition-[left,width] motion-reduce:after:transition-none!\",\n resolved === 'pill' && 'gap-0 rounded-md bg-gray-a3 p-1',\n )}\n {...props}\n />\n {chevron(1)}\n </div>\n )\n}\n\ntype TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Tab>\n\nfunction TabsTrigger({ className, children, ...props }: TabsTriggerProps) {\n return (\n <TabsPrimitive.Tab\n data-slot=\"tabs-trigger\"\n className={cn(\n 'group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors',\n // fill: divide the strip evenly instead of sizing to each label — the\n // min-w floor above still applies, so a strip too narrow to keep every\n // trigger at least that wide still hands over to the scroller.\n 'group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0',\n 'data-active:text-(--spark-tabs-ink,var(--spark-a11))',\n 'focus-visible:outline-hidden',\n 'group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1',\n 'data-disabled:pointer-events-none data-disabled:text-gray-a8',\n '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',\n 'group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]',\n 'group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5',\n 'group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium',\n 'group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3',\n 'group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6',\n className,\n )}\n {...props}\n >\n {/* the Radix two-box anatomy: the hover paints this compact label box (with\n clearance above the bar/track), not the full 48px hit target */}\n <span\n data-slot=\"tabs-trigger-inner\"\n className={cn(\n 'inline-flex items-center gap-1',\n 'group-data-[variant=underline]/tabs:rounded-md group-data-[variant=underline]/tabs:px-2 group-data-[variant=underline]/tabs:py-1 group-data-[variant=underline]/tabs:transition-colors',\n 'group-data-[variant=underline]/tabs:group-hover/trigger:bg-gray-a3',\n 'group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-2 group-data-[variant=underline]/tabs:group-focus-visible/trigger:ring-offset-1',\n )}\n >\n {children}\n </span>\n </TabsPrimitive.Tab>\n )\n}\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerUnderline = TabsTrigger\n\n/** @deprecated Use `TabsTrigger` — it adapts to the `TabsList` variant automatically. */\nconst TabsTriggerPill = TabsTrigger\n\ntype TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Panel>\n\nfunction TabsContent({ className, ...props }: TabsContentProps) {\n return (\n <TabsPrimitive.Panel\n data-slot=\"tabs-content\"\n className={cn(\n 'focus-visible:ring-2 focus-visible:ring-offset-0 focus-visible:outline-hidden',\n className,\n )}\n {...props}\n />\n )\n}\n\nexport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n TabsTriggerPill,\n TabsTriggerUnderline,\n}\nexport type { TabsListProps, TabsListVariant, TabsProps, TabsTriggerProps }\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_chunk = require("../chunk-DY84GLCh.cjs");
4
- const require_components_field = require("./field.cjs");
5
4
  const require_lib_input_box_variants = require("../lib/input-box-variants.cjs");
5
+ const require_components_field = require("./field.cjs");
6
6
  let react = require("react");
7
7
  react = require_chunk.__toESM(react);
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { cn } from "../utils/cn.js";
3
- import { useFieldControl } from "./field.js";
4
3
  import { GHOST_UNDERLINE, boxChrome } from "../lib/input-box-variants.js";
4
+ import { useFieldControl } from "./field.js";
5
5
  import * as React$1 from "react";
6
6
  import { jsx } from "react/jsx-runtime";
7
7
  import { cva } from "class-variance-authority";
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  "use client";
3
- import { cn } from "../utils/cn.js";
4
3
  import { isSparkNeutralFamily } from "../lib/theme-types.js";
5
4
  import { CURATED_ACCENTS, accentSparkVars, appearanceForBackground, generateAccentScale, getMatchingNeutral, invertAccentScale, isParsableColor, neutralFamilyVars, neutralSparkVars, resolveAutoNeutral } from "../lib/accent-theme.js";
5
+ import { cn } from "../utils/cn.js";
6
6
  import * as React$1 from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { mergeProps } from "@base-ui/react/merge-props";
@@ -2,30 +2,30 @@
2
2
  "use client";
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const require_chunk = require("../chunk-DY84GLCh.cjs");
5
- const require_icons_material_symbol = require("../icons/material-symbol.cjs");
6
5
  const require_components_button = require("./button.cjs");
7
6
  const require_components_spinner = require("./spinner.cjs");
8
7
  let react = require("react");
9
8
  react = require_chunk.__toESM(react);
9
+ let _epilot_spark_icons = require("@epilot/spark-icons");
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  let _base_ui_react_toast = require("@base-ui/react/toast");
12
12
  let cnfast = require("cnfast");
13
13
  //#region src/components/toast.tsx
14
14
  const TOAST_ICONS = {
15
15
  success: {
16
- name: "check_circle",
16
+ icon: _epilot_spark_icons.CheckCircle,
17
17
  className: "text-success-light"
18
18
  },
19
19
  info: {
20
- name: "info",
20
+ icon: _epilot_spark_icons.Info,
21
21
  className: "text-info-light"
22
22
  },
23
23
  warning: {
24
- name: "warning",
24
+ icon: _epilot_spark_icons.Warning,
25
25
  className: "text-warning-light"
26
26
  },
27
27
  error: {
28
- name: "cancel",
28
+ icon: _epilot_spark_icons.Cancel,
29
29
  className: "text-error-light"
30
30
  }
31
31
  };
@@ -99,6 +99,7 @@ const toast = Object.assign(emit(), {
99
99
  function ToastIcon({ type }) {
100
100
  const icon = TOAST_ICONS[type];
101
101
  if (type !== "loading" && !icon) return null;
102
+ const Glyph = icon?.icon;
102
103
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
103
104
  "aria-hidden": "true",
104
105
  className: "flex h-5 w-5 shrink-0 items-center justify-center",
@@ -107,10 +108,7 @@ function ToastIcon({ type }) {
107
108
  label: "",
108
109
  role: "presentation",
109
110
  size: "sm"
110
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
111
- className: (0, cnfast.cn)("[--spark-icon-size:20px]", icon.className),
112
- name: icon.name
113
- })
111
+ }) : Glyph ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Glyph, { className: (0, cnfast.cn)("[--spark-icon-size:20px]", icon.className) }) : null
114
112
  });
115
113
  }
116
114
  function ToastItem({ closeButton, richColors, toast: toastObject }) {
@@ -151,10 +149,9 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
151
149
  variant: "tertiary"
152
150
  }), "-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11"),
153
151
  "data-slot": "toast-close",
154
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
152
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_epilot_spark_icons.Close, {
155
153
  "aria-hidden": "true",
156
- className: "[--spark-icon-size:18px]",
157
- name: "close"
154
+ className: "[--spark-icon-size:18px]"
158
155
  })
159
156
  }) : null
160
157
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"toast.cjs","names":["ToastPrimitive","Spinner","MaterialSymbol","buttonVariants"],"sources":["../../src/components/toast.tsx"],"sourcesContent":["'use client'\n\nimport { Toast as ToastPrimitive } from '@base-ui/react/toast'\nimport * as React from 'react'\nimport {\n MaterialSymbol,\n type MaterialSymbolIconName,\n} from '../icons/material-symbol'\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 { name: MaterialSymbolIconName; className: string }\n> = {\n success: { name: 'check_circle', className: 'text-success-light' },\n info: { name: 'info', className: 'text-info-light' },\n warning: { name: 'warning', className: 'text-warning-light' },\n error: { name: '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 // 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 ) : (\n <MaterialSymbol\n className={cn('[--spark-icon-size:20px]', icon.className)}\n name={icon.name}\n />\n )}\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 <MaterialSymbol\n aria-hidden=\"true\"\n className=\"[--spark-icon-size:18px]\"\n name=\"close\"\n />\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.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":"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"}
@@ -1,28 +1,28 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import { cn } from "../utils/cn.js";
4
- import { MaterialSymbol } from "../icons/material-symbol.js";
5
4
  import { buttonVariants } from "./button.js";
6
5
  import { Spinner } from "./spinner.js";
7
6
  import "react";
7
+ import { Cancel, CheckCircle, Close, Info, Warning } from "@epilot/spark-icons";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Toast } from "@base-ui/react/toast";
10
10
  //#region src/components/toast.tsx
11
11
  const TOAST_ICONS = {
12
12
  success: {
13
- name: "check_circle",
13
+ icon: CheckCircle,
14
14
  className: "text-success-light"
15
15
  },
16
16
  info: {
17
- name: "info",
17
+ icon: Info,
18
18
  className: "text-info-light"
19
19
  },
20
20
  warning: {
21
- name: "warning",
21
+ icon: Warning,
22
22
  className: "text-warning-light"
23
23
  },
24
24
  error: {
25
- name: "cancel",
25
+ icon: Cancel,
26
26
  className: "text-error-light"
27
27
  }
28
28
  };
@@ -96,6 +96,7 @@ const toast = Object.assign(emit(), {
96
96
  function ToastIcon({ type }) {
97
97
  const icon = TOAST_ICONS[type];
98
98
  if (type !== "loading" && !icon) return null;
99
+ const Glyph = icon?.icon;
99
100
  return /* @__PURE__ */ jsx("span", {
100
101
  "aria-hidden": "true",
101
102
  className: "flex h-5 w-5 shrink-0 items-center justify-center",
@@ -104,10 +105,7 @@ function ToastIcon({ type }) {
104
105
  label: "",
105
106
  role: "presentation",
106
107
  size: "sm"
107
- }) : /* @__PURE__ */ jsx(MaterialSymbol, {
108
- className: cn("[--spark-icon-size:20px]", icon.className),
109
- name: icon.name
110
- })
108
+ }) : Glyph ? /* @__PURE__ */ jsx(Glyph, { className: cn("[--spark-icon-size:20px]", icon.className) }) : null
111
109
  });
112
110
  }
113
111
  function ToastItem({ closeButton, richColors, toast: toastObject }) {
@@ -148,10 +146,9 @@ function ToastItem({ closeButton, richColors, toast: toastObject }) {
148
146
  variant: "tertiary"
149
147
  }), "-my-1 -mr-1 size-7 shrink-0 self-center text-gray-11"),
150
148
  "data-slot": "toast-close",
151
- children: /* @__PURE__ */ jsx(MaterialSymbol, {
149
+ children: /* @__PURE__ */ jsx(Close, {
152
150
  "aria-hidden": "true",
153
- className: "[--spark-icon-size:18px]",
154
- name: "close"
151
+ className: "[--spark-icon-size:18px]"
155
152
  })
156
153
  }) : null
157
154
  ]
@@ -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 * as React from 'react'\nimport {\n MaterialSymbol,\n type MaterialSymbolIconName,\n} from '../icons/material-symbol'\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 { name: MaterialSymbolIconName; className: string }\n> = {\n success: { name: 'check_circle', className: 'text-success-light' },\n info: { name: 'info', className: 'text-info-light' },\n warning: { name: 'warning', className: 'text-warning-light' },\n error: { name: '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 // 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 ) : (\n <MaterialSymbol\n className={cn('[--spark-icon-size:20px]', icon.className)}\n name={icon.name}\n />\n )}\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 <MaterialSymbol\n aria-hidden=\"true\"\n className=\"[--spark-icon-size:18px]\"\n name=\"close\"\n />\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":";;;;;;;;;;AAsDA,MAAM,cAGF;CACF,SAAS;EAAE,MAAM;EAAgB,WAAW;CAAqB;CACjE,MAAM;EAAE,MAAM;EAAQ,WAAW;CAAkB;CACnD,SAAS;EAAE,MAAM;EAAW,WAAW;CAAqB;CAC5D,OAAO;EAAE,MAAM;EAAU,WAAW;CAAmB;AACzD;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,UAAUA,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;CAIxC,OACE,oBAAC,QAAD;EACE,eAAY;EACZ,WAAU;YAET,SAAS,YAGR,oBAAC,SAAD;GACE,WAAU;GACV,OAAM;GACN,MAAK;GACL,MAAK;EACN,CAAA,IAED,oBAAC,gBAAD;GACE,WAAW,GAAG,4BAA4B,KAAK,SAAS;GACxD,MAAM,KAAK;EACZ,CAAA;CAEC,CAAA;AAEV;AAEA,SAAS,UAAU,EACjB,aACA,YACA,OAAO,eAKN;CACD,MAAM,OAAO,aAAa,YAAY;CACtC,MAAM,YAAY,YAAY,MAAM,eAAe;CAEnD,OACE,qBAACA,MAAe,MAAhB;EACE,aAAU;EACV,WAAW,GAMT,yGACA,cAAc,IAChB;EAGA,gBAAgB;GAAC;GAAM;GAAQ;EAAO;EACtC,OAAO;YAdT;GAgBE,oBAAC,WAAD,EAAW,MAAM,YAAY,KAAO,CAAA;GACpC,qBAACA,MAAe,SAAhB;IAAwB,WAAU;cAAlC,CACE,oBAACA,MAAe,OAAhB;KACE,WAAU;KACV,aAAU;IACX,CAAA,GACD,oBAACA,MAAe,aAAhB;KACE,WAAW,GACT,WACA,cAAc,OAAO,iBAAiB,cACxC;KACA,aAAU;IACX,CAAA,CACqB;;GACvB,YAAY,cACX,oBAACA,MAAe,QAAhB;IAIE,WAAW,GACT,eAAe;KAAE,MAAM;KAAS,SAAS;IAAU,CAAC,GACpD,yCACF;IACA,aAAU;GACX,CAAA,IACC;GACH,YACC,oBAACA,MAAe,OAAhB;IACE,cAAW;IACX,WAAW,GACT,eAAe;KAAE,MAAM;KAAc,SAAS;IAAW,CAAC,GAE1D,sDACF;IACA,aAAU;cAEV,oBAAC,gBAAD;KACE,eAAY;KACZ,WAAU;KACV,MAAK;IACN,CAAA;GACmB,CAAA,IACpB;EACe;;AAEzB;AAEA,SAAS,UAAU,EACjB,aACA,cAIC;CACD,MAAM,EAAE,WAAWA,MAAe,gBAA2B;CAE7D,OAAO,OAAO,KAAK,gBACjB,oBAAC,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,oBAACA,MAAe,UAAhB;EACS;EACP,SAAS;EACT,cAAc;YAEd,oBAACA,MAAe,QAAhB,EAAA,UACE,oBAACA,MAAe,UAAhB;GAGE,WAAW,GACT,+CACA,mBAAmB,QAAQ,UAAU,YACrC,uDACA,iBAAiB,WACjB,SACF;GACA,oBAAkB;GAClB,aAAW,SAAS,WAAW,KAAK,IAAI,QAAQ;GAChD,aAAU;aAEV,oBAAC,WAAD;IAAwB;IAAyB;GAAa,CAAA;EACvC,CAAA,EACJ,CAAA;CACA,CAAA;AAE7B"}
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,9 +1,9 @@
1
1
  "use client";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_chunk = require("../chunk-DY84GLCh.cjs");
4
- const require_icons_material_symbol = require("../icons/material-symbol.cjs");
5
4
  let react = require("react");
6
5
  react = require_chunk.__toESM(react);
6
+ let _epilot_spark_icons = require("@epilot/spark-icons");
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let _base_ui_react_tooltip = require("@base-ui/react/tooltip");
9
9
  let cnfast = require("cnfast");
@@ -65,10 +65,7 @@ const InfoTooltip = ({ content, label, "aria-label": ariaLabel }) => {
65
65
  className: "focus-visible:ring-primary inline-flex size-6 min-h-[24px] min-w-[24px] shrink-0 cursor-help items-center justify-center rounded-full p-0 text-gray-light transition-colors hover:text-gray focus-visible:ring-2 focus-visible:outline-none",
66
66
  type: "button"
67
67
  }),
68
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_icons_material_symbol.MaterialSymbol, {
69
- className: "text-sm!",
70
- name: "info"
71
- })
68
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_epilot_spark_icons.Info, { className: "text-sm!" })
72
69
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipContent, {
73
70
  className: "max-w-xs whitespace-pre-line",
74
71
  children: content