@mohasinac/appkit 4.9.4 → 4.10.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.
Files changed (160) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
  2. package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +5 -1
  3. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +8 -2
  4. package/dist/_internal/client/features/layout/sidebarPositionClasses.d.ts +20 -0
  5. package/dist/_internal/client/features/layout/sidebarPositionClasses.js +20 -0
  6. package/dist/_internal/client/hand-mode/HandModeProvider.d.ts +21 -0
  7. package/dist/_internal/client/hand-mode/HandModeProvider.js +81 -0
  8. package/dist/_internal/client/hand-mode/index.d.ts +2 -0
  9. package/dist/_internal/client/hand-mode/index.js +1 -0
  10. package/dist/_internal/server/features/auctions/actions.js +3 -2
  11. package/dist/_internal/server/features/auctions/service.d.ts +4 -3
  12. package/dist/_internal/server/features/auctions/service.js +6 -6
  13. package/dist/_internal/server/features/bundles/data.d.ts +9 -0
  14. package/dist/_internal/server/features/bundles/data.js +14 -0
  15. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  16. package/dist/_internal/server/features/bundles/index.js +1 -1
  17. package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
  18. package/dist/_internal/server/features/catalogue/og.js +27 -0
  19. package/dist/_internal/server/features/checkout/actions.d.ts +43 -0
  20. package/dist/_internal/server/features/checkout/actions.js +172 -81
  21. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  22. package/dist/_internal/server/features/checkout/index.js +1 -1
  23. package/dist/_internal/server/features/grouped/data.d.ts +12 -0
  24. package/dist/_internal/server/features/grouped/data.js +26 -1
  25. package/dist/_internal/server/features/grouped/index.d.ts +1 -1
  26. package/dist/_internal/server/features/grouped/index.js +1 -1
  27. package/dist/_internal/server/features/products/data.d.ts +23 -0
  28. package/dist/_internal/server/features/products/data.js +159 -0
  29. package/dist/_internal/server/features/products/index.d.ts +1 -1
  30. package/dist/_internal/server/features/products/index.js +1 -1
  31. package/dist/_internal/server/features/seo/robots.js +3 -2
  32. package/dist/_internal/server/features/seo/sitemap.js +70 -5
  33. package/dist/_internal/shared/config/schema.d.ts +1 -0
  34. package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
  35. package/dist/_internal/shared/features/auctions/config.js +31 -1
  36. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
  37. package/dist/_internal/shared/listing-types/art/config.js +1 -1
  38. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
  39. package/dist/_internal/shared/listing-types/classified/config.js +2 -2
  40. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
  41. package/dist/_internal/shared/listing-types/digital-code/config.js +2 -2
  42. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
  43. package/dist/_internal/shared/listing-types/live/config.js +2 -2
  44. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
  45. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  46. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
  47. package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
  48. package/dist/client.d.ts +4 -6
  49. package/dist/client.js +10 -4
  50. package/dist/constants/api-endpoints.d.ts +6 -0
  51. package/dist/constants/api-endpoints.js +2 -0
  52. package/dist/features/about/components/AboutView.js +2 -2
  53. package/dist/features/about/components/DeveloperView.js +2 -2
  54. package/dist/features/about/schemas/firestore.d.ts +1 -0
  55. package/dist/features/account/components/UserSidebar.js +8 -2
  56. package/dist/features/account/hooks/useProfile.d.ts +2 -0
  57. package/dist/features/admin/components/AdminSidebar.js +8 -2
  58. package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
  59. package/dist/features/admin/schemas/firestore.d.ts +9 -0
  60. package/dist/features/admin/schemas/firestore.js +6 -0
  61. package/dist/features/auctions/actions/bid-actions.js +4 -1
  62. package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
  63. package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
  64. package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
  65. package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
  66. package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
  67. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
  68. package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
  69. package/dist/features/auth/schemas/firestore.d.ts +3 -0
  70. package/dist/features/cart/components/CartDrawer.js +4 -1
  71. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
  72. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
  73. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
  74. package/dist/features/catalogue/index.d.ts +2 -0
  75. package/dist/features/catalogue/index.js +1 -0
  76. package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
  77. package/dist/features/categories/components/BundleDetailView.js +3 -2
  78. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
  79. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
  80. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
  82. package/dist/features/contact/components/ContactPageView.js +8 -2
  83. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
  84. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
  85. package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
  86. package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
  87. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
  88. package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
  89. package/dist/features/homepage/components/BrandsSection.js +3 -1
  90. package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
  91. package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
  92. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  93. package/dist/features/homepage/components/SectionCarousel.js +5 -2
  94. package/dist/features/homepage/components/SocialFeedSection.js +2 -2
  95. package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
  96. package/dist/features/layout/BackToTop.js +3 -1
  97. package/dist/features/layout/BottomActions.js +4 -3
  98. package/dist/features/layout/BottomActionsContext.d.ts +10 -0
  99. package/dist/features/layout/BottomActionsContext.js +5 -1
  100. package/dist/features/layout/SidebarLayout.js +7 -1
  101. package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
  102. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  103. package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
  104. package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
  105. package/dist/features/media/MediaVideo.js +17 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +11 -116
  109. package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
  110. package/dist/features/products/components/RelatedItemsSection.js +17 -0
  111. package/dist/features/seller/components/SellerProductShell.js +1 -1
  112. package/dist/features/seller/components/SellerSidebar.js +8 -2
  113. package/dist/features/shell/QuickFormDrawer.js +3 -1
  114. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +114 -3
  115. package/dist/hand-mode/index.d.ts +12 -0
  116. package/dist/hand-mode/index.js +11 -0
  117. package/dist/index.d.ts +2 -0
  118. package/dist/index.js +1 -0
  119. package/dist/next/routing/route-map.d.ts +2 -0
  120. package/dist/next/routing/route-map.js +7 -0
  121. package/dist/react/contexts/SessionContext.d.ts +5 -0
  122. package/dist/react/contexts/SessionContext.js +2 -0
  123. package/dist/seed/bids-seed-data.js +48 -3
  124. package/dist/seed/grouped-listings-seed-data.js +167 -0
  125. package/dist/seed/payouts-seed-data.js +4 -4
  126. package/dist/seed/products-art-seed-data.js +188 -5
  127. package/dist/seed/products-auctions-seed-data.js +215 -6
  128. package/dist/seed/products-classifieds-seed-data.js +255 -8
  129. package/dist/seed/products-digital-codes-seed-data.js +258 -19
  130. package/dist/seed/products-live-items-seed-data.js +143 -10
  131. package/dist/seed/products-preorders-seed-data.js +213 -8
  132. package/dist/seed/products-prize-draws-seed-data.js +198 -0
  133. package/dist/seed/products-standard-seed-data.js +141 -0
  134. package/dist/seed/products-stickers-seed-data.js +178 -5
  135. package/dist/seed/site-settings-seed-data.js +13 -31
  136. package/dist/seed/store-extensions-seed-data.js +1 -1
  137. package/dist/server-entry.d.ts +5 -2
  138. package/dist/server-entry.js +5 -2
  139. package/dist/server.d.ts +4 -2
  140. package/dist/server.js +4 -2
  141. package/dist/styles.css +30 -1
  142. package/dist/tailwind-utilities.css +1 -1
  143. package/dist/ui/components/Drawer.js +7 -4
  144. package/dist/ui/components/FilterDrawer.js +1 -1
  145. package/dist/ui/components/HandModeRow.d.ts +12 -0
  146. package/dist/ui/components/HandModeRow.js +18 -0
  147. package/dist/ui/components/ImageLightbox.js +37 -3
  148. package/dist/ui/components/ImageLightbox.style.css +28 -0
  149. package/dist/ui/components/Layout.d.ts +5 -1
  150. package/dist/ui/components/Layout.js +2 -1
  151. package/dist/ui/components/Layout.style.css +1 -0
  152. package/dist/ui/components/ListingFilterDrawer.js +3 -1
  153. package/dist/ui/components/Modal.js +3 -1
  154. package/dist/ui/components/SideDrawer.js +8 -5
  155. package/dist/ui/components/SideModal.js +7 -4
  156. package/dist/ui/index.d.ts +1 -0
  157. package/dist/ui/index.js +1 -0
  158. package/dist/utils/media-url.d.ts +9 -0
  159. package/dist/utils/media-url.js +39 -0
  160. package/package.json +1 -1
@@ -33,6 +33,7 @@ import { DASHBOARD_DESKTOP_MEDIA_QUERY } from "../../../shared/features/layout/c
33
33
  import { filterNavItems } from "./filterNavItems";
34
34
  import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
35
35
  import { useTheme } from "../../theme";
36
+ import { useHandMode } from "../../hand-mode";
36
37
  import { BackgroundRenderer, Div, Nav, Span } from "../../../../ui";
37
38
  import { useToast } from "../../../../ui/components/Toast";
38
39
  import { useVisualViewportInset } from "../../../../react/hooks/useVisualViewportInset";
@@ -149,7 +150,10 @@ function DashboardBottomNav({ items, activeHref }) {
149
150
  return (_jsxs(Link, { href: item.href, className: `flex flex-1 flex-col items-center justify-center gap-[var(--appkit-space-0-5)] py-[var(--appkit-space-2)] ${isActive ? "text-[var(--appkit-color-primary)]" : "text-[var(--appkit-color-text-muted)]"}`, "aria-current": isActive ? "page" : undefined, children: [item.icon && _jsx(Span, { size: "base", children: item.icon }), _jsx(Span, { size: "xs", className: "max-w-full truncate", children: item.label })] }, item.href));
150
151
  }) }));
151
152
  }
152
- const DEFAULT_CONTENT_PADDING = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pl-14 lg:pr-6 xl:pl-16 xl:pr-10";
153
+ // Asymmetric padding clears the persistent sidebar rail — right-heavy by
154
+ // default (rail docks left), left-heavy in left-hand mode (rail docks right).
155
+ const DEFAULT_CONTENT_PADDING_RIGHT = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pl-14 lg:pr-6 xl:pl-16 xl:pr-10";
156
+ const DEFAULT_CONTENT_PADDING_LEFT = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pr-14 lg:pl-6 xl:pr-16 xl:pl-10";
153
157
  const DEFAULT_CONTENT_MAX_WIDTH = "max-w-screen-2xl";
154
158
  export function DashboardLayoutClient({ variant, groups, permissions, activeHref: explicitActiveHref, responsive: _responsive, crossNav, className, contentPadding, contentSurface, contentMaxWidth, lightBackground, darkBackground, children, }) {
155
159
  const pathname = usePathname();
@@ -178,6 +182,7 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
178
182
  const { data: settings } = useSiteSettings();
179
183
  const navConfig = settings?.navConfig;
180
184
  const { theme } = useTheme();
185
+ const { hand } = useHandMode();
181
186
  // Explicit props win; otherwise fall back to the same siteSettings.background
182
187
  // the public shell reads, so admin/store/user dashboards match it by default.
183
188
  const resolvedLightBackground = lightBackground ?? settings?.background?.light;
@@ -193,7 +198,7 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
193
198
  return (_jsxs(_Fragment, { children: [hasBackground && (_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: resolvedLightBackground ?? { type: "color", value: "" }, darkMode: resolvedDarkBackground ?? { type: "color", value: "" } })), variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), _jsx(Div, { className: [
194
199
  "w-full flex-1 flex flex-col min-h-[calc(100dvh-var(--header-height,3.5rem))]",
195
200
  "pb-[var(--appkit-space-16)] lg:pb-[0]", // clears the fixed mobile bottom-nav
196
- contentPadding ?? DEFAULT_CONTENT_PADDING,
201
+ contentPadding ?? (hand === "left" ? DEFAULT_CONTENT_PADDING_LEFT : DEFAULT_CONTENT_PADDING_RIGHT),
197
202
  contentSurface,
198
203
  ].filter(Boolean).join(" "), children: _jsx(Div, { className: ["w-full flex-1 mx-auto", contentMaxWidth ?? DEFAULT_CONTENT_MAX_WIDTH].filter(Boolean).join(" "), children: children }) }), _jsx(DashboardBottomNav, { items: filteredGroups.flatMap((g) => g.items), activeHref: activeHref })] }));
199
204
  }
@@ -15,5 +15,9 @@ export interface SidebarCollapseToggleProps {
15
15
  /** aria-label override; defaults to "Collapse sidebar" / "Expand sidebar". */
16
16
  expandedLabel?: string;
17
17
  collapsedLabel?: string;
18
+ /** Which edge the parent sidebar panel is docked to. Defaults to "right"
19
+ * (the handle protrudes to the panel's outer/right edge). "left" mirrors
20
+ * the rounded corner and chevron direction for left-hand mode. */
21
+ edge?: "left" | "right";
18
22
  }
19
- export declare function SidebarCollapseToggle({ expanded, onToggle, expandedLabel, collapsedLabel, }: SidebarCollapseToggleProps): React.JSX.Element;
23
+ export declare function SidebarCollapseToggle({ expanded, onToggle, expandedLabel, collapsedLabel, edge, }: SidebarCollapseToggleProps): React.JSX.Element;
@@ -7,6 +7,12 @@ const HANDLE_STYLE = {
7
7
  const CHEVRON_STYLE = {
8
8
  color: "var(--appkit-color-text-on-primary)",
9
9
  };
10
- export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Collapse sidebar", collapsedLabel = "Expand sidebar", }) {
11
- return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: "w-9 shrink-0 rounded-r-[1.25rem] shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]", icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${expanded ? "" : "rotate-180"}`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
10
+ export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Collapse sidebar", collapsedLabel = "Expand sidebar", edge = "right", }) {
11
+ // The panel's free (open) edge is opposite its dock side: a left-docked
12
+ // panel (edge="left") protrudes its handle to the right, and vice versa.
13
+ const roundedClass = edge === "left" ? "rounded-r-[1.25rem]" : "rounded-l-[1.25rem]";
14
+ // Chevron rotation direction mirrors along with the dock side so it keeps
15
+ // pointing "into" the collapsed panel regardless of which edge it's on.
16
+ const chevronRotated = edge === "left" ? !expanded : expanded;
17
+ return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""}`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
12
18
  }
@@ -0,0 +1,20 @@
1
+ import type { HandMode } from "../../hand-mode";
2
+ /** Classes for the desktop persistent sidebar rail (Admin/Seller/User
3
+ * `variant="sidebar"`) — flips which edge it docks to, its border side, and
4
+ * its collapsed-state translate direction, based on the hand-mode
5
+ * preference. Kept as a single shared helper so the flip logic isn't
6
+ * hand-forked identically across all three sidebar files. */
7
+ export interface SidebarRailClasses {
8
+ edgeClass: string;
9
+ borderClass: string;
10
+ collapsedTranslateClass: string;
11
+ }
12
+ export declare function getSidebarRailClasses(hand: HandMode): SidebarRailClasses;
13
+ /** Classes for the mobile overlay drawer variant (Admin/Seller/User
14
+ * `variant="overlay"` → `DrawerPanel`) — same edge/border flip, no
15
+ * translate state since the panel is conditionally mounted, not animated. */
16
+ export interface SidebarOverlayClasses {
17
+ edgeClass: string;
18
+ borderClass: string;
19
+ }
20
+ export declare function getSidebarOverlayClasses(hand: HandMode): SidebarOverlayClasses;
@@ -0,0 +1,20 @@
1
+ export function getSidebarRailClasses(hand) {
2
+ if (hand === "left") {
3
+ return {
4
+ edgeClass: "left-0",
5
+ borderClass: "border-r",
6
+ collapsedTranslateClass: "-translate-x-[calc(100%-1.25rem)]",
7
+ };
8
+ }
9
+ return {
10
+ edgeClass: "right-0",
11
+ borderClass: "border-l",
12
+ collapsedTranslateClass: "translate-x-[calc(100%-1.25rem)]",
13
+ };
14
+ }
15
+ export function getSidebarOverlayClasses(hand) {
16
+ if (hand === "left") {
17
+ return { edgeClass: "left-0", borderClass: "border-r" };
18
+ }
19
+ return { edgeClass: "right-0", borderClass: "border-l" };
20
+ }
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ export type HandMode = "left" | "right";
3
+ export interface HandModeContextValue {
4
+ /** Which side CTA panels/drawers/sidebars dock to and where close buttons sit. */
5
+ hand: HandMode;
6
+ /** Persist a new hand-mode preference and re-render the flipped layout. */
7
+ setHand: (next: HandMode) => void;
8
+ /** Flip preference between right ↔ left. */
9
+ toggleHand: () => void;
10
+ }
11
+ export interface HandModeProviderProps {
12
+ /**
13
+ * Seed value from the signed-in user's server-persisted preference
14
+ * (`user.uiPreferences.handMode`), used only when no value is already
15
+ * cached in localStorage on this device.
16
+ */
17
+ initialHand?: HandMode;
18
+ children: ReactNode;
19
+ }
20
+ export declare function HandModeProvider({ initialHand, children }: HandModeProviderProps): import("react").JSX.Element;
21
+ export declare function useHandMode(): HandModeContextValue;
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { normalizeError } from "../../../errors/normalize";
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
5
+ // Brand-neutral localStorage namespace, matching the `appkit:theme-mode` /
6
+ // `appkit:theme-id` convention. audit-ssr-in-appkit blocks hardcoded brand
7
+ // strings inside `_internal/`.
8
+ const HAND_STORAGE_KEY = "appkit:hand-mode";
9
+ // Default (non-null) context value — unlike `ThemeContext` (which throws
10
+ // outside its provider because it's only ever consumed at the app-shell
11
+ // level), `useHandMode()` is called from base UI primitives (SideDrawer,
12
+ // Modal, Drawer, BackToTop, ...) that ship as part of the public design
13
+ // system and must degrade gracefully to the "right" default for any
14
+ // consumer that hasn't mounted `<HandModeProvider>`.
15
+ const DEFAULT_CONTEXT_VALUE = {
16
+ hand: "right",
17
+ setHand: () => { },
18
+ toggleHand: () => { },
19
+ };
20
+ const HandModeContext = createContext(DEFAULT_CONTEXT_VALUE);
21
+ function readHandPreference() {
22
+ if (typeof window === "undefined")
23
+ return "right";
24
+ try {
25
+ const stored = window.localStorage.getItem(HAND_STORAGE_KEY);
26
+ if (stored === "left" || stored === "right")
27
+ return stored;
28
+ }
29
+ catch (_err) {
30
+ void normalizeError(_err); /* localStorage unavailable in privacy/iframe context */
31
+ }
32
+ return "right";
33
+ }
34
+ /** Apply the hand-mode preference to `<html>` via a `data-hand` attribute.
35
+ * Deliberately NOT `dir="rtl"/"ltr"` — that would trigger the browser's
36
+ * bidi text-direction and native-control-mirroring behavior repo-wide, which
37
+ * is not what this "reachability" preference means. `data-hand` is a plain
38
+ * presentational attribute, same category as `data-theme`. */
39
+ function applyHand(hand) {
40
+ if (typeof document === "undefined")
41
+ return;
42
+ document.documentElement.setAttribute("data-hand", hand);
43
+ }
44
+ export function HandModeProvider({ initialHand, children }) {
45
+ const [hand, setHandState] = useState(() => {
46
+ if (typeof window === "undefined")
47
+ return initialHand ?? "right";
48
+ const stored = readHandPreference();
49
+ // readHandPreference() falls back to "right" when nothing is cached —
50
+ // in that case, prefer the server-seeded value over the hardcoded default.
51
+ try {
52
+ if (window.localStorage.getItem(HAND_STORAGE_KEY) === null) {
53
+ return initialHand ?? stored;
54
+ }
55
+ }
56
+ catch (_err) {
57
+ void normalizeError(_err);
58
+ }
59
+ return stored;
60
+ });
61
+ useEffect(() => {
62
+ applyHand(hand);
63
+ }, [hand]);
64
+ const setHand = useCallback((next) => {
65
+ setHandState(next);
66
+ try {
67
+ window.localStorage.setItem(HAND_STORAGE_KEY, next);
68
+ }
69
+ catch (_err) {
70
+ void normalizeError(_err); /* localStorage unavailable; preference applies for this session */
71
+ }
72
+ }, []);
73
+ const toggleHand = useCallback(() => {
74
+ setHand(hand === "left" ? "right" : "left");
75
+ }, [hand, setHand]);
76
+ const value = useMemo(() => ({ hand, setHand, toggleHand }), [hand, setHand, toggleHand]);
77
+ return _jsx(HandModeContext.Provider, { value: value, children: children });
78
+ }
79
+ export function useHandMode() {
80
+ return useContext(HandModeContext);
81
+ }
@@ -0,0 +1,2 @@
1
+ export { HandModeProvider, useHandMode } from "./HandModeProvider";
2
+ export type { HandMode, HandModeContextValue, HandModeProviderProps, } from "./HandModeProvider";
@@ -0,0 +1 @@
1
+ export { HandModeProvider, useHandMode } from "./HandModeProvider";
@@ -1,6 +1,6 @@
1
1
  "use server";
2
2
  import { wrapAction } from "@mohasinac/appkit/server";
3
- import { bidRepository, productRepository, userRepository } from "../../../../repositories";
3
+ import { bidRepository, productRepository, userRepository, siteSettingsRepository } from "../../../../repositories";
4
4
  import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
5
  import { placeBidSchema } from "../../../shared/features/auctions/schema";
6
6
  import { assertAuctionActive, assertBidAmount, assertNotAuctionOwner, shouldAutoExtend, computeExtendedEndDate, } from "./service";
@@ -14,7 +14,8 @@ export async function placeBidAction(input) {
14
14
  const { auctionId, amount } = parsed.data;
15
15
  const product = await assertAuctionActive(auctionId);
16
16
  assertNotAuctionOwner(product, user.uid);
17
- assertBidAmount(product, amount);
17
+ const settings = await siteSettingsRepository.getSingleton();
18
+ assertBidAmount(product, amount, settings.auctionConfig?.bidIncrementTiers ?? []);
18
19
  const profile = await userRepository.findById(user.uid).catch(() => null);
19
20
  const userName = profile?.displayName ?? user.name ?? "Anonymous";
20
21
  // Mark all current bids for this auction as outbid, then set new winning bid
@@ -1,10 +1,11 @@
1
+ import { type BidIncrementTier } from "../../../shared/features/auctions/config";
1
2
  import type { ProductDocument } from "../../../shared/features/products/types";
2
3
  /** Assert an auction exists, is published, and is still active. Returns the product. */
3
4
  export declare function assertAuctionActive(auctionId: string): Promise<ProductDocument>;
4
- /** Compute the minimum valid bid given the current state. */
5
- export declare function computeMinBid(product: ProductDocument): number;
5
+ /** Compute the minimum valid bid given the current state and the admin's tier table. */
6
+ export declare function computeMinBid(product: ProductDocument, tiers: BidIncrementTier[]): number;
6
7
  /** Assert a bid amount is valid against the current auction state. */
7
- export declare function assertBidAmount(product: ProductDocument, amount: number): void;
8
+ export declare function assertBidAmount(product: ProductDocument, amount: number, tiers: BidIncrementTier[]): void;
8
9
  /** Assert the bidder is not the auction owner. */
9
10
  export declare function assertNotAuctionOwner(product: ProductDocument, bidderId: string): void;
10
11
  /** Return true if the bid was placed within the sniping window and the auction should auto-extend. */
@@ -1,7 +1,7 @@
1
1
  import { productRepository } from "../../../../repositories";
2
2
  import { isAuctionListing } from "../../../../features/products/utils/listing-type";
3
3
  import { AuctionNotFoundError, AuctionEndedError, BidTooLowError, BidOnOwnAuctionError, } from "../../../shared/features/auctions/errors";
4
- import { AUCTION_MIN_BID_INCREMENT, AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, } from "../../../shared/features/auctions/config";
4
+ import { AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, resolveMinBidIncrement, } from "../../../shared/features/auctions/config";
5
5
  /** Assert an auction exists, is published, and is still active. Returns the product. */
6
6
  export async function assertAuctionActive(auctionId) {
7
7
  const product = await productRepository.findByIdOrSlug(auctionId).catch(() => null);
@@ -16,15 +16,15 @@ export async function assertAuctionActive(auctionId) {
16
16
  throw new AuctionEndedError(auctionId);
17
17
  return product;
18
18
  }
19
- /** Compute the minimum valid bid given the current state. */
20
- export function computeMinBid(product) {
19
+ /** Compute the minimum valid bid given the current state and the admin's tier table. */
20
+ export function computeMinBid(product, tiers) {
21
21
  const current = product.currentBid ?? product.startingBid ?? 0;
22
- const increment = product.minBidIncrement ?? AUCTION_MIN_BID_INCREMENT;
22
+ const increment = resolveMinBidIncrement(current, tiers, product.minBidIncrement);
23
23
  return current + increment;
24
24
  }
25
25
  /** Assert a bid amount is valid against the current auction state. */
26
- export function assertBidAmount(product, amount) {
27
- const minBid = computeMinBid(product);
26
+ export function assertBidAmount(product, amount, tiers) {
27
+ const minBid = computeMinBid(product, tiers);
28
28
  if (amount < minBid)
29
29
  throw new BidTooLowError(minBid);
30
30
  }
@@ -45,3 +45,12 @@ export declare function resolveBundleOriginalTotal(productIds: string[]): Promis
45
45
  * `limit` (default 8) and filtered to `isActive: true` server-side.
46
46
  */
47
47
  export declare const listFeaturedBundles: (limit?: number, _opts?: BundleDataOptions) => Promise<CategoryDocument[]>;
48
+ /**
49
+ * "Related Bundles" for the bundle detail page — every other active bundle,
50
+ * excluding the current one. Bundles are a CategoryDocument, not a
51
+ * ProductDocument, so this mirrors BrandDetailPageView's "Related Brands"
52
+ * pattern (all active siblings of the same categoryType) rather than the
53
+ * product-based 4-signal computeRelatedItems() used elsewhere — bundle
54
+ * detail pages had no related section at all before this.
55
+ */
56
+ export declare const getRelatedBundles: (bundle: Pick<CategoryDocument, "id">, limit?: number) => Promise<CategoryDocument[]>;
@@ -79,3 +79,17 @@ export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
79
79
  .catch(() => []);
80
80
  return all;
81
81
  });
82
+ /**
83
+ * "Related Bundles" for the bundle detail page — every other active bundle,
84
+ * excluding the current one. Bundles are a CategoryDocument, not a
85
+ * ProductDocument, so this mirrors BrandDetailPageView's "Related Brands"
86
+ * pattern (all active siblings of the same categoryType) rather than the
87
+ * product-based 4-signal computeRelatedItems() used elsewhere — bundle
88
+ * detail pages had no related section at all before this.
89
+ */
90
+ export const getRelatedBundles = cache(async (bundle, limit = 8) => {
91
+ const all = await categoriesRepository
92
+ .listByType("bundle", { activeOnly: true, limit: limit + 1 })
93
+ .catch(() => []);
94
+ return all.filter((b) => b.id !== bundle.id).slice(0, limit);
95
+ });
@@ -1,4 +1,4 @@
1
- export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
1
+ export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
2
2
  export { buildBundleMetadata, type BundleMetadataOptions } from "./metadata";
3
3
  export { renderBundleOg, renderBundleOgImage, type BundleOgData, } from "./og";
4
4
  export { addBundleToCartAction } from "./actions";
@@ -1,4 +1,4 @@
1
- export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
1
+ export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
2
2
  export { buildBundleMetadata } from "./metadata";
3
3
  // S-SBUNI-4 2026-05-13 — bundle OG renderer.
4
4
  export { renderBundleOg, renderBundleOgImage, } from "./og";
@@ -0,0 +1,20 @@
1
+ import type { ReactElement } from "react";
2
+ export interface CatalogueItemOgData {
3
+ title: string;
4
+ ownerName?: string | null;
5
+ priceLabel?: string | null;
6
+ imageUrl?: string | null;
7
+ }
8
+ interface CatalogueItemDocLike {
9
+ title?: string | null;
10
+ price?: number | null;
11
+ mainImage?: string | null;
12
+ images?: (string | null | undefined)[] | null;
13
+ }
14
+ export declare function renderCatalogueItemOg(doc: CatalogueItemDocLike | null | undefined, opts: {
15
+ siteName: string;
16
+ baseUrl?: string;
17
+ ownerName?: string | null;
18
+ }): ReactElement;
19
+ export declare function renderCatalogueItemOgImage(data: CatalogueItemOgData, siteName: string): ReactElement;
20
+ export {};
@@ -0,0 +1,27 @@
1
+ import { renderOgLayout } from "../seo/og-layout";
2
+ import { resolveOgImageUrl } from "../seo/og";
3
+ export function renderCatalogueItemOg(doc, opts) {
4
+ const priceLabel = doc?.price != null && doc.price > 0
5
+ ? new Intl.NumberFormat("en-IN", {
6
+ style: "currency",
7
+ currency: "INR",
8
+ maximumFractionDigits: 0,
9
+ }).format(doc.price)
10
+ : null;
11
+ return renderCatalogueItemOgImage({
12
+ title: doc?.title ?? "Catalogue Item",
13
+ ownerName: opts.ownerName,
14
+ priceLabel,
15
+ imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
16
+ }, opts.siteName);
17
+ }
18
+ export function renderCatalogueItemOgImage(data, siteName) {
19
+ return renderOgLayout({
20
+ title: data.title,
21
+ subtitle: data.ownerName ? `Owned by ${data.ownerName}` : undefined,
22
+ imageUrl: data.imageUrl,
23
+ siteName: `${siteName} · Catalogue`,
24
+ accentSlot: data.priceLabel,
25
+ theme: { accentColor: "#a78bfa" },
26
+ });
27
+ }
@@ -32,6 +32,18 @@ export interface CreateCheckoutOrderInput {
32
32
  /** Buyer opted into shipment protection at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.shipmentProtectionFeeEnabled is also true. */
33
33
  shipmentProtectionAddon?: boolean;
34
34
  }
35
+ /**
36
+ * Exported so `/api/payment/create-order` (pre-charge amount) and the
37
+ * read-only `previewCheckoutPricing` (Order Summary display) can compute the
38
+ * exact same per-seller shipping fee that order creation charges/records —
39
+ * a single source of truth instead of a third re-implementation.
40
+ */
41
+ export declare function resolveShippingCost(storeId: string | undefined): Promise<{
42
+ shippingFee: number;
43
+ storeOwnerId: string | undefined;
44
+ storeEmiEnabled: boolean;
45
+ storeState: string | undefined;
46
+ }>;
35
47
  /**
36
48
  * Place order(s) from the user's cart in a single Firestore transaction.
37
49
  *
@@ -39,6 +51,37 @@ export interface CreateCheckoutOrderInput {
39
51
  * auth + rate-limiting before calling this and supply the resolved user fields.
40
52
  */
41
53
  export declare function createCheckoutOrderAction(input: CreateCheckoutOrderInput): Promise<CheckoutOrderResult>;
54
+ export interface CheckoutPricingPreviewInput {
55
+ userId: string;
56
+ /** Omitted for digital-only carts — GST can't be computed without a buyer state. */
57
+ addressId?: string;
58
+ paymentMethod: CheckoutPaymentMethod;
59
+ excludedProductIds?: string[];
60
+ whatsappNotifyAddon?: boolean;
61
+ giftWrapAddon?: boolean;
62
+ shipmentProtectionAddon?: boolean;
63
+ }
64
+ export interface CheckoutPricingPreview {
65
+ subtotal: number;
66
+ shippingFee: number;
67
+ codHandlingFee: number;
68
+ whatsappNotifyFee: number;
69
+ giftWrapFee: number;
70
+ shipmentProtectionFee: number;
71
+ gstAmount: number;
72
+ couponDiscount: number;
73
+ total: number;
74
+ }
75
+ /**
76
+ * Read-only "what will I actually be charged" preview for the checkout
77
+ * Order Summary. Reuses the exact same building blocks
78
+ * createCheckoutOrderAction / verifyAndPlaceRazorpayOrderAction use when an
79
+ * order is actually placed — resolveShippingCost, the compute*Fee helpers,
80
+ * computeGroupCouponDiscount, and the GST calc — so the number shown to the
81
+ * buyer can never diverge from what gets charged/recorded. Never decrements
82
+ * stock, creates an order, or writes coupon usage.
83
+ */
84
+ export declare function previewCheckoutPricing(input: CheckoutPricingPreviewInput): Promise<CheckoutPricingPreview>;
42
85
  /**
43
86
  * Mark an order as paid after a payment provider verifies the transaction.
44
87
  * Consumers call this from /api/payment/verify or a payment webhook.