@higinio/design-mcp 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -313,7 +313,7 @@ Export propio (extraído del estado loading de Button). SVG 16px default: círcu
313
313
 
314
314
  ### 10.5 Textarea
315
315
 
316
- Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px. Contador de caracteres `text-xs` `--text-disabled` alineado a la derecha.
316
+ Mismos estados que input. Min-height 96px, `resize: vertical`, padding 12px 14px. Contador de caracteres (`.textarea-counter`) `text-xs` `--text-muted` alineado a la derecha.
317
317
 
318
318
  ### 10.6 Select
319
319
 
@@ -387,7 +387,7 @@ Banner de estado **inline y persistente** (vs Toast, transitorio): errores de fo
387
387
 
388
388
  - 4 tonos: fondo suave semántico (`*-100`), radius `md`, padding `14px 16px`.
389
389
  - Icono semántico 20px en `*-500`: info→`info` · success→`circle-check` · warning→`triangle-alert` · danger→`circle-alert`.
390
- - Título `text-sm` semibold y cuerpo `text-sm` en `--*-text` (cuerpo con opacidad 0.92).
390
+ - Título `text-sm` semibold y cuerpo `text-sm` en `--*-text`, a contraste completo (sin opacidad).
391
391
  - Botón cerrar opcional (× 16px en el color del tono) vía `onDismiss`. `role="status"`.
392
392
 
393
393
  ### 10.17 Toast
@@ -456,6 +456,26 @@ Tile de KPI único (componer dentro de `Card` para métricas de dashboard).
456
456
  - Valor grande en `--font-mono text-3xl` semibold `--text-primary` (tracking -0.01em, line-height 1).
457
457
  - Delta opcional en baseline (gap 10px): `text-sm` semibold con flecha 14px (`trending-up`/`trending-down`/`minus`). Tono derivado del signo (− → danger, resto success) u override `deltaTone`; colores del delta en `--*-text` (no los `-500`).
458
458
 
459
+ #### Chip de métrica
460
+
461
+ Pastilla de una línea con rótulo y valor —por ejemplo, «Agua **3 de 8**»— para métricas secundarias que no merecen un Stat completo.
462
+
463
+ - **`.metric-chip`:** radius `full`, borde `1px --border`, fondo `--surface`, padding `4px 12px`, `text-sm` `--text-secondary`; icono opcional de 16px en `--text-muted`.
464
+ - **Contenido:** `.metric-chip__label` y `.metric-chip__value`; el valor va semibold en `--link` con `tabular-nums`.
465
+ - **Fila:** `.metric-chips` agrupa los chips con `display: flex; flex-wrap: wrap; gap: var(--space-2)`, así que saltan de línea en móvil.
466
+ - **Interactivo** (por ejemplo, «sumar un vaso»): `button.metric-chip`, con `min-height: var(--control-height-md)` (44px) y padding horizontal de 16px. Hover: fondo `--accent-soft` y borde `--accent`. Active: fondo `--accent-soft-strong` y `scale(.98)`. Foco de teclado con el anillo del sistema (outline 2px `--accent`, offset 2px).
467
+
468
+ ```html
469
+ <div class="metric-chips">
470
+ <span class="metric-chip"><span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span></span>
471
+ <button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso">
472
+ <span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span>
473
+ </button>
474
+ </div>
475
+ ```
476
+
477
+ **Accesibilidad:** el chip interactivo es un `<button>` con nombre accesible completo, no solo la cifra («Agua: 3 de 8 vasos. Sumar un vaso»); el nombre empieza por el texto visible (WCAG 2.5.3) y se actualiza con el valor. El chip informativo es texto: no lleva rol ni foco.
478
+
459
479
  ### 10.23 Pagination
460
480
 
461
481
  Navegador de páginas 1-indexado, bajo una `Table`.
@@ -472,6 +492,37 @@ Navegador de páginas 1-indexado, bajo una `Table`.
472
492
  - **Tabs:** borde inferior `--border`; tab activa con indicador 2px `--accent` que se desliza entre tabs (250ms).
473
493
  - **Breadcrumbs:** `text-sm` `--text-muted`, separador `/` (tipográfico), último ítem `--text-primary`.
474
494
 
495
+ #### Sidebar contraíble
496
+
497
+ Variante del Sidebar para apps con muchas secciones: alterna entre la lista con rótulos y un riel de iconos que deja más ancho al contenido.
498
+
499
+ - **Estados:** expandido, 260px (el ancho del Sidebar), y contraído, riel de iconos de 64px. Clases: `.sidebar.sidebar--collapsible` y, contraído, `.is-collapsed`. Cada ítem lleva su icono de 18px y el nombre en `.sidebar__label`.
500
+ - **Grupos con rótulo:** `.sidebar__group` con un `.sidebar__group-label` (`text-xs` semibold en mayúsculas, tracking `--tracking-label`, `--text-muted`); 16px entre grupos. En el riel, cada rótulo se convierte en un separador de 1px `--border` y el del primer grupo se oculta.
501
+ - **Ítem activo:** `.is-active` (fondo `--accent-soft-strong`, texto `--link`) más `aria-current="page"`.
502
+ - **Botón de contraer:** `.sidebar__item.sidebar__toggle` al pie (`margin-top: auto`), con un chevron que apunta hacia donde se moverá el borde (rota 180° en el riel) y el texto «Contraer» / «Expandir» en su `.sidebar__label`.
503
+ - **Tooltip en el riel:** el `.sidebar__label` queda oculto a la vista pero accesible (patrón de `.u-visually-hidden`) y aparece a la derecha del ítem como tooltip (`--tooltip-bg`, `text-xs`, radius `sm`, delay 400ms) al pasar el cursor —solo con puntero real, §11.5— y al enfocar el ítem con teclado (`:focus-visible`). No es un `title`: el nombre sigue en el árbol de accesibilidad. El riel no recorta su contenido (sin `overflow`) para que el tooltip pueda salir de él.
504
+ - **Por debajo de 768px** (`md`), `.sidebar--collapsible` no se muestra fijo: lo sustituye un botón ☰ `.icon-btn.sidebar-trigger` que abre un Drawer `.drawer--left` (§10.29) con los mismos grupos e ítems. El framework muestra `.sidebar-trigger` solo por debajo de 768px y lo oculta (`display: none`) desde 768px, el mismo breakpoint en el que aparece el sidebar fijo; así el ☰ y el sidebar nunca conviven. El botón lleva `aria-label="Abrir navegación"`, `aria-controls` con el id del drawer y `aria-expanded` (`false` cerrado, `true` abierto).
505
+ - **Motion:** el ancho cambia sin transición —el layout usa `grid-template-columns: auto 1fr` y el contenido sigue al sidebar al instante—; nunca se anima `width`. Solo los rótulos se funden al expandir: `opacity` 0→1 y `translateX(-4px)`→0 en `--duration-fast` `--ease-out`. Con `prefers-reduced-motion`, sin animación (reset global, §11.5).
506
+
507
+ ```html
508
+ <nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal">
509
+ <div class="sidebar__group" role="group" aria-labelledby="navHoy">
510
+ <span class="sidebar__group-label" id="navHoy">Hoy</span>
511
+ <a class="sidebar__item is-active" href="/" aria-current="page"><svg aria-hidden="true">…</svg><span class="sidebar__label">Inicio</span></a>
512
+ </div>
513
+ <button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="appNav">
514
+ <svg aria-hidden="true">…</svg><span class="sidebar__label">Contraer</span>
515
+ </button>
516
+ </nav>
517
+
518
+ <!-- Solo por debajo de 768px -->
519
+ <button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
520
+ aria-expanded="false" aria-controls="navDrawer">…menu…</button>
521
+ <div class="drawer drawer--left" id="navDrawer">…los mismos grupos e ítems…</div>
522
+ ```
523
+
524
+ **Accesibilidad:** el contenedor es un `<nav>` con `aria-label`; el ítem de la página actual lleva `aria-current="page"` además de `.is-active`. El toggle es un `<button>` real con `aria-expanded` (`true` expandido, `false` en el riel) y `aria-controls` con el id del sidebar; al alternar, su texto cambia entre «Contraer» y «Expandir». El ☰ `.sidebar-trigger` también es un `<button>` con `aria-expanded` y `aria-controls` hacia el drawer, y el drawer sigue el contrato de foco del Modal. Los grupos se declaran con `role="group"` y `aria-labelledby` apuntando a su rótulo, que en el riel sigue siendo legible para el lector aunque se vea como una línea.
525
+
475
526
  ### 10.25 Skeleton y EmptyState
476
527
 
477
528
  - Skeleton: fondo `--surface-alt` con shimmer (gradiente que barre en 1.4s, loop). Radius igual al contenido que reemplaza.
@@ -541,10 +592,10 @@ Botones adyacentes con bordes compartidos para acciones relacionadas (barra de f
541
592
 
542
593
  Campo de fecha con un calendario en popover. Reusa el patrón visual del `Popover` (§10.30, panel `--surface` + `shadow-lg`, aparición desde el `transform-origin` del disparador) y la navegación por teclado del `Combobox` (§10.28).
543
594
 
544
- - **`.datepicker`** — contenedor relativo (`.is-open` lo abre). **`.datepicker__input`** con el look de `Input` (borde `--border-strong`, focus `--accent` + `--ring`), `readonly` + `aria-haspopup="dialog"` + `aria-expanded` + `aria-controls`; un `.datepicker__icon` de calendario 18px a la derecha (decorativo, `aria-hidden`).
595
+ - **`.datepicker`** — contenedor relativo (`.is-open` lo abre). **`.datepicker__input`** con el look de `Input` (borde `--border-strong`, focus `--accent` + `--ring`), `readonly` + `aria-haspopup="dialog"` + `aria-controls` (sin `aria-expanded`, que no es válido en un `input` de texto; el estado abierto lo da `.is-open`); un `.datepicker__icon` de calendario 18px a la derecha (decorativo, `aria-hidden`).
545
596
  - **`.datepicker__calendar`** — panel flotante `role="dialog"` con `aria-label`, mismo patrón que `.popover__panel`. Variante `.datepicker--right` ancla a la derecha.
546
- - **`.calendar`** — dentro del panel: `.calendar__header` (mes/año en `.calendar__title` + botones prev/next con `#i-chevron-left/right`), `.calendar__weekdays` (7 encabezados `role="columnheader"` con `aria-label` completo: "lunes"…), `.calendar__grid` (`role="grid"`), y `.calendar__day` (botón `role="gridcell"`, `aria-label` de la fecha completa vía `Intl.DateTimeFormat` y `aria-selected`). Semana empieza en lunes.
547
- - **Estados de día:** `.calendar__day--today` (semibold + anillo interior `--border-strong`), `.calendar__day--selected` (fondo `--accent`, texto `--text-on-primary`), `.calendar__day--outside` (días de otro mes, `--text-disabled`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
597
+ - **`.calendar`** — dentro del panel: `.calendar__header` (mes/año en `.calendar__title` + botones prev/next con `#i-chevron-left/right`), `.calendar__table` (`role="grid"`) que envuelve la fila de cabeceras `.calendar__weekdays` (`role="row"`, 7 encabezados `role="columnheader"` con `aria-label` completo: "lunes"…) y `.calendar__grid` (`role="rowgroup"`) con una `.calendar__week` (`role="row"`, `display: contents` para no romper la rejilla) por semana, y `.calendar__day` (botón `role="gridcell"`, `aria-label` de la fecha completa vía `Intl.DateTimeFormat` y `aria-selected`). Semana empieza en lunes.
598
+ - **Estados de día:** `.calendar__day--today` (semibold + anillo interior `--border-strong`), `.calendar__day--selected` (fondo `--accent`, texto `--text-on-primary`), `.calendar__day--outside` (días de otro mes, `--text-muted`), `[disabled]` (no seleccionable). El hover usa `--accent-soft`.
548
599
  - **Teclado y foco (como Popover):** el input abre por click (o Enter/Espacio/↓); al abrir, el foco entra al calendario (día enfocado). Dentro de la rejilla las **flechas** mueven el foco entre días (roving `tabindex`, cambiando de mes al cruzar el borde), Home/End saltan a los extremos de la semana, PageUp/PageDown cambian de mes, Enter/Espacio selecciona; **Esc** cierra y devuelve el foco al input. La fecha se formatea con `Intl` (`es-MX`), nunca a mano.
549
600
  - **Motion:** el panel aparece con fade + `scale(0.96→1)`, `--duration-base` `--ease-out`, bajo el techo `--duration-page`. Solo `transform`/`opacity`.
550
601
 
@@ -335,7 +335,7 @@
335
335
  .input-icon .input { padding-left: 40px; }
336
336
 
337
337
  .textarea { min-height: 96px; padding: 12px 14px; resize: vertical; line-height: var(--leading-base); }
338
- .textarea-counter { font-size: var(--text-xs); color: var(--text-disabled); text-align: right; }
338
+ .textarea-counter { font-size: var(--text-xs); color: var(--text-muted); text-align: right; }
339
339
 
340
340
  .select-wrap { position: relative; }
341
341
  .select { appearance: none; padding-right: 38px; cursor: pointer; }
@@ -634,7 +634,7 @@
634
634
  }
635
635
  .alert > svg { width: 20px; height: 20px; flex: none; margin-top: 1px; }
636
636
  .alert__title { font-weight: var(--weight-semibold); margin: 0 0 2px; }
637
- .alert__body { opacity: .92; margin: 0; }
637
+ .alert__body { margin: 0; }
638
638
  .alert__close {
639
639
  margin-left: auto; flex: none; display: grid; place-items: center;
640
640
  width: 24px; height: 24px; border: 0; border-radius: var(--radius-sm);
@@ -855,6 +855,32 @@
855
855
  .stat__delta--up { color: var(--success-text); }
856
856
  .stat__delta--down { color: var(--danger-text); }
857
857
 
858
+ /* Chip de métrica (§10.22): rótulo y valor en una pastilla de una línea */
859
+ .metric-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
860
+ .metric-chip {
861
+ display: inline-flex; align-items: center; gap: var(--space-2);
862
+ padding: var(--space-1) var(--space-3);
863
+ border: var(--border-width) solid var(--border); border-radius: var(--radius-full);
864
+ background: var(--surface);
865
+ font-family: var(--font-body); font-size: var(--text-sm); line-height: 1.5;
866
+ color: var(--text-secondary); white-space: nowrap;
867
+ }
868
+ .metric-chip svg { width: 16px; height: 16px; flex: none; color: var(--text-muted); }
869
+ .metric-chip__value {
870
+ font-weight: var(--weight-semibold); color: var(--link);
871
+ font-variant-numeric: tabular-nums;
872
+ }
873
+ button.metric-chip {
874
+ min-height: var(--control-height-md); padding-inline: var(--space-4);
875
+ cursor: pointer; text-align: left;
876
+ transition: background-color var(--duration-fast) var(--ease-out),
877
+ border-color var(--duration-fast) var(--ease-out),
878
+ transform var(--duration-instant) var(--ease-out);
879
+ }
880
+ button.metric-chip:hover { background: var(--accent-soft); border-color: var(--accent); }
881
+ button.metric-chip:active { background: var(--accent-soft-strong); transform: scale(.98); }
882
+ button.metric-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
883
+
858
884
  .pagination { display: flex; align-items: center; gap: 4px; list-style: none; padding: 0; margin: 0; }
859
885
  .pagination__btn {
860
886
  display: inline-grid; place-items: center;
@@ -903,7 +929,7 @@
903
929
  .sidebar__item {
904
930
  display: flex; align-items: center; gap: 10px;
905
931
  padding: 10px 12px; border-radius: var(--radius-md);
906
- font-size: var(--text-sm); font-weight: var(--weight-medium);
932
+ font-family: var(--font-body); font-size: var(--text-sm); font-weight: var(--weight-medium);
907
933
  color: var(--text-secondary); text-decoration: none; border: 0; background: transparent;
908
934
  cursor: pointer; text-align: left; width: 100%;
909
935
  transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
@@ -913,6 +939,68 @@
913
939
  .sidebar__item.is-active { background: var(--accent-soft-strong); color: var(--link); }
914
940
  .sidebar__item.is-active svg { color: var(--accent); }
915
941
 
942
+ /* Grupos con rótulo dentro del sidebar */
943
+ .sidebar__group { display: flex; flex-direction: column; gap: 2px; }
944
+ .sidebar__group + .sidebar__group { margin-top: var(--space-4); }
945
+ .sidebar__group-label {
946
+ padding: 0 12px var(--space-1);
947
+ font-size: var(--text-xs); font-weight: var(--weight-semibold);
948
+ text-transform: uppercase; letter-spacing: var(--tracking-label);
949
+ color: var(--text-muted); white-space: nowrap;
950
+ }
951
+ .sidebar__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
952
+
953
+ /* Sidebar contraíble (§10.24): expandido 260 px, riel de iconos de 64 px.
954
+ El ancho cambia sin transición; solo los rótulos se funden con opacity y transform. */
955
+ .sidebar--collapsible .sidebar__label {
956
+ transition: opacity var(--duration-fast) var(--ease-out),
957
+ transform var(--duration-fast) var(--ease-out);
958
+ }
959
+ .sidebar__toggle { margin-top: auto; }
960
+ .sidebar.is-collapsed { width: 64px; padding-inline: var(--space-3); }
961
+ .sidebar.is-collapsed .sidebar__item { position: relative; justify-content: center; padding-inline: 0; }
962
+ .sidebar.is-collapsed .sidebar__toggle svg { transform: rotate(180deg); }
963
+ .sidebar.is-collapsed .sidebar__group + .sidebar__group { margin-top: var(--space-2); }
964
+ /* En el riel, el rótulo de grupo es un separador de 1 px; su texto sigue en el árbol de accesibilidad */
965
+ .sidebar.is-collapsed .sidebar__group-label {
966
+ height: 1px; padding: 0; margin: 0 var(--space-2) var(--space-2);
967
+ overflow: hidden; color: transparent; background: var(--border);
968
+ }
969
+ /* Oculto a la vista pero accesible (patrón de .u-visually-hidden) */
970
+ .sidebar.is-collapsed .sidebar__label,
971
+ .sidebar.is-collapsed .sidebar__group:first-child > .sidebar__group-label {
972
+ position: absolute; width: 1px; height: 1px; margin: -1px;
973
+ clip-path: inset(50%); overflow: hidden; white-space: nowrap;
974
+ }
975
+ .sidebar.is-collapsed .sidebar__label {
976
+ border-radius: var(--radius-sm); background: var(--tooltip-bg); color: var(--tooltip-text);
977
+ font-size: var(--text-xs); line-height: 16px;
978
+ pointer-events: none; z-index: 70;
979
+ opacity: 0; transform: translateX(-4px);
980
+ }
981
+ /* Tooltip del riel: el propio rótulo aparece a la derecha del ítem, también con teclado */
982
+ .sidebar.is-collapsed .sidebar__item:focus-visible .sidebar__label {
983
+ left: calc(100% + 8px); top: calc(50% - 14px); width: auto; height: auto;
984
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
985
+ opacity: 1; transform: none; transition-delay: 400ms;
986
+ }
987
+ /* Por hover solo con puntero real — en touch quedaría pegado tras el tap */
988
+ @media (hover: hover) and (pointer: fine) {
989
+ .sidebar.is-collapsed .sidebar__item:hover .sidebar__label {
990
+ left: calc(100% + 8px); top: calc(50% - 14px); width: auto; height: auto;
991
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
992
+ opacity: 1; transform: none; transition-delay: 400ms;
993
+ }
994
+ }
995
+ /* Por debajo de 768 px la navegación vive en un Drawer --left (§10.29), que abre el botón ☰
996
+ .sidebar-trigger (combinado con .icon-btn); desde 768 px el sidebar es fijo y el botón sobra */
997
+ @media (max-width: 767px) {
998
+ .sidebar--collapsible { display: none; }
999
+ }
1000
+ @media (min-width: 768px) {
1001
+ .sidebar-trigger { display: none; }
1002
+ }
1003
+
916
1004
  .tabs { display: flex; gap: var(--space-6); border-bottom: var(--border-width) solid var(--border); }
917
1005
  .tabs__tab {
918
1006
  padding: 12px 4px; margin-bottom: -1px;
@@ -1128,6 +1216,7 @@
1128
1216
  .calendar__weekdays,
1129
1217
  .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
1130
1218
  .calendar__weekdays { margin-bottom: 4px; }
1219
+ .calendar__week { display: contents; }
1131
1220
  .calendar__weekday {
1132
1221
  display: flex; align-items: center; justify-content: center; height: 28px;
1133
1222
  font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-muted);
@@ -1142,7 +1231,7 @@
1142
1231
  color var(--duration-fast) var(--ease-out);
1143
1232
  }
1144
1233
  .calendar__day:hover:not(:disabled):not(.calendar__day--selected) { background: var(--accent-soft); }
1145
- .calendar__day--outside { color: var(--text-disabled); }
1234
+ .calendar__day--outside { color: var(--text-muted); }
1146
1235
  .calendar__day--today { font-weight: var(--weight-semibold); box-shadow: inset 0 0 0 1px var(--border-strong); }
1147
1236
  .calendar__day--selected { background: var(--accent); color: var(--text-on-primary); font-weight: var(--weight-semibold); }
1148
1237
  .calendar__day:disabled { color: var(--text-disabled); cursor: not-allowed; }