@dfosco/storyboard-core 2.0.0 → 2.2.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.
@@ -1,51 +1,64 @@
1
1
  <!--
2
2
  ToolbarShell — right-side toolbar container with two stacked groups:
3
- 1. Mode-specific tools (from the active mode's `tools` array)
4
- 2. Developer tools (from the active mode's `devTools` array)
3
+ 1. Mode-specific tools (group: 'tools')
4
+ 2. Developer tools (group: 'dev')
5
+
6
+ Reads from the tool store, which sources from the declarative tool
7
+ registry (modes.config.json) + runtime state (setToolState/setToolAction).
5
8
 
6
9
  Fixed to the right side of the viewport, above the ModeSwitch.
7
- Only renders when the current mode provides tools or devTools.
10
+ Only renders when the current mode has visible tools.
8
11
  -->
9
12
 
10
13
  <script lang="ts">
11
- import { modeState } from '../stores/modeStore.js'
12
- import type { ModeToolConfig } from '../stores/types.js'
14
+ import { toolState } from '../stores/toolStore.js'
13
15
 
14
- let tools: ModeToolConfig[] = $derived(
15
- ($modeState.currentModeConfig as any)?.tools ?? []
16
- )
17
- let devTools: ModeToolConfig[] = $derived(
18
- ($modeState.currentModeConfig as any)?.devTools ?? []
19
- )
16
+ function handleClick(tool: any) {
17
+ if (tool.action && tool.state.enabled && !tool.state.busy) {
18
+ tool.action()
19
+ }
20
+ }
20
21
  </script>
21
22
 
22
- {#if tools.length > 0 || devTools.length > 0}
23
+ {#if $toolState.tools.length > 0 || $toolState.devTools.length > 0}
23
24
  <div class="sb-toolbar-shell">
24
- {#if tools.length > 0}
25
+ {#if $toolState.tools.length > 0}
25
26
  <div class="sb-toolbar" role="toolbar" aria-label="Mode tools">
26
27
  <span class="sb-toolbar-label">Tools</span>
27
- {#each tools as tool (tool.id)}
28
+ {#each $toolState.tools as tool (tool.id)}
28
29
  <button
29
30
  class="sb-tool-btn"
30
- onclick={tool.action}
31
+ class:sb-tool-btn-active={tool.state.active}
32
+ class:sb-tool-btn-busy={tool.state.busy}
33
+ onclick={() => handleClick(tool)}
34
+ disabled={!tool.state.enabled || tool.state.busy || !tool.action}
31
35
  title={tool.label}
32
36
  >
33
37
  {tool.label}
38
+ {#if tool.state.badge != null}
39
+ <span class="sb-tool-badge">{tool.state.badge}</span>
40
+ {/if}
34
41
  </button>
35
42
  {/each}
36
43
  </div>
37
44
  {/if}
38
45
 
39
- {#if devTools.length > 0}
46
+ {#if $toolState.devTools.length > 0}
40
47
  <div class="sb-toolbar" role="toolbar" aria-label="Developer tools">
41
48
  <span class="sb-toolbar-label">Dev</span>
42
- {#each devTools as tool (tool.id)}
49
+ {#each $toolState.devTools as tool (tool.id)}
43
50
  <button
44
51
  class="sb-tool-btn"
45
- onclick={tool.action}
52
+ class:sb-tool-btn-active={tool.state.active}
53
+ class:sb-tool-btn-busy={tool.state.busy}
54
+ onclick={() => handleClick(tool)}
55
+ disabled={!tool.state.enabled || tool.state.busy || !tool.action}
46
56
  title={tool.label}
47
57
  >
48
58
  {tool.label}
59
+ {#if tool.state.badge != null}
60
+ <span class="sb-tool-badge">{tool.state.badge}</span>
61
+ {/if}
49
62
  </button>
50
63
  {/each}
51
64
  </div>
@@ -91,13 +104,40 @@
91
104
  white-space: nowrap;
92
105
  text-align: left;
93
106
  line-height: 1;
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 6px;
94
110
  }
95
111
 
96
- .sb-tool-btn:hover {
112
+ .sb-tool-btn:hover:not(:disabled) {
97
113
  color: var(--fgColor-default, #e6edf3);
98
114
  background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.1));
99
115
  }
100
116
 
117
+ .sb-tool-btn:disabled {
118
+ opacity: 0.4;
119
+ cursor: default;
120
+ }
121
+
122
+ .sb-tool-btn-active {
123
+ color: var(--fgColor-default, #e6edf3);
124
+ background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.15));
125
+ }
126
+
127
+ .sb-tool-btn-busy {
128
+ opacity: 0.6;
129
+ }
130
+
131
+ .sb-tool-badge {
132
+ font-size: 10px;
133
+ font-weight: 600;
134
+ background: var(--bgColor-accent-muted, rgba(56, 139, 253, 0.15));
135
+ color: var(--fgColor-accent, #58a6ff);
136
+ padding: 1px 5px;
137
+ border-radius: 10px;
138
+ line-height: 1.2;
139
+ }
140
+
101
141
  .sb-toolbar-label {
102
142
  font-size: 10px;
103
143
  font-weight: 600;
@@ -0,0 +1,573 @@
1
+ <!--
2
+ Viewfinder — prototype index and flow dashboard.
3
+
4
+ Full-page component that lists prototypes as expandable groups,
5
+ each showing its flows. Global flows (not belonging to any prototype)
6
+ appear as an "Other flows" group.
7
+
8
+ Mounted via mountViewfinder() from the viewfinder plugin entry point.
9
+ -->
10
+
11
+ <script lang="ts">
12
+ import { buildPrototypeIndex } from '../../viewfinder.js'
13
+
14
+ interface Props {
15
+ title?: string
16
+ subtitle?: string
17
+ basePath?: string
18
+ knownRoutes?: string[]
19
+ showThumbnails?: boolean
20
+ hideDefaultFlow?: boolean
21
+ }
22
+
23
+ let {
24
+ title = 'Storyboard',
25
+ subtitle = '',
26
+ basePath = '/',
27
+ knownRoutes = [],
28
+ showThumbnails = false,
29
+ hideDefaultFlow = false,
30
+ }: Props = $props()
31
+
32
+ const prototypeIndex = $derived(buildPrototypeIndex(knownRoutes))
33
+
34
+ const globalFlows = $derived(
35
+ hideDefaultFlow
36
+ ? prototypeIndex.globalFlows.filter((f: any) => f.key !== 'default')
37
+ : prototypeIndex.globalFlows
38
+ )
39
+
40
+ // Merge global flows into the prototype list as "Other flows"
41
+ const allGroups = $derived(
42
+ globalFlows.length > 0
43
+ ? [
44
+ ...prototypeIndex.prototypes,
45
+ {
46
+ name: 'Other flows',
47
+ dirName: '__global__',
48
+ description: null,
49
+ author: null,
50
+ gitAuthor: null,
51
+ icon: null,
52
+ team: null,
53
+ tags: null,
54
+ flows: globalFlows,
55
+ },
56
+ ]
57
+ : prototypeIndex.prototypes
58
+ )
59
+
60
+ const totalFlows = $derived(
61
+ allGroups.reduce((sum: number, p: any) => sum + p.flows.length, 0)
62
+ )
63
+
64
+ // Expanded state — all prototypes start expanded
65
+ let expanded: Record<string, boolean> = $state({})
66
+
67
+ function isExpanded(dirName: string): boolean {
68
+ return expanded[dirName] ?? true
69
+ }
70
+
71
+ function togglePrototype(dirName: string) {
72
+ expanded[dirName] = !expanded[dirName]
73
+ }
74
+
75
+ function protoRoute(dirName: string): string {
76
+ return `/${dirName}`
77
+ }
78
+
79
+ function formatName(name: string): string {
80
+ return name
81
+ .split('-')
82
+ .map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
83
+ .join(' ')
84
+ }
85
+
86
+ function placeholderSvg(name: string): string {
87
+ const h = (function hashStr(s: string) {
88
+ let v = 0
89
+ for (let i = 0; i < s.length; i++) v = ((v << 5) - v + s.charCodeAt(i)) | 0
90
+ return Math.abs(v)
91
+ })(name)
92
+
93
+ let rects = ''
94
+ for (let i = 0; i < 12; i++) {
95
+ const s = h * (i + 1)
96
+ const x = (s * 7 + i * 31) % 320
97
+ const y = (s * 13 + i * 17) % 200
98
+ const w = 20 + (s * (i + 3)) % 80
99
+ const ht = 8 + (s * (i + 7)) % 40
100
+ const opacity = 0.06 + ((s * (i + 2)) % 20) / 100
101
+ const fill = i % 3 === 0 ? 'var(--placeholder-accent)' : i % 3 === 1 ? 'var(--placeholder-fg)' : 'var(--placeholder-muted)'
102
+ rects += `<rect x="${x}" y="${y}" width="${w}" height="${ht}" rx="2" fill="${fill}" opacity="${opacity}" />`
103
+ }
104
+
105
+ let lines = ''
106
+ for (let i = 0; i < 6; i++) {
107
+ const s = h * (i + 5)
108
+ const y = 10 + (s % 180)
109
+ lines += `<line x1="0" y1="${y}" x2="320" y2="${y}" stroke="var(--placeholder-grid)" stroke-width="0.5" opacity="0.4" />`
110
+ }
111
+ for (let i = 0; i < 8; i++) {
112
+ const s = h * (i + 9)
113
+ const x = 10 + (s % 300)
114
+ lines += `<line x1="${x}" y1="0" x2="${x}" y2="200" stroke="var(--placeholder-grid)" stroke-width="0.5" opacity="0.3" />`
115
+ }
116
+
117
+ return `<svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="320" height="200" fill="var(--placeholder-bg)" />${lines}${rects}</svg>`
118
+ }
119
+
120
+ // Branch switching
121
+ interface Branch { branch: string; folder: string }
122
+
123
+ const MOCK_BRANCHES: Branch[] = [
124
+ { branch: 'main', folder: '' },
125
+ { branch: 'feat/comments-v2', folder: 'branch--feat-comments-v2' },
126
+ { branch: 'fix/nav-overflow', folder: 'branch--fix-nav-overflow' },
127
+ ]
128
+
129
+ let branches: Branch[] | null = $state(null)
130
+
131
+ const branchBasePath = $derived(
132
+ (basePath || '/storyboard-source/').replace(/\/branch--[^/]*\/$/, '/')
133
+ )
134
+
135
+ const currentBranch = $derived(
136
+ (() => {
137
+ const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
138
+ return m ? m[1] : 'main'
139
+ })()
140
+ )
141
+
142
+ $effect(() => {
143
+ fetch(`${branchBasePath}branches.json`)
144
+ .then(r => r.ok ? r.json() : null)
145
+ .then((data: any) => {
146
+ branches = Array.isArray(data) && data.length > 0 ? data : MOCK_BRANCHES
147
+ })
148
+ .catch(() => { branches = MOCK_BRANCHES })
149
+ })
150
+
151
+ function handleBranchChange(e: Event) {
152
+ const folder = (e.target as HTMLSelectElement).value
153
+ if (folder) {
154
+ window.location.href = `${branchBasePath}${folder}/`
155
+ }
156
+ }
157
+ </script>
158
+
159
+ <div class="container">
160
+ <header class="header">
161
+ <div class="headerTop">
162
+ <div>
163
+ <h1 class="title">{title}</h1>
164
+ {#if subtitle}
165
+ <p class="subtitle">{subtitle}</p>
166
+ {/if}
167
+ </div>
168
+ {#if branches && branches.length > 0}
169
+ <div class="branchDropdown">
170
+ <svg class="branchIcon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
171
+ <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" />
172
+ </svg>
173
+ <select
174
+ class="branchSelect"
175
+ onchange={handleBranchChange}
176
+ aria-label="Switch branch"
177
+ >
178
+ <option value="" disabled selected>{currentBranch}</option>
179
+ {#each branches as b (b.folder)}
180
+ <option value={b.folder}>{b.branch}</option>
181
+ {/each}
182
+ </select>
183
+ </div>
184
+ {/if}
185
+ </div>
186
+ <p class="sceneCount">
187
+ {allGroups.length} prototype{allGroups.length !== 1 ? 's' : ''} · {totalFlows} flow{totalFlows !== 1 ? 's' : ''}
188
+ </p>
189
+ </header>
190
+
191
+ {#if allGroups.length === 0}
192
+ <p class="empty">No flows found. Add a <code>*.flow.json</code> file to get started.</p>
193
+ {:else}
194
+ <div class="list">
195
+ {#each allGroups as proto (proto.dirName)}
196
+ <section class="protoGroup">
197
+ {#if proto.flows.length > 0}
198
+ <!-- Expandable prototype with flows -->
199
+ <button
200
+ class="listItem protoHeader"
201
+ onclick={() => togglePrototype(proto.dirName)}
202
+ aria-expanded={isExpanded(proto.dirName)}
203
+ >
204
+ <div class="cardBody">
205
+ <p class="sceneName">
206
+ {#if proto.icon}<span class="protoIcon">{proto.icon}</span>{/if}
207
+ {proto.name}
208
+ <span class="protoChevron" class:protoChevronOpen={isExpanded(proto.dirName)}>
209
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
210
+ <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z" />
211
+ </svg>
212
+ </span>
213
+ </p>
214
+ {#if proto.description}
215
+ <p class="protoDesc">{proto.description}</p>
216
+ {/if}
217
+ {#if proto.author}
218
+ {@const authors = Array.isArray(proto.author) ? proto.author : [proto.author]}
219
+ <div class="author">
220
+ <span class="authorAvatars">
221
+ {#each authors as a (a)}
222
+ <img
223
+ src="https://github.com/{a}.png?size=48"
224
+ alt={a}
225
+ class="authorAvatar"
226
+ />
227
+ {/each}
228
+ </span>
229
+ <span class="authorName">{authors.join(', ')}</span>
230
+ </div>
231
+ {:else if proto.gitAuthor}
232
+ <p class="authorPlain">{proto.gitAuthor}</p>
233
+ {/if}
234
+ </div>
235
+ </button>
236
+ {:else}
237
+ <!-- Prototype with no flows — navigates directly -->
238
+ <a class="listItem" href={protoRoute(proto.dirName)}>
239
+ <div class="cardBody">
240
+ <p class="sceneName">
241
+ {#if proto.icon}<span class="protoIcon">{proto.icon}</span>{/if}
242
+ {proto.name}
243
+ </p>
244
+ {#if proto.description}
245
+ <p class="protoDesc">{proto.description}</p>
246
+ {/if}
247
+ {#if proto.author}
248
+ {@const authors = Array.isArray(proto.author) ? proto.author : [proto.author]}
249
+ <div class="author">
250
+ <span class="authorAvatars">
251
+ {#each authors as a (a)}
252
+ <img
253
+ src="https://github.com/{a}.png?size=48"
254
+ alt={a}
255
+ class="authorAvatar"
256
+ />
257
+ {/each}
258
+ </span>
259
+ <span class="authorName">{authors.join(', ')}</span>
260
+ </div>
261
+ {:else if proto.gitAuthor}
262
+ <p class="authorPlain">{proto.gitAuthor}</p>
263
+ {/if}
264
+ </div>
265
+ </a>
266
+ {/if}
267
+
268
+ {#if isExpanded(proto.dirName) && proto.flows.length > 0}
269
+ <div class="flowList">
270
+ {#each proto.flows as flow (flow.key)}
271
+ <a href={flow.route} class="listItem flowItem">
272
+ {#if showThumbnails}
273
+ <div class="thumbnail">
274
+ {@html placeholderSvg(flow.key)}
275
+ </div>
276
+ {/if}
277
+ <div class="cardBody">
278
+ <p class="sceneName">{flow.meta?.title || formatName(flow.name)}</p>
279
+ {#if flow.meta?.description}
280
+ <p class="flowDesc">{flow.meta.description}</p>
281
+ {/if}
282
+ </div>
283
+ </a>
284
+ {/each}
285
+ </div>
286
+ {/if}
287
+ </section>
288
+ {/each}
289
+ </div>
290
+ {/if}
291
+ </div>
292
+
293
+ <style>
294
+ .container {
295
+ min-height: 100vh;
296
+ background-color: var(--bgColor-default, #0d1117);
297
+ color: var(--fgColor-default, #e6edf3);
298
+ padding: 80px 32px 48px;
299
+ }
300
+
301
+ .header {
302
+ max-width: 720px;
303
+ margin: 0 auto 64px;
304
+ }
305
+
306
+ .headerTop {
307
+ display: flex;
308
+ align-items: baseline;
309
+ justify-content: space-between;
310
+ gap: 16px;
311
+ }
312
+
313
+ .title {
314
+ font-size: 72px;
315
+ font-weight: 400;
316
+ margin: 0 0 12px;
317
+ color: var(--fgColor-default, #e6edf3);
318
+ letter-spacing: -0.03em;
319
+ line-height: 1;
320
+ }
321
+
322
+ .subtitle {
323
+ font-size: 15px;
324
+ color: var(--fgColor-muted, #848d97);
325
+ margin: 4px 0 0;
326
+ letter-spacing: 0.01em;
327
+ }
328
+
329
+ .sceneCount {
330
+ font-size: 13px;
331
+ color: var(--fgColor-muted, #848d97);
332
+ margin: 16px 0 0;
333
+ letter-spacing: 0.01em;
334
+ }
335
+
336
+ .branchDropdown {
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 0;
340
+ flex-shrink: 0;
341
+ position: relative;
342
+ }
343
+
344
+ .branchIcon {
345
+ position: absolute;
346
+ left: 10px;
347
+ color: var(--fgColor-muted, #848d97);
348
+ pointer-events: none;
349
+ z-index: 1;
350
+ }
351
+
352
+ .branchSelect {
353
+ appearance: none;
354
+ background-color: transparent;
355
+ color: var(--fgColor-default, #e6edf3);
356
+ border: 1px solid var(--borderColor-default, #30363d);
357
+ border-radius: 20px;
358
+ padding: 6px 32px 6px 32px;
359
+ font-size: 13px;
360
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
361
+ cursor: pointer;
362
+ 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");
363
+ background-repeat: no-repeat;
364
+ background-position: right 12px center;
365
+ min-width: 140px;
366
+ max-width: 220px;
367
+ text-overflow: ellipsis;
368
+ overflow: hidden;
369
+ transition: border-color 0.15s ease;
370
+ }
371
+
372
+ .branchSelect:hover {
373
+ border-color: var(--fgColor-muted, #848d97);
374
+ }
375
+
376
+ .branchSelect:focus-visible {
377
+ outline: 2px solid var(--borderColor-accent-emphasis, #1f6feb);
378
+ outline-offset: -1px;
379
+ }
380
+
381
+ .list {
382
+ display: flex;
383
+ flex-direction: column;
384
+ max-width: 720px;
385
+ margin: 0 auto;
386
+ }
387
+
388
+ .protoGroup {
389
+ display: flex;
390
+ flex-direction: column;
391
+ }
392
+
393
+ .listItem {
394
+ display: block;
395
+ padding: 8px 0;
396
+ text-decoration: none;
397
+ color: inherit;
398
+ }
399
+
400
+ .listItem:hover {
401
+ text-decoration: none !important;
402
+ }
403
+
404
+ .protoHeader {
405
+ appearance: none;
406
+ border: none;
407
+ background: none;
408
+ width: 100%;
409
+ text-align: left;
410
+ cursor: pointer;
411
+ color: inherit;
412
+ padding: 8px 0;
413
+ }
414
+
415
+ .cardBody {
416
+ padding: 12px 16px;
417
+ }
418
+
419
+ .cardBody:hover {
420
+ background-color: var(--bgColor-muted, #161b22);
421
+ border-radius: 8px;
422
+ }
423
+
424
+ .sceneName {
425
+ font-size: var(--text-title-size-medium);
426
+ font-weight: 400;
427
+ color: var(--fgColor-default, #e6edf3);
428
+ margin: 0;
429
+ letter-spacing: -0.02em;
430
+ line-height: 1.6;
431
+ transition: font-style 0.15s ease;
432
+ }
433
+
434
+ .protoChevron {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ color: var(--fgColor-muted, #848d97);
438
+ transition: transform 0.15s ease;
439
+ transform: rotate(0deg);
440
+ margin-right: 4px;
441
+ vertical-align: middle;
442
+ }
443
+
444
+ .protoChevronOpen {
445
+ transform: rotate(90deg);
446
+ }
447
+
448
+ .protoIcon {
449
+ margin-right: 4px;
450
+ }
451
+
452
+ .protoDesc {
453
+ font-size: 13px;
454
+ color: var(--fgColor-muted, #848d97);
455
+ margin: 4px 0 0;
456
+ letter-spacing: 0.01em;
457
+ }
458
+
459
+ .author {
460
+ display: flex;
461
+ align-items: center;
462
+ gap: 8px;
463
+ margin-top: 6px;
464
+ }
465
+
466
+ .authorAvatars {
467
+ display: flex;
468
+ flex-direction: row;
469
+ }
470
+
471
+ .authorAvatars:hover .authorAvatar:not(:first-child) {
472
+ margin-left: -2px;
473
+ }
474
+
475
+ .authorAvatar {
476
+ width: 24px;
477
+ height: 24px;
478
+ border-radius: 50%;
479
+ margin-left: -8px;
480
+ transition: margin-left 50ms linear;
481
+ outline: 2px solid var(--bgColor-default, #0d1117);
482
+ position: relative;
483
+ }
484
+
485
+ .authorAvatar:first-child {
486
+ margin-left: 0;
487
+ }
488
+
489
+
490
+ .authorName {
491
+ font-size: 13px;
492
+ color: var(--fgColor-muted, #848d97);
493
+ letter-spacing: 0.01em;
494
+ }
495
+
496
+ .authorPlain {
497
+ font-size: 13px;
498
+ color: var(--fgColor-muted);
499
+ margin: 4px 0 0;
500
+ letter-spacing: 0.01em;
501
+ }
502
+
503
+ .flowList {
504
+ margin: 0 var(--base-size-12);
505
+ padding: 0;
506
+ display: flex;
507
+ flex-direction: column;
508
+ }
509
+
510
+ .flowItem {
511
+ border: 1px solid var(--borderColor-muted, #30363d);
512
+ padding: 0;
513
+ }
514
+
515
+ .flowItem:not(:first-child) {
516
+ margin-top: -1px;
517
+ }
518
+
519
+ .flowItem:first-child {
520
+ border-top-left-radius: var(--base-size-6);
521
+ border-top-right-radius: var(--base-size-6);
522
+ }
523
+
524
+ .flowItem:last-child {
525
+ border-bottom-left-radius: var(--base-size-6);
526
+ border-bottom-right-radius: var(--base-size-6);
527
+ }
528
+
529
+ .flowItem:only-child {
530
+ border-radius: var(--base-size-6);
531
+ }
532
+
533
+ .flowItem .sceneName {
534
+ font-size: var(--text-title-size-small);
535
+ color: var(--fgColor-muted);
536
+ }
537
+
538
+ .flowDesc {
539
+ font-size: 13px;
540
+ color: var(--fgColor-muted, #848d97);
541
+ margin: 4px 0 0;
542
+ letter-spacing: 0.01em;
543
+ }
544
+
545
+ .thumbnail {
546
+ aspect-ratio: 16 / 10;
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ overflow: hidden;
551
+ background: var(--bgColor-inset, #010409);
552
+
553
+ --placeholder-bg: var(--bgColor-inset, #010409);
554
+ --placeholder-grid: var(--borderColor-default, #30363d);
555
+ --placeholder-accent: var(--fgColor-accent, #58a6ff);
556
+ --placeholder-fg: var(--fgColor-default, #c9d1d9);
557
+ --placeholder-muted: var(--fgColor-muted, #484f58);
558
+ }
559
+
560
+ .thumbnail :global(svg) {
561
+ width: 100%;
562
+ height: 100%;
563
+ }
564
+
565
+ .empty {
566
+ text-align: center;
567
+ padding: 80px 24px;
568
+ color: var(--fgColor-muted, #848d97);
569
+ font-size: 15px;
570
+ max-width: 720px;
571
+ margin: 0 auto;
572
+ }
573
+ </style>
@@ -15,3 +15,6 @@ export { modeState, switchMode } from './stores/modeStore.js'
15
15
  // Type re-exports
16
16
  export type { ModeState } from './stores/modeStore.js'
17
17
  export type { ModeConfig, ModeToolConfig } from './stores/types.js'
18
+
19
+ // Viewfinder
20
+ export { mountViewfinder, unmountViewfinder, type ViewfinderProps } from './plugins/viewfinder.js'