@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,153 @@
1
+ /**
2
+ * Date formatting utilities for consistent date/time display.
3
+ *
4
+ * UTC/ISO Timestamp Handling:
5
+ * Database timestamps are often stored without timezone info (e.g., "2024-11-21 15:30:00").
6
+ * These utilities normalize timestamps to ensure correct UTC parsing.
7
+ */
8
+
9
+ /**
10
+ * Normalize a timestamp string to proper ISO 8601 format.
11
+ * Handles database timestamps that may lack 'T' separator or 'Z' suffix.
12
+ */
13
+ export function normalizeTimestamp(timestamp: string): string {
14
+ if (!timestamp) return timestamp;
15
+
16
+ if (timestamp.includes('T')) {
17
+ if (timestamp.endsWith('Z')) return timestamp;
18
+ if (/[+-]\d{2}:\d{2}$/.test(timestamp)) return timestamp;
19
+ return timestamp + 'Z';
20
+ }
21
+
22
+ return timestamp.replace(' ', 'T') + 'Z';
23
+ }
24
+
25
+ /**
26
+ * Parse a potentially non-standard timestamp into a Date object.
27
+ */
28
+ export function parseTimestamp(timestamp: string | null | undefined): Date | null {
29
+ if (!timestamp) return null;
30
+
31
+ const normalized = normalizeTimestamp(timestamp);
32
+ const date = new Date(normalized);
33
+
34
+ return isNaN(date.getTime()) ? null : date;
35
+ }
36
+
37
+ /**
38
+ * Format relative time (e.g., "2d", "3mo", "1y").
39
+ * Compact format suitable for tables and compact UIs.
40
+ */
41
+ export function formatRelativeTime(dateStr: string | null | undefined): string {
42
+ if (!dateStr) return '-';
43
+
44
+ const date = parseTimestamp(dateStr);
45
+ if (!date) return '-';
46
+
47
+ const now = new Date();
48
+ const diffMs = now.getTime() - date.getTime();
49
+ const diffMins = Math.floor(diffMs / (1000 * 60));
50
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
51
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
52
+ const diffWeeks = Math.floor(diffDays / 7);
53
+ const diffMonths = Math.floor(diffDays / 30);
54
+ const diffYears = Math.floor(diffDays / 365);
55
+
56
+ if (diffMins < 1) return 'now';
57
+ if (diffMins < 60) return `${diffMins}m`;
58
+ if (diffHours < 24) return `${diffHours}h`;
59
+ if (diffDays < 7) return `${diffDays}d`;
60
+ if (diffWeeks < 4) return `${diffWeeks}w`;
61
+ if (diffMonths < 12) return `${diffMonths}mo`;
62
+ return `${diffYears}y`;
63
+ }
64
+
65
+ /**
66
+ * Format full date and time for tooltips and detailed views.
67
+ */
68
+ export function formatFullDate(dateStr: string | null | undefined): string {
69
+ if (!dateStr) return '';
70
+
71
+ const date = parseTimestamp(dateStr);
72
+ if (!date) return '';
73
+
74
+ return date.toLocaleString('en-US', {
75
+ month: 'short',
76
+ day: 'numeric',
77
+ year: 'numeric',
78
+ hour: '2-digit',
79
+ minute: '2-digit'
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Format short date for compact displays (e.g., date range picker).
85
+ */
86
+ export function formatShortDate(dateStr: string | null | undefined): string {
87
+ if (!dateStr) return '';
88
+
89
+ const date = parseTimestamp(dateStr);
90
+ if (!date) return '';
91
+
92
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
93
+ }
94
+
95
+ /**
96
+ * Format last activity time for agent cards.
97
+ * Shows "Just now", "Xm ago", "Xh ago", or "Xd ago".
98
+ */
99
+ export function formatLastActivity(timestamp: string | null | undefined): string {
100
+ if (!timestamp) return 'Never';
101
+
102
+ const date = parseTimestamp(timestamp);
103
+ if (!date) return 'Never';
104
+
105
+ const now = new Date();
106
+ const diffMs = now.getTime() - date.getTime();
107
+ const diffMins = Math.floor(diffMs / 60000);
108
+ const diffHours = Math.floor(diffMs / 3600000);
109
+ const diffDays = Math.floor(diffMs / 86400000);
110
+
111
+ if (diffMins < 1) return 'Just now';
112
+ if (diffMins < 60) return `${diffMins}m ago`;
113
+ if (diffHours < 24) return `${diffHours}h ago`;
114
+ return `${diffDays}d ago`;
115
+ }
116
+
117
+ /**
118
+ * Format date for display (simple locale string).
119
+ */
120
+ export function formatDate(dateString: string | null | undefined): string {
121
+ if (!dateString) return 'N/A';
122
+
123
+ const date = parseTimestamp(dateString);
124
+ if (!date) return 'N/A';
125
+
126
+ return date.toLocaleString();
127
+ }
128
+
129
+ /**
130
+ * Get milliseconds since timestamp (for calculations).
131
+ */
132
+ export function getTimeSinceMs(timestamp: string | null | undefined): number {
133
+ if (!timestamp) return Infinity;
134
+
135
+ const date = parseTimestamp(timestamp);
136
+ if (!date) return Infinity;
137
+
138
+ return Date.now() - date.getTime();
139
+ }
140
+
141
+ /**
142
+ * Get minutes since timestamp.
143
+ */
144
+ export function getTimeSinceMinutes(timestamp: string | null | undefined): number {
145
+ return getTimeSinceMs(timestamp) / 60000;
146
+ }
147
+
148
+ /**
149
+ * Check if timestamp is within a given number of minutes.
150
+ */
151
+ export function isWithinMinutes(timestamp: string | null | undefined, minutes: number): boolean {
152
+ return getTimeSinceMinutes(timestamp) < minutes;
153
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Mention Parser Utilities
3
+ *
4
+ * Handles parsing and rendering of @-mentions in messages.
5
+ * Format: [@Display Name](type:id) -> clickable links
6
+ *
7
+ * The URL generator is configurable — consuming apps provide their own
8
+ * routing via the `urlGenerator` parameter or by setting a default.
9
+ */
10
+
11
+ export interface MentionMatch {
12
+ type: string
13
+ id: string
14
+ displayName: string
15
+ url: string
16
+ startIndex: number
17
+ endIndex: number
18
+ fullMatch: string
19
+ }
20
+
21
+ export type MentionUrlGenerator = (type: string, id: string) => string
22
+
23
+ // Regex to match mention pattern: [@Display Name](type:id)
24
+ const MENTION_REGEX = /\[@([^\]]+)\]\(([^:]+):([^)]+)\)/g
25
+
26
+ // Default URL generator (apps can override)
27
+ let defaultUrlGenerator: MentionUrlGenerator = (type, id) => `#${type}/${id}`
28
+
29
+ /**
30
+ * Set the default URL generator for mentions.
31
+ * Call this once at app startup to configure mention link routing.
32
+ */
33
+ export function setMentionUrlGenerator(generator: MentionUrlGenerator): void {
34
+ defaultUrlGenerator = generator
35
+ }
36
+
37
+ /**
38
+ * Parse message content and extract all mentions
39
+ */
40
+ export function parseMentions(
41
+ content: string,
42
+ urlGenerator?: MentionUrlGenerator
43
+ ): MentionMatch[] {
44
+ const mentions: MentionMatch[] = []
45
+ const gen = urlGenerator ?? defaultUrlGenerator
46
+ let match
47
+
48
+ MENTION_REGEX.lastIndex = 0
49
+
50
+ while ((match = MENTION_REGEX.exec(content)) !== null) {
51
+ const [fullMatch, displayName, type, id] = match
52
+
53
+ mentions.push({
54
+ type,
55
+ id,
56
+ displayName,
57
+ url: gen(type, id),
58
+ startIndex: match.index,
59
+ endIndex: match.index + fullMatch.length,
60
+ fullMatch
61
+ })
62
+ }
63
+
64
+ return mentions
65
+ }
66
+
67
+ /**
68
+ * Convert message content with mentions to HTML
69
+ */
70
+ export function renderMentionsAsHTML(
71
+ content: string,
72
+ urlGenerator?: MentionUrlGenerator
73
+ ): string {
74
+ const mentions = parseMentions(content, urlGenerator)
75
+
76
+ if (mentions.length === 0) {
77
+ return escapeHTML(content)
78
+ }
79
+
80
+ let result = ''
81
+ let lastIndex = 0
82
+
83
+ mentions.forEach((mention) => {
84
+ result += escapeHTML(content.substring(lastIndex, mention.startIndex))
85
+ result += `<a href="${mention.url}" class="mention-link mention-${mention.type}" data-mention-type="${mention.type}" data-mention-id="${mention.id}">@${escapeHTML(mention.displayName)}</a>`
86
+ lastIndex = mention.endIndex
87
+ })
88
+
89
+ result += escapeHTML(content.substring(lastIndex))
90
+ return result
91
+ }
92
+
93
+ /**
94
+ * Create a mention string for insertion into message input
95
+ */
96
+ export function createMentionString(type: string, id: string, displayName: string): string {
97
+ return `[@${displayName}](${type}:${id})`
98
+ }
99
+
100
+ /**
101
+ * Extract plain text from message content (remove mention markup)
102
+ */
103
+ export function extractPlainText(content: string): string {
104
+ MENTION_REGEX.lastIndex = 0
105
+ return content.replace(MENTION_REGEX, '@$1')
106
+ }
107
+
108
+ /**
109
+ * Check if cursor is inside a mention
110
+ */
111
+ export function findMentionAtCursor(content: string, cursorPosition: number): MentionMatch | null {
112
+ const mentions = parseMentions(content)
113
+ return (
114
+ mentions.find(
115
+ (mention) => cursorPosition >= mention.startIndex && cursorPosition <= mention.endIndex
116
+ ) || null
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Find @ symbol and following text that could be a mention being typed.
122
+ * Supported symbols: @ $ # ! ~ / +
123
+ */
124
+ export function findPartialMention(
125
+ content: string,
126
+ cursorPosition: number
127
+ ): { start: number; query: string } | null {
128
+ const mentionSymbols = ['@', '$', '#', '!', '~', '/', '+']
129
+ let symbolIndex = -1
130
+
131
+ for (let i = cursorPosition - 1; i >= 0; i--) {
132
+ const char = content[i]
133
+
134
+ if (mentionSymbols.includes(char)) {
135
+ symbolIndex = i
136
+ break
137
+ }
138
+
139
+ if (char === ' ' || char === '\n' || char === ']' || char === ')') {
140
+ break
141
+ }
142
+ }
143
+
144
+ if (symbolIndex === -1) return null
145
+
146
+ const query = content.substring(symbolIndex + 1, cursorPosition).trim()
147
+
148
+ const existingMention = findMentionAtCursor(content, symbolIndex)
149
+ if (existingMention) return null
150
+
151
+ return { start: symbolIndex, query }
152
+ }
153
+
154
+ /**
155
+ * Replace partial mention with complete mention string
156
+ */
157
+ export function replaceMentionInContent(
158
+ content: string,
159
+ startIndex: number,
160
+ endIndex: number,
161
+ mentionString: string
162
+ ): { newContent: string; newCursorPosition: number } {
163
+ const before = content.substring(0, startIndex)
164
+ const after = content.substring(endIndex)
165
+ const newContent = before + mentionString + after
166
+ const newCursorPosition = startIndex + mentionString.length
167
+
168
+ return { newContent, newCursorPosition }
169
+ }
170
+
171
+ /**
172
+ * Simple HTML escape function
173
+ */
174
+ export function escapeHTML(text: string): string {
175
+ return text
176
+ .replace(/&/g, '&amp;')
177
+ .replace(/</g, '&lt;')
178
+ .replace(/>/g, '&gt;')
179
+ .replace(/"/g, '&quot;')
180
+ .replace(/'/g, '&#039;')
181
+ }
182
+
183
+ /**
184
+ * Count mentions in content
185
+ */
186
+ export function countMentions(content: string): number {
187
+ return parseMentions(content).length
188
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Phone number formatting utilities
3
+ *
4
+ * Provides consistent phone number formatting as (xxx) xxx-xxxx
5
+ * Limits input to 10 digits and handles formatting as user types
6
+ */
7
+
8
+ /**
9
+ * Strips all non-numeric characters from a phone number
10
+ */
11
+ export function stripPhoneNumber(value: string): string {
12
+ return value.replace(/\D/g, '');
13
+ }
14
+
15
+ /**
16
+ * Formats a phone number to (xxx) xxx-xxxx format
17
+ */
18
+ export function formatPhoneNumber(value: string): string {
19
+ const digits = stripPhoneNumber(value);
20
+ const limitedDigits = digits.slice(0, 10);
21
+
22
+ if (limitedDigits.length === 0) {
23
+ return '';
24
+ } else if (limitedDigits.length <= 3) {
25
+ return `(${limitedDigits}`;
26
+ } else if (limitedDigits.length <= 6) {
27
+ return `(${limitedDigits.slice(0, 3)}) ${limitedDigits.slice(3)}`;
28
+ } else {
29
+ return `(${limitedDigits.slice(0, 3)}) ${limitedDigits.slice(3, 6)}-${limitedDigits.slice(6)}`;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Checks if a phone number has exactly 10 digits
35
+ */
36
+ export function isCompletePhoneNumber(value: string): boolean {
37
+ return stripPhoneNumber(value).length === 10;
38
+ }
39
+
40
+ /**
41
+ * Validates phone number format (exactly 10 digits)
42
+ */
43
+ export function isValidPhoneNumber(value: string): boolean {
44
+ return isCompletePhoneNumber(value);
45
+ }
46
+
47
+ /**
48
+ * Handles input formatting with cursor position preservation
49
+ */
50
+ export function updatePhoneInput(input: HTMLInputElement, newValue: string, oldValue: string): void {
51
+ const cursorPosition = input.selectionStart || 0;
52
+ const oldLength = oldValue.length;
53
+ const newLength = newValue.length;
54
+
55
+ input.value = newValue;
56
+
57
+ let newCursorPosition = cursorPosition;
58
+
59
+ if (newLength > oldLength) {
60
+ newCursorPosition = cursorPosition + (newLength - oldLength);
61
+ } else if (newLength < oldLength) {
62
+ newCursorPosition = Math.max(0, cursorPosition - (oldLength - newLength));
63
+ }
64
+
65
+ if (newCursorPosition === 1 && newValue.startsWith('(')) {
66
+ newCursorPosition = 1;
67
+ } else if (newCursorPosition === 4 && newValue.includes(') ')) {
68
+ newCursorPosition = 6;
69
+ } else if (newCursorPosition === 9 && newValue.includes('-')) {
70
+ newCursorPosition = 10;
71
+ }
72
+
73
+ input.setSelectionRange(newCursorPosition, newCursorPosition);
74
+ }