@nymbalhq/web-components 1.1.0 → 1.2.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/astro/NymbalElement.astro +19 -0
- package/dist/base-element-CPMgAdyQ.d.ts +91 -0
- package/dist/chunk-2TPQNQYO.js +155 -0
- package/dist/chunk-2TPQNQYO.js.map +1 -0
- package/dist/chunk-2XXBQPDB.js +113 -0
- package/dist/chunk-2XXBQPDB.js.map +1 -0
- package/dist/chunk-42SSME5H.js +84 -0
- package/dist/chunk-42SSME5H.js.map +1 -0
- package/dist/chunk-4COVK727.js +204 -0
- package/dist/chunk-4COVK727.js.map +1 -0
- package/dist/chunk-5VWZ2U5K.js +149 -0
- package/dist/chunk-5VWZ2U5K.js.map +1 -0
- package/dist/chunk-6XCCAAMF.js +656 -0
- package/dist/chunk-6XCCAAMF.js.map +1 -0
- package/dist/chunk-7JAAOVCE.js +114 -0
- package/dist/chunk-7JAAOVCE.js.map +1 -0
- package/dist/chunk-B73QVSA5.js +206 -0
- package/dist/chunk-B73QVSA5.js.map +1 -0
- package/dist/chunk-DR5QB5VN.js +17 -0
- package/dist/chunk-DR5QB5VN.js.map +1 -0
- package/dist/chunk-DXWMNB5F.js +70 -0
- package/dist/chunk-DXWMNB5F.js.map +1 -0
- package/dist/chunk-EBMA7JGV.js +130 -0
- package/dist/chunk-EBMA7JGV.js.map +1 -0
- package/dist/chunk-EYDMJNNK.js +176 -0
- package/dist/chunk-EYDMJNNK.js.map +1 -0
- package/dist/chunk-FIF6GQ6U.js +141 -0
- package/dist/chunk-FIF6GQ6U.js.map +1 -0
- package/dist/chunk-GHXTJ2CF.js +109 -0
- package/dist/chunk-GHXTJ2CF.js.map +1 -0
- package/dist/chunk-H3ZBYNHV.js +153 -0
- package/dist/chunk-H3ZBYNHV.js.map +1 -0
- package/dist/chunk-HPBC3OG7.js +174 -0
- package/dist/chunk-HPBC3OG7.js.map +1 -0
- package/dist/chunk-IYT7VXFS.js +105 -0
- package/dist/chunk-IYT7VXFS.js.map +1 -0
- package/dist/chunk-LHYXNQWY.js +105 -0
- package/dist/chunk-LHYXNQWY.js.map +1 -0
- package/dist/chunk-LO2EW5YP.js +25 -0
- package/dist/chunk-LO2EW5YP.js.map +1 -0
- package/dist/chunk-LSQPUZK6.js +291 -0
- package/dist/chunk-LSQPUZK6.js.map +1 -0
- package/dist/chunk-N4Y5DI4X.js +82 -0
- package/dist/chunk-N4Y5DI4X.js.map +1 -0
- package/dist/chunk-NE6UHTOT.js +148 -0
- package/dist/chunk-NE6UHTOT.js.map +1 -0
- package/dist/chunk-NW47OJVN.js +138 -0
- package/dist/chunk-NW47OJVN.js.map +1 -0
- package/dist/chunk-O2YKCDR2.js +77 -0
- package/dist/chunk-O2YKCDR2.js.map +1 -0
- package/dist/chunk-PN6QKYE4.js +71 -0
- package/dist/chunk-PN6QKYE4.js.map +1 -0
- package/dist/chunk-RAOGZH3Z.js +188 -0
- package/dist/chunk-RAOGZH3Z.js.map +1 -0
- package/dist/chunk-RVSKX2RX.js +11 -0
- package/dist/chunk-RVSKX2RX.js.map +1 -0
- package/dist/chunk-RZVFA3N5.js +154 -0
- package/dist/chunk-RZVFA3N5.js.map +1 -0
- package/dist/chunk-SM7TASEW.js +135 -0
- package/dist/chunk-SM7TASEW.js.map +1 -0
- package/dist/chunk-U2PU4CWB.js +124 -0
- package/dist/chunk-U2PU4CWB.js.map +1 -0
- package/dist/chunk-UCEVCLQ7.js +5 -0
- package/dist/chunk-UCEVCLQ7.js.map +1 -0
- package/dist/chunk-UYSZOEOA.js +32 -0
- package/dist/chunk-UYSZOEOA.js.map +1 -0
- package/dist/chunk-V55K2CS5.js +126 -0
- package/dist/chunk-V55K2CS5.js.map +1 -0
- package/dist/chunk-VTQVGNVC.js +346 -0
- package/dist/chunk-VTQVGNVC.js.map +1 -0
- package/dist/chunk-W23P7SI4.js +239 -0
- package/dist/chunk-W23P7SI4.js.map +1 -0
- package/dist/chunk-W7D3STJH.js +279 -0
- package/dist/chunk-W7D3STJH.js.map +1 -0
- package/dist/chunk-WEEHXWM3.js +172 -0
- package/dist/chunk-WEEHXWM3.js.map +1 -0
- package/dist/chunk-WTEVTCIL.js +128 -0
- package/dist/chunk-WTEVTCIL.js.map +1 -0
- package/dist/chunk-XE6IWISA.js +137 -0
- package/dist/chunk-XE6IWISA.js.map +1 -0
- package/dist/elements/nymbal-add-to-cart.d.ts +14 -0
- package/dist/elements/nymbal-add-to-cart.js +10 -0
- package/dist/elements/nymbal-add-to-cart.js.map +1 -0
- package/dist/elements/nymbal-address-book.d.ts +30 -0
- package/dist/elements/nymbal-address-book.js +13 -0
- package/dist/elements/nymbal-address-book.js.map +1 -0
- package/dist/elements/nymbal-address-form.d.ts +29 -0
- package/dist/elements/nymbal-address-form.js +12 -0
- package/dist/elements/nymbal-address-form.js.map +1 -0
- package/dist/elements/nymbal-breadcrumb.d.ts +15 -0
- package/dist/elements/nymbal-breadcrumb.js +11 -0
- package/dist/elements/nymbal-breadcrumb.js.map +1 -0
- package/dist/elements/nymbal-cart-drawer.d.ts +24 -0
- package/dist/elements/nymbal-cart-drawer.js +11 -0
- package/dist/elements/nymbal-cart-drawer.js.map +1 -0
- package/dist/elements/nymbal-cart-page.d.ts +20 -0
- package/dist/elements/nymbal-cart-page.js +14 -0
- package/dist/elements/nymbal-cart-page.js.map +1 -0
- package/dist/elements/nymbal-category-nav.d.ts +15 -0
- package/dist/elements/nymbal-category-nav.js +11 -0
- package/dist/elements/nymbal-category-nav.js.map +1 -0
- package/dist/elements/nymbal-checkout.d.ts +77 -0
- package/dist/elements/nymbal-checkout.js +15 -0
- package/dist/elements/nymbal-checkout.js.map +1 -0
- package/dist/elements/nymbal-image-gallery.d.ts +19 -0
- package/dist/elements/nymbal-image-gallery.js +10 -0
- package/dist/elements/nymbal-image-gallery.js.map +1 -0
- package/dist/elements/nymbal-login-form.d.ts +17 -0
- package/dist/elements/nymbal-login-form.js +12 -0
- package/dist/elements/nymbal-login-form.js.map +1 -0
- package/dist/elements/nymbal-mini-cart.d.ts +17 -0
- package/dist/elements/nymbal-mini-cart.js +10 -0
- package/dist/elements/nymbal-mini-cart.js.map +1 -0
- package/dist/elements/nymbal-order-summary.d.ts +20 -0
- package/dist/elements/nymbal-order-summary.js +11 -0
- package/dist/elements/nymbal-order-summary.js.map +1 -0
- package/dist/elements/nymbal-payment-element.d.ts +4 -0
- package/dist/elements/nymbal-payment-element.js +10 -0
- package/dist/elements/nymbal-payment-element.js.map +1 -0
- package/dist/elements/nymbal-product-card.d.ts +4 -0
- package/dist/elements/nymbal-product-card.js +12 -0
- package/dist/elements/nymbal-product-card.js.map +1 -0
- package/dist/elements/nymbal-product-filter.d.ts +32 -0
- package/dist/elements/nymbal-product-filter.js +10 -0
- package/dist/elements/nymbal-product-filter.js.map +1 -0
- package/dist/elements/nymbal-product-grid.d.ts +24 -0
- package/dist/elements/nymbal-product-grid.js +14 -0
- package/dist/elements/nymbal-product-grid.js.map +1 -0
- package/dist/elements/nymbal-product-price.d.ts +18 -0
- package/dist/elements/nymbal-product-price.js +12 -0
- package/dist/elements/nymbal-product-price.js.map +1 -0
- package/dist/elements/nymbal-profile-form.d.ts +19 -0
- package/dist/elements/nymbal-profile-form.js +12 -0
- package/dist/elements/nymbal-profile-form.js.map +1 -0
- package/dist/elements/nymbal-quantity-selector.d.ts +20 -0
- package/dist/elements/nymbal-quantity-selector.js +10 -0
- package/dist/elements/nymbal-quantity-selector.js.map +1 -0
- package/dist/elements/nymbal-register-form.d.ts +17 -0
- package/dist/elements/nymbal-register-form.js +12 -0
- package/dist/elements/nymbal-register-form.js.map +1 -0
- package/dist/elements/nymbal-review-form.d.ts +17 -0
- package/dist/elements/nymbal-review-form.js +12 -0
- package/dist/elements/nymbal-review-form.js.map +1 -0
- package/dist/elements/nymbal-review-list.d.ts +14 -0
- package/dist/elements/nymbal-review-list.js +10 -0
- package/dist/elements/nymbal-review-list.js.map +1 -0
- package/dist/elements/nymbal-search-bar.d.ts +19 -0
- package/dist/elements/nymbal-search-bar.js +10 -0
- package/dist/elements/nymbal-search-bar.js.map +1 -0
- package/dist/elements/nymbal-search-results.d.ts +29 -0
- package/dist/elements/nymbal-search-results.js +14 -0
- package/dist/elements/nymbal-search-results.js.map +1 -0
- package/dist/elements/nymbal-site-footer.d.ts +15 -0
- package/dist/elements/nymbal-site-footer.js +11 -0
- package/dist/elements/nymbal-site-footer.js.map +1 -0
- package/dist/elements/nymbal-site-header.d.ts +19 -0
- package/dist/elements/nymbal-site-header.js +11 -0
- package/dist/elements/nymbal-site-header.js.map +1 -0
- package/dist/elements/nymbal-sort-dropdown.d.ts +28 -0
- package/dist/elements/nymbal-sort-dropdown.js +10 -0
- package/dist/elements/nymbal-sort-dropdown.js.map +1 -0
- package/dist/elements/nymbal-stock-badge.d.ts +16 -0
- package/dist/elements/nymbal-stock-badge.js +11 -0
- package/dist/elements/nymbal-stock-badge.js.map +1 -0
- package/dist/elements/nymbal-toast.d.ts +18 -0
- package/dist/elements/nymbal-toast.js +10 -0
- package/dist/elements/nymbal-toast.js.map +1 -0
- package/dist/elements/nymbal-variant-selector.d.ts +16 -0
- package/dist/elements/nymbal-variant-selector.js +10 -0
- package/dist/elements/nymbal-variant-selector.js.map +1 -0
- package/dist/form-element-C-qo3gpD.d.ts +25 -0
- package/dist/hydrate-support.d.ts +2 -0
- package/dist/hydrate-support.js +3 -0
- package/dist/hydrate-support.js.map +1 -0
- package/dist/index.d.ts +38 -157
- package/dist/index.js +127 -1407
- package/dist/index.js.map +1 -1
- package/dist/nymbal-payment-element-Bt8EF42T.d.ts +51 -0
- package/dist/nymbal-product-card-C8HkE_rG.d.ts +47 -0
- package/dist/register-components.d.ts +46 -0
- package/dist/register-components.js +48 -0
- package/dist/register-components.js.map +1 -0
- package/dist/selected-variant-Dym0shlJ.d.ts +23 -0
- package/dist/ssr/index.d.ts +30 -0
- package/dist/ssr/index.js +143 -0
- package/dist/ssr/index.js.map +1 -0
- package/package.json +33 -6
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalElement,
|
|
4
|
+
baseStyles,
|
|
5
|
+
propertiesOf
|
|
6
|
+
} from "./chunk-EYDMJNNK.js";
|
|
7
|
+
|
|
8
|
+
// src/components/search-bar.ts
|
|
9
|
+
import { css, html, nothing } from "lit";
|
|
10
|
+
var SCHEMA = {
|
|
11
|
+
tag: "nymbal-search-bar",
|
|
12
|
+
reactName: "SearchBar",
|
|
13
|
+
attributes: [
|
|
14
|
+
{ name: "placeholder", prop: "placeholder", type: "string" },
|
|
15
|
+
{ name: "debounce-ms", prop: "debounceMs", type: "number" }
|
|
16
|
+
],
|
|
17
|
+
events: [
|
|
18
|
+
{ name: "nymbal:search:selected", prop: "onSearchSelect", detail: "{ product: unknown }" }
|
|
19
|
+
],
|
|
20
|
+
parts: ["input", "results", "loading", "result", "result-name", "result-price"],
|
|
21
|
+
slots: ["loading"],
|
|
22
|
+
children: false
|
|
23
|
+
};
|
|
24
|
+
var NO_SEARCH = { query: "", loading: false, results: [] };
|
|
25
|
+
function formatPrice(minor, currency) {
|
|
26
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(minor / 100);
|
|
27
|
+
}
|
|
28
|
+
var NymbalSearchBar = class extends NymbalElement {
|
|
29
|
+
static schema = SCHEMA;
|
|
30
|
+
static properties = propertiesOf(SCHEMA);
|
|
31
|
+
static styles = [
|
|
32
|
+
baseStyles,
|
|
33
|
+
css`
|
|
34
|
+
:host {
|
|
35
|
+
display: block;
|
|
36
|
+
position: relative;
|
|
37
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
38
|
+
}
|
|
39
|
+
[part~='input'] {
|
|
40
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
41
|
+
font-size: var(--nymbal-font-size-base, 1rem);
|
|
42
|
+
width: 100%;
|
|
43
|
+
padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);
|
|
44
|
+
border: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
45
|
+
border-radius: var(--nymbal-radius-md, 0.375rem);
|
|
46
|
+
outline: none;
|
|
47
|
+
transition: border-color var(--nymbal-transition-fast, 150ms ease);
|
|
48
|
+
}
|
|
49
|
+
[part~='input']:focus {
|
|
50
|
+
border-color: var(--nymbal-color-accent, #2563eb);
|
|
51
|
+
}
|
|
52
|
+
[part~='results'] {
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 100%;
|
|
55
|
+
left: 0;
|
|
56
|
+
right: 0;
|
|
57
|
+
background: var(--nymbal-color-surface, #ffffff);
|
|
58
|
+
border: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
59
|
+
border-top: none;
|
|
60
|
+
border-radius: 0 0 var(--nymbal-radius-md, 0.375rem) var(--nymbal-radius-md, 0.375rem);
|
|
61
|
+
box-shadow: var(--nymbal-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));
|
|
62
|
+
z-index: var(--nymbal-z-dropdown, 100);
|
|
63
|
+
max-height: 300px;
|
|
64
|
+
overflow-y: auto;
|
|
65
|
+
}
|
|
66
|
+
[part~='result'] {
|
|
67
|
+
padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
transition: background var(--nymbal-transition-fast, 150ms ease);
|
|
70
|
+
}
|
|
71
|
+
[part~='result']:hover,
|
|
72
|
+
[part~='result'][aria-selected='true'] {
|
|
73
|
+
background: var(--nymbal-color-secondary, #f4f4f5);
|
|
74
|
+
}
|
|
75
|
+
[part~='result-name'] {
|
|
76
|
+
font-weight: var(--nymbal-font-weight-medium, 500);
|
|
77
|
+
}
|
|
78
|
+
[part~='result-price'] {
|
|
79
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
80
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
81
|
+
}
|
|
82
|
+
[part~='loading'] {
|
|
83
|
+
padding: var(--nymbal-spacing-md, 1rem);
|
|
84
|
+
text-align: center;
|
|
85
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
86
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
];
|
|
90
|
+
debounceTimer = null;
|
|
91
|
+
selectedIndex = -1;
|
|
92
|
+
stores() {
|
|
93
|
+
return [this.client.search];
|
|
94
|
+
}
|
|
95
|
+
render() {
|
|
96
|
+
const state = this.readsStores ? this.client.search.getState() : NO_SEARCH;
|
|
97
|
+
const placeholder = this.getAttribute("placeholder") ?? "Search products...";
|
|
98
|
+
let results = nothing;
|
|
99
|
+
if (state.query && document.querySelector("nymbal-search-results") === null) {
|
|
100
|
+
if (state.loading) {
|
|
101
|
+
results = html`<div part="results" data-testid="search-results">
|
|
102
|
+
<div part="loading"><slot name="loading">Searching...</slot></div>
|
|
103
|
+
</div>`;
|
|
104
|
+
} else if (state.results.length > 0) {
|
|
105
|
+
results = html`<div part="results" data-testid="search-results">
|
|
106
|
+
${state.results.map(
|
|
107
|
+
(product, i) => html`
|
|
108
|
+
<div
|
|
109
|
+
part="result"
|
|
110
|
+
class="nymbal-search-result-item"
|
|
111
|
+
data-testid="search-result-${product.slug}"
|
|
112
|
+
data-slug=${product.slug}
|
|
113
|
+
aria-selected=${String(i === this.selectedIndex)}
|
|
114
|
+
@click=${() => this.select(product)}
|
|
115
|
+
>
|
|
116
|
+
<div part="result-name">${product.name}</div>
|
|
117
|
+
${product.priceMinor !== null ? html`<div part="result-price">
|
|
118
|
+
${formatPrice(product.priceMinor, product.currency ?? "USD")}
|
|
119
|
+
</div>` : nothing}
|
|
120
|
+
</div>
|
|
121
|
+
`
|
|
122
|
+
)}
|
|
123
|
+
</div>`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return html`
|
|
127
|
+
<input
|
|
128
|
+
part="input"
|
|
129
|
+
data-testid="search-input"
|
|
130
|
+
type="search"
|
|
131
|
+
placeholder=${placeholder}
|
|
132
|
+
.value=${state.query}
|
|
133
|
+
autocomplete="off"
|
|
134
|
+
role="combobox"
|
|
135
|
+
aria-expanded=${String(results !== nothing && state.results.length > 0)}
|
|
136
|
+
aria-haspopup="listbox"
|
|
137
|
+
@input=${this.handleInput}
|
|
138
|
+
@keydown=${this.handleKeydown}
|
|
139
|
+
/>
|
|
140
|
+
${results}
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
handleInput = (e) => {
|
|
144
|
+
const query = e.target.value;
|
|
145
|
+
const debounceMs = Number(this.getAttribute("debounce-ms")) || 300;
|
|
146
|
+
if (this.debounceTimer !== null) clearTimeout(this.debounceTimer);
|
|
147
|
+
this.debounceTimer = setTimeout(() => {
|
|
148
|
+
this.selectedIndex = -1;
|
|
149
|
+
if (query.trim()) {
|
|
150
|
+
void this.client.search.search(query);
|
|
151
|
+
} else {
|
|
152
|
+
this.client.search.clearSearch();
|
|
153
|
+
}
|
|
154
|
+
}, debounceMs);
|
|
155
|
+
};
|
|
156
|
+
handleKeydown = (e) => {
|
|
157
|
+
const { results } = this.client.search.getState();
|
|
158
|
+
if (e.key === "ArrowDown") {
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
this.selectedIndex = Math.min(this.selectedIndex + 1, results.length - 1);
|
|
161
|
+
this.requestUpdate();
|
|
162
|
+
} else if (e.key === "ArrowUp") {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
|
|
165
|
+
this.requestUpdate();
|
|
166
|
+
} else if (e.key === "Enter") {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const selected = this.selectedIndex >= 0 ? results[this.selectedIndex] : void 0;
|
|
169
|
+
if (selected) {
|
|
170
|
+
this.select(selected);
|
|
171
|
+
} else {
|
|
172
|
+
const query = e.target.value.trim();
|
|
173
|
+
if (query) {
|
|
174
|
+
window.location.href = `/search?q=${encodeURIComponent(query)}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (e.key === "Escape") {
|
|
178
|
+
this.client.search.clearSearch();
|
|
179
|
+
this.selectedIndex = -1;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
select(product) {
|
|
183
|
+
this.dispatchEvent(
|
|
184
|
+
new CustomEvent("nymbal:search:selected", {
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true,
|
|
187
|
+
detail: { product, query: product.name }
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
this.client.search.clearSearch();
|
|
191
|
+
}
|
|
192
|
+
disconnectedCallback() {
|
|
193
|
+
super.disconnectedCallback();
|
|
194
|
+
if (this.debounceTimer !== null) {
|
|
195
|
+
clearTimeout(this.debounceTimer);
|
|
196
|
+
this.debounceTimer = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export {
|
|
202
|
+
NymbalSearchBar
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=chunk-4COVK727.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/search-bar.ts"],"sourcesContent":["import { css, html, nothing } from 'lit'\nimport type { DenormalisedProduct } from '@nymbalhq/sdk'\nimport { baseStyles, NymbalElement, propertiesOf, type ElementSchema } from '../base-element.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-search-bar',\n reactName: 'SearchBar',\n attributes: [\n { name: 'placeholder', prop: 'placeholder', type: 'string' },\n { name: 'debounce-ms', prop: 'debounceMs', type: 'number' },\n ],\n events: [\n { name: 'nymbal:search:selected', prop: 'onSearchSelect', detail: '{ product: unknown }' },\n ],\n parts: ['input', 'results', 'loading', 'result', 'result-name', 'result-price'],\n slots: ['loading'],\n children: false,\n}\n\n/** What a search bar shows before any search, and on a server. */\nconst NO_SEARCH = { query: '', loading: false, results: [] as DenormalisedProduct[] }\n\nfunction formatPrice(minor: number, currency: string): string {\n return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(minor / 100)\n}\n\nexport class NymbalSearchBar extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n\n static override styles = [\n baseStyles,\n css`\n :host {\n display: block;\n position: relative;\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n }\n [part~='input'] {\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n font-size: var(--nymbal-font-size-base, 1rem);\n width: 100%;\n padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);\n border: 1px solid var(--nymbal-color-border, #e4e4e7);\n border-radius: var(--nymbal-radius-md, 0.375rem);\n outline: none;\n transition: border-color var(--nymbal-transition-fast, 150ms ease);\n }\n [part~='input']:focus {\n border-color: var(--nymbal-color-accent, #2563eb);\n }\n [part~='results'] {\n position: absolute;\n top: 100%;\n left: 0;\n right: 0;\n background: var(--nymbal-color-surface, #ffffff);\n border: 1px solid var(--nymbal-color-border, #e4e4e7);\n border-top: none;\n border-radius: 0 0 var(--nymbal-radius-md, 0.375rem) var(--nymbal-radius-md, 0.375rem);\n box-shadow: var(--nymbal-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1));\n z-index: var(--nymbal-z-dropdown, 100);\n max-height: 300px;\n overflow-y: auto;\n }\n [part~='result'] {\n padding: var(--nymbal-spacing-sm, 0.5rem) var(--nymbal-spacing-md, 1rem);\n cursor: pointer;\n transition: background var(--nymbal-transition-fast, 150ms ease);\n }\n [part~='result']:hover,\n [part~='result'][aria-selected='true'] {\n background: var(--nymbal-color-secondary, #f4f4f5);\n }\n [part~='result-name'] {\n font-weight: var(--nymbal-font-weight-medium, 500);\n }\n [part~='result-price'] {\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n color: var(--nymbal-color-text-muted, #71717a);\n }\n [part~='loading'] {\n padding: var(--nymbal-spacing-md, 1rem);\n text-align: center;\n color: var(--nymbal-color-text-muted, #71717a);\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n }\n `,\n ]\n\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private selectedIndex = -1\n\n protected override stores() {\n return [this.client.search]\n }\n\n protected override render() {\n const state = this.readsStores ? this.client.search.getState() : NO_SEARCH\n const placeholder = this.getAttribute('placeholder') ?? 'Search products...'\n\n let results: unknown = nothing\n // A page that shows the search itself (`nymbal-search-results`) is not covered by the same results as suggestions.\n if (state.query && document.querySelector('nymbal-search-results') === null) {\n if (state.loading) {\n results = html`<div part=\"results\" data-testid=\"search-results\">\n <div part=\"loading\"><slot name=\"loading\">Searching...</slot></div>\n </div>`\n } else if (state.results.length > 0) {\n results = html`<div part=\"results\" data-testid=\"search-results\">\n ${state.results.map(\n (product, i) => html`\n <div\n part=\"result\"\n class=\"nymbal-search-result-item\"\n data-testid=\"search-result-${product.slug}\"\n data-slug=${product.slug}\n aria-selected=${String(i === this.selectedIndex)}\n @click=${() => this.select(product)}\n >\n <div part=\"result-name\">${product.name}</div>\n ${product.priceMinor !== null\n ? html`<div part=\"result-price\">\n ${formatPrice(product.priceMinor, product.currency ?? 'USD')}\n </div>`\n : nothing}\n </div>\n `,\n )}\n </div>`\n }\n }\n\n return html`\n <input\n part=\"input\"\n data-testid=\"search-input\"\n type=\"search\"\n placeholder=${placeholder}\n .value=${state.query}\n autocomplete=\"off\"\n role=\"combobox\"\n aria-expanded=${String(results !== nothing && state.results.length > 0)}\n aria-haspopup=\"listbox\"\n @input=${this.handleInput}\n @keydown=${this.handleKeydown}\n />\n ${results}\n `\n }\n\n private handleInput = (e: Event): void => {\n const query = (e.target as HTMLInputElement).value\n const debounceMs = Number(this.getAttribute('debounce-ms')) || 300\n if (this.debounceTimer !== null) clearTimeout(this.debounceTimer)\n this.debounceTimer = setTimeout(() => {\n this.selectedIndex = -1\n if (query.trim()) {\n void this.client.search.search(query)\n } else {\n this.client.search.clearSearch()\n }\n }, debounceMs)\n }\n\n private handleKeydown = (e: KeyboardEvent): void => {\n const { results } = this.client.search.getState()\n if (e.key === 'ArrowDown') {\n e.preventDefault()\n this.selectedIndex = Math.min(this.selectedIndex + 1, results.length - 1)\n this.requestUpdate()\n } else if (e.key === 'ArrowUp') {\n e.preventDefault()\n this.selectedIndex = Math.max(this.selectedIndex - 1, -1)\n this.requestUpdate()\n } else if (e.key === 'Enter') {\n e.preventDefault()\n const selected = this.selectedIndex >= 0 ? results[this.selectedIndex] : undefined\n if (selected) {\n this.select(selected)\n } else {\n const query = (e.target as HTMLInputElement).value.trim()\n if (query) {\n window.location.href = `/search?q=${encodeURIComponent(query)}`\n }\n }\n } else if (e.key === 'Escape') {\n this.client.search.clearSearch()\n this.selectedIndex = -1\n }\n }\n\n private select(product: DenormalisedProduct): void {\n this.dispatchEvent(\n new CustomEvent('nymbal:search:selected', {\n bubbles: true,\n composed: true,\n detail: { product, query: product.name },\n }),\n )\n this.client.search.clearSearch()\n }\n\n override disconnectedCallback(): void {\n super.disconnectedCallback()\n if (this.debounceTimer !== null) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalProductCard,
|
|
4
|
+
cardProductOf
|
|
5
|
+
} from "./chunk-RZVFA3N5.js";
|
|
6
|
+
import {
|
|
7
|
+
buttonStyles
|
|
8
|
+
} from "./chunk-GHXTJ2CF.js";
|
|
9
|
+
import {
|
|
10
|
+
NymbalElement,
|
|
11
|
+
baseStyles,
|
|
12
|
+
parsedJson,
|
|
13
|
+
propertiesOf
|
|
14
|
+
} from "./chunk-EYDMJNNK.js";
|
|
15
|
+
|
|
16
|
+
// src/components/product-grid.ts
|
|
17
|
+
import { css, html, nothing } from "lit";
|
|
18
|
+
var SCHEMA = {
|
|
19
|
+
tag: "nymbal-product-grid",
|
|
20
|
+
reactName: "ProductGrid",
|
|
21
|
+
// `products`: the cards a server read. `source="product-list"`: the grid is a listing's, and follows the product-list
|
|
22
|
+
// store once it settles (a sort, a filter, load more); without it the grid is static. `has-more`: the server's page
|
|
23
|
+
// has a next one, so its render shows load more exactly when the store will. `lazy`: the grid sits below the fold
|
|
24
|
+
// (a product page's related grid), so no card is eager and its images never outrank the page's LCP image.
|
|
25
|
+
attributes: [
|
|
26
|
+
{ name: "products", prop: "products", type: "json" },
|
|
27
|
+
{ name: "source", prop: "source", type: "string" },
|
|
28
|
+
{ name: "has-more", prop: "hasMore", type: "boolean" },
|
|
29
|
+
{ name: "lazy", prop: "lazy", type: "boolean" }
|
|
30
|
+
],
|
|
31
|
+
events: [],
|
|
32
|
+
parts: ["grid", "empty", "count", "load-more"],
|
|
33
|
+
slots: ["empty"],
|
|
34
|
+
children: false
|
|
35
|
+
};
|
|
36
|
+
var EAGER_CARDS = 4;
|
|
37
|
+
var NymbalProductGrid = class extends NymbalElement {
|
|
38
|
+
static schema = SCHEMA;
|
|
39
|
+
static uses = [NymbalProductCard];
|
|
40
|
+
static properties = propertiesOf(SCHEMA);
|
|
41
|
+
static styles = [
|
|
42
|
+
baseStyles,
|
|
43
|
+
buttonStyles,
|
|
44
|
+
css`
|
|
45
|
+
:host {
|
|
46
|
+
display: block;
|
|
47
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
48
|
+
color: var(--nymbal-color-text, #18181b);
|
|
49
|
+
}
|
|
50
|
+
[part~='count'] {
|
|
51
|
+
margin: 0 0 var(--nymbal-spacing-md, 1rem);
|
|
52
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
53
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
54
|
+
}
|
|
55
|
+
[part~='grid'] {
|
|
56
|
+
display: grid;
|
|
57
|
+
gap: var(--nymbal-spacing-md, 1rem);
|
|
58
|
+
grid-template-columns: 1fr;
|
|
59
|
+
}
|
|
60
|
+
@media (min-width: 641px) {
|
|
61
|
+
[part~='grid'] {
|
|
62
|
+
grid-template-columns: repeat(2, 1fr);
|
|
63
|
+
gap: var(--nymbal-spacing-lg, 1.5rem);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
@media (min-width: 1025px) {
|
|
67
|
+
[part~='grid'] {
|
|
68
|
+
grid-template-columns: repeat(4, 1fr);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
[part~='empty'] {
|
|
72
|
+
padding: var(--nymbal-spacing-xl, 2rem) 0;
|
|
73
|
+
text-align: center;
|
|
74
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
75
|
+
}
|
|
76
|
+
.more {
|
|
77
|
+
display: flex;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
margin-top: var(--nymbal-spacing-xl, 2rem);
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
];
|
|
83
|
+
get listing() {
|
|
84
|
+
return this.source === "product-list";
|
|
85
|
+
}
|
|
86
|
+
stores() {
|
|
87
|
+
return this.listing ? [this.client.productList] : [];
|
|
88
|
+
}
|
|
89
|
+
storeSettled() {
|
|
90
|
+
if (this.client.productList.getState().loading) return;
|
|
91
|
+
this.products = void 0;
|
|
92
|
+
this.hasMore = void 0;
|
|
93
|
+
}
|
|
94
|
+
/** The server's cards while they stand, else (a listing with a client) the product-list store's. */
|
|
95
|
+
shown() {
|
|
96
|
+
const given = parsedJson(this, "products", this.products);
|
|
97
|
+
if (given === void 0 && this.listing && this.readsStores) {
|
|
98
|
+
const state = this.client.productList.getState();
|
|
99
|
+
return {
|
|
100
|
+
products: state.products,
|
|
101
|
+
hasMore: state.pagination.hasMore,
|
|
102
|
+
loading: state.loading
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
products: Array.isArray(given) ? given : [],
|
|
107
|
+
hasMore: this.hasMore === true,
|
|
108
|
+
loading: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
render() {
|
|
112
|
+
const { products, hasMore, loading } = this.shown();
|
|
113
|
+
const count = products.length;
|
|
114
|
+
return html`
|
|
115
|
+
${this.listing ? html`<p part="count">${count} product${count === 1 ? "" : "s"}</p>` : nothing}
|
|
116
|
+
${count === 0 ? loading ? nothing : html`<div part="empty"><slot name="empty">No products found.</slot></div>` : html`
|
|
117
|
+
<div part="grid">
|
|
118
|
+
${products.map(
|
|
119
|
+
(p, i) => html`<nymbal-product-card
|
|
120
|
+
product=${JSON.stringify(
|
|
121
|
+
cardProductOf(p)
|
|
122
|
+
)}
|
|
123
|
+
?eager=${this.lazy !== true && i < EAGER_CARDS}
|
|
124
|
+
></nymbal-product-card>`
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
`}
|
|
128
|
+
${this.listing && hasMore ? html`
|
|
129
|
+
<div class="more">
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
part="load-more"
|
|
133
|
+
class="btn btn-secondary"
|
|
134
|
+
data-testid="load-more"
|
|
135
|
+
?disabled=${loading}
|
|
136
|
+
@click=${() => void this.client.productList.loadMore()}
|
|
137
|
+
>
|
|
138
|
+
Load more
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
` : nothing}
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
NymbalProductGrid
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=chunk-5VWZ2U5K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/product-grid.ts"],"sourcesContent":["import { css, html, nothing } from 'lit'\nimport {\n baseStyles,\n NymbalElement,\n parsedJson,\n propertiesOf,\n type ElementSchema,\n} from '../base-element.js'\nimport { buttonStyles } from '../view-styles.js'\nimport { cardProductOf, NymbalProductCard } from './product-card.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-product-grid',\n reactName: 'ProductGrid',\n // `products`: the cards a server read. `source=\"product-list\"`: the grid is a listing's, and follows the product-list\n // store once it settles (a sort, a filter, load more); without it the grid is static. `has-more`: the server's page\n // has a next one, so its render shows load more exactly when the store will. `lazy`: the grid sits below the fold\n // (a product page's related grid), so no card is eager and its images never outrank the page's LCP image.\n attributes: [\n { name: 'products', prop: 'products', type: 'json' },\n { name: 'source', prop: 'source', type: 'string' },\n { name: 'has-more', prop: 'hasMore', type: 'boolean' },\n { name: 'lazy', prop: 'lazy', type: 'boolean' },\n ],\n events: [],\n parts: ['grid', 'empty', 'count', 'load-more'],\n slots: ['empty'],\n children: false,\n}\n\n/** The first row of a wide grid: those cards' images are the page's likely LCP. */\nconst EAGER_CARDS = 4\n\ninterface Shown {\n products: unknown[]\n hasMore: boolean\n loading: boolean\n}\n\n/** A grid of `nymbal-product-card`s, a listing's (following the product-list store) or a static one. */\nexport class NymbalProductGrid extends NymbalElement {\n static override schema = SCHEMA\n static uses = [NymbalProductCard]\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\n buttonStyles,\n css`\n :host {\n display: block;\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n color: var(--nymbal-color-text, #18181b);\n }\n [part~='count'] {\n margin: 0 0 var(--nymbal-spacing-md, 1rem);\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n color: var(--nymbal-color-text-muted, #71717a);\n }\n [part~='grid'] {\n display: grid;\n gap: var(--nymbal-spacing-md, 1rem);\n grid-template-columns: 1fr;\n }\n @media (min-width: 641px) {\n [part~='grid'] {\n grid-template-columns: repeat(2, 1fr);\n gap: var(--nymbal-spacing-lg, 1.5rem);\n }\n }\n @media (min-width: 1025px) {\n [part~='grid'] {\n grid-template-columns: repeat(4, 1fr);\n }\n }\n [part~='empty'] {\n padding: var(--nymbal-spacing-xl, 2rem) 0;\n text-align: center;\n color: var(--nymbal-color-text-muted, #71717a);\n }\n .more {\n display: flex;\n justify-content: center;\n margin-top: var(--nymbal-spacing-xl, 2rem);\n }\n `,\n ]\n\n declare products: unknown\n declare source: string | undefined\n declare hasMore: boolean | undefined\n declare lazy: boolean | undefined\n\n private get listing(): boolean {\n return this.source === 'product-list'\n }\n\n protected override stores() {\n return this.listing ? [this.client.productList] : []\n }\n\n protected override storeSettled(): void {\n // A store already settled when this grid connected may be loading this page's listing now; its last answer was\n // another listing's (a soft navigation), so the server's cards stand until this load settles.\n if (this.client.productList.getState().loading) return\n this.products = undefined\n this.hasMore = undefined\n }\n\n /** The server's cards while they stand, else (a listing with a client) the product-list store's. */\n private shown(): Shown {\n const given = parsedJson<unknown[]>(this, 'products', this.products)\n if (given === undefined && this.listing && this.readsStores) {\n const state = this.client.productList.getState()\n return {\n products: state.products,\n hasMore: state.pagination.hasMore,\n loading: state.loading,\n }\n }\n return {\n products: Array.isArray(given) ? given : [],\n hasMore: this.hasMore === true,\n loading: false,\n }\n }\n\n protected override render() {\n const { products, hasMore, loading } = this.shown()\n const count = products.length\n return html`\n ${this.listing\n ? html`<p part=\"count\">${count} product${count === 1 ? '' : 's'}</p>`\n : nothing}\n ${count === 0\n ? loading\n ? nothing\n : html`<div part=\"empty\"><slot name=\"empty\">No products found.</slot></div>`\n : html`\n <div part=\"grid\">\n ${products.map(\n (p, i) =>\n html`<nymbal-product-card\n product=${JSON.stringify(\n cardProductOf(p as Parameters<typeof cardProductOf>[0]),\n )}\n ?eager=${this.lazy !== true && i < EAGER_CARDS}\n ></nymbal-product-card>`,\n )}\n </div>\n `}\n ${this.listing && hasMore\n ? html`\n <div class=\"more\">\n <button\n type=\"button\"\n part=\"load-more\"\n class=\"btn btn-secondary\"\n data-testid=\"load-more\"\n ?disabled=${loading}\n @click=${() => void this.client.productList.loadMore()}\n >\n Load more\n </button>\n </div>\n `\n : nothing}\n `\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,KAAK,MAAM,eAAe;AAWnC,IAAM,SAAwB;AAAA,EAC5B,KAAK;AAAA,EACL,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,EAKX,YAAY;AAAA,IACV,EAAE,MAAM,YAAY,MAAM,YAAY,MAAM,OAAO;AAAA,IACnD,EAAE,MAAM,UAAU,MAAM,UAAU,MAAM,SAAS;AAAA,IACjD,EAAE,MAAM,YAAY,MAAM,WAAW,MAAM,UAAU;AAAA,IACrD,EAAE,MAAM,QAAQ,MAAM,QAAQ,MAAM,UAAU;AAAA,EAChD;AAAA,EACA,QAAQ,CAAC;AAAA,EACT,OAAO,CAAC,QAAQ,SAAS,SAAS,WAAW;AAAA,EAC7C,OAAO,CAAC,OAAO;AAAA,EACf,UAAU;AACZ;AAGA,IAAM,cAAc;AASb,IAAM,oBAAN,cAAgC,cAAc;AAAA,EACnD,OAAgB,SAAS;AAAA,EACzB,OAAO,OAAO,CAAC,iBAAiB;AAAA,EAChC,OAAgB,aAAa,aAAa,MAAM;AAAA,EAChD,OAAgB,SAAS;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsCF;AAAA,EAOA,IAAY,UAAmB;AAC7B,WAAO,KAAK,WAAW;AAAA,EACzB;AAAA,EAEmB,SAAS;AAC1B,WAAO,KAAK,UAAU,CAAC,KAAK,OAAO,WAAW,IAAI,CAAC;AAAA,EACrD;AAAA,EAEmB,eAAqB;AAGtC,QAAI,KAAK,OAAO,YAAY,SAAS,EAAE,QAAS;AAChD,SAAK,WAAW;AAChB,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGQ,QAAe;AACrB,UAAM,QAAQ,WAAsB,MAAM,YAAY,KAAK,QAAQ;AACnE,QAAI,UAAU,UAAa,KAAK,WAAW,KAAK,aAAa;AAC3D,YAAM,QAAQ,KAAK,OAAO,YAAY,SAAS;AAC/C,aAAO;AAAA,QACL,UAAU,MAAM;AAAA,QAChB,SAAS,MAAM,WAAW;AAAA,QAC1B,SAAS,MAAM;AAAA,MACjB;AAAA,IACF;AACA,WAAO;AAAA,MACL,UAAU,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC;AAAA,MAC1C,SAAS,KAAK,YAAY;AAAA,MAC1B,SAAS;AAAA,IACX;AAAA,EACF;AAAA,EAEmB,SAAS;AAC1B,UAAM,EAAE,UAAU,SAAS,QAAQ,IAAI,KAAK,MAAM;AAClD,UAAM,QAAQ,SAAS;AACvB,WAAO;AAAA,QACH,KAAK,UACH,uBAAuB,KAAK,WAAW,UAAU,IAAI,KAAK,GAAG,SAC7D,OAAO;AAAA,QACT,UAAU,IACR,UACE,UACA,6EACF;AAAA;AAAA,gBAEM,SAAS;AAAA,MACT,CAAC,GAAG,MACF;AAAA,8BACY,KAAK;AAAA,QACb,cAAc,CAAwC;AAAA,MACxD,CAAC;AAAA,6BACQ,KAAK,SAAS,QAAQ,IAAI,WAAW;AAAA;AAAA,IAEpD,CAAC;AAAA;AAAA,WAEJ;AAAA,QACH,KAAK,WAAW,UACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAOkB,OAAO;AAAA,yBACV,MAAM,KAAK,KAAK,OAAO,YAAY,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,cAM5D,OAAO;AAAA;AAAA,EAEf;AACF;","names":[]}
|