your personal website on atproto - mirror blento.app

Merge pull request #51 from unbedenklich/fixing-timercard

fixed timecard

authored by Florian and committed by GitHub f79e4749 22f95c2f

+89 -125
+25 -55
src/lib/cards/TimerCard/TimerCard.svelte
··· 1 1 <script lang="ts"> 2 - import { Button } from '@foxui/core'; 3 - import { Timer, TimerState } from '@foxui/time'; 4 2 import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte'; 5 3 import type { ContentComponentProps } from '../types'; 6 4 import type { TimerCardData } from './index'; 7 5 import { onMount } from 'svelte'; 8 6 9 - let { item, isEditing }: ContentComponentProps = $props(); 7 + let { item }: ContentComponentProps = $props(); 10 8 11 9 let cardData = $derived(item.cardData as TimerCardData); 12 - 13 - // For timer mode 14 - let timer = $state(new TimerState(cardData.duration ?? 1000 * 60 * 5)); 15 10 16 11 // For clock and event modes - current time 17 12 let now = $state(new Date()); ··· 85 80 }); 86 81 </script> 87 82 88 - <div class="flex h-full w-full flex-col items-center justify-center p-4"> 89 - <!-- Label --> 90 - {#if cardData.label} 91 - <div 92 - class="text-base-600 dark:text-base-400 accent:text-base-700 mb-1 text-center text-sm font-medium" 93 - > 94 - {cardData.label} 95 - </div> 96 - {/if} 97 - 83 + <div class="@container flex h-full w-full flex-col items-center justify-center p-4"> 98 84 <!-- Clock Mode --> 99 85 {#if cardData.mode === 'clock'} 100 86 <NumberFlowGroup> 101 87 <div 102 - class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-center text-4xl font-bold" 88 + class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-center text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 103 89 style="font-variant-numeric: tabular-nums;" 104 90 > 105 - <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} /> 106 - <span class="text-base-400 dark:text-base-500 mx-0.5">:</span> 91 + <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} trend={1} /> 92 + <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span> 107 93 <NumberFlow 108 94 value={clockMinutes} 109 95 format={{ minimumIntegerDigits: 2 }} 110 96 digits={{ 1: { max: 5 } }} 97 + trend={1} 111 98 /> 112 - <span class="text-base-400 dark:text-base-500 mx-0.5">:</span> 99 + <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span> 113 100 <NumberFlow 114 101 value={clockSeconds} 115 102 format={{ minimumIntegerDigits: 2 }} 116 103 digits={{ 1: { max: 5 } }} 104 + trend={1} 117 105 /> 118 106 </div> 119 107 </NumberFlowGroup> 120 108 {#if timezoneDisplay} 121 - <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs"> 109 + <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs @sm:text-sm"> 122 110 {timezoneDisplay} 123 111 </div> 124 112 {/if} 125 113 126 - <!-- Timer Mode --> 127 - {:else if cardData.mode === 'timer'} 128 - <Timer 129 - bind:timer 130 - showHours 131 - showMinutes 132 - showSeconds 133 - class="text-base-900 dark:text-base-100 accent:text-base-900 text-4xl" 134 - /> 135 - {#if isEditing} 136 - <div class="mt-3 flex gap-2"> 137 - {#if timer.isStopped} 138 - <Button size="sm" onclick={() => timer.start()}>Start</Button> 139 - {:else if timer.isRunning} 140 - <Button size="sm" variant="secondary" onclick={() => timer.pause()}>Pause</Button> 141 - {:else if timer.isPaused} 142 - <Button size="sm" onclick={() => timer.resume()}>Resume</Button> 143 - {/if} 144 - {#if !timer.isStopped} 145 - <Button size="sm" variant="ghost" onclick={() => timer.reset()}>Reset</Button> 146 - {/if} 147 - </div> 148 - {/if} 149 - 150 114 <!-- Event Countdown Mode --> 151 115 {:else if cardData.mode === 'event'} 152 116 {#if eventDiff !== null && !isEventComplete} 153 117 <NumberFlowGroup> 154 118 <div 155 - class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-baseline gap-3 text-center" 119 + class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-baseline gap-4 text-center @sm:gap-6 @md:gap-8" 156 120 style="font-variant-numeric: tabular-nums;" 157 121 > 158 122 {#if eventDays > 0} 159 123 <div class="flex flex-col items-center"> 160 - <NumberFlow value={eventDays} trend={-1} class="text-4xl font-bold" /> 161 - <span class="text-base-500 dark:text-base-400 text-xs">days</span> 124 + <NumberFlow 125 + value={eventDays} 126 + trend={-1} 127 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 128 + /> 129 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">days</span> 162 130 </div> 163 131 {/if} 164 132 <div class="flex flex-col items-center"> ··· 166 134 value={eventHours} 167 135 trend={-1} 168 136 format={{ minimumIntegerDigits: 2 }} 169 - class="text-4xl font-bold" 137 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 170 138 /> 171 - <span class="text-base-500 dark:text-base-400 text-xs">hrs</span> 139 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">hrs</span> 172 140 </div> 173 141 <div class="flex flex-col items-center"> 174 142 <NumberFlow ··· 176 144 trend={-1} 177 145 format={{ minimumIntegerDigits: 2 }} 178 146 digits={{ 1: { max: 5 } }} 179 - class="text-4xl font-bold" 147 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 180 148 /> 181 - <span class="text-base-500 dark:text-base-400 text-xs">min</span> 149 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">min</span> 182 150 </div> 183 151 <div class="flex flex-col items-center"> 184 152 <NumberFlow ··· 186 154 trend={-1} 187 155 format={{ minimumIntegerDigits: 2 }} 188 156 digits={{ 1: { max: 5 } }} 189 - class="text-4xl font-bold" 157 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 190 158 /> 191 - <span class="text-base-500 dark:text-base-400 text-xs">sec</span> 159 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">sec</span> 192 160 </div> 193 161 </div> 194 162 </NumberFlowGroup> 195 163 {:else if isEventComplete} 196 - <div class="text-accent-600 dark:text-accent-400 accent:text-accent-900 text-2xl font-bold"> 164 + <div 165 + class="text-accent-600 dark:text-accent-400 accent:text-accent-900 text-xl font-bold @xs:text-2xl @sm:text-3xl @md:text-4xl" 166 + > 197 167 Event Started! 198 168 </div> 199 169 {:else}
+60 -66
src/lib/cards/TimerCard/TimerCardSettings.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Item } from '$lib/types'; 3 - import { Input, Label } from '@foxui/core'; 3 + import { Button, Input, Label } from '@foxui/core'; 4 4 import type { TimerCardData, TimerMode } from './index'; 5 + import { onMount } from 'svelte'; 5 6 6 7 let { item }: { item: Item; onclose: () => void } = $props(); 7 8 ··· 9 10 10 11 const modeOptions = [ 11 12 { value: 'clock', label: 'Clock', desc: 'Show current time' }, 12 - { value: 'timer', label: 'Timer', desc: 'Countdown timer' }, 13 13 { value: 'event', label: 'Event', desc: 'Countdown to date' } 14 14 ]; 15 15 16 + // All 24 timezones with representative cities 16 17 const timezoneOptions = [ 17 - { value: 'UTC', label: 'UTC' }, 18 - { value: 'America/New_York', label: 'New York' }, 19 - { value: 'America/Chicago', label: 'Chicago' }, 20 - { value: 'America/Denver', label: 'Denver' }, 21 - { value: 'America/Los_Angeles', label: 'Los Angeles' }, 22 - { value: 'Europe/London', label: 'London' }, 23 - { value: 'Europe/Paris', label: 'Paris' }, 24 - { value: 'Europe/Berlin', label: 'Berlin' }, 25 - { value: 'Asia/Tokyo', label: 'Tokyo' }, 26 - { value: 'Asia/Shanghai', label: 'Shanghai' }, 27 - { value: 'Asia/Dubai', label: 'Dubai' }, 28 - { value: 'Asia/Kolkata', label: 'Mumbai' }, 29 - { value: 'Australia/Sydney', label: 'Sydney' } 18 + { value: 'Pacific/Midway', label: 'UTC-11 (Midway)' }, 19 + { value: 'Pacific/Honolulu', label: 'UTC-10 (Honolulu)' }, 20 + { value: 'America/Anchorage', label: 'UTC-9 (Anchorage)' }, 21 + { value: 'America/Los_Angeles', label: 'UTC-8 (Los Angeles)' }, 22 + { value: 'America/Denver', label: 'UTC-7 (Denver)' }, 23 + { value: 'America/Chicago', label: 'UTC-6 (Chicago)' }, 24 + { value: 'America/New_York', label: 'UTC-5 (New York)' }, 25 + { value: 'America/Halifax', label: 'UTC-4 (Halifax)' }, 26 + { value: 'America/Sao_Paulo', label: 'UTC-3 (São Paulo)' }, 27 + { value: 'Atlantic/South_Georgia', label: 'UTC-2 (South Georgia)' }, 28 + { value: 'Atlantic/Azores', label: 'UTC-1 (Azores)' }, 29 + { value: 'UTC', label: 'UTC+0 (London)' }, 30 + { value: 'Europe/Paris', label: 'UTC+1 (Paris)' }, 31 + { value: 'Europe/Helsinki', label: 'UTC+2 (Helsinki)' }, 32 + { value: 'Europe/Moscow', label: 'UTC+3 (Moscow)' }, 33 + { value: 'Asia/Dubai', label: 'UTC+4 (Dubai)' }, 34 + { value: 'Asia/Karachi', label: 'UTC+5 (Karachi)' }, 35 + { value: 'Asia/Kolkata', label: 'UTC+5:30 (Mumbai)' }, 36 + { value: 'Asia/Dhaka', label: 'UTC+6 (Dhaka)' }, 37 + { value: 'Asia/Bangkok', label: 'UTC+7 (Bangkok)' }, 38 + { value: 'Asia/Shanghai', label: 'UTC+8 (Shanghai)' }, 39 + { value: 'Asia/Tokyo', label: 'UTC+9 (Tokyo)' }, 40 + { value: 'Australia/Sydney', label: 'UTC+10 (Sydney)' }, 41 + { value: 'Pacific/Noumea', label: 'UTC+11 (Noumea)' }, 42 + { value: 'Pacific/Auckland', label: 'UTC+12 (Auckland)' } 30 43 ]; 31 44 32 - const durationOptions = [ 33 - { value: 1000 * 60, label: '1 minute' }, 34 - { value: 1000 * 60 * 5, label: '5 minutes' }, 35 - { value: 1000 * 60 * 10, label: '10 minutes' }, 36 - { value: 1000 * 60 * 15, label: '15 minutes' }, 37 - { value: 1000 * 60 * 30, label: '30 minutes' }, 38 - { value: 1000 * 60 * 60, label: '1 hour' } 39 - ]; 45 + // Auto-detect timezone on mount if not set 46 + onMount(() => { 47 + if (!cardData.timezone) { 48 + try { 49 + item.cardData.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 50 + } catch { 51 + item.cardData.timezone = 'UTC'; 52 + } 53 + } 54 + }); 55 + 56 + function useLocalTimezone() { 57 + try { 58 + item.cardData.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 59 + } catch { 60 + item.cardData.timezone = 'UTC'; 61 + } 62 + } 40 63 41 64 // Parse target date for inputs 42 65 let targetDateValue = $derived.by(() => { ··· 59 82 <!-- Mode Selection --> 60 83 <div class="flex flex-col gap-2"> 61 84 <Label>Mode</Label> 62 - <div class="grid grid-cols-3 gap-2"> 85 + <div class="grid grid-cols-2 gap-2"> 63 86 {#each modeOptions as opt (opt.value)} 64 87 <button 65 88 type="button" ··· 78 101 </div> 79 102 </div> 80 103 81 - <!-- Label --> 82 - <div class="flex flex-col gap-2"> 83 - <Label for="label">Label (optional)</Label> 84 - <Input 85 - id="label" 86 - value={cardData.label || ''} 87 - oninput={(e) => (item.cardData.label = e.currentTarget.value || undefined)} 88 - placeholder={cardData.mode === 'clock' 89 - ? 'e.g. Tokyo Time' 90 - : cardData.mode === 'event' 91 - ? 'e.g. New Year' 92 - : 'e.g. Focus Time'} 93 - /> 94 - </div> 95 - 96 104 <!-- Clock Settings --> 97 105 {#if cardData.mode === 'clock'} 98 106 <div class="flex flex-col gap-2"> 99 107 <Label for="timezone">Timezone</Label> 100 - <select 101 - id="timezone" 102 - value={cardData.timezone || 'UTC'} 103 - onchange={(e) => (item.cardData.timezone = e.currentTarget.value)} 104 - class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 rounded-xl border px-3 py-2" 105 - > 106 - {#each timezoneOptions as tz (tz.value)} 107 - <option value={tz.value}>{tz.label}</option> 108 - {/each} 109 - </select> 110 - </div> 111 - {/if} 112 - 113 - <!-- Timer Settings --> 114 - {#if cardData.mode === 'timer'} 115 - <div class="flex flex-col gap-2"> 116 - <Label for="duration">Duration</Label> 117 - <select 118 - id="duration" 119 - value={cardData.duration || 1000 * 60 * 5} 120 - onchange={(e) => (item.cardData.duration = parseInt(e.currentTarget.value))} 121 - class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 rounded-xl border px-3 py-2" 122 - > 123 - {#each durationOptions as dur (dur.value)} 124 - <option value={dur.value}>{dur.label}</option> 125 - {/each} 126 - </select> 108 + <div class="flex gap-2"> 109 + <select 110 + id="timezone" 111 + value={cardData.timezone || 'UTC'} 112 + onchange={(e) => (item.cardData.timezone = e.currentTarget.value)} 113 + class="bg-base-100 dark:bg-base-800 border-base-300 dark:border-base-700 text-base-900 dark:text-base-100 flex-1 rounded-xl border px-3 py-2" 114 + > 115 + {#each timezoneOptions as tz (tz.value)} 116 + <option value={tz.value}>{tz.label}</option> 117 + {/each} 118 + </select> 119 + <Button size="sm" variant="ghost" onclick={useLocalTimezone}>Local</Button> 120 + </div> 127 121 </div> 128 122 {/if} 129 123
+4 -4
src/lib/cards/TimerCard/index.ts
··· 2 2 import TimerCard from './TimerCard.svelte'; 3 3 import TimerCardSettings from './TimerCardSettings.svelte'; 4 4 5 - export type TimerMode = 'clock' | 'timer' | 'event'; 5 + export type TimerMode = 'clock' | 'event'; 6 6 7 7 export type TimerCardData = { 8 8 mode: TimerMode; ··· 11 11 timezone?: string; 12 12 // For event mode: target date as ISO string 13 13 targetDate?: string; 14 - // For timer mode: duration in ms 15 - duration?: number; 16 14 }; 17 15 18 16 export const TimerCardDefinition = { ··· 33 31 }, 34 32 35 33 allowSetColor: true, 36 - name: 'Timer Card' 34 + name: 'Timer Card', 35 + minW: 4, 36 + canHaveLabel: true 37 37 } as CardDefinition & { type: 'timer' };