@workbench-kit/shell-react 0.0.2-prototype.0.2.10

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.
Files changed (105) hide show
  1. package/README.md +98 -0
  2. package/package.json +51 -0
  3. package/src/chat/ai-mock-proposals.ts +39 -0
  4. package/src/chat/command-input.ts +52 -0
  5. package/src/chat/command-policy.ts +54 -0
  6. package/src/chat/command-surface.tsx +155 -0
  7. package/src/chat/use-chat-command-proposals.ts +194 -0
  8. package/src/chat/view-data.ts +20 -0
  9. package/src/chat/view.tsx +220 -0
  10. package/src/commands/inspector-surface.tsx +46 -0
  11. package/src/commands/use-command-descriptors.ts +41 -0
  12. package/src/commands/use-context-key-revision.ts +18 -0
  13. package/src/commands/view-data.ts +17 -0
  14. package/src/commands/view.tsx +43 -0
  15. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
  16. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
  17. package/src/devtools/index.ts +11 -0
  18. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
  19. package/src/devtools/workbench-devtools-snapshot.ts +140 -0
  20. package/src/devtools/workbench-devtools.css +82 -0
  21. package/src/editor/area-dnd.ts +69 -0
  22. package/src/editor/area-model.ts +71 -0
  23. package/src/editor/area.css +454 -0
  24. package/src/editor/area.tsx +676 -0
  25. package/src/editor/host-surface.tsx +691 -0
  26. package/src/editor/markdown-document-view-data.ts +16 -0
  27. package/src/editor/pane-visibility.ts +130 -0
  28. package/src/editor/resource.ts +36 -0
  29. package/src/editor/state-storage.ts +182 -0
  30. package/src/editor/tab-context-menu.ts +118 -0
  31. package/src/editor/use-editor.ts +138 -0
  32. package/src/editor/view-providers.tsx +504 -0
  33. package/src/editor/workspace-file-availability.ts +15 -0
  34. package/src/editor/workspace-reconcile.tsx +31 -0
  35. package/src/explorer/context-menu.ts +114 -0
  36. package/src/explorer/create-command-workspace-explorer-port.ts +86 -0
  37. package/src/explorer/reveal.ts +112 -0
  38. package/src/explorer/search-view-data.ts +15 -0
  39. package/src/explorer/search-view.tsx +65 -0
  40. package/src/explorer/use-active-workspace-path.ts +15 -0
  41. package/src/explorer/view-data.ts +58 -0
  42. package/src/explorer/view.tsx +227 -0
  43. package/src/extensions/context-menu.ts +52 -0
  44. package/src/extensions/management-model.ts +313 -0
  45. package/src/extensions/use-extension-management.ts +241 -0
  46. package/src/extensions/view-data.ts +19 -0
  47. package/src/extensions/view.tsx +63 -0
  48. package/src/field-remap/chrome-labels.ts +74 -0
  49. package/src/field-remap/convert-note-editor.tsx +206 -0
  50. package/src/field-remap/convert-palette.tsx +126 -0
  51. package/src/field-remap/demo.tsx +105 -0
  52. package/src/field-remap/detail-panel.tsx +659 -0
  53. package/src/field-remap/editor-surface.tsx +36 -0
  54. package/src/field-remap/flow-adapter.ts +838 -0
  55. package/src/field-remap/flow-ops.ts +442 -0
  56. package/src/field-remap/flow.tsx +948 -0
  57. package/src/field-remap/graph.tsx +559 -0
  58. package/src/field-remap/index.ts +88 -0
  59. package/src/field-remap/io-class-browse.tsx +145 -0
  60. package/src/field-remap/jsonata-transform.ts +166 -0
  61. package/src/field-remap/panel.tsx +466 -0
  62. package/src/field-remap/samples.ts +507 -0
  63. package/src/field-remap/shape-io-editor.tsx +170 -0
  64. package/src/field-remap/table-mapping-adapter.ts +59 -0
  65. package/src/field-remap/transform-options-editor.tsx +312 -0
  66. package/src/field-remap/tree.tsx +404 -0
  67. package/src/field-remap/view-data.ts +16 -0
  68. package/src/field-remap/view.css +1367 -0
  69. package/src/field-remap/view.tsx +86 -0
  70. package/src/index.ts +294 -0
  71. package/src/jdw/document-view-data.ts +31 -0
  72. package/src/jdw/json-form-source-patch.ts +323 -0
  73. package/src/jdw/lab-view-data.ts +25 -0
  74. package/src/jdw/lab-view.css +21 -0
  75. package/src/jdw/lab-view.tsx +79 -0
  76. package/src/jdw/widget-form-view.tsx +68 -0
  77. package/src/jdw/widget-preview-view.tsx +54 -0
  78. package/src/management/keybinding-overrides-storage.ts +49 -0
  79. package/src/management/keybinding-settings.tsx +16 -0
  80. package/src/management/palette-commands.ts +38 -0
  81. package/src/management/preference-settings-storage.ts +50 -0
  82. package/src/management/settings-ids.ts +12 -0
  83. package/src/management/settings.tsx +96 -0
  84. package/src/management/use-command-management.ts +191 -0
  85. package/src/management/use-keybinding-management.ts +130 -0
  86. package/src/shell/model.tsx +149 -0
  87. package/src/shell/provider.tsx +606 -0
  88. package/src/shell/secondary-actions.tsx +59 -0
  89. package/src/shell/settings-constants.ts +9 -0
  90. package/src/shell/settings.tsx +599 -0
  91. package/src/shell/shell.tsx +680 -0
  92. package/src/shell/titlebar-layout-controls.tsx +4 -0
  93. package/src/shell/view-host.tsx +212 -0
  94. package/src/workbench/appearance-storage.ts +107 -0
  95. package/src/workbench/command-host.tsx +243 -0
  96. package/src/workbench/command-palette.ts +208 -0
  97. package/src/workbench/keybinding-bridge.ts +54 -0
  98. package/src/workbench/layout-storage.ts +134 -0
  99. package/src/workbench/profile-modal.tsx +118 -0
  100. package/src/workbench/shell-command-registration.ts +58 -0
  101. package/src/workbench/startup-gate.tsx +136 -0
  102. package/src/workbench/status-sections.ts +43 -0
  103. package/src/workbench/use-persisted-appearance.ts +33 -0
  104. package/src/workbench/user-commands.ts +46 -0
  105. package/src/workbench/workspace-view-state.ts +35 -0
@@ -0,0 +1,659 @@
1
+ import { useMemo, useState, type JSX } from 'react';
2
+ import { Button } from '@workbench-kit/react/primitives';
3
+ import {
4
+ MAX_TRANSFORM_CHAIN,
5
+ findSourceField,
6
+ findTargetSlot,
7
+ flattenSourceFields,
8
+ flattenTargetSlots,
9
+ type MappingEdge,
10
+ type MappingOperator,
11
+ type SourceField,
12
+ type TargetSlot,
13
+ type ValueTransformRegistry,
14
+ } from '@workbench-kit/field-remap';
15
+
16
+ import { ConvertNoteEditor } from './convert-note-editor.js';
17
+ import {
18
+ addTransformStepToEdge,
19
+ bindOperatorInput,
20
+ bindOperatorOutput,
21
+ canEditListContext,
22
+ edgePortTypes,
23
+ enableListContextOnEdge,
24
+ listCompatibleTransforms,
25
+ removeItemEdgeFromParent,
26
+ removeMappingOperator,
27
+ removeOperatorInput,
28
+ removeOperatorOutput,
29
+ removeTransformStepFromEdge,
30
+ updateMappingOperator,
31
+ upsertItemEdgeOnParent,
32
+ type FieldRemapDraftTransform,
33
+ type FieldRemapSelection,
34
+ } from './flow-ops.js';
35
+
36
+ export interface FieldRemapDetailPanelProps {
37
+ readonly selection: FieldRemapSelection;
38
+ readonly edges: readonly MappingEdge[];
39
+ readonly sources: readonly SourceField[];
40
+ readonly targets: readonly TargetSlot[];
41
+ readonly transforms: ValueTransformRegistry;
42
+ readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
43
+ readonly onSelectionChange: (next: FieldRemapSelection) => void;
44
+ readonly drafts?: readonly FieldRemapDraftTransform[] | undefined;
45
+ readonly onDiscardDraft?: ((localId: string) => void) | undefined;
46
+ readonly operators?: readonly MappingOperator[] | undefined;
47
+ readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
48
+ }
49
+
50
+ function fieldLabel(
51
+ id: string,
52
+ sources: readonly SourceField[],
53
+ targets: readonly TargetSlot[],
54
+ ): string {
55
+ const source = findSourceField(sources, id);
56
+ if (source) {
57
+ return source.path ?? source.label;
58
+ }
59
+ const target = findTargetSlot(targets, id);
60
+ return target?.path ?? target?.label ?? id;
61
+ }
62
+
63
+ /**
64
+ * Selection-driven side rail for Flow:
65
+ * - edge / empty → light binding detail (chain overview, palette, list context)
66
+ * - transformStep → dedicated {@link ConvertNoteEditor} surface
67
+ */
68
+ export function FieldRemapDetailPanel({
69
+ selection,
70
+ edges,
71
+ sources,
72
+ targets,
73
+ transforms,
74
+ onEdgesChange,
75
+ onSelectionChange,
76
+ drafts = [],
77
+ onDiscardDraft,
78
+ operators = [],
79
+ onOperatorsChange,
80
+ }: FieldRemapDetailPanelProps): JSX.Element | null {
81
+ const edge = useMemo(() => {
82
+ if (!selection || (selection.kind !== 'edge' && selection.kind !== 'transformStep')) {
83
+ return undefined;
84
+ }
85
+ return edges.find((item) => item.id === selection.edgeId);
86
+ }, [edges, selection]);
87
+
88
+ const draft = useMemo(() => {
89
+ if (!selection || selection.kind !== 'draft') {
90
+ return undefined;
91
+ }
92
+ return drafts.find((item) => item.localId === selection.localId);
93
+ }, [drafts, selection]);
94
+
95
+ const operator = useMemo(() => {
96
+ if (!selection || selection.kind !== 'operator') {
97
+ return undefined;
98
+ }
99
+ return operators.find((item) => item.id === selection.operatorId);
100
+ }, [operators, selection]);
101
+
102
+ const [pendingItemSourceId, setPendingItemSourceId] = useState<string | null>(null);
103
+ const [paletteId, setPaletteId] = useState<string>('');
104
+ const [pendingOperatorFieldId, setPendingOperatorFieldId] = useState('');
105
+ const [pendingOperatorSlotId, setPendingOperatorSlotId] = useState('');
106
+ const flatSources = useMemo(() => flattenSourceFields(sources), [sources]);
107
+ const flatTargets = useMemo(() => flattenTargetSlots(targets), [targets]);
108
+
109
+ if (!selection) {
110
+ return (
111
+ <aside
112
+ className="workbench-field-remap-detail workbench-field-remap-detail--empty"
113
+ data-testid="field-remap-detail"
114
+ aria-label="Binding details"
115
+ >
116
+ <h4>Start with a convert</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>
121
+ </aside>
122
+ );
123
+ }
124
+
125
+ if (selection.kind === 'draft') {
126
+ const definition = draft ? transforms.get(draft.transformId) : undefined;
127
+ return (
128
+ <aside
129
+ className="workbench-field-remap-detail workbench-field-remap-detail--draft"
130
+ data-testid="field-remap-detail"
131
+ aria-label="Draft convert"
132
+ >
133
+ <div className="workbench-field-remap-detail__header">
134
+ <div>
135
+ <h4>Draft convert</h4>
136
+ <code data-testid="field-remap-detail-draft-id">
137
+ {definition?.label ?? draft?.transformId ?? selection.localId}
138
+ </code>
139
+ </div>
140
+ <Button
141
+ compact
142
+ type="button"
143
+ data-testid="field-remap-detail-discard-draft"
144
+ onClick={() => {
145
+ onDiscardDraft?.(selection.localId);
146
+ onSelectionChange(null);
147
+ }}
148
+ >
149
+ Discard
150
+ </Button>
151
+ </div>
152
+ <p className="workbench-field-remap-detail__muted">
153
+ Wire source then target ports. When both bind, the convert note editor opens.
154
+ </p>
155
+ <dl
156
+ className="workbench-field-remap-detail__draft-ports"
157
+ data-testid="field-remap-detail-draft-ports"
158
+ >
159
+ <div>
160
+ <dt>Source</dt>
161
+ <dd>
162
+ <code>{draft?.sourceFieldId ?? 'Unwired'}</code>
163
+ </dd>
164
+ </div>
165
+ <div>
166
+ <dt>Target</dt>
167
+ <dd>
168
+ <code>{draft?.targetSlotId ?? 'Unwired'}</code>
169
+ </dd>
170
+ </div>
171
+ </dl>
172
+ </aside>
173
+ );
174
+ }
175
+
176
+ if (selection.kind === 'operator') {
177
+ if (!operator || !onOperatorsChange) {
178
+ return (
179
+ <aside className="workbench-field-remap-detail" data-testid="field-remap-detail">
180
+ <h4>Operator</h4>
181
+ <code data-testid="field-remap-detail-operator-id">{selection.operatorId}</code>
182
+ <p className="workbench-field-remap-detail__muted">
183
+ Host must wire onOperatorsChange for authoring.
184
+ </p>
185
+ </aside>
186
+ );
187
+ }
188
+ return (
189
+ <aside
190
+ className="workbench-field-remap-detail workbench-field-remap-detail--operator"
191
+ data-testid="field-remap-detail"
192
+ aria-label="Operator details"
193
+ >
194
+ <div className="workbench-field-remap-detail__header">
195
+ <div>
196
+ <h4>{operator.kind === 'combine' ? 'Combine (n→1)' : 'Split (1→n)'}</h4>
197
+ <code data-testid="field-remap-detail-operator-id">{operator.id}</code>
198
+ </div>
199
+ <Button
200
+ compact
201
+ type="button"
202
+ data-testid="field-remap-detail-delete-operator"
203
+ onClick={() => {
204
+ onOperatorsChange(removeMappingOperator(operators, operator.id));
205
+ onSelectionChange(null);
206
+ }}
207
+ >
208
+ Delete
209
+ </Button>
210
+ </div>
211
+ {operator.kind === 'combine' ? (
212
+ <section className="workbench-field-remap-detail__section">
213
+ <h5>Inputs</h5>
214
+ <ul className="workbench-field-remap-detail__item-edges">
215
+ {operator.inputFieldIds.map((fieldId) => (
216
+ <li key={fieldId}>
217
+ <code>{fieldLabel(fieldId, sources, targets)}</code>
218
+ <Button
219
+ compact
220
+ type="button"
221
+ onClick={() =>
222
+ onOperatorsChange(
223
+ updateMappingOperator(operators, operator.id, (current) =>
224
+ removeOperatorInput(current, fieldId),
225
+ ),
226
+ )
227
+ }
228
+ >
229
+ Remove
230
+ </Button>
231
+ </li>
232
+ ))}
233
+ </ul>
234
+ <div className="workbench-field-remap-detail__field">
235
+ <label>
236
+ <span>Add input</span>
237
+ <select
238
+ data-testid="field-remap-operator-add-input"
239
+ value={pendingOperatorFieldId}
240
+ onChange={(event) => setPendingOperatorFieldId(event.target.value)}
241
+ >
242
+ <option value="">Select…</option>
243
+ {flatSources.map((field) => (
244
+ <option key={field.id} value={field.id}>
245
+ {field.path ?? field.label}
246
+ </option>
247
+ ))}
248
+ </select>
249
+ </label>
250
+ <Button
251
+ compact
252
+ type="button"
253
+ data-testid="field-remap-operator-bind-input"
254
+ disabled={!pendingOperatorFieldId}
255
+ onClick={() => {
256
+ if (!pendingOperatorFieldId) return;
257
+ onOperatorsChange(
258
+ updateMappingOperator(operators, operator.id, (current) =>
259
+ bindOperatorInput(current, pendingOperatorFieldId),
260
+ ),
261
+ );
262
+ setPendingOperatorFieldId('');
263
+ }}
264
+ >
265
+ Add
266
+ </Button>
267
+ </div>
268
+ <div className="workbench-field-remap-detail__field">
269
+ <label>
270
+ <span>Output slot</span>
271
+ <select
272
+ data-testid="field-remap-operator-output"
273
+ value={operator.outputSlotId}
274
+ onChange={(event) => {
275
+ const slotId = event.target.value;
276
+ onOperatorsChange(
277
+ updateMappingOperator(operators, operator.id, (current) =>
278
+ slotId
279
+ ? bindOperatorOutput(current, slotId)
280
+ : removeOperatorOutput(current, operator.outputSlotId),
281
+ ),
282
+ );
283
+ }}
284
+ >
285
+ <option value="">Unwired…</option>
286
+ {flatTargets.map((slot) => (
287
+ <option key={slot.id} value={slot.id}>
288
+ {slot.path ?? slot.label}
289
+ </option>
290
+ ))}
291
+ </select>
292
+ </label>
293
+ </div>
294
+ </section>
295
+ ) : (
296
+ <section className="workbench-field-remap-detail__section">
297
+ <div className="workbench-field-remap-detail__field">
298
+ <label>
299
+ <span>Input field</span>
300
+ <select
301
+ data-testid="field-remap-operator-input"
302
+ value={operator.inputFieldId}
303
+ onChange={(event) => {
304
+ const fieldId = event.target.value;
305
+ onOperatorsChange(
306
+ updateMappingOperator(operators, operator.id, (current) =>
307
+ fieldId
308
+ ? bindOperatorInput(current, fieldId)
309
+ : removeOperatorInput(current, operator.inputFieldId),
310
+ ),
311
+ );
312
+ }}
313
+ >
314
+ <option value="">Unwired…</option>
315
+ {flatSources.map((field) => (
316
+ <option key={field.id} value={field.id}>
317
+ {field.path ?? field.label}
318
+ </option>
319
+ ))}
320
+ </select>
321
+ </label>
322
+ </div>
323
+ <h5>Outputs</h5>
324
+ <ul className="workbench-field-remap-detail__item-edges">
325
+ {operator.outputSlotIds.map((slotId) => (
326
+ <li key={slotId}>
327
+ <code>{fieldLabel(slotId, sources, targets)}</code>
328
+ <Button
329
+ compact
330
+ type="button"
331
+ onClick={() =>
332
+ onOperatorsChange(
333
+ updateMappingOperator(operators, operator.id, (current) =>
334
+ removeOperatorOutput(current, slotId),
335
+ ),
336
+ )
337
+ }
338
+ >
339
+ Remove
340
+ </Button>
341
+ </li>
342
+ ))}
343
+ </ul>
344
+ <div className="workbench-field-remap-detail__field">
345
+ <label>
346
+ <span>Add output</span>
347
+ <select
348
+ data-testid="field-remap-operator-add-output"
349
+ value={pendingOperatorSlotId}
350
+ onChange={(event) => setPendingOperatorSlotId(event.target.value)}
351
+ >
352
+ <option value="">Select…</option>
353
+ {flatTargets.map((slot) => (
354
+ <option key={slot.id} value={slot.id}>
355
+ {slot.path ?? slot.label}
356
+ </option>
357
+ ))}
358
+ </select>
359
+ </label>
360
+ <Button
361
+ compact
362
+ type="button"
363
+ data-testid="field-remap-operator-bind-output"
364
+ disabled={!pendingOperatorSlotId}
365
+ onClick={() => {
366
+ if (!pendingOperatorSlotId) return;
367
+ onOperatorsChange(
368
+ updateMappingOperator(operators, operator.id, (current) =>
369
+ bindOperatorOutput(current, pendingOperatorSlotId),
370
+ ),
371
+ );
372
+ setPendingOperatorSlotId('');
373
+ }}
374
+ >
375
+ Add
376
+ </Button>
377
+ </div>
378
+ </section>
379
+ )}
380
+ </aside>
381
+ );
382
+ }
383
+
384
+ if (!edge) {
385
+ return (
386
+ <aside
387
+ className="workbench-field-remap-detail workbench-field-remap-detail--empty"
388
+ data-testid="field-remap-detail"
389
+ aria-label="Binding details"
390
+ >
391
+ <p>Select a binding for mapping details, or a convert node to open the convert editor.</p>
392
+ </aside>
393
+ );
394
+ }
395
+
396
+ if (selection.kind === 'transformStep') {
397
+ return (
398
+ <ConvertNoteEditor
399
+ edge={edge}
400
+ stepIndex={selection.stepIndex}
401
+ sources={sources}
402
+ targets={targets}
403
+ transforms={transforms}
404
+ edges={edges}
405
+ onEdgesChange={onEdgesChange}
406
+ onSelectionChange={onSelectionChange}
407
+ />
408
+ );
409
+ }
410
+
411
+ const portTypes = edgePortTypes(edge, sources, targets);
412
+ const chain = edge.transformIds ?? [];
413
+
414
+ const appendCatalog = listCompatibleTransforms({
415
+ registry: transforms,
416
+ edge,
417
+ stepIndex: chain.length,
418
+ sourceType: portTypes.sourceType,
419
+ targetType: portTypes.targetType,
420
+ mode: 'append',
421
+ });
422
+
423
+ const defaultPaletteId = appendCatalog[0]?.id ?? '';
424
+ const effectivePaletteId =
425
+ paletteId && appendCatalog.some((item) => item.id === paletteId) ? paletteId : defaultPaletteId;
426
+
427
+ const listContextEnabled = canEditListContext(edge, sources, targets);
428
+ const sourceField = findSourceField(sources, edge.sourceFieldId);
429
+ const targetSlot = findTargetSlot(targets, edge.targetSlotId);
430
+ const itemSourceChildren = sourceField?.children ?? [];
431
+ const itemTargetChildren = targetSlot?.children ?? [];
432
+
433
+ const applyEdge = (next: MappingEdge | null) => {
434
+ if (!next) {
435
+ return;
436
+ }
437
+ onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
438
+ };
439
+
440
+ return (
441
+ <aside
442
+ className="workbench-field-remap-detail"
443
+ data-testid="field-remap-detail"
444
+ aria-label="Binding details"
445
+ >
446
+ <div className="workbench-field-remap-detail__header">
447
+ <div>
448
+ <h4>Binding</h4>
449
+ <code data-testid="field-remap-detail-binding">
450
+ {edge.sourceFieldId} → {edge.targetSlotId}
451
+ </code>
452
+ </div>
453
+ <Button
454
+ compact
455
+ type="button"
456
+ data-testid="field-remap-detail-clear"
457
+ onClick={() => onSelectionChange(null)}
458
+ >
459
+ Clear
460
+ </Button>
461
+ </div>
462
+
463
+ <section
464
+ className="workbench-field-remap-detail__section"
465
+ aria-labelledby="field-remap-detail-chain"
466
+ >
467
+ <h5 id="field-remap-detail-chain">Convert chain</h5>
468
+ {chain.length === 0 ? (
469
+ <p className="workbench-field-remap-detail__muted">Direct (no convert)</p>
470
+ ) : (
471
+ <ol className="workbench-field-remap-detail__steps">
472
+ {chain.map((transformId, index) => {
473
+ const definition = transforms.get(transformId);
474
+ return (
475
+ <li key={`${edge.id}-${index}-${transformId}`}>
476
+ <button
477
+ type="button"
478
+ className="workbench-field-remap-detail__step"
479
+ data-testid={`field-remap-detail-step-${index}`}
480
+ onClick={() =>
481
+ onSelectionChange({
482
+ kind: 'transformStep',
483
+ edgeId: edge.id,
484
+ stepIndex: index,
485
+ })
486
+ }
487
+ >
488
+ <strong>{definition?.label ?? transformId}</strong>
489
+ <code>{transformId}</code>
490
+ <span className="workbench-field-remap-detail__step-hint">Edit convert</span>
491
+ </button>
492
+ <Button
493
+ compact
494
+ type="button"
495
+ aria-label={`Remove convert step ${index + 1}`}
496
+ data-testid={`field-remap-detail-remove-step-${index}`}
497
+ onClick={() => {
498
+ const next = removeTransformStepFromEdge(edge, index);
499
+ applyEdge(next);
500
+ }}
501
+ >
502
+ ×
503
+ </Button>
504
+ </li>
505
+ );
506
+ })}
507
+ </ol>
508
+ )}
509
+
510
+ {chain.length < MAX_TRANSFORM_CHAIN ? (
511
+ <div
512
+ className="workbench-field-remap-detail__palette"
513
+ data-testid="field-remap-transform-palette"
514
+ >
515
+ <label>
516
+ <span>Add convert</span>
517
+ <select
518
+ aria-label="Convert palette"
519
+ data-testid="field-remap-palette-select"
520
+ value={effectivePaletteId}
521
+ disabled={appendCatalog.length === 0}
522
+ onChange={(event) => setPaletteId(event.target.value)}
523
+ >
524
+ {appendCatalog.length === 0 ? (
525
+ <option value="">No compatible converts</option>
526
+ ) : null}
527
+ {appendCatalog.map((item) => (
528
+ <option key={item.id} value={item.id}>
529
+ {item.label}
530
+ </option>
531
+ ))}
532
+ </select>
533
+ </label>
534
+ <Button
535
+ compact
536
+ type="button"
537
+ data-testid="field-remap-palette-add"
538
+ disabled={!effectivePaletteId}
539
+ onClick={() => {
540
+ const next = addTransformStepToEdge(edge, effectivePaletteId, {
541
+ registry: transforms,
542
+ sourceType: portTypes.sourceType,
543
+ targetType: portTypes.targetType,
544
+ });
545
+ if (!next) {
546
+ return;
547
+ }
548
+ applyEdge(next);
549
+ onSelectionChange({
550
+ kind: 'transformStep',
551
+ edgeId: edge.id,
552
+ stepIndex: (next.transformIds?.length ?? 1) - 1,
553
+ });
554
+ }}
555
+ >
556
+ Add
557
+ </Button>
558
+ </div>
559
+ ) : null}
560
+ </section>
561
+
562
+ {listContextEnabled ? (
563
+ <section
564
+ className="workbench-field-remap-detail__section"
565
+ aria-labelledby="field-remap-detail-list-context"
566
+ data-testid="field-remap-list-context"
567
+ >
568
+ <div className="workbench-field-remap-detail__list-context-bar">
569
+ <h5 id="field-remap-detail-list-context">List context</h5>
570
+ {!edge.itemEdges ? (
571
+ <Button
572
+ compact
573
+ type="button"
574
+ data-testid="field-remap-enable-list-context"
575
+ onClick={() => applyEdge(enableListContextOnEdge(edge))}
576
+ >
577
+ Enable item fields
578
+ </Button>
579
+ ) : null}
580
+ </div>
581
+
582
+ {edge.itemEdges ? (
583
+ <>
584
+ <p className="workbench-field-remap-detail__muted">
585
+ {pendingItemSourceId
586
+ ? 'Select a target item field to complete the item binding.'
587
+ : 'Select a source item field, then a target item field.'}
588
+ </p>
589
+ <div className="workbench-field-remap-detail__item-pickers">
590
+ <label>
591
+ <span>Source item field</span>
592
+ <select
593
+ aria-label="Source item field"
594
+ data-testid="field-remap-item-source"
595
+ value={pendingItemSourceId ?? ''}
596
+ onChange={(event) => setPendingItemSourceId(event.target.value || null)}
597
+ >
598
+ <option value="">Select…</option>
599
+ {itemSourceChildren.map((child) => (
600
+ <option key={child.id} value={child.id}>
601
+ {child.path ?? child.label}
602
+ </option>
603
+ ))}
604
+ </select>
605
+ </label>
606
+ <label>
607
+ <span>Target item field</span>
608
+ <select
609
+ aria-label="Target item field"
610
+ data-testid="field-remap-item-target"
611
+ value=""
612
+ disabled={!pendingItemSourceId}
613
+ onChange={(event) => {
614
+ const targetId = event.target.value;
615
+ if (!pendingItemSourceId || !targetId) {
616
+ return;
617
+ }
618
+ const itemEdge: MappingEdge = {
619
+ id: `ie-${pendingItemSourceId}-${targetId}-${Date.now()}`,
620
+ sourceFieldId: pendingItemSourceId,
621
+ targetSlotId: targetId,
622
+ };
623
+ applyEdge(upsertItemEdgeOnParent(edge, itemEdge));
624
+ setPendingItemSourceId(null);
625
+ }}
626
+ >
627
+ <option value="">Select…</option>
628
+ {itemTargetChildren.map((child) => (
629
+ <option key={child.id} value={child.id}>
630
+ {child.path ?? child.label}
631
+ </option>
632
+ ))}
633
+ </select>
634
+ </label>
635
+ </div>
636
+ <ul className="workbench-field-remap-detail__item-edges">
637
+ {(edge.itemEdges ?? []).map((itemEdge) => (
638
+ <li key={itemEdge.id} data-testid={`field-remap-item-edge-${itemEdge.id}`}>
639
+ <code>
640
+ {fieldLabel(itemEdge.sourceFieldId, sources, targets)} →{' '}
641
+ {fieldLabel(itemEdge.targetSlotId, sources, targets)}
642
+ </code>
643
+ <Button
644
+ compact
645
+ type="button"
646
+ onClick={() => applyEdge(removeItemEdgeFromParent(edge, itemEdge.id))}
647
+ >
648
+ Remove
649
+ </Button>
650
+ </li>
651
+ ))}
652
+ </ul>
653
+ </>
654
+ ) : null}
655
+ </section>
656
+ ) : null}
657
+ </aside>
658
+ );
659
+ }
@@ -0,0 +1,36 @@
1
+ import type { JSX } from 'react';
2
+ import { ScrollArea } from '@workbench-kit/react/primitives';
3
+
4
+ import { SampleFieldRemapDemo } from './demo.js';
5
+ import { resolveFieldRemapSampleId } from './samples.js';
6
+ import './view.css';
7
+
8
+ export type FieldRemapEditorSurfaceId = string;
9
+
10
+ export interface FieldRemapEditorSurfaceProps {
11
+ readonly resourceUri: string;
12
+ readonly surfaceId: FieldRemapEditorSurfaceId;
13
+ readonly tabId: string;
14
+ }
15
+
16
+ export function FieldRemapEditorSurface({
17
+ resourceUri,
18
+ surfaceId,
19
+ tabId,
20
+ }: FieldRemapEditorSurfaceProps): JSX.Element {
21
+ const sampleId = resolveFieldRemapSampleId(surfaceId);
22
+
23
+ return (
24
+ <ScrollArea
25
+ aria-label={`${sampleId} field remap`}
26
+ className="workbench-field-remap-editor-surface"
27
+ data-editor-host-id={tabId}
28
+ data-resource-uri={resourceUri}
29
+ data-sample-id={sampleId}
30
+ data-testid="field-remap-editor-surface"
31
+ orientation="vertical"
32
+ >
33
+ <SampleFieldRemapDemo sampleId={sampleId} />
34
+ </ScrollArea>
35
+ );
36
+ }