@kubohiroya/turbowarp-plus-editor-git 0.10.0 → 0.12.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.
@@ -0,0 +1,27 @@
1
+ import type { ProjectBlockLocator, ScratchBlockLocationWorkspace } from './block-diff-location.js';
2
+ /** The parts of a Scratch VM the locator reads to find a stage or sprite. */
3
+ export interface LocatorVM {
4
+ runtime: {
5
+ targets: readonly {
6
+ id: string;
7
+ isOriginal?: boolean;
8
+ isStage: boolean;
9
+ getName(): string;
10
+ }[];
11
+ };
12
+ editingTarget?: {
13
+ id: string;
14
+ } | null;
15
+ }
16
+ export interface EditorBlockLocatorOptions {
17
+ vm: LocatorVM;
18
+ /** Opens the code of one stage or sprite in the Editor, as project search does. */
19
+ openTargetCode(targetId: string): void | Promise<void>;
20
+ /** The Editor's code workspace as it is now, or `null` while none is shown. */
21
+ getWorkspace(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
22
+ }
23
+ /**
24
+ * Finds the stage or sprite of a compared version in the open project, opens its code and waits for the block,
25
+ * then centers and outlines it. The caller decides which version matches the open project.
26
+ */
27
+ export declare function createEditorBlockLocator({ vm, openTargetCode, getWorkspace }: EditorBlockLocatorOptions): ProjectBlockLocator;
@@ -3,16 +3,18 @@ import type { FileChange, FileDiff } from './repository.js';
3
3
  import type { labelsFor } from './messages.js';
4
4
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
5
5
  import type { ProjectAssetLoader } from './project-assets.js';
6
+ import type { ProjectBlockLocator } from './block-diff-location.js';
6
7
  /** Project sources are the files the panel opens as projects. */
7
8
  export declare const isProjectSource: (path: string) => boolean;
8
9
  /**
9
10
  * Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
10
11
  */
11
- export declare function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }: {
12
+ export declare function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset, locateBlock }: {
12
13
  change: FileChange;
13
14
  diff: FileDiff | null;
14
15
  labels: ReturnType<typeof labelsFor>;
15
16
  locale: string;
16
17
  renderBlock?: ProjectBlockRenderer;
17
18
  loadAsset?: ProjectAssetLoader;
19
+ locateBlock?: ProjectBlockLocator;
18
20
  }): React.JSX.Element;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { GitConnectionSettings } from './connection-settings.js';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
+ import type { ScratchBlockLocationWorkspace } from './block-diff-location.js';
4
+ export { createScratchBlocksDiffLocator } from './block-diff-location.js';
5
+ export { createEditorBlockLocator } from './editor-block-locator.js';
3
6
  export { createScratchBlocksDiffRenderer } from './scratch-block-diff-renderer.js';
4
7
  export type { ScratchBlockDiffRendererOptions, ScratchBlocksPort } from './scratch-block-diff-renderer.js';
5
8
  export { openRepository, createReadOnlyFs, GitPanelError } from './repository.js';
@@ -11,6 +14,28 @@ export interface GitHost {
11
14
  isProjectDirty(): boolean;
12
15
  isProjectRunning?(): boolean;
13
16
  subscribe?(listener: (event: 'loaded' | 'changed') => void): () => void;
17
+ /**
18
+ * Opens the panel as a tab of the host's layout, with the same shape as the layout package's DocumentPanelRequest.
19
+ * Returning `false` keeps the modal dialog.
20
+ */
21
+ openPanel?(request: GitPanelRequest): boolean;
22
+ /** Opens the code of one stage or sprite, so a diff can point at its block. Without it, no block is located. */
23
+ openTargetCode?(targetId: string): void | Promise<void>;
24
+ /** The Editor's code workspace as it is now. */
25
+ getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
26
+ }
27
+ export interface GitPanelRequest {
28
+ reference: {
29
+ providerId: string;
30
+ documentId: string;
31
+ };
32
+ filename: string;
33
+ title?: string;
34
+ transient?: boolean;
35
+ mount(element: HTMLElement, close: () => void): {
36
+ requestClose(): void;
37
+ dispose(): void;
38
+ };
14
39
  }
15
40
  export declare function mountGitPanel({ vm, document, host }: {
16
41
  vm: object;
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as gitHubRepositoryFromOrigin, c as projectPropertyLabel, g as useCostumeImage, h as useAssetImage, i as createGitHubPullRequests, l as ProjectSourceComparison, r as PullRequestError, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, y as parseProjectSource } from "./scratch-block-diff-renderer-DZR8ep-T.js";
1
+ import { _ as useCostumeImage, a as gitHubRepositoryFromOrigin, b as parseProjectSource, g as useAssetImage, i as createGitHubPullRequests, l as organizeProjectChanges, n as createScratchBlocksDiffLocator, o as ProjectSourceComparison, r as PullRequestError, s as ProjectSourceReview, t as createScratchBlocksDiffRenderer, u as projectPropertyLabel } from "./scratch-block-diff-renderer-e_drVlHE.js";
2
2
  import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
3
  import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-CtvWYPQ8.js";
4
4
  import * as m from "react";
5
5
  import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
- import { render, unmountComponentAtNode } from "react-dom";
6
+ import { createPortal, render, unmountComponentAtNode } from "react-dom";
7
7
  // builtin:esm-external-require-react
8
8
  var require_builtin_esm_external_require_react = /* @__PURE__ */ __commonJSMin(((exports, module) => {
9
9
  init_objectSpread2();
@@ -2684,7 +2684,7 @@ var isProjectSource = (path) => path.split("/").pop() === "project.source.json";
2684
2684
  /**
2685
2685
  * Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
2686
2686
  */
2687
- function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }) {
2687
+ function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset, locateBlock }) {
2688
2688
  var _change$oldMode, _change$newMode;
2689
2689
  const [view, setView] = useState("blocks");
2690
2690
  const names = locale.startsWith("ja") ? ja$3 : en$3;
@@ -2713,6 +2713,7 @@ function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset })
2713
2713
  locale,
2714
2714
  fallback: text,
2715
2715
  loadAsset,
2716
+ locateBlock,
2716
2717
  before: change.before === null ? null : sources.before,
2717
2718
  after: change.after === null ? null : sources.after
2718
2719
  }) : text);
@@ -2965,7 +2966,7 @@ var show = (value) => {
2965
2966
  return text.length > 400 ? text.slice(0, 400) + "…" : text;
2966
2967
  };
2967
2968
  /** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
2968
- function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile }) {
2969
+ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile, locateBlock }) {
2969
2970
  var _element$properties2;
2970
2971
  const labels = projectChangeLabels(locale);
2971
2972
  const { element, target } = selection;
@@ -3000,7 +3001,8 @@ function ProjectElementView({ changes, selection, locale, renderBlock, onSelectF
3000
3001
  target: {
3001
3002
  isStage: target.isStage,
3002
3003
  name: target.name
3003
- }
3004
+ },
3005
+ locateBlock
3004
3006
  }), element.kind === "costumes" && /* @__PURE__ */ React.createElement("div", { className: "twgit-element-pictures" }, ["before", "after"].map((version) => {
3005
3007
  var _element$md5ext3, _element$version;
3006
3008
  return /* @__PURE__ */ React.createElement(Picture, {
@@ -3299,6 +3301,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
3299
3301
  }, labels[error]));
3300
3302
  }
3301
3303
  // src/pull-request-panel.tsx
3304
+ init_objectSpread2();
3302
3305
  var en = {
3303
3306
  title: "Pull requests",
3304
3307
  number: "Pull request number",
@@ -3344,7 +3347,7 @@ var ja = {
3344
3347
  function errorMessage(error, labels) {
3345
3348
  return error instanceof PullRequestError ? labels[error.code] : labels.failed;
3346
3349
  }
3347
- function PullRequests({ name, labels, locale, renderBlock }) {
3350
+ function PullRequests({ name, labels, locale, renderBlock, locateFor }) {
3348
3351
  const client = useMemo(() => createGitHubPullRequests(name), [name]);
3349
3352
  const [page, setPage] = useState(1);
3350
3353
  const [listing, setListing] = useState();
@@ -3432,12 +3435,12 @@ function PullRequests({ name, labels, locale, renderBlock }) {
3432
3435
  }, labels.onGitHub)), /* @__PURE__ */ React.createElement(ProjectSourceReview, {
3433
3436
  title: pr.title,
3434
3437
  description: pr.description,
3435
- files: pr.files,
3438
+ files: locateFor ? pr.files.map((file) => "patch" in file ? _objectSpread2$1(_objectSpread2$1({}, file), {}, { locateBlock: locateFor(file.path, pr.head.sha) }) : file) : pr.files,
3436
3439
  renderBlock,
3437
3440
  locale
3438
3441
  }))));
3439
3442
  }
3440
- function PullRequestReviewPanel({ repository, locale, renderBlock }) {
3443
+ function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }) {
3441
3444
  const labels = locale.startsWith("ja") ? ja : en;
3442
3445
  const [origin, setOrigin] = useState();
3443
3446
  useEffect(() => {
@@ -3471,7 +3474,44 @@ function PullRequestReviewPanel({ repository, locale, renderBlock }) {
3471
3474
  name: origin.name,
3472
3475
  labels,
3473
3476
  locale,
3474
- renderBlock
3477
+ renderBlock,
3478
+ locateFor
3479
+ });
3480
+ }
3481
+ // src/editor-block-locator.ts
3482
+ /**
3483
+ * Finds the stage or sprite of a compared version in the open project, opens its code and waits for the block,
3484
+ * then centers and outlines it. The caller decides which version matches the open project.
3485
+ */
3486
+ function createEditorBlockLocator({ vm, openTargetCode, getWorkspace }) {
3487
+ return createScratchBlocksDiffLocator(async ({ target, id: blockId, signal }) => {
3488
+ const found = vm.runtime.targets.find((item) => target.isStage ? item.isStage : !item.isStage && item.isOriginal !== false && item.getName() === target.name);
3489
+ if (!found || signal.aborted) return null;
3490
+ await openTargetCode(found.id);
3491
+ const deadline = Date.now() + waitLimit;
3492
+ while (!signal.aborted) {
3493
+ var _vm$editingTarget;
3494
+ if (((_vm$editingTarget = vm.editingTarget) === null || _vm$editingTarget === void 0 ? void 0 : _vm$editingTarget.id) === found.id) {
3495
+ const workspace = await getWorkspace();
3496
+ if (signal.aborted) break;
3497
+ if (workspace === null || workspace === void 0 ? void 0 : workspace.getBlockById(blockId)) return workspace;
3498
+ }
3499
+ if (Date.now() >= deadline) break;
3500
+ await nextFrame(signal);
3501
+ }
3502
+ return null;
3503
+ });
3504
+ }
3505
+ var waitLimit = 2e3;
3506
+ function nextFrame(signal) {
3507
+ return new Promise((resolve) => {
3508
+ const done = () => {
3509
+ signal.removeEventListener("abort", done);
3510
+ resolve();
3511
+ };
3512
+ signal.addEventListener("abort", done, { once: true });
3513
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(done);
3514
+ else setTimeout(done, 16);
3475
3515
  });
3476
3516
  }
3477
3517
  // src/index.tsx
@@ -3490,9 +3530,20 @@ var initial = {
3490
3530
  };
3491
3531
  var controllers = /* @__PURE__ */ new WeakMap();
3492
3532
  var watchInterval = 2e3;
3493
- function GitPanel({ document, host, controller }) {
3494
- var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$snapshot;
3533
+ function GitPanel({ vm, document, host, controller }) {
3534
+ var _state$repository, _state$snapshot$head$, _state$snapshot$head, _state$snapshot2;
3495
3535
  const [visible, setVisible] = useState(false);
3536
+ const [slot, setSlot] = useState(null);
3537
+ const shown = visible || slot !== null;
3538
+ const locator = useMemo(() => slot && host.openTargetCode && host.getWorkspace ? createEditorBlockLocator({
3539
+ vm,
3540
+ openTargetCode: host.openTargetCode,
3541
+ getWorkspace: host.getWorkspace
3542
+ }) : void 0, [
3543
+ slot !== null,
3544
+ host,
3545
+ vm
3546
+ ]);
3496
3547
  const [review, setReview] = useState(false);
3497
3548
  const [locale, setLocale] = useState(document.documentElement.lang || "en");
3498
3549
  const [dirty, setDirty] = useState(host.isProjectDirty());
@@ -3550,8 +3601,34 @@ function GitPanel({ document, host, controller }) {
3550
3601
  }
3551
3602
  };
3552
3603
  controller.open = (language) => {
3604
+ var _host$openPanel;
3553
3605
  setLocale(language);
3554
- setVisible(true);
3606
+ if (!((_host$openPanel = host.openPanel) === null || _host$openPanel === void 0 ? void 0 : _host$openPanel.call(host, {
3607
+ reference: {
3608
+ providerId: "turbowarp-plus-git",
3609
+ documentId: "local-git"
3610
+ },
3611
+ filename: "Git",
3612
+ title: labelsFor(language).title,
3613
+ transient: true,
3614
+ mount(element, close) {
3615
+ setVisible(false);
3616
+ setSlot({
3617
+ element,
3618
+ close
3619
+ });
3620
+ return {
3621
+ requestClose: close,
3622
+ dispose() {
3623
+ setSlot((previous) => (previous === null || previous === void 0 ? void 0 : previous.element) === element ? null : previous);
3624
+ }
3625
+ };
3626
+ }
3627
+ }))) setVisible(true);
3628
+ };
3629
+ const closePanel = () => {
3630
+ if (slot) slot.close();
3631
+ else setVisible(false);
3555
3632
  };
3556
3633
  controller.refresh = async (automatic = false) => {
3557
3634
  if (!alive.current || !current.current.repository) return;
@@ -3600,7 +3677,7 @@ function GitPanel({ document, host, controller }) {
3600
3677
  }, [host]);
3601
3678
  useEffect(() => {
3602
3679
  const repository = state.repository;
3603
- if (!visible || !repository) return;
3680
+ if (!shown || !repository) return;
3604
3681
  const view = document.defaultView;
3605
3682
  let stopped = false;
3606
3683
  let checking = false;
@@ -3647,19 +3724,19 @@ function GitPanel({ document, host, controller }) {
3647
3724
  observer === null || observer === void 0 || observer.disconnect();
3648
3725
  };
3649
3726
  }, [
3650
- visible,
3727
+ shown,
3651
3728
  state.repository,
3652
3729
  locale
3653
3730
  ]);
3654
3731
  useEffect(() => {
3655
3732
  var _dialog$current;
3656
- if (!visible) return;
3733
+ if (!visible || slot) return;
3657
3734
  const previous = document.activeElement;
3658
3735
  (_dialog$current = dialog.current) === null || _dialog$current === void 0 || _dialog$current.focus();
3659
3736
  return () => {
3660
3737
  if (previous === null || previous === void 0 ? void 0 : previous.isConnected) previous.focus();
3661
3738
  };
3662
- }, [visible]);
3739
+ }, [visible, slot]);
3663
3740
  const chooseRoot = async () => {
3664
3741
  const view = document.defaultView;
3665
3742
  await run(async () => {
@@ -3696,6 +3773,7 @@ function GitPanel({ document, host, controller }) {
3696
3773
  } finally {
3697
3774
  opening.current = false;
3698
3775
  }
3776
+ if (slot) controller.open(locale);
3699
3777
  if (!alive.current) return {};
3700
3778
  setDirty(host.isProjectDirty());
3701
3779
  setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
@@ -3776,7 +3854,7 @@ function GitPanel({ document, host, controller }) {
3776
3854
  const focusTrap = (event) => {
3777
3855
  if (event.key === "Escape") {
3778
3856
  event.stopPropagation();
3779
- setVisible(false);
3857
+ closePanel();
3780
3858
  return;
3781
3859
  }
3782
3860
  if (event.key !== "Tab") return;
@@ -3795,23 +3873,21 @@ function GitPanel({ document, host, controller }) {
3795
3873
  state.snapshot,
3796
3874
  state.selected
3797
3875
  ]);
3798
- if (!visible) return null;
3876
+ if (!shown) return null;
3799
3877
  const groups = state.commit ? ["commit"] : [
3800
3878
  "staged",
3801
3879
  "unstaged",
3802
3880
  "untracked"
3803
3881
  ];
3804
3882
  const projectList = typeof projectChanges === "object" ? projectChanges : null;
3883
+ const projectFile = state.project === null ? null : [state.project, "project.source.json"].filter(Boolean).join("/");
3884
+ const fileLocator = (change) => locator && change.path === projectFile && (change.group === "unstaged" || change.group === "untracked" || change.group === "staged" && !state.changes.some((item) => item.path === change.path && item.group === "unstaged")) ? locator : void 0;
3885
+ const reviewLocator = (path, headSha) => {
3886
+ var _state$snapshot;
3887
+ return locator && path === projectFile && ((_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.head) === headSha && !state.changes.length ? locator : void 0;
3888
+ };
3805
3889
  const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
3806
- return /* @__PURE__ */ React.createElement("div", { className: "twgit-backdrop" }, /* @__PURE__ */ React.createElement("div", {
3807
- className: "twgit-panel",
3808
- ref: dialog,
3809
- tabIndex: -1,
3810
- role: "dialog",
3811
- "aria-modal": "true",
3812
- "aria-label": labels.title,
3813
- onKeyDown: focusTrap
3814
- }, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, labels.title), /* @__PURE__ */ React.createElement("button", {
3890
+ const content = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, labels.title), /* @__PURE__ */ React.createElement("button", {
3815
3891
  onClick: chooseRoot,
3816
3892
  disabled: state.busy
3817
3893
  }, labels.choose), !review && /* @__PURE__ */ React.createElement("button", {
@@ -3824,7 +3900,7 @@ function GitPanel({ document, host, controller }) {
3824
3900
  type: "button",
3825
3901
  disabled: state.busy,
3826
3902
  onClick: () => setReview(!review)
3827
- }, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick: () => setVisible(false) }, labels.close)), /* @__PURE__ */ React.createElement("div", { className: "twgit-context" }, /* @__PURE__ */ React.createElement("span", null, labels.repository, " · ", ((_state$repository = state.repository) === null || _state$repository === void 0 ? void 0 : _state$repository.root.name) || "—"), /* @__PURE__ */ React.createElement("span", null, labels.project, " · ", state.project === null ? labels.none : state.project || labels.root), /* @__PURE__ */ React.createElement("span", { "data-git-dirty": "" }, dirty ? labels.dirty : labels.saved), state.snapshot && /* @__PURE__ */ React.createElement("span", {
3903
+ }, review ? labels.localChanges : labels.pullRequests), /* @__PURE__ */ React.createElement("button", { onClick: closePanel }, labels.close)), /* @__PURE__ */ React.createElement("div", { className: "twgit-context" }, /* @__PURE__ */ React.createElement("span", null, labels.repository, " · ", ((_state$repository = state.repository) === null || _state$repository === void 0 ? void 0 : _state$repository.root.name) || "—"), /* @__PURE__ */ React.createElement("span", null, labels.project, " · ", state.project === null ? labels.none : state.project || labels.root), /* @__PURE__ */ React.createElement("span", { "data-git-dirty": "" }, dirty ? labels.dirty : labels.saved), state.snapshot && /* @__PURE__ */ React.createElement("span", {
3828
3904
  "data-git-upstream": state.snapshot.upstream.state,
3829
3905
  title: labels.checkedAt
3830
3906
  }, upstreamText(state.snapshot, labels)), !review && host.connectionSettings && /* @__PURE__ */ React.createElement(ConnectionSettings, {
@@ -3845,7 +3921,8 @@ function GitPanel({ document, host, controller }) {
3845
3921
  }, state.notice), state.busy && /* @__PURE__ */ React.createElement("p", { role: "status" }, labels.loading), !state.repository && /* @__PURE__ */ React.createElement("p", null, labels.empty), review && state.repository && host.renderReviewBlock && /* @__PURE__ */ React.createElement(PullRequestReviewPanel, {
3846
3922
  repository: state.repository,
3847
3923
  locale,
3848
- renderBlock: host.renderReviewBlock
3924
+ renderBlock: host.renderReviewBlock,
3925
+ locateFor: reviewLocator
3849
3926
  }), !review && state.repository && /* @__PURE__ */ React.createElement("div", { className: "twgit-content" }, /* @__PURE__ */ React.createElement("aside", null, state.browser && /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.browse), /* @__PURE__ */ React.createElement("p", null, /* @__PURE__ */ React.createElement("code", null, state.browser.path || "/")), /* @__PURE__ */ React.createElement("button", {
3850
3927
  disabled: state.busy || !state.browser.path,
3851
3928
  onClick: () => void browse(state.browser.path.split("/").slice(0, -1).join("/"))
@@ -3878,7 +3955,7 @@ function GitPanel({ document, host, controller }) {
3878
3955
  fileListOpen,
3879
3956
  setFileListOpen
3880
3957
  }) : fileList;
3881
- })(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot = state.snapshot) === null || _state$snapshot === void 0 ? void 0 : _state$snapshot.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3958
+ })(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot2 = state.snapshot) === null || _state$snapshot2 === void 0 ? void 0 : _state$snapshot2.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3882
3959
  disabled: state.busy,
3883
3960
  "aria-pressed": state.commit === commit.oid,
3884
3961
  title: commit.oid,
@@ -3887,6 +3964,7 @@ function GitPanel({ document, host, controller }) {
3887
3964
  changes: projectList,
3888
3965
  selection,
3889
3966
  locale,
3967
+ locateBlock: state.commit ? void 0 : locator,
3890
3968
  renderBlock: host.renderReviewBlock,
3891
3969
  onSelectFile: openFile
3892
3970
  }) : state.selected ? /* @__PURE__ */ React.createElement(FileDiffView, {
@@ -3894,9 +3972,24 @@ function GitPanel({ document, host, controller }) {
3894
3972
  diff: state.diff,
3895
3973
  labels,
3896
3974
  locale,
3975
+ locateBlock: fileLocator(state.selected),
3897
3976
  renderBlock: host.renderReviewBlock,
3898
3977
  loadAsset
3899
- }) : /* @__PURE__ */ React.createElement("p", null, labels.select)))));
3978
+ }) : /* @__PURE__ */ React.createElement("p", null, labels.select))));
3979
+ if (slot) return createPortal(/* @__PURE__ */ React.createElement("div", {
3980
+ className: "twgit-panel twgit-panel-tab",
3981
+ role: "region",
3982
+ "aria-label": labels.title
3983
+ }, content), slot.element);
3984
+ return /* @__PURE__ */ React.createElement("div", { className: "twgit-backdrop" }, /* @__PURE__ */ React.createElement("div", {
3985
+ className: "twgit-panel",
3986
+ ref: dialog,
3987
+ tabIndex: -1,
3988
+ role: "dialog",
3989
+ "aria-modal": "true",
3990
+ "aria-label": labels.title,
3991
+ onKeyDown: focusTrap
3992
+ }, content));
3900
3993
  }
3901
3994
  /** The GitHub relation in plain words, with the number of records where it helps. */
3902
3995
  function upstreamText({ upstream }, labels) {
@@ -3919,6 +4012,7 @@ function mountGitPanel({ vm, document, host }) {
3919
4012
  };
3920
4013
  controllers.set(vm, controller);
3921
4014
  render(/* @__PURE__ */ React.createElement(GitPanel, {
4015
+ vm,
3922
4016
  document,
3923
4017
  host,
3924
4018
  controller
@@ -3945,4 +4039,4 @@ function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection
3945
4039
  }
3946
4040
  }, labelsFor(locale).menu));
3947
4041
  }
3948
- export { GitPanelError, createReadOnlyFs, createScratchBlocksDiffRenderer, mountGitPanel, openRepository, renderGitMenu };
4042
+ export { GitPanelError, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, mountGitPanel, openRepository, renderGitMenu };
@@ -3,6 +3,7 @@ import type { ChangedFile, ProjectChangeList, ProjectElementChange, ProjectTarge
3
3
  import type { ProjectAssetLoader } from './project-assets.js';
4
4
  import type { ChangeGroup, FileChange, Repository, Snapshot } from './repository.js';
5
5
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
6
+ import type { ProjectBlockLocator } from './block-diff-location.js';
6
7
  declare const en: {
7
8
  title: string;
8
9
  stage: string;
@@ -68,7 +69,7 @@ export declare function findElement(list: ProjectChangeList, key: string | null)
68
69
  target: null;
69
70
  } | null;
70
71
  /** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
71
- export declare function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile }: {
72
+ export declare function ProjectElementView({ changes, selection, locale, renderBlock, onSelectFile, locateBlock }: {
72
73
  changes: ProjectChanges;
73
74
  selection: {
74
75
  element: ProjectElementChange;
@@ -77,5 +78,6 @@ export declare function ProjectElementView({ changes, selection, locale, renderB
77
78
  locale: string;
78
79
  renderBlock?: ProjectBlockRenderer;
79
80
  onSelectFile(file: ChangedFile): void;
81
+ locateBlock?: ProjectBlockLocator;
80
82
  }): React.JSX.Element;
81
83
  export {};
@@ -9,6 +9,8 @@ export { createScratchBlocksDiffLocator } from './block-diff-location.js';
9
9
  export type { ProjectBlockLocation, ProjectBlockLocator, ScratchBlockLocationWorkspace } from './block-diff-location.js';
10
10
  export type { BlockPart } from './project-source-patch.js';
11
11
  export { createProjectSourceDiff } from './project-source-patch.js';
12
+ export { compareProjectForReview } from './project-source-review-model.js';
13
+ export type { ScriptChangeKind, ScriptReview, ScriptReviewSide, ScriptSummaryItem, TargetReview } from './project-source-review-model.js';
12
14
  export { currentCostume } from './project-assets.js';
13
15
  export { assetFileName, organizeProjectChanges, projectPropertyLabel } from './project-change-model.js';
14
16
  export type { ChangedFile, OrganizeProjectChangesInput, ProjectChangeList, ProjectChangeStatus, ProjectElementChange, ProjectElementKind, ProjectTargetChanges } from './project-change-model.js';
@@ -1,4 +1,4 @@
1
- import { _ as compareProjectBlocks, a as gitHubRepositoryFromOrigin, b as readProjectSourceDiff, c as projectPropertyLabel, d as BlockPreview, f as createPreviewSync, i as createGitHubPullRequests, l as ProjectSourceComparison, m as currentCostume, n as createScratchBlocksDiffLocator, o as assetFileName, p as usePreviewSync, s as organizeProjectChanges, t as createScratchBlocksDiffRenderer, u as ProjectSourceReview, v as createProjectSourceDiff } from "./scratch-block-diff-renderer-DZR8ep-T.js";
1
+ import { a as gitHubRepositoryFromOrigin, c as assetFileName, d as compareProjectForReview, f as BlockPreview, h as currentCostume, i as createGitHubPullRequests, l as organizeProjectChanges, m as usePreviewSync, n as createScratchBlocksDiffLocator, o as ProjectSourceComparison, p as createPreviewSync, s as ProjectSourceReview, t as createScratchBlocksDiffRenderer, u as projectPropertyLabel, v as compareProjectBlocks, x as readProjectSourceDiff, y as createProjectSourceDiff } from "./scratch-block-diff-renderer-e_drVlHE.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 { ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, organizeProjectChanges, projectPropertyLabel };
172
+ export { ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, compareProjectForReview, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, organizeProjectChanges, projectPropertyLabel };
@@ -3,14 +3,49 @@ import type { ScriptBlockMark } from './scratch-block-diff-renderer.js';
3
3
  export interface ScriptReviewSide extends BlockSide {
4
4
  marks: ScriptBlockMark[];
5
5
  }
6
+ /**
7
+ * What happened to one script. `moved` means only positions changed, `reordered` only how blocks connect;
8
+ * neither changes what the script does.
9
+ */
10
+ export type ScriptChangeKind = 'added' | 'removed' | 'updated' | 'reorganized' | 'moved' | 'reordered';
11
+ /** One phrase of a script's summary, in the order shown. The view words it in the reader's language. */
12
+ export type ScriptSummaryItem = {
13
+ kind: 'hatReplaced';
14
+ before: string;
15
+ after: string;
16
+ } | {
17
+ kind: 'wrapped';
18
+ opcode: string;
19
+ } | {
20
+ kind: 'branchAdded';
21
+ opcode: string;
22
+ } | {
23
+ kind: 'valueChanged';
24
+ opcode: string;
25
+ name: string;
26
+ } | {
27
+ kind: 'blocksAdded';
28
+ count: number;
29
+ } | {
30
+ kind: 'blocksRemoved';
31
+ count: number;
32
+ } | {
33
+ kind: 'reordered';
34
+ } | {
35
+ kind: 'moved';
36
+ };
6
37
  export interface ScriptReview {
7
38
  id: string;
8
39
  before: ScriptReviewSide[];
9
40
  after: ScriptReviewSide[];
41
+ kind: ScriptChangeKind;
42
+ summary: ScriptSummaryItem[];
10
43
  }
11
44
  export interface TargetReview {
12
45
  name: string;
13
46
  isStage: boolean;
47
+ /** Added or removed as a whole, or changed. Added and removed targets list no properties. */
48
+ status: 'added' | 'removed' | 'updated';
14
49
  /** The stage or sprite in each version, absent where it does not exist. */
15
50
  before?: ProjectTarget;
16
51
  after?: ProjectTarget;
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
+ import type { ProjectBlockLocator } from './block-diff-location.js';
3
4
  import type { ProjectAssetLoader } from './project-assets.js';
4
5
  export type ProjectSourceReviewFile = {
5
6
  path: string;
@@ -10,6 +11,8 @@ export type ProjectSourceReviewFile = {
10
11
  patch: string;
11
12
  beforeSource?: string;
12
13
  loadAsset?: ProjectAssetLoader;
14
+ /** Offers "Highlight in Editor" on this file's after version, which the caller has matched with the open project. */
15
+ locateBlock?: ProjectBlockLocator;
13
16
  } | {
14
17
  status: 'added' | 'removed' | 'updated';
15
18
  });
@@ -32,6 +35,8 @@ export interface ProjectSourceComparisonProps {
32
35
  fallback?: React.ReactNode;
33
36
  /** Reads the images of both versions for thumbnails. Without it, stages and sprites are shown by name. */
34
37
  loadAsset?: ProjectAssetLoader;
38
+ /** Offers "Highlight in Editor" on the after version, which the caller has matched with the open project. */
39
+ locateBlock?: ProjectBlockLocator;
35
40
  /** Shows only the code of this stage or sprite, without the overview and the changes outside the code. */
36
41
  target?: {
37
42
  isStage: boolean;
@@ -39,5 +44,5 @@ export interface ProjectSourceComparisonProps {
39
44
  };
40
45
  }
41
46
  /** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
42
- export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, target }: ProjectSourceComparisonProps): React.JSX.Element;
47
+ export declare function ProjectSourceComparison({ before, after, renderBlock, locale, fallback, loadAsset, locateBlock, target }: ProjectSourceComparisonProps): React.JSX.Element;
43
48
  export declare function ProjectSourceReview({ title, description, verification, files, renderBlock, locale }: ProjectSourceReviewProps): React.JSX.Element;
@@ -1,8 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
3
3
  import type { Repository } from './repository.js';
4
- export declare function PullRequestReviewPanel({ repository, locale, renderBlock }: {
4
+ import type { ProjectBlockLocator } from './block-diff-location.js';
5
+ export declare function PullRequestReviewPanel({ repository, locale, renderBlock, locateFor }: {
5
6
  repository: Repository;
6
7
  locale: string;
7
8
  renderBlock: ProjectBlockRenderer;
9
+ /** Returns a locator for a project file whose PR head matches the open project, or `undefined`. */
10
+ locateFor?(path: string, headSha: string): ProjectBlockLocator | undefined;
8
11
  }): React.JSX.Element;
@@ -0,0 +1,6 @@
1
+ import type { ScriptSummaryItem } from './project-source-review-model.js';
2
+ export declare function hatName(opcode: string, locale: string): string | undefined;
3
+ /** One phrase per summary item, in the reader's language. */
4
+ export declare function summaryPhrase(item: ScriptSummaryItem, locale: string): string;
5
+ /** The colours of the previews, said once at the top of a review. */
6
+ export declare const legend: (locale: string) => readonly [readonly ["added", "緑の縁: 追加したブロック"], readonly ["removed", "赤の縁: 削除したブロック"], readonly ["updated", "紫の縁: 中身を変えたブロック"], readonly ["part", "オレンジの枠: 値を変えた欄"]] | readonly [readonly ["added", "Green outline: added block"], readonly ["removed", "Red outline: removed block"], readonly ["updated", "Purple outline: block with changed contents"], readonly ["part", "Orange frame: changed value"]];