@kubohiroya/turbowarp-plus-editor-layout 0.1.15 → 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
  } },
@@ -51,7 +51,7 @@ function SearchBox({ viewId, section, japanese, search }) {
51
51
  }
52
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: propertiesInVariables ? 'プロパティ・変数' : '変数' } :
54
+ const labels = japanese ? { code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: propertiesInVariables ? '変数など' : '変数' } :
55
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 => {
@@ -67,6 +67,7 @@ export function SpriteEditorToolbar({ viewId, locale, section, onSectionChange,
67
67
  } },
68
68
  React.createElement(Icon, { kind: id }),
69
69
  labels[id]))),
70
- search && React.createElement(SearchBox, { viewId, section, japanese, search }),
71
- actions);
70
+ (search || actions) && React.createElement("div", { className: "tw-plus-sprite-toolbar-tools" },
71
+ actions,
72
+ search && React.createElement(SearchBox, { viewId, section, japanese, search })));
72
73
  }
@@ -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
@@ -260,7 +260,7 @@ export function createStandardLayout(React, { styles, targetStyles, context, Lay
260
260
  const { plusLayout, locale } = useContext(context);
261
261
  const [expanded, setExpanded] = useState(false);
262
262
  const tools = useContext(targetTools);
263
- return plusLayout ? element('div', { className: styles.spriteProperties }, inEditor ? element('span', null, isJapanese(locale) ? 'スプライト' : 'Sprite') :
263
+ return plusLayout ? element('div', { className: styles.spriteProperties }, inEditor ? element('span', { className: styles.spritePropertiesTitle }, isJapanese(locale) ? 'スプライト' : 'Sprite') :
264
264
  element('button', {
265
265
  type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
266
266
  onClick: () => setExpanded(open => !open)
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.15",
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",
package/src/editor.css CHANGED
@@ -429,11 +429,15 @@ details.layout-menu button {
429
429
  background: transparent;
430
430
  color: inherit;
431
431
  font: inherit;
432
- font-size: 0.875rem;
433
432
  text-align: start;
434
433
  cursor: pointer;
435
434
  }
436
435
 
436
+ .sprite-properties-title,
437
+ .sprite-properties-toggle {
438
+ font-size: 0.875rem;
439
+ }
440
+
437
441
  .sprite-properties-content {
438
442
  min-block-size: 0;
439
443
  overflow: auto;
@@ -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
  }}>
@@ -58,11 +58,18 @@
58
58
 
59
59
  .tw-plus-sprite-toolbar svg { width: 16px; height: 16px; flex: none; }
60
60
 
61
+ .tw-plus-sprite-toolbar-tools {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 8px;
65
+ min-inline-size: 0;
66
+ margin-inline-start: auto;
67
+ }
68
+
61
69
  .tw-plus-sprite-search {
62
70
  position: relative;
63
- flex: 0 1 132px;
64
- min-width: 0;
65
- margin-inline-start: auto;
71
+ inline-size: 132px;
72
+ min-inline-size: 0;
66
73
  }
67
74
 
68
75
  .tw-plus-sprite-search-field { position: relative; }
@@ -98,7 +98,7 @@ function SearchBox({viewId, section, japanese, search}: {
98
98
  export function SpriteEditorToolbar({viewId, locale, section, onSectionChange, isStage, activeGroup, search,
99
99
  availableSections = spriteEditorSections, propertiesInVariables = false, actions}: SpriteEditorToolbarProps) {
100
100
  const japanese = /^ja(?:-|$)/i.test(locale);
101
- const labels = japanese ? {code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: propertiesInVariables ? 'プロパティ・変数' : '変数'} :
101
+ const labels = japanese ? {code: 'コード', costumes: isStage ? '背景' : 'コスチューム', sounds: '音', variables: propertiesInVariables ? '変数など' : '変数'} :
102
102
  {code: 'Code', costumes: isStage ? 'Backdrops' : 'Costumes', sounds: 'Sounds', variables: propertiesInVariables ? 'Properties & Variables' : 'Variables'};
103
103
  return <div className="tw-plus-sprite-toolbar" data-active-group={activeGroup}>
104
104
  <div className="tw-plus-sprite-toolbar-sections" role="tablist"
@@ -119,7 +119,9 @@ export function SpriteEditorToolbar({viewId, locale, section, onSectionChange, i
119
119
  {labels[id]}
120
120
  </button>)}
121
121
  </div>
122
- {search && <SearchBox {...{viewId, section, japanese, search}} />}
123
- {actions}
122
+ {(search || actions) && <div className="tw-plus-sprite-toolbar-tools">
123
+ {actions}
124
+ {search && <SearchBox {...{viewId, section, japanese, search}} />}
125
+ </div>}
124
126
  </div>;
125
127
  }
@@ -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
@@ -280,7 +280,7 @@ export function createStandardLayout(React, {styles, targetStyles, context, Layo
280
280
  const [expanded, setExpanded] = useState(false);
281
281
  const tools = useContext(targetTools);
282
282
  return plusLayout ? element('div', {className: styles.spriteProperties},
283
- inEditor ? element('span', null, isJapanese(locale) ? 'スプライト' : 'Sprite') :
283
+ inEditor ? element('span', {className: styles.spritePropertiesTitle}, isJapanese(locale) ? 'スプライト' : 'Sprite') :
284
284
  element('button', {
285
285
  type: 'button', className: styles.spritePropertiesToggle, 'aria-expanded': expanded,
286
286
  onClick: () => setExpanded(open => !open)
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;