@joewinke/jatui 0.1.10 → 0.1.19
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/README.md +123 -0
- package/package.json +3 -1
- package/src/lib/actions/railNav.ts +473 -0
- package/src/lib/components/AnnotationLayer.svelte +108 -0
- package/src/lib/components/AnnotationPanel.svelte +319 -0
- package/src/lib/components/AudioWaveform.svelte +9 -5
- package/src/lib/components/AvailabilityModal.svelte +7 -3
- package/src/lib/components/AvatarUpload.svelte +27 -4
- package/src/lib/components/BookingForm.svelte +11 -9
- package/src/lib/components/BurndownChart.svelte +778 -0
- package/src/lib/components/Button.svelte +10 -1
- package/src/lib/components/CalendarPicker.svelte +3 -3
- package/src/lib/components/Card.svelte +2 -2
- package/src/lib/components/ChipInput.svelte +21 -15
- package/src/lib/components/ColorSelector.svelte +17 -13
- package/src/lib/components/CommentThread.svelte +773 -0
- package/src/lib/components/ConfirmDialog.svelte +348 -0
- package/src/lib/components/ConfirmModal.svelte +78 -11
- package/src/lib/components/ContextMenu.svelte +188 -0
- package/src/lib/components/CountdownTimer.svelte +1 -1
- package/src/lib/components/DateRangePicker.svelte +6 -4
- package/src/lib/components/Drawer.svelte +36 -3
- package/src/lib/components/EntityPreviewCard.svelte +104 -0
- package/src/lib/components/FileDropzone.svelte +493 -0
- package/src/lib/components/FilePicker.svelte +83 -14
- package/src/lib/components/FileThumbnail.svelte +80 -0
- package/src/lib/components/FilterDropdown.svelte +11 -11
- package/src/lib/components/HunkDiffView.svelte +348 -0
- package/src/lib/components/ImageLightbox.svelte +274 -0
- package/src/lib/components/ImageUpload.svelte +58 -9
- package/src/lib/components/InlineEdit.svelte +15 -9
- package/src/lib/components/InputDialog.svelte +327 -0
- package/src/lib/components/LazyImage.svelte +1 -0
- package/src/lib/components/LinkShortener.svelte +1 -1
- package/src/lib/components/LoadingSpinner.svelte +6 -2
- package/src/lib/components/MarkupEditor.svelte +485 -0
- package/src/lib/components/MarkupOverlay.svelte +55 -0
- package/src/lib/components/MediaWorkbench.svelte +871 -0
- package/src/lib/components/MilestoneCard.svelte +1 -1
- package/src/lib/components/MilestoneTimeline.svelte +1 -1
- package/src/lib/components/Modal.svelte +39 -4
- package/src/lib/components/PDFViewer.svelte +105 -0
- package/src/lib/components/PdfThumbnail.svelte +3 -1
- package/src/lib/components/PhoneInput.svelte +183 -63
- package/src/lib/components/ResizablePanel.svelte +4 -4
- package/src/lib/components/SearchDropdown.svelte +26 -13
- package/src/lib/components/SelectInput.svelte +26 -4
- package/src/lib/components/SidebarUserFooter.svelte +1 -1
- package/src/lib/components/SignaturePad.svelte +8 -4
- package/src/lib/components/SmartImageEditor.svelte +720 -0
- package/src/lib/components/SortDropdown.svelte +9 -3
- package/src/lib/components/Sparkline.svelte +9 -0
- package/src/lib/components/StatusBadge.svelte +20 -18
- package/src/lib/components/TextArea.svelte +24 -5
- package/src/lib/components/TextInput.svelte +29 -6
- package/src/lib/components/ThemeSelector.svelte +15 -4
- package/src/lib/components/TimeSlotPicker.svelte +7 -7
- package/src/lib/components/UserAvatar.svelte +14 -1
- package/src/lib/components/VariablePicker.svelte +170 -0
- package/src/lib/components/VoicePlayer.svelte +4 -3
- package/src/lib/components/markup.ts +287 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
- package/src/lib/components/messaging/ChannelList.svelte +1 -1
- package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
- package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
- package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
- package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
- package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
- package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
- package/src/lib/components/messaging/MessageInput.svelte +1 -1
- package/src/lib/components/messaging/MessageItem.svelte +6 -3
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
- package/src/lib/components/messaging/StartDMModal.svelte +1 -1
- package/src/lib/components/pipeline/Pipeline.svelte +4 -4
- package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
- package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
- package/src/lib/index.ts +105 -1
- package/src/lib/stores/confirmDialog.svelte.ts +48 -0
- package/src/lib/stores/inputDialog.svelte.ts +51 -0
- package/src/lib/styles/rail.css +63 -0
- package/src/lib/types/annotation.ts +38 -0
- package/src/lib/types/comments.ts +97 -0
- package/src/lib/types/entityPreview.ts +45 -0
- package/src/lib/types/filePicker.ts +2 -0
- package/src/lib/types/smartImageEditor.ts +39 -0
- package/src/lib/types/templateVars.ts +36 -0
- package/src/lib/utils/dateFormatters.ts +12 -10
- package/src/lib/utils/phone.ts +80 -0
- package/src/lib/utils/taskUtils.ts +21 -7
|
@@ -98,6 +98,9 @@
|
|
|
98
98
|
sm: 'w-24 focus:w-36',
|
|
99
99
|
md: 'w-32 focus:w-44'
|
|
100
100
|
}[size]);
|
|
101
|
+
|
|
102
|
+
import { fly } from 'svelte/transition';
|
|
103
|
+
import { cubicOut } from 'svelte/easing';
|
|
101
104
|
</script>
|
|
102
105
|
|
|
103
106
|
<div class="flex items-center gap-1">
|
|
@@ -107,7 +110,7 @@
|
|
|
107
110
|
<div class="dropdown dropdown-end flex-shrink-0" onclick={(e) => e.stopPropagation()}>
|
|
108
111
|
<button
|
|
109
112
|
tabindex="0"
|
|
110
|
-
class="btn {buttonSizeClass} btn-ghost gap-1
|
|
113
|
+
class="btn {buttonSizeClass} btn-ghost gap-1 text-[0.75rem] opacity-70 hover:opacity-100"
|
|
111
114
|
title="Sort"
|
|
112
115
|
>
|
|
113
116
|
<span>{currentIcon}</span>
|
|
@@ -115,7 +118,7 @@
|
|
|
115
118
|
<span class="text-[9px]">{sortDir === 'asc' ? '▲' : '▼'}</span>
|
|
116
119
|
</button>
|
|
117
120
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
118
|
-
<ul tabindex="0" class="dropdown-content menu {menuSizeClass} bg-base-200 rounded-box z-40 w-36 p-1
|
|
121
|
+
<ul tabindex="0" class="dropdown-content menu {menuSizeClass} bg-base-200 rounded-box z-40 w-36 p-1 border border-base-300">
|
|
119
122
|
{#each options as opt (opt.value)}
|
|
120
123
|
<li>
|
|
121
124
|
<button
|
|
@@ -137,7 +140,10 @@
|
|
|
137
140
|
{#if showFilter}
|
|
138
141
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
139
142
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
140
|
-
<div class="flex-shrink-0" onclick={(e) => e.stopPropagation()}
|
|
143
|
+
<div class="flex-shrink-0" onclick={(e) => e.stopPropagation()}
|
|
144
|
+
in:fly={{ x: -8, duration: 180, easing: cubicOut }}
|
|
145
|
+
out:fly={{ x: -4, duration: 120, easing: cubicOut }}
|
|
146
|
+
>
|
|
141
147
|
<input
|
|
142
148
|
type="text"
|
|
143
149
|
placeholder={filterPlaceholder}
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
height = 32,
|
|
14
14
|
color = 'oklch(var(--p))',
|
|
15
15
|
fillOpacity = 0.15,
|
|
16
|
+
labels,
|
|
16
17
|
}: {
|
|
17
18
|
values: number[];
|
|
18
19
|
width?: number | 'auto';
|
|
19
20
|
height?: number;
|
|
20
21
|
color?: string;
|
|
21
22
|
fillOpacity?: number;
|
|
23
|
+
/** Optional tooltip label per bar. If provided, shown as SVG title on hover. */
|
|
24
|
+
labels?: string[];
|
|
22
25
|
} = $props();
|
|
23
26
|
|
|
24
27
|
let el: HTMLDivElement | undefined = $state();
|
|
@@ -76,6 +79,12 @@
|
|
|
76
79
|
.style('fill', color)
|
|
77
80
|
.style('fill-opacity', (_, i) => fillOpacity + (1 - fillOpacity) * (i / n));
|
|
78
81
|
|
|
82
|
+
bars.each(function (d, i) {
|
|
83
|
+
d3.select(this).selectAll('title').remove();
|
|
84
|
+
const label = labels && labels[i] !== undefined ? labels[i] : String(d);
|
|
85
|
+
d3.select(this).append('title').text(label);
|
|
86
|
+
});
|
|
87
|
+
|
|
79
88
|
if (shouldAnimate) {
|
|
80
89
|
bars
|
|
81
90
|
.attr('y', innerH)
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* StatusBadge Component
|
|
4
|
-
*
|
|
5
|
-
* Predefined status badge with semantic colors, icons, and dot indicators.
|
|
6
|
-
*/
|
|
7
|
-
|
|
1
|
+
<script module lang="ts">
|
|
8
2
|
export type StatusValue =
|
|
9
3
|
| 'active'
|
|
10
4
|
| 'inactive'
|
|
@@ -20,6 +14,14 @@
|
|
|
20
14
|
| 'partial'
|
|
21
15
|
| 'assigned'
|
|
22
16
|
| 'available';
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
/**
|
|
21
|
+
* StatusBadge Component
|
|
22
|
+
*
|
|
23
|
+
* Predefined status badge with semantic colors, icons, and dot indicators.
|
|
24
|
+
*/
|
|
23
25
|
|
|
24
26
|
interface Props {
|
|
25
27
|
status: StatusValue;
|
|
@@ -50,18 +52,18 @@
|
|
|
50
52
|
const statusConfig: Record<string, { text: string; variant: string; icon: string }> = {
|
|
51
53
|
active: { text: 'Active', variant: 'success', icon: '✓' },
|
|
52
54
|
inactive: { text: 'Inactive', variant: 'error', icon: '✗' },
|
|
53
|
-
pending: { text: 'Pending', variant: 'warning', icon: '
|
|
55
|
+
pending: { text: 'Pending', variant: 'warning', icon: '' },
|
|
54
56
|
completed: { text: 'Completed', variant: 'success', icon: '✓' },
|
|
55
57
|
cancelled: { text: 'Cancelled', variant: 'error', icon: '✗' },
|
|
56
|
-
draft: { text: 'Draft', variant: 'default', icon: '
|
|
57
|
-
scheduled: { text: 'Scheduled', variant: 'info', icon: '
|
|
58
|
-
'in-progress': { text: 'In Progress', variant: 'primary', icon: '
|
|
59
|
-
overdue: { text: 'Overdue', variant: 'error', icon: '
|
|
60
|
-
paid: { text: 'Paid', variant: 'success', icon: '
|
|
61
|
-
unpaid: { text: 'Unpaid', variant: 'warning', icon: '
|
|
62
|
-
partial: { text: 'Partial', variant: 'warning', icon: '
|
|
58
|
+
draft: { text: 'Draft', variant: 'default', icon: '' },
|
|
59
|
+
scheduled: { text: 'Scheduled', variant: 'info', icon: '' },
|
|
60
|
+
'in-progress': { text: 'In Progress', variant: 'primary', icon: '' },
|
|
61
|
+
overdue: { text: 'Overdue', variant: 'error', icon: '' },
|
|
62
|
+
paid: { text: 'Paid', variant: 'success', icon: '✓' },
|
|
63
|
+
unpaid: { text: 'Unpaid', variant: 'warning', icon: '' },
|
|
64
|
+
partial: { text: 'Partial', variant: 'warning', icon: '' },
|
|
63
65
|
assigned: { text: 'Assigned', variant: 'success', icon: '✓' },
|
|
64
|
-
available: { text: 'Available', variant: 'info', icon: '
|
|
66
|
+
available: { text: 'Available', variant: 'info', icon: '' }
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
const config = $derived(statusConfig[status] || { text: status || 'Unknown', variant: 'default', icon: '?' });
|
|
@@ -135,7 +137,7 @@
|
|
|
135
137
|
onkeydown={handleKeyDown}
|
|
136
138
|
>
|
|
137
139
|
{#if showDot}
|
|
138
|
-
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5"></span>
|
|
140
|
+
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5 {status === 'active' ? 'animate-pulse' : ''}"></span>
|
|
139
141
|
{/if}
|
|
140
142
|
{#if showIcon}
|
|
141
143
|
<span class="mr-1">{config.icon}</span>
|
|
@@ -145,7 +147,7 @@
|
|
|
145
147
|
{:else}
|
|
146
148
|
<span class={badgeClass} aria-label={ariaLabel || `Status: ${displayText}`}>
|
|
147
149
|
{#if showDot}
|
|
148
|
-
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5"></span>
|
|
150
|
+
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5 {status === 'active' ? 'animate-pulse' : ''}"></span>
|
|
149
151
|
{/if}
|
|
150
152
|
{#if showIcon}
|
|
151
153
|
<span class="mr-1">{config.icon}</span>
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
|
|
60
60
|
const textareaClass = $derived(
|
|
61
61
|
[
|
|
62
|
-
'textarea textarea-bordered w-full',
|
|
62
|
+
'textarea textarea-bordered w-full transition-shadow duration-200',
|
|
63
63
|
sizeClass,
|
|
64
64
|
resizeClass,
|
|
65
65
|
error ? 'textarea-error' : '',
|
|
@@ -90,12 +90,30 @@
|
|
|
90
90
|
autoResizeTextarea(textareaElement);
|
|
91
91
|
}
|
|
92
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
|
+
});
|
|
93
111
|
</script>
|
|
94
112
|
|
|
95
113
|
<div class="form-control w-full">
|
|
96
114
|
{#if label}
|
|
97
115
|
<label for={id} class="label">
|
|
98
|
-
<span class="
|
|
116
|
+
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
99
117
|
{label}
|
|
100
118
|
{#if required}
|
|
101
119
|
<span class="text-error ml-1">*</span>
|
|
@@ -121,11 +139,12 @@
|
|
|
121
139
|
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
122
140
|
aria-invalid={error ? 'true' : 'false'}
|
|
123
141
|
style={autoResize ? `min-height: calc(1.5em * ${rows} + 1rem);` : ''}
|
|
142
|
+
onanimationend={() => textareaElement?.classList.remove('t-input-shake')}
|
|
124
143
|
></textarea>
|
|
125
144
|
|
|
126
145
|
{#if showCount && maxlength}
|
|
127
146
|
<div class="label">
|
|
128
|
-
<span class="
|
|
147
|
+
<span class="text-[0.8125rem] {isOverLimit ? 'text-error' : 'text-base-content/45'}">
|
|
129
148
|
{characterCount}/{maxlength}
|
|
130
149
|
</span>
|
|
131
150
|
</div>
|
|
@@ -133,11 +152,11 @@
|
|
|
133
152
|
|
|
134
153
|
{#if error}
|
|
135
154
|
<div class="label">
|
|
136
|
-
<span id="{id}-error" class="
|
|
155
|
+
<span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
|
|
137
156
|
</div>
|
|
138
157
|
{:else if helpText}
|
|
139
158
|
<div class="label">
|
|
140
|
-
<span id="{id}-help" class="
|
|
159
|
+
<span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
|
|
141
160
|
</div>
|
|
142
161
|
{/if}
|
|
143
162
|
</div>
|
|
@@ -51,18 +51,39 @@
|
|
|
51
51
|
const sizeClass = $derived({ sm: 'input-sm', md: '', lg: 'input-lg' }[size]);
|
|
52
52
|
|
|
53
53
|
const inputClass = $derived(
|
|
54
|
-
['input input-bordered w-full', sizeClass, error ? 'input-error' : '', disabled ? 'input-disabled' : '', className]
|
|
54
|
+
['input input-bordered w-full transition-shadow duration-200', sizeClass, error ? 'input-error' : '', disabled ? 'input-disabled' : '', className]
|
|
55
55
|
.filter(Boolean)
|
|
56
56
|
.join(' ')
|
|
57
57
|
);
|
|
58
58
|
|
|
59
59
|
const characterCount = $derived(value?.length || 0);
|
|
60
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
|
+
});
|
|
61
82
|
</script>
|
|
62
83
|
|
|
63
84
|
<div class="form-control w-full">
|
|
64
85
|
<label for={id} class="label">
|
|
65
|
-
<span class="
|
|
86
|
+
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
66
87
|
{label}
|
|
67
88
|
{#if required}
|
|
68
89
|
<span class="text-error ml-1">*</span>
|
|
@@ -71,6 +92,7 @@
|
|
|
71
92
|
</label>
|
|
72
93
|
|
|
73
94
|
<input
|
|
95
|
+
bind:this={inputEl}
|
|
74
96
|
{id}
|
|
75
97
|
{name}
|
|
76
98
|
{type}
|
|
@@ -81,16 +103,17 @@
|
|
|
81
103
|
{maxlength}
|
|
82
104
|
{minlength}
|
|
83
105
|
{pattern}
|
|
84
|
-
{autocomplete}
|
|
106
|
+
autocomplete={autocomplete as AutoFill}
|
|
85
107
|
bind:value
|
|
86
108
|
class={inputClass}
|
|
87
109
|
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
88
110
|
aria-invalid={error ? 'true' : 'false'}
|
|
111
|
+
onanimationend={() => inputEl?.classList.remove('t-input-shake')}
|
|
89
112
|
/>
|
|
90
113
|
|
|
91
114
|
{#if showCount && maxlength}
|
|
92
115
|
<div class="label">
|
|
93
|
-
<span class="
|
|
116
|
+
<span class="text-[0.8125rem] {isOverLimit ? 'text-error' : 'text-base-content/45'}">
|
|
94
117
|
{characterCount}/{maxlength}
|
|
95
118
|
</span>
|
|
96
119
|
</div>
|
|
@@ -98,11 +121,11 @@
|
|
|
98
121
|
|
|
99
122
|
{#if error}
|
|
100
123
|
<div class="label">
|
|
101
|
-
<span id="{id}-error" class="
|
|
124
|
+
<span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
|
|
102
125
|
</div>
|
|
103
126
|
{:else if helpText}
|
|
104
127
|
<div class="label">
|
|
105
|
-
<span id="{id}-help" class="
|
|
128
|
+
<span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
|
|
106
129
|
</div>
|
|
107
130
|
{/if}
|
|
108
131
|
</div>
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
onThemeChange?: (theme: string) => void;
|
|
62
62
|
} = $props();
|
|
63
63
|
|
|
64
|
+
// svelte-ignore state_referenced_locally
|
|
64
65
|
let currentTheme = $state(defaultTheme);
|
|
65
66
|
let isAnimating = $state(false);
|
|
66
67
|
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
>
|
|
115
116
|
<div
|
|
116
117
|
data-theme={currentTheme}
|
|
117
|
-
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1
|
|
118
|
+
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded-md p-1 transition-transform duration-300 ease-out"
|
|
118
119
|
class:theme-spin={isAnimating}
|
|
119
120
|
>
|
|
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>
|
|
@@ -129,8 +130,8 @@
|
|
|
129
130
|
<div
|
|
130
131
|
tabindex="0"
|
|
131
132
|
class="dropdown-content bg-base-200 text-base-content rounded-box
|
|
132
|
-
top-px max-h-[calc(50vh-6.5rem)] overflow-y-auto border border-
|
|
133
|
-
|
|
133
|
+
top-px max-h-[calc(50vh-6.5rem)] overflow-y-auto border border-base-300
|
|
134
|
+
mb-2 z-50"
|
|
134
135
|
>
|
|
135
136
|
<ul class="menu w-56">
|
|
136
137
|
<li class="menu-title text-xs">Theme</li>
|
|
@@ -145,7 +146,7 @@
|
|
|
145
146
|
<div
|
|
146
147
|
data-theme={theme.name}
|
|
147
148
|
class="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5
|
|
148
|
-
rounded-md p-1
|
|
149
|
+
rounded-md p-1"
|
|
149
150
|
>
|
|
150
151
|
<div class="bg-base-content size-1 rounded-full"></div>
|
|
151
152
|
<div class="bg-primary size-1 rounded-full"></div>
|
|
@@ -172,6 +173,16 @@
|
|
|
172
173
|
</div>
|
|
173
174
|
|
|
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
|
+
|
|
175
186
|
.theme-spin {
|
|
176
187
|
animation: theme-spin 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
177
188
|
}
|
|
@@ -76,14 +76,14 @@
|
|
|
76
76
|
{@const { morning, afternoon, evening } = groupedSlots()}
|
|
77
77
|
{@const availableCount = slots.filter(s => s.available).length}
|
|
78
78
|
|
|
79
|
-
<p class="text-
|
|
79
|
+
<p class="text-[0.8125rem] text-base-content/45 mb-4">
|
|
80
80
|
{availableCount} time{availableCount !== 1 ? 's' : ''} available
|
|
81
81
|
</p>
|
|
82
82
|
|
|
83
83
|
<div class="space-y-6">
|
|
84
84
|
{#if morning.length > 0}
|
|
85
85
|
<div>
|
|
86
|
-
<h4 class="text-
|
|
86
|
+
<h4 class="text-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
87
87
|
<!-- Sun icon -->
|
|
88
88
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
89
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" />
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
<button
|
|
96
96
|
onclick={() => handleSlotClick(slot)}
|
|
97
97
|
disabled={!slot.available}
|
|
98
|
-
class="py-3 px-4 rounded-lg text-
|
|
98
|
+
class="py-3 px-4 rounded-lg text-[0.9375rem] font-medium transition-all duration-200
|
|
99
99
|
{slot.available
|
|
100
100
|
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
101
101
|
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
|
|
110
110
|
{#if afternoon.length > 0}
|
|
111
111
|
<div>
|
|
112
|
-
<h4 class="text-
|
|
112
|
+
<h4 class="text-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
113
113
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
114
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
115
|
</svg>
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
<button
|
|
121
121
|
onclick={() => handleSlotClick(slot)}
|
|
122
122
|
disabled={!slot.available}
|
|
123
|
-
class="py-3 px-4 rounded-lg text-
|
|
123
|
+
class="py-3 px-4 rounded-lg text-[0.9375rem] font-medium transition-all duration-200
|
|
124
124
|
{slot.available
|
|
125
125
|
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
126
126
|
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
|
|
135
135
|
{#if evening.length > 0}
|
|
136
136
|
<div>
|
|
137
|
-
<h4 class="text-
|
|
137
|
+
<h4 class="text-[0.8125rem] text-base-content/45 mb-3 flex items-center gap-2 tracking-[0.005em]">
|
|
138
138
|
<!-- Moon icon -->
|
|
139
139
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
140
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" />
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
<button
|
|
147
147
|
onclick={() => handleSlotClick(slot)}
|
|
148
148
|
disabled={!slot.available}
|
|
149
|
-
class="py-3 px-4 rounded-lg text-
|
|
149
|
+
class="py-3 px-4 rounded-lg text-[0.9375rem] font-medium transition-all duration-200
|
|
150
150
|
{slot.available
|
|
151
151
|
? `bg-base-200/50 hover:bg-${accentColor} hover:text-${accentColor}-content`
|
|
152
152
|
: 'bg-base-200/20 text-base-content/30 cursor-not-allowed line-through'}"
|
|
@@ -41,6 +41,19 @@
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
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
|
+
})
|
|
44
57
|
</script>
|
|
45
58
|
|
|
46
59
|
<div
|
|
@@ -51,7 +64,7 @@
|
|
|
51
64
|
{#if avatarUrl}
|
|
52
65
|
<img src={avatarUrl} alt={label} class="w-full h-full object-cover" />
|
|
53
66
|
{:else}
|
|
54
|
-
<div class="w-full h-full
|
|
67
|
+
<div class="w-full h-full flex items-center justify-center font-semibold text-white" style="background: {avatarBg}">
|
|
55
68
|
{initials}
|
|
56
69
|
</div>
|
|
57
70
|
{/if}
|
|
@@ -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>
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
let voices = $state<any[]>([])
|
|
54
54
|
let voicesLoading = $state(false)
|
|
55
55
|
let showVoices = $state(false)
|
|
56
|
+
// svelte-ignore state_referenced_locally
|
|
56
57
|
let selectedVoiceId = $state<string | undefined>(voiceId || "alloy")
|
|
57
58
|
|
|
58
59
|
let speed = $state(1.0)
|
|
@@ -254,7 +255,7 @@
|
|
|
254
255
|
{/if}
|
|
255
256
|
{#if showControls}
|
|
256
257
|
<div
|
|
257
|
-
class="absolute flex items-center gap-2 bg-base-200 border border-base-300 rounded-full
|
|
258
|
+
class="absolute flex items-center gap-2 bg-base-200 border border-base-300 rounded-full z-50 transition-all {expandDirection === 'right' ? 'right-0' : 'left-0'}"
|
|
258
259
|
style="min-width: 180px;"
|
|
259
260
|
>
|
|
260
261
|
<!-- Play/Pause -->
|
|
@@ -330,7 +331,7 @@
|
|
|
330
331
|
<!-- Settings panel -->
|
|
331
332
|
{#if showSettings}
|
|
332
333
|
<div
|
|
333
|
-
class="absolute right-0 top-full z-50 bg-base-200 border border-base-300 rounded-lg
|
|
334
|
+
class="absolute right-0 top-full z-50 bg-base-200 border border-base-300 rounded-lg p-3 w-64"
|
|
334
335
|
style="margin-top: 0.25rem;"
|
|
335
336
|
>
|
|
336
337
|
<div class="mb-3">
|
|
@@ -364,7 +365,7 @@
|
|
|
364
365
|
<!-- Voice selection dropdown -->
|
|
365
366
|
{#if showVoices}
|
|
366
367
|
<div
|
|
367
|
-
class="absolute right-0 top-full z-50 bg-base-200 border border-base-300 rounded-lg
|
|
368
|
+
class="absolute right-0 top-full z-50 bg-base-200 border border-base-300 rounded-lg p-2 max-h-64 overflow-y-auto w-64"
|
|
368
369
|
style="margin-top: 0.25rem;"
|
|
369
370
|
>
|
|
370
371
|
{#if voicesLoading}
|