@dfosco/storyboard-react 4.0.0-beta.43 → 4.0.0-beta.45

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,16 +1,17 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.0.0-beta.43",
3
+ "version": "4.0.0-beta.45",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@base-ui/react": "^1.4.0",
7
- "@dfosco/storyboard-core": "4.0.0-beta.43",
8
- "@dfosco/tiny-canvas": "4.0.0-beta.43",
7
+ "@dfosco/storyboard-core": "4.0.0-beta.45",
8
+ "@dfosco/tiny-canvas": "4.0.0-beta.45",
9
9
  "@neodrag/react": "^2.3.1",
10
10
  "glob": "^11.0.0",
11
11
  "jsonc-parser": "^3.3.1",
12
12
  "remark": "^15.0.1",
13
13
  "remark-gfm": "^4.0.1",
14
+ "react-cmdk": "^1.3.9",
14
15
  "remark-html": "^16.0.1"
15
16
  },
16
17
  "license": "MIT",
@@ -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 &gt; {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 &gt; Discussions &gt; 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
+ }