@kubohiroya/turbowarp-plus-editor-git 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.ja.md +22 -6
  2. package/README.md +9 -1
  3. package/dist/block-diff-preview.d.ts +18 -2
  4. package/dist/file-diff-view.d.ts +3 -1
  5. package/dist/index.js +359 -15
  6. package/dist/{libesm-OWRWzmu_.js → project-asset-paths-KV715791.js} +23 -1
  7. package/dist/project-asset-paths.d.ts +12 -0
  8. package/dist/project-assets.d.ts +23 -0
  9. package/dist/project-change-list.d.ts +81 -0
  10. package/dist/project-change-model.d.ts +73 -0
  11. package/dist/project-source-diff.d.ts +7 -1
  12. package/dist/project-source-diff.js +10 -7
  13. package/dist/project-source-review-model.d.ts +4 -1
  14. package/dist/project-source-review.d.ts +11 -2
  15. package/dist/{repository-DplMQiuo.js → repository-VBp-UNWc.js} +134 -3
  16. package/dist/repository.d.ts +34 -0
  17. package/dist/repository.js +1 -1
  18. package/dist/{scratch-block-diff-renderer-CLYoOIB2.js → scratch-block-diff-renderer-DZR8ep-T.js} +708 -90
  19. package/dist/scratch-block-diff-renderer.d.ts +69 -6
  20. package/dist/style.css +48 -2
  21. package/package.json +7 -2
  22. package/src/block-diff-preview.tsx +89 -8
  23. package/src/file-diff-view.tsx +4 -3
  24. package/src/github-pull-requests.ts +73 -6
  25. package/src/index.tsx +42 -12
  26. package/src/project-asset-paths.ts +33 -0
  27. package/src/project-assets.ts +78 -0
  28. package/src/project-change-list.tsx +210 -0
  29. package/src/project-change-model.ts +212 -0
  30. package/src/project-source-diff.css +31 -2
  31. package/src/project-source-diff.tsx +17 -7
  32. package/src/project-source-review-model.ts +4 -1
  33. package/src/project-source-review.tsx +67 -27
  34. package/src/repository.ts +107 -2
  35. package/src/scratch-block-diff-renderer.ts +129 -11
  36. package/src/style.css +17 -0
package/README.ja.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  TurboWarp Plus Editor のファイルメニューに、読み取り専用の「ローカル Git」を追加します。リポジトリルートを一度選び、配下の sb3-toolchain v1 作品フォルダをパネルから開きます。作品の編集と保存には Editor の project-folder 接続を使います。
6
6
 
7
- ステージ済み・未ステージ・未追跡の変更、現在のブランチ、HEAD、ローカルブランチと直近50件の履歴を表示します。ファイルを選ぶとテキスト差分またはバイナリのサイズを確認できます。作品データ(`project.source.json`)は、ホストがブロックの描画関数を渡していればステージ・スプライト別、スクリプト別のブロックで比べ、「テキスト」に切り替えると行単位の差分を表示します。コミットを選ぶと第一親との差分を表示します。作品を選んでいるときは、変更一覧と差分をそのフォルダに絞ります。
7
+ ステージ済み・未ステージ・未追跡の変更、現在のブランチ、HEAD、ローカルブランチと直近50件の履歴を表示します。ファイルを選ぶとテキスト差分またはバイナリのサイズを確認できます。作品データ(`project.source.json`)は、ホストがブロックの描画関数を渡していればステージ・スプライト別、コード別のブロックで比べ、「テキスト」に切り替えると行単位の差分を表示します。ステージ・スプライトの見出しには、その版の現在のコスチューム(ステージは背景)のサムネイルを添えます。素材は比較する版から読みます(ステージ済みは HEAD とインデックス、未ステージはインデックスと作業ツリー、未追跡は作業ツリー、コミットは第一親とそのコミット)。素材のフォルダはその版の `sb3-source.json` の `assetsDirectory`(既定は `assets`)です。読めない版では名前だけを示します。コミットを選ぶと第一親との差分を表示します。作品を選んでいるときは、変更一覧と差分をそのフォルダに絞ります。
8
8
 
9
9
  パネルを開いている間は、2 秒ごと、ウィンドウにフォーカスが戻ったとき、対応環境では `FileSystemObserver` の通知でリポジトリを確認します。HEAD、インデックス、`packed-refs`、`config`、現在のブランチの参照、選択中の作品フォルダのファイル(作品を選んでいない場合は作業ツリー全体、5,000 件まで)のサイズと更新時刻を比べます。変更があり、差分やコミットを開いていなければ読み直します。差分・コミット・プルリクエストのレビューを開いている間は表示を保ち、**再取得** を案内します。Git がメタデータをロックしている間は確認を待ちます。**作品を再読込** では、Editor の未保存編集を確認してから同じ作品を開き直します。Git の閲覧には読み取り専用のアダプターを使い、インデックスの更新を無効にします。読み取り中にファイルや Git の状態が変わった場合は、外部操作の完了後に再取得します。
10
10
 
@@ -63,17 +63,23 @@ const diffFile = new Blob([patch], {type: 'text/x-diff'});
63
63
 
64
64
  `getBlockly` は読み込み済みの Scratch Blocks、`deserializeBlocks` と `VMBlocks` はホストの Scratch VM が提供するものを使います。表示用の VM ブロックコンテナーとワークスペースは差分ごとに新しく作ります。ホストに定義がない拡張ブロックやプレビュー非対応の mutation には描画不可と表示します。
65
65
 
66
- ブロックはステージまたはスプライト名とブロック ID で照合します。変更されたブロックとその入力を読み取り専用で並べ、追加・削除・入力値などの注釈を付けます。「元のスクリプト内で強調」を選ぶと、つながったスクリプトの中で対象を確認できます。オレンジの枠は変更された入力欄やフィールドを示します。画像・音声など、コード以外に変更があればその旨を表示します。切り戻す場合は、このコンポーネントの呼び出しを外します。
66
+ ブロックはステージまたはスプライト名とブロック ID で照合します。変更されたブロックとその入力を読み取り専用で並べ、追加・削除・入力値などの注釈を付けます。「元のコード内で強調」を選ぶと、つながったコードの中で対象を確認できます。オレンジの枠は変更された入力欄やフィールドを示します。画像・音声など、コード以外に変更があればその旨を表示します。切り戻す場合は、このコンポーネントの呼び出しを外します。
67
67
 
68
68
  ### 編集画面のブロックと照合する
69
69
 
70
- `locateBlock` に `createScratchBlocksDiffLocator(resolveWorkspace)` を渡すと、各版のプレビューに「編集画面で強調」が出ます。`resolveWorkspace` は指定された `version`、対象のステージ・スプライト、ブロック ID に対応するワークスペースを返します。ホストは作品と版を照合し、必要なら対象を切り替えてブロックが表示されるまで待ちます。一致する編集画面がなければ `null` を返します。削除済みのブロックは変更前のスクリプトを展開して確認できます。
70
+ `locateBlock` に `createScratchBlocksDiffLocator(resolveWorkspace)` を渡すと、各版のプレビューに「編集画面で強調」が出ます。`resolveWorkspace` は指定された `version`、対象のステージ・スプライト、ブロック ID に対応するワークスペースを返します。ホストは作品と版を照合し、必要なら対象を切り替えてブロックが表示されるまで待ちます。一致する編集画面がなければ `null` を返します。削除済みのブロックは変更前のコードを展開して確認できます。
71
71
 
72
72
  ロケーターは一致したブロックを中央に移動し、差分ビューと同じ入力欄やフィールドを強調します。別の差分の選択・差分ファイルの切り替え・ビューの終了で強調を解除します。
73
73
 
74
+ ### 表示枠
75
+
76
+ 変更前・変更後の各枠は読み取り専用の作業領域です。Editor のコードエリアと同じ倍率でブロックを描き、枠の高さをブロックに合わせます(160〜420 px。枠の右下をドラッグすると高くできます)。ドラッグとスクロールバーで動かし、拡大縮小ボタンと Ctrl+ホイール(トラックパッドのピンチ)で拡大縮小します。ホイールだけの操作は、レビュー全体のスクロールに使います。同じ比較の枠は拡大率・高さ・スクロール位置を連動させ、「変更前と変更後を連動して動かす」で切り離せます。
77
+
78
+ `createScratchBlocksDiffRenderer` の `attachNavigation(workspace, blockly, watermark)` に、ホストのコードエリアと同じ操作部品を付ける関数を渡せます。TurboWarp Plus Editor は右下にコスチュームのサムネイル、ミニマップ、拡大縮小ボタンの列を並べます。`watermark` はその版の現在のコスチュームを表示する要素で、素材を読む `loadAsset` があるときに渡します。`attachNavigation` がなければ Scratch Blocks の拡大縮小ボタンを使います。独自の描画関数は `{dispose, view}` を返すと、`createPreviewSync` による連動に加われます。
79
+
74
80
  ## PR 単位でレビューする
75
81
 
76
- 同じエントリの `ProjectSourceReview` は、作品ファイルを複数含む変更セットをまとめてレビューするためのビューです。`title`・`description`・`verification` には PR 作者が記した目的と確認観点を渡します。`files` に作品ファイルの `{path, patch, beforeSource?}` や、関連ファイルの `{path, status, summary?}` を渡します。ブロックを描画する `renderBlock` は個別差分ビューと共通です。
82
+ 同じエントリの `ProjectSourceReview` は、作品ファイルを複数含む変更セットをまとめてレビューするためのビューです。`title`・`description`・`verification` には PR 作者が記した目的と確認観点を渡します。`files` に作品ファイルの `{path, patch, beforeSource?, loadAsset?}` や、関連ファイルの `{path, status, summary?}` を渡します。ブロックを描画する `renderBlock` は個別差分ビューと共通です。
77
83
 
78
84
  ```tsx
79
85
  <ProjectSourceReview
@@ -89,13 +95,15 @@ const diffFile = new Blob([patch], {type: 'text/x-diff'});
89
95
  />
90
96
  ```
91
97
 
92
- 作品ファイルはステージ・スプライトごとに整理し、変更のあったスクリプトだけを比較項目にします。分割・結合したスクリプトも、両版に共通するブロック ID を使って一項目にまとめます。展開すると前後のスクリプトをブロックで表示し、編集箇所を枠で示します。変数、コスチュームなどの変更は別に示します。関連ファイルの要約はホストから渡した情報を表示します。
98
+ 作品ファイルはステージ・スプライトごとに整理し、変更のあったコードだけを比較項目にします。ひとかたまりのブロックは Editor のタブ名と同じく「コード」と呼び、見出しには「コードの変更 · N か所」と示します。各項目は帽子ブロックの名前で呼びます。分割・結合したコードも、両版に共通するブロック ID を使って一項目にまとめます。展開すると前後のコードをブロックで表示し、編集箇所を枠で示します。変数、コスチュームなどの変更は別に示します。関連ファイルの要約はホストから渡した情報を表示します。
99
+
100
+ `loadAsset` は `({version, md5ext, signal}) => Promise<Blob | null>` の形で、`version`(`before`/`after`)の作品フォルダの素材を読みます。渡すと、ステージ・スプライトの見出しにその版の現在のコスチューム(ステージは背景)のサムネイルを添えます。変更前後でコスチュームが違えば「変更前 → 変更後」と並べます。関数がないとき、または画像を読めないときは名前だけを示します。`createGitHubPullRequests` が返す作品ファイルには、変更前(マージベース)と変更後(head)の素材を raw.githubusercontent.com から読む `loadAsset` が付きます。この読み込みは GitHub API の利用上限を消費せず、1 ファイル 10 MiB までです。
93
101
 
94
102
  確認済みの印は表示中の比較データに紐づくローカル状態です。PR 本文・ファイル差分の取得と GitHub へのレビュー送信は、呼び出し側が担当します。
95
103
 
96
104
  ### 2 つの版を直接比べる
97
105
 
98
- `ProjectSourceComparison` は、同じ比較を 1 つの作品ファイルについて表示します。変更前と変更後の `project.source.json` の文字列を `before`・`after` に渡します。ファイルがない側は `null` とし、すべてのスクリプトを追加または削除として示します。ローカル Git パネルは作業ツリーと履歴の差分にこのビューを使います。
106
+ `ProjectSourceComparison` は、同じ比較を 1 つの作品ファイルについて表示します。変更前と変更後の `project.source.json` の文字列を `before`・`after` に渡します。ファイルがない側は `null` とし、すべてのコードを追加または削除として示します。`loadAsset` は `ProjectSourceReview` の作品ファイルと同じで、見出しのサムネイルに使います。ローカル Git パネルは作業ツリーと履歴の差分にこのビューを使います。
99
107
 
100
108
  ```tsx
101
109
  <ProjectSourceComparison before={beforeText} after={afterText} renderBlock={renderBlock} locale="ja"
@@ -103,3 +111,11 @@ const diffFile = new Blob([patch], {type: 'text/x-diff'});
103
111
  ```
104
112
 
105
113
  どちらかの版を作品として読めないときは理由を示し、`fallback` を表示します。`fallback` がなければエラーだけを示します。
114
+
115
+ ## 公開
116
+
117
+ npm への公開は `.github/workflows/release.yml` が行う。`package.json` の版と同じ `v<版>` のタグを
118
+ `main` のコミットに push すると、テストを通したうえで npm に入れるアーカイブを作り、GitHub Release に
119
+ 添付してから npm に公開し、`latest` と `next` をその版に向ける。認証はトークンではなく npm の
120
+ Trusted Publisher(OIDC)で、provenance を付けて公開する。npm 側の設定にはこのワークフローの
121
+ ファイル名をそのまま登録するため、改名するときは npm の設定も合わせる。
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Local Git adds a read-only panel to the TurboWarp Plus Editor File menu. Select the repository root once, then browse to an sb3-toolchain v1 project folder inside it. Project editing and saving are supplied by the Editor's project-folder integration.
6
6
 
7
- The panel separates staged, unstaged, and untracked changes. It shows the current branch, HEAD, local branches, and the latest 50 commits. Select a file to inspect its text diff or binary size. When the host supplies a block renderer, project data (`project.source.json`) is compared as blocks by stage, sprite and script, and **Text** switches to the line diff. Select a commit to compare its files with its first parent. The selected project limits the file list and diffs to that folder.
7
+ The panel separates staged, unstaged, and untracked changes. It shows the current branch, HEAD, local branches, and the latest 50 commits. Select a file to inspect its text diff or binary size. When the host supplies a block renderer, project data (`project.source.json`) is compared as blocks by stage, sprite and code, and **Text** switches to the line diff. Each stage and sprite heading shows a thumbnail of its current costume (the backdrop for the stage) in each version. Assets are read from the compared versions: HEAD and the index for staged changes, the index and the working tree for unstaged changes, the working tree for untracked files, and the first parent and the commit for history. The assets folder is the `assetsDirectory` of that version's `sb3-source.json` (`assets` by default). When an asset cannot be read, only the name is shown. Pull request reviews read the same thumbnails from raw.githubusercontent.com at the merge base and head, without using the GitHub API rate limit. Select a commit to compare its files with its first parent. The selected project limits the file list and diffs to that folder.
8
8
 
9
9
  While the panel is open, it checks the repository every two seconds, when the window regains focus, and on `FileSystemObserver` notifications where available. It compares the sizes and modification times of HEAD, the index, `packed-refs`, `config`, the current branch ref, and the files in the selected project folder (the whole work tree when no project is selected, up to 5,000 files). When something changed and no diff or commit is open, the panel reads the repository again. While a diff, a commit or a pull request review is open, it keeps the view and asks you to **Refresh** instead. Checks wait while Git holds a lock on its metadata. **Reload Project** opens the selected project again using the Editor's unsaved-edit confirmation. Git inspection uses a read-only adapter and isomorphic-git with index refresh disabled. If the repository changes during a read, refresh after the other operation completes.
10
10
 
@@ -34,3 +34,11 @@ Each receive-pack POST carries a new UUID v4 in `Git-Push-Id`. A browser replay
34
34
  `npm test` runs the local Git tests and a headless Smart HTTP fixture on IPv4 loopback using `git-http-backend`. The fixture uses disposable repositories and test credentials. [Proxy, authentication and CORS contract](https://github.com/kubohiroya/turbowarp-plus-editor/issues/315).
35
35
 
36
36
  When the Editor host supplies connection settings, the Local Git panel offers fetch, fast-forward pull and push for the current branch and its existing GitHub HTTPS `origin`. Authorize push when connecting to enable the explicit push action. Fetch updates Git metadata. Pull requires a stopped project, a clean worktree, and regular non-executable files. Pull stages the changes, rechecks the local state, and then applies them. Reopen the project from the panel after a successful pull.
37
+
38
+ ## Release
39
+
40
+ `.github/workflows/release.yml` publishes to npm. Push a `v<version>` tag that names the
41
+ `package.json` version on a `main` commit: the workflow runs the tests, packs the archive, attaches it
42
+ to the GitHub Release, publishes it to npm and points `latest` and `next` at it. It authenticates with
43
+ npm Trusted Publishing (OIDC), not a token, and publishes with provenance. The npm settings name this
44
+ workflow file, so rename both together.
@@ -1,9 +1,21 @@
1
1
  import React from 'react';
2
2
  import type { BlockPart, BlockSide } from './project-source-patch.js';
3
- import type { ProjectBlockRenderer, ScriptBlockMark } from './scratch-block-diff-renderer.js';
3
+ import type { BlockPreviewView, ProjectBlockRenderer, ScriptBlockMark } from './scratch-block-diff-renderer.js';
4
+ import type { ProjectAssetLoader, ProjectVersion } from './project-assets.js';
4
5
  export type LocationStatus = 'locating' | 'located' | 'missingLocation';
5
6
  type PreviewLabels = Record<'absent' | 'loading' | 'unavailable', string> & Partial<Record<LocationStatus | 'locate', string>>;
6
- export declare function BlockPreview({ side, title, parts, marks, renderBlock, withContext, labels, onLocate, locationStatus }: {
7
+ /** Keeps the previews of one comparison at the same zoom, height and scroll position while they are linked. */
8
+ export interface PreviewSync {
9
+ add(view: BlockPreviewView): () => void;
10
+ setLinked(linked: boolean): void;
11
+ }
12
+ export declare function createPreviewSync(): PreviewSync;
13
+ /** One sync group per comparison, with the checkbox that links or unlinks its previews. */
14
+ export declare function usePreviewSync(locale: string): {
15
+ sync: PreviewSync;
16
+ toggle: React.JSX.Element;
17
+ };
18
+ export declare function BlockPreview({ side, title, parts, marks, renderBlock, withContext, labels, onLocate, locationStatus, version, loadAsset, sync }: {
7
19
  side?: BlockSide;
8
20
  title: string;
9
21
  parts?: readonly BlockPart[];
@@ -13,5 +25,9 @@ export declare function BlockPreview({ side, title, parts, marks, renderBlock, w
13
25
  labels: PreviewLabels;
14
26
  onLocate?: () => void;
15
27
  locationStatus?: LocationStatus;
28
+ /** Which version this preview shows, for its costume thumbnail. */
29
+ version?: ProjectVersion;
30
+ loadAsset?: ProjectAssetLoader;
31
+ sync?: PreviewSync;
16
32
  }): React.JSX.Element;
17
33
  export {};
@@ -2,15 +2,17 @@ import React from 'react';
2
2
  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
+ import type { ProjectAssetLoader } from './project-assets.js';
5
6
  /** Project sources are the files the panel opens as projects. */
6
7
  export declare const isProjectSource: (path: string) => boolean;
7
8
  /**
8
9
  * Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
9
10
  */
10
- export declare function FileDiffView({ change, diff, labels, locale, renderBlock }: {
11
+ export declare function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }: {
11
12
  change: FileChange;
12
13
  diff: FileDiff | null;
13
14
  labels: ReturnType<typeof labelsFor>;
14
15
  locale: string;
15
16
  renderBlock?: ProjectBlockRenderer;
17
+ loadAsset?: ProjectAssetLoader;
16
18
  }): React.JSX.Element;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { a as gitHubRepositoryFromOrigin, i as createGitHubPullRequests, o as ProjectSourceComparison, r as PullRequestError, s as ProjectSourceReview, t as createScratchBlocksDiffRenderer } from "./scratch-block-diff-renderer-CLYoOIB2.js";
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";
2
2
  import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
3
- import { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-DplMQiuo.js";
3
+ import { n as GitPanelError, r as createReadOnlyFs, t as openRepository } from "./repository-VBp-UNWc.js";
4
4
  import * as m from "react";
5
5
  import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
6
  import { render, unmountComponentAtNode } from "react-dom";
@@ -2669,12 +2669,12 @@ diffMatchPatch.DIFF_EQUAL;
2669
2669
  diffMatchPatch.DIFF_DELETE;
2670
2670
  diffMatchPatch.DIFF_INSERT;
2671
2671
  // src/file-diff-view.tsx
2672
- var en$2 = {
2672
+ var en$3 = {
2673
2673
  view: "Show changes as",
2674
2674
  blocks: "Blocks",
2675
2675
  text: "Text"
2676
2676
  };
2677
- var ja$2 = {
2677
+ var ja$3 = {
2678
2678
  view: "変更の表示",
2679
2679
  blocks: "ブロック",
2680
2680
  text: "テキスト"
@@ -2684,10 +2684,10 @@ 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 }) {
2687
+ function FileDiffView({ change, diff, labels, locale, renderBlock, loadAsset }) {
2688
2688
  var _change$oldMode, _change$newMode;
2689
2689
  const [view, setView] = useState("blocks");
2690
- const names = locale.startsWith("ja") ? ja$2 : en$2;
2690
+ const names = locale.startsWith("ja") ? ja$3 : en$3;
2691
2691
  const parsed = (diff === null || diff === void 0 ? void 0 : diff.patch) ? parseDiff(diff.patch)[0] : null;
2692
2692
  const text = /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("p", null, labels.mode, " · ", ((_change$oldMode = change.oldMode) === null || _change$oldMode === void 0 ? void 0 : _change$oldMode.toString(8)) || "—", " → ", ((_change$newMode = change.newMode) === null || _change$newMode === void 0 ? void 0 : _change$newMode.toString(8)) || "—"), diff && /* @__PURE__ */ React.createElement("p", null, diff.beforeSize, " → ", diff.afterSize, " ", labels.bytes), (diff === null || diff === void 0 ? void 0 : diff.binary) && /* @__PURE__ */ React.createElement("p", null, labels.binary), (diff === null || diff === void 0 ? void 0 : diff.tooLarge) && /* @__PURE__ */ React.createElement("p", null, labels.large), parsed && /* @__PURE__ */ React.createElement(index, {
2693
2693
  viewType: "unified",
@@ -2712,10 +2712,311 @@ function FileDiffView({ change, diff, labels, locale, renderBlock }) {
2712
2712
  renderBlock,
2713
2713
  locale,
2714
2714
  fallback: text,
2715
+ loadAsset,
2715
2716
  before: change.before === null ? null : sources.before,
2716
2717
  after: change.after === null ? null : sources.after
2717
2718
  }) : text);
2718
2719
  }
2720
+ // src/project-change-list.tsx
2721
+ var en$2 = {
2722
+ title: "Changes by project element",
2723
+ stage: "Stage",
2724
+ code: (count) => `Code · ${count} ${count === 1 ? "change" : "changes"}`,
2725
+ costumes: "Costume",
2726
+ backdrops: "Backdrop",
2727
+ sounds: "Sound",
2728
+ data: "Variables and messages",
2729
+ settings: "Other settings",
2730
+ project: "Whole project",
2731
+ other: "Files outside the project",
2732
+ added: "Added",
2733
+ removed: "Removed",
2734
+ updated: "Changed",
2735
+ files: "Files and Git groups",
2736
+ before: "Before",
2737
+ after: "After",
2738
+ absent: "Not in this version",
2739
+ loading: "Reading the project changes…",
2740
+ unreadable: "The project data could not be read, so the changes are listed by file.",
2741
+ fileList: "List by file",
2742
+ property: "Item",
2743
+ format: "Format",
2744
+ rate: "Sample rate",
2745
+ samples: "Samples",
2746
+ file: "File name",
2747
+ noImage: "No image",
2748
+ groups: {
2749
+ staged: "Staged",
2750
+ unstaged: "Unstaged",
2751
+ untracked: "Untracked",
2752
+ commit: "Commit"
2753
+ }
2754
+ };
2755
+ var ja$2 = {
2756
+ title: "作品の要素ごとの変更",
2757
+ stage: "ステージ",
2758
+ code: (count) => `コード · ${count} か所`,
2759
+ costumes: "コスチューム",
2760
+ backdrops: "背景",
2761
+ sounds: "音",
2762
+ data: "変数・リスト・メッセージ",
2763
+ settings: "そのほかの設定",
2764
+ project: "作品全体",
2765
+ other: "作品に属さないファイル",
2766
+ added: "追加",
2767
+ removed: "削除",
2768
+ updated: "変更",
2769
+ files: "ファイルと Git の区分",
2770
+ before: "変更前",
2771
+ after: "変更後",
2772
+ absent: "この版にはありません",
2773
+ loading: "作品の変更を読み込み中…",
2774
+ unreadable: "作品データを読み込めないため、ファイル単位で一覧にします。",
2775
+ fileList: "ファイル単位の一覧",
2776
+ property: "項目",
2777
+ format: "形式",
2778
+ rate: "サンプリング周波数",
2779
+ samples: "サンプル数",
2780
+ file: "ファイル名",
2781
+ noImage: "画像なし",
2782
+ groups: {
2783
+ staged: "ステージ済み",
2784
+ unstaged: "未ステージ",
2785
+ untracked: "未追跡",
2786
+ commit: "コミット"
2787
+ }
2788
+ };
2789
+ var projectChangeLabels = (locale) => locale.startsWith("ja") ? ja$2 : en$2;
2790
+ /**
2791
+ * Reads the selected project folder in the last commit and the working tree (or a commit and its parent) and arranges
2792
+ * the changed files by stage, sprite and element. `undefined` while there is no project; `'loading'` and `'unreadable'`
2793
+ * tell the panel to keep the list by file.
2794
+ */
2795
+ function useProjectChanges(repository, snapshot, project, commit, changes) {
2796
+ const [result, setResult] = useState();
2797
+ const active = !!repository && !!snapshot && project !== null && changes.length > 0;
2798
+ useEffect(() => {
2799
+ if (!repository || !snapshot || project === null || !changes.length) return;
2800
+ let current = true;
2801
+ const comparison = commit ? {
2802
+ kind: "commit",
2803
+ commit
2804
+ } : { kind: "worktree" };
2805
+ repository.readProject(snapshot, project, comparison).then((read) => {
2806
+ if (!current) return;
2807
+ let list;
2808
+ try {
2809
+ list = organizeProjectChanges({
2810
+ folder: project,
2811
+ before: read.before === null ? null : parseProjectSource(read.before),
2812
+ after: read.after === null ? null : parseProjectSource(read.after),
2813
+ assetsDirectory: read.assetsDirectory,
2814
+ files: changes.map(({ path, group }) => ({
2815
+ path,
2816
+ group
2817
+ }))
2818
+ });
2819
+ } catch (_unused) {
2820
+ setResult({
2821
+ changes,
2822
+ value: "unreadable"
2823
+ });
2824
+ return;
2825
+ }
2826
+ setResult({
2827
+ changes,
2828
+ value: {
2829
+ list,
2830
+ sources: {
2831
+ before: read.before,
2832
+ after: read.after
2833
+ },
2834
+ loadAsset: repository.projectAssetLoader(snapshot, project, comparison)
2835
+ }
2836
+ });
2837
+ }, () => {
2838
+ if (current) setResult({
2839
+ changes,
2840
+ value: "unreadable"
2841
+ });
2842
+ });
2843
+ return () => {
2844
+ current = false;
2845
+ };
2846
+ }, [
2847
+ repository,
2848
+ snapshot,
2849
+ project,
2850
+ commit,
2851
+ changes
2852
+ ]);
2853
+ if (!active) return void 0;
2854
+ if ((result === null || result === void 0 ? void 0 : result.changes) === changes || result && typeof result.value === "object") return result.value;
2855
+ return "loading";
2856
+ }
2857
+ function TargetHeading({ target, labels, loadAsset }) {
2858
+ const version = target.after ? "after" : "before";
2859
+ const image = useCostumeImage(target[version], version, loadAsset);
2860
+ return /* @__PURE__ */ React.createElement("h4", { className: "twgit-element-target" }, image ? /* @__PURE__ */ React.createElement("img", {
2861
+ src: image,
2862
+ alt: ""
2863
+ }) : /* @__PURE__ */ React.createElement("span", { className: "twgit-element-thumbnail" }), /* @__PURE__ */ React.createElement("span", null, target.isStage ? labels.stage : target.name), target.status !== "updated" && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-status" }, labels[target.status]));
2864
+ }
2865
+ function elementTitle(element, target, labels, locale) {
2866
+ switch (element.kind) {
2867
+ case "code":
2868
+ var _element$scripts;
2869
+ return labels.code((_element$scripts = element.scripts) !== null && _element$scripts !== void 0 ? _element$scripts : 0);
2870
+ case "costumes":
2871
+ var _element$name;
2872
+ return `${(target === null || target === void 0 ? void 0 : target.isStage) ? labels.backdrops : labels.costumes} · ${(_element$name = element.name) !== null && _element$name !== void 0 ? _element$name : ""}`;
2873
+ case "sounds":
2874
+ var _element$name2;
2875
+ return `${labels.sounds} · ${(_element$name2 = element.name) !== null && _element$name2 !== void 0 ? _element$name2 : ""}`;
2876
+ default: {
2877
+ var _element$properties;
2878
+ const names = ((_element$properties = element.properties) !== null && _element$properties !== void 0 ? _element$properties : []).map((key) => projectPropertyLabel(key, locale)).join("・");
2879
+ return target ? `${element.kind === "data" ? labels.data : labels.settings} · ${names}` : names;
2880
+ }
2881
+ }
2882
+ }
2883
+ function ElementRow({ element, target, labels, locale, loadAsset, selected, busy, onSelect }) {
2884
+ var _element$md5ext, _element$md5ext2;
2885
+ const version = ((_element$md5ext = element.md5ext) === null || _element$md5ext === void 0 ? void 0 : _element$md5ext.after) ? "after" : "before";
2886
+ const image = useAssetImage(element.kind === "costumes" ? (_element$md5ext2 = element.md5ext) === null || _element$md5ext2 === void 0 ? void 0 : _element$md5ext2[version] : void 0, version, loadAsset);
2887
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("button", {
2888
+ className: "twgit-file twgit-element",
2889
+ "data-git-element": element.kind,
2890
+ disabled: busy,
2891
+ "aria-pressed": selected,
2892
+ onClick: onSelect
2893
+ }, element.kind === "costumes" && (image ? /* @__PURE__ */ React.createElement("img", {
2894
+ src: image,
2895
+ alt: ""
2896
+ }) : /* @__PURE__ */ React.createElement("span", { className: "twgit-element-thumbnail" })), /* @__PURE__ */ React.createElement("span", { className: "twgit-element-name" }, elementTitle(element, target, labels, locale)), element.status !== "updated" && /* @__PURE__ */ React.createElement("small", { className: "twgit-review-status" }, labels[element.status])));
2897
+ }
2898
+ /** The change list arranged by the project's stage, sprites and elements, with files outside the project after them. */
2899
+ function ProjectChangeListView({ changes, selected, locale, busy, onSelect, onSelectFile, fileList, fileListOpen, setFileListOpen }) {
2900
+ const labels = projectChangeLabels(locale);
2901
+ const { list, loadAsset } = changes;
2902
+ return /* @__PURE__ */ React.createElement("section", {
2903
+ className: "twgit-element-list",
2904
+ "aria-label": labels.title
2905
+ }, list.targets.map((target) => /* @__PURE__ */ React.createElement("section", { key: target.key }, /* @__PURE__ */ React.createElement(TargetHeading, {
2906
+ target,
2907
+ labels,
2908
+ loadAsset
2909
+ }), /* @__PURE__ */ React.createElement("ul", null, target.elements.map((element) => /* @__PURE__ */ React.createElement(ElementRow, {
2910
+ key: element.key,
2911
+ element,
2912
+ target,
2913
+ labels,
2914
+ locale,
2915
+ loadAsset,
2916
+ selected: selected === element.key,
2917
+ busy,
2918
+ onSelect: () => onSelect(element.key)
2919
+ }))))), list.project.length > 0 && /* @__PURE__ */ React.createElement("section", null, /* @__PURE__ */ React.createElement("h4", { className: "twgit-element-target" }, /* @__PURE__ */ React.createElement("span", null, labels.project)), /* @__PURE__ */ React.createElement("ul", null, list.project.map((element) => /* @__PURE__ */ React.createElement(ElementRow, {
2920
+ key: element.key,
2921
+ element,
2922
+ target: null,
2923
+ labels,
2924
+ locale,
2925
+ loadAsset,
2926
+ selected: selected === element.key,
2927
+ busy,
2928
+ onSelect: () => onSelect(element.key)
2929
+ })))), list.other.length > 0 && /* @__PURE__ */ React.createElement("section", null, /* @__PURE__ */ React.createElement("h4", { className: "twgit-element-target" }, /* @__PURE__ */ React.createElement("span", null, labels.other)), /* @__PURE__ */ React.createElement("ul", null, list.other.map((file) => /* @__PURE__ */ React.createElement("li", { key: file.group + ":" + file.path }, /* @__PURE__ */ React.createElement("button", {
2930
+ className: "twgit-file",
2931
+ "data-git-other": file.group,
2932
+ disabled: busy,
2933
+ onClick: () => onSelectFile(file)
2934
+ }, file.path, " ", /* @__PURE__ */ React.createElement("small", null, labels.groups[file.group])))))), /* @__PURE__ */ React.createElement("details", {
2935
+ className: "twgit-file-list",
2936
+ open: fileListOpen,
2937
+ onToggle: (event) => setFileListOpen(event.currentTarget.open)
2938
+ }, /* @__PURE__ */ React.createElement("summary", null, labels.fileList), fileList));
2939
+ }
2940
+ function findElement(list, key) {
2941
+ if (key === null) return null;
2942
+ for (const target of list.targets) {
2943
+ const element = target.elements.find((item) => item.key === key);
2944
+ if (element) return {
2945
+ element,
2946
+ target
2947
+ };
2948
+ }
2949
+ const element = list.project.find((item) => item.key === key);
2950
+ return element ? {
2951
+ element,
2952
+ target: null
2953
+ } : null;
2954
+ }
2955
+ function Picture({ md5ext, version, loadAsset, labels, title }) {
2956
+ const image = useAssetImage(md5ext, version, loadAsset);
2957
+ return /* @__PURE__ */ React.createElement("figure", { className: "twgit-element-picture" }, /* @__PURE__ */ React.createElement("figcaption", null, title), !md5ext ? /* @__PURE__ */ React.createElement("p", null, labels.absent) : image ? /* @__PURE__ */ React.createElement("img", {
2958
+ src: image,
2959
+ alt: title
2960
+ }) : /* @__PURE__ */ React.createElement("p", null, labels.noImage));
2961
+ }
2962
+ var show = (value) => {
2963
+ if (value === void 0) return "—";
2964
+ const text = typeof value === "string" ? value : JSON.stringify(value);
2965
+ return text.length > 400 ? text.slice(0, 400) + "…" : text;
2966
+ };
2967
+ /** 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
+ var _element$properties2;
2970
+ const labels = projectChangeLabels(locale);
2971
+ const { element, target } = selection;
2972
+ const record = (version) => {
2973
+ if (element.kind === "costumes" || element.kind === "sounds") return element[version];
2974
+ if (!target) {
2975
+ const source = changes.sources[version];
2976
+ try {
2977
+ return source === null ? void 0 : parseProjectSource(source);
2978
+ } catch (_unused2) {
2979
+ return;
2980
+ }
2981
+ }
2982
+ return target[version];
2983
+ };
2984
+ const rows = element.kind === "sounds" ? [
2985
+ ["name", "name"],
2986
+ ["dataFormat", labels.format],
2987
+ ["rate", labels.rate],
2988
+ ["sampleCount", labels.samples],
2989
+ ["md5ext", labels.file]
2990
+ ] : ((_element$properties2 = element.properties) !== null && _element$properties2 !== void 0 ? _element$properties2 : []).map((key) => [key, projectPropertyLabel(key, locale)]);
2991
+ return /* @__PURE__ */ React.createElement("section", {
2992
+ className: "twgit-element-view",
2993
+ "aria-label": elementTitle(element, target, labels, locale)
2994
+ }, /* @__PURE__ */ React.createElement("h3", null, target && /* @__PURE__ */ React.createElement("span", null, target.isStage ? labels.stage : target.name, " · "), elementTitle(element, target, labels, locale)), element.kind === "code" && target && renderBlock && /* @__PURE__ */ React.createElement(ProjectSourceComparison, {
2995
+ before: changes.sources.before,
2996
+ after: changes.sources.after,
2997
+ renderBlock,
2998
+ locale,
2999
+ loadAsset: changes.loadAsset,
3000
+ target: {
3001
+ isStage: target.isStage,
3002
+ name: target.name
3003
+ }
3004
+ }), element.kind === "costumes" && /* @__PURE__ */ React.createElement("div", { className: "twgit-element-pictures" }, ["before", "after"].map((version) => {
3005
+ var _element$md5ext3, _element$version;
3006
+ return /* @__PURE__ */ React.createElement(Picture, {
3007
+ key: version,
3008
+ md5ext: (_element$md5ext3 = element.md5ext) === null || _element$md5ext3 === void 0 ? void 0 : _element$md5ext3[version],
3009
+ version,
3010
+ loadAsset: changes.loadAsset,
3011
+ labels,
3012
+ title: ((_element$version = element[version]) === null || _element$version === void 0 ? void 0 : _element$version.name) === void 0 ? labels[version] : `${labels[version]} · ${String(element[version].name)}`
3013
+ });
3014
+ })), element.kind !== "code" && element.kind !== "costumes" && /* @__PURE__ */ React.createElement("table", { className: "twgit-element-values" }, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("th", null, labels.property), /* @__PURE__ */ React.createElement("th", null, labels.before), /* @__PURE__ */ React.createElement("th", null, labels.after))), /* @__PURE__ */ React.createElement("tbody", null, rows.map(([key, name]) => /* @__PURE__ */ React.createElement("tr", { key }, /* @__PURE__ */ React.createElement("th", { scope: "row" }, name), /* @__PURE__ */ React.createElement("td", null, record("before") ? show(record("before")[key]) : labels.absent), /* @__PURE__ */ React.createElement("td", null, record("after") ? show(record("after")[key]) : labels.absent))))), /* @__PURE__ */ React.createElement("details", { className: "twgit-element-files" }, /* @__PURE__ */ React.createElement("summary", null, labels.files), /* @__PURE__ */ React.createElement("ul", null, element.files.map((file) => /* @__PURE__ */ React.createElement("li", { key: file.group + ":" + file.path }, /* @__PURE__ */ React.createElement("button", {
3015
+ className: "twgit-file",
3016
+ type: "button",
3017
+ onClick: () => onSelectFile(file)
3018
+ }, file.path, " ", /* @__PURE__ */ React.createElement("small", null, labels.groups[file.group])))))));
3019
+ }
2719
3020
  // src/messages.ts
2720
3021
  var messages = {
2721
3022
  en: {
@@ -3384,6 +3685,22 @@ function GitPanel({ document, host, controller }) {
3384
3685
  selected,
3385
3686
  diff: await state.repository.readDiff(state.snapshot, selected)
3386
3687
  }));
3688
+ const [element, setElement] = useState(null);
3689
+ const [fileListOpen, setFileListOpen] = useState(false);
3690
+ const projectChanges = useProjectChanges(state.repository, state.snapshot, state.project, state.commit, state.changes);
3691
+ const openFile = (file) => {
3692
+ const change = state.changes.find((item) => item.path === file.path && item.group === file.group);
3693
+ if (!change) return;
3694
+ setElement(null);
3695
+ selectFile(change);
3696
+ };
3697
+ const openElement = (key) => {
3698
+ setElement(key);
3699
+ setState((previous) => _objectSpread2$1(_objectSpread2$1({}, previous), {}, {
3700
+ selected: null,
3701
+ diff: null
3702
+ }));
3703
+ };
3387
3704
  const selectCommit = (commit) => run(async () => ({
3388
3705
  commit,
3389
3706
  changes: await state.repository.historyChanges(state.snapshot, commit),
@@ -3438,12 +3755,19 @@ function GitPanel({ document, host, controller }) {
3438
3755
  first === null || first === void 0 || first.focus();
3439
3756
  }
3440
3757
  };
3758
+ const loadAsset = useMemo(() => state.repository && state.snapshot && state.selected ? state.repository.assetLoader(state.snapshot, state.selected) : void 0, [
3759
+ state.repository,
3760
+ state.snapshot,
3761
+ state.selected
3762
+ ]);
3441
3763
  if (!visible) return null;
3442
3764
  const groups = state.commit ? ["commit"] : [
3443
3765
  "staged",
3444
3766
  "unstaged",
3445
3767
  "untracked"
3446
3768
  ];
3769
+ const projectList = typeof projectChanges === "object" ? projectChanges : null;
3770
+ const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
3447
3771
  return /* @__PURE__ */ React.createElement("div", { className: "twgit-backdrop" }, /* @__PURE__ */ React.createElement("div", {
3448
3772
  className: "twgit-panel",
3449
3773
  ref: dialog,
@@ -3497,23 +3821,43 @@ function GitPanel({ document, host, controller }) {
3497
3821
  }, name, "/"))))), /* @__PURE__ */ React.createElement("h3", null, /* @__PURE__ */ React.createElement("button", {
3498
3822
  disabled: state.busy,
3499
3823
  onClick: () => void controller.refresh()
3500
- }, labels.worktree)), groups.map((group) => /* @__PURE__ */ React.createElement("section", { key: group }, /* @__PURE__ */ React.createElement("h3", null, labels[group], " · ", state.changes.filter((change) => change.group === group).length), /* @__PURE__ */ React.createElement("ul", null, state.changes.filter((change) => change.group === group).map((change) => /* @__PURE__ */ React.createElement("li", { key: change.path }, /* @__PURE__ */ React.createElement("button", {
3501
- className: "twgit-file",
3502
- "data-git-group": group,
3503
- disabled: state.busy,
3504
- "aria-pressed": state.selected === change,
3505
- onClick: () => void selectFile(change)
3506
- }, /* @__PURE__ */ React.createElement("span", null, !change.after ? "D" : !change.before ? "A" : "M"), " ", change.path)))))), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.branches), /* @__PURE__ */ React.createElement("ul", null, (_state$snapshot4 = state.snapshot) === null || _state$snapshot4 === void 0 ? void 0 : _state$snapshot4.branches.map((branch) => /* @__PURE__ */ React.createElement("li", { key: branch }, branch)))), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot5 = state.snapshot) === null || _state$snapshot5 === void 0 ? void 0 : _state$snapshot5.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3824
+ }, labels.worktree)), projectChanges === "loading" && /* @__PURE__ */ React.createElement("p", { role: "status" }, projectChangeLabels(locale).loading), projectChanges === "unreadable" && /* @__PURE__ */ React.createElement("p", null, projectChangeLabels(locale).unreadable), (() => {
3825
+ const fileList = groups.map((group) => /* @__PURE__ */ React.createElement("section", { key: group }, /* @__PURE__ */ React.createElement("h3", null, labels[group], " · ", state.changes.filter((change) => change.group === group).length), /* @__PURE__ */ React.createElement("ul", null, state.changes.filter((change) => change.group === group).map((change) => /* @__PURE__ */ React.createElement("li", { key: change.path }, /* @__PURE__ */ React.createElement("button", {
3826
+ className: "twgit-file",
3827
+ "data-git-group": group,
3828
+ disabled: state.busy,
3829
+ "aria-pressed": state.selected === change,
3830
+ onClick: () => openFile(change)
3831
+ }, /* @__PURE__ */ React.createElement("span", null, !change.after ? "D" : !change.before ? "A" : "M"), " ", change.path))))));
3832
+ return projectList ? /* @__PURE__ */ React.createElement(ProjectChangeListView, {
3833
+ changes: projectList,
3834
+ selected: selection ? element : null,
3835
+ locale,
3836
+ busy: state.busy,
3837
+ onSelect: openElement,
3838
+ onSelectFile: openFile,
3839
+ fileList,
3840
+ fileListOpen,
3841
+ setFileListOpen
3842
+ }) : fileList;
3843
+ })(), !state.changes.length && /* @__PURE__ */ React.createElement("p", null, labels.clean), /* @__PURE__ */ React.createElement("details", null, /* @__PURE__ */ React.createElement("summary", null, labels.branches), /* @__PURE__ */ React.createElement("ul", null, (_state$snapshot4 = state.snapshot) === null || _state$snapshot4 === void 0 ? void 0 : _state$snapshot4.branches.map((branch) => /* @__PURE__ */ React.createElement("li", { key: branch }, branch)))), /* @__PURE__ */ React.createElement("h3", null, labels.history), /* @__PURE__ */ React.createElement("ol", { className: "twgit-history" }, (_state$snapshot5 = state.snapshot) === null || _state$snapshot5 === void 0 ? void 0 : _state$snapshot5.commits.map((commit) => /* @__PURE__ */ React.createElement("li", { key: commit.oid }, /* @__PURE__ */ React.createElement("button", {
3507
3844
  disabled: state.busy,
3508
3845
  "aria-pressed": state.commit === commit.oid,
3509
3846
  title: commit.oid,
3510
3847
  onClick: () => void selectCommit(commit.oid)
3511
- }, commit.message.split("\n")[0]), /* @__PURE__ */ React.createElement("small", null, /* @__PURE__ */ React.createElement("code", null, commit.oid.slice(0, 8)), " · ", commit.author, " · ", (/* @__PURE__ */ new Date(commit.timestamp * 1e3)).toLocaleString(locale)))))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, state.selected ? /* @__PURE__ */ React.createElement(FileDiffView, {
3848
+ }, commit.message.split("\n")[0]), /* @__PURE__ */ React.createElement("small", null, /* @__PURE__ */ React.createElement("code", null, commit.oid.slice(0, 8)), " · ", commit.author, " · ", (/* @__PURE__ */ new Date(commit.timestamp * 1e3)).toLocaleString(locale)))))), /* @__PURE__ */ React.createElement("main", { className: "twgit-diff" }, selection && projectList ? /* @__PURE__ */ React.createElement(ProjectElementView, {
3849
+ changes: projectList,
3850
+ selection,
3851
+ locale,
3852
+ renderBlock: host.renderReviewBlock,
3853
+ onSelectFile: openFile
3854
+ }) : state.selected ? /* @__PURE__ */ React.createElement(FileDiffView, {
3512
3855
  change: state.selected,
3513
3856
  diff: state.diff,
3514
3857
  labels,
3515
3858
  locale,
3516
- renderBlock: host.renderReviewBlock
3859
+ renderBlock: host.renderReviewBlock,
3860
+ loadAsset
3517
3861
  }) : /* @__PURE__ */ React.createElement("p", null, labels.select))), /* @__PURE__ */ React.createElement("footer", null, labels.permissions)));
3518
3862
  }
3519
3863
  function mountGitPanel({ vm, document, host }) {
@@ -736,4 +736,26 @@ function splitLines(text) {
736
736
  else result.push(result.pop().slice(0, -1));
737
737
  return result;
738
738
  }
739
- export { hasOnlyUnixLineEndings as n, hasOnlyWinLineEndings as r, createTwoFilesPatch as t };
739
+ // src/project-asset-paths.ts
740
+ /** The largest asset a thumbnail reads. */
741
+ var assetLimit = 10485760;
742
+ /** The largest sb3-source.json read to find the assets directory. */
743
+ var folderSettingsLimit = 1048576;
744
+ /** Asset names are plain `<md5>.<ext>` file names; anything else could address a path outside the assets directory. */
745
+ var isAssetName = (name) => typeof name === "string" && /^[0-9A-Za-z_-]{1,128}\.[0-9A-Za-z]{1,8}$/.test(name);
746
+ /**
747
+ * The assets directory named by one version of a project folder's `sb3-source.json`, as path segments relative to the
748
+ * folder. `assets` when that file or its `assetsDirectory` field is missing or unreadable. `null` when the field names
749
+ * anything other than a plain relative path inside the folder.
750
+ */
751
+ function assetsDirectoryParts(settings) {
752
+ let value = "assets";
753
+ if (settings !== null) try {
754
+ const parsed = JSON.parse(settings);
755
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && "assetsDirectory" in parsed) value = parsed.assetsDirectory;
756
+ } catch (_unused) {}
757
+ if (typeof value !== "string" || value.length > 1024) return null;
758
+ const parts = value.split("/");
759
+ return parts.every((part) => part && part !== "." && part !== ".." && part.toLowerCase() !== ".git" && !/[\\\0-\x1f\x7f]/.test(part)) ? parts : null;
760
+ }
761
+ export { createTwoFilesPatch as a, isAssetName as i, assetsDirectoryParts as n, hasOnlyUnixLineEndings as o, folderSettingsLimit as r, hasOnlyWinLineEndings as s, assetLimit as t };