@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,459 @@
1
+ // Messaging System Types
2
+ // Generic types for messaging UI components — no framework or backend dependencies
3
+
4
+ // ─── Core Message Types ───────────────────────────────────────────────
5
+
6
+ export interface Message {
7
+ id: string
8
+ content: string
9
+ senderId: string
10
+ senderName: string
11
+ senderAvatar?: string
12
+ createdAt: string
13
+ editedAt?: string
14
+ isRead?: boolean
15
+ readByCount?: number
16
+ isPinned?: boolean
17
+ pinnedAt?: string
18
+ pinnedBy?: string
19
+ pinnedByName?: string
20
+ parentMessageId?: string
21
+ recipientId?: string
22
+ thread_reply_count?: number
23
+ thread_last_reply_at?: string
24
+ thread_last_sender?: {
25
+ id: string
26
+ full_name: string
27
+ avatar_url?: string
28
+ }
29
+ quotedMessageId?: string
30
+ quotedMessage?: QuotedMessage
31
+ attachments?: MessageAttachment[]
32
+ }
33
+
34
+ export interface QuotedMessage {
35
+ id: string
36
+ content: string
37
+ senderId: string
38
+ senderName: string
39
+ senderAvatar?: string
40
+ createdAt: string
41
+ }
42
+
43
+ export interface MessageAttachment {
44
+ id: string
45
+ file_name: string
46
+ file_type: string
47
+ file_size: number
48
+ storage_path: string
49
+ width?: number
50
+ height?: number
51
+ duration?: number
52
+ thumbnail_path?: string
53
+ uploaded_by: string
54
+ created_at: string
55
+ }
56
+
57
+ export interface PendingAttachment {
58
+ id: string
59
+ file: File
60
+ preview?: string
61
+ uploading: boolean
62
+ progress: number
63
+ error?: string
64
+ storagePath?: string
65
+ width?: number
66
+ height?: number
67
+ }
68
+
69
+ // ─── Channel Types ────────────────────────────────────────────────────
70
+
71
+ export interface Channel {
72
+ id: string
73
+ name: string
74
+ description?: string
75
+ type: 'public' | 'private' | 'direct'
76
+ isActive: boolean
77
+ memberRole: 'admin' | 'moderator' | 'member'
78
+ joinedAt: string
79
+ createdAt: string
80
+ unreadCount?: number
81
+ emoji?: string
82
+ }
83
+
84
+ export interface ChannelMember {
85
+ id: string
86
+ userId: string
87
+ fullName: string
88
+ avatarUrl?: string | null
89
+ role: 'admin' | 'moderator' | 'member'
90
+ userRole?: string
91
+ }
92
+
93
+ // ─── Direct Message Types ─────────────────────────────────────────────
94
+
95
+ export interface DirectConversation {
96
+ id: string
97
+ name: string
98
+ avatar?: string
99
+ lastMessageAt: string
100
+ unreadCount?: number
101
+ isOnline?: boolean
102
+ }
103
+
104
+ export interface OrgMember {
105
+ id: string
106
+ full_name: string
107
+ avatar_url?: string
108
+ role: string
109
+ active?: boolean
110
+ }
111
+
112
+ // ─── Emoji Types ──────────────────────────────────────────────────────
113
+
114
+ export type EmojiCategory =
115
+ | 'search'
116
+ | 'frequently_used'
117
+ | 'smileys_people'
118
+ | 'animals_nature'
119
+ | 'food_drink'
120
+ | 'travel_places'
121
+ | 'activities'
122
+ | 'objects'
123
+ | 'symbols'
124
+ | 'flags'
125
+ | 'custom'
126
+
127
+ export type SkinTone =
128
+ | 'default'
129
+ | 'light'
130
+ | 'medium-light'
131
+ | 'medium'
132
+ | 'medium-dark'
133
+ | 'dark'
134
+
135
+ export interface StandardEmoji {
136
+ unicode: string
137
+ name: string
138
+ shortcode: string
139
+ category: EmojiCategory
140
+ keywords: string[]
141
+ skinTones?: string[]
142
+ }
143
+
144
+ export interface CustomEmoji {
145
+ id: string
146
+ organizationId: string
147
+ name: string
148
+ displayName: string
149
+ description?: string
150
+ fileName: string
151
+ fileSize: number
152
+ contentType: 'image/png' | 'image/gif'
153
+ storagePath: string
154
+ isAnimated: boolean
155
+ isActive: boolean
156
+ usageCount: number
157
+ uploadedBy: string
158
+ createdAt: string
159
+ updatedAt: string
160
+ }
161
+
162
+ export interface EmojiReaction {
163
+ id: string
164
+ messageId: string
165
+ userId: string
166
+ emojiUnicode?: string
167
+ customEmojiId?: string
168
+ skinTone?: SkinTone
169
+ createdAt: string
170
+ }
171
+
172
+ export interface ReactionSummary {
173
+ emojiUnicode?: string
174
+ customEmojiId?: string
175
+ customEmojiName?: string
176
+ customEmojiStoragePath?: string
177
+ customEmojiIsAnimated?: boolean
178
+ reactionCount: number
179
+ userReacted: boolean
180
+ recentUsers: Array<{ id: string; name: string }>
181
+ }
182
+
183
+ export interface EmojiSelection {
184
+ type: 'standard' | 'custom'
185
+ unicode?: string
186
+ customEmojiId?: string
187
+ name: string
188
+ shortcode: string
189
+ skinTone?: SkinTone
190
+ }
191
+
192
+ export interface EmojiPreferences {
193
+ skinTone: SkinTone
194
+ frequentlyUsed: string[]
195
+ lastUsedAt?: string
196
+ customEmojiFavorites?: string[]
197
+ }
198
+
199
+ export interface EmojiCategoryInfo {
200
+ id: EmojiCategory
201
+ name: string
202
+ icon: string
203
+ description: string
204
+ }
205
+
206
+ export interface SkinToneInfo {
207
+ id: SkinTone
208
+ name: string
209
+ unicode: string
210
+ preview: string
211
+ }
212
+
213
+ // ─── Mention Types ────────────────────────────────────────────────────
214
+
215
+ export interface MentionItem {
216
+ type: string
217
+ id: string
218
+ name: string
219
+ secondary: string
220
+ url: string
221
+ }
222
+
223
+ // ─── Notification Types ───────────────────────────────────────────────
224
+
225
+ export interface NotificationChannels {
226
+ email: boolean
227
+ sms: boolean
228
+ }
229
+
230
+ export interface NotificationEvents {
231
+ [key: string]: boolean
232
+ }
233
+
234
+ export interface NotificationTiming {
235
+ [key: string]: string | number
236
+ }
237
+
238
+ export interface NotificationPreferences {
239
+ channels: NotificationChannels
240
+ events: NotificationEvents
241
+ timing: NotificationTiming
242
+ }
243
+
244
+ // ─── Presence Types ───────────────────────────────────────────────────
245
+
246
+ export interface PresenceState {
247
+ [userId: string]: {
248
+ status: string
249
+ technicianStatus?: string
250
+ lastSeen?: string
251
+ }
252
+ }
253
+
254
+ // ─── Callback Interfaces ──────────────────────────────────────────────
255
+ // These define the data-layer contracts that consuming apps must implement.
256
+
257
+ export interface MessageCallbacks {
258
+ loadMessages: (params: {
259
+ channelId?: string
260
+ dmUserId?: string
261
+ before?: string
262
+ limit?: number
263
+ }) => Promise<Message[]>
264
+ sendMessage: (params: {
265
+ content: string
266
+ channelId?: string
267
+ dmUserId?: string
268
+ parentMessageId?: string
269
+ quotedMessageId?: string
270
+ attachments?: PendingAttachment[]
271
+ priority?: string
272
+ }) => Promise<Message>
273
+ editMessage: (messageId: string, content: string) => Promise<Message>
274
+ deleteMessage: (messageId: string) => Promise<void>
275
+ pinMessage: (messageId: string, channelId: string) => Promise<void>
276
+ unpinMessage: (messageId: string, channelId: string) => Promise<void>
277
+ markAsRead: (messageIds: string[]) => Promise<void>
278
+ }
279
+
280
+ export interface ReactionCallbacks {
281
+ loadReactions: (messageId: string) => Promise<ReactionSummary[]>
282
+ addReaction: (
283
+ messageId: string,
284
+ emoji: { unicode?: string; customEmojiId?: string; skinTone?: SkinTone }
285
+ ) => Promise<void>
286
+ removeReaction: (
287
+ messageId: string,
288
+ emoji: { unicode?: string; customEmojiId?: string }
289
+ ) => Promise<void>
290
+ }
291
+
292
+ export interface AttachmentCallbacks {
293
+ getSignedUrl: (storagePath: string) => Promise<string>
294
+ getThumbnailUrl: (thumbnailPath: string) => Promise<string | null>
295
+ uploadFile: (
296
+ file: File,
297
+ organizationId: string
298
+ ) => Promise<{ path: string; error?: string }>
299
+ deleteAttachment: (attachmentId: string) => Promise<void>
300
+ }
301
+
302
+ export interface MentionCallbacks {
303
+ searchMentions: (query: string) => Promise<MentionItem[]>
304
+ }
305
+
306
+ export interface EmojiCallbacks {
307
+ loadCustomEmojis: () => Promise<CustomEmoji[]>
308
+ loadPreferences: () => Promise<EmojiPreferences>
309
+ savePreferences: (prefs: Partial<EmojiPreferences>) => Promise<void>
310
+ }
311
+
312
+ export interface ChannelCallbacks {
313
+ createChannel: (params: {
314
+ name: string
315
+ description?: string
316
+ type: 'public' | 'private'
317
+ emoji?: string
318
+ memberIds?: string[]
319
+ }) => Promise<Channel>
320
+ updateChannel: (
321
+ channelId: string,
322
+ params: Partial<Pick<Channel, 'name' | 'description' | 'emoji'>>
323
+ ) => Promise<Channel>
324
+ deleteChannel: (channelId: string) => Promise<void>
325
+ leaveChannel: (channelId: string) => Promise<void>
326
+ getMembers: (channelId: string) => Promise<ChannelMember[]>
327
+ addMember: (channelId: string, userId: string) => Promise<void>
328
+ removeMember: (channelId: string, userId: string) => Promise<void>
329
+ updateMemberRole: (
330
+ channelId: string,
331
+ userId: string,
332
+ role: 'admin' | 'moderator' | 'member'
333
+ ) => Promise<void>
334
+ }
335
+
336
+ export interface ThreadCallbacks {
337
+ loadReplies: (parentMessageId: string) => Promise<Message[]>
338
+ sendReply: (
339
+ parentMessageId: string,
340
+ content: string,
341
+ attachments?: PendingAttachment[]
342
+ ) => Promise<Message>
343
+ }
344
+
345
+ export interface NotificationCallbacks {
346
+ loadSettings: (params: {
347
+ type: 'global' | 'channel'
348
+ channelId?: string
349
+ }) => Promise<NotificationPreferences>
350
+ saveSettings: (
351
+ params: {
352
+ type: 'global' | 'channel'
353
+ channelId?: string
354
+ },
355
+ settings: NotificationPreferences
356
+ ) => Promise<void>
357
+ }
358
+
359
+ // ─── Emoji Data Constants ─────────────────────────────────────────────
360
+
361
+ export const EMOJI_CATEGORIES: EmojiCategoryInfo[] = [
362
+ { id: 'search', name: 'Search', icon: '🔍', description: 'Search results' },
363
+ {
364
+ id: 'frequently_used',
365
+ name: 'Frequently Used',
366
+ icon: '🕐',
367
+ description: 'Your most used emojis'
368
+ },
369
+ {
370
+ id: 'smileys_people',
371
+ name: 'Smileys & People',
372
+ icon: '😀',
373
+ description: 'Faces, people, and emotions'
374
+ },
375
+ {
376
+ id: 'animals_nature',
377
+ name: 'Animals & Nature',
378
+ icon: '🐻',
379
+ description: 'Animals, plants, and nature'
380
+ },
381
+ {
382
+ id: 'food_drink',
383
+ name: 'Food & Drink',
384
+ icon: '🍎',
385
+ description: 'Food, drinks, and meals'
386
+ },
387
+ {
388
+ id: 'travel_places',
389
+ name: 'Travel & Places',
390
+ icon: '🏠',
391
+ description: 'Places, transport, and travel'
392
+ },
393
+ {
394
+ id: 'activities',
395
+ name: 'Activities',
396
+ icon: '⚽',
397
+ description: 'Sports, games, and activities'
398
+ },
399
+ { id: 'objects', name: 'Objects', icon: '🔨', description: 'Tools, objects, and items' },
400
+ {
401
+ id: 'symbols',
402
+ name: 'Symbols',
403
+ icon: '❤️',
404
+ description: 'Symbols, signs, and icons'
405
+ },
406
+ {
407
+ id: 'flags',
408
+ name: 'Flags',
409
+ icon: '🏳️',
410
+ description: 'Country and regional flags'
411
+ },
412
+ {
413
+ id: 'custom',
414
+ name: 'Custom',
415
+ icon: '🎨',
416
+ description: 'Organization custom emojis'
417
+ }
418
+ ]
419
+
420
+ export const SKIN_TONES: SkinToneInfo[] = [
421
+ { id: 'default', name: 'Default', unicode: '', preview: '👋' },
422
+ { id: 'light', name: 'Light', unicode: '🏻', preview: '👋🏻' },
423
+ { id: 'medium-light', name: 'Medium Light', unicode: '🏼', preview: '👋🏼' },
424
+ { id: 'medium', name: 'Medium', unicode: '🏽', preview: '👋🏽' },
425
+ { id: 'medium-dark', name: 'Medium Dark', unicode: '🏾', preview: '👋🏾' },
426
+ { id: 'dark', name: 'Dark', unicode: '🏿', preview: '👋🏿' }
427
+ ]
428
+
429
+ // Default quick-reaction emojis (used when no user preferences available)
430
+ export const DEFAULT_QUICK_REACTIONS: StandardEmoji[] = [
431
+ {
432
+ unicode: '👍',
433
+ name: 'Thumbs Up',
434
+ shortcode: 'thumbsup',
435
+ category: 'smileys_people',
436
+ keywords: ['like', 'approve', 'yes']
437
+ },
438
+ {
439
+ unicode: '❤️',
440
+ name: 'Red Heart',
441
+ shortcode: 'heart',
442
+ category: 'symbols',
443
+ keywords: ['love', 'heart']
444
+ },
445
+ {
446
+ unicode: '😂',
447
+ name: 'Face with Tears of Joy',
448
+ shortcode: 'joy',
449
+ category: 'smileys_people',
450
+ keywords: ['laugh', 'lol', 'funny']
451
+ },
452
+ {
453
+ unicode: '🎉',
454
+ name: 'Party Popper',
455
+ shortcode: 'tada',
456
+ category: 'activities',
457
+ keywords: ['party', 'celebrate']
458
+ }
459
+ ]
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Currency formatting utilities
3
+ */
4
+
5
+ /**
6
+ * Format a number as USD currency string.
7
+ * Override locale/currency by passing options.
8
+ */
9
+ export function formatCurrency(
10
+ amount: number,
11
+ locale: string = 'en-US',
12
+ currency: string = 'USD'
13
+ ): string {
14
+ return new Intl.NumberFormat(locale, {
15
+ style: 'currency',
16
+ currency,
17
+ minimumFractionDigits: 2,
18
+ maximumFractionDigits: 2
19
+ }).format(amount);
20
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * DaisyUI Color Utility System
3
+ *
4
+ * Provides utilities for accessing DaisyUI semantic colors in JavaScript/Canvas contexts.
5
+ * Handles color format conversion and theme change detection for dynamic updates.
6
+ */
7
+
8
+ let colorCache = new Map<string, string>()
9
+ let themeObserver: MutationObserver | null = null
10
+
11
+ /**
12
+ * Get a DaisyUI semantic color as a hex string usable in Canvas
13
+ * @param colorName - DaisyUI color name (e.g., "primary", "accent", "base-100")
14
+ * @returns Hex color string (e.g., "#3b82f6")
15
+ */
16
+ export function getDaisyUIColor(colorName: string): string {
17
+ const cacheKey = colorName
18
+ if (colorCache.has(cacheKey)) {
19
+ return colorCache.get(cacheKey)!
20
+ }
21
+
22
+ const fallbackColor = getColorFallback(colorName)
23
+ colorCache.set(cacheKey, fallbackColor)
24
+ return fallbackColor
25
+ }
26
+
27
+ function getColorFallback(colorName: string): string {
28
+ const currentTheme =
29
+ document.documentElement.getAttribute("data-theme") || "light"
30
+
31
+ const luxuryColors: Record<string, string> = {
32
+ primary: "#d4af37",
33
+ "primary-content": "#1a1a1a",
34
+ secondary: "#c0c0c0",
35
+ "secondary-content": "#1a1a1a",
36
+ accent: "#ffd700",
37
+ "accent-content": "#1a1a1a",
38
+ neutral: "#6b7280",
39
+ "neutral-content": "#f5f5f5",
40
+ "base-100": "#1a1a1a",
41
+ "base-200": "#2d2d2d",
42
+ "base-300": "#404040",
43
+ "base-content": "#f5f5f5",
44
+ success: "#10b981",
45
+ "success-content": "#1a1a1a",
46
+ warning: "#f59e0b",
47
+ "warning-content": "#1a1a1a",
48
+ error: "#ef4444",
49
+ "error-content": "#f5f5f5",
50
+ info: "#3b82f6",
51
+ "info-content": "#f5f5f5",
52
+ }
53
+
54
+ const nordColors: Record<string, string> = {
55
+ primary: "#5e81ac",
56
+ "primary-content": "#eceff4",
57
+ secondary: "#81a1c1",
58
+ "secondary-content": "#2e3440",
59
+ accent: "#88c0d0",
60
+ "accent-content": "#2e3440",
61
+ neutral: "#4c566a",
62
+ "neutral-content": "#eceff4",
63
+ "base-100": "#2e3440",
64
+ "base-200": "#3b4252",
65
+ "base-300": "#434c5e",
66
+ "base-content": "#eceff4",
67
+ success: "#a3be8c",
68
+ "success-content": "#2e3440",
69
+ warning: "#ebcb8b",
70
+ "warning-content": "#2e3440",
71
+ error: "#bf616a",
72
+ "error-content": "#eceff4",
73
+ info: "#81a1c1",
74
+ "info-content": "#2e3440",
75
+ }
76
+
77
+ const darkColors: Record<string, string> = {
78
+ primary: "#661ae6",
79
+ "primary-content": "#ffffff",
80
+ secondary: "#d926a9",
81
+ "secondary-content": "#ffffff",
82
+ accent: "#1fb2a6",
83
+ "accent-content": "#ffffff",
84
+ neutral: "#2a323c",
85
+ "neutral-content": "#a6adbb",
86
+ "base-100": "#1d232a",
87
+ "base-200": "#191e24",
88
+ "base-300": "#15191e",
89
+ "base-content": "#a6adbb",
90
+ info: "#3abff8",
91
+ "info-content": "#002b3d",
92
+ success: "#36d399",
93
+ "success-content": "#003320",
94
+ warning: "#fbbd23",
95
+ "warning-content": "#382800",
96
+ error: "#f87272",
97
+ "error-content": "#470000",
98
+ }
99
+
100
+ const defaultColors: Record<string, string> = {
101
+ primary: "#3b82f6",
102
+ "primary-content": "#ffffff",
103
+ secondary: "#6b7280",
104
+ "secondary-content": "#ffffff",
105
+ accent: "#f97316",
106
+ "accent-content": "#ffffff",
107
+ neutral: "#6b7280",
108
+ "neutral-content": "#ffffff",
109
+ "base-100": "#ffffff",
110
+ "base-200": "#f1f5f9",
111
+ "base-300": "#e2e8f0",
112
+ "base-content": "#1f2937",
113
+ info: "#3b82f6",
114
+ "info-content": "#ffffff",
115
+ success: "#10b981",
116
+ "success-content": "#ffffff",
117
+ warning: "#f59e0b",
118
+ "warning-content": "#ffffff",
119
+ error: "#ef4444",
120
+ "error-content": "#ffffff",
121
+ }
122
+
123
+ const fallbacks =
124
+ currentTheme === "luxury"
125
+ ? luxuryColors
126
+ : currentTheme === "nord"
127
+ ? nordColors
128
+ : currentTheme === "dark"
129
+ ? darkColors
130
+ : defaultColors
131
+ return fallbacks[colorName] || "#6b7280"
132
+ }
133
+
134
+ /**
135
+ * Create a lighter version of a color
136
+ */
137
+ export function lightenColor(hexColor: string, amount: number = 0.2): string {
138
+ const hex = hexColor.replace("#", "")
139
+ const r = parseInt(hex.substring(0, 2), 16)
140
+ const g = parseInt(hex.substring(2, 4), 16)
141
+ const b = parseInt(hex.substring(4, 6), 16)
142
+
143
+ const newR = Math.min(255, Math.floor(r + (255 - r) * amount))
144
+ const newG = Math.min(255, Math.floor(g + (255 - g) * amount))
145
+ const newB = Math.min(255, Math.floor(b + (255 - b) * amount))
146
+
147
+ return `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, "0")}`
148
+ }
149
+
150
+ /**
151
+ * Create a darker version of a color
152
+ */
153
+ export function darkenColor(hexColor: string, amount: number = 0.2): string {
154
+ const hex = hexColor.replace("#", "")
155
+ const r = parseInt(hex.substring(0, 2), 16)
156
+ const g = parseInt(hex.substring(2, 4), 16)
157
+ const b = parseInt(hex.substring(4, 6), 16)
158
+
159
+ const newR = Math.max(0, Math.floor(r * (1 - amount)))
160
+ const newG = Math.max(0, Math.floor(g * (1 - amount)))
161
+ const newB = Math.max(0, Math.floor(b * (1 - amount)))
162
+
163
+ return `#${((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, "0")}`
164
+ }
165
+
166
+ /**
167
+ * Setup theme change detection
168
+ * Clears color cache when theme changes and optionally calls a callback
169
+ */
170
+ export function watchThemeChanges(onThemeChange?: () => void): () => void {
171
+ if (themeObserver) {
172
+ themeObserver.disconnect()
173
+ }
174
+
175
+ themeObserver = new MutationObserver((mutations: MutationRecord[]): void => {
176
+ let themeChanged = false
177
+
178
+ mutations.forEach((mutation: MutationRecord): void => {
179
+ if (
180
+ mutation.type === "attributes" &&
181
+ (mutation.attributeName === "data-theme" ||
182
+ mutation.attributeName === "class")
183
+ ) {
184
+ themeChanged = true
185
+ }
186
+ })
187
+
188
+ if (themeChanged) {
189
+ colorCache.clear()
190
+ if (onThemeChange) {
191
+ onThemeChange()
192
+ }
193
+ }
194
+ })
195
+
196
+ themeObserver.observe(document.documentElement, {
197
+ attributes: true,
198
+ attributeFilter: ["data-theme", "class"],
199
+ })
200
+
201
+ return () => {
202
+ if (themeObserver) {
203
+ themeObserver.disconnect()
204
+ themeObserver = null
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Check if we're currently in a dark theme
211
+ */
212
+ export function isDarkTheme(): boolean {
213
+ const baseContent = getDaisyUIColor("base-content")
214
+ const base100 = getDaisyUIColor("base-100")
215
+
216
+ const contentBrightness = getBrightness(baseContent)
217
+ const baseBrightness = getBrightness(base100)
218
+
219
+ return contentBrightness > baseBrightness
220
+ }
221
+
222
+ function getBrightness(hexColor: string): number {
223
+ const hex = hexColor.replace("#", "")
224
+ const r = parseInt(hex.substring(0, 2), 16)
225
+ const g = parseInt(hex.substring(2, 4), 16)
226
+ const b = parseInt(hex.substring(4, 6), 16)
227
+
228
+ return (r * 299 + g * 587 + b * 114) / 1000
229
+ }
230
+
231
+ /**
232
+ * Get the current theme name if available
233
+ */
234
+ export function getCurrentTheme(): string | null {
235
+ return document.documentElement.getAttribute("data-theme")
236
+ }
237
+
238
+ /**
239
+ * Clear the color cache manually
240
+ */
241
+ export function clearColorCache(): void {
242
+ colorCache.clear()
243
+ }