@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.
Files changed (62) hide show
  1. package/package.json +46 -0
  2. package/src/lib/components/AudioWaveform.svelte +694 -0
  3. package/src/lib/components/AvailabilityModal.svelte +173 -0
  4. package/src/lib/components/Badge.svelte +38 -0
  5. package/src/lib/components/BookingForm.svelte +276 -0
  6. package/src/lib/components/Button.svelte +72 -0
  7. package/src/lib/components/CalendarPicker.svelte +284 -0
  8. package/src/lib/components/Card.svelte +67 -0
  9. package/src/lib/components/CharacterCounter.svelte +82 -0
  10. package/src/lib/components/ChipInput.svelte +596 -0
  11. package/src/lib/components/ColorSelector.svelte +163 -0
  12. package/src/lib/components/ConfirmModal.svelte +75 -0
  13. package/src/lib/components/CountdownTimer.svelte +94 -0
  14. package/src/lib/components/DateRangePicker.svelte +192 -0
  15. package/src/lib/components/Drawer.svelte +110 -0
  16. package/src/lib/components/FilterDropdown.svelte +202 -0
  17. package/src/lib/components/ImageUpload.svelte +97 -0
  18. package/src/lib/components/InlineEdit.svelte +283 -0
  19. package/src/lib/components/LazyImage.svelte +122 -0
  20. package/src/lib/components/LoadingSpinner.svelte +102 -0
  21. package/src/lib/components/Modal.svelte +208 -0
  22. package/src/lib/components/PhoneInput.svelte +92 -0
  23. package/src/lib/components/ResizableDivider.svelte +305 -0
  24. package/src/lib/components/ResizablePanel.svelte +302 -0
  25. package/src/lib/components/SearchDropdown.svelte +341 -0
  26. package/src/lib/components/SelectInput.svelte +215 -0
  27. package/src/lib/components/SignaturePad.svelte +171 -0
  28. package/src/lib/components/SortDropdown.svelte +148 -0
  29. package/src/lib/components/Sparkline.svelte +107 -0
  30. package/src/lib/components/SpeechForm.svelte +114 -0
  31. package/src/lib/components/StatusBadge.svelte +155 -0
  32. package/src/lib/components/TextArea.svelte +143 -0
  33. package/src/lib/components/TextInput.svelte +108 -0
  34. package/src/lib/components/ThemeSelector.svelte +195 -0
  35. package/src/lib/components/TimeSlotPicker.svelte +162 -0
  36. package/src/lib/components/VoicePlayer.svelte +420 -0
  37. package/src/lib/components/messaging/Avatar.svelte +81 -0
  38. package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
  39. package/src/lib/components/messaging/ChannelList.svelte +107 -0
  40. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
  41. package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
  42. package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
  43. package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
  44. package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
  45. package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
  46. package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
  47. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
  48. package/src/lib/components/messaging/MessageInput.svelte +451 -0
  49. package/src/lib/components/messaging/MessageItem.svelte +338 -0
  50. package/src/lib/components/messaging/MessageThread.svelte +306 -0
  51. package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
  52. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
  53. package/src/lib/components/messaging/StartDMModal.svelte +100 -0
  54. package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
  55. package/src/lib/index.ts +185 -0
  56. package/src/lib/types/booking.ts +143 -0
  57. package/src/lib/types/messaging.ts +459 -0
  58. package/src/lib/utils/currency.ts +20 -0
  59. package/src/lib/utils/daisyuiColors.ts +243 -0
  60. package/src/lib/utils/dateFormatters.ts +153 -0
  61. package/src/lib/utils/mentionParser.ts +188 -0
  62. 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>