@workbench-kit/shell-react 0.0.2-prototype.0.2.40 → 0.0.2-prototype.0.2.43

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 (35) hide show
  1. package/README.md +42 -2
  2. package/package.json +13 -10
  3. package/src/extensions/extension-enablement-controller.ts +40 -9
  4. package/src/extensions/theme-selection-protection.ts +80 -55
  5. package/src/field-remap/chrome-labels.ts +18 -0
  6. package/src/field-remap/convert-note-editor.tsx +30 -24
  7. package/src/field-remap/convert-palette.tsx +6 -0
  8. package/src/field-remap/demo.tsx +8 -2
  9. package/src/field-remap/detail-panel.tsx +225 -192
  10. package/src/field-remap/drag-payload.ts +48 -0
  11. package/src/field-remap/flow-adapter.ts +216 -88
  12. package/src/field-remap/flow-ops.ts +150 -0
  13. package/src/field-remap/flow.tsx +1055 -162
  14. package/src/field-remap/index.ts +2 -0
  15. package/src/field-remap/io-class-browse.tsx +34 -22
  16. package/src/field-remap/keyboard.ts +16 -0
  17. package/src/field-remap/modal-detail.tsx +34 -0
  18. package/src/field-remap/panel.tsx +50 -14
  19. package/src/field-remap/transform-options-editor.tsx +5 -1
  20. package/src/field-remap/view.css +61 -42
  21. package/src/index.ts +7 -0
  22. package/src/management/keybinding-overrides-storage.ts +48 -23
  23. package/src/management/keybinding-settings.tsx +10 -1
  24. package/src/management/use-keybinding-management.ts +69 -19
  25. package/src/shell/appearance-catalog.ts +453 -0
  26. package/src/shell/appearance-controller.ts +331 -0
  27. package/src/shell/appearance-presentation.ts +269 -0
  28. package/src/shell/provider.tsx +147 -18
  29. package/src/shell/settings.tsx +282 -153
  30. package/src/shell/shell.tsx +88 -18
  31. package/src/workbench/appearance-storage.ts +2 -2
  32. package/src/workbench/command-host-controller.tsx +223 -0
  33. package/src/workbench/command-host.tsx +100 -150
  34. package/src/workbench/keybinding-bridge.ts +84 -38
  35. package/src/workbench/shell-command-registration.ts +27 -2
@@ -45,6 +45,7 @@ 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 readOnly?: boolean | undefined;
48
49
  readonly emptyDetailTitle?: string | undefined;
49
50
  readonly emptyDetailDescription?: string | undefined;
50
51
  }
@@ -79,6 +80,7 @@ export function FieldRemapDetailPanel({
79
80
  onDiscardDraft,
80
81
  operators = [],
81
82
  onOperatorsChange,
83
+ readOnly = false,
82
84
  emptyDetailTitle = 'Start with a convert',
83
85
  emptyDetailDescription = 'Use the Convert palette to place a convert, then wire source → draft → target. Or select an existing binding / convert note on the canvas.',
84
86
  }: FieldRemapDetailPanelProps): JSX.Element | null {
@@ -109,6 +111,12 @@ export function FieldRemapDetailPanel({
109
111
  const [pendingOperatorSlotId, setPendingOperatorSlotId] = useState('');
110
112
  const flatSources = useMemo(() => flattenSourceFields(sources), [sources]);
111
113
  const flatTargets = useMemo(() => flattenTargetSlots(targets), [targets]);
114
+ const commitOperators = (next: readonly MappingOperator[]) => {
115
+ if (readOnly) {
116
+ return;
117
+ }
118
+ onOperatorsChange?.(next);
119
+ };
112
120
 
113
121
  if (!selection) {
114
122
  return (
@@ -138,17 +146,19 @@ export function FieldRemapDetailPanel({
138
146
  {definition?.label ?? draft?.transformId ?? selection.localId}
139
147
  </code>
140
148
  </div>
141
- <Button
142
- compact
143
- type="button"
144
- data-testid="field-remap-detail-discard-draft"
145
- onClick={() => {
146
- onDiscardDraft?.(selection.localId);
147
- onSelectionChange(null);
148
- }}
149
- >
150
- Discard
151
- </Button>
149
+ {!readOnly ? (
150
+ <Button
151
+ compact
152
+ type="button"
153
+ data-testid="field-remap-detail-discard-draft"
154
+ onClick={() => {
155
+ onDiscardDraft?.(selection.localId);
156
+ onSelectionChange(null);
157
+ }}
158
+ >
159
+ Discard
160
+ </Button>
161
+ ) : null}
152
162
  </div>
153
163
  <p className="workbench-field-remap-detail__muted">
154
164
  Wire source then target ports. When both bind, the convert note editor opens.
@@ -175,7 +185,7 @@ export function FieldRemapDetailPanel({
175
185
  }
176
186
 
177
187
  if (selection.kind === 'operator') {
178
- if (!operator || !onOperatorsChange) {
188
+ if (!operator || (!readOnly && !onOperatorsChange)) {
179
189
  return (
180
190
  <aside className="workbench-field-remap-detail" data-testid="field-remap-detail">
181
191
  <h4>Operator</h4>
@@ -197,17 +207,19 @@ export function FieldRemapDetailPanel({
197
207
  <h4>{operator.kind === 'combine' ? 'Combine (n→1)' : 'Split (1→n)'}</h4>
198
208
  <code data-testid="field-remap-detail-operator-id">{operator.id}</code>
199
209
  </div>
200
- <Button
201
- compact
202
- type="button"
203
- data-testid="field-remap-detail-delete-operator"
204
- onClick={() => {
205
- onOperatorsChange(removeMappingOperator(operators, operator.id));
206
- onSelectionChange(null);
207
- }}
208
- >
209
- Delete
210
- </Button>
210
+ {!readOnly ? (
211
+ <Button
212
+ compact
213
+ type="button"
214
+ data-testid="field-remap-detail-delete-operator"
215
+ onClick={() => {
216
+ commitOperators(removeMappingOperator(operators, operator.id));
217
+ onSelectionChange(null);
218
+ }}
219
+ >
220
+ Delete
221
+ </Button>
222
+ ) : null}
211
223
  </div>
212
224
  {operator.kind === 'combine' ? (
213
225
  <section className="workbench-field-remap-detail__section">
@@ -216,65 +228,70 @@ export function FieldRemapDetailPanel({
216
228
  {operator.inputFieldIds.map((fieldId) => (
217
229
  <li key={fieldId}>
218
230
  <code>{fieldLabel(fieldId, sources, targets)}</code>
219
- <Button
220
- compact
221
- type="button"
222
- onClick={() =>
223
- onOperatorsChange(
224
- updateMappingOperator(operators, operator.id, (current) =>
225
- removeOperatorInput(current, fieldId),
226
- ),
227
- )
228
- }
229
- >
230
- Remove
231
- </Button>
231
+ {!readOnly ? (
232
+ <Button
233
+ compact
234
+ type="button"
235
+ onClick={() =>
236
+ commitOperators(
237
+ updateMappingOperator(operators, operator.id, (current) =>
238
+ removeOperatorInput(current, fieldId),
239
+ ),
240
+ )
241
+ }
242
+ >
243
+ Remove
244
+ </Button>
245
+ ) : null}
232
246
  </li>
233
247
  ))}
234
248
  </ul>
235
- <div className="workbench-field-remap-detail__field">
236
- <label>
237
- <span>Add input</span>
238
- <select
239
- data-testid="field-remap-operator-add-input"
240
- value={pendingOperatorFieldId}
241
- onChange={(event) => setPendingOperatorFieldId(event.target.value)}
249
+ {!readOnly ? (
250
+ <div className="workbench-field-remap-detail__field">
251
+ <label>
252
+ <span>Add input</span>
253
+ <select
254
+ data-testid="field-remap-operator-add-input"
255
+ value={pendingOperatorFieldId}
256
+ onChange={(event) => setPendingOperatorFieldId(event.target.value)}
257
+ >
258
+ <option value="">Select…</option>
259
+ {flatSources.map((field) => (
260
+ <option key={field.id} value={field.id}>
261
+ {field.path ?? field.label}
262
+ </option>
263
+ ))}
264
+ </select>
265
+ </label>
266
+ <Button
267
+ compact
268
+ type="button"
269
+ data-testid="field-remap-operator-bind-input"
270
+ disabled={!pendingOperatorFieldId}
271
+ onClick={() => {
272
+ if (!pendingOperatorFieldId) return;
273
+ commitOperators(
274
+ updateMappingOperator(operators, operator.id, (current) =>
275
+ bindOperatorInput(current, pendingOperatorFieldId),
276
+ ),
277
+ );
278
+ setPendingOperatorFieldId('');
279
+ }}
242
280
  >
243
- <option value="">Select…</option>
244
- {flatSources.map((field) => (
245
- <option key={field.id} value={field.id}>
246
- {field.path ?? field.label}
247
- </option>
248
- ))}
249
- </select>
250
- </label>
251
- <Button
252
- compact
253
- type="button"
254
- data-testid="field-remap-operator-bind-input"
255
- disabled={!pendingOperatorFieldId}
256
- onClick={() => {
257
- if (!pendingOperatorFieldId) return;
258
- onOperatorsChange(
259
- updateMappingOperator(operators, operator.id, (current) =>
260
- bindOperatorInput(current, pendingOperatorFieldId),
261
- ),
262
- );
263
- setPendingOperatorFieldId('');
264
- }}
265
- >
266
- Add
267
- </Button>
268
- </div>
281
+ Add
282
+ </Button>
283
+ </div>
284
+ ) : null}
269
285
  <div className="workbench-field-remap-detail__field">
270
286
  <label>
271
287
  <span>Output slot</span>
272
288
  <select
273
289
  data-testid="field-remap-operator-output"
274
290
  value={operator.outputSlotId}
291
+ disabled={readOnly}
275
292
  onChange={(event) => {
276
293
  const slotId = event.target.value;
277
- onOperatorsChange(
294
+ commitOperators(
278
295
  updateMappingOperator(operators, operator.id, (current) =>
279
296
  slotId
280
297
  ? bindOperatorOutput(current, slotId)
@@ -301,9 +318,10 @@ export function FieldRemapDetailPanel({
301
318
  <select
302
319
  data-testid="field-remap-operator-input"
303
320
  value={operator.inputFieldId}
321
+ disabled={readOnly}
304
322
  onChange={(event) => {
305
323
  const fieldId = event.target.value;
306
- onOperatorsChange(
324
+ commitOperators(
307
325
  updateMappingOperator(operators, operator.id, (current) =>
308
326
  fieldId
309
327
  ? bindOperatorInput(current, fieldId)
@@ -326,56 +344,60 @@ export function FieldRemapDetailPanel({
326
344
  {operator.outputSlotIds.map((slotId) => (
327
345
  <li key={slotId}>
328
346
  <code>{fieldLabel(slotId, sources, targets)}</code>
329
- <Button
330
- compact
331
- type="button"
332
- onClick={() =>
333
- onOperatorsChange(
334
- updateMappingOperator(operators, operator.id, (current) =>
335
- removeOperatorOutput(current, slotId),
336
- ),
337
- )
338
- }
339
- >
340
- Remove
341
- </Button>
347
+ {!readOnly ? (
348
+ <Button
349
+ compact
350
+ type="button"
351
+ onClick={() =>
352
+ commitOperators(
353
+ updateMappingOperator(operators, operator.id, (current) =>
354
+ removeOperatorOutput(current, slotId),
355
+ ),
356
+ )
357
+ }
358
+ >
359
+ Remove
360
+ </Button>
361
+ ) : null}
342
362
  </li>
343
363
  ))}
344
364
  </ul>
345
- <div className="workbench-field-remap-detail__field">
346
- <label>
347
- <span>Add output</span>
348
- <select
349
- data-testid="field-remap-operator-add-output"
350
- value={pendingOperatorSlotId}
351
- onChange={(event) => setPendingOperatorSlotId(event.target.value)}
365
+ {!readOnly ? (
366
+ <div className="workbench-field-remap-detail__field">
367
+ <label>
368
+ <span>Add output</span>
369
+ <select
370
+ data-testid="field-remap-operator-add-output"
371
+ value={pendingOperatorSlotId}
372
+ onChange={(event) => setPendingOperatorSlotId(event.target.value)}
373
+ >
374
+ <option value="">Select…</option>
375
+ {flatTargets.map((slot) => (
376
+ <option key={slot.id} value={slot.id}>
377
+ {slot.path ?? slot.label}
378
+ </option>
379
+ ))}
380
+ </select>
381
+ </label>
382
+ <Button
383
+ compact
384
+ type="button"
385
+ data-testid="field-remap-operator-bind-output"
386
+ disabled={!pendingOperatorSlotId}
387
+ onClick={() => {
388
+ if (!pendingOperatorSlotId) return;
389
+ commitOperators(
390
+ updateMappingOperator(operators, operator.id, (current) =>
391
+ bindOperatorOutput(current, pendingOperatorSlotId),
392
+ ),
393
+ );
394
+ setPendingOperatorSlotId('');
395
+ }}
352
396
  >
353
- <option value="">Select…</option>
354
- {flatTargets.map((slot) => (
355
- <option key={slot.id} value={slot.id}>
356
- {slot.path ?? slot.label}
357
- </option>
358
- ))}
359
- </select>
360
- </label>
361
- <Button
362
- compact
363
- type="button"
364
- data-testid="field-remap-operator-bind-output"
365
- disabled={!pendingOperatorSlotId}
366
- onClick={() => {
367
- if (!pendingOperatorSlotId) return;
368
- onOperatorsChange(
369
- updateMappingOperator(operators, operator.id, (current) =>
370
- bindOperatorOutput(current, pendingOperatorSlotId),
371
- ),
372
- );
373
- setPendingOperatorSlotId('');
374
- }}
375
- >
376
- Add
377
- </Button>
378
- </div>
397
+ Add
398
+ </Button>
399
+ </div>
400
+ ) : null}
379
401
  </section>
380
402
  )}
381
403
  </aside>
@@ -405,6 +427,7 @@ export function FieldRemapDetailPanel({
405
427
  edges={edges}
406
428
  onEdgesChange={onEdgesChange}
407
429
  onSelectionChange={onSelectionChange}
430
+ readOnly={readOnly}
408
431
  />
409
432
  );
410
433
  }
@@ -432,7 +455,7 @@ export function FieldRemapDetailPanel({
432
455
  const itemTargetChildren = targetSlot?.children ?? [];
433
456
 
434
457
  const applyEdge = (next: MappingEdge | null) => {
435
- if (!next) {
458
+ if (readOnly || !next) {
436
459
  return;
437
460
  }
438
461
  onEdgesChange(edges.map((item) => (item.id === edge.id ? next : item)));
@@ -488,27 +511,31 @@ export function FieldRemapDetailPanel({
488
511
  >
489
512
  <strong>{definition?.label ?? transformId}</strong>
490
513
  <code>{transformId}</code>
491
- <span className="workbench-field-remap-detail__step-hint">Edit convert</span>
514
+ <span className="workbench-field-remap-detail__step-hint">
515
+ {readOnly ? 'Inspect convert' : 'Edit convert'}
516
+ </span>
492
517
  </button>
493
- <Button
494
- compact
495
- type="button"
496
- aria-label={`Remove convert step ${index + 1}`}
497
- data-testid={`field-remap-detail-remove-step-${index}`}
498
- onClick={() => {
499
- const next = removeTransformStepFromEdge(edge, index);
500
- applyEdge(next);
501
- }}
502
- >
503
- ×
504
- </Button>
518
+ {!readOnly ? (
519
+ <Button
520
+ compact
521
+ type="button"
522
+ aria-label={`Remove convert step ${index + 1}`}
523
+ data-testid={`field-remap-detail-remove-step-${index}`}
524
+ onClick={() => {
525
+ const next = removeTransformStepFromEdge(edge, index);
526
+ applyEdge(next);
527
+ }}
528
+ >
529
+ ×
530
+ </Button>
531
+ ) : null}
505
532
  </li>
506
533
  );
507
534
  })}
508
535
  </ol>
509
536
  )}
510
537
 
511
- {chain.length < MAX_TRANSFORM_CHAIN ? (
538
+ {!readOnly && chain.length < MAX_TRANSFORM_CHAIN ? (
512
539
  <div
513
540
  className="workbench-field-remap-detail__palette"
514
541
  data-testid="field-remap-transform-palette"
@@ -568,7 +595,7 @@ export function FieldRemapDetailPanel({
568
595
  >
569
596
  <div className="workbench-field-remap-detail__list-context-bar">
570
597
  <h5 id="field-remap-detail-list-context">List context</h5>
571
- {!edge.itemEdges ? (
598
+ {!readOnly && !edge.itemEdges ? (
572
599
  <Button
573
600
  compact
574
601
  type="button"
@@ -582,58 +609,62 @@ export function FieldRemapDetailPanel({
582
609
 
583
610
  {edge.itemEdges ? (
584
611
  <>
585
- <p className="workbench-field-remap-detail__muted">
586
- {pendingItemSourceId
587
- ? 'Select a target item field to complete the item binding.'
588
- : 'Select a source item field, then a target item field.'}
589
- </p>
590
- <div className="workbench-field-remap-detail__item-pickers">
591
- <label>
592
- <span>Source item field</span>
593
- <select
594
- aria-label="Source item field"
595
- data-testid="field-remap-item-source"
596
- value={pendingItemSourceId ?? ''}
597
- onChange={(event) => setPendingItemSourceId(event.target.value || null)}
598
- >
599
- <option value="">Select…</option>
600
- {itemSourceChildren.map((child) => (
601
- <option key={child.id} value={child.id}>
602
- {child.path ?? child.label}
603
- </option>
604
- ))}
605
- </select>
606
- </label>
607
- <label>
608
- <span>Target item field</span>
609
- <select
610
- aria-label="Target item field"
611
- data-testid="field-remap-item-target"
612
- value=""
613
- disabled={!pendingItemSourceId}
614
- onChange={(event) => {
615
- const targetId = event.target.value;
616
- if (!pendingItemSourceId || !targetId) {
617
- return;
618
- }
619
- const itemEdge: MappingEdge = {
620
- id: `ie-${pendingItemSourceId}-${targetId}-${Date.now()}`,
621
- sourceFieldId: pendingItemSourceId,
622
- targetSlotId: targetId,
623
- };
624
- applyEdge(upsertItemEdgeOnParent(edge, itemEdge));
625
- setPendingItemSourceId(null);
626
- }}
627
- >
628
- <option value="">Select…</option>
629
- {itemTargetChildren.map((child) => (
630
- <option key={child.id} value={child.id}>
631
- {child.path ?? child.label}
632
- </option>
633
- ))}
634
- </select>
635
- </label>
636
- </div>
612
+ {!readOnly ? (
613
+ <p className="workbench-field-remap-detail__muted">
614
+ {pendingItemSourceId
615
+ ? 'Select a target item field to complete the item binding.'
616
+ : 'Select a source item field, then a target item field.'}
617
+ </p>
618
+ ) : null}
619
+ {!readOnly ? (
620
+ <div className="workbench-field-remap-detail__item-pickers">
621
+ <label>
622
+ <span>Source item field</span>
623
+ <select
624
+ aria-label="Source item field"
625
+ data-testid="field-remap-item-source"
626
+ value={pendingItemSourceId ?? ''}
627
+ onChange={(event) => setPendingItemSourceId(event.target.value || null)}
628
+ >
629
+ <option value="">Select…</option>
630
+ {itemSourceChildren.map((child) => (
631
+ <option key={child.id} value={child.id}>
632
+ {child.path ?? child.label}
633
+ </option>
634
+ ))}
635
+ </select>
636
+ </label>
637
+ <label>
638
+ <span>Target item field</span>
639
+ <select
640
+ aria-label="Target item field"
641
+ data-testid="field-remap-item-target"
642
+ value=""
643
+ disabled={!pendingItemSourceId}
644
+ onChange={(event) => {
645
+ const targetId = event.target.value;
646
+ if (!pendingItemSourceId || !targetId) {
647
+ return;
648
+ }
649
+ const itemEdge: MappingEdge = {
650
+ id: `ie-${pendingItemSourceId}-${targetId}-${Date.now()}`,
651
+ sourceFieldId: pendingItemSourceId,
652
+ targetSlotId: targetId,
653
+ };
654
+ applyEdge(upsertItemEdgeOnParent(edge, itemEdge));
655
+ setPendingItemSourceId(null);
656
+ }}
657
+ >
658
+ <option value="">Select…</option>
659
+ {itemTargetChildren.map((child) => (
660
+ <option key={child.id} value={child.id}>
661
+ {child.path ?? child.label}
662
+ </option>
663
+ ))}
664
+ </select>
665
+ </label>
666
+ </div>
667
+ ) : null}
637
668
  <ul className="workbench-field-remap-detail__item-edges">
638
669
  {(edge.itemEdges ?? []).map((itemEdge) => (
639
670
  <li key={itemEdge.id} data-testid={`field-remap-item-edge-${itemEdge.id}`}>
@@ -641,13 +672,15 @@ export function FieldRemapDetailPanel({
641
672
  {fieldLabel(itemEdge.sourceFieldId, sources, targets)} →{' '}
642
673
  {fieldLabel(itemEdge.targetSlotId, sources, targets)}
643
674
  </code>
644
- <Button
645
- compact
646
- type="button"
647
- onClick={() => applyEdge(removeItemEdgeFromParent(edge, itemEdge.id))}
648
- >
649
- Remove
650
- </Button>
675
+ {!readOnly ? (
676
+ <Button
677
+ compact
678
+ type="button"
679
+ onClick={() => applyEdge(removeItemEdgeFromParent(edge, itemEdge.id))}
680
+ >
681
+ Remove
682
+ </Button>
683
+ ) : null}
651
684
  </li>
652
685
  ))}
653
686
  </ul>
@@ -0,0 +1,48 @@
1
+ const FIELD_REMAP_TRANSFORM_DRAG_TYPE = 'application/x-workbench-field-remap-transform' as const;
2
+
3
+ type FieldRemapDragDataReader = Pick<DataTransfer, 'getData' | 'types'>;
4
+ type FieldRemapDragDataWriter = Pick<DataTransfer, 'setData'>;
5
+
6
+ export function hasFieldRemapTransformDragType(dataTransfer: Pick<DataTransfer, 'types'>): boolean {
7
+ return Array.from(dataTransfer.types).includes(FIELD_REMAP_TRANSFORM_DRAG_TYPE);
8
+ }
9
+
10
+ function isCanonicalTransformPayload(value: unknown): value is { readonly transformId: string } {
11
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
12
+ return false;
13
+ }
14
+ const keys = Object.keys(value);
15
+ return (
16
+ keys.length === 1 &&
17
+ keys[0] === 'transformId' &&
18
+ typeof (value as { readonly transformId?: unknown }).transformId === 'string' &&
19
+ (value as { readonly transformId: string }).transformId.length > 0
20
+ );
21
+ }
22
+
23
+ /** Internal same-component payload; intentionally not exported from the package surface. */
24
+ export function writeFieldRemapTransformDragData(
25
+ dataTransfer: FieldRemapDragDataWriter,
26
+ transformId: string,
27
+ ): void {
28
+ dataTransfer.setData(FIELD_REMAP_TRANSFORM_DRAG_TYPE, JSON.stringify({ transformId }));
29
+ }
30
+
31
+ /** Parse only the exact private payload shape before callers resolve the registry id. */
32
+ export function readFieldRemapTransformDragData(
33
+ dataTransfer: FieldRemapDragDataReader,
34
+ ): string | undefined {
35
+ if (!hasFieldRemapTransformDragType(dataTransfer)) {
36
+ return undefined;
37
+ }
38
+ const raw = dataTransfer.getData(FIELD_REMAP_TRANSFORM_DRAG_TYPE);
39
+ if (!raw) {
40
+ return undefined;
41
+ }
42
+ try {
43
+ const parsed: unknown = JSON.parse(raw);
44
+ return isCanonicalTransformPayload(parsed) ? parsed.transformId : undefined;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }