Asset panel grouped by kind with icons: assets are listed under a header per kind (Sprites, Behaviors, ...) with the count on the right, empty groups are not shown, every group starts folded when a project is opened, a click on the header folds and unfolds it, selecting an asset elsewhere unfolds its group and folding the group of the selected asset clears the selection; every asset kind has an SVG icon in src/editor/panels/icons/ drawn before the name, the pure rule for grouping and sorting lives in assetGroups.ts; the same icons replace the kind word in document tabs and the overflow menu (a list icon for the List tab, which also fixes the close x lying over the end of the active tab's text), stand before every entry of the + menu (Button got an icon option) and on the right of scene object rows instead of the type name; the colour swatch next to sprite names is gone from both panels; the SVG loader moved to src/ui/svg.ts for both icon sets; test hook assetRow unfolds the group and scrolls to the row, assetRowShown only reads, new hook menuIcons; e2e 4g and 4h, 14 and 44a check the icons (#176)
game-maker / gate (push) Successful in 1m58s
game-maker / suite (push) Successful in 6m54s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 18:36:20 +02:00
1 parent 1c212b1ccc
commit e3ebf44593
46 files changed
+724 -129

No files matched your search

+101 -28
View File
@@ -10,12 +10,7 @@ import {
} from '../../core/commands/assetCommands';
import { oneStep } from '../../core/commands/CompoundCommand';
import { nextAssetName } from '../../core/commands/naming';
import {
ASSET_TYPE_LABELS,
type AssetType,
hasColorSwatch,
isPlaceableType,
} from '../../core/model/AssetTypes';
import { ASSET_TYPE_LABELS, type AssetType, isPlaceableType } from '../../core/model/AssetTypes';
import type { TextureAsset } from '../../core/model/Asset';
import type { PropertyBag } from '../../core/model/Properties';
import { findAsset } from '../../core/model/Project';
@@ -26,6 +21,7 @@ import type { SoundStore } from '../../core/store/SoundStore';
import type { TextureStore } from '../../core/store/TextureStore';
import {
Button,
type ListItem,
ListView,
Menu,
type MenuEntry,
@@ -34,6 +30,8 @@ import {
ScrollContainer,
startDrag,
} from '../../ui';
import { ASSET_GROUP_LABELS, groupAssets } from './assetGroups';
import { ASSET_ICON_SVG } from './assetIcons';
import { assetMenuGroups } from './assetMenu';
import { confirmDeleteAsset } from '../actions';
import type { EditorState } from '../state';
@@ -41,8 +39,13 @@ import { openFontPicker } from '../font/FontPicker';
import { openSoundPicker } from '../sound/SoundPicker';
import { openImagePicker } from '../texture/ImagePicker';
/** Row ids of the group headings are this plus the type; an asset id never starts with it. */
const HEADING = 'group:';
const headingId = (type: AssetType): string => HEADING + type;
/**
* Lists all assets of the project; creates and deletes them. Placing is a drag into a view.
* Lists all assets of the project, grouped by type under a folding heading (#176); creates and
* deletes them. Placing is a drag into a view.
*
* A row's name turns gold when the asset carries a property the inspector marks – the same rule,
* so the list cannot promise a deviation the inspector then fails to show.
@@ -50,6 +53,10 @@ import { openImagePicker } from '../texture/ImagePicker';
export class AssetPanel extends Panel {
private readonly list = new ListView();
private readonly scroll = new ScrollContainer();
/** Types whose group is unfolded, for the session; every group starts folded. */
private readonly unfolded = new Set<AssetType>();
/** Ids of the rows as shown, headings included. */
private rows: string[] = [];
constructor(
private readonly state: EditorState,
@@ -72,11 +79,16 @@ export class AssetPanel extends Panel {
this.list.onActivate = (id) => this.activate(id);
this.list.onContextMenu = (id, x, y) => this.openContextMenu(id, x, y);
this.list.onDragOut = (id) => this.startPlacementDrag(id);
this.list.onToggle = (id, expand) => this.setFolded(id.slice(HEADING.length) as AssetType, !expand);
this.scroll.setContent(this.list);
this.content.add(this.scroll, 1);
state.on('project', () => this.refresh());
state.on('load', () => this.refresh());
// A different project brings different assets – start folded again.
state.on('load', () => {
this.unfolded.clear();
this.refresh();
});
state.on('selection', () => this.syncSelection());
this.refresh();
}
@@ -85,6 +97,7 @@ export class AssetPanel extends Panel {
private addMenuEntries(): MenuEntry[] {
const entry = (type: AssetType): MenuEntry => ({
label: ASSET_TYPE_LABELS[type],
icon: ASSET_ICON_SVG[type],
onSelect: () => this.addAsset(type),
});
return assetMenuGroups().flatMap((group, index) =>
@@ -97,36 +110,79 @@ export class AssetPanel extends Panel {
* with. Two marks rather than one, because they are two different questions and often two
* different scenes – the level you are editing versus the title screen.
*/
private sceneHint(id: string): string {
private sceneMarks(id: string): string {
const { activeSceneId, startSceneId } = this.state.project;
const marks = `${id === startSceneId ? '▶' : ''}${id === activeSceneId ? '●' : ''}`;
return marks ? `scene ${marks}` : 'scene';
return `${id === startSceneId ? '▶' : ''}${id === activeSceneId ? '●' : ''}`;
}
private refresh(): void {
this.list.setItems(
this.state.project.assets.map((asset) => ({
id: asset.id,
label: asset.name,
hint: asset.type === 'scene' ? this.sceneHint(asset.id) : asset.type,
swatch: hasColorSwatch(asset.type) ? Number(asset.props.color ?? 0) : undefined,
overridden: assetDeviates(this.state.project, asset),
})),
);
this.syncSelection();
const project = this.state.project;
const items: ListItem[] = [];
for (const { type, assets } of groupAssets(project.assets)) {
const expanded = this.unfolded.has(type);
items.push({
id: headingId(type),
label: ASSET_GROUP_LABELS[type],
hint: String(assets.length),
icon: ASSET_ICON_SVG[type],
heading: true,
expanded,
});
if (!expanded) continue;
for (const asset of assets)
items.push({
id: asset.id,
label: asset.name,
hint: asset.type === 'scene' ? this.sceneMarks(asset.id) : undefined,
depth: 1,
overridden: assetDeviates(project, asset),
});
}
this.rows = items.map((item) => item.id);
this.list.setItems(items);
// Measured first: scrolling to the selection needs the new height of the list.
this.scroll.relayoutContent();
this.syncSelection();
}
private setFolded(type: AssetType, fold: boolean): void {
if (fold) {
// A selected asset unfolds its group again – folding lets go of it first.
const s = this.state.selection;
if (s?.kind === 'asset' && findAsset(this.state.project, s.assetId)?.type === type)
this.state.select(null);
this.unfolded.delete(type);
} else this.unfolded.add(type);
this.refresh();
}
private syncSelection(): void {
const s = this.state.selection;
const id = s?.kind === 'asset' ? s.assetId : null;
this.list.select(id, true);
// A selection made anywhere else – a graph node, a picker, a fresh asset – has to be reachable
// in the list, and with fifteen kinds of asset in a project the row is often below the fold.
if (id) {
const index = this.state.project.assets.findIndex((a) => a.id === id);
if (index >= 0) this.scroll.ensureVisible(index * this.list.rowHeight, this.list.rowHeight);
// in the list: its group unfolds, and with many assets the row is often below the fold.
const type = id ? findAsset(this.state.project, id)?.type : undefined;
if (type && !this.unfolded.has(type)) {
this.setFolded(type, false);
return;
}
this.list.select(id, true);
if (id) this.scrollTo(id);
}
private scrollTo(id: string): void {
const index = this.rows.indexOf(id);
if (index >= 0) this.scroll.ensureVisible(index * this.list.rowHeight, this.list.rowHeight);
}
/** The groups as shown, in order (test hook). */
groups(): { type: AssetType; label: string; expanded: boolean; ids: string[] }[] {
return groupAssets(this.state.project.assets).map(({ type, assets }) => ({
type,
label: ASSET_GROUP_LABELS[type],
expanded: this.unfolded.has(type),
ids: assets.map((a) => a.id),
}));
}
private createAsset(type: AssetType, name = ASSET_TYPE_LABELS[type], props: PropertyBag = {}): void {
@@ -199,13 +255,30 @@ export class AssetPanel extends Panel {
this.state.openAsset(id);
}
/** Where an asset's row sits on screen, so a check can pick it up by id (test hook). */
rowGlobal(assetId: string): { x: number; y: number } | null {
/**
* Where an asset's row sits on screen, so a check can pick it up by id (test hook); with `reveal`
* its group is unfolded first, otherwise a folded group's row is `null`.
*/
rowGlobal(assetId: string, reveal = false): { x: number; y: number } | null {
if (reveal) this.reveal(assetId);
return this.list.rowGlobal(assetId);
}
/** Unfolds the asset's group and scrolls to its row, as a user would before clicking it (test hooks). */
private reveal(assetId: string): void {
const type = findAsset(this.state.project, assetId)?.type;
if (type && !this.unfolded.has(type)) this.setFolded(type, false);
this.scrollTo(assetId);
}
/** Centre of a group heading's icon (test hook). */
groupIcon(type: AssetType): { x: number; y: number } | null {
return this.list.iconGlobal(headingId(type));
}
/** Label colour of a row as `#rrggbb` – gold means the asset deviates from the defaults (test hook). */
rowColor(assetId: string): string | null {
this.reveal(assetId);
return this.list.rowColor(assetId);
}
+8 -5
View File
@@ -2,12 +2,10 @@ import type { Command } from '../../core/commands/Command';
import { oneStep } from '../../core/commands/CompoundCommand';
import { UnpackContainerCommand } from '../../core/commands/containerCommands';
import { ReorderObjectCommand, SetParentCommand } from '../../core/commands/sceneObjectCommands';
import { hasColorSwatch } from '../../core/model/AssetTypes';
import { ancestorIdsOf, childIdsOf, findObject, visibleTreeOrder } from '../../core/model/hierarchy';
import { isLiveInstance, isTemplateContent, resolvedHolder } from '../../core/model/instance';
import { findAsset, type ObjectHolder } from '../../core/model/Project';
import { deviationMarks } from '../../core/properties/deviation';
import { resolveSceneObjectProps } from '../../core/properties/resolve';
import { type DropSpot, ListView, Menu, Panel, ScrollContainer } from '../../ui';
import {
addCollider,
@@ -17,6 +15,7 @@ import {
createContainer,
removeObjects,
} from '../actions';
import { ASSET_ICON_SVG } from './assetIcons';
import { type EditorState, objectSelection } from '../state';
/**
@@ -139,6 +138,11 @@ export class HierarchyPanel extends Panel {
return this.list.rowGlobal(id);
}
/** Global centre of the type icon at a row's right edge (test hook). */
iconPosition(id: string): { x: number; y: number } | null {
return this.list.iconGlobal(id);
}
expandAll(): void {
for (const object of this.holder()?.objects ?? []) this.expanded.add(object.id);
this.refresh();
@@ -243,13 +247,12 @@ export class HierarchyPanel extends Panel {
this.list.setItems(
(holder ? visibleTreeOrder(holder, this.expanded) : []).map(({ object, depth, hasChildren }) => {
const asset = findAsset(this.state.project, object.assetId);
const props = asset ? resolveSceneObjectProps(this.state.project, asset, object) : {};
return {
id: object.id,
label: object.name || asset?.name || '?',
hint: asset?.type ?? '',
icon: asset && ASSET_ICON_SVG[asset.type],
iconAtEnd: true,
depth,
swatch: asset && hasColorSwatch(asset.type) ? Number(props.color ?? 0) : undefined,
expanded: hasChildren ? this.expanded.has(object.id) : undefined,
overridden: marks.has(object.id),
};
+51
View File
@@ -0,0 +1,51 @@
import type { Asset } from '../../core/model/Asset';
import type { AssetType } from '../../core/model/AssetTypes';
/**
* Group headings of the asset panel. Plural, because a heading names what is under it – and so no
* heading reads like the `+` menu entry that creates one of its kind.
*/
export const ASSET_GROUP_LABELS: Record<AssetType, string> = {
scene: 'Scenes',
camera: 'Cameras',
sprite: 'Sprites',
text: 'Texts',
behavior: 'Behaviors',
variables: 'Variables',
collider: 'Colliders',
rigidbody: 'Rigidbodies',
container: 'Containers',
texture: 'Textures',
animation: 'Animations',
tile: 'Tiles',
tilemap: 'Tilemaps',
statemachine: 'State machines',
shader: 'Shaders',
font: 'Fonts',
sound: 'Sounds',
particles: 'Particles',
};
export interface AssetGroup {
type: AssetType;
assets: Asset[];
}
/** Case aside and digits as numbers, so `Level 2` comes before `Level 10`. */
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
/**
* The asset panel's rows (#176): one group per type that has assets, groups and the assets in
* each alphabetical. Equal names keep the project's order, so a row does not jump on a rebuild.
*/
export function groupAssets(assets: readonly Asset[]): AssetGroup[] {
const byType = new Map<AssetType, Asset[]>();
for (const asset of assets) {
const list = byType.get(asset.type);
if (list) list.push(asset);
else byType.set(asset.type, [asset]);
}
return [...byType]
.map(([type, list]) => ({ type, assets: list.sort((a, b) => collator.compare(a.name, b.name)) }))
.sort((a, b) => collator.compare(ASSET_GROUP_LABELS[a.type], ASSET_GROUP_LABELS[b.type]));
}
+48
View File
@@ -0,0 +1,48 @@
/**
* One icon per asset type (#176), one SVG file each in `icons/`, drawn by `drawSvg`: the asset
* panel's groups, the `+` menu, the document tabs and the scene objects.
*/
import type { AssetType } from '../../core/model/AssetTypes';
import animation from './icons/animation.svg?raw';
import behavior from './icons/behavior.svg?raw';
import camera from './icons/camera.svg?raw';
import collider from './icons/collider.svg?raw';
import container from './icons/container.svg?raw';
import font from './icons/font.svg?raw';
import list from './icons/list.svg?raw';
import particles from './icons/particles.svg?raw';
import rigidbody from './icons/rigidbody.svg?raw';
import scene from './icons/scene.svg?raw';
import shader from './icons/shader.svg?raw';
import sound from './icons/sound.svg?raw';
import sprite from './icons/sprite.svg?raw';
import statemachine from './icons/statemachine.svg?raw';
import text from './icons/text.svg?raw';
import texture from './icons/texture.svg?raw';
import tile from './icons/tile.svg?raw';
import tilemap from './icons/tilemap.svg?raw';
import variables from './icons/variables.svg?raw';
export const ASSET_ICON_SVG: Record<AssetType, string> = {
animation,
behavior,
camera,
collider,
container,
font,
particles,
rigidbody,
scene,
shader,
sound,
sprite,
statemachine,
text,
texture,
tile,
tilemap,
variables,
};
/** The List tab's: it edits a list value, which is no asset. */
export const LIST_ICON_SVG = list;
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7" y="-6" width="14" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M-2 -3 L3.5 0 L-2 3 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7.5" y="-6.5" width="6" height="5" rx="1" fill="currentColor" />
<rect x="1.5" y="1.5" width="6" height="5" rx="1" fill="currentColor" />
<path d="M-1.5 -4 C2.5 -4 -2.5 4 1.5 4" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 343 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7" y="-4" width="10" height="8" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M3.5 0 L7.5 -3.5 L7.5 3.5 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 265 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-5.5" y="-5.5" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.5" />
<rect x="-7.5" y="-7.5" width="4" height="4" fill="currentColor" />
<rect x="3.5" y="-7.5" width="4" height="4" fill="currentColor" />
<rect x="-7.5" y="3.5" width="4" height="4" fill="currentColor" />
<rect x="3.5" y="3.5" width="4" height="4" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 474 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7" y="-7" width="14" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.5" />
<rect x="-4" y="-4" width="3.5" height="3.5" fill="currentColor" />
<rect x="0.5" y="-4" width="3.5" height="3.5" fill="currentColor" />
<rect x="-4" y="0.5" width="3.5" height="3.5" fill="currentColor" />
<rect x="0.5" y="0.5" width="3.5" height="3.5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 485 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<path d="M-6 7 L0 -7 L6 7 M-3.5 2 H3.5" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 191 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<!-- Three bullet points: the List tab edits a list value, not an asset. -->
<circle cx="-5" cy="-4.5" r="1.25" fill="currentColor" />
<circle cx="-5" cy="0" r="1.25" fill="currentColor" />
<circle cx="-5" cy="4.5" r="1.25" fill="currentColor" />
<line x1="-2" y1="-4.5" x2="6.5" y2="-4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<line x1="-2" y1="0" x2="6.5" y2="0" fill="none" stroke="currentColor" stroke-width="1.5" />
<line x1="-2" y1="4.5" x2="6.5" y2="4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 643 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<!-- As in the scene view: a hollow ring with sparks flying off it. -->
<circle cx="0" cy="0" r="3.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<circle cx="5.5" cy="5.5" r="1.5" fill="currentColor" />
<circle cx="-5.5" cy="5.5" r="1.5" fill="currentColor" />
<circle cx="5.5" cy="-5.5" r="1.5" fill="currentColor" />
<circle cx="-5.5" cy="-5.5" r="1.5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 495 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<!-- A weight, as in the scene view: a ring with an arrow pointing the way gravity pulls. -->
<circle cx="0" cy="0" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M0 -3.5 V1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M-3 0.5 L0 4 L3 0.5 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7" y="-6" width="14" height="12" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M-5 4 L-1.5 -0.5 L1.5 2.5 L3 1 L5 4" fill="none" stroke="currentColor" stroke-width="1.5" />
<circle cx="3" cy="-2.5" r="1.25" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 366 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<circle cx="0" cy="0" r="6.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M0 -6.5 A6.5 6.5 0 0 1 0 6.5 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 247 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<path d="M-7.5 -2.5 H-4.5 L-0.5 -6 V6 L-4.5 2.5 H-7.5 Z" fill="currentColor" />
<path d="M2 -2.5 A3.5 3.5 0 0 1 2 2.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M4 -5.5 A7 7 0 0 1 4 5.5" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>

After

Width:  |  Height:  |  Size: 365 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<circle cx="0" cy="-3.5" r="2.75" fill="currentColor" />
<path d="M-5.5 7 C-5.5 1.5 5.5 1.5 5.5 7 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 221 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<circle cx="-4" cy="-3.5" r="2.75" fill="none" stroke="currentColor" stroke-width="1.5" />
<circle cx="4" cy="3.5" r="2.75" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M-1.5 -3.5 C2 -3.5 4 -2.5 4 0.25" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M2 -0.5 L4 2 L6 -0.5 Z" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 436 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<path d="M-6 -5.5 H6 M0 -5.5 V7" fill="none" stroke="currentColor" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 182 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<!-- A checkerboard, what an image editor shows behind a picture. -->
<rect x="-6.25" y="-6.25" width="12.5" height="12.5" fill="none" stroke="currentColor" stroke-width="1.5" />
<rect x="-6.25" y="-6.25" width="6.25" height="6.25" fill="currentColor" />
<rect x="0" y="0" width="6.25" height="6.25" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 424 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-5.5" y="-5.5" width="11" height="11" rx="1" fill="currentColor" fill-opacity="0.35" stroke="currentColor" stroke-width="1.5" />
<path d="M-5.5 0 H5.5 M0 -5.5 V0 M-2.5 0 V5.5 M2.5 0 V5.5" fill="none" stroke="currentColor" stroke-width="1" />
</svg>

After

Width:  |  Height:  |  Size: 348 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<rect x="-7" y="-7" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M-7 -2.5 H7 M-7 2.5 H7 M-2.5 -7 V7 M2.5 -7 V7" fill="none" stroke="currentColor" stroke-width="1" />
<rect x="-2.5" y="-2.5" width="5" height="5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 376 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-8 -8 16 16" width="16" height="16">
<path d="M-3 -7 C-5 -7 -5 -5.5 -5 -3.5 C-5 -1.5 -5.5 0 -7 0 C-5.5 0 -5 1.5 -5 3.5 C-5 5.5 -5 7 -3 7" fill="none" stroke="currentColor" stroke-width="1.5" />
<path d="M3 -7 C5 -7 5 -5.5 5 -3.5 C5 -1.5 5.5 0 7 0 C5.5 0 5 1.5 5 3.5 C5 5.5 5 7 3 7" fill="none" stroke="currentColor" stroke-width="1.5" />
<circle cx="0" cy="0" r="1.5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 453 B