From 6aef29aa8ee5d534c1f45328bc47b8cd1391f273 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Fri, 6 Mar 2026 08:30:46 -0500 Subject: [PATCH] Add a tap-to-select/tap-to-place interface This sits alongside the existing drag-and-drop interace, so either can be used. --- letterrip/letterrip.css | 7 ++ letterrip/letterrip.jsx | 154 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 147 insertions(+), 14 deletions(-) diff --git a/letterrip/letterrip.css b/letterrip/letterrip.css index 27498ce..2c4a6fe 100644 --- a/letterrip/letterrip.css +++ b/letterrip/letterrip.css @@ -100,6 +100,13 @@ border-style: dashed; } +.tile.selected { + outline: 3px solid #3498db; + outline-offset: -1px; + transform: scale(1.1); + z-index: 1; +} + .tile.dragging { opacity: 0.4; } diff --git a/letterrip/letterrip.jsx b/letterrip/letterrip.jsx index da78d90..f524636 100644 --- a/letterrip/letterrip.jsx +++ b/letterrip/letterrip.jsx @@ -288,11 +288,13 @@ function BlankTileModal(props) { } function Tile(props) { - const { letter, isBlank, invalid, unconnected, onDragStart, onDragEnd, onClick } = props; + const { letter, isBlank, invalid, unconnected, selected, + onDragStart, onDragEnd, onClick } = props; let className = "tile"; if (isBlank) className += " blank"; if (invalid) className += " invalid"; if (unconnected) className += " unconnected"; + if (selected) className += " selected"; return (
i !== sel.rack_index); + } else if (sel.from === "grid") { + delete new_grid[sel.grid_key]; + } + + /* Blank tile that hasn't been assigned a letter — show modal. */ + if (is_blank && !(sel.from === "grid" && this.state.grid[sel.grid_key] && this.state.grid[sel.grid_key].letter)) { + this.setState({ + grid: new_grid, + rack: new_rack, + selected: null, + blank_pending: { r, c, tileIndex: tile_index } + }); + return; + } + + /* Preserve existing letter assignment for blanks being moved. */ + const existing = sel.from === "grid" ? this.state.grid[sel.grid_key] : null; + const letter = existing ? existing.letter : tile_char; + const isBlank = existing ? existing.isBlank : is_blank; + + new_grid[grid_key] = { letter, tileIndex: tile_index, isBlank }; + this.setState({ - blank_pending: { r, c, tileIndex: cell.tileIndex } + grid: new_grid, + rack: new_rack, + selected: null }); } @@ -635,7 +751,7 @@ class Game extends React.Component { } render_board(analysis, invalid_cells, unconnected_cells) { - const { grid, drag_over_cell, drag_source } = this.state; + const { grid, drag_over_cell, drag_source, selected } = this.state; const bounds = grid_bounds(grid, this.state.grid_bounds); /* Store bounds so the grid only grows, never shrinks. */ if (JSON.stringify(bounds) !== JSON.stringify(this.state.grid_bounds)) { @@ -657,15 +773,18 @@ class Game extends React.Component { className={className} onDragOver={(e) => this.on_cell_drag_over(e, key)} onDragLeave={() => this.setState({ drag_over_cell: null })} - onDrop={(e) => this.on_cell_drop(e, r, c)}> + onDrop={(e) => this.on_cell_drop(e, r, c)} + onClick={!cell && selected ? () => this.on_cell_tap(r, c) : null}> {cell ? ( this.on_grid_drag_start(e, key)} onDragEnd={() => this.on_drag_end()} - onClick={cell.isBlank ? () => this.on_blank_click(key) : null} /> + onClick={() => this.on_grid_tile_tap(key)} /> ) : null}
); @@ -686,7 +805,7 @@ class Game extends React.Component { } render_rack(can_complete) { - const { rack, tiles, rack_drag_over } = this.state; + const { rack, tiles, rack_drag_over, selected } = this.state; let className = "tile-rack"; if (rack_drag_over) className += " drag-over"; @@ -695,7 +814,11 @@ class Game extends React.Component { className={className} onDragOver={(e) => this.on_rack_drag_over(e)} onDragLeave={(e) => this.on_rack_drag_leave(e)} - onDrop={(e) => this.on_rack_drop(e)}> + onDrop={(e) => this.on_rack_drop(e)} + onClick={(e) => { + /* Clicking empty rack area returns a selected grid tile. */ + if (e.target === e.currentTarget) this.on_rack_tap(); + }}> {rack.map((tile_index, rack_index) => { const char = tiles[tile_index]; const is_blank = (char === "_" || char === undefined); @@ -703,15 +826,18 @@ class Game extends React.Component { console.error("Unexpected tile character:", JSON.stringify(char), "at index", tile_index, "tiles:", JSON.stringify(tiles)); } + const is_selected = selected && selected.from === "rack" + && selected.tile_index === tile_index; return ( this.on_rack_drag_start(e, rack_index)} onDragEnd={() => this.on_drag_end()} - onClick={null} /> + onClick={() => this.on_rack_tile_tap(rack_index)} /> ); })} {can_complete ? ( -- 2.45.2