@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.
- package/modes.config.json +17 -0
- package/package.json +8 -3
- package/src/bodyClasses.js +11 -8
- package/src/bodyClasses.test.js +26 -12
- package/src/devtools.js +9 -8
- package/src/devtools.test.js +2 -2
- package/src/index.js +20 -5
- package/src/loader.js +116 -35
- package/src/loader.test.js +189 -48
- package/src/modes.js +170 -0
- package/src/modes.test.js +216 -0
- package/src/sceneDebug.js +17 -13
- package/src/sceneDebug.test.js +42 -29
- package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +77 -19
- package/src/svelte-plugin-ui/__tests__/designModes.test.ts +1 -1
- package/src/svelte-plugin-ui/components/ToolbarShell.svelte +59 -19
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +573 -0
- package/src/svelte-plugin-ui/index.ts +3 -0
- package/src/svelte-plugin-ui/stores/toolStore.ts +71 -0
- package/src/svelte-plugin-ui/stores/types.ts +22 -8
- package/src/{svelte-plugin-ui/plugins → ui}/design-modes.ts +9 -7
- package/src/ui/viewfinder.ts +58 -0
- package/src/viewfinder.js +100 -20
- package/src/viewfinder.test.js +64 -42
- package/src/workshop/features/createPage/server.js +8 -8
|
@@ -1,51 +1,64 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
ToolbarShell — right-side toolbar container with two stacked groups:
|
|
3
|
-
1. Mode-specific tools (
|
|
4
|
-
2. Developer tools (
|
|
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
|
|
10
|
+
Only renders when the current mode has visible tools.
|
|
8
11
|
-->
|
|
9
12
|
|
|
10
13
|
<script lang="ts">
|
|
11
|
-
import {
|
|
12
|
-
import type { ModeToolConfig } from '../stores/types.js'
|
|
14
|
+
import { toolState } from '../stores/toolStore.js'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|