@buildinams/core 0.3.1 → 0.4.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 +24 -1
- package/dist/analytics/google/index.d.ts +158 -0
- package/dist/analytics/google/index.js +274 -0
- package/dist/analytics/google/index.js.map +1 -0
- package/dist/analytics/index.d.ts +101 -0
- package/dist/analytics/index.js +384 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/shopify/index.d.ts +186 -0
- package/dist/analytics/shopify/index.js +175 -0
- package/dist/analytics/shopify/index.js.map +1 -0
- package/dist/{chunk-5KDGGGVO.js → chunk-FJ7SNQQI.js} +24 -6
- package/dist/chunk-FJ7SNQQI.js.map +1 -0
- package/dist/chunk-G2BKC4BU.js +8 -0
- package/dist/chunk-G2BKC4BU.js.map +1 -0
- package/dist/chunk-GQVN2ECX.js +14 -0
- package/dist/chunk-GQVN2ECX.js.map +1 -0
- package/dist/chunk-LL3VTFNY.js +22 -0
- package/dist/chunk-LL3VTFNY.js.map +1 -0
- package/dist/chunk-M3HNM5HC.js +48 -0
- package/dist/chunk-M3HNM5HC.js.map +1 -0
- package/dist/chunk-MHFGMH3G.js +11 -0
- package/dist/chunk-MHFGMH3G.js.map +1 -0
- package/dist/chunk-OS2T6P2T.js +50 -0
- package/dist/chunk-OS2T6P2T.js.map +1 -0
- package/dist/chunk-QIJGG2F4.js +51 -0
- package/dist/chunk-QIJGG2F4.js.map +1 -0
- package/dist/consent/cookiebot/index.d.ts +16 -0
- package/dist/consent/cookiebot/index.js +27 -0
- package/dist/consent/cookiebot/index.js.map +1 -0
- package/dist/consent/google/index.d.ts +23 -0
- package/dist/consent/google/index.js +48 -0
- package/dist/consent/google/index.js.map +1 -0
- package/dist/consent/index.d.ts +1 -0
- package/dist/consent/index.js +3 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/consent/onetrust/index.d.ts +23 -0
- package/dist/consent/onetrust/index.js +39 -0
- package/dist/consent/onetrust/index.js.map +1 -0
- package/dist/consent/shopify/index.d.ts +44 -0
- package/dist/consent/shopify/index.js +84 -0
- package/dist/consent/shopify/index.js.map +1 -0
- package/dist/cookies/index.d.ts +230 -0
- package/dist/cookies/index.js +188 -0
- package/dist/cookies/index.js.map +1 -0
- package/dist/define-events-CxZjoUlG.d.ts +255 -0
- package/dist/destination-5KJ5PeR3.d.ts +122 -0
- package/dist/index-Zef1nnTP.d.ts +36 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/listeners-DkU-vBPa.d.ts +15 -0
- package/dist/navigation/index.js +1 -1
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +1 -1
- package/dist/store/index.js +1 -1
- package/dist/tracking/cart/index.d.ts +74 -0
- package/dist/tracking/cart/index.js +118 -0
- package/dist/tracking/cart/index.js.map +1 -0
- package/dist/tracking/dom/index.d.ts +320 -0
- package/dist/tracking/dom/index.js +1196 -0
- package/dist/tracking/dom/index.js.map +1 -0
- package/docs/analytics.md +883 -0
- package/docs/cookies.md +354 -0
- package/docs/decisions/001-analytics-project-events.md +31 -0
- package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
- package/docs/decisions/003-tracking-element-data.md +27 -0
- package/docs/decisions/004-cookies-explicit-store.md +26 -0
- package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
- package/docs/decisions/006-cookies-server-only-types.md +19 -0
- package/docs/decisions/007-cookies-options-on-definition.md +25 -0
- package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
- package/docs/decisions/009-analytics-consent-targets.md +29 -0
- package/docs/decisions/010-analytics-destination-contract.md +29 -0
- package/docs/decisions/011-tracking-decoupled.md +30 -0
- package/docs/tracking.md +221 -0
- package/package.json +83 -2
- package/dist/chunk-5KDGGGVO.js.map +0 -1
- package/dist/chunk-SIU7SKJK.js +0 -139
- package/dist/chunk-SIU7SKJK.js.map +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Tracking reports to listeners; analytics takes trackers as sources
|
|
2
|
+
|
|
3
|
+
Date: 2026-11-11
|
|
4
|
+
|
|
5
|
+
Status: accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
The markup and cart trackers took the analytics instance, and per-event handlers typed by it. That tied them to analytics for tracking, for ordering (`ecommerce()` subscribed to the instance to flush batched views), and for types (plugins created outside the call needed annotations).
|
|
10
|
+
|
|
11
|
+
Decoupled, every project wrote the same glue, `domWatcher.subscribe(({ name, payload }) => analytics.track(name, payload))`. A module re-run by HMR subscribed again and tracked everything twice unless it unsubscribed first.
|
|
12
|
+
|
|
13
|
+
Options considered: keep the instance and return typed helpers, depend on a structural `{ track, subscribe }`, or report to a callback; and for the glue, a `connect(analytics, tracker)` helper or `sources` on `createAnalytics()`.
|
|
14
|
+
|
|
15
|
+
## Decision
|
|
16
|
+
|
|
17
|
+
- **Trackers report occurrences to listeners.** `createDomWatcher(plugins)` and `createCartChanges<Item>()` have no side effects; `subscribe(listener)` hands what they report (`clicked`, `itemsViewed`, `addedToCart`, …) to whoever listens, and the first subscription starts them.
|
|
18
|
+
- **`createAnalytics({ sources })`** tracks what trackers report under the same name. Every event a source reports must be in the catalog with a compatible payload. A source feeds one instance: a later `createAnalytics()` with it takes it over.
|
|
19
|
+
- **Plugins carry their types.** `track()` comes from the watcher and only takes its plugins' types; plugins take the project's own fields, e.g. `clicks<{ cta: string }>()`.
|
|
20
|
+
- **Ordering lives in the watcher:** `ecommerce()` flushes its batch before anything else the watcher reports. `domWatcher.flush()` and `domWatcher.newPage()` cover events tracked outside it.
|
|
21
|
+
- **Its own exports:** `@buildinams/core/tracking/dom` and `/tracking/cart` import nothing from analytics but the catalog types and `shape()`.
|
|
22
|
+
|
|
23
|
+
## Consequences
|
|
24
|
+
|
|
25
|
+
- Trackers work with any sink: analytics, `dataLayer`, a test spy.
|
|
26
|
+
- HMR re-running the analytics module doesn't double-track, and `createAnalytics()` is what visibly starts the trackers.
|
|
27
|
+
- Renaming an occurrence means subscribing yourself instead of using `sources`, and handling HMR yourself; `track()` checks the result against the catalog.
|
|
28
|
+
- `track()` comes from the project's tracking module, which server components can import.
|
|
29
|
+
- A cart change can reach destinations before views `ecommerce()` still batches; GA4 doesn't need that order.
|
|
30
|
+
- A tracker nobody subscribes to tracks nothing, silently. `createCartChanges()` holds the last cart passed before its first `subscribe()`.
|
package/docs/tracking.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Tracking
|
|
2
|
+
|
|
3
|
+
Two trackers report what happens, so call sites don't have to:
|
|
4
|
+
|
|
5
|
+
- **`createDomWatcher(plugins)`** from `@buildinams/core/tracking/dom`: page views, product and promotion views and selections, clicks, videos, forms and scroll depth, from marked elements.
|
|
6
|
+
- **`createCartChanges<CartItem>()`** from `@buildinams/core/tracking/cart`: adds and removes, from the confirmed carts you pass it.
|
|
7
|
+
|
|
8
|
+
They track nothing themselves: they report an _occurrence_ to their `subscribe()` listeners, and do nothing until the first one. Usually that's [`createAnalytics({ sources })`](analytics.md#sources), which tracks each under the same name; their `events` are the catalog entries for it. Create them in a module without side effects, so server components can import `track()`.
|
|
9
|
+
|
|
10
|
+
## What they report
|
|
11
|
+
|
|
12
|
+
| Reported by | Occurrences |
|
|
13
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `pageViews()`, `clicks()`, `videos()`, `forms()`, `scroll()` | `pageViewed`, `clicked`, `videoStarted`/`Progressed`/`Completed`, `formStarted`/`Submitted`, `scrolled` |
|
|
15
|
+
| `ecommerce()` | `itemViewed`, `itemSelected`, `itemsViewed`, `promotionViewed`, `promotionSelected` |
|
|
16
|
+
| `createCartChanges()` | `addedToCart`, `removedFromCart` |
|
|
17
|
+
|
|
18
|
+
Markup fields are the library's, in camelCase (`elementId`, `videoPercent`, …), plus the plugin's own; ecommerce and cart occurrences have a fixed structure with your data in it:
|
|
19
|
+
|
|
20
|
+
| Occurrence | Payload | Placement |
|
|
21
|
+
| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
22
|
+
| `itemViewed` | `{ item, list?, index?, promotion? }` | Where the product was last selected, e.g. the list whose card opened its page. |
|
|
23
|
+
| `itemSelected` | `{ item, list?, index?, promotion? }` | The list the item is in, its position from 0, and the promotion around it. |
|
|
24
|
+
| `itemsViewed` | `{ list?, items: { item, index?, promotion? }[] }` | One per list and batch; without a list for items outside any. |
|
|
25
|
+
| `promotionViewed` | `{ promotion, items }` | `items` are the products featured in it outside a list; `[]` when none. |
|
|
26
|
+
| `promotionSelected` | `{ promotion, item? }` | `item` when the click was on a featured product. |
|
|
27
|
+
| `addedToCart`, … | `{ currency, value, items: { item, quantity, price }[] }` | See [Tracking the cart](#cart). |
|
|
28
|
+
|
|
29
|
+
## Subscribing
|
|
30
|
+
|
|
31
|
+
To track an occurrence under another name, or not at all, subscribe yourself instead of passing the tracker to `sources`. `event` is a union of what the tracker reports, so `switch (event.name)` narrows it:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
domWatcher.subscribe((event) => {
|
|
35
|
+
switch (event.name) {
|
|
36
|
+
case "itemsViewed":
|
|
37
|
+
return analytics.track("productListViewed", {
|
|
38
|
+
list: event.payload.list,
|
|
39
|
+
products: event.payload.items.map(({ item }) => item),
|
|
40
|
+
});
|
|
41
|
+
case "promotionViewed":
|
|
42
|
+
case "promotionSelected":
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
analytics.track(event.name, event.payload);
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Only some events:** `subscribe("clicked", listener)` or `subscribe(["itemsViewed", "itemSelected"], listener)`, narrowed by name.
|
|
50
|
+
- **The element:** markup occurrences, except `ecommerce()`'s, have `element`: the page root, link, `<video>`, `<form>` or scrolled element.
|
|
51
|
+
- **Errors are isolated:** a listener that throws is logged as `[analytics] <occurrence>`, and the others still receive it.
|
|
52
|
+
- **Subscribe once:** a module that subscribes and re-runs, e.g. on HMR, unsubscribes first, or events are tracked twice. `sources` handles this for you.
|
|
53
|
+
- **Map them yourself:** mapping builders like `ga4Mapping()` only cover the occurrences' names.
|
|
54
|
+
|
|
55
|
+
## Markup
|
|
56
|
+
|
|
57
|
+
`createDomWatcher(plugins)` reports what happens on marked elements, with one listener per event type on the root. Elements say what they are with space-separated types in `data-track`, written by the watcher's `track(spec)`: one object with a key per type, whose props you spread on the element.
|
|
58
|
+
|
|
59
|
+
- **Attribute types**, for page views, clicks, locations, auto tracking, videos, forms and scroll, take a few strings, written as plain attributes, e.g. `track({ form: { id: "newsletter" } })` is `data-track="form" data-track-id="newsletter"`. Types whose fields are all optional take `true`: `track({ form: true })`. Server components and CMS templates can write them.
|
|
60
|
+
- **Data types**, for products, lists and promotions (`viewItem`, `quickView`, `list`, `item`, `promotion`), take your data and add a `ref`, which sets it on the element from a client component, so it stays out of the HTML ([Element data](#element-data), [ADR 003](decisions/003-tracking-element-data.md)).
|
|
61
|
+
|
|
62
|
+
Nothing is tracked without its type ([ADR 002](decisions/002-tracking-dom-opt-in.md)). The first `subscribe()` starts the plugins on `root` (`createDomWatcher(plugins, { root })`, `document` by default), and the last unsubscribe stops them. The watcher also has `events` for the catalog, `newPage()` ([Page views without `pageViews()`](#page-views-without-pageviews)) and `flush()`.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export const domWatcher = createDomWatcher([
|
|
66
|
+
pageViews(),
|
|
67
|
+
ecommerce<{ item: Item; list: List; promotion: Promotion }>({
|
|
68
|
+
id: ({ product }) => product.id,
|
|
69
|
+
}),
|
|
70
|
+
clicks(),
|
|
71
|
+
videos(),
|
|
72
|
+
forms(),
|
|
73
|
+
scroll(),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
export const { track } = domWatcher;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Markup | Plugin | Reports |
|
|
80
|
+
| ------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `track({ pageView: { pageType } })` on each page's root | `pageViews()` | `pageViewed` when it appears or is shown again, once per URL |
|
|
82
|
+
| `track({ viewItem: { product, variant } })` on a product page | `ecommerce()` | `itemViewed` when visible, once per URL for each product (by `id`) |
|
|
83
|
+
| `track({ quickView: item })` | `ecommerce()` | `itemViewed` each time it opens |
|
|
84
|
+
| `track({ promotion })` | `ecommerce()` | `promotionViewed` once per page, `promotionSelected` for clicks inside |
|
|
85
|
+
| `track({ list })` | `ecommerce()` | Places the items inside it in the list, with their `index` |
|
|
86
|
+
| `track({ item: { product } })` on each product | `ecommerce()` | `itemsViewed` once per page, batched per list; `itemSelected` for clicks inside |
|
|
87
|
+
| `track({ auto: ["links", "forms"] })` on an area | `clicks()`, `forms()` | `clicked` for each `<a href>` and form events for each form inside. `false` turns it off for a subtree. |
|
|
88
|
+
| `track({ location: { name: "header" } })` on an area | `clicks()` | `location` on every `clicked` inside it |
|
|
89
|
+
| `track({ click: { id: "size-guide" } })` on a button or link | `clicks()` | `clicked` with `elementId`, `url`, `text` and `outbound`. Unmarked buttons aren't tracked. |
|
|
90
|
+
| `track({ video: { title } })` on a `<video>` or an ancestor | `videos()` | `videoStarted`, `videoProgressed` at 10, 25, 50 and 75%, `videoCompleted` |
|
|
91
|
+
| `track({ form: { id: "newsletter" } })` on a form | `forms()` | `formStarted` on the first input, once per page; `formSubmitted`. Never field values. |
|
|
92
|
+
| `track({ scroll: { id } })` on a page's root or an article | `scroll()` | `scrolled` with `percentScrolled` at 25, 50, 75 and 90% (`scroll({ thresholds })`), once per URL |
|
|
93
|
+
|
|
94
|
+
- **Only what's installed:** `track()` only takes the types of the watcher's plugins, e.g. `track({ form: true })` is a compile error without `forms()`, and `auto` without `clicks()` or `forms()`.
|
|
95
|
+
- **Your own fields:** `clicks<{ cta: string }>()` makes `track({ click: { cta } })` require `cta`, written as `data-track-click-cta`, and adds it to `clicked`. The other plugins take fields the same way.
|
|
96
|
+
- **By hand**, e.g. in a CMS: `data-track="auto-links"`, `data-track="click" data-track-id="size-guide"`, `data-track="location" data-track-name="footer"`. Types are kebab-case in HTML: `page-view`, `auto-none`. Without an id, `click`, `form` and `scroll` fall back to the element's `id`. A comma between types makes a different type, which nothing tracks.
|
|
97
|
+
- **Combining:** several types on one element are one `track()` call with a key for each, as spreading two keeps only the last `data-track`. React passes a spread `ref` through:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
<section {...track({ promotion, location: { name: "hero" } })}>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- **One of each plugin per root:** watchers with different plugins work side by side; a watcher started later with the same kind replaces the earlier one's, e.g. after HMR. Plugins handle an event in the table's order, so `itemSelected` comes before `clicked`. Custom plugins aren't supported yet.
|
|
104
|
+
|
|
105
|
+
### Behaviour
|
|
106
|
+
|
|
107
|
+
- **Page views** are reported when the element is added or shown again, so put it on each page's root. Hidden elements wait until shown: Next.js with Cache Components keeps visited pages mounted but hidden in `<Activity>`. A query string change doesn't remount the page: key the element on the search params to count those.
|
|
108
|
+
- **Product views** count when half visible, or filling half the viewport (`ecommerce({ threshold })`), so a closed `<dialog>` or hidden overlay doesn't count. Use an element with a box, not `display: contents`. A variant counts again only when it changes the URL (with the Navigation API, also when the URL changes after the data); a quick view once per opening. A page opened in a background tab counts once shown.
|
|
109
|
+
- **Placement:** a product selected from a list gets its `list`, `index` and `promotion` on its next `itemViewed`, also in a quick view outside the list or on its product page, matched by `ecommerce()`'s `id`. The tab keeps the placements of the last 50 products selected in `sessionStorage` (`ecommerce({ storageKey })`, default `"analytics-placements"`), so a page loaded in full gets them too; change the key when your list's or promotion's shape changes. Without `id`, the item's JSON is its id, so a card's `{ product }` won't match a page's `{ product, variant }`.
|
|
110
|
+
- **Views are batched** for 500 ms (`ecommerce({ batchDelay })`) or 30 per list, and reported before anything else the watcher reports, and when the visitor leaves. Events tracked elsewhere don't wait; call `domWatcher.flush()` first when the order matters.
|
|
111
|
+
- **After a client-side navigation**, products and promotions still in view are viewed again: after `pageViewed`, `domWatcher.newPage()`, or else the first event after the path changes.
|
|
112
|
+
- **Auto tracking:** put `track({ auto: ["links", "forms"] })` on the header, navigation and footer, with a `location`, so reports can group clicks. A rich-text renderer wraps its output in `track({ auto: "links" })`. The nearest `auto` decides; `click` and `form` on an element still track it.
|
|
113
|
+
- **Stable ids:** link text changes with the locale and generated ids (`useId()`) between builds, so give what reports use a fixed `id`.
|
|
114
|
+
- **Videos** are opt-in, so decorative loops aren't tracked; looping videos never complete. YouTube and Vimeo embeds aren't covered.
|
|
115
|
+
- **Scroll depth** is how much of the element has passed the bottom of the viewport, checked per frame while scrolling, also for a restored scroll position. Hidden elements aren't tracked.
|
|
116
|
+
- **Forms:** `formSubmitted` fires when the browser submits, after built-in validation, even if a handler then cancels it.
|
|
117
|
+
|
|
118
|
+
### Element data
|
|
119
|
+
|
|
120
|
+
A data type's `ref` sets the data after mount, so only client components can set it.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
"use client";
|
|
124
|
+
|
|
125
|
+
const ProductGridItem = ({ product }: { product: Product }) => (
|
|
126
|
+
<li {...track({ item: { product } })}>
|
|
127
|
+
<ProductCard product={product} />
|
|
128
|
+
</li>
|
|
129
|
+
);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Server components** can't set data: render the element from a client component, e.g. a `<li>` around a server-rendered card passed as `children`.
|
|
133
|
+
- **Before hydration** an element has no data, so it isn't tracked yet, clicks included.
|
|
134
|
+
- **Every render** can pass new objects: equal data is ignored. Changed data, e.g. a new price, is viewed again, once per page.
|
|
135
|
+
- **Keep it lean:** data is keyed and compared as JSON. Or pass a function, `track({ item: () => toItem(product) })`, built when first read; keep it stable (`useCallback`) when that's costly.
|
|
136
|
+
- **Without data**, e.g. `track({ quickView: product && { product, variant } })` while closed, the element isn't tracked as one.
|
|
137
|
+
- **Forgotten refs** leave an element untracked; in development, `ecommerce()` warns after 2 seconds.
|
|
138
|
+
- **Own refs:** call both, e.g. `const { ref, ...quickView } = track({ quickView: item })` and `ref={(el) => { dialogRef.current = el; ref(el); }}`.
|
|
139
|
+
- **Without a framework**, call the `ref` with the element: `track({ item: data }).ref(element)` sets the data and adds `item` to `data-track`, `track({ quickView: undefined }).ref(element)` removes both.
|
|
140
|
+
|
|
141
|
+
### Promotions with products
|
|
142
|
+
|
|
143
|
+
A promotion can contain product cards. What's tracked depends on whether they're inside a `list`:
|
|
144
|
+
|
|
145
|
+
| Markup | Views | Click on a product |
|
|
146
|
+
| --------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
147
|
+
| `promotion` around `item` cards (recommended) | `promotionViewed` with the products in `items` | `promotionSelected` with that product as `item` |
|
|
148
|
+
| `promotion` around a `list` with `item` cards | `promotionViewed` with `items: []`, and `itemsViewed` with the `promotion` | `itemSelected` with the `promotion` |
|
|
149
|
+
|
|
150
|
+
Clicks on other links inside are `promotionSelected`. Add the list only when the products should also be reported as a list.
|
|
151
|
+
|
|
152
|
+
## Cart
|
|
153
|
+
|
|
154
|
+
`createCartChanges<CartItem>({ storageKey })` from `@buildinams/core/tracking/cart` reports `addedToCart` and `removedFromCart` by comparing the carts the server confirms, so buttons, quantity pickers and drawers don't each track. Pass it every confirmed cart:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// Wherever a confirmed cart arrives: a server action's response, a store, a refetch.
|
|
158
|
+
cartChanges.setCart({
|
|
159
|
+
id: cart.id,
|
|
160
|
+
currency: "EUR",
|
|
161
|
+
lines: cart.lines.map((line) => ({
|
|
162
|
+
id: line.id,
|
|
163
|
+
quantity: line.quantity,
|
|
164
|
+
price: line.unitPrice, // after discounts
|
|
165
|
+
item: { product: line.product, variant: line.variant },
|
|
166
|
+
})),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// No cart, e.g. before the first add or after checkout.
|
|
170
|
+
cartChanges.setCart(null);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
On Shopify, `toStorefrontCartSnapshot(cart)` from `@buildinams/core/analytics/shopify` turns a Storefront API cart into a `CartSnapshot<ShopifyStorefrontCartLine>`; use `ShopifyStorefrontCartLine` as the cart item and `toStorefrontCartProduct` in `shopifyMapping()`.
|
|
174
|
+
|
|
175
|
+
| Change | Reports |
|
|
176
|
+
| -------------------------- | ------------------------------------------------------ |
|
|
177
|
+
| A new line | `addedToCart` with its quantity |
|
|
178
|
+
| A higher or lower quantity | `addedToCart` or `removedFromCart` with the difference |
|
|
179
|
+
| A removed line | `removedFromCart` with its quantity |
|
|
180
|
+
| A swap, e.g. another size | `removedFromCart`, then `addedToCart` |
|
|
181
|
+
| A cart with another id | `addedToCart` for all its lines |
|
|
182
|
+
| `null` | Nothing |
|
|
183
|
+
|
|
184
|
+
- **One occurrence per kind and update**, with `value` as the sum of `price × quantity` of what changed.
|
|
185
|
+
- **Lines are matched by `id`**, so use one that's stable while the line is in the cart.
|
|
186
|
+
- **Confirmed carts only:** pass what the server returned, not optimistic state.
|
|
187
|
+
- **The first cart is the starting point**, so a page loaded with a full cart tracks nothing, unless an earlier page stored a different one, e.g. after a form that adds and redirects.
|
|
188
|
+
- **Once across tabs:** the last cart is kept in `localStorage` (`storageKey`, default `"analytics-cart"`). Change the key when your item's shape changes.
|
|
189
|
+
- **Before the first `subscribe()`**, the last cart passed is held, and reported to every subscription made in the same task, e.g. when a child's effect runs before its parent subscribes.
|
|
190
|
+
|
|
191
|
+
## Page views without `pageViews()`
|
|
192
|
+
|
|
193
|
+
To track page views from the router instead of markup, track from a client component every page renders:
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
"use client";
|
|
197
|
+
|
|
198
|
+
import type { PageType } from "@buildinams/core/tracking/dom";
|
|
199
|
+
|
|
200
|
+
export const PageView = ({ pageType }: { pageType: PageType }) => {
|
|
201
|
+
const pathname = usePathname();
|
|
202
|
+
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
domWatcher.newPage();
|
|
205
|
+
analytics.track("pageViewed", { pageType });
|
|
206
|
+
}, [pathname, pageType]);
|
|
207
|
+
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- **Declare `pageViewed`** in the catalog yourself, e.g. with `payload: shape<{ pageType?: PageType }>()`, as the watcher's `events` only has it with `pageViews()`.
|
|
213
|
+
- **`pathname` is a dependency**, as two product pages can share the component without remounting.
|
|
214
|
+
- **`domWatcher.newPage()`** flushes the previous page's views and re-views what stays in view; leave it out without a markup watcher.
|
|
215
|
+
- **Query strings:** add `useSearchParams()` to the dependencies and wrap `<PageView />` in `<Suspense>`.
|
|
216
|
+
|
|
217
|
+
## Debugging
|
|
218
|
+
|
|
219
|
+
- **Log what's reported:** `domWatcher.subscribe((event) => console.debug(event))`.
|
|
220
|
+
- **See an element's data:** `$0[Symbol.for("@buildinams/core/tracking/dom")]` in the console, per type.
|
|
221
|
+
- **An occurrence missing?** Nothing subscribes to its tracker, the element lacks its type or data, or a listener returned early for it or threw.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildinams/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
5
6
|
"author": "Build in Amsterdam <development@buildinamsterdam.com> (https://www.buildinamsterdam.com/)",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
"build": "tsup",
|
|
15
16
|
"dev": "tsup --watch",
|
|
16
17
|
"lint": "eslint .",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
17
19
|
"test": "vitest watch",
|
|
18
20
|
"test:ci": "vitest run",
|
|
19
21
|
"lint:fix": "eslint . --fix"
|
|
@@ -25,6 +27,42 @@
|
|
|
25
27
|
"types": "./dist/index.d.ts",
|
|
26
28
|
"default": "./dist/index.js"
|
|
27
29
|
},
|
|
30
|
+
"./analytics": {
|
|
31
|
+
"types": "./dist/analytics/index.d.ts",
|
|
32
|
+
"default": "./dist/analytics/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./analytics/google": {
|
|
35
|
+
"types": "./dist/analytics/google/index.d.ts",
|
|
36
|
+
"default": "./dist/analytics/google/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./analytics/shopify": {
|
|
39
|
+
"types": "./dist/analytics/shopify/index.d.ts",
|
|
40
|
+
"default": "./dist/analytics/shopify/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./consent": {
|
|
43
|
+
"types": "./dist/consent/index.d.ts",
|
|
44
|
+
"default": "./dist/consent/index.js"
|
|
45
|
+
},
|
|
46
|
+
"./consent/cookiebot": {
|
|
47
|
+
"types": "./dist/consent/cookiebot/index.d.ts",
|
|
48
|
+
"default": "./dist/consent/cookiebot/index.js"
|
|
49
|
+
},
|
|
50
|
+
"./consent/google": {
|
|
51
|
+
"types": "./dist/consent/google/index.d.ts",
|
|
52
|
+
"default": "./dist/consent/google/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./consent/shopify": {
|
|
55
|
+
"types": "./dist/consent/shopify/index.d.ts",
|
|
56
|
+
"default": "./dist/consent/shopify/index.js"
|
|
57
|
+
},
|
|
58
|
+
"./consent/onetrust": {
|
|
59
|
+
"types": "./dist/consent/onetrust/index.d.ts",
|
|
60
|
+
"default": "./dist/consent/onetrust/index.js"
|
|
61
|
+
},
|
|
62
|
+
"./cookies": {
|
|
63
|
+
"types": "./dist/cookies/index.d.ts",
|
|
64
|
+
"default": "./dist/cookies/index.js"
|
|
65
|
+
},
|
|
28
66
|
"./navigation": {
|
|
29
67
|
"types": "./dist/navigation/index.d.ts",
|
|
30
68
|
"default": "./dist/navigation/index.js"
|
|
@@ -36,6 +74,14 @@
|
|
|
36
74
|
"./store": {
|
|
37
75
|
"types": "./dist/store/index.d.ts",
|
|
38
76
|
"default": "./dist/store/index.js"
|
|
77
|
+
},
|
|
78
|
+
"./tracking/cart": {
|
|
79
|
+
"types": "./dist/tracking/cart/index.d.ts",
|
|
80
|
+
"default": "./dist/tracking/cart/index.js"
|
|
81
|
+
},
|
|
82
|
+
"./tracking/dom": {
|
|
83
|
+
"types": "./dist/tracking/dom/index.d.ts",
|
|
84
|
+
"default": "./dist/tracking/dom/index.js"
|
|
39
85
|
}
|
|
40
86
|
},
|
|
41
87
|
"typesVersions": {
|
|
@@ -43,6 +89,33 @@
|
|
|
43
89
|
"*": [
|
|
44
90
|
"./dist/index.d.ts"
|
|
45
91
|
],
|
|
92
|
+
"./analytics": [
|
|
93
|
+
"./dist/analytics/index.d.ts"
|
|
94
|
+
],
|
|
95
|
+
"./analytics/google": [
|
|
96
|
+
"./dist/analytics/google/index.d.ts"
|
|
97
|
+
],
|
|
98
|
+
"./analytics/shopify": [
|
|
99
|
+
"./dist/analytics/shopify/index.d.ts"
|
|
100
|
+
],
|
|
101
|
+
"./consent": [
|
|
102
|
+
"./dist/consent/index.d.ts"
|
|
103
|
+
],
|
|
104
|
+
"./consent/cookiebot": [
|
|
105
|
+
"./dist/consent/cookiebot/index.d.ts"
|
|
106
|
+
],
|
|
107
|
+
"./consent/onetrust": [
|
|
108
|
+
"./dist/consent/onetrust/index.d.ts"
|
|
109
|
+
],
|
|
110
|
+
"./consent/google": [
|
|
111
|
+
"./dist/consent/google/index.d.ts"
|
|
112
|
+
],
|
|
113
|
+
"./consent/shopify": [
|
|
114
|
+
"./dist/consent/shopify/index.d.ts"
|
|
115
|
+
],
|
|
116
|
+
"./cookies": [
|
|
117
|
+
"./dist/cookies/index.d.ts"
|
|
118
|
+
],
|
|
46
119
|
"./navigation": [
|
|
47
120
|
"./dist/navigation/index.d.ts"
|
|
48
121
|
],
|
|
@@ -51,11 +124,18 @@
|
|
|
51
124
|
],
|
|
52
125
|
"./store": [
|
|
53
126
|
"./dist/store/index.d.ts"
|
|
127
|
+
],
|
|
128
|
+
"./tracking/cart": [
|
|
129
|
+
"./dist/tracking/cart/index.d.ts"
|
|
130
|
+
],
|
|
131
|
+
"./tracking/dom": [
|
|
132
|
+
"./dist/tracking/dom/index.d.ts"
|
|
54
133
|
]
|
|
55
134
|
}
|
|
56
135
|
},
|
|
57
136
|
"files": [
|
|
58
137
|
"dist",
|
|
138
|
+
"docs",
|
|
59
139
|
"README.md"
|
|
60
140
|
],
|
|
61
141
|
"publishConfig": {
|
|
@@ -64,9 +144,10 @@
|
|
|
64
144
|
},
|
|
65
145
|
"devDependencies": {
|
|
66
146
|
"@buildinams/lint": "^0.4.0",
|
|
147
|
+
"next": "^16.4.5",
|
|
67
148
|
"tsup": "^8.0.1",
|
|
68
149
|
"typescript": "^5.4.5",
|
|
69
|
-
"vitest": "^
|
|
150
|
+
"vitest": "^5.1.0"
|
|
70
151
|
},
|
|
71
152
|
"dependencies": {
|
|
72
153
|
"clsx": "^2.1.1"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schema/schema.ts","../src/schema/string-schema.ts","../src/schema/href-schema.ts"],"names":[],"mappings":";AAkGO,IAAM,YAAA,GAAe,CAK3B,QAAA,MACmD;AAAA,EACnD,GAAG,QAAA;AAAA,EAEH,YAAA,EAAc,CAAC,KAAA,EAAO,IAAA,KAAS;AAC9B,IAAA,OAAO,SAAS,SAAA,CAAU,IAAA,CAAK,SAAS,KAAA,CAAM,KAAK,CAAC,CAAC,CAAA;AAAA,EACtD;AACD,CAAA;;;AC9FO,IAAM,kBAAA,GAAqB,CACjC,QAAA,KAC4C;AAC5C,EAAA,MAAM,SAAS,YAAA,CAA+C;AAAA,IAC7D,WAAW,QAAA,CAAS,SAAA;AAAA,IACpB,OAAO,QAAA,CAAS;AAAA,GAChB,CAAA;AAED,EAAA,OAAO;AAAA,IACN,OAAO,MAAA,CAAO,KAAA;AAAA,IACd,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,gBAAgB,MAAA,CAAO;AAAA,GACxB;AACD;;;ACKO,IAAM,aAAa,kBAAA,CASxB;AAAA,EACD,KAAA,EAAO,CAAC,IAAA,KAAS;AAChB,IAAA,MAAM,CAAC,WAAW,EAAA,EAAI,MAAA,GAAS,EAAE,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACnD,IAAA,OAAO;AAAA,MACN,QAAA;AAAA,MACA,YAAA,EAAc,IAAI,eAAA,CAAgB,MAAM;AAAA,KACzC;AAAA,EACD,CAAA;AAAA,EACA,SAAA,EAAW,CAAC,KAAA,KAAU;AACrB,IAAA,IAAI,CAAC,KAAA,CAAM,YAAA,EAAc,OAAO,KAAA,CAAM,QAAA;AACtC,IAAA,MAAM,YAAA,GAAe,KAAA,CAAM,YAAA,CAAa,QAAA,EAAS;AACjD,IAAA,OAAO,KAAA,CAAM,QAAA,IAAY,YAAA,GAAe,CAAA,CAAA,EAAI,YAAY,CAAA,CAAA,GAAK,EAAA,CAAA;AAAA,EAC9D;AACD,CAAC","file":"chunk-5KDGGGVO.js","sourcesContent":["/**\n * Defines the schema for parsing and stringifying states to and from primitive values.\n *\n * The `Schema` type is a generic type that takes:\n * - `State`: A state object type.\n * - `Serialized`: The raw value type\n * - `SerializableState?`: (optional) A (partial) state object type that can be used for serialization. Defaults to `State` if not provided.\n *\n * It returns the following methods:\n * - `parse`: A function that takes a raw value and returns the corresponding state object.\n * - `serialize`: A function that takes a (serializable) state object and returns a raw value that represents the given state.\n * - `parserialize`: A function that takes a raw value and a reducer function - it returns a new raw value that represents the reduced state.\n *\n * @see createSchema for a helper function to create a `Schema` from simpler parsing and serializing functions.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type Schema<State = any, Serialized = any, SerializableState = State> = {\n\t/** Parse a raw value into the state shape */\n\tparse: (value: Serialized) => State;\n\t/** Serialize a state shape into a raw value */\n\tserialize: (state: SerializableState) => Serialized;\n\n\t/** Compute a new serialized value based on a given serialized value and a reducer of the current state. */\n\tparserialize: (\n\t\tvalue: Serialized,\n\t\tnext: (state: State) => SerializableState,\n\t) => Serialized;\n};\n\n/**\n * A utility type to extract the state shape from a given `Schema`.\n */\nexport type SchemaState<T extends Schema> =\n\tT extends Schema<infer U> ? U : never;\n\n/**\n * A utility type to extract the serialized value type from a given `Schema`.\n */\nexport type SchemaSerialized<T extends Schema> =\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Schema<any, infer U> ? U : never;\n\n/**\n * A utility type to extract the serializable state shape from a given `Schema`.\n */\nexport type SchemaSerializableState<T extends Schema> =\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tT extends Schema<any, any, infer U> ? U : never;\n\n/**\n * Low-level helper function to create a basic `Schema` that receives the config for converting between serialized values and state objects.\n *\n * The template requires two methods:\n * - `parse`: A method that takes a serialized value and returns the corresponding state object.\n * - `serialize`: A method that takes a state object and returns a serialized value that represents the given state.\n *\n * The `parserialize` method is automatically implemented based on the provided `parse` and `serialize` methods, allowing you to compute a new serialized value based on an existing serialized value and a reducer function for the state.\n *\n * @example\n * ```ts\n * import { createSchema } from \"@buildinams/core/schema\";\n *\n * const numberSchema = createSchema<\n * number, // State\n * string // Serialized\n * >({\n * parse: (value) => parseInt(value, 10),\n * serialize: (state) => state.toString(),\n * });\n *\n * const result = numberSchema.parserialize(\"42\", (state) => state + 1);\n * console.log(result); // \"43\"\n * ```\n *\n * @example\n * ```ts\n * import { createSchema } from \"@buildinams/core/schema\";\n *\n * const hrefSchema: createSchema<\n * { pathname: string, searchParams: URLSearchParams },\n * string,\n * { pathname: string, searchParams?: URLSearchParams } // SerializableState allows to define different state used for serialization, in this case making searchParams optional to allow for more flexible stringification logic\n * >({\n * parse: (href) => {\n * const [pathname = \"\", search = \"\"] = href.split(\"?\");\n * return {\n * pathname,\n * searchParams: new URLSearchParams(search),\n * };\n * },\n * serialize: (serializableState) => {\n * if (!serializableState.searchParams) return serializableState.pathname;\n * const paramsString = serializableState.searchParams.toString();\n * return serializableState.pathname + (paramsString ? `?${paramsString}` : \"\");\n * },\n * })\n * ```\n */\nexport const createSchema = <\n\tState = never,\n\tSerialized = never,\n\tSerializableState = State,\n>(\n\ttemplate: Omit<Schema<State, Serialized, SerializableState>, \"parserialize\">,\n): Schema<State, Serialized, SerializableState> => ({\n\t...template,\n\n\tparserialize: (value, next) => {\n\t\treturn template.serialize(next(template.parse(value)));\n\t},\n});\n","import type { Schema } from \"./schema\";\nimport { createSchema } from \"./schema\";\n\nexport type StringSchema<State = never, SerializableState = State> = {\n\tparse: Schema<State, string, SerializableState>[\"parse\"];\n\tstringify: Schema<State, string, SerializableState>[\"serialize\"];\n\tparseStringify: Schema<State, string, SerializableState>[\"parserialize\"];\n};\n\n/**\n * Helper function to create a `StringSchema` which converts between strings and state objects.\n *\n * The template requires two functions:\n * - `parse`: A function that takes a string and returns the corresponding state object.\n * - `stringify`: A function that takes a state object and returns a string that represents the given state.\n */\nexport const createStringSchema = <State = never, SerializableState = State>(\n\ttemplate: Omit<StringSchema<State, SerializableState>, \"parseStringify\">,\n): StringSchema<State, SerializableState> => {\n\tconst schema = createSchema<State, string, SerializableState>({\n\t\tserialize: template.stringify,\n\t\tparse: template.parse,\n\t});\n\n\treturn {\n\t\tparse: schema.parse,\n\t\tstringify: schema.serialize,\n\t\tparseStringify: schema.parserialize,\n\t};\n};\n","import { createStringSchema } from \"./string-schema\";\n\n/**\n * Utility schema for parsing and stringifying href strings into structured state objects that include the pathname and URL search parameters.\n *\n * @example\n * ```ts\n * import { createSchema, hrefSchema } from \"@buildinams/core/schema\";\n *\n * const myHrefSchema = createSchema<{\n * page: number,\n * // Used to compute next href - don't use directly. Use `.stringify()` instead\n * _href: string\n * }>({\n * parse: (href) => {\n * const { searchParams } = hrefSchema.parse(href);\n * return {\n * page: parseInt(searchParams.get(\"page\") || \"1\", 10),\n * _href: href,\n * }\n * },\n * serialize: (state) => {\n * const { pathname, searchParams } = hrefSchema.parse(state._href);\n * searchParams.delete(\"page\");\n * if (state.page > 1) {\n * searchParams.set(\"page\", state.page.toString());\n * }\n *\n * // note that `searchParams` are optional for serialization\n * return hrefSchema.stringify({ pathname, searchParams });\n * }\n * });\n * ```\n */\nexport const hrefSchema = createStringSchema<\n\t{\n\t\tpathname: string;\n\t\tsearchParams: URLSearchParams;\n\t},\n\t{\n\t\tpathname: string;\n\t\tsearchParams?: URLSearchParams;\n\t}\n>({\n\tparse: (href) => {\n\t\tconst [pathname = \"\", search = \"\"] = href.split(\"?\");\n\t\treturn {\n\t\t\tpathname,\n\t\t\tsearchParams: new URLSearchParams(search),\n\t\t};\n\t},\n\tstringify: (state) => {\n\t\tif (!state.searchParams) return state.pathname;\n\t\tconst paramsString = state.searchParams.toString();\n\t\treturn state.pathname + (paramsString ? `?${paramsString}` : \"\");\n\t},\n});\n"]}
|
package/dist/chunk-SIU7SKJK.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
|
|
3
|
-
// src/config/index.ts
|
|
4
|
-
var IS_DEVELOPMENT = process.env.NODE_ENV === "development";
|
|
5
|
-
var IS_BUILD = process.env.NODE_ENV === "production";
|
|
6
|
-
var IS_IN_BROWSER = (() => {
|
|
7
|
-
try {
|
|
8
|
-
return !!window && "document" in window && !!window.document?.createElement;
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
})();
|
|
13
|
-
|
|
14
|
-
// src/functions/apply-state.ts
|
|
15
|
-
function isUpdater(argument) {
|
|
16
|
-
return typeof argument === "function";
|
|
17
|
-
}
|
|
18
|
-
function isLazyValue(value) {
|
|
19
|
-
return typeof value === "function";
|
|
20
|
-
}
|
|
21
|
-
function applyState(argument, currentValue) {
|
|
22
|
-
if (isUpdater(argument)) {
|
|
23
|
-
const value = isLazyValue(currentValue) ? currentValue() : currentValue;
|
|
24
|
-
return argument(value);
|
|
25
|
-
}
|
|
26
|
-
return argument;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// src/functions/default-value.ts
|
|
30
|
-
function defaultValue(...values) {
|
|
31
|
-
for (const value of values) {
|
|
32
|
-
if (value !== void 0) return value;
|
|
33
|
-
}
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// src/functions/identity.ts
|
|
38
|
-
function identity(value) {
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/functions/math.ts
|
|
43
|
-
var wrap = (value, [min, max]) => {
|
|
44
|
-
const range = max - min + 1;
|
|
45
|
-
return ((value - min) % range + range) % range + min;
|
|
46
|
-
};
|
|
47
|
-
var clamp = (value, [min, max]) => {
|
|
48
|
-
return Math.min(Math.max(value, min), max);
|
|
49
|
-
};
|
|
50
|
-
var transform = (value, originRange, targetRange) => {
|
|
51
|
-
const [oMin, oMax] = originRange;
|
|
52
|
-
const [tMin, tMax] = targetRange;
|
|
53
|
-
const ratio = (value - oMin) / (oMax - oMin);
|
|
54
|
-
const transformedValue = tMin + ratio * (tMax - tMin);
|
|
55
|
-
return transformedValue;
|
|
56
|
-
};
|
|
57
|
-
var round = (value, decimalPlaces = 0) => {
|
|
58
|
-
const factor = Math.pow(10, decimalPlaces);
|
|
59
|
-
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
60
|
-
};
|
|
61
|
-
var mergeClassNames = (...classNames) => {
|
|
62
|
-
try {
|
|
63
|
-
return clsx(...classNames);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
"mergeClassNames: Invalid input provided. Ensure all class names are strings, undefined, or null."
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// src/functions/platform.ts
|
|
72
|
-
var isTouchDevice = () => {
|
|
73
|
-
return IS_IN_BROWSER && !!navigator.maxTouchPoints;
|
|
74
|
-
};
|
|
75
|
-
var isApple = () => {
|
|
76
|
-
if (!IS_IN_BROWSER) return false;
|
|
77
|
-
return /mac|iphone|ipad|ipod/i.test(navigator.platform);
|
|
78
|
-
};
|
|
79
|
-
var isMac = () => {
|
|
80
|
-
return IS_IN_BROWSER && navigator.platform.startsWith("Mac") && !isTouchDevice();
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// src/functions/remove-undefined-values.ts
|
|
84
|
-
function removeUndefinedValues(obj) {
|
|
85
|
-
const result = {};
|
|
86
|
-
for (const key in obj) {
|
|
87
|
-
if (obj[key] !== void 0) {
|
|
88
|
-
result[key] = obj[key];
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// src/functions/shallow-equal.ts
|
|
95
|
-
function shallowEqual(a, b) {
|
|
96
|
-
if (a === b) return true;
|
|
97
|
-
if (!a) return false;
|
|
98
|
-
if (!b) return false;
|
|
99
|
-
if (typeof a !== "object") return false;
|
|
100
|
-
if (typeof b !== "object") return false;
|
|
101
|
-
const aKeys = Object.keys(a);
|
|
102
|
-
const bKeys = Object.keys(b);
|
|
103
|
-
const { length } = aKeys;
|
|
104
|
-
if (bKeys.length !== length) return false;
|
|
105
|
-
for (const key of aKeys) {
|
|
106
|
-
if (a[key] !== b[key]) {
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// src/functions/type-guards.ts
|
|
114
|
-
var notEmpty = (value) => value !== null && value !== void 0;
|
|
115
|
-
var isHTMLElement = (node) => node instanceof HTMLElement;
|
|
116
|
-
var isInArray = (item, array) => array.includes(item);
|
|
117
|
-
var isObject = (object) => {
|
|
118
|
-
try {
|
|
119
|
-
return typeof object === "object" && object !== null && !Array.isArray(object);
|
|
120
|
-
} catch {
|
|
121
|
-
throw new Error("isObject: Invalid input");
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
var isTruthy = (value) => !!value;
|
|
125
|
-
function invariant(condition, message) {
|
|
126
|
-
if (condition) return;
|
|
127
|
-
if (typeof message !== "string") throw new Error("Invariant failed");
|
|
128
|
-
throw new Error(message);
|
|
129
|
-
}
|
|
130
|
-
var hasOwnProperty = (object, prop) => {
|
|
131
|
-
if (typeof Object.hasOwn === "function") {
|
|
132
|
-
return Object.hasOwn(object, prop);
|
|
133
|
-
}
|
|
134
|
-
return Object.prototype.hasOwnProperty.call(object, prop);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export { IS_BUILD, IS_DEVELOPMENT, IS_IN_BROWSER, applyState, clamp, defaultValue, hasOwnProperty, identity, invariant, isApple, isHTMLElement, isInArray, isMac, isObject, isTouchDevice, isTruthy, mergeClassNames, notEmpty, removeUndefinedValues, round, shallowEqual, transform, wrap };
|
|
138
|
-
//# sourceMappingURL=chunk-SIU7SKJK.js.map
|
|
139
|
-
//# sourceMappingURL=chunk-SIU7SKJK.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/index.ts","../src/functions/apply-state.ts","../src/functions/default-value.ts","../src/functions/identity.ts","../src/functions/math.ts","../src/functions/merge-class-names.ts","../src/functions/platform.ts","../src/functions/remove-undefined-values.ts","../src/functions/shallow-equal.ts","../src/functions/type-guards.ts"],"names":[],"mappings":"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","file":"chunk-SIU7SKJK.js","sourcesContent":["export const IS_DEVELOPMENT = process.env.NODE_ENV === \"development\";\nexport const IS_BUILD = process.env.NODE_ENV === \"production\";\n\nexport const IS_IN_BROWSER = (() => {\n\ttry {\n\t\treturn !!window && \"document\" in window && !!window.document?.createElement;\n\t} catch {\n\t\treturn false;\n\t}\n})();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n\nimport { BivariantCallback, SetStateAction } from \"src/types\";\n\nfunction isUpdater<T>(\n\targument: SetStateAction<T>,\n): argument is BivariantCallback<(prevState: T) => T> {\n\treturn typeof argument === \"function\";\n}\n\nfunction isLazyValue<T>(value: any): value is () => T {\n\treturn typeof value === \"function\";\n}\n\n/**\n * Receives a `setState` argument and calls it with `currentValue` if it's a\n * function. Otherwise return the argument as the new value.\n * @example\n * applyState((value) => value + 1, 1); // 2\n * applyState(2, 1); // 2\n */\nexport function applyState<T>(\n\targument: SetStateAction<T>,\n\tcurrentValue: T | (() => T),\n) {\n\tif (isUpdater(argument)) {\n\t\tconst value = isLazyValue(currentValue) ? currentValue() : currentValue;\n\t\treturn argument(value);\n\t}\n\treturn argument;\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * Returns the first value that is not `undefined`.\n */\nexport function defaultValue<T extends readonly any[]>(\n\t...values: T\n): DefaultValue<T>;\n\nexport function defaultValue(...values: unknown[]) {\n\tfor (const value of values) {\n\t\tif (value !== undefined) return value;\n\t}\n\treturn undefined;\n}\n\ntype DefaultValue<T extends readonly any[], Other = never> = T extends [\n\tinfer Head,\n\t...infer Rest,\n]\n\t? Rest extends []\n\t\t? T[number] | Other\n\t\t: undefined extends Head\n\t\t\t? DefaultValue<Rest, Exclude<Other | Head, undefined>>\n\t\t\t: Exclude<T[number], undefined>\n\t: never;\n","/**\n * Returns the same argument.\n */\nexport function identity<T>(value: T) {\n\treturn value;\n}\n","/**\n * Wraps a number within a specified range.\n * @param value - The number to wrap.\n * @param min - The minimum value of the range (inclusive).\n * @param max - The maximum value of the range (inclusive).\n * @returns The wrapped number within the range [min, max].\n *\n * @example\n * wrap(5, [0, 3]); // Returns 1\n * wrap(-1, [0, 3]); // Returns 2\n * wrap(-3, [0, 3]); // Returns 1\n * wrap(7, [0, 3]); // Returns 3\n */\nexport const wrap = (value: number, [min, max]: [min: number, max: number]) => {\n\tconst range = max - min + 1;\n\treturn ((((value - min) % range) + range) % range) + min;\n};\n\n/**\n * Limits a value between a specified range.\n * @param value - The number to limit.\n * @param min - The minimum value of the range (inclusive).\n * @param max - The maximum value of the range (inclusive).\n * @returns The wrapped number within the range [min, max].\n *\n * @example\n * clamp(2, [0, 3]); // Returns 2\n * clamp(5, [0, 3]); // Returns 3\n * clamp(-1, [0, 3]); // Returns 0\n * clamp(-3, [0, 3]); // Returns 0\n * clamp(7, [0, 3]); // Returns 3\n */\nexport const clamp = (\n\tvalue: number,\n\t[min, max]: [min: number, max: number],\n) => {\n\treturn Math.min(Math.max(value, min), max);\n};\n\n/**\n * Maps a number from one range to another.\n * @param value The number to map.\n * @param originRange The original range of the number.\n * @param targetRange The target range to map the number to.\n * @returns The mapped number within the target range.\n *\n * @example\n * transform(5, [0, 10], [0, 100]); // Returns 50\n * transform(0.5, [0, 1], [0, 10]); // Returns 5\n * transform(-5, [-10, 0], [0, 100]); // Returns 50\n */\nexport const transform = (\n\tvalue: number,\n\toriginRange: [number, number],\n\ttargetRange: [number, number],\n) => {\n\tconst [oMin, oMax] = originRange;\n\tconst [tMin, tMax] = targetRange;\n\n\tconst ratio = (value - oMin) / (oMax - oMin);\n\tconst transformedValue = tMin + ratio * (tMax - tMin);\n\n\treturn transformedValue;\n};\n\n/**\n * Rounds a number to a specified number of decimal places.\n * @param value - The number to round.\n * @param decimalPlaces - The number of decimal places to round to. (default is 0)\n * @returns The rounded number.\n */\nexport const round = (value: number, decimalPlaces: number = 0) => {\n\tconst factor = Math.pow(10, decimalPlaces);\n\treturn Math.round((value + Number.EPSILON) * factor) / factor;\n};\n","import clsx from \"clsx\";\n\nconst mergeClassNames = (...classNames: clsx.ClassValue[]) => {\n\ttry {\n\t\treturn clsx(...classNames);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t\"mergeClassNames: Invalid input provided. Ensure all class names are strings, undefined, or null.\",\n\t\t);\n\t}\n};\n\n// Name isn't very memorable, so we export it in this file to alias it\nexport { mergeClassNames };\n","import { IS_IN_BROWSER } from \"src/config\";\n\n/**\n * Detects if the device has touch capabilities.\n */\nexport const isTouchDevice = () => {\n\treturn IS_IN_BROWSER && !!navigator.maxTouchPoints;\n};\n\n/**\n * Detects Apple device.\n */\nexport const isApple = () => {\n\tif (!IS_IN_BROWSER) return false;\n\treturn /mac|iphone|ipad|ipod/i.test(navigator.platform);\n};\n\n/**\n * Detects Mac computer.\n */\nexport const isMac = () => {\n\treturn (\n\t\tIS_IN_BROWSER && navigator.platform.startsWith(\"Mac\") && !isTouchDevice()\n\t);\n};\n","import { AnyObject } from \"../types\";\n\n/**\n * Removes undefined values from an object.\n */\nexport function removeUndefinedValues<T extends AnyObject>(obj: T) {\n\tconst result = {} as T;\n\tfor (const key in obj) {\n\t\tif (obj[key] !== undefined) {\n\t\t\tresult[key] = obj[key];\n\t\t}\n\t}\n\treturn result;\n}\n","import { AnyObject } from \"src/types\";\n\n/**\n * Compares two objects.\n * @example\n * shallowEqual({ a: \"a\" }, {}); // false\n * shallowEqual({ a: \"a\" }, { b: \"b\" }); // false\n * shallowEqual({ a: \"a\" }, { a: \"a\" }); // true\n * shallowEqual({ a: \"a\" }, { a: \"a\", b: \"b\" }); // false\n */\nexport function shallowEqual(a?: AnyObject, b?: AnyObject) {\n\tif (a === b) return true;\n\tif (!a) return false;\n\tif (!b) return false;\n\tif (typeof a !== \"object\") return false;\n\tif (typeof b !== \"object\") return false;\n\n\tconst aKeys = Object.keys(a);\n\tconst bKeys = Object.keys(b);\n\tconst { length } = aKeys;\n\n\tif (bKeys.length !== length) return false;\n\n\tfor (const key of aKeys) {\n\t\tif (a[key] !== b[key]) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n","import { AnyObject } from \"src/types\";\n\n/**\n * @function notEmpty\n * @description Checks if a value is not null or undefined.\n *\n * @param value - The value to check.\n * @returns Whether the value is not null or undefined.\n *\n * @example\n * ```ts\n * notEmpty(\"Hello\"); // true\n * notEmpty(null); // false\n * notEmpty(undefined); // false\n * ```\n */\nexport const notEmpty = <TValue>(\n\tvalue: TValue | null | undefined,\n): value is TValue => value !== null && value !== undefined;\n\n/**\n * @function isHTMLElement\n * @description Checks if a value is an instance of HTMLElement.\n *\n * @param node - The value to check.\n * @returns Whether the value is an instance of HTMLElement.\n *\n * @example\n * ```ts\n * isHTMLElement(document.querySelector('body')); // true\n * isHTMLElement({}); // false\n * ```\n */\n\nexport const isHTMLElement = (node: unknown): node is HTMLElement =>\n\tnode instanceof HTMLElement;\n\n/**\n * @function isInArray\n * @description Typesafe check if an item is in an array without being explicit about const types\n *\n * @param item - The item to check.\n * @param array - The array to check against.\n *\n * @returns Whether the item is in the array.\n *\n * @example\n * ```ts\n * // Default situation in TS:\n * const arr = ['hello', 'world'] as const;\n *\n * let someString = 'something';\n *\n * arr.includes(someString); // Argument of type 'string' is not assignable to parameter of type '\"hello\" | \"world\"'\n *\n * // Using isInArray:\n * const arr = ['hello', 'world'] as const;\n *\n * let someString = 'something';\n *\n * isInArray(someString, arr); // false, but no type error\n * ```\n */\nexport const isInArray = <T, A extends T>(\n\titem: T,\n\tarray: ReadonlyArray<A>,\n): item is A => array.includes(item as A);\n\n/**\n * @function isObject\n * @description Checks if a value is an object (excluding arrays).\n *\n * @param object - The value to check.\n * @returns Whether the value is truthy.\n *\n * @example\n * ```ts\n * isObject({ hello: world }); // true\n * isObject({}); // true\n * isObject([]); // false\n * isObject(\"\"); // false\n * isObject(null); // false\n * ```\n */\nexport const isObject = (\n\tobject: unknown,\n): object is Record<string, unknown> => {\n\ttry {\n\t\treturn (\n\t\t\ttypeof object === \"object\" && object !== null && !Array.isArray(object)\n\t\t);\n\t} catch {\n\t\tthrow new Error(\"isObject: Invalid input\");\n\t}\n};\n\n/**\n * @function isTruthy\n * @description Checks if a value is truthy.\n *\n * @param value - The value to check.\n * @returns Whether the value is truthy.\n *\n * @example\n * ```ts\n * isTruthy(\"foo\"); // true\n * isTruthy(1); // true\n * isTruthy(0); // false\n * isTruthy(\"\"); // false\n * ```\n *\n */\nexport const isTruthy = <T>(value?: T | undefined | null | false): value is T =>\n\t!!value;\n\n/**\n * Asserts that a condition is true, otherwise throws an error.\n * @example\n * invariant(\n * condition,\n * process.env.NODE_ENV !== \"production\" && \"Invariant failed\"\n * );\n */\n// assert functions need to be an explicit function(){}\nexport function invariant(\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tcondition: any,\n\tmessage?: string | boolean,\n): asserts condition {\n\tif (condition) return;\n\tif (typeof message !== \"string\") throw new Error(\"Invariant failed\");\n\tthrow new Error(message);\n}\n\n/**\n * Checks if an object has a property as its own (not inherited) property.\n * @example\n * hasOwnProperty({ a: \"a\" }, \"a\"); // true\n * hasOwnProperty({ a: \"a\" }, \"b\"); // false\n * hasOwnProperty(Object.create({ a: \"a\" }), \"a\"); // false\n */\nexport const hasOwnProperty = <T extends AnyObject>(\n\tobject: T,\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tprop: keyof any,\n): prop is keyof T => {\n\tif (typeof Object.hasOwn === \"function\") {\n\t\treturn Object.hasOwn(object, prop);\n\t}\n\treturn Object.prototype.hasOwnProperty.call(object, prop);\n};\n"]}
|