@adecore/agents-react 0.24.0 → 0.24.1

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.
@@ -39,6 +39,7 @@ import { useNotice } from './composer/use-notice.js';
39
39
  import { usePendingRequests } from './composer/use-pending-requests.js';
40
40
  import { usePromptHistory } from './composer/use-prompt-history.js';
41
41
  import { usePromptHold } from './composer/use-prompt-hold.js';
42
+ import { surfacePartOf } from './composer/surface.js';
42
43
  import { useTakeBack } from './composer/use-take-back.js';
43
44
  function sendLabelKey(busy, steerable, delivery) {
44
45
  if (!busy) {
@@ -121,29 +122,40 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
121
122
  setText: (next) => box.setText(next),
122
123
  submit
123
124
  });
125
+ const pointSurface = (event) => {
126
+ event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
127
+ };
128
+ const pressSurface = (event) => {
129
+ if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
130
+ return;
131
+ }
132
+ // Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
133
+ event.preventDefault();
134
+ inputRef.current?.setCaret(Infinity);
135
+ };
124
136
  return (_jsxs("div", { className: "chat-composer-content pointer-events-none relative z-10 w-full", children: [!atEnd && !readOnly && (_jsx("div", { className: "absolute inset-x-0 bottom-full mb-2 flex justify-center", children: _jsx(Tooltip, { label: t('composer.jumpToEnd'), name: true, children: _jsx(Surface, { render: _jsx("button", { onClick: () => scrollTimelineToEnd(chatId) }), className: "pointer-events-auto grid h-8 w-8 place-items-center rounded-full text-text-muted hover:text-text", children: _jsx(Icon, { icon: ChevronDown, size: 16 }) }) }) })), _jsx(ChatActivity, { chatId: chatId }), _jsx("div", { className: clsx('@container/composer pointer-events-auto flex flex-col overflow-hidden rounded-2xl border shadow-float backdrop-blur-[14px] focus-ring-within', files.dragging
125
137
  ? 'border-accent bg-[color-mix(in_srgb,var(--accent-soft)_60%,var(--surface-raised))]'
126
- : 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children: _jsxs(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !empty, denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), hold: hold, children: [_jsx(LimitDock, { chatId: chatId, info: info }), _jsx(GoalDock, { chatId: chatId, info: info, goals: capabilities?.goals }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: controls.compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), _jsx(ComposerMenu, { menus: menus }), queue.length > 0 && (_jsx(QueuedMessages, { chatId: chatId, queue: queue, paused: info.queuePaused === true, takingBack: takingBack !== null, onEdit: (message) => void takeBack(message) })), draft.quote !== '' && (_jsx(DraftQuote, { chatId: chatId, quote: draft.quote, source: draft.quoteSource, onRemove: box.removeQuote, onSourceGone: () => notice.setNotice(t('rows.user.quoteGone')) })), draft.chats.length > 0 && _jsx(DraftChats, { chats: draft.chats, onRemove: box.removeChat }), draft.attachments.length > 0 && _jsx(DraftAttachments, { attachments: draft.attachments, onRemove: box.removeAttachment }), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: draft.text, placeholder: placeholder, disabled: !writable, tabbable: focused, extensions: editorExtensions, aria: menus.inputAria, onChange: (value, selection, state) => {
127
- hold.noteInput();
128
- box.setText(value);
129
- menus.setMenuIndex(0);
130
- menus.trackTriggers(value, selection, state);
131
- history.edited(value);
132
- }, onSelectionChange: menus.trackTriggers, onBlur: () => {
133
- menus.close();
134
- hold.release('blur');
135
- }, onComposing: hold.setComposing, onKeyDown: onKeyDown, onPaste: files.onPaste }), notice.notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: notice.notice })), guard.visible && (_jsxs("div", { className: clsx('px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5', guard.tooLong ? 'text-status-error' : 'text-text-faint'), children: [formatNumber(guard.count), " / ", formatNumber(PROMPT_MAX_CHARS), guard.tooLong && ` ${t('composer.tooLong')}`] })), _jsx(ComposerControls, { chatId: chatId, info: info, providerFixed: providerFixed, writable: writable, attachable: capabilities?.attachments !== false, controls: controls, send: {
136
- visible: !busy || !empty,
137
- disabled: empty || !writable || guard.tooLong,
138
- label: t(sendLabelKey(busy, steerable, sendDelivery)),
139
- kbd: sendShortcut(sendKey, draft.text.includes('\n')),
140
- onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
141
- }, dictationToolbar: setDictationToolbar, onFiles: (picked) => {
142
- files.addFiles(picked);
143
- inputRef.current?.focus();
144
- }, onRestore: restoreStashed, insert: (added) => {
145
- if (writable) {
146
- inputRef.current?.insert(added);
147
- }
148
- } })] }) }), _jsx(PromptDialog, { open: controls.confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: controls.forceClearThread, onOpenChange: controls.cancelClear })] }));
138
+ : 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), ...files.dropTarget, children: _jsx(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !empty, denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), hold: hold, children: _jsxs("div", { className: clsx(writable && 'data-[text-cursor]:cursor-text'), onPointerOver: pointSurface, onMouseDown: pressSurface, children: [_jsx(LimitDock, { chatId: chatId, info: info }), _jsx(GoalDock, { chatId: chatId, info: info, goals: capabilities?.goals }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: controls.compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), _jsx(ComposerMenu, { menus: menus }), queue.length > 0 && (_jsx(QueuedMessages, { chatId: chatId, queue: queue, paused: info.queuePaused === true, takingBack: takingBack !== null, onEdit: (message) => void takeBack(message) })), draft.quote !== '' && (_jsx(DraftQuote, { chatId: chatId, quote: draft.quote, source: draft.quoteSource, onRemove: box.removeQuote, onSourceGone: () => notice.setNotice(t('rows.user.quoteGone')) })), draft.chats.length > 0 && _jsx(DraftChats, { chats: draft.chats, onRemove: box.removeChat }), draft.attachments.length > 0 && _jsx(DraftAttachments, { attachments: draft.attachments, onRemove: box.removeAttachment }), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: draft.text, placeholder: placeholder, disabled: !writable, tabbable: focused, extensions: editorExtensions, aria: menus.inputAria, onChange: (value, selection, state) => {
139
+ hold.noteInput();
140
+ box.setText(value);
141
+ menus.setMenuIndex(0);
142
+ menus.trackTriggers(value, selection, state);
143
+ history.edited(value);
144
+ }, onSelectionChange: menus.trackTriggers, onBlur: () => {
145
+ menus.close();
146
+ hold.release('blur');
147
+ }, onComposing: hold.setComposing, onKeyDown: onKeyDown, onPaste: files.onPaste }), notice.notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: notice.notice })), guard.visible && (_jsxs("div", { className: clsx('px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5', guard.tooLong ? 'text-status-error' : 'text-text-faint'), children: [formatNumber(guard.count), " / ", formatNumber(PROMPT_MAX_CHARS), guard.tooLong && ` ${t('composer.tooLong')}`] })), _jsx(ComposerControls, { chatId: chatId, info: info, providerFixed: providerFixed, writable: writable, attachable: capabilities?.attachments !== false, controls: controls, send: {
148
+ visible: !busy || !empty,
149
+ disabled: empty || !writable || guard.tooLong,
150
+ label: t(sendLabelKey(busy, steerable, sendDelivery)),
151
+ kbd: sendShortcut(sendKey, draft.text.includes('\n')),
152
+ onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
153
+ }, dictationToolbar: setDictationToolbar, onFiles: (picked) => {
154
+ files.addFiles(picked);
155
+ inputRef.current?.focus();
156
+ }, onRestore: restoreStashed, insert: (added) => {
157
+ if (writable) {
158
+ inputRef.current?.insert(added);
159
+ }
160
+ } })] }) }) }), _jsx(PromptDialog, { open: controls.confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: controls.forceClearThread, onOpenChange: controls.cancelClear })] }));
149
161
  }
@@ -0,0 +1 @@
1
+ export declare function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface';
@@ -0,0 +1,44 @@
1
+ /* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
2
+ const ACTIONS = [
3
+ 'a',
4
+ 'button',
5
+ 'input',
6
+ 'select',
7
+ 'textarea',
8
+ 'label',
9
+ 'summary',
10
+ '[contenteditable]:not([contenteditable="false"])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ '[role="button"]',
13
+ '[role="link"]',
14
+ '[role="menuitem"]',
15
+ '[role="menuitemcheckbox"]',
16
+ '[role="menuitemradio"]',
17
+ '[role="option"]',
18
+ '[role="listbox"]',
19
+ '[role="checkbox"]',
20
+ '[role="radio"]',
21
+ '[role="switch"]',
22
+ '[role="tab"]',
23
+ '[role="slider"]',
24
+ '[role="combobox"]',
25
+ '[data-composer-action]'
26
+ ].join(', ');
27
+ /*
28
+ * Where a pointer is on the composer: in the text field, on an action, or on the space around them,
29
+ * which stands in for the text field. A target outside the surface is an action, as an event from a
30
+ * popup portaled out of it still reaches the surface through React.
31
+ */
32
+ export function surfacePartOf(target, surface) {
33
+ // Not `instanceof Element`, which fails for an element of another window.
34
+ const element = target;
35
+ if (typeof element?.closest !== 'function' || !surface.contains(element)) {
36
+ return 'action';
37
+ }
38
+ const input = element.closest('.cm-editor');
39
+ if (input !== null && surface.contains(input)) {
40
+ return 'input';
41
+ }
42
+ const action = element.closest(ACTIONS);
43
+ return action !== null && surface.contains(action) ? 'action' : 'surface';
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -67,11 +67,11 @@
67
67
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
68
68
  },
69
69
  "dependencies": {
70
- "@adecore/agent-contracts": "0.24.0",
71
- "@adecore/editor": "0.24.0",
72
- "@adecore/editor-react": "0.24.0",
73
- "@adecore/intelligent-ui": "0.24.0",
74
- "@adecore/merge": "0.24.0",
70
+ "@adecore/agent-contracts": "0.24.1",
71
+ "@adecore/editor": "0.24.1",
72
+ "@adecore/editor-react": "0.24.1",
73
+ "@adecore/intelligent-ui": "0.24.1",
74
+ "@adecore/merge": "0.24.1",
75
75
  "@codemirror/commands": "^6.11.1",
76
76
  "@codemirror/language": "^6.12.4",
77
77
  "@codemirror/state": "^6.7.6",
@@ -93,14 +93,14 @@
93
93
  "zustand": "^5.0.15"
94
94
  },
95
95
  "peerDependencies": {
96
- "@adecore/ui": "^0.24.0",
96
+ "@adecore/ui": "^0.24.1",
97
97
  "i18next": "^26.4.2",
98
98
  "react": "^19.3.0",
99
99
  "react-dom": "^19.3.0",
100
100
  "react-i18next": "^17.0.15"
101
101
  },
102
102
  "devDependencies": {
103
- "@adecore/ui": "0.24.0",
103
+ "@adecore/ui": "0.24.1",
104
104
  "@types/bun": "^1.4.2",
105
105
  "@types/react": "^19.3.0",
106
106
  "@types/react-dom": "^19.3.0",
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
1
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, type MouseEvent, type PointerEvent } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown } from 'lucide-react';
@@ -40,6 +40,7 @@ import { useNotice } from './composer/use-notice';
40
40
  import { usePendingRequests } from './composer/use-pending-requests';
41
41
  import { usePromptHistory } from './composer/use-prompt-history';
42
42
  import { usePromptHold } from './composer/use-prompt-hold';
43
+ import { surfacePartOf } from './composer/surface';
43
44
  import { useTakeBack } from './composer/use-take-back';
44
45
 
45
46
  interface ComposerProps {
@@ -154,6 +155,19 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
154
155
  submit
155
156
  });
156
157
 
158
+ const pointSurface = (event: PointerEvent<HTMLDivElement>) => {
159
+ event.currentTarget.toggleAttribute('data-text-cursor', surfacePartOf(event.target, event.currentTarget) !== 'action');
160
+ };
161
+
162
+ const pressSurface = (event: MouseEvent<HTMLDivElement>) => {
163
+ if (!writable || event.button !== 0 || event.defaultPrevented || surfacePartOf(event.target, event.currentTarget) !== 'surface') {
164
+ return;
165
+ }
166
+ // Nothing around the field selects, so the default would only blur it on the way, closing its picker and lifting a held prompt card.
167
+ event.preventDefault();
168
+ inputRef.current?.setCaret(Infinity);
169
+ };
170
+
157
171
  return (
158
172
  <div className="chat-composer-content pointer-events-none relative z-10 w-full">
159
173
  {/* Over the composer rather than in the thread, since the composer's height is the one it always clears.
@@ -191,104 +205,106 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
191
205
  onAllAnswered={() => inputRef.current?.focus()}
192
206
  hold={hold}
193
207
  >
194
- <LimitDock chatId={chatId} info={info} />
195
- <GoalDock chatId={chatId} info={info} goals={capabilities?.goals} />
196
- {compactionOffer && (
197
- <ResumeCompactionDock
198
- tokens={compactionOffer.tokens}
199
- onCompact={controls.compact}
200
- onDismiss={() => dismissResumeCompaction(scope, chatId, compactionOffer.turnId)}
208
+ <div className={clsx(writable && 'data-[text-cursor]:cursor-text')} onPointerOver={pointSurface} onMouseDown={pressSurface}>
209
+ <LimitDock chatId={chatId} info={info} />
210
+ <GoalDock chatId={chatId} info={info} goals={capabilities?.goals} />
211
+ {compactionOffer && (
212
+ <ResumeCompactionDock
213
+ tokens={compactionOffer.tokens}
214
+ onCompact={controls.compact}
215
+ onDismiss={() => dismissResumeCompaction(scope, chatId, compactionOffer.turnId)}
216
+ />
217
+ )}
218
+ <ComposerMenu menus={menus} />
219
+ {queue.length > 0 && (
220
+ <QueuedMessages
221
+ chatId={chatId}
222
+ queue={queue}
223
+ paused={info.queuePaused === true}
224
+ takingBack={takingBack !== null}
225
+ onEdit={(message) => void takeBack(message)}
226
+ />
227
+ )}
228
+ {draft.quote !== '' && (
229
+ <DraftQuote
230
+ chatId={chatId}
231
+ quote={draft.quote}
232
+ source={draft.quoteSource}
233
+ onRemove={box.removeQuote}
234
+ onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
235
+ />
236
+ )}
237
+ {draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
238
+ {draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
239
+ <ComposerInput
240
+ ref={inputRef}
241
+ dictationToolbar={dictationToolbar}
242
+ className="composer-input select-text text-sm leading-normal text-text"
243
+ value={draft.text}
244
+ placeholder={placeholder}
245
+ disabled={!writable}
246
+ tabbable={focused}
247
+ extensions={editorExtensions}
248
+ aria={menus.inputAria}
249
+ onChange={(value, selection, state) => {
250
+ hold.noteInput();
251
+ box.setText(value);
252
+ menus.setMenuIndex(0);
253
+ menus.trackTriggers(value, selection, state);
254
+ history.edited(value);
255
+ }}
256
+ onSelectionChange={menus.trackTriggers}
257
+ onBlur={() => {
258
+ menus.close();
259
+ hold.release('blur');
260
+ }}
261
+ onComposing={hold.setComposing}
262
+ onKeyDown={onKeyDown}
263
+ onPaste={files.onPaste}
201
264
  />
202
- )}
203
- <ComposerMenu menus={menus} />
204
- {queue.length > 0 && (
205
- <QueuedMessages
265
+ {notice.notice && (
266
+ <div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
267
+ {notice.notice}
268
+ </div>
269
+ )}
270
+ {guard.visible && (
271
+ <div
272
+ className={clsx(
273
+ 'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
274
+ guard.tooLong ? 'text-status-error' : 'text-text-faint'
275
+ )}
276
+ >
277
+ {formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
278
+ {guard.tooLong && ` ${t('composer.tooLong')}`}
279
+ </div>
280
+ )}
281
+ <ComposerControls
206
282
  chatId={chatId}
207
- queue={queue}
208
- paused={info.queuePaused === true}
209
- takingBack={takingBack !== null}
210
- onEdit={(message) => void takeBack(message)}
283
+ info={info}
284
+ providerFixed={providerFixed}
285
+ writable={writable}
286
+ attachable={capabilities?.attachments !== false}
287
+ controls={controls}
288
+ send={{
289
+ visible: !busy || !empty,
290
+ disabled: empty || !writable || guard.tooLong,
291
+ label: t(sendLabelKey(busy, steerable, sendDelivery)),
292
+ kbd: sendShortcut(sendKey, draft.text.includes('\n')),
293
+ onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
294
+ }}
295
+ dictationToolbar={setDictationToolbar}
296
+ onFiles={(picked) => {
297
+ files.addFiles(picked);
298
+ inputRef.current?.focus();
299
+ }}
300
+ onRestore={restoreStashed}
301
+ insert={(added) => {
302
+ if (writable) {
303
+ inputRef.current?.insert(added);
304
+ }
305
+ }}
211
306
  />
212
- )}
213
- {draft.quote !== '' && (
214
- <DraftQuote
215
- chatId={chatId}
216
- quote={draft.quote}
217
- source={draft.quoteSource}
218
- onRemove={box.removeQuote}
219
- onSourceGone={() => notice.setNotice(t('rows.user.quoteGone'))}
220
- />
221
- )}
222
- {draft.chats.length > 0 && <DraftChats chats={draft.chats} onRemove={box.removeChat} />}
223
- {draft.attachments.length > 0 && <DraftAttachments attachments={draft.attachments} onRemove={box.removeAttachment} />}
224
- <ComposerInput
225
- ref={inputRef}
226
- dictationToolbar={dictationToolbar}
227
- className="composer-input select-text text-sm leading-normal text-text"
228
- value={draft.text}
229
- placeholder={placeholder}
230
- disabled={!writable}
231
- tabbable={focused}
232
- extensions={editorExtensions}
233
- aria={menus.inputAria}
234
- onChange={(value, selection, state) => {
235
- hold.noteInput();
236
- box.setText(value);
237
- menus.setMenuIndex(0);
238
- menus.trackTriggers(value, selection, state);
239
- history.edited(value);
240
- }}
241
- onSelectionChange={menus.trackTriggers}
242
- onBlur={() => {
243
- menus.close();
244
- hold.release('blur');
245
- }}
246
- onComposing={hold.setComposing}
247
- onKeyDown={onKeyDown}
248
- onPaste={files.onPaste}
249
- />
250
- {notice.notice && (
251
- <div className="px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5" role="alert">
252
- {notice.notice}
253
- </div>
254
- )}
255
- {guard.visible && (
256
- <div
257
- className={clsx(
258
- 'px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5',
259
- guard.tooLong ? 'text-status-error' : 'text-text-faint'
260
- )}
261
- >
262
- {formatNumber(guard.count)} / {formatNumber(PROMPT_MAX_CHARS)}
263
- {guard.tooLong && ` ${t('composer.tooLong')}`}
264
- </div>
265
- )}
266
- <ComposerControls
267
- chatId={chatId}
268
- info={info}
269
- providerFixed={providerFixed}
270
- writable={writable}
271
- attachable={capabilities?.attachments !== false}
272
- controls={controls}
273
- send={{
274
- visible: !busy || !empty,
275
- disabled: empty || !writable || guard.tooLong,
276
- label: t(sendLabelKey(busy, steerable, sendDelivery)),
277
- kbd: sendShortcut(sendKey, draft.text.includes('\n')),
278
- onSend: (alternate) => submit(alternate ? 'alternate' : 'default')
279
- }}
280
- dictationToolbar={setDictationToolbar}
281
- onFiles={(picked) => {
282
- files.addFiles(picked);
283
- inputRef.current?.focus();
284
- }}
285
- onRestore={restoreStashed}
286
- insert={(added) => {
287
- if (writable) {
288
- inputRef.current?.insert(added);
289
- }
290
- }}
291
- />
307
+ </div>
292
308
  </PromptComposer>
293
309
  </div>
294
310
  <PromptDialog
@@ -0,0 +1,45 @@
1
+ /* Anything with an action of its own; `data-composer-action` marks one a role cannot name, such as a control of the host's. */
2
+ const ACTIONS = [
3
+ 'a',
4
+ 'button',
5
+ 'input',
6
+ 'select',
7
+ 'textarea',
8
+ 'label',
9
+ 'summary',
10
+ '[contenteditable]:not([contenteditable="false"])',
11
+ '[tabindex]:not([tabindex="-1"])',
12
+ '[role="button"]',
13
+ '[role="link"]',
14
+ '[role="menuitem"]',
15
+ '[role="menuitemcheckbox"]',
16
+ '[role="menuitemradio"]',
17
+ '[role="option"]',
18
+ '[role="listbox"]',
19
+ '[role="checkbox"]',
20
+ '[role="radio"]',
21
+ '[role="switch"]',
22
+ '[role="tab"]',
23
+ '[role="slider"]',
24
+ '[role="combobox"]',
25
+ '[data-composer-action]'
26
+ ].join(', ');
27
+
28
+ /*
29
+ * Where a pointer is on the composer: in the text field, on an action, or on the space around them,
30
+ * which stands in for the text field. A target outside the surface is an action, as an event from a
31
+ * popup portaled out of it still reaches the surface through React.
32
+ */
33
+ export function surfacePartOf(target: EventTarget | null, surface: Element): 'input' | 'action' | 'surface' {
34
+ // Not `instanceof Element`, which fails for an element of another window.
35
+ const element = target as Element | null;
36
+ if (typeof element?.closest !== 'function' || !surface.contains(element)) {
37
+ return 'action';
38
+ }
39
+ const input = element.closest('.cm-editor');
40
+ if (input !== null && surface.contains(input)) {
41
+ return 'input';
42
+ }
43
+ const action = element.closest(ACTIONS);
44
+ return action !== null && surface.contains(action) ? 'action' : 'surface';
45
+ }