From ecec5bf2c7d2023cbf963a77d2aa4aaae0f33fb7 Mon Sep 17 00:00:00 2001 From: Ashton Anderson Date: Wed, 25 Feb 2026 15:20:21 -0500 Subject: [PATCH] feat: polish mobile analysis board chrome --- src/components/Analysis/BoardChrome.tsx | 115 ++++++++++++++++----- src/components/Analysis/Highlight.tsx | 130 ++++++++++++++++++------ src/components/Board/MovesContainer.tsx | 5 +- src/components/Common/GameInfo.tsx | 90 +++++++++++----- src/pages/analysis/[...id].tsx | 108 +++++++++++++++----- src/styles/tailwind.css | 13 ++- 6 files changed, 352 insertions(+), 109 deletions(-) diff --git a/src/components/Analysis/BoardChrome.tsx b/src/components/Analysis/BoardChrome.tsx index 8a72e862..382a61f0 100644 --- a/src/components/Analysis/BoardChrome.tsx +++ b/src/components/Analysis/BoardChrome.tsx @@ -40,6 +40,7 @@ interface AnalysisMaiaWinrateBarProps { displayText: string labelPositionTop: MotionValue disabled?: boolean + variant?: StockfishEvalBarVariant className?: string bubbleMinWidthPx?: number desktopSize?: 'compact' | 'expanded' @@ -159,7 +160,7 @@ export const AnalysisStockfishEvalBar: React.FC< ? isExpandedDesktop ? 'h-6 min-w-[42px] rounded-full px-2 text-[11px]' : 'h-5 min-w-[36px] rounded-full px-1.5 text-[10px]' - : 'h-4 min-w-[30px] rounded-full px-1 text-[8px]' + : 'h-[18px] w-[36px] rounded-full px-1 text-[9px]' return (
= ({ displayText, labelPositionTop, disabled = false, + variant = 'desktop', className, bubbleMinWidthPx, desktopSize = 'compact', }) => { + const isDesktop = variant === 'desktop' const isExpandedDesktop = desktopSize === 'expanded' + const widthClass = isDesktop + ? isExpandedDesktop + ? 'w-[18px]' + : 'w-[16px]' + : 'w-4' + const tickTextClass = isDesktop + ? isExpandedDesktop + ? 'text-[9px]' + : 'text-[8px]' + : 'text-[8px]' + const bubbleClass = isDesktop + ? isExpandedDesktop + ? 'h-6 min-w-[42px] px-2 text-[11px]' + : 'h-5 min-w-[36px] px-1.5 text-[10px]' + : 'h-[18px] w-[36px] rounded-full px-1 text-[9px]' return (
-
+
= ({ ), )}
100
0
@@ -296,11 +309,7 @@ export const AnalysisMaiaWinrateBar: React.FC = ({ ) : null}
= ({ type SegmentConfig = { key: 'blunder' | 'ok' | 'good' label: string + mobileLabel?: string probability: number topMoves: { move: string; probability: number; label: string }[] badge: string @@ -376,6 +386,7 @@ export const AnalysisCompactBlunderMeter: React.FC< { key: 'blunder', label: 'Blunders', + mobileLabel: 'Blunders', probability: data.blunderMoves.probability, topMoves: getTopCategoryMoves(data.blunderMoves.moves), badge: '??', @@ -387,6 +398,7 @@ export const AnalysisCompactBlunderMeter: React.FC< { key: 'ok', label: 'Mistakes', + mobileLabel: 'Mistakes', probability: data.okMoves.probability, topMoves: getTopCategoryMoves(data.okMoves.moves), badge: '?', @@ -398,6 +410,7 @@ export const AnalysisCompactBlunderMeter: React.FC< { key: 'good', label: 'Best Moves', + mobileLabel: 'Best', probability: data.goodMoves.probability, topMoves: getTopCategoryMoves(data.goodMoves.moves), badge: '✓', @@ -415,13 +428,13 @@ export const AnalysisCompactBlunderMeter: React.FC< ? 'h-5 min-w-5 text-[10px]' : 'h-3.5 min-w-3.5 text-[8px]' const percentTextClass = isDesktop ? 'text-[11px]' : 'text-[9px]' - const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[9px] py-0.5' + const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[10px] py-1.5' const playedMoveOutlineOuterInsetClass = isDesktop ? '-inset-x-[11px] -inset-y-[5px]' - : '-inset-x-[7px] -inset-y-[4px]' + : '-inset-x-[6px] -inset-y-[2px]' const playedMoveOutlineInnerInsetClass = isDesktop ? '-inset-x-[8px] -inset-y-[3px]' - : '-inset-x-[5px] -inset-y-[3px]' + : '-inset-x-[4px] -inset-y-[1px]' const renderTopMoveButton = ( segmentKey: string, topMove: { move: string; probability: number; label: string }, @@ -471,6 +484,26 @@ export const AnalysisCompactBlunderMeter: React.FC< ) } + const renderMobileMoveSequence = ( + segmentKey: string, + moves: { move: string; probability: number; label: string }[], + startIndex = 0, + ) => + moves.map((topMove, index) => { + const absoluteIndex = startIndex + index + return ( + + {absoluteIndex > 0 ? : null} + {renderTopMoveButton(segmentKey, topMove)} + {absoluteIndex < startIndex + moves.length - 1 ? ( + , + ) : null} + + ) + }) return (
) : (
{segments.map((segment) => (
- {segment.label}:{' '} - {segment.topMoves.length - ? segment.topMoves.map((topMove, index) => ( - - {index > 0 ? , : null} - {renderTopMoveButton(segment.key, topMove)} + + {segment.mobileLabel ?? segment.label}: + + {segment.topMoves.length ? ( + segment.topMoves.length >= 3 ? ( + + + {renderMobileMoveSequence( + segment.key, + segment.topMoves.slice(0, 2), + 0, + )} + + + {renderMobileMoveSequence( + segment.key, + segment.topMoves.slice(2), + 2, + )} - )) - : '-'} + + ) : ( + + {renderMobileMoveSequence( + segment.key, + segment.topMoves, + 0, + )} + + ) + ) : ( + '-' + )}
))}
diff --git a/src/components/Analysis/Highlight.tsx b/src/components/Analysis/Highlight.tsx index 98dbafb9..3b2c6b4e 100644 --- a/src/components/Analysis/Highlight.tsx +++ b/src/components/Analysis/Highlight.tsx @@ -301,6 +301,7 @@ export const Highlight: React.FC = ({ // Track whether description exists (not its content) const hasDescriptionRef = useRef(boardDescription?.segments?.length > 0) const [animationKey, setAnimationKey] = useState(0) + const maiaHeaderSelectRef = useRef(null) // Calculate if we're in the first 10 ply const isInFirst10Ply = currentNode @@ -353,6 +354,27 @@ export const Highlight: React.FC = ({ } }, [boardDescription?.segments?.length]) + const useCompactMobileColumnTitles = isMobile && !simplified + const mobileMaiaColumnTitle = `Maia ${currentMaiaModel.slice(-4)}: Human Moves` + const mobileStockfishColumnTitle = 'SF 17: Engine Moves' + const openMaiaHeaderPicker = () => { + const select = maiaHeaderSelectRef.current as + | (HTMLSelectElement & { showPicker?: () => void }) + | null + if (!select) return + + select.focus() + if (select.showPicker) { + try { + select.showPicker() + return + } catch { + // Fall back to click if showPicker is unavailable or rejected. + } + } + select.click() + } + return (
= ({
{isHomePage ? (
- Maia {currentMaiaModel.slice(-4)} + {useCompactMobileColumnTitles + ? mobileMaiaColumnTitle + : `Maia ${currentMaiaModel.slice(-4)}`}
) : ( <> - setCurrentMaiaModel(e.target.value)} + className="cursor-pointer appearance-none bg-transparent text-center outline-none transition-colors duration-200 hover:text-human-1/80" > - Maia {model.slice(-4)} - - ))} - - - keyboard_arrow_down - + {MAIA_MODELS.map((model) => ( + + ))} + + + : Human Moves + +
+ ) : ( + + )} + )}
@@ -407,11 +473,13 @@ export const Highlight: React.FC = ({
-

- Human Moves -

+ {!useCompactMobileColumnTitles && ( +

+ Human Moves +

+ )}

= ({

- Stockfish 17 + {useCompactMobileColumnTitles + ? mobileStockfishColumnTitle + : 'Stockfish 17'}

@@ -477,11 +547,13 @@ export const Highlight: React.FC = ({
-

- Engine Moves -

+ {!useCompactMobileColumnTitles && ( +

+ Engine Moves +

+ )}

+

{mobileMovePairs.map((pair, pairIndex) => ( diff --git a/src/components/Common/GameInfo.tsx b/src/components/Common/GameInfo.tsx index a7d2f799..b0d67560 100644 --- a/src/components/Common/GameInfo.tsx +++ b/src/components/Common/GameInfo.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { useTour } from 'src/contexts' import { InstructionsType } from 'src/types' import { tourConfigs } from 'src/constants/tours' @@ -18,6 +19,7 @@ interface Props { error: string | null gameEnded: boolean } + mobileActions?: React.ReactNode embedded?: boolean } @@ -32,9 +34,28 @@ export const GameInfo: React.FC = ({ showGameListButton, onGameListClick, streamState, + mobileActions, embedded = false, }: Props) => { const { startTour } = useTour() + const maiaSelectRef = useRef(null) + const openMaiaModelPicker = () => { + const select = maiaSelectRef.current as + | (HTMLSelectElement & { showPicker?: () => void }) + | null + if (!select) return + + select.focus() + if (select.showPicker) { + try { + select.showPicker() + return + } catch { + // Fallback for browsers/event timing that reject showPicker. + } + } + select.click() + } return (
= ({ >
- + {icon} -

{title}

+
+

+ {title} +

+ {currentMaiaModel && setCurrentMaiaModel && ( +
+ using +
+ + +
+
+ )} +
{streamState && (
= ({
)} - {currentMaiaModel && setCurrentMaiaModel && ( -
- using -
- - - arrow_drop_down - -
-
- )}
+ {mobileActions} {showGameListButton && ( + } >
-
-
-
- -
+
+
+ + Maia % + +
+ +
+ + SF Eval + +
+
+
+
+
= ({ /> ) : null}
-
-
- -
+
+
= ({ } } currentNode={controller.currentNode} + hideWhiteWinRateSummary={true} + hideStockfishEvalSummary={true} /> {(!analysisEnabled || controller.learnFromMistakes.state.isActive) && ( @@ -2073,14 +2127,14 @@ const Analysis: React.FC = ({ {analyzedGame - ? `Analyze: ${analyzedGame.whitePlayer.name} vs ${analyzedGame.blackPlayer.name} – Maia Chess` + ? `Analyze: ${formatAnalysisPlayerName(analyzedGame.whitePlayer.name)} vs ${formatAnalysisPlayerName(analyzedGame.blackPlayer.name)} – Maia Chess` : 'Analyze – Maia Chess'} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 1096fc01..6e101881 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -161,13 +161,24 @@ svg { scrollbar-width: none; } +.red-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgb(var(--color-human-accent3)) rgb(var(--color-backdrop) / 0.22); +} + .red-scrollbar::-webkit-scrollbar { width: 5px; + height: 5px; +} + +.red-scrollbar::-webkit-scrollbar-track { + background-color: rgb(var(--color-backdrop) / 0.18); + border-radius: 9999px; } .red-scrollbar::-webkit-scrollbar-thumb { background-color: rgb(var(--color-human-accent3)); - border-radius: 1px; + border-radius: 9999px; } .red-scrollbar::-webkit-scrollbar-thumb:hover {