@joewinke/jatui 0.1.25 → 0.1.26
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/dist/actions/collapse.d.ts +80 -0
- package/dist/actions/collapse.js +134 -0
- package/dist/actions/railNav.d.ts +167 -0
- package/dist/actions/railNav.js +351 -0
- package/dist/components/AnnotationLayer.svelte +108 -0
- package/dist/components/AnnotationLayer.svelte.d.ts +12 -0
- package/dist/components/AnnotationPanel.svelte +319 -0
- package/dist/components/AnnotationPanel.svelte.d.ts +13 -0
- package/dist/components/AudioWaveform.svelte +701 -0
- package/dist/components/AudioWaveform.svelte.d.ts +19 -0
- package/dist/components/AvailabilityModal.svelte +177 -0
- package/dist/components/AvailabilityModal.svelte.d.ts +14 -0
- package/dist/components/AvatarUpload.svelte +234 -0
- package/dist/components/AvatarUpload.svelte.d.ts +23 -0
- package/dist/components/Badge.svelte +38 -0
- package/dist/components/Badge.svelte.d.ts +14 -0
- package/dist/components/BookingForm.svelte +278 -0
- package/dist/components/BookingForm.svelte.d.ts +37 -0
- package/dist/components/BurndownChart.svelte +778 -0
- package/dist/components/BurndownChart.svelte.d.ts +17 -0
- package/dist/components/Button.svelte +81 -0
- package/dist/components/Button.svelte.d.ts +21 -0
- package/dist/components/CalendarPicker.svelte +284 -0
- package/dist/components/CalendarPicker.svelte.d.ts +22 -0
- package/dist/components/Card.svelte +67 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CharacterCounter.svelte +82 -0
- package/dist/components/CharacterCounter.svelte.d.ts +10 -0
- package/dist/components/ChipInput.svelte +609 -0
- package/dist/components/ChipInput.svelte.d.ts +55 -0
- package/dist/components/Collapse.svelte +91 -0
- package/dist/components/Collapse.svelte.d.ts +18 -0
- package/dist/components/ColorSelector.svelte +167 -0
- package/dist/components/ColorSelector.svelte.d.ts +9 -0
- package/dist/components/CommentThread.svelte +773 -0
- package/dist/components/CommentThread.svelte.d.ts +32 -0
- package/dist/components/ConfirmDialog.svelte +348 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/components/ConfirmModal.svelte +142 -0
- package/dist/components/ConfirmModal.svelte.d.ts +15 -0
- package/dist/components/ContextMenu.svelte +188 -0
- package/dist/components/ContextMenu.svelte.d.ts +31 -0
- package/dist/components/CountdownTimer.svelte +94 -0
- package/dist/components/CountdownTimer.svelte.d.ts +10 -0
- package/dist/components/DateRangePicker.svelte +194 -0
- package/dist/components/DateRangePicker.svelte.d.ts +10 -0
- package/dist/components/Drawer.svelte +143 -0
- package/dist/components/Drawer.svelte.d.ts +33 -0
- package/dist/components/EmojiPicker.svelte +159 -0
- package/dist/components/EmojiPicker.svelte.d.ts +8 -0
- package/dist/components/EntityPreviewCard.svelte +104 -0
- package/dist/components/EntityPreviewCard.svelte.d.ts +9 -0
- package/dist/components/FileDropzone.svelte +493 -0
- package/dist/components/FileDropzone.svelte.d.ts +40 -0
- package/dist/components/FilePicker.svelte +488 -0
- package/dist/components/FilePicker.svelte.d.ts +20 -0
- package/dist/components/FileThumbnail.svelte +80 -0
- package/dist/components/FileThumbnail.svelte.d.ts +12 -0
- package/dist/components/FilterDropdown.svelte +203 -0
- package/dist/components/FilterDropdown.svelte.d.ts +48 -0
- package/dist/components/FloatingActionBar.svelte +354 -0
- package/dist/components/FloatingActionBar.svelte.d.ts +51 -0
- package/dist/components/GPSTracker.svelte +202 -0
- package/dist/components/GPSTracker.svelte.d.ts +34 -0
- package/dist/components/HunkDiffView.svelte +348 -0
- package/dist/components/HunkDiffView.svelte.d.ts +9 -0
- package/dist/components/ImageLightbox.svelte +274 -0
- package/dist/components/ImageLightbox.svelte.d.ts +25 -0
- package/dist/components/ImageUpload.svelte +147 -0
- package/dist/components/ImageUpload.svelte.d.ts +10 -0
- package/dist/components/InlineEdit.svelte +291 -0
- package/dist/components/InlineEdit.svelte.d.ts +45 -0
- package/dist/components/InputDialog.svelte +327 -0
- package/dist/components/InputDialog.svelte.d.ts +3 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +296 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +42 -0
- package/dist/components/LazyImage.svelte +123 -0
- package/dist/components/LazyImage.svelte.d.ts +14 -0
- package/dist/components/LinkShortener.svelte +274 -0
- package/dist/components/LinkShortener.svelte.d.ts +27 -0
- package/dist/components/LoadingSpinner.svelte +106 -0
- package/dist/components/LoadingSpinner.svelte.d.ts +21 -0
- package/dist/components/LocationMap.svelte +186 -0
- package/dist/components/LocationMap.svelte.d.ts +11 -0
- package/dist/components/MapView.svelte +341 -0
- package/dist/components/MapView.svelte.d.ts +20 -0
- package/dist/components/MarkupEditor.svelte +485 -0
- package/dist/components/MarkupEditor.svelte.d.ts +10 -0
- package/dist/components/MarkupOverlay.svelte +55 -0
- package/dist/components/MarkupOverlay.svelte.d.ts +8 -0
- package/dist/components/MediaWorkbench.svelte +871 -0
- package/dist/components/MediaWorkbench.svelte.d.ts +9 -0
- package/dist/components/MilestoneCard.svelte +98 -0
- package/dist/components/MilestoneCard.svelte.d.ts +16 -0
- package/dist/components/MilestoneTimeline.svelte +91 -0
- package/dist/components/MilestoneTimeline.svelte.d.ts +15 -0
- package/dist/components/Modal.svelte +244 -0
- package/dist/components/Modal.svelte.d.ts +35 -0
- package/dist/components/PDFViewer.svelte +105 -0
- package/dist/components/PDFViewer.svelte.d.ts +9 -0
- package/dist/components/PdfThumbnail.svelte +63 -0
- package/dist/components/PdfThumbnail.svelte.d.ts +9 -0
- package/dist/components/PhoneInput.svelte +212 -0
- package/dist/components/PhoneInput.svelte.d.ts +25 -0
- package/dist/components/ResizableDivider.svelte +307 -0
- package/dist/components/ResizableDivider.svelte.d.ts +26 -0
- package/dist/components/ResizablePanel.svelte +305 -0
- package/dist/components/ResizablePanel.svelte.d.ts +18 -0
- package/dist/components/SearchDropdown.svelte +421 -0
- package/dist/components/SearchDropdown.svelte.d.ts +24 -0
- package/dist/components/SelectInput.svelte +237 -0
- package/dist/components/SelectInput.svelte.d.ts +26 -0
- package/dist/components/SidebarUserFooter.svelte +34 -0
- package/dist/components/SidebarUserFooter.svelte.d.ts +16 -0
- package/dist/components/SignaturePad.svelte +175 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SmartImageEditor.svelte +720 -0
- package/dist/components/SmartImageEditor.svelte.d.ts +24 -0
- package/dist/components/SortDropdown.svelte +156 -0
- package/dist/components/SortDropdown.svelte.d.ts +40 -0
- package/dist/components/Sparkline.svelte +116 -0
- package/dist/components/Sparkline.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +114 -0
- package/dist/components/SpeechForm.svelte.d.ts +17 -0
- package/dist/components/StatusBadge.svelte +157 -0
- package/dist/components/StatusBadge.svelte.d.ts +24 -0
- package/dist/components/TaskTypeIcon.svelte +54 -0
- package/dist/components/TaskTypeIcon.svelte.d.ts +17 -0
- package/dist/components/TextArea.svelte +162 -0
- package/dist/components/TextArea.svelte.d.ts +30 -0
- package/dist/components/TextInput.svelte +131 -0
- package/dist/components/TextInput.svelte.d.ts +29 -0
- package/dist/components/ThemeSelector.svelte +206 -0
- package/dist/components/ThemeSelector.svelte.d.ts +14 -0
- package/dist/components/TimeSlotPicker.svelte +162 -0
- package/dist/components/TimeSlotPicker.svelte.d.ts +18 -0
- package/dist/components/UserAvatar.svelte +72 -0
- package/dist/components/UserAvatar.svelte.d.ts +11 -0
- package/dist/components/VariablePicker.svelte +170 -0
- package/dist/components/VariablePicker.svelte.d.ts +40 -0
- package/dist/components/VoicePlayer.svelte +422 -0
- package/dist/components/VoicePlayer.svelte.d.ts +18 -0
- package/dist/components/linked-columns/LinkedColumns.svelte +526 -0
- package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +130 -0
- package/dist/components/markup.d.ts +47 -0
- package/dist/components/markup.js +201 -0
- package/dist/components/messaging/Avatar.svelte +81 -0
- package/dist/components/messaging/Avatar.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelInfoModal.svelte +163 -0
- package/dist/components/messaging/ChannelInfoModal.svelte.d.ts +18 -0
- package/dist/components/messaging/ChannelList.svelte +107 -0
- package/dist/components/messaging/ChannelList.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/dist/components/messaging/ChannelMemberAvatarStack.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelMembersModal.svelte +182 -0
- package/dist/components/messaging/ChannelMembersModal.svelte.d.ts +16 -0
- package/dist/components/messaging/CreateChannelModal.svelte +190 -0
- package/dist/components/messaging/CreateChannelModal.svelte.d.ts +14 -0
- package/dist/components/messaging/DirectMessageList.svelte +145 -0
- package/dist/components/messaging/DirectMessageList.svelte.d.ts +12 -0
- package/dist/components/messaging/EmojiSelector.svelte +261 -0
- package/dist/components/messaging/EmojiSelector.svelte.d.ts +19 -0
- package/dist/components/messaging/MentionAutocomplete.svelte +193 -0
- package/dist/components/messaging/MentionAutocomplete.svelte.d.ts +17 -0
- package/dist/components/messaging/MessageAttachment.svelte +273 -0
- package/dist/components/messaging/MessageAttachment.svelte.d.ts +13 -0
- package/dist/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/dist/components/messaging/MessageAttachmentUpload.svelte.d.ts +19 -0
- package/dist/components/messaging/MessageInput.svelte +452 -0
- package/dist/components/messaging/MessageInput.svelte.d.ts +28 -0
- package/dist/components/messaging/MessageItem.svelte +341 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +41 -0
- package/dist/components/messaging/MessageThread.svelte +306 -0
- package/dist/components/messaging/MessageThread.svelte.d.ts +30 -0
- package/dist/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/dist/components/messaging/NotificationSettingsModal.svelte.d.ts +13 -0
- package/dist/components/messaging/QuotedMessageDisplay.svelte +123 -0
- package/dist/components/messaging/QuotedMessageDisplay.svelte.d.ts +10 -0
- package/dist/components/messaging/StartDMModal.svelte +100 -0
- package/dist/components/messaging/StartDMModal.svelte.d.ts +14 -0
- package/dist/components/messaging/ThreadPanel.svelte +153 -0
- package/dist/components/messaging/ThreadPanel.svelte.d.ts +16 -0
- package/dist/components/pipeline/DESIGN.md +361 -0
- package/dist/components/pipeline/Pipeline.svelte +391 -0
- package/dist/components/pipeline/Pipeline.svelte.d.ts +28 -0
- package/dist/components/pipeline/PipelineCard.svelte +94 -0
- package/dist/components/pipeline/PipelineCard.svelte.d.ts +13 -0
- package/dist/components/pipeline/PipelineColumn.svelte +163 -0
- package/dist/components/pipeline/PipelineColumn.svelte.d.ts +30 -0
- package/dist/components/replay/ChapterTimeline.svelte +326 -0
- package/dist/components/replay/ChapterTimeline.svelte.d.ts +39 -0
- package/dist/components/session-nav/transcriptModel.d.ts +132 -0
- package/dist/components/session-nav/transcriptModel.js +285 -0
- package/dist/data/emojis.d.ts +18 -0
- package/dist/data/emojis.js +1196 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +142 -0
- package/dist/stores/confirmDialog.svelte.d.ts +21 -0
- package/dist/stores/confirmDialog.svelte.js +31 -0
- package/dist/stores/inputDialog.svelte.d.ts +24 -0
- package/dist/stores/inputDialog.svelte.js +32 -0
- package/dist/styles/rail.css +63 -0
- package/dist/types/annotation.d.ts +32 -0
- package/dist/types/annotation.js +3 -0
- package/dist/types/booking.d.ts +115 -0
- package/dist/types/booking.js +24 -0
- package/dist/types/comments.d.ts +65 -0
- package/dist/types/comments.js +22 -0
- package/dist/types/entityPreview.d.ts +42 -0
- package/dist/types/entityPreview.js +10 -0
- package/dist/types/filePicker.d.ts +15 -0
- package/dist/types/filePicker.js +1 -0
- package/dist/types/googleMaps.d.ts +51 -0
- package/dist/types/maps.d.ts +43 -0
- package/dist/types/maps.js +4 -0
- package/dist/types/messaging.d.ts +282 -0
- package/dist/types/messaging.js +100 -0
- package/dist/types/milestone.d.ts +20 -0
- package/dist/types/milestone.js +7 -0
- package/dist/types/pipeline.d.ts +115 -0
- package/dist/types/pipeline.js +15 -0
- package/dist/types/smartImageEditor.d.ts +42 -0
- package/dist/types/smartImageEditor.js +1 -0
- package/dist/types/templateVars.d.ts +34 -0
- package/dist/types/templateVars.js +12 -0
- package/dist/utils/currency.d.ts +8 -0
- package/dist/utils/currency.js +17 -0
- package/dist/utils/daisyuiColors.d.ts +37 -0
- package/dist/utils/daisyuiColors.js +152 -0
- package/dist/utils/dateFormatters.d.ts +69 -0
- package/dist/utils/dateFormatters.js +199 -0
- package/dist/utils/googleMapsLoader.d.ts +19 -0
- package/dist/utils/googleMapsLoader.js +121 -0
- package/dist/utils/mentionParser.d.ts +67 -0
- package/dist/utils/mentionParser.js +132 -0
- package/dist/utils/phone.d.ts +13 -0
- package/dist/utils/phone.js +67 -0
- package/dist/utils/phoneFormat.d.ts +26 -0
- package/dist/utils/phoneFormat.js +69 -0
- package/dist/utils/taskUtils.d.ts +37 -0
- package/dist/utils/taskUtils.js +71 -0
- package/package.json +3 -2
|
@@ -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-[0.8125rem] text-base-content/45 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-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
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-[0.9375rem] 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-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
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-[0.9375rem] 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-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
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-[0.9375rem] 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>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeSlotPicker — Display available time slots grouped by morning/afternoon/evening.
|
|
3
|
+
*
|
|
4
|
+
* Accepts an array of TimeSlot objects with `start` (Date), `end` (Date), and `available` (boolean).
|
|
5
|
+
* Grouped automatically by hour: morning (<12), afternoon (<17), evening (17+).
|
|
6
|
+
*/
|
|
7
|
+
import type { TimeSlot } from '../types/booking';
|
|
8
|
+
type $$ComponentProps = {
|
|
9
|
+
slots: TimeSlot[];
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
onSelect: (slot: TimeSlot) => void;
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const TimeSlotPicker: import("svelte").Component<$$ComponentProps, {
|
|
15
|
+
TimeSlot: typeof TimeSlot;
|
|
16
|
+
}, "">;
|
|
17
|
+
type TimeSlotPicker = ReturnType<typeof TimeSlotPicker>;
|
|
18
|
+
export default TimeSlotPicker;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
UserAvatar — shows a user's avatar image or initials fallback.
|
|
3
|
+
Generic component with no DB type coupling.
|
|
4
|
+
For use anywhere a user needs to be visually represented.
|
|
5
|
+
-->
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
interface Props {
|
|
8
|
+
name?: string | null
|
|
9
|
+
email?: string | null
|
|
10
|
+
avatarUrl?: string | null
|
|
11
|
+
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
12
|
+
tooltip?: boolean
|
|
13
|
+
class?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let {
|
|
17
|
+
name = null,
|
|
18
|
+
email = null,
|
|
19
|
+
avatarUrl = null,
|
|
20
|
+
size = 'sm',
|
|
21
|
+
tooltip = true,
|
|
22
|
+
class: className = '',
|
|
23
|
+
}: Props = $props()
|
|
24
|
+
|
|
25
|
+
const sizeClasses: Record<string, string> = {
|
|
26
|
+
xs: 'w-5 h-5 text-[9px]',
|
|
27
|
+
sm: 'w-7 h-7 text-xs',
|
|
28
|
+
md: 'w-9 h-9 text-sm',
|
|
29
|
+
lg: 'w-12 h-12 text-base',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const initials = $derived.by(() => {
|
|
33
|
+
const source = name || email || ''
|
|
34
|
+
if (!source) return '?'
|
|
35
|
+
// For email, use first char before @
|
|
36
|
+
const parts = source.includes('@')
|
|
37
|
+
? [source.split('@')[0]]
|
|
38
|
+
: source.split(' ').filter(Boolean)
|
|
39
|
+
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
40
|
+
return parts[0]?.charAt(0).toUpperCase() || '?'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const label = $derived(name || email || 'Unknown')
|
|
44
|
+
|
|
45
|
+
// Derive a consistent color per user from their name/email
|
|
46
|
+
const avatarBg = $derived.by(() => {
|
|
47
|
+
const source = name || email || ''
|
|
48
|
+
if (!source) return 'oklch(0.65 0.10 240)'
|
|
49
|
+
let hash = 0
|
|
50
|
+
for (let i = 0; i < source.length; i++) {
|
|
51
|
+
hash = source.charCodeAt(i) + ((hash << 5) - hash)
|
|
52
|
+
}
|
|
53
|
+
const hues = [30, 85, 145, 200, 240, 270, 310, 350]
|
|
54
|
+
const hue = hues[Math.abs(hash) % hues.length]
|
|
55
|
+
return `oklch(0.62 0.14 ${hue})`
|
|
56
|
+
})
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<div
|
|
60
|
+
class="inline-flex shrink-0 {className}"
|
|
61
|
+
title={tooltip ? label : undefined}
|
|
62
|
+
>
|
|
63
|
+
<div class="rounded-full overflow-hidden {sizeClasses[size]}">
|
|
64
|
+
{#if avatarUrl}
|
|
65
|
+
<img src={avatarUrl} alt={label} class="w-full h-full object-cover" />
|
|
66
|
+
{:else}
|
|
67
|
+
<div class="w-full h-full flex items-center justify-center font-semibold text-white" style="background: {avatarBg}">
|
|
68
|
+
{initials}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
name?: string | null;
|
|
3
|
+
email?: string | null;
|
|
4
|
+
avatarUrl?: string | null;
|
|
5
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
6
|
+
tooltip?: boolean;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const UserAvatar: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type UserAvatar = ReturnType<typeof UserAvatar>;
|
|
11
|
+
export default UserAvatar;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* VariablePicker — browse and insert template merge variables.
|
|
4
|
+
*
|
|
5
|
+
* Renders the available merge variables for a document template, grouped by
|
|
6
|
+
* context (client / org / financial / custom). Clicking a variable inserts
|
|
7
|
+
* its `{{name}}` token at the caret of a bound textarea/input and/or fires
|
|
8
|
+
* an `onInsert` callback.
|
|
9
|
+
*
|
|
10
|
+
* Source of `groups`: the JST server-side registry
|
|
11
|
+
* `getVarsForCategory(category, template.available_vars)` in
|
|
12
|
+
* `src/lib/server/template-vars.ts`. Compute it in a `+page.server.ts` load
|
|
13
|
+
* (the registry is server-only) and pass the result straight in.
|
|
14
|
+
*
|
|
15
|
+
* This is a self-contained panel (not a modal) so it can sit beside a
|
|
16
|
+
* template editor as a sidebar. Wrap it in your own Drawer/Modal if needed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TemplateVar, TemplateVarGroup } from "../types/templateVars"
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
/** Variable groups to show. From `getVarsForCategory(...)`. */
|
|
23
|
+
groups: TemplateVarGroup[]
|
|
24
|
+
/**
|
|
25
|
+
* Optional textarea/input to insert into. When set, clicking a variable
|
|
26
|
+
* splices `{{name}}` at the current caret, restores focus + caret after
|
|
27
|
+
* the token, and dispatches an `input` event so `bind:value` updates.
|
|
28
|
+
*/
|
|
29
|
+
targetEl?: HTMLTextAreaElement | HTMLInputElement | null
|
|
30
|
+
/** Called with the inserted token (`{{name}}`) and the bare var name. */
|
|
31
|
+
onInsert?: (token: string, varName: string) => void
|
|
32
|
+
/** Show the filter box. Default true. */
|
|
33
|
+
searchable?: boolean
|
|
34
|
+
/** Denser layout for narrow sidebars. Default false. */
|
|
35
|
+
compact?: boolean
|
|
36
|
+
/** Panel heading. Default "Variables". */
|
|
37
|
+
title?: string
|
|
38
|
+
/** Shown when there are no groups/vars at all. */
|
|
39
|
+
emptyText?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let {
|
|
43
|
+
groups,
|
|
44
|
+
targetEl = null,
|
|
45
|
+
onInsert,
|
|
46
|
+
searchable = true,
|
|
47
|
+
compact = false,
|
|
48
|
+
title = "Variables",
|
|
49
|
+
emptyText = "No variables available for this template.",
|
|
50
|
+
}: Props = $props()
|
|
51
|
+
|
|
52
|
+
let query = $state("")
|
|
53
|
+
|
|
54
|
+
const totalVars = $derived(
|
|
55
|
+
groups.reduce((n, g) => n + g.vars.length, 0),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Filter vars by name/label/description, dropping groups left empty.
|
|
59
|
+
const filteredGroups = $derived.by<TemplateVarGroup[]>(() => {
|
|
60
|
+
const q = query.trim().toLowerCase()
|
|
61
|
+
if (!q) return groups
|
|
62
|
+
const out: TemplateVarGroup[] = []
|
|
63
|
+
for (const g of groups) {
|
|
64
|
+
const vars = g.vars.filter(
|
|
65
|
+
(v) =>
|
|
66
|
+
v.name.toLowerCase().includes(q) ||
|
|
67
|
+
v.label.toLowerCase().includes(q) ||
|
|
68
|
+
(v.description?.toLowerCase().includes(q) ?? false),
|
|
69
|
+
)
|
|
70
|
+
if (vars.length > 0) out.push({ ...g, vars })
|
|
71
|
+
}
|
|
72
|
+
return out
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
function tokenFor(v: TemplateVar): string {
|
|
76
|
+
return `{{${v.name}}}`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function insert(v: TemplateVar) {
|
|
80
|
+
const token = tokenFor(v)
|
|
81
|
+
|
|
82
|
+
if (targetEl) {
|
|
83
|
+
const el = targetEl
|
|
84
|
+
const start = el.selectionStart ?? el.value.length
|
|
85
|
+
const end = el.selectionEnd ?? el.value.length
|
|
86
|
+
el.value = el.value.slice(0, start) + token + el.value.slice(end)
|
|
87
|
+
const caret = start + token.length
|
|
88
|
+
// Restore focus + caret after the inserted token.
|
|
89
|
+
el.focus()
|
|
90
|
+
el.setSelectionRange(caret, caret)
|
|
91
|
+
// Notify Svelte bindings / listeners.
|
|
92
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onInsert?.(token, v.name)
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<div
|
|
100
|
+
class="border-base-300 bg-base-100 flex flex-col rounded-box border"
|
|
101
|
+
class:text-sm={compact}
|
|
102
|
+
>
|
|
103
|
+
<div class="border-base-300 flex items-center justify-between border-b px-4 py-3">
|
|
104
|
+
<h3 class="font-semibold">{title}</h3>
|
|
105
|
+
{#if totalVars > 0}
|
|
106
|
+
<span class="badge badge-ghost badge-sm">{totalVars}</span>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{#if searchable && totalVars > 0}
|
|
111
|
+
<div class="border-base-300 border-b p-3">
|
|
112
|
+
<input
|
|
113
|
+
type="text"
|
|
114
|
+
class="input input-bordered input-sm w-full"
|
|
115
|
+
placeholder="Filter variables…"
|
|
116
|
+
aria-label="Filter variables"
|
|
117
|
+
bind:value={query}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
{/if}
|
|
121
|
+
|
|
122
|
+
<div class="flex-1 overflow-y-auto p-2" class:max-h-96={!compact}>
|
|
123
|
+
{#if totalVars === 0}
|
|
124
|
+
<p class="text-base-content/60 px-2 py-6 text-center text-sm">
|
|
125
|
+
{emptyText}
|
|
126
|
+
</p>
|
|
127
|
+
{:else if filteredGroups.length === 0}
|
|
128
|
+
<p class="text-base-content/60 px-2 py-6 text-center text-sm">
|
|
129
|
+
No variables match "{query}".
|
|
130
|
+
</p>
|
|
131
|
+
{:else}
|
|
132
|
+
{#each filteredGroups as group (group.id)}
|
|
133
|
+
<div class="mb-3 last:mb-0">
|
|
134
|
+
<div
|
|
135
|
+
class="text-base-content/45 px-2 pb-1 text-[0.8125rem] tracking-[0.005em]"
|
|
136
|
+
>
|
|
137
|
+
{group.label}
|
|
138
|
+
</div>
|
|
139
|
+
<div class="flex flex-col gap-0.5">
|
|
140
|
+
{#each group.vars as v (v.name)}
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
class="hover:bg-base-200 focus-visible:bg-base-200 group flex w-full flex-col items-start gap-0.5 rounded-md px-2 py-1.5 text-left transition-colors"
|
|
144
|
+
onclick={() => insert(v)}
|
|
145
|
+
title={`Insert ${tokenFor(v)}`}
|
|
146
|
+
>
|
|
147
|
+
<div class="flex w-full items-baseline gap-2">
|
|
148
|
+
<span class="font-medium flex-1 min-w-0 truncate">{v.label}</span>
|
|
149
|
+
<code
|
|
150
|
+
class="text-base-content/60 group-hover:text-primary shrink-0 font-mono text-xs"
|
|
151
|
+
>
|
|
152
|
+
{tokenFor(v)}
|
|
153
|
+
</code>
|
|
154
|
+
</div>
|
|
155
|
+
{#if v.description && !compact}
|
|
156
|
+
<span class="text-base-content/45 text-[0.75rem]">
|
|
157
|
+
{v.description}{#if v.example}
|
|
158
|
+
<span class="text-base-content/45">
|
|
159
|
+
— e.g. {v.example}</span
|
|
160
|
+
>{/if}
|
|
161
|
+
</span>
|
|
162
|
+
{/if}
|
|
163
|
+
</button>
|
|
164
|
+
{/each}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
{/each}
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VariablePicker — browse and insert template merge variables.
|
|
3
|
+
*
|
|
4
|
+
* Renders the available merge variables for a document template, grouped by
|
|
5
|
+
* context (client / org / financial / custom). Clicking a variable inserts
|
|
6
|
+
* its `{{name}}` token at the caret of a bound textarea/input and/or fires
|
|
7
|
+
* an `onInsert` callback.
|
|
8
|
+
*
|
|
9
|
+
* Source of `groups`: the JST server-side registry
|
|
10
|
+
* `getVarsForCategory(category, template.available_vars)` in
|
|
11
|
+
* `src/lib/server/template-vars.ts`. Compute it in a `+page.server.ts` load
|
|
12
|
+
* (the registry is server-only) and pass the result straight in.
|
|
13
|
+
*
|
|
14
|
+
* This is a self-contained panel (not a modal) so it can sit beside a
|
|
15
|
+
* template editor as a sidebar. Wrap it in your own Drawer/Modal if needed.
|
|
16
|
+
*/
|
|
17
|
+
import type { TemplateVarGroup } from "../types/templateVars";
|
|
18
|
+
interface Props {
|
|
19
|
+
/** Variable groups to show. From `getVarsForCategory(...)`. */
|
|
20
|
+
groups: TemplateVarGroup[];
|
|
21
|
+
/**
|
|
22
|
+
* Optional textarea/input to insert into. When set, clicking a variable
|
|
23
|
+
* splices `{{name}}` at the current caret, restores focus + caret after
|
|
24
|
+
* the token, and dispatches an `input` event so `bind:value` updates.
|
|
25
|
+
*/
|
|
26
|
+
targetEl?: HTMLTextAreaElement | HTMLInputElement | null;
|
|
27
|
+
/** Called with the inserted token (`{{name}}`) and the bare var name. */
|
|
28
|
+
onInsert?: (token: string, varName: string) => void;
|
|
29
|
+
/** Show the filter box. Default true. */
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
/** Denser layout for narrow sidebars. Default false. */
|
|
32
|
+
compact?: boolean;
|
|
33
|
+
/** Panel heading. Default "Variables". */
|
|
34
|
+
title?: string;
|
|
35
|
+
/** Shown when there are no groups/vars at all. */
|
|
36
|
+
emptyText?: string;
|
|
37
|
+
}
|
|
38
|
+
declare const VariablePicker: import("svelte").Component<Props, {}, "">;
|
|
39
|
+
type VariablePicker = ReturnType<typeof VariablePicker>;
|
|
40
|
+
export default VariablePicker;
|