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 <script lang="ts"> 2 - import { Button } from '@foxui/core'; 3 - import { Timer, TimerState } from '@foxui/time'; 4 import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte'; 5 import type { ContentComponentProps } from '../types'; 6 import type { TimerCardData } from './index'; 7 import { onMount } from 'svelte'; 8 9 - let { item, isEditing }: ContentComponentProps = $props(); 10 11 let cardData = $derived(item.cardData as TimerCardData); 12 - 13 - // For timer mode 14 - let timer = $state(new TimerState(cardData.duration ?? 1000 * 60 * 5)); 15 16 // For clock and event modes - current time 17 let now = $state(new Date()); ··· 85 }); 86 </script> 87 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 - 98 <!-- Clock Mode --> 99 {#if cardData.mode === 'clock'} 100 <NumberFlowGroup> 101 <div 102 - class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-center text-4xl font-bold" 103 style="font-variant-numeric: tabular-nums;" 104 > 105 - <NumberFlow value={clockHours} format={{ minimumIntegerDigits: 2 }} /> 106 - <span class="text-base-400 dark:text-base-500 mx-0.5">:</span> 107 <NumberFlow 108 value={clockMinutes} 109 format={{ minimumIntegerDigits: 2 }} 110 digits={{ 1: { max: 5 } }} 111 /> 112 - <span class="text-base-400 dark:text-base-500 mx-0.5">:</span> 113 <NumberFlow 114 value={clockSeconds} 115 format={{ minimumIntegerDigits: 2 }} 116 digits={{ 1: { max: 5 } }} 117 /> 118 </div> 119 </NumberFlowGroup> 120 {#if timezoneDisplay} 121 - <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs"> 122 {timezoneDisplay} 123 </div> 124 {/if} 125 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 <!-- Event Countdown Mode --> 151 {:else if cardData.mode === 'event'} 152 {#if eventDiff !== null && !isEventComplete} 153 <NumberFlowGroup> 154 <div 155 - class="text-base-900 dark:text-base-100 accent:text-base-900 flex items-baseline gap-3 text-center" 156 style="font-variant-numeric: tabular-nums;" 157 > 158 {#if eventDays > 0} 159 <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> 162 </div> 163 {/if} 164 <div class="flex flex-col items-center"> ··· 166 value={eventHours} 167 trend={-1} 168 format={{ minimumIntegerDigits: 2 }} 169 - class="text-4xl font-bold" 170 /> 171 - <span class="text-base-500 dark:text-base-400 text-xs">hrs</span> 172 </div> 173 <div class="flex flex-col items-center"> 174 <NumberFlow ··· 176 trend={-1} 177 format={{ minimumIntegerDigits: 2 }} 178 digits={{ 1: { max: 5 } }} 179 - class="text-4xl font-bold" 180 /> 181 - <span class="text-base-500 dark:text-base-400 text-xs">min</span> 182 </div> 183 <div class="flex flex-col items-center"> 184 <NumberFlow ··· 186 trend={-1} 187 format={{ minimumIntegerDigits: 2 }} 188 digits={{ 1: { max: 5 } }} 189 - class="text-4xl font-bold" 190 /> 191 - <span class="text-base-500 dark:text-base-400 text-xs">sec</span> 192 </div> 193 </div> 194 </NumberFlowGroup> 195 {:else if isEventComplete} 196 - <div class="text-accent-600 dark:text-accent-400 accent:text-accent-900 text-2xl font-bold"> 197 Event Started! 198 </div> 199 {:else}
··· 1 <script lang="ts"> 2 import NumberFlow, { NumberFlowGroup } from '@number-flow/svelte'; 3 import type { ContentComponentProps } from '../types'; 4 import type { TimerCardData } from './index'; 5 import { onMount } from 'svelte'; 6 7 + let { item }: ContentComponentProps = $props(); 8 9 let cardData = $derived(item.cardData as TimerCardData); 10 11 // For clock and event modes - current time 12 let now = $state(new Date()); ··· 80 }); 81 </script> 82 83 + <div class="@container flex h-full w-full flex-col items-center justify-center p-4"> 84 <!-- Clock Mode --> 85 {#if cardData.mode === 'clock'} 86 <NumberFlowGroup> 87 <div 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" 89 style="font-variant-numeric: tabular-nums;" 90 > 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> 93 <NumberFlow 94 value={clockMinutes} 95 format={{ minimumIntegerDigits: 2 }} 96 digits={{ 1: { max: 5 } }} 97 + trend={1} 98 /> 99 + <span class="text-base-400 dark:text-base-500 mx-0.5 @sm:mx-1">:</span> 100 <NumberFlow 101 value={clockSeconds} 102 format={{ minimumIntegerDigits: 2 }} 103 digits={{ 1: { max: 5 } }} 104 + trend={1} 105 /> 106 </div> 107 </NumberFlowGroup> 108 {#if timezoneDisplay} 109 + <div class="text-base-500 dark:text-base-400 accent:text-base-600 mt-1 text-xs @sm:text-sm"> 110 {timezoneDisplay} 111 </div> 112 {/if} 113 114 <!-- Event Countdown Mode --> 115 {:else if cardData.mode === 'event'} 116 {#if eventDiff !== null && !isEventComplete} 117 <NumberFlowGroup> 118 <div 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" 120 style="font-variant-numeric: tabular-nums;" 121 > 122 {#if eventDays > 0} 123 <div class="flex flex-col items-center"> 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> 130 </div> 131 {/if} 132 <div class="flex flex-col items-center"> ··· 134 value={eventHours} 135 trend={-1} 136 format={{ minimumIntegerDigits: 2 }} 137 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 138 /> 139 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">hrs</span> 140 </div> 141 <div class="flex flex-col items-center"> 142 <NumberFlow ··· 144 trend={-1} 145 format={{ minimumIntegerDigits: 2 }} 146 digits={{ 1: { max: 5 } }} 147 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 148 /> 149 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">min</span> 150 </div> 151 <div class="flex flex-col items-center"> 152 <NumberFlow ··· 154 trend={-1} 155 format={{ minimumIntegerDigits: 2 }} 156 digits={{ 1: { max: 5 } }} 157 + class="text-3xl font-bold @xs:text-4xl @sm:text-5xl @md:text-6xl @lg:text-7xl" 158 /> 159 + <span class="text-base-500 dark:text-base-400 text-xs @sm:text-sm">sec</span> 160 </div> 161 </div> 162 </NumberFlowGroup> 163 {:else if isEventComplete} 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 + > 167 Event Started! 168 </div> 169 {:else}
+60 -66
src/lib/cards/TimerCard/TimerCardSettings.svelte
··· 1 <script lang="ts"> 2 import type { Item } from '$lib/types'; 3 - import { Input, Label } from '@foxui/core'; 4 import type { TimerCardData, TimerMode } from './index'; 5 6 let { item }: { item: Item; onclose: () => void } = $props(); 7 ··· 9 10 const modeOptions = [ 11 { value: 'clock', label: 'Clock', desc: 'Show current time' }, 12 - { value: 'timer', label: 'Timer', desc: 'Countdown timer' }, 13 { value: 'event', label: 'Event', desc: 'Countdown to date' } 14 ]; 15 16 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' } 30 ]; 31 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 - ]; 40 41 // Parse target date for inputs 42 let targetDateValue = $derived.by(() => { ··· 59 <!-- Mode Selection --> 60 <div class="flex flex-col gap-2"> 61 <Label>Mode</Label> 62 - <div class="grid grid-cols-3 gap-2"> 63 {#each modeOptions as opt (opt.value)} 64 <button 65 type="button" ··· 78 </div> 79 </div> 80 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 <!-- Clock Settings --> 97 {#if cardData.mode === 'clock'} 98 <div class="flex flex-col gap-2"> 99 <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> 127 </div> 128 {/if} 129
··· 1 <script lang="ts"> 2 import type { Item } from '$lib/types'; 3 + import { Button, Input, Label } from '@foxui/core'; 4 import type { TimerCardData, TimerMode } from './index'; 5 + import { onMount } from 'svelte'; 6 7 let { item }: { item: Item; onclose: () => void } = $props(); 8 ··· 10 11 const modeOptions = [ 12 { value: 'clock', label: 'Clock', desc: 'Show current time' }, 13 { value: 'event', label: 'Event', desc: 'Countdown to date' } 14 ]; 15 16 + // All 24 timezones with representative cities 17 const timezoneOptions = [ 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)' } 43 ]; 44 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 + } 63 64 // Parse target date for inputs 65 let targetDateValue = $derived.by(() => { ··· 82 <!-- Mode Selection --> 83 <div class="flex flex-col gap-2"> 84 <Label>Mode</Label> 85 + <div class="grid grid-cols-2 gap-2"> 86 {#each modeOptions as opt (opt.value)} 87 <button 88 type="button" ··· 101 </div> 102 </div> 103 104 <!-- Clock Settings --> 105 {#if cardData.mode === 'clock'} 106 <div class="flex flex-col gap-2"> 107 <Label for="timezone">Timezone</Label> 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> 121 </div> 122 {/if} 123
+4 -4
src/lib/cards/TimerCard/index.ts
··· 2 import TimerCard from './TimerCard.svelte'; 3 import TimerCardSettings from './TimerCardSettings.svelte'; 4 5 - export type TimerMode = 'clock' | 'timer' | 'event'; 6 7 export type TimerCardData = { 8 mode: TimerMode; ··· 11 timezone?: string; 12 // For event mode: target date as ISO string 13 targetDate?: string; 14 - // For timer mode: duration in ms 15 - duration?: number; 16 }; 17 18 export const TimerCardDefinition = { ··· 33 }, 34 35 allowSetColor: true, 36 - name: 'Timer Card' 37 } as CardDefinition & { type: 'timer' };
··· 2 import TimerCard from './TimerCard.svelte'; 3 import TimerCardSettings from './TimerCardSettings.svelte'; 4 5 + export type TimerMode = 'clock' | 'event'; 6 7 export type TimerCardData = { 8 mode: TimerMode; ··· 11 timezone?: string; 12 // For event mode: target date as ISO string 13 targetDate?: string; 14 }; 15 16 export const TimerCardDefinition = { ··· 31 }, 32 33 allowSetColor: true, 34 + name: 'Timer Card', 35 + minW: 4, 36 + canHaveLabel: true 37 } as CardDefinition & { type: 'timer' };