@dfosco/storyboard-react 4.0.0-beta.44 → 4.0.0-beta.46
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 +3 -3
- package/src/AuthModal/AuthModal.jsx +128 -0
- package/src/AuthModal/AuthModal.module.css +200 -0
- package/src/BranchBar/BranchBar.jsx +50 -0
- package/src/BranchBar/BranchBar.module.css +230 -0
- package/src/BranchBar/useBranches.js +79 -0
- package/src/CommandPalette/CommandPalette.jsx +6 -1
- package/src/index.js +7 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@base-ui/react": "^1.4.0",
|
|
7
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
8
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
7
|
+
"@dfosco/storyboard-core": "4.0.0-beta.46",
|
|
8
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.46",
|
|
9
9
|
"@neodrag/react": "^2.3.1",
|
|
10
10
|
"glob": "^11.0.0",
|
|
11
11
|
"jsonc-parser": "^3.3.1",
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthModal — Global PAT entry dialog for comments authentication.
|
|
3
|
+
* Mounted at app root, triggered by:
|
|
4
|
+
* - Svelte CoreUIBar (comments tool / "C" shortcut) via 'storyboard:open-auth-modal' event
|
|
5
|
+
* - ViewfinderNew sidebar login button via same event
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
8
|
+
import css from './AuthModal.module.css'
|
|
9
|
+
|
|
10
|
+
const COMMENTS_TOKEN_KEY = 'sb-comments-token'
|
|
11
|
+
|
|
12
|
+
function getRepoInfo() {
|
|
13
|
+
try {
|
|
14
|
+
// eslint-disable-next-line no-undef
|
|
15
|
+
const cfg = typeof __STORYBOARD_CONFIG__ !== 'undefined' ? __STORYBOARD_CONFIG__ : null
|
|
16
|
+
const repo = cfg?.repository
|
|
17
|
+
if (repo?.owner && repo?.name) return repo
|
|
18
|
+
} catch { /* ignore */ }
|
|
19
|
+
return { owner: 'github', name: 'storyboard' }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function AuthModal() {
|
|
23
|
+
const [open, setOpen] = useState(false)
|
|
24
|
+
const [tokenValue, setTokenValue] = useState('')
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
function handleOpen() { setOpen(true) }
|
|
28
|
+
document.addEventListener('storyboard:open-auth-modal', handleOpen)
|
|
29
|
+
return () => document.removeEventListener('storyboard:open-auth-modal', handleOpen)
|
|
30
|
+
}, [])
|
|
31
|
+
|
|
32
|
+
const handleClose = useCallback(() => {
|
|
33
|
+
setOpen(false)
|
|
34
|
+
setTokenValue('')
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const handleSignIn = useCallback(() => {
|
|
38
|
+
const trimmed = tokenValue.trim()
|
|
39
|
+
if (!trimmed) return
|
|
40
|
+
|
|
41
|
+
try { localStorage.setItem(COMMENTS_TOKEN_KEY, trimmed) } catch { /* ignore */ }
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
import('@dfosco/storyboard-core/comments').then(({ setToken }) => {
|
|
45
|
+
setToken(trimmed)
|
|
46
|
+
}).catch(() => {})
|
|
47
|
+
} catch { /* comments module may not be initialized */ }
|
|
48
|
+
|
|
49
|
+
setTokenValue('')
|
|
50
|
+
setOpen(false)
|
|
51
|
+
}, [tokenValue])
|
|
52
|
+
|
|
53
|
+
const handleKeyDown = useCallback((e) => {
|
|
54
|
+
if (e.key === 'Enter') handleSignIn()
|
|
55
|
+
}, [handleSignIn])
|
|
56
|
+
|
|
57
|
+
if (!open) return null
|
|
58
|
+
|
|
59
|
+
const repo = getRepoInfo()
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={css.overlay} onClick={handleClose}>
|
|
63
|
+
<div className={css.dialog} onClick={e => e.stopPropagation()}>
|
|
64
|
+
<button className={css.closeBtn} onClick={handleClose} aria-label="Close">×</button>
|
|
65
|
+
|
|
66
|
+
<div className={css.title}>Sign in for comments</div>
|
|
67
|
+
<div className={css.desc}>
|
|
68
|
+
Leave comments for other users to see and respond, and react to! Storyboard comments use Discussions as a back-end and require a GitHub PAT to be enabled.
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<hr className={css.separator} />
|
|
72
|
+
|
|
73
|
+
<div className={css.tokenCard}>
|
|
74
|
+
<div className={css.tokenCardTitle}>Fine-grained Personal Access Token</div>
|
|
75
|
+
<div className={css.tokenCardRow}>
|
|
76
|
+
<span className={css.tokenCardLabel}>Owner:</span>
|
|
77
|
+
<code className={css.tokenCardCode}>{repo.owner}</code>
|
|
78
|
+
</div>
|
|
79
|
+
<div className={css.tokenCardRow}>
|
|
80
|
+
<span className={css.tokenCardLabel}>Expiration:</span>
|
|
81
|
+
<code className={css.tokenCardCode}>366 days</code>
|
|
82
|
+
<span className={css.tokenCardHint}>(recommended)</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className={css.tokenCardRow}>
|
|
85
|
+
<span className={css.tokenCardLabel}>Repository access:</span>
|
|
86
|
+
<code className={css.tokenCardCode}>Only select repositories > {repo.owner}/{repo.name}</code>
|
|
87
|
+
</div>
|
|
88
|
+
<div className={css.tokenCardRow}>
|
|
89
|
+
<span className={css.tokenCardLabel}>Permissions:</span>
|
|
90
|
+
<code className={css.tokenCardCode}>Repositories > Discussions > Access: Read and Write</code>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<a
|
|
95
|
+
className={css.tokenLink}
|
|
96
|
+
href="https://github.com/settings/personal-access-tokens/new"
|
|
97
|
+
target="_blank"
|
|
98
|
+
rel="noopener noreferrer"
|
|
99
|
+
>
|
|
100
|
+
Create a GitHub Fine-Grained Personal Access Token ↗
|
|
101
|
+
</a>
|
|
102
|
+
|
|
103
|
+
<hr className={css.separator} />
|
|
104
|
+
|
|
105
|
+
<label className={css.label}>Personal Access Token</label>
|
|
106
|
+
<input
|
|
107
|
+
className={css.input}
|
|
108
|
+
placeholder="github_pat_… or ghp_…"
|
|
109
|
+
type="password"
|
|
110
|
+
autoFocus
|
|
111
|
+
value={tokenValue}
|
|
112
|
+
onChange={e => setTokenValue(e.target.value)}
|
|
113
|
+
onKeyDown={handleKeyDown}
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<div className={css.warning}>
|
|
117
|
+
<span className={css.warningIcon}>⚠️</span>
|
|
118
|
+
<span>Comments are an experimental feature and may be unstable.</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className={css.actions}>
|
|
122
|
+
<button className={css.btnSecondary} onClick={handleClose}>Cancel</button>
|
|
123
|
+
<button className={css.btnPrimary} onClick={handleSignIn}>Sign in</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/* AuthModal — PAT entry dialog */
|
|
2
|
+
|
|
3
|
+
.overlay {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
background: rgba(0, 0, 0, 0.4);
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
z-index: 10000;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dialog {
|
|
14
|
+
background: #fff;
|
|
15
|
+
border-radius: 12px;
|
|
16
|
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12);
|
|
17
|
+
padding: 28px;
|
|
18
|
+
width: 480px;
|
|
19
|
+
max-width: 90vw;
|
|
20
|
+
position: relative;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.closeBtn {
|
|
24
|
+
position: absolute;
|
|
25
|
+
top: 16px;
|
|
26
|
+
right: 16px;
|
|
27
|
+
background: none;
|
|
28
|
+
border: none;
|
|
29
|
+
font-size: 24px;
|
|
30
|
+
line-height: 1;
|
|
31
|
+
color: #999;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
padding: 4px 8px;
|
|
34
|
+
border-radius: 4px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.closeBtn:hover {
|
|
38
|
+
background: #f0f0f0;
|
|
39
|
+
color: #555;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.title {
|
|
43
|
+
font-size: 18px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
margin-bottom: 8px;
|
|
46
|
+
color: #1a1a1a;
|
|
47
|
+
padding-right: 32px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.desc {
|
|
51
|
+
font-size: 15px;
|
|
52
|
+
color: #666;
|
|
53
|
+
margin-bottom: 16px;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.separator {
|
|
58
|
+
border: none;
|
|
59
|
+
border-top: 1px solid #e5e5e5;
|
|
60
|
+
margin: 16px 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Token config card — matches GitHub PAT settings screenshot */
|
|
64
|
+
|
|
65
|
+
.tokenCard {
|
|
66
|
+
background: #f6f8fa;
|
|
67
|
+
border: 1px solid #e5e5e5;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
padding: 14px 16px;
|
|
70
|
+
margin-bottom: 12px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.tokenCardTitle {
|
|
74
|
+
font-size: 15px;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
color: #1a1a1a;
|
|
77
|
+
margin-bottom: 8px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.tokenCardRow {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: baseline;
|
|
83
|
+
gap: 6px;
|
|
84
|
+
font-size: 14px;
|
|
85
|
+
line-height: 1.8;
|
|
86
|
+
color: #656d76;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.tokenCardLabel {
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.tokenCardCode {
|
|
94
|
+
font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
color: #1a1a1a;
|
|
97
|
+
background: #fff;
|
|
98
|
+
padding: 1px 6px;
|
|
99
|
+
border-radius: 4px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.tokenCardHint {
|
|
103
|
+
color: #8b949e;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.tokenLink {
|
|
107
|
+
display: inline-block;
|
|
108
|
+
font-size: 15px;
|
|
109
|
+
color: #0969da;
|
|
110
|
+
text-decoration: none;
|
|
111
|
+
margin-bottom: 4px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.tokenLink:hover {
|
|
115
|
+
text-decoration: underline;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Input */
|
|
119
|
+
|
|
120
|
+
.label {
|
|
121
|
+
display: block;
|
|
122
|
+
font-size: 15px;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
color: #1a1a1a;
|
|
125
|
+
margin-bottom: 6px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.input {
|
|
129
|
+
width: 100%;
|
|
130
|
+
padding: 10px 12px;
|
|
131
|
+
border: 1px solid #e5e5e5;
|
|
132
|
+
border-radius: 6px;
|
|
133
|
+
font-size: 15px;
|
|
134
|
+
font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
135
|
+
margin-bottom: 16px;
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.input:focus {
|
|
140
|
+
outline: none;
|
|
141
|
+
border-color: #999;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Warning banner */
|
|
145
|
+
|
|
146
|
+
.warning {
|
|
147
|
+
display: flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
gap: 8px;
|
|
150
|
+
padding: 10px 12px;
|
|
151
|
+
background: #fff8c5;
|
|
152
|
+
border: 1px solid #d4a72c;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
font-size: 14px;
|
|
155
|
+
color: #6a5300;
|
|
156
|
+
margin-bottom: 8px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.warningIcon {
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
font-size: 16px;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Actions */
|
|
165
|
+
|
|
166
|
+
.actions {
|
|
167
|
+
display: flex;
|
|
168
|
+
justify-content: flex-end;
|
|
169
|
+
gap: 8px;
|
|
170
|
+
margin-top: 8px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.btnSecondary {
|
|
174
|
+
padding: 8px 16px;
|
|
175
|
+
background: #fff;
|
|
176
|
+
border: 1px solid #e5e5e5;
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
font-size: 15px;
|
|
179
|
+
color: #555;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.btnSecondary:hover {
|
|
184
|
+
background: #f5f5f5;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.btnPrimary {
|
|
188
|
+
padding: 8px 16px;
|
|
189
|
+
background: #1a1a1a;
|
|
190
|
+
border: none;
|
|
191
|
+
border-radius: 6px;
|
|
192
|
+
font-size: 15px;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
color: #fff;
|
|
195
|
+
cursor: pointer;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.btnPrimary:hover {
|
|
199
|
+
background: #333;
|
|
200
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BranchBar — dark top bar showing current branch on non-main routes.
|
|
3
|
+
*
|
|
4
|
+
* Dev: shows branch name as a static label (use CLI to switch branches).
|
|
5
|
+
* Prod: same label (dropdown switching deferred to ViewfinderNew).
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
8
|
+
import { GitBranchIcon } from '@primer/octicons-react'
|
|
9
|
+
import css from './BranchBar.module.css'
|
|
10
|
+
|
|
11
|
+
export default function BranchBar({ basePath }) {
|
|
12
|
+
const [hidden, setHidden] = useState(false)
|
|
13
|
+
|
|
14
|
+
const currentBranch = useMemo(() => {
|
|
15
|
+
const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
|
|
16
|
+
return m ? m[1] : 'main'
|
|
17
|
+
}, [basePath])
|
|
18
|
+
|
|
19
|
+
const isOnBranch = currentBranch !== 'main'
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const observer = new MutationObserver(() => {
|
|
23
|
+
setHidden(document.documentElement.classList.contains('storyboard-chrome-hidden'))
|
|
24
|
+
})
|
|
25
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
|
|
26
|
+
return () => observer.disconnect()
|
|
27
|
+
}, [])
|
|
28
|
+
|
|
29
|
+
if (!isOnBranch || hidden) return null
|
|
30
|
+
|
|
31
|
+
function hideChrome() {
|
|
32
|
+
window.dispatchEvent(new KeyboardEvent('keydown', {
|
|
33
|
+
key: '.', metaKey: true, bubbles: true,
|
|
34
|
+
}))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={css.bar} data-branch-bar>
|
|
39
|
+
<div className={css.barInner}>
|
|
40
|
+
<span className={css.barLabel}>
|
|
41
|
+
<GitBranchIcon size={12} />
|
|
42
|
+
<span className={css.barBranchName}>{currentBranch}</span>
|
|
43
|
+
</span>
|
|
44
|
+
<div className={css.barActions}>
|
|
45
|
+
<button className={css.barAction} onClick={hideChrome}>Hide</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/* ─── BranchBar (fixed top bar) ─── */
|
|
2
|
+
|
|
3
|
+
.bar {
|
|
4
|
+
position: fixed;
|
|
5
|
+
top: 0;
|
|
6
|
+
left: 0;
|
|
7
|
+
right: 0;
|
|
8
|
+
z-index: 10000;
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
font-size: 12px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.barInner {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
height: 32px;
|
|
19
|
+
background: var(--bgColor-emphasis, #1a1a1a);
|
|
20
|
+
color: var(--fgColor-onEmphasis, #ccc);
|
|
21
|
+
padding: 4px 12px;
|
|
22
|
+
position: relative;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.barTrigger {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: 5px;
|
|
29
|
+
background: none;
|
|
30
|
+
border: none;
|
|
31
|
+
color: var(--fgColor-onEmphasis, #ddd);
|
|
32
|
+
font-size: 11px;
|
|
33
|
+
font-weight: 400;
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
padding: 2px 8px;
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
transition: background 0.1s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.barTrigger:hover {
|
|
42
|
+
background: rgba(255, 255, 255, 0.1);
|
|
43
|
+
color: #fff;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.barActions {
|
|
47
|
+
position: absolute;
|
|
48
|
+
right: 8px;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
gap: 2px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.barLabel {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
gap: 5px;
|
|
58
|
+
color: var(--fgColor-onEmphasis, #ddd);
|
|
59
|
+
font-size: 11px;
|
|
60
|
+
font-weight: 400;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.barBranchName {
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
max-width: 240px;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
text-overflow: ellipsis;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.barAction {
|
|
72
|
+
background: none;
|
|
73
|
+
border: none;
|
|
74
|
+
color: var(--fgColor-onEmphasis, #777);
|
|
75
|
+
font-size: 11px;
|
|
76
|
+
font-weight: 400;
|
|
77
|
+
font-family: inherit;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
padding: 2px 8px;
|
|
80
|
+
border-radius: 3px;
|
|
81
|
+
transition: all 0.1s;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.barAction:hover {
|
|
85
|
+
color: var(--fgColor-onEmphasis, #fff);
|
|
86
|
+
background: rgba(255, 255, 255, 0.1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Push page content down */
|
|
90
|
+
:global(html:has([data-branch-bar])) {
|
|
91
|
+
--sb-branch-bar-height: 32px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:global(html:has([data-branch-bar]) body) {
|
|
95
|
+
padding-top: 32px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
:global(html.storyboard-chrome-hidden [data-branch-bar]) {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ─── Shared dropdown (used by both BranchBar and ViewfinderNew) ─── */
|
|
103
|
+
|
|
104
|
+
.branchBtn {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
gap: 6px;
|
|
108
|
+
padding: 7px 14px;
|
|
109
|
+
background: #fff;
|
|
110
|
+
color: #555;
|
|
111
|
+
border: 1px solid #e5e5e5;
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
font-size: 16px;
|
|
114
|
+
font-weight: 500;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
transition: all 0.15s;
|
|
117
|
+
font-family: inherit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.branchBtn:hover {
|
|
121
|
+
background: #f5f5f5;
|
|
122
|
+
border-color: #ccc;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.branchBtnText {
|
|
126
|
+
max-width: 160px;
|
|
127
|
+
overflow: hidden;
|
|
128
|
+
text-overflow: ellipsis;
|
|
129
|
+
white-space: nowrap;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.spinner {
|
|
133
|
+
display: inline-block;
|
|
134
|
+
width: 12px;
|
|
135
|
+
height: 12px;
|
|
136
|
+
border: 1.5px solid rgba(255, 255, 255, 0.15);
|
|
137
|
+
border-top-color: currentColor;
|
|
138
|
+
border-radius: 50%;
|
|
139
|
+
animation: spin 0.6s linear infinite;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@keyframes spin {
|
|
143
|
+
to { transform: rotate(360deg); }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.branchPositioner {
|
|
147
|
+
z-index: 10001;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.branchPopup {
|
|
151
|
+
background: #fff;
|
|
152
|
+
border: 1px solid #e5e5e5;
|
|
153
|
+
border-radius: 10px;
|
|
154
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
155
|
+
min-width: 240px;
|
|
156
|
+
max-width: 320px;
|
|
157
|
+
padding: 6px 0;
|
|
158
|
+
font-family: 'Mona Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.branchSectionLabel {
|
|
162
|
+
padding: 8px 14px 4px;
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
color: #999;
|
|
166
|
+
text-transform: uppercase;
|
|
167
|
+
letter-spacing: 0.4px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.branchItem {
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
padding: 7px 14px;
|
|
175
|
+
font-size: 16px;
|
|
176
|
+
color: #555;
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
transition: background 0.1s;
|
|
179
|
+
border: none;
|
|
180
|
+
background: none;
|
|
181
|
+
width: 100%;
|
|
182
|
+
text-align: left;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.branchItem:hover,
|
|
186
|
+
.branchItem[data-highlighted] {
|
|
187
|
+
background: #f5f5f5;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.branchItemActive {
|
|
191
|
+
composes: branchItem;
|
|
192
|
+
color: #1a1a1a;
|
|
193
|
+
font-weight: 600;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.branchSeparator {
|
|
197
|
+
height: 1px;
|
|
198
|
+
background: #e5e5e5;
|
|
199
|
+
margin: 4px 10px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.branchViewport {
|
|
203
|
+
max-height: 280px;
|
|
204
|
+
overflow-y: auto;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.branchShowAll {
|
|
208
|
+
display: block;
|
|
209
|
+
width: 100%;
|
|
210
|
+
padding: 8px 14px;
|
|
211
|
+
font-size: 16px;
|
|
212
|
+
color: #2563eb;
|
|
213
|
+
background: none;
|
|
214
|
+
border: none;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
text-align: left;
|
|
217
|
+
font-family: inherit;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.branchShowAll:hover {
|
|
221
|
+
background: #f5f5f5;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.branchError {
|
|
225
|
+
padding: 6px 14px;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
color: #ef4444;
|
|
228
|
+
background: #fef2f2;
|
|
229
|
+
border-bottom: 1px solid #fecaca;
|
|
230
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBranches — shared hook for branch data across BranchBar and BranchDropdown.
|
|
3
|
+
* Fetches live branch list from /_storyboard/worktrees API on mount.
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect, useMemo, useCallback } from 'react'
|
|
6
|
+
|
|
7
|
+
const isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
|
|
8
|
+
|
|
9
|
+
export function useBranches(basePath) {
|
|
10
|
+
const [branches, setBranches] = useState(() => {
|
|
11
|
+
if (typeof window !== 'undefined' && Array.isArray(window.__SB_BRANCHES__)) {
|
|
12
|
+
return window.__SB_BRANCHES__
|
|
13
|
+
}
|
|
14
|
+
return null
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const [gitUser, setGitUser] = useState(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const apiBase = (basePath || '/').replace(/\/$/, '')
|
|
21
|
+
|
|
22
|
+
fetch(`${apiBase}/_storyboard/git-user`).then(r => r.ok ? r.json() : null)
|
|
23
|
+
.then(data => { if (data?.name) setGitUser(data.name) })
|
|
24
|
+
.catch(() => {})
|
|
25
|
+
|
|
26
|
+
// Always fetch live branch list
|
|
27
|
+
fetch(`${apiBase}/_storyboard/worktrees`).then(r => r.ok ? r.json() : null)
|
|
28
|
+
.then(data => { if (Array.isArray(data) && data.length > 0) setBranches(data) })
|
|
29
|
+
.catch(() => {})
|
|
30
|
+
}, [basePath])
|
|
31
|
+
|
|
32
|
+
const currentBranch = useMemo(() => {
|
|
33
|
+
const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
|
|
34
|
+
return m ? m[1] : 'main'
|
|
35
|
+
}, [basePath])
|
|
36
|
+
|
|
37
|
+
const branchBasePath = (basePath || '/').replace(/\/branch--[^/]*\/$/, '/')
|
|
38
|
+
|
|
39
|
+
return { branches, currentBranch, branchBasePath, gitUser }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useSwitchBranch(basePath, branchBasePath) {
|
|
43
|
+
const [switching, setSwitching] = useState(null)
|
|
44
|
+
const [switchError, setSwitchError] = useState(null)
|
|
45
|
+
|
|
46
|
+
const switchBranch = useCallback(async (branch, folder) => {
|
|
47
|
+
if (switching) return
|
|
48
|
+
setSwitching(branch)
|
|
49
|
+
setSwitchError(null)
|
|
50
|
+
|
|
51
|
+
if (!isLocalDev) {
|
|
52
|
+
// Prod: direct navigation
|
|
53
|
+
window.location.href = `${branchBasePath}${folder || (branch === 'main' ? '' : `branch--${branch}/`)}`
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Dev: call switch-branch API to spin up server
|
|
58
|
+
const apiBase = (basePath || '/').replace(/\/$/, '')
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${apiBase}/_storyboard/switch-branch`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ branch }),
|
|
64
|
+
})
|
|
65
|
+
const data = await res.json()
|
|
66
|
+
if (res.ok && data.url) {
|
|
67
|
+
window.location.href = data.url
|
|
68
|
+
} else {
|
|
69
|
+
setSwitchError(data.error || 'Failed to switch')
|
|
70
|
+
setSwitching(null)
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
setSwitchError(e.message || 'Server not reachable')
|
|
74
|
+
setSwitching(null)
|
|
75
|
+
}
|
|
76
|
+
}, [basePath, branchBasePath, switching])
|
|
77
|
+
|
|
78
|
+
return { switching, switchError, switchBranch }
|
|
79
|
+
}
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
isExcludedByRoute,
|
|
21
21
|
} from '@dfosco/storyboard-core'
|
|
22
22
|
import CreateDialog from './CreateDialog.jsx'
|
|
23
|
+
import BranchBar from '../BranchBar/BranchBar.jsx'
|
|
24
|
+
import AuthModal from '../AuthModal/AuthModal.jsx'
|
|
23
25
|
import './command-palette.css'
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -797,8 +799,9 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
797
799
|
|
|
798
800
|
// Author search: match usernames against author index
|
|
799
801
|
const q = search.toLowerCase()
|
|
802
|
+
const authorQ = q.startsWith('@') ? q.slice(1) : q
|
|
800
803
|
for (const [key, { author, items: authorItems }] of authorIndex) {
|
|
801
|
-
if (!key.includes(
|
|
804
|
+
if (!key.includes(authorQ)) continue
|
|
802
805
|
// Avoid duplicates with already-shown artifact items
|
|
803
806
|
const shownIds = new Set(result.flatMap(g => g.items.map(i => i.id)))
|
|
804
807
|
const uniqueItems = authorItems.filter(item => !shownIds.has(`author:${item.id}`))
|
|
@@ -913,6 +916,8 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
913
916
|
basePath={basePath}
|
|
914
917
|
onClose={() => setCreateType(null)}
|
|
915
918
|
/>
|
|
919
|
+
<BranchBar basePath={basePath} />
|
|
920
|
+
<AuthModal />
|
|
916
921
|
</>
|
|
917
922
|
)
|
|
918
923
|
}
|
package/src/index.js
CHANGED
|
@@ -37,9 +37,15 @@ export { FormContext } from './context/FormContext.js'
|
|
|
37
37
|
// Viewfinder dashboard
|
|
38
38
|
export { default as Viewfinder } from './Viewfinder.jsx'
|
|
39
39
|
|
|
40
|
-
// Command Palette
|
|
40
|
+
// Command Palette (includes BranchBar automatically)
|
|
41
41
|
export { default as StoryboardCommandPalette } from './CommandPalette/CommandPalette.jsx'
|
|
42
42
|
|
|
43
|
+
// Branch Bar (standalone, for consumers who don't use CommandPalette)
|
|
44
|
+
export { default as BranchBar } from './BranchBar/BranchBar.jsx'
|
|
45
|
+
|
|
46
|
+
// Auth Modal (standalone, for consumers who don't use CommandPalette)
|
|
47
|
+
export { default as AuthModal } from './AuthModal/AuthModal.jsx'
|
|
48
|
+
|
|
43
49
|
// Canvas
|
|
44
50
|
export { default as CanvasPage } from './canvas/CanvasPage.jsx'
|
|
45
51
|
export { useCanvas } from './canvas/useCanvas.js'
|