feat: tooltips, two-column timer, font selector, tray behavior, icons, readme

- Custom tooltip directive (WCAG AAA) on every button in the app
- Two-column timer layout with sticky hero and recent entries sidebar
- Timer font selector with 16 monospace Google Fonts and live preview
- UI font selector with 15+ Google Fonts
- Close-to-tray and minimize-to-tray settings
- New app icons (no-glow variants), platform icon set
- Mini timer pop-out window
- Favorites strip with drag-reorder and inline actions
- Comprehensive README with feature documentation
- Remove tracked files that belong in gitignore
This commit is contained in:
Your Name
2026-02-21 01:15:57 +02:00
parent 2608f447de
commit 514090eed4
144 changed files with 13351 additions and 3456 deletions

View File

@@ -21,6 +21,7 @@ const triggerRef = ref<HTMLDivElement | null>(null)
const panelRef = ref<HTMLDivElement | null>(null)
const panelStyle = ref<Record<string, string>>({})
const inputRef = ref<HTMLInputElement | null>(null)
const highlightedIndex = ref(-1)
const selectedTags = computed(() => {
return tagsStore.tags.filter(t => t.id && props.modelValue.includes(t.id))
@@ -69,14 +70,17 @@ async function createAndAdd() {
function updatePosition() {
if (!triggerRef.value) return
panelStyle.value = computeDropdownPosition(triggerRef.value, { minWidth: 200, estimatedHeight: 200 })
panelStyle.value = computeDropdownPosition(triggerRef.value, { minWidth: 200, estimatedHeight: 200, panelEl: panelRef.value })
}
function open() {
isOpen.value = true
searchQuery.value = ''
updatePosition()
nextTick(() => inputRef.value?.focus())
nextTick(() => {
updatePosition()
inputRef.value?.focus()
})
document.addEventListener('click', onClickOutside, true)
document.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
@@ -99,6 +103,30 @@ function onScrollOrResize() {
if (isOpen.value) updatePosition()
}
function onSearchKeydown(e: KeyboardEvent) {
const total = filteredTags.value.length + (showCreateOption.value ? 1 : 0)
if (e.key === 'ArrowDown') {
e.preventDefault()
highlightedIndex.value = Math.min(highlightedIndex.value + 1, total - 1)
} else if (e.key === 'ArrowUp') {
e.preventDefault()
highlightedIndex.value = Math.max(highlightedIndex.value - 1, -1)
} else if (e.key === 'Enter' && highlightedIndex.value >= 0) {
e.preventDefault()
if (highlightedIndex.value < filteredTags.value.length) {
const tag = filteredTags.value[highlightedIndex.value]
toggleTag(tag.id!)
searchQuery.value = ''
} else if (showCreateOption.value) {
createAndAdd()
}
highlightedIndex.value = -1
} else if (e.key === 'Escape') {
e.preventDefault()
close()
}
}
onBeforeUnmount(() => {
document.removeEventListener('click', onClickOutside, true)
document.removeEventListener('scroll', onScrollOrResize, true)
@@ -116,30 +144,34 @@ onBeforeUnmount(() => {
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[0.6875rem] text-text-primary"
:style="{ backgroundColor: tag.color + '22', borderColor: tag.color + '44', border: '1px solid' }"
>
<span class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: tag.color }" />
<span class="w-1.5 h-1.5 rounded-full" :style="{ backgroundColor: tag.color }" aria-hidden="true" />
{{ tag.name }}
<button @click.stop="removeTag(tag.id!)" class="ml-0.5 hover:text-status-error">
<X class="w-2.5 h-2.5" />
<button @click.stop="removeTag(tag.id!)" :aria-label="'Remove tag ' + tag.name" v-tooltip="'Remove tag'" class="ml-0.5 min-w-[24px] min-h-[24px] flex items-center justify-center hover:text-status-error">
<X class="w-2.5 h-2.5" aria-hidden="true" />
</button>
</span>
<button
key="__add_btn__"
type="button"
@click="isOpen ? close() : open()"
aria-label="Add tag"
:aria-expanded="isOpen"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[0.6875rem] text-text-tertiary border border-border-subtle hover:text-text-secondary hover:border-border-visible transition-colors"
>
<Plus class="w-3 h-3" />
<Plus class="w-3 h-3" aria-hidden="true" />
Tag
</button>
</TransitionGroup>
<!-- Dropdown -->
<Teleport to="body">
<Teleport to="#app">
<Transition name="dropdown">
<div
v-if="isOpen"
ref="panelRef"
:style="panelStyle"
role="listbox"
aria-label="Tag options"
class="bg-bg-surface border border-border-visible rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.4)] overflow-hidden"
>
<div class="px-2 pt-2 pb-1">
@@ -147,26 +179,33 @@ onBeforeUnmount(() => {
ref="inputRef"
v-model="searchQuery"
type="text"
aria-label="Search or create tag"
@keydown="onSearchKeydown"
class="w-full px-2.5 py-1.5 bg-bg-inset border border-border-subtle rounded-lg text-[0.75rem] text-text-primary placeholder-text-tertiary focus:outline-none focus:border-border-visible"
placeholder="Search or create tag..."
/>
</div>
<div class="max-h-[160px] overflow-y-auto py-1">
<div
v-for="tag in filteredTags"
v-for="(tag, index) in filteredTags"
:key="tag.id"
:id="'tag-option-' + tag.id"
role="option"
@click="toggleTag(tag.id!); searchQuery = ''"
class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-bg-elevated transition-colors"
:class="{ 'bg-bg-elevated': index === highlightedIndex }"
>
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: tag.color }" />
<span class="w-2 h-2 rounded-full" :style="{ backgroundColor: tag.color }" aria-hidden="true" />
<span class="text-[0.75rem] text-text-primary">{{ tag.name }}</span>
</div>
<div
v-if="showCreateOption"
role="option"
@click="createAndAdd"
class="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-bg-elevated transition-colors text-accent-text"
:class="{ 'bg-bg-elevated': filteredTags.length === highlightedIndex }"
>
<Plus class="w-3 h-3" />
<Plus class="w-3 h-3" aria-hidden="true" />
<span class="text-[0.75rem]">Create "{{ searchQuery.trim() }}"</span>
</div>
<div v-if="filteredTags.length === 0 && !showCreateOption" class="px-3 py-3 text-center text-[0.75rem] text-text-tertiary">