@joewinke/jatui 0.1.23 → 0.1.25
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/CLAUDE.md +244 -0
- package/package.json +23 -10
- package/src/lib/actions/collapse.ts +0 -136
- package/src/lib/actions/railNav.ts +0 -473
- package/src/lib/components/AnnotationLayer.svelte +0 -108
- package/src/lib/components/AnnotationPanel.svelte +0 -319
- package/src/lib/components/AudioWaveform.svelte +0 -698
- package/src/lib/components/AvailabilityModal.svelte +0 -177
- package/src/lib/components/AvatarUpload.svelte +0 -234
- package/src/lib/components/Badge.svelte +0 -38
- package/src/lib/components/BookingForm.svelte +0 -278
- package/src/lib/components/BurndownChart.svelte +0 -778
- package/src/lib/components/Button.svelte +0 -81
- package/src/lib/components/CalendarPicker.svelte +0 -284
- package/src/lib/components/Card.svelte +0 -67
- package/src/lib/components/CharacterCounter.svelte +0 -82
- package/src/lib/components/ChipInput.svelte +0 -609
- package/src/lib/components/Collapse.svelte +0 -82
- package/src/lib/components/ColorSelector.svelte +0 -167
- package/src/lib/components/CommentThread.svelte +0 -773
- package/src/lib/components/ConfirmDialog.svelte +0 -348
- package/src/lib/components/ConfirmModal.svelte +0 -142
- package/src/lib/components/ContextMenu.svelte +0 -188
- package/src/lib/components/CountdownTimer.svelte +0 -94
- package/src/lib/components/DateRangePicker.svelte +0 -194
- package/src/lib/components/Drawer.svelte +0 -143
- package/src/lib/components/EmojiPicker.svelte +0 -159
- package/src/lib/components/EntityPreviewCard.svelte +0 -104
- package/src/lib/components/FileDropzone.svelte +0 -493
- package/src/lib/components/FilePicker.svelte +0 -488
- package/src/lib/components/FileThumbnail.svelte +0 -80
- package/src/lib/components/FilterDropdown.svelte +0 -203
- package/src/lib/components/FloatingActionBar.svelte +0 -354
- package/src/lib/components/GPSTracker.svelte +0 -202
- package/src/lib/components/HunkDiffView.svelte +0 -348
- package/src/lib/components/ImageLightbox.svelte +0 -274
- package/src/lib/components/ImageUpload.svelte +0 -147
- package/src/lib/components/InlineEdit.svelte +0 -291
- package/src/lib/components/InputDialog.svelte +0 -327
- package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
- package/src/lib/components/LazyImage.svelte +0 -123
- package/src/lib/components/LinkShortener.svelte +0 -274
- package/src/lib/components/LoadingSpinner.svelte +0 -106
- package/src/lib/components/LocationMap.svelte +0 -186
- package/src/lib/components/MapView.svelte +0 -341
- package/src/lib/components/MarkupEditor.svelte +0 -485
- package/src/lib/components/MarkupOverlay.svelte +0 -55
- package/src/lib/components/MediaWorkbench.svelte +0 -871
- package/src/lib/components/MilestoneCard.svelte +0 -98
- package/src/lib/components/MilestoneTimeline.svelte +0 -91
- package/src/lib/components/Modal.svelte +0 -244
- package/src/lib/components/PDFViewer.svelte +0 -105
- package/src/lib/components/PdfThumbnail.svelte +0 -63
- package/src/lib/components/PhoneInput.svelte +0 -212
- package/src/lib/components/ResizableDivider.svelte +0 -307
- package/src/lib/components/ResizablePanel.svelte +0 -305
- package/src/lib/components/SearchDropdown.svelte +0 -421
- package/src/lib/components/SelectInput.svelte +0 -237
- package/src/lib/components/SidebarUserFooter.svelte +0 -34
- package/src/lib/components/SignaturePad.svelte +0 -175
- package/src/lib/components/SmartImageEditor.svelte +0 -720
- package/src/lib/components/SortDropdown.svelte +0 -156
- package/src/lib/components/Sparkline.svelte +0 -116
- package/src/lib/components/SpeechForm.svelte +0 -114
- package/src/lib/components/StatusBadge.svelte +0 -157
- package/src/lib/components/TaskTypeIcon.svelte +0 -54
- package/src/lib/components/TextArea.svelte +0 -162
- package/src/lib/components/TextInput.svelte +0 -131
- package/src/lib/components/ThemeSelector.svelte +0 -206
- package/src/lib/components/TimeSlotPicker.svelte +0 -162
- package/src/lib/components/UserAvatar.svelte +0 -72
- package/src/lib/components/VariablePicker.svelte +0 -170
- package/src/lib/components/VoicePlayer.svelte +0 -422
- package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
- package/src/lib/components/markup.ts +0 -287
- package/src/lib/components/messaging/Avatar.svelte +0 -81
- package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
- package/src/lib/components/messaging/ChannelList.svelte +0 -107
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
- package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
- package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
- package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
- package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
- package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
- package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
- package/src/lib/components/messaging/MessageInput.svelte +0 -452
- package/src/lib/components/messaging/MessageItem.svelte +0 -341
- package/src/lib/components/messaging/MessageThread.svelte +0 -306
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
- package/src/lib/components/messaging/StartDMModal.svelte +0 -100
- package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
- package/src/lib/components/pipeline/DESIGN.md +0 -361
- package/src/lib/components/pipeline/Pipeline.svelte +0 -391
- package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
- package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
- package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
- package/src/lib/components/session-nav/transcriptModel.ts +0 -352
- package/src/lib/data/emojis.ts +0 -1208
- package/src/lib/index.ts +0 -405
- package/src/lib/stores/confirmDialog.svelte.ts +0 -48
- package/src/lib/stores/inputDialog.svelte.ts +0 -51
- package/src/lib/styles/rail.css +0 -63
- package/src/lib/types/annotation.ts +0 -38
- package/src/lib/types/booking.ts +0 -143
- package/src/lib/types/comments.ts +0 -97
- package/src/lib/types/entityPreview.ts +0 -45
- package/src/lib/types/filePicker.ts +0 -16
- package/src/lib/types/googleMaps.d.ts +0 -51
- package/src/lib/types/maps.ts +0 -43
- package/src/lib/types/messaging.ts +0 -459
- package/src/lib/types/milestone.ts +0 -22
- package/src/lib/types/pipeline.ts +0 -150
- package/src/lib/types/smartImageEditor.ts +0 -39
- package/src/lib/types/templateVars.ts +0 -36
- package/src/lib/utils/currency.ts +0 -20
- package/src/lib/utils/daisyuiColors.ts +0 -243
- package/src/lib/utils/dateFormatters.ts +0 -196
- package/src/lib/utils/googleMapsLoader.ts +0 -84
- package/src/lib/utils/mentionParser.ts +0 -188
- package/src/lib/utils/phone.ts +0 -80
- package/src/lib/utils/phoneFormat.ts +0 -74
- package/src/lib/utils/taskUtils.ts +0 -75
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* TextArea Component
|
|
4
|
-
*
|
|
5
|
-
* Multi-line text input with validation, auto-resize, and character count.
|
|
6
|
-
* Converted from Svelte 4 to Svelte 5 runes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
label?: string;
|
|
11
|
-
value?: string;
|
|
12
|
-
placeholder?: string;
|
|
13
|
-
rows?: number;
|
|
14
|
-
maxlength?: number;
|
|
15
|
-
minlength?: number;
|
|
16
|
-
required?: boolean;
|
|
17
|
-
disabled?: boolean;
|
|
18
|
-
readonly?: boolean;
|
|
19
|
-
autoResize?: boolean;
|
|
20
|
-
error?: string;
|
|
21
|
-
helpText?: string;
|
|
22
|
-
showCount?: boolean;
|
|
23
|
-
size?: 'sm' | 'md' | 'lg';
|
|
24
|
-
class?: string;
|
|
25
|
-
id?: string;
|
|
26
|
-
name?: string;
|
|
27
|
-
resize?: 'none' | 'vertical' | 'horizontal' | 'both';
|
|
28
|
-
oninput?: (value: string) => void;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let {
|
|
32
|
-
label = '',
|
|
33
|
-
value = $bindable(''),
|
|
34
|
-
placeholder = '',
|
|
35
|
-
rows = 3,
|
|
36
|
-
maxlength,
|
|
37
|
-
minlength,
|
|
38
|
-
required = false,
|
|
39
|
-
disabled = false,
|
|
40
|
-
readonly = false,
|
|
41
|
-
autoResize = false,
|
|
42
|
-
error = '',
|
|
43
|
-
helpText = '',
|
|
44
|
-
showCount = false,
|
|
45
|
-
size = 'md',
|
|
46
|
-
class: className = '',
|
|
47
|
-
id = `textarea-${Math.random().toString(36).substr(2, 9)}`,
|
|
48
|
-
name = '',
|
|
49
|
-
resize = 'vertical',
|
|
50
|
-
oninput
|
|
51
|
-
}: Props = $props();
|
|
52
|
-
|
|
53
|
-
let textareaElement: HTMLTextAreaElement | undefined = $state();
|
|
54
|
-
|
|
55
|
-
const sizeClass = $derived({ sm: 'textarea-sm', md: '', lg: 'textarea-lg' }[size]);
|
|
56
|
-
const resizeClass = $derived(
|
|
57
|
-
{ none: 'resize-none', vertical: 'resize-y', horizontal: 'resize-x', both: 'resize' }[resize]
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
const textareaClass = $derived(
|
|
61
|
-
[
|
|
62
|
-
'textarea textarea-bordered w-full transition-shadow duration-200',
|
|
63
|
-
sizeClass,
|
|
64
|
-
resizeClass,
|
|
65
|
-
error ? 'textarea-error' : '',
|
|
66
|
-
disabled ? 'textarea-disabled' : '',
|
|
67
|
-
className
|
|
68
|
-
]
|
|
69
|
-
.filter(Boolean)
|
|
70
|
-
.join(' ')
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const characterCount = $derived(value?.length || 0);
|
|
74
|
-
const isOverLimit = $derived(maxlength ? characterCount > maxlength : false);
|
|
75
|
-
|
|
76
|
-
function handleInput(event: Event) {
|
|
77
|
-
const target = event.target as HTMLTextAreaElement;
|
|
78
|
-
value = target.value;
|
|
79
|
-
if (autoResize) autoResizeTextarea(target);
|
|
80
|
-
oninput?.(value);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function autoResizeTextarea(textarea: HTMLTextAreaElement) {
|
|
84
|
-
textarea.style.height = 'auto';
|
|
85
|
-
textarea.style.height = textarea.scrollHeight + 'px';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
$effect(() => {
|
|
89
|
-
if (autoResize && textareaElement && value !== undefined) {
|
|
90
|
-
autoResizeTextarea(textareaElement);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Error-state shake (transitions.dev). Replays `t-input-shake` when `error`
|
|
95
|
-
// becomes set. See TextInput for the full rationale.
|
|
96
|
-
let hadError = false;
|
|
97
|
-
$effect(() => {
|
|
98
|
-
const hasError = !!error;
|
|
99
|
-
if (hasError && !hadError && textareaElement) {
|
|
100
|
-
const reduce =
|
|
101
|
-
typeof window !== 'undefined' &&
|
|
102
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
103
|
-
if (!reduce) {
|
|
104
|
-
textareaElement.classList.remove('t-input-shake');
|
|
105
|
-
void textareaElement.offsetWidth;
|
|
106
|
-
textareaElement.classList.add('t-input-shake');
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
hadError = hasError;
|
|
110
|
-
});
|
|
111
|
-
</script>
|
|
112
|
-
|
|
113
|
-
<div class="form-control w-full">
|
|
114
|
-
{#if label}
|
|
115
|
-
<label for={id} class="label">
|
|
116
|
-
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
117
|
-
{label}
|
|
118
|
-
{#if required}
|
|
119
|
-
<span class="text-error ml-1">*</span>
|
|
120
|
-
{/if}
|
|
121
|
-
</span>
|
|
122
|
-
</label>
|
|
123
|
-
{/if}
|
|
124
|
-
|
|
125
|
-
<textarea
|
|
126
|
-
bind:this={textareaElement}
|
|
127
|
-
{id}
|
|
128
|
-
{name}
|
|
129
|
-
{placeholder}
|
|
130
|
-
{rows}
|
|
131
|
-
{maxlength}
|
|
132
|
-
{minlength}
|
|
133
|
-
{required}
|
|
134
|
-
{disabled}
|
|
135
|
-
{readonly}
|
|
136
|
-
bind:value
|
|
137
|
-
oninput={handleInput}
|
|
138
|
-
class={textareaClass}
|
|
139
|
-
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
140
|
-
aria-invalid={error ? 'true' : 'false'}
|
|
141
|
-
style={autoResize ? `min-height: calc(1.5em * ${rows} + 1rem);` : ''}
|
|
142
|
-
onanimationend={() => textareaElement?.classList.remove('t-input-shake')}
|
|
143
|
-
></textarea>
|
|
144
|
-
|
|
145
|
-
{#if showCount && maxlength}
|
|
146
|
-
<div class="label">
|
|
147
|
-
<span class="text-[0.8125rem] {isOverLimit ? 'text-error' : 'text-base-content/45'}">
|
|
148
|
-
{characterCount}/{maxlength}
|
|
149
|
-
</span>
|
|
150
|
-
</div>
|
|
151
|
-
{/if}
|
|
152
|
-
|
|
153
|
-
{#if error}
|
|
154
|
-
<div class="label">
|
|
155
|
-
<span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
|
|
156
|
-
</div>
|
|
157
|
-
{:else if helpText}
|
|
158
|
-
<div class="label">
|
|
159
|
-
<span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
|
|
160
|
-
</div>
|
|
161
|
-
{/if}
|
|
162
|
-
</div>
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* TextInput Component
|
|
4
|
-
*
|
|
5
|
-
* Labeled text input with validation, error state, help text, and character count.
|
|
6
|
-
* Converted from Svelte 4 to Svelte 5 runes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
label: string;
|
|
11
|
-
value?: string;
|
|
12
|
-
placeholder?: string;
|
|
13
|
-
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
14
|
-
required?: boolean;
|
|
15
|
-
disabled?: boolean;
|
|
16
|
-
readonly?: boolean;
|
|
17
|
-
error?: string;
|
|
18
|
-
helpText?: string;
|
|
19
|
-
size?: 'sm' | 'md' | 'lg';
|
|
20
|
-
maxlength?: number;
|
|
21
|
-
minlength?: number;
|
|
22
|
-
pattern?: string;
|
|
23
|
-
showCount?: boolean;
|
|
24
|
-
class?: string;
|
|
25
|
-
id?: string;
|
|
26
|
-
name?: string;
|
|
27
|
-
autocomplete?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
let {
|
|
31
|
-
label,
|
|
32
|
-
value = $bindable(''),
|
|
33
|
-
placeholder = '',
|
|
34
|
-
type = 'text',
|
|
35
|
-
required = false,
|
|
36
|
-
disabled = false,
|
|
37
|
-
readonly = false,
|
|
38
|
-
error = '',
|
|
39
|
-
helpText = '',
|
|
40
|
-
size = 'md',
|
|
41
|
-
maxlength,
|
|
42
|
-
minlength,
|
|
43
|
-
pattern,
|
|
44
|
-
showCount = false,
|
|
45
|
-
class: className = '',
|
|
46
|
-
id = `text-input-${Math.random().toString(36).substr(2, 9)}`,
|
|
47
|
-
name = '',
|
|
48
|
-
autocomplete = ''
|
|
49
|
-
}: Props = $props();
|
|
50
|
-
|
|
51
|
-
const sizeClass = $derived({ sm: 'input-sm', md: '', lg: 'input-lg' }[size]);
|
|
52
|
-
|
|
53
|
-
const inputClass = $derived(
|
|
54
|
-
['input input-bordered w-full transition-shadow duration-200', sizeClass, error ? 'input-error' : '', disabled ? 'input-disabled' : '', className]
|
|
55
|
-
.filter(Boolean)
|
|
56
|
-
.join(' ')
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const characterCount = $derived(value?.length || 0);
|
|
60
|
-
const isOverLimit = $derived(maxlength ? characterCount > maxlength : false);
|
|
61
|
-
|
|
62
|
-
// Error-state shake (transitions.dev "Error state shake"). When `error`
|
|
63
|
-
// transitions from empty to set, replay the `t-input-shake` keyframe from the
|
|
64
|
-
// host app's transitions.css. Self-contained reflow-restart so a repeat error
|
|
65
|
-
// re-shakes. Colour stays on the `input-error` class; this owns only motion.
|
|
66
|
-
let inputEl: HTMLInputElement | undefined = $state();
|
|
67
|
-
let hadError = false;
|
|
68
|
-
$effect(() => {
|
|
69
|
-
const hasError = !!error;
|
|
70
|
-
if (hasError && !hadError && inputEl) {
|
|
71
|
-
const reduce =
|
|
72
|
-
typeof window !== 'undefined' &&
|
|
73
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
74
|
-
if (!reduce) {
|
|
75
|
-
inputEl.classList.remove('t-input-shake');
|
|
76
|
-
void inputEl.offsetWidth; // force reflow to restart the animation
|
|
77
|
-
inputEl.classList.add('t-input-shake');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
hadError = hasError;
|
|
81
|
-
});
|
|
82
|
-
</script>
|
|
83
|
-
|
|
84
|
-
<div class="form-control w-full">
|
|
85
|
-
<label for={id} class="label">
|
|
86
|
-
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
87
|
-
{label}
|
|
88
|
-
{#if required}
|
|
89
|
-
<span class="text-error ml-1">*</span>
|
|
90
|
-
{/if}
|
|
91
|
-
</span>
|
|
92
|
-
</label>
|
|
93
|
-
|
|
94
|
-
<input
|
|
95
|
-
bind:this={inputEl}
|
|
96
|
-
{id}
|
|
97
|
-
{name}
|
|
98
|
-
{type}
|
|
99
|
-
{placeholder}
|
|
100
|
-
{required}
|
|
101
|
-
{disabled}
|
|
102
|
-
{readonly}
|
|
103
|
-
{maxlength}
|
|
104
|
-
{minlength}
|
|
105
|
-
{pattern}
|
|
106
|
-
autocomplete={autocomplete as AutoFill}
|
|
107
|
-
bind:value
|
|
108
|
-
class={inputClass}
|
|
109
|
-
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
110
|
-
aria-invalid={error ? 'true' : 'false'}
|
|
111
|
-
onanimationend={() => inputEl?.classList.remove('t-input-shake')}
|
|
112
|
-
/>
|
|
113
|
-
|
|
114
|
-
{#if showCount && maxlength}
|
|
115
|
-
<div class="label">
|
|
116
|
-
<span class="text-[0.8125rem] {isOverLimit ? 'text-error' : 'text-base-content/45'}">
|
|
117
|
-
{characterCount}/{maxlength}
|
|
118
|
-
</span>
|
|
119
|
-
</div>
|
|
120
|
-
{/if}
|
|
121
|
-
|
|
122
|
-
{#if error}
|
|
123
|
-
<div class="label">
|
|
124
|
-
<span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
|
|
125
|
-
</div>
|
|
126
|
-
{:else if helpText}
|
|
127
|
-
<div class="label">
|
|
128
|
-
<span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
|
|
129
|
-
</div>
|
|
130
|
-
{/if}
|
|
131
|
-
</div>
|
|
@@ -1,206 +0,0 @@
|
|
|
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
|
-
// svelte-ignore state_referenced_locally
|
|
65
|
-
let currentTheme = $state(defaultTheme);
|
|
66
|
-
let isAnimating = $state(false);
|
|
67
|
-
|
|
68
|
-
const currentThemeLabel = $derived(
|
|
69
|
-
themes.find((t) => t.name === currentTheme)?.label || currentTheme
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
onMount(() => {
|
|
73
|
-
const savedTheme = localStorage.getItem(storageKey);
|
|
74
|
-
if (savedTheme && themes.some((t) => t.name === savedTheme)) {
|
|
75
|
-
currentTheme = savedTheme;
|
|
76
|
-
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
77
|
-
} else {
|
|
78
|
-
currentTheme = defaultTheme;
|
|
79
|
-
document.documentElement.setAttribute('data-theme', defaultTheme);
|
|
80
|
-
localStorage.setItem(storageKey, defaultTheme);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
function handleThemeChange(themeName: string) {
|
|
85
|
-
if (themeName === currentTheme) return;
|
|
86
|
-
|
|
87
|
-
// Close dropdown first so the trigger is visible during animation
|
|
88
|
-
if (document.activeElement instanceof HTMLElement) {
|
|
89
|
-
document.activeElement.blur();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
isAnimating = true;
|
|
93
|
-
|
|
94
|
-
setTimeout(() => {
|
|
95
|
-
currentTheme = themeName;
|
|
96
|
-
document.documentElement.setAttribute('data-theme', themeName);
|
|
97
|
-
localStorage.setItem(storageKey, themeName);
|
|
98
|
-
onThemeChange?.(themeName);
|
|
99
|
-
}, 150);
|
|
100
|
-
|
|
101
|
-
setTimeout(() => {
|
|
102
|
-
isAnimating = false;
|
|
103
|
-
}, 500);
|
|
104
|
-
}
|
|
105
|
-
</script>
|
|
106
|
-
|
|
107
|
-
<div class="dropdown dropdown-end" class:dropdown-top={dropUp}>
|
|
108
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
109
|
-
<div
|
|
110
|
-
tabindex="0"
|
|
111
|
-
role="button"
|
|
112
|
-
class="flex items-center gap-2 cursor-pointer hover:bg-base-300 transition-colors px-2 py-1 rounded"
|
|
113
|
-
aria-label="Change Theme"
|
|
114
|
-
title="Change Theme"
|
|
115
|
-
>
|
|
116
|
-
<div
|
|
117
|
-
data-theme={currentTheme}
|
|
118
|
-
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 transition-transform duration-300 ease-out"
|
|
119
|
-
class:theme-spin={isAnimating}
|
|
120
|
-
>
|
|
121
|
-
<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>
|
|
122
|
-
<div class="bg-primary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 50ms"></div>
|
|
123
|
-
<div class="bg-secondary size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 100ms"></div>
|
|
124
|
-
<div class="bg-accent size-1 rounded-full transition-all duration-300 theme-dot" class:theme-dot-animate={isAnimating} style="--dot-delay: 150ms"></div>
|
|
125
|
-
</div>
|
|
126
|
-
<span class="text-sm font-medium transition-opacity duration-200" class:opacity-50={isAnimating}>{currentThemeLabel}</span>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
130
|
-
<div
|
|
131
|
-
tabindex="0"
|
|
132
|
-
class="dropdown-content bg-base-200 text-base-content rounded-box
|
|
133
|
-
top-px max-h-[calc(50vh-6.5rem)] overflow-y-auto border border-base-300
|
|
134
|
-
mb-2 z-50"
|
|
135
|
-
>
|
|
136
|
-
<ul class="menu w-56">
|
|
137
|
-
<li class="menu-title text-xs">Theme</li>
|
|
138
|
-
|
|
139
|
-
{#each themes as theme}
|
|
140
|
-
<li>
|
|
141
|
-
<button
|
|
142
|
-
class="gap-3 px-2"
|
|
143
|
-
onclick={() => handleThemeChange(theme.name)}
|
|
144
|
-
class:active={currentTheme === theme.name}
|
|
145
|
-
>
|
|
146
|
-
<div
|
|
147
|
-
data-theme={theme.name}
|
|
148
|
-
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5
|
|
149
|
-
rounded-md p-1"
|
|
150
|
-
>
|
|
151
|
-
<div class="bg-base-content size-1 rounded-full"></div>
|
|
152
|
-
<div class="bg-primary size-1 rounded-full"></div>
|
|
153
|
-
<div class="bg-secondary size-1 rounded-full"></div>
|
|
154
|
-
<div class="bg-accent size-1 rounded-full"></div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div class="w-32 truncate">{theme.label}</div>
|
|
158
|
-
|
|
159
|
-
{#if currentTheme === theme.name}
|
|
160
|
-
<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">
|
|
161
|
-
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"></path>
|
|
162
|
-
</svg>
|
|
163
|
-
{:else}
|
|
164
|
-
<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">
|
|
165
|
-
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"></path>
|
|
166
|
-
</svg>
|
|
167
|
-
{/if}
|
|
168
|
-
</button>
|
|
169
|
-
</li>
|
|
170
|
-
{/each}
|
|
171
|
-
</ul>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
<style>
|
|
176
|
-
/* NOTE: DaisyUI v5.0.0 has a stuck-animation bug on `.dropdown-content`
|
|
177
|
-
(`@keyframes dropdown { 0% { opacity: 0 } }` pins opacity to 0). If the
|
|
178
|
-
theme dropdown opens but stays invisible, the consuming project needs
|
|
179
|
-
the global fix in its `app.css`:
|
|
180
|
-
|
|
181
|
-
.dropdown-content { animation: none !important; }
|
|
182
|
-
|
|
183
|
-
Upstream daisyui >= 5.5.x patches this; once you upgrade you can drop
|
|
184
|
-
the override. */
|
|
185
|
-
|
|
186
|
-
.theme-spin {
|
|
187
|
-
animation: theme-spin 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
@keyframes theme-spin {
|
|
191
|
-
0% { transform: rotate(0deg) scale(1); }
|
|
192
|
-
50% { transform: rotate(180deg) scale(1.2); }
|
|
193
|
-
100% { transform: rotate(360deg) scale(1); }
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.theme-dot-animate {
|
|
197
|
-
animation: dot-pulse 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
|
198
|
-
animation-delay: var(--dot-delay);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
@keyframes dot-pulse {
|
|
202
|
-
0% { transform: scale(1); opacity: 1; }
|
|
203
|
-
50% { transform: scale(0.5); opacity: 0.5; }
|
|
204
|
-
100% { transform: scale(1); opacity: 1; }
|
|
205
|
-
}
|
|
206
|
-
</style>
|
|
@@ -1,162 +0,0 @@
|
|
|
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>
|