mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 03:32:15 +01:00
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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
44
resources/js/packages/ui/src/Input/TextareaInput.vue
Normal file
44
resources/js/packages/ui/src/Input/TextareaInput.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user