feat(ui): vs-AI comms trim, overlay confirm button, recall-to-slot drag
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 51s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
- vs-AI: the comms hub drops the Chat tab (Dictionary only); a finished AI game has no comms at all, so its 💬 history-header entry is hidden too. - Confirm-move ✅: redone as an absolute overlay pinned to the right edge (its right edge sits under the preview caption), so the rack keeps a fixed tile size — this reverts the tile-shrink that resized the letters at 6 tiles. - Recall: dragging a placed tile back to the rack now drops it at the slot the pointer is over (drag-to-position, a gap opens), instead of snapping to its original slot; double-tap still recalls to the origin. New pure recallToSlot. Tests: placement recallToSlot units; e2e recall-to-position; vs-AI comms e2e updated. Docs: UI_DESIGN + FUNCTIONAL (+ru).
This commit is contained in:
+34
-11
@@ -31,6 +31,7 @@
|
||||
placementFromHint,
|
||||
rackView,
|
||||
recallAt,
|
||||
recallToSlot,
|
||||
reorderIndices,
|
||||
reset,
|
||||
toSubmit,
|
||||
@@ -481,7 +482,12 @@
|
||||
if (c) {
|
||||
dropTarget = !board[c.row]?.[c.col] && !pendingMap.has(`${c.row},${c.col}`) ? c : null;
|
||||
reorderTo = null;
|
||||
} else if (reorderDragId != null && overRack(e.clientY) && placement.pending.length === 0) {
|
||||
} else if (
|
||||
overRack(e.clientY) &&
|
||||
// A rack-source reorder (only with a clean, pending-free rack) or a board tile dragged
|
||||
// back to the rack — both preview the drop slot as a gap the dropped tile will fill.
|
||||
((reorderDragId != null && placement.pending.length === 0) || draggingPend != null)
|
||||
) {
|
||||
reorderTo = dropSlotAt(e.clientX);
|
||||
dropTarget = null;
|
||||
} else {
|
||||
@@ -527,8 +533,15 @@
|
||||
// Dropped a placed tile on another board cell → relocate it there.
|
||||
relocatePending(di.src.row, di.src.col, cell.row, cell.col);
|
||||
} else if (di.src.from === 'board' && onRack) {
|
||||
// Dropped a placed tile back onto the rack → recall it to its original slot.
|
||||
placement = recallAt(placement, di.src.row, di.src.col);
|
||||
// Dropped a placed tile back onto the rack → recall it to the drop slot the player aimed
|
||||
// at (drag-to-position), or to its original slot when the drop is not over a gap.
|
||||
if (to != null) {
|
||||
const res = recallToSlot(placement, di.src.row, di.src.col, to);
|
||||
rackIds = res.order.map((i) => rackIds[i] ?? i);
|
||||
placement = res.placement;
|
||||
} else {
|
||||
placement = recallAt(placement, di.src.row, di.src.col);
|
||||
}
|
||||
selected = null;
|
||||
recompute();
|
||||
scheduleDraftSave();
|
||||
@@ -1114,9 +1127,13 @@
|
||||
<button class="hicon" onclick={() => (resignOpen = true)} disabled={waitingForOpponent} aria-label={t('game.dropGame')}>🏁</button>
|
||||
{/if}
|
||||
{#if !view.game.multipleWordsPerTurn}<span class="oneword-label">{t('game.oneWordRule')}</span>{/if}
|
||||
<button class="hicon" onclick={() => navigate(`/game/${id}/chat`)} aria-label={t('game.chat')}>
|
||||
{#key chatBlink}<span class="chat-ico" class:blink={chatBlink > 0 && !app.reduceMotion}>💬</span>{/key}
|
||||
</button>
|
||||
<!-- A finished AI game has no comms at all (no chat, and the dictionary closes with the
|
||||
game), so the entry is dropped; an active AI game keeps it (it opens the dictionary). -->
|
||||
{#if !(gameOver && view.game.vsAi)}
|
||||
<button class="hicon" onclick={() => navigate(`/game/${id}/chat`)} aria-label={t('game.chat')}>
|
||||
{#key chatBlink}<span class="chat-ico" class:blink={chatBlink > 0 && !app.reduceMotion}>💬</span>{/key}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="hgridwrap">
|
||||
<div class="hgrid" style="grid-template-columns: repeat({view.game.seats.length}, 1fr)">
|
||||
@@ -1456,9 +1473,9 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rack-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
padding: 0 var(--pad) 6px;
|
||||
}
|
||||
@@ -1471,11 +1488,17 @@
|
||||
min-width: 0;
|
||||
}
|
||||
/* A borderless icon button (like the tab bar), not a filled accent button — and disabled
|
||||
while the pending word is known to be illegal. The glyph is right-aligned within its box so
|
||||
its right edge lands under the right edge of the preview caption above it (both sit at
|
||||
var(--pad) from the screen edge — .status and .rack-row share that padding). */
|
||||
while the pending word is known to be illegal. It is an overlay, NOT a flex sibling of the
|
||||
rack: the rack keeps the full row width with a fixed tile size (no reflow or tile resize
|
||||
when a tile is staged), and the button is absolutely pinned over the slots a staged tile
|
||||
frees on the right. Its right edge sits at var(--pad) — directly under the right edge of the
|
||||
preview caption above (.status shares that padding). */
|
||||
.make {
|
||||
min-width: 56px;
|
||||
position: absolute;
|
||||
right: var(--pad);
|
||||
top: 0;
|
||||
bottom: 6px;
|
||||
width: 56px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
border: none;
|
||||
|
||||
Reference in New Issue
Block a user