@kubohiroya/turbowarp-plus-editor-layout 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export declare function observeTileNameTitles(list: HTMLElement | null): () => void;
@@ -0,0 +1,16 @@
1
+ export function observeTileNameTitles(list) {
2
+ if (!list)
3
+ return;
4
+ const update = () => {
5
+ for (const tile of list.querySelectorAll('[class*="sprite-selector-item_sprite-selector-item_"]')) {
6
+ const name = tile.querySelector('[class*="sprite-selector-item_sprite-name_"]')?.textContent ?? '';
7
+ if (tile.title !== name)
8
+ tile.title = name;
9
+ }
10
+ };
11
+ // The native sortable list can mount or replace its tiles after the host renders.
12
+ const observer = new list.ownerDocument.defaultView.MutationObserver(update);
13
+ observer.observe(list, { subtree: true, childList: true, characterData: true });
14
+ update();
15
+ return () => observer.disconnect();
16
+ }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { type SpritePropertiesPanelProps } from './sprite-properties.tsx';
3
- import type { SpriteAssetPreviewItem } from './sprite-tabs.ts';
3
+ import type { SpriteAssetKind, SpriteAssetPreviewItem } from './sprite-tabs.ts';
4
4
  interface NativeAssetPanelProps {
5
5
  children?: React.ReactNode;
6
6
  buttons: readonly {
@@ -39,11 +39,13 @@ interface PanelState {
39
39
  getScrollTop(): number;
40
40
  rememberScrollTop(position: number): void;
41
41
  }
42
- export declare function NativeAssetPanel({ render, previewSelector, panel, state }: {
42
+ export declare function NativeAssetPanel({ render, previewSelector, panel, kind, locale, state }: {
43
43
  render: NativeAssetPanelRenderer;
44
44
  previewSelector: NativeAssetSelector;
45
45
  panel: NativeAssetPanelProps;
46
- state: PanelState;
46
+ kind?: SpriteAssetKind;
47
+ locale?: string;
48
+ state?: PanelState;
47
49
  }): React.JSX.Element;
48
50
  export interface SpriteMediaAsset {
49
51
  /** Host identity for this item within its target, independent of the list order. */
@@ -1,10 +1,14 @@
1
1
  import React from 'react';
2
+ import { observeTileNameTitles } from "./native-tile-title.js";
2
3
  import { SpritePropertiesPanel } from "./sprite-properties.js";
3
4
  const noop = () => { };
4
- function NativeAssetPanelSurface({ render, panel, state, previewSelector, scrollTop }) {
5
+ function NativeAssetPanelSurface({ render, panel, kind, state, previewSelector, scrollTop }) {
5
6
  const list = React.useRef(null);
7
+ const stopObserving = React.useRef();
6
8
  const setList = React.useCallback((node) => {
9
+ stopObserving.current?.();
7
10
  list.current = node;
11
+ stopObserving.current = observeTileNameTitles(node);
8
12
  }, []);
9
13
  React.useLayoutEffect(() => {
10
14
  const position = state?.getScrollTop() ?? scrollTop;
@@ -12,7 +16,7 @@ function NativeAssetPanelSurface({ render, panel, state, previewSelector, scroll
12
16
  list.current.scrollTop = position;
13
17
  }, [state, scrollTop]);
14
18
  return render(panel, {
15
- panelAttributes: { 'data-layout-asset-panel': '' },
19
+ panelAttributes: { 'data-layout-asset-panel': kind ?? '' },
16
20
  detailAttributes: { 'data-layout-asset-detail': '' },
17
21
  renderSelector(Component, props) {
18
22
  const selectorProps = { ...props, listRef: setList, wrapperAttributes: { 'data-layout-asset-list': '' } };
@@ -27,12 +31,14 @@ function NativeAssetPanelSurface({ render, panel, state, previewSelector, scroll
27
31
  }
28
32
  });
29
33
  }
30
- export function NativeAssetPanel({ render, previewSelector, panel, state }) {
34
+ export function NativeAssetPanel({ render, previewSelector, panel, kind, locale = 'en', state }) {
31
35
  const { items, buttons, isRtl, listActions } = panel;
32
36
  React.useLayoutEffect(() => {
37
+ if (!state)
38
+ return;
33
39
  const previousItems = new Map(items.map(item => [item.dragPayload, item]));
34
40
  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: {
41
+ state.rememberLayout((previewItems, selectedItemIndex, detail, scrollTop) => React.createElement(NativeAssetPanelSurface, { render: render, kind: kind, previewSelector: previewSelector, scrollTop: scrollTop, panel: {
36
42
  isRtl, listActions, selectedItemIndex, buttons: readOnlyButtons,
37
43
  onItemClick: noop, children: detail,
38
44
  items: previewItems.map(item => {
@@ -40,8 +46,14 @@ export function NativeAssetPanel({ render, previewSelector, panel, state }) {
40
46
  return { name: item.name, url: item.imageUrl || previous?.url, details: previous?.details };
41
47
  })
42
48
  } }));
43
- }, [state, render, previewSelector, items, buttons, isRtl, listActions]);
44
- return React.createElement(NativeAssetPanelSurface, { render: render, panel: panel, state: state });
49
+ }, [state, render, previewSelector, items, buttons, isRtl, listActions, kind]);
50
+ const activePanel = kind === 'sounds' ? {
51
+ ...panel,
52
+ children: React.createElement(React.Fragment, null,
53
+ items.length === 0 && React.createElement("p", { className: "tw-sprite-media__empty" }, labelsFor(locale).emptySounds),
54
+ panel.children)
55
+ } : panel;
56
+ return React.createElement(NativeAssetPanelSurface, { render: render, panel: activePanel, kind: kind, state: state });
45
57
  }
46
58
  const english = {
47
59
  costumes: 'Costumes', sounds: 'Sounds', variables: 'Variables', name: 'Name', value: 'Value',
@@ -76,7 +88,7 @@ function MediaPane({ kind, items, selectedId, onSelect, renderEditor, editable,
76
88
  const duration = selected?.duration;
77
89
  return React.createElement("section", { className: detailOnly ? 'tw-sprite-media tw-sprite-media--detail' : 'tw-sprite-media', "aria-label": labels[kind] },
78
90
  !detailOnly && React.createElement("ul", { className: "tw-sprite-media__list", "aria-label": labels[kind] }, items.map(asset => React.createElement("li", { key: asset.id },
79
- React.createElement("button", { type: "button", className: "tw-sprite-media__item", "aria-current": asset.id === selectedId || undefined, "aria-disabled": !editable || undefined, tabIndex: editable ? 0 : -1, onClick: () => {
91
+ React.createElement("button", { type: "button", className: "tw-sprite-media__item", title: asset.name, "aria-current": asset.id === selectedId || undefined, "aria-disabled": !editable || undefined, tabIndex: editable ? 0 : -1, onClick: () => {
80
92
  if (editable)
81
93
  onSelect(asset.id);
82
94
  } },
@@ -9,7 +9,7 @@ import { readWorkspaceLayout, workspaceWithoutTransientPanels } from "./workspac
9
9
  export { getDocumentWorkspace } from "./document-workspace.js";
10
10
  export { useDockviewStage } from "./dockview-stage.js";
11
11
  const variableSection = spriteEditorSections.indexOf('variables');
12
- function createAssetSelections(target, source) {
12
+ function createAssetSelections(target, locale, source) {
13
13
  const selected = new Map(source?.selected);
14
14
  const layouts = new Map(source?.layouts);
15
15
  const scroll = new Map(source?.scroll);
@@ -26,7 +26,7 @@ function createAssetSelections(target, source) {
26
26
  };
27
27
  return {
28
28
  ...state,
29
- renderPanel: (render, previewSelector, panel) => React.createElement(NativeAssetPanel, { render, previewSelector, panel, state }),
29
+ renderPanel: (render, previewSelector, panel) => React.createElement(NativeAssetPanel, { render, previewSelector, panel, kind, locale, state }),
30
30
  restore(current, assets) {
31
31
  if (current !== target)
32
32
  return;
@@ -406,7 +406,7 @@ export function mountSpriteTabs(root, content, options) {
406
406
  }
407
407
  const source = splitPosition && views.get(splitPosition.referencePanel);
408
408
  views.set(id, { id, target, section: section ?? remembered.get(target) ?? options.getSection(),
409
- assets: createAssetSelections(target, source?.assets) });
409
+ assets: createAssetSelections(target, locale, source?.assets) });
410
410
  try {
411
411
  return dockview.addPanel({ id, title: target.getName(), component: 'sprite', inactive: !splitPosition,
412
412
  position: splitPosition, params: { icon: getIcon(target) } });
@@ -616,7 +616,7 @@ export function mountSpriteTabs(root, content, options) {
616
616
  const target = targets.find(item => item.isStage === reference.stage && targetName(item) === reference.name);
617
617
  if (!target || id === stagePanelId || (reference.section === variableSection && !options.variables))
618
618
  return finishRestore();
619
- resolvedSprites.set(id, { id, target, section: reference.section, assets: createAssetSelections(target) });
619
+ resolvedSprites.set(id, { id, target, section: reference.section, assets: createAssetSelections(target, locale) });
620
620
  panels[id] = { id, title: target.getName(), contentComponent: 'sprite', params: { icon: getIcon(target) } };
621
621
  }
622
622
  else if (reference.kind === 'stage') {
@@ -997,6 +997,14 @@ function SpriteTabs(props) {
997
997
  export function useSpriteTabs({ available, enabled, locale, ...props }) {
998
998
  const controller = React.useRef(null);
999
999
  const [selection, setSelection] = React.useState({ id: '' });
1000
+ const defaultAssets = React.useMemo(() => {
1001
+ const forKind = (kind) => ({
1002
+ restore: () => undefined,
1003
+ remember() { },
1004
+ renderPanel: (render, previewSelector, panel) => React.createElement(NativeAssetPanel, { render, previewSelector, panel, kind, locale })
1005
+ });
1006
+ return { costumes: forKind('costumes'), sounds: forKind('sounds') };
1007
+ }, [locale]);
1000
1008
  const [ready, setReady] = React.useState(false);
1001
1009
  const [slots] = React.useState(() => available ? Array.from({ length: 3 }, () => {
1002
1010
  const slot = document.createElement('div');
@@ -1014,7 +1022,7 @@ export function useSpriteTabs({ available, enabled, locale, ...props }) {
1014
1022
  const select = (id) => controller.current?.select(id);
1015
1023
  return {
1016
1024
  controller,
1017
- assetSelections: enabled ? selection.assets : undefined,
1025
+ assetSelections: (enabled ? selection.assets : undefined) ?? defaultAssets,
1018
1026
  context: enabled ? {
1019
1027
  selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select,
1020
1028
  rename: props.multiView && props.getProperties ? (id) => controller.current?.rename(id) : undefined
package/dist/targets.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { observeTileNameTitles } from "./native-tile-title.js";
1
2
  const tileHeight = 80;
2
3
  const minTileWidth = 72;
3
4
  const gap = 6;
@@ -26,6 +27,7 @@ export function createTargetList(React, styles) {
26
27
  observer.observe(viewport);
27
28
  return () => observer.disconnect();
28
29
  }, [compact]);
30
+ useLayoutEffect(() => observeTileNameTitles(grid.current), []);
29
31
  useLayoutEffect(() => {
30
32
  const name = query.trim().toLocaleLowerCase();
31
33
  if (!name)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubohiroya/turbowarp-plus-editor-layout",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Resizable panes and layout for TurboWarp Plus Editor",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-or-later",
@@ -0,0 +1,14 @@
1
+ export function observeTileNameTitles(list: HTMLElement | null) {
2
+ if (!list) return;
3
+ const update = () => {
4
+ for (const tile of list.querySelectorAll<HTMLElement>('[class*="sprite-selector-item_sprite-selector-item_"]')) {
5
+ const name = tile.querySelector('[class*="sprite-selector-item_sprite-name_"]')?.textContent ?? '';
6
+ if (tile.title !== name) tile.title = name;
7
+ }
8
+ };
9
+ // The native sortable list can mount or replace its tiles after the host renders.
10
+ const observer = new list.ownerDocument.defaultView.MutationObserver(update);
11
+ observer.observe(list, {subtree: true, childList: true, characterData: true});
12
+ update();
13
+ return () => observer.disconnect();
14
+ }
@@ -1,3 +1,14 @@
1
+ [data-layout-asset-panel="costumes"] [class*="sprite-selector-item_delete-button_"] {
2
+ display: none;
3
+ }
4
+
5
+ .tw-sprite-media__empty {
6
+ margin: auto;
7
+ padding: 1rem;
8
+ color: var(--text-primary, #273146);
9
+ text-align: center;
10
+ }
11
+
1
12
  .tw-sprite-media,
2
13
  .tw-sprite-variables {
3
14
  box-sizing: border-box;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
+ import {observeTileNameTitles} from './native-tile-title.ts';
2
3
  import {SpritePropertiesPanel, type SpritePropertiesPanelProps} from './sprite-properties.tsx';
3
- import type {SpriteAssetPreviewItem} from './sprite-tabs.ts';
4
+ import type {SpriteAssetKind, SpriteAssetPreviewItem} from './sprite-tabs.ts';
4
5
 
5
6
  interface NativeAssetPanelProps {
6
7
  children?: React.ReactNode;
@@ -39,24 +40,27 @@ interface PanelState {
39
40
 
40
41
  const noop = () => {};
41
42
 
42
- function NativeAssetPanelSurface({render, panel, state, previewSelector, scrollTop}: {
43
+ function NativeAssetPanelSurface({render, panel, kind, state, previewSelector, scrollTop}: {
43
44
  render: NativeAssetPanelRenderer;
44
45
  panel: NativeAssetPanelProps;
46
+ kind?: SpriteAssetKind;
45
47
  state?: PanelState;
46
48
  previewSelector?: NativeAssetSelector;
47
49
  scrollTop?: number;
48
50
  }) {
49
51
  const list = React.useRef<HTMLElement | null>(null);
52
+ const stopObserving = React.useRef<() => void>();
50
53
  const setList = React.useCallback((node: HTMLElement | null) => {
54
+ stopObserving.current?.();
51
55
  list.current = node;
56
+ stopObserving.current = observeTileNameTitles(node);
52
57
  }, []);
53
58
  React.useLayoutEffect(() => {
54
59
  const position = state?.getScrollTop() ?? scrollTop;
55
60
  if (list.current && position !== undefined) list.current.scrollTop = position;
56
61
  }, [state, scrollTop]);
57
-
58
62
  return render(panel, {
59
- panelAttributes: {'data-layout-asset-panel': ''},
63
+ panelAttributes: {'data-layout-asset-panel': kind ?? ''},
60
64
  detailAttributes: {'data-layout-asset-detail': ''},
61
65
  renderSelector(Component, props) {
62
66
  const selectorProps = {...props, listRef: setList, wrapperAttributes: {'data-layout-asset-list': ''}};
@@ -74,18 +78,21 @@ function NativeAssetPanelSurface({render, panel, state, previewSelector, scrollT
74
78
  });
75
79
  }
76
80
 
77
- export function NativeAssetPanel({render, previewSelector, panel, state}: {
81
+ export function NativeAssetPanel({render, previewSelector, panel, kind, locale = 'en', state}: {
78
82
  render: NativeAssetPanelRenderer;
79
83
  previewSelector: NativeAssetSelector;
80
84
  panel: NativeAssetPanelProps;
81
- state: PanelState;
85
+ kind?: SpriteAssetKind;
86
+ locale?: string;
87
+ state?: PanelState;
82
88
  }) {
83
89
  const {items, buttons, isRtl, listActions} = panel;
84
90
  React.useLayoutEffect(() => {
91
+ if (!state) return;
85
92
  const previousItems = new Map(items.map(item => [item.dragPayload, item]));
86
93
  const readOnlyButtons = buttons.map(({title, img}) => ({title, img, onClick: noop}));
87
94
  state.rememberLayout((previewItems, selectedItemIndex, detail, scrollTop) => <NativeAssetPanelSurface
88
- render={render} previewSelector={previewSelector} scrollTop={scrollTop} panel={{
95
+ render={render} kind={kind} previewSelector={previewSelector} scrollTop={scrollTop} panel={{
89
96
  isRtl, listActions, selectedItemIndex, buttons: readOnlyButtons,
90
97
  onItemClick: noop, children: detail,
91
98
  items: previewItems.map(item => {
@@ -93,8 +100,15 @@ export function NativeAssetPanel({render, previewSelector, panel, state}: {
93
100
  return {name: item.name, url: item.imageUrl || previous?.url, details: previous?.details};
94
101
  })
95
102
  }} />);
96
- }, [state, render, previewSelector, items, buttons, isRtl, listActions]);
97
- return <NativeAssetPanelSurface render={render} panel={panel} state={state} />;
103
+ }, [state, render, previewSelector, items, buttons, isRtl, listActions, kind]);
104
+ const activePanel = kind === 'sounds' ? {
105
+ ...panel,
106
+ children: <>
107
+ {items.length === 0 && <p className="tw-sprite-media__empty">{labelsFor(locale).emptySounds}</p>}
108
+ {panel.children}
109
+ </>
110
+ } : panel;
111
+ return <NativeAssetPanelSurface render={render} panel={activePanel} kind={kind} state={state} />;
98
112
  }
99
113
 
100
114
  export interface SpriteMediaAsset {
@@ -178,7 +192,7 @@ function MediaPane({kind, items, selectedId, onSelect, renderEditor, editable, l
178
192
  return <section className={detailOnly ? 'tw-sprite-media tw-sprite-media--detail' : 'tw-sprite-media'} aria-label={labels[kind]}>
179
193
  {!detailOnly && <ul className="tw-sprite-media__list" aria-label={labels[kind]}>
180
194
  {items.map(asset => <li key={asset.id}>
181
- <button type="button" className="tw-sprite-media__item" aria-current={asset.id === selectedId || undefined}
195
+ <button type="button" className="tw-sprite-media__item" title={asset.name} aria-current={asset.id === selectedId || undefined}
182
196
  aria-disabled={!editable || undefined} tabIndex={editable ? 0 : -1} onClick={() => {
183
197
  if (editable) onSelect(asset.id);
184
198
  }}>
@@ -89,7 +89,7 @@ export interface SpriteAssetPreviewData {
89
89
  }
90
90
  type AssetSelections = Readonly<Record<AssetKind, SpriteAssetSelection>>;
91
91
 
92
- function createAssetSelections(target: Target, source?: {
92
+ function createAssetSelections(target: Target, locale: string, source?: {
93
93
  selected: Map<AssetKind, object>;
94
94
  layouts: Map<AssetKind, SpriteAssetLayout>;
95
95
  scroll: Map<AssetKind, number>;
@@ -111,7 +111,7 @@ function createAssetSelections(target: Target, source?: {
111
111
  return {
112
112
  ...state,
113
113
  renderPanel: (render, previewSelector, panel) =>
114
- React.createElement(NativeAssetPanel, {render, previewSelector, panel, state}),
114
+ React.createElement(NativeAssetPanel, {render, previewSelector, panel, kind, locale, state}),
115
115
  restore(current, assets) {
116
116
  if (current !== target) return;
117
117
  const index = assets.indexOf(selected.get(kind));
@@ -532,7 +532,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
532
532
  }
533
533
  const source = splitPosition && views.get(splitPosition.referencePanel);
534
534
  views.set(id, {id, target, section: section ?? remembered.get(target) ?? options.getSection(),
535
- assets: createAssetSelections(target, source?.assets)});
535
+ assets: createAssetSelections(target, locale, source?.assets)});
536
536
  try {
537
537
  return dockview.addPanel({id, title: target.getName(), component: 'sprite', inactive: !splitPosition,
538
538
  position: splitPosition, params: {icon: getIcon(target)}});
@@ -728,7 +728,7 @@ export function mountSpriteTabs<Snapshot>(root: HTMLElement, content: HTMLElemen
728
728
  if (reference.kind === 'sprite') {
729
729
  const target = targets.find(item => item.isStage === reference.stage && targetName(item) === reference.name);
730
730
  if (!target || id === stagePanelId || (reference.section === variableSection && !options.variables)) return finishRestore();
731
- resolvedSprites.set(id, {id, target, section: reference.section, assets: createAssetSelections(target)});
731
+ resolvedSprites.set(id, {id, target, section: reference.section, assets: createAssetSelections(target, locale)});
732
732
  panels[id] = {id, title: target.getName(), contentComponent: 'sprite', params: {icon: getIcon(target)}};
733
733
  } else if (reference.kind === 'stage') {
734
734
  if (!stagePane || id !== stagePanelId) return finishRestore();
@@ -1116,6 +1116,15 @@ export function useSpriteTabs<Snapshot>({available, enabled, locale, ...props}:
1116
1116
  }) {
1117
1117
  const controller = React.useRef<SpriteTabsController<Snapshot>>(null);
1118
1118
  const [selection, setSelection] = React.useState<{id: string; assets?: AssetSelections}>({id: ''});
1119
+ const defaultAssets = React.useMemo<AssetSelections>(() => {
1120
+ const forKind = (kind: AssetKind): SpriteAssetSelection => ({
1121
+ restore: () => undefined,
1122
+ remember() {},
1123
+ renderPanel: (render, previewSelector, panel) =>
1124
+ React.createElement(NativeAssetPanel, {render, previewSelector, panel, kind, locale})
1125
+ });
1126
+ return {costumes: forKind('costumes'), sounds: forKind('sounds')};
1127
+ }, [locale]);
1119
1128
  const [ready, setReady] = React.useState(false);
1120
1129
  const [slots] = React.useState(() => available ? Array.from({length: 3}, () => {
1121
1130
  const slot = document.createElement('div');
@@ -1132,7 +1141,7 @@ export function useSpriteTabs<Snapshot>({available, enabled, locale, ...props}:
1132
1141
  const select = (id: string) => controller.current?.select(id);
1133
1142
  return {
1134
1143
  controller,
1135
- assetSelections: enabled ? selection.assets : undefined,
1144
+ assetSelections: (enabled ? selection.assets : undefined) ?? defaultAssets,
1136
1145
  context: enabled ? {
1137
1146
  selectedId: selection.id, openLabel: japanese ? '開く' : 'Open', select, open: select,
1138
1147
  rename: props.multiView && props.getProperties ? (id: string) => controller.current?.rename(id) : undefined
package/src/targets.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import {observeTileNameTitles} from './native-tile-title.ts';
2
+
1
3
  const tileHeight = 80;
2
4
  const minTileWidth = 72;
3
5
  const gap = 6;
@@ -28,6 +30,8 @@ export function createTargetList(React, styles) {
28
30
  return () => observer.disconnect();
29
31
  }, [compact]);
30
32
 
33
+ useLayoutEffect(() => observeTileNameTitles(grid.current), []);
34
+
31
35
  useLayoutEffect(() => {
32
36
  const name = query.trim().toLocaleLowerCase();
33
37
  if (!name) return;