@joewinke/jatui 0.1.11 → 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.
Files changed (90) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +21 -15
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/HunkDiffView.svelte +348 -0
  29. package/src/lib/components/ImageLightbox.svelte +274 -0
  30. package/src/lib/components/ImageUpload.svelte +58 -9
  31. package/src/lib/components/InlineEdit.svelte +15 -9
  32. package/src/lib/components/InputDialog.svelte +327 -0
  33. package/src/lib/components/LazyImage.svelte +1 -0
  34. package/src/lib/components/LinkShortener.svelte +1 -1
  35. package/src/lib/components/LoadingSpinner.svelte +6 -2
  36. package/src/lib/components/MarkupEditor.svelte +485 -0
  37. package/src/lib/components/MarkupOverlay.svelte +55 -0
  38. package/src/lib/components/MediaWorkbench.svelte +871 -0
  39. package/src/lib/components/MilestoneCard.svelte +1 -1
  40. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  41. package/src/lib/components/Modal.svelte +39 -4
  42. package/src/lib/components/PDFViewer.svelte +105 -0
  43. package/src/lib/components/PdfThumbnail.svelte +3 -1
  44. package/src/lib/components/PhoneInput.svelte +1 -1
  45. package/src/lib/components/ResizablePanel.svelte +4 -4
  46. package/src/lib/components/SearchDropdown.svelte +26 -13
  47. package/src/lib/components/SelectInput.svelte +26 -4
  48. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  49. package/src/lib/components/SignaturePad.svelte +8 -4
  50. package/src/lib/components/SmartImageEditor.svelte +720 -0
  51. package/src/lib/components/SortDropdown.svelte +9 -3
  52. package/src/lib/components/Sparkline.svelte +9 -0
  53. package/src/lib/components/StatusBadge.svelte +20 -18
  54. package/src/lib/components/TextArea.svelte +24 -5
  55. package/src/lib/components/TextInput.svelte +29 -6
  56. package/src/lib/components/ThemeSelector.svelte +15 -4
  57. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  58. package/src/lib/components/UserAvatar.svelte +14 -1
  59. package/src/lib/components/VariablePicker.svelte +170 -0
  60. package/src/lib/components/VoicePlayer.svelte +4 -3
  61. package/src/lib/components/markup.ts +287 -0
  62. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  63. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  64. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  65. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  66. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  67. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  68. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  69. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  70. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  71. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  72. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  73. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  74. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  75. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  76. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  77. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  78. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  79. package/src/lib/index.ts +91 -0
  80. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  81. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  82. package/src/lib/styles/rail.css +63 -0
  83. package/src/lib/types/annotation.ts +38 -0
  84. package/src/lib/types/comments.ts +97 -0
  85. package/src/lib/types/entityPreview.ts +45 -0
  86. package/src/lib/types/filePicker.ts +2 -0
  87. package/src/lib/types/smartImageEditor.ts +39 -0
  88. package/src/lib/types/templateVars.ts +36 -0
  89. package/src/lib/utils/dateFormatters.ts +12 -10
  90. package/src/lib/utils/taskUtils.ts +21 -7
@@ -42,7 +42,7 @@
42
42
 
43
43
  <div
44
44
  class="card bg-base-100 border border-base-300 {isCurrent
45
- ? 'ring-2 ring-primary/50 shadow-lg'
45
+ ? 'ring-2 ring-primary/50'
46
46
  : ''}"
47
47
  >
48
48
  <div class="card-body p-4 gap-2">
@@ -34,7 +34,7 @@
34
34
 
35
35
  <div class="space-y-1">
36
36
  <!-- Progress summary -->
37
- <div class="flex items-center justify-between text-sm mb-4">
37
+ <div class="flex items-center justify-between text-[0.9375rem] mb-4">
38
38
  <span class="text-base-content/60">
39
39
  {paidCount} of {sorted.length} milestones paid
40
40
  </span>
@@ -7,6 +7,8 @@
7
7
  */
8
8
 
9
9
  import { onMount } from 'svelte';
10
+ import { fly, fade } from 'svelte/transition';
11
+ import { cubicOut } from 'svelte/easing';
10
12
 
11
13
  interface Props {
12
14
  open?: boolean;
@@ -22,6 +24,18 @@
22
24
  ariaLabel?: string;
23
25
  dismissible?: boolean;
24
26
  zIndex?: number;
27
+ /**
28
+ * When true (default), keydown events fired inside the modal are stopped
29
+ * from propagating to window — preventing page-level list/keyboard
30
+ * navigation handlers (listNav-style) from receiving arrow keys, j/k,
31
+ * vim chords, digit counts, etc. while the modal is open.
32
+ *
33
+ * Free-text fields (INPUT, TEXTAREA, contentEditable) are intentionally
34
+ * excluded so normal typing still works. Set to false only when the
35
+ * consumer needs custom window-level key handling even while the modal
36
+ * is open.
37
+ */
38
+ trapKeys?: boolean;
25
39
  onclose?: () => void;
26
40
  onopen?: () => void;
27
41
  header?: import('svelte').Snippet;
@@ -43,6 +57,7 @@
43
57
  ariaLabel = '',
44
58
  dismissible = true,
45
59
  zIndex = 1000,
60
+ trapKeys = true,
46
61
  onclose,
47
62
  onopen,
48
63
  header,
@@ -53,6 +68,11 @@
53
68
  let modalElement: HTMLDialogElement | undefined = $state();
54
69
  let previousFocus: HTMLElement | null = null;
55
70
 
71
+ // Respect prefers-reduced-motion: skip the entrance/exit transitions.
72
+ const reducedMotion =
73
+ typeof window !== 'undefined' &&
74
+ window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
75
+
56
76
  const sizeClass = $derived(
57
77
  { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-lg', xl: 'max-w-xl', full: 'max-w-full h-full' }[size]
58
78
  );
@@ -98,6 +118,21 @@
98
118
  }
99
119
 
100
120
  function handleKeyDown(event: KeyboardEvent) {
121
+ // Stop keydown events from bubbling to window while the modal is open.
122
+ // This prevents page-level keyboard navigation (listNav-style handlers
123
+ // on window) from reacting to arrow keys, j/k, vim chords, digit counts,
124
+ // etc. typed inside the modal.
125
+ //
126
+ // Free-text fields are excluded: page-level handlers already ignore them,
127
+ // and we must not interfere with normal typing or textarea shortcuts.
128
+ if (trapKeys) {
129
+ const el = document.activeElement as HTMLElement | null;
130
+ const tag = el?.tagName;
131
+ const isTextField =
132
+ !!el && (tag === 'INPUT' || tag === 'TEXTAREA' || el.isContentEditable === true);
133
+ if (!isTextField) event.stopPropagation();
134
+ }
135
+
101
136
  if (event.key === 'Tab') {
102
137
  trapFocus(event);
103
138
  }
@@ -157,8 +192,8 @@
157
192
  >
158
193
  <!-- svelte-ignore a11y_click_events_have_key_events -->
159
194
  <!-- svelte-ignore a11y_no_static_element_interactions -->
160
- <div class="modal-backdrop" onclick={handleBackdropClick}>
161
- <div class={contentClasses}>
195
+ <div class="modal-backdrop" onclick={handleBackdropClick} transition:fade={{ duration: reducedMotion ? 0 : 200 }}>
196
+ <div class={contentClasses} in:fly={{ y: reducedMotion ? 0 : 12, duration: reducedMotion ? 0 : 220, easing: cubicOut }} out:fade={{ duration: reducedMotion ? 0 : 150 }}>
162
197
  {#if showHeader && (title || showCloseButton || header)}
163
198
  <div class="flex items-center justify-between mb-4">
164
199
  {#if header}
@@ -168,7 +203,7 @@
168
203
  {/if}
169
204
 
170
205
  {#if showCloseButton && dismissible}
171
- <button type="button" class="btn btn-sm btn-circle btn-primary" onclick={close} aria-label="Close modal">
206
+ <button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={close} aria-label="Close modal">
172
207
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
173
208
  <path
174
209
  stroke-linecap="round"
@@ -188,7 +223,7 @@
188
223
  {/if}
189
224
  </div>
190
225
 
191
- {#if showFooter && footer}
226
+ {#if footer}
192
227
  <div class="modal-action mt-6">
193
228
  {@render footer()}
194
229
  </div>
@@ -0,0 +1,105 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte"
3
+
4
+ interface Props {
5
+ fileId: string
6
+ fileName?: string
7
+ pdfUrl?: string
8
+ class?: string
9
+ }
10
+
11
+ let { fileId, fileName = "document.pdf", class: className = "" }: Props = $props()
12
+
13
+ let pageCount = $state(0)
14
+ let currentPage = $state(1)
15
+ let loading = $state(true)
16
+ let pageLoading = $state(false)
17
+ let error = $state<string | null>(null)
18
+ let imgSrc = $state<string | null>(null)
19
+
20
+ onMount(async () => {
21
+ try {
22
+ const res = await fetch(`/api/files/${fileId}/pdf-info`)
23
+ if (!res.ok) throw new Error(`Failed to load PDF info: ${res.status}`)
24
+ const data = await res.json()
25
+ pageCount = data.pageCount ?? 1
26
+ } catch (err) {
27
+ error = err instanceof Error ? err.message : "Failed to load PDF"
28
+ } finally {
29
+ loading = false
30
+ }
31
+ if (!error) loadPage(1)
32
+ })
33
+
34
+ async function loadPage(page: number) {
35
+ pageLoading = true
36
+ imgSrc = null
37
+ try {
38
+ const res = await fetch(`/api/files/${fileId}/pdf-page/${page}?scale=2`)
39
+ if (!res.ok) throw new Error(`Failed to render page ${page}`)
40
+ const blob = await res.blob()
41
+ imgSrc = URL.createObjectURL(blob)
42
+ currentPage = page
43
+ } catch (err) {
44
+ error = err instanceof Error ? err.message : "Failed to load page"
45
+ } finally {
46
+ pageLoading = false
47
+ }
48
+ }
49
+
50
+ function prevPage() {
51
+ if (currentPage > 1) loadPage(currentPage - 1)
52
+ }
53
+
54
+ function nextPage() {
55
+ if (currentPage < pageCount) loadPage(currentPage + 1)
56
+ }
57
+ </script>
58
+
59
+ <div class="flex flex-col h-full {className}">
60
+ {#if loading}
61
+ <div class="flex-1 flex items-center justify-center text-base-content/40 text-sm">
62
+ Loading PDF…
63
+ </div>
64
+ {:else if error}
65
+ <div class="flex-1 flex items-center justify-center">
66
+ <p class="text-error text-sm text-center px-4">{error}</p>
67
+ </div>
68
+ {:else}
69
+ <!-- Viewer area -->
70
+ <div class="flex-1 min-h-0 flex items-center justify-center bg-base-200/30 p-4 overflow-auto">
71
+ {#if pageLoading}
72
+ <div class="text-base-content/40 text-sm">Rendering page…</div>
73
+ {:else if imgSrc}
74
+ <img
75
+ src={imgSrc}
76
+ alt="Page {currentPage} of {fileName}"
77
+ class="max-w-full max-h-full object-contain shadow-md rounded"
78
+ />
79
+ {/if}
80
+ </div>
81
+
82
+ <!-- Pagination controls -->
83
+ {#if pageCount > 1}
84
+ <div class="flex items-center justify-center gap-3 py-2 border-t border-base-300 text-sm">
85
+ <button
86
+ type="button"
87
+ class="btn btn-ghost btn-xs"
88
+ onclick={prevPage}
89
+ disabled={currentPage <= 1}
90
+ aria-label="Previous page"
91
+ >‹</button>
92
+ <span class="text-base-content/60 tabular-nums">
93
+ {currentPage} / {pageCount}
94
+ </span>
95
+ <button
96
+ type="button"
97
+ class="btn btn-ghost btn-xs"
98
+ onclick={nextPage}
99
+ disabled={currentPage >= pageCount}
100
+ aria-label="Next page"
101
+ >›</button>
102
+ </div>
103
+ {/if}
104
+ {/if}
105
+ </div>
@@ -9,11 +9,12 @@
9
9
  }
10
10
 
11
11
  let { src, class: className = "" }: Props = $props()
12
- let canvas: HTMLCanvasElement
12
+ let canvas = $state<HTMLCanvasElement | null>(null)
13
13
  let loading = $state(true)
14
14
  let failed = $state(false)
15
15
 
16
16
  onMount(async () => {
17
+ if (!canvas) return
17
18
  try {
18
19
  const pdfjsLib = await import("pdfjs-dist")
19
20
  pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
@@ -36,6 +37,7 @@
36
37
 
37
38
  await page.render({
38
39
  canvasContext: canvas.getContext("2d")!,
40
+ canvas,
39
41
  viewport: scaled,
40
42
  }).promise
41
43
 
@@ -138,7 +138,7 @@
138
138
 
139
139
  {#if dropdownOpen}
140
140
  <div
141
- class="absolute z-50 mt-1 w-72 max-h-80 overflow-y-auto bg-base-100 border border-base-300 rounded-lg shadow-lg"
141
+ class="absolute z-50 mt-1 w-72 max-h-80 overflow-y-auto bg-base-200 border border-base-300 rounded-lg"
142
142
  role="listbox"
143
143
  >
144
144
  <div class="sticky top-0 bg-base-100 p-2 border-b border-base-300">
@@ -271,13 +271,13 @@
271
271
  width: 30px;
272
272
  height: 4px;
273
273
  border-radius: 2px;
274
- background-color: var(--color-base-300, #cbd5e1);
274
+ background-color: var(--color-base-300, oklch(0.82 0.03 255));
275
275
  transition: background-color 0.2s ease;
276
276
  }
277
277
 
278
278
  .drag-handle:hover .drag-indicator,
279
279
  .dragging .drag-indicator {
280
- background-color: var(--color-primary, #3b82f6);
280
+ background-color: var(--color-primary, oklch(0.64 0.18 259));
281
281
  }
282
282
 
283
283
  .panel-content {
@@ -297,9 +297,9 @@
297
297
  .drag-preview-inner {
298
298
  width: 100px;
299
299
  height: 60px;
300
- background-color: var(--color-primary, #3b82f6);
300
+ background-color: var(--color-primary, oklch(0.64 0.18 259));
301
301
  opacity: 0.3;
302
302
  border-radius: 4px;
303
- border: 2px solid var(--color-primary, #3b82f6);
303
+ border: 2px solid var(--color-primary, oklch(0.64 0.18 259));
304
304
  }
305
305
  </style>
@@ -40,7 +40,7 @@
40
40
 
41
41
  let open = $state(false);
42
42
  let searchQuery = $state('');
43
- let searchInput: HTMLInputElement | undefined;
43
+ let searchInput = $state<HTMLInputElement | undefined>(undefined);
44
44
  let containerRef: HTMLDivElement | undefined;
45
45
 
46
46
  // Find the currently selected option across all groups
@@ -104,7 +104,7 @@
104
104
  type="button"
105
105
  class="sd-trigger"
106
106
  class:sd-disabled={disabled}
107
- style={activeColor ? `border-left-color: ${activeColor}; border-left-width: 3px; color: ${activeColor};` : ''}
107
+ style={activeColor ? `color: ${activeColor};` : ''}
108
108
  onclick={() => { if (!disabled) open = !open; }}
109
109
  {disabled}
110
110
  >
@@ -169,7 +169,7 @@
169
169
  onclick={() => select(option.value)}
170
170
  class="sd-option"
171
171
  class:sd-option-selected={value === option.value}
172
- style={colorFn && colorFn(option.value) ? `border-left-color: ${colorFn(option.value)}; color: ${colorFn(option.value)};` : ''}
172
+ style={colorFn && colorFn(option.value) ? `color: ${colorFn(option.value)};` : ''}
173
173
  >
174
174
  {#if option.image}
175
175
  <img src={option.image} alt="" class="sd-avatar" />
@@ -189,7 +189,12 @@
189
189
  {/each}
190
190
  {/each}
191
191
  {:else}
192
- <li class="sd-empty">No matches for "{searchQuery}"</li>
192
+ <li class="sd-empty">
193
+ <svg class="sd-empty-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
194
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803 7.5 7.5 0 0015.803 15.803z" />
195
+ </svg>
196
+ No matches for "{searchQuery}"
197
+ </li>
193
198
  {/if}
194
199
  </ul>
195
200
  {#if footer}
@@ -210,7 +215,7 @@
210
215
  --sd-border: var(--sd-color-border, color-mix(in oklch, var(--color-base-content, CanvasText) 20%, transparent));
211
216
  --sd-text: var(--sd-color-text, var(--color-base-content, CanvasText));
212
217
  --sd-text-muted: var(--sd-color-text-muted, color-mix(in oklch, var(--color-base-content, CanvasText) 50%, transparent));
213
- --sd-text-label: var(--sd-color-text-label, color-mix(in oklch, var(--color-primary, LinkText) 70%, transparent));
218
+ --sd-text-label: var(--sd-color-text-label, color-mix(in oklch, var(--color-base-content, CanvasText) 45%, transparent));
214
219
  --sd-accent: var(--sd-color-accent, var(--color-primary, LinkText));
215
220
  --sd-success: var(--sd-color-success, var(--color-success, green));
216
221
  }
@@ -265,7 +270,7 @@
265
270
  justify-content: center;
266
271
  font-size: 0.6rem;
267
272
  font-weight: 700;
268
- color: #fff;
273
+ color: oklch(1 0 0);
269
274
  line-height: 1;
270
275
  }
271
276
 
@@ -347,11 +352,10 @@
347
352
  padding: 0.375rem 0.75rem 0.125rem;
348
353
  }
349
354
  .sd-group-label span {
350
- font-size: 0.75em;
355
+ font-size: 0.8125rem;
351
356
  font-family: inherit;
352
- font-weight: 600;
353
- text-transform: uppercase;
354
- letter-spacing: 0.05em;
357
+ font-weight: 400;
358
+ letter-spacing: 0.005em;
355
359
  color: var(--sd-text-label);
356
360
  }
357
361
 
@@ -368,7 +372,6 @@
368
372
  cursor: pointer;
369
373
  background: transparent;
370
374
  border: none;
371
- border-left: 2px solid transparent;
372
375
  color: var(--sd-text);
373
376
  }
374
377
  .sd-option:hover {
@@ -376,7 +379,7 @@
376
379
  }
377
380
  .sd-option-selected {
378
381
  background: var(--sd-bg-selected);
379
- border-left-color: var(--sd-accent);
382
+ font-weight: 600;
380
383
  }
381
384
 
382
385
  .sd-option-icon {
@@ -399,10 +402,20 @@
399
402
  }
400
403
 
401
404
  .sd-empty {
402
- padding: 0.75rem;
405
+ padding: 1rem 0.75rem;
403
406
  text-align: center;
404
407
  font-size: inherit;
405
408
  font-family: inherit;
406
409
  color: var(--sd-text-muted);
410
+ display: flex;
411
+ flex-direction: column;
412
+ align-items: center;
413
+ gap: 0.375rem;
414
+ }
415
+
416
+ .sd-empty-icon {
417
+ width: 1.25rem;
418
+ height: 1.25rem;
419
+ opacity: 0.35;
407
420
  }
408
421
  </style>
@@ -115,11 +115,33 @@
115
115
  document.addEventListener('click', handleClickOutside);
116
116
  return () => document.removeEventListener('click', handleClickOutside);
117
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
+ });
118
136
  </script>
119
137
 
120
- <div class="form-control w-full" bind:this={selectElement}>
138
+ <div
139
+ class="form-control w-full"
140
+ bind:this={selectElement}
141
+ onanimationend={() => selectElement?.classList.remove('t-input-shake')}
142
+ >
121
143
  <label for={id} class="label">
122
- <span class="label-text font-medium">
144
+ <span class="text-[0.8125rem] font-medium text-base-content/85">
123
145
  {label}
124
146
  {#if required}
125
147
  <span class="text-error ml-1">*</span>
@@ -205,11 +227,11 @@
205
227
 
206
228
  {#if error}
207
229
  <div class="label">
208
- <span id="{id}-error" class="label-text-alt text-error" role="alert">{error}</span>
230
+ <span id="{id}-error" class="text-[0.8125rem] text-error" role="alert">{error}</span>
209
231
  </div>
210
232
  {:else if helpText}
211
233
  <div class="label">
212
- <span id="{id}-help" class="label-text-alt text-base-content/70">{helpText}</span>
234
+ <span id="{id}-help" class="text-[0.8125rem] text-base-content/45">{helpText}</span>
213
235
  </div>
214
236
  {/if}
215
237
  </div>
@@ -28,7 +28,7 @@
28
28
  <div class="flex items-center gap-2 px-2 py-1">
29
29
  <a href={accountHref} class="flex items-center gap-2 flex-1 min-w-0" title="Account settings">
30
30
  <UserAvatar name={profile?.full_name} email={user?.email} avatarUrl={profile?.avatar_url} size="sm" />
31
- <span class="truncate text-sm">{displayName}</span>
31
+ <span class="truncate text-[0.9375rem]">{displayName}</span>
32
32
  </a>
33
33
  <a href={signOutHref} class="btn btn-ghost btn-xs shrink-0">Sign out</a>
34
34
  </div>
@@ -12,7 +12,7 @@
12
12
  onSignatureChange,
13
13
  width = 400,
14
14
  height = 150,
15
- strokeColor = '#000',
15
+ strokeColor = '',
16
16
  strokeWidth = 2,
17
17
  label = 'Signature',
18
18
  required = false,
@@ -41,7 +41,10 @@
41
41
  const ctx = canvas.getContext('2d');
42
42
  if (!ctx) return;
43
43
 
44
- ctx.strokeStyle = strokeColor;
44
+ const resolvedStroke = strokeColor ||
45
+ getComputedStyle(canvas).getPropertyValue('--color-base-content').trim() ||
46
+ 'oklch(0.21 0 0)';
47
+ ctx.strokeStyle = resolvedStroke;
45
48
  ctx.lineWidth = strokeWidth;
46
49
  ctx.lineCap = 'round';
47
50
  ctx.lineJoin = 'round';
@@ -73,7 +76,7 @@
73
76
  }
74
77
 
75
78
  function draw(e: MouseEvent | TouchEvent) {
76
- if (!isDrawing) return;
79
+ if (!isDrawing || !ctx) return;
77
80
  e.preventDefault();
78
81
 
79
82
  const coords = getCoordinates(e);
@@ -148,7 +151,8 @@
148
151
  </div>
149
152
  {/if}
150
153
 
151
- <div class="border-2 border-base-300 rounded-lg bg-white overflow-hidden relative">
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'}">
152
156
  <canvas
153
157
  bind:this={canvas}
154
158
  {width}