@joewinke/jatui 0.1.23 → 0.1.24
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,237 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SelectInput Component
|
|
4
|
-
*
|
|
5
|
-
* Dropdown select with searchable mode, grouped options, and validation.
|
|
6
|
-
* Converted from Svelte 4 to Svelte 5 runes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { onMount } from 'svelte';
|
|
10
|
-
|
|
11
|
-
export interface SelectOption {
|
|
12
|
-
value: string | number;
|
|
13
|
-
label: string;
|
|
14
|
-
disabled?: boolean;
|
|
15
|
-
group?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Props {
|
|
19
|
-
label: string;
|
|
20
|
-
value?: string | number;
|
|
21
|
-
options?: SelectOption[];
|
|
22
|
-
placeholder?: string;
|
|
23
|
-
required?: boolean;
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
searchable?: boolean;
|
|
26
|
-
error?: string;
|
|
27
|
-
helpText?: string;
|
|
28
|
-
size?: 'sm' | 'md' | 'lg';
|
|
29
|
-
class?: string;
|
|
30
|
-
id?: string;
|
|
31
|
-
name?: string;
|
|
32
|
-
clearSearchOnSelect?: boolean;
|
|
33
|
-
onchange?: (value: string | number, option: SelectOption | undefined) => void;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let {
|
|
37
|
-
label,
|
|
38
|
-
value = $bindable(''),
|
|
39
|
-
options = [],
|
|
40
|
-
placeholder = 'Select an option',
|
|
41
|
-
required = false,
|
|
42
|
-
disabled = false,
|
|
43
|
-
searchable = false,
|
|
44
|
-
error = '',
|
|
45
|
-
helpText = '',
|
|
46
|
-
size = 'md',
|
|
47
|
-
class: className = '',
|
|
48
|
-
id = `select-input-${Math.random().toString(36).substr(2, 9)}`,
|
|
49
|
-
name = '',
|
|
50
|
-
clearSearchOnSelect = true,
|
|
51
|
-
onchange
|
|
52
|
-
}: Props = $props();
|
|
53
|
-
|
|
54
|
-
let searchTerm = $state('');
|
|
55
|
-
let isOpen = $state(false);
|
|
56
|
-
let selectElement: HTMLElement | undefined = $state();
|
|
57
|
-
|
|
58
|
-
const sizeClass = $derived({ sm: 'select-sm', md: '', lg: 'select-lg' }[size]);
|
|
59
|
-
|
|
60
|
-
const selectClass = $derived(
|
|
61
|
-
['select select-bordered w-full', sizeClass, error ? 'select-error' : '', disabled ? 'select-disabled' : '', className]
|
|
62
|
-
.filter(Boolean)
|
|
63
|
-
.join(' ')
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const filteredOptions = $derived(
|
|
67
|
-
searchable && searchTerm
|
|
68
|
-
? options.filter((o) => o.label.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
69
|
-
: options
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const selectedOption = $derived(options.find((o) => o.value === value));
|
|
73
|
-
|
|
74
|
-
const groupedOptions = $derived(
|
|
75
|
-
filteredOptions.reduce(
|
|
76
|
-
(acc, option) => {
|
|
77
|
-
const group = option.group || 'default';
|
|
78
|
-
if (!acc[group]) acc[group] = [];
|
|
79
|
-
acc[group].push(option);
|
|
80
|
-
return acc;
|
|
81
|
-
},
|
|
82
|
-
{} as Record<string, SelectOption[]>
|
|
83
|
-
)
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
function handleSelect(option: SelectOption) {
|
|
87
|
-
if (option.disabled) return;
|
|
88
|
-
value = option.value;
|
|
89
|
-
onchange?.(value, option);
|
|
90
|
-
if (clearSearchOnSelect) searchTerm = '';
|
|
91
|
-
isOpen = false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
95
|
-
if (event.key === 'Escape') {
|
|
96
|
-
isOpen = false;
|
|
97
|
-
searchTerm = '';
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function handleClickOutside(event: Event) {
|
|
102
|
-
if (selectElement && !selectElement.contains(event.target as Node)) {
|
|
103
|
-
isOpen = false;
|
|
104
|
-
searchTerm = '';
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function handleNativeChange(e: Event) {
|
|
109
|
-
const target = e.currentTarget as HTMLSelectElement;
|
|
110
|
-
const option = options.find((opt) => opt.value.toString() === target.value);
|
|
111
|
-
onchange?.(value!, option);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
onMount(() => {
|
|
115
|
-
document.addEventListener('click', handleClickOutside);
|
|
116
|
-
return () => document.removeEventListener('click', handleClickOutside);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Error-state shake (transitions.dev). Shakes the whole control wrapper when
|
|
120
|
-
// `error` becomes set. See TextInput for the rationale.
|
|
121
|
-
let hadError = false;
|
|
122
|
-
$effect(() => {
|
|
123
|
-
const hasError = !!error;
|
|
124
|
-
if (hasError && !hadError && selectElement) {
|
|
125
|
-
const reduce =
|
|
126
|
-
typeof window !== 'undefined' &&
|
|
127
|
-
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
128
|
-
if (!reduce) {
|
|
129
|
-
selectElement.classList.remove('t-input-shake');
|
|
130
|
-
void selectElement.offsetWidth;
|
|
131
|
-
selectElement.classList.add('t-input-shake');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
hadError = hasError;
|
|
135
|
-
});
|
|
136
|
-
</script>
|
|
137
|
-
|
|
138
|
-
<div
|
|
139
|
-
class="form-control w-full"
|
|
140
|
-
bind:this={selectElement}
|
|
141
|
-
onanimationend={() => selectElement?.classList.remove('t-input-shake')}
|
|
142
|
-
>
|
|
143
|
-
<label for={id} class="label">
|
|
144
|
-
<span class="text-[0.8125rem] font-medium text-base-content/85">
|
|
145
|
-
{label}
|
|
146
|
-
{#if required}
|
|
147
|
-
<span class="text-error ml-1">*</span>
|
|
148
|
-
{/if}
|
|
149
|
-
</span>
|
|
150
|
-
</label>
|
|
151
|
-
|
|
152
|
-
{#if searchable}
|
|
153
|
-
<div class="dropdown w-full {isOpen ? 'dropdown-open' : ''}">
|
|
154
|
-
<div tabindex="0" role="button" class="btn btn-outline w-full justify-between {sizeClass}">
|
|
155
|
-
<input
|
|
156
|
-
{id}
|
|
157
|
-
{name}
|
|
158
|
-
type="text"
|
|
159
|
-
placeholder={selectedOption?.label || placeholder}
|
|
160
|
-
bind:value={searchTerm}
|
|
161
|
-
onfocus={() => (isOpen = true)}
|
|
162
|
-
onkeydown={handleKeydown}
|
|
163
|
-
class="input input-ghost w-full p-0 h-auto min-h-0 focus:outline-none"
|
|
164
|
-
{disabled}
|
|
165
|
-
{required}
|
|
166
|
-
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
167
|
-
aria-invalid={error ? 'true' : 'false'}
|
|
168
|
-
/>
|
|
169
|
-
<svg class="w-4 h-4 opacity-70" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
170
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
171
|
-
</svg>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{#if isOpen}
|
|
175
|
-
<div class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-full max-h-60 overflow-auto">
|
|
176
|
-
{#each Object.entries(groupedOptions) as [groupName, groupOpts]}
|
|
177
|
-
{#if groupName !== 'default'}
|
|
178
|
-
<div class="menu-title"><span>{groupName}</span></div>
|
|
179
|
-
{/if}
|
|
180
|
-
{#each groupOpts as option}
|
|
181
|
-
<li>
|
|
182
|
-
<button
|
|
183
|
-
type="button"
|
|
184
|
-
class="text-left {option.disabled ? 'disabled' : ''}"
|
|
185
|
-
class:active={option.value === value}
|
|
186
|
-
onclick={() => handleSelect(option)}
|
|
187
|
-
disabled={option.disabled}
|
|
188
|
-
>
|
|
189
|
-
{option.label}
|
|
190
|
-
</button>
|
|
191
|
-
</li>
|
|
192
|
-
{/each}
|
|
193
|
-
{/each}
|
|
194
|
-
</div>
|
|
195
|
-
{/if}
|
|
196
|
-
</div>
|
|
197
|
-
{:else}
|
|
198
|
-
<select
|
|
199
|
-
{id}
|
|
200
|
-
{name}
|
|
201
|
-
{required}
|
|
202
|
-
{disabled}
|
|
203
|
-
bind:value
|
|
204
|
-
class={selectClass}
|
|
205
|
-
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
206
|
-
aria-invalid={error ? 'true' : 'false'}
|
|
207
|
-
onchange={handleNativeChange}
|
|
208
|
-
>
|
|
209
|
-
{#if placeholder}
|
|
210
|
-
<option value="" disabled>{placeholder}</option>
|
|
211
|
-
{/if}
|
|
212
|
-
{#each Object.entries(groupedOptions) as [groupName, groupOpts]}
|
|
213
|
-
{#if groupName !== 'default'}
|
|
214
|
-
<optgroup label={groupName}>
|
|
215
|
-
{#each groupOpts as option}
|
|
216
|
-
<option value={option.value} disabled={option.disabled}>{option.label}</option>
|
|
217
|
-
{/each}
|
|
218
|
-
</optgroup>
|
|
219
|
-
{:else}
|
|
220
|
-
{#each groupOpts as option}
|
|
221
|
-
<option value={option.value} disabled={option.disabled}>{option.label}</option>
|
|
222
|
-
{/each}
|
|
223
|
-
{/if}
|
|
224
|
-
{/each}
|
|
225
|
-
</select>
|
|
226
|
-
{/if}
|
|
227
|
-
|
|
228
|
-
{#if error}
|
|
229
|
-
<div class="label">
|
|
230
|
-
<span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
|
|
231
|
-
</div>
|
|
232
|
-
{:else if helpText}
|
|
233
|
-
<div class="label">
|
|
234
|
-
<span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
|
|
235
|
-
</div>
|
|
236
|
-
{/if}
|
|
237
|
-
</div>
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
SidebarUserFooter — avatar + name (links to account) + sign out button.
|
|
3
|
-
Placed at the bottom of app sidebar drawers (admin, facilitator, portal).
|
|
4
|
-
Used in JST template layouts.
|
|
5
|
-
-->
|
|
6
|
-
<script lang="ts">
|
|
7
|
-
import UserAvatar from './UserAvatar.svelte'
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
profile?: { full_name?: string | null; avatar_url?: string | null } | null
|
|
11
|
-
user?: { email?: string | null } | null
|
|
12
|
-
/** Where the avatar/name links to. Defaults to /account */
|
|
13
|
-
accountHref?: string
|
|
14
|
-
/** Sign out URL. Defaults to /account/sign_out */
|
|
15
|
-
signOutHref?: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
let {
|
|
19
|
-
profile,
|
|
20
|
-
user,
|
|
21
|
-
accountHref = '/account',
|
|
22
|
-
signOutHref = '/account/sign_out',
|
|
23
|
-
}: Props = $props()
|
|
24
|
-
|
|
25
|
-
let displayName = $derived(profile?.full_name || user?.email || '?')
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<div class="flex items-center gap-2 px-2 py-1">
|
|
29
|
-
<a href={accountHref} class="flex items-center gap-2 flex-1 min-w-0" title="Account settings">
|
|
30
|
-
<UserAvatar name={profile?.full_name} email={user?.email} avatarUrl={profile?.avatar_url} size="sm" />
|
|
31
|
-
<span class="truncate text-[0.9375rem]">{displayName}</span>
|
|
32
|
-
</a>
|
|
33
|
-
<a href={signOutHref} class="btn btn-ghost btn-xs shrink-0">Sign out</a>
|
|
34
|
-
</div>
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* SignaturePad — Canvas-based digital signature capture with mouse and touch support.
|
|
4
|
-
*
|
|
5
|
-
* Exports `clear()` and `isEmpty()` methods. Outputs signature as base64 PNG data URL
|
|
6
|
-
* via the bindable `signatureData` prop or `onSignatureChange` callback.
|
|
7
|
-
*/
|
|
8
|
-
import { onMount } from 'svelte';
|
|
9
|
-
|
|
10
|
-
let {
|
|
11
|
-
signatureData = $bindable(''),
|
|
12
|
-
onSignatureChange,
|
|
13
|
-
width = 400,
|
|
14
|
-
height = 150,
|
|
15
|
-
strokeColor = '',
|
|
16
|
-
strokeWidth = 2,
|
|
17
|
-
label = 'Signature',
|
|
18
|
-
required = false,
|
|
19
|
-
placeholder = 'Sign here',
|
|
20
|
-
}: {
|
|
21
|
-
signatureData?: string;
|
|
22
|
-
onSignatureChange?: (data: string) => void;
|
|
23
|
-
width?: number;
|
|
24
|
-
height?: number;
|
|
25
|
-
strokeColor?: string;
|
|
26
|
-
strokeWidth?: number;
|
|
27
|
-
label?: string;
|
|
28
|
-
required?: boolean;
|
|
29
|
-
placeholder?: string;
|
|
30
|
-
} = $props();
|
|
31
|
-
|
|
32
|
-
let canvas: HTMLCanvasElement;
|
|
33
|
-
let isDrawing = false;
|
|
34
|
-
let lastX = 0;
|
|
35
|
-
let lastY = 0;
|
|
36
|
-
let hasDrawn = $state(false);
|
|
37
|
-
|
|
38
|
-
onMount(() => {
|
|
39
|
-
if (!canvas) return;
|
|
40
|
-
|
|
41
|
-
const ctx = canvas.getContext('2d');
|
|
42
|
-
if (!ctx) return;
|
|
43
|
-
|
|
44
|
-
const resolvedStroke = strokeColor ||
|
|
45
|
-
getComputedStyle(canvas).getPropertyValue('--color-base-content').trim() ||
|
|
46
|
-
'oklch(0.21 0 0)';
|
|
47
|
-
ctx.strokeStyle = resolvedStroke;
|
|
48
|
-
ctx.lineWidth = strokeWidth;
|
|
49
|
-
ctx.lineCap = 'round';
|
|
50
|
-
ctx.lineJoin = 'round';
|
|
51
|
-
|
|
52
|
-
function getCoordinates(e: MouseEvent | TouchEvent) {
|
|
53
|
-
const rect = canvas.getBoundingClientRect();
|
|
54
|
-
const scaleX = canvas.width / rect.width;
|
|
55
|
-
const scaleY = canvas.height / rect.height;
|
|
56
|
-
|
|
57
|
-
if (e instanceof MouseEvent) {
|
|
58
|
-
return {
|
|
59
|
-
x: (e.clientX - rect.left) * scaleX,
|
|
60
|
-
y: (e.clientY - rect.top) * scaleY
|
|
61
|
-
};
|
|
62
|
-
} else {
|
|
63
|
-
return {
|
|
64
|
-
x: (e.touches[0].clientX - rect.left) * scaleX,
|
|
65
|
-
y: (e.touches[0].clientY - rect.top) * scaleY
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function startDrawing(e: MouseEvent | TouchEvent) {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
isDrawing = true;
|
|
73
|
-
const coords = getCoordinates(e);
|
|
74
|
-
lastX = coords.x;
|
|
75
|
-
lastY = coords.y;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function draw(e: MouseEvent | TouchEvent) {
|
|
79
|
-
if (!isDrawing || !ctx) return;
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
|
|
82
|
-
const coords = getCoordinates(e);
|
|
83
|
-
ctx.beginPath();
|
|
84
|
-
ctx.moveTo(lastX, lastY);
|
|
85
|
-
ctx.lineTo(coords.x, coords.y);
|
|
86
|
-
ctx.stroke();
|
|
87
|
-
lastX = coords.x;
|
|
88
|
-
lastY = coords.y;
|
|
89
|
-
hasDrawn = true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function stopDrawing() {
|
|
93
|
-
if (isDrawing && hasDrawn) {
|
|
94
|
-
isDrawing = false;
|
|
95
|
-
signatureData = canvas.toDataURL();
|
|
96
|
-
onSignatureChange?.(signatureData);
|
|
97
|
-
}
|
|
98
|
-
isDrawing = false;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
canvas.addEventListener('mousedown', startDrawing);
|
|
102
|
-
canvas.addEventListener('mousemove', draw);
|
|
103
|
-
canvas.addEventListener('mouseup', stopDrawing);
|
|
104
|
-
canvas.addEventListener('mouseleave', stopDrawing);
|
|
105
|
-
|
|
106
|
-
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
|
107
|
-
canvas.addEventListener('touchmove', draw, { passive: false });
|
|
108
|
-
canvas.addEventListener('touchend', stopDrawing);
|
|
109
|
-
canvas.addEventListener('touchcancel', stopDrawing);
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
canvas.removeEventListener('mousedown', startDrawing);
|
|
113
|
-
canvas.removeEventListener('mousemove', draw);
|
|
114
|
-
canvas.removeEventListener('mouseup', stopDrawing);
|
|
115
|
-
canvas.removeEventListener('mouseleave', stopDrawing);
|
|
116
|
-
canvas.removeEventListener('touchstart', startDrawing);
|
|
117
|
-
canvas.removeEventListener('touchmove', draw);
|
|
118
|
-
canvas.removeEventListener('touchend', stopDrawing);
|
|
119
|
-
canvas.removeEventListener('touchcancel', stopDrawing);
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export function clear() {
|
|
124
|
-
if (!canvas) return;
|
|
125
|
-
const ctx = canvas.getContext('2d');
|
|
126
|
-
if (!ctx) return;
|
|
127
|
-
|
|
128
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
129
|
-
signatureData = '';
|
|
130
|
-
hasDrawn = false;
|
|
131
|
-
onSignatureChange?.('');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function isEmpty(): boolean {
|
|
135
|
-
return !hasDrawn || !signatureData;
|
|
136
|
-
}
|
|
137
|
-
</script>
|
|
138
|
-
|
|
139
|
-
<div class="signature-pad-container">
|
|
140
|
-
{#if label}
|
|
141
|
-
<div class="flex justify-between items-center mb-2">
|
|
142
|
-
<span class="text-sm text-base-content/70">
|
|
143
|
-
{label}
|
|
144
|
-
{#if required}
|
|
145
|
-
<span class="text-error">*</span>
|
|
146
|
-
{/if}
|
|
147
|
-
</span>
|
|
148
|
-
<button type="button" onclick={() => clear()} class="btn btn-ghost btn-xs">
|
|
149
|
-
Clear
|
|
150
|
-
</button>
|
|
151
|
-
</div>
|
|
152
|
-
{/if}
|
|
153
|
-
|
|
154
|
-
<div class="rounded-lg bg-base-100 overflow-hidden relative transition-all duration-300
|
|
155
|
-
{hasDrawn ? 'border-2 border-success/60' : 'border-2 border-base-300'}">
|
|
156
|
-
<canvas
|
|
157
|
-
bind:this={canvas}
|
|
158
|
-
{width}
|
|
159
|
-
{height}
|
|
160
|
-
class="w-full touch-none cursor-crosshair"
|
|
161
|
-
></canvas>
|
|
162
|
-
|
|
163
|
-
{#if !hasDrawn}
|
|
164
|
-
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
|
165
|
-
<span class="text-base-content/30 text-sm">{placeholder}</span>
|
|
166
|
-
</div>
|
|
167
|
-
{/if}
|
|
168
|
-
</div>
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
<style>
|
|
172
|
-
canvas {
|
|
173
|
-
display: block;
|
|
174
|
-
}
|
|
175
|
-
</style>
|