First commit

This commit is contained in:
2026-09-30 01:49:35 +02:00
commit 6edd03aace
109 changed files with 27020 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
/* One account's actions on the dashboard, folded into a quiet ⋮: give the admin
role or take it back, and delete the account. The menu behaves like the
article's (see ArticleActionsMenu) — closed by a click outside, by esc, by
navigating, and by search, settings or the profile popup opening over it —
and only one row's menu is open at a time, which is what the shared state
keys off. */
import type { AuthRole } from '~/composables/useAuth'
const props = defineProps<{
/** the account this row belongs to */
accountId: string
/** whose account it is — the menu says so where the ⋮ waits for a hover */
username: string
/** what that account may do right now */
role: AuthRole
/** a request about this account is in flight: the menu waits with it */
busy: boolean
}>()
const emit = defineEmits<{
'set-role': [role: AuthRole]
remove: []
}>()
const route = useRoute()
const { t } = useI18n()
const { openId, show, hide } = useAccountActionsMenu()
const { open: searchOpen } = useSearch()
const { open: settingsOpen } = useSettings()
const { open: profileOpen } = useProfileMenu()
const open = computed(() => openId.value === props.accountId)
function toggle() {
if (open.value) hide()
else show(props.accountId)
}
const wrapEl = ref<HTMLElement | null>(null)
function onDocClick(e: MouseEvent) {
if (open.value && !wrapEl.value?.contains(e.target as Node)) hide()
}
/* Captured so this runs before the page's own Esc handler sees the key: while a
menu is open, esc folds it away rather than what the page would do with it. */
function onKeydown(e: KeyboardEvent) {
if (e.key !== 'Escape' || !open.value) return
hide()
e.stopPropagation()
}
onMounted(() => {
document.addEventListener('click', onDocClick)
window.addEventListener('keydown', onKeydown, true)
})
onBeforeUnmount(() => {
document.removeEventListener('click', onDocClick)
window.removeEventListener('keydown', onKeydown, true)
})
watch(() => route.fullPath, hide)
watch([searchOpen, settingsOpen, profileOpen], ([search, settings, profile]) => {
if (search || settings || profile) hide()
})
</script>
<template>
<div ref="wrapEl" class="kebab">
<button
type="button"
class="kebab-toggle"
:class="{ open }"
:aria-expanded="String(open)"
aria-haspopup="menu"
:title="t('accountActions.forUser', { username })"
:aria-label="t('accountActions.aria')"
@click="toggle"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
<circle cx="12" cy="5" r="0.6" />
<circle cx="12" cy="12" r="0.6" />
<circle cx="12" cy="19" r="0.6" />
</svg>
</button>
<div v-if="open" class="kebab-dropdown" role="menu" :aria-label="t('accountActions.aria')">
<button
type="button"
class="kebab-item"
role="menuitem"
:disabled="busy"
@click="hide(); emit('set-role', role === 'admin' ? 'member' : 'admin')"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 22s8-4.5 8-10V5.5L12 2 4 5.5V12c0 5.5 8 10 8 10z" />
<path v-if="role === 'admin'" d="m9 12 2 2 4-4" />
<path v-else d="M9 12h6" />
</svg>
{{ role === 'admin' ? t('accountActions.removeAdmin') : t('accountActions.makeAdmin') }}
</button>
<div class="kebab-sep" />
<button
type="button"
class="kebab-item kebab-danger"
role="menuitem"
:disabled="busy"
@click="hide(); emit('remove')"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<path d="M10 11v6M14 11v6" />
</svg>
{{ t('common.delete') }}
</button>
</div>
</div>
</template>