Skip to content

Commit ecec5bf

Browse files
feat: polish mobile analysis board chrome
1 parent 74b879f commit ecec5bf

6 files changed

Lines changed: 352 additions & 109 deletions

File tree

src/components/Analysis/BoardChrome.tsx

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface AnalysisMaiaWinrateBarProps {
4040
displayText: string
4141
labelPositionTop: MotionValue<string>
4242
disabled?: boolean
43+
variant?: StockfishEvalBarVariant
4344
className?: string
4445
bubbleMinWidthPx?: number
4546
desktopSize?: 'compact' | 'expanded'
@@ -159,7 +160,7 @@ export const AnalysisStockfishEvalBar: React.FC<
159160
? isExpandedDesktop
160161
? 'h-6 min-w-[42px] rounded-full px-2 text-[11px]'
161162
: 'h-5 min-w-[36px] rounded-full px-1.5 text-[10px]'
162-
: 'h-4 min-w-[30px] rounded-full px-1 text-[8px]'
163+
: 'h-[18px] w-[36px] rounded-full px-1 text-[9px]'
163164

164165
return (
165166
<div
@@ -234,23 +235,35 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
234235
displayText,
235236
labelPositionTop,
236237
disabled = false,
238+
variant = 'desktop',
237239
className,
238240
bubbleMinWidthPx,
239241
desktopSize = 'compact',
240242
}) => {
243+
const isDesktop = variant === 'desktop'
241244
const isExpandedDesktop = desktopSize === 'expanded'
245+
const widthClass = isDesktop
246+
? isExpandedDesktop
247+
? 'w-[18px]'
248+
: 'w-[16px]'
249+
: 'w-4'
250+
const tickTextClass = isDesktop
251+
? isExpandedDesktop
252+
? 'text-[9px]'
253+
: 'text-[8px]'
254+
: 'text-[8px]'
255+
const bubbleClass = isDesktop
256+
? isExpandedDesktop
257+
? 'h-6 min-w-[42px] px-2 text-[11px]'
258+
: 'h-5 min-w-[36px] px-1.5 text-[10px]'
259+
: 'h-[18px] w-[36px] rounded-full px-1 text-[9px]'
242260
return (
243261
<div
244-
className={[
245-
`relative h-full ${isExpandedDesktop ? 'w-[18px]' : 'w-[16px]'}`,
246-
className,
247-
]
262+
className={[`relative h-full ${widthClass}`, className]
248263
.filter(Boolean)
249264
.join(' ')}
250265
>
251-
<div
252-
className={`relative h-full ${isExpandedDesktop ? 'w-[18px]' : 'w-[16px]'}`}
253-
>
266+
<div className={`relative h-full ${widthClass}`}>
254267
<div
255268
className="absolute inset-0 overflow-hidden rounded-[5px] border border-glass-border bg-glass-strong shadow-[0_0_0_1px_rgb(var(--color-backdrop)/0.35)]"
256269
style={{
@@ -282,12 +295,12 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
282295
),
283296
)}
284297
<div
285-
className={`absolute left-1/2 top-0 -translate-x-1/2 font-bold leading-none text-black/90 [text-shadow:0_1px_1px_rgb(255_255_255_/_0.5)] ${isExpandedDesktop ? 'text-[9px]' : 'text-[8px]'}`}
298+
className={`absolute left-1/2 top-0 -translate-x-1/2 font-bold leading-none text-black/90 [text-shadow:0_1px_1px_rgb(255_255_255_/_0.5)] ${tickTextClass}`}
286299
>
287300
100
288301
</div>
289302
<div
290-
className={`absolute bottom-0 left-1/2 -translate-x-1/2 font-bold leading-none text-white/95 [text-shadow:0_1px_1px_rgb(0_0_0_/_0.55)] ${isExpandedDesktop ? 'text-[9px]' : 'text-[8px]'}`}
303+
className={`absolute bottom-0 left-1/2 -translate-x-1/2 font-bold leading-none text-white/95 [text-shadow:0_1px_1px_rgb(0_0_0_/_0.55)] ${tickTextClass}`}
291304
>
292305
0
293306
</div>
@@ -296,11 +309,7 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
296309
) : null}
297310
</div>
298311
<motion.div
299-
className={`absolute left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-black/45 bg-white font-bold leading-none text-black/85 ${
300-
isExpandedDesktop
301-
? 'h-6 min-w-[42px] px-2 text-[11px]'
302-
: 'h-5 min-w-[36px] px-1.5 text-[10px]'
303-
}`}
312+
className={`absolute left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-black/45 bg-white font-bold leading-none text-black/85 ${bubbleClass}`}
304313
style={{
305314
top: labelPositionTop,
306315
boxShadow: '0 0 0 2px rgb(255 255 255 / 0.32)',
@@ -318,6 +327,7 @@ export const AnalysisMaiaWinrateBar: React.FC<AnalysisMaiaWinrateBarProps> = ({
318327
type SegmentConfig = {
319328
key: 'blunder' | 'ok' | 'good'
320329
label: string
330+
mobileLabel?: string
321331
probability: number
322332
topMoves: { move: string; probability: number; label: string }[]
323333
badge: string
@@ -376,6 +386,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
376386
{
377387
key: 'blunder',
378388
label: 'Blunders',
389+
mobileLabel: 'Blunders',
379390
probability: data.blunderMoves.probability,
380391
topMoves: getTopCategoryMoves(data.blunderMoves.moves),
381392
badge: '??',
@@ -387,6 +398,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
387398
{
388399
key: 'ok',
389400
label: 'Mistakes',
401+
mobileLabel: 'Mistakes',
390402
probability: data.okMoves.probability,
391403
topMoves: getTopCategoryMoves(data.okMoves.moves),
392404
badge: '?',
@@ -398,6 +410,7 @@ export const AnalysisCompactBlunderMeter: React.FC<
398410
{
399411
key: 'good',
400412
label: 'Best Moves',
413+
mobileLabel: 'Best',
401414
probability: data.goodMoves.probability,
402415
topMoves: getTopCategoryMoves(data.goodMoves.moves),
403416
badge: '✓',
@@ -415,13 +428,13 @@ export const AnalysisCompactBlunderMeter: React.FC<
415428
? 'h-5 min-w-5 text-[10px]'
416429
: 'h-3.5 min-w-3.5 text-[8px]'
417430
const percentTextClass = isDesktop ? 'text-[11px]' : 'text-[9px]'
418-
const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[9px] py-0.5'
431+
const metaTextClass = isDesktop ? 'text-xs py-1.5' : 'text-[10px] py-1.5'
419432
const playedMoveOutlineOuterInsetClass = isDesktop
420433
? '-inset-x-[11px] -inset-y-[5px]'
421-
: '-inset-x-[7px] -inset-y-[4px]'
434+
: '-inset-x-[6px] -inset-y-[2px]'
422435
const playedMoveOutlineInnerInsetClass = isDesktop
423436
? '-inset-x-[8px] -inset-y-[3px]'
424-
: '-inset-x-[5px] -inset-y-[3px]'
437+
: '-inset-x-[4px] -inset-y-[1px]'
425438
const renderTopMoveButton = (
426439
segmentKey: string,
427440
topMove: { move: string; probability: number; label: string },
@@ -471,6 +484,26 @@ export const AnalysisCompactBlunderMeter: React.FC<
471484
</button>
472485
)
473486
}
487+
const renderMobileMoveSequence = (
488+
segmentKey: string,
489+
moves: { move: string; probability: number; label: string }[],
490+
startIndex = 0,
491+
) =>
492+
moves.map((topMove, index) => {
493+
const absoluteIndex = startIndex + index
494+
return (
495+
<span
496+
key={`${segmentKey}-${topMove.move}-${absoluteIndex}`}
497+
className="inline-flex min-w-0 items-baseline"
498+
>
499+
{absoluteIndex > 0 ? <span className="mr-1"> </span> : null}
500+
{renderTopMoveButton(segmentKey, topMove)}
501+
{absoluteIndex < startIndex + moves.length - 1 ? (
502+
<span className="mr-1">,</span>
503+
) : null}
504+
</span>
505+
)
506+
})
474507

475508
return (
476509
<div
@@ -546,22 +579,48 @@ export const AnalysisCompactBlunderMeter: React.FC<
546579
</div>
547580
) : (
548581
<div
549-
className={`flex items-center gap-2.5 whitespace-nowrap font-semibold leading-tight tracking-[0.01em] ${metaTextClass}`}
582+
className={`flex min-h-[38px] items-start gap-2.5 pt-1 font-semibold leading-snug tracking-[0.01em] ${metaTextClass}`}
550583
>
551584
{segments.map((segment) => (
552585
<div
553586
key={`maia-top-moves-${segment.key}`}
554-
className={`min-w-0 flex-1 truncate ${segment.moveClass}`}
587+
className={`flex min-w-0 flex-1 items-start ${segment.moveClass} ${
588+
segment.key === 'good' ? '-ml-5' : ''
589+
}`}
555590
>
556-
{segment.label}:{' '}
557-
{segment.topMoves.length
558-
? segment.topMoves.map((topMove, index) => (
559-
<span key={`${segment.key}-${topMove.move}`}>
560-
{index > 0 ? <span>, </span> : null}
561-
{renderTopMoveButton(segment.key, topMove)}
591+
<span className="mr-2 shrink-0 whitespace-nowrap">
592+
{segment.mobileLabel ?? segment.label}:
593+
</span>
594+
{segment.topMoves.length ? (
595+
segment.topMoves.length >= 3 ? (
596+
<span className="flex min-w-0 flex-col gap-0.5 leading-tight">
597+
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
598+
{renderMobileMoveSequence(
599+
segment.key,
600+
segment.topMoves.slice(0, 2),
601+
0,
602+
)}
603+
</span>
604+
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
605+
{renderMobileMoveSequence(
606+
segment.key,
607+
segment.topMoves.slice(2),
608+
2,
609+
)}
562610
</span>
563-
))
564-
: '-'}
611+
</span>
612+
) : (
613+
<span className="-mx-[6px] inline-flex min-w-0 items-baseline whitespace-nowrap px-[6px]">
614+
{renderMobileMoveSequence(
615+
segment.key,
616+
segment.topMoves,
617+
0,
618+
)}
619+
</span>
620+
)
621+
) : (
622+
'-'
623+
)}
565624
</div>
566625
))}
567626
</div>

src/components/Analysis/Highlight.tsx

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ export const Highlight: React.FC<Props> = ({
301301
// Track whether description exists (not its content)
302302
const hasDescriptionRef = useRef(boardDescription?.segments?.length > 0)
303303
const [animationKey, setAnimationKey] = useState(0)
304+
const maiaHeaderSelectRef = useRef<HTMLSelectElement | null>(null)
304305

305306
// Calculate if we're in the first 10 ply
306307
const isInFirst10Ply = currentNode
@@ -353,6 +354,27 @@ export const Highlight: React.FC<Props> = ({
353354
}
354355
}, [boardDescription?.segments?.length])
355356

357+
const useCompactMobileColumnTitles = isMobile && !simplified
358+
const mobileMaiaColumnTitle = `Maia ${currentMaiaModel.slice(-4)}: Human Moves`
359+
const mobileStockfishColumnTitle = 'SF 17: Engine Moves'
360+
const openMaiaHeaderPicker = () => {
361+
const select = maiaHeaderSelectRef.current as
362+
| (HTMLSelectElement & { showPicker?: () => void })
363+
| null
364+
if (!select) return
365+
366+
select.focus()
367+
if (select.showPicker) {
368+
try {
369+
select.showPicker()
370+
return
371+
} catch {
372+
// Fall back to click if showPicker is unavailable or rejected.
373+
}
374+
}
375+
select.click()
376+
}
377+
356378
return (
357379
<div
358380
id="analysis-highlight"
@@ -367,28 +389,72 @@ export const Highlight: React.FC<Props> = ({
367389
<div className="relative flex w-full flex-col border-b border-white/5">
368390
{isHomePage ? (
369391
<div className="py-2 text-center text-sm font-semibold text-human-1 md:text-xxs lg:text-xs">
370-
Maia {currentMaiaModel.slice(-4)}
392+
{useCompactMobileColumnTitles
393+
? mobileMaiaColumnTitle
394+
: `Maia ${currentMaiaModel.slice(-4)}`}
371395
</div>
372396
) : (
373397
<>
374-
<select
375-
value={currentMaiaModel}
376-
onChange={(e) => setCurrentMaiaModel(e.target.value)}
377-
className="cursor-pointer appearance-none bg-transparent py-2 text-center text-sm font-semibold text-human-1 outline-none transition-colors duration-200 hover:text-human-1/80 md:text-xxs lg:text-xs"
378-
>
379-
{MAIA_MODELS.map((model) => (
380-
<option
381-
value={model}
382-
key={model}
383-
className="bg-transparent text-human-1"
398+
{useCompactMobileColumnTitles ? (
399+
<div className="flex items-center justify-center py-2 pr-4 text-sm font-semibold text-human-1">
400+
<select
401+
ref={maiaHeaderSelectRef}
402+
value={currentMaiaModel}
403+
onChange={(e) => setCurrentMaiaModel(e.target.value)}
404+
className="cursor-pointer appearance-none bg-transparent text-center outline-none transition-colors duration-200 hover:text-human-1/80"
384405
>
385-
Maia {model.slice(-4)}
386-
</option>
387-
))}
388-
</select>
389-
<span className="material-symbols-outlined pointer-events-none absolute right-1 top-1/2 -translate-y-1/2 text-sm text-human-1/60">
390-
keyboard_arrow_down
391-
</span>
406+
{MAIA_MODELS.map((model) => (
407+
<option
408+
value={model}
409+
key={model}
410+
className="bg-transparent text-human-1"
411+
>
412+
{`Maia ${model.slice(-4)}`}
413+
</option>
414+
))}
415+
</select>
416+
<span className="ml-0.5 whitespace-nowrap">
417+
: Human Moves
418+
</span>
419+
</div>
420+
) : (
421+
<select
422+
ref={maiaHeaderSelectRef}
423+
value={currentMaiaModel}
424+
onChange={(e) => setCurrentMaiaModel(e.target.value)}
425+
className="cursor-pointer appearance-none bg-transparent py-2 text-center text-sm font-semibold text-human-1 outline-none transition-colors duration-200 hover:text-human-1/80 md:text-xxs lg:text-xs"
426+
>
427+
{MAIA_MODELS.map((model) => (
428+
<option
429+
value={model}
430+
key={model}
431+
className="bg-transparent text-human-1"
432+
>
433+
{`Maia ${model.slice(-4)}`}
434+
</option>
435+
))}
436+
</select>
437+
)}
438+
<button
439+
type="button"
440+
className="material-symbols-outlined absolute right-0.5 top-1/2 inline-flex h-5 w-5 -translate-y-1/2 items-center justify-center text-base leading-none text-human-1/65"
441+
onMouseDown={(e) => {
442+
e.preventDefault()
443+
openMaiaHeaderPicker()
444+
}}
445+
onClick={(e) => {
446+
e.preventDefault()
447+
}}
448+
onKeyDown={(e) => {
449+
if (e.key === 'Enter' || e.key === ' ') {
450+
e.preventDefault()
451+
openMaiaHeaderPicker()
452+
}
453+
}}
454+
aria-label="Change Maia model"
455+
>
456+
expand_more
457+
</button>
392458
</>
393459
)}
394460
</div>
@@ -407,11 +473,13 @@ export const Highlight: React.FC<Props> = ({
407473
<div
408474
className={`flex w-full flex-col items-start justify-center md:items-center ${simplified ? 'p-3' : 'px-2 py-1.5 xl:py-2'}`}
409475
>
410-
<p
411-
className={`mb-1 whitespace-nowrap text-sm font-semibold text-human-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
412-
>
413-
Human Moves
414-
</p>
476+
{!useCompactMobileColumnTitles && (
477+
<p
478+
className={`mb-1 whitespace-nowrap text-sm font-semibold text-human-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
479+
>
480+
Human Moves
481+
</p>
482+
)}
415483
<div className="flex w-full cursor-pointer items-center justify-between">
416484
<p
417485
className={`text-left font-mono ${simplified ? 'text-xs' : 'text-sm md:text-xxs'} text-secondary/50`}
@@ -454,7 +522,9 @@ export const Highlight: React.FC<Props> = ({
454522
<div className="flex flex-col items-center justify-start gap-0.5 xl:gap-1">
455523
<div className="flex w-full flex-col border-b border-white/5 py-2">
456524
<p className="whitespace-nowrap text-center text-sm font-semibold text-engine-1 md:text-xxs lg:text-xs">
457-
Stockfish 17
525+
{useCompactMobileColumnTitles
526+
? mobileStockfishColumnTitle
527+
: 'Stockfish 17'}
458528
</p>
459529
</div>
460530

@@ -477,11 +547,13 @@ export const Highlight: React.FC<Props> = ({
477547
<div
478548
className={`flex w-full flex-col items-start justify-center ${simplified ? 'p-3' : 'px-2 py-1.5 xl:py-2'} md:items-center`}
479549
>
480-
<p
481-
className={`mb-1 whitespace-nowrap text-sm font-semibold text-engine-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
482-
>
483-
Engine Moves
484-
</p>
550+
{!useCompactMobileColumnTitles && (
551+
<p
552+
className={`mb-1 whitespace-nowrap text-sm font-semibold text-engine-2 ${simplified ? 'text-sm' : 'md:text-xxs lg:text-xs'}`}
553+
>
554+
Engine Moves
555+
</p>
556+
)}
485557
<div className="flex w-full cursor-pointer items-center justify-between">
486558
<p
487559
className={`text-left font-mono text-secondary/50 ${simplified ? 'text-xs' : 'text-sm md:text-xxs'}`}

0 commit comments

Comments
 (0)