Blocks
Blocks are the visible, draggable units of the program. This page covers the actions, the palette, the block chrome, and the container blocks.
The eight actions
domain/actions.ts defines the full set:
| Action | Kind | What it does |
|---|---|---|
start | synthetic | The implicit program head; never draggable, never stored. |
move-forward | leaf | One step in the facing direction, then an ice slide if the floor is slippery. |
turn-right | leaf | Rotate 90 degrees clockwise. |
turn-left | leaf | Rotate 90 degrees counter-clockwise. |
for | container | Repeat its children count times (1 to 99). |
while | container | Repeat its children until the mouse reaches the target. |
if | container | Run children when the condition is true, otherwise elseChildren. |
switch | container | Run the first branch whose condition is true; default matches always and sorts last. |
Conditions (domain/conditions.ts) are the three relative directions:
path-ahead, path-left, path-right. A condition is true when the
adjacent cell in that direction (relative to facing) is passable; walls
fail, and so does being out of bounds. Note that lava and holes are passable,
so a path condition reads them as open: that is exactly what makes them
traps (see Simulation).
The registry
blocks/registry.tsx is the one place that maps an action name to its UI:
type BlockDefinition = {
action: ActionName;
label: string;
render: (props?: {...}) => JSX.Element;
};
resolveActions(names) turns a level's allowedActions into the palette
list. Every container has two render modes, passed explicitly as
variant='palette' | 'sequence': the palette shows an empty container, the
sequence shows the container with its children and drop zones. Start is the
fixed head of the program (blocks/Start.tsx).
Block chrome: BlockShell
blocks/BlockShell.tsx owns every visual aspect a block shares, so no block
re-declares bg-*/border-* classes:
- A rounded "clipart" body with a top notch and a bottom nub (the Scratch puzzle-piece look, drawn with two rotated squares behind the content).
- A variant system:
sky,emerald,yellow,green,red,orangefor solid blocks,washed-out-*for container bodies (60% opacity vs 80%), andgrayfor locked/default blocks. Each maps to semantic tokens (border-block-*-border,bg-block-*/80, hover and active states). - A drag-over state that swaps the washed bodies to their stronger
-washed-hovertokens. - Sizing that scales with the viewport (
clamp()paddings and text, a shared--block-height), so blocks read the same on a phone and a projector.
One subtle rule: only the solid variants are CSS hover groups
(group-hover:), because a group on a washed container body would light up
every nested block when the pointer is over the body, instead of only the
block actually under the pointer.
The container blocks
- ForBlock - shows the
forheader with a repeat-count input (block_for_*messages) and a body drop zone.fornever collapses. - WhileBlock - "while target not reached" header and a body. Collapsible.
- IfBlock - a condition chooser (
path-ahead/path-left/path-right), a then-zone and an else-zone. Collapsible. - SwitchBlock - a list of branches, each with its own condition and
body; the
defaultbranch always sorts last. Collapsible.
while, if and switch share BlockCollapseToggle (hover, active and
focus-visible states in one place); when collapsed, their bodies are
summarised with a translated plural message (block_container_collapsed_count).
The palette
features/level/components/Palette.tsx renders the level's blocks as
draggables, wrapped in a fixed-height, scrollable tray
(--palette-height) so it never grows or shrinks between levels. The tray
doubles as the delete zone: dragging an existing block over it shows a
"Drop to delete" hint (palette_drop_to_delete) and dropping removes the
block. While a fresh block is being dragged out of the palette, its source is
hidden (invisible) but the palette keeps its width, so the layout never
jumps.
How blocks relate to the data
The UI never stores anything itself. A block is a rendering of a
SequenceItem node (see The program tree); drags
and drops mutate that tree through domain/sequence-tree.ts; labels and
conditions stay in the UI layer (blocks/conditionLabel.ts) so domain/
remains free of i18n.
Next: The program tree.