Period picker
Selector de períodos para series temporales (períodos de planilla, meses fiscales, ciclos). Resuelve el problema que un typeahead no puede: la selección es temporal/estructural, no textual — nadie digita "la quincena pasada". La lista de un <select> nativo además crece linealmente con la antigüedad de los datos (una empresa quincenal acumula 24 períodos por año).
El DS distribuye solo el patrón visual: las clases .cl-period-picker* y el contrato de markup/ARIA de abajo. El comportamiento (navegación, teclado, estado ARIA) lo aporta la app consumidora con su propio JS.
Degradación sin JS
Renderizá server-side un <select class="cl-input"> nativo agrupado por mes dentro del wrapper y dejá que el JS de la app lo reemplace al conectar: sin JS el select agrupado sigue funcionando. El select queda oculto pero en el DOM (es el control que viaja en el form y recibe change); la barra y el panel los construye el JS. Cada <option> lleva la metadata que el JS necesita como data-attributes.
<div class="cl-period-picker">
<select class="cl-input" name="pay_period_id">
<option value="">Seleccione un período</option>
<optgroup label="Julio 2026">
<option value="42" data-start-date="2026-07-01" data-end-date="2026-07-15"
data-status="borrador" data-status-label="Borrador"
data-payment-date="15/07/2026">01–15 · Quincenal</option>
</optgroup>
</select>
</div>
Anatomía (post-enhancement)
Una barra [◀] [trigger ▾] [▶] y un panel popover (role="dialog"). Los steppers mueven la selección un período hacia atrás/adelante cronológicamente (deshabilitados en los extremos). El trigger muestra la selección vigente (etiqueta + badge de estado) y abre el panel: accesos rápidos, chips de año, grilla de meses 4×3 (los meses sin períodos deshabilitados) y la lista de períodos del mes con metadata secundaria. El demo muestra el estado abierto congelado (en la app el panel va position: fixed).
<div class="cl-period-picker">
<select class="cl-input" name="pay_period_id" hidden></select>
<div class="cl-period-picker-bar">
<button type="button" class="cl-period-picker-step" aria-label="Período anterior">‹</button>
<button type="button" class="cl-period-picker-trigger"
aria-haspopup="dialog" aria-expanded="true">
<span class="cl-period-picker-value">01–15 · Quincenal — Julio 2026</span>
<span class="cl-badge cl-badge--warning">Borrador</span>
<span class="cl-period-picker-caret">▾</span>
</button>
<button type="button" class="cl-period-picker-step" aria-label="Período siguiente" disabled>›</button>
</div>
<div class="cl-period-picker-panel" role="dialog" aria-label="Seleccionar período de pago">
<div class="cl-period-picker-quick">
<button type="button" class="cl-btn cl-btn--secondary cl-btn--sm">Período actual</button>
<button type="button" class="cl-btn cl-btn--secondary cl-btn--sm">Anterior</button>
</div>
<div class="cl-period-picker-years" role="group" aria-label="Año">
<button type="button" class="cl-period-picker-year">2025</button>
<button type="button" class="cl-period-picker-year is-active">2026</button>
</div>
<div class="cl-period-picker-months" role="group" aria-label="Mes">
<button type="button" class="cl-period-picker-month">Ene</button>
<!-- … -->
<button type="button" class="cl-period-picker-month is-active">Jul</button>
<button type="button" class="cl-period-picker-month" disabled>Ago</button>
</div>
<ul class="cl-period-picker-list">
<li>
<button type="button" class="cl-period-picker-option is-selected" aria-current="true">
<span>01–15 · Quincenal</span>
<span class="cl-period-picker-option-meta">Borrador · pago 15/07/2026</span>
</button>
</li>
</ul>
</div>
</div>
Estado vacío
Sin períodos disponibles, el panel muestra una única fila no interactiva con .cl-period-picker-empty.
No hay períodos disponibles
<div class="cl-period-picker-panel" role="dialog">
<p class="cl-period-picker-empty">No hay períodos disponibles</p>
</div>
Contrato de comportamiento (lo implementa la app)
- Steppers:
◀/▶seleccionan el período cronológicamente anterior/siguiente y despachanchangesobre el select oculto; deshabilitados en los extremos. Desde el prompt vacío, ambos aterrizan en el período "actual". - Trigger: click abre/cierra el panel;
↓lo abre.aria-expandedrefleja el estado. El trigger hereda eliddel select para conservar el<label for>. - Panel:
role="dialog"posicionadoposition: fixeddesde la barra (escapaoverflow: clipde ancestros como.cl-card, con flip hacia arriba si no hay espacio abajo; cerrar al scrollear fuera).Escapecierra y devuelve el foco al trigger;↓/↑mueven el foco entre las filas de período; todos los controles son<button>reales (Tab navega, Enter/Espacio activan). - Accesos rápidos: "Período actual" = el más recientemente iniciado a hoy; "Anterior" = el cronológicamente previo. Ocultos cuando no existen.
- Default inteligente (server-side): preseleccioná el período actual cuando no venga selección explícita.
- Click fuera cierra el panel; al seleccionar, sincronizá el
<select>oculto y despacháchangesobre él.