Skip to content
Closed
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
2 changes: 1 addition & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ DS-T-01 (#1623), DS-T-02 (#1610), DS-T-03 (#1624), DS-T-04 (#1609) und DS-X-01 b
| DS-H-02a | Probe-Halle: Kacheln Boden, Wand, Tür, Band: `hall/tiles/` | Root disponiert | DS-H-01 | Server | B | Erledigt: #1642; sieben Kacheln (Boden, Wand, Tür, Band, Rückweg, zwei Kurven) als Textraster plus Hallen-Raster `tiles/layout.ts`; Generator weist fehlerhafte Eingaben mit klarer Meldung ab (Nacharbeit aus #1637) | benannte Tests rot→grün; prepush; zwei Reviews | – |
| DS-H-02b | Probe-Halle: Stationen, Tor und Requisiten mit Textnamen: `hall/tiles/` | Root disponiert | DS-H-02a | Server | B | In Arbeit (16.10.): Draft-PR mit vier Rollenstationen (Planungstisch, Werkbank, Prüfstand, Betriebspult), Tor, Übergabeablage und Stempel als Textraster `tiles/stations.px.ts`, deutsche Textnamen im Wörterbuch `src/i18n/de.ts` (Zuordnung Sprite → Schlüssel in `tiles/names.ts`); Nacharbeit aus #1642 (R1642-G1/K1, K2, K3); Reviews ausstehend | wie DS-H-01 | – |
| DS-H-03 | Probe-Halle: vier Rollenfiguren und Posen, jede Pose aus genau einer `StateForm` oder `unknown`: `hall/figures/` | Root disponiert | DS-H-01 | Server | B | Erledigt: #1646; vier Rollenfiguren (Planung, Umsetzung, Prüfung, Betrieb) mit eigener Kopfform, je zwölf Bilder in acht Posen als Delta zur Grundhaltung; `poseFor` ordnet jede `StateForm` genau einer Pose zu, alles andere `unknown`, ohne Lauf `ready` | benannte Tests rot→grün; prepush; zwei Reviews | – |
| DS-H-04 | Probe-Halle statisch: Layout, drei Werkstücke, Tor, Stempel, leerer und ungelesener Zustand in Worten: `hall/Hall.tsx` | Root disponiert | DS-H-02b, DS-H-03; Zustandsaliase aus DS-X-04 (#1603) | Server | B | Geplant: nicht disponiert (16.10.) | wie DS-H-01 | – |
| DS-H-04 | Probe-Halle statisch: Layout, drei Werkstücke, Tor, Stempel, leerer und ungelesener Zustand in Worten: `hall/Hall.tsx` | Implementierer · Claude; Root nimmt ab | DS-H-02b, DS-H-03; Zustandsaliase aus DS-X-04 (#1603) | Server | B | Offen: Entwurfs-PR (16.10.), Reviews ausstehend; `Hall` zeigt gefüllt, leer und ungelesen mit Schild „Beispieldaten“, Werkstück-Kachel `piece-crate-0` neu, im Register eingetragen; noch in keiner Ansicht eingebunden (Galerie DS-H-07) | Hall-Tests rot→grün; typecheck, contrast-check, build-sprites sauber, prepush Exit 0; Bildschirmfotos hell/dunkel angesehen; zwei Stufe-B-Reviews fremder Nicht-Anthropic-Familien | – |
| DS-H-05 | Probe-Halle: Listenprojektion, gemeinsame Auswahl und Inspektor, jedes Werkstück einmal per Tastatur erreichbar: `hall/HallList.tsx` | Root disponiert | DS-H-04; Listenzeile aus DS-X-11 (nicht disponiert) | Server | B | Geplant: nicht disponiert (16.10.) | wie DS-H-01 | – |
| DS-H-06 | Probe-Halle: drei Bewegungsmodi Ruhig/Lebendig/Aus, höchstens zwölf bewegte Figuren, Pause bei verstecktem Dokument: `hall/motion.css` | Root disponiert | DS-H-05; Bewegungsmodus aus DS-X-03 (#1606) | Server | B | Geplant: nicht disponiert (16.10.) | wie DS-H-01 | – |
| DS-H-07 | Probe-Halle: Galerie-Seite und Messaufbau (`shot.mjs --dpr --motion`), danach Abnahme durch den Nutzer: zuerst Bilder und kurzes Video vom Server (3 Bewegungsmodi × hell/dunkel), dann nach seinem Ja in der App am PC mit Speichermessung | Root disponiert; Abnahme der Nutzer | DS-H-06; Galerie aus DS-X-14 und DS-X-15 (nicht disponiert) | Server | B | Geplant: nicht disponiert (16.10.); das Urteil des Nutzers entscheidet über die übrigen Tabs | wie DS-H-01; Urteil des Nutzers | – |
Expand Down
54 changes: 54 additions & 0 deletions src/design/hall/Hall.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, screen } from "@testing-library/react";
import { expect, it } from "vitest";

import { de } from "../../i18n/de";
import { STATE_UNKNOWN_WORD } from "../state/states";
import { EMPTY_HALL, SAMPLE_HALL } from "./fixtures";
import { Hall } from "./Hall";

const sprites = (c: HTMLElement, part: string) => [...c.querySelectorAll(`[data-sprite*="${part}"]`)];

it("every hall view shows the Beispieldaten label", () => {
for (const scene of [SAMPLE_HALL, EMPTY_HALL, undefined]) {
const { unmount } = render(<Hall scene={scene} />);
expect(screen.getByText(de["hall.sample"])).toBeVisible();
unmount();
}
});

it("an empty hall says so in words and shows no figure in the work pose", () => {
const { container } = render(<Hall scene={EMPTY_HALL} />);
expect(screen.getByText(de["hall.empty"])).toBeVisible();
expect(sprites(container, "fig-")).toHaveLength(4);
expect(sprites(container, "-run-")).toEqual([]);
expect(sprites(container, "piece-")).toEqual([]);
});

it("an unread hall says it is not read yet", () => {
const { container } = render(<Hall scene={undefined} />);
expect(screen.getByText(de["hall.unread"])).toBeVisible();
expect(screen.queryByText(de["hall.empty"])).toBeNull();
expect(sprites(container, "fig-")).toEqual([]);
});

it("a missing state shows the unknown pose", () => {
const { container } = render(<Hall scene={{ pieces: [{ id: "probe-x", name: "hall.piece.a", role: "build" }] }} />);
expect(sprites(container, "fig-build-unknown-0")).toHaveLength(1);
expect(screen.getByText(STATE_UNKNOWN_WORD)).toBeVisible();
// A role without a piece has no observed run: the configured profile stands ready.
expect(sprites(container, "fig-plan-ready-0")).toHaveLength(1);
});

it("three work pieces each carry a text name", () => {
const { container } = render(<Hall scene={SAMPLE_HALL} />);
expect(SAMPLE_HALL.pieces).toHaveLength(3);
for (const piece of SAMPLE_HALL.pieces) {
expect(piece.id).toMatch(/^probe-/);
expect(screen.getByText(de[piece.name])).toBeVisible();
}
expect(sprites(container, "piece-")).toHaveLength(3);
expect(sprites(container, "gate-hall-0")).toHaveLength(1);
expect(sprites(container, "prop-stamp-0")).toHaveLength(1);
expect(screen.getByText(de["hall.gate"])).toBeVisible();
expect(screen.getByText(de["hall.prop.stamp"])).toBeVisible();
});
83 changes: 83 additions & 0 deletions src/design/hall/Hall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { CSSProperties } from "react";

import { de } from "../../i18n/de";
import { StateMark } from "../state/StateMark";
import { STATE_UNKNOWN_FORM, type StateForm } from "../state/states";
import { poseFor, ROLE_WORDS, ROLES, type Role } from "./figures/poses";
import type { HallScene } from "./fixtures";
import figureMap from "./sprites/hall-figures.json";
import tileMap from "./sprites/hall-tiles.json";
import { FLOOR, TILE } from "./tiles/layout";
import { NAMES } from "./tiles/names";
import { usePixelScale } from "./usePixelScale";
import "./hall.css";

// DS-H-04 (ASSETS.md §1, §6, §8): the static probe hall on synthetic data. Sprites are decorative; every
// meaning is also HTML text below the scene. Positions are whole tiles, sizes whole art pixels.

type Frame = readonly number[];
const ATLASES = { tiles: tileMap, figures: figureMap } as Record<"tiles" | "figures", Record<string, Frame>>;
const extent = (map: Record<string, Frame>, i: 0 | 1) => Math.max(...Object.values(map).map((f) => f[i] + f[i + 2]));

/** Station of each role at (column, row 2); its figure stands right of it, the piece below it on the belt. */
const STATION_COL: Record<Role, number> = { plan: 0, build: 3, check: 6, ops: 9 };
const GATE = [3, 1];
const T = 16;

function Sprite({ sheet, id, col, row }: { sheet: keyof typeof ATLASES; id: string; col: number; row: number }) {
const map = ATLASES[sheet];
const [x, y, w, h] = map[id];
return (
<span
className={`g-hall__sprite g-hall__sprite--${sheet}`}
data-sprite={id}
// Numbers only; hall.css turns art pixels into CSS pixels with --s.
style={{ "--l": col * T, "--t": row * T, "--w": w, "--h": h, "--x": x, "--y": y, "--aw": extent(map, 0), "--ah": extent(map, 1) } as CSSProperties}
/>
);
}

/** `scene` undefined: nothing read yet. A piece without a state is unknown; a role without a piece is `ready`. */
export function Hall({ scene }: { scene: HallScene | undefined }) {
const s = usePixelScale().css;
const pieces = scene?.pieces ?? [];
const pieceOf = (role: Role) => pieces.find((p) => p.role === role);
return (
<section className="g-hall" aria-label={de["hall.title"]} style={{ "--s": s } as CSSProperties}>
<p className="g-hall__sample">{de["hall.sample"]}</p>
<div className="g-hall__scene" aria-hidden="true" style={{ "--cols": FLOOR[0].length, "--rows": FLOOR.length } as CSSProperties}>
{FLOOR.flatMap((line, row) => [...line].map((c, col) => <Sprite key={`${col}-${row}`} sheet="tiles" id={TILE[c]} col={col} row={row} />))}
<Sprite sheet="tiles" id="gate-hall-0" col={GATE[0]} row={GATE[1]} />
{ROLES.map((role) => {
const c = STATION_COL[role];
const piece = pieceOf(role);
// R1646-K1: poseFor(null) means "no run observed"; a piece with a missing state must stay unknown.
const pose = poseFor(piece ? piece.state ?? STATE_UNKNOWN_FORM : null);
return [
<Sprite key={role} sheet="tiles" id={`station-${role}-0`} col={c} row={2} />,
scene && <Sprite key={`f-${role}`} sheet="figures" id={`fig-${role}-${pose}-0`} col={c + 1} row={1} />,
piece && <Sprite key={piece.id} sheet="tiles" id="piece-crate-0" col={c + 1} row={3} />,
piece?.stamped && <Sprite key="stamp" sheet="tiles" id="prop-stamp-0" col={c + 2} row={3} />,
];
})}
</div>
<ul className="g-hall__legend">
{ROLES.map((role) => {
const piece = pieceOf(role);
return (
<li key={role}>
<span>{de[NAMES[`station-${role}-0`]]} <span className="g-hall__ex">{de["hall.example"]}</span></span>
<span>{ROLE_WORDS[role]}</span>
{piece && <span>{de[piece.name]}</span>}
{piece && <StateMark form={piece.state as StateForm | undefined} />}
{piece?.stamped && <span>{de[NAMES["prop-stamp-0"]]}</span>}
</li>
);
})}
</ul>
<p className="g-hall__note"><span>{de[NAMES["gate-hall-0"]]}</span> <span className="g-hall__ex">{de["hall.example"]}</span></p>
{!scene && <p className="g-hall__note">{de["hall.unread"]}</p>}
{scene && pieces.length === 0 && <p className="g-hall__note">{de["hall.empty"]}</p>}
</section>
);
}
28 changes: 28 additions & 0 deletions src/design/hall/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { MessageKey } from "../../i18n/de";
import type { Role } from "./figures/poses";

// DS-H-04 (ASSETS.md §8): fixed synthetic data for the probe hall, never real agents, PRs or people.

export interface HallPiece {
id: string;
name: MessageKey;
role: Role;
/** The observed state form; missing means not observed and draws as unknown. */
state?: string;
/** A valid evidence stamp lies next to the piece. */
stamped?: boolean;
}

export interface HallScene {
pieces: readonly HallPiece[];
}

export const SAMPLE_HALL: HallScene = {
pieces: [
{ id: "probe-a", name: "hall.piece.a", role: "build", state: "run" },
{ id: "probe-b", name: "hall.piece.b", role: "check", state: "ok", stamped: true },
{ id: "probe-c", name: "hall.piece.c", role: "ops" },
],
};

export const EMPTY_HALL: HallScene = { pieces: [] };
52 changes: 52 additions & 0 deletions src/design/hall/hall.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* DS-H-04 probe hall. Only the active theme's atlas loads (ASSETS.md §5); tokens only, no colour literal. */
.g-hall {
--hall-tiles: url("./sprites/hall-tiles-light.png");
--hall-figures: url("./sprites/hall-figures-light.png");
display: grid;
gap: var(--g-space-4);
justify-items: start;
color: var(--g-ink);
font: var(--g-t-note);
}
:root[data-theme="dark"] .g-hall {
--hall-tiles: url("./sprites/hall-tiles-dark.png");
--hall-figures: url("./sprites/hall-figures-dark.png");
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .g-hall {
--hall-tiles: url("./sprites/hall-tiles-dark.png");
--hall-figures: url("./sprites/hall-figures-dark.png");
}
}
.g-hall__sample { margin: 0; font: var(--g-t-label); letter-spacing: var(--g-track-eyebrow); }
.g-hall__scene {
position: relative;
width: calc(var(--cols) * 16px * var(--s));
height: calc(var(--rows) * 16px * var(--s));
}
.g-hall__sprite {
position: absolute;
left: calc(var(--l) * 1px * var(--s));
top: calc(var(--t) * 1px * var(--s));
width: calc(var(--w) * 1px * var(--s));
height: calc(var(--h) * 1px * var(--s));
background-position: calc(var(--x) * -1px * var(--s)) calc(var(--y) * -1px * var(--s));
background-size: calc(var(--aw) * 1px * var(--s)) calc(var(--ah) * 1px * var(--s));
background-repeat: no-repeat;
image-rendering: pixelated;
}
.g-hall__sprite--tiles { background-image: var(--hall-tiles); }
.g-hall__sprite--figures { background-image: var(--hall-figures); }
/* One column per station, at least as wide as its three-tile group. */
.g-hall__legend {
display: grid;
grid-template-columns: repeat(4, minmax(calc(48px * var(--s)), max-content));
column-gap: var(--g-space-6);
margin: 0;
padding: 0;
list-style: none;
}
.g-hall__legend > li { display: grid; gap: var(--g-space-1); align-content: start; justify-items: start; }
.g-hall__ex, .g-hall__note { color: var(--g-muted); }
.g-hall__legend .g-hall__ex { display: block; }
.g-hall__note { margin: 0; }
Binary file modified src/design/hall/sprites/hall-tiles-dark.png
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 src/design/hall/sprites/hall-tiles-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/design/hall/sprites/hall-tiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"station-ops-0": [32,16,16,16],
"gate-hall-0": [48,16,16,16],
"prop-tray-0": [64,16,16,16],
"prop-stamp-0": [80,16,16,16]
"prop-stamp-0": [80,16,16,16],
"piece-crate-0": [96,16,16,16]
}
19 changes: 19 additions & 0 deletions src/design/hall/tiles/stations.px.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,23 @@ export const frames: Frames = {
".....44444444...",
"................",
],
// DS-H-04 work piece: a slatted crate, the same for every piece; its name is HTML text.
"piece-crate-0": [
"................",
"................",
"..111111111111..",
"..1444444444214.",
"..14bbbbbbbb214.",
"..1111111111114.",
"..14bbbbbbbb214.",
"..14bbbbbbbb214.",
"..1111111111114.",
"..14bbbbbbbb214.",
"..1222222222214.",
"..1111111111114.",
"...444444444444.",
"................",
"................",
"................",
],
};
6 changes: 6 additions & 0 deletions src/design/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ export const COMPONENT_REGISTRY: readonly ComponentEntry[] = [
a11y: "role=\"img\" named by label plus state word; the inner glyph is mandatory, colour is never alone.",
text: "src/i18n/de.ts fabrik.orbName and state words via states.ts; label from the caller.",
},
{
name: "Hall", family: "hall",
variants: [], states: ["filled", "empty pieces: empty in words", "scene undefined: unread in words", "missing piece state: unknown pose and word"],
a11y: "Labelled region; sprites are aria-hidden, every station, role, piece, gate and stamp is also text.",
text: "src/i18n/de.ts hall.* and state words via StateMark; always shows hall.sample.",
},
{
name: "Avatar", family: "inputs",
variants: ["size sm, md, lg; tone owner, lead, sys, 1 to 6"], states: ["unknown tone draws the plain avatar"],
Expand Down
8 changes: 8 additions & 0 deletions src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export const de = {
"hall.gate": "Tor",
"hall.prop.tray": "Übergabeablage",
"hall.prop.stamp": "Stempel",
"hall.title": "Probehalle",
"hall.sample": "Beispieldaten",
"hall.example": "Beispiel",
"hall.empty": "Die Halle ist leer: kein Werkstück da.",
"hall.unread": "Noch nicht gelesen: der Stand der Halle ist unbekannt.",
"hall.piece.a": "Beispielstück A",
"hall.piece.b": "Beispielstück B",
"hall.piece.c": "Beispielstück C",
"settings.changelog.title": "Neuigkeiten",
"settings.changelog.summary": "Die neuesten Versionen · bis zu drei Änderungen je Version",
"settings.changelog.empty": "Keine Neuigkeiten vorhanden",
Expand Down
Loading