Files
zeroclock/src/components/AppNumberInput.vue
Your Name 514090eed4 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
2026-02-21 01:15:57 +02:00

160 lines
4.7 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
import { Minus, Plus } from 'lucide-vue-next'
interface Props {
modelValue: number
min?: number
max?: number
step?: number
precision?: number
prefix?: string
suffix?: string
label?: string
compact?: boolean
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: Infinity,
step: 1,
precision: 0,
prefix: '',
suffix: '',
label: 'Number input',
compact: false,
})
const emit = defineEmits<{
'update:modelValue': [value: number]
}>()
const isEditing = ref(false)
const editValue = ref('')
const inputRef = ref<HTMLInputElement | null>(null)
const displayValue = computed(() => {
return props.modelValue.toFixed(props.precision)
})
function setValue(val: number) {
const clamped = Math.min(props.max, Math.max(props.min, val))
const rounded = parseFloat(clamped.toFixed(props.precision))
emit('update:modelValue', rounded)
}
function increment() {
setValue(props.modelValue + props.step)
}
function decrement() {
setValue(props.modelValue - props.step)
}
// ── Press-and-hold ───────────────────────────────────────────────
let holdTimeout: ReturnType<typeof setTimeout> | null = null
let holdInterval: ReturnType<typeof setInterval> | null = null
function startHold(action: () => void) {
action()
holdTimeout = setTimeout(() => {
holdInterval = setInterval(action, 80)
}, 400)
}
function stopHold() {
if (holdTimeout) { clearTimeout(holdTimeout); holdTimeout = null }
if (holdInterval) { clearInterval(holdInterval); holdInterval = null }
}
// ── Inline editing ───────────────────────────────────────────────
function startEdit() {
isEditing.value = true
editValue.value = displayValue.value
setTimeout(() => inputRef.value?.select(), 0)
}
function commitEdit() {
isEditing.value = false
const parsed = parseFloat(editValue.value)
if (!isNaN(parsed)) {
setValue(parsed)
}
}
function cancelEdit() {
isEditing.value = false
}
</script>
<template>
<div
class="flex items-center"
:class="compact ? 'gap-1' : 'gap-2'"
role="group"
:aria-label="label"
>
<button
type="button"
aria-label="Decrease value"
v-tooltip="'Decrease'"
@mousedown.prevent="startHold(decrement)"
@mouseup="stopHold"
@mouseleave="stopHold"
@touchstart.prevent="startHold(decrement)"
@touchend="stopHold"
class="flex items-center justify-center border border-border-visible rounded-lg text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
:class="compact ? 'w-6 h-6' : 'w-8 h-8'"
:disabled="modelValue <= min"
>
<Minus :class="compact ? 'w-3 h-3' : 'w-3.5 h-3.5'" aria-hidden="true" :stroke-width="2" />
</button>
<div
v-if="!isEditing"
@click="startEdit"
@keydown.enter="startEdit"
@keydown.space.prevent="startEdit"
tabindex="0"
role="button"
:aria-label="'Edit value: ' + displayValue"
class="text-center font-mono text-text-primary cursor-text select-none"
:class="compact ? 'min-w-[2.5rem] text-[0.75rem]' : 'min-w-[4rem] text-[0.8125rem]'"
aria-live="polite"
>
<span v-if="prefix" class="text-text-tertiary">{{ prefix }}</span>
{{ displayValue }}
<span v-if="suffix" class="text-text-tertiary ml-0.5">{{ suffix }}</span>
</div>
<input
v-else
ref="inputRef"
v-model="editValue"
type="text"
inputmode="decimal"
:aria-label="label"
class="text-center px-1 py-0.5 bg-bg-inset border border-accent rounded-lg font-mono text-text-primary focus:outline-none"
:class="compact ? 'w-16 text-[0.75rem]' : 'w-20 text-[0.8125rem]'"
@blur="commitEdit"
@keydown.enter="commitEdit"
@keydown.escape="cancelEdit"
/>
<button
type="button"
aria-label="Increase value"
v-tooltip="'Increase'"
@mousedown.prevent="startHold(increment)"
@mouseup="stopHold"
@mouseleave="stopHold"
@touchstart.prevent="startHold(increment)"
@touchend="stopHold"
class="flex items-center justify-center border border-border-visible rounded-lg text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
:class="compact ? 'w-6 h-6' : 'w-8 h-8'"
:disabled="modelValue >= max"
>
<Plus :class="compact ? 'w-3 h-3' : 'w-3.5 h-3.5'" aria-hidden="true" :stroke-width="2" />
</button>
</div>
</template>