mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 12:12:15 +01:00
improve focus state styling
This commit is contained in:
@@ -87,7 +87,7 @@ async function deleteReport() {
|
|||||||
<span v-else>Copied!</span>
|
<span v-else>Copied!</span>
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
<button
|
<button
|
||||||
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-white/80"
|
class="outline-0 focus-visible:ring-2 w-6 h-6 flex items-center justify-center rounded focus-visible:ring-ring"
|
||||||
@click="openSharableLink">
|
@click="openSharableLink">
|
||||||
<ArrowTopRightOnSquareIcon
|
<ArrowTopRightOnSquareIcon
|
||||||
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
class="w-4 text-text-tertiary hover:text-text-secondary transition"></ArrowTopRightOnSquareIcon>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function downloadCurrentExport() {
|
|||||||
@close="showExportModal = false"
|
@close="showExportModal = false"
|
||||||
:show="showExportModal">
|
:show="showExportModal">
|
||||||
<button
|
<button
|
||||||
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-white/80 transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
|
class="text-text-tertiary w-6 mx-auto absolute focus-visible:outline-none focus-visible:ring-2 rounded-full focus-visible:ring-ring transition focus-visible:text-text-primary hover:text-text-primary top-2 right-2">
|
||||||
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
<XMarkIcon @click="showExportModal = false"></XMarkIcon>
|
||||||
</button>
|
</button>
|
||||||
<div class="text-center text-text-primary py-6">
|
<div class="text-center text-text-primary py-6">
|
||||||
|
|||||||
@@ -485,13 +485,13 @@ async function downloadExport(format: ExportFormat) {
|
|||||||
</template>
|
</template>
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.navigation-item {
|
.navigation-item {
|
||||||
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
|
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-item {
|
.pagination-item {
|
||||||
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
|
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
|
||||||
}
|
}
|
||||||
.pagination-item[data-selected] {
|
.pagination-item[data-selected] {
|
||||||
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150;
|
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -174,13 +174,13 @@ watch(currentPage, () => {
|
|||||||
</template>
|
</template>
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.navigation-item {
|
.navigation-item {
|
||||||
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
|
@apply bg-quaternary h-8 w-8 flex items-center justify-center rounded border border-border-primary text-text-tertiary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-item {
|
.pagination-item {
|
||||||
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-white/80;
|
@apply bg-secondary h-8 w-8 flex items-center justify-center rounded border border-border-tertiary text-text-secondary hover:text-text-primary transition cursor-pointer hover:border-border-secondary hover:bg-secondary focus-visible:text-text-primary focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-ring;
|
||||||
}
|
}
|
||||||
.pagination-item[data-selected] {
|
.pagination-item[data-selected] {
|
||||||
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-white/80 transition ease-in-out duration-150;
|
@apply text-white bg-accent-300/10 border border-accent-300/20 rounded-md font-medium hover:bg-accent-300/20 active:bg-accent-300/20 outline-0 focus-visible:ring-2 focus:ring-ring transition ease-in-out duration-150;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ const borderClasses = computed(() => {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const tagClasses = computed(() => {
|
||||||
|
if (props.tag === 'button') {
|
||||||
|
return 'hover:bg-tertiary';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -37,9 +44,10 @@ const borderClasses = computed(() => {
|
|||||||
:is="tag"
|
:is="tag"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
|
tagClasses,
|
||||||
badgeClasses[size],
|
badgeClasses[size],
|
||||||
borderClasses,
|
borderClasses,
|
||||||
'rounded inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80',
|
'rounded transition inline-flex items-center font-semibold text-white disabled:text-text-quaternary outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const props = withDefaults(
|
|||||||
<button
|
<button
|
||||||
:type="type"
|
:type="type"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
class="inline-flex items-center px-2 sm:px-3 py-1 sm:py-2 bg-accent-300/10 border border-accent-300/20 rounded-md font-medium text-xs sm:text-sm text-white hover:bg-accent-300/20 active:bg-accent-300/20 focus:outline-none focus-visible:ring-2 focus-visible:border-transparent focus-visible:ring-ring transition ease-in-out duration-150">
|
||||||
<span
|
<span
|
||||||
:class="
|
:class="
|
||||||
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
|
twMerge('flex items-center ', props.icon ? 'space-x-1.5' : '')
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const sizeClasses = {
|
|||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:border-input-border-active focus:outline-none focus:ring-0 disabled:opacity-25 ease-in-out',
|
'bg-button-secondary-background border border-button-secondary-border hover:bg-button-secondary-background-hover shadow-sm transition text-white rounded-lg font-semibold inline-flex items-center space-x-1.5 focus-visible:outline-none focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring focus:border-transparent disabled:opacity-25 ease-in-out',
|
||||||
sizeClasses[props.size],
|
sizeClasses[props.size],
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const expandedStatusClasses = computed(() => {
|
|||||||
if (props.expanded) {
|
if (props.expanded) {
|
||||||
return 'border-card-border border bg-card-background-active text-white';
|
return 'border-card-border border bg-card-background-active text-white';
|
||||||
}
|
}
|
||||||
return 'border-card-border border bg-card-background text-muted';
|
return 'border-card-border border bg-card-background hover:bg-card-background-active hover:text-text-primary transition text-muted';
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ const expandedStatusClasses = computed(() => {
|
|||||||
<button
|
<button
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'font-medium rounded flex items-center transition justify-center',
|
'font-medium rounded flex items-center transition justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent',
|
||||||
expandedStatusClasses,
|
expandedStatusClasses,
|
||||||
props.size
|
props.size
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const iconSizeClasses = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const iconSizeWrapperClasses =
|
const iconSizeWrapperClasses =
|
||||||
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-11 h-11';
|
props.size === 'small' ? 'w-6 sm:w-8 h-6 sm:h-8' : 'w-10 h-10';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -45,7 +45,7 @@ const iconSizeWrapperClasses =
|
|||||||
twMerge(
|
twMerge(
|
||||||
iconColorClasses,
|
iconColorClasses,
|
||||||
iconSizeWrapperClasses,
|
iconSizeWrapperClasses,
|
||||||
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
'flex-shrink-0 ring-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center'
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<BillableIcon :class="iconSizeClasses"></BillableIcon>
|
<BillableIcon :class="iconSizeClasses"></BillableIcon>
|
||||||
|
|||||||
@@ -35,5 +35,5 @@ const proxyChecked = computed({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
:id="id"
|
:id="id"
|
||||||
:value="value"
|
:value="value"
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:outline-none focus:ring-ring/50 focus-visible:outline-none focus-visible:ring-ring/50" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const model = defineModel();
|
|||||||
ref="input"
|
ref="input"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'border-input-border border bg-input-background text-white focus:ring-input-border-active focus:ring-0 focus-visible:border-input-border-active rounded-md shadow-sm',
|
'border-input-border border bg-input-background text-white focus-visible:ring-2 focus-visible:ring-ring focus-visible:border-transparent rounded-md shadow-sm',
|
||||||
props.class
|
props.class
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
|
|||||||
119
resources/js/packages/ui/src/Input/TimePickerSimple.vue
Normal file
119
resources/js/packages/ui/src/Input/TimePickerSimple.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { getLocalizedDayJs } from '@/packages/ui/src/utils/time';
|
||||||
|
import { useFocus } from '@vueuse/core';
|
||||||
|
import { TextInput } from '@/packages/ui/src';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
// This has to be a localized timestamp, not UTC
|
||||||
|
const model = defineModel<string | null>({
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
size: 'base' | 'large';
|
||||||
|
focus: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 'base',
|
||||||
|
focus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function updateTime(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const newValue = target.value.trim();
|
||||||
|
if (newValue.split(':').length === 2) {
|
||||||
|
const [hours, minutes] = newValue.split(':');
|
||||||
|
if (!isNaN(parseInt(hours)) && !isNaN(parseInt(minutes))) {
|
||||||
|
model.value = getLocalizedDayJs(model.value)
|
||||||
|
.set('hours', Math.min(parseInt(hours), 23))
|
||||||
|
.set('minutes', Math.min(parseInt(minutes), 59))
|
||||||
|
.format();
|
||||||
|
emit('changed', model.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if input is only numbers
|
||||||
|
else if (/^\d+$/.test(newValue)) {
|
||||||
|
if (newValue.length === 4) {
|
||||||
|
// parse 1300 to 13:00
|
||||||
|
const [hours, minutes] = [
|
||||||
|
newValue.slice(0, 2),
|
||||||
|
newValue.slice(2, 4),
|
||||||
|
];
|
||||||
|
model.value = getLocalizedDayJs(model.value)
|
||||||
|
.set('hours', Math.min(parseInt(hours), 23))
|
||||||
|
.set('minutes', Math.min(parseInt(minutes), 59))
|
||||||
|
.format();
|
||||||
|
emit('changed', model.value);
|
||||||
|
} else if (newValue.length === 3) {
|
||||||
|
// parse 130 to 01:30
|
||||||
|
const [hours, minutes] = [
|
||||||
|
newValue.slice(0, 1),
|
||||||
|
newValue.slice(1, 3),
|
||||||
|
];
|
||||||
|
model.value = getLocalizedDayJs(model.value)
|
||||||
|
.set('hours', Math.min(parseInt(hours), 23))
|
||||||
|
.set('minutes', Math.min(parseInt(minutes), 59))
|
||||||
|
.format();
|
||||||
|
emit('changed', model.value);
|
||||||
|
} else if (newValue.length === 2) {
|
||||||
|
// parse 13 to 13:00
|
||||||
|
model.value = getLocalizedDayJs(model.value)
|
||||||
|
.set('hours', Math.min(parseInt(newValue), 23))
|
||||||
|
.set('minutes', 0)
|
||||||
|
.format();
|
||||||
|
emit('changed', model.value);
|
||||||
|
} else if (newValue.length === 1) {
|
||||||
|
// parse 1 to 01:00
|
||||||
|
model.value = getLocalizedDayJs(model.value)
|
||||||
|
.set('hours', Math.min(parseInt(newValue), 23))
|
||||||
|
.set('minutes', 0)
|
||||||
|
.format();
|
||||||
|
emit('changed', model.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputValue.value = getLocalizedDayJs(model.value).format('HH:mm');
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(model, (value) => {
|
||||||
|
inputValue.value = value ? getLocalizedDayJs(value).format('HH:mm') : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const emit = defineEmits(['changed']);
|
||||||
|
|
||||||
|
useFocus(timeInput, { initialValue: props.focus });
|
||||||
|
|
||||||
|
const inputValue = ref(
|
||||||
|
model.value ? getLocalizedDayJs(model.value).format('HH:mm') : null
|
||||||
|
);
|
||||||
|
const open = ref(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TextInput
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-model="inputValue"
|
||||||
|
ref="timeInput"
|
||||||
|
:class="
|
||||||
|
twMerge('text-center w-24 px-3 py-2', size === 'large' && 'w-28')
|
||||||
|
"
|
||||||
|
@blur="updateTime"
|
||||||
|
@keydown.enter="
|
||||||
|
updateTime($event);
|
||||||
|
open = false;
|
||||||
|
"
|
||||||
|
@keydown.tab="open = false"
|
||||||
|
@focus="($event.target as HTMLInputElement).select()"
|
||||||
|
@mouseup="($event.target as HTMLInputElement).select()"
|
||||||
|
@click="($event.target as HTMLInputElement).select()"
|
||||||
|
@pointerup="($event.target as HTMLInputElement).select()"
|
||||||
|
@focusin="open = true"
|
||||||
|
data-testid="time_picker_input"
|
||||||
|
type="text" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineProps, ref, watch } from 'vue';
|
import { defineProps, ref, watch } from 'vue';
|
||||||
import TimePicker from '@/packages/ui/src/Input/TimePicker.vue';
|
|
||||||
import { useFocusWithin } from '@vueuse/core';
|
import { useFocusWithin } from '@vueuse/core';
|
||||||
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
import DatePicker from '@/packages/ui/src/Input/DatePicker.vue';
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
getLocalizedDayJs,
|
getLocalizedDayJs,
|
||||||
} from '@/packages/ui/src/utils/time';
|
} from '@/packages/ui/src/utils/time';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import TimePickerSimple from '@/packages/ui/src/Input/TimePickerSimple.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
start: string;
|
start: string;
|
||||||
@@ -16,7 +16,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
// The timestamps for the changed event are UTC
|
// The timestamps for the changed event are UTC
|
||||||
const emit = defineEmits(['changed']);
|
const emit = defineEmits(['changed', 'close']);
|
||||||
|
|
||||||
const tempStart = ref(
|
const tempStart = ref(
|
||||||
props.start ? getLocalizedDayJs(props.start).format() : dayjs().format()
|
props.start ? getLocalizedDayJs(props.start).format() : dayjs().format()
|
||||||
@@ -58,24 +58,26 @@ watch(focused, (newValue, oldValue) => {
|
|||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<div class="font-bold text-white text-sm pb-2">Start</div>
|
<div class="font-bold text-white text-sm pb-2">Start</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<TimePicker
|
<TimePickerSimple
|
||||||
data-testid="time_entry_range_start"
|
data-testid="time_entry_range_start"
|
||||||
|
tabindex="0"
|
||||||
:focus
|
:focus
|
||||||
@changed="updateTimeEntry"
|
@changed="updateTimeEntry"
|
||||||
v-model="tempStart"></TimePicker>
|
v-model="tempStart"></TimePickerSimple>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
|
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
|
||||||
@changed="updateTimeEntry"
|
@changed="updateTimeEntry"
|
||||||
|
@blur.stop.prevent="emit('close')"
|
||||||
v-model="tempStart"></DatePicker>
|
v-model="tempStart"></DatePicker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<div class="font-bold text-white text-sm pb-2">End</div>
|
<div class="font-bold text-white text-sm pb-2">End</div>
|
||||||
<div v-if="tempEnd !== null" class="space-y-2">
|
<div v-if="tempEnd !== null" class="space-y-2">
|
||||||
<TimePicker
|
<TimePickerSimple
|
||||||
data-testid="time_entry_range_end"
|
data-testid="time_entry_range_end"
|
||||||
@changed="updateTimeEntry"
|
@changed="updateTimeEntry"
|
||||||
v-model="tempEnd"></TimePicker>
|
v-model="tempEnd"></TimePickerSimple>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
|
class="text-xs text-text-tertiary max-w-24 px-1.5 py-1.5"
|
||||||
@changed="updateTimeEntry"
|
@changed="updateTimeEntry"
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ defineProps<{
|
|||||||
<Dropdown align="bottom-end">
|
<Dropdown align="bottom-end">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
class="focus-visible:outline-none focus-visible:bg-card-background rounded-full focus-visible:ring-1 focus-visible:ring-input-border-active 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-muted"
|
||||||
:aria-label="label">
|
:aria-label="label">
|
||||||
<svg
|
<svg
|
||||||
class="h-10 w-10 p-2 rounded-full"
|
class="h-8 w-8 p-1 rounded-full"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -28,10 +28,8 @@ const indicatorClasses = {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Badge :name :size :tag :class="props.class" :color :border>
|
<Badge :name :size :tag :class="props.class" :color :border>
|
||||||
<TagIcon
|
<TagIcon :class="twMerge(indicatorClasses[size])"></TagIcon>
|
||||||
:style="{ color: color }"
|
<span v-if="name">
|
||||||
:class="twMerge(indicatorClasses[size])"></TagIcon>
|
|
||||||
<span>
|
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
import TimeEntryRow from '@/packages/ui/src/TimeEntry/TimeEntryRow.vue';
|
||||||
import GroupedItemsCountButton from '@/packages/ui/src/GroupedItemsCountButton.vue';
|
import GroupedItemsCountButton from '@/packages/ui/src/GroupedItemsCountButton.vue';
|
||||||
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
|
import type { TimeEntriesGroupedByType } from '@/types/time-entries';
|
||||||
|
import { Checkbox } from '@/packages/ui/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
timeEntry: TimeEntriesGroupedByType;
|
timeEntry: TimeEntriesGroupedByType;
|
||||||
@@ -76,6 +77,7 @@ function updateProjectAndTask(projectId: string, taskId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
|
|
||||||
function onSelectChange(event: Event) {
|
function onSelectChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
if (target.checked) {
|
if (target.checked) {
|
||||||
@@ -92,10 +94,10 @@ function onSelectChange(event: Event) {
|
|||||||
data-testid="time_entry_row">
|
data-testid="time_entry_row">
|
||||||
<MainContainer class="min-w-0">
|
<MainContainer class="min-w-0">
|
||||||
<div
|
<div
|
||||||
class="sm:flex py-0.5 items-center min-w-0 justify-between group">
|
class="sm:flex py-2 items-center min-w-0 justify-between group">
|
||||||
<div class="flex space-x-3 items-center min-w-0">
|
<div class="flex space-x-3 items-center min-w-0">
|
||||||
<input
|
<Checkbox
|
||||||
@change="onSelectChange"
|
@update:checked="onSelectChange"
|
||||||
:checked="
|
:checked="
|
||||||
timeEntry.timeEntries.every(
|
timeEntry.timeEntries.every(
|
||||||
(aggregateTimeEntry: TimeEntry) =>
|
(aggregateTimeEntry: TimeEntry) =>
|
||||||
@@ -103,9 +105,7 @@ function onSelectChange(event: Event) {
|
|||||||
aggregateTimeEntry
|
aggregateTimeEntry
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
"
|
" />
|
||||||
type="checkbox"
|
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
|
||||||
<div class="flex items-center min-w-0">
|
<div class="flex items-center min-w-0">
|
||||||
<GroupedItemsCountButton
|
<GroupedItemsCountButton
|
||||||
:expanded="expanded"
|
:expanded="expanded"
|
||||||
@@ -113,7 +113,7 @@ function onSelectChange(event: Event) {
|
|||||||
{{ timeEntry?.timeEntries?.length }}
|
{{ timeEntry?.timeEntries?.length }}
|
||||||
</GroupedItemsCountButton>
|
</GroupedItemsCountButton>
|
||||||
<TimeEntryDescriptionInput
|
<TimeEntryDescriptionInput
|
||||||
class="min-w-0"
|
class="min-w-0 mr-4"
|
||||||
@changed="updateTimeEntryDescription"
|
@changed="updateTimeEntryDescription"
|
||||||
:modelValue="
|
:modelValue="
|
||||||
timeEntry.description
|
timeEntry.description
|
||||||
@@ -130,6 +130,7 @@ function onSelectChange(event: Event) {
|
|||||||
:project="timeEntry.project_id"
|
:project="timeEntry.project_id"
|
||||||
:enableEstimatedTime
|
:enableEstimatedTime
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
|
class="border border-border-primary"
|
||||||
:task="
|
:task="
|
||||||
timeEntry.task_id
|
timeEntry.task_id
|
||||||
"></TimeTrackerProjectTaskDropdown>
|
"></TimeTrackerProjectTaskDropdown>
|
||||||
@@ -143,6 +144,7 @@ function onSelectChange(event: Event) {
|
|||||||
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
|
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
|
||||||
<BillableToggleButton
|
<BillableToggleButton
|
||||||
:modelValue="timeEntry.billable"
|
:modelValue="timeEntry.billable"
|
||||||
|
class="opacity-50 focus-visible:opacity-100 group-hover:opacity-100"
|
||||||
size="small"
|
size="small"
|
||||||
@changed="
|
@changed="
|
||||||
updateTimeEntryBillable
|
updateTimeEntryBillable
|
||||||
@@ -150,13 +152,13 @@ function onSelectChange(event: Event) {
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<button
|
<button
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
class="hidden lg:block text-muted w-[110px] px-2 py-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium">
|
class="hidden lg:block text-muted w-[105px] 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">
|
||||||
{{ formatStartEnd(timeEntry.start, timeEntry.end) }}
|
{{ formatStartEnd(timeEntry.start, timeEntry.end) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
class="text-white w-[100px] px-3 py-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold">
|
class="text-white min-w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary">
|
||||||
{{
|
{{
|
||||||
formatHumanReadableDuration(timeEntry.duration ?? 0)
|
formatHumanReadableDuration(timeEntry.duration ?? 0)
|
||||||
}}
|
}}
|
||||||
@@ -165,7 +167,7 @@ function onSelectChange(event: Event) {
|
|||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
@changed="onStartStopClick(timeEntry)"
|
@changed="onStartStopClick(timeEntry)"
|
||||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||||
class="opacity-20 hidden sm:flex group-hover:opacity-100"></TimeTrackerStartStop>
|
class="opacity-20 hidden sm:flex group-hover:opacity-100 focus-visible:opacity-100"></TimeTrackerStartStop>
|
||||||
<TimeEntryMoreOptionsDropdown
|
<TimeEntryMoreOptionsDropdown
|
||||||
@delete="
|
@delete="
|
||||||
deleteTimeEntries(timeEntry?.timeEntries ?? [])
|
deleteTimeEntries(timeEntry?.timeEntries ?? [])
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const displaysPlaceholder = computed(() => {
|
|||||||
<div class="relative text-sm font-medium min-w-0">
|
<div class="relative text-sm font-medium min-w-0">
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'opacity-0 py-2 text-sm whitespace-pre min-w-0 pl-3 pr-1',
|
'opacity-0 h-4 text-sm whitespace-pre font-medium min-w-0 pl-3 pr-1',
|
||||||
{ 'min-w-[130px]': displaysPlaceholder },
|
{ 'min-w-[130px]': displaysPlaceholder },
|
||||||
]">
|
]">
|
||||||
{{ liveDataValue }}
|
{{ liveDataValue }}
|
||||||
|
|||||||
@@ -88,14 +88,14 @@ const showMassUpdateModal = ref(false);
|
|||||||
>Select All</InputLabel
|
>Select All</InputLabel
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-text-tertiary flex space-x-1 items-center hover:text-text-secondary transition focus-visible:ring-2 outline-0 focus-visible:text-text-primary focus-visible:ring-white/80 rounded h-full px-2"
|
class="text-text-tertiary flex space-x-1 items-center hover:text-text-secondary transition focus-visible:ring-2 outline-0 focus-visible:text-text-primary focus-visible:ring-ring rounded h-full px-2"
|
||||||
@click="showMassUpdateModal = true"
|
@click="showMassUpdateModal = true"
|
||||||
v-if="selectedTimeEntries.length">
|
v-if="selectedTimeEntries.length">
|
||||||
<PencilSquareIcon class="w-4"></PencilSquareIcon>
|
<PencilSquareIcon class="w-4"></PencilSquareIcon>
|
||||||
<span> Edit </span>
|
<span> Edit </span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="text-red-400 h-full px-2 space-x-1 items-center flex hover:text-red-500 transition focus-visible:ring-2 outline-0 focus-visible:text-red-500 focus-visible:ring-white/80 rounded"
|
class="text-red-400 h-full px-2 space-x-1 items-center flex hover:text-red-500 transition focus-visible:ring-2 outline-0 focus-visible:text-red-500 focus-visible:ring-ring rounded"
|
||||||
@click="deleteSelected"
|
@click="deleteSelected"
|
||||||
v-if="selectedTimeEntries.length">
|
v-if="selectedTimeEntries.length">
|
||||||
<TrashIcon class="w-3.5"></TrashIcon>
|
<TrashIcon class="w-3.5"></TrashIcon>
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ const open = ref(false);
|
|||||||
data-testid="time_entry_range_selector"
|
data-testid="time_entry_range_selector"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
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-visible:ring-2 focus-visible:ring-white/80',
|
'text-muted w-[105px] 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
|
showDate
|
||||||
? 'text-xs py-1.5 font-semibold'
|
? 'text-xs py-1.5 font-semibold'
|
||||||
: 'text-sm py-2 font-medium',
|
: 'text-sm py-1.5 font-medium',
|
||||||
open && 'border-card-border bg-card-background'
|
open && 'border-card-border bg-card-background'
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import TimeEntryMoreOptionsDropdown from '@/packages/ui/src/TimeEntry/TimeEntryM
|
|||||||
import BillableToggleButton from '@/packages/ui/src/Input/BillableToggleButton.vue';
|
import BillableToggleButton from '@/packages/ui/src/Input/BillableToggleButton.vue';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
import TimeTrackerProjectTaskDropdown from '@/packages/ui/src/TimeTracker/TimeTrackerProjectTaskDropdown.vue';
|
||||||
|
import { Checkbox } from '@/packages/ui/src';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
timeEntry: TimeEntry;
|
timeEntry: TimeEntry;
|
||||||
@@ -96,16 +97,15 @@ function onSelectChange(event: Event) {
|
|||||||
data-testid="time_entry_row">
|
data-testid="time_entry_row">
|
||||||
<MainContainer class="min-w-0">
|
<MainContainer class="min-w-0">
|
||||||
<div
|
<div
|
||||||
class="sm:flex py-0.5 min-w-0 items-center justify-between group">
|
class="sm:flex py-2 min-w-0 items-center justify-between group">
|
||||||
<div class="flex space-x-1 items-center min-w-0">
|
<div class="flex items-center min-w-0">
|
||||||
<input
|
<Checkbox
|
||||||
@change="onSelectChange"
|
@update:checked="onSelectChange"
|
||||||
:checked="selected"
|
:checked="selected" />
|
||||||
type="checkbox"
|
<div class="w-10 h-7" v-if="indent === true"></div>
|
||||||
class="h-4 w-4 rounded bg-card-background border-input-border text-accent-500/80 focus:ring-accent-500/80" />
|
|
||||||
<div class="w-7 h-7" v-if="indent === true"></div>
|
|
||||||
<TimeEntryDescriptionInput
|
<TimeEntryDescriptionInput
|
||||||
@changed="updateTimeEntryDescription"
|
@changed="updateTimeEntryDescription"
|
||||||
|
class="min-w-0 mr-4"
|
||||||
:modelValue="
|
:modelValue="
|
||||||
timeEntry.description
|
timeEntry.description
|
||||||
"></TimeEntryDescriptionInput>
|
"></TimeEntryDescriptionInput>
|
||||||
@@ -120,12 +120,14 @@ function onSelectChange(event: Event) {
|
|||||||
@changed="updateProjectAndTask"
|
@changed="updateProjectAndTask"
|
||||||
:project="timeEntry.project_id"
|
:project="timeEntry.project_id"
|
||||||
:currency="currency"
|
:currency="currency"
|
||||||
|
class="border border-border-primary"
|
||||||
:enableEstimatedTime
|
:enableEstimatedTime
|
||||||
:task="
|
:task="
|
||||||
timeEntry.task_id
|
timeEntry.task_id
|
||||||
"></TimeTrackerProjectTaskDropdown>
|
"></TimeTrackerProjectTaskDropdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center font-medium lg:space-x-2">
|
<div
|
||||||
|
class="flex items-center font-medium space-x-1 lg:space-x-2">
|
||||||
<div class="text-sm px-2" v-if="showMember && members">
|
<div class="text-sm px-2" v-if="showMember && members">
|
||||||
{{ memberName }}
|
{{ memberName }}
|
||||||
</div>
|
</div>
|
||||||
@@ -136,6 +138,7 @@ function onSelectChange(event: Event) {
|
|||||||
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
|
:modelValue="timeEntry.tags"></TimeEntryRowTagDropdown>
|
||||||
<BillableToggleButton
|
<BillableToggleButton
|
||||||
:modelValue="timeEntry.billable"
|
:modelValue="timeEntry.billable"
|
||||||
|
class="opacity-50 group-hover:opacity-100 focus-visible:opacity-100"
|
||||||
size="small"
|
size="small"
|
||||||
@changed="
|
@changed="
|
||||||
updateTimeEntryBillable
|
updateTimeEntryBillable
|
||||||
@@ -159,7 +162,7 @@ function onSelectChange(event: Event) {
|
|||||||
<TimeTrackerStartStop
|
<TimeTrackerStartStop
|
||||||
@changed="onStartStopClick"
|
@changed="onStartStopClick"
|
||||||
:active="!!(timeEntry.start && !timeEntry.end)"
|
:active="!!(timeEntry.start && !timeEntry.end)"
|
||||||
class="opacity-20 hidden sm:flex group-hover:opacity-100"></TimeTrackerStartStop>
|
class="opacity-20 hidden sm:flex focus-visible:opacity-100 group-hover:opacity-100"></TimeTrackerStartStop>
|
||||||
<TimeEntryMoreOptionsDropdown
|
<TimeEntryMoreOptionsDropdown
|
||||||
@delete="
|
@delete="
|
||||||
deleteTimeEntry
|
deleteTimeEntry
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function selectInput(event: Event) {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<input
|
<input
|
||||||
data-testid="time_entry_duration_input"
|
data-testid="time_entry_duration_input"
|
||||||
class="text-white w-[100px] px-3 py-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-white/80"
|
class="text-white w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
@focus="selectInput"
|
@focus="selectInput"
|
||||||
@keydown.tab="open = false"
|
@keydown.tab="open = false"
|
||||||
@blur="updateTimerAndStartLiveTimerUpdate"
|
@blur="updateTimerAndStartLiveTimerUpdate"
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function selectUnselectAll(value: boolean) {
|
|||||||
{{ formatDate(date) }}
|
{{ formatDate(date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted pr-[90px] lg:pr-[108px]">
|
<div class="text-muted pr-[90px] lg:pr-[92px]">
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
{{ formatHumanReadableDuration(duration) }}
|
{{ formatHumanReadableDuration(duration) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ const timeEntryTags = computed<Tag[]>(() => {
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
data-testid="time_entry_tag_dropdown"
|
data-testid="time_entry_tag_dropdown"
|
||||||
class="opacity-50 group-hover:opacity-100 transition">
|
class="opacity-50 group-hover:opacity-100 group/dropdown focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:opacity-100 transition focus:bg-card-background-separator hover:bg-card-background-separator rounded-full flex items-center justify-center">
|
||||||
<TagBadge
|
<TagBadge
|
||||||
:border="false"
|
:border="false"
|
||||||
size="large"
|
size="large"
|
||||||
class="border-0"
|
class="border-0 sm:px-1.5 text-icon-default group-focus-within/dropdown:text-text-primary"
|
||||||
:name="
|
:name="
|
||||||
timeEntryTags.map((tag: Tag) => tag.name).join(', ')
|
timeEntryTags.map((tag: Tag) => tag.name).join(', ')
|
||||||
"></TagBadge>
|
"></TagBadge>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const project = defineModel<string | null>('project', {
|
|||||||
const searchInput = ref<HTMLInputElement | null>(null);
|
const searchInput = ref<HTMLInputElement | null>(null);
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const dropdownViewport = ref<HTMLElement | null>(null);
|
const dropdownViewport = ref<HTMLElement | null>(null);
|
||||||
|
import { UseFocusTrap } from '@vueuse/integrations/useFocusTrap/component';
|
||||||
|
|
||||||
const searchValue = ref('');
|
const searchValue = ref('');
|
||||||
|
|
||||||
@@ -586,132 +587,139 @@ const showCreateProject = ref(false);
|
|||||||
</ProjectBadge>
|
</ProjectBadge>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<input
|
<UseFocusTrap
|
||||||
:value="searchValue"
|
v-if="open"
|
||||||
@input="updateSearchValue"
|
:options="{ immediate: true, allowOutsideClick: true }">
|
||||||
@keydown.enter="addClientIfNoneExists"
|
<input
|
||||||
@click.prevent="searchInput?.focus()"
|
:value="searchValue"
|
||||||
data-testid="client_dropdown_search"
|
@input="updateSearchValue"
|
||||||
@keydown.up.prevent="moveHighlightUp"
|
@keydown.enter.prevent="addClientIfNoneExists"
|
||||||
@keydown.down.prevent="moveHighlightDown"
|
data-testid="client_dropdown_search"
|
||||||
@keydown.right.prevent="expandProject"
|
@keydown.up.prevent="moveHighlightUp"
|
||||||
@keydown.left.prevent="collapseProject"
|
@keydown.down.prevent="moveHighlightDown"
|
||||||
ref="searchInput"
|
@keydown.right.prevent="expandProject"
|
||||||
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
|
@keydown.left.prevent="collapseProject"
|
||||||
placeholder="Search for a project or task..." />
|
ref="searchInput"
|
||||||
<div
|
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-separator focus:border-card-background-separator w-full"
|
||||||
ref="dropdownViewport"
|
placeholder="Search for a project or task..." />
|
||||||
@mousemove="mouseEnterHighlightActivated = true"
|
<div
|
||||||
class="min-w-[350px] max-h-[350px] overflow-y-scroll relative">
|
ref="dropdownViewport"
|
||||||
<template v-for="client in filteredResults" :key="client.id">
|
@mousemove="mouseEnterHighlightActivated = true"
|
||||||
<div
|
class="min-w-[350px] max-h-[350px] overflow-y-scroll relative">
|
||||||
v-if="client.id !== 'no_project_no_client'"
|
|
||||||
class="w-full pb-1 pt-2 px-2 text-text-tertiary text-xs font-semibold flex space-x-1 items-center">
|
|
||||||
<span>
|
|
||||||
{{ client.name }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<template
|
<template
|
||||||
v-for="projectWithTasks in client.projects"
|
v-for="client in filteredResults"
|
||||||
:key="projectWithTasks.id">
|
:key="client.id">
|
||||||
<div
|
<div
|
||||||
role="option"
|
v-if="client.id !== 'no_project_no_client'"
|
||||||
class="px-1 py-0.5 cursor-default"
|
class="w-full pb-1 pt-2 px-2 text-text-tertiary text-xs font-semibold flex space-x-1 items-center">
|
||||||
:value="projectWithTasks.id"
|
<span>
|
||||||
@click="selectProject(projectWithTasks.id)"
|
{{ client.name }}
|
||||||
:data-project-id="projectWithTasks.id">
|
</span>
|
||||||
|
</div>
|
||||||
|
<template
|
||||||
|
v-for="projectWithTasks in client.projects"
|
||||||
|
:key="projectWithTasks.id">
|
||||||
<div
|
<div
|
||||||
class="rounded-lg"
|
role="option"
|
||||||
:class="{
|
class="px-1 py-0.5 cursor-default"
|
||||||
'bg-card-background-active':
|
:value="projectWithTasks.id"
|
||||||
projectWithTasks.id ===
|
@click="selectProject(projectWithTasks.id)"
|
||||||
highlightedItemId,
|
:data-project-id="projectWithTasks.id">
|
||||||
}">
|
<div
|
||||||
<ProjectDropdownItem
|
class="rounded-lg"
|
||||||
class="hover:!bg-transparent"
|
:class="{
|
||||||
:selected="
|
'bg-card-background-active':
|
||||||
isProjectSelected(projectWithTasks)
|
projectWithTasks.id ===
|
||||||
"
|
highlightedItemId,
|
||||||
@mouseenter="
|
}">
|
||||||
setHighlightItemId(projectWithTasks.id)
|
<ProjectDropdownItem
|
||||||
"
|
class="hover:!bg-transparent"
|
||||||
:name="projectWithTasks.name"
|
:selected="
|
||||||
:color="projectWithTasks.color">
|
isProjectSelected(projectWithTasks)
|
||||||
<template #actions>
|
"
|
||||||
<button
|
@mouseenter="
|
||||||
tabindex="-1"
|
setHighlightItemId(
|
||||||
v-if="
|
projectWithTasks.id
|
||||||
projectWithTasks.tasks.length >
|
)
|
||||||
0
|
"
|
||||||
"
|
:name="projectWithTasks.name"
|
||||||
@click.prevent.stop="
|
:color="projectWithTasks.color">
|
||||||
() => {
|
<template #actions>
|
||||||
projectWithTasks.expanded =
|
<button
|
||||||
!projectWithTasks.expanded;
|
tabindex="-1"
|
||||||
searchInput?.focus();
|
v-if="
|
||||||
}
|
|
||||||
"
|
|
||||||
class="px-2 py-0.5 mr-2 relative transition items-center rounded flex space-x-0.5 text-xs"
|
|
||||||
:class="{
|
|
||||||
'bg-white/5 text-text-secondary':
|
|
||||||
projectWithTasks.expanded,
|
|
||||||
'hover:bg-white/5 hover:text-text-secondary text-text-tertiary':
|
|
||||||
!projectWithTasks.expanded,
|
|
||||||
}">
|
|
||||||
<span
|
|
||||||
>{{
|
|
||||||
projectWithTasks.tasks
|
projectWithTasks.tasks
|
||||||
.length
|
.length > 0
|
||||||
}}
|
"
|
||||||
Tasks</span
|
@click.prevent.stop="
|
||||||
>
|
() => {
|
||||||
<ChevronDownIcon
|
projectWithTasks.expanded =
|
||||||
|
!projectWithTasks.expanded;
|
||||||
|
searchInput?.focus();
|
||||||
|
}
|
||||||
|
"
|
||||||
|
class="px-2 py-0.5 mr-2 relative transition items-center rounded flex space-x-0.5 text-xs"
|
||||||
:class="{
|
:class="{
|
||||||
'transform rotate-180':
|
'bg-white/5 text-text-secondary':
|
||||||
projectWithTasks.expanded,
|
projectWithTasks.expanded,
|
||||||
}"
|
'hover:bg-white/5 hover:text-text-secondary text-text-tertiary':
|
||||||
class="w-4"></ChevronDownIcon>
|
!projectWithTasks.expanded,
|
||||||
</button>
|
}">
|
||||||
</template>
|
<span
|
||||||
</ProjectDropdownItem>
|
>{{
|
||||||
|
projectWithTasks.tasks
|
||||||
|
.length
|
||||||
|
}}
|
||||||
|
Tasks</span
|
||||||
|
>
|
||||||
|
<ChevronDownIcon
|
||||||
|
:class="{
|
||||||
|
'transform rotate-180':
|
||||||
|
projectWithTasks.expanded,
|
||||||
|
}"
|
||||||
|
class="w-4"></ChevronDownIcon>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</ProjectDropdownItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="projectWithTasks.expanded"
|
|
||||||
class="bg-quaternary">
|
|
||||||
<div
|
<div
|
||||||
v-for="task in projectWithTasks.tasks"
|
v-if="projectWithTasks.expanded"
|
||||||
:key="task.id"
|
class="bg-quaternary">
|
||||||
@click="selectTask(task.id)"
|
<div
|
||||||
@mouseenter="setHighlightItemId(task.id)"
|
v-for="task in projectWithTasks.tasks"
|
||||||
:data-task-id="task.id"
|
:key="task.id"
|
||||||
:class="{
|
@click="selectTask(task.id)"
|
||||||
'bg-card-background-active':
|
@mouseenter="setHighlightItemId(task.id)"
|
||||||
task.id === highlightedItemId,
|
:data-task-id="task.id"
|
||||||
}"
|
:class="{
|
||||||
class="flex items-center space-x-2 w-full px-5 py-1.5 text-start text-xs font-semibold leading-5 text-white focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
'bg-card-background-active':
|
||||||
<MinusIcon
|
task.id === highlightedItemId,
|
||||||
class="w-3 h-3 text-text-quaternary"></MinusIcon>
|
}"
|
||||||
<span>{{ task.name }}</span>
|
class="flex items-center space-x-2 w-full px-5 py-1.5 text-start text-xs font-semibold leading-5 text-white focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
|
||||||
|
<MinusIcon
|
||||||
|
class="w-3 h-3 text-text-quaternary"></MinusIcon>
|
||||||
|
<span>{{ task.name }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div
|
v-if="canCreateProject"
|
||||||
v-if="canCreateProject"
|
class="hover:bg-card-background-active rounded-b-lg">
|
||||||
class="hover:bg-card-background-active rounded-b-lg">
|
<button
|
||||||
<button
|
@click="
|
||||||
@click="
|
open = false;
|
||||||
open = false;
|
showCreateProject = true;
|
||||||
showCreateProject = true;
|
"
|
||||||
"
|
class="text-white flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator">
|
||||||
class="text-white flex space-x-3 items-center px-4 py-3 text-xs font-semibold border-t border-card-background-separator">
|
<PlusCircleIcon
|
||||||
<PlusCircleIcon
|
class="w-5 flex-shrink-0 text-icon-default"></PlusCircleIcon>
|
||||||
class="w-5 flex-shrink-0 text-icon-default"></PlusCircleIcon>
|
<span>Create new Project</span>
|
||||||
<span>Create new Project</span>
|
</button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</UseFocusTrap>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<ProjectCreateModal
|
<ProjectCreateModal
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ defineProps<{
|
|||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
iconColorClasses,
|
iconColorClasses,
|
||||||
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus-visible:bg-card-background-separator hover:bg-card-background-separator rounded-full w-11 h-11 flex items-center justify-center'
|
'flex-shrink-0 ring-0 focus:outline-none focus:ring-2 focus:ring-ring transition focus-visible:bg-card-background-separator hover:bg-card-background-separator rounded-full w-10 h-10 flex items-center justify-center'
|
||||||
)
|
)
|
||||||
">
|
">
|
||||||
<TagIcon class="w-5 h-5 lg:h-6 lg:w-6"></TagIcon>
|
<TagIcon class="w-5 h-5 lg:h-6 lg:w-6"></TagIcon>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const props = withDefaults(
|
|||||||
);
|
);
|
||||||
const buttonSizeClasses = {
|
const buttonSizeClasses = {
|
||||||
small: 'w-6 h-6 bg-accent-200/40 hover:bg-accent-300/70',
|
small: 'w-6 h-6 bg-accent-200/40 hover:bg-accent-300/70',
|
||||||
base: 'w-8 h-8 bg-accent-200/40 hover:scale-110 hover:bg-accent-300/70 ring-accent-200/10 focus-visible:ring-white/80 ring-4 hover:ring-4',
|
base: 'w-8 h-8 bg-accent-200/40 hover:scale-110 hover:bg-accent-300/70 ring-accent-200/10 focus-visible:ring-ring ring-4 hover:ring-4',
|
||||||
large: 'w-11 h-11 ring-accent-200/10 focus-visible:ring-white/80 ring-4 sm:ring-[6px] hover:scale-110',
|
large: 'w-11 h-11 ring-accent-200/10 focus-visible:ring-ring focus-visible:ring-2 ring-4 sm:ring-[6px] hover:scale-110',
|
||||||
};
|
};
|
||||||
const iconClass = {
|
const iconClass = {
|
||||||
small: 'w-2.5 h-2.5',
|
small: 'w-2.5 h-2.5',
|
||||||
@@ -29,7 +29,7 @@ const buttonColorClasses = computed(() => {
|
|||||||
if (props.active) {
|
if (props.active) {
|
||||||
return 'bg-red-400/80 hover:bg-red-500/80 focus:bg-red-500/80';
|
return 'bg-red-400/80 hover:bg-red-500/80 focus:bg-red-500/80';
|
||||||
} else {
|
} else {
|
||||||
return 'bg-accent-300/70 hover:bg-accent-400/70 focus:bg-accent-500/70';
|
return 'bg-accent-300/70 hover:bg-accent-400/70 focus:bg-accent-700';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default {
|
|||||||
sans: ['Outfit', ...defaultTheme.fontFamily.sans],
|
sans: ['Outfit', ...defaultTheme.fontFamily.sans],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
'ring': 'rgba(255,255,255,0.5)',
|
||||||
'primary': 'var(--color-bg-primary)',
|
'primary': 'var(--color-bg-primary)',
|
||||||
'secondary': 'var(--color-bg-secondary)',
|
'secondary': 'var(--color-bg-secondary)',
|
||||||
'tertiary': 'var(--color-bg-tertiary)',
|
'tertiary': 'var(--color-bg-tertiary)',
|
||||||
|
|||||||
Reference in New Issue
Block a user