improve focus state styling

This commit is contained in:
Gregor Vostrak
2025-01-27 14:12:56 +01:00
parent dce608e403
commit b783ea9ecd
27 changed files with 319 additions and 178 deletions

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
) )
"> ">

View File

@@ -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' : '')

View File

@@ -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
) )

View File

@@ -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
) )

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
) )
" "

View 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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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 ?? [])

View File

@@ -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 }}

View File

@@ -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>

View File

@@ -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'
) )
"> ">

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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';
} }
}); });

View File

@@ -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)',