@lit-roa/resumen-pedido 0.0.0-stage → 0.1.1

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/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # @lit-roa/resumen-pedido
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Resumen de pedido: subtotal, descuento, envío (o "Gratis", o "Se calcula en
4
+ el siguiente paso"), cargos extra y total, con campo de cupón opcional.
5
+ No calcula precios: los montos vienen del sitio o de la API. El total se
6
+ calcula solo si no lo indicas.
7
+
8
+ ```bash
9
+ npm install @lit-roa/resumen-pedido
10
+ ```
11
+
12
+ ```html
13
+ <lr-resumen-pedido subtotal="527" piezas="3" envio="0" con-cupon>
14
+ <lr-boton slot="acciones" bloque>Ir a pagar</lr-boton>
15
+ <span slot="nota">Precios con IVA incluido.</span>
16
+ </lr-resumen-pedido>
17
+ ```
18
+
19
+ ## Cupones
20
+
21
+ ```js
22
+ resumen.addEventListener('aplicar-cupon', async (e) => {
23
+ resumen.cuponCargando = true;
24
+ const r = await api.validarCupon(e.detail.codigo);
25
+ resumen.cuponCargando = false;
26
+ if (r.ok) {
27
+ resumen.cuponAplicado = e.detail.codigo;
28
+ resumen.descuento = r.descuento;
29
+ } else {
30
+ resumen.errorCupon = 'Ese código no existe o ya venció.';
31
+ }
32
+ });
33
+ resumen.addEventListener('quitar-cupon', () => { resumen.cuponAplicado = ''; resumen.descuento = 0; });
34
+ ```
35
+
36
+ ## Propiedades
37
+
38
+ | Atributo | Tipo | Por omisión |
39
+ | --- | --- | --- |
40
+ | `titulo` | `string` (vacío para ocultarlo) | `'Resumen del pedido'` |
41
+ | `subtotal` | `number` | `0` |
42
+ | `piezas` | `number` (muestra "Subtotal (3 productos)") | — |
43
+ | `envio` | `number` (`0` = Gratis; sin valor = pendiente) | — |
44
+ | `descuento` | `number` positivo | `0` |
45
+ | `cargos` | `{ concepto, monto }[]` (JSON en atributo) | `[]` |
46
+ | `total` | `number` | calculado |
47
+ | `moneda` / `mostrar-moneda` | `string` / `boolean` | `'MXN'` / `false` |
48
+ | `con-cupon` | `boolean` | `false` |
49
+ | `cupon-aplicado` / `error-cupon` | `string` | `''` |
50
+ | `cupon-cargando` | `boolean` | `false` |
51
+ | `sin-borde` | `boolean` (sin caja, para meterlo en otro panel) | `false` |
52
+
53
+ ## Eventos
54
+
55
+ | Evento | `detail` |
56
+ | --- | --- |
57
+ | `aplicar-cupon` | `{ codigo }` (en mayúsculas) |
58
+ | `quitar-cupon` | `{ codigo }` |
59
+
60
+ Slots: por omisión (arriba de los montos), `acciones` y `nota`.
61
+ Estilos: `--lr-resumen-fondo`, `--lr-resumen-relleno`, `--lr-color-exito`, `::part(resumen)`.
62
+
63
+ ## Desarrollo
64
+
65
+ ```bash
66
+ npm run dev -w @lit-roa/resumen-pedido
67
+ ```
@@ -0,0 +1,2 @@
1
+ export { LrResumenPedido, type CargoExtra, type DetalleCupon } from './resumen-pedido.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { LrResumenPedido } from './resumen-pedido.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAsC,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { LitElement } from 'lit';
2
+ export interface DetalleCupon {
3
+ codigo: string;
4
+ }
5
+ export interface CargoExtra {
6
+ concepto: string;
7
+ monto: number;
8
+ }
9
+ /**
10
+ * Resumen de pedido: subtotal, envío, descuento, cargos extra y total, con
11
+ * un campo opcional para cupones. No calcula precios por su cuenta salvo el
12
+ * total si no se lo das: el sitio (o la API) manda los montos.
13
+ *
14
+ * @fires aplicar-cupon - `detail`: `{ codigo }`. Mientras validas, pon `cupon-cargando`.
15
+ * @fires quitar-cupon - `detail`: `{ codigo }`.
16
+ * @slot - Arriba de los montos (ej. líneas del carrito en modo solo lectura).
17
+ * @slot acciones - Debajo del total (ej. botón "Pagar").
18
+ * @slot nota - Texto pequeño al final (ej. "Impuestos incluidos").
19
+ */
20
+ export declare class LrResumenPedido extends LitElement {
21
+ static styles: import("lit").CSSResult[];
22
+ titulo: string;
23
+ subtotal: number;
24
+ /** Costo de envío. `0` = "Gratis"; sin valor = "Se calcula en el siguiente paso". */
25
+ envio?: number;
26
+ /** Descuento como número positivo; se muestra restando. */
27
+ descuento: number;
28
+ /** Cargos extra (ej. impuestos, propina). Como atributo, en JSON. */
29
+ cargos: CargoExtra[];
30
+ /** Total. Si no se indica: subtotal + envío + cargos − descuento. */
31
+ total?: number;
32
+ moneda: string;
33
+ /** Muestra el código de moneda junto al total. */
34
+ mostrarMoneda: boolean;
35
+ /** Muestra el campo de cupón. */
36
+ conCupon: boolean;
37
+ /** Código ya aplicado: se muestra como etiqueta con botón para quitarlo. */
38
+ cuponAplicado: string;
39
+ errorCupon: string;
40
+ cuponCargando: boolean;
41
+ piezas?: number;
42
+ private codigo;
43
+ private conAcciones;
44
+ render(): import("lit-html").TemplateResult<1>;
45
+ private renderCupon;
46
+ private aplicarCupon;
47
+ private quitarCupon;
48
+ }
49
+ declare global {
50
+ interface HTMLElementTagNameMap {
51
+ 'lr-resumen-pedido': LrResumenPedido;
52
+ }
53
+ }
54
+ //# sourceMappingURL=resumen-pedido.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumen-pedido.d.ts","sourceRoot":"","sources":["../src/resumen-pedido.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAIrD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,qBACa,eAAgB,SAAQ,UAAU;IAC7C,OAAgB,MAAM,4BAmJpB;IAEU,MAAM,SAAwB;IAEd,QAAQ,SAAK;IAEzC,qFAAqF;IACzD,KAAK,CAAC,EAAE,MAAM,CAAC;IAE3C,2DAA2D;IAC/B,SAAS,SAAK;IAE1C,qEAAqE;IAC1C,MAAM,EAAE,UAAU,EAAE,CAAM;IAErD,qEAAqE;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IAE/B,MAAM,SAAS;IAE3B,kDAAkD;IACQ,aAAa,UAAS;IAEhF,iCAAiC;IACoB,QAAQ,UAAS;IAEtE,4EAA4E;IACjC,aAAa,SAAM;IAEtB,UAAU,SAAM;IAEE,aAAa,UAAS;IAE/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAExD,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,WAAW,CAAS;IAE5B,MAAM;IAuCf,OAAO,CAAC,WAAW;IAqBnB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,WAAW;CAKpB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,mBAAmB,EAAE,eAAe,CAAC;KACtC;CACF"}
@@ -0,0 +1,309 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html, nothing } from 'lit';
8
+ import { customElement, property, state } from 'lit/decorators.js';
9
+ import { emitir, estilosBase, formatearPrecio, iconoTag, iconoX, tokens } from '@lit-roa/base';
10
+ /**
11
+ * Resumen de pedido: subtotal, envío, descuento, cargos extra y total, con
12
+ * un campo opcional para cupones. No calcula precios por su cuenta salvo el
13
+ * total si no se lo das: el sitio (o la API) manda los montos.
14
+ *
15
+ * @fires aplicar-cupon - `detail`: `{ codigo }`. Mientras validas, pon `cupon-cargando`.
16
+ * @fires quitar-cupon - `detail`: `{ codigo }`.
17
+ * @slot - Arriba de los montos (ej. líneas del carrito en modo solo lectura).
18
+ * @slot acciones - Debajo del total (ej. botón "Pagar").
19
+ * @slot nota - Texto pequeño al final (ej. "Impuestos incluidos").
20
+ */
21
+ let LrResumenPedido = class LrResumenPedido extends LitElement {
22
+ constructor() {
23
+ super(...arguments);
24
+ this.titulo = 'Resumen del pedido';
25
+ this.subtotal = 0;
26
+ /** Descuento como número positivo; se muestra restando. */
27
+ this.descuento = 0;
28
+ /** Cargos extra (ej. impuestos, propina). Como atributo, en JSON. */
29
+ this.cargos = [];
30
+ this.moneda = 'MXN';
31
+ /** Muestra el código de moneda junto al total. */
32
+ this.mostrarMoneda = false;
33
+ /** Muestra el campo de cupón. */
34
+ this.conCupon = false;
35
+ /** Código ya aplicado: se muestra como etiqueta con botón para quitarlo. */
36
+ this.cuponAplicado = '';
37
+ this.errorCupon = '';
38
+ this.cuponCargando = false;
39
+ this.codigo = '';
40
+ this.conAcciones = false;
41
+ }
42
+ static { this.styles = [
43
+ tokens,
44
+ estilosBase,
45
+ css `
46
+ :host {
47
+ display: block;
48
+ }
49
+ .resumen {
50
+ padding: var(--lr-resumen-relleno, 1.25rem);
51
+ border: 1px solid var(--_color-borde);
52
+ border-radius: var(--_radio);
53
+ background: var(--lr-resumen-fondo, var(--_color-fondo));
54
+ }
55
+ :host([sin-borde]) .resumen {
56
+ padding: 0;
57
+ border: 0;
58
+ background: transparent;
59
+ }
60
+ h2 {
61
+ margin: 0 0 1rem;
62
+ font-size: 1.125rem;
63
+ }
64
+ dl {
65
+ display: grid;
66
+ grid-template-columns: 1fr auto;
67
+ row-gap: 0.625rem;
68
+ margin: 0;
69
+ font-size: 0.9375rem;
70
+ }
71
+ dt {
72
+ color: var(--_color-texto-suave);
73
+ }
74
+ dd {
75
+ margin: 0;
76
+ padding-left: 1rem;
77
+ text-align: right;
78
+ font-variant-numeric: tabular-nums;
79
+ }
80
+ .descuento {
81
+ color: var(--lr-color-exito, #15803d);
82
+ }
83
+ .gratis {
84
+ color: var(--lr-color-exito, #15803d);
85
+ font-weight: 600;
86
+ }
87
+ .pendiente {
88
+ color: var(--_color-texto-suave);
89
+ font-size: 0.8125rem;
90
+ }
91
+ .total {
92
+ padding-top: 0.875rem;
93
+ border-top: 1px solid var(--_color-borde);
94
+ font-size: 1.125rem;
95
+ font-weight: 700;
96
+ color: var(--_color-texto);
97
+ }
98
+ .moneda {
99
+ color: var(--_color-texto-suave);
100
+ font-size: 0.75rem;
101
+ font-weight: 500;
102
+ margin-right: 0.25rem;
103
+ }
104
+ form {
105
+ display: flex;
106
+ gap: 0.5rem;
107
+ margin: 1rem 0;
108
+ }
109
+ input {
110
+ flex: 1;
111
+ min-width: 0;
112
+ height: 2.5rem;
113
+ padding: 0 0.75rem;
114
+ border: 1.5px solid #d1d5db;
115
+ border-radius: calc(var(--_radio) / 1.5);
116
+ font: inherit;
117
+ text-transform: uppercase;
118
+ }
119
+ input:focus {
120
+ outline: 0;
121
+ border-color: var(--_color-primario);
122
+ }
123
+ input[aria-invalid='true'] {
124
+ border-color: var(--lr-color-peligro, #dc2626);
125
+ }
126
+ form button {
127
+ all: unset;
128
+ padding: 0 1rem;
129
+ border-radius: calc(var(--_radio) / 1.5);
130
+ border: 1.5px solid var(--_color-primario);
131
+ color: var(--_color-primario);
132
+ font-weight: 600;
133
+ cursor: pointer;
134
+ }
135
+ form button:disabled {
136
+ opacity: 0.5;
137
+ cursor: wait;
138
+ }
139
+ form button:focus-visible,
140
+ .chip button:focus-visible {
141
+ outline: 2px solid var(--_color-primario);
142
+ outline-offset: 2px;
143
+ }
144
+ .error-cupon {
145
+ margin: -0.5rem 0 1rem;
146
+ color: var(--lr-color-peligro, #dc2626);
147
+ font-size: 0.8125rem;
148
+ }
149
+ .chip {
150
+ display: inline-flex;
151
+ align-items: center;
152
+ gap: 0.375rem;
153
+ margin: 1rem 0;
154
+ padding: 0.25rem 0.375rem 0.25rem 0.625rem;
155
+ border-radius: 999px;
156
+ background: #dcfce7;
157
+ color: #166534;
158
+ font-size: 0.8125rem;
159
+ font-weight: 700;
160
+ }
161
+ .chip button {
162
+ all: unset;
163
+ display: grid;
164
+ place-items: center;
165
+ width: 1.25rem;
166
+ height: 1.25rem;
167
+ border-radius: 999px;
168
+ cursor: pointer;
169
+ }
170
+ .chip button:hover {
171
+ background: rgb(0 0 0 / 0.08);
172
+ }
173
+ .acciones {
174
+ display: grid;
175
+ gap: 0.5rem;
176
+ margin-top: 1.25rem;
177
+ }
178
+ .acciones[hidden] {
179
+ display: none;
180
+ }
181
+ ::slotted([slot='nota']) {
182
+ display: block;
183
+ margin-top: 0.75rem;
184
+ color: var(--_color-texto-suave);
185
+ font-size: 0.75rem;
186
+ text-align: center;
187
+ }
188
+ `,
189
+ ]; }
190
+ render() {
191
+ const extra = this.cargos.reduce((s, c) => s + c.monto, 0);
192
+ const total = this.total ?? this.subtotal + (this.envio ?? 0) + extra - this.descuento;
193
+ const p = (n) => formatearPrecio(n, this.moneda);
194
+ const etiquetaSubtotal = this.piezas !== undefined ? `Subtotal (${this.piezas} ${this.piezas === 1 ? 'producto' : 'productos'})` : 'Subtotal';
195
+ return html `<section class="resumen" part="resumen" aria-label=${this.titulo}>
196
+ ${this.titulo ? html `<h2>${this.titulo}</h2>` : nothing}
197
+ <slot></slot>
198
+ ${this.conCupon ? this.renderCupon() : nothing}
199
+ <dl>
200
+ <dt>${etiquetaSubtotal}</dt>
201
+ <dd>${p(this.subtotal)}</dd>
202
+ ${this.descuento
203
+ ? html `<dt>Descuento${this.cuponAplicado ? ` (${this.cuponAplicado})` : ''}</dt>
204
+ <dd class="descuento">−${p(this.descuento)}</dd>`
205
+ : nothing}
206
+ <dt>Envío</dt>
207
+ <dd>
208
+ ${this.envio === undefined
209
+ ? html `<span class="pendiente">Se calcula en el siguiente paso</span>`
210
+ : this.envio === 0
211
+ ? html `<span class="gratis">Gratis</span>`
212
+ : p(this.envio)}
213
+ </dd>
214
+ ${this.cargos.map((c) => html `<dt>${c.concepto}</dt><dd>${p(c.monto)}</dd>`)}
215
+ <dt class="total">Total</dt>
216
+ <dd class="total">${this.mostrarMoneda ? html `<span class="moneda">${this.moneda}</span>` : nothing}${p(total)}</dd>
217
+ </dl>
218
+ <div class="acciones" ?hidden=${!this.conAcciones}>
219
+ <slot
220
+ name="acciones"
221
+ @slotchange=${(e) => (this.conAcciones = e.target.assignedNodes().length > 0)}
222
+ ></slot>
223
+ </div>
224
+ <slot name="nota"></slot>
225
+ </section>`;
226
+ }
227
+ renderCupon() {
228
+ if (this.cuponAplicado) {
229
+ return html `<span class="chip">${iconoTag} ${this.cuponAplicado}
230
+ <button aria-label="Quitar cupón ${this.cuponAplicado}" @click=${this.quitarCupon}>${iconoX}</button></span
231
+ >`;
232
+ }
233
+ return html `<form @submit=${this.aplicarCupon}>
234
+ <input
235
+ aria-label="Código de descuento"
236
+ placeholder="Código de descuento"
237
+ autocomplete="off"
238
+ .value=${this.codigo}
239
+ aria-invalid=${this.errorCupon ? 'true' : 'false'}
240
+ aria-describedby=${this.errorCupon ? 'error-cupon' : nothing}
241
+ @input=${(e) => (this.codigo = e.target.value)}
242
+ />
243
+ <button ?disabled=${this.cuponCargando || !this.codigo.trim()}>${this.cuponCargando ? 'Aplicando…' : 'Aplicar'}</button>
244
+ </form>
245
+ ${this.errorCupon ? html `<p class="error-cupon" id="error-cupon" role="alert">${this.errorCupon}</p>` : nothing}`;
246
+ }
247
+ aplicarCupon(evento) {
248
+ evento.preventDefault();
249
+ const codigo = this.codigo.trim().toUpperCase();
250
+ if (!codigo || this.cuponCargando)
251
+ return;
252
+ emitir(this, 'aplicar-cupon', { codigo });
253
+ }
254
+ quitarCupon() {
255
+ const codigo = this.cuponAplicado;
256
+ this.codigo = '';
257
+ emitir(this, 'quitar-cupon', { codigo });
258
+ }
259
+ };
260
+ __decorate([
261
+ property()
262
+ ], LrResumenPedido.prototype, "titulo", void 0);
263
+ __decorate([
264
+ property({ type: Number })
265
+ ], LrResumenPedido.prototype, "subtotal", void 0);
266
+ __decorate([
267
+ property({ type: Number })
268
+ ], LrResumenPedido.prototype, "envio", void 0);
269
+ __decorate([
270
+ property({ type: Number })
271
+ ], LrResumenPedido.prototype, "descuento", void 0);
272
+ __decorate([
273
+ property({ type: Array })
274
+ ], LrResumenPedido.prototype, "cargos", void 0);
275
+ __decorate([
276
+ property({ type: Number })
277
+ ], LrResumenPedido.prototype, "total", void 0);
278
+ __decorate([
279
+ property()
280
+ ], LrResumenPedido.prototype, "moneda", void 0);
281
+ __decorate([
282
+ property({ type: Boolean, attribute: 'mostrar-moneda' })
283
+ ], LrResumenPedido.prototype, "mostrarMoneda", void 0);
284
+ __decorate([
285
+ property({ type: Boolean, attribute: 'con-cupon' })
286
+ ], LrResumenPedido.prototype, "conCupon", void 0);
287
+ __decorate([
288
+ property({ attribute: 'cupon-aplicado' })
289
+ ], LrResumenPedido.prototype, "cuponAplicado", void 0);
290
+ __decorate([
291
+ property({ attribute: 'error-cupon' })
292
+ ], LrResumenPedido.prototype, "errorCupon", void 0);
293
+ __decorate([
294
+ property({ type: Boolean, attribute: 'cupon-cargando' })
295
+ ], LrResumenPedido.prototype, "cuponCargando", void 0);
296
+ __decorate([
297
+ property({ type: Number, attribute: 'piezas' })
298
+ ], LrResumenPedido.prototype, "piezas", void 0);
299
+ __decorate([
300
+ state()
301
+ ], LrResumenPedido.prototype, "codigo", void 0);
302
+ __decorate([
303
+ state()
304
+ ], LrResumenPedido.prototype, "conAcciones", void 0);
305
+ LrResumenPedido = __decorate([
306
+ customElement('lr-resumen-pedido')
307
+ ], LrResumenPedido);
308
+ export { LrResumenPedido };
309
+ //# sourceMappingURL=resumen-pedido.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumen-pedido.js","sourceRoot":"","sources":["../src/resumen-pedido.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@lit-roa/resumen-pedido",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Resumen de pedido con subtotal, envío, cupón y total.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "sideEffects": [
19
+ "./dist/index.js"
20
+ ],
21
+ "scripts": {
22
+ "dev": "vite",
23
+ "build": "tsc -b"
24
+ },
25
+ "dependencies": {
26
+ "@lit-roa/base": "^0.4.0",
27
+ "lit": "^3.3.3"
28
+ },
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@lit-roa/boton": "^0.1.2"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { LrResumenPedido, type CargoExtra, type DetalleCupon } from './resumen-pedido.js';
@@ -0,0 +1,290 @@
1
+ import { LitElement, css, html, nothing } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { emitir, estilosBase, formatearPrecio, iconoTag, iconoX, tokens } from '@lit-roa/base';
4
+
5
+ export interface DetalleCupon {
6
+ codigo: string;
7
+ }
8
+
9
+ export interface CargoExtra {
10
+ concepto: string;
11
+ monto: number;
12
+ }
13
+
14
+ /**
15
+ * Resumen de pedido: subtotal, envío, descuento, cargos extra y total, con
16
+ * un campo opcional para cupones. No calcula precios por su cuenta salvo el
17
+ * total si no se lo das: el sitio (o la API) manda los montos.
18
+ *
19
+ * @fires aplicar-cupon - `detail`: `{ codigo }`. Mientras validas, pon `cupon-cargando`.
20
+ * @fires quitar-cupon - `detail`: `{ codigo }`.
21
+ * @slot - Arriba de los montos (ej. líneas del carrito en modo solo lectura).
22
+ * @slot acciones - Debajo del total (ej. botón "Pagar").
23
+ * @slot nota - Texto pequeño al final (ej. "Impuestos incluidos").
24
+ */
25
+ @customElement('lr-resumen-pedido')
26
+ export class LrResumenPedido extends LitElement {
27
+ static override styles = [
28
+ tokens,
29
+ estilosBase,
30
+ css`
31
+ :host {
32
+ display: block;
33
+ }
34
+ .resumen {
35
+ padding: var(--lr-resumen-relleno, 1.25rem);
36
+ border: 1px solid var(--_color-borde);
37
+ border-radius: var(--_radio);
38
+ background: var(--lr-resumen-fondo, var(--_color-fondo));
39
+ }
40
+ :host([sin-borde]) .resumen {
41
+ padding: 0;
42
+ border: 0;
43
+ background: transparent;
44
+ }
45
+ h2 {
46
+ margin: 0 0 1rem;
47
+ font-size: 1.125rem;
48
+ }
49
+ dl {
50
+ display: grid;
51
+ grid-template-columns: 1fr auto;
52
+ row-gap: 0.625rem;
53
+ margin: 0;
54
+ font-size: 0.9375rem;
55
+ }
56
+ dt {
57
+ color: var(--_color-texto-suave);
58
+ }
59
+ dd {
60
+ margin: 0;
61
+ padding-left: 1rem;
62
+ text-align: right;
63
+ font-variant-numeric: tabular-nums;
64
+ }
65
+ .descuento {
66
+ color: var(--lr-color-exito, #15803d);
67
+ }
68
+ .gratis {
69
+ color: var(--lr-color-exito, #15803d);
70
+ font-weight: 600;
71
+ }
72
+ .pendiente {
73
+ color: var(--_color-texto-suave);
74
+ font-size: 0.8125rem;
75
+ }
76
+ .total {
77
+ padding-top: 0.875rem;
78
+ border-top: 1px solid var(--_color-borde);
79
+ font-size: 1.125rem;
80
+ font-weight: 700;
81
+ color: var(--_color-texto);
82
+ }
83
+ .moneda {
84
+ color: var(--_color-texto-suave);
85
+ font-size: 0.75rem;
86
+ font-weight: 500;
87
+ margin-right: 0.25rem;
88
+ }
89
+ form {
90
+ display: flex;
91
+ gap: 0.5rem;
92
+ margin: 1rem 0;
93
+ }
94
+ input {
95
+ flex: 1;
96
+ min-width: 0;
97
+ height: 2.5rem;
98
+ padding: 0 0.75rem;
99
+ border: 1.5px solid #d1d5db;
100
+ border-radius: calc(var(--_radio) / 1.5);
101
+ font: inherit;
102
+ text-transform: uppercase;
103
+ }
104
+ input:focus {
105
+ outline: 0;
106
+ border-color: var(--_color-primario);
107
+ }
108
+ input[aria-invalid='true'] {
109
+ border-color: var(--lr-color-peligro, #dc2626);
110
+ }
111
+ form button {
112
+ all: unset;
113
+ padding: 0 1rem;
114
+ border-radius: calc(var(--_radio) / 1.5);
115
+ border: 1.5px solid var(--_color-primario);
116
+ color: var(--_color-primario);
117
+ font-weight: 600;
118
+ cursor: pointer;
119
+ }
120
+ form button:disabled {
121
+ opacity: 0.5;
122
+ cursor: wait;
123
+ }
124
+ form button:focus-visible,
125
+ .chip button:focus-visible {
126
+ outline: 2px solid var(--_color-primario);
127
+ outline-offset: 2px;
128
+ }
129
+ .error-cupon {
130
+ margin: -0.5rem 0 1rem;
131
+ color: var(--lr-color-peligro, #dc2626);
132
+ font-size: 0.8125rem;
133
+ }
134
+ .chip {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ gap: 0.375rem;
138
+ margin: 1rem 0;
139
+ padding: 0.25rem 0.375rem 0.25rem 0.625rem;
140
+ border-radius: 999px;
141
+ background: #dcfce7;
142
+ color: #166534;
143
+ font-size: 0.8125rem;
144
+ font-weight: 700;
145
+ }
146
+ .chip button {
147
+ all: unset;
148
+ display: grid;
149
+ place-items: center;
150
+ width: 1.25rem;
151
+ height: 1.25rem;
152
+ border-radius: 999px;
153
+ cursor: pointer;
154
+ }
155
+ .chip button:hover {
156
+ background: rgb(0 0 0 / 0.08);
157
+ }
158
+ .acciones {
159
+ display: grid;
160
+ gap: 0.5rem;
161
+ margin-top: 1.25rem;
162
+ }
163
+ .acciones[hidden] {
164
+ display: none;
165
+ }
166
+ ::slotted([slot='nota']) {
167
+ display: block;
168
+ margin-top: 0.75rem;
169
+ color: var(--_color-texto-suave);
170
+ font-size: 0.75rem;
171
+ text-align: center;
172
+ }
173
+ `,
174
+ ];
175
+
176
+ @property() titulo = 'Resumen del pedido';
177
+
178
+ @property({ type: Number }) subtotal = 0;
179
+
180
+ /** Costo de envío. `0` = "Gratis"; sin valor = "Se calcula en el siguiente paso". */
181
+ @property({ type: Number }) envio?: number;
182
+
183
+ /** Descuento como número positivo; se muestra restando. */
184
+ @property({ type: Number }) descuento = 0;
185
+
186
+ /** Cargos extra (ej. impuestos, propina). Como atributo, en JSON. */
187
+ @property({ type: Array }) cargos: CargoExtra[] = [];
188
+
189
+ /** Total. Si no se indica: subtotal + envío + cargos − descuento. */
190
+ @property({ type: Number }) total?: number;
191
+
192
+ @property() moneda = 'MXN';
193
+
194
+ /** Muestra el código de moneda junto al total. */
195
+ @property({ type: Boolean, attribute: 'mostrar-moneda' }) mostrarMoneda = false;
196
+
197
+ /** Muestra el campo de cupón. */
198
+ @property({ type: Boolean, attribute: 'con-cupon' }) conCupon = false;
199
+
200
+ /** Código ya aplicado: se muestra como etiqueta con botón para quitarlo. */
201
+ @property({ attribute: 'cupon-aplicado' }) cuponAplicado = '';
202
+
203
+ @property({ attribute: 'error-cupon' }) errorCupon = '';
204
+
205
+ @property({ type: Boolean, attribute: 'cupon-cargando' }) cuponCargando = false;
206
+
207
+ @property({ type: Number, attribute: 'piezas' }) piezas?: number;
208
+
209
+ @state() private codigo = '';
210
+ @state() private conAcciones = false;
211
+
212
+ override render() {
213
+ const extra = this.cargos.reduce((s, c) => s + c.monto, 0);
214
+ const total = this.total ?? this.subtotal + (this.envio ?? 0) + extra - this.descuento;
215
+ const p = (n: number) => formatearPrecio(n, this.moneda);
216
+ const etiquetaSubtotal = this.piezas !== undefined ? `Subtotal (${this.piezas} ${this.piezas === 1 ? 'producto' : 'productos'})` : 'Subtotal';
217
+
218
+ return html`<section class="resumen" part="resumen" aria-label=${this.titulo}>
219
+ ${this.titulo ? html`<h2>${this.titulo}</h2>` : nothing}
220
+ <slot></slot>
221
+ ${this.conCupon ? this.renderCupon() : nothing}
222
+ <dl>
223
+ <dt>${etiquetaSubtotal}</dt>
224
+ <dd>${p(this.subtotal)}</dd>
225
+ ${this.descuento
226
+ ? html`<dt>Descuento${this.cuponAplicado ? ` (${this.cuponAplicado})` : ''}</dt>
227
+ <dd class="descuento">−${p(this.descuento)}</dd>`
228
+ : nothing}
229
+ <dt>Envío</dt>
230
+ <dd>
231
+ ${this.envio === undefined
232
+ ? html`<span class="pendiente">Se calcula en el siguiente paso</span>`
233
+ : this.envio === 0
234
+ ? html`<span class="gratis">Gratis</span>`
235
+ : p(this.envio)}
236
+ </dd>
237
+ ${this.cargos.map((c) => html`<dt>${c.concepto}</dt><dd>${p(c.monto)}</dd>`)}
238
+ <dt class="total">Total</dt>
239
+ <dd class="total">${this.mostrarMoneda ? html`<span class="moneda">${this.moneda}</span>` : nothing}${p(total)}</dd>
240
+ </dl>
241
+ <div class="acciones" ?hidden=${!this.conAcciones}>
242
+ <slot
243
+ name="acciones"
244
+ @slotchange=${(e: Event) => (this.conAcciones = (e.target as HTMLSlotElement).assignedNodes().length > 0)}
245
+ ></slot>
246
+ </div>
247
+ <slot name="nota"></slot>
248
+ </section>`;
249
+ }
250
+
251
+ private renderCupon() {
252
+ if (this.cuponAplicado) {
253
+ return html`<span class="chip">${iconoTag} ${this.cuponAplicado}
254
+ <button aria-label="Quitar cupón ${this.cuponAplicado}" @click=${this.quitarCupon}>${iconoX}</button></span
255
+ >`;
256
+ }
257
+ return html`<form @submit=${this.aplicarCupon}>
258
+ <input
259
+ aria-label="Código de descuento"
260
+ placeholder="Código de descuento"
261
+ autocomplete="off"
262
+ .value=${this.codigo}
263
+ aria-invalid=${this.errorCupon ? 'true' : 'false'}
264
+ aria-describedby=${this.errorCupon ? 'error-cupon' : nothing}
265
+ @input=${(e: Event) => (this.codigo = (e.target as HTMLInputElement).value)}
266
+ />
267
+ <button ?disabled=${this.cuponCargando || !this.codigo.trim()}>${this.cuponCargando ? 'Aplicando…' : 'Aplicar'}</button>
268
+ </form>
269
+ ${this.errorCupon ? html`<p class="error-cupon" id="error-cupon" role="alert">${this.errorCupon}</p>` : nothing}`;
270
+ }
271
+
272
+ private aplicarCupon(evento: Event) {
273
+ evento.preventDefault();
274
+ const codigo = this.codigo.trim().toUpperCase();
275
+ if (!codigo || this.cuponCargando) return;
276
+ emitir<DetalleCupon>(this, 'aplicar-cupon', { codigo });
277
+ }
278
+
279
+ private quitarCupon() {
280
+ const codigo = this.cuponAplicado;
281
+ this.codigo = '';
282
+ emitir<DetalleCupon>(this, 'quitar-cupon', { codigo });
283
+ }
284
+ }
285
+
286
+ declare global {
287
+ interface HTMLElementTagNameMap {
288
+ 'lr-resumen-pedido': LrResumenPedido;
289
+ }
290
+ }