@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +81 -2
- package/check.mjs +236 -0
- package/components/accordion/accordion.d.ts +13 -0
- package/components/accordion/accordion.js +35 -0
- package/components/accordion/accordion.module.css +15 -0
- package/components/action-button/action-button.d.ts +11 -0
- package/components/action-button/action-button.js +57 -0
- package/components/action-button/action-button.module.css +15 -0
- package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
- package/components/activity-heatmap/activity-heatmap.js +303 -0
- package/components/activity-heatmap/activity-heatmap.module.css +83 -0
- package/components/alert/alert.d.ts +13 -0
- package/components/alert/alert.js +79 -0
- package/components/alert/alert.module.css +15 -0
- package/components/animated-counter/animated-counter.d.ts +13 -0
- package/components/animated-counter/animated-counter.js +73 -0
- package/components/animated-counter/animated-counter.module.css +12 -0
- package/components/announcement-bar/announcement-bar.d.ts +53 -0
- package/components/announcement-bar/announcement-bar.js +187 -0
- package/components/announcement-bar/announcement-bar.module.css +60 -0
- package/components/avatar/avatar.d.ts +8 -0
- package/components/avatar/avatar.js +18 -0
- package/components/avatar/avatar.module.css +60 -0
- package/components/avatar-group/avatar-group.d.ts +13 -0
- package/components/avatar-group/avatar-group.js +24 -0
- package/components/avatar-group/avatar-group.module.css +27 -0
- package/components/badge/badge.d.ts +10 -0
- package/components/badge/badge.js +66 -0
- package/components/badge/badge.module.css +52 -0
- package/components/bar-chart/bar-chart.d.ts +32 -0
- package/components/bar-chart/bar-chart.js +327 -0
- package/components/bar-chart/bar-chart.module.css +49 -0
- package/components/billing-toggle/billing-toggle.d.ts +35 -0
- package/components/billing-toggle/billing-toggle.js +83 -0
- package/components/billing-toggle/billing-toggle.module.css +139 -0
- package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
- package/components/bottom-sheet/bottom-sheet.js +386 -0
- package/components/bottom-sheet/bottom-sheet.module.css +69 -0
- package/components/breadcrumb/breadcrumb.d.ts +13 -0
- package/components/breadcrumb/breadcrumb.js +19 -0
- package/components/breadcrumb/breadcrumb.module.css +12 -0
- package/components/brush-chart/brush-chart.d.ts +45 -0
- package/components/brush-chart/brush-chart.js +426 -0
- package/components/brush-chart/brush-chart.module.css +69 -0
- package/components/button/button.d.ts +10 -0
- package/components/button/button.js +97 -0
- package/components/button/button.module.css +39 -0
- package/components/button-group/button-group.d.ts +59 -0
- package/components/button-group/button-group.js +280 -0
- package/components/button-group/button-group.module.css +87 -0
- package/components/button-link/button-link.d.ts +24 -0
- package/components/button-link/button-link.js +11 -0
- package/components/button-link/button-link.module.css +7 -0
- package/components/calendar/calendar.d.ts +22 -0
- package/components/calendar/calendar.js +253 -0
- package/components/calendar/calendar.module.css +51 -0
- package/components/card/card.d.ts +19 -0
- package/components/card/card.js +108 -0
- package/components/card/card.module.css +51 -0
- package/components/card-stack/card-stack.d.ts +34 -0
- package/components/card-stack/card-stack.js +291 -0
- package/components/card-stack/card-stack.module.css +63 -0
- package/components/carousel/carousel.d.ts +28 -0
- package/components/carousel/carousel.js +332 -0
- package/components/carousel/carousel.module.css +42 -0
- package/components/chat-thread/chat-thread.d.ts +135 -0
- package/components/chat-thread/chat-thread.js +406 -0
- package/components/chat-thread/chat-thread.module.css +242 -0
- package/components/checkbox/checkbox.d.ts +7 -0
- package/components/checkbox/checkbox.js +23 -0
- package/components/checkbox/checkbox.module.css +15 -0
- package/components/chip-group/chip-group.d.ts +24 -0
- package/components/chip-group/chip-group.js +196 -0
- package/components/chip-group/chip-group.module.css +38 -0
- package/components/code-block/code-block.d.ts +11 -0
- package/components/code-block/code-block.js +133 -0
- package/components/code-block/code-block.module.css +43 -0
- package/components/color-picker/color-picker.d.ts +40 -0
- package/components/color-picker/color-picker.js +449 -0
- package/components/color-picker/color-picker.module.css +111 -0
- package/components/combobox/combobox.d.ts +19 -0
- package/components/combobox/combobox.js +167 -0
- package/components/combobox/combobox.module.css +28 -0
- package/components/command-palette/command-palette.d.ts +20 -0
- package/components/command-palette/command-palette.js +151 -0
- package/components/command-palette/command-palette.module.css +94 -0
- package/components/comment-thread/comment-thread.d.ts +120 -0
- package/components/comment-thread/comment-thread.js +343 -0
- package/components/comment-thread/comment-thread.module.css +195 -0
- package/components/confirm-morph/confirm-morph.d.ts +65 -0
- package/components/confirm-morph/confirm-morph.js +355 -0
- package/components/confirm-morph/confirm-morph.module.css +146 -0
- package/components/context-menu/context-menu.d.ts +18 -0
- package/components/context-menu/context-menu.js +148 -0
- package/components/context-menu/context-menu.module.css +17 -0
- package/components/copy-button/copy-button.d.ts +10 -0
- package/components/copy-button/copy-button.js +69 -0
- package/components/copy-button/copy-button.module.css +21 -0
- package/components/countdown/countdown.d.ts +31 -0
- package/components/countdown/countdown.js +177 -0
- package/components/countdown/countdown.module.css +49 -0
- package/components/date-picker/date-picker.d.ts +17 -0
- package/components/date-picker/date-picker.js +100 -0
- package/components/date-picker/date-picker.module.css +34 -0
- package/components/date-range-picker/date-range-picker.d.ts +44 -0
- package/components/date-range-picker/date-range-picker.js +534 -0
- package/components/date-range-picker/date-range-picker.module.css +214 -0
- package/components/dialog/dialog.d.ts +11 -0
- package/components/dialog/dialog.js +52 -0
- package/components/dialog/dialog.module.css +36 -0
- package/components/donut-chart/donut-chart.d.ts +46 -0
- package/components/donut-chart/donut-chart.js +382 -0
- package/components/donut-chart/donut-chart.module.css +48 -0
- package/components/drawer/drawer.d.ts +20 -0
- package/components/drawer/drawer.js +111 -0
- package/components/drawer/drawer.module.css +97 -0
- package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
- package/components/dropdown-menu/dropdown-menu.js +39 -0
- package/components/dropdown-menu/dropdown-menu.module.css +38 -0
- package/components/elastic-slider/elastic-slider.d.ts +39 -0
- package/components/elastic-slider/elastic-slider.js +261 -0
- package/components/elastic-slider/elastic-slider.module.css +40 -0
- package/components/empty-state/empty-state.d.ts +12 -0
- package/components/empty-state/empty-state.js +69 -0
- package/components/empty-state/empty-state.module.css +13 -0
- package/components/expandable-card/expandable-card.d.ts +13 -0
- package/components/expandable-card/expandable-card.js +89 -0
- package/components/expandable-card/expandable-card.module.css +22 -0
- package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
- package/components/expanding-button-group/expanding-button-group.js +258 -0
- package/components/expanding-button-group/expanding-button-group.module.css +44 -0
- package/components/expanding-search/expanding-search.d.ts +38 -0
- package/components/expanding-search/expanding-search.js +242 -0
- package/components/expanding-search/expanding-search.module.css +52 -0
- package/components/file-dropzone/file-dropzone.d.ts +44 -0
- package/components/file-dropzone/file-dropzone.js +379 -0
- package/components/file-dropzone/file-dropzone.module.css +115 -0
- package/components/file-upload/file-upload.d.ts +22 -0
- package/components/file-upload/file-upload.js +192 -0
- package/components/file-upload/file-upload.module.css +49 -0
- package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
- package/components/filter-toolbar/filter-toolbar.js +446 -0
- package/components/filter-toolbar/filter-toolbar.module.css +70 -0
- package/components/floating-button-group/floating-button-group.d.ts +51 -0
- package/components/floating-button-group/floating-button-group.js +215 -0
- package/components/floating-button-group/floating-button-group.module.css +74 -0
- package/components/gauge/gauge.d.ts +18 -0
- package/components/gauge/gauge.js +101 -0
- package/components/gauge/gauge.module.css +22 -0
- package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
- package/components/hold-to-confirm/hold-to-confirm.js +165 -0
- package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
- package/components/hover-card/hover-card.d.ts +38 -0
- package/components/hover-card/hover-card.js +145 -0
- package/components/hover-card/hover-card.module.css +20 -0
- package/components/image-compare/image-compare.d.ts +27 -0
- package/components/image-compare/image-compare.js +267 -0
- package/components/image-compare/image-compare.module.css +25 -0
- package/components/in-view-title/in-view-title.d.ts +17 -0
- package/components/in-view-title/in-view-title.js +70 -0
- package/components/in-view-title/in-view-title.module.css +13 -0
- package/components/inline-edit/inline-edit.d.ts +27 -0
- package/components/inline-edit/inline-edit.js +315 -0
- package/components/inline-edit/inline-edit.module.css +73 -0
- package/components/input/input.d.ts +7 -0
- package/components/input/input.js +60 -0
- package/components/input/input.module.css +22 -0
- package/components/json-viewer/json-viewer.d.ts +48 -0
- package/components/json-viewer/json-viewer.js +463 -0
- package/components/json-viewer/json-viewer.module.css +92 -0
- package/components/line-chart/line-chart.d.ts +58 -0
- package/components/line-chart/line-chart.js +383 -0
- package/components/line-chart/line-chart.module.css +60 -0
- package/components/mention-input/mention-input.d.ts +72 -0
- package/components/mention-input/mention-input.js +417 -0
- package/components/mention-input/mention-input.module.css +134 -0
- package/components/metric-card/metric-card.d.ts +8 -0
- package/components/metric-card/metric-card.js +44 -0
- package/components/metric-card/metric-card.module.css +18 -0
- package/components/money-input/money-input.d.ts +48 -0
- package/components/money-input/money-input.js +503 -0
- package/components/money-input/money-input.module.css +77 -0
- package/components/morph-select/morph-select.d.ts +50 -0
- package/components/morph-select/morph-select.js +395 -0
- package/components/morph-select/morph-select.module.css +88 -0
- package/components/multi-select/multi-select.d.ts +18 -0
- package/components/multi-select/multi-select.js +105 -0
- package/components/multi-select/multi-select.module.css +37 -0
- package/components/notification-center/notification-center.d.ts +24 -0
- package/components/notification-center/notification-center.js +146 -0
- package/components/notification-center/notification-center.module.css +79 -0
- package/components/number-field/number-field.d.ts +41 -0
- package/components/number-field/number-field.js +455 -0
- package/components/number-field/number-field.module.css +71 -0
- package/components/otp-input/otp-input.d.ts +13 -0
- package/components/otp-input/otp-input.js +129 -0
- package/components/otp-input/otp-input.module.css +28 -0
- package/components/pagination/pagination.d.ts +7 -0
- package/components/pagination/pagination.js +60 -0
- package/components/pagination/pagination.module.css +16 -0
- package/components/password-field/password-field.d.ts +14 -0
- package/components/password-field/password-field.js +51 -0
- package/components/password-field/password-field.module.css +27 -0
- package/components/password-strength/password-strength.d.ts +107 -0
- package/components/password-strength/password-strength.js +161 -0
- package/components/password-strength/password-strength.module.css +57 -0
- package/components/phone-input/phone-input.d.ts +73 -0
- package/components/phone-input/phone-input.js +612 -0
- package/components/phone-input/phone-input.module.css +98 -0
- package/components/popover/popover.d.ts +6 -0
- package/components/popover/popover.js +19 -0
- package/components/popover/popover.module.css +32 -0
- package/components/progress/progress.d.ts +8 -0
- package/components/progress/progress.js +42 -0
- package/components/progress/progress.module.css +1 -0
- package/components/radio-cards/radio-cards.d.ts +35 -0
- package/components/radio-cards/radio-cards.js +110 -0
- package/components/radio-cards/radio-cards.module.css +84 -0
- package/components/radio-group/radio-group.d.ts +13 -0
- package/components/radio-group/radio-group.js +55 -0
- package/components/radio-group/radio-group.module.css +30 -0
- package/components/resizable-panels/resizable-panels.d.ts +37 -0
- package/components/resizable-panels/resizable-panels.js +338 -0
- package/components/resizable-panels/resizable-panels.module.css +40 -0
- package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
- package/components/rich-text-editor/rich-text-editor.js +1733 -0
- package/components/rich-text-editor/rich-text-editor.module.css +201 -0
- package/components/ridgeline/ridgeline.d.ts +37 -0
- package/components/ridgeline/ridgeline.js +346 -0
- package/components/ridgeline/ridgeline.module.css +58 -0
- package/components/scroll-area/scroll-area.d.ts +40 -0
- package/components/scroll-area/scroll-area.js +153 -0
- package/components/scroll-area/scroll-area.module.css +79 -0
- package/components/search-field/search-field.d.ts +7 -0
- package/components/search-field/search-field.js +20 -0
- package/components/search-field/search-field.module.css +17 -0
- package/components/segmented-control/segmented-control.d.ts +16 -0
- package/components/segmented-control/segmented-control.js +56 -0
- package/components/segmented-control/segmented-control.module.css +15 -0
- package/components/select/select.d.ts +15 -0
- package/components/select/select.js +36 -0
- package/components/select/select.module.css +35 -0
- package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
- package/components/shortcut-recorder/shortcut-recorder.js +380 -0
- package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
- package/components/signature-pad/signature-pad.d.ts +72 -0
- package/components/signature-pad/signature-pad.js +450 -0
- package/components/signature-pad/signature-pad.module.css +84 -0
- package/components/skeleton/skeleton.d.ts +13 -0
- package/components/skeleton/skeleton.js +53 -0
- package/components/skeleton/skeleton.module.css +11 -0
- package/components/slider/slider.d.ts +45 -0
- package/components/slider/slider.js +347 -0
- package/components/slider/slider.module.css +45 -0
- package/components/slope-chart/slope-chart.d.ts +38 -0
- package/components/slope-chart/slope-chart.js +183 -0
- package/components/slope-chart/slope-chart.module.css +51 -0
- package/components/slot-text/slot-text.d.ts +26 -0
- package/components/slot-text/slot-text.js +119 -0
- package/components/slot-text/slot-text.module.css +19 -0
- package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
- package/components/sortable-data-table/sortable-data-table.js +240 -0
- package/components/sortable-data-table/sortable-data-table.module.css +116 -0
- package/components/sparkline/sparkline.d.ts +19 -0
- package/components/sparkline/sparkline.js +239 -0
- package/components/sparkline/sparkline.module.css +26 -0
- package/components/split-button/split-button.d.ts +19 -0
- package/components/split-button/split-button.js +33 -0
- package/components/split-button/split-button.module.css +51 -0
- package/components/stepper/stepper.d.ts +36 -0
- package/components/stepper/stepper.js +108 -0
- package/components/stepper/stepper.module.css +74 -0
- package/components/streamgraph/streamgraph.d.ts +51 -0
- package/components/streamgraph/streamgraph.js +376 -0
- package/components/streamgraph/streamgraph.module.css +55 -0
- package/components/swipe-actions/swipe-actions.d.ts +35 -0
- package/components/swipe-actions/swipe-actions.js +330 -0
- package/components/swipe-actions/swipe-actions.module.css +64 -0
- package/components/switch/switch.d.ts +7 -0
- package/components/switch/switch.js +45 -0
- package/components/switch/switch.module.css +12 -0
- package/components/tabs/tabs.d.ts +8 -0
- package/components/tabs/tabs.js +138 -0
- package/components/tabs/tabs.module.css +27 -0
- package/components/tag-input/tag-input.d.ts +15 -0
- package/components/tag-input/tag-input.js +150 -0
- package/components/tag-input/tag-input.module.css +34 -0
- package/components/text-morph/text-morph.d.ts +13 -0
- package/components/text-morph/text-morph.js +69 -0
- package/components/text-morph/text-morph.module.css +8 -0
- package/components/text-reveal/text-reveal.d.ts +14 -0
- package/components/text-reveal/text-reveal.js +20 -0
- package/components/text-reveal/text-reveal.module.css +13 -0
- package/components/text-shimmer/text-shimmer.d.ts +18 -0
- package/components/text-shimmer/text-shimmer.js +90 -0
- package/components/text-shimmer/text-shimmer.module.css +9 -0
- package/components/textarea/textarea.d.ts +7 -0
- package/components/textarea/textarea.js +56 -0
- package/components/textarea/textarea.module.css +22 -0
- package/components/theme-switch/theme-switch.d.ts +11 -0
- package/components/theme-switch/theme-switch.js +37 -0
- package/components/theme-switch/theme-switch.module.css +31 -0
- package/components/time-picker/time-picker.d.ts +13 -0
- package/components/time-picker/time-picker.js +95 -0
- package/components/time-picker/time-picker.module.css +23 -0
- package/components/timeline/timeline.d.ts +47 -0
- package/components/timeline/timeline.js +171 -0
- package/components/timeline/timeline.module.css +51 -0
- package/components/toast/toast.d.ts +8 -0
- package/components/toast/toast.js +118 -0
- package/components/toast/toast.module.css +100 -0
- package/components/toast-stack/toast-stack.d.ts +57 -0
- package/components/toast-stack/toast-stack.js +399 -0
- package/components/toast-stack/toast-stack.module.css +61 -0
- package/components/tooltip/tooltip.d.ts +8 -0
- package/components/tooltip/tooltip.js +62 -0
- package/components/tooltip/tooltip.module.css +18 -0
- package/components/tree-view/tree-view.d.ts +17 -0
- package/components/tree-view/tree-view.js +147 -0
- package/components/tree-view/tree-view.module.css +22 -0
- package/components/treemap/treemap.d.ts +35 -0
- package/components/treemap/treemap.js +375 -0
- package/components/treemap/treemap.module.css +59 -0
- package/components/usage-meter/usage-meter.d.ts +24 -0
- package/components/usage-meter/usage-meter.js +296 -0
- package/components/usage-meter/usage-meter.module.css +61 -0
- package/components/user-menu/user-menu.d.ts +62 -0
- package/components/user-menu/user-menu.js +316 -0
- package/components/user-menu/user-menu.module.css +97 -0
- package/components/waffle-chart/waffle-chart.d.ts +39 -0
- package/components/waffle-chart/waffle-chart.js +244 -0
- package/components/waffle-chart/waffle-chart.module.css +52 -0
- package/components.json +1330 -0
- package/eslint.js +53 -0
- package/lib/escape.d.ts +19 -0
- package/lib/escape.js +50 -0
- package/lib/media.d.ts +353 -0
- package/lib/media.js +65 -0
- package/lib/menu-highlight.d.ts +28 -0
- package/lib/menu-highlight.js +43 -0
- package/lib/motion-tokens.d.ts +60 -0
- package/lib/motion-tokens.js +25 -0
- package/lib/return-focus.d.ts +9 -0
- package/lib/return-focus.js +53 -0
- package/lib/silicon-provider.d.ts +60 -0
- package/lib/silicon-provider.js +47 -0
- package/lib/use-copy-feedback.d.ts +8 -0
- package/lib/use-copy-feedback.js +36 -0
- package/lib/use-today.d.ts +2 -0
- package/lib/use-today.js +29 -0
- package/package.json +76 -4
- package/styles.css +218 -0
- package/styles.css.d.ts +1 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, isValidElement, useEffect, useEffectEvent, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, motionValue, useMotionValue, useReducedMotion, useTransform } from "motion/react";
|
|
5
|
+
import { ChevronLeft, ChevronRight, Pause, Play } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./carousel.module.css";
|
|
8
|
+
/** Movement before a press is read as a drag or handed to the page as a vertical scroll. */
|
|
9
|
+
const SLOP = 6;
|
|
10
|
+
/** Release speed in px/s that always moves at least one slide. */
|
|
11
|
+
const FLICK = 360;
|
|
12
|
+
/** Width of a resting dot and of the active pill, in px. */
|
|
13
|
+
const DOT = 6, PILL = 22;
|
|
14
|
+
/** Scale and opacity of a slide one full step from the center. */
|
|
15
|
+
const SCALE = .9, DIM = .5;
|
|
16
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
17
|
+
/** Momentum projection with a paging deceleration rate: where a flick would come to rest. */
|
|
18
|
+
const project = (velocity, rate = .995) => velocity / 1000 * rate / (1 - rate);
|
|
19
|
+
/** Past an edge every pixel costs more, like pulling against elastic. */
|
|
20
|
+
const rubber = (overshoot, dimension) => overshoot * dimension * .55 / (dimension + .55 * overshoot);
|
|
21
|
+
const unrubber = (distance, dimension) => distance * dimension / (.55 * Math.max(1, dimension - distance));
|
|
22
|
+
const defaultSlideLabel = (index, count) => `${index + 1} of ${count}`;
|
|
23
|
+
const subscribeNothing = () => () => { };
|
|
24
|
+
const iconIn = { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` };
|
|
25
|
+
const iconRest = { opacity: 1, scale: 1, filter: "blur(0px)" };
|
|
26
|
+
const iconOut = { ...iconIn, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } };
|
|
27
|
+
const iconEnter = { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] }, filter: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } };
|
|
28
|
+
function velocityOf(samples, now) {
|
|
29
|
+
const recent = samples.filter(([time]) => now - time <= 90);
|
|
30
|
+
if (recent.length < 2 || now - recent[recent.length - 1][0] > 50)
|
|
31
|
+
return 0;
|
|
32
|
+
const [firstTime, firstX] = recent[0], [lastTime, lastX] = recent[recent.length - 1];
|
|
33
|
+
return lastTime > firstTime ? (lastX - firstX) / ((lastTime - firstTime) / 1000) : 0;
|
|
34
|
+
}
|
|
35
|
+
export function Carousel({ label, children, index: controlledIndex, defaultIndex = 0, onIndexChange, slideSize = "min(80cqw, 340px)", slideLabel = defaultSlideLabel, interval, autoplay = false, className }) {
|
|
36
|
+
const slides = Children.toArray(children);
|
|
37
|
+
const count = slides.length;
|
|
38
|
+
const last = Math.max(0, count - 1);
|
|
39
|
+
const id = useId();
|
|
40
|
+
const reduced = useReducedMotion() ?? false;
|
|
41
|
+
const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);
|
|
42
|
+
const [ownIndex, setOwnIndex] = useState(() => clamp(defaultIndex, 0, last));
|
|
43
|
+
const index = clamp(controlledIndex ?? ownIndex, 0, last);
|
|
44
|
+
const [startIndex] = useState(index);
|
|
45
|
+
const [playing, setPlaying] = useState(autoplay);
|
|
46
|
+
const [hovered, setHovered] = useState(false);
|
|
47
|
+
const [focusPaused, setFocusPaused] = useState(false);
|
|
48
|
+
const [dragging, setDragging] = useState(false);
|
|
49
|
+
const [inView, setInView] = useState(true);
|
|
50
|
+
const [pageVisible, setPageVisible] = useState(true);
|
|
51
|
+
const [announcement, setAnnouncement] = useState("");
|
|
52
|
+
const x = useMotionValue(0);
|
|
53
|
+
const progress = useMotionValue(index);
|
|
54
|
+
// One drain per indicator, so a pill that just ran out keeps its state while it shrinks away.
|
|
55
|
+
const drains = useMemo(() => Array.from({ length: count }, () => motionValue(0)), [count]);
|
|
56
|
+
const rootRef = useRef(null);
|
|
57
|
+
const viewportRef = useRef(null);
|
|
58
|
+
const trackRef = useRef(null);
|
|
59
|
+
const playRef = useRef(null);
|
|
60
|
+
const tabRefs = useRef([]);
|
|
61
|
+
const metrics = useRef({ step: 0, width: 0 });
|
|
62
|
+
const target = useRef(index);
|
|
63
|
+
const travel = useRef(null);
|
|
64
|
+
const drag = useRef(null);
|
|
65
|
+
const wheel = useRef({ active: false, raw: 0, timer: 0 });
|
|
66
|
+
const swallowClick = useRef(false);
|
|
67
|
+
const canRotate = Boolean(interval) && count > 1 && !(hydrated && reduced);
|
|
68
|
+
const running = canRotate && playing && !hovered && !focusPaused && !dragging && inView && pageVisible;
|
|
69
|
+
const bounds = () => ({ min: -last * metrics.current.step, max: 0 });
|
|
70
|
+
const band = (raw) => {
|
|
71
|
+
const { min, max } = bounds(), size = metrics.current.width || 1;
|
|
72
|
+
return raw > max ? max + rubber(raw - max, size) : raw < min ? min - rubber(min - raw, size) : raw;
|
|
73
|
+
};
|
|
74
|
+
const unband = (shown) => {
|
|
75
|
+
const { min, max } = bounds(), size = metrics.current.width || 1;
|
|
76
|
+
return shown > max ? max + unrubber(shown - max, size) : shown < min ? min - unrubber(min - shown, size) : shown;
|
|
77
|
+
};
|
|
78
|
+
/** Springs the track to a slide from wherever it is, carrying any velocity it already has. */
|
|
79
|
+
function glide(to, velocity = 0) {
|
|
80
|
+
const { step } = metrics.current;
|
|
81
|
+
travel.current?.stop();
|
|
82
|
+
travel.current = null;
|
|
83
|
+
if (!step)
|
|
84
|
+
return;
|
|
85
|
+
if (reduced) {
|
|
86
|
+
x.jump(-to * step);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
travel.current = animate(x, -to * step, { ...motionTokens.spring.smooth, velocity });
|
|
90
|
+
}
|
|
91
|
+
function select(to, { velocity = 0, announce = true } = {}) {
|
|
92
|
+
const next = clamp(to, 0, last);
|
|
93
|
+
target.current = next;
|
|
94
|
+
glide(next, velocity);
|
|
95
|
+
if (next === index)
|
|
96
|
+
return;
|
|
97
|
+
if (controlledIndex === undefined)
|
|
98
|
+
setOwnIndex(next);
|
|
99
|
+
onIndexChange?.(next);
|
|
100
|
+
if (announce)
|
|
101
|
+
setAnnouncement(slideLabel(next, count));
|
|
102
|
+
}
|
|
103
|
+
const measure = useEffectEvent(() => {
|
|
104
|
+
const track = trackRef.current, viewport = viewportRef.current;
|
|
105
|
+
const [first, second] = Array.from(track?.children ?? []);
|
|
106
|
+
if (!track || !viewport || !first)
|
|
107
|
+
return;
|
|
108
|
+
// Centers are unaffected by each slide's scale, and rects keep the sub-pixel step that offsetLeft would round away.
|
|
109
|
+
const center = (node) => { const rect = node.getBoundingClientRect(); return rect.left + rect.width / 2; };
|
|
110
|
+
const step = second ? center(second) - center(first) : first.offsetWidth;
|
|
111
|
+
const changed = step !== metrics.current.step;
|
|
112
|
+
metrics.current = { step, width: viewport.clientWidth };
|
|
113
|
+
// The server positions the start slide with CSS; from here on the motion value owns the track.
|
|
114
|
+
track.dataset.measured = "";
|
|
115
|
+
if (changed && !drag.current) {
|
|
116
|
+
travel.current?.stop();
|
|
117
|
+
travel.current = null;
|
|
118
|
+
x.jump(-target.current * step);
|
|
119
|
+
progress.set(target.current);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
measure();
|
|
124
|
+
const viewport = viewportRef.current;
|
|
125
|
+
if (!viewport || typeof ResizeObserver === "undefined")
|
|
126
|
+
return;
|
|
127
|
+
const observer = new ResizeObserver(() => measure());
|
|
128
|
+
observer.observe(viewport);
|
|
129
|
+
return () => observer.disconnect();
|
|
130
|
+
}, []);
|
|
131
|
+
// Progress is the continuous slide position that the scale, dim, and indicator read every frame.
|
|
132
|
+
useEffect(() => x.on("change", value => {
|
|
133
|
+
const { step } = metrics.current;
|
|
134
|
+
if (!step)
|
|
135
|
+
return;
|
|
136
|
+
const position = -value / step;
|
|
137
|
+
progress.set(position);
|
|
138
|
+
drains.forEach((drain, i) => { if (i !== target.current && Math.abs(position - i) >= 1 && drain.get() !== 0)
|
|
139
|
+
drain.jump(0); });
|
|
140
|
+
}), [x, progress, drains]);
|
|
141
|
+
const followIndexProp = useEffectEvent((next) => { if (target.current !== next) {
|
|
142
|
+
target.current = next;
|
|
143
|
+
glide(next);
|
|
144
|
+
} });
|
|
145
|
+
useEffect(() => { followIndexProp(index); }, [index]);
|
|
146
|
+
const advance = useEffectEvent(() => select(index >= last ? 0 : index + 1, { announce: false }));
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!running || !interval)
|
|
149
|
+
return;
|
|
150
|
+
const drain = drains[index];
|
|
151
|
+
if (!drain)
|
|
152
|
+
return;
|
|
153
|
+
const controls = animate(drain, 1, { duration: (1 - drain.get()) * interval / 1000, ease: "linear", onComplete: () => advance() });
|
|
154
|
+
return () => controls.stop();
|
|
155
|
+
}, [running, index, interval, drains]);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const root = rootRef.current;
|
|
158
|
+
if (!interval || !root)
|
|
159
|
+
return;
|
|
160
|
+
const observer = typeof IntersectionObserver === "undefined" ? null : new IntersectionObserver(([entry]) => { if (entry)
|
|
161
|
+
setInView(entry.isIntersecting); });
|
|
162
|
+
observer?.observe(root);
|
|
163
|
+
const onVisibility = () => setPageVisible(document.visibilityState !== "hidden");
|
|
164
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
165
|
+
return () => { observer?.disconnect(); document.removeEventListener("visibilitychange", onVisibility); };
|
|
166
|
+
}, [interval]);
|
|
167
|
+
// A horizontal trackpad swipe moves the track 1:1 and settles on the nearest slide once the swipe, including its momentum, ends.
|
|
168
|
+
const onWheel = useEffectEvent((event) => {
|
|
169
|
+
const state = wheel.current;
|
|
170
|
+
if (count < 2 || drag.current || !metrics.current.step)
|
|
171
|
+
return;
|
|
172
|
+
if (!state.active && Math.abs(event.deltaX) <= Math.abs(event.deltaY))
|
|
173
|
+
return;
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
if (!state.active) {
|
|
176
|
+
state.active = true;
|
|
177
|
+
travel.current?.stop();
|
|
178
|
+
travel.current = null;
|
|
179
|
+
state.raw = unband(x.get());
|
|
180
|
+
}
|
|
181
|
+
state.raw -= event.deltaX * (event.deltaMode === 1 ? 16 : 1);
|
|
182
|
+
x.set(band(state.raw));
|
|
183
|
+
window.clearTimeout(state.timer);
|
|
184
|
+
state.timer = window.setTimeout(() => { state.active = false; select(Math.round(-x.get() / metrics.current.step), { velocity: x.getVelocity() }); }, 120);
|
|
185
|
+
});
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const viewport = viewportRef.current, state = wheel.current;
|
|
188
|
+
if (!viewport)
|
|
189
|
+
return;
|
|
190
|
+
const listener = (event) => onWheel(event);
|
|
191
|
+
viewport.addEventListener("wheel", listener, { passive: false });
|
|
192
|
+
return () => { viewport.removeEventListener("wheel", listener); window.clearTimeout(state.timer); };
|
|
193
|
+
}, []);
|
|
194
|
+
function slideAt(clientX) {
|
|
195
|
+
const nodes = Array.from(trackRef.current?.children ?? []);
|
|
196
|
+
const hit = nodes.findIndex(node => { const rect = node.getBoundingClientRect(); return clientX >= rect.left && clientX <= rect.right; });
|
|
197
|
+
return hit < 0 ? null : hit;
|
|
198
|
+
}
|
|
199
|
+
function onPointerDown(event) {
|
|
200
|
+
if (count < 2 || drag.current || !metrics.current.step || (event.pointerType === "mouse" && event.button !== 0))
|
|
201
|
+
return;
|
|
202
|
+
swallowClick.current = false;
|
|
203
|
+
// Touching a visibly moving track catches it where it is, the way a hand stops a spinning wheel. A settling tail is left alone so a tap still selects.
|
|
204
|
+
const caught = travel.current !== null && x.isAnimating() && Math.abs(x.get() + target.current * metrics.current.step) > 12;
|
|
205
|
+
if (caught) {
|
|
206
|
+
travel.current?.stop();
|
|
207
|
+
travel.current = null;
|
|
208
|
+
}
|
|
209
|
+
drag.current = { pointer: event.pointerId, startX: event.clientX, startY: event.clientY, origin: 0, from: target.current, locked: null, caught, samples: [] };
|
|
210
|
+
}
|
|
211
|
+
function onPointerMove(event) {
|
|
212
|
+
const current = drag.current;
|
|
213
|
+
if (!current || current.pointer !== event.pointerId)
|
|
214
|
+
return;
|
|
215
|
+
if (!current.locked) {
|
|
216
|
+
const dx = event.clientX - current.startX, dy = event.clientY - current.startY;
|
|
217
|
+
if (Math.abs(dx) < SLOP && Math.abs(dy) < SLOP)
|
|
218
|
+
return;
|
|
219
|
+
if (Math.abs(dy) > Math.abs(dx)) {
|
|
220
|
+
current.locked = "y";
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// The slop is consumed rather than applied, so the track starts moving from the finger without a jump.
|
|
224
|
+
current.locked = "x";
|
|
225
|
+
travel.current?.stop();
|
|
226
|
+
travel.current = null;
|
|
227
|
+
current.startX = event.clientX;
|
|
228
|
+
current.origin = unband(x.get());
|
|
229
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
230
|
+
setDragging(true);
|
|
231
|
+
}
|
|
232
|
+
if (current.locked !== "x")
|
|
233
|
+
return;
|
|
234
|
+
x.set(band(current.origin + event.clientX - current.startX));
|
|
235
|
+
current.samples.push([event.timeStamp, event.clientX]);
|
|
236
|
+
if (current.samples.length > 16)
|
|
237
|
+
current.samples.shift();
|
|
238
|
+
}
|
|
239
|
+
function finish(event, cancelled) {
|
|
240
|
+
const current = drag.current;
|
|
241
|
+
if (!current || current.pointer !== event.pointerId)
|
|
242
|
+
return;
|
|
243
|
+
drag.current = null;
|
|
244
|
+
const { step } = metrics.current;
|
|
245
|
+
if (current.locked === "x") {
|
|
246
|
+
swallowClick.current = true;
|
|
247
|
+
setDragging(false);
|
|
248
|
+
const velocity = cancelled ? 0 : velocityOf(current.samples, event.timeStamp);
|
|
249
|
+
// The landing slide comes from where the flick would carry the track, not from where the finger let go.
|
|
250
|
+
let to = Math.round(-(x.get() + project(velocity)) / step);
|
|
251
|
+
if (Math.abs(velocity) > FLICK) {
|
|
252
|
+
const direction = velocity < 0 ? 1 : -1;
|
|
253
|
+
if ((to - current.from) * direction < 1)
|
|
254
|
+
to = current.from + direction;
|
|
255
|
+
}
|
|
256
|
+
// Inside the bounds the track moves 1:1 with the pointer, so the sampled release speed carries into the spring even when the
|
|
257
|
+
// last move landed a frame early (the value's own velocity reads zero after 30ms). Past an edge, the banded value speed is the true one.
|
|
258
|
+
const { min, max } = bounds(), shown = x.get();
|
|
259
|
+
select(to, { velocity: shown >= min && shown <= max ? velocity : x.getVelocity() });
|
|
260
|
+
}
|
|
261
|
+
else if (current.caught) {
|
|
262
|
+
swallowClick.current = true;
|
|
263
|
+
select(Math.round(-x.get() / step));
|
|
264
|
+
}
|
|
265
|
+
else if (!cancelled && current.locked === null) {
|
|
266
|
+
const hit = slideAt(event.clientX);
|
|
267
|
+
if (hit !== null && hit !== target.current) {
|
|
268
|
+
swallowClick.current = true;
|
|
269
|
+
select(hit);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (cancelled) {
|
|
273
|
+
select(target.current);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function onKeyDown(event) {
|
|
277
|
+
if (event.altKey || event.ctrlKey || event.metaKey || count < 2)
|
|
278
|
+
return;
|
|
279
|
+
const node = event.target;
|
|
280
|
+
if (node.closest("input, textarea, select, [contenteditable]"))
|
|
281
|
+
return;
|
|
282
|
+
const from = target.current;
|
|
283
|
+
const to = event.key === "ArrowLeft" ? from - 1 : event.key === "ArrowRight" ? from + 1 : event.key === "Home" ? 0 : event.key === "End" ? last : null;
|
|
284
|
+
if (to === null)
|
|
285
|
+
return;
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
const next = clamp(to, 0, last);
|
|
288
|
+
select(next);
|
|
289
|
+
if (node.getAttribute("role") === "tab")
|
|
290
|
+
tabRefs.current[next]?.focus({ preventScroll: true });
|
|
291
|
+
else if (trackRef.current?.contains(node))
|
|
292
|
+
viewportRef.current?.focus({ preventScroll: true });
|
|
293
|
+
}
|
|
294
|
+
function toggleRotation() {
|
|
295
|
+
const drain = drains[index];
|
|
296
|
+
if (!playing) {
|
|
297
|
+
setPlaying(true);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
setPlaying(false);
|
|
301
|
+
// Pausing refills the pill: this slide stays until the viewer moves on.
|
|
302
|
+
if (drain) {
|
|
303
|
+
if (reduced)
|
|
304
|
+
drain.jump(0);
|
|
305
|
+
else
|
|
306
|
+
animate(drain, 0, { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.standard] });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const rootStyle = { "--carousel-slide": slideSize, "--carousel-start": startIndex };
|
|
310
|
+
return _jsxs("section", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), style: rootStyle, "aria-roledescription": "carousel", "aria-label": label, onKeyDown: onKeyDown, onFocus: event => setFocusPaused(event.target !== playRef.current && event.target.matches(":focus-visible")), onBlur: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
311
|
+
setFocusPaused(false); }, children: [_jsx("div", { ref: viewportRef, className: styles.viewport, role: "group", "aria-label": "Slides", tabIndex: 0, "data-dragging": dragging || undefined, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: event => finish(event, false), onPointerCancel: event => finish(event, true), onPointerEnter: event => { if (event.pointerType === "mouse")
|
|
312
|
+
setHovered(true); }, onPointerLeave: () => setHovered(false), onClickCapture: event => { if (swallowClick.current) {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
event.stopPropagation();
|
|
315
|
+
swallowClick.current = false;
|
|
316
|
+
} }, onDragStart: event => event.preventDefault(), children: _jsx(motion.div, { ref: trackRef, className: styles.track, style: { x }, children: slides.map((slide, i) => _jsx(Slide, { id: `${id}-slide-${i}`, index: i, progress: progress, active: i === index, label: slideLabel(i, count), children: slide }, isValidElement(slide) && slide.key != null ? slide.key : i)) }) }), count > 1 && _jsxs("div", { className: styles.controls, children: [canRotate && _jsx("button", { ref: playRef, type: "button", className: styles.control, "aria-label": playing ? "Pause slide rotation" : "Start slide rotation", onClick: toggleRotation, children: _jsx("span", { className: styles.iconSlot, children: _jsx(AnimatePresence, { initial: false, children: _jsx(motion.span, { className: styles.icon, initial: reduced ? { opacity: 0 } : iconIn, animate: iconRest, exit: reduced ? { opacity: 0, transition: { duration: motionTokens.duration.instant } } : iconOut, transition: reduced ? { duration: motionTokens.duration.instant } : iconEnter, children: playing ? _jsx(Pause, { size: 15, strokeWidth: 1.75, fill: "currentColor", "aria-hidden": "true" }) : _jsx(Play, { size: 15, strokeWidth: 1.75, fill: "currentColor", "aria-hidden": "true" }) }, playing ? "pause" : "play") }) }) }), _jsx("div", { className: styles.tabs, role: "tablist", "aria-label": "Choose a slide", children: slides.map((_, i) => _jsx(Dot, { index: i, last: last, progress: progress, drain: drains[i], selected: i === index, label: slideLabel(i, count), controls: `${id}-slide-${i}`, tabRef: node => { tabRefs.current[i] = node; }, onSelect: () => select(i) }, i)) }), _jsxs("div", { className: styles.arrows, children: [_jsx("button", { type: "button", className: styles.control, "aria-label": "Previous slide", "aria-disabled": index === 0, onClick: () => { if (target.current > 0)
|
|
317
|
+
select(target.current - 1); }, children: _jsx(ChevronLeft, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: styles.control, "aria-label": "Next slide", "aria-disabled": index === last, onClick: () => { if (target.current < last)
|
|
318
|
+
select(target.current + 1); }, children: _jsx(ChevronRight, { size: 18, strokeWidth: 1.75, "aria-hidden": "true" }) })] })] }), _jsx("span", { className: styles.visuallyHidden, "aria-live": "polite", "aria-atomic": "true", children: announcement })] });
|
|
319
|
+
}
|
|
320
|
+
/** A slide shrinks and dims with its distance from the center, read straight from the track position so it never lags the finger. */
|
|
321
|
+
function Slide({ id, index, progress, active, label, children }) {
|
|
322
|
+
const scale = useTransform(progress, [index - 1, index, index + 1], [SCALE, 1, SCALE]);
|
|
323
|
+
const opacity = useTransform(progress, [index - 1, index, index + 1], [DIM, 1, DIM]);
|
|
324
|
+
return _jsx(motion.div, { id: id, className: styles.slide, role: "tabpanel", "aria-roledescription": "slide", "aria-label": label, inert: !active, style: { scale, opacity }, children: children });
|
|
325
|
+
}
|
|
326
|
+
/** Each indicator grows toward a pill as the track nears its slide, so the pill hands over continuously instead of hopping. */
|
|
327
|
+
function Dot({ index, last, progress, drain, selected, label, controls, tabRef, onSelect }) {
|
|
328
|
+
const emphasis = useTransform(progress, value => Math.max(0, 1 - Math.abs(clamp(value, 0, last) - index)));
|
|
329
|
+
const width = useTransform(emphasis, value => DOT + (PILL - DOT) * value);
|
|
330
|
+
return _jsx("button", { ref: tabRef, type: "button", role: "tab", className: styles.tab, "aria-selected": selected, "aria-controls": controls, "aria-label": label, tabIndex: selected ? 0 : -1, onClick: onSelect, children: _jsxs(motion.span, { className: styles.dot, style: { width }, "aria-hidden": "true", children: [_jsx(motion.span, { className: styles.dotFill, style: { opacity: emphasis } }), _jsx(motion.span, { className: styles.dotDrain, style: { scaleX: drain } })] }) });
|
|
331
|
+
}
|
|
332
|
+
export default Carousel;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* The root is the size container, so slide widths, centering padding, and the control row all share one measure. It needs a definite width. */
|
|
2
|
+
.root { --carousel-gap: var(--space-inset); display: grid; width: 100%; grid-template-columns: minmax(0, 1fr); min-width: 0; gap: var(--space-inset); container-type: inline-size; }
|
|
3
|
+
/* Only the horizontal axis clips, so focus rings inside the active slide are never cut off at the top or bottom. */
|
|
4
|
+
.viewport { position: relative; min-width: 0; overflow-x: clip; overflow-y: visible; border-radius: var(--radius-container); corner-shape: var(--corner-shape); cursor: grab; touch-action: pan-y; -webkit-tap-highlight-color: transparent; }
|
|
5
|
+
.viewport[data-dragging] { cursor: grabbing; }
|
|
6
|
+
.viewport:focus-visible { --focus-outline-offset: 4px; }
|
|
7
|
+
/* Side padding centers the first and last slides; before hydration the start slide is placed with CSS so nothing moves on first paint. */
|
|
8
|
+
.track { display: flex; gap: var(--carousel-gap); padding-inline: calc((100cqw - var(--carousel-slide)) / 2); user-select: none; -webkit-user-select: none; }
|
|
9
|
+
.track:not([data-measured]) { translate: calc(var(--carousel-start) * -1 * (var(--carousel-slide) + var(--carousel-gap))) 0; }
|
|
10
|
+
.slide { flex: 0 0 var(--carousel-slide); min-width: 0; transform-origin: 50% 50%; will-change: transform, opacity; }
|
|
11
|
+
.slide img { -webkit-user-drag: none; }
|
|
12
|
+
|
|
13
|
+
/* Controls line up with the edges of the centered slide. */
|
|
14
|
+
.controls { display: flex; min-width: 0; align-items: center; gap: var(--space-related); padding-inline: calc((100cqw - var(--carousel-slide)) / 2); }
|
|
15
|
+
.tabs { display: flex; min-width: 0; align-items: center; }
|
|
16
|
+
.tab { display: grid; height: var(--control-height-sm); flex: none; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; padding: 0 5px; background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
17
|
+
/* Resting dots use the strong border color; the fill fades in as the pill forms, and the drain covers it from the right while rotation runs. */
|
|
18
|
+
.dot { position: relative; display: block; width: 6px; height: 6px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
19
|
+
.dotFill, .dotDrain { position: absolute; inset: 0; }
|
|
20
|
+
.dotFill { background: var(--foreground); }
|
|
21
|
+
.dotDrain { background: var(--border-strong); transform-origin: 100% 50%; }
|
|
22
|
+
@media (hover: hover) and (pointer: fine) { .tab:hover .dot { background: var(--text-muted); } }
|
|
23
|
+
.tab:focus-visible { --focus-outline-offset: -4px; }
|
|
24
|
+
|
|
25
|
+
.arrows { display: flex; flex: none; gap: var(--space-related); margin-left: auto; }
|
|
26
|
+
.control { display: grid; width: var(--control-height-sm); height: var(--control-height-sm); flex: none; place-items: center; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; padding: 0; background: var(--surface); color: var(--foreground); cursor: pointer; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
|
|
27
|
+
@media (hover: hover) and (pointer: fine) { .control:hover:not([aria-disabled="true"]) { background: var(--surface-muted); } }
|
|
28
|
+
.control:active:not([aria-disabled="true"]) { transform: scale(.97); transition: transform 100ms var(--ease-standard); }
|
|
29
|
+
.control[aria-disabled="true"] { color: var(--text-muted); cursor: default; opacity: .5; }
|
|
30
|
+
.iconSlot { display: grid; width: 16px; height: 16px; place-items: center; }
|
|
31
|
+
.icon { grid-area: 1 / 1; display: grid; place-items: center; }
|
|
32
|
+
/* On phone widths the row tightens so play, indicators, and arrows still fit beside a narrow slide. */
|
|
33
|
+
@container (max-width: 340px) {
|
|
34
|
+
.controls { gap: var(--space-tight); padding-inline: max(0px, calc((100cqw - var(--carousel-slide)) / 2 - var(--space-related))); }
|
|
35
|
+
.arrows { gap: var(--space-tight); }
|
|
36
|
+
.tab { padding: 0 4px; }
|
|
37
|
+
}
|
|
38
|
+
.visuallyHidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
39
|
+
|
|
40
|
+
@media (prefers-reduced-motion: reduce) {
|
|
41
|
+
.dot, .control, .control:active:not([aria-disabled="true"]) { transition: none; transform: none; }
|
|
42
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export interface ChatParticipant {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
/** Portrait URL. Initials show when it is missing. */
|
|
5
|
+
avatar?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ChatAttachment {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
kind: "image" | "file";
|
|
11
|
+
url?: string;
|
|
12
|
+
/** Bytes. */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** Intrinsic image size, used to reserve space before the image loads. */
|
|
15
|
+
width?: number;
|
|
16
|
+
height?: number;
|
|
17
|
+
alt?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ChatReaction {
|
|
20
|
+
emoji: string;
|
|
21
|
+
count: number;
|
|
22
|
+
/** The current user reacted with this emoji. */
|
|
23
|
+
mine?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export type ChatMessageStatus = "sending" | "sent" | "delivered" | "read" | "failed";
|
|
26
|
+
export interface ChatMessage {
|
|
27
|
+
id: string;
|
|
28
|
+
authorId: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
createdAt: Date | string | number;
|
|
31
|
+
attachments?: ChatAttachment[];
|
|
32
|
+
reactions?: ChatReaction[];
|
|
33
|
+
/** Delivery state of the current user's own messages. */
|
|
34
|
+
status?: ChatMessageStatus;
|
|
35
|
+
}
|
|
36
|
+
export interface ChatDraft {
|
|
37
|
+
text: string;
|
|
38
|
+
files: File[];
|
|
39
|
+
}
|
|
40
|
+
/** Words the composer shows or announces. Each has an English default. */
|
|
41
|
+
export interface ChatComposerLabels {
|
|
42
|
+
/** Accessible name of the send button. */
|
|
43
|
+
send?: string;
|
|
44
|
+
/** Accessible name of the attach button. */
|
|
45
|
+
attach?: string;
|
|
46
|
+
/** Accessible name of the list of files waiting to send. */
|
|
47
|
+
attachments?: string;
|
|
48
|
+
/** Accessible name of the button that takes a file back out. */
|
|
49
|
+
removeFile?: (name: string) => string;
|
|
50
|
+
}
|
|
51
|
+
/** Every word the thread shows or announces. Each has an English default; words with a count or names are functions, so an app can apply its own plural rules. */
|
|
52
|
+
export interface ChatThreadLabels extends ChatComposerLabels {
|
|
53
|
+
sending?: string;
|
|
54
|
+
sent?: string;
|
|
55
|
+
delivered?: string;
|
|
56
|
+
read?: string;
|
|
57
|
+
failed?: string;
|
|
58
|
+
retry?: string;
|
|
59
|
+
/** Accessible name of the read receipts under a message. */
|
|
60
|
+
readBy?: (names: string[]) => string;
|
|
61
|
+
/** Day separators for the last two days. Older days are formatted by `locale`. Defaults come from Intl for `locale`. */
|
|
62
|
+
today?: string;
|
|
63
|
+
yesterday?: string;
|
|
64
|
+
/** Screen reader announcement of who is typing. */
|
|
65
|
+
typing?: (names: string[]) => string;
|
|
66
|
+
/** The pill when new messages land out of view. The count inside it rolls. */
|
|
67
|
+
newMessages?: (count: number) => string;
|
|
68
|
+
jumpToLatest?: string;
|
|
69
|
+
/** Name shown for a message whose author is not in `participants`. */
|
|
70
|
+
unknownAuthor?: string;
|
|
71
|
+
addReaction?: string;
|
|
72
|
+
/** Accessible name of the reaction picker and of the reactions under a message. */
|
|
73
|
+
reactions?: string;
|
|
74
|
+
/** Accessible name of an emoji in the picker. */
|
|
75
|
+
reactWith?: (emoji: string) => string;
|
|
76
|
+
/** Accessible name of a reaction under a message. */
|
|
77
|
+
reactionCount?: (emoji: string, count: number, includesYou: boolean) => string;
|
|
78
|
+
}
|
|
79
|
+
export interface ChatThreadHandle {
|
|
80
|
+
scrollToBottom: (smooth?: boolean) => void;
|
|
81
|
+
focusComposer: () => void;
|
|
82
|
+
}
|
|
83
|
+
export interface ChatThreadProps {
|
|
84
|
+
participants: ChatParticipant[];
|
|
85
|
+
currentUserId: string;
|
|
86
|
+
/** Controlled messages, oldest first. Leave it out to let the thread keep its own list. */
|
|
87
|
+
messages?: ChatMessage[];
|
|
88
|
+
defaultMessages?: ChatMessage[];
|
|
89
|
+
/** Participant ids currently typing. */
|
|
90
|
+
typing?: string[];
|
|
91
|
+
/** The last message each participant has read, by participant id. Their avatar sits under that message and glides as it moves. */
|
|
92
|
+
readBy?: Record<string, string>;
|
|
93
|
+
/** Called with the composer's text and files. Uncontrolled threads also append the message themselves. */
|
|
94
|
+
onSend?: (draft: ChatDraft) => void | Promise<void>;
|
|
95
|
+
onReact?: (messageId: string, emoji: string) => void;
|
|
96
|
+
onRetry?: (messageId: string) => void;
|
|
97
|
+
/** Emoji offered by the reaction picker. */
|
|
98
|
+
reactions?: string[];
|
|
99
|
+
placeholder?: string;
|
|
100
|
+
/** Show the composer. Defaults to true. */
|
|
101
|
+
composer?: boolean;
|
|
102
|
+
allowAttachments?: boolean;
|
|
103
|
+
/** File types the attach button offers, as for `<input accept>`. */
|
|
104
|
+
accept?: string;
|
|
105
|
+
/** Messages from one person closer together than this, in ms, share a group. Defaults to five minutes. */
|
|
106
|
+
groupWindow?: number;
|
|
107
|
+
locale?: string;
|
|
108
|
+
/** Accessible name of the message log. */
|
|
109
|
+
label?: string;
|
|
110
|
+
/** Words for localization. Leave out any key to keep its English default. */
|
|
111
|
+
labels?: ChatThreadLabels;
|
|
112
|
+
className?: string;
|
|
113
|
+
}
|
|
114
|
+
export declare function formatBytes(bytes?: number): string;
|
|
115
|
+
export interface ChatComposerProps {
|
|
116
|
+
onSend: (draft: ChatDraft) => void;
|
|
117
|
+
placeholder?: string;
|
|
118
|
+
allowAttachments?: boolean;
|
|
119
|
+
accept?: string;
|
|
120
|
+
disabled?: boolean;
|
|
121
|
+
/** Words for localization. Leave out any key to keep its English default. */
|
|
122
|
+
labels?: ChatComposerLabels;
|
|
123
|
+
className?: string;
|
|
124
|
+
}
|
|
125
|
+
export interface ChatComposerHandle {
|
|
126
|
+
focus: () => void;
|
|
127
|
+
}
|
|
128
|
+
/** The message box on its own: autosizing text, attachments by button, paste, or drop, and a send button that morphs when there is something to send. */
|
|
129
|
+
export declare const ChatComposer: import("react").ForwardRefExoticComponent<ChatComposerProps & import("react").RefAttributes<ChatComposerHandle>>;
|
|
130
|
+
/**
|
|
131
|
+
* A chat or support thread: grouped messages with day separators, reactions, read receipts that glide to the latest read message,
|
|
132
|
+
* a typing indicator, and a composer. The list stays pinned to the newest message and offers a pill when new messages land out of view.
|
|
133
|
+
*/
|
|
134
|
+
export declare const ChatThread: import("react").ForwardRefExoticComponent<ChatThreadProps & import("react").RefAttributes<ChatThreadHandle>>;
|
|
135
|
+
export default ChatThread;
|