@shoppexio/storefront-react 0.0.0 → 0.1.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +137 -0
  4. package/dist/account.css +1171 -0
  5. package/dist/account.d.ts +23 -0
  6. package/dist/account.js +12685 -0
  7. package/dist/account.js.map +1 -0
  8. package/dist/chunk-D6XKDVAV.js +181 -0
  9. package/dist/chunk-D6XKDVAV.js.map +1 -0
  10. package/dist/index.d.ts +3087 -0
  11. package/dist/index.js +2801 -0
  12. package/dist/index.js.map +1 -0
  13. package/eject/account/account-availability.ts +171 -0
  14. package/eject/account/account-downloads.ts +118 -0
  15. package/eject/account/account-favorites.ts +99 -0
  16. package/eject/account/account-loyalty.ts +241 -0
  17. package/eject/account/account-page.tsx +410 -0
  18. package/eject/account/account-payment-method.ts +99 -0
  19. package/eject/account/account-referrals.ts +68 -0
  20. package/eject/account/account-reseller-invite.ts +88 -0
  21. package/eject/account/account-reseller.ts +685 -0
  22. package/eject/account/account-rewards.ts +104 -0
  23. package/eject/account/account-settings.ts +181 -0
  24. package/eject/account/account-signin-link.ts +122 -0
  25. package/eject/account/account-subscriptions.ts +130 -0
  26. package/eject/account/account-support.ts +155 -0
  27. package/eject/account/account-warranties.ts +118 -0
  28. package/eject/account/customer-account.ts +93 -0
  29. package/eject/account/dashboard.tsx +139 -0
  30. package/eject/account/downloads.tsx +343 -0
  31. package/eject/account/favorites.tsx +201 -0
  32. package/eject/account/i18n.ts +175 -0
  33. package/eject/account/icons.tsx +218 -0
  34. package/eject/account/login.tsx +253 -0
  35. package/eject/account/messages/ar.json +464 -0
  36. package/eject/account/messages/de.json +464 -0
  37. package/eject/account/messages/en.json +464 -0
  38. package/eject/account/messages/es.json +464 -0
  39. package/eject/account/messages/fi.json +464 -0
  40. package/eject/account/messages/fr.json +464 -0
  41. package/eject/account/messages/hi.json +464 -0
  42. package/eject/account/messages/it.json +464 -0
  43. package/eject/account/messages/lt.json +464 -0
  44. package/eject/account/messages/nl.json +464 -0
  45. package/eject/account/messages/pl.json +464 -0
  46. package/eject/account/messages/pt.json +464 -0
  47. package/eject/account/messages/ru.json +464 -0
  48. package/eject/account/messages/tr.json +464 -0
  49. package/eject/account/messages/uk.json +464 -0
  50. package/eject/account/messages/zh.json +464 -0
  51. package/eject/account/orders.tsx +791 -0
  52. package/eject/account/overview.tsx +147 -0
  53. package/eject/account/referrals.tsx +327 -0
  54. package/eject/account/reseller.tsx +1365 -0
  55. package/eject/account/rewards.tsx +641 -0
  56. package/eject/account/settings.tsx +699 -0
  57. package/eject/account/shared.tsx +398 -0
  58. package/eject/account/subscriptions.tsx +421 -0
  59. package/eject/account/support.tsx +453 -0
  60. package/package.json +75 -1
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The products a buyer saved for later.
3
+ *
4
+ * The one panel with a list route of its own (`GET /favorites`), and the only
5
+ * one whose collection the dashboard payload does not carry.
6
+ *
7
+ * No price is shown, and that is a decision rather than an omission. The
8
+ * producer sends `price_display` as a bare number with no currency beside it —
9
+ * it is the product's stored display price, and nothing in this payload says
10
+ * which currency it was measured in. Labelling it with whatever currency the
11
+ * page happens to be pricing in turns 19.99 EUR into 19.99 USD on every shop
12
+ * where the two differ, which is worse than not saying.
13
+ */
14
+
15
+ import { useEffect, useState } from 'react';
16
+ import { Link } from 'react-router-dom';
17
+ import {
18
+ favoriteProductHref,
19
+ narrowFavorites,
20
+ withoutFavorite,
21
+ type CustomerFavorite,
22
+ } from './account-favorites';
23
+ import { callCustomerApi } from './customer-account';
24
+ import { t } from './i18n';
25
+ import { ProductPlaceholderIcon } from './icons';
26
+ import {
27
+ AccountEmpty,
28
+ AccountError,
29
+ AccountRowsSkeleton,
30
+ BusySpinner,
31
+ formatAccountDate,
32
+ useAccountResource,
33
+ } from './shared';
34
+
35
+ export function AccountFavorites() {
36
+ const { state, reload } = useAccountResource('favorites', () =>
37
+ callCustomerApi((sdk) => sdk.favorites()));
38
+ const [favorites, setFavorites] = useState<CustomerFavorite[] | null>(null);
39
+ const [contractBroken, setContractBroken] = useState(false);
40
+ const [removing, setRemoving] = useState<string | null>(null);
41
+ const [removeError, setRemoveError] = useState<string | null>(null);
42
+
43
+ /*
44
+ * The list is copied into state because removing writes to it. Narrowing runs
45
+ * once per load rather than on every render, and a payload that does not
46
+ * match the producer is RECORDED as broken — never rendered as an empty
47
+ * wishlist, which would tell a buyer their saved products are gone.
48
+ */
49
+ useEffect(() => {
50
+ if (state.status !== 'ready') return;
51
+ const narrowed = narrowFavorites(state.data);
52
+ setContractBroken(!narrowed.ok);
53
+ setFavorites(narrowed.ok ? narrowed.favorites : null);
54
+ setRemoveError(null);
55
+ }, [state]);
56
+
57
+ if (state.status === 'loading') {
58
+ return <AccountRowsSkeleton rows={3} label={t('favorites.loading')} />;
59
+ }
60
+
61
+ if (state.status === 'error') {
62
+ return <AccountError message={state.message} onRetry={reload} />;
63
+ }
64
+
65
+ if (contractBroken) {
66
+ return <AccountError message={t('common.genericError')} onRetry={reload} />;
67
+ }
68
+
69
+ // The response landed but the effect that narrows it has not run yet.
70
+ if (favorites === null) {
71
+ return <AccountRowsSkeleton rows={3} label={t('favorites.loading')} />;
72
+ }
73
+
74
+ if (favorites.length === 0) {
75
+ return (
76
+ <AccountEmpty
77
+ title={t('favorites.emptyTitle')}
78
+ body={t('favorites.emptyBody')}
79
+ action={(
80
+ <Link to="/all-products" className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-10 sxa:items-center sxa:px-5 sxa:text-small">
81
+ {t('common.browseStore')}
82
+ </Link>
83
+ )}
84
+ />
85
+ );
86
+ }
87
+
88
+ const remove = async (favorite: CustomerFavorite) => {
89
+ setRemoving(favorite.product_uniqid);
90
+ setRemoveError(null);
91
+ const result = await callCustomerApi((sdk) => sdk.removeFavorite(favorite.product_uniqid));
92
+ setRemoving(null);
93
+
94
+ if (!result.success) {
95
+ setRemoveError(result.message?.trim() || t('favorites.removeFailed'));
96
+ return;
97
+ }
98
+
99
+ // Dropped only once the server confirmed it. Removing the card first and
100
+ // putting it back on failure makes the buyer watch their own action undo
101
+ // itself, and leaves the two lists disagreeing whenever a retry is slow.
102
+ setFavorites((current) => (
103
+ current ? withoutFavorite(current, favorite.product_uniqid) : current
104
+ ));
105
+ };
106
+
107
+ return (
108
+ <div>
109
+ {removeError ? (
110
+ <div className="sxa:mb-6">
111
+ <AccountError message={removeError} />
112
+ </div>
113
+ ) : null}
114
+
115
+ <ul className="sxa:grid sxa:gap-4 sxa:sm:grid-cols-2 sxa:lg:grid-cols-3">
116
+ {favorites.map((favorite) => (
117
+ <li
118
+ key={favorite.product_uniqid}
119
+ className="sxa:flex sxa:flex-col sxa:overflow-hidden sxa:rounded-input sxa:border sxa:bg-surface sxa:shadow-card"
120
+ >
121
+ <FavoriteImage favorite={favorite} />
122
+ <div className="sxa:flex sxa:flex-1 sxa:flex-col sxa:p-5">
123
+ <p className="sxa:text-small sxa:font-semibold">{favorite.title}</p>
124
+ <p className="sxa:mt-1 sxa:text-micro sxa:text-muted">
125
+ {t('favorites.added', { date: formatAccountDate(favorite.created_at) })}
126
+ </p>
127
+ {/* The merchant deleted the product. The row stays so the buyer
128
+ can clear it; the link is what goes away. */}
129
+ {favorite.unavailable ? (
130
+ <p className="sxa:mt-2 sxa:text-micro sxa:font-medium sxa:text-muted">{t('favorites.gone')}</p>
131
+ ) : null}
132
+
133
+ <div className="sxa:mt-auto sxa:flex sxa:flex-wrap sxa:items-center sxa:gap-2 sxa:pt-4">
134
+ <FavoriteLink favorite={favorite} />
135
+ <button
136
+ type="button"
137
+ onClick={() => void remove(favorite)}
138
+ disabled={removing !== null}
139
+ aria-busy={removing === favorite.product_uniqid || undefined}
140
+ className="sx-account-button sxa:card-button sxa:card-button--add sxa:disabled:cursor-not-allowed sxa:disabled:opacity-50"
141
+ >
142
+ {removing === favorite.product_uniqid ? (
143
+ <BusySpinner className="sxa:mr-1.5 sxa:h-3.5 sxa:w-3.5" />
144
+ ) : null}
145
+ {removing === favorite.product_uniqid
146
+ ? t('favorites.removing')
147
+ : t('favorites.remove')}
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </li>
152
+ ))}
153
+ </ul>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ /**
159
+ * The saved product's picture, or the same placeholder tile the cart draws.
160
+ *
161
+ * An image URL that fails to load falls back to the placeholder rather than
162
+ * leaving a broken-image icon in the grid — the product is still saved either
163
+ * way, and the card must stay readable.
164
+ */
165
+ function FavoriteImage({ favorite }: { favorite: CustomerFavorite }) {
166
+ const [failed, setFailed] = useState(false);
167
+
168
+ if (!favorite.image_url || failed) {
169
+ return (
170
+ <div
171
+ className="sxa:flex sxa:aspect-[4/3] sxa:items-center sxa:justify-center sxa:border-b sxa:bg-bg"
172
+ role="img"
173
+ aria-label={t('favorites.noImage', { title: favorite.title })}
174
+ >
175
+ <ProductPlaceholderIcon className="sxa:h-6 sxa:w-6 sxa:text-muted" strokeWidth="1.25" />
176
+ </div>
177
+ );
178
+ }
179
+
180
+ return (
181
+ <img
182
+ src={favorite.image_url}
183
+ alt={favorite.title}
184
+ loading="lazy"
185
+ decoding="async"
186
+ onError={() => setFailed(true)}
187
+ className="sxa:aspect-[4/3] sxa:w-full sxa:border-b sxa:object-cover"
188
+ />
189
+ );
190
+ }
191
+
192
+ /** Nothing at all for a product with nowhere to go — see `favoriteProductHref`. */
193
+ function FavoriteLink({ favorite }: { favorite: CustomerFavorite }) {
194
+ const href = favoriteProductHref(favorite);
195
+ if (!href) return null;
196
+ return (
197
+ <Link to={href} className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-8 sxa:items-center sxa:px-4 sxa:text-micro sxa:font-semibold">
198
+ {t('favorites.view')}
199
+ </Link>
200
+ );
201
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The customer account's message catalog.
3
+ *
4
+ * A code storefront is an immutable artifact served from R2 — there is no
5
+ * server render and no request-time catalog injection. Every locale therefore
6
+ * ships INSIDE the account chunk, and the one thing that varies per request is
7
+ * which of them is read.
8
+ *
9
+ * The locale comes from `<html lang>`, which the Shoppex worker rewrites to the
10
+ * locale the page was served in (`withServedHtmlLang` in
11
+ * `workers/storefront/src/code-page-metadata.ts`) before the bundle runs. That
12
+ * is the only locale signal a code storefront gets, and it is the same one the
13
+ * document lane resolves its catalog from — so a buyer on `/de/dashboard` reads
14
+ * German on either lane. `navigator.language` is deliberately NOT consulted: it
15
+ * is the browser's preference, not the page the edge decided to serve, and
16
+ * mixing the two produces a German page with English panels inside it.
17
+ *
18
+ * English is the compiled-in contract. `translate` falls back to it for a key a
19
+ * locale catalog is missing, exactly like the document lane's runtime, and the
20
+ * parity test in `tests/messages.test.ts` is what keeps that fallback from ever
21
+ * being needed in a release.
22
+ *
23
+ * All sixteen are STATIC imports, and that is a decision with a price: they add
24
+ * roughly 66 KB gzipped to a bundle that only ever reads one of them. Splitting
25
+ * them into per-locale chunks would trade those bytes for a second failure mode
26
+ * — a chunk that does not arrive leaves a buyer reading English on a German
27
+ * page, quietly, and the boot would have to become asynchronous to allow for
28
+ * it. Bytes that are always right beat a request that can be wrong; revisit
29
+ * this if the catalog grows past the account surface.
30
+ */
31
+
32
+ import arMessages from './messages/ar.json';
33
+ import deMessages from './messages/de.json';
34
+ import enMessages from './messages/en.json';
35
+ import esMessages from './messages/es.json';
36
+ import fiMessages from './messages/fi.json';
37
+ import frMessages from './messages/fr.json';
38
+ import hiMessages from './messages/hi.json';
39
+ import itMessages from './messages/it.json';
40
+ import ltMessages from './messages/lt.json';
41
+ import nlMessages from './messages/nl.json';
42
+ import plMessages from './messages/pl.json';
43
+ import ptMessages from './messages/pt.json';
44
+ import ruMessages from './messages/ru.json';
45
+ import trMessages from './messages/tr.json';
46
+ import ukMessages from './messages/uk.json';
47
+ import zhMessages from './messages/zh.json';
48
+
49
+ export type MessageParams = Record<string, string | number>;
50
+ export type MessageCatalog = { [key: string]: string | MessageCatalog };
51
+
52
+ /** The locales the Shoppex storefront edge can serve. */
53
+ export const SUPPORTED_LOCALES = [
54
+ 'ar', 'de', 'en', 'es', 'fi', 'fr', 'hi', 'it',
55
+ 'lt', 'nl', 'pl', 'pt', 'ru', 'tr', 'uk', 'zh',
56
+ ] as const;
57
+
58
+ export type Locale = (typeof SUPPORTED_LOCALES)[number];
59
+
60
+ export const DEFAULT_LOCALE: Locale = 'en';
61
+
62
+ export const MESSAGE_CATALOGS: Readonly<Record<Locale, MessageCatalog>> = {
63
+ ar: arMessages as MessageCatalog,
64
+ de: deMessages as MessageCatalog,
65
+ en: enMessages as MessageCatalog,
66
+ es: esMessages as MessageCatalog,
67
+ fi: fiMessages as MessageCatalog,
68
+ fr: frMessages as MessageCatalog,
69
+ hi: hiMessages as MessageCatalog,
70
+ it: itMessages as MessageCatalog,
71
+ lt: ltMessages as MessageCatalog,
72
+ nl: nlMessages as MessageCatalog,
73
+ pl: plMessages as MessageCatalog,
74
+ pt: ptMessages as MessageCatalog,
75
+ ru: ruMessages as MessageCatalog,
76
+ tr: trMessages as MessageCatalog,
77
+ uk: ukMessages as MessageCatalog,
78
+ zh: zhMessages as MessageCatalog,
79
+ };
80
+
81
+ export const EN_MESSAGES: MessageCatalog = MESSAGE_CATALOGS.en;
82
+
83
+ /** Locales written right-to-left, so the account shell can flip its direction. */
84
+ const RTL_LOCALES: ReadonlySet<Locale> = new Set<Locale>(['ar']);
85
+
86
+ export function isSupportedLocale(value: string): value is Locale {
87
+ return (SUPPORTED_LOCALES as readonly string[]).includes(value);
88
+ }
89
+
90
+ /**
91
+ * The catalog a `lang` attribute names.
92
+ *
93
+ * `de-AT` and `DE` both resolve to `de`: the edge writes a bare two-letter code
94
+ * today, and a region-tagged one must not silently read as "no locale at all".
95
+ * Anything that names no catalog is English, which is the complete contract
96
+ * rather than a guess at what the buyer wanted.
97
+ */
98
+ export function resolveLocale(lang: string | null | undefined): Locale {
99
+ const base = (lang ?? '').trim().toLowerCase().split(/[-_]/)[0] ?? '';
100
+ return isSupportedLocale(base) ? base : DEFAULT_LOCALE;
101
+ }
102
+
103
+ export function isRightToLeft(locale: Locale): boolean {
104
+ return RTL_LOCALES.has(locale);
105
+ }
106
+
107
+ function messageAt(catalog: MessageCatalog | undefined, key: string): string | undefined {
108
+ let value: string | MessageCatalog | undefined = catalog;
109
+ for (const segment of key.split('.')) {
110
+ if (!value || typeof value === 'string') return undefined;
111
+ value = value[segment];
112
+ }
113
+ return typeof value === 'string' ? value : undefined;
114
+ }
115
+
116
+ function interpolate(message: string, params: MessageParams | undefined): string {
117
+ if (!params) return message;
118
+ return message.replace(/\{([^{}]+)\}/g, (token, name: string) => (
119
+ Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : token
120
+ ));
121
+ }
122
+
123
+ /** Whether English — the contract — defines this key at all. */
124
+ export function hasMessage(key: string): boolean {
125
+ return messageAt(EN_MESSAGES, key) !== undefined;
126
+ }
127
+
128
+ export function translate(locale: Locale, key: string, params?: MessageParams): string {
129
+ const message = messageAt(MESSAGE_CATALOGS[locale], key) ?? messageAt(EN_MESSAGES, key);
130
+ // The key itself, so a missing string is visible in the UI rather than blank.
131
+ if (message === undefined) return key;
132
+ return interpolate(message, params);
133
+ }
134
+
135
+ /**
136
+ * A message only when one exists.
137
+ *
138
+ * Wire enums (invoice status, reward reason, delivery state) are OPEN sets: the
139
+ * server may emit a value this catalog has never heard of. Those callers need
140
+ * "no translation" as an answer they can act on, so they can print the server's
141
+ * own spelling instead of the key.
142
+ */
143
+ export function translateOptional(
144
+ locale: Locale,
145
+ key: string,
146
+ params?: MessageParams,
147
+ ): string | undefined {
148
+ return hasMessage(key) ? translate(locale, key, params) : undefined;
149
+ }
150
+
151
+ let activeLocale: Locale | undefined;
152
+
153
+ function documentLocale(): Locale {
154
+ if (typeof document === 'undefined') return DEFAULT_LOCALE;
155
+ return resolveLocale(document.documentElement.getAttribute('lang'));
156
+ }
157
+
158
+ /** The locale this page load is being read in. Resolved once, from the edge. */
159
+ export function getActiveLocale(): Locale {
160
+ if (activeLocale === undefined) activeLocale = documentLocale();
161
+ return activeLocale;
162
+ }
163
+
164
+ /** Pins the locale. The tests are the only caller; the browser uses `<html lang>`. */
165
+ export function setActiveLocale(locale: Locale | undefined): void {
166
+ activeLocale = locale;
167
+ }
168
+
169
+ export function t(key: string, params?: MessageParams): string {
170
+ return translate(getActiveLocale(), key, params);
171
+ }
172
+
173
+ export function tOptional(key: string, params?: MessageParams): string | undefined {
174
+ return translateOptional(getActiveLocale(), key, params);
175
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * The icons the account draws, inlined.
3
+ *
4
+ * Storefront artifacts are served behind a strict CSP and build with no
5
+ * network, so there is no icon CDN. Every icon is a plain SVG that takes its
6
+ * size and colour from the classes around it (`currentColor`).
7
+ *
8
+ * Outline marks are the 24x24 / stroke-2 Lucide geometry (ISC).
9
+ */
10
+
11
+ import type { SVGProps } from 'react';
12
+
13
+ type IconProps = SVGProps<SVGSVGElement>;
14
+
15
+ function OutlineIcon({ children, ...props }: IconProps) {
16
+ return (
17
+ <svg
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ width="24"
20
+ height="24"
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ strokeWidth="2"
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ aria-hidden="true"
28
+ {...props}
29
+ >
30
+ {children}
31
+ </svg>
32
+ );
33
+ }
34
+
35
+ export function ArrowLeftIcon(props: IconProps) {
36
+ return (
37
+ <OutlineIcon {...props}>
38
+ <path d="m12 19-7-7 7-7" />
39
+ <path d="M19 12H5" />
40
+ </OutlineIcon>
41
+ );
42
+ }
43
+
44
+ export function ArrowRightIcon(props: IconProps) {
45
+ return (
46
+ <OutlineIcon {...props}>
47
+ <path d="M5 12h14" />
48
+ <path d="m12 5 7 7-7 7" />
49
+ </OutlineIcon>
50
+ );
51
+ }
52
+
53
+ /** Placeholder art for a bundle with no image of its own. */
54
+ export function BoxIcon(props: IconProps) {
55
+ return (
56
+ <OutlineIcon strokeWidth="1.25" {...props}>
57
+ <path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
58
+ <path d="M3.3 7 12 12l8.7-5" />
59
+ <path d="M12 22V12" />
60
+ </OutlineIcon>
61
+ );
62
+ }
63
+
64
+ /** The tick inside a selected variant pill or a ticked addon box. */
65
+ export function CheckIcon(props: IconProps) {
66
+ return (
67
+ <OutlineIcon strokeWidth="3" {...props}>
68
+ <path d="m5 12 5 5L20 7" />
69
+ </OutlineIcon>
70
+ );
71
+ }
72
+
73
+ export function ChevronDownIcon(props: IconProps) {
74
+ return (
75
+ <OutlineIcon {...props}>
76
+ <path d="m6 9 6 6 6-6" />
77
+ </OutlineIcon>
78
+ );
79
+ }
80
+
81
+ export function CircleAlertIcon(props: IconProps) {
82
+ return (
83
+ <OutlineIcon {...props}>
84
+ <circle cx="12" cy="12" r="10" />
85
+ <line x1="12" x2="12" y1="8" y2="12" />
86
+ <line x1="12" x2="12.01" y1="16" y2="16" />
87
+ </OutlineIcon>
88
+ );
89
+ }
90
+
91
+ /** Lucide `copy`. The account surface copies licence keys and referral links. */
92
+ export function CopyIcon(props: IconProps) {
93
+ return (
94
+ <OutlineIcon {...props}>
95
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
96
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
97
+ </OutlineIcon>
98
+ );
99
+ }
100
+
101
+ /** Leaves the storefront — every community and portal link carries it. */
102
+ export function ExternalLinkIcon(props: IconProps) {
103
+ return (
104
+ <OutlineIcon {...props}>
105
+ <path d="M15 3h6v6" />
106
+ <path d="M10 14 21 3" />
107
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
108
+ </OutlineIcon>
109
+ );
110
+ }
111
+
112
+ /** The reward-program teaser's mark. */
113
+ export function GiftIcon(props: IconProps) {
114
+ return (
115
+ <OutlineIcon {...props}>
116
+ <rect x="3" y="8" width="18" height="4" rx="1" />
117
+ <path d="M12 8v13" />
118
+ <path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" />
119
+ <path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" />
120
+ </OutlineIcon>
121
+ );
122
+ }
123
+
124
+ /** The spinner on a submitting form. It is the only mark the theme rotates. */
125
+ export function LoaderIcon(props: IconProps) {
126
+ return (
127
+ <OutlineIcon {...props}>
128
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
129
+ </OutlineIcon>
130
+ );
131
+ }
132
+
133
+ export function LockIcon(props: IconProps) {
134
+ return (
135
+ <OutlineIcon {...props}>
136
+ <rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
137
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
138
+ </OutlineIcon>
139
+ );
140
+ }
141
+
142
+ export function MailIcon(props: IconProps) {
143
+ return (
144
+ <OutlineIcon {...props}>
145
+ <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
146
+ <rect x="2" y="4" width="20" height="16" rx="2" />
147
+ </OutlineIcon>
148
+ );
149
+ }
150
+
151
+ /** The reviews badge, the contact form's header and its message field. */
152
+ export function MessageSquareIcon(props: IconProps) {
153
+ return (
154
+ <OutlineIcon {...props}>
155
+ <path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
156
+ </OutlineIcon>
157
+ );
158
+ }
159
+
160
+ /** Placeholder art for a product with no image. */
161
+ export function ProductPlaceholderIcon(props: IconProps) {
162
+ return (
163
+ <OutlineIcon strokeWidth="1.25" {...props}>
164
+ <path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" />
165
+ <path d="M12 22V12" />
166
+ <polyline points="3.29 7 12 12 20.71 7" />
167
+ <path d="m7.5 4.27 9 5.15" />
168
+ </OutlineIcon>
169
+ );
170
+ }
171
+
172
+ /** Marks the order/invoice field the merchant can make mandatory. */
173
+ export function ReceiptIcon(props: IconProps) {
174
+ return (
175
+ <OutlineIcon {...props}>
176
+ <path d="M12 17V7" />
177
+ <path d="M16 8h-6a2 2 0 0 0 0 4h4a2 2 0 0 1 0 4H8" />
178
+ <path d="M4 3a1 1 0 0 1 1-1 1.3 1.3 0 0 1 .7.2l.933.6a1.3 1.3 0 0 0 1.4 0l.934-.6a1.3 1.3 0 0 1 1.4 0l.933.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6a1.3 1.3 0 0 1 1.4 0l.934.6a1.3 1.3 0 0 0 1.4 0l.933-.6A1.3 1.3 0 0 1 19 2a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1 1.3 1.3 0 0 1-.7-.2l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.934.6a1.3 1.3 0 0 1-1.4 0l-.933-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-1.4 0l-.934-.6a1.3 1.3 0 0 0-1.4 0l-.933.6a1.3 1.3 0 0 1-.7.2 1 1 0 0 1-1-1z" />
179
+ </OutlineIcon>
180
+ );
181
+ }
182
+
183
+ export function SearchIcon(props: IconProps) {
184
+ return (
185
+ <OutlineIcon {...props}>
186
+ <path d="m21 21-4.34-4.34" />
187
+ <circle cx="11" cy="11" r="8" />
188
+ </OutlineIcon>
189
+ );
190
+ }
191
+
192
+ /** Shared by the trust strip and the why-choose-us feature panels. */
193
+ export function ShieldIcon(props: IconProps) {
194
+ return (
195
+ <OutlineIcon {...props}>
196
+ <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" />
197
+ <path d="m9 12 2 2 4-4" />
198
+ </OutlineIcon>
199
+ );
200
+ }
201
+
202
+ export function TagIcon(props: IconProps) {
203
+ return (
204
+ <OutlineIcon {...props}>
205
+ <path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />
206
+ <circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
207
+ </OutlineIcon>
208
+ );
209
+ }
210
+
211
+ /** Shared by the trust strip and the buy box's delivery badge. */
212
+ export function ZapIcon(props: IconProps) {
213
+ return (
214
+ <OutlineIcon {...props}>
215
+ <path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" />
216
+ </OutlineIcon>
217
+ );
218
+ }