@workbench-kit/shell-react 0.0.2-prototype.0.2.41 → 0.0.2-prototype.0.2.44

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 (39) hide show
  1. package/README.md +81 -2
  2. package/package.json +14 -10
  3. package/src/editor/workspace-reconcile.tsx +12 -7
  4. package/src/extensions/extension-enablement-controller.ts +40 -9
  5. package/src/extensions/theme-selection-protection.ts +80 -55
  6. package/src/field-remap/chrome-labels.ts +117 -0
  7. package/src/field-remap/convert-note-editor.tsx +119 -104
  8. package/src/field-remap/convert-palette.tsx +6 -0
  9. package/src/field-remap/demo.tsx +8 -2
  10. package/src/field-remap/detail-panel.tsx +535 -434
  11. package/src/field-remap/document-io.tsx +299 -0
  12. package/src/field-remap/drag-payload.ts +48 -0
  13. package/src/field-remap/flow-adapter.ts +216 -88
  14. package/src/field-remap/flow-ops.ts +150 -0
  15. package/src/field-remap/flow.tsx +1227 -272
  16. package/src/field-remap/index.ts +2 -0
  17. package/src/field-remap/io-class-browse.tsx +34 -22
  18. package/src/field-remap/keyboard.ts +16 -0
  19. package/src/field-remap/modal-detail.tsx +34 -0
  20. package/src/field-remap/panel.tsx +110 -14
  21. package/src/field-remap/transform-options-editor.tsx +68 -48
  22. package/src/field-remap/view.css +107 -189
  23. package/src/index.ts +7 -0
  24. package/src/keybinding-management-settings.ts +4 -0
  25. package/src/management/keybinding-overrides-storage.ts +48 -23
  26. package/src/management/keybinding-settings-view.tsx +31 -0
  27. package/src/management/keybinding-settings.tsx +4 -12
  28. package/src/management/use-keybinding-management.ts +72 -22
  29. package/src/shell/appearance-catalog.ts +453 -0
  30. package/src/shell/appearance-controller.ts +331 -0
  31. package/src/shell/appearance-presentation.ts +269 -0
  32. package/src/shell/provider.tsx +157 -19
  33. package/src/shell/settings.tsx +282 -153
  34. package/src/shell/shell.tsx +88 -18
  35. package/src/workbench/appearance-storage.ts +2 -2
  36. package/src/workbench/command-host-controller.tsx +223 -0
  37. package/src/workbench/command-host.tsx +100 -150
  38. package/src/workbench/keybinding-bridge.ts +84 -38
  39. package/src/workbench/shell-command-registration.ts +27 -2
@@ -0,0 +1,299 @@
1
+ import { useEffect, useId, useMemo, useRef, useState, type FormEvent, type JSX } from 'react';
2
+ import { Modal } from '@workbench-kit/react/modal';
3
+ import { Button, TextArea } from '@workbench-kit/react/primitives';
4
+ import { WorkbenchModalPortal } from '@workbench-kit/react/workbench/modal-portal';
5
+ import type { FieldRemapImportFailureCode } from '@workbench-kit/field-remap';
6
+
7
+ import {
8
+ resolveFieldRemapChromeLabels,
9
+ type FieldRemapChromeLabels,
10
+ type FieldRemapTranslate,
11
+ } from './chrome-labels.js';
12
+
13
+ export type FieldRemapDocumentImportActionResult =
14
+ | { readonly status: 'accepted' }
15
+ | {
16
+ readonly status: 'rejected';
17
+ readonly code: FieldRemapImportFailureCode;
18
+ };
19
+
20
+ interface FieldRemapDocumentIoProps {
21
+ readonly getDocumentJson: () => string;
22
+ readonly importAvailable: boolean;
23
+ readonly labels?: Partial<FieldRemapChromeLabels> | undefined;
24
+ readonly t?: FieldRemapTranslate | undefined;
25
+ readonly onImportText: (text: string) => FieldRemapDocumentImportActionResult;
26
+ }
27
+
28
+ type DocumentIoStatus =
29
+ | { readonly kind: 'success'; readonly message: string }
30
+ | { readonly kind: 'error'; readonly message: string };
31
+
32
+ function importFailureMessage(
33
+ code: FieldRemapImportFailureCode,
34
+ labels: FieldRemapChromeLabels,
35
+ ): string {
36
+ switch (code) {
37
+ case 'invalid-json':
38
+ return labels.documentImportInvalidJson ?? '';
39
+ case 'unsupported-version':
40
+ return labels.documentImportUnsupportedVersion ?? '';
41
+ case 'duplicate-id':
42
+ return labels.documentImportDuplicateId ?? '';
43
+ case 'incompatible-source':
44
+ return labels.documentImportIncompatibleSource ?? '';
45
+ case 'incompatible-target':
46
+ return labels.documentImportIncompatibleTarget ?? '';
47
+ case 'unavailable-transform':
48
+ return labels.documentImportUnavailableTransform ?? '';
49
+ case 'invalid-document':
50
+ return labels.documentImportInvalidDocument ?? '';
51
+ }
52
+ }
53
+
54
+ export function FieldRemapDocumentIo({
55
+ getDocumentJson,
56
+ importAvailable,
57
+ labels: labelOverrides,
58
+ t,
59
+ onImportText,
60
+ }: FieldRemapDocumentIoProps): JSX.Element {
61
+ const labels = useMemo(
62
+ () => resolveFieldRemapChromeLabels(labelOverrides, t),
63
+ [labelOverrides, t],
64
+ );
65
+ const [exportOpen, setExportOpen] = useState(false);
66
+ const [exportText, setExportText] = useState('');
67
+ const [importOpen, setImportOpen] = useState(false);
68
+ const [draft, setDraft] = useState('');
69
+ const [importError, setImportError] = useState<string | null>(null);
70
+ const [status, setStatus] = useState<DocumentIoStatus | null>(null);
71
+ const instanceId = useId();
72
+ const exportTextId = `field-remap-document-export-text-${instanceId}`;
73
+ const importTextId = `field-remap-document-import-text-${instanceId}`;
74
+ const importErrorId = `field-remap-document-import-error-${instanceId}`;
75
+ const exportTextareaRef = useRef<HTMLTextAreaElement>(null);
76
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
77
+
78
+ useEffect(() => {
79
+ if (!importAvailable && importOpen) {
80
+ setImportOpen(false);
81
+ setDraft('');
82
+ setImportError(null);
83
+ }
84
+ }, [importAvailable, importOpen]);
85
+
86
+ const closeImport = () => {
87
+ setImportOpen(false);
88
+ setDraft('');
89
+ setImportError(null);
90
+ };
91
+
92
+ const closeExport = () => {
93
+ setExportOpen(false);
94
+ setExportText('');
95
+ setStatus(null);
96
+ };
97
+
98
+ const openExport = () => {
99
+ try {
100
+ setExportText(getDocumentJson());
101
+ setStatus(null);
102
+ setExportOpen(true);
103
+ } catch {
104
+ setStatus({ kind: 'error', message: labels.documentCopyFailed ?? '' });
105
+ }
106
+ };
107
+
108
+ const copyDocument = async () => {
109
+ try {
110
+ const clipboard = globalThis.navigator?.clipboard;
111
+ if (!clipboard || typeof clipboard.writeText !== 'function') {
112
+ throw new Error('Clipboard write is unavailable.');
113
+ }
114
+ await clipboard.writeText(exportText);
115
+ setStatus({ kind: 'success', message: labels.documentCopied ?? '' });
116
+ } catch {
117
+ setStatus({ kind: 'error', message: labels.documentCopyFailed ?? '' });
118
+ }
119
+ };
120
+
121
+ const openImport = () => {
122
+ if (!importAvailable) {
123
+ return;
124
+ }
125
+ setDraft('');
126
+ setImportError(null);
127
+ setStatus(null);
128
+ setImportOpen(true);
129
+ };
130
+
131
+ const applyImport = (event: FormEvent<HTMLFormElement>) => {
132
+ event.preventDefault();
133
+ const result = onImportText(draft);
134
+ if (result.status === 'rejected') {
135
+ setImportError(importFailureMessage(result.code, labels));
136
+ queueMicrotask(() => textareaRef.current?.focus());
137
+ return;
138
+ }
139
+ closeImport();
140
+ };
141
+
142
+ return (
143
+ <div className="workbench-field-remap-document-io" data-testid="field-remap-document-io">
144
+ <div className="workbench-field-remap-document-io__actions">
145
+ <Button
146
+ compact
147
+ type="button"
148
+ data-testid="field-remap-export-document"
149
+ onClick={openExport}
150
+ >
151
+ {labels.exportDocumentJson}
152
+ </Button>
153
+ <Button
154
+ compact
155
+ type="button"
156
+ data-testid="field-remap-import-document"
157
+ disabled={!importAvailable}
158
+ onClick={openImport}
159
+ >
160
+ {labels.importDocumentJson}
161
+ </Button>
162
+ </div>
163
+ {!importAvailable ? (
164
+ <span className="workbench-field-remap-document-io__availability">
165
+ {labels.documentImportUnavailable}
166
+ </span>
167
+ ) : null}
168
+ {status && !exportOpen ? (
169
+ <span
170
+ className="workbench-field-remap-document-io__status"
171
+ data-status={status.kind}
172
+ role={status.kind === 'error' ? 'alert' : 'status'}
173
+ >
174
+ {status.message}
175
+ </span>
176
+ ) : null}
177
+
178
+ {exportOpen ? (
179
+ <WorkbenchModalPortal>
180
+ <Modal
181
+ bodyClassName="workbench-field-remap-document-export__body"
182
+ bodyLayout="stack"
183
+ bodyPadding="lg"
184
+ bodyScroll="auto"
185
+ className="workbench-field-remap-document-export"
186
+ closeLabel={labels.closeDocumentExport}
187
+ footer={
188
+ <>
189
+ <Button
190
+ type="button"
191
+ data-testid="field-remap-copy-document"
192
+ onClick={() => void copyDocument()}
193
+ >
194
+ {labels.copyDocumentJson}
195
+ </Button>
196
+ <Button type="button" onClick={closeExport}>
197
+ {labels.closeDocumentExport}
198
+ </Button>
199
+ </>
200
+ }
201
+ initialFocusRef={exportTextareaRef}
202
+ title={labels.exportDocumentTitle}
203
+ onClose={closeExport}
204
+ >
205
+ <p className="workbench-field-remap-document-export__description">
206
+ {labels.exportDocumentDescription}
207
+ </p>
208
+ <label className="workbench-field-remap-document-export__label" htmlFor={exportTextId}>
209
+ {labels.exportDocumentLabel}
210
+ </label>
211
+ <TextArea
212
+ ref={exportTextareaRef}
213
+ id={exportTextId}
214
+ controlWidth="full"
215
+ data-testid="field-remap-document-export-text"
216
+ monospace
217
+ readOnly
218
+ rows={12}
219
+ value={exportText}
220
+ onFocus={(event) => event.currentTarget.select()}
221
+ />
222
+ {status ? (
223
+ <p
224
+ className="workbench-field-remap-document-io__status"
225
+ data-status={status.kind}
226
+ role={status.kind === 'error' ? 'alert' : 'status'}
227
+ >
228
+ {status.message}
229
+ </p>
230
+ ) : null}
231
+ </Modal>
232
+ </WorkbenchModalPortal>
233
+ ) : null}
234
+
235
+ {importOpen ? (
236
+ <WorkbenchModalPortal>
237
+ <Modal
238
+ bodyClassName="workbench-field-remap-document-import__body"
239
+ bodyLayout="stack"
240
+ bodyPadding="lg"
241
+ bodyScroll="auto"
242
+ className="workbench-field-remap-document-import"
243
+ closeLabel={labels.closeDocumentImport}
244
+ footer={
245
+ <>
246
+ <Button type="button" onClick={closeImport}>
247
+ {labels.cancelDocumentImport}
248
+ </Button>
249
+ <Button
250
+ type="submit"
251
+ variant="primary"
252
+ data-testid="field-remap-apply-document-import"
253
+ >
254
+ {labels.applyDocumentImport}
255
+ </Button>
256
+ </>
257
+ }
258
+ initialFocusRef={textareaRef}
259
+ title={labels.importDocumentTitle}
260
+ onClose={closeImport}
261
+ onSubmit={applyImport}
262
+ >
263
+ <p className="workbench-field-remap-document-import__description">
264
+ {labels.importDocumentDescription}
265
+ </p>
266
+ <label className="workbench-field-remap-document-import__label" htmlFor={importTextId}>
267
+ {labels.importDocumentLabel}
268
+ </label>
269
+ <TextArea
270
+ ref={textareaRef}
271
+ id={importTextId}
272
+ aria-describedby={importError ? importErrorId : undefined}
273
+ aria-invalid={importError ? true : undefined}
274
+ controlWidth="full"
275
+ data-testid="field-remap-document-import-text"
276
+ monospace
277
+ placeholder={labels.importDocumentPlaceholder}
278
+ rows={12}
279
+ value={draft}
280
+ onChange={(event) => {
281
+ setDraft(event.currentTarget.value);
282
+ setImportError(null);
283
+ }}
284
+ />
285
+ {importError ? (
286
+ <p
287
+ id={importErrorId}
288
+ className="workbench-field-remap-document-import__error"
289
+ role="alert"
290
+ >
291
+ {importError}
292
+ </p>
293
+ ) : null}
294
+ </Modal>
295
+ </WorkbenchModalPortal>
296
+ ) : null}
297
+ </div>
298
+ );
299
+ }
@@ -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
+ }