@trackunit/react-components 1.15.21 → 1.15.22
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/index.cjs.js +401 -0
- package/index.esm.js +401 -1
- package/package.json +1 -1
- package/src/hooks/useKeyboardShortcut/formatShortcutLabel.d.ts +24 -0
- package/src/hooks/useKeyboardShortcut/platformUtils.d.ts +20 -0
- package/src/hooks/useKeyboardShortcut/reservedShortcutTypes.d.ts +188 -0
- package/src/hooks/useKeyboardShortcut/reservedShortcuts.d.ts +20 -0
- package/src/hooks/useKeyboardShortcut/shortcutMatcher.d.ts +18 -0
- package/src/hooks/useKeyboardShortcut/shortcutUtils.d.ts +6 -0
- package/src/hooks/useKeyboardShortcut/shortcutUtils.testUtils.d.ts +12 -0
- package/src/hooks/useKeyboardShortcut/types.d.ts +97 -0
- package/src/hooks/useKeyboardShortcut/useKeyboardShortcut.d.ts +56 -0
- package/src/hooks/useKeyboardShortcut/useShortcutConflicts.d.ts +41 -0
- package/src/index.d.ts +1 -0
package/index.esm.js
CHANGED
|
@@ -7571,6 +7571,406 @@ const useIsFullscreen = () => {
|
|
|
7571
7571
|
return isFullScreen;
|
|
7572
7572
|
};
|
|
7573
7573
|
|
|
7574
|
+
/**
|
|
7575
|
+
* Detects the current platform based on the user agent.
|
|
7576
|
+
*
|
|
7577
|
+
* @returns {Platform} The detected platform: "mac", "windows", or "linux"
|
|
7578
|
+
*/
|
|
7579
|
+
const getPlatform = () => {
|
|
7580
|
+
if (typeof navigator === "undefined") {
|
|
7581
|
+
// SSR default - most users are on Windows
|
|
7582
|
+
return "windows";
|
|
7583
|
+
}
|
|
7584
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
7585
|
+
if (ua.includes("mac")) {
|
|
7586
|
+
return "mac";
|
|
7587
|
+
}
|
|
7588
|
+
if (ua.includes("linux")) {
|
|
7589
|
+
return "linux";
|
|
7590
|
+
}
|
|
7591
|
+
return "windows";
|
|
7592
|
+
};
|
|
7593
|
+
|
|
7594
|
+
/** Display order for modifiers: mod first, then alt, then shift */
|
|
7595
|
+
const MODIFIER_DISPLAY_ORDER = ["mod", "alt", "shift"];
|
|
7596
|
+
/** Labels for each modifier, mapped per platform */
|
|
7597
|
+
const MODIFIER_LABELS = {
|
|
7598
|
+
mod: { mac: "Cmd", other: "Ctrl" },
|
|
7599
|
+
alt: { mac: "Option", other: "Alt" },
|
|
7600
|
+
shift: { mac: "Shift", other: "Shift" },
|
|
7601
|
+
};
|
|
7602
|
+
/**
|
|
7603
|
+
* Maps a semantic modifier to a display label based on the current platform.
|
|
7604
|
+
*/
|
|
7605
|
+
const getModifierLabel = (mod) => {
|
|
7606
|
+
const labels = MODIFIER_LABELS[mod];
|
|
7607
|
+
return getPlatform() === "mac" ? labels.mac : labels.other;
|
|
7608
|
+
};
|
|
7609
|
+
/**
|
|
7610
|
+
* Maps a key to a more readable display label.
|
|
7611
|
+
*/
|
|
7612
|
+
const getKeyLabel = (key) => {
|
|
7613
|
+
const keyLabels = {
|
|
7614
|
+
arrowup: "↑",
|
|
7615
|
+
arrowdown: "↓",
|
|
7616
|
+
arrowleft: "←",
|
|
7617
|
+
arrowright: "→",
|
|
7618
|
+
enter: "Enter",
|
|
7619
|
+
escape: "Esc",
|
|
7620
|
+
tab: "Tab",
|
|
7621
|
+
space: "Space",
|
|
7622
|
+
backspace: "Backspace",
|
|
7623
|
+
delete: "Delete",
|
|
7624
|
+
};
|
|
7625
|
+
const lowerKey = key.toLowerCase();
|
|
7626
|
+
return keyLabels[lowerKey] ?? key.toUpperCase();
|
|
7627
|
+
};
|
|
7628
|
+
/**
|
|
7629
|
+
* Formats a shortcut definition into a human-readable label.
|
|
7630
|
+
*
|
|
7631
|
+
* The label is platform-aware:
|
|
7632
|
+
* - Mac: "Cmd + K", "Option + Shift + L"
|
|
7633
|
+
* - Windows/Linux: "Ctrl + K", "Alt + Shift + L"
|
|
7634
|
+
*
|
|
7635
|
+
* @param shortcut - The shortcut definition to format
|
|
7636
|
+
* @param separator - The separator between parts (default: " + ")
|
|
7637
|
+
* @returns {string} A formatted string like "Cmd + K" or "Ctrl + Shift + L"
|
|
7638
|
+
* @example
|
|
7639
|
+
* formatShortcutLabel({ key: "k", modifiers: "mod" })
|
|
7640
|
+
* // Mac: "Cmd + K"
|
|
7641
|
+
* // Windows: "Ctrl + K"
|
|
7642
|
+
* @example
|
|
7643
|
+
* formatShortcutLabel({ key: "l", modifiers: "mod+shift" })
|
|
7644
|
+
* // Mac: "Cmd + Shift + L"
|
|
7645
|
+
* // Windows: "Ctrl + Shift + L"
|
|
7646
|
+
* @example
|
|
7647
|
+
* formatShortcutLabel({ key: "Escape" })
|
|
7648
|
+
* // "Esc"
|
|
7649
|
+
*/
|
|
7650
|
+
const formatShortcutLabel = (shortcut, separator = " + ") => {
|
|
7651
|
+
// Parse modifier string and sort in display order (mod, alt, shift)
|
|
7652
|
+
const modifierSet = new Set(shortcut.modifiers?.split("+") ?? []);
|
|
7653
|
+
const sortedModifiers = MODIFIER_DISPLAY_ORDER.filter(mod => modifierSet.has(mod));
|
|
7654
|
+
const modLabels = sortedModifiers.map(getModifierLabel);
|
|
7655
|
+
const keyLabel = getKeyLabel(shortcut.key);
|
|
7656
|
+
return [...modLabels, keyLabel].join(separator);
|
|
7657
|
+
};
|
|
7658
|
+
|
|
7659
|
+
/**
|
|
7660
|
+
* Parses a modifier string into a Set for easy lookup.
|
|
7661
|
+
*
|
|
7662
|
+
* @param modifiers - The modifier string (e.g., "mod+shift")
|
|
7663
|
+
* @returns {Set<string>} A Set containing the individual modifiers
|
|
7664
|
+
*/
|
|
7665
|
+
const parseModifiers = (modifiers) => {
|
|
7666
|
+
if (!modifiers) {
|
|
7667
|
+
return new Set();
|
|
7668
|
+
}
|
|
7669
|
+
return new Set(modifiers.split("+"));
|
|
7670
|
+
};
|
|
7671
|
+
/**
|
|
7672
|
+
* Checks if a keyboard event matches the shortcut definition.
|
|
7673
|
+
*
|
|
7674
|
+
* This is a pure function that handles:
|
|
7675
|
+
* - Case-insensitive key matching
|
|
7676
|
+
* - Platform-aware modifier key checking (Cmd on Mac, Ctrl on Windows/Linux)
|
|
7677
|
+
* - Space key normalization
|
|
7678
|
+
* - Extra modifier rejection (ensures only expected modifiers are pressed)
|
|
7679
|
+
*
|
|
7680
|
+
* @param event - The keyboard event to check
|
|
7681
|
+
* @param shortcut - The shortcut definition to match against
|
|
7682
|
+
* @returns {boolean} true if the event matches the shortcut, false otherwise
|
|
7683
|
+
* @example
|
|
7684
|
+
* // Check if Cmd+K (Mac) or Ctrl+K (Windows) was pressed
|
|
7685
|
+
* const isMatch = matchesShortcut(event, { key: "k", modifiers: "mod" });
|
|
7686
|
+
*/
|
|
7687
|
+
const matchesShortcut = (event, shortcut) => {
|
|
7688
|
+
// Check the primary key (case-insensitive)
|
|
7689
|
+
const eventKey = event.key.toLowerCase();
|
|
7690
|
+
const shortcutKey = shortcut.key.toLowerCase();
|
|
7691
|
+
// Handle special keys that might have different representations
|
|
7692
|
+
// Normalize both to the same representation for comparison
|
|
7693
|
+
const normalizedEventKey = eventKey === " " ? "space" : eventKey;
|
|
7694
|
+
const normalizedShortcutKey = shortcutKey === " " ? "space" : shortcutKey;
|
|
7695
|
+
if (normalizedEventKey !== normalizedShortcutKey) {
|
|
7696
|
+
return false;
|
|
7697
|
+
}
|
|
7698
|
+
const modifiers = parseModifiers(shortcut.modifiers);
|
|
7699
|
+
// Check "mod" modifier (Cmd on Mac, Ctrl on Windows/Linux)
|
|
7700
|
+
const expectMod = modifiers.has("mod");
|
|
7701
|
+
const hasMod = getPlatform() === "mac" ? event.metaKey : event.ctrlKey;
|
|
7702
|
+
if (expectMod !== hasMod) {
|
|
7703
|
+
return false;
|
|
7704
|
+
}
|
|
7705
|
+
// Check "alt" modifier (Option on Mac, Alt on Windows/Linux)
|
|
7706
|
+
const expectAlt = modifiers.has("alt");
|
|
7707
|
+
if (expectAlt !== event.altKey) {
|
|
7708
|
+
return false;
|
|
7709
|
+
}
|
|
7710
|
+
// Check "shift" modifier
|
|
7711
|
+
const expectShift = modifiers.has("shift");
|
|
7712
|
+
if (expectShift !== event.shiftKey) {
|
|
7713
|
+
return false;
|
|
7714
|
+
}
|
|
7715
|
+
// Make sure no extra modifiers are pressed
|
|
7716
|
+
// If mod is expected, we've already checked the right key (meta or ctrl)
|
|
7717
|
+
// But we need to ensure the OTHER mod key isn't also pressed
|
|
7718
|
+
if (!expectMod) {
|
|
7719
|
+
// If we don't expect mod, neither metaKey nor ctrlKey should be pressed
|
|
7720
|
+
if (event.metaKey || event.ctrlKey) {
|
|
7721
|
+
return false;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
else {
|
|
7725
|
+
// If we expect mod, the opposite mod key shouldn't be pressed
|
|
7726
|
+
const oppositeModPressed = getPlatform() === "mac" ? event.ctrlKey : event.metaKey;
|
|
7727
|
+
if (oppositeModPressed) {
|
|
7728
|
+
return false;
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
return true;
|
|
7732
|
+
};
|
|
7733
|
+
|
|
7734
|
+
/**
|
|
7735
|
+
* Reserved application shortcuts.
|
|
7736
|
+
* These are host-level shortcuts that are blocked at the type level.
|
|
7737
|
+
* See reservedShortcutTypes.ts for the type-level enforcement.
|
|
7738
|
+
*
|
|
7739
|
+
* IMPORTANT: When adding a new reserved shortcut here, you must also
|
|
7740
|
+
* update the type definitions in reservedShortcutTypes.ts and the
|
|
7741
|
+
* isGlobalShortcut() function in apps/iris-app-loader/src/forward-events-to-host.ts.
|
|
7742
|
+
*/
|
|
7743
|
+
const RESERVED_SHORTCUTS = {
|
|
7744
|
+
"mod+k": {
|
|
7745
|
+
name: "Global Search",
|
|
7746
|
+
owner: "host-navigation",
|
|
7747
|
+
},
|
|
7748
|
+
"mod+shift+l": {
|
|
7749
|
+
name: "Local Dev Mode Toggle",
|
|
7750
|
+
owner: "host-navigation",
|
|
7751
|
+
},
|
|
7752
|
+
};
|
|
7753
|
+
/**
|
|
7754
|
+
* Common browser shortcuts that cannot be reliably overridden.
|
|
7755
|
+
* Using these shortcuts will trigger a development warning since
|
|
7756
|
+
* the browser typically intercepts them before JavaScript can handle them.
|
|
7757
|
+
*
|
|
7758
|
+
* Note: Some of these can be overridden with preventDefault(), but it's
|
|
7759
|
+
* generally a bad UX to override expected browser behavior.
|
|
7760
|
+
*/
|
|
7761
|
+
const BROWSER_SHORTCUTS = {
|
|
7762
|
+
// Tab management
|
|
7763
|
+
"mod+t": "New tab",
|
|
7764
|
+
"mod+w": "Close tab",
|
|
7765
|
+
"mod+shift+t": "Reopen closed tab",
|
|
7766
|
+
"mod+n": "New window",
|
|
7767
|
+
"mod+shift+n": "New incognito window",
|
|
7768
|
+
// Navigation
|
|
7769
|
+
"mod+l": "Focus address bar",
|
|
7770
|
+
"mod+r": "Reload page",
|
|
7771
|
+
"mod+shift+r": "Hard reload",
|
|
7772
|
+
// Page actions
|
|
7773
|
+
"mod+s": "Save page",
|
|
7774
|
+
"mod+p": "Print",
|
|
7775
|
+
"mod+f": "Find in page",
|
|
7776
|
+
"mod+g": "Find next",
|
|
7777
|
+
"mod+shift+g": "Find previous",
|
|
7778
|
+
"mod+o": "Open file",
|
|
7779
|
+
// Browser features
|
|
7780
|
+
"mod+h": "History (Chrome) / Hide window (Mac)",
|
|
7781
|
+
"mod+j": "Downloads",
|
|
7782
|
+
"mod+d": "Bookmark page",
|
|
7783
|
+
"mod+shift+b": "Toggle bookmarks bar",
|
|
7784
|
+
// Developer tools
|
|
7785
|
+
"mod+shift+i": "Developer tools",
|
|
7786
|
+
"mod+shift+j": "JavaScript console",
|
|
7787
|
+
"mod+shift+c": "Inspect element",
|
|
7788
|
+
// Function keys
|
|
7789
|
+
f1: "Help",
|
|
7790
|
+
f3: "Find next",
|
|
7791
|
+
f5: "Reload",
|
|
7792
|
+
f11: "Fullscreen",
|
|
7793
|
+
f12: "Developer tools",
|
|
7794
|
+
// Zoom
|
|
7795
|
+
"mod+0": "Reset zoom",
|
|
7796
|
+
"mod+-": "Zoom out",
|
|
7797
|
+
"mod+=": "Zoom in",
|
|
7798
|
+
};
|
|
7799
|
+
|
|
7800
|
+
/**
|
|
7801
|
+
* Converts a ShortcutDefinition to a normalized string key for lookup.
|
|
7802
|
+
* Format: "mod+shift+k" (modifiers sorted alphabetically, then key)
|
|
7803
|
+
*/
|
|
7804
|
+
const shortcutToString = (shortcut) => {
|
|
7805
|
+
if (!shortcut.modifiers) {
|
|
7806
|
+
return shortcut.key.toLowerCase();
|
|
7807
|
+
}
|
|
7808
|
+
// Modifiers are already in alphabetical order (enforced by type)
|
|
7809
|
+
return `${shortcut.modifiers}+${shortcut.key.toLowerCase()}`;
|
|
7810
|
+
};
|
|
7811
|
+
|
|
7812
|
+
/**
|
|
7813
|
+
* Hook for checking keyboard shortcut conflicts.
|
|
7814
|
+
*
|
|
7815
|
+
* This hook checks if a shortcut conflicts with:
|
|
7816
|
+
* - Reserved application shortcuts (host-level features)
|
|
7817
|
+
* - Browser shortcuts (that cannot be reliably overridden)
|
|
7818
|
+
*
|
|
7819
|
+
* In development mode, it logs a warning if the shortcut conflicts with
|
|
7820
|
+
* browser shortcuts. Reserved shortcuts are blocked at the type level.
|
|
7821
|
+
*
|
|
7822
|
+
* @param shortcut - The shortcut definition to check
|
|
7823
|
+
* @param options - Optional configuration
|
|
7824
|
+
* @returns {ShortcutConflictInfo} Conflict information for the shortcut
|
|
7825
|
+
* @example
|
|
7826
|
+
* const conflicts = useShortcutConflicts({ key: "k", modifiers: "mod" });
|
|
7827
|
+
* if (conflicts.isReserved) {
|
|
7828
|
+
* console.log(`Reserved by: ${conflicts.reservedInfo?.owner}`);
|
|
7829
|
+
* }
|
|
7830
|
+
*/
|
|
7831
|
+
const useShortcutConflicts = (shortcut, options) => {
|
|
7832
|
+
const disabled = options?.disabled ?? false;
|
|
7833
|
+
const conflictInfo = useMemo(() => {
|
|
7834
|
+
if (disabled) {
|
|
7835
|
+
return {
|
|
7836
|
+
isReserved: false,
|
|
7837
|
+
reservedInfo: undefined,
|
|
7838
|
+
browserConflict: undefined,
|
|
7839
|
+
};
|
|
7840
|
+
}
|
|
7841
|
+
const key = shortcutToString(shortcut);
|
|
7842
|
+
const reservedInfo = RESERVED_SHORTCUTS[key];
|
|
7843
|
+
const browserConflict = BROWSER_SHORTCUTS[key];
|
|
7844
|
+
return {
|
|
7845
|
+
isReserved: reservedInfo !== undefined,
|
|
7846
|
+
reservedInfo,
|
|
7847
|
+
browserConflict,
|
|
7848
|
+
};
|
|
7849
|
+
}, [shortcut, disabled]);
|
|
7850
|
+
// Log development warning for browser conflicts
|
|
7851
|
+
useEffect(() => {
|
|
7852
|
+
if (disabled || process.env.NODE_ENV === "production") {
|
|
7853
|
+
return;
|
|
7854
|
+
}
|
|
7855
|
+
if (conflictInfo.browserConflict) {
|
|
7856
|
+
const shortcutStr = shortcutToString(shortcut);
|
|
7857
|
+
// eslint-disable-next-line no-console -- Development warning for browser shortcut conflicts
|
|
7858
|
+
console.warn(`[useKeyboardShortcut] Shortcut "${shortcutStr}" conflicts with browser shortcut: "${conflictInfo.browserConflict}". ` +
|
|
7859
|
+
`This shortcut may not work reliably as the browser may intercept it.`);
|
|
7860
|
+
}
|
|
7861
|
+
// Only run on mount
|
|
7862
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
7863
|
+
}, []);
|
|
7864
|
+
return conflictInfo;
|
|
7865
|
+
};
|
|
7866
|
+
|
|
7867
|
+
/** Time window in ms for the browser fallback escape hatch. */
|
|
7868
|
+
const REPEAT_WINDOW_MS = 400;
|
|
7869
|
+
/**
|
|
7870
|
+
* Hook for handling keyboard shortcuts with platform-aware modifiers.
|
|
7871
|
+
*
|
|
7872
|
+
* Reserved shortcuts are blocked at the type level and will show specific error messages.
|
|
7873
|
+
* There are two categories:
|
|
7874
|
+
*
|
|
7875
|
+
* **App-reserved** (host-level features):
|
|
7876
|
+
* - mod+k: Global Search
|
|
7877
|
+
* - mod+shift+l: Local Dev Mode Toggle
|
|
7878
|
+
*
|
|
7879
|
+
* **Browser-reserved** (native browser shortcuts):
|
|
7880
|
+
* - Clipboard: mod+c (Copy), mod+v (Paste), mod+x (Cut), mod+a (Select All)
|
|
7881
|
+
* - Undo/Redo: mod+z (Undo), mod+shift+z (Redo Mac), mod+y (Redo Win/Linux)
|
|
7882
|
+
* - Find: mod+f (Find), mod+g (Find next), mod+shift+g (Find previous)
|
|
7883
|
+
* - Navigation: mod+r (Reload), mod+shift+r (Hard reload), mod+t (New tab),
|
|
7884
|
+
* mod+w (Close tab), mod+shift+t (Reopen tab), mod+n (New window),
|
|
7885
|
+
* mod+shift+n (Incognito), mod+l (Address bar), mod+d (Bookmark)
|
|
7886
|
+
* - Zoom: mod+= (Zoom in), mod+- (Zoom out), mod+0 (Reset zoom)
|
|
7887
|
+
* - Other: mod+p (Print), mod+s (Save), mod+o (Open), mod+h (History/Hide),
|
|
7888
|
+
* mod+j (Downloads), mod+q (Quit Mac)
|
|
7889
|
+
*
|
|
7890
|
+
* See reservedShortcutTypes.ts for the full list with error messages.
|
|
7891
|
+
*
|
|
7892
|
+
* @param shortcut - The shortcut definition with key and optional modifiers
|
|
7893
|
+
* @param options - Configuration options for the shortcut handler
|
|
7894
|
+
* @param options.onTrigger - Callback invoked when the shortcut is triggered
|
|
7895
|
+
* @param options.disabled - Disable the shortcut conditionally (default: false)
|
|
7896
|
+
* @param options.preventDefault - Controls browser default behavior:
|
|
7897
|
+
* - `"never"` (default) - Browser handles the event normally
|
|
7898
|
+
* - `"once"` - Prevent default, but rapid repeat (within 400ms) passes to browser
|
|
7899
|
+
* - `"always"` - Always prevent default, no browser escape hatch
|
|
7900
|
+
* @param options.target - Target element for the listener (default: "window")
|
|
7901
|
+
* @returns {{ label: string }} Object containing the formatted shortcut label (e.g., "Cmd + K" on Mac)
|
|
7902
|
+
* @example
|
|
7903
|
+
* // Simple Escape key handler (no preventDefault)
|
|
7904
|
+
* const { label } = useKeyboardShortcut(
|
|
7905
|
+
* { key: "Escape" },
|
|
7906
|
+
* { onTrigger: () => closeModal() }
|
|
7907
|
+
* );
|
|
7908
|
+
* // label = "Esc"
|
|
7909
|
+
* @example
|
|
7910
|
+
* // Always prevent default, allow rapid re-triggering
|
|
7911
|
+
* const { label } = useKeyboardShortcut(
|
|
7912
|
+
* { key: "m", modifiers: "mod" },
|
|
7913
|
+
* {
|
|
7914
|
+
* onTrigger: () => createNewItem(),
|
|
7915
|
+
* preventDefault: "always",
|
|
7916
|
+
* }
|
|
7917
|
+
* );
|
|
7918
|
+
* // label = "Cmd + M" (Mac) or "Ctrl + M" (Windows/Linux)
|
|
7919
|
+
*/
|
|
7920
|
+
const useKeyboardShortcut = (shortcut, { onTrigger, disabled = false, preventDefault = "never", target = "window" }) => {
|
|
7921
|
+
// Use refs to avoid recreating the handler on every render
|
|
7922
|
+
const onTriggerRef = useRef(onTrigger);
|
|
7923
|
+
const preventDefaultRef = useRef(preventDefault);
|
|
7924
|
+
// Track when the shortcut was last triggered for browser fallback
|
|
7925
|
+
const lastTriggerRef = useRef(0);
|
|
7926
|
+
// Keep refs up to date
|
|
7927
|
+
useEffect(() => {
|
|
7928
|
+
onTriggerRef.current = onTrigger;
|
|
7929
|
+
preventDefaultRef.current = preventDefault;
|
|
7930
|
+
}, [onTrigger, preventDefault]);
|
|
7931
|
+
// Check for conflicts and warn in development (handled internally by the hook)
|
|
7932
|
+
useShortcutConflicts(shortcut, { disabled });
|
|
7933
|
+
const handleKeyDown = useCallback((event) => {
|
|
7934
|
+
if (matchesShortcut(event, shortcut)) {
|
|
7935
|
+
const now = Date.now();
|
|
7936
|
+
const timeSinceLastTrigger = now - lastTriggerRef.current;
|
|
7937
|
+
const mode = preventDefaultRef.current;
|
|
7938
|
+
// Browser fallback: "once" mode skips handling if pressed within the repeat window
|
|
7939
|
+
if (mode === "once" && timeSinceLastTrigger < REPEAT_WINDOW_MS) {
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7942
|
+
if (mode !== "never") {
|
|
7943
|
+
event.preventDefault();
|
|
7944
|
+
}
|
|
7945
|
+
lastTriggerRef.current = now;
|
|
7946
|
+
onTriggerRef.current();
|
|
7947
|
+
}
|
|
7948
|
+
}, [shortcut]);
|
|
7949
|
+
useEffect(() => {
|
|
7950
|
+
if (disabled) {
|
|
7951
|
+
return;
|
|
7952
|
+
}
|
|
7953
|
+
const targetElement = target === "window" ? window : target.current;
|
|
7954
|
+
if (!targetElement) {
|
|
7955
|
+
return;
|
|
7956
|
+
}
|
|
7957
|
+
// Use a wrapper that properly handles the event type for both Window and HTMLElement
|
|
7958
|
+
const keydownHandler = (e) => {
|
|
7959
|
+
if (e instanceof KeyboardEvent) {
|
|
7960
|
+
handleKeyDown(e);
|
|
7961
|
+
}
|
|
7962
|
+
};
|
|
7963
|
+
targetElement.addEventListener("keydown", keydownHandler);
|
|
7964
|
+
return () => {
|
|
7965
|
+
targetElement.removeEventListener("keydown", keydownHandler);
|
|
7966
|
+
};
|
|
7967
|
+
}, [disabled, target, handleKeyDown]);
|
|
7968
|
+
// Compute the human-readable label for the shortcut
|
|
7969
|
+
const label = useMemo(() => formatShortcutLabel(shortcut), [shortcut]);
|
|
7970
|
+
// Memoize the return object for referential stability
|
|
7971
|
+
return useMemo(() => ({ label }), [label]);
|
|
7972
|
+
};
|
|
7973
|
+
|
|
7574
7974
|
/**
|
|
7575
7975
|
* Hook that returns true if any modifier key (Ctrl, Alt, Shift, Meta/Cmd) is pressed
|
|
7576
7976
|
*
|
|
@@ -7939,4 +8339,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
|
|
|
7939
8339
|
return useMemo(() => ({ focused }), [focused]);
|
|
7940
8340
|
};
|
|
7941
8341
|
|
|
7942
|
-
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
|
8342
|
+
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SectionHeader, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getResponsiveRandomWidthPercentage, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ShortcutDefinition } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Formats a shortcut definition into a human-readable label.
|
|
4
|
+
*
|
|
5
|
+
* The label is platform-aware:
|
|
6
|
+
* - Mac: "Cmd + K", "Option + Shift + L"
|
|
7
|
+
* - Windows/Linux: "Ctrl + K", "Alt + Shift + L"
|
|
8
|
+
*
|
|
9
|
+
* @param shortcut - The shortcut definition to format
|
|
10
|
+
* @param separator - The separator between parts (default: " + ")
|
|
11
|
+
* @returns {string} A formatted string like "Cmd + K" or "Ctrl + Shift + L"
|
|
12
|
+
* @example
|
|
13
|
+
* formatShortcutLabel({ key: "k", modifiers: "mod" })
|
|
14
|
+
* // Mac: "Cmd + K"
|
|
15
|
+
* // Windows: "Ctrl + K"
|
|
16
|
+
* @example
|
|
17
|
+
* formatShortcutLabel({ key: "l", modifiers: "mod+shift" })
|
|
18
|
+
* // Mac: "Cmd + Shift + L"
|
|
19
|
+
* // Windows: "Ctrl + Shift + L"
|
|
20
|
+
* @example
|
|
21
|
+
* formatShortcutLabel({ key: "Escape" })
|
|
22
|
+
* // "Esc"
|
|
23
|
+
*/
|
|
24
|
+
export declare const formatShortcutLabel: (shortcut: ShortcutDefinition, separator?: string) => string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Platform } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Detects the current platform based on the user agent.
|
|
4
|
+
*
|
|
5
|
+
* @returns {Platform} The detected platform: "mac", "windows", or "linux"
|
|
6
|
+
*/
|
|
7
|
+
export declare const getPlatform: () => Platform;
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the primary modifier key is pressed based on platform.
|
|
10
|
+
* - Mac: metaKey (Cmd)
|
|
11
|
+
* - Windows/Linux: ctrlKey
|
|
12
|
+
*/
|
|
13
|
+
export declare const isModKeyPressed: (event: KeyboardEvent) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if the opposite modifier key is pressed (the one NOT used for "mod").
|
|
16
|
+
* This helps detect when a user presses the wrong modifier for their platform.
|
|
17
|
+
* - Mac: ctrlKey (Ctrl pressed when Cmd expected)
|
|
18
|
+
* - Windows/Linux: metaKey (Win key pressed when Ctrl expected)
|
|
19
|
+
*/
|
|
20
|
+
export declare const isOppositeModKeyPressed: (event: KeyboardEvent) => boolean;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ShortcutDefinition } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Type-level utilities for preventing reserved shortcuts from being used.
|
|
4
|
+
*
|
|
5
|
+
* There are two categories of reserved shortcuts:
|
|
6
|
+
*
|
|
7
|
+
* 1. APP-RESERVED: Host-level shortcuts used by our application
|
|
8
|
+
* - mod+k: Global search
|
|
9
|
+
* - mod+shift+l: Local dev mode toggle
|
|
10
|
+
*
|
|
11
|
+
* 2. BROWSER-RESERVED: Native browser shortcuts that should not be overridden
|
|
12
|
+
* - Clipboard: mod+c, mod+v, mod+x, mod+a
|
|
13
|
+
* - Undo/Redo: mod+z, mod+shift+z, mod+y
|
|
14
|
+
* - Find: mod+f, mod+g, mod+shift+g
|
|
15
|
+
* - Navigation: mod+r, mod+shift+r, mod+t, mod+w, mod+shift+t, mod+n, mod+shift+n, mod+l, mod+d
|
|
16
|
+
* - Zoom: mod+=, mod+-, mod+0
|
|
17
|
+
* - Other: mod+p, mod+s, mod+o, mod+h, mod+j, mod+q
|
|
18
|
+
*
|
|
19
|
+
* The type system prevents their use. Legitimate uses (in host-navigation)
|
|
20
|
+
* must use ts-expect-error comments with an explanation.
|
|
21
|
+
*/
|
|
22
|
+
/** Template for reserved shortcut error messages */
|
|
23
|
+
type ReservedMessage<TCategory extends string, TKeys extends string, TFeature extends string> = `${TCategory} reserved: ${TKeys} is used for ${TFeature}, which should not be overwritten`;
|
|
24
|
+
/** App reserved: mod+k for Global Search */
|
|
25
|
+
type AppReservedModK = {
|
|
26
|
+
readonly key: "k";
|
|
27
|
+
readonly modifiers: "mod";
|
|
28
|
+
};
|
|
29
|
+
/** App reserved: mod+shift+l for Local Dev Mode Toggle */
|
|
30
|
+
type AppReservedModShiftL = {
|
|
31
|
+
readonly key: "l";
|
|
32
|
+
readonly modifiers: "mod+shift";
|
|
33
|
+
};
|
|
34
|
+
/** Union of all app-reserved shortcuts */
|
|
35
|
+
type AppReservedShortcut = AppReservedModK | AppReservedModShiftL;
|
|
36
|
+
/** Maps each app-reserved shortcut to its feature description */
|
|
37
|
+
type AppShortcutFeature<TShortcut> = TShortcut extends AppReservedModK ? "Global Search" : TShortcut extends AppReservedModShiftL ? "Local Dev Mode Toggle" : never;
|
|
38
|
+
/** Maps each app-reserved shortcut to its key string */
|
|
39
|
+
type AppShortcutKeys<TShortcut> = TShortcut extends AppReservedModK ? "mod+k" : TShortcut extends AppReservedModShiftL ? "mod+shift+l" : never;
|
|
40
|
+
/** Maps each app-reserved shortcut to its error message */
|
|
41
|
+
type AppReservedErrorMessage<TShortcut> = ReservedMessage<"App", AppShortcutKeys<TShortcut>, AppShortcutFeature<TShortcut>>;
|
|
42
|
+
type BrowserReservedModC = {
|
|
43
|
+
readonly key: "c";
|
|
44
|
+
readonly modifiers: "mod";
|
|
45
|
+
};
|
|
46
|
+
type BrowserReservedModV = {
|
|
47
|
+
readonly key: "v";
|
|
48
|
+
readonly modifiers: "mod";
|
|
49
|
+
};
|
|
50
|
+
type BrowserReservedModX = {
|
|
51
|
+
readonly key: "x";
|
|
52
|
+
readonly modifiers: "mod";
|
|
53
|
+
};
|
|
54
|
+
type BrowserReservedModA = {
|
|
55
|
+
readonly key: "a";
|
|
56
|
+
readonly modifiers: "mod";
|
|
57
|
+
};
|
|
58
|
+
type BrowserReservedModZ = {
|
|
59
|
+
readonly key: "z";
|
|
60
|
+
readonly modifiers: "mod";
|
|
61
|
+
};
|
|
62
|
+
type BrowserReservedModShiftZ = {
|
|
63
|
+
readonly key: "z";
|
|
64
|
+
readonly modifiers: "mod+shift";
|
|
65
|
+
};
|
|
66
|
+
type BrowserReservedModY = {
|
|
67
|
+
readonly key: "y";
|
|
68
|
+
readonly modifiers: "mod";
|
|
69
|
+
};
|
|
70
|
+
type BrowserReservedModF = {
|
|
71
|
+
readonly key: "f";
|
|
72
|
+
readonly modifiers: "mod";
|
|
73
|
+
};
|
|
74
|
+
type BrowserReservedModG = {
|
|
75
|
+
readonly key: "g";
|
|
76
|
+
readonly modifiers: "mod";
|
|
77
|
+
};
|
|
78
|
+
type BrowserReservedModShiftG = {
|
|
79
|
+
readonly key: "g";
|
|
80
|
+
readonly modifiers: "mod+shift";
|
|
81
|
+
};
|
|
82
|
+
type BrowserReservedModR = {
|
|
83
|
+
readonly key: "r";
|
|
84
|
+
readonly modifiers: "mod";
|
|
85
|
+
};
|
|
86
|
+
type BrowserReservedModShiftR = {
|
|
87
|
+
readonly key: "r";
|
|
88
|
+
readonly modifiers: "mod+shift";
|
|
89
|
+
};
|
|
90
|
+
type BrowserReservedModT = {
|
|
91
|
+
readonly key: "t";
|
|
92
|
+
readonly modifiers: "mod";
|
|
93
|
+
};
|
|
94
|
+
type BrowserReservedModW = {
|
|
95
|
+
readonly key: "w";
|
|
96
|
+
readonly modifiers: "mod";
|
|
97
|
+
};
|
|
98
|
+
type BrowserReservedModShiftT = {
|
|
99
|
+
readonly key: "t";
|
|
100
|
+
readonly modifiers: "mod+shift";
|
|
101
|
+
};
|
|
102
|
+
type BrowserReservedModN = {
|
|
103
|
+
readonly key: "n";
|
|
104
|
+
readonly modifiers: "mod";
|
|
105
|
+
};
|
|
106
|
+
type BrowserReservedModShiftN = {
|
|
107
|
+
readonly key: "n";
|
|
108
|
+
readonly modifiers: "mod+shift";
|
|
109
|
+
};
|
|
110
|
+
type BrowserReservedModL = {
|
|
111
|
+
readonly key: "l";
|
|
112
|
+
readonly modifiers: "mod";
|
|
113
|
+
};
|
|
114
|
+
type BrowserReservedModD = {
|
|
115
|
+
readonly key: "d";
|
|
116
|
+
readonly modifiers: "mod";
|
|
117
|
+
};
|
|
118
|
+
type BrowserReservedModEquals = {
|
|
119
|
+
readonly key: "=";
|
|
120
|
+
readonly modifiers: "mod";
|
|
121
|
+
};
|
|
122
|
+
type BrowserReservedModMinus = {
|
|
123
|
+
readonly key: "-";
|
|
124
|
+
readonly modifiers: "mod";
|
|
125
|
+
};
|
|
126
|
+
type BrowserReservedMod0 = {
|
|
127
|
+
readonly key: "0";
|
|
128
|
+
readonly modifiers: "mod";
|
|
129
|
+
};
|
|
130
|
+
type BrowserReservedModP = {
|
|
131
|
+
readonly key: "p";
|
|
132
|
+
readonly modifiers: "mod";
|
|
133
|
+
};
|
|
134
|
+
type BrowserReservedModS = {
|
|
135
|
+
readonly key: "s";
|
|
136
|
+
readonly modifiers: "mod";
|
|
137
|
+
};
|
|
138
|
+
type BrowserReservedModO = {
|
|
139
|
+
readonly key: "o";
|
|
140
|
+
readonly modifiers: "mod";
|
|
141
|
+
};
|
|
142
|
+
type BrowserReservedModH = {
|
|
143
|
+
readonly key: "h";
|
|
144
|
+
readonly modifiers: "mod";
|
|
145
|
+
};
|
|
146
|
+
type BrowserReservedModJ = {
|
|
147
|
+
readonly key: "j";
|
|
148
|
+
readonly modifiers: "mod";
|
|
149
|
+
};
|
|
150
|
+
type BrowserReservedModQ = {
|
|
151
|
+
readonly key: "q";
|
|
152
|
+
readonly modifiers: "mod";
|
|
153
|
+
};
|
|
154
|
+
/** Union of all browser-reserved shortcuts */
|
|
155
|
+
type BrowserReservedShortcut = BrowserReservedModC | BrowserReservedModV | BrowserReservedModX | BrowserReservedModA | BrowserReservedModZ | BrowserReservedModShiftZ | BrowserReservedModY | BrowserReservedModF | BrowserReservedModG | BrowserReservedModShiftG | BrowserReservedModR | BrowserReservedModShiftR | BrowserReservedModT | BrowserReservedModW | BrowserReservedModShiftT | BrowserReservedModN | BrowserReservedModShiftN | BrowserReservedModL | BrowserReservedModD | BrowserReservedModEquals | BrowserReservedModMinus | BrowserReservedMod0 | BrowserReservedModP | BrowserReservedModS | BrowserReservedModO | BrowserReservedModH | BrowserReservedModJ | BrowserReservedModQ;
|
|
156
|
+
/** Maps each browser-reserved shortcut to its feature description */
|
|
157
|
+
type BrowserShortcutFeature<TShortcut> = TShortcut extends BrowserReservedModC ? "Copy" : TShortcut extends BrowserReservedModV ? "Paste" : TShortcut extends BrowserReservedModX ? "Cut" : TShortcut extends BrowserReservedModA ? "Select All" : TShortcut extends BrowserReservedModZ ? "Undo" : TShortcut extends BrowserReservedModShiftZ ? "Redo (Mac)" : TShortcut extends BrowserReservedModY ? "Redo (Windows/Linux)" : TShortcut extends BrowserReservedModF ? "Find on page" : TShortcut extends BrowserReservedModG ? "Find next" : TShortcut extends BrowserReservedModShiftG ? "Find previous" : TShortcut extends BrowserReservedModR ? "Reload page" : TShortcut extends BrowserReservedModShiftR ? "Hard reload" : TShortcut extends BrowserReservedModT ? "New tab" : TShortcut extends BrowserReservedModW ? "Close tab" : TShortcut extends BrowserReservedModShiftT ? "Reopen closed tab" : TShortcut extends BrowserReservedModN ? "New window" : TShortcut extends BrowserReservedModShiftN ? "New incognito/private window" : TShortcut extends BrowserReservedModL ? "Focus address bar" : TShortcut extends BrowserReservedModD ? "Bookmark page" : TShortcut extends BrowserReservedModEquals ? "Zoom in" : TShortcut extends BrowserReservedModMinus ? "Zoom out" : TShortcut extends BrowserReservedMod0 ? "Reset zoom" : TShortcut extends BrowserReservedModP ? "Print" : TShortcut extends BrowserReservedModS ? "Save page" : TShortcut extends BrowserReservedModO ? "Open file" : TShortcut extends BrowserReservedModH ? "History (Chrome) / Hide window (Mac)" : TShortcut extends BrowserReservedModJ ? "Downloads" : TShortcut extends BrowserReservedModQ ? "Quit application (Mac only)" : never;
|
|
158
|
+
/** Maps each browser-reserved shortcut to its key string */
|
|
159
|
+
type BrowserShortcutKeys<TShortcut> = TShortcut extends BrowserReservedModC ? "mod+c" : TShortcut extends BrowserReservedModV ? "mod+v" : TShortcut extends BrowserReservedModX ? "mod+x" : TShortcut extends BrowserReservedModA ? "mod+a" : TShortcut extends BrowserReservedModZ ? "mod+z" : TShortcut extends BrowserReservedModShiftZ ? "mod+shift+z" : TShortcut extends BrowserReservedModY ? "mod+y" : TShortcut extends BrowserReservedModF ? "mod+f" : TShortcut extends BrowserReservedModG ? "mod+g" : TShortcut extends BrowserReservedModShiftG ? "mod+shift+g" : TShortcut extends BrowserReservedModR ? "mod+r" : TShortcut extends BrowserReservedModShiftR ? "mod+shift+r" : TShortcut extends BrowserReservedModT ? "mod+t" : TShortcut extends BrowserReservedModW ? "mod+w" : TShortcut extends BrowserReservedModShiftT ? "mod+shift+t" : TShortcut extends BrowserReservedModN ? "mod+n" : TShortcut extends BrowserReservedModShiftN ? "mod+shift+n" : TShortcut extends BrowserReservedModL ? "mod+l" : TShortcut extends BrowserReservedModD ? "mod+d" : TShortcut extends BrowserReservedModEquals ? "mod+=" : TShortcut extends BrowserReservedModMinus ? "mod+-" : TShortcut extends BrowserReservedMod0 ? "mod+0" : TShortcut extends BrowserReservedModP ? "mod+p" : TShortcut extends BrowserReservedModS ? "mod+s" : TShortcut extends BrowserReservedModO ? "mod+o" : TShortcut extends BrowserReservedModH ? "mod+h" : TShortcut extends BrowserReservedModJ ? "mod+j" : TShortcut extends BrowserReservedModQ ? "mod+q" : never;
|
|
160
|
+
/** Maps each browser-reserved shortcut to its error message */
|
|
161
|
+
type BrowserReservedErrorMessage<TShortcut> = ReservedMessage<"Browser", BrowserShortcutKeys<TShortcut>, BrowserShortcutFeature<TShortcut>>;
|
|
162
|
+
/** Union of all reserved shortcuts (app + browser) */
|
|
163
|
+
type ReservedShortcut = AppReservedShortcut | BrowserReservedShortcut;
|
|
164
|
+
/**
|
|
165
|
+
* Type predicate that returns true if the shortcut is reserved.
|
|
166
|
+
* Reserved shortcuts cannot be used without explicit ts-expect-error.
|
|
167
|
+
*/
|
|
168
|
+
export type IsReservedShortcut<TShortcut extends ShortcutDefinition> = TShortcut extends ReservedShortcut ? true : false;
|
|
169
|
+
/** Maps any reserved shortcut to its specific error message */
|
|
170
|
+
type ReservedErrorMessage<TShortcut> = TShortcut extends AppReservedShortcut ? AppReservedErrorMessage<TShortcut> : TShortcut extends BrowserReservedShortcut ? BrowserReservedErrorMessage<TShortcut> : never;
|
|
171
|
+
/**
|
|
172
|
+
* Validates a shortcut parameter at the type level.
|
|
173
|
+
* Returns the shortcut type if valid, or a specific error message if reserved.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // This type-checks:
|
|
177
|
+
* const shortcut: ValidShortcutParam<{ key: "m"; modifiers: "mod" }> = { key: "m", modifiers: "mod" };
|
|
178
|
+
*
|
|
179
|
+
* // This fails type-checking with app-reserved error:
|
|
180
|
+
* const reserved: ValidShortcutParam<{ key: "k"; modifiers: "mod" }> = { key: "k", modifiers: "mod" };
|
|
181
|
+
* // Error: Type '...' is not assignable to type '"App reserved: mod+k is used for Global Search, which should not be overwritten"'
|
|
182
|
+
*
|
|
183
|
+
* // This fails type-checking with browser-reserved error:
|
|
184
|
+
* const browserReserved: ValidShortcutParam<{ key: "c"; modifiers: "mod" }> = { key: "c", modifiers: "mod" };
|
|
185
|
+
* // Error: Type '...' is not assignable to type '"Browser reserved: mod+c is used for Copy, which should not be overwritten"'
|
|
186
|
+
*/
|
|
187
|
+
export type ValidShortcutParam<TShortcut extends ShortcutDefinition> = TShortcut extends ReservedShortcut ? ReservedErrorMessage<TShortcut> : TShortcut;
|
|
188
|
+
export {};
|