@kubohiroya/turbowarp-plus-editor-layout 0.1.7 → 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.
@@ -4,6 +4,7 @@ export interface DocumentPanelRequest {
4
4
  documentId: string;
5
5
  };
6
6
  filename: string;
7
+ transient?: boolean;
7
8
  mount(element: HTMLElement, close: () => void): {
8
9
  requestClose(): void;
9
10
  dispose(): void;
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { type SpritePropertiesPanelProps } from './sprite-properties.tsx';
2
3
  import type { SpriteAssetPreviewItem } from './sprite-tabs.ts';
3
4
  interface NativeAssetPanelProps {
4
5
  children?: React.ReactNode;
@@ -75,6 +76,9 @@ export interface SpriteMediaPaneProps extends PaneProps {
75
76
  export interface SpriteVariablePaneProps extends PaneProps {
76
77
  kind: 'variables';
77
78
  variables: readonly SpriteVariable[];
79
+ properties?: SpritePropertiesPanelProps['values'];
80
+ targetId?: string;
81
+ onWriteProperties?: SpritePropertiesPanelProps['onChange'];
78
82
  onWrite(id: string, value: SpriteVariable['value']): void;
79
83
  }
80
84
  export type SpriteAssetPaneProps = SpriteMediaPaneProps | SpriteVariablePaneProps;
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { SpritePropertiesPanel } from "./sprite-properties.js";
2
3
  const noop = () => { };
3
4
  function NativeAssetPanelSurface({ render, panel, state, previewSelector, scrollTop }) {
4
5
  const list = React.useRef(null);
@@ -133,19 +134,22 @@ function VariableField({ variable, editable, onWrite }) {
133
134
  finish(event.key === 'Enter');
134
135
  } });
135
136
  }
136
- function VariablePane({ variables, editable, onWrite, locale }) {
137
+ function VariablePane({ variables, editable, onWrite, locale, properties, targetId, onWriteProperties }) {
137
138
  const labels = labelsFor(locale);
138
- return React.createElement("section", { className: "tw-sprite-variables", "aria-label": labels.variables }, variables.length ? React.createElement("table", { className: "tw-sprite-variables__table" },
139
- React.createElement("thead", null,
140
- React.createElement("tr", null,
141
- React.createElement("th", { scope: "col" }, labels.name),
142
- React.createElement("th", { scope: "col" }, labels.value))),
143
- React.createElement("tbody", null, variables.map(variable => React.createElement("tr", { key: variable.id },
144
- React.createElement("th", { scope: "row" },
145
- variable.name,
146
- variable.scope && React.createElement("span", { className: "tw-sprite-variables__scope" }, labels[variable.scope])),
147
- React.createElement("td", null,
148
- React.createElement(VariableField, { variable: variable, editable: editable, onWrite: onWrite })))))) : React.createElement("p", null, labels.emptyVariables));
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));
149
153
  }
150
154
  export function SpriteAssetPane(props) {
151
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;
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { NativeAssetPanel } from './sprite-asset-pane.tsx';
3
3
  import type { NativeAssetPanelRenderer, NativeAssetSelector, SpriteAssetLayout } from './sprite-asset-pane.tsx';
4
4
  import type { SpriteVariable } from './sprite-asset-pane.tsx';
5
+ import type { SpritePropertiesPanelProps } from './sprite-properties.tsx';
5
6
  import type { SpriteSearchResult } from './sprite-editor-toolbar.tsx';
6
7
  import type { DockviewStagePane } from './dockview-stage.ts';
7
8
  export { getDocumentWorkspace } from './document-workspace.ts';
@@ -80,7 +81,7 @@ interface TabOptions<Snapshot = unknown> {
80
81
  reopenLabel: string;
81
82
  multiView?: boolean;
82
83
  layoutStorage?: {
83
- storage: Storage;
84
+ storage: Pick<Storage, 'getItem' | 'setItem'>;
84
85
  key: string;
85
86
  isProjectLoading?(): boolean;
86
87
  subscribeProject?(listener: () => void): (() => void) | undefined;
@@ -137,6 +138,7 @@ export declare function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HT
137
138
  setViewSection: (id: string, section: number) => boolean;
138
139
  syncActiveSection: (section: number) => void;
139
140
  refreshSurfaces: () => void;
141
+ rename(id: string): void;
140
142
  selectAsset(id: string, kind: AssetKind, index: number): void;
141
143
  currentView: () => SpriteTabView;
142
144
  views: () => SpriteTabView[];
@@ -156,6 +158,8 @@ type SpriteTabsProps<Snapshot> = Omit<TabOptions<Snapshot>, 'activateSection' |
156
158
  renderToolbar?(view: SpriteTabView, actions: ToolbarActions): React.ReactNode;
157
159
  getAssetPreview?(targetId: string, kind: SpriteAssetKind): SpriteAssetPreviewData;
158
160
  getVariables?(targetId: string): readonly SpriteVariable[];
161
+ getProperties?(targetId: string): SpritePropertiesPanelProps['values'];
162
+ writeProperties?: SpritePropertiesPanelProps['onChange'];
159
163
  writeVariable?(targetId: string, id: string, value: SpriteVariable['value']): void;
160
164
  getCodeSearchItems?(targetId: string): readonly SpriteSearchResult[];
161
165
  openCodeSearchResult?(targetId: string, id: string): void;
@@ -175,6 +179,7 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
175
179
  setViewSection: (id: string, section: number) => boolean;
176
180
  syncActiveSection: (section: number) => void;
177
181
  refreshSurfaces: () => void;
182
+ rename(id: string): void;
178
183
  selectAsset(id: string, kind: AssetKind, index: number): void;
179
184
  currentView: () => SpriteTabView;
180
185
  views: () => SpriteTabView[];
@@ -186,6 +191,7 @@ export declare function useSpriteTabs<Snapshot>({ available, enabled, locale, ..
186
191
  openLabel: string;
187
192
  select: (id: string) => void;
188
193
  open: (id: string) => void;
194
+ rename: (id: string) => void;
189
195
  };
190
196
  renderEditorTabRow: (children: React.ReactNode) => React.FunctionComponentElement<SpriteTabsProps<Snapshot>>;
191
197
  renderEditorPanel: (index: number, children: React.ReactNode) => React.ReactPortal;
@@ -5,7 +5,7 @@ import { createSpriteLayoutMenu } from "./sprite-layout-menu.js";
5
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');
@@ -227,11 +227,9 @@ export function mountSpriteTabs(root, content, options) {
227
227
  });
228
228
  return { ...menu, init() { } };
229
229
  }
230
- function isOpen(target) {
231
- return [...views.values()].some(view => view.target === target) && vm.runtime.targets.includes(target);
232
- }
230
+ const canRename = (target) => target.isOriginal && !target.isStage && Boolean(vm.renameSprite) && vm.runtime.targets.includes(target);
233
231
  function openRenameDialog(target) {
234
- if (renameDialog || !isOpen(target))
232
+ if (renameDialog || !canRename(target))
235
233
  return;
236
234
  const dialog = document.createElement('dialog');
237
235
  dialog.className = 'tw-plus-sprite-rename';
@@ -259,7 +257,7 @@ export function mountSpriteTabs(root, content, options) {
259
257
  if (renameDialog !== dialog)
260
258
  return;
261
259
  renameDialog = undefined;
262
- if (dialog.returnValue === 'save' && input.value.trim() && isOpen(target) &&
260
+ if (dialog.returnValue === 'save' && input.value.trim() && canRename(target) &&
263
261
  input.value !== target.getName())
264
262
  vm.renameSprite?.(target.id, input.value);
265
263
  }, { once: true });
@@ -562,9 +560,17 @@ export function mountSpriteTabs(root, content, options) {
562
560
  }
563
561
  if (dockview.getPanel(stagePanelId))
564
562
  panels[stagePanelId] = { kind: 'stage' };
565
- for (const [id, { request }] of documents)
566
- panels[id] = { kind: 'document', ...request.reference };
567
- const snapshot = { version: 1, project: projectSignature(targets), grid, activeGroup,
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,
568
574
  editingView: active?.id, panels };
569
575
  try {
570
576
  persistence.storage.setItem(persistence.key, JSON.stringify(snapshot));
@@ -758,6 +764,11 @@ export function mountSpriteTabs(root, content, options) {
758
764
  refresh();
759
765
  return {
760
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
+ },
761
772
  selectAsset(id, kind, index) {
762
773
  if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
763
774
  active.assets.selectors.get(kind)?.(index);
@@ -934,7 +945,7 @@ function SpriteTabs(props) {
934
945
  const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
935
946
  React.createElement(SpriteEditorToolbar, {
936
947
  viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
937
- activeGroup: view.groupActive,
948
+ activeGroup: view.groupActive, propertiesInVariables: Boolean(props.getProperties),
938
949
  actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
939
950
  onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
940
951
  search: {
@@ -969,6 +980,12 @@ function SpriteTabs(props) {
969
980
  return createPortal(toolbar, element, view.id);
970
981
  }), assetPreviewSlots.map(({ id, targetId, kind, element, editable, selected, renderLayout, scrollTop }) => createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
971
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
+ },
972
989
  onWrite(variableId, value) {
973
990
  props.writeVariable?.(targetId, variableId, value);
974
991
  workspace?.refreshSurfaces();
@@ -998,7 +1015,10 @@ export function useSpriteTabs({ available, enabled, locale, ...props }) {
998
1015
  return {
999
1016
  controller,
1000
1017
  assetSelections: enabled ? selection.assets : undefined,
1001
- context: enabled ? { selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select } : null,
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,
1002
1022
  renderEditorTabRow: available ? (children) => React.createElement((SpriteTabs), {
1003
1023
  ...props, enabled, locale, children, slots, onAttach: () => setReady(true),
1004
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
  };
@@ -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('button', {
244
- type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
245
- onClick: () => setExpanded(open => !open)
246
- }, isJapanese(locale) ? 'スプライト' : 'Sprite'), element('div', { ref: tools?.[1] }), element('div', { className: styles.spritePropertiesContent, hidden: !expanded }, children)) : fallback;
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
- }, children, stageTile && element('div', { className: styles.backdropCell, key: 'stage' }, stageTile));
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 {};
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-layout",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Resizable panes and layout for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -1,6 +1,7 @@
1
1
  export interface DocumentPanelRequest {
2
2
  reference: {providerId: string; documentId: string};
3
3
  filename: string;
4
+ transient?: boolean;
4
5
  mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
5
6
  }
6
7
 
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,5 @@
1
1
  import React from 'react';
2
+ import {SpritePropertiesPanel, type SpritePropertiesPanelProps} from './sprite-properties.tsx';
2
3
  import type {SpriteAssetPreviewItem} from './sprite-tabs.ts';
3
4
 
4
5
  interface NativeAssetPanelProps {
@@ -131,6 +132,9 @@ export interface SpriteMediaPaneProps extends PaneProps {
131
132
  export interface SpriteVariablePaneProps extends PaneProps {
132
133
  kind: 'variables';
133
134
  variables: readonly SpriteVariable[];
135
+ properties?: SpritePropertiesPanelProps['values'];
136
+ targetId?: string;
137
+ onWriteProperties?: SpritePropertiesPanelProps['onChange'];
134
138
  onWrite(id: string, value: SpriteVariable['value']): void;
135
139
  }
136
140
 
@@ -244,9 +248,12 @@ function VariableField({variable, editable, onWrite}: {
244
248
  }} />;
245
249
  }
246
250
 
247
- function VariablePane({variables, editable, onWrite, locale}: SpriteVariablePaneProps) {
251
+ function VariablePane({variables, editable, onWrite, locale, properties, targetId, onWriteProperties}: SpriteVariablePaneProps) {
248
252
  const labels = labelsFor(locale);
249
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} />}
250
257
  {variables.length ? <table className="tw-sprite-variables__table">
251
258
  <thead><tr><th scope="col">{labels.name}</th><th scope="col">{labels.value}</th></tr></thead>
252
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'}>
@@ -6,12 +6,13 @@ import {createSpriteLayoutMenu} from './sprite-layout-menu.ts';
6
6
  import {NativeAssetPanel, SpriteAssetPane} from './sprite-asset-pane.tsx';
7
7
  import type {NativeAssetPanelRenderer, NativeAssetSelector, SpriteAssetLayout} from './sprite-asset-pane.tsx';
8
8
  import type {SpriteVariable} from './sprite-asset-pane.tsx';
9
+ import type {SpritePropertiesPanelProps} from './sprite-properties.tsx';
9
10
  import {SpriteEditorToolbar, spriteEditorSections} from './sprite-editor-toolbar.tsx';
10
11
  import type {SpriteSearchResult} from './sprite-editor-toolbar.tsx';
11
12
  import type {DockviewStagePane} from './dockview-stage.ts';
12
13
  import {getDocumentWorkspace} from './document-workspace.ts';
13
14
  import type {DocumentPanelRequest} from './document-workspace.ts';
14
- import {readWorkspaceLayout} from './workspace-layout.ts';
15
+ import {readWorkspaceLayout, workspaceWithoutTransientPanels} from './workspace-layout.ts';
15
16
  import type {PanelReference, WorkspaceLayout} from './workspace-layout.ts';
16
17
 
17
18
  export {getDocumentWorkspace} from './document-workspace.ts';
@@ -144,7 +145,7 @@ interface TabOptions<Snapshot = unknown> {
144
145
  reopenLabel: string;
145
146
  multiView?: boolean;
146
147
  layoutStorage?: {
147
- storage: Storage;
148
+ storage: Pick<Storage, 'getItem' | 'setItem'>;
148
149
  key: string;
149
150
  isProjectLoading?(): boolean;
150
151
  subscribeProject?(listener: () => void): (() => void) | undefined;
@@ -365,12 +366,11 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
365
366
  return {...menu, init() {}};
366
367
  }
367
368
 
368
- function isOpen(target: Target) {
369
- return [...views.values()].some(view => view.target === target) && vm.runtime.targets.includes(target);
370
- }
369
+ const canRename = (target: Target) =>
370
+ target.isOriginal && !target.isStage && Boolean(vm.renameSprite) && vm.runtime.targets.includes(target);
371
371
 
372
372
  function openRenameDialog(target: Target) {
373
- if (renameDialog || !isOpen(target)) return;
373
+ if (renameDialog || !canRename(target)) return;
374
374
  const dialog = document.createElement('dialog');
375
375
  dialog.className = 'tw-plus-sprite-rename';
376
376
  dialog.setAttribute('aria-label', text.title);
@@ -396,7 +396,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
396
396
  dialog.remove();
397
397
  if (renameDialog !== dialog) return;
398
398
  renameDialog = undefined;
399
- if (dialog.returnValue === 'save' && input.value.trim() && isOpen(target) &&
399
+ if (dialog.returnValue === 'save' && input.value.trim() && canRename(target) &&
400
400
  input.value !== target.getName()) vm.renameSprite?.(target.id, input.value);
401
401
  }, {once: true});
402
402
  renameDialog = dialog;
@@ -680,8 +680,14 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
680
680
  panels[id] = {kind: 'sprite', name: targetName(view.target), stage: view.target.isStage, section: view.section};
681
681
  }
682
682
  if (dockview.getPanel(stagePanelId)) panels[stagePanelId] = {kind: 'stage'};
683
- for (const [id, {request}] of documents) panels[id] = {kind: 'document', ...request.reference};
684
- const snapshot: WorkspaceLayout = {version: 1, project: projectSignature(targets), grid, activeGroup,
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,
685
691
  editingView: active?.id, panels};
686
692
  try { persistence.storage.setItem(persistence.key, JSON.stringify(snapshot)); } catch { /* Storage can be unavailable. */ }
687
693
  }
@@ -850,6 +856,10 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
850
856
  refresh();
851
857
  return {
852
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
+ },
853
863
  selectAsset(id: string, kind: AssetKind, index: number) {
854
864
  if (active?.id === id && active.section === (kind === 'costumes' ? 1 : 2)) {
855
865
  active.assets.selectors.get(kind)?.(index);
@@ -900,6 +910,8 @@ type SpriteTabsProps<Snapshot> = Omit<TabOptions<Snapshot>, 'activateSection' |
900
910
  renderToolbar?(view: SpriteTabView, actions: ToolbarActions): React.ReactNode;
901
911
  getAssetPreview?(targetId: string, kind: SpriteAssetKind): SpriteAssetPreviewData;
902
912
  getVariables?(targetId: string): readonly SpriteVariable[];
913
+ getProperties?(targetId: string): SpritePropertiesPanelProps['values'];
914
+ writeProperties?: SpritePropertiesPanelProps['onChange'];
903
915
  writeVariable?(targetId: string, id: string, value: SpriteVariable['value']): void;
904
916
  getCodeSearchItems?(targetId: string): readonly SpriteSearchResult[];
905
917
  openCodeSearchResult?(targetId: string, id: string): void;
@@ -1048,7 +1060,7 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
1048
1060
  const toolbar = props.renderToolbar?.(view, actions) ?? (props.getVariables &&
1049
1061
  React.createElement(SpriteEditorToolbar, {
1050
1062
  viewId: view.id, locale: props.locale, section: currentSection, isStage: view.isStage,
1051
- activeGroup: view.groupActive,
1063
+ activeGroup: view.groupActive, propertiesInVariables: Boolean(props.getProperties),
1052
1064
  actions: view.active && currentSection !== 'variables' ? props.activeTools : undefined,
1053
1065
  onSectionChange: section => actions.setSection(spriteEditorSections.indexOf(section)),
1054
1066
  search: {
@@ -1080,6 +1092,12 @@ function SpriteTabs<Snapshot>(props: SpriteTabsProps<Snapshot>) {
1080
1092
  }), assetPreviewSlots.map(({id, targetId, kind, element, editable, selected, renderLayout, scrollTop}) =>
1081
1093
  createPortal(kind === 'variables' ? React.createElement(SpriteAssetPane, {
1082
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
+ },
1083
1101
  onWrite(variableId, value) {
1084
1102
  props.writeVariable?.(targetId, variableId, value);
1085
1103
  workspace?.refreshSurfaces();
@@ -1115,7 +1133,10 @@ export function useSpriteTabs<Snapshot>({available, enabled, locale, ...props}:
1115
1133
  return {
1116
1134
  controller,
1117
1135
  assetSelections: enabled ? selection.assets : undefined,
1118
- context: enabled ? {selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select} : null,
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,
1119
1140
  renderEditorTabRow: available ? (children: React.ReactNode) => React.createElement(SpriteTabs<Snapshot>, {
1120
1141
  ...props, enabled, locale, children, slots, onAttach: () => setReady(true),
1121
1142
  selectTarget: (id, assets) => setSelection({id, assets}),
@@ -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('button', {
264
- type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
265
- onClick: () => setExpanded(open => !open)
266
- }, isJapanese(locale) ? 'スプライト' : 'Sprite'),
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
- }, children, stageTile && element('div', {className: styles.backdropCell, key: 'stage'}, stageTile));
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}) {
@@ -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;