@epilot/spark-ui 0.18.1 → 0.19.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.
- package/dist/components/attachment.d.cts +1 -1
- package/dist/components/attachment.d.ts +1 -1
- package/dist/components/bar-chart.cjs +1 -1
- package/dist/components/bar-chart.js +2 -2
- package/dist/components/breadcrumbs.cjs +1 -1
- package/dist/components/breadcrumbs.cjs.map +1 -1
- package/dist/components/breadcrumbs.js +1 -1
- package/dist/components/breadcrumbs.js.map +1 -1
- package/dist/components/bubble.d.cts +1 -1
- package/dist/components/bubble.d.ts +1 -1
- package/dist/components/button-group.cjs +1 -1
- package/dist/components/button-group.cjs.map +1 -1
- package/dist/components/button-group.js +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/button.d.cts +2 -2
- package/dist/components/button.d.ts +2 -2
- package/dist/components/calendar.cjs +4 -4
- package/dist/components/calendar.cjs.map +1 -1
- package/dist/components/calendar.js +4 -4
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/checkbox.cjs +1 -1
- package/dist/components/checkbox.cjs.map +1 -1
- package/dist/components/checkbox.js +1 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/day-of-week-picker.cjs +2 -2
- package/dist/components/day-of-week-picker.cjs.map +1 -1
- package/dist/components/day-of-week-picker.js +2 -2
- package/dist/components/day-of-week-picker.js.map +1 -1
- package/dist/components/dropdown-menu.cjs +8 -8
- package/dist/components/dropdown-menu.cjs.map +1 -1
- package/dist/components/dropdown-menu.js +8 -8
- package/dist/components/dropdown-menu.js.map +1 -1
- package/dist/components/field.cjs +1 -1
- package/dist/components/field.cjs.map +1 -1
- package/dist/components/field.js +1 -1
- package/dist/components/field.js.map +1 -1
- package/dist/components/input-date.cjs +6 -6
- package/dist/components/input-date.cjs.map +1 -1
- package/dist/components/input-date.d.cts +2 -1
- package/dist/components/input-date.d.ts +2 -1
- package/dist/components/input-date.js +6 -6
- package/dist/components/input-date.js.map +1 -1
- package/dist/components/input-otp.cjs +2 -2
- package/dist/components/input-otp.cjs.map +1 -1
- package/dist/components/input-otp.js +2 -2
- package/dist/components/input-otp.js.map +1 -1
- package/dist/components/input.cjs +2 -2
- package/dist/components/input.cjs.map +1 -1
- package/dist/components/input.js +2 -2
- package/dist/components/input.js.map +1 -1
- package/dist/components/label.cjs +1 -1
- package/dist/components/label.cjs.map +1 -1
- package/dist/components/label.js +1 -1
- package/dist/components/label.js.map +1 -1
- package/dist/components/line-chart.cjs +3 -3
- package/dist/components/line-chart.js +4 -4
- package/dist/components/list.cjs +2 -2
- package/dist/components/list.cjs.map +1 -1
- package/dist/components/list.js +2 -2
- package/dist/components/list.js.map +1 -1
- package/dist/components/marker.cjs +2 -2
- package/dist/components/marker.cjs.map +1 -1
- package/dist/components/marker.d.cts +1 -1
- package/dist/components/marker.d.ts +1 -1
- package/dist/components/marker.js +2 -2
- package/dist/components/marker.js.map +1 -1
- package/dist/components/message-scroller.cjs +1 -1
- package/dist/components/message-scroller.js +2 -2
- package/dist/components/on-off-button.cjs +1 -1
- package/dist/components/on-off-button.cjs.map +1 -1
- package/dist/components/on-off-button.js +1 -1
- package/dist/components/on-off-button.js.map +1 -1
- package/dist/components/page-header.d.cts +1 -1
- package/dist/components/page-header.d.ts +1 -1
- package/dist/components/power-history-chart.cjs +3 -3
- package/dist/components/power-history-chart.js +4 -4
- package/dist/components/power-history.cjs +1 -1
- package/dist/components/power-history.js +1 -1
- package/dist/components/power-network.js +1 -1
- package/dist/components/radio.cjs +1 -1
- package/dist/components/radio.cjs.map +1 -1
- package/dist/components/radio.js +1 -1
- package/dist/components/radio.js.map +1 -1
- package/dist/components/schedule-list.cjs +1 -1
- package/dist/components/schedule-list.cjs.map +1 -1
- package/dist/components/schedule-list.js +1 -1
- package/dist/components/schedule-list.js.map +1 -1
- package/dist/components/scroll-area.cjs +2 -2
- package/dist/components/scroll-area.cjs.map +1 -1
- package/dist/components/scroll-area.js +2 -2
- package/dist/components/scroll-area.js.map +1 -1
- package/dist/components/segmented-control.cjs +1 -1
- package/dist/components/segmented-control.cjs.map +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/segmented-control.js.map +1 -1
- package/dist/components/select.cjs +6 -6
- package/dist/components/select.cjs.map +1 -1
- package/dist/components/select.js +6 -6
- package/dist/components/select.js.map +1 -1
- package/dist/components/series-legend.cjs +2 -2
- package/dist/components/series-legend.cjs.map +1 -1
- package/dist/components/series-legend.js +2 -2
- package/dist/components/series-legend.js.map +1 -1
- package/dist/components/slider.cjs +1 -1
- package/dist/components/slider.cjs.map +1 -1
- package/dist/components/slider.js +1 -1
- package/dist/components/slider.js.map +1 -1
- package/dist/components/smart-toggle-button.cjs +1 -1
- package/dist/components/smart-toggle-button.cjs.map +1 -1
- package/dist/components/smart-toggle-button.js +1 -1
- package/dist/components/smart-toggle-button.js.map +1 -1
- package/dist/components/solar-forecast-chart.js +1 -1
- package/dist/components/stepper.cjs +2 -2
- package/dist/components/stepper.cjs.map +1 -1
- package/dist/components/stepper.js +2 -2
- package/dist/components/stepper.js.map +1 -1
- package/dist/components/switch.cjs +1 -1
- package/dist/components/switch.cjs.map +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.cjs +147 -0
- package/dist/components/table.cjs.map +1 -0
- package/dist/components/table.d.cts +75 -0
- package/dist/components/table.d.ts +75 -0
- package/dist/components/table.js +138 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.cjs +2 -2
- package/dist/components/tabs.cjs.map +1 -1
- package/dist/components/tabs.js +2 -2
- package/dist/components/tabs.js.map +1 -1
- package/dist/components/textarea.cjs +2 -2
- package/dist/components/textarea.cjs.map +1 -1
- package/dist/components/textarea.js +2 -2
- package/dist/components/textarea.js.map +1 -1
- package/dist/components/theme.cjs.map +1 -1
- package/dist/components/theme.d.cts +2 -2
- package/dist/components/theme.d.ts +2 -2
- package/dist/components/theme.js.map +1 -1
- package/dist/components/time-picker.cjs +1 -1
- package/dist/components/time-picker.cjs.map +1 -1
- package/dist/components/time-picker.js +1 -1
- package/dist/components/time-picker.js.map +1 -1
- package/dist/components/tooltip.cjs +1 -1
- package/dist/components/tooltip.cjs.map +1 -1
- package/dist/components/tooltip.js +1 -1
- package/dist/components/tooltip.js.map +1 -1
- package/dist/lib/accent-theme.cjs +199 -12
- package/dist/lib/accent-theme.cjs.map +1 -1
- package/dist/lib/accent-theme.d.cts +45 -2
- package/dist/lib/accent-theme.d.ts +45 -2
- package/dist/lib/accent-theme.js +195 -13
- package/dist/lib/accent-theme.js.map +1 -1
- package/dist/lib/container-chrome.cjs +1 -1
- package/dist/lib/container-chrome.cjs.map +1 -1
- package/dist/lib/container-chrome.js +1 -1
- package/dist/lib/container-chrome.js.map +1 -1
- package/dist/styles/index.css +9 -27
- package/dist/styles/spark-default.css +373 -19
- package/dist/styles/theme.css +2 -2
- package/package.json +17 -1
package/dist/components/tabs.js
CHANGED
|
@@ -120,7 +120,7 @@ function TabsList({ className, style, variant = "underline", fill, ref, ...props
|
|
|
120
120
|
onClick: scrollByPage(dir),
|
|
121
121
|
tabIndex: -1,
|
|
122
122
|
type: "button",
|
|
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"),
|
|
123
|
+
className: cn("touch-target absolute top-1/2 z-1 hidden size-10 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[max(var(--spark-rounded-md),var(--spark-rounded-full))] bg-gray-1/72 text-gray-a11 backdrop-blur-sm hover:text-gray-12", dir === 1 ? "right-0 group-data-overflow-end/tabs:flex" : "left-0 group-data-overflow-start/tabs:flex"),
|
|
124
124
|
children: dir === 1 ? /* @__PURE__ */ jsx(ChevronRight, {}) : /* @__PURE__ */ jsx(ChevronLeft, {})
|
|
125
125
|
});
|
|
126
126
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -145,7 +145,7 @@ function TabsList({ className, style, variant = "underline", fill, ref, ...props
|
|
|
145
145
|
function TabsTrigger({ className, children, ...props }) {
|
|
146
146
|
return /* @__PURE__ */ jsx(Tabs$1.Tab, {
|
|
147
147
|
"data-slot": "tabs-trigger",
|
|
148
|
-
className: cn("group/trigger inline-flex min-w-(--tabs-trigger-minw,0px) shrink-0 cursor-pointer scroll-mx-10 items-center justify-center whitespace-nowrap text-gray-12 transition-colors", "group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0", "data-active:text-(--spark-tabs-ink,var(--spark-a11))", "focus-visible:outline-hidden", "group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1", "data-disabled:pointer-events-none data-disabled:text-gray-a8", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4", "group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]", "group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5", "group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium", "group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3", "group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6", className),
|
|
148
|
+
className: cn("group/trigger touch-target 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 motion-reduce:transition-none", "group-data-fill/tabs:shrink group-data-fill/tabs:grow group-data-fill/tabs:basis-0", "data-active:text-(--spark-tabs-ink,var(--spark-a11))", "focus-visible:outline-hidden", "group-data-[variant=pill]/tabs:focus-visible:ring-2 group-data-[variant=pill]/tabs:focus-visible:ring-offset-1", "data-disabled:pointer-events-none data-disabled:text-gray-a8", "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4", "group-data-[variant=underline]/tabs:min-h-12 group-data-[variant=underline]/tabs:px-1 group-data-[variant=underline]/tabs:text-base group-data-[variant=underline]/tabs:font-bold group-data-[variant=underline]/tabs:[--tabs-trigger-minw:5rem]", "group-data-[variant=underline]/tabs:border-b-2 group-data-[variant=underline]/tabs:border-gray-a5", "group-data-[variant=pill]/tabs:h-8 group-data-[variant=pill]/tabs:rounded-sm group-data-[variant=pill]/tabs:px-4 group-data-[variant=pill]/tabs:text-sm group-data-[variant=pill]/tabs:font-medium", "group-data-[variant=pill]/tabs:not-data-active:hover:bg-gray-a3", "group-data-[variant=pill]/tabs:data-active:bg-gray-1 group-data-[variant=pill]/tabs:data-active:shadow-sm dark:group-data-[variant=pill]/tabs:data-active:bg-gray-a4 dark:group-data-[variant=pill]/tabs:data-active:inset-ring dark:group-data-[variant=pill]/tabs:data-active:inset-ring-gray-a6", className),
|
|
149
149
|
...props,
|
|
150
150
|
children: /* @__PURE__ */ jsx("span", {
|
|
151
151
|
"data-slot": "tabs-trigger-inner",
|
|
@@ -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 { 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
|
+
{"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 'touch-target 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 // touch-target on the base, not per-variant: pill is 32px and underline\n // 48px only at scaling=100% (min-h-12 tracks the knob), so both want the\n // coarse-pointer floor. The overlay never shrinks an already-large box.\n 'group/trigger touch-target 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 motion-reduce:transition-none',\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,15 +1,15 @@
|
|
|
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");
|
|
9
9
|
let class_variance_authority = require("class-variance-authority");
|
|
10
10
|
let cnfast = require("cnfast");
|
|
11
11
|
//#region src/components/textarea.tsx
|
|
12
|
-
const textareaVariants = (0, class_variance_authority.cva)("flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-
|
|
12
|
+
const textareaVariants = (0, class_variance_authority.cva)("flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-placeholder disabled:cursor-not-allowed disabled:text-gray-disabled disabled:placeholder:text-gray-disabled aria-invalid:text-error motion-reduce:transition-none", {
|
|
13
13
|
variants: {
|
|
14
14
|
chrome: {
|
|
15
15
|
none: "border-0 px-0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.cjs","names":["GHOST_UNDERLINE","boxChrome","React","useFieldControl"],"sources":["../../src/components/textarea.tsx"],"sourcesContent":["import { cva } from 'class-variance-authority'\nimport * as React from 'react'\nimport { boxChrome, GHOST_UNDERLINE } from '../lib/input-box-variants'\nimport type { SparkInputStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\nimport { useFieldControl } from './field'\n\ntype TextareaVariant = SparkInputStyle | 'none'\n\nconst textareaVariants = cva(\n 'flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-
|
|
1
|
+
{"version":3,"file":"textarea.cjs","names":["GHOST_UNDERLINE","boxChrome","React","useFieldControl"],"sources":["../../src/components/textarea.tsx"],"sourcesContent":["import { cva } from 'class-variance-authority'\nimport * as React from 'react'\nimport { boxChrome, GHOST_UNDERLINE } from '../lib/input-box-variants'\nimport type { SparkInputStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\nimport { useFieldControl } from './field'\n\ntype TextareaVariant = SparkInputStyle | 'none'\n\nconst textareaVariants = cva(\n 'flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-placeholder disabled:cursor-not-allowed disabled:text-gray-disabled disabled:placeholder:text-gray-disabled aria-invalid:text-error motion-reduce:transition-none',\n {\n variants: {\n chrome: {\n none: 'border-0 px-0',\n inset: GHOST_UNDERLINE,\n box: 'min-h-11 rounded-md px-3 py-2 text-base transition-all motion-reduce:transition-none',\n },\n inputStyle: { soft: '', surface: '', outlined: '', ghost: '' },\n inputColor: { brand: '', gray: '' },\n autoResize: {\n true: 'field-sizing-content resize-none',\n false: 'resize-y',\n },\n },\n compoundVariants: [\n {\n chrome: 'box',\n inputStyle: 'soft',\n inputColor: 'brand',\n class: boxChrome.soft.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'soft',\n inputColor: 'gray',\n class: boxChrome.soft.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'surface',\n inputColor: 'brand',\n class: boxChrome.surface.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'surface',\n inputColor: 'gray',\n class: boxChrome.surface.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'outlined',\n inputColor: 'brand',\n class: boxChrome.outlined.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'outlined',\n inputColor: 'gray',\n class: boxChrome.outlined.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'ghost',\n inputColor: 'brand',\n class: boxChrome.ghost.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'ghost',\n inputColor: 'gray',\n class: boxChrome.ghost.gray,\n },\n { chrome: 'inset', inputColor: 'brand', class: 'border-b-input' },\n { chrome: 'inset', inputColor: 'gray', class: 'border-b-input-gray' },\n // GHOST_UNDERLINE and boxChrome.ghost.* hardcode h-10; un-clamp for\n // textarea's min-height model — must come after those compounds above\n { chrome: 'inset', class: 'h-auto min-h-10' },\n { chrome: 'box', inputStyle: 'ghost', class: 'h-auto' },\n ],\n },\n)\n\ninterface TextareaProps extends React.ComponentProps<'textarea'> {\n variant?: TextareaVariant\n autoResize?: boolean\n}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n function Textarea(\n {\n variant,\n autoResize = false,\n rows = 3,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedByProp,\n ...props\n },\n ref,\n ) {\n const { field, id, ariaDescribedBy, ariaInvalid, inputStyle, inputColor } =\n useFieldControl({\n id: idProp,\n ariaDescribedBy: ariaDescribedByProp,\n variant: variant !== 'none' ? variant : undefined,\n })\n // an explicit variant prop keeps the box even inside an inset field\n const chrome =\n variant === 'none'\n ? 'none'\n : !variant && field?.labelPosition === 'inset'\n ? 'inset'\n : 'box'\n\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n textareaVariants({ chrome, inputStyle, inputColor, autoResize }),\n className,\n )}\n id={id}\n rows={rows}\n ref={ref}\n aria-invalid={ariaInvalid}\n {...props}\n aria-describedby={ariaDescribedBy}\n />\n )\n },\n)\n\nexport { Textarea }\nexport type { TextareaProps, TextareaVariant }\n"],"mappings":";;;;;;;;;;;AASA,MAAM,oBAAA,GAAA,yBAAA,KACJ,iTACA;CACE,UAAU;EACR,QAAQ;GACN,MAAM;GACN,OAAOA,+BAAAA;GACP,KAAK;EACP;EACA,YAAY;GAAE,MAAM;GAAI,SAAS;GAAI,UAAU;GAAI,OAAO;EAAG;EAC7D,YAAY;GAAE,OAAO;GAAI,MAAM;EAAG;EAClC,YAAY;GACV,MAAM;GACN,OAAO;EACT;CACF;CACA,kBAAkB;EAChB;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOC,+BAAAA,UAAU,KAAK;EACxB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,KAAK;EACxB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,QAAQ;EAC3B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,QAAQ;EAC3B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,SAAS;EAC5B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,SAAS;EAC5B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,MAAM;EACzB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAOA,+BAAAA,UAAU,MAAM;EACzB;EACA;GAAE,QAAQ;GAAS,YAAY;GAAS,OAAO;EAAiB;EAChE;GAAE,QAAQ;GAAS,YAAY;GAAQ,OAAO;EAAsB;EAGpE;GAAE,QAAQ;GAAS,OAAO;EAAkB;EAC5C;GAAE,QAAQ;GAAO,YAAY;GAAS,OAAO;EAAS;CACxD;AACF,CACF;AAOA,MAAM,WAAWC,MAAM,WACrB,SAAS,SACP,EACE,SACA,aAAa,OACb,OAAO,GACP,WACA,IAAI,QACJ,oBAAoB,qBACpB,GAAG,SAEL,KACA;CACA,MAAM,EAAE,OAAO,IAAI,iBAAiB,aAAa,YAAY,eAC3DC,yBAAAA,gBAAgB;EACd,IAAI;EACJ,iBAAiB;EACjB,SAAS,YAAY,SAAS,UAAU,KAAA;CAC1C,CAAC;CASH,OACE,iBAAA,GAAA,kBAAA,KAAC,YAAD;EACE,aAAU;EACV,YAAA,GAAA,OAAA,IACE,iBAAiB;GAAE,QAVvB,YAAY,SACR,SACA,CAAC,WAAW,OAAO,kBAAkB,UACnC,UACA;GAMyB;GAAY;GAAY;EAAW,CAAC,GAC/D,SACF;EACI;EACE;EACD;EACL,gBAAc;EACd,GAAI;EACJ,oBAAkB;CACnB,CAAA;AAEL,CACF"}
|
|
@@ -1,12 +1,12 @@
|
|
|
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";
|
|
8
8
|
//#region src/components/textarea.tsx
|
|
9
|
-
const textareaVariants = cva("flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-
|
|
9
|
+
const textareaVariants = cva("flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-placeholder disabled:cursor-not-allowed disabled:text-gray-disabled disabled:placeholder:text-gray-disabled aria-invalid:text-error motion-reduce:transition-none", {
|
|
10
10
|
variants: {
|
|
11
11
|
chrome: {
|
|
12
12
|
none: "border-0 px-0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textarea.js","names":["React"],"sources":["../../src/components/textarea.tsx"],"sourcesContent":["import { cva } from 'class-variance-authority'\nimport * as React from 'react'\nimport { boxChrome, GHOST_UNDERLINE } from '../lib/input-box-variants'\nimport type { SparkInputStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\nimport { useFieldControl } from './field'\n\ntype TextareaVariant = SparkInputStyle | 'none'\n\nconst textareaVariants = cva(\n 'flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-
|
|
1
|
+
{"version":3,"file":"textarea.js","names":["React"],"sources":["../../src/components/textarea.tsx"],"sourcesContent":["import { cva } from 'class-variance-authority'\nimport * as React from 'react'\nimport { boxChrome, GHOST_UNDERLINE } from '../lib/input-box-variants'\nimport type { SparkInputStyle } from '../lib/theme-types'\nimport { cn } from '../utils/cn'\nimport { useFieldControl } from './field'\n\ntype TextareaVariant = SparkInputStyle | 'none'\n\nconst textareaVariants = cva(\n 'flex w-full min-w-0 bg-transparent text-gray outline-hidden transition-all group-data-[invalid=true]/field:text-error placeholder:text-gray-placeholder disabled:cursor-not-allowed disabled:text-gray-disabled disabled:placeholder:text-gray-disabled aria-invalid:text-error motion-reduce:transition-none',\n {\n variants: {\n chrome: {\n none: 'border-0 px-0',\n inset: GHOST_UNDERLINE,\n box: 'min-h-11 rounded-md px-3 py-2 text-base transition-all motion-reduce:transition-none',\n },\n inputStyle: { soft: '', surface: '', outlined: '', ghost: '' },\n inputColor: { brand: '', gray: '' },\n autoResize: {\n true: 'field-sizing-content resize-none',\n false: 'resize-y',\n },\n },\n compoundVariants: [\n {\n chrome: 'box',\n inputStyle: 'soft',\n inputColor: 'brand',\n class: boxChrome.soft.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'soft',\n inputColor: 'gray',\n class: boxChrome.soft.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'surface',\n inputColor: 'brand',\n class: boxChrome.surface.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'surface',\n inputColor: 'gray',\n class: boxChrome.surface.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'outlined',\n inputColor: 'brand',\n class: boxChrome.outlined.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'outlined',\n inputColor: 'gray',\n class: boxChrome.outlined.gray,\n },\n {\n chrome: 'box',\n inputStyle: 'ghost',\n inputColor: 'brand',\n class: boxChrome.ghost.brand,\n },\n {\n chrome: 'box',\n inputStyle: 'ghost',\n inputColor: 'gray',\n class: boxChrome.ghost.gray,\n },\n { chrome: 'inset', inputColor: 'brand', class: 'border-b-input' },\n { chrome: 'inset', inputColor: 'gray', class: 'border-b-input-gray' },\n // GHOST_UNDERLINE and boxChrome.ghost.* hardcode h-10; un-clamp for\n // textarea's min-height model — must come after those compounds above\n { chrome: 'inset', class: 'h-auto min-h-10' },\n { chrome: 'box', inputStyle: 'ghost', class: 'h-auto' },\n ],\n },\n)\n\ninterface TextareaProps extends React.ComponentProps<'textarea'> {\n variant?: TextareaVariant\n autoResize?: boolean\n}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n function Textarea(\n {\n variant,\n autoResize = false,\n rows = 3,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedByProp,\n ...props\n },\n ref,\n ) {\n const { field, id, ariaDescribedBy, ariaInvalid, inputStyle, inputColor } =\n useFieldControl({\n id: idProp,\n ariaDescribedBy: ariaDescribedByProp,\n variant: variant !== 'none' ? variant : undefined,\n })\n // an explicit variant prop keeps the box even inside an inset field\n const chrome =\n variant === 'none'\n ? 'none'\n : !variant && field?.labelPosition === 'inset'\n ? 'inset'\n : 'box'\n\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n textareaVariants({ chrome, inputStyle, inputColor, autoResize }),\n className,\n )}\n id={id}\n rows={rows}\n ref={ref}\n aria-invalid={ariaInvalid}\n {...props}\n aria-describedby={ariaDescribedBy}\n />\n )\n },\n)\n\nexport { Textarea }\nexport type { TextareaProps, TextareaVariant }\n"],"mappings":";;;;;;;;AASA,MAAM,mBAAmB,IACvB,iTACA;CACE,UAAU;EACR,QAAQ;GACN,MAAM;GACN,OAAO;GACP,KAAK;EACP;EACA,YAAY;GAAE,MAAM;GAAI,SAAS;GAAI,UAAU;GAAI,OAAO;EAAG;EAC7D,YAAY;GAAE,OAAO;GAAI,MAAM;EAAG;EAClC,YAAY;GACV,MAAM;GACN,OAAO;EACT;CACF;CACA,kBAAkB;EAChB;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,KAAK;EACxB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,KAAK;EACxB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,QAAQ;EAC3B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,QAAQ;EAC3B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,SAAS;EAC5B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,SAAS;EAC5B;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,MAAM;EACzB;EACA;GACE,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ,OAAO,UAAU,MAAM;EACzB;EACA;GAAE,QAAQ;GAAS,YAAY;GAAS,OAAO;EAAiB;EAChE;GAAE,QAAQ;GAAS,YAAY;GAAQ,OAAO;EAAsB;EAGpE;GAAE,QAAQ;GAAS,OAAO;EAAkB;EAC5C;GAAE,QAAQ;GAAO,YAAY;GAAS,OAAO;EAAS;CACxD;AACF,CACF;AAOA,MAAM,WAAWA,QAAM,WACrB,SAAS,SACP,EACE,SACA,aAAa,OACb,OAAO,GACP,WACA,IAAI,QACJ,oBAAoB,qBACpB,GAAG,SAEL,KACA;CACA,MAAM,EAAE,OAAO,IAAI,iBAAiB,aAAa,YAAY,eAC3D,gBAAgB;EACd,IAAI;EACJ,iBAAiB;EACjB,SAAS,YAAY,SAAS,UAAU,KAAA;CAC1C,CAAC;CASH,OACE,oBAAC,YAAD;EACE,aAAU;EACV,WAAW,GACT,iBAAiB;GAAE,QAVvB,YAAY,SACR,SACA,CAAC,WAAW,OAAO,kBAAkB,UACnC,UACA;GAMyB;GAAY;GAAY;EAAW,CAAC,GAC/D,SACF;EACI;EACE;EACD;EACL,gBAAc;EACd,GAAI;EACJ,oBAAkB;CACnB,CAAA;AAEL,CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.cjs","names":["React","resolveAutoNeutral","isSparkNeutralFamily","isParsableColor","getMatchingNeutral","CURATED_ACCENTS","appearanceForBackground","accentSparkVars","neutralSparkVars","generateAccentScale","invertAccentScale","neutralFamilyVars"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n return emit(inverted ? invertAccentScale(scale) : scale, opts != null)\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n return {\n inlineVars: toVars(build(value.appearance)),\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) =>\n bgAppearance === appearance\n ? build(appearance)\n : [...build(appearance), ...offCanvasNeutrals]\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const sel = `[data-spark-scope=\"${scopeId}\"]`\n const css = `${sel}{${light.join('')}}:is(.dark,.dark-theme) ${sel}{${dark.join('')}}`\n return { inlineVars: {} as React.CSSProperties, css, scoped: true }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-inverted': inverted ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n return (\n <ThemeContext.Provider value={value}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeContext.Provider>\n )\n },\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme.cjs","names":["React","resolveAutoNeutral","isSparkNeutralFamily","isParsableColor","getMatchingNeutral","CURATED_ACCENTS","appearanceForBackground","accentSparkVars","neutralSparkVars","generateAccentScale","invertAccentScale","neutralFamilyVars"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkNeutralFamily,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n return emit(inverted ? invertAccentScale(scale) : scale, opts != null)\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n return {\n inlineVars: toVars(build(value.appearance)),\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) =>\n bgAppearance === appearance\n ? build(appearance)\n : [...build(appearance), ...offCanvasNeutrals]\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const sel = `[data-spark-scope=\"${scopeId}\"]`\n const css = `${sel}{${light.join('')}}:is(.dark,.dark-theme) ${sel}{${dark.join('')}}`\n return { inlineVars: {} as React.CSSProperties, css, scoped: true }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-inverted': inverted ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n return (\n <ThemeContext.Provider value={value}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeContext.Provider>\n )\n },\n)\n"],"mappings":";;;;;;;;;;;;;;AAiDA,MAAM,iBAAiB;AAgDvB,MAAM,eAAeA,MAAM,cAA0B,CAAC,CAAC;;AAGvD,SAAgB,WAAuB;CACrC,OAAOA,MAAM,WAAW,YAAY;AACtC;;AAMA,MAAa,QAAQA,MAAM,WACzB,SAAS,MACP,EACE,aACA,iBACA,cACA,QACA,SACA,SACA,cACA,cACA,UACA,eACA,YACA,YACA,aACA,WACA,YACA,aACA,UACA,QACA,WACA,OACA,UACA,GAAG,QAEL,KACA;CACA,MAAM,SAAS,SAAS;CAExB,MAAM,QAAQA,MAAM,eACX;EACL,aAAa,eAAe,OAAO;EACnC,iBAAiB,mBAAmB,OAAO;EAC3C,cAAc,gBAAgB,OAAO;EACrC,QAAQ,UAAU,OAAO;EACzB,SAAS,WAAW,OAAO;EAC3B,SAAS,WAAW,OAAO;EAC3B,cAAc,gBAAgB,OAAO;EACrC,cAAc,gBAAgB,OAAO;EACrC,UAAU,YAAY,OAAO;EAC7B,eAAe,iBAAiB,OAAO;EACvC,YAAY,cAAc,OAAO;EACjC,YAAY,cAAc,OAAO;EACjC,aAAa,eAAe,OAAO;EACnC,WAAW,aAAa,OAAO;EAC/B,YAAY,cAAc,OAAO;EACjC,aAAa,eAAe,OAAO;EACnC,UAAU,YAAY,OAAO;CAC/B,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAGA,MAAM,UAAUA,MAAM,MAAM;CAO5B,MAAM,kBACJ,iBAAiB,SACbC,yBAAAA,mBACE,MAAM,eAAe,gBACrB,eACF,IACA;CAMN,MAAM,sBACJ,oBAAoB,KAAA,KACpBC,wBAAAA,qBAAqB,eAAe,KACpCC,yBAAAA,gBAAgB,eAAe;CACjC,IAAI,CAAC,qBACH,QAAQ,KACN,0CAA0C,gBAAgB,+CAC5D;CAEF,MAAM,uBAAuB,sBACzB,kBACAC,yBAAAA,mBAAmB,MAAM,eAAe,cAAc;CAW1D,MAAM,mBACJ,yBAAyB,KAAA,IACrB,KAAA,IACAF,wBAAAA,qBAAqB,oBAAoB,IACvC,uBACAE,yBAAAA,mBAAmB,MAAM,eAAe,cAAc;CAE9D,MAAM,SAASJ,MAAM,cAAc;EACjC,MAAM,QAAQ;GACZ,YAAY,CAAC;GACb,KAAK;GACL,QAAQ;EACV;EAGA,IAAI,CAAC,eAAe,CAAC,iBAAiB,OAAO;EAE7C,MAAM,UAAU,UACd,OAAO,YACL,MACG,QAAQ,SAAS,KAAK,SAAS,GAAG,CAAC,EACnC,KAAK,SAAS;GACb,MAAM,IAAI,KAAK,QAAQ,GAAG;GAC1B,OAAO,CACL,KAAK,MAAM,GAAG,CAAC,EAAE,KAAK,GACtB,KACG,MAAM,IAAI,CAAC,EACX,QAAQ,MAAM,EAAE,EAChB,KAAK,CACV;EACF,CAAC,CACL;EAEF,IAAI;GACF,MAAM,SAAS,MAAM,eAAe;GACpC,MAAM,MAAMK,yBAAAA,gBAAgB,WAAW;GAOvC,MAAM,eAAe,kBACjBC,yBAAAA,wBAAwB,eAAe,IACvC,KAAA;GAIJ,IACE,mBACA,MAAM,cACN,iBAAiB,MAAM,YAEvB,QAAQ,KACN,qCAAqC,gBAAgB,mBAAmB,aAAa,4BAA4B,MAAM,WAAW,kBAAkB,MAAM,WAAW,wDACvK;GAKF,MAAM,WAAW,eACf,mBAAmB,iBAAiB,aAChC;IACE,YAAY;IACZ,SAAS,wBAAyB;GACpC,IACA,KAAA;GAIN,MAAM,QAAQ,OAAyB,WACrC,SACI,CAAC,GAAGC,yBAAAA,gBAAgB,KAAK,GAAG,GAAGC,yBAAAA,iBAAiB,KAAK,CAAC,IACtDD,yBAAAA,gBAAgB,KAAK;GAE3B,MAAM,SAAS,eAA2B;IACxC,MAAM,OAAO,QAAQ,UAAU;IAC/B,MAAM,QAAQE,yBAAAA,oBAAoB,KAAK,YAAY,IAAI;IAGvD,OAAO,KAAK,WAAWC,yBAAAA,kBAAkB,KAAK,IAAI,OAAO,QAAQ,IAAI;GACvE;GAIA,IAAI,MAAM,YACR,OAAO;IACL,YAAY,OAAO,MAAM,MAAM,UAAU,CAAC;IAC1C,KAAK;IACL,QAAQ;GACV;GAcF,MAAM,oBAAoB,kBACtBC,yBAAAA,kBAAkB,wBAAwB,OAAO,IACjD,CAAC;GACL,MAAM,YAAY,eAChB,iBAAiB,aACb,MAAM,UAAU,IAChB,CAAC,GAAG,MAAM,UAAU,GAAG,GAAG,iBAAiB;GACjD,MAAM,QAAQ,SAAS,OAAO;GAC9B,MAAM,OAAO,SAAS,MAAM;GAC5B,MAAM,MAAM,sBAAsB,QAAQ;GAE1C,OAAO;IAAE,YAAY,CAAC;IAA0B,KAAA,GADjC,IAAI,GAAG,MAAM,KAAK,EAAE,EAAE,0BAA0B,IAAI,GAAG,KAAK,KAAK,EAAE,EAAE;IAC/B,QAAQ;GAAK;EACpE,SAAS,OAAO;GAGd,QAAQ,KACN,yCAAyC,YAAY,uBAAuB,gBAAgB,KAC5F,KACF;GACA,OAAO;EACT;CACF,GAAG;EACD;EACA;EACA;EACA;EACA,MAAM;EACN,MAAM;EACN;CACF,CAAC;CAED,MAAM,WAAA,GAAA,0BAAA,WAAoB;EACxB,gBAAgB;EAChB;EACA;EACA,QAAA,GAAA,2BAAA,YACE;GACE,YAAA,GAAA,OAAA,IAAc,eAAe,YAAY,SAAS;GAClD,sBAAsB;GACtB,eAAe;GACf,gBAAgB;GAChB,gBAAgB;GAChB,cAAc;GACd,sBAAsB,eAAe,KAAK,KAAA;GAC1C,iBAAiB,WAAW,KAAK,KAAA;GACjC,uBAAuB;GACvB,oBAAoB;GACpB,oBAAoB;GACpB,qBAAqB;GACrB,mBAAmB,cAAc,WAAW,WAAW,KAAA;GACvD,oBAAoB,OAAO,SAAS,UAAU,KAAA;GAC9C,OAAO;IACL,GAAG,OAAO;IAIV,GAAI,kBACA,EAAE,iBAAiB,0BAA0B,IAC7C;IACJ,GAAI,cAAc,EAAE,wBAAwB,YAAY,IAAI;IAC5D,GAAI,WAAW,EAAE,qBAAqB,SAAS,IAAI;IACnD,GAAG;GACL;GACA;EACF,GACA,IACF;CACF,CAAC;CAED,OACE,iBAAA,GAAA,kBAAA,MAAC,aAAa,UAAd;EAA8B;YAA9B,CACG,OAAO,MAAM,iBAAA,GAAA,kBAAA,KAAC,SAAD,EAAA,UAAQ,OAAO,IAAW,CAAA,IAAI,MAC3C,OACoB;;AAE3B,CACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { a as SparkInputStyle, d as SparkSpacing, f as SparkStyle, i as SparkInputColor, l as SparkRadius, n as SparkCardColor, o as SparkLabelPosition, r as SparkCardVariant, s as SparkNeutral, u as SparkScaling } from "../theme-types-Bn5E15-P.cjs";
|
|
3
|
+
import { a as SparkInputStyle, c as SparkNeutralFamily, d as SparkSpacing, f as SparkStyle, i as SparkInputColor, l as SparkRadius, n as SparkCardColor, o as SparkLabelPosition, r as SparkCardVariant, s as SparkNeutral, u as SparkScaling } from "../theme-types-Bn5E15-P.cjs";
|
|
4
4
|
import { Appearance } from "../lib/accent-theme.cjs";
|
|
5
5
|
import * as React$1 from "react";
|
|
6
6
|
import { useRender } from "@base-ui/react/use-render";
|
|
@@ -57,5 +57,5 @@ interface ThemeProps extends useRender.ComponentProps<'div'>, ThemeValue {}
|
|
|
57
57
|
/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
|
|
58
58
|
declare const Theme: React$1.ForwardRefExoticComponent<Omit<ThemeProps, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
|
|
59
59
|
//#endregion
|
|
60
|
-
export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
|
|
60
|
+
export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
|
|
61
61
|
//# sourceMappingURL=theme.d.cts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { a as SparkInputStyle, d as SparkSpacing, f as SparkStyle, i as SparkInputColor, l as SparkRadius, n as SparkCardColor, o as SparkLabelPosition, r as SparkCardVariant, s as SparkNeutral, u as SparkScaling } from "../theme-types-Bn5E15-P.js";
|
|
3
|
+
import { a as SparkInputStyle, c as SparkNeutralFamily, d as SparkSpacing, f as SparkStyle, i as SparkInputColor, l as SparkRadius, n as SparkCardColor, o as SparkLabelPosition, r as SparkCardVariant, s as SparkNeutral, u as SparkScaling } from "../theme-types-Bn5E15-P.js";
|
|
4
4
|
import { Appearance } from "../lib/accent-theme.js";
|
|
5
5
|
import * as React$1 from "react";
|
|
6
6
|
import { useRender } from "@base-ui/react/use-render";
|
|
@@ -57,5 +57,5 @@ interface ThemeProps extends useRender.ComponentProps<'div'>, ThemeValue {}
|
|
|
57
57
|
/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */
|
|
58
58
|
declare const Theme: React$1.ForwardRefExoticComponent<Omit<ThemeProps, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
|
|
59
59
|
//#endregion
|
|
60
|
-
export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
|
|
60
|
+
export { type SparkCardColor, type SparkCardVariant, type SparkInputColor, type SparkInputStyle, type SparkLabelPosition, type SparkNeutral, type SparkNeutralFamily, type SparkRadius, type SparkScaling, type SparkSpacing, type SparkStyle, Theme, ThemeProps, ThemeValue, useTheme };
|
|
61
61
|
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.js","names":["React"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n return emit(inverted ? invertAccentScale(scale) : scale, opts != null)\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n return {\n inlineVars: toVars(build(value.appearance)),\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) =>\n bgAppearance === appearance\n ? build(appearance)\n : [...build(appearance), ...offCanvasNeutrals]\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const sel = `[data-spark-scope=\"${scopeId}\"]`\n const css = `${sel}{${light.join('')}}:is(.dark,.dark-theme) ${sel}{${dark.join('')}}`\n return { inlineVars: {} as React.CSSProperties, css, scoped: true }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-inverted': inverted ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n return (\n <ThemeContext.Provider value={value}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeContext.Provider>\n )\n },\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"theme.js","names":["React"],"sources":["../../src/components/theme.tsx"],"sourcesContent":["'use client'\n\nimport { mergeProps } from '@base-ui/react/merge-props'\nimport { useRender } from '@base-ui/react/use-render'\nimport * as React from 'react'\nimport {\n CURATED_ACCENTS,\n accentSparkVars,\n appearanceForBackground,\n generateAccentScale,\n getMatchingNeutral,\n invertAccentScale,\n isParsableColor,\n neutralFamilyVars,\n neutralSparkVars,\n resolveAutoNeutral,\n type Appearance,\n type SparkAccentScale,\n} from '../lib/accent-theme'\nimport {\n isSparkNeutralFamily,\n type SparkCardColor,\n type SparkCardVariant,\n type SparkInputColor,\n type SparkInputStyle,\n type SparkLabelPosition,\n type SparkNeutral,\n type SparkRadius,\n type SparkScaling,\n type SparkSpacing,\n type SparkStyle,\n} from '../lib/theme-types'\nimport { cn } from '../utils/cn'\n\nexport type {\n SparkCardColor,\n SparkCardVariant,\n SparkInputColor,\n SparkInputStyle,\n SparkLabelPosition,\n SparkNeutral,\n SparkNeutralFamily,\n SparkRadius,\n SparkScaling,\n SparkSpacing,\n SparkStyle,\n} from '../lib/theme-types'\n\n/** The epilot brand accent — the scale `spark-default.css` ships statically. */\nconst DEFAULT_ACCENT = '#005EB4'\n\nexport interface ThemeValue {\n /** Curated accent name or a custom hex. */\n accentColor?: string\n /**\n * Page canvas as a custom hex. Regenerates BOTH the accent and neutral scales against\n * it (each alpha step is composited against the canvas, so a saturated colour needs\n * the neutral ramp regenerated too, not just the accent). Unset → the fixed\n * `#ffffff` / `#000000` default, and neutrals stay on the static Radix families.\n */\n backgroundColor?: string\n /**\n * A curated family name, an arbitrary color to seed the neutral scale, or `'auto'` to\n * resolve one — preferring `backgroundColor`'s own hue when it's perceptibly tinted,\n * else pairing with the accent (Radix's `grayColor=\"auto\"`). A custom seed (from `auto`\n * or set directly) only regenerates the scale alongside a `backgroundColor`; without\n * one, `data-neutral-color` falls back to the accent-matched family and that's what\n * actually paints.\n */\n neutralColor?: SparkNeutral\n radius?: SparkRadius\n scaling?: SparkScaling\n spacing?: SparkSpacing\n styleVariant?: SparkStyle\n /** Orthogonal to `styleVariant`: shifts any style toward higher-contrast steps. */\n highContrast?: boolean\n /**\n * Orthogonal to `styleVariant`: re-casts the accent scale for use ON its own solid fill,\n * for controls sitting on a solid accent surface (a `<Card variant=\"solid\" color=\"accent\">`),\n * where a solid control would otherwise paint the same `--spark-9` as the card and vanish.\n * Swaps step 9 with its contrast ink, so the fill/ink pair stays WCAG-safe for any accent.\n */\n inverted?: boolean\n labelPosition?: SparkLabelPosition\n inputStyle?: SparkInputStyle\n inputColor?: SparkInputColor\n /** App-level default `<Card>` chrome; a per-card `variant` prop overrides it. */\n cardVariant?: SparkCardVariant\n /** App-level default `<Card>` palette; a per-card `color` prop overrides it. */\n cardColor?: SparkCardColor\n appearance?: Appearance\n /** Font stack for `h1`–`h6`. Any valid CSS `font-family` value. */\n fontHeading?: string\n /** Font stack for everything else. Any valid CSS `font-family` value. */\n fontBody?: string\n}\n\nconst ThemeContext = React.createContext<ThemeValue>({})\n\n/** Merged theme values from enclosing `<Theme>` providers. */\nexport function useTheme(): ThemeValue {\n return React.useContext(ThemeContext)\n}\n\nexport interface ThemeProps\n extends useRender.ComponentProps<'div'>, ThemeValue {}\n\n/** Optional sugar: writes the theming `data-*` attrs + accent `--spark-*` vars onto a wrapper. */\nexport const Theme = React.forwardRef<HTMLDivElement, ThemeProps>(\n function Theme(\n {\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n render,\n className,\n style,\n children,\n ...rest\n },\n ref,\n ) {\n const parent = useTheme()\n\n const value = React.useMemo<ThemeValue>(\n () => ({\n accentColor: accentColor ?? parent.accentColor,\n backgroundColor: backgroundColor ?? parent.backgroundColor,\n neutralColor: neutralColor ?? parent.neutralColor,\n radius: radius ?? parent.radius,\n scaling: scaling ?? parent.scaling,\n spacing: spacing ?? parent.spacing,\n styleVariant: styleVariant ?? parent.styleVariant,\n highContrast: highContrast ?? parent.highContrast,\n inverted: inverted ?? parent.inverted,\n labelPosition: labelPosition ?? parent.labelPosition,\n inputStyle: inputStyle ?? parent.inputStyle,\n inputColor: inputColor ?? parent.inputColor,\n cardVariant: cardVariant ?? parent.cardVariant,\n cardColor: cardColor ?? parent.cardColor,\n appearance: appearance ?? parent.appearance,\n fontHeading: fontHeading ?? parent.fontHeading,\n fontBody: fontBody ?? parent.fontBody,\n }),\n [\n accentColor,\n backgroundColor,\n neutralColor,\n radius,\n scaling,\n spacing,\n styleVariant,\n highContrast,\n inverted,\n labelPosition,\n inputStyle,\n inputColor,\n cardVariant,\n cardColor,\n appearance,\n fontHeading,\n fontBody,\n parent,\n ],\n )\n\n // Scope id for the ambient-appearance case (both scales emitted in a <style>).\n const scopeId = React.useId()\n\n // 'auto' pairs a neutral with the canvas's own tint when it has one, else the\n // effective accent (Radix's grayColor=\"auto\") — see resolveAutoNeutral. Reads the\n // OWN backgroundColor, not the merged value.backgroundColor: optsFor (below) only\n // ever regenerates against a scope's own canvas too, so preferring an inherited one\n // here would resolve a seed that then gets silently discarded.\n const rawNeutralColor =\n neutralColor === 'auto'\n ? resolveAutoNeutral(\n value.accentColor ?? DEFAULT_ACCENT,\n backgroundColor,\n )\n : neutralColor\n\n // A custom neutral that isn't a recognized family must be a real, parseable color —\n // reject invalid input here (e.g. a typo'd family name like \"slat\") so it can't throw\n // deep inside the shared generator, where the catch below would otherwise blame\n // accentColor/backgroundColor without ever naming neutralColor as the actual cause.\n const neutralColorIsValid =\n rawNeutralColor === undefined ||\n isSparkNeutralFamily(rawNeutralColor) ||\n isParsableColor(rawNeutralColor)\n if (!neutralColorIsValid) {\n console.warn(\n `[Theme] Ignoring invalid neutralColor \"${rawNeutralColor}\" — falling back to the accent-matched family.`,\n )\n }\n const resolvedNeutralColor = neutralColorIsValid\n ? rawNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n // The DOM attribute stays one of the 6 static families `[data-neutral-color]` has CSS\n // for — a raw color (from the canvas-tinted auto path, or an arbitrary neutralColor\n // prop) falls back to its accent-matched family here instead, so the attribute is\n // always CSS-selectable/debuggable rather than a sentinel nothing matches.\n // generateAccentScale still gets the real resolvedNeutralColor for generation, below —\n // this substitution is display-only, and doesn't by itself guarantee the *other*\n // appearance's neutrals in ambient (no explicit `appearance`) mode: that's a\n // pre-existing cross-appearance leak in the same family as review-findings.md #5, not\n // something this attribute fallback fixes or regresses.\n const neutralColorAttr =\n resolvedNeutralColor === undefined\n ? undefined\n : isSparkNeutralFamily(resolvedNeutralColor)\n ? resolvedNeutralColor\n : getMatchingNeutral(value.accentColor ?? DEFAULT_ACCENT)\n\n const accent = React.useMemo(() => {\n const empty = {\n inlineVars: {} as React.CSSProperties,\n css: '',\n scoped: false,\n }\n // A custom canvas generates even with no accent of its own: the neutral ramp is\n // composited against it, and --spark-background has to carry it.\n if (!accentColor && !backgroundColor) return empty\n\n const toVars = (decls: string[]) =>\n Object.fromEntries(\n decls\n .filter((decl) => decl.includes(':'))\n .map((decl) => {\n const i = decl.indexOf(':')\n return [\n decl.slice(0, i).trim(),\n decl\n .slice(i + 1)\n .replace(/;$/, '')\n .trim(),\n ]\n }),\n ) as React.CSSProperties\n\n try {\n const source = value.accentColor ?? DEFAULT_ACCENT\n const hex = CURATED_ACCENTS[source] ?? source\n\n // A canvas has a polarity, unlike an accent hue: a yellow page is a *light*\n // page, and reusing it for the dark scale yields light ink on a light canvas\n // (measured 1.16:1). So the custom canvas applies to the appearance it actually\n // belongs to, and the other keeps its default. One knob still covers the common\n // case — a tenant wanting both sets an explicit appearance per scope.\n const bgAppearance = backgroundColor\n ? appearanceForBackground(backgroundColor)\n : undefined\n\n // Honouring a mismatched canvas would paint light ink on a light page, so it's\n // dropped — but say so, or the prop looks broken.\n if (\n backgroundColor &&\n value.appearance &&\n bgAppearance !== value.appearance\n ) {\n console.warn(\n `[Theme] Ignoring backgroundColor \"${backgroundColor}\": it reads as a ${bgAppearance} canvas on an appearance=\"${value.appearance}\" scope. Pass a ${value.appearance} colour, or set it on a scope whose appearance matches.`,\n )\n }\n\n // Default to slate so a regenerated neutral stays in the family the static\n // `:root` default already points at, rather than the generator's ECP gray seed.\n const optsFor = (appearance: Appearance) =>\n backgroundColor && bgAppearance === appearance\n ? {\n background: backgroundColor,\n neutral: resolvedNeutralColor ?? ('slate' as const),\n }\n : undefined\n\n // Neutrals are only worth regenerating against a custom canvas — see\n // neutralSparkVars. Without one the static Radix families are already correct.\n const emit = (scale: SparkAccentScale, custom: boolean) =>\n custom\n ? [...accentSparkVars(scale), ...neutralSparkVars(scale)]\n : accentSparkVars(scale)\n\n const build = (appearance: Appearance) => {\n const opts = optsFor(appearance)\n const scale = generateAccentScale(hex, appearance, opts)\n // `inverted` re-casts the accent for a solid-accent surface; the neutral ramp is\n // unrelated to that swap, so it passes through untouched.\n return emit(inverted ? invertAccentScale(scale) : scale, opts != null)\n }\n\n // Explicit appearance → single-scale inline vars. Correct for a light\n // island inside a dark page (which must NOT pick up the dark scale).\n if (value.appearance) {\n return {\n inlineVars: toVars(build(value.appearance)),\n css: '',\n scoped: false,\n }\n }\n\n // Ambient appearance → emit BOTH scales in a scoped <style> so the\n // `.dark` class selects the right one. Without this, a single-appearance\n // inline scale (default light) breaks components in dark mode.\n //\n // Both blocks must declare the SAME tokens. A custom canvas only regenerates\n // neutrals for its own appearance (see optsFor), and a custom property the other\n // block leaves undeclared keeps its sibling's value rather than falling back — so\n // the off-canvas appearance would paint this one's canvas-tinted neutrals, and\n // being unlayered they'd beat `[data-neutral-color]`'s @layer base mapping too.\n // Point it at the static family instead: the same seed the canvas side was given,\n // so the two appearances agree on family and differ only in canvas.\n const offCanvasNeutrals = backgroundColor\n ? neutralFamilyVars(resolvedNeutralColor ?? 'slate')\n : []\n const scaleFor = (appearance: Appearance) =>\n bgAppearance === appearance\n ? build(appearance)\n : [...build(appearance), ...offCanvasNeutrals]\n const light = scaleFor('light')\n const dark = scaleFor('dark')\n const sel = `[data-spark-scope=\"${scopeId}\"]`\n const css = `${sel}{${light.join('')}}:is(.dark,.dark-theme) ${sel}{${dark.join('')}}`\n return { inlineVars: {} as React.CSSProperties, css, scoped: true }\n } catch (error) {\n // A bad accentColor/backgroundColor must not throw through render — fall back\n // to the default theme.\n console.warn(\n `[Theme] Ignoring invalid accentColor \"${accentColor}\" / backgroundColor \"${backgroundColor}\".`,\n error,\n )\n return empty\n }\n }, [\n accentColor,\n backgroundColor,\n inverted,\n resolvedNeutralColor,\n value.accentColor,\n value.appearance,\n scopeId,\n ])\n\n const element = useRender({\n defaultTagName: 'div',\n render,\n ref,\n props: mergeProps<'div'>(\n {\n className: cn('spark-theme', appearance, className),\n 'data-neutral-color': neutralColorAttr,\n 'data-radius': radius,\n 'data-scaling': scaling,\n 'data-spacing': spacing,\n 'data-style': styleVariant,\n 'data-high-contrast': highContrast ? '' : undefined,\n 'data-inverted': inverted ? '' : undefined,\n 'data-label-position': labelPosition,\n 'data-input-style': inputStyle,\n 'data-input-color': inputColor,\n 'data-card-variant': cardVariant,\n 'data-card-color': cardColor === 'accent' ? 'accent' : undefined,\n 'data-spark-scope': accent.scoped ? scopeId : undefined,\n style: {\n ...accent.inlineVars,\n // Paint the canvas we were handed, else the prop emits a token and visibly\n // does nothing. Reads the var (not the raw prop) so the scoped <style>'s\n // per-appearance value still wins under ambient appearance.\n ...(backgroundColor\n ? { backgroundColor: 'var(--spark-background)' }\n : null),\n ...(fontHeading ? { '--spark-font-heading': fontHeading } : null),\n ...(fontBody ? { '--spark-font-body': fontBody } : null),\n ...style,\n } as React.CSSProperties,\n children,\n } as React.ComponentProps<'div'>,\n rest,\n ),\n })\n\n return (\n <ThemeContext.Provider value={value}>\n {accent.css ? <style>{accent.css}</style> : null}\n {element}\n </ThemeContext.Provider>\n )\n },\n)\n"],"mappings":"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"}
|
|
@@ -64,7 +64,7 @@ const TimePicker = react.forwardRef(({ "aria-label": ariaLabel, className, disab
|
|
|
64
64
|
if (snapped !== event.currentTarget.value) onValueChange(snapped);
|
|
65
65
|
},
|
|
66
66
|
"aria-label": ariaLabel,
|
|
67
|
-
className: (0, cnfast.cn)("h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className)
|
|
67
|
+
className: (0, cnfast.cn)("touch-target h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className)
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
TimePicker.displayName = "TimePicker";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-picker.cjs","names":["React"],"sources":["../../src/components/time-picker.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '../utils/cn'\n\ninterface TimePickerProps {\n /** `HH:MM` 24-hour string. */\n value: string\n onValueChange: (next: string) => void\n disabled?: boolean\n /** Step in seconds (defaults to 60 — minute-resolution). */\n step?: number\n className?: string\n id?: string\n 'aria-label'?: string\n}\n\nconst MINUTES_PER_DAY = 24 * 60\n\n/**\n * Move `HH:mm` one grid step in `direction`, aligning to the `step` grid first.\n * A native `<input type=\"time\">` steps each focused segment by one unit and\n * ignores `step`, so callers that want arrow keys to move by a coarse step\n * (e.g. 5 minutes) route the arrow keys through this instead. Clamped to\n * `00:00`–`23:55` (no wrap); an off-grid start lands on the next grid point in\n * the pressed direction.\n */\nfunction stepGridValue(value: string, step: number, direction: 1 | -1): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n const base = match ? Number(match[1]) * 60 + Number(match[2]) : 0\n const next =\n direction === 1\n ? Math.floor(base / stepMinutes) * stepMinutes + stepMinutes\n : Math.ceil(base / stepMinutes) * stepMinutes - stepMinutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const clamped = Math.max(0, Math.min(next, lastOnGrid))\n const hh = String(Math.floor(clamped / 60)).padStart(2, '0')\n const mm = String(clamped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n/**\n * Round `HH:mm` to the nearest multiple of `step` (used to autofix a typed\n * off-grid value on blur). Already-on-grid values pass through unchanged (so a\n * seconds suffix is preserved); anything that isn't `HH:mm(:ss)` passes through.\n */\nfunction snapToNearestStep(value: string, step: number): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n if (!match) return value\n const minutes = Number(match[2])\n if (minutes % stepMinutes === 0) return value\n const total = Number(match[1]) * 60 + minutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const snapped = Math.min(\n Math.round(total / stepMinutes) * stepMinutes,\n lastOnGrid,\n )\n const hh = String(Math.floor(snapped / 60)).padStart(2, '0')\n const mm = String(snapped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\nconst TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(\n (\n {\n 'aria-label': ariaLabel,\n className,\n disabled,\n id,\n onValueChange,\n step = 60,\n value,\n },\n ref,\n ) => {\n const stepMinutes = Math.floor(step / 60)\n return (\n <input\n ref={ref}\n id={id}\n type=\"time\"\n value={value}\n step={step}\n disabled={disabled}\n onChange={(event) => onValueChange(event.target.value)}\n onKeyDown={(event) => {\n // Native arrows step the focused segment by one unit and ignore step;\n // for a coarse grid, drive up/down through the grid instead.\n if (stepMinutes <= 1) return\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n event.preventDefault()\n onValueChange(\n stepGridValue(value, step, event.key === 'ArrowUp' ? 1 : -1),\n )\n }}\n onBlur={(event) => {\n // Autofix a typed off-grid value to the closest grid point on blur.\n if (stepMinutes <= 1) return\n const snapped = snapToNearestStep(event.currentTarget.value, step)\n if (snapped !== event.currentTarget.value) onValueChange(snapped)\n }}\n aria-label={ariaLabel}\n className={cn(\n 'h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n />\n )\n },\n)\n\nTimePicker.displayName = 'TimePicker'\n\ninterface TimeRangePickerProps {\n start: string\n end: string\n onStartChange: (next: string) => void\n onEndChange: (next: string) => void\n disabled?: boolean\n startLabel?: string\n endLabel?: string\n /** Step in seconds, forwarded to both inputs (defaults to 60). */\n step?: number\n className?: string\n}\n\nfunction TimeRangePicker({\n className,\n disabled,\n end,\n endLabel = 'End',\n onEndChange,\n onStartChange,\n start,\n startLabel = 'Start',\n step,\n}: TimeRangePickerProps) {\n return (\n <div className={cn('grid grid-cols-2 gap-2', className)}>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">\n {startLabel}\n </span>\n <TimePicker\n value={start}\n onValueChange={onStartChange}\n disabled={disabled}\n step={step}\n aria-label={startLabel}\n />\n </label>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">{endLabel}</span>\n <TimePicker\n value={end}\n onValueChange={onEndChange}\n disabled={disabled}\n step={step}\n aria-label={endLabel}\n />\n </label>\n </div>\n )\n}\n\nexport { TimePicker, TimeRangePicker }\n"],"mappings":";;;;;;;;AAeA,MAAM,kBAAkB;;;;;;;;;AAUxB,SAAS,cAAc,OAAe,MAAc,WAA2B;CAC7E,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,IAAI,eAAe,GAAG,OAAO;CAC7B,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,MAAM,OAAO,QAAQ,OAAO,MAAM,EAAE,IAAI,KAAK,OAAO,MAAM,EAAE,IAAI;CAChE,MAAM,OACJ,cAAc,IACV,KAAK,MAAM,OAAO,WAAW,IAAI,cAAc,cAC/C,KAAK,KAAK,OAAO,WAAW,IAAI,cAAc;CACpD,MAAM,aACJ,KAAK,OAAO,kBAAkB,KAAK,WAAW,IAAI;CACpD,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC;CAGtD,OAAO,GAFI,OAAO,KAAK,MAAM,UAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAE7C,EAAE,GADF,OAAO,UAAU,EAAE,EAAE,SAAS,GAAG,GAC3B;AACnB;;;;;;AAOA,SAAS,kBAAkB,OAAe,MAAsB;CAC9D,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,IAAI,eAAe,GAAG,OAAO;CAC7B,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,UAAU,OAAO,MAAM,EAAE;CAC/B,IAAI,UAAU,gBAAgB,GAAG,OAAO;CACxC,MAAM,QAAQ,OAAO,MAAM,EAAE,IAAI,KAAK;CACtC,MAAM,aACJ,KAAK,OAAO,kBAAkB,KAAK,WAAW,IAAI;CACpD,MAAM,UAAU,KAAK,IACnB,KAAK,MAAM,QAAQ,WAAW,IAAI,aAClC,UACF;CAGA,OAAO,GAFI,OAAO,KAAK,MAAM,UAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAE7C,EAAE,GADF,OAAO,UAAU,EAAE,EAAE,SAAS,GAAG,GAC3B;AACnB;AAEA,MAAM,aAAaA,MAAM,YAErB,EACE,cAAc,WACd,WACA,UACA,IACA,eACA,OAAO,IACP,SAEF,QACG;CACH,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,OACE,iBAAA,GAAA,kBAAA,KAAC,SAAD;EACO;EACD;EACJ,MAAK;EACE;EACD;EACI;EACV,WAAW,UAAU,cAAc,MAAM,OAAO,KAAK;EACrD,YAAY,UAAU;GAGpB,IAAI,eAAe,GAAG;GACtB,IAAI,MAAM,QAAQ,aAAa,MAAM,QAAQ,aAAa;GAC1D,MAAM,eAAe;GACrB,cACE,cAAc,OAAO,MAAM,MAAM,QAAQ,YAAY,IAAI,EAAE,CAC7D;EACF;EACA,SAAS,UAAU;GAEjB,IAAI,eAAe,GAAG;GACtB,MAAM,UAAU,kBAAkB,MAAM,cAAc,OAAO,IAAI;GACjE,IAAI,YAAY,MAAM,cAAc,OAAO,cAAc,OAAO;EAClE;EACA,cAAY;EACZ,YAAA,GAAA,OAAA,IACE,
|
|
1
|
+
{"version":3,"file":"time-picker.cjs","names":["React"],"sources":["../../src/components/time-picker.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '../utils/cn'\n\ninterface TimePickerProps {\n /** `HH:MM` 24-hour string. */\n value: string\n onValueChange: (next: string) => void\n disabled?: boolean\n /** Step in seconds (defaults to 60 — minute-resolution). */\n step?: number\n className?: string\n id?: string\n 'aria-label'?: string\n}\n\nconst MINUTES_PER_DAY = 24 * 60\n\n/**\n * Move `HH:mm` one grid step in `direction`, aligning to the `step` grid first.\n * A native `<input type=\"time\">` steps each focused segment by one unit and\n * ignores `step`, so callers that want arrow keys to move by a coarse step\n * (e.g. 5 minutes) route the arrow keys through this instead. Clamped to\n * `00:00`–`23:55` (no wrap); an off-grid start lands on the next grid point in\n * the pressed direction.\n */\nfunction stepGridValue(value: string, step: number, direction: 1 | -1): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n const base = match ? Number(match[1]) * 60 + Number(match[2]) : 0\n const next =\n direction === 1\n ? Math.floor(base / stepMinutes) * stepMinutes + stepMinutes\n : Math.ceil(base / stepMinutes) * stepMinutes - stepMinutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const clamped = Math.max(0, Math.min(next, lastOnGrid))\n const hh = String(Math.floor(clamped / 60)).padStart(2, '0')\n const mm = String(clamped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n/**\n * Round `HH:mm` to the nearest multiple of `step` (used to autofix a typed\n * off-grid value on blur). Already-on-grid values pass through unchanged (so a\n * seconds suffix is preserved); anything that isn't `HH:mm(:ss)` passes through.\n */\nfunction snapToNearestStep(value: string, step: number): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n if (!match) return value\n const minutes = Number(match[2])\n if (minutes % stepMinutes === 0) return value\n const total = Number(match[1]) * 60 + minutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const snapped = Math.min(\n Math.round(total / stepMinutes) * stepMinutes,\n lastOnGrid,\n )\n const hh = String(Math.floor(snapped / 60)).padStart(2, '0')\n const mm = String(snapped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\nconst TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(\n (\n {\n 'aria-label': ariaLabel,\n className,\n disabled,\n id,\n onValueChange,\n step = 60,\n value,\n },\n ref,\n ) => {\n const stepMinutes = Math.floor(step / 60)\n return (\n <input\n ref={ref}\n id={id}\n type=\"time\"\n value={value}\n step={step}\n disabled={disabled}\n onChange={(event) => onValueChange(event.target.value)}\n onKeyDown={(event) => {\n // Native arrows step the focused segment by one unit and ignore step;\n // for a coarse grid, drive up/down through the grid instead.\n if (stepMinutes <= 1) return\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n event.preventDefault()\n onValueChange(\n stepGridValue(value, step, event.key === 'ArrowUp' ? 1 : -1),\n )\n }}\n onBlur={(event) => {\n // Autofix a typed off-grid value to the closest grid point on blur.\n if (stepMinutes <= 1) return\n const snapped = snapToNearestStep(event.currentTarget.value, step)\n if (snapped !== event.currentTarget.value) onValueChange(snapped)\n }}\n aria-label={ariaLabel}\n className={cn(\n 'touch-target h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n />\n )\n },\n)\n\nTimePicker.displayName = 'TimePicker'\n\ninterface TimeRangePickerProps {\n start: string\n end: string\n onStartChange: (next: string) => void\n onEndChange: (next: string) => void\n disabled?: boolean\n startLabel?: string\n endLabel?: string\n /** Step in seconds, forwarded to both inputs (defaults to 60). */\n step?: number\n className?: string\n}\n\nfunction TimeRangePicker({\n className,\n disabled,\n end,\n endLabel = 'End',\n onEndChange,\n onStartChange,\n start,\n startLabel = 'Start',\n step,\n}: TimeRangePickerProps) {\n return (\n <div className={cn('grid grid-cols-2 gap-2', className)}>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">\n {startLabel}\n </span>\n <TimePicker\n value={start}\n onValueChange={onStartChange}\n disabled={disabled}\n step={step}\n aria-label={startLabel}\n />\n </label>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">{endLabel}</span>\n <TimePicker\n value={end}\n onValueChange={onEndChange}\n disabled={disabled}\n step={step}\n aria-label={endLabel}\n />\n </label>\n </div>\n )\n}\n\nexport { TimePicker, TimeRangePicker }\n"],"mappings":"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"}
|
|
@@ -61,7 +61,7 @@ const TimePicker = React$1.forwardRef(({ "aria-label": ariaLabel, className, dis
|
|
|
61
61
|
if (snapped !== event.currentTarget.value) onValueChange(snapped);
|
|
62
62
|
},
|
|
63
63
|
"aria-label": ariaLabel,
|
|
64
|
-
className: cn("h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className)
|
|
64
|
+
className: cn("touch-target h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50", className)
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
TimePicker.displayName = "TimePicker";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-picker.js","names":["React"],"sources":["../../src/components/time-picker.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '../utils/cn'\n\ninterface TimePickerProps {\n /** `HH:MM` 24-hour string. */\n value: string\n onValueChange: (next: string) => void\n disabled?: boolean\n /** Step in seconds (defaults to 60 — minute-resolution). */\n step?: number\n className?: string\n id?: string\n 'aria-label'?: string\n}\n\nconst MINUTES_PER_DAY = 24 * 60\n\n/**\n * Move `HH:mm` one grid step in `direction`, aligning to the `step` grid first.\n * A native `<input type=\"time\">` steps each focused segment by one unit and\n * ignores `step`, so callers that want arrow keys to move by a coarse step\n * (e.g. 5 minutes) route the arrow keys through this instead. Clamped to\n * `00:00`–`23:55` (no wrap); an off-grid start lands on the next grid point in\n * the pressed direction.\n */\nfunction stepGridValue(value: string, step: number, direction: 1 | -1): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n const base = match ? Number(match[1]) * 60 + Number(match[2]) : 0\n const next =\n direction === 1\n ? Math.floor(base / stepMinutes) * stepMinutes + stepMinutes\n : Math.ceil(base / stepMinutes) * stepMinutes - stepMinutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const clamped = Math.max(0, Math.min(next, lastOnGrid))\n const hh = String(Math.floor(clamped / 60)).padStart(2, '0')\n const mm = String(clamped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n/**\n * Round `HH:mm` to the nearest multiple of `step` (used to autofix a typed\n * off-grid value on blur). Already-on-grid values pass through unchanged (so a\n * seconds suffix is preserved); anything that isn't `HH:mm(:ss)` passes through.\n */\nfunction snapToNearestStep(value: string, step: number): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n if (!match) return value\n const minutes = Number(match[2])\n if (minutes % stepMinutes === 0) return value\n const total = Number(match[1]) * 60 + minutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const snapped = Math.min(\n Math.round(total / stepMinutes) * stepMinutes,\n lastOnGrid,\n )\n const hh = String(Math.floor(snapped / 60)).padStart(2, '0')\n const mm = String(snapped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\nconst TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(\n (\n {\n 'aria-label': ariaLabel,\n className,\n disabled,\n id,\n onValueChange,\n step = 60,\n value,\n },\n ref,\n ) => {\n const stepMinutes = Math.floor(step / 60)\n return (\n <input\n ref={ref}\n id={id}\n type=\"time\"\n value={value}\n step={step}\n disabled={disabled}\n onChange={(event) => onValueChange(event.target.value)}\n onKeyDown={(event) => {\n // Native arrows step the focused segment by one unit and ignore step;\n // for a coarse grid, drive up/down through the grid instead.\n if (stepMinutes <= 1) return\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n event.preventDefault()\n onValueChange(\n stepGridValue(value, step, event.key === 'ArrowUp' ? 1 : -1),\n )\n }}\n onBlur={(event) => {\n // Autofix a typed off-grid value to the closest grid point on blur.\n if (stepMinutes <= 1) return\n const snapped = snapToNearestStep(event.currentTarget.value, step)\n if (snapped !== event.currentTarget.value) onValueChange(snapped)\n }}\n aria-label={ariaLabel}\n className={cn(\n 'h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n />\n )\n },\n)\n\nTimePicker.displayName = 'TimePicker'\n\ninterface TimeRangePickerProps {\n start: string\n end: string\n onStartChange: (next: string) => void\n onEndChange: (next: string) => void\n disabled?: boolean\n startLabel?: string\n endLabel?: string\n /** Step in seconds, forwarded to both inputs (defaults to 60). */\n step?: number\n className?: string\n}\n\nfunction TimeRangePicker({\n className,\n disabled,\n end,\n endLabel = 'End',\n onEndChange,\n onStartChange,\n start,\n startLabel = 'Start',\n step,\n}: TimeRangePickerProps) {\n return (\n <div className={cn('grid grid-cols-2 gap-2', className)}>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">\n {startLabel}\n </span>\n <TimePicker\n value={start}\n onValueChange={onStartChange}\n disabled={disabled}\n step={step}\n aria-label={startLabel}\n />\n </label>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">{endLabel}</span>\n <TimePicker\n value={end}\n onValueChange={onEndChange}\n disabled={disabled}\n step={step}\n aria-label={endLabel}\n />\n </label>\n </div>\n )\n}\n\nexport { TimePicker, TimeRangePicker }\n"],"mappings":";;;;;AAeA,MAAM,kBAAkB;;;;;;;;;AAUxB,SAAS,cAAc,OAAe,MAAc,WAA2B;CAC7E,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,IAAI,eAAe,GAAG,OAAO;CAC7B,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,MAAM,OAAO,QAAQ,OAAO,MAAM,EAAE,IAAI,KAAK,OAAO,MAAM,EAAE,IAAI;CAChE,MAAM,OACJ,cAAc,IACV,KAAK,MAAM,OAAO,WAAW,IAAI,cAAc,cAC/C,KAAK,KAAK,OAAO,WAAW,IAAI,cAAc;CACpD,MAAM,aACJ,KAAK,OAAO,kBAAkB,KAAK,WAAW,IAAI;CACpD,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,UAAU,CAAC;CAGtD,OAAO,GAFI,OAAO,KAAK,MAAM,UAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAE7C,EAAE,GADF,OAAO,UAAU,EAAE,EAAE,SAAS,GAAG,GAC3B;AACnB;;;;;;AAOA,SAAS,kBAAkB,OAAe,MAAsB;CAC9D,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,IAAI,eAAe,GAAG,OAAO;CAC7B,MAAM,QAAQ,qBAAqB,KAAK,KAAK;CAC7C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,UAAU,OAAO,MAAM,EAAE;CAC/B,IAAI,UAAU,gBAAgB,GAAG,OAAO;CACxC,MAAM,QAAQ,OAAO,MAAM,EAAE,IAAI,KAAK;CACtC,MAAM,aACJ,KAAK,OAAO,kBAAkB,KAAK,WAAW,IAAI;CACpD,MAAM,UAAU,KAAK,IACnB,KAAK,MAAM,QAAQ,WAAW,IAAI,aAClC,UACF;CAGA,OAAO,GAFI,OAAO,KAAK,MAAM,UAAU,EAAE,CAAC,EAAE,SAAS,GAAG,GAE7C,EAAE,GADF,OAAO,UAAU,EAAE,EAAE,SAAS,GAAG,GAC3B;AACnB;AAEA,MAAM,aAAaA,QAAM,YAErB,EACE,cAAc,WACd,WACA,UACA,IACA,eACA,OAAO,IACP,SAEF,QACG;CACH,MAAM,cAAc,KAAK,MAAM,OAAO,EAAE;CACxC,OACE,oBAAC,SAAD;EACO;EACD;EACJ,MAAK;EACE;EACD;EACI;EACV,WAAW,UAAU,cAAc,MAAM,OAAO,KAAK;EACrD,YAAY,UAAU;GAGpB,IAAI,eAAe,GAAG;GACtB,IAAI,MAAM,QAAQ,aAAa,MAAM,QAAQ,aAAa;GAC1D,MAAM,eAAe;GACrB,cACE,cAAc,OAAO,MAAM,MAAM,QAAQ,YAAY,IAAI,EAAE,CAC7D;EACF;EACA,SAAS,UAAU;GAEjB,IAAI,eAAe,GAAG;GACtB,MAAM,UAAU,kBAAkB,MAAM,cAAc,OAAO,IAAI;GACjE,IAAI,YAAY,MAAM,cAAc,OAAO,cAAc,OAAO;EAClE;EACA,cAAY;EACZ,WAAW,GACT,
|
|
1
|
+
{"version":3,"file":"time-picker.js","names":["React"],"sources":["../../src/components/time-picker.tsx"],"sourcesContent":["import * as React from 'react'\nimport { cn } from '../utils/cn'\n\ninterface TimePickerProps {\n /** `HH:MM` 24-hour string. */\n value: string\n onValueChange: (next: string) => void\n disabled?: boolean\n /** Step in seconds (defaults to 60 — minute-resolution). */\n step?: number\n className?: string\n id?: string\n 'aria-label'?: string\n}\n\nconst MINUTES_PER_DAY = 24 * 60\n\n/**\n * Move `HH:mm` one grid step in `direction`, aligning to the `step` grid first.\n * A native `<input type=\"time\">` steps each focused segment by one unit and\n * ignores `step`, so callers that want arrow keys to move by a coarse step\n * (e.g. 5 minutes) route the arrow keys through this instead. Clamped to\n * `00:00`–`23:55` (no wrap); an off-grid start lands on the next grid point in\n * the pressed direction.\n */\nfunction stepGridValue(value: string, step: number, direction: 1 | -1): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n const base = match ? Number(match[1]) * 60 + Number(match[2]) : 0\n const next =\n direction === 1\n ? Math.floor(base / stepMinutes) * stepMinutes + stepMinutes\n : Math.ceil(base / stepMinutes) * stepMinutes - stepMinutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const clamped = Math.max(0, Math.min(next, lastOnGrid))\n const hh = String(Math.floor(clamped / 60)).padStart(2, '0')\n const mm = String(clamped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n/**\n * Round `HH:mm` to the nearest multiple of `step` (used to autofix a typed\n * off-grid value on blur). Already-on-grid values pass through unchanged (so a\n * seconds suffix is preserved); anything that isn't `HH:mm(:ss)` passes through.\n */\nfunction snapToNearestStep(value: string, step: number): string {\n const stepMinutes = Math.floor(step / 60)\n if (stepMinutes <= 1) return value\n const match = /^(\\d{1,2}):(\\d{2})/.exec(value)\n if (!match) return value\n const minutes = Number(match[2])\n if (minutes % stepMinutes === 0) return value\n const total = Number(match[1]) * 60 + minutes\n const lastOnGrid =\n Math.floor((MINUTES_PER_DAY - 1) / stepMinutes) * stepMinutes\n const snapped = Math.min(\n Math.round(total / stepMinutes) * stepMinutes,\n lastOnGrid,\n )\n const hh = String(Math.floor(snapped / 60)).padStart(2, '0')\n const mm = String(snapped % 60).padStart(2, '0')\n return `${hh}:${mm}`\n}\n\nconst TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(\n (\n {\n 'aria-label': ariaLabel,\n className,\n disabled,\n id,\n onValueChange,\n step = 60,\n value,\n },\n ref,\n ) => {\n const stepMinutes = Math.floor(step / 60)\n return (\n <input\n ref={ref}\n id={id}\n type=\"time\"\n value={value}\n step={step}\n disabled={disabled}\n onChange={(event) => onValueChange(event.target.value)}\n onKeyDown={(event) => {\n // Native arrows step the focused segment by one unit and ignore step;\n // for a coarse grid, drive up/down through the grid instead.\n if (stepMinutes <= 1) return\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n event.preventDefault()\n onValueChange(\n stepGridValue(value, step, event.key === 'ArrowUp' ? 1 : -1),\n )\n }}\n onBlur={(event) => {\n // Autofix a typed off-grid value to the closest grid point on blur.\n if (stepMinutes <= 1) return\n const snapped = snapToNearestStep(event.currentTarget.value, step)\n if (snapped !== event.currentTarget.value) onValueChange(snapped)\n }}\n aria-label={ariaLabel}\n className={cn(\n 'touch-target h-10 w-full rounded-md bg-base-gray-soft px-3 text-base font-medium text-gray tabular-nums transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n />\n )\n },\n)\n\nTimePicker.displayName = 'TimePicker'\n\ninterface TimeRangePickerProps {\n start: string\n end: string\n onStartChange: (next: string) => void\n onEndChange: (next: string) => void\n disabled?: boolean\n startLabel?: string\n endLabel?: string\n /** Step in seconds, forwarded to both inputs (defaults to 60). */\n step?: number\n className?: string\n}\n\nfunction TimeRangePicker({\n className,\n disabled,\n end,\n endLabel = 'End',\n onEndChange,\n onStartChange,\n start,\n startLabel = 'Start',\n step,\n}: TimeRangePickerProps) {\n return (\n <div className={cn('grid grid-cols-2 gap-2', className)}>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">\n {startLabel}\n </span>\n <TimePicker\n value={start}\n onValueChange={onStartChange}\n disabled={disabled}\n step={step}\n aria-label={startLabel}\n />\n </label>\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-gray-light\">{endLabel}</span>\n <TimePicker\n value={end}\n onValueChange={onEndChange}\n disabled={disabled}\n step={step}\n aria-label={endLabel}\n />\n </label>\n </div>\n )\n}\n\nexport { TimePicker, TimeRangePicker }\n"],"mappings":"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"}
|
|
@@ -62,7 +62,7 @@ const InfoTooltip = ({ content, label, "aria-label": ariaLabel }) => {
|
|
|
62
62
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipTrigger, {
|
|
63
63
|
render: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
64
64
|
"aria-label": ariaLabel ?? label,
|
|
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",
|
|
65
|
+
className: "focus-visible:ring-primary touch-target 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
68
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_epilot_spark_icons.Info, { className: "text-sm!" })
|