@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,343 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
|
+
import { Check, ChevronRight, CornerDownRight, RotateCcw, SmilePlus, X } from "lucide-react";
|
|
6
|
+
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import styles from "./comment-thread.module.css";
|
|
8
|
+
const DEFAULT_LABELS = {
|
|
9
|
+
thread: "Comment thread",
|
|
10
|
+
commentCount: count => `${count} ${count === 1 ? "comment" : "comments"}`,
|
|
11
|
+
peopleCount: count => `${count} ${count === 1 ? "person" : "people"}`,
|
|
12
|
+
resolve: "Resolve",
|
|
13
|
+
resolved: "Resolved",
|
|
14
|
+
reopen: "Reopen",
|
|
15
|
+
empty: "No comments yet. Start the conversation below.",
|
|
16
|
+
reply: "Reply",
|
|
17
|
+
edit: "Edit",
|
|
18
|
+
delete: "Delete",
|
|
19
|
+
cancel: "Cancel",
|
|
20
|
+
send: "Send",
|
|
21
|
+
save: "Save",
|
|
22
|
+
editComment: "Edit comment",
|
|
23
|
+
edited: "edited",
|
|
24
|
+
deleteConfirm: "Delete this comment?",
|
|
25
|
+
deletedText: "This comment was deleted",
|
|
26
|
+
deletedComment: "Deleted comment",
|
|
27
|
+
replyingTo: name => `Replying to ${name}`,
|
|
28
|
+
cancelReply: "Cancel reply",
|
|
29
|
+
showReplies: count => `Show ${count} ${count === 1 ? "reply" : "replies"}`,
|
|
30
|
+
hideReplies: "Hide replies",
|
|
31
|
+
addReaction: "Add reaction",
|
|
32
|
+
closeReactions: "Close reactions",
|
|
33
|
+
reactions: "Reactions",
|
|
34
|
+
reactWith: emoji => `React with ${emoji}`,
|
|
35
|
+
reactionCount: (emoji, count, includesYou) => `${emoji} ${count}${includesYou ? ", including you" : ""}`,
|
|
36
|
+
mentions: "People",
|
|
37
|
+
};
|
|
38
|
+
/** A key passed as undefined keeps its default too. */
|
|
39
|
+
const withDefaults = (defaults, given) => ({ ...defaults, ...Object.fromEntries(Object.entries(given ?? {}).filter(([, value]) => value !== undefined)) });
|
|
40
|
+
const enter = [...motionTokens.ease.enter];
|
|
41
|
+
const standard = [...motionTokens.ease.standard];
|
|
42
|
+
const physical = (visualDuration, bounce) => {
|
|
43
|
+
const root = 2 * Math.PI / (visualDuration * 1.2);
|
|
44
|
+
return { type: "spring", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };
|
|
45
|
+
};
|
|
46
|
+
const HEIGHT = physical(.42, 0);
|
|
47
|
+
const DEFAULT_REACTIONS = ["👍", "❤️", "🎉", "👀", "🚀", "✅"];
|
|
48
|
+
const subscribe = () => () => { };
|
|
49
|
+
function useReducedFlag() {
|
|
50
|
+
const hydrated = useSyncExternalStore(subscribe, () => true, () => false);
|
|
51
|
+
return !!useReducedMotion() && hydrated;
|
|
52
|
+
}
|
|
53
|
+
/* Tree helpers. Every change returns a new tree so controlled parents can store it as is. */
|
|
54
|
+
const mapTree = (list, id, fn) => list.flatMap(comment => {
|
|
55
|
+
if (comment.id === id) {
|
|
56
|
+
const next = fn(comment);
|
|
57
|
+
return next ? [next] : [];
|
|
58
|
+
}
|
|
59
|
+
return comment.replies?.length ? [{ ...comment, replies: mapTree(comment.replies, id, fn) }] : [comment];
|
|
60
|
+
});
|
|
61
|
+
const countTree = (list) => list.reduce((sum, comment) => sum + (comment.deleted ? 0 : 1) + countTree(comment.replies ?? []), 0);
|
|
62
|
+
const authorsOf = (list, into = new Map()) => {
|
|
63
|
+
list.forEach(comment => { if (!comment.deleted)
|
|
64
|
+
into.set(comment.author.id, comment.author); authorsOf(comment.replies ?? [], into); });
|
|
65
|
+
return into;
|
|
66
|
+
};
|
|
67
|
+
/** Path of ids from the root to `id`. */
|
|
68
|
+
const pathTo = (list, id, trail = []) => {
|
|
69
|
+
for (const comment of list) {
|
|
70
|
+
if (comment.id === id)
|
|
71
|
+
return [...trail, id];
|
|
72
|
+
const found = pathTo(comment.replies ?? [], id, [...trail, comment.id]);
|
|
73
|
+
if (found)
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
const find = (list, id) => {
|
|
79
|
+
for (const comment of list) {
|
|
80
|
+
if (comment.id === id)
|
|
81
|
+
return comment;
|
|
82
|
+
const found = find(comment.replies ?? [], id);
|
|
83
|
+
if (found)
|
|
84
|
+
return found;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
/** Wraps the first `part` inside `text`, so a translated sentence keeps its highlighted name wherever the language puts it. */
|
|
89
|
+
const highlight = (text, part, className) => {
|
|
90
|
+
const at = part ? text.indexOf(part) : -1;
|
|
91
|
+
return at < 0 ? text : _jsxs(_Fragment, { children: [text.slice(0, at), _jsx("span", { className: className, children: part }), text.slice(at + part.length)] });
|
|
92
|
+
};
|
|
93
|
+
const initials = (name) => name.split(/\s+/).map(part => part[0]).slice(0, 2).join("").toUpperCase();
|
|
94
|
+
let serial = 0;
|
|
95
|
+
const newId = () => `c-${Date.now().toString(36)}-${(serial++).toString(36)}`;
|
|
96
|
+
function Avatar({ author, size = 28 }) {
|
|
97
|
+
return author.avatar
|
|
98
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
99
|
+
? _jsx("img", { className: styles.avatar, src: author.avatar, alt: "", width: size, height: size, style: { width: size, height: size } })
|
|
100
|
+
: _jsx("span", { className: styles.avatar, style: { width: size, height: size, fontSize: Math.round(size * .38) }, "aria-hidden": "true", children: initials(author.name) });
|
|
101
|
+
}
|
|
102
|
+
/** Highlights "@Full Name" for known people. */
|
|
103
|
+
function Body({ text, people }) {
|
|
104
|
+
const parts = useMemo(() => {
|
|
105
|
+
const names = people.map(person => person.name).sort((a, b) => b.length - a.length).map(name => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
106
|
+
if (!names.length)
|
|
107
|
+
return [text];
|
|
108
|
+
return text.split(new RegExp(`(@(?:${names.join("|")}))`, "g"));
|
|
109
|
+
}, [people, text]);
|
|
110
|
+
return _jsx("p", { className: styles.body, children: parts.map((part, index) => index % 2 ? _jsx("span", { className: styles.mention, children: part }, index) : part) });
|
|
111
|
+
}
|
|
112
|
+
const roll = {
|
|
113
|
+
hidden: (dir) => ({ y: `${dir * 70}%`, opacity: 0 }),
|
|
114
|
+
shown: { y: 0, opacity: 1 },
|
|
115
|
+
gone: (dir) => ({ y: `${dir * -70}%`, opacity: 0 }),
|
|
116
|
+
};
|
|
117
|
+
const rollReduced = { hidden: { opacity: 0 }, shown: { opacity: 1 }, gone: { opacity: 0 } };
|
|
118
|
+
/** Rolls a count up or down in place. */
|
|
119
|
+
function Count({ value, reduced }) {
|
|
120
|
+
const [last, setLast] = useState(value);
|
|
121
|
+
const [dir, setDir] = useState(1);
|
|
122
|
+
if (value !== last) {
|
|
123
|
+
setDir(value > last ? 1 : -1);
|
|
124
|
+
setLast(value);
|
|
125
|
+
}
|
|
126
|
+
return _jsx("span", { className: styles.count, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: dir, children: _jsx(motion.span, { custom: dir, variants: reduced ? rollReduced : roll, initial: "hidden", animate: "shown", exit: "gone", transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: value }, value) }) });
|
|
127
|
+
}
|
|
128
|
+
/** Follows its content's height exactly, except when `morphKey` changes: then it springs, so the thread and the resolved chip morph into each other. */
|
|
129
|
+
function AutoHeight({ children, reduced, morphKey }) {
|
|
130
|
+
const inner = useRef(null);
|
|
131
|
+
const height = useMotionValue(0);
|
|
132
|
+
const [measured, setMeasured] = useState(false);
|
|
133
|
+
const morphing = useRef(false);
|
|
134
|
+
const lastKey = useRef(morphKey);
|
|
135
|
+
useLayoutEffect(() => {
|
|
136
|
+
if (lastKey.current !== morphKey) {
|
|
137
|
+
lastKey.current = morphKey;
|
|
138
|
+
morphing.current = true;
|
|
139
|
+
}
|
|
140
|
+
}, [morphKey]);
|
|
141
|
+
useLayoutEffect(() => {
|
|
142
|
+
const node = inner.current;
|
|
143
|
+
if (!node || typeof ResizeObserver === "undefined")
|
|
144
|
+
return;
|
|
145
|
+
// The border box keeps subpixel line heights, so the frame never clips a fraction of the last line.
|
|
146
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
147
|
+
const next = entry?.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight;
|
|
148
|
+
if (morphing.current && !reduced) {
|
|
149
|
+
morphing.current = false;
|
|
150
|
+
animate(height, next, HEIGHT);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (height.isAnimating()) {
|
|
154
|
+
animate(height, next, HEIGHT);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
height.jump(next);
|
|
158
|
+
setMeasured(true);
|
|
159
|
+
});
|
|
160
|
+
observer.observe(node);
|
|
161
|
+
return () => observer.disconnect();
|
|
162
|
+
}, [height, reduced]);
|
|
163
|
+
return _jsx(motion.div, { className: styles.autoHeight, style: { height: measured ? height : "auto" }, children: _jsx("div", { ref: inner, className: styles.autoInner, children: children }) });
|
|
164
|
+
}
|
|
165
|
+
/** A growing text field with @mention suggestions. ⌘ or Ctrl + Enter sends; Escape cancels. */
|
|
166
|
+
function Composer({ people, initial = "", placeholder, submitLabel, autoFocus, focusKey, onSubmit, onCancel, leading, compact, cancelLabel, mentionsLabel }) {
|
|
167
|
+
const uid = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
168
|
+
const reduced = useReducedFlag();
|
|
169
|
+
const field = useRef(null);
|
|
170
|
+
const [text, setText] = useState(initial);
|
|
171
|
+
const [query, setQuery] = useState(null);
|
|
172
|
+
const [active, setActive] = useState(0);
|
|
173
|
+
const suggestions = useMemo(() => {
|
|
174
|
+
if (!query)
|
|
175
|
+
return [];
|
|
176
|
+
const term = query.term.toLowerCase();
|
|
177
|
+
return people.filter(person => person.name.toLowerCase().split(" ").some(part => part.startsWith(term)) || person.name.toLowerCase().startsWith(term)).slice(0, 5);
|
|
178
|
+
}, [people, query]);
|
|
179
|
+
const open = !!query && suggestions.length > 0;
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!autoFocus && !focusKey)
|
|
182
|
+
return;
|
|
183
|
+
const node = field.current;
|
|
184
|
+
if (!node)
|
|
185
|
+
return;
|
|
186
|
+
node.focus({ preventScroll: true });
|
|
187
|
+
node.setSelectionRange(node.value.length, node.value.length);
|
|
188
|
+
}, [autoFocus, focusKey]);
|
|
189
|
+
const readQuery = (value, caret) => {
|
|
190
|
+
const match = /(^|\s)@([\p{L}]*)$/u.exec(value.slice(0, caret));
|
|
191
|
+
setQuery(match ? { start: caret - match[2].length - 1, term: match[2] } : null);
|
|
192
|
+
setActive(0);
|
|
193
|
+
};
|
|
194
|
+
const insert = (person) => {
|
|
195
|
+
const node = field.current;
|
|
196
|
+
if (!node || !query)
|
|
197
|
+
return;
|
|
198
|
+
const caret = node.selectionStart ?? text.length;
|
|
199
|
+
const next = `${text.slice(0, query.start)}@${person.name} ${text.slice(caret)}`;
|
|
200
|
+
const at = query.start + person.name.length + 2;
|
|
201
|
+
setText(next);
|
|
202
|
+
setQuery(null);
|
|
203
|
+
requestAnimationFrame(() => { node.focus(); node.setSelectionRange(at, at); });
|
|
204
|
+
};
|
|
205
|
+
const send = () => {
|
|
206
|
+
const body = text.trim();
|
|
207
|
+
if (!body)
|
|
208
|
+
return;
|
|
209
|
+
onSubmit(body);
|
|
210
|
+
setText("");
|
|
211
|
+
setQuery(null);
|
|
212
|
+
};
|
|
213
|
+
const onKeyDown = (event) => {
|
|
214
|
+
if (open) {
|
|
215
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
setActive(index => (index + (event.key === "ArrowDown" ? 1 : -1) + suggestions.length) % suggestions.length);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
insert(suggestions[active]);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (event.key === "Escape") {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
event.stopPropagation();
|
|
228
|
+
setQuery(null);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
233
|
+
event.preventDefault();
|
|
234
|
+
send();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (event.key === "Escape" && onCancel) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
event.stopPropagation();
|
|
240
|
+
onCancel();
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
return _jsxs("div", { className: styles.composer, "data-compact": compact || undefined, children: [leading, _jsxs("div", { className: styles.fieldWrap, children: [_jsx("textarea", { ref: field, className: styles.field, value: text, placeholder: placeholder, rows: 1, "aria-label": placeholder, role: "combobox", "aria-autocomplete": "list", "aria-expanded": open, "aria-controls": `${uid}-people`, "aria-activedescendant": open ? `${uid}-person-${active}` : undefined, onChange: event => { setText(event.target.value); readQuery(event.target.value, event.target.selectionStart); }, onSelect: event => readQuery(event.currentTarget.value, event.currentTarget.selectionStart), onBlur: () => setQuery(null), onKeyDown: onKeyDown }), _jsx(AnimatePresence, { children: open && _jsx(motion.ul, { id: `${uid}-people`, role: "listbox", "aria-label": mentionsLabel, className: styles.suggestions, initial: reduced ? { opacity: 0 } : { opacity: 0, height: 0 }, animate: { opacity: 1, height: "auto" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, height: 0, transition: { duration: motionTokens.duration.instant, ease: standard } }, transition: reduced ? { duration: 0 } : { height: HEIGHT, opacity: { duration: motionTokens.duration.fast, ease: enter } }, children: suggestions.map((person, index) => _jsxs("li", { id: `${uid}-person-${index}`, role: "option", "aria-selected": index === active, className: styles.suggestion, onPointerDown: event => event.preventDefault(), onPointerMove: () => setActive(index), onClick: () => insert(person), children: [_jsx(Avatar, { author: person, size: 20 }), person.name] }, person.id)) }, "people") })] }), _jsxs("div", { className: styles.composerActions, children: [onCancel && _jsx("button", { type: "button", className: styles.ghost, onClick: onCancel, children: cancelLabel }), _jsx("button", { type: "button", className: styles.send, disabled: !text.trim() || text.trim() === initial.trim(), onClick: send, children: submitLabel })] })] });
|
|
244
|
+
}
|
|
245
|
+
export const CommentThread = forwardRef(function CommentThread({ comments, defaultComments = [], onCommentsChange, currentUser, people, resolved, defaultResolved = false, onResolvedChange, title, reactions = DEFAULT_REACTIONS, placeholder = "Reply, or @mention someone", maxDepth = 2, nowLabel = "Just now", labels, className, }, forwardedRef) {
|
|
246
|
+
const t = withDefaults(DEFAULT_LABELS, labels);
|
|
247
|
+
const reduced = useReducedFlag();
|
|
248
|
+
const rootRef = useRef(null);
|
|
249
|
+
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
250
|
+
const [internal, setInternal] = useState(defaultComments);
|
|
251
|
+
const list = comments ?? internal;
|
|
252
|
+
const [internalResolved, setInternalResolved] = useState(defaultResolved);
|
|
253
|
+
const isResolved = resolved ?? internalResolved;
|
|
254
|
+
const [ui, setUi] = useState({ editing: null, confirming: null, picker: null, collapsed: {} });
|
|
255
|
+
const [replyTo, setReplyTo] = useState(null);
|
|
256
|
+
const [focusKey, setFocusKey] = useState(0);
|
|
257
|
+
const mentionable = useMemo(() => people ?? [...authorsOf(list, new Map([[currentUser.id, currentUser]])).values()], [currentUser, list, people]);
|
|
258
|
+
const total = countTree(list);
|
|
259
|
+
const participants = useMemo(() => [...authorsOf(list).values()], [list]);
|
|
260
|
+
const commit = useCallback((next, event) => {
|
|
261
|
+
if (comments === undefined)
|
|
262
|
+
setInternal(next);
|
|
263
|
+
onCommentsChange?.(next, event);
|
|
264
|
+
}, [comments, onCommentsChange]);
|
|
265
|
+
const setResolved = (next) => {
|
|
266
|
+
if (resolved === undefined)
|
|
267
|
+
setInternalResolved(next);
|
|
268
|
+
onResolvedChange?.(next);
|
|
269
|
+
setUi(current => ({ ...current, editing: null, confirming: null, picker: null }));
|
|
270
|
+
setReplyTo(null);
|
|
271
|
+
requestAnimationFrame(() => rootRef.current?.querySelector(next ? "[data-reopen]" : "[data-resolve]")?.focus({ preventScroll: true }));
|
|
272
|
+
};
|
|
273
|
+
const addReply = (body) => {
|
|
274
|
+
const comment = { id: newId(), author: currentUser, body, createdAt: nowLabel, reactions: [] };
|
|
275
|
+
let parentId = null;
|
|
276
|
+
if (replyTo) {
|
|
277
|
+
const path = pathTo(list, replyTo.id) ?? [];
|
|
278
|
+
parentId = path[Math.min(path.length, maxDepth) - 1] ?? null;
|
|
279
|
+
}
|
|
280
|
+
const next = parentId ? mapTree(list, parentId, parent => ({ ...parent, replies: [...(parent.replies ?? []), comment] })) : [...list, comment];
|
|
281
|
+
if (parentId)
|
|
282
|
+
setUi(current => ({ ...current, collapsed: { ...current.collapsed, [parentId]: false } }));
|
|
283
|
+
commit(next, { type: "reply", comment, parentId });
|
|
284
|
+
setReplyTo(null);
|
|
285
|
+
};
|
|
286
|
+
const edit = (id, body) => {
|
|
287
|
+
commit(mapTree(list, id, comment => ({ ...comment, body, edited: true })), { type: "edit", id, body });
|
|
288
|
+
setUi(current => ({ ...current, editing: null }));
|
|
289
|
+
};
|
|
290
|
+
const remove = (id) => {
|
|
291
|
+
commit(mapTree(list, id, comment => comment.replies?.length ? { ...comment, deleted: true, body: "", reactions: [] } : null), { type: "delete", id });
|
|
292
|
+
setUi(current => ({ ...current, confirming: null }));
|
|
293
|
+
if (replyTo?.id === id)
|
|
294
|
+
setReplyTo(null);
|
|
295
|
+
};
|
|
296
|
+
const react = (id, emoji) => {
|
|
297
|
+
const target = find(list, id);
|
|
298
|
+
if (!target)
|
|
299
|
+
return;
|
|
300
|
+
const existing = target.reactions?.find(reaction => reaction.emoji === emoji);
|
|
301
|
+
const added = !existing?.users.includes(currentUser.id);
|
|
302
|
+
const nextReactions = existing
|
|
303
|
+
? (target.reactions ?? []).map(reaction => reaction.emoji !== emoji ? reaction : { ...reaction, users: added ? [...reaction.users, currentUser.id] : reaction.users.filter(user => user !== currentUser.id) }).filter(reaction => reaction.users.length)
|
|
304
|
+
: [...(target.reactions ?? []), { emoji, users: [currentUser.id] }];
|
|
305
|
+
commit(mapTree(list, id, comment => ({ ...comment, reactions: nextReactions })), { type: "react", id, emoji, added });
|
|
306
|
+
setUi(current => ({ ...current, picker: null }));
|
|
307
|
+
};
|
|
308
|
+
const grow = reduced
|
|
309
|
+
? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: motionTokens.duration.instant } }
|
|
310
|
+
: { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { height: HEIGHT, opacity: { duration: motionTokens.duration.standard, ease: enter } } };
|
|
311
|
+
const swap = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter } };
|
|
312
|
+
const renderComment = (comment, depth) => {
|
|
313
|
+
const mine = comment.author.id === currentUser.id;
|
|
314
|
+
const replies = comment.replies ?? [];
|
|
315
|
+
const collapsed = !!ui.collapsed[comment.id];
|
|
316
|
+
const editing = ui.editing === comment.id, confirming = ui.confirming === comment.id, picking = ui.picker === comment.id;
|
|
317
|
+
return _jsxs(motion.li, { className: styles.item, ...grow, children: [_jsxs("article", { className: styles.comment, "aria-label": comment.deleted ? t.deletedComment : `${comment.author.name}, ${comment.createdAt}`, children: [comment.deleted ? _jsx("span", { className: styles.deletedDot, "aria-hidden": "true" }) : _jsx(Avatar, { author: comment.author }), _jsxs("div", { className: styles.main, children: [comment.deleted
|
|
318
|
+
? _jsx("p", { className: styles.deleted, children: t.deletedText })
|
|
319
|
+
: _jsxs(_Fragment, { children: [_jsxs("header", { className: styles.meta, children: [_jsx("span", { className: styles.name, children: comment.author.name }), _jsxs("span", { className: styles.time, children: [comment.createdAt, comment.edited && ` · ${t.edited}`] })] }), _jsx(AutoHeight, { reduced: reduced, morphKey: editing ? "edit" : confirming ? "confirm" : picking ? "picker" : "view", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: editing
|
|
320
|
+
? _jsx(motion.div, { ...swap, children: _jsx(Composer, { people: mentionable, initial: comment.body, placeholder: t.editComment, submitLabel: t.save, cancelLabel: t.cancel, mentionsLabel: t.mentions, autoFocus: true, compact: true, onSubmit: body => edit(comment.id, body), onCancel: () => setUi(current => ({ ...current, editing: null })) }) }, "edit")
|
|
321
|
+
: _jsxs(motion.div, { ...swap, children: [_jsx(Body, { text: comment.body, people: mentionable }), _jsxs("div", { className: styles.footer, children: [(comment.reactions ?? []).length > 0 && _jsx("div", { className: styles.reactions, children: _jsx(AnimatePresence, { initial: false, mode: "popLayout", children: (comment.reactions ?? []).map(reaction => {
|
|
322
|
+
const pressed = reaction.users.includes(currentUser.id);
|
|
323
|
+
return _jsxs(motion.button, { type: "button", layout: !reduced, className: styles.reaction, "aria-pressed": pressed, "aria-label": t.reactionCount(reaction.emoji, reaction.users.length, pressed), onClick: () => react(comment.id, reaction.emoji), initial: reduced ? { opacity: 0 } : { opacity: 0, scale: .7 }, animate: { opacity: 1, scale: 1 }, exit: reduced ? { opacity: 0 } : { opacity: 0, scale: .7 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, children: [_jsx("span", { "aria-hidden": "true", children: reaction.emoji }), _jsx(Count, { value: reaction.users.length, reduced: reduced })] }, reaction.emoji);
|
|
324
|
+
}) }) }), _jsx(AnimatePresence, { mode: "wait", initial: false, children: confirming
|
|
325
|
+
? _jsxs(motion.div, { className: styles.actions, "data-open": "", initial: reduced ? { opacity: 0 } : { opacity: 0, x: 6 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter }, onKeyDown: event => { if (event.key === "Escape") {
|
|
326
|
+
event.stopPropagation();
|
|
327
|
+
setUi(current => ({ ...current, confirming: null }));
|
|
328
|
+
} }, children: [_jsx("span", { className: styles.prompt, children: t.deleteConfirm }), _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, confirming: null })), children: t.cancel }), _jsx("button", { type: "button", className: styles.action, "data-tone": "danger", autoFocus: true, onClick: () => remove(comment.id), children: t.delete })] }, "confirm")
|
|
329
|
+
: picking
|
|
330
|
+
? _jsxs(motion.div, { role: "group", "aria-label": t.reactions, className: styles.actions, "data-open": "", onKeyDown: event => { if (event.key === "Escape") {
|
|
331
|
+
event.stopPropagation();
|
|
332
|
+
setUi(current => ({ ...current, picker: null }));
|
|
333
|
+
} }, initial: reduced ? { opacity: 0 } : { opacity: 0, x: -6 }, animate: { opacity: 1, x: 0 }, exit: { opacity: 0, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard } }, transition: { duration: reduced ? 0 : motionTokens.duration.fast, ease: enter }, children: [reactions.map((emoji, index) => _jsx(motion.button, { type: "button", className: styles.emoji, autoFocus: index === 0, "aria-label": t.reactWith(emoji), "aria-pressed": !!comment.reactions?.find(reaction => reaction.emoji === emoji)?.users.includes(currentUser.id), onClick: () => react(comment.id, emoji), initial: reduced ? false : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, transition: reduced ? { duration: 0 } : { ...motionTokens.spring.snappy, delay: index * motionTokens.stagger.item }, children: emoji }, emoji)), _jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.closeReactions, onClick: () => setUi(current => ({ ...current, picker: null })), children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) })] }, "picker")
|
|
334
|
+
: _jsxs(motion.div, { className: styles.actions, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: reduced ? 0 : motionTokens.duration.instant, ease: standard }, children: [_jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.addReaction, onClick: () => setUi(current => ({ ...current, picker: comment.id, confirming: null })), children: _jsx(SmilePlus, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("button", { type: "button", className: styles.action, onClick: () => { setReplyTo({ id: comment.id, name: comment.author.name }); setFocusKey(key => key + 1); }, children: t.reply }), mine && _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, editing: comment.id, confirming: null, picker: null })), children: t.edit }), mine && _jsx("button", { type: "button", className: styles.action, onClick: () => setUi(current => ({ ...current, confirming: comment.id, editing: null, picker: null })), children: t.delete })] }, "actions") })] })] }, "view") }) })] }), replies.length > 0 && _jsxs("button", { type: "button", className: styles.toggle, "aria-expanded": !collapsed, onClick: () => setUi(current => ({ ...current, collapsed: { ...current.collapsed, [comment.id]: !collapsed } })), children: [_jsx(motion.span, { className: styles.toggleIcon, initial: false, animate: { rotate: collapsed ? 0 : 90 }, transition: reduced ? { duration: 0 } : motionTokens.spring.snappy, "aria-hidden": "true", children: _jsx(ChevronRight, { size: 13, strokeWidth: 1.75 }) }), collapsed ? t.showReplies(countTree(replies)) : t.hideReplies, collapsed && _jsx("span", { className: styles.stack, "aria-hidden": "true", children: [...authorsOf(replies).values()].slice(0, 3).map(author => _jsx(Avatar, { author: author, size: 18 }, author.id)) })] })] })] }), _jsx(AnimatePresence, { initial: false, children: replies.length > 0 && !collapsed && _jsx(motion.div, { className: styles.repliesWrap, ...grow, children: _jsx("ul", { className: styles.replies, "data-depth": depth + 1, children: _jsx(AnimatePresence, { initial: false, children: replies.map(reply => renderComment(reply, depth + 1)) }) }) }, "replies") })] }, comment.id);
|
|
335
|
+
};
|
|
336
|
+
const face = { initial: reduced ? { opacity: 0 } : { opacity: 0, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, filter: "blur(0px)" }, exit: { opacity: 0, filter: reduced ? "blur(0px)" : `blur(${motionTokens.blur.subtle}px)` }, transition: { duration: reduced ? 0 : motionTokens.duration.standard, ease: enter } };
|
|
337
|
+
return _jsx("section", { ref: rootRef, className: [styles.root, className].filter(Boolean).join(" "), "data-resolved": isResolved || undefined, "aria-label": t.thread, children: _jsx(AutoHeight, { reduced: reduced, morphKey: isResolved ? "resolved" : "thread", children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: isResolved
|
|
338
|
+
? _jsxs(motion.div, { className: styles.resolved, ...face, children: [_jsx("span", { className: styles.resolvedIcon, "aria-hidden": "true", children: _jsx(Check, { size: 14, strokeWidth: 2 }) }), _jsxs("span", { className: styles.resolvedText, children: [_jsxs("span", { className: styles.resolvedTitle, children: [t.resolved, title ? _jsxs(_Fragment, { children: [" \u00B7 ", _jsx("span", { className: styles.resolvedSubject, children: title })] }) : null] }), _jsxs("span", { className: styles.resolvedMeta, children: [t.commentCount(total), " \u00B7 ", t.peopleCount(participants.length)] })] }), _jsx("span", { className: styles.stack, "aria-hidden": "true", children: participants.slice(0, 3).map(author => _jsx(Avatar, { author: author, size: 22 }, author.id)) }), _jsxs("button", { type: "button", className: styles.reopen, "data-reopen": true, onClick: () => setResolved(false), children: [_jsx(RotateCcw, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), t.reopen] })] }, "resolved")
|
|
339
|
+
: _jsxs(motion.div, { className: styles.thread, ...face, children: [_jsxs("header", { className: styles.header, children: [_jsxs("div", { className: styles.heading, children: [title && _jsx("h3", { className: styles.title, children: title }), _jsx("span", { className: styles.subtitle, children: t.commentCount(total) })] }), _jsxs("button", { type: "button", className: styles.resolve, "data-resolve": true, disabled: !total, onClick: () => setResolved(true), children: [_jsx(Check, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), t.resolve] })] }), list.length > 0
|
|
340
|
+
? _jsx("ul", { className: styles.list, children: _jsx(AnimatePresence, { initial: false, children: list.map(comment => renderComment(comment, 0)) }) })
|
|
341
|
+
: _jsx("p", { className: styles.empty, children: t.empty }), _jsxs("div", { className: styles.composerArea, children: [_jsx(AnimatePresence, { initial: false, children: replyTo && _jsx(motion.div, { className: styles.replying, ...grow, children: _jsxs("span", { className: styles.replyingInner, children: [_jsx(CornerDownRight, { size: 13, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("span", { children: highlight(t.replyingTo(replyTo.name), replyTo.name, styles.replyingName) }), _jsx("button", { type: "button", className: styles.iconAction, "aria-label": t.cancelReply, onClick: () => setReplyTo(null), children: _jsx(X, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }) })] }) }, "replying") }), _jsx(Composer, { people: mentionable, placeholder: placeholder, submitLabel: t.send, cancelLabel: t.cancel, mentionsLabel: t.mentions, focusKey: focusKey, onSubmit: addReply, onCancel: replyTo ? () => setReplyTo(null) : undefined, leading: _jsx(Avatar, { author: currentUser }) })] })] }, "thread") }) }) });
|
|
342
|
+
});
|
|
343
|
+
export default CommentThread;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
border: 1px solid var(--border);
|
|
5
|
+
border-radius: var(--radius-container); corner-shape: var(--corner-shape);
|
|
6
|
+
background: var(--surface);
|
|
7
|
+
color: var(--foreground);
|
|
8
|
+
font-family: var(--font-body);
|
|
9
|
+
font-size: var(--text-sm);
|
|
10
|
+
letter-spacing: var(--tracking-body);
|
|
11
|
+
line-height: var(--leading-body);
|
|
12
|
+
transition: border-radius var(--duration-spring) var(--ease-spring);
|
|
13
|
+
}
|
|
14
|
+
.root *, .root *::before, .root *::after { box-sizing: border-box; }
|
|
15
|
+
.root[data-resolved] { border-radius: var(--radius-container); corner-shape: var(--corner-shape); }
|
|
16
|
+
.autoHeight { overflow: hidden; border-radius: inherit; corner-shape: inherit; }
|
|
17
|
+
.autoInner { position: relative; }
|
|
18
|
+
|
|
19
|
+
/* Header */
|
|
20
|
+
.thread { display: grid; }
|
|
21
|
+
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 12px 8px 18px; }
|
|
22
|
+
.heading { display: grid; min-width: 0; }
|
|
23
|
+
.title { margin: 0; overflow: hidden; font-size: var(--text-sm); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
|
+
.subtitle { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
25
|
+
.resolve,
|
|
26
|
+
.reopen {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
height: 32px;
|
|
29
|
+
flex: none;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
padding: 0 12px 0 10px;
|
|
33
|
+
border: 1px solid var(--border);
|
|
34
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
35
|
+
background: var(--surface);
|
|
36
|
+
color: var(--foreground);
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
font: inherit;
|
|
39
|
+
font-size: var(--text-xs);
|
|
40
|
+
font-weight: 500;
|
|
41
|
+
transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
42
|
+
}
|
|
43
|
+
.resolve > svg { color: var(--success); }
|
|
44
|
+
.resolve:active:not(:disabled), .reopen:active { transform: scale(.97); }
|
|
45
|
+
.resolve:disabled { opacity: .45; cursor: default; }
|
|
46
|
+
|
|
47
|
+
/* Comments */
|
|
48
|
+
.list, .replies { display: grid; margin: 0; padding: 0; list-style: none; }
|
|
49
|
+
.list { padding: 0 8px; }
|
|
50
|
+
.item { overflow: clip; overflow-clip-margin: 4px; }
|
|
51
|
+
.repliesWrap { overflow: clip; }
|
|
52
|
+
/* Replies hang from a quiet rail under their parent's avatar. */
|
|
53
|
+
.replies { position: relative; margin-left: 24px; padding-left: 12px; }
|
|
54
|
+
.replies::before { content: ""; position: absolute; top: 2px; bottom: 14px; left: 0; width: 1px; background: var(--border); }
|
|
55
|
+
|
|
56
|
+
.comment { display: flex; gap: 10px; padding: 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
|
|
57
|
+
.avatar { display: inline-grid; flex: none; place-items: center; overflow: hidden; border-radius: 50%; corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); font-weight: 500; letter-spacing: 0; object-fit: cover; }
|
|
58
|
+
.deletedDot { width: 28px; height: 28px; flex: none; border: 1px dashed var(--border-strong); border-radius: 50%; corner-shape: round; }
|
|
59
|
+
.main { display: grid; min-width: 0; flex: 1; gap: 2px; }
|
|
60
|
+
.meta { display: flex; min-width: 0; align-items: baseline; gap: 8px; }
|
|
61
|
+
.name { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
62
|
+
.time { flex: none; color: var(--text-muted); font-size: var(--text-xs); }
|
|
63
|
+
.body { margin: 0; color: var(--text-secondary); overflow-wrap: anywhere; white-space: pre-wrap; text-wrap: pretty; }
|
|
64
|
+
.mention { color: var(--accent-strong); font-weight: 500; }
|
|
65
|
+
.deleted { margin: 4px 0 0; color: var(--text-muted); font-style: italic; }
|
|
66
|
+
|
|
67
|
+
.footer { display: flex; min-height: 30px; flex-wrap: wrap; align-items: center; gap: 4px 8px; margin-top: 4px; }
|
|
68
|
+
.reactions { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
69
|
+
.reaction {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
height: 26px;
|
|
72
|
+
align-items: center;
|
|
73
|
+
gap: 5px;
|
|
74
|
+
padding: 0 9px 0 7px;
|
|
75
|
+
border: 1px solid var(--border);
|
|
76
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
77
|
+
background: var(--surface);
|
|
78
|
+
color: var(--text-secondary);
|
|
79
|
+
cursor: pointer;
|
|
80
|
+
font: inherit;
|
|
81
|
+
font-size: var(--text-xs);
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
84
|
+
}
|
|
85
|
+
.reaction[aria-pressed="true"] { border-color: color-mix(in oklab, var(--accent) 45%, var(--border)); background: var(--accent-subtle); color: var(--foreground); }
|
|
86
|
+
.reaction:active { transform: scale(.94); }
|
|
87
|
+
.count { position: relative; display: inline-grid; min-width: 1ch; overflow: hidden; font-variant-numeric: tabular-nums; }
|
|
88
|
+
.count > span { grid-area: 1 / 1; }
|
|
89
|
+
|
|
90
|
+
/* Actions sit quietly after the reactions and come forward on hover or focus; touch screens always show them. */
|
|
91
|
+
.actions { display: flex; min-height: 28px; flex-wrap: wrap; align-items: center; gap: 2px; transition: opacity var(--duration-fast) var(--ease-standard); }
|
|
92
|
+
.action,
|
|
93
|
+
.iconAction {
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
height: 28px;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
padding: 0 8px;
|
|
99
|
+
border: 0;
|
|
100
|
+
border-radius: var(--radius-full); corner-shape: round;
|
|
101
|
+
background: transparent;
|
|
102
|
+
color: var(--text-muted);
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
font: inherit;
|
|
105
|
+
font-size: var(--text-xs);
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
-webkit-tap-highlight-color: transparent;
|
|
108
|
+
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard);
|
|
109
|
+
}
|
|
110
|
+
.iconAction { width: 28px; padding: 0; }
|
|
111
|
+
.action[data-tone="danger"] { color: var(--danger); }
|
|
112
|
+
.prompt { margin-right: 4px; color: var(--foreground); font-size: var(--text-xs); }
|
|
113
|
+
.emoji { display: grid; width: 28px; height: 28px; place-items: center; padding: 0; border: 0; border-radius: 50%; corner-shape: round; background: transparent; cursor: pointer; font-size: var(--text-base); line-height: 1; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
114
|
+
.emoji[aria-pressed="true"] { background: var(--accent-subtle); }
|
|
115
|
+
.emoji:active { transform: scale(.88); }
|
|
116
|
+
|
|
117
|
+
.toggle { display: inline-flex; width: fit-content; height: 28px; align-items: center; gap: 6px; margin: 2px 0 0 -6px; padding: 0 8px 0 6px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
118
|
+
.toggleIcon { display: grid; place-items: center; color: var(--text-muted); }
|
|
119
|
+
.stack { display: inline-flex; flex: none; padding-left: 4px; }
|
|
120
|
+
.stack > * { margin-left: -5px; box-shadow: 0 0 0 2px var(--surface); }
|
|
121
|
+
|
|
122
|
+
.empty { margin: 0; padding: 8px 18px 16px; color: var(--text-muted); }
|
|
123
|
+
|
|
124
|
+
/* Composer */
|
|
125
|
+
.composerArea { display: grid; margin-top: 4px; padding: 10px 12px 12px; border-top: 1px solid var(--border-subtle); }
|
|
126
|
+
.replying { overflow: clip; }
|
|
127
|
+
.replyingInner { display: flex; align-items: center; gap: 6px; padding: 0 2px 8px 38px; color: var(--text-secondary); font-size: var(--text-xs); }
|
|
128
|
+
.replyingInner .iconAction { width: 22px; height: 22px; margin-left: 2px; }
|
|
129
|
+
.replyingName { color: var(--foreground); font-weight: 500; }
|
|
130
|
+
|
|
131
|
+
.composer { display: grid; align-items: start; gap: 8px 10px; grid-template-columns: auto minmax(0, 1fr) auto; }
|
|
132
|
+
.composer > .avatar { margin-top: 5px; }
|
|
133
|
+
.composer[data-compact] { grid-template-columns: minmax(0, 1fr); margin-top: 4px; }
|
|
134
|
+
.fieldWrap { display: grid; min-width: 0; }
|
|
135
|
+
.field {
|
|
136
|
+
width: 100%;
|
|
137
|
+
min-height: 38px;
|
|
138
|
+
max-height: 160px;
|
|
139
|
+
padding: 8px 12px;
|
|
140
|
+
border: 1px solid var(--border);
|
|
141
|
+
border-radius: var(--radius-control); corner-shape: var(--corner-shape);
|
|
142
|
+
background: var(--surface);
|
|
143
|
+
color: var(--foreground);
|
|
144
|
+
font: inherit;
|
|
145
|
+
resize: none;
|
|
146
|
+
field-sizing: content;
|
|
147
|
+
transition: border-color var(--duration-fast) var(--ease-standard);
|
|
148
|
+
}
|
|
149
|
+
.field::placeholder { color: var(--text-muted); }
|
|
150
|
+
.field:focus { border-color: var(--border-strong); }
|
|
151
|
+
.composerActions { display: flex; align-items: center; gap: 4px; }
|
|
152
|
+
.composer[data-compact] .composerActions { justify-content: flex-end; }
|
|
153
|
+
.send,
|
|
154
|
+
.ghost { height: 38px; padding: 0 14px; border: 0; border-radius: var(--radius-full); corner-shape: round; cursor: pointer; font: inherit; font-weight: 500; transition: background-color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
155
|
+
.composer[data-compact] :is(.send, .ghost) { height: 32px; font-size: var(--text-xs); }
|
|
156
|
+
.send { background: var(--foreground); color: var(--background); }
|
|
157
|
+
.send:disabled { opacity: .35; cursor: default; }
|
|
158
|
+
.send:active:not(:disabled), .ghost:active { transform: scale(.97); }
|
|
159
|
+
.ghost { background: transparent; color: var(--text-secondary); }
|
|
160
|
+
|
|
161
|
+
.suggestions { display: grid; margin: 6px 0 0; padding: 4px; overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); list-style: none; }
|
|
162
|
+
.suggestion { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 8px; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); cursor: pointer; }
|
|
163
|
+
.suggestion[aria-selected="true"] { background: var(--surface-muted); }
|
|
164
|
+
|
|
165
|
+
/* Resolved chip */
|
|
166
|
+
.resolved { display: flex; align-items: center; gap: 12px; padding: 10px 10px 10px 14px; }
|
|
167
|
+
.resolvedIcon { display: grid; width: 24px; height: 24px; flex: none; place-items: center; border-radius: 50%; corner-shape: round; background: color-mix(in oklab, var(--success) 14%, transparent); color: var(--success); }
|
|
168
|
+
.resolvedText { display: grid; min-width: 0; flex: 1; }
|
|
169
|
+
.resolvedTitle { overflow: hidden; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
170
|
+
.resolvedSubject { color: var(--text-secondary); font-weight: 400; }
|
|
171
|
+
.resolvedMeta { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
172
|
+
|
|
173
|
+
@media (hover: hover) and (pointer: fine) {
|
|
174
|
+
.comment .actions:not([data-open]) { opacity: 0; }
|
|
175
|
+
.comment:hover .actions,
|
|
176
|
+
.comment:focus-within .actions { opacity: 1; }
|
|
177
|
+
.action:hover, .iconAction:hover, .toggle:hover, .ghost:hover { background: var(--surface-muted); color: var(--foreground); }
|
|
178
|
+
.action[data-tone="danger"]:hover { background: color-mix(in oklab, var(--danger) 9%, transparent); color: var(--danger); }
|
|
179
|
+
.reaction:hover { border-color: var(--border-strong); }
|
|
180
|
+
.emoji:hover { background: var(--surface-muted); }
|
|
181
|
+
.resolve:hover:not(:disabled), .reopen:hover { border-color: var(--border-strong); background: var(--surface-muted); }
|
|
182
|
+
.send:hover:not(:disabled) { background: color-mix(in oklab, var(--foreground) 86%, var(--background)); }
|
|
183
|
+
}
|
|
184
|
+
.action:focus-visible, .iconAction:focus-visible, .toggle:focus-visible, .emoji:focus-visible { background: var(--surface-muted); color: var(--foreground); }
|
|
185
|
+
|
|
186
|
+
@media (max-width: 420px) {
|
|
187
|
+
.composer { grid-template-columns: minmax(0, 1fr) auto; }
|
|
188
|
+
.composer > .avatar { display: none; }
|
|
189
|
+
.replyingInner { padding-left: 2px; }
|
|
190
|
+
.replies { margin-left: 12px; }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@media (prefers-reduced-motion: reduce) {
|
|
194
|
+
.root, .actions, .reaction, .action, .iconAction, .emoji, .send, .ghost, .resolve, .reopen { transition: none; }
|
|
195
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode, Ref } from "react";
|
|
2
|
+
/** Where the control is in its life: resting, asking, working, finished, or failed. */
|
|
3
|
+
export type ConfirmMorphState = "idle" | "confirming" | "pending" | "done" | "error";
|
|
4
|
+
/** How the question is answered: a press, a press held until the fill completes, or a press followed by a short countdown. */
|
|
5
|
+
export type ConfirmMorphMode = "press" | "hold" | "countdown";
|
|
6
|
+
/**
|
|
7
|
+
* A button for destructive or important actions that asks in place. Pressing it morphs the same pill into an inline question
|
|
8
|
+
* with Cancel and Confirm, then into a spinner, then into a result with Undo. The width springs to each face while the faces
|
|
9
|
+
* crossfade at their natural width, so text never squashes and nothing around the control jumps. Escape, an outside press,
|
|
10
|
+
* or the timeout all return it to rest.
|
|
11
|
+
* Use it where a modal dialog would be heavy: deleting a project, revoking access, discarding a draft.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConfirmMorphProps {
|
|
14
|
+
/** The resting label, such as "Delete project". */
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
/** A plain icon before the resting label. */
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
/** The question shown while confirming, such as "Delete 3 files?". Defaults to the label with a question mark. */
|
|
19
|
+
prompt?: ReactNode;
|
|
20
|
+
confirmLabel?: string;
|
|
21
|
+
cancelLabel?: string;
|
|
22
|
+
/** Shown beside the spinner while `onConfirm` resolves. */
|
|
23
|
+
pendingLabel?: string;
|
|
24
|
+
doneLabel?: string;
|
|
25
|
+
errorLabel?: string;
|
|
26
|
+
retryLabel?: string;
|
|
27
|
+
undoLabel?: string;
|
|
28
|
+
/** Shown beside the spinner while `onUndo` resolves. */
|
|
29
|
+
undoingLabel?: string;
|
|
30
|
+
/**
|
|
31
|
+
* `danger` (default) gives the confirm button the danger colour, for actions that destroy something. `neutral` gives it the
|
|
32
|
+
* accent, for important actions that can be reversed. The resting button is neutral either way.
|
|
33
|
+
*/
|
|
34
|
+
tone?: "danger" | "neutral";
|
|
35
|
+
/** `press` confirms on a press. `hold` asks for a press held for `holdDuration`. `countdown` confirms, then counts down `countdown` seconds that Cancel or Escape can still stop. */
|
|
36
|
+
confirmMode?: ConfirmMorphMode;
|
|
37
|
+
/** Milliseconds the confirm button must be held in `hold` mode. */
|
|
38
|
+
holdDuration?: number;
|
|
39
|
+
/** Seconds counted down before the action runs in `countdown` mode. */
|
|
40
|
+
countdown?: number;
|
|
41
|
+
/** Text before the seconds in `countdown` mode, such as "Deleting in". Defaults to the pending label followed by "in". */
|
|
42
|
+
countdownLabel?: string;
|
|
43
|
+
/** Runs on confirm. Return a promise to show the pending face; a rejection shows the error face with Retry. */
|
|
44
|
+
onConfirm?: () => void | Promise<unknown>;
|
|
45
|
+
/** Offering it adds Undo to the result. Return a promise to show a pending face while it runs. */
|
|
46
|
+
onUndo?: () => void | Promise<unknown>;
|
|
47
|
+
onCancel?: () => void;
|
|
48
|
+
/** Controlled state. Pair it with `onStateChange`. */
|
|
49
|
+
state?: ConfirmMorphState;
|
|
50
|
+
/** Starting state when uncontrolled. */
|
|
51
|
+
defaultState?: ConfirmMorphState;
|
|
52
|
+
onStateChange?: (state: ConfirmMorphState) => void;
|
|
53
|
+
/** Milliseconds before an unanswered question returns to rest. Resting the pointer on the control pauses it. 0 turns it off. */
|
|
54
|
+
confirmTimeout?: number;
|
|
55
|
+
/** Milliseconds a result stays before returning to rest. Resting the pointer on the control pauses it. 0 turns it off. */
|
|
56
|
+
resultTimeout?: number;
|
|
57
|
+
/** A press outside the control cancels an open question. Defaults to true. */
|
|
58
|
+
cancelOnOutsidePress?: boolean;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
className?: string;
|
|
61
|
+
/** Receives the root element, which also takes focus while the action is pending. */
|
|
62
|
+
ref?: Ref<HTMLDivElement>;
|
|
63
|
+
}
|
|
64
|
+
export declare function ConfirmMorph({ label, icon, prompt, confirmLabel, cancelLabel, pendingLabel, doneLabel, errorLabel, retryLabel, undoLabel, undoingLabel, tone, confirmMode, holdDuration, countdown, countdownLabel, onConfirm, onUndo, onCancel, state: stateProp, defaultState, onStateChange, confirmTimeout, resultTimeout, cancelOnOutsidePress, disabled, className, ref, }: ConfirmMorphProps): import("react").JSX.Element;
|
|
65
|
+
export default ConfirmMorph;
|