@higinio/design-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -291,6 +291,8 @@ html, body { overflow-x: clip; }
291
291
  .field__help { font-size: var(--text-sm); color: var(--text-muted); }
292
292
  .field__error { font-size: var(--text-sm); color: var(--danger-500); display: inline-flex; align-items: center; gap: 6px; }
293
293
  .field__error svg { width: 14px; height: 14px; flex: none; }
294
+ /* Campo obligatorio (§10.4): asterisco decorativo; el input lleva required/aria-required */
295
+ .field--required .field__label::after { content: " *"; color: var(--danger-500); }
294
296
 
295
297
  .input, .textarea, .select {
296
298
  width: 100%; font-family: var(--font-body); font-size: var(--text-base);
@@ -320,7 +322,7 @@ html, body { overflow-x: clip; }
320
322
  .input-icon .input { padding-left: 40px; }
321
323
 
322
324
  .textarea { min-height: 96px; padding: 12px 14px; resize: vertical; line-height: var(--leading-base); }
323
- .textarea-counter { font-size: var(--text-xs); color: var(--text-disabled); text-align: right; }
325
+ .textarea-counter { font-size: var(--text-xs); color: var(--text-muted); text-align: right; }
324
326
 
325
327
  .select-wrap { position: relative; }
326
328
  .select { appearance: none; padding-right: 38px; cursor: pointer; }
@@ -402,6 +404,15 @@ html, body { overflow-x: clip; }
402
404
  .checkbox input:checked ~ .checkbox__box svg { stroke-dashoffset: 0; }
403
405
  .checkbox input:focus-visible ~ .checkbox__box { box-shadow: var(--ring); }
404
406
  .checkbox input:disabled ~ * { opacity: .5; cursor: not-allowed; }
407
+ /* Estado indeterminate (§10.9): guion horizontal para "seleccionar todo" parcial */
408
+ .checkbox__box { position: relative; }
409
+ .checkbox input:indeterminate ~ .checkbox__box { background: var(--accent); border-color: var(--accent); }
410
+ .checkbox input:indeterminate ~ .checkbox__box svg { visibility: hidden; }
411
+ .checkbox input:indeterminate ~ .checkbox__box::after {
412
+ content: ""; position: absolute; top: 50%; left: 50%;
413
+ width: 10px; height: 2px;
414
+ background: var(--text-on-primary); transform: translate(-50%, -50%);
415
+ }
405
416
 
406
417
  .radio__dot {
407
418
  width: 20px; height: 20px; flex: none; display: grid; place-items: center;
@@ -504,6 +515,19 @@ html, body { overflow-x: clip; }
504
515
  .avatar__dot--busy { background: var(--danger-500); }
505
516
  .avatar__dot--away { background: var(--warning-500); }
506
517
 
518
+ /* AvatarGroup (§10.27): stack solapado con anillo de superficie */
519
+ .avatar-group { display: inline-flex; align-items: center; }
520
+ .avatar-group .avatar { border: 2px solid var(--surface); border-radius: var(--radius-full); }
521
+ .avatar-group .avatar + .avatar,
522
+ .avatar-group .avatar-group__more { margin-left: -8px; }
523
+ .avatar-group__more {
524
+ display: grid; place-items: center; flex: none;
525
+ width: 36px; height: 36px; border-radius: var(--radius-full);
526
+ border: 2px solid var(--surface);
527
+ background: var(--accent-soft-strong); color: var(--text-on-primary-solid);
528
+ font-family: var(--font-body); font-size: var(--text-xs); font-weight: var(--weight-semibold);
529
+ }
530
+
507
531
  .divider { border: 0; border-top: var(--border-width) solid var(--border); margin: var(--space-4) 0; }
508
532
  .divider--label {
509
533
  display: flex; align-items: center; gap: 12px; border: 0; margin: var(--space-4) 0;
@@ -584,7 +608,7 @@ html, body { overflow-x: clip; }
584
608
  }
585
609
  .alert > svg { width: 20px; height: 20px; flex: none; margin-top: 1px; }
586
610
  .alert__title { font-weight: var(--weight-semibold); margin: 0 0 2px; }
587
- .alert__body { opacity: .92; margin: 0; }
611
+ .alert__body { margin: 0; }
588
612
  .alert__close {
589
613
  margin-left: auto; flex: none; display: grid; place-items: center;
590
614
  width: 36px; height: 36px; border: 0; border-radius: var(--radius-sm);
@@ -755,6 +779,30 @@ html, body { overflow-x: clip; }
755
779
  .table tbody tr.is-selected { background: var(--accent-soft); }
756
780
  .table .num, .table td.num, .table th.num { text-align: right; font-family: var(--font-mono); }
757
781
 
782
+ /* Variantes de tabla (§10.21): zebra, sticky header, orden y selección */
783
+ .table--zebra tbody tr:nth-child(even) { background: var(--surface-alt); }
784
+ .table--zebra tbody tr:nth-child(even):hover { background: var(--bg); }
785
+ .table--sticky thead th { position: sticky; top: 0; z-index: 1; }
786
+ .table tbody tr.row--selected,
787
+ .table tbody tr.row--selected:hover { background: var(--accent-soft); }
788
+ .table .col-select { width: 44px; text-align: center; }
789
+ .table .col-select .checkbox { display: inline-flex; justify-content: center; gap: 0; }
790
+
791
+ .th-sort {
792
+ display: inline-flex; align-items: center; gap: 6px; width: 100%;
793
+ border: 0; background: transparent; cursor: pointer;
794
+ font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; text-align: left;
795
+ }
796
+ .th-sort--num { justify-content: flex-end; }
797
+ .th-sort .sort-icon {
798
+ width: 14px; height: 14px; flex: none; opacity: .35;
799
+ transition: opacity var(--duration-fast) var(--ease-out),
800
+ transform var(--duration-base) var(--ease-out);
801
+ }
802
+ .table th[aria-sort="ascending"] .sort-icon,
803
+ .table th[aria-sort="descending"] .sort-icon { opacity: 1; color: var(--accent); }
804
+ .table th[aria-sort="descending"] .sort-icon { transform: rotate(180deg); }
805
+
758
806
  .stat__label {
759
807
  display: inline-flex; align-items: center; gap: 8px; margin-bottom: 8px;
760
808
  font-size: var(--text-sm); color: var(--text-muted);
@@ -775,6 +823,32 @@ html, body { overflow-x: clip; }
775
823
  .stat__delta--up { color: var(--success-text); }
776
824
  .stat__delta--down { color: var(--danger-text); }
777
825
 
826
+ /* Chip de métrica (§10.22): rótulo y valor en una pastilla de una línea */
827
+ .metric-chips { display: flex; flex-wrap: wrap; gap: var(--space-2); }
828
+ .metric-chip {
829
+ display: inline-flex; align-items: center; gap: var(--space-2);
830
+ padding: var(--space-1) var(--space-3);
831
+ border: var(--border-width) solid var(--border); border-radius: var(--radius-full);
832
+ background: var(--surface);
833
+ font-family: var(--font-body); font-size: var(--text-sm); line-height: 1.5;
834
+ color: var(--text-secondary); white-space: nowrap;
835
+ }
836
+ .metric-chip svg { width: 16px; height: 16px; flex: none; color: var(--text-muted); }
837
+ .metric-chip__value {
838
+ font-weight: var(--weight-semibold); color: var(--link);
839
+ font-variant-numeric: tabular-nums;
840
+ }
841
+ button.metric-chip {
842
+ min-height: var(--control-height-md); padding-inline: var(--space-4);
843
+ cursor: pointer; text-align: left;
844
+ transition: background-color var(--duration-fast) var(--ease-out),
845
+ border-color var(--duration-fast) var(--ease-out),
846
+ transform var(--duration-instant) var(--ease-out);
847
+ }
848
+ button.metric-chip:hover { background: var(--accent-soft); border-color: var(--accent); }
849
+ button.metric-chip:active { background: var(--accent-soft-strong); transform: scale(.98); }
850
+ button.metric-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
851
+
778
852
  .pagination { display: flex; align-items: center; gap: 4px; list-style: none; padding: 0; margin: 0; }
779
853
  .pagination__btn {
780
854
  display: inline-grid; place-items: center;
@@ -820,7 +894,7 @@ html, body { overflow-x: clip; }
820
894
  .sidebar__item {
821
895
  display: flex; align-items: center; gap: 10px;
822
896
  padding: 10px 12px; border-radius: var(--radius-md);
823
- font-size: var(--text-sm); font-weight: var(--weight-medium);
897
+ font-family: var(--font-body); font-size: var(--text-sm); font-weight: var(--weight-medium);
824
898
  color: var(--text-secondary); text-decoration: none; border: 0; background: transparent;
825
899
  cursor: pointer; text-align: left; width: 100%;
826
900
  transition: background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out);
@@ -830,6 +904,68 @@ html, body { overflow-x: clip; }
830
904
  .sidebar__item.is-active { background: var(--accent-soft-strong); color: var(--link); }
831
905
  .sidebar__item.is-active svg { color: var(--accent); }
832
906
 
907
+ /* Grupos con rótulo dentro del sidebar */
908
+ .sidebar__group { display: flex; flex-direction: column; gap: 2px; }
909
+ .sidebar__group + .sidebar__group { margin-top: var(--space-4); }
910
+ .sidebar__group-label {
911
+ padding: 0 12px var(--space-1);
912
+ font-size: var(--text-xs); font-weight: var(--weight-semibold);
913
+ text-transform: uppercase; letter-spacing: var(--tracking-label);
914
+ color: var(--text-muted); white-space: nowrap;
915
+ }
916
+ .sidebar__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
917
+
918
+ /* Sidebar contraíble (§10.24): expandido 260 px, riel de iconos de 64 px.
919
+ El ancho cambia sin transición; solo los rótulos se funden con opacity y transform. */
920
+ .sidebar--collapsible .sidebar__label {
921
+ transition: opacity var(--duration-fast) var(--ease-out),
922
+ transform var(--duration-fast) var(--ease-out);
923
+ }
924
+ .sidebar__toggle { margin-top: auto; }
925
+ .sidebar.is-collapsed { width: 64px; padding-inline: var(--space-3); }
926
+ .sidebar.is-collapsed .sidebar__item { position: relative; justify-content: center; padding-inline: 0; }
927
+ .sidebar.is-collapsed .sidebar__toggle svg { transform: rotate(180deg); }
928
+ .sidebar.is-collapsed .sidebar__group + .sidebar__group { margin-top: var(--space-2); }
929
+ /* En el riel, el rótulo de grupo es un separador de 1 px; su texto sigue en el árbol de accesibilidad */
930
+ .sidebar.is-collapsed .sidebar__group-label {
931
+ height: 1px; padding: 0; margin: 0 var(--space-2) var(--space-2);
932
+ overflow: hidden; color: transparent; background: var(--border);
933
+ }
934
+ /* Oculto a la vista pero accesible (patrón de .u-visually-hidden) */
935
+ .sidebar.is-collapsed .sidebar__label,
936
+ .sidebar.is-collapsed .sidebar__group:first-child > .sidebar__group-label {
937
+ position: absolute; width: 1px; height: 1px; margin: -1px;
938
+ clip-path: inset(50%); overflow: hidden; white-space: nowrap;
939
+ }
940
+ .sidebar.is-collapsed .sidebar__label {
941
+ border-radius: var(--radius-sm); background: var(--tooltip-bg); color: var(--tooltip-text);
942
+ font-size: var(--text-xs); line-height: 16px;
943
+ pointer-events: none; z-index: 70;
944
+ opacity: 0; transform: translateX(-4px);
945
+ }
946
+ /* Tooltip del riel: el propio rótulo aparece a la derecha del ítem, también con teclado */
947
+ .sidebar.is-collapsed .sidebar__item:focus-visible .sidebar__label {
948
+ left: calc(100% + 8px); top: calc(50% - 14px); width: auto; height: auto;
949
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
950
+ opacity: 1; transform: none; transition-delay: 400ms;
951
+ }
952
+ /* Por hover solo con puntero real — en touch quedaría pegado tras el tap */
953
+ @media (hover: hover) and (pointer: fine) {
954
+ .sidebar.is-collapsed .sidebar__item:hover .sidebar__label {
955
+ left: calc(100% + 8px); top: calc(50% - 14px); width: auto; height: auto;
956
+ margin: 0; padding: 6px 10px; clip-path: none; overflow: visible;
957
+ opacity: 1; transform: none; transition-delay: 400ms;
958
+ }
959
+ }
960
+ /* Por debajo de 768 px la navegación vive en un Drawer --left (§10.29), que abre el botón ☰
961
+ .sidebar-trigger (combinado con .icon-btn); desde 768 px el sidebar es fijo y el botón sobra */
962
+ @media (max-width: 767px) {
963
+ .sidebar--collapsible { display: none; }
964
+ }
965
+ @media (min-width: 768px) {
966
+ .sidebar-trigger { display: none; }
967
+ }
968
+
833
969
  .tabs { display: flex; gap: var(--space-6); border-bottom: var(--border-width) solid var(--border); }
834
970
  .tabs__tab {
835
971
  padding: 12px 4px; margin-bottom: -1px;
@@ -864,6 +1000,299 @@ html, body { overflow-x: clip; }
864
1000
  border-radius: var(--radius-sm); padding: 1px 6px;
865
1001
  }
866
1002
 
1003
+ /* Combobox / Autocomplete (§10.28) */
1004
+ .combobox { position: relative; }
1005
+ .combobox__input {
1006
+ width: 100%; height: var(--control-height-md); padding: 0 14px;
1007
+ font-family: var(--font-body); font-size: var(--text-base);
1008
+ color: var(--text-primary); background: var(--surface);
1009
+ border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
1010
+ transition: border-color var(--duration-fast) var(--ease-out),
1011
+ box-shadow var(--duration-fast) var(--ease-out);
1012
+ }
1013
+ .combobox__input:hover:not(:disabled):not(:focus) { border-color: var(--text-disabled); }
1014
+ .combobox__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
1015
+ .combobox__input:disabled { background: var(--surface-alt); color: var(--text-disabled); cursor: not-allowed; }
1016
+ .combobox__list {
1017
+ position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 50;
1018
+ max-height: 264px; overflow-y: auto; margin: 0; padding: 6px; list-style: none;
1019
+ background: var(--surface); border: var(--border-width) solid var(--border);
1020
+ border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
1021
+ opacity: 0; transform: translateY(-4px); pointer-events: none;
1022
+ transition: opacity var(--duration-base) var(--ease-out),
1023
+ transform var(--duration-base) var(--ease-out);
1024
+ }
1025
+ .combobox.is-open .combobox__list { opacity: 1; transform: none; pointer-events: auto; }
1026
+ .combobox__option {
1027
+ display: flex; align-items: center; gap: 8px;
1028
+ padding: 10px 12px; border-radius: var(--radius-sm);
1029
+ font-size: var(--text-sm); color: var(--text-primary); cursor: pointer;
1030
+ transition: background-color var(--duration-fast) var(--ease-out);
1031
+ }
1032
+ .combobox__option .combobox__check {
1033
+ width: 16px; height: 16px; margin-left: auto; flex: none;
1034
+ color: var(--accent); opacity: 0;
1035
+ }
1036
+ .combobox__option.is-active { background: var(--accent-soft); }
1037
+ .combobox__option[aria-selected="true"] { color: var(--link); font-weight: var(--weight-medium); }
1038
+ .combobox__option[aria-selected="true"] .combobox__check { opacity: 1; }
1039
+ .combobox__empty { padding: 10px 12px; font-size: var(--text-sm); color: var(--text-muted); }
1040
+
1041
+ /* Búsqueda (§10.28) */
1042
+ .search { position: relative; display: flex; align-items: center; }
1043
+ .search__icon {
1044
+ position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
1045
+ width: 18px; height: 18px; color: var(--text-disabled); pointer-events: none;
1046
+ }
1047
+ .search__input {
1048
+ width: 100%; height: var(--control-height-md); padding: 0 40px;
1049
+ font-family: var(--font-body); font-size: var(--text-base);
1050
+ color: var(--text-primary); background: var(--surface);
1051
+ border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
1052
+ transition: border-color var(--duration-fast) var(--ease-out),
1053
+ box-shadow var(--duration-fast) var(--ease-out);
1054
+ }
1055
+ .search__input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
1056
+ .search__input:hover:not(:focus) { border-color: var(--text-disabled); }
1057
+ .search__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
1058
+ .search__clear {
1059
+ position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
1060
+ display: none; width: 32px; height: 32px; align-items: center; justify-content: center;
1061
+ border: 0; border-radius: var(--radius-full); background: transparent;
1062
+ color: var(--text-muted); cursor: pointer;
1063
+ transition: background-color var(--duration-fast) var(--ease-out),
1064
+ color var(--duration-fast) var(--ease-out);
1065
+ }
1066
+ .search__clear:hover { background: var(--surface-alt); color: var(--text-primary); }
1067
+ .search__clear svg { width: 16px; height: 16px; }
1068
+ .search__input:not(:placeholder-shown) ~ .search__clear { display: inline-flex; }
1069
+
1070
+ /* Drawer / Sheet (§10.29) */
1071
+ .drawer {
1072
+ position: fixed; inset: 0; z-index: 100;
1073
+ visibility: hidden; pointer-events: none;
1074
+ transition: visibility 0s linear var(--duration-base);
1075
+ }
1076
+ .drawer.is-open { visibility: visible; pointer-events: auto; transition-delay: 0s; }
1077
+ .drawer__overlay {
1078
+ position: absolute; inset: 0; background: var(--overlay);
1079
+ opacity: 0; transition: opacity var(--duration-base) var(--ease-out);
1080
+ }
1081
+ .drawer.is-open .drawer__overlay { opacity: 1; }
1082
+ .drawer__panel {
1083
+ position: absolute; inset: 0 0 0 auto; width: min(400px, 100%);
1084
+ display: flex; flex-direction: column; gap: var(--space-5);
1085
+ padding: var(--card-padding); background: var(--surface);
1086
+ box-shadow: var(--shadow-xl); overflow-y: auto; overscroll-behavior: contain;
1087
+ transform: translateX(100%);
1088
+ transition: transform var(--duration-base) var(--ease-in);
1089
+ }
1090
+ .drawer.is-open .drawer__panel {
1091
+ transform: none;
1092
+ transition: transform var(--duration-slow) var(--ease-out);
1093
+ }
1094
+ .drawer--right .drawer__panel { inset: 0 0 0 auto; transform: translateX(100%); }
1095
+ .drawer--left .drawer__panel { inset: 0 auto 0 0; transform: translateX(-100%); }
1096
+ .drawer--bottom .drawer__panel {
1097
+ inset: auto 0 0 0; width: 100%; max-height: 85vh;
1098
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0; transform: translateY(100%);
1099
+ }
1100
+ .drawer__header {
1101
+ display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
1102
+ }
1103
+ .drawer__title { font-size: var(--text-lg); font-weight: var(--weight-semibold); margin: 0; }
1104
+ .drawer__body { flex: 1; }
1105
+ .drawer__footer {
1106
+ display: flex; justify-content: flex-end; gap: var(--space-3); margin-top: auto;
1107
+ }
1108
+
1109
+ /* Popover (§10.30) */
1110
+ .popover { position: relative; display: inline-block; }
1111
+ .popover__panel {
1112
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1113
+ width: max-content; max-width: 300px; padding: var(--space-4);
1114
+ background: var(--surface); border: var(--border-width) solid var(--border);
1115
+ border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
1116
+ transform-origin: top left; opacity: 0; transform: scale(.96); pointer-events: none;
1117
+ transition: opacity var(--duration-base) var(--ease-out),
1118
+ transform var(--duration-base) var(--ease-out);
1119
+ }
1120
+ .popover.is-open .popover__panel { opacity: 1; transform: none; pointer-events: auto; }
1121
+ .popover--right .popover__panel { left: auto; right: 0; transform-origin: top right; }
1122
+
1123
+ /* ButtonGroup (§10.31) */
1124
+ .btn-group { display: inline-flex; }
1125
+ .btn-group .btn { border-radius: 0; }
1126
+ .btn-group .btn:first-child {
1127
+ border-top-left-radius: var(--radius-md); border-bottom-left-radius: var(--radius-md);
1128
+ }
1129
+ .btn-group .btn:last-child {
1130
+ border-top-right-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md);
1131
+ }
1132
+ .btn-group .btn:not(:first-child) { margin-left: calc(var(--border-width) * -1); }
1133
+ .btn-group .btn:hover, .btn-group .btn:focus-visible { z-index: 1; }
1134
+
1135
+ /* DatePicker (§10.32) */
1136
+ .datepicker { position: relative; display: inline-block; }
1137
+ .datepicker__input {
1138
+ width: 100%; height: var(--control-height-md); padding: 0 40px 0 14px;
1139
+ font-family: var(--font-body); font-size: var(--text-base);
1140
+ color: var(--text-primary); background: var(--surface);
1141
+ border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-sm);
1142
+ transition: border-color var(--duration-fast) var(--ease-out),
1143
+ box-shadow var(--duration-fast) var(--ease-out);
1144
+ }
1145
+ .datepicker__input:hover:not(:disabled):not(:focus) { border-color: var(--text-disabled); }
1146
+ .datepicker__input:focus { outline: none; border-color: var(--accent); box-shadow: var(--ring); }
1147
+ .datepicker__input:disabled { background: var(--surface-alt); color: var(--text-disabled); cursor: not-allowed; }
1148
+ .datepicker__icon {
1149
+ position: absolute; right: 12px; top: calc(var(--control-height-md) / 2); transform: translateY(-50%);
1150
+ width: 18px; height: 18px; color: var(--text-muted); pointer-events: none;
1151
+ }
1152
+ .datepicker__calendar {
1153
+ position: absolute; top: calc(100% + 8px); left: 0; z-index: 60;
1154
+ width: max-content; padding: var(--space-4);
1155
+ background: var(--surface); border: var(--border-width) solid var(--border);
1156
+ border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
1157
+ transform-origin: top left; opacity: 0; transform: scale(.96); pointer-events: none;
1158
+ transition: opacity var(--duration-base) var(--ease-out),
1159
+ transform var(--duration-base) var(--ease-out);
1160
+ }
1161
+ .datepicker.is-open .datepicker__calendar { opacity: 1; transform: none; pointer-events: auto; }
1162
+ .datepicker--right .datepicker__calendar { left: auto; right: 0; transform-origin: top right; }
1163
+
1164
+ .calendar { width: 15.75rem; }
1165
+ .calendar__header {
1166
+ display: flex; align-items: center; justify-content: space-between;
1167
+ gap: var(--space-2); margin-bottom: var(--space-3);
1168
+ }
1169
+ .calendar__title { font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-primary); }
1170
+ .calendar__weekdays,
1171
+ .calendar__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
1172
+ .calendar__weekdays { margin-bottom: 4px; }
1173
+ .calendar__week { display: contents; }
1174
+ .calendar__weekday {
1175
+ display: flex; align-items: center; justify-content: center; height: 28px;
1176
+ font-size: var(--text-xs); font-weight: var(--weight-medium); color: var(--text-muted);
1177
+ }
1178
+ .calendar__day {
1179
+ display: flex; align-items: center; justify-content: center;
1180
+ width: 34px; height: 34px; margin: 0 auto; padding: 0;
1181
+ border: 0; border-radius: var(--radius-sm); background: transparent;
1182
+ font-family: var(--font-body); font-size: var(--text-sm); color: var(--text-primary);
1183
+ cursor: pointer;
1184
+ transition: background-color var(--duration-fast) var(--ease-out),
1185
+ color var(--duration-fast) var(--ease-out);
1186
+ }
1187
+ .calendar__day:hover:not(:disabled):not(.calendar__day--selected) { background: var(--accent-soft); }
1188
+ .calendar__day--outside { color: var(--text-muted); }
1189
+ .calendar__day--today { font-weight: var(--weight-semibold); box-shadow: inset 0 0 0 1px var(--border-strong); }
1190
+ .calendar__day--selected { background: var(--accent); color: var(--text-on-primary); font-weight: var(--weight-semibold); }
1191
+ .calendar__day:disabled { color: var(--text-disabled); cursor: not-allowed; }
1192
+
1193
+ /* FileUpload (§10.33) */
1194
+ .file-upload {
1195
+ border: 2px dashed var(--border-strong); border-radius: var(--radius-lg);
1196
+ background: var(--surface);
1197
+ transition: border-color var(--duration-fast) var(--ease-out),
1198
+ background-color var(--duration-fast) var(--ease-out);
1199
+ }
1200
+ .file-upload.is-dragover { border-color: var(--accent); background: var(--accent-soft); }
1201
+ .file-upload__input {
1202
+ position: absolute; width: 1px; height: 1px; margin: -1px;
1203
+ clip-path: inset(50%); overflow: hidden; white-space: nowrap;
1204
+ }
1205
+ .file-upload__label {
1206
+ display: flex; flex-direction: column; align-items: center; text-align: center;
1207
+ gap: var(--space-2); padding: var(--space-8) var(--space-6); cursor: pointer;
1208
+ }
1209
+ .file-upload__label > svg { width: 32px; height: 32px; color: var(--text-muted); }
1210
+ .file-upload__title { font-size: var(--text-sm); color: var(--text-secondary); }
1211
+ .file-upload__hint { font-size: var(--text-xs); color: var(--text-muted); }
1212
+ .file-upload__button {
1213
+ display: inline-flex; align-items: center; justify-content: center;
1214
+ height: var(--control-height-sm); padding: 0 16px; margin-top: var(--space-1);
1215
+ border: var(--border-width) solid var(--border-strong); border-radius: var(--radius-md);
1216
+ font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-primary);
1217
+ background: var(--surface);
1218
+ transition: background-color var(--duration-fast) var(--ease-out);
1219
+ }
1220
+ .file-upload__label:hover .file-upload__button { background: var(--bg); }
1221
+ .file-upload__list { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-2); }
1222
+ .file-upload__item {
1223
+ display: flex; align-items: center; gap: var(--space-3);
1224
+ padding: var(--space-3); border: var(--border-width) solid var(--border);
1225
+ border-radius: var(--radius-md); background: var(--surface);
1226
+ }
1227
+ .file-upload__icon { width: 20px; height: 20px; color: var(--text-muted); flex: none; }
1228
+ .file-upload__meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
1229
+ .file-upload__name {
1230
+ font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary);
1231
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1232
+ }
1233
+ .file-upload__size { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-xs); color: var(--text-muted); }
1234
+ .file-upload__size svg { width: 12px; height: 12px; flex: none; }
1235
+ .file-upload__meta .progress { margin-top: 2px; }
1236
+ .file-upload__remove {
1237
+ display: inline-flex; align-items: center; justify-content: center;
1238
+ width: 32px; height: 32px; flex: none; border: 0; border-radius: var(--radius-full);
1239
+ background: transparent; color: var(--text-muted); cursor: pointer;
1240
+ transition: background-color var(--duration-fast) var(--ease-out),
1241
+ color var(--duration-fast) var(--ease-out);
1242
+ }
1243
+ .file-upload__remove:hover { background: var(--surface-alt); color: var(--text-primary); }
1244
+ .file-upload__remove svg { width: 16px; height: 16px; }
1245
+ .file-upload__item--error { border-color: var(--danger-500); }
1246
+ .file-upload__item--error .file-upload__icon,
1247
+ .file-upload__item--error .file-upload__size { color: var(--danger-text); }
1248
+
1249
+ /* Stepper / Wizard (§10.34) */
1250
+ .stepper { display: flex; list-style: none; margin: 0; padding: 0; }
1251
+ .step {
1252
+ position: relative; flex: 1;
1253
+ display: flex; flex-direction: column; align-items: center; text-align: center; gap: var(--space-2);
1254
+ }
1255
+ .step__marker {
1256
+ position: relative; z-index: 1;
1257
+ display: inline-flex; align-items: center; justify-content: center;
1258
+ width: 32px; height: 32px; flex: none; border-radius: var(--radius-full);
1259
+ border: 2px solid var(--border-strong); background: var(--surface);
1260
+ font-size: var(--text-sm); font-weight: var(--weight-semibold); color: var(--text-muted);
1261
+ }
1262
+ .step__marker svg { width: 16px; height: 16px; }
1263
+ .step__label { font-size: var(--text-sm); color: var(--text-secondary); }
1264
+ .step__connector {
1265
+ position: absolute; top: 16px; z-index: 0; left: 50%; right: -50%;
1266
+ height: 2px; margin: 0 20px; background: var(--border);
1267
+ }
1268
+ .step--complete .step__marker { background: var(--accent); border-color: var(--accent); color: var(--text-on-primary); }
1269
+ .step--complete .step__label { color: var(--text-primary); }
1270
+ .step--complete .step__connector { background: var(--accent); }
1271
+ .step--current .step__marker { border-color: var(--accent); color: var(--accent); }
1272
+ .step--current .step__label { color: var(--text-primary); font-weight: var(--weight-semibold); }
1273
+ .stepper--vertical { flex-direction: column; align-items: stretch; }
1274
+ .stepper--vertical .step {
1275
+ flex-direction: row; align-items: flex-start; text-align: left;
1276
+ gap: var(--space-3); padding-bottom: var(--space-6);
1277
+ }
1278
+ .stepper--vertical .step:last-child { padding-bottom: 0; }
1279
+ .stepper--vertical .step__connector {
1280
+ top: 36px; bottom: -4px; left: 15px; right: auto;
1281
+ width: 2px; height: auto; margin: 0;
1282
+ }
1283
+ .stepper--vertical .step__label { padding-top: 5px; }
1284
+
1285
+ /* Kbd (§10.35) */
1286
+ .kbd {
1287
+ display: inline-flex; align-items: center; justify-content: center;
1288
+ min-width: 20px; height: 20px; padding: 0 6px;
1289
+ font-family: var(--font-mono); font-size: var(--text-xs); line-height: 1;
1290
+ color: var(--text-secondary); background: var(--surface);
1291
+ border: var(--border-width) solid var(--border-strong);
1292
+ border-radius: var(--radius-sm); box-shadow: var(--shadow-xs);
1293
+ }
1294
+ .kbd-combo { display: inline-flex; align-items: center; gap: 4px; color: var(--text-muted); font-size: var(--text-xs); }
1295
+
867
1296
  .u-visually-hidden {
868
1297
  position: absolute; width: 1px; height: 1px; margin: -1px;
869
1298
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
@@ -947,6 +1376,14 @@ html, body { overflow-x: clip; }
947
1376
  .ds-demo--grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); align-items: stretch; }
948
1377
  .ds-demo:has(table) { overflow-x: auto; }
949
1378
 
1379
+ /* Demo del sidebar contraíble: rejilla auto 1fr, el contenido sigue al riel sin animar el ancho */
1380
+ .ds-app-demo { display: grid; grid-template-columns: auto 1fr; align-items: stretch; gap: 0; min-height: 400px; padding: 0; }
1381
+ .ds-app-demo > .sidebar { border-radius: var(--radius-lg) 0 0 var(--radius-lg); }
1382
+ .ds-app-demo__main { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-3); min-width: 0; padding: var(--space-6); }
1383
+ @media (max-width: 767px) {
1384
+ .ds-app-demo { grid-template-columns: 1fr; min-height: 0; }
1385
+ }
1386
+
950
1387
  .ds-code { margin: var(--space-2) 0 0; }
951
1388
  .ds-code summary {
952
1389
  cursor: pointer; user-select: none; font-size: var(--text-sm);
@@ -1115,6 +1552,10 @@ html, body { overflow-x: clip; }
1115
1552
  <symbol id="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></symbol>
1116
1553
  <symbol id="i-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16"/><path d="M4 12h16"/><path d="M4 18h16"/></symbol>
1117
1554
  <symbol id="i-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></symbol>
1555
+ <symbol id="i-upload" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/></symbol>
1556
+ <symbol id="i-file" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/></symbol>
1557
+ <symbol id="i-calendar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></symbol>
1558
+ <symbol id="i-arrow-up" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></symbol>
1118
1559
  </defs>
1119
1560
  </svg>
1120
1561
 
@@ -1160,6 +1601,7 @@ html, body { overflow-x: clip; }
1160
1601
  <a href="#comp-switch">Switch</a>
1161
1602
  <a href="#comp-badge-tag">Badge y Tag</a>
1162
1603
  <a href="#comp-avatar">Avatar</a>
1604
+ <a href="#comp-avatar-group">AvatarGroup</a>
1163
1605
  <a href="#comp-divider">Divider</a>
1164
1606
  <a href="#comp-accordion">Accordion</a>
1165
1607
  <a href="#comp-tooltip">Tooltip</a>
@@ -1174,6 +1616,14 @@ html, body { overflow-x: clip; }
1174
1616
  <a href="#comp-navegacion">Navegación</a>
1175
1617
  <a href="#comp-skeleton">Skeleton y EmptyState</a>
1176
1618
  <a href="#comp-card">Card, Link y Code</a>
1619
+ <a href="#comp-combobox">Combobox y búsqueda</a>
1620
+ <a href="#comp-drawer">Drawer / Sheet</a>
1621
+ <a href="#comp-popover">Popover</a>
1622
+ <a href="#comp-button-group">ButtonGroup</a>
1623
+ <a href="#comp-datepicker">DatePicker</a>
1624
+ <a href="#comp-file-upload">FileUpload</a>
1625
+ <a href="#comp-stepper">Stepper / Wizard</a>
1626
+ <a href="#comp-kbd">Kbd</a>
1177
1627
  </div>
1178
1628
  <div class="ds-nav__group">Recursos</div>
1179
1629
  <a href="#motion">Motion</a>
@@ -1415,7 +1865,7 @@ html, body { overflow-x: clip; }
1415
1865
  <section id="componentes">
1416
1866
  <p class="ds-eyebrow">Biblioteca</p>
1417
1867
  <h2>Componentes</h2>
1418
- <p class="ds-lead">Los 26 componentes del sistema, renderizados en vivo con las clases del framework <span class="u-mono">celeste-clasica.css</span>. Cada bloque incluye su código de uso.</p>
1868
+ <p class="ds-lead">Los 35 componentes del sistema, renderizados en vivo con las clases del framework <span class="u-mono">celeste-clasica.css</span>. Cada bloque incluye su código de uso.</p>
1419
1869
 
1420
1870
  <!-- Botones -->
1421
1871
  <div class="ds-component" id="comp-botones">
@@ -1471,7 +1921,7 @@ html, body { overflow-x: clip; }
1471
1921
  &lt;svg&gt;…icono 18px…&lt;/svg&gt;
1472
1922
  &lt;/button&gt;
1473
1923
  &lt;!-- Variantes: --ghost (default) · --secondary · --primary
1474
- Tamaños: --sm 32px · (md 40px) · --lg 48px --&gt;</code></pre>
1924
+ Tamaños: --sm 36px · (md 44px) · --lg 48px --&gt;</code></pre>
1475
1925
  </div>
1476
1926
  </details>
1477
1927
  </div>
@@ -1499,8 +1949,12 @@ html, body { overflow-x: clip; }
1499
1949
  <!-- Input -->
1500
1950
  <div class="ds-component" id="comp-input">
1501
1951
  <h3>Input</h3>
1502
- <p class="ds-spec">Altura 44 px · label siempre visible (nunca placeholder como label) · focus con borde <span class="u-mono">--accent</span> + ring.</p>
1952
+ <p class="ds-spec">Altura 44 px · label siempre visible (nunca placeholder como label) · focus con borde <span class="u-mono">--accent</span> + ring · obligatorio con <span class="u-mono">.field--required</span> (asterisco <span class="u-mono">--danger</span> + <span class="u-mono">required/aria-required</span>).</p>
1503
1953
  <div class="ds-demo ds-demo--col" style="max-width:420px">
1954
+ <div class="field field--required">
1955
+ <label class="field__label" for="demo-input-req">Nombre completo</label>
1956
+ <input class="input" id="demo-input-req" type="text" required aria-required="true" autocomplete="name" placeholder="Tu nombre">
1957
+ </div>
1504
1958
  <div class="field">
1505
1959
  <label class="field__label" for="demo-input-1">Correo electrónico</label>
1506
1960
  <input class="input" id="demo-input-1" type="email" placeholder="tu@correo.com">
@@ -1535,6 +1989,12 @@ html, body { overflow-x: clip; }
1535
1989
  &lt;span class="field__help"&gt;Nunca compartiremos tu correo.&lt;/span&gt;
1536
1990
  &lt;/div&gt;
1537
1991
 
1992
+ &lt;!-- Obligatorio: .field--required en el .field + required/aria-required en el input --&gt;
1993
+ &lt;div class="field field--required"&gt;
1994
+ &lt;label class="field__label" for="nombre"&gt;Nombre completo&lt;/label&gt;
1995
+ &lt;input class="input" id="nombre" required aria-required="true"&gt;
1996
+ &lt;/div&gt;
1997
+
1538
1998
  &lt;!-- Estados: input--error (+ .field__error) · input--success · disabled
1539
1999
  Icono leading: envolver en .input-icon --&gt;</code></pre>
1540
2000
  </div>
@@ -1648,10 +2108,11 @@ html, body { overflow-x: clip; }
1648
2108
  <!-- Checkbox y Radio -->
1649
2109
  <div class="ds-component" id="comp-checkbox-radio">
1650
2110
  <h3>Checkbox y Radio</h3>
1651
- <p class="ds-spec">20 px · borde 2 px · check que se dibuja con <span class="u-mono">ease-spring</span> · label clicable con gap 10 px.</p>
2111
+ <p class="ds-spec">20 px · borde 2 px · check que se dibuja con <span class="u-mono">ease-spring</span> · label clicable con gap 10 px · estado <span class="u-mono">indeterminate</span> (guion) para «seleccionar todo» parcial.</p>
1652
2112
  <div class="ds-demo">
1653
2113
  <label class="checkbox"><input type="checkbox" checked><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>Recibir novedades</label>
1654
2114
  <label class="checkbox"><input type="checkbox"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>Recordarme</label>
2115
+ <label class="checkbox"><input type="checkbox" id="demoIndeterminate"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>Seleccionar todo (parcial)</label>
1655
2116
  <label class="checkbox"><input type="checkbox" disabled><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span>Deshabilitado</label>
1656
2117
  </div>
1657
2118
  <div class="ds-demo" role="radiogroup" aria-label="Plan">
@@ -1673,7 +2134,10 @@ html, body { overflow-x: clip; }
1673
2134
  &lt;input type="radio" name="plan" checked&gt;
1674
2135
  &lt;span class="radio__dot"&gt;&lt;/span&gt;
1675
2136
  Plan mensual
1676
- &lt;/label&gt;</code></pre>
2137
+ &lt;/label&gt;
2138
+
2139
+ &lt;!-- Indeterminate: propiedad del DOM, no atributo HTML --&gt;
2140
+ &lt;!-- checkbox.indeterminate = true (guion horizontal) --&gt;</code></pre>
1677
2141
  </div>
1678
2142
  </details>
1679
2143
  </div>
@@ -1756,6 +2220,33 @@ html, body { overflow-x: clip; }
1756
2220
  </details>
1757
2221
  </div>
1758
2222
 
2223
+ <!-- AvatarGroup -->
2224
+ <div class="ds-component" id="comp-avatar-group">
2225
+ <h3>AvatarGroup</h3>
2226
+ <p class="ds-spec">Stack de avatares solapados (<span class="u-mono">-8px</span>) con anillo 2 px de <span class="u-mono">--surface</span> que los separa · <span class="u-mono">avatar-group__more</span> para el +N de overflow.</p>
2227
+ <div class="ds-demo">
2228
+ <div class="avatar-group">
2229
+ <span class="avatar"><span class="avatar__initials">HS</span></span>
2230
+ <span class="avatar"><span class="avatar__initials">MG</span></span>
2231
+ <span class="avatar"><span class="avatar__initials">AL</span></span>
2232
+ <span class="avatar"><span class="avatar__initials">RD</span></span>
2233
+ <span class="avatar-group__more">+3</span>
2234
+ </div>
2235
+ </div>
2236
+ <details class="ds-code"><summary>Ver código</summary>
2237
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2238
+ <pre class="ds-pre"><code>&lt;div class="avatar-group"&gt;
2239
+ &lt;span class="avatar"&gt;&lt;span class="avatar__initials"&gt;HS&lt;/span&gt;&lt;/span&gt;
2240
+ &lt;span class="avatar"&gt;&lt;span class="avatar__initials"&gt;MG&lt;/span&gt;&lt;/span&gt;
2241
+ &lt;span class="avatar"&gt;&lt;span class="avatar__initials"&gt;AL&lt;/span&gt;&lt;/span&gt;
2242
+ &lt;span class="avatar"&gt;&lt;span class="avatar__initials"&gt;RD&lt;/span&gt;&lt;/span&gt;
2243
+ &lt;span class="avatar-group__more"&gt;+3&lt;/span&gt;
2244
+ &lt;/div&gt;
2245
+ &lt;!-- Solape -8px · cada avatar con anillo 2px de --surface --&gt;</code></pre>
2246
+ </div>
2247
+ </details>
2248
+ </div>
2249
+
1759
2250
  <!-- Divider -->
1760
2251
  <div class="ds-component" id="comp-divider">
1761
2252
  <h3>Divider</h3>
@@ -1903,11 +2394,11 @@ html, body { overflow-x: clip; }
1903
2394
  <div class="ds-demo ds-demo--col" style="max-width:420px">
1904
2395
  <div>
1905
2396
  <div class="progress-row"><span class="progress-label">Almacenamiento</span><span class="progress-value js-progress-value">68%</span></div>
1906
- <div class="progress"><div class="progress__bar js-progress-bar" style="width:68%" role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"></div></div>
2397
+ <div class="progress"><div class="progress__bar js-progress-bar" style="width:68%" role="progressbar" aria-label="Almacenamiento" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"></div></div>
1907
2398
  </div>
1908
2399
  <div>
1909
2400
  <div class="progress-row"><span class="progress-label">Tareas completadas</span><span class="progress-value">24/30</span></div>
1910
- <div class="progress"><div class="progress__bar progress__bar--success" style="width:80%" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div></div>
2401
+ <div class="progress"><div class="progress__bar progress__bar--success" style="width:80%" role="progressbar" aria-label="Tareas completadas" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div></div>
1911
2402
  </div>
1912
2403
  <div style="display:flex;align-items:center;gap:24px">
1913
2404
  <div class="progress-ring js-ring" data-value="68">
@@ -1936,28 +2427,32 @@ html, body { overflow-x: clip; }
1936
2427
  <!-- Modal -->
1937
2428
  <div class="ds-component" id="comp-modal">
1938
2429
  <h3>Modal</h3>
1939
- <p class="ds-spec">Overlay <span class="u-mono">--overlay</span> · panel 480 px (confirmación) o 640 px (formulario) · fade + scale(0.96→1) · Esc y click en overlay cierran.</p>
2430
+ <p class="ds-spec"><span class="u-mono">role="dialog"</span> · <span class="u-mono">aria-modal</span> · <span class="u-mono">aria-labelledby</span> + <span class="u-mono">aria-describedby</span> · al abrir el foco entra al panel, queda atrapado (Tab/Shift+Tab ciclan) y vuelve al disparador al cerrar · Esc y click en overlay cierran, salvo flujos destructivos (el de confirmación no cierra por overlay).</p>
1940
2431
  <div class="ds-demo">
1941
2432
  <button class="btn btn--secondary" data-modal-open="demoModal">Abrir confirmación</button>
1942
2433
  <button class="btn btn--secondary" data-modal-open="demoModalLg">Abrir formulario</button>
1943
2434
  </div>
1944
2435
  <details class="ds-code"><summary>Ver código</summary>
1945
2436
  <div class="ds-code-box"><button class="ds-copy">Copiar</button>
1946
- <pre class="ds-pre"><code>&lt;div class="modal" id="miModal" role="dialog" aria-modal="true" aria-labelledby="miModalTitulo"&gt;
2437
+ <pre class="ds-pre"><code>&lt;div class="modal" id="miModal" role="dialog" aria-modal="true"
2438
+ aria-labelledby="miModalTitulo" aria-describedby="miModalDesc"&gt;
2439
+ &lt;!-- overlay sin data-modal-close en flujos destructivos --&gt;
1947
2440
  &lt;div class="modal__overlay" data-modal-close&gt;&lt;/div&gt;
1948
- &lt;div class="modal__panel"&gt;
2441
+ &lt;div class="modal__panel" tabindex="-1"&gt;
1949
2442
  &lt;div class="modal__header"&gt;
1950
2443
  &lt;h4 class="modal__title" id="miModalTitulo"&gt;¿Eliminar proyecto?&lt;/h4&gt;
1951
2444
  &lt;button class="icon-btn icon-btn--ghost" data-modal-close aria-label="Cerrar"&gt;…x…&lt;/button&gt;
1952
2445
  &lt;/div&gt;
1953
- &lt;p&gt;Esta acción no se puede deshacer.&lt;/p&gt;
2446
+ &lt;p id="miModalDesc"&gt;Esta acción no se puede deshacer.&lt;/p&gt;
1954
2447
  &lt;div class="modal__footer"&gt;
1955
2448
  &lt;button class="btn btn--secondary" data-modal-close&gt;Cancelar&lt;/button&gt;
1956
2449
  &lt;button class="btn btn--danger"&gt;Eliminar&lt;/button&gt;
1957
2450
  &lt;/div&gt;
1958
2451
  &lt;/div&gt;
1959
2452
  &lt;/div&gt;
1960
- &lt;!-- Panel ancho: modal__panel--lg (640px) --&gt;</code></pre>
2453
+ &lt;!-- Panel ancho: modal__panel--lg (640px) --&gt;
2454
+ &lt;!-- JS: al abrir guarda el disparador y enfoca el panel; Tab/Shift+Tab
2455
+ atrapados dentro; al cerrar (Esc, ×, Cancelar) el foco vuelve al disparador --&gt;</code></pre>
1961
2456
  </div>
1962
2457
  </details>
1963
2458
  </div>
@@ -1996,27 +2491,77 @@ html, body { overflow-x: clip; }
1996
2491
  <!-- Tabla -->
1997
2492
  <div class="ds-component" id="comp-tabla">
1998
2493
  <h3>Tabla</h3>
1999
- <p class="ds-spec">Header uppercase <span class="u-mono">+0.04em</span> · filas 52 px · numéricos a la derecha con <span class="u-mono">tabular-nums</span> · seleccionada en <span class="u-mono">--accent-soft</span>.</p>
2000
- <div class="ds-demo ds-demo--col" style="padding:0;overflow-x:auto">
2001
- <table class="table">
2002
- <thead><tr><th>Cliente</th><th>Estado</th><th class="num">Facturas</th><th class="num">Monto</th></tr></thead>
2003
- <tbody>
2004
- <tr><td>Constructora Delta</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>Activo</span></td><td class="num">12</td><td class="num">$ 48,200.00</td></tr>
2005
- <tr class="is-selected"><td>Grupo Andrade</td><td><span class="badge badge--warning"><svg><use href="#i-triangle-alert"/></svg>Pendiente</span></td><td class="num">3</td><td class="num">$ 9,850.00</td></tr>
2006
- <tr><td>Studio Nébula</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>Activo</span></td><td class="num">27</td><td class="num">$ 132,400.00</td></tr>
2007
- <tr><td>Farmacias Luna</td><td><span class="badge badge--danger"><svg><use href="#i-circle-alert"/></svg>Vencido</span></td><td class="num">1</td><td class="num">$ 1,120.00</td></tr>
2008
- </tbody>
2009
- </table>
2494
+ <p class="ds-spec">Header uppercase <span class="u-mono">+0.04em</span> · filas 52 px · numéricos a la derecha con <span class="u-mono">tabular-nums</span>. Variantes: <span class="u-mono">.table--zebra</span> · <span class="u-mono">.table--sticky</span> (header fijo) · columna ordenable con <span class="u-mono">aria-sort</span> · selección con <span class="u-mono">.row--selected</span> y «seleccionar todo» indeterminate.</p>
2495
+ <div class="ds-demo ds-demo--col" style="padding:0">
2496
+ <div style="max-height:264px;overflow:auto;width:100%">
2497
+ <table class="table table--zebra table--sticky" id="demoTable">
2498
+ <thead>
2499
+ <tr>
2500
+ <th class="col-select" scope="col">
2501
+ <label class="checkbox"><input type="checkbox" id="demoSelectAll" aria-label="Seleccionar todas las filas"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span></label>
2502
+ </th>
2503
+ <th scope="col">Cliente</th>
2504
+ <th scope="col">Estado</th>
2505
+ <th class="num" scope="col" aria-sort="none">
2506
+ <button type="button" class="th-sort th-sort--num" data-sort="monto">Monto<svg class="sort-icon" aria-hidden="true"><use href="#i-arrow-up"/></svg></button>
2507
+ </th>
2508
+ </tr>
2509
+ </thead>
2510
+ <tbody>
2511
+ <tr>
2512
+ <td class="col-select"><label class="checkbox"><input type="checkbox" class="js-row-select" aria-label="Seleccionar Constructora Delta"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span></label></td>
2513
+ <td>Constructora Delta</td>
2514
+ <td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>Activo</span></td>
2515
+ <td class="num" data-monto="48200">$ 48,200.00</td>
2516
+ </tr>
2517
+ <tr class="row--selected">
2518
+ <td class="col-select"><label class="checkbox"><input type="checkbox" class="js-row-select" checked aria-label="Seleccionar Grupo Andrade"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span></label></td>
2519
+ <td>Grupo Andrade</td>
2520
+ <td><span class="badge badge--warning"><svg><use href="#i-triangle-alert"/></svg>Pendiente</span></td>
2521
+ <td class="num" data-monto="9850">$ 9,850.00</td>
2522
+ </tr>
2523
+ <tr>
2524
+ <td class="col-select"><label class="checkbox"><input type="checkbox" class="js-row-select" aria-label="Seleccionar Studio Nébula"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span></label></td>
2525
+ <td>Studio Nébula</td>
2526
+ <td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>Activo</span></td>
2527
+ <td class="num" data-monto="132400">$ 132,400.00</td>
2528
+ </tr>
2529
+ <tr>
2530
+ <td class="col-select"><label class="checkbox"><input type="checkbox" class="js-row-select" aria-label="Seleccionar Farmacias Luna"><span class="checkbox__box"><svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg></span></label></td>
2531
+ <td>Farmacias Luna</td>
2532
+ <td><span class="badge badge--danger"><svg><use href="#i-circle-alert"/></svg>Vencido</span></td>
2533
+ <td class="num" data-monto="1120">$ 1,120.00</td>
2534
+ </tr>
2535
+ </tbody>
2536
+ </table>
2537
+ </div>
2010
2538
  </div>
2011
2539
  <details class="ds-code"><summary>Ver código</summary>
2012
2540
  <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2013
- <pre class="ds-pre"><code>&lt;table class="table"&gt;
2014
- &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Cliente&lt;/th&gt;&lt;th class="num"&gt;Monto&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
2541
+ <pre class="ds-pre"><code>&lt;table class="table table--zebra table--sticky"&gt;
2542
+ &lt;thead&gt;
2543
+ &lt;tr&gt;
2544
+ &lt;th class="col-select" scope="col"&gt;
2545
+ &lt;label class="checkbox"&gt;&lt;input type="checkbox" aria-label="Seleccionar todo"&gt;
2546
+ &lt;span class="checkbox__box"&gt;&lt;svg&gt;…check…&lt;/svg&gt;&lt;/span&gt;&lt;/label&gt;
2547
+ &lt;/th&gt;
2548
+ &lt;th scope="col"&gt;Cliente&lt;/th&gt;
2549
+ &lt;th class="num" scope="col" aria-sort="none"&gt;
2550
+ &lt;button class="th-sort th-sort--num" data-sort="monto"&gt;
2551
+ Monto&lt;svg class="sort-icon" aria-hidden="true"&gt;…&lt;/svg&gt;&lt;/button&gt;
2552
+ &lt;/th&gt;
2553
+ &lt;/tr&gt;
2554
+ &lt;/thead&gt;
2015
2555
  &lt;tbody&gt;
2016
- &lt;tr&gt;&lt;td&gt;Constructora Delta&lt;/td&gt;&lt;td class="num"&gt;$ 48,200.00&lt;/td&gt;&lt;/tr&gt;
2017
- &lt;tr class="is-selected"&gt;&lt;td&gt;Grupo Andrade&lt;/td&gt;&lt;td class="num"&gt;$ 9,850.00&lt;/td&gt;&lt;/tr&gt;
2556
+ &lt;tr class="row--selected"&gt;
2557
+ &lt;td class="col-select"&gt;&lt;label class="checkbox"&gt;&lt;input type="checkbox" checked
2558
+ aria-label="Seleccionar Grupo Andrade"&gt;…&lt;/label&gt;&lt;/td&gt;
2559
+ &lt;td&gt;Grupo Andrade&lt;/td&gt;&lt;td class="num" data-monto="9850"&gt;$ 9,850.00&lt;/td&gt;
2560
+ &lt;/tr&gt;
2018
2561
  &lt;/tbody&gt;
2019
- &lt;/table&gt;</code></pre>
2562
+ &lt;/table&gt;
2563
+ &lt;!-- JS: click en .th-sort alterna aria-sort asc/desc y reordena tbody;
2564
+ el checkbox del header pasa a indeterminate con selección parcial --&gt;</code></pre>
2020
2565
  </div>
2021
2566
  </details>
2022
2567
  </div>
@@ -2068,6 +2613,30 @@ html, body { overflow-x: clip; }
2068
2613
  &lt;!-- Delta: --up (success-text) · --down (danger-text) · neutro (text-muted) --&gt;</code></pre>
2069
2614
  </div>
2070
2615
  </details>
2616
+
2617
+ <h4 style="margin-top:var(--space-10)">Chip de métrica</h4>
2618
+ <p class="ds-spec">Pastilla de una línea con rótulo y valor en <span class="u-mono">--link</span> · fila con <span class="u-mono">gap</span> que salta de línea en móvil · el chip interactivo es un <span class="u-mono">&lt;button&gt;</span> de 44 px con nombre accesible completo.</p>
2619
+ <div class="ds-demo">
2620
+ <div class="metric-chips">
2621
+ <span class="metric-chip"><span class="metric-chip__label">Pasos</span> <span class="metric-chip__value">6,240</span></span>
2622
+ <span class="metric-chip"><span class="metric-chip__label">Sueño</span> <span class="metric-chip__value">7 h 20 min</span></span>
2623
+ <button class="metric-chip" type="button" id="dsWaterChip" aria-label="Agua: 3 de 8 vasos. Sumar un vaso"><svg aria-hidden="true"><use href="#i-plus"/></svg><span class="metric-chip__label">Agua</span> <span class="metric-chip__value">3 de 8</span></button>
2624
+ </div>
2625
+ </div>
2626
+ <details class="ds-code"><summary>Ver código</summary>
2627
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2628
+ <pre class="ds-pre"><code>&lt;div class="metric-chips"&gt;
2629
+ &lt;span class="metric-chip"&gt;
2630
+ &lt;span class="metric-chip__label"&gt;Pasos&lt;/span&gt; &lt;span class="metric-chip__value"&gt;6,240&lt;/span&gt;
2631
+ &lt;/span&gt;
2632
+ &lt;button class="metric-chip" type="button" aria-label="Agua: 3 de 8 vasos. Sumar un vaso"&gt;
2633
+ &lt;svg aria-hidden="true"&gt;…plus…&lt;/svg&gt;
2634
+ &lt;span class="metric-chip__label"&gt;Agua&lt;/span&gt; &lt;span class="metric-chip__value"&gt;3 de 8&lt;/span&gt;
2635
+ &lt;/button&gt;
2636
+ &lt;/div&gt;
2637
+ &lt;!-- El aria-label se actualiza con el valor; empieza por el texto visible --&gt;</code></pre>
2638
+ </div>
2639
+ </details>
2071
2640
  </div>
2072
2641
 
2073
2642
  <!-- Pagination -->
@@ -2155,6 +2724,51 @@ html, body { overflow-x: clip; }
2155
2724
  &lt;/nav&gt;</code></pre>
2156
2725
  </div>
2157
2726
  </details>
2727
+
2728
+ <h4 style="margin-top:var(--space-10)">Sidebar contraíble</h4>
2729
+ <p class="ds-spec">Expandido 260 px · riel de iconos de 64 px con grupos separados por una línea · en el riel, el nombre aparece como tooltip al pasar el cursor o al enfocar con teclado · el ancho cambia sin transición y solo los rótulos se funden · por debajo de 768 px lo sustituye un Drawer <span class="u-mono">--left</span>.</p>
2730
+ <div class="ds-demo ds-app-demo">
2731
+ <nav class="sidebar sidebar--collapsible" id="demoAppNav" aria-label="Navegación de ejemplo">
2732
+ <div class="sidebar__group" role="group" aria-labelledby="demoAppNavG1">
2733
+ <span class="sidebar__group-label" id="demoAppNavG1">Trabajo</span>
2734
+ <a class="sidebar__item is-active" href="#comp-navegacion" aria-current="page"><svg aria-hidden="true"><use href="#i-home"/></svg><span class="sidebar__label">Inicio</span></a>
2735
+ <a class="sidebar__item" href="#comp-navegacion"><svg aria-hidden="true"><use href="#i-folder"/></svg><span class="sidebar__label">Proyectos</span></a>
2736
+ <a class="sidebar__item" href="#comp-navegacion"><svg aria-hidden="true"><use href="#i-calendar"/></svg><span class="sidebar__label">Calendario</span></a>
2737
+ </div>
2738
+ <div class="sidebar__group" role="group" aria-labelledby="demoAppNavG2">
2739
+ <span class="sidebar__group-label" id="demoAppNavG2">Equipo</span>
2740
+ <a class="sidebar__item" href="#comp-navegacion"><svg aria-hidden="true"><use href="#i-chart"/></svg><span class="sidebar__label">Informes</span></a>
2741
+ <a class="sidebar__item" href="#comp-navegacion"><svg aria-hidden="true"><use href="#i-user"/></svg><span class="sidebar__label">Personas</span></a>
2742
+ <a class="sidebar__item" href="#comp-navegacion"><svg aria-hidden="true"><use href="#i-inbox"/></svg><span class="sidebar__label">Bandeja</span></a>
2743
+ </div>
2744
+ <button class="sidebar__item sidebar__toggle" type="button" aria-expanded="true" aria-controls="demoAppNav"><svg aria-hidden="true"><use href="#i-chevron-left"/></svg><span class="sidebar__label">Contraer</span></button>
2745
+ </nav>
2746
+ <div class="ds-app-demo__main">
2747
+ <button class="icon-btn icon-btn--secondary sidebar-trigger" type="button" data-drawer-open="demoNavDrawer" aria-label="Abrir navegación" aria-expanded="false" aria-controls="demoNavDrawer"><svg aria-hidden="true"><use href="#i-menu"/></svg></button>
2748
+ <h5 style="margin:0">Inicio</h5>
2749
+ <p class="ds-spec" style="margin:0">El contenido ocupa el ancho que deja el sidebar: la rejilla <span class="u-mono">auto 1fr</span> sigue al riel al instante, sin animar el ancho.</p>
2750
+ </div>
2751
+ </div>
2752
+ <details class="ds-code"><summary>Ver código</summary>
2753
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2754
+ <pre class="ds-pre"><code>&lt;nav class="sidebar sidebar--collapsible" id="appNav" aria-label="Principal"&gt;
2755
+ &lt;div class="sidebar__group" role="group" aria-labelledby="navTrabajo"&gt;
2756
+ &lt;span class="sidebar__group-label" id="navTrabajo"&gt;Trabajo&lt;/span&gt;
2757
+ &lt;a class="sidebar__item is-active" href="/" aria-current="page"&gt;
2758
+ &lt;svg aria-hidden="true"&gt;…&lt;/svg&gt;&lt;span class="sidebar__label"&gt;Inicio&lt;/span&gt;
2759
+ &lt;/a&gt;
2760
+ &lt;/div&gt;
2761
+ &lt;button class="sidebar__item sidebar__toggle" type="button"
2762
+ aria-expanded="true" aria-controls="appNav"&gt;
2763
+ &lt;svg aria-hidden="true"&gt;…chevron-left…&lt;/svg&gt;&lt;span class="sidebar__label"&gt;Contraer&lt;/span&gt;
2764
+ &lt;/button&gt;
2765
+ &lt;/nav&gt;
2766
+ &lt;!-- Contraído: .is-collapsed en el nav, aria-expanded="false" y texto «Expandir».
2767
+ Por debajo de 768 px: botón ☰ .sidebar-trigger que abre un .drawer.drawer--left con los mismos ítems. --&gt;
2768
+ &lt;button class="icon-btn sidebar-trigger" type="button" aria-label="Abrir navegación"
2769
+ aria-expanded="false" aria-controls="navDrawer"&gt;…menu…&lt;/button&gt;</code></pre>
2770
+ </div>
2771
+ </details>
2158
2772
  </div>
2159
2773
 
2160
2774
  <!-- Skeleton y EmptyState -->
@@ -2222,6 +2836,372 @@ html, body { overflow-x: clip; }
2222
2836
  </div>
2223
2837
  </details>
2224
2838
  </div>
2839
+
2840
+ <!-- Combobox / Autocomplete y Búsqueda -->
2841
+ <div class="ds-component" id="comp-combobox">
2842
+ <h3>Combobox y búsqueda</h3>
2843
+ <p class="ds-spec"><span class="u-mono">role="combobox"</span> · <span class="u-mono">aria-expanded</span> + <span class="u-mono">aria-controls</span> + <span class="u-mono">aria-activedescendant</span> · lista <span class="u-mono">role="listbox"</span>, opciones <span class="u-mono">role="option"</span> · ↑/↓ mueven, Enter elige, Esc cierra, Home/End a extremos. La búsqueda se limpia con la ×.</p>
2844
+ <div class="ds-demo ds-demo--col" style="max-width:420px">
2845
+ <div class="field">
2846
+ <label class="field__label" for="cbxInput">Ciudad</label>
2847
+ <div class="combobox" id="cbx">
2848
+ <input class="combobox__input" id="cbxInput" type="text" role="combobox" aria-expanded="false" aria-controls="cbxList" aria-autocomplete="list" autocomplete="off" spellcheck="false" placeholder="Escribe para filtrar…">
2849
+ <ul class="combobox__list" id="cbxList" role="listbox" aria-label="Ciudades"></ul>
2850
+ </div>
2851
+ </div>
2852
+ <div class="field">
2853
+ <label class="field__label" for="searchInput">Buscar</label>
2854
+ <div class="search">
2855
+ <svg class="search__icon" aria-hidden="true"><use href="#i-search"/></svg>
2856
+ <input class="search__input" id="searchInput" type="search" autocomplete="off" spellcheck="false" placeholder="Buscar proyectos…">
2857
+ <button class="search__clear" type="button" aria-label="Limpiar"><svg><use href="#i-x"/></svg></button>
2858
+ </div>
2859
+ </div>
2860
+ </div>
2861
+ <details class="ds-code"><summary>Ver código</summary>
2862
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2863
+ <pre class="ds-pre"><code>&lt;div class="combobox"&gt;
2864
+ &lt;input class="combobox__input" type="text" role="combobox"
2865
+ aria-expanded="false" aria-controls="lista" aria-autocomplete="list"
2866
+ aria-activedescendant="opt-0" autocomplete="off"&gt;
2867
+ &lt;ul class="combobox__list" id="lista" role="listbox" aria-label="Ciudades"&gt;
2868
+ &lt;li class="combobox__option is-active" role="option" id="opt-0"
2869
+ aria-selected="false"&gt;Guadalajara&lt;/li&gt;
2870
+ &lt;li class="combobox__option" role="option" id="opt-1"
2871
+ aria-selected="true"&gt;Monterrey
2872
+ &lt;svg class="combobox__check"&gt;…check…&lt;/svg&gt;&lt;/li&gt;
2873
+ &lt;/ul&gt;
2874
+ &lt;/div&gt;
2875
+
2876
+ &lt;div class="search"&gt;
2877
+ &lt;svg class="search__icon" aria-hidden="true"&gt;…lupa…&lt;/svg&gt;
2878
+ &lt;input class="search__input" type="search" placeholder="Buscar…"&gt;
2879
+ &lt;button class="search__clear" aria-label="Limpiar"&gt;…x…&lt;/button&gt;
2880
+ &lt;/div&gt;</code></pre>
2881
+ </div>
2882
+ </details>
2883
+ </div>
2884
+
2885
+ <!-- Drawer / Sheet -->
2886
+ <div class="ds-component" id="comp-drawer">
2887
+ <h3>Drawer / Sheet</h3>
2888
+ <p class="ds-spec"><span class="u-mono">role="dialog"</span> · <span class="u-mono">aria-modal</span> · foco atrapado y devuelto al disparador (patrón del Modal) · <span class="u-mono">overscroll-behavior: contain</span> · entra deslizando desde el borde y sale más corto · Esc y click en overlay cierran. Variantes <span class="u-mono">--right</span> (default), <span class="u-mono">--left</span>, <span class="u-mono">--bottom</span>.</p>
2889
+ <div class="ds-demo">
2890
+ <button class="btn btn--secondary" data-drawer-open="demoDrawer">Abrir panel de filtros</button>
2891
+ </div>
2892
+ <details class="ds-code"><summary>Ver código</summary>
2893
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2894
+ <pre class="ds-pre"><code>&lt;div class="drawer drawer--right" id="miDrawer" role="dialog"
2895
+ aria-modal="true" aria-labelledby="miDrawerTitulo"&gt;
2896
+ &lt;div class="drawer__overlay" data-drawer-close&gt;&lt;/div&gt;
2897
+ &lt;div class="drawer__panel" tabindex="-1"&gt;
2898
+ &lt;div class="drawer__header"&gt;
2899
+ &lt;h4 class="drawer__title" id="miDrawerTitulo"&gt;Filtrar proyectos&lt;/h4&gt;
2900
+ &lt;button class="icon-btn icon-btn--ghost" data-drawer-close aria-label="Cerrar"&gt;…x…&lt;/button&gt;
2901
+ &lt;/div&gt;
2902
+ &lt;!-- campos… --&gt;
2903
+ &lt;div class="drawer__footer"&gt;
2904
+ &lt;button class="btn btn--secondary" data-drawer-close&gt;Cancelar&lt;/button&gt;
2905
+ &lt;button class="btn btn--primary" data-drawer-close&gt;Aplicar&lt;/button&gt;
2906
+ &lt;/div&gt;
2907
+ &lt;/div&gt;
2908
+ &lt;/div&gt;</code></pre>
2909
+ </div>
2910
+ </details>
2911
+ </div>
2912
+
2913
+ <!-- Popover -->
2914
+ <div class="ds-component" id="comp-popover">
2915
+ <h3>Popover</h3>
2916
+ <p class="ds-spec">Se abre por <strong>click</strong> (no hover, a diferencia del tooltip) y lleva contenido interactivo · trigger <span class="u-mono">aria-haspopup="dialog"</span> + <span class="u-mono">aria-expanded</span> · panel <span class="u-mono">role="dialog"</span> · aparece desde el trigger (fade + scale 0.96→1) · Esc y click fuera cierran.</p>
2917
+ <div class="ds-demo">
2918
+ <div class="popover" id="demoPopover">
2919
+ <button class="btn btn--secondary popover__trigger" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="demoPopoverPanel"><svg><use href="#i-info"/></svg>Detalles del plan</button>
2920
+ <div class="popover__panel" id="demoPopoverPanel" role="dialog" aria-label="Detalles del plan" tabindex="-1">
2921
+ <p style="margin:0 0 6px;font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--text-primary)">Plan Estudio</p>
2922
+ <p style="margin:0 0 12px;font-size:var(--text-sm);color:var(--text-secondary)">Incluye 12 tableros, 3 integraciones y respaldo diario. Puedes cambiar de plan cuando quieras.</p>
2923
+ <a class="link" href="#comp-popover">Ver comparación de planes</a>
2924
+ </div>
2925
+ </div>
2926
+ </div>
2927
+ <details class="ds-code"><summary>Ver código</summary>
2928
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2929
+ <pre class="ds-pre"><code>&lt;div class="popover"&gt;
2930
+ &lt;button class="btn btn--secondary popover__trigger"
2931
+ aria-haspopup="dialog" aria-expanded="false"
2932
+ aria-controls="pop"&gt;Detalles del plan&lt;/button&gt;
2933
+ &lt;div class="popover__panel" id="pop" role="dialog"
2934
+ aria-label="Detalles del plan" tabindex="-1"&gt;
2935
+ &lt;p&gt;Contenido con &lt;a class="link" href="#"&gt;un enlace&lt;/a&gt;.&lt;/p&gt;
2936
+ &lt;/div&gt;
2937
+ &lt;/div&gt;</code></pre>
2938
+ </div>
2939
+ </details>
2940
+ </div>
2941
+
2942
+ <!-- ButtonGroup -->
2943
+ <div class="ds-component" id="comp-button-group">
2944
+ <h3>ButtonGroup</h3>
2945
+ <p class="ds-spec">Botones adyacentes con bordes compartidos (radius solo en los extremos, bordes internos colapsados) · <span class="u-mono">role="group"</span> + <span class="u-mono">aria-label</span> · usa los <span class="u-mono">.btn</span> existentes. Para un único-seleccionado, usa SegmentedControl.</p>
2946
+ <div class="ds-demo">
2947
+ <div class="btn-group" role="group" aria-label="Navegación de semana">
2948
+ <button class="btn btn--secondary"><svg><use href="#i-chevron-left"/></svg>Anterior</button>
2949
+ <button class="btn btn--secondary">Hoy</button>
2950
+ <button class="btn btn--secondary">Siguiente<svg><use href="#i-chevron-right"/></svg></button>
2951
+ </div>
2952
+ </div>
2953
+ <details class="ds-code"><summary>Ver código</summary>
2954
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2955
+ <pre class="ds-pre"><code>&lt;div class="btn-group" role="group" aria-label="Navegación de semana"&gt;
2956
+ &lt;button class="btn btn--secondary"&gt;Anterior&lt;/button&gt;
2957
+ &lt;button class="btn btn--secondary"&gt;Hoy&lt;/button&gt;
2958
+ &lt;button class="btn btn--secondary"&gt;Siguiente&lt;/button&gt;
2959
+ &lt;/div&gt;</code></pre>
2960
+ </div>
2961
+ </details>
2962
+ </div>
2963
+
2964
+ <!-- DatePicker -->
2965
+ <div class="ds-component" id="comp-datepicker">
2966
+ <h3>DatePicker</h3>
2967
+ <p class="ds-spec">Input + calendario en popover · rejilla <span class="u-mono">role="grid"</span>, encabezados <span class="u-mono">role="columnheader"</span> con <span class="u-mono">aria-label</span> completo, días <span class="u-mono">role="gridcell"</span> con <span class="u-mono">aria-label</span> de la fecha entera y <span class="u-mono">aria-selected</span> · las flechas mueven entre días · el input abre el calendario (click), Esc cierra y el foco entra al calendario · hoy marcado y un día seleccionado.</p>
2968
+ <div class="ds-demo" style="min-height:360px;align-items:flex-start">
2969
+ <div class="field" style="max-width:280px;width:100%">
2970
+ <label class="field__label" for="dpInput">Fecha de entrega</label>
2971
+ <div class="datepicker" id="demoDatepicker">
2972
+ <input class="datepicker__input" id="dpInput" type="text" readonly autocomplete="off"
2973
+ aria-haspopup="dialog" aria-controls="dpCal" placeholder="Elige una fecha">
2974
+ <svg class="datepicker__icon" aria-hidden="true"><use href="#i-calendar"/></svg>
2975
+ <div class="datepicker__calendar" id="dpCal" role="dialog" aria-label="Elegir fecha" tabindex="-1">
2976
+ <div class="calendar">
2977
+ <div class="calendar__header">
2978
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" data-cal-prev aria-label="Mes anterior"><svg><use href="#i-chevron-left"/></svg></button>
2979
+ <span class="calendar__title" aria-live="polite">—</span>
2980
+ <button class="icon-btn icon-btn--ghost icon-btn--sm" type="button" data-cal-next aria-label="Mes siguiente"><svg><use href="#i-chevron-right"/></svg></button>
2981
+ </div>
2982
+ <div class="calendar__table" role="grid" aria-label="Calendario">
2983
+ <div class="calendar__weekdays" role="row">
2984
+ <span class="calendar__weekday" role="columnheader" aria-label="lunes">lu</span>
2985
+ <span class="calendar__weekday" role="columnheader" aria-label="martes">ma</span>
2986
+ <span class="calendar__weekday" role="columnheader" aria-label="miércoles">mi</span>
2987
+ <span class="calendar__weekday" role="columnheader" aria-label="jueves">ju</span>
2988
+ <span class="calendar__weekday" role="columnheader" aria-label="viernes">vi</span>
2989
+ <span class="calendar__weekday" role="columnheader" aria-label="sábado">sá</span>
2990
+ <span class="calendar__weekday" role="columnheader" aria-label="domingo">do</span>
2991
+ </div>
2992
+ <div class="calendar__grid" id="dpGrid" role="rowgroup"></div>
2993
+ </div>
2994
+ </div>
2995
+ </div>
2996
+ </div>
2997
+ </div>
2998
+ </div>
2999
+ <details class="ds-code"><summary>Ver código</summary>
3000
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
3001
+ <pre class="ds-pre"><code>&lt;div class="datepicker"&gt;
3002
+ &lt;input class="datepicker__input" type="text" readonly
3003
+ aria-haspopup="dialog" aria-controls="cal"&gt;
3004
+ &lt;svg class="datepicker__icon" aria-hidden="true"&gt;…calendario…&lt;/svg&gt;
3005
+ &lt;div class="datepicker__calendar" id="cal" role="dialog"
3006
+ aria-label="Elegir fecha" tabindex="-1"&gt;
3007
+ &lt;div class="calendar"&gt;
3008
+ &lt;div class="calendar__header"&gt;
3009
+ &lt;button aria-label="Mes anterior"&gt;…&lt;/button&gt;
3010
+ &lt;span class="calendar__title"&gt;julio 2026&lt;/span&gt;
3011
+ &lt;button aria-label="Mes siguiente"&gt;…&lt;/button&gt;
3012
+ &lt;/div&gt;
3013
+ &lt;div class="calendar__table" role="grid" aria-label="julio 2026"&gt;
3014
+ &lt;div class="calendar__weekdays" role="row"&gt;
3015
+ &lt;span class="calendar__weekday" role="columnheader"
3016
+ aria-label="lunes"&gt;lu&lt;/span&gt; …
3017
+ &lt;/div&gt;
3018
+ &lt;div class="calendar__grid" role="rowgroup"&gt;
3019
+ &lt;div class="calendar__week" role="row"&gt;
3020
+ &lt;button class="calendar__day calendar__day--today" role="gridcell"
3021
+ aria-label="viernes, 10 de julio de 2026"
3022
+ aria-selected="false"&gt;10&lt;/button&gt;
3023
+ &lt;button class="calendar__day calendar__day--selected" role="gridcell"
3024
+ aria-label="viernes, 17 de julio de 2026"
3025
+ aria-selected="true"&gt;17&lt;/button&gt; …
3026
+ &lt;/div&gt;
3027
+ &lt;/div&gt;
3028
+ &lt;/div&gt;
3029
+ &lt;/div&gt;
3030
+ &lt;/div&gt;
3031
+ &lt;/div&gt;</code></pre>
3032
+ </div>
3033
+ </details>
3034
+ </div>
3035
+
3036
+ <!-- FileUpload -->
3037
+ <div class="ds-component" id="comp-file-upload">
3038
+ <h3>FileUpload</h3>
3039
+ <p class="ds-spec">Dropzone + lista · el input <span class="u-mono">file</span> real va oculto pero con label asociado y <span class="u-mono">aria-describedby</span> de formatos y tamaño · la zona describe la acción · estado <span class="u-mono">.is-dragover</span> al arrastrar · el error no se comunica solo por color (icono + texto) · el item que sube reusa la barra <span class="u-mono">.progress</span>.</p>
3040
+ <div class="ds-demo ds-demo--col" style="max-width:480px">
3041
+ <div class="file-upload" id="demoFileUpload">
3042
+ <input class="file-upload__input" id="fuInput" type="file" multiple aria-describedby="fuHint">
3043
+ <label class="file-upload__label" for="fuInput">
3044
+ <svg aria-hidden="true"><use href="#i-upload"/></svg>
3045
+ <span class="file-upload__title">Arrastra archivos o elige</span>
3046
+ <span class="file-upload__button">Elegir archivos</span>
3047
+ <span class="file-upload__hint" id="fuHint">PDF, PNG o JPG · hasta 10 MB</span>
3048
+ </label>
3049
+ </div>
3050
+ <ul class="file-upload__list" id="fuList" aria-label="Archivos seleccionados">
3051
+ <li class="file-upload__item">
3052
+ <svg class="file-upload__icon" aria-hidden="true"><use href="#i-file"/></svg>
3053
+ <div class="file-upload__meta">
3054
+ <span class="file-upload__name">informe-anual-2026.pdf</span>
3055
+ <span class="file-upload__size">2.4 MB · Subiendo 45%</span>
3056
+ <div class="progress"><div class="progress__bar" style="width:45%" role="progressbar" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" aria-label="Progreso de subida"></div></div>
3057
+ </div>
3058
+ <button class="file-upload__remove" type="button" aria-label="Cancelar informe-anual-2026.pdf"><svg><use href="#i-x"/></svg></button>
3059
+ </li>
3060
+ <li class="file-upload__item">
3061
+ <svg class="file-upload__icon" aria-hidden="true"><use href="#i-file"/></svg>
3062
+ <div class="file-upload__meta">
3063
+ <span class="file-upload__name">portada.png</span>
3064
+ <span class="file-upload__size"><svg aria-hidden="true"><use href="#i-circle-check"/></svg>820 KB · Completo</span>
3065
+ </div>
3066
+ <button class="file-upload__remove" type="button" aria-label="Quitar portada.png"><svg><use href="#i-x"/></svg></button>
3067
+ </li>
3068
+ <li class="file-upload__item file-upload__item--error">
3069
+ <svg class="file-upload__icon" aria-hidden="true"><use href="#i-circle-alert"/></svg>
3070
+ <div class="file-upload__meta">
3071
+ <span class="file-upload__name">video-demo.mov</span>
3072
+ <span class="file-upload__size"><svg aria-hidden="true"><use href="#i-circle-alert"/></svg>Formato no admitido · elige PDF, PNG o JPG</span>
3073
+ </div>
3074
+ <button class="file-upload__remove" type="button" aria-label="Quitar video-demo.mov"><svg><use href="#i-x"/></svg></button>
3075
+ </li>
3076
+ </ul>
3077
+ </div>
3078
+ <details class="ds-code"><summary>Ver código</summary>
3079
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
3080
+ <pre class="ds-pre"><code>&lt;div class="file-upload"&gt;
3081
+ &lt;input class="file-upload__input" type="file" multiple
3082
+ aria-describedby="hint"&gt;
3083
+ &lt;label class="file-upload__label" for="…"&gt;
3084
+ &lt;svg aria-hidden="true"&gt;…subir…&lt;/svg&gt;
3085
+ &lt;span class="file-upload__title"&gt;Arrastra archivos o elige&lt;/span&gt;
3086
+ &lt;span class="file-upload__button"&gt;Elegir archivos&lt;/span&gt;
3087
+ &lt;span class="file-upload__hint" id="hint"&gt;PDF, PNG o JPG · hasta 10 MB&lt;/span&gt;
3088
+ &lt;/label&gt;
3089
+ &lt;/div&gt;
3090
+ &lt;ul class="file-upload__list"&gt;
3091
+ &lt;li class="file-upload__item"&gt;
3092
+ &lt;svg class="file-upload__icon"&gt;…&lt;/svg&gt;
3093
+ &lt;div class="file-upload__meta"&gt;
3094
+ &lt;span class="file-upload__name"&gt;informe.pdf&lt;/span&gt;
3095
+ &lt;span class="file-upload__size"&gt;2.4 MB · Subiendo 45%&lt;/span&gt;
3096
+ &lt;div class="progress"&gt;&lt;div class="progress__bar" style="width:45%"&gt;&lt;/div&gt;&lt;/div&gt;
3097
+ &lt;/div&gt;
3098
+ &lt;button class="file-upload__remove" aria-label="Cancelar informe.pdf"&gt;×&lt;/button&gt;
3099
+ &lt;/li&gt;
3100
+ &lt;li class="file-upload__item file-upload__item--error"&gt;
3101
+ &lt;svg class="file-upload__icon"&gt;…alerta…&lt;/svg&gt;
3102
+ &lt;div class="file-upload__meta"&gt;
3103
+ &lt;span class="file-upload__name"&gt;video.mov&lt;/span&gt;
3104
+ &lt;span class="file-upload__size"&gt;…alerta… Formato no admitido&lt;/span&gt;
3105
+ &lt;/div&gt;
3106
+ &lt;button class="file-upload__remove" aria-label="Quitar video.mov"&gt;×&lt;/button&gt;
3107
+ &lt;/li&gt;
3108
+ &lt;/ul&gt;</code></pre>
3109
+ </div>
3110
+ </details>
3111
+ </div>
3112
+
3113
+ <!-- Stepper / Wizard -->
3114
+ <div class="ds-component" id="comp-stepper">
3115
+ <h3>Stepper / Wizard</h3>
3116
+ <p class="ds-spec"><span class="u-mono">&lt;ol&gt;</span> · <span class="u-mono">aria-current="step"</span> en el paso actual · el completo muestra un check (no solo color), el próximo muestra su número · estados <span class="u-mono">--complete</span>/<span class="u-mono">--current</span>/<span class="u-mono">--upcoming</span> · <span class="u-mono">.step__connector</span> entre pasos · variante <span class="u-mono">--vertical</span>.</p>
3117
+ <div class="ds-demo ds-demo--col" style="gap:40px">
3118
+ <ol class="stepper" aria-label="Progreso de configuración">
3119
+ <li class="step step--complete">
3120
+ <span class="step__connector" aria-hidden="true"></span>
3121
+ <span class="step__marker"><svg aria-hidden="true"><use href="#i-check"/></svg></span>
3122
+ <span class="step__label">Cuenta</span>
3123
+ </li>
3124
+ <li class="step step--complete">
3125
+ <span class="step__connector" aria-hidden="true"></span>
3126
+ <span class="step__marker"><svg aria-hidden="true"><use href="#i-check"/></svg></span>
3127
+ <span class="step__label">Perfil</span>
3128
+ </li>
3129
+ <li class="step step--current" aria-current="step">
3130
+ <span class="step__connector" aria-hidden="true"></span>
3131
+ <span class="step__marker">3</span>
3132
+ <span class="step__label">Facturación</span>
3133
+ </li>
3134
+ <li class="step step--upcoming">
3135
+ <span class="step__marker">4</span>
3136
+ <span class="step__label">Confirmar</span>
3137
+ </li>
3138
+ </ol>
3139
+ <ol class="stepper stepper--vertical" aria-label="Pasos de publicación" style="align-self:flex-start;min-width:220px">
3140
+ <li class="step step--complete">
3141
+ <span class="step__connector" aria-hidden="true"></span>
3142
+ <span class="step__marker"><svg aria-hidden="true"><use href="#i-check"/></svg></span>
3143
+ <span class="step__label">Borrador guardado</span>
3144
+ </li>
3145
+ <li class="step step--current" aria-current="step">
3146
+ <span class="step__connector" aria-hidden="true"></span>
3147
+ <span class="step__marker">2</span>
3148
+ <span class="step__label">Revisión del equipo</span>
3149
+ </li>
3150
+ <li class="step step--upcoming">
3151
+ <span class="step__marker">3</span>
3152
+ <span class="step__label">Publicación</span>
3153
+ </li>
3154
+ </ol>
3155
+ </div>
3156
+ <details class="ds-code"><summary>Ver código</summary>
3157
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
3158
+ <pre class="ds-pre"><code>&lt;ol class="stepper" aria-label="Progreso de configuración"&gt;
3159
+ &lt;li class="step step--complete"&gt;
3160
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
3161
+ &lt;span class="step__marker"&gt;…check…&lt;/span&gt;
3162
+ &lt;span class="step__label"&gt;Cuenta&lt;/span&gt;
3163
+ &lt;/li&gt;
3164
+ &lt;li class="step step--current" aria-current="step"&gt;
3165
+ &lt;span class="step__connector" aria-hidden="true"&gt;&lt;/span&gt;
3166
+ &lt;span class="step__marker"&gt;3&lt;/span&gt;
3167
+ &lt;span class="step__label"&gt;Facturación&lt;/span&gt;
3168
+ &lt;/li&gt;
3169
+ &lt;li class="step step--upcoming"&gt;
3170
+ &lt;span class="step__marker"&gt;4&lt;/span&gt;
3171
+ &lt;span class="step__label"&gt;Confirmar&lt;/span&gt;
3172
+ &lt;/li&gt;
3173
+ &lt;/ol&gt;
3174
+
3175
+ &lt;!-- Vertical: añade la clase --vertical al &lt;ol&gt; --&gt;
3176
+ &lt;ol class="stepper stepper--vertical"&gt;…&lt;/ol&gt;</code></pre>
3177
+ </div>
3178
+ </details>
3179
+ </div>
3180
+
3181
+ <!-- Kbd -->
3182
+ <div class="ds-component" id="comp-kbd">
3183
+ <h3>Kbd</h3>
3184
+ <p class="ds-spec">Tecla en <span class="u-mono">&lt;kbd&gt;</span> con borde, sombra sutil de tinta y tipografía mono · tamaño pequeño, mínimo 20px · combos con "+" entre teclas.</p>
3185
+ <div class="ds-demo" style="gap:24px">
3186
+ <kbd class="kbd">Esc</kbd>
3187
+ <kbd class="kbd">Tab</kbd>
3188
+ <kbd class="kbd">⏎</kbd>
3189
+ <span class="kbd-combo"><kbd class="kbd">⌘</kbd> + <kbd class="kbd">K</kbd></span>
3190
+ <span class="kbd-combo"><kbd class="kbd">Ctrl</kbd> + <kbd class="kbd">S</kbd></span>
3191
+ <span class="kbd-combo"><kbd class="kbd">⌘</kbd> + <kbd class="kbd">⇧</kbd> + <kbd class="kbd">P</kbd></span>
3192
+ </div>
3193
+ <p class="ds-spec">Inline en el texto: abre la paleta con <kbd class="kbd">⌘</kbd> <kbd class="kbd">K</kbd> y guarda con <kbd class="kbd">Ctrl</kbd> <kbd class="kbd">S</kbd>.</p>
3194
+ <details class="ds-code"><summary>Ver código</summary>
3195
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
3196
+ <pre class="ds-pre"><code>&lt;kbd class="kbd"&gt;Esc&lt;/kbd&gt;
3197
+
3198
+ &lt;!-- Combo: "+" literal entre teclas --&gt;
3199
+ &lt;span class="kbd-combo"&gt;
3200
+ &lt;kbd class="kbd"&gt;⌘&lt;/kbd&gt; + &lt;kbd class="kbd"&gt;K&lt;/kbd&gt;
3201
+ &lt;/span&gt;</code></pre>
3202
+ </div>
3203
+ </details>
3204
+ </div>
2225
3205
  </section>
2226
3206
 
2227
3207
  <!-- ============================== MOTION ============================== -->
@@ -2264,11 +3244,11 @@ html, body { overflow-x: clip; }
2264
3244
  <table class="table">
2265
3245
  <thead><tr><th>Combinación</th><th class="num">Contraste</th><th>Veredicto</th></tr></thead>
2266
3246
  <tbody>
2267
- <tr><td><span class="u-mono">neutral-800</span> sobre blanco</td><td class="num">≈ 10.4:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AAA</span></td></tr>
2268
- <tr><td>Blanco sobre <span class="u-mono">primary-500</span></td><td class="num">≈ 3.4:1</td><td><span class="badge badge--warning"><svg><use href="#i-triangle-alert"/></svg>Solo texto ≥18px o UI</span></td></tr>
2269
- <tr><td><span class="u-mono">--link</span> (primary-700) sobre blanco</td><td class="num">≈ 6:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AA texto normal</span></td></tr>
2270
- <tr><td><span class="u-mono">primary-300</span> sobre blanco</td><td class="num">≈ 1.8:1</td><td><span class="badge badge--danger"><svg><use href="#i-circle-alert"/></svg>Nunca como texto</span></td></tr>
2271
- <tr><td>Tonos <span class="u-mono">--*-text</span> sobre <span class="u-mono">*-100</span></td><td class="num">≥ 4.5:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AA garantizado</span></td></tr>
3247
+ <tr><td><span class="u-mono">neutral-800</span> sobre blanco</td><td class="num">12.42:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AAA</span></td></tr>
3248
+ <tr><td>Blanco sobre <span class="u-mono">primary-500</span></td><td class="num">3.22:1</td><td><span class="badge badge--warning"><svg><use href="#i-triangle-alert"/></svg>Solo texto ≥18px o UI</span></td></tr>
3249
+ <tr><td><span class="u-mono">--link</span> (primary-700) sobre blanco</td><td class="num">5.94:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AA texto normal</span></td></tr>
3250
+ <tr><td><span class="u-mono">primary-300</span> sobre blanco</td><td class="num">1.91:1</td><td><span class="badge badge--danger"><svg><use href="#i-circle-alert"/></svg>Nunca como texto</span></td></tr>
3251
+ <tr><td>Tonos <span class="u-mono">--*-text</span> sobre <span class="u-mono">*-100</span></td><td class="num">4.72–6.60:1</td><td><span class="badge badge--success"><svg><use href="#i-circle-check"/></svg>AA garantizado</span></td></tr>
2272
3252
  </tbody>
2273
3253
  </table>
2274
3254
  </div>
@@ -2401,7 +3381,7 @@ html, body { overflow-x: clip; }
2401
3381
  <svg><use href="#i-info"/></svg>
2402
3382
  <div>
2403
3383
  <p class="alert__title">Cómo consumirlo en un proyecto nuevo</p>
2404
- <p class="alert__body">1) Copia <span class="u-mono">dist/celeste-clasica/celeste-clasica.css</span> (o solo <span class="u-mono">tokens.css</span>) e impórtalo antes de tus estilos, o pega el bloque <span class="u-mono">@theme</span> en tu CSS de Tailwind v4. 2) Aplica <span class="u-mono">.celeste-reset</span> al body. 3) Consume solo aliases semánticos. 4) Alterna <span class="u-mono">data-theme</span> en <span class="u-mono">&lt;html&gt;</span> para light/dark.</p>
3384
+ <p class="alert__body">1) Instala <span class="u-mono">@higinio/design-css</span> (e importa <span class="u-mono">@higinio/design-css/celeste/framework.css</span>) o copia <span class="u-mono">dist/celeste-clasica/celeste-clasica.css</span> (o solo <span class="u-mono">tokens.css</span>) e impórtalo antes de tus estilos, o pega el bloque <span class="u-mono">@theme</span> en tu CSS de Tailwind v4. 2) Aplica <span class="u-mono">.celeste-reset</span> al body. 3) Consume solo aliases semánticos. 4) Alterna <span class="u-mono">data-theme</span> en <span class="u-mono">&lt;html&gt;</span> para light/dark.</p>
2405
3385
  </div>
2406
3386
  </div>
2407
3387
  </section>
@@ -2410,9 +3390,22 @@ html, body { overflow-x: clip; }
2410
3390
  <section id="instalacion">
2411
3391
  <p class="ds-eyebrow">Sistema</p>
2412
3392
  <h2>Instalación</h2>
2413
- <p class="ds-lead">El sistema se distribuye como archivos en <span class="u-mono">dist/celeste-clasica/</span> — cópialos a tu proyecto (aún no hay paquete npm). Elige la vía según tu stack. En todas: <strong>light por defecto</strong>; <span class="u-mono">&lt;html data-theme="dark"&gt;</span> activa el modo oscuro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
3393
+ <p class="ds-lead">El sistema se distribuye como paquete npm (<span class="u-mono">@higinio/design-css</span>) y también como archivos sueltos en <span class="u-mono">dist/celeste-clasica/</span>. Instálalo o cópialos, y elige qué importar según tu stack (A/B/C). En todas: <strong>light por defecto</strong>; <span class="u-mono">&lt;html data-theme="dark"&gt;</span> activa el modo oscuro, y los componentes consumen <strong>solo aliases semánticos</strong>, nunca hex.</p>
3394
+
3395
+ <h3>Instalar desde npm (recomendado)</h3>
3396
+ <p>Un solo paquete con todos los sistemas, cada uno en su subruta; Celeste Clásica vive en <span class="u-mono">/celeste/</span>. Instálalo y usa las vías A/B/C apuntando a la subruta, en vez de a un archivo copiado.</p>
3397
+ <div class="ds-code-box"><button class="ds-copy">Copiar</button>
3398
+ <pre class="ds-pre"><code>npm i @higinio/design-css
3399
+
3400
+ /* framework completo (vía A) */
3401
+ @import "@higinio/design-css/celeste/framework.css";
3402
+ /* o solo tokens (vía B) */
3403
+ @import "@higinio/design-css/celeste/tokens.css";
3404
+ /* o Tailwind v4 (vía C) */
3405
+ @import "@higinio/design-css/celeste/tailwind.css";</code></pre>
3406
+ </div>
2414
3407
 
2415
- <h3>A · CSS drop-in (framework completo)</h3>
3408
+ <h3 style="margin-top:32px">A · CSS drop-in (framework completo)</h3>
2416
3409
  <p>Para cualquier proyecto — HTML plano o cualquier framework. Trae variables + clases de componentes listas (tipo Bootstrap) y ya importa las fuentes. Aplica <span class="u-mono">.celeste-reset</span> al <span class="u-mono">&lt;body&gt;</span>.</p>
2417
3410
  <div class="ds-code-box"><button class="ds-copy">Copiar</button>
2418
3411
  <pre class="ds-pre"><code>&lt;link rel="stylesheet" href="/estilos/celeste-clasica.css"&gt;
@@ -2462,20 +3455,23 @@ npm i tailwindcss @tailwindcss/postcss
2462
3455
  <section id="mcp">
2463
3456
  <p class="ds-eyebrow">Sistema</p>
2464
3457
  <h2>MCP</h2>
2465
- <p class="ds-lead">El paquete <span class="code">@higinio/design-mcp</span> expone ambos sistemas de diseño — Celeste Clásica y Nocturna Eléctrica — a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.</p>
3458
+ <p class="ds-lead">El paquete <span class="code">@higinio/design-mcp</span> expone los sistemas de diseño de Higinio Sosa, entre ellos Celeste Clásica (<span class="u-mono">system="celeste"</span>), a cualquier IDE o cliente compatible con Model Context Protocol: tokens, componentes, reglas duras y validación de código. Complementa a las skills de Claude, no las reemplaza: misma fuente de verdad, accesible también fuera del entorno de Claude.</p>
2466
3459
 
2467
- <h3>Las 8 tools</h3>
3460
+ <h3>Las 11 tools</h3>
2468
3461
  <div class="ds-demo ds-demo--col" style="padding:0;overflow-x:auto">
2469
3462
  <table class="table">
2470
3463
  <thead><tr><th>Tool</th><th>Qué hace</th></tr></thead>
2471
3464
  <tbody>
2472
3465
  <tr><td class="u-mono">list_design_systems</td><td>Lista los sistemas de diseño disponibles.</td></tr>
2473
- <tr><td class="u-mono">get_token</td><td>Devuelve el valor resuelto de un token o alias de un sistema.</td></tr>
3466
+ <tr><td class="u-mono">get_token</td><td>Resuelve un alias semántico o un token DTCG por path a su valor.</td></tr>
2474
3467
  <tr><td class="u-mono">search_tokens</td><td>Busca tokens por nombre, categoría o valor.</td></tr>
2475
3468
  <tr><td class="u-mono">list_components</td><td>Lista los componentes documentados de un sistema.</td></tr>
2476
3469
  <tr><td class="u-mono">get_component_spec</td><td>Devuelve la especificación completa de un componente: estados, medidas, variantes.</td></tr>
2477
3470
  <tr><td class="u-mono">get_rules</td><td>Devuelve las reglas duras (no negociables) del sistema.</td></tr>
2478
3471
  <tr><td class="u-mono">validate_code</td><td>Audita CSS/HTML contra las reglas del sistema — hex directos, contraste, aliases.</td></tr>
3472
+ <tr><td class="u-mono">validate_motion</td><td>Audita el motion contra las reglas del sistema: <span class="u-mono">transition</span> sobre todas las propiedades, ease-in en entradas, propiedades de layout, duraciones por encima de <span class="u-mono">--duration-page</span>.</td></tr>
3473
+ <tr><td class="u-mono">check_slop</td><td>Detecta clichés de diseño generado por IA: gradientes, glassmorphism, sombras de negro puro, fuentes genéricas y copy de marketing.</td></tr>
3474
+ <tr><td class="u-mono">checklist_preflight</td><td>Checklist estático de pre-producción sobre HTML/CSS: metadatos, headings, imágenes y accesibilidad de controles.</td></tr>
2479
3475
  <tr><td class="u-mono">generate_snippet</td><td>Genera un snippet de componente listo para usar, con los aliases correctos.</td></tr>
2480
3476
  </tbody>
2481
3477
  </table>
@@ -2523,14 +3519,14 @@ npm i tailwindcss @tailwindcss/postcss
2523
3519
  </footer>
2524
3520
 
2525
3521
  <!-- Modales de demostración -->
2526
- <div class="modal" id="demoModal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle">
2527
- <div class="modal__overlay" data-modal-close></div>
2528
- <div class="modal__panel">
3522
+ <div class="modal" id="demoModal" role="dialog" aria-modal="true" aria-labelledby="demoModalTitle" aria-describedby="demoModalDesc">
3523
+ <div class="modal__overlay"></div>
3524
+ <div class="modal__panel" tabindex="-1">
2529
3525
  <div class="modal__header">
2530
3526
  <h4 class="modal__title" id="demoModalTitle">¿Eliminar proyecto?</h4>
2531
3527
  <button class="icon-btn icon-btn--ghost" data-modal-close aria-label="Cerrar"><svg><use href="#i-x"/></svg></button>
2532
3528
  </div>
2533
- <p style="font-size:var(--text-sm);color:var(--text-secondary)">Se eliminará "Panel Higinio" con sus 12 tableros. Esta acción no se puede deshacer.</p>
3529
+ <p id="demoModalDesc" style="font-size:var(--text-sm);color:var(--text-secondary)">Se eliminará «Panel Higinio» con sus 12 tableros. Esta acción no se puede deshacer.</p>
2534
3530
  <div class="modal__footer">
2535
3531
  <button class="btn btn--secondary" data-modal-close>Cancelar</button>
2536
3532
  <button class="btn btn--danger" data-modal-close>Eliminar proyecto</button>
@@ -2540,7 +3536,7 @@ npm i tailwindcss @tailwindcss/postcss
2540
3536
 
2541
3537
  <div class="modal" id="demoModalLg" role="dialog" aria-modal="true" aria-labelledby="demoModalLgTitle">
2542
3538
  <div class="modal__overlay" data-modal-close></div>
2543
- <div class="modal__panel modal__panel--lg">
3539
+ <div class="modal__panel modal__panel--lg" tabindex="-1">
2544
3540
  <div class="modal__header">
2545
3541
  <h4 class="modal__title" id="demoModalLgTitle">Invitar al equipo</h4>
2546
3542
  <button class="icon-btn icon-btn--ghost" data-modal-close aria-label="Cerrar"><svg><use href="#i-x"/></svg></button>
@@ -2569,6 +3565,65 @@ npm i tailwindcss @tailwindcss/postcss
2569
3565
  </div>
2570
3566
  </div>
2571
3567
 
3568
+ <!-- Navegación móvil del Sidebar contraíble (§10.24): Drawer --left -->
3569
+ <div class="drawer drawer--left" id="demoNavDrawer" role="dialog" aria-modal="true" aria-labelledby="demoNavDrawerTitle">
3570
+ <div class="drawer__overlay" data-drawer-close></div>
3571
+ <div class="drawer__panel" tabindex="-1">
3572
+ <div class="drawer__header">
3573
+ <h4 class="drawer__title" id="demoNavDrawerTitle">Navegación</h4>
3574
+ <button class="icon-btn icon-btn--ghost" data-drawer-close aria-label="Cerrar"><svg><use href="#i-x"/></svg></button>
3575
+ </div>
3576
+ <nav class="drawer__body" aria-label="Navegación de ejemplo">
3577
+ <div class="sidebar__group" role="group" aria-labelledby="demoNavDrawerG1">
3578
+ <span class="sidebar__group-label" id="demoNavDrawerG1">Trabajo</span>
3579
+ <a class="sidebar__item is-active" href="#comp-navegacion" aria-current="page" data-drawer-close><svg aria-hidden="true"><use href="#i-home"/></svg><span class="sidebar__label">Inicio</span></a>
3580
+ <a class="sidebar__item" href="#comp-navegacion" data-drawer-close><svg aria-hidden="true"><use href="#i-folder"/></svg><span class="sidebar__label">Proyectos</span></a>
3581
+ <a class="sidebar__item" href="#comp-navegacion" data-drawer-close><svg aria-hidden="true"><use href="#i-calendar"/></svg><span class="sidebar__label">Calendario</span></a>
3582
+ </div>
3583
+ <div class="sidebar__group" role="group" aria-labelledby="demoNavDrawerG2">
3584
+ <span class="sidebar__group-label" id="demoNavDrawerG2">Equipo</span>
3585
+ <a class="sidebar__item" href="#comp-navegacion" data-drawer-close><svg aria-hidden="true"><use href="#i-chart"/></svg><span class="sidebar__label">Informes</span></a>
3586
+ <a class="sidebar__item" href="#comp-navegacion" data-drawer-close><svg aria-hidden="true"><use href="#i-user"/></svg><span class="sidebar__label">Personas</span></a>
3587
+ <a class="sidebar__item" href="#comp-navegacion" data-drawer-close><svg aria-hidden="true"><use href="#i-inbox"/></svg><span class="sidebar__label">Bandeja</span></a>
3588
+ </div>
3589
+ </nav>
3590
+ </div>
3591
+ </div>
3592
+
3593
+ <!-- Drawer de demostración (§10.29) -->
3594
+ <div class="drawer drawer--right" id="demoDrawer" role="dialog" aria-modal="true" aria-labelledby="demoDrawerTitle">
3595
+ <div class="drawer__overlay" data-drawer-close></div>
3596
+ <div class="drawer__panel" tabindex="-1">
3597
+ <div class="drawer__header">
3598
+ <h4 class="drawer__title" id="demoDrawerTitle">Filtrar proyectos</h4>
3599
+ <button class="icon-btn icon-btn--ghost" data-drawer-close aria-label="Cerrar"><svg><use href="#i-x"/></svg></button>
3600
+ </div>
3601
+ <div class="field">
3602
+ <label class="field__label" for="drawer-nombre">Nombre</label>
3603
+ <input class="input" id="drawer-nombre" type="text" autocomplete="off" placeholder="Buscar por nombre">
3604
+ </div>
3605
+ <div class="field">
3606
+ <label class="field__label" for="drawer-estado">Estado</label>
3607
+ <div class="select-wrap">
3608
+ <select class="select" id="drawer-estado">
3609
+ <option>Todos</option>
3610
+ <option>Activos</option>
3611
+ <option>Archivados</option>
3612
+ </select>
3613
+ </div>
3614
+ </div>
3615
+ <label class="switch">
3616
+ <input type="checkbox">
3617
+ <span class="switch__track"></span>
3618
+ Solo mis proyectos
3619
+ </label>
3620
+ <div class="drawer__footer">
3621
+ <button class="btn btn--secondary" data-drawer-close>Cancelar</button>
3622
+ <button class="btn btn--primary" data-drawer-close>Aplicar filtros</button>
3623
+ </div>
3624
+ </div>
3625
+ </div>
3626
+
2572
3627
  <!-- Contenedor de toasts -->
2573
3628
  <div class="toast-stack" id="toastStack" role="status" aria-live="polite"></div>
2574
3629
 
@@ -2863,30 +3918,102 @@ npm i tailwindcss @tailwindcss/postcss
2863
3918
  });
2864
3919
  });
2865
3920
 
2866
- /* ---------- Modal ---------- */
2867
- var openModalBtns = d.querySelectorAll("[data-modal-open]");
2868
- Array.prototype.forEach.call(openModalBtns, function (btn) {
2869
- btn.addEventListener("click", function () {
2870
- var modal = d.getElementById(btn.getAttribute("data-modal-open"));
2871
- if (modal) modal.classList.add("is-open");
3921
+ /* ---------- Modal (§10.19): focus-trap + retorno de foco + Esc ---------- */
3922
+ var modalTrigger = null;
3923
+ var FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
3924
+ function modalFocusables(modal) {
3925
+ return Array.prototype.filter.call(modal.querySelectorAll(FOCUSABLE), function (el) {
3926
+ return el.offsetWidth > 0 || el.offsetHeight > 0;
2872
3927
  });
3928
+ }
3929
+ function openModal(modal, trigger) {
3930
+ if (!modal) return;
3931
+ modalTrigger = trigger || d.activeElement;
3932
+ modal.classList.add("is-open");
3933
+ var f = modalFocusables(modal);
3934
+ var panel = modal.querySelector(".modal__panel");
3935
+ window.setTimeout(function () { (f[0] || panel).focus(); }, 0);
3936
+ }
3937
+ function closeModal(modal) {
3938
+ if (!modal || !modal.classList.contains("is-open")) return;
3939
+ modal.classList.remove("is-open");
3940
+ if (modalTrigger && typeof modalTrigger.focus === "function") modalTrigger.focus();
3941
+ modalTrigger = null;
3942
+ }
3943
+ Array.prototype.forEach.call(d.querySelectorAll("[data-modal-open]"), function (btn) {
3944
+ btn.addEventListener("click", function () { openModal(d.getElementById(btn.getAttribute("data-modal-open")), btn); });
2873
3945
  });
2874
3946
  d.addEventListener("click", function (e) {
2875
- if (e.target.closest("[data-modal-close]")) {
2876
- var modal = e.target.closest(".modal");
2877
- if (modal) modal.classList.remove("is-open");
2878
- }
3947
+ if (e.target.closest("[data-modal-close]")) closeModal(e.target.closest(".modal"));
2879
3948
  });
2880
3949
  d.addEventListener("keydown", function (e) {
2881
- if (e.key !== "Escape") return;
2882
- Array.prototype.forEach.call(d.querySelectorAll(".modal.is-open"), function (m) {
2883
- m.classList.remove("is-open");
2884
- });
2885
- Array.prototype.forEach.call(d.querySelectorAll(".menu.is-open"), function (m) {
2886
- m.classList.remove("is-open");
2887
- });
3950
+ var openM = d.querySelector(".modal.is-open");
3951
+ if (openM) {
3952
+ if (e.key === "Escape") { e.preventDefault(); closeModal(openM); return; }
3953
+ if (e.key === "Tab") {
3954
+ var f = modalFocusables(openM);
3955
+ if (!f.length) { e.preventDefault(); return; }
3956
+ var first = f[0], last = f[f.length - 1];
3957
+ if (!openM.contains(d.activeElement)) { e.preventDefault(); first.focus(); }
3958
+ else if (e.shiftKey && d.activeElement === first) { e.preventDefault(); last.focus(); }
3959
+ else if (!e.shiftKey && d.activeElement === last) { e.preventDefault(); first.focus(); }
3960
+ }
3961
+ return;
3962
+ }
3963
+ if (e.key === "Escape") {
3964
+ Array.prototype.forEach.call(d.querySelectorAll(".menu.is-open"), function (m) {
3965
+ m.classList.remove("is-open");
3966
+ });
3967
+ }
2888
3968
  });
2889
3969
 
3970
+ /* ---------- Tabla (§10.21): orden + selección + indeterminate ---------- */
3971
+ var demoTable = d.getElementById("demoTable");
3972
+ if (demoTable) {
3973
+ var selectAll = d.getElementById("demoSelectAll");
3974
+ function rowBoxes() { return Array.prototype.slice.call(demoTable.querySelectorAll(".js-row-select")); }
3975
+ function syncSelectAll() {
3976
+ var boxes = rowBoxes();
3977
+ var checked = boxes.filter(function (b) { return b.checked; }).length;
3978
+ selectAll.checked = checked === boxes.length;
3979
+ selectAll.indeterminate = checked > 0 && checked < boxes.length;
3980
+ }
3981
+ rowBoxes().forEach(function (b) {
3982
+ b.addEventListener("change", function () {
3983
+ b.closest("tr").classList.toggle("row--selected", b.checked);
3984
+ syncSelectAll();
3985
+ });
3986
+ });
3987
+ if (selectAll) {
3988
+ selectAll.addEventListener("change", function () {
3989
+ rowBoxes().forEach(function (b) {
3990
+ b.checked = selectAll.checked;
3991
+ b.closest("tr").classList.toggle("row--selected", b.checked);
3992
+ });
3993
+ selectAll.indeterminate = false;
3994
+ });
3995
+ syncSelectAll();
3996
+ }
3997
+ var sortBtn = demoTable.querySelector(".th-sort[data-sort='monto']");
3998
+ if (sortBtn) {
3999
+ var sortTh = sortBtn.closest("th");
4000
+ sortBtn.addEventListener("click", function () {
4001
+ var dir = sortTh.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
4002
+ sortTh.setAttribute("aria-sort", dir);
4003
+ var tbody = demoTable.querySelector("tbody");
4004
+ Array.prototype.slice.call(tbody.querySelectorAll("tr")).sort(function (a, b) {
4005
+ var va = +a.querySelector("[data-monto]").getAttribute("data-monto");
4006
+ var vb = +b.querySelector("[data-monto]").getAttribute("data-monto");
4007
+ return dir === "ascending" ? va - vb : vb - va;
4008
+ }).forEach(function (r) { tbody.appendChild(r); });
4009
+ });
4010
+ }
4011
+ }
4012
+
4013
+ /* ---------- Checkbox indeterminate (§10.9) — solo se fija por JS ---------- */
4014
+ var indet = d.getElementById("demoIndeterminate");
4015
+ if (indet) indet.indeterminate = true;
4016
+
2890
4017
  /* ---------- Menu ---------- */
2891
4018
  Array.prototype.forEach.call(d.querySelectorAll(".js-menu-toggle"), function (btn) {
2892
4019
  btn.addEventListener("click", function (e) {
@@ -2916,6 +4043,31 @@ npm i tailwindcss @tailwindcss/postcss
2916
4043
  });
2917
4044
  });
2918
4045
 
4046
+ /* ---------- Sidebar contraíble (§10.24): riel de iconos ---------- */
4047
+ Array.prototype.forEach.call(d.querySelectorAll(".sidebar__toggle"), function (btn) {
4048
+ var nav = d.getElementById(btn.getAttribute("aria-controls"));
4049
+ var label = btn.querySelector(".sidebar__label");
4050
+ if (!nav) return;
4051
+ btn.addEventListener("click", function () {
4052
+ var collapsed = nav.classList.toggle("is-collapsed");
4053
+ btn.setAttribute("aria-expanded", collapsed ? "false" : "true");
4054
+ if (label) label.textContent = collapsed ? "Expandir" : "Contraer";
4055
+ });
4056
+ });
4057
+
4058
+ /* ---------- Chip de métrica (§10.22): sumar un vaso ---------- */
4059
+ var waterChip = d.getElementById("dsWaterChip");
4060
+ if (waterChip) {
4061
+ var waterValue = waterChip.querySelector(".metric-chip__value");
4062
+ var waterMax = 8, water = 3;
4063
+ waterChip.addEventListener("click", function () {
4064
+ water = water >= waterMax ? 0 : water + 1;
4065
+ waterValue.textContent = water + " de " + waterMax;
4066
+ waterChip.setAttribute("aria-label", "Agua: " + water + " de " + waterMax + " vasos. " +
4067
+ (water < waterMax ? "Sumar un vaso" : "Empezar de nuevo"));
4068
+ });
4069
+ }
4070
+
2919
4071
  /* ---------- Toast ---------- */
2920
4072
  var TOAST_DATA = {
2921
4073
  success: ["i-circle-check", "Cambios guardados", "El proyecto se actualizó correctamente."],
@@ -3054,6 +4206,324 @@ npm i tailwindcss @tailwindcss/postcss
3054
4206
  } else {
3055
4207
  Array.prototype.forEach.call(countEls, countUp);
3056
4208
  }
4209
+
4210
+ /* ---------- Drawer (§10.29): focus-trap + retorno de foco + Esc ---------- */
4211
+ var drawerTrigger = null;
4212
+ function openDrawer(dr, trigger) {
4213
+ if (!dr) return;
4214
+ drawerTrigger = trigger || d.activeElement;
4215
+ dr.classList.add("is-open");
4216
+ if (drawerTrigger.hasAttribute("aria-expanded")) drawerTrigger.setAttribute("aria-expanded", "true");
4217
+ var f = modalFocusables(dr);
4218
+ var panel = dr.querySelector(".drawer__panel");
4219
+ window.setTimeout(function () { (f[0] || panel).focus(); }, 0);
4220
+ }
4221
+ function closeDrawer(dr) {
4222
+ if (!dr || !dr.classList.contains("is-open")) return;
4223
+ dr.classList.remove("is-open");
4224
+ if (drawerTrigger && drawerTrigger.hasAttribute("aria-expanded")) drawerTrigger.setAttribute("aria-expanded", "false");
4225
+ if (drawerTrigger && typeof drawerTrigger.focus === "function") drawerTrigger.focus();
4226
+ drawerTrigger = null;
4227
+ }
4228
+ Array.prototype.forEach.call(d.querySelectorAll("[data-drawer-open]"), function (btn) {
4229
+ btn.addEventListener("click", function () {
4230
+ openDrawer(d.getElementById(btn.getAttribute("data-drawer-open")), btn);
4231
+ });
4232
+ });
4233
+ d.addEventListener("click", function (e) {
4234
+ if (e.target.closest("[data-drawer-close]")) closeDrawer(e.target.closest(".drawer"));
4235
+ });
4236
+ d.addEventListener("keydown", function (e) {
4237
+ var openD = d.querySelector(".drawer.is-open");
4238
+ if (!openD) return;
4239
+ if (e.key === "Escape") { e.preventDefault(); closeDrawer(openD); return; }
4240
+ if (e.key === "Tab") {
4241
+ var f = modalFocusables(openD);
4242
+ if (!f.length) { e.preventDefault(); return; }
4243
+ var first = f[0], last = f[f.length - 1];
4244
+ if (!openD.contains(d.activeElement)) { e.preventDefault(); first.focus(); }
4245
+ else if (e.shiftKey && d.activeElement === first) { e.preventDefault(); last.focus(); }
4246
+ else if (!e.shiftKey && d.activeElement === last) { e.preventDefault(); first.focus(); }
4247
+ }
4248
+ });
4249
+
4250
+ /* ---------- Popover (§10.30): click, foco al panel, Esc y click-out ---------- */
4251
+ Array.prototype.forEach.call(d.querySelectorAll(".popover"), function (pop) {
4252
+ var trigger = pop.querySelector(".popover__trigger");
4253
+ var panel = pop.querySelector(".popover__panel");
4254
+ if (!trigger || !panel) return;
4255
+ function openPop() {
4256
+ pop.classList.add("is-open");
4257
+ trigger.setAttribute("aria-expanded", "true");
4258
+ window.setTimeout(function () { panel.focus(); }, 0);
4259
+ }
4260
+ function closePop(returnFocus) {
4261
+ if (!pop.classList.contains("is-open")) return;
4262
+ pop.classList.remove("is-open");
4263
+ trigger.setAttribute("aria-expanded", "false");
4264
+ if (returnFocus) trigger.focus();
4265
+ }
4266
+ trigger.addEventListener("click", function (e) {
4267
+ e.stopPropagation();
4268
+ if (pop.classList.contains("is-open")) closePop(true); else openPop();
4269
+ });
4270
+ d.addEventListener("click", function (e) {
4271
+ if (pop.classList.contains("is-open") && !pop.contains(e.target)) closePop(false);
4272
+ });
4273
+ pop.addEventListener("keydown", function (e) {
4274
+ if (e.key === "Escape") { e.preventDefault(); closePop(true); }
4275
+ });
4276
+ });
4277
+
4278
+ /* ---------- Combobox (§10.28): filtro en vivo + navegación por teclado ---------- */
4279
+ var cbx = d.getElementById("cbx");
4280
+ if (cbx) {
4281
+ var cbxInput = d.getElementById("cbxInput");
4282
+ var cbxList = d.getElementById("cbxList");
4283
+ var CITIES = ["Aguascalientes", "Ciudad de México", "Guadalajara", "Mérida",
4284
+ "Monterrey", "Puebla", "Querétaro", "Tijuana"];
4285
+ var cbxActive = -1;
4286
+ function cbxOptions() {
4287
+ return Array.prototype.slice.call(cbxList.querySelectorAll(".combobox__option"));
4288
+ }
4289
+ function cbxRender() {
4290
+ var q = cbxInput.value.trim().toLowerCase();
4291
+ var matches = CITIES.filter(function (c) { return c.toLowerCase().indexOf(q) !== -1; });
4292
+ cbxList.innerHTML = "";
4293
+ if (!matches.length) {
4294
+ var empty = d.createElement("li");
4295
+ empty.className = "combobox__empty";
4296
+ empty.textContent = "Sin resultados";
4297
+ cbxList.appendChild(empty);
4298
+ } else {
4299
+ matches.forEach(function (c, i) {
4300
+ var li = d.createElement("li");
4301
+ li.className = "combobox__option";
4302
+ li.id = "cbx-opt-" + i;
4303
+ li.setAttribute("role", "option");
4304
+ li.setAttribute("aria-selected", c === cbxInput.value ? "true" : "false");
4305
+ li.appendChild(d.createTextNode(c));
4306
+ li.insertAdjacentHTML("beforeend",
4307
+ '<svg class="combobox__check" aria-hidden="true"><use href="#i-check"/></svg>');
4308
+ cbxList.appendChild(li);
4309
+ });
4310
+ }
4311
+ cbxActive = -1;
4312
+ cbxInput.removeAttribute("aria-activedescendant");
4313
+ }
4314
+ function cbxOpen() { cbx.classList.add("is-open"); cbxInput.setAttribute("aria-expanded", "true"); }
4315
+ function cbxClose() { cbx.classList.remove("is-open"); cbxInput.setAttribute("aria-expanded", "false"); cbxSetActive(-1); }
4316
+ function cbxSetActive(i) {
4317
+ var opts = cbxOptions();
4318
+ opts.forEach(function (o) { o.classList.remove("is-active"); });
4319
+ cbxActive = i;
4320
+ if (i >= 0 && opts[i]) {
4321
+ opts[i].classList.add("is-active");
4322
+ cbxInput.setAttribute("aria-activedescendant", opts[i].id);
4323
+ opts[i].scrollIntoView({ block: "nearest" });
4324
+ } else {
4325
+ cbxInput.removeAttribute("aria-activedescendant");
4326
+ }
4327
+ }
4328
+ function cbxChoose(opt) {
4329
+ if (!opt || opt.classList.contains("combobox__empty")) return;
4330
+ cbxInput.value = opt.textContent.trim();
4331
+ cbxRender();
4332
+ cbxClose();
4333
+ }
4334
+ cbxInput.addEventListener("input", function () { cbxRender(); cbxOpen(); });
4335
+ cbxInput.addEventListener("focus", function () { cbxRender(); cbxOpen(); });
4336
+ cbxInput.addEventListener("keydown", function (e) {
4337
+ var opts = cbxOptions();
4338
+ if (e.key === "ArrowDown") {
4339
+ e.preventDefault();
4340
+ if (!cbx.classList.contains("is-open")) cbxOpen();
4341
+ if (opts.length) cbxSetActive(Math.min(opts.length - 1, cbxActive + 1));
4342
+ } else if (e.key === "ArrowUp") {
4343
+ e.preventDefault();
4344
+ if (opts.length) cbxSetActive(Math.max(0, cbxActive - 1));
4345
+ } else if (e.key === "Home") {
4346
+ if (opts.length) { e.preventDefault(); cbxSetActive(0); }
4347
+ } else if (e.key === "End") {
4348
+ if (opts.length) { e.preventDefault(); cbxSetActive(opts.length - 1); }
4349
+ } else if (e.key === "Enter") {
4350
+ if (cbxActive >= 0 && opts[cbxActive]) { e.preventDefault(); cbxChoose(opts[cbxActive]); }
4351
+ } else if (e.key === "Escape") {
4352
+ if (cbx.classList.contains("is-open")) { e.preventDefault(); cbxClose(); }
4353
+ }
4354
+ });
4355
+ cbxList.addEventListener("mousedown", function (e) {
4356
+ var opt = e.target.closest(".combobox__option");
4357
+ if (opt) { e.preventDefault(); cbxChoose(opt); }
4358
+ });
4359
+ cbxInput.addEventListener("blur", function () { window.setTimeout(cbxClose, 120); });
4360
+ d.addEventListener("click", function (e) { if (!cbx.contains(e.target)) cbxClose(); });
4361
+ cbxRender();
4362
+ }
4363
+
4364
+ /* ---------- Búsqueda (§10.28): limpiar ---------- */
4365
+ Array.prototype.forEach.call(d.querySelectorAll(".search"), function (s) {
4366
+ var input = s.querySelector(".search__input");
4367
+ var clear = s.querySelector(".search__clear");
4368
+ if (!input || !clear) return;
4369
+ clear.addEventListener("click", function () {
4370
+ input.value = "";
4371
+ input.focus();
4372
+ input.dispatchEvent(new Event("input", { bubbles: true }));
4373
+ });
4374
+ });
4375
+
4376
+ /* ---------- DatePicker (§10.32): calendario + navegación por teclado ---------- */
4377
+ var dp = d.getElementById("demoDatepicker");
4378
+ if (dp) {
4379
+ var dpInput = d.getElementById("dpInput");
4380
+ var dpCal = d.getElementById("dpCal");
4381
+ var dpGrid = d.getElementById("dpGrid");
4382
+ var dpTable = dpGrid.parentNode;
4383
+ var dpTitle = dp.querySelector(".calendar__title");
4384
+ var MONTHS = ["enero", "febrero", "marzo", "abril", "mayo", "junio",
4385
+ "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];
4386
+ var fmtLong = new Intl.DateTimeFormat("es-MX", { weekday: "long", day: "numeric", month: "long", year: "numeric" });
4387
+ var fmtInput = new Intl.DateTimeFormat("es-MX", { day: "2-digit", month: "2-digit", year: "numeric" });
4388
+ function atMidnight(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()); }
4389
+ function sameDay(a, b) {
4390
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
4391
+ }
4392
+ var today = atMidnight(new Date());
4393
+ var selected = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 7);
4394
+ var focusDate = new Date(selected);
4395
+ var view = new Date(selected.getFullYear(), selected.getMonth(), 1);
4396
+ function dpRender() {
4397
+ var y = view.getFullYear(), m = view.getMonth();
4398
+ dpTitle.textContent = MONTHS[m] + " " + y;
4399
+ dpTable.setAttribute("aria-label", MONTHS[m] + " " + y);
4400
+ dpGrid.innerHTML = "";
4401
+ var startDow = (new Date(y, m, 1).getDay() + 6) % 7; // lunes = 0
4402
+ var daysInMonth = new Date(y, m + 1, 0).getDate();
4403
+ var weeks = Math.ceil((startDow + daysInMonth) / 7);
4404
+ var start = new Date(y, m, 1 - startDow);
4405
+ var week = null;
4406
+ for (var i = 0; i < weeks * 7; i++) {
4407
+ if (i % 7 === 0) {
4408
+ week = d.createElement("div");
4409
+ week.className = "calendar__week";
4410
+ week.setAttribute("role", "row");
4411
+ dpGrid.appendChild(week);
4412
+ }
4413
+ var cell = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
4414
+ var btn = d.createElement("button");
4415
+ btn.type = "button";
4416
+ btn.className = "calendar__day";
4417
+ btn.setAttribute("role", "gridcell");
4418
+ btn.textContent = String(cell.getDate());
4419
+ if (cell.getMonth() !== m) btn.classList.add("calendar__day--outside");
4420
+ if (sameDay(cell, today)) btn.classList.add("calendar__day--today");
4421
+ var isSel = sameDay(cell, selected);
4422
+ if (isSel) btn.classList.add("calendar__day--selected");
4423
+ btn.setAttribute("aria-selected", isSel ? "true" : "false");
4424
+ btn.setAttribute("aria-label", fmtLong.format(cell));
4425
+ btn.tabIndex = sameDay(cell, focusDate) ? 0 : -1;
4426
+ btn.setAttribute("data-ts", String(cell.getTime()));
4427
+ week.appendChild(btn);
4428
+ }
4429
+ }
4430
+ function dpButtonFor(dt) { return dpGrid.querySelector('[data-ts="' + atMidnight(dt).getTime() + '"]'); }
4431
+ function dpMoveFocus(dt) {
4432
+ focusDate = atMidnight(dt);
4433
+ if (focusDate.getMonth() !== view.getMonth() || focusDate.getFullYear() !== view.getFullYear()) {
4434
+ view = new Date(focusDate.getFullYear(), focusDate.getMonth(), 1);
4435
+ dpRender();
4436
+ } else {
4437
+ Array.prototype.forEach.call(dpGrid.querySelectorAll(".calendar__day"), function (b) {
4438
+ b.tabIndex = b.getAttribute("data-ts") === String(focusDate.getTime()) ? 0 : -1;
4439
+ });
4440
+ }
4441
+ var b = dpButtonFor(focusDate);
4442
+ if (b) b.focus();
4443
+ }
4444
+ function dpOpen() {
4445
+ dp.classList.add("is-open");
4446
+ focusDate = new Date(selected);
4447
+ view = new Date(focusDate.getFullYear(), focusDate.getMonth(), 1);
4448
+ dpRender();
4449
+ window.setTimeout(function () { var b = dpButtonFor(focusDate); if (b) b.focus(); else dpCal.focus(); }, 0);
4450
+ }
4451
+ function dpClose(returnFocus) {
4452
+ if (!dp.classList.contains("is-open")) return;
4453
+ dp.classList.remove("is-open");
4454
+ if (returnFocus) dpInput.focus();
4455
+ }
4456
+ function dpSelect(dt) {
4457
+ selected = atMidnight(dt);
4458
+ dpInput.value = fmtInput.format(selected);
4459
+ dpClose(true);
4460
+ }
4461
+ dpInput.addEventListener("click", function () {
4462
+ if (dp.classList.contains("is-open")) dpClose(true); else dpOpen();
4463
+ });
4464
+ dpInput.addEventListener("keydown", function (e) {
4465
+ if ((e.key === "Enter" || e.key === " " || e.key === "ArrowDown") && !dp.classList.contains("is-open")) {
4466
+ e.preventDefault(); dpOpen();
4467
+ }
4468
+ });
4469
+ dp.querySelector("[data-cal-prev]").addEventListener("click", function () {
4470
+ view = new Date(view.getFullYear(), view.getMonth() - 1, 1); dpRender();
4471
+ });
4472
+ dp.querySelector("[data-cal-next]").addEventListener("click", function () {
4473
+ view = new Date(view.getFullYear(), view.getMonth() + 1, 1); dpRender();
4474
+ });
4475
+ dpGrid.addEventListener("click", function (e) {
4476
+ var btn = e.target.closest(".calendar__day");
4477
+ if (btn) dpSelect(new Date(Number(btn.getAttribute("data-ts"))));
4478
+ });
4479
+ dpGrid.addEventListener("keydown", function (e) {
4480
+ var delta = null;
4481
+ if (e.key === "ArrowLeft") delta = -1;
4482
+ else if (e.key === "ArrowRight") delta = 1;
4483
+ else if (e.key === "ArrowUp") delta = -7;
4484
+ else if (e.key === "ArrowDown") delta = 7;
4485
+ else if (e.key === "Home") delta = -((focusDate.getDay() + 6) % 7);
4486
+ else if (e.key === "End") delta = 6 - ((focusDate.getDay() + 6) % 7);
4487
+ else if (e.key === "PageUp") { e.preventDefault(); dpMoveFocus(new Date(focusDate.getFullYear(), focusDate.getMonth() - 1, focusDate.getDate())); return; }
4488
+ else if (e.key === "PageDown") { e.preventDefault(); dpMoveFocus(new Date(focusDate.getFullYear(), focusDate.getMonth() + 1, focusDate.getDate())); return; }
4489
+ else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); dpSelect(focusDate); return; }
4490
+ else return;
4491
+ e.preventDefault();
4492
+ dpMoveFocus(new Date(focusDate.getFullYear(), focusDate.getMonth(), focusDate.getDate() + delta));
4493
+ });
4494
+ dpCal.addEventListener("keydown", function (e) {
4495
+ if (e.key === "Escape") { e.preventDefault(); dpClose(true); }
4496
+ });
4497
+ d.addEventListener("click", function (e) {
4498
+ if (dp.classList.contains("is-open") && !dp.contains(e.target)) dpClose(false);
4499
+ });
4500
+ dpInput.value = fmtInput.format(selected);
4501
+ dpRender();
4502
+ }
4503
+
4504
+ /* ---------- FileUpload (§10.33): dragover + quitar item ---------- */
4505
+ var fu = d.getElementById("demoFileUpload");
4506
+ if (fu) {
4507
+ ["dragenter", "dragover"].forEach(function (ev) {
4508
+ fu.addEventListener(ev, function (e) { e.preventDefault(); fu.classList.add("is-dragover"); });
4509
+ });
4510
+ ["dragleave", "drop"].forEach(function (ev) {
4511
+ fu.addEventListener(ev, function (e) {
4512
+ e.preventDefault();
4513
+ if (ev === "dragleave" && e.relatedTarget && fu.contains(e.relatedTarget)) return;
4514
+ fu.classList.remove("is-dragover");
4515
+ });
4516
+ });
4517
+ }
4518
+ var fuList = d.getElementById("fuList");
4519
+ if (fuList) {
4520
+ fuList.addEventListener("click", function (e) {
4521
+ var btn = e.target.closest(".file-upload__remove");
4522
+ if (!btn) return;
4523
+ var item = btn.closest(".file-upload__item");
4524
+ if (item && item.parentNode) item.parentNode.removeChild(item);
4525
+ });
4526
+ }
3057
4527
  })();
3058
4528
  </script>
3059
4529
  </body>