From b0ff80ddb29822eece319cab5f975b3b8ae5bc99 Mon Sep 17 00:00:00 2001 From: ianfhunter Date: Mon, 14 Sep 2026 20:05:22 +0100 Subject: [PATCH 1/2] Add draft entry so solvers can mark tentative letters --- app/__tests__/puzzleReducer.test.ts | 116 +++++++++++++++++++++++++++- app/components/Cell.module.css | 9 +++ app/components/Cell.tsx | 9 ++- app/components/ClueList.module.scss | 4 + app/components/ClueList.tsx | 8 +- app/components/Grid.tsx | 2 + app/components/Keyboard.module.scss | 5 ++ app/components/Keyboard.tsx | 27 +++++++ app/components/Puzzle.tsx | 67 +++++++++++++--- app/lib/dbtypes.ts | 2 + app/lib/style.ts | 13 ++++ app/lib/types.ts | 3 + app/locales/en/messages.po | 14 +++- app/locales/es/messages.po | 12 +++ app/locales/fr/messages.po | 12 +++ app/locales/id/messages.po | 12 +++ app/locales/it/messages.po | 12 +++ app/reducers/gridReducer.ts | 59 ++++++++------ app/reducers/puzzleReducer.ts | 11 +++ app/reducers/puzzleUtils.ts | 6 ++ 20 files changed, 360 insertions(+), 43 deletions(-) diff --git a/app/__tests__/puzzleReducer.test.ts b/app/__tests__/puzzleReducer.test.ts index 1961b763..c4291f38 100644 --- a/app/__tests__/puzzleReducer.test.ts +++ b/app/__tests__/puzzleReducer.test.ts @@ -4,9 +4,11 @@ import { getMockedPuzzle } from '../lib/getMockedPuzzle'; import { markdownToHast } from '../lib/markdown/markdown'; -import { BLOCK, CheatUnit, puzzleFromDB } from '../lib/types'; +import { BLOCK, CheatUnit, Direction, KeyK, puzzleFromDB } from '../lib/types'; import { allSolutions } from '../lib/utils'; import { addClues, fromCells } from '../lib/viewableGrid'; +import { KeypressAction } from '../reducers/commonActions'; +import { PuzzleState, puzzleReducer } from '../reducers/puzzleReducer'; import { CheatablePuzzleState, cheat, @@ -136,6 +138,7 @@ test('check without alt', () => { verifiedCells: new Set(), revealedCells: new Set(), wrongCells: new Set(), + draftCells: new Set(), grid, solutions: allSolutions(dbpuz.g, [])[0], cellsIterationCount: [], @@ -184,6 +187,7 @@ test('check with alt', () => { verifiedCells: new Set(), revealedCells: new Set(), wrongCells: new Set(), + draftCells: new Set(), grid, solutions: allSolutions(dbpuz.g, [[[0, 'M']]])[0], cellsIterationCount: [], @@ -204,3 +208,113 @@ test('check with alt', () => { } `); }); + +function getPuzzleState(): PuzzleState { + const dbpuz = getMockedPuzzle(); + const fromDB = puzzleFromDB(dbpuz, 'puzId'); + const ourGrid = fromDB.grid.map((s): string => (s === BLOCK ? BLOCK : ' ')); + const grid = addClues( + fromCells({ + mapper: (e) => e, + width: fromDB.size.cols, + height: fromDB.size.rows, + cells: ourGrid, + allowBlockEditing: false, + cellStyles: new Map(), + vBars: new Set(fromDB.vBars), + hBars: new Set(fromDB.hBars), + hidden: new Set(fromDB.hidden), + }), + fromDB.clues, + (c: string) => markdownToHast({ text: c, inline: true }) + ); + return { + type: 'puzzle', + wasEntryClick: false, + active: { col: 0, row: 0, dir: Direction.Across }, + grid, + showExtraKeyLayout: false, + answers: fromDB.grid, + alternateSolutions: fromDB.alternateSolutions, + solutions: allSolutions(fromDB.grid, fromDB.alternateSolutions)[0], + verifiedCells: new Set(), + wrongCells: new Set(), + revealedCells: new Set(), + draftCells: new Set(), + draftMode: false, + downsOnly: false, + isEnteringRebus: false, + rebusValue: '', + success: false, + ranSuccessEffects: false, + filled: false, + autocheck: false, + dismissedKeepTrying: false, + dismissedSuccess: false, + moderating: false, + showingEmbedOverlay: false, + displaySeconds: 0, + bankedSeconds: 0, + ranMetaSubmitEffects: false, + currentTimeWindowStart: Date.now(), + didCheat: false, + clueView: false, + cellsUpdatedAt: fromDB.grid.map(() => 0), + cellsIterationCount: fromDB.grid.map(() => 0), + cellsEverMarkedWrong: new Set(), + loadedPlayState: true, + isEditable(cellIndex) { + return !this.verifiedCells.has(cellIndex) && !this.success; + }, + }; +} + +function press(key: KeypressAction['key']): KeypressAction { + return { type: 'KEYPRESS', key }; +} + +test('draft mode toggles with period or draft key', () => { + const state = getPuzzleState(); + const withDot = puzzleReducer(state, press({ k: KeyK.Dot })); + expect(withDot.draftMode).toBe(true); + const toggledOff = puzzleReducer(withDot, press({ k: KeyK.Draft })); + expect(toggledOff.draftMode).toBe(false); +}); + +test('letters entered in draft mode are marked draft until confirmed', () => { + let state = getPuzzleState(); + state = puzzleReducer(state, press({ k: KeyK.Draft })); + state = puzzleReducer(state, press({ k: KeyK.AllowedCharacter, c: 'A' })); + expect(state.grid.cells[0]).toBe('A'); + expect(state.draftCells.has(0)).toBe(true); + + state = puzzleReducer(state, press({ k: KeyK.Draft })); + state = puzzleReducer(state, press({ k: KeyK.ArrowLeft })); + state = puzzleReducer(state, press({ k: KeyK.AllowedCharacter, c: 'B' })); + expect(state.grid.cells[0]).toBe('B'); + expect(state.draftCells.has(0)).toBe(false); +}); + +test('backspace removes draft status', () => { + let state = getPuzzleState(); + state = puzzleReducer(state, press({ k: KeyK.Draft })); + state = puzzleReducer(state, press({ k: KeyK.AllowedCharacter, c: 'A' })); + expect(state.draftCells.has(0)).toBe(true); + + state = puzzleReducer(state, press({ k: KeyK.ArrowLeft })); + state = puzzleReducer(state, press({ k: KeyK.Backspace })); + expect(state.grid.cells[0]?.trim()).toBe(''); + expect(state.draftCells.has(0)).toBe(false); +}); + +test('revealing a draft cell confirms it', () => { + let state = getPuzzleState(); + state = puzzleReducer(state, press({ k: KeyK.Draft })); + state = puzzleReducer(state, press({ k: KeyK.AllowedCharacter, c: 'A' })); + expect(state.draftCells.has(0)).toBe(true); + + state = puzzleReducer(state, press({ k: KeyK.ArrowLeft })); + state = cheat(state, CheatUnit.Square, true); + expect(state.draftCells.has(0)).toBe(false); + expect(state.verifiedCells.has(0)).toBe(true); +}); diff --git a/app/components/Cell.module.css b/app/components/Cell.module.css index df65e5b1..52266efa 100644 --- a/app/components/Cell.module.css +++ b/app/components/Cell.module.css @@ -36,6 +36,7 @@ --current-cell-bg: var(--cell-bg); --current-cell-text: var(--text); --current-cell-verified: var(--verified-on-bg); + --current-cell-draft: var(--draft-on-bg); position: relative; float: left; @@ -75,24 +76,28 @@ --current-cell-bg: var(--primary); --current-cell-text: var(--onprimary); --current-cell-verified: var(--verified-on-primary); + --current-cell-draft: var(--draft-on-primary); } .cellContainerSelected { --current-cell-bg: var(--selected-cell); --current-cell-text: var(--on-selected-cell); --current-cell-verified: var(--verified-on-selected-cell); + --current-cell-draft: var(--draft-on-selected-cell); } .cellContainerEntryCell { --current-cell-bg: var(--lighter); --current-cell-text: var(--on-lighter); --current-cell-verified: var(--verified-on-lighter); + --current-cell-draft: var(--draft-on-lighter); } .cellContainerRefed { --current-cell-bg: var(--secondary); --current-cell-text: var(--on-secondary); --current-cell-verified: var(--verified-on-secondary); + --current-cell-draft: var(--draft-on-secondary); } .cq { @@ -186,6 +191,10 @@ color: var(--current-cell-text); } +.contentsDraft { + color: var(--current-cell-draft); +} + .borderBottom { border-bottom: 1px solid black; } diff --git a/app/components/Cell.tsx b/app/components/Cell.tsx index 01315a2b..2077bf49 100644 --- a/app/components/Cell.tsx +++ b/app/components/Cell.tsx @@ -33,6 +33,7 @@ interface CellProps { onMouseEnter: (pos: Position) => void; isVerified: boolean | undefined; isWrong: boolean | undefined; + isDraft: boolean | undefined; wasRevealed: boolean | undefined; cellColor?: number; isOpposite: boolean; @@ -179,9 +180,11 @@ export const Cell = memo(function Cell(props: CellProps) { styles.contents, props.isVerified ? styles.contentsVerified - : filledValue - ? styles.contentsFilled - : null + : props.isDraft && filledValue + ? styles.contentsDraft + : filledValue + ? styles.contentsFilled + : null )} > {props.isWrong ? ( diff --git a/app/components/ClueList.module.scss b/app/components/ClueList.module.scss index ec4eb21c..f5610300 100644 --- a/app/components/ClueList.module.scss +++ b/app/components/ClueList.module.scss @@ -94,6 +94,10 @@ text-align: center; } +.clueModeCell[data-draft='true'] { + color: var(--draft-on-bg); +} + .clueModeCell[data-active-cell='true'] { border: 1px solid var(--black); } diff --git a/app/components/ClueList.tsx b/app/components/ClueList.tsx index 25c9bc40..7f4de18e 100644 --- a/app/components/ClueList.tsx +++ b/app/components/ClueList.tsx @@ -1,6 +1,6 @@ import type { Root } from 'hast'; import { Dispatch, MouseEvent, RefObject, memo, useRef } from 'react'; -import { EntryBase, GridBase, valAt } from '../lib/gridBase.js'; +import { EntryBase, GridBase, cellIndex, valAt } from '../lib/gridBase.js'; import { Position, directionString } from '../lib/types.js'; import { CluedEntry } from '../lib/viewableGrid.js'; import { PuzzleAction } from '../reducers/commonActions.js'; @@ -25,6 +25,7 @@ interface ClueListItemProps { grid: GridBase; scrollToCross: boolean; listRef: RefObject; + draftCells?: Set; } const ClueListItem = memo(function ClueListItem({ @@ -97,6 +98,9 @@ const ClueListItem = memo(function ClueListItem({ {props.isEnteringRebus && isActiveCell @@ -132,6 +136,7 @@ interface ClueListProps { rebusValue?: string; grid: GridBase; scrollToCross: boolean; + draftCells?: Set; } export const ClueList = (props: ClueListProps): React.JSX.Element => { @@ -161,6 +166,7 @@ export const ClueList = (props: ClueListProps): React.JSX.Element => { active={ props.showEntries && (isActive || isCross) ? props.active : null } + draftCells={props.draftCells} /> ); }); diff --git a/app/components/Grid.tsx b/app/components/Grid.tsx index def5e03d..2e523c07 100644 --- a/app/components/Grid.tsx +++ b/app/components/Grid.tsx @@ -26,6 +26,7 @@ interface GridViewProps { dispatch: Dispatch; revealedCells?: Set; verifiedCells?: Set; + draftCells?: Set; isEnteringRebus?: boolean; rebusValue?: string; wrongCells?: Set; @@ -176,6 +177,7 @@ export const GridView = ({ isBlock={cellValue === BLOCK} isOpposite={isOpposite} isVerified={props.verifiedCells?.has(idx) || showAsVerified} + isDraft={props.draftCells?.has(idx)} isWrong={props.wrongCells?.has(idx)} wasRevealed={props.revealedCells?.has(idx)} styles={Array.from(grid.cellStyles.entries()) diff --git a/app/components/Keyboard.module.scss b/app/components/Keyboard.module.scss index 9c2b0858..ffa7f8d7 100644 --- a/app/components/Keyboard.module.scss +++ b/app/components/Keyboard.module.scss @@ -90,6 +90,11 @@ background: var(--key-bg-click); } +.key[data-active='true'] { + color: var(--onprimary); + background: var(--primary); +} + /* stylelint-disable-next-line media-query-no-invalid */ @media ($small-and-up) { .key[data-only-on-tablet='true'] { diff --git a/app/components/Keyboard.tsx b/app/components/Keyboard.tsx index f21c3420..9c5bbfbc 100644 --- a/app/components/Keyboard.tsx +++ b/app/components/Keyboard.tsx @@ -49,6 +49,7 @@ interface KeyProps { onlyOnTablet?: boolean; notOnTablet?: boolean; className?: string; + active?: boolean; touched: RefObject; } const Key = ({ touched, ...props }: KeyProps) => { @@ -59,6 +60,7 @@ const Key = ({ touched, ...props }: KeyProps) => { data-large-size={props.largeSize} data-only-on-tablet={props.onlyOnTablet} data-not-on-tablet={props.notOnTablet} + data-active={props.active} className={clsx(styles.key, props.className)} onClick={(e) => { e.preventDefault(); @@ -84,6 +86,8 @@ interface KeyboardProps { showExtraKeyLayout: boolean; includeBlockKey: boolean; toggleKeyboard: boolean; + showDraftKey?: boolean; + draftMode?: boolean; } export const Keyboard = memo(function Keyboard({ muted, @@ -164,6 +168,17 @@ export const Keyboard = memo(function Keyboard({ display="ABC" onKeypress={keypress} /> + {props.showDraftKey ? ( + + ) : ( + '' + )} + {props.showDraftKey ? ( + + ) : ( + '' + )} (play ? play.vc : []), wrongCells: new Set(play ? play.wc : []), revealedCells: new Set(play ? play.rc : []), + draftCells: new Set(play?.dc ?? []), + draftMode: false, downsOnly: play?.do ?? false, isEnteringRebus: false, rebusValue: '', @@ -510,6 +514,7 @@ export const Puzzle = ({ wc: Array.from(state.wrongCells), we: Array.from(state.cellsEverMarkedWrong), rc: Array.from(state.revealedCells), + dc: Array.from(state.draftCells), t: playTime, ch: state.didCheat, do: state.downsOnly, @@ -550,6 +555,7 @@ export const Puzzle = ({ state.success, state.verifiedCells, state.wrongCells, + state.draftCells, puzzle.title, state.bankedSeconds, state.currentTimeWindowStart, @@ -809,6 +815,7 @@ export const Puzzle = ({ cross={cross?.index} scrollToCross={scrollToCross} dispatch={dispatch} + draftCells={state.draftCells} /> } right={ @@ -828,6 +835,7 @@ export const Puzzle = ({ cross={cross?.index} scrollToCross={scrollToCross} dispatch={dispatch} + draftCells={state.draftCells} /> } /> @@ -849,6 +857,7 @@ export const Puzzle = ({ dispatch={dispatch} revealedCells={state.revealedCells} verifiedCells={state.verifiedCells} + draftCells={state.draftCells} wrongCells={state.wrongCells} showAlternates={state.success ? state.alternateSolutions : null} answers={state.answers} @@ -1030,18 +1039,34 @@ export const Puzzle = ({ {(closeDropdown) => (
{!state.success ? ( - } - text={t`Enter Rebus`} - shortcutHint={} - onClick={() => { - const kpa: KeypressAction = { - type: 'KEYPRESS', - key: { k: KeyK.Escape }, - }; - dispatch(kpa); - }} - /> + <> + } + text={t`Enter Rebus`} + shortcutHint={} + onClick={() => { + const kpa: KeypressAction = { + type: 'KEYPRESS', + key: { k: KeyK.Escape }, + }; + dispatch(kpa); + }} + /> + } + text={ + state.draftMode ? t`Stop Draft Entry` : t`Draft Entry` + } + shortcutHint={} + onClick={() => { + const kpa: KeypressAction = { + type: 'KEYPRESS', + key: { k: KeyK.Draft }, + }; + dispatch(kpa); + }} + /> + ) : ( '' )} @@ -1193,6 +1218,7 @@ export const Puzzle = ({ puzzle, setMuted, state.success, + state.draftMode, toggleKeyboard, setToggleKeyboard, isEmbed, @@ -1341,6 +1367,21 @@ export const Puzzle = ({ }} /> {isSlate ? : ''} + {state.draftMode && !isSlate ? ( + } + text={t`Drafting`} + onClick={() => { + const kpa: KeypressAction = { + type: 'KEYPRESS', + key: { k: KeyK.Draft }, + }; + dispatch(kpa); + }} + /> + ) : ( + '' + )} {checkRevealMenus} {isSlate ? : ''} {moreMenu} @@ -1454,6 +1495,8 @@ export const Puzzle = ({ muted={muted} showExtraKeyLayout={state.showExtraKeyLayout} includeBlockKey={false} + showDraftKey={true} + draftMode={state.draftMode} />
diff --git a/app/lib/dbtypes.ts b/app/lib/dbtypes.ts index ca629618..cfc37db1 100644 --- a/app/lib/dbtypes.ts +++ b/app/lib/dbtypes.ts @@ -335,6 +335,8 @@ const PlayBaseV = t.intersection([ ct_em: t.string, /** solved (or is solving) downs-only */ do: t.boolean, + /** list of draft (non-final) cells */ + dc: t.array(t.number), }), ]); diff --git a/app/lib/style.ts b/app/lib/style.ts index dd907955..918a7f59 100644 --- a/app/lib/style.ts +++ b/app/lib/style.ts @@ -128,6 +128,19 @@ export const colorTheme = ({ '--verified-on-bg': makeReadable(cellBG, verified), '--verified-on-secondary': makeReadable(secondary, verified), '--verified-on-selected-cell': makeReadable(selectedCell, verified), + '--draft-on-primary': mix(readableColor(p, darkMode), p, 0.5), + '--draft-on-lighter': mix(readableColor(lighter, darkMode), lighter, 0.5), + '--draft-on-bg': mix(text, cellBG, 0.5), + '--draft-on-secondary': mix( + readableColor(secondary, darkMode), + secondary, + 0.5 + ), + '--draft-on-selected-cell': mix( + readableColor(selectedCell, darkMode), + selectedCell, + 0.5 + ), '--autofill': darkMode ? '#999' : '#bbb', '--shade-highlight': darkMode ? 'rgba(255, 255, 255, 0.2)' diff --git a/app/lib/types.ts b/app/lib/types.ts index 803827ad..ded21c49 100644 --- a/app/lib/types.ts +++ b/app/lib/types.ts @@ -472,6 +472,7 @@ export enum KeyK { Block, Undo, Redo, + Draft, } export const ALLOWABLE_GRID_CHARS = /^[A-Za-z0-9ÑÅÄÖñåäö&/\\]$/; @@ -565,6 +566,8 @@ export function fromKeyboardEvent(event: { return KeyK.Rebus; case '{block}': return KeyK.Block; + case '{draft}': + return KeyK.Draft; default: return null; } diff --git a/app/locales/en/messages.po b/app/locales/en/messages.po index a152763e..11cef86e 100644 --- a/app/locales/en/messages.po +++ b/app/locales/en/messages.po @@ -275,10 +275,22 @@ msgstr "" msgid "enable across clues" msgstr "" -#: components/Puzzle.tsx:1032 +#: components/Puzzle.tsx:1045 msgid "Enter Rebus" msgstr "" +#: components/Puzzle.tsx:1058 +msgid "Draft Entry" +msgstr "" + +#: components/Puzzle.tsx:1058 +msgid "Stop Draft Entry" +msgstr "" + +#: components/Puzzle.tsx:1373 +msgid "Drafting" +msgstr "" + #: components/Comments.tsx:522 msgid "Enter your reply" msgstr "" diff --git a/app/locales/es/messages.po b/app/locales/es/messages.po index 0750e1c7..27e40db5 100644 --- a/app/locales/es/messages.po +++ b/app/locales/es/messages.po @@ -253,6 +253,18 @@ msgstr "activar pistas horizontales" msgid "Enter Rebus" msgstr "Ingresar jeroglífico" +#: components/Puzzle.tsx:1058 +msgid "Draft Entry" +msgstr "Entrada en borrador" + +#: components/Puzzle.tsx:1058 +msgid "Stop Draft Entry" +msgstr "Dejar de usar borrador" + +#: components/Puzzle.tsx:1373 +msgid "Drafting" +msgstr "Borrador" + #: components/Comments.tsx:522 msgid "Enter your reply" msgstr "Ingresar tu respuesta" diff --git a/app/locales/fr/messages.po b/app/locales/fr/messages.po index 3ea38d27..2c69b20b 100644 --- a/app/locales/fr/messages.po +++ b/app/locales/fr/messages.po @@ -257,6 +257,18 @@ msgstr "activer les indices horizontalement" msgid "Enter Rebus" msgstr "Insérer Rebus" +#: components/Puzzle.tsx:1058 +msgid "Draft Entry" +msgstr "Saisie brouillon" + +#: components/Puzzle.tsx:1058 +msgid "Stop Draft Entry" +msgstr "Arrêter la saisie brouillon" + +#: components/Puzzle.tsx:1373 +msgid "Drafting" +msgstr "Brouillon" + #: components/Comments.tsx:522 msgid "Enter your reply" msgstr "Insérer votre réponse" diff --git a/app/locales/id/messages.po b/app/locales/id/messages.po index 5b523eb1..7b1fbbd9 100644 --- a/app/locales/id/messages.po +++ b/app/locales/id/messages.po @@ -255,6 +255,18 @@ msgstr "aktifkan soal mendatar" msgid "Enter Rebus" msgstr "Masukkan Ribas" +#: components/Puzzle.tsx:1058 +msgid "Draft Entry" +msgstr "Entri draf" + +#: components/Puzzle.tsx:1058 +msgid "Stop Draft Entry" +msgstr "Hentikan entri draf" + +#: components/Puzzle.tsx:1373 +msgid "Drafting" +msgstr "Draf" + #: components/Comments.tsx:522 msgid "Enter your reply" msgstr "Masukkan balasan Anda" diff --git a/app/locales/it/messages.po b/app/locales/it/messages.po index 0835666b..a3f2975a 100644 --- a/app/locales/it/messages.po +++ b/app/locales/it/messages.po @@ -250,6 +250,18 @@ msgstr "abilita le definizioni orizzontali" msgid "Enter Rebus" msgstr "Inserisci Rebus" +#: components/Puzzle.tsx:1058 +msgid "Draft Entry" +msgstr "Inserimento bozza" + +#: components/Puzzle.tsx:1058 +msgid "Stop Draft Entry" +msgstr "Interrompi inserimento bozza" + +#: components/Puzzle.tsx:1373 +msgid "Drafting" +msgstr "Bozza" + #: components/Comments.tsx:522 msgid "Enter your reply" msgstr "Inserisci la tua risposta" diff --git a/app/reducers/gridReducer.ts b/app/reducers/gridReducer.ts index b31b29a8..184f5256 100644 --- a/app/reducers/gridReducer.ts +++ b/app/reducers/gridReducer.ts @@ -145,11 +145,40 @@ function enterCharAt( grid = gridWithNewChar(grid, pos, char || EMPTY, symmetry); } state = clearSelection(state); - state = postEdit({ ...state, grid }, ci); + if (isPuzzleState(state)) { + const draftCells = new Set(state.draftCells); + if (!char.trim() || char === BLOCK) { + draftCells.delete(ci); + } else if (state.draftMode) { + draftCells.add(ci); + } else { + draftCells.delete(ci); + } + state = postEdit({ ...state, grid, draftCells }, ci); + } else { + state = postEdit({ ...state, grid }, ci); + } } return state; } +function clearCharAt(state: T, pos: Position): T { + const ci = cellIndex(state.grid, pos); + if (!state.isEditable(ci)) { + return state; + } + const symmetry = isBuilderState(state) ? state.symmetry : Symmetry.None; + const grid = gridWithNewChar(state.grid, pos, EMPTY, symmetry); + if (isPuzzleState(state)) { + const elapsed = getCurrentTime(state); + state.cellsUpdatedAt[ci] = elapsed; + const draftCells = new Set(state.draftCells); + draftCells.delete(ci); + return postEdit({ ...state, grid, draftCells }, ci); + } + return postEdit({ ...state, grid }, ci); +} + export function closeRebus(state: T): T { if (!state.isEnteringRebus) { return state; @@ -336,6 +365,8 @@ export function gridInterfaceReducer( }; } else if (key.k === KeyK.Escape) { return { ...state, isEnteringRebus: false, rebusValue: '' }; + } else if (key.k === KeyK.Draft || key.k === KeyK.Dot) { + return state; } return closeRebus(state); } @@ -461,18 +492,7 @@ export function gridInterfaceReducer( ? getSelectionCells(state.selection) : [state.active]; for (const position of positions) { - const ci = cellIndex(state.grid, position); - if (state.isEditable(ci)) { - const symmetry = isBuilderState(state) - ? state.symmetry - : Symmetry.None; - if (isPuzzleState(state)) { - const elapsed = getCurrentTime(state); - state.cellsUpdatedAt[ci] = elapsed; - } - const grid = gridWithNewChar(state.grid, position, EMPTY, symmetry); - state = postEdit({ ...state, grid }, ci); - } + state = clearCharAt(state, position); } state = clearSelection(state); return { @@ -486,18 +506,7 @@ export function gridInterfaceReducer( ? getSelectionCells(state.selection) : [state.active]; for (const position of positions) { - const ci = cellIndex(state.grid, position); - if (state.isEditable(ci)) { - const symmetry = isBuilderState(state) - ? state.symmetry - : Symmetry.None; - if (isPuzzleState(state)) { - const elapsed = getCurrentTime(state); - state.cellsUpdatedAt[ci] = elapsed; - } - const grid = gridWithNewChar(state.grid, position, EMPTY, symmetry); - state = postEdit({ ...state, grid }, ci); - } + state = clearCharAt(state, position); } state = clearSelection(state); return { diff --git a/app/reducers/puzzleReducer.ts b/app/reducers/puzzleReducer.ts index b1fbda7b..92fb642e 100644 --- a/app/reducers/puzzleReducer.ts +++ b/app/reducers/puzzleReducer.ts @@ -26,6 +26,8 @@ export interface PuzzleState extends GridInterfaceState { verifiedCells: Set; revealedCells: Set; wrongCells: Set; + draftCells: Set; + draftMode: boolean; success: boolean; ranSuccessEffects: boolean; ranMetaSubmitEffects: boolean; @@ -145,6 +147,12 @@ export function puzzleReducer( state = gridInterfaceReducer(state, action); if (isKeypressAction(action)) { const key = action.key; + if ( + (key.k === KeyK.Draft || key.k === KeyK.Dot) && + !state.isEnteringRebus + ) { + return { ...state, draftMode: !state.draftMode }; + } if (key.k === KeyK.ShiftArrowRight) { return { ...state, @@ -245,6 +253,8 @@ export function puzzleReducer( ranSuccessEffects: true, ranMetaSubmitEffects: true, grid: { ...state.grid, cells: state.solutions[0] }, + draftCells: new Set(), + draftMode: false, }; } const play = action.play; @@ -270,6 +280,7 @@ export function puzzleReducer( verifiedCells: new Set(play.vc), wrongCells: new Set(play.wc), revealedCells: new Set(play.rc), + draftCells: new Set(play.dc ?? []), success: play.f, ranSuccessEffects: play.f, downsOnly, diff --git a/app/reducers/puzzleUtils.ts b/app/reducers/puzzleUtils.ts index 3e134665..cbea1107 100644 --- a/app/reducers/puzzleUtils.ts +++ b/app/reducers/puzzleUtils.ts @@ -20,6 +20,7 @@ export type CheatablePuzzleState = Pick< | 'verifiedCells' | 'revealedCells' | 'wrongCells' + | 'draftCells' | 'grid' | 'solutions' | 'cellsIterationCount' @@ -69,6 +70,7 @@ function cheatCells( const revealedCells = new Set(state.revealedCells); const verifiedCells = new Set(state.verifiedCells); const wrongCells = new Set(state.wrongCells); + const draftCells = new Set(state.draftCells); let grid = state.grid; const answers = closestAlt(state.grid.cells, state.solutions); @@ -85,10 +87,12 @@ function cheatCells( if (shouldBe === currentVal) { verifiedCells.add(ci); wrongCells.delete(ci); + draftCells.delete(ci); } else if (isReveal) { revealedCells.add(ci); wrongCells.delete(ci); verifiedCells.add(ci); + draftCells.delete(ci); grid = gridWithNewChar(grid, cell, shouldBe, Symmetry.None); state.cellsUpdatedAt[ci] = elapsed; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -106,6 +110,7 @@ function cheatCells( wrongCells, revealedCells, verifiedCells, + draftCells, }); } @@ -156,6 +161,7 @@ export function checkComplete(state: T): T { bankedSeconds, currentTimeWindowStart, dismissedKeepTrying, + ...(success && { draftCells: new Set(), draftMode: false }), }; } return state; From 1a48be98f11945578a0306ecdd1fc5ae5553b37e Mon Sep 17 00:00:00 2001 From: ianfhunter Date: Thu, 17 Sep 2026 20:09:27 +0100 Subject: [PATCH 2/2] fix for rebus drafts --- app/components/ClueList.tsx | 11 ++++++++--- app/components/Grid.tsx | 7 ++++++- app/components/Puzzle.tsx | 3 +++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/components/ClueList.tsx b/app/components/ClueList.tsx index 7f4de18e..3f675d8d 100644 --- a/app/components/ClueList.tsx +++ b/app/components/ClueList.tsx @@ -26,6 +26,7 @@ interface ClueListItemProps { scrollToCross: boolean; listRef: RefObject; draftCells?: Set; + draftMode?: boolean; } const ClueListItem = memo(function ClueListItem({ @@ -98,9 +99,11 @@ const ClueListItem = memo(function ClueListItem({ {props.isEnteringRebus && isActiveCell @@ -137,6 +140,7 @@ interface ClueListProps { grid: GridBase; scrollToCross: boolean; draftCells?: Set; + draftMode?: boolean; } export const ClueList = (props: ClueListProps): React.JSX.Element => { @@ -167,6 +171,7 @@ export const ClueList = (props: ClueListProps): React.JSX.Element => { props.showEntries && (isActive || isCross) ? props.active : null } draftCells={props.draftCells} + draftMode={props.draftMode} /> ); }); diff --git a/app/components/Grid.tsx b/app/components/Grid.tsx index 2e523c07..64d47397 100644 --- a/app/components/Grid.tsx +++ b/app/components/Grid.tsx @@ -27,6 +27,7 @@ interface GridViewProps { revealedCells?: Set; verifiedCells?: Set; draftCells?: Set; + draftMode?: boolean; isEnteringRebus?: boolean; rebusValue?: string; wrongCells?: Set; @@ -177,7 +178,11 @@ export const GridView = ({ isBlock={cellValue === BLOCK} isOpposite={isOpposite} isVerified={props.verifiedCells?.has(idx) || showAsVerified} - isDraft={props.draftCells?.has(idx)} + isDraft={ + props.isEnteringRebus && isActive + ? Boolean(props.draftMode) + : props.draftCells?.has(idx) + } isWrong={props.wrongCells?.has(idx)} wasRevealed={props.revealedCells?.has(idx)} styles={Array.from(grid.cellStyles.entries()) diff --git a/app/components/Puzzle.tsx b/app/components/Puzzle.tsx index 4c7a1463..4b69dd53 100644 --- a/app/components/Puzzle.tsx +++ b/app/components/Puzzle.tsx @@ -815,6 +815,7 @@ export const Puzzle = ({ scrollToCross={scrollToCross} dispatch={dispatch} draftCells={state.draftCells} + draftMode={state.draftMode} /> } right={ @@ -835,6 +836,7 @@ export const Puzzle = ({ scrollToCross={scrollToCross} dispatch={dispatch} draftCells={state.draftCells} + draftMode={state.draftMode} /> } /> @@ -857,6 +859,7 @@ export const Puzzle = ({ revealedCells={state.revealedCells} verifiedCells={state.verifiedCells} draftCells={state.draftCells} + draftMode={state.draftMode} wrongCells={state.wrongCells} showAlternates={state.success ? state.alternateSolutions : null} answers={state.answers}