@tai42/studio-sdk 30.0.1 → 31.0.0
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 +98 -10
- package/dist/components/agent-picker.d.ts +1 -1
- package/dist/components/agent-picker.d.ts.map +1 -1
- package/dist/components/agent-picker.js +1 -0
- package/dist/components/agent-picker.js.map +1 -1
- package/dist/components/channel-picker.d.ts +1 -1
- package/dist/components/channel-picker.d.ts.map +1 -1
- package/dist/components/channel-picker.js +1 -0
- package/dist/components/channel-picker.js.map +1 -1
- package/dist/components/components.css +53 -0
- package/dist/components/explorer-body.d.ts +1 -0
- package/dist/components/explorer-body.d.ts.map +1 -1
- package/dist/components/explorer-body.js +6 -4
- package/dist/components/explorer-body.js.map +1 -1
- package/dist/components/explorer-view.d.ts +16 -7
- package/dist/components/explorer-view.d.ts.map +1 -1
- package/dist/components/explorer-view.js +27 -13
- package/dist/components/explorer-view.js.map +1 -1
- package/dist/components/extension-config-editor.js.map +1 -1
- package/dist/components/field.d.ts +6 -0
- package/dist/components/field.d.ts.map +1 -1
- package/dist/components/field.js +15 -2
- package/dist/components/field.js.map +1 -1
- package/dist/components/folder-actions.d.ts +51 -0
- package/dist/components/folder-actions.d.ts.map +1 -0
- package/dist/components/folder-actions.js +173 -0
- package/dist/components/folder-actions.js.map +1 -0
- package/dist/components/folder-filing.d.ts +32 -0
- package/dist/components/folder-filing.d.ts.map +1 -0
- package/dist/components/folder-filing.js +51 -0
- package/dist/components/folder-filing.js.map +1 -0
- package/dist/components/folder-nav.d.ts +9 -4
- package/dist/components/folder-nav.d.ts.map +1 -1
- package/dist/components/folder-nav.js +18 -22
- package/dist/components/folder-nav.js.map +1 -1
- package/dist/components/folder-occupancy.d.ts +27 -0
- package/dist/components/folder-occupancy.d.ts.map +1 -0
- package/dist/components/folder-occupancy.js +54 -0
- package/dist/components/folder-occupancy.js.map +1 -0
- package/dist/components/named-list-picker.d.ts +5 -3
- package/dist/components/named-list-picker.d.ts.map +1 -1
- package/dist/components/named-list-picker.js +7 -7
- package/dist/components/named-list-picker.js.map +1 -1
- package/dist/components/primitives.d.ts +2 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +2 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/searchable-select-model.d.ts +40 -0
- package/dist/components/searchable-select-model.d.ts.map +1 -0
- package/dist/components/searchable-select-model.js +69 -0
- package/dist/components/searchable-select-model.js.map +1 -0
- package/dist/components/searchable-select.d.ts +38 -0
- package/dist/components/searchable-select.d.ts.map +1 -0
- package/dist/components/searchable-select.js +200 -0
- package/dist/components/searchable-select.js.map +1 -0
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +2 -2
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/table.d.ts +7 -1
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +2 -2
- package/dist/components/table.js.map +1 -1
- package/dist/components/tool-meta-access.d.ts +28 -0
- package/dist/components/tool-meta-access.d.ts.map +1 -0
- package/dist/components/tool-meta-access.js +65 -0
- package/dist/components/tool-meta-access.js.map +1 -0
- package/dist/components/tool-name-entry.d.ts +26 -0
- package/dist/components/tool-name-entry.d.ts.map +1 -0
- package/dist/components/tool-name-entry.js +43 -0
- package/dist/components/tool-name-entry.js.map +1 -0
- package/dist/components/tool-picker.d.ts +23 -18
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +51 -33
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/tool-select.d.ts +6 -5
- package/dist/components/tool-select.d.ts.map +1 -1
- package/dist/components/tool-select.js +31 -14
- package/dist/components/tool-select.js.map +1 -1
- package/dist/hooks/useApi.d.ts +1 -0
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.d.ts +27 -12
- package/dist/hooks/useSystemKinds.d.ts.map +1 -1
- package/dist/hooks/useSystemKinds.js +56 -16
- package/dist/hooks/useSystemKinds.js.map +1 -1
- package/dist/index.d.ts +51 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -26
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/use-mount-install.d.ts +6 -6
- package/dist/plugin/version.d.ts +1 -1
- package/dist/plugin/version.js +1 -1
- package/dist/schema-editor/SchemaEditor.d.ts +7 -2
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -1
- package/dist/schema-editor/SchemaEditor.js +1 -1
- package/dist/schema-editor/SchemaEditor.js.map +1 -1
- package/dist/schema-editor/index.d.ts +2 -2
- package/dist/schema-editor/index.d.ts.map +1 -1
- package/dist/schema-form/array-field.d.ts.map +1 -1
- package/dist/schema-form/array-field.js +6 -2
- package/dist/schema-form/array-field.js.map +1 -1
- package/dist/schema-form/classify.d.ts +4 -1
- package/dist/schema-form/classify.d.ts.map +1 -1
- package/dist/schema-form/classify.js +36 -15
- package/dist/schema-form/classify.js.map +1 -1
- package/dist/schema-form/collapsed-object-field.d.ts +20 -0
- package/dist/schema-form/collapsed-object-field.d.ts.map +1 -0
- package/dist/schema-form/collapsed-object-field.js +14 -0
- package/dist/schema-form/collapsed-object-field.js.map +1 -0
- package/dist/schema-form/context.d.ts +27 -1
- package/dist/schema-form/context.d.ts.map +1 -1
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -1
- package/dist/schema-form/default-value.d.ts +21 -6
- package/dist/schema-form/default-value.d.ts.map +1 -1
- package/dist/schema-form/default-value.js +74 -15
- package/dist/schema-form/default-value.js.map +1 -1
- package/dist/schema-form/expression-field.d.ts +48 -0
- package/dist/schema-form/expression-field.d.ts.map +1 -0
- package/dist/schema-form/expression-field.js +88 -0
- package/dist/schema-form/expression-field.js.map +1 -0
- package/dist/schema-form/field-model.d.ts +23 -4
- package/dist/schema-form/field-model.d.ts.map +1 -1
- package/dist/schema-form/field-node.d.ts.map +1 -1
- package/dist/schema-form/field-node.js +16 -3
- package/dist/schema-form/field-node.js.map +1 -1
- package/dist/schema-form/index.d.ts +3 -3
- package/dist/schema-form/index.d.ts.map +1 -1
- package/dist/schema-form/index.js +2 -2
- package/dist/schema-form/index.js.map +1 -1
- package/dist/schema-form/object-fields.d.ts +6 -3
- package/dist/schema-form/object-fields.d.ts.map +1 -1
- package/dist/schema-form/object-fields.js +14 -3
- package/dist/schema-form/object-fields.js.map +1 -1
- package/dist/schema-form/record-field.d.ts.map +1 -1
- package/dist/schema-form/record-field.js +6 -2
- package/dist/schema-form/record-field.js.map +1 -1
- package/dist/schema-form/seeding-action.d.ts +15 -0
- package/dist/schema-form/seeding-action.d.ts.map +1 -0
- package/dist/schema-form/seeding-action.js +35 -0
- package/dist/schema-form/seeding-action.js.map +1 -0
- package/dist/schema-form/string-annotations.d.ts +22 -8
- package/dist/schema-form/string-annotations.d.ts.map +1 -1
- package/dist/schema-form/string-annotations.js +39 -14
- package/dist/schema-form/string-annotations.js.map +1 -1
- package/dist/schema-form/string-field.js +2 -84
- package/dist/schema-form/string-field.js.map +1 -1
- package/dist/schema-form/templated-field.d.ts +13 -0
- package/dist/schema-form/templated-field.d.ts.map +1 -0
- package/dist/schema-form/templated-field.js +43 -0
- package/dist/schema-form/templated-field.js.map +1 -0
- package/dist/schema-form/union-field.d.ts.map +1 -1
- package/dist/schema-form/union-field.js +6 -2
- package/dist/schema-form/union-field.js.map +1 -1
- package/dist/schema-form/union.d.ts +4 -4
- package/dist/schema-form/union.d.ts.map +1 -1
- package/dist/schema-form/union.js +8 -1
- package/dist/schema-form/union.js.map +1 -1
- package/dist/schema-form/validate.d.ts.map +1 -1
- package/dist/schema-form/validate.js +28 -0
- package/dist/schema-form/validate.js.map +1 -1
- package/dist/search-match.d.ts +29 -0
- package/dist/search-match.d.ts.map +1 -0
- package/dist/search-match.js +38 -0
- package/dist/search-match.js.map +1 -0
- package/dist/tool-organization.d.ts +15 -0
- package/dist/tool-organization.d.ts.map +1 -0
- package/dist/tool-organization.js +6 -0
- package/dist/tool-organization.js.map +1 -0
- package/package.json +3 -2
- package/src/components/agent-picker.tsx +2 -1
- package/src/components/channel-picker.tsx +2 -1
- package/src/components/components.css +53 -0
- package/src/components/explorer-body.tsx +29 -5
- package/src/components/explorer-view.tsx +48 -17
- package/src/components/extension-config-editor.tsx +3 -3
- package/src/components/field.tsx +18 -2
- package/src/components/folder-actions.tsx +341 -0
- package/src/components/folder-filing.tsx +109 -0
- package/src/components/folder-nav.tsx +26 -10
- package/src/components/folder-occupancy.ts +83 -0
- package/src/components/named-list-picker.tsx +11 -7
- package/src/components/primitives.tsx +2 -1
- package/src/components/searchable-select-model.ts +105 -0
- package/src/components/searchable-select.tsx +432 -0
- package/src/components/subject-section.tsx +3 -2
- package/src/components/table.tsx +12 -2
- package/src/components/tool-meta-access.tsx +101 -0
- package/src/components/tool-name-entry.tsx +77 -0
- package/src/components/tool-picker.tsx +83 -50
- package/src/components/tool-select.tsx +49 -38
- package/src/hooks/useSystemKinds.tsx +77 -17
- package/src/index.ts +98 -94
- package/src/plugin/version.ts +1 -1
- package/src/schema-editor/SchemaEditor.tsx +9 -3
- package/src/schema-editor/index.ts +2 -2
- package/src/schema-form/array-field.tsx +10 -2
- package/src/schema-form/classify.ts +41 -15
- package/src/schema-form/collapsed-object-field.tsx +56 -0
- package/src/schema-form/context.ts +33 -1
- package/src/schema-form/default-value.ts +89 -15
- package/src/schema-form/expression-field.tsx +162 -0
- package/src/schema-form/field-model.ts +27 -4
- package/src/schema-form/field-node.tsx +41 -2
- package/src/schema-form/index.ts +3 -2
- package/src/schema-form/object-fields.tsx +11 -4
- package/src/schema-form/record-field.tsx +10 -2
- package/src/schema-form/seeding-action.ts +44 -0
- package/src/schema-form/string-annotations.ts +40 -14
- package/src/schema-form/string-field.tsx +2 -147
- package/src/schema-form/templated-field.tsx +124 -0
- package/src/schema-form/union-field.tsx +11 -2
- package/src/schema-form/union.ts +12 -5
- package/src/schema-form/validate.ts +28 -0
- package/src/search-match.ts +41 -0
- package/src/tool-organization.ts +34 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SearchableSelect` — a single-choice select whose popup opens with a search box:
|
|
3
|
+
* typing narrows the options to those whose label contains the text (the SDK's one
|
|
4
|
+
* search rule, `matchesSearch`). It takes the same option shapes as `Select` — a flat
|
|
5
|
+
* `options` run or labelled `groups`, never both — and its trigger wears the same
|
|
6
|
+
* class, auto-wires to an enclosing `Field` the same way, and has the same
|
|
7
|
+
* `combobox` role, so the two read as one control with and without search.
|
|
8
|
+
*
|
|
9
|
+
* Keyboard: on the trigger, Enter, ArrowDown or ArrowUp opens the popup as the key goes
|
|
10
|
+
* down (as on `Select`), Space opens it as a button press, and a printable key opens it
|
|
11
|
+
* with that key already typed. In the search box ArrowDown
|
|
12
|
+
* and ArrowUp move the highlight, Enter chooses it, and Escape closes the popup and
|
|
13
|
+
* returns focus to the trigger. The highlight is exposed as the search box's
|
|
14
|
+
* `aria-activedescendant`, so focus never leaves the box while the reader moves.
|
|
15
|
+
*
|
|
16
|
+
* Large lists: the options are indexed once per options change, a keystroke runs one
|
|
17
|
+
* substring test per option against that index, and the list renders from a deferred
|
|
18
|
+
* copy of the query so typing stays responsive. Each row is memoised on its indexed
|
|
19
|
+
* entry, so a keystroke re-renders only the rows whose highlight or selection changed;
|
|
20
|
+
* the rest are kept or removed by React without rendering.
|
|
21
|
+
*
|
|
22
|
+
* The popup is a Radix Popover, so it shares the layer, focus and dismiss stack the
|
|
23
|
+
* design system's dialogs use: it opens above a dialog and closes on Escape or an
|
|
24
|
+
* outside press without closing the dialog under it.
|
|
25
|
+
*/
|
|
26
|
+
import * as RadixPopover from '@radix-ui/react-popover';
|
|
27
|
+
import {
|
|
28
|
+
type KeyboardEvent,
|
|
29
|
+
memo,
|
|
30
|
+
type MouseEvent,
|
|
31
|
+
type ReactNode,
|
|
32
|
+
useCallback,
|
|
33
|
+
useDeferredValue,
|
|
34
|
+
useEffect,
|
|
35
|
+
useId,
|
|
36
|
+
useMemo,
|
|
37
|
+
useRef,
|
|
38
|
+
useState,
|
|
39
|
+
} from 'react';
|
|
40
|
+
|
|
41
|
+
import { SELECT_TRIGGER_CLASS } from './control-styles';
|
|
42
|
+
import { useFieldControl, useFieldLabelId } from './field';
|
|
43
|
+
import { CheckIcon, ChevronDownIcon } from './icons';
|
|
44
|
+
import {
|
|
45
|
+
indexSections,
|
|
46
|
+
type SearchEntry,
|
|
47
|
+
type SearchSection,
|
|
48
|
+
shownEntry,
|
|
49
|
+
stepEntry,
|
|
50
|
+
visibleOptions,
|
|
51
|
+
} from './searchable-select-model';
|
|
52
|
+
import type { SelectGroup, SelectOption } from './select';
|
|
53
|
+
|
|
54
|
+
/** Everything the two option shapes have in common. */
|
|
55
|
+
interface SearchableSelectSharedProps {
|
|
56
|
+
/**
|
|
57
|
+
* The chosen option's value; `''` for none (the placeholder shows). A value no option
|
|
58
|
+
* carries shows as itself on the trigger.
|
|
59
|
+
*/
|
|
60
|
+
readonly value: string;
|
|
61
|
+
readonly onValueChange: (value: string) => void;
|
|
62
|
+
readonly placeholder?: string;
|
|
63
|
+
readonly disabled?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Accessible name for the trigger, its popup and its listbox when it is not wired to
|
|
66
|
+
* an enclosing `Field`. A `Field` wins where both are given, as on `Select`.
|
|
67
|
+
*/
|
|
68
|
+
readonly 'aria-label'?: string;
|
|
69
|
+
/** IDREFs describing the trigger, concatenated ahead of an enclosing `Field`'s own. */
|
|
70
|
+
readonly 'aria-describedby'?: string;
|
|
71
|
+
/** Accessible name of the search box (e.g. "Search tools"). */
|
|
72
|
+
readonly searchLabel: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The FLAT shape: one ungrouped run of options. */
|
|
76
|
+
export interface SearchableSelectProps extends SearchableSelectSharedProps {
|
|
77
|
+
readonly options: readonly SelectOption[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The GROUPED shape: labelled clusters, each carrying its own options. */
|
|
81
|
+
export interface SearchableSelectGroupsProps extends SearchableSelectSharedProps {
|
|
82
|
+
readonly groups: readonly SelectGroup[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type SearchableSelectArgs =
|
|
86
|
+
| (SearchableSelectProps & { readonly groups?: undefined })
|
|
87
|
+
| (SearchableSelectGroupsProps & { readonly options?: undefined });
|
|
88
|
+
|
|
89
|
+
interface OptionRowProps {
|
|
90
|
+
readonly entry: SearchEntry;
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly highlighted: boolean;
|
|
93
|
+
readonly selected: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One option. Pointer handling is delegated to the listbox, so a row is pure markup. */
|
|
97
|
+
const OptionRow = memo(function OptionRow({ entry, id, highlighted, selected }: OptionRowProps) {
|
|
98
|
+
const disabled = entry.option.disabled === true;
|
|
99
|
+
return (
|
|
100
|
+
<div
|
|
101
|
+
id={id}
|
|
102
|
+
role="option"
|
|
103
|
+
aria-selected={selected}
|
|
104
|
+
aria-disabled={disabled ? true : undefined}
|
|
105
|
+
className="tai-select-item"
|
|
106
|
+
data-position={entry.position}
|
|
107
|
+
data-highlighted={highlighted ? '' : undefined}
|
|
108
|
+
data-disabled={disabled ? '' : undefined}
|
|
109
|
+
>
|
|
110
|
+
<span>{entry.option.label}</span>
|
|
111
|
+
{selected ? (
|
|
112
|
+
<span className="tai-select-item-indicator" aria-hidden="true">
|
|
113
|
+
<CheckIcon />
|
|
114
|
+
</span>
|
|
115
|
+
) : null}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
/** The position of the option row an event happened in, or `undefined` outside one. */
|
|
121
|
+
function positionAt(target: EventTarget): number | undefined {
|
|
122
|
+
if (!(target instanceof Element)) return undefined;
|
|
123
|
+
const row = target.closest<HTMLElement>('[role="option"]');
|
|
124
|
+
if (row === null || row.dataset.disabled !== undefined) return undefined;
|
|
125
|
+
const position = Number(row.dataset.position);
|
|
126
|
+
return Number.isInteger(position) ? position : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A key that types a character (and so starts a search from the closed trigger). */
|
|
130
|
+
function isPrintable(event: KeyboardEvent): boolean {
|
|
131
|
+
return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The select's interaction state over its indexed `sections`: whether the popup is
|
|
136
|
+
* open, the query and what it leaves visible, the highlight, and the key and pointer
|
|
137
|
+
* handlers that move it and choose.
|
|
138
|
+
*
|
|
139
|
+
* The highlight is held as an option VALUE, so a list that changes while open keeps
|
|
140
|
+
* it on the same option. Choosing and the empty status read the live query; only the
|
|
141
|
+
* rendered rows trail it through `useDeferredValue`, so a fast type-and-Enter picks
|
|
142
|
+
* from what was typed.
|
|
143
|
+
*/
|
|
144
|
+
function useSearchSelection(
|
|
145
|
+
sections: readonly SearchSection[],
|
|
146
|
+
value: string,
|
|
147
|
+
onValueChange: (value: string) => void,
|
|
148
|
+
) {
|
|
149
|
+
const listboxId = useId();
|
|
150
|
+
const optionId = useCallback(
|
|
151
|
+
(position: number) => `${listboxId}-${String(position)}`,
|
|
152
|
+
[listboxId],
|
|
153
|
+
);
|
|
154
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
155
|
+
const [open, setOpen] = useState(false);
|
|
156
|
+
const [query, setQuery] = useState('');
|
|
157
|
+
const [wanted, setWanted] = useState<string | undefined>(undefined);
|
|
158
|
+
// Only a keyboard move scrolls the highlight into view; a pointer already sees it.
|
|
159
|
+
const scrollOnMove = useRef(false);
|
|
160
|
+
|
|
161
|
+
const indexed = useMemo(() => sections.flatMap((section) => section.entries), [sections]);
|
|
162
|
+
const selectedEntry = useMemo(
|
|
163
|
+
() => indexed.find((entry) => entry.option.value === value),
|
|
164
|
+
[indexed, value],
|
|
165
|
+
);
|
|
166
|
+
const live = useMemo(() => visibleOptions(sections, query), [sections, query]);
|
|
167
|
+
const deferredQuery = useDeferredValue(query);
|
|
168
|
+
const rendered = useMemo(
|
|
169
|
+
() => (deferredQuery === query ? live : visibleOptions(sections, deferredQuery)),
|
|
170
|
+
[sections, deferredQuery, query, live],
|
|
171
|
+
);
|
|
172
|
+
const highlightedEntry = shownEntry(live.entries, wanted);
|
|
173
|
+
const highlighted = highlightedEntry?.position;
|
|
174
|
+
|
|
175
|
+
const scrollToHighlight = useCallback(
|
|
176
|
+
(position: number | undefined) => {
|
|
177
|
+
if (position === undefined) return;
|
|
178
|
+
document.getElementById(optionId(position))?.scrollIntoView({ block: 'nearest' });
|
|
179
|
+
},
|
|
180
|
+
[optionId],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!scrollOnMove.current) return;
|
|
185
|
+
scrollOnMove.current = false;
|
|
186
|
+
scrollToHighlight(highlighted);
|
|
187
|
+
}, [highlighted, scrollToHighlight]);
|
|
188
|
+
|
|
189
|
+
const onOpenChange = (next: boolean): void => {
|
|
190
|
+
setOpen(next);
|
|
191
|
+
if (!next) setQuery('');
|
|
192
|
+
else setWanted(selectedEntry?.option.value);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Runs once the popup's rows are mounted: focus goes to the search box, and the
|
|
196
|
+
* highlight (the chosen option on open) is brought into view. */
|
|
197
|
+
const onOpened = (): void => {
|
|
198
|
+
inputRef.current?.focus();
|
|
199
|
+
scrollToHighlight(highlighted);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const choose = (entry: SearchEntry | undefined): void => {
|
|
203
|
+
if (entry === undefined || entry.option.disabled === true) return;
|
|
204
|
+
if (entry.option.value !== value) onValueChange(entry.option.value);
|
|
205
|
+
onOpenChange(false);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const move = (to: SearchEntry | undefined): void => {
|
|
209
|
+
scrollOnMove.current = true;
|
|
210
|
+
setWanted(to?.option.value);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
|
214
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
move(stepEntry(live.entries, highlightedEntry, event.key === 'ArrowDown' ? 1 : -1));
|
|
217
|
+
} else if (event.key === 'Enter') {
|
|
218
|
+
// Enter chooses; it must never submit the form the select sits in.
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
choose(highlightedEntry);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const onTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>): void => {
|
|
225
|
+
if (open) return;
|
|
226
|
+
if (event.key === 'Enter' || event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
227
|
+
// Prevented, so a native button press does not follow and toggle it shut again.
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
onOpenChange(true);
|
|
230
|
+
} else if (isPrintable(event) && event.key !== ' ') {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
onOpenChange(true);
|
|
233
|
+
setQuery(event.key);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const entryAt = (target: EventTarget): SearchEntry | undefined => {
|
|
238
|
+
const position = positionAt(target);
|
|
239
|
+
return position === undefined ? undefined : indexed[position];
|
|
240
|
+
};
|
|
241
|
+
const onListClick = (event: MouseEvent<HTMLDivElement>): void => {
|
|
242
|
+
choose(entryAt(event.target));
|
|
243
|
+
};
|
|
244
|
+
const onListPointerMove = (event: MouseEvent<HTMLDivElement>): void => {
|
|
245
|
+
const entry = entryAt(event.target);
|
|
246
|
+
if (entry !== undefined && entry.option.value !== wanted) setWanted(entry.option.value);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
listboxId,
|
|
251
|
+
optionId,
|
|
252
|
+
inputRef,
|
|
253
|
+
open,
|
|
254
|
+
onOpenChange,
|
|
255
|
+
onOpened,
|
|
256
|
+
query,
|
|
257
|
+
setQuery,
|
|
258
|
+
empty: live.entries.length === 0,
|
|
259
|
+
rendered,
|
|
260
|
+
highlighted,
|
|
261
|
+
selectedEntry,
|
|
262
|
+
onSearchKeyDown,
|
|
263
|
+
onTriggerKeyDown,
|
|
264
|
+
onListClick,
|
|
265
|
+
onListPointerMove,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function SearchableSelect(props: SearchableSelectArgs): ReactNode {
|
|
270
|
+
const {
|
|
271
|
+
value,
|
|
272
|
+
onValueChange,
|
|
273
|
+
placeholder = 'Select…',
|
|
274
|
+
disabled,
|
|
275
|
+
'aria-label': ariaLabel,
|
|
276
|
+
'aria-describedby': ownDescribedBy,
|
|
277
|
+
searchLabel,
|
|
278
|
+
} = props;
|
|
279
|
+
// Untyped JavaScript is the only caller that reaches this; dropping one list would
|
|
280
|
+
// lose its every entry without a word.
|
|
281
|
+
if ((props.options === undefined) === (props.groups === undefined)) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
'SearchableSelect renders exactly one of `options` or `groups`; it was given both, or neither.',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const field = useFieldControl();
|
|
287
|
+
const fieldLabelId = useFieldLabelId();
|
|
288
|
+
const sections = useMemo(
|
|
289
|
+
() => indexSections(props.options, props.groups),
|
|
290
|
+
[props.options, props.groups],
|
|
291
|
+
);
|
|
292
|
+
const {
|
|
293
|
+
listboxId,
|
|
294
|
+
optionId,
|
|
295
|
+
inputRef,
|
|
296
|
+
open,
|
|
297
|
+
onOpenChange,
|
|
298
|
+
onOpened,
|
|
299
|
+
query,
|
|
300
|
+
setQuery,
|
|
301
|
+
empty,
|
|
302
|
+
rendered,
|
|
303
|
+
highlighted,
|
|
304
|
+
selectedEntry,
|
|
305
|
+
onSearchKeyDown,
|
|
306
|
+
onTriggerKeyDown,
|
|
307
|
+
onListClick,
|
|
308
|
+
onListPointerMove,
|
|
309
|
+
} = useSearchSelection(sections, value, onValueChange);
|
|
310
|
+
// The popup and its listbox carry the control's own name: the Field's label, else
|
|
311
|
+
// the caller's `aria-label`. The search box is named apart by `searchLabel`.
|
|
312
|
+
const popupName =
|
|
313
|
+
field.id !== undefined && fieldLabelId !== undefined
|
|
314
|
+
? { 'aria-labelledby': fieldLabelId }
|
|
315
|
+
: { 'aria-label': ariaLabel };
|
|
316
|
+
const describedBy = [ownDescribedBy, field['aria-describedby']].filter(
|
|
317
|
+
(id): id is string => id !== undefined,
|
|
318
|
+
);
|
|
319
|
+
const rows = (entries: readonly SearchEntry[]): ReactNode =>
|
|
320
|
+
entries.map((entry) => (
|
|
321
|
+
<OptionRow
|
|
322
|
+
key={entry.position}
|
|
323
|
+
entry={entry}
|
|
324
|
+
id={optionId(entry.position)}
|
|
325
|
+
highlighted={entry.position === highlighted}
|
|
326
|
+
selected={entry === selectedEntry}
|
|
327
|
+
/>
|
|
328
|
+
));
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<RadixPopover.Root open={open} onOpenChange={onOpenChange}>
|
|
332
|
+
<RadixPopover.Trigger
|
|
333
|
+
type="button"
|
|
334
|
+
role="combobox"
|
|
335
|
+
id={field.id}
|
|
336
|
+
className={SELECT_TRIGGER_CLASS}
|
|
337
|
+
disabled={disabled}
|
|
338
|
+
// A trigger claiming the field's id is named by that field's `<label for>`, and
|
|
339
|
+
// `aria-label` outranks a native label: emitting both would drop the visible text.
|
|
340
|
+
aria-label={field.id === undefined ? ariaLabel : undefined}
|
|
341
|
+
aria-describedby={describedBy.length > 0 ? describedBy.join(' ') : undefined}
|
|
342
|
+
aria-invalid={field['aria-invalid']}
|
|
343
|
+
onKeyDown={onTriggerKeyDown}
|
|
344
|
+
>
|
|
345
|
+
<span>{selectedEntry?.option.label ?? (value === '' ? placeholder : value)}</span>
|
|
346
|
+
<ChevronDownIcon />
|
|
347
|
+
</RadixPopover.Trigger>
|
|
348
|
+
<RadixPopover.Portal>
|
|
349
|
+
<RadixPopover.Content
|
|
350
|
+
className="tai-select-content tai-search-select-content"
|
|
351
|
+
align="start"
|
|
352
|
+
sideOffset={4}
|
|
353
|
+
{...popupName}
|
|
354
|
+
onOpenAutoFocus={(event) => {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
onOpened();
|
|
357
|
+
}}
|
|
358
|
+
>
|
|
359
|
+
<div className="tai-search-select-search">
|
|
360
|
+
<input
|
|
361
|
+
ref={inputRef}
|
|
362
|
+
type="text"
|
|
363
|
+
role="combobox"
|
|
364
|
+
className="tai-input"
|
|
365
|
+
aria-label={searchLabel}
|
|
366
|
+
aria-expanded="true"
|
|
367
|
+
aria-controls={listboxId}
|
|
368
|
+
aria-autocomplete="list"
|
|
369
|
+
aria-activedescendant={highlighted === undefined ? undefined : optionId(highlighted)}
|
|
370
|
+
autoComplete="off"
|
|
371
|
+
spellCheck={false}
|
|
372
|
+
placeholder={searchLabel}
|
|
373
|
+
value={query}
|
|
374
|
+
onChange={(event) => {
|
|
375
|
+
setQuery(event.target.value);
|
|
376
|
+
}}
|
|
377
|
+
onKeyDown={onSearchKeyDown}
|
|
378
|
+
/>
|
|
379
|
+
</div>
|
|
380
|
+
{/* Rows take no focus: the search box keeps it and points at the highlight,
|
|
381
|
+
so a press on a row must not move focus out of the box first. */}
|
|
382
|
+
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- the search box owns the keyboard through aria-activedescendant; focus never reaches the list */}
|
|
383
|
+
<div
|
|
384
|
+
id={listboxId}
|
|
385
|
+
role="listbox"
|
|
386
|
+
{...popupName}
|
|
387
|
+
tabIndex={-1}
|
|
388
|
+
className="tai-select-viewport tai-search-select-list"
|
|
389
|
+
onMouseDown={(event) => {
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
}}
|
|
392
|
+
onClick={onListClick}
|
|
393
|
+
onPointerMove={onListPointerMove}
|
|
394
|
+
>
|
|
395
|
+
{rendered.sections.map((section) =>
|
|
396
|
+
section.label === undefined ? (
|
|
397
|
+
rows(section.entries)
|
|
398
|
+
) : (
|
|
399
|
+
<SectionGroup key={section.label} label={section.label}>
|
|
400
|
+
{rows(section.entries)}
|
|
401
|
+
</SectionGroup>
|
|
402
|
+
),
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
{empty ? (
|
|
406
|
+
<p role="status" className="tai-status tai-search-select-empty">
|
|
407
|
+
{query.trim() === '' ? 'Nothing to choose.' : `Nothing matches “${query.trim()}”.`}
|
|
408
|
+
</p>
|
|
409
|
+
) : null}
|
|
410
|
+
</RadixPopover.Content>
|
|
411
|
+
</RadixPopover.Portal>
|
|
412
|
+
</RadixPopover.Root>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function SectionGroup({
|
|
417
|
+
label,
|
|
418
|
+
children,
|
|
419
|
+
}: {
|
|
420
|
+
readonly label: string;
|
|
421
|
+
readonly children: ReactNode;
|
|
422
|
+
}): ReactNode {
|
|
423
|
+
const labelId = useId();
|
|
424
|
+
return (
|
|
425
|
+
<div role="group" aria-labelledby={labelId}>
|
|
426
|
+
<div id={labelId} className="tai-select-group-label tai-label">
|
|
427
|
+
{label}
|
|
428
|
+
</div>
|
|
429
|
+
{children}
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
@@ -16,7 +16,7 @@ import { type ReactNode, useState } from 'react';
|
|
|
16
16
|
|
|
17
17
|
import { Field } from './field';
|
|
18
18
|
import { TextInput } from './inputs';
|
|
19
|
-
import {
|
|
19
|
+
import { SearchableSelect } from './searchable-select';
|
|
20
20
|
|
|
21
21
|
/** One conversation-target option: `target_kind:target_name` value, `kind · name` label. */
|
|
22
22
|
export interface SubjectTargetOption {
|
|
@@ -141,12 +141,13 @@ export function SubjectSection({
|
|
|
141
141
|
>
|
|
142
142
|
<p style={{ margin: 0, color: 'var(--tai-color-text-muted)' }}>{caption}</p>
|
|
143
143
|
<Field label="Target">
|
|
144
|
-
<
|
|
144
|
+
<SearchableSelect
|
|
145
145
|
value={target}
|
|
146
146
|
onValueChange={onTargetChange}
|
|
147
147
|
aria-label="Subject target"
|
|
148
148
|
placeholder={targetPlaceholder}
|
|
149
149
|
options={targetOptions}
|
|
150
|
+
searchLabel="Search targets"
|
|
150
151
|
/>
|
|
151
152
|
</Field>
|
|
152
153
|
<Field
|
package/src/components/table.tsx
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* A numeric column opts in with `numeric`, which stamps `data-numeric="true"` —
|
|
9
9
|
* the hook the stylesheet right-aligns on tabular figures so digits line up.
|
|
10
10
|
*
|
|
11
|
+
* A selected body row opts in with `selected`, which stamps `data-selected="true"` —
|
|
12
|
+
* the hook the stylesheet draws as the one selected-row look every table shares. It
|
|
13
|
+
* is presentation only: the row's own link carries `aria-current` for assistive tech.
|
|
14
|
+
*
|
|
11
15
|
* Each wrapper FORWARDS a consumer `ref` to the native element it renders — the
|
|
12
16
|
* attribute spread carries it, the same way it carries every other native prop —
|
|
13
17
|
* so a caller that has to measure or scroll a cell is not forced back to a raw
|
|
@@ -48,10 +52,16 @@ export interface TableRowProps extends HTMLAttributes<HTMLTableRowElement> {
|
|
|
48
52
|
readonly children?: ReactNode;
|
|
49
53
|
/** A consumer ref for the `<tr>` itself. */
|
|
50
54
|
readonly ref?: Ref<HTMLTableRowElement>;
|
|
55
|
+
/** Draw the row as the selected one. */
|
|
56
|
+
readonly selected?: boolean;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
export function TR({ children, ...props }: TableRowProps) {
|
|
54
|
-
return
|
|
59
|
+
export function TR({ children, selected = false, ...props }: TableRowProps) {
|
|
60
|
+
return (
|
|
61
|
+
<tr {...props} data-selected={selected ? 'true' : undefined}>
|
|
62
|
+
{children}
|
|
63
|
+
</tr>
|
|
64
|
+
);
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/** Right-align a column of digits on tabular figures. */
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may write the tool-organization overlay, and what a refused overlay write shows.
|
|
3
|
+
*
|
|
4
|
+
* Each write is gated on the exact `method path` it calls — filing or editing a tool
|
|
5
|
+
* (`PATCH /api/tool-meta/tools/{tool_name}`), and creating, renaming, moving or
|
|
6
|
+
* deleting a folder (each its own folder route) — so a caller whose projection reaches
|
|
7
|
+
* one of them is offered that one alone. Every write also closes once the tool_meta
|
|
8
|
+
* store is known OFF: from the kind-status table before any write, or from a write's
|
|
9
|
+
* 501 `tool-meta-not-configured` refusal, which {@link useReportToolMetaRefusal}
|
|
10
|
+
* records for every screen at once.
|
|
11
|
+
*/
|
|
12
|
+
import type { apiRoutes } from '@tai42/api-client';
|
|
13
|
+
import { type ReactNode, useCallback } from 'react';
|
|
14
|
+
|
|
15
|
+
import { errorMessage } from '../errors';
|
|
16
|
+
import { FeatureDisabled, featureDisabledMessage, isFeatureDisabled } from '../feature-disabled';
|
|
17
|
+
import { useCanWrite } from '../hooks/useCapabilities';
|
|
18
|
+
import { useFeatureOff, useReportFeatureOff } from '../hooks/useSystemKinds';
|
|
19
|
+
import { ErrorState } from './primitives';
|
|
20
|
+
|
|
21
|
+
/** The kind-status kind of the tool-organization overlay store. */
|
|
22
|
+
const TOOL_META_KIND = 'tool_meta';
|
|
23
|
+
|
|
24
|
+
type ApiRoutes = typeof apiRoutes;
|
|
25
|
+
|
|
26
|
+
// The overlay write routes. Spelled here because the SDK takes no runtime import of
|
|
27
|
+
// the API client; the `satisfies` pin fails to compile if a route's method or path
|
|
28
|
+
// drifts from the generated route table.
|
|
29
|
+
const TOOL_META_WRITE_ROUTES = {
|
|
30
|
+
editTool: { method: 'PATCH', path: '/api/tool-meta/tools/{tool_name}' },
|
|
31
|
+
createFolder: { method: 'POST', path: '/api/tool-meta/folders' },
|
|
32
|
+
renameFolder: { method: 'POST', path: '/api/tool-meta/folders/{folder_id}/rename' },
|
|
33
|
+
moveFolder: { method: 'POST', path: '/api/tool-meta/folders/{folder_id}/move' },
|
|
34
|
+
deleteFolder: { method: 'DELETE', path: '/api/tool-meta/folders/{folder_id}' },
|
|
35
|
+
} as const satisfies {
|
|
36
|
+
readonly editTool: ApiRoutes['patch_api_tool_meta_tools_tool_name'];
|
|
37
|
+
readonly createFolder: ApiRoutes['post_api_tool_meta_folders'];
|
|
38
|
+
readonly renameFolder: ApiRoutes['post_api_tool_meta_folders_folder_id_rename'];
|
|
39
|
+
readonly moveFolder: ApiRoutes['post_api_tool_meta_folders_folder_id_move'];
|
|
40
|
+
readonly deleteFolder: ApiRoutes['delete_api_tool_meta_folders_folder_id'];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Which overlay writes the caller may be offered. */
|
|
44
|
+
export interface ToolMetaWriteAccess {
|
|
45
|
+
/** Edit a tool's overlay row — its display name, tags, visibility or folder. */
|
|
46
|
+
readonly editTool: boolean;
|
|
47
|
+
readonly createFolder: boolean;
|
|
48
|
+
readonly renameFolder: boolean;
|
|
49
|
+
readonly moveFolder: boolean;
|
|
50
|
+
readonly deleteFolder: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function useRouteOpen(route: { readonly method: string; readonly path: string }): boolean {
|
|
54
|
+
return useCanWrite(route.path, route.method);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The overlay writes the caller may be offered: each one's own route reached by the
|
|
59
|
+
* projection (fail closed until the projection is ready) and the tool_meta store not
|
|
60
|
+
* known OFF.
|
|
61
|
+
*/
|
|
62
|
+
export function useToolMetaWriteAccess(): ToolMetaWriteAccess {
|
|
63
|
+
const off = useFeatureOff(TOOL_META_KIND);
|
|
64
|
+
const editTool = useRouteOpen(TOOL_META_WRITE_ROUTES.editTool);
|
|
65
|
+
const createFolder = useRouteOpen(TOOL_META_WRITE_ROUTES.createFolder);
|
|
66
|
+
const renameFolder = useRouteOpen(TOOL_META_WRITE_ROUTES.renameFolder);
|
|
67
|
+
const moveFolder = useRouteOpen(TOOL_META_WRITE_ROUTES.moveFolder);
|
|
68
|
+
const deleteFolder = useRouteOpen(TOOL_META_WRITE_ROUTES.deleteFolder);
|
|
69
|
+
return {
|
|
70
|
+
editTool: editTool && !off,
|
|
71
|
+
createFolder: createFolder && !off,
|
|
72
|
+
renameFolder: renameFolder && !off,
|
|
73
|
+
moveFolder: moveFolder && !off,
|
|
74
|
+
deleteFolder: deleteFolder && !off,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Hand a failed overlay write here: a not-configured refusal (501
|
|
80
|
+
* `tool-meta-not-configured`) is recorded so every screen withdraws its overlay
|
|
81
|
+
* writes; any other failure is left to the caller, which shows it.
|
|
82
|
+
*/
|
|
83
|
+
export function useReportToolMetaRefusal(): (error: unknown) => void {
|
|
84
|
+
const reportOff = useReportFeatureOff();
|
|
85
|
+
return useCallback(
|
|
86
|
+
(error: unknown) => {
|
|
87
|
+
if (isFeatureDisabled(error)) reportOff(TOOL_META_KIND, featureDisabledMessage(error));
|
|
88
|
+
},
|
|
89
|
+
[reportOff],
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A failed overlay write as its dialog shows it: the muted OFF note with the server's
|
|
94
|
+
* message for a not-configured refusal, an error for anything else. */
|
|
95
|
+
export function ToolMetaWriteError({ error }: { readonly error: unknown }): ReactNode {
|
|
96
|
+
if (error === null || error === undefined) return null;
|
|
97
|
+
if (isFeatureDisabled(error)) {
|
|
98
|
+
return <FeatureDisabled feature="Tool metadata" message={featureDisabledMessage(error)} />;
|
|
99
|
+
}
|
|
100
|
+
return <ErrorState message={errorMessage(error)} />;
|
|
101
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ToolPicker`'s typed entry: the control it draws when the session may not list tools
|
|
3
|
+
* (the read answered 403). The server checks a named tool on save, so a reader who
|
|
4
|
+
* knows the name can still fill the field; the note under the input says why there is
|
|
5
|
+
* no list.
|
|
6
|
+
*
|
|
7
|
+
* The input holds a draft while the reader types and hands the trimmed name to
|
|
8
|
+
* `onChange` on blur or Enter, so a host that acts on every change (an "add" control
|
|
9
|
+
* that appends each emitted name) receives one whole name, never each keystroke. Enter
|
|
10
|
+
* commits instead of submitting the enclosing form, since the form would otherwise read
|
|
11
|
+
* the value from before the commit. A blank draft emits nothing: the input shows the
|
|
12
|
+
* stored value again.
|
|
13
|
+
*/
|
|
14
|
+
import { type KeyboardEvent, type ReactNode, useId, useState } from 'react';
|
|
15
|
+
|
|
16
|
+
import { useFieldControl } from './field';
|
|
17
|
+
import { TextInput } from './inputs';
|
|
18
|
+
|
|
19
|
+
/** The sentence under the typed entry, naming why there is no list and what saving does. */
|
|
20
|
+
export const TOOL_LIST_REFUSED_NOTE =
|
|
21
|
+
"This session cannot list tools, so type the tool's exact name. Saving checks that the deployment serves it.";
|
|
22
|
+
|
|
23
|
+
const NOTE_STYLE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
|
|
24
|
+
|
|
25
|
+
export interface ToolNameEntryProps {
|
|
26
|
+
/** The stored name, or `null` for none. */
|
|
27
|
+
readonly value: string | null;
|
|
28
|
+
readonly onChange: (toolName: string) => void;
|
|
29
|
+
readonly disabled: boolean;
|
|
30
|
+
/** Names the input when no enclosing `Field` labels it; a `Field`'s label wins. */
|
|
31
|
+
readonly ariaLabel: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ToolNameEntry({
|
|
35
|
+
value,
|
|
36
|
+
onChange,
|
|
37
|
+
disabled,
|
|
38
|
+
ariaLabel,
|
|
39
|
+
}: ToolNameEntryProps): ReactNode {
|
|
40
|
+
const noteId = useId();
|
|
41
|
+
const labelled = useFieldControl().id !== undefined;
|
|
42
|
+
const [draft, setDraft] = useState<string | undefined>(undefined);
|
|
43
|
+
|
|
44
|
+
const commit = (): void => {
|
|
45
|
+
if (draft === undefined) return;
|
|
46
|
+
const name = draft.trim();
|
|
47
|
+
setDraft(undefined);
|
|
48
|
+
if (name !== '' && name !== value) onChange(name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
|
|
52
|
+
if (event.key !== 'Enter') return;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
commit();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<TextInput
|
|
60
|
+
value={draft ?? value ?? ''}
|
|
61
|
+
onChange={(event) => {
|
|
62
|
+
setDraft(event.target.value);
|
|
63
|
+
}}
|
|
64
|
+
onBlur={commit}
|
|
65
|
+
onKeyDown={onKeyDown}
|
|
66
|
+
disabled={disabled}
|
|
67
|
+
aria-label={labelled ? undefined : ariaLabel}
|
|
68
|
+
aria-describedby={noteId}
|
|
69
|
+
autoComplete="off"
|
|
70
|
+
spellCheck={false}
|
|
71
|
+
/>
|
|
72
|
+
<p id={noteId} role="status" style={NOTE_STYLE}>
|
|
73
|
+
{TOOL_LIST_REFUSED_NOTE}
|
|
74
|
+
</p>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|