@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.
- package/README.md +81 -2
- package/package.json +14 -10
- package/src/editor/workspace-reconcile.tsx +12 -7
- package/src/extensions/extension-enablement-controller.ts +40 -9
- package/src/extensions/theme-selection-protection.ts +80 -55
- package/src/field-remap/chrome-labels.ts +117 -0
- package/src/field-remap/convert-note-editor.tsx +119 -104
- package/src/field-remap/convert-palette.tsx +6 -0
- package/src/field-remap/demo.tsx +8 -2
- package/src/field-remap/detail-panel.tsx +535 -434
- package/src/field-remap/document-io.tsx +299 -0
- package/src/field-remap/drag-payload.ts +48 -0
- package/src/field-remap/flow-adapter.ts +216 -88
- package/src/field-remap/flow-ops.ts +150 -0
- package/src/field-remap/flow.tsx +1227 -272
- package/src/field-remap/index.ts +2 -0
- package/src/field-remap/io-class-browse.tsx +34 -22
- package/src/field-remap/keyboard.ts +16 -0
- package/src/field-remap/modal-detail.tsx +34 -0
- package/src/field-remap/panel.tsx +110 -14
- package/src/field-remap/transform-options-editor.tsx +68 -48
- package/src/field-remap/view.css +107 -189
- package/src/index.ts +7 -0
- package/src/keybinding-management-settings.ts +4 -0
- package/src/management/keybinding-overrides-storage.ts +48 -23
- package/src/management/keybinding-settings-view.tsx +31 -0
- package/src/management/keybinding-settings.tsx +4 -12
- package/src/management/use-keybinding-management.ts +72 -22
- package/src/shell/appearance-catalog.ts +453 -0
- package/src/shell/appearance-controller.ts +331 -0
- package/src/shell/appearance-presentation.ts +269 -0
- package/src/shell/provider.tsx +157 -19
- package/src/shell/settings.tsx +282 -153
- package/src/shell/shell.tsx +88 -18
- package/src/workbench/appearance-storage.ts +2 -2
- package/src/workbench/command-host-controller.tsx +223 -0
- package/src/workbench/command-host.tsx +100 -150
- package/src/workbench/keybinding-bridge.ts +84 -38
- 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
|
+
}
|