@civilization/dsh-git-workbench 0.0.0-stage → 0.4.1

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.
@@ -0,0 +1,292 @@
1
+ /* Status bar Git branch pill and popover branch switcher (unified DSH design) */
2
+
3
+ .anchor {
4
+ position: relative;
5
+ display: inline-flex;
6
+ min-width: 0;
7
+ font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px);
8
+ line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px));
9
+ }
10
+
11
+ .pill {
12
+ display: inline-flex;
13
+ align-items: center;
14
+ gap: 6px;
15
+ box-sizing: border-box;
16
+ max-width: 100%;
17
+ padding: 1px 8px;
18
+ border: none;
19
+ border-radius: var(--dsw-radius-sm);
20
+ background: transparent;
21
+ color: var(--dsw-alias-label-tertiary);
22
+ font: inherit;
23
+ font-weight: 500;
24
+ font-variant-numeric: tabular-nums;
25
+ line-height: inherit;
26
+ white-space: nowrap;
27
+ cursor: pointer;
28
+ transition: background 0.15s ease, color 0.15s ease;
29
+ }
30
+
31
+ .pill:hover,
32
+ .pill[aria-expanded='true'] {
33
+ background: var(--dsw-alias-interactive-bg-hover);
34
+ color: var(--dsw-alias-label-primary);
35
+ }
36
+
37
+ .icon {
38
+ width: 14px;
39
+ height: 14px;
40
+ flex: none;
41
+ color: var(--dsw-alias-label-tertiary);
42
+ }
43
+
44
+ .pill:hover .icon,
45
+ .pill[aria-expanded='true'] .icon {
46
+ color: var(--dsw-alias-state-business-primary);
47
+ }
48
+
49
+ .branchName {
50
+ max-width: 150px;
51
+ overflow: hidden;
52
+ text-overflow: ellipsis;
53
+ white-space: nowrap;
54
+ }
55
+
56
+ .badge {
57
+ display: inline-flex;
58
+ align-items: center;
59
+ padding: 0 4px;
60
+ border-radius: var(--dsw-radius-xs, 3px);
61
+ font-size: 11px;
62
+ line-height: 14px;
63
+ font-weight: 500;
64
+ background: var(--dsw-alias-interactive-bg-hover);
65
+ color: var(--dsw-alias-label-secondary);
66
+ }
67
+
68
+ .badgeAhead {
69
+ color: var(--dsw-alias-state-business-primary);
70
+ }
71
+
72
+ .badgeBehind {
73
+ color: var(--dsw-alias-state-warn-primary);
74
+ }
75
+
76
+ /* Branch switch popover panel (wrapped in MenuSurface) */
77
+ .popover {
78
+ position: fixed;
79
+ z-index: 1100;
80
+ width: 310px;
81
+ max-height: 440px;
82
+ display: flex;
83
+ flex-direction: column;
84
+ box-sizing: border-box;
85
+ border: 0;
86
+ border-radius: var(--dsw-radius-lg);
87
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
88
+ box-shadow: var(--dsw-elevation-prominent);
89
+ color: var(--dsw-alias-label-primary);
90
+ font-size: 12px;
91
+ line-height: 18px;
92
+ overflow: hidden;
93
+ cursor: default;
94
+ animation: popoverFadeIn 0.15s cubic-bezier(0.16, 1, 0.3, 1);
95
+ }
96
+
97
+ @keyframes popoverFadeIn {
98
+ from {
99
+ opacity: 0;
100
+ transform: translateY(4px) scale(0.98);
101
+ }
102
+ to {
103
+ opacity: 1;
104
+ transform: translateY(0) scale(1);
105
+ }
106
+ }
107
+
108
+ .header {
109
+ padding: 8px 10px 6px;
110
+ border-bottom: 0.5px solid var(--dsw-alias-border-l2);
111
+ display: flex;
112
+ flex-direction: column;
113
+ gap: 6px;
114
+ }
115
+
116
+ .searchRow {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 6px;
120
+ padding: 4px 8px;
121
+ border-radius: var(--dsw-radius-sm);
122
+ background: var(--dsw-alias-interactive-bg-hover);
123
+ border: 0.5px solid var(--dsw-alias-border-l2);
124
+ }
125
+
126
+ .searchIcon {
127
+ width: 13px;
128
+ height: 13px;
129
+ color: var(--dsw-alias-label-tertiary);
130
+ flex: none;
131
+ }
132
+
133
+ .searchInput {
134
+ flex: 1;
135
+ border: none;
136
+ background: transparent;
137
+ color: var(--dsw-alias-label-primary);
138
+ font-size: 12px;
139
+ outline: none;
140
+ }
141
+
142
+ .searchInput::placeholder {
143
+ color: var(--dsw-alias-label-tertiary);
144
+ }
145
+
146
+ .actionBar {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ gap: 4px;
151
+ }
152
+
153
+ .toolBtnGroup {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 4px;
157
+ }
158
+
159
+ .toolBtn {
160
+ display: inline-flex;
161
+ align-items: center;
162
+ gap: 4px;
163
+ padding: 3px 8px;
164
+ border: 0.5px solid var(--dsw-alias-border-l2);
165
+ border-radius: var(--dsw-radius-sm);
166
+ background: var(--dsw-alias-interactive-bg-hover);
167
+ color: var(--dsw-alias-label-secondary);
168
+ font-size: 11px;
169
+ font-family: inherit;
170
+ font-weight: 500;
171
+ cursor: pointer;
172
+ transition: background 0.15s ease, color 0.15s ease;
173
+ }
174
+
175
+ .toolBtn:hover {
176
+ background: var(--dsw-alias-interactive-bg-active);
177
+ color: var(--dsw-alias-label-primary);
178
+ }
179
+
180
+ .btnIcon {
181
+ width: 12px;
182
+ height: 12px;
183
+ flex: none;
184
+ }
185
+
186
+ .newBranchBox {
187
+ display: flex;
188
+ gap: 6px;
189
+ margin-top: 2px;
190
+ }
191
+
192
+ .newBranchInput {
193
+ flex: 1;
194
+ padding: 4px 8px;
195
+ border-radius: var(--dsw-radius-sm);
196
+ border: 0.5px solid var(--dsw-alias-state-business-primary);
197
+ background: var(--dsw-alias-interactive-bg-hover);
198
+ color: var(--dsw-alias-label-primary);
199
+ font-size: 12px;
200
+ outline: none;
201
+ }
202
+
203
+ .newBranchInput::placeholder {
204
+ color: var(--dsw-alias-label-tertiary);
205
+ }
206
+
207
+ .branchList {
208
+ flex: 1;
209
+ overflow-y: auto;
210
+ padding: 4px;
211
+ }
212
+
213
+ .groupLabel {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 6px;
217
+ padding: 6px 8px 3px;
218
+ font-size: 11px;
219
+ font-weight: 500;
220
+ color: var(--dsw-alias-label-tertiary);
221
+ }
222
+
223
+ .branchItem {
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: space-between;
227
+ width: 100%;
228
+ padding: 5px 8px;
229
+ border-radius: var(--dsw-radius-sm);
230
+ border: none;
231
+ background: transparent;
232
+ color: var(--dsw-alias-label-secondary);
233
+ font: inherit;
234
+ font-size: 12px;
235
+ text-align: left;
236
+ cursor: pointer;
237
+ transition: background 0.12s ease, color 0.12s ease;
238
+ }
239
+
240
+ .branchItem:hover {
241
+ background: var(--dsw-alias-interactive-bg-hover);
242
+ color: var(--dsw-alias-label-primary);
243
+ }
244
+
245
+ .currentBranch {
246
+ background: var(--dsw-alias-interactive-bg-active);
247
+ color: var(--dsw-alias-state-business-primary);
248
+ font-weight: 500;
249
+ }
250
+
251
+ .currentBranch:hover {
252
+ background: var(--dsw-alias-interactive-bg-active);
253
+ }
254
+
255
+ .branchItemLeft {
256
+ display: flex;
257
+ align-items: center;
258
+ gap: 6px;
259
+ min-width: 0;
260
+ flex: 1;
261
+ }
262
+
263
+ .itemIcon {
264
+ width: 13px;
265
+ height: 13px;
266
+ flex: none;
267
+ color: var(--dsw-alias-label-tertiary);
268
+ }
269
+
270
+ .currentBranch .itemIcon {
271
+ color: var(--dsw-alias-state-business-primary);
272
+ }
273
+
274
+ .itemText {
275
+ overflow: hidden;
276
+ text-overflow: ellipsis;
277
+ white-space: nowrap;
278
+ }
279
+
280
+ .checkMark {
281
+ width: 14px;
282
+ height: 14px;
283
+ color: var(--dsw-alias-state-business-primary);
284
+ flex: none;
285
+ }
286
+
287
+ .emptyTip {
288
+ padding: 16px 8px;
289
+ text-align: center;
290
+ color: var(--dsw-alias-label-tertiary);
291
+ font-size: 12px;
292
+ }
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Status bar Git branch pill and popover branch switcher (IDEA / VS Code style).
3
+ * Injected into the 'conversation.composer.dock' slot at order: -1.
4
+ */
5
+
6
+ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
7
+ import type { KeyboardEvent, MouseEvent } from 'react'
8
+ import { createPortal } from 'react-dom'
9
+ import type { GitBranchesSummary, GitStatusSummary } from '../types.ts'
10
+ import css from './GitBranchPill.module.css'
11
+
12
+ const MenuSurface = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
13
+ ({ children, className, style, ...rest }, ref) => (
14
+ <div ref={ref} className={className} style={style} role="dialog" {...rest}>
15
+ {children}
16
+ </div>
17
+ ),
18
+ )
19
+ MenuSurface.displayName = 'MenuSurface'
20
+
21
+ function IconBranchOutlineRegular({ className }: { className?: string }): React.ReactElement {
22
+ return (
23
+ <svg className={className} width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
24
+ <path d="M11.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zm-2.25.75a2.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.25zM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zM3.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0z" />
25
+ </svg>
26
+ )
27
+ }
28
+
29
+ function IconCheckOutlineRegular({ className }: { className?: string }): React.ReactElement {
30
+ return (
31
+ <svg className={className} width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
32
+ <polyline points="3 8 7 12 13 4" />
33
+ </svg>
34
+ )
35
+ }
36
+
37
+ function IconPlusOutlineRegular({ className }: { className?: string }): React.ReactElement {
38
+ return (
39
+ <svg className={className} width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
40
+ <line x1="8" y1="3" x2="8" y2="13" />
41
+ <line x1="3" y1="8" x2="13" y2="8" />
42
+ </svg>
43
+ )
44
+ }
45
+
46
+ function IconRefreshOutlineRegular({ className }: { className?: string }): React.ReactElement {
47
+ return (
48
+ <svg className={className} width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
49
+ <path d="M2.5 8a5.5 5.5 0 1 0 1.6-3.9L2 6" />
50
+ <path d="M2 2v4h4" />
51
+ </svg>
52
+ )
53
+ }
54
+
55
+ function IconSearchOutlineRegular({ className }: { className?: string }): React.ReactElement {
56
+ return (
57
+ <svg className={className} width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
58
+ <circle cx="7" cy="7" r="4.5" />
59
+ <line x1="10.5" y1="10.5" x2="14" y2="14" />
60
+ </svg>
61
+ )
62
+ }
63
+
64
+
65
+ export interface GitBranchPillProps {
66
+ getStatus?: ((sessionId?: string) => Promise<GitStatusSummary>) | undefined
67
+ getBranches?: ((sessionId?: string) => Promise<GitBranchesSummary>) | undefined
68
+ checkout?: (
69
+ (sessionId: string | undefined, branch: string, create?: boolean) => Promise<{ success: boolean; message?: string }>
70
+ ) | undefined
71
+ pull?: ((sessionId?: string) => Promise<{ success: boolean; message?: string }>) | undefined
72
+ push?: ((sessionId?: string) => Promise<{ success: boolean; message?: string }>) | undefined
73
+ sessionId?: string | undefined
74
+ }
75
+
76
+ /**
77
+ * Git Branch Pill rendered in the Composer Dock.
78
+ */
79
+ export const GitBranchPill = memo(function GitBranchPill({
80
+ getStatus,
81
+ getBranches,
82
+ checkout,
83
+ pull,
84
+ push,
85
+ sessionId,
86
+ }: GitBranchPillProps) {
87
+ const [status, setStatus] = useState<GitStatusSummary | null>(null)
88
+ const [branches, setBranches] = useState<GitBranchesSummary | null>(null)
89
+ const [open, setOpen] = useState(false)
90
+ const [filter, setFilter] = useState('')
91
+ const [creatingBranch, setCreatingBranch] = useState(false)
92
+ const [newBranchName, setNewBranchName] = useState('')
93
+ const [switching, setSwitching] = useState(false)
94
+
95
+ const anchorRef = useRef<HTMLSpanElement>(null)
96
+ const popoverRef = useRef<HTMLDivElement>(null)
97
+ const [popoverPos, setPopoverPos] = useState<{ bottom: number; left: number } | null>(null)
98
+
99
+ // Fetch status and branches from injected actions
100
+ const refresh = useCallback(async () => {
101
+ if (!getStatus || !getBranches) return
102
+
103
+ try {
104
+ const [statusRes, branchRes] = await Promise.all([
105
+ getStatus(sessionId),
106
+ getBranches(sessionId),
107
+ ])
108
+ setStatus(statusRes)
109
+ setBranches(branchRes)
110
+ } catch {
111
+ // safe fallback on communication errors
112
+ }
113
+ }, [getStatus, getBranches, sessionId])
114
+
115
+ useEffect(() => {
116
+ void refresh()
117
+ const timer = setInterval(() => { void refresh() }, 5000)
118
+ return () => { clearInterval(timer) }
119
+ }, [refresh])
120
+
121
+ // Recalculate popover position relative to anchor button
122
+ const updatePosition = useCallback(() => {
123
+ if (!anchorRef.current) return
124
+ const rect = anchorRef.current.getBoundingClientRect()
125
+ setPopoverPos({
126
+ bottom: window.innerHeight - rect.top + 6,
127
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - 330)),
128
+ })
129
+ }, [])
130
+
131
+ const handleToggle = useCallback((e: MouseEvent) => {
132
+ e.stopPropagation()
133
+ if (!open) {
134
+ updatePosition()
135
+ void refresh()
136
+ }
137
+ setOpen(prev => !prev)
138
+ setCreatingBranch(false)
139
+ setFilter('')
140
+ }, [open, refresh, updatePosition])
141
+
142
+ // Dismiss popover on outside click or escape
143
+ useEffect(() => {
144
+ if (!open) return
145
+ const onPointerDown = (e: PointerEvent) => {
146
+ const target = e.target as Node | null
147
+ if (popoverRef.current?.contains(target) || anchorRef.current?.contains(target)) return
148
+ setOpen(false)
149
+ }
150
+ const onKeyDown = (e: globalThis.KeyboardEvent) => {
151
+ if (e.key === 'Escape') setOpen(false)
152
+ }
153
+
154
+ document.addEventListener('pointerdown', onPointerDown)
155
+ document.addEventListener('keydown', onKeyDown)
156
+ return () => {
157
+ document.removeEventListener('pointerdown', onPointerDown)
158
+ document.removeEventListener('keydown', onKeyDown)
159
+ }
160
+ }, [open])
161
+
162
+ // Checkout branch
163
+ const handleCheckout = useCallback(async (branchName: string, create = false) => {
164
+ if (!checkout || switching) return
165
+
166
+ setSwitching(true)
167
+ try {
168
+ const res = await checkout(sessionId, branchName, create)
169
+ if (res.success) {
170
+ setOpen(false)
171
+ setCreatingBranch(false)
172
+ setNewBranchName('')
173
+ await refresh()
174
+ } else {
175
+ alert(res.message ?? 'Checkout failed')
176
+ }
177
+ } finally {
178
+ setSwitching(false)
179
+ }
180
+ }, [checkout, refresh, sessionId, switching])
181
+
182
+ // Create new branch
183
+ const handleCreateBranchKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
184
+ if (e.key === 'Enter') {
185
+ const trimmed = newBranchName.trim()
186
+ if (trimmed) void handleCheckout(trimmed, true)
187
+ } else if (e.key === 'Escape') {
188
+ setCreatingBranch(false)
189
+ }
190
+ }, [handleCheckout, newBranchName])
191
+
192
+ // Pull
193
+ const handlePull = useCallback(async () => {
194
+ if (!pull) return
195
+ await pull(sessionId)
196
+ await refresh()
197
+ }, [pull, refresh, sessionId])
198
+
199
+ // Push
200
+ const handlePush = useCallback(async () => {
201
+ if (!push) return
202
+ await push(sessionId)
203
+ await refresh()
204
+ }, [push, refresh, sessionId])
205
+
206
+ // Filtered branches
207
+ const filteredLocal = useMemo(() => {
208
+ if (!branches?.local) return []
209
+ const q = filter.trim().toLowerCase()
210
+ return q ? branches.local.filter(b => b.name.toLowerCase().includes(q)) : branches.local
211
+ }, [branches?.local, filter])
212
+
213
+ const filteredRemote = useMemo(() => {
214
+ if (!branches?.remote) return []
215
+ const q = filter.trim().toLowerCase()
216
+ return q ? branches.remote.filter(b => b.name.toLowerCase().includes(q)) : branches.remote
217
+ }, [branches?.remote, filter])
218
+
219
+ if (status && !status.isRepo) {
220
+ return null
221
+ }
222
+
223
+ const currentBranchName = status?.branch || 'master'
224
+
225
+ return (
226
+ <span ref={anchorRef} className={css.anchor} data-composer-stat="git-branch">
227
+ <button
228
+ type="button"
229
+ className={css.pill}
230
+ aria-haspopup="dialog"
231
+ aria-expanded={open}
232
+ title={`Git Branch: ${currentBranchName}`}
233
+ onClick={handleToggle}
234
+ >
235
+ <IconBranchOutlineRegular className={css.icon} />
236
+ <span className={css.branchName}>{currentBranchName}</span>
237
+ {status !== null && status.ahead > 0 && <span className={`${css.badge} ${css.badgeAhead}`}>↑{status.ahead}</span>}
238
+ {status !== null && status.behind > 0 && <span className={`${css.badge} ${css.badgeBehind}`}>↓{status.behind}</span>}
239
+ </button>
240
+
241
+ {open && popoverPos && createPortal(
242
+ <MenuSurface
243
+ ref={popoverRef}
244
+ className={css.popover}
245
+ style={{ bottom: `${popoverPos.bottom}px`, left: `${popoverPos.left}px` }}
246
+ >
247
+ <div className={css.header}>
248
+ <div className={css.searchRow}>
249
+ <IconSearchOutlineRegular className={css.searchIcon} />
250
+ <input
251
+ autoFocus
252
+ className={css.searchInput}
253
+ placeholder="搜索或过滤分支..."
254
+ value={filter}
255
+ onChange={(e) => { setFilter(e.target.value) }}
256
+ />
257
+ </div>
258
+
259
+ <div className={css.actionBar}>
260
+ <button
261
+ type="button"
262
+ className={css.toolBtn}
263
+ title="新建分支"
264
+ onClick={() => { setCreatingBranch(prev => !prev) }}
265
+ >
266
+ <IconPlusOutlineRegular className={css.btnIcon} />
267
+ <span>新建分支</span>
268
+ </button>
269
+ <div className={css.toolBtnGroup}>
270
+ <button
271
+ type="button"
272
+ className={css.toolBtn}
273
+ title="拉取 (Pull)"
274
+ onClick={() => { void handlePull() }}
275
+ >
276
+ Pull
277
+ </button>
278
+ <button
279
+ type="button"
280
+ className={css.toolBtn}
281
+ title="推送 (Push)"
282
+ onClick={() => { void handlePush() }}
283
+ >
284
+ Push
285
+ </button>
286
+ <button
287
+ type="button"
288
+ className={css.toolBtn}
289
+ title="刷新状态"
290
+ onClick={() => { void refresh() }}
291
+ >
292
+ <IconRefreshOutlineRegular className={css.btnIcon} />
293
+ </button>
294
+ </div>
295
+ </div>
296
+
297
+ {creatingBranch && (
298
+ <div className={css.newBranchBox}>
299
+ <input
300
+ autoFocus
301
+ className={css.newBranchInput}
302
+ placeholder="新分支名称 (按 Enter 创建)..."
303
+ value={newBranchName}
304
+ onChange={(e) => { setNewBranchName(e.target.value) }}
305
+ onKeyDown={handleCreateBranchKeyDown}
306
+ />
307
+ </div>
308
+ )}
309
+ </div>
310
+
311
+ <div className={css.branchList}>
312
+ {/* Local Branches */}
313
+ <div className={css.groupLabel}>本地分支 ({filteredLocal.length})</div>
314
+ {filteredLocal.map(b => (
315
+ <button
316
+ key={b.name}
317
+ type="button"
318
+ className={`${css.branchItem} ${b.isCurrent ? css.currentBranch : ''}`}
319
+ disabled={switching}
320
+ onClick={() => {
321
+ if (!b.isCurrent) void handleCheckout(b.name)
322
+ }}
323
+ >
324
+ <div className={css.branchItemLeft}>
325
+ <IconBranchOutlineRegular className={css.itemIcon} />
326
+ <span className={css.itemText}>{b.name}</span>
327
+ </div>
328
+ {b.isCurrent ? (
329
+ <IconCheckOutlineRegular className={css.checkMark} />
330
+ ) : (
331
+ b.ahead !== undefined && b.ahead > 0 && <span className={css.badge}>↑{b.ahead}</span>
332
+ )}
333
+ </button>
334
+ ))}
335
+
336
+ {/* Remote Branches */}
337
+ {filteredRemote.length > 0 && (
338
+ <>
339
+ <div className={css.groupLabel} style={{ marginTop: '6px' }}>
340
+ 远程分支 ({filteredRemote.length})
341
+ </div>
342
+ {filteredRemote.map(b => (
343
+ <button
344
+ key={b.name}
345
+ type="button"
346
+ className={css.branchItem}
347
+ disabled={switching}
348
+ onClick={() => { void handleCheckout(b.name) }}
349
+ >
350
+ <div className={css.branchItemLeft}>
351
+ <IconBranchOutlineRegular className={css.itemIcon} />
352
+ <span className={css.itemText}>{b.name}</span>
353
+ </div>
354
+ </button>
355
+ ))}
356
+ </>
357
+ )}
358
+
359
+ {filteredLocal.length === 0 && filteredRemote.length === 0 && (
360
+ <div className={css.emptyTip}>未找到匹配的分支</div>
361
+ )}
362
+ </div>
363
+ </MenuSurface>,
364
+ document.body,
365
+ )}
366
+ </span>
367
+ )
368
+ })
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The Git tab's chip title in the right-sidebar header.
3
+ */
4
+
5
+ import type { ReactNode } from 'react'
6
+ import css from './GitBody.module.css'
7
+
8
+ export interface GitTitleProps {
9
+ useTabInfo: () => { tab: { title: string } }
10
+ }
11
+
12
+ /**
13
+ * Git chip title component with branch glyph and title text.
14
+ * @param props - Slot props including tab info hook.
15
+ * @returns React node for tab chip.
16
+ */
17
+ export function GitTitle({ useTabInfo }: GitTitleProps): ReactNode {
18
+ const { tab } = useTabInfo()
19
+ return (
20
+ <span className={css.titleContainer}>
21
+ <svg className={css.titleIcon} width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
22
+ <path d="M11.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zm-2.25.75a2.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.25zM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5zM3.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0z" />
23
+ </svg>
24
+ <span className={css.titleText}>{tab.title}</span>
25
+ </span>
26
+ )
27
+ }