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).
[37m│ ♠ 5│[39m␊
[37m╰─────────────╯[39m␊
`
+
+## render stack with custom cards
+
+> Snapshot 1
+
+ `␊
+ Custom Stack (2)␊
+ [31m╭──────────╮[39m␊
+ [31m│[1mFlame Lanc[22m│[39m␊
+ [31m│[39m [31m│[39m␊
+ [31m│[39m [31m│[39m␊
+ [31m│[39m [31m│[39m␊
+ [31m│[39m [31m│[39m␊
+ [31m╰──────────╯[39m␊
+ ␊
+ [34m╭──────────╮[39m␊
+ [34m│[1mArcane Den[22m│[39m␊
+ [34m│[39m [34m│[39m␊
+ [34m│[39m [34m│[39m␊
+ [34m│[39m [34m│[39m␊
+ [34m│[39m [34m│[39m␊
+ [34m╰──────────╯[39m␊
+ `
+
+## render stack with mixed standard and custom cards
+
+> Snapshot 1
+
+ `␊
+ Mixed Stack (3)␊
+ [37m╭─────────╮[39m␊
+ [37m│[31mA [37m│[39m␊
+ [37m│[31m [37m│[39m␊
+ [37m│[31m [37m│[39m␊
+ [37m│[31m ♥ [37m│[39m␊
+ [37m│[31m [37m│[39m␊
+ [37m│[31m [37m│[39m␊
+ [37m│[31m A[37m│[39m␊
+ [37m╰─────────╯[39m␊
+ ␊
+ [33m╭──────────╮[39m␊
+ [33m│[1m[37mWild [22m[33m│[39m␊
+ [33m│[39m [33m│[39m␊
+ [33m│[39m [33m│[39m␊
+ [33m│[39m [33m│[39m␊
+ [33m│[39m [33m│[39m␊
+ [33m╰──────────╯[39m␊
+ ␊
+ [37m╭─────────╮[39m␊
+ [37m│K │[39m␊
+ [37m│ WW│[39m␊
+ [37m│ {)│[39m␊
+ [37m│ ♠ %%│[39m␊
+ [37m│ %%%│[39m␊
+ [37m│ _%%%>│[39m␊
+ [37m│ K│[39m␊
+ [37m╰─────────╯[39m␊
+ `
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)ltQb=NuCfU5V~;)F#(btvT?<
zKCv|U2N9Yld1<1{52IuPUXr<*Bw$XA^=$2`x|aWw8onW__4H<<(ZPrYV|~&@Hl)VO
z3@XvRj7FUGiG56No&1Uj&Evd2>B%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).
[31m│[39m [31m🃅[39m [31m│[39m␊
[31m│[39m [31m│[39m␊
[31m╰──────────────────────────────────────────────────────────────────────────────────────────────────╯[39m`
+
+## render diamonds shows red color
+
+> Snapshot 1
+
+ '[31m🃃[39m'
+
+## render clubs shows white color
+
+> Snapshot 1
+
+ '[37m🃙[39m'
+
+## render joker with hearts suit
+
+> Snapshot 1
+
+ '[31m🂿[39m'
+
+## render joker with spades suit
+
+> Snapshot 1
+
+ '[37m🃏[39m'
+
+## render joker with clubs suit
+
+> Snapshot 1
+
+ '[37m🃟[39m'
+
+## render bordered with rounded false
+
+> Snapshot 1
+
+ `[37m┌──────────────────────────────────────────────────────────────────────────────────────────────────┐[39m␊
+ [37m│🂡[39m [37m│[39m␊
+ [37m└──────────────────────────────────────────────────────────────────────────────────────────────────┘[39m`
+
+## render face down without dimmed uses suit color
+
+> Snapshot 1
+
+ '[31m🂠[39m'
+
+## render 10 value card
+
+> Snapshot 1
+
+ '[31m🂺[39m'
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{+eRi^
z`Pi9x5mm0`R5|^XE;}8o5(kcSZcn{IXzeg1i#|>XtM~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'