@classic-homes/theme-svelte 1.22.1 → 1.22.2
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/dist/lib/components/Combobox.svelte +2 -2
- package/dist/lib/components/DataTable.svelte +15 -6
- package/dist/lib/components/DataTable.svelte.d.ts +1 -1
- package/dist/lib/components/DateTimePicker.svelte +31 -34
- package/dist/lib/components/DropdownMenu.svelte +52 -30
- package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
- package/dist/lib/components/FileUpload.svelte +5 -19
- package/dist/lib/components/MultiSelect.svelte +2 -2
- package/dist/lib/components/NumberInput.svelte +15 -2
- package/dist/lib/components/Select.svelte +19 -5
- package/dist/lib/components/Select.svelte.d.ts +4 -0
- package/dist/lib/components/Signature.svelte +15 -4
- package/dist/lib/components/Signature.svelte.d.ts +1 -1
- package/dist/lib/components/Slider.svelte +8 -6
- package/dist/lib/components/Tabs.svelte +10 -12
- package/dist/lib/components/Toast.svelte +12 -12
- package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
- package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
- package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
- package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
- package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
- package/dist/lib/components/layout/Sidebar.svelte +28 -0
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
- package/dist/lib/stores/sidebar.svelte.js +15 -0
- package/dist/lib/stores/toast.svelte.js +12 -1
- package/dist/lib/utils/date.d.ts +3 -1
- package/dist/lib/utils/date.js +54 -34
- package/dist/lib/utils/form.d.ts +2 -1
- package/dist/lib/utils/form.js +3 -2
- package/package.json +1 -1
|
@@ -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 =
|
|
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 {
|
|
139
|
+
return { ...$state.snapshot(initialValues), ...draft };
|
|
131
140
|
}
|
|
132
|
-
return
|
|
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:
|
|
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 = { ...
|
|
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(
|
|
199
|
-
if (
|
|
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,
|
|
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 =
|
|
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)
|
package/dist/lib/utils/date.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/utils/date.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
137
|
-
|
|
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 (!
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 (!
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
parsedHour
|
|
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
|
-
//
|
|
220
|
-
if (options?.dateOnly
|
|
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 };
|
package/dist/lib/utils/form.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/utils/form.js
CHANGED
|
@@ -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);
|