@classic-homes/theme-svelte 1.22.1 → 1.22.3

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 (39) hide show
  1. package/dist/lib/components/Combobox.svelte +2 -2
  2. package/dist/lib/components/DataTable.svelte +15 -6
  3. package/dist/lib/components/DataTable.svelte.d.ts +1 -1
  4. package/dist/lib/components/DateTimePicker.svelte +31 -34
  5. package/dist/lib/components/DropdownMenu.svelte +52 -30
  6. package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
  7. package/dist/lib/components/FileUpload.svelte +5 -19
  8. package/dist/lib/components/MultiSelect.svelte +2 -2
  9. package/dist/lib/components/NumberInput.svelte +15 -2
  10. package/dist/lib/components/Select.svelte +19 -5
  11. package/dist/lib/components/Select.svelte.d.ts +4 -0
  12. package/dist/lib/components/Signature.svelte +15 -4
  13. package/dist/lib/components/Signature.svelte.d.ts +1 -1
  14. package/dist/lib/components/Slider.svelte +8 -6
  15. package/dist/lib/components/Tabs.svelte +10 -12
  16. package/dist/lib/components/Toast.svelte +12 -12
  17. package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
  18. package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
  19. package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
  20. package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
  21. package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
  22. package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
  23. package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
  24. package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
  25. package/dist/lib/components/layout/Sidebar.svelte +28 -0
  26. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +66 -50
  27. package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte.d.ts +2 -0
  28. package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
  29. package/dist/lib/composables/useAsync.svelte.js +33 -14
  30. package/dist/lib/composables/useForm.svelte.js +19 -7
  31. package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
  32. package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
  33. package/dist/lib/stores/sidebar.svelte.js +15 -0
  34. package/dist/lib/stores/toast.svelte.js +12 -1
  35. package/dist/lib/utils/date.d.ts +3 -1
  36. package/dist/lib/utils/date.js +54 -34
  37. package/dist/lib/utils/form.d.ts +2 -1
  38. package/dist/lib/utils/form.js +3 -2
  39. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  * - Error handling with toast integration
7
7
  * - Success/error callbacks
8
8
  * - Retry functionality
9
- * - Abort/cancel support
9
+ * - Overlapping calls: only the latest call (and none after reset) updates state
10
10
  * - Debounce support
11
11
  *
12
12
  * @example
@@ -51,21 +51,35 @@ export function useAsync(options) {
51
51
  // Derived state
52
52
  const isSuccess = $derived(isExecuted && !error && !isLoading);
53
53
  const isError = $derived(isExecuted && !!error);
54
- // Debounce timer
54
+ // Debounce timer, and the resolver of the call it is holding back
55
55
  let debounceTimer;
56
+ let pendingResolve;
57
+ // Id of the latest call; results from older calls are ignored
58
+ let callId = 0;
56
59
  /**
57
- * Execute the async function
60
+ * Cancel a pending debounced call, settling its promise with undefined
58
61
  */
59
- async function execute(...args) {
60
- // Clear any pending debounce
62
+ function cancelPending() {
61
63
  if (debounceTimer) {
62
64
  clearTimeout(debounceTimer);
63
65
  debounceTimer = undefined;
64
66
  }
67
+ pendingResolve?.(undefined);
68
+ pendingResolve = undefined;
69
+ }
70
+ /**
71
+ * Execute the async function
72
+ */
73
+ async function execute(...args) {
74
+ // Clear any pending debounce
75
+ cancelPending();
65
76
  // If debounce is set, delay execution
66
77
  if (debounceMs && debounceMs > 0) {
67
78
  return new Promise((resolve) => {
79
+ pendingResolve = resolve;
68
80
  debounceTimer = setTimeout(async () => {
81
+ debounceTimer = undefined;
82
+ pendingResolve = undefined;
69
83
  const result = await executeImmediate(...args);
70
84
  resolve(result);
71
85
  }, debounceMs);
@@ -77,12 +91,16 @@ export function useAsync(options) {
77
91
  * Execute immediately without debounce
78
92
  */
79
93
  async function executeImmediate(...args) {
94
+ const id = ++callId;
80
95
  lastArgs = args;
81
96
  isLoading = true;
82
97
  error = undefined;
83
98
  isExecuted = true;
84
99
  try {
85
100
  const result = await fn(...args);
101
+ // A newer call (or reset) superseded this one: leave state alone
102
+ if (id !== callId)
103
+ return result;
86
104
  data = result;
87
105
  if (onSuccess) {
88
106
  onSuccess(result);
@@ -93,6 +111,8 @@ export function useAsync(options) {
93
111
  return result;
94
112
  }
95
113
  catch (err) {
114
+ if (id !== callId)
115
+ return undefined;
96
116
  const normalizedError = err instanceof Error ? err : new Error(String(err));
97
117
  error = normalizedError;
98
118
  if (onError) {
@@ -104,7 +124,8 @@ export function useAsync(options) {
104
124
  return undefined;
105
125
  }
106
126
  finally {
107
- isLoading = false;
127
+ if (id === callId)
128
+ isLoading = false;
108
129
  }
109
130
  }
110
131
  /**
@@ -121,11 +142,9 @@ export function useAsync(options) {
121
142
  * Reset state to initial values
122
143
  */
123
144
  function reset() {
124
- // Clear any pending debounce
125
- if (debounceTimer) {
126
- clearTimeout(debounceTimer);
127
- debounceTimer = undefined;
128
- }
145
+ // Clear any pending debounce, and drop the result of any call in flight
146
+ cancelPending();
147
+ callId++;
129
148
  data = initialData;
130
149
  error = undefined;
131
150
  isLoading = false;
@@ -153,11 +172,11 @@ export function useAsync(options) {
153
172
  function clearError() {
154
173
  error = undefined;
155
174
  }
156
- // Execute immediately if requested
157
- if (immediate && immediateArgs) {
175
+ // Execute immediately if requested (zero-argument functions need no immediateArgs)
176
+ if (immediate) {
158
177
  // Use $effect-like behavior by queueing the execution
159
178
  queueMicrotask(() => {
160
- execute(...immediateArgs);
179
+ execute(...(immediateArgs ?? []));
161
180
  });
162
181
  }
163
182
  return {
@@ -46,8 +46,9 @@ import { toastStore } from '../stores/toast.svelte.js';
46
46
  */
47
47
  export function useForm(options) {
48
48
  const { schema, initialValues, onSubmit, onError, showToastOnError = true, resetOnSuccess = false, errorMessage, successMessage, } = options;
49
- // Deep clone to avoid reference issues
50
- const cloneInitialValues = () => JSON.parse(JSON.stringify(initialValues));
49
+ // Deep clone to avoid reference issues. $state.snapshot keeps Date, Map, Set
50
+ // and File values (a JSON round-trip would turn a Date into a string).
51
+ const cloneInitialValues = () => $state.snapshot(initialValues);
51
52
  // Reactive state using Svelte 5 runes
52
53
  let data = $state(cloneInitialValues());
53
54
  let errors = $state({});
@@ -145,14 +146,20 @@ export function useForm(options) {
145
146
  * Validate the entire form
146
147
  */
147
148
  function validate() {
149
+ return parseForm() !== null;
150
+ }
151
+ /**
152
+ * Validate the entire form, returning the parsed (transformed) data or null
153
+ */
154
+ function parseForm() {
148
155
  const result = schema.safeParse(data);
149
156
  if (result.success) {
150
157
  errors = {};
151
- return true;
158
+ return result.data;
152
159
  }
153
160
  else {
154
161
  errors = parseZodErrors(result.error);
155
- return false;
162
+ return null;
156
163
  }
157
164
  }
158
165
  /**
@@ -160,12 +167,17 @@ export function useForm(options) {
160
167
  */
161
168
  async function handleSubmit(event) {
162
169
  event?.preventDefault();
170
+ // Ignore a second submit (e.g. a double click) while one is in flight
171
+ if (isSubmitting)
172
+ return;
163
173
  // Clear previous errors
164
174
  errors = {};
165
175
  globalError = '';
166
176
  isSubmitted = true;
167
- // Validate all fields
168
- if (!validate()) {
177
+ // Validate all fields. onSubmit gets the schema's output, so transforms,
178
+ // coercions and defaults apply, as a plain object rather than the proxy.
179
+ const parsed = parseForm();
180
+ if (parsed === null) {
169
181
  const errorCount = Object.keys(errors).length;
170
182
  if (showToastOnError) {
171
183
  toastStore.error(errorMessage || `Please fix ${errorCount} validation error${errorCount > 1 ? 's' : ''}`);
@@ -174,7 +186,7 @@ export function useForm(options) {
174
186
  }
175
187
  isSubmitting = true;
176
188
  try {
177
- await onSubmit(data);
189
+ await onSubmit(parsed);
178
190
  if (successMessage) {
179
191
  toastStore.success(successMessage);
180
192
  }
@@ -75,8 +75,11 @@ export function usePersistedForm(options) {
75
75
  let draftRestored = $state(false);
76
76
  let lastSubmitTime = $state(0);
77
77
  let debounceTimer = null;
78
+ // Serialized persistable data last written (or deliberately not written).
79
+ // null after clearDraft, so the next edit is always saved.
80
+ let lastPersisted = null;
78
81
  // Store original initialValues for reset (before draft merge)
79
- const originalInitialValues = JSON.parse(JSON.stringify(initialValues));
82
+ const originalInitialValues = $state.snapshot(initialValues);
80
83
  /**
81
84
  * Load draft from localStorage
82
85
  */
@@ -104,6 +107,12 @@ export function usePersistedForm(options) {
104
107
  * Clear draft from localStorage
105
108
  */
106
109
  function clearDraft() {
110
+ // Drop a pending save, or it would write the draft back after clearing
111
+ if (debounceTimer) {
112
+ clearTimeout(debounceTimer);
113
+ debounceTimer = null;
114
+ }
115
+ lastPersisted = null;
107
116
  if (!isStorageAvailable())
108
117
  return;
109
118
  try {
@@ -127,9 +136,9 @@ export function usePersistedForm(options) {
127
136
  });
128
137
  }
129
138
  // Merge draft with initial values (keeps excluded fields at defaults)
130
- return { ...JSON.parse(JSON.stringify(initialValues)), ...draft };
139
+ return { ...$state.snapshot(initialValues), ...draft };
131
140
  }
132
- return JSON.parse(JSON.stringify(initialValues));
141
+ return $state.snapshot(initialValues);
133
142
  }
134
143
  /**
135
144
  * Wrapped onSubmit with cooldown and draft clearing
@@ -144,8 +153,10 @@ export function usePersistedForm(options) {
144
153
  try {
145
154
  await onSubmit(data);
146
155
  lastSubmitTime = now;
147
- // Clear draft on successful submission
156
+ // Clear draft on successful submission. Treat the start values as
157
+ // persisted so a resetOnSuccess back to them does not save a new draft.
148
158
  clearDraft();
159
+ lastPersisted = startSerialized;
149
160
  }
150
161
  catch (error) {
151
162
  // Reset cooldown on error to allow retry
@@ -154,16 +165,17 @@ export function usePersistedForm(options) {
154
165
  }
155
166
  }
156
167
  // Create base form with potentially restored initial values
168
+ const startValues = getInitialValues();
157
169
  const baseForm = useForm({
158
170
  ...baseOptions,
159
- initialValues: getInitialValues(),
171
+ initialValues: startValues,
160
172
  onSubmit: wrappedOnSubmit,
161
173
  });
162
174
  /**
163
175
  * Get data to persist (excludes specified fields)
164
176
  */
165
- function getDataToPersist() {
166
- const dataCopy = { ...baseForm.data };
177
+ function getDataToPersist(values = baseForm.data) {
178
+ const dataCopy = { ...values };
167
179
  for (const field of excludeFields) {
168
180
  delete dataCopy[field];
169
181
  }
@@ -192,13 +204,27 @@ export function usePersistedForm(options) {
192
204
  }
193
205
  }, debounceMs);
194
206
  }
207
+ // Edits made with bind:value never mark fields dirty, so changes are
208
+ // detected by content rather than by isDirty.
209
+ const startSerialized = JSON.stringify(getDataToPersist(startValues));
210
+ const originalSerialized = JSON.stringify(getDataToPersist(originalInitialValues));
211
+ lastPersisted = startSerialized;
195
212
  // Watch for form changes and save draft
196
213
  $effect(() => {
197
214
  // Access data to create dependency
198
- JSON.stringify(baseForm.data);
199
- if (baseForm.isDirty) {
215
+ const serialized = JSON.stringify(getDataToPersist());
216
+ if (serialized === lastPersisted)
217
+ return;
218
+ if (serialized === originalSerialized) {
219
+ // Edited back to the initial values: there is nothing to restore, so
220
+ // drop the stored draft (and any pending save) instead of leaving a
221
+ // stale one behind.
222
+ clearDraft();
223
+ }
224
+ else {
200
225
  saveDraft();
201
226
  }
227
+ lastPersisted = serialized;
202
228
  });
203
229
  /**
204
230
  * Extended reset that also clears the draft
@@ -209,9 +235,10 @@ export function usePersistedForm(options) {
209
235
  baseForm.reset();
210
236
  // Then overwrite data with original initialValues (not the draft-merged ones)
211
237
  // We modify the data object directly to avoid triggering markDirty
212
- Object.assign(baseForm.data, JSON.parse(JSON.stringify(originalInitialValues)));
238
+ Object.assign(baseForm.data, $state.snapshot(originalInitialValues));
213
239
  draftRestored = false;
214
240
  clearDraft();
241
+ lastPersisted = JSON.stringify(getDataToPersist());
215
242
  }
216
243
  /**
217
244
  * Dismiss the draft restored notification
@@ -98,6 +98,12 @@ declare class SidebarStore {
98
98
  * This bypasses the persisted state check but doesn't persist the changes
99
99
  */
100
100
  forceExpandSections(sectionIds: string[]): void;
101
+ /**
102
+ * Expand the sections and items that hold the current page so its active
103
+ * item is visible. Like a search expansion it is not persisted; unlike one
104
+ * it is kept when a search clears. A no-op when all are already expanded.
105
+ */
106
+ revealExpansion(sectionIds: string[], itemIds: string[]): void;
101
107
  /**
102
108
  * Undo search force-expansions, restoring the expansion state from before the
103
109
  * search (including any toggles made while it was active). Call when the
@@ -250,6 +250,21 @@ class SidebarStore {
250
250
  this.expandedSections = newSet;
251
251
  // Don't persist - this is temporary for search
252
252
  }
253
+ /**
254
+ * Expand the sections and items that hold the current page so its active
255
+ * item is visible. Like a search expansion it is not persisted; unlike one
256
+ * it is kept when a search clears. A no-op when all are already expanded.
257
+ */
258
+ revealExpansion(sectionIds, itemIds) {
259
+ const withAll = (set, ids) => ids.every((id) => set.has(id)) ? set : new Set([...set, ...ids]);
260
+ this.expandedSections = withAll(this.expandedSections, sectionIds);
261
+ this.expandedItems = withAll(this.expandedItems, itemIds);
262
+ const snap = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f");
263
+ if (snap) {
264
+ snap.sections = withAll(snap.sections, sectionIds);
265
+ snap.items = withAll(snap.items, itemIds);
266
+ }
267
+ }
253
268
  /**
254
269
  * Undo search force-expansions, restoring the expansion state from before the
255
270
  * search (including any toggles made while it was active). Call when the
@@ -7,6 +7,17 @@
7
7
  * - Proper timeout cleanup to prevent memory leaks
8
8
  * - Convenience methods for common toast types
9
9
  */
10
+ let fallbackIdCounter = 0;
11
+ /**
12
+ * crypto.randomUUID only exists in secure contexts (https, localhost), so
13
+ * fall back on plain-http origins such as a LAN IP or a staging host.
14
+ */
15
+ function createToastId() {
16
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
17
+ return crypto.randomUUID();
18
+ }
19
+ return `toast-${Date.now()}-${++fallbackIdCounter}`;
20
+ }
10
21
  class ToastStore {
11
22
  constructor() {
12
23
  this.toasts = $state([]);
@@ -19,7 +30,7 @@ class ToastStore {
19
30
  * @returns The generated toast id
20
31
  */
21
32
  add(toast) {
22
- const id = crypto.randomUUID();
33
+ const id = createToastId();
23
34
  const newToast = { ...toast, id };
24
35
  this.toasts = [...this.toasts, newToast];
25
36
  // Auto-dismiss unless duration is explicitly 0 (persistent)
@@ -77,7 +77,9 @@ export interface ParseResult {
77
77
  * - ISO: `2024-03-15`, `2024-03-15T14:30`
78
78
  * - US: `03/15/2024`, `3/15/24`, `03-15-2024`
79
79
  * - Natural: `Mar 15, 2024`, `March 15 2024`
80
- * - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`
80
+ * - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`, `Mar 15, 2024 at 2:30 PM`
81
+ *
82
+ * Out-of-range parts (`02/30/2024`, `25:99`) are rejected, not rolled over.
81
83
  *
82
84
  * @param input - The user-typed date string
83
85
  * @param options - Parsing options
@@ -104,6 +104,37 @@ export function isSameDay(date1, date2) {
104
104
  date1.getMonth() === date2.getMonth() &&
105
105
  date1.getDate() === date2.getDate());
106
106
  }
107
+ /**
108
+ * Build a local Date, or null when a part is out of range. `new Date()` rolls
109
+ * overflow into the next unit (Feb 30 -> Mar 1, 25:00 -> next day), so the
110
+ * date parts must round-trip and the time parts must be in range.
111
+ */
112
+ function buildDate(year, monthIndex, day, hour, minute) {
113
+ if (hour > 23 || minute > 59)
114
+ return null;
115
+ const date = new Date(year, monthIndex, day, hour, minute);
116
+ if (date.getFullYear() !== year || date.getMonth() !== monthIndex || date.getDate() !== day) {
117
+ return null;
118
+ }
119
+ return date;
120
+ }
121
+ /**
122
+ * Convert a typed hour (with optional AM/PM) to 0-23, or null when out of
123
+ * range for its clock (e.g. `13 PM`, `0 AM`).
124
+ */
125
+ function toHour24(hour, meridiem) {
126
+ if (!hour)
127
+ return 0;
128
+ const parsed = parseInt(hour);
129
+ if (!meridiem)
130
+ return parsed;
131
+ if (parsed < 1 || parsed > 12)
132
+ return null;
133
+ const isPm = meridiem.toUpperCase() === 'PM';
134
+ if (parsed === 12)
135
+ return isPm ? 12 : 0;
136
+ return isPm ? parsed + 12 : parsed;
137
+ }
107
138
  /**
108
139
  * Parse a user-typed date input string into a Date object.
109
140
  *
@@ -111,7 +142,9 @@ export function isSameDay(date1, date2) {
111
142
  * - ISO: `2024-03-15`, `2024-03-15T14:30`
112
143
  * - US: `03/15/2024`, `3/15/24`, `03-15-2024`
113
144
  * - Natural: `Mar 15, 2024`, `March 15 2024`
114
- * - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`
145
+ * - With time: `03/15/2024 2:30 PM`, `03/15/2024 14:30`, `Mar 15, 2024 at 2:30 PM`
146
+ *
147
+ * Out-of-range parts (`02/30/2024`, `25:99`) are rejected, not rolled over.
115
148
  *
116
149
  * @param input - The user-typed date string
117
150
  * @param options - Parsing options
@@ -128,17 +161,17 @@ export function parseDateInput(input, options) {
128
161
  }
129
162
  // Try parsing with various patterns
130
163
  let date = null;
131
- let hasTime = false;
164
+ let matched = false;
132
165
  // Pattern 1: ISO format (2024-03-15 or 2024-03-15T14:30)
133
166
  const isoMatch = trimmed.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
134
167
  if (isoMatch) {
135
168
  const [, year, month, day, hour, minute] = isoMatch;
136
- date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), hour ? parseInt(hour) : 0, minute ? parseInt(minute) : 0);
137
- hasTime = !!hour;
169
+ matched = true;
170
+ date = buildDate(parseInt(year), parseInt(month) - 1, parseInt(day), hour ? parseInt(hour) : 0, minute ? parseInt(minute) : 0);
138
171
  }
139
172
  // Pattern 2: US format with slashes or dashes (03/15/2024, 3/15/24, 03-15-2024)
140
- if (!date) {
141
- const usMatch = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})(?:\s+(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
173
+ if (!matched) {
174
+ const usMatch = trimmed.match(/^(\d{1,2})[/-](\d{1,2})[/-](\d{2,4})(?:\s+(?:at\s+)?(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
142
175
  if (usMatch) {
143
176
  const [, month, day, yearStr, hour, minute, meridiem] = usMatch;
144
177
  let year = parseInt(yearStr);
@@ -146,22 +179,16 @@ export function parseDateInput(input, options) {
146
179
  if (year < 100) {
147
180
  year += year < 50 ? 2000 : 1900;
148
181
  }
149
- let parsedHour = hour ? parseInt(hour) : 0;
150
- if (meridiem) {
151
- const upper = meridiem.toUpperCase();
152
- if (upper === 'PM' && parsedHour !== 12) {
153
- parsedHour += 12;
154
- }
155
- else if (upper === 'AM' && parsedHour === 12) {
156
- parsedHour = 0;
157
- }
158
- }
159
- date = new Date(year, parseInt(month) - 1, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
160
- hasTime = !!hour;
182
+ matched = true;
183
+ const parsedHour = toHour24(hour, meridiem);
184
+ date =
185
+ parsedHour === null
186
+ ? null
187
+ : buildDate(year, parseInt(month) - 1, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
161
188
  }
162
189
  }
163
190
  // Pattern 3: Natural format (Mar 15, 2024 or March 15 2024)
164
- if (!date) {
191
+ if (!matched) {
165
192
  const monthNames = {
166
193
  jan: 0,
167
194
  january: 0,
@@ -188,23 +215,16 @@ export function parseDateInput(input, options) {
188
215
  dec: 11,
189
216
  december: 11,
190
217
  };
191
- const naturalMatch = trimmed.match(/^([a-zA-Z]+)\s+(\d{1,2}),?\s+(\d{4})(?:\s+(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
218
+ const naturalMatch = trimmed.match(/^([a-zA-Z]+)\s+(\d{1,2}),?\s+(\d{4})(?:\s+(?:at\s+)?(\d{1,2}):(\d{2})(?:\s*(AM|PM))?)?$/i);
192
219
  if (naturalMatch) {
193
220
  const [, monthStr, day, year, hour, minute, meridiem] = naturalMatch;
194
221
  const monthNum = monthNames[monthStr.toLowerCase()];
195
222
  if (monthNum !== undefined) {
196
- let parsedHour = hour ? parseInt(hour) : 0;
197
- if (meridiem) {
198
- const upper = meridiem.toUpperCase();
199
- if (upper === 'PM' && parsedHour !== 12) {
200
- parsedHour += 12;
201
- }
202
- else if (upper === 'AM' && parsedHour === 12) {
203
- parsedHour = 0;
204
- }
205
- }
206
- date = new Date(parseInt(year), monthNum, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
207
- hasTime = !!hour;
223
+ const parsedHour = toHour24(hour, meridiem);
224
+ date =
225
+ parsedHour === null
226
+ ? null
227
+ : buildDate(parseInt(year), monthNum, parseInt(day), parsedHour, minute ? parseInt(minute) : 0);
208
228
  }
209
229
  }
210
230
  }
@@ -216,8 +236,8 @@ export function parseDateInput(input, options) {
216
236
  error: 'Invalid date format. Try MM/DD/YYYY or YYYY-MM-DD',
217
237
  };
218
238
  }
219
- // If dateOnly mode and time was provided, still accept it but set time to midnight
220
- if (options?.dateOnly && !hasTime) {
239
+ // In dateOnly mode a typed time is accepted but reset to midnight
240
+ if (options?.dateOnly) {
221
241
  date.setHours(0, 0, 0, 0);
222
242
  }
223
243
  return { date, isValid: true };
@@ -11,7 +11,8 @@
11
11
  export declare function generateId(prefix?: string): string;
12
12
  /**
13
13
  * Computes the aria-describedby attribute value based on hint and error states.
14
- * Returns undefined if neither hint nor error is present.
14
+ * Returns undefined if neither hint nor error is present. The hint is left out
15
+ * while an error shows, since form fields render the error in its place.
15
16
  *
16
17
  * @param hintId - The ID of the hint element
17
18
  * @param errorId - The ID of the error element
@@ -13,7 +13,8 @@ export function generateId(prefix = 'field') {
13
13
  }
14
14
  /**
15
15
  * Computes the aria-describedby attribute value based on hint and error states.
16
- * Returns undefined if neither hint nor error is present.
16
+ * Returns undefined if neither hint nor error is present. The hint is left out
17
+ * while an error shows, since form fields render the error in its place.
17
18
  *
18
19
  * @param hintId - The ID of the hint element
19
20
  * @param errorId - The ID of the error element
@@ -23,7 +24,7 @@ export function generateId(prefix = 'field') {
23
24
  */
24
25
  export function computeDescribedBy(hintId, errorId, hasHint, hasError) {
25
26
  const ids = [];
26
- if (hasHint && hintId)
27
+ if (hasHint && !hasError && hintId)
27
28
  ids.push(hintId);
28
29
  if (hasError && errorId)
29
30
  ids.push(errorId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/theme-svelte",
3
- "version": "1.22.1",
3
+ "version": "1.22.3",
4
4
  "description": "Svelte components for the Classic theme system",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",