@adea-ai/ui 0.63.0 → 0.63.2

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 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;AAK5C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;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,eAqI1D;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"}
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
- event.preventDefault();
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":";;;;;;;;;;;;;;;;AAiEA,SAAgBgC,gBAAgBC,OAA6B;CAC3D,MAAM,CAACC,OAAOC,QAAQ3B,WAAWyB,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAS,CACV;CAED,MAAM,CAACG,SAASC,cAAc9B,aAAa,KAAK;CAChD,MAAM,CAAC+B,OAAOC,YAAYhC,aAA4B,IAAI;CAC1D,MAAMiC,MAAM1B,uBAAuB;CAEnC,MAAM2B,gBAAgBP,MAAMjB,MAAMyB,KAAK,CAAC,CAACC,SAAS,KAAK,CAACT,MAAMZ,YAAY,CAACc,QAAQ;CAEnF,MAAMQ,OAAO,YAAY;EACvB,IAAI,CAACH,QAAQ,GAAG;EAChBJ,WAAW,IAAI;EACfE,SAAS,IAAI;EACb,IAAI;GACF,MAAML,MAAMf,SAAS;EACvB,QAAQ;GAGNoB,SAAS,iFAAiF;EAC5F,UAAU;GACRF,WAAW,KAAK;EAClB;CACF;CAEA,MAAMQ,aAAwEI,UAAU;EACtF,IAAIA,MAAMC,kBAAkB;EAC5B,IAAID,MAAME,QAAQ,YAAYb,MAAM,GAAG;GACrCC,SAAS,IAAI;GACb;EACF;EACA,IAAIU,MAAME,QAAQ,SAAS;EAE3B,IAAIF,MAAMG,YAAY,CAACH,MAAMI,WAAW,CAACJ,MAAMK,SAAS;EACxD,IAAI,CAACd,IAAIe,WAAWN,KAAK,GAAG;EAC5B,KAAU;CACZ;CAEA,cAAA;EAAA,IAAAO,OAAAC,QAAA,GAAAC,QAAAF,KAAAG,YAAAC,QAAAF,MAAAC,YAAAE,QAAAD,MAAAD,YAAAG,SAAAJ,MAAAK;EAAAP,KAAAQ,iBAAA,WAIef,UAAU;GACnBA,MAAMgB,eAAe;GACrB,KAAU;EACZ,CAAC;EAAAC,OAAAV,MAAAW,WAAA,EAAA,KAAA,WAAA;GAAA,OAJM1D,GAAG,uBAAuByB,MAAMkC,KAAK;EAAC,EAAA,GAKzCjC,IAAI,GAAA,OAAA,IAAA;EAAAkC,OAAAb,MAAAc,gBAEPhE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAErC,MAAMN;GAAI;GAAA,IAAA4C,WAAA;IAAA,IAAAC,QAAAC,OAAA;IAAAL,OAAAI,aACdvC,MAAMN,IAAI;IAAA,OAAA6C;GAAA;EAAA,CAAA,GAAAf,KAAA;EAAAW,OAAAb,MAAAc,gBAGjBhE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAErC,MAAML;GAAO;GAAA2C,WACrBG,iBAAK;IAAA,IAAAC,SAAAC,QAAA,GAAAC,SAAAF,OAAAjB;IAAAmB,OAAAnB;IAAAU,OAAAS,cAEgDH,MAAM,CAAC,CAAC7C,OAAK,IAAA;IAAAuC,OAAAO,QAAAN,gBAC/D5D,QAAM;KACLsE,MAAI;KACJC,SAAO;KACPC,MAAI;KAAA,cAAA;KAEJC,eAAeR,MAAM,CAAC,CAAC5C,UAAU;KAAC,IAAAyC,WAAA;MAAA,OAAAF,gBAEjCnE,GAAC,CAAA,CAAA;KAAA;IAAA,CAAA,GAAA,IAAA;IAAA,OAAAyE;GAAA,EAAA,CAAA;EAGP,CAAA,GAAAlB,KAAA;EAAAW,OAAAX,OAAAY,gBAWAzD,UAAQ;GAAA,IACPuE,qBAAkB;IAAA,OAAE5C,IAAI4C;GAAkB;GAAA,IAC1CC,mBAAgB;IAAA,OAAE7C,IAAI6C;GAAgB;GAAA,IACtCC,UAAO;IAAA,OAAE9C,IAAI8C;GAAO;GAAA,IACpBC,SAAM;IAAA,OAAE/C,IAAI+C;GAAM;GAAA,IAClBtE,QAAK;IAAA,OAAEiB,MAAMjB;GAAK;GAClBuE,UAAUvC,UAAUf,MAAMhB,cAAc+B,MAAMwC,cAAcxE,KAAK;GACtD4B;GAAS,IACpBxB,cAAW;IAAA,OAAEa,MAAMb,eAAe;GAAkB;GAAA,IACpDC,WAAQ;IAAA,OAAEY,MAAMZ;GAAQ;GACxBoE,MAAM;GAAC,KAAA,gBAAA;IAAA,OACKE,WAAA,CAAA,CAAA1D,MAAMX,WAAW,CAAA,CAAA,IAAG,GAAGW,MAAMX,YAAW,YAAa;GAAS;GAAA,SAAA;EAAA,CAAA,GAAAqC,KAAA;EAAAS,OAAAT,OAAAU,gBAKzEhE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAErC,MAAMT;GAAO;GAAA,IAAA+C,WAAA;IAAA,OAAGtC,MAAMT;GAAO;EAAA,CAAA,GAAAoC,KAAA;EAAAQ,OAAAR,OAAAS,gBAEtChE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAErC,MAAMP;GAAQ;GAAA,IAAA6C,WAAA;IAAA,OAAGtC,MAAMP;GAAQ;EAAA,CAAA,GAAA,IAAA;EAAA0C,OAAAR,OAAAS,gBAC1C5D,QAAM;GACLsE,MAAI;GACJE,MAAI;GAAA,IACJ5D,WAAQ;IAAA,OAAE,CAACmB,QAAQ;GAAC;GAAA,KAAA,gBAAA;IAAA,OACRL,QAAQ,IAAI,oBAAoB;GAAc;GAAA,IAAAoC,WAAA;IAAA,OAAAF,gBAEzDhE,MAAI;KAAA,IAACiE,OAAI;MAAA,OAAEnC,QAAQ;KAAC;KAAA,IAAEyD,WAAQ;MAAA,OAAAvB,gBAAGpE,MAAI,CAAA,CAAA;KAAA;KAAA,IAAAsE,WAAA;MAAA,OAAAF,gBACnC1D,SAAO;OAACsE,MAAI;OAAMpD,OAAO;OAAK,SAAA;MAAA,CAAA;KAAA;IAAA,CAAA;GAAA;EAAA,CAAA,GAAA,IAAA;EAAAuC,OAAAb,MAAAc,gBAOxChE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAErC,MAAMV;GAAQ;GAAA,IAAAgD,WAAA;IAAA,IAAAsB,QAAAC,QAAA,GAAAC,QAAAF,MAAAnC,YAAAuC,QAAAF,MAAAjC,YAAAA,aAAAoC,QAAAD,MAAAnC;IAAAoC,MAAApC;IAAAM,OAAAyB,OAAAxB,gBAErB3D,KAAG,EAAA6D,UAAA,QAAA,CAAA,GAAAwB,KAAA;IAAA3B,OAAAyB,OAAAxB,gBAEH3D,KAAG,EAAA6D,UAAA,QAAA,CAAA,GAAA0B,KAAA;IAAA7B,OAAAyB,OAAAxB,gBAAc3D,KAAG,EAAA6D,UAAA,QAAA,CAAA,GAAA2B,KAAA;IAAA,OAAAL;GAAA;EAAA,CAAA,GAAAhC,MAAA;EAAAO,OAAAP,QAAAQ,gBAOtBhE,MAAI;GAAA,IAACiE,OAAI;IAAA,OAAEjC,MAAM;GAAC;GAAAkC,WACf6B,mBAAO;IAAA,IAAAC,SAAAC,QAAA;IAAAlC,OAAAiC,QAA0CD,OAAO;IAAA,OAAAC;GAAA,EAAA,CAAA;EAAO,CAAA,CAAA;EAAAE,aAAAC,UAAA/C,OApD5DjD,GACL,kEACA,uDACA,iFACAyB,MAAMZ,YAAY,YACpB,CAAC,CAAA;EAAA,OAAAkC;CAAA,EAAA,CAAA;AAoDT;;;;;;;;;AAUA,SAAgBkD,yBACdzE,OACA;CACA,MAAM,CAACC,OAAOC,QAAQ3B,WAAWyB,OAAO;EAAC;EAAS;EAAQ;CAAU,CAAC;CAErE,OAAAqC,gBACG5D,QAAMyD,WAAA;EACLa,MAAI;EACJC,SAAO;EACPC,MAAI;EAAA,KAAA,gBAAA;GAAA,OACQU,WAAA,CAAA,CAAA1D,MAAMyE,KAAK,CAAA,CAAA,IAAG,GAAGzE,MAAMyE,MAAK,gCAAiC;EAAmB;EAAA,KAAA,mBAAA;GAAA,OAC7EzE,MAAM0E;EAAI;EAAA,SAAA;CAAA,GAErBzE,MAAI,EAAA,IAAAqC,WAAA;EAAA,OAEPtC,MAAMsC,YAAQF,gBAAKrE,WAAS,CAAA,CAAA;CAAG,EAAA,CAAA,CAAA;AAGtC;;AAGA,SAAgB6G,aAAa7E,OAA4B;CACvD,MAAM,CAACC,OAAOC,QAAQ3B,WAAWyB,OAAO,CAAC,SAAS,UAAU,CAAC;CAC7D,cAAA;EAAA,IAAA8E,SAAAC,QAAA;EAAA9C,OAAA6C,QAAA5C,WAAA,EAAA,KAAA,WAAA;GAAA,OAEW1D,GAAG,4DAA4DyB,MAAMkC,KAAK;EAAC,EAAA,GAC9EjC,IAAI,GAAA,OAAA,IAAA;EAAAkC,OAAA0C,cAEP7E,MAAMsC,YAAQ;GAAAF,gBAEVtE,gBAAc;IAAA,eAAA;IAAA,SAAA;GAAA,CAAA;GAAAsE,gBACd3D,KAAG,EAAA6D,UAAA,QAAA,CAAA;GAAA;EAAA,CAEP;EAAA,OAAAuC;CAAA,EAAA,CAAA;AAGP"}
@@ -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",
@@ -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
- event.preventDefault()
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.63.0",
3
+ "version": "0.63.2",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
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
- event.preventDefault()
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}