Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85acb5c185 | ||
|
|
f9ef64bb00 |
@@ -553,9 +553,7 @@ export function ActionBar({
|
||||
</div>
|
||||
)}
|
||||
{!browseMode && nameInput.length >= 2 && !hasSuggestions && (
|
||||
<Button type="submit" size="sm">
|
||||
Add
|
||||
</Button>
|
||||
<Button type="submit">Add</Button>
|
||||
)}
|
||||
{showRollAllInitiative && onRollAllInitiative && (
|
||||
<Button
|
||||
|
||||
@@ -28,9 +28,7 @@ export function BulkImportPrompt({
|
||||
<div className="rounded-md border border-green-500/50 bg-green-500/10 px-3 py-2 text-sm text-green-400">
|
||||
All sources loaded
|
||||
</div>
|
||||
<Button size="sm" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
<Button onClick={onDone}>Done</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -42,9 +40,7 @@ export function BulkImportPrompt({
|
||||
Loaded {importState.completed}/{importState.total} sources (
|
||||
{importState.failed} failed)
|
||||
</div>
|
||||
<Button size="sm" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
<Button onClick={onDone}>Done</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,11 +99,7 @@ export function BulkImportPrompt({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onStartImport(baseUrl)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<Button onClick={() => onStartImport(baseUrl)} disabled={isDisabled}>
|
||||
Load All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -100,13 +100,14 @@ export function CreatePlayerModal({
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{isEdit ? "Edit Player" : "Create Player"}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
|
||||
interface HpAdjustPopoverProps {
|
||||
@@ -109,30 +108,26 @@ export function HpAdjustPopover({ onAdjust, onClose }: HpAdjustPopoverProps) {
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={!isValid}
|
||||
className="h-7 w-7 shrink-0 text-red-400 hover:bg-red-950 hover:text-red-300"
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-red-400 transition-colors hover:bg-red-950 hover:text-red-300 disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => applyDelta(-1)}
|
||||
title="Apply damage"
|
||||
aria-label="Apply damage"
|
||||
>
|
||||
<Sword size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={!isValid}
|
||||
className="h-7 w-7 shrink-0 text-emerald-400 hover:bg-emerald-950 hover:text-emerald-300"
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-emerald-400 transition-colors hover:bg-emerald-950 hover:text-emerald-300 disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => applyDelta(1)}
|
||||
title="Apply healing"
|
||||
aria-label="Apply healing"
|
||||
>
|
||||
<Heart size={14} />
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -52,19 +52,20 @@ export function PlayerManagement({
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Player Characters
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{characters.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<p className="text-muted-foreground">No player characters yet</p>
|
||||
<Button onClick={onCreate} size="sm">
|
||||
<Button onClick={onCreate}>
|
||||
<Plus size={16} />
|
||||
Create your first player character
|
||||
</Button>
|
||||
@@ -78,7 +79,7 @@ export function PlayerManagement({
|
||||
return (
|
||||
<div
|
||||
key={pc.id}
|
||||
className="group flex items-center gap-3 rounded-md px-3 py-2 hover:bg-background/50"
|
||||
className="group flex items-center gap-3 rounded-md px-3 py-2 hover:bg-hover-neutral-bg"
|
||||
>
|
||||
{Icon && (
|
||||
<Icon size={18} style={{ color }} className="shrink-0" />
|
||||
@@ -92,25 +93,27 @@ export function PlayerManagement({
|
||||
<span className="text-xs tabular-nums text-muted-foreground">
|
||||
HP {pc.maxHp}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => onEdit(pc)}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="text-muted-foreground"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
</Button>
|
||||
<ConfirmButton
|
||||
icon={<Trash2 size={14} />}
|
||||
label="Delete player character"
|
||||
onConfirm={() => onDelete(pc.id)}
|
||||
className="h-6 w-6 text-muted-foreground"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button onClick={onCreate} size="sm" variant="ghost">
|
||||
<Button onClick={onCreate} variant="ghost">
|
||||
<Plus size={16} />
|
||||
Add
|
||||
</Button>
|
||||
|
||||
@@ -88,11 +88,7 @@ export function SourceFetchPrompt({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleFetch}
|
||||
disabled={status === "fetching" || !url}
|
||||
>
|
||||
<Button onClick={handleFetch} disabled={status === "fetching" || !url}>
|
||||
{status === "fetching" ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
@@ -104,7 +100,6 @@ export function SourceFetchPrompt({
|
||||
<span className="text-xs text-muted-foreground">or</span>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={status === "fetching"}
|
||||
|
||||
@@ -48,7 +48,6 @@ export function SourceManager({ onCacheCleared }: SourceManagerProps) {
|
||||
Cached Sources
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="hover:text-hover-destructive hover:border-hover-destructive"
|
||||
onClick={handleClearAll}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSwipeToDismiss } from "../hooks/use-swipe-to-dismiss.js";
|
||||
import { BulkImportPrompt } from "./bulk-import-prompt.js";
|
||||
import { SourceFetchPrompt } from "./source-fetch-prompt.js";
|
||||
import { StatBlock } from "./stat-block.js";
|
||||
import { Button } from "./ui/button.js";
|
||||
|
||||
interface StatBlockPanelProps {
|
||||
creatureId: CreatureId | null;
|
||||
@@ -81,36 +82,39 @@ function PanelHeader({
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{panelRole === "browse" && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onToggleFold}
|
||||
className="text-muted-foreground hover:text-hover-neutral"
|
||||
className="text-muted-foreground"
|
||||
aria-label="Fold stat block panel"
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{panelRole === "browse" && showPinButton && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onPin}
|
||||
className="text-muted-foreground hover:text-hover-neutral"
|
||||
className="text-muted-foreground"
|
||||
aria-label="Pin creature"
|
||||
>
|
||||
<Pin className="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
{panelRole === "pinned" && (
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onUnpin}
|
||||
className="text-muted-foreground hover:text-hover-neutral"
|
||||
className="text-muted-foreground"
|
||||
aria-label="Unpin creature"
|
||||
>
|
||||
<PinOff className="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,14 +199,15 @@ function MobileDrawer({
|
||||
{...handlers}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-border px-4 py-2">
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onDismiss}
|
||||
className="text-muted-foreground hover:text-hover-neutral"
|
||||
className="text-muted-foreground"
|
||||
aria-label="Fold stat block panel"
|
||||
>
|
||||
<PanelRightClose className="h-4 w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[calc(100%-41px)] overflow-y-auto p-4">
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Button } from "./ui/button.js";
|
||||
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
@@ -33,13 +34,14 @@ export function Toast({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onDismiss}
|
||||
className="text-muted-foreground hover:text-hover-neutral"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -23,6 +23,7 @@ export function TurnNavigation({
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-md border border-border bg-card px-4 py-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onRetreatTurn}
|
||||
disabled={!hasCombatants || isAtStart}
|
||||
@@ -52,6 +53,7 @@ export function TurnNavigation({
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={onAdvanceTurn}
|
||||
disabled={!hasCombatants}
|
||||
|
||||
@@ -13,9 +13,9 @@ const buttonVariants = cva(
|
||||
ghost: "hover:bg-hover-neutral-bg hover:text-hover-neutral",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 px-3 text-xs",
|
||||
default: "h-8 px-3 text-xs",
|
||||
icon: "h-8 w-8",
|
||||
"icon-sm": "h-6 w-6",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -13,6 +13,7 @@ interface ConfirmButtonProps {
|
||||
readonly onConfirm: () => void;
|
||||
readonly icon: ReactElement;
|
||||
readonly label: string;
|
||||
readonly size?: "icon" | "icon-sm";
|
||||
readonly className?: string;
|
||||
readonly disabled?: boolean;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ export function ConfirmButton({
|
||||
onConfirm,
|
||||
icon,
|
||||
label,
|
||||
size = "icon",
|
||||
className,
|
||||
disabled,
|
||||
}: ConfirmButtonProps) {
|
||||
@@ -94,7 +96,7 @@ export function ConfirmButton({
|
||||
<div ref={wrapperRef} className="inline-flex">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
size={size}
|
||||
className={cn(
|
||||
className,
|
||||
isConfirming
|
||||
|
||||
@@ -54,7 +54,7 @@ export function OverflowMenu({ items }: OverflowMenuProps) {
|
||||
<button
|
||||
key={item.label}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-foreground hover:bg-muted/20 disabled:pointer-events-none disabled:opacity-50"
|
||||
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm text-foreground hover:bg-hover-neutral-bg disabled:pointer-events-none disabled:opacity-50"
|
||||
disabled={item.disabled}
|
||||
onClick={() => {
|
||||
item.onClick();
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
--color-hover-neutral: var(--color-primary);
|
||||
--color-hover-action: var(--color-primary);
|
||||
--color-hover-destructive: var(--color-destructive);
|
||||
--color-hover-neutral-bg: var(--color-card);
|
||||
--color-hover-neutral-bg: oklch(0.623 0.214 259 / 0.15);
|
||||
--color-hover-action-bg: var(--color-muted);
|
||||
--color-hover-destructive-bg: transparent;
|
||||
--radius-sm: 0.25rem;
|
||||
|
||||
188
docs/agents/research/2026-03-13-css-classes-buttons-hover.md
Normal file
188
docs/agents/research/2026-03-13-css-classes-buttons-hover.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
date: "2026-03-13T15:35:07.699570+00:00"
|
||||
git_commit: bd398080008349b47726d0016f4b03587f453833
|
||||
branch: main
|
||||
topic: "CSS class usage, button categorization, and hover effects across all components"
|
||||
tags: [research, codebase, css, tailwind, buttons, hover, ui]
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Research: CSS Class Usage, Button Categorization, and Hover Effects
|
||||
|
||||
## Research Question
|
||||
How are CSS classes used across all components? How are buttons categorized — are there primary and secondary buttons? What hover effects exist, and are they unified?
|
||||
|
||||
## Summary
|
||||
|
||||
The project uses **Tailwind CSS v4** with a custom dark theme defined in `index.css` via `@theme`. All class merging goes through a `cn()` utility (clsx + tailwind-merge). Buttons are built on a shared `Button` component using **class-variance-authority (CVA)** with three variants: **default** (primary), **outline**, and **ghost**. Hover effects are partially unified through semantic color tokens (`hover-neutral`, `hover-action`, `hover-destructive`) defined in the theme, but several components use **one-off hardcoded hover colors** that bypass the token system.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### Theme System (`index.css`)
|
||||
|
||||
All colors are defined as CSS custom properties via Tailwind v4's `@theme` directive (`index.css:3-26`):
|
||||
|
||||
| Token | Value | Purpose |
|
||||
|---|---|---|
|
||||
| `--color-background` | `#0f172a` | Page background |
|
||||
| `--color-foreground` | `#e2e8f0` | Default text |
|
||||
| `--color-muted` | `#64748b` | Subdued elements |
|
||||
| `--color-muted-foreground` | `#94a3b8` | Secondary text |
|
||||
| `--color-card` | `#1e293b` | Card/panel surfaces |
|
||||
| `--color-border` | `#334155` | Borders |
|
||||
| `--color-primary` | `#3b82f6` | Primary actions (blue) |
|
||||
| `--color-accent` | `#3b82f6` | Accent (same as primary) |
|
||||
| `--color-destructive` | `#ef4444` | Destructive actions (red) |
|
||||
|
||||
**Hover tokens** (semantic layer for hover states):
|
||||
|
||||
| Token | Resolves to | Usage |
|
||||
|---|---|---|
|
||||
| `hover-neutral` | `primary` (blue) | Text color on neutral hover |
|
||||
| `hover-action` | `primary` (blue) | Text color on action hover |
|
||||
| `hover-destructive` | `destructive` (red) | Text color on destructive hover |
|
||||
| `hover-neutral-bg` | `card` (slate) | Background on neutral hover |
|
||||
| `hover-action-bg` | `muted` | Background on action hover |
|
||||
| `hover-destructive-bg` | `transparent` | Background on destructive hover |
|
||||
|
||||
### Button Component (`components/ui/button.tsx`)
|
||||
|
||||
Uses CVA with three variants and three sizes:
|
||||
|
||||
**Variants:**
|
||||
|
||||
| Variant | Base styles | Hover |
|
||||
|---|---|---|
|
||||
| `default` (primary) | `bg-primary text-primary-foreground` | `hover:bg-primary/90` |
|
||||
| `outline` | `border border-border bg-transparent` | `hover:bg-hover-neutral-bg hover:text-hover-neutral` |
|
||||
| `ghost` | (no background/border) | `hover:bg-hover-neutral-bg hover:text-hover-neutral` |
|
||||
|
||||
**Sizes:**
|
||||
|
||||
| Size | Classes |
|
||||
|---|---|
|
||||
| `default` | `h-9 px-4 py-2` |
|
||||
| `sm` | `h-8 px-3 text-xs` |
|
||||
| `icon` | `h-8 w-8` |
|
||||
|
||||
All variants share: `rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50`.
|
||||
|
||||
There is **no "secondary" variant** — the outline variant is the closest equivalent.
|
||||
|
||||
### Composite Button Components
|
||||
|
||||
**ConfirmButton** (`components/ui/confirm-button.tsx`):
|
||||
- Wraps `Button variant="ghost" size="icon"`
|
||||
- Default state: `hover:text-hover-destructive` (uses token)
|
||||
- Confirming state: `bg-destructive text-primary-foreground animate-confirm-pulse hover:bg-destructive hover:text-primary-foreground`
|
||||
|
||||
**OverflowMenu** (`components/ui/overflow-menu.tsx`):
|
||||
- Trigger: `Button variant="ghost" size="icon"` with `text-muted-foreground hover:text-hover-neutral`
|
||||
- Menu items: raw `<button>` elements with `hover:bg-muted/20` (**not using the token system**)
|
||||
|
||||
### Button Usage Across Components
|
||||
|
||||
| Component | Button type | Variant/Style |
|
||||
|---|---|---|
|
||||
| `action-bar.tsx:556` | `<Button type="submit">` | default (primary) — "Add" |
|
||||
| `action-bar.tsx:561` | `<Button type="button">` | default (primary) — "Roll all" |
|
||||
| `turn-navigation.tsx:25,54` | `<Button size="icon">` | default — prev/next turn |
|
||||
| `turn-navigation.tsx:47` | `<ConfirmButton>` | ghost+destructive — clear encounter |
|
||||
| `source-fetch-prompt.tsx:91` | `<Button size="sm">` | default — "Load" |
|
||||
| `source-fetch-prompt.tsx:106` | `<Button size="sm" variant="outline">` | outline — "Upload file" |
|
||||
| `bulk-import-prompt.tsx:31,45,106` | `<Button size="sm">` | default — "Done"/"Load All" |
|
||||
| `source-manager.tsx:50` | `<Button size="sm" variant="outline">` | outline — "Clear all" |
|
||||
| `hp-adjust-popover.tsx:112` | `<Button variant="ghost" size="icon">` | ghost + custom red — damage |
|
||||
| `hp-adjust-popover.tsx:124` | `<Button variant="ghost" size="icon">` | ghost + custom green — heal |
|
||||
| `player-management.tsx:67` | `<Button>` | default — "Create first player" |
|
||||
| `player-management.tsx:113` | `<Button variant="ghost">` | ghost — "Add player" |
|
||||
| `create-player-modal.tsx:177` | `<Button variant="ghost">` | ghost — "Cancel" |
|
||||
| `create-player-modal.tsx:180` | `<Button type="submit">` | default — "Save"/"Create" |
|
||||
| `combatant-row.tsx:625` | `<ConfirmButton>` | ghost+destructive — remove combatant |
|
||||
|
||||
**Raw `<button>` elements** (not using the Button component):
|
||||
- `action-bar.tsx` — suggestion items, count increment/decrement, browse toggle, custom add (all inline-styled)
|
||||
- `combatant-row.tsx` — editable name, HP display, AC, initiative, concentration toggle
|
||||
- `stat-block-panel.tsx` — fold/close/pin/unpin buttons
|
||||
- `condition-picker.tsx` — condition items
|
||||
- `condition-tags.tsx` — condition tags, add condition button
|
||||
- `toast.tsx` — dismiss button
|
||||
- `player-management.tsx` — close modal, edit player
|
||||
- `create-player-modal.tsx` — close modal
|
||||
- `color-palette.tsx` — color swatches
|
||||
- `icon-grid.tsx` — icon options
|
||||
|
||||
### Hover Effects Inventory
|
||||
|
||||
**Using semantic tokens (unified):**
|
||||
|
||||
| Hover class | Meaning | Used in |
|
||||
|---|---|---|
|
||||
| `hover:bg-hover-neutral-bg` | Neutral background highlight | button.tsx (outline/ghost), action-bar.tsx, condition-picker.tsx, condition-tags.tsx |
|
||||
| `hover:text-hover-neutral` | Text turns primary blue | button.tsx (outline/ghost), action-bar.tsx, combatant-row.tsx, stat-block-panel.tsx, ac-shield.tsx, toast.tsx, overflow-menu.tsx, condition-tags.tsx |
|
||||
| `hover:text-hover-action` | Action text (same as neutral) | action-bar.tsx (overflow trigger) |
|
||||
| `hover:text-hover-destructive` | Destructive text turns red | confirm-button.tsx, source-manager.tsx |
|
||||
| `hover:bg-hover-destructive-bg` | Destructive background (transparent) | source-manager.tsx |
|
||||
|
||||
**One-off / hardcoded hover colors (NOT using tokens):**
|
||||
|
||||
| Hover class | Used in | Context |
|
||||
|---|---|---|
|
||||
| `hover:bg-primary/90` | button.tsx (default variant) | Primary button darken |
|
||||
| `hover:bg-accent/20` | action-bar.tsx | Suggestion highlight, custom add |
|
||||
| `hover:bg-accent/40` | action-bar.tsx | Count +/- buttons, confirm queued |
|
||||
| `hover:bg-muted/20` | overflow-menu.tsx | Menu item highlight |
|
||||
| `hover:bg-red-950` | hp-adjust-popover.tsx | Damage button |
|
||||
| `hover:text-red-300` | hp-adjust-popover.tsx | Damage button text |
|
||||
| `hover:bg-emerald-950` | hp-adjust-popover.tsx | Heal button |
|
||||
| `hover:text-emerald-300` | hp-adjust-popover.tsx | Heal button text |
|
||||
| `hover:text-foreground` | player-management.tsx, create-player-modal.tsx, icon-grid.tsx | Close/edit buttons |
|
||||
| `hover:bg-background/50` | player-management.tsx | Player row hover |
|
||||
| `hover:bg-card` | icon-grid.tsx | Icon option hover |
|
||||
| `hover:border-hover-destructive` | source-manager.tsx | Clear all button border |
|
||||
| `hover:scale-110` | color-palette.tsx | Color swatch enlarge |
|
||||
| `hover:bg-destructive` | confirm-button.tsx (confirming state) | Maintain red bg on hover |
|
||||
| `hover:text-primary-foreground` | confirm-button.tsx (confirming state) | Maintain white text on hover |
|
||||
|
||||
### Hover unification assessment
|
||||
|
||||
The hover token system (`hover-neutral`, `hover-action`, `hover-destructive`) provides a consistent pattern for the most common interactions. The `Button` component's outline and ghost variants use these tokens, and many inline buttons in action-bar, combatant-row, stat-block-panel, and condition components also use them.
|
||||
|
||||
However, there are notable gaps:
|
||||
1. **HP adjust popover** uses hardcoded red/green colors (`red-950`, `emerald-950`) instead of tokens
|
||||
2. **Overflow menu items** use `hover:bg-muted/20` instead of `hover:bg-hover-neutral-bg`
|
||||
3. **Player management modals** use `hover:text-foreground` and `hover:bg-background/50` instead of the semantic tokens
|
||||
4. **Action-bar suggestion items** use `hover:bg-accent/20` and `hover:bg-accent/40` — accent-specific patterns not in the token system
|
||||
5. **Icon grid** and **color palette** use their own hover patterns (`hover:bg-card`, `hover:scale-110`)
|
||||
|
||||
## Code References
|
||||
|
||||
- `apps/web/src/index.css:3-26` — Theme color definitions including hover tokens
|
||||
- `apps/web/src/components/ui/button.tsx:1-38` — Button component with CVA variants
|
||||
- `apps/web/src/components/ui/confirm-button.tsx:93-115` — ConfirmButton with destructive hover states
|
||||
- `apps/web/src/components/ui/overflow-menu.tsx:38-72` — OverflowMenu with non-token hover
|
||||
- `apps/web/src/components/hp-adjust-popover.tsx:117-129` — Hardcoded red/green hover colors
|
||||
- `apps/web/src/components/action-bar.tsx:80-188` — Mixed token and accent-based hovers
|
||||
- `apps/web/src/components/combatant-row.tsx:147-629` — Inline buttons with token hovers
|
||||
- `apps/web/src/components/player-management.tsx:58-98` — Non-token hover patterns
|
||||
- `apps/web/src/components/stat-block-panel.tsx:55-109` — Consistent token usage
|
||||
- `apps/web/src/lib/utils.ts:1-5` — `cn()` utility (clsx + twMerge)
|
||||
|
||||
## Architecture Documentation
|
||||
|
||||
The styling architecture follows this pattern:
|
||||
|
||||
1. **Theme layer**: `index.css` defines all color tokens via `@theme`, including semantic hover tokens
|
||||
2. **Component layer**: `Button` (CVA) provides the shared button abstraction with three variants
|
||||
3. **Composite layer**: `ConfirmButton` and `OverflowMenu` wrap `Button` with additional behavior
|
||||
4. **Usage layer**: Components use either `Button` component or raw `<button>` elements with inline Tailwind classes
|
||||
|
||||
The `cn()` utility from `lib/utils.ts` is used in 9+ components for conditional class merging.
|
||||
|
||||
Custom animations are defined in `index.css` via `@keyframes` + `@utility` pairs: slide-in-right, confirm-pulse, settle-to-bottom, rise-to-center, slide-down-in, slide-up-out, concentration-pulse.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. The `hover-action` and `hover-action-bg` tokens are defined but rarely used — `hover-action` appears only once in `action-bar.tsx:565`. Is this intentional or an incomplete migration?
|
||||
2. The `accent` color (`#3b82f6`) is identical to `primary` — are they intended to diverge in the future, or is this redundancy?
|
||||
3. Should the hardcoded HP adjust colors (red/emerald) be promoted to theme tokens (e.g., `hover-damage`, `hover-heal`)?
|
||||
Reference in New Issue
Block a user