Skip to content

Commit b90a75d

Browse files
Merge branch 'canary' of https://github.com/gridaco/grida
2 parents c52f3b7 + 316c5f7 commit b90a75d

File tree

3 files changed

+39
-16
lines changed

3 files changed

+39
-16
lines changed

editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import { SearchIcon } from "lucide-react";
2525

2626
interface KeyboardShortcutRowProps {
2727
action: {
28+
/**
29+
* Stable unique identifier for the action (derived from the `actions` registry key).
30+
*/
31+
id?: string;
2832
name: string;
2933
description: string;
3034
command: string;
@@ -119,12 +123,17 @@ export function KeyboardShortcuts() {
119123
const [searchQuery, setSearchQuery] = useState("");
120124

121125
const filteredActions = useMemo(() => {
126+
const actionsWithId = Object.entries(actions).map(([id, action]) => ({
127+
id,
128+
...action,
129+
}));
130+
122131
if (!searchQuery.trim()) {
123-
return Object.values(actions);
132+
return actionsWithId;
124133
}
125134

126135
const query = searchQuery.toLowerCase().trim();
127-
return Object.values(actions).filter(
136+
return actionsWithId.filter(
128137
(action) =>
129138
action.name.toLowerCase().includes(query) ||
130139
action.description.toLowerCase().includes(query) ||
@@ -175,9 +184,13 @@ export function KeyboardShortcuts() {
175184
No shortcuts found matching "{searchQuery}"
176185
</div>
177186
) : (
178-
filteredActions.map((action) => (
187+
filteredActions.map((action, index) => (
179188
<KeyboardShortcutRow
180-
key={action.command}
189+
key={
190+
action.id ??
191+
// Fallback: deterministic-ish unique string to avoid React key collisions
192+
`${action.command}:${action.name}:${action.description}:${index}`
193+
}
181194
action={action}
182195
selectedPlatform={selectedPlatform}
183196
/>

editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ function sequenceToString(
4848

4949
/**
5050
* Get the first keybinding as a string for an action
51-
* Returns empty string if not found
5251
*
53-
* @throws {Error} If actionId is not a valid action ID
52+
* Returns an empty string when the action has no resolved keybinding for the
53+
* requested platform (including when the `actionId` is unknown).
5454
*/
5555
export function keyboardShortcutText(
5656
actionId: UXHostActionId,
@@ -82,6 +82,9 @@ export function keyboardShortcutTextAll(
8282
/**
8383
* Get keyboard shortcut as a formatted string with separators between chunks
8484
* Useful for display in tooltips or settings
85+
*
86+
* Returns an empty string when the action has no resolved keybinding for the
87+
* requested platform (including when the `actionId` is unknown).
8588
*/
8689
export function keyboardShortcutTextFormatted(
8790
actionId: UXHostActionId,

editor/grida-canvas/keybinding.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ function isApplePlatform(): boolean {
200200
* mostly to determine cmdctrl key (and for cases that keybindings are fundamentally different, e.g. ctrl+c being color picker on mac, but copy on windows/linux)
201201
*/
202202
export function getKeyboardOS(): "mac" | "windows" | "linux" {
203+
// SSR / non-browser safety: `navigator` is not defined in Node.js environments.
204+
// Pick a reasonable default for headless contexts.
205+
if (typeof navigator === "undefined" || !navigator.platform) {
206+
return "linux";
207+
}
203208
if (isApplePlatform()) return "mac";
204209
const platform = navigator.platform.toLowerCase();
205210
if (platform.includes("win")) return "windows";
@@ -385,18 +390,20 @@ export function uikbdk(
385390
): string {
386391
const targetPlatform = platform || getKeyboardOS();
387392

388-
// Check if it's a modifier bitmask (M enum values are small bitmasks: 1, 2, 4, 8, 16)
389-
// KeyCodes are enum values that start from a much higher number
390-
// We check if it's a valid modifier by trying to resolve it
391-
// If it resolves to modifier keys, it's a modifier; otherwise treat as KeyCode
392-
const resolvedMods = resolveMods(key, targetPlatform);
393+
// Only treat *single* modifier constants as modifiers.
394+
// Do NOT try to infer modifiers from bit overlap, as some KeyCode values can overlap by chance.
393395
if (
394-
resolvedMods.length > 0 &&
395-
key & (M.Ctrl | M.Shift | M.Alt | M.Meta | M.CtrlCmd)
396+
key === M.Ctrl ||
397+
key === M.Shift ||
398+
key === M.Alt ||
399+
key === M.Meta ||
400+
key === M.CtrlCmd
396401
) {
397-
// It's a modifier bitmask - return the first resolved key's label
398-
// In practice, most single modifiers resolve to a single key
399-
return keycodeToPlatformUILabel(resolvedMods[0], targetPlatform);
402+
const resolvedMods = resolveMods(key, targetPlatform);
403+
// In practice, single modifiers resolve to a single key (Ctrl/Cmd/Shift/Alt).
404+
if (resolvedMods.length > 0) {
405+
return keycodeToPlatformUILabel(resolvedMods[0], targetPlatform);
406+
}
400407
}
401408

402409
// It's a KeyCode

0 commit comments

Comments
 (0)