Improve admin candidate modal UX and add clip menu visibility toggle
- Widen AdminCandidateEditorModal to size lg for better readability - Rename "Clip-Compilation" section to "Clip / Compilation", update copy to reflect single clips too, drop upload hint and Clip-Plattform field, rename label to "Link" - Fix NativeSelect dropdown clipping inside overflow-y-auto modals by teleporting the menu to body with fixed positioning, flip-up logic, and dynamic maxHeight capped to viewport - Add ClipAdminMenuVisible setting (backend domain, contracts, endpoint, migration) with matching frontend types, defaults, form wiring, and toggle in the Clip-Workflow modal — hides the Clips nav item from the admin sidebar when disabled Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
</select>
|
||||
|
||||
<button
|
||||
ref="triggerEl"
|
||||
type="button"
|
||||
class="native-select__trigger"
|
||||
:disabled="disabled"
|
||||
@@ -33,7 +34,8 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div v-if="open" :id="listboxId" class="native-select__menu" role="listbox">
|
||||
<Teleport to="body">
|
||||
<div v-if="open" :id="listboxId" class="native-select__menu" :style="menuStyle" role="listbox">
|
||||
<template v-for="(option, index) in options" :key="optionKey(option.value)">
|
||||
<p v-if="option.group && option.group !== options[index - 1]?.group" class="native-select__group">
|
||||
{{ option.group }}
|
||||
@@ -56,12 +58,13 @@
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, ChevronDown } from '@lucide/vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
|
||||
|
||||
type SelectOptionValue = string | number | null
|
||||
|
||||
@@ -88,9 +91,48 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const rootEl = ref<HTMLElement | null>(null)
|
||||
const triggerEl = ref<HTMLButtonElement | null>(null)
|
||||
const open = ref(false)
|
||||
const activeIndex = ref(0)
|
||||
const listboxId = `native-select-${Math.random().toString(36).slice(2)}`
|
||||
const menuPos = ref({ top: 0, bottom: 'auto' as string | number, left: 0, width: 0, maxHeight: 286 })
|
||||
|
||||
const menuStyle = computed<CSSProperties>(() => ({
|
||||
position: 'fixed',
|
||||
top: menuPos.value.top !== 0 ? `${menuPos.value.top}px` : 'auto',
|
||||
bottom: menuPos.value.bottom !== 'auto' ? `${menuPos.value.bottom}px` : 'auto',
|
||||
left: `${menuPos.value.left}px`,
|
||||
width: `${menuPos.value.width}px`,
|
||||
maxHeight: `${menuPos.value.maxHeight}px`,
|
||||
zIndex: 9999,
|
||||
}))
|
||||
|
||||
function updateMenuRect() {
|
||||
if (!triggerEl.value) return
|
||||
const r = triggerEl.value.getBoundingClientRect()
|
||||
const PADDING = 8
|
||||
const MAX_H = 286
|
||||
const spaceBelow = window.innerHeight - r.bottom - PADDING
|
||||
const spaceAbove = r.top - PADDING
|
||||
|
||||
if (spaceBelow >= Math.min(MAX_H, 160) || spaceBelow >= spaceAbove) {
|
||||
menuPos.value = {
|
||||
top: r.bottom + PADDING,
|
||||
bottom: 'auto',
|
||||
left: r.left,
|
||||
width: r.width,
|
||||
maxHeight: Math.min(MAX_H, spaceBelow),
|
||||
}
|
||||
} else {
|
||||
menuPos.value = {
|
||||
top: 0,
|
||||
bottom: window.innerHeight - r.top + PADDING,
|
||||
left: r.left,
|
||||
width: r.width,
|
||||
maxHeight: Math.min(MAX_H, spaceAbove),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedOption = computed(() =>
|
||||
props.options.find((option) => isSameValue(option.value, props.modelValue)) ?? null,
|
||||
@@ -132,8 +174,11 @@ function setActiveIndex(index: number) {
|
||||
function toggleDropdown() {
|
||||
if (props.disabled || props.options.length === 0) return
|
||||
open.value = !open.value
|
||||
if (open.value && props.options[activeIndex.value]?.disabled) {
|
||||
activeIndex.value = firstEnabledIndex()
|
||||
if (open.value) {
|
||||
updateMenuRect()
|
||||
if (props.options[activeIndex.value]?.disabled) {
|
||||
activeIndex.value = firstEnabledIndex()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,10 +247,14 @@ function onDocumentPointerDown(event: PointerEvent) {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', onDocumentPointerDown)
|
||||
window.addEventListener('scroll', updateMenuRect, true)
|
||||
window.addEventListener('resize', updateMenuRect)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('pointerdown', onDocumentPointerDown)
|
||||
window.removeEventListener('scroll', updateMenuRect, true)
|
||||
window.removeEventListener('resize', updateMenuRect)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -218,7 +267,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.native-select--open{
|
||||
z-index:45;
|
||||
z-index:80;
|
||||
}
|
||||
|
||||
.native-select__native{
|
||||
@@ -305,10 +354,6 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.native-select__menu{
|
||||
position:absolute;
|
||||
top:calc(100% + 8px);
|
||||
left:0;
|
||||
right:0;
|
||||
display:grid;
|
||||
gap:4px;
|
||||
max-height:286px;
|
||||
|
||||
Reference in New Issue
Block a user