@classic-homes/theme-svelte 1.8.2 → 1.9.1

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Sidebar Collapse Tests
3
+ *
4
+ * DashboardLayout applies its `sidebarCollapsed` default only when no open
5
+ * state is persisted. A first session (nothing in localStorage yet) must
6
+ * still let the user collapse the sidebar — the default must not re-apply
7
+ * on every toggle and snap it back open.
8
+ */
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+ import { render, screen, fireEvent } from '@testing-library/svelte';
11
+ import { createRawSnippet, flushSync } from 'svelte';
12
+ import DashboardLayout from '../lib/components/layout/DashboardLayout.svelte';
13
+ import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
14
+ let appCounter = 0;
15
+ function freshAppId() {
16
+ return `test-collapse-${++appCounter}`;
17
+ }
18
+ function renderLayout(appId) {
19
+ return render(DashboardLayout, {
20
+ props: {
21
+ navigation: [{ id: 'main', items: [{ id: 'home', name: 'Home', href: '/' }] }],
22
+ appId,
23
+ children: createRawSnippet(() => ({ render: () => '<p>content</p>' })),
24
+ },
25
+ });
26
+ }
27
+ describe('sidebar collapse', () => {
28
+ beforeEach(() => {
29
+ localStorage.clear();
30
+ sidebarStore.setMobile(false);
31
+ });
32
+ it('collapses on a first session with no persisted state', async () => {
33
+ const appId = freshAppId();
34
+ renderLayout(appId);
35
+ expect(sidebarStore.isOpen).toBe(true);
36
+ await fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }));
37
+ flushSync();
38
+ expect(sidebarStore.isOpen).toBe(false);
39
+ expect(localStorage.getItem(`classic-theme-sidebar-${appId}-open`)).toBe('false');
40
+ expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument();
41
+ });
42
+ it('keeps the collapse after a mobile round-trip in a first session', () => {
43
+ renderLayout(freshAppId());
44
+ sidebarStore.toggle();
45
+ flushSync();
46
+ sidebarStore.setMobile(true);
47
+ flushSync();
48
+ sidebarStore.setMobile(false);
49
+ flushSync();
50
+ expect(sidebarStore.isOpen).toBe(false);
51
+ });
52
+ it('marks state as persisted once the open state is written', () => {
53
+ sidebarStore.initialize(freshAppId());
54
+ expect(sidebarStore.hasPersistedState).toBe(false);
55
+ sidebarStore.close();
56
+ expect(sidebarStore.hasPersistedState).toBe(true);
57
+ });
58
+ });
@@ -10,7 +10,7 @@
10
10
  * - Main content area with proper spacing
11
11
  * - Integrated with sidebar store for state management
12
12
  */
13
- import type { Snippet } from 'svelte';
13
+ import { untrack, type Snippet } from 'svelte';
14
14
  import type {
15
15
  NavSection,
16
16
  NavItem,
@@ -201,13 +201,18 @@
201
201
  $effect(() => {
202
202
  sidebarStore.initialize(appId);
203
203
 
204
- // Only apply prop-based initial state if no persisted state exists
204
+ // Only apply prop-based initial state if no persisted state exists.
205
+ // Untracked: open()/close() read `isOpen`, and tracking it would re-run
206
+ // this effect on every toggle and snap the sidebar back to the default.
205
207
  if (!sidebarStore.hasPersistedState && !isMobile) {
206
- if (sidebarCollapsed) {
207
- sidebarStore.close();
208
- } else {
209
- sidebarStore.open();
210
- }
208
+ const collapsed = sidebarCollapsed;
209
+ untrack(() => {
210
+ if (collapsed) {
211
+ sidebarStore.close();
212
+ } else {
213
+ sidebarStore.open();
214
+ }
215
+ });
211
216
  }
212
217
  });
213
218
 
@@ -9,7 +9,7 @@
9
9
  * - Main content area with proper spacing
10
10
  * - Integrated with sidebar store for state management
11
11
  */
12
- import type { Snippet } from 'svelte';
12
+ import { type Snippet } from 'svelte';
13
13
  import type { NavSection, NavItem, User, QuickLink, BackLink, HeaderSearchConfig } from '../../types/layout.js';
14
14
  type Breakpoint = 'sm' | 'md' | 'lg';
15
15
  interface Props {
@@ -18,7 +18,8 @@ declare class SidebarStore {
18
18
  */
19
19
  get initialized(): boolean;
20
20
  /**
21
- * Whether the sidebar open state was loaded from localStorage
21
+ * Whether a sidebar open state exists in localStorage (loaded at
22
+ * initialize, or written since)
22
23
  */
23
24
  get hasPersistedState(): boolean;
24
25
  /**
@@ -45,7 +45,8 @@ class SidebarStore {
45
45
  return __classPrivateFieldGet(this, _SidebarStore_initialized, "f");
46
46
  }
47
47
  /**
48
- * Whether the sidebar open state was loaded from localStorage
48
+ * Whether a sidebar open state exists in localStorage (loaded at
49
+ * initialize, or written since)
49
50
  */
50
51
  get hasPersistedState() {
51
52
  return __classPrivateFieldGet(this, _SidebarStore_loadedFromStorage, "f");
@@ -340,6 +341,9 @@ _SidebarStore_initialized = new WeakMap(), _SidebarStore_loadedFromStorage = new
340
341
  return;
341
342
  try {
342
343
  localStorage.setItem(__classPrivateFieldGet(this, _SidebarStore_instances, "m", _SidebarStore_getStorageKey).call(this, 'open'), String(this.isOpen));
344
+ // The key now exists: a caller that applies defaults only when nothing
345
+ // is persisted must stop doing so, or it undoes the user's toggle.
346
+ __classPrivateFieldSet(this, _SidebarStore_loadedFromStorage, true, "f");
343
347
  }
344
348
  catch {
345
349
  // localStorage not available
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/theme-svelte",
3
- "version": "1.8.2",
3
+ "version": "1.9.1",
4
4
  "description": "Svelte components for the Classic theme system",
5
5
  "type": "module",
6
6
  "svelte": "./dist/lib/index.js",