@erplora/outfitkit 0.1.16 → 0.1.18

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/dist/cdn.d.ts CHANGED
@@ -38,6 +38,7 @@ import './components/ok-invoice/ok-invoice.js';
38
38
  import './components/ok-timeline/ok-timeline.js';
39
39
  import './components/ok-qty-stepper/ok-qty-stepper.js';
40
40
  import './components/ok-command-palette/ok-command-palette.js';
41
+ import './components/ok-spotlight-search/ok-spotlight-search.js';
41
42
  import './components/ok-color-picker/ok-color-picker.js';
42
43
  import './components/ok-avatar/ok-avatar.js';
43
44
  import './components/ok-status-pill/ok-status-pill.js';
@@ -77,6 +77,11 @@ export interface ReceiptData {
77
77
  qr?: string;
78
78
  /** Leyenda bajo el QR. */
79
79
  qr_note?: string;
80
+ /** QR promocional del negocio (reseñas Google, redes, web). Va SIEMPRE al final,
81
+ * después del fiscal (que es el legal) y más pequeño. Si vacío, no deja rastro. */
82
+ promo_qr?: string;
83
+ /** Leyenda sobre el QR promocional (p.ej. «Escanea y déjanos una reseña»). */
84
+ promo_note?: string;
80
85
  }
81
86
  export declare class OkReceipt extends LitElement {
82
87
  static styles: import("lit").CSSResult;
@@ -95,6 +100,8 @@ export declare class OkReceipt extends LitElement {
95
100
  private renderLines;
96
101
  private renderTotals;
97
102
  private renderQr;
103
+ /** QR promocional (reseñas/redes): al final del papel y más pequeño que el fiscal. */
104
+ private renderPromo;
98
105
  }
99
106
  declare global {
100
107
  interface HTMLElementTagNameMap {
@@ -0,0 +1 @@
1
+ import './ok-receipt.js';
@@ -0,0 +1,27 @@
1
+ import { LitElement } from 'lit';
2
+ export declare class OkSpotlightSearch extends LitElement {
3
+ static styles: import("lit").CSSResult;
4
+ /** Abierto/cerrado (reflejado para CSS externo). */
5
+ open: boolean;
6
+ /** Texto guía del input de búsqueda. */
7
+ placeholder: string;
8
+ /** Texto de búsqueda actual (el consumidor puede resetearlo). */
9
+ value: string;
10
+ /** Icono (Iconify/ionicon) del botón-trigger propio; si vacío, no se pinta trigger. */
11
+ triggerIcon: string;
12
+ /** Nombre accesible del botón-trigger (aria-label). */
13
+ triggerLabel: string;
14
+ private input?;
15
+ openSearch(): void;
16
+ close(): void;
17
+ toggle(): void;
18
+ private emitOpen;
19
+ private onInput;
20
+ protected updated(): void;
21
+ render(): unknown;
22
+ }
23
+ declare global {
24
+ interface HTMLElementTagNameMap {
25
+ 'ok-spotlight-search': OkSpotlightSearch;
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ import './ok-spotlight-search.js';
package/dist/index.d.ts CHANGED
@@ -59,6 +59,7 @@ export type { OkTimelineItem } from './components/ok-timeline/ok-timeline.js';
59
59
  export { OkQtyStepper } from './components/ok-qty-stepper/ok-qty-stepper.js';
60
60
  export { OkCommandPalette } from './components/ok-command-palette/ok-command-palette.js';
61
61
  export type { OkCommand } from './components/ok-command-palette/ok-command-palette.js';
62
+ export { OkSpotlightSearch } from './components/ok-spotlight-search/ok-spotlight-search.js';
62
63
  export { OkColorPicker } from './components/ok-color-picker/ok-color-picker.js';
63
64
  export type { OkRgb } from './components/ok-color-picker/ok-color-picker.js';
64
65
  export { OkAvatar } from './components/ok-avatar/ok-avatar.js';
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ import { OkInvoice } from "./ok-invoice.js";
39
39
  import { OkTimeline } from "./ok-timeline.js";
40
40
  import { OkQtyStepper } from "./ok-qty-stepper.js";
41
41
  import { OkCommandPalette } from "./ok-command-palette.js";
42
+ import { OkSpotlightSearch } from "./ok-spotlight-search.js";
42
43
  import { OkColorPicker } from "./ok-color-picker.js";
43
44
  import { OkAvatar } from "./ok-avatar.js";
44
45
  import { OkStatusPill } from "./ok-status-pill.js";
@@ -180,6 +181,7 @@ export {
180
181
  OkSparkline,
181
182
  OkSplitButton,
182
183
  OkSplitter,
184
+ OkSpotlightSearch,
183
185
  OkStat,
184
186
  OkStatusDot,
185
187
  OkStatusPill,
@@ -76,6 +76,8 @@ class OkReceipt extends LitElement {
76
76
  .footer { font-size: 10px; white-space: pre-line; }
77
77
  .qr-wrap { display: flex; flex-direction: column; align-items: center; gap: 1mm; margin-top: 2mm; }
78
78
  .qr-note { font-size: 8px; text-align: center; word-break: break-word; }
79
+ .promo-wrap { display: flex; flex-direction: column; align-items: center; gap: 1mm; margin-top: 2mm; }
80
+ .promo-note { font-size: 9px; text-align: center; word-break: break-word; }
79
81
  .empty { padding: 4mm; text-align: center; color: #888; font-style: italic; }
80
82
  `;
81
83
  }
@@ -101,6 +103,7 @@ class OkReceipt extends LitElement {
101
103
  ${this.renderTotals(r)}
102
104
  ${r.footer ? html`<hr class="sep" /><div class="center footer">${r.footer}</div>` : nothing}
103
105
  ${this.renderQr(r)}
106
+ ${this.renderPromo(r)}
104
107
  </div>`;
105
108
  }
106
109
  renderHeader(r) {
@@ -167,6 +170,14 @@ class OkReceipt extends LitElement {
167
170
  ${r.qr_note ? html`<div class="qr-note">${r.qr_note}</div>` : nothing}
168
171
  </div>`;
169
172
  }
173
+ /** QR promocional (reseñas/redes): al final del papel y más pequeño que el fiscal. */
174
+ renderPromo(r) {
175
+ if (!r.promo_qr) return nothing;
176
+ return html`<div class="promo-wrap">
177
+ ${r.promo_note ? html`<div class="promo-note">${r.promo_note}</div>` : nothing}
178
+ <ok-qr .value=${r.promo_qr} .size=${Math.round(this.qrSize * 0.7)} ec="M" color="#000" background="#fff"></ok-qr>
179
+ </div>`;
180
+ }
170
181
  }
171
182
  __decorateClass([
172
183
  property({ attribute: false })
@@ -0,0 +1,211 @@
1
+ import { LitElement, css, nothing, html } from "lit";
2
+ import { property, query } from "lit/decorators.js";
3
+ import { define } from "./define.js";
4
+ import { L as iconSearchOutline, v as iconCloseOutline, o as okIcon } from "./shared/icons.js";
5
+ var __defProp = Object.defineProperty;
6
+ var __decorateClass = (decorators, target, key, kind) => {
7
+ var result = void 0;
8
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
9
+ if (decorator = decorators[i])
10
+ result = decorator(target, key, result) || result;
11
+ if (result) __defProp(target, key, result);
12
+ return result;
13
+ };
14
+ class OkSpotlightSearch extends LitElement {
15
+ constructor() {
16
+ super(...arguments);
17
+ this.open = false;
18
+ this.placeholder = "";
19
+ this.value = "";
20
+ this.triggerIcon = "";
21
+ this.triggerLabel = "";
22
+ }
23
+ static {
24
+ this.styles = css`
25
+ :host {
26
+ --color: var(--ok-text, var(--ion-text-color, #1c1b17));
27
+ --color-muted: var(--ok-text-muted, rgba(var(--ion-text-color-rgb, 28, 27, 23), 0.6));
28
+ --panel-bg: var(--ok-surface, var(--ion-background-color, #ffffff));
29
+ --scrim-bg: var(--ok-scrim, rgba(0, 0, 0, 0.28));
30
+ --border-soft: var(--ok-border-soft, rgba(var(--ion-text-color-rgb, 28, 27, 23), 0.1));
31
+ --radius: var(--ok-radius, 16px);
32
+ --shadow: var(--ok-shadow, 0 24px 80px rgba(0, 0, 0, 0.35));
33
+ --font: var(--ok-font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
34
+ display: contents;
35
+ }
36
+
37
+ /* Botón-trigger opcional (icon-only). */
38
+ button.trigger {
39
+ display: inline-flex;
40
+ align-items: center;
41
+ justify-content: center;
42
+ width: 2.4rem;
43
+ height: 2.4rem;
44
+ padding: 0;
45
+ border: 0;
46
+ border-radius: 10px;
47
+ background: none;
48
+ color: var(--color-muted);
49
+ cursor: pointer;
50
+ }
51
+ button.trigger[data-assigned] { color: var(--ok-primary, var(--ion-color-primary, #3880ff)); }
52
+ button.trigger ion-icon { font-size: 1.35rem; }
53
+
54
+ /* El <dialog> flota arriba-centro, translúcido con blur (Spotlight). El top layer lo saca de
55
+ cualquier containing block. */
56
+ dialog {
57
+ margin: 10vh auto auto;
58
+ width: min(92vw, 36rem);
59
+ max-height: 72vh;
60
+ padding: 0;
61
+ border: none;
62
+ border-radius: var(--radius);
63
+ overflow: hidden;
64
+ color: var(--color);
65
+ font-family: var(--font);
66
+ background: color-mix(in srgb, var(--panel-bg) 80%, transparent);
67
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
68
+ backdrop-filter: blur(22px) saturate(180%);
69
+ box-shadow: var(--shadow), 0 0 0 1px rgba(128, 128, 128, 0.18);
70
+ }
71
+ dialog::backdrop {
72
+ background: var(--scrim-bg);
73
+ -webkit-backdrop-filter: blur(3px);
74
+ backdrop-filter: blur(3px);
75
+ }
76
+
77
+ /* Fila del input hero + cierre. */
78
+ .top {
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ padding: 0.7rem 0.85rem;
83
+ border-bottom: 1px solid var(--border-soft);
84
+ }
85
+ .top .lupa { flex: 0 0 auto; font-size: 1.25rem; color: var(--color-muted); }
86
+ .top input {
87
+ flex: 1 1 auto;
88
+ min-width: 0;
89
+ border: 0;
90
+ outline: none;
91
+ background: none;
92
+ color: inherit;
93
+ font: inherit;
94
+ font-size: 1.05rem;
95
+ }
96
+ .top input::placeholder { color: var(--color-muted); }
97
+ .top .close {
98
+ flex: 0 0 auto;
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ width: 2rem;
103
+ height: 2rem;
104
+ padding: 0;
105
+ border: 0;
106
+ border-radius: 8px;
107
+ background: none;
108
+ color: var(--color-muted);
109
+ cursor: pointer;
110
+ }
111
+ .top .close ion-icon { font-size: 1.2rem; }
112
+
113
+ /* Cuerpo scrollable: aquí caen los resultados del consumidor (slot por defecto). */
114
+ .results { max-height: 56vh; overflow-y: auto; padding: 0.35rem; }
115
+ .footer:not(:empty) { border-top: 1px solid var(--border-soft); padding: 0.3rem 0.6rem; }
116
+
117
+ @media (max-width: 560px) {
118
+ dialog { margin: 0 auto auto; width: 100vw; max-width: 100vw; max-height: 100vh; height: auto; border-radius: 0; }
119
+ }
120
+ `;
121
+ }
122
+ // ── API pública ──────────────────────────────────────────────────────────
123
+ openSearch() {
124
+ if (this.open) return;
125
+ this.open = true;
126
+ this.emitOpen(true);
127
+ }
128
+ close() {
129
+ if (!this.open) return;
130
+ this.open = false;
131
+ this.emitOpen(false);
132
+ }
133
+ toggle() {
134
+ this.open ? this.close() : this.openSearch();
135
+ }
136
+ emitOpen(open) {
137
+ this.dispatchEvent(new CustomEvent("ok-open", { detail: { open }, bubbles: true, composed: true }));
138
+ }
139
+ onInput(e) {
140
+ this.value = e.target.value;
141
+ this.dispatchEvent(new CustomEvent("ok-input", { detail: { value: this.value }, bubbles: true, composed: true }));
142
+ }
143
+ // Sincroniza `open` ↔ el <dialog> nativo (top layer). try/catch porque happy-dom (tests) no
144
+ // implementa showModal/close; ahí `open` sigue siendo la verdad.
145
+ updated() {
146
+ const d = this.renderRoot.querySelector("dialog");
147
+ if (!d) return;
148
+ try {
149
+ if (this.open && !d.open) {
150
+ d.showModal();
151
+ this.input?.focus();
152
+ } else if (!this.open && d.open) {
153
+ d.close();
154
+ }
155
+ } catch {
156
+ }
157
+ }
158
+ render() {
159
+ return html`
160
+ ${this.triggerIcon ? html`<button class="trigger" ?data-assigned=${this.open} aria-label=${this.triggerLabel || this.placeholder}
161
+ title=${this.triggerLabel || this.placeholder} @click=${() => this.openSearch()}>
162
+ <ion-icon .icon=${okIcon(this.triggerIcon)}></ion-icon>
163
+ </button>` : nothing}
164
+
165
+ <dialog aria-label=${this.triggerLabel || this.placeholder}
166
+ @close=${() => {
167
+ if (this.open) {
168
+ this.open = false;
169
+ this.emitOpen(false);
170
+ }
171
+ }}
172
+ @click=${(e) => {
173
+ if (e.target === e.currentTarget) this.close();
174
+ }}>
175
+ <div class="top">
176
+ <ion-icon class="lupa" .icon=${iconSearchOutline}></ion-icon>
177
+ <input type="text" .value=${this.value} placeholder=${this.placeholder}
178
+ aria-label=${this.placeholder} autocomplete="off" spellcheck="false"
179
+ @input=${(e) => this.onInput(e)} />
180
+ <button class="close" aria-label="Cerrar" @click=${() => this.close()}>
181
+ <ion-icon .icon=${iconCloseOutline}></ion-icon>
182
+ </button>
183
+ </div>
184
+ <div class="results"><slot></slot></div>
185
+ <div class="footer"><slot name="footer"></slot></div>
186
+ </dialog>
187
+ `;
188
+ }
189
+ }
190
+ __decorateClass([
191
+ property({ type: Boolean, reflect: true })
192
+ ], OkSpotlightSearch.prototype, "open");
193
+ __decorateClass([
194
+ property()
195
+ ], OkSpotlightSearch.prototype, "placeholder");
196
+ __decorateClass([
197
+ property()
198
+ ], OkSpotlightSearch.prototype, "value");
199
+ __decorateClass([
200
+ property({ attribute: "trigger-icon" })
201
+ ], OkSpotlightSearch.prototype, "triggerIcon");
202
+ __decorateClass([
203
+ property({ attribute: "trigger-label" })
204
+ ], OkSpotlightSearch.prototype, "triggerLabel");
205
+ __decorateClass([
206
+ query(".top input")
207
+ ], OkSpotlightSearch.prototype, "input");
208
+ define("ok-spotlight-search", OkSpotlightSearch);
209
+ export {
210
+ OkSpotlightSearch
211
+ };