@adea-ai/ui 0.63.1 → 0.63.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/NOTICE +28 -0
- package/dist/components/conversation/ime-guard.d.ts +10 -0
- package/dist/components/conversation/ime-guard.d.ts.map +1 -0
- package/dist/components/conversation/ime-guard.js +45 -0
- package/dist/components/conversation/ime-guard.js.map +1 -0
- package/dist/components/conversation/message-composer.d.ts +5 -0
- package/dist/components/conversation/message-composer.d.ts.map +1 -1
- package/dist/components/conversation/message-composer.js +16 -1
- package/dist/components/conversation/message-composer.js.map +1 -1
- package/dist/r/conversation.json +5 -0
- package/dist/r/registry.json +5 -0
- package/dist/r/src/components/conversation/ime-guard.ts +68 -0
- package/dist/r/src/components/conversation/message-composer.tsx +13 -1
- package/package.json +1 -1
- package/registry.json +5 -0
- package/src/components/conversation/ime-guard.ts +68 -0
- package/src/components/conversation/message-composer.tsx +13 -1
package/README.md
CHANGED
|
@@ -153,6 +153,21 @@ The toolbar switches theme, accent, typeface and density live, which is the
|
|
|
153
153
|
fastest way to see how a component behaves across the catalogue rather than in
|
|
154
154
|
one theme.
|
|
155
155
|
|
|
156
|
+
## Composer keyboard behavior
|
|
157
|
+
|
|
158
|
+
`MessageComposer` preserves native IME candidate commits and suppresses sending
|
|
159
|
+
during composition, including the 50 ms commit window when browser flags have
|
|
160
|
+
already cleared. A separate Enter sends after that window; Shift+Enter remains
|
|
161
|
+
a soft break. A host or menu that already prevented the event keeps ownership.
|
|
162
|
+
The selected KiroCrew textarea guard is translated to Solid ownership; it adds
|
|
163
|
+
no document listener or application session state. Source and license details
|
|
164
|
+
are retained in `NOTICE`.
|
|
165
|
+
|
|
166
|
+
Run `bun run test:components` for the built Solid component contracts in
|
|
167
|
+
Chromium and WebKit. The event-sequence tests exercise browser handling of
|
|
168
|
+
composition flags, timers and focus recovery; manual operating-system IME
|
|
169
|
+
acceptance and each application's mounted chat journey remain separate gates.
|
|
170
|
+
|
|
156
171
|
## Licence
|
|
157
172
|
|
|
158
173
|
Apache-2.0.
|
package/dist/NOTICE
CHANGED
|
@@ -49,6 +49,34 @@ Origin UI / coss (AGPL-3.0-or-later) was consulted for visual ideas only. No
|
|
|
49
49
|
code, class string, or asset from that project is reproduced here, because its
|
|
50
50
|
licence is incompatible with this distribution.
|
|
51
51
|
|
|
52
|
+
## Composer IME input ownership translated from KiroCrew
|
|
53
|
+
|
|
54
|
+
`packages/ui/src/components/conversation/ime-guard.ts` and its MessageComposer
|
|
55
|
+
binding substantially translate the native latch and textarea-scoped lifecycle
|
|
56
|
+
from `website/src/hooks/useImeGuard.ts`, pinned at KiroCrew revision
|
|
57
|
+
`283e136c0f902e965a535a7c9548c57c7504fed0`.
|
|
58
|
+
|
|
59
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
60
|
+
This product includes software developed at Amazon.com, Inc.
|
|
61
|
+
(https://www.amazon.com/).
|
|
62
|
+
Licensed under the Apache License, Version 2.0.
|
|
63
|
+
|
|
64
|
+
Retained: native isComposing/keyCode229 signals, the 50ms post-composition window,
|
|
65
|
+
back-to-back composition fencing, native candidate default actions, and recovery
|
|
66
|
+
on focus/blur. Solid owner cleanup replaces React effects/ref ownership; native
|
|
67
|
+
propagation replaces React's synthetic-event half. A repeated compositionend
|
|
68
|
+
replaces the previous timer so an older expiry cannot release the latest window.
|
|
69
|
+
The document-latch/focus-trap branch is not copied: this is one textarea's send
|
|
70
|
+
binding, and maintained Kobalte primitives retain overlay focus authority.
|
|
71
|
+
|
|
72
|
+
Browser integration tests translate relevant cases from
|
|
73
|
+
`website/src/test/SideChat.imeEnter.test.tsx` and the lifecycle/window cases in
|
|
74
|
+
`website/src/hooks/useImeGuard.documentLatch.test.tsx`; React/Redux/query/API test
|
|
75
|
+
providers are replaced by a controlled Solid fixture. No React dependency,
|
|
76
|
+
service transport, dictation engine, font, asset or document listener is copied.
|
|
77
|
+
This is selected-unit hardening, not a claim that the full KiroCrew composer,
|
|
78
|
+
goal flow or Adea runtime-session Chat has been ported or integrated.
|
|
79
|
+
|
|
52
80
|
## Appearance editor translated from Zeron
|
|
53
81
|
|
|
54
82
|
The System/Light/Dark miniature composition, palette bands, theme selectors,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Internal textarea guard: no application session or draft authority. */
|
|
2
|
+
export declare function createComposerImeGuard(): {
|
|
3
|
+
onCompositionStart(): void;
|
|
4
|
+
onCompositionEnd(): void;
|
|
5
|
+
onFocus: () => void;
|
|
6
|
+
onBlur: () => void;
|
|
7
|
+
/** Claim only a key the composer already recognizes as its send binding. */
|
|
8
|
+
claimEnter(event: KeyboardEvent): boolean;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=ime-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ime-guard.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/ime-guard.ts"],"names":[],"mappings":"AA2BA,0EAA0E;AAC1E,wBAAgB,sBAAsB;;;;;IA0BlC,4EAA4E;sBAC1D,aAAa;EAYlC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { onCleanup } from "solid-js";
|
|
2
|
+
//#region src/components/conversation/ime-guard.ts
|
|
3
|
+
var POST_COMPOSITION_MS = 50;
|
|
4
|
+
/** Internal textarea guard: no application session or draft authority. */
|
|
5
|
+
function createComposerImeGuard() {
|
|
6
|
+
let latched = false;
|
|
7
|
+
let timer;
|
|
8
|
+
const reset = () => {
|
|
9
|
+
clearTimeout(timer);
|
|
10
|
+
timer = void 0;
|
|
11
|
+
latched = false;
|
|
12
|
+
};
|
|
13
|
+
onCleanup(reset);
|
|
14
|
+
return {
|
|
15
|
+
onCompositionStart() {
|
|
16
|
+
clearTimeout(timer);
|
|
17
|
+
timer = void 0;
|
|
18
|
+
latched = true;
|
|
19
|
+
},
|
|
20
|
+
onCompositionEnd() {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
latched = true;
|
|
23
|
+
timer = setTimeout(() => {
|
|
24
|
+
latched = false;
|
|
25
|
+
timer = void 0;
|
|
26
|
+
}, POST_COMPOSITION_MS);
|
|
27
|
+
},
|
|
28
|
+
onFocus: reset,
|
|
29
|
+
onBlur: reset,
|
|
30
|
+
/** Claim only a key the composer already recognizes as its send binding. */
|
|
31
|
+
claimEnter(event) {
|
|
32
|
+
const nativeComposition = event.isComposing || event.keyCode === 229;
|
|
33
|
+
if (!nativeComposition) event.preventDefault();
|
|
34
|
+
if (latched || nativeComposition) {
|
|
35
|
+
event.stopPropagation();
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createComposerImeGuard };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=ime-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ime-guard.js","names":[],"sources":["../../../src/components/conversation/ime-guard.ts"],"sourcesContent":["/*\n * Substantially translated from KiroCrew website/src/hooks/useImeGuard.ts\n * (`createImeLatch` and the textarea-scoped `useImeGuard` binding), pinned at\n * 283e136c0f902e965a535a7c9548c57c7504fed0.\n * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n * This product includes software developed at Amazon.com, Inc.\n * (https://www.amazon.com/).\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n * http://www.apache.org/licenses/LICENSE-2.0\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n *\n * Native events replace React synthetic propagation; Solid ownership replaces\n * React refs/effects. No document listeners or focus trap are introduced.\n * A repeated compositionend replaces its old timer as well as a new start,\n * so an older expiry cannot prematurely release the new commit window.\n */\nimport { onCleanup } from 'solid-js'\n\nconst POST_COMPOSITION_MS = 50\n\n/** Internal textarea guard: no application session or draft authority. */\nexport function createComposerImeGuard() {\n let latched = false\n let timer: ReturnType<typeof setTimeout> | undefined\n const reset = () => {\n clearTimeout(timer)\n timer = undefined\n latched = false\n }\n onCleanup(reset)\n\n return {\n onCompositionStart() {\n clearTimeout(timer)\n timer = undefined\n latched = true\n },\n onCompositionEnd() {\n clearTimeout(timer)\n latched = true\n timer = setTimeout(() => {\n latched = false\n timer = undefined\n }, POST_COMPOSITION_MS)\n },\n onFocus: reset,\n onBlur: reset,\n /** Claim only a key the composer already recognizes as its send binding. */\n claimEnter(event: KeyboardEvent) {\n const nativeComposition = event.isComposing || event.keyCode === 229\n // Native IME commits retain their default action. Only the latch-only\n // post-commit window suppresses the newline a plain Enter would insert.\n if (!nativeComposition) event.preventDefault()\n if (latched || nativeComposition) {\n event.stopPropagation()\n return false\n }\n return true\n },\n }\n}\n"],"mappings":";;AAyBA,IAAM,sBAAsB;;AAG5B,SAAgB,yBAAyB;CACvC,IAAI,UAAU;CACd,IAAI;CACJ,MAAM,cAAc;EAClB,aAAa,KAAK;EAClB,QAAQ,KAAA;EACR,UAAU;CACZ;CACA,UAAU,KAAK;CAEf,OAAO;EACL,qBAAqB;GACnB,aAAa,KAAK;GAClB,QAAQ,KAAA;GACR,UAAU;EACZ;EACA,mBAAmB;GACjB,aAAa,KAAK;GAClB,UAAU;GACV,QAAQ,iBAAiB;IACvB,UAAU;IACV,QAAQ,KAAA;GACV,GAAG,mBAAmB;EACxB;EACA,SAAS;EACT,QAAQ;;EAER,WAAW,OAAsB;GAC/B,MAAM,oBAAoB,MAAM,eAAe,MAAM,YAAY;GAGjE,IAAI,CAAC,mBAAmB,MAAM,eAAe;GAC7C,IAAI,WAAW,mBAAmB;IAChC,MAAM,gBAAgB;IACtB,OAAO;GACT;GACA,OAAO;EACT;CACF;AACF"}
|
|
@@ -26,6 +26,11 @@ import { Button } from '../ui/button/button';
|
|
|
26
26
|
*
|
|
27
27
|
* **The status region is `aria-live`.** "Sending…" and a failure both land there, so
|
|
28
28
|
* a screen reader hears the outcome without the composer stealing focus.
|
|
29
|
+
*
|
|
30
|
+
* Enter ownership follows KiroCrew's pinned IME latch: candidate commits keep
|
|
31
|
+
* their native default action, and the post-composition Enter cannot send or
|
|
32
|
+
* insert an accidental newline. Composition abandoned by focus/blur recovers;
|
|
33
|
+
* the Solid owner's cleanup settles its timer. See ime-guard.ts for provenance.
|
|
29
34
|
*/
|
|
30
35
|
export type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {
|
|
31
36
|
/** The draft. Controlled: the caller owns the text, because it usually persists. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-composer.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message-composer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAGnD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"message-composer.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/message-composer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAGnD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAM5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,GAAG;IAC5E,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,sDAAsD;IACtD,OAAO,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACrB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACtB,iEAAiE;IACjE,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IAClB,0DAA0D;IAC1D,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CACnD,CAAA;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,eA2I1D;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,cAAc,CAAC,OAAO,MAAM,CAAC,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,eAiB1E;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,eAetD"}
|
|
@@ -3,6 +3,7 @@ import { Button } from "../ui/button/button.js";
|
|
|
3
3
|
import { Kbd } from "../ui/kbd/kbd.js";
|
|
4
4
|
import { Spinner } from "../ui/spinner/spinner.js";
|
|
5
5
|
import { Textarea } from "../ui/textarea/textarea.js";
|
|
6
|
+
import { createComposerImeGuard } from "./ime-guard.js";
|
|
6
7
|
import { className, createComponent, effect, insert, memo, mergeProps, spread, template } from "solid-js/web";
|
|
7
8
|
import { CornerDownLeft, Paperclip, Send, X } from "lucide-solid";
|
|
8
9
|
import { Show, createSignal, splitProps } from "solid-js";
|
|
@@ -30,6 +31,7 @@ function MessageComposer(props) {
|
|
|
30
31
|
]);
|
|
31
32
|
const [sending, setSending] = createSignal(false);
|
|
32
33
|
const [error, setError] = createSignal(null);
|
|
34
|
+
const ime = createComposerImeGuard();
|
|
33
35
|
const canSend = () => local.value.trim().length > 0 && !local.disabled && !sending();
|
|
34
36
|
const send = async () => {
|
|
35
37
|
if (!canSend()) return;
|
|
@@ -44,13 +46,14 @@ function MessageComposer(props) {
|
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
const onKeyDown = (event) => {
|
|
49
|
+
if (event.defaultPrevented) return;
|
|
47
50
|
if (event.key === "Escape" && error()) {
|
|
48
51
|
setError(null);
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
if (event.key !== "Enter") return;
|
|
52
55
|
if (event.shiftKey && !event.metaKey && !event.ctrlKey) return;
|
|
53
|
-
|
|
56
|
+
if (!ime.claimEnter(event)) return;
|
|
54
57
|
send();
|
|
55
58
|
};
|
|
56
59
|
return (() => {
|
|
@@ -94,6 +97,18 @@ function MessageComposer(props) {
|
|
|
94
97
|
})()
|
|
95
98
|
}), _el$3);
|
|
96
99
|
insert(_el$3, createComponent(Textarea, {
|
|
100
|
+
get onCompositionStart() {
|
|
101
|
+
return ime.onCompositionStart;
|
|
102
|
+
},
|
|
103
|
+
get onCompositionEnd() {
|
|
104
|
+
return ime.onCompositionEnd;
|
|
105
|
+
},
|
|
106
|
+
get onFocus() {
|
|
107
|
+
return ime.onFocus;
|
|
108
|
+
},
|
|
109
|
+
get onBlur() {
|
|
110
|
+
return ime.onBlur;
|
|
111
|
+
},
|
|
97
112
|
get value() {
|
|
98
113
|
return local.value;
|
|
99
114
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-composer.js","names":["CornerDownLeft","Paperclip","Send","X","ComponentProps","JSX","Show","createSignal","splitProps","cn","Button","Kbd","Spinner","Textarea","MessageComposerProps","Omit","value","onValueChange","onSubmit","Promise","placeholder","disabled","submitLabel","showHint","leading","Element","trailing","menu","replyTo","label","onDismiss","MessageComposer","props","local","rest","sending","setSending","error","setError","canSend","trim","length","send","onKeyDown","EventHandlerUnion","HTMLTextAreaElement","KeyboardEvent","event","key","shiftKey","metaKey","ctrlKey","preventDefault","_el$","_tmpl$3","_el$3","firstChild","_el$4","_el$5","_el$10","nextSibling","addEventListener","_$spread","_$mergeProps","class","_$insert","_$createComponent","when","children","_el$2","_tmpl$","reply","_el$11","_tmpl$4","_el$12","_el$13","type","variant","size","onClick","onInput","currentTarget","rows","aria-label","_$memo","fallback","_el$6","_tmpl$2","_el$7","_el$8","_el$9","_el$1","_el$0","message","_el$14","_tmpl$5","_$effect","_$className","ComposerAttachmentButton","count","open","aria-expanded","ComposerHint","_el$15","_tmpl$6","_$mergeProps","_$createComponent","_$memo","_$className"],"sources":["../../../src/components/conversation/message-composer.tsx"],"sourcesContent":["import { CornerDownLeft, Paperclip, Send, X } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, createSignal, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { Kbd } from '../ui/kbd/kbd'\nimport { Spinner } from '../ui/spinner/spinner'\nimport { Textarea } from '../ui/textarea/textarea'\n\n/**\n * MessageComposer.\n *\n * The field a message is written in. It is a form, and it behaves like one: Enter\n * sends, Shift+Enter breaks the line, Escape clears the error.\n *\n * ## What it does not know\n *\n * No mention model, no attachment model, no dictation, no idempotency keys. The\n * first version of this composer was written against adea's agent and artifact\n * types, which meant it could only ever be used by adea. Here the attachment control\n * is a *slot*, the mention menu is a slot, and a caller that has an agent model\n * renders one; a caller that does not gets a composer without it.\n *\n * ## The three things it does own\n *\n * **Enter sends, Shift+Enter breaks the line.** That is the convention every chat\n * application shares, and getting it wrong is the single most irritating thing a\n * composer can do. `⌘`/`Ctrl`+Enter is accepted too, because someone arriving from an\n * editor will try it.\n *\n * **The send button is disabled when there is nothing to send.** A control that\n * accepts a press and does nothing is worse than one that says it cannot.\n *\n * **The status region is `aria-live`.** \"Sending…\" and a failure both land there, so\n * a screen reader hears the outcome without the composer stealing focus.\n */\nexport type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {\n /** The draft. Controlled: the caller owns the text, because it usually persists. */\n value: string\n onValueChange: (value: string) => void\n /** Called on send. A rejection is surfaced as an error and the draft is kept. */\n onSubmit: () => void | Promise<void>\n placeholder?: string\n /** Disable the whole composer, e.g. while the channel is read-only. */\n disabled?: boolean\n /** Label the send action for a context other than a message, e.g. a reply. */\n submitLabel?: string\n /** Show the keyboard hint under the field. */\n showHint?: boolean\n /** A leading control group, e.g. an attach button. */\n leading?: JSX.Element\n /** A trailing group before the send button, e.g. a dictation toggle. */\n trailing?: JSX.Element\n /** A menu rendered above the field, e.g. mention suggestions. */\n menu?: JSX.Element\n /** The reply target, drawn as a strip above the field. */\n replyTo?: { label: string; onDismiss: () => void }\n}\n\nexport function MessageComposer(props: MessageComposerProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'value',\n 'onValueChange',\n 'onSubmit',\n 'placeholder',\n 'disabled',\n 'submitLabel',\n 'showHint',\n 'leading',\n 'trailing',\n 'menu',\n 'replyTo',\n ])\n\n const [sending, setSending] = createSignal(false)\n const [error, setError] = createSignal<string | null>(null)\n\n const canSend = () => local.value.trim().length > 0 && !local.disabled && !sending()\n\n const send = async () => {\n if (!canSend()) return\n setSending(true)\n setError(null)\n try {\n await local.onSubmit()\n } catch {\n // The draft is deliberately left in place. A send that fails and clears the\n // field loses the user's work, which is the worst outcome available here.\n setError('Message not sent. Your draft is still here; retry when the connection recovers.')\n } finally {\n setSending(false)\n }\n }\n\n const onKeyDown: JSX.EventHandlerUnion<HTMLTextAreaElement, KeyboardEvent> = (event) => {\n if (event.key === 'Escape' && error()) {\n setError(null)\n return\n }\n if (event.key !== 'Enter') return\n // Shift breaks the line; a bare Enter, or a modified one, sends.\n if (event.shiftKey && !event.metaKey && !event.ctrlKey) return\n event.preventDefault()\n void send()\n }\n\n return (\n <form\n data-slot=\"message-composer\"\n class={cn('flex flex-col gap-2', local.class)}\n onSubmit={(event) => {\n event.preventDefault()\n void send()\n }}\n {...rest}\n >\n <Show when={local.menu}>\n <div>{local.menu}</div>\n </Show>\n\n <Show when={local.replyTo}>\n {(reply) => (\n <div class=\"bg-muted text-muted-foreground flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs\">\n <span class=\"min-w-0 flex-1 truncate\">Replying to {reply().label}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-2xs\"\n aria-label=\"Cancel reply\"\n onClick={() => reply().onDismiss()}\n >\n <X />\n </Button>\n </div>\n )}\n </Show>\n\n <div\n class={cn(\n 'border-input bg-card flex flex-col gap-1 rounded-lg border p-2',\n 'transition-[color,box-shadow,border-color] ease-out',\n 'focus-within:border-ring focus-within:ring-3 focus-within:ring-primary-subtle',\n local.disabled && 'opacity-50'\n )}\n >\n <Textarea\n value={local.value}\n onInput={(event) => local.onValueChange(event.currentTarget.value)}\n onKeyDown={onKeyDown}\n placeholder={local.placeholder ?? 'Write a message…'}\n disabled={local.disabled}\n rows={1}\n aria-label={local.submitLabel ? `${local.submitLabel} message` : 'Message'}\n class=\"min-h-9 resize-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0\"\n />\n\n <div class=\"flex items-center gap-1\">\n <Show when={local.leading}>{local.leading}</Show>\n <div class=\"ms-auto flex items-center gap-1\">\n <Show when={local.trailing}>{local.trailing}</Show>\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n disabled={!canSend()}\n aria-label={sending() ? 'Sending message' : 'Send message'}\n >\n <Show when={sending()} fallback={<Send />}>\n <Spinner size=\"sm\" label={false} class=\"text-primary-foreground\" />\n </Show>\n </Button>\n </div>\n </div>\n </div>\n\n <Show when={local.showHint}>\n <p class=\"text-muted-foreground flex items-center gap-1.5 text-2xs\">\n <Kbd>Enter</Kbd> to send\n <span aria-hidden=\"true\">·</span>\n <Kbd>Shift</Kbd> <Kbd>Enter</Kbd> for a new line\n </p>\n </Show>\n\n {/* A live region, so the outcome is announced without moving focus out of the\n field the user is typing in. */}\n <div aria-live=\"polite\" class=\"min-h-0\">\n <Show when={error()}>\n {(message) => <p class=\"text-destructive text-xs\">{message()}</p>}\n </Show>\n </div>\n </form>\n )\n}\n\n/**\n * The attachment control.\n *\n * A slot rather than a built-in, because what can be attached is the application's\n * business — but the *button* is shared, so it looks and behaves the same in both.\n * The count is announced, since a paperclip alone does not say whether anything is\n * attached.\n */\nexport function ComposerAttachmentButton(\n props: ComponentProps<typeof Button> & { count?: number; open?: boolean }\n) {\n const [local, rest] = splitProps(props, ['count', 'open', 'children'])\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={local.count ? `${local.count} attached, add an attachment` : 'Add an attachment'}\n aria-expanded={local.open}\n class=\"text-muted-foreground\"\n {...rest}\n >\n {local.children ?? <Paperclip />}\n </Button>\n )\n}\n\n/** The hint a composer shows when the field is empty, as a one-line prompt. */\nexport function ComposerHint(props: ComponentProps<'p'>) {\n const [local, rest] = splitProps(props, ['class', 'children'])\n return (\n <p\n class={cn('text-muted-foreground flex items-center gap-1.5 text-2xs', local.class)}\n {...rest}\n >\n {local.children ?? (\n <>\n <CornerDownLeft aria-hidden=\"true\" class=\"size-3\" />\n <Kbd>Enter</Kbd> to send\n </>\n )}\n </p>\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"message-composer.js","names":["CornerDownLeft","Paperclip","Send","X","ComponentProps","JSX","Show","createSignal","splitProps","cn","Button","Kbd","Spinner","Textarea","createComposerImeGuard","MessageComposerProps","Omit","value","onValueChange","onSubmit","Promise","placeholder","disabled","submitLabel","showHint","leading","Element","trailing","menu","replyTo","label","onDismiss","MessageComposer","props","local","rest","sending","setSending","error","setError","ime","canSend","trim","length","send","onKeyDown","EventHandlerUnion","HTMLTextAreaElement","KeyboardEvent","event","defaultPrevented","key","shiftKey","metaKey","ctrlKey","claimEnter","_el$","_tmpl$3","_el$3","firstChild","_el$4","_el$5","_el$10","nextSibling","addEventListener","preventDefault","_$spread","_$mergeProps","class","_$insert","_$createComponent","when","children","_el$2","_tmpl$","reply","_el$11","_tmpl$4","_el$12","_el$13","type","variant","size","onClick","onCompositionStart","onCompositionEnd","onFocus","onBlur","onInput","currentTarget","rows","aria-label","_$memo","fallback","_el$6","_tmpl$2","_el$7","_el$8","_el$9","_el$1","_el$0","message","_el$14","_tmpl$5","_$effect","_$className","ComposerAttachmentButton","count","open","aria-expanded","ComposerHint","_el$15","_tmpl$6","_$mergeProps","_$createComponent","_$memo","_$className"],"sources":["../../../src/components/conversation/message-composer.tsx"],"sourcesContent":["import { CornerDownLeft, Paperclip, Send, X } from 'lucide-solid'\nimport type { ComponentProps, JSX } from 'solid-js'\nimport { Show, createSignal, splitProps } from 'solid-js'\nimport { cn } from '#lib/utils'\nimport { Button } from '../ui/button/button'\nimport { Kbd } from '../ui/kbd/kbd'\nimport { Spinner } from '../ui/spinner/spinner'\nimport { Textarea } from '../ui/textarea/textarea'\nimport { createComposerImeGuard } from './ime-guard'\n\n/**\n * MessageComposer.\n *\n * The field a message is written in. It is a form, and it behaves like one: Enter\n * sends, Shift+Enter breaks the line, Escape clears the error.\n *\n * ## What it does not know\n *\n * No mention model, no attachment model, no dictation, no idempotency keys. The\n * first version of this composer was written against adea's agent and artifact\n * types, which meant it could only ever be used by adea. Here the attachment control\n * is a *slot*, the mention menu is a slot, and a caller that has an agent model\n * renders one; a caller that does not gets a composer without it.\n *\n * ## The three things it does own\n *\n * **Enter sends, Shift+Enter breaks the line.** That is the convention every chat\n * application shares, and getting it wrong is the single most irritating thing a\n * composer can do. `⌘`/`Ctrl`+Enter is accepted too, because someone arriving from an\n * editor will try it.\n *\n * **The send button is disabled when there is nothing to send.** A control that\n * accepts a press and does nothing is worse than one that says it cannot.\n *\n * **The status region is `aria-live`.** \"Sending…\" and a failure both land there, so\n * a screen reader hears the outcome without the composer stealing focus.\n *\n * Enter ownership follows KiroCrew's pinned IME latch: candidate commits keep\n * their native default action, and the post-composition Enter cannot send or\n * insert an accidental newline. Composition abandoned by focus/blur recovers;\n * the Solid owner's cleanup settles its timer. See ime-guard.ts for provenance.\n */\nexport type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {\n /** The draft. Controlled: the caller owns the text, because it usually persists. */\n value: string\n onValueChange: (value: string) => void\n /** Called on send. A rejection is surfaced as an error and the draft is kept. */\n onSubmit: () => void | Promise<void>\n placeholder?: string\n /** Disable the whole composer, e.g. while the channel is read-only. */\n disabled?: boolean\n /** Label the send action for a context other than a message, e.g. a reply. */\n submitLabel?: string\n /** Show the keyboard hint under the field. */\n showHint?: boolean\n /** A leading control group, e.g. an attach button. */\n leading?: JSX.Element\n /** A trailing group before the send button, e.g. a dictation toggle. */\n trailing?: JSX.Element\n /** A menu rendered above the field, e.g. mention suggestions. */\n menu?: JSX.Element\n /** The reply target, drawn as a strip above the field. */\n replyTo?: { label: string; onDismiss: () => void }\n}\n\nexport function MessageComposer(props: MessageComposerProps) {\n const [local, rest] = splitProps(props, [\n 'class',\n 'value',\n 'onValueChange',\n 'onSubmit',\n 'placeholder',\n 'disabled',\n 'submitLabel',\n 'showHint',\n 'leading',\n 'trailing',\n 'menu',\n 'replyTo',\n ])\n\n const [sending, setSending] = createSignal(false)\n const [error, setError] = createSignal<string | null>(null)\n const ime = createComposerImeGuard()\n\n const canSend = () => local.value.trim().length > 0 && !local.disabled && !sending()\n\n const send = async () => {\n if (!canSend()) return\n setSending(true)\n setError(null)\n try {\n await local.onSubmit()\n } catch {\n // The draft is deliberately left in place. A send that fails and clears the\n // field loses the user's work, which is the worst outcome available here.\n setError('Message not sent. Your draft is still here; retry when the connection recovers.')\n } finally {\n setSending(false)\n }\n }\n\n const onKeyDown: JSX.EventHandlerUnion<HTMLTextAreaElement, KeyboardEvent> = (event) => {\n if (event.defaultPrevented) return\n if (event.key === 'Escape' && error()) {\n setError(null)\n return\n }\n if (event.key !== 'Enter') return\n // Shift breaks the line; a bare Enter, or a modified one, sends.\n if (event.shiftKey && !event.metaKey && !event.ctrlKey) return\n if (!ime.claimEnter(event)) return\n void send()\n }\n\n return (\n <form\n data-slot=\"message-composer\"\n class={cn('flex flex-col gap-2', local.class)}\n onSubmit={(event) => {\n event.preventDefault()\n void send()\n }}\n {...rest}\n >\n <Show when={local.menu}>\n <div>{local.menu}</div>\n </Show>\n\n <Show when={local.replyTo}>\n {(reply) => (\n <div class=\"bg-muted text-muted-foreground flex items-center gap-2 rounded-md px-2.5 py-1.5 text-xs\">\n <span class=\"min-w-0 flex-1 truncate\">Replying to {reply().label}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-2xs\"\n aria-label=\"Cancel reply\"\n onClick={() => reply().onDismiss()}\n >\n <X />\n </Button>\n </div>\n )}\n </Show>\n\n <div\n class={cn(\n 'border-input bg-card flex flex-col gap-1 rounded-lg border p-2',\n 'transition-[color,box-shadow,border-color] ease-out',\n 'focus-within:border-ring focus-within:ring-3 focus-within:ring-primary-subtle',\n local.disabled && 'opacity-50'\n )}\n >\n <Textarea\n onCompositionStart={ime.onCompositionStart}\n onCompositionEnd={ime.onCompositionEnd}\n onFocus={ime.onFocus}\n onBlur={ime.onBlur}\n value={local.value}\n onInput={(event) => local.onValueChange(event.currentTarget.value)}\n onKeyDown={onKeyDown}\n placeholder={local.placeholder ?? 'Write a message…'}\n disabled={local.disabled}\n rows={1}\n aria-label={local.submitLabel ? `${local.submitLabel} message` : 'Message'}\n class=\"min-h-9 resize-none border-0 bg-transparent p-0 text-sm shadow-none focus-visible:ring-0\"\n />\n\n <div class=\"flex items-center gap-1\">\n <Show when={local.leading}>{local.leading}</Show>\n <div class=\"ms-auto flex items-center gap-1\">\n <Show when={local.trailing}>{local.trailing}</Show>\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n disabled={!canSend()}\n aria-label={sending() ? 'Sending message' : 'Send message'}\n >\n <Show when={sending()} fallback={<Send />}>\n <Spinner size=\"sm\" label={false} class=\"text-primary-foreground\" />\n </Show>\n </Button>\n </div>\n </div>\n </div>\n\n <Show when={local.showHint}>\n <p class=\"text-muted-foreground flex items-center gap-1.5 text-2xs\">\n <Kbd>Enter</Kbd> to send\n <span aria-hidden=\"true\">·</span>\n <Kbd>Shift</Kbd> <Kbd>Enter</Kbd> for a new line\n </p>\n </Show>\n\n {/* A live region, so the outcome is announced without moving focus out of the\n field the user is typing in. */}\n <div aria-live=\"polite\" class=\"min-h-0\">\n <Show when={error()}>\n {(message) => <p class=\"text-destructive text-xs\">{message()}</p>}\n </Show>\n </div>\n </form>\n )\n}\n\n/**\n * The attachment control.\n *\n * A slot rather than a built-in, because what can be attached is the application's\n * business — but the *button* is shared, so it looks and behaves the same in both.\n * The count is announced, since a paperclip alone does not say whether anything is\n * attached.\n */\nexport function ComposerAttachmentButton(\n props: ComponentProps<typeof Button> & { count?: number; open?: boolean }\n) {\n const [local, rest] = splitProps(props, ['count', 'open', 'children'])\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={local.count ? `${local.count} attached, add an attachment` : 'Add an attachment'}\n aria-expanded={local.open}\n class=\"text-muted-foreground\"\n {...rest}\n >\n {local.children ?? <Paperclip />}\n </Button>\n )\n}\n\n/** The hint a composer shows when the field is empty, as a one-line prompt. */\nexport function ComposerHint(props: ComponentProps<'p'>) {\n const [local, rest] = splitProps(props, ['class', 'children'])\n return (\n <p\n class={cn('text-muted-foreground flex items-center gap-1.5 text-2xs', local.class)}\n {...rest}\n >\n {local.children ?? (\n <>\n <CornerDownLeft aria-hidden=\"true\" class=\"size-3\" />\n <Kbd>Enter</Kbd> to send\n </>\n )}\n </p>\n )\n}\n"],"mappings":"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"}
|
package/dist/r/conversation.json
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
"type": "registry:ui",
|
|
26
26
|
"target": "components/conversation/conversation-surface.tsx"
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
"path": "src/components/conversation/ime-guard.ts",
|
|
30
|
+
"type": "registry:ui",
|
|
31
|
+
"target": "components/conversation/ime-guard.ts"
|
|
32
|
+
},
|
|
28
33
|
{
|
|
29
34
|
"path": "src/components/conversation/index.ts",
|
|
30
35
|
"type": "registry:ui",
|
package/dist/r/registry.json
CHANGED
|
@@ -1798,6 +1798,11 @@
|
|
|
1798
1798
|
"type": "registry:ui",
|
|
1799
1799
|
"target": "components/conversation/conversation-surface.tsx"
|
|
1800
1800
|
},
|
|
1801
|
+
{
|
|
1802
|
+
"path": "src/components/conversation/ime-guard.ts",
|
|
1803
|
+
"type": "registry:ui",
|
|
1804
|
+
"target": "components/conversation/ime-guard.ts"
|
|
1805
|
+
},
|
|
1801
1806
|
{
|
|
1802
1807
|
"path": "src/components/conversation/index.ts",
|
|
1803
1808
|
"type": "registry:ui",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Substantially translated from KiroCrew website/src/hooks/useImeGuard.ts
|
|
3
|
+
* (`createImeLatch` and the textarea-scoped `useImeGuard` binding), pinned at
|
|
4
|
+
* 283e136c0f902e965a535a7c9548c57c7504fed0.
|
|
5
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
6
|
+
* This product includes software developed at Amazon.com, Inc.
|
|
7
|
+
* (https://www.amazon.com/).
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*
|
|
19
|
+
* Native events replace React synthetic propagation; Solid ownership replaces
|
|
20
|
+
* React refs/effects. No document listeners or focus trap are introduced.
|
|
21
|
+
* A repeated compositionend replaces its old timer as well as a new start,
|
|
22
|
+
* so an older expiry cannot prematurely release the new commit window.
|
|
23
|
+
*/
|
|
24
|
+
import { onCleanup } from 'solid-js'
|
|
25
|
+
|
|
26
|
+
const POST_COMPOSITION_MS = 50
|
|
27
|
+
|
|
28
|
+
/** Internal textarea guard: no application session or draft authority. */
|
|
29
|
+
export function createComposerImeGuard() {
|
|
30
|
+
let latched = false
|
|
31
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
32
|
+
const reset = () => {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
timer = undefined
|
|
35
|
+
latched = false
|
|
36
|
+
}
|
|
37
|
+
onCleanup(reset)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
onCompositionStart() {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
timer = undefined
|
|
43
|
+
latched = true
|
|
44
|
+
},
|
|
45
|
+
onCompositionEnd() {
|
|
46
|
+
clearTimeout(timer)
|
|
47
|
+
latched = true
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
latched = false
|
|
50
|
+
timer = undefined
|
|
51
|
+
}, POST_COMPOSITION_MS)
|
|
52
|
+
},
|
|
53
|
+
onFocus: reset,
|
|
54
|
+
onBlur: reset,
|
|
55
|
+
/** Claim only a key the composer already recognizes as its send binding. */
|
|
56
|
+
claimEnter(event: KeyboardEvent) {
|
|
57
|
+
const nativeComposition = event.isComposing || event.keyCode === 229
|
|
58
|
+
// Native IME commits retain their default action. Only the latch-only
|
|
59
|
+
// post-commit window suppresses the newline a plain Enter would insert.
|
|
60
|
+
if (!nativeComposition) event.preventDefault()
|
|
61
|
+
if (latched || nativeComposition) {
|
|
62
|
+
event.stopPropagation()
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Button } from '../ui/button/button'
|
|
|
6
6
|
import { Kbd } from '../ui/kbd/kbd'
|
|
7
7
|
import { Spinner } from '../ui/spinner/spinner'
|
|
8
8
|
import { Textarea } from '../ui/textarea/textarea'
|
|
9
|
+
import { createComposerImeGuard } from './ime-guard'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* MessageComposer.
|
|
@@ -33,6 +34,11 @@ import { Textarea } from '../ui/textarea/textarea'
|
|
|
33
34
|
*
|
|
34
35
|
* **The status region is `aria-live`.** "Sending…" and a failure both land there, so
|
|
35
36
|
* a screen reader hears the outcome without the composer stealing focus.
|
|
37
|
+
*
|
|
38
|
+
* Enter ownership follows KiroCrew's pinned IME latch: candidate commits keep
|
|
39
|
+
* their native default action, and the post-composition Enter cannot send or
|
|
40
|
+
* insert an accidental newline. Composition abandoned by focus/blur recovers;
|
|
41
|
+
* the Solid owner's cleanup settles its timer. See ime-guard.ts for provenance.
|
|
36
42
|
*/
|
|
37
43
|
export type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {
|
|
38
44
|
/** The draft. Controlled: the caller owns the text, because it usually persists. */
|
|
@@ -75,6 +81,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
75
81
|
|
|
76
82
|
const [sending, setSending] = createSignal(false)
|
|
77
83
|
const [error, setError] = createSignal<string | null>(null)
|
|
84
|
+
const ime = createComposerImeGuard()
|
|
78
85
|
|
|
79
86
|
const canSend = () => local.value.trim().length > 0 && !local.disabled && !sending()
|
|
80
87
|
|
|
@@ -94,6 +101,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
const onKeyDown: JSX.EventHandlerUnion<HTMLTextAreaElement, KeyboardEvent> = (event) => {
|
|
104
|
+
if (event.defaultPrevented) return
|
|
97
105
|
if (event.key === 'Escape' && error()) {
|
|
98
106
|
setError(null)
|
|
99
107
|
return
|
|
@@ -101,7 +109,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
101
109
|
if (event.key !== 'Enter') return
|
|
102
110
|
// Shift breaks the line; a bare Enter, or a modified one, sends.
|
|
103
111
|
if (event.shiftKey && !event.metaKey && !event.ctrlKey) return
|
|
104
|
-
|
|
112
|
+
if (!ime.claimEnter(event)) return
|
|
105
113
|
void send()
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -145,6 +153,10 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
145
153
|
)}
|
|
146
154
|
>
|
|
147
155
|
<Textarea
|
|
156
|
+
onCompositionStart={ime.onCompositionStart}
|
|
157
|
+
onCompositionEnd={ime.onCompositionEnd}
|
|
158
|
+
onFocus={ime.onFocus}
|
|
159
|
+
onBlur={ime.onBlur}
|
|
148
160
|
value={local.value}
|
|
149
161
|
onInput={(event) => local.onValueChange(event.currentTarget.value)}
|
|
150
162
|
onKeyDown={onKeyDown}
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -1798,6 +1798,11 @@
|
|
|
1798
1798
|
"type": "registry:ui",
|
|
1799
1799
|
"target": "components/conversation/conversation-surface.tsx"
|
|
1800
1800
|
},
|
|
1801
|
+
{
|
|
1802
|
+
"path": "src/components/conversation/ime-guard.ts",
|
|
1803
|
+
"type": "registry:ui",
|
|
1804
|
+
"target": "components/conversation/ime-guard.ts"
|
|
1805
|
+
},
|
|
1801
1806
|
{
|
|
1802
1807
|
"path": "src/components/conversation/index.ts",
|
|
1803
1808
|
"type": "registry:ui",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Substantially translated from KiroCrew website/src/hooks/useImeGuard.ts
|
|
3
|
+
* (`createImeLatch` and the textarea-scoped `useImeGuard` binding), pinned at
|
|
4
|
+
* 283e136c0f902e965a535a7c9548c57c7504fed0.
|
|
5
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
6
|
+
* This product includes software developed at Amazon.com, Inc.
|
|
7
|
+
* (https://www.amazon.com/).
|
|
8
|
+
*
|
|
9
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
10
|
+
* you may not use this file except in compliance with the License.
|
|
11
|
+
* You may obtain a copy of the License at
|
|
12
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*
|
|
19
|
+
* Native events replace React synthetic propagation; Solid ownership replaces
|
|
20
|
+
* React refs/effects. No document listeners or focus trap are introduced.
|
|
21
|
+
* A repeated compositionend replaces its old timer as well as a new start,
|
|
22
|
+
* so an older expiry cannot prematurely release the new commit window.
|
|
23
|
+
*/
|
|
24
|
+
import { onCleanup } from 'solid-js'
|
|
25
|
+
|
|
26
|
+
const POST_COMPOSITION_MS = 50
|
|
27
|
+
|
|
28
|
+
/** Internal textarea guard: no application session or draft authority. */
|
|
29
|
+
export function createComposerImeGuard() {
|
|
30
|
+
let latched = false
|
|
31
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
32
|
+
const reset = () => {
|
|
33
|
+
clearTimeout(timer)
|
|
34
|
+
timer = undefined
|
|
35
|
+
latched = false
|
|
36
|
+
}
|
|
37
|
+
onCleanup(reset)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
onCompositionStart() {
|
|
41
|
+
clearTimeout(timer)
|
|
42
|
+
timer = undefined
|
|
43
|
+
latched = true
|
|
44
|
+
},
|
|
45
|
+
onCompositionEnd() {
|
|
46
|
+
clearTimeout(timer)
|
|
47
|
+
latched = true
|
|
48
|
+
timer = setTimeout(() => {
|
|
49
|
+
latched = false
|
|
50
|
+
timer = undefined
|
|
51
|
+
}, POST_COMPOSITION_MS)
|
|
52
|
+
},
|
|
53
|
+
onFocus: reset,
|
|
54
|
+
onBlur: reset,
|
|
55
|
+
/** Claim only a key the composer already recognizes as its send binding. */
|
|
56
|
+
claimEnter(event: KeyboardEvent) {
|
|
57
|
+
const nativeComposition = event.isComposing || event.keyCode === 229
|
|
58
|
+
// Native IME commits retain their default action. Only the latch-only
|
|
59
|
+
// post-commit window suppresses the newline a plain Enter would insert.
|
|
60
|
+
if (!nativeComposition) event.preventDefault()
|
|
61
|
+
if (latched || nativeComposition) {
|
|
62
|
+
event.stopPropagation()
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Button } from '../ui/button/button'
|
|
|
6
6
|
import { Kbd } from '../ui/kbd/kbd'
|
|
7
7
|
import { Spinner } from '../ui/spinner/spinner'
|
|
8
8
|
import { Textarea } from '../ui/textarea/textarea'
|
|
9
|
+
import { createComposerImeGuard } from './ime-guard'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* MessageComposer.
|
|
@@ -33,6 +34,11 @@ import { Textarea } from '../ui/textarea/textarea'
|
|
|
33
34
|
*
|
|
34
35
|
* **The status region is `aria-live`.** "Sending…" and a failure both land there, so
|
|
35
36
|
* a screen reader hears the outcome without the composer stealing focus.
|
|
37
|
+
*
|
|
38
|
+
* Enter ownership follows KiroCrew's pinned IME latch: candidate commits keep
|
|
39
|
+
* their native default action, and the post-composition Enter cannot send or
|
|
40
|
+
* insert an accidental newline. Composition abandoned by focus/blur recovers;
|
|
41
|
+
* the Solid owner's cleanup settles its timer. See ime-guard.ts for provenance.
|
|
36
42
|
*/
|
|
37
43
|
export type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {
|
|
38
44
|
/** The draft. Controlled: the caller owns the text, because it usually persists. */
|
|
@@ -75,6 +81,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
75
81
|
|
|
76
82
|
const [sending, setSending] = createSignal(false)
|
|
77
83
|
const [error, setError] = createSignal<string | null>(null)
|
|
84
|
+
const ime = createComposerImeGuard()
|
|
78
85
|
|
|
79
86
|
const canSend = () => local.value.trim().length > 0 && !local.disabled && !sending()
|
|
80
87
|
|
|
@@ -94,6 +101,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
94
101
|
}
|
|
95
102
|
|
|
96
103
|
const onKeyDown: JSX.EventHandlerUnion<HTMLTextAreaElement, KeyboardEvent> = (event) => {
|
|
104
|
+
if (event.defaultPrevented) return
|
|
97
105
|
if (event.key === 'Escape' && error()) {
|
|
98
106
|
setError(null)
|
|
99
107
|
return
|
|
@@ -101,7 +109,7 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
101
109
|
if (event.key !== 'Enter') return
|
|
102
110
|
// Shift breaks the line; a bare Enter, or a modified one, sends.
|
|
103
111
|
if (event.shiftKey && !event.metaKey && !event.ctrlKey) return
|
|
104
|
-
|
|
112
|
+
if (!ime.claimEnter(event)) return
|
|
105
113
|
void send()
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -145,6 +153,10 @@ export function MessageComposer(props: MessageComposerProps) {
|
|
|
145
153
|
)}
|
|
146
154
|
>
|
|
147
155
|
<Textarea
|
|
156
|
+
onCompositionStart={ime.onCompositionStart}
|
|
157
|
+
onCompositionEnd={ime.onCompositionEnd}
|
|
158
|
+
onFocus={ime.onFocus}
|
|
159
|
+
onBlur={ime.onBlur}
|
|
148
160
|
value={local.value}
|
|
149
161
|
onInput={(event) => local.onValueChange(event.currentTarget.value)}
|
|
150
162
|
onKeyDown={onKeyDown}
|