mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42: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:
@@ -20,8 +20,8 @@ const props = withDefaults(
|
||||
|
||||
const badgeClasses = {
|
||||
base: 'py-1 px-2 space-x-1.5 text-xs',
|
||||
large: 'py-1 sm:py-1.5 px-2 sm:px-3 space-x-1.5 sm:space-x-2 text-xs sm:text-sm text-muted',
|
||||
xlarge: 'py-2 sm:py-2.5 px-3 sm:px-3.5 space-x-2 sm:space-x-3 text-sm sm:text-sm text-muted',
|
||||
large: 'py-1 sm:py-1.5 px-2 sm:px-3 space-x-1.5 sm:space-x-2 text-xs sm:text-sm text-text-secondary',
|
||||
xlarge: 'py-2 sm:py-2.5 px-3 sm:px-3.5 space-x-2 sm:space-x-3 text-sm sm:text-sm text-text-secondary',
|
||||
};
|
||||
|
||||
const borderClasses = computed(() => {
|
||||
|
||||
@@ -34,7 +34,7 @@ const close = () => {
|
||||
<slot name="title" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-muted">
|
||||
<div class="mt-4 text-sm text-text-secondary">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ const expandedStatusClasses = computed(() => {
|
||||
if (props.expanded) {
|
||||
return 'border-card-border border bg-card-background-active text-text-primary';
|
||||
}
|
||||
return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-muted';
|
||||
return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-text-secondary';
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useId } from 'radix-vue';
|
||||
import { isLastLayer, layers } from '@/packages/ui/src/utils/dismissableLayer';
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap';
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
} from '@/Components/ui/dialog'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -22,51 +23,12 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
if (props.show) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'visible';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
if (props.closeable) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
const id = useId();
|
||||
|
||||
const closeOnEscape = (e: KeyboardEvent) => {
|
||||
if (isLastLayer(id)) {
|
||||
if (e.key === 'Escape' && props.show) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', closeOnEscape);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
document.body.style.overflow = 'visible';
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(value) => {
|
||||
if (value) {
|
||||
layers.value.push(id);
|
||||
} else {
|
||||
layers.value = layers.value.filter((layer) => layer !== id);
|
||||
}
|
||||
}
|
||||
);
|
||||
const maxWidthClass = computed(() => {
|
||||
return {
|
||||
sm: 'sm:max-w-sm',
|
||||
@@ -76,67 +38,19 @@ const maxWidthClass = computed(() => {
|
||||
'2xl': 'sm:max-w-2xl',
|
||||
}[props.maxWidth];
|
||||
});
|
||||
|
||||
const target = ref();
|
||||
const { activate, deactivate } = useFocusTrap(target, {
|
||||
allowOutsideClick: true,
|
||||
});
|
||||
watch(
|
||||
() => props.show,
|
||||
(value) => {
|
||||
if (value) {
|
||||
nextTick(() => {
|
||||
activate();
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
deactivate();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<transition leave-active-class="duration-200">
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 overflow-y-auto px-4 py-32 sm:px-0 z-50"
|
||||
scroll-region>
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0">
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 transform transition-all backdrop-blur-sm"
|
||||
@click="close">
|
||||
<div
|
||||
class="absolute inset-0 bg-default-background opacity-30" />
|
||||
</div>
|
||||
</transition>
|
||||
<Dialog :open="show" @update:open="close">
|
||||
<DialogContent :class="maxWidthClass">
|
||||
|
||||
<transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
||||
<div
|
||||
v-show="show"
|
||||
ref="target"
|
||||
role="dialog"
|
||||
class="mb-6 bg-default-background border border-card-border rounded-lg shadow-xl transform transition-all sm:w-full sm:mx-auto"
|
||||
:class="maxWidthClass">
|
||||
<slot v-if="show" />
|
||||
</div>
|
||||
</transition>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
|
||||
<DialogFooter>
|
||||
<slot name="footer" />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -7,10 +7,10 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dropdown align="bottom-end">
|
||||
<Dropdown align="end">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-2 focus-visible:ring-ring focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity text-muted"
|
||||
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-2 focus-visible:ring-ring focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity text-text-secondary"
|
||||
:aria-label="label">
|
||||
<svg
|
||||
class="h-8 w-8 p-1 rounded-full"
|
||||
|
||||
@@ -56,7 +56,7 @@ function getNameForKey(key: BillableKey | undefined) {
|
||||
<span>
|
||||
{{ getNameForKey(model) }}
|
||||
</span>
|
||||
<ChevronDownIcon class="text-muted w-5"></ChevronDownIcon>
|
||||
<ChevronDownIcon class="text-text-secondary w-5"></ChevronDownIcon>
|
||||
</Badge>
|
||||
</template>
|
||||
</SelectDropdown>
|
||||
|
||||
@@ -7,7 +7,7 @@ const model = defineModel<string>({ default: '' });
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<Dropdown align="bottom">
|
||||
<Dropdown align="center">
|
||||
<template #trigger>
|
||||
<button
|
||||
class="p-2 bg-input-background hover:bg-tertiary transition rounded-full border border-input-border">
|
||||
|
||||
@@ -77,7 +77,7 @@ const emit = defineEmits(['submit']);
|
||||
@keydown.enter="emit('submit')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center text-muted text-xs pt-2 pl-1">
|
||||
<div class="flex items-center text-text-secondary text-xs pt-2 pl-1">
|
||||
<span>
|
||||
<span class="font-semibold"> Info: </span>
|
||||
{{ billableOptionInfoText }}
|
||||
|
||||
@@ -5,17 +5,16 @@ import { type Component, computed, nextTick, ref, watch } from 'vue';
|
||||
import TagCreateModal from '@/packages/ui/src/Tag/TagCreateModal.vue';
|
||||
import MultiselectDropdownItem from '@/packages/ui/src/Input/MultiselectDropdownItem.vue';
|
||||
import type { Tag } from '@/packages/api/src';
|
||||
import type { Placement } from '@floating-ui/vue';
|
||||
import {UseFocusTrap} from "@vueuse/integrations/useFocusTrap/component";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tags: Tag[];
|
||||
createTag: (name: string) => Promise<Tag | undefined>;
|
||||
align?: Placement;
|
||||
align?: 'center' | 'end' | 'start';
|
||||
}>(),
|
||||
{
|
||||
align: 'bottom-start',
|
||||
align: 'start',
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ function onSelectChange(event: Event) {
|
||||
"></BillableToggleButton>
|
||||
<div class="flex-1">
|
||||
<button
|
||||
class="hidden lg:block text-muted w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
|
||||
class="hidden lg:block text-text-secondary w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary"
|
||||
@click="expanded = !expanded">
|
||||
{{ formatStartEnd(timeEntry.start, timeEntry.end) }}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { TrashIcon } from '@heroicons/vue/20/solid';
|
||||
import MoreOptionsDropdown from '@/packages/ui/src/MoreOptionsDropdown.vue';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/Components/ui/dropdown-menu';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
@@ -8,15 +13,35 @@ const emit = defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MoreOptionsDropdown label="Actions for the time entry">
|
||||
<button
|
||||
data-testid="time_entry_delete"
|
||||
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-text-primary hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out"
|
||||
@click="emit('delete')">
|
||||
<TrashIcon class="w-5 text-icon-active"></TrashIcon>
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</MoreOptionsDropdown>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<button
|
||||
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-2 focus-visible:ring-ring focus-visible:opacity-100 hover:bg-card-background group-hover:opacity-100 opacity-20 transition-opacity text-text-secondary"
|
||||
aria-label="Actions for the time entry">
|
||||
<svg
|
||||
class="h-8 w-8 p-1 rounded-full"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M12 5.92A.96.96 0 1 0 12 4a.96.96 0 0 0 0 1.92m0 7.04a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92M12 20a.96.96 0 1 0 0-1.92a.96.96 0 0 0 0 1.92" />
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="min-w-[150px]" align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="time_entry_delete"
|
||||
class="flex items-center space-x-3 cursor-pointer text-destructive focus:text-destructive"
|
||||
@click="emit('delete')">
|
||||
<TrashIcon class="w-5" />
|
||||
<span>Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
@@ -26,7 +26,7 @@ const triggerElement = ref<HTMLButtonElement | null>(null);
|
||||
<div class="relative">
|
||||
<Dropdown
|
||||
v-model="open"
|
||||
align="bottom"
|
||||
align="center"
|
||||
:close-on-content-click="false"
|
||||
@submit="open = false">
|
||||
<template #trigger>
|
||||
@@ -35,7 +35,7 @@ const triggerElement = ref<HTMLButtonElement | null>(null);
|
||||
data-testid="time_entry_range_selector"
|
||||
:class="
|
||||
twMerge(
|
||||
'text-muted w-[110px] px-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:text-text-primary focus-visible:ring-ring focus-visible:bg-tertiary',
|
||||
'text-text-secondary w-[110px] px-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:text-text-primary focus-visible:ring-ring focus-visible:bg-tertiary',
|
||||
showDate
|
||||
? 'text-xs py-1.5 font-semibold'
|
||||
: 'text-sm py-1.5 font-medium',
|
||||
|
||||
@@ -52,11 +52,11 @@ function selectUnselectAll(value: boolean) {
|
||||
<span class="font-semibold text-text-primary">
|
||||
{{ formatWeekday(date) }}
|
||||
</span>
|
||||
<span class="font-semibold text-muted">
|
||||
<span class="font-semibold text-text-secondary">
|
||||
{{ formatDate(date) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-muted pr-[90px] lg:pr-[92px]">
|
||||
<div class="text-text-secondary pr-[90px] lg:pr-[92px]">
|
||||
<span class="font-semibold">
|
||||
{{ formatHumanReadableDuration(duration) }}
|
||||
</span>
|
||||
|
||||
@@ -25,7 +25,7 @@ const timeEntryTags = computed<Tag[]>(() => {
|
||||
<TagDropdown
|
||||
v-model="model"
|
||||
:tags="tags"
|
||||
align="bottom-end"
|
||||
align="end"
|
||||
:create-tag
|
||||
@changed="emit('changed', model)">
|
||||
<template #trigger>
|
||||
|
||||
@@ -547,7 +547,7 @@ const showCreateProject = ref(false);
|
||||
<span>Add new project</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<Dropdown v-else v-model="open" :close-on-content-click="false" align="bottom">
|
||||
<Dropdown v-else v-model="open" :close-on-content-click="false" align="center">
|
||||
<template #trigger>
|
||||
<ProjectBadge
|
||||
ref="projectDropdownTrigger"
|
||||
@@ -568,7 +568,7 @@ const showCreateProject = ref(false);
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="currentTask"
|
||||
class="w-4 lg:w-5 text-muted shrink-0"></ChevronRightIcon>
|
||||
class="w-4 lg:w-5 text-text-secondary shrink-0"></ChevronRightIcon>
|
||||
<div
|
||||
v-if="currentTask"
|
||||
class="min-w-0 shrink text-xs lg:text-sm truncate">
|
||||
|
||||
@@ -170,7 +170,7 @@ function closeAndFocusInput() {
|
||||
<div class="relative">
|
||||
<Dropdown
|
||||
v-model="open"
|
||||
align="bottom"
|
||||
align="center"
|
||||
:close-on-content-click="false"
|
||||
@submit="open = false">
|
||||
<template #trigger>
|
||||
|
||||
@@ -49,7 +49,7 @@ const task = computed(() => {
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
v-if="task"
|
||||
class="w-4 lg:w-5 text-muted shrink-0"></ChevronRightIcon>
|
||||
class="w-4 lg:w-5 text-text-secondary shrink-0"></ChevronRightIcon>
|
||||
<div
|
||||
v-if="task"
|
||||
class="min-w-0 shrink text-xs truncate">
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const layers = ref<string[]>([]);
|
||||
|
||||
export function isLastLayer(id: string) {
|
||||
return layers.value[layers.value.length - 1] === id;
|
||||
}
|
||||
Reference in New Issue
Block a user