# GFIBER-671 — Botones de scroll izquierda/derecha en el Attendance Grid
## Context
**Ticket:** GFIBER-671 (High, To Do, asignado a Alejandro Lembke) — *"Add buttons to move the attendance calendar to the right or to the left (like scrolling)"*. Surgió en el daily del 2026-07-02: *"The scroll bar doesn't get displayed for some users, so we need something else to enable them to navigate the attendance calendar."*
El mismo día se cerró **GFIBER-663** (PR #89 → `dev`), que atacó el síntoma raíz por CSS: en Windows con "Automatically hide scroll bars" (activado por defecto en muchas configuraciones), el scrollbar horizontal del `AttendanceGrid` nunca se renderizaba como barra persistente/arrastrable para usuarios de mouse en desktop. El fix fue `scrollbar-width`/`scrollbar-color` + `::-webkit-scrollbar*` sobre el contenedor `.scroll`, forzando un scrollbar clásico siempre visible.
GFIBER-671 es el fast-follow: incluso con un scrollbar visible, seguir dependiendo *exclusivamente* de arrastrar una barra de 10px es una interacción frágil (precisión de mouse, descubribilidad, accesibilidad motora). El pedido es un control explícito e independiente del navegador — flechas ‹ › que desplazan la grilla — como refuerzo, no como reemplazo del scrollbar ya corregido.
**Resultado buscado:** dos botones flecha (izquierda/derecha) superpuestos sobre el borde del área scrolleable del `AttendanceGrid`, que aparecen solo cuando hay contenido para desplazar, se deshabilitan en los extremos, y desplazan la grilla con un click — sin tocar persistencia, Server Actions, ni el modelo de datos (es una mejora puramente de interacción de UI).
## Investigación previa
Confirmado leyendo el código real (no solo la documentación de Docmost):
- **Componente objetivo:** `packages/design-system/src/components/AttendanceGrid/index.tsx` — component plano `"use client"`, sin refs ni estado hoy (100% driven by props). El wrapper de la app, `components/dashboard/AttendanceGrid.tsx`, **no necesita cambios**: no usa el prop `monthLabel`/`onPrevMonth`/`onNextMonth` del DS (tiene su propio header de navegación de mes fuera de este componente) y solo envuelve el render del DS en un `
` para capturar `onPointerDown` del status-picker — no posee ref al contenedor de scroll. Todo el trabajo va **dentro del DS**, consistente con la convención DS-first del proyecto (ningún control nuevo va inline en la app).
- **Contenedor de scroll:** `
` (index.tsx:125) — ya tiene `position: relative` (style.module.scss:58), heredado del fix de GFIBER-663. Esto es clave: **no hace falta reestructurar CSS** para tener un contexto de posicionamiento — los botones pueden ser `position: absolute` hijos directos de `.scroll`, igual que ya hace `.scrollHint` (style.module.scss:232-240, right-edge fade decorativo). Un elemento `position: absolute` cuyo *containing block* es el propio elemento con `overflow-x: auto` **no se desplaza con el scroll** (se posiciona respecto al padding-box estático del contenedor, no respecto al contenido que se desplaza) — es el mismo mecanismo que ya usa `.scrollHint` para quedar fijo en el borde derecho.
- **Columnas sticky a esquivar:** `.agentCell`/`.agentHead` (ancho 200px, sticky `left:0`) y, si `showSummary` está activo (default `true`), `.summaryCell`/`.summaryHead` (ancho 88px adicional, sticky `left:200px`). El botón **izquierdo no debe ir en `left:0`** (taparía el nombre del agente) — debe ir en el borde donde terminan las columnas congeladas: `left: 200px` (sin summary) o `left: 288px` (con summary). El botón derecho va en `right: 0`, en el mismo slot que hoy ocupa `.scrollHint`.
- **Estilo a reutilizar (decisión confirmada con el usuario):** `.navBtn` (style.module.scss:30-47) — círculo 28px, `‹`/`›` unicode, hover `--fiber-gray-100`, focus-visible `--fiber-blue`. Hoy este estilo solo lo usa el header de mes opcional del propio componente (index.tsx:107-123), que la app no consume — es decir, **existe y está validado visualmente pero no tiene ningún consumidor real todavía**. Reutilizarlo mantiene consistencia visual con la identidad ya definida en Penpot para este componente y cumple el mínimo de WCAG 2.2 SC 2.5.8 (24×24px). Se le suma `box-shadow` + fondo sólido (`--fiber-surface`) para que se lea flotando sobre celdas de cualquier color (weekend, today, status tonos).
- **Sin precedente de teclado Left/Right:** el único roving-focus del repo usa `ArrowUp`/`ArrowDown` (Picker en `components/dashboard/AttendanceGrid.tsx:229-247`, para el menú de status). No hay ningún patrón `ArrowLeft`/`ArrowRight` existente — no es necesario inventarlo para este ticket (los botones son suficientes; el foco natural por Tab ya los hace accesibles por teclado).
- **DataTable/UserTable comparten el mismo `overflow-x:auto` sin botones** — confirmado, fuera de alcance de este ticket (ya está anotado como deuda fast-follow desde GFIBER-663).
## Approach
Todo el cambio vive en `packages/design-system/src/components/AttendanceGrid/`. Component sigue siendo controlado por props para todo lo existente; se le agrega estado *interno* puramente de presentación (no se expone a la app):
1. `useRef` sobre el `
`.
2. `useState` con `{ canLeft: boolean, canRight: boolean }`. Un `useEffect` calcula el estado inicial al montar/cuando cambian `agents`/`days` (el ancho de la tabla puede cambiar de mes a mes) comparando `scrollWidth` vs `clientWidth` del ref; si no hay overflow (`scrollWidth <= clientWidth`), ambos botones se **ocultan por completo** (no solo deshabilitan) — no tiene sentido mostrar controles de scroll cuando no hay nada que desplazar.
3. Listener de `scroll` sobre el propio contenedor (recalcula `canLeft`/`canRight` en cada evento) + `ResizeObserver` sobre el contenedor (recalcula si el usuario resizea la ventana o cambia el layout del dashboard). Ambos se limpian en el cleanup del `useEffect`.
4. Dos `