]> git.cworth.org Git - lmno.games/commitdiff
Several fixes for styling of the "blank modal" selector
authorCarl Worth <cworth@cworth.org>
Fri, 6 Mar 2026 21:44:58 +0000 (16:44 -0500)
committerCarl Worth <cworth@cworth.org>
Sat, 7 Mar 2026 13:19:16 +0000 (08:19 -0500)
This is the modal dialog that pops up when the user clicks on a
blank tile to select what letter it represents. The changes in
this commit are:

1. Use the same appearance (referencing the same CSS class) for
   the tiles in this modal as for tiles in other contexts, (this
   replaces the previous custom styling which was white text on
   a light background and hard to read).

2. Restructure the modal so that it is centered over the grid
   rather than centered over the viewport (also constrain the
   rack to not be wider than the grid).

3. Fix a bug that resulted from the user cancelling a selection of a
   blank tile, (it was "returned" to the rack before being properly
   removed from the rack, resulting in two occurences of the same tile
   on the rack). See the reassign_true logic in this commit for the
   relevant fix.

letterrip/letterrip.css
letterrip/letterrip.jsx

index 32458c1e2302b1023fbcd7c8e358bdb2225de2c6..653df6356612e052ca64a60ea1e865edbd38285c 100644 (file)
@@ -92,6 +92,7 @@
   justify-content: center;
   width: 44px;
   height: 44px;
+  color: #000;
   background: #f5e6c8;
   border: 2px solid #c9a96e;
   border-radius: 4px;
 }
 
 /* Board grid */
+.board-and-rack {
+  display: inline-flex;
+  flex-direction: column;
+  max-width: 100%;
+}
+
 .board-container {
   margin-bottom: 1em;
   overflow: auto;
   max-width: 100%;
+  --cell-size: 48px;
 }
 
 .board {
   background: #fff;
 }
 
-.board-container {
-  --cell-size: 48px;
-}
-
 .cell {
   width: var(--cell-size);
   height: var(--cell-size);
   cursor: grab;
 }
 
-/* Blank tile modal */
+/* Blank tile modal — positioned relative to the board-and-rack
+   container so it centers over the grid, not the full viewport. */
+.board-and-rack {
+  position: relative;
+}
+
 .blank-modal-overlay {
   position: fixed;
   top: 0;
   right: 0;
   bottom: 0;
   background: rgba(0,0,0,0.4);
+  z-index: 100;
+}
+
+.blank-modal-anchor {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
   display: flex;
   align-items: center;
   justify-content: center;
-  z-index: 100;
+  z-index: 101;
 }
 
 .blank-modal {
 .blank-modal .letter-btn {
   width: 38px;
   height: 38px;
-  border: 1px solid #999;
-  border-radius: 4px;
-  background: #f5e6c8;
   font-size: 18px;
-  font-weight: bold;
   cursor: pointer;
-  display: flex;
-  align-items: center;
-  justify-content: center;
 }
 
 .blank-modal .letter-btn:hover {
index 4d58ca8e58afe68091ff562d8568f999d91fb69f..2fea89a15dc5b8eea8fbf5a53f76b7b6634cbf99 100644 (file)
@@ -286,14 +286,15 @@ function PlayerList(props) {
 
 function BlankTileModal(props) {
   const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
-  return (
-    <div className="blank-modal-overlay" onClick={props.onCancel}>
+  return [
+    <div key="overlay" className="blank-modal-overlay" onClick={props.onCancel} />,
+    <div key="anchor" className="blank-modal-anchor" onClick={props.onCancel}>
       <div className="blank-modal" onClick={e => e.stopPropagation()}>
         <h3>Choose a letter for blank tile</h3>
         <div className="letter-grid">
           {letters.map(l => (
             <button key={l}
-                    className="letter-btn"
+                    className="tile letter-btn"
                     onClick={() => props.onChoose(l)}>
               {l}
             </button>
@@ -301,7 +302,7 @@ function BlankTileModal(props) {
         </div>
       </div>
     </div>
-  );
+  ];
 }
 
 function Tile(props) {
@@ -836,7 +837,7 @@ class Game extends React.Component {
     if (cell && cell.isBlank) {
       const [r, c] = grid_key.split(",").map(Number);
       this.setState({
-        blank_pending: { r, c, tileIndex: cell.tileIndex }
+        blank_pending: { r, c, tileIndex: cell.tileIndex, reassign: true }
       });
       return;
     }
@@ -964,29 +965,35 @@ class Game extends React.Component {
         </div>
       ) : null,
 
-      state.joined ? this.render_board(analysis, invalid_cells, unconnected_cells) : null,
-
-      state.joined ? this.render_rack(can_complete) : null,
-
-      state.blank_pending ? (
-        <BlankTileModal
-          key="bm"
-          onChoose={(l) => this.choose_blank_letter(l)}
-          onCancel={() => {
-            /* Put the blank tile back on the rack if cancelled. */
-            const { tileIndex } = state.blank_pending;
-            const new_rack = [...state.rack, tileIndex];
-            this.setState({ blank_pending: null, rack: new_rack });
-          }}
-        />
-      ) : null,
-
-      state.joined && Object.keys(state.grid).length > 0 && !can_complete ? (
-        <div key="status" className="status">
-          {!analysis.all_valid ? "Some words are not valid." :
-           !analysis.all_connected ? "All tiles must be connected." :
-           state.rack.length > 0 ? "Place all your tiles to complete." :
-           null}
+      state.joined ? (
+        <div key="board-rack" className="board-and-rack">
+          {this.render_board(analysis, invalid_cells, unconnected_cells)}
+          {this.render_rack(can_complete)}
+          {state.blank_pending ? (
+            <BlankTileModal
+              onChoose={(l) => this.choose_blank_letter(l)}
+              onCancel={() => {
+                /* Put the blank tile back on the rack if cancelled,
+                 * but only if it was freshly placed (not a reassignment
+                 * of a blank already on the grid). */
+                if (state.blank_pending.reassign) {
+                  this.setState({ blank_pending: null });
+                } else {
+                  const { tileIndex } = state.blank_pending;
+                  const new_rack = [...state.rack, tileIndex];
+                  this.setState({ blank_pending: null, rack: new_rack });
+                }
+              }}
+            />
+          ) : null}
+          {Object.keys(state.grid).length > 0 && !can_complete ? (
+            <div className="status">
+              {!analysis.all_valid ? "Some words are not valid." :
+               !analysis.all_connected ? "All tiles must be connected." :
+               state.rack.length > 0 ? "Place all your tiles to complete." :
+               null}
+            </div>
+          ) : null}
         </div>
       ) : null
     ];
@@ -1036,7 +1043,7 @@ class Game extends React.Component {
     const cols = bounds.maxC - bounds.minC + 1;
 
     return (
-      <div key="board" className="board-container">
+      <div className="board-container">
         <div className="board"
              style={{ gridTemplateColumns: `repeat(${cols}, var(--cell-size))` }}>
           {rows.flat()}
@@ -1051,8 +1058,7 @@ class Game extends React.Component {
     if (rack_drag_over) className += " drag-over";
 
     return (
-      <div key="rack"
-           data-rack="true"
+      <div data-rack="true"
            className={className}
            onDragOver={(e) => this.on_rack_drag_over(e)}
            onDragLeave={(e) => this.on_rack_drag_leave(e)}