@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,99 @@
1
+ ---
2
+ name: sui-password-strength
3
+ selector: sui-password-strength
4
+ resumen: Barra de 3 niveles y lista de condiciones que acompaña al campo de una contraseña nueva
5
+ alternativas: [sui-input con hint (una sola regla, como «Mínimo 6 caracteres»)]
6
+ ---
7
+
8
+ # sui-password-strength
9
+
10
+ Story: [Molecules/Input/Password Strength](https://sui-design-system.web.app/?path=/docs/molecules-input-password-strength--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** debajo del campo de una **contraseña nueva** cuando hay **dos o más reglas** que cumplir:
15
+ la barra se llena (rojo, naranja, verde) y cada condición se marca a medida que se escribe.
16
+
17
+ **No lo uses** para:
18
+
19
+ - El login: la contraseña ya existe, no se evalúa.
20
+ - Una sola regla («Mínimo 6 caracteres»): alcanza el `hint` del `sui-input`, como hoy en
21
+ `pages/auth/register/register.html:121`.
22
+
23
+ Regla corta: **contraseña nueva con 2+ reglas → este · una regla → hint.**
24
+
25
+ ## Qué variante usar
26
+
27
+ > **En la guía del DS es un ejemplo de su página del input** («Password With Levels»: campo, barra
28
+ > de niveles y «Debe contener al menos» con tres condiciones). No dice cuándo usarlo.
29
+ >
30
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app`: **0 usos**. El registro pide solo 6
31
+ > caracteres (lo que exige Firebase).
32
+
33
+ | prop / evento | cuándo | fuente |
34
+ |---|---|---|
35
+ | `value` | La contraseña que se está escribiendo. | API |
36
+ | `conditions` | **Las reglas reales de la app.** Los defaults (mayúscula, número, 8 caracteres) no son las de Master Pedidos. `[]` deja solo la barra. | según la guía del DS (condiciones bajo la barra); qué reglas, propuesta |
37
+ | `caption` | «Debe contener al menos:» (default). `''` lo oculta. | según la guía del DS |
38
+ | `strength` | Forzar el nivel cuando lo calcula el servidor. Hoy sin caso. | API |
39
+ | `levelLabels` · `ariaLabel` | Lo que lee el lector de pantalla. Los defaults en español sirven. | API |
40
+ | `(strengthChange)` | Apagar «Crear cuenta» hasta llegar a `strong`. | propuesta, falta aprobación |
41
+
42
+ ## Tamaño en mobile y en web
43
+
44
+ No se toca: es texto (12 px) y una barra de 4 px a todo el ancho. **No necesita `bp`.**
45
+
46
+ ## «No usar»: por qué no hoy y cuándo sí
47
+
48
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
49
+ |---|---|---|
50
+ | el componente entero (0 usos) | el registro pide una sola regla (6 caracteres) | si las cuentas con dinero (local, super admin) pasan a exigir contraseña más fuerte |
51
+ | `strength` forzado | nadie calcula el nivel afuera | si el servidor rechaza contraseñas filtradas y devuelve el nivel |
52
+ | condiciones default | no son las reglas de la app | — (siempre pasar las propias) |
53
+
54
+ ## API
55
+
56
+ <!-- api:inicio -->
57
+ <!-- generada: no editar a mano — `npm run docs` -->
58
+
59
+ | input | tipo | default |
60
+ |---|---|---|
61
+ | `value` | `string` | `''` |
62
+ | `strength` | `'empty' \| 'weak' \| 'moderate' \| 'strong' \| null` | `null` |
63
+ | `caption` | `string` | `'Debe contener al menos:'` |
64
+ | `conditions` | `SuiPasswordStrengthCondition[]` | `SUI_PASSWORD_STRENGTH_DEFAULT_CONDITIONS` |
65
+ | `levelLabels` | `Partial<Record<SuiPasswordStrengthLevel, string>>` | `{}` |
66
+ | `ariaLabel` | `string` | `'Seguridad de la contraseña'` |
67
+
68
+ | output | payload |
69
+ |---|---|
70
+ | `strengthChange` | `'empty' \| 'weak' \| 'moderate' \| 'strong'` |
71
+ <!-- api:fin -->
72
+
73
+ Exporta `SUI_PASSWORD_STRENGTH_DEFAULT_CONDITIONS` y `SUI_PASSWORD_STRENGTH_DEFAULT_LEVEL_LABELS`.
74
+
75
+ ```ts
76
+ interface SuiPasswordStrengthCondition {
77
+ label: string;
78
+ test?: RegExp | ((value: string) => boolean);
79
+ met?: boolean; // si está, gana sobre test
80
+ }
81
+ ```
82
+
83
+ ## Trampas
84
+
85
+ - **Las condiciones default piden 8 caracteres, una mayúscula y un número**: más que lo que pide la
86
+ app (6). Sin `[conditions]` propias, la barra marca «débil» contraseñas que el registro acepta.
87
+ - Los segmentos tienen color fijo (1 rojo, 2 naranja, 3 verde): en «fuerte» la barra es de tres
88
+ colores, no toda verde.
89
+ - `strengthChange` no emite el nivel inicial.
90
+ - No incluye el campo: va debajo de un `sui-input type="password-toggle"`.
91
+
92
+ ## Ejemplo
93
+
94
+ ```html
95
+ <sui-input label="Contraseña" type="password-toggle" autocomplete="new-password" [(value)]="clave" />
96
+ <sui-password-strength [value]="clave()" [conditions]="reglas" />
97
+ <!-- reglas = [{ label: 'Al menos 8 caracteres', test: v => v.length >= 8 },
98
+ { label: 'Al menos 1 número', test: /\d/ }] -->
99
+ ```
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: sui-period-range
3
+ selector: sui-period-range
4
+ resumen: Un período predefinido (Hoy, Últimos 7 días) en la columna de presets del calendario de rango
5
+ alternativas: [sui-datepicker mode="range" (ya los trae), sui-tab-chips (períodos fijos a la vista: super admin, ganancias del repartidor), sui-select (períodos fijos en poco espacio: resumen del local)]
6
+ ---
7
+
8
+ # sui-period-range
9
+
10
+ Story: [Molecules/Date & Time/Period Range](https://sui-design-system.web.app/?path=/docs/molecules-date-time-period-range--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** solo dentro de un **selector de rango** armado a mano, como la columna de atajos al
15
+ costado del calendario.
16
+
17
+ **No lo uses** para:
18
+
19
+ - El rango normal: `sui-datepicker mode="range"` ya pinta los presets con este componente y los
20
+ cambia por `[presets]`.
21
+ - **Cambiar de período en un resumen** («Hoy · 7 días · 30 días»): `sui-tab-chips`, como hoy en
22
+ el super admin (`master-overview.html:13`) y en las ganancias del repartidor; o `sui-select`,
23
+ como el resumen del local (`overview.html:46`).
24
+
25
+ ## Qué variante usar
26
+
27
+ > **La guía del DS no tiene página para este componente** (vive dentro del datepicker). Todo es
28
+ > **propuesta, falta aprobación**.
29
+ >
30
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app`: **0 usos directos**. Lo usa por dentro
31
+ > `sui-datepicker`.
32
+
33
+ | input | valor | cuándo | fuente |
34
+ |---|---|---|---|
35
+ | `label` | texto | El período en palabras de la persona: «Hoy», «Últimos 7 días», «Este mes». Sin fechas: para eso está el calendario al lado. | propuesta |
36
+ | `active` | `true` | El preset que coincide con el rango elegido. Uno solo; si el rango se tocó a mano, ninguno. | propuesta |
37
+ | `disabled` | `true` | El período no tiene datos (un local que abrió hace 10 días con «Últimos 3 meses»). Mejor sacarlo de la lista si nunca va a tener datos. | propuesta |
38
+
39
+ Usos de hoy que rompen la regla: **0**.
40
+
41
+ ## Tamaño en mobile y en web
42
+
43
+ **No tiene `size`.** Mide 168 × 36: en mobile **no llega** a los 44 px. En el datepicker, en
44
+ mobile, los presets pasan a una fila que se desplaza de costado.
45
+
46
+ | uso | mobile | web | fuente |
47
+ |---|---|---|---|
48
+ | preset de un rango | 36 px de alto — **no llega** | 168 × 36 | medido |
49
+
50
+ ```html
51
+ <!-- Web: columna de presets -->
52
+ <sui-period-range label="Últimos 7 días" [active]="preset() === '7d'" (clicked)="aplicar('7d')" />
53
+ ```
54
+
55
+ **Sin tamaño suficiente para mobile.** En el celular, los períodos fijos van en `sui-tab-chips`,
56
+ como ya hacen el super admin y las ganancias del repartidor.
57
+
58
+ ## «No usar»: por qué no hoy y cuándo sí
59
+
60
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
61
+ |---|---|---|
62
+ | todo el componente suelto (0 usos) | los períodos de la app son fijos y van en tab-chips o select | una columna de atajos al lado de un calendario propio, en web |
63
+ | `disabled` | ningún período queda sin datos hoy | presets de un local nuevo («Últimos 12 meses» con 2 semanas de vida) |
64
+
65
+ ## API
66
+
67
+ <!-- api:inicio -->
68
+ <!-- generada: no editar a mano — `npm run docs` -->
69
+
70
+ | input | tipo | default |
71
+ |---|---|---|
72
+ | `label` | `string` | — (requerido) |
73
+ | `active` | `boolean` | `false` |
74
+ | `disabled` | `boolean` | `false` |
75
+
76
+ | output | payload |
77
+ |---|---|
78
+ | `clicked` | `void` |
79
+ <!-- api:fin -->
80
+
81
+ ## Trampas
82
+
83
+ - **No calcula fechas**: solo emite `clicked`. El rango lo resuelve quien lo usa (en el datepicker,
84
+ `DatepickerPreset.resolve`).
85
+ - Ancho fijo de 168 px.
86
+ - `active` no se apaga solo: lo maneja quien lo usa.
87
+
88
+ ## Ejemplo
89
+
90
+ ```html
91
+ <sui-period-range label="Hoy" [active]="true" />
92
+ <sui-period-range label="Últimos 30 días" (clicked)="aplicar('30d')" />
93
+ ```
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: sui-popover
3
+ selector: sui-popover
4
+ resumen: Panel con flecha que señala un elemento de la pantalla y explica algo nuevo, con una o dos acciones; sirve para recorridos de bienvenida
5
+ alternativas: [sui-tooltip (ayuda corta al pasar el mouse, sin acciones), sui-alert (aviso dentro del contenido), sui-modal (algo que pide decidir antes de seguir)]
6
+ ---
7
+
8
+ # sui-popover
9
+
10
+ Story: [Molecules/Popover](https://sui-design-system.web.app/?path=/docs/molecules-popover--docs)
11
+
12
+ Es **solo el panel**: no se abre solo, no se ancla al elemento ni se cierra al tocar afuera. Quien
13
+ lo usa decide cuándo mostrarlo y dónde ponerlo (un overlay del CDK, o posicionado a mano junto al
14
+ elemento). `position` solo mueve la flecha.
15
+
16
+ ## Cuándo usarlo / cuándo no
17
+
18
+ **Úsalo** para **señalar una parte de la pantalla** y decir algo que la persona todavía no sabe,
19
+ con un paso a seguir: el recorrido de bienvenida del panel del local la primera vez que entra, o
20
+ una función nueva que conviene mostrar donde está («Aquí programas tus pedidos»).
21
+
22
+ **No lo uses** para:
23
+
24
+ - La explicación de un ícono o un término sin acción: en web es `sui-tooltip`; en mobile, texto
25
+ visible o un `sui-modal-sheet`.
26
+ - Un aviso que no señala nada en particular: `sui-alert` o `sui-toast`.
27
+ - Pedir una decisión que bloquea («¿Cancelar el pedido?»): `sui-modal`.
28
+ - Un menú de opciones: `sui-dropdown-menu`.
29
+
30
+ ## Qué variante usar
31
+
32
+ > La guía del DS dice para qué es, pero no cuándo va cada pie ni cada posición. Lo marcado «según la
33
+ > guía del DS» sale de su texto; «canon» es lo ya aprobado en la app; el resto es **propuesta**.
34
+ >
35
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app`: **0 usos**, y ningún recorrido de
36
+ > bienvenida hecho a mano. Todo lo de abajo es para el primer flujo que lo necesite.
37
+
38
+ ### Lo que sí dice la guía del DS
39
+
40
+ - **Para qué es**: llamar la atención sobre **elementos específicos** de la interfaz.
41
+ - Se posiciona arriba, abajo o a los costados del elemento que señala.
42
+ - La guía no dice nada del pie con pasos; eso viene del Figma del DS (pie con puntos, con texto o
43
+ con dos botones a lo ancho).
44
+
45
+ ### `footer` — el pie
46
+
47
+ | footer | cuándo | fuente |
48
+ |---|---|---|
49
+ | `stretch` (default) | Un aviso suelto con dos salidas a lo ancho: «Ahora no» / «Probar». Con `doubleButton=false`, una sola: «Entendido». | propuesta |
50
+ | `stepper` | Un **recorrido de varios pasos**: los puntos dicen en cuál va y cuántos faltan. | propuesta |
51
+ | `none` | Solo informa y se cierra con la X: «Aquí ves lo que ganaste hoy». | propuesta |
52
+ | `text-stepper` | Recorridos de más de 5 pasos, donde los puntos ya no se cuentan: «Paso 3 de 7» (en español desde 03-10-2026). | propuesta |
53
+
54
+ ### Botones del pie
55
+
56
+ | prop | valor | cuándo | fuente |
57
+ |---|---|---|---|
58
+ | `doubleButton` | `true` (default) | Desde el segundo paso de un recorrido («Atrás» / «Siguiente»), o un aviso suelto con «Ahora no» / «Probar». | propuesta |
59
+ | `doubleButton` | `false` | El primer paso de un recorrido o un aviso suelto con una sola salida («Entendido»). | propuesta |
60
+ | `backLabel` · `nextLabel` | texto en español | Los defaults ya son «Atrás» y «Siguiente» (03-10-2026); el último paso pasa `nextLabel="Listo"`. | canon (copy de la app: tuteo, sentence case) |
61
+ | orden | secundario a la izquierda, primario a la derecha | Ya lo hace el componente (stroke neutral + filled primary), igual que `sui-modal-footer`. | canon (orden de botones de la app) |
62
+ | `step` | `{ current, total }` | Con `stepper`. Sin `step`, el componente asume **1 de 4**. | propuesta |
63
+
64
+ ### Flecha, ícono, texto y cierre
65
+
66
+ | prop | valor | cuándo | fuente |
67
+ |---|---|---|---|
68
+ | `tail` | `true` (default) | Siempre que señala algo: la flecha es lo que dice «es esto». | según la guía del DS (se posiciona junto al elemento) |
69
+ | `tail` | `false` | No usar hoy. **Cuándo sí:** la bienvenida inicial de un recorrido, centrada, sin elemento al que apuntar. | propuesta |
70
+ | `position` | `top-*` | El popover va **debajo** del elemento (la flecha arriba). Lo normal: el elemento sigue a la vista. | propuesta |
71
+ | `position` | `bottom-*` | El elemento está abajo (la barra de pestañas): el popover va encima. | propuesta |
72
+ | `position` | `left-*` · `right-*` | Elementos de una barra lateral (el sidebar del panel). | propuesta |
73
+ | `position` | `*-left` · `*-center` · `*-right` | Alinea la flecha con el elemento cuando el popover no puede centrarse sobre él (está cerca de un borde). | propuesta |
74
+ | `icon` | el de lo que se explica | Siempre uno que diga el tema (`calendar-schedule-line` para programar). El default es un ícono de persona. | propuesta |
75
+ | `icon` | `null` | Cuando el texto ya basta y el ícono sería decoración. | propuesta |
76
+ | `title` | frase corta | Qué es, en sentence case y sin punto final. | canon (copy de la app) |
77
+ | `description` | una o dos frases | Qué hace o por qué importa. | propuesta |
78
+ | `dismissIcon` | `true` (default) | Siempre: un recorrido se puede saltar. | propuesta |
79
+ | `dismissIcon` | `false` | No usar hoy. **Cuándo sí:** un paso que no se puede saltar porque sin él no funciona lo siguiente. | propuesta |
80
+ | `[data-slot=body]` | contenido propio | No usar hoy. **Cuándo sí:** el paso necesita una imagen o una lista en vez de título y texto. | propuesta |
81
+
82
+ ## Tamaño en mobile y en web
83
+
84
+ El panel mide **320 px** de ancho fijo: cabe en un teléfono de 360 px o más con 16 px de margen a
85
+ cada lado; en uno más angosto se sale. El `size` cambia **los botones del pie**:
86
+
87
+ | size | botones del pie | fuente |
88
+ |---|---|---|
89
+ | `small` (default) | 36 px (`sui-button` `small`) | Figma |
90
+ | `medium` | 40 px (`sui-button` `medium`) | canon (aprobado por el dueño, 03-10-2026): extensión SUI |
91
+
92
+ La X se ve de 24 y **se toca en 40 × 40** en los dos (un `::before` invisible sobre su botón,
93
+ extensión SUI 03-10-2026).
94
+
95
+ | uso | mobile | web | fuente |
96
+ |---|---|---|---|
97
+ | el panel | 320 px (cabe desde 352 px de pantalla) | 320 px | según el componente |
98
+ | botones del pie | `medium` (40) | `small` (36) | propuesta: el tamaño es extensión SUI aprobada (03-10-2026); el «cuándo» falta aprobarlo |
99
+ | la X | se ve de 24, se toca en 40 | igual | canon (aprobado por el dueño, 03-10-2026) |
100
+
101
+ ```html
102
+ <sui-popover footer="stretch" [size]="bp.isMobile() ? 'medium' : 'small'" … />
103
+ ```
104
+
105
+ En mobile, si el recorrido es la bienvenida de la app, conviene una pantalla o un
106
+ `sui-modal-sheet` por paso en vez del popover (propuesta, falta aprobación).
107
+
108
+ ## «No usar»: por qué no hoy y cuándo sí
109
+
110
+ Hoy el popover entero tiene **0 usos**; esto es lo que no se usaría aunque llegue un flujo nuevo.
111
+
112
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
113
+ |---|---|---|
114
+ | el componente | no hay recorridos ni funciones nuevas que señalar | el recorrido de bienvenida del panel del local; mostrar dónde está una función nueva |
115
+ | `footer="text-stepper"` | ningún recorrido largo hoy | recorridos de más de 5 pasos, donde los puntos ya no se cuentan («Paso 3 de 7») |
116
+ | `tail=false` | todo popover señala algo | la bienvenida inicial, centrada |
117
+ | `dismissIcon=false` | un recorrido siempre se puede saltar | un paso sin el que no funciona el siguiente |
118
+ | `[data-slot=body]` | título y texto alcanzan | un paso con imagen o lista |
119
+
120
+ ## API
121
+
122
+ <!-- api:inicio -->
123
+ <!-- generada: no editar a mano — `npm run docs` -->
124
+
125
+ | input | tipo | default |
126
+ |---|---|---|
127
+ | `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right' \| 'right-top' \| 'right-center' \| 'right-bottom' \| 'left-top' \| 'left-center' \| 'left-bottom'` | `'top-left'` |
128
+ | `tail` | `boolean` | `true` |
129
+ | `icon` | `string \| null` | `'user-6-line'` |
130
+ | `title` | `string` | `''` |
131
+ | `description` | `string \| null` | `null` |
132
+ | `dismissIcon` | `boolean` | `true` |
133
+ | `footer` | `'none' \| 'stretch' \| 'text-stepper' \| 'stepper'` | `'stretch'` |
134
+ | `backLabel` | `string` | `'Atrás'` |
135
+ | `nextLabel` | `string` | `'Siguiente'` |
136
+ | `doubleButton` | `boolean` | `true` |
137
+ | `step` | `{ current: number; total: number } \| null` | `null` |
138
+ | `size` | `'small' \| 'medium'` | `'small'` |
139
+
140
+ | output | payload |
141
+ |---|---|
142
+ | `dismissed` | `void` |
143
+ | `back` | `void` |
144
+ | `next` | `void` |
145
+ <!-- api:fin -->
146
+
147
+ Slot: `[data-slot=body]` reemplaza título y descripción.
148
+
149
+ ## Trampas
150
+
151
+ - **Textos por defecto en español** (desde 03-10-2026): «Atrás», «Siguiente» y, en `text-stepper`,
152
+ «Paso X de N» (este último no se puede cambiar).
153
+ - **El ícono por defecto es una persona** (`user-6-line`). Si no quieres ícono, `[icon]="null"`.
154
+ - **`step` vacío = 1 de 4**: con `stepper` o `text-stepper`, pásalo siempre.
155
+ - **No se posiciona solo**: `position` cambia dónde sale la flecha, no dónde aparece el panel.
156
+ Tampoco se cierra al tocar afuera ni con Escape; cierra quien escucha `(dismissed)`.
157
+ - `position` se lee «borde de la flecha, luego su lugar»: `top-left` es flecha **arriba a la
158
+ izquierda**, o sea, el popover queda **debajo** del elemento.
159
+ - El host lleva `role="dialog"` con `aria-label` = `title` y borra el `title` nativo.
160
+ - Los botones del pie no tienen `disabled`: si un paso no puede avanzar, se maneja desde `(next)`.
161
+ - **`stretch` reparte el ancho entre los dos botones** y en los steppers miden 80 px como mínimo
162
+ (crecen si el texto no entra). Hasta el 03-10-2026 no pasaba: el `.scss` apuntaba a
163
+ `.sui-popover__btn` y el template pone `.sui-popover__action`.
164
+
165
+ ## Ejemplo
166
+
167
+ ```html
168
+ <!-- Paso 2 de 3 de un recorrido -->
169
+ <sui-popover position="top-left" icon="calendar-schedule-line"
170
+ title="Programa tus pedidos" description="Elige el día y la hora, y el pedido entra solo a la cocina."
171
+ footer="stepper" [step]="{ current: 2, total: 3 }"
172
+ backLabel="Atrás" nextLabel="Siguiente"
173
+ (back)="anterior()" (next)="siguiente()" (dismissed)="saltar()" />
174
+
175
+ <!-- Aviso suelto con una sola salida -->
176
+ <sui-popover position="bottom-center" icon="wallet-3-line" title="Aquí ves lo que ganaste hoy"
177
+ [doubleButton]="false" nextLabel="Entendido" (next)="cerrar()" (dismissed)="cerrar()" />
178
+ ```
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: sui-product-card
3
+ selector: sui-product-card
4
+ resumen: Tarjeta de un producto de la carta con foto, nombre, descripción, precio y el botón para agregar (o editar)
5
+ alternativas: [sui-store-card (un local, no un producto), sui-card (cualquier otro contenido), sui-content-card (un producto en una fila, sin foto grande)]
6
+ ---
7
+
8
+ # sui-product-card
9
+
10
+ Story: [Molecules/Product Card](https://sui-design-system.web.app/?path=/docs/molecules-product-card--docs)
11
+
12
+ ## Cuándo usarlo / cuándo no
13
+
14
+ **Úsalo** para cada producto en una grilla de la carta: la carta del local que ve el cliente y la
15
+ grilla de productos que ve el dueño del local (con el lápiz).
16
+
17
+ **No lo uses** para:
18
+
19
+ - Un producto dentro del carrito, del checkout o del detalle de un pedido: ahí va una fila (sin
20
+ foto grande, con cantidad y subtotal).
21
+ - Un local: `sui-store-card`.
22
+ - Una promo o una campaña: tienen su propio carrusel.
23
+
24
+ Regla corta: **producto en la grilla de la carta → product-card · producto en una lista de pedido
25
+ → fila.**
26
+
27
+ ## Qué variante usar
28
+
29
+ > **La guía del DS no tiene página de la product card.** Es un componente propio de SUI, portado
30
+ > de la carta de la app. La card **dibuja, no decide**: el precio, si se vende ahora y la
31
+ > etiqueta de no disponible le llegan ya resueltos.
32
+ >
33
+ > Uso medido el 03-10-2026 en `master-pedidos/src/app` (misma exclusión que `sui-card`): **3
34
+ > product-cards en 2 archivos**: la carta del cliente
35
+ > (`pages/customer/restaurant-menu/restaurant-menu.html:324` y `:387`) y la grilla del local
36
+ > (`pages/products/products.html:145`, con `action="edit"`).
37
+
38
+ ### Las piezas
39
+
40
+ | prop | cuándo | fuente |
41
+ |---|---|---|
42
+ | `name`, `price` (obligatorios) | Siempre. El precio sale con 2 decimales y punto: «S/ 35.00». | propuesta |
43
+ | `description` | Una línea que diga qué trae: «Con hierbas aromáticas y papas nativas». | propuesta |
44
+ | `image` como objeto `{ url, fallbackUrl, position }` | Lo normal: `url` = la miniatura, `fallbackUrl` = la foto grande por si la miniatura no carga, `position` = encuadre vertical guardado. | propuesta (así lo hace la carta) |
45
+ | `image` como string | Solo la url, sin respaldo. | propuesta |
46
+ | `image = null` + `fallback` | Sin foto: banda de color con el nombre (default). La carta pasa `{ color }` para variar el color por visita con `SUI_PRODUCT_FALLBACK_COLORS`. | propuesta |
47
+ | `available=false` + `unavailableLabel` | Agotado o fuera de horario; la etiqueta dice cuál («Agotado», «Desde las 12 m.»). Se atenúa y pierde el «+». | propuesta |
48
+ | `quantity` | Cuántos hay en el carrito: con más de 0, el «+» pasa a stepper (− / cantidad / +). | propuesta |
49
+ | `closed` | El local no recibe pedidos: se ve, no se agrega. | propuesta |
50
+ | `action="add"` (default) | La carta del cliente. | propuesta |
51
+ | `action="edit"` | La grilla del dueño: lápiz siempre (también en agotado, que es justo el que quiere tocar), nunca stepper. | propuesta |
52
+ | `currencySymbol` | Default `S/`. No se cambia: la app cobra en soles. | propuesta |
53
+ | `fallback.variant="icon"` | Ver «No usar». | propuesta |
54
+
55
+ Outputs: `openDetail` (toque en la card), `add`, `increment`, `decrement`, `edit`.
56
+
57
+ **Usos de hoy que rompen las reglas:** 0. **Falta en el DS:** el precio tachado y la chapa de
58
+ descuento. La carta los dibuja afuera (`pages/customer/restaurant-menu/restaurant-menu.scss:426`,
59
+ deuda anotada): cuando se pueda publicar, va un input `precioLista`.
60
+
61
+ ## Tamaño en mobile y en web
62
+
63
+ El ancho lo da la grilla y la card llena su celda. El `size` cambia **los botones del pie**
64
+ («+», lápiz y stepper), en la escala del `sui-button`:
65
+
66
+ | size | «+» / lápiz | stepper (cada lado) | glyph | fuente |
67
+ |---|---|---|---|---|
68
+ | `small` (default) | 36 × 36 | 34 × 36 | 18 / 16 | lo que había en el DS |
69
+ | `medium` | 40 × 40 | 40 × 40 (pill de 104) | 20 | canon (aprobado por el dueño, 03-10-2026): extensión SUI |
70
+
71
+ En `medium` el stepper (104 px) no entra al lado del precio en la grilla de 2 columnas de mobile:
72
+ el pie envuelve y el stepper baja a su propia línea, a la derecha. El «+» y el lápiz sí siguen al
73
+ lado del precio.
74
+
75
+ | uso | mobile | web | fuente |
76
+ |---|---|---|---|
77
+ | grilla de la carta | 2 columnas (`1fr 1fr`, gap 12) | `repeat(auto-fill, minmax(220px, 1fr))` desde 600 px y `minmax(180px, 1fr)` en desktop | lo que hay en la app |
78
+ | botón «+», lápiz, stepper | `medium` (40) | `small` (36) | propuesta: el tamaño es extensión SUI aprobada (03-10-2026); el «cuándo» falta aprobarlo |
79
+
80
+ ```html
81
+ <sui-product-card [size]="bp.isMobile() ? 'medium' : 'small'" … />
82
+ ```
83
+
84
+ ## Precio con descuento
85
+
86
+ `precioLista` es el precio de referencia. Si es **mayor** que `price`, la card pinta el precio en
87
+ rojo con la chapa del ahorro al lado (`-25%`, calculada: no se pasa) y el de referencia tachado
88
+ debajo. Igual o menor, o `null`, se ignora. Rojo `--sui-red-700` (5.26:1 sobre blanco, AA) y en
89
+ oscuro `--sui-red-400`; no `--sui-state-error-base`, que da 3.66:1. Propuesta, falta aprobación
90
+ del dueño: reemplaza el bloque que la app dibuja afuera en `restaurant-menu.scss` (precio tachado
91
+ por `::after` y chapa por `::before` sobre `.product__price`), que se borra al migrar.
92
+
93
+ ## «No usar»: por qué no hoy y cuándo sí
94
+
95
+ | qué | por qué no hoy | cuándo sí, en un flujo nuevo |
96
+ |---|---|---|
97
+ | `fallback: { variant: 'icon' }` | la banda con el nombre dice más que un ícono | una carta de códigos (repuestos, ferretería) donde el nombre no ayuda a reconocerlo |
98
+ | `currencySymbol` distinto de `S/` | la app solo cobra en soles | si algún día se cobra en dólares |
99
+
100
+ ## API
101
+
102
+ <!-- api:inicio -->
103
+ <!-- generada: no editar a mano — `npm run docs` -->
104
+
105
+ | input | tipo | default |
106
+ |---|---|---|
107
+ | `name` | `string` | — (requerido) |
108
+ | `description` | `string` | `''` |
109
+ | `price` | `number` | — (requerido) |
110
+ | `image` | `SuiProductCardImage \| string \| null` | `null` |
111
+ | `available` | `boolean` | `true` |
112
+ | `quantity` | `number` | `0` |
113
+ | `closed` | `boolean` | `false` |
114
+ | `currencySymbol` | `string` | `'S/'` |
115
+ | `fallback` | `SuiProductCardFallback \| null` | `null` |
116
+ | `unavailableLabel` | `string` | `'Agotado'` |
117
+ | `action` | `'add' \| 'edit'` | `'add'` |
118
+ | `precioLista` | `number \| null` | `null` |
119
+ | `size` | `'small' \| 'medium'` | `'small'` |
120
+
121
+ | output | payload |
122
+ |---|---|
123
+ | `openDetail` | `void` |
124
+ | `add` | `void` |
125
+ | `increment` | `void` |
126
+ | `decrement` | `void` |
127
+ | `edit` | `void` |
128
+ <!-- api:fin -->
129
+
130
+ ```ts
131
+ interface SuiProductCardImage { url: string; fallbackUrl?: string; position?: number; }
132
+ interface SuiProductCardFallback { variant?: 'name' | 'icon'; color?: string | null; }
133
+ ```
134
+
135
+ `SUI_PRODUCT_FALLBACK_COLORS` (exportada): los colores `--sui-state-*-base` para la banda.
136
+
137
+ ## Trampas
138
+
139
+ - **El host lleva `content-visibility: auto`**: las cards fuera de pantalla no se pintan. Al
140
+ imprimir ya no pasa (el componente lo apaga en `@media print`). En una captura de página
141
+ completa (shot-scraper `--full`) sí pueden salir en blanco: el opt-out es
142
+ `sui-product-card { content-visibility: visible; }` desde afuera.
143
+ - **Necesita una celda con alto**: el host es `height: 100%` y la foto usa `NgOptimizedImage`
144
+ `fill`. En un contenedor sin alto la card colapsa; ponla en una grilla.
145
+ - **La card es un `<div>`, no un `<button>`** (desde 03-10-2026): lo que abre el detalle es
146
+ `.product__hit`, un botón invisible estirado sobre toda la card con el nombre como
147
+ `aria-label`; los botones del pie van por encima (`z-index` 2). Así no hay un botón dentro de
148
+ otro. Un estilo de afuera que apunte a `button.product` ya no engancha.
149
+ - **La app pinta hoy el descuento por fuera** con `::before`/`::after` sobre `.product__price`: si
150
+ se pasa `precioLista` hay que borrar ese bloque en el mismo cambio, o salen dos tachados.
151
+ - Con `quantity === 1` el «−» pasa a basurero («Quitar del carrito»).
152
+ - El color sin foto es determinista por nombre: el mismo producto siempre sale del mismo color,
153
+ salvo que pases `fallback.color`.
154
+ - Cada url nueva vuelve a mostrar el barrido de carga; si la miniatura falla, intenta con
155
+ `fallbackUrl` antes de apagarlo.
156
+
157
+ ## Ejemplo
158
+
159
+ ```html
160
+ <sui-product-card
161
+ [name]="p.name"
162
+ [description]="p.description ?? ''"
163
+ [price]="p.price"
164
+ [image]="p.thumbnailUrl ? { url: p.thumbnailUrl, fallbackUrl: p.imageUrl, position: p.imagePosition ?? 50 } : null"
165
+ [available]="disponibleAhora(p)"
166
+ [unavailableLabel]="etiquetaNoDisponible(p)"
167
+ [quantity]="cantidadEnCarrito(p)"
168
+ [closed]="noSePuedePedir()"
169
+ (openDetail)="abrirDetalle(p)"
170
+ (add)="agregar(p)"
171
+ (increment)="sumar(p)"
172
+ (decrement)="restar(p)" />
173
+ ```