Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/colorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function readableTextColor(hex: string): '#000' | '#fff' {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16) / 255;
const g = parseInt(h.slice(2, 4), 16) / 255;
const b = parseInt(h.slice(4, 6), 16) / 255;
const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
return L > 0.42 ? '#000' : '#fff';
}

export function withInkOpacity(ink: string, opacity: number): string {
return ink === '#000' ? `rgba(0,0,0,${opacity})` : `rgba(255,255,255,${opacity})`;
}
32 changes: 7 additions & 25 deletions src/components/Boards/ListBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,13 @@ import { shallowEqual } from 'react-redux';

import { selectGameById } from '../../../redux/GamesSlice';
import { useAppSelector } from '../../../redux/hooks';
import { readableTextColor, withInkOpacity } from '../../colorUtils';
import DialOverlay from '../Interactions/Dial/DialOverlay';
import { useMenuOpen } from '../MenuOpenContext';
import { bottomSheetHeight } from '../Sheets/GameSheet';

const ROW_BOARD_PADDING = 12;

// TODO: consolidate inkFor/inkA into a shared src/colorUtils.ts module and rename:
// inkFor → readableColor (use getContrastRatio from 'colorsheet', add data migration
// to backfill player colors, then introduce usePlayerColors hook)
// inkA → withOpacity
// Same change needed in DialOverlay.tsx and DialControl.tsx.
function inkFor(hex: string): string {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16) / 255;
const g = parseInt(h.slice(2, 4), 16) / 255;
const b = parseInt(h.slice(4, 6), 16) / 255;
const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
return L > 0.42 ? '#000' : '#fff';
}

function inkA(ink: string, a: number): string {
return ink === '#000' ? `rgba(0,0,0,${a})` : `rgba(255,255,255,${a})`;
}

interface PlayerRowProps {
playerId: string;
index: number;
Expand Down Expand Up @@ -87,13 +69,13 @@ const PlayerRow: React.FC<PlayerRowProps> = ({ playerId, index, svDimmed, disabl

if (!playerName) return null;

const ink = inkFor(color);
const ink = readableTextColor(color);

const separatorSign = currentRoundScore < 0 ? '−' : '+';
const roundAbs = Math.abs(currentRoundScore);

const secondaryNumberStyle = {
color: inkA(ink, 0.45),
color: withInkOpacity(ink, 0.45),
fontSize: 18,
fontWeight: '600' as const,
lineHeight: 22,
Expand All @@ -107,13 +89,13 @@ const PlayerRow: React.FC<PlayerRowProps> = ({ playerId, index, svDimmed, disabl
fontVariant: ['tabular-nums' as const]
};
const captionStyle = {
color: inkA(ink, 0.65),
color: withInkOpacity(ink, 0.65),
fontSize: 8,
fontWeight: '800' as const,
letterSpacing: 1.0,
marginTop: 1
};
const operatorStyle = { color: inkA(ink, 0.5), fontSize: 16, fontWeight: '500' as const };
const operatorStyle = { color: withInkOpacity(ink, 0.5), fontSize: 16, fontWeight: '500' as const };

return (
<Animated.View style={rowStyle} testID={`player-row-${index}`}>
Expand All @@ -137,8 +119,8 @@ const PlayerRow: React.FC<PlayerRowProps> = ({ playerId, index, svDimmed, disabl
/* Locked: hide PREV/RND, show winner pill in that slot */
isWinner && (
<View style={styles.winnerBadge}>
<Icon name="trophy" type="ionicon" color={inkA(ink, 0.75)} size={11} />
<Text style={[styles.winnerText, { color: inkA(ink, 0.75) }]}>WINNER</Text>
<Icon name="trophy" type="ionicon" color={withInkOpacity(ink, 0.75)} size={11} />
<Text style={[styles.winnerText, { color: withInkOpacity(ink, 0.75) }]}>WINNER</Text>
</View>
)
) : (
Expand Down
21 changes: 9 additions & 12 deletions src/components/Interactions/Dial/DialControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
} from 'react-native-reanimated';
import Svg, { Circle, Line, Path } from 'react-native-svg';

import { withInkOpacity } from '../../../colorUtils';

Check failure on line 20 in src/components/Interactions/Dial/DialControl.tsx

View workflow job for this annotation

GitHub Actions / Lint & Test

There should be no empty line within import group

// Extend TextInputProps to include Reanimated's animated text content prop
type AnimatedTextInputProps = TextInputProps & {
text?: string;
Expand All @@ -42,11 +44,6 @@
return Math.max(CENTER_VALUE_MIN_SCALE, Math.min(1, CENTER_VALUE_TARGET_CHARS / textLength));
}

// TODO: see ListBoard.tsx — consolidate inkFor/inkA into shared colorUtils module
function inkA(ink: string, a: number): string {
return ink === '#000' ? `rgba(0,0,0,${a})` : `rgba(255,255,255,${a})`;
}

function fmtSigned(v: number): string {
'worklet';
if (v > 0) return '+' + v;
Expand Down Expand Up @@ -383,7 +380,7 @@

// --- SVG geometry ---
const ringColor = isSecondary ? ACCENT : ink;
const trackColor = inkA(ink, 0.18);
const trackColor = withInkOpacity(ink, 0.18);

// Tick marks — memoized, only recomputed when dial size changes
const ticks = useMemo(() => Array.from({ length: 12 }, (_, i) => {
Expand All @@ -402,10 +399,10 @@
{!landscape && (
<Animated.View style={[
styles.pill,
{ backgroundColor: pillActive ? ACCENT : inkA(ink, 0.12) },
{ backgroundColor: pillActive ? ACCENT : withInkOpacity(ink, 0.12) },
pillStyle,
]}>
<View style={[styles.pillDot, { backgroundColor: pillActive ? '#fff' : inkA(ink, 0.4) }]} />
<View style={[styles.pillDot, { backgroundColor: pillActive ? '#fff' : withInkOpacity(ink, 0.4) }]} />
<Text style={[styles.pillText, { color: pillActive ? '#fff' : ink }]}>
STEP +{pillActive ? addendTwo : addendOne}
</Text>
Expand Down Expand Up @@ -475,7 +472,7 @@
underlineColorAndroid="transparent"
/>
</Animated.View>
<Text style={[styles.centerLabel, { color: inkA(ink, 0.62), fontSize: D * 0.049 }]}>
<Text style={[styles.centerLabel, { color: withInkOpacity(ink, 0.62), fontSize: D * 0.049 }]}>
THIS ROUND
</Text>
</View>
Expand Down Expand Up @@ -510,7 +507,7 @@
}}
style={({ pressed }) => [
styles.stepBtn,
{ backgroundColor: inkA(ink, pressed ? 0.26 : 0.15), width: D * 0.265, height: D * 0.224 },
{ backgroundColor: withInkOpacity(ink, pressed ? 0.26 : 0.15), width: D * 0.265, height: D * 0.224 },
]}
>
<Text style={[styles.stepBtnText, { color: ink, fontSize: D * 0.12 }]}>
Expand All @@ -528,7 +525,7 @@
caretHidden={true}
underlineColorAndroid="transparent"
/>
<Text style={[styles.newTotalLabel, { color: inkA(ink, 0.62), fontSize: D * 0.044 }]}>
<Text style={[styles.newTotalLabel, { color: withInkOpacity(ink, 0.62), fontSize: D * 0.044 }]}>
NEW TOTAL
</Text>
</View>
Expand All @@ -543,7 +540,7 @@
}}
style={({ pressed }) => [
styles.stepBtn,
{ backgroundColor: inkA(ink, pressed ? 0.26 : 0.15), width: D * 0.265, height: D * 0.224 },
{ backgroundColor: withInkOpacity(ink, pressed ? 0.26 : 0.15), width: D * 0.265, height: D * 0.224 },
]}
>
<Text style={[styles.stepBtnText, { color: ink, fontSize: D * 0.12 }]}>
Expand Down
36 changes: 11 additions & 25 deletions src/components/Interactions/Dial/DialOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useAppDispatch, useAppSelector } from '../../../../redux/hooks';
import { playerRoundScoreSet, selectPlayerById, selectPlayerRoundStats } from '../../../../redux/PlayersSlice';
import { selectCurrentGame } from '../../../../redux/selectors';
import { setLastUsedInteractionType } from '../../../../redux/SettingsSlice';
import { readableTextColor, withInkOpacity } from '../../../colorUtils';
import { useMenuOpen } from '../../MenuOpenContext';
import { InteractionType } from '../InteractionType';

Expand All @@ -41,21 +42,6 @@ function resistedDrag(t: number): number {
return t / (1 + t / 400); // nearly 1:1 at small pulls, strong resistance beyond ~150 px
}

// TODO: see ListBoard.tsx — consolidate inkFor/inkA into shared colorUtils module
function inkFor(hex: string): string {
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16) / 255;
const g = parseInt(h.slice(2, 4), 16) / 255;
const b = parseInt(h.slice(4, 6), 16) / 255;
const lin = (c: number) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
return L > 0.42 ? '#000' : '#fff';
}

function inkA(ink: string, a: number): string {
return ink === '#000' ? `rgba(0,0,0,${a})` : `rgba(255,255,255,${a})`;
}

function bounded(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
Expand Down Expand Up @@ -168,7 +154,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({

if (!player) return null;

const ink = inkFor(player.color ?? '#444');
const ink = readableTextColor(player.color ?? '#444');
const playerColor = player.color ?? '#444';
const isLandscape = pageWidth > pageHeight;

Expand All @@ -177,8 +163,8 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({

if (isLandscape) {
const lsDialSize = Math.min(Math.round((pageHeight - 52 * scale) / 1.25), Math.round(200 * scale));
const stepBg = isSecondary ? LS_ACCENT : inkA(ink, 0.12);
const stepDotBg = isSecondary ? '#fff' : inkA(ink, 0.4);
const stepBg = isSecondary ? LS_ACCENT : withInkOpacity(ink, 0.12);
const stepDotBg = isSecondary ? '#fff' : withInkOpacity(ink, 0.4);
const stepTextColor = isSecondary ? '#fff' : ink;

return (
Expand All @@ -191,7 +177,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
paddingHorizontal: 20 * scale,
paddingBottom: 6 * scale,
}]}>
<View style={[styles.dragHandle, { backgroundColor: inkA(ink, 0.3) }]} />
<View style={[styles.dragHandle, { backgroundColor: withInkOpacity(ink, 0.3) }]} />
<Text style={[styles.name, { color: ink, fontSize: 22 * scale, lineHeight: 26 * scale }]}
numberOfLines={1} adjustsFontSizeToFit minimumFontScale={0.6}>
{player.playerName}
Expand All @@ -209,7 +195,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
<View style={[styles.lsCol, { gap: 16 * scale }]}>
<View style={styles.prevBlock}>
<Text style={[styles.prevNumber, { color: ink, fontSize: 22 * scale, lineHeight: 26 * scale }]}>{previousTotal}</Text>
<Text style={[styles.prevLabel, { color: inkA(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>PREVIOUS TOTAL</Text>
<Text style={[styles.prevLabel, { color: withInkOpacity(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>PREVIOUS TOTAL</Text>
</View>
<View style={[styles.lsPill, {
backgroundColor: stepBg,
Expand Down Expand Up @@ -246,14 +232,14 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
<View style={[styles.lsCol, { gap: 16 * scale }]}>
<View style={styles.prevBlock}>
<Text style={[styles.prevNumber, { color: ink, fontSize: 22 * scale, lineHeight: 26 * scale }]}>{currentRoundTotalScore}</Text>
<Text style={[styles.prevLabel, { color: inkA(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>NEW TOTAL</Text>
<Text style={[styles.prevLabel, { color: withInkOpacity(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>NEW TOTAL</Text>
</View>
<Pressable
onPress={onDone}
style={({ pressed }) => [
styles.lsDoneBtn,
{
backgroundColor: inkA(ink, pressed ? 0.28 : 0.16),
backgroundColor: withInkOpacity(ink, pressed ? 0.28 : 0.16),
height: 44 * scale,
borderRadius: 14 * scale,
},
Expand All @@ -280,7 +266,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
{/* Top group: drag handle + name + prev total — also the swipe-to-dismiss zone */}
<GestureDetector gesture={dismissGesture}>
<View style={[styles.topGroup, { gap: 8 * scale }]}>
<View style={[styles.dragHandle, { backgroundColor: inkA(ink, 0.3) }]} />
<View style={[styles.dragHandle, { backgroundColor: withInkOpacity(ink, 0.3) }]} />
<Text
style={[styles.name, { color: ink, fontSize: 32 * scale, lineHeight: 36 * scale }]}
numberOfLines={1}
Expand All @@ -291,7 +277,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
</Text>
<View style={styles.prevBlock}>
<Text style={[styles.prevNumber, { color: ink, fontSize: 22 * scale, lineHeight: 26 * scale }]}>{previousTotal}</Text>
<Text style={[styles.prevLabel, { color: inkA(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>PREVIOUS TOTAL</Text>
<Text style={[styles.prevLabel, { color: withInkOpacity(ink, 0.6), fontSize: 10 * scale, lineHeight: 12 * scale }]}>PREVIOUS TOTAL</Text>
</View>
</View>
</GestureDetector>
Expand Down Expand Up @@ -319,7 +305,7 @@ const PlayerDialPage: React.FC<PlayerDialPageProps> = ({
style={({ pressed }) => [
styles.doneBtn,
{
backgroundColor: inkA(ink, pressed ? 0.28 : 0.16),
backgroundColor: withInkOpacity(ink, pressed ? 0.28 : 0.16),
height: 48 * scale,
borderRadius: 14 * scale,
},
Expand Down
Loading