@dfosco/storyboard-react 1.5.0 → 1.7.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.5.0",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@dfosco/storyboard-core": "*",
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/dfosco/storyboard.git",
13
+ "url": "https://github.com/dfosco/storyboard-source.git",
14
14
  "directory": "packages/react"
15
15
  },
16
16
  "files": [
@@ -0,0 +1,178 @@
1
+ /* eslint-disable react/prop-types */
2
+ import { useState, useEffect, useMemo } from 'react'
3
+ import { hash, resolveSceneRoute, getSceneMeta } from '@dfosco/storyboard-core'
4
+ import { Link } from 'react-router-dom'
5
+ import styles from './Viewfinder.module.css'
6
+
7
+ function PlaceholderGraphic({ name }) {
8
+ const seed = hash(name)
9
+ const rects = []
10
+
11
+ for (let i = 0; i < 12; i++) {
12
+ const s = seed * (i + 1)
13
+ const x = (s * 7 + i * 31) % 320
14
+ const y = (s * 13 + i * 17) % 200
15
+ const w = 20 + (s * (i + 3)) % 80
16
+ const h = 8 + (s * (i + 7)) % 40
17
+ const opacity = 0.06 + ((s * (i + 2)) % 20) / 100
18
+ const fill = i % 3 === 0 ? 'var(--placeholder-accent)' : i % 3 === 1 ? 'var(--placeholder-fg)' : 'var(--placeholder-muted)'
19
+
20
+ rects.push(
21
+ <rect
22
+ key={i}
23
+ x={x}
24
+ y={y}
25
+ width={w}
26
+ height={h}
27
+ rx={2}
28
+ fill={fill}
29
+ opacity={opacity}
30
+ />
31
+ )
32
+ }
33
+
34
+ const lines = []
35
+ for (let i = 0; i < 6; i++) {
36
+ const s = seed * (i + 5)
37
+ const y = 10 + (s % 180)
38
+ lines.push(
39
+ <line
40
+ key={`h${i}`}
41
+ x1={0}
42
+ y1={y}
43
+ x2={320}
44
+ y2={y}
45
+ stroke="var(--placeholder-grid)"
46
+ strokeWidth={0.5}
47
+ opacity={0.4}
48
+ />
49
+ )
50
+ }
51
+ for (let i = 0; i < 8; i++) {
52
+ const s = seed * (i + 9)
53
+ const x = 10 + (s % 300)
54
+ lines.push(
55
+ <line
56
+ key={`v${i}`}
57
+ x1={x}
58
+ y1={0}
59
+ x2={x}
60
+ y2={200}
61
+ stroke="var(--placeholder-grid)"
62
+ strokeWidth={0.5}
63
+ opacity={0.3}
64
+ />
65
+ )
66
+ }
67
+
68
+ return (
69
+ <svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
70
+ <rect width="320" height="200" fill="var(--placeholder-bg)" />
71
+ {lines}
72
+ {rects}
73
+ </svg>
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Viewfinder — scene index and branch preview dashboard.
79
+ *
80
+ * @param {Object} props
81
+ * @param {Record<string, unknown>} props.scenes - Scene index object (keys are scene names)
82
+ * @param {Record<string, unknown>} props.pageModules - import.meta.glob result for page files
83
+ * @param {string} [props.basePath] - Base URL path (defaults to import.meta.env.BASE_URL)
84
+ * @param {string} [props.title] - Header title (defaults to "Viewfinder")
85
+ */
86
+ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, title = 'Viewfinder' }) {
87
+ const [branches, setBranches] = useState(null)
88
+
89
+ const sceneNames = useMemo(() => Object.keys(scenes), [scenes])
90
+
91
+ const knownRoutes = useMemo(() =>
92
+ Object.keys(pageModules)
93
+ .map(p => p.replace('/src/pages/', '').replace('.jsx', ''))
94
+ .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder'),
95
+ [pageModules]
96
+ )
97
+
98
+ const branchBasePath = useMemo(() => {
99
+ const base = basePath || '/storyboard-source/'
100
+ return base.replace(/\/[^/]*\/$/, '/')
101
+ }, [basePath])
102
+
103
+ useEffect(() => {
104
+ const url = `${branchBasePath}branches.json`
105
+ fetch(url)
106
+ .then(r => r.ok ? r.json() : [])
107
+ .then(data => setBranches(Array.isArray(data) ? data : []))
108
+ .catch(() => setBranches([]))
109
+ }, [branchBasePath])
110
+
111
+ return (
112
+ <div className={styles.container}>
113
+ <header className={styles.header}>
114
+ <h1 className={styles.title}>{title}</h1>
115
+ <p className={styles.subtitle}>
116
+ {sceneNames.length} scene{sceneNames.length !== 1 ? 's' : ''}
117
+ {branches && branches.length > 0 ? ` · ${branches.length} branch preview${branches.length !== 1 ? 's' : ''}` : ''}
118
+ </p>
119
+ </header>
120
+
121
+ {sceneNames.length === 0 ? (
122
+ <p className={styles.empty}>No scenes found. Add a <code>*.scene.json</code> file to get started.</p>
123
+ ) : (
124
+ <section>
125
+ <h2 className={styles.sectionTitle}>Scenes</h2>
126
+ <div className={styles.grid}>
127
+ {sceneNames.map((name) => {
128
+ const meta = getSceneMeta(name)
129
+ return (
130
+ <Link key={name} to={resolveSceneRoute(name, knownRoutes)} className={styles.card}>
131
+ <div className={styles.thumbnail}>
132
+ <PlaceholderGraphic name={name} />
133
+ </div>
134
+ <div className={styles.cardBody}>
135
+ <p className={styles.sceneName}>{name}</p>
136
+ {meta?.author && (
137
+ <div className={styles.author}>
138
+ <img
139
+ src={`https://github.com/${meta.author}.png?size=32`}
140
+ alt={meta.author}
141
+ className={styles.authorAvatar}
142
+ />
143
+ <span className={styles.authorName}>{meta.author}</span>
144
+ </div>
145
+ )}
146
+ </div>
147
+ </Link>
148
+ )
149
+ })}
150
+ </div>
151
+ </section>
152
+ )}
153
+
154
+ {branches && branches.length > 0 && (
155
+ <section className={styles.branchSection}>
156
+ <h2 className={styles.sectionTitle}>Branch Previews</h2>
157
+ <div className={styles.grid}>
158
+ {branches.map((b) => (
159
+ <a
160
+ key={b.folder}
161
+ href={`${branchBasePath}${b.folder}/`}
162
+ className={styles.card}
163
+ >
164
+ <div className={styles.thumbnail}>
165
+ <PlaceholderGraphic name={b.branch} />
166
+ </div>
167
+ <div className={styles.cardBody}>
168
+ <p className={styles.sceneName}>{b.branch}</p>
169
+ <p className={styles.branchMeta}>{b.folder}</p>
170
+ </div>
171
+ </a>
172
+ ))}
173
+ </div>
174
+ </section>
175
+ )}
176
+ </div>
177
+ )
178
+ }
@@ -0,0 +1,129 @@
1
+ .container {
2
+ min-height: 100vh;
3
+ background-color: var(--bgColor-default, #0d1117);
4
+ color: var(--fgColor-default, #e6edf3);
5
+ padding: 48px 32px;
6
+ }
7
+
8
+ .header {
9
+ max-width: 960px;
10
+ margin: 0 auto 40px;
11
+ }
12
+
13
+ .title {
14
+ font-size: 28px;
15
+ font-weight: 600;
16
+ margin: 0 0 8px;
17
+ color: var(--fgColor-default, #e6edf3);
18
+ letter-spacing: -0.02em;
19
+ }
20
+
21
+ .subtitle {
22
+ font-size: 14px;
23
+ color: var(--fgColor-muted, #848d97);
24
+ margin: 0;
25
+ }
26
+
27
+ .grid {
28
+ display: grid;
29
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
30
+ gap: 16px;
31
+ max-width: 960px;
32
+ margin: 0 auto;
33
+ }
34
+
35
+ .card {
36
+ display: block;
37
+ border: 1px solid var(--borderColor-default, #30363d);
38
+ border-radius: 8px;
39
+ overflow: hidden;
40
+ background: var(--bgColor-muted, #161b22);
41
+ text-decoration: none;
42
+ color: inherit;
43
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
44
+ }
45
+
46
+ .card:hover {
47
+ border-color: var(--borderColor-accent-emphasis, #1f6feb);
48
+ box-shadow: 0 0 0 1px var(--borderColor-accent-emphasis, #1f6feb);
49
+ }
50
+
51
+ .thumbnail {
52
+ aspect-ratio: 16 / 10;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ overflow: hidden;
57
+ background: var(--bgColor-inset, #010409);
58
+
59
+ --placeholder-bg: var(--bgColor-inset, #010409);
60
+ --placeholder-grid: var(--borderColor-default, #30363d);
61
+ --placeholder-accent: var(--fgColor-accent, #58a6ff);
62
+ --placeholder-fg: var(--fgColor-default, #c9d1d9);
63
+ --placeholder-muted: var(--fgColor-muted, #484f58);
64
+ }
65
+
66
+ .thumbnail svg {
67
+ width: 100%;
68
+ height: 100%;
69
+ }
70
+
71
+ .cardBody {
72
+ padding: 12px 16px;
73
+ border-top: 1px solid var(--borderColor-default, #30363d);
74
+ }
75
+
76
+ .sceneName {
77
+ font-size: 14px;
78
+ font-weight: 500;
79
+ color: var(--fgColor-default, #e6edf3);
80
+ margin: 0;
81
+ }
82
+
83
+ .empty {
84
+ text-align: center;
85
+ padding: 80px 24px;
86
+ color: var(--fgColor-muted, #848d97);
87
+ font-size: 14px;
88
+ max-width: 960px;
89
+ margin: 0 auto;
90
+ }
91
+
92
+ .sectionTitle {
93
+ font-size: 12px;
94
+ font-weight: 600;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.06em;
97
+ color: var(--fgColor-muted, #848d97);
98
+ margin: 0 auto 12px;
99
+ max-width: 960px;
100
+ }
101
+
102
+ .branchSection {
103
+ margin-top: 40px;
104
+ }
105
+
106
+ .branchMeta {
107
+ font-size: 12px;
108
+ color: var(--fgColor-muted, #848d97);
109
+ margin: 4px 0 0;
110
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
111
+ }
112
+
113
+ .author {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 6px;
117
+ margin-top: 6px;
118
+ }
119
+
120
+ .authorAvatar {
121
+ width: 16px;
122
+ height: 16px;
123
+ border-radius: 50%;
124
+ }
125
+
126
+ .authorName {
127
+ font-size: 12px;
128
+ color: var(--fgColor-muted, #848d97);
129
+ }
package/src/index.js CHANGED
@@ -25,3 +25,6 @@ export { installHashPreserver } from './hashPreserver.js'
25
25
 
26
26
  // Form context (for design system packages to use)
27
27
  export { FormContext } from './context/FormContext.js'
28
+
29
+ // Viewfinder dashboard
30
+ export { default as Viewfinder } from './Viewfinder.jsx'