From 86f0f87d6ff56545420a5888bee51998dd516906 Mon Sep 17 00:00:00 2001 From: Griffen Fargo <3642037+gfargo@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:36:43 -0400 Subject: [PATCH] fix: component improvements and missing exports - Fix Deck component: replace React.CSSProperties with Ink BoxProps, remove @ts-ignore - Fix DeckContext: create per-provider EventManager/EffectManager instances instead of shared singletons - Fix GameContext: add dispatch to useMemo dependency array for consistency - Export UnicodeCard and UnicodeCardProps from public API - CardStack: support rendering custom cards alongside standard cards - Delete dead robotCard.test.bak file - Add 8 new UnicodeCard tests (jokers, suits, rounded, edge cases) - Add 2 new CardStack tests (custom cards, mixed standard+custom) --- src/components/Card/robotCard.test.bak | 17 ----- src/components/CardStack/CardStack.test.tsx | 46 ++++++++++++++ .../CardStack/CardStack.test.tsx.md | 58 ++++++++++++++++++ .../CardStack/CardStack.test.tsx.snap | Bin 1129 -> 1359 bytes src/components/CardStack/index.tsx | 5 +- src/components/Deck/index.tsx | 7 +-- .../UnicodeCard/UnicodeCard.test.tsx | 56 +++++++++++++++++ .../UnicodeCard/UnicodeCard.test.tsx.md | 50 +++++++++++++++ .../UnicodeCard/UnicodeCard.test.tsx.snap | Bin 458 -> 630 bytes src/contexts/DeckContext.tsx | 34 +++++----- src/contexts/GameContext.tsx | 5 +- src/index.tsx | 4 ++ 12 files changed, 244 insertions(+), 38 deletions(-) delete mode 100644 src/components/Card/robotCard.test.bak diff --git a/src/components/Card/robotCard.test.bak b/src/components/Card/robotCard.test.bak deleted file mode 100644 index 164714a..0000000 --- a/src/components/Card/robotCard.test.bak +++ /dev/null @@ -1,17 +0,0 @@ -import { ROBOT_FEATURES, ROBOT_THEME } from '../../constants/robotTheme.js' -import { renderCardArt } from '../../utils/cardArtRenderer.js' - -// Test rendering of a robot Jack of Clubs -const jackOfClubs = ROBOT_THEME['J']! -const features = ROBOT_FEATURES.clubs - -const art = renderCardArt( - jackOfClubs, - 15, // Card width - { - ...features, - suit: '♣', - } -) - -console.log(art.join('\n')) diff --git a/src/components/CardStack/CardStack.test.tsx b/src/components/CardStack/CardStack.test.tsx index dd9d6d8..19608b2 100644 --- a/src/components/CardStack/CardStack.test.tsx +++ b/src/components/CardStack/CardStack.test.tsx @@ -415,3 +415,49 @@ test('render ascii variant', (t) => { const asciiVariantLastFrame = lastFrame() t.snapshot(asciiVariantLastFrame) }) + +test('render stack with custom cards', (t) => { + const { lastFrame } = render( + + ) + t.snapshot(lastFrame()) +}) + +test('render stack with mixed standard and custom cards', (t) => { + const { lastFrame } = render( + + ) + t.snapshot(lastFrame()) +}) diff --git a/src/components/CardStack/CardStack.test.tsx.md b/src/components/CardStack/CardStack.test.tsx.md index a9c5cd1..331d863 100644 --- a/src/components/CardStack/CardStack.test.tsx.md +++ b/src/components/CardStack/CardStack.test.tsx.md @@ -558,3 +558,61 @@ Generated by [AVA](https://avajs.dev). │ ♠ 5│␊ ╰─────────────╯␊ ` + +## render stack with custom cards + +> Snapshot 1 + + `␊ + Custom Stack (2)␊ + ╭──────────╮␊ + │Flame Lanc│␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + ╰──────────╯␊ + ␊ + ╭──────────╮␊ + │Arcane Den│␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + ╰──────────╯␊ + ` + +## render stack with mixed standard and custom cards + +> Snapshot 1 + + `␊ + Mixed Stack (3)␊ + ╭─────────╮␊ + │A │␊ + │ │␊ + │ │␊ + │ ♥ │␊ + │ │␊ + │ │␊ + │ A│␊ + ╰─────────╯␊ + ␊ + ╭──────────╮␊ + │Wild │␊ + │ │␊ + │ │␊ + │ │␊ + │ │␊ + ╰──────────╯␊ + ␊ + ╭─────────╮␊ + │K │␊ + │ WW│␊ + │ {)│␊ + │ ♠ %%│␊ + │ %%%│␊ + │ _%%%>│␊ + │ K│␊ + ╰─────────╯␊ + ` diff --git a/src/components/CardStack/CardStack.test.tsx.snap b/src/components/CardStack/CardStack.test.tsx.snap index dd1e4db251879c603c52beb2110d0e091781deed..e4ae476e0dbc41294386100222caf7365043205f 100644 GIT binary patch literal 1359 zcmV-V1+e--RzVF}X$^6uW2kX#KxJiC|lBMU6 z421l&7$1uW00000000B+TupD|L>O+GU66X?xMX)3J}lY`s$-{B4^SdL4!eA4#i2qG zs%C9h@iw-L?bHhe3HHE+Yb(L63tDc#O8fvM{sO;%969ZQ3n!4{k7jI-=fe|{im95a z^}L=LKhOI<@0&NHK0LTP@{Ggan`_@$p&gE_`!{^cH7y@}Jhm(scn823jqe8F02&q; zAK8HmkAk5W246ljO&G!tTUvYN>6Q4ab#>*L)t6dpAhd!Itg9Pqpfk|6oypmclhZG- z-)BE{2HK938J|A51JbX_>4U;hIuJ$Jp_BFMp~<)31E46l`xM;JzM?4aV^;%ribH3Q zYoGP!#5e={W(qb0VdTYN4=w12b!eX;Xpfqs-5F@A)6Je|=4#0(f$bnT)o6S+*u1DY;q(7OlBixx7@UObv z4EN`9+|^A=mHHZi{xd-;rEXHGL97MYON?+NMq=lq-XQ=$-4vi%HwrWoi-ZM}_$Ysg z$=r#N*!ie;I4=^3BKQ_U;#PAKbN+C0l)dI4W`uhQD^EFS2LJOp{P&=7NGXP&AQapa z6i2rhYTqcLFgn?w(>8^s$RZl;?<>lHW~N&LqUJ>cZKfBw6~sjdaYG@*?Krz+;&x~X z-^*ykS@0PamDUOGA@u9YHr}Bc5o@wqUr-OBjJ1r8rHqbeQ^u0+fdT!xB-H*JVe8L! z#+D{y1BN-v@s`dR!A)8)ltQbB%D(GJ7sOdvfB_tKTp0Sq9z~9@UCHLG?u;=o#ya zCSfl%T4q3r?qxJ$lA0)0nxW_0pL%WxM<58HaadNF)iXq8zFK8c8NMN6Nm9(A_35He z5L9J7vcx0hLQ(hYlLvNvA}I z-1zqv(T%GWraneh2WT3ROp^{uSB1@c)IwgW0w&sp1Jb@fGd z8gyF7fyfSrz!(Ri=OlfziubV}A=%@-)iv-&a>EDHDb~A57EviYX4X;EM1EA~?Gbb= z@D6m1&Y;^xMM*8chet0j&WcC@5>GlKI(mjq-#4IZfj2F;jE-KtSA7YrsL{#%QZ4(X zW8b&T_=0Of-vrP#39j1y4RO_9+H2t5=^Ux2HaXv>I{ldkCZ|~;MhiK>(&wlR5MS3A zeO=3ZeY(<~Ju literal 1129 zcmV-v1eW_jRzVp5y!Q`9czLQ5~hZ*Y}>E=icXi z^V&z78!b!ScHVu^T-|MHPv>mSY-l!k($+K+Serm?wKp8Fsi+!g@92)H>^M!!bzUDe z8j7nt&dK?S%M<=9H$8D>@=k6BxSHdF`O?A+DAeVe(d~Th9=t;Sb-onp@{$o6AMCGz z;MG0Yj|~L_o`)RTn@=94*e(w3JqYbqcC-t1xnwMc*K^_9KBFFt$24YCbMZ4uv8u&Hqjap@I%lnEYnsnn zLstyTY~YxC2{C7KVos*V=o3f0hccJUfxAM)U6n@M?n9*A%T8JqCvImV)q6)v&M?lb zW&?dT0=?#g?(8TH4Tt(NgnBVM>NPw9hah-QjUD04o&mepS~lEoM7WoVlq&TWg8l_3 zl`0jf)WFw*@ZcjHJtMyJQLhjHP%3iJthpGC#3EtHBpycxpUmAe;yWMp3I|0ZQ3M}C zBv!MN81aXaqwpAjm=Vq;cs}DG8~isT__q{wn^Fv)K@@CpilfyOwXf$<80{_4X`4h- zWD$+l*CnY=Gt(-EsJTs`tzmg)0&yNhT$2lNOU5#pxFwpxcN~ov3qDn&(mL)vgcV%b z#yW%%u`J$JIQ03wtnVAwqy|-NTfwCjApJZy)e-$tJn@(j`459A>jtPOTKzB51ldFN&Y+7xbSepESgyu9VP0HeClt{oM zGFO%a%!qM0^qo}Kvfrs;8=~@8&L$cKglMF!PqN5{(0GwS1-j#C#8{u`yX4l%H%Mqs zqWYvF9=Q7qOsESzlxcd!f-H0}6D<(I}PFc!AP1 zEn9zXnXb|Tj;pBK{VFrrfmG(rWGaonUkI|9=WFKl$ z6w?GSK@U?e50nGCM`}@(vqz%%EGjIc?j~;v3bD&QfObiVuuD*%aY%&!pMTZ=4S9z_ zq*CC>?)4(psLu%;00alP=QU0t@PDv|=rTnb9a?~j#r1C%H+-_kQFR?H;g|Bz{`fVW z_V*4=eG&8wNX6KNE(B-8XaWD>oT&``1+cVjQbb9b{ZkHVDr7ANT_VHq`Shf=(3t!8 vr%%~x8W5+IuqYoKrial*<6Buc2hl7;TLBfThiThmNv!?_s+7_!$U6W4$W$29 diff --git a/src/components/CardStack/index.tsx b/src/components/CardStack/index.tsx index e9d08a8..6b71ad1 100644 --- a/src/components/CardStack/index.tsx +++ b/src/components/CardStack/index.tsx @@ -1,7 +1,8 @@ import { Box, Text, type BoxProps } from 'ink' import React from 'react' -import { isStandardCard, type TCard } from '../../types/index.js' +import { isCustomCard, isStandardCard, type TCard } from '../../types/index.js' import Card from '../Card/index.js' +import { CustomCard } from '../CustomCard/index.js' import { MiniCard } from '../MiniCard/index.js' type CardStackProperties = { @@ -94,6 +95,8 @@ export function CardStack({ variant={variant} /> ) + ) : isCustomCard(card) ? ( + ) : null} ) diff --git a/src/components/Deck/index.tsx b/src/components/Deck/index.tsx index a9a40fd..37edb68 100644 --- a/src/components/Deck/index.tsx +++ b/src/components/Deck/index.tsx @@ -1,4 +1,4 @@ -import { Box } from 'ink' +import { Box, type BoxProps } from 'ink' import React, { useMemo } from 'react' import { useDeck } from '../../hooks/useDeck.js' import { @@ -10,7 +10,7 @@ import Card from '../Card/index.js' type DeckProperties = { readonly showTopCard?: boolean - readonly style?: React.CSSProperties + readonly style?: BoxProps readonly variant?: 'simple' | 'ascii' | 'minimal' readonly placeholderCard?: { suit: TSuit; value: TCardValue } } @@ -23,7 +23,7 @@ export function Deck({ }: DeckProperties) { const { deck } = useDeck() - const deckStyle = { + const deckStyle: BoxProps = { padding: 1, borderStyle: 'single', ...style, @@ -51,7 +51,6 @@ export function Deck({ }, [deck, showTopCard, variant]) return ( - // @ts-ignore {renderTopCard} diff --git a/src/components/UnicodeCard/UnicodeCard.test.tsx b/src/components/UnicodeCard/UnicodeCard.test.tsx index b0bc813..027d9fd 100644 --- a/src/components/UnicodeCard/UnicodeCard.test.tsx +++ b/src/components/UnicodeCard/UnicodeCard.test.tsx @@ -63,3 +63,59 @@ test('render with size prop', (t) => { ) t.snapshot(lastFrame()) }) + +test('render diamonds shows red color', (t) => { + const { lastFrame } = render() + t.snapshot(lastFrame()) +}) + +test('render clubs shows white color', (t) => { + const { lastFrame } = render() + t.snapshot(lastFrame()) +}) + +test('render joker with hearts suit', (t) => { + const { lastFrame } = render() + const frame = lastFrame() + t.snapshot(frame) + if (frame) { + t.true(frame.includes(SPECIAL_CARDS.RED_JOKER)) + } +}) + +test('render joker with spades suit', (t) => { + const { lastFrame } = render() + const frame = lastFrame() + t.snapshot(frame) + if (frame) { + t.true(frame.includes(SPECIAL_CARDS.BLACK_JOKER)) + } +}) + +test('render joker with clubs suit', (t) => { + const { lastFrame } = render() + const frame = lastFrame() + t.snapshot(frame) + if (frame) { + t.true(frame.includes(SPECIAL_CARDS.WHITE_JOKER)) + } +}) + +test('render bordered with rounded false', (t) => { + const { lastFrame } = render( + + ) + t.snapshot(lastFrame()) +}) + +test('render face down without dimmed uses suit color', (t) => { + const { lastFrame } = render( + + ) + t.snapshot(lastFrame()) +}) + +test('render 10 value card', (t) => { + const { lastFrame } = render() + t.snapshot(lastFrame()) +}) diff --git a/src/components/UnicodeCard/UnicodeCard.test.tsx.md b/src/components/UnicodeCard/UnicodeCard.test.tsx.md index 45cfc9f..f24c04b 100644 --- a/src/components/UnicodeCard/UnicodeCard.test.tsx.md +++ b/src/components/UnicodeCard/UnicodeCard.test.tsx.md @@ -59,3 +59,53 @@ Generated by [AVA](https://avajs.dev). │ 🃅 │␊ │ │␊ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯` + +## render diamonds shows red color + +> Snapshot 1 + + '🃃' + +## render clubs shows white color + +> Snapshot 1 + + '🃙' + +## render joker with hearts suit + +> Snapshot 1 + + '🂿' + +## render joker with spades suit + +> Snapshot 1 + + '🃏' + +## render joker with clubs suit + +> Snapshot 1 + + '🃟' + +## render bordered with rounded false + +> Snapshot 1 + + `┌──────────────────────────────────────────────────────────────────────────────────────────────────┐␊ + │🂡 │␊ + └──────────────────────────────────────────────────────────────────────────────────────────────────┘` + +## render face down without dimmed uses suit color + +> Snapshot 1 + + '🂠' + +## render 10 value card + +> Snapshot 1 + + '🂺' diff --git a/src/components/UnicodeCard/UnicodeCard.test.tsx.snap b/src/components/UnicodeCard/UnicodeCard.test.tsx.snap index ec46f332659e194795f0578f5ad79f1e8510cd15..4adfc67a37b9136e98060488aec3db6408183e0c 100644 GIT binary patch literal 630 zcmV-+0*U=WRzVH*PnG{dQgkXv9HtLm26}_q#D_5&mHujpW z_;|EG-q>sI#s%|+Wp-(DUqeH{T+RjE(a^6B6j`XD5Y|tS2w7jC0CR%cIOuwF-#Ogt zmfNQ?L~X_ia3HA1+4Mp0)qa(UweK~9%(D-9`eYqvNA}UA{_;uM(*5Jn;Dz~v&U+O* zmiNz&mY~F4v%;}xw$%RuWaER z94<^g#kQx1I9zs4Rwdp#@@bUiLuecWIW;v86WiUMHnD4EW}t**F$!43xQ{8`(Y!BZ zo+{%6eGGr`!wVHh)j#TMKb)_y{^5s<5%WwloNR8pq`|XA9Oj=VIq$XZ3I6Bmx;?>7 zTQLa5F-wTB&ADz2yz7v;b^`~yX|X}1+6A*8QVIT4W0P-Y&LXYz zSx`Gn`9jS}U(WQ364a(*9_Gkl-m=U|=Vq{+se~9OiUjygKCL)UpD+I=u0{+eRiXtM~b;p4-hyyUWftdWa(h=83}_ Q_1U0)1D;V{dFT-U0MSS@j{pDw literal 458 zcmV;*0X6`Z&G*~XI|UI-JNA-#OPE? z&?P8v34}1a_vaq;VfG$a=MFLJs=H~m5-P$D&t>=UdtYAWndj}4OC}=~ad8jQkR}}G zIM@_8!UPa2BjI2Q0+rQg)iH#P9icNR&N`H0i4SraZADWjwXChD*0RO?zSV8lJK18s zVD*_>0kdM*M+&L~=5iwFu7bWspeTU)6v}Eg5<061L@=kQgh?ef_b$TS47fEz>Zrsx z1r8KBoQ*bmnEnhC({E`X>A4SH^EA=lav%NCulB~)tasauOaBj=_UTs)*LR=iKaquy z`xJ4^_E^VYn<4=w5D9eDIhj<%IUmY7dCPN~o_m!jhHeHoSB>pDcz7GFnc1gbcGyH~ z-ZNPg-A3$_3)_bpmIZVeYB55rurnHB8(}7?3dt-)n8|pYP~20zmoTpy#tGvDe#pZm zE#m6;Op=F_J*?mIa5iQh8-|18!IsoGpG7mjb~5dg>;(V0y1kv?3+dH>BTx(g0E>0g A`Tzg` diff --git a/src/contexts/DeckContext.tsx b/src/contexts/DeckContext.tsx index de8adb9..bd5e12a 100644 --- a/src/contexts/DeckContext.tsx +++ b/src/contexts/DeckContext.tsx @@ -32,19 +32,16 @@ export const defaultBackArtwork: BackArtwork = { minimal: genBack('minimal'), } -const sharedEventManager = new EventManager() -const sharedEffectManager = new EffectManager() - -const initialState: DeckContextType = { +const createInitialState = (): DeckContextType => ({ zones: { deck: [], hands: {}, discardPile: [], playArea: [] }, players: [], backArtwork: defaultBackArtwork, - eventManager: sharedEventManager, - effectManager: sharedEffectManager, + eventManager: new EventManager(), + effectManager: new EffectManager(), dispatch: () => null, -} +}) -export const DeckContext = createContext(initialState) +export const DeckContext = createContext(createInitialState()) const deckReducer = ( state: DeckContextType, @@ -235,13 +232,20 @@ export function DeckProvider({ initialCards, customReducer, }: DeckProviderProperties) { - const [state, dispatch] = useReducer(customReducer ?? deckReducer, { - ...initialState, - zones: { - ...initialState.zones, - deck: initialCards ?? createStandardDeck(), - }, - }) + const [state, dispatch] = useReducer( + customReducer ?? deckReducer, + initialCards, + (cards) => { + const base = createInitialState() + return { + ...base, + zones: { + ...base.zones, + deck: cards ?? createStandardDeck(), + }, + } + } + ) const contextValue = useMemo( () => ({ ...state, dispatch }), [state, dispatch] diff --git a/src/contexts/GameContext.tsx b/src/contexts/GameContext.tsx index f2b4944..3f06842 100644 --- a/src/contexts/GameContext.tsx +++ b/src/contexts/GameContext.tsx @@ -67,7 +67,10 @@ export function GameProvider({ currentPlayerId: initialPlayers[0] ?? '', }) - const contextValue = useMemo(() => ({ ...state, dispatch }), [state]) + const contextValue = useMemo( + () => ({ ...state, dispatch }), + [state, dispatch] + ) return ( {children} diff --git a/src/index.tsx b/src/index.tsx index 5126cad..760a313 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,6 +10,10 @@ export { createStandardDeck, } from './components/Deck/utils.js' export { MiniCard } from './components/MiniCard/index.js' +export { + UnicodeCard, + type UnicodeCardProps, +} from './components/UnicodeCard/index.js' export { GameContext, GameProvider } from './contexts/GameContext.js' export { useDeck } from './hooks/useDeck.js' export { useHand } from './hooks/useHand.js'