From 04a3db732693618542f9f283d8ddf08264a0d3e6 Mon Sep 17 00:00:00 2001 From: clacina Date: Tue, 28 Apr 2026 07:32:01 -0700 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20feat:=20complete=20flashcard=20?= =?UTF-8?q?session=20with=20landing=20page,=20category=20context,=20and=20?= =?UTF-8?q?responsive=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a landing page hero above the category selector, displays the selected category name and description as a header in the session view, makes the term list permanently visible with a focus guard for arrow keys, shows a placeholder for terms with no video URL, and fixes viewport overflow by making the video aspect-ratio responsive and adding min-height: 0 to flex containers. Adds 15 component tests for FlashcardSession. Co-Authored-By: Claude Sonnet 4.6 --- _plans/footer.txt | 7 ++ _specs/flashcard-session.md | 61 ++++++++++++++ package.json | 1 + src/App.css | 108 ++++++++++++++++++++---- src/App.jsx | 14 ++-- src/components/FlashcardSession.jsx | 94 +++++++++++---------- src/components/LandingPage.jsx | 16 ++++ src/components/TermInput.jsx | 20 ++--- tests/FlashcardSession.test.jsx | 125 ++++++++++++++++++++++++++++ yarn.lock | 69 ++++++++++++++- 10 files changed, 436 insertions(+), 79 deletions(-) create mode 100644 _plans/footer.txt create mode 100644 _specs/flashcard-session.md create mode 100644 src/components/LandingPage.jsx create mode 100644 tests/FlashcardSession.test.jsx diff --git a/_plans/footer.txt b/_plans/footer.txt new file mode 100644 index 0000000..2c1c5be --- /dev/null +++ b/_plans/footer.txt @@ -0,0 +1,7 @@ +Add a footer to the TermInput page. +Include an icon representing American Sign Language. +Include a disclaimer section with notes saying: + - that this site is not for professional use and is only for helping study ASL. + - uses public domain videos that are not owned by this creator. + - does not promise that the signs provided are the most recent versions of ASL + - attempts to restrict signs to ASL vs other sign languages (BSL, etc). diff --git a/_specs/flashcard-session.md b/_specs/flashcard-session.md new file mode 100644 index 0000000..ff2a020 --- /dev/null +++ b/_specs/flashcard-session.md @@ -0,0 +1,61 @@ +# Spec for flashcard-session + +branch: claude/feature/flashcard-session + +## Summary + +A full-screen flashcard practice session that displays ASL terms one at a time with color-coded cards, embedded video demonstrations, keyboard and button navigation, shuffle, and an alphabetical term list for direct access. + +## Functional Requirements + +- Display the current term in a color-coded card that fills the available width +- Show an embedded video (via iframe) for the current term below the card +- Derive the video URL from the term's `code` field; fall back to a constructed StartASL URL when `code` is empty; handle the special `type: "spell"` case by showing a fingerspelling GIF +- Show a position counter (e.g. "3 / 42") below the video +- Provide Prev and Next buttons to step through terms sequentially, wrapping around at the ends +- Support left/right arrow-key navigation equivalent to Prev/Next +- Provide a Shuffle button that randomizes the term order and resets to position 1; card colors follow the shuffle +- Provide a List button that toggles an alphabetical listbox to the right of the main content area +- The listbox must be sorted alphabetically and computed only once per term list (recalculated only after a shuffle) +- Selecting an item in the listbox navigates directly to that term; the selected item stays highlighted and tracks the current card as the user navigates with Prev/Next/arrows +- Provide a Back button that returns to the category selection screen + +## Possible Edge Cases + +- Term with an empty `code` field and no matching StartASL video URL (video will 404 silently) +- Term with `type: "spell"` and no fingerspelling GIF present in the public folder +- Single-term list (Prev/Next wrapping lands on the same card) +- Very long term labels that overflow the card area +- Shuffling while the listbox is open — listbox should remain open and reflect the new order + +## Acceptance Criteria + +- Navigating with Prev, Next, and arrow keys updates the card term, video, position counter, and listbox selection +- Shuffle randomizes the card order, resets to position 1, and updates the listbox +- The listbox appears to the right of the card/video column when toggled and does not reflow the main content +- The listbox selection always matches the current card, including after keyboard and button navigation +- The sorted order of the listbox does not change unless a shuffle occurs +- The Back button exits the session without side effects + +## Open Questions + +- Should the List panel be persistent (always visible) or remain a toggle? + - Always visible. +- Should terms with no resolvable video URL show a placeholder rather than a broken iframe? + - Please show a placeholder when no resolvable URL available. +- Should arrow-key navigation be disabled when focus is inside the listbox to avoid conflicts? + - yes + +## Testing Guidelines + +Create a test file(s) in the ./tests folder for the new feature, and create meaningful tests for the following cases, without going too heavy: + +- Renders the first term on mount +- Next button advances the index; wraps from last to first +- Prev button decrements the index; wraps from first to last +- ArrowRight and ArrowLeft key events trigger the same navigation as buttons +- Shuffle produces a different order and resets currentIndex to 0 +- List button toggles the listbox visibility +- Selecting an option in the listbox sets currentIndex to the correct value +- Listbox value stays in sync with currentIndex after button navigation +- sortedTerms is sorted alphabetically and stable between renders diff --git a/package.json b/package.json index a92addc..a0d9e6e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/react": "^19.2.14", diff --git a/src/App.css b/src/App.css index ce0b4ef..77715ed 100644 --- a/src/App.css +++ b/src/App.css @@ -1,7 +1,7 @@ .flashcard-app { - max-width: 960px; + width: 100%; margin: 0 auto; - padding: 48px 32px; + padding: 28px 12px; height: 100svh; overflow: hidden; display: flex; @@ -12,17 +12,46 @@ } } -/* ── Input screen ─────────────────────────────────────── */ +/* ── Landing page ─────────────────────────────────────── */ -.term-input { +.landing { display: flex; flex-direction: column; - gap: 28px; + gap: 32px; flex-grow: 1; } -.term-input__title { +.landing-hero { + display: flex; + flex-direction: column; + gap: 10px; +} + +.landing-hero__title { margin: 0; + font-size: clamp(28px, 5vw, 42px); + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-h); + padding-bottom: 10px; +} + +.landing-hero__desc { + font-size: 16px; + line-height: 1.6; + color: var(--text); + margin-left: 50px; + text-wrap: wrap; + margin-right: 50px; +} + +/* ── Input screen ─────────────────────────────────────── */ + +.term-input { + display: flex; + flex-direction: column; + gap: 28px; + flex-grow: 1; } .term-input__body { @@ -160,30 +189,54 @@ /* ── Session screen ───────────────────────────────────── */ +.flashcard-session-header { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.flashcard-session-title { + margin: 0; + font-size: clamp(20px, 3vw, 28px); + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-h); +} + +.flashcard-session-desc { + margin: 4px 0 0; + font-size: 14px; + color: var(--text); +} + .flashcard-session { display: flex; flex-direction: column; - gap: 24px; + gap: 16px; flex-grow: 1; + min-height: 0; } .flashcard-session-body { display: flex; flex-direction: row; - align-items: flex-start; + align-items: stretch; gap: 24px; flex-grow: 1; + min-height: 0; } .flashcard-session-content { display: flex; flex-direction: column; align-items: center; - gap: 24px; + gap: 16px; flex-grow: 1; + min-height: 0; } .btn-back { + flex-shrink: 0; align-self: flex-start; background: none; border: none; @@ -199,17 +252,16 @@ } .flashcard-card { - flex-grow: 1; + flex: 1 1 0; + min-height: 60px; display: flex; align-items: center; justify-content: center; width: 100%; - min-height: 200; border-radius: 16px; - padding: 40px 32px; + padding: 24px 32px; box-sizing: border-box; text-align: center; - /*overflow: hidden;*/ transition: background-color 0.25s ease; } @@ -225,7 +277,33 @@ } .flashcard-video { + width: 100%; + max-width: 560px; + aspect-ratio: 16 / 9; + flex-shrink: 1; + min-height: 0; +} +.flashcard-video-iframe { + width: 100%; + height: 100%; + display: block; + border: none; + border-radius: 8px; +} + +.flashcard-video-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 15px; + opacity: 0.6; } .flashcard-position { @@ -285,7 +363,9 @@ } .term-select { - min-width: 200px; + flex: 0 0 200px; + align-self: stretch; + overflow-y: auto; background: var(--code-bg); color: var(--text); border: 1px solid var(--border); diff --git a/src/App.jsx b/src/App.jsx index 51a12ae..0081f8a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { CardColors } from './data/card-colors' -import { TermInput } from './components/TermInput' +import { LandingPage } from './components/LandingPage' import { FlashcardSession } from './components/FlashcardSession' import './App.css' @@ -8,12 +8,16 @@ function App() { const [view, setView] = useState('input') const [terms, setTerms] = useState([]) const [cardColors, setCardColors] = useState([]) + const [categoryTitle, setCategoryTitle] = useState('') + const [categoryDescription, setCategoryDescription] = useState('') - function handleStart(parsedTerms) { + function handleStart(parsedTerms, title, description) { const palette = CardColors.length > 0 ? CardColors : ['#D8D4BC'] const colors = parsedTerms.map(() => palette[Math.floor(Math.random() * palette.length)]) setTerms(parsedTerms) setCardColors(colors) + setCategoryTitle(title) + setCategoryDescription(description) setView('session') } @@ -24,11 +28,9 @@ function App() { return (
{view === 'input' ? ( - + ) : ( - + )}
) diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index 81bf24e..da6238a 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -1,24 +1,20 @@ -import {useState, useEffect, useCallback, useMemo} from "react"; +import {useState, useEffect, useCallback, useMemo, useRef} from "react"; import {contrastColor} from "../utils/contrastColor"; import {shuffle} from "../utils/shuffle"; -export function FlashcardSession({terms, cardColors, onBack}) { +export function FlashcardSession({terms, cardColors, onBack, title, description}) { const [currentIndex, setCurrentIndex] = useState(0); const [localTerms, setLocalTerms] = useState(terms); const [localColors, setLocalColors] = useState(cardColors); - const [showSelect, setShowSelect] = useState(false); + const selectRef = useRef(null); const goNext = useCallback(() => { - const newIndex = (currentIndex + 1) % localTerms.length; - console.log("New Index: ", newIndex); - setCurrentIndex(newIndex); - }, [currentIndex, localTerms.length]); + setCurrentIndex(i => (i + 1) % localTerms.length); + }, [localTerms.length]); const goPrev = useCallback(() => { - const newIndex = (currentIndex - 1 + localTerms.length) % localTerms.length; - console.log("New Index: ", newIndex); - setCurrentIndex(newIndex); - }, [currentIndex, localTerms.length]); + setCurrentIndex(i => (i - 1 + localTerms.length) % localTerms.length); + }, [localTerms.length]); function handleShuffle() { const indices = shuffle([...localTerms.keys()]); @@ -28,27 +24,24 @@ export function FlashcardSession({terms, cardColors, onBack}) { } function getPlaybackUrl() { - console.log("Entry: ", localTerms[currentIndex]); - const term = localTerms[currentIndex].term; - const code = localTerms[currentIndex].code; - if (Object.prototype.hasOwnProperty.call(localTerms[currentIndex], "type")) { - switch (localTerms[currentIndex].type) { + const current = localTerms[currentIndex]; + if (Object.prototype.hasOwnProperty.call(current, "type")) { + switch (current.type) { case "spell": - console.log("returning url: ", '/fingerspell/aslg.gif'); - return ('/fingerspell/aslg.gif'); + return '/fingerspell/aslg.gif'; default: - console.error("Unknown term type: ", localTerms[currentIndex].type); + console.error("Unknown term type: ", current.type); } } - if(localTerms[currentIndex].code.length === 0) { - return `https://media.signbsl.com/videos/asl/startasl/mp4/${term.toLowerCase()}.mp4`; + if (current.code.length === 0) { + return null; } - console.log("returning url: ", code); - return localTerms[currentIndex].code; + return current.code; } useEffect(() => { function handleKey(e) { + if (document.activeElement === selectRef.current) return; if (e.key === "ArrowRight") goNext(); if (e.key === "ArrowLeft") goPrev(); } @@ -57,7 +50,6 @@ export function FlashcardSession({terms, cardColors, onBack}) { return () => window.removeEventListener("keydown", handleKey); }, [goNext, goPrev]); - const sortedTerms = useMemo( () => localTerms.map((t, i) => ({term: t.term, i})).sort((a, b) => a.term.localeCompare(b.term)), [localTerms] @@ -65,45 +57,55 @@ export function FlashcardSession({terms, cardColors, onBack}) { const bg = localColors[currentIndex]; const fg = contrastColor(bg); + const playbackUrl = getPlaybackUrl(); return (
- +
+ +
+

{title}

+

{description}

+
+
{localTerms[currentIndex].term}
- + {playbackUrl ? ( + + ) : ( +
+ No video available +
+ )}

{currentIndex + 1} / {localTerms.length}

-
- {showSelect && ( - - )} +
); diff --git a/src/components/LandingPage.jsx b/src/components/LandingPage.jsx new file mode 100644 index 0000000..3bc17e2 --- /dev/null +++ b/src/components/LandingPage.jsx @@ -0,0 +1,16 @@ +import {TermInput} from "./TermInput"; + +export function LandingPage({onStart}) { + return ( +
+
+

ASL Flashcards

+

+ Practice American Sign Language vocabulary with interactive flashcards + and embedded video demonstrations.
Choose a category below to get started. +

+
+ +
+ ); +} diff --git a/src/components/TermInput.jsx b/src/components/TermInput.jsx index db0a73e..fbc5dde 100644 --- a/src/components/TermInput.jsx +++ b/src/components/TermInput.jsx @@ -7,13 +7,13 @@ import verbs from "../data/verbs.json" with {type: "json"}; import axios from "axios"; const CATEGORIES = [ - {title: "Spelling", terms: terms}, - {title: "Numbers", terms: terms}, - {title: "Class Terms", terms: terms}, - {title: "Other Terms", terms: other_terms}, - {title: "Faire Terms", terms: terms}, - {title: "Questions", terms: questions}, - {title: "Verbs", terms: verbs} + {title: "Finger Spelling", description: "Practice spelling words letter by letter using ASL handshapes.", terms: terms}, + {title: "Numbers", description: "Learn to sign numbers in American Sign Language.", terms: terms}, + {title: "ASL Level I && II Class Terms", description: "Core vocabulary from ASL Level I and II coursework.", terms: terms}, + {title: "Questions", description: "Essential question words and phrases used in ASL conversation.", terms: questions}, + {title: "Verbs", description: "Common action words and verbs in American Sign Language.", terms: verbs}, + {title: "Other Terms", description: "Additional vocabulary terms for expanding your ASL knowledge.", terms: other_terms}, + {title: "Renaissance Faire Terms", description: "Specialized vocabulary for Renaissance Faire settings.", terms: terms}, ]; const webResources = [ @@ -61,7 +61,7 @@ export function TermInput({onStart}) { useEffect(() => { CATEGORIES.find((category) => category.title === "Numbers").terms = numberList; - CATEGORIES.find((category) => category.title === "Spelling").terms = wordlist; + CATEGORIES.find((category) => category.title === "Finger Spelling").terms = wordlist; }, [wordlist, numberList]); function handleStart(category) { @@ -71,12 +71,10 @@ export function TermInput({onStart}) { return; } setError(""); - onStart(terms); + onStart(terms, category.title, category.description); } return (
-

ASL Flashcards

-
{CATEGORIES.map(category => ( diff --git a/tests/FlashcardSession.test.jsx b/tests/FlashcardSession.test.jsx new file mode 100644 index 0000000..e6a3bc1 --- /dev/null +++ b/tests/FlashcardSession.test.jsx @@ -0,0 +1,125 @@ +import {describe, it, expect} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import {FlashcardSession} from '../src/components/FlashcardSession'; + +const TERMS = [ + {term: 'Banana', code: 'https://example.com/banana.mp4'}, + {term: 'Apple', code: 'https://example.com/apple.mp4'}, + {term: 'Cherry', code: ''}, +]; + +const COLORS = ['#F6C992', '#30525C', '#ACC0D3']; + +function renderSession(terms = TERMS, colors = COLORS) { + render( {}} />); +} + +// Returns the text currently shown on the flashcard card (not the listbox) +function displayedTerm() { + return document.querySelector('.flashcard-term').textContent; +} + +describe('FlashcardSession', () => { + it('renders the first term on mount', () => { + renderSession(); + expect(displayedTerm()).toBe('Banana'); + expect(screen.getByText('1 / 3')).toBeInTheDocument(); + }); + + it('Next button advances the index', () => { + renderSession(); + fireEvent.click(screen.getByText('Next →')); + expect(displayedTerm()).toBe('Apple'); + expect(screen.getByText('2 / 3')).toBeInTheDocument(); + }); + + it('Next button wraps from last to first', () => { + renderSession(); + fireEvent.click(screen.getByText('Next →')); + fireEvent.click(screen.getByText('Next →')); + fireEvent.click(screen.getByText('Next →')); + expect(displayedTerm()).toBe('Banana'); + expect(screen.getByText('1 / 3')).toBeInTheDocument(); + }); + + it('Prev button decrements the index', () => { + renderSession(); + fireEvent.click(screen.getByText('Next →')); + fireEvent.click(screen.getByText('← Prev')); + expect(displayedTerm()).toBe('Banana'); + }); + + it('Prev button wraps from first to last', () => { + renderSession(); + fireEvent.click(screen.getByText('← Prev')); + expect(displayedTerm()).toBe('Cherry'); + expect(screen.getByText('3 / 3')).toBeInTheDocument(); + }); + + it('ArrowRight key navigates forward', () => { + renderSession(); + fireEvent.keyDown(window, {key: 'ArrowRight'}); + expect(displayedTerm()).toBe('Apple'); + }); + + it('ArrowLeft key navigates backward', () => { + renderSession(); + fireEvent.keyDown(window, {key: 'ArrowLeft'}); + expect(displayedTerm()).toBe('Cherry'); + }); + + it('arrow keys do not navigate when listbox is focused', () => { + renderSession(); + const listbox = screen.getByRole('listbox'); + listbox.focus(); + fireEvent.keyDown(window, {key: 'ArrowRight'}); + expect(displayedTerm()).toBe('Banana'); + }); + + it('listbox is always visible without a toggle', () => { + renderSession(); + expect(screen.getByRole('listbox')).toBeInTheDocument(); + expect(screen.queryByText('List')).not.toBeInTheDocument(); + }); + + it('listbox is sorted alphabetically', () => { + renderSession(); + const options = screen.getAllByRole('option'); + const labels = options.map(o => o.textContent); + expect(labels).toEqual([...labels].sort((a, b) => a.localeCompare(b))); + }); + + it('selecting a listbox option updates the displayed term', () => { + renderSession(); + const listbox = screen.getByRole('listbox'); + // Apple is at index 1 in localTerms + fireEvent.change(listbox, {target: {value: '1'}}); + expect(displayedTerm()).toBe('Apple'); + }); + + it('listbox value stays in sync after button navigation', () => { + renderSession(); + fireEvent.click(screen.getByText('Next →')); + expect(screen.getByRole('listbox').value).toBe('1'); + }); + + it('shows a placeholder when a term has an empty code field', () => { + renderSession([{term: 'NoVideo', code: ''}], ['#F6C992']); + expect(screen.getByText('No video available')).toBeInTheDocument(); + expect(screen.queryByTitle('ASL sign video')).not.toBeInTheDocument(); + }); + + it('shows an iframe when a term has a non-empty code field', () => { + renderSession([{term: 'HasVideo', code: 'https://example.com/video.mp4'}], ['#F6C992']); + expect(screen.getByTitle('ASL sign video')).toBeInTheDocument(); + expect(screen.queryByText('No video available')).not.toBeInTheDocument(); + }); + + it('Shuffle resets to index 0', () => { + renderSession(); + fireEvent.click(screen.getByText('Next →')); + fireEvent.click(screen.getByText('⇄ Shuffle')); + expect(screen.getByText('1 / 3')).toBeInTheDocument(); + }); +}); diff --git a/yarn.lock b/yarn.lock index a962fca..468cae5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32,7 +32,7 @@ resolved "https://registry.yarnpkg.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24" integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== -"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": +"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== @@ -474,6 +474,20 @@ resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== +"@testing-library/dom@^10.4.1": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + "@testing-library/jest-dom@^6.9.1": version "6.9.1" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz#7613a04e146dd2976d24ddf019730d57a89d56c2" @@ -500,6 +514,11 @@ dependencies: tslib "^2.4.0" +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + "@types/chai@^5.2.2": version "5.2.3" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-5.2.3.tgz#8e9cd9e1c3581fa6b341a5aed5588eb285be0b4a" @@ -622,6 +641,11 @@ ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -629,11 +653,23 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + aria-query@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" @@ -811,11 +847,21 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + detect-libc@^2.0.3: version "2.1.2" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.1.2.tgz#689c5dcdc1900ef5583a4cb9f6d7b473742074ad" integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + dom-accessibility-api@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" @@ -1391,6 +1437,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + magic-string@^0.30.21: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" @@ -1512,7 +1563,7 @@ pathe@^2.0.3: resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== -picocolors@^1.1.1: +picocolors@1.1.1, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -1536,6 +1587,15 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + proxy-from-env@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz#a7487568adad577cfaaa7e88c49cab3ab3081aba" @@ -1553,6 +1613,11 @@ react-dom@^19.2.4: dependencies: scheduler "^0.27.0" +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + react@^19.2.4: version "19.2.5" resolved "https://registry.yarnpkg.com/react/-/react-19.2.5.tgz#c888ab8b8ef33e2597fae8bdb2d77edbdb42858b" From a33d3a95d2688cb32d4069d66fcb449412d503ca Mon Sep 17 00:00:00 2001 From: clacina Date: Tue, 28 Apr 2026 08:06:40 -0700 Subject: [PATCH 2/2] adding some new slash commands --- .claude/commands/code-review2.md | 95 +++++++++++++++ .claude/commands/commit-and-push.md | 4 + .claude/commands/commit-message.md | 42 ------- .claude/commands/evaluate-repository.md | 155 ++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 42 deletions(-) create mode 100644 .claude/commands/code-review2.md create mode 100644 .claude/commands/commit-and-push.md delete mode 100644 .claude/commands/commit-message.md create mode 100644 .claude/commands/evaluate-repository.md diff --git a/.claude/commands/code-review2.md b/.claude/commands/code-review2.md new file mode 100644 index 0000000..d6b1fe7 --- /dev/null +++ b/.claude/commands/code-review2.md @@ -0,0 +1,95 @@ +# Code Reviewer Assistant for Claude Code + + You are an expert code reviewer tasked with analyzing a codebase and providing actionable feedback. Your primary responsibilities are: + + ## Core Review Process + + 1. **Analyze the codebase structure** - Understand the project architecture, technologies used, and coding patterns + 2. **Identify issues and improvements** across these categories: + - **Security vulnerabilities** and potential attack vectors + - **Performance bottlenecks** and optimization opportunities + - **Code quality issues** (readability, maintainability, complexity) + - **Best practices violations** for the specific language/framework + - **Bug risks** and potential runtime errors + - **Architecture concerns** and design pattern improvements + - **Testing gaps** and test quality issues + - **Documentation deficiencies** + + 3. **Prioritize findings** using this severity scale: + - 🔴 **Critical**: Security vulnerabilities, breaking bugs, major performance issues + - 🟠 **High**: Significant code quality issues, architectural problems + - 🟡 **Medium**: Minor bugs, style inconsistencies, missing tests + - 🟢 **Low**: Documentation improvements, minor optimizations + + ## TASK.md Management + + Always read the existing TASK.md file first. Then update it by: + + ### Adding New Tasks + - Append new review findings to the appropriate priority sections + - Use clear, actionable task descriptions + - Include file paths and line numbers where relevant + - Reference specific code snippets when helpful + + ### Task Format + ```markdown + ## 🔴 Critical Priority + - [ ] **[SECURITY]** Fix SQL injection vulnerability in `src/auth/login.js:45-52` + - [ ] **[BUG]** Handle null pointer exception in `utils/parser.js:120` + + ## 🟠 High Priority + - [ ] **[REFACTOR]** Extract complex validation logic from `UserController.js` into separate service + - [ ] **[PERFORMANCE]** Optimize database queries in `reports/generator.js` + + ## 🟡 Medium Priority + - [ ] **[TESTING]** Add unit tests for `PaymentProcessor` class + - [ ] **[STYLE]** Consistent error handling patterns across API endpoints + + ## 🟢 Low Priority + - [ ] **[DOCS]** Add JSDoc comments to public API methods + - [ ] **[CLEANUP]** Remove unused imports in `components/` directory + ``` + + ### Maintaining Existing Tasks + - Don't duplicate existing tasks + - Mark completed items you can verify as `[x]` + - Update or clarify existing task descriptions if needed + + ## Review Guidelines + + ### Be Specific and Actionable + - ✅ "Extract the 50-line validation function in `UserService.js:120-170` into a separate `ValidationService` class" + - ❌ "Code is too complex" + + ### Include Context + - Explain *why* something needs to be changed + - Suggest specific solutions or alternatives + - Reference relevant documentation or best practices + + ### Focus on Impact + - Prioritize issues that affect security, performance, or maintainability + - Consider the effort-to-benefit ratio of suggestions + + ### Language/Framework Specific Checks + - Apply appropriate linting rules and conventions + - Check for framework-specific anti-patterns + - Validate dependency usage and versions + + ## Output Format + + Provide a summary of your review findings, then show the updated TASK.md content. Structure your response as: + + 1. **Review Summary** - High-level overview of findings + 2. **Key Issues Found** - Brief list of most important problems + 3. **Updated TASK.md** - The complete updated file content + + ## Commands to Execute + + When invoked, you should: + 1. Scan the entire codebase for issues + 2. Read the current TASK.md file + 3. Analyze and categorize all findings + 4. Update TASK.md with new actionable tasks + 5. Provide a comprehensive review summary + + Focus on being thorough but practical - aim for improvements that will genuinely make the codebase more secure, performant, and maintainable. diff --git a/.claude/commands/commit-and-push.md b/.claude/commands/commit-and-push.md new file mode 100644 index 0000000..3be0a62 --- /dev/null +++ b/.claude/commands/commit-and-push.md @@ -0,0 +1,4 @@ +ADD all modified and new files to git. If you think there are files that should not be in version control, ask the user. If you see files that you think should be bundled into separate commits, ask the user. +THEN commit with a clear and concise one-line commit message, using semantic commit notation. +THEN push the commit to origin. +The user is EXPLICITLY asking you to perform these git tasks. diff --git a/.claude/commands/commit-message.md b/.claude/commands/commit-message.md deleted file mode 100644 index 3ab4e13..0000000 --- a/.claude/commands/commit-message.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: Create a commit message by analyzing git diffs -allowed-tools: Bash(git status:*), Bash(git diff --staged), Bash(git commit:*) ---- - -## Run these commands: - -```bash -git status -git diff --staged -``` - -## Your task: - -Analyze above staged git changes and create a commit message. Use present tense and explain "why" something has changed, not just "what" has changed. - -## Commit types with emojis: -Only use the following emojis: - -- ✨ `feat:` - New feature -- 🐛 `fix:` - Bug fix -- 🔨 `refactor:` - Refactoring code -- 📝 `docs:` - Documentation -- 🎨 `style:` - Styling/formatting -- ✅ `test:` - Tests -- ⚡ `perf:` - Performance - -## Format: -Use the following format for making the commit message: - -``` - : - -``` - -## Output: - -1. Show summary of changes currently staged -2. Propose commit message with appropriate emoji -3. Ask for confirmation before committing - -DO NOT auto-commit - wait for user approval, and only commit if the user says so. \ No newline at end of file diff --git a/.claude/commands/evaluate-repository.md b/.claude/commands/evaluate-repository.md new file mode 100644 index 0000000..e287054 --- /dev/null +++ b/.claude/commands/evaluate-repository.md @@ -0,0 +1,155 @@ +# Repository Evaluation Prompt (Awesome-Claude-Code · Full Version) + +## Evaluation Context (Claude Code Ecosystem) + +You are evaluating a repository intended for use in or alongside **Claude Code**, where certain features (such as hooks, commands, scripts, or automation) may execute implicitly or with elevated trust once enabled by a user. + +In this ecosystem, risk commonly arises not from overtly malicious code, but from implicit execution surfaces, including: +- Hooks that execute automatically based on tool lifecycle events +- Custom commands that may invoke shell scripts +- Scripts that run in the user’s local environment +- Persistent state files that influence control flow +- Network access triggered indirectly by tooling + +Your task is to perform a conservative, evidence-based, static review that: +- Identifies trust boundaries and implicit execution +- Distinguishes declared behavior from effective capability +- Surfaces red flags or areas requiring further manual inspection +- Avoids inferring author intent beyond what is observable + +When uncertain, prefer explicit uncertainty over confident speculation. + +--- + +## Instructions + +Perform a static, read-only review of the repository named at the end of this prompt. + +Do not run any code, install dependencies, or execute scripts. +Base your assessment solely on repository contents and documentation. + +This evaluation supports curation and triage, not automated approval. + +--- + +## Evaluation Criteria + +For each category below: +- Assign a score from 1–10 +- Provide concise justification +- Explicitly note uncertainty +- Separate red flags from speculation + +### 1. Code Quality +Assess structure, readability, correctness, and internal consistency. + +### 2. Security & Safety +Assess risks related to: +- Implicit execution (hooks, background behavior) +- File system access +- Network access +- Credential handling +- Tool escalation or privilege assumptions + +### 3. Documentation & Transparency +Assess whether documentation accurately describes behavior, discloses side effects, and matches implementation. + +### 4. Functionality & Scope +Assess whether the repository appears to do what it claims within its stated scope. + +### 5. Repository Hygiene & Maintenance +Assess signals of care, maintainability, licensing, and publication quality. + +--- + +## Claude-Code-Specific Checklist + +Explicitly answer each item: +- Defines hooks (stop, lifecycle, or similar) +- Hooks execute shell scripts +- Commands invoke shell or external tools +- Writes persistent local state files +- Reads state to control execution flow +- Performs implicit execution without explicit confirmation +- Documents hook or command side effects +- Includes safe defaults +- Includes a clear disable or cancel mechanism + +Briefly explain any checked item. + +--- + +## Permissions & Side Effects Analysis + +### A. Reported / Declared Permissions +From documentation or config: +- File system: +- Network: +- Execution / hooks: +- APIs / tools: + +### B. Likely Actual Permissions (Inferred) +From static inspection: +- File system: +- Network: +- Execution / hooks: +- APIs / tools: + +Mark items as confirmed, likely, or unclear. + +### C. Discrepancies +List mismatches between declared and inferred behavior. + +--- + +## Red Flag Scan + +Check all that apply and justify: +- Malware or spyware indicators +- Undisclosed implicit execution +- Undocumented file or network activity +- Unsupported claims +- Supply-chain or trust risks + +--- + +## Overall Assessment + +### Overall Score +Score: X / 10 + +### Recommendation +Choose one: +- Recommend +- Recommend with caveats +- Needs further manual review +- Definitely reject + +### Fast-Reject Heuristic +If "Definitely reject", specify which applies: +- Clear malicious behavior +- Undisclosed high-risk implicit execution +- Severe claim/behavior mismatch +- Unsafe defaults with no mitigation +- Other (explain) + +--- + +## Possible Remedies / Improvement Suggestions + +If applicable, list specific, minimal changes that could materially improve the submission or change the recommendation (e.g., documentation clarifications, safer defaults, permission scoping). + +--- + +## Output Format + +Use clear section headings corresponding to the sections above. +Keep the evaluation concise, precise, and evidence-based. + +--- + +REPOSITORY: + +IF PRESENT: $ARGUMENTS + +ELSE: The repository you are currently working in.