@dfosco/storyboard-react 1.15.0 → 1.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "1.15.0",
3
+ "version": "1.15.2",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@dfosco/storyboard-core": "*",
@@ -3,10 +3,15 @@ import { useParams } from 'react-router-dom'
3
3
  import { loadRecord } from '@dfosco/storyboard-core'
4
4
  import { deepClone, setByPath } from '@dfosco/storyboard-core'
5
5
  import { getAllParams } from '@dfosco/storyboard-core'
6
+ import { isHideMode, getAllShadows } from '@dfosco/storyboard-core'
6
7
  import { subscribeToHash, getHashSnapshot } from '@dfosco/storyboard-core'
8
+ import { subscribeToStorage, getStorageSnapshot } from '@dfosco/storyboard-core'
7
9
 
8
10
  /**
9
- * Collect hash overrides for a record and merge them into the base array.
11
+ * Collect overrides for a record and merge them into the base array.
12
+ *
13
+ * In normal mode reads from URL hash params; in hide mode reads from
14
+ * localStorage shadow snapshots.
10
15
  *
11
16
  * Hash convention: record.{recordName}.{entryId}.{field}=value
12
17
  *
@@ -18,7 +23,7 @@ import { subscribeToHash, getHashSnapshot } from '@dfosco/storyboard-core'
18
23
  * @returns {Array} Merged array
19
24
  */
20
25
  function applyRecordOverrides(baseRecords, recordName) {
21
- const allParams = getAllParams()
26
+ const allParams = isHideMode() ? getAllShadows() : getAllParams()
22
27
  const prefix = `record.${recordName}.`
23
28
 
24
29
  // Collect only the params that target this record
@@ -87,8 +92,9 @@ export function useRecord(recordName, paramName = 'id') {
87
92
  const params = useParams()
88
93
  const paramValue = params[paramName]
89
94
 
90
- // Re-render on hash changes so overrides are reactive
95
+ // Re-render on hash or localStorage changes so overrides are reactive
91
96
  const hashString = useSyncExternalStore(subscribeToHash, getHashSnapshot)
97
+ const storageString = useSyncExternalStore(subscribeToStorage, getStorageSnapshot)
92
98
 
93
99
  return useMemo(() => {
94
100
  if (!paramValue) return null
@@ -100,7 +106,7 @@ export function useRecord(recordName, paramName = 'id') {
100
106
  console.error(`[useRecord] ${err.message}`)
101
107
  return null
102
108
  }
103
- }, [recordName, paramName, paramValue, hashString]) // eslint-disable-line react-hooks/exhaustive-deps
109
+ }, [recordName, paramName, paramValue, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
104
110
  }
105
111
 
106
112
  /**
@@ -115,8 +121,9 @@ export function useRecord(recordName, paramName = 'id') {
115
121
  * const allPosts = useRecords('posts')
116
122
  */
117
123
  export function useRecords(recordName) {
118
- // Re-render on hash changes so overrides are reactive
124
+ // Re-render on hash or localStorage changes so overrides are reactive
119
125
  const hashString = useSyncExternalStore(subscribeToHash, getHashSnapshot)
126
+ const storageString = useSyncExternalStore(subscribeToStorage, getStorageSnapshot)
120
127
 
121
128
  return useMemo(() => {
122
129
  try {
@@ -126,5 +133,5 @@ export function useRecords(recordName) {
126
133
  console.error(`[useRecords] ${err.message}`)
127
134
  return []
128
135
  }
129
- }, [recordName, hashString]) // eslint-disable-line react-hooks/exhaustive-deps
136
+ }, [recordName, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
130
137
  }
@@ -1,5 +1,6 @@
1
- import { renderHook } from '@testing-library/react'
1
+ import { renderHook, act } from '@testing-library/react'
2
2
  import { seedTestData, TEST_RECORDS } from '../../test-utils.js'
3
+ import { activateHideMode, setShadow } from '@dfosco/storyboard-core'
3
4
 
4
5
  vi.mock('react-router-dom', async () => {
5
6
  const actual = await vi.importActual('react-router-dom')
@@ -79,3 +80,51 @@ describe('useRecords', () => {
79
80
  expect(newPost.title).toBe('New')
80
81
  })
81
82
  })
83
+
84
+ // ── Hide mode ──
85
+
86
+ describe('useRecord (hide mode)', () => {
87
+ beforeEach(() => {
88
+ act(() => { activateHideMode() })
89
+ })
90
+
91
+ it('reads overrides from localStorage shadow in hide mode', () => {
92
+ useParams.mockReturnValue({ id: 'post-1' })
93
+ act(() => { setShadow('record.posts.post-1.title', 'Shadow Title') })
94
+
95
+ const { result } = renderHook(() => useRecord('posts'))
96
+ expect(result.current.title).toBe('Shadow Title')
97
+ })
98
+
99
+ it('reactively updates when shadow changes in hide mode', () => {
100
+ useParams.mockReturnValue({ id: 'post-1' })
101
+ const { result } = renderHook(() => useRecord('posts'))
102
+ expect(result.current.title).toBe('First Post')
103
+
104
+ act(() => { setShadow('record.posts.post-1.title', 'Updated via Shadow') })
105
+ expect(result.current.title).toBe('Updated via Shadow')
106
+ })
107
+ })
108
+
109
+ describe('useRecords (hide mode)', () => {
110
+ beforeEach(() => {
111
+ act(() => { activateHideMode() })
112
+ })
113
+
114
+ it('applies shadow overrides to existing entries', () => {
115
+ act(() => { setShadow('record.posts.post-1.title', 'Hidden Update') })
116
+
117
+ const { result } = renderHook(() => useRecords('posts'))
118
+ const post1 = result.current.find(e => e.id === 'post-1')
119
+ expect(post1.title).toBe('Hidden Update')
120
+ })
121
+
122
+ it('creates new entries from shadow overrides', () => {
123
+ act(() => { setShadow('record.posts.shadow-post.title', 'New Shadow') })
124
+
125
+ const { result } = renderHook(() => useRecords('posts'))
126
+ const newPost = result.current.find(e => e.id === 'shadow-post')
127
+ expect(newPost).toBeTruthy()
128
+ expect(newPost.title).toBe('New Shadow')
129
+ })
130
+ })