Files
feuerwerk/src/editor/sectionFolds.ts
T
adminandClaude Opus 5.5 393caadc9d
game-maker / gate (push) Successful in 2m39s
game-maker / suite (push) Successful in 13m10s
Head the groups of the inspector with bands that fold (#290)
The groups of the inspector had a small grey label as their title, in
the same grid as a property row, so with many rows one group hardly
stood apart from the next. Now every group has a band as its head,
from edge to edge of the panel, and a click on it folds the group.

- A band runs across the padding of its column in the header colour,
  with the title and a mark that says whether the group is open. The
  titles stay as they are, as answered in the issue.
- A click on a band folds its group: the rows go, the band stays and
  the next band moves up against it. A control in the band (Open,
  + Variable, + Property) does its own job and folds nothing.
- A placed object has a group SWITCHES under its name, asset and
  parent: Enabled, Keep on scene load and the switches of a behavior.
  An asset has SWITCHES and PROPERTIES, and a group without rows is
  left out.
- The same bands head the tilemap panel, the Variables tab, the side
  of the Pixel art tab and the shape list of vector art. A list with a
  title (Layers, Frames, Shapes) folds with a click on its header,
  clear of its plus.
- ui/sections.ts holds the rules as pure functions, SectionStack is
  the column and SectionHeader the band. Every title of the inspector
  goes through addTitle or sectionHeader in inspector/host.ts.

Decided here:

- A fold is kept for good, in localStorage of this browser. The issue
  asked whether for the session or for good and the answer left it
  open.
- A fold is remembered by title, per panel. PROPERTIES folded once is
  folded on every object, asset and node. A number in a title does
  not count.
- A behavior asset has all its rows under SWITCHES. A Variables asset
  and a state machine get a group SCOPE.
- A greyed out control in a band takes the click too. Without that the
  click fell through to the band and folded the group.
- The Curve tab has no column of this kind and is unchanged.

Found on the way:

- The inspector of a sprite is taller than the 800 pixel window of
  the checks now. The hooks that hand out a row scroll it into view
  first, and module 28 scrolls before it looks for a row by its text.

Tests:
- Gate: 202 files, 3008 tests (2982 + 26), exit 0
- e2e: no complete run for this state. Modules 28 to 61 passed alone
  (749 checks). A full run was cut off in the accounts module at 1492
  passes without a failure, the modules after it and the smokes are
  missing. The sum of 1639 (1635 + 4) is reckoned, CI measures it.
- 26 unit tests in tests/editor/sections.test.ts
- Four e2e checks with a real mouse, three in module 02 and one in
  module 48
- 25 unit counter-probes all fail tests, run on stein2
- Screenshots of the bands looked at before the last build, none
  after it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:08:28 +02:00

69 lines
2.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Which groups of rows are folded (#290), kept in this browser: a group folded once stays folded
* for every selection that shows it, and after a reload. Best effort, like the saves – a browser
* that refuses `localStorage` forgets with the page.
*/
import { Theme } from '../ui/Theme';
const STORAGE_KEY = 'game-maker.folded-sections';
let folded: Set<string> | null = null;
function storage(): Pick<Storage, 'getItem' | 'setItem'> | null {
try {
return typeof window === 'undefined' ? null : window.localStorage;
} catch {
return null;
}
}
function read(): Set<string> {
try {
const stored: unknown = JSON.parse(storage()?.getItem(STORAGE_KEY) ?? '[]');
return new Set(Array.isArray(stored) ? stored.filter((key) => typeof key === 'string') : []);
} catch {
return new Set();
}
}
/**
* Names a group: the panel it stands in and its title, numbers left out – "3 OBJECTS SELECTED" and
* "KEY AT FRAME 12" are the same group whatever they count.
*/
export function foldKey(scope: string, title: string): string {
return `${scope}:${title.replace(/\d+/g, '#')}`;
}
export function isFolded(key: string): boolean {
folded ??= read();
return folded.has(key);
}
export function setFolded(key: string, on: boolean): void {
folded ??= read();
if (on) folded.add(key);
else folded.delete(key);
try {
storage()?.setItem(STORAGE_KEY, JSON.stringify([...folded].sort()));
} catch {
// Kept for this page only.
}
}
/**
* What a list with a header of its own takes to fold like the groups of the column it stands in:
* the band reaches across the column's padding, and the fold is remembered under the list's title.
*/
export function listFold(
scope: string,
title: string,
): { bleed: number; collapsed: boolean; onFold: (collapsed: boolean) => void } {
const key = foldKey(scope, title.toUpperCase());
return { bleed: Theme.padding, collapsed: isFolded(key), onFold: (on) => setFolded(key, on) };
}
/** Forgets what was read, so the next question asks the browser again (tests). */
export function resetFolds(): void {
folded = null;
}