@workbench-kit/shell-react 0.0.2-prototype.0.2.32 → 0.0.2-prototype.0.2.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/shell-react",
3
- "version": "0.0.2-prototype.0.2.32",
3
+ "version": "0.0.2-prototype.0.2.33",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -31,14 +31,14 @@
31
31
  "@radix-ui/react-slot": "^1.3.0",
32
32
  "@xyflow/react": "^12.11.2",
33
33
  "jsonata": "^2.2.0",
34
- "@workbench-kit/platform": "0.0.2-prototype.0.2.32",
35
- "@workbench-kit/field-remap": "0.0.2-prototype.0.2.32",
36
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.32",
37
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.32",
38
- "@workbench-kit/workbench-extension-sdk": "0.0.2-prototype.0.2.32",
39
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.32",
40
- "@workbench-kit/react": "0.0.2-prototype.0.2.32",
41
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.32"
34
+ "@workbench-kit/react": "0.0.2-prototype.0.2.33",
35
+ "@workbench-kit/field-remap": "0.0.2-prototype.0.2.33",
36
+ "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.33",
37
+ "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.33",
38
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.33",
39
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.33",
40
+ "@workbench-kit/workbench-extension-sdk": "0.0.2-prototype.0.2.33",
41
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.33"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": "^19.0.0",
@@ -19,12 +19,19 @@ export interface FieldRemapChromeLabels {
19
19
  readonly convertPaletteAriaLabel: string;
20
20
  readonly convertsListAriaLabel: string;
21
21
  readonly placeConvert: string;
22
+ readonly addCombine: string;
23
+ readonly addSplit: string;
22
24
  readonly operatorsTitle: string;
23
25
  readonly operatorsDescription: string;
26
+ readonly addTransform: string;
27
+ readonly editItems: string;
28
+ readonly removeBinding: string;
24
29
  readonly showMinimap: string;
25
30
  readonly hideMinimap: string;
26
31
  readonly showHiddenFields: string;
27
32
  readonly hideHiddenFields: string;
33
+ readonly emptyDetailTitle: string;
34
+ readonly emptyDetailDescription: string;
28
35
  }
29
36
 
30
37
  export const defaultFieldRemapChromeLabels: FieldRemapChromeLabels = {
@@ -35,13 +42,21 @@ export const defaultFieldRemapChromeLabels: FieldRemapChromeLabels = {
35
42
  convertPaletteAriaLabel: 'Convert palette',
36
43
  convertsListAriaLabel: 'Converts',
37
44
  placeConvert: 'Place convert',
45
+ addCombine: 'Add combine',
46
+ addSplit: 'Add split',
38
47
  operatorsTitle: 'n→m operators',
39
48
  operatorsDescription:
40
49
  'Create combine (n→1) or split (1→n), then wire ports or edit in the side rail.',
50
+ addTransform: 'Add convert',
51
+ editItems: 'Edit items',
52
+ removeBinding: 'Remove binding',
41
53
  showMinimap: 'Show minimap',
42
54
  hideMinimap: 'Hide minimap',
43
55
  showHiddenFields: 'Show hidden fields',
44
56
  hideHiddenFields: 'Hide hidden fields',
57
+ emptyDetailTitle: 'Start with a convert',
58
+ emptyDetailDescription:
59
+ 'Use the Convert palette to place a convert, then wire source → draft → target. Or select an existing binding / convert note on the canvas.',
45
60
  };
46
61
 
47
62
  /** Stable capability ids for optional `t()` injection (not free prose). */
@@ -52,12 +67,19 @@ export const fieldRemapChromeLabelKeys = {
52
67
  convertPaletteAriaLabel: 'fieldRemap.convertPaletteAriaLabel',
53
68
  convertsListAriaLabel: 'fieldRemap.convertsListAriaLabel',
54
69
  placeConvert: 'fieldRemap.placeConvert',
70
+ addCombine: 'fieldRemap.addCombine',
71
+ addSplit: 'fieldRemap.addSplit',
55
72
  operatorsTitle: 'fieldRemap.operatorsTitle',
56
73
  operatorsDescription: 'fieldRemap.operatorsDescription',
74
+ addTransform: 'fieldRemap.addTransform',
75
+ editItems: 'fieldRemap.editItems',
76
+ removeBinding: 'fieldRemap.removeBinding',
57
77
  showMinimap: 'fieldRemap.showMinimap',
58
78
  hideMinimap: 'fieldRemap.hideMinimap',
59
79
  showHiddenFields: 'fieldRemap.showHiddenFields',
60
80
  hideHiddenFields: 'fieldRemap.hideHiddenFields',
81
+ emptyDetailTitle: 'fieldRemap.emptyDetailTitle',
82
+ emptyDetailDescription: 'fieldRemap.emptyDetailDescription',
61
83
  } as const satisfies Record<keyof FieldRemapChromeLabels, string>;
62
84
 
63
85
  export function resolveFieldRemapChromeLabels(
@@ -80,11 +102,18 @@ export function resolveFieldRemapChromeLabels(
80
102
  convertPaletteAriaLabel: resolve('convertPaletteAriaLabel'),
81
103
  convertsListAriaLabel: resolve('convertsListAriaLabel'),
82
104
  placeConvert: resolve('placeConvert'),
105
+ addCombine: resolve('addCombine'),
106
+ addSplit: resolve('addSplit'),
83
107
  operatorsTitle: resolve('operatorsTitle'),
84
108
  operatorsDescription: resolve('operatorsDescription'),
109
+ addTransform: resolve('addTransform'),
110
+ editItems: resolve('editItems'),
111
+ removeBinding: resolve('removeBinding'),
85
112
  showMinimap: resolve('showMinimap'),
86
113
  hideMinimap: resolve('hideMinimap'),
87
114
  showHiddenFields: resolve('showHiddenFields'),
88
115
  hideHiddenFields: resolve('hideHiddenFields'),
116
+ emptyDetailTitle: resolve('emptyDetailTitle'),
117
+ emptyDetailDescription: resolve('emptyDetailDescription'),
89
118
  };
90
119
  }
@@ -1,6 +1,6 @@
1
1
  import type { JSX } from 'react';
2
2
  import { SideBarList, SideBarListItem } from '@workbench-kit/react/layout';
3
- import { Button } from '@workbench-kit/react/primitives';
3
+ import { 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';
@@ -45,19 +45,20 @@ export function FieldRemapConvertPalette({
45
45
  </header>
46
46
 
47
47
  <div className="workbench-field-remap-convert-palette__place">
48
- <Button
48
+ <IconButton
49
+ compact
49
50
  type="button"
50
51
  data-testid="field-remap-place-draft"
51
52
  disabled={!selectedTransformId}
53
+ icon="codicon-add"
54
+ label={chromeLabels.placeConvert}
52
55
  onClick={() => {
53
56
  if (!selectedTransformId) {
54
57
  return;
55
58
  }
56
59
  onPlaceDraft(selectedTransformId);
57
60
  }}
58
- >
59
- {chromeLabels.placeConvert}
60
- </Button>
61
+ />
61
62
  </div>
62
63
 
63
64
  <SideBarList
@@ -95,24 +96,24 @@ export function FieldRemapConvertPalette({
95
96
  <p>{chromeLabels.operatorsDescription}</p>
96
97
  <div className="workbench-field-remap-convert-palette__operator-actions">
97
98
  {onAddCombine ? (
98
- <Button
99
+ <IconButton
99
100
  compact
100
101
  type="button"
101
102
  data-testid="field-remap-add-combine"
103
+ icon="codicon-git-merge"
104
+ label={chromeLabels.addCombine}
102
105
  onClick={onAddCombine}
103
- >
104
- Add combine
105
- </Button>
106
+ />
106
107
  ) : null}
107
108
  {onAddSplit ? (
108
- <Button
109
+ <IconButton
109
110
  compact
110
111
  type="button"
111
112
  data-testid="field-remap-add-split"
113
+ icon="codicon-split-horizontal"
114
+ label={chromeLabels.addSplit}
112
115
  onClick={onAddSplit}
113
- >
114
- Add split
115
- </Button>
116
+ />
116
117
  ) : null}
117
118
  </div>
118
119
  </div>
@@ -2,7 +2,7 @@ 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
7
  import { FieldRemapPanel } from './panel.js';
8
8
  import {
@@ -15,6 +15,12 @@ export interface SampleFieldRemapDemoProps {
15
15
  readonly sampleId?: FieldRemapSampleId | string | undefined;
16
16
  /** Forwarded to Flow mapper (default true). */
17
17
  readonly showMinimap?: boolean | undefined;
18
+ /** Forwarded Flow chrome preset. */
19
+ readonly chrome?: FieldRemapFlowMapperProps['chrome'];
20
+ readonly showFlowHint?: FieldRemapFlowMapperProps['showFlowHint'];
21
+ readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
22
+ readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
23
+ readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
18
24
  /** When true, show host-chrome demo controls (minimap toggle + fit view). */
19
25
  readonly showHostChromeDemo?: boolean | undefined;
20
26
  /** Prefer `browse` for I/O class/field inspection demos. */
@@ -31,6 +37,11 @@ export interface SampleFieldRemapDemoProps {
31
37
  export function SampleFieldRemapDemo({
32
38
  sampleId = 'nested-ab',
33
39
  showMinimap: showMinimapProp,
40
+ chrome,
41
+ showFlowHint,
42
+ showBindingsList,
43
+ showConvertPalette,
44
+ emptyDetail,
34
45
  showHostChromeDemo = false,
35
46
  ioChrome,
36
47
  browseSeedShapes = false,
@@ -76,6 +87,11 @@ export function SampleFieldRemapDemo({
76
87
  key={`${sample.id}:${ioChrome ?? 'default'}:${browseSeedShapes ? 'seed' : 'plain'}`}
77
88
  sample={sample}
78
89
  showMinimap={showMinimap}
90
+ chrome={chrome}
91
+ showFlowHint={showFlowHint}
92
+ showBindingsList={showBindingsList}
93
+ showConvertPalette={showConvertPalette}
94
+ emptyDetail={emptyDetail}
79
95
  onShowMinimapChange={setShowMinimap}
80
96
  ioChrome={ioChrome}
81
97
  editableShapes={ioChrome === 'browse' ? false : undefined}
@@ -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>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>
120
+ <h4>{emptyDetailTitle}</h4>
121
+ <p data-testid="field-remap-detail-empty-hint">{emptyDetailDescription}</p>
121
122
  </aside>
122
123
  );
123
124
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Children,
2
3
  useCallback,
3
4
  useEffect,
4
5
  useImperativeHandle,
@@ -9,6 +10,7 @@ import {
9
10
  type KeyboardEvent,
10
11
  type MouseEvent,
11
12
  type Ref,
13
+ type ReactNode,
12
14
  } from 'react';
13
15
  import {
14
16
  Background,
@@ -28,7 +30,8 @@ import {
28
30
  type NodeProps,
29
31
  } from '@xyflow/react';
30
32
  import '@xyflow/react/dist/style.css';
31
- import { Badge, Button } from '@workbench-kit/react/primitives';
33
+ import { Badge, IconButton } from '@workbench-kit/react/primitives';
34
+ import { SplitView } from '@workbench-kit/react/workbench/split-view';
32
35
  import {
33
36
  MAX_TRANSFORM_CHAIN,
34
37
  type MappingEdge,
@@ -323,6 +326,22 @@ export interface FieldRemapFlowMapperProps {
323
326
  readonly edges: readonly MappingEdge[];
324
327
  readonly transforms: ValueTransformRegistry;
325
328
  readonly onEdgesChange: (edges: readonly MappingEdge[]) => void;
329
+ /**
330
+ * `card` preserves the demo chrome. `embed` omits the flow hint and binding list;
331
+ * explicit `show*` props below take precedence.
332
+ */
333
+ readonly chrome?: 'card' | 'embed' | undefined;
334
+ /** Show the empty-selection detail hint, or collapse that rail until a selection exists. */
335
+ readonly emptyDetail?: 'hint' | 'collapse' | undefined;
336
+ /** Show the convert-first hint. Defaults to true for `card`, false for `embed`. */
337
+ readonly showFlowHint?: boolean | undefined;
338
+ /** Show the bottom binding list. Defaults to true for `card`, false for `embed`. */
339
+ readonly showBindingsList?: boolean | undefined;
340
+ /**
341
+ * Mount the primary Convert palette. Defaults to true for both chrome modes; when false,
342
+ * the workspace expands without leaving an empty grid track.
343
+ */
344
+ readonly showConvertPalette?: boolean | undefined;
326
345
  /** Document v2 n→m operators (display + authoring when onOperatorsChange is set). */
327
346
  readonly operators?: readonly MappingOperator[] | undefined;
328
347
  readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
@@ -380,12 +399,87 @@ export interface FieldRemapFlowMapperProps {
380
399
  readonly t?: FieldRemapTranslate | undefined;
381
400
  }
382
401
 
402
+ interface FieldRemapSplitWorkspaceProps {
403
+ readonly children: ReactNode;
404
+ readonly layout: 'wide' | 'medium' | 'narrow';
405
+ readonly showConvertPalette: boolean;
406
+ readonly showDetail: boolean;
407
+ readonly surface: 'binding' | 'convert-note' | 'draft-convert' | 'operator';
408
+ }
409
+
410
+ /**
411
+ * The Flow rails are deliberately composed from the shared SplitView primitive so keyboard and
412
+ * pointer resizing stay consistent with the rest of the workbench. At narrower host widths the
413
+ * palette and/or detail move above or below the canvas rather than squeezing its usable width.
414
+ */
415
+ function FieldRemapSplitWorkspace({
416
+ children,
417
+ layout,
418
+ showConvertPalette,
419
+ showDetail,
420
+ surface,
421
+ }: FieldRemapSplitWorkspaceProps): JSX.Element {
422
+ const [palette, canvas, detail] = Children.toArray(children);
423
+ const isNarrow = layout === 'narrow';
424
+ const canvasWithDetail = showDetail ? (
425
+ <SplitView
426
+ className="workbench-field-remap-flow__canvas-detail-split"
427
+ defaultSecondarySizePx={isNarrow ? 220 : 320}
428
+ layoutMode="secondary-fixed"
429
+ maxSecondarySizePx={isNarrow ? 320 : 480}
430
+ minPrimarySizePx={isNarrow ? 200 : 280}
431
+ minSecondarySizePx={isNarrow ? 160 : 256}
432
+ orientation={isNarrow ? 'vertical' : 'horizontal'}
433
+ primary={canvas}
434
+ secondary={detail}
435
+ />
436
+ ) : (
437
+ canvas
438
+ );
439
+
440
+ const content = showConvertPalette ? (
441
+ <SplitView
442
+ className="workbench-field-remap-flow__palette-split"
443
+ defaultPrimarySizePx={isNarrow ? 192 : 240}
444
+ maxPrimarySizePx={isNarrow ? 288 : 320}
445
+ minPrimarySizePx={isNarrow ? 160 : 192}
446
+ minSecondarySizePx={isNarrow ? 200 : 280}
447
+ orientation={layout === 'wide' ? 'horizontal' : 'vertical'}
448
+ primary={palette}
449
+ primarySizeUnit="pixels"
450
+ secondary={canvasWithDetail}
451
+ />
452
+ ) : (
453
+ canvasWithDetail
454
+ );
455
+
456
+ return (
457
+ <div
458
+ className={
459
+ showConvertPalette
460
+ ? 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--split'
461
+ : 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--split workbench-field-remap-flow__workspace--without-palette'
462
+ }
463
+ data-layout={layout}
464
+ data-surface={surface}
465
+ data-testid="field-remap-workspace"
466
+ >
467
+ {content}
468
+ </div>
469
+ );
470
+ }
471
+
383
472
  function FieldRemapFlowCanvas({
384
473
  sources,
385
474
  targets,
386
475
  edges,
387
476
  transforms,
388
477
  onEdgesChange,
478
+ chrome = 'card',
479
+ emptyDetail: emptyDetailProp,
480
+ showFlowHint: showFlowHintProp,
481
+ showBindingsList: showBindingsListProp,
482
+ showConvertPalette = true,
389
483
  operators = [],
390
484
  onOperatorsChange,
391
485
  sourceTitle,
@@ -407,6 +501,11 @@ function FieldRemapFlowCanvas({
407
501
  () => resolveFieldRemapChromeLabels(labelOverrides, t),
408
502
  [labelOverrides, t],
409
503
  );
504
+ const showFlowHint = showFlowHintProp ?? chrome !== 'embed';
505
+ const showBindingsList = showBindingsListProp ?? chrome !== 'embed';
506
+ const emptyDetail = emptyDetailProp ?? (chrome === 'embed' ? 'collapse' : 'hint');
507
+ const mapperRef = useRef<HTMLDivElement>(null);
508
+ const [workspaceLayout, setWorkspaceLayout] = useState<'wide' | 'medium' | 'narrow'>('wide');
410
509
  const [internalSelection, setInternalSelection] = useState<FieldRemapSelection>(null);
411
510
  const selection = selectionProp !== undefined ? selectionProp : internalSelection;
412
511
  const setSelection = onSelectionChangeProp ?? setInternalSelection;
@@ -417,6 +516,29 @@ function FieldRemapFlowCanvas({
417
516
  });
418
517
  const transformRegistrySignature = createTransformRegistrySignature(transforms);
419
518
 
519
+ useEffect(() => {
520
+ const element = mapperRef.current;
521
+ if (!element || typeof ResizeObserver === 'undefined') {
522
+ return;
523
+ }
524
+
525
+ const updateLayout = () => {
526
+ const remSize = Number.parseFloat(getComputedStyle(element).fontSize) || 16;
527
+ const width = element.getBoundingClientRect().width;
528
+ if (width <= 0) {
529
+ return;
530
+ }
531
+ const nextLayout =
532
+ width <= 60 * remSize ? 'narrow' : width <= 68.75 * remSize ? 'medium' : 'wide';
533
+ setWorkspaceLayout((current) => (current === nextLayout ? current : nextLayout));
534
+ };
535
+
536
+ updateLayout();
537
+ const observer = new ResizeObserver(updateLayout);
538
+ observer.observe(element);
539
+ return () => observer.disconnect();
540
+ }, []);
541
+
420
542
  const graph = useMemo(
421
543
  () =>
422
544
  mappingToFlowGraph({
@@ -746,26 +868,32 @@ function FieldRemapFlowCanvas({
746
868
 
747
869
  return (
748
870
  <div
871
+ ref={mapperRef}
749
872
  className="workbench-field-remap-flow"
750
873
  data-testid="field-remap-mapper"
874
+ data-chrome={chrome}
875
+ data-flow-hint={showFlowHint ? 'on' : 'off'}
876
+ data-bindings-list={showBindingsList ? 'on' : 'off'}
877
+ data-convert-palette={showConvertPalette ? 'on' : 'off'}
878
+ data-empty-detail={emptyDetail}
751
879
  data-minimap={showMinimap ? 'on' : 'off'}
752
880
  data-hidden-fields={includeHidden ? 'on' : 'off'}
753
881
  onKeyDown={onKeyDown}
754
882
  >
755
- <p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
756
- Convert-first: pick a convert in the palette, place it, then wire source → draft → target.
757
- Select a convert note for the Convert editor; select a binding for lighter mapping detail.
758
- Use n→m actions to author combine/split. Alt-click removes a convert step or operator.
759
- Escape clears selection and unfinished drafts.
760
- </p>
883
+ {showFlowHint ? (
884
+ <p className="workbench-field-remap-mapper__hint" data-testid="field-remap-hint">
885
+ Convert-first: pick a convert in the palette, place it, then wire source draft → target.
886
+ Select a convert note for the Convert editor; select a binding for lighter mapping detail.
887
+ Use n→m actions to author combine/split. Alt-click removes a convert step or operator.
888
+ Escape clears selection and unfinished drafts.
889
+ </p>
890
+ ) : null}
761
891
 
762
- <div
763
- className={
764
- selection?.kind === 'transformStep'
765
- ? 'workbench-field-remap-flow__workspace workbench-field-remap-flow__workspace--convert'
766
- : 'workbench-field-remap-flow__workspace'
767
- }
768
- data-surface={
892
+ <FieldRemapSplitWorkspace
893
+ layout={workspaceLayout}
894
+ showConvertPalette={showConvertPalette}
895
+ showDetail={emptyDetail === 'hint' || selection !== null}
896
+ surface={
769
897
  selection?.kind === 'transformStep'
770
898
  ? 'convert-note'
771
899
  : selection?.kind === 'draft'
@@ -775,31 +903,35 @@ function FieldRemapFlowCanvas({
775
903
  : 'binding'
776
904
  }
777
905
  >
778
- <FieldRemapConvertPalette
779
- transforms={transforms}
780
- selectedTransformId={placeTransformId}
781
- onSelectedTransformIdChange={setPlaceTransformId}
782
- onPlaceDraft={placeDraft}
783
- chromeLabels={chromeLabels}
784
- onAddCombine={
785
- onOperatorsChange
786
- ? () => {
787
- const next = createCombineOperator();
788
- onOperatorsChange([...operators, next]);
789
- setSelection({ kind: 'operator', operatorId: next.id });
790
- }
791
- : undefined
792
- }
793
- onAddSplit={
794
- onOperatorsChange
795
- ? () => {
796
- const next = createSplitOperator();
797
- onOperatorsChange([...operators, next]);
798
- setSelection({ kind: 'operator', operatorId: next.id });
799
- }
800
- : undefined
801
- }
802
- />
906
+ <>
907
+ {showConvertPalette ? (
908
+ <FieldRemapConvertPalette
909
+ transforms={transforms}
910
+ selectedTransformId={placeTransformId}
911
+ onSelectedTransformIdChange={setPlaceTransformId}
912
+ onPlaceDraft={placeDraft}
913
+ chromeLabels={chromeLabels}
914
+ onAddCombine={
915
+ onOperatorsChange
916
+ ? () => {
917
+ const next = createCombineOperator();
918
+ onOperatorsChange([...operators, next]);
919
+ setSelection({ kind: 'operator', operatorId: next.id });
920
+ }
921
+ : undefined
922
+ }
923
+ onAddSplit={
924
+ onOperatorsChange
925
+ ? () => {
926
+ const next = createSplitOperator();
927
+ onOperatorsChange([...operators, next]);
928
+ setSelection({ kind: 'operator', operatorId: next.id });
929
+ }
930
+ : undefined
931
+ }
932
+ />
933
+ ) : null}
934
+ </>
803
935
 
804
936
  <div className="workbench-field-remap-flow__canvas" data-testid="field-remap-flow">
805
937
  <ReactFlow
@@ -930,130 +1062,140 @@ function FieldRemapFlowCanvas({
930
1062
  </ReactFlow>
931
1063
  </div>
932
1064
 
933
- <FieldRemapDetailPanel
934
- selection={selection}
935
- edges={edges}
936
- sources={sources}
937
- targets={targets}
938
- transforms={transforms}
939
- onEdgesChange={onEdgesChange}
940
- onSelectionChange={setSelection}
941
- drafts={drafts}
942
- onDiscardDraft={(localId) => {
943
- setDrafts((current) => current.filter((item) => item.localId !== localId));
944
- }}
945
- operators={operators}
946
- onOperatorsChange={onOperatorsChange}
947
- />
948
- </div>
1065
+ <>
1066
+ {emptyDetail === 'hint' || selection !== null ? (
1067
+ <FieldRemapDetailPanel
1068
+ selection={selection}
1069
+ edges={edges}
1070
+ sources={sources}
1071
+ targets={targets}
1072
+ transforms={transforms}
1073
+ onEdgesChange={onEdgesChange}
1074
+ onSelectionChange={setSelection}
1075
+ drafts={drafts}
1076
+ onDiscardDraft={(localId) => {
1077
+ setDrafts((current) => current.filter((item) => item.localId !== localId));
1078
+ }}
1079
+ operators={operators}
1080
+ onOperatorsChange={onOperatorsChange}
1081
+ emptyDetailTitle={chromeLabels.emptyDetailTitle}
1082
+ emptyDetailDescription={chromeLabels.emptyDetailDescription}
1083
+ />
1084
+ ) : null}
1085
+ </>
1086
+ </FieldRemapSplitWorkspace>
949
1087
 
950
- <div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
951
- <h4>{chromeLabels.bindingsTitle}</h4>
952
- <ul>
953
- {edges.map((edge) => {
954
- const portTypes = edgePortTypes(edge, sources, targets);
955
- const appendCatalog = listCompatibleTransforms({
956
- registry: transforms,
957
- edge,
958
- stepIndex: edge.transformIds?.length ?? 0,
959
- sourceType: portTypes.sourceType,
960
- targetType: portTypes.targetType,
961
- mode: 'append',
962
- });
963
- const defaultAddId = appendCatalog[0]?.id;
964
- const listContext = canEditListContext(edge, sources, targets);
965
- const selected =
966
- (selection?.kind === 'edge' || selection?.kind === 'transformStep') &&
967
- selection.edgeId === edge.id;
1088
+ {showBindingsList ? (
1089
+ <div className="workbench-field-remap-flow__bindings" data-testid="field-remap-edges">
1090
+ <h4>{chromeLabels.bindingsTitle}</h4>
1091
+ <ul>
1092
+ {edges.map((edge) => {
1093
+ const portTypes = edgePortTypes(edge, sources, targets);
1094
+ const appendCatalog = listCompatibleTransforms({
1095
+ registry: transforms,
1096
+ edge,
1097
+ stepIndex: edge.transformIds?.length ?? 0,
1098
+ sourceType: portTypes.sourceType,
1099
+ targetType: portTypes.targetType,
1100
+ mode: 'append',
1101
+ });
1102
+ const defaultAddId = appendCatalog[0]?.id;
1103
+ const listContext = canEditListContext(edge, sources, targets);
1104
+ const selected =
1105
+ (selection?.kind === 'edge' || selection?.kind === 'transformStep') &&
1106
+ selection.edgeId === edge.id;
968
1107
 
969
- return (
970
- <li
971
- key={edge.id}
972
- className={selected ? 'is-selected' : undefined}
973
- data-testid={`field-remap-lane-${edge.id}`}
974
- >
975
- <button
976
- type="button"
977
- className="workbench-field-remap-flow__binding-select"
978
- data-testid={`field-remap-select-edge-${edge.id}`}
979
- onClick={() => setSelection({ kind: 'edge', edgeId: edge.id })}
1108
+ return (
1109
+ <li
1110
+ key={edge.id}
1111
+ className={selected ? 'is-selected' : undefined}
1112
+ data-testid={`field-remap-lane-${edge.id}`}
980
1113
  >
981
- <code>
982
- {edge.sourceFieldId} →{' '}
983
- {(edge.transformIds ?? []).length > 0
984
- ? `${(edge.transformIds ?? []).join(' → ')}`
985
- : ''}
986
- {edge.targetSlotId}
987
- {edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
988
- </code>
989
- </button>
990
- <span className="workbench-field-remap-mapper__edge-actions">
991
- {(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
992
- <Button
993
- compact
994
- type="button"
995
- data-testid={`field-remap-add-node-${edge.id}`}
996
- onClick={() => {
997
- const next = addTransformStepToEdge(edge, defaultAddId, {
998
- registry: transforms,
999
- sourceType: portTypes.sourceType,
1000
- targetType: portTypes.targetType,
1001
- });
1002
- if (!next) {
1003
- return;
1004
- }
1005
- onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
1006
- setSelection({
1007
- kind: 'transformStep',
1008
- edgeId: edge.id,
1009
- stepIndex: (next.transformIds?.length ?? 1) - 1,
1010
- });
1011
- }}
1012
- >
1013
- + node
1014
- </Button>
1015
- ) : null}
1016
- {listContext ? (
1017
- <Button
1114
+ <button
1115
+ type="button"
1116
+ className="workbench-field-remap-flow__binding-select"
1117
+ data-testid={`field-remap-select-edge-${edge.id}`}
1118
+ onClick={() => setSelection({ kind: 'edge', edgeId: edge.id })}
1119
+ >
1120
+ <code>
1121
+ {edge.sourceFieldId} →{' '}
1122
+ {(edge.transformIds ?? []).length > 0
1123
+ ? `${(edge.transformIds ?? []).join(' → ')} → `
1124
+ : ''}
1125
+ {edge.targetSlotId}
1126
+ {edge.itemEdges ? ` · ${edge.itemEdges.length} item fields` : ''}
1127
+ </code>
1128
+ </button>
1129
+ <span className="workbench-field-remap-mapper__edge-actions">
1130
+ {(edge.transformIds?.length ?? 0) < MAX_TRANSFORM_CHAIN && defaultAddId ? (
1131
+ <IconButton
1132
+ compact
1133
+ type="button"
1134
+ data-testid={`field-remap-add-node-${edge.id}`}
1135
+ icon="codicon-add"
1136
+ label={chromeLabels.addTransform}
1137
+ onClick={() => {
1138
+ const next = addTransformStepToEdge(edge, defaultAddId, {
1139
+ registry: transforms,
1140
+ sourceType: portTypes.sourceType,
1141
+ targetType: portTypes.targetType,
1142
+ });
1143
+ if (!next) {
1144
+ return;
1145
+ }
1146
+ onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
1147
+ setSelection({
1148
+ kind: 'transformStep',
1149
+ edgeId: edge.id,
1150
+ stepIndex: (next.transformIds?.length ?? 1) - 1,
1151
+ });
1152
+ }}
1153
+ />
1154
+ ) : null}
1155
+ {listContext ? (
1156
+ <IconButton
1157
+ compact
1158
+ type="button"
1159
+ data-testid={`field-remap-edit-items-${edge.id}`}
1160
+ icon="codicon-edit"
1161
+ label={chromeLabels.editItems}
1162
+ onClick={() => {
1163
+ if (!edge.itemEdges) {
1164
+ onEdgesChange(
1165
+ edges.map((item) =>
1166
+ item.id === edge.id ? enableListContextOnEdge(item) : item,
1167
+ ),
1168
+ );
1169
+ }
1170
+ setSelection({ kind: 'edge', edgeId: edge.id });
1171
+ }}
1172
+ />
1173
+ ) : null}
1174
+ <IconButton
1018
1175
  compact
1019
1176
  type="button"
1020
- data-testid={`field-remap-edit-items-${edge.id}`}
1177
+ data-testid={`field-remap-remove-edge-${edge.id}`}
1178
+ icon="codicon-trash"
1179
+ label={chromeLabels.removeBinding}
1180
+ variant="danger"
1021
1181
  onClick={() => {
1022
- if (!edge.itemEdges) {
1023
- onEdgesChange(
1024
- edges.map((item) =>
1025
- item.id === edge.id ? enableListContextOnEdge(item) : item,
1026
- ),
1027
- );
1182
+ onEdgesChange(edges.filter((item) => item.id !== edge.id));
1183
+ if (
1184
+ selection &&
1185
+ (selection.kind === 'edge' || selection.kind === 'transformStep') &&
1186
+ selection.edgeId === edge.id
1187
+ ) {
1188
+ setSelection(null);
1028
1189
  }
1029
- setSelection({ kind: 'edge', edgeId: edge.id });
1030
1190
  }}
1031
- >
1032
- Edit items
1033
- </Button>
1034
- ) : null}
1035
- <Button
1036
- compact
1037
- type="button"
1038
- onClick={() => {
1039
- onEdgesChange(edges.filter((item) => item.id !== edge.id));
1040
- if (
1041
- selection &&
1042
- (selection.kind === 'edge' || selection.kind === 'transformStep') &&
1043
- selection.edgeId === edge.id
1044
- ) {
1045
- setSelection(null);
1046
- }
1047
- }}
1048
- >
1049
- Remove
1050
- </Button>
1051
- </span>
1052
- </li>
1053
- );
1054
- })}
1055
- </ul>
1056
- </div>
1191
+ />
1192
+ </span>
1193
+ </li>
1194
+ );
1195
+ })}
1196
+ </ul>
1197
+ </div>
1198
+ ) : null}
1057
1199
  </div>
1058
1200
  );
1059
1201
  }
@@ -83,6 +83,13 @@ export interface FieldRemapPanelProps {
83
83
  readonly targetShape?: unknown;
84
84
  /** Forwarded to {@link FieldRemapFlowMapper} (default true). */
85
85
  readonly showMinimap?: boolean | undefined;
86
+ /** Forwarded Flow chrome preset. `embed` hides the hint and binding list by default. */
87
+ readonly chrome?: FieldRemapFlowMapperProps['chrome'];
88
+ /** Explicit Flow chrome overrides take precedence over {@link FieldRemapPanelProps.chrome}. */
89
+ readonly showFlowHint?: FieldRemapFlowMapperProps['showFlowHint'];
90
+ readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
91
+ readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
92
+ readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
86
93
  /** Forwarded to {@link FieldRemapFlowMapper} Controls MiniMap toggle. */
87
94
  readonly onShowMinimapChange?: FieldRemapFlowMapperProps['onShowMinimapChange'];
88
95
  readonly onPaneContextMenu?: FieldRemapFlowMapperProps['onPaneContextMenu'];
@@ -138,6 +145,11 @@ export function FieldRemapPanel({
138
145
  sourceSample: sourceSampleProp,
139
146
  targetShape: targetShapeProp,
140
147
  showMinimap,
148
+ chrome,
149
+ showFlowHint,
150
+ showBindingsList,
151
+ showConvertPalette,
152
+ emptyDetail,
141
153
  onShowMinimapChange,
142
154
  onPaneContextMenu,
143
155
  onNodeContextMenu,
@@ -420,6 +432,11 @@ export function FieldRemapPanel({
420
432
  sourceTitle={sample.sourceLabel}
421
433
  targetTitle={sample.targetLabel}
422
434
  showMinimap={showMinimap}
435
+ chrome={chrome}
436
+ showFlowHint={showFlowHint}
437
+ showBindingsList={showBindingsList}
438
+ showConvertPalette={showConvertPalette}
439
+ emptyDetail={emptyDetail}
423
440
  onShowMinimapChange={onShowMinimapChange}
424
441
  includeHidden={includeHidden}
425
442
  onIncludeHiddenChange={setIncludeHidden}
@@ -301,6 +301,8 @@
301
301
  display: flex;
302
302
  flex-direction: column;
303
303
  gap: 0.75rem;
304
+ container-name: field-remap-flow;
305
+ container-type: inline-size;
304
306
  }
305
307
 
306
308
  .workbench-field-remap-flow__workspace {
@@ -311,7 +313,74 @@
311
313
  align-items: stretch;
312
314
  }
313
315
 
314
- @media (max-width: 1100px) {
316
+ .workbench-field-remap-flow__workspace--without-palette {
317
+ grid-template-columns: minmax(0, 1fr) minmax(16rem, 20rem);
318
+ }
319
+
320
+ .workbench-field-remap-flow__workspace--split {
321
+ display: flex;
322
+ height: min(40rem, 70vh);
323
+ min-height: 26rem;
324
+ gap: 0;
325
+ }
326
+
327
+ .workbench-field-remap-flow__workspace--split > .ui-workbench-split-view,
328
+ .workbench-field-remap-flow__workspace--split
329
+ .ui-workbench-split-view__secondary
330
+ > .ui-workbench-split-view {
331
+ width: 100%;
332
+ height: 100%;
333
+ }
334
+
335
+ .workbench-field-remap-flow__workspace--split .workbench-field-remap-flow__canvas {
336
+ height: auto;
337
+ min-height: 0;
338
+ }
339
+
340
+ /* Embed hosts own the outer pane. Keep Flow rails flat and separate them with dividers only. */
341
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-flow__workspace {
342
+ gap: 0;
343
+ }
344
+
345
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-palette,
346
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-flow__canvas,
347
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-detail,
348
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-note {
349
+ border-radius: 0;
350
+ }
351
+
352
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-palette {
353
+ border: 0;
354
+ border-inline-end: 1px solid var(--vscode-panel-border, var(--color-border));
355
+ }
356
+
357
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-flow__canvas {
358
+ border: 0;
359
+ }
360
+
361
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-detail,
362
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-note {
363
+ border: 0;
364
+ border-inline-start: 1px solid var(--vscode-panel-border, var(--color-border));
365
+ }
366
+
367
+ /* SplitView supplies the shared rail divider in embed mode, avoiding a doubled border. */
368
+ .workbench-field-remap-flow[data-chrome='embed']
369
+ .workbench-field-remap-flow__workspace--split
370
+ .workbench-field-remap-convert-palette,
371
+ .workbench-field-remap-flow[data-chrome='embed']
372
+ .workbench-field-remap-flow__workspace--split
373
+ .workbench-field-remap-flow__canvas,
374
+ .workbench-field-remap-flow[data-chrome='embed']
375
+ .workbench-field-remap-flow__workspace--split
376
+ .workbench-field-remap-detail,
377
+ .workbench-field-remap-flow[data-chrome='embed']
378
+ .workbench-field-remap-flow__workspace--split
379
+ .workbench-field-remap-convert-note {
380
+ border: 0;
381
+ }
382
+
383
+ @container field-remap-flow (max-width: 68.75rem) {
315
384
  .workbench-field-remap-flow__workspace {
316
385
  grid-template-columns: minmax(0, 1fr) minmax(16rem, 20rem);
317
386
  }
@@ -319,12 +388,23 @@
319
388
  .workbench-field-remap-convert-palette {
320
389
  grid-column: 1 / -1;
321
390
  }
391
+
392
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-palette {
393
+ border-inline-end: 0;
394
+ border-block-end: 1px solid var(--vscode-panel-border, var(--color-border));
395
+ }
322
396
  }
323
397
 
324
- @media (max-width: 960px) {
398
+ @container field-remap-flow (max-width: 60rem) {
325
399
  .workbench-field-remap-flow__workspace {
326
400
  grid-template-columns: 1fr;
327
401
  }
402
+
403
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-detail,
404
+ .workbench-field-remap-flow[data-chrome='embed'] .workbench-field-remap-convert-note {
405
+ border-inline-start: 0;
406
+ border-block-start: 1px solid var(--vscode-panel-border, var(--color-border));
407
+ }
328
408
  }
329
409
 
330
410
  .workbench-field-remap-convert-palette {