@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.
Files changed (91) hide show
  1. package/README.md +123 -0
  2. package/package.json +3 -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 +188 -0
  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 +183 -63
  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 +105 -1
  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/phone.ts +80 -0
  91. 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
 
@@ -1,92 +1,212 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * PhoneInput Component
3
+ * PhoneInput Component — International phone number input.
4
4
  *
5
- * Tel input with basic validation and format indicator.
6
- * Converted from Svelte 4 to Svelte 5 runes.
5
+ * Country selector dropdown (flag + dial code + search) alongside a tel input.
6
+ * Formats numbers with libphonenumber-js AsYouType as the user types.
7
+ * Exposes the parsed E.164 value via `bind:value` and a hidden form field
8
+ * named `name` so it submits cleanly with native `<form>` actions.
9
+ *
10
+ * Auto-detects country when the user's input begins with a `+` prefix.
7
11
  */
12
+ import { AsYouType, parsePhoneNumberFromString, type CountryCode } from "libphonenumber-js";
13
+ import { listCountries, PRIORITY_COUNTRIES, type CountryOption } from "../utils/phone";
8
14
 
9
- interface Props {
10
- label: string;
15
+ type Props = {
16
+ name?: string;
11
17
  value?: string;
18
+ defaultCountry?: CountryCode;
12
19
  required?: boolean;
13
- disabled?: boolean;
14
- error?: string;
15
- helpText?: string;
16
20
  placeholder?: string;
17
- class?: string;
18
21
  id?: string;
19
- name?: string;
20
- oninput?: (value: string, isValid: boolean) => void;
21
- }
22
+ class?: string;
23
+ disabled?: boolean;
24
+ autocomplete?: HTMLInputElement["autocomplete"];
25
+ };
22
26
 
23
27
  let {
24
- label,
25
- value = $bindable(''),
28
+ name = "",
29
+ value = $bindable(""),
30
+ defaultCountry = "US",
26
31
  required = false,
32
+ placeholder = "Phone number",
33
+ id,
34
+ class: klass = "",
27
35
  disabled = false,
28
- error = '',
29
- helpText = '',
30
- placeholder = '',
31
- class: className = '',
32
- id = `phone-input-${Math.random().toString(36).substr(2, 9)}`,
33
- name = '',
34
- oninput
36
+ autocomplete = "tel" as HTMLInputElement["autocomplete"],
35
37
  }: Props = $props();
36
38
 
37
- const displayPlaceholder = $derived(placeholder || '(555) 123-4567');
38
- const isValid = $derived(
39
- /^[\d\s\-\(\)\.+]{10,}$/.test(value) && value.replace(/\D/g, '').length === 10
39
+ const all = listCountries();
40
+ const priorityOptions = PRIORITY_COUNTRIES.map((c) => all.find((o) => o.code === c)).filter(
41
+ (o): o is CountryOption => !!o,
40
42
  );
43
+ const otherOptions = all.filter((o) => !PRIORITY_COUNTRIES.includes(o.code));
41
44
 
42
- function handleInput(event: Event) {
43
- const target = event.target as HTMLInputElement;
44
- value = target.value;
45
- oninput?.(value, isValid);
45
+ function initialFrom(e164: string): { country: CountryCode; national: string } {
46
+ if (e164) {
47
+ const parsed = parsePhoneNumberFromString(e164);
48
+ if (parsed?.country) {
49
+ return { country: parsed.country, national: parsed.formatNational() };
50
+ }
51
+ }
52
+ return { country: defaultCountry, national: "" };
46
53
  }
54
+
55
+ const init = initialFrom(value ?? "");
56
+ let country = $state<CountryCode>(init.country);
57
+ let display = $state<string>(init.national);
58
+ let dropdownOpen = $state(false);
59
+ let search = $state("");
60
+
61
+ const current = $derived(all.find((o) => o.code === country) ?? all[0]);
62
+
63
+ const filteredOthers = $derived.by(() => {
64
+ const q = search.trim().toLowerCase();
65
+ if (!q) return otherOptions;
66
+ return otherOptions.filter(
67
+ (o) => o.name.toLowerCase().includes(q) || o.dialCode.includes(q) || o.code.toLowerCase().includes(q),
68
+ );
69
+ });
70
+
71
+ function updateE164(raw: string, cc: CountryCode) {
72
+ const parsed = parsePhoneNumberFromString(raw, cc);
73
+ value = parsed?.isValid() ? parsed.number : "";
74
+ }
75
+
76
+ function onInput(e: Event) {
77
+ const raw = (e.target as HTMLInputElement).value;
78
+ if (raw.startsWith("+")) {
79
+ const parsed = parsePhoneNumberFromString(raw);
80
+ if (parsed?.country) {
81
+ country = parsed.country;
82
+ display = parsed.formatNational();
83
+ value = parsed.isValid() ? parsed.number : "";
84
+ return;
85
+ }
86
+ display = raw;
87
+ value = "";
88
+ return;
89
+ }
90
+ const formatter = new AsYouType(country);
91
+ display = formatter.input(raw);
92
+ updateE164(raw, country);
93
+ }
94
+
95
+ function pickCountry(code: CountryCode) {
96
+ country = code;
97
+ dropdownOpen = false;
98
+ search = "";
99
+ updateE164(display, code);
100
+ }
101
+
102
+ function toggleDropdown() {
103
+ dropdownOpen = !dropdownOpen;
104
+ if (dropdownOpen) search = "";
105
+ }
106
+
107
+ function onBlur() {
108
+ setTimeout(() => (dropdownOpen = false), 150);
109
+ }
110
+
111
+ $effect(() => {
112
+ if (!value) return;
113
+ const parsed = parsePhoneNumberFromString(value);
114
+ if (parsed?.country && parsed.country !== country) {
115
+ country = parsed.country;
116
+ display = parsed.formatNational();
117
+ }
118
+ });
47
119
  </script>
48
120
 
49
- <div class="form-control w-full">
50
- <label for={id} class="label">
51
- <span class="label-text font-medium">
52
- {label}
53
- {#if required}
54
- <span class="text-error ml-1">*</span>
55
- {/if}
56
- </span>
57
- </label>
121
+ <div class="flex w-full {klass}">
122
+ <div class="relative">
123
+ <button
124
+ type="button"
125
+ class="btn btn-outline rounded-r-none border-r-0 px-3 min-w-[5.5rem] justify-start"
126
+ onclick={toggleDropdown}
127
+ {disabled}
128
+ aria-label="Select country code"
129
+ aria-expanded={dropdownOpen}
130
+ aria-haspopup="listbox"
131
+ >
132
+ <span class="text-lg leading-none">{current?.flag}</span>
133
+ <span class="text-sm font-mono">{current?.dialCode}</span>
134
+ <svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
135
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
136
+ </svg>
137
+ </button>
138
+
139
+ {#if dropdownOpen}
140
+ <div
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
+ role="listbox"
143
+ >
144
+ <div class="sticky top-0 bg-base-100 p-2 border-b border-base-300">
145
+ <input
146
+ type="text"
147
+ class="input input-sm input-bordered w-full"
148
+ placeholder="Search country…"
149
+ bind:value={search}
150
+ aria-label="Search country"
151
+ />
152
+ </div>
153
+ <ul class="py-1">
154
+ {#if !search}
155
+ {#each priorityOptions as opt (opt.code)}
156
+ <li>
157
+ <button
158
+ type="button"
159
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-base-200 {opt.code === country ? 'bg-primary/10' : ''}"
160
+ onclick={() => pickCountry(opt.code)}
161
+ role="option"
162
+ aria-selected={opt.code === country}
163
+ >
164
+ <span class="text-lg leading-none">{opt.flag}</span>
165
+ <span class="flex-1 text-left">{opt.name}</span>
166
+ <span class="text-base-content/60 font-mono text-xs">{opt.dialCode}</span>
167
+ </button>
168
+ </li>
169
+ {/each}
170
+ <li><div class="border-t border-base-300 my-1"></div></li>
171
+ {/if}
172
+ {#each filteredOthers as opt (opt.code)}
173
+ <li>
174
+ <button
175
+ type="button"
176
+ class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-base-200 {opt.code === country ? 'bg-primary/10' : ''}"
177
+ onclick={() => pickCountry(opt.code)}
178
+ role="option"
179
+ aria-selected={opt.code === country}
180
+ >
181
+ <span class="text-lg leading-none">{opt.flag}</span>
182
+ <span class="flex-1 text-left">{opt.name}</span>
183
+ <span class="text-base-content/60 font-mono text-xs">{opt.dialCode}</span>
184
+ </button>
185
+ </li>
186
+ {/each}
187
+ {#if filteredOthers.length === 0}
188
+ <li class="px-3 py-2 text-sm text-base-content/60">No matches</li>
189
+ {/if}
190
+ </ul>
191
+ </div>
192
+ {/if}
193
+ </div>
58
194
 
59
195
  <input
60
- {id}
61
- {name}
62
196
  type="tel"
197
+ class="input input-bordered rounded-l-none flex-1 min-w-0"
198
+ {id}
199
+ {placeholder}
63
200
  {required}
64
201
  {disabled}
65
- placeholder={displayPlaceholder}
66
- {value}
67
- oninput={handleInput}
68
- pattern="[0-9\s\-\(\)\.+]*"
69
- class="input input-bordered w-full {error ? 'input-error' : ''} {className}"
70
- aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
71
- aria-invalid={error ? 'true' : 'false'}
72
- autocomplete="tel"
202
+ {autocomplete}
203
+ value={display}
204
+ oninput={onInput}
205
+ onblur={onBlur}
206
+ aria-describedby={id ? `${id}-help` : undefined}
73
207
  />
74
208
 
75
- {#if error}
76
- <div class="label">
77
- <span id="{id}-error" class="label-text-alt text-error" role="alert">{error}</span>
78
- </div>
79
- {:else if helpText}
80
- <div class="label">
81
- <span id="{id}-help" class="label-text-alt text-base-content/70">{helpText}</span>
82
- </div>
83
- {/if}
84
-
85
- {#if value && !error}
86
- <div class="label">
87
- <span class="label-text-alt {isValid ? 'text-success' : 'text-warning'}">
88
- {isValid ? '✓ Valid phone number' : '⚠ Invalid phone number'}
89
- </span>
90
- </div>
209
+ {#if name}
210
+ <input type="hidden" {name} {value} />
91
211
  {/if}
92
212
  </div>
@@ -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>