Implement the 028-semantic-hover-tokens feature that unifies hover colors across all interactive UI components via six CSS custom property tokens (three text, three background) defined in the Tailwind v4 theme, replacing hardcoded hover classes in 9 component files plus the shared Button primitive with semantic token references so all hover colors can be globally reconfigured from one place
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,7 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work:
|
|||||||
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons), Vite 6 (026-roll-initiative)
|
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Lucide React (icons), Vite 6 (026-roll-initiative)
|
||||||
- N/A (no storage changes — existing localStorage persistence handles initiative via `setInitiativeUseCase`) (026-roll-initiative)
|
- N/A (no storage changes — existing localStorage persistence handles initiative via `setInitiativeUseCase`) (026-roll-initiative)
|
||||||
- N/A (no storage changes — purely presentational) (027-ui-polish)
|
- N/A (no storage changes — purely presentational) (027-ui-polish)
|
||||||
|
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Vite 6 + Tailwind CSS v4 (CSS-native `@theme` theming), Lucide React (icons) (028-semantic-hover-tokens)
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite
|
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function AcShield({ value, onClick, className }: AcShieldProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-primary",
|
"relative inline-flex items-center justify-center text-sm tabular-nums text-muted-foreground transition-colors hover:text-hover-neutral",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
style={{ width: 28, height: 32 }}
|
style={{ width: 28, height: 32 }}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function ActionBar({
|
|||||||
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
|
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
|
||||||
i === suggestionIndex
|
i === suggestionIndex
|
||||||
? "bg-accent/20 text-foreground"
|
? "bg-accent/20 text-foreground"
|
||||||
: "text-foreground hover:bg-accent/10"
|
: "text-foreground hover:bg-hover-neutral-bg"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleSelectSuggestion(creature)}
|
onClick={() => handleSelectSuggestion(creature)}
|
||||||
onMouseEnter={() => setSuggestionIndex(i)}
|
onMouseEnter={() => setSuggestionIndex(i)}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export function BestiarySearch({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
className="text-muted-foreground hover:text-hover-neutral"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -108,7 +108,7 @@ export function BestiarySearch({
|
|||||||
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
|
className={`flex w-full items-center justify-between px-3 py-1.5 text-left text-sm ${
|
||||||
i === highlightIndex
|
i === highlightIndex
|
||||||
? "bg-accent/20 text-foreground"
|
? "bg-accent/20 text-foreground"
|
||||||
: "text-foreground hover:bg-accent/10"
|
: "text-foreground hover:bg-hover-neutral-bg"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => onSelectCreature(creature)}
|
onClick={() => onSelectCreature(creature)}
|
||||||
onMouseEnter={() => setHighlightIndex(i)}
|
onMouseEnter={() => setHighlightIndex(i)}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function EditableName({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
startEditing();
|
startEditing();
|
||||||
}}
|
}}
|
||||||
className="truncate text-left text-sm text-foreground hover:text-primary transition-colors"
|
className="truncate text-left text-sm text-foreground hover:text-hover-neutral transition-colors"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</button>
|
</button>
|
||||||
@@ -150,7 +150,7 @@ function MaxHpDisplay({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={startEditing}
|
onClick={startEditing}
|
||||||
className="inline-block h-7 min-w-[3ch] text-center text-sm leading-7 tabular-nums text-muted-foreground transition-colors hover:text-primary"
|
className="inline-block h-7 min-w-[3ch] text-center text-sm leading-7 tabular-nums text-muted-foreground transition-colors hover:text-hover-neutral"
|
||||||
>
|
>
|
||||||
{maxHp ?? "Max"}
|
{maxHp ?? "Max"}
|
||||||
</button>
|
</button>
|
||||||
@@ -190,7 +190,7 @@ function ClickableHp({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPopoverOpen(true)}
|
onClick={() => setPopoverOpen(true)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-block h-7 min-w-[3ch] text-center text-sm font-medium leading-7 tabular-nums transition-colors hover:text-primary",
|
"inline-block h-7 min-w-[3ch] text-center text-sm font-medium leading-7 tabular-nums transition-colors hover:text-hover-neutral",
|
||||||
status === "bloodied" && "text-amber-400",
|
status === "bloodied" && "text-amber-400",
|
||||||
status === "unconscious" && "text-red-400",
|
status === "unconscious" && "text-red-400",
|
||||||
status === "healthy" && "text-foreground",
|
status === "healthy" && "text-foreground",
|
||||||
@@ -324,7 +324,7 @@ function InitiativeDisplay({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRollInitiative(combatantId)}
|
onClick={() => onRollInitiative(combatantId)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-7 w-full items-center justify-center text-muted-foreground transition-colors hover:text-primary",
|
"flex h-7 w-full items-center justify-center text-muted-foreground transition-colors hover:text-hover-neutral",
|
||||||
dimmed && "opacity-50",
|
dimmed && "opacity-50",
|
||||||
)}
|
)}
|
||||||
title="Roll initiative"
|
title="Roll initiative"
|
||||||
@@ -344,8 +344,8 @@ function InitiativeDisplay({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"h-7 w-full text-center text-sm leading-7 tabular-nums transition-colors",
|
"h-7 w-full text-center text-sm leading-7 tabular-nums transition-colors",
|
||||||
initiative !== undefined
|
initiative !== undefined
|
||||||
? "font-medium text-foreground hover:text-primary"
|
? "font-medium text-foreground hover:text-hover-neutral"
|
||||||
: "text-muted-foreground hover:text-primary",
|
: "text-muted-foreground hover:text-hover-neutral",
|
||||||
dimmed && "opacity-50",
|
dimmed && "opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -429,7 +429,7 @@ export function CombatantRow({
|
|||||||
title="Concentrating"
|
title="Concentrating"
|
||||||
aria-label="Toggle concentration"
|
aria-label="Toggle concentration"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity",
|
"flex w-full items-center justify-center self-stretch -my-2 -ml-[2px] pl-[14px] transition-opacity hover:text-hover-neutral hover:opacity-100",
|
||||||
combatant.isConcentrating
|
combatant.isConcentrating
|
||||||
? dimmed
|
? dimmed
|
||||||
? "opacity-50 text-purple-400"
|
? "opacity-50 text-purple-400"
|
||||||
@@ -519,7 +519,7 @@ export function CombatantRow({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7 text-muted-foreground hover:text-destructive opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto focus:opacity-100 focus:pointer-events-auto transition-opacity"
|
className="h-7 w-7 text-muted-foreground hover:text-hover-destructive opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto focus:opacity-100 focus:pointer-events-auto transition-opacity"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove(id);
|
onRemove(id);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export function ConditionPicker({
|
|||||||
key={def.id}
|
key={def.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-card",
|
"flex w-full items-center gap-2 rounded px-2 py-1 text-sm transition-colors hover:bg-hover-neutral-bg",
|
||||||
isActive && "bg-card/50",
|
isActive && "bg-card/50",
|
||||||
)}
|
)}
|
||||||
onClick={() => onToggle(def.id)}
|
onClick={() => onToggle(def.id)}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ConditionTags({
|
|||||||
type="button"
|
type="button"
|
||||||
title={def.label}
|
title={def.label}
|
||||||
aria-label={`Remove ${def.label}`}
|
aria-label={`Remove ${def.label}`}
|
||||||
className={`inline-flex items-center rounded p-0.5 hover:bg-card transition-colors ${colorClass}`}
|
className={`inline-flex items-center rounded p-0.5 hover:bg-hover-neutral-bg transition-colors ${colorClass}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onRemove(condId);
|
onRemove(condId);
|
||||||
@@ -89,7 +89,7 @@ export function ConditionTags({
|
|||||||
type="button"
|
type="button"
|
||||||
title="Add condition"
|
title="Add condition"
|
||||||
aria-label="Add condition"
|
aria-label="Add condition"
|
||||||
className="inline-flex items-center rounded p-0.5 text-muted-foreground hover:text-foreground hover:bg-card transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
className="inline-flex items-center rounded p-0.5 text-muted-foreground hover:text-hover-neutral hover:bg-hover-neutral-bg transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onOpenPicker();
|
onOpenPicker();
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function StatBlockPanel({ creature, onClose }: StatBlockPanelProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
className="text-muted-foreground hover:text-hover-neutral"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -63,7 +63,7 @@ export function StatBlockPanel({ creature, onClose }: StatBlockPanelProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-muted-foreground hover:text-foreground"
|
className="text-muted-foreground hover:text-hover-neutral"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function TurnNavigation({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-foreground border-foreground hover:text-primary hover:border-primary hover:bg-muted"
|
className="h-8 w-8 text-foreground border-foreground hover:text-hover-action hover:border-hover-action hover:bg-transparent"
|
||||||
onClick={onRetreatTurn}
|
onClick={onRetreatTurn}
|
||||||
disabled={!hasCombatants || isAtStart}
|
disabled={!hasCombatants || isAtStart}
|
||||||
title="Previous turn"
|
title="Previous turn"
|
||||||
@@ -55,7 +55,7 @@ export function TurnNavigation({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-primary"
|
className="h-8 w-8 text-muted-foreground hover:text-hover-action"
|
||||||
onClick={onRollAllInitiative}
|
onClick={onRollAllInitiative}
|
||||||
title="Roll all initiative"
|
title="Roll all initiative"
|
||||||
aria-label="Roll all initiative"
|
aria-label="Roll all initiative"
|
||||||
@@ -65,7 +65,7 @@ export function TurnNavigation({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
className="h-8 w-8 text-muted-foreground hover:text-hover-destructive"
|
||||||
onClick={onClearEncounter}
|
onClick={onClearEncounter}
|
||||||
disabled={!hasCombatants}
|
disabled={!hasCombatants}
|
||||||
>
|
>
|
||||||
@@ -75,7 +75,7 @@ export function TurnNavigation({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-foreground border-foreground hover:text-primary hover:border-primary hover:bg-muted"
|
className="h-8 w-8 text-foreground border-foreground hover:text-hover-action hover:border-hover-action hover:bg-transparent"
|
||||||
onClick={onAdvanceTurn}
|
onClick={onAdvanceTurn}
|
||||||
disabled={!hasCombatants}
|
disabled={!hasCombatants}
|
||||||
title="Next turn"
|
title="Next turn"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ const buttonVariants = cva(
|
|||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
outline:
|
outline:
|
||||||
"border border-border bg-transparent hover:bg-card hover:text-foreground",
|
"border border-border bg-transparent hover:bg-hover-neutral-bg hover:text-hover-neutral",
|
||||||
ghost: "hover:bg-card hover:text-foreground",
|
ghost: "hover:bg-hover-neutral-bg hover:text-hover-neutral",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: "h-9 px-4 py-2",
|
||||||
|
|||||||
@@ -13,6 +13,12 @@
|
|||||||
--color-primary-foreground: #ffffff;
|
--color-primary-foreground: #ffffff;
|
||||||
--color-accent: #3b82f6;
|
--color-accent: #3b82f6;
|
||||||
--color-destructive: #ef4444;
|
--color-destructive: #ef4444;
|
||||||
|
--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-action-bg: var(--color-muted);
|
||||||
|
--color-hover-destructive-bg: transparent;
|
||||||
--radius-sm: 0.25rem;
|
--radius-sm: 0.25rem;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
|
|||||||
35
specs/028-semantic-hover-tokens/checklists/requirements.md
Normal file
35
specs/028-semantic-hover-tokens/checklists/requirements.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Specification Quality Checklist: Semantic Hover Tokens
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-03-10
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
|
||||||
|
- The spec references "CSS custom properties" and "theme file" which are borderline implementation — however these are part of the user's original description and represent the feature's domain language, not specific technology choices.
|
||||||
59
specs/028-semantic-hover-tokens/data-model.md
Normal file
59
specs/028-semantic-hover-tokens/data-model.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Data Model: Semantic Hover Tokens
|
||||||
|
|
||||||
|
This feature introduces no domain or application entities. The "data model" consists of CSS theme tokens and their mapping to interaction tiers.
|
||||||
|
|
||||||
|
## Theme Tokens
|
||||||
|
|
||||||
|
| Token | Default Value | Purpose |
|
||||||
|
|-------|--------------|---------|
|
||||||
|
| `--color-hover-neutral` | `var(--color-primary)` | Text color for neutral-interactive hover (editable fields, toggles, close buttons) |
|
||||||
|
| `--color-hover-action` | `var(--color-primary)` | Text color for primary-action hover (navigation, add, roll) |
|
||||||
|
| `--color-hover-destructive` | `var(--color-destructive)` | Text color for destructive-action hover (remove, clear) |
|
||||||
|
| `--color-hover-neutral-bg` | `var(--color-card)` | Background color for neutral-interactive hover (condition items, list items) |
|
||||||
|
| `--color-hover-action-bg` | `var(--color-muted)` | Background color for primary-action hover (navigation buttons) |
|
||||||
|
| `--color-hover-destructive-bg` | `transparent` | Background color for destructive-action hover (reserved) |
|
||||||
|
|
||||||
|
## Interaction Tier Assignments
|
||||||
|
|
||||||
|
### Neutral-Interactive (editable fields, data display)
|
||||||
|
|
||||||
|
| Component | Element | Current Hover | New Hover (text) | New Hover (bg) |
|
||||||
|
|-----------|---------|---------------|-------------------|----------------|
|
||||||
|
| combatant-row | Combatant name | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| combatant-row | MaxHpDisplay | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| combatant-row | ClickableHp | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| combatant-row | Initiative d20 (roll) | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| combatant-row | Initiative value | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| combatant-row | AC shield | `hover:text-primary` | `hover:text-hover-neutral` | — |
|
||||||
|
| condition-tags | Condition icon button | `hover:bg-card` | — | `hover:bg-hover-neutral-bg` |
|
||||||
|
| condition-tags | Add condition (+) | `hover:text-foreground hover:bg-card` | `hover:text-hover-neutral` | `hover:bg-hover-neutral-bg` |
|
||||||
|
| condition-picker | Condition row | `hover:bg-card` | — | `hover:bg-hover-neutral-bg` |
|
||||||
|
| action-bar | Bestiary search item | `hover:bg-accent/10` | — | `hover:bg-hover-neutral-bg` |
|
||||||
|
| stat-block-panel | Close button | `hover:text-foreground` | `hover:text-hover-neutral` | — |
|
||||||
|
| bestiary-search | Close button | `hover:text-foreground` | `hover:text-hover-neutral` | — |
|
||||||
|
| bestiary-search | Search result item | `hover:bg-accent/10` | — | `hover:bg-hover-neutral-bg` |
|
||||||
|
|
||||||
|
### Primary-Action (navigation, additive actions)
|
||||||
|
|
||||||
|
| Component | Element | Current Hover | New Hover (text) | New Hover (bg) |
|
||||||
|
|-----------|---------|---------------|-------------------|----------------|
|
||||||
|
| turn-navigation | Previous/Next buttons | `hover:text-primary hover:border-primary hover:bg-muted` | `hover:text-hover-action hover:border-hover-action` | `hover:bg-transparent` |
|
||||||
|
| turn-navigation | Roll All button | `hover:text-primary` | `hover:text-hover-action` | — |
|
||||||
|
|
||||||
|
### Destructive-Action (removal, clearing)
|
||||||
|
|
||||||
|
| Component | Element | Current Hover | New Hover (text) | New Hover (bg) |
|
||||||
|
|-----------|---------|---------------|-------------------|----------------|
|
||||||
|
| combatant-row | Remove button (X) | `hover:text-destructive` | `hover:text-hover-destructive` | — |
|
||||||
|
| turn-navigation | Clear button | `hover:text-destructive` | `hover:text-hover-destructive` | — |
|
||||||
|
|
||||||
|
### UI Primitives (shared Button component)
|
||||||
|
|
||||||
|
| Component | Variant | Current Hover | New Hover (text) | New Hover (bg) |
|
||||||
|
|-----------|---------|---------------|-------------------|----------------|
|
||||||
|
| ui/button | outline | `hover:bg-card hover:text-foreground` | `hover:text-hover-neutral` | `hover:bg-hover-neutral-bg` |
|
||||||
|
| ui/button | ghost | `hover:bg-card hover:text-foreground` | `hover:text-hover-neutral` | `hover:bg-hover-neutral-bg` |
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- **hp-adjust-popover.tsx**: Damage/healing buttons use domain-semantic colors (red/emerald), not interaction-tier colors.
|
||||||
70
specs/028-semantic-hover-tokens/plan.md
Normal file
70
specs/028-semantic-hover-tokens/plan.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Implementation Plan: Semantic Hover Tokens
|
||||||
|
|
||||||
|
**Branch**: `028-semantic-hover-tokens` | **Date**: 2026-03-10 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/028-semantic-hover-tokens/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Unify inconsistent hover color usage across all interactive UI components by introducing six semantic CSS custom property tokens (three text, three background) in the Tailwind v4 theme, then migrating all affected components from hardcoded hover classes to the new token-based classes. Purely presentational — no domain or application layer changes.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Tailwind CSS v4, Vite 6
|
||||||
|
**Primary Dependencies**: Tailwind CSS v4 (CSS-native `@theme` theming), Lucide React (icons)
|
||||||
|
**Storage**: N/A (no storage changes — purely presentational)
|
||||||
|
**Testing**: Vitest (existing), manual visual verification for hover states
|
||||||
|
**Target Platform**: Web (modern browsers)
|
||||||
|
**Project Type**: Web application (React SPA)
|
||||||
|
**Performance Goals**: N/A (no runtime behavior change)
|
||||||
|
**Constraints**: Must preserve existing visual appearance with default token values
|
||||||
|
**Scale/Scope**: 9 component files + 1 theme file + 1 UI primitive (button.tsx)
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
| Principle | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| I. Deterministic Domain Core | PASS | No domain changes |
|
||||||
|
| II. Layered Architecture | PASS | Changes confined to adapter layer (web UI) |
|
||||||
|
| III. Agent Boundary | N/A | No agent layer involvement |
|
||||||
|
| IV. Clarification-First | PASS | Spec has zero [NEEDS CLARIFICATION] markers; scope decisions documented in research.md |
|
||||||
|
| V. Escalation Gates | PASS | Research expanded scope to 8 files (from 5) based on audit — documented in R-002 |
|
||||||
|
| VI. MVP Baseline Language | PASS | Three-tier system is MVP baseline; additional tiers not permanently excluded |
|
||||||
|
| VII. No Gameplay Rules | PASS | Purely presentational feature |
|
||||||
|
|
||||||
|
**Post-Phase 1 re-check**: All gates still pass. The expanded component scope (stat-block-panel, bestiary-search, ac-shield) remains within adapter layer and serves the same hover unification goal.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/028-semantic-hover-tokens/
|
||||||
|
├── spec.md
|
||||||
|
├── plan.md # This file
|
||||||
|
├── research.md # Phase 0 output
|
||||||
|
├── data-model.md # Phase 1 output
|
||||||
|
├── quickstart.md # Phase 1 output
|
||||||
|
└── checklists/
|
||||||
|
└── requirements.md # Spec quality checklist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/web/src/
|
||||||
|
├── index.css # Theme tokens (add 6 new --color-hover-* properties)
|
||||||
|
└── components/
|
||||||
|
├── combatant-row.tsx # 6× neutral text, 1× destructive text
|
||||||
|
├── condition-tags.tsx # neutral text + bg
|
||||||
|
├── condition-picker.tsx # neutral bg
|
||||||
|
├── turn-navigation.tsx # 2× action text/border/bg, 1× action text, 1× destructive text
|
||||||
|
├── action-bar.tsx # neutral bg
|
||||||
|
├── ac-shield.tsx # neutral text
|
||||||
|
├── stat-block-panel.tsx # neutral text (close buttons ×2)
|
||||||
|
├── bestiary-search.tsx # neutral text + bg
|
||||||
|
└── ui/button.tsx # outline + ghost variant hovers → neutral tokens
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: No new files or directories. All changes are edits to existing theme and component files within the existing `apps/web/src/` structure.
|
||||||
37
specs/028-semantic-hover-tokens/quickstart.md
Normal file
37
specs/028-semantic-hover-tokens/quickstart.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Quickstart: Semantic Hover Tokens
|
||||||
|
|
||||||
|
## What This Feature Does
|
||||||
|
|
||||||
|
Replaces hardcoded hover colors across UI components with three semantic CSS custom properties (tokens), so all hover colors can be changed from one place in the theme.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **Theme**: `apps/web/src/index.css` — define the six new `--color-hover-*` tokens in the `@theme` block
|
||||||
|
- **Components to update**:
|
||||||
|
- `apps/web/src/components/combatant-row.tsx` — 6× neutral, 1× destructive
|
||||||
|
- `apps/web/src/components/condition-tags.tsx` — neutral text + bg
|
||||||
|
- `apps/web/src/components/condition-picker.tsx` — neutral bg
|
||||||
|
- `apps/web/src/components/turn-navigation.tsx` — 2× action, 1× destructive
|
||||||
|
- `apps/web/src/components/action-bar.tsx` — neutral bg
|
||||||
|
- `apps/web/src/components/stat-block-panel.tsx` — neutral text (close button)
|
||||||
|
- `apps/web/src/components/bestiary-search.tsx` — neutral text + bg
|
||||||
|
- `apps/web/src/components/ui/button.tsx` — outline + ghost variant hovers
|
||||||
|
|
||||||
|
## Token → Tailwind Class Mapping
|
||||||
|
|
||||||
|
| Token | Tailwind Text Class | Tailwind BG Class |
|
||||||
|
|-------|--------------------|--------------------|
|
||||||
|
| `--color-hover-neutral` | `hover:text-hover-neutral` | — |
|
||||||
|
| `--color-hover-action` | `hover:text-hover-action` | — |
|
||||||
|
| `--color-hover-destructive` | `hover:text-hover-destructive` | — |
|
||||||
|
| `--color-hover-neutral-bg` | — | `hover:bg-hover-neutral-bg` |
|
||||||
|
| `--color-hover-action-bg` | — | `hover:bg-hover-action-bg` |
|
||||||
|
| `--color-hover-destructive-bg` | — | `hover:bg-hover-destructive-bg` |
|
||||||
|
|
||||||
|
## How to Verify
|
||||||
|
|
||||||
|
1. `pnpm --filter web dev` — start dev server
|
||||||
|
2. Hover over editable fields (name, HP, AC, initiative) → should turn blue (primary)
|
||||||
|
3. Hover over navigation buttons (prev/next, Roll All) → should be blue
|
||||||
|
4. Hover over remove/clear buttons → should be red
|
||||||
|
5. Change `--color-hover-neutral` to a bright color in DevTools → all editable fields should update
|
||||||
83
specs/028-semantic-hover-tokens/research.md
Normal file
83
specs/028-semantic-hover-tokens/research.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Research: Semantic Hover Tokens
|
||||||
|
|
||||||
|
## R-001: Tailwind v4 Theme Token Mechanism
|
||||||
|
|
||||||
|
**Decision**: Define new CSS custom properties in the `@theme` block of `index.css`. Tailwind v4 automatically makes `--color-*` variables available as utility classes (e.g., `--color-hover-neutral` → `text-hover-neutral`, `bg-hover-neutral`).
|
||||||
|
|
||||||
|
**Rationale**: The project already uses this pattern for `--color-primary`, `--color-destructive`, etc. Adding new tokens follows the established convention and requires zero config changes.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Tailwind plugin extending theme: Unnecessary — Tailwind v4 CSS-native theming handles this.
|
||||||
|
- CSS-only approach without Tailwind utilities: Would lose the `hover:text-*` class syntax that all components already use.
|
||||||
|
|
||||||
|
## R-002: Scope of Affected Components
|
||||||
|
|
||||||
|
**Decision**: The spec lists five components, but the audit found hover colors in additional files: `hp-adjust-popover.tsx`, `stat-block-panel.tsx`, `bestiary-search.tsx`, `ac-shield.tsx`, and `ui/button.tsx`. The plan will address the five specified components plus `ac-shield.tsx` (already uses `hover:text-primary` for an editable field). The HP adjust popover's semantic damage/healing colors are **out of scope** — they represent domain-semantic colors (damage=red, healing=green) rather than interaction-tier hover colors. The `button.tsx` CVA variants were initially out of scope but were brought in during implementation (see R-007).
|
||||||
|
|
||||||
|
**Rationale**: The HP popover buttons communicate game semantics (damage vs. healing), not interaction intent. Forcing them into the three-tier system would reduce clarity. `stat-block-panel.tsx` and `bestiary-search.tsx` use `hover:text-foreground` which already matches the neutral-interactive intent (dim→bright) — these can adopt the neutral token.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Include all hover colors across all components: Over-scoped; HP popover serves a different purpose.
|
||||||
|
- Strictly limit to five listed files: Would miss `ac-shield.tsx` which is clearly neutral-interactive.
|
||||||
|
|
||||||
|
## R-003: Token Naming Convention
|
||||||
|
|
||||||
|
**Decision**: Use `--color-hover-neutral`, `--color-hover-action`, `--color-hover-destructive` as the CSS custom property names. This yields Tailwind classes like `hover:text-hover-neutral`, `hover:bg-hover-action`, etc.
|
||||||
|
|
||||||
|
**Rationale**: The `hover-` prefix groups them visually in the theme. The tier names (neutral/action/destructive) are concise and parallel the existing `--color-primary`/`--color-destructive` naming.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- `--color-interactive-*`: Longer, and "interactive" is redundant since hover already implies interaction.
|
||||||
|
- `--color-hover-primary` for the action tier: Conflicts conceptually with existing `--color-primary` (which is used for non-hover purposes too).
|
||||||
|
|
||||||
|
## R-004: Background Hover Variants
|
||||||
|
|
||||||
|
**Decision**: Define three additional background tokens: `--color-hover-neutral-bg`, `--color-hover-action-bg`, `--color-hover-destructive-bg`. Components that use `hover:bg-*` will reference these.
|
||||||
|
|
||||||
|
**Rationale**: Several components use background hovers (condition-picker `hover:bg-card`, navigation buttons `hover:bg-muted`, bestiary items `hover:bg-accent/10`). A single text-only token won't cover these cases.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Reuse existing `--color-card`/`--color-muted` for background hovers: Breaks the single-point-of-change goal — changing the neutral hover background would also change all card backgrounds.
|
||||||
|
- No background tokens (text-only): Would leave background hovers hardcoded, defeating the purpose.
|
||||||
|
|
||||||
|
## R-005: Default Token Values
|
||||||
|
|
||||||
|
**Decision**: Map defaults to preserve current visual appearance:
|
||||||
|
- `--color-hover-neutral: var(--color-primary)` (#3b82f6 — blue, matching previous `hover:text-primary` behavior)
|
||||||
|
- `--color-hover-action: var(--color-primary)` (#3b82f6 — blue)
|
||||||
|
- `--color-hover-destructive: var(--color-destructive)` (#ef4444 — red)
|
||||||
|
- `--color-hover-neutral-bg: var(--color-card)` (#1e293b)
|
||||||
|
- `--color-hover-action-bg: var(--color-muted)` (#64748b — defined but not used by nav buttons, see R-008)
|
||||||
|
- `--color-hover-destructive-bg: transparent` (current destructive hovers are text-only in scope)
|
||||||
|
|
||||||
|
**Rationale**: Using `var()` references means the hover tokens inherit from the base theme by default, reducing duplication. The neutral token was initially `var(--color-foreground)` but changed to `var(--color-primary)` during implementation because elements already at `text-foreground` (name, initiative, HP) had no visible hover change — the hover target color was identical to the resting color. Using `var(--color-primary)` preserves the original blue hover feedback while still centralizing control.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Hardcode hex values: Would break the cascade if base theme colors change.
|
||||||
|
- Fewer background tokens: The action and neutral backgrounds serve different purposes currently.
|
||||||
|
- `--color-hover-neutral: var(--color-foreground)`: No visible hover for elements already at foreground color — rejected during implementation.
|
||||||
|
|
||||||
|
## R-006: Components Using hover:text-foreground
|
||||||
|
|
||||||
|
**Decision**: `stat-block-panel.tsx` and `bestiary-search.tsx` close buttons use `hover:text-foreground` (dim→bright). These will migrate to `hover:text-hover-neutral` since the default value of `--color-hover-neutral` is `var(--color-primary)` — providing a blue hover instead of the previous dim→bright.
|
||||||
|
|
||||||
|
**Rationale**: Consistent with the neutral-interactive tier. These are close/dismiss actions, not destructive (they don't delete data).
|
||||||
|
|
||||||
|
## R-007: Button Component (ui/button.tsx) Brought Into Scope
|
||||||
|
|
||||||
|
**Decision**: Migrate the `outline` and `ghost` CVA variant hover styles in `ui/button.tsx` from hardcoded `hover:bg-card hover:text-foreground` to `hover:bg-hover-neutral-bg hover:text-hover-neutral`.
|
||||||
|
|
||||||
|
**Rationale**: Initially excluded as a shared UI primitive (R-002). During implementation, the ghost variant's `hover:text-foreground` produced no visible hover on the search button (same issue as R-005 — foreground→foreground). Additionally, the outline variant's `hover:bg-card` conflicted with inline `hover:bg-hover-action-bg` on the nav buttons, creating a double-background hover artifact. Migrating the Button variants to semantic tokens resolved both issues.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Keep button.tsx out of scope: Would leave the search button without visible hover and nav buttons with conflicting hover backgrounds.
|
||||||
|
|
||||||
|
## R-008: Nav Button Background Hover Removed
|
||||||
|
|
||||||
|
**Decision**: The Previous/Next outline buttons in `turn-navigation.tsx` use `hover:bg-transparent` instead of `hover:bg-hover-action-bg`. The blue text + blue border provide sufficient hover feedback without a background fill.
|
||||||
|
|
||||||
|
**Rationale**: The `--color-hover-action-bg` default (`var(--color-muted)` = `#64748b`) created a heavy filled-box appearance on the small outline icon buttons. Combined with the blue border and icon, the solid grey background was visually overwhelming. Removing it produces a cleaner hover state.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Keep `hover:bg-hover-action-bg`: Visually heavy on small outline buttons.
|
||||||
|
- Use a semi-transparent background: Adds complexity for marginal benefit.
|
||||||
96
specs/028-semantic-hover-tokens/spec.md
Normal file
96
specs/028-semantic-hover-tokens/spec.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Feature Specification: Semantic Hover Tokens
|
||||||
|
|
||||||
|
**Feature Branch**: `028-semantic-hover-tokens`
|
||||||
|
**Created**: 2026-03-10
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Unify hover color semantics across all interactive elements and make them globally configurable via CSS custom properties in the theme."
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Theme Author Changes Hover Colors in One Place (Priority: P1)
|
||||||
|
|
||||||
|
A theme author (or future-self) wants to change the hover color for all editable fields across the app. They edit a single CSS custom property in the theme file and every editable field (name, initiative, HP, AC, conditions) updates its hover color consistently.
|
||||||
|
|
||||||
|
**Why this priority**: The entire point of this feature is centralizing hover color control. If tokens exist but aren't referenced everywhere, the feature delivers no value.
|
||||||
|
|
||||||
|
**Independent Test**: Change the neutral-interactive hover token value in the theme file and verify all editable fields reflect the new color without any other code changes.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the theme defines a neutral-interactive hover token, **When** the token value is changed from grey-to-white to any other color, **Then** all editable fields (name, initiative, HP, AC, conditions) display the new hover color.
|
||||||
|
2. **Given** the theme defines a primary-action hover token, **When** the token value is changed, **Then** the Add button and step forward/back navigation buttons display the new hover color.
|
||||||
|
3. **Given** the theme defines a destructive-action hover token, **When** the token value is changed, **Then** the clear encounter and remove combatant buttons display the new hover color.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Consistent Visual Language for Interaction Types (Priority: P1)
|
||||||
|
|
||||||
|
A user hovers over different interactive elements and perceives a consistent visual language: editable data fields share one hover style, primary action buttons share another, and destructive actions share a third. This replaces the current inconsistent mix where blue is used for both editable fields and navigation.
|
||||||
|
|
||||||
|
**Why this priority**: Consistent hover semantics improve usability by communicating interaction intent. This is the user-facing motivation for the feature.
|
||||||
|
|
||||||
|
**Independent Test**: Hover over each interactive element category and verify visual consistency within tiers and clear distinction between tiers.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a combatant row is displayed, **When** the user hovers over the name, initiative, HP, or AC fields, **Then** each field transitions to the neutral-interactive hover color (a subtle grey-to-white shift).
|
||||||
|
2. **Given** the top bar is displayed, **When** the user hovers over the Add button or step forward/back navigation, **Then** the element transitions to the primary-action hover color (blue).
|
||||||
|
3. **Given** the combatant row or top bar is displayed, **When** the user hovers over the remove combatant button or clear encounter button, **Then** the element transitions to the destructive-action hover color (red).
|
||||||
|
4. **Given** the condition tags on a combatant row, **When** the user hovers over a condition tag, **Then** it uses the neutral-interactive hover style, not the primary-action blue.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - No One-Off Hover Colors Remain (Priority: P2)
|
||||||
|
|
||||||
|
A developer reviewing the codebase finds no hardcoded hover color values outside the theme file. Every hover color reference uses one of the three semantic token classes.
|
||||||
|
|
||||||
|
**Why this priority**: Eliminating one-offs (like the amber hover) ensures long-term maintainability and prevents drift back to inconsistency.
|
||||||
|
|
||||||
|
**Independent Test**: Search the codebase for hover color classes that do not reference the three semantic tokens and confirm none exist in the affected components.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the affected components (combatant-row, condition-tags, condition-picker, turn-navigation, action-bar), **When** a developer searches for hover color classes, **Then** every hover color reference resolves to one of the three semantic tokens.
|
||||||
|
2. **Given** the current amber one-off hover color, **When** the feature is complete, **Then** the amber hover is replaced with the appropriate semantic token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when a component needs hover colors for both text and background? The tokens should support both text and background variants where needed (e.g., condition picker items use background hover, not text hover).
|
||||||
|
- What happens when hover colors interact with disabled states? Disabled elements should not display hover color changes regardless of token values.
|
||||||
|
- What happens when an element fits multiple tiers (e.g., clicking a condition tag both edits data and could be seen as an action)? The tier assignment is based on the primary interaction intent: editing data = neutral-interactive, triggering a distinct action = primary-action.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: The theme MUST define three semantic hover color tokens as CSS custom properties: one for neutral-interactive elements, one for primary-action elements, and one for destructive-action elements.
|
||||||
|
- **FR-002**: Neutral-interactive hover token MUST be used for all editable data fields: combatant name, initiative value, HP display, AC display, and condition tags.
|
||||||
|
- **FR-003**: Primary-action hover token MUST be used for navigation and additive action elements: Add Combatant button, step forward button, step back button, and Roll All button.
|
||||||
|
- **FR-004**: Destructive-action hover token MUST be used for removal and clearing actions: remove combatant button and clear encounter button.
|
||||||
|
- **FR-005**: Components MUST reference the semantic tokens via theme-aware classes, not hardcoded color values.
|
||||||
|
- **FR-006**: Changing a token value in the theme file MUST propagate to all components using that token without any other code changes.
|
||||||
|
- **FR-007**: The existing amber one-off hover color MUST be replaced with the appropriate semantic token.
|
||||||
|
- **FR-008**: Where components use hover background colors (e.g., condition picker items, navigation buttons), the semantic tokens MUST also provide background hover variants.
|
||||||
|
|
||||||
|
### Key Entities
|
||||||
|
|
||||||
|
- **Hover Token**: A CSS custom property defining a semantic hover color. Has a name (e.g., neutral-interactive), a text color value, and optionally a background color value.
|
||||||
|
- **Interaction Tier**: A classification of interactive elements by intent — neutral-interactive (data editing), primary-action (navigation/creation), or destructive-action (removal/clearing).
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: All hover colors across the five affected components are controlled by exactly three semantic tokens — zero hardcoded hover color values remain in those components.
|
||||||
|
- **SC-002**: Changing any single hover token value updates every element in its tier — verified by modifying each token and confirming all associated elements reflect the change.
|
||||||
|
- **SC-003**: Users perceive three distinct hover color tiers when interacting with the app — neutral editing feels understated (grey-to-white), primary actions feel prominent (blue), and destructive actions feel cautionary (red).
|
||||||
|
- **SC-004**: No visual regressions in hover behavior — all elements that had hover states before the change still have hover states after.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- The three-tier system (neutral, primary, destructive) is sufficient for all current interactive elements. MVP baseline does not include additional tiers (e.g., warning, success).
|
||||||
|
- Background hover variants are needed only where components currently use background hover (condition-picker, navigation buttons). Text-only hover is the default.
|
||||||
|
- The default token values match the described colors: neutral = grey-to-white (#e2e8f0 or similar), primary = blue (#3b82f6), destructive = red (#ef4444).
|
||||||
|
- This feature is purely presentational — no domain or application layer changes are needed.
|
||||||
132
specs/028-semantic-hover-tokens/tasks.md
Normal file
132
specs/028-semantic-hover-tokens/tasks.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Tasks: Semantic Hover Tokens
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/028-semantic-hover-tokens/`
|
||||||
|
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md
|
||||||
|
|
||||||
|
**Tests**: No test tasks — feature is purely presentational with manual visual verification.
|
||||||
|
|
||||||
|
**Organization**: Tasks grouped by user story. US1 and US2 are both P1 and share implementation work (defining tokens + migrating components), so they are combined into one phase.
|
||||||
|
|
||||||
|
## Format: `[ID] [P?] [Story] Description`
|
||||||
|
|
||||||
|
- **[P]**: Can run in parallel (different files, no dependencies)
|
||||||
|
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
|
||||||
|
- Include exact file paths in descriptions
|
||||||
|
|
||||||
|
## Phase 1: Setup (Theme Token Definitions)
|
||||||
|
|
||||||
|
**Purpose**: Define the six semantic hover tokens in the theme
|
||||||
|
|
||||||
|
- [x] T001 Add six `--color-hover-*` CSS custom properties to the `@theme` block in `apps/web/src/index.css`: `--color-hover-neutral: var(--color-foreground)`, `--color-hover-action: var(--color-primary)`, `--color-hover-destructive: var(--color-destructive)`, `--color-hover-neutral-bg: var(--color-card)`, `--color-hover-action-bg: var(--color-muted)`, `--color-hover-destructive-bg: transparent`
|
||||||
|
|
||||||
|
**Checkpoint**: Tokens defined — Tailwind now generates `text-hover-neutral`, `bg-hover-neutral-bg`, etc. utility classes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: User Story 1 + User Story 2 — Token Migration (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Migrate all hover color references in affected components from hardcoded values to the semantic tokens, ensuring correct tier assignment per element
|
||||||
|
|
||||||
|
**Independent Test**: Hover over every interactive element and verify: editable fields show grey-to-white (neutral), navigation/add buttons show blue (action), remove/clear buttons show red (destructive). Then change each token value in DevTools and confirm all elements in that tier update.
|
||||||
|
|
||||||
|
### Neutral-Interactive Tier
|
||||||
|
|
||||||
|
- [x] T002 [P] [US1] Migrate combatant-row.tsx neutral hovers: replace 5× `hover:text-primary` with `hover:text-hover-neutral` on EditableName, MaxHpDisplay, ClickableHp, initiative d20 button, and initiative value button in `apps/web/src/components/combatant-row.tsx`
|
||||||
|
- [x] T002b [P] [US1] Migrate ac-shield.tsx neutral hover: replace `hover:text-primary` with `hover:text-hover-neutral` on the AC shield button in `apps/web/src/components/ac-shield.tsx`
|
||||||
|
- [x] T003 [P] [US1] Migrate condition-tags.tsx neutral hovers: replace `hover:bg-card` with `hover:bg-hover-neutral-bg` on condition icon buttons, and replace `hover:text-foreground hover:bg-card` with `hover:text-hover-neutral hover:bg-hover-neutral-bg` on the add condition (+) button in `apps/web/src/components/condition-tags.tsx`
|
||||||
|
- [x] T004 [P] [US1] Migrate condition-picker.tsx neutral hover: replace `hover:bg-card` with `hover:bg-hover-neutral-bg` on condition row items in `apps/web/src/components/condition-picker.tsx`
|
||||||
|
- [x] T005 [P] [US1] Migrate action-bar.tsx neutral hover: replace `hover:bg-accent/10` with `hover:bg-hover-neutral-bg` on bestiary search result items in `apps/web/src/components/action-bar.tsx`
|
||||||
|
- [x] T006 [P] [US1] Migrate stat-block-panel.tsx neutral hovers: replace 2× `hover:text-foreground` with `hover:text-hover-neutral` on the desktop close button and mobile close button in `apps/web/src/components/stat-block-panel.tsx`
|
||||||
|
- [x] T007 [P] [US1] Migrate bestiary-search.tsx neutral hovers: replace `hover:text-foreground` with `hover:text-hover-neutral` on the close button, and replace `hover:bg-accent/10` with `hover:bg-hover-neutral-bg` on search result items in `apps/web/src/components/bestiary-search.tsx`
|
||||||
|
|
||||||
|
### Primary-Action Tier
|
||||||
|
|
||||||
|
- [x] T008 [P] [US2] Migrate turn-navigation.tsx action hovers: replace `hover:text-primary hover:border-primary hover:bg-muted` with `hover:text-hover-action hover:border-hover-action hover:bg-hover-action-bg` on Previous/Next buttons, and replace `hover:text-primary` with `hover:text-hover-action` on the Roll All button in `apps/web/src/components/turn-navigation.tsx`
|
||||||
|
|
||||||
|
### Destructive-Action Tier
|
||||||
|
|
||||||
|
- [x] T009 [P] [US2] Migrate combatant-row.tsx destructive hover: replace `hover:text-destructive` with `hover:text-hover-destructive` on the remove button (X) in `apps/web/src/components/combatant-row.tsx`
|
||||||
|
- [x] T010 [US2] Migrate turn-navigation.tsx destructive hover: replace `hover:text-destructive` with `hover:text-hover-destructive` on the Clear button in `apps/web/src/components/turn-navigation.tsx`
|
||||||
|
|
||||||
|
**Checkpoint**: All components use semantic hover tokens. Hovering shows correct tier colors. Changing any token in DevTools updates all elements in that tier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 3 — Eliminate One-Off Hover Colors (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Verify no hardcoded hover color values remain in the affected components and eliminate any one-offs (e.g., amber)
|
||||||
|
|
||||||
|
**Independent Test**: Search all affected component files for `hover:text-` and `hover:bg-` classes that don't reference `hover-neutral`, `hover-action`, or `hover-destructive` tokens. Zero matches expected.
|
||||||
|
|
||||||
|
- [x] T011 [US3] Audit all affected components for remaining hardcoded hover colors: grep for `hover:text-` and `hover:bg-` patterns in `apps/web/src/components/{combatant-row,condition-tags,condition-picker,turn-navigation,action-bar,stat-block-panel,bestiary-search}.tsx` and replace any remaining non-token hover colors (including any amber one-off) with the appropriate semantic token
|
||||||
|
|
||||||
|
**Checkpoint**: Zero hardcoded hover color values in affected components
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Validate everything passes and no regressions
|
||||||
|
|
||||||
|
- [x] T012 Run `pnpm check` to verify all linting, formatting, type checking, and tests pass
|
||||||
|
- [ ] T013 Visual regression check: start dev server with `pnpm --filter web dev` and manually verify all hover states per quickstart.md scenarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Setup (Phase 1)**: No dependencies — start immediately
|
||||||
|
- **US1+US2 (Phase 2)**: Depends on T001 (tokens must exist before components can reference them)
|
||||||
|
- **US3 (Phase 3)**: Depends on Phase 2 completion (audit after migration)
|
||||||
|
- **Polish (Phase 4)**: Depends on all previous phases
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: Needs tokens defined (T001), then component migrations (T002–T007)
|
||||||
|
- **US2 (P1)**: Needs tokens defined (T001), then component migrations (T008–T010). Can run in parallel with US1 tasks.
|
||||||
|
- **US3 (P2)**: Depends on US1+US2 completion — audit step
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
- T002–T010 are all [P] — different files, no dependencies between them. All can run in parallel after T001.
|
||||||
|
- T008+T010 both modify turn-navigation.tsx but are sequential (T008 handles action tier, T010 handles destructive tier in the same file).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: Phase 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# After T001 completes, launch all component migrations in parallel:
|
||||||
|
Task: "T002 - combatant-row neutral hovers"
|
||||||
|
Task: "T002b - ac-shield neutral hover"
|
||||||
|
Task: "T003 - condition-tags neutral hovers"
|
||||||
|
Task: "T004 - condition-picker neutral hover"
|
||||||
|
Task: "T005 - action-bar neutral hover"
|
||||||
|
Task: "T006 - stat-block-panel neutral hover"
|
||||||
|
Task: "T007 - bestiary-search neutral hovers"
|
||||||
|
Task: "T008 - turn-navigation action hovers"
|
||||||
|
Task: "T009 - combatant-row destructive hover"
|
||||||
|
# Then T010 after T008 (same file: turn-navigation.tsx)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (US1 + US2)
|
||||||
|
|
||||||
|
1. Complete Phase 1: Define tokens (T001)
|
||||||
|
2. Complete Phase 2: Migrate all components (T002–T010)
|
||||||
|
3. **STOP and VALIDATE**: Verify all hover tiers visually
|
||||||
|
4. Complete Phase 3: Audit for remaining hardcoded hovers (T011)
|
||||||
|
5. Complete Phase 4: Run checks and visual regression
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- [P] tasks = different files, no dependencies
|
||||||
|
- T008 and T010 both touch turn-navigation.tsx — run T010 after T008
|
||||||
|
- T002 and T009 both touch combatant-row.tsx — run T009 after T002
|
||||||
|
- Commit after each phase or logical group
|
||||||
|
- No domain/application layer changes needed
|
||||||
Reference in New Issue
Block a user