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

@@ -2,6 +2,7 @@
import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue'
import { Pipette } from 'lucide-vue-next'
import { computeDropdownPosition } from '../utils/dropdown'
import { useFocusTrap } from '../utils/focusTrap'
interface Props {
modelValue: string
@@ -16,6 +17,8 @@ const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const { activate: activateTrap, deactivate: deactivateTrap } = useFocusTrap()
const isOpen = ref(false)
const triggerRef = ref<HTMLButtonElement | null>(null)
const panelRef = ref<HTMLDivElement | null>(null)
@@ -245,7 +248,7 @@ function onHuePointerUp() {
function updatePosition() {
if (!triggerRef.value) return
panelStyle.value = computeDropdownPosition(triggerRef.value, { minWidth: 260, estimatedHeight: 330 })
panelStyle.value = computeDropdownPosition(triggerRef.value, { minWidth: 260, estimatedHeight: 330, panelEl: panelRef.value })
}
// ── Open / Close ────────────────────────────────────────────────────
@@ -261,15 +264,18 @@ function open() {
updatePosition()
nextTick(() => {
updatePosition()
drawGradient()
drawHueStrip()
document.addEventListener('click', onClickOutside, true)
document.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
if (panelRef.value) activateTrap(panelRef.value)
})
}
function close() {
deactivateTrap()
isOpen.value = false
document.removeEventListener('click', onClickOutside, true)
document.removeEventListener('scroll', onScrollOrResize, true)
@@ -296,6 +302,34 @@ onBeforeUnmount(() => {
document.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
})
// ── Keyboard Handlers for Accessibility ─────────────────────────────
function onGradientKeydown(e: KeyboardEvent) {
const step = 5
let handled = false
if (e.key === 'ArrowRight') { saturation.value = Math.min(100, saturation.value + step); handled = true }
else if (e.key === 'ArrowLeft') { saturation.value = Math.max(0, saturation.value - step); handled = true }
else if (e.key === 'ArrowUp') { brightness.value = Math.min(100, brightness.value + step); handled = true }
else if (e.key === 'ArrowDown') { brightness.value = Math.max(0, brightness.value - step); handled = true }
if (handled) {
e.preventDefault()
emitColor()
}
}
function onHueKeydown(e: KeyboardEvent) {
const step = 5
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault()
hue.value = Math.min(360, hue.value + step)
emitColor()
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault()
hue.value = Math.max(0, hue.value - step)
emitColor()
}
}
</script>
<template>
@@ -305,6 +339,9 @@ onBeforeUnmount(() => {
ref="triggerRef"
type="button"
@click="toggle"
aria-label="Color picker"
:aria-expanded="isOpen"
aria-haspopup="dialog"
class="w-full flex items-center gap-2.5 px-3 py-2 bg-bg-inset border border-border-subtle rounded-xl text-[0.8125rem] text-left cursor-pointer transition-colors"
:style="
isOpen
@@ -317,20 +354,26 @@ onBeforeUnmount(() => {
"
>
<span
role="img"
:aria-label="'Current color: ' + (modelValue?.toUpperCase() || '#000000')"
class="w-5 h-5 rounded-md border border-border-subtle shrink-0"
:style="{ backgroundColor: modelValue }"
/>
<span class="text-text-primary font-mono text-[0.75rem] tracking-wide">{{ modelValue?.toUpperCase() || '#000000' }}</span>
<Pipette class="w-4 h-4 text-text-secondary shrink-0 ml-auto" :stroke-width="2" />
<Pipette class="w-4 h-4 text-text-secondary shrink-0 ml-auto" :stroke-width="2" aria-hidden="true" />
</button>
<!-- Color picker popover -->
<Teleport to="body">
<Teleport to="#app">
<Transition name="dropdown">
<div
v-if="isOpen"
ref="panelRef"
:style="panelStyle"
role="dialog"
aria-modal="true"
aria-label="Choose color"
@keydown.escape.prevent="close"
class="bg-bg-surface border border-border-visible rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.4)] overflow-hidden"
>
<!-- Preset swatches -->
@@ -341,7 +384,9 @@ onBeforeUnmount(() => {
:key="c"
type="button"
@click="selectPreset(c)"
class="w-6 h-6 rounded-full border-2 transition-colors cursor-pointer hover:scale-110"
:aria-label="'Color preset ' + c"
:aria-pressed="currentHex === c.toUpperCase()"
class="w-8 h-8 rounded-full border-2 transition-colors cursor-pointer hover:scale-110"
:class="currentHex === c.toUpperCase() ? 'border-text-primary' : 'border-transparent'"
:style="{ backgroundColor: c }"
/>
@@ -351,11 +396,15 @@ onBeforeUnmount(() => {
<!-- Saturation/Brightness gradient -->
<div class="px-3 pb-2">
<div
role="application"
aria-label="Saturation and brightness"
tabindex="0"
class="relative rounded-lg overflow-hidden cursor-crosshair"
style="touch-action: none;"
@pointerdown="onGradientPointerDown"
@pointermove="onGradientPointerMove"
@pointerup="onGradientPointerUp"
@keydown="onGradientKeydown"
>
<canvas
ref="gradientRef"
@@ -378,11 +427,18 @@ onBeforeUnmount(() => {
<!-- Hue slider -->
<div class="px-3 pb-2">
<div
role="slider"
aria-label="Hue"
:aria-valuenow="Math.round(hue)"
aria-valuemin="0"
aria-valuemax="360"
tabindex="0"
class="relative rounded-md overflow-hidden cursor-pointer"
style="touch-action: none;"
@pointerdown="onHuePointerDown"
@pointermove="onHuePointerMove"
@pointerup="onHuePointerUp"
@keydown="onHueKeydown"
>
<canvas
ref="hueRef"
@@ -404,6 +460,8 @@ onBeforeUnmount(() => {
<!-- Hex input + preview -->
<div class="px-3 pb-3 flex items-center gap-2">
<span
role="img"
:aria-label="'Selected color: ' + currentHex"
class="w-8 h-8 rounded-lg border border-border-subtle shrink-0"
:style="{ backgroundColor: currentHex }"
/>
@@ -412,6 +470,7 @@ onBeforeUnmount(() => {
@input="onHexInput"
type="text"
maxlength="7"
aria-label="Hex color value"
class="flex-1 px-2.5 py-1.5 bg-bg-inset border border-border-subtle rounded-lg text-[0.75rem] text-text-primary font-mono tracking-wide placeholder-text-tertiary focus:outline-none focus:border-border-visible"
placeholder="#D97706"
/>