event.preventDefault()}
>
- {progressLetters.map((progressLetter, index) => {
- const progressValid =
- progressLetter.progress === candidateLetters[index] ||
- progressLetter.progress === '' ||
- !shuffled;
- const candidateLetter = candidateLetters[index];
+ {cellsWithProgress.map((cellWithProgress, index) => {
return (
- (inputRefs.current[index] = el)}
- draggable={true}
- value={candidateLetters[index]}
- onDragStart={() => setDragItemIndex(index)}
- onDragEnter={() => setDragOverItemIndex(index)}
- onDragEnd={onDragEnd}
- onChange={() => {}} // TODO: remove the need for this
- onKeyDown={updateCandidateLetter}
- maxLength={1}
- tabIndex={index + 1}
- data-index={index}
- css={css`
- box-sizing: border-box;
- border: 1px solid ${theme.background};
- border-radius: 4px;
- width: ${theme.cellSize - 1}px;
- height: ${theme.cellSize - 1}px;
- margin-left: 1px;
- text-align: center;
- align-content: center;
- caret-color: transparent;
- :active {
- cursor: grabbing;
- }
- `}
- />
- {progressLetter.progress !== candidateLetter && shuffled && (
-
- )}
);
diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx
deleted file mode 100644
index 886d3e57f..000000000
--- a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.stories.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-import { groupedClues as data } from '../../stories/formats/grouped-clues';
-import { ContextProvider } from '../context/ContextProvider';
-import { SolutionDisplayCell } from './SolutionDisplayCell';
-
-const meta: Meta
= {
- component: SolutionDisplayCell,
- title: 'Components/SolutionDisplayCell',
-
- args: {},
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- progressLetter: {
- coords: { x: 0, y: 0 },
- progress: 'T',
- isSaved: true,
- },
- progressValid: true,
- },
-};
-
-export const Temporary: Story = {
- args: {
- progressLetter: {
- coords: { x: 0, y: 0 },
- progress: 'T',
- isSaved: false,
- },
- progressValid: true,
- },
-};
-
-export const NotMatching: Story = {
- args: {
- progressLetter: {
- coords: { x: 0, y: 0 },
- progress: 'T',
- isSaved: true,
- },
- progressValid: false,
- },
-};
-
-export const TemporaryNotMatching: Story = {
- args: {
- progressLetter: {
- coords: { x: 0, y: 0 },
- progress: 'T',
- isSaved: false,
- },
- progressValid: false,
- },
-};
diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx
deleted file mode 100644
index 815c92979..000000000
--- a/libs/@guardian/react-crossword/src/components/SolutionDisplayCell.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { css } from '@emotion/react';
-import { textSans12 } from '@guardian/source/foundations';
-import { useTheme } from '../context/Theme';
-import type { AnagramHelperProgress } from '../utils/getAnagramHelperProgressForGroup';
-
-export const SolutionDisplayCell = ({
- progressLetter,
- progressValid,
-}: {
- progressLetter: AnagramHelperProgress;
- progressValid: boolean;
-}) => {
- const theme = useTheme();
- return (
-
- {progressLetter.separator === '-' && (
-
- )}
- {progressLetter.number && (
-
- {progressLetter.number}
-
- )}
- {progressLetter.progress}
-
- );
-};
diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx
deleted file mode 100644
index 959378fc5..000000000
--- a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import type { Meta, StoryObj } from '@storybook/react';
-import { groupedClues as data } from '../../stories/formats/grouped-clues';
-import { ContextProvider } from '../context/ContextProvider';
-import { SolutionDisplayKey } from './SolutionDisplayKey';
-
-const meta: Meta = {
- component: SolutionDisplayKey,
- title: 'Components/Solution Display Key',
-
- args: {},
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {},
-};
diff --git a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx b/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx
deleted file mode 100644
index 5d3bcee72..000000000
--- a/libs/@guardian/react-crossword/src/components/SolutionDisplayKey.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { css } from '@emotion/react';
-import { SolutionDisplayCell } from './SolutionDisplayCell';
-
-export const SolutionDisplayKey = () => {
- return (
-
-
Unsaved value
-
-
Mismatched value
-
-
- );
-};
diff --git a/libs/@guardian/react-crossword/src/components/WordWheel.tsx b/libs/@guardian/react-crossword/src/components/WordWheel.tsx
index 27f2ad65f..9bd8325e5 100644
--- a/libs/@guardian/react-crossword/src/components/WordWheel.tsx
+++ b/libs/@guardian/react-crossword/src/components/WordWheel.tsx
@@ -52,7 +52,11 @@ const renderOuterLetters = ({
const WordWheelComponent = ({ letters }: { letters: string[] }) => {
const theme = useTheme();
- const centerLetter = letters.length > 4 ? letters.shift() : undefined;
+ // Copy array to avoid mutating the original
+ const wordWheelLetters = [...letters];
+
+ const centerLetter =
+ letters.length > 4 ? wordWheelLetters.shift() : undefined;
return (
);
};
diff --git a/libs/@guardian/react-crossword/src/theme.ts b/libs/@guardian/react-crossword/src/theme.ts
index b256a2f0a..1528be6cf 100644
--- a/libs/@guardian/react-crossword/src/theme.ts
+++ b/libs/@guardian/react-crossword/src/theme.ts
@@ -5,6 +5,7 @@ export const defaultTheme: Theme = {
background: palette.neutral[7],
text: palette.neutral[10],
errorText: 'red',
+ provisionalText: palette.neutral[60],
unsavedBackground: 'lightPink',
foreground: palette.neutral[100],
gutter: 1,
@@ -15,7 +16,7 @@ export const defaultTheme: Theme = {
buttonBackground: 'hotpink',
buttonBackgroundHover: 'lightpink',
border: 'orchid',
- anagramHelperBackground: 'lightcoral',
+ anagramHelperBackground: 'floralwhite',
clueMinWidth: 240,
clueMaxWidth: 480,
};
diff --git a/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts b/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts
new file mode 100644
index 000000000..51545557f
--- /dev/null
+++ b/libs/@guardian/react-crossword/src/utils/biasedShuffle.ts
@@ -0,0 +1,32 @@
+import { isUndefined } from '@guardian/libs';
+
+// This uses the Fisher Yates shuffle algorithm, but with a bias factor.
+// The Math.min(i + 1, n / 2) ensures that the random index j
+// is biased toward indices farther from the current index i.
+// This encourages items to move farther from their original positions.
+
+export const biasedShuffle = (array: T[]): T[] => {
+ const shuffled = [...array]; // Create a shallow copy of the array
+ const n = shuffled.length;
+
+ for (let i = n - 1; i > 0; i--) {
+ // Use a bias factor but ensure the range includes all indices [0, i]
+ const biasFactor = Math.max(1, Math.min(i + 1, Math.floor(n / 2)));
+ const biasedJ = Math.max(0, i - Math.floor(Math.random() * biasFactor));
+
+ const unbiasedJ = Math.floor(Math.random() * (i + 1));
+
+ // Choose between full range and biased swap 50% chance to use bias
+ const finalJ = Math.random() > 0.5 ? unbiasedJ : biasedJ;
+
+ if (!isUndefined(shuffled[i])) {
+ const temp = shuffled[i];
+ if (!isUndefined(shuffled[finalJ]) && !isUndefined(temp)) {
+ shuffled[i] = shuffled[finalJ];
+ shuffled[finalJ] = temp;
+ }
+ }
+ }
+
+ return shuffled;
+};
diff --git a/libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts b/libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts
similarity index 57%
rename from libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts
rename to libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts
index 4fdec34c6..74459a3f1 100644
--- a/libs/@guardian/react-crossword/src/utils/getAnagramHelperProgressForGroup.ts
+++ b/libs/@guardian/react-crossword/src/utils/getCellsWithProgressForGroup.ts
@@ -1,25 +1,25 @@
import { isUndefined } from '@guardian/libs';
import type { CAPIEntry } from '../@types/CAPI';
-import type { Coords, Entries, Progress } from '../@types/crossword';
+import type { Cell, Cells, Entries, Progress } from '../@types/crossword';
-export type AnagramHelperProgress = {
+export type CellWithProgress = Cell & {
progress: string;
- coords: Coords;
- isSaved: boolean;
- number?: number;
separator?: ',' | '-';
};
+export type CellsWithProgress = CellWithProgress[];
-export const getAnagramHelperProgressForGroup = ({
+export const getCellsWithProgressForGroup = ({
entry,
entries,
+ cells,
progress,
}: {
entry?: CAPIEntry;
entries: Entries;
+ cells: Cells;
progress: Progress;
}) => {
- const groupProgress: AnagramHelperProgress[] = [];
+ const groupProgress: CellsWithProgress = [];
if (isUndefined(entry)) {
return groupProgress;
}
@@ -27,33 +27,40 @@ export const getAnagramHelperProgressForGroup = ({
const entry = entries.get(entryId);
if (!isUndefined(entry)) {
groupProgress.push(
- ...getAnagramHelperProgressForEntry({ entry, progress }),
+ ...getCellsWithProgressForEntry({ entry, cells, progress }),
);
}
}
return groupProgress;
};
-export const getAnagramHelperProgressForEntry = ({
+export const getCellsWithProgressForEntry = ({
entry,
+ cells,
progress,
}: {
entry: CAPIEntry;
+ cells: Cells;
progress: Progress;
-}): AnagramHelperProgress[] => {
- return Array.from({ length: entry.length }, (_, i) => {
+}): CellsWithProgress => {
+ const cellsWithProgress: CellsWithProgress = [];
+ for (let i = 0; i < entry.length; i++) {
const x =
entry.direction === 'across' ? entry.position.x + i : entry.position.x;
const y =
entry.direction === 'across' ? entry.position.y : entry.position.y + i;
- return {
- coords: { x, y },
- number: i === 0 ? entry.number : undefined,
- isSaved: true,
- progress: progress.at(x)?.[y] ?? '',
- separator: getSeparatorFromEntry(entry, i),
- };
- });
+
+ const cell = cells.getByCoords({ x, y });
+ if (cell) {
+ cellsWithProgress.push({
+ ...cell,
+ progress: progress.at(x)?.[y] ?? '',
+ separator: getSeparatorFromEntry(entry, i),
+ });
+ }
+ }
+
+ return cellsWithProgress;
};
const getSeparatorFromEntry = (
diff --git a/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts b/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts
index d6da49007..b160f8970 100644
--- a/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts
+++ b/libs/@guardian/react-crossword/stories/formats/grouped-clues.progress.ts
@@ -15,3 +15,21 @@ export const progress = [
['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
];
+
+export const progress12Across = [
+ ['', '', '', '', '', 'F', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', 'O', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', 'O', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+ ['', '', '', '', '', '', '', '', '', '', '', '', '', '', ''],
+];