diff --git a/README.md b/README.md
index 559bd90..0e82db4 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ Instances talk to each other over ActivityPub, and nobody stands in the middle.
**Comfort and access**
- Light, dark, and system themes, warm palettes, and text scaling up to 200%
-- A screen-reader native grid: every cell is focusable and announced with its number, letter, and state, and Alt+A / Alt+D read the across and down words aloud (/ reads the clue, space reads the cell, Ctrl+H the puzzle overview)
+- A screen-reader native grid: every cell is focusable and announced with its number, letter, and state, and Alt+A / Alt+D read the across and down words aloud (/ or Alt+P reads the clue, space reads the cell, Ctrl+H the puzzle overview)
- A voice narrator with three verbosity levels and an output mode (voice, screen reader, or both) so blind players get the same flow through their own screen reader
- Colorblind palettes and reduced-motion cell animations
- Sound effects and a tor-friendly mode that keeps the page light for slow connections
diff --git a/frontend/src/components/MultiplayerPanel.tsx b/frontend/src/components/MultiplayerPanel.tsx
index 43c5d97..907814e 100644
--- a/frontend/src/components/MultiplayerPanel.tsx
+++ b/frontend/src/components/MultiplayerPanel.tsx
@@ -199,19 +199,21 @@ export default function MultiplayerPanel({ onClose }: { onClose: () => void }) {
// already selected, do nothing
} else {
const rect = e.currentTarget.getBoundingClientRect()
- const canvas = document.createElement('canvas')
- canvas.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:9999'
- document.body.appendChild(canvas)
- const myConfetti = confetti.create(canvas, { resize: true })
- myConfetti({
- particleCount: 60,
- spread: 70,
- startVelocity: 25,
- origin: {
- x: (rect.left + rect.width / 2) / window.innerWidth,
- y: (rect.top + rect.height / 2) / window.innerHeight,
- },
- }).then(() => canvas.remove())
+ if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ const canvas = document.createElement('canvas')
+ canvas.style.cssText = 'position:fixed;inset:0;width:100%;height:100%;pointer-events:none;z-index:9999'
+ document.body.appendChild(canvas)
+ const myConfetti = confetti.create(canvas, { resize: true })
+ myConfetti({
+ particleCount: 60,
+ spread: 70,
+ startVelocity: 25,
+ origin: {
+ x: (rect.left + rect.width / 2) / window.innerWidth,
+ y: (rect.top + rect.height / 2) / window.innerHeight,
+ },
+ }).then(() => canvas.remove())
+ }
}
}}
>
diff --git a/frontend/src/components/ui/NumberInput.tsx b/frontend/src/components/ui/NumberInput.tsx
index 17aa280..3c491e5 100644
--- a/frontend/src/components/ui/NumberInput.tsx
+++ b/frontend/src/components/ui/NumberInput.tsx
@@ -1,3 +1,5 @@
+import { useTranslation } from 'react-i18next'
+
interface Props {
value: number
min?: number
@@ -9,14 +11,31 @@ interface Props {
}
export default function NumberInput({ value, min = 0, max = 999, onChange, className, style, 'aria-label': ariaLabel }: Props) {
+ const { t } = useTranslation()
const clamp = (n: number) => Math.min(max, Math.max(min, n))
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ let next: number | null = null
+ if (e.key === 'ArrowUp') next = value + 1
+ else if (e.key === 'ArrowDown') next = value - 1
+ else if (e.key === 'PageUp') next = value + 10
+ else if (e.key === 'PageDown') next = value - 10
+ else if (e.key === 'Home') next = min
+ else if (e.key === 'End') next = max
+ if (next === null) return
+ e.preventDefault()
+ onChange(clamp(next))
+ }
+
return (