@workbench-kit/shell-react 0.0.2-prototype.0.2.6 → 0.0.2-prototype.0.2.8

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 (95) hide show
  1. package/README.md +41 -0
  2. package/package.json +9 -9
  3. package/src/{chat-ai-mock-proposals.ts → chat/ai-mock-proposals.ts} +39 -39
  4. package/src/{chat-command-input.ts → chat/command-input.ts} +52 -52
  5. package/src/{chat-command-policy.ts → chat/command-policy.ts} +54 -54
  6. package/src/{chat-command-surface.tsx → chat/command-surface.tsx} +155 -155
  7. package/src/{use-workbench-chat-command-proposals.ts → chat/use-chat-command-proposals.ts} +194 -194
  8. package/src/{chat-view-data.ts → chat/view-data.ts} +20 -20
  9. package/src/{chat-view.tsx → chat/view.tsx} +220 -220
  10. package/src/{command-inspector-surface.tsx → commands/inspector-surface.tsx} +46 -46
  11. package/src/{use-workbench-command-descriptors.ts → commands/use-command-descriptors.ts} +41 -41
  12. package/src/{use-context-key-revision.ts → commands/use-context-key-revision.ts} +18 -18
  13. package/src/{commands-view-data.ts → commands/view-data.ts} +17 -17
  14. package/src/{commands-view.tsx → commands/view.tsx} +43 -43
  15. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -124
  16. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -17
  17. package/src/devtools/index.ts +11 -11
  18. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -84
  19. package/src/devtools/workbench-devtools-snapshot.ts +140 -140
  20. package/src/devtools/workbench-devtools.css +82 -82
  21. package/src/{editor-area-dnd.ts → editor/area-dnd.ts} +69 -69
  22. package/src/{editor-area-model.ts → editor/area-model.ts} +71 -71
  23. package/src/{editor-area.css → editor/area.css} +454 -454
  24. package/src/{editor-area.tsx → editor/area.tsx} +676 -676
  25. package/src/{editor-host-surface.tsx → editor/host-surface.tsx} +631 -634
  26. package/src/{editor-pane-visibility.ts → editor/pane-visibility.ts} +130 -130
  27. package/src/{editor-resource.ts → editor/resource.ts} +36 -36
  28. package/src/{editor-state-storage.ts → editor/state-storage.ts} +182 -193
  29. package/src/{editor-tab-context-menu.ts → editor/tab-context-menu.ts} +118 -118
  30. package/src/{use-editor.ts → editor/use-editor.ts} +114 -107
  31. package/src/{editor-view-providers.tsx → editor/view-providers.tsx} +552 -552
  32. package/src/{editor-workspace-file-availability.ts → editor/workspace-file-availability.ts} +15 -15
  33. package/src/{editor-workspace-reconcile.tsx → editor/workspace-reconcile.tsx} +31 -28
  34. package/src/{explorer-context-menu.ts → explorer/context-menu.ts} +114 -114
  35. package/src/{createCommandWorkspaceExplorerPort.ts → explorer/create-command-workspace-explorer-port.ts} +86 -86
  36. package/src/{explorer-reveal.ts → explorer/reveal.ts} +112 -112
  37. package/src/{search-view-data.ts → explorer/search-view-data.ts} +15 -15
  38. package/src/{search-view.tsx → explorer/search-view.tsx} +65 -62
  39. package/src/{use-active-workspace-path.ts → explorer/use-active-workspace-path.ts} +15 -15
  40. package/src/{explorer-view-data.ts → explorer/view-data.ts} +58 -58
  41. package/src/{explorer-view.tsx → explorer/view.tsx} +227 -224
  42. package/src/{extension-context-menu.ts → extensions/context-menu.ts} +52 -52
  43. package/src/{extension-management-model.ts → extensions/management-model.ts} +313 -313
  44. package/src/{use-extension-management.ts → extensions/use-extension-management.ts} +241 -193
  45. package/src/{extensions-view-data.ts → extensions/view-data.ts} +19 -19
  46. package/src/{extensions-view.tsx → extensions/view.tsx} +63 -38
  47. package/src/{field-remap-demo.tsx → field-remap/demo.tsx} +18 -18
  48. package/src/{field-remap-editor-surface.tsx → field-remap/editor-surface.tsx} +36 -36
  49. package/src/{field-remap-flow-adapter.ts → field-remap/flow-adapter.ts} +240 -240
  50. package/src/{field-remap-flow.tsx → field-remap/flow.tsx} +369 -369
  51. package/src/{field-remap-graph.tsx → field-remap/graph.tsx} +559 -559
  52. package/src/field-remap/jsonata-transform.ts +166 -0
  53. package/src/{field-remap-panel.tsx → field-remap/panel.tsx} +193 -175
  54. package/src/{field-remap-samples.ts → field-remap/samples.ts} +352 -352
  55. package/src/{table-mapping-adapter.ts → field-remap/table-mapping-adapter.ts} +59 -59
  56. package/src/{field-remap-tree.tsx → field-remap/tree.tsx} +404 -404
  57. package/src/{field-remap-view-data.ts → field-remap/view-data.ts} +16 -16
  58. package/src/{field-remap-view.css → field-remap/view.css} +676 -676
  59. package/src/{field-remap-view.tsx → field-remap/view.tsx} +86 -86
  60. package/src/index.ts +212 -199
  61. package/src/{json-form-source-patch.ts → jdw/json-form-source-patch.ts} +323 -323
  62. package/src/{jdw-lab-view-data.ts → jdw/lab-view-data.ts} +25 -25
  63. package/src/{jdw-lab-view.css → jdw/lab-view.css} +21 -21
  64. package/src/{jdw-lab-view.tsx → jdw/lab-view.tsx} +79 -79
  65. package/src/{jdw-widget-form-view.tsx → jdw/widget-form-view.tsx} +68 -65
  66. package/src/{jdw-widget-preview-view.tsx → jdw/widget-preview-view.tsx} +54 -51
  67. package/src/{keybinding-overrides-storage.ts → management/keybinding-overrides-storage.ts} +49 -57
  68. package/src/{keybinding-management-settings.tsx → management/keybinding-settings.tsx} +16 -16
  69. package/src/{management-palette-commands.ts → management/palette-commands.ts} +38 -38
  70. package/src/{preference-settings-storage.ts → management/preference-settings-storage.ts} +50 -58
  71. package/src/{management-settings-ids.ts → management/settings-ids.ts} +12 -12
  72. package/src/{management-settings.tsx → management/settings.tsx} +96 -75
  73. package/src/{use-command-management.ts → management/use-command-management.ts} +191 -191
  74. package/src/{use-keybinding-management.ts → management/use-keybinding-management.ts} +130 -130
  75. package/src/{shell-model.tsx → shell/model.tsx} +149 -149
  76. package/src/{provider.tsx → shell/provider.tsx} +603 -573
  77. package/src/{shell-secondary-actions.tsx → shell/secondary-actions.tsx} +59 -59
  78. package/src/{shell-settings-constants.ts → shell/settings-constants.ts} +9 -9
  79. package/src/{shell-settings.tsx → shell/settings.tsx} +599 -599
  80. package/src/{shell.tsx → shell/shell.tsx} +680 -649
  81. package/src/{shell-titlebar-layout-controls.tsx → shell/titlebar-layout-controls.tsx} +4 -4
  82. package/src/{shell-view-host.tsx → shell/view-host.tsx} +212 -199
  83. package/src/{workbench-appearance-storage.ts → workbench/appearance-storage.ts} +107 -115
  84. package/src/{workbench-command-host.tsx → workbench/command-host.tsx} +243 -243
  85. package/src/{workbench-command-palette.ts → workbench/command-palette.ts} +208 -208
  86. package/src/{workbench-keybinding-bridge.ts → workbench/keybinding-bridge.ts} +54 -54
  87. package/src/{workbench-layout-storage.ts → workbench/layout-storage.ts} +134 -145
  88. package/src/{workbench-profile-modal.tsx → workbench/profile-modal.tsx} +118 -118
  89. package/src/{workbench-shell-command-registration.ts → workbench/shell-command-registration.ts} +58 -58
  90. package/src/{workbench-startup-gate.tsx → workbench/startup-gate.tsx} +136 -136
  91. package/src/{workbench-status-sections.ts → workbench/status-sections.ts} +43 -43
  92. package/src/{use-persisted-workbench-appearance.ts → workbench/use-persisted-appearance.ts} +33 -33
  93. package/src/{workbench-user-commands.ts → workbench/user-commands.ts} +46 -46
  94. package/src/{workspace-view-state.ts → workbench/workspace-view-state.ts} +35 -35
  95. package/src/jsonata-transform.ts +0 -36
@@ -0,0 +1,166 @@
1
+ import jsonata from 'jsonata';
2
+ import type { ValueTransformDefinition } from '@workbench-kit/field-remap';
3
+
4
+ /** Host-registered JSONata expression transform (Stedi-style advanced mapping). */
5
+ export const JSONATA_TRANSFORM_ID = 'expr:jsonata' as const;
6
+
7
+ /** Default wall-clock budget for a single expression evaluation. */
8
+ export const DEFAULT_JSONATA_TIMEOUT_MS = 2_000;
9
+
10
+ /** Default maximum expression source length (characters). */
11
+ export const DEFAULT_JSONATA_MAX_EXPRESSION_LENGTH = 4_096;
12
+
13
+ export type JsonataTransformErrorPolicy = 'passthrough' | 'throw';
14
+
15
+ export interface CreateJsonataValueTransformOptions {
16
+ /** Wall-clock timeout for `evaluate` (default {@link DEFAULT_JSONATA_TIMEOUT_MS}). */
17
+ readonly timeoutMs?: number;
18
+ /**
19
+ * Reject expressions longer than this many characters
20
+ * (default {@link DEFAULT_JSONATA_MAX_EXPRESSION_LENGTH}).
21
+ */
22
+ readonly maxExpressionLength?: number;
23
+ /**
24
+ * - `throw` (default): surface timeout / compile / evaluate failures to the host
25
+ * - `passthrough`: return the original value (legacy silent identity)
26
+ */
27
+ readonly onError?: JsonataTransformErrorPolicy;
28
+ }
29
+
30
+ export function createJsonataValueTransform(
31
+ options: CreateJsonataValueTransformOptions = {},
32
+ ): ValueTransformDefinition {
33
+ const timeoutMs = options.timeoutMs ?? DEFAULT_JSONATA_TIMEOUT_MS;
34
+ const maxExpressionLength = options.maxExpressionLength ?? DEFAULT_JSONATA_MAX_EXPRESSION_LENGTH;
35
+ const onError = options.onError ?? 'throw';
36
+
37
+ return {
38
+ id: JSONATA_TRANSFORM_ID,
39
+ label: 'JSONata expression',
40
+ description:
41
+ 'Evaluate a JSONata expression against the source value (host-registered; bounded).',
42
+ category: 'expression',
43
+ inputTypes: ['string', 'number', 'boolean', 'object', 'array', 'unknown'],
44
+ outputType: 'unknown',
45
+ optionFields: [
46
+ {
47
+ key: 'expression',
48
+ label: 'Expression',
49
+ kind: 'string',
50
+ },
51
+ ],
52
+ apply: async (value, context) => {
53
+ const expression =
54
+ typeof context.options?.expression === 'string' ? context.options.expression.trim() : '';
55
+ if (!expression) {
56
+ return value;
57
+ }
58
+
59
+ if (expression.length > maxExpressionLength) {
60
+ return handleJsonataError(
61
+ new Error(
62
+ `JSONata expression exceeds max length (${expression.length} > ${maxExpressionLength}).`,
63
+ ),
64
+ value,
65
+ onError,
66
+ );
67
+ }
68
+
69
+ try {
70
+ const compiled = jsonata(expression);
71
+ // jsonata@2.x evaluate returns a Promise (1.x was synchronous).
72
+ return await raceJsonataEvaluation(compiled.evaluate(value), {
73
+ timeoutMs,
74
+ signal: context.signal,
75
+ });
76
+ } catch (error) {
77
+ return handleJsonataError(error, value, onError);
78
+ }
79
+ },
80
+ };
81
+ }
82
+
83
+ /** Default bounded transform (`onError: 'throw'`, 2s timeout, 4k expression cap). */
84
+ export const jsonataValueTransform: ValueTransformDefinition = createJsonataValueTransform();
85
+
86
+ export class JsonataTransformTimeoutError extends Error {
87
+ override readonly name = 'JsonataTransformTimeoutError';
88
+
89
+ constructor(timeoutMs: number) {
90
+ super(`JSONata evaluation timed out after ${timeoutMs}ms.`);
91
+ }
92
+ }
93
+
94
+ /** Race a JSONata evaluation against timeout / AbortSignal (exported for unit tests). */
95
+ export async function raceJsonataEvaluation<T>(
96
+ evaluation: PromiseLike<T>,
97
+ bounds: { readonly timeoutMs: number; readonly signal?: AbortSignal },
98
+ ): Promise<T> {
99
+ const { timeoutMs, signal } = bounds;
100
+ if (signal?.aborted) {
101
+ throw signal.reason instanceof Error
102
+ ? signal.reason
103
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' });
104
+ }
105
+
106
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
107
+ return evaluation;
108
+ }
109
+
110
+ return new Promise<T>((resolve, reject) => {
111
+ let settled = false;
112
+ const timer = setTimeout(() => {
113
+ if (settled) return;
114
+ settled = true;
115
+ cleanup();
116
+ reject(new JsonataTransformTimeoutError(timeoutMs));
117
+ }, timeoutMs);
118
+
119
+ const onAbort = () => {
120
+ if (settled) return;
121
+ settled = true;
122
+ cleanup();
123
+ reject(
124
+ signal?.reason instanceof Error
125
+ ? signal.reason
126
+ : Object.assign(new Error('The operation was aborted.'), { name: 'AbortError' }),
127
+ );
128
+ };
129
+
130
+ const cleanup = () => {
131
+ clearTimeout(timer);
132
+ signal?.removeEventListener('abort', onAbort);
133
+ };
134
+
135
+ signal?.addEventListener('abort', onAbort, { once: true });
136
+
137
+ Promise.resolve(evaluation).then(
138
+ (result) => {
139
+ if (settled) return;
140
+ settled = true;
141
+ cleanup();
142
+ resolve(result);
143
+ },
144
+ (error: unknown) => {
145
+ if (settled) return;
146
+ settled = true;
147
+ cleanup();
148
+ reject(error);
149
+ },
150
+ );
151
+ });
152
+ }
153
+
154
+ function handleJsonataError(
155
+ error: unknown,
156
+ value: unknown,
157
+ onError: JsonataTransformErrorPolicy,
158
+ ): unknown {
159
+ if (onError === 'passthrough') {
160
+ return value;
161
+ }
162
+ if (error instanceof Error) {
163
+ throw error;
164
+ }
165
+ throw new Error(String(error));
166
+ }
@@ -1,175 +1,193 @@
1
- import { useMemo, useState, type JSX } from 'react';
2
- import {
3
- convertToShape,
4
- createBuiltinValueTransformRegistry,
5
- defineConversion,
6
- defineDataShape,
7
- findParentChildMappingConflicts,
8
- sourceFieldsFromPlainObject,
9
- targetSlotsFromPlainObject,
10
- withConversionEdges,
11
- type MappingEdge,
12
- type ValueTransformRegistry,
13
- } from '@workbench-kit/field-remap';
14
-
15
- import { FieldRemapFlowMapper } from './field-remap-flow.js';
16
- import {
17
- getFieldRemapSample,
18
- type FieldRemapSampleDefinition,
19
- type FieldRemapSampleId,
20
- } from './field-remap-samples.js';
21
- import { jsonataValueTransform } from './jsonata-transform.js';
22
- import './field-remap-view.css';
23
-
24
- export interface FieldRemapPanelProps {
25
- /** Catalog sample id, or a full sample definition. */
26
- readonly sample?: FieldRemapSampleId | FieldRemapSampleDefinition | undefined;
27
- /** Optional host-owned transform registry (defaults to builtins + JSONata). */
28
- readonly transforms?: ValueTransformRegistry | undefined;
29
- readonly className?: string | undefined;
30
- }
31
-
32
- function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSampleDefinition {
33
- if (!sample) {
34
- return getFieldRemapSample('nested-ab');
35
- }
36
- if (typeof sample === 'string') {
37
- return getFieldRemapSample(sample);
38
- }
39
- return sample;
40
- }
41
-
42
- /**
43
- * Self-contained field-remap workbench panel:
44
- * XYFlow mapper + convertToShape preview for one A→B (or T_A→T_B) sample.
45
- *
46
- * Remount with `key={sampleId}` when switching catalog entries so edge state resets.
47
- */
48
- export function FieldRemapPanel({
49
- sample: sampleProp,
50
- transforms: transformsProp,
51
- className,
52
- }: FieldRemapPanelProps): JSX.Element {
53
- const sample = resolveSample(sampleProp);
54
- const registry = useMemo(() => {
55
- if (transformsProp) {
56
- return transformsProp;
57
- }
58
- const next = createBuiltinValueTransformRegistry();
59
- next.register(jsonataValueTransform);
60
- return next;
61
- }, [transformsProp]);
62
-
63
- const [edges, setEdges] = useState<readonly MappingEdge[]>(() => [...sample.edges]);
64
-
65
- const sourceFields = useMemo(
66
- () => sourceFieldsFromPlainObject(sample.source, { idPrefix: sample.sourceIdPrefix }),
67
- [sample],
68
- );
69
- const targetSlots = useMemo(
70
- () => targetSlotsFromPlainObject(sample.targetShape, { idPrefix: sample.targetIdPrefix }),
71
- [sample],
72
- );
73
-
74
- const shapes = useMemo(
75
- () => [
76
- defineDataShape({
77
- id: sample.sourceIdPrefix,
78
- label: sample.sourceLabel,
79
- role: 'source',
80
- fields: sourceFields,
81
- }),
82
- defineDataShape({
83
- id: sample.targetIdPrefix,
84
- label: sample.targetLabel,
85
- role: 'target',
86
- fields: targetSlots,
87
- }),
88
- ],
89
- [sample, sourceFields, targetSlots],
90
- );
91
-
92
- const conflicts = useMemo(
93
- () => findParentChildMappingConflicts(edges, sourceFields, targetSlots),
94
- [edges, sourceFields, targetSlots],
95
- );
96
-
97
- const result = useMemo(() => {
98
- const conversion = withConversionEdges(
99
- defineConversion({
100
- id: `${sample.sourceIdPrefix}→${sample.targetIdPrefix}`,
101
- sourceShapeIds: [sample.sourceIdPrefix],
102
- targetShapeId: sample.targetIdPrefix,
103
- edges: [...sample.edges],
104
- }),
105
- edges,
106
- );
107
- try {
108
- return convertToShape({
109
- conversion,
110
- shapes,
111
- inputs: { [sample.sourceIdPrefix]: sample.source },
112
- transforms: registry,
113
- });
114
- } catch (error) {
115
- return {
116
- output: {} as Record<string, unknown>,
117
- error: error instanceof Error ? error.message : String(error),
118
- };
119
- }
120
- }, [edges, registry, sample, shapes]);
121
-
122
- return (
123
- <div
124
- className={['workbench-field-remap-demo', className].filter(Boolean).join(' ')}
125
- data-testid="field-remap-demo"
126
- data-sample-id={sample.id}
127
- >
128
- <header className="workbench-field-remap-demo__header">
129
- <h2 className="workbench-field-remap-demo__title">{sample.title}</h2>
130
- <p className="workbench-field-remap-demo__intro">{sample.description}</p>
131
- </header>
132
-
133
- <FieldRemapFlowMapper
134
- sources={sourceFields}
135
- targets={targetSlots}
136
- edges={edges}
137
- transforms={registry}
138
- onEdgesChange={setEdges}
139
- sourceTitle={sample.sourceLabel}
140
- targetTitle={sample.targetLabel}
141
- />
142
-
143
- {conflicts.length > 0 ? (
144
- <p
145
- className="workbench-field-remap-demo__warn"
146
- role="status"
147
- data-testid="field-remap-conflicts"
148
- >
149
- Warning: parent and child fields are both mapped (
150
- {conflicts.map((item) => `${item.parentId} / ${item.childId}`).join('; ')}). Prefer one
151
- level.
152
- </p>
153
- ) : null}
154
-
155
- {'error' in result && result.error ? (
156
- <p className="workbench-field-remap-demo__error" role="alert">
157
- {result.error}
158
- </p>
159
- ) : null}
160
-
161
- <div className="workbench-field-remap-demo__panes">
162
- <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-source">
163
- <h3 id="field-remap-source">{sample.sourceLabel}</h3>
164
- <pre data-testid="field-remap-input">{JSON.stringify(sample.source, null, 2)}</pre>
165
- </section>
166
- <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-target">
167
- <h3 id="field-remap-target">{sample.targetLabel}</h3>
168
- <pre data-testid="field-remap-result">
169
- {JSON.stringify('output' in result ? result.output : {}, null, 2)}
170
- </pre>
171
- </section>
172
- </div>
173
- </div>
174
- );
175
- }
1
+ import { useEffect, useMemo, useState, type JSX } from 'react';
2
+ import {
3
+ convertToShape,
4
+ createBuiltinValueTransformRegistry,
5
+ defineConversion,
6
+ defineDataShape,
7
+ findParentChildMappingConflicts,
8
+ sourceFieldsFromPlainObject,
9
+ targetSlotsFromPlainObject,
10
+ withConversionEdges,
11
+ type MappingEdge,
12
+ type ValueTransformRegistry,
13
+ } from '@workbench-kit/field-remap';
14
+
15
+ import { FieldRemapFlowMapper } from './flow.js';
16
+ import {
17
+ getFieldRemapSample,
18
+ type FieldRemapSampleDefinition,
19
+ type FieldRemapSampleId,
20
+ } from './samples.js';
21
+ import { jsonataValueTransform } from './jsonata-transform.js';
22
+ import './view.css';
23
+
24
+ export interface FieldRemapPanelProps {
25
+ /** Catalog sample id, or a full sample definition. */
26
+ readonly sample?: FieldRemapSampleId | FieldRemapSampleDefinition | undefined;
27
+ /** Optional host-owned transform registry (defaults to builtins + JSONata). */
28
+ readonly transforms?: ValueTransformRegistry | undefined;
29
+ readonly className?: string | undefined;
30
+ }
31
+
32
+ type FieldRemapPreviewResult = {
33
+ readonly output: Record<string, unknown>;
34
+ readonly error?: string;
35
+ };
36
+
37
+ function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSampleDefinition {
38
+ if (!sample) {
39
+ return getFieldRemapSample('nested-ab');
40
+ }
41
+ if (typeof sample === 'string') {
42
+ return getFieldRemapSample(sample);
43
+ }
44
+ return sample;
45
+ }
46
+
47
+ /**
48
+ * Self-contained field-remap workbench panel:
49
+ * XYFlow mapper + convertToShape preview for one A→B (or T_A→T_B) sample.
50
+ *
51
+ * Remount with `key={sampleId}` when switching catalog entries so edge state resets.
52
+ */
53
+ export function FieldRemapPanel({
54
+ sample: sampleProp,
55
+ transforms: transformsProp,
56
+ className,
57
+ }: FieldRemapPanelProps): JSX.Element {
58
+ const sample = resolveSample(sampleProp);
59
+ const registry = useMemo(() => {
60
+ if (transformsProp) {
61
+ return transformsProp;
62
+ }
63
+ const next = createBuiltinValueTransformRegistry();
64
+ next.register(jsonataValueTransform);
65
+ return next;
66
+ }, [transformsProp]);
67
+
68
+ const [edges, setEdges] = useState<readonly MappingEdge[]>(() => [...sample.edges]);
69
+ const [result, setResult] = useState<FieldRemapPreviewResult>({ output: {} });
70
+
71
+ const sourceFields = useMemo(
72
+ () => sourceFieldsFromPlainObject(sample.source, { idPrefix: sample.sourceIdPrefix }),
73
+ [sample],
74
+ );
75
+ const targetSlots = useMemo(
76
+ () => targetSlotsFromPlainObject(sample.targetShape, { idPrefix: sample.targetIdPrefix }),
77
+ [sample],
78
+ );
79
+
80
+ const shapes = useMemo(
81
+ () => [
82
+ defineDataShape({
83
+ id: sample.sourceIdPrefix,
84
+ label: sample.sourceLabel,
85
+ role: 'source',
86
+ fields: sourceFields,
87
+ }),
88
+ defineDataShape({
89
+ id: sample.targetIdPrefix,
90
+ label: sample.targetLabel,
91
+ role: 'target',
92
+ fields: targetSlots,
93
+ }),
94
+ ],
95
+ [sample, sourceFields, targetSlots],
96
+ );
97
+
98
+ const conflicts = useMemo(
99
+ () => findParentChildMappingConflicts(edges, sourceFields, targetSlots),
100
+ [edges, sourceFields, targetSlots],
101
+ );
102
+
103
+ useEffect(() => {
104
+ const controller = new AbortController();
105
+ const conversion = withConversionEdges(
106
+ defineConversion({
107
+ id: `${sample.sourceIdPrefix}→${sample.targetIdPrefix}`,
108
+ sourceShapeIds: [sample.sourceIdPrefix],
109
+ targetShapeId: sample.targetIdPrefix,
110
+ edges: [...sample.edges],
111
+ }),
112
+ edges,
113
+ );
114
+
115
+ void convertToShape({
116
+ conversion,
117
+ shapes,
118
+ inputs: { [sample.sourceIdPrefix]: sample.source },
119
+ transforms: registry,
120
+ signal: controller.signal,
121
+ })
122
+ .then((next) => {
123
+ if (!controller.signal.aborted) {
124
+ setResult({ output: next.output });
125
+ }
126
+ })
127
+ .catch((error: unknown) => {
128
+ if (controller.signal.aborted) {
129
+ return;
130
+ }
131
+ setResult({
132
+ output: {},
133
+ error: error instanceof Error ? error.message : String(error),
134
+ });
135
+ });
136
+
137
+ return () => {
138
+ controller.abort();
139
+ };
140
+ }, [edges, registry, sample, shapes]);
141
+
142
+ return (
143
+ <div
144
+ className={['workbench-field-remap-demo', className].filter(Boolean).join(' ')}
145
+ data-testid="field-remap-demo"
146
+ data-sample-id={sample.id}
147
+ >
148
+ <header className="workbench-field-remap-demo__header">
149
+ <h2 className="workbench-field-remap-demo__title">{sample.title}</h2>
150
+ <p className="workbench-field-remap-demo__intro">{sample.description}</p>
151
+ </header>
152
+
153
+ <FieldRemapFlowMapper
154
+ sources={sourceFields}
155
+ targets={targetSlots}
156
+ edges={edges}
157
+ transforms={registry}
158
+ onEdgesChange={setEdges}
159
+ sourceTitle={sample.sourceLabel}
160
+ targetTitle={sample.targetLabel}
161
+ />
162
+
163
+ {conflicts.length > 0 ? (
164
+ <p
165
+ className="workbench-field-remap-demo__warn"
166
+ role="status"
167
+ data-testid="field-remap-conflicts"
168
+ >
169
+ Warning: parent and child fields are both mapped (
170
+ {conflicts.map((item) => `${item.parentId} / ${item.childId}`).join('; ')}). Prefer one
171
+ level.
172
+ </p>
173
+ ) : null}
174
+
175
+ {result.error ? (
176
+ <p className="workbench-field-remap-demo__error" role="alert">
177
+ {result.error}
178
+ </p>
179
+ ) : null}
180
+
181
+ <div className="workbench-field-remap-demo__panes">
182
+ <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-source">
183
+ <h3 id="field-remap-source">{sample.sourceLabel}</h3>
184
+ <pre data-testid="field-remap-input">{JSON.stringify(sample.source, null, 2)}</pre>
185
+ </section>
186
+ <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-target">
187
+ <h3 id="field-remap-target">{sample.targetLabel}</h3>
188
+ <pre data-testid="field-remap-result">{JSON.stringify(result.output, null, 2)}</pre>
189
+ </section>
190
+ </div>
191
+ </div>
192
+ );
193
+ }