@classic-homes/theme-svelte 1.22.0 → 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/Alert.svelte +8 -10
- package/dist/lib/components/Combobox.svelte +2 -2
- package/dist/lib/components/DataPanel.svelte +19 -5
- 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/HeaderSearch.svelte +18 -10
- 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/Skeleton.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/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
- package/dist/lib/components/layout/Footer.svelte +6 -6
- package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
- package/dist/lib/components/layout/Sidebar.svelte +53 -21
- package/dist/lib/components/media/Image.svelte.d.ts +1 -1
- package/dist/lib/components/media/Lightbox.svelte +13 -9
- package/dist/lib/components/media/MediaCarousel.svelte +5 -0
- package/dist/lib/components/media/MediaGrid.svelte +4 -1
- package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
- package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
- package/dist/lib/components/studio/InsertPalette.svelte +4 -1
- package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
- package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
- package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
- package/dist/lib/composables/useAsync.svelte.js +33 -14
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/composables/useForm.svelte.js +19 -7
- package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
- package/dist/lib/notices/sanitize.js +10 -3
- package/dist/lib/notices/store.d.ts +3 -3
- package/dist/lib/notices/store.js +14 -2
- package/dist/lib/notices/types.d.ts +8 -8
- package/dist/lib/notices/types.js +5 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +12 -0
- package/dist/lib/stores/sidebar.svelte.js +76 -26
- 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/dist/lib/utils/media.js +4 -3
- package/dist/lib/utils/url.d.ts +10 -0
- package/dist/lib/utils/url.js +21 -0
- package/dist/lib/utils.js +14 -1
- package/package.json +5 -4
- package/dist/__tests__/FileUpload.test.js +0 -45
- package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
- package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
- package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
- package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
- package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
- package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
- package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
- package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
- package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
- package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
- package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
- package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
- package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
- package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
- package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
- package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
- package/dist/__tests__/action-header.test.js +0 -150
- package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
- package/dist/__tests__/code-field.test.js +0 -65
- package/dist/__tests__/control-text-color.test.js +0 -29
- package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
- package/dist/__tests__/data-panel-viewport.test.js +0 -45
- package/dist/__tests__/dropdown-trigger.test.js +0 -39
- package/dist/__tests__/header-search.test.js +0 -90
- package/dist/__tests__/media-components.test.js +0 -206
- package/dist/__tests__/media-hls.test.js +0 -273
- package/dist/__tests__/media-matterport.test.js +0 -296
- package/dist/__tests__/media-players.test.js +0 -244
- package/dist/__tests__/media-scrubber.test.js +0 -44
- package/dist/__tests__/media-utils.test.js +0 -427
- package/dist/__tests__/nav-access.test.js +0 -145
- package/dist/__tests__/page-header.test.js +0 -36
- package/dist/__tests__/public-layout-drawer.test.js +0 -116
- package/dist/__tests__/setup.js +0 -56
- package/dist/__tests__/sidebar-collapse.test.js +0 -58
- package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
- package/dist/__tests__/sidebar-expansion.test.js +0 -59
- package/dist/__tests__/sidebar-items.test.js +0 -59
- package/dist/__tests__/sidebar-mobile.test.js +0 -86
- package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
- package/dist/__tests__/studio.test.js +0 -240
|
@@ -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
|
-
|
|
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
|
|
158
|
+
return result.data;
|
|
152
159
|
}
|
|
153
160
|
else {
|
|
154
161
|
errors = parseZodErrors(result.error);
|
|
155
|
-
return
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
@@ -15,8 +15,10 @@ import { BROWSER } from 'esm-env';
|
|
|
15
15
|
const ALLOWED_TAGS = ['a', 'b', 'br', 'em', 'i', 'p', 'span', 'strong', 'u', 'ul', 'ol', 'li'];
|
|
16
16
|
/**
|
|
17
17
|
* Allowed HTML attributes for notice messages.
|
|
18
|
+
* `class` is deliberately excluded: with Tailwind on the page, arbitrary
|
|
19
|
+
* utilities (`fixed inset-0 z-[9999]`) would let a message overlay the app.
|
|
18
20
|
*/
|
|
19
|
-
const ALLOWED_ATTR = ['href', 'target', 'rel'
|
|
21
|
+
const ALLOWED_ATTR = ['href', 'target', 'rel'];
|
|
20
22
|
/**
|
|
21
23
|
* Escape HTML entities for safe rendering when DOMPurify isn't available.
|
|
22
24
|
* This is a fallback for server-side rendering.
|
|
@@ -31,6 +33,12 @@ function escapeHtml(html) {
|
|
|
31
33
|
}
|
|
32
34
|
// Cached DOMPurify instance (only loaded on client)
|
|
33
35
|
let purify = null;
|
|
36
|
+
/** Force rel="noopener noreferrer" on any element that kept a target. */
|
|
37
|
+
function enforceSafeRel(node) {
|
|
38
|
+
if (node.hasAttribute?.('target')) {
|
|
39
|
+
node.setAttribute('rel', 'noopener noreferrer');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
34
42
|
/**
|
|
35
43
|
* Initialize DOMPurify on the client.
|
|
36
44
|
* Must be called before sanitizeHtml on the client side.
|
|
@@ -38,6 +46,7 @@ let purify = null;
|
|
|
38
46
|
export async function initSanitizer() {
|
|
39
47
|
if (BROWSER && !purify) {
|
|
40
48
|
const DOMPurify = await import('isomorphic-dompurify');
|
|
49
|
+
DOMPurify.default.addHook('afterSanitizeAttributes', enforceSafeRel);
|
|
41
50
|
purify = DOMPurify.default;
|
|
42
51
|
}
|
|
43
52
|
}
|
|
@@ -71,7 +80,5 @@ export function sanitizeHtml(html) {
|
|
|
71
80
|
return purify.sanitize(html, {
|
|
72
81
|
ALLOWED_TAGS,
|
|
73
82
|
ALLOWED_ATTR,
|
|
74
|
-
// Ensure links open safely
|
|
75
|
-
ADD_ATTR: ['target'],
|
|
76
83
|
});
|
|
77
84
|
}
|
|
@@ -77,8 +77,8 @@ export declare const activeNotices: import("svelte/store").Readable<{
|
|
|
77
77
|
location: "banner" | "both" | "card";
|
|
78
78
|
pages: string[];
|
|
79
79
|
link?: {
|
|
80
|
-
href: string;
|
|
81
80
|
text: string;
|
|
81
|
+
href: string;
|
|
82
82
|
external?: boolean | undefined;
|
|
83
83
|
} | undefined;
|
|
84
84
|
title?: string | undefined;
|
|
@@ -98,8 +98,8 @@ export declare const bannerNotices: import("svelte/store").Readable<{
|
|
|
98
98
|
location: "banner" | "both" | "card";
|
|
99
99
|
pages: string[];
|
|
100
100
|
link?: {
|
|
101
|
-
href: string;
|
|
102
101
|
text: string;
|
|
102
|
+
href: string;
|
|
103
103
|
external?: boolean | undefined;
|
|
104
104
|
} | undefined;
|
|
105
105
|
title?: string | undefined;
|
|
@@ -119,8 +119,8 @@ export declare const cardNotices: import("svelte/store").Readable<{
|
|
|
119
119
|
location: "banner" | "both" | "card";
|
|
120
120
|
pages: string[];
|
|
121
121
|
link?: {
|
|
122
|
-
href: string;
|
|
123
122
|
text: string;
|
|
123
|
+
href: string;
|
|
124
124
|
external?: boolean | undefined;
|
|
125
125
|
} | undefined;
|
|
126
126
|
title?: string | undefined;
|
|
@@ -36,10 +36,22 @@ function getDismissedNotices() {
|
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
38
38
|
console.warn('[notices] Failed to parse dismissed notices from localStorage', { error });
|
|
39
|
-
|
|
39
|
+
removeDismissedStorage();
|
|
40
40
|
}
|
|
41
41
|
return new Set();
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Remove the dismissed-notices key. Storage itself can throw (blocked cookies,
|
|
45
|
+
* sandboxed iframes), so this never propagates.
|
|
46
|
+
*/
|
|
47
|
+
function removeDismissedStorage() {
|
|
48
|
+
try {
|
|
49
|
+
localStorage.removeItem(DISMISSED_STORAGE_KEY);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Storage unavailable - nothing to clear
|
|
53
|
+
}
|
|
54
|
+
}
|
|
43
55
|
/**
|
|
44
56
|
* Save dismissed notice IDs to localStorage
|
|
45
57
|
*/
|
|
@@ -166,7 +178,7 @@ export function dismissNotice(noticeId) {
|
|
|
166
178
|
export function clearDismissedNotices() {
|
|
167
179
|
dismissedIds.set(new Set());
|
|
168
180
|
if (BROWSER) {
|
|
169
|
-
|
|
181
|
+
removeDismissedStorage();
|
|
170
182
|
}
|
|
171
183
|
}
|
|
172
184
|
/**
|
|
@@ -23,15 +23,15 @@ export type NoticePage = '*' | string;
|
|
|
23
23
|
*/
|
|
24
24
|
export declare const NoticeLinkSchema: z.ZodObject<{
|
|
25
25
|
text: z.ZodString;
|
|
26
|
-
href: z.ZodString
|
|
26
|
+
href: z.ZodEffects<z.ZodString, string, string>;
|
|
27
27
|
external: z.ZodOptional<z.ZodBoolean>;
|
|
28
28
|
}, "strip", z.ZodTypeAny, {
|
|
29
|
-
href: string;
|
|
30
29
|
text: string;
|
|
30
|
+
href: string;
|
|
31
31
|
external?: boolean | undefined;
|
|
32
32
|
}, {
|
|
33
|
-
href: string;
|
|
34
33
|
text: string;
|
|
34
|
+
href: string;
|
|
35
35
|
external?: boolean | undefined;
|
|
36
36
|
}>;
|
|
37
37
|
/**
|
|
@@ -61,15 +61,15 @@ export declare const NoticeSchema: z.ZodObject<{
|
|
|
61
61
|
/** Optional link for "Learn more" or CTA */
|
|
62
62
|
link: z.ZodOptional<z.ZodObject<{
|
|
63
63
|
text: z.ZodString;
|
|
64
|
-
href: z.ZodString
|
|
64
|
+
href: z.ZodEffects<z.ZodString, string, string>;
|
|
65
65
|
external: z.ZodOptional<z.ZodBoolean>;
|
|
66
66
|
}, "strip", z.ZodTypeAny, {
|
|
67
|
-
href: string;
|
|
68
67
|
text: string;
|
|
68
|
+
href: string;
|
|
69
69
|
external?: boolean | undefined;
|
|
70
70
|
}, {
|
|
71
|
-
href: string;
|
|
72
71
|
text: string;
|
|
72
|
+
href: string;
|
|
73
73
|
external?: boolean | undefined;
|
|
74
74
|
}>>;
|
|
75
75
|
/** Whether the notice is enabled (quick toggle without removing) */
|
|
@@ -81,8 +81,8 @@ export declare const NoticeSchema: z.ZodObject<{
|
|
|
81
81
|
location: "banner" | "both" | "card";
|
|
82
82
|
pages: string[];
|
|
83
83
|
link?: {
|
|
84
|
-
href: string;
|
|
85
84
|
text: string;
|
|
85
|
+
href: string;
|
|
86
86
|
external?: boolean | undefined;
|
|
87
87
|
} | undefined;
|
|
88
88
|
title?: string | undefined;
|
|
@@ -98,8 +98,8 @@ export declare const NoticeSchema: z.ZodObject<{
|
|
|
98
98
|
location: "banner" | "both" | "card";
|
|
99
99
|
pages: string[];
|
|
100
100
|
link?: {
|
|
101
|
-
href: string;
|
|
102
101
|
text: string;
|
|
102
|
+
href: string;
|
|
103
103
|
external?: boolean | undefined;
|
|
104
104
|
} | undefined;
|
|
105
105
|
title?: string | undefined;
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* as banners at the top of pages or as cards within form pages.
|
|
6
6
|
*/
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import { safeHref } from '../utils/url.js';
|
|
8
9
|
/**
|
|
9
10
|
* Zod schema for notice link
|
|
10
11
|
*/
|
|
11
12
|
export const NoticeLinkSchema = z.object({
|
|
12
13
|
text: z.string().min(1, 'Link text is required'),
|
|
13
|
-
href: z
|
|
14
|
+
href: z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1, 'Link href is required')
|
|
17
|
+
.refine((href) => safeHref(href) !== null, 'Link href must be relative or http(s)/mailto/tel'),
|
|
14
18
|
external: z.boolean().optional(),
|
|
15
19
|
});
|
|
16
20
|
/**
|
|
@@ -98,6 +98,18 @@ 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;
|
|
107
|
+
/**
|
|
108
|
+
* Undo search force-expansions, restoring the expansion state from before the
|
|
109
|
+
* search (including any toggles made while it was active). Call when the
|
|
110
|
+
* search query is cleared; a no-op when no search expanded anything.
|
|
111
|
+
*/
|
|
112
|
+
restoreSearchExpansion(): void;
|
|
101
113
|
/**
|
|
102
114
|
* Set callback for when pinned items change (for external persistence)
|
|
103
115
|
*/
|
|
@@ -16,7 +16,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
16
16
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
17
17
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
18
18
|
};
|
|
19
|
-
var _SidebarStore_instances, _SidebarStore_initialized, _SidebarStore_loadedFromStorage, _SidebarStore_loadedExpansionFromStorage, _SidebarStore_loadedPinnedFromStorage, _SidebarStore_onPinnedChange, _SidebarStore_appId, _SidebarStore_desktopOpen, _SidebarStore_getStorageKey, _SidebarStore_persist, _SidebarStore_persistExpansion, _SidebarStore_persistPinned, _SidebarStore_persistDesktopState;
|
|
19
|
+
var _SidebarStore_instances, _SidebarStore_initialized, _SidebarStore_loadedFromStorage, _SidebarStore_loadedExpansionFromStorage, _SidebarStore_loadedPinnedFromStorage, _SidebarStore_onPinnedChange, _SidebarStore_appId, _SidebarStore_desktopOpen, _SidebarStore_searchSnapshot, _SidebarStore_getStorageKey, _SidebarStore_persist, _SidebarStore_persistExpansion, _SidebarStore_persistPinned, _SidebarStore_persistDesktopState, _SidebarStore_takeSearchSnapshot;
|
|
20
20
|
import { matchesAccess } from '../utils/nav-access.js';
|
|
21
21
|
const STORAGE_PREFIX = 'classic-theme-sidebar';
|
|
22
22
|
class SidebarStore {
|
|
@@ -43,6 +43,9 @@ class SidebarStore {
|
|
|
43
43
|
// mobile drawer (which must always start closed and never persists) can't
|
|
44
44
|
// clobber it. Restored when leaving mobile mode.
|
|
45
45
|
_SidebarStore_desktopOpen.set(this, null);
|
|
46
|
+
// Expansion state from before a search force-expanded everything; restored
|
|
47
|
+
// when the search clears. Null while no search is active.
|
|
48
|
+
_SidebarStore_searchSnapshot.set(this, null);
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* Whether the store has been initialized
|
|
@@ -87,6 +90,7 @@ class SidebarStore {
|
|
|
87
90
|
__classPrivateFieldSet(this, _SidebarStore_loadedFromStorage, false, "f");
|
|
88
91
|
__classPrivateFieldSet(this, _SidebarStore_loadedExpansionFromStorage, false, "f");
|
|
89
92
|
__classPrivateFieldSet(this, _SidebarStore_loadedPinnedFromStorage, false, "f");
|
|
93
|
+
__classPrivateFieldSet(this, _SidebarStore_searchSnapshot, null, "f");
|
|
90
94
|
try {
|
|
91
95
|
// Load sidebar open state. On mobile only the desktop preference is
|
|
92
96
|
// recorded — the drawer must never open on first paint.
|
|
@@ -176,36 +180,24 @@ class SidebarStore {
|
|
|
176
180
|
* Toggle section expansion state. When expanding, sections in `collapseOnExpand` close.
|
|
177
181
|
*/
|
|
178
182
|
toggleSection(sectionId, collapseOnExpand = []) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
newSet.add(sectionId);
|
|
185
|
-
// Accordion behaviour: opening one closes these (its siblings)
|
|
186
|
-
for (const id of collapseOnExpand)
|
|
187
|
-
if (id !== sectionId)
|
|
188
|
-
newSet.delete(id);
|
|
183
|
+
const expand = !this.expandedSections.has(sectionId);
|
|
184
|
+
this.expandedSections = applyToggle(this.expandedSections, sectionId, expand, collapseOnExpand);
|
|
185
|
+
if (__classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f")) {
|
|
186
|
+
const snap = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f");
|
|
187
|
+
snap.sections = applyToggle(snap.sections, sectionId, expand, collapseOnExpand);
|
|
189
188
|
}
|
|
190
|
-
this.expandedSections = newSet;
|
|
191
189
|
__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_persistExpansion).call(this);
|
|
192
190
|
}
|
|
193
191
|
/**
|
|
194
192
|
* Toggle item expansion state. When expanding, items in `collapseOnExpand` close.
|
|
195
193
|
*/
|
|
196
194
|
toggleItem(itemId, collapseOnExpand = []) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
const expand = !this.expandedItems.has(itemId);
|
|
196
|
+
this.expandedItems = applyToggle(this.expandedItems, itemId, expand, collapseOnExpand);
|
|
197
|
+
if (__classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f")) {
|
|
198
|
+
const snap = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f");
|
|
199
|
+
snap.items = applyToggle(snap.items, itemId, expand, collapseOnExpand);
|
|
200
200
|
}
|
|
201
|
-
else {
|
|
202
|
-
newSet.add(itemId);
|
|
203
|
-
// Accordion behaviour: opening one closes these (its siblings)
|
|
204
|
-
for (const id of collapseOnExpand)
|
|
205
|
-
if (id !== itemId)
|
|
206
|
-
newSet.delete(id);
|
|
207
|
-
}
|
|
208
|
-
this.expandedItems = newSet;
|
|
209
201
|
__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_persistExpansion).call(this);
|
|
210
202
|
}
|
|
211
203
|
/**
|
|
@@ -241,6 +233,7 @@ class SidebarStore {
|
|
|
241
233
|
* This bypasses the persisted state check but doesn't persist the changes
|
|
242
234
|
*/
|
|
243
235
|
forceExpandItems(itemIds) {
|
|
236
|
+
__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_takeSearchSnapshot).call(this);
|
|
244
237
|
const newSet = new Set(this.expandedItems);
|
|
245
238
|
itemIds.forEach((id) => newSet.add(id));
|
|
246
239
|
this.expandedItems = newSet;
|
|
@@ -251,11 +244,40 @@ class SidebarStore {
|
|
|
251
244
|
* This bypasses the persisted state check but doesn't persist the changes
|
|
252
245
|
*/
|
|
253
246
|
forceExpandSections(sectionIds) {
|
|
247
|
+
__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_takeSearchSnapshot).call(this);
|
|
254
248
|
const newSet = new Set(this.expandedSections);
|
|
255
249
|
sectionIds.forEach((id) => newSet.add(id));
|
|
256
250
|
this.expandedSections = newSet;
|
|
257
251
|
// Don't persist - this is temporary for search
|
|
258
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
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Undo search force-expansions, restoring the expansion state from before the
|
|
270
|
+
* search (including any toggles made while it was active). Call when the
|
|
271
|
+
* search query is cleared; a no-op when no search expanded anything.
|
|
272
|
+
*/
|
|
273
|
+
restoreSearchExpansion() {
|
|
274
|
+
const snap = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f");
|
|
275
|
+
if (!snap)
|
|
276
|
+
return;
|
|
277
|
+
__classPrivateFieldSet(this, _SidebarStore_searchSnapshot, null, "f");
|
|
278
|
+
this.expandedSections = snap.sections;
|
|
279
|
+
this.expandedItems = snap.items;
|
|
280
|
+
}
|
|
259
281
|
// ============================================
|
|
260
282
|
// Pinning Methods
|
|
261
283
|
// ============================================
|
|
@@ -353,7 +375,7 @@ class SidebarStore {
|
|
|
353
375
|
};
|
|
354
376
|
}
|
|
355
377
|
}
|
|
356
|
-
_SidebarStore_initialized = new WeakMap(), _SidebarStore_loadedFromStorage = new WeakMap(), _SidebarStore_loadedExpansionFromStorage = new WeakMap(), _SidebarStore_loadedPinnedFromStorage = new WeakMap(), _SidebarStore_onPinnedChange = new WeakMap(), _SidebarStore_appId = new WeakMap(), _SidebarStore_desktopOpen = new WeakMap(), _SidebarStore_instances = new WeakSet(), _SidebarStore_getStorageKey = function _SidebarStore_getStorageKey(suffix) {
|
|
378
|
+
_SidebarStore_initialized = new WeakMap(), _SidebarStore_loadedFromStorage = new WeakMap(), _SidebarStore_loadedExpansionFromStorage = new WeakMap(), _SidebarStore_loadedPinnedFromStorage = new WeakMap(), _SidebarStore_onPinnedChange = new WeakMap(), _SidebarStore_appId = new WeakMap(), _SidebarStore_desktopOpen = new WeakMap(), _SidebarStore_searchSnapshot = new WeakMap(), _SidebarStore_instances = new WeakSet(), _SidebarStore_getStorageKey = function _SidebarStore_getStorageKey(suffix) {
|
|
357
379
|
return __classPrivateFieldGet(this, _SidebarStore_appId, "f")
|
|
358
380
|
? `${STORAGE_PREFIX}-${__classPrivateFieldGet(this, _SidebarStore_appId, "f")}-${suffix}`
|
|
359
381
|
: `${STORAGE_PREFIX}-${suffix}`;
|
|
@@ -372,9 +394,16 @@ _SidebarStore_initialized = new WeakMap(), _SidebarStore_loadedFromStorage = new
|
|
|
372
394
|
}, _SidebarStore_persistExpansion = function _SidebarStore_persistExpansion() {
|
|
373
395
|
if (typeof window === 'undefined')
|
|
374
396
|
return;
|
|
397
|
+
// During a search the live sets hold temporary force-expansions; persist
|
|
398
|
+
// the user's real state instead.
|
|
399
|
+
const sections = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f")?.sections ?? this.expandedSections;
|
|
400
|
+
const items = __classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f")?.items ?? this.expandedItems;
|
|
375
401
|
try {
|
|
376
|
-
localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'sections'), JSON.stringify([...
|
|
377
|
-
localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'items'), JSON.stringify([...
|
|
402
|
+
localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'sections'), JSON.stringify([...sections]));
|
|
403
|
+
localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'items'), JSON.stringify([...items]));
|
|
404
|
+
// The keys now exist: defaults applied "only when nothing is persisted"
|
|
405
|
+
// must stop, or they undo the user's toggle (same as #persist).
|
|
406
|
+
__classPrivateFieldSet(this, _SidebarStore_loadedExpansionFromStorage, true, "f");
|
|
378
407
|
}
|
|
379
408
|
catch {
|
|
380
409
|
// localStorage not available
|
|
@@ -395,5 +424,26 @@ _SidebarStore_initialized = new WeakMap(), _SidebarStore_loadedFromStorage = new
|
|
|
395
424
|
return;
|
|
396
425
|
__classPrivateFieldSet(this, _SidebarStore_desktopOpen, this.isOpen, "f");
|
|
397
426
|
__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_persist).call(this);
|
|
427
|
+
}, _SidebarStore_takeSearchSnapshot = function _SidebarStore_takeSearchSnapshot() {
|
|
428
|
+
if (__classPrivateFieldGet(this, _SidebarStore_searchSnapshot, "f"))
|
|
429
|
+
return;
|
|
430
|
+
__classPrivateFieldSet(this, _SidebarStore_searchSnapshot, {
|
|
431
|
+
sections: new Set(this.expandedSections),
|
|
432
|
+
items: new Set(this.expandedItems),
|
|
433
|
+
}, "f");
|
|
398
434
|
};
|
|
435
|
+
/** Copy of `set` with `id` expanded or collapsed; expanding closes `collapseOnExpand` (accordion). */
|
|
436
|
+
function applyToggle(set, id, expand, collapseOnExpand) {
|
|
437
|
+
const next = new Set(set);
|
|
438
|
+
if (expand) {
|
|
439
|
+
next.add(id);
|
|
440
|
+
for (const other of collapseOnExpand)
|
|
441
|
+
if (other !== id)
|
|
442
|
+
next.delete(other);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
next.delete(id);
|
|
446
|
+
}
|
|
447
|
+
return next;
|
|
448
|
+
}
|
|
399
449
|
export const sidebarStore = new SidebarStore();
|
|
@@ -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
|