@dfosco/storyboard-react 1.24.0 → 2.0.0-beta.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.
- package/package.json +2 -2
- package/src/Viewfinder.jsx +46 -215
- package/src/context.jsx +71 -19
- package/src/context.test.jsx +97 -14
- package/src/hooks/useMode.js +43 -0
- package/src/hooks/useRecord.js +16 -8
- package/src/hooks/useScene.js +19 -10
- package/src/hooks/useScene.test.js +40 -13
- package/src/hooks/useSceneData.js +17 -11
- package/src/hooks/useSceneData.test.js +60 -32
- package/src/index.js +7 -1
- package/src/test-utils.js +8 -5
- package/src/vite/data-plugin.js +141 -17
- package/src/vite/data-plugin.test.js +135 -2
package/src/hooks/useRecord.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useMemo, useSyncExternalStore } from 'react'
|
|
1
|
+
import { useContext, useMemo, useSyncExternalStore } from 'react'
|
|
2
2
|
import { useParams } from 'react-router-dom'
|
|
3
|
-
import { loadRecord } from '@dfosco/storyboard-core'
|
|
3
|
+
import { loadRecord, resolveRecordName } from '@dfosco/storyboard-core'
|
|
4
4
|
import { deepClone, setByPath } from '@dfosco/storyboard-core'
|
|
5
5
|
import { getAllParams } from '@dfosco/storyboard-core'
|
|
6
6
|
import { isHideMode, getAllShadows } from '@dfosco/storyboard-core'
|
|
7
7
|
import { subscribeToHash, getHashSnapshot } from '@dfosco/storyboard-core'
|
|
8
8
|
import { subscribeToStorage, getStorageSnapshot } from '@dfosco/storyboard-core'
|
|
9
|
+
import { StoryboardContext } from '../StoryboardContext.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Collect overrides for a record and merge them into the base array.
|
|
@@ -91,6 +92,8 @@ function applyRecordOverrides(baseRecords, recordName) {
|
|
|
91
92
|
export function useRecord(recordName, paramName = 'id') {
|
|
92
93
|
const params = useParams()
|
|
93
94
|
const paramValue = params[paramName]
|
|
95
|
+
const context = useContext(StoryboardContext)
|
|
96
|
+
const prototypeName = context?.prototypeName ?? null
|
|
94
97
|
|
|
95
98
|
// Re-render on hash or localStorage changes so overrides are reactive
|
|
96
99
|
const hashString = useSyncExternalStore(subscribeToHash, getHashSnapshot)
|
|
@@ -99,14 +102,15 @@ export function useRecord(recordName, paramName = 'id') {
|
|
|
99
102
|
return useMemo(() => {
|
|
100
103
|
if (!paramValue) return null
|
|
101
104
|
try {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
105
|
+
const resolvedName = resolveRecordName(prototypeName, recordName)
|
|
106
|
+
const base = loadRecord(resolvedName)
|
|
107
|
+
const merged = applyRecordOverrides(base, resolvedName)
|
|
104
108
|
return merged.find(e => e[paramName] === paramValue) ?? null
|
|
105
109
|
} catch (err) {
|
|
106
110
|
console.error(`[useRecord] ${err.message}`)
|
|
107
111
|
return null
|
|
108
112
|
}
|
|
109
|
-
}, [recordName, paramName, paramValue, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
113
|
+
}, [recordName, paramName, paramValue, prototypeName, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
|
@@ -121,17 +125,21 @@ export function useRecord(recordName, paramName = 'id') {
|
|
|
121
125
|
* const allPosts = useRecords('posts')
|
|
122
126
|
*/
|
|
123
127
|
export function useRecords(recordName) {
|
|
128
|
+
const context = useContext(StoryboardContext)
|
|
129
|
+
const prototypeName = context?.prototypeName ?? null
|
|
130
|
+
|
|
124
131
|
// Re-render on hash or localStorage changes so overrides are reactive
|
|
125
132
|
const hashString = useSyncExternalStore(subscribeToHash, getHashSnapshot)
|
|
126
133
|
const storageString = useSyncExternalStore(subscribeToStorage, getStorageSnapshot)
|
|
127
134
|
|
|
128
135
|
return useMemo(() => {
|
|
129
136
|
try {
|
|
130
|
-
const
|
|
131
|
-
|
|
137
|
+
const resolvedName = resolveRecordName(prototypeName, recordName)
|
|
138
|
+
const base = loadRecord(resolvedName)
|
|
139
|
+
return applyRecordOverrides(base, resolvedName)
|
|
132
140
|
} catch (err) {
|
|
133
141
|
console.error(`[useRecords] ${err.message}`)
|
|
134
142
|
return []
|
|
135
143
|
}
|
|
136
|
-
}, [recordName, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
144
|
+
}, [recordName, prototypeName, hashString, storageString]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
137
145
|
}
|
package/src/hooks/useScene.js
CHANGED
|
@@ -2,27 +2,36 @@ import { useContext, useCallback } from 'react'
|
|
|
2
2
|
import { StoryboardContext } from '../StoryboardContext.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Read the current
|
|
5
|
+
* Read the current flow name and programmatically switch flows.
|
|
6
6
|
*
|
|
7
|
-
* @returns {{
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
7
|
+
* @returns {{ flowName: string, switchFlow: (name: string) => void }}
|
|
8
|
+
* - flowName – current active flow (e.g. "default")
|
|
9
|
+
* - switchFlow – navigate to a different flow by updating ?scene= param
|
|
10
10
|
*/
|
|
11
|
-
export function
|
|
11
|
+
export function useFlow() {
|
|
12
12
|
const context = useContext(StoryboardContext)
|
|
13
13
|
if (context === null) {
|
|
14
|
-
throw new Error('
|
|
14
|
+
throw new Error('useFlow must be used within a <StoryboardProvider>')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const switchFlow = useCallback((name) => {
|
|
18
18
|
const url = new URL(window.location.href)
|
|
19
19
|
url.searchParams.set('scene', name)
|
|
20
|
-
// Preserve hash params across
|
|
20
|
+
// Preserve hash params across flow switches
|
|
21
21
|
window.location.href = url.toString()
|
|
22
22
|
}, [])
|
|
23
23
|
|
|
24
24
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
flowName: context.flowName,
|
|
26
|
+
switchFlow,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @deprecated Use useFlow() */
|
|
31
|
+
export function useScene() {
|
|
32
|
+
const { flowName, switchFlow } = useFlow()
|
|
33
|
+
return {
|
|
34
|
+
sceneName: flowName,
|
|
35
|
+
switchScene: switchFlow,
|
|
27
36
|
}
|
|
28
37
|
}
|
|
@@ -1,39 +1,66 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { useFlow } from './useScene.js'
|
|
2
3
|
import { useScene } from './useScene.js'
|
|
3
|
-
import { seedTestData, createWrapper,
|
|
4
|
+
import { seedTestData, createWrapper, TEST_FLOWS } from '../test-utils.js'
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const flowData = TEST_FLOWS.default
|
|
6
7
|
|
|
7
8
|
beforeEach(() => {
|
|
8
9
|
seedTestData()
|
|
9
10
|
})
|
|
10
11
|
|
|
11
|
-
describe('
|
|
12
|
+
describe('useFlow', () => {
|
|
13
|
+
it('returns { flowName, switchFlow }', () => {
|
|
14
|
+
const { result } = renderHook(() => useFlow(), {
|
|
15
|
+
wrapper: createWrapper(flowData),
|
|
16
|
+
})
|
|
17
|
+
expect(result.current).toHaveProperty('flowName')
|
|
18
|
+
expect(result.current).toHaveProperty('switchFlow')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('flowName matches the value from context', () => {
|
|
22
|
+
const { result } = renderHook(() => useFlow(), {
|
|
23
|
+
wrapper: createWrapper(flowData, 'other'),
|
|
24
|
+
})
|
|
25
|
+
expect(result.current.flowName).toBe('other')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('switchFlow is a function', () => {
|
|
29
|
+
const { result } = renderHook(() => useFlow(), {
|
|
30
|
+
wrapper: createWrapper(flowData),
|
|
31
|
+
})
|
|
32
|
+
expect(typeof result.current.switchFlow).toBe('function')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('throws when used outside StoryboardProvider', () => {
|
|
36
|
+
expect(() => {
|
|
37
|
+
renderHook(() => useFlow())
|
|
38
|
+
}).toThrow('useFlow must be used within a <StoryboardProvider>')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ── useScene (deprecated alias) ──
|
|
43
|
+
|
|
44
|
+
describe('useScene (deprecated alias)', () => {
|
|
12
45
|
it('returns { sceneName, switchScene }', () => {
|
|
13
46
|
const { result } = renderHook(() => useScene(), {
|
|
14
|
-
wrapper: createWrapper(
|
|
47
|
+
wrapper: createWrapper(flowData),
|
|
15
48
|
})
|
|
16
49
|
expect(result.current).toHaveProperty('sceneName')
|
|
17
50
|
expect(result.current).toHaveProperty('switchScene')
|
|
18
51
|
})
|
|
19
52
|
|
|
20
|
-
it('sceneName matches the
|
|
53
|
+
it('sceneName matches the flow name from context', () => {
|
|
21
54
|
const { result } = renderHook(() => useScene(), {
|
|
22
|
-
wrapper: createWrapper(
|
|
55
|
+
wrapper: createWrapper(flowData, 'other'),
|
|
23
56
|
})
|
|
24
57
|
expect(result.current.sceneName).toBe('other')
|
|
25
58
|
})
|
|
26
59
|
|
|
27
60
|
it('switchScene is a function', () => {
|
|
28
61
|
const { result } = renderHook(() => useScene(), {
|
|
29
|
-
wrapper: createWrapper(
|
|
62
|
+
wrapper: createWrapper(flowData),
|
|
30
63
|
})
|
|
31
64
|
expect(typeof result.current.switchScene).toBe('function')
|
|
32
65
|
})
|
|
33
|
-
|
|
34
|
-
it('throws when used outside StoryboardProvider', () => {
|
|
35
|
-
expect(() => {
|
|
36
|
-
renderHook(() => useScene())
|
|
37
|
-
}).toThrow('useScene must be used within a <StoryboardProvider>')
|
|
38
|
-
})
|
|
39
66
|
})
|
|
@@ -7,24 +7,24 @@ import { isHideMode, getShadow, getAllShadows } from '@dfosco/storyboard-core'
|
|
|
7
7
|
import { subscribeToStorage, getStorageSnapshot } from '@dfosco/storyboard-core'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Access
|
|
11
|
-
* Hash params override
|
|
10
|
+
* Access flow data by dot-notation path.
|
|
11
|
+
* Hash params override flow data — both exact matches and nested paths.
|
|
12
12
|
*
|
|
13
13
|
* Examples:
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* useFlowData('user.name') with #user.name=Alice → "Alice"
|
|
15
|
+
* useFlowData('repositories') with #repositories.0.name=Foo
|
|
16
16
|
* → deep clone of repositories array with [0].name overridden to "Foo"
|
|
17
17
|
*
|
|
18
18
|
* @param {string} [path] - Dot-notation path (e.g. 'user.profile.name').
|
|
19
|
-
* Omit to get the entire
|
|
19
|
+
* Omit to get the entire flow object.
|
|
20
20
|
* @returns {*} The resolved value. Returns {} if path is missing after loading.
|
|
21
21
|
* @throws If used outside a StoryboardProvider.
|
|
22
22
|
*/
|
|
23
|
-
export function
|
|
23
|
+
export function useFlowData(path) {
|
|
24
24
|
const context = useContext(StoryboardContext)
|
|
25
25
|
|
|
26
26
|
if (context === null) {
|
|
27
|
-
throw new Error('
|
|
27
|
+
throw new Error('useFlowData must be used within a <StoryboardProvider>')
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const { data, loading, error } = context
|
|
@@ -73,7 +73,7 @@ export function useSceneData(path) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
if (sceneValue === undefined) {
|
|
76
|
-
console.warn(`[
|
|
76
|
+
console.warn(`[useFlowData] Path "${path}" not found in flow data.`)
|
|
77
77
|
return {}
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -83,15 +83,21 @@ export function useSceneData(path) {
|
|
|
83
83
|
return result
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/** @deprecated Use useFlowData() */
|
|
87
|
+
export const useSceneData = useFlowData
|
|
88
|
+
|
|
86
89
|
/**
|
|
87
|
-
* Returns true while
|
|
90
|
+
* Returns true while flow data is still loading.
|
|
88
91
|
*/
|
|
89
|
-
export function
|
|
92
|
+
export function useFlowLoading() {
|
|
90
93
|
const context = useContext(StoryboardContext)
|
|
91
94
|
|
|
92
95
|
if (context === null) {
|
|
93
|
-
throw new Error('
|
|
96
|
+
throw new Error('useFlowLoading must be used within a <StoryboardProvider>')
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
return context.loading
|
|
97
100
|
}
|
|
101
|
+
|
|
102
|
+
/** @deprecated Use useFlowLoading() */
|
|
103
|
+
export const useSceneLoading = useFlowLoading
|
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react'
|
|
2
|
-
import { useSceneData, useSceneLoading } from './useSceneData.js'
|
|
3
|
-
import { seedTestData, createWrapper,
|
|
2
|
+
import { useFlowData, useFlowLoading, useSceneData, useSceneLoading } from './useSceneData.js'
|
|
3
|
+
import { seedTestData, createWrapper, TEST_FLOWS } from '../test-utils.js'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const flowData = TEST_FLOWS.default
|
|
6
6
|
|
|
7
7
|
beforeEach(() => {
|
|
8
8
|
seedTestData()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
describe('
|
|
12
|
-
it('returns entire
|
|
13
|
-
const { result } = renderHook(() =>
|
|
14
|
-
wrapper: createWrapper(
|
|
11
|
+
describe('useFlowData', () => {
|
|
12
|
+
it('returns entire flow object when no path given', () => {
|
|
13
|
+
const { result } = renderHook(() => useFlowData(), {
|
|
14
|
+
wrapper: createWrapper(flowData),
|
|
15
15
|
})
|
|
16
|
-
expect(result.current).toEqual(
|
|
16
|
+
expect(result.current).toEqual(flowData)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
it('returns nested value by dot-notation path', () => {
|
|
20
|
-
const { result } = renderHook(() =>
|
|
21
|
-
wrapper: createWrapper(
|
|
20
|
+
const { result } = renderHook(() => useFlowData('user.name'), {
|
|
21
|
+
wrapper: createWrapper(flowData),
|
|
22
22
|
})
|
|
23
23
|
expect(result.current).toBe('Jane')
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
it('returns deep nested value', () => {
|
|
27
|
-
const { result } = renderHook(() =>
|
|
28
|
-
wrapper: createWrapper(
|
|
27
|
+
const { result } = renderHook(() => useFlowData('user.profile.bio'), {
|
|
28
|
+
wrapper: createWrapper(flowData),
|
|
29
29
|
})
|
|
30
30
|
expect(result.current).toBe('Dev')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
it('returns array by path', () => {
|
|
34
|
-
const { result } = renderHook(() =>
|
|
35
|
-
wrapper: createWrapper(
|
|
34
|
+
const { result } = renderHook(() => useFlowData('projects'), {
|
|
35
|
+
wrapper: createWrapper(flowData),
|
|
36
36
|
})
|
|
37
37
|
expect(result.current).toEqual([
|
|
38
38
|
{ id: 1, name: 'alpha' },
|
|
@@ -41,16 +41,16 @@ describe('useSceneData', () => {
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('returns array element by index path', () => {
|
|
44
|
-
const { result } = renderHook(() =>
|
|
45
|
-
wrapper: createWrapper(
|
|
44
|
+
const { result } = renderHook(() => useFlowData('projects.0'), {
|
|
45
|
+
wrapper: createWrapper(flowData),
|
|
46
46
|
})
|
|
47
47
|
expect(result.current).toEqual({ id: 1, name: 'alpha' })
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
it('returns empty object and warns for missing path', () => {
|
|
51
51
|
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
52
|
-
const { result } = renderHook(() =>
|
|
53
|
-
wrapper: createWrapper(
|
|
52
|
+
const { result } = renderHook(() => useFlowData('nonexistent.path'), {
|
|
53
|
+
wrapper: createWrapper(flowData),
|
|
54
54
|
})
|
|
55
55
|
expect(result.current).toEqual({})
|
|
56
56
|
expect(spy).toHaveBeenCalledWith(
|
|
@@ -61,48 +61,76 @@ describe('useSceneData', () => {
|
|
|
61
61
|
|
|
62
62
|
it('throws when used outside StoryboardProvider', () => {
|
|
63
63
|
expect(() => {
|
|
64
|
-
renderHook(() =>
|
|
65
|
-
}).toThrow('
|
|
64
|
+
renderHook(() => useFlowData())
|
|
65
|
+
}).toThrow('useFlowData must be used within a <StoryboardProvider>')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it('returns hash override value when param matches path', () => {
|
|
69
69
|
window.location.hash = '#user.name=Alice'
|
|
70
|
-
const { result } = renderHook(() =>
|
|
71
|
-
wrapper: createWrapper(
|
|
70
|
+
const { result } = renderHook(() => useFlowData('user.name'), {
|
|
71
|
+
wrapper: createWrapper(flowData),
|
|
72
72
|
})
|
|
73
73
|
expect(result.current).toBe('Alice')
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('applies child overrides to arrays', () => {
|
|
77
77
|
window.location.hash = '#projects.0.name=gamma'
|
|
78
|
-
const { result } = renderHook(() =>
|
|
79
|
-
wrapper: createWrapper(
|
|
78
|
+
const { result } = renderHook(() => useFlowData('projects'), {
|
|
79
|
+
wrapper: createWrapper(flowData),
|
|
80
80
|
})
|
|
81
81
|
expect(result.current[0].name).toBe('gamma')
|
|
82
82
|
expect(result.current[1].name).toBe('beta')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
-
it('returns full
|
|
85
|
+
it('returns full flow with all overrides applied when no path', () => {
|
|
86
86
|
window.location.hash = '#user.name=Alice'
|
|
87
|
-
const { result } = renderHook(() =>
|
|
88
|
-
wrapper: createWrapper(
|
|
87
|
+
const { result } = renderHook(() => useFlowData(), {
|
|
88
|
+
wrapper: createWrapper(flowData),
|
|
89
89
|
})
|
|
90
90
|
expect(result.current.user.name).toBe('Alice')
|
|
91
91
|
expect(result.current.settings.theme).toBe('dark')
|
|
92
92
|
})
|
|
93
93
|
})
|
|
94
94
|
|
|
95
|
-
describe('
|
|
95
|
+
describe('useFlowLoading', () => {
|
|
96
96
|
it('returns false when not loading', () => {
|
|
97
|
-
const { result } = renderHook(() =>
|
|
98
|
-
wrapper: createWrapper(
|
|
97
|
+
const { result } = renderHook(() => useFlowLoading(), {
|
|
98
|
+
wrapper: createWrapper(flowData),
|
|
99
99
|
})
|
|
100
100
|
expect(result.current).toBe(false)
|
|
101
101
|
})
|
|
102
102
|
|
|
103
103
|
it('throws when used outside StoryboardProvider', () => {
|
|
104
104
|
expect(() => {
|
|
105
|
-
renderHook(() =>
|
|
106
|
-
}).toThrow('
|
|
105
|
+
renderHook(() => useFlowLoading())
|
|
106
|
+
}).toThrow('useFlowLoading must be used within a <StoryboardProvider>')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// ── Deprecated aliases ──
|
|
111
|
+
|
|
112
|
+
describe('useSceneData (deprecated alias)', () => {
|
|
113
|
+
it('is the same function as useFlowData', () => {
|
|
114
|
+
expect(useSceneData).toBe(useFlowData)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('returns flow data', () => {
|
|
118
|
+
const { result } = renderHook(() => useSceneData(), {
|
|
119
|
+
wrapper: createWrapper(flowData),
|
|
120
|
+
})
|
|
121
|
+
expect(result.current).toEqual(flowData)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('useSceneLoading (deprecated alias)', () => {
|
|
126
|
+
it('is the same function as useFlowLoading', () => {
|
|
127
|
+
expect(useSceneLoading).toBe(useFlowLoading)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns loading state', () => {
|
|
131
|
+
const { result } = renderHook(() => useSceneLoading(), {
|
|
132
|
+
wrapper: createWrapper(flowData),
|
|
133
|
+
})
|
|
134
|
+
expect(result.current).toBe(false)
|
|
107
135
|
})
|
|
108
136
|
})
|
package/src/index.js
CHANGED
|
@@ -10,16 +10,19 @@ export { default as StoryboardProvider } from './context.jsx'
|
|
|
10
10
|
export { StoryboardContext } from './StoryboardContext.js'
|
|
11
11
|
|
|
12
12
|
// Hooks
|
|
13
|
+
export { useFlowData, useFlowLoading } from './hooks/useSceneData.js'
|
|
14
|
+
// Deprecated aliases
|
|
13
15
|
export { useSceneData, useSceneLoading } from './hooks/useSceneData.js'
|
|
14
16
|
export { useOverride } from './hooks/useOverride.js'
|
|
15
17
|
export { useOverride as useSession } from './hooks/useOverride.js' // deprecated alias
|
|
16
|
-
export { useScene } from './hooks/useScene.js'
|
|
18
|
+
export { useFlow, useScene } from './hooks/useScene.js'
|
|
17
19
|
export { useRecord, useRecords } from './hooks/useRecord.js'
|
|
18
20
|
export { useObject } from './hooks/useObject.js'
|
|
19
21
|
export { useLocalStorage } from './hooks/useLocalStorage.js'
|
|
20
22
|
export { useHideMode } from './hooks/useHideMode.js'
|
|
21
23
|
export { useUndoRedo } from './hooks/useUndoRedo.js'
|
|
22
24
|
export { useFeatureFlag } from './hooks/useFeatureFlag.js'
|
|
25
|
+
export { useMode } from './hooks/useMode.js'
|
|
23
26
|
|
|
24
27
|
// React Router integration
|
|
25
28
|
export { installHashPreserver } from './hashPreserver.js'
|
|
@@ -27,5 +30,8 @@ export { installHashPreserver } from './hashPreserver.js'
|
|
|
27
30
|
// Form context (for design system packages to use)
|
|
28
31
|
export { FormContext } from './context/FormContext.js'
|
|
29
32
|
|
|
33
|
+
// Design mode hook (keep — React apps may still read mode state)
|
|
34
|
+
// ModeSwitch and ToolbarShell UI moved to @dfosco/storyboard-svelte-ui
|
|
35
|
+
|
|
30
36
|
// Viewfinder dashboard
|
|
31
37
|
export { default as Viewfinder } from './Viewfinder.jsx'
|
package/src/test-utils.js
CHANGED
|
@@ -3,7 +3,7 @@ import { StoryboardContext } from './StoryboardContext.js'
|
|
|
3
3
|
import { init } from '@dfosco/storyboard-core'
|
|
4
4
|
|
|
5
5
|
// Default test data
|
|
6
|
-
export const
|
|
6
|
+
export const TEST_FLOWS = {
|
|
7
7
|
default: {
|
|
8
8
|
user: { name: 'Jane', profile: { bio: 'Dev' } },
|
|
9
9
|
settings: { theme: 'dark' },
|
|
@@ -15,6 +15,9 @@ export const TEST_SCENES = {
|
|
|
15
15
|
},
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** @deprecated Use TEST_FLOWS */
|
|
19
|
+
export const TEST_SCENES = TEST_FLOWS
|
|
20
|
+
|
|
18
21
|
export const TEST_OBJECTS = {
|
|
19
22
|
'jane-doe': { name: 'Jane Doe', role: 'admin' },
|
|
20
23
|
}
|
|
@@ -27,15 +30,15 @@ export const TEST_RECORDS = {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export function seedTestData() {
|
|
30
|
-
init({
|
|
33
|
+
init({ flows: TEST_FLOWS, objects: TEST_OBJECTS, records: TEST_RECORDS })
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Wrapper that provides StoryboardContext with given
|
|
34
|
-
export function createWrapper(
|
|
36
|
+
// Wrapper that provides StoryboardContext with given flow data
|
|
37
|
+
export function createWrapper(flowData, flowName = 'default') {
|
|
35
38
|
return function Wrapper({ children }) {
|
|
36
39
|
return createElement(
|
|
37
40
|
StoryboardContext.Provider,
|
|
38
|
-
{ value: { data:
|
|
41
|
+
{ value: { data: flowData, error: null, loading: false, flowName, sceneName: flowName } },
|
|
39
42
|
children
|
|
40
43
|
)
|
|
41
44
|
}
|