feat(kanban): Seleccionar Aleatorio en columna con ruleta (issue 0090)

Adds a column-level "Seleccionar Aleatorio" context menu entry that picks
a card at random from the column with a roulette-style highlight
animation that decelerates to the winner.

Selection respects existing filters (uses cardsByColumn which is the
post-filter view) and always excludes locked cards. Menu entry is
disabled when nothing pickable is left.

Implementation:
- KanbanColumn: new Menu.Item with IconDice5; data-test selector for e2e.
- onPickRandom prop wired from KanbanColumn -> App.
- handlePickRandom in App.tsx: cryptographically random winner via
  crypto.getRandomValues, 2 full laps + offset, cubic decay 50ms -> 220ms,
  follows the active card with scrollIntoView.
- src/styles/roulette.css: .kanban-roulette-active (blue pulse, single
  step) and .kanban-roulette-winner (green pulse + scale, ~1.6s).
  Imported globally from main.tsx.

Manual verification only (visual timing + needs real cards). Backend
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 13:27:39 +02:00
parent 9f4fd85db3
commit c93ac46c37
8 changed files with 1290 additions and 1179 deletions
+11
View File
@@ -26,6 +26,7 @@ import {
IconChevronDown,
IconChevronRight,
IconClock,
IconDice5,
IconDotsVertical,
IconGripVertical,
IconPencil,
@@ -49,6 +50,7 @@ interface Props {
onDeleteColumn: (id: string) => void;
onSetWIPLimit: (id: string, limit: number) => void;
onSetMaxTimeMinutes: (id: string, minutes: number) => void;
onPickRandom: (columnId: string) => void;
onToggleDone: (id: string, is_done: boolean) => void;
onEditCard: (card: Card) => void;
onDeleteCard: (id: string) => void;
@@ -83,6 +85,7 @@ function KanbanColumnImpl({
onDeleteColumn,
onSetWIPLimit,
onSetMaxTimeMinutes,
onPickRandom,
onToggleDone,
onEditCard,
onDeleteCard,
@@ -411,6 +414,14 @@ function KanbanColumnImpl({
Tiempo maximo
{column.max_time_minutes > 0 ? ` (${column.max_time_minutes} min)` : ""}
</Menu.Item>
<Menu.Item
leftSection={<IconDice5 size={14} />}
data-test="column-random-pick"
disabled={cards.filter((c) => !c.locked).length === 0}
onClick={() => onPickRandom(column.id)}
>
Seleccionar Aleatorio
</Menu.Item>
<Menu.Item
leftSection={<ArchiveIcon size={14} />}
onClick={() => onMoveColumnLocation(column.id, isInSidebar ? "board" : "sidebar")}