From 2457df9ce9cdb92b82764ea6346f22a5f5fae1d3 Mon Sep 17 00:00:00 2001 From: Sam Bender <2336186+rednebmas@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:37:28 -0500 Subject: [PATCH 1/5] fix: improve keyboard horizontal overflow handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow keyboard to scroll horizontally when it overflows the viewport while preventing unwanted horizontal scroll at the layout level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/react/src/components/keyboard/KeyBoard.tsx | 12 ++++++------ apps/react/src/components/layout/Layout.tsx | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/react/src/components/keyboard/KeyBoard.tsx b/apps/react/src/components/keyboard/KeyBoard.tsx index b9384e0..0178084 100644 --- a/apps/react/src/components/keyboard/KeyBoard.tsx +++ b/apps/react/src/components/keyboard/KeyBoard.tsx @@ -6,12 +6,12 @@ import { BlackKey } from './BlackKey'; import { WhiteKey } from './WhiteKey'; export const Keyboard = () => { - return ( -
-
- {/*
*/} - {[1, 2, 3, 4, 5].map((octave) => ( - + return ( +
+
+ {/*
*/} + {[1, 2, 3, 4, 5].map((octave) => ( + ))}
diff --git a/apps/react/src/components/layout/Layout.tsx b/apps/react/src/components/layout/Layout.tsx index df94c58..d27094f 100644 --- a/apps/react/src/components/layout/Layout.tsx +++ b/apps/react/src/components/layout/Layout.tsx @@ -19,7 +19,10 @@ export const Layout: React.FC = ({ back, }) => { return ( -
+
Date: Wed, 31 Dec 2025 19:49:56 -0500 Subject: [PATCH 2/5] chore: add CLAUDE.md symlink to AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow both Codex and Claude Code to use the same project instructions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 10 ++++++++++ CLAUDE.md | 1 + 2 files changed, 11 insertions(+) create mode 100644 .claude/settings.local.json create mode 120000 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ab61245 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(cat:*)", + "Bash(yarn workspace:*)", + "Bash(git commit:*)" + ] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From bf3f63c48ac6a2a2af5a24718c7560f32ea6caa5 Mon Sep 17 00:00:00 2001 From: Sam Bender <2336186+rednebmas@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:28:19 -0800 Subject: [PATCH 3/5] Record failed attempt on reveal, dark mode chord tones, remote screenshot tests - Record a failed spaced-repetition attempt when "Reveal answer" is clicked - Add dark mode Tailwind variants to ChordToneDisplay component - Add remote screenshot test script and yarn shortcut - Replace CLAUDE.md symlink with @AGENTS.md import - Document screenshot test workflow and sandbox requirements in AGENTS.md Co-Authored-By: Claude Opus 4.6 --- .claude/settings.local.json | 3 +- AGENTS.md | 12 +++- CLAUDE.md | 2 +- .../src/components/FlashCardOptionsMenu.tsx | 6 +- .../components/notation/ChordToneDisplay.tsx | 4 +- package.json | 3 +- scripts/test-screenshots-remote.sh | 57 +++++++++++++++++++ 7 files changed, 80 insertions(+), 7 deletions(-) mode change 120000 => 100644 CLAUDE.md create mode 100755 scripts/test-screenshots-remote.sh diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ab61245..e06d45f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,8 @@ "Bash(find:*)", "Bash(cat:*)", "Bash(yarn workspace:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(git add:*)" ] } } diff --git a/AGENTS.md b/AGENTS.md index 4942f4d..07ad849 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,16 @@ This project prefers a highly componentized React codebase that avoids duplicate - **Redux**: Compose selectors and helpers rather than copy/pasting logic. UI components should avoid data manipulation—use Redux selectors to transform and format data instead of doing it in components. - **Error Handling**: Keep error handling reasonable but not excessive. This is a small app - simple null checks and basic 404s are fine. Don't over-engineer with detailed error types for every edge case. - **Unit Tests**: Write unit tests for important service functions, especially those involving business logic or data transformations. -- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass. +- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass. Requires `dangerouslyDisableSandbox` (MongoMemoryServer binds to `0.0.0.0`). + +## Screenshot Tests (Remote) + +Screenshot tests run via GitHub Actions since they need Playwright with a browser. + +1. Run `./scripts/test-screenshots-remote.sh` (`dangerouslyDisableSandbox` required) — pushes a temp branch, triggers the `test.yml` workflow +2. Parse the branch name from stdout (format: `screenshot-test-`) +3. Poll for the run ID: `gh run list --workflow=test.yml --branch=$BRANCH --limit=1 --json databaseId,status` +4. Watch it: `gh run watch $RUN_ID` +5. Clean up: `git push origin --delete $BRANCH` Follow these guidelines to keep the codebase clean and maintainable. diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/apps/react/src/components/FlashCardOptionsMenu.tsx b/apps/react/src/components/FlashCardOptionsMenu.tsx index 343ade7..9106fd9 100644 --- a/apps/react/src/components/FlashCardOptionsMenu.tsx +++ b/apps/react/src/components/FlashCardOptionsMenu.tsx @@ -9,6 +9,7 @@ import { useIsCardOwner } from '../utils/useIsCardOwner'; import { useAppDispatch, useAppSelector } from 'MemoryFlashCore/src/redux/store'; import { updateHiddenCards } from 'MemoryFlashCore/src/redux/actions/update-hidden-cards-action'; import { deleteCard } from 'MemoryFlashCore/src/redux/actions/delete-card-action'; +import { recordAttempt } from 'MemoryFlashCore/src/redux/actions/record-attempt-action'; import { CardWithAttempts, selectHiddenCardIds, @@ -52,7 +53,10 @@ export const FlashCardOptionsMenu: React.FC = ({ if (canReveal) { items.unshift({ label: 'Reveal answer', - onClick: () => setShowAnswer(true), + onClick: () => { + dispatch(recordAttempt(false)); + setShowAnswer(true); + }, }); } diff --git a/apps/react/src/components/notation/ChordToneDisplay.tsx b/apps/react/src/components/notation/ChordToneDisplay.tsx index 7acbe84..1e3f0e0 100644 --- a/apps/react/src/components/notation/ChordToneDisplay.tsx +++ b/apps/react/src/components/notation/ChordToneDisplay.tsx @@ -14,7 +14,7 @@ const ToneChip: React.FC = ({ tone, required, onToggle }) => ( className={`px-2 py-0.5 rounded text-xs font-medium transition-colors ${ required ? 'bg-blue-600 text-white' - : 'bg-gray-200 text-gray-600 border border-dashed border-gray-400' + : 'bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-200 border border-dashed border-gray-400 dark:border-gray-500' }`} > {tone} @@ -32,7 +32,7 @@ export const ChordToneDisplay: React.FC = ({ chord, onTog return (
{chord.chordName}
{isInvalid ? ( diff --git a/package.json b/package.json index 6fb0a4a..2c05917 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "concurrently \"yarn workspace MemoryFlashServer dev\" \"yarn workspace MemoryFlashReact dev\"", "test": "yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test && yarn workspace MemoryFlashReact test:screenshots", - "test:codex": "yarn workspace MemoryFlashServer build && yarn workspace MemoryFlashReact build && yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test" + "test:codex": "yarn workspace MemoryFlashServer build && yarn workspace MemoryFlashReact build && yarn workspace MemoryFlashServer test && yarn workspace MemoryFlashCore test", + "test:screenshots:remote": "./scripts/test-screenshots-remote.sh" }, "workspaces": [ "apps/*", diff --git a/scripts/test-screenshots-remote.sh b/scripts/test-screenshots-remote.sh new file mode 100755 index 0000000..942ccd4 --- /dev/null +++ b/scripts/test-screenshots-remote.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +# Get current branch and create temp branch name +ORIGINAL_BRANCH=$(git rev-parse --abbrev-ref HEAD) +TEMP_BRANCH="screenshot-test-$(date +%s)" + +echo "Creating temporary branch: $TEMP_BRANCH" + +# Stash any changes (including untracked) +STASH_RESULT=$(git stash push -u -m "temp-screenshot-test" 2>&1) || true +HAS_STASH=false +if [[ "$STASH_RESULT" != *"No local changes"* ]]; then + HAS_STASH=true +fi + +# Create and switch to temp branch +git checkout -b "$TEMP_BRANCH" + +# Apply stashed changes if we had any +if [ "$HAS_STASH" = true ]; then + git stash pop +fi + +# Commit everything +git add -A +git commit -m "Screenshot test - temporary commit" --allow-empty + +# Push temp branch +git push origin "$TEMP_BRANCH" + +# Trigger workflow +echo "Triggering workflow on $TEMP_BRANCH..." +gh workflow run test.yml --ref "$TEMP_BRANCH" + +# Stash changes again before switching back (so we can restore them) +if [ "$HAS_STASH" = true ]; then + git stash push -u -m "restore-to-original" +fi + +# Switch back to original branch +git checkout "$ORIGINAL_BRANCH" + +# Restore uncommitted changes to original branch +if [ "$HAS_STASH" = true ]; then + git stash pop +fi + +# Delete local temp branch +git branch -D "$TEMP_BRANCH" + +echo "" +echo "Workflow triggered on branch: $TEMP_BRANCH" +echo "Run 'gh run watch' to monitor progress" +echo "" +echo "To clean up remote branch after tests complete:" +echo " git push origin --delete $TEMP_BRANCH" From 2039dc1932f7ef7193b9677e5ba3ae03c0a8a962 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 3 Mar 2026 19:30:49 +0000 Subject: [PATCH 4/5] style: format code --- .claude/settings.local.json | 18 +++++++++--------- .../react/src/components/keyboard/KeyBoard.tsx | 12 ++++++------ apps/react/src/components/layout/Layout.tsx | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e06d45f..6ab1ef0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,11 +1,11 @@ { - "permissions": { - "allow": [ - "Bash(find:*)", - "Bash(cat:*)", - "Bash(yarn workspace:*)", - "Bash(git commit:*)", - "Bash(git add:*)" - ] - } + "permissions": { + "allow": [ + "Bash(find:*)", + "Bash(cat:*)", + "Bash(yarn workspace:*)", + "Bash(git commit:*)", + "Bash(git add:*)" + ] + } } diff --git a/apps/react/src/components/keyboard/KeyBoard.tsx b/apps/react/src/components/keyboard/KeyBoard.tsx index 0178084..b5cfcfe 100644 --- a/apps/react/src/components/keyboard/KeyBoard.tsx +++ b/apps/react/src/components/keyboard/KeyBoard.tsx @@ -6,12 +6,12 @@ import { BlackKey } from './BlackKey'; import { WhiteKey } from './WhiteKey'; export const Keyboard = () => { - return ( -
-
- {/*
*/} - {[1, 2, 3, 4, 5].map((octave) => ( - + return ( +
+
+ {/*
*/} + {[1, 2, 3, 4, 5].map((octave) => ( + ))}
diff --git a/apps/react/src/components/layout/Layout.tsx b/apps/react/src/components/layout/Layout.tsx index d27094f..136f371 100644 --- a/apps/react/src/components/layout/Layout.tsx +++ b/apps/react/src/components/layout/Layout.tsx @@ -19,10 +19,10 @@ export const Layout: React.FC = ({ back, }) => { return ( -
+
Date: Tue, 3 Mar 2026 12:13:37 -0800 Subject: [PATCH 5/5] fix: update stale .overflow-scroll selector in screenshot tests The Layout component changed from overflow-scroll to overflow-y-auto in 2457df9, but the test scroll helpers still queried .overflow-scroll, causing screenshots to capture at wrong scroll positions. Co-Authored-By: Claude Opus 4.6 --- apps/react/tests/custom-deck-chord-memory-to-study.spec.ts | 2 +- apps/react/tests/custom-deck-notation-to-study.spec.ts | 2 +- apps/react/tests/helpers/visual.ts | 2 +- apps/react/tests/notation-input-e-major-g-sharp.spec.ts | 2 +- apps/react/tests/notation-input-text-prompt.spec.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts index 4fcdb10..17cb2bf 100644 --- a/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts +++ b/apps/react/tests/custom-deck-chord-memory-to-study.spec.ts @@ -78,7 +78,7 @@ test('Create custom deck with Chord Memory card, study it, then edit it', async await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 600); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 600); }); await expect(output).toHaveScreenshot('custom-deck-chord-memory-edit.png', screenshotOpts); diff --git a/apps/react/tests/custom-deck-notation-to-study.spec.ts b/apps/react/tests/custom-deck-notation-to-study.spec.ts index f7a44be..c13161b 100644 --- a/apps/react/tests/custom-deck-notation-to-study.spec.ts +++ b/apps/react/tests/custom-deck-notation-to-study.spec.ts @@ -94,7 +94,7 @@ test('Create custom deck, add notation and text cards, then study', async ({ await expect(page.locator('#text-prompt')).toHaveValue(promptText); await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 600); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 600); }); await expect(output).toHaveScreenshot( 'custom-deck-notation-to-study-notation-input-edit.png', diff --git a/apps/react/tests/helpers/visual.ts b/apps/react/tests/helpers/visual.ts index e33c29b..71d1e2e 100644 --- a/apps/react/tests/helpers/visual.ts +++ b/apps/react/tests/helpers/visual.ts @@ -16,7 +16,7 @@ export async function setStaticScroll( await page.evaluate( ({ windowTop: winTop, overflowTop: overTop }) => { window.scrollTo(0, winTop); - document.querySelector('.overflow-scroll')?.scrollTo(0, overTop); + document.querySelector('.overflow-y-auto')?.scrollTo(0, overTop); }, { windowTop, overflowTop }, ); diff --git a/apps/react/tests/notation-input-e-major-g-sharp.spec.ts b/apps/react/tests/notation-input-e-major-g-sharp.spec.ts index d535210..d43a7c5 100644 --- a/apps/react/tests/notation-input-e-major-g-sharp.spec.ts +++ b/apps/react/tests/notation-input-e-major-g-sharp.spec.ts @@ -18,7 +18,7 @@ test('NotationInputScreen E major G# rendering', async ({ page }) => { // Wait a bit for rendering await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 300); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 300); }); await expect(output).toHaveScreenshot('notation-input-e-major-g-sharp.png', screenshotOpts); diff --git a/apps/react/tests/notation-input-text-prompt.spec.ts b/apps/react/tests/notation-input-text-prompt.spec.ts index 14abb4a..64e301d 100644 --- a/apps/react/tests/notation-input-text-prompt.spec.ts +++ b/apps/react/tests/notation-input-text-prompt.spec.ts @@ -13,7 +13,7 @@ test('NotationInputScreen text prompt card type', async ({ page }) => { await page.waitForTimeout(200); await page.evaluate(() => { window.scrollTo(0, 0); - document.querySelector('.overflow-scroll')?.scrollTo(0, 300); + document.querySelector('.overflow-y-auto')?.scrollTo(0, 300); }); await expect(output).toHaveScreenshot('notation-input-text-prompt.png', screenshotOpts);