@kubohiroya/turbowarp-plus-editor-layout 0.1.6 → 0.1.9
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/dist/document-workspace.d.ts +1 -0
- package/dist/sprite-asset-pane.d.ts +49 -0
- package/dist/sprite-asset-pane.js +59 -12
- package/dist/sprite-editor-toolbar.d.ts +2 -1
- package/dist/sprite-editor-toolbar.js +3 -3
- package/dist/sprite-tabs.d.ts +10 -2
- package/dist/sprite-tabs.js +64 -38
- package/dist/standard-layout.d.ts +2 -1
- package/dist/standard-layout.js +6 -5
- package/dist/targets.d.ts +2 -1
- package/dist/targets.js +8 -2
- package/dist/workspace-layout.d.ts +4 -0
- package/dist/workspace-layout.js +27 -0
- package/package.json +1 -1
- package/src/document-workspace.ts +1 -0
- package/src/editor.css +11 -0
- package/src/sprite-asset-pane.tsx +104 -1
- package/src/sprite-editor-toolbar.tsx +4 -3
- package/src/sprite-tabs.ts +63 -33
- package/src/standard-layout.ts +7 -6
- package/src/targets.ts +7 -2
- package/src/workspace-layout.ts +25 -0
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { type SpritePropertiesPanelProps } from './sprite-properties.tsx';
|
|
3
|
+
import type { SpriteAssetPreviewItem } from './sprite-tabs.ts';
|
|
4
|
+
interface NativeAssetPanelProps {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
buttons: readonly {
|
|
7
|
+
title: string;
|
|
8
|
+
img: string;
|
|
9
|
+
onClick(): void;
|
|
10
|
+
}[];
|
|
11
|
+
items: readonly {
|
|
12
|
+
dragPayload?: object;
|
|
13
|
+
name: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
details?: React.ReactNode;
|
|
16
|
+
}[];
|
|
17
|
+
isRtl?: boolean;
|
|
18
|
+
listActions?: React.ElementType;
|
|
19
|
+
selectedItemIndex: number;
|
|
20
|
+
onItemClick(index: number): void;
|
|
21
|
+
}
|
|
22
|
+
interface NativeSelectorProps extends NativeAssetPanelProps {
|
|
23
|
+
wrapperAttributes?: Record<string, string>;
|
|
24
|
+
listRef?(node: HTMLElement | null): void;
|
|
25
|
+
onListScroll?(event: React.UIEvent<HTMLElement>): void;
|
|
26
|
+
draggingType?: null;
|
|
27
|
+
onAddSortable?(): void;
|
|
28
|
+
onRemoveSortable?(): void;
|
|
29
|
+
}
|
|
30
|
+
export type NativeAssetSelector = React.ComponentType<NativeSelectorProps>;
|
|
31
|
+
export type NativeAssetPanelRenderer = (props: NativeAssetPanelProps, options: {
|
|
32
|
+
panelAttributes: Record<string, string>;
|
|
33
|
+
detailAttributes: Record<string, string>;
|
|
34
|
+
renderSelector(Component: NativeAssetSelector, props: NativeSelectorProps): React.ReactNode;
|
|
35
|
+
}) => React.ReactNode;
|
|
36
|
+
export type SpriteAssetLayout = (items: readonly SpriteAssetPreviewItem[], index: number, detail: React.ReactNode, scrollTop: number) => React.ReactNode;
|
|
37
|
+
interface PanelState {
|
|
38
|
+
rememberLayout(render: SpriteAssetLayout): void;
|
|
39
|
+
getScrollTop(): number;
|
|
40
|
+
rememberScrollTop(position: number): void;
|
|
41
|
+
}
|
|
42
|
+
export declare function NativeAssetPanel({ render, previewSelector, panel, state }: {
|
|
43
|
+
render: NativeAssetPanelRenderer;
|
|
44
|
+
previewSelector: NativeAssetSelector;
|
|
45
|
+
panel: NativeAssetPanelProps;
|
|
46
|
+
state: PanelState;
|
|
47
|
+
}): React.JSX.Element;
|
|
2
48
|
export interface SpriteMediaAsset {
|
|
3
49
|
/** Host identity for this item within its target, independent of the list order. */
|
|
4
50
|
id: string;
|
|
@@ -30,6 +76,9 @@ export interface SpriteMediaPaneProps extends PaneProps {
|
|
|
30
76
|
export interface SpriteVariablePaneProps extends PaneProps {
|
|
31
77
|
kind: 'variables';
|
|
32
78
|
variables: readonly SpriteVariable[];
|
|
79
|
+
properties?: SpritePropertiesPanelProps['values'];
|
|
80
|
+
targetId?: string;
|
|
81
|
+
onWriteProperties?: SpritePropertiesPanelProps['onChange'];
|
|
33
82
|
onWrite(id: string, value: SpriteVariable['value']): void;
|
|
34
83
|
}
|
|
35
84
|
export type SpriteAssetPaneProps = SpriteMediaPaneProps | SpriteVariablePaneProps;
|
|
@@ -1,4 +1,48 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { SpritePropertiesPanel } from "./sprite-properties.js";
|
|
3
|
+
const noop = () => { };
|
|
4
|
+
function NativeAssetPanelSurface({ render, panel, state, previewSelector, scrollTop }) {
|
|
5
|
+
const list = React.useRef(null);
|
|
6
|
+
const setList = React.useCallback((node) => {
|
|
7
|
+
list.current = node;
|
|
8
|
+
}, []);
|
|
9
|
+
React.useLayoutEffect(() => {
|
|
10
|
+
const position = state?.getScrollTop() ?? scrollTop;
|
|
11
|
+
if (list.current && position !== undefined)
|
|
12
|
+
list.current.scrollTop = position;
|
|
13
|
+
}, [state, scrollTop]);
|
|
14
|
+
return render(panel, {
|
|
15
|
+
panelAttributes: { 'data-layout-asset-panel': '' },
|
|
16
|
+
detailAttributes: { 'data-layout-asset-detail': '' },
|
|
17
|
+
renderSelector(Component, props) {
|
|
18
|
+
const selectorProps = { ...props, listRef: setList, wrapperAttributes: { 'data-layout-asset-list': '' } };
|
|
19
|
+
if (previewSelector) {
|
|
20
|
+
return React.createElement("div", { inert: '', style: { display: 'contents' } }, React.createElement(previewSelector, {
|
|
21
|
+
...selectorProps, draggingType: null, onAddSortable: noop, onRemoveSortable: noop
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
return React.createElement(Component, { ...selectorProps, onListScroll: event => {
|
|
25
|
+
state?.rememberScrollTop(event.currentTarget.scrollTop);
|
|
26
|
+
} });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function NativeAssetPanel({ render, previewSelector, panel, state }) {
|
|
31
|
+
const { items, buttons, isRtl, listActions } = panel;
|
|
32
|
+
React.useLayoutEffect(() => {
|
|
33
|
+
const previousItems = new Map(items.map(item => [item.dragPayload, item]));
|
|
34
|
+
const readOnlyButtons = buttons.map(({ title, img }) => ({ title, img, onClick: noop }));
|
|
35
|
+
state.rememberLayout((previewItems, selectedItemIndex, detail, scrollTop) => React.createElement(NativeAssetPanelSurface, { render: render, previewSelector: previewSelector, scrollTop: scrollTop, panel: {
|
|
36
|
+
isRtl, listActions, selectedItemIndex, buttons: readOnlyButtons,
|
|
37
|
+
onItemClick: noop, children: detail,
|
|
38
|
+
items: previewItems.map(item => {
|
|
39
|
+
const previous = previousItems.get(item.asset);
|
|
40
|
+
return { name: item.name, url: item.imageUrl || previous?.url, details: previous?.details };
|
|
41
|
+
})
|
|
42
|
+
} }));
|
|
43
|
+
}, [state, render, previewSelector, items, buttons, isRtl, listActions]);
|
|
44
|
+
return React.createElement(NativeAssetPanelSurface, { render: render, panel: panel, state: state });
|
|
45
|
+
}
|
|
2
46
|
const english = {
|
|
3
47
|
costumes: 'Costumes', sounds: 'Sounds', variables: 'Variables', name: 'Name', value: 'Value',
|
|
4
48
|
local: 'Sprite', global: 'Project', emptyCostumes: 'No costumes', emptySounds: 'No sounds',
|
|
@@ -90,19 +134,22 @@ function VariableField({ variable, editable, onWrite }) {
|
|
|
90
134
|
finish(event.key === 'Enter');
|
|
91
135
|
} });
|
|
92
136
|
}
|
|
93
|
-
function VariablePane({ variables, editable, onWrite, locale }) {
|
|
137
|
+
function VariablePane({ variables, editable, onWrite, locale, properties, targetId, onWriteProperties }) {
|
|
94
138
|
const labels = labelsFor(locale);
|
|
95
|
-
return React.createElement("section", { className: "tw-sprite-variables", "aria-label": labels.variables },
|
|
96
|
-
|
|
97
|
-
React.createElement(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
139
|
+
return React.createElement("section", { className: "tw-sprite-variables", "aria-label": labels.variables },
|
|
140
|
+
properties && targetId && onWriteProperties &&
|
|
141
|
+
React.createElement(SpritePropertiesPanel, { targetId: targetId, values: properties, locale: locale, disabled: !editable, onChange: onWriteProperties }),
|
|
142
|
+
variables.length ? React.createElement("table", { className: "tw-sprite-variables__table" },
|
|
143
|
+
React.createElement("thead", null,
|
|
144
|
+
React.createElement("tr", null,
|
|
145
|
+
React.createElement("th", { scope: "col" }, labels.name),
|
|
146
|
+
React.createElement("th", { scope: "col" }, labels.value))),
|
|
147
|
+
React.createElement("tbody", null, variables.map(variable => React.createElement("tr", { key: variable.id },
|
|
148
|
+
React.createElement("th", { scope: "row" },
|
|
149
|
+
variable.name,
|
|
150
|
+
variable.scope && React.createElement("span", { className: "tw-sprite-variables__scope" }, labels[variable.scope])),
|
|
151
|
+
React.createElement("td", null,
|
|
152
|
+
React.createElement(VariableField, { variable: variable, editable: editable, onWrite: onWrite })))))) : React.createElement("p", null, labels.emptyVariables));
|
|
106
153
|
}
|
|
107
154
|
export function SpriteAssetPane(props) {
|
|
108
155
|
return props.kind === 'variables' ? React.createElement(VariablePane, { ...props }) : React.createElement(MediaPane, { ...props });
|
|
@@ -19,7 +19,8 @@ interface SpriteEditorToolbarProps {
|
|
|
19
19
|
activeGroup?: boolean;
|
|
20
20
|
search?: SpriteEditorSearch;
|
|
21
21
|
availableSections?: readonly SpriteEditorSection[];
|
|
22
|
+
propertiesInVariables?: boolean;
|
|
22
23
|
actions?: React.ReactNode;
|
|
23
24
|
}
|
|
24
|
-
export declare function SpriteEditorToolbar({ viewId, locale, section, onSectionChange, isStage, activeGroup, search, availableSections, actions }: SpriteEditorToolbarProps): React.JSX.Element;
|
|
25
|
+
export declare function SpriteEditorToolbar({ viewId, locale, section, onSectionChange, isStage, activeGroup, search, availableSections, propertiesInVariables, actions }: SpriteEditorToolbarProps): React.JSX.Element;
|
|
25
26
|
export {};
|
|
@@ -49,10 +49,10 @@ function SearchBox({ viewId, section, japanese, search }) {
|
|
|
49
49
|
} })),
|
|
50
50
|
expanded && React.createElement("ul", { role: "listbox", id: listId, "aria-label": japanese ? '検索結果' : 'Search results' }, results.map((result, position) => React.createElement("li", { key: result.id, id: optionId(position), role: "option", "aria-selected": position === index, onPointerDown: event => event.preventDefault(), onClick: () => choose(result) }, result.label))));
|
|
51
51
|
}
|
|
52
|
-
export function SpriteEditorToolbar({ viewId, locale, section, onSectionChange, isStage, activeGroup, search, availableSections = spriteEditorSections, actions }) {
|
|
52
|
+
export function SpriteEditorToolbar({ viewId, locale, section, onSectionChange, isStage, activeGroup, search, availableSections = spriteEditorSections, propertiesInVariables = false, actions }) {
|
|
53
53
|
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
54
|
-
const labels = japanese ? { code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: '変数' } :
|
|
55
|
-
{ code: 'Code', costumes: isStage ? 'Backdrops' : 'Costumes', sounds: 'Sounds', variables: 'Variables' };
|
|
54
|
+
const labels = japanese ? { code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: propertiesInVariables ? 'プロパティ・変数' : '変数' } :
|
|
55
|
+
{ code: 'Code', costumes: isStage ? 'Backdrops' : 'Costumes', sounds: 'Sounds', variables: propertiesInVariables ? 'Properties & Variables' : 'Variables' };
|
|
56
56
|
return React.createElement("div", { className: "tw-plus-sprite-toolbar", "data-active-group": activeGroup },
|
|
57
57
|
React.createElement("div", { className: "tw-plus-sprite-toolbar-sections", role: "tablist", "aria-label": japanese ? 'スプライトの編集内容' : 'Sprite editor sections' }, availableSections.map((id, index) => React.createElement("button", { key: id, type: "button", role: "tab", id: viewId + '-section-' + id, "aria-selected": section === id, tabIndex: section === id ? 0 : -1, "aria-controls": viewId + '-content', onClick: () => onSectionChange(id), onKeyDown: event => {
|
|
58
58
|
const direction = event.currentTarget.ownerDocument.dir === 'rtl' ? -1 : 1;
|
package/dist/sprite-tabs.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { NativeAssetPanel } from './sprite-asset-pane.tsx';
|
|
3
|
+
import type { NativeAssetPanelRenderer, NativeAssetSelector, SpriteAssetLayout } from './sprite-asset-pane.tsx';
|
|
2
4
|
import type { SpriteVariable } from './sprite-asset-pane.tsx';
|
|
5
|
+
import type { SpritePropertiesPanelProps } from './sprite-properties.tsx';
|
|
3
6
|
import type { SpriteSearchResult } from './sprite-editor-toolbar.tsx';
|
|
4
7
|
import type { DockviewStagePane } from './dockview-stage.ts';
|
|
5
8
|
export { getDocumentWorkspace } from './document-workspace.ts';
|
|
@@ -49,6 +52,7 @@ export interface SpriteTabAdapter<Snapshot = unknown> {
|
|
|
49
52
|
export interface SpriteAssetSelection {
|
|
50
53
|
restore(target: object, assets: readonly object[]): number | undefined;
|
|
51
54
|
remember(target: object, asset: object | undefined, select?: (index: number) => void): void;
|
|
55
|
+
renderPanel?(render: NativeAssetPanelRenderer, previewSelector: NativeAssetSelector, panel: React.ComponentProps<typeof NativeAssetPanel>['panel']): React.ReactNode;
|
|
52
56
|
rememberLayout?(render: SpriteAssetLayout): void;
|
|
53
57
|
getScrollTop?(): number;
|
|
54
58
|
rememberScrollTop?(position: number): void;
|
|
@@ -62,7 +66,6 @@ export interface SpriteAssetPreviewItem {
|
|
|
62
66
|
buffer?: AudioBuffer;
|
|
63
67
|
duration?: number;
|
|
64
68
|
}
|
|
65
|
-
type SpriteAssetLayout = (items: readonly SpriteAssetPreviewItem[], index: number, detail: React.ReactNode, scrollTop: number) => React.ReactNode;
|
|
66
69
|
export interface SpriteAssetPreviewData {
|
|
67
70
|
items: readonly SpriteAssetPreviewItem[];
|
|
68
71
|
defaultAsset?: object;
|
|
@@ -78,7 +81,7 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
78
81
|
reopenLabel: string;
|
|
79
82
|
multiView?: boolean;
|
|
80
83
|
layoutStorage?: {
|
|
81
|
-
storage: Storage
|
|
84
|
+
storage: Pick<Storage, 'getItem' | 'setItem'>;
|
|
82
85
|
key: string;
|
|
83
86
|
isProjectLoading?(): boolean;
|
|
84
87
|
subscribeProject?(listener: () => void): (() => void) | undefined;
|
|
@@ -135,6 +138,7 @@ export declare function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HT
|
|
|
135
138
|
setViewSection: (id: string, section: number) => boolean;
|
|
136
139
|
syncActiveSection: (section: number) => void;
|
|
137
140
|
refreshSurfaces: () => void;
|
|
141
|
+
rename(id: string): void;
|
|
138
142
|
selectAsset(id: string, kind: AssetKind, index: number): void;
|
|
139
143
|
currentView: () => SpriteTabView;
|
|
140
144
|
views: () => SpriteTabView[];
|
|
@@ -154,6 +158,8 @@ type SpriteTabsProps<Snapshot> = Omit<TabOptions<Snapshot>, 'activateSection' |
|
|
|
154
158
|
renderToolbar?(view: SpriteTabView, actions: ToolbarActions): React.ReactNode;
|
|
155
159
|
getAssetPreview?(targetId: string, kind: SpriteAssetKind): SpriteAssetPreviewData;
|
|
156
160
|
getVariables?(targetId: string): readonly SpriteVariable[];
|
|
161
|
+
getProperties?(targetId: string): SpritePropertiesPanelProps['values'];
|
|
162
|
+
writeProperties?: SpritePropertiesPanelProps['onChange'];
|
|
157
163
|
writeVariable?(targetId: string, id: string, value: SpriteVariable['value']): void;
|
|
158
164
|
getCodeSearchItems?(targetId: string): readonly SpriteSearchResult[];
|
|
159
165
|
openCodeSearchResult?(targetId: string, id: string): void;
|
|
@@ -173,6 +179,7 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
|
|
|
173
179
|
setViewSection: (id: string, section: number) => boolean;
|
|
174
180
|
syncActiveSection: (section: number) => void;
|
|
175
181
|
refreshSurfaces: () => void;
|
|
182
|
+
rename(id: string): void;
|
|
176
183
|
selectAsset(id: string, kind: AssetKind, index: number): void;
|
|
177
184
|
currentView: () => SpriteTabView;
|
|
178
185
|
views: () => SpriteTabView[];
|
|
@@ -184,6 +191,7 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
|
|
|
184
191
|
openLabel: string;
|
|
185
192
|
select: (id: string) => void;
|
|
186
193
|
open: (id: string) => void;
|
|
194
|
+
rename: (id: string) => void;
|
|
187
195
|
};
|
|
188
196
|
renderEditorTabRow: (children: React.ReactNode) => React.FunctionComponentElement<SpriteTabsProps<Snapshot>>;
|
|
189
197
|
renderEditorPanel: (index: number, children: React.ReactNode) => React.ReactPortal;
|
package/dist/sprite-tabs.js
CHANGED
|
@@ -2,10 +2,10 @@ import React from 'react';
|
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { createDockview, DefaultTab, themeLight } from 'dockview';
|
|
4
4
|
import { createSpriteLayoutMenu } from "./sprite-layout-menu.js";
|
|
5
|
-
import { SpriteAssetPane } from "./sprite-asset-pane.js";
|
|
5
|
+
import { NativeAssetPanel, SpriteAssetPane } from "./sprite-asset-pane.js";
|
|
6
6
|
import { SpriteEditorToolbar, spriteEditorSections } from "./sprite-editor-toolbar.js";
|
|
7
7
|
import { getDocumentWorkspace } from "./document-workspace.js";
|
|
8
|
-
import { readWorkspaceLayout } from "./workspace-layout.js";
|
|
8
|
+
import { readWorkspaceLayout, workspaceWithoutTransientPanels } from "./workspace-layout.js";
|
|
9
9
|
export { getDocumentWorkspace } from "./document-workspace.js";
|
|
10
10
|
export { useDockviewStage } from "./dockview-stage.js";
|
|
11
11
|
const variableSection = spriteEditorSections.indexOf('variables');
|
|
@@ -14,32 +14,38 @@ function createAssetSelections(target, source) {
|
|
|
14
14
|
const layouts = new Map(source?.layouts);
|
|
15
15
|
const scroll = new Map(source?.scroll);
|
|
16
16
|
const selectors = new Map();
|
|
17
|
-
const forKind = (kind) =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
17
|
+
const forKind = (kind) => {
|
|
18
|
+
const state = {
|
|
19
|
+
rememberLayout(render) {
|
|
20
|
+
layouts.set(kind, render);
|
|
21
|
+
},
|
|
22
|
+
getScrollTop: () => scroll.get(kind) ?? 0,
|
|
23
|
+
rememberScrollTop(position) {
|
|
24
|
+
scroll.set(kind, position);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
...state,
|
|
29
|
+
renderPanel: (render, previewSelector, panel) => React.createElement(NativeAssetPanel, { render, previewSelector, panel, state }),
|
|
30
|
+
restore(current, assets) {
|
|
31
|
+
if (current !== target)
|
|
32
|
+
return;
|
|
33
|
+
const index = assets.indexOf(selected.get(kind));
|
|
34
|
+
if (index >= 0)
|
|
35
|
+
return index;
|
|
36
|
+
},
|
|
37
|
+
remember(current, asset, select) {
|
|
38
|
+
if (current !== target)
|
|
39
|
+
return;
|
|
40
|
+
if (asset)
|
|
41
|
+
selected.set(kind, asset);
|
|
42
|
+
else
|
|
43
|
+
selected.delete(kind);
|
|
44
|
+
if (select)
|
|
45
|
+
selectors.set(kind, select);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
};
|
|
43
49
|
const ports = { costumes: forKind('costumes'), sounds: forKind('sounds') };
|
|
44
50
|
return { selected, ports, layouts, selectors, scroll };
|
|
45
51
|
}
|
|
@@ -221,11 +227,9 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
221
227
|
});
|
|
222
228
|
return { ...menu, init() { } };
|
|
223
229
|
}
|
|
224
|
-
|
|
225
|
-
return [...views.values()].some(view => view.target === target) && vm.runtime.targets.includes(target);
|
|
226
|
-
}
|
|
230
|
+
const canRename = (target) => target.isOriginal && !target.isStage && Boolean(vm.renameSprite) && vm.runtime.targets.includes(target);
|
|
227
231
|
function openRenameDialog(target) {
|
|
228
|
-
if (renameDialog || !
|
|
232
|
+
if (renameDialog || !canRename(target))
|
|
229
233
|
return;
|
|
230
234
|
const dialog = document.createElement('dialog');
|
|
231
235
|
dialog.className = 'tw-plus-sprite-rename';
|
|
@@ -253,7 +257,7 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
253
257
|
if (renameDialog !== dialog)
|
|
254
258
|
return;
|
|
255
259
|
renameDialog = undefined;
|
|
256
|
-
if (dialog.returnValue === 'save' && input.value.trim() &&
|
|
260
|
+
if (dialog.returnValue === 'save' && input.value.trim() && canRename(target) &&
|
|
257
261
|
input.value !== target.getName())
|
|
258
262
|
vm.renameSprite?.(target.id, input.value);
|
|
259
263
|
}, { once: true });
|
|
@@ -556,9 +560,17 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
556
560
|
}
|
|
557
561
|
if (dockview.getPanel(stagePanelId))
|
|
558
562
|
panels[stagePanelId] = { kind: 'stage' };
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
563
|
+
const transient = new Set();
|
|
564
|
+
for (const [id, { request }] of documents) {
|
|
565
|
+
if (request.transient)
|
|
566
|
+
transient.add(id);
|
|
567
|
+
else
|
|
568
|
+
panels[id] = { kind: 'document', ...request.reference };
|
|
569
|
+
}
|
|
570
|
+
const saved = workspaceWithoutTransientPanels(grid, activeGroup, transient, active?.id);
|
|
571
|
+
if (!saved)
|
|
572
|
+
return;
|
|
573
|
+
const snapshot = { version: 1, project: projectSignature(targets), ...saved,
|
|
562
574
|
editingView: active?.id, panels };
|
|
563
575
|
try {
|
|
564
576
|
persistence.storage.setItem(persistence.key, JSON.stringify(snapshot));
|
|
@@ -752,6 +764,11 @@ export function mountSpriteTabs(root, content, options) {
|
|
|
752
764
|
refresh();
|
|
753
765
|
return {
|
|
754
766
|
select, openStage, closeStage, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
767
|
+
rename(id) {
|
|
768
|
+
const target = vm.runtime.targets.find(item => item.id === id);
|
|
769
|
+
if (target)
|
|
770
|
+
openRenameDialog(target);
|
|
771
|
+
},
|
|
755
772
|
selectAsset(id, kind, index) {
|
|
756
773
|
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
757
774
|
active.assets.selectors.get(kind)?.(index);
|
|
@@ -928,7 +945,7 @@ function SpriteTabs(props) {
|
|
|
928
945
|
const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
|
|
929
946
|
React.createElement(SpriteEditorToolbar, {
|
|
930
947
|
viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
|
|
931
|
-
activeGroup: view.groupActive,
|
|
948
|
+
activeGroup: view.groupActive, propertiesInVariables: Boolean(props.getProperties),
|
|
932
949
|
actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
|
|
933
950
|
onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
|
|
934
951
|
search: {
|
|
@@ -963,6 +980,12 @@ function SpriteTabs(props) {
|
|
|
963
980
|
return createPortal(toolbar, element, view.id);
|
|
964
981
|
}), assetPreviewSlots.map(({ id, targetId, kind, element, editable, selected, renderLayout, scrollTop }) => createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
|
|
965
982
|
kind, locale: props.locale, editable, variables: props.getVariables?.(targetId) ?? [],
|
|
983
|
+
properties: props.getProperties?.(targetId),
|
|
984
|
+
targetId,
|
|
985
|
+
onWriteProperties(id, patch) {
|
|
986
|
+
props.writeProperties?.(id, patch);
|
|
987
|
+
workspace?.refreshSurfaces();
|
|
988
|
+
},
|
|
966
989
|
onWrite(variableId, value) {
|
|
967
990
|
props.writeVariable?.(targetId, variableId, value);
|
|
968
991
|
workspace?.refreshSurfaces();
|
|
@@ -992,7 +1015,10 @@ export function useSpriteTabs({ available, enabled, locale, ...props }) {
|
|
|
992
1015
|
return {
|
|
993
1016
|
controller,
|
|
994
1017
|
assetSelections: enabled ? selection.assets : undefined,
|
|
995
|
-
context: enabled ? {
|
|
1018
|
+
context: enabled ? {
|
|
1019
|
+
selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select,
|
|
1020
|
+
rename: props.multiView && props.getProperties ? (id) => controller.current?.rename(id) : undefined
|
|
1021
|
+
} : null,
|
|
996
1022
|
renderEditorTabRow: available ? (children) => React.createElement((SpriteTabs), {
|
|
997
1023
|
...props, enabled, locale, children, slots, onAttach: () => setReady(true),
|
|
998
1024
|
selectTarget: (id, assets) => setSelection({ id, assets }),
|
|
@@ -55,8 +55,9 @@ export declare function createStandardLayout(React: any, { styles, targetStyles,
|
|
|
55
55
|
buttons: any;
|
|
56
56
|
children: any;
|
|
57
57
|
}) => any;
|
|
58
|
-
SpritePropertiesSection: ({ fallback, children }: {
|
|
58
|
+
SpritePropertiesSection: ({ fallback, children, inEditor }: {
|
|
59
59
|
fallback: any;
|
|
60
60
|
children: any;
|
|
61
|
+
inEditor?: boolean;
|
|
61
62
|
}) => any;
|
|
62
63
|
};
|
package/dist/standard-layout.js
CHANGED
|
@@ -236,14 +236,15 @@ export function createStandardLayout(React, { styles, targetStyles, context, Lay
|
|
|
236
236
|
const unique = buttons.filter(({ title }, index) => buttons.findIndex(button => button.title === title) === index);
|
|
237
237
|
return element(targets.BackdropFooter, { buttons: unique, label: isJapanese(locale) ? '背景を追加' : 'Add a backdrop' });
|
|
238
238
|
}
|
|
239
|
-
function SpritePropertiesSection({ fallback, children }) {
|
|
239
|
+
function SpritePropertiesSection({ fallback, children, inEditor = false }) {
|
|
240
240
|
const { plusLayout, locale } = useContext(context);
|
|
241
241
|
const [expanded, setExpanded] = useState(false);
|
|
242
242
|
const tools = useContext(targetTools);
|
|
243
|
-
return plusLayout ? element('div', { className: styles.spriteProperties }, element('
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
return plusLayout ? element('div', { className: styles.spriteProperties }, inEditor ? element('span', null, isJapanese(locale) ? 'スプライト' : 'Sprite') :
|
|
244
|
+
element('button', {
|
|
245
|
+
type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
|
|
246
|
+
onClick: () => setExpanded(open => !open)
|
|
247
|
+
}, isJapanese(locale) ? 'スプライト' : 'Sprite'), element('div', { ref: tools?.[1] }), !inEditor && element('div', { className: styles.spritePropertiesContent, hidden: !expanded }, children)) : fallback;
|
|
247
248
|
}
|
|
248
249
|
function useSpriteListTools() {
|
|
249
250
|
const { locale, targetsPinned, setTargetsPinned } = useContext(context);
|
package/dist/targets.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export declare function createTargetList(React: any, styles: any): {
|
|
2
|
-
SpriteGrid: ({ className, compact, standard, stageTile, children, query }: {
|
|
2
|
+
SpriteGrid: ({ className, compact, standard, stageTile, children, query, onRename }: {
|
|
3
3
|
className: any;
|
|
4
4
|
compact: any;
|
|
5
5
|
standard: any;
|
|
6
6
|
stageTile: any;
|
|
7
7
|
children: any;
|
|
8
8
|
query?: string;
|
|
9
|
+
onRename: any;
|
|
9
10
|
}) => any;
|
|
10
11
|
BackdropTile: ({ url, label, selected, highlighted, containerRef, onClick, onMouseEnter, onMouseLeave }: {
|
|
11
12
|
url: any;
|
package/dist/targets.js
CHANGED
|
@@ -4,7 +4,7 @@ const gap = 6;
|
|
|
4
4
|
const padding = gap * 2;
|
|
5
5
|
export function createTargetList(React, styles) {
|
|
6
6
|
const { createElement: element, useLayoutEffect, useRef, useState } = React;
|
|
7
|
-
function SpriteGrid({ className, compact, standard, stageTile, children, query = '' }) {
|
|
7
|
+
function SpriteGrid({ className, compact, standard, stageTile, children, query = '', onRename }) {
|
|
8
8
|
const grid = useRef(null);
|
|
9
9
|
const [size, setSize] = useState({ width: 0, height: 0, visibleRows: 0 });
|
|
10
10
|
useLayoutEffect(() => {
|
|
@@ -53,6 +53,12 @@ export function createTargetList(React, styles) {
|
|
|
53
53
|
const placement = standard || size.width < size.height ? 'bottom-right' :
|
|
54
54
|
(columns >= 2 ? 'top-right' : 'inline');
|
|
55
55
|
const rows = Math.max(size.visibleRows, Math.ceil((React.Children.count(children) + 1) / columns));
|
|
56
|
+
const tiles = onRename ? React.Children.map(children, child => {
|
|
57
|
+
const tile = React.isValidElement(child) && child.props.children;
|
|
58
|
+
if (!React.isValidElement(tile) || !tile.props.id)
|
|
59
|
+
return child;
|
|
60
|
+
return React.cloneElement(child, {}, React.cloneElement(tile, { onRenameButtonClick: onRename }));
|
|
61
|
+
}) : children;
|
|
56
62
|
return element('div', {
|
|
57
63
|
className: compact ? `${className} ${styles.grid}` : className,
|
|
58
64
|
'data-standard-target-grid': standard || undefined,
|
|
@@ -63,7 +69,7 @@ export function createTargetList(React, styles) {
|
|
|
63
69
|
'--target-columns': columns,
|
|
64
70
|
'--target-rows-before-last': rows > 1 ? `repeat(${rows - 1}, ${tileHeight}px)` : undefined
|
|
65
71
|
} : undefined
|
|
66
|
-
},
|
|
72
|
+
}, tiles, stageTile && element('div', { className: styles.backdropCell, key: 'stage' }, stageTile));
|
|
67
73
|
}
|
|
68
74
|
function BackdropTile({ url, label, selected, highlighted, containerRef, onClick, onMouseEnter, onMouseLeave }) {
|
|
69
75
|
return element('button', {
|
|
@@ -19,6 +19,10 @@ export interface WorkspaceLayout {
|
|
|
19
19
|
editingView?: string;
|
|
20
20
|
panels: Record<string, PanelReference>;
|
|
21
21
|
}
|
|
22
|
+
export declare function workspaceWithoutTransientPanels(grid: Grid, activeGroup: string, transient: ReadonlySet<string>, editingView?: string): {
|
|
23
|
+
grid: Grid;
|
|
24
|
+
activeGroup: string;
|
|
25
|
+
} | undefined;
|
|
22
26
|
/** Rebuild only the supported docked grid and logical panel references from storage. */
|
|
23
27
|
export declare function readWorkspaceLayout(raw: string, project: string): WorkspaceLayout | undefined;
|
|
24
28
|
export {};
|
package/dist/workspace-layout.js
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import { Orientation } from 'dockview';
|
|
2
|
+
export function workspaceWithoutTransientPanels(grid, activeGroup, transient, editingView) {
|
|
3
|
+
if (!transient.size)
|
|
4
|
+
return { grid, activeGroup };
|
|
5
|
+
const groups = new Map();
|
|
6
|
+
const trim = (node) => {
|
|
7
|
+
if (Array.isArray(node.data)) {
|
|
8
|
+
const data = node.data.flatMap(child => {
|
|
9
|
+
const kept = trim(child);
|
|
10
|
+
return kept ? [kept] : [];
|
|
11
|
+
});
|
|
12
|
+
return data.length ? { ...node, data } : undefined;
|
|
13
|
+
}
|
|
14
|
+
const views = node.data.views.filter(id => !transient.has(id));
|
|
15
|
+
if (!views.length)
|
|
16
|
+
return;
|
|
17
|
+
const activeView = views.includes(node.data.activeView) ? node.data.activeView : views[0];
|
|
18
|
+
groups.set(node.data.id, views);
|
|
19
|
+
return { ...node, data: { ...node.data, views, activeView } };
|
|
20
|
+
};
|
|
21
|
+
const root = trim(grid.root);
|
|
22
|
+
if (!root)
|
|
23
|
+
return;
|
|
24
|
+
if (groups.has(activeGroup))
|
|
25
|
+
return { grid: { ...grid, root }, activeGroup };
|
|
26
|
+
const fallback = [...groups].find(([, views]) => editingView && views.includes(editingView));
|
|
27
|
+
return { grid: { ...grid, root }, activeGroup: fallback?.[0] ?? groups.keys().next().value };
|
|
28
|
+
}
|
|
2
29
|
const limit = 256;
|
|
3
30
|
const validId = (value) => typeof value === 'string' && value.length > 0 && value.length <= 1024;
|
|
4
31
|
const size = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100000;
|
package/package.json
CHANGED
package/src/editor.css
CHANGED
|
@@ -191,6 +191,17 @@
|
|
|
191
191
|
min-block-size: 0;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/* The native stage marker is shared by the editor, fullscreen and player entries. */
|
|
195
|
+
:global([data-gui-stage]) {
|
|
196
|
+
--stage-toolbar-inset: 5.75rem;
|
|
197
|
+
--stage-overlay-z-index: 500;
|
|
198
|
+
--stage-overlay-gap: 0.5rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
:global([data-gui-stage="fullscreen"]) {
|
|
202
|
+
--stage-toolbar-inset: 2.75rem;
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
.stage-frame:not([data-fullscreen]) {
|
|
195
206
|
padding: 0 0.25rem 0.25rem;
|
|
196
207
|
border: 1px solid var(--ui-black-transparent);
|
|
@@ -1,4 +1,101 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import {SpritePropertiesPanel, type SpritePropertiesPanelProps} from './sprite-properties.tsx';
|
|
3
|
+
import type {SpriteAssetPreviewItem} from './sprite-tabs.ts';
|
|
4
|
+
|
|
5
|
+
interface NativeAssetPanelProps {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
buttons: readonly {title: string; img: string; onClick(): void}[];
|
|
8
|
+
items: readonly {dragPayload?: object; name: string; url?: string; details?: React.ReactNode}[];
|
|
9
|
+
isRtl?: boolean;
|
|
10
|
+
listActions?: React.ElementType;
|
|
11
|
+
selectedItemIndex: number;
|
|
12
|
+
onItemClick(index: number): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface NativeSelectorProps extends NativeAssetPanelProps {
|
|
16
|
+
wrapperAttributes?: Record<string, string>;
|
|
17
|
+
listRef?(node: HTMLElement | null): void;
|
|
18
|
+
onListScroll?(event: React.UIEvent<HTMLElement>): void;
|
|
19
|
+
draggingType?: null;
|
|
20
|
+
onAddSortable?(): void;
|
|
21
|
+
onRemoveSortable?(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type NativeAssetSelector = React.ComponentType<NativeSelectorProps>;
|
|
25
|
+
export type NativeAssetPanelRenderer = (props: NativeAssetPanelProps, options: {
|
|
26
|
+
panelAttributes: Record<string, string>;
|
|
27
|
+
detailAttributes: Record<string, string>;
|
|
28
|
+
renderSelector(Component: NativeAssetSelector, props: NativeSelectorProps): React.ReactNode;
|
|
29
|
+
}) => React.ReactNode;
|
|
30
|
+
|
|
31
|
+
export type SpriteAssetLayout = (items: readonly SpriteAssetPreviewItem[], index: number,
|
|
32
|
+
detail: React.ReactNode, scrollTop: number) => React.ReactNode;
|
|
33
|
+
|
|
34
|
+
interface PanelState {
|
|
35
|
+
rememberLayout(render: SpriteAssetLayout): void;
|
|
36
|
+
getScrollTop(): number;
|
|
37
|
+
rememberScrollTop(position: number): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const noop = () => {};
|
|
41
|
+
|
|
42
|
+
function NativeAssetPanelSurface({render, panel, state, previewSelector, scrollTop}: {
|
|
43
|
+
render: NativeAssetPanelRenderer;
|
|
44
|
+
panel: NativeAssetPanelProps;
|
|
45
|
+
state?: PanelState;
|
|
46
|
+
previewSelector?: NativeAssetSelector;
|
|
47
|
+
scrollTop?: number;
|
|
48
|
+
}) {
|
|
49
|
+
const list = React.useRef<HTMLElement | null>(null);
|
|
50
|
+
const setList = React.useCallback((node: HTMLElement | null) => {
|
|
51
|
+
list.current = node;
|
|
52
|
+
}, []);
|
|
53
|
+
React.useLayoutEffect(() => {
|
|
54
|
+
const position = state?.getScrollTop() ?? scrollTop;
|
|
55
|
+
if (list.current && position !== undefined) list.current.scrollTop = position;
|
|
56
|
+
}, [state, scrollTop]);
|
|
57
|
+
|
|
58
|
+
return render(panel, {
|
|
59
|
+
panelAttributes: {'data-layout-asset-panel': ''},
|
|
60
|
+
detailAttributes: {'data-layout-asset-detail': ''},
|
|
61
|
+
renderSelector(Component, props) {
|
|
62
|
+
const selectorProps = {...props, listRef: setList, wrapperAttributes: {'data-layout-asset-list': ''}};
|
|
63
|
+
if (previewSelector) {
|
|
64
|
+
return <div {...{inert: ''}} style={{display: 'contents'}}>
|
|
65
|
+
{React.createElement(previewSelector, {
|
|
66
|
+
...selectorProps, draggingType: null, onAddSortable: noop, onRemoveSortable: noop
|
|
67
|
+
})}
|
|
68
|
+
</div>;
|
|
69
|
+
}
|
|
70
|
+
return <Component {...selectorProps} onListScroll={event => {
|
|
71
|
+
state?.rememberScrollTop(event.currentTarget.scrollTop);
|
|
72
|
+
}} />;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function NativeAssetPanel({render, previewSelector, panel, state}: {
|
|
78
|
+
render: NativeAssetPanelRenderer;
|
|
79
|
+
previewSelector: NativeAssetSelector;
|
|
80
|
+
panel: NativeAssetPanelProps;
|
|
81
|
+
state: PanelState;
|
|
82
|
+
}) {
|
|
83
|
+
const {items, buttons, isRtl, listActions} = panel;
|
|
84
|
+
React.useLayoutEffect(() => {
|
|
85
|
+
const previousItems = new Map(items.map(item => [item.dragPayload, item]));
|
|
86
|
+
const readOnlyButtons = buttons.map(({title, img}) => ({title, img, onClick: noop}));
|
|
87
|
+
state.rememberLayout((previewItems, selectedItemIndex, detail, scrollTop) => <NativeAssetPanelSurface
|
|
88
|
+
render={render} previewSelector={previewSelector} scrollTop={scrollTop} panel={{
|
|
89
|
+
isRtl, listActions, selectedItemIndex, buttons: readOnlyButtons,
|
|
90
|
+
onItemClick: noop, children: detail,
|
|
91
|
+
items: previewItems.map(item => {
|
|
92
|
+
const previous = previousItems.get(item.asset);
|
|
93
|
+
return {name: item.name, url: item.imageUrl || previous?.url, details: previous?.details};
|
|
94
|
+
})
|
|
95
|
+
}} />);
|
|
96
|
+
}, [state, render, previewSelector, items, buttons, isRtl, listActions]);
|
|
97
|
+
return <NativeAssetPanelSurface render={render} panel={panel} state={state} />;
|
|
98
|
+
}
|
|
2
99
|
|
|
3
100
|
export interface SpriteMediaAsset {
|
|
4
101
|
/** Host identity for this item within its target, independent of the list order. */
|
|
@@ -35,6 +132,9 @@ export interface SpriteMediaPaneProps extends PaneProps {
|
|
|
35
132
|
export interface SpriteVariablePaneProps extends PaneProps {
|
|
36
133
|
kind: 'variables';
|
|
37
134
|
variables: readonly SpriteVariable[];
|
|
135
|
+
properties?: SpritePropertiesPanelProps['values'];
|
|
136
|
+
targetId?: string;
|
|
137
|
+
onWriteProperties?: SpritePropertiesPanelProps['onChange'];
|
|
38
138
|
onWrite(id: string, value: SpriteVariable['value']): void;
|
|
39
139
|
}
|
|
40
140
|
|
|
@@ -148,9 +248,12 @@ function VariableField({variable, editable, onWrite}: {
|
|
|
148
248
|
}} />;
|
|
149
249
|
}
|
|
150
250
|
|
|
151
|
-
function VariablePane({variables, editable, onWrite, locale}: SpriteVariablePaneProps) {
|
|
251
|
+
function VariablePane({variables, editable, onWrite, locale, properties, targetId, onWriteProperties}: SpriteVariablePaneProps) {
|
|
152
252
|
const labels = labelsFor(locale);
|
|
153
253
|
return <section className="tw-sprite-variables" aria-label={labels.variables}>
|
|
254
|
+
{properties && targetId && onWriteProperties &&
|
|
255
|
+
<SpritePropertiesPanel targetId={targetId} values={properties} locale={locale}
|
|
256
|
+
disabled={!editable} onChange={onWriteProperties} />}
|
|
154
257
|
{variables.length ? <table className="tw-sprite-variables__table">
|
|
155
258
|
<thead><tr><th scope="col">{labels.name}</th><th scope="col">{labels.value}</th></tr></thead>
|
|
156
259
|
<tbody>{variables.map(variable => <tr key={variable.id}>
|
|
@@ -23,6 +23,7 @@ interface SpriteEditorToolbarProps {
|
|
|
23
23
|
activeGroup?: boolean;
|
|
24
24
|
search?: SpriteEditorSearch;
|
|
25
25
|
availableSections?: readonly SpriteEditorSection[];
|
|
26
|
+
propertiesInVariables?: boolean;
|
|
26
27
|
actions?: React.ReactNode;
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -95,10 +96,10 @@ function SearchBox({viewId, section, japanese, search}: {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
export function SpriteEditorToolbar({viewId, locale, section, onSectionChange, isStage, activeGroup, search,
|
|
98
|
-
availableSections = spriteEditorSections, actions}: SpriteEditorToolbarProps) {
|
|
99
|
+
availableSections = spriteEditorSections, propertiesInVariables = false, actions}: SpriteEditorToolbarProps) {
|
|
99
100
|
const japanese = /^ja(?:-|$)/i.test(locale);
|
|
100
|
-
const labels = japanese ? {code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: '変数'} :
|
|
101
|
-
{code: 'Code', costumes: isStage ? 'Backdrops' : 'Costumes', sounds: 'Sounds', variables: 'Variables'};
|
|
101
|
+
const labels = japanese ? {code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: propertiesInVariables ? 'プロパティ・変数' : '変数'} :
|
|
102
|
+
{code: 'Code', costumes: isStage ? 'Backdrops' : 'Costumes', sounds: 'Sounds', variables: propertiesInVariables ? 'Properties & Variables' : 'Variables'};
|
|
102
103
|
return <div className="tw-plus-sprite-toolbar" data-active-group={activeGroup}>
|
|
103
104
|
<div className="tw-plus-sprite-toolbar-sections" role="tablist"
|
|
104
105
|
aria-label={japanese ? 'スプライトの編集内容' : 'Sprite editor sections'}>
|
package/src/sprite-tabs.ts
CHANGED
|
@@ -3,14 +3,16 @@ import {createPortal} from 'react-dom';
|
|
|
3
3
|
import {createDockview, DefaultTab, themeLight} from 'dockview';
|
|
4
4
|
import type {DockviewGroupPanel, GroupPanelPartInitParameters, IDockviewPanel, SerializedDockview} from 'dockview';
|
|
5
5
|
import {createSpriteLayoutMenu} from './sprite-layout-menu.ts';
|
|
6
|
-
import {SpriteAssetPane} from './sprite-asset-pane.tsx';
|
|
6
|
+
import {NativeAssetPanel, SpriteAssetPane} from './sprite-asset-pane.tsx';
|
|
7
|
+
import type {NativeAssetPanelRenderer, NativeAssetSelector, SpriteAssetLayout} from './sprite-asset-pane.tsx';
|
|
7
8
|
import type {SpriteVariable} from './sprite-asset-pane.tsx';
|
|
9
|
+
import type {SpritePropertiesPanelProps} from './sprite-properties.tsx';
|
|
8
10
|
import {SpriteEditorToolbar, spriteEditorSections} from './sprite-editor-toolbar.tsx';
|
|
9
11
|
import type {SpriteSearchResult} from './sprite-editor-toolbar.tsx';
|
|
10
12
|
import type {DockviewStagePane} from './dockview-stage.ts';
|
|
11
13
|
import {getDocumentWorkspace} from './document-workspace.ts';
|
|
12
14
|
import type {DocumentPanelRequest} from './document-workspace.ts';
|
|
13
|
-
import {readWorkspaceLayout} from './workspace-layout.ts';
|
|
15
|
+
import {readWorkspaceLayout, workspaceWithoutTransientPanels} from './workspace-layout.ts';
|
|
14
16
|
import type {PanelReference, WorkspaceLayout} from './workspace-layout.ts';
|
|
15
17
|
|
|
16
18
|
export {getDocumentWorkspace} from './document-workspace.ts';
|
|
@@ -61,6 +63,8 @@ export interface SpriteTabAdapter<Snapshot = unknown> {
|
|
|
61
63
|
export interface SpriteAssetSelection {
|
|
62
64
|
restore(target: object, assets: readonly object[]): number | undefined;
|
|
63
65
|
remember(target: object, asset: object | undefined, select?: (index: number) => void): void;
|
|
66
|
+
renderPanel?(render: NativeAssetPanelRenderer, previewSelector: NativeAssetSelector,
|
|
67
|
+
panel: React.ComponentProps<typeof NativeAssetPanel>['panel']): React.ReactNode;
|
|
64
68
|
rememberLayout?(render: SpriteAssetLayout): void;
|
|
65
69
|
getScrollTop?(): number;
|
|
66
70
|
rememberScrollTop?(position: number): void;
|
|
@@ -77,7 +81,6 @@ export interface SpriteAssetPreviewItem {
|
|
|
77
81
|
duration?: number;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
type SpriteAssetLayout = (items: readonly SpriteAssetPreviewItem[], index: number, detail: React.ReactNode, scrollTop: number) => React.ReactNode;
|
|
81
84
|
const variableSection = spriteEditorSections.indexOf('variables');
|
|
82
85
|
|
|
83
86
|
export interface SpriteAssetPreviewData {
|
|
@@ -95,26 +98,33 @@ function createAssetSelections(target: Target, source?: {
|
|
|
95
98
|
const layouts = new Map(source?.layouts);
|
|
96
99
|
const scroll = new Map(source?.scroll);
|
|
97
100
|
const selectors = new Map<AssetKind, (index: number) => void>();
|
|
98
|
-
const forKind = (kind: AssetKind): SpriteAssetSelection =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
const forKind = (kind: AssetKind): SpriteAssetSelection => {
|
|
102
|
+
const state = {
|
|
103
|
+
rememberLayout(render: SpriteAssetLayout) {
|
|
104
|
+
layouts.set(kind, render);
|
|
105
|
+
},
|
|
106
|
+
getScrollTop: () => scroll.get(kind) ?? 0,
|
|
107
|
+
rememberScrollTop(position: number) {
|
|
108
|
+
scroll.set(kind, position);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
...state,
|
|
113
|
+
renderPanel: (render, previewSelector, panel) =>
|
|
114
|
+
React.createElement(NativeAssetPanel, {render, previewSelector, panel, state}),
|
|
115
|
+
restore(current, assets) {
|
|
116
|
+
if (current !== target) return;
|
|
117
|
+
const index = assets.indexOf(selected.get(kind));
|
|
118
|
+
if (index >= 0) return index;
|
|
119
|
+
},
|
|
120
|
+
remember(current, asset, select) {
|
|
121
|
+
if (current !== target) return;
|
|
122
|
+
if (asset) selected.set(kind, asset);
|
|
123
|
+
else selected.delete(kind);
|
|
124
|
+
if (select) selectors.set(kind, select);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
};
|
|
118
128
|
const ports: AssetSelections = {costumes: forKind('costumes'), sounds: forKind('sounds')};
|
|
119
129
|
return {selected, ports, layouts, selectors, scroll};
|
|
120
130
|
}
|
|
@@ -135,7 +145,7 @@ interface TabOptions<Snapshot = unknown> {
|
|
|
135
145
|
reopenLabel: string;
|
|
136
146
|
multiView?: boolean;
|
|
137
147
|
layoutStorage?: {
|
|
138
|
-
storage: Storage
|
|
148
|
+
storage: Pick<Storage, 'getItem' | 'setItem'>;
|
|
139
149
|
key: string;
|
|
140
150
|
isProjectLoading?(): boolean;
|
|
141
151
|
subscribeProject?(listener: () => void): (() => void) | undefined;
|
|
@@ -356,12 +366,11 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
356
366
|
return {...menu, init() {}};
|
|
357
367
|
}
|
|
358
368
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
369
|
+
const canRename = (target: Target) =>
|
|
370
|
+
target.isOriginal && !target.isStage && Boolean(vm.renameSprite) && vm.runtime.targets.includes(target);
|
|
362
371
|
|
|
363
372
|
function openRenameDialog(target: Target) {
|
|
364
|
-
if (renameDialog || !
|
|
373
|
+
if (renameDialog || !canRename(target)) return;
|
|
365
374
|
const dialog = document.createElement('dialog');
|
|
366
375
|
dialog.className = 'tw-plus-sprite-rename';
|
|
367
376
|
dialog.setAttribute('aria-label', text.title);
|
|
@@ -387,7 +396,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
387
396
|
dialog.remove();
|
|
388
397
|
if (renameDialog !== dialog) return;
|
|
389
398
|
renameDialog = undefined;
|
|
390
|
-
if (dialog.returnValue === 'save' && input.value.trim() &&
|
|
399
|
+
if (dialog.returnValue === 'save' && input.value.trim() && canRename(target) &&
|
|
391
400
|
input.value !== target.getName()) vm.renameSprite?.(target.id, input.value);
|
|
392
401
|
}, {once: true});
|
|
393
402
|
renameDialog = dialog;
|
|
@@ -671,8 +680,14 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
671
680
|
panels[id] = {kind: 'sprite', name: targetName(view.target), stage: view.target.isStage, section: view.section};
|
|
672
681
|
}
|
|
673
682
|
if (dockview.getPanel(stagePanelId)) panels[stagePanelId] = {kind: 'stage'};
|
|
674
|
-
|
|
675
|
-
const
|
|
683
|
+
const transient = new Set<string>();
|
|
684
|
+
for (const [id, {request}] of documents) {
|
|
685
|
+
if (request.transient) transient.add(id);
|
|
686
|
+
else panels[id] = {kind: 'document', ...request.reference};
|
|
687
|
+
}
|
|
688
|
+
const saved = workspaceWithoutTransientPanels(grid, activeGroup, transient, active?.id);
|
|
689
|
+
if (!saved) return;
|
|
690
|
+
const snapshot: WorkspaceLayout = {version: 1, project: projectSignature(targets), ...saved,
|
|
676
691
|
editingView: active?.id, panels};
|
|
677
692
|
try { persistence.storage.setItem(persistence.key, JSON.stringify(snapshot)); } catch { /* Storage can be unavailable. */ }
|
|
678
693
|
}
|
|
@@ -841,6 +856,10 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
|
|
|
841
856
|
refresh();
|
|
842
857
|
return {
|
|
843
858
|
select, openStage, closeStage, split, closeSplit, activateView, setViewSection, syncActiveSection, refreshSurfaces,
|
|
859
|
+
rename(id: string) {
|
|
860
|
+
const target = vm.runtime.targets.find(item => item.id === id);
|
|
861
|
+
if (target) openRenameDialog(target);
|
|
862
|
+
},
|
|
844
863
|
selectAsset(id: string, kind: AssetKind, index: number) {
|
|
845
864
|
if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
|
|
846
865
|
active.assets.selectors.get(kind)?.(index);
|
|
@@ -891,6 +910,8 @@ type SpriteTabsProps<Snapshot> = Omit<TabOptions<Snapshot>, 'activateSection' |
|
|
|
891
910
|
renderToolbar?(view: SpriteTabView, actions: ToolbarActions): React.ReactNode;
|
|
892
911
|
getAssetPreview?(targetId: string, kind: SpriteAssetKind): SpriteAssetPreviewData;
|
|
893
912
|
getVariables?(targetId: string): readonly SpriteVariable[];
|
|
913
|
+
getProperties?(targetId: string): SpritePropertiesPanelProps['values'];
|
|
914
|
+
writeProperties?: SpritePropertiesPanelProps['onChange'];
|
|
894
915
|
writeVariable?(targetId: string, id: string, value: SpriteVariable['value']): void;
|
|
895
916
|
getCodeSearchItems?(targetId: string): readonly SpriteSearchResult[];
|
|
896
917
|
openCodeSearchResult?(targetId: string, id: string): void;
|
|
@@ -1039,7 +1060,7 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
1039
1060
|
const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
|
|
1040
1061
|
React.createElement(SpriteEditorToolbar, {
|
|
1041
1062
|
viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
|
|
1042
|
-
activeGroup: view.groupActive,
|
|
1063
|
+
activeGroup: view.groupActive, propertiesInVariables: Boolean(props.getProperties),
|
|
1043
1064
|
actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
|
|
1044
1065
|
onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
|
|
1045
1066
|
search: {
|
|
@@ -1071,6 +1092,12 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
|
|
|
1071
1092
|
}), assetPreviewSlots.map(({id, targetId, kind, element, editable, selected, renderLayout, scrollTop}) =>
|
|
1072
1093
|
createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
|
|
1073
1094
|
kind, locale: props.locale, editable, variables: props.getVariables?.(targetId) ?? [],
|
|
1095
|
+
properties: props.getProperties?.(targetId),
|
|
1096
|
+
targetId,
|
|
1097
|
+
onWriteProperties(id: string, patch: Parameters<SpritePropertiesPanelProps['onChange']>[1]) {
|
|
1098
|
+
props.writeProperties?.(id, patch);
|
|
1099
|
+
workspace?.refreshSurfaces();
|
|
1100
|
+
},
|
|
1074
1101
|
onWrite(variableId, value) {
|
|
1075
1102
|
props.writeVariable?.(targetId, variableId, value);
|
|
1076
1103
|
workspace?.refreshSurfaces();
|
|
@@ -1106,7 +1133,10 @@ export function useSpriteTabs<Snapshot>({available, enabled, locale, ...props}:
|
|
|
1106
1133
|
return {
|
|
1107
1134
|
controller,
|
|
1108
1135
|
assetSelections: enabled ? selection.assets : undefined,
|
|
1109
|
-
context: enabled ? {
|
|
1136
|
+
context: enabled ? {
|
|
1137
|
+
selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select,
|
|
1138
|
+
rename: props.multiView && props.getProperties ? (id: string) => controller.current?.rename(id) : undefined
|
|
1139
|
+
} : null,
|
|
1110
1140
|
renderEditorTabRow: available ? (children: React.ReactNode) => React.createElement(SpriteTabs<Snapshot>, {
|
|
1111
1141
|
...props, enabled, locale, children, slots, onAttach: () => setReady(true),
|
|
1112
1142
|
selectTarget: (id, assets) => setSelection({id, assets}),
|
package/src/standard-layout.ts
CHANGED
|
@@ -255,17 +255,18 @@ export function createStandardLayout(React, {styles, targetStyles, context, Layo
|
|
|
255
255
|
return element(targets.BackdropFooter, {buttons: unique, label: isJapanese(locale) ? '背景を追加' : 'Add a backdrop'});
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
function SpritePropertiesSection({fallback, children}) {
|
|
258
|
+
function SpritePropertiesSection({fallback, children, inEditor = false}) {
|
|
259
259
|
const {plusLayout, locale} = useContext(context);
|
|
260
260
|
const [expanded, setExpanded] = useState(false);
|
|
261
261
|
const tools = useContext(targetTools);
|
|
262
262
|
return plusLayout ? element('div', {className: styles.spriteProperties},
|
|
263
|
-
element('
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
inEditor ? element('span', null, isJapanese(locale) ? 'スプライト' : 'Sprite') :
|
|
264
|
+
element('button', {
|
|
265
|
+
type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
|
|
266
|
+
onClick: () => setExpanded(open => !open)
|
|
267
|
+
}, isJapanese(locale) ? 'スプライト' : 'Sprite'),
|
|
267
268
|
element('div', {ref: tools?.[1]}),
|
|
268
|
-
element('div', {className: styles.spritePropertiesContent, hidden: !expanded}, children)
|
|
269
|
+
!inEditor && element('div', {className: styles.spritePropertiesContent, hidden: !expanded}, children)
|
|
269
270
|
) : fallback;
|
|
270
271
|
}
|
|
271
272
|
|
package/src/targets.ts
CHANGED
|
@@ -6,7 +6,7 @@ const padding = gap * 2;
|
|
|
6
6
|
export function createTargetList(React, styles) {
|
|
7
7
|
const {createElement: element, useLayoutEffect, useRef, useState} = React;
|
|
8
8
|
|
|
9
|
-
function SpriteGrid({className, compact, standard, stageTile, children, query = ''}) {
|
|
9
|
+
function SpriteGrid({className, compact, standard, stageTile, children, query = '', onRename}) {
|
|
10
10
|
const grid = useRef(null);
|
|
11
11
|
const [size, setSize] = useState({width: 0, height: 0, visibleRows: 0});
|
|
12
12
|
useLayoutEffect(() => {
|
|
@@ -51,6 +51,11 @@ export function createTargetList(React, styles) {
|
|
|
51
51
|
const placement = standard || size.width < size.height ? 'bottom-right' :
|
|
52
52
|
(columns >= 2 ? 'top-right' : 'inline');
|
|
53
53
|
const rows = Math.max(size.visibleRows, Math.ceil((React.Children.count(children) + 1) / columns));
|
|
54
|
+
const tiles = onRename ? React.Children.map(children, child => {
|
|
55
|
+
const tile = React.isValidElement(child) && child.props.children;
|
|
56
|
+
if (!React.isValidElement(tile) || !tile.props.id) return child;
|
|
57
|
+
return React.cloneElement(child, {}, React.cloneElement(tile, {onRenameButtonClick: onRename}));
|
|
58
|
+
}) : children;
|
|
54
59
|
return element('div', {
|
|
55
60
|
className: compact ? `${className} ${styles.grid}` : className,
|
|
56
61
|
'data-standard-target-grid': standard || undefined,
|
|
@@ -61,7 +66,7 @@ export function createTargetList(React, styles) {
|
|
|
61
66
|
'--target-columns': columns,
|
|
62
67
|
'--target-rows-before-last': rows > 1 ? `repeat(${rows - 1}, ${tileHeight}px)` : undefined
|
|
63
68
|
} : undefined
|
|
64
|
-
},
|
|
69
|
+
}, tiles, stageTile && element('div', {className: styles.backdropCell, key: 'stage'}, stageTile));
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function BackdropTile({url, label, selected, highlighted, containerRef, onClick, onMouseEnter, onMouseLeave}) {
|
package/src/workspace-layout.ts
CHANGED
|
@@ -18,6 +18,31 @@ export interface WorkspaceLayout {
|
|
|
18
18
|
panels: Record<string, PanelReference>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function workspaceWithoutTransientPanels(grid: Grid, activeGroup: string, transient: ReadonlySet<string>,
|
|
22
|
+
editingView?: string): {grid: Grid; activeGroup: string} | undefined {
|
|
23
|
+
if (!transient.size) return {grid, activeGroup};
|
|
24
|
+
const groups = new Map<string, readonly string[]>();
|
|
25
|
+
const trim = (node: Node): Node | undefined => {
|
|
26
|
+
if (Array.isArray(node.data)) {
|
|
27
|
+
const data = node.data.flatMap(child => {
|
|
28
|
+
const kept = trim(child);
|
|
29
|
+
return kept ? [kept] : [];
|
|
30
|
+
});
|
|
31
|
+
return data.length ? {...node, data} : undefined;
|
|
32
|
+
}
|
|
33
|
+
const views = node.data.views.filter(id => !transient.has(id));
|
|
34
|
+
if (!views.length) return;
|
|
35
|
+
const activeView = views.includes(node.data.activeView!) ? node.data.activeView : views[0];
|
|
36
|
+
groups.set(node.data.id, views);
|
|
37
|
+
return {...node, data: {...node.data, views, activeView}};
|
|
38
|
+
};
|
|
39
|
+
const root = trim(grid.root);
|
|
40
|
+
if (!root) return;
|
|
41
|
+
if (groups.has(activeGroup)) return {grid: {...grid, root}, activeGroup};
|
|
42
|
+
const fallback = [...groups].find(([, views]) => editingView && views.includes(editingView));
|
|
43
|
+
return {grid: {...grid, root}, activeGroup: fallback?.[0] ?? groups.keys().next().value!};
|
|
44
|
+
}
|
|
45
|
+
|
|
21
46
|
const limit = 256;
|
|
22
47
|
const validId = (value: unknown): value is string =>
|
|
23
48
|
typeof value === 'string' && value.length > 0 && value.length <= 1024;
|