@joewinke/jatui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +46 -0
- package/src/lib/components/AudioWaveform.svelte +694 -0
- package/src/lib/components/AvailabilityModal.svelte +173 -0
- package/src/lib/components/Badge.svelte +38 -0
- package/src/lib/components/BookingForm.svelte +276 -0
- package/src/lib/components/Button.svelte +72 -0
- package/src/lib/components/CalendarPicker.svelte +284 -0
- package/src/lib/components/Card.svelte +67 -0
- package/src/lib/components/CharacterCounter.svelte +82 -0
- package/src/lib/components/ChipInput.svelte +596 -0
- package/src/lib/components/ColorSelector.svelte +163 -0
- package/src/lib/components/ConfirmModal.svelte +75 -0
- package/src/lib/components/CountdownTimer.svelte +94 -0
- package/src/lib/components/DateRangePicker.svelte +192 -0
- package/src/lib/components/Drawer.svelte +110 -0
- package/src/lib/components/FilterDropdown.svelte +202 -0
- package/src/lib/components/ImageUpload.svelte +97 -0
- package/src/lib/components/InlineEdit.svelte +283 -0
- package/src/lib/components/LazyImage.svelte +122 -0
- package/src/lib/components/LoadingSpinner.svelte +102 -0
- package/src/lib/components/Modal.svelte +208 -0
- package/src/lib/components/PhoneInput.svelte +92 -0
- package/src/lib/components/ResizableDivider.svelte +305 -0
- package/src/lib/components/ResizablePanel.svelte +302 -0
- package/src/lib/components/SearchDropdown.svelte +341 -0
- package/src/lib/components/SelectInput.svelte +215 -0
- package/src/lib/components/SignaturePad.svelte +171 -0
- package/src/lib/components/SortDropdown.svelte +148 -0
- package/src/lib/components/Sparkline.svelte +107 -0
- package/src/lib/components/SpeechForm.svelte +114 -0
- package/src/lib/components/StatusBadge.svelte +155 -0
- package/src/lib/components/TextArea.svelte +143 -0
- package/src/lib/components/TextInput.svelte +108 -0
- package/src/lib/components/ThemeSelector.svelte +195 -0
- package/src/lib/components/TimeSlotPicker.svelte +162 -0
- package/src/lib/components/VoicePlayer.svelte +420 -0
- package/src/lib/components/messaging/Avatar.svelte +81 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
- package/src/lib/components/messaging/ChannelList.svelte +107 -0
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
- package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
- package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
- package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
- package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
- package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/src/lib/components/messaging/MessageInput.svelte +451 -0
- package/src/lib/components/messaging/MessageItem.svelte +338 -0
- package/src/lib/components/messaging/MessageThread.svelte +306 -0
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
- package/src/lib/components/messaging/StartDMModal.svelte +100 -0
- package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
- package/src/lib/index.ts +185 -0
- package/src/lib/types/booking.ts +143 -0
- package/src/lib/types/messaging.ts +459 -0
- package/src/lib/utils/currency.ts +20 -0
- package/src/lib/utils/daisyuiColors.ts +243 -0
- package/src/lib/utils/dateFormatters.ts +153 -0
- package/src/lib/utils/mentionParser.ts +188 -0
- package/src/lib/utils/phoneFormat.ts +74 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ThemeSelector — DaisyUI theme switcher with visual preview dots, localStorage persistence,
|
|
4
|
+
* and animated transitions on theme change.
|
|
5
|
+
*
|
|
6
|
+
* By default includes all 33 standard DaisyUI themes. Pass custom `themes` prop to override.
|
|
7
|
+
*/
|
|
8
|
+
import { onMount } from 'svelte';
|
|
9
|
+
|
|
10
|
+
interface ThemeOption {
|
|
11
|
+
name: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_THEMES: ThemeOption[] = [
|
|
16
|
+
{ name: 'light', label: 'Light' },
|
|
17
|
+
{ name: 'dark', label: 'Dark' },
|
|
18
|
+
{ name: 'cupcake', label: 'Cupcake' },
|
|
19
|
+
{ name: 'bumblebee', label: 'Bumblebee' },
|
|
20
|
+
{ name: 'emerald', label: 'Emerald' },
|
|
21
|
+
{ name: 'corporate', label: 'Corporate' },
|
|
22
|
+
{ name: 'synthwave', label: 'Synthwave' },
|
|
23
|
+
{ name: 'retro', label: 'Retro' },
|
|
24
|
+
{ name: 'cyberpunk', label: 'Cyberpunk' },
|
|
25
|
+
{ name: 'valentine', label: 'Valentine' },
|
|
26
|
+
{ name: 'halloween', label: 'Halloween' },
|
|
27
|
+
{ name: 'garden', label: 'Garden' },
|
|
28
|
+
{ name: 'forest', label: 'Forest' },
|
|
29
|
+
{ name: 'aqua', label: 'Aqua' },
|
|
30
|
+
{ name: 'lofi', label: 'Lofi' },
|
|
31
|
+
{ name: 'pastel', label: 'Pastel' },
|
|
32
|
+
{ name: 'fantasy', label: 'Fantasy' },
|
|
33
|
+
{ name: 'wireframe', label: 'Wireframe' },
|
|
34
|
+
{ name: 'black', label: 'Black' },
|
|
35
|
+
{ name: 'luxury', label: 'Luxury' },
|
|
36
|
+
{ name: 'dracula', label: 'Dracula' },
|
|
37
|
+
{ name: 'cmyk', label: 'CMYK' },
|
|
38
|
+
{ name: 'autumn', label: 'Autumn' },
|
|
39
|
+
{ name: 'business', label: 'Business' },
|
|
40
|
+
{ name: 'acid', label: 'Acid' },
|
|
41
|
+
{ name: 'lemonade', label: 'Lemonade' },
|
|
42
|
+
{ name: 'night', label: 'Night' },
|
|
43
|
+
{ name: 'coffee', label: 'Coffee' },
|
|
44
|
+
{ name: 'winter', label: 'Winter' },
|
|
45
|
+
{ name: 'dim', label: 'Dim' },
|
|
46
|
+
{ name: 'nord', label: 'Nord' },
|
|
47
|
+
{ name: 'sunset', label: 'Sunset' }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
let {
|
|
51
|
+
dropUp = false,
|
|
52
|
+
themes = DEFAULT_THEMES,
|
|
53
|
+
defaultTheme = 'light',
|
|
54
|
+
storageKey = 'theme',
|
|
55
|
+
onThemeChange,
|
|
56
|
+
}: {
|
|
57
|
+
dropUp?: boolean;
|
|
58
|
+
themes?: ThemeOption[];
|
|
59
|
+
defaultTheme?: string;
|
|
60
|
+
storageKey?: string;
|
|
61
|
+
onThemeChange?: (theme: string) => void;
|
|
62
|
+
} = $props();
|
|
63
|
+
|
|
64
|
+
let currentTheme = $state(defaultTheme);
|
|
65
|
+
let isAnimating = $state(false);
|
|
66
|
+
|
|
67
|
+
const currentThemeLabel = $derived(
|
|
68
|
+
themes.find((t) => t.name === currentTheme)?.label || currentTheme
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
onMount(() => {
|
|
72
|
+
const savedTheme = localStorage.getItem(storageKey);
|
|
73
|
+
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
|
74
|
+
currentTheme = savedTheme;
|
|
75
|
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
76
|
+
} else {
|
|
77
|
+
currentTheme = defaultTheme;
|
|
78
|
+
document.documentElement.setAttribute('data-theme', defaultTheme);
|
|
79
|
+
localStorage.setItem(storageKey, defaultTheme);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function handleThemeChange(themeName: string) {
|
|
84
|
+
if (themeName === currentTheme) return;
|
|
85
|
+
|
|
86
|
+
// Close dropdown first so the trigger is visible during animation
|
|
87
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
88
|
+
document.activeElement.blur();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
isAnimating = true;
|
|
92
|
+
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
currentTheme = themeName;
|
|
95
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
96
|
+
localStorage.setItem(storageKey, themeName);
|
|
97
|
+
onThemeChange?.(themeName);
|
|
98
|
+
}, 150);
|
|
99
|
+
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
isAnimating = false;
|
|
102
|
+
}, 500);
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<div class="dropdown dropdown-end" class:dropdown-top={dropUp}>
|
|
107
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
108
|
+
<div
|
|
109
|
+
tabindex="0"
|
|
110
|
+
role="button"
|
|
111
|
+
class="flex items-center gap-2 cursor-pointer hover:bg-base-300 transition-colors px-2 py-1 rounded"
|
|
112
|
+
aria-label="Change Theme"
|
|
113
|
+
title="Change Theme"
|
|
114
|
+
>
|
|
115
|
+
<div
|
|
116
|
+
data-theme={currentTheme}
|
|
117
|
+
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 shadow-sm transition-transform duration-300 ease-out"
|
|
118
|
+
class:theme-spin={isAnimating}
|
|
119
|
+
>
|
|
120
|
+
<div class="bg-base-content size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 0ms"></div>
|
|
121
|
+
<div class="bg-primary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 50ms"></div>
|
|
122
|
+
<div class="bg-secondary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 100ms"></div>
|
|
123
|
+
<div class="bg-accent size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 150ms"></div>
|
|
124
|
+
</div>
|
|
125
|
+
<span class="text-sm font-medium transition-opacity duration-200" class:opacity-50={isAnimating}>{currentThemeLabel}</span>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
129
|
+
<div
|
|
130
|
+
tabindex="0"
|
|
131
|
+
class="dropdown-content bg-base-200 text-base-content rounded-box
|
|
132
|
+
top-px max-h-[calc(50vh-6.5rem)] overflow-y-auto border border-white/5
|
|
133
|
+
shadow-2xl outline-1 outline-black/5 mb-2 z-50"
|
|
134
|
+
>
|
|
135
|
+
<ul class="menu w-56">
|
|
136
|
+
<li class="menu-title text-xs">Theme</li>
|
|
137
|
+
|
|
138
|
+
{#each themes as theme}
|
|
139
|
+
<li>
|
|
140
|
+
<button
|
|
141
|
+
class="gap-3 px-2"
|
|
142
|
+
onclick={() => handleThemeChange(theme.name)}
|
|
143
|
+
class:active={currentTheme === theme.name}
|
|
144
|
+
>
|
|
145
|
+
<div
|
|
146
|
+
data-theme={theme.name}
|
|
147
|
+
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5
|
|
148
|
+
rounded-md p-1 shadow-sm"
|
|
149
|
+
>
|
|
150
|
+
<div class="bg-base-content size-1 rounded-full"></div>
|
|
151
|
+
<div class="bg-primary size-1 rounded-full"></div>
|
|
152
|
+
<div class="bg-secondary size-1 rounded-full"></div>
|
|
153
|
+
<div class="bg-accent size-1 rounded-full"></div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="w-32 truncate">{theme.label}</div>
|
|
157
|
+
|
|
158
|
+
{#if currentTheme === theme.name}
|
|
159
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3 shrink-0">
|
|
160
|
+
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"></path>
|
|
161
|
+
</svg>
|
|
162
|
+
{:else}
|
|
163
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="invisible h-3 w-3 shrink-0">
|
|
164
|
+
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"></path>
|
|
165
|
+
</svg>
|
|
166
|
+
{/if}
|
|
167
|
+
</button>
|
|
168
|
+
</li>
|
|
169
|
+
{/each}
|
|
170
|
+
</ul>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<style>
|
|
175
|
+
.theme-spin {
|
|
176
|
+
animation: theme-spin 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@keyframes theme-spin {
|
|
180
|
+
0% { transform: rotate(0deg) scale(1); }
|
|
181
|
+
50% { transform: rotate(180deg) scale(1.2); }
|
|
182
|
+
100% { transform: rotate(360deg) scale(1); }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.theme-dot-animate {
|
|
186
|
+
animation: dot-pulse 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
187
|
+
animation-delay: var(--dot-delay);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@keyframes dot-pulse {
|
|
191
|
+
0% { transform: scale(1); opacity: 1; }
|
|
192
|
+
50% { transform: scale(0.5); opacity: 0.5; }
|
|
193
|
+
100% { transform: scale(1); opacity: 1; }
|
|
194
|
+
}
|
|
195
|
+
</style>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TimeSlotPicker — Display available time slots grouped by morning/afternoon/evening.
|
|
4
|
+
*
|
|
5
|
+
* Accepts an array of TimeSlot objects with `start` (Date), `end` (Date), and `available` (boolean).
|
|
6
|
+
* Grouped automatically by hour: morning (<12), afternoon (<17), evening (17+).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TimeSlot } from '../types/booking';
|
|
10
|
+
|
|
11
|
+
export type { TimeSlot };
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
slots,
|
|
15
|
+
loading = false,
|
|
16
|
+
onSelect,
|
|
17
|
+
accentColor = 'primary',
|
|
18
|
+
}: {
|
|
19
|
+
slots: TimeSlot[];
|
|
20
|
+
loading?: boolean;
|
|
21
|
+
onSelect: (slot: TimeSlot) => void;
|
|
22
|
+
accentColor?: string;
|
|
23
|
+
} = $props();
|
|
24
|
+
|
|
25
|
+
const groupedSlots = $derived(() => {
|
|
26
|
+
const morning: TimeSlot[] = [];
|
|
27
|
+
const afternoon: TimeSlot[] = [];
|
|
28
|
+
const evening: TimeSlot[] = [];
|
|
29
|
+
|
|
30
|
+
for (const slot of slots) {
|
|
31
|
+
const hour = slot.start.getHours();
|
|
32
|
+
if (hour < 12) {
|
|
33
|
+
morning.push(slot);
|
|
34
|
+
} else if (hour < 17) {
|
|
35
|
+
afternoon.push(slot);
|
|
36
|
+
} else {
|
|
37
|
+
evening.push(slot);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { morning, afternoon, evening };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function formatTime(date: Date): string {
|
|
45
|
+
return date.toLocaleTimeString('en-US', {
|
|
46
|
+
hour: 'numeric',
|
|
47
|
+
minute: '2-digit',
|
|
48
|
+
hour12: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleSlotClick(slot: TimeSlot) {
|
|
53
|
+
if (slot.available) {
|
|
54
|
+
onSelect(slot);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div class="time-slot-picker">
|
|
60
|
+
{#if loading}
|
|
61
|
+
<div class="flex justify-center py-12">
|
|
62
|
+
<span class="loading loading-spinner loading-lg text-{accentColor}"></span>
|
|
63
|
+
</div>
|
|
64
|
+
{:else if slots.length === 0}
|
|
65
|
+
<div class="text-center py-12">
|
|
66
|
+
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-base-200 flex items-center justify-center">
|
|
67
|
+
<!-- Clock icon -->
|
|
68
|
+
<svg class="w-8 h-8 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
69
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
70
|
+
</svg>
|
|
71
|
+
</div>
|
|
72
|
+
<h3 class="text-lg font-semibold mb-2">No Available Times</h3>
|
|
73
|
+
<p class="text-base-content/60">Please select a different date.</p>
|
|
74
|
+
</div>
|
|
75
|
+
{:else}
|
|
76
|
+
{@const { morning, afternoon, evening } = groupedSlots()}
|
|
77
|
+
{@const availableCount = slots.filter(s => s.available).length}
|
|
78
|
+
|
|
79
|
+
<p class="text-sm text-base-content/60 mb-4">
|
|
80
|
+
{availableCount} time{availableCount !== 1 ? 's' : ''} available
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<div class="space-y-6">
|
|
84
|
+
{#if morning.length > 0}
|
|
85
|
+
<div>
|
|
86
|
+
<h4 class="text-sm font-medium text-base-content/70 mb-3 flex items-center gap-2">
|
|
87
|
+
<!-- Sun icon -->
|
|
88
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
89
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
90
|
+
</svg>
|
|
91
|
+
Morning
|
|
92
|
+
</h4>
|
|
93
|
+
<div class="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
94
|
+
{#each morning as slot}
|
|
95
|
+
<button
|
|
96
|
+
onclick={() => handleSlotClick(slot)}
|
|
97
|
+
disabled={!slot.available}
|
|
98
|
+
class="py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200
|
|
99
|
+
{slot.available
|
|
100
|
+
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
101
|
+
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
102
|
+
>
|
|
103
|
+
{formatTime(slot.start)}
|
|
104
|
+
</button>
|
|
105
|
+
{/each}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
{#if afternoon.length > 0}
|
|
111
|
+
<div>
|
|
112
|
+
<h4 class="text-sm font-medium text-base-content/70 mb-3 flex items-center gap-2">
|
|
113
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
114
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
115
|
+
</svg>
|
|
116
|
+
Afternoon
|
|
117
|
+
</h4>
|
|
118
|
+
<div class="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
119
|
+
{#each afternoon as slot}
|
|
120
|
+
<button
|
|
121
|
+
onclick={() => handleSlotClick(slot)}
|
|
122
|
+
disabled={!slot.available}
|
|
123
|
+
class="py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200
|
|
124
|
+
{slot.available
|
|
125
|
+
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
126
|
+
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
127
|
+
>
|
|
128
|
+
{formatTime(slot.start)}
|
|
129
|
+
</button>
|
|
130
|
+
{/each}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if evening.length > 0}
|
|
136
|
+
<div>
|
|
137
|
+
<h4 class="text-sm font-medium text-base-content/70 mb-3 flex items-center gap-2">
|
|
138
|
+
<!-- Moon icon -->
|
|
139
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
140
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
141
|
+
</svg>
|
|
142
|
+
Evening
|
|
143
|
+
</h4>
|
|
144
|
+
<div class="grid grid-cols-3 sm:grid-cols-4 gap-2">
|
|
145
|
+
{#each evening as slot}
|
|
146
|
+
<button
|
|
147
|
+
onclick={() => handleSlotClick(slot)}
|
|
148
|
+
disabled={!slot.available}
|
|
149
|
+
class="py-3 px-4 rounded-lg text-sm font-medium transition-all duration-200
|
|
150
|
+
{slot.available
|
|
151
|
+
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
152
|
+
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
153
|
+
>
|
|
154
|
+
{formatTime(slot.start)}
|
|
155
|
+
</button>
|
|
156
|
+
{/each}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
{/if}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|