@kubohiroya/turbowarp-plus-editor-layout 0.0.0-stage → 0.1.2
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.
- package/LICENSE +674 -0
- package/README.ja.md +51 -0
- package/README.md +47 -2
- package/dist/dockview-styles.d.ts +1 -0
- package/dist/dockview-styles.js +1 -0
- package/dist/extension-file-menu.d.ts +31 -0
- package/dist/extension-file-menu.js +56 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +166 -0
- package/dist/monitor-drag.d.ts +4 -0
- package/dist/monitor-drag.js +78 -0
- package/dist/sprite-asset-pane.d.ts +37 -0
- package/dist/sprite-asset-pane.js +109 -0
- package/dist/sprite-editor-toolbar.d.ts +25 -0
- package/dist/sprite-editor-toolbar.js +72 -0
- package/dist/sprite-layout-menu.d.ts +11 -0
- package/dist/sprite-layout-menu.js +114 -0
- package/dist/sprite-properties.d.ts +41 -0
- package/dist/sprite-properties.js +124 -0
- package/dist/sprite-tabs.d.ts +174 -0
- package/dist/sprite-tabs.js +744 -0
- package/dist/stage-input.d.ts +24 -0
- package/dist/stage-input.js +50 -0
- package/dist/standard-layout.d.ts +60 -0
- package/dist/standard-layout.js +266 -0
- package/dist/targets.d.ts +24 -0
- package/dist/targets.js +79 -0
- package/dist/windows/dockable-panel.d.ts +10 -0
- package/dist/windows/dockable-panel.js +30 -0
- package/dist/windows/editor-fullscreen.d.ts +5 -0
- package/dist/windows/editor-fullscreen.js +43 -0
- package/dist/windows/index.d.ts +7 -0
- package/dist/windows/index.js +5 -0
- package/dist/windows/layout-menu.d.ts +14 -0
- package/dist/windows/layout-menu.js +36 -0
- package/dist/windows/title-bar.d.ts +31 -0
- package/dist/windows/title-bar.js +39 -0
- package/dist/windows/window-desktop.d.ts +34 -0
- package/dist/windows/window-desktop.js +232 -0
- package/dist/workspace-zoom.d.ts +39 -0
- package/dist/workspace-zoom.js +68 -0
- package/package.json +81 -4
- package/src/dockview-css.d.ts +1 -0
- package/src/dockview-styles.ts +1 -0
- package/src/editor.css +733 -0
- package/src/extension-file-menu.ts +71 -0
- package/src/index.ts +177 -0
- package/src/monitor-drag.ts +80 -0
- package/src/sprite-asset-pane.css +169 -0
- package/src/sprite-asset-pane.tsx +168 -0
- package/src/sprite-editor-toolbar.css +108 -0
- package/src/sprite-editor-toolbar.tsx +124 -0
- package/src/sprite-layout-menu.css +54 -0
- package/src/sprite-layout-menu.ts +119 -0
- package/src/sprite-properties.css +73 -0
- package/src/sprite-properties.tsx +206 -0
- package/src/sprite-tabs.ts +870 -0
- package/src/stage-input.ts +59 -0
- package/src/standard-layout.ts +297 -0
- package/src/targets.css +132 -0
- package/src/targets.ts +86 -0
- package/src/windows/dockable-panel.tsx +42 -0
- package/src/windows/editor-fullscreen.tsx +47 -0
- package/src/windows/index.ts +9 -0
- package/src/windows/layout-menu.tsx +58 -0
- package/src/windows/styles.css +217 -0
- package/src/windows/title-bar.tsx +82 -0
- package/src/windows/window-desktop.tsx +357 -0
- package/src/workspace-zoom.ts +92 -0
- package/tsconfig.json +23 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# TurboWarp Plus Editor Layout
|
|
2
|
+
|
|
3
|
+
同じ document 内で「編集領域/ステージ列」と「ステージ/対象領域」の境界を調整する。ホストは `createLayout(React)` に既存の React を渡し、`render` に渡される境界・スタイル・表示サイズを既存コンポーネントに接続する。ホストがストレージとキーを指定し、このパッケージが設定を保存・復元する。
|
|
4
|
+
|
|
5
|
+
境界はドラッグ、矢印キー、Home/End で移動できる。Enter またはダブルクリックで、その境界を既定の位置に戻す。
|
|
6
|
+
|
|
7
|
+
## Editor での配分
|
|
8
|
+
|
|
9
|
+
| 境界 | 最小サイズ |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 編集領域/ステージ列 | 編集領域 500 px、ステージ列 350 px |
|
|
12
|
+
| ステージ/対象領域 | ステージ 135 px、対象領域 175 px |
|
|
13
|
+
|
|
14
|
+
境界の幅は 8 px。分割比率は各領域の最小サイズまでに制限する。最小サイズを両立できない表示領域では通常配分に戻し、表示領域を広げると保存した比率で分割を再開する。プロパティと一覧はスプライト選択ペイン内にまとまっており、バックパックは開閉式のトレイなので、この段階では既存の操作で扱う。メニューバーは分割領域の外にある。
|
|
15
|
+
|
|
16
|
+
Scratch 配置では分割を既定で有効にする。既存のステージサイズボタンを押すと、指定したプリセットで配分を初期化する。設定は作品や SB3 と別に保存する。分割だけを無効にする場合は Web の起動 URL に `twPlusSplitter=0` を指定するか、Desktop を `TW_PLUS_DESKTOP_SPLITTER=0` で起動する。
|
|
17
|
+
|
|
18
|
+
## Plus 標準配置
|
|
19
|
+
|
|
20
|
+
`createStandardLayout(React, {styles, targetStyles, context})` は、⚓️ での配置切り替え、ステージの表示と計測、スプライト一覧の2列配置を担当する。`editor.css` と `targets.css` をクラス名マップとして渡す。ホストは起動設定、ストレージ、ウィンドウ操作部品、パレット表示のコールバックと描画先を渡す。配置メニューの描画を差し替える場合は、任意の `LayoutMenu` コンポーネントをファクトリーに渡す。ステージを隠しても同じ描画先を使う。
|
|
21
|
+
|
|
22
|
+
試験用ホストの `/example/?standard=1` で Plus と Scratch の往復を確認できる。`standard=0` は起動時の切り戻しを再現する。
|
|
23
|
+
|
|
24
|
+
Editor はこのパッケージの `0.1.0` をローカル依存としてロックし、Editor のリビジョンと一緒にソースを固定する。
|
|
25
|
+
|
|
26
|
+
## 検証
|
|
27
|
+
|
|
28
|
+
`npm test` で配分・縦横比・設定の読み書きを、`npm run test:browser` で独立した試験用ホストを検証する。試験用ホストを手動で開くには、このディレクトリで `npm install` と `python3 -m http.server` を実行し、`/example/` を表示する。
|
|
29
|
+
|
|
30
|
+
Editor のルートから、`site/source.json` の GUI を `npm run build:pages` でビルドし、`E2E_GUI_ROOT=/absolute/gui-checkout E2E_PATH_PREFIX=/turbowarp-plus-editor/ node --test test/editor-layout.browser.ts` を実行する。同じ GUI をホスト接続なしでビルドしたディレクトリを `E2E_LAYOUT_BARE_BUILD` に渡すと、GUI 単体の動作も検証する。
|
|
31
|
+
## 別窓ステージとスプライト設定
|
|
32
|
+
|
|
33
|
+
`attachStageInput` は、ホストから渡された別ウィンドウの document とキャンバスを使って、入力の購読と解放を管理する。ポインター処理とドラッグ状態の解除はホストのコールバックを使用する。
|
|
34
|
+
|
|
35
|
+
`sprite-properties` は設定フォーム、一覧のスタイル、選択が変わった場合の編集拒否と VM への更新を担当する。ホストは選択中の値、VM、従来フォームと表示先を渡す。
|
|
36
|
+
|
|
37
|
+
設定フォームは以前のスプライト設定ソース `80825f7`、ウィンドウ部品は以前の配布アーカイブのソース `b0390c3` から移設した。Scratch GUI の本家実装はこのパッケージに取り込まず、パッケージの既存ライセンスは GPL-3.0-or-later とする。
|
|
38
|
+
|
|
39
|
+
## Editor と組み合わせたローカル検証
|
|
40
|
+
|
|
41
|
+
`npm test` でタブの状態管理、ファイルメニューの操作、既存レイアウトを検証する。
|
|
42
|
+
`TW_PLUS_LAYOUT_EDITOR_URL` に、固定済み Scratch GUI と組み合わせた Editor の URL を指定して
|
|
43
|
+
`npm run test:browser` を実行すると、既存のタブ列・編集パネル接続で Dockview を検証する。
|
|
44
|
+
ファイルメニューの任意接続を備えた GUI を確認する場合は、同じコマンドに
|
|
45
|
+
`TW_PLUS_LAYOUT_FILE_MENU_URL` を指定する。
|
|
46
|
+
|
|
47
|
+
Plus 標準配置ではスプライト編集タブを既定で表示する。`twPlusDockviewSpriteTabs=0` で従来の編集表示に戻る。
|
|
48
|
+
Editor がファイルメニューの接続口を提供している場合は、各項目のアイコンと「機能拡張の追加」を表示する。
|
|
49
|
+
`twPlusExtensionFileMenu=0` で左下のボタンに戻る。起動時のパラメーターを変更したら再読み込みする。
|
|
50
|
+
|
|
51
|
+
Web 版の Plus 標準配置で `twPlusDockview=1` を付けて起動すると、スプライト編集ペインを左右・上下に分割できます。同じスプライトを複数のペインで開いた場合、表示セクション、コスチュームと音の選択、素材一覧のスクロール位置をペインごとに保持します。変数の値はプロジェクト内で共有され、編集中のペインで変更できます。URL から `twPlusDockview=1` を外して再読み込みすると、通常のスプライトタブに戻ります。
|
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TurboWarp Plus Editor Layout
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[日本語](README.ja.md)
|
|
4
|
+
|
|
5
|
+
Resize the divider between the editor and the stage column, or the divider between the stage and the target pane, within a single document. The host passes its React instance to `createLayout(React)` and connects the dividers, styles, and display dimensions provided by the `render` callback to its existing components. The host supplies a storage provider and key, and this package saves and restores the layout settings.
|
|
6
|
+
|
|
7
|
+
Drag a divider or use the arrow keys, Home, or End to move it. Press Enter or double-click to reset that divider to its default position.
|
|
8
|
+
|
|
9
|
+
## Editor layout sizing
|
|
10
|
+
|
|
11
|
+
| Divider | Minimum sizes |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Editor / stage column | Editor 500 px, stage column 350 px |
|
|
14
|
+
| Stage / target pane | Stage 135 px, target pane 175 px |
|
|
15
|
+
|
|
16
|
+
Dividers are 8 px wide. Each split is constrained by the minimum size of its panes. If the available space is too small to satisfy both minimums, the editor falls back to its usual sizing. The split resumes at the saved ratio when the available space grows. The sprite properties and list stay together in the sprite selector, and the backpack remains a collapsible tray. The menu bar sits outside the split area.
|
|
17
|
+
|
|
18
|
+
Dividers are enabled by default in the Scratch layout. Selecting a stage size preset resets the split to that preset. Layout settings are stored separately from projects and SB3 files. To disable the dividers, add `twPlusSplitter=0` to the Web launch URL or start Desktop with `TW_PLUS_DESKTOP_SPLITTER=0`.
|
|
19
|
+
|
|
20
|
+
## Plus standard layout
|
|
21
|
+
|
|
22
|
+
`createStandardLayout(React, {styles, targetStyles, context})` handles layout switching via the anchor control (⚓️), stage visibility and measurement, and the two-column sprite list. Pass `editor.css` and `targets.css` as class-name maps. The host provides startup settings, storage, window controls, and the callback and mount point for the palette. Pass an optional `LayoutMenu` component to the factory to customize the layout menu. The stage keeps the same mount point while hidden.
|
|
23
|
+
|
|
24
|
+
Use `/example/?standard=1` in the test host to try the Plus layout. Use `standard=0` to try the startup fallback to the Scratch layout.
|
|
25
|
+
|
|
26
|
+
The Editor locks version `0.1.0` of this package as a local dependency and pins its source alongside the Editor revision.
|
|
27
|
+
|
|
28
|
+
## Verification
|
|
29
|
+
|
|
30
|
+
Run `npm test` to verify split sizing, aspect ratios, and settings persistence. Run `npm run test:browser` to test the standalone host. To open the test host manually, run `npm install` and `python3 -m http.server` in this directory, then open `/example/`.
|
|
31
|
+
|
|
32
|
+
From the Editor root, build the GUI pinned in `site/source.json` with `npm run build:pages`, then run `E2E_GUI_ROOT=/absolute/gui-checkout E2E_PATH_PREFIX=/turbowarp-plus-editor/ node --test test/editor-layout.browser.ts`. To verify the GUI on its own, set `E2E_LAYOUT_BARE_BUILD` to the directory where the same GUI was built without the host integration.
|
|
33
|
+
|
|
34
|
+
## Separate stage window and sprite properties
|
|
35
|
+
|
|
36
|
+
`attachStageInput` manages input subscriptions and cleanup using the separate window's document and canvas provided by the host. Pointer handling and drag cleanup use the host's callbacks.
|
|
37
|
+
|
|
38
|
+
`sprite-properties` provides the properties form and list styles, rejects stale edits when the selected sprite changes, and applies updates to the VM. The host provides the selected values, the VM, the fallback form, and the mount point.
|
|
39
|
+
|
|
40
|
+
The properties form came from the previous sprite properties source at `80825f7`, and the window components came from the source of the previous release archive at `b0390c3`. The extraction kept the upstream Scratch GUI implementation in its original repository. Its existing license is GPL-3.0-or-later.
|
|
41
|
+
|
|
42
|
+
## Local verification with the Editor
|
|
43
|
+
|
|
44
|
+
Run `npm test` to check tab state, file menu actions, and the existing layout. Set `TW_PLUS_LAYOUT_EDITOR_URL` to an Editor URL built with the pinned Scratch GUI and run `npm run test:browser` to verify Dockview through the existing tab strip and editor panel integration. Set `TW_PLUS_LAYOUT_FILE_MENU_URL` for the same command to check a GUI build with the optional file menu integration.
|
|
45
|
+
|
|
46
|
+
Sprite editing tabs are enabled by default in the Plus standard layout. Set `twPlusDockviewSpriteTabs=0` to restore the previous editing view. When the Editor exposes the file menu integration, the menu shows item icons and the Add Extension command. Set `twPlusExtensionFileMenu=0` to use the button at the bottom left again. Reload the editor after changing startup parameters.
|
|
47
|
+
|
|
48
|
+
In the Plus web editor, add `twPlusDockview=1` to enable split sprite panes. Each pane remembers its section, costume and sound selections, and asset-list scroll position. Variable values are shared across panes and can be changed in the active pane. Remove the startup parameter and reload to return to the regular sprite tabs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'dockview/dist/styles/dockview.css';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'dockview/dist/styles/dockview.css';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ExtensionMenuOptions {
|
|
3
|
+
locale?: string;
|
|
4
|
+
MenuSection: React.ElementType;
|
|
5
|
+
MenuItem: React.ElementType;
|
|
6
|
+
onAddExtension?: () => void;
|
|
7
|
+
onRequestClose(): void;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderExtensionFileMenu({ locale, MenuSection, MenuItem, onAddExtension, onRequestClose }: ExtensionMenuOptions): React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
|
10
|
+
declare const glyphs: {
|
|
11
|
+
new: string;
|
|
12
|
+
window: string;
|
|
13
|
+
save: string;
|
|
14
|
+
copy: string;
|
|
15
|
+
remix: string;
|
|
16
|
+
open: string;
|
|
17
|
+
'save-as': string;
|
|
18
|
+
download: string;
|
|
19
|
+
package: string;
|
|
20
|
+
restore: string;
|
|
21
|
+
properties: string;
|
|
22
|
+
payloads: string;
|
|
23
|
+
'bundle-assets': string;
|
|
24
|
+
diagnostics: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function createFileMenuItem(MenuItem: React.ElementType, extensionIcon: string): ({ command, children, ...props }: {
|
|
27
|
+
command?: keyof typeof glyphs | "extension";
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
[prop: string]: unknown;
|
|
30
|
+
}) => React.ReactElement<any, string | React.JSXElementConstructor<any>>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export function renderExtensionFileMenu({ locale = 'en', MenuSection, MenuItem, onAddExtension, onRequestClose }) {
|
|
3
|
+
if (!onAddExtension)
|
|
4
|
+
return null;
|
|
5
|
+
function open() {
|
|
6
|
+
onRequestClose();
|
|
7
|
+
onAddExtension();
|
|
8
|
+
}
|
|
9
|
+
return React.createElement(MenuSection, null, React.createElement(MenuItem, { command: 'extension', onClick: open }, React.createElement('span', {
|
|
10
|
+
role: 'button',
|
|
11
|
+
tabIndex: 0,
|
|
12
|
+
onKeyDown(event) {
|
|
13
|
+
if (event.key !== 'Enter' && event.key !== ' ')
|
|
14
|
+
return;
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
event.stopPropagation();
|
|
17
|
+
open();
|
|
18
|
+
}
|
|
19
|
+
}, /^ja(?:-|$)/i.test(locale) ? '機能拡張の追加' : 'Add Extension')));
|
|
20
|
+
}
|
|
21
|
+
const glyphs = {
|
|
22
|
+
new: 'M5 3h9l5 5v13H5z M14 3v5h5 M9 14h6 M12 11v6',
|
|
23
|
+
window: 'M3 5h18v14H3z M3 9h18 M9 14h6 M12 11v6',
|
|
24
|
+
save: 'M4 3h13l3 3v15H4z M8 3v6h8V3 M8 21v-7h8v7',
|
|
25
|
+
copy: 'M8 8h12v13H8z M16 8V3H3v13h5',
|
|
26
|
+
remix: 'M18 8a7 7 0 1 0 0 8 M18 3v5h-5 M18 8l2-2',
|
|
27
|
+
open: 'M3 6h7l2 2h9l-2 11H3z M3 11h16',
|
|
28
|
+
'save-as': 'M4 3h13l3 3v15H4z M8 3v6h8V3 M9 17h6 M12 14v6',
|
|
29
|
+
download: 'M12 3v12 M7 10l5 5 5-5 M4 17v4h16v-4',
|
|
30
|
+
package: 'm12 3 8 4v10l-8 4-8-4V7z M4 7l8 4 8-4 M12 11v10 M8 5l8 4',
|
|
31
|
+
restore: 'M4 10a8 8 0 1 1 2 8 M4 4v6h6 M12 7v5l3 2',
|
|
32
|
+
properties: 'M4 6h16 M4 12h16 M4 18h16 M8 3v6 M16 9v6 M10 15v6',
|
|
33
|
+
payloads: 'M9 5 4 12l5 7 M15 5l5 7-5 7 M14 4l-4 16',
|
|
34
|
+
'bundle-assets': 'M4 5h16v14H4z M12 8v8 M8 12h8',
|
|
35
|
+
diagnostics: 'M3 12h4l2-5 4 10 2-5h6'
|
|
36
|
+
};
|
|
37
|
+
const iconSize = 16;
|
|
38
|
+
const iconStyle = { width: iconSize, height: iconSize, flex: 'none' };
|
|
39
|
+
const rowStyle = { display: 'inline-flex', alignItems: 'center', gap: 8, verticalAlign: 'middle' };
|
|
40
|
+
export function createFileMenuItem(MenuItem, extensionIcon) {
|
|
41
|
+
return function FileMenuItem({ command, children, ...props }) {
|
|
42
|
+
let icon;
|
|
43
|
+
if (command === 'extension') {
|
|
44
|
+
icon = React.createElement('span', { 'aria-hidden': true, 'data-file-menu-icon': command, style: {
|
|
45
|
+
...iconStyle, backgroundColor: 'currentColor', mask: `url("${extensionIcon}") center / contain no-repeat`
|
|
46
|
+
} });
|
|
47
|
+
}
|
|
48
|
+
else if (command && glyphs[command]) {
|
|
49
|
+
icon = React.createElement('svg', { 'aria-hidden': true, 'data-file-menu-icon': command,
|
|
50
|
+
viewBox: '0 0 24 24', style: iconStyle, fill: 'none', stroke: 'currentColor', strokeWidth: 1.7,
|
|
51
|
+
strokeLinecap: 'round', strokeLinejoin: 'round' }, React.createElement('path', { d: glyphs[command] }));
|
|
52
|
+
}
|
|
53
|
+
return React.createElement(MenuItem, props, icon ?
|
|
54
|
+
React.createElement('span', { style: rowStyle }, icon, React.createElement('span', null, children)) : children);
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export { attachExternalMonitorDrag } from './monitor-drag.ts';
|
|
2
|
+
export { attachStageInput } from './stage-input.ts';
|
|
3
|
+
type Axis = 'columns' | 'rows';
|
|
4
|
+
type Ratios = Record<Axis, number | null>;
|
|
5
|
+
type LayoutStorage = Pick<Storage, 'getItem' | 'setItem'>;
|
|
6
|
+
export declare function splitAxis(length: number, ratio: number | null, fallback: number, minimumFirst: number, minimumSecond: number): {
|
|
7
|
+
available: number;
|
|
8
|
+
minimum: number;
|
|
9
|
+
maximum: number;
|
|
10
|
+
first: number;
|
|
11
|
+
second: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function readRatios(storage: LayoutStorage | null, key: string): Ratios;
|
|
14
|
+
export declare function writeRatios(storage: LayoutStorage | null, key: string, ratios: Ratios): void;
|
|
15
|
+
export declare function fitStage(columnWidth: number, rowHeight: number, aspect: number, horizontalInset: number, verticalInset: number): number;
|
|
16
|
+
export declare function createSplitter(React: any): ({ axis, split, rtl, label, onChange, onReset, className }: {
|
|
17
|
+
axis: any;
|
|
18
|
+
split: any;
|
|
19
|
+
rtl: any;
|
|
20
|
+
label: any;
|
|
21
|
+
onChange: any;
|
|
22
|
+
onReset: any;
|
|
23
|
+
className: any;
|
|
24
|
+
}) => any;
|
|
25
|
+
export declare function createLayout(React: any): ({ render, enabled, fullscreen, rtl, labels, separatorClassName, stageTools, defaultStageWidth, defaultStageHeight, stageAspect, horizontalInset, verticalInset, minimumEditor, minimumStageColumn, minimumStageRow, minimumTargets, storage, storageKey }: {
|
|
26
|
+
render: any;
|
|
27
|
+
enabled: any;
|
|
28
|
+
fullscreen: any;
|
|
29
|
+
rtl: any;
|
|
30
|
+
labels: any;
|
|
31
|
+
separatorClassName: any;
|
|
32
|
+
stageTools: any;
|
|
33
|
+
defaultStageWidth: any;
|
|
34
|
+
defaultStageHeight: any;
|
|
35
|
+
stageAspect: any;
|
|
36
|
+
horizontalInset: any;
|
|
37
|
+
verticalInset: any;
|
|
38
|
+
minimumEditor: any;
|
|
39
|
+
minimumStageColumn: any;
|
|
40
|
+
minimumStageRow: any;
|
|
41
|
+
minimumTargets: any;
|
|
42
|
+
storage: any;
|
|
43
|
+
storageKey: any;
|
|
44
|
+
}) => any;
|
|
45
|
+
export { createTargetList } from './targets.ts';
|
|
46
|
+
export { attachHorizontalWorkspaceZoom, workspaceControlsCorner } from './workspace-zoom.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export { attachExternalMonitorDrag } from "./monitor-drag.js";
|
|
2
|
+
export { attachStageInput } from "./stage-input.js";
|
|
3
|
+
const separatorSize = 8;
|
|
4
|
+
const keyboardStep = 16;
|
|
5
|
+
export function splitAxis(length, ratio, fallback, minimumFirst, minimumSecond) {
|
|
6
|
+
const available = Math.max(0, Math.round(length) - separatorSize);
|
|
7
|
+
const minimum = Math.min(minimumFirst, available);
|
|
8
|
+
const maximum = Math.max(minimum, available - minimumSecond);
|
|
9
|
+
const desired = ratio === null ? fallback : available * ratio;
|
|
10
|
+
const first = Math.round(Math.min(maximum, Math.max(minimum, desired)));
|
|
11
|
+
return { available, minimum, maximum, first, second: available - first };
|
|
12
|
+
}
|
|
13
|
+
export function readRatios(storage, key) {
|
|
14
|
+
try {
|
|
15
|
+
const saved = JSON.parse(storage?.getItem(key));
|
|
16
|
+
if (saved?.version === 1) {
|
|
17
|
+
const ratio = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1 ? value : null;
|
|
18
|
+
return { columns: ratio(saved.columns), rows: ratio(saved.rows) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Storage can be unavailable or contain obsolete data.
|
|
23
|
+
}
|
|
24
|
+
return { columns: null, rows: null };
|
|
25
|
+
}
|
|
26
|
+
export function writeRatios(storage, key, ratios) {
|
|
27
|
+
try {
|
|
28
|
+
storage?.setItem(key, JSON.stringify({ version: 1, ...ratios }));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Keep the current layout usable when persistence is denied.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function fitStage(columnWidth, rowHeight, aspect, horizontalInset, verticalInset) {
|
|
35
|
+
return Math.max(1, Math.floor(Math.min(columnWidth - horizontalInset, (rowHeight - verticalInset) * aspect)));
|
|
36
|
+
}
|
|
37
|
+
export function createSplitter(React) {
|
|
38
|
+
return function Splitter({ axis, split, rtl, label, onChange, onReset, className }) {
|
|
39
|
+
const drag = React.useRef(null);
|
|
40
|
+
const vertical = axis === 'columns';
|
|
41
|
+
const coordinate = event => vertical ? event.clientX : event.clientY;
|
|
42
|
+
const direction = vertical && rtl ? -1 : 1;
|
|
43
|
+
const update = position => {
|
|
44
|
+
if (split.available > 0) {
|
|
45
|
+
const value = Math.max(split.minimum, Math.min(split.maximum, position));
|
|
46
|
+
onChange(value / split.available);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const onKeyDown = event => {
|
|
50
|
+
const { key } = event;
|
|
51
|
+
const previous = vertical ? 'ArrowLeft' : 'ArrowUp';
|
|
52
|
+
const next = vertical ? 'ArrowRight' : 'ArrowDown';
|
|
53
|
+
if (key === previous)
|
|
54
|
+
update(split.first - keyboardStep * direction);
|
|
55
|
+
else if (key === next)
|
|
56
|
+
update(split.first + keyboardStep * direction);
|
|
57
|
+
else if (key === 'Home')
|
|
58
|
+
update(split.minimum);
|
|
59
|
+
else if (key === 'End')
|
|
60
|
+
update(split.maximum);
|
|
61
|
+
else if (key === 'Enter')
|
|
62
|
+
onReset();
|
|
63
|
+
else
|
|
64
|
+
return;
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
};
|
|
68
|
+
const clearDrag = event => {
|
|
69
|
+
if (drag.current?.id === event.pointerId)
|
|
70
|
+
drag.current = null;
|
|
71
|
+
};
|
|
72
|
+
return React.createElement('div', {
|
|
73
|
+
role: 'separator',
|
|
74
|
+
tabIndex: 0,
|
|
75
|
+
'aria-label': label,
|
|
76
|
+
'aria-orientation': vertical ? 'vertical' : 'horizontal',
|
|
77
|
+
'aria-valuemin': split.minimum,
|
|
78
|
+
'aria-valuemax': split.maximum,
|
|
79
|
+
'aria-valuenow': split.first,
|
|
80
|
+
className,
|
|
81
|
+
style: {
|
|
82
|
+
touchAction: 'none',
|
|
83
|
+
userSelect: 'none',
|
|
84
|
+
cursor: vertical ? 'col-resize' : 'row-resize'
|
|
85
|
+
},
|
|
86
|
+
onKeyDown,
|
|
87
|
+
onDoubleClick: onReset,
|
|
88
|
+
onPointerDown: event => {
|
|
89
|
+
if (event.button !== 0 || drag.current)
|
|
90
|
+
return;
|
|
91
|
+
drag.current = { id: event.pointerId, start: coordinate(event), first: split.first };
|
|
92
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
93
|
+
event.currentTarget.focus();
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
},
|
|
96
|
+
onPointerMove: event => {
|
|
97
|
+
if (drag.current?.id !== event.pointerId)
|
|
98
|
+
return;
|
|
99
|
+
update(drag.current.first + (coordinate(event) - drag.current.start) * direction);
|
|
100
|
+
},
|
|
101
|
+
onPointerUp: clearDrag,
|
|
102
|
+
onPointerCancel: clearDrag,
|
|
103
|
+
onLostPointerCapture: clearDrag
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function createLayout(React) {
|
|
108
|
+
const Splitter = createSplitter(React);
|
|
109
|
+
return function SplitLayout({ render, enabled, fullscreen, rtl, labels, separatorClassName, stageTools, defaultStageWidth, defaultStageHeight, stageAspect, horizontalInset, verticalInset, minimumEditor, minimumStageColumn, minimumStageRow, minimumTargets, storage, storageKey }) {
|
|
110
|
+
const [ratios, setRatios] = React.useState(() => readRatios(storage, storageKey));
|
|
111
|
+
const [size, setSize] = React.useState(null);
|
|
112
|
+
const element = React.useRef(null);
|
|
113
|
+
const observe = React.useCallback(node => {
|
|
114
|
+
element.current = node;
|
|
115
|
+
}, []);
|
|
116
|
+
const active = enabled && !fullscreen;
|
|
117
|
+
React.useLayoutEffect(() => {
|
|
118
|
+
const node = element.current;
|
|
119
|
+
const ResizeObserver = node?.ownerDocument.defaultView.ResizeObserver;
|
|
120
|
+
if (!active || !ResizeObserver)
|
|
121
|
+
return;
|
|
122
|
+
const measure = () => {
|
|
123
|
+
const { clientWidth: width, clientHeight: height } = node;
|
|
124
|
+
setSize(previous => previous?.width === width && previous.height === height ?
|
|
125
|
+
previous : { width, height });
|
|
126
|
+
};
|
|
127
|
+
measure();
|
|
128
|
+
const observer = new ResizeObserver(measure);
|
|
129
|
+
observer.observe(node);
|
|
130
|
+
return () => observer.disconnect();
|
|
131
|
+
}, [active]);
|
|
132
|
+
const base = { observe, stageTools };
|
|
133
|
+
if (!active || !size ||
|
|
134
|
+
size.width < minimumEditor + separatorSize + minimumStageColumn ||
|
|
135
|
+
size.height < minimumStageRow + separatorSize + minimumTargets)
|
|
136
|
+
return render(base);
|
|
137
|
+
const columns = splitAxis(size.width, ratios.columns, size.width - separatorSize - Math.max(minimumStageColumn, defaultStageWidth + horizontalInset), minimumEditor, minimumStageColumn);
|
|
138
|
+
const rows = splitAxis(size.height, ratios.rows, defaultStageHeight + verticalInset, minimumStageRow, minimumTargets);
|
|
139
|
+
const save = updated => {
|
|
140
|
+
setRatios(updated);
|
|
141
|
+
writeRatios(storage, storageKey, updated);
|
|
142
|
+
};
|
|
143
|
+
const change = (axis, value) => save({ ...ratios, [axis]: value });
|
|
144
|
+
const separator = (axis, split) => React.createElement(Splitter, {
|
|
145
|
+
axis, split, rtl,
|
|
146
|
+
label: labels[axis],
|
|
147
|
+
className: separatorClassName,
|
|
148
|
+
onChange: value => change(axis, value),
|
|
149
|
+
onReset: () => change(axis, null)
|
|
150
|
+
});
|
|
151
|
+
return render({
|
|
152
|
+
...base,
|
|
153
|
+
columnSplitter: separator('columns', columns),
|
|
154
|
+
rowSplitter: separator('rows', rows),
|
|
155
|
+
bodyStyle: { display: 'grid', gridTemplateColumns: `minmax(0, 1fr) ${separatorSize}px ${columns.second}px` },
|
|
156
|
+
editorStyle: { minWidth: 0, minHeight: 0 },
|
|
157
|
+
stageColumnStyle: { display: 'grid', minWidth: 0, minHeight: 0,
|
|
158
|
+
gridTemplateRows: `${rows.first}px ${separatorSize}px minmax(0, 1fr)` },
|
|
159
|
+
stageDisplayWidth: fitStage(columns.second, rows.first, stageAspect, horizontalInset, verticalInset),
|
|
160
|
+
editorWidth: columns.first,
|
|
161
|
+
handleStagePreset: () => save({ columns: null, rows: null })
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export { createTargetList } from "./targets.js";
|
|
166
|
+
export { attachHorizontalWorkspaceZoom, workspaceControlsCorner } from "./workspace-zoom.js";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function attachExternalMonitorDrag(element, onMove, isDraggable, editorDocument) {
|
|
2
|
+
let drag;
|
|
3
|
+
function clear() {
|
|
4
|
+
if (!drag)
|
|
5
|
+
return;
|
|
6
|
+
const { id, translate } = drag;
|
|
7
|
+
drag = undefined;
|
|
8
|
+
element.style.translate = translate;
|
|
9
|
+
if (element.hasPointerCapture(id))
|
|
10
|
+
element.releasePointerCapture(id);
|
|
11
|
+
}
|
|
12
|
+
function start(event) {
|
|
13
|
+
const owner = element.ownerDocument;
|
|
14
|
+
if (drag || owner === editorDocument || event.pointerType !== 'mouse' || event.button !== 0 ||
|
|
15
|
+
!isDraggable() || event.target.closest?.('.no-drag'))
|
|
16
|
+
return;
|
|
17
|
+
const overlay = element.closest('.monitor-overlay');
|
|
18
|
+
if (!overlay || !overlay.clientWidth || !overlay.clientHeight)
|
|
19
|
+
return;
|
|
20
|
+
const rect = overlay.getBoundingClientRect();
|
|
21
|
+
if (!rect.width || !rect.height)
|
|
22
|
+
return;
|
|
23
|
+
const scaleX = rect.width / overlay.clientWidth;
|
|
24
|
+
const scaleY = rect.height / overlay.clientHeight;
|
|
25
|
+
const monitorRect = element.getBoundingClientRect();
|
|
26
|
+
const left = Math.round((monitorRect.left - rect.left) / scaleX);
|
|
27
|
+
const top = Math.round((monitorRect.top - rect.top) / scaleY);
|
|
28
|
+
drag = {
|
|
29
|
+
id: event.pointerId, owner, clientX: event.clientX, clientY: event.clientY,
|
|
30
|
+
scaleX, scaleY, left, top, offsetLeft: element.offsetLeft, offsetTop: element.offsetTop,
|
|
31
|
+
minX: -left, minY: -top,
|
|
32
|
+
maxX: overlay.clientWidth - element.offsetWidth - left,
|
|
33
|
+
maxY: overlay.clientHeight - element.offsetHeight - top,
|
|
34
|
+
translate: element.style.translate
|
|
35
|
+
};
|
|
36
|
+
element.setPointerCapture(drag.id);
|
|
37
|
+
// Stop the compatibility mousedown from starting the in-editor drag path.
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
}
|
|
40
|
+
function offset(event) {
|
|
41
|
+
const x = Math.round((event.clientX - drag.clientX) / drag.scaleX);
|
|
42
|
+
const y = Math.round((event.clientY - drag.clientY) / drag.scaleY);
|
|
43
|
+
return {
|
|
44
|
+
x: Math.max(drag.minX, Math.min(drag.maxX, x)),
|
|
45
|
+
y: Math.max(drag.minY, Math.min(drag.maxY, y))
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function move(event) {
|
|
49
|
+
if (drag?.id !== event.pointerId)
|
|
50
|
+
return;
|
|
51
|
+
if (drag.owner !== element.ownerDocument)
|
|
52
|
+
return clear();
|
|
53
|
+
const { x, y } = offset(event);
|
|
54
|
+
element.style.translate = `${x}px ${y}px`;
|
|
55
|
+
}
|
|
56
|
+
function finish(event) {
|
|
57
|
+
if (drag?.id !== event.pointerId)
|
|
58
|
+
return;
|
|
59
|
+
const position = event.type === 'pointerup' && drag.owner === element.ownerDocument && offset(event);
|
|
60
|
+
const { left, top, offsetLeft, offsetTop } = drag;
|
|
61
|
+
clear();
|
|
62
|
+
if (!position || !(position.x || position.y))
|
|
63
|
+
return;
|
|
64
|
+
onMove(event, { x: left + position.x - offsetLeft, y: top + position.y - offsetTop });
|
|
65
|
+
// Keep the editor's existing drag transform and update only the layout position.
|
|
66
|
+
element.style.left = `${offsetLeft + position.x}px`;
|
|
67
|
+
element.style.top = `${offsetTop + position.y}px`;
|
|
68
|
+
}
|
|
69
|
+
const handlers = { pointerdown: start, pointermove: move, pointerup: finish,
|
|
70
|
+
pointercancel: finish, lostpointercapture: finish };
|
|
71
|
+
for (const [name, handler] of Object.entries(handlers))
|
|
72
|
+
element.addEventListener(name, handler);
|
|
73
|
+
return () => {
|
|
74
|
+
for (const [name, handler] of Object.entries(handlers))
|
|
75
|
+
element.removeEventListener(name, handler);
|
|
76
|
+
clear();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface SpriteMediaAsset {
|
|
3
|
+
/** Host identity for this item within its target, independent of the list order. */
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
imageUrl?: string;
|
|
7
|
+
duration?: number;
|
|
8
|
+
/** Downsampled waveform magnitudes in the range from zero to one. */
|
|
9
|
+
peaks?: readonly number[];
|
|
10
|
+
}
|
|
11
|
+
export interface SpriteVariable {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
value: string | number;
|
|
15
|
+
scope?: 'local' | 'global';
|
|
16
|
+
}
|
|
17
|
+
interface PaneProps {
|
|
18
|
+
locale: string;
|
|
19
|
+
editable: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface SpriteMediaPaneProps extends PaneProps {
|
|
22
|
+
kind: 'costumes' | 'sounds';
|
|
23
|
+
items: readonly SpriteMediaAsset[];
|
|
24
|
+
selectedId: string | null;
|
|
25
|
+
detailOnly?: boolean;
|
|
26
|
+
onSelect(id: string): void;
|
|
27
|
+
/** Optional host surface. It must honor editable without unmounting its layout. */
|
|
28
|
+
renderEditor?(selected: SpriteMediaAsset, editable: boolean): React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export interface SpriteVariablePaneProps extends PaneProps {
|
|
31
|
+
kind: 'variables';
|
|
32
|
+
variables: readonly SpriteVariable[];
|
|
33
|
+
onWrite(id: string, value: SpriteVariable['value']): void;
|
|
34
|
+
}
|
|
35
|
+
export type SpriteAssetPaneProps = SpriteMediaPaneProps | SpriteVariablePaneProps;
|
|
36
|
+
export declare function SpriteAssetPane(props: SpriteAssetPaneProps): React.JSX.Element;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const english = {
|
|
3
|
+
costumes: 'Costumes', sounds: 'Sounds', variables: 'Variables', name: 'Name', value: 'Value',
|
|
4
|
+
local: 'Sprite', global: 'Project', emptyCostumes: 'No costumes', emptySounds: 'No sounds',
|
|
5
|
+
emptyVariables: 'No variables', selectCostume: 'Select a costume', selectSound: 'Select a sound',
|
|
6
|
+
imageUnavailable: 'Image unavailable', waveformUnavailable: 'Waveform unavailable', waveform: 'Waveform',
|
|
7
|
+
seconds: 's'
|
|
8
|
+
};
|
|
9
|
+
const japanese = {
|
|
10
|
+
costumes: 'コスチューム', sounds: '音', variables: '変数', name: '名前', value: '値',
|
|
11
|
+
local: 'スプライト', global: '作品', emptyCostumes: 'コスチュームがありません', emptySounds: '音がありません',
|
|
12
|
+
emptyVariables: '変数がありません', selectCostume: 'コスチュームを選択してください', selectSound: '音を選択してください',
|
|
13
|
+
imageUnavailable: '画像を表示できません', waveformUnavailable: '波形を表示できません', waveform: '波形',
|
|
14
|
+
seconds: '秒'
|
|
15
|
+
};
|
|
16
|
+
const labelsFor = (locale) => /^ja(?:-|$)/i.test(locale) ? japanese : english;
|
|
17
|
+
function Waveform({ name, peaks, label }) {
|
|
18
|
+
const path = peaks.map((sample, index) => {
|
|
19
|
+
const x = (index + 0.5) * 100 / peaks.length;
|
|
20
|
+
const magnitude = Number.isFinite(sample) ? Math.max(0, Math.min(sample, 1)) : 0;
|
|
21
|
+
const y = magnitude * 49;
|
|
22
|
+
return 'M' + x + ' ' + (50 - y) + 'V' + (50 + y);
|
|
23
|
+
}).join(' ');
|
|
24
|
+
return React.createElement("svg", { className: "tw-sprite-media__waveform", viewBox: "0 0 100 100", preserveAspectRatio: "none", role: "img", "aria-label": label + ': ' + name },
|
|
25
|
+
React.createElement("path", { d: "M0 50H100", vectorEffect: "non-scaling-stroke", className: "tw-sprite-media__axis" }),
|
|
26
|
+
React.createElement("path", { d: path, vectorEffect: "non-scaling-stroke", className: "tw-sprite-media__peaks" }));
|
|
27
|
+
}
|
|
28
|
+
function MediaPane({ kind, items, selectedId, onSelect, renderEditor, editable, locale, detailOnly }) {
|
|
29
|
+
const labels = labelsFor(locale);
|
|
30
|
+
const isSound = kind === 'sounds';
|
|
31
|
+
const selected = items.find(asset => asset.id === selectedId);
|
|
32
|
+
const duration = selected?.duration;
|
|
33
|
+
return React.createElement("section", { className: detailOnly ? 'tw-sprite-media tw-sprite-media--detail' : 'tw-sprite-media', "aria-label": labels[kind] },
|
|
34
|
+
!detailOnly && React.createElement("ul", { className: "tw-sprite-media__list", "aria-label": labels[kind] }, items.map(asset => React.createElement("li", { key: asset.id },
|
|
35
|
+
React.createElement("button", { type: "button", className: "tw-sprite-media__item", "aria-current": asset.id === selectedId || undefined, "aria-disabled": !editable || undefined, tabIndex: editable ? 0 : -1, onClick: () => {
|
|
36
|
+
if (editable)
|
|
37
|
+
onSelect(asset.id);
|
|
38
|
+
} },
|
|
39
|
+
asset.imageUrl ? React.createElement("img", { src: asset.imageUrl, alt: "", className: "tw-sprite-media__thumbnail" }) :
|
|
40
|
+
React.createElement("span", { className: "tw-sprite-media__symbol", "aria-hidden": "true" }, isSound ? '♪' : '◫'),
|
|
41
|
+
React.createElement("span", { className: "tw-sprite-media__name" }, asset.name))))),
|
|
42
|
+
React.createElement("div", { className: "tw-sprite-media__detail" }, selected ? React.createElement(React.Fragment, null,
|
|
43
|
+
React.createElement("h3", { className: "tw-sprite-media__title" }, selected.name),
|
|
44
|
+
React.createElement("div", { className: "tw-sprite-media__surface" }, renderEditor?.(selected, editable) ?? (isSound ?
|
|
45
|
+
(selected.peaks?.length ? React.createElement(Waveform, { peaks: selected.peaks, name: selected.name, label: labels.waveform }) :
|
|
46
|
+
React.createElement("p", null, labels.waveformUnavailable)) :
|
|
47
|
+
(selected.imageUrl ? React.createElement("img", { src: selected.imageUrl, alt: selected.name, className: "tw-sprite-media__preview" }) :
|
|
48
|
+
React.createElement("p", null, labels.imageUnavailable)))),
|
|
49
|
+
isSound && Number.isFinite(duration) && duration >= 0 &&
|
|
50
|
+
React.createElement("p", { className: "tw-sprite-media__duration" },
|
|
51
|
+
new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(duration),
|
|
52
|
+
" ",
|
|
53
|
+
labels.seconds)) : React.createElement("p", null, items.length ? (isSound ? labels.selectSound : labels.selectCostume) :
|
|
54
|
+
(isSound ? labels.emptySounds : labels.emptyCostumes))));
|
|
55
|
+
}
|
|
56
|
+
function VariableField({ variable, editable, onWrite }) {
|
|
57
|
+
const [text, setText] = React.useState(null);
|
|
58
|
+
const pending = React.useRef(null);
|
|
59
|
+
const finish = (save) => {
|
|
60
|
+
const draft = pending.current;
|
|
61
|
+
if (!draft)
|
|
62
|
+
return;
|
|
63
|
+
pending.current = null;
|
|
64
|
+
setText(null);
|
|
65
|
+
if (!save || draft.text === String(variable.value))
|
|
66
|
+
return;
|
|
67
|
+
const number = Number(draft.text);
|
|
68
|
+
const value = typeof draft.variable.value === 'number' && draft.text.trim() && Number.isFinite(number) ?
|
|
69
|
+
number : draft.text;
|
|
70
|
+
draft.write(draft.variable.id, value);
|
|
71
|
+
};
|
|
72
|
+
React.useLayoutEffect(() => {
|
|
73
|
+
// A click can change the active pane before the old input receives blur.
|
|
74
|
+
if (!editable)
|
|
75
|
+
finish(true);
|
|
76
|
+
}, [editable]);
|
|
77
|
+
return React.createElement("input", { className: "tw-sprite-variables__input", "aria-label": variable.name, "data-variable-id": variable.id, readOnly: !editable, tabIndex: editable ? 0 : -1, value: text ?? String(variable.value), onChange: event => {
|
|
78
|
+
if (!editable)
|
|
79
|
+
return;
|
|
80
|
+
const next = event.currentTarget.value;
|
|
81
|
+
pending.current = { text: next, variable, write: onWrite };
|
|
82
|
+
setText(next);
|
|
83
|
+
}, onBlur: () => finish(true), onKeyDown: event => {
|
|
84
|
+
if (!editable || event.nativeEvent.isComposing)
|
|
85
|
+
return;
|
|
86
|
+
if (event.key !== 'Enter' && event.key !== 'Escape')
|
|
87
|
+
return;
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
event.stopPropagation();
|
|
90
|
+
finish(event.key === 'Enter');
|
|
91
|
+
} });
|
|
92
|
+
}
|
|
93
|
+
function VariablePane({ variables, editable, onWrite, locale }) {
|
|
94
|
+
const labels = labelsFor(locale);
|
|
95
|
+
return React.createElement("section", { className: "tw-sprite-variables", "aria-label": labels.variables }, variables.length ? React.createElement("table", { className: "tw-sprite-variables__table" },
|
|
96
|
+
React.createElement("thead", null,
|
|
97
|
+
React.createElement("tr", null,
|
|
98
|
+
React.createElement("th", { scope: "col" }, labels.name),
|
|
99
|
+
React.createElement("th", { scope: "col" }, labels.value))),
|
|
100
|
+
React.createElement("tbody", null, variables.map(variable => React.createElement("tr", { key: variable.id },
|
|
101
|
+
React.createElement("th", { scope: "row" },
|
|
102
|
+
variable.name,
|
|
103
|
+
variable.scope && React.createElement("span", { className: "tw-sprite-variables__scope" }, labels[variable.scope])),
|
|
104
|
+
React.createElement("td", null,
|
|
105
|
+
React.createElement(VariableField, { variable: variable, editable: editable, onWrite: onWrite })))))) : React.createElement("p", null, labels.emptyVariables));
|
|
106
|
+
}
|
|
107
|
+
export function SpriteAssetPane(props) {
|
|
108
|
+
return props.kind === 'variables' ? React.createElement(VariablePane, { ...props }) : React.createElement(MediaPane, { ...props });
|
|
109
|
+
}
|