@dfosco/storyboard-react 1.15.2 → 1.17.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.15.2",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@dfosco/storyboard-core": "*",
@@ -90,8 +90,10 @@ function getCurrentBranch(basePath) {
90
90
  * @param {Record<string, unknown>} props.pageModules - import.meta.glob result for page files
91
91
  * @param {string} [props.basePath] - Base URL path (defaults to import.meta.env.BASE_URL)
92
92
  * @param {string} [props.title] - Header title (defaults to "Viewfinder")
93
+ * @param {string} [props.subtitle] - Optional subtitle displayed below the title
94
+ * @param {boolean} [props.showThumbnails] - Show thumbnail previews (defaults to false)
93
95
  */
94
- export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, title = 'Viewfinder' }) {
96
+ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, title = 'Viewfinder', subtitle, showThumbnails = false }) {
95
97
  const [branches, setBranches] = useState(null)
96
98
 
97
99
  const sceneNames = useMemo(() => Object.keys(scenes), [scenes])
@@ -110,13 +112,19 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
110
112
 
111
113
  const currentBranch = useMemo(() => getCurrentBranch(basePath), [basePath])
112
114
 
115
+ const MOCK_BRANCHES = useMemo(() => [
116
+ { branch: 'main', folder: '' },
117
+ { branch: 'feat/comments-v2', folder: 'branch--feat-comments-v2' },
118
+ { branch: 'fix/nav-overflow', folder: 'branch--fix-nav-overflow' },
119
+ ], [])
120
+
113
121
  useEffect(() => {
114
122
  const url = `${branchBasePath}branches.json`
115
123
  fetch(url)
116
- .then(r => r.ok ? r.json() : [])
117
- .then(data => setBranches(Array.isArray(data) ? data : []))
118
- .catch(() => setBranches([]))
119
- }, [branchBasePath])
124
+ .then(r => r.ok ? r.json() : null)
125
+ .then(data => setBranches(Array.isArray(data) && data.length > 0 ? data : MOCK_BRANCHES))
126
+ .catch(() => setBranches(MOCK_BRANCHES))
127
+ }, [branchBasePath, MOCK_BRANCHES])
120
128
 
121
129
  const handleBranchChange = (e) => {
122
130
  const folder = e.target.value
@@ -129,15 +137,21 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
129
137
  <div className={styles.container}>
130
138
  <header className={styles.header}>
131
139
  <div className={styles.headerTop}>
132
- <h1 className={styles.title}>{title}</h1>
140
+ <div>
141
+ <h1 className={styles.title}>{title}</h1>
142
+ {subtitle && <p className={styles.subtitle}>{subtitle}</p>}
143
+ </div>
133
144
  {branches && branches.length > 0 && (
134
145
  <div className={styles.branchDropdown}>
135
- <label className={styles.branchLabel} htmlFor="branch-select">Branch</label>
146
+ <svg className={styles.branchIcon} width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
147
+ <path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.492 2.492 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z" />
148
+ </svg>
136
149
  <select
137
150
  id="branch-select"
138
151
  className={styles.branchSelect}
139
152
  defaultValue=""
140
153
  onChange={handleBranchChange}
154
+ aria-label="Switch branch"
141
155
  >
142
156
  <option value="" disabled>{currentBranch}</option>
143
157
  {branches.map((b) => (
@@ -149,7 +163,7 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
149
163
  </div>
150
164
  )}
151
165
  </div>
152
- <p className={styles.subtitle}>
166
+ <p className={styles.sceneCount}>
153
167
  {sceneNames.length} scene{sceneNames.length !== 1 ? 's' : ''}
154
168
  </p>
155
169
  </header>
@@ -158,27 +172,37 @@ export default function Viewfinder({ scenes = {}, pageModules = {}, basePath, ti
158
172
  <p className={styles.empty}>No scenes found. Add a <code>*.scene.json</code> file to get started.</p>
159
173
  ) : (
160
174
  <section>
161
- <h2 className={styles.sectionTitle}>Scenes</h2>
162
- <div className={styles.grid}>
175
+ {/* <h2 className={styles.sectionTitle}>Scenes</h2> */}
176
+ <div className={showThumbnails ? styles.grid : styles.list}>
163
177
  {sceneNames.map((name) => {
164
178
  const meta = getSceneMeta(name)
165
179
  return (
166
- <a key={name} href={resolveSceneRoute(name, knownRoutes)} className={styles.card}>
167
- <div className={styles.thumbnail}>
168
- <PlaceholderGraphic name={name} />
169
- </div>
180
+ <a key={name} href={resolveSceneRoute(name, knownRoutes)} className={showThumbnails ? styles.card : styles.listItem}>
181
+ {showThumbnails && (
182
+ <div className={styles.thumbnail}>
183
+ <PlaceholderGraphic name={name} />
184
+ </div>
185
+ )}
170
186
  <div className={styles.cardBody}>
171
187
  <p className={styles.sceneName}>{name}</p>
172
- {meta?.author && (
173
- <div className={styles.author}>
174
- <img
175
- src={`https://github.com/${meta.author}.png?size=32`}
176
- alt={meta.author}
177
- className={styles.authorAvatar}
178
- />
179
- <span className={styles.authorName}>{meta.author}</span>
180
- </div>
181
- )}
188
+ {meta?.author && (() => {
189
+ const authors = Array.isArray(meta.author) ? meta.author : [meta.author]
190
+ return (
191
+ <div className={styles.author}>
192
+ <span className={styles.authorAvatars}>
193
+ {authors.map((a) => (
194
+ <img
195
+ key={a}
196
+ src={`https://github.com/${a}.png?size=32`}
197
+ alt={a}
198
+ className={styles.authorAvatar}
199
+ />
200
+ ))}
201
+ </span>
202
+ <span className={styles.authorName}>{authors.join(', ')}</span>
203
+ </div>
204
+ )
205
+ })()}
182
206
  </div>
183
207
  </a>
184
208
  )
@@ -2,36 +2,72 @@
2
2
  min-height: 100vh;
3
3
  background-color: var(--bgColor-default, #0d1117);
4
4
  color: var(--fgColor-default, #e6edf3);
5
- padding: 48px 32px;
5
+ padding: 80px 32px 48px;
6
6
  }
7
7
 
8
8
  .header {
9
- max-width: 960px;
10
- margin: 0 auto 40px;
9
+ max-width: 720px;
10
+ margin: 0 auto 64px;
11
11
  }
12
12
 
13
13
  .title {
14
- font-size: 28px;
15
- font-weight: 600;
16
- margin: 0 0 8px;
14
+ font-size: 72px;
15
+ font-weight: 400;
16
+ margin: 0 0 12px;
17
17
  color: var(--fgColor-default, #e6edf3);
18
- letter-spacing: -0.02em;
18
+ letter-spacing: -0.03em;
19
+ line-height: 1;
19
20
  }
20
21
 
21
22
  .subtitle {
22
- font-size: 14px;
23
+ font-size: 15px;
23
24
  color: var(--fgColor-muted, #848d97);
24
- margin: 0;
25
+ margin: 4px 0 0;
26
+ letter-spacing: 0.01em;
27
+ }
28
+
29
+ .sceneCount {
30
+ font-size: 13px;
31
+ color: var(--fgColor-muted, #848d97);
32
+ margin: 16px 0 0;
33
+ letter-spacing: 0.01em;
25
34
  }
26
35
 
27
36
  .grid {
28
37
  display: grid;
29
38
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
30
39
  gap: 16px;
31
- max-width: 960px;
40
+ max-width: 720px;
41
+ margin: 0 auto;
42
+ }
43
+
44
+ .list {
45
+ display: flex;
46
+ flex-direction: column;
47
+ max-width: 720px;
32
48
  margin: 0 auto;
33
49
  }
34
50
 
51
+ .listItem {
52
+ display: block;
53
+ padding: 8px 0;
54
+ text-decoration: none;
55
+ color: inherit;
56
+ /* border-bottom: 1px solid var(--borderColor-muted, #21262d); */
57
+ }
58
+
59
+ .listItem:first-child {
60
+ /* border-top: 1px solid var(--borderColor-muted, #21262d); */
61
+ }
62
+
63
+ .listItem:hover {
64
+ text-decoration: none !important;
65
+ }
66
+
67
+ .listItem .author {
68
+ margin-top: 8px;
69
+ }
70
+
35
71
  .card {
36
72
  display: block;
37
73
  border: 1px solid var(--borderColor-default, #30363d);
@@ -71,33 +107,39 @@
71
107
 
72
108
  .cardBody {
73
109
  padding: 12px 16px;
74
- border-top: 1px solid var(--borderColor-default, #30363d);
110
+
111
+ &:hover {
112
+ background-color: var(--bgColor-muted);
113
+ }
75
114
  }
76
115
 
77
116
  .sceneName {
78
- font-size: 14px;
79
- font-weight: 500;
117
+ font-size: 28px;
118
+ font-weight: 400;
80
119
  color: var(--fgColor-default, #e6edf3);
81
120
  margin: 0;
121
+ letter-spacing: -0.02em;
122
+ line-height: 1.2;
123
+ transition: font-style 0.15s ease;
82
124
  }
83
125
 
84
126
  .empty {
85
127
  text-align: center;
86
128
  padding: 80px 24px;
87
129
  color: var(--fgColor-muted, #848d97);
88
- font-size: 14px;
89
- max-width: 960px;
130
+ font-size: 15px;
131
+ max-width: 720px;
90
132
  margin: 0 auto;
91
133
  }
92
134
 
93
135
  .sectionTitle {
94
- font-size: 12px;
95
- font-weight: 600;
136
+ font-size: 11px;
137
+ font-weight: 700;
96
138
  text-transform: uppercase;
97
- letter-spacing: 0.06em;
139
+ letter-spacing: 0.12em;
98
140
  color: var(--fgColor-muted, #848d97);
99
- margin: 0 auto 12px;
100
- max-width: 960px;
141
+ margin: 0 auto 20px;
142
+ max-width: 720px;
101
143
  }
102
144
 
103
145
  .headerTop {
@@ -110,35 +152,38 @@
110
152
  .branchDropdown {
111
153
  display: flex;
112
154
  align-items: center;
113
- gap: 8px;
155
+ gap: 0;
114
156
  flex-shrink: 0;
157
+ position: relative;
115
158
  }
116
159
 
117
- .branchLabel {
118
- font-size: 12px;
119
- font-weight: 500;
160
+ .branchIcon {
161
+ position: absolute;
162
+ left: 10px;
120
163
  color: var(--fgColor-muted, #848d97);
121
- white-space: nowrap;
164
+ pointer-events: none;
165
+ z-index: 1;
122
166
  }
123
167
 
124
168
  .branchSelect {
125
169
  appearance: none;
126
- background-color: var(--bgColor-muted, #161b22);
170
+ background-color: transparent;
127
171
  color: var(--fgColor-default, #e6edf3);
128
172
  border: 1px solid var(--borderColor-default, #30363d);
129
- border-radius: 6px;
130
- padding: 4px 28px 4px 10px;
173
+ border-radius: 20px;
174
+ padding: 6px 32px 6px 32px;
131
175
  font-size: 13px;
132
176
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
133
177
  cursor: pointer;
134
178
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23848d97'%3E%3Cpath d='M6 8.5L1.5 4h9L6 8.5z'/%3E%3C/svg%3E");
135
179
  background-repeat: no-repeat;
136
- background-position: right 8px center;
180
+ background-position: right 12px center;
137
181
  min-width: 140px;
182
+ transition: border-color 0.15s ease;
138
183
  }
139
184
 
140
185
  .branchSelect:hover {
141
- border-color: var(--borderColor-accent-emphasis, #1f6feb);
186
+ border-color: var(--fgColor-muted, #848d97);
142
187
  }
143
188
 
144
189
  .branchSelect:focus-visible {
@@ -149,17 +194,39 @@
149
194
  .author {
150
195
  display: flex;
151
196
  align-items: center;
152
- gap: 6px;
197
+ gap: 8px;
153
198
  margin-top: 6px;
154
199
  }
155
200
 
201
+ .authorAvatars {
202
+ display: flex;
203
+ flex-direction: row;
204
+ }
205
+
206
+ .authorAvatars:hover .authorAvatar {
207
+ margin-left: 2px;
208
+ }
209
+
210
+ .authorAvatars:hover .authorAvatar:first-child {
211
+ margin-left: 0;
212
+ }
213
+
156
214
  .authorAvatar {
157
- width: 16px;
158
- height: 16px;
215
+ width: 18px;
216
+ height: 18px;
159
217
  border-radius: 50%;
218
+ margin-left: -6px;
219
+ transition: margin-left 0.15s ease;
220
+ outline: 2px solid var(--bgColor-default, #0d1117);
221
+ position: relative;
222
+ }
223
+
224
+ .authorAvatar:first-child {
225
+ margin-left: 0;
160
226
  }
161
227
 
162
228
  .authorName {
163
- font-size: 12px;
229
+ font-size: 13px;
164
230
  color: var(--fgColor-muted, #848d97);
231
+ letter-spacing: 0.01em;
165
232
  }