@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.
- package/docs/INDEX.md +51 -51
- package/docs/sui-alert/README.md +13 -0
- package/docs/sui-badge/README.md +17 -0
- package/docs/sui-banner/README.md +190 -0
- package/docs/sui-breadcrumb-item/README.md +96 -0
- package/docs/sui-breadcrumbs/README.md +122 -0
- package/docs/sui-button/README.md +17 -0
- package/docs/sui-card/README.md +168 -0
- package/docs/sui-chart-legend/README.md +140 -0
- package/docs/sui-chart-legend-dot/README.md +118 -0
- package/docs/sui-checkbox-card/README.md +150 -0
- package/docs/sui-compact-select/README.md +127 -0
- package/docs/sui-compact-select-for-input/README.md +117 -0
- package/docs/sui-content-card/README.md +147 -0
- package/docs/sui-content-label/README.md +148 -0
- package/docs/sui-counter-input/README.md +145 -0
- package/docs/sui-date-selector/README.md +98 -0
- package/docs/sui-datepicker/README.md +193 -0
- package/docs/sui-day-cell/README.md +118 -0
- package/docs/sui-day-label/README.md +81 -0
- package/docs/sui-drawer-footer/README.md +127 -0
- package/docs/sui-drawer-header/README.md +140 -0
- package/docs/sui-empty-state/README.md +145 -0
- package/docs/sui-file-upload/README.md +154 -0
- package/docs/sui-file-upload-card/README.md +129 -0
- package/docs/sui-image-upload/README.md +200 -0
- package/docs/sui-image-upload-profile/README.md +145 -0
- package/docs/sui-inline-input/README.md +104 -0
- package/docs/sui-inline-select/README.md +101 -0
- package/docs/sui-input/README.md +244 -0
- package/docs/sui-integration-switch/README.md +142 -0
- package/docs/sui-notification/README.md +179 -0
- package/docs/sui-notification-footer/README.md +98 -0
- package/docs/sui-notification-tab-menu/README.md +112 -0
- package/docs/sui-otp-input/README.md +116 -0
- package/docs/sui-pagination/README.md +146 -0
- package/docs/sui-password-strength/README.md +99 -0
- package/docs/sui-period-range/README.md +93 -0
- package/docs/sui-popover/README.md +178 -0
- package/docs/sui-product-card/README.md +173 -0
- package/docs/sui-radio-card/README.md +160 -0
- package/docs/sui-rating/README.md +7 -6
- package/docs/sui-rich-editor/README.md +114 -0
- package/docs/sui-select/README.md +180 -0
- package/docs/sui-stepper/README.md +166 -0
- package/docs/sui-store-card/README.md +147 -0
- package/docs/sui-switch-card/README.md +149 -0
- package/docs/sui-tab-chips/README.md +185 -0
- package/docs/sui-tab-menu-horizontal/README.md +142 -0
- package/docs/sui-tab-menu-vertical/README.md +136 -0
- package/docs/sui-tag-input/README.md +120 -0
- package/docs/sui-textarea/README.md +157 -0
- package/docs/sui-time-picker/README.md +237 -0
- package/docs/sui-toast/README.md +190 -0
- package/docs/sui-tooltip/README.md +201 -0
- package/fesm2022/ajuarezso-sui-design-system.mjs +273 -231
- package/fesm2022/ajuarezso-sui-design-system.mjs.map +1 -1
- package/package.json +1 -1
- 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
|
+
```
|