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

@@ -47,7 +47,7 @@ const emit = defineEmits(['changed']);
</script>
<template>
<div class="flex items-center text-muted">
<div class="flex items-center text-text-secondary">
<input
id="start"
ref="datePicker"

View File

@@ -101,7 +101,7 @@ function setLastYear() {
<Dropdown
v-model="open"
:close-on-content-click="false"
align="bottom-end"
align="end"
@submit="emit('submit')">
<template #trigger>
<button
@@ -109,7 +109,7 @@ function setLastYear() {
<CalendarIcon class="w-5"></CalendarIcon>
<div class="text-text-primary">
{{ formatDateLocalized(start) }}
<span class="px-1.5 text-muted">-</span>
<span class="px-1.5 text-text-secondary">-</span>
{{ formatDateLocalized(end) }}
</div>
</button>
@@ -134,13 +134,13 @@ function setLastYear() {
</div>
<div class="pl-5">
<div class="space-y-1 flex-col flex items-start">
<div class="text-xs font-semibold text-muted">
<div class="text-xs font-semibold text-text-secondary">
Start Date
</div>
<DatePicker v-model="start"></DatePicker>
</div>
<div class="mt-2 space-y-1 flex-col flex items-start">
<div class="text-sm font-medium text-muted">
<div class="text-sm font-medium text-text-secondary">
End Date
</div>
<DatePicker v-model="end"></DatePicker>

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>

View File

@@ -151,7 +151,7 @@ const highlightedItem = computed(() => {
</script>
<template>
<Dropdown v-model="open" align="bottom-start" :close-on-content-click="false">
<Dropdown v-model="open" align="start" :close-on-content-click="false">
<template #trigger>
<slot name="trigger"></slot>
</template>

View File

@@ -3,7 +3,6 @@ import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import {computed, nextTick, onMounted, ref, watch} from 'vue';
import SelectDropdownItem from '@/packages/ui/src/Input/SelectDropdownItem.vue';
import { onKeyStroke } from '@vueuse/core';
import { type Placement } from '@floating-ui/vue';
import { twMerge } from 'tailwind-merge';
const model = defineModel<string | null>({
default: null,
@@ -18,11 +17,11 @@ const props = withDefaults(
items: T[];
getKeyFromItem: (item: T) => string | null;
getNameForItem: (item: T) => string;
align?: Placement;
align?: 'center' | 'end' | 'start';
class?: string;
}>(),
{
align: 'bottom-start',
align: 'start',
}
);

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import { computed } from 'vue';
import { twMerge } from 'tailwind-merge';
const model = defineModel<string | number>({
default: '',
});
const props = withDefaults(defineProps<{
id?: string;
type?: string;
class?: string;
rows?: number;
placeholder?: string;
disabled?: boolean;
required?: boolean;
}>(), {
type: 'text',
class: '',
rows: 3,
disabled: false,
required: false,
});
const classes = computed(() => {
return twMerge(
'block w-full rounded-md bg-input-background border-input-border shadow-sm focus:border-transparent focus:ring-ring disabled:opacity-50',
props.class
);
});
</script>
<template>
<textarea
:id="id"
v-model="model"
:type="type"
:class="classes"
:rows="rows"
:placeholder="placeholder"
:disabled="disabled"
:required="required"
/>
</template>

View File

@@ -86,7 +86,7 @@ watch(focused, (newValue, oldValue) => {
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
@changed="updateTimeEntry"></DatePicker>
</div>
<div v-else class="text-muted">-- : --</div>
<div v-else class="text-text-secondary">-- : --</div>
<div tabindex="0" @focusin="emit('close')"></div>
</div>
</div>