@ctrl-ui/react 0.8.6 → 0.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-bar.d.ts +15 -7
- package/dist/action-bar.js +39 -19
- package/dist/activity.js +7 -15
- package/dist/audio-recorder.d.ts +14 -4
- package/dist/audio-recorder.js +82 -29
- package/dist/audio-visualizer.js +1 -1
- package/dist/blocks/coding-agent.js +54 -57
- package/dist/blocks/design-canvas-data.d.ts +32 -0
- package/dist/blocks/design-canvas-data.js +100 -0
- package/dist/blocks/design-canvas-inspector.d.ts +8 -0
- package/dist/blocks/design-canvas-inspector.js +238 -0
- package/dist/blocks/design-canvas.d.ts +21 -0
- package/dist/blocks/design-canvas.js +452 -0
- package/dist/blocks/discussion.d.ts +28 -0
- package/dist/blocks/discussion.js +67 -0
- package/dist/blocks/file-explorer-data.d.ts +1 -0
- package/dist/blocks/file-explorer-data.js +4 -1
- package/dist/blocks/file-explorer.d.ts +9 -1
- package/dist/blocks/file-explorer.js +461 -192
- package/dist/blocks/settings.d.ts +2 -1
- package/dist/blocks/settings.js +101 -79
- package/dist/blocks/team-chat.d.ts +12 -0
- package/dist/blocks/team-chat.js +26 -0
- package/dist/blocks/theme-toggle.js +23 -31
- package/dist/chat-composer-attachment.d.ts +12 -3
- package/dist/chat-composer-attachment.js +61 -19
- package/dist/chat-composer-editor/extensions/mention.d.ts +5 -1
- package/dist/chat-composer-editor/extensions/mention.js +58 -27
- package/dist/chat-composer-editor/ghost.js +2 -4
- package/dist/chat-composer-editor/types.d.ts +5 -0
- package/dist/chat-composer-editor.d.ts +1 -1
- package/dist/chat-composer-editor.js +71 -28
- package/dist/chat-composer.d.ts +13 -32
- package/dist/chat-composer.js +54 -13
- package/dist/chat-layout.d.ts +19 -0
- package/dist/chat-layout.js +96 -32
- package/dist/chat-message/social.d.ts +26 -0
- package/dist/chat-message/social.js +122 -0
- package/dist/chat-message.d.ts +28 -29
- package/dist/chat-message.js +106 -25
- package/dist/context.d.ts +2 -2
- package/dist/context.js +38 -23
- package/dist/dynamic-notification-canvas-loop.d.ts +24 -0
- package/dist/dynamic-notification-canvas-loop.js +113 -0
- package/dist/dynamic-notification-glass.js +23 -114
- package/dist/dynamic-notification-liquid.js +14 -118
- package/dist/dynamic-notification.d.ts +16 -8
- package/dist/dynamic-notification.js +85 -22
- package/dist/email/email-brand.d.ts +7 -4
- package/dist/email/email-brand.js +9 -6
- package/dist/email/email-code.d.ts +3 -2
- package/dist/email/email-code.js +11 -9
- package/dist/email/email-footer.d.ts +5 -3
- package/dist/email/email-footer.js +11 -5
- package/dist/email/email.d.ts +31 -9
- package/dist/email/email.js +50 -26
- package/dist/email/templates.d.ts +10 -9
- package/dist/email/templates.js +102 -38
- package/dist/email/theme.js +1 -1
- package/dist/environment-variables.d.ts +4 -2
- package/dist/environment-variables.js +52 -32
- package/dist/filter-bar/context.d.ts +32 -0
- package/dist/filter-bar/context.js +11 -0
- package/dist/filter-bar/editor.d.ts +14 -0
- package/dist/filter-bar/editor.js +216 -0
- package/dist/filter-bar/model.d.ts +10 -0
- package/dist/filter-bar/model.js +57 -0
- package/dist/filter-bar/motion.d.ts +10 -0
- package/dist/filter-bar/motion.js +84 -0
- package/dist/filter-bar/parts.d.ts +19 -0
- package/dist/filter-bar/parts.js +244 -0
- package/dist/filter-bar/types.d.ts +54 -0
- package/dist/filter-bar/types.js +0 -0
- package/dist/filter-bar.d.ts +27 -0
- package/dist/filter-bar.js +310 -0
- package/dist/hooks/use-audio-recorder.js +12 -6
- package/dist/hooks/use-chat-composer.d.ts +21 -7
- package/dist/hooks/use-chat-composer.js +32 -5
- package/dist/hooks/use-chat-message.d.ts +28 -2
- package/dist/hooks/use-chat-message.js +41 -10
- package/dist/hooks/use-chat-thread-scroll.js +43 -7
- package/dist/hooks/use-color-area.d.ts +3 -3
- package/dist/hooks/use-color-area.js +13 -12
- package/dist/hooks/use-copy-to-clipboard.d.ts +2 -1
- package/dist/hooks/use-copy-to-clipboard.js +20 -20
- package/dist/hooks/use-dropzone.d.ts +7 -4
- package/dist/hooks/use-dropzone.js +34 -3
- package/dist/hooks/use-dynamic-notification.d.ts +6 -2
- package/dist/hooks/use-dynamic-notification.js +26 -10
- package/dist/hooks/use-mobile.d.ts +3 -2
- package/dist/hooks/use-mobile.js +8 -4
- package/dist/hooks/use-textarea-trigger-menu.js +2 -1
- package/dist/hooks/use-trigger-menu.d.ts +54 -0
- package/dist/hooks/use-trigger-menu.js +102 -47
- package/dist/hooks/use-user-ask.d.ts +2 -0
- package/dist/hooks/use-user-ask.js +35 -41
- package/dist/inline-attachment.d.ts +6 -2
- package/dist/inline-attachment.js +44 -29
- package/dist/inline-citation.d.ts +12 -5
- package/dist/inline-citation.js +25 -16
- package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
- package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
- package/dist/knob-contracts/audio-visualizer-knobs.d.ts +1 -1
- package/dist/knob-contracts/audio-visualizer-knobs.js +2 -1
- package/dist/knob-contracts/avatar-knobs.d.ts +1 -1
- package/dist/knob-contracts/avatar-knobs.js +6 -1
- package/dist/knob-contracts/badge-knobs.d.ts +1 -1
- package/dist/knob-contracts/badge-knobs.js +2 -1
- package/dist/knob-contracts/breadcrumb-knobs.d.ts +4 -0
- package/dist/knob-contracts/breadcrumb-knobs.js +8 -0
- package/dist/knob-contracts/button-knobs.d.ts +1 -1
- package/dist/knob-contracts/button-knobs.js +1 -0
- package/dist/knob-contracts/chat-layout-knobs.d.ts +1 -1
- package/dist/knob-contracts/chat-layout-knobs.js +2 -1
- package/dist/knob-contracts/chat-message-knobs.d.ts +1 -1
- package/dist/knob-contracts/chat-message-knobs.js +8 -1
- package/dist/knob-contracts/choice-knobs.d.ts +1 -1
- package/dist/knob-contracts/choice-knobs.js +1 -0
- package/dist/knob-contracts/code-diff-knobs.d.ts +1 -1
- package/dist/knob-contracts/code-diff-knobs.js +2 -0
- package/dist/knob-contracts/code-knobs.d.ts +1 -1
- package/dist/knob-contracts/code-knobs.js +2 -1
- package/dist/knob-contracts/collapsible-knobs.d.ts +1 -1
- package/dist/knob-contracts/collapsible-knobs.js +1 -1
- package/dist/knob-contracts/dynamic-notification-knobs.d.ts +1 -1
- package/dist/knob-contracts/dynamic-notification-knobs.js +2 -0
- package/dist/knob-contracts/emoji-picker-knobs.d.ts +4 -0
- package/dist/knob-contracts/emoji-picker-knobs.js +10 -0
- package/dist/knob-contracts/field-knobs.d.ts +1 -1
- package/dist/knob-contracts/field-knobs.js +5 -1
- package/dist/knob-contracts/filter-bar-knobs.d.ts +4 -0
- package/dist/knob-contracts/filter-bar-knobs.js +15 -0
- package/dist/knob-contracts/infinite-canvas-knobs.d.ts +1 -1
- package/dist/knob-contracts/infinite-canvas-knobs.js +3 -0
- package/dist/knob-contracts/markdown-knobs.d.ts +1 -1
- package/dist/knob-contracts/markdown-knobs.js +3 -1
- package/dist/knob-contracts/page-layout-knobs.d.ts +1 -1
- package/dist/knob-contracts/page-layout-knobs.js +4 -1
- package/dist/knob-contracts/popup-knobs.d.ts +1 -1
- package/dist/knob-contracts/popup-knobs.js +28 -0
- package/dist/knob-contracts/range-knobs.d.ts +1 -1
- package/dist/knob-contracts/range-knobs.js +1 -0
- package/dist/knob-contracts/rich-tooltip-knobs.d.ts +1 -1
- package/dist/knob-contracts/rich-tooltip-knobs.js +2 -1
- package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
- package/dist/knob-contracts/sidebar-knobs.js +27 -2
- package/dist/knob-contracts/spinner-knobs.d.ts +1 -1
- package/dist/knob-contracts/spinner-knobs.js +1 -1
- package/dist/knob-contracts/stepper-knobs.d.ts +1 -1
- package/dist/knob-contracts/stepper-knobs.js +4 -1
- package/dist/knob-contracts/switch-knobs.d.ts +1 -1
- package/dist/knob-contracts/switch-knobs.js +5 -0
- package/dist/knob-contracts/table-of-contents-knobs.d.ts +1 -1
- package/dist/knob-contracts/table-of-contents-knobs.js +4 -5
- package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
- package/dist/knob-contracts/tabs-knobs.js +1 -0
- package/dist/knob-contracts/tree-knobs.d.ts +1 -1
- package/dist/knob-contracts/tree-knobs.js +2 -1
- package/dist/lib/cn.js +0 -4
- package/dist/lib/contrast.d.ts +1 -0
- package/dist/lib/contrast.js +17 -5
- package/dist/lib/dropzone-validation.js +41 -28
- package/dist/lib/env-file.d.ts +1 -1
- package/dist/lib/env-file.js +5 -5
- package/dist/lib/motion.d.ts +4 -0
- package/dist/lib/motion.js +15 -0
- package/dist/lib/trigger-detect.js +6 -8
- package/dist/markdown-block.js +13 -8
- package/dist/popup-parts.d.ts +8 -0
- package/dist/popup-parts.js +43 -0
- package/dist/scripts/contrast-eval.js +5 -4
- package/dist/source-badge.js +3 -1
- package/dist/styles/code.css +0 -2
- package/dist/styles/effects.css +6 -32
- package/dist/styles/index.css +14 -8
- package/dist/styles/recipes/accordion.css +9 -6
- package/dist/styles/recipes/action-bar.css +0 -15
- package/dist/styles/recipes/activity.css +5 -11
- package/dist/styles/recipes/alert.css +4 -4
- package/dist/styles/recipes/audio-recorder.css +15 -3
- package/dist/styles/recipes/audio-visualizer.css +13 -2
- package/dist/styles/recipes/avatar.css +78 -1
- package/dist/styles/recipes/badge.css +14 -4
- package/dist/styles/recipes/breadcrumb.css +77 -0
- package/dist/styles/recipes/button-group.css +6 -8
- package/dist/styles/recipes/button-states.css +66 -0
- package/dist/styles/recipes/button.css +26 -85
- package/dist/styles/recipes/calendar.css +17 -23
- package/dist/styles/recipes/card.css +2 -2
- package/dist/styles/recipes/chat-composer-attachment.css +9 -3
- package/dist/styles/recipes/chat-composer.css +21 -16
- package/dist/styles/recipes/chat-layout.css +64 -2
- package/dist/styles/recipes/chat-message-social.css +123 -0
- package/dist/styles/recipes/chat-message.css +157 -37
- package/dist/styles/recipes/choice.css +25 -21
- package/dist/styles/recipes/code-diff.css +51 -26
- package/dist/styles/recipes/code.css +44 -18
- package/dist/styles/recipes/collapsible.css +43 -1
- package/dist/styles/recipes/color-picker-details.css +3 -4
- package/dist/styles/recipes/color-picker-values.css +9 -8
- package/dist/styles/recipes/color-picker.css +23 -4
- package/dist/styles/recipes/context.css +3 -3
- package/dist/styles/recipes/dockable-panel.css +18 -7
- package/dist/styles/recipes/dropzone.css +7 -8
- package/dist/styles/recipes/dynamic-notification-input.css +8 -5
- package/dist/styles/recipes/dynamic-notification-motion.css +12 -21
- package/dist/styles/recipes/dynamic-notification.css +40 -26
- package/dist/styles/recipes/emoji-picker.css +132 -0
- package/dist/styles/recipes/empty.css +5 -4
- package/dist/styles/recipes/environment-variables.css +4 -15
- package/dist/styles/recipes/field-parts.css +89 -21
- package/dist/styles/recipes/field.css +83 -26
- package/dist/styles/recipes/filter-bar.css +161 -0
- package/dist/styles/recipes/gradient-editor.css +18 -7
- package/dist/styles/recipes/infinite-canvas.css +51 -4
- package/dist/styles/recipes/inline-attachment.css +9 -4
- package/dist/styles/recipes/inline-citation.css +10 -4
- package/dist/styles/recipes/item.css +23 -9
- package/dist/styles/recipes/markdown-block.css +5 -4
- package/dist/styles/recipes/markdown.css +68 -26
- package/dist/styles/recipes/morphing-panel.css +8 -8
- package/dist/styles/recipes/page-layout.css +36 -10
- package/dist/styles/recipes/pagination.css +5 -6
- package/dist/styles/recipes/phone-input.css +12 -8
- package/dist/styles/recipes/popup-controls.css +175 -26
- package/dist/styles/recipes/popup-drawer.css +35 -44
- package/dist/styles/recipes/popup-lists.css +139 -29
- package/dist/styles/recipes/popup-motion.css +4 -45
- package/dist/styles/recipes/popup-toast.css +55 -26
- package/dist/styles/recipes/popup.css +149 -153
- package/dist/styles/recipes/range.css +55 -19
- package/dist/styles/recipes/resizable.css +36 -7
- package/dist/styles/recipes/rich-tooltip.css +24 -34
- package/dist/styles/recipes/scroll-area.css +8 -1
- package/dist/styles/recipes/sidebar-menu.css +149 -34
- package/dist/styles/recipes/sidebar-parts.css +154 -0
- package/dist/styles/recipes/sidebar.css +187 -128
- package/dist/styles/recipes/source-badge.css +1 -1
- package/dist/styles/recipes/spinner.css +12 -1
- package/dist/styles/recipes/stepper.css +31 -13
- package/dist/styles/recipes/switch.css +68 -23
- package/dist/styles/recipes/table-of-contents.css +49 -41
- package/dist/styles/recipes/table.css +14 -10
- package/dist/styles/recipes/tabs.css +38 -12
- package/dist/styles/recipes/task-list.css +7 -14
- package/dist/styles/recipes/thread-rail.css +42 -19
- package/dist/styles/recipes/timeline.css +2 -0
- package/dist/styles/recipes/toolbar.css +27 -25
- package/dist/styles/recipes/track-highlight.css +12 -8
- package/dist/styles/recipes/transcript-divider.css +2 -2
- package/dist/styles/recipes/tree.css +31 -4
- package/dist/styles/recipes/user-ask-details.css +0 -1
- package/dist/styles/recipes/user-ask.css +4 -4
- package/dist/styles/theme.css +287 -107
- package/dist/task-list.d.ts +4 -1
- package/dist/task-list.js +24 -8
- package/dist/thread-rail.d.ts +1 -1
- package/dist/thread-rail.js +61 -19
- package/dist/transcript-divider.js +2 -1
- package/dist/ui/alert.d.ts +3 -1
- package/dist/ui/alert.js +14 -8
- package/dist/ui/autocomplete.js +7 -1
- package/dist/ui/avatar.d.ts +8 -0
- package/dist/ui/avatar.js +23 -3
- package/dist/ui/badge.js +1 -1
- package/dist/ui/breadcrumb.d.ts +27 -0
- package/dist/ui/breadcrumb.js +88 -0
- package/dist/ui/button.d.ts +2 -1
- package/dist/ui/button.js +1 -1
- package/dist/ui/calendar.js +2 -2
- package/dist/ui/card.d.ts +3 -1
- package/dist/ui/card.js +12 -7
- package/dist/ui/checkbox-group.js +1 -1
- package/dist/ui/checkbox.d.ts +2 -0
- package/dist/ui/code-diff.d.ts +1 -1
- package/dist/ui/code-diff.js +71 -27
- package/dist/ui/code.d.ts +5 -3
- package/dist/ui/code.js +27 -28
- package/dist/ui/color-picker.d.ts +5 -2
- package/dist/ui/color-picker.js +84 -36
- package/dist/ui/combobox-disabled-selection.d.ts +1 -2
- package/dist/ui/combobox-disabled-selection.js +7 -8
- package/dist/ui/combobox.d.ts +5 -23
- package/dist/ui/combobox.js +36 -16
- package/dist/ui/command.d.ts +8 -2
- package/dist/ui/command.js +39 -15
- package/dist/ui/context-menu.js +3 -0
- package/dist/ui/dialog.d.ts +2 -1
- package/dist/ui/dialog.js +3 -22
- package/dist/ui/dockable-panel-geometry.d.ts +1 -0
- package/dist/ui/dockable-panel-geometry.js +7 -1
- package/dist/ui/dockable-panel.d.ts +9 -6
- package/dist/ui/dockable-panel.js +81 -33
- package/dist/ui/drawer.d.ts +7 -4
- package/dist/ui/drawer.js +35 -22
- package/dist/ui/dropdown-menu.js +4 -3
- package/dist/ui/dropzone.d.ts +5 -3
- package/dist/ui/dropzone.js +78 -17
- package/dist/ui/emoji-picker.d.ts +18 -0
- package/dist/ui/emoji-picker.js +136 -0
- package/dist/ui/empty.d.ts +3 -1
- package/dist/ui/empty.js +12 -7
- package/dist/ui/field.js +1 -1
- package/dist/ui/gradient-editor.d.ts +1 -1
- package/dist/ui/gradient-editor.js +37 -21
- package/dist/ui/infinite-canvas.d.ts +13 -1
- package/dist/ui/infinite-canvas.js +139 -20
- package/dist/ui/input-group.d.ts +2 -4
- package/dist/ui/input-group.js +13 -14
- package/dist/ui/input-otp.d.ts +1 -1
- package/dist/ui/input-otp.js +32 -15
- package/dist/ui/input.js +11 -2
- package/dist/ui/item.d.ts +2 -1
- package/dist/ui/item.js +11 -7
- package/dist/ui/live-status.d.ts +7 -0
- package/dist/ui/live-status.js +17 -0
- package/dist/ui/markdown-elements.d.ts +6 -0
- package/dist/ui/markdown-elements.js +42 -9
- package/dist/ui/menubar.js +2 -2
- package/dist/ui/morphing-panel.js +2 -1
- package/dist/ui/native-select.d.ts +1 -1
- package/dist/ui/native-select.js +24 -11
- package/dist/ui/number-field.d.ts +1 -1
- package/dist/ui/number-field.js +13 -2
- package/dist/ui/page-layout.d.ts +2 -2
- package/dist/ui/page-layout.js +2 -4
- package/dist/ui/pagination.d.ts +7 -5
- package/dist/ui/pagination.js +19 -13
- package/dist/ui/phone-input.d.ts +1 -0
- package/dist/ui/phone-input.js +56 -58
- package/dist/ui/radio-group.d.ts +3 -0
- package/dist/ui/radio-group.js +11 -1
- package/dist/ui/resizable.d.ts +20 -2
- package/dist/ui/resizable.js +130 -8
- package/dist/ui/responsive-dialog.d.ts +1 -1
- package/dist/ui/responsive-dialog.js +9 -23
- package/dist/ui/rich-tooltip.d.ts +10 -4
- package/dist/ui/rich-tooltip.js +81 -33
- package/dist/ui/scroll-area.d.ts +4 -1
- package/dist/ui/scroll-area.js +44 -28
- package/dist/ui/select.d.ts +2 -1
- package/dist/ui/select.js +57 -32
- package/dist/ui/sidebar-menu.d.ts +5 -1
- package/dist/ui/sidebar-menu.js +31 -33
- package/dist/ui/sidebar-provider.d.ts +10 -1
- package/dist/ui/sidebar-provider.js +38 -13
- package/dist/ui/sidebar-resize-rail.js +7 -3
- package/dist/ui/sidebar.d.ts +11 -6
- package/dist/ui/sidebar.js +46 -23
- package/dist/ui/slider.js +31 -22
- package/dist/ui/spinner.d.ts +3 -2
- package/dist/ui/spinner.js +3 -2
- package/dist/ui/stepper.d.ts +8 -1
- package/dist/ui/stepper.js +74 -25
- package/dist/ui/switch.d.ts +1 -0
- package/dist/ui/table-of-contents.d.ts +5 -4
- package/dist/ui/table-of-contents.js +143 -52
- package/dist/ui/table.js +15 -9
- package/dist/ui/textarea.d.ts +1 -1
- package/dist/ui/textarea.js +20 -9
- package/dist/ui/timeline.d.ts +3 -1
- package/dist/ui/timeline.js +17 -9
- package/dist/ui/toast.d.ts +11 -5
- package/dist/ui/toast.js +100 -54
- package/dist/ui/toggle.d.ts +1 -1
- package/dist/ui/toggle.js +2 -1
- package/dist/ui/toolbar.d.ts +3 -2
- package/dist/ui/toolbar.js +4 -2
- package/dist/ui/tooltip.js +3 -20
- package/dist/ui/tree.js +120 -80
- package/dist/ui/trigger-menu.js +7 -2
- package/dist/user-ask.d.ts +10 -5
- package/dist/user-ask.js +31 -11
- package/package.json +5 -1
- package/dist/hooks/use-keyboard-navigation.d.ts +0 -3
- package/dist/hooks/use-keyboard-navigation.js +0 -22
|
@@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
|
4
4
|
//#region src/hooks/use-dropzone.ts
|
|
5
5
|
const registeredDropzoneRoots = /* @__PURE__ */ new WeakSet();
|
|
6
6
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
7
|
-
function useDropzone({ value: controlledValue, defaultValue = [], onValueChange, policy, disabled = false, drag = true, preventDropOnDocument = true, trackDocumentDrag = false, getFilesFromEvent, onDrop, onError } = {}) {
|
|
7
|
+
function useDropzone({ value: controlledValue, defaultValue = [], onValueChange, policy, disabled = false, drag = true, paste = true, preventDropOnDocument = true, trackDocumentDrag = false, getFilesFromEvent, onDrop, onError } = {}) {
|
|
8
8
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
9
9
|
const [fileRejections, setFileRejections] = useState([]);
|
|
10
10
|
const [isDragActive, setIsDragActive] = useState(false);
|
|
@@ -25,12 +25,15 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
25
25
|
const disabledRef = useRef(disabled);
|
|
26
26
|
const dragEnabled = drag && !disabled;
|
|
27
27
|
const dragEnabledRef = useRef(dragEnabled);
|
|
28
|
+
const pasteEnabled = paste && !disabled;
|
|
29
|
+
const pasteEnabledRef = useRef(pasteEnabled);
|
|
28
30
|
const policyRef = useRef(policy);
|
|
29
31
|
const customExtractionRef = useRef(Boolean(getFilesFromEvent));
|
|
30
32
|
useIsomorphicLayoutEffect(() => {
|
|
31
33
|
currentValueRef.current = currentValue;
|
|
32
34
|
disabledRef.current = disabled;
|
|
33
35
|
dragEnabledRef.current = dragEnabled;
|
|
36
|
+
pasteEnabledRef.current = pasteEnabled;
|
|
34
37
|
policyRef.current = policy;
|
|
35
38
|
customExtractionRef.current = Boolean(getFilesFromEvent);
|
|
36
39
|
});
|
|
@@ -136,6 +139,10 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
136
139
|
removedFiles
|
|
137
140
|
});
|
|
138
141
|
}
|
|
142
|
+
function removeRejection(file) {
|
|
143
|
+
if (disabledRef.current || processingRef.current) return;
|
|
144
|
+
setFileRejections((current) => current.filter((rejection) => rejection.file !== file));
|
|
145
|
+
}
|
|
139
146
|
function clearRejections() {
|
|
140
147
|
if (disabledRef.current || processingRef.current || fileRejections.length === 0) return;
|
|
141
148
|
setFileRejections([]);
|
|
@@ -153,7 +160,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
153
160
|
if (fileRejections.length > 0) setFileRejections([]);
|
|
154
161
|
}
|
|
155
162
|
function getRootProps(props = {}) {
|
|
156
|
-
const { onDragEnter, onDragOver, onDragLeave, onDragEnd, onDrop: callerOnDrop, ref, ...rootProps } = props;
|
|
163
|
+
const { onDragEnter, onDragOver, onDragLeave, onDragEnd, onDrop: callerOnDrop, onPaste, ref, ...rootProps } = props;
|
|
157
164
|
return {
|
|
158
165
|
...rootProps,
|
|
159
166
|
ref: (node) => {
|
|
@@ -207,6 +214,14 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
207
214
|
if (event.defaultPrevented || !dragEnabledRef.current) return;
|
|
208
215
|
event.preventDefault();
|
|
209
216
|
intake(event, "drop");
|
|
217
|
+
},
|
|
218
|
+
onPaste: (event) => {
|
|
219
|
+
onPaste?.(event);
|
|
220
|
+
if (!isFilePaste(event.clipboardData)) return;
|
|
221
|
+
event.stopPropagation();
|
|
222
|
+
if (event.defaultPrevented || !pasteEnabledRef.current) return;
|
|
223
|
+
event.preventDefault();
|
|
224
|
+
intake(event, "paste");
|
|
210
225
|
}
|
|
211
226
|
};
|
|
212
227
|
}
|
|
@@ -326,6 +341,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
326
341
|
multiple: policy?.multiple ?? true,
|
|
327
342
|
disabled,
|
|
328
343
|
dragEnabled,
|
|
344
|
+
pasteEnabled,
|
|
329
345
|
visualState,
|
|
330
346
|
isDragActive,
|
|
331
347
|
isDragAccept: visualState === "accept",
|
|
@@ -339,6 +355,7 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
339
355
|
open,
|
|
340
356
|
removeFile,
|
|
341
357
|
clearFiles,
|
|
358
|
+
removeRejection,
|
|
342
359
|
clearRejections,
|
|
343
360
|
reset,
|
|
344
361
|
getRootProps,
|
|
@@ -346,11 +363,25 @@ function useDropzone({ value: controlledValue, defaultValue = [], onValueChange,
|
|
|
346
363
|
};
|
|
347
364
|
}
|
|
348
365
|
function defaultGetFilesFromEvent(event) {
|
|
349
|
-
|
|
366
|
+
if ("clipboardData" in event) return getClipboardFiles(event.clipboardData);
|
|
367
|
+
if ("dataTransfer" in event) return Array.from(event.dataTransfer.files);
|
|
368
|
+
return Array.from(event.currentTarget.files ?? []);
|
|
369
|
+
}
|
|
370
|
+
/** Safari exposes a pasted screenshot through `items` only, so `files` alone loses it. */
|
|
371
|
+
function getClipboardFiles(clipboardData) {
|
|
372
|
+
const files = Array.from(clipboardData.files);
|
|
373
|
+
if (files.length > 0) return files;
|
|
374
|
+
return Array.from(clipboardData.items).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((file) => file !== null);
|
|
350
375
|
}
|
|
351
376
|
function isFileDrag(dataTransfer) {
|
|
352
377
|
return dataTransfer ? Array.from(dataTransfer.types).includes("Files") : false;
|
|
353
378
|
}
|
|
379
|
+
/** Clipboards that also carry text belong to the focused editor: only a text-free paste is an attachment. */
|
|
380
|
+
function isFilePaste(clipboardData) {
|
|
381
|
+
if (!clipboardData) return false;
|
|
382
|
+
const types = Array.from(clipboardData.types);
|
|
383
|
+
return types.includes("Files") && !types.includes("text/plain");
|
|
384
|
+
}
|
|
354
385
|
function getRegisteredDropzoneRoot(event) {
|
|
355
386
|
for (const target of event.composedPath()) if (target && registeredDropzoneRoots.has(target)) return target;
|
|
356
387
|
}
|
|
@@ -18,18 +18,22 @@ export type DynamicNotificationProps = Omit<ComponentProps<"div">, "onChange" |
|
|
|
18
18
|
onReply?: (payload: DynamicNotificationReplyPayload) => void | Promise<void>;
|
|
19
19
|
variant?: DynamicNotificationVariant;
|
|
20
20
|
disabled?: boolean;
|
|
21
|
+
thinkingLabel?: string;
|
|
22
|
+
replyErrorLabel?: string;
|
|
21
23
|
style?: CSSProperties & DynamicNotificationKnobStyle;
|
|
22
24
|
};
|
|
23
25
|
export type DynamicNotificationController = {
|
|
24
26
|
open: boolean;
|
|
25
27
|
disabled: boolean;
|
|
26
|
-
setOpen: (nextOpen: boolean, reason: OpenChangeReason, event: Event, trigger?: Element) =>
|
|
28
|
+
setOpen: (nextOpen: boolean, reason: OpenChangeReason, event: Event, trigger?: Element) => boolean;
|
|
27
29
|
reply: string;
|
|
28
30
|
setReply: (nextValue: string) => void;
|
|
29
31
|
normalizedReply: string;
|
|
30
32
|
canSubmit: boolean;
|
|
33
|
+
replyPending: boolean;
|
|
34
|
+
replyFailed: boolean;
|
|
31
35
|
clear: () => void;
|
|
32
|
-
submitReply: () => void
|
|
36
|
+
submitReply: () => Promise<void>;
|
|
33
37
|
handleReplySubmit: (event: FormSubmitEvent) => void;
|
|
34
38
|
};
|
|
35
39
|
export declare function useDynamicNotification({ open, defaultOpen, onOpenChange, replyValue, defaultReplyValue, onReplyValueChange, onReply, disabled }: Pick<DynamicNotificationProps, "open" | "defaultOpen" | "onOpenChange" | "replyValue" | "defaultReplyValue" | "onReplyValueChange" | "onReply" | "disabled">): DynamicNotificationController;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useMemo, useRef, useState } from "react";
|
|
2
2
|
//#region src/hooks/use-dynamic-notification.ts
|
|
3
3
|
function createOpenChangeEventDetails(reason, event, trigger) {
|
|
4
4
|
let canceled = false;
|
|
@@ -39,13 +39,17 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
|
|
|
39
39
|
onValueChange: onReplyValueChange
|
|
40
40
|
});
|
|
41
41
|
const normalizedReply = reply.trim();
|
|
42
|
-
const
|
|
42
|
+
const [replyPending, setReplyPending] = useState(false);
|
|
43
|
+
const [replyFailed, setReplyFailed] = useState(false);
|
|
44
|
+
const replyInFlight = useRef(false);
|
|
45
|
+
const canSubmit = normalizedReply.length > 0 && !disabled && !replyPending;
|
|
43
46
|
const setOpen = useMemo(() => (nextOpen, reason, event, trigger) => {
|
|
44
|
-
if (disabled || nextOpen === isOpen) return;
|
|
47
|
+
if (disabled || nextOpen === isOpen) return false;
|
|
45
48
|
const details = createOpenChangeEventDetails(reason, event, trigger);
|
|
46
49
|
onOpenChange?.(nextOpen, details);
|
|
47
|
-
if (details.isCanceled) return;
|
|
50
|
+
if (details.isCanceled) return false;
|
|
48
51
|
if (!isControlled) setInternalOpen(nextOpen);
|
|
52
|
+
return true;
|
|
49
53
|
}, [
|
|
50
54
|
disabled,
|
|
51
55
|
isControlled,
|
|
@@ -55,12 +59,22 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
|
|
|
55
59
|
const clear = useMemo(() => () => {
|
|
56
60
|
setReply("");
|
|
57
61
|
}, [setReply]);
|
|
58
|
-
const submitReply = useMemo(() => () => {
|
|
59
|
-
if (!canSubmit) return;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const submitReply = useMemo(() => async () => {
|
|
63
|
+
if (!canSubmit || replyInFlight.current) return;
|
|
64
|
+
replyInFlight.current = true;
|
|
65
|
+
setReplyPending(true);
|
|
66
|
+
setReplyFailed(false);
|
|
67
|
+
try {
|
|
68
|
+
await onReply?.({
|
|
69
|
+
value: normalizedReply,
|
|
70
|
+
clear
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
setReplyFailed(true);
|
|
74
|
+
} finally {
|
|
75
|
+
replyInFlight.current = false;
|
|
76
|
+
setReplyPending(false);
|
|
77
|
+
}
|
|
64
78
|
}, [
|
|
65
79
|
canSubmit,
|
|
66
80
|
clear,
|
|
@@ -75,6 +89,8 @@ function useDynamicNotification({ open, defaultOpen = false, onOpenChange, reply
|
|
|
75
89
|
setReply,
|
|
76
90
|
normalizedReply,
|
|
77
91
|
canSubmit,
|
|
92
|
+
replyPending,
|
|
93
|
+
replyFailed,
|
|
78
94
|
clear,
|
|
79
95
|
submitReply,
|
|
80
96
|
handleReplySubmit: useMemo(() => (event) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/hooks/use-mobile.d.ts
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
type Breakpoint = number | `--${string}`;
|
|
3
|
+
/** True below `breakpoint`: px (Tailwind `md` by default) or a theme breakpoint token such as `--breakpoint-lg`, so JS and the matching Tailwind variant share one value. */
|
|
4
|
+
export declare function useIsMobile(breakpoint?: Breakpoint): boolean;
|
|
4
5
|
//#endregion
|
package/dist/hooks/use-mobile.js
CHANGED
|
@@ -5,14 +5,18 @@ const viewportStores = /* @__PURE__ */ new Map();
|
|
|
5
5
|
function viewportStore(breakpoint) {
|
|
6
6
|
const cached = viewportStores.get(breakpoint);
|
|
7
7
|
if (cached) return cached;
|
|
8
|
-
|
|
8
|
+
let resolvedQuery;
|
|
9
|
+
function mobileQuery() {
|
|
10
|
+
resolvedQuery ??= typeof breakpoint === "number" ? `(width < ${breakpoint}px)` : `(width < ${getComputedStyle(document.documentElement).getPropertyValue(breakpoint).trim()})`;
|
|
11
|
+
return resolvedQuery;
|
|
12
|
+
}
|
|
9
13
|
const store = {
|
|
10
14
|
subscribe(onChange) {
|
|
11
|
-
const query = window.matchMedia(mobileQuery);
|
|
15
|
+
const query = window.matchMedia(mobileQuery());
|
|
12
16
|
query.addEventListener("change", onChange);
|
|
13
17
|
return () => query.removeEventListener("change", onChange);
|
|
14
18
|
},
|
|
15
|
-
getSnapshot: () => window.matchMedia(mobileQuery).matches
|
|
19
|
+
getSnapshot: () => window.matchMedia(mobileQuery()).matches
|
|
16
20
|
};
|
|
17
21
|
viewportStores.set(breakpoint, store);
|
|
18
22
|
return store;
|
|
@@ -20,7 +24,7 @@ function viewportStore(breakpoint) {
|
|
|
20
24
|
function getServerViewportSnapshot() {
|
|
21
25
|
return false;
|
|
22
26
|
}
|
|
23
|
-
/** True below `breakpoint
|
|
27
|
+
/** True below `breakpoint`: px (Tailwind `md` by default) or a theme breakpoint token such as `--breakpoint-lg`, so JS and the matching Tailwind variant share one value. */
|
|
24
28
|
function useIsMobile(breakpoint = MOBILE_BREAKPOINT) {
|
|
25
29
|
const store = viewportStore(breakpoint);
|
|
26
30
|
return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerViewportSnapshot);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { isComposingKey, useTriggerMenu } from "./use-trigger-menu.js";
|
|
2
3
|
import { caretRectInTextarea, detectTrigger } from "../lib/trigger-detect.js";
|
|
3
|
-
import { useTriggerMenu } from "./use-trigger-menu.js";
|
|
4
4
|
import { useEffect, useEffectEvent } from "react";
|
|
5
5
|
//#region src/hooks/use-textarea-trigger-menu.ts
|
|
6
6
|
function replaceRange(element, start, end, text) {
|
|
@@ -36,6 +36,7 @@ function useTextareaTriggerMenu(ref, options) {
|
|
|
36
36
|
controller.report(match, match === null ? null : caretRectInTextarea(element, match.start));
|
|
37
37
|
});
|
|
38
38
|
const handleKeyDown = useEffectEvent((event) => {
|
|
39
|
+
if (isComposingKey(event)) return;
|
|
39
40
|
if (controller.open && controller.handleKeyDown(event.key)) event.preventDefault();
|
|
40
41
|
});
|
|
41
42
|
const closeAfterBlur = useEffectEvent(() => {
|
|
@@ -27,6 +27,16 @@ export type UseTriggerMenuOptions<Item extends TriggerMenuItemData> = {
|
|
|
27
27
|
triggers: readonly TriggerConfig<Item>[];
|
|
28
28
|
onCommit: (item: Item, trigger: TriggerConfig<Item>, match: TriggerMatch) => void;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Popup attributes for the element that keeps focus (textarea or editor) while the menu is up. It stays a
|
|
32
|
+
* multiline textbox (a <textarea> can't take role="combobox"), so open state is conveyed by `aria-controls`.
|
|
33
|
+
*/
|
|
34
|
+
export type TriggerMenuInputAria = {
|
|
35
|
+
"aria-autocomplete": "list";
|
|
36
|
+
"aria-haspopup": "listbox";
|
|
37
|
+
"aria-controls"?: string;
|
|
38
|
+
"aria-activedescendant"?: string;
|
|
39
|
+
};
|
|
30
40
|
export type TriggerMenuController<Item extends TriggerMenuItemData> = {
|
|
31
41
|
open: boolean;
|
|
32
42
|
items: readonly Item[];
|
|
@@ -34,6 +44,11 @@ export type TriggerMenuController<Item extends TriggerMenuItemData> = {
|
|
|
34
44
|
anchorRect: DOMRect | null;
|
|
35
45
|
activeChar: string | null;
|
|
36
46
|
query: string;
|
|
47
|
+
/** Id for `TriggerMenuList`; the input's `aria-controls` points at it. */
|
|
48
|
+
listId: string;
|
|
49
|
+
/** Id for the option at `index`; the input's `aria-activedescendant` points at the active one. */
|
|
50
|
+
optionId: (index: number) => string;
|
|
51
|
+
inputAria: TriggerMenuInputAria;
|
|
37
52
|
/** Binding → engine: publish current trigger match (or null) and caret rect to anchor to. */
|
|
38
53
|
report: (match: TriggerMatch | null, anchorRect: DOMRect | null) => void;
|
|
39
54
|
/** Binding → engine: route keydown while open. Returns true when engine consumed key. */
|
|
@@ -44,5 +59,44 @@ export type TriggerMenuController<Item extends TriggerMenuItemData> = {
|
|
|
44
59
|
close: () => void;
|
|
45
60
|
setOpen: (open: boolean) => void;
|
|
46
61
|
};
|
|
62
|
+
type DismissedMatch = {
|
|
63
|
+
char: string;
|
|
64
|
+
start: number;
|
|
65
|
+
query: string;
|
|
66
|
+
};
|
|
67
|
+
export type TriggerMenuState = {
|
|
68
|
+
open: boolean;
|
|
69
|
+
char: string | null;
|
|
70
|
+
query: string;
|
|
71
|
+
anchorRect: DOMRect | null;
|
|
72
|
+
activeIndex: number;
|
|
73
|
+
dismissed: DismissedMatch | null;
|
|
74
|
+
};
|
|
75
|
+
export type TriggerMenuAction = {
|
|
76
|
+
type: "report";
|
|
77
|
+
match: TriggerMatch | null;
|
|
78
|
+
anchorRect: DOMRect | null;
|
|
79
|
+
} | {
|
|
80
|
+
type: "close";
|
|
81
|
+
dismissed: DismissedMatch | null;
|
|
82
|
+
} | {
|
|
83
|
+
type: "move";
|
|
84
|
+
delta: number;
|
|
85
|
+
count: number;
|
|
86
|
+
} | {
|
|
87
|
+
type: "activate";
|
|
88
|
+
index: number;
|
|
89
|
+
};
|
|
90
|
+
export declare const CLOSED_TRIGGER_MENU: TriggerMenuState;
|
|
91
|
+
/** A dismissed token stays closed until its query changes, so keyup/click re-detection after Escape can't reopen it. */
|
|
92
|
+
export declare function triggerMenuReducer(state: TriggerMenuState, action: TriggerMenuAction): TriggerMenuState;
|
|
93
|
+
/** Keys an IME is still composing must never reach the menu or the composer's submit. */
|
|
94
|
+
export declare function isComposingKey(event: {
|
|
95
|
+
isComposing: boolean;
|
|
96
|
+
keyCode: number;
|
|
97
|
+
}): boolean;
|
|
98
|
+
export type TriggerMenuKeyIntent = "next" | "previous" | "commit" | "dismiss" | "pass";
|
|
99
|
+
/** What a key means to an open menu; an empty list passes Enter/Tab through so newline, submit and Tab-out still work. */
|
|
100
|
+
export declare function triggerMenuKeyIntent(key: string, open: boolean, itemCount: number): TriggerMenuKeyIntent;
|
|
47
101
|
export declare function useTriggerMenu<Item extends TriggerMenuItemData>({ triggers, onCommit }: UseTriggerMenuOptions<Item>): TriggerMenuController<Item>;
|
|
48
102
|
//#endregion
|
|
@@ -1,12 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useId, useReducer, useRef } from "react";
|
|
2
2
|
//#region src/hooks/use-trigger-menu.ts
|
|
3
|
-
const
|
|
3
|
+
const CLOSED_TRIGGER_MENU = {
|
|
4
4
|
open: false,
|
|
5
5
|
char: null,
|
|
6
6
|
query: "",
|
|
7
7
|
anchorRect: null,
|
|
8
|
-
activeIndex: 0
|
|
8
|
+
activeIndex: 0,
|
|
9
|
+
dismissed: null
|
|
9
10
|
};
|
|
11
|
+
/** A dismissed token stays closed until its query changes, so keyup/click re-detection after Escape can't reopen it. */
|
|
12
|
+
function triggerMenuReducer(state, action) {
|
|
13
|
+
switch (action.type) {
|
|
14
|
+
case "report": {
|
|
15
|
+
const { match, anchorRect } = action;
|
|
16
|
+
if (match === null) return { ...CLOSED_TRIGGER_MENU };
|
|
17
|
+
const { dismissed } = state;
|
|
18
|
+
if (dismissed !== null && dismissed.char === match.char && dismissed.start === match.start && dismissed.query === match.query) return {
|
|
19
|
+
...CLOSED_TRIGGER_MENU,
|
|
20
|
+
dismissed
|
|
21
|
+
};
|
|
22
|
+
const activeIndex = state.open && state.char === match.char && state.query === match.query ? state.activeIndex : 0;
|
|
23
|
+
return {
|
|
24
|
+
open: true,
|
|
25
|
+
char: match.char,
|
|
26
|
+
query: match.query,
|
|
27
|
+
anchorRect,
|
|
28
|
+
activeIndex,
|
|
29
|
+
dismissed: null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
case "close": return {
|
|
33
|
+
...CLOSED_TRIGGER_MENU,
|
|
34
|
+
dismissed: action.dismissed
|
|
35
|
+
};
|
|
36
|
+
case "move": return {
|
|
37
|
+
...state,
|
|
38
|
+
activeIndex: action.count === 0 ? 0 : (state.activeIndex + action.delta + action.count) % action.count
|
|
39
|
+
};
|
|
40
|
+
case "activate": return {
|
|
41
|
+
...state,
|
|
42
|
+
activeIndex: action.index
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
10
46
|
function matchesQuery(item, query) {
|
|
11
47
|
if (query === "") return true;
|
|
12
48
|
const needle = query.toLowerCase();
|
|
@@ -18,14 +54,45 @@ function resolveItems(trigger, query) {
|
|
|
18
54
|
const filter = trigger.filter ?? matchesQuery;
|
|
19
55
|
return trigger.items.filter((item) => filter(item, query));
|
|
20
56
|
}
|
|
57
|
+
/** Keys an IME is still composing must never reach the menu or the composer's submit. */
|
|
58
|
+
function isComposingKey(event) {
|
|
59
|
+
return event.isComposing || event.keyCode === 229;
|
|
60
|
+
}
|
|
61
|
+
/** What a key means to an open menu; an empty list passes Enter/Tab through so newline, submit and Tab-out still work. */
|
|
62
|
+
function triggerMenuKeyIntent(key, open, itemCount) {
|
|
63
|
+
if (!open) return "pass";
|
|
64
|
+
if (key === "ArrowDown") return "next";
|
|
65
|
+
if (key === "ArrowUp") return "previous";
|
|
66
|
+
if (key === "Escape") return "dismiss";
|
|
67
|
+
if ((key === "Enter" || key === "Tab") && itemCount > 0) return "commit";
|
|
68
|
+
return "pass";
|
|
69
|
+
}
|
|
21
70
|
function useTriggerMenu({ triggers, onCommit }) {
|
|
22
|
-
const [state,
|
|
71
|
+
const [state, dispatch] = useReducer(triggerMenuReducer, CLOSED_TRIGGER_MENU);
|
|
23
72
|
const lastMatch = useRef(null);
|
|
73
|
+
const listId = useId();
|
|
24
74
|
const activeTrigger = state.char === null ? null : triggers.find((trigger) => trigger.char === state.char) ?? null;
|
|
25
75
|
const items = activeTrigger === null ? [] : resolveItems(activeTrigger, state.query);
|
|
76
|
+
const optionId = (index) => `${listId}-option-${index}`;
|
|
77
|
+
const hasActiveItem = state.open && state.activeIndex < items.length;
|
|
78
|
+
function dismiss() {
|
|
79
|
+
const match = lastMatch.current;
|
|
80
|
+
lastMatch.current = null;
|
|
81
|
+
dispatch({
|
|
82
|
+
type: "close",
|
|
83
|
+
dismissed: match === null ? null : {
|
|
84
|
+
char: match.char,
|
|
85
|
+
start: match.start,
|
|
86
|
+
query: match.query
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
26
90
|
function close() {
|
|
27
91
|
lastMatch.current = null;
|
|
28
|
-
|
|
92
|
+
dispatch({
|
|
93
|
+
type: "close",
|
|
94
|
+
dismissed: null
|
|
95
|
+
});
|
|
29
96
|
}
|
|
30
97
|
function select(item) {
|
|
31
98
|
const match = lastMatch.current;
|
|
@@ -33,55 +100,35 @@ function useTriggerMenu({ triggers, onCommit }) {
|
|
|
33
100
|
close();
|
|
34
101
|
}
|
|
35
102
|
function report(match, anchorRect) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
open: true,
|
|
43
|
-
char: match.char,
|
|
44
|
-
query: match.query,
|
|
45
|
-
anchorRect,
|
|
46
|
-
activeIndex
|
|
47
|
-
};
|
|
103
|
+
const known = match !== null && triggers.some((trigger) => trigger.char === match.char);
|
|
104
|
+
lastMatch.current = known ? match : null;
|
|
105
|
+
dispatch({
|
|
106
|
+
type: "report",
|
|
107
|
+
match: known ? match : null,
|
|
108
|
+
anchorRect
|
|
48
109
|
});
|
|
49
110
|
}
|
|
50
111
|
function handleKeyDown(key) {
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
if (key === "ArrowUp") {
|
|
60
|
-
setState((current) => ({
|
|
61
|
-
...current,
|
|
62
|
-
activeIndex: items.length === 0 ? 0 : (current.activeIndex - 1 + items.length) % items.length
|
|
63
|
-
}));
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (key === "Enter" || key === "Tab") {
|
|
112
|
+
const intent = triggerMenuKeyIntent(key, state.open, items.length);
|
|
113
|
+
if (intent === "next" || intent === "previous") dispatch({
|
|
114
|
+
type: "move",
|
|
115
|
+
delta: intent === "next" ? 1 : -1,
|
|
116
|
+
count: items.length
|
|
117
|
+
});
|
|
118
|
+
else if (intent === "commit") {
|
|
67
119
|
const item = items[state.activeIndex];
|
|
68
120
|
if (item && !item.disabled) select(item);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (key === "Escape") {
|
|
72
|
-
close();
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
121
|
+
} else if (intent === "dismiss") dismiss();
|
|
122
|
+
return intent !== "pass";
|
|
76
123
|
}
|
|
77
124
|
function setOpen(open) {
|
|
78
|
-
if (!open)
|
|
125
|
+
if (!open) dismiss();
|
|
79
126
|
}
|
|
80
|
-
function setActiveIndex(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
127
|
+
function setActiveIndex(index) {
|
|
128
|
+
dispatch({
|
|
129
|
+
type: "activate",
|
|
130
|
+
index
|
|
131
|
+
});
|
|
85
132
|
}
|
|
86
133
|
return {
|
|
87
134
|
open: state.open,
|
|
@@ -90,6 +137,14 @@ function useTriggerMenu({ triggers, onCommit }) {
|
|
|
90
137
|
anchorRect: state.anchorRect,
|
|
91
138
|
activeChar: state.char,
|
|
92
139
|
query: state.query,
|
|
140
|
+
listId,
|
|
141
|
+
optionId,
|
|
142
|
+
inputAria: {
|
|
143
|
+
"aria-autocomplete": "list",
|
|
144
|
+
"aria-haspopup": "listbox",
|
|
145
|
+
"aria-controls": state.open ? listId : void 0,
|
|
146
|
+
"aria-activedescendant": hasActiveItem ? optionId(state.activeIndex) : void 0
|
|
147
|
+
},
|
|
93
148
|
report,
|
|
94
149
|
handleKeyDown,
|
|
95
150
|
setActiveIndex,
|
|
@@ -99,4 +154,4 @@ function useTriggerMenu({ triggers, onCommit }) {
|
|
|
99
154
|
};
|
|
100
155
|
}
|
|
101
156
|
//#endregion
|
|
102
|
-
export { useTriggerMenu };
|
|
157
|
+
export { CLOSED_TRIGGER_MENU, isComposingKey, triggerMenuKeyIntent, triggerMenuReducer, useTriggerMenu };
|
|
@@ -6,6 +6,7 @@ export type UserAskProps = Omit<ComponentProps<"section">, "style"> & {
|
|
|
6
6
|
children?: ReactNode;
|
|
7
7
|
onComplete?: (answers: UserAskAnswers) => void;
|
|
8
8
|
onDismiss?: () => void;
|
|
9
|
+
questionStatusLabel?: (current: number, total: number, title: string) => string;
|
|
9
10
|
style?: CSSProperties & UserAskKnobStyle;
|
|
10
11
|
};
|
|
11
12
|
export type UserAskQuestionEntry = {
|
|
@@ -36,6 +37,7 @@ export declare function useUserAsk({ onComplete, onDismiss }: Pick<UserAskProps,
|
|
|
36
37
|
allowsMultiple: (questionId: string) => boolean;
|
|
37
38
|
selectionFor: (questionId: string) => string;
|
|
38
39
|
isSelected: (questionId: string, optionKey: string) => boolean;
|
|
40
|
+
isTabStop: (questionId: string, optionKey: string) => boolean;
|
|
39
41
|
select: (questionId: string, optionKey: string) => void;
|
|
40
42
|
freeformTextFor: (questionId: string) => string;
|
|
41
43
|
setFreeformText: (questionId: string, text: string) => void;
|
|
@@ -8,6 +8,12 @@ function upsert(entries, entry) {
|
|
|
8
8
|
next[index] = entry;
|
|
9
9
|
return next;
|
|
10
10
|
}
|
|
11
|
+
const arrowDeltas = {
|
|
12
|
+
ArrowDown: 1,
|
|
13
|
+
ArrowRight: 1,
|
|
14
|
+
ArrowUp: -1,
|
|
15
|
+
ArrowLeft: -1
|
|
16
|
+
};
|
|
11
17
|
function useUserAsk({ onComplete, onDismiss }) {
|
|
12
18
|
const [questions, setQuestions] = useState([]);
|
|
13
19
|
const [optionsByQuestion, setOptionsByQuestion] = useState({});
|
|
@@ -114,21 +120,11 @@ function useUserAsk({ onComplete, onDismiss }) {
|
|
|
114
120
|
function dismiss() {
|
|
115
121
|
onDismiss?.();
|
|
116
122
|
}
|
|
117
|
-
function
|
|
118
|
-
if (
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
const currentIndex = options.findIndex((option) => option.key === currentKey);
|
|
123
|
-
let start = currentIndex;
|
|
124
|
-
if (currentIndex === -1) start = delta > 0 ? -1 : 0;
|
|
125
|
-
for (let step = 1; step <= options.length; step += 1) {
|
|
126
|
-
const candidate = options[(start + delta * step + options.length * step) % options.length];
|
|
127
|
-
if (candidate && !candidate.disabled) {
|
|
128
|
-
select(activeQuestion.id, candidate.key);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
123
|
+
function isTabStop(questionId, optionKey) {
|
|
124
|
+
if (allowsMultiple(questionId)) return true;
|
|
125
|
+
const selectedKey = selectionFor(questionId);
|
|
126
|
+
if (selectedKey !== void 0) return selectedKey === optionKey;
|
|
127
|
+
return optionsFor(questionId).find((option) => !option.disabled)?.key === optionKey;
|
|
132
128
|
}
|
|
133
129
|
function selectDigit(digit) {
|
|
134
130
|
if (!activeQuestion) return;
|
|
@@ -137,39 +133,28 @@ function useUserAsk({ onComplete, onDismiss }) {
|
|
|
137
133
|
}
|
|
138
134
|
function moveActiveOption(event, delta) {
|
|
139
135
|
if (!activeQuestion) return;
|
|
140
|
-
if (!allowsMultiple(activeQuestion.id)) {
|
|
141
|
-
moveSelection(delta);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
136
|
const rows = [...event.currentTarget.querySelector("[data-slot=\"question\"][data-active]")?.querySelectorAll("[data-slot=\"option\"]:not([disabled])") ?? []];
|
|
145
137
|
if (rows.length === 0) return;
|
|
146
138
|
const focused = rows.findIndex((candidate) => candidate.contains(document.activeElement));
|
|
147
139
|
let start = focused;
|
|
148
140
|
if (focused === -1) start = delta > 0 ? -1 : 0;
|
|
149
141
|
const row = rows[(start + delta + rows.length) % rows.length];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
const target = row?.matches("button, input") ? row : row?.querySelector("input, button");
|
|
143
|
+
target?.focus();
|
|
144
|
+
if (!allowsMultiple(activeQuestion.id) && target?.matches("button")) target.click();
|
|
145
|
+
}
|
|
146
|
+
function handleEnter(event) {
|
|
147
|
+
if (event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
148
|
+
if (event.target instanceof Element && event.target.closest("button:not([role=\"radio\"]):not([role=\"checkbox\"]), a[href]")) return;
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
advance();
|
|
151
|
+
}
|
|
152
|
+
function handleOptionKey(event) {
|
|
153
|
+
if (event.target instanceof Element && event.target.closest("input, textarea, [contenteditable]")) return;
|
|
154
|
+
const delta = arrowDeltas[event.key];
|
|
155
|
+
if (delta !== void 0) {
|
|
160
156
|
event.preventDefault();
|
|
161
|
-
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (event.target instanceof HTMLElement && event.target.closest("input, textarea, [contenteditable]")) return;
|
|
165
|
-
if (event.key === "ArrowDown") {
|
|
166
|
-
event.preventDefault();
|
|
167
|
-
moveActiveOption(event, 1);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (event.key === "ArrowUp") {
|
|
171
|
-
event.preventDefault();
|
|
172
|
-
moveActiveOption(event, -1);
|
|
157
|
+
moveActiveOption(event, delta);
|
|
173
158
|
return;
|
|
174
159
|
}
|
|
175
160
|
if (/^[1-9]$/.test(event.key)) {
|
|
@@ -177,6 +162,14 @@ function useUserAsk({ onComplete, onDismiss }) {
|
|
|
177
162
|
selectDigit(Number(event.key));
|
|
178
163
|
}
|
|
179
164
|
}
|
|
165
|
+
function handleKeyDown(event) {
|
|
166
|
+
if (event.defaultPrevented) return;
|
|
167
|
+
if (event.key === "Escape") {
|
|
168
|
+
event.preventDefault();
|
|
169
|
+
dismiss();
|
|
170
|
+
} else if (event.key === "Enter") handleEnter(event);
|
|
171
|
+
else handleOptionKey(event);
|
|
172
|
+
}
|
|
180
173
|
return {
|
|
181
174
|
questions,
|
|
182
175
|
activeIndex,
|
|
@@ -191,6 +184,7 @@ function useUserAsk({ onComplete, onDismiss }) {
|
|
|
191
184
|
allowsMultiple,
|
|
192
185
|
selectionFor,
|
|
193
186
|
isSelected,
|
|
187
|
+
isTabStop,
|
|
194
188
|
select,
|
|
195
189
|
freeformTextFor,
|
|
196
190
|
setFreeformText,
|