feat: command palette (#200)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
QiroNT 2022-11-29 16:15:05 +08:00 committed by GitHub
parent 07622e9606
commit 59802f0896
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 911 additions and 101 deletions

View file

@ -1,11 +1,13 @@
<script setup lang="ts">
import type { Account } from 'masto'
const { account } = defineProps<{
const { account, command } = defineProps<{
account: Account
command?: boolean
}>()
const isSelf = $computed(() => currentUser.value?.account.id === account.id)
const enable = $computed(() => !isSelf && currentUser.value)
let relationship = $(useRelationship(account))
async function toggleFollow() {
@ -18,11 +20,24 @@ async function toggleFollow() {
relationship!.following = !relationship!.following
}
}
useCommand({
scope: 'Actions',
order: -2,
visible: () => command && enable,
name: () => `${relationship?.following ? 'Unfollow' : 'Follow'} ${getShortHandle(account)}`,
icon: 'i-ri:star-line',
onActivate: () => toggleFollow(),
})
</script>
<template>
<button
v-if="!isSelf && currentUser"
v-if="enable"
flex gap-1 items-center h-fit rounded hover="op100 text-white b-orange" group btn-base
:disabled="relationship?.requested"
@click="toggleFollow"

View file

@ -3,6 +3,7 @@ import type { Account, Field } from 'masto'
const { account } = defineProps<{
account: Account
command?: boolean
}>()
const createdAt = $(useFormattedDateTime(() => account.createdAt, {
@ -89,8 +90,8 @@ watchEffect(() => {
</div>
</div>
<div absolute top="1/2" right-0 translate-y="-1/2" flex gap-2 items-center>
<AccountMoreButton :account="account" />
<AccountFollowButton :account="account" />
<AccountMoreButton :account="account" :command="command" />
<AccountFollowButton :account="account" :command="command" />
<!-- <button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
<div rounded p2 group-hover="bg-rose/10">
<div i-ri:bell-line />

View file

@ -3,6 +3,7 @@ import type { Account } from 'masto'
const { account } = defineProps<{
account: Account
command?: boolean
}>()
let relationship = $(useRelationship(account))
@ -35,7 +36,7 @@ const toggleBlockDomain = async () => {
</script>
<template>
<CommonDropdown>
<CommonDropdown :eager-mount="command">
<button flex gap-1 items-center w-full rounded op75 hover="op100 text-purple" group>
<div rounded-5 p2 group-hover="bg-purple/10">
<div i-ri:more-2-fill />
@ -44,73 +45,111 @@ const toggleBlockDomain = async () => {
<template #popper>
<NuxtLink :to="account.url" target="_blank">
<CommonDropdownItem icon="i-ri:arrow-right-up-line">
{{ $t('menu.open_in_original_site') }}
</CommonDropdownItem>
<CommonDropdownItem
:text="$t('menu.open_in_original_site')"
icon="i-ri:arrow-right-up-line"
:command="command"
/>
</NuxtLink>
<template v-if="currentUser">
<template v-if="!isSelf">
<CommonDropdownItem icon="i-ri:at-line" @click="mentionUser(account)">
{{ $t('menu.mention_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem icon="i-ri:message-3-line" @click="directMessageUser(account)">
{{ $t('menu.direct_message_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem
:text="$t('menu.mention_account', [`@${account.acct}`])"
icon="i-ri:at-line"
:command="command"
@click="mentionUser(account)"
/>
<CommonDropdownItem
:text="$t('menu.direct_message_account', [`@${account.acct}`])"
icon="i-ri:message-3-line"
:command="command"
@click="directMessageUser(account)"
/>
<CommonDropdownItem v-if="!relationship?.muting" icon="i-ri:volume-up-fill" @click="toggleMute">
{{ $t('menu.mute_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem v-else icon="i-ri:volume-mute-line" @click="toggleMute">
{{ $t('menu.unmute_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem
v-if="!relationship?.muting"
:text="$t('menu.mute_account', [`@${account.acct}`])"
icon="i-ri:volume-up-fill"
:command="command"
@click="toggleMute"
/>
<CommonDropdownItem
v-else
:text="$t('menu.unmute_account', [`@${account.acct}`])"
icon="i-ri:volume-mute-line"
:command="command"
@click="toggleMute"
/>
<CommonDropdownItem v-if="!relationship?.blocking" icon="i-ri:forbid-2-line" @click="toggleBlockUser">
{{ $t('menu.block_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem v-else icon="i-ri:checkbox-circle-line" @click="toggleBlockUser">
{{ $t('menu.unblock_account', [`@${account.acct}`]) }}
</CommonDropdownItem>
<CommonDropdownItem
v-if="!relationship?.blocking"
:text="$t('menu.block_account', [`@${account.acct}`])"
icon="i-ri:forbid-2-line"
:command="command"
@click="toggleBlockUser"
/>
<CommonDropdownItem
v-else
:text="$t('menu.unblock_account', [`@${account.acct}`])"
icon="i-ri:checkbox-circle-line"
:command="command"
@click="toggleBlockUser"
/>
<template v-if="getServerName(account) !== currentServer">
<CommonDropdownItem
v-if="!relationship?.domainBlocking"
:text="$t('menu.block_domain', [getServerName(account)])"
icon="i-ri:shut-down-line"
:command="command"
@click="toggleBlockDomain"
>
{{ $t('menu.block_domain', [getServerName(account)]) }}
</CommonDropdownItem>
<CommonDropdownItem v-else icon="i-ri:restart-line" @click="toggleBlockDomain">
{{ $t('menu.unblock_domain', [getServerName(account)]) }}
</CommonDropdownItem>
/>
<CommonDropdownItem
v-else
:text="$t('menu.unblock_domain', [getServerName(account)])"
icon="i-ri:restart-line"
:command="command"
@click="toggleBlockDomain"
/>
</template>
</template>
<template v-else>
<NuxtLink to="/pinned">
<CommonDropdownItem icon="i-ri:pushpin-line">
Pinned
</CommonDropdownItem>
<CommonDropdownItem
text="Pinned"
icon="i-ri:pushpin-line"
:command="command"
/>
</NuxtLink>
<NuxtLink to="/favourites">
<CommonDropdownItem icon="i-ri:heart-3-line">
Favourites
</CommonDropdownItem>
<CommonDropdownItem
text="Favourites"
icon="i-ri:heart-3-line"
:command="command"
/>
</NuxtLink>
<NuxtLink to="/mutes">
<CommonDropdownItem icon="i-ri:volume-mute-line">
Muted users
</CommonDropdownItem>
<CommonDropdownItem
text="Muted users"
icon="i-ri:volume-mute-line"
:command="command"
/>
</NuxtLink>
<NuxtLink to="/blocks">
<CommonDropdownItem icon="i-ri:forbid-2-line">
Blocked users
</CommonDropdownItem>
<CommonDropdownItem
text="Blocked users"
icon="i-ri:forbid-2-line"
:command="command"
/>
</NuxtLink>
<NuxtLink to="/domain_blocks">
<CommonDropdownItem icon="i-ri:shut-down-line">
Blocked domains
</CommonDropdownItem>
<CommonDropdownItem
text="Blocked domains"
icon="i-ri:shut-down-line"
:command="command"
/>
</NuxtLink>
</template>
</template>

View file

@ -0,0 +1,39 @@
<script setup lang="ts">
const props = defineProps<{
name: string
}>()
const isMac = useIsMac()
const keys = $computed(() => props.name.toLowerCase().split('+'))
</script>
<template>
<div class="flex items-center px-1">
<template v-for="(key, index) in keys" :key="key">
<div v-if="index > 0" class="inline-block px-.5">
+
</div>
<div
class="p-1 grid place-items-center rounded-lg shadow-sm"
text="xs secondary"
border="1 base"
>
<div v-if="key === 'enter'" i-material-symbols:keyboard-return-rounded />
<div v-else-if="key === 'meta' && isMac" i-material-symbols:keyboard-command-key />
<div v-else-if="key === 'meta' && !isMac" i-material-symbols:window-sharp />
<div v-else-if="key === 'alt' && isMac" i-material-symbols:keyboard-option-key-rounded />
<div v-else-if="key === 'arrowup'" i-ri:arrow-up-line />
<div v-else-if="key === 'arrowdown'" i-ri:arrow-down-line />
<div v-else-if="key === 'arrowleft'" i-ri:arrow-left-line />
<div v-else-if="key === 'arrowright'" i-ri:arrow-right-line />
<template v-else-if="key === 'escape'">
ESC
</template>
<div v-else :class="{ 'px-.5': key.length === 1 }">
{{ key[0].toUpperCase() + key.slice(1) }}
</div>
</div>
</template>
</div>
</template>

View file

@ -0,0 +1,253 @@
<script setup lang="ts">
import type { CommandScope, QueryIndexedCommand } from '@/composables/command'
const isMac = useIsMac()
const registry = useCommandRegistry()
const inputEl = $ref<HTMLInputElement>()
const resultEl = $ref<HTMLDivElement>()
let show = $ref(false)
let scopes = $ref<CommandScope[]>([])
let input = $ref('')
// listen to ctrl+/ on windows/linux or cmd+/ on mac
useEventListener('keydown', async (e: KeyboardEvent) => {
if (e.key === '/' && (isMac.value ? e.metaKey : e.ctrlKey)) {
e.preventDefault()
show = true
scopes = []
input = '>'
await nextTick()
inputEl?.focus()
}
})
onKeyStroke('Escape', (e) => {
e.preventDefault()
show = false
}, { target: document })
const commandMode = $computed(() => input.startsWith('>'))
const result = $computed(() => commandMode
? registry.query(scopes.map(s => s.id).join('.'), input.slice(1))
: { length: 0, items: [], grouped: {} })
let active = $ref(0)
watch($$(result), (n, o) => {
if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx]))
active = 0
})
const findItemEl = (index: number) =>
resultEl?.querySelector(`[data-index="${index}"]`) as HTMLDivElement | null
const onCommandActivate = (item: QueryIndexedCommand) => {
if (item.onActivate) {
item.onActivate()
show = false
}
else if (item.onComplete) {
scopes.push(item.onComplete())
input = '>'
}
}
const onCommandComplete = (item: QueryIndexedCommand) => {
if (item.onComplete) {
scopes.push(item.onComplete())
input = '>'
}
else if (item.onActivate) {
item.onActivate()
}
}
const intoView = (index: number) => {
const el = findItemEl(index)
if (el)
el.scrollIntoView({ block: 'nearest' })
}
const onKeyDown = (e: KeyboardEvent) => {
switch (e.key) {
case 'ArrowUp': {
e.preventDefault()
active = Math.max(0, active - 1)
intoView(active)
break
}
case 'ArrowDown': {
e.preventDefault()
active = Math.min(result.length - 1, active + 1)
intoView(active)
break
}
case 'Home': {
e.preventDefault()
active = 0
intoView(active)
break
}
case 'End': {
e.preventDefault()
active = result.length - 1
intoView(active)
break
}
case 'Enter': {
e.preventDefault()
const cmd = result.items[active]
if (cmd)
onCommandActivate(cmd)
break
}
case 'Tab': {
e.preventDefault()
const cmd = result.items[active]
if (cmd)
onCommandComplete(cmd)
break
}
case 'Backspace': {
if (input === '>' && scopes.length) {
e.preventDefault()
scopes.pop()
}
break
}
}
}
</script>
<template>
<!-- Overlay -->
<Transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="transform opacity-100"
leave-active-class="transition duration-100 ease-in"
leave-from-class="transform opacity-100"
leave-to-class="transform opacity-0"
>
<div
v-if="show"
class="z-100 fixed inset-0 opacity-70 bg-base"
@click="show = false"
/>
</Transition>
<!-- Panel -->
<Transition
enter-active-class="transition duration-65 ease-out"
enter-from-class="transform scale-95"
enter-to-class="transform scale-100"
leave-active-class="transition duration-100 ease-in"
leave-from-class="transform scale-100"
leave-to-class="transform scale-95"
>
<div v-if="show" class="z-100 fixed inset-0 grid place-items-center pointer-events-none">
<div
class="flex flex-col w-50vw h-50vh rounded-md bg-base shadow-lg pointer-events-auto"
border="1 base"
>
<!-- Input -->
<label class="flex mx-3 my-1 items-center">
<div mx-1 i-ri:search-line />
<div v-for="scope in scopes" :key="scope.id" class="flex items-center mx-1 gap-2">
<div class="text-sm">{{ scope.display }}</div>
<span class="text-secondary">/</span>
</div>
<input
ref="inputEl"
v-model="input"
class="focus:outline-none flex-1 p-2 rounded bg-base"
placeholder="Search"
@keydown="onKeyDown"
>
<CommandKey name="Escape" />
</label>
<div class="w-full border-b-1 border-base" />
<!-- Results -->
<div ref="resultEl" class="flex-1 mx-1 overflow-y-auto">
<template v-for="[scope, group] in result.grouped" :key="scope">
<div class="mt-2 px-2 py-1 text-sm text-secondary">
{{ scope }}
</div>
<template v-for="cmd in group" :key="cmd.index">
<div
class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10"
:class="{ 'bg-active': active === cmd.index }"
:data-index="cmd.index"
@click="onCommandActivate(cmd)"
>
<div v-if="cmd.icon" mr-2 :class="cmd.icon" />
<div class="flex-1 flex items-baseline gap-2">
<div :class="{ 'font-medium': active === cmd.index }">
{{ cmd.name }}
</div>
<div v-if="cmd.description" class="text-xs text-secondary">
{{ cmd.description }}
</div>
</div>
<div
v-if="cmd.onComplete"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
Complete
</div>
<CommandKey name="Tab" />
</div>
<div
v-if="cmd.onActivate"
class="flex items-center gap-1 transition-all duration-65 ease-in-out"
:class="active === cmd.index ? 'opacity-100' : 'opacity-0'"
>
<div class="text-xs text-secondary">
Activate
</div>
<CommandKey name="Enter" />
</div>
</div>
</template>
</template>
</div>
<div class="w-full border-b-1 border-base" />
<!-- Footer -->
<div class="flex items-center px-3 py-1 text-xs">
<div i-ri:lightbulb-flash-line /> Tip: Use
<!-- <CommandKey name="Ctrl+K" /> to search, -->
<CommandKey name="Ctrl+/" /> to activate command mode.
</div>
</div>
</div>
</Transition>
</template>

View file

@ -0,0 +1,7 @@
<script setup lang="ts">
provideGlobalCommands()
</script>
<template>
<CommandPanel />
</template>

View file

@ -1,13 +1,18 @@
<script setup lang="ts">
const { options } = defineProps<{
options: string[] | { name: string; display: string }[]
const { options, command } = defineProps<{
options: string[] | {
name: string
icon?: string
display: string
}[]
command?: boolean
}>()
const { modelValue } = defineModel<{
modelValue: string
}>()
const tabs = computed(() => {
const tabs = $computed(() => {
return options.map((option) => {
if (typeof option === 'string')
return { name: option, display: option }
@ -19,6 +24,17 @@ const tabs = computed(() => {
function toValidName(otpion: string) {
return otpion.toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')
}
useCommands(() => command
? tabs.map(tab => ({
scope: 'Tabs',
name: tab.display,
icon: tab.icon ?? 'i-ri:file-list-2-line',
onActivate: () => modelValue.value = tab.name,
}))
: [])
</script>
<template>

View file

@ -1,31 +1,58 @@
<script setup lang="ts">
import { dropdownContextKey } from './ctx'
defineProps<{
const props = defineProps<{
text?: string
description?: string
icon?: string
checked?: boolean
command?: boolean
}>()
const emit = defineEmits(['click'])
const { hide } = inject(dropdownContextKey, undefined) || {}
const el = ref<HTMLDivElement>()
const handleClick = (evt: MouseEvent) => {
hide?.()
emit('click', evt)
}
useCommand({
scope: 'Actions',
order: -1,
visible: () => props.command && props.text,
name: () => props.text!,
icon: () => props.icon ?? 'i-ri:question-line',
description: () => props.description,
onActivate() {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
})
el.value?.dispatchEvent(clickEvent)
},
})
</script>
<template>
<div
flex gap-3 items-center cursor-pointer px4 py3 hover-bg-active
v-bind="$attrs"
v-bind="$attrs" ref="el"
flex gap-3 items-center cursor-pointer px4 py3
hover-bg-active
@click="handleClick"
>
<div v-if="icon" :class="icon" />
<div flex="~ col">
<div text-15px>
<slot />
<slot>
{{ text }}
</slot>
</div>
<div text-3 text-secondary>
<slot name="description">

View file

@ -15,11 +15,18 @@ const { t } = useI18n()
<NavSideItem :text="t('nav_side.conversations')" to="/conversations" icon="i-ri:at-line" />
<NavSideItem :text="t('nav_side.favourites')" to="/favourites" icon="i-ri:heart-3-line" />
<NavSideItem :text="t('nav_side.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line " />
<NavSideItem :to="getAccountPath(currentUser.account)" icon="i-ri:list-check-2-line">
<NavSideItem
:text="currentUser.account.displayName"
:to="getAccountPath(currentUser.account)"
icon="i-ri:account-circle-line"
>
<template #icon>
<AccountAvatar :account="currentUser.account" h="1.2em" />
</template>
<ContentRich :content="getDisplayName(currentUser.account, { rich: true }) || t('nav_side.profile')" :emojis="currentUser.account.emojis" />
<ContentRich
:content="getDisplayName(currentUser.account, { rich: true }) || t('nav_side.profile')"
:emojis="currentUser.account.emojis"
/>
</NavSideItem>
</template>
</nav>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
defineProps<{
const props = defineProps<{
text?: string
icon: string
to: string
@ -9,6 +9,19 @@ defineSlots<{
icon: {}
default: {}
}>()
const router = useRouter()
useCommand({
scope: 'Navigation',
name: () => props.text ?? props.to,
icon: () => props.icon,
onActivate() {
router.push(props.to)
},
})
</script>
<template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
defineProps<{
const props = defineProps<{
text?: string | number
content: string
color: string
@ -10,20 +10,44 @@ defineProps<{
active?: boolean
disabled?: boolean
as?: string
command?: boolean
}>()
defineOptions({
inheritAttrs: false,
})
const el = ref<HTMLDivElement>()
useCommand({
scope: 'Actions',
order: -2,
visible: () => props.command && !props.disabled,
name: () => props.content,
icon: () => props.icon,
onActivate() {
const clickEvent = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
})
el.value?.dispatchEvent(clickEvent)
},
})
</script>
<template>
<component
:is="as || 'button'" w-fit
flex gap-1 items-center rounded group
:hover="hover" focus:outline-none :focus-visible="hover"
:is="as || 'button'"
v-bind="$attrs" ref="el"
w-fit flex gap-1 items-center
rounded group :hover="hover"
focus:outline-none
:focus-visible="hover"
:class="active ? [color] : 'text-secondary'"
v-bind="$attrs"
>
<CommonTooltip placement="bottom" :content="content">
<div rounded-full p2 :group-hover="groupHover" :group-focus-visible="groupHover" group-focus-visible:ring="2 current">

View file

@ -1,9 +1,10 @@
<script setup lang="ts">
import type { Status } from 'masto'
const { status: _status, details } = defineProps<{
const { status: _status, details, command } = defineProps<{
status: Status
details?: boolean
command?: boolean
}>()
let status = $ref<Status>({ ..._status })
@ -134,6 +135,7 @@ function editStatus() {
:text="status.repliesCount"
color="text-blue" hover="text-blue" group-hover="bg-blue/10"
icon="i-ri:chat-3-line"
:command="command"
@click="reply"
/>
</div>
@ -147,6 +149,7 @@ function editStatus() {
active-icon="i-ri:repeat-fill"
:active="status.reblogged"
:disabled="isLoading.reblogged"
:command="command"
@click="toggleReblog()"
/>
</div>
@ -160,6 +163,7 @@ function editStatus() {
active-icon="i-ri:heart-3-fill"
:active="status.favourited"
:disabled="isLoading.favourited"
:command="command"
@click="toggleFavourite()"
/>
</div>
@ -172,11 +176,12 @@ function editStatus() {
active-icon="i-ri:bookmark-fill"
:active="status.bookmarked"
:disabled="isLoading.bookmarked"
:command="command"
@click="toggleBookmark()"
/>
</div>
<CommonDropdown flex-none ml3 placement="bottom">
<CommonDropdown flex-none ml3 placement="bottom" :eager-mount="command">
<StatusActionButton
content="More"
color="text-purple" hover="text-purple" group-hover="bg-purple/10"
@ -185,59 +190,69 @@ function editStatus() {
<template #popper>
<div flex="~ col">
<CommonDropdownItem icon="i-ri:link" @click="copyLink">
Copy link to this post
</CommonDropdownItem>
<CommonDropdownItem
text="Copy link to this post"
icon="i-ri:link"
:command="command"
@click="copyLink"
/>
<NuxtLink :to="status.url" target="_blank">
<CommonDropdownItem v-if="status.url" icon="i-ri:arrow-right-up-line">
Open in original site
</CommonDropdownItem>
<CommonDropdownItem
v-if="status.url"
text="Open in original site"
icon="i-ri:arrow-right-up-line"
:command="command"
/>
</NuxtLink>
<CommonDropdownItem v-if="isTranslationEnabled && status.language !== languageCode" icon="i-ri:translate" @click="toggleTranslation">
<template v-if="!translation.visible">
Translate post
</template>
<template v-else>
Show untranslated
</template>
</CommonDropdownItem>
<CommonDropdownItem
v-if="isTranslationEnabled && status.language !== languageCode"
:text="translation.visible ? 'Show untranslated' : 'Translate post'"
icon="i-ri:translate"
:command="command"
@click="toggleTranslation"
/>
<template v-if="currentUser">
<template v-if="isAuthor">
<CommonDropdownItem
:text="status.pinned ? 'Unpin on profile' : 'Pin on profile'"
icon="i-ri:pushpin-line"
:command="command"
@click="togglePin"
>
{{ status.pinned ? 'Unpin on profile' : 'Pin on profile' }}
</CommonDropdownItem>
<CommonDropdownItem icon="i-ri:edit-line" @click="editStatus">
Edit
</CommonDropdownItem>
/>
<CommonDropdownItem
icon="i-ri:delete-bin-line" text-red-600
text="Edit"
icon="i-ri:edit-line"
:command="command"
@click="editStatus"
/>
<CommonDropdownItem
text="Delete"
icon="i-ri:delete-bin-line"
text-red-600
:command="command"
@click="deleteStatus"
>
Delete
</CommonDropdownItem>
/>
<CommonDropdownItem
icon="i-ri:eraser-line" text-red-600
text="Delete & re-draft"
icon="i-ri:eraser-line"
text-red-600
:command="command"
@click="deleteAndRedraft"
>
Delete & re-draft
</CommonDropdownItem>
/>
</template>
<template v-else>
<CommonDropdownItem
:text="`Mention @${status.account.acct}`"
icon="i-ri:at-line"
:command="command"
@click="mentionUser(status.account)"
>
Mention @{{ status.account.acct }}
</CommonDropdownItem>
/>
</template>
</template>
</div>

View file

@ -3,6 +3,7 @@ import type { Status } from 'masto'
const props = defineProps<{
status: Status
command?: boolean
}>()
const status = $computed(() => {
@ -52,6 +53,6 @@ const visibility = $computed(() => STATUS_VISIBILITIES.find(v => v.value === sta
· {{ status.application?.name }}
</div>
</div>
<StatusActions :status="status" details border="t base" pt-2 />
<StatusActions :status="status" details :command="command" border="t base" pt-2 />
</div>
</template>