@occluze/tether 0.6.7 → 0.7.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/README.md +11 -11
- package/dist/esm/astro.js +2 -2
- package/dist/esm/chunks/account-UIPWJQ3Y.js +1 -0
- package/dist/esm/chunks/account-pages-6ROA6Y4V.js +1 -0
- package/dist/esm/chunks/analytics-2V632IRV.js +1 -0
- package/dist/esm/chunks/chunk-4GCXM764.js +1 -0
- package/dist/esm/chunks/chunk-56FCLJJD.js +1 -0
- package/dist/esm/chunks/chunk-5Z5O3767.js +1 -0
- package/dist/esm/chunks/chunk-6OZJCAGZ.js +1 -0
- package/dist/esm/chunks/chunk-A5SYCVQM.js +1 -0
- package/dist/esm/chunks/chunk-BA4LQJJQ.js +1 -0
- package/dist/esm/chunks/chunk-BFTZR4OB.js +4 -0
- package/dist/esm/chunks/chunk-C5JYQCWH.js +1 -0
- package/dist/esm/chunks/chunk-E5YO6274.js +1 -0
- package/dist/esm/chunks/chunk-MPDWM2NN.js +1 -0
- package/dist/esm/chunks/chunk-T44MUL4Y.js +1 -0
- package/dist/esm/chunks/chunk-YLUULATY.js +1 -0
- package/dist/esm/chunks/diagnose-3FEKRIEK.js +14 -0
- package/dist/esm/chunks/extras-6POFBNOP.js +1 -0
- package/dist/esm/chunks/markets-ui-FD6NI2OG.js +1 -0
- package/dist/esm/chunks/search-UJ73YBQ5.js +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/server.js +2 -1
- package/dist/presets/barba.esm.js +55 -39
- package/dist/tether.account-pages.js +781 -495
- package/dist/tether.account-pages.min.js +1 -1
- package/dist/tether.account.js +462 -381
- package/dist/tether.account.min.js +1 -1
- package/dist/tether.analytics.js +299 -194
- package/dist/tether.analytics.min.js +1 -1
- package/dist/tether.diagnose.js +354 -262
- package/dist/tether.diagnose.min.js +2 -2
- package/dist/tether.extras.js +82 -45
- package/dist/tether.extras.min.js +1 -1
- package/dist/tether.js +3653 -2116
- package/dist/tether.markets.js +238 -150
- package/dist/tether.markets.min.js +1 -1
- package/dist/tether.min.js +3 -3
- package/dist/tether.min.js.map +4 -4
- package/dist/tether.search.js +207 -136
- package/dist/tether.search.min.js +1 -1
- package/dist/types/server/src/shared/account-orders.d.ts +44 -0
- package/dist/types/server/src/shared/account-routes.d.ts +153 -0
- package/dist/types/server/src/shared/api-versions.d.ts +13 -0
- package/dist/types/server/src/shared/error-message.d.ts +2 -0
- package/dist/types/server/src/shared/json.d.ts +25 -0
- package/dist/types/server/src/shared/market-codes.d.ts +16 -0
- package/dist/types/src/account/addresses-view.d.ts +5 -0
- package/dist/types/src/account/answers.d.ts +40 -0
- package/dist/types/src/account/markup.d.ts +19 -0
- package/dist/types/src/account/operations.d.ts +41 -0
- package/dist/types/src/account/order-lines.d.ts +13 -0
- package/dist/types/src/account/order-list.d.ts +12 -0
- package/dist/types/src/account/order-page.d.ts +6 -0
- package/dist/types/src/account/page-answers.d.ts +182 -0
- package/dist/types/src/account/page-events.d.ts +2 -0
- package/dist/types/src/account/server-client.d.ts +60 -0
- package/dist/types/src/account/subscriptions-view.d.ts +2 -0
- package/dist/types/src/account-api.d.ts +18 -0
- package/dist/types/src/account-marks.d.ts +15 -0
- package/dist/types/src/account-pages.d.ts +8 -0
- package/dist/types/src/account.d.ts +64 -0
- package/dist/types/src/amounts.d.ts +12 -0
- package/dist/types/src/analytics-shopify.d.ts +41 -0
- package/dist/types/src/analytics.d.ts +2 -0
- package/dist/types/src/api-version.d.ts +28 -0
- package/dist/types/src/astro.d.ts +42 -0
- package/dist/types/src/cart/cart-shape.d.ts +43 -0
- package/dist/types/src/cart/checkout.d.ts +16 -0
- package/dist/types/src/cart/drawer.d.ts +18 -0
- package/dist/types/src/cart/mutations.d.ts +79 -0
- package/dist/types/src/cart/render.d.ts +17 -0
- package/dist/types/src/cart/store.d.ts +182 -0
- package/dist/types/src/cart.d.ts +106 -0
- package/dist/types/src/client.d.ts +100 -0
- package/dist/types/src/collection.d.ts +25 -0
- package/dist/types/src/config-sections.d.ts +40 -0
- package/dist/types/src/config.d.ts +185 -0
- package/dist/types/src/controls/cart-controls.d.ts +2 -0
- package/dist/types/src/controls/discount.d.ts +2 -0
- package/dist/types/src/controls/extras.d.ts +5 -0
- package/dist/types/src/controls/navigation.d.ts +12 -0
- package/dist/types/src/diagnose-markup.d.ts +19 -0
- package/dist/types/src/diagnose.d.ts +60 -0
- package/dist/types/src/dom.d.ts +75 -0
- package/dist/types/src/extras.d.ts +21 -0
- package/dist/types/src/fragments.d.ts +158 -0
- package/dist/types/src/globals.d.ts +15 -0
- package/dist/types/src/images.d.ts +20 -0
- package/dist/types/src/index.d.ts +167 -0
- package/dist/types/src/initial.d.ts +56 -0
- package/dist/types/src/lazy.d.ts +28 -0
- package/dist/types/src/loader.d.ts +84 -0
- package/dist/types/src/market-codes.d.ts +6 -0
- package/dist/types/src/markets-ui.d.ts +32 -0
- package/dist/types/src/markets.d.ts +72 -0
- package/dist/types/src/markup.d.ts +131 -0
- package/dist/types/src/mounting.d.ts +46 -0
- package/dist/types/src/page-market.d.ts +27 -0
- package/dist/types/src/presets/barba.d.ts +56 -0
- package/dist/types/src/product-events.d.ts +2 -0
- package/dist/types/src/product-render.d.ts +17 -0
- package/dist/types/src/product-shape.d.ts +88 -0
- package/dist/types/src/product-state.d.ts +110 -0
- package/dist/types/src/product.d.ts +66 -0
- package/dist/types/src/repeaters.d.ts +16 -0
- package/dist/types/src/search.d.ts +33 -0
- package/dist/types/src/selling-plans.d.ts +64 -0
- package/dist/types/src/server.d.ts +88 -0
- package/dist/types/src/shopify-values.d.ts +46 -0
- package/dist/types/src/status.d.ts +33 -0
- package/dist/types/src/storage.d.ts +12 -0
- package/dist/types/src/store-html.d.ts +39 -0
- package/dist/types/src/storefront/catalog.d.ts +38 -0
- package/dist/types/src/storefront/json-ld.d.ts +20 -0
- package/dist/types/src/variant-render.d.ts +14 -0
- package/package.json +42 -22
- package/src/account/addresses-view.ts +58 -0
- package/src/account/answers.ts +79 -0
- package/src/account/markup.ts +36 -0
- package/src/account/operations.ts +145 -0
- package/src/account/order-lines.ts +75 -0
- package/src/account/order-list.ts +107 -0
- package/src/account/order-page.ts +137 -0
- package/src/account/page-answers.ts +317 -0
- package/src/account/page-events.ts +253 -0
- package/src/account/server-client.ts +183 -0
- package/src/account/subscriptions-view.ts +115 -0
- package/src/account-api.ts +98 -0
- package/src/account-marks.ts +35 -0
- package/src/account-pages.ts +81 -0
- package/src/account.ts +284 -0
- package/src/amounts.ts +64 -0
- package/src/analytics-shopify.ts +246 -0
- package/src/analytics.ts +315 -0
- package/src/api-version.ts +79 -0
- package/src/astro.ts +272 -0
- package/src/cart/cart-shape.ts +25 -0
- package/src/cart/checkout.ts +101 -0
- package/src/cart/drawer.ts +125 -0
- package/src/cart/mutations.ts +237 -0
- package/src/cart/render.ts +266 -0
- package/src/cart/store.ts +279 -0
- package/src/cart.ts +345 -0
- package/src/client.ts +391 -0
- package/src/collection.ts +175 -0
- package/src/config-sections.ts +284 -0
- package/src/config.ts +480 -0
- package/src/controls/cart-controls.ts +123 -0
- package/src/controls/discount.ts +75 -0
- package/src/controls/extras.ts +54 -0
- package/src/controls/navigation.ts +77 -0
- package/src/diagnose-markup.ts +298 -0
- package/src/diagnose.ts +294 -0
- package/src/dom.ts +268 -0
- package/src/extras.ts +136 -0
- package/src/fragments.ts +261 -0
- package/src/globals.d.ts +15 -0
- package/src/images.ts +91 -0
- package/src/index.ts +415 -0
- package/src/initial.ts +101 -0
- package/src/lazy-iife.ts +197 -0
- package/src/lazy.ts +42 -0
- package/src/loader.ts +363 -0
- package/src/market-codes.ts +6 -0
- package/src/markets-ui.ts +227 -0
- package/src/markets.ts +246 -0
- package/src/markup.ts +127 -0
- package/src/mounting.ts +264 -0
- package/src/page-market.ts +110 -0
- package/src/part-integrity.ts +7 -0
- package/src/parts/account-pages.ts +7 -0
- package/src/parts/account.ts +7 -0
- package/src/parts/analytics.ts +7 -0
- package/src/parts/diagnose.ts +7 -0
- package/src/parts/extras.ts +7 -0
- package/src/parts/markets.ts +7 -0
- package/src/parts/search.ts +7 -0
- package/src/presets/barba.ts +194 -0
- package/src/product-events.ts +115 -0
- package/src/product-render.ts +218 -0
- package/src/product-shape.ts +49 -0
- package/src/product-state.ts +318 -0
- package/src/product.ts +308 -0
- package/src/repeaters.ts +161 -0
- package/src/search.ts +305 -0
- package/src/selling-plans.ts +145 -0
- package/src/server.ts +211 -0
- package/src/shopify-values.ts +91 -0
- package/src/status.ts +106 -0
- package/src/storage.ts +34 -0
- package/src/store-html.ts +359 -0
- package/src/storefront/catalog.ts +196 -0
- package/src/storefront/json-ld.ts +144 -0
- package/src/variant-render.ts +139 -0
- package/dist/esm/chunks/account-Q6E2TFEV.js +0 -1
- package/dist/esm/chunks/account-pages-UZXR4R5F.js +0 -1
- package/dist/esm/chunks/analytics-IJBKATUP.js +0 -1
- package/dist/esm/chunks/chunk-4QJKLPYR.js +0 -1
- package/dist/esm/chunks/chunk-64TUD3QR.js +0 -1
- package/dist/esm/chunks/chunk-AJLPU2JC.js +0 -4
- package/dist/esm/chunks/chunk-FIOZPPC5.js +0 -1
- package/dist/esm/chunks/chunk-K2YEQAIH.js +0 -1
- package/dist/esm/chunks/chunk-KIDH76B7.js +0 -1
- package/dist/esm/chunks/chunk-PAYT5ZD7.js +0 -1
- package/dist/esm/chunks/chunk-RREHGH4A.js +0 -1
- package/dist/esm/chunks/chunk-SSJT43CJ.js +0 -1
- package/dist/esm/chunks/chunk-VC46IEJQ.js +0 -1
- package/dist/esm/chunks/diagnose-3WVAWNTX.js +0 -14
- package/dist/esm/chunks/extras-7I7WJX35.js +0 -1
- package/dist/esm/chunks/markets-ui-545XLFTE.js +0 -1
- package/dist/esm/chunks/search-NILMGYXG.js +0 -1
- package/src/account-pages.js +0 -318
- package/src/account.js +0 -385
- package/src/analytics.js +0 -232
- package/src/api-version.js +0 -35
- package/src/astro.js +0 -101
- package/src/cart.js +0 -559
- package/src/client.js +0 -131
- package/src/collection.js +0 -114
- package/src/config.js +0 -85
- package/src/diagnose.js +0 -246
- package/src/dom.js +0 -288
- package/src/extras.js +0 -63
- package/src/fragments.js +0 -144
- package/src/index.js +0 -395
- package/src/initial.js +0 -75
- package/src/lazy-iife.js +0 -46
- package/src/lazy.js +0 -18
- package/src/loader.js +0 -198
- package/src/markets-ui.js +0 -90
- package/src/markets.js +0 -133
- package/src/mounting.js +0 -154
- package/src/parts/account-pages.js +0 -3
- package/src/parts/account.js +0 -3
- package/src/parts/analytics.js +0 -3
- package/src/parts/diagnose.js +0 -3
- package/src/parts/extras.js +0 -3
- package/src/parts/markets.js +0 -3
- package/src/parts/search.js +0 -3
- package/src/presets/barba.js +0 -100
- package/src/product.js +0 -570
- package/src/search.js +0 -153
- package/src/selling-plans.js +0 -94
- package/src/server.js +0 -252
- package/src/status.js +0 -66
- package/types/astro.d.ts +0 -17
- package/types/barba.d.ts +0 -16
- package/types/index.d.ts +0 -426
- package/types/server.d.ts +0 -110
package/src/search.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search as you type (Shopify predictive search). Optional part: loaded only on pages with
|
|
3
|
-
* [tether-search].
|
|
4
|
-
*
|
|
5
|
-
* <form tether-search action="/search"> <!-- Enter: your full results page -->
|
|
6
|
-
* <input tether-search-input name="q" type="search" placeholder="Search">
|
|
7
|
-
* <div tether-search-results>
|
|
8
|
-
* <div tether-search-products><a><img tether-search-bind="image" max-width="120"><span tether-search-bind="title"></span> <span tether-search-bind="price"></span></a></div>
|
|
9
|
-
* <div tether-search-queries><a><span tether-search-bind="text"></span></a></div>
|
|
10
|
-
* <div tether-search-collections><a><span tether-search-bind="title"></span></a></div>
|
|
11
|
-
* <p tether-show="search-empty">No results</p>
|
|
12
|
-
* </div>
|
|
13
|
-
* </form>
|
|
14
|
-
*
|
|
15
|
-
* Each list's first child is the template. The root gets is-open while there are results (or none)
|
|
16
|
-
* to show, and tether-search-state="loading|ready|empty|error". Arrow keys move through the results,
|
|
17
|
-
* Escape closes. tether-search-limit (default 6), tether-search-min (characters, default 2).
|
|
18
|
-
*/
|
|
19
|
-
import { cfg, reportError } from './config.js';
|
|
20
|
-
import { gql } from './client.js';
|
|
21
|
-
import { attr, sel, get, qsa, setText, setImage, repeat, show, formatMoney, productUrl, emit } from './dom.js';
|
|
22
|
-
|
|
23
|
-
const QUERY = `query TetherPredictive($q: String!, $limit: Int!) {
|
|
24
|
-
predictiveSearch(query: $q, limit: $limit, limitScope: EACH, types: [PRODUCT, COLLECTION, QUERY]) {
|
|
25
|
-
products { id handle title availableForSale featuredImage { url altText } priceRange { minVariantPrice { amount currencyCode } } }
|
|
26
|
-
collections { handle title image { url altText } }
|
|
27
|
-
queries { text }
|
|
28
|
-
}
|
|
29
|
-
}`;
|
|
30
|
-
|
|
31
|
-
const mounted = new WeakSet();
|
|
32
|
-
let uid = 0;
|
|
33
|
-
|
|
34
|
-
export function mountSearch(root = document) {
|
|
35
|
-
const roots = [...(root.matches?.(sel('search')) ? [root] : []), ...qsa(root, sel('search'))];
|
|
36
|
-
roots.forEach((el) => { if (!mounted.has(el)) { mounted.add(el); setup(el); } });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function setup(rootEl) {
|
|
40
|
-
const input = rootEl.querySelector(sel('search-input')) || rootEl.querySelector('input');
|
|
41
|
-
const results = rootEl.querySelector(sel('search-results'));
|
|
42
|
-
if (!input || !results) return;
|
|
43
|
-
const limit = parseInt(get(rootEl, 'search-limit') || '6', 10) || 6;
|
|
44
|
-
const min = parseInt(get(rootEl, 'search-min') || '2', 10) || 2;
|
|
45
|
-
const id = results.id || (results.id = `${cfg.prefix}-search-${++uid}`);
|
|
46
|
-
input.setAttribute('role', 'combobox');
|
|
47
|
-
input.setAttribute('aria-autocomplete', 'list');
|
|
48
|
-
input.setAttribute('aria-controls', id);
|
|
49
|
-
input.setAttribute('aria-expanded', 'false');
|
|
50
|
-
input.setAttribute('autocomplete', 'off');
|
|
51
|
-
if (!results.hasAttribute('role')) results.setAttribute('role', 'listbox');
|
|
52
|
-
results.hidden = true;
|
|
53
|
-
|
|
54
|
-
let token = 0;
|
|
55
|
-
let timer = null;
|
|
56
|
-
let last = '';
|
|
57
|
-
const cache = new Map(); // term -> results, for typing back and forth
|
|
58
|
-
|
|
59
|
-
const setState = (state) => rootEl.setAttribute(attr('search-state'), state);
|
|
60
|
-
const open = (yes) => {
|
|
61
|
-
rootEl.classList.toggle('is-open', yes);
|
|
62
|
-
results.hidden = !yes;
|
|
63
|
-
input.setAttribute('aria-expanded', String(yes));
|
|
64
|
-
if (!yes) input.removeAttribute('aria-activedescendant');
|
|
65
|
-
};
|
|
66
|
-
const items = () => qsa(results, '[role="option"]');
|
|
67
|
-
|
|
68
|
-
async function run(term) {
|
|
69
|
-
const t = ++token;
|
|
70
|
-
if (term.length < min) { open(false); setState('idle'); return; }
|
|
71
|
-
setState('loading');
|
|
72
|
-
try {
|
|
73
|
-
const data = cache.get(term) || (await gql(QUERY, { q: term, limit })).predictiveSearch;
|
|
74
|
-
cache.set(term, data);
|
|
75
|
-
if (t !== token) return; // a newer search is on its way
|
|
76
|
-
render(data, term);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (t !== token) return;
|
|
79
|
-
setState('error');
|
|
80
|
-
reportError(err, 'search');
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function render(data, term) {
|
|
85
|
-
const products = data?.products || [];
|
|
86
|
-
const collections = data?.collections || [];
|
|
87
|
-
const queries = (data?.queries || []).filter((q) => q.text.toLowerCase() !== term.toLowerCase());
|
|
88
|
-
const fill = (name, list, each) => qsa(results, sel(name)).forEach((c) => {
|
|
89
|
-
repeat(c, list, each);
|
|
90
|
-
show(c, list.length > 0);
|
|
91
|
-
});
|
|
92
|
-
fill('search-products', products, (node, p) => {
|
|
93
|
-
binds(node, { title: p.title, price: formatMoney(p.priceRange?.minVariantPrice), image: p.featuredImage });
|
|
94
|
-
link(node, productUrl(p.handle, p.id));
|
|
95
|
-
node.classList.toggle('is-sold-out', !p.availableForSale);
|
|
96
|
-
});
|
|
97
|
-
fill('search-collections', collections, (node, c) => {
|
|
98
|
-
binds(node, { title: c.title, image: c.image });
|
|
99
|
-
link(node, `${(cfg.collectionPath || '/collection').replace(/\/$/, '')}/${c.handle}`);
|
|
100
|
-
});
|
|
101
|
-
fill('search-queries', queries, (node, q) => {
|
|
102
|
-
binds(node, { text: q.text });
|
|
103
|
-
const a = node.tagName === 'A' ? node : node.querySelector('a');
|
|
104
|
-
const action = rootEl.getAttribute('action') || '/search';
|
|
105
|
-
if (a) a.setAttribute('href', `${action}${action.includes('?') ? '&' : '?'}${input.name || 'q'}=${encodeURIComponent(q.text)}`);
|
|
106
|
-
});
|
|
107
|
-
const count = products.length + collections.length + queries.length;
|
|
108
|
-
qsa(rootEl, sel('show')).forEach((el) => {
|
|
109
|
-
const k = get(el, 'show');
|
|
110
|
-
if (k === 'search-empty') show(el, count === 0);
|
|
111
|
-
if (k === 'search-results') show(el, count > 0);
|
|
112
|
-
});
|
|
113
|
-
items().forEach((el, i) => { el.id ||= `${id}-${i}`; el.setAttribute('aria-selected', 'false'); });
|
|
114
|
-
setState(count ? 'ready' : 'empty');
|
|
115
|
-
open(true);
|
|
116
|
-
emit('search', { term, products, collections, queries }, rootEl);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function binds(node, values) {
|
|
120
|
-
qsa(node, sel('search-bind')).forEach((el) => {
|
|
121
|
-
const k = get(el, 'search-bind');
|
|
122
|
-
if (k === 'image') setImage(el, values.image?.url, values.image?.altText || values.title);
|
|
123
|
-
else if (values[k] != null) setText(el, values[k]);
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function link(node, href) {
|
|
128
|
-
const a = node.tagName === 'A' ? node : node.querySelector('a');
|
|
129
|
-
if (a) { a.setAttribute('href', href); a.setAttribute('role', 'option'); }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
input.addEventListener('input', () => {
|
|
133
|
-
const term = input.value.trim();
|
|
134
|
-
if (term === last) return;
|
|
135
|
-
last = term;
|
|
136
|
-
clearTimeout(timer);
|
|
137
|
-
timer = setTimeout(() => run(term), cfg.searchDelay ?? 150);
|
|
138
|
-
});
|
|
139
|
-
input.addEventListener('focus', () => { if (input.value.trim().length >= min && items().length) open(true); });
|
|
140
|
-
input.addEventListener('keydown', (e) => {
|
|
141
|
-
const list = items();
|
|
142
|
-
const cur = list.findIndex((el) => el.getAttribute('aria-selected') === 'true');
|
|
143
|
-
const move = (i) => {
|
|
144
|
-
list.forEach((el, j) => el.setAttribute('aria-selected', String(j === i)));
|
|
145
|
-
if (list[i]) { input.setAttribute('aria-activedescendant', list[i].id); list[i].scrollIntoView?.({ block: 'nearest' }); }
|
|
146
|
-
};
|
|
147
|
-
if (e.key === 'ArrowDown' && list.length) { e.preventDefault(); open(true); move((cur + 1) % list.length); }
|
|
148
|
-
else if (e.key === 'ArrowUp' && list.length) { e.preventDefault(); move(cur <= 0 ? list.length - 1 : cur - 1); }
|
|
149
|
-
else if (e.key === 'Enter' && cur >= 0) { e.preventDefault(); list[cur].click(); }
|
|
150
|
-
else if (e.key === 'Escape') { open(false); }
|
|
151
|
-
});
|
|
152
|
-
document.addEventListener('click', (e) => { if (!rootEl.contains(e.target)) open(false); });
|
|
153
|
-
}
|
package/src/selling-plans.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for Shopify selling plans (subscriptions). No DOM, easy to test.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const GID = {
|
|
6
|
-
product: (v) => toGid(v, 'Product'),
|
|
7
|
-
variant: (v) => toGid(v, 'ProductVariant'),
|
|
8
|
-
sellingPlan: (v) => toGid(v, 'SellingPlan'),
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function toGid(value, type) {
|
|
12
|
-
const v = String(value ?? '').trim();
|
|
13
|
-
if (!v) return null;
|
|
14
|
-
if (v.startsWith('gid://')) return v;
|
|
15
|
-
if (/^\d+$/.test(v)) return `gid://shopify/${type}/${v}`;
|
|
16
|
-
return v;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** All plans of a product, flattened, each with its group attached. */
|
|
20
|
-
export function allPlans(product) {
|
|
21
|
-
const groups = product?.sellingPlanGroups?.nodes || [];
|
|
22
|
-
return groups.flatMap((g) => (g.sellingPlans?.nodes || []).map((p) => ({ ...p, group: g })));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function hasPlans(product) {
|
|
26
|
-
return allPlans(product).length > 0;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** The allocation of `planId` on `variant`, or null when the variant isn't in that plan. */
|
|
30
|
-
export function allocationFor(variant, planId) {
|
|
31
|
-
if (!variant || !planId) return null;
|
|
32
|
-
return (variant.sellingPlanAllocations?.nodes || []).find((a) => a.sellingPlan?.id === planId) || null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Plans that can actually be bought with this variant. */
|
|
36
|
-
export function plansForVariant(product, variant) {
|
|
37
|
-
return allPlans(product).filter((p) => allocationFor(variant, p.id));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Price per delivery for a variant on a plan (falls back to the one-time price). */
|
|
41
|
-
export function planPrice(variant, planId) {
|
|
42
|
-
const a = allocationFor(variant, planId);
|
|
43
|
-
const adj = a?.priceAdjustments?.[0];
|
|
44
|
-
return adj?.perDeliveryPrice || adj?.price || variant?.price || null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** What the customer pays at checkout for this plan (prepaid plans charge more up front). */
|
|
48
|
-
export function planCheckoutCharge(variant, planId) {
|
|
49
|
-
const a = allocationFor(variant, planId);
|
|
50
|
-
return a?.checkoutChargeAmount || a?.priceAdjustments?.[0]?.price || variant?.price || null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Savings of a plan vs the one-time price, as { amount, percentage }. */
|
|
54
|
-
export function planSavings(variant, planId) {
|
|
55
|
-
const base = parseFloat(variant?.price?.amount);
|
|
56
|
-
const sub = parseFloat(planPrice(variant, planId)?.amount);
|
|
57
|
-
if (!Number.isFinite(base) || !Number.isFinite(sub) || base <= 0 || sub >= base) {
|
|
58
|
-
return { amount: null, percentage: 0 };
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
amount: { amount: (base - sub).toFixed(2), currencyCode: variant.price.currencyCode },
|
|
62
|
-
percentage: Math.round((1 - sub / base) * 100),
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The plan's own discount description, independent of a variant:
|
|
68
|
-
* '10%', or a fixed amount / fixed price.
|
|
69
|
-
*/
|
|
70
|
-
export function planAdjustment(plan) {
|
|
71
|
-
const v = plan?.priceAdjustments?.[0]?.adjustmentValue;
|
|
72
|
-
if (!v) return null;
|
|
73
|
-
switch (v.__typename) {
|
|
74
|
-
case 'SellingPlanPercentagePriceAdjustment': return { type: 'percentage', value: v.adjustmentPercentage };
|
|
75
|
-
case 'SellingPlanFixedAmountPriceAdjustment': return { type: 'amount', value: v.adjustmentAmount };
|
|
76
|
-
case 'SellingPlanFixedPriceAdjustment': return { type: 'price', value: v.price };
|
|
77
|
-
default: return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Finds a plan by id, numeric id, name or option value (case-insensitive),
|
|
83
|
-
* so Webflow editors can write tether-default-plan="Every month".
|
|
84
|
-
*/
|
|
85
|
-
export function findPlan(product, ref) {
|
|
86
|
-
if (!ref) return null;
|
|
87
|
-
const plans = allPlans(product);
|
|
88
|
-
const gid = GID.sellingPlan(ref);
|
|
89
|
-
const needle = String(ref).trim().toLowerCase();
|
|
90
|
-
return plans.find((p) => p.id === gid)
|
|
91
|
-
|| plans.find((p) => p.name.toLowerCase() === needle)
|
|
92
|
-
|| plans.find((p) => (p.options || []).some((o) => String(o.value).toLowerCase() === needle))
|
|
93
|
-
|| null;
|
|
94
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @occluze/tether/server: Shopify product data at build time or on the server (Astro, Eleventy,
|
|
3
|
-
* Node, Cloudflare Workers). No browser APIs. Uses the same queries as the browser script, so
|
|
4
|
-
* products handed over with hydrate() are exactly what Tether expects and need no extra request.
|
|
5
|
-
*
|
|
6
|
-
* import { createStorefront } from '@occluze/tether/server';
|
|
7
|
-
* const shop = createStorefront({ domain, token, country: 'NL' });
|
|
8
|
-
* const product = await shop.getProduct('coffee-box');
|
|
9
|
-
* const all = await shop.allProducts();
|
|
10
|
-
*/
|
|
11
|
-
import { DEFAULTS } from './config.js';
|
|
12
|
-
import { request, withContext, TetherError } from './client.js';
|
|
13
|
-
import { productFragment, collectionFields } from './fragments.js';
|
|
14
|
-
import * as plans from './selling-plans.js';
|
|
15
|
-
import { imageUrl, formatPercent } from './dom.js';
|
|
16
|
-
import { initialState } from './initial.js';
|
|
17
|
-
|
|
18
|
-
export { imageUrl, initialState, formatPercent };
|
|
19
|
-
|
|
20
|
-
export { TetherError, plans, withContext };
|
|
21
|
-
|
|
22
|
-
const isGid = (v) => typeof v === 'string' && v.startsWith('gid://');
|
|
23
|
-
|
|
24
|
-
/** Handle, numeric id, gid or product URL -> handle or gid. */
|
|
25
|
-
export function productKey(raw) {
|
|
26
|
-
const v = String(raw ?? '').trim();
|
|
27
|
-
if (!v) return null;
|
|
28
|
-
if (isGid(v)) return v;
|
|
29
|
-
if (/^\d+$/.test(v)) return `gid://shopify/Product/${v}`;
|
|
30
|
-
const m = v.match(/\/products\/([^/?#]+)/);
|
|
31
|
-
return m ? m[1] : v;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Money as text, e.g. formatMoney({ amount: '30.0', currencyCode: 'EUR' }, 'nl-NL') -> "€ 30,00". */
|
|
35
|
-
export function formatMoney(money, locale) {
|
|
36
|
-
if (!money) return '';
|
|
37
|
-
const amount = parseFloat(money.amount);
|
|
38
|
-
if (Number.isNaN(amount)) return '';
|
|
39
|
-
try {
|
|
40
|
-
return new Intl.NumberFormat(locale || undefined, { style: 'currency', currency: money.currencyCode }).format(amount);
|
|
41
|
-
} catch {
|
|
42
|
-
return `${money.currencyCode} ${amount.toFixed(2)}`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** productPath '/products' -> /products/<handle>; templates '/p?handle={handle}' and '{id}' work too. */
|
|
47
|
-
export function productUrl(handle, { productPath = '/product', id } = {}) {
|
|
48
|
-
if (!/\{(handle|id)\}/.test(productPath)) return `${productPath.replace(/\/$/, '')}/${handle}`;
|
|
49
|
-
return productPath.replace(/\{handle\}/g, encodeURIComponent(handle || '')).replace(/\{id\}/g, encodeURIComponent(id ? String(id).split('/').pop() : ''));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** JSON for a <script> tag: escapes < so product text can never close the tag. */
|
|
53
|
-
const safeJson = (v) => JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
54
|
-
|
|
55
|
-
// createStorefront(import.meta.env) works too: these env names map to domain and token.
|
|
56
|
-
const ENV = {
|
|
57
|
-
domain: ['PUBLIC_SHOPIFY_DOMAIN', 'PUBLIC_SHOPIFY_STORE_DOMAIN', 'SHOPIFY_DOMAIN'],
|
|
58
|
-
token: ['PUBLIC_SHOPIFY_STOREFRONT_TOKEN', 'PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN', 'SHOPIFY_STOREFRONT_TOKEN'],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export function createStorefront(options = {}) {
|
|
62
|
-
const known = Object.fromEntries(Object.entries(options).filter(([k]) => k in DEFAULTS || k === 'fetch' || k === 'pageSize' || k === 'siteUrl'));
|
|
63
|
-
const conf = { ...DEFAULTS, ...known };
|
|
64
|
-
for (const [key, names] of Object.entries(ENV)) if (!conf[key]) conf[key] = names.map((n) => options[n]).find(Boolean) || '';
|
|
65
|
-
conf.domain = String(conf.domain || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '');
|
|
66
|
-
if (!conf.domain || !conf.token) {
|
|
67
|
-
throw new TetherError('createStorefront needs domain and token: pass them, or import.meta.env with PUBLIC_SHOPIFY_DOMAIN and PUBLIC_SHOPIFY_STOREFRONT_TOKEN', { code: 'NOT_CONFIGURED' });
|
|
68
|
-
}
|
|
69
|
-
const pageSize = options.pageSize || 25;
|
|
70
|
-
const q = (query, variables, opts) => request(conf, query, variables, opts);
|
|
71
|
-
const fragment = () => productFragment(conf);
|
|
72
|
-
|
|
73
|
-
async function getProduct(raw) {
|
|
74
|
-
const key = productKey(raw);
|
|
75
|
-
if (!key) return null;
|
|
76
|
-
const data = isGid(key)
|
|
77
|
-
? await q(`query TetherServerProductById($id: ID!) { node(id: $id) { ... on Product { ...TetherProduct } } }\n${fragment()}`, { id: key })
|
|
78
|
-
: await q(`query TetherServerProduct($handle: String!) { product(handle: $handle) { ...TetherProduct } }\n${fragment()}`, { handle: key });
|
|
79
|
-
const p = isGid(key) ? data.node : data.product;
|
|
80
|
-
return p && p.id ? p : null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function getProducts({ collection, query, first = pageSize, after = null, sortKey, reverse = false, filters } = {}) {
|
|
84
|
-
if (collection) {
|
|
85
|
-
const data = await q(`query TetherServerCollection($handle: String!, $first: Int!, $after: String, $sortKey: ProductCollectionSortKeys, $reverse: Boolean, $filters: [ProductFilter!]) {
|
|
86
|
-
collection(handle: $handle) {
|
|
87
|
-
${collectionFields()}
|
|
88
|
-
products(first: $first, after: $after, sortKey: $sortKey, reverse: $reverse, filters: $filters) {
|
|
89
|
-
nodes { ...TetherProduct }
|
|
90
|
-
pageInfo { hasNextPage endCursor }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
${fragment()}`, { handle: collection, first, after, sortKey: sortKey || undefined, reverse, filters: filters || undefined });
|
|
95
|
-
if (!data.collection) return { collection: null, products: [], pageInfo: { hasNextPage: false, endCursor: null } };
|
|
96
|
-
const { products, ...rest } = data.collection;
|
|
97
|
-
return { collection: rest, products: products.nodes, pageInfo: products.pageInfo };
|
|
98
|
-
}
|
|
99
|
-
const data = await q(`query TetherServerProducts($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) {
|
|
100
|
-
products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) {
|
|
101
|
-
nodes { ...TetherProduct }
|
|
102
|
-
pageInfo { hasNextPage endCursor }
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
${fragment()}`, { first, after, query: query || undefined, sortKey: sortKey || undefined, reverse });
|
|
106
|
-
return { collection: null, products: data.products.nodes, pageInfo: data.products.pageInfo };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Every product (of a collection or a search), page by page. For getStaticPaths(). */
|
|
110
|
-
async function allProducts(opts = {}) {
|
|
111
|
-
const out = [];
|
|
112
|
-
let after = null;
|
|
113
|
-
for (let i = 0; i < 500; i++) {
|
|
114
|
-
const page = await getProducts({ ...opts, first: pageSize, after });
|
|
115
|
-
out.push(...page.products);
|
|
116
|
-
if (!page.pageInfo.hasNextPage) break;
|
|
117
|
-
after = page.pageInfo.endCursor;
|
|
118
|
-
}
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function getCollection(handle, opts = {}) {
|
|
123
|
-
const page = await getProducts({ ...opts, collection: handle });
|
|
124
|
-
return page.collection ? { ...page.collection, products: page.products, pageInfo: page.pageInfo } : null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function allCollections() {
|
|
128
|
-
const out = [];
|
|
129
|
-
let after = null;
|
|
130
|
-
for (let i = 0; i < 100; i++) {
|
|
131
|
-
const data = await q(`query TetherServerCollections($first: Int!, $after: String) {
|
|
132
|
-
collections(first: $first, after: $after) { nodes { ${collectionFields()} } pageInfo { hasNextPage endCursor } }
|
|
133
|
-
}`, { first: 100, after });
|
|
134
|
-
out.push(...data.collections.nodes);
|
|
135
|
-
if (!data.collections.pageInfo.hasNextPage) break;
|
|
136
|
-
after = data.collections.pageInfo.endCursor;
|
|
137
|
-
}
|
|
138
|
-
return out;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** By product id (gid or numeric) or handle, so it can run next to getProduct(handle). */
|
|
142
|
-
async function getRecommendations(product, intent = 'RELATED') {
|
|
143
|
-
const key = productKey(typeof product === 'object' ? product?.id : product);
|
|
144
|
-
const byId = key?.startsWith('gid://');
|
|
145
|
-
const data = await q(`query TetherServerRecs(${byId ? '$id: ID!' : '$handle: String!'}, $intent: ProductRecommendationIntent) {
|
|
146
|
-
productRecommendations(${byId ? 'productId: $id' : 'productHandle: $handle'}, intent: $intent) { ...TetherProduct }
|
|
147
|
-
}
|
|
148
|
-
${fragment()}`, byId ? { id: key, intent } : { handle: key, intent });
|
|
149
|
-
return data.productRecommendations || [];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* A <script> tag that hands products to the browser script: Tether renders them at once,
|
|
154
|
-
* without its own request, then refreshes prices and stock in the background.
|
|
155
|
-
* Put it anywhere in the page (Astro: <Fragment set:html={shop.hydrate(products)} />).
|
|
156
|
-
*/
|
|
157
|
-
/**
|
|
158
|
-
* { card: true } for lists (home, collections, recommendations): about 40% smaller, without
|
|
159
|
-
* descriptions, variant images and images after the first two (a hover image, tether-bind="image:2";
|
|
160
|
-
* more with { card: true, images: 3 }). Price, stock and plans per variant stay. The full
|
|
161
|
-
* product comes with the background refresh.
|
|
162
|
-
*/
|
|
163
|
-
function hydrate(products, { card = false, images = 2 } = {}) {
|
|
164
|
-
let list = (Array.isArray(products) ? products : [products]).filter(Boolean);
|
|
165
|
-
if (card) list = list.map((p) => slim(p, images));
|
|
166
|
-
const payload = { v: 1, at: new Date().toISOString(), country: conf.country || null, language: conf.language || null, products: list };
|
|
167
|
-
return `<script type="application/json" data-tether-hydrate>${safeJson(payload)}</script>`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* schema.org data for search engines (Google's product rich results): a Product, or a ProductGroup
|
|
172
|
-
* with one Product per variant (price, currency, stock, sku, image, link to ?variant=).
|
|
173
|
-
* jsonLd(product, { url: 'https://shop.example/products/x' }) -> the <script> tag; jsonLdData() -> the object.
|
|
174
|
-
*/
|
|
175
|
-
function jsonLdData(p, { url, brand } = {}) {
|
|
176
|
-
if (!p) return null;
|
|
177
|
-
const pageUrl = url || (conf.siteUrl ? new URL(productUrl(p.handle, { productPath: conf.productPath, id: p.id }), conf.siteUrl).toString() : undefined);
|
|
178
|
-
const vid = (v) => String(v.id).split('/').pop();
|
|
179
|
-
const withVariant = (v) => (pageUrl ? `${pageUrl}${pageUrl.includes('?') ? '&' : '?'}variant=${vid(v)}` : undefined);
|
|
180
|
-
const text = (p.description || '').trim() || undefined;
|
|
181
|
-
const images = (p.images?.nodes || []).map((i) => i.url).filter(Boolean);
|
|
182
|
-
const brandName = brand || p.vendor;
|
|
183
|
-
// Only buyable on a subscription: the plan price is the price (the first plan the variant has).
|
|
184
|
-
const priceOf = (v) => {
|
|
185
|
-
if (!p.requiresSellingPlan) return v.price;
|
|
186
|
-
const plan = plans.allPlans(p).find((pl) => plans.allocationFor(v, pl.id));
|
|
187
|
-
return plan ? plans.planPrice(v, plan.id) : v.price;
|
|
188
|
-
};
|
|
189
|
-
const offer = (v) => ({
|
|
190
|
-
'@type': 'Offer',
|
|
191
|
-
price: priceOf(v)?.amount,
|
|
192
|
-
priceCurrency: priceOf(v)?.currencyCode,
|
|
193
|
-
availability: `https://schema.org/${v.availableForSale ? (v.currentlyNotInStock ? 'BackOrder' : 'InStock') : 'OutOfStock'}`,
|
|
194
|
-
url: withVariant(v),
|
|
195
|
-
});
|
|
196
|
-
const vs = p.variants?.nodes || [];
|
|
197
|
-
const clean = (o) => JSON.parse(JSON.stringify(o)); // drops undefined
|
|
198
|
-
if (vs.length <= 1) {
|
|
199
|
-
const v = vs[0];
|
|
200
|
-
return clean({
|
|
201
|
-
'@context': 'https://schema.org', '@type': 'Product', name: p.title, description: text, url: pageUrl,
|
|
202
|
-
image: images, sku: v?.sku || undefined, brand: brandName ? { '@type': 'Brand', name: brandName } : undefined,
|
|
203
|
-
offers: v ? offer(v) : undefined,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
const varies = { size: 'https://schema.org/size', color: 'https://schema.org/color', colour: 'https://schema.org/color', material: 'https://schema.org/material' };
|
|
207
|
-
return clean({
|
|
208
|
-
'@context': 'https://schema.org', '@type': 'ProductGroup', name: p.title, description: text, url: pageUrl,
|
|
209
|
-
productGroupID: String(p.id).split('/').pop(), image: images,
|
|
210
|
-
brand: brandName ? { '@type': 'Brand', name: brandName } : undefined,
|
|
211
|
-
variesBy: [...new Set((p.options || []).map((o) => varies[o.name.toLowerCase()]).filter(Boolean))],
|
|
212
|
-
hasVariant: vs.map((v) => ({
|
|
213
|
-
'@type': 'Product',
|
|
214
|
-
name: `${p.title} - ${v.title}`,
|
|
215
|
-
sku: v.sku || undefined,
|
|
216
|
-
image: v.image?.url || images[0],
|
|
217
|
-
...Object.fromEntries((v.selectedOptions || []).filter((o) => varies[o.name.toLowerCase()]).map((o) => [varies[o.name.toLowerCase()].split('/').pop(), o.value])),
|
|
218
|
-
offers: offer(v),
|
|
219
|
-
})),
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
const jsonLd = (p, opts) => (p ? `<script type="application/ld+json">${safeJson(jsonLdData(p, opts))}</script>` : '');
|
|
223
|
-
|
|
224
|
-
function slim(p, keep = 2) {
|
|
225
|
-
return {
|
|
226
|
-
...p,
|
|
227
|
-
description: '', descriptionHtml: '',
|
|
228
|
-
images: { nodes: (p.images?.nodes || []).slice(0, Math.max(1, keep)) },
|
|
229
|
-
variants: { nodes: (p.variants?.nodes || []).map((v) => ({ ...v, image: null })) },
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
config: conf,
|
|
235
|
-
query: q,
|
|
236
|
-
getProduct,
|
|
237
|
-
getProducts,
|
|
238
|
-
allProducts,
|
|
239
|
-
getCollection,
|
|
240
|
-
allCollections,
|
|
241
|
-
getRecommendations,
|
|
242
|
-
hydrate,
|
|
243
|
-
formatMoney: (money, locale = conf.locale) => formatMoney(money, locale),
|
|
244
|
-
formatPercent: (n, locale = conf.locale) => formatPercent(n, locale),
|
|
245
|
-
imageUrl,
|
|
246
|
-
jsonLd,
|
|
247
|
-
jsonLdData,
|
|
248
|
-
initialState: (product, opts = {}) => initialState(product, { defaultPurchase: conf.defaultPurchase, lowStock: conf.lowStock, ...opts }),
|
|
249
|
-
productUrl: (handle, opts = {}) => productUrl(handle, { productPath: conf.productPath, ...opts }),
|
|
250
|
-
plans,
|
|
251
|
-
};
|
|
252
|
-
}
|
package/src/status.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where a product scope gets its key from, and the loading / ready / error /
|
|
3
|
-
* not-found state of a scope (so a failing API never leaves a broken page).
|
|
4
|
-
*/
|
|
5
|
-
import { attr, sel, get, scoped, show } from './dom.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* tether-product values:
|
|
9
|
-
* "coffee-box" handle, numeric id, gid or product URL (as before)
|
|
10
|
-
* "url:handle" ?handle=... from the page URL (any param name: "url:p" -> ?p=)
|
|
11
|
-
* "url:path" the last segment of the page path (/shop/coffee-box -> coffee-box)
|
|
12
|
-
* Returns { key, fromUrl, raw }. key is null when the URL has no value.
|
|
13
|
-
*/
|
|
14
|
-
export function resolveProductKey(el, location = window.location, name = 'product') {
|
|
15
|
-
const raw = (get(el, name) || '').trim();
|
|
16
|
-
if (!raw.toLowerCase().startsWith('url:')) return { key: raw || null, fromUrl: false, raw };
|
|
17
|
-
const what = raw.slice(4).trim();
|
|
18
|
-
let key = null;
|
|
19
|
-
if (what.toLowerCase() === 'path') {
|
|
20
|
-
const parts = location.pathname.split('/').filter(Boolean);
|
|
21
|
-
key = parts.length ? safeDecode(parts[parts.length - 1]) : null;
|
|
22
|
-
} else {
|
|
23
|
-
key = new URLSearchParams(location.search).get(what || 'handle');
|
|
24
|
-
}
|
|
25
|
-
key = key && key.trim() ? key.trim() : null;
|
|
26
|
-
return { key, fromUrl: true, raw };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function safeDecode(s) { try { return decodeURIComponent(s); } catch { return s; } }
|
|
30
|
-
|
|
31
|
-
/** Values of tether-show that follow the scope state. */
|
|
32
|
-
export const STATUS_KEYS = ['loading', 'ready', 'error', 'not-found'];
|
|
33
|
-
|
|
34
|
-
export const isNotFound = (err) => err?.code === 'NOT_FOUND';
|
|
35
|
-
|
|
36
|
-
/** Product scopes and lists both own their state elements; the nearest one wins. */
|
|
37
|
-
export const boundary = () => ['product', 'collection', 'products', 'recommendations'].map((n) => sel(n)).join(',');
|
|
38
|
-
|
|
39
|
-
/** True when `el` belongs to the scope `root` (not to a nested product or list). */
|
|
40
|
-
export const ownedBy = (el, root) => el === root || el.parentElement?.closest(boundary()) === root;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Sets tether-state (loading | ready | error) and tether-error (not-found or an
|
|
44
|
-
* error code) on `root`, toggles tether-show="loading|ready|error|not-found"
|
|
45
|
-
* inside it, and disables add-to-cart buttons when there's no product.
|
|
46
|
-
*/
|
|
47
|
-
export function setScopeStatus(root, status, err) {
|
|
48
|
-
const notFound = status === 'not-found';
|
|
49
|
-
root.setAttribute(attr('state'), notFound ? 'error' : status);
|
|
50
|
-
if (status === 'error' || notFound) root.setAttribute(attr('error'), notFound ? 'not-found' : (err?.code || 'error'));
|
|
51
|
-
else root.removeAttribute(attr('error'));
|
|
52
|
-
|
|
53
|
-
scoped(root, sel('show'), boundary()).forEach((el) => {
|
|
54
|
-
const k = get(el, 'show');
|
|
55
|
-
if (!STATUS_KEYS.includes(k)) return;
|
|
56
|
-
show(el, k === status);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (status === 'error' || notFound) {
|
|
60
|
-
scoped(root, sel('add-to-cart'), boundary()).forEach((btn) => {
|
|
61
|
-
btn.classList.add('is-disabled');
|
|
62
|
-
btn.setAttribute('aria-disabled', 'true');
|
|
63
|
-
if ('disabled' in btn) btn.disabled = true;
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
package/types/astro.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { TetherConfig } from './index.js';
|
|
2
|
-
|
|
3
|
-
type Serializable<T> = { [K in keyof T as T[K] extends ((...args: any[]) => any) | null | undefined ? (Exclude<T[K], null | undefined> extends (...args: any[]) => any ? never : K) : K]: T[K] };
|
|
4
|
-
|
|
5
|
-
export interface TetherAstroOptions extends Partial<Serializable<TetherConfig>> {
|
|
6
|
-
/** Add the Tether script to every page (default true). False: import and init it yourself. */
|
|
7
|
-
inject?: boolean;
|
|
8
|
-
/** Files that restart the dev server when they change (default: src/shop.config.js/.mjs/.ts). */
|
|
9
|
-
watch?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Astro integration: Tether on every page, store and token from .env
|
|
14
|
-
* (PUBLIC_SHOPIFY_DOMAIN, PUBLIC_SHOPIFY_STOREFRONT_TOKEN). Functions (onError, hooks) go in
|
|
15
|
-
* window.TETHER_CONFIG in your layout.
|
|
16
|
-
*/
|
|
17
|
-
export default function tether(options?: TetherAstroOptions): { name: string; hooks: Record<string, (...args: any[]) => unknown> };
|
package/types/barba.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { TetherConfig } from './index.js';
|
|
2
|
-
|
|
3
|
-
export interface BarbaPresetOptions {
|
|
4
|
-
gsap?: () => unknown;
|
|
5
|
-
lenis?: (() => { resize?(): void; stop?(): void; start?(): void } | null | undefined) | null;
|
|
6
|
-
scrollTrigger?: () => unknown;
|
|
7
|
-
lockScroll?: ((drawer: HTMLElement) => void) | null;
|
|
8
|
-
unlockScroll?: (() => void) | null;
|
|
9
|
-
onResize?: (() => void) | null;
|
|
10
|
-
onValueChange?: ((el: HTMLElement, previousText: string) => void) | null;
|
|
11
|
-
animateLines?: boolean;
|
|
12
|
-
lineDuration?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Hooks for sites with page transitions, smooth scroll and GSAP: spread into Tether.init(). */
|
|
16
|
-
export declare function barbaPreset(options?: BarbaPresetOptions): Partial<TetherConfig>;
|