164 lines
4.8 KiB
Svelte
164 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { ChevronUp, ChevronDown } from 'lucide-svelte';
|
|
|
|
interface Props {
|
|
hours: number;
|
|
minutes: number;
|
|
onChange: (hours: number, minutes: number) => void;
|
|
label?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
let { hours, minutes, onChange, label, disabled = false }: Props = $props();
|
|
|
|
function incrementHours() {
|
|
if (disabled) return;
|
|
const newHours = hours + 1;
|
|
onChange(newHours, minutes);
|
|
}
|
|
|
|
function decrementHours() {
|
|
if (disabled) return;
|
|
const newHours = Math.max(0, hours - 1);
|
|
onChange(newHours, minutes);
|
|
}
|
|
|
|
function incrementMinutes() {
|
|
if (disabled) return;
|
|
const newMinutes = minutes + 15;
|
|
if (newMinutes >= 60) {
|
|
const extraHours = Math.floor(newMinutes / 60);
|
|
const remainingMinutes = newMinutes % 60;
|
|
onChange(hours + extraHours, remainingMinutes);
|
|
} else {
|
|
onChange(hours, newMinutes);
|
|
}
|
|
}
|
|
|
|
function decrementMinutes() {
|
|
if (disabled) return;
|
|
const newMinutes = minutes - 15;
|
|
if (newMinutes < 0) {
|
|
if (hours > 0) {
|
|
onChange(hours - 1, 60 + newMinutes);
|
|
} else {
|
|
onChange(0, 0);
|
|
}
|
|
} else {
|
|
onChange(hours, newMinutes);
|
|
}
|
|
}
|
|
|
|
function handleHoursInput(e: Event) {
|
|
if (disabled) return;
|
|
const target = e.target as HTMLInputElement;
|
|
const value = parseInt(target.value) || 0;
|
|
onChange(Math.max(0, value), minutes);
|
|
}
|
|
|
|
function handleMinutesInput(e: Event) {
|
|
if (disabled) return;
|
|
const target = e.target as HTMLInputElement;
|
|
const value = parseInt(target.value) || 0;
|
|
const clampedValue = Math.max(0, Math.min(59, value));
|
|
onChange(hours, clampedValue);
|
|
}
|
|
|
|
const totalMinutes = $derived(hours * 60 + minutes);
|
|
const displayText = $derived.by(() => {
|
|
if (totalMinutes === 0) return '0h 0min';
|
|
const h = Math.floor(totalMinutes / 60);
|
|
const m = totalMinutes % 60;
|
|
return `${h}h ${m}min`;
|
|
});
|
|
</script>
|
|
|
|
<div class="time-picker">
|
|
{#if label}
|
|
<div class="mb-2 block text-sm font-medium text-gray-700">{label}</div>
|
|
{/if}
|
|
|
|
<div class="flex items-center gap-3">
|
|
<!-- Horas -->
|
|
<div class="flex flex-col items-center">
|
|
<button
|
|
type="button"
|
|
onclick={incrementHours}
|
|
disabled={disabled}
|
|
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ChevronUp class="h-4 w-4 text-gray-600" />
|
|
</button>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={hours}
|
|
oninput={handleHoursInput}
|
|
disabled={disabled}
|
|
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onclick={decrementHours}
|
|
disabled={disabled || hours === 0}
|
|
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ChevronDown class="h-4 w-4 text-gray-600" />
|
|
</button>
|
|
<span class="mt-1 text-xs text-gray-500">horas</span>
|
|
</div>
|
|
|
|
<!-- Separador -->
|
|
<div class="text-2xl font-bold text-gray-400">:</div>
|
|
|
|
<!-- Minutos -->
|
|
<div class="flex flex-col items-center">
|
|
<button
|
|
type="button"
|
|
onclick={incrementMinutes}
|
|
disabled={disabled}
|
|
class="flex h-10 w-12 items-center justify-center rounded-t-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ChevronUp class="h-4 w-4 text-gray-600" />
|
|
</button>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
max="59"
|
|
value={minutes}
|
|
oninput={handleMinutesInput}
|
|
disabled={disabled}
|
|
class="h-14 w-12 border-x border-gray-300 bg-white text-center text-xl font-bold text-gray-900 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 disabled:cursor-not-allowed disabled:bg-gray-50"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onclick={decrementMinutes}
|
|
disabled={disabled || (hours === 0 && minutes === 0)}
|
|
class="flex h-10 w-12 items-center justify-center rounded-b-lg border border-gray-300 bg-gray-50 transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<ChevronDown class="h-4 w-4 text-gray-600" />
|
|
</button>
|
|
<span class="mt-1 text-xs text-gray-500">min</span>
|
|
</div>
|
|
|
|
<!-- Total -->
|
|
<div class="ml-4 flex flex-col items-center justify-center rounded-lg bg-primary/10 px-4 py-2">
|
|
<span class="text-xs text-gray-600">Total</span>
|
|
<span class="text-lg font-bold text-primary">{displayText}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.time-picker input[type='number']::-webkit-inner-spin-button,
|
|
.time-picker input[type='number']::-webkit-outer-spin-button {
|
|
-webkit-appearance: none;
|
|
margin: 0;
|
|
}
|
|
|
|
.time-picker input[type='number'] {
|
|
-moz-appearance: textfield;
|
|
}
|
|
</style>
|
|
|