@reblu/site-blocks 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { ShopErrorCode, PageDetail, PageListItem, ShopProductListItem, ShopFacets, ShopOrder } from '@reblu/site-contracts';
1
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PageDetail, PageListItem, ShopProductListItem, ShopFacets } from '@reblu/site-contracts';
3
+ import { ReactNode, FormEvent } from 'react';
3
4
 
4
5
  declare function readingTime(content: string | null | undefined): number | null;
5
6
 
@@ -28,6 +29,30 @@ interface ShareUrls {
28
29
  }
29
30
  declare function buildShareUrls(url: string, title: string): ShareUrls;
30
31
 
32
+ /**
33
+ * Default Spanish copy for every stable `ShopErrorCode` (REB-836) the shop
34
+ * write endpoints (`checkout`/`validateCart`/`applyDiscount`) may return in
35
+ * the error body's optional `code` field — see `ApiError.body.code` in
36
+ * `@reblu/site-client`. A SITE overrides any subset through
37
+ * {@link getShopErrorMessage}'s `overrides` parameter, or ignores `code`
38
+ * entirely and reads `ApiError.message` as before (REB-836 is additive).
39
+ */
40
+ declare const DEFAULT_SHOP_ERROR_MESSAGES: Record<ShopErrorCode, string>;
41
+ /** Fallback copy used when `code` is missing or not recognised. */
42
+ declare const DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE = "No se ha podido completar la operaci\u00F3n. Int\u00E9ntalo de nuevo.";
43
+ /**
44
+ * Resolves the user-facing message for a checkout/validate-cart/apply-discount
45
+ * error `code` (REB-836) — e.g. `ApiError.body?.code` from `@reblu/site-client`.
46
+ *
47
+ * @param code - The stable error code, or `null`/`undefined` when the response
48
+ * carried none (an older base-api, or a non-shop error).
49
+ * @param overrides - Replaces any subset of {@link DEFAULT_SHOP_ERROR_MESSAGES}
50
+ * without having to restate every other code — e.g. for a non-Spanish SITE.
51
+ * @param fallback - Used when `code` is missing or unrecognised.
52
+ * Defaults to {@link DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE}.
53
+ */
54
+ declare function getShopErrorMessage(code: string | null | undefined, overrides?: Partial<Record<ShopErrorCode, string>>, fallback?: string): string;
55
+
31
56
  /**
32
57
  * Canonical Blog markdown → **safe HTML** pipeline for the Reblu SITE SDK.
33
58
  *
@@ -383,4 +408,328 @@ interface ShopFiltersProps {
383
408
  declare function shouldRenderShopFilters(facets: ShopFacets, active?: ShopFiltersActive): boolean;
384
409
  declare function ShopFilters({ facets, hrefs, active, onChange, priceBands, labels, currencyCode, locale, className, }: ShopFiltersProps): react_jsx_runtime.JSX.Element | null;
385
410
 
386
- export { ALLOWED_YOUTUBE_HOSTS, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };
411
+ /**
412
+ * `CartLineList` — presentational cart lines (REB-829), container/
413
+ * presentational split: it takes `items` + optional `onQuantityChange`/
414
+ * `onRemove` callbacks, never fetches or owns cart state itself. The
415
+ * consuming SITE wires these to `@reblu/site-next/cart`'s `useCart()`.
416
+ *
417
+ * RSC-safe by construction (same `OptionLink` contract as `ShopFilters`):
418
+ * the quantity stepper/remove buttons only do anything when `onQuantityChange`/
419
+ * `onRemove` are supplied from a Client Component — omit them from a Server
420
+ * Component to render a read-only summary instead.
421
+ */
422
+ interface CartLineItemView {
423
+ productId: string;
424
+ variantId?: string | null;
425
+ name: string;
426
+ variantName?: string;
427
+ /** Integer cents. */
428
+ unitPrice: number;
429
+ quantity: number;
430
+ imageUrl?: string;
431
+ /** `null`/`undefined` = untracked stock — never clamps the stepper's max. */
432
+ maxStock?: number | null;
433
+ }
434
+ interface CartLineListLabels {
435
+ quantityLabel: string;
436
+ decrease: string;
437
+ increase: string;
438
+ remove: string;
439
+ unitPriceSuffix: string;
440
+ /** REB-837 — default copy for `notice={true}`. */
441
+ cancelledNotice: string;
442
+ }
443
+ interface CartLineListProps {
444
+ items: CartLineItemView[];
445
+ onQuantityChange?: (productId: string, variantId: string | null | undefined, quantity: number) => void;
446
+ onRemove?: (productId: string, variantId: string | null | undefined) => void;
447
+ /**
448
+ * A visually-hidden `aria-live="polite"` announcement (e.g. "Cantidad
449
+ * actualizada", "Producto eliminado del carrito") — the consuming SITE
450
+ * decides WHEN to set it (after a successful mutation), this component
451
+ * only renders the live region.
452
+ */
453
+ liveMessage?: string;
454
+ /**
455
+ * REB-837 — a notice shown above the lines. `true` renders the default
456
+ * "pago cancelado" copy (`labels.cancelledNotice`); pass any other
457
+ * `ReactNode` for custom content, or omit/`false` to render nothing.
458
+ */
459
+ notice?: boolean | React.ReactNode;
460
+ labels?: Partial<CartLineListLabels>;
461
+ currencyCode?: string;
462
+ locale?: string;
463
+ className?: string;
464
+ }
465
+ declare function CartLineList({ items, onQuantityChange, onRemove, liveMessage, notice, labels, currencyCode, locale, className, }: CartLineListProps): react_jsx_runtime.JSX.Element;
466
+
467
+ /**
468
+ * `CartSummary` — presentational price breakdown (REB-829): subtotal,
469
+ * optional discount, optional shipping (sourced from `ShopConfig` by the
470
+ * consuming SITE — this component only renders what it's given), and total.
471
+ * Pure data in, RSC-safe (no hooks, no callbacks).
472
+ */
473
+ interface CartSummaryLabels {
474
+ subtotal: string;
475
+ discountPrefix: string;
476
+ shipping: string;
477
+ freeShipping: string;
478
+ /**
479
+ * Shown when `shipping` is `null`/`undefined` (unknown yet) — REB-829
480
+ * review finding: this is NOT the same state as `0` (confirmed free).
481
+ * Claiming "free shipping" for an amount the SITE simply hasn't computed
482
+ * yet understates the real total and is reversed at checkout.
483
+ */
484
+ shippingCalculated: string;
485
+ total: string;
486
+ /** REB-837 — default copy for `notice={true}`. */
487
+ cancelledNotice: string;
488
+ }
489
+ interface CartSummaryProps {
490
+ /** Integer cents. */
491
+ subtotal: number;
492
+ /** Integer cents, positive. Omit (or `0`) to hide the discount line. */
493
+ discount?: number;
494
+ /** The applied discount code, shown next to the discount amount. */
495
+ discountCode?: string;
496
+ /** Integer cents. Only rendered when `shippingEnabled` is true. */
497
+ shipping?: number | null;
498
+ /** Whether this tenant's shop even has shipping cost — hides the line entirely when false/omitted. */
499
+ shippingEnabled?: boolean;
500
+ /** Integer cents — the final charged amount. */
501
+ total: number;
502
+ /**
503
+ * REB-837 — a notice shown above the breakdown. `true` renders the
504
+ * default "pago cancelado" copy (`labels.cancelledNotice`) — the standard
505
+ * message for a visitor returning from a cancelled Stripe Checkout
506
+ * session (`useCheckoutReturn().status === 'cancelled'`). Pass any other
507
+ * `ReactNode` for custom content, or omit/`false` to render nothing.
508
+ */
509
+ notice?: boolean | React.ReactNode;
510
+ labels?: Partial<CartSummaryLabels>;
511
+ currencyCode?: string;
512
+ locale?: string;
513
+ className?: string;
514
+ }
515
+ declare function CartSummary({ subtotal, discount, discountCode, shipping, shippingEnabled, total, notice, labels, currencyCode, locale, className, }: CartSummaryProps): react_jsx_runtime.JSX.Element;
516
+
517
+ /**
518
+ * `AddedToCartToast` — add-to-cart feedback toast CONTENT (REB-837). Pure
519
+ * presentational, no hooks.
520
+ *
521
+ * Deliberately does NOT import `@reblu/site-ui`'s `Toast` (the root entry):
522
+ * that barrel also re-exports `Button`, which this package's own
523
+ * `test/rsc-safety.test.ts` bans from the RSC-safe root entry (a Radix
524
+ * `Slot` copy whose server-evaluability depends on a CONSUMER's resolution —
525
+ * see that test's docblock). `Toast`'s small shell is reproduced inline here
526
+ * with `cn` from the certified-React-free `@reblu/site-ui/utils`, same
527
+ * pattern as every other organism in this file's sibling components. A SITE
528
+ * that wants `@reblu/site-ui`'s standalone `Toast` directly still can — the
529
+ * two are independent, not layered.
530
+ *
531
+ * This is the CONTENT to pass into whatever toaster the consuming SITE
532
+ * already mounts (sonner's `toast.custom((id) => <AddedToCartToast ... />,
533
+ * { id: `cart-add-${productId}` })` — a stable per-product id lets a repeat
534
+ * add UPDATE the same toast instead of stacking a new one). It never renders
535
+ * its own `aria-live` region: the host toaster's live region owns
536
+ * announcement, so adding a second one here would double-announce.
537
+ *
538
+ * `imageUrl` only renders a thumbnail when it's a real, non-blank string —
539
+ * an empty/whitespace value (a product with no image) renders no `<img>`
540
+ * rather than a broken one.
541
+ *
542
+ * `cartHref` is a plain `href` the consumer builds, rendered as a real
543
+ * `<a>` — never a `useRouter().push(...)` call, which breaks outside the
544
+ * Next.js App Router and in non-router test renders.
545
+ */
546
+ type AddedToCartToastStatus = 'success' | 'error' | 'outOfStock';
547
+ interface AddedToCartToastLabels {
548
+ success: string;
549
+ error: string;
550
+ outOfStock: string;
551
+ viewCart: string;
552
+ }
553
+ interface AddedToCartToastProps {
554
+ status?: AddedToCartToastStatus;
555
+ productName: string;
556
+ /** The resulting line quantity, e.g. from `addItem`'s outcome. Only shown when > 1. */
557
+ quantity?: number;
558
+ /** Rendered as a thumbnail ONLY when non-empty after trimming. */
559
+ imageUrl?: string;
560
+ /** "Ver carrito" link target — a real href, not a router navigation. */
561
+ cartHref: string;
562
+ labels?: Partial<AddedToCartToastLabels>;
563
+ className?: string;
564
+ }
565
+ declare function AddedToCartToast({ status, productName, quantity, imageUrl, cartHref, labels, className, }: AddedToCartToastProps): react_jsx_runtime.JSX.Element;
566
+
567
+ /**
568
+ * `CheckoutLoginPrompt` — the "login prompt hook" the checkout flow needs
569
+ * (REB-829): a guest-checkout identification dialog offering "continue with
570
+ * this email" (so the SITE can check for an existing account via
571
+ * `client.shop.checkEmail`) or "continue as guest" outright. Fully
572
+ * controlled/presentational — it performs NO fetch itself (unlike
573
+ * demo-site's original `CheckoutLoginModal`, which called
574
+ * `/api/shop/check-email` internally); the consuming SITE owns `email`
575
+ * state and the `checkEmail` call, and drives `checking`/`error` from its
576
+ * own result.
577
+ */
578
+ interface CheckoutLoginPromptLabels {
579
+ title: string;
580
+ description: string;
581
+ emailLabel: string;
582
+ emailPlaceholder: string;
583
+ continueWithEmail: string;
584
+ checking: string;
585
+ continueAsGuest: string;
586
+ close: string;
587
+ }
588
+ interface CheckoutLoginPromptProps {
589
+ open: boolean;
590
+ email: string;
591
+ onEmailChange: (value: string) => void;
592
+ onClose: () => void;
593
+ /** Called when the visitor confirms a (valid-looking) email. */
594
+ onSubmitEmail: () => void;
595
+ onContinueAsGuest: () => void;
596
+ checking?: boolean;
597
+ error?: string | null;
598
+ labels?: Partial<CheckoutLoginPromptLabels>;
599
+ className?: string;
600
+ }
601
+ declare function CheckoutLoginPrompt({ open, email, onEmailChange, onClose, onSubmitEmail, onContinueAsGuest, checking, error, labels, className, }: CheckoutLoginPromptProps): react_jsx_runtime.JSX.Element | null;
602
+
603
+ /**
604
+ * `CheckoutForm` — presentational guest-checkout form (REB-829): contact
605
+ * fields, a discount-code section, an optional `loginPrompt` slot, and the
606
+ * submit CTA. Fully controlled — `values`/callbacks only, no internal state,
607
+ * no fetch, no react-hook-form/zod dependency (keeping this package's
608
+ * dependency surface minimal); the consuming SITE owns validation and wires
609
+ * its own `onSubmit`.
610
+ *
611
+ * Scope note: shipping/billing address collection is intentionally OUT of
612
+ * this component — those field sets vary by tenant shop config (shipping
613
+ * enabled/disabled, invoicing requirements) far more than the guest-email +
614
+ * discount-code flow the issue named. A SITE that needs them renders its own
615
+ * fields via the `children` slot, rendered between the contact and discount
616
+ * sections.
617
+ */
618
+ interface CheckoutContactValues {
619
+ name: string;
620
+ email: string;
621
+ phone?: string;
622
+ }
623
+ type CheckoutContactErrors = Partial<Record<keyof CheckoutContactValues, string>>;
624
+ interface CheckoutAppliedDiscount {
625
+ code: string;
626
+ amount: number;
627
+ }
628
+ interface CheckoutFormLabels {
629
+ contactTitle: string;
630
+ nameLabel: string;
631
+ namePlaceholder: string;
632
+ emailLabel: string;
633
+ emailPlaceholder: string;
634
+ phoneLabel: string;
635
+ discountTitle: string;
636
+ discountPlaceholder: string;
637
+ applyDiscount: string;
638
+ applyingDiscount: string;
639
+ removeDiscount: string;
640
+ discountAppliedSuffix: (formattedAmount: string) => string;
641
+ submit: string;
642
+ submitting: string;
643
+ }
644
+ interface CheckoutFormProps {
645
+ contact: CheckoutContactValues;
646
+ onContactChange: (values: CheckoutContactValues) => void;
647
+ contactErrors?: CheckoutContactErrors;
648
+ discountCode: string;
649
+ onDiscountCodeChange: (value: string) => void;
650
+ onApplyDiscount: () => void;
651
+ onRemoveDiscount: () => void;
652
+ appliedDiscount?: CheckoutAppliedDiscount | null;
653
+ discountApplying?: boolean;
654
+ discountError?: string | null;
655
+ /**
656
+ * Rendered OUTSIDE the `<form>`, immediately before it — e.g. the
657
+ * `CheckoutLoginPrompt` dialog. It must never render inside the `<form>`:
658
+ * an Enter keypress in one of ITS inputs would otherwise submit checkout
659
+ * (REB-829 review finding — see `handleDiscountCodeKeyDown` below for the
660
+ * same defect class).
661
+ */
662
+ loginPrompt?: ReactNode;
663
+ /** Rendered between the contact and discount sections — e.g. shipping/billing fields. */
664
+ children?: ReactNode;
665
+ submitting?: boolean;
666
+ submitDisabled?: boolean;
667
+ onSubmit: (event: FormEvent<HTMLFormElement>) => void;
668
+ labels?: Partial<CheckoutFormLabels>;
669
+ currencyCode?: string;
670
+ locale?: string;
671
+ className?: string;
672
+ }
673
+ declare function CheckoutForm({ contact, onContactChange, contactErrors, discountCode, onDiscountCodeChange, onApplyDiscount, onRemoveDiscount, appliedDiscount, discountApplying, discountError, loginPrompt, children, submitting, submitDisabled, onSubmit, labels, currencyCode, locale, className, }: CheckoutFormProps): react_jsx_runtime.JSX.Element;
674
+
675
+ /**
676
+ * `OrderConfirmation` — presentational order-confirmation view (REB-829,
677
+ * extended REB-837). Pure data + status in, RSC-safe (no hooks). The
678
+ * consuming SITE owns the actual lookup/polling — today via
679
+ * `@reblu/site-next/shop`'s `useShopOrderStatus(sessionId)` hook — and
680
+ * passes the resulting `status`/`order` straight through.
681
+ *
682
+ * Backward compatible: the original three states (`polling`, `found`,
683
+ * `timeout`) keep their EXACT existing copy and behavior. REB-837 adds
684
+ * `useShopOrderStatus`'s native vocabulary (`loading`/`paid` are drop-in
685
+ * aliases for `polling`/`found`) plus five new terminal states with their
686
+ * own default Spanish copy, all overridable via `labels`.
687
+ */
688
+ type OrderConfirmationOrder = ShopOrder;
689
+ type OrderConfirmationStatus = 'polling' | 'found' | 'timeout' | 'loading' | 'paid' | 'pending_timeout' | 'failed' | 'refunded' | 'partially_refunded' | 'not_found' | 'error';
690
+ interface OrderConfirmationLabels {
691
+ pollingTitle: string;
692
+ pollingDescription: string;
693
+ foundTitle: string;
694
+ foundDescription: string;
695
+ timeoutTitle: string;
696
+ timeoutDescription: string;
697
+ orderNumberLabel: string;
698
+ totalLabel: string;
699
+ confirmationEmailPrefix: string;
700
+ sessionReferenceLabel: string;
701
+ /** REB-837 */
702
+ failedTitle: string;
703
+ failedDescription: string;
704
+ backToCartLabel: string;
705
+ refundedTitle: string;
706
+ refundedDescription: string;
707
+ partiallyRefundedTitle: string;
708
+ partiallyRefundedDescription: string;
709
+ pendingTimeoutTitle: string;
710
+ pendingTimeoutDescription: string;
711
+ recheckLabel: string;
712
+ notFoundTitle: string;
713
+ notFoundDescription: string;
714
+ errorTitle: string;
715
+ errorDescription: string;
716
+ /** REB-837 review fix — label for the neutral order-number-only reference shown on refunded/pending_timeout. */
717
+ orderReferenceLabel: string;
718
+ }
719
+ interface OrderConfirmationProps {
720
+ status: OrderConfirmationStatus;
721
+ order: OrderConfirmationOrder | null;
722
+ /** Shown as a fallback reference when there is no order to display. */
723
+ sessionId?: string;
724
+ /** "Volver al carrito" target, shown on `status === 'failed'`. Omit to render no link. */
725
+ cartHref?: string;
726
+ /** Called by the "Volver a comprobar" button on `pending_timeout`/`error`. Omit to render no button. */
727
+ onRecheck?: () => void;
728
+ labels?: Partial<OrderConfirmationLabels>;
729
+ currencyCode?: string;
730
+ locale?: string;
731
+ className?: string;
732
+ }
733
+ declare function OrderConfirmation({ status, order, sessionId, cartHref, onRecheck, labels, currencyCode, locale, className, }: OrderConfirmationProps): react_jsx_runtime.JSX.Element;
734
+
735
+ export { ALLOWED_YOUTUBE_HOSTS, AddedToCartToast, type AddedToCartToastLabels, type AddedToCartToastProps, type AddedToCartToastStatus, type CartLineItemView, CartLineList, type CartLineListLabels, type CartLineListProps, CartSummary, type CartSummaryLabels, type CartSummaryProps, type CheckoutAppliedDiscount, type CheckoutContactErrors, type CheckoutContactValues, CheckoutForm, type CheckoutFormLabels, type CheckoutFormProps, CheckoutLoginPrompt, type CheckoutLoginPromptLabels, type CheckoutLoginPromptProps, DEFAULT_SHOP_ERROR_FALLBACK_MESSAGE, DEFAULT_SHOP_ERROR_MESSAGES, DEFAULT_SHOP_PRICE_BANDS, MarkdownRenderer, type MarkdownRendererProps, OrderConfirmation, type OrderConfirmationLabels, type OrderConfirmationOrder, type OrderConfirmationProps, type OrderConfirmationStatus, ProductCard, type ProductCardProps, ProfileCard, type ProfileCardProps, SafeHtml, type SafeHtmlProps, type ShareUrls, type ShopFilterPriceBand, ShopFilters, type ShopFiltersActive, type ShopFiltersHrefs, type ShopFiltersLabels, type ShopFiltersProps, Specialty, type SpecialtyProps, Treatment, type TreatmentProps, buildShareUrls, formatPrice, getShopErrorMessage, isAllowedYouTubeHost, markdownToSafeHtml, readingTime, sanitizeHtml, shouldRenderShopFilters };