@kubohiroya/turbowarp-plus-editor-git 0.17.0 → 0.18.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.
@@ -3,7 +3,6 @@ export declare const messages: {
3
3
  pullRequests: string;
4
4
  localChanges: string;
5
5
  title: string;
6
- menu: string;
7
6
  close: string;
8
7
  choose: string;
9
8
  refresh: string;
@@ -110,12 +109,15 @@ export declare const messages: {
110
109
  historyCopyFailed: string;
111
110
  historyRefs: string;
112
111
  cancel: string;
112
+ branchMenu: string;
113
+ openPanel: string;
114
+ newBranchItem: string;
115
+ newBranchTitle: string;
113
116
  };
114
117
  ja: {
115
118
  pullRequests: string;
116
119
  localChanges: string;
117
120
  title: string;
118
- menu: string;
119
121
  close: string;
120
122
  choose: string;
121
123
  refresh: string;
@@ -222,6 +224,10 @@ export declare const messages: {
222
224
  historyCopyFailed: string;
223
225
  historyRefs: string;
224
226
  cancel: string;
227
+ branchMenu: string;
228
+ openPanel: string;
229
+ newBranchItem: string;
230
+ newBranchTitle: string;
225
231
  };
226
232
  };
227
233
  export type Labels = typeof messages.en;
@@ -1410,4 +1410,4 @@ function assetsDirectoryParts(settings) {
1410
1410
  const parts = value.split("/");
1411
1411
  return parts.every((part) => part && part !== "." && part !== ".." && part.toLowerCase() !== ".git" && !/[\\\0-\x1f\x7f]/.test(part)) ? parts : null;
1412
1412
  }
1413
- export { compareProjectBlocks as a, isRecord as c, targetKey as d, createTwoFilesPatch as f, isAssetName as i, parseProjectSource as l, assetsDirectoryParts as n, createProjectSourceDiff as o, folderSettingsLimit as r, equal as s, assetLimit as t, readProjectSourceDiff as u };
1413
+ export { compareProjectBlocks as a, isRecord as c, targetKey as d, _objectWithoutProperties as f, isAssetName as i, parseProjectSource as l, assetsDirectoryParts as n, createProjectSourceDiff as o, createTwoFilesPatch as p, folderSettingsLimit as r, equal as s, assetLimit as t, readProjectSourceDiff as u };
@@ -1,5 +1,5 @@
1
1
  import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
2
- import { a as compareProjectBlocks, c as isRecord, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-nL5sV35A.js";
2
+ import { a as compareProjectBlocks, c as isRecord, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { render, unmountComponentAtNode } from "react-dom";
5
5
  // src/close-button.tsx
@@ -1,5 +1,5 @@
1
- import { a as compareProjectBlocks, o as createProjectSourceDiff, u as readProjectSourceDiff } from "./project-asset-paths-nL5sV35A.js";
2
- import { _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume } from "./project-comparison-DjMlwyp8.js";
1
+ import { a as compareProjectBlocks, o as createProjectSourceDiff, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
2
+ import { _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume } from "./project-comparison-BXOQcZ73.js";
3
3
  import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
4
4
  import React, { useEffect, useMemo, useRef, useState } from "react";
5
5
  // src/project-source-diff.tsx
@@ -1,5 +1,5 @@
1
1
  import { i as __toESM, n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
2
- import { f as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, t as assetLimit } from "./project-asset-paths-nL5sV35A.js";
2
+ import { i as isAssetName, n as assetsDirectoryParts, o as createProjectSourceDiff, p as createTwoFilesPatch, r as folderSettingsLimit, t as assetLimit } from "./project-asset-paths-BzwVf9HE.js";
3
3
  import { a as _OverloadYield, i as _wrapAsyncGenerator, o as _asyncIterator, r as createGitSmartHttp, t as GitSmartHttpError } from "./smart-http-BmHEkE7K.js";
4
4
  // node_modules/inherits/inherits_browser.js
5
5
  var require_inherits_browser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -18014,12 +18014,7 @@ async function openRepository(root) {
18014
18014
  const assertSnapshot = async (snapshot) => {
18015
18015
  if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError("changed");
18016
18016
  };
18017
- const read = async (scope = "") => {
18018
- scope = pathParts(scope).join("/");
18019
- fs.clearReads();
18020
- cache = {};
18021
- const fingerprint = await probe(scope);
18022
- const before = await metadata();
18017
+ const readRefs = async () => {
18023
18018
  let head;
18024
18019
  try {
18025
18020
  head = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: "HEAD" }));
@@ -18030,6 +18025,20 @@ async function openRepository(root) {
18030
18025
  const branch = await currentBranch(options);
18031
18026
  const branches = await listBranches(options);
18032
18027
  const remoteBranches = (await listBranches(_objectSpread2(_objectSpread2({}, options), {}, { remote: "origin" }))).filter((name) => name !== "HEAD" && !branches.includes(name)).sort();
18028
+ return {
18029
+ branch: branch || void 0,
18030
+ head,
18031
+ branches,
18032
+ remoteBranches
18033
+ };
18034
+ };
18035
+ const read = async (scope = "") => {
18036
+ scope = pathParts(scope).join("/");
18037
+ fs.clearReads();
18038
+ cache = {};
18039
+ const fingerprint = await probe(scope);
18040
+ const before = await metadata();
18041
+ const { branch, head, branches, remoteBranches } = await readRefs();
18033
18042
  const commits = head ? (await log(_objectSpread2(_objectSpread2({}, options), {}, {
18034
18043
  cache,
18035
18044
  ref: head,
@@ -18082,7 +18091,7 @@ async function openRepository(root) {
18082
18091
  if (!sameMetadata(before, await metadata())) throw new GitPanelError("changed");
18083
18092
  await fs.checkReads();
18084
18093
  return {
18085
- branch: branch || void 0,
18094
+ branch,
18086
18095
  head,
18087
18096
  branches,
18088
18097
  remoteBranches,
@@ -18694,6 +18703,14 @@ async function openRepository(root) {
18694
18703
  }
18695
18704
  }),
18696
18705
  read: (scope) => serial(() => read(scope)),
18706
+ /**
18707
+ * The current branch, HEAD and the branch names, read from refs only. Cheap enough to follow branch changes made
18708
+ * by other tools while nothing else is read; `read` returns the same fields.
18709
+ */
18710
+ refs: () => serial(() => {
18711
+ fs.clearReads();
18712
+ return readRefs();
18713
+ }),
18697
18714
  readDiff: (snapshot, change) => serial(() => readDiff(snapshot, change)),
18698
18715
  historyChanges: (snapshot, oid) => serial(() => historyChanges(snapshot, oid)),
18699
18716
  /**
@@ -82,6 +82,8 @@ export interface HistoryPage {
82
82
  /** Commits read per history page unless asked otherwise. */
83
83
  export declare const historyPageSize = 50;
84
84
  type Metadata = Map<string, Uint8Array | null>;
85
+ /** The branch fields of a snapshot. */
86
+ export type BranchRefs = Pick<Snapshot, 'branch' | 'head' | 'branches' | 'remoteBranches'>;
85
87
  export interface Snapshot {
86
88
  branch: string | undefined;
87
89
  head: string | null;
@@ -156,6 +158,11 @@ export declare function openRepository(root: FileSystemDirectoryHandle): Promise
156
158
  */
157
159
  hasFolder: (name: string, path: string, remote?: boolean | "commit") => Promise<boolean>;
158
160
  read: (scope?: string) => Promise<Snapshot>;
161
+ /**
162
+ * The current branch, HEAD and the branch names, read from refs only. Cheap enough to follow branch changes made
163
+ * by other tools while nothing else is read; `read` returns the same fields.
164
+ */
165
+ refs: () => Promise<BranchRefs>;
159
166
  readDiff: (snapshot: Snapshot, change: FileChange) => Promise<FileDiff>;
160
167
  historyChanges: (snapshot: Snapshot, oid: string) => Promise<FileChange[]>;
161
168
  /**
@@ -1 +1 @@
1
- export { o as GitPanelError, s as createReadOnlyFs, t as historyPageSize, n as openRepository, a as pullRequestRef, r as upstreamWalkLimit } from "./repository-xIv0iZkj.js";
1
+ export { o as GitPanelError, s as createReadOnlyFs, t as historyPageSize, n as openRepository, a as pullRequestRef, r as upstreamWalkLimit } from "./repository-ByKRBftJ.js";
package/dist/style.css CHANGED
@@ -214,4 +214,32 @@
214
214
  .twgit-graph-status { margin: 4px 0; font-size: 11px; min-height: 1em; }
215
215
  .twgit-panel .twgit-graph-popup [role="menuitem"]:focus-visible, .twgit-graph-popup [role="menuitem"]:focus-visible { outline-offset: -2px; }
216
216
  .twgit-graph-form-error { margin: 0; color: #a11919; overflow-wrap: anywhere; }
217
+ /* The branch button sits in the host's menu bar and takes its text color; its menu uses the host's menu colors. */
218
+ .twgit-branch-entry { position: relative; flex: 0 1 auto; align-self: center; display: flex; min-width: 0; font: inherit; }
219
+ .twgit-branch-button { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 16rem; min-height: 2rem; padding: 0 8px;
220
+ color: inherit; background: transparent; border: 0; border-radius: 4px; font: inherit; white-space: nowrap; cursor: pointer; }
221
+ .twgit-branch-button:hover, .twgit-branch-button[aria-expanded="true"] { background: color-mix(in srgb, currentColor 15%, transparent); }
222
+ .twgit-branch-button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
223
+ .twgit-branch-icon { flex: none; }
224
+ .twgit-branch-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
225
+ .twgit-branch-unborn { flex: none; font-size: 11px; opacity: .85; }
226
+ .twgit-branch-caret { flex: none; font-size: 10px; }
227
+ .twgit-branch-popup { position: absolute; z-index: 510; top: calc(100% + 4px); right: 0; display: grid; gap: 2px; min-width: 220px;
228
+ max-width: min(360px, 90vw); max-height: 70vh; overflow: auto; box-sizing: border-box; padding: 4px;
229
+ color: var(--text-primary, CanvasText); background: var(--ui-white, Canvas); border: 1px solid var(--ui-black-transparent, GrayText);
230
+ border-radius: 6px; box-shadow: 0 8px 24px #0004; font: 14px/1.5 system-ui, sans-serif; white-space: normal; }
231
+ .twgit-branch-popup [role^="menuitem"] { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; color: inherit;
232
+ background: transparent; border: 0; border-radius: 4px; font: inherit; text-align: start; overflow-wrap: anywhere; cursor: pointer; }
233
+ .twgit-branch-popup [role^="menuitem"]:hover, .twgit-branch-popup [role^="menuitem"]:focus-visible {
234
+ background: color-mix(in srgb, currentColor 12%, transparent); }
235
+ .twgit-branch-popup [role^="menuitem"]:focus-visible { outline: 2px solid var(--looks-secondary, Highlight); outline-offset: -2px; }
236
+ .twgit-branch-popup [aria-disabled="true"]:not([aria-checked="true"]) { opacity: .55; cursor: default; }
237
+ .twgit-branch-popup [aria-checked="true"] { font-weight: 600; }
238
+ .twgit-branch-popup [data-separator-before="true"] { margin-top: 3px; box-shadow: 0 -3px 0 -2px color-mix(in srgb, currentColor 25%, transparent); }
239
+ .twgit-branch-check { flex: none; width: 1em; text-align: center; }
240
+ .twgit-branch-popup form { display: grid; gap: 6px; padding: 6px; font-size: 12px; }
241
+ .twgit-branch-popup form label { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
242
+ .twgit-branch-popup form input:not([type]) { flex: 1; min-width: 10em; font: inherit; }
243
+ .twgit-branch-popup form button { color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 4px; padding: 3px 8px; font: inherit; cursor: pointer; }
244
+ .twgit-branch-popup form button:disabled { opacity: .5; cursor: default; }
217
245
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-git",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Read-only local Git panel for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "MPL-2.0",
@@ -0,0 +1,216 @@
1
+ import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
2
+ import type {HistoryActionResult} from './history-graph-view.js';
3
+ import {labelsFor} from './messages.js';
4
+ import type {BranchRefs} from './repository.js';
5
+
6
+ /** What the branch button shows: the branches of the chosen repository as the Git panel last read them. */
7
+ export interface BranchButtonState extends BranchRefs {
8
+ /** Whether a repository has been chosen. */
9
+ repository: boolean;
10
+ /** Whether the host lets the panel switch and create branches. */
11
+ writable: boolean;
12
+ /** Whether the panel is running an operation. */
13
+ busy: boolean;
14
+ }
15
+ /** What the branch button asks the Git panel to do. Branch operations resolve as the panel's own buttons do. */
16
+ export interface BranchButtonActions {
17
+ open(): void;
18
+ choose(): void;
19
+ switchBranch(name: string): Promise<HistoryActionResult>;
20
+ /** Creates a branch at HEAD. */
21
+ createBranch(name: string, checkout: boolean): Promise<HistoryActionResult>;
22
+ /** Creates a local branch from `origin/<name>` and switches to it. */
23
+ createFromRemote(name: string): Promise<HistoryActionResult>;
24
+ }
25
+ export interface BranchButtonProps extends BranchButtonActions {
26
+ state: BranchButtonState;
27
+ locale?: string;
28
+ }
29
+ interface Item {id: string; label: string; checked?: boolean; disabled?: boolean; separatorBefore?: boolean; run(): void}
30
+
31
+ const menuItems = '[role="menuitem"], [role="menuitemradio"]';
32
+
33
+ /** Git's branch glyph, drawn in the text color. */
34
+ function BranchIcon() {
35
+ return <svg className="twgit-branch-icon" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
36
+ <g fill="none" stroke="currentColor" strokeWidth="1.5">
37
+ <circle cx="4.5" cy="3.5" r="1.75"/><circle cx="4.5" cy="12.5" r="1.75"/><circle cx="11.5" cy="5.5" r="1.75"/>
38
+ <path d="M4.5 5.25v5.5M11.5 7.25c0 2.5-2.5 2.75-6 3.75"/>
39
+ </g>
40
+ </svg>;
41
+ }
42
+
43
+ /**
44
+ * The current branch as a menu button for the host's menu bar, with the menu of branches. Before a repository is
45
+ * chosen it reads "Git" and offers to choose one. A detached HEAD shows its short commit ID. The menu moves with the
46
+ * arrow keys, Home and End; Escape closes it and returns to the button. Without `writable` it lists the branches only.
47
+ */
48
+ export function BranchButton({state, locale = 'en', ...actions}: BranchButtonProps) {
49
+ const labels = labelsFor(locale);
50
+ const [mode, setMode] = useState<'closed' | 'menu' | 'create'>('closed');
51
+ const [initial, setInitial] = useState<'checked' | 'first' | 'last'>('checked');
52
+ const [name, setName] = useState('');
53
+ const [checkout, setCheckout] = useState(true);
54
+ const [creating, setCreating] = useState(false);
55
+ const [createError, setCreateError] = useState('');
56
+ const root = useRef<HTMLDivElement>(null);
57
+ const trigger = useRef<HTMLButtonElement>(null);
58
+ const popup = useRef<HTMLDivElement>(null);
59
+ const mounted = useRef(true);
60
+ useEffect(() => () => {mounted.current = false;}, []);
61
+ useLayoutEffect(() => {
62
+ const element = popup.current;
63
+ if (!element) return;
64
+ if (mode === 'create') {
65
+ element.querySelector('input')?.focus();
66
+ return;
67
+ }
68
+ const items = [...element.querySelectorAll<HTMLElement>(menuItems)];
69
+ const checked = items.find(item => item.getAttribute('aria-checked') === 'true');
70
+ (initial === 'last' ? items[items.length - 1] : initial === 'checked' && checked || items[0])?.focus();
71
+ }, [mode]);
72
+ // A click elsewhere closes the menu, also where nothing takes the focus.
73
+ useEffect(() => {
74
+ if (mode === 'closed') return;
75
+ const node = root.current!;
76
+ const dismiss = (event: Event) => {
77
+ if (!node.contains(event.target as Node) && !creating) setMode('closed');
78
+ };
79
+ node.ownerDocument.addEventListener('pointerdown', dismiss, true);
80
+ return () => node.ownerDocument.removeEventListener('pointerdown', dismiss, true);
81
+ }, [mode, creating]);
82
+ // After a refused create, the name is selected for correcting.
83
+ useEffect(() => {
84
+ if (!createError || creating) return;
85
+ const input = popup.current?.querySelector('input:not([type])') as HTMLInputElement | null;
86
+ input?.focus();
87
+ input?.select();
88
+ }, [createError, creating]);
89
+
90
+ const {branch, head} = state;
91
+ const unborn = state.repository && !head;
92
+ const text = !state.repository ? 'Git' : branch ?? (head ? head.slice(0, 7) : labels.unborn);
93
+ const accessibleName = !state.repository ? labels.title :
94
+ `${branch ? labels.branch : labels.detached}: ${text}${unborn && branch ? ` (${labels.unborn})` : ''}`;
95
+ const close = (focus = true) => {
96
+ setMode('closed');
97
+ if (focus) trigger.current?.focus({preventScroll: true});
98
+ };
99
+ const openMenu = (from: 'checked' | 'first' | 'last') => {
100
+ setInitial(from);
101
+ setMode('menu');
102
+ };
103
+ /** Runs a branch operation and shows its result, or why it did not run, in the panel. */
104
+ const change = async (operation: () => Promise<HistoryActionResult>) => {
105
+ const result = await operation();
106
+ if (result.done || result.error) actions.open();
107
+ };
108
+ const items: Item[] = [];
109
+ if (!state.repository) {
110
+ items.push({id: 'choose', label: labels.choose, run: () => {close(); actions.choose();}});
111
+ } else {
112
+ const switching = state.writable && !state.busy;
113
+ for (const local of state.branches) {
114
+ items.push({id: 'branch:' + local, label: local, checked: local === branch, disabled: !state.writable || state.busy && local !== branch,
115
+ run: () => {
116
+ close();
117
+ if (local !== branch && switching) void change(() => actions.switchBranch(local));
118
+ }});
119
+ }
120
+ if (state.writable) {
121
+ state.remoteBranches.forEach((remote, index) => items.push({id: 'remote:' + remote,
122
+ label: labels.historyCreateFromRemote.replace('{name}', remote), disabled: state.busy, separatorBefore: index === 0,
123
+ run: () => {
124
+ close();
125
+ if (!state.busy) void change(() => actions.createFromRemote(remote));
126
+ }}));
127
+ if (head) items.push({id: 'create', label: labels.newBranchItem, disabled: state.busy, separatorBefore: true, run: () => {
128
+ if (state.busy) return;
129
+ setName('');
130
+ setCheckout(true);
131
+ setCreateError('');
132
+ setMode('create');
133
+ }});
134
+ }
135
+ }
136
+ // "Open Local Git" goes with "New Branch…" and stands apart from the branches.
137
+ items.push({id: 'open', label: labels.openPanel, separatorBefore: items.length > 0 && items[items.length - 1].id !== 'create',
138
+ run: () => {close(); actions.open();}});
139
+
140
+ const onMenuKey = (event: React.KeyboardEvent<HTMLElement>) => {
141
+ if (event.key === 'Escape') {
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ if (!creating) close();
145
+ return;
146
+ }
147
+ if (mode !== 'menu') return;
148
+ if (event.key === 'Tab') {close(false); return;}
149
+ const elements = [...event.currentTarget.querySelectorAll<HTMLElement>(menuItems)];
150
+ const index = elements.indexOf(event.target as HTMLElement);
151
+ const target = {ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: elements.length - 1}[event.key];
152
+ if (target === undefined || !elements.length) return;
153
+ event.preventDefault();
154
+ elements[(target + elements.length) % elements.length].focus();
155
+ };
156
+ const submit = async (event: React.FormEvent) => {
157
+ event.preventDefault();
158
+ const branchName = name.trim();
159
+ if (!branchName || creating) return;
160
+ setCreating(true);
161
+ setCreateError('');
162
+ let result: HistoryActionResult;
163
+ try {
164
+ result = await actions.createBranch(branchName, checkout);
165
+ } catch (error) {
166
+ result = {done: false, error: error instanceof Error && error.message ? error.message : labels.failed};
167
+ }
168
+ if (!mounted.current) return;
169
+ setCreating(false);
170
+ if (result.done) {
171
+ close();
172
+ actions.open();
173
+ } else setCreateError(result.error ?? '');
174
+ };
175
+ return <div className="twgit-branch-entry" ref={root} onBlur={event => {
176
+ const next = event.relatedTarget as Node | null;
177
+ if (next && !event.currentTarget.contains(next) && !creating) setMode('closed');
178
+ }}>
179
+ <button type="button" className="twgit-branch-button" ref={trigger} aria-label={accessibleName} title={accessibleName}
180
+ aria-haspopup="menu" aria-expanded={mode === 'menu'} data-git-branch={state.repository ? branch ?? head ?? '' : undefined}
181
+ onClick={() => mode === 'closed' ? openMenu('checked') : close(false)}
182
+ onKeyDown={event => {
183
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
184
+ event.preventDefault();
185
+ openMenu(event.key === 'ArrowUp' ? 'last' : 'checked');
186
+ }}>
187
+ <BranchIcon/>
188
+ <span className="twgit-branch-name">{text}</span>
189
+ {unborn && branch && <small className="twgit-branch-unborn">{labels.unborn}</small>}
190
+ <span className="twgit-branch-caret" aria-hidden="true">▾</span>
191
+ </button>
192
+ {mode === 'menu' && <div ref={popup} className="twgit-branch-popup" role="menu" aria-label={labels.branchMenu} onKeyDown={onMenuKey}>
193
+ {items.map(item => <button key={item.id} type="button" tabIndex={-1} data-action={item.id}
194
+ role={item.checked === undefined ? 'menuitem' : 'menuitemradio'} aria-checked={item.checked}
195
+ aria-disabled={item.disabled || undefined} data-separator-before={item.separatorBefore || undefined}
196
+ onClick={() => {if (!item.disabled || item.checked) item.run();}}>
197
+ {item.checked !== undefined && <span className="twgit-branch-check" aria-hidden="true">{item.checked ? '✓' : ''}</span>}
198
+ {item.label}
199
+ </button>)}
200
+ </div>}
201
+ {mode === 'create' && <div ref={popup} className="twgit-branch-popup" role="dialog" aria-label={labels.newBranchTitle}
202
+ onKeyDown={onMenuKey}>
203
+ <form onSubmit={event => void submit(event)}>
204
+ <label>{labels.newBranch} <input value={name} required disabled={creating} aria-invalid={createError ? true : undefined}
205
+ onChange={event => setName(event.currentTarget.value)}/></label>
206
+ <label><input type="checkbox" checked={checkout} disabled={creating}
207
+ onChange={event => setCheckout(event.currentTarget.checked)}/> {labels.switchAfterCreate}</label>
208
+ {createError && <p className="twgit-graph-form-error" role="alert">{createError}</p>}
209
+ <span className="twgit-graph-form-actions">
210
+ <button type="submit" disabled={creating || state.busy || !name.trim()}>{labels.createBranch}</button>
211
+ <button type="button" disabled={creating} onClick={() => close()}>{labels.cancel}</button>
212
+ </span>
213
+ </form>
214
+ </div>}
215
+ </div>;
216
+ }