Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .claude/rules/components.md

Large diffs are not rendered by default.

76 changes: 40 additions & 36 deletions apps/web/src/games/sm/components/SmControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,47 +239,51 @@ export const SmControls = () => {
<span className="sr-only">Redo</span>
</button>
<SmAutotrack />
<button
className="tracker-controls__button"
onClick={handleExport}
title="Export progress to a file"
type="button"
>
<ExportIcon />
<span className="toolbar-label">Export</span>
</button>
<button
className="tracker-controls__button"
onClick={() => fileRef.current?.click()}
title="Import progress from a file"
type="button"
>
<ImportIcon />
<span className="toolbar-label">Import</span>
</button>
<button
className="tracker-controls__button"
data-testid="share-button"
onClick={handleShare}
title={SHARE_HELP}
type="button"
>
<ShareIcon />
<span className="toolbar-label">Share</span>
</button>
{/* OBS cannot load the desktop app's page nor follow its storage */}
{!isDesktopApp && (
{/* The secondary actions lose their labels before the primary controls
do (ui.css); with no More menu they never move */}
<span className="toolbar-actions">
<button
className="tracker-controls__button"
data-testid="sm-stream-view"
onClick={() => setDialog("stream")}
title="The stream view, for OBS"
onClick={handleExport}
title="Export progress to a file"
type="button"
>
<StreamIcon />
<span className="toolbar-label">Stream view</span>
<ExportIcon />
<span className="toolbar-label">Export</span>
</button>
)}
<button
className="tracker-controls__button"
onClick={() => fileRef.current?.click()}
title="Import progress from a file"
type="button"
>
<ImportIcon />
<span className="toolbar-label">Import</span>
</button>
<button
className="tracker-controls__button"
data-testid="share-button"
onClick={handleShare}
title={SHARE_HELP}
type="button"
>
<ShareIcon />
<span className="toolbar-label">Share</span>
</button>
{/* OBS cannot load the desktop app's page nor follow its storage */}
{!isDesktopApp && (
<button
className="tracker-controls__button"
data-testid="sm-stream-view"
onClick={() => setDialog("stream")}
title="The stream view, for OBS"
type="button"
>
<StreamIcon />
<span className="toolbar-label">Stream view</span>
</button>
)}
</span>
<input
accept="application/json,.json"
aria-label="Import a save file"
Expand Down
76 changes: 40 additions & 36 deletions apps/web/src/games/smz3/components/Smz3Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,47 +322,51 @@ export const Smz3Controls = () => {
<span className="sr-only">Redo</span>
</button>
<Smz3Autotrack />
<button
className="tracker-controls__button"
onClick={handleExport}
title="Export progress to a file"
type="button"
>
<ExportIcon />
<span className="toolbar-label">Export</span>
</button>
<button
className="tracker-controls__button"
onClick={() => fileRef.current?.click()}
title="Import progress from a file"
type="button"
>
<ImportIcon />
<span className="toolbar-label">Import</span>
</button>
<button
className="tracker-controls__button"
data-testid="share-button"
onClick={handleShare}
title={SHARE_HELP}
type="button"
>
<ShareIcon />
<span className="toolbar-label">Share</span>
</button>
{/* OBS cannot load the desktop app's page nor follow its storage */}
{!isDesktopApp && (
{/* The secondary actions lose their labels before the primary controls
do (ui.css); with no More menu they never move */}
<span className="toolbar-actions">
<button
className="tracker-controls__button"
data-testid="smz3-stream-view"
onClick={() => setDialog("stream")}
title="The stream view, for OBS"
onClick={handleExport}
title="Export progress to a file"
type="button"
>
<StreamIcon />
<span className="toolbar-label">Stream view</span>
<ExportIcon />
<span className="toolbar-label">Export</span>
</button>
)}
<button
className="tracker-controls__button"
onClick={() => fileRef.current?.click()}
title="Import progress from a file"
type="button"
>
<ImportIcon />
<span className="toolbar-label">Import</span>
</button>
<button
className="tracker-controls__button"
data-testid="share-button"
onClick={handleShare}
title={SHARE_HELP}
type="button"
>
<ShareIcon />
<span className="toolbar-label">Share</span>
</button>
{/* OBS cannot load the desktop app's page nor follow its storage */}
{!isDesktopApp && (
<button
className="tracker-controls__button"
data-testid="smz3-stream-view"
onClick={() => setDialog("stream")}
title="The stream view, for OBS"
type="button"
>
<StreamIcon />
<span className="toolbar-label">Stream view</span>
</button>
)}
</span>
<input
accept="application/json,.json"
aria-label="Import a save file"
Expand Down
30 changes: 23 additions & 7 deletions apps/web/src/hooks/useToolbarFit.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ const LAYOUTS: Record<ToolbarFit, FitLayout> = {
"more-autotrack": { gap: 0, padding: 0, widths: [50, 50, 50] },
"more-keys": { gap: 0, padding: 0, widths: [100, 100, 100, 100] },
"more-map": { gap: 8, padding: 0, widths: [150, 150, 150] },
"secondary-icons": { gap: 8, padding: 0, widths: [400, 400] },
};
const layoutOf = (fit: ToolbarFit) => LAYOUTS[fit];

describe("chooseFit", () => {
it.each([
[1100, "labels"],
[1008, "labels"],
[1007, "icons"],
[1007, "secondary-icons"],
[808, "secondary-icons"],
[807, "icons"],
[608, "icons"],
[607, "more"],
[408, "more"],
Expand Down Expand Up @@ -71,13 +74,18 @@ describe("chooseFit", () => {

chooseFit(700, asked);

expect(asked.mock.calls.map(([fit]) => fit)).toEqual(["labels", "icons"]);
expect(asked.mock.calls.map(([fit]) => fit)).toEqual([
"labels",
"secondary-icons",
"icons",
]);
});

it("keeps a form while it fits, and falls back as soon as it no longer does", () => {
expect(chooseFit(1008, layoutOf, "labels")).toBe("labels");
expect(chooseFit(408, layoutOf, "more")).toBe("more");
expect(chooseFit(1007, layoutOf, "labels")).toBe("icons");
expect(chooseFit(1007, layoutOf, "labels")).toBe("secondary-icons");
expect(chooseFit(807, layoutOf, "secondary-icons")).toBe("icons");
expect(chooseFit(407, layoutOf, "more")).toBe("more-map");
expect(chooseFit(250, layoutOf, "labels")).toBe("more-keys");
});
Expand All @@ -87,10 +95,16 @@ describe("chooseFit", () => {
"more-map",
);
expect(chooseFit(408 + HYSTERESIS_PX, layoutOf, "more-map")).toBe("more");
expect(chooseFit(1008 + HYSTERESIS_PX - 1, layoutOf, "icons")).toBe(
"icons",
expect(
chooseFit(1008 + HYSTERESIS_PX - 1, layoutOf, "secondary-icons"),
).toBe("secondary-icons");
expect(chooseFit(1008 + HYSTERESIS_PX, layoutOf, "secondary-icons")).toBe(
"labels",
);
expect(chooseFit(808 + HYSTERESIS_PX - 1, layoutOf, "icons")).toBe("icons");
expect(chooseFit(808 + HYSTERESIS_PX, layoutOf, "icons")).toBe(
"secondary-icons",
);
expect(chooseFit(1008 + HYSTERESIS_PX, layoutOf, "icons")).toBe("labels");
// Without the margin, a scrollbar's width could undo each switch
expect(HYSTERESIS_PX).toBeGreaterThan(17);
});
Expand All @@ -112,6 +126,7 @@ beforeEach(() => {
"more-autotrack": 40,
"more-keys": 80,
"more-map": 100,
"secondary-icons": 300,
};
seen.length = 0;
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
Expand Down Expand Up @@ -236,7 +251,7 @@ describe("useToolbarFit", () => {
render(<Toolbar />);
const folds: Record<number, [string?, string?]> = {};

for (const width of [700, 400, 250, 170, 130, 100]) {
for (const width of [900, 700, 400, 250, 170, 130, 100]) {
resizeTo(width);
folds[width] = [toolbar().dataset.fit, toolbar().dataset.fold];
}
Expand All @@ -248,6 +263,7 @@ describe("useToolbarFit", () => {
250: ["more-map", "labels secondary map"],
400: ["more", "labels secondary"],
700: ["icons", "labels"],
900: ["secondary-icons", "secondary-labels"],
});
});

Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/hooks/useToolbarFit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { type RefObject, useLayoutEffect } from "react";
* during a run fold first:
*
* - `labels`: every action with its label, on one row.
* - `secondary-icons`: Export, Import, Share, Stream view and Shortcuts show
* as icons while the primary controls (Mode, New game, Autotracking, the
* map layout, the key cells toggle) keep their labels. A button added to
* the bar later costs this step its few pixels, never every label at once.
* - `icons`: every action as an icon, on one row.
* - `more`: Export, Import, Stream view and Shortcuts move into More.
* - `more-map`: the map layout too, set once per device and the widest
Expand All @@ -20,6 +24,7 @@ import { type RefObject, useLayoutEffect } from "react";
*/
export const FITS = [
{ fit: "labels", fold: [], rows: 1 },
{ fit: "secondary-icons", fold: ["secondary-labels"], rows: 1 },
{ fit: "icons", fold: ["labels"], rows: 1 },
{ fit: "more", fold: ["labels", "secondary"], rows: 2 },
{ fit: "more-map", fold: ["labels", "secondary", "map"], rows: 2 },
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/styles/toolbar-menu.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/*
* How the toolbar folds, by the tokens useToolbarFit writes to its data-fold
* from the widths it measures (see FITS there for the order): the secondary
* actions, then the map layout, then the key cells toggle move into the More
* menu, then New game shows as an icon, and last Autotracking moves into More
* actions lose their labels, then every button does (ui.css); then the
* secondary actions, the map layout and the key cells toggle move into the
* More menu, New game shows as an icon, and last Autotracking moves into More
* too, so the bar holds on one or two rows down to the narrowest phone.
* .toolbar-actions groups the secondary actions of a bar with no More menu
* (Super Metroid, SMZ3): their labels fold as ALttPR's, they never move.
*/
.toolbar-secondary,
.toolbar-actions,
.toolbar-autotrack {
display: contents;
}
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/styles/ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,23 @@
}

/* Unless the labelled bar fits on one row (data-fold, set by useToolbarFit
from the widths it measures), its buttons are icons, square 40 px targets */
from the widths it measures), its buttons are icons, square 40 px targets:
first the secondary actions only ("secondary-labels", inside
.toolbar-secondary or .toolbar-actions), then every button ("labels") */
.toolbar[data-fold~="labels"] .tracker-controls__button,
.toolbar[data-fold~="labels"] .bigkey-toggle {
.toolbar[data-fold~="labels"] .bigkey-toggle,
.toolbar[data-fold~="secondary-labels"]
:is(.toolbar-secondary, .toolbar-actions)
.tracker-controls__button {
justify-content: center;
min-width: 2.5rem;
padding-inline: var(--spacing-sm);
}

.toolbar[data-fold~="labels"] .toolbar-label,
.toolbar[data-fold~="secondary-labels"]
:is(.toolbar-secondary, .toolbar-actions)
.toolbar-label,
.toolbar[data-fold~="new-game"] .new-game__label {
position: absolute;
width: 1px;
Expand Down
Binary file modified docs/screenshots/alttpr.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/sm.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading