@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.
Files changed (110) hide show
  1. package/dist/lib/components/Alert.svelte +8 -10
  2. package/dist/lib/components/Combobox.svelte +2 -2
  3. package/dist/lib/components/DataPanel.svelte +19 -5
  4. package/dist/lib/components/DataTable.svelte +15 -6
  5. package/dist/lib/components/DataTable.svelte.d.ts +1 -1
  6. package/dist/lib/components/DateTimePicker.svelte +31 -34
  7. package/dist/lib/components/DropdownMenu.svelte +52 -30
  8. package/dist/lib/components/DropdownMenu.svelte.d.ts +5 -0
  9. package/dist/lib/components/FileUpload.svelte +5 -19
  10. package/dist/lib/components/HeaderSearch.svelte +18 -10
  11. package/dist/lib/components/MultiSelect.svelte +2 -2
  12. package/dist/lib/components/NumberInput.svelte +15 -2
  13. package/dist/lib/components/Select.svelte +19 -5
  14. package/dist/lib/components/Select.svelte.d.ts +4 -0
  15. package/dist/lib/components/Signature.svelte +15 -4
  16. package/dist/lib/components/Signature.svelte.d.ts +1 -1
  17. package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
  18. package/dist/lib/components/Slider.svelte +8 -6
  19. package/dist/lib/components/Tabs.svelte +10 -12
  20. package/dist/lib/components/Toast.svelte +12 -12
  21. package/dist/lib/components/data-table/DataTableColumnVisibility.svelte +24 -26
  22. package/dist/lib/components/data-table/DataTableEditableCell.svelte +31 -18
  23. package/dist/lib/components/data-table/DataTableEditableCell.svelte.d.ts +0 -7
  24. package/dist/lib/components/data-table/DataTablePagination.svelte +5 -1
  25. package/dist/lib/components/data-table/DataTableVirtual.svelte +4 -7
  26. package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +3 -0
  27. package/dist/lib/components/data-table/createDataTable.svelte.d.ts +6 -1
  28. package/dist/lib/components/data-table/createDataTable.svelte.js +22 -9
  29. package/dist/lib/components/layout/AppShell.svelte +1 -1
  30. package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
  31. package/dist/lib/components/layout/Footer.svelte +6 -6
  32. package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
  33. package/dist/lib/components/layout/Sidebar.svelte +53 -21
  34. package/dist/lib/components/media/Image.svelte.d.ts +1 -1
  35. package/dist/lib/components/media/Lightbox.svelte +13 -9
  36. package/dist/lib/components/media/MediaCarousel.svelte +5 -0
  37. package/dist/lib/components/media/MediaGrid.svelte +4 -1
  38. package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
  39. package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
  40. package/dist/lib/components/studio/InsertPalette.svelte +4 -1
  41. package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
  42. package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
  43. package/dist/lib/composables/useAsync.svelte.d.ts +1 -1
  44. package/dist/lib/composables/useAsync.svelte.js +33 -14
  45. package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
  46. package/dist/lib/composables/useForm.svelte.js +19 -7
  47. package/dist/lib/composables/usePersistedForm.svelte.js +37 -10
  48. package/dist/lib/notices/sanitize.js +10 -3
  49. package/dist/lib/notices/store.d.ts +3 -3
  50. package/dist/lib/notices/store.js +14 -2
  51. package/dist/lib/notices/types.d.ts +8 -8
  52. package/dist/lib/notices/types.js +5 -1
  53. package/dist/lib/stores/sidebar.svelte.d.ts +12 -0
  54. package/dist/lib/stores/sidebar.svelte.js +76 -26
  55. package/dist/lib/stores/toast.svelte.js +12 -1
  56. package/dist/lib/utils/date.d.ts +3 -1
  57. package/dist/lib/utils/date.js +54 -34
  58. package/dist/lib/utils/form.d.ts +2 -1
  59. package/dist/lib/utils/form.js +3 -2
  60. package/dist/lib/utils/media.js +4 -3
  61. package/dist/lib/utils/url.d.ts +10 -0
  62. package/dist/lib/utils/url.js +21 -0
  63. package/dist/lib/utils.js +14 -1
  64. package/package.json +5 -4
  65. package/dist/__tests__/FileUpload.test.js +0 -45
  66. package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
  67. package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
  68. package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
  69. package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
  70. package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
  71. package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
  72. package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
  73. package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
  74. package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
  75. package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
  76. package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
  77. package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
  78. package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
  79. package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
  80. package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
  81. package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
  82. package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
  83. package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
  84. package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
  85. package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
  86. package/dist/__tests__/action-header.test.js +0 -150
  87. package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
  88. package/dist/__tests__/code-field.test.js +0 -65
  89. package/dist/__tests__/control-text-color.test.js +0 -29
  90. package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
  91. package/dist/__tests__/data-panel-viewport.test.js +0 -45
  92. package/dist/__tests__/dropdown-trigger.test.js +0 -39
  93. package/dist/__tests__/header-search.test.js +0 -90
  94. package/dist/__tests__/media-components.test.js +0 -206
  95. package/dist/__tests__/media-hls.test.js +0 -273
  96. package/dist/__tests__/media-matterport.test.js +0 -296
  97. package/dist/__tests__/media-players.test.js +0 -244
  98. package/dist/__tests__/media-scrubber.test.js +0 -44
  99. package/dist/__tests__/media-utils.test.js +0 -427
  100. package/dist/__tests__/nav-access.test.js +0 -145
  101. package/dist/__tests__/page-header.test.js +0 -36
  102. package/dist/__tests__/public-layout-drawer.test.js +0 -116
  103. package/dist/__tests__/setup.js +0 -56
  104. package/dist/__tests__/sidebar-collapse.test.js +0 -58
  105. package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
  106. package/dist/__tests__/sidebar-expansion.test.js +0 -59
  107. package/dist/__tests__/sidebar-items.test.js +0 -59
  108. package/dist/__tests__/sidebar-mobile.test.js +0 -86
  109. package/dist/__tests__/studio-canvas-item.svelte.test.js +0 -98
  110. 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
- 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
@@ -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', 'class'];
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
- localStorage.removeItem(DISMISSED_STORAGE_KEY);
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
- localStorage.removeItem(DISMISSED_STORAGE_KEY);
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.string().min(1, 'Link href is required'),
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 newSet = new Set(this.expandedSections);
180
- if (newSet.has(sectionId)) {
181
- newSet.delete(sectionId);
182
- }
183
- else {
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 newSet = new Set(this.expandedItems);
198
- if (newSet.has(itemId)) {
199
- newSet.delete(itemId);
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([...this.expandedSections]));
377
- localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'items'), JSON.stringify([...this.expandedItems]));
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 = 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