tangled
alpha
login
or
join now
flo-bit.dev
/
blento
your personal website on atproto - mirror
blento.app
20
fork
atom
overview
issues
pulls
pipelines
fixes
unbedenklich
2 weeks ago
070499f9
22f95c2f
+89
-125
3 changed files
expand all
collapse all
unified
split
src
lib
cards
TimerCard
TimerCard.svelte
TimerCardSettings.svelte
index.ts
+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 } }}
0
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 } }}
0
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>
0
0
0
0
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">
0
0
197
Event Started!
198
</div>
199
{:else}
···
1
<script lang="ts">
0
0
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);
0
0
0
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">
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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';
0
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
0
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' }
0
0
0
0
0
0
0
0
0
0
0
0
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
-
];
0
0
0
0
0
0
0
0
0
0
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' },
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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'
0
0
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;
0
0
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' };