@ajuarezso/sui-design-system 0.20.0 → 0.21.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.
Files changed (59) hide show
  1. package/docs/INDEX.md +51 -51
  2. package/docs/sui-alert/README.md +13 -0
  3. package/docs/sui-badge/README.md +17 -0
  4. package/docs/sui-banner/README.md +190 -0
  5. package/docs/sui-breadcrumb-item/README.md +96 -0
  6. package/docs/sui-breadcrumbs/README.md +122 -0
  7. package/docs/sui-button/README.md +17 -0
  8. package/docs/sui-card/README.md +168 -0
  9. package/docs/sui-chart-legend/README.md +140 -0
  10. package/docs/sui-chart-legend-dot/README.md +118 -0
  11. package/docs/sui-checkbox-card/README.md +150 -0
  12. package/docs/sui-compact-select/README.md +127 -0
  13. package/docs/sui-compact-select-for-input/README.md +117 -0
  14. package/docs/sui-content-card/README.md +147 -0
  15. package/docs/sui-content-label/README.md +148 -0
  16. package/docs/sui-counter-input/README.md +145 -0
  17. package/docs/sui-date-selector/README.md +98 -0
  18. package/docs/sui-datepicker/README.md +193 -0
  19. package/docs/sui-day-cell/README.md +118 -0
  20. package/docs/sui-day-label/README.md +81 -0
  21. package/docs/sui-drawer-footer/README.md +127 -0
  22. package/docs/sui-drawer-header/README.md +140 -0
  23. package/docs/sui-empty-state/README.md +145 -0
  24. package/docs/sui-file-upload/README.md +154 -0
  25. package/docs/sui-file-upload-card/README.md +129 -0
  26. package/docs/sui-image-upload/README.md +200 -0
  27. package/docs/sui-image-upload-profile/README.md +145 -0
  28. package/docs/sui-inline-input/README.md +104 -0
  29. package/docs/sui-inline-select/README.md +101 -0
  30. package/docs/sui-input/README.md +244 -0
  31. package/docs/sui-integration-switch/README.md +142 -0
  32. package/docs/sui-notification/README.md +179 -0
  33. package/docs/sui-notification-footer/README.md +98 -0
  34. package/docs/sui-notification-tab-menu/README.md +112 -0
  35. package/docs/sui-otp-input/README.md +116 -0
  36. package/docs/sui-pagination/README.md +146 -0
  37. package/docs/sui-password-strength/README.md +99 -0
  38. package/docs/sui-period-range/README.md +93 -0
  39. package/docs/sui-popover/README.md +178 -0
  40. package/docs/sui-product-card/README.md +173 -0
  41. package/docs/sui-radio-card/README.md +160 -0
  42. package/docs/sui-rating/README.md +7 -6
  43. package/docs/sui-rich-editor/README.md +114 -0
  44. package/docs/sui-select/README.md +180 -0
  45. package/docs/sui-stepper/README.md +166 -0
  46. package/docs/sui-store-card/README.md +147 -0
  47. package/docs/sui-switch-card/README.md +149 -0
  48. package/docs/sui-tab-chips/README.md +185 -0
  49. package/docs/sui-tab-menu-horizontal/README.md +142 -0
  50. package/docs/sui-tab-menu-vertical/README.md +136 -0
  51. package/docs/sui-tag-input/README.md +120 -0
  52. package/docs/sui-textarea/README.md +157 -0
  53. package/docs/sui-time-picker/README.md +237 -0
  54. package/docs/sui-toast/README.md +190 -0
  55. package/docs/sui-tooltip/README.md +201 -0
  56. package/fesm2022/ajuarezso-sui-design-system.mjs +273 -231
  57. package/fesm2022/ajuarezso-sui-design-system.mjs.map +1 -1
  58. package/package.json +1 -1
  59. package/types/ajuarezso-sui-design-system.d.ts +208 -52
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: sui-tab-menu-vertical
3
+ selector: sui-tab-menu-vertical
4
+ resumen: Menú apilado para cambiar entre secciones de una página de ajustes en web
5
+ alternativas: [sui-tab-menu-horizontal (pocas secciones arriba del contenido), sui-sidebar (navegación de toda la app), sui-tab-chips (filtros o categorías)]
6
+ ---
7
+
8
+ # sui-tab-menu-vertical
9
+
10
+ Story: [Molecules/Tabs/Tab Menu Vertical](https://sui-design-system.web.app/?path=/docs/molecules-tabs-tab-menu-vertical--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** en **web** para una página con varias secciones que se leen de a una, con el menú a la
15
+ izquierda y el contenido a la derecha: los ajustes del local («Datos del local · Horario · Métodos
16
+ de pago · Repartidores»), el perfil del super admin.
17
+
18
+ **No lo uses** para:
19
+
20
+ - **Mobile**: el ítem mide 36 px y ocupa el ancho de la pantalla dejando el contenido debajo. En
21
+ mobile cada sección es una fila de una lista que abre su propia pantalla.
22
+ - La navegación de toda la app: `sui-sidebar`.
23
+ - 2 a 4 secciones arriba del contenido: `sui-tab-menu-horizontal`.
24
+ - Filtros de una lista: `sui-tab-chips`.
25
+
26
+ Regla corta: **secciones de una página de ajustes en web → tab menu vertical · en mobile → lista de
27
+ filas que abren pantalla.**
28
+
29
+ ## Qué variante usar
30
+
31
+ > **Lo que dice la guía del DS**: es un layout apilado para **cambiar entre secciones o categorías**.
32
+ > Sus ejemplos son ajustes («Profile Settings · Contact Information · Social Links · Export Data»),
33
+ > uno con **título arriba** (en SUI, `dividerLabel`) y otro con **contenedor con estilo** (en SUI,
34
+ > `menuStyle="card"`). No trae do/don't.
35
+ >
36
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app` (misma exclusión que `sui-badge`): **0 usos**.
37
+
38
+ ### `menuStyle`
39
+
40
+ | valor | cuándo | fuente |
41
+ |---|---|---|
42
+ | `card` | El menú flota junto al contenido sobre un fondo gris (`bg-weak-50`): el borde y la sombra lo separan. | según la guía del DS (ejemplo «contenedor con estilo»); el cuándo es propuesta |
43
+ | `list` | El menú vive dentro de una columna que ya tiene fondo o borde propio (una card, un panel). | según la guía del DS (ejemplo base); el cuándo es propuesta |
44
+
45
+ ### `dividerLabel`
46
+
47
+ | valor | cuándo | fuente |
48
+ |---|---|---|
49
+ | vacío (default) | Un solo grupo de secciones. | propuesta, falta aprobación |
50
+ | texto | Nombra el grupo cuando hay más de un menú apilado («Tu local» · «Tu cuenta»). Sale en MAYÚSCULAS y gris. | según la guía del DS (ejemplo con título) |
51
+
52
+ ### Ítems (`tabs`)
53
+
54
+ | qué | cuándo | fuente |
55
+ |---|---|---|
56
+ | texto | El nombre de la sección. Una o dos palabras; si es más largo, se corta con «…». | según la guía del DS (ejemplos con texto) |
57
+ | `icon` | Sí, en todos los ítems: en un menú lateral el ícono ayuda a encontrar rápido. El activo se pinta naranja. | propuesta, falta aprobación |
58
+ | `number` | Pendientes que piden acción en esa sección (círculo rojo). | propuesta, falta aprobación |
59
+ | `rightIcon` | Cuando el ítem lleva a otra página en vez de cambiar el panel de al lado. | propuesta, falta aprobación |
60
+ | `disabled` | Una sección que depende del plan. | propuesta, falta aprobación |
61
+
62
+ Usos de hoy que rompen las reglas: **0** (no se usa).
63
+
64
+ ## Tamaño en mobile y en web
65
+
66
+ Ítem de **36 px** (8 + 20 + 8), ícono de 20. **No tiene `size`** y no tiene tamaño para mobile.
67
+
68
+ | uso | mobile | web | fuente |
69
+ |---|---|---|---|
70
+ | secciones de ajustes | no se usa: lista de filas que abren pantalla | `tab-menu-vertical` | propuesta, falta aprobación |
71
+
72
+ ```html
73
+ @if (!bp.isMobile()) {
74
+ <sui-tab-menu-vertical [tabs]="secciones" [(active)]="seccion" menuStyle="card" />
75
+ }
76
+ ```
77
+
78
+ ## «No usar»: por qué no hoy y cuándo sí
79
+
80
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
81
+ |---|---|---|
82
+ | el componente entero | ninguna pantalla de la app tiene hoy un menú lateral de secciones | una página de ajustes en web con 5+ secciones (ajustes del local en desktop) |
83
+ | en mobile | 36 px y sin lugar para el contenido al lado | nunca |
84
+ | `rightIcon` | ningún menú mezcla paneles y páginas | un ítem «Facturación» que sale a otra página mientras el resto cambia el panel |
85
+
86
+ ## API
87
+
88
+ <!-- api:inicio -->
89
+ <!-- generada: no editar a mano — `npm run docs` -->
90
+
91
+ | input | tipo | default |
92
+ |---|---|---|
93
+ | `tabs` | `SuiTabMenuVerticalItem<T>[]` | — (requerido) |
94
+ | `active` · model, `[(active)]` | `T` | `undefined` |
95
+ | `menuStyle` | `'card' \| 'list'` | `'card'` |
96
+ | `dividerLabel` | `string` | `''` |
97
+
98
+ | output | payload |
99
+ |---|---|
100
+ | `tabChange` | `T` |
101
+ <!-- api:fin -->
102
+
103
+ ```ts
104
+ interface SuiTabMenuVerticalItem<T = string> {
105
+ value: T;
106
+ label: string;
107
+ icon?: string; // RemixIcon, 20 px
108
+ rightIcon?: boolean; // flecha en píldora con sombra
109
+ number?: number; // círculo rojo; 0/undefined no se dibuja
110
+ disabled?: boolean;
111
+ }
112
+ type SuiTabMenuVerticalStyle = 'card' | 'list';
113
+ ```
114
+
115
+ ## Trampas
116
+
117
+ - **El estilo se llama `menuStyle`**, no `style` ni `variant` (ni `verticalStyle`, que era el nombre
118
+ viejo de `sui-tabs`).
119
+ - **Sin `active` no se marca ningún ítem.** Pásale un valor inicial.
120
+ - **Ocupa el ancho del padre**: dale un ancho fijo a la columna (240-280 px).
121
+ - **El ícono activo es naranja** (`primary-base`); en el tab menu horizontal es negro. Es a propósito.
122
+ - **Emite dos veces**: `tabChange` y `activeChange`.
123
+ - `role="tablist"`: si cada ítem navega a una ruta, el lector de pantalla lo anuncia mal; para
124
+ navegación entre rutas, `sui-sidebar`.
125
+
126
+ ## Ejemplo
127
+
128
+ ```html
129
+ <sui-tab-menu-vertical
130
+ menuStyle="card"
131
+ dividerLabel="Tu local"
132
+ [tabs]="[{ value: 'datos', label: 'Datos del local', icon: 'store-2-line' },
133
+ { value: 'horario', label: 'Horario', icon: 'time-line' },
134
+ { value: 'pagos', label: 'Métodos de pago', icon: 'bank-card-line' }]"
135
+ [(active)]="seccion" />
136
+ ```
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: sui-tag-input
3
+ selector: sui-tag-input
4
+ resumen: Campo donde se escriben varios valores cortos que se van sumando como chips (palabras clave)
5
+ alternativas: [sui-input (un solo valor), sui-select (una opción de una lista), sui-checkbox-label (varias opciones fijas)]
6
+ ---
7
+
8
+ # sui-tag-input
9
+
10
+ Story: [Molecules/Input/Tag Input](https://sui-design-system.web.app/?path=/docs/molecules-input-tag-input--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** para una lista de valores **libres y cortos** que la persona escribe uno por uno: palabras
15
+ clave, rubros, sinónimos. Enter (y la coma, si se pide) convierte lo escrito en un chip; la ✕ de
16
+ cada chip lo quita; borrar con el campo vacío quita el último.
17
+
18
+ **No lo uses** para:
19
+
20
+ - Elegir varias opciones que ya existen: casillas (`sui-checkbox-label`).
21
+ - Un solo valor: `sui-input`.
22
+ - Una lista que se guarda como texto separado por comas y nadie ve como lista.
23
+
24
+ Regla corta: **varios valores libres → tag-input · varias opciones fijas → casillas.**
25
+
26
+ ## Qué variante usar
27
+
28
+ > **En la guía del DS es un ejemplo de su página del input** («With Tags»: un campo con los chips
29
+ > Berlin, London, Paris debajo). No dice cuándo usarlo.
30
+ >
31
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app`: **0 usos**. Candidato: «Rubros que entran»
32
+ > en `pages/super-admin/master-settings/master-settings.html:134`, un `sui-input` donde se escriben
33
+ > palabras separadas por coma («ferreteria, ferretería, construcción») y la pantalla las parte a mano.
34
+
35
+ | prop / evento | cuándo | fuente |
36
+ |---|---|---|
37
+ | `label` · `required` · `sublabel` · `tooltip` · `hint` | Igual que `sui-input`. El hint dice cómo se agrega: «Escribe y presiona Enter». | según la guía del DS (ejemplo con label «Tag Input») |
38
+ | `placeholder` | **Siempre**: el default es «Add tags...». «Agrega una palabra…». | propuesta, falta aprobación |
39
+ | `commaSeparator` | `true` cuando la gente ya escribe con comas (rubros, sinónimos). | propuesta, falta aprobación |
40
+ | `addOnBlur` | `true` en mobile: si sale del campo sin Enter, lo escrito no se pierde. | propuesta, falta aprobación |
41
+ | `maxTags` | Cuando hay un tope real. Hoy no hay. | propuesta, falta aprobación |
42
+ | `allowDuplicates` | `false` (default). | propuesta, falta aprobación |
43
+ | `rightIcon` | No usar hoy (ícono de info a la derecha). | — |
44
+ | `error` + `errorMessage` | Falta al menos un valor en un campo obligatorio. | según la guía del DS (estado de error del Figma) |
45
+ | `disabled` | Mientras se guarda. | API |
46
+ | `(tagAdded)` · `(tagRemoved)` | Guardar al momento, sin botón. | API |
47
+
48
+ ## Tamaño en mobile y en web
49
+
50
+ Publicado (0.19.0): `xsmall` (32), `small` (36), `medium` (40). **En el repo del DS, sin publicar:**
51
+ `large` (48) y `xlarge` (52), los mismos altos de `sui-input`, y el texto a 16 px en mobile (en
52
+ 0.19.0 queda en 14). Los chips de abajo son `sui-tag` de tamaño fijo: **la ✕ para quitar un chip
53
+ no crece** con el campo.
54
+
55
+ | uso | mobile | web | fuente |
56
+ |---|---|---|---|
57
+ | campo de formulario | `large` (48) **cuando se publique**; hasta entonces `medium` (40) | `medium` (40) | propuesta, falta aprobación |
58
+
59
+ ```html
60
+ <sui-tag-input [size]="bp.isMobile() ? 'large' : 'medium'" … /> <!-- 'large' desde la próxima versión -->
61
+ ```
62
+
63
+ ## «No usar»: por qué no hoy y cuándo sí
64
+
65
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
66
+ |---|---|---|
67
+ | el componente entero (0 usos) | nadie carga listas de palabras | «Rubros que entran» de una categoría del home (`master-settings.html:134`), o sinónimos de un producto para el buscador |
68
+ | `rightIcon` | el hint explica mejor | — |
69
+ | `maxTags` | no hay topes | un máximo de 5 palabras clave por producto |
70
+ | `allowDuplicates` | repetir no sirve | — |
71
+ | `size` `xsmall` / `small` | no hay tag-inputs en tablas | un filtro denso del panel |
72
+
73
+ ## API
74
+
75
+ <!-- api:inicio -->
76
+ <!-- generada: no editar a mano — `npm run docs` -->
77
+
78
+ | input | tipo | default |
79
+ |---|---|---|
80
+ | `size` | `'xsmall' \| 'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` |
81
+ | `placeholder` | `string` | `'Add tags...'` |
82
+ | `disabled` | `boolean` | `false` |
83
+ | `error` | `boolean` | `false` |
84
+ | `errorMessage` | `string \| null` | `null` |
85
+ | `rightIcon` | `string \| boolean` | `false` |
86
+ | `label` | `string \| null` | `null` |
87
+ | `sublabel` | `string \| null` | `null` |
88
+ | `required` | `boolean` | `false` |
89
+ | `tooltip` | `string \| null` | `null` |
90
+ | `hint` | `string \| null` | `null` |
91
+ | `commaSeparator` | `boolean` | `false` |
92
+ | `allowDuplicates` | `boolean` | `false` |
93
+ | `maxTags` | `number \| null` | `null` |
94
+ | `addOnBlur` | `boolean` | `false` |
95
+ | `value` · model, `[(value)]` | `string[]` | `[]` |
96
+
97
+ | output | payload |
98
+ |---|---|
99
+ | `tagAdded` | `string` |
100
+ | `tagRemoved` | `string` |
101
+ <!-- api:fin -->
102
+
103
+ `value` es `model<string[]>`. Implementa `ControlValueAccessor`: lo que está escrito sin confirmar
104
+ no es parte del valor. Slot `[slot=right]` para un contenido propio a la derecha.
105
+
106
+ ## Trampas
107
+
108
+ - **Placeholder en inglés por default** («Add tags...»). Pásalo siempre.
109
+ - Sin `commaSeparator` la coma es un carácter más: «a, b» queda como un solo chip.
110
+ - Sin `addOnBlur`, lo que se escribió y no se confirmó con Enter se pierde al salir.
111
+ - Los chips van **debajo** del campo, no adentro.
112
+ - El hover quita el borde (a diferencia de `sui-input`, que lo oscurece).
113
+
114
+ ## Ejemplo
115
+
116
+ ```html
117
+ <sui-tag-input label="Rubros que entran" placeholder="Agrega una palabra…"
118
+ hint="Un local entra si su rubro contiene alguna" [commaSeparator]="true"
119
+ [addOnBlur]="true" [(value)]="rubros" />
120
+ ```
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: sui-textarea
3
+ selector: sui-textarea
4
+ resumen: Campo de varias líneas para un texto libre (motivo, nota al repartidor, descripción)
5
+ alternativas: [sui-input (un dato de una línea), sui-rich-editor (texto con formato, hoy sin uso), sui-select (motivos que se pueden elegir de una lista)]
6
+ ---
7
+
8
+ # sui-textarea
9
+
10
+ Story: [Molecules/Input/Textarea](https://sui-design-system.web.app/?path=/docs/molecules-input-textarea--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** para un texto que la persona redacta y que puede ocupar más de una línea: el motivo de
15
+ un rechazo o una cancelación, la nota para el repartidor, la descripción de un producto, un
16
+ comentario al calificar.
17
+
18
+ **No lo uses** para:
19
+
20
+ - Un dato de una línea (nombre, referencia corta, URL, un slug): `sui-input`. Hoy hay uno así:
21
+ «Personaliza tu url» en `pages/mi-negocio/mi-negocio.html:706`.
22
+ - Un motivo que se puede elegir de una lista cerrada: radios o `sui-select`, y la opción «Otro»
23
+ abre el textarea (como ya hace `pages/envios/envios.html:819`, que además corta a 200 a mano con
24
+ `.slice(0, 200)` en vez de `[maxLength]="200"`).
25
+ - Texto con negritas o enlaces: no hay caso hoy (ver `sui-rich-editor`).
26
+ - Un `<textarea>` nativo: queda **1** (`pages/products/add-product/add-product.html:155`).
27
+
28
+ Regla corta: **se redacta y puede pasar de una línea → textarea · una línea → input · se elige → select.**
29
+
30
+ ## Qué variante usar
31
+
32
+ > **La guía del DS lo presenta con contador de caracteres y su propia manija para agrandarlo**
33
+ > («78/200»), y aparte una versión simple sin contador. No dice cuándo va cada una.
34
+ >
35
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app` (misma exclusión que `sui-badge`, más
36
+ > `master-ejercicios`): **50 usos en 29 archivos**. `rows`: 32 en 3, 13 en 2, 4 en 4, 1 en 8.
37
+ > Con `maxLength`: 8. Con `resizable=false`: 2. 38 con label, 12 sin.
38
+
39
+ | prop | cuándo | fuente |
40
+ |---|---|---|
41
+ | `label` | Siempre, salvo cuando la pregunta ya está escrita justo encima (la calificación: «¿Qué te gustó?»). | según la guía del DS (ejemplo con label) |
42
+ | `required` | Motivo obligatorio (rechazar, anular). | según la guía del DS (`Enter Your Message*`) |
43
+ | `sublabel` | **«(Opcional)»**, con paréntesis, cuando no es obligatorio, en vez de «(opcional)» dentro del label. Sin paréntesis queda pegado al label. | según la guía del DS (`(Optional)`) y canon (README de `sui-form-label`) |
44
+ | `tooltip` | Hoy sin caso. | según la guía del DS (ícono de info del label) |
45
+ | `hint` | Para quién es el texto: «Lo va a leer el cliente», «Solo lo ve tu equipo». | según la guía del DS (ejemplo con hint) |
46
+ | `error` + `errorMessage` | Motivo obligatorio vacío. El mensaje reemplaza al hint y el contador se pone rojo. | según la guía del DS (`Has Error`) |
47
+ | `placeholder` | Un ejemplo de lo que se espera: «Ej. frágil, tocar el timbre, entregar en recepción». Elipsis `…`. | canon (CANON-SUI §5.2) |
48
+ | `maxLength` | **Siempre que otro lo va a leer** (cliente, repartidor, local) o se guarda en la base: muestra «n/200» y corta el exceso. 200 para motivos y notas; 500 para descripciones. | según la guía del DS (su ejemplo principal lleva contador); los topes son propuesta |
49
+ | `size` | El mismo de los inputs del formulario: `large` en mobile, `medium` en web. Ver «Tamaño en mobile y en web». | canon (aprobado por el dueño, 03-10-2026), extensión SUI; el «cuándo» es propuesta, falta aprobación |
50
+ | `rows` | `3` por defecto (motivos, notas). `2` para una nota corta en un formulario largo. `4` o más solo para una descripción larga. | propuesta, falta aprobación |
51
+ | `resizable` | `true` en web. En mobile **`false`**: la manija no se puede arrastrar con el dedo y parece un adorno. | según la guía del DS (versión con y sin manija); el «cuándo» es propuesta |
52
+ | `readonly` | Mostrar un texto ya enviado con la misma forma del campo (copiar un mensaje). | propuesta, falta aprobación |
53
+ | `disabled` | Mientras se envía. | según la guía del DS |
54
+ | `autocomplete` | No hace falta: ningún texto libre se autocompleta. | propuesta, falta aprobación |
55
+
56
+ **Usos de hoy que rompen la regla:**
57
+
58
+ - **3 `sublabel="Opcional"` sin paréntesis** (se lee «Notas adicionalesOpcional») → «(Opcional)»:
59
+ `pages/customer/checkout/checkout.html:209`, `pages/self-delivery/self-delivery.html:101`,
60
+ `components/add-product-modal/add-product-modal.html:184`.
61
+ - **9 «(opcional)» dentro del label** → `sublabel="(Opcional)"`: `pages/super-admin/super-admin.html:333`
62
+ («Notas (opcional)»), `pages/super-admin/master-orders/master-orders.html:795`,
63
+ `components/cuenta-bloqueada/cuenta-bloqueada.html:21`.
64
+ - **Textos que lee otra persona sin `maxLength`** (42 de 50 no lo tienen). Los que más importan:
65
+ `pages/envios/envios.html:419` («Nota para el repartidor»), `pages/customer/checkout/checkout.html:209`
66
+ («Notas adicionales»), `pages/super-admin/master-users/master-users.html:266` («Motivo (lo va a leer)»).
67
+ - **4 placeholders con `...`**: `pages/customer/checkout/checkout.html:209`,
68
+ `pages/self-delivery/self-delivery.html:101`, `pages/driver/driver-order-detail/driver-order-detail.html:823`.
69
+
70
+ ## Tamaño en mobile y en web
71
+
72
+ Tiene `size` con **los mismos nombres que `sui-input`** (`xsmall` · `small` · `medium` · `large` ·
73
+ `xlarge`): cambia el padding y el radio, y el texto arranca en la misma línea vertical que el del
74
+ input del mismo tamaño. El alto lo siguen poniendo las `rows`. `medium` es el de la guía del DS y el
75
+ default (padding 10/12, radio 12); el resto es extensión SUI.
76
+
77
+ | size | padding (arriba/der · izq) | radio | fuente |
78
+ |---|---|---|---|
79
+ | `xlarge` | 14 · 16 | 12 | canon (aprobado por el dueño, 03-10-2026), extensión SUI |
80
+ | `large` | 12 · 14 | 12 | canon (aprobado por el dueño, 03-10-2026), extensión SUI |
81
+ | `medium` (default) | 10 · 12 | 12 | según la guía del DS |
82
+ | `small` | 8 · 10 | 10 | canon (aprobado por el dueño, 03-10-2026), extensión SUI |
83
+ | `xsmall` | 6 · 8 | 8 | canon (aprobado por el dueño, 03-10-2026), extensión SUI |
84
+
85
+ **En mobile (≤ 767 px) la letra sube sola a 16/24** (Paragraph/Medium), como el input sube a 16:
86
+ evita el zoom de iOS al enfocar y el textarea deja de verse más chico que los campos de arriba. Por
87
+ eso cada fila mide 24 px en el teléfono y 20 en web (3 filas ≈ 72 + padding en mobile, 60 + padding
88
+ en web): el área que se toca siempre pasa los 44 px.
89
+
90
+ | uso | mobile | web | fuente |
91
+ |---|---|---|---|
92
+ | motivo, nota | el `size` del formulario (`large` si los inputs van en `large`), `rows=3`, `[resizable]="false"` | `medium`, `rows=3`, manija | propuesta, falta aprobación |
93
+ | descripción larga | igual, `rows=4` | `medium`, `rows=4` | propuesta, falta aprobación |
94
+
95
+ ```html
96
+ <sui-textarea [size]="bp.isMobile() ? 'large' : 'medium'" [rows]="3"
97
+ [resizable]="!bp.isMobile()" … /> <!-- bp = BreakpointsService -->
98
+ ```
99
+
100
+ ## «No usar»: por qué no hoy y cuándo sí
101
+
102
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
103
+ |---|---|---|
104
+ | `tooltip` | el hint alcanza | un campo del panel con una regla que no entra en una línea («qué puede ver el cliente») |
105
+ | `rows` 6 o más | ningún texto de la app es tan largo | términos y condiciones propios de un local, o la descripción larga de una campaña |
106
+ | `autocomplete` | un texto libre no se autocompleta | — |
107
+
108
+ ## API
109
+
110
+ <!-- api:inicio -->
111
+ <!-- generada: no editar a mano — `npm run docs` -->
112
+
113
+ | input | tipo | default |
114
+ |---|---|---|
115
+ | `rows` | `number` | `4` |
116
+ | `size` | `'xsmall' \| 'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` |
117
+ | `placeholder` | `string` | `''` |
118
+ | `disabled` | `boolean` | `false` |
119
+ | `readonly` | `boolean` | `false` |
120
+ | `error` | `boolean` | `false` |
121
+ | `errorMessage` | `string \| null` | `null` |
122
+ | `label` | `string \| null` | `null` |
123
+ | `hint` | `string \| null` | `null` |
124
+ | `autocomplete` | `string \| null` | `null` |
125
+ | `resizable` | `boolean` | `true` |
126
+ | `maxLength` | `number \| null` | `null` |
127
+ | `sublabel` | `string \| null` | `null` |
128
+ | `required` | `boolean` | `false` |
129
+ | `tooltip` | `string \| null` | `null` |
130
+ | `value` · model, `[(value)]` | `string` | `''` |
131
+
132
+ Outputs: ninguno.
133
+ <!-- api:fin -->
134
+
135
+ `value` es `model<string>`. Implementa `ControlValueAccessor` (`formControlName`, `[(ngModel)]`).
136
+
137
+ ## Trampas
138
+
139
+ - **`maxLength` con L mayúscula** (en `sui-input` es `maxlength`). Con el nombre equivocado Angular
140
+ no avisa en un atributo plano y el tope no se aplica.
141
+ - Sin `maxLength` **no hay contador**; la manija sale igual mientras `resizable` sea `true`.
142
+ - El tope también corta lo que se pega (no solo lo que se tipea).
143
+ - El hint lleva siempre el ícono `information-fill`, también el neutro (así está en el Figma).
144
+ - El `size` no cambia el alto: eso lo deciden las `rows`. Lo que alinea con el input es el padding.
145
+ - `error` sin `errorMessage` pinta el borde y no dice qué falta.
146
+
147
+ ## Ejemplo
148
+
149
+ ```html
150
+ <sui-textarea label="Nota para el repartidor" sublabel="(Opcional)" [rows]="3" [maxLength]="200"
151
+ [size]="bp.isMobile() ? 'large' : 'medium'" placeholder="Ej. frágil, tocar el timbre, entregar en recepción"
152
+ [resizable]="!bp.isMobile()" [(value)]="nota" />
153
+
154
+ <sui-textarea label="Motivo del rechazo" [required]="true" [rows]="3" [maxLength]="200"
155
+ hint="Lo va a leer el local" [error]="intentado() && !motivo()"
156
+ errorMessage="Escribe el motivo" [(value)]="motivo" />
157
+ ```
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: sui-time-picker
3
+ selector: sui-time-picker
4
+ resumen: Campo para elegir una hora del día, con columnas de hora, minuto y AM/PM
5
+ alternativas: [sui-tab-chips + sui-radio-label (día y franja de entrega de un pedido programado), sui-select (pocas horas fijas), sui-input type="time" (hora libre con el selector del sistema)]
6
+ ---
7
+
8
+ # sui-time-picker
9
+
10
+ Story: [Molecules/Date & Time/Time Picker](https://sui-design-system.web.app/?path=/docs/molecules-date-time-time-picker--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** para una **hora del día** que la persona escribe con libertad: el horario de apertura y
15
+ cierre del local, el turno del repartidor, «Cerrar hasta las 9:30 PM».
16
+
17
+ **No lo uses** para:
18
+
19
+ - **La hora de entrega de un pedido programado**: no se elige una hora libre, se elige una
20
+ **franja** que el local puede cumplir. Va la hoja del checkout (`sui-tab-chips` para el día y
21
+ `sui-radio-label` con `flip` para cada franja). Es la maqueta aprobada, y el propio código
22
+ descartó `sui-time-picker-item` para esas filas porque son densas y sin separación
23
+ (`checkout.html:475`).
24
+ - Pocas horas fijas («12:00», «13:00», «14:00»): `sui-select`.
25
+ - Una duración («30 min», «1 h»): no hay componente de duración; un `sui-select` o
26
+ `sui-tab-chips` con los valores (ver Trampas: los presets de este componente no son duraciones).
27
+
28
+ Regla corta: **hora libre del día → time-picker · franja de entrega → la hoja del checkout ·
29
+ pocas horas → select.**
30
+
31
+ ## Qué variante usar
32
+
33
+ > **La guía del DS no tiene página para este componente.** Todo es **propuesta, falta
34
+ > aprobación**, aunque ya sea práctica de la app.
35
+ >
36
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app` (sin prototipos ni vitrinas): **7 usos en
37
+ > 4 pantallas** — horario del local (`weekly-schedule-editor`, 2), turnos del repartidor
38
+ > (`driver-schedule`, 2), votaciones de cierre y recargo (`driver-dashboard`, 2) y el cierre del
39
+ > local en la central (`central-inicio`, 1). Los 7 van con `size="medium"`, `format="12h"`,
40
+ > `[durationPresets]="[]"` y footer. `step`: 5 en 5 usos, 30 en 2.
41
+
42
+ ### `format`
43
+
44
+ | valor | cuándo | fuente |
45
+ |---|---|---|
46
+ | `12h` (default) | Siempre: en Perú la hora se dice «9:30 PM». Los 7 usos lo llevan. | propuesta |
47
+ | `24h` | No usar en la app. | no usar |
48
+
49
+ ### `step`
50
+
51
+ | valor | cuándo | fuente |
52
+ |---|---|---|
53
+ | `5` (default) | Horas reales que la persona conoce al minuto: apertura y cierre del local, «cerrar hasta». Con saltos de 30 se redondeaban cierres reales (comentario en `weekly-schedule-editor.html:41`). | propuesta |
54
+ | `30` | Bloques de un turno, que se planifican por media hora (`driver-schedule.html:142`). | propuesta |
55
+ | `15` | Citas o reservas en cuartos de hora. Sin caso hoy. | propuesta |
56
+ | `1` | No usar: 60 opciones en la columna para una precisión que nadie pide. | no usar |
57
+
58
+ ### `durationPresets`, `showFooter`, `inline`
59
+
60
+ | input | valor | cuándo | fuente |
61
+ |---|---|---|---|
62
+ | `durationPresets` | `[]` | **Siempre.** Los presets de fábrica (15 min, 30 min, 45 min, 1 h) ponen la hora como «tiempo desde la medianoche»: «30 min» deja 12:30 AM. Los 7 usos los apagan. | propuesta |
63
+ | `durationPresets` | lista propia | No usar mientras se lean como hora del día. | no usar |
64
+ | `showFooter` | `true` (default) | Cancelar/Aplicar: la hora no cambia hasta confirmar. Es lo correcto cuando la hora se guarda o publica algo (el horario del local). | propuesta |
65
+ | `showFooter` | `false` | Se aplica al tocar cada columna. Solo dentro de un formulario que ya tiene su propio «Guardar». | propuesta |
66
+ | `inline` | `true` | El panel es el contenido, dentro de un `sui-modal` (sin campo ni popover). | propuesta |
67
+
68
+ ### `size`, `label`, `hint`, `error`, `required`, `disabled`
69
+
70
+ | input | cuándo | fuente |
71
+ |---|---|---|
72
+ | `size="medium"` (40 px, default) | Siempre: es el más grande que hay (ver «Tamaño»). | propuesta |
73
+ | `size="small"` (36 px) | Filas densas en web. | propuesta |
74
+ | `label` | Siempre, corto y en verbo o momento: «Abre», «Cierra», «Inicio», «Cerrar hasta». | propuesta |
75
+ | `sublabel` / `tooltip` | «Opcional» o una aclaración que no entra en el label. | propuesta |
76
+ | `hint` | Qué pasa con esa hora: «Se reabre sola a medianoche». | propuesta |
77
+ | `error` + `errorMessage` | La hora no cuadra: «Cierra antes de abrir». | propuesta |
78
+ | `required` | La hora es obligatoria para guardar. | propuesta |
79
+ | `disabled` | Mientras se guarda (como hacen el horario y los turnos). | propuesta |
80
+ | `placeholder` | Lo que se elige: «Hora de cierre». El default «Seleccionar hora» sirve. | propuesta |
81
+
82
+ Usos de hoy que rompen la regla: **0**. Los 7 cumplen `12h`, `durationPresets=[]` y `label`.
83
+
84
+ ### Las piezas que trae (exportadas, sin carpeta ni story propias)
85
+
86
+ | pieza | qué es | cuándo suelta | fuente |
87
+ |---|---|---|---|
88
+ | `sui-time-picker-item` | una fila de la columna (36 px) | No usar: para filas de horario que se tocan con el pulgar va `sui-radio-label` (`checkout.html:475`). | no usar |
89
+ | `sui-time-picker-duration` | la pastilla de un preset | No usar: es la de los presets de duración. | no usar |
90
+ | `sui-time-picker-status` | pastilla Disponible/Ocupado/En reunión/Desconectado | No usar: sus textos de fábrica están en inglés y el estado del repartidor ya tiene su badge. | no usar |
91
+
92
+ ## Tamaño en mobile y en web
93
+
94
+ En mobile se toca con el dedo: lo que se toca mide **44 px o más**. En web, con mouse, va más
95
+ chico. En pantallas de los dos se elige con el servicio de breakpoints de la app (`bp`, en
96
+ `services/breakpoints.service.ts`).
97
+
98
+ `size` tiene `small` (36), `medium` (40, default) y **`large` (48)**, extensión SUI: mide lo mismo
99
+ que `sui-input` y `sui-select` `large`, que van al lado en los formularios de horario. Con `large`
100
+ el panel también crece: **filas de 44** y **Cancelar / Aplicar en `large` (48)**. En mobile
101
+ (≤ 767 px) las filas miden 44 con cualquier `size`.
102
+
103
+ | pieza | `small` · `medium` en web | `large` | mobile (cualquier size) | fuente |
104
+ |---|---|---|---|---|
105
+ | campo (trigger) | 36 · 40 | 48 | el del `size` | 36/40: medido · 48: canon (aprobado por el dueño, 03-10-2026), extensión SUI |
106
+ | fila de la columna | 36 | 44 | 44 | 36: Figma · 44: canon (aprobado por el dueño, 03-10-2026), extensión SUI |
107
+ | Cancelar / Aplicar | 36 | 48 | el del `size` | canon (aprobado por el dueño, 03-10-2026), extensión SUI |
108
+
109
+ | uso | mobile | web | fuente |
110
+ |---|---|---|---|
111
+ | horario en un formulario | `large` | `medium`, o `small` en filas densas | propuesta, falta aprobación |
112
+
113
+ ```html
114
+ <sui-time-picker label="Cierra" format="12h" [step]="5" [durationPresets]="[]"
115
+ [size]="bp.isMobile() ? 'large' : 'medium'"
116
+ [value]="cierre()" (valueChange)="cierre.set($event)" />
117
+ ```
118
+
119
+ ## «No usar»: por qué no hoy y cuándo sí
120
+
121
+ Nada se borra del DS. Lo marcado «no usar» no tiene caso en la app de hoy; si un flujo nuevo cae
122
+ en la columna de la derecha, se usa (aprobado por el dueño, 03-10-2026).
123
+
124
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
125
+ |---|---|---|
126
+ | `format="24h"` | en Perú se dice «9:30 PM» | una pantalla técnica del super admin (registro de eventos) donde se comparan horas |
127
+ | `step="1"` | 60 opciones sin necesidad | registrar la hora exacta de un hecho (hora real de entrega en un reclamo) |
128
+ | `durationPresets` (cualquiera) | se leen como hora desde la medianoche, no como duración | si el DS los cambia para sumar sobre la hora elegida («en 30 min») |
129
+ | `sui-time-picker-item` suelto | fila de 36 px, densa | una lista desplegable de horas en web |
130
+ | `sui-time-picker-duration` suelto | es parte de los presets | — |
131
+ | `sui-time-picker-status` | textos en inglés; el estado del repartidor ya tiene su badge | un panel de disponibilidad del equipo del local, con los textos en español |
132
+
133
+ ## API
134
+
135
+ <!-- api:inicio -->
136
+ <!-- generada: no editar a mano — `npm run docs` -->
137
+
138
+ ### `sui-time-picker`
139
+
140
+ | input | tipo | default |
141
+ |---|---|---|
142
+ | `label` | `string \| null` | `null` |
143
+ | `sublabel` | `string \| null` | `null` |
144
+ | `required` | `boolean` | `false` |
145
+ | `tooltip` | `string \| null` | `null` |
146
+ | `hint` | `string \| null` | `null` |
147
+ | `errorMessage` | `string \| null` | `null` |
148
+ | `error` | `boolean` | `false` |
149
+ | `placeholder` | `string` | `'Seleccionar hora'` |
150
+ | `disabled` | `boolean` | `false` |
151
+ | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` |
152
+ | `format` | `'12h' \| '24h'` | `'12h'` |
153
+ | `step` | `number` | `5` |
154
+ | `durationPresets` | `readonly SuiTimeDurationPreset[]` | `DEFAULT_DURATION_PRESETS` |
155
+ | `inline` | `boolean` | `false` |
156
+ | `showFooter` | `boolean` | `true` |
157
+ | `value` · model, `[(value)]` | `SuiTimeValue \| null` | `null` |
158
+
159
+ Outputs: ninguno.
160
+
161
+ ### `sui-time-picker-duration`
162
+
163
+ | input | tipo | default |
164
+ |---|---|---|
165
+ | `label` | `string` | — (requerido) |
166
+ | `active` | `boolean` | `false` |
167
+ | `disabled` | `boolean` | `false` |
168
+
169
+ | output | payload |
170
+ |---|---|
171
+ | `clicked` | `Event` |
172
+
173
+ ### `sui-time-picker-item`
174
+
175
+ | input | tipo | default |
176
+ |---|---|---|
177
+ | `time` | `string` | — (requerido) |
178
+ | `meridiem` | `string \| null` | `null` |
179
+ | `rightTime` | `string \| null` | `null` |
180
+ | `rightMeridiem` | `string \| null` | `null` |
181
+ | `direction` | `'right' \| 'center'` | `'right'` |
182
+ | `active` | `boolean` | `false` |
183
+ | `highlighted` | `boolean` | `false` |
184
+ | `disabled` | `boolean` | `false` |
185
+
186
+ | output | payload |
187
+ |---|---|
188
+ | `clicked` | `Event` |
189
+
190
+ ### `sui-time-picker-status`
191
+
192
+ | input | tipo | default |
193
+ |---|---|---|
194
+ | `type` | `'available' \| 'busy' \| 'in-meeting' \| 'offline'` | `'available'` |
195
+ | `label` | `string \| null` | `null` |
196
+ | `selected` | `boolean` | `false` |
197
+ | `disabled` | `boolean` | `false` |
198
+
199
+ | output | payload |
200
+ |---|---|
201
+ | `clicked` | `Event` |
202
+ <!-- api:fin -->
203
+
204
+ Es `ControlValueAccessor`; `writeValue` acepta también un string `"21:30"`.
205
+
206
+ ```ts
207
+ interface SuiTimeValue { hours: number; minutes: number; } // hours en 0–23 aunque el format sea 12h
208
+ interface SuiTimeDurationPreset { label: string; minutes: number; }
209
+ ```
210
+
211
+ ## Trampas
212
+
213
+ - **Los presets de duración no son duraciones**: «30 min» pone la hora en 00:30 (12:30 AM). Por eso
214
+ se pasan siempre `[durationPresets]="[]"`. Sin eso, el panel muestra cuatro pastillas que
215
+ rompen la hora.
216
+ - **`value.hours` va de 0 a 23** aunque se muestre en 12h: 9:30 PM es `{ hours: 21, minutes: 30 }`.
217
+ - **Al abrir sin valor arranca en 9:00 AM**, no vacío.
218
+ - **Cerrar con clic afuera o Escape descarta** lo elegido, igual que Cancelar.
219
+ - Con `step` que no contiene el minuto guardado (21:37 con step 5), la columna de minutos no
220
+ marca ninguno.
221
+ - Cancelar / Aplicar solo crecen con `size="large"`: con `medium` en mobile siguen en 36. En el
222
+ teléfono va `large`.
223
+ - El panel flota con el overlay del DS (como `sui-select`): se da vuelta si no entra abajo.
224
+ - `sui-time-picker-status` dice «Disponible», «Ocupado», «En reunión» y «Desconectado» por
225
+ defecto (antes venían en inglés del Figma); `label` sigue sirviendo para cambiarlo.
226
+ - El atributo `title` del host se borra solo (no sale el tooltip gris del navegador).
227
+
228
+ ## Ejemplo
229
+
230
+ ```html
231
+ <!-- Horario del local: abre / cierra -->
232
+ <sui-time-picker label="Abre" format="12h" [step]="5" [durationPresets]="[]"
233
+ [value]="apertura()" (valueChange)="apertura.set($event)" />
234
+ <sui-time-picker label="Cierra" format="12h" [step]="5" [durationPresets]="[]"
235
+ [error]="cierraAntes()" errorMessage="Cierra antes de abrir"
236
+ [value]="cierre()" (valueChange)="cierre.set($event)" />
237
+ ```