@lime-bundles/react 0.2.0 → 2.0.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.
@@ -0,0 +1,158 @@
1
+ # CSS variables reference
2
+
3
+ Lime Bundles components style themselves with CSS custom properties prefixed `--lb-*`. Override any of them on the component host (or any ancestor) to brand-match the widget to your storefront.
4
+
5
+ Every variable has a merchant-configurable default set in the Lime Bundles admin editor. The canonical source is [`app/lib/bundle-types.ts`](../../app/lib/bundle-types.ts) (`WidgetConfigSchema`) and the stylesheets in [`extensions/bundle-theme/assets/`](../../extensions/bundle-theme/assets).
6
+
7
+ ## How to override
8
+
9
+ ### React components
10
+
11
+ Set variables on any wrapping element:
12
+
13
+ ```tsx
14
+ <div style={{ "--lb-primary-color": "#e91e63", "--lb-radius": "16px" } as React.CSSProperties}>
15
+ <FixedBundle ... />
16
+ </div>
17
+ ```
18
+
19
+ ### Web component
20
+
21
+ Variables apply to the shadow-DOM host, so set them directly on `<lime-bundle>`:
22
+
23
+ ```html
24
+ <lime-bundle
25
+ shop-domain="..."
26
+ style="--lb-primary-color: #e91e63; --lb-radius: 16px;"
27
+ ></lime-bundle>
28
+ ```
29
+
30
+ ### Merchant custom CSS
31
+
32
+ Merchants can set these (and any other CSS) via Lime Bundles admin → Settings → Custom CSS. That CSS ships with the widget automatically. No code change on your side.
33
+
34
+ ## Layout
35
+
36
+ | Variable | Default | What it styles |
37
+ |---|---|---|
38
+ | `--lb-bg` | `#FFFFFF` | Widget background. |
39
+ | `--lb-text` | `#1A1A1A` | Base body text colour. |
40
+ | `--lb-border` | `#E5E5E5` | Outer card border colour. |
41
+ | `--lb-border-width` | `1px` | Outer card border width. |
42
+ | `--lb-radius` | `12px` | Outer card corner radius. |
43
+
44
+ ## Header
45
+
46
+ | Variable | Default | What it styles |
47
+ |---|---|---|
48
+ | `--lb-header-start` | `#C62828` | Gradient start (ignored when header style is `solid`). |
49
+ | `--lb-header-end` | `#AD1457` | Gradient end. |
50
+ | `--lb-header-bg` | *derived* | Final header background. Emitted as a solid colour or a gradient depending on the merchant's header-style pick. |
51
+ | `--lb-header-text` | `#FFFFFF` | Header text colour. |
52
+ | `--lb-countdown-bg` | `#FFF0F3` | Countdown badge background inside the header. |
53
+ | `--lb-countdown-text` | `#C62828` | Countdown badge text colour. |
54
+
55
+ ## Save badge
56
+
57
+ | Variable | Default | What it styles |
58
+ |---|---|---|
59
+ | `--lb-save-badge-bg` | `#FFFFFF` | Background. |
60
+ | `--lb-save-badge-text` | `#AD1457` | Text colour. |
61
+ | `--lb-save-badge-border-color` | `#FFFFFF` | Border colour. |
62
+ | `--lb-save-badge-border-width` | `0px` | Border width. |
63
+ | `--lb-save-badge-radius` | `8px` | Corner radius. |
64
+
65
+ ## Product list
66
+
67
+ | Variable | Default | What it styles |
68
+ |---|---|---|
69
+ | `--lb-image-border-color` | `#E5E5E5` | Product thumbnail border. |
70
+ | `--lb-image-border-width` | `0px` | Product thumbnail border width. |
71
+ | `--lb-image-border-radius` | `8px` | Product thumbnail corner radius. |
72
+ | `--lb-variant-border-color` | `#E5E5E5` | Variant selector border. |
73
+ | `--lb-variant-border-width` | `1px` | Variant selector border width. |
74
+ | `--lb-variant-radius` | `8px` | Variant selector corner radius. |
75
+ | `--lb-variant-chevron` | *derived SVG data URL* | Chevron icon shown in the variant dropdown. Auto-coloured to match `--lb-variant-border-color`. |
76
+ | `--lb-qty-badge-bg` | `#1A1A1A` | Quantity badge background. |
77
+ | `--lb-qty-badge-color` | `#FFFFFF` | Quantity badge text. |
78
+ | `--lb-qty-badge-display` | `flex` (shown) / `none` (hidden) | Toggle; merchant config controls this. |
79
+ | `--lb-product-price-display` | `block` (shown) / `none` (hidden) | Toggle. |
80
+ | `--lb-product-compare-display` | `inline` (shown) / `none` (hidden) | Toggle. |
81
+
82
+ ## CTA button
83
+
84
+ | Variable | Default | What it styles |
85
+ |---|---|---|
86
+ | `--lb-primary-color` | `#1A1A1A` | Button background. |
87
+ | `--lb-btn-text` | `#FFFFFF` | Button text. |
88
+ | `--lb-cta-border-color` | `#1A1A1A` | Border colour. |
89
+ | `--lb-cta-border-width` | `0px` | Border width. |
90
+ | `--lb-cta-radius` | `12px` | Corner radius. |
91
+
92
+ ## Savings bar
93
+
94
+ | Variable | Default | What it styles |
95
+ |---|---|---|
96
+ | `--lb-savings-bar-bg` | `#EDFBF1` | Bar background. |
97
+ | `--lb-savings-bar-text` | `#2DB554` | Bar text colour. |
98
+ | `--lb-savings-bar-border-color` | `#2DB554` | Border colour. |
99
+ | `--lb-savings-bar-border-width` | `0px` | Border width. |
100
+ | `--lb-savings-bar-radius` | `8px` | Corner radius. |
101
+
102
+ ## Volume tier cards
103
+
104
+ | Variable | Default | What it styles |
105
+ |---|---|---|
106
+ | `--lb-tier-border-color` | `#E5E5E5` | Tier card border. |
107
+ | `--lb-tier-border-width` | `1px` | Tier card border width. |
108
+ | `--lb-tier-radius` | `12px` | Tier card corner radius. |
109
+ | `--lb-tier-selected-border-color` | `#1A1A1A` | Border colour for the active tier. |
110
+ | `--lb-tier-selected-border-width` | `2px` | Border width for the active tier. |
111
+
112
+ ## "Most popular" badge (volume bundle)
113
+
114
+ | Variable | Default | What it styles |
115
+ |---|---|---|
116
+ | `--lb-popular-badge-bg` | `#1A1A1A` | Badge background. |
117
+ | `--lb-popular-badge-text` | `#FFFFFF` | Badge text. |
118
+ | `--lb-popular-badge-border-color` | `#1A1A1A` | Badge border colour. |
119
+ | `--lb-popular-badge-border-width` | `0px` | Badge border width. |
120
+ | `--lb-popular-badge-radius` | `12px` | Badge corner radius. |
121
+
122
+ ## Mix-and-match picker modal
123
+
124
+ | Variable | Default | What it styles |
125
+ |---|---|---|
126
+ | `--lb-picker-bg` | `#FFFFFF` | Modal background. |
127
+ | `--lb-picker-text` | `#1A1A1A` | Modal text colour. |
128
+ | `--lb-picker-border-color` | `#E5E5E5` | Modal outer border. |
129
+ | `--lb-picker-border-width` | `0px` | Modal outer border width. |
130
+ | `--lb-picker-radius` | `16px` | Modal corner radius. |
131
+ | `--lb-picker-search-border-color` | `#E5E5E5` | Search input border. |
132
+ | `--lb-picker-search-border-width` | `1px` | Search input border width. |
133
+ | `--lb-picker-search-radius` | `8px` | Search input corner radius. |
134
+ | `--lb-picker-product-border-color` | `#E5E5E5` | Product thumbnail border. |
135
+ | `--lb-picker-product-border-width` | `0px` | Product thumbnail border width. |
136
+ | `--lb-picker-product-radius` | `8px` | Product thumbnail corner radius. |
137
+ | `--lb-picker-qty-badge-bg` | `#1A1A1A` | Picker quantity-badge background. |
138
+ | `--lb-picker-qty-badge-color` | `#FFFFFF` | Picker quantity-badge text. |
139
+ | `--lb-picker-qty-badge-display` | `flex` (shown) / `none` (hidden) | Toggle; picker quantity badge visibility. |
140
+ | `--lb-picker-variant-border-color` | `#1A1A1A` | Variant select border. |
141
+ | `--lb-picker-variant-border-width` | `1px` | Variant select border width. |
142
+ | `--lb-picker-variant-radius` | `8px` | Variant select corner radius. |
143
+ | `--lb-picker-variant-chevron` | *derived SVG data URL* | Chevron icon on the picker's variant dropdown. Auto-coloured to match `--lb-picker-variant-border-color`. |
144
+ | `--lb-picker-add-bg` | `#1A1A1A` | Add-to-bundle button background. |
145
+ | `--lb-picker-add-label` | `#FFFFFF` | Add-to-bundle button text. |
146
+ | `--lb-picker-add-border-color` | `#1A1A1A` | Add-to-bundle button border colour. |
147
+ | `--lb-picker-add-border-width` | `0px` | Add-to-bundle button border width. |
148
+ | `--lb-picker-add-radius` | `8px` | Add-to-bundle button corner radius. |
149
+
150
+ ## Internal variables (don't override)
151
+
152
+ These exist for internal composition and are not merchant-configurable. The CSS in `extensions/bundle-theme/assets/bundle-base.css` defines them on `.lb-bundle-widget` with fixed values.
153
+
154
+ - `--lb-text-muted`: muted body text colour.
155
+ - `--lb-thumbnail-bg`: placeholder background behind images.
156
+ - `--lb-widget-pad`: internal padding token.
157
+ - `--lb-progress-color`: volume progress bar accent.
158
+
@@ -0,0 +1,185 @@
1
+ # Hydrogen
2
+
3
+ > **Before you start:** most Hydrogen merchants don't need this guide. The [web component](./web-component.md) (`<lime-bundle>`) works inside any Hydrogen page and handles cart and checkout via Shopify's tokenless Storefront Cart API. Paste the snippet from `/app/settings/headless` into your product route and you're done. Reach for this guide only when you want React components (`<FixedBundle>`) that thread cart mutations through Hydrogen's `useCart` state.
4
+
5
+ SSR-first integration: fetch the bundle server-side in a route loader, hydrate a `<FixedBundle>` (or volume / mix-match) with Hydrogen's `useCart` for the add-to-cart callback.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @lime-bundles/react @lime-bundles/core
11
+ ```
12
+
13
+ `@lime-bundles/core` is a transitive dependency. Installing it explicitly lets you call `fetchBundleData` from a loader without pulling React.
14
+
15
+ ## Environment
16
+
17
+ Open `/app/settings/headless` in your Lime Bundles admin, click **Generate token**, then append the value to your Hydrogen project's `.env`:
18
+
19
+ ```bash
20
+ # Existing values you already had: keep these as-is
21
+ PUBLIC_STORE_DOMAIN=my-shop.myshopify.com
22
+ PUBLIC_STOREFRONT_API_TOKEN=<your existing Shopify Storefront token>
23
+
24
+ # New: Lime Bundles' own Storefront token
25
+ PUBLIC_LIME_BUNDLES_TOKEN=<paste the generated token>
26
+ ```
27
+
28
+ The variable is intentionally namespaced so it doesn't collide with the `PUBLIC_STOREFRONT_API_TOKEN` that Shopify's Headless app issues for your primary storefront. Lime Bundles uses its own Storefront Access Token, scoped to bundle metaobjects and product listings only.
29
+
30
+ Lime Bundles creates the token for you via Shopify's Admin API. No custom-app configuration needed (Shopify deprecated that flow on 2026-01-01). The token carries the read-only scopes `unauthenticated_read_metaobjects` and `unauthenticated_read_product_listings` and nothing else. To rotate, click **Regenerate** on the same page. It revokes the current token and issues a new one atomically.
31
+
32
+ ## Getting a bundle GID
33
+
34
+ Each bundle you create in the Lime Bundles admin has a Shopify Metaobject GID (`gid://shopify/Metaobject/<numericId>`). You have two ways to use it:
35
+
36
+ - **Pin one bundle**: hardcode the GID. Find it in the admin at `/app/bundles/<id>/analytics` (URL), or by inspecting a bundle's API response.
37
+ - **Render every bundle configured against a product**: skip the explicit GID and use `fetchBundlesForProduct({ productHandle })` instead. Hand it the product handle from your loader; it returns every active bundle the merchant attached to that product. See the [React guide](./react-nextjs.md#auto-fetch-every-bundle-for-a-product) for the client-side `useBundlesForProduct` hook.
38
+
39
+ Both patterns are shown below.
40
+
41
+ ## Fetch a specific bundle in a loader
42
+
43
+ Hydrogen runs on Oxygen, where `process.env` is not available in the browser. Surface the values the route component needs through the loader. Oxygen injects environment variables as `context.env`:
44
+
45
+ ```tsx
46
+ // app/routes/products.$handle.tsx
47
+ import { fetchBundleData } from "@lime-bundles/react";
48
+ import { useLoaderData } from "@shopify/remix-oxygen";
49
+ import type { LoaderFunctionArgs } from "@shopify/remix-oxygen";
50
+
51
+ export async function loader({ request, context }: LoaderFunctionArgs) {
52
+ const shopDomain = context.env.PUBLIC_STORE_DOMAIN;
53
+ const storefrontAccessToken = context.env.PUBLIC_LIME_BUNDLES_TOKEN;
54
+ const bundleGid = "gid://shopify/Metaobject/123456789";
55
+
56
+ const bundle = await fetchBundleData({
57
+ shopDomain,
58
+ storefrontAccessToken,
59
+ bundleGid,
60
+ // Required on SSR. Without it Shopify may return 430 Security Rejection
61
+ // for server-originated traffic on a private token. `x-forwarded-for` is
62
+ // set by Oxygen and most other hosts; take the first IP in the list.
63
+ buyerIp:
64
+ request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? undefined,
65
+ });
66
+
67
+ return { bundle, shopDomain, storefrontAccessToken, bundleGid };
68
+ }
69
+ ```
70
+
71
+ `fetchBundleData` throws `StorefrontApiError` on HTTP / GraphQL failure and `BundleParseError` when the metaobject is missing, inactive, not yet scheduled, or expired. Hydrogen surfaces thrown loader errors via its standard `ErrorBoundary`.
72
+
73
+ ## Fetch every bundle for a product (auto-detect)
74
+
75
+ If you'd rather not pin a GID, use `fetchBundlesForProduct` in the loader. Pass it the product handle from route params and it returns every active bundle the merchant attached to that product (inactive, draft, not-yet-started, and expired bundles are filtered out):
76
+
77
+ ```tsx
78
+ // app/routes/products.$handle.tsx
79
+ import { fetchBundlesForProduct } from "@lime-bundles/react";
80
+ import type { LoaderFunctionArgs } from "@shopify/remix-oxygen";
81
+
82
+ export async function loader({ request, params, context }: LoaderFunctionArgs) {
83
+ const shopDomain = context.env.PUBLIC_STORE_DOMAIN;
84
+ const storefrontAccessToken = context.env.PUBLIC_LIME_BUNDLES_TOKEN;
85
+
86
+ const bundles = await fetchBundlesForProduct({
87
+ shopDomain,
88
+ storefrontAccessToken,
89
+ productHandle: params.handle!,
90
+ buyerIp:
91
+ request.headers.get("x-forwarded-for")?.split(",")[0].trim() ?? undefined,
92
+ });
93
+
94
+ return { bundles, shopDomain, storefrontAccessToken };
95
+ }
96
+ ```
97
+
98
+ The route component then maps the list and renders the right component per `bundleType` (see the [React guide](./react-nextjs.md#auto-fetch-every-bundle-for-a-product)). Prefer this shape when the product has more than one bundle configured or when you don't want to redeploy each time a merchant swaps bundles.
99
+
100
+ For a purely client-side version (no loader), use the `useBundlesForProduct` hook in a component.
101
+
102
+ ## Render and wire cart
103
+
104
+ ```tsx
105
+ // Same file: route component
106
+ import { FixedBundle } from "@lime-bundles/react";
107
+ import { useCart } from "@shopify/hydrogen-react";
108
+
109
+ export default function ProductRoute() {
110
+ const { shopDomain, storefrontAccessToken, bundleGid } =
111
+ useLoaderData<typeof loader>();
112
+ const { linesAdd } = useCart();
113
+
114
+ return (
115
+ <FixedBundle
116
+ shopDomain={shopDomain}
117
+ storefrontAccessToken={storefrontAccessToken}
118
+ bundleGid={bundleGid}
119
+ onAddToCart={async (lines) => {
120
+ await linesAdd(lines);
121
+ }}
122
+ onError={(err) => console.error("Bundle error", err)}
123
+ />
124
+ );
125
+ }
126
+ ```
127
+
128
+ The component fetches the bundle client-side via `useBundleData` on mount. Server-side prefetching in the loader warms Shopify's CDN cache so the client-side fetch returns instantly. If you want to avoid the duplicate fetch entirely, pass the `bundle` prop from the loader into a client component that renders directly without the Lime Bundles component. See [React guide](./react-nextjs.md#ssr-without-refetch) for that pattern.
129
+
130
+ ## The cart contract
131
+
132
+ `onAddToCart` receives a Hydrogen-compatible `CartLineInput[]`:
133
+
134
+ ```ts
135
+ type CartLineInput = {
136
+ merchandiseId: string; // variant GID
137
+ quantity: number;
138
+ attributes: Array<{ key: string; value: string }>;
139
+ };
140
+ ```
141
+
142
+ The `attributes` array always includes `{ key: "_lime_bundle_gid", value: <bundle GID> }`. **Do not strip this.** The `orders/create` webhook relies on it for purchase attribution. Hydrogen's `cart.linesAdd` preserves custom attributes through checkout by default.
143
+
144
+ ## Worked example: Volume bundle
145
+
146
+ ```tsx
147
+ import { VolumeBundle } from "@lime-bundles/react";
148
+
149
+ <VolumeBundle
150
+ shopDomain={SHOP_DOMAIN}
151
+ storefrontAccessToken={TOKEN}
152
+ bundleGid="gid://shopify/Metaobject/42"
153
+ onAddToCart={async (lines) => {
154
+ await linesAdd(lines);
155
+ }}
156
+ />
157
+ ```
158
+
159
+ The component renders tier cards, auto-selects the configured default tier, and calls `onAddToCart` with the chosen tier's merchandise and quantity.
160
+
161
+ ## Styling and merchant widget config
162
+
163
+ The React SDK does **not** automatically apply the widget styles the merchant set in the admin editor. That's deliberate. If you chose `@lime-bundles/react` on Hydrogen you likely want full control over the look so bundles match your storefront's design system.
164
+
165
+ Every `ParsedBundle` that `fetchBundleData` resolves to exposes the merchant's full config as `bundle.widgetConfig`. Read any field you want from it:
166
+
167
+ ```tsx
168
+ <button style={{ background: bundle.widgetConfig.cta.primaryColor }}>
169
+ {bundle.widgetConfig.cta.ctaText}
170
+ </button>
171
+ ```
172
+
173
+ Want drop-in parity with the admin preview instead? Swap to the [web component](./web-component.md): it honours `widgetConfig` end-to-end without any wiring on your side.
174
+
175
+ Full field list in [`packages/core/src/bundle/types.ts`](../../packages/core/src/bundle/types.ts). Helpers `WIDGET_CONFIG_DEFAULTS`, `mergeWidgetConfig`, `flattenWidgetConfig`, and `applyWidgetConfigVars` are exported from `@lime-bundles/core` if you want to apply the CSS custom properties to your own DOM.
176
+
177
+ ## What's NOT your job
178
+
179
+ - **Purchase attribution.** The Lime Bundles `orders/create` webhook scans every line item's `_lime_bundle_gid` attribute and records a `bundle_purchased` AnalyticsEvent. Don't add client-side `checkout_completed` tracking; it double-counts.
180
+ - **Custom CSS.** If merchants configure custom CSS in `/app/settings/custom-css`, `useBundleData` auto-fetches and injects a scoped `<style>` tag. No action needed.
181
+
182
+ ## Next steps
183
+
184
+ - [CSS variables reference](./css-variables.md): brand-match the widget to your theme.
185
+ - [Web component guide](./web-component.md): fallback for sections of the site that aren't React-controlled.