Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
79475b7
WIP: basic ui logic command bar
Gianthard-cyh Jan 31, 2026
727cc82
WIP(command-bar): trigger handlers
Gianthard-cyh Jan 31, 2026
1fbab7e
WIP(command-bar): trrigger feedback
Gianthard-cyh Jan 31, 2026
6d44e65
WIP(command-bar): command prompt
Gianthard-cyh Jan 31, 2026
7ada4b1
Update app/components/CommandBar.vue
Gianthard-cyh Jan 31, 2026
cc11944
WIP(command-bar): basic api
Gianthard-cyh Jan 31, 2026
4d8e6aa
WIP(command-bar): scoped command example
Gianthard-cyh Jan 31, 2026
95639ea
feat(command-bar): context api (input, select, etc.)
Gianthard-cyh Feb 1, 2026
270c386
chore: revert [...package].vue
Gianthard-cyh Feb 1, 2026
d92cd1b
Merge branch 'main' into main
Gianthard-cyh Feb 1, 2026
e2b331e
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 1, 2026
af1a50a
fix(test): remove unused variable
Gianthard-cyh Feb 1, 2026
f87acf9
Merge branch 'main' of github.com:Gianthard-cyh/npmx.dev
Gianthard-cyh Feb 1, 2026
6020a50
chore: remove unused variable
Gianthard-cyh Feb 1, 2026
516baeb
chore: apply suggestions from copilot
Gianthard-cyh Feb 1, 2026
8f087d4
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 1, 2026
efce157
fix: remove useI18n() to prevent error
Gianthard-cyh Feb 1, 2026
acc860e
Merge branch 'main' of github.com:Gianthard-cyh/npmx.dev
Gianthard-cyh Feb 1, 2026
24a56be
Merge branch 'main' into Gianthard-cyh/main
danielroe Feb 1, 2026
0c69097
fix(test): ignore a11y due to the need of full app context.
Gianthard-cyh Feb 1, 2026
527cdd4
test: add a11y test to CommandBar.vue
Gianthard-cyh Feb 1, 2026
f587200
Merge remote-tracking branch 'upstream/main'
Gianthard-cyh Feb 6, 2026
604dfe0
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const localeMap = locales.value.reduce(
{} as Record<string, Directions>,
)

const commandBarRef = useTemplateRef('commandBarRef')

useHead({
htmlAttrs: {
lang: () => locale.value,
Expand All @@ -37,6 +39,11 @@ if (import.meta.server) {
function handleGlobalKeydown(e: KeyboardEvent) {
const target = e.target as HTMLElement

if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
commandBarRef.value?.toggle()
}

const isEditableTarget =
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable

Expand Down Expand Up @@ -69,6 +76,7 @@ if (import.meta.client) {
<template>
<div class="min-h-screen flex flex-col bg-bg text-fg">
<a href="#main-content" class="skip-link font-mono">{{ $t('common.skip_link') }}</a>
<CommandBar ref="commandBarRef" />

<AppHeader :show-logo="!isHomepage" />

Expand Down
177 changes: 177 additions & 0 deletions app/components/CommandBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<template>
<Transition name="fade">
<div
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm"
v-show="show"
>
<div
class="cmdbar-container flex items-center justify-center border border-border shadow-lg rounded-xl bg-bg p2 flex-col gap-2"
>
Comment on lines +4 to +7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clicking the backdrop does not close the modal.

Users expect that clicking outside a modal dialogue dismisses it. The overlay div has no click handler. Without this, keyboard-only close (Escape) is the only way out, and pointer users are left stranded.

Proposed fix
       class="fixed inset-0 z-[1000] flex items-start justify-center bg-black/50 backdrop-blur-sm"
       v-show="show"
+      `@click.self`="close"
     >

Using @click.self ensures only clicks directly on the backdrop (not bubbled from the inner container) trigger close.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
class="fixed inset-0 z-[1000] flex items-start justify-center bg-black/50 backdrop-blur-sm"
v-show="show"
>
<div
class="fixed inset-0 z-[1000] flex items-start justify-center bg-black/50 backdrop-blur-sm"
v-show="show"
`@click.self`="close"
>

<label for="command-input" class="sr-only">command-input</label>
Comment thread
Gianthard-cyh marked this conversation as resolved.
Outdated

<search class="relative w-xl h-12 flex items-center">
<span class="absolute inset-is-4 text-fg-subtle font-mono pointer-events-none"> > </span>
<input
type="text"
label="Enter command..."
v-model="inputVal"
id="command-input"
ref="inputRef"
class="w-full h-full px-4 pl-8 text-fg outline-none bg-bg-subtle border border-border rounded-md"
placeholder="Enter command..."
@keydown="handleKeydown"
/>
</search>

<div class="w-xl h-lg overflow-auto">
<div
v-for="item in filteredCmdList"
:key="item.id"
class="flex-col items-center justify-between px-4 py-2 not-first:mt-2 hover:bg-bg-elevated select-none cursor-pointer rounded-md transition"
:class="{
'bg-bg-subtle': item.id === selected,
'trigger-anim': item.id === triggeringId,
}"
Comment thread
Gianthard-cyh marked this conversation as resolved.
Outdated
@click="triggerCommand(item.id)"
>
<div class="text-fg">{{ item.name }}</div>
<div class="text-fg-subtle text-sm">{{ item.description }}</div>
</div>
</div>
</div>
</div>
</Transition>
</template>

<script setup lang="ts">
const { commands } = useCommandRegistry()

const selected = shallowRef(commands.value[0]?.id || '')
const inputVal = shallowRef('')
const show = shallowRef(false)
const triggeringId = shallowRef('')
const inputRef = useTemplateRef('inputRef')

const { focused: inputFocused } = useFocus(inputRef)

const filteredCmdList = computed(() => {
if (!inputVal.value) {
return commands.value
}
const filter = inputVal.value.trim().toLowerCase()
return commands.value.filter(
item =>
item.name.toLowerCase().includes(filter) ||
item.description?.toLowerCase().includes(filter) ||
item.id.includes(filter),
)
})
Comment on lines +93 to +110
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

filteredCmdList accesses .name / .description on any items without guards.

In SELECT mode, view.value.items is typed any[], so item.name may not exist. If a select handler provides items without a name field, this will render undefined and toLowerCase() will throw at runtime (Line 106).

Either tighten the item type (preferred) or add a guard:

Defensive guard (if typing is deferred)
   return list.filter(
-    (item: any) =>
-      item.name.toLowerCase().includes(filter) ||
-      item.description?.toLowerCase().includes(filter) ||
+    (item: { id: string; name?: string; description?: string }) =>
+      item.name?.toLowerCase().includes(filter) ||
+      item.description?.toLowerCase().includes(filter) ||
       item.id.includes(filter),
   )


watch(
() => filteredCmdList.value,
newVal => {
if (newVal.length) {
selected.value = newVal[0]?.id || ''
}
},
)

function focusInput() {
inputFocused.value = true
}

function open() {
inputVal.value = ''
selected.value = commands.value[0]?.id || ''
show.value = true
nextTick(focusInput)
}

function close() {
inputVal.value = ''
selected.value = commands.value[0]?.id || ''
show.value = false
}

function toggle() {
if (show.value) {
close()
} else {
open()
}
}

function triggerCommand(id: string) {
const selectedItem = filteredCmdList.value.find(item => item.id === id)
selectedItem?.handler?.({} as CommandContext)
triggeringId.value = id
setTimeout(() => {
close()
triggeringId.value = ''
}, 100)
}
Comment on lines +147 to +167
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handler result is fire-and-forget — unhandled rejections will be silent.

selectedItem?.handler?.(cmdCtx) returns a Promise<void> but is neither awaited nor .catch()-ed. If a handler throws after the ctx.input() / ctx.select() call, the rejection is swallowed. At minimum, add a .catch() to log/surface the error.

Proposed fix
-    selectedItem?.handler?.(cmdCtx)
+    selectedItem?.handler?.(cmdCtx).catch((err: unknown) => {
+      console.error(`[CommandBar] handler "${id}" failed:`, err)
+    })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function onTrigger(id: string) {
triggeringId.value = id
if (view.value.type === 'ROOT') {
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
selectedItem?.handler?.(cmdCtx)
setTimeout(() => {
triggeringId.value = ''
if (view.value.type === 'ROOT') {
close()
}
}, 100)
} else if (view.value.type === 'INPUT') {
view.value.resolve(inputVal.value)
close()
} else if (view.value.type === 'SELECT') {
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
view.value.resolve(selectedItem)
close()
}
}
function onTrigger(id: string) {
triggeringId.value = id
if (view.value.type === 'ROOT') {
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
selectedItem?.handler?.(cmdCtx)?.catch((err: unknown) => {
console.error(`[CommandBar] handler "${id}" failed:`, err)
})
setTimeout(() => {
triggeringId.value = ''
if (view.value.type === 'ROOT') {
close()
}
}, 100)
} else if (view.value.type === 'INPUT') {
view.value.resolve(inputVal.value)
close()
} else if (view.value.type === 'SELECT') {
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
view.value.resolve(selectedItem)
close()
}
}


const handleKeydown = useThrottleFn((e: KeyboardEvent) => {
if (!filteredCmdList.value.length) return

const currentIndex = filteredCmdList.value.findIndex(item => item.id === selected.value)

if (e.key === 'ArrowDown') {
e.preventDefault()
const nextIndex = (currentIndex + 1) % filteredCmdList.value.length
selected.value = filteredCmdList.value[nextIndex]?.id || ''
} else if (e.key === 'ArrowUp') {
e.preventDefault()
const prevIndex =
(currentIndex - 1 + filteredCmdList.value.length) % filteredCmdList.value.length
Comment thread
Gianthard-cyh marked this conversation as resolved.
selected.value = filteredCmdList.value[prevIndex]?.id || ''
} else if (e.key === 'Enter') {
e.preventDefault()
triggerCommand(selected.value)
} else if (e.key === 'Escape') {
e.preventDefault()
close()
}
}, 50)
Comment on lines +169 to +199
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

useThrottleFn at 50 ms throttles all keystrokes, including typing.

Because handleKeydown is the sole @keydown handler on the text input, every character the user types is also throttled to 50 ms. On fast typists this may drop characters or feel laggy. The throttle is presumably intended only for arrow-key navigation. Consider either:

  • Moving the throttle inside the arrow/Enter branches only, or
  • Returning early for printable characters before the throttle takes effect.


defineExpose({
open,
close,
toggle,
})
</script>

<style scoped>
.fade-enter-active {
transition: all 0.05s ease-out;
}

.fade-leave-active {
transition: all 0.1s ease-in;
}

.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-10px);
}

@keyframes trigger-pulse {
0% {
transform: scale(1);
}

50% {
transform: scale(0.96);
background-color: var(--bg-elevated);
}

100% {
transform: scale(1);
}
}

.trigger-anim {
animation: trigger-pulse 0.1s ease-in-out;
}
</style>
81 changes: 81 additions & 0 deletions app/composables/useCommandRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { computed } from 'vue'

export interface Command {
id: string
name: string
description: string | undefined
handler?: (ctx: CommandContext) => Promise<void>
}

export interface CommandContext {
input: (options: CommandInputOptions) => Promise<string | undefined>
select: <T>(options: CommandSelectOptions<T>) => Promise<T | undefined>
}
Comment on lines +10 to +13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

CommandContext promises can dangle if the modal is closed without resolution.

input() and select() return Promise<string | undefined> / Promise<T | undefined>, but the CommandBar's close() does not resolve these promises — it simply resets the view state. Any handler await-ing ctx.input(...) will hang indefinitely if the user presses Escape or clicks the backdrop.

Consider resolving with undefined when the modal closes (e.g., store the pending resolve in a ref and call resolve(undefined) in close()).


export interface CommandInputOptions {
prompt: string
}

export interface CommandSelectOptions<T> {
prompt: string
items: T[]
}

/**
* Composable for global command registry.
* @public
*/
export const useCommandRegistry = () => {
const commands = useState<Map<string, Command>>('commands', () => new Map())

const register = (command: Command) => {
const serverCommand = {
...command,
handler: undefined,
}
if (import.meta.server) {
commands.value.set(command.id, serverCommand)
} else {
commands.value.set(command.id, command)
}
return () => {
commands.value.delete(command.id)
Comment thread
Gianthard-cyh marked this conversation as resolved.
}
}

return {
register,
commands: computed(() => Array.from(commands.value.values())),
}
}
Comment on lines +28 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Nuxt 3 useState Map serialization SSR hydration

💡 Result:

In Nuxt 3, useState() is persisted from SSR → client by serializing its value into the Nuxt payload. By default, this means you must store JSON-serializable data (plain objects/arrays/primitives). Putting a Map in useState can therefore fail (e.g., “Cannot stringify arbitrary non‑POJOs”) or hydrate inconsistently. [1][2]

Fix options

  1. Store a serializable shape, rebuild the Map
// SSR-safe
const entries = useState<[string, number][]>('myMapEntries', () => [])
const myMap = computed(() => new Map(entries.value))
  1. Enable rich payload serialization (supports Map/Set/Date/etc.)
    Nuxt added an experimental option that supports richer JS types in the SSR payload, including Map and Set: [3]
// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    renderJsonPayloads: true
  }
})
  1. Custom payload serializer/deserializer
    If you need custom class-like types (or want explicit control), Nuxt recommends adding a custom payload serializer/deserializer via definePayloadPlugin. [1]

Sources: Nuxt state management + JSON-serialization constraint for useState [2], useState troubleshooting + payload plugin mention [1], Nuxt payload enhancements (renderJsonPayloads supports Map/Set) [3].

[1] https://nuxt.com/docs/4.x/api/composables/use-state
[2] https://nuxt.com/docs/getting-started/state-management/
[3] https://dev.nuxt.com/blog/v3-4


🏁 Script executed:

# Check the actual file structure and content
fd useCommandRegistry.ts

Repository: npmx-dev/npmx.dev

Length of output: 98


🏁 Script executed:

# Look for the full file to understand context
cat -n $(fd useCommandRegistry.ts)

Repository: npmx-dev/npmx.dev

Length of output: 2435


🏁 Script executed:

# Check for payload plugins or SSR-related configuration
fd "payload" --type f | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Search for useState usage with Map or non-serializable objects
rg "useState.*Map|useState.*Set" -t ts -t tsx

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check for nuxt.config configuration
fd "nuxt.config" -t f

Repository: npmx-dev/npmx.dev

Length of output: 95


🏁 Script executed:

# Look at the nuxt config to see if experimental.renderJsonPayloads is enabled
cat -n $(fd "nuxt.config" -t f)

Repository: npmx-dev/npmx.dev

Length of output: 9998


🏁 Script executed:

# Check for any definePayloadPlugin usage
rg "definePayloadPlugin" -t ts -t tsx

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check Nuxt version in package.json to understand compatibility
cat package.json | jq '.dependencies | {nuxt, "@nuxt/framework"}' 2>/dev/null || rg '"nuxt"' package.json -A 1 -B 1

Repository: npmx-dev/npmx.dev

Length of output: 109


🏁 Script executed:

# Check how commands are used/accessed in the codebase to understand impact
rg "\.commands" --type ts -A 2 -B 2 | head -50

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Check how the commands are actually used to understand the impact better
rg "useCommandRegistry\|registerGlobalCommand\|registerScopedCommand" --type ts -B 2 -A 5 | head -80

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

# Search more broadly for usage patterns
rg "commands" app/composables/ --type ts -B 3 -A 3

Repository: npmx-dev/npmx.dev

Length of output: 3055


🏁 Script executed:

# Check if there are any payload plugins at all
find . -name "*payload*" -o -name "*serializ*" 2>/dev/null | grep -v node_modules | head -20

Repository: npmx-dev/npmx.dev

Length of output: 43


Replace Map with Record<string, Command> for SSR serialisation safety.

Nuxt's useState() serialises its payload as JSON when transferring server state to the client. Map is not JSON-serialisable by default and will cause hydration issues. Since experimental.renderJsonPayloads is not enabled in your Nuxt config, use a plain object instead.

Recommended fix — use a plain object
-  const commands = useState<Map<string, Command>>('commands', () => new Map())
+  const commands = useState<Record<string, Command>>('commands', () => ({}))

   const register = (command: Command) => {
     const serverCommand = {
       ...command,
       handler: undefined,
     }
     if (import.meta.server) {
-      commands.value.set(command.id, serverCommand)
+      commands.value[command.id] = serverCommand
     } else {
-      commands.value.set(command.id, command)
+      commands.value[command.id] = command
     }
     return () => {
-      commands.value.delete(command.id)
+      delete commands.value[command.id]
     }
   }

   return {
     register,
-    commands: computed(() => Array.from(commands.value.values())),
+    commands: computed(() => Object.values(commands.value)),
   }

Alternatively, you can enable experimental.renderJsonPayloads: true in nuxt.config.ts to support native Map serialisation (Nuxt 4.3.0 supports this).


/**
* Registers a global command.
* @public
*/
export const registerGlobalCommand = (command: Command) => {
const { register } = useCommandRegistry()
return register(command)
}

/**
* Registers a command bound to the current component's lifecycle.
*
* The command is automatically unregistered when the component unmounts.
* Use this to register commands that rely on local component state (context)
* via closure capture.
*
* @public
*/
export const registerScopedCommand = (command: Command) => {
const { register } = useCommandRegistry()
let unregister: () => void

onMounted(() => {
unregister = register(command)
})

onUnmounted(() => {
unregister()
})
Comment thread
Gianthard-cyh marked this conversation as resolved.
Comment on lines +70 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

unregister is used uninitialised if onUnmounted fires without a prior onMounted.

This is an edge case (e.g., SSR-only or <Suspense> fallback teardown), but calling unregister() when it is undefined would throw.

Defensive initialisation
   const { register } = useCommandRegistry()
-  let unregister: () => void
+  let unregister: (() => void) | undefined

   onMounted(() => {
     unregister = register(command)
   })

   onUnmounted(() => {
-    unregister()
+    unregister?.()
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const registerScopedCommand = (command: Command) => {
const { register } = useCommandRegistry()
let unregister: () => void
onMounted(() => {
unregister = register(command)
})
onUnmounted(() => {
unregister()
})
export const registerScopedCommand = (command: Command) => {
const { register } = useCommandRegistry()
let unregister: (() => void) | undefined
onMounted(() => {
unregister = register(command)
})
onUnmounted(() => {
unregister?.()
})

}
13 changes: 13 additions & 0 deletions app/pages/[...package].vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ definePageMeta({
alias: ['/package/:package(.*)*'],
})

registerScopedCommand({
id: 'package hello',
name: 'Hello',
description: 'Say hello to the package',
handler: async () => {
console.log(`Hello ${packageName.value}!`)
},
})

const router = useRouter()

const { packageName, requestedVersion, orgName } = usePackageRoute()
Expand Down Expand Up @@ -1071,18 +1080,22 @@ function handleClick(event: MouseEvent) {
grid-area: header;
overflow-x: hidden;
}

Comment thread
Gianthard-cyh marked this conversation as resolved.
.area-install {
grid-area: install;
overflow-x: hidden;
}

.area-vulns {
grid-area: vulns;
overflow-x: hidden;
}

.area-readme {
grid-area: readme;
overflow-x: hidden;
}

.area-sidebar {
grid-area: sidebar;
}
Expand Down
11 changes: 11 additions & 0 deletions app/plugins/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default defineNuxtPlugin(() => {
const { register } = useCommandRegistry()
register({
id: 'npmx:hello',
name: 'Hello',
description: 'Say hello to npmx',
handler: async () => {
console.log('Hello npmx!')
},
})
Comment on lines +7 to +23
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Command type definition and the command registry implementation
rg -n 'interface Command|type Command' --type=ts -A10

Repository: npmx-dev/npmx.dev

Length of output: 1803


🏁 Script executed:

#!/bin/bash
# Read the entire useCommandRegistry.ts file to see the register() implementation
cat -n app/composables/useCommandRegistry.ts

Repository: npmx-dev/npmx.dev

Length of output: 2435


🏁 Script executed:

#!/bin/bash
# Read the entire commands.ts plugin file
cat -n app/plugins/commands.ts

Repository: npmx-dev/npmx.dev

Length of output: 799


Command name and description won't update when the user changes locale.

The translation strings t('command.package_search') and t('command.package_search_desc') are evaluated once at plugin initialisation and stored as plain strings in the command registry. The Command interface defines name and description as static string types (not reactive), so if the user switches language, the command bar will continue displaying the original translations.

To fix this, either modify the Command interface to support ComputedRef<string> values for name and description, or add a watch listener to re-register the command when the locale changes.

})
Loading