@seekalfred/primitives 0.7.1 → 0.7.4
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/README.md +255 -5
- package/dist/alert/alert.d.ts +49 -4
- package/dist/alert/alert.js +66 -38
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert/index.d.ts +2 -2
- package/dist/alert/index.js +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +31 -4
- package/dist/alert-dialog/alert-dialog.js +75 -26
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +1 -1
- package/dist/calendar/calendar.d.ts +1 -1
- package/dist/card/card.d.ts +22 -1
- package/dist/card/card.js +60 -7
- package/dist/card/card.js.map +1 -1
- package/dist/carousel/carousel.d.ts +21 -0
- package/dist/carousel/carousel.js +34 -8
- package/dist/carousel/carousel.js.map +1 -1
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/copy-button/copy-button.d.ts +37 -5
- package/dist/copy-button/copy-button.js +35 -11
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/donut-chart/donut-chart.d.ts +22 -0
- package/dist/donut-chart/donut-chart.js +35 -18
- package/dist/donut-chart/donut-chart.js.map +1 -1
- package/dist/donut-chart/index.d.ts +2 -2
- package/dist/empty-state/empty-state.d.ts +21 -0
- package/dist/empty-state/empty-state.js +16 -4
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/empty-state/index.d.ts +2 -2
- package/dist/empty-state/index.js +2 -2
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/filter-bar/filter-bar.d.ts +4 -1
- package/dist/filter-bar/filter-bar.js +53 -9
- package/dist/filter-bar/filter-bar.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +15 -14
- package/dist/index.js +9 -9
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +7 -5
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +6 -3
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/field-surface.js +22 -1
- package/dist/lib/field-surface.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/use-copy-feedback.js +17 -3
- package/dist/lib/use-copy-feedback.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +144 -2
- package/dist/line-chart/line-chart.js +314 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +36 -1
- package/dist/money-input/money-input.js +30 -14
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +26 -1
- package/dist/morph-select/morph-select.js +28 -6
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/index.d.ts +2 -2
- package/dist/multi-select/index.js +2 -2
- package/dist/multi-select/multi-select.d.ts +74 -2
- package/dist/multi-select/multi-select.js +213 -39
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/password-field/password-field.d.ts +16 -0
- package/dist/password-field/password-field.js +37 -8
- package/dist/password-field/password-field.js.map +1 -1
- package/dist/popover/index.d.ts +2 -2
- package/dist/popover/index.js +2 -2
- package/dist/popover/popover.d.ts +39 -2
- package/dist/popover/popover.js +29 -2
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +68 -3
- package/dist/progress/progress.js +156 -39
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/index.d.ts +2 -2
- package/dist/radio-group/index.js +2 -2
- package/dist/radio-group/radio-group.d.ts +33 -3
- package/dist/radio-group/radio-group.js +109 -9
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/scroll-area/scroll-area.js +4 -3
- package/dist/scroll-area/scroll-area.js.map +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.d.ts +22 -1
- package/dist/search-field/search-field.js +22 -7
- package/dist/search-field/search-field.js.map +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +11 -8
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +21 -9
- package/dist/separator/separator.js +33 -11
- package/dist/separator/separator.js.map +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +65 -1
- package/dist/stat-card/stat-card.js +98 -25
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +115 -5
- package/dist/table/table.js +374 -94
- package/dist/table/table.js.map +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +3 -1
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/time-picker/time-picker.d.ts +36 -1
- package/dist/time-picker/time-picker.js +33 -5
- package/dist/time-picker/time-picker.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/usage-meter/index.d.ts +2 -2
- package/dist/usage-meter/usage-meter.d.ts +30 -0
- package/dist/usage-meter/usage-meter.js +56 -38
- package/dist/usage-meter/usage-meter.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"carousel.js","names":[],"sources":["../../src/carousel/carousel.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Children,\n forwardRef,\n isValidElement,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { useIsomorphicLayoutEffect, useReducedMotion } from \"../lib/motion.js\";\n\n/**\n * Carousel: a row of slides people browse by swiping, a trackpad, dragging\n * with a mouse, the arrow keys, or the controls below it. Use it for a short,\n * ordered set of peers such as products or case studies, where the neighbours\n * stay in view as a hint that there is more.\n *\n * The slides sit on a native scroll-snap track, so touch and trackpad swipes\n * are the browser's own. The active slide is centred; its neighbours shrink and\n * dim with their distance, and each page indicator stretches into a pill as the\n * track nears its slide, all written straight from the scroll position without a\n * re-render. A mouse can drag the track, and a flick carries on to the next\n * slide. Arrow keys, Home and End move between slides from the track or the\n * indicators. `interval` adds a play control that rotates through the slides;\n * rotation pauses on hover, keyboard focus and touch, while the carousel is off\n * screen or the tab is hidden, and is unavailable under reduced motion (where\n * scrolling also jumps instead of gliding).\n */\nexport interface CarouselProps {\n /** Names the carousel for assistive technology, such as \"From the Harbour suites\". */\n label: string;\n /** One child per slide. */\n children: ReactNode;\n /** Controlled active slide. Pair with `onIndexChange`. */\n index?: number;\n /** @default 0 */\n defaultIndex?: number;\n onIndexChange?: (index: number) => void;\n /** Width of one slide as a CSS length; `cqw` is a share of the carousel's width. @default \"min(80cqw, 340px)\" */\n slideSize?: string;\n /** Names each slide and its indicator. @default (index, count) => `${index + 1} of ${count}` */\n slideLabel?: (index: number, count: number) => string;\n /** Milliseconds per slide. Adds a play control that rotates through the slides. */\n interval?: number;\n /** Starts rotating on mount. Off by default, so motion begins with the viewer. @default false */\n autoplay?: boolean;\n /** Names of the controls, for translation. */\n labels?: {\n /** @default \"Previous slide\" */\n previous?: string;\n /** @default \"Next slide\" */\n next?: string;\n /** @default \"Start slide rotation\" */\n play?: string;\n /** @default \"Pause slide rotation\" */\n pause?: string;\n /** Names the group of indicators. @default \"Choose a slide\" */\n slides?: string;\n };\n /** Extra classes for the carousel. Merged last. */\n className?: string;\n}\n\n/** Movement before a mouse press is read as a drag. */\nconst SLOP = 6;\n/** Release speed in px/s that always moves at least one slide. */\nconst FLICK = 360;\n/** Where a flick would come to rest: velocity (px/s) projected with a paging deceleration rate. */\nconst project = (velocity: number, rate = 0.995) => ((velocity / 1000) * rate) / (1 - rate);\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\nconst defaultSlideLabel = (index: number, count: number) => `${index + 1} of ${count}`;\n\nconst PlayGlyph = () => (\n <svg width={15} height={15} viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5Z\" />\n </svg>\n);\nconst PauseGlyph = () => (\n <svg width={15} height={15} viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"3.5\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\"1\" />\n <rect x=\"9.3\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\"1\" />\n </svg>\n);\nconst Chevron = ({ back }: { back?: boolean }) => (\n <svg width={18} height={18} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d={back ? \"M10 3.5 5.5 8l4.5 4.5\" : \"M6 3.5 10.5 8 6 12.5\"} />\n </svg>\n);\n\ntype Drag = { pointer: number; startX: number; startLeft: number; from: number; locked: boolean; samples: [number, number][] };\n\nexport const Carousel = forwardRef<HTMLElement, CarouselProps>(function Carousel(\n {\n label,\n children,\n index: controlledIndex,\n defaultIndex = 0,\n onIndexChange,\n slideSize,\n slideLabel = defaultSlideLabel,\n interval,\n autoplay = false,\n labels,\n className,\n },\n ref,\n) {\n const slides = Children.toArray(children);\n const count = slides.length;\n const last = Math.max(0, count - 1);\n const id = useId();\n const reduced = useReducedMotion();\n const [ownIndex, setOwnIndex] = useState(() => clamp(defaultIndex, 0, last));\n const index = clamp(controlledIndex ?? ownIndex, 0, last);\n const [playing, setPlaying] = useState(autoplay);\n const [hovered, setHovered] = useState(false);\n const [touching, setTouching] = useState(false);\n const [focusPaused, setFocusPaused] = useState(false);\n const [inView, setInView] = useState(true);\n const [pageVisible, setPageVisible] = useState(true);\n const [announcement, setAnnouncement] = useState(\"\");\n\n const root = useRef<HTMLElement | null>(null);\n const rootRef = useMemo(() => mergeRefs(ref, root), [ref]);\n const viewport = useRef<HTMLDivElement>(null);\n const track = useRef<HTMLDivElement>(null);\n const play = useRef<HTMLButtonElement>(null);\n const tabs = useRef<(HTMLButtonElement | null)[]>([]);\n // The slide the carousel is on, as the component last decided it (state lags a render behind).\n const target = useRef(index);\n // Set while a scroll we started is in flight, so the in-between slides are not reported as the viewer's choice.\n const travelling = useRef<number | null>(null);\n const backstop = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const drag = useRef<Drag | null>(null);\n const swallowClick = useRef(false);\n\n const canRotate = Boolean(interval) && count > 1 && !reduced;\n const running = canRotate && playing && !hovered && !touching && !focusPaused && inView && pageVisible;\n\n const step = () => {\n const [first, second] = Array.from(track.current?.children ?? []) as HTMLElement[];\n return first && second ? second.offsetLeft - first.offsetLeft : (first?.offsetWidth ?? 0);\n };\n /** The scroll position that centres slide `i`. */\n const leftOf = (i: number) => {\n const node = track.current?.children[i] as HTMLElement | undefined;\n const box = viewport.current;\n return node && box ? node.offsetLeft - (box.clientWidth - node.offsetWidth) / 2 : 0;\n };\n\n /** Writes how far each slide and indicator is from the centre, straight from the scroll position. Returns the position in slides. */\n const paint = useCallback(() => {\n const box = viewport.current;\n const size = step();\n if (!box || !size) return target.current;\n const position = box.scrollLeft / size;\n Array.from(track.current?.children ?? []).forEach((slide, i) => {\n (slide as HTMLElement).style.setProperty(\"--d\", String(Math.min(1, Math.abs(position - i))));\n });\n tabs.current.forEach((tab, i) => {\n const dot = tab?.firstElementChild;\n if (dot instanceof HTMLElement) dot.style.setProperty(\"--e\", String(Math.max(0, 1 - Math.abs(clamp(position, 0, last) - i))));\n });\n return position;\n }, [last]);\n\n function scrollToIndex(i: number, smooth: boolean) {\n const box = viewport.current;\n if (!box) return;\n const left = leftOf(i);\n if (!smooth || reduced || Math.abs(box.scrollLeft - left) < 1) {\n travelling.current = null;\n box.scrollTo({ left, behavior: \"auto\" });\n paint();\n return;\n }\n travelling.current = i;\n clearTimeout(backstop.current);\n // A scroll that never reports arriving (a resize mid-flight) must not hold the carousel deaf to the viewer's own swipes.\n backstop.current = setTimeout(() => (travelling.current = null), 1000);\n box.scrollTo({ left, behavior: \"smooth\" });\n }\n\n function commit(next: number, announce: boolean) {\n target.current = next;\n if (controlledIndex === undefined) setOwnIndex(next);\n onIndexChange?.(next);\n if (announce) setAnnouncement(slideLabel(next, count));\n }\n\n function select(to: number, announce = true) {\n const next = clamp(to, 0, last);\n if (next !== target.current) commit(next, announce);\n scrollToIndex(next, true);\n }\n\n // The start slide is in place before first paint, so nothing glides on mount; later resizes keep the carousel on its slide.\n useIsomorphicLayoutEffect(() => {\n scrollToIndex(target.current, false);\n const box = viewport.current;\n if (!box || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n if (!drag.current && travelling.current === null) scrollToIndex(target.current, false);\n });\n observer.observe(box);\n return () => observer.disconnect();\n }, []);\n\n // The viewer's own slides, inert unless active, are the only ones in the tab order or the accessibility tree's reach.\n useIsomorphicLayoutEffect(() => {\n Array.from(track.current?.children ?? []).forEach((slide, i) => slide.toggleAttribute(\"inert\", i !== index));\n }, [index, count]);\n\n // A controlled index that moves by itself brings the carousel along.\n useEffect(() => {\n if (target.current !== index) {\n target.current = index;\n scrollToIndex(index, true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only a changed index matters\n }, [index]);\n\n useEffect(() => () => clearTimeout(backstop.current), []);\n\n function onScroll() {\n const position = paint();\n const box = viewport.current;\n if (!box || drag.current?.locked) return;\n if (travelling.current !== null) {\n if (Math.abs(box.scrollLeft - leftOf(travelling.current)) < 2) travelling.current = null;\n return;\n }\n const next = clamp(Math.round(position), 0, last);\n if (next !== target.current) commit(next, true);\n }\n\n const advance = () => select(index >= last ? 0 : index + 1, false);\n\n useEffect(() => {\n const node = root.current;\n if (!interval || !node) return;\n const observer = typeof IntersectionObserver === \"undefined\" ? null : new IntersectionObserver(([entry]) => entry && setInView(entry.isIntersecting));\n observer?.observe(node);\n const onVisibility = () => setPageVisible(document.visibilityState !== \"hidden\");\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => {\n observer?.disconnect();\n document.removeEventListener(\"visibilitychange\", onVisibility);\n };\n }, [interval]);\n\n function onPointerDown(event: PointerEvent<HTMLDivElement>) {\n if (count < 2 || drag.current || event.pointerType !== \"mouse\" || event.button !== 0) return;\n swallowClick.current = false;\n drag.current = { pointer: event.pointerId, startX: event.clientX, startLeft: event.currentTarget.scrollLeft, from: target.current, locked: false, samples: [] };\n }\n\n function onPointerMove(event: PointerEvent<HTMLDivElement>) {\n const current = drag.current;\n if (!current || current.pointer !== event.pointerId) return;\n const box = event.currentTarget;\n if (!current.locked) {\n if (Math.abs(event.clientX - current.startX) < SLOP) return;\n // The slop is consumed, so the track starts moving from the pointer without a jump. Snapping is off while it is held.\n current.locked = true;\n current.startX = event.clientX;\n travelling.current = null;\n try {\n box.setPointerCapture(event.pointerId);\n } catch {\n // The pointer already ended; the drag simply stops following it.\n }\n box.dataset.dragging = \"\";\n }\n box.scrollLeft = current.startLeft - (event.clientX - current.startX);\n current.samples.push([event.timeStamp, box.scrollLeft]);\n if (current.samples.length > 16) current.samples.shift();\n }\n\n function finish(event: PointerEvent<HTMLDivElement>, cancelled: boolean) {\n const current = drag.current;\n if (!current || current.pointer !== event.pointerId) return;\n drag.current = null;\n if (!current.locked) return;\n const box = event.currentTarget;\n delete box.dataset.dragging;\n swallowClick.current = true;\n const recent = current.samples.filter(([time]) => event.timeStamp - time <= 90);\n const first = recent[0];\n const lastSample = recent[recent.length - 1];\n const velocity =\n !cancelled && first && lastSample && recent.length > 1 && event.timeStamp - lastSample[0] <= 50 && lastSample[0] > first[0]\n ? ((lastSample[1] - first[1]) / (lastSample[0] - first[0])) * 1000\n : 0;\n const size = step() || 1;\n // The landing slide comes from where the flick would carry the track, not from where the pointer let go.\n let to = Math.round((box.scrollLeft + project(velocity)) / size);\n if (Math.abs(velocity) > FLICK) {\n const direction = velocity > 0 ? 1 : -1;\n if ((to - current.from) * direction < 1) to = current.from + direction;\n }\n select(to);\n }\n\n /** A tap on a neighbour brings it to the centre; a click that ends a drag is not a tap. */\n function onClick(event: MouseEvent<HTMLDivElement>) {\n if (swallowClick.current) {\n swallowClick.current = false;\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n const hit = Array.from(track.current?.children ?? []).findIndex((slide) => {\n const rect = slide.getBoundingClientRect();\n return event.clientX >= rect.left && event.clientX <= rect.right;\n });\n if (hit >= 0 && hit !== target.current) select(hit);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLElement>) {\n if (event.altKey || event.ctrlKey || event.metaKey || count < 2) return;\n const node = event.target as HTMLElement;\n if (node.closest(\"input, textarea, select, [contenteditable]\")) return;\n const from = target.current;\n const to =\n event.key === \"ArrowLeft\" ? from - 1 : event.key === \"ArrowRight\" ? from + 1 : event.key === \"Home\" ? 0 : event.key === \"End\" ? last : null;\n if (to === null) return;\n event.preventDefault();\n const next = clamp(to, 0, last);\n select(next);\n if (node.getAttribute(\"role\") === \"tab\") tabs.current[next]?.focus({ preventScroll: true });\n else if (track.current?.contains(node)) viewport.current?.focus({ preventScroll: true });\n }\n\n const text = { previous: \"Previous slide\", next: \"Next slide\", play: \"Start slide rotation\", pause: \"Pause slide rotation\", slides: \"Choose a slide\", ...labels };\n\n return (\n <section\n ref={rootRef}\n data-slot=\"carousel\"\n aria-roledescription=\"carousel\"\n aria-label={label}\n className={cn(\"carousel\", className)}\n style={slideSize ? ({ \"--carousel-slide-size\": slideSize } as CSSProperties) : undefined}\n onKeyDown={onKeyDown}\n onFocus={(event) => setFocusPaused(event.target !== play.current && event.target.matches(\":focus-visible\"))}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) setFocusPaused(false);\n }}\n >\n <div\n ref={viewport}\n role=\"region\"\n aria-label=\"Slides\"\n tabIndex={0}\n data-slot=\"carousel-viewport\"\n className=\"carousel-viewport\"\n onScroll={onScroll}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={(event) => finish(event, false)}\n onPointerCancel={(event) => finish(event, true)}\n onPointerEnter={(event) => event.pointerType === \"mouse\" && setHovered(true)}\n onPointerLeave={() => setHovered(false)}\n onTouchStart={() => setTouching(true)}\n onTouchEnd={() => setTouching(false)}\n onTouchCancel={() => setTouching(false)}\n onClickCapture={onClick}\n onDragStart={(event) => event.preventDefault()}\n >\n <div ref={track} className=\"carousel-track\">\n {slides.map((slide, i) => (\n <div\n key={isValidElement(slide) && slide.key != null ? slide.key : i}\n id={`${id}-slide-${i}`}\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-label={slideLabel(i, count)}\n data-active={i === index || undefined}\n className=\"carousel-slide\"\n >\n {slide}\n </div>\n ))}\n </div>\n </div>\n {count > 1 ? (\n <div className=\"carousel-controls\">\n {canRotate ? (\n <button\n ref={play}\n type=\"button\"\n aria-label={playing ? text.pause : text.play}\n className=\"carousel-control\"\n onClick={() => setPlaying(!playing)}\n >\n {playing ? <PauseGlyph /> : <PlayGlyph />}\n </button>\n ) : null}\n <div role=\"tablist\" aria-label={text.slides} className=\"carousel-tabs\">\n {slides.map((_, i) => (\n <button\n key={i}\n ref={(node) => {\n tabs.current[i] = node;\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={i === index}\n aria-controls={`${id}-slide-${i}`}\n aria-label={slideLabel(i, count)}\n tabIndex={i === index ? 0 : -1}\n className=\"carousel-tab\"\n onClick={() => select(i)}\n >\n <span aria-hidden=\"true\" className=\"carousel-dot\">\n <span className=\"carousel-dot-fill\" />\n {canRotate && playing && i === index ? (\n <span\n key={index}\n className=\"carousel-drain\"\n style={{ animationDuration: `${interval}ms`, animationPlayState: running ? \"running\" : \"paused\" }}\n onAnimationEnd={advance}\n />\n ) : null}\n </span>\n </button>\n ))}\n </div>\n <div className=\"carousel-arrows\">\n <button\n type=\"button\"\n aria-label={text.previous}\n aria-disabled={index === 0}\n className=\"carousel-control\"\n onClick={() => target.current > 0 && select(target.current - 1)}\n >\n <Chevron back />\n </button>\n <button\n type=\"button\"\n aria-label={text.next}\n aria-disabled={index === last}\n className=\"carousel-control\"\n onClick={() => target.current < last && select(target.current + 1)}\n >\n <Chevron />\n </button>\n </div>\n </div>\n ) : null}\n <span className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcement}\n </span>\n </section>\n );\n});\n"],"mappings":";;;;;;;;AA4EA,MAAM,OAAO;;AAEb,MAAM,QAAQ;;AAEd,MAAM,WAAW,UAAkB,OAAO,SAAY,WAAW,MAAQ,QAAS,IAAI;AACtF,MAAM,SAAS,OAAe,KAAa,QAAgB,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,KAAK,CAAC;AAC7F,MAAM,qBAAqB,OAAe,UAAkB,GAAG,QAAQ,EAAE,MAAM;AAE/E,MAAM,kBACJ,oBAAC,OAAD;CAAK,OAAO;CAAI,QAAQ;CAAI,SAAQ;CAAY,MAAK;CAAe,eAAY;CAC9E,UAAA,oBAAC,QAAD,EAAM,GAAE,gFAAiF,CAAA;AACtF,CAAA;AAEP,MAAM,mBACJ,qBAAC,OAAD;CAAK,OAAO;CAAI,QAAQ;CAAI,SAAQ;CAAY,MAAK;CAAe,eAAY;CAAhF,UAAA,CACE,oBAAC,QAAD;EAAM,GAAE;EAAM,GAAE;EAAM,OAAM;EAAM,QAAO;EAAK,IAAG;CAAK,CAAA,GACtD,oBAAC,QAAD;EAAM,GAAE;EAAM,GAAE;EAAM,OAAM;EAAM,QAAO;EAAK,IAAG;CAAK,CAAA,CACnD;;AAEP,MAAM,WAAW,EAAE,WACjB,oBAAC,OAAD;CAAK,OAAO;CAAI,QAAQ;CAAI,SAAQ;CAAY,MAAK;CAAO,QAAO;CAAe,aAAa;CAAM,eAAc;CAAQ,gBAAe;CAAQ,eAAY;CAC5J,UAAA,oBAAC,QAAD,EAAM,GAAG,OAAO,0BAA0B,uBAAyB,CAAA;AAChE,CAAA;AAKP,MAAa,WAAW,WAAuC,SAAS,SACtE,EACE,OACA,UACA,OAAO,iBACP,eAAe,GACf,eACA,WACA,aAAa,mBACb,UACA,WAAW,OACX,QACA,aAEF,KACA;CACA,MAAM,SAAS,SAAS,QAAQ,QAAQ;CACxC,MAAM,QAAQ,OAAO;CACrB,MAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,CAAC;CAClC,MAAM,KAAK,MAAM;CACjB,MAAM,UAAU,iBAAiB;CACjC,MAAM,CAAC,UAAU,eAAe,eAAe,MAAM,cAAc,GAAG,IAAI,CAAC;CAC3E,MAAM,QAAQ,MAAM,mBAAmB,UAAU,GAAG,IAAI;CACxD,MAAM,CAAC,SAAS,cAAc,SAAS,QAAQ;CAC/C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,CAAC,aAAa,kBAAkB,SAAS,KAAK;CACpD,MAAM,CAAC,QAAQ,aAAa,SAAS,IAAI;CACzC,MAAM,CAAC,aAAa,kBAAkB,SAAS,IAAI;CACnD,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CAEnD,MAAM,OAAO,OAA2B,IAAI;CAC5C,MAAM,UAAU,cAAc,UAAU,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC;CACzD,MAAM,WAAW,OAAuB,IAAI;CAC5C,MAAM,QAAQ,OAAuB,IAAI;CACzC,MAAM,OAAO,OAA0B,IAAI;CAC3C,MAAM,OAAO,OAAqC,CAAC,CAAC;CAEpD,MAAM,SAAS,OAAO,KAAK;CAE3B,MAAM,aAAa,OAAsB,IAAI;CAC7C,MAAM,WAAW,OAAkD,KAAA,CAAS;CAC5E,MAAM,OAAO,OAAoB,IAAI;CACrC,MAAM,eAAe,OAAO,KAAK;CAEjC,MAAM,YAAY,QAAQ,QAAQ,KAAK,QAAQ,KAAK,CAAC;CACrD,MAAM,UAAU,aAAa,WAAW,CAAC,WAAW,CAAC,YAAY,CAAC,eAAe,UAAU;CAE3F,MAAM,aAAa;EACjB,MAAM,CAAC,OAAO,UAAU,MAAM,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC;EAChE,OAAO,SAAS,SAAS,OAAO,aAAa,MAAM,aAAc,OAAO,eAAe;CACzF;;CAEA,MAAM,UAAU,MAAc;EAC5B,MAAM,OAAO,MAAM,SAAS,SAAS;EACrC,MAAM,MAAM,SAAS;EACrB,OAAO,QAAQ,MAAM,KAAK,cAAc,IAAI,cAAc,KAAK,eAAe,IAAI;CACpF;;CAGA,MAAM,QAAQ,kBAAkB;EAC9B,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,OAAO,CAAC,MAAM,OAAO,OAAO;EACjC,MAAM,WAAW,IAAI,aAAa;EAClC,MAAM,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,OAAO,MAAM;GAC9D,MAAuB,MAAM,YAAY,OAAO,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;EAC7F,CAAC;EACD,KAAK,QAAQ,SAAS,KAAK,MAAM;GAC/B,MAAM,MAAM,KAAK;GACjB,IAAI,eAAe,aAAa,IAAI,MAAM,YAAY,OAAO,OAAO,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;EAC9H,CAAC;EACD,OAAO;CACT,GAAG,CAAC,IAAI,CAAC;CAET,SAAS,cAAc,GAAW,QAAiB;EACjD,MAAM,MAAM,SAAS;EACrB,IAAI,CAAC,KAAK;EACV,MAAM,OAAO,OAAO,CAAC;EACrB,IAAI,CAAC,UAAU,WAAW,KAAK,IAAI,IAAI,aAAa,IAAI,IAAI,GAAG;GAC7D,WAAW,UAAU;GACrB,IAAI,SAAS;IAAE;IAAM,UAAU;GAAO,CAAC;GACvC,MAAM;GACN;EACF;EACA,WAAW,UAAU;EACrB,aAAa,SAAS,OAAO;EAE7B,SAAS,UAAU,iBAAkB,WAAW,UAAU,MAAO,GAAI;EACrE,IAAI,SAAS;GAAE;GAAM,UAAU;EAAS,CAAC;CAC3C;CAEA,SAAS,OAAO,MAAc,UAAmB;EAC/C,OAAO,UAAU;EACjB,IAAI,oBAAoB,KAAA,GAAW,YAAY,IAAI;EACnD,gBAAgB,IAAI;EACpB,IAAI,UAAU,gBAAgB,WAAW,MAAM,KAAK,CAAC;CACvD;CAEA,SAAS,OAAO,IAAY,WAAW,MAAM;EAC3C,MAAM,OAAO,MAAM,IAAI,GAAG,IAAI;EAC9B,IAAI,SAAS,OAAO,SAAS,OAAO,MAAM,QAAQ;EAClD,cAAc,MAAM,IAAI;CAC1B;CAGA,gCAAgC;EAC9B,cAAc,OAAO,SAAS,KAAK;EACnC,MAAM,MAAM,SAAS;EACrB,IAAI,CAAC,OAAO,OAAO,mBAAmB,aAAa;EACnD,MAAM,WAAW,IAAI,qBAAqB;GACxC,IAAI,CAAC,KAAK,WAAW,WAAW,YAAY,MAAM,cAAc,OAAO,SAAS,KAAK;EACvF,CAAC;EACD,SAAS,QAAQ,GAAG;EACpB,aAAa,SAAS,WAAW;CACnC,GAAG,CAAC,CAAC;CAGL,gCAAgC;EAC9B,MAAM,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC,CAAC,CAAC,SAAS,OAAO,MAAM,MAAM,gBAAgB,SAAS,MAAM,KAAK,CAAC;CAC7G,GAAG,CAAC,OAAO,KAAK,CAAC;CAGjB,gBAAgB;EACd,IAAI,OAAO,YAAY,OAAO;GAC5B,OAAO,UAAU;GACjB,cAAc,OAAO,IAAI;EAC3B;CAEF,GAAG,CAAC,KAAK,CAAC;CAEV,sBAAsB,aAAa,SAAS,OAAO,GAAG,CAAC,CAAC;CAExD,SAAS,WAAW;EAClB,MAAM,WAAW,MAAM;EACvB,MAAM,MAAM,SAAS;EACrB,IAAI,CAAC,OAAO,KAAK,SAAS,QAAQ;EAClC,IAAI,WAAW,YAAY,MAAM;GAC/B,IAAI,KAAK,IAAI,IAAI,aAAa,OAAO,WAAW,OAAO,CAAC,IAAI,GAAG,WAAW,UAAU;GACpF;EACF;EACA,MAAM,OAAO,MAAM,KAAK,MAAM,QAAQ,GAAG,GAAG,IAAI;EAChD,IAAI,SAAS,OAAO,SAAS,OAAO,MAAM,IAAI;CAChD;CAEA,MAAM,gBAAgB,OAAO,SAAS,OAAO,IAAI,QAAQ,GAAG,KAAK;CAEjE,gBAAgB;EACd,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,YAAY,CAAC,MAAM;EACxB,MAAM,WAAW,OAAO,yBAAyB,cAAc,OAAO,IAAI,sBAAsB,CAAC,WAAW,SAAS,UAAU,MAAM,cAAc,CAAC;EACpJ,UAAU,QAAQ,IAAI;EACtB,MAAM,qBAAqB,eAAe,SAAS,oBAAoB,QAAQ;EAC/E,SAAS,iBAAiB,oBAAoB,YAAY;EAC1D,aAAa;GACX,UAAU,WAAW;GACrB,SAAS,oBAAoB,oBAAoB,YAAY;EAC/D;CACF,GAAG,CAAC,QAAQ,CAAC;CAEb,SAAS,cAAc,OAAqC;EAC1D,IAAI,QAAQ,KAAK,KAAK,WAAW,MAAM,gBAAgB,WAAW,MAAM,WAAW,GAAG;EACtF,aAAa,UAAU;EACvB,KAAK,UAAU;GAAE,SAAS,MAAM;GAAW,QAAQ,MAAM;GAAS,WAAW,MAAM,cAAc;GAAY,MAAM,OAAO;GAAS,QAAQ;GAAO,SAAS,CAAC;EAAE;CAChK;CAEA,SAAS,cAAc,OAAqC;EAC1D,MAAM,UAAU,KAAK;EACrB,IAAI,CAAC,WAAW,QAAQ,YAAY,MAAM,WAAW;EACrD,MAAM,MAAM,MAAM;EAClB,IAAI,CAAC,QAAQ,QAAQ;GACnB,IAAI,KAAK,IAAI,MAAM,UAAU,QAAQ,MAAM,IAAI,MAAM;GAErD,QAAQ,SAAS;GACjB,QAAQ,SAAS,MAAM;GACvB,WAAW,UAAU;GACrB,IAAI;IACF,IAAI,kBAAkB,MAAM,SAAS;GACvC,QAAQ,CAER;GACA,IAAI,QAAQ,WAAW;EACzB;EACA,IAAI,aAAa,QAAQ,aAAa,MAAM,UAAU,QAAQ;EAC9D,QAAQ,QAAQ,KAAK,CAAC,MAAM,WAAW,IAAI,UAAU,CAAC;EACtD,IAAI,QAAQ,QAAQ,SAAS,IAAI,QAAQ,QAAQ,MAAM;CACzD;CAEA,SAAS,OAAO,OAAqC,WAAoB;EACvE,MAAM,UAAU,KAAK;EACrB,IAAI,CAAC,WAAW,QAAQ,YAAY,MAAM,WAAW;EACrD,KAAK,UAAU;EACf,IAAI,CAAC,QAAQ,QAAQ;EACrB,MAAM,MAAM,MAAM;EAClB,OAAO,IAAI,QAAQ;EACnB,aAAa,UAAU;EACvB,MAAM,SAAS,QAAQ,QAAQ,QAAQ,CAAC,UAAU,MAAM,YAAY,QAAQ,EAAE;EAC9E,MAAM,QAAQ,OAAO;EACrB,MAAM,aAAa,OAAO,OAAO,SAAS;EAC1C,MAAM,WACJ,CAAC,aAAa,SAAS,cAAc,OAAO,SAAS,KAAK,MAAM,YAAY,WAAW,MAAM,MAAM,WAAW,KAAK,MAAM,MACnH,WAAW,KAAK,MAAM,OAAO,WAAW,KAAK,MAAM,MAAO,MAC5D;EACN,MAAM,OAAO,KAAK,KAAK;EAEvB,IAAI,KAAK,KAAK,OAAO,IAAI,aAAa,QAAQ,QAAQ,KAAK,IAAI;EAC/D,IAAI,KAAK,IAAI,QAAQ,IAAI,OAAO;GAC9B,MAAM,YAAY,WAAW,IAAI,IAAI;GACrC,KAAK,KAAK,QAAQ,QAAQ,YAAY,GAAG,KAAK,QAAQ,OAAO;EAC/D;EACA,OAAO,EAAE;CACX;;CAGA,SAAS,QAAQ,OAAmC;EAClD,IAAI,aAAa,SAAS;GACxB,aAAa,UAAU;GACvB,MAAM,eAAe;GACrB,MAAM,gBAAgB;GACtB;EACF;EACA,MAAM,MAAM,MAAM,KAAK,MAAM,SAAS,YAAY,CAAC,CAAC,CAAC,CAAC,WAAW,UAAU;GACzE,MAAM,OAAO,MAAM,sBAAsB;GACzC,OAAO,MAAM,WAAW,KAAK,QAAQ,MAAM,WAAW,KAAK;EAC7D,CAAC;EACD,IAAI,OAAO,KAAK,QAAQ,OAAO,SAAS,OAAO,GAAG;CACpD;CAEA,SAAS,UAAU,OAAmC;EACpD,IAAI,MAAM,UAAU,MAAM,WAAW,MAAM,WAAW,QAAQ,GAAG;EACjE,MAAM,OAAO,MAAM;EACnB,IAAI,KAAK,QAAQ,4CAA4C,GAAG;EAChE,MAAM,OAAO,OAAO;EACpB,MAAM,KACJ,MAAM,QAAQ,cAAc,OAAO,IAAI,MAAM,QAAQ,eAAe,OAAO,IAAI,MAAM,QAAQ,SAAS,IAAI,MAAM,QAAQ,QAAQ,OAAO;EACzI,IAAI,OAAO,MAAM;EACjB,MAAM,eAAe;EACrB,MAAM,OAAO,MAAM,IAAI,GAAG,IAAI;EAC9B,OAAO,IAAI;EACX,IAAI,KAAK,aAAa,MAAM,MAAM,OAAO,KAAK,QAAQ,KAAK,EAAE,MAAM,EAAE,eAAe,KAAK,CAAC;OACrF,IAAI,MAAM,SAAS,SAAS,IAAI,GAAG,SAAS,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;CACzF;CAEA,MAAM,OAAO;EAAE,UAAU;EAAkB,MAAM;EAAc,MAAM;EAAwB,OAAO;EAAwB,QAAQ;EAAkB,GAAG;CAAO;CAEhK,OACE,qBAAC,WAAD;EACE,KAAK;EACL,aAAU;EACV,wBAAqB;EACrB,cAAY;EACZ,WAAW,GAAG,YAAY,SAAS;EACnC,OAAO,YAAa,EAAE,yBAAyB,UAAU,IAAsB,KAAA;EACpE;EACX,UAAU,UAAU,eAAe,MAAM,WAAW,KAAK,WAAW,MAAM,OAAO,QAAQ,gBAAgB,CAAC;EAC1G,SAAS,UAAU;GACjB,IAAI,CAAC,MAAM,cAAc,SAAS,MAAM,aAAa,GAAG,eAAe,KAAK;EAC9E;EAXF,UAAA;GAaE,oBAAC,OAAD;IACE,KAAK;IACL,MAAK;IACL,cAAW;IACX,UAAU;IACV,aAAU;IACV,WAAU;IACA;IACK;IACA;IACf,cAAc,UAAU,OAAO,OAAO,KAAK;IAC3C,kBAAkB,UAAU,OAAO,OAAO,IAAI;IAC9C,iBAAiB,UAAU,MAAM,gBAAgB,WAAW,WAAW,IAAI;IAC3E,sBAAsB,WAAW,KAAK;IACtC,oBAAoB,YAAY,IAAI;IACpC,kBAAkB,YAAY,KAAK;IACnC,qBAAqB,YAAY,KAAK;IACtC,gBAAgB;IAChB,cAAc,UAAU,MAAM,eAAe;IAE7C,UAAA,oBAAC,OAAD;KAAK,KAAK;KAAO,WAAU;KACxB,UAAA,OAAO,KAAK,OAAO,MAClB,oBAAC,OAAD;MAEE,IAAI,GAAG,GAAG,SAAS;MACnB,MAAK;MACL,wBAAqB;MACrB,cAAY,WAAW,GAAG,KAAK;MAC/B,eAAa,MAAM,SAAS,KAAA;MAC5B,WAAU;MAET,UAAA;KACE,GATE,eAAe,KAAK,KAAK,MAAM,OAAO,OAAO,MAAM,MAAM,CAS3D,CACN;IACE,CAAA;GACF,CAAA;GACJ,QAAQ,IACP,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACG,YACC,oBAAC,UAAD;MACE,KAAK;MACL,MAAK;MACL,cAAY,UAAU,KAAK,QAAQ,KAAK;MACxC,WAAU;MACV,eAAe,WAAW,CAAC,OAAO;MAEjC,UAAA,UAAU,oBAAC,YAAD,CAAa,CAAA,IAAI,oBAAC,WAAD,CAAY,CAAA;KAClC,CAAA,IACN;KACJ,oBAAC,OAAD;MAAK,MAAK;MAAU,cAAY,KAAK;MAAQ,WAAU;MACpD,UAAA,OAAO,KAAK,GAAG,MACd,oBAAC,UAAD;OAEE,MAAM,SAAS;QACb,KAAK,QAAQ,KAAK;OACpB;OACA,MAAK;OACL,MAAK;OACL,iBAAe,MAAM;OACrB,iBAAe,GAAG,GAAG,SAAS;OAC9B,cAAY,WAAW,GAAG,KAAK;OAC/B,UAAU,MAAM,QAAQ,IAAI;OAC5B,WAAU;OACV,eAAe,OAAO,CAAC;OAEvB,UAAA,qBAAC,QAAD;QAAM,eAAY;QAAO,WAAU;QAAnC,UAAA,CACE,oBAAC,QAAD,EAAM,WAAU,oBAAqB,CAAA,GACpC,aAAa,WAAW,MAAM,QAC7B,oBAAC,QAAD;SAEE,WAAU;SACV,OAAO;UAAE,mBAAmB,GAAG,SAAS;UAAK,oBAAoB,UAAU,YAAY;SAAS;SAChG,gBAAgB;QACjB,GAJM,KAIN,IACC,IACA;;MACA,GAxBD,CAwBC,CACT;KACE,CAAA;KACL,qBAAC,OAAD;MAAK,WAAU;MAAf,UAAA,CACE,oBAAC,UAAD;OACE,MAAK;OACL,cAAY,KAAK;OACjB,iBAAe,UAAU;OACzB,WAAU;OACV,eAAe,OAAO,UAAU,KAAK,OAAO,OAAO,UAAU,CAAC;OAE9D,UAAA,oBAAC,SAAD,EAAS,MAAA,KAAM,CAAA;MACT,CAAA,GACR,oBAAC,UAAD;OACE,MAAK;OACL,cAAY,KAAK;OACjB,iBAAe,UAAU;OACzB,WAAU;OACV,eAAe,OAAO,UAAU,QAAQ,OAAO,OAAO,UAAU,CAAC;OAEjE,UAAA,oBAAC,SAAD,CAAU,CAAA;MACJ,CAAA,CACL;;IACF;GACH,CAAA,IAAA;GACJ,oBAAC,QAAD;IAAM,WAAU;IAAU,aAAU;IAAS,eAAY;IACtD,UAAA;GACG,CAAA;EACC;;AAEb,CAAC"}
|
|
1
|
+
{"version":3,"file":"carousel.js","names":[],"sources":["../../src/carousel/carousel.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n Children,\n forwardRef,\n isValidElement,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type KeyboardEvent,\n type MouseEvent,\n type PointerEvent,\n type ReactNode,\n} from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { useIsomorphicLayoutEffect, useReducedMotion } from \"../lib/motion.js\";\n\n/**\n * Carousel: a row of slides people browse by swiping, a trackpad, dragging\n * with a mouse, the arrow keys, or the controls below it. Use it for a short,\n * ordered set of peers such as products or case studies, where the neighbours\n * stay in view as a hint that there is more.\n *\n * The slides sit on a native scroll-snap track, so touch and trackpad swipes\n * are the browser's own. The active slide is centred; its neighbours shrink and\n * dim with their distance, and each page indicator stretches into a pill as the\n * track nears its slide, all written straight from the scroll position without a\n * re-render. A mouse can drag the track, and a flick carries on to the next\n * slide. Arrow keys, Home and End move between slides from the track or the\n * indicators. `interval` adds a play control that rotates through the slides;\n * rotation pauses on hover, keyboard focus and touch, while the carousel is off\n * screen or the tab is hidden, and is unavailable under reduced motion (where\n * scrolling also jumps instead of gliding).\n *\n * `loop` wraps the ends: next on the last slide goes to the first, previous on\n * the first to the last, from the controls and the arrow keys alike (a swipe\n * or drag still stops at the end of the track). `peek={false}` shows one slide\n * at a time across the full width, with no neighbours in view, for a viewer\n * such as a single creative asset.\n */\nexport interface CarouselProps {\n /** Names the carousel for assistive technology, such as \"From the Harbour suites\". */\n label: string;\n /** One child per slide. */\n children: ReactNode;\n /** Controlled active slide. Pair with `onIndexChange`. */\n index?: number;\n /** @default 0 */\n defaultIndex?: number;\n onIndexChange?: (index: number) => void;\n /** Width of one slide as a CSS length; `cqw` is a share of the carousel's width. @default \"min(80cqw, 340px)\" */\n slideSize?: string;\n /** Names each slide and its indicator. @default (index, count) => `${index + 1} of ${count}` */\n slideLabel?: (index: number, count: number) => string;\n /** Milliseconds per slide. Adds a play control that rotates through the slides. */\n interval?: number;\n /** Starts rotating on mount. Off by default, so motion begins with the viewer. @default false */\n autoplay?: boolean;\n /**\n * Wraps around at the ends: next on the last slide goes to the first and\n * previous on the first goes to the last, from the controls and the arrow\n * keys. The controls are then never disabled. Home and End still go to the\n * first and last slide; a swipe or drag stops at the end of the track.\n * @default false\n */\n loop?: boolean;\n /**\n * Neighbouring slides peek in at the sides. `false` shows one slide at a\n * time across the full width (`slideSize` is ignored), with the controls\n * spanning it too.\n * @default true\n */\n peek?: boolean;\n /** Names of the controls, for translation. */\n labels?: {\n /** @default \"Previous slide\" */\n previous?: string;\n /** @default \"Next slide\" */\n next?: string;\n /** @default \"Start slide rotation\" */\n play?: string;\n /** @default \"Pause slide rotation\" */\n pause?: string;\n /** Names the group of indicators. @default \"Choose a slide\" */\n slides?: string;\n };\n /** Extra classes for the carousel. Merged last. */\n className?: string;\n}\n\n/** Movement before a mouse press is read as a drag. */\nconst SLOP = 6;\n/** Release speed in px/s that always moves at least one slide. */\nconst FLICK = 360;\n/**\n * How long a glide may take before the carousel stops waiting for it: one slide,\n * plus more per extra slide (a `loop` wrap crosses them all), capped.\n */\nconst BACKSTOP = { base: 1000, perSlide: 250, max: 3000 };\n/** Where a flick would come to rest: velocity (px/s) projected with a paging deceleration rate. */\nconst project = (velocity: number, rate = 0.995) => ((velocity / 1000) * rate) / (1 - rate);\nconst clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\nconst defaultSlideLabel = (index: number, count: number) => `${index + 1} of ${count}`;\n\nconst PlayGlyph = () => (\n <svg width={15} height={15} viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"M4.5 2.8v10.4a.6.6 0 0 0 .9.5l8.2-5.2a.6.6 0 0 0 0-1L5.4 2.3a.6.6 0 0 0-.9.5Z\" />\n </svg>\n);\nconst PauseGlyph = () => (\n <svg width={15} height={15} viewBox=\"0 0 16 16\" fill=\"currentColor\" aria-hidden=\"true\">\n <rect x=\"3.5\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\"1\" />\n <rect x=\"9.3\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\"1\" />\n </svg>\n);\nconst Chevron = ({ back }: { back?: boolean }) => (\n <svg width={18} height={18} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d={back ? \"M10 3.5 5.5 8l4.5 4.5\" : \"M6 3.5 10.5 8 6 12.5\"} />\n </svg>\n);\n\ntype Drag = { pointer: number; startX: number; startLeft: number; from: number; locked: boolean; samples: [number, number][] };\n\nexport const Carousel = forwardRef<HTMLElement, CarouselProps>(function Carousel(\n {\n label,\n children,\n index: controlledIndex,\n defaultIndex = 0,\n onIndexChange,\n slideSize,\n slideLabel = defaultSlideLabel,\n interval,\n autoplay = false,\n loop = false,\n peek = true,\n labels,\n className,\n },\n ref,\n) {\n const slides = Children.toArray(children);\n const count = slides.length;\n const last = Math.max(0, count - 1);\n const id = useId();\n const reduced = useReducedMotion();\n const [ownIndex, setOwnIndex] = useState(() => clamp(defaultIndex, 0, last));\n const index = clamp(controlledIndex ?? ownIndex, 0, last);\n const [playing, setPlaying] = useState(autoplay);\n const [hovered, setHovered] = useState(false);\n const [touching, setTouching] = useState(false);\n const [focusPaused, setFocusPaused] = useState(false);\n const [inView, setInView] = useState(true);\n const [pageVisible, setPageVisible] = useState(true);\n const [announcement, setAnnouncement] = useState(\"\");\n\n const root = useRef<HTMLElement | null>(null);\n const rootRef = useMemo(() => mergeRefs(ref, root), [ref]);\n const viewport = useRef<HTMLDivElement>(null);\n const track = useRef<HTMLDivElement>(null);\n const play = useRef<HTMLButtonElement>(null);\n const tabs = useRef<(HTMLButtonElement | null)[]>([]);\n // The slide the carousel is on, as the component last decided it (state lags a render behind).\n const target = useRef(index);\n // Set while a scroll we started is in flight, so the in-between slides are not reported as the viewer's choice.\n const travelling = useRef<number | null>(null);\n const backstop = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n const drag = useRef<Drag | null>(null);\n const swallowClick = useRef(false);\n\n const canRotate = Boolean(interval) && count > 1 && !reduced;\n const running = canRotate && playing && !hovered && !touching && !focusPaused && inView && pageVisible;\n\n const step = () => {\n const [first, second] = Array.from(track.current?.children ?? []) as HTMLElement[];\n return first && second ? second.offsetLeft - first.offsetLeft : (first?.offsetWidth ?? 0);\n };\n /** The scroll position that centres slide `i`. */\n const leftOf = (i: number) => {\n const node = track.current?.children[i] as HTMLElement | undefined;\n const box = viewport.current;\n return node && box ? node.offsetLeft - (box.clientWidth - node.offsetWidth) / 2 : 0;\n };\n\n /** Writes how far each slide and indicator is from the centre, straight from the scroll position. Returns the position in slides. */\n const paint = useCallback(() => {\n const box = viewport.current;\n const size = step();\n if (!box || !size) return target.current;\n const position = box.scrollLeft / size;\n Array.from(track.current?.children ?? []).forEach((slide, i) => {\n (slide as HTMLElement).style.setProperty(\"--d\", String(Math.min(1, Math.abs(position - i))));\n });\n tabs.current.forEach((tab, i) => {\n const dot = tab?.firstElementChild;\n if (dot instanceof HTMLElement) dot.style.setProperty(\"--e\", String(Math.max(0, 1 - Math.abs(clamp(position, 0, last) - i))));\n });\n return position;\n }, [last]);\n\n function scrollToIndex(i: number, smooth: boolean) {\n const box = viewport.current;\n if (!box) return;\n const left = leftOf(i);\n if (!smooth || reduced || Math.abs(box.scrollLeft - left) < 1) {\n travelling.current = null;\n box.scrollTo({ left, behavior: \"auto\" });\n paint();\n return;\n }\n travelling.current = i;\n clearTimeout(backstop.current);\n // A scroll that never reports arriving (a resize mid-flight) must not hold the carousel deaf to the viewer's own swipes.\n // The glide normally ends on `scrollend`; this timer is the fallback, scaled to the\n // distance so a long wrap is not cut short.\n const distance = Math.abs(box.scrollLeft - left) / (step() || 1);\n const wait = Math.min(\n BACKSTOP.max,\n BACKSTOP.base + BACKSTOP.perSlide * Math.max(0, distance - 1),\n );\n backstop.current = setTimeout(() => (travelling.current = null), wait);\n box.scrollTo({ left, behavior: \"smooth\" });\n }\n\n function commit(next: number, announce: boolean) {\n target.current = next;\n if (controlledIndex === undefined) setOwnIndex(next);\n onIndexChange?.(next);\n if (announce) setAnnouncement(slideLabel(next, count));\n }\n\n function select(to: number, announce = true) {\n const next = clamp(to, 0, last);\n if (next !== target.current) commit(next, announce);\n scrollToIndex(next, true);\n }\n\n // The start slide is in place before first paint, so nothing glides on mount; later resizes keep the carousel on its slide.\n useIsomorphicLayoutEffect(() => {\n scrollToIndex(target.current, false);\n const box = viewport.current;\n if (!box || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n if (!drag.current && travelling.current === null) scrollToIndex(target.current, false);\n });\n observer.observe(box);\n return () => observer.disconnect();\n }, []);\n\n // The viewer's own slides, inert unless active, are the only ones in the tab order or the accessibility tree's reach.\n useIsomorphicLayoutEffect(() => {\n Array.from(track.current?.children ?? []).forEach((slide, i) => slide.toggleAttribute(\"inert\", i !== index));\n }, [index, count]);\n\n // A controlled index that moves by itself brings the carousel along.\n useEffect(() => {\n if (target.current !== index) {\n target.current = index;\n scrollToIndex(index, true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- only a changed index matters\n }, [index]);\n\n useEffect(() => () => clearTimeout(backstop.current), []);\n\n // A glide is over when the browser says the scroll has ended, however long it took.\n useEffect(() => {\n const box = viewport.current;\n if (!box) return;\n const onScrollEnd = () => {\n if (travelling.current === null) return;\n travelling.current = null;\n clearTimeout(backstop.current);\n };\n box.addEventListener(\"scrollend\", onScrollEnd);\n return () => box.removeEventListener(\"scrollend\", onScrollEnd);\n }, []);\n\n function onScroll() {\n const position = paint();\n const box = viewport.current;\n if (!box || drag.current?.locked) return;\n if (travelling.current !== null) {\n if (Math.abs(box.scrollLeft - leftOf(travelling.current)) < 2) travelling.current = null;\n return;\n }\n const next = clamp(Math.round(position), 0, last);\n if (next !== target.current) commit(next, true);\n }\n\n const advance = () => select(index >= last ? 0 : index + 1, false);\n /** The slide `by` steps from `from`: clamped at the ends, or around them with `loop`. */\n const stepFrom = (from: number, by: number) => (loop ? (from + by + count) % count : clamp(from + by, 0, last));\n\n useEffect(() => {\n const node = root.current;\n if (!interval || !node) return;\n const observer = typeof IntersectionObserver === \"undefined\" ? null : new IntersectionObserver(([entry]) => entry && setInView(entry.isIntersecting));\n observer?.observe(node);\n const onVisibility = () => setPageVisible(document.visibilityState !== \"hidden\");\n document.addEventListener(\"visibilitychange\", onVisibility);\n return () => {\n observer?.disconnect();\n document.removeEventListener(\"visibilitychange\", onVisibility);\n };\n }, [interval]);\n\n function onPointerDown(event: PointerEvent<HTMLDivElement>) {\n if (count < 2 || drag.current || event.pointerType !== \"mouse\" || event.button !== 0) return;\n swallowClick.current = false;\n drag.current = { pointer: event.pointerId, startX: event.clientX, startLeft: event.currentTarget.scrollLeft, from: target.current, locked: false, samples: [] };\n }\n\n function onPointerMove(event: PointerEvent<HTMLDivElement>) {\n const current = drag.current;\n if (!current || current.pointer !== event.pointerId) return;\n const box = event.currentTarget;\n if (!current.locked) {\n if (Math.abs(event.clientX - current.startX) < SLOP) return;\n // The slop is consumed, so the track starts moving from the pointer without a jump. Snapping is off while it is held.\n current.locked = true;\n current.startX = event.clientX;\n travelling.current = null;\n try {\n box.setPointerCapture(event.pointerId);\n } catch {\n // The pointer already ended; the drag simply stops following it.\n }\n box.dataset.dragging = \"\";\n }\n box.scrollLeft = current.startLeft - (event.clientX - current.startX);\n current.samples.push([event.timeStamp, box.scrollLeft]);\n if (current.samples.length > 16) current.samples.shift();\n }\n\n function finish(event: PointerEvent<HTMLDivElement>, cancelled: boolean) {\n const current = drag.current;\n if (!current || current.pointer !== event.pointerId) return;\n drag.current = null;\n if (!current.locked) return;\n const box = event.currentTarget;\n delete box.dataset.dragging;\n swallowClick.current = true;\n const recent = current.samples.filter(([time]) => event.timeStamp - time <= 90);\n const first = recent[0];\n const lastSample = recent[recent.length - 1];\n const velocity =\n !cancelled && first && lastSample && recent.length > 1 && event.timeStamp - lastSample[0] <= 50 && lastSample[0] > first[0]\n ? ((lastSample[1] - first[1]) / (lastSample[0] - first[0])) * 1000\n : 0;\n const size = step() || 1;\n // The landing slide comes from where the flick would carry the track, not from where the pointer let go.\n let to = Math.round((box.scrollLeft + project(velocity)) / size);\n if (Math.abs(velocity) > FLICK) {\n const direction = velocity > 0 ? 1 : -1;\n if ((to - current.from) * direction < 1) to = current.from + direction;\n }\n select(to);\n }\n\n /** A tap on a neighbour brings it to the centre; a click that ends a drag is not a tap. */\n function onClick(event: MouseEvent<HTMLDivElement>) {\n if (swallowClick.current) {\n swallowClick.current = false;\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n const hit = Array.from(track.current?.children ?? []).findIndex((slide) => {\n const rect = slide.getBoundingClientRect();\n return event.clientX >= rect.left && event.clientX <= rect.right;\n });\n if (hit >= 0 && hit !== target.current) select(hit);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLElement>) {\n if (event.altKey || event.ctrlKey || event.metaKey || count < 2) return;\n const node = event.target as HTMLElement;\n if (node.closest(\"input, textarea, select, [contenteditable]\")) return;\n const from = target.current;\n const to =\n event.key === \"ArrowLeft\" ? stepFrom(from, -1) : event.key === \"ArrowRight\" ? stepFrom(from, 1) : event.key === \"Home\" ? 0 : event.key === \"End\" ? last : null;\n if (to === null) return;\n event.preventDefault();\n const next = clamp(to, 0, last);\n select(next);\n if (node.getAttribute(\"role\") === \"tab\") tabs.current[next]?.focus({ preventScroll: true });\n else if (track.current?.contains(node)) viewport.current?.focus({ preventScroll: true });\n }\n\n const text = { previous: \"Previous slide\", next: \"Next slide\", play: \"Start slide rotation\", pause: \"Pause slide rotation\", slides: \"Choose a slide\", ...labels };\n\n return (\n <section\n ref={rootRef}\n data-slot=\"carousel\"\n aria-roledescription=\"carousel\"\n aria-label={label}\n data-loop={loop || undefined}\n data-peek={peek ? undefined : \"false\"}\n className={cn(\"carousel\", className)}\n style={slideSize && peek ? ({ \"--carousel-slide-size\": slideSize } as CSSProperties) : undefined}\n onKeyDown={onKeyDown}\n onFocus={(event) => setFocusPaused(event.target !== play.current && event.target.matches(\":focus-visible\"))}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget)) setFocusPaused(false);\n }}\n >\n <div\n ref={viewport}\n role=\"region\"\n aria-label=\"Slides\"\n tabIndex={0}\n data-slot=\"carousel-viewport\"\n className=\"carousel-viewport\"\n onScroll={onScroll}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={(event) => finish(event, false)}\n onPointerCancel={(event) => finish(event, true)}\n onPointerEnter={(event) => event.pointerType === \"mouse\" && setHovered(true)}\n onPointerLeave={() => setHovered(false)}\n onTouchStart={() => setTouching(true)}\n onTouchEnd={() => setTouching(false)}\n onTouchCancel={() => setTouching(false)}\n onClickCapture={onClick}\n onDragStart={(event) => event.preventDefault()}\n >\n <div ref={track} className=\"carousel-track\">\n {slides.map((slide, i) => (\n <div\n key={isValidElement(slide) && slide.key != null ? slide.key : i}\n id={`${id}-slide-${i}`}\n role=\"tabpanel\"\n aria-roledescription=\"slide\"\n aria-label={slideLabel(i, count)}\n data-active={i === index || undefined}\n className=\"carousel-slide\"\n >\n {slide}\n </div>\n ))}\n </div>\n </div>\n {count > 1 ? (\n <div className=\"carousel-controls\">\n {canRotate ? (\n <button\n ref={play}\n type=\"button\"\n aria-label={playing ? text.pause : text.play}\n className=\"carousel-control\"\n onClick={() => setPlaying(!playing)}\n >\n {playing ? <PauseGlyph /> : <PlayGlyph />}\n </button>\n ) : null}\n <div role=\"tablist\" aria-label={text.slides} className=\"carousel-tabs\">\n {slides.map((_, i) => (\n <button\n key={i}\n ref={(node) => {\n tabs.current[i] = node;\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={i === index}\n aria-controls={`${id}-slide-${i}`}\n aria-label={slideLabel(i, count)}\n tabIndex={i === index ? 0 : -1}\n className=\"carousel-tab\"\n onClick={() => select(i)}\n >\n <span aria-hidden=\"true\" className=\"carousel-dot\">\n <span className=\"carousel-dot-fill\" />\n {canRotate && playing && i === index ? (\n <span\n key={index}\n className=\"carousel-drain\"\n style={{ animationDuration: `${interval}ms`, animationPlayState: running ? \"running\" : \"paused\" }}\n onAnimationEnd={advance}\n />\n ) : null}\n </span>\n </button>\n ))}\n </div>\n <div className=\"carousel-arrows\">\n <button\n type=\"button\"\n aria-label={text.previous}\n aria-disabled={!loop && index === 0}\n className=\"carousel-control\"\n onClick={() => (loop || target.current > 0) && select(stepFrom(target.current, -1))}\n >\n <Chevron back />\n </button>\n <button\n type=\"button\"\n aria-label={text.next}\n aria-disabled={!loop && index === last}\n className=\"carousel-control\"\n onClick={() => (loop || target.current < last) && select(stepFrom(target.current, 1))}\n >\n <Chevron />\n </button>\n </div>\n </div>\n ) : null}\n <span className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n {announcement}\n </span>\n </section>\n );\n});\n"],"mappings":"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"}
|
|
@@ -14,7 +14,18 @@
|
|
|
14
14
|
export interface ChipOption {
|
|
15
15
|
value: string;
|
|
16
16
|
label: string;
|
|
17
|
+
/**
|
|
18
|
+
* Extra `data-*` attributes for this chip's button, such as
|
|
19
|
+
* `{ "data-testid": "goal-market-IN" }`, so tests and analytics can address
|
|
20
|
+
* one chip. They never override the group's own `data-chip`, `data-slot`,
|
|
21
|
+
* `data-flip`, `data-more` or `data-selected`; those keys are dropped.
|
|
22
|
+
*/
|
|
23
|
+
attrs?: ChipAttrs;
|
|
17
24
|
}
|
|
25
|
+
/** `data-*` attributes passed through to one chip's button. */
|
|
26
|
+
export type ChipAttrs = {
|
|
27
|
+
[key: `data-${string}`]: string | undefined;
|
|
28
|
+
};
|
|
18
29
|
export interface ChipGroupProps {
|
|
19
30
|
options: ChipOption[];
|
|
20
31
|
value: string[];
|
|
@@ -4,11 +4,26 @@ import { mergeRefs } from "../lib/merge-refs.js";
|
|
|
4
4
|
import { SwapText } from "../lib/swap-text.js";
|
|
5
5
|
import { useHeightFollow } from "../lib/height-follow.js";
|
|
6
6
|
import { useFlip } from "../lib/flip.js";
|
|
7
|
-
import { forwardRef, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { createElement, forwardRef, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/chip-group/chip-group.tsx
|
|
10
10
|
/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */
|
|
11
11
|
const MORE = "\0more";
|
|
12
|
+
/** Attributes the group owns; a chip's `attrs` can never set them. */
|
|
13
|
+
const OWN_ATTRS = /* @__PURE__ */ new Set([
|
|
14
|
+
"data-chip",
|
|
15
|
+
"data-slot",
|
|
16
|
+
"data-flip",
|
|
17
|
+
"data-more",
|
|
18
|
+
"data-selected"
|
|
19
|
+
]);
|
|
20
|
+
/** Keeps only `data-*` keys the group does not own, so a chip's attrs cannot break its styling or roving focus. */
|
|
21
|
+
function chipAttrs(attrs) {
|
|
22
|
+
if (!attrs) return void 0;
|
|
23
|
+
const kept = {};
|
|
24
|
+
for (const [key, value] of Object.entries(attrs)) if (key.startsWith("data-") && !OWN_ATTRS.has(key)) kept[key] = value;
|
|
25
|
+
return kept;
|
|
26
|
+
}
|
|
12
27
|
/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */
|
|
13
28
|
const STAGGER_MS = 35;
|
|
14
29
|
const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange, label, multiple = true, maxVisible = Infinity, moreLabel = (hidden) => `+${hidden} more`, lessLabel = "Show less", className }, ref) {
|
|
@@ -76,7 +91,9 @@ const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange,
|
|
|
76
91
|
children: [visible.map((option) => {
|
|
77
92
|
const selected = value.includes(option.value);
|
|
78
93
|
const delay = revealedAt.get(option.value);
|
|
79
|
-
return /* @__PURE__ */
|
|
94
|
+
return /* @__PURE__ */ createElement("button", {
|
|
95
|
+
...chipAttrs(option.attrs),
|
|
96
|
+
key: option.value,
|
|
80
97
|
type: "button",
|
|
81
98
|
"data-flip": "",
|
|
82
99
|
"data-chip": option.value,
|
|
@@ -86,40 +103,39 @@ const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange,
|
|
|
86
103
|
onClick: () => toggle(option.value),
|
|
87
104
|
onFocus: () => setActive(option.value),
|
|
88
105
|
style: delay === void 0 ? void 0 : { animationDelay: `${delay}ms` },
|
|
89
|
-
className: cn("chip", delay === void 0 ? "" : "motion-safe:animate-chip-reveal")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
106
|
+
className: cn("chip", delay === void 0 ? "" : "motion-safe:animate-chip-reveal")
|
|
107
|
+
}, /* @__PURE__ */ jsxs("span", {
|
|
108
|
+
"data-selected": selected,
|
|
109
|
+
className: "chip-body",
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsx("span", {
|
|
112
|
+
"aria-hidden": "true",
|
|
113
|
+
className: "chip-check",
|
|
114
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
115
|
+
width: 14,
|
|
116
|
+
height: 14,
|
|
117
|
+
viewBox: "0 0 24 24",
|
|
118
|
+
fill: "none",
|
|
119
|
+
stroke: "currentColor",
|
|
120
|
+
strokeWidth: 2.5,
|
|
121
|
+
strokeLinecap: "round",
|
|
122
|
+
strokeLinejoin: "round",
|
|
123
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
124
|
+
d: "M4 12.5 9.5 18 20 6.5",
|
|
125
|
+
pathLength: 1
|
|
110
126
|
})
|
|
111
|
-
}),
|
|
112
|
-
/* @__PURE__ */ jsx("span", {
|
|
113
|
-
"aria-hidden": "true",
|
|
114
|
-
className: "chip-slot"
|
|
115
|
-
}),
|
|
116
|
-
/* @__PURE__ */ jsx("span", {
|
|
117
|
-
className: "truncate",
|
|
118
|
-
children: option.label
|
|
119
127
|
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx("span", {
|
|
130
|
+
"aria-hidden": "true",
|
|
131
|
+
className: "chip-slot"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ jsx("span", {
|
|
134
|
+
className: "truncate",
|
|
135
|
+
children: option.label
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}));
|
|
123
139
|
}), showMore ? /* @__PURE__ */ jsx("button", {
|
|
124
140
|
type: "button",
|
|
125
141
|
"data-flip": "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chip-group.js","names":[],"sources":["../../src/chip-group/chip-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * ChipGroup: selectable filter chips for narrowing a list by facets people\n * toggle often, such as topics, statuses or tags.\n *\n * Selecting a chip grows a check in at its start (it draws itself as it\n * scales up from a soft blur), the label slides over, the chip widens on the\n * morph spring, and its neighbours glide to their new places, even across\n * lines. The row follows its own height. Long sets fold behind a \"+N more\"\n * chip; opening it grows the hidden chips in one after another. Arrow keys move\n * between chips, Space or Enter toggles, and each chip is a pressed or\n * unpressed button. Selected reads as a tint plus the check, never colour alone.\n */\nexport interface ChipOption {\n value: string;\n label: string;\n}\n\nexport interface ChipGroupProps {\n options: ChipOption[];\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Accessible name of the group, such as \"Topics\". */\n label: string;\n /** Allow several chips at once. In single mode the selected chip can still be cleared. @default true */\n multiple?: boolean;\n /** Chips shown before the rest fold behind a \"+N more\" chip. Chips selected when it folds stay in view. */\n maxVisible?: number;\n /** Text of the overflow chip. @default (hidden) => `+${hidden} more` */\n moreLabel?: (hidden: number) => string;\n /** @default \"Show less\" */\n lessLabel?: string;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\n/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */\nconst MORE = \"\\u0000more\";\n/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */\nconst STAGGER_MS = 35;\n\nexport const ChipGroup = forwardRef<HTMLDivElement, ChipGroupProps>(function ChipGroup(\n {\n options,\n value,\n onValueChange,\n label,\n multiple = true,\n maxVisible = Infinity,\n moreLabel = (hidden) => `+${hidden} more`,\n lessLabel = \"Show less\",\n className,\n },\n ref,\n) {\n const [expanded, setExpanded] = useState(false);\n // Chips selected when the overflow folds stay in view, so a selection is never hidden.\n const [pinned, setPinned] = useState<string[]>(value);\n const [active, setActive] = useState<string | null>(null);\n const foldable = options.length > maxVisible;\n const visible =\n !foldable || expanded\n ? options\n : options.filter(\n (option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value),\n );\n const hidden = options.length - visible.length;\n const showMore = foldable && (expanded || hidden > 0);\n const keys = [...visible.map((option) => option.value), ...(showMore ? [MORE] : [])];\n // One chip holds the tab stop: the last one focused, else the first selected, else the first.\n const tabStop =\n active !== null && keys.includes(active)\n ? active\n : (visible.find((option) => value.includes(option.value))?.value ?? keys[0]);\n\n const group = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, group), [ref]);\n const frame = useRef<HTMLDivElement>(null);\n const morphKey = `${expanded}|${value.join(\",\")}`;\n // Neighbours glide to their new places when a chip widens, even across lines; the row follows its height.\n useFlip(group, morphKey, \"var(--duration-morph)\", \"var(--ease-morph)\");\n useHeightFollow(frame, group, morphKey);\n // Chips that appear after the first render grow in; the ones there from the start never animate.\n const seen = useRef(new Set(options.map((option) => option.value)));\n const revealedAt = new Map<string, number>();\n let rank = 0;\n for (const option of visible) {\n if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));\n }\n\n function toggle(next: string) {\n const on = value.includes(next);\n if (!multiple) {\n onValueChange(on ? [] : [next]);\n return;\n }\n onValueChange(\n options.filter((option) => (option.value === next ? !on : value.includes(option.value))).map((option) => option.value),\n );\n }\n\n function toggleMore() {\n if (expanded) setPinned(value);\n // Only the chips this opening reveals grow in; the ones already on screen stay put.\n else seen.current = new Set(visible.map((option) => option.value));\n setExpanded(!expanded);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button[data-chip]\"));\n const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n if (index < 0) return;\n const last = buttons.length - 1;\n const moves: Record<string, number> = {\n ArrowRight: index === last ? 0 : index + 1,\n ArrowDown: index === last ? 0 : index + 1,\n ArrowLeft: index === 0 ? last : index - 1,\n ArrowUp: index === 0 ? last : index - 1,\n Home: 0,\n End: last,\n };\n if (!(event.key in moves)) return;\n event.preventDefault();\n buttons[moves[event.key]!]!.focus();\n }\n\n const moreText = expanded ? lessLabel : moreLabel(hidden);\n\n return (\n <div ref={frame} data-slot=\"chip-group-frame\" className=\"height-follow min-w-0\">\n <div\n ref={refs}\n role=\"group\"\n aria-label={label}\n data-slot=\"chip-group\"\n onKeyDown={onKeyDown}\n className={cn(\"relative isolate flex flex-wrap gap-2 box-border font-sans\", className)}\n >\n {visible.map((option) => {\n const selected = value.includes(option.value);\n const delay = revealedAt.get(option.value);\n return (\n <button\n key={option.value}\n type=\"button\"\n data-flip=\"\"\n data-chip={option.value}\n data-slot=\"chip\"\n aria-pressed={selected}\n tabIndex={tabStop === option.value ? 0 : -1}\n onClick={() => toggle(option.value)}\n onFocus={() => setActive(option.value)}\n style={delay === undefined ? undefined : ({ animationDelay: `${delay}ms` })}\n className={cn(\"chip\", delay === undefined ? \"\" : \"motion-safe:animate-chip-reveal\")}\n >\n <span data-selected={selected} className=\"chip-body\">\n <span aria-hidden=\"true\" className=\"chip-check\">\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4 12.5 9.5 18 20 6.5\" pathLength={1} />\n </svg>\n </span>\n <span aria-hidden=\"true\" className=\"chip-slot\" />\n <span className=\"truncate\">{option.label}</span>\n </span>\n </button>\n );\n })}\n {showMore ? (\n <button\n key={MORE}\n type=\"button\"\n data-flip=\"\"\n data-chip={MORE}\n data-more=\"\"\n aria-expanded={expanded}\n tabIndex={tabStop === MORE ? 0 : -1}\n onClick={toggleMore}\n onFocus={() => setActive(MORE)}\n className=\"chip chip-more\"\n >\n <span className=\"chip-body tabular-nums\">\n <SwapText swapKey={moreText} motion=\"rise\">\n {moreText}\n </SwapText>\n </span>\n </button>\n ) : null}\n </div>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chip-group.js","names":[],"sources":["../../src/chip-group/chip-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * ChipGroup: selectable filter chips for narrowing a list by facets people\n * toggle often, such as topics, statuses or tags.\n *\n * Selecting a chip grows a check in at its start (it draws itself as it\n * scales up from a soft blur), the label slides over, the chip widens on the\n * morph spring, and its neighbours glide to their new places, even across\n * lines. The row follows its own height. Long sets fold behind a \"+N more\"\n * chip; opening it grows the hidden chips in one after another. Arrow keys move\n * between chips, Space or Enter toggles, and each chip is a pressed or\n * unpressed button. Selected reads as a tint plus the check, never colour alone.\n */\nexport interface ChipOption {\n value: string;\n label: string;\n /**\n * Extra `data-*` attributes for this chip's button, such as\n * `{ \"data-testid\": \"goal-market-IN\" }`, so tests and analytics can address\n * one chip. They never override the group's own `data-chip`, `data-slot`,\n * `data-flip`, `data-more` or `data-selected`; those keys are dropped.\n */\n attrs?: ChipAttrs;\n}\n\n/** `data-*` attributes passed through to one chip's button. */\nexport type ChipAttrs = { [key: `data-${string}`]: string | undefined };\n\nexport interface ChipGroupProps {\n options: ChipOption[];\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Accessible name of the group, such as \"Topics\". */\n label: string;\n /** Allow several chips at once. In single mode the selected chip can still be cleared. @default true */\n multiple?: boolean;\n /** Chips shown before the rest fold behind a \"+N more\" chip. Chips selected when it folds stay in view. */\n maxVisible?: number;\n /** Text of the overflow chip. @default (hidden) => `+${hidden} more` */\n moreLabel?: (hidden: number) => string;\n /** @default \"Show less\" */\n lessLabel?: string;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\n/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */\nconst MORE = \"\\u0000more\";\n/** Attributes the group owns; a chip's `attrs` can never set them. */\nconst OWN_ATTRS = new Set([\"data-chip\", \"data-slot\", \"data-flip\", \"data-more\", \"data-selected\"]);\n\n/** Keeps only `data-*` keys the group does not own, so a chip's attrs cannot break its styling or roving focus. */\nfunction chipAttrs(attrs: ChipAttrs | undefined): ChipAttrs | undefined {\n if (!attrs) return undefined;\n const kept: ChipAttrs = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (key.startsWith(\"data-\") && !OWN_ATTRS.has(key)) kept[key as `data-${string}`] = value;\n }\n return kept;\n}\n\n/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */\nconst STAGGER_MS = 35;\n\nexport const ChipGroup = forwardRef<HTMLDivElement, ChipGroupProps>(function ChipGroup(\n {\n options,\n value,\n onValueChange,\n label,\n multiple = true,\n maxVisible = Infinity,\n moreLabel = (hidden) => `+${hidden} more`,\n lessLabel = \"Show less\",\n className,\n },\n ref,\n) {\n const [expanded, setExpanded] = useState(false);\n // Chips selected when the overflow folds stay in view, so a selection is never hidden.\n const [pinned, setPinned] = useState<string[]>(value);\n const [active, setActive] = useState<string | null>(null);\n const foldable = options.length > maxVisible;\n const visible =\n !foldable || expanded\n ? options\n : options.filter(\n (option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value),\n );\n const hidden = options.length - visible.length;\n const showMore = foldable && (expanded || hidden > 0);\n const keys = [...visible.map((option) => option.value), ...(showMore ? [MORE] : [])];\n // One chip holds the tab stop: the last one focused, else the first selected, else the first.\n const tabStop =\n active !== null && keys.includes(active)\n ? active\n : (visible.find((option) => value.includes(option.value))?.value ?? keys[0]);\n\n const group = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, group), [ref]);\n const frame = useRef<HTMLDivElement>(null);\n const morphKey = `${expanded}|${value.join(\",\")}`;\n // Neighbours glide to their new places when a chip widens, even across lines; the row follows its height.\n useFlip(group, morphKey, \"var(--duration-morph)\", \"var(--ease-morph)\");\n useHeightFollow(frame, group, morphKey);\n // Chips that appear after the first render grow in; the ones there from the start never animate.\n const seen = useRef(new Set(options.map((option) => option.value)));\n const revealedAt = new Map<string, number>();\n let rank = 0;\n for (const option of visible) {\n if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));\n }\n\n function toggle(next: string) {\n const on = value.includes(next);\n if (!multiple) {\n onValueChange(on ? [] : [next]);\n return;\n }\n onValueChange(\n options.filter((option) => (option.value === next ? !on : value.includes(option.value))).map((option) => option.value),\n );\n }\n\n function toggleMore() {\n if (expanded) setPinned(value);\n // Only the chips this opening reveals grow in; the ones already on screen stay put.\n else seen.current = new Set(visible.map((option) => option.value));\n setExpanded(!expanded);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button[data-chip]\"));\n const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n if (index < 0) return;\n const last = buttons.length - 1;\n const moves: Record<string, number> = {\n ArrowRight: index === last ? 0 : index + 1,\n ArrowDown: index === last ? 0 : index + 1,\n ArrowLeft: index === 0 ? last : index - 1,\n ArrowUp: index === 0 ? last : index - 1,\n Home: 0,\n End: last,\n };\n if (!(event.key in moves)) return;\n event.preventDefault();\n buttons[moves[event.key]!]!.focus();\n }\n\n const moreText = expanded ? lessLabel : moreLabel(hidden);\n\n return (\n <div ref={frame} data-slot=\"chip-group-frame\" className=\"height-follow min-w-0\">\n <div\n ref={refs}\n role=\"group\"\n aria-label={label}\n data-slot=\"chip-group\"\n onKeyDown={onKeyDown}\n className={cn(\"relative isolate flex flex-wrap gap-2 box-border font-sans\", className)}\n >\n {visible.map((option) => {\n const selected = value.includes(option.value);\n const delay = revealedAt.get(option.value);\n return (\n <button\n {...chipAttrs(option.attrs)}\n key={option.value}\n type=\"button\"\n data-flip=\"\"\n data-chip={option.value}\n data-slot=\"chip\"\n aria-pressed={selected}\n tabIndex={tabStop === option.value ? 0 : -1}\n onClick={() => toggle(option.value)}\n onFocus={() => setActive(option.value)}\n style={delay === undefined ? undefined : ({ animationDelay: `${delay}ms` })}\n className={cn(\"chip\", delay === undefined ? \"\" : \"motion-safe:animate-chip-reveal\")}\n >\n <span data-selected={selected} className=\"chip-body\">\n <span aria-hidden=\"true\" className=\"chip-check\">\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4 12.5 9.5 18 20 6.5\" pathLength={1} />\n </svg>\n </span>\n <span aria-hidden=\"true\" className=\"chip-slot\" />\n <span className=\"truncate\">{option.label}</span>\n </span>\n </button>\n );\n })}\n {showMore ? (\n <button\n key={MORE}\n type=\"button\"\n data-flip=\"\"\n data-chip={MORE}\n data-more=\"\"\n aria-expanded={expanded}\n tabIndex={tabStop === MORE ? 0 : -1}\n onClick={toggleMore}\n onFocus={() => setActive(MORE)}\n className=\"chip chip-more\"\n >\n <span className=\"chip-body tabular-nums\">\n <SwapText swapKey={moreText} motion=\"rise\">\n {moreText}\n </SwapText>\n </span>\n </button>\n ) : null}\n </div>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ChipGroup, ChipGroupProps, ChipOption } from "./chip-group.js";
|
|
2
|
-
export { ChipGroup, type ChipGroupProps, type ChipOption };
|
|
1
|
+
import { ChipAttrs, ChipGroup, ChipGroupProps, ChipOption } from "./chip-group.js";
|
|
2
|
+
export { type ChipAttrs, ChipGroup, type ChipGroupProps, type ChipOption };
|
|
@@ -2,15 +2,42 @@
|
|
|
2
2
|
export interface CopyButtonProps {
|
|
3
3
|
/** What goes on the clipboard. */
|
|
4
4
|
value: string;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The label at rest. Unless `aria-label` is set it is also the button's accessible name.
|
|
7
|
+
* @default "Copy"
|
|
8
|
+
*/
|
|
6
9
|
children?: string;
|
|
7
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* An accessible name that differs from the visible label, for when the short label leans on its
|
|
12
|
+
* surroundings: visible "Copy" next to a code block, named "Copy code". It also prefixes the
|
|
13
|
+
* live-region announcement ("Copy code: Copied"). It stays constant while the visible label
|
|
14
|
+
* shows "Copied" or "Failed".
|
|
15
|
+
*
|
|
16
|
+
* Keep the visible label inside it, as written and ideally at the start ("Copy" → "Copy code",
|
|
17
|
+
* "Copy page" → "Copy page draft"), so a speech-input user can say what they see
|
|
18
|
+
* (WCAG 2.5.3 Label in Name). Unset or empty, the accessible name is `children`, as before.
|
|
19
|
+
*/
|
|
20
|
+
"aria-label"?: string;
|
|
21
|
+
/** Only the icon, a square button. The label (or `aria-label`) is still its accessible name. */
|
|
8
22
|
iconOnly?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
|
|
23
|
+
/**
|
|
24
|
+
* `plain` has no border until hovered. `primary` is the filled brand button, for a copy that is the
|
|
25
|
+
* main action of its row or dialog. @default "outline"
|
|
26
|
+
*/
|
|
27
|
+
variant?: "outline" | "plain" | "primary";
|
|
28
|
+
/**
|
|
29
|
+
* Control height, from the density scale: `sm` sits in a row, `md` matches a default Button.
|
|
30
|
+
* @default "sm"
|
|
31
|
+
*/
|
|
32
|
+
size?: "sm" | "md";
|
|
11
33
|
isDisabled?: boolean;
|
|
12
34
|
/** Called once the text has reached the clipboard. */
|
|
13
35
|
onCopied?: () => void;
|
|
36
|
+
/**
|
|
37
|
+
* Called when the clipboard refused the text, with what it rejected with. The button already shows
|
|
38
|
+
* "Failed" and announces it; use this to keep a toast or log of your own.
|
|
39
|
+
*/
|
|
40
|
+
onError?: (error: unknown) => void;
|
|
14
41
|
/** Extra classes. Merged last, so they win conflicts with ours. */
|
|
15
42
|
className?: string;
|
|
16
43
|
}
|
|
@@ -21,11 +48,16 @@ export interface CopyButtonProps {
|
|
|
21
48
|
* its label cell reserves the widest of the three texts.
|
|
22
49
|
*
|
|
23
50
|
* It goes back to rest by itself after a moment, and a live region announces the
|
|
24
|
-
* result. The accessible name is
|
|
51
|
+
* result. The accessible name is the resting label, or `aria-label` when the visible
|
|
52
|
+
* label is too short to stand alone; it never changes with the state.
|
|
25
53
|
*
|
|
26
54
|
* @example
|
|
27
55
|
* <CopyButton value={token} />
|
|
28
56
|
* <CopyButton value={url} iconOnly variant="plain" />
|
|
57
|
+
* <CopyButton value={code} aria-label="Copy code">Copy</CopyButton>
|
|
58
|
+
* <CopyButton value={ticket} variant="primary" size="md" onError={() => toast("Copy failed")}>
|
|
59
|
+
* Copy as ticket
|
|
60
|
+
* </CopyButton>
|
|
29
61
|
*/
|
|
30
62
|
export declare const CopyButton: import("react").ForwardRefExoticComponent<CopyButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
31
63
|
//#endregion
|
|
@@ -3,12 +3,29 @@ import { cn } from "../lib/cn.js";
|
|
|
3
3
|
import { buttonVariants } from "../button/button.js";
|
|
4
4
|
import { MorphText } from "../lib/morph-text.js";
|
|
5
5
|
import { IconSwap } from "../lib/icon-swap.js";
|
|
6
|
-
import { useCopyFeedback } from "../lib/use-copy-feedback.js";
|
|
6
|
+
import { callSafely, useCopyFeedback } from "../lib/use-copy-feedback.js";
|
|
7
7
|
import { forwardRef } from "react";
|
|
8
8
|
import { Button } from "react-aria-components";
|
|
9
9
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/copy-button/copy-button.tsx
|
|
11
11
|
const ICON = "size-4";
|
|
12
|
+
/** CopyButton's variant names, mapped onto Button's. */
|
|
13
|
+
const BUTTON_VARIANT = {
|
|
14
|
+
outline: "outlineNeutral",
|
|
15
|
+
plain: "ghost",
|
|
16
|
+
primary: "primary"
|
|
17
|
+
};
|
|
18
|
+
/** An icon-only button is square: as wide as its rung is tall. */
|
|
19
|
+
const ICON_ONLY_WIDTH = {
|
|
20
|
+
sm: " w-(--density-control-h-sm) px-0",
|
|
21
|
+
md: " w-(--density-control-h-md) px-0"
|
|
22
|
+
};
|
|
23
|
+
/** The result icon's tone on a page-surface button; at rest it takes the label's ink. */
|
|
24
|
+
const RESULT_TONE = {
|
|
25
|
+
idle: void 0,
|
|
26
|
+
copied: "text-success-strong",
|
|
27
|
+
error: "text-destructive-strong"
|
|
28
|
+
};
|
|
12
29
|
/** The tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place. */
|
|
13
30
|
function DrawnCheck() {
|
|
14
31
|
return /* @__PURE__ */ jsx("svg", {
|
|
@@ -65,32 +82,39 @@ function StateIcon({ state }) {
|
|
|
65
82
|
* its label cell reserves the widest of the three texts.
|
|
66
83
|
*
|
|
67
84
|
* It goes back to rest by itself after a moment, and a live region announces the
|
|
68
|
-
* result. The accessible name is
|
|
85
|
+
* result. The accessible name is the resting label, or `aria-label` when the visible
|
|
86
|
+
* label is too short to stand alone; it never changes with the state.
|
|
69
87
|
*
|
|
70
88
|
* @example
|
|
71
89
|
* <CopyButton value={token} />
|
|
72
90
|
* <CopyButton value={url} iconOnly variant="plain" />
|
|
91
|
+
* <CopyButton value={code} aria-label="Copy code">Copy</CopyButton>
|
|
92
|
+
* <CopyButton value={ticket} variant="primary" size="md" onError={() => toast("Copy failed")}>
|
|
93
|
+
* Copy as ticket
|
|
94
|
+
* </CopyButton>
|
|
73
95
|
*/
|
|
74
|
-
const CopyButton = forwardRef(function CopyButton({ value, children: label = "Copy", iconOnly = false, variant = "outline", isDisabled, onCopied, className }, ref) {
|
|
96
|
+
const CopyButton = forwardRef(function CopyButton({ value, children: label = "Copy", "aria-label": ariaLabel, iconOnly = false, variant = "outline", size = "sm", isDisabled, onCopied, onError, className }, ref) {
|
|
75
97
|
const { state, copy } = useCopyFeedback();
|
|
98
|
+
const name = ariaLabel || label;
|
|
76
99
|
const text = state === "copied" ? "Copied" : state === "error" ? "Failed" : label;
|
|
77
100
|
const pressStart = (event) => {
|
|
78
101
|
event.target.style.setProperty("--press-scale", iconOnly ? "0.96" : "0.97");
|
|
79
102
|
};
|
|
80
103
|
const classes = cn(buttonVariants({
|
|
81
|
-
variant: variant
|
|
82
|
-
size
|
|
83
|
-
}) + " [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]" + (iconOnly ?
|
|
104
|
+
variant: BUTTON_VARIANT[variant],
|
|
105
|
+
size
|
|
106
|
+
}) + " [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]" + (iconOnly ? ICON_ONLY_WIDTH[size] : ""), className);
|
|
107
|
+
const onBrandFill = variant === "primary";
|
|
84
108
|
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
85
109
|
ref,
|
|
86
110
|
"data-slot": "button",
|
|
87
111
|
"data-copy-state": state,
|
|
88
|
-
"aria-label":
|
|
112
|
+
"aria-label": name,
|
|
89
113
|
isDisabled,
|
|
90
114
|
onPressStart: pressStart,
|
|
91
115
|
onPress: () => {
|
|
92
|
-
copy(value).then((ok) => {
|
|
93
|
-
if (ok) onCopied?.();
|
|
116
|
+
copy(value, onError).then((ok) => {
|
|
117
|
+
if (ok) callSafely(() => onCopied?.());
|
|
94
118
|
});
|
|
95
119
|
},
|
|
96
120
|
className: classes,
|
|
@@ -99,7 +123,7 @@ const CopyButton = forwardRef(function CopyButton({ value, children: label = "Co
|
|
|
99
123
|
calm: true,
|
|
100
124
|
className: cn(ICON, "relative"),
|
|
101
125
|
children: /* @__PURE__ */ jsx("span", {
|
|
102
|
-
className:
|
|
126
|
+
className: onBrandFill ? void 0 : RESULT_TONE[state],
|
|
103
127
|
children: /* @__PURE__ */ jsx(StateIcon, { state })
|
|
104
128
|
})
|
|
105
129
|
}), iconOnly ? null : /* @__PURE__ */ jsxs("span", {
|
|
@@ -125,7 +149,7 @@ const CopyButton = forwardRef(function CopyButton({ value, children: label = "Co
|
|
|
125
149
|
className: "sr-only",
|
|
126
150
|
role: "status",
|
|
127
151
|
"aria-live": "polite",
|
|
128
|
-
children: state === "idle" ? "" : state === "error" ?
|
|
152
|
+
children: state === "idle" ? "" : state === "error" ? name + ": Could not copy" : name + ": Copied"
|
|
129
153
|
})] });
|
|
130
154
|
});
|
|
131
155
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"copy-button.js","names":["AriaButton"],"sources":["../../src/copy-button/copy-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Button as AriaButton, type PressEvent } from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { IconSwap } from \"../lib/icon-swap.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport { useCopyFeedback, type CopyFeedbackState } from \"../lib/use-copy-feedback.js\";\n\nexport interface CopyButtonProps {\n /** What goes on the clipboard. */\n value: string;\n /** The label at rest, and the button's accessible name. @default \"Copy\" */\n children?: string;\n /** Only the icon, a square button. The label is still its accessible name. */\n iconOnly?: boolean;\n /** `plain` has no border until hovered. @default \"outline\" */\n variant?: \"outline\" | \"plain\";\n isDisabled?: boolean;\n /** Called once the text has reached the clipboard. */\n onCopied?: () => void;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst ICON = \"size-4\";\n\n/** The tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place. */\nfunction DrawnCheck() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 12l5 5L20 6\"\n pathLength={1}\n className=\"[stroke-dasharray:1] motion-safe:animate-check-draw-calm\"\n />\n </svg>\n );\n}\n\nfunction StateIcon({ state }: { state: CopyFeedbackState }) {\n if (state === \"copied\") return <DrawnCheck />;\n const common = {\n className: ICON,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 1.75,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n \"aria-hidden\": true,\n } as const;\n if (state === \"error\") {\n return (\n <svg {...common}>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4M12 16h.01\" />\n </svg>\n );\n }\n return (\n <svg {...common}>\n <rect x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n );\n}\n\n/**\n * Copies a value and says so, calmly. The icon trades places in one soft breath\n * (copy, then a tick that draws itself, or an alert if the clipboard refused)\n * while the label's changed letters rise in, and the button never changes width:\n * its label cell reserves the widest of the three texts.\n *\n * It goes back to rest by itself after a moment, and a live region announces the\n * result. The accessible name is always the resting label.\n *\n * @example\n * <CopyButton value={token} />\n * <CopyButton value={url} iconOnly variant=\"plain\" />\n */\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n {\n value,\n children: label = \"Copy\",\n iconOnly = false,\n variant = \"outline\",\n isDisabled,\n onCopied,\n className,\n },\n ref,\n) {\n const { state, copy } = useCopyFeedback();\n const text = state === \"copied\" ? \"Copied\" : state === \"error\" ? \"Failed\" : label;\n\n // Not width-based like Button's press: an icon button presses a little deeper, every other one the same.\n const pressStart = (event: PressEvent) => {\n (event.target as HTMLElement).style.setProperty(\"--press-scale\", iconOnly ? \"0.96\" : \"0.97\");\n };\n\n const classes = cn(\n buttonVariants({ variant: variant === \"plain\" ? \"ghost\" : \"outlineNeutral\", size: \"sm\" }) +\n \" [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]\" +\n (iconOnly ? \" w-(--density-control-h-sm) px-0\" : \"\"),\n className,\n );\n\n return (\n <>\n <AriaButton\n ref={ref}\n data-slot=\"button\"\n data-copy-state={state}\n aria-label={label}\n isDisabled={isDisabled}\n onPressStart={pressStart}\n onPress={() => {\n void copy(value).then((ok) => {\n if (ok) onCopied?.();\n });\n }}\n className={classes}\n >\n <IconSwap iconKey={state} calm className={cn(ICON, \"relative\")}>\n <span\n className={\n state === \"copied\"\n ? \"text-success-strong\"\n : state === \"error\"\n ? \"text-destructive-strong\"\n : undefined\n }\n >\n <StateIcon state={state} />\n </span>\n </IconSwap>\n {iconOnly ? null : (\n // The cell reserves the widest state, so nothing moves but the letters. The inline padding gives blurred\n // edge letters room before the horizontal clip; the negative margin keeps the box the same size.\n <span\n className=\"-mx-1 grid min-w-0 overflow-x-clip px-1 text-start whitespace-nowrap\"\n aria-hidden=\"true\"\n >\n {[label, \"Copied\", \"Failed\"].map((measure) => (\n <span key={measure} className=\"invisible col-start-1 row-start-1\">\n {measure}\n </span>\n ))}\n <span className=\"col-start-1 row-start-1 justify-self-start\">\n <MorphText text={text} morphWidth={false} calm />\n </span>\n </span>\n )}\n </AriaButton>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state === \"idle\"\n ? \"\"\n : state === \"error\"\n ? label + \": Could not copy\"\n : label + \": Copied\"}\n </span>\n </>\n );\n});\n"],"mappings":";;;;;;;;;;AA2BA,MAAM,OAAO;;AAGb,SAAS,aAAa;CACpB,OACE,oBAAC,OAAD;EACE,WAAW;EACX,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAa;EACb,eAAc;EACd,gBAAe;EACf,eAAY;EAEZ,UAAA,oBAAC,QAAD;GACE,GAAE;GACF,YAAY;GACZ,WAAU;EACX,CAAA;CACE,CAAA;AAET;AAEA,SAAS,UAAU,EAAE,SAAuC;CAC1D,IAAI,UAAU,UAAU,OAAO,oBAAC,YAAD,CAAa,CAAA;CAC5C,MAAM,SAAS;EACb,WAAW;EACX,SAAS;EACT,MAAM;EACN,QAAQ;EACR,aAAa;EACb,eAAe;EACf,gBAAgB;EAChB,eAAe;CACjB;CACA,IAAI,UAAU,SACZ,OACE,qBAAC,OAAD;EAAK,GAAI;EAAT,UAAA,CACE,oBAAC,UAAD;GAAQ,IAAG;GAAK,IAAG;GAAK,GAAE;EAAM,CAAA,GAChC,oBAAC,QAAD,EAAM,GAAE,oBAAqB,CAAA,CAC1B;;CAGT,OACE,qBAAC,OAAD;EAAK,GAAI;EAAT,UAAA,CACE,oBAAC,QAAD;GAAM,GAAE;GAAI,GAAE;GAAI,OAAM;GAAK,QAAO;GAAK,IAAG;EAAK,CAAA,GACjD,oBAAC,QAAD,EAAM,GAAE,0DAA2D,CAAA,CAChE;;AAET;;;;;;;;;;;;;;AAeA,MAAa,aAAa,WAA+C,SAAS,WAChF,EACE,OACA,UAAU,QAAQ,QAClB,WAAW,OACX,UAAU,WACV,YACA,UACA,aAEF,KACA;CACA,MAAM,EAAE,OAAO,SAAS,gBAAgB;CACxC,MAAM,OAAO,UAAU,WAAW,WAAW,UAAU,UAAU,WAAW;CAG5E,MAAM,cAAc,UAAsB;EACxC,MAAO,OAAuB,MAAM,YAAY,iBAAiB,WAAW,SAAS,MAAM;CAC7F;CAEA,MAAM,UAAU,GACd,eAAe;EAAE,SAAS,YAAY,UAAU,UAAU;EAAkB,MAAM;CAAK,CAAC,IACtF,sFACC,WAAW,qCAAqC,KACnD,SACF;CAEA,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,qBAACA,QAAD;EACO;EACL,aAAU;EACV,mBAAiB;EACjB,cAAY;EACA;EACZ,cAAc;EACd,eAAe;GACb,KAAU,KAAK,CAAC,CAAC,MAAM,OAAO;IAC5B,IAAI,IAAI,WAAW;GACrB,CAAC;EACH;EACA,WAAW;EAZb,UAAA,CAcE,oBAAC,UAAD;GAAU,SAAS;GAAO,MAAA;GAAK,WAAW,GAAG,MAAM,UAAU;GAC3D,UAAA,oBAAC,QAAD;IACE,WACE,UAAU,WACN,wBACA,UAAU,UACR,4BACA,KAAA;IAGR,UAAA,oBAAC,WAAD,EAAkB,MAAQ,CAAA;GACtB,CAAA;EACE,CAAA,GACT,WAAW,OAGV,qBAAC,QAAD;GACE,WAAU;GACV,eAAY;GAFd,UAAA,CAIG;IAAC;IAAO;IAAU;GAAQ,CAAC,CAAC,KAAK,YAChC,oBAAC,QAAD;IAAoB,WAAU;IAC3B,UAAA;GACG,GAFK,OAEL,CACP,GACD,oBAAC,QAAD;IAAM,WAAU;IACd,UAAA,oBAAC,WAAD;KAAiB;KAAM,YAAY;KAAO,MAAA;IAAM,CAAA;GAC5C,CAAA,CACF;EAEE,CAAA,CAAA;CACZ,CAAA,GAAA,oBAAC,QAAD;EAAM,WAAU;EAAU,MAAK;EAAS,aAAU;EAC/C,UAAA,UAAU,SACP,KACA,UAAU,UACR,QAAQ,qBACR,QAAQ;CACV,CAAA,CACN,EAAA,CAAA;AAEN,CAAC"}
|
|
1
|
+
{"version":3,"file":"copy-button.js","names":["AriaButton"],"sources":["../../src/copy-button/copy-button.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Button as AriaButton, type PressEvent } from \"react-aria-components\";\n\nimport { buttonVariants } from \"../button/button.js\";\nimport { cn } from \"../lib/cn.js\";\nimport { IconSwap } from \"../lib/icon-swap.js\";\nimport { MorphText } from \"../lib/morph-text.js\";\nimport { callSafely, useCopyFeedback, type CopyFeedbackState } from \"../lib/use-copy-feedback.js\";\n\nexport interface CopyButtonProps {\n /** What goes on the clipboard. */\n value: string;\n /**\n * The label at rest. Unless `aria-label` is set it is also the button's accessible name.\n * @default \"Copy\"\n */\n children?: string;\n /**\n * An accessible name that differs from the visible label, for when the short label leans on its\n * surroundings: visible \"Copy\" next to a code block, named \"Copy code\". It also prefixes the\n * live-region announcement (\"Copy code: Copied\"). It stays constant while the visible label\n * shows \"Copied\" or \"Failed\".\n *\n * Keep the visible label inside it, as written and ideally at the start (\"Copy\" → \"Copy code\",\n * \"Copy page\" → \"Copy page draft\"), so a speech-input user can say what they see\n * (WCAG 2.5.3 Label in Name). Unset or empty, the accessible name is `children`, as before.\n */\n \"aria-label\"?: string;\n /** Only the icon, a square button. The label (or `aria-label`) is still its accessible name. */\n iconOnly?: boolean;\n /**\n * `plain` has no border until hovered. `primary` is the filled brand button, for a copy that is the\n * main action of its row or dialog. @default \"outline\"\n */\n variant?: \"outline\" | \"plain\" | \"primary\";\n /**\n * Control height, from the density scale: `sm` sits in a row, `md` matches a default Button.\n * @default \"sm\"\n */\n size?: \"sm\" | \"md\";\n isDisabled?: boolean;\n /** Called once the text has reached the clipboard. */\n onCopied?: () => void;\n /**\n * Called when the clipboard refused the text, with what it rejected with. The button already shows\n * \"Failed\" and announces it; use this to keep a toast or log of your own.\n */\n onError?: (error: unknown) => void;\n /** Extra classes. Merged last, so they win conflicts with ours. */\n className?: string;\n}\n\nconst ICON = \"size-4\";\n\n/** CopyButton's variant names, mapped onto Button's. */\nconst BUTTON_VARIANT = {\n outline: \"outlineNeutral\",\n plain: \"ghost\",\n primary: \"primary\",\n} as const;\n\n/** An icon-only button is square: as wide as its rung is tall. */\nconst ICON_ONLY_WIDTH = {\n sm: \" w-(--density-control-h-sm) px-0\",\n md: \" w-(--density-control-h-md) px-0\",\n} as const;\n\n/** The result icon's tone on a page-surface button; at rest it takes the label's ink. */\nconst RESULT_TONE: Record<CopyFeedbackState, string | undefined> = {\n idle: undefined,\n copied: \"text-success-strong\",\n error: \"text-destructive-strong\",\n};\n\n/** The tick draws itself from its short stroke, the way a hand would write it: quick to start, then easing into place. */\nfunction DrawnCheck() {\n return (\n <svg\n className={ICON}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path\n d=\"M4 12l5 5L20 6\"\n pathLength={1}\n className=\"[stroke-dasharray:1] motion-safe:animate-check-draw-calm\"\n />\n </svg>\n );\n}\n\nfunction StateIcon({ state }: { state: CopyFeedbackState }) {\n if (state === \"copied\") return <DrawnCheck />;\n const common = {\n className: ICON,\n viewBox: \"0 0 24 24\",\n fill: \"none\",\n stroke: \"currentColor\",\n strokeWidth: 1.75,\n strokeLinecap: \"round\",\n strokeLinejoin: \"round\",\n \"aria-hidden\": true,\n } as const;\n if (state === \"error\") {\n return (\n <svg {...common}>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4M12 16h.01\" />\n </svg>\n );\n }\n return (\n <svg {...common}>\n <rect x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\" />\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\n </svg>\n );\n}\n\n/**\n * Copies a value and says so, calmly. The icon trades places in one soft breath\n * (copy, then a tick that draws itself, or an alert if the clipboard refused)\n * while the label's changed letters rise in, and the button never changes width:\n * its label cell reserves the widest of the three texts.\n *\n * It goes back to rest by itself after a moment, and a live region announces the\n * result. The accessible name is the resting label, or `aria-label` when the visible\n * label is too short to stand alone; it never changes with the state.\n *\n * @example\n * <CopyButton value={token} />\n * <CopyButton value={url} iconOnly variant=\"plain\" />\n * <CopyButton value={code} aria-label=\"Copy code\">Copy</CopyButton>\n * <CopyButton value={ticket} variant=\"primary\" size=\"md\" onError={() => toast(\"Copy failed\")}>\n * Copy as ticket\n * </CopyButton>\n */\nexport const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(function CopyButton(\n {\n value,\n children: label = \"Copy\",\n \"aria-label\": ariaLabel,\n iconOnly = false,\n variant = \"outline\",\n size = \"sm\",\n isDisabled,\n onCopied,\n onError,\n className,\n },\n ref,\n) {\n const { state, copy } = useCopyFeedback();\n // Empty falls back too: an empty aria-label would leave the button with no name at all.\n const name = ariaLabel || label;\n const text = state === \"copied\" ? \"Copied\" : state === \"error\" ? \"Failed\" : label;\n\n // Not width-based like Button's press: an icon button presses a little deeper, every other one the same.\n const pressStart = (event: PressEvent) => {\n (event.target as HTMLElement).style.setProperty(\"--press-scale\", iconOnly ? \"0.96\" : \"0.97\");\n };\n\n const classes = cn(\n buttonVariants({ variant: BUTTON_VARIANT[variant], size }) +\n \" [--release-duration:var(--duration-gentle)] [--release-ease:var(--ease-gentle)]\" +\n (iconOnly ? ICON_ONLY_WIDTH[size] : \"\"),\n className,\n );\n // On the brand fill the result icon keeps the label's ink: the success and destructive tones are set\n // against the page surface, not the orange.\n const onBrandFill = variant === \"primary\";\n\n return (\n <>\n <AriaButton\n ref={ref}\n data-slot=\"button\"\n data-copy-state={state}\n aria-label={name}\n isDisabled={isDisabled}\n onPressStart={pressStart}\n onPress={() => {\n void copy(value, onError).then((ok) => {\n if (ok) callSafely(() => onCopied?.());\n });\n }}\n className={classes}\n >\n <IconSwap iconKey={state} calm className={cn(ICON, \"relative\")}>\n <span className={onBrandFill ? undefined : RESULT_TONE[state]}>\n <StateIcon state={state} />\n </span>\n </IconSwap>\n {iconOnly ? null : (\n // The cell reserves the widest state, so nothing moves but the letters. The inline padding gives blurred\n // edge letters room before the horizontal clip; the negative margin keeps the box the same size.\n <span\n className=\"-mx-1 grid min-w-0 overflow-x-clip px-1 text-start whitespace-nowrap\"\n aria-hidden=\"true\"\n >\n {[label, \"Copied\", \"Failed\"].map((measure) => (\n <span key={measure} className=\"invisible col-start-1 row-start-1\">\n {measure}\n </span>\n ))}\n <span className=\"col-start-1 row-start-1 justify-self-start\">\n <MorphText text={text} morphWidth={false} calm />\n </span>\n </span>\n )}\n </AriaButton>\n <span className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state === \"idle\" ? \"\" : state === \"error\" ? name + \": Could not copy\" : name + \": Copied\"}\n </span>\n </>\n );\n});\n"],"mappings":";;;;;;;;;;AAsDA,MAAM,OAAO;;AAGb,MAAM,iBAAiB;CACrB,SAAS;CACT,OAAO;CACP,SAAS;AACX;;AAGA,MAAM,kBAAkB;CACtB,IAAI;CACJ,IAAI;AACN;;AAGA,MAAM,cAA6D;CACjE,MAAM,KAAA;CACN,QAAQ;CACR,OAAO;AACT;;AAGA,SAAS,aAAa;CACpB,OACE,oBAAC,OAAD;EACE,WAAW;EACX,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAa;EACb,eAAc;EACd,gBAAe;EACf,eAAY;EAEZ,UAAA,oBAAC,QAAD;GACE,GAAE;GACF,YAAY;GACZ,WAAU;EACX,CAAA;CACE,CAAA;AAET;AAEA,SAAS,UAAU,EAAE,SAAuC;CAC1D,IAAI,UAAU,UAAU,OAAO,oBAAC,YAAD,CAAa,CAAA;CAC5C,MAAM,SAAS;EACb,WAAW;EACX,SAAS;EACT,MAAM;EACN,QAAQ;EACR,aAAa;EACb,eAAe;EACf,gBAAgB;EAChB,eAAe;CACjB;CACA,IAAI,UAAU,SACZ,OACE,qBAAC,OAAD;EAAK,GAAI;EAAT,UAAA,CACE,oBAAC,UAAD;GAAQ,IAAG;GAAK,IAAG;GAAK,GAAE;EAAM,CAAA,GAChC,oBAAC,QAAD,EAAM,GAAE,oBAAqB,CAAA,CAC1B;;CAGT,OACE,qBAAC,OAAD;EAAK,GAAI;EAAT,UAAA,CACE,oBAAC,QAAD;GAAM,GAAE;GAAI,GAAE;GAAI,OAAM;GAAK,QAAO;GAAK,IAAG;EAAK,CAAA,GACjD,oBAAC,QAAD,EAAM,GAAE,0DAA2D,CAAA,CAChE;;AAET;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,aAAa,WAA+C,SAAS,WAChF,EACE,OACA,UAAU,QAAQ,QAClB,cAAc,WACd,WAAW,OACX,UAAU,WACV,OAAO,MACP,YACA,UACA,SACA,aAEF,KACA;CACA,MAAM,EAAE,OAAO,SAAS,gBAAgB;CAExC,MAAM,OAAO,aAAa;CAC1B,MAAM,OAAO,UAAU,WAAW,WAAW,UAAU,UAAU,WAAW;CAG5E,MAAM,cAAc,UAAsB;EACxC,MAAO,OAAuB,MAAM,YAAY,iBAAiB,WAAW,SAAS,MAAM;CAC7F;CAEA,MAAM,UAAU,GACd,eAAe;EAAE,SAAS,eAAe;EAAU;CAAK,CAAC,IACvD,sFACC,WAAW,gBAAgB,QAAQ,KACtC,SACF;CAGA,MAAM,cAAc,YAAY;CAEhC,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,qBAACA,QAAD;EACO;EACL,aAAU;EACV,mBAAiB;EACjB,cAAY;EACA;EACZ,cAAc;EACd,eAAe;GACb,KAAU,OAAO,OAAO,CAAC,CAAC,MAAM,OAAO;IACrC,IAAI,IAAI,iBAAiB,WAAW,CAAC;GACvC,CAAC;EACH;EACA,WAAW;EAZb,UAAA,CAcE,oBAAC,UAAD;GAAU,SAAS;GAAO,MAAA;GAAK,WAAW,GAAG,MAAM,UAAU;GAC3D,UAAA,oBAAC,QAAD;IAAM,WAAW,cAAc,KAAA,IAAY,YAAY;IACrD,UAAA,oBAAC,WAAD,EAAkB,MAAQ,CAAA;GACtB,CAAA;EACE,CAAA,GACT,WAAW,OAGV,qBAAC,QAAD;GACE,WAAU;GACV,eAAY;GAFd,UAAA,CAIG;IAAC;IAAO;IAAU;GAAQ,CAAC,CAAC,KAAK,YAChC,oBAAC,QAAD;IAAoB,WAAU;IAC3B,UAAA;GACG,GAFK,OAEL,CACP,GACD,oBAAC,QAAD;IAAM,WAAU;IACd,UAAA,oBAAC,WAAD;KAAiB;KAAM,YAAY;KAAO,MAAA;IAAM,CAAA;GAC5C,CAAA,CACF;EAEE,CAAA,CAAA;CACZ,CAAA,GAAA,oBAAC,QAAD;EAAM,WAAU;EAAU,MAAK;EAAS,aAAU;EAC/C,UAAA,UAAU,SAAS,KAAK,UAAU,UAAU,OAAO,qBAAqB,OAAO;CAC5E,CAAA,CACN,EAAA,CAAA;AAEN,CAAC"}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* focus, selection) slides out along its middle and thickens; the rest dim.
|
|
11
11
|
* The centre rolls like a drum between the total and each segment, and every
|
|
12
12
|
* number counts to its new value. Small parts fold into one "Other" segment.
|
|
13
|
+
* Where the parts do not add up to anything (a rate such as CTR, CPC or ROAS
|
|
14
|
+
* per source), `center` hides the total or puts a figure of the caller's own
|
|
15
|
+
* at rest instead; the segment readouts still roll in on hover and focus.
|
|
13
16
|
*
|
|
14
17
|
* Plain SVG and CSS; one small spring tween moves the arcs. The legend is the
|
|
15
18
|
* keyboard path (or the ring itself, without a legend), and a visually hidden
|
|
@@ -23,6 +26,16 @@ export interface DonutChartDatum {
|
|
|
23
26
|
/** Any CSS colour. Segments default to chart-1 to chart-4 in data order, then neutral steps. */
|
|
24
27
|
color?: string;
|
|
25
28
|
}
|
|
29
|
+
/** A figure of the caller's own for the centre at rest, in place of the summed total. */
|
|
30
|
+
export interface DonutChartCenter {
|
|
31
|
+
/** The line above the figure, such as "Blended CTR". */
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* The figure. A number is formatted with `formatValue` and counts to each new
|
|
35
|
+
* value, with `unit` under it; a string is shown as written (`"2.4%"`).
|
|
36
|
+
*/
|
|
37
|
+
value: number | string;
|
|
38
|
+
}
|
|
26
39
|
export interface DonutChartProps {
|
|
27
40
|
data: DonutChartDatum[];
|
|
28
41
|
/** What the whole is, such as "Visits by source". Names the chart for assistive technology. */
|
|
@@ -32,6 +45,15 @@ export interface DonutChartProps {
|
|
|
32
45
|
formatValue?: (value: number) => string;
|
|
33
46
|
/** Centre label at rest, above the total. @default "Total" */
|
|
34
47
|
totalLabel?: string;
|
|
48
|
+
/**
|
|
49
|
+
* What the centre shows at rest. `"total"` sums the visible segments under
|
|
50
|
+
* `totalLabel`. `"none"` leaves it empty, for parts whose sum means nothing
|
|
51
|
+
* (CTR, CPC or ROAS by source). An object puts a figure of your own there
|
|
52
|
+
* instead, such as the blended rate. Hovering or focusing a segment still
|
|
53
|
+
* shows that segment's readout either way.
|
|
54
|
+
* @default "total"
|
|
55
|
+
*/
|
|
56
|
+
center?: "total" | "none" | DonutChartCenter;
|
|
35
57
|
/** Diameter in pixels. The chart scales down to fit narrower containers. @default 208 */
|
|
36
58
|
size?: number;
|
|
37
59
|
/** Ring thickness in pixels. @default 24 */
|