@kispace-io/extension-command-palette 0.8.0

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/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@kispace-io/extension-command-palette",
3
+ "version": "0.8.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./src/index.ts",
9
+ "types": "./src/index.ts"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@kispace-io/core": "*"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.9.3"
17
+ }
18
+ }
@@ -0,0 +1,530 @@
1
+ /**
2
+ * Command Palette Extension for geo!space
3
+ *
4
+ * Provides a VS Code-style command palette at the top center of the interface
5
+ * for quick command execution.
6
+ */
7
+
8
+ import { html } from "lit";
9
+ import { customElement, state } from "lit/decorators.js";
10
+ import { css } from "lit";
11
+ import { createRef, ref, Ref } from "lit/directives/ref.js";
12
+ import { KPart } from "@kispace-io/core";
13
+ import { TOOLBAR_MAIN_CENTER } from "@kispace-io/core";
14
+ import { subscribe } from "@kispace-io/core";
15
+ import { CommandRegistry, ExecutionContext, commandRegistry } from "@kispace-io/core";
16
+ import { SYSTEM_LANGUAGE_BUNDLES, i18n } from "@kispace-io/core";
17
+ import commandpaletteBundle from "./commandpalette.json";
18
+
19
+ const t = i18n('commandpalette');
20
+
21
+ // Event topic for opening the command palette
22
+ const TOPIC_OPEN_COMMAND_PALETTE = "commandpalette/open";
23
+
24
+ @customElement('k-command-palette')
25
+ export class KCommandPalette extends KPart {
26
+ @state()
27
+ private inputValue: string = '';
28
+
29
+ @state()
30
+ private filteredCommands: any[] = [];
31
+
32
+ @state()
33
+ private allCommands: any[] = [];
34
+
35
+ @state()
36
+ private showParameterPrompt: boolean = false;
37
+
38
+ @state()
39
+ private selectedCommand: any = null;
40
+
41
+ @state()
42
+ private parameterValues: { [key: string]: string } = {};
43
+
44
+ @state()
45
+ private isPaletteOpen: boolean = false;
46
+
47
+ private inputRef: Ref<any> = createRef();
48
+ private dialogRef: Ref<any> = createRef();
49
+ private boundDocumentClickHandler?: (e: MouseEvent) => void;
50
+ private executionContext: ExecutionContext | undefined;
51
+
52
+ protected async doInitUI() {
53
+ // Subscribe to open command palette event
54
+ subscribe(TOPIC_OPEN_COMMAND_PALETTE, () => {
55
+ this.openPalette();
56
+ });
57
+
58
+ // Add click-outside listener to close palette
59
+ this.boundDocumentClickHandler = this.handleDocumentClick.bind(this);
60
+ document.addEventListener('click', this.boundDocumentClickHandler);
61
+ }
62
+
63
+ private async handleDocumentClick(e: MouseEvent) {
64
+ if (!this.isPaletteOpen && !this.showParameterPrompt) return;
65
+
66
+ await this.updateComplete;
67
+ const target = e.target as Node;
68
+
69
+ // Check if click is inside the component
70
+ if (this.contains(target)) return;
71
+
72
+ // Check if click is inside the dialog element
73
+ if (this.dialogRef.value) {
74
+ const dialog = this.dialogRef.value as HTMLElement;
75
+ if (dialog.contains(target)) return;
76
+ }
77
+
78
+ // Check if the target is inside any wa-dialog (in case of shadow DOM)
79
+ let element = target as HTMLElement;
80
+ while (element) {
81
+ if (element.tagName === 'WA-DIALOG') return;
82
+ element = element.parentElement as HTMLElement;
83
+ }
84
+
85
+ // Click is outside, close everything
86
+ this.closePalette();
87
+ this.closeParameterPrompt();
88
+ }
89
+
90
+ private handleDialogClick(e: Event) {
91
+ e.stopPropagation();
92
+ }
93
+
94
+ private handleInputFocus() {
95
+ // Use requestAnimationFrame to prevent interrupting the focus
96
+ requestAnimationFrame(() => {
97
+ this.isPaletteOpen = true;
98
+ });
99
+ }
100
+
101
+ private handleInputClick(e: Event) {
102
+ e.stopPropagation();
103
+ this.openPalette();
104
+ }
105
+
106
+ private handleInputMouseDown(e: Event) {
107
+ e.stopPropagation();
108
+ }
109
+
110
+ private updateCommandList() {
111
+ const commands = commandRegistry.listCommands(this.executionContext || {});
112
+ this.allCommands = Object.values(commands)
113
+ .filter((cmd: any) => cmd.id !== 'commandpalette.open')
114
+ .map((cmd: any) => ({
115
+ id: cmd.id,
116
+ name: cmd.name,
117
+ description: cmd.description,
118
+ icon: cmd.icon,
119
+ keyBinding: cmd.keyBinding
120
+ }));
121
+
122
+ this.filteredCommands = [...this.allCommands];
123
+ }
124
+
125
+ public async openPalette() {
126
+ this.executionContext = commandRegistry.createExecutionContext();
127
+ this.inputValue = '';
128
+ this.updateCommandList();
129
+ this.showParameterPrompt = false;
130
+ this.isPaletteOpen = true;
131
+
132
+ await this.updateComplete;
133
+
134
+ if (this.inputRef.value) {
135
+ this.inputRef.value.focus();
136
+ }
137
+ }
138
+
139
+ private closePalette() {
140
+ this.isPaletteOpen = false;
141
+ this.inputValue = '';
142
+ this.showParameterPrompt = false;
143
+ this.executionContext = undefined;
144
+ }
145
+
146
+ private handleInputChange(e: Event) {
147
+ const input = e.target as any;
148
+ this.inputValue = input.value;
149
+ this.filterCommands();
150
+ }
151
+
152
+ private filterCommands() {
153
+ if (!this.inputValue.trim()) {
154
+ this.filteredCommands = [...this.allCommands];
155
+ } else {
156
+ const searchLower = this.inputValue.toLowerCase();
157
+ this.filteredCommands = this.allCommands.filter(cmd =>
158
+ cmd.name.toLowerCase().includes(searchLower) ||
159
+ cmd.id.toLowerCase().includes(searchLower) ||
160
+ (cmd.description && cmd.description.toLowerCase().includes(searchLower))
161
+ );
162
+ }
163
+ }
164
+
165
+ private handleKeyDown(e: KeyboardEvent) {
166
+ if (e.key === 'Escape') {
167
+ e.preventDefault();
168
+ this.closePalette();
169
+ }
170
+ }
171
+
172
+ private handleCommandClick(e: Event, command: any) {
173
+ if (!command) return;
174
+ e.stopPropagation();
175
+ this.inputValue = '';
176
+ this.filterCommands();
177
+ this.runCommand(command);
178
+ }
179
+
180
+ private handleContainerClick(e: Event) {
181
+ e.stopPropagation();
182
+ }
183
+
184
+ private runCommand(command: any) {
185
+ if (!command || !commandRegistry) return;
186
+
187
+ // Get the full command details from registry
188
+ const fullCommand = commandRegistry.getCommand(command.id);
189
+
190
+ // Check if command has any parameters (required or optional)
191
+ const hasParameters = fullCommand?.parameters && fullCommand.parameters.length > 0;
192
+
193
+ if (hasParameters) {
194
+ // Show parameter prompt for any command with parameters
195
+ this.selectedCommand = fullCommand;
196
+ this.parameterValues = {};
197
+ this.showParameterPrompt = true;
198
+ } else {
199
+ // Execute directly without parameters
200
+ this.executeCommandWithParams(command.id, {});
201
+ }
202
+ }
203
+
204
+ private executeCommandWithParams(commandId: string, params: any) {
205
+ try {
206
+ commandRegistry.execute(commandId, { ...(this.executionContext || {}), params });
207
+ this.closePalette();
208
+ this.closeParameterPrompt();
209
+ } catch (error: any) {
210
+ console.error('Failed to execute command:', error);
211
+ }
212
+ }
213
+
214
+ private closeParameterPrompt() {
215
+ this.showParameterPrompt = false;
216
+ this.selectedCommand = null;
217
+ this.parameterValues = {};
218
+ }
219
+
220
+ private handleParameterInput(paramName: string, value: string) {
221
+ this.parameterValues = {
222
+ ...this.parameterValues,
223
+ [paramName]: value
224
+ };
225
+ }
226
+
227
+ private executeWithParameters() {
228
+ if (!this.selectedCommand) return;
229
+
230
+ // Validate required parameters
231
+ const missingParams = this.selectedCommand.parameters
232
+ ?.filter((p: any) => p.required && !this.parameterValues[p.name])
233
+ .map((p: any) => p.name);
234
+
235
+ if (missingParams && missingParams.length > 0) {
236
+ if ((this as any).toastError) {
237
+ (this as any).toastError(t('MISSING_REQUIRED_PARAMS', { params: missingParams.join(', ') }));
238
+ }
239
+ return;
240
+ }
241
+
242
+ this.executeCommandWithParams(this.selectedCommand.id, this.parameterValues);
243
+ }
244
+
245
+ protected doClose() {
246
+ if (this.boundDocumentClickHandler) {
247
+ document.removeEventListener('click', this.boundDocumentClickHandler);
248
+ }
249
+ }
250
+
251
+ static styles = css`
252
+ :host {
253
+ display: block;
254
+ width: 100%;
255
+ max-width: 600px;
256
+ position: relative;
257
+ }
258
+
259
+ wa-input {
260
+ max-width: 300px;
261
+ }
262
+
263
+ .commands-container {
264
+ position: absolute;
265
+ top: 100%;
266
+ left: 0;
267
+ right: 0;
268
+ margin-top: 4px;
269
+ background: var(--wa-color-neutral-05);
270
+ border: 1px solid var(--wa-color-neutral-25);
271
+ border-radius: 4px;
272
+ max-height: 400px;
273
+ overflow-y: auto;
274
+ z-index: 1000;
275
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8);
276
+ display: none;
277
+ }
278
+
279
+ :host-context(.wa-light) .commands-container {
280
+ background: var(--wa-color-neutral-95);
281
+ border: 1px solid var(--wa-color-neutral-75);
282
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
283
+ }
284
+
285
+ .commands-container.open {
286
+ display: block;
287
+ }
288
+
289
+ .command-item {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 12px;
293
+ padding: 10px 16px;
294
+ cursor: pointer;
295
+ transition: background-color 0.15s;
296
+ border-bottom: 1px solid var(--wa-color-neutral-15);
297
+ }
298
+
299
+ :host-context(.wa-light) .command-item {
300
+ border-bottom: 1px solid var(--wa-color-neutral-85);
301
+ }
302
+
303
+ .command-item:last-child {
304
+ border-bottom: none;
305
+ }
306
+
307
+ .command-item:hover {
308
+ background: var(--wa-color-neutral-20);
309
+ }
310
+
311
+ :host-context(.wa-light) .command-item:hover {
312
+ background: var(--wa-color-neutral-80);
313
+ }
314
+
315
+ .command-icon {
316
+ flex-shrink: 0;
317
+ width: 20px;
318
+ height: 20px;
319
+ display: flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ opacity: 0.7;
323
+ }
324
+
325
+ .command-info {
326
+ flex: 1;
327
+ min-width: 0;
328
+ }
329
+
330
+ .command-name {
331
+ font-size: 14px;
332
+ font-weight: 500;
333
+ margin-bottom: 2px;
334
+ }
335
+
336
+ .command-id {
337
+ font-size: 11px;
338
+ opacity: 0.5;
339
+ font-family: monospace;
340
+ margin-bottom: 2px;
341
+ }
342
+
343
+ .command-description {
344
+ font-size: 12px;
345
+ opacity: 0.7;
346
+ }
347
+
348
+ .command-keybinding {
349
+ flex-shrink: 0;
350
+ margin-left: auto;
351
+ padding: 2px 8px;
352
+ background: var(--wa-color-neutral-15);
353
+ border: 1px solid var(--wa-color-neutral-25);
354
+ border-radius: 3px;
355
+ font-size: 11px;
356
+ font-family: monospace;
357
+ opacity: 0.7;
358
+ }
359
+
360
+ :host-context(.wa-light) .command-keybinding {
361
+ background: var(--wa-color-neutral-85);
362
+ border: 1px solid var(--wa-color-neutral-75);
363
+ }
364
+
365
+ .no-results {
366
+ padding: 20px;
367
+ text-align: center;
368
+ color: var(--wa-color-neutral-60);
369
+ }
370
+
371
+ :host-context(.wa-light) .no-results {
372
+ color: var(--wa-color-neutral-40);
373
+ }
374
+
375
+ wa-dialog::part(panel) {
376
+ max-width: 600px;
377
+ width: 90vw;
378
+ }
379
+
380
+ wa-dialog::part(body) {
381
+ padding: 20px;
382
+ }
383
+
384
+ .parameter-prompt-title {
385
+ font-size: 16px;
386
+ font-weight: 600;
387
+ margin-bottom: 16px;
388
+ }
389
+
390
+ .parameter-field {
391
+ margin-bottom: 12px;
392
+ }
393
+
394
+ .parameter-field wa-input {
395
+ width: 100%;
396
+ }
397
+
398
+ .parameter-actions {
399
+ display: flex;
400
+ gap: 8px;
401
+ justify-content: flex-end;
402
+ margin-top: 20px;
403
+ }
404
+ `;
405
+
406
+ render() {
407
+ return html`
408
+ <wa-input
409
+ ${ref(this.inputRef)}
410
+ placeholder="${t('PLACEHOLDER')}"
411
+ .value=${this.inputValue}
412
+ @input=${this.handleInputChange}
413
+ @keydown=${this.handleKeyDown}
414
+ @focus=${this.handleInputFocus}
415
+ @click=${this.handleInputClick}
416
+ @mousedown=${this.handleInputMouseDown}
417
+ autocomplete="off"
418
+ size="small"
419
+ >
420
+ <wa-icon slot="start" name="terminal" label="Terminal"></wa-icon>
421
+ </wa-input>
422
+
423
+ <div class="commands-container ${this.isPaletteOpen ? 'open' : ''}" @click=${this.handleContainerClick}>
424
+ ${this.filteredCommands.length > 0 ? html`
425
+ ${this.filteredCommands.map(cmd => html`
426
+ <div class="command-item" @click=${(e: Event) => this.handleCommandClick(e, cmd)}>
427
+ ${cmd.icon ? html`
428
+ <div class="command-icon">
429
+ <wa-icon name="${cmd.icon}" label="${cmd.name}"></wa-icon>
430
+ </div>
431
+ ` : html`
432
+ <div class="command-icon">
433
+ <wa-icon name="terminal" label="Command"></wa-icon>
434
+ </div>
435
+ `}
436
+ <div class="command-info">
437
+ <div class="command-name">${cmd.name}</div>
438
+ <div class="command-id">${cmd.id}</div>
439
+ ${cmd.description ? html`
440
+ <div class="command-description">${cmd.description}</div>
441
+ ` : ''}
442
+ </div>
443
+ ${cmd.keyBinding ? html`
444
+ <div class="command-keybinding">${cmd.keyBinding}</div>
445
+ ` : ''}
446
+ </div>
447
+ `)}
448
+ ` : html`
449
+ <div class="no-results">
450
+ <wa-icon name="search" label="${t('NO_COMMANDS_FOUND')}" style="font-size: 24px; margin-bottom: 4px; opacity: 0.3;"></wa-icon>
451
+ <div>${t('NO_COMMANDS_FOUND')}</div>
452
+ </div>
453
+ `}
454
+ </div>
455
+
456
+ ${this.showParameterPrompt && this.selectedCommand ? html`
457
+ <wa-dialog
458
+ ${ref(this.dialogRef)}
459
+ label="${this.selectedCommand.name} - ${t('PARAMETERS')}"
460
+ open
461
+ @wa-request-close=${this.closeParameterPrompt}
462
+ @click=${this.handleDialogClick}
463
+ >
464
+ <div class="parameter-prompt-title">
465
+ ${t('ENTER_PARAMETERS', { commandName: this.selectedCommand.name })}
466
+ </div>
467
+ ${this.selectedCommand.parameters?.map((param: any) => html`
468
+ <div class="parameter-field">
469
+ <wa-input
470
+ label="${param.name}${param.required ? ' *' : ''}"
471
+ hint=${param.description || ''}
472
+ placeholder=${param.description || t('ENTER_PARAM', { paramName: param.name })}
473
+ .value=${this.parameterValues[param.name] || ''}
474
+ @input=${(e: Event) => this.handleParameterInput(param.name, (e.target as any).value)}
475
+ ></wa-input>
476
+ </div>
477
+ `)}
478
+ <div class="parameter-actions">
479
+ <wa-button variant="default" @click=${this.closeParameterPrompt}>
480
+ ${t('CANCEL')}
481
+ </wa-button>
482
+ <wa-button variant="primary" @click=${this.executeWithParameters}>
483
+ ${t('EXECUTE')}
484
+ </wa-button>
485
+ </div>
486
+ </wa-dialog>
487
+ ` : ''}
488
+ `;
489
+ }
490
+ }
491
+
492
+ export default ({ contributionRegistry, commandRegistry, toastInfo, toastError, html, publish }: any) => {
493
+ // Register language bundle
494
+ contributionRegistry.registerContribution(SYSTEM_LANGUAGE_BUNDLES,
495
+ commandpaletteBundle as any);
496
+
497
+ // Register command to open palette using events
498
+ commandRegistry.registerHandler('commandpalette.open', {
499
+ execute: () => {
500
+ // Publish event to open the palette
501
+ publish(TOPIC_OPEN_COMMAND_PALETTE, null);
502
+ }
503
+ });
504
+
505
+ commandRegistry.registerCommand({
506
+ id: 'commandpalette.open',
507
+ name: t('OPEN_COMMAND_PALETTE'),
508
+ description: t('OPEN_COMMAND_PALETTE_DESC'),
509
+ icon: 'terminal',
510
+ keyBinding: 'CTRL+SHIFT+P'
511
+ });
512
+
513
+ // Create the command palette element once and reuse it
514
+ const commandPaletteElement = (() => {
515
+ const element = document.createElement('k-command-palette') as any;
516
+ element.commandRegistry = commandRegistry;
517
+ element.toastInfo = toastInfo;
518
+ element.toastError = toastError;
519
+ return element;
520
+ })();
521
+
522
+ // Register UI component at the top center
523
+ contributionRegistry.registerContribution(TOOLBAR_MAIN_CENTER, {
524
+ label: "Command Palette",
525
+ icon: "terminal",
526
+ html: () => html`${commandPaletteElement}`
527
+ });
528
+
529
+ }
530
+
@@ -0,0 +1,28 @@
1
+ {
2
+ "namespace": "commandpalette",
3
+ "en": {
4
+ "PLACEHOLDER": "Type a command name...",
5
+ "NO_COMMANDS_FOUND": "No commands found",
6
+ "PARAMETERS": "Parameters",
7
+ "ENTER_PARAMETERS": "Enter parameters for {commandName}",
8
+ "CANCEL": "Cancel",
9
+ "EXECUTE": "Execute",
10
+ "OPEN_COMMAND_PALETTE": "Open Command Palette",
11
+ "OPEN_COMMAND_PALETTE_DESC": "Opens the command palette to execute commands",
12
+ "MISSING_REQUIRED_PARAMS": "Missing required parameters: {params}",
13
+ "ENTER_PARAM": "Enter {paramName}"
14
+ },
15
+ "de": {
16
+ "PLACEHOLDER": "Befehlsname eingeben...",
17
+ "NO_COMMANDS_FOUND": "Keine Befehle gefunden",
18
+ "PARAMETERS": "Parameter",
19
+ "ENTER_PARAMETERS": "Parameter für {commandName} eingeben",
20
+ "CANCEL": "Abbrechen",
21
+ "EXECUTE": "Ausführen",
22
+ "OPEN_COMMAND_PALETTE": "Befehls-Palette öffnen",
23
+ "OPEN_COMMAND_PALETTE_DESC": "Öffnet die Befehls-Palette zum Ausführen von Befehlen",
24
+ "MISSING_REQUIRED_PARAMS": "Erforderliche Parameter fehlen: {params}",
25
+ "ENTER_PARAM": "{paramName} eingeben"
26
+ }
27
+ }
28
+
package/src/i18n.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "namespace": "extensions",
3
+ "en": {
4
+ "EXT_COMMANDPALETTE_NAME": "Command Palette",
5
+ "EXT_COMMANDPALETTE_DESC": "VS Code-style command palette at the top center for quick command execution (Ctrl+Shift+P)"
6
+ },
7
+ "de": {
8
+ "EXT_COMMANDPALETTE_NAME": "Befehls-Palette",
9
+ "EXT_COMMANDPALETTE_DESC": "VS Code-ähnliche Befehls-Palette oben in der Mitte für schnelle Befehlsausführung (Strg+Umschalt+P)"
10
+ }
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { extensionRegistry, i18nLazy, contributionRegistry, SYSTEM_LANGUAGE_BUNDLES } from '@kispace-io/core';
2
+ import bundle from './i18n.json';
3
+
4
+ contributionRegistry.registerContribution(SYSTEM_LANGUAGE_BUNDLES, bundle as any);
5
+
6
+ const t = i18nLazy('extensions');
7
+
8
+ extensionRegistry.registerExtension({
9
+ id: "system.commandpalette",
10
+ name: t('EXT_COMMANDPALETTE_NAME'),
11
+ description: t('EXT_COMMANDPALETTE_DESC'),
12
+ loader: () => import("./command-palette-extension"),
13
+ icon: "terminal",
14
+
15
+
16
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true
8
+ },
9
+ "include": [
10
+ "src/**/*"
11
+ ]
12
+ }