@workbench-kit/shell-react 0.0.2-prototype.0.2.32 → 0.0.2-prototype.0.2.34
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 +32 -0
- package/package.json +9 -9
- package/src/commands/use-command-descriptors.ts +7 -3
- package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
- package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
- package/src/devtools/workbench-devtools-snapshot.ts +32 -13
- package/src/editor/area.tsx +3 -2
- package/src/editor/state-storage.ts +31 -0
- package/src/editor/tab-context-menu.ts +8 -8
- package/src/editor/use-editor.ts +6 -6
- package/src/explorer/context-menu.ts +8 -4
- package/src/explorer/view.tsx +4 -3
- package/src/extensions/canonical-extension-descriptions.ts +58 -0
- package/src/extensions/context-menu.ts +9 -6
- package/src/extensions/extension-enablement-context.ts +15 -0
- package/src/extensions/extension-enablement-controller.ts +453 -0
- package/src/extensions/management-model.ts +111 -47
- package/src/extensions/theme-selection-protection.ts +98 -0
- package/src/extensions/uninstall-eligibility.ts +103 -0
- package/src/extensions/use-extension-management.ts +151 -67
- package/src/extensions/view.tsx +4 -0
- package/src/field-remap/chrome-labels.ts +87 -2
- package/src/field-remap/convert-palette.tsx +164 -18
- package/src/field-remap/demo.tsx +48 -3
- package/src/field-remap/detail-panel.tsx +6 -5
- package/src/field-remap/flow.tsx +341 -155
- package/src/field-remap/history.ts +99 -0
- package/src/field-remap/index.ts +9 -1
- package/src/field-remap/panel.tsx +388 -97
- package/src/field-remap/preview-controller.ts +122 -0
- package/src/field-remap/preview.tsx +171 -0
- package/src/field-remap/view.css +154 -2
- package/src/index.ts +16 -1
- package/src/management/keybinding-overrides-storage.ts +28 -0
- package/src/management/preference-settings-storage.ts +28 -0
- package/src/management/settings.tsx +2 -0
- package/src/management/use-command-management.ts +31 -26
- package/src/management/use-keybinding-management.ts +25 -12
- package/src/shell/focused-extension-services.ts +138 -0
- package/src/shell/host-shell.tsx +13 -10
- package/src/shell/persistence-diagnostic-context.ts +11 -0
- package/src/shell/provider.tsx +373 -69
- package/src/shell/settings.tsx +25 -16
- package/src/shell/shell.tsx +119 -78
- package/src/shell/view-host.tsx +23 -22
- package/src/storage/local-json-storage.ts +43 -29
- package/src/storage/persistence-diagnostics.ts +72 -0
- package/src/workbench/appearance-storage.ts +28 -0
- package/src/workbench/command-host.tsx +19 -22
- package/src/workbench/command-palette.ts +14 -13
- package/src/workbench/layout-storage.ts +52 -5
- package/src/workbench/use-persisted-appearance.ts +38 -12
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useRef, useState, type JSX, type KeyboardEvent } from 'react';
|
|
2
2
|
import { SideBarList, SideBarListItem } from '@workbench-kit/react/layout';
|
|
3
|
-
import {
|
|
3
|
+
import { ClearableTextInput, IconButton } from '@workbench-kit/react/primitives';
|
|
4
4
|
import type { ValueTransformRegistry } from '@workbench-kit/field-remap';
|
|
5
5
|
|
|
6
6
|
import { defaultFieldRemapChromeLabels, type FieldRemapChromeLabels } from './chrome-labels.js';
|
|
@@ -32,6 +32,120 @@ export function FieldRemapConvertPalette({
|
|
|
32
32
|
// The registry supports replacing definitions through `register`; resolve
|
|
33
33
|
// display metadata on each host render even when the registry object is stable.
|
|
34
34
|
const catalog = transforms.list().filter((definition) => definition.id !== 'identity');
|
|
35
|
+
const [filterQuery, setFilterQuery] = useState('');
|
|
36
|
+
const [requestedRovingTransformId, setRequestedRovingTransformId] = useState(() =>
|
|
37
|
+
catalog.some((definition) => definition.id === selectedTransformId)
|
|
38
|
+
? selectedTransformId
|
|
39
|
+
: (catalog[0]?.id ?? ''),
|
|
40
|
+
);
|
|
41
|
+
const optionRefs = useRef(new Map<string, HTMLButtonElement>());
|
|
42
|
+
const normalizedFilterQuery = filterQuery.trim().toLowerCase();
|
|
43
|
+
const visibleCatalog = catalog.filter((definition) => {
|
|
44
|
+
if (!normalizedFilterQuery) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
definition.id.toLowerCase().includes(normalizedFilterQuery) ||
|
|
49
|
+
definition.label.toLowerCase().includes(normalizedFilterQuery)
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
const visibleTransformIds = visibleCatalog.map((definition) => definition.id);
|
|
53
|
+
const selectedTransformIsVisible = visibleTransformIds.includes(selectedTransformId);
|
|
54
|
+
const rovingTransformId = visibleTransformIds.includes(requestedRovingTransformId)
|
|
55
|
+
? requestedRovingTransformId
|
|
56
|
+
: selectedTransformIsVisible
|
|
57
|
+
? selectedTransformId
|
|
58
|
+
: (visibleTransformIds[0] ?? '');
|
|
59
|
+
const filterLabel =
|
|
60
|
+
chromeLabels.convertFilterLabel ?? defaultFieldRemapChromeLabels.convertFilterLabel;
|
|
61
|
+
const filterPlaceholder =
|
|
62
|
+
chromeLabels.convertFilterPlaceholder ?? defaultFieldRemapChromeLabels.convertFilterPlaceholder;
|
|
63
|
+
const clearFilterLabel =
|
|
64
|
+
chromeLabels.clearConvertFilter ?? defaultFieldRemapChromeLabels.clearConvertFilter;
|
|
65
|
+
const noMatchingConverts =
|
|
66
|
+
chromeLabels.noMatchingConverts ?? defaultFieldRemapChromeLabels.noMatchingConverts;
|
|
67
|
+
|
|
68
|
+
const updateFilterQuery = (nextQuery: string) => {
|
|
69
|
+
const normalizedNextQuery = nextQuery.trim().toLowerCase();
|
|
70
|
+
const nextVisibleIds = catalog
|
|
71
|
+
.filter(
|
|
72
|
+
(definition) =>
|
|
73
|
+
!normalizedNextQuery ||
|
|
74
|
+
definition.id.toLowerCase().includes(normalizedNextQuery) ||
|
|
75
|
+
definition.label.toLowerCase().includes(normalizedNextQuery),
|
|
76
|
+
)
|
|
77
|
+
.map((definition) => definition.id);
|
|
78
|
+
setFilterQuery(nextQuery);
|
|
79
|
+
setRequestedRovingTransformId(
|
|
80
|
+
nextVisibleIds.includes(selectedTransformId)
|
|
81
|
+
? selectedTransformId
|
|
82
|
+
: (nextVisibleIds[0] ?? ''),
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const focusVisibleTransform = (transformId: string) => {
|
|
87
|
+
setRequestedRovingTransformId(transformId);
|
|
88
|
+
optionRefs.current.get(transformId)?.focus();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleFilterKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
|
|
92
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const targetId = selectedTransformIsVisible
|
|
96
|
+
? selectedTransformId
|
|
97
|
+
: event.key === 'ArrowDown'
|
|
98
|
+
? visibleTransformIds[0]
|
|
99
|
+
: visibleTransformIds[visibleTransformIds.length - 1];
|
|
100
|
+
if (!targetId) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
focusVisibleTransform(targetId);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleOptionKeyDown = (event: KeyboardEvent<HTMLButtonElement>, transformId: string) => {
|
|
108
|
+
if (event.key === 'Enter') {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
if (transformId !== selectedTransformId) {
|
|
111
|
+
onSelectedTransformIdChange(transformId);
|
|
112
|
+
}
|
|
113
|
+
onPlaceDraft(transformId);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentIndex = visibleTransformIds.indexOf(transformId);
|
|
118
|
+
if (currentIndex < 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let nextIndex: number | undefined;
|
|
122
|
+
switch (event.key) {
|
|
123
|
+
case 'ArrowDown':
|
|
124
|
+
nextIndex = Math.min(currentIndex + 1, visibleTransformIds.length - 1);
|
|
125
|
+
break;
|
|
126
|
+
case 'ArrowUp':
|
|
127
|
+
nextIndex = Math.max(currentIndex - 1, 0);
|
|
128
|
+
break;
|
|
129
|
+
case 'Home':
|
|
130
|
+
nextIndex = 0;
|
|
131
|
+
break;
|
|
132
|
+
case 'End':
|
|
133
|
+
nextIndex = visibleTransformIds.length - 1;
|
|
134
|
+
break;
|
|
135
|
+
default:
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const nextTransformId = visibleTransformIds[nextIndex];
|
|
140
|
+
if (!nextTransformId) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
focusVisibleTransform(nextTransformId);
|
|
145
|
+
if (nextTransformId !== selectedTransformId) {
|
|
146
|
+
onSelectedTransformIdChange(nextTransformId);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
35
149
|
|
|
36
150
|
return (
|
|
37
151
|
<aside
|
|
@@ -45,39 +159,66 @@ export function FieldRemapConvertPalette({
|
|
|
45
159
|
</header>
|
|
46
160
|
|
|
47
161
|
<div className="workbench-field-remap-convert-palette__place">
|
|
48
|
-
<
|
|
162
|
+
<IconButton
|
|
163
|
+
compact
|
|
49
164
|
type="button"
|
|
50
165
|
data-testid="field-remap-place-draft"
|
|
51
|
-
disabled={!selectedTransformId}
|
|
166
|
+
disabled={!selectedTransformId || !selectedTransformIsVisible}
|
|
167
|
+
icon="codicon-add"
|
|
168
|
+
label={chromeLabels.placeConvert}
|
|
52
169
|
onClick={() => {
|
|
53
|
-
if (!selectedTransformId) {
|
|
170
|
+
if (!selectedTransformId || !selectedTransformIsVisible) {
|
|
54
171
|
return;
|
|
55
172
|
}
|
|
56
173
|
onPlaceDraft(selectedTransformId);
|
|
57
174
|
}}
|
|
58
|
-
|
|
59
|
-
{chromeLabels.placeConvert}
|
|
60
|
-
</Button>
|
|
175
|
+
/>
|
|
61
176
|
</div>
|
|
62
177
|
|
|
178
|
+
<ClearableTextInput
|
|
179
|
+
aria-label={filterLabel}
|
|
180
|
+
className="workbench-field-remap-convert-palette__filter"
|
|
181
|
+
clearLabel={clearFilterLabel}
|
|
182
|
+
controlWidth="full"
|
|
183
|
+
placeholder={filterPlaceholder}
|
|
184
|
+
type="search"
|
|
185
|
+
value={filterQuery}
|
|
186
|
+
onClear={() => updateFilterQuery('')}
|
|
187
|
+
onKeyDown={handleFilterKeyDown}
|
|
188
|
+
onValueChange={updateFilterQuery}
|
|
189
|
+
/>
|
|
190
|
+
|
|
63
191
|
<SideBarList
|
|
64
192
|
className="workbench-field-remap-convert-palette__list"
|
|
65
193
|
role="listbox"
|
|
66
194
|
aria-label={chromeLabels.convertsListAriaLabel}
|
|
67
195
|
>
|
|
68
|
-
{
|
|
196
|
+
{visibleCatalog.map((definition) => {
|
|
69
197
|
const selected = definition.id === selectedTransformId;
|
|
70
198
|
return (
|
|
71
199
|
<SideBarListItem
|
|
200
|
+
ref={(element) => {
|
|
201
|
+
if (element) {
|
|
202
|
+
optionRefs.current.set(definition.id, element);
|
|
203
|
+
} else {
|
|
204
|
+
optionRefs.current.delete(definition.id);
|
|
205
|
+
}
|
|
206
|
+
}}
|
|
72
207
|
key={definition.id}
|
|
73
208
|
aria-selected={selected}
|
|
74
209
|
data-testid={`field-remap-palette-item-${definition.id}`}
|
|
75
210
|
role="option"
|
|
76
211
|
selected={selected}
|
|
212
|
+
tabIndex={definition.id === rovingTransformId ? 0 : -1}
|
|
77
213
|
variant="stacked"
|
|
78
214
|
wrapperProps={{ role: 'presentation' }}
|
|
79
|
-
onClick={() =>
|
|
215
|
+
onClick={() => {
|
|
216
|
+
setRequestedRovingTransformId(definition.id);
|
|
217
|
+
onSelectedTransformIdChange(definition.id);
|
|
218
|
+
}}
|
|
80
219
|
onDoubleClick={() => onPlaceDraft(definition.id)}
|
|
220
|
+
onFocus={() => setRequestedRovingTransformId(definition.id)}
|
|
221
|
+
onKeyDown={(event) => handleOptionKeyDown(event, definition.id)}
|
|
81
222
|
>
|
|
82
223
|
<strong>{definition.label}</strong>
|
|
83
224
|
<code title={definition.id}>{definition.id}</code>
|
|
@@ -85,6 +226,11 @@ export function FieldRemapConvertPalette({
|
|
|
85
226
|
);
|
|
86
227
|
})}
|
|
87
228
|
</SideBarList>
|
|
229
|
+
{visibleCatalog.length === 0 ? (
|
|
230
|
+
<p className="workbench-field-remap-convert-palette__empty" role="status">
|
|
231
|
+
{noMatchingConverts}
|
|
232
|
+
</p>
|
|
233
|
+
) : null}
|
|
88
234
|
|
|
89
235
|
{onAddCombine || onAddSplit ? (
|
|
90
236
|
<div
|
|
@@ -95,24 +241,24 @@ export function FieldRemapConvertPalette({
|
|
|
95
241
|
<p>{chromeLabels.operatorsDescription}</p>
|
|
96
242
|
<div className="workbench-field-remap-convert-palette__operator-actions">
|
|
97
243
|
{onAddCombine ? (
|
|
98
|
-
<
|
|
244
|
+
<IconButton
|
|
99
245
|
compact
|
|
100
246
|
type="button"
|
|
101
247
|
data-testid="field-remap-add-combine"
|
|
248
|
+
icon="codicon-git-merge"
|
|
249
|
+
label={chromeLabels.addCombine}
|
|
102
250
|
onClick={onAddCombine}
|
|
103
|
-
|
|
104
|
-
Add combine
|
|
105
|
-
</Button>
|
|
251
|
+
/>
|
|
106
252
|
) : null}
|
|
107
253
|
{onAddSplit ? (
|
|
108
|
-
<
|
|
254
|
+
<IconButton
|
|
109
255
|
compact
|
|
110
256
|
type="button"
|
|
111
257
|
data-testid="field-remap-add-split"
|
|
258
|
+
icon="codicon-split-horizontal"
|
|
259
|
+
label={chromeLabels.addSplit}
|
|
112
260
|
onClick={onAddSplit}
|
|
113
|
-
|
|
114
|
-
Add split
|
|
115
|
-
</Button>
|
|
261
|
+
/>
|
|
116
262
|
) : null}
|
|
117
263
|
</div>
|
|
118
264
|
</div>
|
package/src/field-remap/demo.tsx
CHANGED
|
@@ -2,9 +2,13 @@ import { useEffect, useRef, useState, type JSX } from 'react';
|
|
|
2
2
|
import { Button } from '@workbench-kit/react/primitives';
|
|
3
3
|
|
|
4
4
|
import type { FieldRemapChromeLabels, FieldRemapTranslate } from './chrome-labels.js';
|
|
5
|
-
import type { FieldRemapFlowActions } from './flow.js';
|
|
5
|
+
import type { FieldRemapFlowActions, FieldRemapFlowMapperProps } from './flow.js';
|
|
6
6
|
import type { FieldRemapIoChrome } from './io-class-browse.js';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
FieldRemapPanel,
|
|
9
|
+
type FieldRemapHistoryActions,
|
|
10
|
+
type FieldRemapHistoryAvailability,
|
|
11
|
+
} from './panel.js';
|
|
8
12
|
import {
|
|
9
13
|
getFieldRemapBrowseDemoShapes,
|
|
10
14
|
getFieldRemapSample,
|
|
@@ -15,7 +19,13 @@ export interface SampleFieldRemapDemoProps {
|
|
|
15
19
|
readonly sampleId?: FieldRemapSampleId | string | undefined;
|
|
16
20
|
/** Forwarded to Flow mapper (default true). */
|
|
17
21
|
readonly showMinimap?: boolean | undefined;
|
|
18
|
-
/**
|
|
22
|
+
/** Forwarded Flow chrome preset. */
|
|
23
|
+
readonly chrome?: FieldRemapFlowMapperProps['chrome'];
|
|
24
|
+
readonly showFlowHint?: FieldRemapFlowMapperProps['showFlowHint'];
|
|
25
|
+
readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
|
|
26
|
+
readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
|
|
27
|
+
readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
|
|
28
|
+
/** When true, show host-chrome demo controls (history + fit view). */
|
|
19
29
|
readonly showHostChromeDemo?: boolean | undefined;
|
|
20
30
|
/** Prefer `browse` for I/O class/field inspection demos. */
|
|
21
31
|
readonly ioChrome?: FieldRemapIoChrome | undefined;
|
|
@@ -31,6 +41,11 @@ export interface SampleFieldRemapDemoProps {
|
|
|
31
41
|
export function SampleFieldRemapDemo({
|
|
32
42
|
sampleId = 'nested-ab',
|
|
33
43
|
showMinimap: showMinimapProp,
|
|
44
|
+
chrome,
|
|
45
|
+
showFlowHint,
|
|
46
|
+
showBindingsList,
|
|
47
|
+
showConvertPalette,
|
|
48
|
+
emptyDetail,
|
|
34
49
|
showHostChromeDemo = false,
|
|
35
50
|
ioChrome,
|
|
36
51
|
browseSeedShapes = false,
|
|
@@ -40,7 +55,12 @@ export function SampleFieldRemapDemo({
|
|
|
40
55
|
const sample = getFieldRemapSample(sampleId);
|
|
41
56
|
const browseShapes = browseSeedShapes ? getFieldRemapBrowseDemoShapes() : null;
|
|
42
57
|
const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
|
|
58
|
+
const historyActionsRef = useRef<FieldRemapHistoryActions | null>(null);
|
|
43
59
|
const [showMinimap, setShowMinimap] = useState(showMinimapProp ?? true);
|
|
60
|
+
const [historyAvailability, setHistoryAvailability] = useState<FieldRemapHistoryAvailability>({
|
|
61
|
+
canUndo: false,
|
|
62
|
+
canRedo: false,
|
|
63
|
+
});
|
|
44
64
|
const [lastPaneMenu, setLastPaneMenu] = useState<string | null>(null);
|
|
45
65
|
|
|
46
66
|
useEffect(() => {
|
|
@@ -65,6 +85,24 @@ export function SampleFieldRemapDemo({
|
|
|
65
85
|
>
|
|
66
86
|
Fit view
|
|
67
87
|
</Button>
|
|
88
|
+
<Button
|
|
89
|
+
compact
|
|
90
|
+
type="button"
|
|
91
|
+
data-testid="field-remap-undo"
|
|
92
|
+
disabled={!historyAvailability.canUndo}
|
|
93
|
+
onClick={() => historyActionsRef.current?.undo()}
|
|
94
|
+
>
|
|
95
|
+
Undo
|
|
96
|
+
</Button>
|
|
97
|
+
<Button
|
|
98
|
+
compact
|
|
99
|
+
type="button"
|
|
100
|
+
data-testid="field-remap-redo"
|
|
101
|
+
disabled={!historyAvailability.canRedo}
|
|
102
|
+
onClick={() => historyActionsRef.current?.redo()}
|
|
103
|
+
>
|
|
104
|
+
Redo
|
|
105
|
+
</Button>
|
|
68
106
|
{lastPaneMenu ? (
|
|
69
107
|
<span data-testid="field-remap-pane-menu-log" role="status">
|
|
70
108
|
Pane menu: {lastPaneMenu}
|
|
@@ -76,6 +114,11 @@ export function SampleFieldRemapDemo({
|
|
|
76
114
|
key={`${sample.id}:${ioChrome ?? 'default'}:${browseSeedShapes ? 'seed' : 'plain'}`}
|
|
77
115
|
sample={sample}
|
|
78
116
|
showMinimap={showMinimap}
|
|
117
|
+
chrome={chrome}
|
|
118
|
+
showFlowHint={showFlowHint}
|
|
119
|
+
showBindingsList={showBindingsList}
|
|
120
|
+
showConvertPalette={showConvertPalette}
|
|
121
|
+
emptyDetail={emptyDetail}
|
|
79
122
|
onShowMinimapChange={setShowMinimap}
|
|
80
123
|
ioChrome={ioChrome}
|
|
81
124
|
editableShapes={ioChrome === 'browse' ? false : undefined}
|
|
@@ -84,6 +127,8 @@ export function SampleFieldRemapDemo({
|
|
|
84
127
|
labels={labels}
|
|
85
128
|
t={t}
|
|
86
129
|
flowActionsRef={showHostChromeDemo ? flowActionsRef : undefined}
|
|
130
|
+
historyActionsRef={showHostChromeDemo ? historyActionsRef : undefined}
|
|
131
|
+
onHistoryAvailabilityChange={showHostChromeDemo ? setHistoryAvailability : undefined}
|
|
87
132
|
onPaneContextMenu={
|
|
88
133
|
showHostChromeDemo
|
|
89
134
|
? (event) => {
|
|
@@ -45,6 +45,8 @@ export interface FieldRemapDetailPanelProps {
|
|
|
45
45
|
readonly onDiscardDraft?: ((localId: string) => void) | undefined;
|
|
46
46
|
readonly operators?: readonly MappingOperator[] | undefined;
|
|
47
47
|
readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
|
|
48
|
+
readonly emptyDetailTitle?: string | undefined;
|
|
49
|
+
readonly emptyDetailDescription?: string | undefined;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
function fieldLabel(
|
|
@@ -77,6 +79,8 @@ export function FieldRemapDetailPanel({
|
|
|
77
79
|
onDiscardDraft,
|
|
78
80
|
operators = [],
|
|
79
81
|
onOperatorsChange,
|
|
82
|
+
emptyDetailTitle = 'Start with a convert',
|
|
83
|
+
emptyDetailDescription = 'Use the Convert palette to place a convert, then wire source → draft → target. Or select an existing binding / convert note on the canvas.',
|
|
80
84
|
}: FieldRemapDetailPanelProps): JSX.Element | null {
|
|
81
85
|
const edge = useMemo(() => {
|
|
82
86
|
if (!selection || (selection.kind !== 'edge' && selection.kind !== 'transformStep')) {
|
|
@@ -113,11 +117,8 @@ export function FieldRemapDetailPanel({
|
|
|
113
117
|
data-testid="field-remap-detail"
|
|
114
118
|
aria-label="Binding details"
|
|
115
119
|
>
|
|
116
|
-
<h4>
|
|
117
|
-
<p data-testid="field-remap-detail-empty-hint">
|
|
118
|
-
Use the Convert palette to place a convert, then wire source → draft → target. Or select
|
|
119
|
-
an existing binding / convert note on the canvas.
|
|
120
|
-
</p>
|
|
120
|
+
<h4>{emptyDetailTitle}</h4>
|
|
121
|
+
<p data-testid="field-remap-detail-empty-hint">{emptyDetailDescription}</p>
|
|
121
122
|
</aside>
|
|
122
123
|
);
|
|
123
124
|
}
|