@ctrl-ui/react 0.8.7 → 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 +40 -20
- package/dist/activity.js +8 -16
- 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 +39 -39
- package/dist/blocks/design-canvas-inspector.js +2 -2
- package/dist/blocks/design-canvas.d.ts +5 -1
- package/dist/blocks/design-canvas.js +200 -94
- 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 +102 -80
- 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 +4 -1
- package/dist/chat-composer-attachment.js +35 -10
- 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 +57 -30
- package/dist/chat-composer.d.ts +13 -32
- package/dist/chat-composer.js +53 -13
- package/dist/chat-layout.d.ts +19 -0
- package/dist/chat-layout.js +91 -40
- 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 +39 -24
- 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 +86 -23
- 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/editor.js +20 -8
- package/dist/filter-bar/model.d.ts +1 -0
- package/dist/filter-bar/model.js +10 -1
- package/dist/filter-bar/motion.js +4 -4
- package/dist/filter-bar/parts.js +20 -9
- package/dist/filter-bar/types.d.ts +1 -0
- package/dist/filter-bar.js +20 -14
- package/dist/hooks/use-audio-recorder.js +12 -6
- package/dist/hooks/use-chat-composer.d.ts +19 -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-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 +36 -15
- package/dist/inline-citation.d.ts +12 -5
- package/dist/inline-citation.js +26 -17
- 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-message-knobs.d.ts +1 -1
- package/dist/knob-contracts/chat-message-knobs.js +8 -1
- 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/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 +1 -1
- package/dist/knob-contracts/filter-bar-knobs.js +1 -2
- 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 +2 -1
- package/dist/knob-contracts/popup-knobs.d.ts +1 -1
- package/dist/knob-contracts/popup-knobs.js +27 -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 +2 -1
- 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/phone-input-format.js +1 -1
- package/dist/lib/trigger-detect.js +6 -8
- package/dist/markdown-block.js +2 -1
- package/dist/popup-parts.d.ts +8 -0
- package/dist/popup-parts.js +43 -0
- 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 +10 -5
- 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 +5 -7
- package/dist/styles/recipes/button-states.css +66 -0
- package/dist/styles/recipes/button.css +24 -83
- 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 +7 -10
- package/dist/styles/recipes/chat-layout.css +35 -0
- 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 +18 -21
- package/dist/styles/recipes/code-diff.css +46 -33
- package/dist/styles/recipes/code.css +36 -16
- package/dist/styles/recipes/collapsible.css +7 -2
- 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 +11 -2
- package/dist/styles/recipes/context.css +3 -3
- package/dist/styles/recipes/dockable-panel.css +7 -4
- 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 +30 -24
- package/dist/styles/recipes/field.css +61 -26
- package/dist/styles/recipes/filter-bar.css +18 -31
- package/dist/styles/recipes/gradient-editor.css +18 -7
- package/dist/styles/recipes/infinite-canvas.css +0 -1
- package/dist/styles/recipes/inline-attachment.css +3 -3
- 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 +20 -12
- package/dist/styles/recipes/pagination.css +4 -5
- package/dist/styles/recipes/phone-input.css +12 -8
- package/dist/styles/recipes/popup-controls.css +166 -33
- package/dist/styles/recipes/popup-drawer.css +3 -14
- package/dist/styles/recipes/popup-lists.css +128 -27
- package/dist/styles/recipes/popup-motion.css +4 -45
- package/dist/styles/recipes/popup-toast.css +55 -26
- package/dist/styles/recipes/popup.css +139 -141
- package/dist/styles/recipes/range.css +54 -18
- package/dist/styles/recipes/resizable.css +2 -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 +8 -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 +9 -1
- package/dist/styles/recipes/table.css +14 -10
- package/dist/styles/recipes/tabs.css +17 -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 +10 -7
- 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 +37 -16
- 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-dialog.js +1 -1
- package/dist/ui/alert.d.ts +3 -1
- package/dist/ui/alert.js +14 -8
- package/dist/ui/autocomplete.js +4 -2
- 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 +3 -3
- 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.d.ts +2 -0
- package/dist/ui/code-diff.d.ts +1 -1
- package/dist/ui/code-diff.js +72 -28
- package/dist/ui/code.d.ts +5 -3
- package/dist/ui/code.js +28 -29
- package/dist/ui/collapsible.js +1 -1
- package/dist/ui/color-picker.d.ts +5 -2
- package/dist/ui/color-picker.js +85 -37
- package/dist/ui/combobox-disabled-selection.d.ts +1 -2
- package/dist/ui/combobox-disabled-selection.js +7 -9
- package/dist/ui/combobox.js +14 -6
- package/dist/ui/command.d.ts +8 -2
- package/dist/ui/command.js +30 -14
- package/dist/ui/context-menu.js +4 -1
- package/dist/ui/dialog.d.ts +2 -1
- package/dist/ui/dialog.js +4 -23
- 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 +63 -23
- package/dist/ui/drawer.js +1 -1
- package/dist/ui/dropdown-menu.js +5 -4
- package/dist/ui/dropzone.d.ts +2 -2
- package/dist/ui/dropzone.js +75 -16
- 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/gradient-editor.d.ts +1 -1
- package/dist/ui/gradient-editor.js +37 -21
- package/dist/ui/hover-card.js +1 -1
- package/dist/ui/infinite-canvas.d.ts +3 -2
- package/dist/ui/infinite-canvas.js +63 -19
- package/dist/ui/input-group.d.ts +0 -3
- package/dist/ui/input-group.js +4 -14
- package/dist/ui/input-otp.d.ts +1 -1
- package/dist/ui/input-otp.js +32 -15
- package/dist/ui/input.js +4 -4
- package/dist/ui/item.d.ts +2 -1
- package/dist/ui/item.js +12 -8
- 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 +31 -1
- package/dist/ui/menubar.js +3 -3
- package/dist/ui/morphing-panel.js +3 -2
- package/dist/ui/native-select.d.ts +1 -1
- package/dist/ui/native-select.js +24 -11
- package/dist/ui/navigation-menu.js +1 -1
- 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 +58 -60
- package/dist/ui/popover.js +1 -1
- package/dist/ui/radio-group.d.ts +2 -0
- package/dist/ui/resizable.d.ts +3 -2
- package/dist/ui/resizable.js +5 -3
- package/dist/ui/responsive-dialog.d.ts +1 -1
- package/dist/ui/responsive-dialog.js +4 -21
- package/dist/ui/rich-tooltip.d.ts +10 -4
- package/dist/ui/rich-tooltip.js +82 -34
- package/dist/ui/scroll-area.d.ts +1 -1
- package/dist/ui/scroll-area.js +43 -28
- package/dist/ui/select.js +1 -1
- package/dist/ui/sheet.js +1 -1
- package/dist/ui/sidebar-menu.d.ts +5 -1
- package/dist/ui/sidebar-menu.js +32 -34
- package/dist/ui/sidebar-provider.d.ts +10 -1
- package/dist/ui/sidebar-provider.js +39 -14
- package/dist/ui/sidebar-resize-rail.js +8 -4
- package/dist/ui/sidebar.d.ts +11 -6
- package/dist/ui/sidebar.js +47 -24
- 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 +2 -2
- package/dist/ui/table-of-contents.js +20 -8
- package/dist/ui/table.js +4 -2
- 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 +101 -55
- package/dist/ui/toolbar.d.ts +3 -2
- package/dist/ui/toolbar.js +4 -2
- package/dist/ui/tooltip.js +4 -21
- package/dist/ui/tree.js +121 -81
- package/dist/ui/trigger-menu.js +2 -1
- package/dist/user-ask.d.ts +10 -5
- package/dist/user-ask.js +32 -12
- package/package.json +5 -1
- package/dist/hooks/use-keyboard-navigation.d.ts +0 -3
- package/dist/hooks/use-keyboard-navigation.js +0 -22
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FormSubmitEvent } from "../control-props.js";
|
|
2
|
-
import { ChatComposerKnobStyle } from "../knob-contracts/chat-composer-knobs.js";
|
|
3
2
|
import { ChatDensity } from "./use-chat-message.js";
|
|
3
|
+
import { ChatComposerKnobStyle } from "../knob-contracts/chat-composer-knobs.js";
|
|
4
4
|
import { CSSProperties, ComponentProps, ReactNode } from "react";
|
|
5
5
|
//#region src/hooks/use-chat-composer.d.ts
|
|
6
6
|
export type ChatComposerSubmitPayload = {
|
|
@@ -8,6 +8,7 @@ export type ChatComposerSubmitPayload = {
|
|
|
8
8
|
clear: () => void;
|
|
9
9
|
mentions?: MentionItem[];
|
|
10
10
|
};
|
|
11
|
+
export type ChatComposerSubmitKey = "enter" | "mod-enter";
|
|
11
12
|
export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit" | "style"> & {
|
|
12
13
|
children?: ReactNode;
|
|
13
14
|
value?: string;
|
|
@@ -19,6 +20,7 @@ export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit" | "style
|
|
|
19
20
|
disabled?: boolean;
|
|
20
21
|
/** Let an empty message send, for composers where an attachment or recording carries the turn. */
|
|
21
22
|
allowEmptySubmit?: boolean;
|
|
23
|
+
submitKey?: ChatComposerSubmitKey;
|
|
22
24
|
style?: CSSProperties & ChatComposerKnobStyle;
|
|
23
25
|
};
|
|
24
26
|
export type MentionItem = {
|
|
@@ -29,23 +31,27 @@ export type MentionItem = {
|
|
|
29
31
|
type ComposerKeyEvent = {
|
|
30
32
|
key: string;
|
|
31
33
|
shiftKey: boolean;
|
|
34
|
+
metaKey: boolean;
|
|
35
|
+
ctrlKey: boolean;
|
|
32
36
|
defaultPrevented: boolean;
|
|
33
37
|
nativeEvent: {
|
|
34
38
|
isComposing: boolean;
|
|
39
|
+
keyCode: number;
|
|
35
40
|
};
|
|
36
41
|
preventDefault: () => void;
|
|
37
42
|
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}): {
|
|
43
|
+
type InsertText = (text: string) => void;
|
|
44
|
+
export type RegisterInsertionTarget = (insert: InsertText) => () => void;
|
|
45
|
+
export type ChatComposerController = {
|
|
42
46
|
value: string;
|
|
43
|
-
setValue: (
|
|
47
|
+
setValue: (value: string) => void;
|
|
44
48
|
normalizedValue: string;
|
|
45
|
-
state: "
|
|
49
|
+
state: NonNullable<ChatComposerProps["state"]>;
|
|
46
50
|
density: ChatDensity;
|
|
51
|
+
submitKey: ChatComposerSubmitKey;
|
|
47
52
|
isCompact: boolean;
|
|
48
53
|
isDisabled: boolean;
|
|
54
|
+
isLocked: boolean;
|
|
49
55
|
canSubmit: boolean;
|
|
50
56
|
rows: number;
|
|
51
57
|
sendCount: number;
|
|
@@ -53,5 +59,11 @@ export declare function useChatComposer({ value, defaultValue, onValueChange, on
|
|
|
53
59
|
submit: (extra?: Partial<ChatComposerSubmitPayload>) => void;
|
|
54
60
|
handleSubmit: (event: FormSubmitEvent) => void;
|
|
55
61
|
handleKeyDown: (event: ComposerKeyEvent) => void;
|
|
62
|
+
insertText: InsertText;
|
|
63
|
+
registerInsertionTarget: RegisterInsertionTarget;
|
|
56
64
|
};
|
|
65
|
+
export declare function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state, density, disabled, allowEmptySubmit, submitKey, trackSends }: Pick<ChatComposerProps, "value" | "defaultValue" | "onValueChange" | "onSubmit" | "state" | "density" | "disabled" | "allowEmptySubmit" | "submitKey"> & {
|
|
66
|
+
/** Count successful submits — only enabled when something reads counter (send-layer anchor), so idle apps pay no extra state update. */
|
|
67
|
+
trackSends?: boolean;
|
|
68
|
+
}): ChatComposerController;
|
|
57
69
|
//#endregion
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isComposingKey } from "./use-trigger-menu.js";
|
|
1
2
|
import { useState } from "react";
|
|
2
3
|
//#region src/hooks/use-chat-composer.ts
|
|
3
4
|
function useControllableText({ value, defaultValue = "", onValueChange }) {
|
|
@@ -10,16 +11,30 @@ function useControllableText({ value, defaultValue = "", onValueChange }) {
|
|
|
10
11
|
}
|
|
11
12
|
return [currentValue, setValue];
|
|
12
13
|
}
|
|
13
|
-
function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, allowEmptySubmit = false, trackSends = false }) {
|
|
14
|
+
function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state = "idle", density = "comfortable", disabled = false, allowEmptySubmit = false, submitKey = "enter", trackSends = false }) {
|
|
14
15
|
const [inputValue, setInputValue] = useControllableText({
|
|
15
16
|
value,
|
|
16
17
|
defaultValue,
|
|
17
18
|
onValueChange
|
|
18
19
|
});
|
|
19
20
|
const [sendCount, setSendCount] = useState(0);
|
|
21
|
+
const [insertion] = useState(() => {
|
|
22
|
+
let target = null;
|
|
23
|
+
const register = (insert) => {
|
|
24
|
+
target = insert;
|
|
25
|
+
return () => {
|
|
26
|
+
if (target === insert) target = null;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
register,
|
|
31
|
+
current: () => target
|
|
32
|
+
};
|
|
33
|
+
});
|
|
20
34
|
const normalizedValue = inputValue.trim();
|
|
21
|
-
const isDisabled = disabled || state === "disabled"
|
|
22
|
-
const
|
|
35
|
+
const isDisabled = disabled || state === "disabled";
|
|
36
|
+
const isLocked = state === "submitting";
|
|
37
|
+
const canSubmit = (normalizedValue.length > 0 || allowEmptySubmit) && !isDisabled && !isLocked;
|
|
23
38
|
const isCompact = density === "compact";
|
|
24
39
|
function clear() {
|
|
25
40
|
setInputValue("");
|
|
@@ -38,25 +53,37 @@ function useChatComposer({ value, defaultValue, onValueChange, onSubmit, state =
|
|
|
38
53
|
submit();
|
|
39
54
|
}
|
|
40
55
|
function handleKeyDown(event) {
|
|
41
|
-
if (event.defaultPrevented || event.key !== "Enter" || event.shiftKey || event.nativeEvent
|
|
56
|
+
if (event.defaultPrevented || event.key !== "Enter" || event.shiftKey || isComposingKey(event.nativeEvent)) return;
|
|
57
|
+
const modifierHeld = event.metaKey || event.ctrlKey;
|
|
58
|
+
if (submitKey === "mod-enter" && !modifierHeld) return;
|
|
42
59
|
event.preventDefault();
|
|
43
60
|
submit();
|
|
44
61
|
}
|
|
62
|
+
function insertText(text) {
|
|
63
|
+
if (isDisabled || isLocked) return;
|
|
64
|
+
const insertAtCaret = insertion.current();
|
|
65
|
+
if (insertAtCaret) insertAtCaret(text);
|
|
66
|
+
else setInputValue(`${inputValue}${text}`);
|
|
67
|
+
}
|
|
45
68
|
return {
|
|
46
69
|
value: inputValue,
|
|
47
70
|
setValue: setInputValue,
|
|
48
71
|
normalizedValue,
|
|
49
72
|
state,
|
|
50
73
|
density,
|
|
74
|
+
submitKey,
|
|
51
75
|
isCompact,
|
|
52
76
|
isDisabled,
|
|
77
|
+
isLocked,
|
|
53
78
|
canSubmit,
|
|
54
79
|
rows: isCompact ? 2 : 4,
|
|
55
80
|
sendCount,
|
|
56
81
|
clear,
|
|
57
82
|
submit,
|
|
58
83
|
handleSubmit,
|
|
59
|
-
handleKeyDown
|
|
84
|
+
handleKeyDown,
|
|
85
|
+
insertText,
|
|
86
|
+
registerInsertionTarget: insertion.register
|
|
60
87
|
};
|
|
61
88
|
}
|
|
62
89
|
//#endregion
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { ChatMessageKnobStyle } from "../knob-contracts/chat-message-knobs.js";
|
|
2
2
|
import { CSSProperties, ComponentProps } from "react";
|
|
3
3
|
//#region src/hooks/use-chat-message.d.ts
|
|
4
|
-
export type ChatRole = "user" | "assistant" | "system" | "tool";
|
|
4
|
+
export type ChatRole = "user" | "assistant" | "participant" | "system" | "tool";
|
|
5
5
|
export type ChatDensity = "compact" | "comfortable";
|
|
6
6
|
export type ChatState = "idle" | "streaming" | "pending" | "error";
|
|
7
|
+
export type ChatMessageLayout = "bubble" | "flat";
|
|
8
|
+
/** Transition announcements a message publishes to its thread. */
|
|
9
|
+
export type ChatMessageStatusLabels = {
|
|
10
|
+
pending: string;
|
|
11
|
+
replied: string;
|
|
12
|
+
error: string;
|
|
13
|
+
};
|
|
7
14
|
export type ChatMessageProps = ComponentProps<"article"> & {
|
|
8
15
|
from: ChatRole;
|
|
9
16
|
state?: ChatState;
|
|
10
17
|
density?: ChatDensity;
|
|
18
|
+
layout?: ChatMessageLayout;
|
|
19
|
+
continuation?: boolean;
|
|
20
|
+
/** Accessible name of the message, so the speaker isn't conveyed by alignment and colour alone. */
|
|
21
|
+
authorLabel?: string;
|
|
22
|
+
statusLabels?: Partial<ChatMessageStatusLabels>;
|
|
11
23
|
} & {
|
|
12
24
|
style?: CSSProperties & ChatMessageKnobStyle;
|
|
13
25
|
};
|
|
@@ -15,14 +27,28 @@ export type ChatMessageContext = {
|
|
|
15
27
|
from: ChatRole;
|
|
16
28
|
state: ChatState;
|
|
17
29
|
density: ChatDensity;
|
|
30
|
+
layout: ChatMessageLayout;
|
|
31
|
+
continuation: boolean;
|
|
18
32
|
isUser: boolean;
|
|
19
33
|
isAssistant: boolean;
|
|
34
|
+
isParticipant: boolean;
|
|
20
35
|
isSystem: boolean;
|
|
21
36
|
isTool: boolean;
|
|
22
37
|
isCompact: boolean;
|
|
23
38
|
isStreaming: boolean;
|
|
24
39
|
isPending: boolean;
|
|
25
40
|
isError: boolean;
|
|
41
|
+
isEndAligned: boolean;
|
|
42
|
+
isBubble: boolean;
|
|
26
43
|
};
|
|
27
|
-
export declare
|
|
44
|
+
export declare const chatAuthorLabels: Record<ChatRole, string>;
|
|
45
|
+
export declare const chatMessageStatusLabels: ChatMessageStatusLabels;
|
|
46
|
+
export declare const ChatThreadAnnounceContext: import("react").Context<(message: string) => void>;
|
|
47
|
+
/** Publishes a state transition (never streamed text) to the thread's one polite status region; a no-op outside `ChatThread`. */
|
|
48
|
+
export declare function useChatThreadAnnounce(): (message: string) => void;
|
|
49
|
+
export declare const ChatMessageStateProvider: import("react").Provider<ChatMessageContext | null>;
|
|
50
|
+
export declare function useChatMessageContext(): ChatMessageContext;
|
|
51
|
+
/** The announcement for a state change, or null when the change isn't worth announcing. */
|
|
52
|
+
export declare function chatMessageTransition(previous: ChatState, next: ChatState, labels: ChatMessageStatusLabels): string | null;
|
|
53
|
+
export declare function useChatMessage({ from, state, density, layout, continuation }: Pick<ChatMessageProps, "from" | "state" | "density" | "layout" | "continuation">): ChatMessageContext;
|
|
28
54
|
//#endregion
|
|
@@ -1,25 +1,56 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
1
2
|
//#region src/hooks/use-chat-message.ts
|
|
2
|
-
|
|
3
|
+
const chatAuthorLabels = {
|
|
4
|
+
user: "You",
|
|
5
|
+
assistant: "Assistant",
|
|
6
|
+
participant: "Participant",
|
|
7
|
+
system: "System",
|
|
8
|
+
tool: "Tool"
|
|
9
|
+
};
|
|
10
|
+
const chatMessageStatusLabels = {
|
|
11
|
+
pending: "Assistant is replying",
|
|
12
|
+
replied: "Assistant replied",
|
|
13
|
+
error: "Reply failed"
|
|
14
|
+
};
|
|
15
|
+
const ChatThreadAnnounceContext = createContext(() => {});
|
|
16
|
+
/** Publishes a state transition (never streamed text) to the thread's one polite status region; a no-op outside `ChatThread`. */
|
|
17
|
+
function useChatThreadAnnounce() {
|
|
18
|
+
return useContext(ChatThreadAnnounceContext);
|
|
19
|
+
}
|
|
20
|
+
const ChatMessageStateContext = createContext(null);
|
|
21
|
+
const ChatMessageStateProvider = ChatMessageStateContext.Provider;
|
|
22
|
+
function useChatMessageContext() {
|
|
23
|
+
const context = useContext(ChatMessageStateContext);
|
|
24
|
+
if (!context) throw new Error("ChatMessage compound components must be rendered inside <ChatMessage>.");
|
|
25
|
+
return context;
|
|
26
|
+
}
|
|
27
|
+
/** The announcement for a state change, or null when the change isn't worth announcing. */
|
|
28
|
+
function chatMessageTransition(previous, next, labels) {
|
|
29
|
+
if (previous === next) return null;
|
|
30
|
+
if (next === "pending") return labels.pending;
|
|
31
|
+
if (next === "error") return labels.error;
|
|
32
|
+
if (next === "idle" && (previous === "pending" || previous === "streaming")) return labels.replied;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function useChatMessage({ from, state = "idle", density = "comfortable", layout = "bubble", continuation = false }) {
|
|
3
36
|
return {
|
|
4
37
|
from,
|
|
5
38
|
state,
|
|
6
39
|
density,
|
|
40
|
+
layout,
|
|
41
|
+
continuation,
|
|
7
42
|
isUser: from === "user",
|
|
8
43
|
isAssistant: from === "assistant",
|
|
44
|
+
isParticipant: from === "participant",
|
|
9
45
|
isSystem: from === "system",
|
|
10
46
|
isTool: from === "tool",
|
|
11
47
|
isCompact: density === "compact",
|
|
12
48
|
isStreaming: state === "streaming",
|
|
13
49
|
isPending: state === "pending",
|
|
14
|
-
isError: state === "error"
|
|
50
|
+
isError: state === "error",
|
|
51
|
+
isEndAligned: from === "user" && layout === "bubble",
|
|
52
|
+
isBubble: layout === "bubble" && (from === "user" || from === "participant")
|
|
15
53
|
};
|
|
16
54
|
}
|
|
17
|
-
function useChatMessage({ from, state = "idle", density = "comfortable" }) {
|
|
18
|
-
return getChatMessageContext({
|
|
19
|
-
from,
|
|
20
|
-
state,
|
|
21
|
-
density
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
55
|
//#endregion
|
|
25
|
-
export { useChatMessage };
|
|
56
|
+
export { ChatMessageStateProvider, ChatThreadAnnounceContext, chatAuthorLabels, chatMessageStatusLabels, chatMessageTransition, useChatMessage, useChatMessageContext, useChatThreadAnnounce };
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
import { prefersReducedMotion } from "../lib/motion.js";
|
|
1
2
|
import { useEffect, useRef, useState } from "react";
|
|
2
3
|
//#region src/hooks/use-chat-thread-scroll.ts
|
|
3
4
|
const BOTTOM_THRESHOLD = 24;
|
|
4
5
|
const SETTLE_TIMEOUT = 700;
|
|
6
|
+
function firstVisibleChild(list, viewportTop) {
|
|
7
|
+
const children = list.children;
|
|
8
|
+
let low = 0;
|
|
9
|
+
let high = children.length - 1;
|
|
10
|
+
let found;
|
|
11
|
+
while (low <= high) {
|
|
12
|
+
const middle = low + high >> 1;
|
|
13
|
+
if (children[middle].getBoundingClientRect().bottom > viewportTop) {
|
|
14
|
+
found = children[middle];
|
|
15
|
+
high = middle - 1;
|
|
16
|
+
} else low = middle + 1;
|
|
17
|
+
}
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
5
20
|
function useChatThreadScroll() {
|
|
6
21
|
const viewportRef = useRef(null);
|
|
7
22
|
const contentRef = useRef(null);
|
|
8
23
|
const pinnedRef = useRef(true);
|
|
9
24
|
const [atBottom, setAtBottom] = useState(true);
|
|
10
25
|
const settlingRef = useRef(void 0);
|
|
11
|
-
function scrollToBottom(behavior
|
|
26
|
+
function scrollToBottom(behavior) {
|
|
12
27
|
const viewport = viewportRef.current;
|
|
13
28
|
if (!viewport) return;
|
|
14
29
|
pinnedRef.current = true;
|
|
@@ -19,13 +34,29 @@ function useChatThreadScroll() {
|
|
|
19
34
|
}, SETTLE_TIMEOUT);
|
|
20
35
|
viewport.scrollTo({
|
|
21
36
|
top: viewport.scrollHeight,
|
|
22
|
-
behavior
|
|
37
|
+
behavior: behavior ?? (prefersReducedMotion(viewport) ? "auto" : "smooth")
|
|
23
38
|
});
|
|
24
39
|
}
|
|
25
40
|
useEffect(() => {
|
|
26
41
|
const viewport = viewportRef.current;
|
|
27
42
|
const content = contentRef.current;
|
|
28
|
-
|
|
43
|
+
const messageList = content?.firstElementChild;
|
|
44
|
+
if (!viewport || !content || !messageList) return;
|
|
45
|
+
let anchor;
|
|
46
|
+
function rememberAnchor() {
|
|
47
|
+
if (!viewport || !messageList) return;
|
|
48
|
+
const viewportTop = viewport.getBoundingClientRect().top;
|
|
49
|
+
const element = firstVisibleChild(messageList, viewportTop);
|
|
50
|
+
anchor = element ? {
|
|
51
|
+
element,
|
|
52
|
+
offset: element.getBoundingClientRect().top - viewportTop
|
|
53
|
+
} : void 0;
|
|
54
|
+
}
|
|
55
|
+
function keepAnchorInPlace() {
|
|
56
|
+
if (!viewport || !anchor?.element.isConnected) return;
|
|
57
|
+
const drift = anchor.element.getBoundingClientRect().top - viewport.getBoundingClientRect().top - anchor.offset;
|
|
58
|
+
if (drift !== 0) viewport.scrollTop += drift;
|
|
59
|
+
}
|
|
29
60
|
function settle() {
|
|
30
61
|
clearTimeout(settlingRef.current);
|
|
31
62
|
settlingRef.current = void 0;
|
|
@@ -37,21 +68,26 @@ function useChatThreadScroll() {
|
|
|
37
68
|
pinnedRef.current = distance <= BOTTOM_THRESHOLD;
|
|
38
69
|
setAtBottom(pinnedRef.current);
|
|
39
70
|
}
|
|
71
|
+
function handleScroll() {
|
|
72
|
+
rememberAnchor();
|
|
73
|
+
readPosition();
|
|
74
|
+
}
|
|
40
75
|
const observer = new ResizeObserver(() => {
|
|
41
76
|
if (!viewport) return;
|
|
42
77
|
if (pinnedRef.current) viewport.scrollTop = viewport.scrollHeight;
|
|
43
|
-
else
|
|
78
|
+
else keepAnchorInPlace();
|
|
79
|
+
handleScroll();
|
|
44
80
|
});
|
|
45
81
|
observer.observe(content);
|
|
46
82
|
observer.observe(viewport);
|
|
47
|
-
viewport.addEventListener("scroll",
|
|
83
|
+
viewport.addEventListener("scroll", handleScroll, { passive: true });
|
|
48
84
|
viewport.addEventListener("scrollend", settle);
|
|
49
85
|
viewport.scrollTop = viewport.scrollHeight;
|
|
50
|
-
|
|
86
|
+
handleScroll();
|
|
51
87
|
return () => {
|
|
52
88
|
observer.disconnect();
|
|
53
89
|
clearTimeout(settlingRef.current);
|
|
54
|
-
viewport.removeEventListener("scroll",
|
|
90
|
+
viewport.removeEventListener("scroll", handleScroll);
|
|
55
91
|
viewport.removeEventListener("scrollend", settle);
|
|
56
92
|
};
|
|
57
93
|
}, []);
|
|
@@ -8,10 +8,10 @@ export type ColorAreaRect = {
|
|
|
8
8
|
width: number;
|
|
9
9
|
height: number;
|
|
10
10
|
};
|
|
11
|
-
export type ColorAreaPointer = {
|
|
12
|
-
areaRef: RefObject<
|
|
11
|
+
export type ColorAreaPointer<Element extends HTMLElement> = {
|
|
12
|
+
areaRef: RefObject<Element | null>;
|
|
13
13
|
dragging: boolean;
|
|
14
14
|
onPointerDown: (event: PointerEvent) => void;
|
|
15
15
|
};
|
|
16
|
-
export declare function useColorArea(onChange: (offset: ColorAreaOffset, rect: ColorAreaRect) => void): ColorAreaPointer
|
|
16
|
+
export declare function useColorArea<Element extends HTMLElement = HTMLDivElement>(onChange: (offset: ColorAreaOffset, rect: ColorAreaRect) => void, targetRef?: RefObject<Element | null>): ColorAreaPointer<Element>;
|
|
17
17
|
//#endregion
|
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
|
3
3
|
//#region src/hooks/use-color-area.ts
|
|
4
|
-
function useColorArea(onChange) {
|
|
5
|
-
const
|
|
4
|
+
function useColorArea(onChange, targetRef) {
|
|
5
|
+
const ownRef = useRef(null);
|
|
6
|
+
const areaRef = targetRef ?? ownRef;
|
|
6
7
|
const [dragging, setDragging] = useState(false);
|
|
7
8
|
const emitDragChange = useEffectEvent(onChange);
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
if (!dragging) return;
|
|
10
|
-
function
|
|
11
|
+
function onMove(event) {
|
|
11
12
|
const el = areaRef.current;
|
|
12
13
|
if (!el) return;
|
|
13
14
|
const rect = el.getBoundingClientRect();
|
|
14
15
|
emitDragChange({
|
|
15
|
-
x: clientX - rect.left,
|
|
16
|
-
y: clientY - rect.top
|
|
16
|
+
x: event.clientX - rect.left,
|
|
17
|
+
y: event.clientY - rect.top
|
|
17
18
|
}, {
|
|
18
19
|
width: rect.width,
|
|
19
20
|
height: rect.height
|
|
20
21
|
});
|
|
21
22
|
}
|
|
22
|
-
function
|
|
23
|
-
emit(event.clientX, event.clientY);
|
|
24
|
-
}
|
|
25
|
-
function onUp() {
|
|
23
|
+
function onEnd() {
|
|
26
24
|
setDragging(false);
|
|
27
25
|
}
|
|
28
26
|
const previousSelect = document.body.style.userSelect;
|
|
29
27
|
document.body.style.userSelect = "none";
|
|
30
28
|
window.addEventListener("pointermove", onMove);
|
|
31
|
-
window.addEventListener("pointerup",
|
|
29
|
+
window.addEventListener("pointerup", onEnd);
|
|
30
|
+
window.addEventListener("pointercancel", onEnd);
|
|
32
31
|
return () => {
|
|
33
32
|
window.removeEventListener("pointermove", onMove);
|
|
34
|
-
window.removeEventListener("pointerup",
|
|
33
|
+
window.removeEventListener("pointerup", onEnd);
|
|
34
|
+
window.removeEventListener("pointercancel", onEnd);
|
|
35
35
|
document.body.style.userSelect = previousSelect;
|
|
36
36
|
};
|
|
37
|
-
}, [dragging]);
|
|
37
|
+
}, [dragging, areaRef]);
|
|
38
38
|
function onPointerDown(event) {
|
|
39
|
+
if (event.button !== 0) return;
|
|
39
40
|
const el = areaRef.current;
|
|
40
41
|
if (el) {
|
|
41
42
|
const rect = el.getBoundingClientRect();
|
|
@@ -5,8 +5,9 @@ export type UseCopyToClipboardOptions = {
|
|
|
5
5
|
onCopy?: (value: string) => void;
|
|
6
6
|
onCopyError?: (error: unknown) => void;
|
|
7
7
|
};
|
|
8
|
+
export type CopyStatus = "idle" | "copied" | "failed";
|
|
8
9
|
export type UseCopyToClipboardResult = {
|
|
9
|
-
|
|
10
|
+
status: CopyStatus;
|
|
10
11
|
copyToClipboard: (value: string) => Promise<boolean>;
|
|
11
12
|
resetCopied: () => void;
|
|
12
13
|
};
|
|
@@ -17,15 +17,15 @@ function copyViaSelection(value) {
|
|
|
17
17
|
textarea.setAttribute("readonly", "");
|
|
18
18
|
textarea.setAttribute("aria-hidden", "true");
|
|
19
19
|
textarea.style.position = "fixed";
|
|
20
|
-
textarea.style.
|
|
21
|
-
textarea.style.
|
|
20
|
+
textarea.style.insetBlockStart = "0";
|
|
21
|
+
textarea.style.insetInlineStart = "0";
|
|
22
22
|
textarea.style.width = "1px";
|
|
23
23
|
textarea.style.height = "1px";
|
|
24
24
|
textarea.style.opacity = "0";
|
|
25
25
|
textarea.style.pointerEvents = "none";
|
|
26
26
|
document.body.appendChild(textarea);
|
|
27
27
|
try {
|
|
28
|
-
textarea.focus();
|
|
28
|
+
textarea.focus({ preventScroll: true });
|
|
29
29
|
textarea.select();
|
|
30
30
|
textarea.setSelectionRange(0, value.length);
|
|
31
31
|
return document.execCommand("copy");
|
|
@@ -41,7 +41,7 @@ async function copyTextToClipboard(value) {
|
|
|
41
41
|
return copyViaSelection(value);
|
|
42
42
|
}
|
|
43
43
|
function useCopyToClipboard({ text, copiedDuration = 1200, onCopy, onCopyError } = {}) {
|
|
44
|
-
const [
|
|
44
|
+
const [status, setStatus] = useState("idle");
|
|
45
45
|
const resetTimeout = useRef(null);
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
const timeoutRef = resetTimeout;
|
|
@@ -52,38 +52,38 @@ function useCopyToClipboard({ text, copiedDuration = 1200, onCopy, onCopyError }
|
|
|
52
52
|
function resetCopied() {
|
|
53
53
|
if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
|
|
54
54
|
resetTimeout.current = null;
|
|
55
|
-
|
|
55
|
+
setStatus("idle");
|
|
56
|
+
}
|
|
57
|
+
function settle(next) {
|
|
58
|
+
setStatus(next);
|
|
59
|
+
if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
|
|
60
|
+
resetTimeout.current = next === "copied" ? window.setTimeout(() => setStatus("idle"), copiedDuration) : null;
|
|
61
|
+
return next === "copied";
|
|
56
62
|
}
|
|
57
63
|
async function copyToClipboard(value) {
|
|
58
64
|
if (!await copyTextToClipboard(value)) {
|
|
59
65
|
onCopyError?.(/* @__PURE__ */ new Error("Unable to copy text"));
|
|
60
|
-
|
|
61
|
-
return false;
|
|
66
|
+
return settle("failed");
|
|
62
67
|
}
|
|
63
68
|
try {
|
|
64
69
|
onCopy?.(value);
|
|
65
70
|
} catch (error) {
|
|
66
71
|
onCopyError?.(error);
|
|
67
|
-
|
|
68
|
-
return false;
|
|
72
|
+
return settle("failed");
|
|
69
73
|
}
|
|
70
|
-
|
|
71
|
-
if (resetTimeout.current) window.clearTimeout(resetTimeout.current);
|
|
72
|
-
resetTimeout.current = window.setTimeout(() => setIsCopied(false), copiedDuration);
|
|
73
|
-
return true;
|
|
74
|
+
return settle("copied");
|
|
74
75
|
}
|
|
75
76
|
function handleCopy() {
|
|
76
77
|
return text !== void 0 ? copyToClipboard(text) : Promise.resolve(false);
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const state = {
|
|
80
|
+
status,
|
|
80
81
|
copyToClipboard,
|
|
81
82
|
resetCopied
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
handleCopy
|
|
86
|
-
resetCopied
|
|
83
|
+
};
|
|
84
|
+
return text === void 0 ? state : {
|
|
85
|
+
...state,
|
|
86
|
+
handleCopy
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
|
@@ -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);
|