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

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 (52) hide show
  1. package/README.md +32 -0
  2. package/package.json +9 -9
  3. package/src/commands/use-command-descriptors.ts +7 -3
  4. package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
  5. package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
  6. package/src/devtools/workbench-devtools-snapshot.ts +32 -13
  7. package/src/editor/area.tsx +3 -2
  8. package/src/editor/state-storage.ts +31 -0
  9. package/src/editor/tab-context-menu.ts +8 -8
  10. package/src/editor/use-editor.ts +6 -6
  11. package/src/explorer/context-menu.ts +8 -4
  12. package/src/explorer/view.tsx +4 -3
  13. package/src/extensions/canonical-extension-descriptions.ts +58 -0
  14. package/src/extensions/context-menu.ts +9 -6
  15. package/src/extensions/extension-enablement-context.ts +15 -0
  16. package/src/extensions/extension-enablement-controller.ts +453 -0
  17. package/src/extensions/management-model.ts +111 -47
  18. package/src/extensions/theme-selection-protection.ts +98 -0
  19. package/src/extensions/uninstall-eligibility.ts +103 -0
  20. package/src/extensions/use-extension-management.ts +151 -67
  21. package/src/extensions/view.tsx +4 -0
  22. package/src/field-remap/chrome-labels.ts +87 -2
  23. package/src/field-remap/convert-palette.tsx +164 -18
  24. package/src/field-remap/demo.tsx +48 -3
  25. package/src/field-remap/detail-panel.tsx +6 -5
  26. package/src/field-remap/flow.tsx +341 -155
  27. package/src/field-remap/history.ts +99 -0
  28. package/src/field-remap/index.ts +9 -1
  29. package/src/field-remap/panel.tsx +388 -97
  30. package/src/field-remap/preview-controller.ts +122 -0
  31. package/src/field-remap/preview.tsx +171 -0
  32. package/src/field-remap/view.css +154 -2
  33. package/src/index.ts +16 -1
  34. package/src/management/keybinding-overrides-storage.ts +28 -0
  35. package/src/management/preference-settings-storage.ts +28 -0
  36. package/src/management/settings.tsx +2 -0
  37. package/src/management/use-command-management.ts +31 -26
  38. package/src/management/use-keybinding-management.ts +25 -12
  39. package/src/shell/focused-extension-services.ts +138 -0
  40. package/src/shell/host-shell.tsx +13 -10
  41. package/src/shell/persistence-diagnostic-context.ts +11 -0
  42. package/src/shell/provider.tsx +373 -69
  43. package/src/shell/settings.tsx +25 -16
  44. package/src/shell/shell.tsx +119 -78
  45. package/src/shell/view-host.tsx +23 -22
  46. package/src/storage/local-json-storage.ts +43 -29
  47. package/src/storage/persistence-diagnostics.ts +72 -0
  48. package/src/workbench/appearance-storage.ts +28 -0
  49. package/src/workbench/command-host.tsx +19 -22
  50. package/src/workbench/command-palette.ts +14 -13
  51. package/src/workbench/layout-storage.ts +52 -5
  52. package/src/workbench/use-persisted-appearance.ts +38 -12
@@ -1,6 +1,6 @@
1
- import type { JSX } from 'react';
1
+ import { useRef, useState, type JSX, type KeyboardEvent } from 'react';
2
2
  import { SideBarList, SideBarListItem } from '@workbench-kit/react/layout';
3
- import { Button } from '@workbench-kit/react/primitives';
3
+ import { ClearableTextInput, 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';
@@ -32,6 +32,120 @@ export function FieldRemapConvertPalette({
32
32
  // The registry supports replacing definitions through `register`; resolve
33
33
  // display metadata on each host render even when the registry object is stable.
34
34
  const catalog = transforms.list().filter((definition) => definition.id !== 'identity');
35
+ const [filterQuery, setFilterQuery] = useState('');
36
+ const [requestedRovingTransformId, setRequestedRovingTransformId] = useState(() =>
37
+ catalog.some((definition) => definition.id === selectedTransformId)
38
+ ? selectedTransformId
39
+ : (catalog[0]?.id ?? ''),
40
+ );
41
+ const optionRefs = useRef(new Map<string, HTMLButtonElement>());
42
+ const normalizedFilterQuery = filterQuery.trim().toLowerCase();
43
+ const visibleCatalog = catalog.filter((definition) => {
44
+ if (!normalizedFilterQuery) {
45
+ return true;
46
+ }
47
+ return (
48
+ definition.id.toLowerCase().includes(normalizedFilterQuery) ||
49
+ definition.label.toLowerCase().includes(normalizedFilterQuery)
50
+ );
51
+ });
52
+ const visibleTransformIds = visibleCatalog.map((definition) => definition.id);
53
+ const selectedTransformIsVisible = visibleTransformIds.includes(selectedTransformId);
54
+ const rovingTransformId = visibleTransformIds.includes(requestedRovingTransformId)
55
+ ? requestedRovingTransformId
56
+ : selectedTransformIsVisible
57
+ ? selectedTransformId
58
+ : (visibleTransformIds[0] ?? '');
59
+ const filterLabel =
60
+ chromeLabels.convertFilterLabel ?? defaultFieldRemapChromeLabels.convertFilterLabel;
61
+ const filterPlaceholder =
62
+ chromeLabels.convertFilterPlaceholder ?? defaultFieldRemapChromeLabels.convertFilterPlaceholder;
63
+ const clearFilterLabel =
64
+ chromeLabels.clearConvertFilter ?? defaultFieldRemapChromeLabels.clearConvertFilter;
65
+ const noMatchingConverts =
66
+ chromeLabels.noMatchingConverts ?? defaultFieldRemapChromeLabels.noMatchingConverts;
67
+
68
+ const updateFilterQuery = (nextQuery: string) => {
69
+ const normalizedNextQuery = nextQuery.trim().toLowerCase();
70
+ const nextVisibleIds = catalog
71
+ .filter(
72
+ (definition) =>
73
+ !normalizedNextQuery ||
74
+ definition.id.toLowerCase().includes(normalizedNextQuery) ||
75
+ definition.label.toLowerCase().includes(normalizedNextQuery),
76
+ )
77
+ .map((definition) => definition.id);
78
+ setFilterQuery(nextQuery);
79
+ setRequestedRovingTransformId(
80
+ nextVisibleIds.includes(selectedTransformId)
81
+ ? selectedTransformId
82
+ : (nextVisibleIds[0] ?? ''),
83
+ );
84
+ };
85
+
86
+ const focusVisibleTransform = (transformId: string) => {
87
+ setRequestedRovingTransformId(transformId);
88
+ optionRefs.current.get(transformId)?.focus();
89
+ };
90
+
91
+ const handleFilterKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
92
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') {
93
+ return;
94
+ }
95
+ const targetId = selectedTransformIsVisible
96
+ ? selectedTransformId
97
+ : event.key === 'ArrowDown'
98
+ ? visibleTransformIds[0]
99
+ : visibleTransformIds[visibleTransformIds.length - 1];
100
+ if (!targetId) {
101
+ return;
102
+ }
103
+ event.preventDefault();
104
+ focusVisibleTransform(targetId);
105
+ };
106
+
107
+ const handleOptionKeyDown = (event: KeyboardEvent<HTMLButtonElement>, transformId: string) => {
108
+ if (event.key === 'Enter') {
109
+ event.preventDefault();
110
+ if (transformId !== selectedTransformId) {
111
+ onSelectedTransformIdChange(transformId);
112
+ }
113
+ onPlaceDraft(transformId);
114
+ return;
115
+ }
116
+
117
+ const currentIndex = visibleTransformIds.indexOf(transformId);
118
+ if (currentIndex < 0) {
119
+ return;
120
+ }
121
+ let nextIndex: number | undefined;
122
+ switch (event.key) {
123
+ case 'ArrowDown':
124
+ nextIndex = Math.min(currentIndex + 1, visibleTransformIds.length - 1);
125
+ break;
126
+ case 'ArrowUp':
127
+ nextIndex = Math.max(currentIndex - 1, 0);
128
+ break;
129
+ case 'Home':
130
+ nextIndex = 0;
131
+ break;
132
+ case 'End':
133
+ nextIndex = visibleTransformIds.length - 1;
134
+ break;
135
+ default:
136
+ return;
137
+ }
138
+
139
+ const nextTransformId = visibleTransformIds[nextIndex];
140
+ if (!nextTransformId) {
141
+ return;
142
+ }
143
+ event.preventDefault();
144
+ focusVisibleTransform(nextTransformId);
145
+ if (nextTransformId !== selectedTransformId) {
146
+ onSelectedTransformIdChange(nextTransformId);
147
+ }
148
+ };
35
149
 
36
150
  return (
37
151
  <aside
@@ -45,39 +159,66 @@ export function FieldRemapConvertPalette({
45
159
  </header>
46
160
 
47
161
  <div className="workbench-field-remap-convert-palette__place">
48
- <Button
162
+ <IconButton
163
+ compact
49
164
  type="button"
50
165
  data-testid="field-remap-place-draft"
51
- disabled={!selectedTransformId}
166
+ disabled={!selectedTransformId || !selectedTransformIsVisible}
167
+ icon="codicon-add"
168
+ label={chromeLabels.placeConvert}
52
169
  onClick={() => {
53
- if (!selectedTransformId) {
170
+ if (!selectedTransformId || !selectedTransformIsVisible) {
54
171
  return;
55
172
  }
56
173
  onPlaceDraft(selectedTransformId);
57
174
  }}
58
- >
59
- {chromeLabels.placeConvert}
60
- </Button>
175
+ />
61
176
  </div>
62
177
 
178
+ <ClearableTextInput
179
+ aria-label={filterLabel}
180
+ className="workbench-field-remap-convert-palette__filter"
181
+ clearLabel={clearFilterLabel}
182
+ controlWidth="full"
183
+ placeholder={filterPlaceholder}
184
+ type="search"
185
+ value={filterQuery}
186
+ onClear={() => updateFilterQuery('')}
187
+ onKeyDown={handleFilterKeyDown}
188
+ onValueChange={updateFilterQuery}
189
+ />
190
+
63
191
  <SideBarList
64
192
  className="workbench-field-remap-convert-palette__list"
65
193
  role="listbox"
66
194
  aria-label={chromeLabels.convertsListAriaLabel}
67
195
  >
68
- {catalog.map((definition) => {
196
+ {visibleCatalog.map((definition) => {
69
197
  const selected = definition.id === selectedTransformId;
70
198
  return (
71
199
  <SideBarListItem
200
+ ref={(element) => {
201
+ if (element) {
202
+ optionRefs.current.set(definition.id, element);
203
+ } else {
204
+ optionRefs.current.delete(definition.id);
205
+ }
206
+ }}
72
207
  key={definition.id}
73
208
  aria-selected={selected}
74
209
  data-testid={`field-remap-palette-item-${definition.id}`}
75
210
  role="option"
76
211
  selected={selected}
212
+ tabIndex={definition.id === rovingTransformId ? 0 : -1}
77
213
  variant="stacked"
78
214
  wrapperProps={{ role: 'presentation' }}
79
- onClick={() => onSelectedTransformIdChange(definition.id)}
215
+ onClick={() => {
216
+ setRequestedRovingTransformId(definition.id);
217
+ onSelectedTransformIdChange(definition.id);
218
+ }}
80
219
  onDoubleClick={() => onPlaceDraft(definition.id)}
220
+ onFocus={() => setRequestedRovingTransformId(definition.id)}
221
+ onKeyDown={(event) => handleOptionKeyDown(event, definition.id)}
81
222
  >
82
223
  <strong>{definition.label}</strong>
83
224
  <code title={definition.id}>{definition.id}</code>
@@ -85,6 +226,11 @@ export function FieldRemapConvertPalette({
85
226
  );
86
227
  })}
87
228
  </SideBarList>
229
+ {visibleCatalog.length === 0 ? (
230
+ <p className="workbench-field-remap-convert-palette__empty" role="status">
231
+ {noMatchingConverts}
232
+ </p>
233
+ ) : null}
88
234
 
89
235
  {onAddCombine || onAddSplit ? (
90
236
  <div
@@ -95,24 +241,24 @@ export function FieldRemapConvertPalette({
95
241
  <p>{chromeLabels.operatorsDescription}</p>
96
242
  <div className="workbench-field-remap-convert-palette__operator-actions">
97
243
  {onAddCombine ? (
98
- <Button
244
+ <IconButton
99
245
  compact
100
246
  type="button"
101
247
  data-testid="field-remap-add-combine"
248
+ icon="codicon-git-merge"
249
+ label={chromeLabels.addCombine}
102
250
  onClick={onAddCombine}
103
- >
104
- Add combine
105
- </Button>
251
+ />
106
252
  ) : null}
107
253
  {onAddSplit ? (
108
- <Button
254
+ <IconButton
109
255
  compact
110
256
  type="button"
111
257
  data-testid="field-remap-add-split"
258
+ icon="codicon-split-horizontal"
259
+ label={chromeLabels.addSplit}
112
260
  onClick={onAddSplit}
113
- >
114
- Add split
115
- </Button>
261
+ />
116
262
  ) : null}
117
263
  </div>
118
264
  </div>
@@ -2,9 +2,13 @@ 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
- import { FieldRemapPanel } from './panel.js';
7
+ import {
8
+ FieldRemapPanel,
9
+ type FieldRemapHistoryActions,
10
+ type FieldRemapHistoryAvailability,
11
+ } from './panel.js';
8
12
  import {
9
13
  getFieldRemapBrowseDemoShapes,
10
14
  getFieldRemapSample,
@@ -15,7 +19,13 @@ export interface SampleFieldRemapDemoProps {
15
19
  readonly sampleId?: FieldRemapSampleId | string | undefined;
16
20
  /** Forwarded to Flow mapper (default true). */
17
21
  readonly showMinimap?: boolean | undefined;
18
- /** When true, show host-chrome demo controls (minimap toggle + fit view). */
22
+ /** Forwarded Flow chrome preset. */
23
+ readonly chrome?: FieldRemapFlowMapperProps['chrome'];
24
+ readonly showFlowHint?: FieldRemapFlowMapperProps['showFlowHint'];
25
+ readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
26
+ readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
27
+ readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
28
+ /** When true, show host-chrome demo controls (history + fit view). */
19
29
  readonly showHostChromeDemo?: boolean | undefined;
20
30
  /** Prefer `browse` for I/O class/field inspection demos. */
21
31
  readonly ioChrome?: FieldRemapIoChrome | undefined;
@@ -31,6 +41,11 @@ export interface SampleFieldRemapDemoProps {
31
41
  export function SampleFieldRemapDemo({
32
42
  sampleId = 'nested-ab',
33
43
  showMinimap: showMinimapProp,
44
+ chrome,
45
+ showFlowHint,
46
+ showBindingsList,
47
+ showConvertPalette,
48
+ emptyDetail,
34
49
  showHostChromeDemo = false,
35
50
  ioChrome,
36
51
  browseSeedShapes = false,
@@ -40,7 +55,12 @@ export function SampleFieldRemapDemo({
40
55
  const sample = getFieldRemapSample(sampleId);
41
56
  const browseShapes = browseSeedShapes ? getFieldRemapBrowseDemoShapes() : null;
42
57
  const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
58
+ const historyActionsRef = useRef<FieldRemapHistoryActions | null>(null);
43
59
  const [showMinimap, setShowMinimap] = useState(showMinimapProp ?? true);
60
+ const [historyAvailability, setHistoryAvailability] = useState<FieldRemapHistoryAvailability>({
61
+ canUndo: false,
62
+ canRedo: false,
63
+ });
44
64
  const [lastPaneMenu, setLastPaneMenu] = useState<string | null>(null);
45
65
 
46
66
  useEffect(() => {
@@ -65,6 +85,24 @@ export function SampleFieldRemapDemo({
65
85
  >
66
86
  Fit view
67
87
  </Button>
88
+ <Button
89
+ compact
90
+ type="button"
91
+ data-testid="field-remap-undo"
92
+ disabled={!historyAvailability.canUndo}
93
+ onClick={() => historyActionsRef.current?.undo()}
94
+ >
95
+ Undo
96
+ </Button>
97
+ <Button
98
+ compact
99
+ type="button"
100
+ data-testid="field-remap-redo"
101
+ disabled={!historyAvailability.canRedo}
102
+ onClick={() => historyActionsRef.current?.redo()}
103
+ >
104
+ Redo
105
+ </Button>
68
106
  {lastPaneMenu ? (
69
107
  <span data-testid="field-remap-pane-menu-log" role="status">
70
108
  Pane menu: {lastPaneMenu}
@@ -76,6 +114,11 @@ export function SampleFieldRemapDemo({
76
114
  key={`${sample.id}:${ioChrome ?? 'default'}:${browseSeedShapes ? 'seed' : 'plain'}`}
77
115
  sample={sample}
78
116
  showMinimap={showMinimap}
117
+ chrome={chrome}
118
+ showFlowHint={showFlowHint}
119
+ showBindingsList={showBindingsList}
120
+ showConvertPalette={showConvertPalette}
121
+ emptyDetail={emptyDetail}
79
122
  onShowMinimapChange={setShowMinimap}
80
123
  ioChrome={ioChrome}
81
124
  editableShapes={ioChrome === 'browse' ? false : undefined}
@@ -84,6 +127,8 @@ export function SampleFieldRemapDemo({
84
127
  labels={labels}
85
128
  t={t}
86
129
  flowActionsRef={showHostChromeDemo ? flowActionsRef : undefined}
130
+ historyActionsRef={showHostChromeDemo ? historyActionsRef : undefined}
131
+ onHistoryAvailabilityChange={showHostChromeDemo ? setHistoryAvailability : undefined}
87
132
  onPaneContextMenu={
88
133
  showHostChromeDemo
89
134
  ? (event) => {
@@ -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
  }