@ds-mo/ui 2.3.0 → 2.4.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.
@@ -11,6 +11,8 @@ export declare class AppShell {
11
11
  * When set, overrides the built-in radial wash.
12
12
  */
13
13
  gradientSrc: string;
14
+ /** When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer. */
15
+ shortcutsEnabled: boolean;
14
16
  el: HTMLElement;
15
17
  private resizeObserver;
16
18
  private readonly panelNavTransition;
@@ -40,6 +42,7 @@ export declare class AppShell {
40
42
  /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */
41
43
  private resolveViewportDimensions;
42
44
  private chromeLayerActive;
45
+ handleWindowKeyDown(e: KeyboardEvent): void;
43
46
  private syncChrome;
44
47
  render(): any;
45
48
  }
@@ -90,7 +90,6 @@ export declare class PanelNav {
90
90
  /** Centralised toggle: updates `collapsed`, emits `dsNavToggle`, optionally persists. */
91
91
  private applyToggle;
92
92
  private getAllItems;
93
- private getRovingTotal;
94
93
  private getFooterRovingIndex;
95
94
  private getUserRovingIndex;
96
95
  private getRovingElement;
@@ -101,6 +100,8 @@ export declare class PanelNav {
101
100
  private handleBodyScroll;
102
101
  private handleItemClick;
103
102
  private handleToggle;
103
+ /** Toggle expanded/collapsed panel nav — used by shell keyboard shortcuts. */
104
+ toggleCollapsed(): Promise<void>;
104
105
  private handleFooterAction;
105
106
  private handleUserAction;
106
107
  private clearEdgeOverlayTimer;
@@ -45,6 +45,10 @@ export declare class PanelTools {
45
45
  private isViewActive;
46
46
  private headerLabel;
47
47
  private handleToolChange;
48
+ /** Toggle any rail tool open/closed — shell shortcuts ⌘/Ctrl+K/A/S/M/N call this. */
49
+ activateTool(id: PanelToolsToolId): Promise<void>;
50
+ /** Close the tools drawer when open — used by shell keyboard shortcuts. */
51
+ closeDrawer(): Promise<void>;
48
52
  private focusRailAt;
49
53
  private handleRailKeyDown;
50
54
  private renderRailAction;
@@ -1,7 +1,13 @@
1
- import type { PanelToolsItem } from './panel-tools-types';
1
+ import type { PanelToolsItem, PanelToolsToolId } from './panel-tools-types';
2
2
  export declare function parsePanelToolsItems(items: PanelToolsItem[], itemsJson: string): PanelToolsItem[];
3
3
  export declare function shouldResyncPanelToolsItems(prev: PanelToolsItem[], next: PanelToolsItem[]): boolean;
4
4
  export type PanelToolsMotion = 'opening' | 'closing' | 'idle';
5
5
  /** True when the drawer clip is fully closed — safe to skip painting slot content. */
6
6
  export declare function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean;
7
+ /** Toggle or switch rail tool selection — repeat activation closes the active tool. */
8
+ export declare function resolvePanelToolActivation(open: boolean, activeTool: PanelToolsToolId | '', id: PanelToolsToolId): {
9
+ open: boolean;
10
+ activeTool: PanelToolsToolId;
11
+ selected: boolean;
12
+ };
7
13
  //# sourceMappingURL=panel-tools-utils.d.ts.map
@@ -118,6 +118,11 @@ export namespace Components {
118
118
  * @default 'dashboard'
119
119
  */
120
120
  "navStyle": NavChromeStyle;
121
+ /**
122
+ * When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer.
123
+ * @default true
124
+ */
125
+ "shortcutsEnabled": boolean;
121
126
  }
122
127
  interface DsBadge {
123
128
  /**
@@ -697,6 +702,10 @@ export namespace Components {
697
702
  * @default ''
698
703
  */
699
704
  "storageKey": string;
705
+ /**
706
+ * Toggle expanded/collapsed panel nav — used by shell keyboard shortcuts.
707
+ */
708
+ "toggleCollapsed": () => Promise<void>;
700
709
  /**
701
710
  * Single character shown in the collapsed avatar
702
711
  * @default ''
@@ -709,11 +718,19 @@ export namespace Components {
709
718
  "userName": string;
710
719
  }
711
720
  interface DsPanelTools {
721
+ /**
722
+ * Toggle any rail tool open/closed — shell shortcuts ⌘/Ctrl+K/A/S/M/N call this.
723
+ */
724
+ "activateTool": (id: PanelToolsToolId) => Promise<void>;
712
725
  /**
713
726
  * Active tool view — `search`, `agents`, `messages`, `stacks`, or `activity`.
714
727
  * @default ''
715
728
  */
716
729
  "activeTool": PanelToolsToolId | '';
730
+ /**
731
+ * Close the tools drawer when open — used by shell keyboard shortcuts.
732
+ */
733
+ "closeDrawer": () => Promise<void>;
717
734
  /**
718
735
  * Rail items rendered in the right column. Set via JS property: `el.items = [...]`. Replace the array reference to update.
719
736
  * @default []
@@ -1838,6 +1855,11 @@ declare namespace LocalJSX {
1838
1855
  * @default 'dashboard'
1839
1856
  */
1840
1857
  "navStyle"?: NavChromeStyle;
1858
+ /**
1859
+ * When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer.
1860
+ * @default true
1861
+ */
1862
+ "shortcutsEnabled"?: boolean;
1841
1863
  }
1842
1864
  interface DsBadge {
1843
1865
  /**
@@ -2942,6 +2964,7 @@ declare namespace LocalJSX {
2942
2964
  "gradient": boolean;
2943
2965
  "grid": boolean;
2944
2966
  "gradientSrc": string;
2967
+ "shortcutsEnabled": boolean;
2945
2968
  }
2946
2969
  interface DsBadgeAttributes {
2947
2970
  "variant": BadgeVariant;
@@ -0,0 +1,10 @@
1
+ import type { PanelToolsToolId } from '../components/PanelTools/panel-tools-types';
2
+ export type ShellShortcutAction = 'toggle-panel-nav' | 'close-panel-tools' | `open-tool:${PanelToolsToolId}`;
3
+ /** Tool shortcut chords (⌘/Ctrl+K/A/S/M/N) toggle open/closed via `ds-panel-tools.activateTool`. */
4
+ /** True when Cmd (macOS) or Ctrl (Windows/Linux) is held without Alt/Shift. */
5
+ export declare function isShellShortcutModifier(e: Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'>): boolean;
6
+ /** Skip shell shortcuts while typing in an editable control. */
7
+ export declare function isEditableShortcutTarget(target: EventTarget | null): boolean;
8
+ /** Resolve a shell chrome shortcut, or `null` when the chord is not handled. */
9
+ export declare function resolveShellShortcut(e: Pick<KeyboardEvent, 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'>): ShellShortcutAction | null;
10
+ //# sourceMappingURL=shell-shortcuts.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds-mo/ui",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "CompoMo — composable web components styled with TokoMo design tokens",
5
5
  "keywords": [
6
6
  "web-components",
@@ -69,7 +69,7 @@
69
69
  "pretest": "node scripts/generate-icon-catalog.mjs",
70
70
  "build": "stencil build && node scripts/patch-index-types.mjs && node scripts/verify-icons-externalized.mjs && node scripts/write-build-stamp.mjs",
71
71
  "verify:pack": "node scripts/verify-npm-pack.mjs && node scripts/verify-nav-import.mjs",
72
- "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/panel-tools.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/shell-chrome-metrics.test.ts tests/shell-chrome-layer.test.ts tests/chrome-transition.test.ts tests/bar-nav-overflow-hysteresis.test.ts tests/badge-gradient-ring.test.ts tests/scroll-edge-fade.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/menu-position.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
72
+ "test": "node --import tsx/esm --test tests/panel-nav-utils.test.ts tests/panel-tools.test.ts tests/shell-shortcuts.test.ts tests/shell-view-transition.test.ts tests/shell-gradient.test.ts tests/shell-chrome-metrics.test.ts tests/shell-chrome-layer.test.ts tests/chrome-transition.test.ts tests/bar-nav-overflow-hysteresis.test.ts tests/badge-gradient-ring.test.ts tests/scroll-edge-fade.test.ts tests/resolve-css-length-px.test.ts tests/resolve-css-time-ms.test.ts tests/overlay-positioning.test.ts tests/menu-position.test.ts tests/bar-nav-utils.test.ts tests/bar-nav-tabs-menu-utils.test.ts tests/bar-nav-dom-utils.test.ts tests/icon-catalog.test.ts",
73
73
  "test:e2e": "npm run build && playwright test",
74
74
  "test:e2e:install": "playwright install chromium",
75
75
  "build:docs": "stencil build --docs",
@@ -36,14 +36,14 @@ export declare interface DsAccordion extends Components.DsAccordion {
36
36
 
37
37
 
38
38
  @ProxyCmp({
39
- inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle']
39
+ inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle', 'shortcutsEnabled']
40
40
  })
41
41
  @Component({
42
42
  selector: 'ds-app-shell',
43
43
  changeDetection: ChangeDetectionStrategy.OnPush,
44
44
  template: '<ng-content></ng-content>',
45
45
  // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
46
- inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle'],
46
+ inputs: ['gradient', 'gradientSrc', 'grid', 'navStyle', 'shortcutsEnabled'],
47
47
  standalone: false
48
48
  })
49
49
  export class DsAppShell {
@@ -639,7 +639,8 @@ export declare interface DsPagination extends Components.DsPagination {
639
639
 
640
640
 
641
641
  @ProxyCmp({
642
- inputs: ['activeId', 'breakpoint', 'collapsed', 'currentUrl', 'disableViewTransition', 'groups', 'navStyle', 'routerMode', 'storageKey', 'userInitial', 'userName']
642
+ inputs: ['activeId', 'breakpoint', 'collapsed', 'currentUrl', 'disableViewTransition', 'groups', 'navStyle', 'routerMode', 'storageKey', 'userInitial', 'userName'],
643
+ methods: ['toggleCollapsed']
643
644
  })
644
645
  @Component({
645
646
  selector: 'ds-panel-nav',
@@ -695,7 +696,8 @@ export declare interface DsPanelNav extends Components.DsPanelNav {
695
696
 
696
697
 
697
698
  @ProxyCmp({
698
- inputs: ['activeTool', 'items', 'itemsJson', 'open']
699
+ inputs: ['activeTool', 'items', 'itemsJson', 'open'],
700
+ methods: ['activateTool', 'closeDrawer']
699
701
  })
700
702
  @Component({
701
703
  selector: 'ds-panel-tools',
@@ -1,5 +1,10 @@
1
- import { Component, Prop, Element, Watch, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Element, Watch, Listen, h, Host } from '@stencil/core';
2
2
  import type { NavChromeStyle } from '../../nav/nav-chrome';
3
+ import {
4
+ isEditableShortcutTarget,
5
+ resolveShellShortcut,
6
+ } from '../../nav/shell-shortcuts';
7
+ import type { PanelToolsToolId } from '../PanelTools/panel-tools-types';
3
8
  import {
4
9
  CHROME_TRANSITION_END,
5
10
  CHROME_TRANSITION_START,
@@ -48,6 +53,9 @@ export class AppShell {
48
53
  */
49
54
  @Prop() gradientSrc: string = '';
50
55
 
56
+ /** When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer. */
57
+ @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;
58
+
51
59
  @Element() el!: HTMLElement;
52
60
 
53
61
  private resizeObserver: ResizeObserver | null = null;
@@ -242,6 +250,35 @@ export class AppShell {
242
250
  return this.gradient || this.grid;
243
251
  }
244
252
 
253
+ @Listen('keydown', { target: 'window', capture: true })
254
+ handleWindowKeyDown(e: KeyboardEvent) {
255
+ if (!this.shortcutsEnabled) return;
256
+ if (isEditableShortcutTarget(e.target)) return;
257
+
258
+ const action = resolveShellShortcut(e);
259
+ if (!action) return;
260
+
261
+ e.preventDefault();
262
+
263
+ const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;
264
+ const tools = this.el.querySelector('ds-panel-tools') as HTMLDsPanelToolsElement | null;
265
+
266
+ if (action === 'toggle-panel-nav') {
267
+ void panel?.toggleCollapsed();
268
+ return;
269
+ }
270
+
271
+ if (action === 'close-panel-tools') {
272
+ void tools?.closeDrawer();
273
+ return;
274
+ }
275
+
276
+ if (action.startsWith('open-tool:') && tools) {
277
+ const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;
278
+ void tools.activateTool(toolId);
279
+ }
280
+ }
281
+
245
282
  private syncChrome() {
246
283
  const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;
247
284
  const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;
@@ -1,4 +1,4 @@
1
- import { Component, Prop, Event, EventEmitter, Watch, State, Element, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Event, EventEmitter, Watch, State, Element, Method, h, Host } from '@stencil/core';
2
2
  import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
3
3
  import type { NavChromeStyle } from '../../nav/nav-chrome';
4
4
  import {
@@ -288,10 +288,6 @@ export class PanelNav {
288
288
  return this.parsedGroups.flatMap(g => g.items);
289
289
  }
290
290
 
291
- private getRovingTotal(): number {
292
- return 1 + this.getAllItems().length + 2;
293
- }
294
-
295
291
  private getFooterRovingIndex(): number {
296
292
  return 1 + this.getAllItems().length;
297
293
  }
@@ -351,35 +347,29 @@ export class PanelNav {
351
347
  return;
352
348
  }
353
349
 
354
- const total = this.getRovingTotal();
355
350
  const footerIdx = this.getFooterRovingIndex();
356
351
  const userIdx = this.getUserRovingIndex();
357
- let next: number;
352
+ let next: number | undefined;
358
353
 
359
354
  switch (e.key) {
360
355
  case 'ArrowDown':
361
- e.preventDefault();
356
+ if (index === userIdx) return;
362
357
  if (index === footerIdx) next = userIdx;
363
- else if (index === userIdx) next = 0;
364
- else next = (index + 1) % total;
358
+ else next = index + 1;
365
359
  break;
366
360
  case 'ArrowUp':
367
- e.preventDefault();
368
- if (index === 0) next = userIdx;
369
- else if (index === userIdx) next = footerIdx;
370
- else next = (index - 1 + total) % total;
361
+ if (index === 0) return;
362
+ if (index === userIdx) next = footerIdx;
363
+ else if (index === footerIdx) next = footerIdx - 1;
364
+ else next = index - 1;
371
365
  break;
372
366
  case 'ArrowRight':
373
- if (index === footerIdx) {
374
- e.preventDefault();
375
- next = userIdx;
376
- } else return;
367
+ if (index === footerIdx) next = userIdx;
368
+ else return;
377
369
  break;
378
370
  case 'ArrowLeft':
379
- if (index === userIdx) {
380
- e.preventDefault();
381
- next = footerIdx;
382
- } else return;
371
+ if (index === userIdx) next = footerIdx;
372
+ else return;
383
373
  break;
384
374
  case 'Home':
385
375
  e.preventDefault();
@@ -393,6 +383,8 @@ export class PanelNav {
393
383
  return;
394
384
  }
395
385
 
386
+ if (next === undefined || next === index) return;
387
+ e.preventDefault();
396
388
  this.focusRovingAt(next);
397
389
  }
398
390
 
@@ -417,6 +409,12 @@ export class PanelNav {
417
409
  this.applyToggle(!this.collapsed);
418
410
  }
419
411
 
412
+ /** Toggle expanded/collapsed panel nav — used by shell keyboard shortcuts. */
413
+ @Method()
414
+ async toggleCollapsed() {
415
+ this.handleToggle();
416
+ }
417
+
420
418
  private handleFooterAction() {
421
419
  this.dsNavFooterAction.emit();
422
420
  }
@@ -1,4 +1,4 @@
1
- import { Component, Prop, Event, EventEmitter, Element, State, Watch, h, Host } from '@stencil/core';
1
+ import { Component, Prop, Event, EventEmitter, Element, State, Watch, Method, h, Host } from '@stencil/core';
2
2
  import type { ChromeTransitionDetail } from '../../nav/chrome-transition';
3
3
  import {
4
4
  PANEL_TOOLS_LABELS,
@@ -6,7 +6,7 @@ import {
6
6
  type PanelToolsItem,
7
7
  type PanelToolsToolId,
8
8
  } from './panel-tools-types';
9
- import { parsePanelToolsItems, panelToolsDrawerResting } from './panel-tools-utils';
9
+ import { parsePanelToolsItems, panelToolsDrawerResting, resolvePanelToolActivation } from './panel-tools-utils';
10
10
 
11
11
  @Component({
12
12
  tag: 'ds-panel-tools',
@@ -142,20 +142,36 @@ export class PanelTools {
142
142
  }
143
143
 
144
144
  private handleToolChange = (id: PanelToolsToolId) => {
145
- const selected = !this.isRailSelected(id);
146
- if (selected) {
147
- this.open = true;
148
- this.activeTool = id;
149
- } else {
150
- this.open = false;
151
- }
152
- this.dsToolChange.emit({ id, selected });
145
+ const next = resolvePanelToolActivation(this.open, this.activeTool, id);
146
+ this.open = next.open;
147
+ this.activeTool = next.activeTool;
148
+ this.dsToolChange.emit({ id, selected: next.selected });
153
149
  };
154
150
 
151
+ /** Toggle any rail tool open/closed — shell shortcuts ⌘/Ctrl+K/A/S/M/N call this. */
152
+ @Method()
153
+ async activateTool(id: PanelToolsToolId) {
154
+ const item = this.railItems.find(entry => entry.id === id);
155
+ if (!item || item.inactive) return;
156
+ this.handleToolChange(id);
157
+ }
158
+
159
+ /** Close the tools drawer when open — used by shell keyboard shortcuts. */
160
+ @Method()
161
+ async closeDrawer() {
162
+ if (!this.open) return;
163
+ const id = this.activeTool;
164
+ this.open = false;
165
+ if (id) {
166
+ this.dsToolChange.emit({ id, selected: false });
167
+ }
168
+ }
169
+
155
170
  private focusRailAt(index: number) {
156
171
  const items = this.orderedRailItems;
157
172
  if (!items.length) return;
158
- const bounded = ((index % items.length) + items.length) % items.length;
173
+ const bounded = Math.max(0, Math.min(index, items.length - 1));
174
+ if (bounded === this.rovingIndex) return;
159
175
  this.rovingIndex = bounded;
160
176
  const actions = Array.from(
161
177
  this.el.querySelectorAll<HTMLElement>('.panel-tools__rail-action .button-icon'),
@@ -174,12 +190,14 @@ export class PanelTools {
174
190
  }
175
191
 
176
192
  if (e.key === 'ArrowDown') {
193
+ if (index >= items.length - 1) return;
177
194
  e.preventDefault();
178
195
  this.focusRailAt(index + 1);
179
196
  return;
180
197
  }
181
198
 
182
199
  if (e.key === 'ArrowUp') {
200
+ if (index <= 0) return;
183
201
  e.preventDefault();
184
202
  this.focusRailAt(index - 1);
185
203
  }
@@ -1,5 +1,5 @@
1
1
  import { parseJsonArrayProp } from '../BarNav/bar-nav-utils';
2
- import type { PanelToolsItem } from './panel-tools-types';
2
+ import type { PanelToolsItem, PanelToolsToolId } from './panel-tools-types';
3
3
 
4
4
  export function parsePanelToolsItems(
5
5
  items: PanelToolsItem[],
@@ -36,3 +36,15 @@ export type PanelToolsMotion = 'opening' | 'closing' | 'idle';
36
36
  export function panelToolsDrawerResting(open: boolean, motion: PanelToolsMotion): boolean {
37
37
  return !open && motion === 'idle';
38
38
  }
39
+
40
+ /** Toggle or switch rail tool selection — repeat activation closes the active tool. */
41
+ export function resolvePanelToolActivation(
42
+ open: boolean,
43
+ activeTool: PanelToolsToolId | '',
44
+ id: PanelToolsToolId,
45
+ ): { open: boolean; activeTool: PanelToolsToolId; selected: boolean } {
46
+ if (open && activeTool === id) {
47
+ return { open: false, activeTool: id, selected: false };
48
+ }
49
+ return { open: true, activeTool: id, selected: true };
50
+ }
@@ -118,6 +118,11 @@ export namespace Components {
118
118
  * @default 'dashboard'
119
119
  */
120
120
  "navStyle": NavChromeStyle;
121
+ /**
122
+ * When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer.
123
+ * @default true
124
+ */
125
+ "shortcutsEnabled": boolean;
121
126
  }
122
127
  interface DsBadge {
123
128
  /**
@@ -697,6 +702,10 @@ export namespace Components {
697
702
  * @default ''
698
703
  */
699
704
  "storageKey": string;
705
+ /**
706
+ * Toggle expanded/collapsed panel nav — used by shell keyboard shortcuts.
707
+ */
708
+ "toggleCollapsed": () => Promise<void>;
700
709
  /**
701
710
  * Single character shown in the collapsed avatar
702
711
  * @default ''
@@ -709,11 +718,19 @@ export namespace Components {
709
718
  "userName": string;
710
719
  }
711
720
  interface DsPanelTools {
721
+ /**
722
+ * Toggle any rail tool open/closed — shell shortcuts ⌘/Ctrl+K/A/S/M/N call this.
723
+ */
724
+ "activateTool": (id: PanelToolsToolId) => Promise<void>;
712
725
  /**
713
726
  * Active tool view — `search`, `agents`, `messages`, `stacks`, or `activity`.
714
727
  * @default ''
715
728
  */
716
729
  "activeTool": PanelToolsToolId | '';
730
+ /**
731
+ * Close the tools drawer when open — used by shell keyboard shortcuts.
732
+ */
733
+ "closeDrawer": () => Promise<void>;
717
734
  /**
718
735
  * Rail items rendered in the right column. Set via JS property: `el.items = [...]`. Replace the array reference to update.
719
736
  * @default []
@@ -1838,6 +1855,11 @@ declare namespace LocalJSX {
1838
1855
  * @default 'dashboard'
1839
1856
  */
1840
1857
  "navStyle"?: NavChromeStyle;
1858
+ /**
1859
+ * When `true` (default), registers global shell keyboard shortcuts. Tool chords (⌘/Ctrl+K/A/S/M/N) toggle their drawer open and closed; ⌘/Ctrl+[ toggles panel nav; ⌘/Ctrl+] closes any open tool drawer.
1860
+ * @default true
1861
+ */
1862
+ "shortcutsEnabled"?: boolean;
1841
1863
  }
1842
1864
  interface DsBadge {
1843
1865
  /**
@@ -2942,6 +2964,7 @@ declare namespace LocalJSX {
2942
2964
  "gradient": boolean;
2943
2965
  "grid": boolean;
2944
2966
  "gradientSrc": string;
2967
+ "shortcutsEnabled": boolean;
2945
2968
  }
2946
2969
  interface DsBadgeAttributes {
2947
2970
  "variant": BadgeVariant;
@@ -0,0 +1,63 @@
1
+ import type { PanelToolsToolId } from '../components/PanelTools/panel-tools-types';
2
+
3
+ export type ShellShortcutAction =
4
+ | 'toggle-panel-nav'
5
+ | 'close-panel-tools'
6
+ | `open-tool:${PanelToolsToolId}`;
7
+
8
+ const TOOL_SHORTCUT_KEYS: Record<string, PanelToolsToolId> = {
9
+ k: 'search',
10
+ a: 'agents',
11
+ s: 'stacks',
12
+ m: 'messages',
13
+ n: 'activity',
14
+ };
15
+
16
+ /** Tool shortcut chords (⌘/Ctrl+K/A/S/M/N) toggle open/closed via `ds-panel-tools.activateTool`. */
17
+
18
+ /** True when Cmd (macOS) or Ctrl (Windows/Linux) is held without Alt/Shift. */
19
+ export function isShellShortcutModifier(
20
+ e: Pick<KeyboardEvent, 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'>,
21
+ ): boolean {
22
+ return (e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey;
23
+ }
24
+
25
+ /** Skip shell shortcuts while typing in an editable control. */
26
+ export function isEditableShortcutTarget(target: EventTarget | null): boolean {
27
+ if (!target || typeof target !== 'object') return false;
28
+ if (!('closest' in target) || typeof target.closest !== 'function') return false;
29
+
30
+ const el = target as Element & { isContentEditable?: boolean };
31
+
32
+ const editable = el.closest(
33
+ 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]',
34
+ );
35
+ if (editable) return true;
36
+
37
+ const role = el.getAttribute('role');
38
+ if (role === 'textbox' || role === 'combobox' || role === 'searchbox') return true;
39
+
40
+ return Boolean(el.isContentEditable);
41
+ }
42
+
43
+ function normalizedShortcutKey(e: Pick<KeyboardEvent, 'key' | 'code'>): string {
44
+ if (e.key === '[' || e.code === 'BracketLeft') return '[';
45
+ if (e.key === ']' || e.code === 'BracketRight') return ']';
46
+ return e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase();
47
+ }
48
+
49
+ /** Resolve a shell chrome shortcut, or `null` when the chord is not handled. */
50
+ export function resolveShellShortcut(
51
+ e: Pick<KeyboardEvent, 'key' | 'code' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'>,
52
+ ): ShellShortcutAction | null {
53
+ if (!isShellShortcutModifier(e)) return null;
54
+
55
+ const key = normalizedShortcutKey(e);
56
+ if (key === '[') return 'toggle-panel-nav';
57
+ if (key === ']') return 'close-panel-tools';
58
+
59
+ const toolId = TOOL_SHORTCUT_KEYS[key];
60
+ if (toolId) return `open-tool:${toolId}`;
61
+
62
+ return null;
63
+ }