@dfosco/storyboard-react 1.7.1 → 1.9.0

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.7.1",
3
+ "version": "1.9.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@dfosco/storyboard-core": "*",
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import { useState, useEffect, useMemo } from 'react'
3
3
  import { hash, resolveSceneRoute, getSceneMeta } from '@dfosco/storyboard-core'
4
- import { Link } from 'react-router-dom'
5
4
  import styles from './Viewfinder.module.css'
6
5
 
7
6
  function PlaceholderGraphic({ name }) {
@@ -127,7 +126,7 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
127
126
  {sceneNames.map((name) => {
128
127
  const meta = getSceneMeta(name)
129
128
  return (
130
- <Link key={name} to={resolveSceneRoute(name, knownRoutes)} className={styles.card}>
129
+ <a key={name} href={resolveSceneRoute(name, knownRoutes)} className={styles.card}>
131
130
  <div className={styles.thumbnail}>
132
131
  <PlaceholderGraphic name={name} />
133
132
  </div>
@@ -144,7 +143,7 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
144
143
  </div>
145
144
  )}
146
145
  </div>
147
- </Link>
146
+ </a>
148
147
  )
149
148
  })}
150
149
  </div>
@@ -46,6 +46,7 @@
46
46
  .card:hover {
47
47
  border-color: var(--borderColor-accent-emphasis, #1f6feb);
48
48
  box-shadow: 0 0 0 1px var(--borderColor-accent-emphasis, #1f6feb);
49
+ text-decoration: none !important;
49
50
  }
50
51
 
51
52
  .thumbnail {
package/src/context.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import { useMemo } from 'react'
3
- import { useParams } from 'react-router-dom'
3
+ import { useParams, useLocation } from 'react-router-dom'
4
4
  // Side-effect import: seeds the core data index via init()
5
5
  import 'virtual:storyboard-data-index'
6
6
  import { loadScene, sceneExists, findRecord, deepMerge } from '@dfosco/storyboard-core'
@@ -9,20 +9,11 @@ import { StoryboardContext } from './StoryboardContext.js'
9
9
  export { StoryboardContext }
10
10
 
11
11
  /**
12
- * Read the ?scene= param directly from window.location.
13
- * Avoids useSearchParams() which re-renders on every router
14
- * navigation event (including hash changes), causing a flash.
15
- */
16
- function getSceneParam() {
17
- return new URLSearchParams(window.location.search).get('scene')
18
- }
19
-
20
- /**
21
- * Derives a scene name from the current page pathname.
12
+ * Derives a scene name from a pathname.
22
13
  * "/Overview" → "Overview", "/" → "index", "/nested/Page" → "Page"
23
14
  */
24
- function getPageSceneName() {
25
- const path = window.location.pathname.replace(/\/+$/, '') || '/'
15
+ function getPageSceneName(pathname) {
16
+ const path = pathname.replace(/\/+$/, '') || '/'
26
17
  if (path === '/') return 'index'
27
18
  const last = path.split('/').pop()
28
19
  return last || 'index'
@@ -37,8 +28,10 @@ function getPageSceneName() {
37
28
  * The matched record entry is injected under the "record" key in scene data.
38
29
  */
39
30
  export default function StoryboardProvider({ sceneName, recordName, recordParam, children }) {
40
- const pageScene = getPageSceneName()
41
- const activeSceneName = getSceneParam() || sceneName || (sceneExists(pageScene) ? pageScene : 'default')
31
+ const location = useLocation()
32
+ const sceneParam = new URLSearchParams(location.search).get('scene')
33
+ const pageScene = getPageSceneName(location.pathname)
34
+ const activeSceneName = sceneParam || sceneName || (sceneExists(pageScene) ? pageScene : 'default')
42
35
  const params = useParams()
43
36
 
44
37
  const { data, error } = useMemo(() => {
@@ -57,7 +50,7 @@ export default function StoryboardProvider({ sceneName, recordName, recordParam,
57
50
  } catch (err) {
58
51
  return { data: null, error: err.message }
59
52
  }
60
- }, [activeSceneName, recordName, recordParam, params])
53
+ }, [activeSceneName, recordName, recordParam, params, location.pathname])
61
54
 
62
55
  const value = {
63
56
  data,
@@ -2,10 +2,17 @@ import { render, screen } from '@testing-library/react'
2
2
  import { useContext } from 'react'
3
3
  import { init } from '@dfosco/storyboard-core'
4
4
  import StoryboardProvider, { StoryboardContext } from './context.jsx'
5
+ import { useLocation } from 'react-router-dom'
6
+
7
+ const mockUseLocation = vi.fn(() => ({ pathname: '/', search: '', hash: '' }))
5
8
 
6
9
  vi.mock('react-router-dom', async () => {
7
10
  const actual = await vi.importActual('react-router-dom')
8
- return { ...actual, useParams: vi.fn(() => ({})) }
11
+ return {
12
+ ...actual,
13
+ useParams: vi.fn(() => ({})),
14
+ useLocation: (...args) => mockUseLocation(...args),
15
+ }
9
16
  })
10
17
 
11
18
  beforeEach(() => {
@@ -99,4 +106,73 @@ describe('StoryboardProvider', () => {
99
106
  )
100
107
  expect(screen.getByTestId('loading')).toHaveTextContent('false')
101
108
  })
109
+
110
+ it('auto-matches scene by pathname and resolves $ref data', () => {
111
+ init({
112
+ scenes: {
113
+ default: { title: 'Default' },
114
+ Repositories: {
115
+ '$global': ['navigation'],
116
+ heading: 'All repos',
117
+ },
118
+ },
119
+ objects: {
120
+ navigation: { topnav: [{ label: 'Home' }, { label: 'Repos' }] },
121
+ },
122
+ records: {},
123
+ })
124
+
125
+ // Simulate navigating to /base/Repositories
126
+ mockUseLocation.mockReturnValue({ pathname: '/base/Repositories', search: '', hash: '' })
127
+
128
+ render(
129
+ <StoryboardProvider>
130
+ <ContextReader path="heading" />
131
+ </StoryboardProvider>,
132
+ )
133
+ expect(screen.getByTestId('ctx')).toHaveTextContent('All repos')
134
+ })
135
+
136
+ it('resolves $ref objects when auto-matching scene by pathname', () => {
137
+ init({
138
+ scenes: {
139
+ default: { title: 'Default' },
140
+ Repositories: {
141
+ '$global': ['navigation'],
142
+ heading: 'All repos',
143
+ },
144
+ },
145
+ objects: {
146
+ navigation: { topnav: [{ label: 'Home' }, { label: 'Repos' }] },
147
+ },
148
+ records: {},
149
+ })
150
+
151
+ mockUseLocation.mockReturnValue({ pathname: '/base/Repositories', search: '', hash: '' })
152
+
153
+ function NavReader() {
154
+ const ctx = useContext(StoryboardContext)
155
+ const topnav = ctx?.data?.topnav
156
+ return <span data-testid="nav">{topnav ? topnav.map(n => n.label).join(',') : 'none'}</span>
157
+ }
158
+
159
+ render(
160
+ <StoryboardProvider>
161
+ <NavReader />
162
+ </StoryboardProvider>,
163
+ )
164
+ // $global navigation object should be resolved — topnav merged at root
165
+ expect(screen.getByTestId('nav')).toHaveTextContent('Home,Repos')
166
+ })
167
+
168
+ it('reads ?scene= param from location.search', () => {
169
+ mockUseLocation.mockReturnValue({ pathname: '/whatever', search: '?scene=other', hash: '' })
170
+
171
+ render(
172
+ <StoryboardProvider>
173
+ <ContextReader path="title" />
174
+ </StoryboardProvider>,
175
+ )
176
+ expect(screen.getByTestId('ctx')).toHaveTextContent('Other Scene')
177
+ })
102
178
  })