@kubohiroya/turbowarp-plus-editor-git 0.15.0 → 0.16.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.
Files changed (38) hide show
  1. package/README.ja.md +5 -3
  2. package/README.md +5 -3
  3. package/dist/close-button.d.ts +6 -0
  4. package/dist/file-diff-view.d.ts +3 -1
  5. package/dist/filesystem.d.ts +2 -2
  6. package/dist/history-graph-view.d.ts +40 -0
  7. package/dist/history-graph.d.ts +44 -0
  8. package/dist/index.d.ts +18 -1
  9. package/dist/index.js +697 -31
  10. package/dist/local-operations.d.ts +14 -0
  11. package/dist/messages.d.ts +52 -0
  12. package/dist/{project-comparison-LAyP_beL.js → project-comparison-1rggIJse.js} +240 -20
  13. package/dist/project-comparison.d.ts +1 -0
  14. package/dist/project-source-diff.d.ts +2 -0
  15. package/dist/project-source-diff.js +2 -2
  16. package/dist/project-source-review.d.ts +43 -0
  17. package/dist/project-source-tabs.d.ts +35 -0
  18. package/dist/pull-request-panel.d.ts +6 -1
  19. package/dist/{repository-Cx-mxT9l.js → repository-BYno-5f8.js} +491 -4
  20. package/dist/repository.d.ts +44 -0
  21. package/dist/repository.js +1 -1
  22. package/dist/style.css +31 -4
  23. package/package.json +1 -1
  24. package/src/close-button.tsx +10 -0
  25. package/src/file-diff-view.tsx +7 -3
  26. package/src/filesystem.ts +2 -1
  27. package/src/history-graph-view.tsx +233 -0
  28. package/src/history-graph.ts +90 -0
  29. package/src/index.tsx +137 -17
  30. package/src/local-operations.ts +59 -4
  31. package/src/messages.ts +26 -2
  32. package/src/project-comparison.tsx +4 -2
  33. package/src/project-source-diff.tsx +2 -0
  34. package/src/project-source-review.tsx +127 -15
  35. package/src/project-source-tabs.tsx +118 -0
  36. package/src/pull-request-panel.tsx +26 -5
  37. package/src/repository.ts +155 -1
  38. package/src/style.css +31 -4
@@ -13,6 +13,20 @@ export type LocalOperation = {
13
13
  kind: 'commit';
14
14
  message: string;
15
15
  author?: GitAuthor;
16
+ }
17
+ /**
18
+ * Creates `name` at HEAD, at the commit `at`, or at `origin/<name>` with `fromRemote`, and switches to it with
19
+ * `checkout`.
20
+ */
21
+ | {
22
+ kind: 'createBranch';
23
+ name: string;
24
+ fromRemote?: boolean;
25
+ at?: string;
26
+ checkout: boolean;
27
+ } | {
28
+ kind: 'switchBranch';
29
+ name: string;
16
30
  };
17
31
  /**
18
32
  * Stages, unstages or commits in the granted repository. Like pull, Git's writes are collected first and applied
@@ -42,6 +42,10 @@ export declare const messages: {
42
42
  outdated: string;
43
43
  unsupported: string;
44
44
  commitMessage: string;
45
+ switchUnsafe: string;
46
+ branchExists: string;
47
+ locked: string;
48
+ branchName: string;
45
49
  pullUnsafe: string;
46
50
  diverged: string;
47
51
  session: string;
@@ -60,6 +64,16 @@ export declare const messages: {
60
64
  saveFirst: string;
61
65
  author: string;
62
66
  nothingStaged: string;
67
+ switchTo: string;
68
+ currentBranch: string;
69
+ createBranch: string;
70
+ newBranch: string;
71
+ switchAfterCreate: string;
72
+ createFromRemote: string;
73
+ remoteBranches: string;
74
+ switched: string;
75
+ branchCreated: string;
76
+ projectMissing: string;
63
77
  pulledProject: string;
64
78
  confirmPush: string;
65
79
  details: string;
@@ -80,6 +94,18 @@ export declare const messages: {
80
94
  records: string;
81
95
  checkedAt: string;
82
96
  failed: string;
97
+ historyMore: string;
98
+ historyLoading: string;
99
+ historyFailed: string;
100
+ historyActions: string;
101
+ historyCreateHere: string;
102
+ historySwitch: string;
103
+ historyCreateFromRemote: string;
104
+ historyCopy: string;
105
+ historyCopied: string;
106
+ historyCopyFailed: string;
107
+ historyRefs: string;
108
+ cancel: string;
83
109
  };
84
110
  ja: {
85
111
  pullRequests: string;
@@ -124,6 +150,10 @@ export declare const messages: {
124
150
  outdated: string;
125
151
  unsupported: string;
126
152
  commitMessage: string;
153
+ switchUnsafe: string;
154
+ branchExists: string;
155
+ locked: string;
156
+ branchName: string;
127
157
  pullUnsafe: string;
128
158
  diverged: string;
129
159
  session: string;
@@ -142,6 +172,16 @@ export declare const messages: {
142
172
  saveFirst: string;
143
173
  author: string;
144
174
  nothingStaged: string;
175
+ switchTo: string;
176
+ currentBranch: string;
177
+ createBranch: string;
178
+ newBranch: string;
179
+ switchAfterCreate: string;
180
+ createFromRemote: string;
181
+ remoteBranches: string;
182
+ switched: string;
183
+ branchCreated: string;
184
+ projectMissing: string;
145
185
  pulledProject: string;
146
186
  confirmPush: string;
147
187
  details: string;
@@ -162,6 +202,18 @@ export declare const messages: {
162
202
  records: string;
163
203
  checkedAt: string;
164
204
  failed: string;
205
+ historyMore: string;
206
+ historyLoading: string;
207
+ historyFailed: string;
208
+ historyActions: string;
209
+ historyCreateHere: string;
210
+ historySwitch: string;
211
+ historyCreateFromRemote: string;
212
+ historyCopy: string;
213
+ historyCopied: string;
214
+ historyCopyFailed: string;
215
+ historyRefs: string;
216
+ cancel: string;
165
217
  };
166
218
  };
167
219
  export type Labels = typeof messages.en;
@@ -2,6 +2,29 @@ import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dk
2
2
  import { a as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as hasOnlyUnixLineEndings, r as folderSettingsLimit, s as hasOnlyWinLineEndings, t as assetLimit } from "./project-asset-paths-KV715791.js";
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { render, unmountComponentAtNode } from "react-dom";
5
+ // src/close-button.tsx
6
+ /** A button drawn as an X, named by `label` for screen readers and in its tooltip. */
7
+ function CloseButton({ label, onClick }) {
8
+ return /* @__PURE__ */ React.createElement("button", {
9
+ type: "button",
10
+ className: "twgit-close",
11
+ "aria-label": label,
12
+ title: label,
13
+ onClick
14
+ }, /* @__PURE__ */ React.createElement("svg", {
15
+ viewBox: "0 0 16 16",
16
+ width: "18",
17
+ height: "18",
18
+ "aria-hidden": "true",
19
+ focusable: "false"
20
+ }, /* @__PURE__ */ React.createElement("path", {
21
+ d: "M4 4l8 8M12 4l-8 8",
22
+ stroke: "currentColor",
23
+ strokeWidth: "1.6",
24
+ strokeLinecap: "round",
25
+ fill: "none"
26
+ })));
27
+ }
5
28
  // node_modules/diff/libesm/patch/line-endings.js
6
29
  function unixToWin(patch) {
7
30
  if (Array.isArray(patch)) return patch.map((p) => unixToWin(p));
@@ -1569,17 +1592,22 @@ function useBlockLocation(comparison, locateBlock) {
1569
1592
  status: (side) => (location === null || location === void 0 ? void 0 : location.comparison) === comparison && location.locator === locateBlock && location.key === sideKey(side) ? location.status : void 0
1570
1593
  };
1571
1594
  }
1572
- function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location }) {
1573
- const [open, setOpen] = useState(false);
1595
+ /** What a script is called in a review: its hat block, or both hat blocks when the change replaced it. */
1596
+ function scriptReviewTitle(script, labels) {
1597
+ const beforeTitle = script.before[0] && scriptTitle(script.before[0], labels);
1598
+ const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
1599
+ return beforeTitle && afterTitle && beforeTitle !== afterTitle ? beforeTitle + " → " + afterTitle : afterTitle !== null && afterTitle !== void 0 ? afterTitle : beforeTitle;
1600
+ }
1601
+ function ScriptComparison({ script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location, expanded = false }) {
1602
+ const [open, setOpen] = useState(expanded);
1574
1603
  const { sync, toggle } = usePreviewSync(labels === ja ? "ja" : "en");
1575
1604
  const status = script.kind;
1576
1605
  const locale = labels === ja ? "ja" : "en";
1577
1606
  const summary = script.summary.map((item) => summaryPhrase(item, locale)).join(labels === ja ? "、" : ", ");
1578
- const beforeTitle = script.before[0] && scriptTitle(script.before[0], labels);
1579
- const afterTitle = script.after[0] && scriptTitle(script.after[0], labels);
1580
- const title = beforeTitle && afterTitle && beforeTitle !== afterTitle ? beforeTitle + " → " + afterTitle : afterTitle !== null && afterTitle !== void 0 ? afterTitle : beforeTitle;
1607
+ const title = scriptReviewTitle(script, labels);
1581
1608
  return /* @__PURE__ */ React.createElement("details", {
1582
1609
  className: "twgit-review-script",
1610
+ open: expanded || void 0,
1583
1611
  onToggle: (event) => setOpen(event.currentTarget.open)
1584
1612
  }, /* @__PURE__ */ React.createElement("summary", null, /* @__PURE__ */ React.createElement("span", { className: "twgit-review-status" }, labels[status]), /* @__PURE__ */ React.createElement("strong", null, title), status === "reorganized" && /* @__PURE__ */ React.createElement("small", null, script.before.length, " → ", script.after.length), summary && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-summary" }, summary), reviewed && /* @__PURE__ */ React.createElement("small", null, labels.reviewed)), open && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "twgit-block-diff-sides" }, ["before", "after"].map((version) => /* @__PURE__ */ React.createElement("div", { key: version }, script[version].length ? script[version].map((side, index, sides) => /* @__PURE__ */ React.createElement(BlockPreview, {
1585
1613
  key: side.id,
@@ -1781,7 +1809,20 @@ var scriptPrint = (script) => fingerprint([
1781
1809
  script.after.map(scriptBlocks)
1782
1810
  ]);
1783
1811
  var storageKey = (name) => `turbowarp-plus-git:reviewed:${name}`;
1784
- var readMarks = (name) => {
1812
+ var memoryMarks = /* @__PURE__ */ new Map();
1813
+ var markListeners = /* @__PURE__ */ new Map();
1814
+ var subscribeMarks = (name, listener) => {
1815
+ var _markListeners$get;
1816
+ const listeners = (_markListeners$get = markListeners.get(name)) !== null && _markListeners$get !== void 0 ? _markListeners$get : /* @__PURE__ */ new Set();
1817
+ markListeners.set(name, listeners.add(listener));
1818
+ return () => {
1819
+ listeners.delete(listener);
1820
+ if (!listeners.size) markListeners.delete(name);
1821
+ };
1822
+ };
1823
+ var readMarks = (name, remember = true) => {
1824
+ var _memoryMarks$get;
1825
+ if (!remember) return (_memoryMarks$get = memoryMarks.get(name)) !== null && _memoryMarks$get !== void 0 ? _memoryMarks$get : {};
1785
1826
  try {
1786
1827
  var _globalThis$localStor, _globalThis$localStor2;
1787
1828
  const value = JSON.parse((_globalThis$localStor = (_globalThis$localStor2 = globalThis.localStorage) === null || _globalThis$localStor2 === void 0 ? void 0 : _globalThis$localStor2.getItem(storageKey(name))) !== null && _globalThis$localStor !== void 0 ? _globalThis$localStor : "{}");
@@ -1790,12 +1831,18 @@ var readMarks = (name) => {
1790
1831
  return {};
1791
1832
  }
1792
1833
  };
1793
- var writeMarks = (name, marks) => {
1834
+ var writeMarks = (name, marks, remember = true) => {
1835
+ var _markListeners$get2;
1836
+ if (!remember) {
1837
+ if (Object.keys(marks).length) memoryMarks.set(name, marks);
1838
+ else memoryMarks.delete(name);
1839
+ }
1794
1840
  try {
1795
1841
  var _globalThis$localStor3, _globalThis$localStor4;
1796
- if (Object.keys(marks).length) (_globalThis$localStor3 = globalThis.localStorage) === null || _globalThis$localStor3 === void 0 || _globalThis$localStor3.setItem(storageKey(name), JSON.stringify(marks));
1797
- else (_globalThis$localStor4 = globalThis.localStorage) === null || _globalThis$localStor4 === void 0 || _globalThis$localStor4.removeItem(storageKey(name));
1842
+ if (remember && Object.keys(marks).length) (_globalThis$localStor3 = globalThis.localStorage) === null || _globalThis$localStor3 === void 0 || _globalThis$localStor3.setItem(storageKey(name), JSON.stringify(marks));
1843
+ else if (remember) (_globalThis$localStor4 = globalThis.localStorage) === null || _globalThis$localStor4 === void 0 || _globalThis$localStor4.removeItem(storageKey(name));
1798
1844
  } catch (_unused4) {}
1845
+ (_markListeners$get2 = markListeners.get(name)) === null || _markListeners$get2 === void 0 || _markListeners$get2.forEach((listener) => listener());
1799
1846
  };
1800
1847
  function ProjectSourceReview({ title, description, verification, files, renderBlock, locale = "en", rememberAs }) {
1801
1848
  const labels = locale.startsWith("ja") ? ja : en;
@@ -1826,20 +1873,24 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1826
1873
  return prints;
1827
1874
  }, [entries]);
1828
1875
  const [checked, setChecked] = useState();
1876
+ const [, refresh] = useState(0);
1877
+ useEffect(() => rememberAs ? subscribeMarks(rememberAs, () => {
1878
+ setChecked(void 0);
1879
+ refresh((count) => count + 1);
1880
+ }) : void 0, [rememberAs]);
1829
1881
  const marks = (checked === null || checked === void 0 ? void 0 : checked.entries) === entries ? checked.marks : rememberAs ? readMarks(rememberAs) : {};
1830
1882
  const reviewed = new Set([...scriptPrints].filter(([key, print]) => marks[key] === print).map(([key]) => key));
1831
- const setReviewed = (key, value) => setChecked((previous) => {
1832
- const current = (previous === null || previous === void 0 ? void 0 : previous.entries) === entries ? previous.marks : rememberAs ? readMarks(rememberAs) : {};
1883
+ const setReviewed = (key, value) => {
1833
1884
  const next = {};
1834
- for (const [name, print] of scriptPrints) if (current[name] === print) next[name] = print;
1885
+ for (const [name, print] of scriptPrints) if (marks[name] === print) next[name] = print;
1835
1886
  if (value) next[key] = scriptPrints.get(key);
1836
1887
  else delete next[key];
1837
1888
  if (rememberAs) writeMarks(rememberAs, next);
1838
- return {
1889
+ setChecked({
1839
1890
  entries,
1840
1891
  marks: next
1841
- };
1842
- });
1892
+ });
1893
+ };
1843
1894
  const targets = entries.reduce((total, entry) => {
1844
1895
  var _entry$comparison$tar, _entry$comparison;
1845
1896
  return total + ((_entry$comparison$tar = (_entry$comparison = entry.comparison) === null || _entry$comparison === void 0 ? void 0 : _entry$comparison.targets.length) !== null && _entry$comparison$tar !== void 0 ? _entry$comparison$tar : 0);
@@ -1871,6 +1922,62 @@ function ProjectSourceReview({ title, description, verification, files, renderBl
1871
1922
  }
1872
1923
  }))));
1873
1924
  }
1925
+ /** The words of one tab of a project comparison opened as tabs. */
1926
+ function reviewTabWords(locale) {
1927
+ const labels = locale.startsWith("ja") ? ja : en;
1928
+ return {
1929
+ script: (script) => `${scriptReviewTitle(script, labels)} · ${labels[script.kind]}`,
1930
+ target: (target) => target.isStage ? labels.stage : target.name,
1931
+ targetDetails: labels.properties,
1932
+ project: labels.comparison,
1933
+ noChanges: labels.noChanges
1934
+ };
1935
+ }
1936
+ function useReviewMark(place, script) {
1937
+ const [, refresh] = useState(0);
1938
+ useEffect(() => place && subscribeMarks(place.name, () => refresh((count) => count + 1)), [place === null || place === void 0 ? void 0 : place.name]);
1939
+ if (!place) return {};
1940
+ const key = reviewKey(place.file, place.target, script);
1941
+ const print = scriptPrint(script);
1942
+ return {
1943
+ reviewed: readMarks(place.name, place.remember)[key] === print,
1944
+ setReviewed(value) {
1945
+ const marks = _objectSpread2({}, readMarks(place.name, place.remember));
1946
+ if (value) marks[key] = print;
1947
+ else delete marks[key];
1948
+ writeMarks(place.name, marks, place.remember);
1949
+ }
1950
+ };
1951
+ }
1952
+ /** One script of a comparison, open, for a tab of its own. */
1953
+ function ScriptReviewTab({ comparison, script, renderBlock, locale = "en", loadAsset, locateBlock, mark }) {
1954
+ const labels = locale.startsWith("ja") ? ja : en;
1955
+ const location = useBlockLocation(comparison, locateBlock);
1956
+ const { reviewed, setReviewed } = useReviewMark(mark, script);
1957
+ return /* @__PURE__ */ React.createElement("section", { className: "twgit-block-diff twgit-review twgit-review-tab" }, /* @__PURE__ */ React.createElement(ScriptComparison, {
1958
+ script,
1959
+ labels,
1960
+ renderBlock,
1961
+ loadAsset,
1962
+ location,
1963
+ expanded: true,
1964
+ reviewed,
1965
+ setReviewed
1966
+ }), /* @__PURE__ */ React.createElement(Legend, { labels }));
1967
+ }
1968
+ /**
1969
+ * What changed in a stage or sprite outside its code, or in the project as a whole when no target is given,
1970
+ * for a tab of its own.
1971
+ */
1972
+ function TargetReviewTab({ comparison, target, locale = "en", loadAsset }) {
1973
+ const labels = locale.startsWith("ja") ? ja : en;
1974
+ if (!target) return /* @__PURE__ */ React.createElement("section", { className: "twgit-block-diff twgit-review twgit-review-tab" }, comparison.properties.length > 0 ? /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(comparison.properties, labels)) : /* @__PURE__ */ React.createElement("p", null, labels.noChanges));
1975
+ return /* @__PURE__ */ React.createElement("section", { className: "twgit-block-diff twgit-review twgit-review-tab" }, /* @__PURE__ */ React.createElement("h5", null, /* @__PURE__ */ React.createElement(TargetThumbnails, {
1976
+ target,
1977
+ labels,
1978
+ loadAsset
1979
+ }), target.isStage ? labels.stage : target.name), target.status !== "updated" && /* @__PURE__ */ React.createElement("p", { className: "twgit-review-target-status" }, (target.status === "added" ? labels.targetAdded : labels.targetRemoved)(target.name, target.isStage)), target.status === "updated" && target.properties.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.properties, " · ", propertyNames(target.properties, labels)), target.scripts.length > 0 && /* @__PURE__ */ React.createElement("p", null, labels.scriptCount(target.scripts.length)));
1980
+ }
1874
1981
  // src/github-pull-requests.ts
1875
1982
  init_objectSpread2();
1876
1983
  var PullRequestError = class extends Error {
@@ -2116,6 +2223,119 @@ function createGitHubPullRequests(repository, send = fetch) {
2116
2223
  }
2117
2224
  };
2118
2225
  }
2226
+ // src/project-source-tabs.tsx
2227
+ var providerId = "turbowarp-plus-git";
2228
+ var statusColor = {
2229
+ added: "green",
2230
+ removed: "red",
2231
+ updated: "blue"
2232
+ };
2233
+ /** Renders its content once the tab is first shown, so a comparison with many scripts draws only what is looked at. */
2234
+ function WhenShown({ children }) {
2235
+ const element = useRef(null);
2236
+ const [shown, setShown] = useState(false);
2237
+ useEffect(() => {
2238
+ var _element$current;
2239
+ const view = (_element$current = element.current) === null || _element$current === void 0 ? void 0 : _element$current.ownerDocument.defaultView;
2240
+ if (shown || !element.current) return;
2241
+ if (!(view === null || view === void 0 ? void 0 : view.IntersectionObserver)) {
2242
+ setShown(true);
2243
+ return;
2244
+ }
2245
+ const observer = new view.IntersectionObserver((entries) => {
2246
+ if (entries.some((entry) => entry.isIntersecting)) setShown(true);
2247
+ });
2248
+ observer.observe(element.current);
2249
+ return () => observer.disconnect();
2250
+ }, [shown]);
2251
+ return /* @__PURE__ */ React.createElement("div", {
2252
+ ref: element,
2253
+ className: "twgit-review-tab-body"
2254
+ }, shown && children);
2255
+ }
2256
+ /**
2257
+ * Opens a comparison of project.source.json files as tabs of the host's workspace: a tab group for each changed
2258
+ * stage or sprite, holding a tab for each changed script and one for its changes outside the code.
2259
+ * Returns how many tabs were asked for, or `0` when the host has no workspace to take them.
2260
+ */
2261
+ function openProjectSourceTabs(openPanel, { key, files, renderBlock, locale = "en", rememberAs }) {
2262
+ if (!openPanel) return 0;
2263
+ const words = reviewTabWords(locale);
2264
+ const area = `${providerId}:diff:${key}`;
2265
+ const requests = [];
2266
+ const tab = (documentId, title, content, tabGroup) => ({
2267
+ reference: {
2268
+ providerId,
2269
+ documentId: `diff:${key}:${documentId}`
2270
+ },
2271
+ filename: title,
2272
+ title,
2273
+ transient: true,
2274
+ area,
2275
+ tabGroup,
2276
+ inactive: requests.length > 0,
2277
+ mount(element) {
2278
+ element.classList.add("twgit-panel-tab", "twgit-review-tab-slot");
2279
+ render(/* @__PURE__ */ React.createElement(WhenShown, null, content()), element);
2280
+ return {
2281
+ requestClose: () => {},
2282
+ dispose: () => {
2283
+ unmountComponentAtNode(element);
2284
+ }
2285
+ };
2286
+ }
2287
+ });
2288
+ for (const file of files) {
2289
+ const comparison = compareProjectForReview(file.before, file.after);
2290
+ const prefix = files.length > 1 ? file.path.split("/").slice(0, -1).join("/") : "";
2291
+ const labelOf = (target) => prefix ? `${prefix}: ${words.target(target)}` : words.target(target);
2292
+ if (comparison.properties.length || !comparison.targets.length) {
2293
+ const title = prefix ? `${prefix}: ${words.project}` : words.project;
2294
+ requests.push(tab(`${file.path}:project`, title, () => /* @__PURE__ */ React.createElement(TargetReviewTab, {
2295
+ comparison,
2296
+ locale
2297
+ })));
2298
+ }
2299
+ for (const target of comparison.targets) {
2300
+ const targetKey = JSON.stringify([
2301
+ file.path,
2302
+ target.isStage,
2303
+ target.name
2304
+ ]);
2305
+ const tabGroup = {
2306
+ id: `${area}:${targetKey}`,
2307
+ label: labelOf(target),
2308
+ color: statusColor[target.status]
2309
+ };
2310
+ if (target.status !== "updated" || target.properties.length || !target.scripts.length) requests.push(tab(`${targetKey}:details`, words.targetDetails, () => /* @__PURE__ */ React.createElement(TargetReviewTab, {
2311
+ comparison,
2312
+ target,
2313
+ locale,
2314
+ loadAsset: file.loadAsset
2315
+ }), tabGroup));
2316
+ for (const script of target.scripts) requests.push(tab(`${targetKey}:${script.id}`, words.script(script), () => /* @__PURE__ */ React.createElement(ScriptReviewTab, {
2317
+ comparison,
2318
+ script,
2319
+ renderBlock,
2320
+ locale,
2321
+ loadAsset: file.loadAsset,
2322
+ locateBlock: file.locateBlock,
2323
+ mark: {
2324
+ name: rememberAs !== null && rememberAs !== void 0 ? rememberAs : area,
2325
+ remember: !!rememberAs,
2326
+ file: file.path,
2327
+ target
2328
+ }
2329
+ }), tabGroup));
2330
+ }
2331
+ }
2332
+ let opened = 0;
2333
+ for (const request of requests) {
2334
+ if (!openPanel(request)) break;
2335
+ opened++;
2336
+ }
2337
+ return opened;
2338
+ }
2119
2339
  // src/block-diff-location.ts
2120
2340
  function markBlockLocation(block, parts) {
2121
2341
  const root = block.getSvgRoot();
@@ -2473,7 +2693,7 @@ function createScratchBlocksDiffRenderer(options) {
2473
2693
  init_objectSpread2();
2474
2694
  var focusable = "button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], summary, [tabindex=\"0\"]";
2475
2695
  /** A modal that lays project comparisons side by side in sections and asks the user to choose an action. */
2476
- function ProjectComparisonDialog({ title, description, notes = [], sections, actions, closeLabel = "Close", renderBlock, locale = "en", onChoose }) {
2696
+ function ProjectComparisonDialog({ title, description, notes = [], sections, actions, closeLabel, renderBlock, locale = "en", onChoose }) {
2477
2697
  const dialog = useRef(null);
2478
2698
  const first = useRef(null);
2479
2699
  useEffect(() => {
@@ -2510,10 +2730,10 @@ function ProjectComparisonDialog({ title, description, notes = [], sections, act
2510
2730
  "aria-modal": "true",
2511
2731
  "aria-label": title,
2512
2732
  onKeyDown: keyDown
2513
- }, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, title), /* @__PURE__ */ React.createElement("button", {
2514
- type: "button",
2733
+ }, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, title), /* @__PURE__ */ React.createElement(CloseButton, {
2734
+ label: closeLabel !== null && closeLabel !== void 0 ? closeLabel : locale.startsWith("ja") ? "閉じる" : "Close",
2515
2735
  onClick: () => onChoose(null)
2516
- }, closeLabel)), /* @__PURE__ */ React.createElement("div", { className: "twgit-comparison-body" }, description && /* @__PURE__ */ React.createElement("p", null, description), notes.map((note) => /* @__PURE__ */ React.createElement("p", {
2736
+ })), /* @__PURE__ */ React.createElement("div", { className: "twgit-comparison-body" }, description && /* @__PURE__ */ React.createElement("p", null, description), notes.map((note) => /* @__PURE__ */ React.createElement("p", {
2517
2737
  key: note,
2518
2738
  role: "note"
2519
2739
  }, note)), sections.map((section) => /* @__PURE__ */ React.createElement("section", {
@@ -2546,4 +2766,4 @@ function openProjectComparison(document, options) {
2546
2766
  render(/* @__PURE__ */ React.createElement(ProjectComparisonDialog, _objectSpread2(_objectSpread2({}, options), {}, { onChoose: choose })), root);
2547
2767
  });
2548
2768
  }
2549
- export { readProjectSourceDiff as C, parseProjectSource as S, currentCostume as _, PullRequestError as a, compareProjectBlocks as b, ProjectSourceComparison as c, organizeProjectChanges as d, projectPropertyLabel as f, usePreviewSync as g, createPreviewSync as h, createScratchBlocksDiffLocator as i, ProjectSourceReview as l, BlockPreview as m, openProjectComparison as n, createGitHubPullRequests as o, compareProjectForReview as p, createScratchBlocksDiffRenderer as r, gitHubRepositoryFromOrigin as s, ProjectComparisonDialog as t, assetFileName as u, useAssetImage as v, createProjectSourceDiff as x, useCostumeImage as y };
2769
+ export { parseProjectSource as C, createProjectSourceDiff as S, CloseButton as T, usePreviewSync as _, openProjectSourceTabs as a, useCostumeImage as b, gitHubRepositoryFromOrigin as c, assetFileName as d, organizeProjectChanges as f, createPreviewSync as g, BlockPreview as h, createScratchBlocksDiffLocator as i, ProjectSourceComparison as l, compareProjectForReview as m, openProjectComparison as n, PullRequestError as o, projectPropertyLabel as p, createScratchBlocksDiffRenderer as r, createGitHubPullRequests as s, ProjectComparisonDialog as t, ProjectSourceReview as u, currentCostume as v, readProjectSourceDiff as w, compareProjectBlocks as x, useAssetImage as y };
@@ -18,6 +18,7 @@ export interface ProjectComparisonOptions {
18
18
  sections: readonly ProjectComparisonSection[];
19
19
  /** The first action is the one the dialog focuses. Closing the dialog or pressing Escape chooses none. */
20
20
  actions: readonly ProjectComparisonAction[];
21
+ /** The tooltip and accessible name of the X button that closes the dialog. By default, "Close" in the locale. */
21
22
  closeLabel?: string;
22
23
  renderBlock: ProjectBlockRenderer;
23
24
  locale?: string;
@@ -5,6 +5,8 @@ import './project-source-diff.css';
5
5
  export { createGitHubPullRequests, gitHubRepositoryFromOrigin } from './github-pull-requests.js';
6
6
  export { ProjectSourceComparison, ProjectSourceReview } from './project-source-review.js';
7
7
  export { ProjectComparisonDialog, openProjectComparison } from './project-comparison.js';
8
+ export { openProjectSourceTabs } from './project-source-tabs.js';
9
+ export type { ProjectSourceTabsFile, ProjectSourceTabsOptions } from './project-source-tabs.js';
8
10
  export type { ProjectComparisonAction, ProjectComparisonDialogProps, ProjectComparisonOptions, ProjectComparisonSection } from './project-comparison.js';
9
11
  export type { ProjectSourceComparisonProps, ProjectSourceReviewFile, ProjectSourceReviewProps } from './project-source-review.js';
10
12
  export { createScratchBlocksDiffLocator } from './block-diff-location.js';
@@ -1,4 +1,4 @@
1
- import { C as readProjectSourceDiff, _ as currentCostume, b as compareProjectBlocks, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, g as usePreviewSync, h as createPreviewSync, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, m as BlockPreview, n as openProjectComparison, o as createGitHubPullRequests, p as compareProjectForReview, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, t as ProjectComparisonDialog, u as assetFileName, x as createProjectSourceDiff } from "./project-comparison-LAyP_beL.js";
1
+ import { S as createProjectSourceDiff, _ 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, w as readProjectSourceDiff, x as compareProjectBlocks } from "./project-comparison-1rggIJse.js";
2
2
  import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
3
3
  import React, { useEffect, useMemo, useRef, useState } from "react";
4
4
  // src/project-source-diff.tsx
@@ -169,4 +169,4 @@ function ProjectSourceBlockDiff({ patch, beforeSource, renderBlock, locateBlock,
169
169
  })
170
170
  }, labels.more));
171
171
  }
172
- export { ProjectComparisonDialog, ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, compareProjectForReview, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, openProjectComparison, organizeProjectChanges, projectPropertyLabel };
172
+ export { ProjectComparisonDialog, ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, compareProjectForReview, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, openProjectComparison, openProjectSourceTabs, organizeProjectChanges, projectPropertyLabel };
@@ -1,7 +1,10 @@
1
1
  import React from 'react';
2
+ import { compareProjectForReview } from './project-source-review-model.js';
3
+ import type { ScriptReview, TargetReview } from './project-source-review-model.js';
2
4
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
5
  import type { ProjectBlockLocator } from './block-diff-location.js';
4
6
  import type { ProjectAssetLoader } from './project-assets.js';
7
+ type Comparison = ReturnType<typeof compareProjectForReview>;
5
8
  export type ProjectSourceReviewFile = {
6
9
  path: string;
7
10
  summary?: string;
@@ -51,3 +54,43 @@ export interface ProjectSourceComparisonProps {
51
54
  /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
52
55
  export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, locateBlock, target }: ProjectSourceComparisonProps): React.JSX.Element;
53
56
  export declare function ProjectSourceReview({ title, description, verification, files, renderBlock, locale, rememberAs }: ProjectSourceReviewProps): React.JSX.Element;
57
+ /** The words of one tab of a project comparison opened as tabs. */
58
+ export declare function reviewTabWords(locale: string): {
59
+ script: (script: ScriptReview) => string;
60
+ target: (target: TargetReview) => string;
61
+ targetDetails: string;
62
+ project: string;
63
+ noChanges: string;
64
+ };
65
+ /**
66
+ * Where a tab keeps its reviewed mark: under `name` with the review of the same name, in the browser when
67
+ * `remember` is set, or else while the page is open.
68
+ */
69
+ export interface ReviewMarkPlace {
70
+ name: string;
71
+ remember: boolean;
72
+ file: string;
73
+ target: TargetReview;
74
+ }
75
+ /** One script of a comparison, open, for a tab of its own. */
76
+ export declare function ScriptReviewTab({ comparison, script, renderBlock, locale, loadAsset, locateBlock, mark }: {
77
+ comparison: Comparison;
78
+ script: ScriptReview;
79
+ renderBlock: ProjectBlockRenderer;
80
+ locale?: string;
81
+ loadAsset?: ProjectAssetLoader;
82
+ locateBlock?: ProjectBlockLocator;
83
+ /** Offers the reviewed mark, shared with the review panel and other tabs that use the same place. */
84
+ mark?: ReviewMarkPlace;
85
+ }): React.JSX.Element;
86
+ /**
87
+ * What changed in a stage or sprite outside its code, or in the project as a whole when no target is given,
88
+ * for a tab of its own.
89
+ */
90
+ export declare function TargetReviewTab({ comparison, target, locale, loadAsset }: {
91
+ comparison: Comparison;
92
+ target?: TargetReview;
93
+ locale?: string;
94
+ loadAsset?: ProjectAssetLoader;
95
+ }): React.JSX.Element;
96
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { ProjectSource } from './project-source-patch.js';
2
+ import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
+ import type { ProjectAssetLoader } from './project-assets.js';
4
+ import type { ProjectBlockLocator } from './block-diff-location.js';
5
+ import type { GitPanelRequest } from './index.js';
6
+ /** One project.source.json of a comparison, read into both versions. */
7
+ export interface ProjectSourceTabsFile {
8
+ path: string;
9
+ before: ProjectSource;
10
+ after: ProjectSource;
11
+ loadAsset?: ProjectAssetLoader;
12
+ /** Offers "Highlight in Editor" on the after version, which the caller has matched with the open project. */
13
+ locateBlock?: ProjectBlockLocator;
14
+ }
15
+ export interface ProjectSourceTabsOptions {
16
+ /**
17
+ * Names this comparison, such as `owner/repo#42@<head>` or `<commit>:project.source.json`. Its tabs share one
18
+ * split, and opening the same comparison again brings its tabs forward instead of adding them twice.
19
+ */
20
+ key: string;
21
+ files: readonly ProjectSourceTabsFile[];
22
+ renderBlock: ProjectBlockRenderer;
23
+ locale?: string;
24
+ /**
25
+ * Shares the reviewed marks of the script tabs with the review of this name, such as `owner/repo#42`, and keeps
26
+ * them in the browser. Without it, the marks are kept for this comparison while the page is open.
27
+ */
28
+ rememberAs?: string;
29
+ }
30
+ /**
31
+ * Opens a comparison of project.source.json files as tabs of the host's workspace: a tab group for each changed
32
+ * stage or sprite, holding a tab for each changed script and one for its changes outside the code.
33
+ * Returns how many tabs were asked for, or `0` when the host has no workspace to take them.
34
+ */
35
+ export declare function openProjectSourceTabs(openPanel: ((request: GitPanelRequest) => boolean) | undefined, { key, files, renderBlock, locale, rememberAs }: ProjectSourceTabsOptions): number;
@@ -2,10 +2,15 @@ import React from 'react';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
3
  import type { Repository } from './repository.js';
4
4
  import type { ProjectBlockLocator } from './block-diff-location.js';
5
- export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }: {
5
+ import type { ProjectSourceTabsFile } from './project-source-tabs.js';
6
+ type OpenTabs = (key: string, files: readonly ProjectSourceTabsFile[], rememberAs?: string) => number;
7
+ export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor, openTabs }: {
6
8
  repository: Repository;
7
9
  locale: string;
8
10
  renderBlock: ProjectBlockRenderer;
11
+ /** Opens a pull request's project changes as tabs of the host's layout. */
12
+ openTabs?: OpenTabs;
9
13
  /** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
10
14
  locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
11
15
  }): React.JSX.Element;
16
+ export {};