@tanstack/hotkeys 0.22.1 → 0.23.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.
@@ -0,0 +1,21 @@
1
+ //#region src/_server.ts
2
+ const SERVER_AGENT = /^(?:Node\.js\/|Deno\/|Bun\/|Cloudflare-Workers)/;
3
+ /** Whether this runs on a server: no `navigator`, or a server runtime's. */
4
+ function isServerRuntime() {
5
+ return typeof navigator === "undefined" || SERVER_AGENT.test(String(navigator.userAgent));
6
+ }
7
+ let warned = false;
8
+ /**
9
+ * Warns once, in development, when a label's platform is detected during
10
+ * server rendering. The server then renders 'linux' labels, which differ for
11
+ * users on macOS and Windows after hydration.
12
+ */
13
+ function warnServerPlatform() {
14
+ if (process.env.NODE_ENV !== "production" && !warned && isServerRuntime()) {
15
+ warned = true;
16
+ console.warn("[TanStack Hotkeys] A hotkey label was formatted during server rendering without a platform, so it uses 'linux'. Mac and Windows users get a different label after hydration. Read the platform from the request with `detectPlatform(userAgent)`, set it as the `platform` of your hotkeys provider or scope, and format labels with your adapter's formatter, such as `useFormatForDisplay`. Or render the labels only in the browser. See the server rendering section of the Displaying Hotkeys guide.");
17
+ }
18
+ }
19
+
20
+ //#endregion
21
+ export { isServerRuntime, warnServerPlatform };
package/dist/adapter.d.ts CHANGED
@@ -2,6 +2,7 @@ import { Store, createStore } from "./_store.js";
2
2
  import { HotkeyCallback, HotkeyChord, HotkeySequence, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
3
3
  import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyOptions, HotkeySequenceOptions, Target } from "./hotkey-manager.js";
4
4
  import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
5
+ import { HotkeyPlatform } from "./platform.js";
5
6
  import { definedOptions, withDefaults } from "./_registration.js";
6
7
  import { createAppHotkeysStore } from "@tanstack/hotkeys";
7
8
  //#region src/adapter.d.ts
@@ -147,11 +148,20 @@ export interface HotkeysProviderScopeProps {
147
148
  /**
148
149
  * Merges a provider's default options over its parent's. A provider with a
149
150
  * `scope` sets `hotkey.scope`, so hotkeys registered inside it use that scope.
151
+ * A provider's `platform` is inherited, and is the default platform for its
152
+ * hotkeys and recorders; `hotkey.platform` and `hotkeyRecorder.platform` win.
150
153
  */
151
154
  export declare function mergeHotkeysProviderDefaults<T extends {
155
+ platform?: HotkeyPlatform;
152
156
  hotkey?: object;
153
157
  hotkeyRecorder?: object;
154
158
  }>(parent: T | undefined, own: T | undefined, scope: string | undefined): T;
159
+ /**
160
+ * The platform to render with during server rendering and hydration when no
161
+ * provider sets one, for a `useSyncExternalStore` server snapshot. Warns once
162
+ * in development on the server.
163
+ */
164
+ export declare function getServerPlatformSnapshot(): HotkeyPlatform;
155
165
  /**
156
166
  * A key for a provider's scope activation, or `null` when the scope should not
157
167
  * be active. Equal keys mean the same activation, so adapters reactivate only
package/dist/adapter.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { warnServerPlatform } from "./_server.js";
1
2
  import { chordHasKey, defaultHotkeyOptions, definedOptions, withDefaults } from "./_registration.js";
2
3
  import { createStore } from "./_store.js";
3
4
  import { createAppHotkeysStore, detectPlatform, getAppHotkeyRegistrationOptions, getHotkeyManager, isAppHotkey, isHotkeySequence, normalizeHotkey, parseHotkey, resolveAppHotkey } from "@tanstack/hotkeys";
@@ -122,17 +123,31 @@ function subscribePublishedAppHotkeys(onChange) {
122
123
  /**
123
124
  * Merges a provider's default options over its parent's. A provider with a
124
125
  * `scope` sets `hotkey.scope`, so hotkeys registered inside it use that scope.
126
+ * A provider's `platform` is inherited, and is the default platform for its
127
+ * hotkeys and recorders; `hotkey.platform` and `hotkeyRecorder.platform` win.
125
128
  */
126
129
  function mergeHotkeysProviderDefaults(parent, own, scope) {
130
+ const parentPlatform = { platform: parent?.platform };
131
+ const ownPlatform = { platform: own?.platform };
127
132
  return {
133
+ ...definedOptions(parentPlatform, ownPlatform),
128
134
  hotkey: {
129
- ...definedOptions(parent?.hotkey, own?.hotkey),
135
+ ...definedOptions(parentPlatform, parent?.hotkey, ownPlatform, own?.hotkey),
130
136
  ...scope && { scope }
131
137
  },
132
- hotkeyRecorder: definedOptions(parent?.hotkeyRecorder, own?.hotkeyRecorder)
138
+ hotkeyRecorder: definedOptions(parentPlatform, parent?.hotkeyRecorder, ownPlatform, own?.hotkeyRecorder)
133
139
  };
134
140
  }
135
141
  /**
142
+ * The platform to render with during server rendering and hydration when no
143
+ * provider sets one, for a `useSyncExternalStore` server snapshot. Warns once
144
+ * in development on the server.
145
+ */
146
+ function getServerPlatformSnapshot() {
147
+ warnServerPlatform();
148
+ return "linux";
149
+ }
150
+ /**
136
151
  * A key for a provider's scope activation, or `null` when the scope should not
137
152
  * be active. Equal keys mean the same activation, so adapters reactivate only
138
153
  * when the key changes, not when an inline `exclusive` object is recreated.
@@ -159,4 +174,4 @@ function activateHotkeysProviderScope(key) {
159
174
  }
160
175
 
161
176
  //#endregion
162
- export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, definedOptions, getHotkeysProviderScopeKey, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys, withDefaults };
177
+ export { activateHotkeysProviderScope, createAppHotkeysStore, createHotkeyBindings, createStore, definedOptions, getHotkeysProviderScopeKey, getServerPlatformSnapshot, mergeHotkeysProviderDefaults, prepareHotkeyDefinitions, subscribePublishedAppHotkeys, withDefaults };
package/dist/format.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
2
2
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
3
+ import { warnServerPlatform } from "./_server.js";
3
4
  import { detectPlatform } from "./platform.js";
4
5
  import { assertLogicalKey, codeToLogicalKey, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
5
6
  import { normalizeHotkey } from "./parse.js";
@@ -23,16 +24,21 @@ function formatForDisplay(hotkey, options = {}) {
23
24
  const parts = formatChordParts(hotkey, options);
24
25
  return options.parts ? parts : parts.join(chordSeparator(options));
25
26
  }
27
+ /** Detects the platform for a label, warning in development on a server. */
28
+ function detectDisplayPlatform() {
29
+ warnServerPlatform();
30
+ return detectPlatform();
31
+ }
26
32
  function modifierSymbolsEnabled(options) {
27
33
  const symbols = options.useSymbols ?? true;
28
34
  return typeof symbols === "boolean" ? symbols : symbols.modifiers ?? true;
29
35
  }
30
36
  function chordSeparator(options) {
31
- const platform = options.platform ?? detectPlatform();
37
+ const platform = options.platform ?? detectDisplayPlatform();
32
38
  return options.separatorToken ?? (platform === "mac" && modifierSymbolsEnabled(options) ? " " : "+");
33
39
  }
34
40
  function formatChordParts(hotkey, options) {
35
- const platform = options.platform ?? detectPlatform();
41
+ const platform = options.platform ?? detectDisplayPlatform();
36
42
  const symbols = options.useSymbols ?? true;
37
43
  const modifierSymbols = modifierSymbolsEnabled(options);
38
44
  const keySymbols = typeof symbols === "boolean" ? symbols : symbols.keys ?? true;
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterK
2
2
  import { ConflictBehavior, FourModifierHotkeyChord, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyMeta, HotkeySequence, HotkeySequenceCallbackContext, ParsedHotkeyChord, ParsedModifiers, RawHotkeyChord, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, SingleModifierHotkeyChord, ThreeModifierHotkeyChord, TwoModifierHotkeyChord } from "./hotkey.types.js";
3
3
  import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
4
4
  import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
5
+ import { HotkeyPlatform, detectPlatform, resolveModifier } from "./platform.js";
5
6
  import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
6
7
  import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
7
- import { detectPlatform, resolveModifier } from "./platform.js";
8
8
  import { DisplayHotkey, DisplayHotkeyChord, FormatDisplayOptions, formatForDisplay, formatHotkey } from "./format.js";
9
9
  import { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, parseHotkey, parseKeyboardEvent, simplifyHotkey } from "./parse.js";
10
10
  import { ValidatableHotkey, ValidationResult, assertValidHotkey, checkHotkey, validateHotkey } from "./validate.js";
@@ -14,4 +14,4 @@ import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./co
14
14
  import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
15
15
  import { HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./hotkey-recorder.js";
16
16
  import { createAppHotkeysStore } from "./_app-hotkeys-factory.js";
17
- export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createAppHotkeysStore, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
17
+ export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyPlatform, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createAppHotkeysStore, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
package/dist/match.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { HotkeyCallback, HotkeyChord, HotkeyChordCallbackContext, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeySequence } from "./hotkey.types.js";
2
+ import { HotkeyPlatform } from "./platform.js";
2
3
  //#region src/match.d.ts
3
4
  export interface KeyboardEventMatch {
4
5
  matched: boolean;
@@ -96,7 +97,7 @@ export declare function createMultiHotkeyChordHandler(handlers: MultiHotkeyChord
96
97
  * Aliases resolve on the supplied platform. A physical code and a logical key
97
98
  * remain distinct even when one keyboard event could match both.
98
99
  */
99
- export declare function areHotkeysEqual(left: RegisterableHotkey, right: RegisterableHotkey, platform?: "mac" | "windows" | "linux"): boolean;
100
+ export declare function areHotkeysEqual(left: RegisterableHotkey, right: RegisterableHotkey, platform?: HotkeyPlatform): boolean;
100
101
  /** Default timeout between sequence steps (in milliseconds). */
101
102
  export declare const DEFAULT_SEQUENCE_TIMEOUT = 1000;
102
103
  /**
@@ -1,21 +1,33 @@
1
1
  import { CanonicalModifier } from "./key.types.js";
2
2
  //#region src/platform.d.ts
3
+ /** A platform for resolving `Mod` and formatting labels. */
4
+ export type HotkeyPlatform = 'mac' | 'windows' | 'linux';
3
5
  /**
4
- * Detects the current platform based on browser navigator properties.
6
+ * Detects a platform from a user agent string, or from the browser's
7
+ * `navigator` when called without one.
5
8
  *
6
9
  * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
7
- * Control elsewhere) and for platform-specific hotkey formatting.
10
+ * Control elsewhere) and for platform-specific hotkey formatting. During server
11
+ * rendering, pass the request's `User-Agent` header to detect the user's
12
+ * platform instead of the server's.
8
13
  *
14
+ * @param userAgent - A user agent string, such as a request's `User-Agent` header.
15
+ * When omitted or `null` (a missing header), reads `navigator`.
9
16
  * @returns The detected platform: 'mac', 'windows', or 'linux'
10
- * @remarks Defaults to 'linux' in SSR environments where navigator is undefined
17
+ * @remarks Without a user agent, returns 'linux' on a server runtime (no
18
+ * `navigator`, or Node, Deno, Bun, or Cloudflare Workers), where `navigator`
19
+ * describes the server.
11
20
  *
12
21
  * @example
13
22
  * ```ts
14
23
  * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
15
24
  * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
25
+ *
26
+ * // On the server
27
+ * detectPlatform(request.headers.get('user-agent'))
16
28
  * ```
17
29
  */
18
- export declare function detectPlatform(): 'mac' | 'windows' | 'linux';
30
+ export declare function detectPlatform(userAgent?: string | null): HotkeyPlatform;
19
31
  /**
20
32
  * Resolves the platform-adaptive 'Mod' modifier to the appropriate canonical modifier.
21
33
  *
package/dist/platform.js CHANGED
@@ -1,25 +1,42 @@
1
+ import { isServerRuntime } from "./_server.js";
2
+
1
3
  //#region src/platform.ts
2
4
  /**
3
- * Detects the current platform based on browser navigator properties.
5
+ * Detects a platform from a user agent string, or from the browser's
6
+ * `navigator` when called without one.
4
7
  *
5
8
  * Used internally to resolve platform-adaptive modifiers like 'Mod' (Command on Mac,
6
- * Control elsewhere) and for platform-specific hotkey formatting.
9
+ * Control elsewhere) and for platform-specific hotkey formatting. During server
10
+ * rendering, pass the request's `User-Agent` header to detect the user's
11
+ * platform instead of the server's.
7
12
  *
13
+ * @param userAgent - A user agent string, such as a request's `User-Agent` header.
14
+ * When omitted or `null` (a missing header), reads `navigator`.
8
15
  * @returns The detected platform: 'mac', 'windows', or 'linux'
9
- * @remarks Defaults to 'linux' in SSR environments where navigator is undefined
16
+ * @remarks Without a user agent, returns 'linux' on a server runtime (no
17
+ * `navigator`, or Node, Deno, Bun, or Cloudflare Workers), where `navigator`
18
+ * describes the server.
10
19
  *
11
20
  * @example
12
21
  * ```ts
13
22
  * const platform = detectPlatform() // 'mac' | 'windows' | 'linux'
14
23
  * const modifier = resolveModifier('Mod', platform) // 'Meta' on Mac, 'Control' elsewhere
24
+ *
25
+ * // On the server
26
+ * detectPlatform(request.headers.get('user-agent'))
15
27
  * ```
16
28
  */
17
- function detectPlatform() {
18
- if (typeof navigator === "undefined") return "linux";
19
- const platform = navigator.platform?.toLowerCase() ?? "";
20
- const userAgent = navigator.userAgent?.toLowerCase() ?? "";
21
- if (platform.includes("mac") || userAgent.includes("mac")) return "mac";
22
- if (platform.includes("win") || userAgent.includes("win")) return "windows";
29
+ function detectPlatform(userAgent) {
30
+ let platform = "";
31
+ let agent = userAgent;
32
+ if (agent == null) {
33
+ if (isServerRuntime()) return "linux";
34
+ platform = navigator.platform?.toLowerCase() ?? "";
35
+ agent = navigator.userAgent ?? "";
36
+ }
37
+ agent = agent.toLowerCase();
38
+ if (platform.includes("mac") || agent.includes("mac")) return "mac";
39
+ if (platform.includes("win") || agent.includes("win")) return "windows";
23
40
  return "linux";
24
41
  }
25
42
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -66,6 +66,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
66
66
  - **Single-character shortcuts need an off switch.** For WCAG 2.1.4, `K`, `?`, and `['G', 'H']` need a user setting passed as `enabled`, or an app hotkey users can remap.
67
67
  - **react-hotkeys-hook names don't carry over.** `useHotkeys` and `HotkeysProvider` share names, not signatures: there's no dependency array, `preventDefault` is on by default, and sequences are arrays. Remove the old library, so two libraries don't handle the same keys.
68
68
  - **Install only the adapter.** A separately installed `@tanstack/hotkeys` at another version is a second copy, and scopes don't apply across copies.
69
+ - **Server-rendered labels need the user's platform.** On a server, `formatForDisplay` renders Linux labels, because the server can't see the user's OS. Read the request's `User-Agent` header, pass `detectPlatform(userAgent)` as the platform of the adapter's provider or scope, and format labels with the adapter's formatter, such as `useFormatForDisplay`. Those pages then depend on the user agent, so keep them out of shared caches or add the platform to the cache key.
69
70
 
70
71
  When writing tests for hotkeys, read [testing](references/testing.md).
71
72