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
12 changes: 12 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@
does not, and checked the browser support and the cascade layers in the
Tailwind documentation. Claude checked lint, build and tests.

## Element variant card (`src/components/ui/`, `src/lib/difficulty.ts`)

- **Tool:** GitHub Copilot
- **What the team did:** Specified a reusable element card and difficulty
badge, including the letter-to-value display, the existing navy palette,
accessible labeling, and a sample catalog image.
- **What GitHub Copilot did:** Added the card primitives, a difficulty badge,
the shared A–J value mapping, and a sample element-variant card. The title
uses an explicit native heading level and the description uses a paragraph.
- **Review:** The team refined the layout, colors, difficulty display, and
accessibility semantics during review.

## Catalog search (`backend/src/catalog/search.ts`, `eslint.config.js`, `docs/API.md`)

- **Tool:** Claude Code
Expand Down
39 changes: 39 additions & 0 deletions src/components/ui/DifficultyBadge.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Just a tiny polish for screen readers:

We can remove the title attribute so mouse tooltips don't clutter the UI, and wrap "Difficulty " and "Value " in <span className="sr-only"> directly next to their respective values. That way, assistive tech reads "Difficulty A, Value 0.1" cleanly without double-announcing.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cn } from 'cn'
import { getDifficultyValue } from '@/lib/difficulty'

export default function DifficultyBadge({
difficulty,
}: {
difficulty: string | number
}) {
const letter =
typeof difficulty === 'string' ? difficulty.toUpperCase() : undefined
const value = letter ? getDifficultyValue(letter) : undefined
const displayDifficulty = value !== undefined ? letter : difficulty

return (
<span
title={
value !== undefined
? `Difficulty ${letter}, value ${value.toFixed(1)}`
: `Difficulty ${difficulty}`
}
className="inline-flex shrink-0 flex-col items-center gap-1"
>
<span className="sr-only">Difficulty</span>
<span
className={cn(
'inline-flex size-10 items-center justify-center rounded-full',
'bg-text-color-dark px-1 text-lg font-semibold leading-none text-white',
)}
>
{displayDifficulty}
</span>
{value !== undefined && (
<span className="text-xs font-medium leading-none text-text-color-dark">
{value.toFixed(1)}
</span>
)}
</span>
)
}
28 changes: 28 additions & 0 deletions src/components/ui/ElementVariants.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import DifficultyBadge from '@/components/ui/DifficultyBadge'
import elementImage from '../../../data/images/BB/BB-1.101-a.webp'

const ElementVariant = () => {
return (
<Card>
<CardHeader className="text-left">
<CardTitle as="h2">
Code - Name (if any) - Element variant id (a,b,c)
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between gap-4">
<p>Element description goes here.</p>
<DifficultyBadge difficulty="B" />
</div>
<img
src={elementImage}
alt="Balance beam element diagram"
className="mt-4 block w-full rounded-xl"
/>
</CardContent>
</Card>
)
}

export default ElementVariant
106 changes: 106 additions & 0 deletions src/components/ui/card.tsx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

Just one TypeScript fix needed in card.tsx: Make as optional in CardTitleProps (as?: 'h2' | 'h3' | 'h4') and set a fallback default like as: Heading = 'h3'. Otherwise, calling <CardTitle> without as throws a type check error across the rest of the codebase.

Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from 'react'
import { cn } from 'cn'

function Card({
className,
size = 'default',
...props
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
'group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-accent-color py-(--card-spacing) text-sm text-card-foreground ring-1 ring-box-stroke [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
className,
)}
{...props}
/>
)
}

function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
className,
)}
{...props}
/>
)
}

type CardTitleProps = React.ComponentProps<'h2'> & {
as: 'h2' | 'h3' | 'h4'
}

function CardTitle({ as: Heading, className, ...props }: CardTitleProps) {
return (
<Heading
data-slot="card-title"
className={cn(
'font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
className,
)}
{...props}
/>
)
}

function CardDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
<p
data-slot="card-description"
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)
}

function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
)
}

function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-(--card-spacing)', className)}
{...props}
/>
)
}

function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn(
'flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)',
className,
)}
{...props}
/>
)
}

export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}
24 changes: 24 additions & 0 deletions src/lib/difficulty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export const DIFFICULTY_VALUE_BY_LETTER = {
A: 0.1,
B: 0.2,
C: 0.3,
D: 0.4,
E: 0.5,
F: 0.6,
G: 0.7,
H: 0.8,
I: 0.9,
J: 1.0,
} as const

export type DifficultyLetter = keyof typeof DIFFICULTY_VALUE_BY_LETTER

export function getDifficultyValue(letter: string): number | undefined {
const normalizedLetter = letter.toUpperCase()

if (!Object.hasOwn(DIFFICULTY_VALUE_BY_LETTER, normalizedLetter)) {
return undefined
}

return DIFFICULTY_VALUE_BY_LETTER[normalizedLetter as DifficultyLetter]
}