@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,507 @@
1
+ import type {
2
+ MappingEdge,
3
+ MappingOperator,
4
+ SourceField,
5
+ TargetSlot,
6
+ } from '@workbench-kit/field-remap';
7
+
8
+ /**
9
+ * Field-remap demo scenarios (table / JSON shaped).
10
+ * Runtime stays MappingEdge + convertToShape; UI binds via FieldRemapPanel.
11
+ */
12
+ export type FieldRemapSampleId =
13
+ | 'nested-ab'
14
+ | 't-user-contact'
15
+ | 't-event-time'
16
+ | 't-emp-dept'
17
+ | 't-product-catalog'
18
+ | 'nm-combine-split';
19
+
20
+ export interface FieldRemapSampleDefinition {
21
+ readonly id: FieldRemapSampleId;
22
+ readonly title: string;
23
+ readonly description: string;
24
+ readonly sourceLabel: string;
25
+ readonly targetLabel: string;
26
+ readonly source: Record<string, unknown>;
27
+ readonly targetShape: Record<string, unknown>;
28
+ readonly sourceIdPrefix: string;
29
+ readonly targetIdPrefix: string;
30
+ readonly edges: readonly MappingEdge[];
31
+ readonly operators?: readonly MappingOperator[];
32
+ }
33
+
34
+ /** Alias kept so older URIs still open a known sample. */
35
+ export const FIELD_REMAP_LEGACY_SAMPLE_ALIASES: Readonly<Record<string, FieldRemapSampleId>> = {
36
+ 'interactive-bindings': 'nested-ab',
37
+ 't-order-invoice': 't-event-time',
38
+ };
39
+
40
+ export const FIELD_REMAP_SAMPLES: readonly FieldRemapSampleDefinition[] = [
41
+ {
42
+ id: 'nested-ab',
43
+ title: 'A → B',
44
+ description:
45
+ 'Schema columns A/B with port wires: trim/upper convert chain, leaf location map, array reduce, and itemEdges',
46
+ sourceLabel: 'A',
47
+ targetLabel: 'B',
48
+ sourceIdPrefix: 'a',
49
+ targetIdPrefix: 'b',
50
+ source: {
51
+ user_name: ' Ada Lovelace ',
52
+ profile: {
53
+ city: ' London ',
54
+ country: 'UK',
55
+ },
56
+ tags: [
57
+ { name: 'math', rank: 1 },
58
+ { name: 'computing', rank: 2 },
59
+ ],
60
+ },
61
+ targetShape: {
62
+ name: '',
63
+ title: '',
64
+ location: {
65
+ city: '',
66
+ country: '',
67
+ },
68
+ labels: [{ title: '', order: 0 }],
69
+ firstTag: '',
70
+ tagLine: '',
71
+ },
72
+ edges: [
73
+ {
74
+ id: 'e-name',
75
+ sourceFieldId: 'a.user_name',
76
+ targetSlotId: 'b.name',
77
+ transformIds: ['string:trim'],
78
+ },
79
+ {
80
+ id: 'e-title',
81
+ sourceFieldId: 'a.user_name',
82
+ targetSlotId: 'b.title',
83
+ transformIds: ['string:trim', 'string:upper'],
84
+ },
85
+ {
86
+ // Leaf convert chain (source → trim → upper → target), as in the BINDINGS topology.
87
+ id: 'e-city',
88
+ sourceFieldId: 'a.profile.city',
89
+ targetSlotId: 'b.location.city',
90
+ transformIds: ['string:trim', 'string:upper'],
91
+ },
92
+ {
93
+ id: 'e-country',
94
+ sourceFieldId: 'a.profile.country',
95
+ targetSlotId: 'b.location.country',
96
+ },
97
+ {
98
+ id: 'e-tags',
99
+ sourceFieldId: 'a.tags',
100
+ targetSlotId: 'b.labels',
101
+ itemEdges: [
102
+ {
103
+ id: 'e-tag-title',
104
+ sourceFieldId: 'a.tags.item.name',
105
+ targetSlotId: 'b.labels.item.title',
106
+ },
107
+ {
108
+ id: 'e-tag-order',
109
+ sourceFieldId: 'a.tags.item.rank',
110
+ targetSlotId: 'b.labels.item.order',
111
+ },
112
+ ],
113
+ },
114
+ {
115
+ id: 'e-first-tag',
116
+ sourceFieldId: 'a.tags',
117
+ targetSlotId: 'b.firstTag',
118
+ itemSourcePath: 'name',
119
+ transformIds: ['array:first'],
120
+ },
121
+ {
122
+ id: 'e-tag-line',
123
+ sourceFieldId: 'a.tags',
124
+ targetSlotId: 'b.tagLine',
125
+ itemSourcePath: 'name',
126
+ transformIds: ['array:join'],
127
+ transformOptionSteps: [{ separator: ' · ' }],
128
+ },
129
+ ],
130
+ },
131
+ {
132
+ id: 't-user-contact',
133
+ title: 'T_USER → T_CONTACT',
134
+ description: 'Column map plus NAME object → fullName via string:template',
135
+ sourceLabel: 'T_USER',
136
+ targetLabel: 'T_CONTACT',
137
+ sourceIdPrefix: 't_user',
138
+ targetIdPrefix: 't_contact',
139
+ source: {
140
+ USER_ID: 1001,
141
+ NAME: {
142
+ FIRST_NM: 'Ada',
143
+ LAST_NM: 'Lovelace',
144
+ },
145
+ EMAIL_ADDR: 'ada@example.com',
146
+ PHONE_NO: '555-0100',
147
+ },
148
+ targetShape: {
149
+ id: 0,
150
+ fullName: '',
151
+ email: '',
152
+ phone: '',
153
+ },
154
+ edges: [
155
+ {
156
+ id: 'map-id',
157
+ sourceFieldId: 't_user.USER_ID',
158
+ targetSlotId: 't_contact.id',
159
+ },
160
+ {
161
+ id: 'map-full-name',
162
+ sourceFieldId: 't_user.NAME',
163
+ targetSlotId: 't_contact.fullName',
164
+ transformIds: ['string:template'],
165
+ transformOptionSteps: [{ template: '{FIRST_NM} {LAST_NM}' }],
166
+ },
167
+ {
168
+ id: 'map-email',
169
+ sourceFieldId: 't_user.EMAIL_ADDR',
170
+ targetSlotId: 't_contact.email',
171
+ transformIds: ['string:lower'],
172
+ },
173
+ {
174
+ id: 'map-phone',
175
+ sourceFieldId: 't_user.PHONE_NO',
176
+ targetSlotId: 't_contact.phone',
177
+ },
178
+ ],
179
+ },
180
+ {
181
+ id: 't-event-time',
182
+ title: 'T_EVENT → T_SLOT',
183
+ description: 'Date reformat, date+time combine, and datetime split (fan-out)',
184
+ sourceLabel: 'T_EVENT',
185
+ targetLabel: 'T_SLOT',
186
+ sourceIdPrefix: 't_event',
187
+ targetIdPrefix: 't_slot',
188
+ source: {
189
+ EVENT_ID: 'EV-7',
190
+ YMD_RAW: '20260720',
191
+ WHEN: {
192
+ date: '2026-07-20',
193
+ time: '14:30:00',
194
+ },
195
+ OCCURS_AT: '2026-07-21T09:15:00',
196
+ },
197
+ targetShape: {
198
+ eventId: '',
199
+ displayDate: '',
200
+ startsAt: '',
201
+ occurDate: '',
202
+ occurTime: '',
203
+ },
204
+ edges: [
205
+ {
206
+ id: 'ev-id',
207
+ sourceFieldId: 't_event.EVENT_ID',
208
+ targetSlotId: 't_slot.eventId',
209
+ },
210
+ {
211
+ id: 'ev-display-date',
212
+ sourceFieldId: 't_event.YMD_RAW',
213
+ targetSlotId: 't_slot.displayDate',
214
+ transformIds: ['date:reformat'],
215
+ transformOptionSteps: [{ inputFormat: 'YYYYMMDD', outputFormat: 'YYYY.MM.DD' }],
216
+ },
217
+ {
218
+ id: 'ev-starts',
219
+ sourceFieldId: 't_event.WHEN',
220
+ targetSlotId: 't_slot.startsAt',
221
+ transformIds: ['datetime:combine'],
222
+ },
223
+ {
224
+ id: 'ev-occur-date',
225
+ sourceFieldId: 't_event.OCCURS_AT',
226
+ targetSlotId: 't_slot.occurDate',
227
+ transformIds: ['datetime:date'],
228
+ },
229
+ {
230
+ id: 'ev-occur-time',
231
+ sourceFieldId: 't_event.OCCURS_AT',
232
+ targetSlotId: 't_slot.occurTime',
233
+ transformIds: ['datetime:time'],
234
+ },
235
+ ],
236
+ },
237
+ {
238
+ id: 't-emp-dept',
239
+ title: 'T_EMP → T_EMP_ROW',
240
+ description: 'Join-style flatten: nested DEPT object into flat row columns',
241
+ sourceLabel: 'T_EMP',
242
+ targetLabel: 'T_EMP_ROW',
243
+ sourceIdPrefix: 't_emp',
244
+ targetIdPrefix: 't_emp_row',
245
+ source: {
246
+ EMP_NO: 'E-42',
247
+ EMP_NM: 'Grace Hopper',
248
+ DEPT: {
249
+ DEPT_CD: 'RND',
250
+ DEPT_NM: 'Research',
251
+ },
252
+ },
253
+ targetShape: {
254
+ empNo: '',
255
+ empName: '',
256
+ deptCode: '',
257
+ deptName: '',
258
+ },
259
+ edges: [
260
+ {
261
+ id: 'emp-no',
262
+ sourceFieldId: 't_emp.EMP_NO',
263
+ targetSlotId: 't_emp_row.empNo',
264
+ },
265
+ {
266
+ id: 'emp-nm',
267
+ sourceFieldId: 't_emp.EMP_NM',
268
+ targetSlotId: 't_emp_row.empName',
269
+ },
270
+ {
271
+ id: 'emp-dept-cd',
272
+ sourceFieldId: 't_emp.DEPT.DEPT_CD',
273
+ targetSlotId: 't_emp_row.deptCode',
274
+ },
275
+ {
276
+ id: 'emp-dept-nm',
277
+ sourceFieldId: 't_emp.DEPT.DEPT_NM',
278
+ targetSlotId: 't_emp_row.deptName',
279
+ transformIds: ['string:upper'],
280
+ },
281
+ ],
282
+ },
283
+ {
284
+ id: 't-product-catalog',
285
+ title: 'T_PRODUCT → T_CATALOG_ITEM',
286
+ description: 'Parent row plus TAGS[] itemEdges into catalog labels',
287
+ sourceLabel: 'T_PRODUCT',
288
+ targetLabel: 'T_CATALOG_ITEM',
289
+ sourceIdPrefix: 't_product',
290
+ targetIdPrefix: 't_catalog',
291
+ source: {
292
+ PROD_ID: 'P-9',
293
+ PROD_NM: ' Analytical Engine ',
294
+ TAGS: [
295
+ { TAG_CD: 'hist', TAG_NM: 'history' },
296
+ { TAG_CD: 'comp', TAG_NM: 'computing' },
297
+ ],
298
+ },
299
+ targetShape: {
300
+ productId: '',
301
+ name: '',
302
+ labels: [{ code: '', title: '' }],
303
+ tagLine: '',
304
+ },
305
+ edges: [
306
+ {
307
+ id: 'prod-id',
308
+ sourceFieldId: 't_product.PROD_ID',
309
+ targetSlotId: 't_catalog.productId',
310
+ },
311
+ {
312
+ id: 'prod-nm',
313
+ sourceFieldId: 't_product.PROD_NM',
314
+ targetSlotId: 't_catalog.name',
315
+ transformIds: ['string:trim'],
316
+ },
317
+ {
318
+ id: 'prod-tags',
319
+ sourceFieldId: 't_product.TAGS',
320
+ targetSlotId: 't_catalog.labels',
321
+ itemEdges: [
322
+ {
323
+ id: 'tag-code',
324
+ sourceFieldId: 't_product.TAGS.item.TAG_CD',
325
+ targetSlotId: 't_catalog.labels.item.code',
326
+ transformIds: ['string:upper'],
327
+ },
328
+ {
329
+ id: 'tag-title',
330
+ sourceFieldId: 't_product.TAGS.item.TAG_NM',
331
+ targetSlotId: 't_catalog.labels.item.title',
332
+ },
333
+ ],
334
+ },
335
+ {
336
+ id: 'prod-tag-line',
337
+ sourceFieldId: 't_product.TAGS',
338
+ targetSlotId: 't_catalog.tagLine',
339
+ itemSourcePath: 'TAG_NM',
340
+ transformIds: ['array:join'],
341
+ transformOptionSteps: [{ separator: ' / ' }],
342
+ },
343
+ ],
344
+ },
345
+ {
346
+ id: 'nm-combine-split',
347
+ title: 'n→m combine / split',
348
+ description:
349
+ 'Document v2 operators: fan-in combine (first+last → nameBag) and fan-out split (address → city/zip)',
350
+ sourceLabel: 'A',
351
+ targetLabel: 'B',
352
+ sourceIdPrefix: 'a',
353
+ targetIdPrefix: 'b',
354
+ source: {
355
+ first: 'Ada',
356
+ last: 'Lovelace',
357
+ address: { city: 'London', zip: 'E1' },
358
+ },
359
+ targetShape: {
360
+ nameBag: { first: '', last: '' },
361
+ city: '',
362
+ zip: '',
363
+ },
364
+ edges: [],
365
+ operators: [
366
+ {
367
+ kind: 'combine',
368
+ id: 'op-name',
369
+ inputFieldIds: ['a.first', 'a.last'],
370
+ outputSlotId: 'b.nameBag',
371
+ },
372
+ {
373
+ kind: 'split',
374
+ id: 'op-address',
375
+ inputFieldId: 'a.address',
376
+ outputSlotIds: ['b.city', 'b.zip'],
377
+ },
378
+ ],
379
+ },
380
+ ];
381
+
382
+ const SAMPLE_BY_ID = new Map(FIELD_REMAP_SAMPLES.map((sample) => [sample.id, sample]));
383
+
384
+ export function isFieldRemapSampleId(value: string): value is FieldRemapSampleId {
385
+ return SAMPLE_BY_ID.has(value as FieldRemapSampleId);
386
+ }
387
+
388
+ export function resolveFieldRemapSampleId(value: string | undefined): FieldRemapSampleId {
389
+ if (!value) {
390
+ return 'nested-ab';
391
+ }
392
+ const aliased = FIELD_REMAP_LEGACY_SAMPLE_ALIASES[value];
393
+ if (aliased) {
394
+ return aliased;
395
+ }
396
+ if (isFieldRemapSampleId(value)) {
397
+ return value;
398
+ }
399
+ return 'nested-ab';
400
+ }
401
+
402
+ export function getFieldRemapSample(sampleId: string | undefined): FieldRemapSampleDefinition {
403
+ const id = resolveFieldRemapSampleId(sampleId);
404
+ return SAMPLE_BY_ID.get(id) ?? FIELD_REMAP_SAMPLES[0]!;
405
+ }
406
+
407
+ /**
408
+ * Controlled shapes for browse-chrome demos: nested `classRef` + one `hidden` leaf.
409
+ * Pair with {@link getFieldRemapSample}(`nested-ab`) edges / sample JSON.
410
+ */
411
+ export function getFieldRemapBrowseDemoShapes(): {
412
+ readonly sources: readonly SourceField[];
413
+ readonly targets: readonly TargetSlot[];
414
+ } {
415
+ return {
416
+ sources: [
417
+ {
418
+ id: 'a',
419
+ label: 'A',
420
+ path: '',
421
+ dataType: 'object',
422
+ children: [
423
+ {
424
+ id: 'a.user_name',
425
+ label: 'user_name',
426
+ path: 'user_name',
427
+ dataType: 'string',
428
+ sampleValue: ' Ada Lovelace ',
429
+ },
430
+ {
431
+ id: 'a.profile',
432
+ label: 'profile',
433
+ path: 'profile',
434
+ dataType: 'object',
435
+ classRef: { id: 'PersonProfile', version: 1 },
436
+ children: [
437
+ {
438
+ id: 'a.profile.city',
439
+ label: 'city',
440
+ path: 'profile.city',
441
+ dataType: 'string',
442
+ sampleValue: ' London ',
443
+ },
444
+ {
445
+ id: 'a.profile.country',
446
+ label: 'country',
447
+ path: 'profile.country',
448
+ dataType: 'string',
449
+ sampleValue: 'UK',
450
+ },
451
+ {
452
+ id: 'a.profile.internal_id',
453
+ label: 'internal_id',
454
+ path: 'profile.internal_id',
455
+ dataType: 'string',
456
+ hidden: true,
457
+ sampleValue: 'secret-row',
458
+ },
459
+ ],
460
+ },
461
+ {
462
+ id: 'a.tags',
463
+ label: 'tags',
464
+ path: 'tags',
465
+ dataType: 'array',
466
+ },
467
+ ],
468
+ },
469
+ ],
470
+ targets: [
471
+ {
472
+ id: 'b',
473
+ label: 'B',
474
+ path: '',
475
+ dataType: 'object',
476
+ children: [
477
+ { id: 'b.name', label: 'name', path: 'name', dataType: 'string' },
478
+ { id: 'b.title', label: 'title', path: 'title', dataType: 'string' },
479
+ {
480
+ id: 'b.location',
481
+ label: 'location',
482
+ path: 'location',
483
+ dataType: 'object',
484
+ classRef: { id: 'GeoLocation', version: 2 },
485
+ children: [
486
+ { id: 'b.location.city', label: 'city', path: 'location.city', dataType: 'string' },
487
+ {
488
+ id: 'b.location.country',
489
+ label: 'country',
490
+ path: 'location.country',
491
+ dataType: 'string',
492
+ },
493
+ ],
494
+ },
495
+ {
496
+ id: 'b.labels',
497
+ label: 'labels',
498
+ path: 'labels',
499
+ dataType: 'array',
500
+ },
501
+ { id: 'b.firstTag', label: 'firstTag', path: 'firstTag', dataType: 'string' },
502
+ { id: 'b.tagLine', label: 'tagLine', path: 'tagLine', dataType: 'string' },
503
+ ],
504
+ },
505
+ ],
506
+ };
507
+ }
@@ -0,0 +1,170 @@
1
+ import { useMemo, useState, type JSX } from 'react';
2
+ import { Button } from '@workbench-kit/react/primitives';
3
+ import {
4
+ FIELD_DATA_TYPES,
5
+ flattenSourceFields,
6
+ flattenTargetSlots,
7
+ isFieldDataType,
8
+ setSourceFieldDataType,
9
+ setTargetSlotDataType,
10
+ sourceFieldsFromPlainObject,
11
+ targetSlotsFromPlainObject,
12
+ type FieldDataType,
13
+ type SourceField,
14
+ type TargetSlot,
15
+ } from '@workbench-kit/field-remap';
16
+
17
+ export type FieldRemapShapeRole = 'source' | 'target';
18
+
19
+ export interface FieldRemapShapeIoEditorProps {
20
+ readonly role: FieldRemapShapeRole;
21
+ readonly title: string;
22
+ readonly idPrefix: string;
23
+ /** Current plain sample used for ingest / preview input. */
24
+ readonly sampleJson: string;
25
+ readonly fields: readonly SourceField[] | readonly TargetSlot[];
26
+ readonly onSampleJsonChange: (next: string) => void;
27
+ readonly onApplySample: (parsed: unknown) => void;
28
+ readonly onFieldsChange: (next: readonly SourceField[] | readonly TargetSlot[]) => void;
29
+ }
30
+
31
+ function formatJson(value: unknown): string {
32
+ return JSON.stringify(value, null, 2);
33
+ }
34
+
35
+ /**
36
+ * Host-owned shape IO surface: paste JSON → ingest fields/slots, edit FieldDataType.
37
+ * Persistence stays host-owned (`FieldRemapDocument` v1 is edges-only).
38
+ */
39
+ export function FieldRemapShapeIoEditor({
40
+ role,
41
+ title,
42
+ idPrefix,
43
+ sampleJson,
44
+ fields,
45
+ onSampleJsonChange,
46
+ onApplySample,
47
+ onFieldsChange,
48
+ }: FieldRemapShapeIoEditorProps): JSX.Element {
49
+ const [parseError, setParseError] = useState<string | undefined>();
50
+ const flat = useMemo(
51
+ () =>
52
+ role === 'source'
53
+ ? flattenSourceFields(fields as readonly SourceField[])
54
+ : flattenTargetSlots(fields as readonly TargetSlot[]),
55
+ [fields, role],
56
+ );
57
+
58
+ const applyJson = () => {
59
+ try {
60
+ const parsed: unknown = JSON.parse(sampleJson);
61
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
62
+ setParseError('Shape sample must be a JSON object.');
63
+ return;
64
+ }
65
+ setParseError(undefined);
66
+ onApplySample(parsed);
67
+ } catch (error) {
68
+ setParseError(error instanceof Error ? error.message : String(error));
69
+ }
70
+ };
71
+
72
+ const onTypeChange = (id: string, dataType: FieldDataType) => {
73
+ if (role === 'source') {
74
+ onFieldsChange(setSourceFieldDataType(fields as readonly SourceField[], id, dataType));
75
+ return;
76
+ }
77
+ onFieldsChange(setTargetSlotDataType(fields as readonly TargetSlot[], id, dataType));
78
+ };
79
+
80
+ return (
81
+ <section
82
+ className="workbench-field-remap-shape-io"
83
+ data-testid={`field-remap-shape-io-${role}`}
84
+ aria-labelledby={`field-remap-shape-io-${role}-title`}
85
+ >
86
+ <header className="workbench-field-remap-shape-io__header">
87
+ <h3 id={`field-remap-shape-io-${role}-title`}>{title}</h3>
88
+ <p className="workbench-field-remap-shape-io__hint">
89
+ Paste JSON to ingest {role === 'source' ? 'source fields' : 'target slots'} (prefix{' '}
90
+ <code>{idPrefix}</code>). Types stay editable; hosts own shape persistence.
91
+ </p>
92
+ </header>
93
+
94
+ <label className="workbench-field-remap-shape-io__json-label">
95
+ <span>{role === 'source' ? 'Source JSON' : 'Target shape JSON'}</span>
96
+ <textarea
97
+ className="workbench-field-remap-shape-io__json"
98
+ aria-label={`${title} JSON`}
99
+ rows={8}
100
+ value={sampleJson}
101
+ onChange={(event) => onSampleJsonChange(event.target.value)}
102
+ spellCheck={false}
103
+ />
104
+ </label>
105
+
106
+ <div className="workbench-field-remap-shape-io__actions">
107
+ <Button type="button" onClick={applyJson}>
108
+ Apply JSON
109
+ </Button>
110
+ </div>
111
+
112
+ {parseError ? (
113
+ <p className="workbench-field-remap-shape-io__error" role="alert">
114
+ {parseError}
115
+ </p>
116
+ ) : null}
117
+
118
+ <ul className="workbench-field-remap-shape-io__fields" aria-label={`${title} fields`}>
119
+ {flat.map((field) => (
120
+ <li key={field.id} className="workbench-field-remap-shape-io__field">
121
+ <div className="workbench-field-remap-shape-io__field-meta">
122
+ <code>{field.id}</code>
123
+ <span>{field.path ?? field.label}</span>
124
+ </div>
125
+ <label className="workbench-field-remap-shape-io__type">
126
+ <span className="workbench-field-remap-shape-io__type-label">Type</span>
127
+ <select
128
+ aria-label={`Type for ${field.id}`}
129
+ value={field.dataType ?? 'unknown'}
130
+ onChange={(event) => {
131
+ const next = event.target.value;
132
+ if (isFieldDataType(next)) {
133
+ onTypeChange(field.id, next);
134
+ }
135
+ }}
136
+ >
137
+ {FIELD_DATA_TYPES.map((type) => (
138
+ <option key={type} value={type}>
139
+ {type}
140
+ </option>
141
+ ))}
142
+ </select>
143
+ </label>
144
+ </li>
145
+ ))}
146
+ </ul>
147
+ </section>
148
+ );
149
+ }
150
+
151
+ /** Ingest helpers shared by the panel when Apply JSON runs. */
152
+ export function ingestSourceShape(
153
+ sample: unknown,
154
+ idPrefix: string,
155
+ ): { fields: SourceField[]; sampleJson: string } {
156
+ return {
157
+ fields: sourceFieldsFromPlainObject(sample, { idPrefix }),
158
+ sampleJson: formatJson(sample),
159
+ };
160
+ }
161
+
162
+ export function ingestTargetShape(
163
+ sample: unknown,
164
+ idPrefix: string,
165
+ ): { fields: TargetSlot[]; sampleJson: string } {
166
+ return {
167
+ fields: targetSlotsFromPlainObject(sample, { idPrefix }),
168
+ sampleJson: formatJson(sample),
169
+ };
170
+ }