@mohasinac/appkit 4.62.1 → 4.63.1
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/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -867,16 +867,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
867
867
|
opacity?: number | undefined;
|
|
868
868
|
subtitle?: string | undefined;
|
|
869
869
|
} | undefined;
|
|
870
|
+
settings?: {
|
|
871
|
+
height?: "medium" | "viewport" | "tall" | undefined;
|
|
872
|
+
autoplayDelayMs?: number | undefined;
|
|
873
|
+
} | undefined;
|
|
870
874
|
analytics?: {
|
|
871
875
|
views: number;
|
|
872
876
|
lastViewed?: string | Date | z.objectOutputType<{
|
|
873
877
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
874
878
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
875
879
|
} | undefined;
|
|
876
|
-
settings?: {
|
|
877
|
-
height?: "medium" | "viewport" | "tall" | undefined;
|
|
878
|
-
autoplayDelayMs?: number | undefined;
|
|
879
|
-
} | undefined;
|
|
880
880
|
carouselId?: string | null | undefined;
|
|
881
881
|
}, {
|
|
882
882
|
id: string;
|
|
@@ -976,16 +976,16 @@ export declare const carouselSlideFirestoreSchema: z.ZodObject<{
|
|
|
976
976
|
opacity?: number | undefined;
|
|
977
977
|
subtitle?: string | undefined;
|
|
978
978
|
} | undefined;
|
|
979
|
+
settings?: {
|
|
980
|
+
height?: "medium" | "viewport" | "tall" | undefined;
|
|
981
|
+
autoplayDelayMs?: number | undefined;
|
|
982
|
+
} | undefined;
|
|
979
983
|
analytics?: {
|
|
980
984
|
views: number;
|
|
981
985
|
lastViewed?: string | Date | z.objectInputType<{
|
|
982
986
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
983
987
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
984
988
|
} | undefined;
|
|
985
|
-
settings?: {
|
|
986
|
-
height?: "medium" | "viewport" | "tall" | undefined;
|
|
987
|
-
autoplayDelayMs?: number | undefined;
|
|
988
|
-
} | undefined;
|
|
989
989
|
carouselId?: string | null | undefined;
|
|
990
990
|
}>;
|
|
991
991
|
export type HomepageSectionFromSchema = z.infer<typeof homepageSectionFirestoreSchema>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HOLD on stock that is not yet an order.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this has to exist before S5b (Part 6)
|
|
5
|
+
*
|
|
6
|
+
* The plan calls this "the largest hidden cost" of the shared-purchase work,
|
|
7
|
+
* and the reason is that today there is no way to hold stock at all. The only
|
|
8
|
+
* mechanism is: `onOrderCreate` decrements `availableQuantity`, and
|
|
9
|
+
* `pendingOrderTimeout` puts it back if the buyer does not pay inside the
|
|
10
|
+
* 15-minute window. Both are keyed to an ORDER.
|
|
11
|
+
*
|
|
12
|
+
* A shared purchase runs for DAYS while co-buyers pay. With no primitive,
|
|
13
|
+
* either the coordinator places an order up front — which starts a 15-minute
|
|
14
|
+
* payment clock on a multi-day agreement — or nothing is held, and the item is
|
|
15
|
+
* sold out from under the arrangement by an ordinary buyer who happened to
|
|
16
|
+
* check out first. Neither is a product.
|
|
17
|
+
*
|
|
18
|
+
* ## The invariant, and where it already lives
|
|
19
|
+
*
|
|
20
|
+
* `products.repository.ts` already states it:
|
|
21
|
+
*
|
|
22
|
+
* reserved = stockQuantity - availableQuantity
|
|
23
|
+
*
|
|
24
|
+
* …as the comment on `resolveStockResync` puts it, *"the difference between
|
|
25
|
+
* the two IS the outstanding reservation"*. This collection does not invent a
|
|
26
|
+
* new idea; it makes that difference ADDRESSABLE — every held unit gets a row
|
|
27
|
+
* saying who holds it and until when, instead of being an anonymous gap
|
|
28
|
+
* between two numbers that only an order can close.
|
|
29
|
+
*
|
|
30
|
+
* 🛑 So a reservation MUST move `availableQuantity` in the same transaction
|
|
31
|
+
* that writes its row. A row without the decrement holds nothing; a decrement
|
|
32
|
+
* without a row is the anonymous gap again, and nothing can expire it.
|
|
33
|
+
*
|
|
34
|
+
* ## Why a TTL rather than a cancel button
|
|
35
|
+
*
|
|
36
|
+
* A hold with no expiry is a leak: the holder closes the tab, and those units
|
|
37
|
+
* are off the shelf until a human notices. `expiresAt` means the worst case is
|
|
38
|
+
* bounded by the sweep interval, and the seller never has to ask anyone to
|
|
39
|
+
* release anything.
|
|
40
|
+
*/
|
|
41
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
42
|
+
export declare const STOCK_RESERVATION_COLLECTION = "stockReservations";
|
|
43
|
+
/**
|
|
44
|
+
* Who is holding, and therefore what releases it.
|
|
45
|
+
*
|
|
46
|
+
* 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
|
|
47
|
+
* and the release path both need to know WHICH system owns the hold before
|
|
48
|
+
* they can decide whether it is still legitimate. A discriminator makes
|
|
49
|
+
* "release every hold belonging to arrangement X" expressible; an opaque id
|
|
50
|
+
* makes it a string-prefix convention nobody enforces.
|
|
51
|
+
*/
|
|
52
|
+
export declare const RESERVATION_HOLDER_KINDS: readonly ["shared-purchase", "manual"];
|
|
53
|
+
export type ReservationHolderKind = (typeof RESERVATION_HOLDER_KINDS)[number];
|
|
54
|
+
/**
|
|
55
|
+
* 🛑 `cart` and `order` are deliberately NOT holder kinds.
|
|
56
|
+
*
|
|
57
|
+
* A cart holds nothing today — adding an item does not decrement anything —
|
|
58
|
+
* and making it hold would silently turn every abandoned cart into held
|
|
59
|
+
* stock. An order already has its own hold through `onOrderCreate` +
|
|
60
|
+
* `pendingOrderTimeout`; a second mechanism over the same units would
|
|
61
|
+
* double-decrement, which is exactly the class of bug `resolveStockResync`'s
|
|
62
|
+
* delta exists to prevent. Add a kind only when the thing that owns it also
|
|
63
|
+
* owns its release.
|
|
64
|
+
*/
|
|
65
|
+
export declare const RESERVATION_STATUSES: readonly ["held", "consumed", "released", "expired"];
|
|
66
|
+
export type ReservationStatus = (typeof RESERVATION_STATUSES)[number];
|
|
67
|
+
export interface StockReservationDocument extends BaseDocument {
|
|
68
|
+
productId: string;
|
|
69
|
+
/** Denormalised so the sweep can restore stock without reading the product first. */
|
|
70
|
+
quantity: number;
|
|
71
|
+
holderKind: ReservationHolderKind;
|
|
72
|
+
/** The arrangement / manual-hold id. Scoped by `holderKind`, never globally unique. */
|
|
73
|
+
holderId: string;
|
|
74
|
+
/** Who asked for it — an Auth uid, for the audit trail and for admin release. */
|
|
75
|
+
createdBy: string;
|
|
76
|
+
status: ReservationStatus;
|
|
77
|
+
/**
|
|
78
|
+
* 🛑 The whole point. A hold with no expiry is a leak: the holder closes the
|
|
79
|
+
* tab and the units are gone until a human notices.
|
|
80
|
+
*/
|
|
81
|
+
expiresAt: Date;
|
|
82
|
+
/**
|
|
83
|
+
* Set when the hold ENDS, whichever way it ended. One field rather than
|
|
84
|
+
* three (`consumedAt`/`releasedAt`/`expiredAt`) because `status` already
|
|
85
|
+
* says which, and three nullable dates is three chances for the one that
|
|
86
|
+
* matters to be the one nobody set.
|
|
87
|
+
*/
|
|
88
|
+
endedAt?: Date;
|
|
89
|
+
/** Free text on a manual release — why an admin took the hold back. */
|
|
90
|
+
endedReason?: string;
|
|
91
|
+
}
|
|
92
|
+
/** Default hold, when a caller names none. */
|
|
93
|
+
export declare const RESERVATION_DEFAULT_TTL_HOURS = 72;
|
|
94
|
+
/**
|
|
95
|
+
* 🛑 A ceiling, and it is load-bearing rather than defensive.
|
|
96
|
+
*
|
|
97
|
+
* A caller may pass any TTL it likes, which makes "hold this for a year" one
|
|
98
|
+
* typo away — and a year-long hold is indistinguishable from permanently
|
|
99
|
+
* deleted stock, because nothing in the UI shows a seller why their own
|
|
100
|
+
* product is unbuyable. 30 days is longer than any agreement window Part 6
|
|
101
|
+
* contemplates and short enough that a mistake self-heals.
|
|
102
|
+
*/
|
|
103
|
+
export declare const RESERVATION_MAX_TTL_HOURS: number;
|
|
104
|
+
/** A reservation is only holding stock while it is `held` and unexpired. */
|
|
105
|
+
export declare function isReservationActive(reservation: Pick<StockReservationDocument, "status" | "expiresAt">, now?: Date): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
|
|
108
|
+
* the path a document took to get here, and the adapter that normalises those
|
|
109
|
+
* is not reachable from this file. Returning `null` on anything unparseable is
|
|
110
|
+
* the honest answer — and callers treat an unparseable expiry as NOT active,
|
|
111
|
+
* so the failure mode is a hold released early rather than one held forever.
|
|
112
|
+
*/
|
|
113
|
+
declare function toDate(value: unknown): Date | null;
|
|
114
|
+
export { toDate as reservationExpiryDate };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HOLD on stock that is not yet an order.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this has to exist before S5b (Part 6)
|
|
5
|
+
*
|
|
6
|
+
* The plan calls this "the largest hidden cost" of the shared-purchase work,
|
|
7
|
+
* and the reason is that today there is no way to hold stock at all. The only
|
|
8
|
+
* mechanism is: `onOrderCreate` decrements `availableQuantity`, and
|
|
9
|
+
* `pendingOrderTimeout` puts it back if the buyer does not pay inside the
|
|
10
|
+
* 15-minute window. Both are keyed to an ORDER.
|
|
11
|
+
*
|
|
12
|
+
* A shared purchase runs for DAYS while co-buyers pay. With no primitive,
|
|
13
|
+
* either the coordinator places an order up front — which starts a 15-minute
|
|
14
|
+
* payment clock on a multi-day agreement — or nothing is held, and the item is
|
|
15
|
+
* sold out from under the arrangement by an ordinary buyer who happened to
|
|
16
|
+
* check out first. Neither is a product.
|
|
17
|
+
*
|
|
18
|
+
* ## The invariant, and where it already lives
|
|
19
|
+
*
|
|
20
|
+
* `products.repository.ts` already states it:
|
|
21
|
+
*
|
|
22
|
+
* reserved = stockQuantity - availableQuantity
|
|
23
|
+
*
|
|
24
|
+
* …as the comment on `resolveStockResync` puts it, *"the difference between
|
|
25
|
+
* the two IS the outstanding reservation"*. This collection does not invent a
|
|
26
|
+
* new idea; it makes that difference ADDRESSABLE — every held unit gets a row
|
|
27
|
+
* saying who holds it and until when, instead of being an anonymous gap
|
|
28
|
+
* between two numbers that only an order can close.
|
|
29
|
+
*
|
|
30
|
+
* 🛑 So a reservation MUST move `availableQuantity` in the same transaction
|
|
31
|
+
* that writes its row. A row without the decrement holds nothing; a decrement
|
|
32
|
+
* without a row is the anonymous gap again, and nothing can expire it.
|
|
33
|
+
*
|
|
34
|
+
* ## Why a TTL rather than a cancel button
|
|
35
|
+
*
|
|
36
|
+
* A hold with no expiry is a leak: the holder closes the tab, and those units
|
|
37
|
+
* are off the shelf until a human notices. `expiresAt` means the worst case is
|
|
38
|
+
* bounded by the sweep interval, and the seller never has to ask anyone to
|
|
39
|
+
* release anything.
|
|
40
|
+
*/
|
|
41
|
+
export const STOCK_RESERVATION_COLLECTION = "stockReservations";
|
|
42
|
+
/**
|
|
43
|
+
* Who is holding, and therefore what releases it.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 A plain `holderId: string` was the first draft and it is wrong: the sweep
|
|
46
|
+
* and the release path both need to know WHICH system owns the hold before
|
|
47
|
+
* they can decide whether it is still legitimate. A discriminator makes
|
|
48
|
+
* "release every hold belonging to arrangement X" expressible; an opaque id
|
|
49
|
+
* makes it a string-prefix convention nobody enforces.
|
|
50
|
+
*/
|
|
51
|
+
export const RESERVATION_HOLDER_KINDS = ["shared-purchase", "manual"];
|
|
52
|
+
/**
|
|
53
|
+
* 🛑 `cart` and `order` are deliberately NOT holder kinds.
|
|
54
|
+
*
|
|
55
|
+
* A cart holds nothing today — adding an item does not decrement anything —
|
|
56
|
+
* and making it hold would silently turn every abandoned cart into held
|
|
57
|
+
* stock. An order already has its own hold through `onOrderCreate` +
|
|
58
|
+
* `pendingOrderTimeout`; a second mechanism over the same units would
|
|
59
|
+
* double-decrement, which is exactly the class of bug `resolveStockResync`'s
|
|
60
|
+
* delta exists to prevent. Add a kind only when the thing that owns it also
|
|
61
|
+
* owns its release.
|
|
62
|
+
*/
|
|
63
|
+
export const RESERVATION_STATUSES = ["held", "consumed", "released", "expired"];
|
|
64
|
+
/** Default hold, when a caller names none. */
|
|
65
|
+
export const RESERVATION_DEFAULT_TTL_HOURS = 72;
|
|
66
|
+
/**
|
|
67
|
+
* 🛑 A ceiling, and it is load-bearing rather than defensive.
|
|
68
|
+
*
|
|
69
|
+
* A caller may pass any TTL it likes, which makes "hold this for a year" one
|
|
70
|
+
* typo away — and a year-long hold is indistinguishable from permanently
|
|
71
|
+
* deleted stock, because nothing in the UI shows a seller why their own
|
|
72
|
+
* product is unbuyable. 30 days is longer than any agreement window Part 6
|
|
73
|
+
* contemplates and short enough that a mistake self-heals.
|
|
74
|
+
*/
|
|
75
|
+
export const RESERVATION_MAX_TTL_HOURS = 24 * 30;
|
|
76
|
+
/** A reservation is only holding stock while it is `held` and unexpired. */
|
|
77
|
+
export function isReservationActive(reservation, now = new Date()) {
|
|
78
|
+
if (reservation.status !== "held")
|
|
79
|
+
return false;
|
|
80
|
+
const expires = toDate(reservation.expiresAt);
|
|
81
|
+
return expires !== null && expires.getTime() > now.getTime();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Firestore hands back a `Timestamp`, a `Date` or an ISO string depending on
|
|
85
|
+
* the path a document took to get here, and the adapter that normalises those
|
|
86
|
+
* is not reachable from this file. Returning `null` on anything unparseable is
|
|
87
|
+
* the honest answer — and callers treat an unparseable expiry as NOT active,
|
|
88
|
+
* so the failure mode is a hold released early rather than one held forever.
|
|
89
|
+
*/
|
|
90
|
+
function toDate(value) {
|
|
91
|
+
if (value instanceof Date)
|
|
92
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
93
|
+
if (value && typeof value.toDate === "function") {
|
|
94
|
+
const d = value.toDate();
|
|
95
|
+
return d instanceof Date && !Number.isNaN(d.getTime()) ? d : null;
|
|
96
|
+
}
|
|
97
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
98
|
+
const d = new Date(value);
|
|
99
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
export { toDate as reservationExpiryDate };
|
|
@@ -94,6 +94,14 @@ export interface AppLayoutShellProps {
|
|
|
94
94
|
userWishlistHref?: string;
|
|
95
95
|
/** Href for the user settings page — auto-generates PROFILE section in sidebar. */
|
|
96
96
|
userSettingsHref?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Href for the user's OWN reviews list — the target of the "Reviews" stat.
|
|
99
|
+
*
|
|
100
|
+
* The stat pointed at `${profileHref}#reviews` before, an anchor the profile
|
|
101
|
+
* page does not render, so the count linked to the top of an unrelated page.
|
|
102
|
+
* Omitting this prop hides the tile rather than guessing at a route.
|
|
103
|
+
*/
|
|
104
|
+
userReviewsHref?: string;
|
|
97
105
|
/** Locale switcher node rendered at the bottom of the sidebar. */
|
|
98
106
|
sidebarLocaleSlot?: React.ReactNode;
|
|
99
107
|
/** When true, shows theme toggle button in the sidebar footer. */
|
|
@@ -148,4 +156,4 @@ export interface AppLayoutShellProps {
|
|
|
148
156
|
};
|
|
149
157
|
};
|
|
150
158
|
}
|
|
151
|
-
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
|
|
159
|
+
export declare function AppLayoutShell({ children, navItems, sidebarItems, sidebarSections, sidebarPrimaryActions, sidebarTitle, hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle, suppressDashboardNav, hideSidebarToggle, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, eventBannerSlot, showBottomNav, contentClassName, lightBackground, darkBackground, }: AppLayoutShellProps): React.JSX.Element;
|
|
@@ -17,6 +17,7 @@ const ROLE_DOT_BG_CLASS = {
|
|
|
17
17
|
import { useTheme } from "../../_internal/client/theme/index.js";
|
|
18
18
|
import { useAuth } from "../../react/contexts/SessionContext.js";
|
|
19
19
|
import { useVisualViewportInset } from "../../react/hooks/useVisualViewportInset.js";
|
|
20
|
+
import { useListingTypeFlags } from "../../react/hooks/useListingTypeFlags.js";
|
|
20
21
|
import { isBuyerUser } from "../auth/role-predicates.js";
|
|
21
22
|
import { NavbarWithSettings } from "./NavbarWithSettings.js";
|
|
22
23
|
import SideActionDock from "./SideActionDock.js";
|
|
@@ -135,13 +136,20 @@ function SidebarUserHeader({ user, onClose, profileHref, }) {
|
|
|
135
136
|
function SidebarGuestHeader({ sidebarTitle, onClose, }) {
|
|
136
137
|
return (_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Div, { textWeight: "semibold", textSize: "sm", color: "primary", children: sidebarTitle }), _jsx("button", { type: "button", "aria-label": "Close menu", onClick: onClose, className: "rounded-full p-[var(--appkit-space-2)] hover:bg-surface-hover hover:text-zinc-900 text-[var(--appkit-color-text-muted)] dark:hover:text-zinc-100 transition-all hover:rotate-90", children: _jsx("svg", { className: "w-4 h-4 shrink-0", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] }));
|
|
137
138
|
}
|
|
138
|
-
function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
|
|
139
|
+
function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions, user, activeHref, profileHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, adminHref, storeHref, sellerHref, sidebarLocaleSlot, showThemeToggleInSidebar, sidebarProfileLabels, theme, activeThemeName, toggleTheme, onLogout, onAfterLogout, }) {
|
|
139
140
|
const hasLegacyItems = sidebarItems.length > 0;
|
|
140
141
|
const hasSections = !!(sidebarSections && sidebarSections.length > 0);
|
|
141
142
|
const isAuthenticated = !!user;
|
|
142
143
|
const role = user?.role ?? "user";
|
|
143
144
|
const isAdminOrSeller = role === "admin" || role === "seller";
|
|
144
145
|
const resolvedStoreHref = storeHref ?? sellerHref;
|
|
146
|
+
/*
|
|
147
|
+
* Defaults to ENABLED while site settings are loading, matching the hook's
|
|
148
|
+
* own contract — hiding the tile on first paint and popping it in a moment
|
|
149
|
+
* later is worse than showing it to the handful of installs that have
|
|
150
|
+
* auctions switched off.
|
|
151
|
+
*/
|
|
152
|
+
const auctionsEnabled = useListingTypeFlags().isEnabled("auction");
|
|
145
153
|
const labels = {
|
|
146
154
|
sectionTitle: sidebarProfileLabels?.sectionTitle ?? "Profile",
|
|
147
155
|
profile: sidebarProfileLabels?.profile ?? "My Profile",
|
|
@@ -177,10 +185,10 @@ function SidebarContent({ sidebarItems, sidebarSections, sidebarPrimaryActions,
|
|
|
177
185
|
action.variant === "outline"
|
|
178
186
|
? "border hover:bg-surface-hover border-[var(--appkit-color-border)] text-[var(--appkit-color-text)]"
|
|
179
187
|
: "bg-primary text-white hover:bg-primary-600 dark:bg-primary dark:hover:bg-primary-600 btn-glow",
|
|
180
|
-
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && (_jsxs(TextLink, { href:
|
|
188
|
+
].join(" "), children: action.label }, `${action.href}-${action.label}`))) })), isAuthenticated && (_jsx(CollapsibleNavGroup, { title: labels.sectionTitle, hasActive: !!activeProfileLink, children: _jsxs(Ul, { spacing: "2xs", children: [_jsx(Li, { children: _jsx(TextLink, { href: profileHref, variant: "none", className: activeProfileLink?.href === profileHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === profileHref ? "page" : undefined, children: labels.profile }) }), userOrdersHref && (_jsx(Li, { children: _jsx(TextLink, { href: userOrdersHref, variant: "none", className: activeProfileLink?.href === userOrdersHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userOrdersHref ? "page" : undefined, children: labels.orders }) })), userWishlistHref && (_jsx(Li, { children: _jsx(TextLink, { href: userWishlistHref, variant: "none", className: activeProfileLink?.href === userWishlistHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userWishlistHref ? "page" : undefined, children: labels.wishlist }) })), userSettingsHref && (_jsx(Li, { children: _jsx(TextLink, { href: userSettingsHref, variant: "none", className: activeProfileLink?.href === userSettingsHref ? navItemActiveClass : navItemClass, "aria-current": activeProfileLink?.href === userSettingsHref ? "page" : undefined, children: labels.settings }) }))] }) })), isAuthenticated && user?.stats && (_jsxs(Div, { layout: "grid", gap: "2", className: "grid-cols-2", children: [user.stats.totalOrders != null && userOrdersHref && (_jsxs(TextLink, { href: userOrdersHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.totalOrders }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Orders" })] })), user.stats.reviewsCount != null && userReviewsHref && (_jsxs(TextLink, { href: userReviewsHref, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.reviewsCount }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Reviews" })] })), auctionsEnabled && user.stats.auctionsWon != null && userOrdersHref && (_jsxs(TextLink, { href: `${userOrdersHref}?orderType=auction`, variant: "none", className: CLS_STAT_BOX, children: [_jsx(Text, { className: "leading-none", color: "primary", size: "lg", weight: "bold", children: user.stats.auctionsWon }), _jsx(Text, { className: CLS_STAT_LABEL, children: "Auctions Won" })] }))] })), isAuthenticated && isAdminOrSeller && (adminHref || resolvedStoreHref) && (_jsx(CollapsibleNavGroup, { title: labels.dashboardSectionTitle, hasActive: !!activeDashboardLink, children: _jsxs(Ul, { spacing: "2xs", children: [adminHref && role === "admin" && (_jsx(Li, { children: _jsx(TextLink, { href: adminHref, variant: "none", className: activeDashboardLink?.href === adminHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === adminHref ? "page" : undefined, children: labels.adminDashboard }) })), resolvedStoreHref && isAdminOrSeller && (_jsx(Li, { children: _jsx(TextLink, { href: resolvedStoreHref, variant: "none", className: activeDashboardLink?.href === resolvedStoreHref ? navItemActiveClass : navItemClass, "aria-current": activeDashboardLink?.href === resolvedStoreHref ? "page" : undefined, children: labels.storeDashboard }) }))] }) })), normalizedSections.map((section, sectionIndex) => (_jsx(CollapsibleSidebarSection, { section: section, navItemClass: navItemClass, navItemActiveClass: navItemActiveClass, activeHref: activeHref }, `sidebar-section-${sectionIndex}`))), (sidebarLocaleSlot || showThemeToggleInSidebar || (isAuthenticated && onLogout)) && (_jsxs(Stack, { border: "default", className: "border-t border-[var(--appkit-color-border-subtle)]", padding: "t-md", gap: "3", children: [sidebarLocaleSlot, showThemeToggleInSidebar && (_jsxs("button", { type: "button", onClick: toggleTheme, className: "flex w-full items-center justify-end gap-[var(--appkit-space-3)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] transition-colors hover:bg-primary-surface hover:text-primary-800 dark:hover:text-primary-200", children: [_jsx(Span, { "aria-hidden": "true", children: theme === "dark" ? "☀️" : "🌙" }), activeThemeName] })), isAuthenticated && onLogout && (_jsx("button", { type: "button", onClick: () => { onLogout(); onAfterLogout(); }, className: CLS_LOGOUT_BTN, children: labels.logout }))] }))] }));
|
|
181
189
|
}
|
|
182
190
|
// ─── Main component ────────────────────────────────────────────────────────────
|
|
183
|
-
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
191
|
+
export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarSections, sidebarPrimaryActions, sidebarTitle = "Navigation", hiddenNavItems, user, brandName, brandShortName, siteLogoUrl, logoHref, promotionsHref, cartHref, wishlistHref, messagesHref, unreadMessageCount, userId, profileHref, loginHref, registerHref, homeHref, shopHref, footer, showFooter = true, searchSlot, searchSlotRenderer, titleBarNavSlot, titleBarNotificationSlot, titleBarDevSlot, titleBarPromoStripText, showThemeToggle = false, suppressDashboardNav = false, hideSidebarToggle = false, onLogout, adminHref, storeHref, sellerHref, userOrdersHref, userWishlistHref, userSettingsHref, userReviewsHref, sidebarLocaleSlot, showThemeToggleInSidebar = false, sidebarProfileLabels, eventBannerSlot, showBottomNav = true, contentClassName, lightBackground = DEFAULT_LIGHT_BG, darkBackground = DEFAULT_DARK_BG, }) {
|
|
184
192
|
// QueryClientProvider used to be created here so AppLayoutShell could stand
|
|
185
193
|
// alone. The real cause of the 2026-06-10/11 "No QueryClient set" prod crash
|
|
186
194
|
// turned out to be a peer-dep duplicate of @tanstack/query-core under
|
|
@@ -228,7 +236,7 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
|
|
|
228
236
|
// by its countdown alone got no clearance). That mirror is gone: BottomChrome
|
|
229
237
|
// measures the real tier and publishes --bottom-chrome-height, and the footer
|
|
230
238
|
// reserves space from that. Nothing needs to guess any more.
|
|
231
|
-
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
239
|
+
const sidebarContent = (_jsx(SidebarContent, { sidebarItems: sidebarItems, sidebarSections: sidebarSections, sidebarPrimaryActions: sidebarPrimaryActions, user: user, activeHref: pathname ?? "", profileHref: profileHref, userOrdersHref: userOrdersHref, userWishlistHref: userWishlistHref, userSettingsHref: userSettingsHref, userReviewsHref: userReviewsHref, adminHref: adminHref, storeHref: storeHref, sellerHref: sellerHref, sidebarLocaleSlot: sidebarLocaleSlot, showThemeToggleInSidebar: showThemeToggleInSidebar, sidebarProfileLabels: sidebarProfileLabels, theme: theme, activeThemeName: activeTheme.name, toggleTheme: toggleTheme, onLogout: onLogout, onAfterLogout: () => setSidebarOpen(false) }));
|
|
232
240
|
const normalizedLightBackground = {
|
|
233
241
|
type: lightBackground.type,
|
|
234
242
|
value: lightBackground.value,
|
|
@@ -233,7 +233,8 @@ export default function BottomActions() {
|
|
|
233
233
|
* own reveal — bulk mode is a state the user just entered deliberately, and
|
|
234
234
|
* making them scroll to act on it would be a puzzle.
|
|
235
235
|
*/
|
|
236
|
-
const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId)
|
|
236
|
+
const { revealed: ctaRevealed } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
|
|
237
|
+
const revealed = ctaRevealed || isBulkMode;
|
|
237
238
|
const isVisible = hasContent && revealed;
|
|
238
239
|
// A panel left open after its content is withdrawn (bulk mode taking over, or
|
|
239
240
|
// the page clearing it) would sit there as an empty sheet.
|
|
@@ -32,12 +32,20 @@ export interface BottomNavbarUser {
|
|
|
32
32
|
};
|
|
33
33
|
zoom: number;
|
|
34
34
|
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* The account holder's own activity AS A BUYER, derived live by
|
|
37
|
+
* `/api/user/profile`. `undefined` on a member means the count could not be
|
|
38
|
+
* taken — render nothing, never 0.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 `itemsSold` and `rating` were removed, not forgotten. `itemsSold` is a
|
|
41
|
+
* SELLER number and had no business in the account holder's own profile
|
|
42
|
+
* block (see `AppLayoutShell`); `rating` had no writer anywhere in either
|
|
43
|
+
* repo, so its `!= null` guard meant the tile could never render.
|
|
44
|
+
*/
|
|
35
45
|
stats?: {
|
|
36
46
|
totalOrders?: number;
|
|
37
47
|
auctionsWon?: number;
|
|
38
|
-
itemsSold?: number;
|
|
39
48
|
reviewsCount?: number;
|
|
40
|
-
rating?: number;
|
|
41
49
|
} | null;
|
|
42
50
|
}
|
|
43
51
|
export interface BottomNavbarProps {
|
|
@@ -54,7 +54,7 @@ import { useClickOutside } from "../../react/index.js";
|
|
|
54
54
|
import { useHandMode } from "../../_internal/client/hand-mode/index.js";
|
|
55
55
|
import { useCtaReveal } from "./useCtaReveal.js";
|
|
56
56
|
import { useIsDesktop } from "./useIsDesktop.js";
|
|
57
|
-
import { ActionRow, Div, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
|
|
57
|
+
import { ActionRow, Div, ICON_SIZE, IconButton, Row, Span, Stack, Text, Button } from "../../ui/index.js";
|
|
58
58
|
const PANEL_SURFACE = "bg-[color-mix(in_srgb,var(--appkit-color-bg)_94%,transparent)] backdrop-blur-md border border-[var(--appkit-color-border)]";
|
|
59
59
|
const FLEX_CENTER = "flex items-center justify-center";
|
|
60
60
|
const CLS_COUNT_BADGE = "bg-error-solid text-error-on-solid";
|
|
@@ -143,8 +143,40 @@ export default function SideActionDock() {
|
|
|
143
143
|
!!infoLabel ||
|
|
144
144
|
!!secondaryLabel;
|
|
145
145
|
// Rule 3 — a selection is its own reveal.
|
|
146
|
-
const revealed = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId)
|
|
146
|
+
const { revealed: ctaRevealed, hasAnchor, scrollToAnchor, } = useCtaReveal(hasContent && !isBulkMode, ctaAnchorId);
|
|
147
|
+
const revealed = ctaRevealed || isBulkMode;
|
|
147
148
|
const isVisible = isDesktop && hasContent && revealed;
|
|
149
|
+
/*
|
|
150
|
+
* ── Rule 4 — a page with a REAL Buy button gets a way BACK to it ─────────
|
|
151
|
+
*
|
|
152
|
+
* On a detail page the dock was a second copy of the CTA you had just
|
|
153
|
+
* scrolled past, which is the one thing rule 2 says a floating control must
|
|
154
|
+
* not be: `useCtaReveal` already means "you have met the Buy button and left
|
|
155
|
+
* it", so re-offering the purchase mid-screen answers a question the user
|
|
156
|
+
* did not ask. Taking them back to the real control answers the one they
|
|
157
|
+
* did — and it keeps the purchase decision on the page's own CTA, beside
|
|
158
|
+
* the price, the variant and the stock state that qualify it.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 Gated on `hasAnchor`, NOT on a route or a listing type. A bulk screen
|
|
161
|
+
* marks no anchor, so it keeps the centred dock carrying its actions — which
|
|
162
|
+
* is correct, because there was never an inline control there to return to.
|
|
163
|
+
* `ctaAnchorProps` has exactly one caller today (`ProductDetailPageView`),
|
|
164
|
+
* so this branch is opt-in by construction and ~70 admin bulk bars cannot
|
|
165
|
+
* reach it.
|
|
166
|
+
*
|
|
167
|
+
* It sits in the bottom-right cluster above `BackToTop` and shares that
|
|
168
|
+
* control's bottom-tier `calc()` verbatim, so the two stack rather than
|
|
169
|
+
* overlap — and rule 1's objection (a bottom-anchored bar reads as a browser
|
|
170
|
+
* download bar) does not apply to a single round puck in the corner that is
|
|
171
|
+
* already where the scroll affordance lives.
|
|
172
|
+
*
|
|
173
|
+
* 🛑 The RETURN for this branch is deliberately deferred to just above the
|
|
174
|
+
* main one. An early return here would sit above this component's remaining
|
|
175
|
+
* `useEffect`s, so the hook order would differ between a detail page and a
|
|
176
|
+
* bulk screen — React's "rendered fewer hooks than expected" crash, and the
|
|
177
|
+
* kind that only shows up on the page nobody opened during review.
|
|
178
|
+
*/
|
|
179
|
+
const isBackToCta = isDesktop && hasAnchor && !isBulkMode;
|
|
148
180
|
// Rule 1 — opposite the sidebar, which docks to the hand-mode side.
|
|
149
181
|
const dockSideClass = hand === "left" ? "right-0" : "left-0";
|
|
150
182
|
const panelAlignClass = hand === "left" ? "items-end" : "items-start";
|
|
@@ -216,6 +248,24 @@ export default function SideActionDock() {
|
|
|
216
248
|
const fabLabel = isBulkMode
|
|
217
249
|
? `${bulk?.selectedCount ?? 0} selected — bulk actions`
|
|
218
250
|
: (primaryAction?.label ?? infoLabel ?? "Page actions");
|
|
251
|
+
// Rule 4's deferred return — below every hook, for the reason given above.
|
|
252
|
+
if (isBackToCta) {
|
|
253
|
+
if (!ctaRevealed)
|
|
254
|
+
return null;
|
|
255
|
+
return (_jsx(Div, { className: [
|
|
256
|
+
"fixed pointer-events-auto z-[var(--appkit-z-back-to-top)]",
|
|
257
|
+
hand === "left" ? "left-4" : "right-4",
|
|
258
|
+
/*
|
|
259
|
+
* `BackToTop`'s own expression verbatim — including its `4rem`
|
|
260
|
+
* default for `--bottom-nav-height`, not the `0px` the dock's other
|
|
261
|
+
* branch would imply — plus 3.5rem to clear that control's touch
|
|
262
|
+
* box and its gap. Copying the expression rather than inventing one
|
|
263
|
+
* is what keeps the two stacked when the tier changes height.
|
|
264
|
+
*/
|
|
265
|
+
"bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--bottom-chrome-height,0px)+var(--appkit-space-4)+3.5rem)]",
|
|
266
|
+
"transition-[bottom] duration-300 ease-out",
|
|
267
|
+
].join(" "), "data-section": "back-to-cta", children: _jsxs(Button, { type: "button", variant: "primary", size: "sm", onClick: scrollToAnchor, "aria-label": `Back to ${primaryAction?.label ?? "the buy button"}`, children: [_jsx(ChevronUp, { "aria-hidden": "true", size: ICON_SIZE.sm }), `Back to ${primaryAction?.label ?? "buy"}`] }) }));
|
|
268
|
+
}
|
|
219
269
|
return (_jsxs(Div, { ref: containerRef, role: "toolbar", "aria-label": isBulkMode ? "Bulk actions" : "Page actions", className: [
|
|
220
270
|
"fixed z-[var(--appkit-z-overlay)] pointer-events-auto flex flex-col",
|
|
221
271
|
panelAlignClass,
|
|
@@ -23,6 +23,15 @@ const iconBtn = "flex items-center justify-center w-11 h-11 rounded-lg text-[var
|
|
|
23
23
|
* `important: true`, so the utility would beat the unlayered rule and the badge
|
|
24
24
|
* would silently stop mirroring. The vertical `-top-0.5` is hand-neutral and stays. */
|
|
25
25
|
const countBadge = "appkit-hand-badge absolute -top-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-solid text-error-on-solid text-[10px] font-bold leading-none";
|
|
26
|
+
/**
|
|
27
|
+
* Applied to the GLYPH, never the link, when the icon is carrying something.
|
|
28
|
+
*
|
|
29
|
+
* On the glyph so the count badge stays put: animating the link would swing
|
|
30
|
+
* the number with it, which turns a readable badge into a moving target. The
|
|
31
|
+
* keyframes and the `prefers-reduced-motion` opt-out live in
|
|
32
|
+
* `TitleBarLayout.style.css`.
|
|
33
|
+
*/
|
|
34
|
+
const nudgeWhenCarrying = (count) => count > 0 ? "appkit-nav-icon--has-items" : "";
|
|
26
35
|
/**
|
|
27
36
|
* TitleBarLayout — generic top sticky title-bar shell.
|
|
28
37
|
*
|
|
@@ -50,13 +59,13 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
50
59
|
// Dedicated notification bell — rendered immediately before the wishlist
|
|
51
60
|
// icon (per layout request). Falls back to invisible when no href is set;
|
|
52
61
|
// the count badge mirrors the wishlist/cart pattern.
|
|
53
|
-
const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg" }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
|
|
62
|
+
const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "notification", size: "lg", className: nudgeWhenCarrying(unreadNotificationCount) }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
|
|
54
63
|
// D9 — chat. Same shape as the notification bell: absent entirely when no
|
|
55
64
|
// href is supplied (the consumer only passes one when
|
|
56
65
|
// `siteSettings.chat.enabled === true`), never rendered-but-disabled.
|
|
57
|
-
const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg" }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
|
|
58
|
-
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg" }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
|
|
59
|
-
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg" }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
|
|
66
|
+
const messagesEl = messagesHref ? (_jsxs(Link, { href: messagesHref, "aria-label": `Messages${unreadMessageCount > 0 ? `, ${unreadMessageCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx(Icon, { name: "message", size: "lg", className: nudgeWhenCarrying(unreadMessageCount) }), unreadMessageCount > 0 && (_jsx(Span, { className: countBadge, children: unreadMessageCount > 99 ? "99+" : unreadMessageCount }))] })) : null;
|
|
67
|
+
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", children: [_jsx(Icon, { name: "wishlist", size: "lg", className: nudgeWhenCarrying(wishlistCount) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
|
|
68
|
+
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", children: [_jsx(Icon, { name: "cart", size: "lg", className: nudgeWhenCarrying(cartCount) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
|
|
60
69
|
const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, "data-tour": "nav-profile", children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
|
|
61
70
|
const authButtonsEl = !user && (loginHref || registerHref) ? (
|
|
62
71
|
// Mirrors with TB1 so Register (the primary CTA) keeps the outer, more
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Title-bar icons that are carrying something.
|
|
3
|
+
*
|
|
4
|
+
* The count badge already states the fact, but it is a 16px dot in the corner
|
|
5
|
+
* of a dense bar — easy to miss on a phone, and it never changes once drawn.
|
|
6
|
+
* A small periodic nudge is what makes "you have three things in your cart"
|
|
7
|
+
* noticeable without adding a second place to read the number.
|
|
8
|
+
*
|
|
9
|
+
* 🛑 It is a PERIODIC nudge, not a loop. The movement lasts about half a
|
|
10
|
+
* second at the END of a 10s cycle and the icon is still for the other ~9.5s,
|
|
11
|
+
* so it reads as an occasional glance-catcher rather than a permanent
|
|
12
|
+
* animation sitting in the chrome of every page. A continuously-moving element
|
|
13
|
+
* in a fixed header is the kind of thing people learn to tune out — or leave.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 The percentages and the duration are ONE number, not two. The keyframe
|
|
16
|
+
* stops are a fraction of the cycle, so changing `10s` alone silently
|
|
17
|
+
* stretches the nudge itself — at 20s the same stops would swing the bell over
|
|
18
|
+
* a full second, which reads as a glitch rather than a nudge. Rescale the
|
|
19
|
+
* stops with it: the window is 4.8% here, which is ~480ms of a 10s cycle.
|
|
20
|
+
*
|
|
21
|
+
* The pivot is near the TOP of the glyph so the motion reads as a bell swing
|
|
22
|
+
* rather than a wobble around the middle, which is what the notification bell
|
|
23
|
+
* wants and what the cart and wishlist tolerate.
|
|
24
|
+
*/
|
|
25
|
+
@keyframes appkit-nav-icon-nudge {
|
|
26
|
+
0%,
|
|
27
|
+
95.2%,
|
|
28
|
+
100% {
|
|
29
|
+
transform: rotate(0deg);
|
|
30
|
+
}
|
|
31
|
+
96.1% {
|
|
32
|
+
transform: rotate(-12deg);
|
|
33
|
+
}
|
|
34
|
+
97.3% {
|
|
35
|
+
transform: rotate(9deg);
|
|
36
|
+
}
|
|
37
|
+
98.5% {
|
|
38
|
+
transform: rotate(-5deg);
|
|
39
|
+
}
|
|
40
|
+
99.4% {
|
|
41
|
+
transform: rotate(2deg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.appkit-nav-icon--has-items {
|
|
46
|
+
transform-origin: 50% 15%;
|
|
47
|
+
animation: appkit-nav-icon-nudge 10s ease-in-out infinite;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* 🛑 Honoured, not approximated. This is decoration carrying no information
|
|
52
|
+
* the badge does not already carry, so for a reader who has asked for less
|
|
53
|
+
* motion the correct amount is none — not a shorter or gentler version.
|
|
54
|
+
*/
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
.appkit-nav-icon--has-items {
|
|
57
|
+
animation: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -21,4 +21,19 @@
|
|
|
21
21
|
* page that owns it mounts AFTER this dock — the dock lives in the app shell
|
|
22
22
|
* and the CTA arrives with the route.
|
|
23
23
|
*/
|
|
24
|
-
export
|
|
24
|
+
export interface CtaRevealState {
|
|
25
|
+
/** Should the floating dock / bar be on screen? */
|
|
26
|
+
revealed: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Does this page mark an inline CTA at all?
|
|
29
|
+
*
|
|
30
|
+
* The discriminator between a DETAIL page (a real Buy button exists, so the
|
|
31
|
+
* floating control's job is to take you back to it) and a BULK screen (no
|
|
32
|
+
* Buy button was ever there, so the floating control IS the only affordance
|
|
33
|
+
* and must carry the actions itself).
|
|
34
|
+
*/
|
|
35
|
+
hasAnchor: boolean;
|
|
36
|
+
/** Scroll the inline CTA back into view. A no-op when there is no anchor. */
|
|
37
|
+
scrollToAnchor: () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function useCtaReveal(enabled?: boolean, anchorId?: string): CtaRevealState;
|