👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

avoid tailwind jit issues, jfc

+89 -53
+17
CLAUDE.md
··· 116 116 ``` 117 117 (Overlay still appears on hover, just instantly with reduced motion) 118 118 119 + **Tailwind JIT and Dynamic Classes:** 120 + - Tailwind's JIT compiler scans source code at build time to find class names 121 + - **NEVER use template literals with variables for Tailwind arbitrary values:** 122 + ```tsx 123 + // BAD - Tailwind can't see this at build time, CSS won't be generated 124 + className={`aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}]`} 125 + 126 + // GOOD - Use inline styles for dynamic values 127 + style={{ aspectRatio: CARD_ASPECT_RATIO, borderRadius: CARD_BORDER_RADIUS }} 128 + 129 + // GOOD - Hardcoded arbitrary values work fine (with comment explaining magic numbers) 130 + className="aspect-[672/936] rounded-[4.75%/3.5%]" 131 + ``` 132 + - This only affects arbitrary value syntax like `aspect-[...]`, `max-w-[...]`, `rounded-[...]` 133 + - Regular class composition with variables is fine: `className={`p-4 ${isActive ? 'bg-blue-500' : ''}`}` 134 + - Card aspect ratio and border radius use `CARD_STYLES` from `@/components/CardImage` for inline styles 135 + 119 136 ### Development Tooling 120 137 121 138 - **Nix**: flake.nix provides Node.js 22, TypeSpec, and language servers
+47 -14
src/components/CardImage.tsx
··· 26 26 export const CARD_ASPECT_RATIO = "672/936"; 27 27 28 28 /** Card border radius matching physical card corners */ 29 - export const CARD_BORDER_RADIUS = "4.75%/3.5%"; 29 + export const CARD_BORDER_RADIUS = "4.75% / 3.5%"; 30 + 31 + /** 32 + * Inline styles for card dimensions. 33 + * Using inline styles instead of Tailwind arbitrary values like `aspect-[${VAR}]` 34 + * because Tailwind's JIT compiler can't see dynamic template literals at build time. 35 + */ 36 + export const CARD_STYLES = { 37 + aspectRatio: CARD_ASPECT_RATIO, 38 + borderRadius: CARD_BORDER_RADIUS, 39 + } as const; 30 40 31 41 interface CardImageProps { 32 42 card: Pick<Card, "name" | "id"> & { layout?: Layout }; ··· 90 100 91 101 // Base styles always applied to images 92 102 // text-transparent hides browser's ugly alt text rendering 93 - const imgBaseClassName = `rounded-[${CARD_BORDER_RADIUS}] aspect-[${CARD_ASPECT_RATIO}] max-w-full text-transparent ${imgClassName ?? ""}`; 103 + const imgBaseClassName = `max-w-full text-transparent ${imgClassName ?? ""}`; 94 104 95 105 if (!flippable) { 96 106 // Non-flippable: no wrapper, both outer and img classes go on the img ··· 99 109 src={getImageUri(card.id, size, face)} 100 110 alt={card.name} 101 111 className={`${outerClassName ?? ""} ${imgBaseClassName} bg-gray-200 dark:bg-zinc-700`} 102 - style={{ backgroundImage: PLACEHOLDER_STRIPES }} 112 + style={{ ...CARD_STYLES, backgroundImage: PLACEHOLDER_STRIPES }} 103 113 loading="lazy" 104 114 /> 105 115 ); ··· 110 120 111 121 return ( 112 122 <div 113 - className={`relative group aspect-[${CARD_ASPECT_RATIO}] ${outerClassName ?? ""}`} 123 + className={`relative group ${outerClassName ?? ""}`} 124 + style={{ aspectRatio: CARD_ASPECT_RATIO }} 114 125 > 115 126 {flipBehavior === "transform" && hasBack ? ( 116 127 <div ··· 126 137 className={`w-full ${imgBaseClassName} bg-gray-200 dark:bg-zinc-700`} 127 138 loading="lazy" 128 139 style={{ 140 + ...CARD_STYLES, 129 141 backfaceVisibility: "hidden", 130 142 backgroundImage: PLACEHOLDER_STRIPES, 131 143 }} ··· 136 148 className={`w-full ${imgBaseClassName} bg-gray-200 dark:bg-zinc-700 absolute inset-0`} 137 149 loading="lazy" 138 150 style={{ 151 + ...CARD_STYLES, 139 152 backfaceVisibility: "hidden", 140 153 transform: "rotateY(180deg)", 141 154 backgroundImage: PLACEHOLDER_STRIPES, ··· 149 162 className={`w-full ${imgBaseClassName} bg-gray-200 dark:bg-zinc-700 motion-safe:transition-transform motion-safe:duration-500 motion-safe:ease-in-out`} 150 163 loading="lazy" 151 164 style={{ 165 + ...CARD_STYLES, 152 166 backgroundImage: PLACEHOLDER_STRIPES, 153 167 transformOrigin: "center center", 154 168 transform: isFlipped ··· 177 191 export function CardSkeleton({ className }: { className?: string }) { 178 192 return ( 179 193 <div 180 - className={`aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}] bg-gray-200 dark:bg-zinc-700 animate-pulse ${className ?? ""}`} 181 - style={{ backgroundImage: PLACEHOLDER_STRIPES }} 194 + className={`bg-gray-200 dark:bg-zinc-700 animate-pulse ${className ?? ""}`} 195 + style={{ ...CARD_STYLES, backgroundImage: PLACEHOLDER_STRIPES }} 182 196 /> 183 197 ); 184 198 } ··· 216 230 </> 217 231 ); 218 232 219 - const className = `group relative aspect-[${CARD_ASPECT_RATIO}] overflow-hidden hover:ring-2 hover:ring-cyan-500 motion-safe:transition-shadow block rounded-[${CARD_BORDER_RADIUS}]`; 233 + const wrapperClassName = 234 + "group relative overflow-hidden hover:ring-2 hover:ring-cyan-500 motion-safe:transition-shadow block"; 220 235 221 236 if (href) { 222 237 return ( 223 - <Link to={href} className={className} onClick={onClick}> 238 + <Link 239 + to={href} 240 + className={wrapperClassName} 241 + style={CARD_STYLES} 242 + onClick={onClick} 243 + > 224 244 {content} 225 245 </Link> 226 246 ); ··· 228 248 229 249 if (onClick) { 230 250 return ( 231 - <button type="button" onClick={onClick} className={className}> 251 + <button 252 + type="button" 253 + onClick={onClick} 254 + className={wrapperClassName} 255 + style={CARD_STYLES} 256 + > 232 257 {content} 233 258 </button> 234 259 ); 235 260 } 236 261 237 - return <div className={className}>{content}</div>; 262 + return ( 263 + <div className={wrapperClassName} style={CARD_STYLES}> 264 + {content} 265 + </div> 266 + ); 238 267 } 239 268 240 269 interface CardPreviewProps { ··· 265 294 /> 266 295 ); 267 296 268 - const baseClassName = `aspect-[${CARD_ASPECT_RATIO}] overflow-hidden hover:ring-2 hover:ring-cyan-500 motion-safe:transition-shadow block rounded-[${CARD_BORDER_RADIUS}]`; 269 - const finalClassName = `${baseClassName} ${className ?? ""}`; 297 + const baseClassName = `overflow-hidden hover:ring-2 hover:ring-cyan-500 motion-safe:transition-shadow block ${className ?? ""}`; 270 298 271 299 if (href) { 272 300 return ( 273 - <Link to={href} className={finalClassName} title={setName}> 301 + <Link 302 + to={href} 303 + className={baseClassName} 304 + style={CARD_STYLES} 305 + title={setName} 306 + > 274 307 {content} 275 308 </Link> 276 309 ); 277 310 } 278 311 279 312 return ( 280 - <div className={finalClassName} title={setName}> 313 + <div className={baseClassName} style={CARD_STYLES} title={setName}> 281 314 {content} 282 315 </div> 283 316 );
+4 -10
src/components/deck/GoldfishDragDropProvider.tsx
··· 12 12 useSensors, 13 13 } from "@dnd-kit/core"; 14 14 import { type ReactNode, useId, useRef, useState } from "react"; 15 - import { 16 - CARD_ASPECT_RATIO, 17 - CARD_BORDER_RADIUS, 18 - PLACEHOLDER_STRIPES, 19 - } from "@/components/CardImage"; 15 + import { CARD_STYLES, PLACEHOLDER_STRIPES } from "@/components/CardImage"; 20 16 import type { CardInstance } from "@/lib/goldfish/types"; 21 17 import { getImageUri } from "@/lib/scryfall-utils"; 22 18 ··· 120 116 <img 121 117 src={imageSrc} 122 118 alt="Dragging card" 123 - className={`h-40 aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}] bg-gray-200 dark:bg-zinc-700 shadow-2xl`} 124 - style={{ backgroundImage: PLACEHOLDER_STRIPES }} 119 + className="h-40 bg-gray-200 dark:bg-zinc-700 shadow-2xl" 120 + style={{ ...CARD_STYLES, backgroundImage: PLACEHOLDER_STRIPES }} 125 121 draggable={false} 126 122 /> 127 123 ) : ( 128 - <div 129 - className={`h-40 aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}] bg-amber-700 shadow-2xl`} 130 - /> 124 + <div className="h-40 bg-amber-700 shadow-2xl" style={CARD_STYLES} /> 131 125 )} 132 126 {counterEntries.length > 0 && ( 133 127 <div className="absolute bottom-1 left-1 flex flex-wrap gap-1 max-w-full">
+3 -6
src/components/deck/goldfish/GoldfishBoard.tsx
··· 1 1 import type { DragEndEvent } from "@dnd-kit/core"; 2 2 import { useCallback, useRef } from "react"; 3 - import { 4 - CARD_ASPECT_RATIO, 5 - CARD_BORDER_RADIUS, 6 - CardImage, 7 - } from "@/components/CardImage"; 3 + import { CARD_STYLES, CardImage } from "@/components/CardImage"; 8 4 import type { DeckCard } from "@/lib/deck-types"; 9 5 import { useGoldfishGame } from "@/lib/goldfish"; 10 6 import type { CardInstance, Zone } from "@/lib/goldfish/types"; ··· 132 128 /> 133 129 ) : ( 134 130 <div 135 - className={`w-full aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}] bg-gray-100 dark:bg-zinc-800 flex items-center justify-center text-gray-400 dark:text-zinc-400 text-sm`} 131 + className="w-full bg-gray-100 dark:bg-zinc-800 flex items-center justify-center text-gray-400 dark:text-zinc-400 text-sm" 132 + style={CARD_STYLES} 136 133 > 137 134 {hoveredCard?.isFaceDown ? "Face down" : "Hover a card"} 138 135 </div>
+4 -10
src/components/deck/goldfish/GoldfishCard.tsx
··· 1 1 import { useDraggable } from "@dnd-kit/core"; 2 - import { 3 - CARD_ASPECT_RATIO, 4 - CARD_BORDER_RADIUS, 5 - PLACEHOLDER_STRIPES, 6 - } from "@/components/CardImage"; 2 + import { CARD_STYLES, PLACEHOLDER_STRIPES } from "@/components/CardImage"; 7 3 import type { CardInstance } from "@/lib/goldfish/types"; 8 4 import type { Card } from "@/lib/scryfall-types"; 9 5 import { getImageUri } from "@/lib/scryfall-utils"; ··· 64 60 <img 65 61 src={imageSrc} 66 62 alt={card?.name ?? "Card"} 67 - className={`rounded-[${CARD_BORDER_RADIUS}] bg-gray-200 dark:bg-zinc-700 ${sizeClass} aspect-[${CARD_ASPECT_RATIO}]`} 68 - style={{ backgroundImage: PLACEHOLDER_STRIPES }} 63 + className={`bg-gray-200 dark:bg-zinc-700 ${sizeClass}`} 64 + style={{ ...CARD_STYLES, backgroundImage: PLACEHOLDER_STRIPES }} 69 65 draggable={false} 70 66 loading="lazy" 71 67 /> 72 68 ) : ( 73 - <div 74 - className={`rounded-[${CARD_BORDER_RADIUS}] bg-amber-700 ${sizeClass} aspect-[${CARD_ASPECT_RATIO}]`} 75 - /> 69 + <div className={`bg-amber-700 ${sizeClass}`} style={CARD_STYLES} /> 76 70 )} 77 71 {counterEntries.length > 0 && ( 78 72 <div className="absolute bottom-1 left-1 flex flex-wrap gap-1 max-w-full">
+3 -2
src/components/deck/goldfish/GoldfishSidebar.tsx
··· 1 1 import { useDroppable } from "@dnd-kit/core"; 2 2 import { Droplet, Minus, Plus, RefreshCw, RotateCcw } from "lucide-react"; 3 - import { CARD_ASPECT_RATIO, CARD_BORDER_RADIUS } from "@/components/CardImage"; 3 + import { CARD_STYLES } from "@/components/CardImage"; 4 4 import type { CardInstance, PlayerState } from "@/lib/goldfish/types"; 5 5 import type { Card, ScryfallId } from "@/lib/scryfall-types"; 6 6 import { GoldfishCard } from "./GoldfishCard"; ··· 71 71 /> 72 72 ) : ( 73 73 <div 74 - className={`h-40 aspect-[${CARD_ASPECT_RATIO}] rounded-[${CARD_BORDER_RADIUS}] border-2 border-dashed border-gray-300 dark:border-zinc-600`} 74 + className="h-40 border-2 border-dashed border-gray-300 dark:border-zinc-600" 75 + style={CARD_STYLES} 75 76 /> 76 77 )} 77 78 </div>
+5 -11
src/routes/card/$id.tsx
··· 1 1 import { useQueries, useQuery } from "@tanstack/react-query"; 2 2 import { createFileRoute, Link } from "@tanstack/react-router"; 3 3 import { useEffect, useRef, useState } from "react"; 4 - import { 5 - CARD_ASPECT_RATIO, 6 - CardImage, 7 - CardSkeleton, 8 - } from "@/components/CardImage"; 4 + import { CardImage, CardSkeleton } from "@/components/CardImage"; 9 5 import { CommentsPanel } from "@/components/comments"; 10 6 import { ManaCost } from "@/components/ManaCost"; 11 7 import { OracleText } from "@/components/OracleText"; ··· 232 228 <div className="max-w-7xl mx-auto px-6 py-8"> 233 229 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start"> 234 230 <div className="sticky top-0 z-10 bg-white dark:bg-zinc-900 py-4 -mx-6 px-6 lg:mx-0 lg:px-0 lg:py-0 lg:bg-transparent lg:dark:bg-transparent lg:top-8 flex justify-center lg:justify-end"> 235 - <div 236 - className={`w-full max-w-[calc(50vh*${CARD_ASPECT_RATIO})] lg:max-w-[calc(80vh*${CARD_ASPECT_RATIO})]`} 237 - > 231 + {/* 672/936 = card aspect ratio, hardcoded because Tailwind JIT can't see template literals */} 232 + <div className="w-full max-w-[calc(50vh*672/936)] lg:max-w-[calc(80vh*672/936)]"> 238 233 <CardSkeleton className="h-full w-full shadow-[0_8px_30px_rgba(0,0,0,0.4)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.8)]" /> 239 234 </div> 240 235 </div> ··· 275 270 <div className="max-w-7xl mx-auto px-6 py-8"> 276 271 <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-start"> 277 272 <div className="sticky top-0 z-10 bg-white dark:bg-zinc-900 py-4 -mx-6 px-6 lg:mx-0 lg:px-0 lg:py-0 lg:bg-transparent lg:dark:bg-transparent lg:top-8 flex justify-center lg:justify-end"> 278 - <div 279 - className={`w-full max-w-[calc(50vh*${CARD_ASPECT_RATIO})] lg:max-w-[calc(80vh*${CARD_ASPECT_RATIO})]`} 280 - > 273 + {/* 672/936 = card aspect ratio, hardcoded because Tailwind JIT can't see template literals */} 274 + <div className="w-full max-w-[calc(50vh*672/936)] lg:max-w-[calc(80vh*672/936)]"> 281 275 <CardImage 282 276 card={displayCard} 283 277 size="large"
+6
todos.md
··· 117 117 118 118 ### Lower Priority 119 119 120 + #### CSS variable for card aspect ratio 121 + - **Location**: `src/routes/card/$id.tsx`, various places with `672/936` hardcoded 122 + - **Issue**: Card aspect ratio (672/936) is hardcoded in Tailwind arbitrary values because JIT can't see template literals 123 + - **Fix**: Define `--card-aspect-ratio: 672/936` in styles.css, use `calc(50vh*var(--card-aspect-ratio))` in Tailwind 124 + - **Effort**: Trivial (15 min) 125 + 120 126 #### Extract meta tag builder in card route 121 127 - **Location**: `src/routes/card/$id.tsx:62-113` 122 128 - **Issue**: 51 lines of nested object literals for OG/Twitter meta tags