@cartbase/storefront 0.3.0 → 0.5.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 +1 -1
- package/package.json +6 -4
- package/src/api/checkout.ts +38 -3
- package/src/api/search.ts +30 -0
- package/src/checkout/use-checkout-orchestration.ts +63 -31
- package/theme/index.css +25 -0
- package/theme/theme.css +150 -0
- package/theme/tokens.css +106 -0
- package/tailwind-preset.cjs +0 -72
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ NEXT_PUBLIC_CARTBASE_PUBLISHABLE_KEY= # optional, channel-scoped catalogs
|
|
|
43
43
|
| `@cartbase/storefront/store` | Listing pages: pagination, sorting, collection/category/search templates |
|
|
44
44
|
| `@cartbase/storefront/order` | Order confirmation surfaces |
|
|
45
45
|
| `@cartbase/storefront/tracking` | Consent banner + consent-gated Meta Pixel / GA4 / analytics |
|
|
46
|
-
| `@cartbase/storefront/
|
|
46
|
+
| `@cartbase/storefront/theme` | The design system: token names plus a filled default set of values (Tailwind 4, CSS-first) |
|
|
47
47
|
|
|
48
48
|
## Documentation
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartbase/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Storefront SDK + UI component library for Cartbase stores: typed API client, checkout orchestration, cart drawer, product/catalog components, tracking. Source-shipped TypeScript — add it to transpilePackages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"theme",
|
|
16
|
+
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./src/index.ts",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"./api/integrations": "./src/api/integrations.ts",
|
|
39
39
|
"./api/consent": "./src/api/consent.ts",
|
|
40
40
|
"./api/redirects": "./src/api/redirects.ts",
|
|
41
|
-
"./
|
|
41
|
+
"./theme": "./theme/index.css",
|
|
42
|
+
"./theme/tokens.css": "./theme/tokens.css",
|
|
43
|
+
"./theme/theme.css": "./theme/theme.css",
|
|
42
44
|
"./lib/utils": "./src/lib/utils.ts",
|
|
43
45
|
"./lib/money": "./src/lib/money.ts",
|
|
44
46
|
"./lib/cart-helpers": "./src/lib/cart-helpers.ts",
|
package/src/api/checkout.ts
CHANGED
|
@@ -47,11 +47,10 @@ export interface StoreShippingOption {
|
|
|
47
47
|
name: string
|
|
48
48
|
provider_id: string | null
|
|
49
49
|
service_zone_id: string | null
|
|
50
|
-
shipping_option_type_id: string | null
|
|
51
50
|
shipping_profile_id: string | null
|
|
52
51
|
data: Record<string, unknown> | null
|
|
53
|
-
/**
|
|
54
|
-
type:
|
|
52
|
+
/** The option's display label (e.g. "Express") as `{ label }`, or null. */
|
|
53
|
+
type: { label: string } | null
|
|
55
54
|
/** Flat price in the cart currency; null without `cart_id` or when no price row matches. */
|
|
56
55
|
amount: MajorUnitAmount | null
|
|
57
56
|
/** Always "flat" today — calculated-rate carriers are not wired yet. */
|
|
@@ -489,3 +488,39 @@ export async function refreshPaymentIfTerminal(
|
|
|
489
488
|
): Promise<RefreshPaymentResult> {
|
|
490
489
|
return client.post(`/api/store/carts/${cartId}/refresh-payment-if-terminal`)
|
|
491
490
|
}
|
|
491
|
+
|
|
492
|
+
// ---------------------------------------------------------------------------
|
|
493
|
+
// Browser-side error reporting
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
|
|
496
|
+
/** Body of POST /api/store/checkout-errors (`.strict()`). */
|
|
497
|
+
export interface ReportCheckoutErrorInput {
|
|
498
|
+
/** The orchestration hook's errorType, e.g. "place_order_error". */
|
|
499
|
+
error_type: string
|
|
500
|
+
message: string
|
|
501
|
+
cart_id?: string
|
|
502
|
+
/** Redacted context — ids, codes, flags. Never card data, never addresses. */
|
|
503
|
+
context?: Record<string, unknown>
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Report a browser-side checkout failure into the platform's
|
|
508
|
+
* checkout_error_logs (step `browser`) so the merchant sees WHY a checkout
|
|
509
|
+
* died in the customer's browser — Stripe.js confirm errors, a 3DS return
|
|
510
|
+
* that came back not-succeeded, a place-order rejection after payment.
|
|
511
|
+
* `useCheckoutOrchestration` calls this BY DEFAULT when no `logError`
|
|
512
|
+
* override is given. Fire-and-forget by contract: swallows every failure —
|
|
513
|
+
* reporting an error must never break a checkout. Rate-limited server-side.
|
|
514
|
+
*
|
|
515
|
+
* Auth: anon `x-client-id`.
|
|
516
|
+
*/
|
|
517
|
+
export async function reportCheckoutError(
|
|
518
|
+
client: StorefrontClient,
|
|
519
|
+
input: ReportCheckoutErrorInput
|
|
520
|
+
): Promise<void> {
|
|
521
|
+
try {
|
|
522
|
+
await client.post(`/api/store/checkout-errors`, input)
|
|
523
|
+
} catch {
|
|
524
|
+
// Swallowed by contract — the sink must never take the checkout down.
|
|
525
|
+
}
|
|
526
|
+
}
|
package/src/api/search.ts
CHANGED
|
@@ -71,6 +71,17 @@ export interface RelatedProductsQuery extends PricingContextQuery {
|
|
|
71
71
|
limit?: number
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Complementary products — the merchant's "goes with this" picks, for the
|
|
76
|
+
* slot ABOVE the product rather than the carousel below it.
|
|
77
|
+
*
|
|
78
|
+
* It has no fallback and never will: "similar to this" can be computed from
|
|
79
|
+
* collections and tags, "buy this with it" is a merchant judgement, and a
|
|
80
|
+
* guessed upsell beside the buy button is worse than an empty slot. An empty
|
|
81
|
+
* response means the merchant has not chosen — render nothing.
|
|
82
|
+
*/
|
|
83
|
+
export type ComplementaryProductsQuery = RelatedProductsQuery
|
|
84
|
+
|
|
74
85
|
export interface RelatedProductsResponse {
|
|
75
86
|
products: StoreProduct[]
|
|
76
87
|
count: number
|
|
@@ -131,3 +142,22 @@ export async function listRelatedProducts(
|
|
|
131
142
|
query: { ...query },
|
|
132
143
|
})
|
|
133
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Complementary products for a PDP: the merchant's manual picks, in their
|
|
148
|
+
* order, and nothing else — `auto_filled` is always false.
|
|
149
|
+
*
|
|
150
|
+
* Same endpoint as related, discriminated by `kind`, because the two lists
|
|
151
|
+
* are one table and one code path apart only by meaning. Auth, errors and
|
|
152
|
+
* channel scoping are identical to `listRelatedProducts`.
|
|
153
|
+
* Settings: Admin → Product → Complementary products.
|
|
154
|
+
*/
|
|
155
|
+
export async function listComplementaryProducts(
|
|
156
|
+
client: StorefrontClient,
|
|
157
|
+
idOrHandle: string,
|
|
158
|
+
query?: ComplementaryProductsQuery
|
|
159
|
+
): Promise<RelatedProductsResponse> {
|
|
160
|
+
return client.get(`/api/store/products/${encodeURIComponent(idOrHandle)}/related`, {
|
|
161
|
+
query: { ...query, kind: "complementary" },
|
|
162
|
+
})
|
|
163
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
calculateShippingOption,
|
|
23
23
|
prepareCheckout,
|
|
24
24
|
refreshPaymentIfTerminal as refreshPaymentIfTerminalApi,
|
|
25
|
+
reportCheckoutError,
|
|
25
26
|
syncPaymentAmount as syncPaymentAmountApi,
|
|
26
27
|
isMethodEntry,
|
|
27
28
|
type PrepareCheckoutInput,
|
|
@@ -152,11 +153,21 @@ export type UseCheckoutOrchestrationOptions = {
|
|
|
152
153
|
*/
|
|
153
154
|
resolveTrackingMetadata?: () => Record<string, unknown> | undefined
|
|
154
155
|
/**
|
|
155
|
-
* Operational-visibility sink
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* Operational-visibility sink. Called with (errorType, message, context).
|
|
157
|
+
* DEFAULTS to reporting into the platform's checkout error log
|
|
158
|
+
* (POST /api/store/checkout-errors, step `browser`) so browser-side money
|
|
159
|
+
* failures reach the merchant without any wiring — a platform guarantee,
|
|
160
|
+
* not a merchant chore (money-chain-hardening, 2026-08-24). Override to
|
|
161
|
+
* add your own sink; fire-and-forget either way.
|
|
158
162
|
*/
|
|
159
163
|
logError?: CheckoutLogError
|
|
164
|
+
/**
|
|
165
|
+
* Verbose [buy-click] console output for local debugging. OFF by default:
|
|
166
|
+
* the unguarded dumps used to print the full prepare-checkout payload —
|
|
167
|
+
* customer name, phone, email, address — into every customer's browser
|
|
168
|
+
* console. Redacted sink events fire regardless of this flag.
|
|
169
|
+
*/
|
|
170
|
+
debug?: boolean
|
|
160
171
|
}
|
|
161
172
|
|
|
162
173
|
/**
|
|
@@ -253,8 +264,35 @@ export function useCheckoutOrchestration({
|
|
|
253
264
|
orderConfirmedPath: orderConfirmedPathProp,
|
|
254
265
|
onOrderPlaced,
|
|
255
266
|
resolveTrackingMetadata,
|
|
256
|
-
logError,
|
|
267
|
+
logError: logErrorProp,
|
|
268
|
+
debug = false,
|
|
257
269
|
}: UseCheckoutOrchestrationOptions) {
|
|
270
|
+
// The default sink: the platform's own checkout error log. Browser money
|
|
271
|
+
// failures (Stripe confirm errors, failed 3DS returns, place-order
|
|
272
|
+
// rejections) land in checkout_error_logs step `browser` with no merchant
|
|
273
|
+
// wiring. The two per-click debug snapshots are gated behind `debug` and
|
|
274
|
+
// never sent by default — the sink is for FAILURES, not telemetry.
|
|
275
|
+
const logError: CheckoutLogError = useCallback(
|
|
276
|
+
(errorType, message, context) => {
|
|
277
|
+
if (logErrorProp) {
|
|
278
|
+
logErrorProp(errorType, message, context)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
void reportCheckoutError(client, {
|
|
282
|
+
error_type: errorType,
|
|
283
|
+
message,
|
|
284
|
+
cart_id: cart?.id,
|
|
285
|
+
context,
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
289
|
+
[logErrorProp, client, cart?.id]
|
|
290
|
+
)
|
|
291
|
+
// Debug-gated console output — see the `debug` option's doc.
|
|
292
|
+
const dbg = debug
|
|
293
|
+
? // eslint-disable-next-line no-console
|
|
294
|
+
(...args: unknown[]) => console.log(...args)
|
|
295
|
+
: () => {}
|
|
258
296
|
// Resolve the order-confirmed path. Prop wins; otherwise fall back to
|
|
259
297
|
// the value provided by CheckoutProvider context.
|
|
260
298
|
const contextOrderConfirmedPath = useOrderConfirmedPath()
|
|
@@ -677,13 +715,11 @@ export function useCheckoutOrchestration({
|
|
|
677
715
|
string | null
|
|
678
716
|
>(cart?.shipping_methods?.at(-1)?.shipping_option_id || null)
|
|
679
717
|
|
|
718
|
+
// Every listed option is offerable: the Medusa-era pickup filter read an
|
|
719
|
+
// embedded fulfillment_set the Cartbase API never sends (it never fired),
|
|
720
|
+
// and the concept itself died in the shipping vocabulary trial.
|
|
680
721
|
const shippingMethods = useMemo(
|
|
681
|
-
() =>
|
|
682
|
-
(availableShippingMethods ?? []).filter(
|
|
683
|
-
(sm) =>
|
|
684
|
-
(sm as { service_zone?: { fulfillment_set?: { type?: string } } })
|
|
685
|
-
.service_zone?.fulfillment_set?.type !== "pickup"
|
|
686
|
-
),
|
|
722
|
+
() => availableShippingMethods ?? [],
|
|
687
723
|
[availableShippingMethods]
|
|
688
724
|
)
|
|
689
725
|
|
|
@@ -1249,9 +1285,10 @@ export function useCheckoutOrchestration({
|
|
|
1249
1285
|
: null,
|
|
1250
1286
|
hasStripeBundle: !!stripeBundle,
|
|
1251
1287
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1288
|
+
dbg("[buy-click] STATE", stateSnapshot)
|
|
1289
|
+
// Per-click telemetry goes only to a merchant-wired sink, never to the
|
|
1290
|
+
// default platform error log — the sink is for failures.
|
|
1291
|
+
logErrorProp?.("other", "buy_click_state", stateSnapshot)
|
|
1255
1292
|
|
|
1256
1293
|
await flushAddressSave()
|
|
1257
1294
|
|
|
@@ -1261,8 +1298,7 @@ export function useCheckoutOrchestration({
|
|
|
1261
1298
|
console.error("[buy-click] card path but no stripe bundle")
|
|
1262
1299
|
throw new Error("Stripe not ready")
|
|
1263
1300
|
}
|
|
1264
|
-
|
|
1265
|
-
console.log("[buy-click] elements.submit() …")
|
|
1301
|
+
dbg("[buy-click] elements.submit() …")
|
|
1266
1302
|
const { error: submitError } = await stripeBundle.submit()
|
|
1267
1303
|
if (submitError) {
|
|
1268
1304
|
// eslint-disable-next-line no-console
|
|
@@ -1272,9 +1308,10 @@ export function useCheckoutOrchestration({
|
|
|
1272
1308
|
}
|
|
1273
1309
|
|
|
1274
1310
|
const payload = buildPrepareCheckoutPayload()
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1277
|
-
|
|
1311
|
+
// debug-only: the full payload carries the customer's name, phone,
|
|
1312
|
+
// email and address — it must never print in a customer's console.
|
|
1313
|
+
dbg("[buy-click] PAYLOAD →", payload)
|
|
1314
|
+
logErrorProp?.("other", "buy_click_payload", {
|
|
1278
1315
|
provider_id: payload.payment_provider ?? null,
|
|
1279
1316
|
payment_method_id: payload.payment_method_id ?? null,
|
|
1280
1317
|
shipping_method_id: payload.shipping_method_id,
|
|
@@ -1289,8 +1326,7 @@ export function useCheckoutOrchestration({
|
|
|
1289
1326
|
throw e
|
|
1290
1327
|
}
|
|
1291
1328
|
)
|
|
1292
|
-
|
|
1293
|
-
console.log("[buy-click] prepareCheckout response", {
|
|
1329
|
+
dbg("[buy-click] prepareCheckout response", {
|
|
1294
1330
|
has_client_secret: !!prep.client_secret,
|
|
1295
1331
|
provider_id: prep.provider_id,
|
|
1296
1332
|
})
|
|
@@ -1350,8 +1386,7 @@ export function useCheckoutOrchestration({
|
|
|
1350
1386
|
},
|
|
1351
1387
|
}
|
|
1352
1388
|
|
|
1353
|
-
|
|
1354
|
-
console.log("[buy-click] stripe.confirmPayment() …", {
|
|
1389
|
+
dbg("[buy-click] stripe.confirmPayment() …", {
|
|
1355
1390
|
has_country: !!billingDetails.address.country,
|
|
1356
1391
|
})
|
|
1357
1392
|
const { error } = await (stripeBundle.stripe as unknown as {
|
|
@@ -1397,23 +1432,20 @@ export function useCheckoutOrchestration({
|
|
|
1397
1432
|
// confirmPayment succeeded synchronously (no redirect needed,
|
|
1398
1433
|
// e.g. non-3DS card flow). The PaymentIntent is now in
|
|
1399
1434
|
// requires_capture or succeeded — complete will pass authorize.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
logError?.("stripe_confirm_succeeded", "ok", {
|
|
1435
|
+
dbg("[buy-click] stripe.confirmPayment SUCCESS (no redirect)")
|
|
1436
|
+
logErrorProp?.("stripe_confirm_succeeded", "ok", {
|
|
1403
1437
|
cart_id: cart.id,
|
|
1404
1438
|
client_secret_prefix: prep.client_secret.slice(0, 8),
|
|
1405
1439
|
})
|
|
1406
1440
|
} else if (zeroRemainderGiftPath) {
|
|
1407
|
-
|
|
1408
|
-
console.log(
|
|
1441
|
+
dbg(
|
|
1409
1442
|
"[buy-click] zero-remainder gift path — skipping Stripe, completing on the gift session"
|
|
1410
1443
|
)
|
|
1411
|
-
|
|
1444
|
+
logErrorProp?.("other", "zero_remainder_gift_path", { cart_id: cart.id })
|
|
1412
1445
|
}
|
|
1413
1446
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
logError?.("order_placed", "called", {
|
|
1447
|
+
dbg("[buy-click] placeOrder() …")
|
|
1448
|
+
logErrorProp?.("order_placed", "called", {
|
|
1417
1449
|
cart_id: cart.id,
|
|
1418
1450
|
path: paymentTab,
|
|
1419
1451
|
})
|
package/theme/index.css
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Cartbase storefront theme — the one line a storefront imports.
|
|
3
|
+
*
|
|
4
|
+
* @import "tailwindcss";
|
|
5
|
+
* @import "@cartbase/storefront/theme";
|
|
6
|
+
*
|
|
7
|
+
* That is the whole setup. There is no Tailwind config file, no preset to
|
|
8
|
+
* register, and no list of token names to copy: Tailwind 4 reads the theme
|
|
9
|
+
* from CSS, and this ships filled, so a fresh install renders as a designed
|
|
10
|
+
* store rather than grey boxes.
|
|
11
|
+
*
|
|
12
|
+
* To retheme, override the values from tokens.css in your own stylesheet
|
|
13
|
+
* AFTER this import. Never edit the vocabulary — components reference those
|
|
14
|
+
* names, and changing one moves every storefront at once.
|
|
15
|
+
*/
|
|
16
|
+
@import "./tokens.css";
|
|
17
|
+
@import "./theme.css";
|
|
18
|
+
|
|
19
|
+
/* The page's own ground. Without this the body inherits the browser default
|
|
20
|
+
and every surface token above renders against white regardless of theme. */
|
|
21
|
+
body {
|
|
22
|
+
background-color: var(--background);
|
|
23
|
+
color: var(--foreground);
|
|
24
|
+
font-family: var(--font-body-family);
|
|
25
|
+
}
|
package/theme/theme.css
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Cartbase storefront — the VOCABULARY.
|
|
3
|
+
*
|
|
4
|
+
* Every line maps a value from tokens.css into a Tailwind namespace, which is
|
|
5
|
+
* what turns it into a utility class. Registering `--color-primary` is what
|
|
6
|
+
* makes `bg-primary`, `text-primary`, `border-primary` and `ring-primary`
|
|
7
|
+
* exist. Registering `--text-h2` is what makes `text-h2` exist.
|
|
8
|
+
*
|
|
9
|
+
* THIS FILE HOLDS NO VALUES, and a theme never edits it. A theme swaps
|
|
10
|
+
* tokens.css and the whole library repaints, because components only ever
|
|
11
|
+
* reference names from here.
|
|
12
|
+
*
|
|
13
|
+
* Every namespace below is Tailwind's own. We invented none of them and we
|
|
14
|
+
* add none of our own, because the requirement is that a developer or a
|
|
15
|
+
* coding agent reads `text-h2` or `bg-primary` and needs no documentation to
|
|
16
|
+
* know what it is or how to change it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@theme {
|
|
20
|
+
/* ── Colour ───────────────────────────────────────────────────────────── */
|
|
21
|
+
--color-background: var(--background);
|
|
22
|
+
--color-foreground: var(--foreground);
|
|
23
|
+
--color-card: var(--card);
|
|
24
|
+
--color-card-foreground: var(--card-foreground);
|
|
25
|
+
--color-popover: var(--popover);
|
|
26
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
27
|
+
|
|
28
|
+
--color-primary: var(--primary);
|
|
29
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
30
|
+
--color-secondary: var(--secondary);
|
|
31
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
32
|
+
--color-muted: var(--muted);
|
|
33
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
34
|
+
--color-accent: var(--accent);
|
|
35
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
36
|
+
|
|
37
|
+
--color-destructive: var(--destructive);
|
|
38
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
39
|
+
--color-success: var(--success);
|
|
40
|
+
--color-success-foreground: var(--success-foreground);
|
|
41
|
+
--color-warning: var(--warning);
|
|
42
|
+
--color-warning-foreground: var(--warning-foreground);
|
|
43
|
+
|
|
44
|
+
--color-border: var(--border);
|
|
45
|
+
--color-input: var(--input);
|
|
46
|
+
--color-ring: var(--ring);
|
|
47
|
+
|
|
48
|
+
/* ── Typefaces ────────────────────────────────────────────────────────── */
|
|
49
|
+
--font-body: var(--font-body-family);
|
|
50
|
+
--font-display: var(--font-display-family);
|
|
51
|
+
--font-mono: var(--font-mono-family);
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* ── Type scale ──────────────────────────────────────────────────────────
|
|
55
|
+
* Generated at a 1.2 ratio from a 16px base and rounded to 4px steps, the
|
|
56
|
+
* major-third scale Polaris uses. Named h1 to h6 because that is what every
|
|
57
|
+
* web developer reads without translation.
|
|
58
|
+
*
|
|
59
|
+
* The name is a STYLE, never a tag. Put `text-h2` on an <h1> when the page
|
|
60
|
+
* needs a smaller heading: the tag stays correct for search engines and
|
|
61
|
+
* screen readers, the style is a separate decision. This is exactly how
|
|
62
|
+
* Webflow's Client-First `heading-style-h*` classes work.
|
|
63
|
+
*/
|
|
64
|
+
--text-h1: 2.5rem;
|
|
65
|
+
--text-h1--line-height: 1.1;
|
|
66
|
+
--text-h1--letter-spacing: -0.02em;
|
|
67
|
+
--text-h1--font-weight: 600;
|
|
68
|
+
|
|
69
|
+
--text-h2: 2rem;
|
|
70
|
+
--text-h2--line-height: 1.15;
|
|
71
|
+
--text-h2--letter-spacing: -0.015em;
|
|
72
|
+
--text-h2--font-weight: 600;
|
|
73
|
+
|
|
74
|
+
--text-h3: 1.75rem;
|
|
75
|
+
--text-h3--line-height: 1.2;
|
|
76
|
+
--text-h3--letter-spacing: -0.01em;
|
|
77
|
+
--text-h3--font-weight: 600;
|
|
78
|
+
|
|
79
|
+
--text-h4: 1.5rem;
|
|
80
|
+
--text-h4--line-height: 1.25;
|
|
81
|
+
--text-h4--font-weight: 600;
|
|
82
|
+
|
|
83
|
+
--text-h5: 1.25rem;
|
|
84
|
+
--text-h5--line-height: 1.3;
|
|
85
|
+
--text-h5--font-weight: 600;
|
|
86
|
+
|
|
87
|
+
--text-h6: 1rem;
|
|
88
|
+
--text-h6--line-height: 1.4;
|
|
89
|
+
--text-h6--font-weight: 600;
|
|
90
|
+
|
|
91
|
+
--text-body-large: 1.125rem;
|
|
92
|
+
--text-body-large--line-height: 1.6;
|
|
93
|
+
|
|
94
|
+
--text-body: 1rem;
|
|
95
|
+
--text-body--line-height: 1.6;
|
|
96
|
+
|
|
97
|
+
--text-body-small: 0.875rem;
|
|
98
|
+
--text-body-small--line-height: 1.5;
|
|
99
|
+
|
|
100
|
+
/* Buttons, form labels, badges — short, never a sentence. */
|
|
101
|
+
--text-label: 0.875rem;
|
|
102
|
+
--text-label--line-height: 1.2;
|
|
103
|
+
--text-label--font-weight: 500;
|
|
104
|
+
|
|
105
|
+
/* Fine print, metadata, timestamps. */
|
|
106
|
+
--text-caption: 0.75rem;
|
|
107
|
+
--text-caption--line-height: 1.4;
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* ── Weight ──────────────────────────────────────────────────────────────
|
|
111
|
+
* Three, not four. Bold and semibold were doing the same job in different
|
|
112
|
+
* places, which is how a scale rots.
|
|
113
|
+
*/
|
|
114
|
+
--font-weight-regular: 400;
|
|
115
|
+
--font-weight-medium: 500;
|
|
116
|
+
--font-weight-semibold: 600;
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
* ── Spacing ─────────────────────────────────────────────────────────────
|
|
120
|
+
* Tailwind's own 4px step, stated rather than assumed so a theme can widen
|
|
121
|
+
* the whole rhythm with one number.
|
|
122
|
+
*/
|
|
123
|
+
--spacing: 0.25rem;
|
|
124
|
+
|
|
125
|
+
/* ── Shape ────────────────────────────────────────────────────────────── */
|
|
126
|
+
--radius-sm: calc(var(--radius-base) - 4px);
|
|
127
|
+
--radius-md: calc(var(--radius-base) - 2px);
|
|
128
|
+
--radius-lg: var(--radius-base);
|
|
129
|
+
--radius-xl: calc(var(--radius-base) + 4px);
|
|
130
|
+
--radius-2xl: calc(var(--radius-base) + 12px);
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* ── Elevation ───────────────────────────────────────────────────────────
|
|
134
|
+
* Named for what is raised, not for how big the blur is. `shadow-card` says
|
|
135
|
+
* what it is for; `shadow-md` says nothing and cannot be retuned per theme.
|
|
136
|
+
*/
|
|
137
|
+
--shadow-card: var(--elevation-card);
|
|
138
|
+
--shadow-card-hover: var(--elevation-card-hover);
|
|
139
|
+
--shadow-popover: var(--elevation-popover);
|
|
140
|
+
--shadow-drawer: var(--elevation-drawer);
|
|
141
|
+
|
|
142
|
+
/* ── Motion ───────────────────────────────────────────────────────────── */
|
|
143
|
+
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
|
144
|
+
--ease-entrance: cubic-bezier(0, 0, 0.2, 1);
|
|
145
|
+
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
146
|
+
|
|
147
|
+
/* ── Layout ───────────────────────────────────────────────────────────── */
|
|
148
|
+
--container-page: var(--layout-page-width);
|
|
149
|
+
--container-prose: var(--layout-prose-width);
|
|
150
|
+
}
|
package/theme/tokens.css
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Cartbase storefront — the DEFAULT THEME's values.
|
|
3
|
+
*
|
|
4
|
+
* This file is one half of the two-layer token model. It holds VALUES and
|
|
5
|
+
* nothing else. The other half (theme.css) holds the NAMES and never holds a
|
|
6
|
+
* value. A theme replaces this file; it never touches the other one.
|
|
7
|
+
*
|
|
8
|
+
* Naming follows shadcn/ui exactly, because that is the vocabulary every
|
|
9
|
+
* developer and every coding agent already knows without reading a document:
|
|
10
|
+
* a semantic role (`primary`, `muted`, `destructive`) always paired with its
|
|
11
|
+
* `-foreground`, so text on a surface is never a guess.
|
|
12
|
+
*
|
|
13
|
+
* Colours are oklch. Opacity modifiers (`bg-primary/50`) work on full colour
|
|
14
|
+
* values in Tailwind 4, so unlike the old preset these are complete colours
|
|
15
|
+
* rather than bare channels waiting for a wrapper.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
color-scheme: light;
|
|
20
|
+
|
|
21
|
+
/* ── Surfaces ─────────────────────────────────────────────────────────── */
|
|
22
|
+
--background: oklch(1 0 0);
|
|
23
|
+
--foreground: oklch(0.145 0 0);
|
|
24
|
+
--card: oklch(1 0 0);
|
|
25
|
+
--card-foreground: oklch(0.145 0 0);
|
|
26
|
+
--popover: oklch(1 0 0);
|
|
27
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
28
|
+
|
|
29
|
+
/* ── Roles ────────────────────────────────────────────────────────────── */
|
|
30
|
+
--primary: oklch(0.205 0 0);
|
|
31
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
32
|
+
--secondary: oklch(0.97 0 0);
|
|
33
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
34
|
+
--muted: oklch(0.97 0 0);
|
|
35
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
36
|
+
--accent: oklch(0.97 0 0);
|
|
37
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
38
|
+
|
|
39
|
+
/* ── Feedback ─────────────────────────────────────────────────────────── */
|
|
40
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
41
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
42
|
+
--success: oklch(0.648 0.15 160);
|
|
43
|
+
--success-foreground: oklch(0.985 0 0);
|
|
44
|
+
--warning: oklch(0.828 0.189 84.429);
|
|
45
|
+
--warning-foreground: oklch(0.279 0.077 45.635);
|
|
46
|
+
|
|
47
|
+
/* ── Lines and focus ──────────────────────────────────────────────────── */
|
|
48
|
+
--border: oklch(0.922 0 0);
|
|
49
|
+
--input: oklch(0.922 0 0);
|
|
50
|
+
--ring: oklch(0.708 0 0);
|
|
51
|
+
|
|
52
|
+
/* ── Typefaces ────────────────────────────────────────────────────────── */
|
|
53
|
+
--font-body-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
54
|
+
--font-display-family: ui-serif, Georgia, serif;
|
|
55
|
+
--font-mono-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
56
|
+
|
|
57
|
+
/* ── Shape ────────────────────────────────────────────────────────────── */
|
|
58
|
+
--radius-base: 0.5rem;
|
|
59
|
+
|
|
60
|
+
/* ── Elevation ────────────────────────────────────────────────────────── */
|
|
61
|
+
--elevation-card: 0 1px 2px 0 oklch(0 0 0 / 5%);
|
|
62
|
+
--elevation-card-hover: 0 4px 12px -2px oklch(0 0 0 / 10%), 0 2px 4px -2px oklch(0 0 0 / 6%);
|
|
63
|
+
--elevation-popover: 0 8px 24px -4px oklch(0 0 0 / 12%), 0 2px 6px -2px oklch(0 0 0 / 8%);
|
|
64
|
+
--elevation-drawer: -8px 0 32px -8px oklch(0 0 0 / 16%);
|
|
65
|
+
|
|
66
|
+
/* ── Layout ───────────────────────────────────────────────────────────── */
|
|
67
|
+
--layout-page-width: 80rem;
|
|
68
|
+
--layout-prose-width: 42rem;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dark {
|
|
72
|
+
color-scheme: dark;
|
|
73
|
+
|
|
74
|
+
--background: oklch(0.145 0 0);
|
|
75
|
+
--foreground: oklch(0.985 0 0);
|
|
76
|
+
--card: oklch(0.205 0 0);
|
|
77
|
+
--card-foreground: oklch(0.985 0 0);
|
|
78
|
+
--popover: oklch(0.205 0 0);
|
|
79
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
80
|
+
|
|
81
|
+
--primary: oklch(0.985 0 0);
|
|
82
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
83
|
+
--secondary: oklch(0.269 0 0);
|
|
84
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
85
|
+
--muted: oklch(0.269 0 0);
|
|
86
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
87
|
+
--accent: oklch(0.269 0 0);
|
|
88
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
89
|
+
|
|
90
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
91
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
92
|
+
--success: oklch(0.696 0.17 162.48);
|
|
93
|
+
--success-foreground: oklch(0.145 0 0);
|
|
94
|
+
--warning: oklch(0.828 0.189 84.429);
|
|
95
|
+
--warning-foreground: oklch(0.279 0.077 45.635);
|
|
96
|
+
|
|
97
|
+
--border: oklch(1 0 0 / 12%);
|
|
98
|
+
--input: oklch(1 0 0 / 18%);
|
|
99
|
+
--ring: oklch(0.556 0 0);
|
|
100
|
+
|
|
101
|
+
/* Shadows have to be heavier on a dark ground or they vanish entirely. */
|
|
102
|
+
--elevation-card: 0 1px 2px 0 oklch(0 0 0 / 40%);
|
|
103
|
+
--elevation-card-hover: 0 4px 12px -2px oklch(0 0 0 / 55%), 0 2px 4px -2px oklch(0 0 0 / 40%);
|
|
104
|
+
--elevation-popover: 0 8px 24px -4px oklch(0 0 0 / 60%), 0 2px 6px -2px oklch(0 0 0 / 45%);
|
|
105
|
+
--elevation-drawer: -8px 0 32px -8px oklch(0 0 0 / 65%);
|
|
106
|
+
}
|
package/tailwind-preset.cjs
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cartbase/storefront Tailwind preset — shadcn/ui standard token structure
|
|
3
|
-
* (ported verbatim from @1click/ui tailwind-preset.cjs v2.3.1)
|
|
4
|
-
*
|
|
5
|
-
* Uses oklch color space (modern CSS standard, matches shadcn/ui Create output).
|
|
6
|
-
* CSS variables store raw oklch channels so Tailwind's opacity modifiers work:
|
|
7
|
-
* --primary: 0.553 0.195 38.402;
|
|
8
|
-
* → bg-primary = oklch(0.553 0.195 38.402)
|
|
9
|
-
* → bg-primary/50 = oklch(0.553 0.195 38.402 / 50%)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/** @type {import('tailwindcss').Config} */
|
|
13
|
-
module.exports = {
|
|
14
|
-
theme: {
|
|
15
|
-
extend: {
|
|
16
|
-
colors: {
|
|
17
|
-
background: "oklch(var(--background) / <alpha-value>)",
|
|
18
|
-
foreground: "oklch(var(--foreground) / <alpha-value>)",
|
|
19
|
-
card: {
|
|
20
|
-
DEFAULT: "oklch(var(--card) / <alpha-value>)",
|
|
21
|
-
foreground: "oklch(var(--card-foreground) / <alpha-value>)",
|
|
22
|
-
},
|
|
23
|
-
popover: {
|
|
24
|
-
DEFAULT: "oklch(var(--popover) / <alpha-value>)",
|
|
25
|
-
foreground: "oklch(var(--popover-foreground) / <alpha-value>)",
|
|
26
|
-
},
|
|
27
|
-
primary: {
|
|
28
|
-
DEFAULT: "oklch(var(--primary) / <alpha-value>)",
|
|
29
|
-
foreground: "oklch(var(--primary-foreground) / <alpha-value>)",
|
|
30
|
-
},
|
|
31
|
-
secondary: {
|
|
32
|
-
DEFAULT: "oklch(var(--secondary) / <alpha-value>)",
|
|
33
|
-
foreground: "oklch(var(--secondary-foreground) / <alpha-value>)",
|
|
34
|
-
},
|
|
35
|
-
muted: {
|
|
36
|
-
DEFAULT: "oklch(var(--muted) / <alpha-value>)",
|
|
37
|
-
foreground: "oklch(var(--muted-foreground) / <alpha-value>)",
|
|
38
|
-
},
|
|
39
|
-
accent: {
|
|
40
|
-
DEFAULT: "oklch(var(--accent) / <alpha-value>)",
|
|
41
|
-
foreground: "oklch(var(--accent-foreground) / <alpha-value>)",
|
|
42
|
-
},
|
|
43
|
-
destructive: {
|
|
44
|
-
DEFAULT: "oklch(var(--destructive) / <alpha-value>)",
|
|
45
|
-
foreground: "oklch(var(--destructive-foreground) / <alpha-value>)",
|
|
46
|
-
},
|
|
47
|
-
border: "oklch(var(--border) / <alpha-value>)",
|
|
48
|
-
input: "oklch(var(--input) / <alpha-value>)",
|
|
49
|
-
ring: "oklch(var(--ring) / <alpha-value>)",
|
|
50
|
-
success: {
|
|
51
|
-
DEFAULT: "oklch(var(--success) / <alpha-value>)",
|
|
52
|
-
foreground: "oklch(var(--success-foreground) / <alpha-value>)",
|
|
53
|
-
},
|
|
54
|
-
warning: {
|
|
55
|
-
DEFAULT: "oklch(var(--warning) / <alpha-value>)",
|
|
56
|
-
foreground: "oklch(var(--warning-foreground) / <alpha-value>)",
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
fontFamily: {
|
|
60
|
-
display: ["var(--font-display)", "serif"],
|
|
61
|
-
body: ["var(--font-body)", "system-ui", "sans-serif"],
|
|
62
|
-
mono: ["var(--font-mono)", "ui-monospace", "monospace"],
|
|
63
|
-
},
|
|
64
|
-
borderRadius: {
|
|
65
|
-
lg: "var(--radius)",
|
|
66
|
-
md: "calc(var(--radius) - 2px)",
|
|
67
|
-
sm: "calc(var(--radius) - 4px)",
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
plugins: [],
|
|
72
|
-
}
|