@higinio/design-mcp 0.1.0 → 0.1.1

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.
@@ -26,6 +26,7 @@
26
26
  /* -- Primitivos: acento -- */
27
27
  --accent-100: #d9f0fb; --accent-200: #a8ddf6; --accent-300: #7dccf1;
28
28
  --accent-400: #52baed; --accent-500: #2fa3dd; --accent-600: #1f87bb; --accent-700: #186a94;
29
+ --accent-800: #12506f;
29
30
 
30
31
  /* -- Primitivos: índigo -- */
31
32
  --indigo-300: #7a83c4; --indigo-400: #565fa3; --indigo-500: #353d7c;
@@ -34,6 +35,7 @@
34
35
  /* -- Primitivos: escala nocturna -- */
35
36
  --night-950: #010203; --night-900: #0b0d12; --night-800: #14171f;
36
37
  --night-700: #1e222d; --night-600: #2a2f3d; --night-500: #3d4354;
38
+ --night-450: #5f6575; /* texto atenuado en light */
37
39
  --night-400: #6b7183; --night-300: #9aa0b0; --night-200: #c5c9d4;
38
40
  --night-100: #e8eaef; --night-50: #f6f7f9;
39
41
 
@@ -156,27 +158,27 @@
156
158
  --border-strong: #b9c3d1;
157
159
  --text-primary: #14171f;
158
160
  --text-secondary: #3d4354;
159
- --text-muted: #6b7183;
161
+ --text-muted: #5f6575;
160
162
  --text-disabled: #9aa0b0;
161
163
  --text-heading: #0b0d12;
162
164
  --text-on-accent: #010203; /* el CTA no cambia */
163
- --success: #15803d; --success-bg: #e3f5ea;
164
- --warning: #a16207; --warning-bg: #fbf3dd;
165
- --danger: #c03535; --danger-bg: #fbe7e7;
166
- --info: #1f87bb; --info-bg: #e3f2fb;
165
+ --success: #166534; --success-bg: #e3f5ea;
166
+ --warning: #854d0e; --warning-bg: #fbf3dd;
167
+ --danger: #b02c2c; --danger-bg: #fbe7e7;
168
+ --info: #186a94; --info-bg: #e3f2fb;
167
169
  --accent-ghost-hover: rgba(31,135,187,.08);
168
170
  --accent-ghost-active: rgba(31,135,187,.14);
169
171
  --accent-selected: rgba(82,186,237,.12);
170
172
  --overlay: rgba(20,23,31,.45);
171
173
 
172
- --link: #1f87bb;
173
- --link-hover: #186a94;
174
- --focus-color: #1f87bb;
174
+ --link: #186a94;
175
+ --link-hover: #12506f;
176
+ --focus-color: #1f87bb; /* UI (4.01:1 ≥ 3), no texto */
175
177
  --border-hover: #b9c3d1;
176
178
  --outline-hover-bg: #eaf1f7;
177
179
  --outline-active-bg: #d8dfe8;
178
180
  --text-on-danger: #ffffff;
179
- --data-color: #1f87bb;
181
+ --data-color: #186a94; /* cifras y .progress__value: es texto */
180
182
  --elevated-border: #d8dfe8;
181
183
  --card-border: #d8dfe8;
182
184
  --code-color: #186a94;
@@ -387,6 +389,8 @@ a:hover { color: var(--link-hover); }
387
389
  ============================================================ */
388
390
  .field { display: flex; flex-direction: column; margin-bottom: var(--form-gap); }
389
391
  .field__label { font-size: var(--text-sm); font-weight: 500; color: var(--text-secondary); margin-bottom: 6px; }
392
+ /* Campo obligatorio: asterisco decorativo; la semántica va en required/aria-required del control */
393
+ .field--required > .field__label::after { content: "*"; margin-left: 2px; color: var(--danger); font-weight: 600; }
390
394
  .field__help { font-size: var(--text-sm); color: var(--text-muted); margin-top: 6px; }
391
395
  .field__error {
392
396
  display: inline-flex; align-items: center; gap: 6px;
@@ -530,6 +534,12 @@ a:hover { color: var(--link-hover); }
530
534
  margin-top: -2px;
531
535
  }
532
536
  .checkbox:checked::after { transform: rotate(-45deg) scale(1); }
537
+ /* Indeterminate: guion horizontal, no el check. Solo por JS (el.indeterminate = true) */
538
+ .checkbox:indeterminate { background: var(--accent-400); border-color: var(--accent-400); }
539
+ .checkbox:indeterminate::after {
540
+ content: ""; width: 10px; height: 2px; margin-top: 0;
541
+ border: 0; border-radius: var(--radius-full); background: var(--text-on-accent); transform: none;
542
+ }
533
543
  .radio::after {
534
544
  content: ""; width: 8px; height: 8px; border-radius: var(--radius-full);
535
545
  background: var(--text-on-accent); transform: scale(0);
@@ -613,6 +623,17 @@ a:hover { color: var(--link-hover); }
613
623
  .avatar__dot--away { background: var(--warning); }
614
624
  .avatar__dot--offline { background: var(--border); }
615
625
 
626
+ /* AvatarGroup — avatares solapados + chip +N.
627
+ Borde 2px del color del fondo (--surface-1 en tarjetas, --bg en el lienzo) para separar. */
628
+ .avatar-group { display: inline-flex; align-items: center; }
629
+ .avatar-group .avatar { border: 2px solid var(--surface-1); }
630
+ .avatar-group .avatar:not(:first-child) { margin-left: -8px; }
631
+ .avatar-group__more {
632
+ background: var(--surface-3); color: var(--text-secondary);
633
+ font-family: var(--font-body); font-weight: 600;
634
+ letter-spacing: 0; text-transform: none;
635
+ }
636
+
616
637
  /* ============================================================
617
638
  12B. DIVIDER — .divider (hairline), .divider--label, .divider--vertical
618
639
  ============================================================ */
@@ -820,9 +841,39 @@ a:hover { color: var(--link-hover); }
820
841
  }
821
842
  .table tbody tr { transition: background-color var(--duration-fast) var(--ease-out); }
822
843
  .table tbody tr:hover { background: var(--row-hover); }
823
- .table tbody tr.is-selected { background: var(--accent-selected); box-shadow: inset 2px 0 0 var(--accent-400); }
824
844
  .table .num, .table td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
825
845
 
846
+ /* Zebra — filas pares en surface-2; hover y selección mandan sobre la banda */
847
+ .table--zebra tbody tr:nth-child(even) { background: var(--surface-2); }
848
+ .table--zebra tbody tr:hover { background: var(--row-hover); }
849
+
850
+ /* Sticky header — dentro de un contenedor con overflow: auto y altura acotada */
851
+ .table--sticky thead th { position: sticky; top: 0; z-index: 1; }
852
+
853
+ /* Header ordenable — botón dentro del th + icono; el estado vive en aria-sort del th */
854
+ .th-sort {
855
+ display: inline-flex; align-items: center; gap: 6px;
856
+ margin: 0; padding: 0; border: 0; background: transparent;
857
+ font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;
858
+ cursor: pointer;
859
+ }
860
+ .th-sort:hover { color: var(--text-secondary); }
861
+ .th-sort:focus-visible { outline: var(--focus-ring); outline-offset: 2px; border-radius: var(--radius-sm); }
862
+ .sort-icon {
863
+ width: 14px; height: 14px; flex: none; opacity: .45;
864
+ transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out);
865
+ }
866
+ .table th[aria-sort="ascending"] .sort-icon,
867
+ .table th[aria-sort="descending"] .sort-icon { opacity: 1; color: var(--accent-400); }
868
+ .table th[aria-sort="descending"] .sort-icon { transform: rotate(180deg); }
869
+
870
+ /* Columna de selección (checkbox en th y en cada fila) */
871
+ .table .col-select { width: 44px; text-align: center; padding-left: var(--space-2); padding-right: var(--space-2); }
872
+
873
+ /* Fila seleccionada — .row--selected (contrato); .is-selected se mantiene como alias */
874
+ .table tbody tr.is-selected,
875
+ .table tbody tr.row--selected { background: var(--accent-selected); box-shadow: inset 2px 0 0 var(--accent-400); }
876
+
826
877
  /* ============================================================
827
878
  19. NAVEGACIÓN — .navbar, .sidebar, .tabs, .breadcrumbs
828
879
  ============================================================ */
@@ -1007,6 +1058,273 @@ a:hover { color: var(--link-hover); }
1007
1058
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
1008
1059
  }
1009
1060
 
1061
+ /* ============================================================
1062
+ 25. COMBOBOX / AUTOCOMPLETE — .combobox + SEARCH — .search
1063
+ El listado vive sobre --surface-2 (superficie de dropdowns).
1064
+ ============================================================ */
1065
+ .combobox { position: relative; }
1066
+ .combobox__input { width: 100%; } /* consume .input para el resto de estilos */
1067
+ .combobox__list {
1068
+ position: absolute; left: 0; right: 0; top: calc(100% + 4px); z-index: 40;
1069
+ margin: 0; padding: 6px; list-style: none; max-height: 264px; overflow: auto;
1070
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1071
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1072
+ }
1073
+ .combobox__list[hidden] { display: none; }
1074
+ .combobox__option {
1075
+ display: flex; align-items: center; gap: 10px; width: 100%;
1076
+ padding: 10px 12px; border-radius: var(--radius-sm);
1077
+ color: var(--text-primary); font-size: var(--text-sm); cursor: pointer;
1078
+ transition: background-color var(--duration-fast) var(--ease-out);
1079
+ }
1080
+ .combobox__option[hidden] { display: none; }
1081
+ /* .is-active = resaltado por teclado; [aria-selected] = la elegida (fondo + icono, no solo color) */
1082
+ .combobox__option.is-active { background: var(--surface-3); }
1083
+ .combobox__option[aria-selected="true"] { background: var(--accent-selected); color: var(--accent-300); }
1084
+ [data-theme="light"] .combobox__option[aria-selected="true"] { color: var(--link); }
1085
+ .combobox__check { display: none; margin-left: auto; width: 16px; height: 16px; color: var(--accent-400); flex: none; }
1086
+ .combobox__option[aria-selected="true"] .combobox__check { display: block; }
1087
+
1088
+ /* Search — wrapper con lupa (aria-hidden) + botón limpiar (×, aria-label="Limpiar") */
1089
+ .search { position: relative; display: flex; align-items: center; }
1090
+ .search__icon {
1091
+ position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
1092
+ width: 18px; height: 18px; color: var(--text-disabled); pointer-events: none;
1093
+ }
1094
+ .search__input { width: 100%; padding-left: 40px; padding-right: 40px; } /* consume .input */
1095
+ /* Oculta el clear nativo de WebKit para no duplicar el affordance del .search__clear */
1096
+ .search__input::-webkit-search-cancel-button,
1097
+ .search__input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
1098
+ .search__clear {
1099
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
1100
+ display: inline-grid; place-content: center; width: 28px; height: 28px; padding: 0;
1101
+ border: 0; border-radius: var(--radius-full); background: transparent;
1102
+ color: var(--text-muted); cursor: pointer;
1103
+ transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
1104
+ }
1105
+ .search__clear:hover { background: var(--surface-3); color: var(--text-primary); }
1106
+ .search__clear svg { width: 16px; height: 16px; }
1107
+ .search__clear[hidden] { display: none; }
1108
+
1109
+ /* ============================================================
1110
+ 26. DRAWER / SHEET — .drawer + __overlay + __panel (focus-trap por JS)
1111
+ Motion: transform translate + fade del overlay (--duration-slow).
1112
+ El overlay reutiliza --overlay del modal; SIN blur nuevo.
1113
+ ============================================================ */
1114
+ .drawer { position: fixed; inset: 0; z-index: 100; }
1115
+ .drawer[hidden] { display: none; }
1116
+ .drawer__overlay {
1117
+ position: absolute; inset: 0; background: var(--overlay);
1118
+ opacity: 0; transition: opacity var(--duration-slow) var(--ease-out);
1119
+ }
1120
+ .drawer.is-open .drawer__overlay { opacity: 1; }
1121
+ .drawer__panel {
1122
+ position: absolute; top: 0; right: 0; bottom: 0;
1123
+ display: flex; flex-direction: column;
1124
+ width: min(400px, 100%); padding: var(--card-padding);
1125
+ background: var(--surface-2); border-left: 1px solid var(--elevated-border);
1126
+ box-shadow: var(--shadow-3);
1127
+ overflow-y: auto; overscroll-behavior: contain;
1128
+ transform: translateX(100%);
1129
+ transition: transform var(--duration-slow) var(--ease-out);
1130
+ }
1131
+ .drawer.is-open .drawer__panel { transform: none; }
1132
+ /* Variante izquierda */
1133
+ .drawer--left .drawer__panel {
1134
+ right: auto; left: 0; border-left: 0; border-right: 1px solid var(--elevated-border);
1135
+ transform: translateX(-100%);
1136
+ }
1137
+ .drawer--left.is-open .drawer__panel { transform: none; }
1138
+ /* Variante inferior (sheet) */
1139
+ .drawer--bottom .drawer__panel {
1140
+ top: auto; left: 0; right: 0; width: 100%; max-height: 80dvh;
1141
+ border-left: 0; border-top: 1px solid var(--elevated-border);
1142
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1143
+ transform: translateY(100%);
1144
+ }
1145
+ .drawer--bottom.is-open .drawer__panel { transform: none; }
1146
+ .drawer__header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin-bottom: var(--space-4); }
1147
+ .drawer__title { font-family: var(--font-body); font-size: var(--text-xl); font-weight: 600; letter-spacing: normal; color: var(--text-heading); margin: 0; }
1148
+ .drawer__body { flex: 1; }
1149
+ .drawer__footer { display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: var(--space-6); }
1150
+
1151
+ /* ============================================================
1152
+ 27. POPOVER — .popover + __trigger + __panel (abre por CLICK)
1153
+ Motion: fade + scale(0.96→1) desde el transform-origin del trigger.
1154
+ Panel sobre --surface-2. Se distingue del Tooltip: click, no hover.
1155
+ ============================================================ */
1156
+ .popover { position: relative; display: inline-flex; }
1157
+ .popover__panel {
1158
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1159
+ min-width: 240px; max-width: 320px; padding: var(--space-4);
1160
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1161
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1162
+ transform-origin: top left; /* origin-aware: crece desde el disparador */
1163
+ opacity: 0; transform: scale(.96); visibility: hidden;
1164
+ transition: opacity var(--duration-base) var(--ease-out),
1165
+ transform var(--duration-base) var(--ease-out),
1166
+ visibility 0s linear var(--duration-base);
1167
+ }
1168
+ .popover.is-open .popover__panel {
1169
+ opacity: 1; transform: none; visibility: visible;
1170
+ transition: opacity var(--duration-base) var(--ease-out),
1171
+ transform var(--duration-base) var(--ease-out),
1172
+ visibility 0s;
1173
+ }
1174
+ /* Alineado a la derecha del disparador */
1175
+ .popover--right .popover__panel { left: auto; right: 0; transform-origin: top right; }
1176
+
1177
+ /* ============================================================
1178
+ 28. BUTTON GROUP — .btn-group (bordes compartidos, radios en extremos)
1179
+ ============================================================ */
1180
+ .btn-group { display: inline-flex; }
1181
+ .btn-group > .btn { border-radius: 0; }
1182
+ .btn-group > .btn:not(:first-child) { margin-left: -1px; }
1183
+ .btn-group > .btn:first-child { border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md); }
1184
+ .btn-group > .btn:last-child { border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
1185
+ /* El botón enfocado/hover sube de z-index para que su borde no lo tape el vecino */
1186
+ .btn-group > .btn:hover, .btn-group > .btn:focus-visible, .btn-group > .btn.is-active { z-index: 1; }
1187
+
1188
+ /* ============================================================
1189
+ 29. DATEPICKER — .datepicker + calendario en popover
1190
+ El panel reutiliza el look de .popover__panel sobre --surface-2.
1191
+ Grid role="grid"; días como <button role="gridcell">.
1192
+ ============================================================ */
1193
+ .datepicker { position: relative; }
1194
+ .datepicker__input { cursor: pointer; } /* consume .input; el icono va con .input-wrap */
1195
+ .datepicker__calendar {
1196
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1197
+ padding: var(--space-4);
1198
+ background: var(--surface-2); border: 1px solid var(--elevated-border);
1199
+ border-radius: var(--radius-md); box-shadow: var(--shadow-3);
1200
+ }
1201
+ .datepicker__calendar[hidden] { display: none; }
1202
+
1203
+ .calendar { width: 252px; }
1204
+ .calendar__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-3); }
1205
+ .calendar__title { font-size: var(--text-sm); font-weight: 600; color: var(--text-heading); } /* mes en minúscula (es) */
1206
+ .calendar__weekdays { display: grid; grid-template-columns: repeat(7, 1fr); margin-bottom: var(--space-1); }
1207
+ .calendar__weekdays span {
1208
+ display: grid; place-items: center; height: 28px;
1209
+ font-size: var(--text-xs); font-weight: 500; color: var(--text-muted);
1210
+ }
1211
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
1212
+ .calendar__week { display: contents; } /* fila semántica (role="row") sin romper el grid */
1213
+ .calendar__day {
1214
+ display: grid; place-items: center; width: 100%; height: 36px; padding: 0;
1215
+ border: 1px solid transparent; border-radius: var(--radius-sm); background: transparent;
1216
+ color: var(--text-primary); font-family: var(--font-mono); font-size: var(--text-sm);
1217
+ font-variant-numeric: tabular-nums; cursor: pointer;
1218
+ transition: background-color var(--duration-fast) var(--ease-out),
1219
+ color var(--duration-fast) var(--ease-out);
1220
+ }
1221
+ .calendar__day:hover:not([disabled]):not(.calendar__day--selected) { background: var(--surface-3); }
1222
+ /* Hoy: aro de acento por borde (no glow) + texto de acento */
1223
+ .calendar__day--today { border-color: var(--accent-400); color: var(--accent-300); }
1224
+ /* Seleccionado: fondo de acento + texto oscuro + peso — no depende solo del color */
1225
+ .calendar__day--selected { background: var(--accent-400); border-color: var(--accent-400); color: var(--text-on-accent); font-weight: 600; }
1226
+ .calendar__day--outside { color: var(--text-disabled); }
1227
+ .calendar__day[disabled] { color: var(--text-disabled); opacity: .5; cursor: not-allowed; }
1228
+
1229
+ /* ============================================================
1230
+ 30. FILE UPLOAD — .file-upload (dropzone) + __list
1231
+ El input file real va oculto accesiblemente (.visually-hidden).
1232
+ ============================================================ */
1233
+ .file-upload { display: block; }
1234
+ .file-upload__label {
1235
+ display: flex; flex-direction: column; align-items: center; gap: var(--space-2);
1236
+ padding: var(--space-8) var(--space-6); text-align: center;
1237
+ background: var(--surface-1); border: 1px dashed var(--border-strong);
1238
+ border-radius: var(--radius-lg); color: var(--text-muted); cursor: pointer;
1239
+ transition: border-color var(--duration-fast) var(--ease-out),
1240
+ background-color var(--duration-fast) var(--ease-out),
1241
+ color var(--duration-fast) var(--ease-out);
1242
+ }
1243
+ .file-upload__label > svg { width: 28px; height: 28px; color: var(--text-muted); }
1244
+ .file-upload__button { color: var(--link); font-weight: 600; }
1245
+ .file-upload__hint { font-size: var(--text-xs); color: var(--text-disabled); }
1246
+ @media (hover: hover) and (pointer: fine) {
1247
+ .file-upload__label:hover { border-color: var(--border-hover); background: var(--surface-2); color: var(--text-secondary); }
1248
+ }
1249
+ /* El ring se pinta sobre el label porque el input está oculto */
1250
+ .file-upload__input:focus-visible + .file-upload__label { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); }
1251
+ /* Arrastrando encima: acento (icono + borde, no solo color) */
1252
+ .file-upload.is-dragover .file-upload__label { border-color: var(--accent-400); background: var(--accent-ghost-hover); color: var(--text-secondary); }
1253
+
1254
+ .file-upload__list { list-style: none; margin: var(--space-3) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
1255
+ .file-upload__item {
1256
+ display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: var(--space-3);
1257
+ padding: var(--space-3); background: var(--surface-2);
1258
+ border: 1px solid var(--border); border-radius: var(--radius-md);
1259
+ }
1260
+ .file-upload__item > svg { width: 20px; height: 20px; color: var(--text-muted); flex: none; }
1261
+ .file-upload__item > div { min-width: 0; } /* permite truncar el nombre */
1262
+ .file-upload__item .progress { margin-top: var(--space-2); }
1263
+ .file-upload__name { display: block; font-size: var(--text-sm); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1264
+ .file-upload__size { display: block; font-size: var(--text-xs); color: var(--text-muted); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
1265
+ /* Error: borde + icono + texto en danger, nunca solo por color */
1266
+ .file-upload__item--error { border-color: color-mix(in srgb, var(--danger) 45%, transparent); background: var(--danger-bg); }
1267
+ .file-upload__item--error > svg { color: var(--danger); }
1268
+ .file-upload__item--error .file-upload__size { color: var(--danger); }
1269
+ .file-upload__remove {
1270
+ display: inline-grid; place-content: center; width: 28px; height: 28px; padding: 0; flex: none;
1271
+ border: 0; border-radius: var(--radius-full); background: transparent;
1272
+ color: var(--text-muted); cursor: pointer;
1273
+ transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
1274
+ }
1275
+ .file-upload__remove:hover { background: var(--surface-3); color: var(--text-primary); }
1276
+ .file-upload__remove svg { width: 16px; height: 16px; }
1277
+
1278
+ /* ============================================================
1279
+ 31. STEPPER / WIZARD — .stepper (<ol>) + .step
1280
+ Completo → check (no solo color); actual → acento.
1281
+ ============================================================ */
1282
+ .stepper { display: flex; list-style: none; margin: 0; padding: 0; }
1283
+ .step {
1284
+ position: relative; flex: 1; display: flex; flex-direction: column; align-items: center;
1285
+ gap: var(--space-2); text-align: center; min-width: 0;
1286
+ }
1287
+ .step__marker {
1288
+ position: relative; z-index: 1;
1289
+ display: grid; place-items: center; width: 32px; height: 32px; flex: none;
1290
+ border-radius: var(--radius-full); border: 2px solid var(--border);
1291
+ background: var(--surface-1); color: var(--text-muted);
1292
+ font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums;
1293
+ }
1294
+ .step__marker svg { width: 16px; height: 16px; }
1295
+ .step__label { font-size: var(--text-sm); color: var(--text-muted); }
1296
+ .step__connector {
1297
+ position: absolute; top: 15px; left: 50%; z-index: 0;
1298
+ width: 100%; height: 2px; background: var(--border);
1299
+ }
1300
+ /* Completo */
1301
+ .step--complete .step__marker { background: var(--accent-400); border-color: var(--accent-400); color: var(--text-on-accent); }
1302
+ .step--complete .step__label { color: var(--text-secondary); }
1303
+ .step--complete .step__connector { background: var(--accent-400); }
1304
+ /* Actual */
1305
+ .step--current .step__marker { border-color: var(--accent-400); color: var(--accent-300); }
1306
+ .step--current .step__label { color: var(--text-primary); font-weight: 600; }
1307
+ /* Próximo = valores por defecto (atenuado) */
1308
+
1309
+ /* Variante vertical */
1310
+ .stepper--vertical { flex-direction: column; }
1311
+ .stepper--vertical .step { flex-direction: row; align-items: flex-start; gap: var(--space-3); text-align: left; padding-bottom: var(--space-6); }
1312
+ .stepper--vertical .step:last-child { padding-bottom: 0; }
1313
+ .stepper--vertical .step__label { padding-top: var(--space-1); }
1314
+ .stepper--vertical .step__connector { top: 34px; left: 15px; width: 2px; height: calc(100% - 34px); }
1315
+
1316
+ /* ============================================================
1317
+ 32. KBD — .kbd (tecla / atajo de teclado)
1318
+ Combos: varias .kbd con un "+" literal entre ellas.
1319
+ ============================================================ */
1320
+ .kbd {
1321
+ display: inline-flex; align-items: center; justify-content: center;
1322
+ min-width: 20px; height: 20px; padding: 0 6px;
1323
+ font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 500; line-height: 1;
1324
+ color: var(--text-secondary); background: var(--surface-2);
1325
+ border: 1px solid var(--border-strong); border-radius: 6px; box-shadow: var(--shadow-1);
1326
+ }
1327
+
1010
1328
 
1011
1329
  /* ============================================================
1012
1330
  ESTILOS DE LA PÁGINA DE DOCS (.ds-*)
@@ -1220,6 +1538,14 @@ html { scroll-behavior: smooth; }
1220
1538
  <a href="#comp-navegacion">Navegación</a>
1221
1539
  <a href="#comp-skeleton">Skeleton y EmptyState</a>
1222
1540
  <a href="#comp-card">Card, Link y Code</a>
1541
+ <a href="#comp-combobox">Combobox y Search</a>
1542
+ <a href="#comp-drawer">Drawer</a>
1543
+ <a href="#comp-popover">Popover</a>
1544
+ <a href="#comp-buttongroup">ButtonGroup</a>
1545
+ <a href="#comp-datepicker">DatePicker</a>
1546
+ <a href="#comp-fileupload">FileUpload</a>
1547
+ <a href="#comp-stepper">Stepper</a>
1548
+ <a href="#comp-kbd">Kbd</a>
1223
1549
  </div>
1224
1550
  <div class="overline ds-group">Recursos</div>
1225
1551
  <a href="#motion">Motion</a>
@@ -1284,7 +1610,7 @@ html { scroll-behavior: smooth; }
1284
1610
  <div class="ds-swatches" id="swIndigo"></div>
1285
1611
  <div class="ds-callout ds-callout--warning">
1286
1612
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>
1287
- <div>El índigo <span class="code">#353d7c</span> sobre negro da un contraste de ≈ 1.9:1 — <strong>nunca es texto sobre fondos nocturnos</strong>, solo fondos de sección, sidebars y estados seleccionados.</div>
1613
+ <div>El índigo <span class="code">#353d7c</span> sobre negro da un contraste de 2.09:1 — <strong>nunca es texto sobre fondos nocturnos</strong>, solo fondos de sección, sidebars y estados seleccionados.</div>
1288
1614
  </div>
1289
1615
 
1290
1616
  <h3 style="margin-top:32px;">Neutrales — Escala Nocturna</h3>
@@ -1386,13 +1712,13 @@ html { scroll-behavior: smooth; }
1386
1712
  <section id="componentes">
1387
1713
  <span class="overline">Biblioteca</span>
1388
1714
  <h2>Componentes</h2>
1389
- <p class="ds-lead">Los 26 componentes del sistema, renderizados en vivo con las clases del framework <span class="code">nocturna-electrica.css</span>. Cada bloque incluye su código de uso.</p>
1715
+ <p class="ds-lead">Los 35 componentes del sistema, renderizados en vivo con las clases del framework <span class="code">nocturna-electrica.css</span>. Cada bloque incluye su código de uso.</p>
1390
1716
  </section>
1391
1717
 
1392
1718
  <!-- ================= BOTONES ================= -->
1393
1719
  <section id="comp-botones">
1394
1720
  <h2>Botones</h2>
1395
- <p>Alturas 36 / 44 / 52px. El primario lleva <strong>texto oscuro</strong> (contraste ≈ 8.5:1) — nunca blanco. Un solo primario por pantalla.</p>
1721
+ <p>Alturas 36 / 44 / 52px. El primario lleva <strong>texto oscuro</strong> (contraste 9.50:1) — nunca blanco. Un solo primario por pantalla.</p>
1396
1722
  <div class="ds-demo">
1397
1723
  <button class="btn btn--primary">Guardar cambios</button>
1398
1724
  <button class="btn btn--secondary">Ver informe</button>
@@ -1474,12 +1800,23 @@ html { scroll-behavior: smooth; }
1474
1800
  <label class="field__label" for="i4">Deshabilitado</label>
1475
1801
  <input class="input" id="i4" type="text" value="Solo lectura" disabled>
1476
1802
  </div>
1803
+ <div class="field field--required">
1804
+ <label class="field__label" for="i5">Nombre completo</label>
1805
+ <input class="input" id="i5" type="text" placeholder="Tu nombre" required aria-required="true">
1806
+ <span class="field__help">El asterisco es decorativo; lo obligatorio lo marca <span class="code">required</span>.</span>
1807
+ </div>
1477
1808
  </div>
1478
1809
  <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="field"&gt;
1479
1810
  &lt;label class="field__label" for="email"&gt;Correo&lt;/label&gt;
1480
1811
  &lt;input class="input" id="email" type="email" placeholder="tu@correo.com"&gt;
1481
1812
  &lt;span class="field__help"&gt;Texto de ayuda.&lt;/span&gt;
1482
1813
  &lt;/div&gt;
1814
+ &lt;!-- Obligatorio: .field--required en el .field pinta el asterisco (decorativo); --&gt;
1815
+ &lt;!-- el control lleva required + aria-required="true" --&gt;
1816
+ &lt;div class="field field--required"&gt;
1817
+ &lt;label class="field__label" for="name"&gt;Nombre&lt;/label&gt;
1818
+ &lt;input class="input" id="name" type="text" required aria-required="true"&gt;
1819
+ &lt;/div&gt;
1483
1820
  &lt;!-- Estados: .input--error (+ .field__error) · .input--success · disabled --&gt;
1484
1821
  &lt;!-- Icono leading: envolver en .input-wrap con un svg antes del input --&gt;</code></pre></details>
1485
1822
  </section>
@@ -1573,11 +1910,14 @@ html { scroll-behavior: smooth; }
1573
1910
  <div class="ds-demo" style="gap:32px;">
1574
1911
  <label class="check-row"><input type="checkbox" class="checkbox" checked> Notificarme por correo</label>
1575
1912
  <label class="check-row"><input type="checkbox" class="checkbox"> Recordar sesión</label>
1913
+ <label class="check-row"><input type="checkbox" class="checkbox" id="cbIndeterminate"> Seleccionar todo <span class="text-muted">(indeterminate)</span></label>
1576
1914
  <label class="check-row"><input type="radio" name="r1" class="radio" checked> Mensual</label>
1577
1915
  <label class="check-row"><input type="radio" name="r1" class="radio"> Anual</label>
1578
1916
  </div>
1579
1917
  <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;label class="check-row"&gt;&lt;input type="checkbox" class="checkbox"&gt; Etiqueta&lt;/label&gt;
1580
- &lt;label class="check-row"&gt;&lt;input type="radio" name="g" class="radio"&gt; Opción&lt;/label&gt;</code></pre></details>
1918
+ &lt;label class="check-row"&gt;&lt;input type="radio" name="g" class="radio"&gt; Opción&lt;/label&gt;
1919
+ &lt;!-- Indeterminate = tercer estado (guion horizontal). Solo por JS: --&gt;
1920
+ &lt;!-- document.querySelector('.checkbox').indeterminate = true; --&gt;</code></pre></details>
1581
1921
  </section>
1582
1922
 
1583
1923
  <!-- ================= SWITCH ================= -->
@@ -1618,6 +1958,25 @@ html { scroll-behavior: smooth; }
1618
1958
  <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;span class="avatar"&gt;HS&lt;span class="avatar__dot avatar__dot--online"&gt;&lt;/span&gt;&lt;/span&gt;
1619
1959
  &lt;!-- Tamaños: --xs 24 · --sm 32 · default 40 · --lg 48 · --xl 64 --&gt;
1620
1960
  &lt;!-- Con imagen: &lt;span class="avatar"&gt;&lt;img src="…" alt="Nombre"&gt;&lt;/span&gt; --&gt;</code></pre></details>
1961
+
1962
+ <h3 style="margin-top:var(--space-8);">AvatarGroup</h3>
1963
+ <p>Avatares solapados 8px con borde <span class="code">2px</span> del color del fondo; el chip <span class="code">.avatar-group__more</span> resume los ocultos.</p>
1964
+ <div class="ds-demo">
1965
+ <div class="avatar-group" role="group" aria-label="Equipo del proyecto: 4 personas y 3 más">
1966
+ <span class="avatar">hs</span>
1967
+ <span class="avatar">ml</span>
1968
+ <span class="avatar">ar</span>
1969
+ <span class="avatar">jt</span>
1970
+ <span class="avatar avatar-group__more">+3</span>
1971
+ </div>
1972
+ </div>
1973
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="avatar-group" role="group" aria-label="Equipo del proyecto"&gt;
1974
+ &lt;span class="avatar"&gt;HS&lt;/span&gt;
1975
+ &lt;span class="avatar"&gt;ML&lt;/span&gt;
1976
+ &lt;span class="avatar"&gt;AR&lt;/span&gt;
1977
+ &lt;span class="avatar avatar-group__more"&gt;+3&lt;/span&gt;
1978
+ &lt;/div&gt;
1979
+ &lt;!-- Solapamiento -8px + borde del color del fondo (--surface-1 / --bg). --&gt;</code></pre></details>
1621
1980
  </section>
1622
1981
 
1623
1982
  <!-- ================= DIVIDER ================= -->
@@ -1651,7 +2010,7 @@ html { scroll-behavior: smooth; }
1651
2010
  </div>
1652
2011
  <div class="accordion__item">
1653
2012
  <button class="accordion__header" aria-expanded="false">¿Por qué el CTA lleva texto oscuro?<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg></button>
1654
- <div class="accordion__panel"><div class="accordion__inner"><div class="accordion__content">Negro nocturno sobre celeste eléctrico da ≈ 8.5:1 de contraste; blanco sobre celeste no pasa AA.</div></div></div>
2013
+ <div class="accordion__panel"><div class="accordion__inner"><div class="accordion__content">Negro nocturno sobre celeste eléctrico da 9.50:1 de contraste; blanco sobre celeste no pasa AA.</div></div></div>
1655
2014
  </div>
1656
2015
  </div>
1657
2016
  </div>
@@ -1802,24 +2161,30 @@ html { scroll-behavior: smooth; }
1802
2161
  <!-- ================= MODAL ================= -->
1803
2162
  <section id="comp-modal">
1804
2163
  <h2>Modal</h2>
1805
- <p>Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos).</p>
2164
+ <p>Overlay con blur 4px; se cierra con Esc o clic fuera (excepto flujos destructivos con <span class="code">data-modal-persistent</span>). Al abrir, el foco entra al panel y queda atrapado (Tab/Shift+Tab ciclan dentro); al cerrar, el foco vuelve al botón que lo abrió.</p>
1806
2165
  <div class="ds-demo">
1807
2166
  <button class="btn btn--primary" id="openModal">Abrir modal</button>
1808
2167
  <button class="btn btn--outline" id="openModalLg">Abrir formulario</button>
1809
2168
  </div>
1810
2169
  <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="modal-overlay"&gt;
1811
- &lt;div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle"&gt;
2170
+ &lt;!-- aria-modal, aria-labelledby (título) y aria-describedby (descripción, si la hay);
2171
+ tabindex="-1" permite mover el foco al panel al abrir --&gt;
2172
+ &lt;div class="modal" role="dialog" aria-modal="true"
2173
+ aria-labelledby="mTitle" aria-describedby="mDesc" tabindex="-1"&gt;
1812
2174
  &lt;div class="modal__header"&gt;
1813
2175
  &lt;h4 class="modal__title" id="mTitle"&gt;Título&lt;/h4&gt;
1814
- &lt;button class="icon-btn icon-btn--ghost" aria-label="Cerrar"&gt;×&lt;/button&gt;
2176
+ &lt;button class="icon-btn icon-btn--ghost" data-close-modal aria-label="Cerrar"&gt;×&lt;/button&gt;
1815
2177
  &lt;/div&gt;
1816
- &lt;p&gt;Contenido…&lt;/p&gt;
2178
+ &lt;p id="mDesc"&gt;Contenido…&lt;/p&gt;
1817
2179
  &lt;div class="modal__footer"&gt;
1818
- &lt;button class="btn btn--outline"&gt;Cancelar&lt;/button&gt;
1819
- &lt;button class="btn btn--primary"&gt;Confirmar&lt;/button&gt;
2180
+ &lt;button class="btn btn--outline" data-close-modal&gt;Cancelar&lt;/button&gt;
2181
+ &lt;button class="btn btn--primary" data-close-modal&gt;Confirmar&lt;/button&gt;
1820
2182
  &lt;/div&gt;
1821
2183
  &lt;/div&gt;
1822
- &lt;/div&gt;</code></pre></details>
2184
+ &lt;/div&gt;
2185
+ &lt;!-- JS: al abrir guarda document.activeElement y enfoca el panel; Tab cicla dentro
2186
+ (focus-trap); Esc / clic en overlay / data-close-modal cierran y devuelven el foco.
2187
+ Destructivo: añade data-modal-persistent al .modal-overlay. --&gt;</code></pre></details>
1823
2188
  </section>
1824
2189
 
1825
2190
  <!-- ================= MENU ================= -->
@@ -1845,23 +2210,73 @@ html { scroll-behavior: smooth; }
1845
2210
  <!-- ================= TABLA ================= -->
1846
2211
  <section id="comp-tabla">
1847
2212
  <h2>Tabla</h2>
1848
- <div class="ds-demo ds-demo--col" style="padding:0;overflow-x:auto;">
1849
- <table class="table">
1850
- <thead><tr><th>Proyecto</th><th>Estado</th><th class="num">Ingresos</th><th class="num">Cambio</th></tr></thead>
2213
+ <p>Zebra (<span class="code">.table--zebra</span>) + encabezado fijo (<span class="code">.table--sticky</span>, haz scroll dentro del recuadro) + columna ordenable (clic en «Proyecto» o «Ingresos») + columna de selección. La fila marcada usa <span class="code">.row--selected</span>; el checkbox maestro está en estado indeterminate.</p>
2214
+ <div class="ds-demo ds-demo--col" style="padding:0;max-height:264px;overflow:auto;">
2215
+ <table class="table table--zebra table--sticky" id="demoSortTable">
2216
+ <thead>
2217
+ <tr>
2218
+ <th scope="col" class="col-select"><input type="checkbox" class="checkbox" id="tblSelectAll" aria-label="Seleccionar todas las filas"></th>
2219
+ <th scope="col" aria-sort="ascending"><button class="th-sort" type="button">Proyecto <svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
2220
+ <th scope="col">Estado</th>
2221
+ <th scope="col" class="num" aria-sort="none"><button class="th-sort" type="button">Ingresos <svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m18 15-6-6-6 6"/></svg></button></th>
2222
+ <th scope="col" class="num">Cambio</th>
2223
+ </tr>
2224
+ </thead>
1851
2225
  <tbody>
1852
- <tr><td style="color:var(--text-primary);">Panel analítico</td><td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td><td class="num">$48 210</td><td class="num" style="color:var(--success);">+8,2 %</td></tr>
1853
- <tr class="is-selected"><td style="color:var(--text-primary);">API de pagos</td><td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td><td class="num">$31 990</td><td class="num" style="color:var(--success);">+2,4 %</td></tr>
1854
- <tr><td style="color:var(--text-primary);">App móvil</td><td><span class="badge badge--warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>Pendiente</span></td><td class="num">$12 040</td><td class="num" style="color:var(--danger);">−1,1 %</td></tr>
2226
+ <tr>
2227
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"></td>
2228
+ <td style="color:var(--text-primary);">API de pagos</td>
2229
+ <td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td>
2230
+ <td class="num" data-sort="31990">$31 990</td>
2231
+ <td class="num" style="color:var(--success);">+2,4 %</td>
2232
+ </tr>
2233
+ <tr class="row--selected">
2234
+ <td class="col-select"><input type="checkbox" class="checkbox" checked aria-label="Seleccionar App móvil"></td>
2235
+ <td style="color:var(--text-primary);">App móvil</td>
2236
+ <td><span class="badge badge--warning"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.7 18-8-14a2 2 0 0 0-3.5 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3z"/><path d="M12 9v4m0 4h.01"/></svg>Pendiente</span></td>
2237
+ <td class="num" data-sort="12040">$12 040</td>
2238
+ <td class="num" style="color:var(--danger);">−1,1 %</td>
2239
+ </tr>
2240
+ <tr>
2241
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Panel analítico"></td>
2242
+ <td style="color:var(--text-primary);">Panel analítico</td>
2243
+ <td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td>
2244
+ <td class="num" data-sort="48210">$48 210</td>
2245
+ <td class="num" style="color:var(--success);">+8,2 %</td>
2246
+ </tr>
2247
+ <tr>
2248
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Portal de socios"></td>
2249
+ <td style="color:var(--text-primary);">Portal de socios</td>
2250
+ <td><span class="badge badge--success"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>Activo</span></td>
2251
+ <td class="num" data-sort="27500">$27 500</td>
2252
+ <td class="num" style="color:var(--success);">+5,0 %</td>
2253
+ </tr>
2254
+ <tr>
2255
+ <td class="col-select"><input type="checkbox" class="checkbox" aria-label="Seleccionar Reportes"></td>
2256
+ <td style="color:var(--text-primary);">Reportes</td>
2257
+ <td><span class="badge badge--info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4m0-4h.01"/></svg>Beta</span></td>
2258
+ <td class="num" data-sort="9800">$9 800</td>
2259
+ <td class="num" style="color:var(--danger);">−0,4 %</td>
2260
+ </tr>
1855
2261
  </tbody>
1856
2262
  </table>
1857
2263
  </div>
1858
- <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;table class="table"&gt;
1859
- &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Proyecto&lt;/th&gt;&lt;th class="num"&gt;Ingresos&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
2264
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;table class="table table--zebra table--sticky"&gt;
2265
+ &lt;thead&gt;&lt;tr&gt;
2266
+ &lt;th scope="col" class="col-select"&gt;
2267
+ &lt;input type="checkbox" class="checkbox" aria-label="Seleccionar todas"&gt;&lt;/th&gt;
2268
+ &lt;th scope="col" aria-sort="ascending"&gt;
2269
+ &lt;button class="th-sort" type="button"&gt;Proyecto
2270
+ &lt;svg class="sort-icon" …&gt;&lt;path d="m18 15-6-6-6 6"/&gt;&lt;/svg&gt;
2271
+ &lt;/button&gt;&lt;/th&gt;
2272
+ &lt;th scope="col" class="num"&gt;Ingresos&lt;/th&gt;
2273
+ &lt;/tr&gt;&lt;/thead&gt;
1860
2274
  &lt;tbody&gt;
1861
- &lt;tr&gt;&lt;td&gt;Panel analítico&lt;/td&gt;&lt;td class="num"&gt;$48 210&lt;/td&gt;&lt;/tr&gt;
1862
- &lt;tr class="is-selected"&gt;…&lt;/tr&gt;
2275
+ &lt;tr&gt;&lt;td class="col-select"&gt;&lt;input type="checkbox" class="checkbox" aria-label="Seleccionar API de pagos"&gt;&lt;/td&gt;…&lt;/tr&gt;
2276
+ &lt;tr class="row--selected"&gt;…&lt;/tr&gt;
1863
2277
  &lt;/tbody&gt;
1864
- &lt;/table&gt;</code></pre></details>
2278
+ &lt;/table&gt;
2279
+ &lt;!-- El icono se atenúa si aria-sort no está fijado; JS reordena y alterna aria-sort. --&gt;</code></pre></details>
1865
2280
  </section>
1866
2281
 
1867
2282
  <!-- ================= STAT ================= -->
@@ -2007,6 +2422,365 @@ html { scroll-behavior: smooth; }
2007
2422
  &lt;span class="code"&gt;var(--accent-400)&lt;/span&gt;</code></pre></details>
2008
2423
  </section>
2009
2424
 
2425
+ <!-- ================= COMBOBOX / SEARCH ================= -->
2426
+ <section id="comp-combobox">
2427
+ <h2>Combobox y Search</h2>
2428
+ <p>Combobox con autocompletado: filtra en vivo y se navega con ↑/↓, Enter selecciona, Esc cierra. El listado vive sobre <span class="code">--surface-2</span>. Al lado, un campo de búsqueda con botón para limpiar.</p>
2429
+ <div class="ds-demo ds-demo--grid" style="align-items:start;overflow:visible;">
2430
+ <div>
2431
+ <span class="field__label" style="display:block;margin-bottom:6px;">Framework</span>
2432
+ <div class="combobox">
2433
+ <input class="input combobox__input" type="text" id="cbInput"
2434
+ role="combobox" aria-expanded="false" aria-controls="cbList"
2435
+ aria-autocomplete="list" aria-activedescendant=""
2436
+ placeholder="Escribe para filtrar…" autocomplete="off" spellcheck="false">
2437
+ <ul class="combobox__list" id="cbList" role="listbox" aria-label="Frameworks" hidden>
2438
+ <li class="combobox__option" role="option" id="cbo-0" aria-selected="false">Angular<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2439
+ <li class="combobox__option" role="option" id="cbo-1" aria-selected="false">Astro<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2440
+ <li class="combobox__option" role="option" id="cbo-2" aria-selected="false">React<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2441
+ <li class="combobox__option" role="option" id="cbo-3" aria-selected="false">Solid<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2442
+ <li class="combobox__option" role="option" id="cbo-4" aria-selected="false">Svelte<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2443
+ <li class="combobox__option" role="option" id="cbo-5" aria-selected="false">Vue<svg class="combobox__check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></li>
2444
+ </ul>
2445
+ </div>
2446
+ </div>
2447
+ <div>
2448
+ <span class="field__label" style="display:block;margin-bottom:6px;">Buscar</span>
2449
+ <div class="search">
2450
+ <svg class="search__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
2451
+ <input class="input search__input" type="search" id="searchInput" placeholder="Buscar proyectos…" aria-label="Buscar proyectos" autocomplete="off">
2452
+ <button class="search__clear" type="button" id="searchClear" aria-label="Limpiar" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2453
+ </div>
2454
+ </div>
2455
+ </div>
2456
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="combobox"&gt;
2457
+ &lt;input class="input combobox__input" role="combobox" aria-expanded="false"
2458
+ aria-controls="lista" aria-autocomplete="list" aria-activedescendant=""&gt;
2459
+ &lt;ul class="combobox__list" id="lista" role="listbox" aria-label="Frameworks" hidden&gt;
2460
+ &lt;li class="combobox__option" role="option" id="op-0" aria-selected="false"&gt;Angular&lt;/li&gt;
2461
+ &lt;!-- .is-active = resaltado por teclado · aria-selected="true" = elegida --&gt;
2462
+ &lt;/ul&gt;
2463
+ &lt;/div&gt;
2464
+
2465
+ &lt;div class="search"&gt;
2466
+ &lt;svg class="search__icon" aria-hidden="true"&gt;…lupa…&lt;/svg&gt;
2467
+ &lt;input class="input search__input" type="search" aria-label="Buscar"&gt;
2468
+ &lt;button class="search__clear" aria-label="Limpiar" hidden&gt;×&lt;/button&gt;
2469
+ &lt;/div&gt;
2470
+ &lt;!-- JS: filtra en vivo, ↑/↓/Enter/Esc; el clear vacía y devuelve el foco. --&gt;</code></pre></details>
2471
+ </section>
2472
+
2473
+ <!-- ================= DRAWER ================= -->
2474
+ <section id="comp-drawer">
2475
+ <h2>Drawer</h2>
2476
+ <p>Panel que entra deslizándose desde un borde (derecha por defecto; <span class="code">--left</span> / <span class="code">--bottom</span>). Igual que el modal: <span class="code">role="dialog"</span> + <span class="code">aria-modal</span>, foco atrapado, retorno de foco, y cierre con Esc o clic en el overlay. El panel usa <span class="code">overscroll-behavior: contain</span>.</p>
2477
+ <div class="ds-demo">
2478
+ <button class="btn btn--primary" id="openDrawer">Abrir panel</button>
2479
+ </div>
2480
+ <div class="drawer drawer--right" id="demoDrawer" hidden>
2481
+ <div class="drawer__overlay" data-close-drawer></div>
2482
+ <aside class="drawer__panel" role="dialog" aria-modal="true" aria-labelledby="drawerTitle" tabindex="-1">
2483
+ <div class="drawer__header">
2484
+ <h3 class="drawer__title" id="drawerTitle">Filtros</h3>
2485
+ <button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar panel"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2486
+ </div>
2487
+ <div class="drawer__body">
2488
+ <div class="field">
2489
+ <label class="field__label" for="drawerRange">Rango de fechas</label>
2490
+ <div class="select-wrap">
2491
+ <select class="select" id="drawerRange">
2492
+ <option>Últimos 7 días</option>
2493
+ <option>Últimos 30 días</option>
2494
+ <option>Este trimestre</option>
2495
+ </select>
2496
+ </div>
2497
+ </div>
2498
+ <div style="display:flex;flex-direction:column;gap:var(--space-3);">
2499
+ <label class="check-row"><input type="checkbox" class="checkbox" checked> Solo proyectos activos</label>
2500
+ <label class="check-row"><input type="checkbox" class="checkbox"> Incluir archivados</label>
2501
+ </div>
2502
+ </div>
2503
+ <div class="drawer__footer">
2504
+ <button class="btn btn--outline" data-close-drawer>Cancelar</button>
2505
+ <button class="btn btn--primary" data-close-drawer>Aplicar filtros</button>
2506
+ </div>
2507
+ </aside>
2508
+ </div>
2509
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="drawer drawer--right" hidden&gt;
2510
+ &lt;div class="drawer__overlay" data-close-drawer&gt;&lt;/div&gt;
2511
+ &lt;aside class="drawer__panel" role="dialog" aria-modal="true"
2512
+ aria-labelledby="dTitle" tabindex="-1"&gt;
2513
+ &lt;div class="drawer__header"&gt;
2514
+ &lt;h3 class="drawer__title" id="dTitle"&gt;Filtros&lt;/h3&gt;
2515
+ &lt;button class="icon-btn icon-btn--ghost" data-close-drawer aria-label="Cerrar"&gt;×&lt;/button&gt;
2516
+ &lt;/div&gt;
2517
+ &lt;div class="drawer__body"&gt;…&lt;/div&gt;
2518
+ &lt;div class="drawer__footer"&gt;…&lt;/div&gt;
2519
+ &lt;/aside&gt;
2520
+ &lt;/div&gt;
2521
+ &lt;!-- Variantes: --right (default) · --left · --bottom (sheet).
2522
+ JS: .is-open dispara el slide; Esc / overlay cierran y devuelven el foco. --&gt;</code></pre></details>
2523
+ </section>
2524
+
2525
+ <!-- ================= POPOVER ================= -->
2526
+ <section id="comp-popover">
2527
+ <h2>Popover</h2>
2528
+ <p>Se abre por <strong>clic</strong> —eso lo distingue del Tooltip, que es hover— y crece desde el <span class="code">transform-origin</span> del disparador. <span class="code">role="dialog"</span>, el foco entra al panel; cierra con Esc o clic fuera. Panel sobre <span class="code">--surface-2</span>.</p>
2529
+ <div class="ds-demo" style="overflow:visible;">
2530
+ <div class="popover">
2531
+ <button class="btn btn--outline popover__trigger" type="button" id="popTrigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="popPanel">
2532
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="m16 6-4-4-4 4"/><path d="M12 2v13"/></svg>
2533
+ Compartir
2534
+ </button>
2535
+ <div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="popPanel">
2536
+ <h3 style="margin:0 0 var(--space-2);font-family:var(--font-body);font-size:var(--text-base);font-weight:600;letter-spacing:normal;color:var(--text-heading);">Compartir proyecto</h3>
2537
+ <p style="font-size:var(--text-sm);color:var(--text-muted);margin:0 0 var(--space-3);max-width:none;">Cualquiera con el enlace podrá ver este proyecto en modo lectura.</p>
2538
+ <div style="display:flex;gap:var(--space-2);align-items:center;">
2539
+ <input class="input" type="text" readonly value="higinio.dev/p/8f2a" aria-label="Enlace para compartir">
2540
+ <button class="btn btn--primary btn--sm" type="button">Copiar</button>
2541
+ </div>
2542
+ <a class="link" href="#comp-popover" style="margin-top:var(--space-3);">Ver permisos avanzados</a>
2543
+ </div>
2544
+ </div>
2545
+ </div>
2546
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="popover"&gt;
2547
+ &lt;button class="btn btn--outline popover__trigger" type="button"
2548
+ aria-haspopup="dialog" aria-expanded="false" aria-controls="panel"&gt;Compartir&lt;/button&gt;
2549
+ &lt;div class="popover__panel" role="dialog" aria-label="Compartir proyecto" id="panel"&gt;
2550
+ &lt;p&gt;Contenido de varias líneas…&lt;/p&gt;
2551
+ &lt;button class="btn btn--primary btn--sm"&gt;Copiar&lt;/button&gt;
2552
+ &lt;a class="link" href="…"&gt;Ver permisos avanzados&lt;/a&gt;
2553
+ &lt;/div&gt;
2554
+ &lt;/div&gt;
2555
+ &lt;!-- Abre por CLICK (no hover); el foco entra al panel; Esc / clic fuera cierran. --&gt;</code></pre></details>
2556
+ </section>
2557
+
2558
+ <!-- ================= BUTTON GROUP ================= -->
2559
+ <section id="comp-buttongroup">
2560
+ <h2>ButtonGroup</h2>
2561
+ <p>Botones adyacentes con bordes compartidos: el radio queda solo en los extremos y los bordes internos se colapsan. Reutiliza los <span class="code">.btn</span> existentes. <span class="code">role="group"</span> + <span class="code">aria-label</span>.</p>
2562
+ <div class="ds-demo">
2563
+ <div class="btn-group" role="group" aria-label="Alineación del texto">
2564
+ <button class="btn btn--outline" type="button">Izquierda</button>
2565
+ <button class="btn btn--outline" type="button">Centro</button>
2566
+ <button class="btn btn--outline" type="button">Derecha</button>
2567
+ </div>
2568
+ </div>
2569
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="btn-group" role="group" aria-label="Alineación del texto"&gt;
2570
+ &lt;button class="btn btn--outline"&gt;Izquierda&lt;/button&gt;
2571
+ &lt;button class="btn btn--outline"&gt;Centro&lt;/button&gt;
2572
+ &lt;button class="btn btn--outline"&gt;Derecha&lt;/button&gt;
2573
+ &lt;/div&gt;
2574
+ &lt;!-- Radios solo en los extremos; bordes internos colapsados (margin-left:-1px). --&gt;</code></pre></details>
2575
+ </section>
2576
+
2577
+ <!-- ================= DATEPICKER ================= -->
2578
+ <section id="comp-datepicker">
2579
+ <h2>DatePicker</h2>
2580
+ <p>Campo de fecha con calendario en popover: el input abre el calendario al hacer clic, eliges un día con el ratón o las flechas y el valor vuelve al campo. El panel reutiliza el look de <span class="code">.popover__panel</span> sobre <span class="code">--surface-2</span>. El <span class="code">.calendar__grid</span> es <span class="code">role="grid"</span> y cada día un <span class="code">&lt;button role="gridcell"&gt;</span> con la fecha completa en <span class="code">aria-label</span>.</p>
2581
+ <div class="ds-demo" style="overflow:visible;">
2582
+ <div class="datepicker" id="datepicker">
2583
+ <span class="field__label" id="dpLabel" style="display:block;margin-bottom:6px;">Fecha de entrega</span>
2584
+ <div class="input-wrap" style="max-width:220px;">
2585
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
2586
+ <input class="input datepicker__input" type="text" id="dpInput" readonly
2587
+ aria-haspopup="dialog" aria-expanded="false" aria-controls="dpCalendar"
2588
+ aria-labelledby="dpLabel" value="15 de julio de 2026">
2589
+ </div>
2590
+ <div class="datepicker__calendar" id="dpCalendar" role="dialog" aria-label="Elegir fecha" hidden>
2591
+ <div class="calendar">
2592
+ <div class="calendar__header">
2593
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" id="dpPrev" aria-label="Mes anterior"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg></button>
2594
+ <span class="calendar__title" id="dpTitle" aria-live="polite">julio 2026</span>
2595
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" id="dpNext" aria-label="Mes siguiente"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg></button>
2596
+ </div>
2597
+ <div class="calendar__weekdays" role="row">
2598
+ <span role="columnheader" aria-label="lunes">L</span>
2599
+ <span role="columnheader" aria-label="martes">M</span>
2600
+ <span role="columnheader" aria-label="miércoles">X</span>
2601
+ <span role="columnheader" aria-label="jueves">J</span>
2602
+ <span role="columnheader" aria-label="viernes">V</span>
2603
+ <span role="columnheader" aria-label="sábado">S</span>
2604
+ <span role="columnheader" aria-label="domingo">D</span>
2605
+ </div>
2606
+ <div class="calendar__grid" id="dpGrid" role="grid" aria-labelledby="dpTitle"></div>
2607
+ </div>
2608
+ </div>
2609
+ </div>
2610
+ </div>
2611
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="datepicker"&gt;
2612
+ &lt;div class="input-wrap"&gt;
2613
+ &lt;svg aria-hidden="true"&gt;…calendario…&lt;/svg&gt;
2614
+ &lt;input class="input datepicker__input" readonly aria-haspopup="dialog"
2615
+ aria-expanded="false" aria-controls="cal" value="15 de julio de 2026"&gt;
2616
+ &lt;/div&gt;
2617
+ &lt;div class="datepicker__calendar" id="cal" role="dialog" aria-label="Elegir fecha" hidden&gt;
2618
+ &lt;div class="calendar"&gt;
2619
+ &lt;div class="calendar__header"&gt;
2620
+ &lt;button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes anterior"&gt;‹&lt;/button&gt;
2621
+ &lt;span class="calendar__title" aria-live="polite"&gt;julio 2026&lt;/span&gt;
2622
+ &lt;button class="icon-btn icon-btn--ghost icon-btn--sm" aria-label="Mes siguiente"&gt;›&lt;/button&gt;
2623
+ &lt;/div&gt;
2624
+ &lt;div class="calendar__weekdays" role="row"&gt;
2625
+ &lt;span role="columnheader" aria-label="lunes"&gt;L&lt;/span&gt; …
2626
+ &lt;/div&gt;
2627
+ &lt;div class="calendar__grid" role="grid"&gt;
2628
+ &lt;div class="calendar__week" role="row"&gt;
2629
+ &lt;button class="calendar__day" role="gridcell"
2630
+ aria-label="15 de julio de 2026" aria-selected="true"&gt;15&lt;/button&gt; …
2631
+ &lt;/div&gt;
2632
+ &lt;/div&gt;
2633
+ &lt;/div&gt;
2634
+ &lt;/div&gt;
2635
+ &lt;/div&gt;
2636
+ &lt;!-- JS: clic abre; flechas mueven el día (±1, ±7) cruzando meses; Enter elige; Esc cierra. --&gt;</code></pre></details>
2637
+ </section>
2638
+
2639
+ <!-- ================= FILE UPLOAD ================= -->
2640
+ <section id="comp-fileupload">
2641
+ <h2>FileUpload</h2>
2642
+ <p>Zona de arrastre con lista de archivos. El input file real va oculto accesiblemente (<span class="code">.visually-hidden</span>, sigue tabulable) y el ring de foco se pinta sobre el label. Al arrastrar un archivo encima, <span class="code">.is-dragover</span> tiñe el borde de acento. Un ítem subiendo incrusta el <span class="code">.progress</span>; el error se marca con icono + texto, no solo por color.</p>
2643
+ <div class="ds-demo">
2644
+ <div class="file-upload" id="fileUpload" style="width:100%;max-width:520px;">
2645
+ <input class="file-upload__input visually-hidden" type="file" id="fuInput" multiple aria-describedby="fuHint">
2646
+ <label class="file-upload__label" for="fuInput">
2647
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="M17 8l-5-5-5 5"/><path d="M12 3v13"/></svg>
2648
+ <span>Arrastra archivos aquí o <span class="file-upload__button">explora</span></span>
2649
+ <span class="file-upload__hint" id="fuHint">PNG, JPG o PDF · máximo 10 MB</span>
2650
+ </label>
2651
+ <ul class="file-upload__list" id="fuList">
2652
+ <li class="file-upload__item">
2653
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
2654
+ <div>
2655
+ <span class="file-upload__name">informe-trimestral-q3.pdf</span>
2656
+ <span class="file-upload__size">2.4 MB · subiendo…</span>
2657
+ <div class="progress" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" aria-label="Subiendo informe-trimestral-q3.pdf"><div class="progress__bar" style="width:60%;"></div></div>
2658
+ </div>
2659
+ <button class="file-upload__remove" type="button" aria-label="Cancelar informe-trimestral-q3.pdf"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2660
+ </li>
2661
+ <li class="file-upload__item">
2662
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>
2663
+ <div>
2664
+ <span class="file-upload__name">mockup-portada.png</span>
2665
+ <span class="file-upload__size">840 KB · completo</span>
2666
+ </div>
2667
+ <button class="file-upload__remove" type="button" aria-label="Quitar mockup-portada.png"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2668
+ </li>
2669
+ <li class="file-upload__item file-upload__item--error">
2670
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 8v4m0 4h.01"/></svg>
2671
+ <div>
2672
+ <span class="file-upload__name">video-demo.mov</span>
2673
+ <span class="file-upload__size">Error: supera los 10 MB</span>
2674
+ </div>
2675
+ <button class="file-upload__remove" type="button" aria-label="Quitar video-demo.mov"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
2676
+ </li>
2677
+ </ul>
2678
+ </div>
2679
+ </div>
2680
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;div class="file-upload"&gt;
2681
+ &lt;input class="file-upload__input visually-hidden" type="file" id="f" multiple
2682
+ aria-describedby="hint"&gt;
2683
+ &lt;label class="file-upload__label" for="f"&gt;
2684
+ &lt;svg aria-hidden="true"&gt;…subir…&lt;/svg&gt;
2685
+ &lt;span&gt;Arrastra archivos aquí o &lt;span class="file-upload__button"&gt;explora&lt;/span&gt;&lt;/span&gt;
2686
+ &lt;span class="file-upload__hint" id="hint"&gt;PNG, JPG o PDF · máximo 10 MB&lt;/span&gt;
2687
+ &lt;/label&gt;
2688
+ &lt;ul class="file-upload__list"&gt;
2689
+ &lt;li class="file-upload__item"&gt;
2690
+ &lt;svg aria-hidden="true"&gt;…archivo…&lt;/svg&gt;
2691
+ &lt;div&gt;
2692
+ &lt;span class="file-upload__name"&gt;informe.pdf&lt;/span&gt;
2693
+ &lt;span class="file-upload__size"&gt;2.4 MB · subiendo…&lt;/span&gt;
2694
+ &lt;div class="progress" role="progressbar" aria-valuenow="60" …&gt;
2695
+ &lt;div class="progress__bar" style="width:60%"&gt;&lt;/div&gt;&lt;/div&gt;
2696
+ &lt;/div&gt;
2697
+ &lt;button class="file-upload__remove" aria-label="Cancelar informe.pdf"&gt;×&lt;/button&gt;
2698
+ &lt;/li&gt;
2699
+ &lt;li class="file-upload__item file-upload__item--error"&gt;…icono + texto de error…&lt;/li&gt;
2700
+ &lt;/ul&gt;
2701
+ &lt;/div&gt;
2702
+ &lt;!-- JS: dragover añade .is-dragover; el botón × quita el ítem. --&gt;</code></pre></details>
2703
+ </section>
2704
+
2705
+ <!-- ================= STEPPER ================= -->
2706
+ <section id="comp-stepper">
2707
+ <h2>Stepper / Wizard</h2>
2708
+ <p>Indicador de progreso por pasos. Es un <span class="code">&lt;ol&gt;</span>, así que el orden y el conteo se anuncian solos; el paso en curso lleva <span class="code">aria-current="step"</span> y los pasos completos muestran un check (estado no solo por color). El conector se tiñe de acento en el tramo ya recorrido. Variante <span class="code">.stepper--vertical</span> para apilarlos.</p>
2709
+ <div class="ds-demo ds-demo--grid" style="align-items:start;">
2710
+ <ol class="stepper" aria-label="Progreso de la configuración" style="width:100%;">
2711
+ <li class="step step--complete">
2712
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2713
+ <span class="step__label">Cuenta</span>
2714
+ <span class="step__connector" aria-hidden="true"></span>
2715
+ </li>
2716
+ <li class="step step--complete">
2717
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2718
+ <span class="step__label">Perfil</span>
2719
+ <span class="step__connector" aria-hidden="true"></span>
2720
+ </li>
2721
+ <li class="step step--current" aria-current="step">
2722
+ <span class="step__marker"><span class="visually-hidden">Paso actual: </span>3</span>
2723
+ <span class="step__label">Pago</span>
2724
+ <span class="step__connector" aria-hidden="true"></span>
2725
+ </li>
2726
+ <li class="step step--upcoming">
2727
+ <span class="step__marker"><span class="visually-hidden">Pendiente: </span>4</span>
2728
+ <span class="step__label">Confirmación</span>
2729
+ </li>
2730
+ </ol>
2731
+ <ol class="stepper stepper--vertical" aria-label="Progreso del despliegue">
2732
+ <li class="step step--complete">
2733
+ <span class="step__marker"><span class="visually-hidden">Completado: </span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></span>
2734
+ <span class="step__label">Compilar el proyecto</span>
2735
+ <span class="step__connector" aria-hidden="true"></span>
2736
+ </li>
2737
+ <li class="step step--current" aria-current="step">
2738
+ <span class="step__marker"><span class="visually-hidden">Paso actual: </span>2</span>
2739
+ <span class="step__label">Ejecutar pruebas</span>
2740
+ <span class="step__connector" aria-hidden="true"></span>
2741
+ </li>
2742
+ <li class="step step--upcoming">
2743
+ <span class="step__marker"><span class="visually-hidden">Pendiente: </span>3</span>
2744
+ <span class="step__label">Publicar en producción</span>
2745
+ </li>
2746
+ </ol>
2747
+ </div>
2748
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>&lt;ol class="stepper" aria-label="Progreso de la configuración"&gt;
2749
+ &lt;li class="step step--complete"&gt;
2750
+ &lt;span class="step__marker"&gt;&lt;svg aria-hidden="true"&gt;…check…&lt;/svg&gt;&lt;/span&gt;
2751
+ &lt;span class="step__label"&gt;Cuenta&lt;/span&gt;
2752
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
2753
+ &lt;/li&gt;
2754
+ &lt;li class="step step--current" aria-current="step"&gt;
2755
+ &lt;span class="step__marker"&gt;3&lt;/span&gt;
2756
+ &lt;span class="step__label"&gt;Pago&lt;/span&gt;
2757
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
2758
+ &lt;/li&gt;
2759
+ &lt;li class="step step--upcoming"&gt;
2760
+ &lt;span class="step__marker"&gt;4&lt;/span&gt;&lt;span class="step__label"&gt;Confirmación&lt;/span&gt;
2761
+ &lt;/li&gt;
2762
+ &lt;/ol&gt;
2763
+ &lt;!-- El último paso no lleva conector. Variante: .stepper--vertical. --&gt;</code></pre></details>
2764
+ </section>
2765
+
2766
+ <!-- ================= KBD ================= -->
2767
+ <section id="comp-kbd">
2768
+ <h2>Kbd</h2>
2769
+ <p>Tecla o atajo de teclado sobre <span class="code">&lt;kbd&gt;</span>: JetBrains Mono, borde <span class="code">--border-strong</span> y sombra sutil, alto y ancho mínimo 20px. Para combos se encadenan varias con un "+" literal. Es informativa, nunca un botón.</p>
2770
+ <div class="ds-demo" style="gap:var(--space-4);align-items:center;">
2771
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">Esc</kbd></span>
2772
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">Tab</kbd></span>
2773
+ <span style="display:inline-flex;align-items:center;gap:6px;"><kbd class="kbd">↵</kbd></span>
2774
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd></span>
2775
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">S</kbd></span>
2776
+ <span style="display:inline-flex;align-items:center;gap:6px;color:var(--text-muted);"><kbd class="kbd">Shift</kbd> + <kbd class="kbd">⇥</kbd></span>
2777
+ </div>
2778
+ <details class="ds-code"><summary>Ver código<button class="ds-copy" type="button">Copiar</button></summary><pre><code>Pulsa &lt;kbd class="kbd"&gt;Esc&lt;/kbd&gt; para cerrar.
2779
+
2780
+ Abre el buscador con &lt;kbd class="kbd"&gt;⌘&lt;/kbd&gt; + &lt;kbd class="kbd"&gt;K&lt;/kbd&gt;.
2781
+ Guarda con &lt;kbd class="kbd"&gt;Ctrl&lt;/kbd&gt; + &lt;kbd class="kbd"&gt;S&lt;/kbd&gt;.</code></pre></details>
2782
+ </section>
2783
+
2010
2784
  <!-- ================= MOTION ================= -->
2011
2785
  <section id="motion">
2012
2786
  <h2>Motion</h2>
@@ -2042,16 +2816,22 @@ html { scroll-behavior: smooth; }
2042
2816
  <table class="table">
2043
2817
  <thead><tr><th>Combinación</th><th class="num">Contraste</th><th>Veredicto</th></tr></thead>
2044
2818
  <tbody>
2045
- <tr><td>night-100 sobre night-950</td><td class="num">≈ 16:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2046
- <tr><td>accent-400 sobre night-950</td><td class="num">≈ 8.5:1</td><td><span class="badge badge--success">AA — sirve como texto/link</span></td></tr>
2047
- <tr><td>night-950 sobre accent-400 (CTA)</td><td class="num">≈ 8.5:1</td><td><span class="badge badge--success">AA — texto del CTA oscuro</span></td></tr>
2048
- <tr><td>indigo-500 sobre night-950</td><td class="num">≈ 1.9:1</td><td><span class="badge badge--danger">Solo fondo, nunca texto</span></td></tr>
2049
- <tr><td>night-50 sobre indigo-500</td><td class="num">≈ 9:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2819
+ <tr><td>night-100 sobre night-950</td><td class="num">17.25:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2820
+ <tr><td>accent-400 sobre night-950</td><td class="num">9.50:1</td><td><span class="badge badge--success">AA — sirve como texto/link</span></td></tr>
2821
+ <tr><td>night-950 sobre accent-400 (CTA)</td><td class="num">9.50:1</td><td><span class="badge badge--success">AA — texto del CTA oscuro</span></td></tr>
2822
+ <tr><td>indigo-500 sobre night-950</td><td class="num">2.09:1</td><td><span class="badge badge--danger">Solo fondo, nunca texto</span></td></tr>
2823
+ <tr><td>night-50 sobre indigo-500</td><td class="num">9.29:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2824
+ <tr><td>accent-600 sobre blanco <em>(modo claro)</em></td><td class="num">4.01:1</td><td><span class="badge badge--warning">Solo UI — nunca texto</span></td></tr>
2825
+ <tr><td>accent-700 sobre blanco <em>(texto de acción en claro)</em></td><td class="num">5.95:1</td><td><span class="badge badge--success">AA</span></td></tr>
2826
+ <tr><td>accent-800 sobre blanco <em>(hover en claro)</em></td><td class="num">8.74:1</td><td><span class="badge badge--success">AAA</span></td></tr>
2827
+ <tr><td>info sobre info-bg <em>(badge en claro)</em></td><td class="num">5.20:1</td><td><span class="badge badge--success">AA</span></td></tr>
2050
2828
  </tbody>
2051
2829
  </table>
2052
2830
  </div>
2053
2831
  <ul style="color:var(--text-secondary);line-height:1.9;font-size:var(--text-sm);">
2054
2832
  <li>Mínimos WCAG AA: 4.5:1 texto normal, 3:1 texto grande y componentes UI.</li>
2833
+ <li>Ratios <strong>calculados</strong> con la fórmula WCAG 2.x, no estimados: <span class="code">mcp/test/contraste-tokens.test.ts</span> los recalcula desde <span class="code">tokens.css</span> en cada corrida.</li>
2834
+ <li>En modo claro, <span class="code">accent-600</span> (<span class="code">#1f87bb</span>) vale como <strong>UI</strong> (focus, bordes, iconos) pero <strong>no como texto</strong>: da 4.01:1. El texto de acción usa <span class="code">accent-700</span>.</li>
2055
2835
  <li>Focus visible siempre (<span class="code">:focus-visible</span> global); estado nunca solo con color — siempre color + icono + texto.</li>
2056
2836
  <li>Touch targets de 44×44px. IconButton siempre con <span class="code">aria-label</span>.</li>
2057
2837
  <li><span class="code">role="progressbar"</span> en Progress, <span class="code">aria-expanded</span> en Accordion, <span class="code">aria-current</span> en navegación.</li>
@@ -2117,7 +2897,7 @@ html { scroll-behavior: smooth; }
2117
2897
  --color-accent-100: #d9f0fb; --color-accent-200: #a8ddf6;
2118
2898
  --color-accent-300: #7dccf1; --color-accent-400: #52baed;
2119
2899
  --color-accent-500: #2fa3dd; --color-accent-600: #1f87bb;
2120
- --color-accent-700: #186a94;
2900
+ --color-accent-700: #186a94; --color-accent-800: #12506f;
2121
2901
  --color-indigo-300: #7a83c4; --color-indigo-400: #565fa3;
2122
2902
  --color-indigo-500: #353d7c; --color-indigo-600: #2b3264;
2123
2903
  --color-indigo-700: #22284f; --color-indigo-800: #1a1e3b;
@@ -2150,7 +2930,18 @@ html { scroll-behavior: smooth; }
2150
2930
  <!-- ================= INSTALACIÓN ================= -->
2151
2931
  <section id="instalacion">
2152
2932
  <h2>Instalación</h2>
2153
- <p>El sistema se distribuye como archivos en <span class="code">dist/nocturna-electrica/</span> — cópialos a tu proyecto (aún no hay paquete npm). Elige la vía según tu stack. En todas: <strong>dark por defecto</strong>; <span class="code">&lt;html data-theme="light"&gt;</span> activa el modo claro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
2933
+ <p>El sistema se distribuye como paquete npm (<span class="code">@higinio/design-css</span>) y también como archivos sueltos en <span class="code">dist/nocturna-electrica/</span>. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C/D). En todas: <strong>dark por defecto</strong>; <span class="code">&lt;html data-theme="light"&gt;</span> activa el modo claro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
2934
+
2935
+ <h3 style="margin-top:32px;">Instalar desde npm (recomendado)</h3>
2936
+ <p>Un solo paquete con los dos sistemas en subrutas <span class="code">/celeste/</span> y <span class="code">/nocturna/</span>. Instálalo y usa las vías de abajo apuntando a la subruta, en vez de a un archivo copiado.</p>
2937
+ <details class="ds-code" open><summary>terminal + CSS de entrada<button class="ds-copy" type="button">Copiar</button></summary><pre><code>npm i @higinio/design-css
2938
+
2939
+ /* framework completo (vía A) */
2940
+ @import "@higinio/design-css/nocturna/framework.css";
2941
+ /* o solo tokens (vía B) */
2942
+ @import "@higinio/design-css/nocturna/tokens.css";
2943
+ /* o Tailwind v4 (vía C) */
2944
+ @import "@higinio/design-css/nocturna/tailwind.css";</code></pre></details>
2154
2945
 
2155
2946
  <h3 style="margin-top:32px;">A · CSS drop-in (framework completo)</h3>
2156
2947
  <p>Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes.</p>
@@ -2242,14 +3033,14 @@ npm i tailwindcss @tailwindcss/postcss
2242
3033
 
2243
3034
  <div class="toast-stack" id="toastStack" role="status" aria-live="polite"></div>
2244
3035
  <div class="modal-overlay" id="demoModal" hidden>
2245
- <div class="modal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle">
3036
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle" aria-describedby="demoModalDesc" tabindex="-1">
2246
3037
  <div class="modal__header">
2247
3038
  <h4 class="modal__title" id="demoModalTitle">Archivar proyecto</h4>
2248
3039
  <button class="icon-btn icon-btn--ghost icon-btn--sm" data-close-modal aria-label="Cerrar">
2249
3040
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
2250
3041
  </button>
2251
3042
  </div>
2252
- <p style="font-size:var(--text-sm);color:var(--text-secondary);">El proyecto dejará de aparecer en el panel, pero conservará sus datos. Puedes restaurarlo cuando quieras.</p>
3043
+ <p id="demoModalDesc" style="font-size:var(--text-sm);color:var(--text-secondary);">El proyecto dejará de aparecer en el panel, pero conservará sus datos. Puedes restaurarlo cuando quieras.</p>
2253
3044
  <div class="modal__footer">
2254
3045
  <button class="btn btn--outline" data-close-modal>Cancelar</button>
2255
3046
  <button class="btn btn--primary" data-close-modal>Archivar</button>
@@ -2257,7 +3048,7 @@ npm i tailwindcss @tailwindcss/postcss
2257
3048
  </div>
2258
3049
  </div>
2259
3050
  <div class="modal-overlay" id="demoModalLg" hidden>
2260
- <div class="modal modal--lg" role="dialog" aria-modal="true" aria-labelledby="demoModalLgTitle">
3051
+ <div class="modal modal--lg" role="dialog" aria-modal="true" aria-labelledby="demoModalLgTitle" tabindex="-1">
2261
3052
  <div class="modal__header">
2262
3053
  <h4 class="modal__title" id="demoModalLgTitle">Invitar al equipo</h4>
2263
3054
  <button class="icon-btn icon-btn--ghost icon-btn--sm" data-close-modal aria-label="Cerrar">
@@ -2565,22 +3356,269 @@ document.querySelectorAll('[data-toast]').forEach(function(b){
2565
3356
  });
2566
3357
  });
2567
3358
 
2568
- /* ---------- Modal ---------- */
2569
- function wireModal(triggerId, modalId){
3359
+ /* ---------- Modal (focus-trap + retorno de foco + Esc) ---------- */
3360
+ var FOCUSABLE_SEL = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
3361
+ function getFocusable(container){
3362
+ return Array.prototype.slice.call(container.querySelectorAll(FOCUSABLE_SEL))
3363
+ .filter(function(el){ return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length; });
3364
+ }
3365
+ function wireModal(triggerId, overlayId){
2570
3366
  var trigger = document.getElementById(triggerId);
2571
- var modalEl = document.getElementById(modalId);
2572
- if (!trigger || !modalEl) return;
2573
- trigger.addEventListener('click', function(){ modalEl.hidden = false; });
2574
- modalEl.addEventListener('click', function(e){
2575
- if (e.target === modalEl || e.target.closest('[data-close-modal]')) modalEl.hidden = true;
3367
+ var overlay = document.getElementById(overlayId);
3368
+ if (!trigger || !overlay) return;
3369
+ var panel = overlay.querySelector('.modal');
3370
+ var persistent = overlay.hasAttribute('data-modal-persistent'); /* flujos destructivos */
3371
+ var lastFocused = null;
3372
+
3373
+ function open(){
3374
+ lastFocused = document.activeElement;
3375
+ overlay.hidden = false;
3376
+ if (panel) panel.focus(); /* el panel tiene tabindex="-1"; el lector anuncia el título */
3377
+ }
3378
+ function close(){
3379
+ if (overlay.hidden) return;
3380
+ overlay.hidden = true;
3381
+ if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus(); /* retorno de foco */
3382
+ }
3383
+
3384
+ trigger.addEventListener('click', open);
3385
+
3386
+ overlay.addEventListener('click', function(e){
3387
+ if (e.target.closest('[data-close-modal]')) { close(); return; }
3388
+ if (e.target === overlay && !persistent) close(); /* click en overlay */
2576
3389
  });
2577
- document.addEventListener('keydown', function(e){
2578
- if (e.key === 'Escape' && !modalEl.hidden) modalEl.hidden = true;
3390
+
3391
+ overlay.addEventListener('keydown', function(e){
3392
+ if (e.key === 'Escape' && !persistent) { e.stopPropagation(); close(); return; }
3393
+ if (e.key !== 'Tab' || !panel) return;
3394
+ var f = getFocusable(panel);
3395
+ if (!f.length) { e.preventDefault(); return; }
3396
+ var first = f[0], last = f[f.length - 1], active = document.activeElement;
3397
+ if (f.indexOf(active) === -1) { /* foco en el panel (tabindex=-1): entra al extremo correcto */
3398
+ e.preventDefault(); (e.shiftKey ? last : first).focus(); return;
3399
+ }
3400
+ if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
3401
+ else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
2579
3402
  });
2580
3403
  }
2581
3404
  wireModal('openModal', 'demoModal');
2582
3405
  wireModal('openModalLg', 'demoModalLg');
2583
3406
 
3407
+ /* ---------- Drawer (focus-trap + retorno de foco + Esc + overlay) ---------- */
3408
+ function wireDrawer(triggerId, drawerId){
3409
+ var trigger = document.getElementById(triggerId);
3410
+ var drawer = document.getElementById(drawerId);
3411
+ if (!trigger || !drawer) return;
3412
+ var panel = drawer.querySelector('.drawer__panel');
3413
+ var lastFocused = null;
3414
+ function open(){
3415
+ lastFocused = document.activeElement;
3416
+ drawer.hidden = false;
3417
+ /* dos pasos: pinta oculto y en el siguiente frame añade .is-open para que el transform anime */
3418
+ requestAnimationFrame(function(){ drawer.classList.add('is-open'); });
3419
+ if (panel) panel.focus(); /* tabindex="-1"; el lector anuncia el título */
3420
+ }
3421
+ function close(){
3422
+ if (drawer.hidden) return;
3423
+ drawer.classList.remove('is-open');
3424
+ var finish = function(){ drawer.hidden = true; if (lastFocused && typeof lastFocused.focus === 'function') lastFocused.focus(); };
3425
+ if (reduced) finish(); else setTimeout(finish, 250); /* espera el slide de salida (--duration-slow) */
3426
+ }
3427
+ trigger.addEventListener('click', open);
3428
+ drawer.addEventListener('click', function(e){
3429
+ if (e.target.closest('[data-close-drawer]')) close(); /* overlay o botón de cierre */
3430
+ });
3431
+ drawer.addEventListener('keydown', function(e){
3432
+ if (e.key === 'Escape') { e.stopPropagation(); close(); return; }
3433
+ if (e.key !== 'Tab' || !panel) return;
3434
+ var f = getFocusable(panel);
3435
+ if (!f.length) { e.preventDefault(); return; }
3436
+ var first = f[0], last = f[f.length - 1], active = document.activeElement;
3437
+ if (f.indexOf(active) === -1) { e.preventDefault(); (e.shiftKey ? last : first).focus(); return; }
3438
+ if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); }
3439
+ else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); }
3440
+ });
3441
+ }
3442
+ wireDrawer('openDrawer', 'demoDrawer');
3443
+
3444
+ /* ---------- Popover (abre por CLICK; foco entra; Esc / clic fuera cierran) ---------- */
3445
+ (function(){
3446
+ var pop = document.querySelector('#comp-popover .popover');
3447
+ var trigger = document.getElementById('popTrigger');
3448
+ var panel = document.getElementById('popPanel');
3449
+ if (!pop || !trigger || !panel) return;
3450
+ function open(){
3451
+ pop.classList.add('is-open');
3452
+ trigger.setAttribute('aria-expanded', 'true');
3453
+ var f = getFocusable(panel);
3454
+ if (f.length) f[0].focus();
3455
+ }
3456
+ function close(returnFocus){
3457
+ if (!pop.classList.contains('is-open')) return;
3458
+ pop.classList.remove('is-open');
3459
+ trigger.setAttribute('aria-expanded', 'false');
3460
+ if (returnFocus) trigger.focus();
3461
+ }
3462
+ trigger.addEventListener('click', function(e){
3463
+ e.stopPropagation();
3464
+ if (pop.classList.contains('is-open')) close(true); else open();
3465
+ });
3466
+ document.addEventListener('click', function(e){
3467
+ if (pop.classList.contains('is-open') && !pop.contains(e.target)) close(false);
3468
+ });
3469
+ document.addEventListener('keydown', function(e){
3470
+ if (e.key === 'Escape' && pop.classList.contains('is-open')) close(true);
3471
+ });
3472
+ })();
3473
+
3474
+ /* ---------- Combobox (filtra en vivo + teclado ↑/↓/Enter/Esc) ---------- */
3475
+ (function(){
3476
+ var input = document.getElementById('cbInput');
3477
+ var list = document.getElementById('cbList');
3478
+ if (!input || !list) return;
3479
+ var options = Array.prototype.slice.call(list.querySelectorAll('.combobox__option'));
3480
+ var activeIndex = -1;
3481
+ function visible(){ return options.filter(function(o){ return !o.hidden; }); }
3482
+ function openList(){ list.hidden = false; input.setAttribute('aria-expanded', 'true'); }
3483
+ function closeList(){ list.hidden = true; input.setAttribute('aria-expanded', 'false'); setActive(-1); }
3484
+ function setActive(i){
3485
+ options.forEach(function(o){ o.classList.remove('is-active'); });
3486
+ var vis = visible();
3487
+ activeIndex = i;
3488
+ if (i >= 0 && i < vis.length) {
3489
+ vis[i].classList.add('is-active');
3490
+ input.setAttribute('aria-activedescendant', vis[i].id);
3491
+ vis[i].scrollIntoView({ block: 'nearest' });
3492
+ } else {
3493
+ input.setAttribute('aria-activedescendant', '');
3494
+ }
3495
+ }
3496
+ function filter(){
3497
+ var q = input.value.trim().toLowerCase();
3498
+ options.forEach(function(o){
3499
+ o.hidden = o.textContent.toLowerCase().indexOf(q) === -1;
3500
+ });
3501
+ if (visible().length) openList(); else closeList();
3502
+ setActive(-1);
3503
+ }
3504
+ function choose(opt){
3505
+ options.forEach(function(o){ o.setAttribute('aria-selected', 'false'); o.hidden = false; });
3506
+ opt.setAttribute('aria-selected', 'true');
3507
+ input.value = opt.textContent.trim();
3508
+ closeList();
3509
+ input.focus();
3510
+ }
3511
+ input.addEventListener('input', filter);
3512
+ input.addEventListener('keydown', function(e){
3513
+ if (e.key === 'ArrowDown') {
3514
+ e.preventDefault();
3515
+ if (list.hidden) openList();
3516
+ var v = visible();
3517
+ setActive(activeIndex < 0 ? 0 : Math.min(activeIndex + 1, v.length - 1));
3518
+ } else if (e.key === 'ArrowUp') {
3519
+ e.preventDefault();
3520
+ if (list.hidden) openList();
3521
+ setActive(Math.max(activeIndex - 1, 0));
3522
+ } else if (e.key === 'Enter') {
3523
+ var v = visible();
3524
+ if (!list.hidden && activeIndex >= 0 && v[activeIndex]) { e.preventDefault(); choose(v[activeIndex]); }
3525
+ } else if (e.key === 'Escape') {
3526
+ if (!list.hidden) { e.preventDefault(); closeList(); }
3527
+ }
3528
+ });
3529
+ list.addEventListener('click', function(e){
3530
+ var opt = e.target.closest('.combobox__option');
3531
+ if (opt) choose(opt);
3532
+ });
3533
+ list.addEventListener('mousemove', function(e){
3534
+ var opt = e.target.closest('.combobox__option');
3535
+ if (!opt) return;
3536
+ var i = visible().indexOf(opt);
3537
+ if (i !== -1 && i !== activeIndex) setActive(i);
3538
+ });
3539
+ document.addEventListener('click', function(e){
3540
+ if (!list.hidden && e.target !== input && !list.contains(e.target)) closeList();
3541
+ });
3542
+ })();
3543
+
3544
+ /* ---------- Search (botón limpiar funcional) ---------- */
3545
+ (function(){
3546
+ var input = document.getElementById('searchInput');
3547
+ var clear = document.getElementById('searchClear');
3548
+ if (!input || !clear) return;
3549
+ function sync(){ clear.hidden = input.value.length === 0; }
3550
+ input.addEventListener('input', sync);
3551
+ clear.addEventListener('click', function(){ input.value = ''; sync(); input.focus(); });
3552
+ sync();
3553
+ })();
3554
+
3555
+ /* ---------- Checkbox indeterminate (solo se puede fijar por JS) ---------- */
3556
+ var cbInd = document.getElementById('cbIndeterminate');
3557
+ if (cbInd) cbInd.indeterminate = true;
3558
+
3559
+ /* ---------- Tabla: orden por columna + seleccionar todo ---------- */
3560
+ (function(){
3561
+ var table = document.getElementById('demoSortTable');
3562
+ if (!table) return;
3563
+ var tbody = table.tBodies[0];
3564
+ var headRow = table.tHead.rows[0];
3565
+
3566
+ function cellValue(row, idx){
3567
+ var cell = row.cells[idx];
3568
+ if (!cell) return '';
3569
+ var raw = cell.getAttribute('data-sort');
3570
+ if (raw !== null) return parseFloat(raw);
3571
+ if (cell.classList.contains('num')) return parseFloat(cell.textContent.replace(/[^\d.-]/g, '')) || 0;
3572
+ return cell.textContent.trim().toLowerCase();
3573
+ }
3574
+
3575
+ Array.prototype.forEach.call(headRow.cells, function(th, idx){
3576
+ var btn = th.querySelector('.th-sort');
3577
+ if (!btn) return;
3578
+ btn.addEventListener('click', function(){
3579
+ var current = th.getAttribute('aria-sort');
3580
+ var dir = current === 'ascending' ? 'descending' : 'ascending';
3581
+ /* una sola columna ordenada a la vez */
3582
+ Array.prototype.forEach.call(headRow.cells, function(other){
3583
+ if (other !== th && other.hasAttribute('aria-sort')) other.setAttribute('aria-sort', 'none');
3584
+ });
3585
+ th.setAttribute('aria-sort', dir);
3586
+ var rows = Array.prototype.slice.call(tbody.rows);
3587
+ rows.sort(function(a, b){
3588
+ var va = cellValue(a, idx), vb = cellValue(b, idx);
3589
+ if (va < vb) return dir === 'ascending' ? -1 : 1;
3590
+ if (va > vb) return dir === 'ascending' ? 1 : -1;
3591
+ return 0;
3592
+ });
3593
+ rows.forEach(function(r){ tbody.appendChild(r); });
3594
+ });
3595
+ });
3596
+
3597
+ /* Seleccionar todo: refleja el estado (todas / ninguna / indeterminate) */
3598
+ var selectAll = document.getElementById('tblSelectAll');
3599
+ function rowBoxes(){ return Array.prototype.slice.call(tbody.querySelectorAll('.col-select .checkbox')); }
3600
+ function syncSelectAll(){
3601
+ if (!selectAll) return;
3602
+ var boxes = rowBoxes(), checked = boxes.filter(function(b){ return b.checked; }).length;
3603
+ selectAll.checked = checked === boxes.length && boxes.length > 0;
3604
+ selectAll.indeterminate = checked > 0 && checked < boxes.length;
3605
+ }
3606
+ function syncRow(box){
3607
+ var tr = box.closest('tr');
3608
+ if (tr) tr.classList.toggle('row--selected', box.checked);
3609
+ }
3610
+ if (selectAll) {
3611
+ selectAll.addEventListener('change', function(){
3612
+ rowBoxes().forEach(function(b){ b.checked = selectAll.checked; syncRow(b); });
3613
+ selectAll.indeterminate = false;
3614
+ });
3615
+ }
3616
+ rowBoxes().forEach(function(b){
3617
+ b.addEventListener('change', function(){ syncRow(b); syncSelectAll(); });
3618
+ });
3619
+ syncSelectAll(); /* estado inicial: una fila marcada → indeterminate */
3620
+ })();
3621
+
2584
3622
  /* ---------- Progress demo ---------- */
2585
3623
  var seq = [22, 47, 64, 81, 95], seqI = 2;
2586
3624
  function paintProgressRing(v){
@@ -2708,6 +3746,129 @@ if (mReplayBtn) {
2708
3746
  });
2709
3747
  });
2710
3748
  }
3749
+
3750
+ /* ---------- DatePicker (calendario en popover; flechas + Enter/Esc) ---------- */
3751
+ (function(){
3752
+ var wrap = document.getElementById('datepicker');
3753
+ var input = document.getElementById('dpInput');
3754
+ var cal = document.getElementById('dpCalendar');
3755
+ var grid = document.getElementById('dpGrid');
3756
+ var title = document.getElementById('dpTitle');
3757
+ var prev = document.getElementById('dpPrev');
3758
+ var next = document.getElementById('dpNext');
3759
+ if (!wrap || !input || !cal || !grid) return;
3760
+
3761
+ var MONTHS = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];
3762
+ var today = new Date(2026, 6, 10); // 10 de julio de 2026 (fecha del demo)
3763
+ var selected = new Date(2026, 6, 15); // día seleccionado inicial
3764
+ var view = new Date(2026, 6, 1); // mes visible
3765
+ var focusDate = new Date(selected); // día con el roving tabindex
3766
+
3767
+ function sameDay(a, b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); }
3768
+ function fmtLabel(d){ return d.getDate() + ' de ' + MONTHS[d.getMonth()] + ' de ' + d.getFullYear(); }
3769
+
3770
+ function render(){
3771
+ var y = view.getFullYear(), m = view.getMonth();
3772
+ title.textContent = MONTHS[m] + ' ' + y;
3773
+ grid.innerHTML = '';
3774
+ var first = new Date(y, m, 1);
3775
+ var startOffset = (first.getDay() + 6) % 7; // la semana empieza en lunes
3776
+ var start = new Date(y, m, 1 - startOffset);
3777
+ for (var w = 0; w < 6; w++) {
3778
+ var row = document.createElement('div');
3779
+ row.className = 'calendar__week'; row.setAttribute('role', 'row');
3780
+ for (var d = 0; d < 7; d++) {
3781
+ var cur = new Date(start.getFullYear(), start.getMonth(), start.getDate() + (w*7 + d));
3782
+ var btn = document.createElement('button');
3783
+ btn.type = 'button';
3784
+ btn.className = 'calendar__day';
3785
+ btn.setAttribute('role', 'gridcell');
3786
+ btn.textContent = cur.getDate();
3787
+ btn.setAttribute('aria-label', fmtLabel(cur));
3788
+ btn.dataset.y = cur.getFullYear(); btn.dataset.m = cur.getMonth(); btn.dataset.d = cur.getDate();
3789
+ if (cur.getMonth() !== m) btn.classList.add('calendar__day--outside');
3790
+ if (sameDay(cur, today)) { btn.classList.add('calendar__day--today'); btn.setAttribute('aria-current', 'date'); }
3791
+ var isSel = sameDay(cur, selected);
3792
+ btn.setAttribute('aria-selected', isSel ? 'true' : 'false');
3793
+ if (isSel) btn.classList.add('calendar__day--selected');
3794
+ btn.tabIndex = sameDay(cur, focusDate) ? 0 : -1;
3795
+ row.appendChild(btn);
3796
+ }
3797
+ grid.appendChild(row);
3798
+ }
3799
+ }
3800
+ function focusRoving(){ var f = grid.querySelector('.calendar__day[tabindex="0"]'); if (f) f.focus(); }
3801
+ function open(){ render(); cal.hidden = false; input.setAttribute('aria-expanded', 'true'); focusRoving(); }
3802
+ function close(returnFocus){
3803
+ if (cal.hidden) return;
3804
+ cal.hidden = true;
3805
+ input.setAttribute('aria-expanded', 'false');
3806
+ if (returnFocus) input.focus();
3807
+ }
3808
+ function setView(y, m){ view = new Date(y, m, 1); render(); }
3809
+ function moveFocus(newDate){
3810
+ focusDate = newDate;
3811
+ setView(newDate.getFullYear(), newDate.getMonth());
3812
+ focusRoving();
3813
+ }
3814
+ function choose(y, m, d){
3815
+ selected = new Date(y, m, d);
3816
+ focusDate = new Date(selected);
3817
+ input.value = fmtLabel(selected);
3818
+ close(true);
3819
+ }
3820
+
3821
+ input.addEventListener('click', function(){ if (cal.hidden) open(); else close(true); });
3822
+ prev.addEventListener('click', function(){ focusDate = new Date(view.getFullYear(), view.getMonth() - 1, Math.min(focusDate.getDate(), 28)); setView(view.getFullYear(), view.getMonth() - 1); });
3823
+ next.addEventListener('click', function(){ focusDate = new Date(view.getFullYear(), view.getMonth() + 1, Math.min(focusDate.getDate(), 28)); setView(view.getFullYear(), view.getMonth() + 1); });
3824
+
3825
+ grid.addEventListener('click', function(e){
3826
+ var b = e.target.closest('.calendar__day');
3827
+ if (!b || b.disabled) return;
3828
+ choose(+b.dataset.y, +b.dataset.m, +b.dataset.d);
3829
+ });
3830
+ grid.addEventListener('keydown', function(e){
3831
+ var b = e.target.closest('.calendar__day');
3832
+ if (!b) return;
3833
+ var cur = new Date(+b.dataset.y, +b.dataset.m, +b.dataset.d), delta = 0;
3834
+ if (e.key === 'ArrowLeft') delta = -1;
3835
+ else if (e.key === 'ArrowRight') delta = 1;
3836
+ else if (e.key === 'ArrowUp') delta = -7;
3837
+ else if (e.key === 'ArrowDown') delta = 7;
3838
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); choose(+b.dataset.y, +b.dataset.m, +b.dataset.d); return; }
3839
+ else if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
3840
+ else if (e.key === 'Home') { e.preventDefault(); moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() - ((cur.getDay()+6)%7))); return; }
3841
+ else if (e.key === 'End') { e.preventDefault(); moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + (6 - ((cur.getDay()+6)%7)))); return; }
3842
+ else return;
3843
+ e.preventDefault();
3844
+ moveFocus(new Date(cur.getFullYear(), cur.getMonth(), cur.getDate() + delta));
3845
+ });
3846
+ document.addEventListener('click', function(e){
3847
+ if (!cal.hidden && !wrap.contains(e.target)) close(false);
3848
+ });
3849
+
3850
+ render(); // estado inicial coherente aunque el panel esté oculto
3851
+ })();
3852
+
3853
+ /* ---------- FileUpload (dragover + quitar ítem) ---------- */
3854
+ (function(){
3855
+ var fu = document.getElementById('fileUpload');
3856
+ var list = document.getElementById('fuList');
3857
+ if (!fu) return;
3858
+ var depth = 0; // cuenta enter/leave anidados para no parpadear con hijos
3859
+ fu.addEventListener('dragenter', function(e){ e.preventDefault(); depth++; fu.classList.add('is-dragover'); });
3860
+ fu.addEventListener('dragover', function(e){ e.preventDefault(); });
3861
+ fu.addEventListener('dragleave', function(){ depth--; if (depth <= 0) { depth = 0; fu.classList.remove('is-dragover'); } });
3862
+ fu.addEventListener('drop', function(e){ e.preventDefault(); depth = 0; fu.classList.remove('is-dragover'); });
3863
+ if (list) {
3864
+ list.addEventListener('click', function(e){
3865
+ var btn = e.target.closest('.file-upload__remove');
3866
+ if (!btn) return;
3867
+ var item = btn.closest('.file-upload__item');
3868
+ if (item) item.parentNode.removeChild(item);
3869
+ });
3870
+ }
3871
+ })();
2711
3872
  })();
2712
3873
  </script>
2713
3874
  </body>