@ajuarezso/sui-design-system 0.20.0 → 0.22.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 (85) hide show
  1. package/docs/INDEX.md +75 -80
  2. package/docs/sui-accordion/README.md +171 -0
  3. package/docs/sui-activity-feed/README.md +155 -0
  4. package/docs/sui-activity-filter/README.md +100 -0
  5. package/docs/sui-alert/README.md +13 -0
  6. package/docs/sui-badge/README.md +17 -0
  7. package/docs/sui-banner/README.md +190 -0
  8. package/docs/sui-breadcrumb-item/README.md +96 -0
  9. package/docs/sui-breadcrumbs/README.md +122 -0
  10. package/docs/sui-button/README.md +36 -0
  11. package/docs/sui-card/README.md +168 -0
  12. package/docs/sui-chart-legend/README.md +140 -0
  13. package/docs/sui-chart-legend-dot/README.md +118 -0
  14. package/docs/sui-checkbox-card/README.md +150 -0
  15. package/docs/sui-command-menu-footer/README.md +100 -0
  16. package/docs/sui-command-menu-item/README.md +140 -0
  17. package/docs/sui-command-menu-search/README.md +105 -0
  18. package/docs/sui-compact-select/README.md +130 -0
  19. package/docs/sui-compact-select-for-input/README.md +120 -0
  20. package/docs/sui-content-card/README.md +147 -0
  21. package/docs/sui-content-label/README.md +148 -0
  22. package/docs/sui-counter-input/README.md +148 -0
  23. package/docs/sui-date-selector/README.md +98 -0
  24. package/docs/sui-datepicker/README.md +197 -0
  25. package/docs/sui-day-cell/README.md +118 -0
  26. package/docs/sui-day-label/README.md +81 -0
  27. package/docs/sui-drawer-footer/README.md +127 -0
  28. package/docs/sui-drawer-header/README.md +140 -0
  29. package/docs/sui-dropdown-caption/README.md +92 -0
  30. package/docs/sui-dropdown-item/README.md +163 -0
  31. package/docs/sui-dropdown-menu/README.md +196 -0
  32. package/docs/sui-dropdown-search/README.md +113 -0
  33. package/docs/sui-empty-state/README.md +145 -0
  34. package/docs/sui-file-upload/README.md +154 -0
  35. package/docs/sui-file-upload-card/README.md +129 -0
  36. package/docs/sui-filter/README.md +181 -0
  37. package/docs/sui-href/README.md +85 -0
  38. package/docs/sui-image-upload/README.md +200 -0
  39. package/docs/sui-image-upload-profile/README.md +145 -0
  40. package/docs/sui-img-retry/README.md +107 -0
  41. package/docs/sui-inline-input/README.md +104 -0
  42. package/docs/sui-inline-select/README.md +101 -0
  43. package/docs/sui-input/README.md +247 -0
  44. package/docs/sui-integration-switch/README.md +142 -0
  45. package/docs/sui-modal/README.md +238 -0
  46. package/docs/sui-modal-footer/README.md +178 -0
  47. package/docs/sui-modal-header/README.md +115 -0
  48. package/docs/sui-modal-sheet/README.md +170 -0
  49. package/docs/sui-notification/README.md +179 -0
  50. package/docs/sui-notification-footer/README.md +98 -0
  51. package/docs/sui-notification-tab-menu/README.md +112 -0
  52. package/docs/sui-otp-input/README.md +116 -0
  53. package/docs/sui-page-header/README.md +123 -0
  54. package/docs/sui-pagination/README.md +146 -0
  55. package/docs/sui-password-strength/README.md +99 -0
  56. package/docs/sui-period-range/README.md +93 -0
  57. package/docs/sui-popover/README.md +178 -0
  58. package/docs/sui-product-card/README.md +173 -0
  59. package/docs/sui-radio-card/README.md +160 -0
  60. package/docs/sui-rating/README.md +7 -6
  61. package/docs/sui-rich-editor/README.md +114 -0
  62. package/docs/sui-segmented-control/README.md +3 -0
  63. package/docs/sui-select/README.md +183 -0
  64. package/docs/sui-sidebar/README.md +217 -0
  65. package/docs/sui-stepper/README.md +166 -0
  66. package/docs/sui-store-card/README.md +147 -0
  67. package/docs/sui-switch-card/README.md +149 -0
  68. package/docs/sui-switch-label/README.md +3 -0
  69. package/docs/sui-tab-chips/README.md +188 -0
  70. package/docs/sui-tab-menu-horizontal/README.md +142 -0
  71. package/docs/sui-tab-menu-vertical/README.md +136 -0
  72. package/docs/sui-table/README.md +264 -0
  73. package/docs/sui-tag-input/README.md +123 -0
  74. package/docs/sui-textarea/README.md +160 -0
  75. package/docs/sui-time-picker/README.md +240 -0
  76. package/docs/sui-toast/README.md +190 -0
  77. package/docs/sui-tooltip/README.md +201 -0
  78. package/docs/sui-topbar/README.md +153 -0
  79. package/docs/sui-topbar-button/README.md +95 -0
  80. package/docs/sui-topbar-item/README.md +97 -0
  81. package/docs/sui-topbar-user/README.md +83 -0
  82. package/fesm2022/ajuarezso-sui-design-system.mjs +291 -246
  83. package/fesm2022/ajuarezso-sui-design-system.mjs.map +1 -1
  84. package/package.json +1 -1
  85. package/types/ajuarezso-sui-design-system.d.ts +208 -50
@@ -0,0 +1,160 @@
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
+ En large y xlarge el texto sube a 16 px (regla del 03-10-2026): en esos dos tamaños va en 16/24
91
+ también en web, así que cada fila mide 24 en las dos plataformas.
92
+
93
+ | uso | mobile | web | fuente |
94
+ |---|---|---|---|
95
+ | 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 |
96
+ | descripción larga | igual, `rows=4` | `medium`, `rows=4` | propuesta, falta aprobación |
97
+
98
+ ```html
99
+ <sui-textarea [size]="bp.isMobile() ? 'large' : 'medium'" [rows]="3"
100
+ [resizable]="!bp.isMobile()" … /> <!-- bp = BreakpointsService -->
101
+ ```
102
+
103
+ ## «No usar»: por qué no hoy y cuándo sí
104
+
105
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
106
+ |---|---|---|
107
+ | `tooltip` | el hint alcanza | un campo del panel con una regla que no entra en una línea («qué puede ver el cliente») |
108
+ | `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 |
109
+ | `autocomplete` | un texto libre no se autocompleta | — |
110
+
111
+ ## API
112
+
113
+ <!-- api:inicio -->
114
+ <!-- generada: no editar a mano — `npm run docs` -->
115
+
116
+ | input | tipo | default |
117
+ |---|---|---|
118
+ | `rows` | `number` | `4` |
119
+ | `size` | `'xsmall' \| 'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` |
120
+ | `placeholder` | `string` | `''` |
121
+ | `disabled` | `boolean` | `false` |
122
+ | `readonly` | `boolean` | `false` |
123
+ | `error` | `boolean` | `false` |
124
+ | `errorMessage` | `string \| null` | `null` |
125
+ | `label` | `string \| null` | `null` |
126
+ | `hint` | `string \| null` | `null` |
127
+ | `autocomplete` | `string \| null` | `null` |
128
+ | `resizable` | `boolean` | `true` |
129
+ | `maxLength` | `number \| null` | `null` |
130
+ | `sublabel` | `string \| null` | `null` |
131
+ | `required` | `boolean` | `false` |
132
+ | `tooltip` | `string \| null` | `null` |
133
+ | `value` · model, `[(value)]` | `string` | `''` |
134
+
135
+ Outputs: ninguno.
136
+ <!-- api:fin -->
137
+
138
+ `value` es `model<string>`. Implementa `ControlValueAccessor` (`formControlName`, `[(ngModel)]`).
139
+
140
+ ## Trampas
141
+
142
+ - **`maxLength` con L mayúscula** (en `sui-input` es `maxlength`). Con el nombre equivocado Angular
143
+ no avisa en un atributo plano y el tope no se aplica.
144
+ - Sin `maxLength` **no hay contador**; la manija sale igual mientras `resizable` sea `true`.
145
+ - El tope también corta lo que se pega (no solo lo que se tipea).
146
+ - El hint lleva siempre el ícono `information-fill`, también el neutro (así está en el Figma).
147
+ - El `size` no cambia el alto: eso lo deciden las `rows`. Lo que alinea con el input es el padding.
148
+ - `error` sin `errorMessage` pinta el borde y no dice qué falta.
149
+
150
+ ## Ejemplo
151
+
152
+ ```html
153
+ <sui-textarea label="Nota para el repartidor" sublabel="(Opcional)" [rows]="3" [maxLength]="200"
154
+ [size]="bp.isMobile() ? 'large' : 'medium'" placeholder="Ej. frágil, tocar el timbre, entregar en recepción"
155
+ [resizable]="!bp.isMobile()" [(value)]="nota" />
156
+
157
+ <sui-textarea label="Motivo del rechazo" [required]="true" [rows]="3" [maxLength]="200"
158
+ hint="Lo va a leer el local" [error]="intentado() && !motivo()"
159
+ errorMessage="Escribe el motivo" [(value)]="motivo" />
160
+ ```
@@ -0,0 +1,240 @@
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
+ En large y xlarge el texto sube a 16 px (regla del 03-10-2026): con `large`, la hora del campo y la
110
+ de las filas van en 16/24; la fila sigue en 44 (el padding bajó de 12 a 10).
111
+
112
+ | uso | mobile | web | fuente |
113
+ |---|---|---|---|
114
+ | horario en un formulario | `large` | `medium`, o `small` en filas densas | propuesta, falta aprobación |
115
+
116
+ ```html
117
+ <sui-time-picker label="Cierra" format="12h" [step]="5" [durationPresets]="[]"
118
+ [size]="bp.isMobile() ? 'large' : 'medium'"
119
+ [value]="cierre()" (valueChange)="cierre.set($event)" />
120
+ ```
121
+
122
+ ## «No usar»: por qué no hoy y cuándo sí
123
+
124
+ Nada se borra del DS. Lo marcado «no usar» no tiene caso en la app de hoy; si un flujo nuevo cae
125
+ en la columna de la derecha, se usa (aprobado por el dueño, 03-10-2026).
126
+
127
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
128
+ |---|---|---|
129
+ | `format="24h"` | en Perú se dice «9:30 PM» | una pantalla técnica del super admin (registro de eventos) donde se comparan horas |
130
+ | `step="1"` | 60 opciones sin necesidad | registrar la hora exacta de un hecho (hora real de entrega en un reclamo) |
131
+ | `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») |
132
+ | `sui-time-picker-item` suelto | fila de 36 px, densa | una lista desplegable de horas en web |
133
+ | `sui-time-picker-duration` suelto | es parte de los presets | — |
134
+ | `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 |
135
+
136
+ ## API
137
+
138
+ <!-- api:inicio -->
139
+ <!-- generada: no editar a mano — `npm run docs` -->
140
+
141
+ ### `sui-time-picker`
142
+
143
+ | input | tipo | default |
144
+ |---|---|---|
145
+ | `label` | `string \| null` | `null` |
146
+ | `sublabel` | `string \| null` | `null` |
147
+ | `required` | `boolean` | `false` |
148
+ | `tooltip` | `string \| null` | `null` |
149
+ | `hint` | `string \| null` | `null` |
150
+ | `errorMessage` | `string \| null` | `null` |
151
+ | `error` | `boolean` | `false` |
152
+ | `placeholder` | `string` | `'Seleccionar hora'` |
153
+ | `disabled` | `boolean` | `false` |
154
+ | `size` | `'small' \| 'medium' \| 'large'` | `'medium'` |
155
+ | `format` | `'12h' \| '24h'` | `'12h'` |
156
+ | `step` | `number` | `5` |
157
+ | `durationPresets` | `readonly SuiTimeDurationPreset[]` | `DEFAULT_DURATION_PRESETS` |
158
+ | `inline` | `boolean` | `false` |
159
+ | `showFooter` | `boolean` | `true` |
160
+ | `value` · model, `[(value)]` | `SuiTimeValue \| null` | `null` |
161
+
162
+ Outputs: ninguno.
163
+
164
+ ### `sui-time-picker-duration`
165
+
166
+ | input | tipo | default |
167
+ |---|---|---|
168
+ | `label` | `string` | — (requerido) |
169
+ | `active` | `boolean` | `false` |
170
+ | `disabled` | `boolean` | `false` |
171
+
172
+ | output | payload |
173
+ |---|---|
174
+ | `clicked` | `Event` |
175
+
176
+ ### `sui-time-picker-item`
177
+
178
+ | input | tipo | default |
179
+ |---|---|---|
180
+ | `time` | `string` | — (requerido) |
181
+ | `meridiem` | `string \| null` | `null` |
182
+ | `rightTime` | `string \| null` | `null` |
183
+ | `rightMeridiem` | `string \| null` | `null` |
184
+ | `direction` | `'right' \| 'center'` | `'right'` |
185
+ | `active` | `boolean` | `false` |
186
+ | `highlighted` | `boolean` | `false` |
187
+ | `disabled` | `boolean` | `false` |
188
+
189
+ | output | payload |
190
+ |---|---|
191
+ | `clicked` | `Event` |
192
+
193
+ ### `sui-time-picker-status`
194
+
195
+ | input | tipo | default |
196
+ |---|---|---|
197
+ | `type` | `'available' \| 'busy' \| 'in-meeting' \| 'offline'` | `'available'` |
198
+ | `label` | `string \| null` | `null` |
199
+ | `selected` | `boolean` | `false` |
200
+ | `disabled` | `boolean` | `false` |
201
+
202
+ | output | payload |
203
+ |---|---|
204
+ | `clicked` | `Event` |
205
+ <!-- api:fin -->
206
+
207
+ Es `ControlValueAccessor`; `writeValue` acepta también un string `"21:30"`.
208
+
209
+ ```ts
210
+ interface SuiTimeValue { hours: number; minutes: number; } // hours en 0–23 aunque el format sea 12h
211
+ interface SuiTimeDurationPreset { label: string; minutes: number; }
212
+ ```
213
+
214
+ ## Trampas
215
+
216
+ - **Los presets de duración no son duraciones**: «30 min» pone la hora en 00:30 (12:30 AM). Por eso
217
+ se pasan siempre `[durationPresets]="[]"`. Sin eso, el panel muestra cuatro pastillas que
218
+ rompen la hora.
219
+ - **`value.hours` va de 0 a 23** aunque se muestre en 12h: 9:30 PM es `{ hours: 21, minutes: 30 }`.
220
+ - **Al abrir sin valor arranca en 9:00 AM**, no vacío.
221
+ - **Cerrar con clic afuera o Escape descarta** lo elegido, igual que Cancelar.
222
+ - Con `step` que no contiene el minuto guardado (21:37 con step 5), la columna de minutos no
223
+ marca ninguno.
224
+ - Cancelar / Aplicar solo crecen con `size="large"`: con `medium` en mobile siguen en 36. En el
225
+ teléfono va `large`.
226
+ - El panel flota con el overlay del DS (como `sui-select`): se da vuelta si no entra abajo.
227
+ - `sui-time-picker-status` dice «Disponible», «Ocupado», «En reunión» y «Desconectado» por
228
+ defecto (antes venían en inglés del Figma); `label` sigue sirviendo para cambiarlo.
229
+ - El atributo `title` del host se borra solo (no sale el tooltip gris del navegador).
230
+
231
+ ## Ejemplo
232
+
233
+ ```html
234
+ <!-- Horario del local: abre / cierra -->
235
+ <sui-time-picker label="Abre" format="12h" [step]="5" [durationPresets]="[]"
236
+ [value]="apertura()" (valueChange)="apertura.set($event)" />
237
+ <sui-time-picker label="Cierra" format="12h" [step]="5" [durationPresets]="[]"
238
+ [error]="cierraAntes()" errorMessage="Cierra antes de abrir"
239
+ [value]="cierre()" (valueChange)="cierre.set($event)" />
240
+ ```
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: sui-toast
3
+ selector: sui-toast-outlet
4
+ resumen: Aviso efímero que confirma el resultado de una acción y se va solo; se dispara con SuiToastService
5
+ alternativas: [sui-alert (aviso que se queda, en su contexto), sui-banner (toda la app), sui-notification (fila de la bandeja, con hora), sui-form-hint (error de un campo)]
6
+ ---
7
+
8
+ # sui-toast
9
+
10
+ Story: [Molecules/Feedback/Toast](https://sui-design-system.web.app/?path=/docs/molecules-feedback-toast--docs)
11
+
12
+ No hay una etiqueta `<sui-toast>`: el toast son **dos piezas**. `SuiToastService` (se inyecta y se
13
+ llama: `toast.success(…)`) y `<sui-toast-outlet />`, que se monta **una sola vez** en la raíz de
14
+ la app y dibuja la pila. Cada toast es, por dentro, un `sui-alert` `size="large"`. El apilado, la
15
+ salida y los tiempos copian a Sonner (la librería de toasts que usa la guía del DS).
16
+
17
+ ## Cuándo usarlo / cuándo no
18
+
19
+ **Úsalo** para el **resultado de algo que la persona acaba de hacer** («Cambios guardados», «No
20
+ se pudo eliminar el producto») y para un aviso que **llega mientras usa la app** y no necesita
21
+ quedarse (un push con la app abierta, «Nueva versión disponible»). Aparece, se lee y se va.
22
+
23
+ **No lo uses** para:
24
+
25
+ - Algo que tiene que seguir visible para que la persona actúe: un error que bloquea, un motivo
26
+ de rechazo, un «no puedes eliminar tu cuenta porque…». Eso es `sui-alert`, ahí mismo. Un toast
27
+ con `duration: Infinity` y sin acción es un alert mal puesto.
28
+ - Un **error al cargar** una pantalla: el toast se va y deja la pantalla vacía sin explicación.
29
+ Va un `sui-alert` `filled` o un `sui-empty-state` con «Reintentar».
30
+ - El error de un campo de formulario: `sui-form-hint` o el `errorMessage` del input.
31
+ - Algo de toda la app que dura (plan vencido): `sui-banner`.
32
+
33
+ Regla corta (canon, la misma del alert): **contexto → alert · toda la app → banner · efímero →
34
+ toast · bandeja → notification.**
35
+
36
+ ## Qué variante usar
37
+
38
+ > La guía del DS define el toast pero no dice cuándo va cada tipo. Lo marcado «según la guía del
39
+ > DS» sale de su texto o de su código publicado; «canon» es lo ya aprobado (README de `sui-alert`,
40
+ > `docs/CANON-SUI.md` o un pedido del dueño escrito en el código); el resto es **propuesta**.
41
+ >
42
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app` (sin prototipos, vitrinas ni tests):
43
+ > **~700 llamadas en 106 archivos**; de ellas, **560 con título fijo** (en 102 archivos) — 348 `error`,
44
+ > 179 `success`, 23 `info`, 10 `warning`, 0 `feature` — más 3 `show()`. El resto arma el título con variables.
45
+
46
+ ### Lo que sí dice la guía del DS
47
+
48
+ - **Para qué es**: feedback **breve** en tamaño compacto; varios se pueden **apilar**.
49
+ - **Una línea**: lo arma con un alert `small` (título solo, sin descripción) de 360 px de ancho.
50
+ SUI lo arma con un alert `large`, que sí admite descripción.
51
+ - **Defaults**: relleno `stroke`, posición `bottom-center`, cerrable. SUI arranca en
52
+ `stroke-dark` y `bottom-right`.
53
+ - **Ojo con la palabra «notification»**: en la guía, una notification es un aviso temporal en una
54
+ esquina con título, descripción y acción (un pago, una actualización). En SUI ese caso **también
55
+ es un toast** (`description` + `action`); `sui-notification` es otra cosa: la fila de la bandeja.
56
+
57
+ ### El tipo — qué método llamar
58
+
59
+ | método | cuándo | ejemplo real de la app | fuente |
60
+ |---|---|---|---|
61
+ | `success()` | La acción salió como se esperaba. Dice **qué** quedó hecho. | `pages/sales-channels/instagram/instagram.ts:102` («Cambios guardados») | propuesta (sigue la regla canon del alert) |
62
+ | `error()` | La acción **no** salió. Dice qué no se pudo, nunca «Error al…» ni el mensaje técnico. | `pages/driver/driver-profile/driver-profile.ts:516` («No se pudo guardar el vehículo») | canon (copy: `docs/CANON-SUI.md` §5.7) |
63
+ | `warning()` | Salió, **pero** con un pero que la persona tiene que saber. | `pages/super-admin/master-promos/master-promos.ts:171` («Push enviada, pero ningún cliente está dentro del radio») | propuesta |
64
+ | `info()` | Algo que **llega** y no es el resultado de lo que hiciste: un push con la app abierta, una versión nueva. | `core/services/aviso-unico.service.ts:52` · `core/services/update.service.ts:59` | propuesta |
65
+ | `feature()` | No usar hoy (0 usos). **Cuándo sí:** avisar una función nueva en el momento en que se activa («Ya puedes programar tus pedidos»). | — | propuesta |
66
+ | `show({ variant })` | Lo mismo que los de arriba, cuando hace falta pasar todo junto. | `pages/empresa/about/about.ts:103` | según el componente |
67
+
68
+ ### `style` — el relleno
69
+
70
+ | style | cuándo | fuente |
71
+ |---|---|---|
72
+ | `stroke-dark` (default SUI) | **Siempre.** Flota sobre cualquier pantalla y el oscuro contrasta con todas. | canon (README de `sui-alert`: «default del toast») |
73
+ | `stroke` (default de la guía) | No usar hoy: blanco sobre blanco se confunde con el contenido. **Cuándo sí:** una pantalla de fondo oscuro (una landing), donde el oscuro se pierde. | propuesta |
74
+ | `lighter` · `light` · `filled` | No usar hoy: un toast que pesa más que el oscuro compite con el botón principal, y si tiene que pesar más, no es un toast. **Cuándo sí:** ninguno previsto; ese caso es un alert o un modal. | propuesta |
75
+
76
+ ### Tiempo, cierre, acción y texto
77
+
78
+ | opción | valor | cuándo | fuente |
79
+ |---|---|---|---|
80
+ | `duration` | `4000` (default) | Un título corto. | según el componente (valor de Sonner) |
81
+ | `duration` | `6000` | Título más `description`, o un título largo que no se alcanza a leer en 4 s. | propuesta |
82
+ | `duration` | `60_000` | Los avisos de un pedido que llegan con la app abierta: tiempo de verlos sin quedarse tapando. | canon (pedido del dueño, 28-09-2026, en `aviso-unico.service.ts`) |
83
+ | `duration` | `Infinity` | **Solo con `action`**: hay algo que hacer y no se puede perder («Actualizar»). Sin acción, es un alert. | propuesta |
84
+ | `dismissible` | `true` (default) | Siempre: la X permite quitarlo antes. La guía lo trae cerrable por defecto. | según la guía del DS |
85
+ | `dismissible` | `false` | No usar hoy. **Cuándo sí:** un toast de progreso que se reemplaza solo usando el mismo `id` («Subiendo foto…» → «Foto subida»). | propuesta (la guía muestra un ejemplo no cerrable) |
86
+ | `action` | `{ label, onClick, keepOpen? }` | **Una** acción corta que resuelve ahí mismo: «Actualizar», «Deshacer», «Reintentar». Se dibuja como link subrayado. | propuesta |
87
+ | `title` | frase corta | Qué pasó, en sentence case, **sin punto final**. Una idea. | canon (copy de la app) |
88
+ | `description` | segunda frase | Si hace falta una segunda frase, va aquí y no pegada al título con un punto. | propuesta |
89
+ | `position` | `bottom-right` (default) | Siempre. En mobile toda posición pasa a ancho completo; las `bottom-*` quedan abajo, por encima de la barra inferior. | propuesta |
90
+ | `position` | `top-*` | No usar hoy. **Cuándo sí:** una pantalla con una barra de acción fija abajo que el toast taparía. | propuesta |
91
+ | `position` | `bottom-left` · `bottom-center` | No usar hoy: dos esquinas abren dos pilas. **Cuándo sí:** si se decide cambiar la esquina de toda la app, con `setDefaultPosition()`, una sola vez. | propuesta |
92
+ | `id` | texto | Para reemplazar un toast en curso o no repetir el mismo aviso. | según el componente |
93
+
94
+ ### Usos de hoy que rompen la regla
95
+
96
+ - **Rompen el canon — 3 toasts de error que no se van y no tienen acción → `sui-alert`** en el
97
+ modal de eliminar cuenta: `pages/empresa/about/about.ts:103`, `pages/repartidor/about/about.ts:97`,
98
+ `pages/customer/about/about.ts:135` («No puedes eliminar tu cuenta» + el motivo, `duration: Infinity`).
99
+ - **Rompen el canon de copy — 104 errores que empiezan con «Error…»** («Error al cargar productos»)
100
+ → «No se pudo…»: `pages/products/products.ts:415`, `pages/customers/customers.ts:244`,
101
+ `pages/settings/team/team.ts:315`. Y **26** que concatenan `e.message` al texto (puede salir
102
+ texto técnico o en inglés).
103
+ - **Rompen el canon de copy — 5 títulos en minúscula:** `pages/driver/driver-profile/driver-profile.ts:538`
104
+ («ubicación activada»), `:552`, `:597`, `:614`, `pages/driver/driver-order-detail/driver-order-detail.ts:438`.
105
+ - **Romperían la propuesta — 47 errores al cargar como toast** → alert o empty state con
106
+ «Reintentar»: `pages/products/products.ts:415`, `pages/driver/driver-history/driver-history.ts:198`.
107
+ - **Romperían la propuesta — 126 títulos con punto final**, casi todos dos frases en una →
108
+ título + `description`: `components/map-picker-modal/map-picker-modal.ts:260` («Permiso denegado.
109
+ Usa la barra de búsqueda…»), `core/services/compartir-viaje.service.ts:78`. 62 pasan de 60
110
+ caracteres.
111
+
112
+ ## Tamaño en mobile y en web
113
+
114
+ El toast **no tiene `size`**: siempre es un alert `large`.
115
+
116
+ | uso | mobile (≤ 767 px) | web | fuente |
117
+ |---|---|---|---|
118
+ | ancho | todo el ancho, con margen lateral | hasta 360 px, en la esquina | según el componente |
119
+ | dónde | abajo, por encima de la barra inferior (nativa en iOS) | abajo a la derecha | según el componente |
120
+ | la X de cerrar | se ve de 20 px; **se toca en 40 × 40** | igual | canon (aprobado por el dueño, 03-10-2026): área táctil invisible, extensión SUI |
121
+ | la acción | link `medium` (20 px de alto) | link `medium` | según el componente |
122
+
123
+ No hay patrón `bp.isMobile()`: el outlet ya cambia solo por breakpoint. Lo que sí cambia por
124
+ plataforma es el largo del texto: en mobile el título envuelve antes, así que una sola idea por toast.
125
+
126
+ ## «No usar»: por qué no hoy y cuándo sí
127
+
128
+ Nada se borra del DS. Si un flujo nuevo cae en la columna de la derecha, se usa.
129
+
130
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
131
+ |---|---|---|
132
+ | `feature()` | no hay novedades que se anuncien al activarlas | «Ya puedes programar tus pedidos», justo cuando se activa |
133
+ | `style` `stroke` | blanco sobre blanco | una pantalla de fondo oscuro (landing) |
134
+ | `style` `lighter`, `light`, `filled` | un toast que pesa más ya no es un toast | ninguno previsto: es un alert o un modal |
135
+ | `dismissible: false` | todo toast se puede quitar | un toast de progreso que se reemplaza con el mismo `id` |
136
+ | `position` distinta de la default | dos pilas en dos esquinas | una pantalla con barra fija abajo (`top-*`), o mover toda la app con `setDefaultPosition()` |
137
+ | `duration: Infinity` sin `action` | no se va y no hay nada que hacer | ninguno: eso es un alert |
138
+
139
+ ## API
140
+
141
+ <!-- api:inicio -->
142
+ <!-- generada: no editar a mano — `npm run docs` -->
143
+
144
+ Inputs: ninguno.
145
+
146
+ Outputs: ninguno.
147
+ <!-- api:fin -->
148
+
149
+ ## Trampas
150
+
151
+ - **La X se toca en 40 × 40** aunque se vea de 20 (un `::before` invisible sobre su `<button>`,
152
+ desde 03-10-2026). Solo llega a la app cuando deje de usar su copia local del toast (ver abajo).
153
+ - **La app no usa el servicio del paquete.** `master-pedidos` tiene una copia local en
154
+ `src/app/components/sui/sui-toast/` y el outlet montado en `app.ts:66` es esa copia. La API es la
155
+ misma; la diferencia es que la copia lee la altura de la barra nativa desde `LiquidGlassService`,
156
+ y la del paquete desde el token `SUI_TAB_BAR`. Si inyectas `SuiToastService` del paquete en la
157
+ app, **no aparece nada**: su outlet no está montado. Migrar = importar del paquete y proveer
158
+ `SUI_TAB_BAR`.
159
+ - **El comentario de `style` miente:** dice `// default 'stroke'`, pero el código pone `'stroke-dark'`.
160
+ - **Sin outlet no hay toasts:** el temporizador de cierre vive en el outlet, no en el servicio. Si el
161
+ outlet no está montado, los toasts se acumulan en el signal y nunca se ven ni se van.
162
+ - `dismissible` arranca en `true` aquí y en `false` en `sui-alert` y `sui-banner`.
163
+ - La acción cierra el toast al tocarla, salvo `keepOpen: true`.
164
+ - Al pasar el dedo o el mouse por la pila se abre en abanico y se queda abierta 1,2 s después de
165
+ cerrar uno, para seguir tocando (patrón Sonner).
166
+ - La pila lleva `aria-live="polite"`: el lector de pantalla anuncia cada toast.
167
+ - El título es texto plano; la `description` del alert interno va por `innerHTML`: no le pases
168
+ texto escrito por el usuario sin sanear.
169
+
170
+ ## Ejemplo
171
+
172
+ ```ts
173
+ private readonly toast = inject(SuiToastService);
174
+
175
+ guardar() {
176
+ this.toast.success('Cambios guardados');
177
+ }
178
+
179
+ fallo() {
180
+ this.toast.error('No se pudo eliminar el producto', { description: 'Revisa tu conexión y vuelve a intentar.', duration: 6000 });
181
+ }
182
+
183
+ versionNueva(v: string) {
184
+ this.toast.info('Nueva versión disponible', {
185
+ description: `Actualiza para usar la versión ${v}.`,
186
+ duration: Infinity,
187
+ action: { label: 'Actualizar', onClick: () => location.reload() },
188
+ });
189
+ }
190
+ ```