@dfosco/storyboard-react 4.0.0-beta.9 → 4.0.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.
Files changed (75) hide show
  1. package/package.json +6 -3
  2. package/src/AuthModal/AuthModal.jsx +134 -0
  3. package/src/AuthModal/AuthModal.module.css +221 -0
  4. package/src/BranchBar/BranchBar.jsx +56 -0
  5. package/src/BranchBar/BranchBar.module.css +230 -0
  6. package/src/BranchBar/useBranches.js +79 -0
  7. package/src/CommandPalette/CommandPalette.jsx +936 -0
  8. package/src/CommandPalette/CreateDialog.jsx +219 -0
  9. package/src/CommandPalette/command-palette.css +111 -0
  10. package/src/Icon.jsx +180 -0
  11. package/src/Viewfinder.jsx +1104 -57
  12. package/src/Viewfinder.module.css +1107 -149
  13. package/src/canvas/CanvasControls.jsx +51 -2
  14. package/src/canvas/CanvasControls.module.css +31 -0
  15. package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
  16. package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
  17. package/src/canvas/CanvasPage.jsx +807 -251
  18. package/src/canvas/CanvasPage.module.css +98 -50
  19. package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
  20. package/src/canvas/CanvasToolbar.jsx +2 -2
  21. package/src/canvas/MarqueeOverlay.jsx +20 -0
  22. package/src/canvas/PageSelector.jsx +239 -0
  23. package/src/canvas/PageSelector.module.css +165 -0
  24. package/src/canvas/PageSelector.test.jsx +104 -0
  25. package/src/canvas/canvasApi.js +22 -8
  26. package/src/canvas/canvasTheme.js +96 -52
  27. package/src/canvas/componentIsolate.jsx +33 -7
  28. package/src/canvas/useCanvas.js +9 -8
  29. package/src/canvas/useCanvas.test.js +4 -4
  30. package/src/canvas/useMarqueeSelect.js +187 -0
  31. package/src/canvas/useMarqueeSelect.test.js +78 -0
  32. package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
  33. package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
  34. package/src/canvas/widgets/ComponentWidget.jsx +42 -10
  35. package/src/canvas/widgets/ComponentWidget.module.css +6 -5
  36. package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
  37. package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
  38. package/src/canvas/widgets/LinkPreview.jsx +297 -11
  39. package/src/canvas/widgets/LinkPreview.module.css +386 -18
  40. package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
  41. package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
  42. package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
  43. package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
  44. package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
  45. package/src/canvas/widgets/StickyNote.module.css +5 -0
  46. package/src/canvas/widgets/StickyNote.test.jsx +9 -9
  47. package/src/canvas/widgets/StoryWidget.jsx +277 -0
  48. package/src/canvas/widgets/StoryWidget.module.css +211 -0
  49. package/src/canvas/widgets/WidgetChrome.jsx +76 -20
  50. package/src/canvas/widgets/WidgetChrome.module.css +2 -6
  51. package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
  52. package/src/canvas/widgets/codepenUrl.js +75 -0
  53. package/src/canvas/widgets/codepenUrl.test.js +76 -0
  54. package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
  55. package/src/canvas/widgets/embedOverlay.module.css +35 -0
  56. package/src/canvas/widgets/embedTheme.js +138 -39
  57. package/src/canvas/widgets/githubUrl.js +82 -0
  58. package/src/canvas/widgets/githubUrl.test.js +74 -0
  59. package/src/canvas/widgets/iframeDevLogs.js +49 -0
  60. package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  61. package/src/canvas/widgets/index.js +4 -0
  62. package/src/canvas/widgets/pasteRules.js +295 -0
  63. package/src/canvas/widgets/pasteRules.test.js +474 -0
  64. package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
  65. package/src/canvas/widgets/widgetConfig.js +16 -5
  66. package/src/canvas/widgets/widgetConfig.test.js +34 -12
  67. package/src/context.jsx +145 -16
  68. package/src/hooks/useSceneData.js +4 -2
  69. package/src/hooks/useThemeState.js +61 -0
  70. package/src/hooks/useThemeState.test.js +66 -0
  71. package/src/index.js +10 -0
  72. package/src/story/StoryPage.jsx +117 -0
  73. package/src/story/StoryPage.module.css +18 -0
  74. package/src/vite/data-plugin.js +348 -66
  75. package/src/vite/data-plugin.test.js +405 -5
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.0.0-beta.9",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "4.0.0-beta.9",
7
- "@dfosco/tiny-canvas": "4.0.0-beta.9",
6
+ "@base-ui/react": "^1.4.0",
7
+ "@dfosco/storyboard-core": "4.0.0",
8
+ "@dfosco/tiny-canvas": "4.0.0",
8
9
  "@neodrag/react": "^2.3.1",
9
10
  "glob": "^11.0.0",
10
11
  "jsonc-parser": "^3.3.1",
11
12
  "remark": "^15.0.1",
12
13
  "remark-gfm": "^4.0.1",
14
+ "react-cmdk": "^1.3.9",
13
15
  "remark-html": "^16.0.1"
14
16
  },
15
17
  "license": "MIT",
@@ -22,6 +24,7 @@
22
24
  "src"
23
25
  ],
24
26
  "peerDependencies": {
27
+ "@primer/octicons-react": ">=19",
25
28
  "react": ">=18",
26
29
  "react-router-dom": ">=6",
27
30
  "vite": ">=5"
@@ -0,0 +1,134 @@
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 { Dialog } from '@base-ui/react/dialog'
9
+ import { Button } from '@base-ui/react/button'
10
+ import css from './AuthModal.module.css'
11
+
12
+ const COMMENTS_TOKEN_KEY = 'sb-comments-token'
13
+
14
+ function getRepoInfo() {
15
+ try {
16
+ // eslint-disable-next-line no-undef
17
+ const cfg = typeof __STORYBOARD_CONFIG__ !== 'undefined' ? __STORYBOARD_CONFIG__ : null
18
+ const repo = cfg?.repository
19
+ if (repo?.owner && repo?.name) return repo
20
+ } catch { /* ignore */ }
21
+ return { owner: 'github', name: 'storyboard' }
22
+ }
23
+
24
+ export default function AuthModal() {
25
+ const [open, setOpen] = useState(false)
26
+ const [tokenValue, setTokenValue] = useState('')
27
+
28
+ useEffect(() => {
29
+ function handleOpen() { setOpen(true) }
30
+ document.addEventListener('storyboard:open-auth-modal', handleOpen)
31
+ return () => document.removeEventListener('storyboard:open-auth-modal', handleOpen)
32
+ }, [])
33
+
34
+ const handleClose = useCallback(() => {
35
+ setOpen(false)
36
+ setTokenValue('')
37
+ }, [])
38
+
39
+ const handleSignIn = useCallback(() => {
40
+ const trimmed = tokenValue.trim()
41
+ if (!trimmed) return
42
+
43
+ try { localStorage.setItem(COMMENTS_TOKEN_KEY, trimmed) } catch { /* ignore */ }
44
+
45
+ try {
46
+ import('@dfosco/storyboard-core/comments').then(({ setToken }) => {
47
+ setToken(trimmed)
48
+ }).catch(() => {})
49
+ } catch { /* comments module may not be initialized */ }
50
+
51
+ setTokenValue('')
52
+ setOpen(false)
53
+ }, [tokenValue])
54
+
55
+ const handleKeyDown = useCallback((e) => {
56
+ if (e.key === 'Enter') handleSignIn()
57
+ }, [handleSignIn])
58
+
59
+ const repo = getRepoInfo()
60
+
61
+ return (
62
+ <Dialog.Root open={open} onOpenChange={setOpen}>
63
+ <Dialog.Portal>
64
+ <Dialog.Backdrop className={css.backdrop} />
65
+ <div className={css.popupWrap}>
66
+ <Dialog.Popup className={css.popup}>
67
+ <Dialog.Title className={css.title}>Sign in for comments</Dialog.Title>
68
+ <Dialog.Description className={css.desc}>
69
+ Leave comments for other users to see and respond, and react to! Storyboard
70
+ comments use Discussions as a back-end and require a GitHub PAT to be enabled.
71
+ </Dialog.Description>
72
+ <Dialog.Close className={css.closeBtn} aria-label="Close">×</Dialog.Close>
73
+
74
+ <hr className={css.separator} />
75
+
76
+ <div className={css.tokenCard}>
77
+ <p className={css.tokenCardTitle}>Fine-grained Personal Access Token</p>
78
+ <div className={css.tokenCardRow}>
79
+ <span className={css.tokenCardLabel}>Owner:</span>
80
+ <code className={css.tokenCardCode}>{repo.owner}</code>
81
+ </div>
82
+ <div className={css.tokenCardRow}>
83
+ <span className={css.tokenCardLabel}>Expiration:</span>
84
+ <code className={css.tokenCardCode}>366 days</code>
85
+ <span className={css.tokenCardHint}>(recommended)</span>
86
+ </div>
87
+ <div className={css.tokenCardRow}>
88
+ <span className={css.tokenCardLabel}>Repository access:</span>
89
+ <code className={css.tokenCardCode}>Only select repositories &gt; {repo.owner}/{repo.name}</code>
90
+ </div>
91
+ <div className={css.tokenCardRow}>
92
+ <span className={css.tokenCardLabel}>Permissions:</span>
93
+ <code className={css.tokenCardCode}>Repositories &gt; Discussions &gt; Access: Read and Write</code>
94
+ </div>
95
+ </div>
96
+
97
+ <a
98
+ className={css.tokenLink}
99
+ href="https://github.com/settings/personal-access-tokens/new"
100
+ target="_blank"
101
+ rel="noopener noreferrer"
102
+ >
103
+ Create a GitHub Fine-Grained Personal Access Token ↗
104
+ </a>
105
+
106
+ <hr className={css.separator} />
107
+
108
+ <label className={css.label} htmlFor="auth-modal-token">Personal Access Token</label>
109
+ <input
110
+ id="auth-modal-token"
111
+ className={css.input}
112
+ placeholder="github_pat_… or ghp_…"
113
+ type="password"
114
+ autoFocus
115
+ value={tokenValue}
116
+ onChange={e => setTokenValue(e.target.value)}
117
+ onKeyDown={handleKeyDown}
118
+ />
119
+
120
+ <div className={css.warning}>
121
+ <span className={css.warningIcon}>⚠️</span>
122
+ <span>Comments are an experimental feature and may be unstable.</span>
123
+ </div>
124
+
125
+ <div className={css.actions}>
126
+ <Dialog.Close className={css.btnSecondary}>Cancel</Dialog.Close>
127
+ <button className={css.btnPrimary} onClick={handleSignIn}>Sign in</button>
128
+ </div>
129
+ </Dialog.Popup>
130
+ </div>
131
+ </Dialog.Portal>
132
+ </Dialog.Root>
133
+ )
134
+ }
@@ -0,0 +1,221 @@
1
+ /* AuthModal — PAT entry dialog (BaseUI Dialog) */
2
+
3
+ .backdrop {
4
+ position: fixed;
5
+ inset: 0;
6
+ background: var(--overlay-backdrop-bgColor, rgba(0, 0, 0, 0.4));
7
+ z-index: 10000;
8
+ }
9
+
10
+ .popupWrap {
11
+ position: fixed;
12
+ inset: 0;
13
+ z-index: 10001;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ pointer-events: none;
18
+ }
19
+
20
+ .popupWrap > * {
21
+ pointer-events: auto;
22
+ }
23
+
24
+ .popup {
25
+ background: var(--bgColor-default);
26
+ border-radius: 12px;
27
+ box-shadow: var(--shadow-overlay, 0 16px 48px rgba(0, 0, 0, 0.12));
28
+ padding: 28px;
29
+ max-width: 620px;
30
+ max-height: 90vh;
31
+ overflow-y: auto;
32
+ color: var(--fgColor-default);
33
+ position: relative;
34
+ }
35
+
36
+ .closeBtn {
37
+ position: absolute;
38
+ top: 16px;
39
+ right: 16px;
40
+ background: none;
41
+ border: none;
42
+ font-size: 24px;
43
+ line-height: 1;
44
+ color: var(--fgColor-muted);
45
+ cursor: pointer;
46
+ padding: 4px 8px;
47
+ border-radius: 4px;
48
+ }
49
+
50
+ .closeBtn:hover {
51
+ background: var(--bgColor-neutral-muted);
52
+ color: var(--fgColor-default);
53
+ }
54
+
55
+ .title {
56
+ font-size: 18px;
57
+ font-weight: 600;
58
+ margin-bottom: 8px;
59
+ color: var(--fgColor-default);
60
+ padding-right: 32px;
61
+ }
62
+
63
+ .desc {
64
+ font-size: 15px;
65
+ color: var(--fgColor-muted);
66
+ margin-bottom: 16px;
67
+ line-height: 1.5;
68
+ }
69
+
70
+ .separator {
71
+ border: none;
72
+ border-top: 1px solid var(--borderColor-default);
73
+ margin: 16px 0;
74
+ }
75
+
76
+ /* Token config card — matches GitHub PAT settings screenshot */
77
+
78
+ .tokenCard {
79
+ background: var(--bgColor-muted);
80
+ border: 1px solid var(--borderColor-default);
81
+ border-radius: 8px;
82
+ padding: 14px 16px;
83
+ margin-bottom: 12px;
84
+ }
85
+
86
+ .tokenCardTitle {
87
+ font-size: 15px;
88
+ font-weight: 600;
89
+ color: var(--fgColor-default);
90
+ margin-bottom: 8px;
91
+ }
92
+
93
+ .tokenCardRow {
94
+ display: flex;
95
+ align-items: baseline;
96
+ gap: 6px;
97
+ font-size: 14px;
98
+ line-height: 1.8;
99
+ color: var(--fgColor-muted);
100
+ }
101
+
102
+ .tokenCardLabel {
103
+ flex-shrink: 0;
104
+ }
105
+
106
+ .tokenCardCode {
107
+ font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
108
+ font-size: 13px;
109
+ color: var(--fgColor-default);
110
+ background: var(--bgColor-default);
111
+ padding: 1px 6px;
112
+ border-radius: 4px;
113
+ }
114
+
115
+ .tokenCardHint {
116
+ color: var(--fgColor-muted);
117
+ }
118
+
119
+ .tokenLink {
120
+ display: inline-block;
121
+ font-size: 15px;
122
+ color: var(--fgColor-accent);
123
+ text-decoration: none;
124
+ margin-bottom: 4px;
125
+ }
126
+
127
+ .tokenLink:hover {
128
+ text-decoration: underline;
129
+ }
130
+
131
+ /* Input */
132
+
133
+ .label {
134
+ display: block;
135
+ font-size: 15px;
136
+ font-weight: 600;
137
+ color: var(--fgColor-default);
138
+ margin-bottom: 6px;
139
+ }
140
+
141
+ .input {
142
+ width: 100%;
143
+ padding: 10px 12px;
144
+ border: 1px solid var(--borderColor-default);
145
+ border-radius: 6px;
146
+ font-size: 15px;
147
+ font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
148
+ margin-bottom: 16px;
149
+ box-sizing: border-box;
150
+ background: var(--bgColor-default);
151
+ color: var(--fgColor-default);
152
+ }
153
+
154
+ .input::placeholder {
155
+ color: var(--fgColor-muted);
156
+ }
157
+
158
+ .input:focus {
159
+ outline: none;
160
+ border-color: var(--borderColor-accent-emphasis);
161
+ box-shadow: 0 0 0 3px var(--borderColor-accent-muted);
162
+ }
163
+
164
+ /* Warning banner */
165
+
166
+ .warning {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 8px;
170
+ padding: 10px 12px;
171
+ background: var(--bgColor-attention-muted);
172
+ border: 1px solid var(--borderColor-attention-muted);
173
+ border-radius: 6px;
174
+ font-size: 14px;
175
+ color: var(--fgColor-attention);
176
+ margin-bottom: 8px;
177
+ }
178
+
179
+ .warningIcon {
180
+ flex-shrink: 0;
181
+ font-size: 16px;
182
+ }
183
+
184
+ /* Actions */
185
+
186
+ .actions {
187
+ display: flex;
188
+ justify-content: flex-end;
189
+ gap: 8px;
190
+ margin-top: 8px;
191
+ }
192
+
193
+ .btnSecondary {
194
+ padding: 8px 16px;
195
+ background: transparent;
196
+ border: none;
197
+ border-radius: 6px;
198
+ font-size: 15px;
199
+ color: var(--fgColor-muted);
200
+ cursor: pointer;
201
+ }
202
+
203
+ .btnSecondary:hover {
204
+ color: var(--fgColor-default);
205
+ background: var(--bgColor-neutral-muted);
206
+ }
207
+
208
+ .btnPrimary {
209
+ padding: 8px 20px;
210
+ background: var(--bgColor-accent-emphasis);
211
+ border: none;
212
+ border-radius: 6px;
213
+ font-size: 15px;
214
+ font-weight: 600;
215
+ color: var(--fgColor-onEmphasis);
216
+ cursor: pointer;
217
+ }
218
+
219
+ .btnPrimary:hover {
220
+ background: var(--bgColor-accent-emphasis-hover, var(--bgColor-accent-emphasis));
221
+ }
@@ -0,0 +1,56 @@
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 isHiddenByParam = useMemo(() => {
15
+ if (typeof window === 'undefined') return false
16
+ const params = new URLSearchParams(window.location.search)
17
+ return params.has('_sb_hide_branch_bar') || params.has('_sb_embed')
18
+ }, [])
19
+
20
+ const currentBranch = useMemo(() => {
21
+ const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
22
+ return m ? m[1] : 'main'
23
+ }, [basePath])
24
+
25
+ const isOnBranch = currentBranch !== 'main'
26
+
27
+ useEffect(() => {
28
+ const observer = new MutationObserver(() => {
29
+ setHidden(document.documentElement.classList.contains('storyboard-chrome-hidden'))
30
+ })
31
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
32
+ return () => observer.disconnect()
33
+ }, [])
34
+
35
+ if (!isOnBranch || hidden || isHiddenByParam) return null
36
+
37
+ function hideChrome() {
38
+ window.dispatchEvent(new KeyboardEvent('keydown', {
39
+ key: '.', metaKey: true, bubbles: true,
40
+ }))
41
+ }
42
+
43
+ return (
44
+ <div className={css.bar} data-branch-bar>
45
+ <div className={css.barInner}>
46
+ <span className={css.barLabel}>
47
+ <GitBranchIcon size={12} />
48
+ <span className={css.barBranchName}>{currentBranch}</span>
49
+ </span>
50
+ <div className={css.barActions}>
51
+ <button className={css.barAction} onClick={hideChrome}>Hide</button>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ )
56
+ }
@@ -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
+ }