@nymbalhq/web-components 1.0.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 +37 -105
- package/dist/index.js +130 -1098
- 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,154 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
formatPrice
|
|
4
|
+
} from "./chunk-RVSKX2RX.js";
|
|
5
|
+
import {
|
|
6
|
+
navigateOnClick
|
|
7
|
+
} from "./chunk-LO2EW5YP.js";
|
|
8
|
+
import {
|
|
9
|
+
NymbalElement,
|
|
10
|
+
baseStyles,
|
|
11
|
+
propertiesOf
|
|
12
|
+
} from "./chunk-EYDMJNNK.js";
|
|
13
|
+
|
|
14
|
+
// src/components/product-card.ts
|
|
15
|
+
import { css, html, nothing } from "lit";
|
|
16
|
+
var SCHEMA = {
|
|
17
|
+
tag: "nymbal-product-card",
|
|
18
|
+
reactName: "ProductCard",
|
|
19
|
+
// `eager`: the card is in a grid's first row, so its image is the page's likely LCP and is fetched at once.
|
|
20
|
+
attributes: [
|
|
21
|
+
{ name: "product", prop: "product", type: "json" },
|
|
22
|
+
{ name: "eager", prop: "eager", type: "boolean" }
|
|
23
|
+
],
|
|
24
|
+
events: [],
|
|
25
|
+
parts: ["card", "media", "image", "body", "name", "price"],
|
|
26
|
+
slots: [],
|
|
27
|
+
children: false
|
|
28
|
+
};
|
|
29
|
+
function cardImageOf(p) {
|
|
30
|
+
if (typeof p.imageUrl === "string" && p.imageUrl !== "") return { url: p.imageUrl, alt: p.name };
|
|
31
|
+
const media = p.media?.find((m) => typeof m.url === "string" && m.url !== "");
|
|
32
|
+
if (media !== void 0) return { url: media.url, alt: media.alt ?? p.name };
|
|
33
|
+
return { url: `https://picsum.photos/seed/${encodeURIComponent(p.slug)}/600/600`, alt: p.name };
|
|
34
|
+
}
|
|
35
|
+
function cardProductOf(p) {
|
|
36
|
+
const list = Array.isArray(p.media) ? p.media : [];
|
|
37
|
+
const media = list.find((m) => typeof m.url === "string" && m.url !== "");
|
|
38
|
+
const image = typeof p.imageUrl === "string" && p.imageUrl !== "" ? { imageUrl: p.imageUrl } : media === void 0 ? {} : { media: [{ url: media.url, ...media.alt !== void 0 && { alt: media.alt } }] };
|
|
39
|
+
return {
|
|
40
|
+
slug: p.slug,
|
|
41
|
+
name: p.name,
|
|
42
|
+
priceMinor: p.priceMinor ?? null,
|
|
43
|
+
priceRange: p.priceRange ?? null,
|
|
44
|
+
currency: p.currency ?? null,
|
|
45
|
+
...image
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function parse(raw) {
|
|
49
|
+
if (raw === void 0 || raw === "") return null;
|
|
50
|
+
try {
|
|
51
|
+
const value = JSON.parse(raw);
|
|
52
|
+
return value !== null && typeof value.slug === "string" ? value : null;
|
|
53
|
+
} catch {
|
|
54
|
+
console.warn("[nymbal-product-card] the product attribute is not JSON", raw);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function priceOf(p) {
|
|
59
|
+
const from = p.priceRange?.minMinor ?? p.priceMinor;
|
|
60
|
+
return from == null ? null : formatPrice(from, p.currency ?? "GBP");
|
|
61
|
+
}
|
|
62
|
+
var NymbalProductCard = class extends NymbalElement {
|
|
63
|
+
static schema = SCHEMA;
|
|
64
|
+
static properties = propertiesOf(SCHEMA);
|
|
65
|
+
static styles = [
|
|
66
|
+
baseStyles,
|
|
67
|
+
css`
|
|
68
|
+
:host {
|
|
69
|
+
display: block;
|
|
70
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
71
|
+
}
|
|
72
|
+
[part~='card'] {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
height: 100%;
|
|
76
|
+
color: var(--nymbal-color-text, #18181b);
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
border: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
79
|
+
border-radius: var(--nymbal-radius-lg, 0.75rem);
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
background: var(--nymbal-color-surface, #ffffff);
|
|
82
|
+
transition: box-shadow var(--nymbal-transition-fast, 150ms ease);
|
|
83
|
+
}
|
|
84
|
+
[part~='card']:hover {
|
|
85
|
+
box-shadow: var(--nymbal-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.07));
|
|
86
|
+
}
|
|
87
|
+
[part~='card']:focus-visible {
|
|
88
|
+
outline: 2px solid var(--nymbal-color-accent, #2563eb);
|
|
89
|
+
outline-offset: 2px;
|
|
90
|
+
}
|
|
91
|
+
[part~='media'] {
|
|
92
|
+
aspect-ratio: 1 / 1;
|
|
93
|
+
background: #f4f4f5;
|
|
94
|
+
}
|
|
95
|
+
[part~='image'] {
|
|
96
|
+
display: block;
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 100%;
|
|
99
|
+
object-fit: cover;
|
|
100
|
+
}
|
|
101
|
+
[part~='body'] {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: var(--nymbal-spacing-xs, 0.25rem);
|
|
105
|
+
padding: var(--nymbal-spacing-md, 1rem);
|
|
106
|
+
}
|
|
107
|
+
[part~='name'] {
|
|
108
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
109
|
+
font-weight: var(--nymbal-font-weight-medium, 500);
|
|
110
|
+
}
|
|
111
|
+
[part~='price'] {
|
|
112
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
113
|
+
font-weight: var(--nymbal-font-weight-bold, 700);
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
];
|
|
117
|
+
render() {
|
|
118
|
+
const p = parse(this.product);
|
|
119
|
+
if (p === null) return nothing;
|
|
120
|
+
const image = cardImageOf(p);
|
|
121
|
+
const price = priceOf(p);
|
|
122
|
+
return html`
|
|
123
|
+
<a
|
|
124
|
+
part="card"
|
|
125
|
+
href="/product/${encodeURIComponent(p.slug)}"
|
|
126
|
+
data-testid="product-card-${p.slug}"
|
|
127
|
+
@click=${navigateOnClick}
|
|
128
|
+
>
|
|
129
|
+
<div part="media">
|
|
130
|
+
<img
|
|
131
|
+
part="image"
|
|
132
|
+
src=${image.url}
|
|
133
|
+
alt=${image.alt}
|
|
134
|
+
loading=${this.eager === true ? "eager" : "lazy"}
|
|
135
|
+
fetchpriority=${this.eager === true ? "high" : nothing}
|
|
136
|
+
width="600"
|
|
137
|
+
height="600"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
<div part="body">
|
|
141
|
+
<span part="name">${p.name}</span>
|
|
142
|
+
${price === null ? nothing : html`<span part="price">${price}</span>`}
|
|
143
|
+
</div>
|
|
144
|
+
</a>
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
cardImageOf,
|
|
151
|
+
cardProductOf,
|
|
152
|
+
NymbalProductCard
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=chunk-RZVFA3N5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/product-card.ts"],"sourcesContent":["import { css, html, nothing } from 'lit'\nimport { baseStyles, NymbalElement, propertiesOf, type ElementSchema } from '../base-element.js'\nimport { formatPrice } from '../format.js'\nimport { navigateOnClick } from '../navigate.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-product-card',\n reactName: 'ProductCard',\n // `eager`: the card is in a grid's first row, so its image is the page's likely LCP and is fetched at once.\n attributes: [\n { name: 'product', prop: 'product', type: 'json' },\n { name: 'eager', prop: 'eager', type: 'boolean' },\n ],\n events: [],\n parts: ['card', 'media', 'image', 'body', 'name', 'price'],\n slots: [],\n children: false,\n}\n\n/** The fields a card reads; a search result or a listing product carries them. */\nexport interface CardProduct {\n slug: string\n name: string\n priceMinor?: number | null\n priceRange?: { minMinor: number; maxMinor: number } | null\n currency?: string | null\n imageUrl?: string | null\n media?: Array<{ url?: string; alt?: string }>\n}\n\n/**\n * The product's own image, else a placeholder seeded by its slug so a card without media keeps its shape. Exported so\n * a page preloads exactly the image its first card requests.\n */\nexport function cardImageOf(p: CardProduct): { url: string; alt: string } {\n if (typeof p.imageUrl === 'string' && p.imageUrl !== '') return { url: p.imageUrl, alt: p.name }\n const media = p.media?.find((m) => typeof m.url === 'string' && m.url !== '')\n if (media !== undefined) return { url: media.url!, alt: media.alt ?? p.name }\n return { url: `https://picsum.photos/seed/${encodeURIComponent(p.slug)}/600/600`, alt: p.name }\n}\n\n/**\n * A product as a card reads it, and nothing else: a listing's products carry their variants, descriptions and\n * categories, which a page of cards would otherwise put in its HTML once per card.\n */\nexport function cardProductOf(p: Omit<CardProduct, 'media'> & { media?: unknown }): CardProduct {\n const list = Array.isArray(p.media) ? (p.media as NonNullable<CardProduct['media']>) : []\n const media = list.find((m) => typeof m.url === 'string' && m.url !== '')\n const image =\n typeof p.imageUrl === 'string' && p.imageUrl !== ''\n ? { imageUrl: p.imageUrl }\n : media === undefined\n ? {}\n : { media: [{ url: media.url!, ...(media.alt !== undefined && { alt: media.alt }) }] }\n return {\n slug: p.slug,\n name: p.name,\n priceMinor: p.priceMinor ?? null,\n priceRange: p.priceRange ?? null,\n currency: p.currency ?? null,\n ...image,\n }\n}\n\nfunction parse(raw: string | undefined): CardProduct | null {\n if (raw === undefined || raw === '') return null\n try {\n const value = JSON.parse(raw) as Partial<CardProduct> | null\n return value !== null && typeof value.slug === 'string' ? (value as CardProduct) : null\n } catch {\n console.warn('[nymbal-product-card] the product attribute is not JSON', raw)\n return null\n }\n}\n\n/** The \"from\" price, which the price sorts order by: the cheapest variant's when the product has a range. */\nfunction priceOf(p: CardProduct): string | null {\n const from = p.priceRange?.minMinor ?? p.priceMinor\n return from == null ? null : formatPrice(from, p.currency ?? 'GBP')\n}\n\n/** One product as a link to its page: image, name and price. */\nexport class NymbalProductCard extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\n css`\n :host {\n display: block;\n font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);\n }\n [part~='card'] {\n display: flex;\n flex-direction: column;\n height: 100%;\n color: var(--nymbal-color-text, #18181b);\n text-decoration: none;\n border: 1px solid var(--nymbal-color-border, #e4e4e7);\n border-radius: var(--nymbal-radius-lg, 0.75rem);\n overflow: hidden;\n background: var(--nymbal-color-surface, #ffffff);\n transition: box-shadow var(--nymbal-transition-fast, 150ms ease);\n }\n [part~='card']:hover {\n box-shadow: var(--nymbal-shadow-md, 0 4px 6px rgba(0, 0, 0, 0.07));\n }\n [part~='card']:focus-visible {\n outline: 2px solid var(--nymbal-color-accent, #2563eb);\n outline-offset: 2px;\n }\n [part~='media'] {\n aspect-ratio: 1 / 1;\n background: #f4f4f5;\n }\n [part~='image'] {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n [part~='body'] {\n display: flex;\n flex-direction: column;\n gap: var(--nymbal-spacing-xs, 0.25rem);\n padding: var(--nymbal-spacing-md, 1rem);\n }\n [part~='name'] {\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n font-weight: var(--nymbal-font-weight-medium, 500);\n }\n [part~='price'] {\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n font-weight: var(--nymbal-font-weight-bold, 700);\n }\n `,\n ]\n\n declare product: string | undefined\n declare eager: boolean | undefined\n\n protected override render() {\n const p = parse(this.product)\n if (p === null) return nothing\n const image = cardImageOf(p)\n const price = priceOf(p)\n return html`\n <a\n part=\"card\"\n href=\"/product/${encodeURIComponent(p.slug)}\"\n data-testid=\"product-card-${p.slug}\"\n @click=${navigateOnClick}\n >\n <div part=\"media\">\n <img\n part=\"image\"\n src=${image.url}\n alt=${image.alt}\n loading=${this.eager === true ? 'eager' : 'lazy'}\n fetchpriority=${this.eager === true ? 'high' : nothing}\n width=\"600\"\n height=\"600\"\n />\n </div>\n <div part=\"body\">\n <span part=\"name\">${p.name}</span>\n ${price === null ? nothing : html`<span part=\"price\">${price}</span>`}\n </div>\n </a>\n `\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
formatPrice
|
|
4
|
+
} from "./chunk-RVSKX2RX.js";
|
|
5
|
+
import {
|
|
6
|
+
NymbalElement,
|
|
7
|
+
baseStyles,
|
|
8
|
+
parsedJson,
|
|
9
|
+
propertiesOf
|
|
10
|
+
} from "./chunk-EYDMJNNK.js";
|
|
11
|
+
|
|
12
|
+
// src/components/order-summary.ts
|
|
13
|
+
import { css, html, nothing } from "lit";
|
|
14
|
+
var SCHEMA = {
|
|
15
|
+
tag: "nymbal-order-summary",
|
|
16
|
+
reactName: "OrderSummary",
|
|
17
|
+
// An order's lines and money as `GET /api/orders/:n` serves them; the element adds nothing up itself.
|
|
18
|
+
attributes: [
|
|
19
|
+
{ name: "items", prop: "items", type: "json" },
|
|
20
|
+
{ name: "subtotal-minor", prop: "subtotalMinor", type: "number" },
|
|
21
|
+
{ name: "discount-minor", prop: "discountMinor", type: "number" },
|
|
22
|
+
{ name: "shipping-minor", prop: "shippingMinor", type: "number" },
|
|
23
|
+
{ name: "tax-minor", prop: "taxMinor", type: "number" },
|
|
24
|
+
{ name: "total-minor", prop: "totalMinor", type: "number" },
|
|
25
|
+
{ name: "currency", prop: "currency", type: "string" }
|
|
26
|
+
],
|
|
27
|
+
events: [],
|
|
28
|
+
parts: ["summary", "line", "totals", "subtotal", "discount", "shipping", "tax", "total"],
|
|
29
|
+
slots: [],
|
|
30
|
+
children: false
|
|
31
|
+
};
|
|
32
|
+
var known = (n) => n !== void 0 && !Number.isNaN(n);
|
|
33
|
+
var NymbalOrderSummary = class extends NymbalElement {
|
|
34
|
+
static schema = SCHEMA;
|
|
35
|
+
static properties = propertiesOf(SCHEMA);
|
|
36
|
+
static styles = [
|
|
37
|
+
baseStyles,
|
|
38
|
+
css`
|
|
39
|
+
:host {
|
|
40
|
+
display: block;
|
|
41
|
+
font-family: var(--nymbal-font-family, system-ui, -apple-system, sans-serif);
|
|
42
|
+
color: var(--nymbal-color-text, #18181b);
|
|
43
|
+
}
|
|
44
|
+
[part~='summary'] {
|
|
45
|
+
padding: var(--nymbal-spacing-lg, 1.5rem);
|
|
46
|
+
background-color: var(--nymbal-color-surface, #ffffff);
|
|
47
|
+
border: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
48
|
+
border-radius: var(--nymbal-radius-lg, 0.75rem);
|
|
49
|
+
}
|
|
50
|
+
h2 {
|
|
51
|
+
margin: 0 0 var(--nymbal-spacing-md, 1rem);
|
|
52
|
+
line-height: 1.2;
|
|
53
|
+
font-size: var(--nymbal-font-size-lg, 1.125rem);
|
|
54
|
+
font-weight: var(--nymbal-font-weight-bold, 700);
|
|
55
|
+
}
|
|
56
|
+
ul {
|
|
57
|
+
margin: 0 0 var(--nymbal-spacing-md, 1rem);
|
|
58
|
+
padding: 0;
|
|
59
|
+
list-style: none;
|
|
60
|
+
}
|
|
61
|
+
[part~='line'],
|
|
62
|
+
[part~='subtotal'],
|
|
63
|
+
[part~='discount'],
|
|
64
|
+
[part~='shipping'],
|
|
65
|
+
[part~='tax'],
|
|
66
|
+
[part~='total'] {
|
|
67
|
+
display: flex;
|
|
68
|
+
justify-content: space-between;
|
|
69
|
+
gap: var(--nymbal-spacing-md, 1rem);
|
|
70
|
+
padding-block: var(--nymbal-spacing-xs, 0.25rem);
|
|
71
|
+
font-size: var(--nymbal-font-size-sm, 0.875rem);
|
|
72
|
+
}
|
|
73
|
+
.qty {
|
|
74
|
+
color: var(--nymbal-color-text-muted, #71717a);
|
|
75
|
+
}
|
|
76
|
+
[part~='totals'] {
|
|
77
|
+
margin: 0;
|
|
78
|
+
padding-top: var(--nymbal-spacing-sm, 0.5rem);
|
|
79
|
+
border-top: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
80
|
+
}
|
|
81
|
+
dt,
|
|
82
|
+
dd {
|
|
83
|
+
margin: 0;
|
|
84
|
+
}
|
|
85
|
+
[part~='discount'] dd {
|
|
86
|
+
color: var(--nymbal-color-success, #16a34a);
|
|
87
|
+
}
|
|
88
|
+
[part~='total'] {
|
|
89
|
+
margin-top: var(--nymbal-spacing-sm, 0.5rem);
|
|
90
|
+
padding-top: var(--nymbal-spacing-sm, 0.5rem);
|
|
91
|
+
border-top: 1px solid var(--nymbal-color-border, #e4e4e7);
|
|
92
|
+
font-size: var(--nymbal-font-size-base, 1rem);
|
|
93
|
+
font-weight: var(--nymbal-font-weight-bold, 700);
|
|
94
|
+
}
|
|
95
|
+
`
|
|
96
|
+
];
|
|
97
|
+
render() {
|
|
98
|
+
const lines = parsedJson(this, "items", this.items);
|
|
99
|
+
const money = (minor) => formatPrice(minor, this.currency ?? "GBP");
|
|
100
|
+
const row = (part, label, minor, shown = money) => known(minor) ? html`<div part=${part}>
|
|
101
|
+
<dt>${label}</dt>
|
|
102
|
+
<dd>${shown(minor)}</dd>
|
|
103
|
+
</div>` : nothing;
|
|
104
|
+
return html`
|
|
105
|
+
<section part="summary" data-testid="order-summary">
|
|
106
|
+
<h2>Order Summary</h2>
|
|
107
|
+
<ul>
|
|
108
|
+
${(Array.isArray(lines) ? lines : []).map(
|
|
109
|
+
(line) => html`
|
|
110
|
+
<li part="line">
|
|
111
|
+
<span>${line.name} <span class="qty">× ${line.qty}</span></span>
|
|
112
|
+
<span>${money(line.lineTotalMinor)}</span>
|
|
113
|
+
</li>
|
|
114
|
+
`
|
|
115
|
+
)}
|
|
116
|
+
</ul>
|
|
117
|
+
<dl part="totals">
|
|
118
|
+
${row("subtotal", "Subtotal", this.subtotalMinor)}
|
|
119
|
+
${known(this.discountMinor) && this.discountMinor !== 0 ? row("discount", "Discount", this.discountMinor, (m) => `\u2212${money(m)}`) : nothing}
|
|
120
|
+
${row("shipping", "Shipping", this.shippingMinor, (m) => m === 0 ? "Free" : money(m))}
|
|
121
|
+
${row("tax", "Tax", this.taxMinor)}
|
|
122
|
+
</dl>
|
|
123
|
+
${known(this.totalMinor) ? html`<div part="total">
|
|
124
|
+
<span>Total</span>
|
|
125
|
+
<span>${money(this.totalMinor)}</span>
|
|
126
|
+
</div>` : nothing}
|
|
127
|
+
</section>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
NymbalOrderSummary
|
|
134
|
+
};
|
|
135
|
+
//# sourceMappingURL=chunk-SM7TASEW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/order-summary.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 { formatPrice } from '../format.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-order-summary',\n reactName: 'OrderSummary',\n // An order's lines and money as `GET /api/orders/:n` serves them; the element adds nothing up itself.\n attributes: [\n { name: 'items', prop: 'items', type: 'json' },\n { name: 'subtotal-minor', prop: 'subtotalMinor', type: 'number' },\n { name: 'discount-minor', prop: 'discountMinor', type: 'number' },\n { name: 'shipping-minor', prop: 'shippingMinor', type: 'number' },\n { name: 'tax-minor', prop: 'taxMinor', type: 'number' },\n { name: 'total-minor', prop: 'totalMinor', type: 'number' },\n { name: 'currency', prop: 'currency', type: 'string' },\n ],\n events: [],\n parts: ['summary', 'line', 'totals', 'subtotal', 'discount', 'shipping', 'tax', 'total'],\n slots: [],\n children: false,\n}\n\ninterface Line {\n name: string\n qty: number\n unitPriceMinor: number\n lineTotalMinor: number\n}\n\nconst known = (n: number | undefined): n is number => n !== undefined && !Number.isNaN(n)\n\n/** A placed order's lines, subtotal, discount (when there is one), shipping, tax and total. */\nexport class NymbalOrderSummary extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\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~='summary'] {\n padding: var(--nymbal-spacing-lg, 1.5rem);\n background-color: var(--nymbal-color-surface, #ffffff);\n border: 1px solid var(--nymbal-color-border, #e4e4e7);\n border-radius: var(--nymbal-radius-lg, 0.75rem);\n }\n h2 {\n margin: 0 0 var(--nymbal-spacing-md, 1rem);\n line-height: 1.2;\n font-size: var(--nymbal-font-size-lg, 1.125rem);\n font-weight: var(--nymbal-font-weight-bold, 700);\n }\n ul {\n margin: 0 0 var(--nymbal-spacing-md, 1rem);\n padding: 0;\n list-style: none;\n }\n [part~='line'],\n [part~='subtotal'],\n [part~='discount'],\n [part~='shipping'],\n [part~='tax'],\n [part~='total'] {\n display: flex;\n justify-content: space-between;\n gap: var(--nymbal-spacing-md, 1rem);\n padding-block: var(--nymbal-spacing-xs, 0.25rem);\n font-size: var(--nymbal-font-size-sm, 0.875rem);\n }\n .qty {\n color: var(--nymbal-color-text-muted, #71717a);\n }\n [part~='totals'] {\n margin: 0;\n padding-top: var(--nymbal-spacing-sm, 0.5rem);\n border-top: 1px solid var(--nymbal-color-border, #e4e4e7);\n }\n dt,\n dd {\n margin: 0;\n }\n [part~='discount'] dd {\n color: var(--nymbal-color-success, #16a34a);\n }\n [part~='total'] {\n margin-top: var(--nymbal-spacing-sm, 0.5rem);\n padding-top: var(--nymbal-spacing-sm, 0.5rem);\n border-top: 1px solid var(--nymbal-color-border, #e4e4e7);\n font-size: var(--nymbal-font-size-base, 1rem);\n font-weight: var(--nymbal-font-weight-bold, 700);\n }\n `,\n ]\n\n declare items: unknown\n declare subtotalMinor: number | undefined\n declare discountMinor: number | undefined\n declare shippingMinor: number | undefined\n declare taxMinor: number | undefined\n declare totalMinor: number | undefined\n declare currency: string | undefined\n\n protected override render() {\n const lines = parsedJson<Line[]>(this, 'items', this.items)\n const money = (minor: number): string => formatPrice(minor, this.currency ?? 'GBP')\n const row = (part: string, label: string, minor: number | undefined, shown = money) =>\n known(minor)\n ? html`<div part=${part}>\n <dt>${label}</dt>\n <dd>${shown(minor)}</dd>\n </div>`\n : nothing\n return html`\n <section part=\"summary\" data-testid=\"order-summary\">\n <h2>Order Summary</h2>\n <ul>\n ${(Array.isArray(lines) ? lines : []).map(\n (line) => html`\n <li part=\"line\">\n <span>${line.name} <span class=\"qty\">× ${line.qty}</span></span>\n <span>${money(line.lineTotalMinor)}</span>\n </li>\n `,\n )}\n </ul>\n <dl part=\"totals\">\n ${row('subtotal', 'Subtotal', this.subtotalMinor)}\n ${known(this.discountMinor) && this.discountMinor !== 0\n ? row('discount', 'Discount', this.discountMinor, (m) => `−${money(m)}`)\n : nothing}\n ${row('shipping', 'Shipping', this.shippingMinor, (m) => (m === 0 ? 'Free' : money(m)))}\n ${row('tax', 'Tax', this.taxMinor)}\n </dl>\n ${known(this.totalMinor)\n ? html`<div part=\"total\">\n <span>Total</span>\n <span>${money(this.totalMinor)}</span>\n </div>`\n : nothing}\n </section>\n `\n }\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,KAAK,MAAM,eAAe;AAUnC,IAAM,SAAwB;AAAA,EAC5B,KAAK;AAAA,EACL,WAAW;AAAA;AAAA,EAEX,YAAY;AAAA,IACV,EAAE,MAAM,SAAS,MAAM,SAAS,MAAM,OAAO;AAAA,IAC7C,EAAE,MAAM,kBAAkB,MAAM,iBAAiB,MAAM,SAAS;AAAA,IAChE,EAAE,MAAM,kBAAkB,MAAM,iBAAiB,MAAM,SAAS;AAAA,IAChE,EAAE,MAAM,kBAAkB,MAAM,iBAAiB,MAAM,SAAS;AAAA,IAChE,EAAE,MAAM,aAAa,MAAM,YAAY,MAAM,SAAS;AAAA,IACtD,EAAE,MAAM,eAAe,MAAM,cAAc,MAAM,SAAS;AAAA,IAC1D,EAAE,MAAM,YAAY,MAAM,YAAY,MAAM,SAAS;AAAA,EACvD;AAAA,EACA,QAAQ,CAAC;AAAA,EACT,OAAO,CAAC,WAAW,QAAQ,UAAU,YAAY,YAAY,YAAY,OAAO,OAAO;AAAA,EACvF,OAAO,CAAC;AAAA,EACR,UAAU;AACZ;AASA,IAAM,QAAQ,CAAC,MAAuC,MAAM,UAAa,CAAC,OAAO,MAAM,CAAC;AAGjF,IAAM,qBAAN,cAAiC,cAAc;AAAA,EACpD,OAAgB,SAAS;AAAA,EACzB,OAAgB,aAAa,aAAa,MAAM;AAAA,EAChD,OAAgB,SAAS;AAAA,IACvB;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA0DF;AAAA,EAUmB,SAAS;AAC1B,UAAM,QAAQ,WAAmB,MAAM,SAAS,KAAK,KAAK;AAC1D,UAAM,QAAQ,CAAC,UAA0B,YAAY,OAAO,KAAK,YAAY,KAAK;AAClF,UAAM,MAAM,CAAC,MAAc,OAAe,OAA2B,QAAQ,UAC3E,MAAM,KAAK,IACP,iBAAiB,IAAI;AAAA,kBACb,KAAK;AAAA,kBACL,MAAM,KAAK,CAAC;AAAA,oBAEpB;AACN,WAAO;AAAA;AAAA;AAAA;AAAA,aAIE,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,GAAG;AAAA,MACpC,CAAC,SAAS;AAAA;AAAA,wBAEE,KAAK,IAAI,8BAA8B,KAAK,GAAG;AAAA,wBAC/C,MAAM,KAAK,cAAc,CAAC;AAAA;AAAA;AAAA,IAGxC,CAAC;AAAA;AAAA;AAAA,YAGC,IAAI,YAAY,YAAY,KAAK,aAAa,CAAC;AAAA,YAC/C,MAAM,KAAK,aAAa,KAAK,KAAK,kBAAkB,IAClD,IAAI,YAAY,YAAY,KAAK,eAAe,CAAC,MAAM,SAAI,MAAM,CAAC,CAAC,EAAE,IACrE,OAAO;AAAA,YACT,IAAI,YAAY,YAAY,KAAK,eAAe,CAAC,MAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAE,CAAC;AAAA,YACrF,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC;AAAA;AAAA,UAElC,MAAM,KAAK,UAAU,IACnB;AAAA;AAAA,sBAEU,MAAM,KAAK,UAAU,CAAC;AAAA,sBAEhC,OAAO;AAAA;AAAA;AAAA,EAGjB;AACF;","names":[]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalElement,
|
|
4
|
+
baseStyles,
|
|
5
|
+
parsedJson,
|
|
6
|
+
propertiesOf
|
|
7
|
+
} from "./chunk-EYDMJNNK.js";
|
|
8
|
+
|
|
9
|
+
// src/components/image-gallery.ts
|
|
10
|
+
import { css, html, nothing } from "lit";
|
|
11
|
+
var SCHEMA = {
|
|
12
|
+
tag: "nymbal-image-gallery",
|
|
13
|
+
reactName: "ImageGallery",
|
|
14
|
+
attributes: [{ name: "images", prop: "images", type: "json" }],
|
|
15
|
+
events: [],
|
|
16
|
+
parts: ["main", "thumbnails", "thumbnail"],
|
|
17
|
+
slots: [],
|
|
18
|
+
children: false
|
|
19
|
+
};
|
|
20
|
+
var NymbalImageGallery = class extends NymbalElement {
|
|
21
|
+
static schema = SCHEMA;
|
|
22
|
+
static properties = propertiesOf(SCHEMA);
|
|
23
|
+
static styles = [
|
|
24
|
+
baseStyles,
|
|
25
|
+
css`
|
|
26
|
+
:host {
|
|
27
|
+
display: block;
|
|
28
|
+
}
|
|
29
|
+
[part~='main'] {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: auto;
|
|
33
|
+
aspect-ratio: 1 / 1;
|
|
34
|
+
object-fit: cover;
|
|
35
|
+
margin-bottom: var(--nymbal-spacing-sm, 0.5rem);
|
|
36
|
+
border-radius: var(--nymbal-radius-lg, 0.75rem);
|
|
37
|
+
background-color: var(--nymbal-color-border, #f4f4f5);
|
|
38
|
+
}
|
|
39
|
+
[part~='thumbnails'] {
|
|
40
|
+
display: flex;
|
|
41
|
+
gap: var(--nymbal-spacing-sm, 0.5rem);
|
|
42
|
+
overflow-x: auto;
|
|
43
|
+
padding-bottom: var(--nymbal-spacing-xs, 0.25rem);
|
|
44
|
+
}
|
|
45
|
+
[part~='thumbnail'] {
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
width: 64px;
|
|
48
|
+
height: 64px;
|
|
49
|
+
padding: 0;
|
|
50
|
+
border: 2px solid transparent;
|
|
51
|
+
border-radius: var(--nymbal-radius-md, 0.5rem);
|
|
52
|
+
background: none;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: border-color var(--nymbal-transition-fast, 150ms ease);
|
|
56
|
+
}
|
|
57
|
+
[part~='thumbnail']:hover {
|
|
58
|
+
border-color: var(--nymbal-color-border, #e4e4e7);
|
|
59
|
+
}
|
|
60
|
+
[part~='thumbnail'][aria-pressed='true'] {
|
|
61
|
+
border-color: var(--nymbal-color-primary, #18181b);
|
|
62
|
+
}
|
|
63
|
+
[part~='thumbnail'] img {
|
|
64
|
+
display: block;
|
|
65
|
+
width: 100%;
|
|
66
|
+
height: 100%;
|
|
67
|
+
object-fit: cover;
|
|
68
|
+
}
|
|
69
|
+
@media (min-width: 641px) {
|
|
70
|
+
[part~='thumbnail'] {
|
|
71
|
+
width: 80px;
|
|
72
|
+
height: 80px;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
];
|
|
77
|
+
/** The image shown; a new list of images starts again at its first. */
|
|
78
|
+
shownIndex = 0;
|
|
79
|
+
willUpdate(changed) {
|
|
80
|
+
if (changed.has("images")) this.shownIndex = 0;
|
|
81
|
+
}
|
|
82
|
+
show(index) {
|
|
83
|
+
this.shownIndex = index;
|
|
84
|
+
this.requestUpdate();
|
|
85
|
+
}
|
|
86
|
+
render() {
|
|
87
|
+
const images = parsedJson(this, "images", this.images);
|
|
88
|
+
if (!Array.isArray(images) || images.length === 0) return nothing;
|
|
89
|
+
const main = images[this.shownIndex] ?? images[0];
|
|
90
|
+
return html`
|
|
91
|
+
<img
|
|
92
|
+
part="main"
|
|
93
|
+
src=${main.url}
|
|
94
|
+
alt=${main.alt ?? ""}
|
|
95
|
+
width="600"
|
|
96
|
+
height="600"
|
|
97
|
+
fetchpriority="high"
|
|
98
|
+
data-testid="product-gallery"
|
|
99
|
+
/>
|
|
100
|
+
${images.length > 1 ? html`
|
|
101
|
+
<div part="thumbnails">
|
|
102
|
+
${images.map(
|
|
103
|
+
(image, i) => html`
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
part="thumbnail"
|
|
107
|
+
aria-label="Show image ${i + 1}"
|
|
108
|
+
aria-pressed=${i === this.shownIndex ? "true" : "false"}
|
|
109
|
+
@click=${() => this.show(i)}
|
|
110
|
+
>
|
|
111
|
+
<img src=${image.url} alt="" width="80" height="80" loading="lazy" />
|
|
112
|
+
</button>
|
|
113
|
+
`
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
` : nothing}
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
NymbalImageGallery
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=chunk-U2PU4CWB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/image-gallery.ts"],"sourcesContent":["import { css, html, nothing, type PropertyValues } from 'lit'\nimport {\n baseStyles,\n NymbalElement,\n parsedJson,\n propertiesOf,\n type ElementSchema,\n} from '../base-element.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-image-gallery',\n reactName: 'ImageGallery',\n attributes: [{ name: 'images', prop: 'images', type: 'json' }],\n events: [],\n parts: ['main', 'thumbnails', 'thumbnail'],\n slots: [],\n children: false,\n}\n\ninterface Image {\n url: string\n alt?: string\n}\n\n/** A product's images: the one shown, and a thumbnail per image (when there are several) that shows it. */\nexport class NymbalImageGallery extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\n css`\n :host {\n display: block;\n }\n [part~='main'] {\n display: block;\n width: 100%;\n height: auto;\n aspect-ratio: 1 / 1;\n object-fit: cover;\n margin-bottom: var(--nymbal-spacing-sm, 0.5rem);\n border-radius: var(--nymbal-radius-lg, 0.75rem);\n background-color: var(--nymbal-color-border, #f4f4f5);\n }\n [part~='thumbnails'] {\n display: flex;\n gap: var(--nymbal-spacing-sm, 0.5rem);\n overflow-x: auto;\n padding-bottom: var(--nymbal-spacing-xs, 0.25rem);\n }\n [part~='thumbnail'] {\n flex-shrink: 0;\n width: 64px;\n height: 64px;\n padding: 0;\n border: 2px solid transparent;\n border-radius: var(--nymbal-radius-md, 0.5rem);\n background: none;\n overflow: hidden;\n cursor: pointer;\n transition: border-color var(--nymbal-transition-fast, 150ms ease);\n }\n [part~='thumbnail']:hover {\n border-color: var(--nymbal-color-border, #e4e4e7);\n }\n [part~='thumbnail'][aria-pressed='true'] {\n border-color: var(--nymbal-color-primary, #18181b);\n }\n [part~='thumbnail'] img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n @media (min-width: 641px) {\n [part~='thumbnail'] {\n width: 80px;\n height: 80px;\n }\n }\n `,\n ]\n\n declare images: unknown\n\n /** The image shown; a new list of images starts again at its first. */\n private shownIndex = 0\n\n protected override willUpdate(changed: PropertyValues): void {\n if (changed.has('images')) this.shownIndex = 0\n }\n\n private show(index: number): void {\n this.shownIndex = index\n this.requestUpdate()\n }\n\n protected override render() {\n const images = parsedJson<Image[]>(this, 'images', this.images)\n if (!Array.isArray(images) || images.length === 0) return nothing\n const main = images[this.shownIndex] ?? images[0]!\n return html`\n <img\n part=\"main\"\n src=${main.url}\n alt=${main.alt ?? ''}\n width=\"600\"\n height=\"600\"\n fetchpriority=\"high\"\n data-testid=\"product-gallery\"\n />\n ${images.length > 1\n ? html`\n <div part=\"thumbnails\">\n ${images.map(\n (image, i) => html`\n <button\n type=\"button\"\n part=\"thumbnail\"\n aria-label=\"Show image ${i + 1}\"\n aria-pressed=${i === this.shownIndex ? 'true' : 'false'}\n @click=${() => this.show(i)}\n >\n <img src=${image.url} alt=\"\" width=\"80\" height=\"80\" loading=\"lazy\" />\n </button>\n `,\n )}\n </div>\n `\n : nothing}\n `\n }\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,KAAK,MAAM,eAAoC;AASxD,IAAM,SAAwB;AAAA,EAC5B,KAAK;AAAA,EACL,WAAW;AAAA,EACX,YAAY,CAAC,EAAE,MAAM,UAAU,MAAM,UAAU,MAAM,OAAO,CAAC;AAAA,EAC7D,QAAQ,CAAC;AAAA,EACT,OAAO,CAAC,QAAQ,cAAc,WAAW;AAAA,EACzC,OAAO,CAAC;AAAA,EACR,UAAU;AACZ;AAQO,IAAM,qBAAN,cAAiC,cAAc;AAAA,EACpD,OAAgB,SAAS;AAAA,EACzB,OAAgB,aAAa,aAAa,MAAM;AAAA,EAChD,OAAgB,SAAS;AAAA,IACvB;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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmDF;AAAA;AAAA,EAKQ,aAAa;AAAA,EAEF,WAAW,SAA+B;AAC3D,QAAI,QAAQ,IAAI,QAAQ,EAAG,MAAK,aAAa;AAAA,EAC/C;AAAA,EAEQ,KAAK,OAAqB;AAChC,SAAK,aAAa;AAClB,SAAK,cAAc;AAAA,EACrB;AAAA,EAEmB,SAAS;AAC1B,UAAM,SAAS,WAAoB,MAAM,UAAU,KAAK,MAAM;AAC9D,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO;AAC1D,UAAM,OAAO,OAAO,KAAK,UAAU,KAAK,OAAO,CAAC;AAChD,WAAO;AAAA;AAAA;AAAA,cAGG,KAAK,GAAG;AAAA,cACR,KAAK,OAAO,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMpB,OAAO,SAAS,IACd;AAAA;AAAA,gBAEM,OAAO;AAAA,MACP,CAAC,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA,6CAIe,IAAI,CAAC;AAAA,mCACf,MAAM,KAAK,aAAa,SAAS,OAAO;AAAA,6BAC9C,MAAM,KAAK,KAAK,CAAC,CAAC;AAAA;AAAA,+BAEhB,MAAM,GAAG;AAAA;AAAA;AAAA,IAG1B,CAAC;AAAA;AAAA,cAGL,OAAO;AAAA;AAAA,EAEf;AACF;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hydrate-support.ts"],"sourcesContent":["// Lets an element take over the shadow root a server rendered instead of rendering a second one. `lit-element` reads\n// the hook this installs once, when it evaluates, so this module is the first import of every entry and of\n// base-element: ESM evaluates imports depth-first in source order, and every component imports `lit` first.\nimport '@lit-labs/ssr-client/lit-element-hydrate-support.js'\n"],"mappings":";;;AAGA,OAAO;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
NymbalElement
|
|
4
|
+
} from "./chunk-EYDMJNNK.js";
|
|
5
|
+
|
|
6
|
+
// src/selected-variant.ts
|
|
7
|
+
var NymbalVariantElement = class extends NymbalElement {
|
|
8
|
+
following = false;
|
|
9
|
+
stores() {
|
|
10
|
+
return [this.client.product];
|
|
11
|
+
}
|
|
12
|
+
storeSettled() {
|
|
13
|
+
if (this.ownProduct() !== null) this.following = true;
|
|
14
|
+
}
|
|
15
|
+
ownProduct() {
|
|
16
|
+
const { product } = this.client.product.getState();
|
|
17
|
+
return product !== null && this.productId !== void 0 && product.id === this.productId ? product : null;
|
|
18
|
+
}
|
|
19
|
+
/** The store's product and its selected variant, once this element follows the store; else null. */
|
|
20
|
+
selected() {
|
|
21
|
+
if (!this.following || !this.readsStores) return null;
|
|
22
|
+
const product = this.ownProduct();
|
|
23
|
+
if (product === null) return null;
|
|
24
|
+
const { selectedVariant } = this.client.product.getState();
|
|
25
|
+
return { product, variant: selectedVariant ?? product.variants?.[0] ?? null };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
NymbalVariantElement
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=chunk-UYSZOEOA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/selected-variant.ts"],"sourcesContent":["import type { DenormalisedProduct, DenormalisedVariant } from '@nymbalhq/sdk'\nimport { NymbalElement } from './base-element.js'\n\n/**\n * An element showing one product's selected variant (its price, its stock). It renders from its attributes until the\n * product store settles on the product its `product-id` names, then from the store's selected variant. A store\n * settled on another product (the page a soft navigation left) is not this element's, so it keeps its attributes.\n */\nexport abstract class NymbalVariantElement extends NymbalElement {\n declare productId: string | undefined\n\n private following = false\n\n protected override stores() {\n return [this.client.product]\n }\n\n protected override storeSettled(): void {\n if (this.ownProduct() !== null) this.following = true\n }\n\n private ownProduct(): DenormalisedProduct | null {\n const { product } = this.client.product.getState()\n return product !== null && this.productId !== undefined && product.id === this.productId\n ? product\n : null\n }\n\n /** The store's product and its selected variant, once this element follows the store; else null. */\n protected selected(): {\n product: DenormalisedProduct\n variant: DenormalisedVariant | null\n } | null {\n if (!this.following || !this.readsStores) return null\n const product = this.ownProduct()\n if (product === null) return null\n const { selectedVariant } = this.client.product.getState()\n return { product, variant: selectedVariant ?? product.variants?.[0] ?? null }\n }\n}\n"],"mappings":";;;;;;AAQO,IAAe,uBAAf,cAA4C,cAAc;AAAA,EAGvD,YAAY;AAAA,EAED,SAAS;AAC1B,WAAO,CAAC,KAAK,OAAO,OAAO;AAAA,EAC7B;AAAA,EAEmB,eAAqB;AACtC,QAAI,KAAK,WAAW,MAAM,KAAM,MAAK,YAAY;AAAA,EACnD;AAAA,EAEQ,aAAyC;AAC/C,UAAM,EAAE,QAAQ,IAAI,KAAK,OAAO,QAAQ,SAAS;AACjD,WAAO,YAAY,QAAQ,KAAK,cAAc,UAAa,QAAQ,OAAO,KAAK,YAC3E,UACA;AAAA,EACN;AAAA;AAAA,EAGU,WAGD;AACP,QAAI,CAAC,KAAK,aAAa,CAAC,KAAK,YAAa,QAAO;AACjD,UAAM,UAAU,KAAK,WAAW;AAChC,QAAI,YAAY,KAAM,QAAO;AAC7B,UAAM,EAAE,gBAAgB,IAAI,KAAK,OAAO,QAAQ,SAAS;AACzD,WAAO,EAAE,SAAS,SAAS,mBAAmB,QAAQ,WAAW,CAAC,KAAK,KAAK;AAAA,EAC9E;AACF;","names":[]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import '@lit-labs/ssr-client/lit-element-hydrate-support.js'
|
|
2
|
+
import {
|
|
3
|
+
renderField
|
|
4
|
+
} from "./chunk-PN6QKYE4.js";
|
|
5
|
+
import {
|
|
6
|
+
fieldStyles
|
|
7
|
+
} from "./chunk-GHXTJ2CF.js";
|
|
8
|
+
import {
|
|
9
|
+
NymbalElement,
|
|
10
|
+
baseStyles,
|
|
11
|
+
propertiesOf
|
|
12
|
+
} from "./chunk-EYDMJNNK.js";
|
|
13
|
+
|
|
14
|
+
// src/components/address-form.ts
|
|
15
|
+
import { css, html } from "lit";
|
|
16
|
+
var SCHEMA = {
|
|
17
|
+
tag: "nymbal-address-form",
|
|
18
|
+
reactName: "AddressForm",
|
|
19
|
+
attributes: [{ name: "id-prefix", prop: "idPrefix", type: "string" }],
|
|
20
|
+
events: [],
|
|
21
|
+
parts: ["fields", "field", "label", "input"],
|
|
22
|
+
slots: [],
|
|
23
|
+
children: false
|
|
24
|
+
};
|
|
25
|
+
var FIELDS = [
|
|
26
|
+
{ name: "firstName", label: "First Name", autocomplete: "given-name", required: true },
|
|
27
|
+
{ name: "lastName", label: "Last Name", autocomplete: "family-name", required: true },
|
|
28
|
+
{
|
|
29
|
+
name: "addressLine1",
|
|
30
|
+
label: "Address Line 1",
|
|
31
|
+
autocomplete: "address-line1",
|
|
32
|
+
required: true,
|
|
33
|
+
full: true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "addressLine2",
|
|
37
|
+
label: "Address Line 2 (optional)",
|
|
38
|
+
autocomplete: "address-line2",
|
|
39
|
+
full: true
|
|
40
|
+
},
|
|
41
|
+
{ name: "city", label: "City", autocomplete: "address-level2", required: true },
|
|
42
|
+
{ name: "region", label: "State / County", autocomplete: "address-level1", required: true },
|
|
43
|
+
{ name: "postalCode", label: "Postcode", autocomplete: "postal-code", required: true },
|
|
44
|
+
{
|
|
45
|
+
name: "country",
|
|
46
|
+
label: "Country (2-letter code)",
|
|
47
|
+
autocomplete: "country",
|
|
48
|
+
required: true,
|
|
49
|
+
placeholder: "GB"
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
var NymbalAddressForm = class extends NymbalElement {
|
|
53
|
+
static schema = SCHEMA;
|
|
54
|
+
static properties = propertiesOf(SCHEMA);
|
|
55
|
+
static styles = [
|
|
56
|
+
baseStyles,
|
|
57
|
+
fieldStyles,
|
|
58
|
+
css`
|
|
59
|
+
[part~='fields'] {
|
|
60
|
+
display: grid;
|
|
61
|
+
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
|
62
|
+
gap: 0 var(--nymbal-spacing-md, 1rem);
|
|
63
|
+
}
|
|
64
|
+
.full {
|
|
65
|
+
grid-column: 1 / -1;
|
|
66
|
+
}
|
|
67
|
+
`
|
|
68
|
+
];
|
|
69
|
+
input(name) {
|
|
70
|
+
return this.renderRoot.querySelector(`[name="${name}"]`);
|
|
71
|
+
}
|
|
72
|
+
/** The address as typed, trimmed; an empty second line is left out. */
|
|
73
|
+
get value() {
|
|
74
|
+
const field = (name) => this.input(name)?.value.trim() ?? "";
|
|
75
|
+
const line2 = field("addressLine2");
|
|
76
|
+
return {
|
|
77
|
+
firstName: field("firstName"),
|
|
78
|
+
lastName: field("lastName"),
|
|
79
|
+
addressLine1: field("addressLine1"),
|
|
80
|
+
...line2 !== "" && { addressLine2: line2 },
|
|
81
|
+
city: field("city"),
|
|
82
|
+
region: field("region"),
|
|
83
|
+
postalCode: field("postalCode"),
|
|
84
|
+
country: field("country")
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Shows the browser's message on the first field that is not valid, as a native form would. */
|
|
88
|
+
reportValidity() {
|
|
89
|
+
for (const input of this.renderRoot.querySelectorAll("input")) {
|
|
90
|
+
if (!input.reportValidity()) return false;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
reset() {
|
|
95
|
+
for (const input of this.renderRoot.querySelectorAll("input")) input.value = "";
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The inputs sit in this shadow root, so they have no form owner and Enter in one submits nothing natively. Enter
|
|
99
|
+
* submits the form that holds this element instead, as it would if the fields were its own.
|
|
100
|
+
*/
|
|
101
|
+
onKeydown = (event) => {
|
|
102
|
+
if (event.key !== "Enter" || event.isComposing || event.defaultPrevented) return;
|
|
103
|
+
if (!(event.target instanceof HTMLInputElement)) return;
|
|
104
|
+
const form = this.closest("form");
|
|
105
|
+
if (form === null) return;
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
form.requestSubmit();
|
|
108
|
+
};
|
|
109
|
+
render() {
|
|
110
|
+
const prefix = this.idPrefix ?? "address";
|
|
111
|
+
return html`
|
|
112
|
+
<div part="fields" @keydown=${this.onKeydown}>
|
|
113
|
+
${FIELDS.map(
|
|
114
|
+
({ full, ...f }) => html`<div class=${full ? "full" : ""}>
|
|
115
|
+
${renderField({ ...f, id: `${prefix}-${f.name}` })}
|
|
116
|
+
</div>`
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
NymbalAddressForm
|
|
125
|
+
};
|
|
126
|
+
//# sourceMappingURL=chunk-V55K2CS5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/address-form.ts"],"sourcesContent":["import { css, html } from 'lit'\nimport type { Address } from '@nymbalhq/types'\nimport { baseStyles, NymbalElement, propertiesOf, type ElementSchema } from '../base-element.js'\nimport { renderField, type FieldSpec } from '../form-element.js'\nimport { fieldStyles } from '../view-styles.js'\n\nconst SCHEMA: ElementSchema = {\n tag: 'nymbal-address-form',\n reactName: 'AddressForm',\n attributes: [{ name: 'id-prefix', prop: 'idPrefix', type: 'string' }],\n events: [],\n parts: ['fields', 'field', 'label', 'input'],\n slots: [],\n children: false,\n}\n\ntype FieldName = Exclude<keyof Address, 'company' | 'phone'>\n\nconst FIELDS: Array<Omit<FieldSpec, 'id' | 'name'> & { name: FieldName; full?: boolean }> = [\n { name: 'firstName', label: 'First Name', autocomplete: 'given-name', required: true },\n { name: 'lastName', label: 'Last Name', autocomplete: 'family-name', required: true },\n {\n name: 'addressLine1',\n label: 'Address Line 1',\n autocomplete: 'address-line1',\n required: true,\n full: true,\n },\n {\n name: 'addressLine2',\n label: 'Address Line 2 (optional)',\n autocomplete: 'address-line2',\n full: true,\n },\n { name: 'city', label: 'City', autocomplete: 'address-level2', required: true },\n { name: 'region', label: 'State / County', autocomplete: 'address-level1', required: true },\n { name: 'postalCode', label: 'Postcode', autocomplete: 'postal-code', required: true },\n {\n name: 'country',\n label: 'Country (2-letter code)',\n autocomplete: 'country',\n required: true,\n placeholder: 'GB',\n },\n]\n\n/**\n * The address fields, with ids and test ids `<id-prefix>-<field>`. It renders no `<form>`: the view that holds it owns\n * the submit (Enter in a field submits it) and reads `value`.\n */\nexport class NymbalAddressForm extends NymbalElement {\n static override schema = SCHEMA\n static override properties = propertiesOf(SCHEMA)\n static override styles = [\n baseStyles,\n fieldStyles,\n css`\n [part~='fields'] {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));\n gap: 0 var(--nymbal-spacing-md, 1rem);\n }\n .full {\n grid-column: 1 / -1;\n }\n `,\n ]\n\n declare idPrefix: string | undefined\n\n private input(name: FieldName): HTMLInputElement | null {\n return this.renderRoot.querySelector<HTMLInputElement>(`[name=\"${name}\"]`)\n }\n\n /** The address as typed, trimmed; an empty second line is left out. */\n get value(): Address {\n const field = (name: FieldName): string => this.input(name)?.value.trim() ?? ''\n const line2 = field('addressLine2')\n return {\n firstName: field('firstName'),\n lastName: field('lastName'),\n addressLine1: field('addressLine1'),\n ...(line2 !== '' && { addressLine2: line2 }),\n city: field('city'),\n region: field('region'),\n postalCode: field('postalCode'),\n country: field('country'),\n }\n }\n\n /** Shows the browser's message on the first field that is not valid, as a native form would. */\n reportValidity(): boolean {\n for (const input of this.renderRoot.querySelectorAll('input')) {\n if (!input.reportValidity()) return false\n }\n return true\n }\n\n reset(): void {\n for (const input of this.renderRoot.querySelectorAll('input')) input.value = ''\n }\n\n /**\n * The inputs sit in this shadow root, so they have no form owner and Enter in one submits nothing natively. Enter\n * submits the form that holds this element instead, as it would if the fields were its own.\n */\n private readonly onKeydown = (event: KeyboardEvent): void => {\n if (event.key !== 'Enter' || event.isComposing || event.defaultPrevented) return\n if (!(event.target instanceof HTMLInputElement)) return\n const form = this.closest('form')\n if (form === null) return\n event.preventDefault()\n form.requestSubmit()\n }\n\n protected override render() {\n const prefix = this.idPrefix ?? 'address'\n return html`\n <div part=\"fields\" @keydown=${this.onKeydown}>\n ${FIELDS.map(\n ({ full, ...f }) =>\n html`<div class=${full ? 'full' : ''}>\n ${renderField({ ...f, id: `${prefix}-${f.name}` })}\n </div>`,\n )}\n </div>\n `\n }\n}\n"],"mappings":"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","names":[]}
|