refactor to shadcn components, dynamically load extension frontend

add jetstream permissions, add dynamic inertia module loading, add shadcn components, change modals and dropdowns to shadcn dismissable layer,
This commit is contained in:
Gregor Vostrak
2025-04-13 23:06:58 +02:00
parent ae00fdb0e9
commit e78a551098
204 changed files with 5458 additions and 1275 deletions

View File

@@ -1,59 +1,22 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import {
flip,
limitShift,
type Placement,
shift,
useFloating,
} from '@floating-ui/vue';
import { offset } from '@floating-ui/vue';
import { autoUpdate } from '@floating-ui/vue';
import { useId } from 'radix-vue';
import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
import { Popover, PopoverContent, PopoverTrigger } from '@/Components/ui/popover';
const props = withDefaults(
defineProps<{
align?: Placement;
align?: 'center' | 'end' | 'start';
closeOnContentClick?: boolean;
}>(),
{
align: 'bottom-start',
align: 'start',
closeOnContentClick: true,
}
);
const emit = defineEmits(['open', 'submit']);
const open = defineModel({ default: false });
const id = useId();
const closeOnEscape = (e: KeyboardEvent) => {
if (isLastLayer(id)) {
if (open.value && e.key === 'Escape') {
open.value = false;
}
if (open.value && e.key === 'Enter') {
emit('submit');
if (props.closeOnContentClick) open.value = false;
}
}
};
watch(open, (value) => {
if (value) {
layers.value.push(id);
} else {
layers.value = layers.value.filter((layer) => layer !== id);
reference.value
?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
?.focus();
}
});
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
function onContentClick() {
if (props.closeOnContentClick === true) {
@@ -61,68 +24,27 @@ function onContentClick() {
}
}
function toggleOpen() {
open.value = !open.value;
if (open.value === true) {
function onOpenChange(value: boolean) {
open.value = value;
if (value === true) {
emit('open');
}
}
function onBackgroundClick() {
emit('submit');
open.value = false;
}
const reference = ref<null | HTMLElement>(null);
const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, {
placement: props.align,
whileElementsMounted: autoUpdate,
middleware: [
offset(10),
shift({
limiter: limitShift({
offset: 5,
}),
}),
flip({
fallbackAxisSideDirection: 'start',
}),
],
});
</script>
<template>
<div class="min-w-0 isolate">
<div ref="reference" class="min-w-0 flex items-center" @click.prevent="toggleOpen">
<slot name="trigger" />
</div>
<!-- Full Screen Dropdown Overlay -->
<Teleport to="body">
<div
v-show="open"
class="fixed inset-0 z-50"
@click.prevent="onBackgroundClick" />
<transition
enter-active-class="transition-opacity ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition-opacity ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95">
<div
v-if="open"
ref="floating"
class="z-50"
:style="floatingStyles"
@click="onContentClick">
<div
class="rounded-lg overflow-hidden relative border border-card-border overflow-none shadow-dropdown bg-card-background">
<slot name="content" />
</div>
</div>
</transition>
</Teleport>
<Popover v-model:open="open" @update:open="onOpenChange">
<PopoverTrigger as-child>
<slot class="min-w-0 flex items-center" name="trigger" />
</PopoverTrigger>
<PopoverContent
:align="align"
class="rounded-lg overflow-hidden relative border border-card-border overflow-none shadow-dropdown bg-card-background"
@click="onContentClick">
<slot name="content" />
</PopoverContent>
</Popover>
</div>
</template>