@mohasinac/appkit 4.9.3 → 4.9.5

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 (67) 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/checkout/actions.d.ts +43 -0
  11. package/dist/_internal/server/features/checkout/actions.js +172 -81
  12. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  13. package/dist/_internal/server/features/checkout/index.js +1 -1
  14. package/dist/_internal/server/features/seo/robots.js +3 -2
  15. package/dist/_internal/server/features/seo/sitemap.js +70 -5
  16. package/dist/_internal/shared/config/schema.d.ts +1 -0
  17. package/dist/_internal/shared/listing-types/classified/config.js +1 -1
  18. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  19. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  20. package/dist/client.d.ts +4 -0
  21. package/dist/client.js +9 -0
  22. package/dist/constants/api-endpoints.d.ts +3 -0
  23. package/dist/constants/api-endpoints.js +1 -0
  24. package/dist/features/about/components/AboutView.js +2 -2
  25. package/dist/features/about/components/DeveloperView.js +2 -2
  26. package/dist/features/about/schemas/firestore.d.ts +1 -0
  27. package/dist/features/account/components/UserSidebar.js +8 -2
  28. package/dist/features/account/hooks/useProfile.d.ts +2 -0
  29. package/dist/features/admin/components/AdminSidebar.js +8 -2
  30. package/dist/features/auth/schemas/firestore.d.ts +3 -0
  31. package/dist/features/cart/components/CartDrawer.js +4 -1
  32. package/dist/features/homepage/components/BrandsSection.js +3 -1
  33. package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
  34. package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
  35. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  36. package/dist/features/homepage/components/SectionCarousel.js +5 -2
  37. package/dist/features/homepage/components/SocialFeedSection.js +2 -2
  38. package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
  39. package/dist/features/layout/BackToTop.js +3 -1
  40. package/dist/features/layout/SidebarLayout.js +7 -1
  41. package/dist/features/seller/components/SellerSidebar.js +8 -2
  42. package/dist/features/shell/QuickFormDrawer.js +3 -1
  43. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +31 -0
  44. package/dist/hand-mode/index.d.ts +12 -0
  45. package/dist/hand-mode/index.js +11 -0
  46. package/dist/providers/db-firebase/base.js +17 -1
  47. package/dist/react/contexts/SessionContext.d.ts +5 -0
  48. package/dist/react/contexts/SessionContext.js +2 -0
  49. package/dist/seed/site-settings-seed-data.js +3 -2
  50. package/dist/server-entry.d.ts +1 -1
  51. package/dist/server-entry.js +1 -1
  52. package/dist/styles.css +2 -1
  53. package/dist/tailwind-utilities.css +1 -1
  54. package/dist/ui/components/Drawer.js +7 -4
  55. package/dist/ui/components/FilterDrawer.js +1 -1
  56. package/dist/ui/components/HandModeRow.d.ts +12 -0
  57. package/dist/ui/components/HandModeRow.js +18 -0
  58. package/dist/ui/components/Layout.d.ts +5 -1
  59. package/dist/ui/components/Layout.js +2 -1
  60. package/dist/ui/components/Layout.style.css +1 -0
  61. package/dist/ui/components/ListingFilterDrawer.js +3 -1
  62. package/dist/ui/components/Modal.js +3 -1
  63. package/dist/ui/components/SideDrawer.js +8 -5
  64. package/dist/ui/components/SideModal.js +7 -4
  65. package/dist/ui/index.d.ts +1 -0
  66. package/dist/ui/index.js +1 -0
  67. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { MediaImage } from "../../media/MediaImage";
7
7
  import { MediaVideo } from "../../media/MediaVideo";
8
8
  import { useHeroCarousel } from "../hooks/useHeroCarousel";
9
9
  import { HERO_CAROUSEL_ARROW, HERO_CAROUSEL_DOT_ACTIVE, HERO_CAROUSEL_DOT_INACTIVE, POSITION_FILL, FLEX_CENTER, HERO_PLACEHOLDER_BG } from "../constants/hero-carousel-styles";
10
+ import { useHandMode } from "../../../_internal/client/hand-mode";
10
11
  function makeButtonClickHandler(href, openInNewTab, push) {
11
12
  return () => {
12
13
  if (openInNewTab) {
@@ -124,6 +125,7 @@ function CarouselCardRenderer({ card, isMobile, push, }) {
124
125
  }) }))] })] })), card.isButtonOnly && card.buttons?.[0] && (_jsx(Button, { variant: "ghost", rounded: "none", className: `${POSITION_FILL} ${FLEX_CENTER} font-[600] text-white hover:bg-[rgba(0,0,0,0.2)] transition-colors p-[0]`, onClick: makeButtonClickHandler(card.buttons[0].href, card.buttons[0].openInNewTab, push), children: _jsx(Span, { size: "lg", mdSize: "2xl", children: card.buttons[0].text }) }))] }));
125
126
  }
126
127
  export function HeroCarousel({ initialSlides, push } = {}) {
128
+ const { hand } = useHandMode();
127
129
  const [currentSlide, setCurrentSlide] = useState(0);
128
130
  const [isPaused, setIsPaused] = useState(false);
129
131
  const sectionRef = useRef(null);
@@ -193,5 +195,5 @@ export function HeroCarousel({ initialSlides, push } = {}) {
193
195
  }, children: slide.cards.slice(0, 6).map((card) => (_jsx(CarouselCardRenderer, { card: card, isMobile: isMobile, push: push }, card.id))) }))] }, slide.id));
194
196
  }) }) }), slides.length > 1 && (_jsx(Row, { gap: "sm", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10", children: slides.map((_, index) => (_jsx(Button, { variant: "ghost", className: `relative overflow-hidden rounded-full transition-all duration-500 p-[var(--appkit-space-0)] !min-h-0 ${index === currentSlide
195
197
  ? HERO_CAROUSEL_DOT_ACTIVE
196
- : `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), slides.length > 1 && (_jsxs(Row, { gap: "sm", className: "absolute bottom-4 right-4 z-20", children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
198
+ : `${HERO_CAROUSEL_DOT_INACTIVE} hover:bg-white/75`}`, onClick: () => goToSlide(index), "aria-label": `Go to slide ${index + 1}`, children: index === currentSlide && (_jsx(Span, { className: "absolute inset-y-0 left-0 bg-[rgba(0,0,0,0.2)] animate-[progress-fill_4s_linear_forwards]", rounded: "full", "aria-hidden": "true" })) }, index))) })), slides.length > 1 && (_jsxs(Row, { gap: "sm", className: `absolute bottom-4 ${hand === "left" ? "left-4" : "right-4"} z-20`, children: [_jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goPrev, "aria-label": "Previous slide", children: _jsx("svg", { className: "w-7 h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }), _jsx(Button, { variant: "ghost", className: `p-[0] ${HERO_CAROUSEL_ARROW}`, onClick: goNext, "aria-label": "Next slide", children: _jsx("svg", { className: "w-7 h-7", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M9 5l7 7-7 7" }) }) })] }))] }));
197
199
  }
@@ -1,9 +1,11 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { THEMED_BG_SECONDARY, THEMED_TEXT_PRIMARY, THEMED_TEXT_SECONDARY } from "../../../_internal/shared/styles/themed";
3
4
  import { SKELETON as skeleton } from "../../../_internal/shared/styles/skeleton";
4
5
  import { Div, Heading, HorizontalScroller, Row, Section, Span, Stack, Text, TextLink } from "../../../ui";
5
6
  import { MediaImage } from "../../media/MediaImage";
6
7
  import { CAROUSEL_PER_VIEW } from "../constants/carousel-per-view";
8
+ import { useHandMode } from "../../../_internal/client/hand-mode";
7
9
  const __O = {
8
10
  hidden: "overflow-hidden",
9
11
  };
@@ -13,6 +15,7 @@ function CarouselSkeleton({ count }) {
13
15
  }
14
16
  // --- Component ---------------------------------------------------------------
15
17
  export function SectionCarousel({ title, description, headingVariant = "editorial", pillLabel, backgroundImage, viewMoreHref, viewMoreLabel = "View all →", items, renderItem, perView = CAROUSEL_PER_VIEW.standard, gap = 16, autoScroll = false, autoScrollInterval = 3500, loop = true, keyExtractor, rows = 1, className = "", isLoading = false, skeletonCount = 4, lightText, showPeek: _showPeek = false, minItemWidth = 220, }) {
18
+ const { hand } = useHandMode();
16
19
  if (!isLoading && items.length === 0)
17
20
  return null;
18
21
  const hasBg = Boolean(backgroundImage);
@@ -35,7 +38,7 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
35
38
  className,
36
39
  ]
37
40
  .filter(Boolean)
38
- .join(" "), children: [hasBg && backgroundImage && (_jsxs(_Fragment, { children: [_jsx(Div, { className: `absolute inset-0 ${__O.hidden}`, children: _jsx(MediaImage, { src: backgroundImage, alt: "", size: "hero" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 w-full max-w-7xl mx-auto", children: [_jsxs(Div, { className: "text-left mb-6", children: [headingVariant === "editorial" && pillLabel && (_jsx(Div, { className: "mb-4", children: _jsxs(Span, { className: pillClass, children: [_jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" }), pillLabel, _jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" })] }) })), _jsx(Heading, { level: 2, variant: "none", gradient: headingGradient, className: [
41
+ .join(" "), children: [hasBg && backgroundImage && (_jsxs(_Fragment, { children: [_jsx(Div, { className: `absolute inset-0 ${__O.hidden}`, children: _jsx(MediaImage, { src: backgroundImage, alt: "", size: "hero" }) }), _jsx(Div, { surface: "overlay-md", className: "absolute inset-0" })] })), _jsxs(Div, { className: "relative z-10 w-full max-w-7xl mx-auto", children: [_jsxs(Div, { className: hand === "left" ? "text-right mb-6" : "text-left mb-6", children: [headingVariant === "editorial" && pillLabel && (_jsx(Div, { className: "mb-4", children: _jsxs(Span, { className: pillClass, children: [_jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" }), pillLabel, _jsx(Span, { className: "w-1.5 h-1.5 bg-primary-500 inline-block", rounded: "full", "aria-hidden": "true" })] }) })), _jsx(Heading, { level: 2, variant: "none", gradient: headingGradient, className: [
39
42
  "text-[length:var(--appkit-text-3xl)] md:text-[length:var(--appkit-text-4xl)] font-bold mb-2",
40
43
  headingVariant === "editorial" ? "font-display text-[length:var(--appkit-text-4xl)]" : "",
41
44
  headingClass,
@@ -45,5 +48,5 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
45
48
  // Double-row grids (e.g. the homepage Auctions section's
46
49
  // rows:2 config) get the full-height sliver arrows — much
47
50
  // easier to hit than a small circle when the track is tall.
48
- arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
51
+ arrowStyle: rows > 1 ? "full-height" : "circle", snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: hand === "left" ? "end" : "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, shadow: useLightText ? "lg" : "none", className: "inline-flex items-[center] gap-[0.375rem] transition-colors bg-primary text-white hover:bg-primary-600", size: "sm", weight: "semibold", children: viewMoreLabel }) }))] })] }));
49
52
  }
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { normalizeError } from "../../../errors/normalize";
3
- import { Anchor, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
3
+ import { Anchor, Div, Heading, HandModeRow, Section, Stack, Text } from "../../../ui";
4
4
  import { SocialPostCard } from "./SocialPostCard";
5
5
  import { fetchInstagramPosts, fetchFacebookPosts, fetchTikTokPosts, fetchDeviantArtPosts, } from "../lib/social-feed-fetcher";
6
6
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
@@ -99,5 +99,5 @@ export async function SocialFeedSection(config) {
99
99
  : layout === "masonry"
100
100
  ? "break-inside-avoid"
101
101
  : "";
102
- return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", padding: "x-md", children: [_jsxs(Row, { className: "mb-8", align: "end", justify: "between", gap: "md", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "mb-1", children: title || `${PLATFORM_LABELS[platform]} Feed` }), subtitle && (_jsx(Text, { size: "sm", variant: "muted", children: subtitle }))] }), profileUrl && (_jsxs(Anchor, { href: profileUrl, tone: "none", underline: "none", size: "sm", weight: "medium", className: "shrink-0 text-[var(--appkit-color-primary)] hover:opacity-80", children: ["Follow on ", PLATFORM_LABELS[platform], " \u2192"] }))] }), error ? (_jsx(Div, { textSize: "sm", className: "text-left", color: "faint", padding: "y-3xl", children: error })) : posts.length === 0 ? (_jsx(SocialFeedEmpty, { platform: platform })) : (_jsx(Div, { className: gridClass, children: posts.slice(0, count).map((post) => (_jsx(Div, { className: cardClass, children: _jsx(SocialPostCard, { post: post, showCaption: showCaption, showStats: showStats }) }, post.id))) }))] }) }));
102
+ return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: "w-full max-w-7xl mx-auto", padding: "x-md", children: [_jsxs(HandModeRow, { className: "mb-8", align: "end", justify: "between", gap: "md", children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "mb-1", children: title || `${PLATFORM_LABELS[platform]} Feed` }), subtitle && (_jsx(Text, { size: "sm", variant: "muted", children: subtitle }))] }), profileUrl && (_jsxs(Anchor, { href: profileUrl, tone: "none", underline: "none", size: "sm", weight: "medium", className: "shrink-0 text-[var(--appkit-color-primary)] hover:opacity-80", children: ["Follow on ", PLATFORM_LABELS[platform], " \u2192"] }))] }), error ? (_jsx(Div, { textSize: "sm", className: "text-left", color: "faint", padding: "y-3xl", children: error })) : posts.length === 0 ? (_jsx(SocialFeedEmpty, { platform: platform })) : (_jsx(Div, { className: gridClass, children: posts.slice(0, count).map((post) => (_jsx(Div, { className: cardClass, children: _jsx(SocialPostCard, { post: post, showCaption: showCaption, showStats: showStats }) }, post.id))) }))] }) }));
103
103
  }
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Blockquote, Button, Div, Heading, RichText, Row, Section, Span, Text } from "../../../ui";
4
4
  import { Users } from "lucide-react";
5
5
  import { getDefaultLocale } from "../../../core/baseline-resolver";
6
+ import { useHandMode } from "../../../_internal/client/hand-mode";
6
7
  // --- Section -----------------------------------------------------------------
7
8
  export function WhatsAppCommunitySection({ title, descriptionHtml, memberCount, memberCountLabel = "members", testimonial, benefits = [], buttonText = "Join our WhatsApp community", groupLink, isLoading = false, className = "", }) {
9
+ const { hand } = useHandMode();
8
10
  if (isLoading) {
9
11
  return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsx(Div, { className: "w-full max-w-7xl mx-auto", children: _jsx(Div, { className: "h-72 animate-pulse", surface: "subtle", rounded: "2xl" }) }) }));
10
12
  }
@@ -14,5 +16,5 @@ export function WhatsAppCommunitySection({ title, descriptionHtml, memberCount,
14
16
  if (groupLink)
15
17
  window.open(groupLink, "_blank");
16
18
  };
17
- return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsx(Div, { className: "w-full max-w-7xl mx-auto", children: _jsxs(Section, { color: "inverse", tone: "accent-banner", rounded: "2xl", shadow: "lg", className: "relative overflow-hidden", children: [_jsx(Div, { className: "absolute inset-0 opacity-[0.06] pointer-events-none bg-[repeating-linear-gradient(45deg,white_0,white_1px,transparent_0,transparent_50%)] bg-[size:10px_10px]", "aria-hidden": true }), _jsxs(Div, { className: "relative z-10 px-[1.5rem] sm:px-[2.5rem] sm:py-[3rem]", padding: "y-2xl", children: [_jsxs(Row, { align: "center", justify: "between", gap: "md", wrap: true, className: `mb-8`, children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "w-12 h-12 bg-[var(--appkit-color-whatsapp)] flex-shrink-0", align: "center", justify: "center", rounded: "xl", shadow: "lg", children: _jsx("svg", { className: "w-7 h-7 text-white", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }) }), _jsx(Span, { color: "inverse", size: "sm", weight: "medium", className: "/80 tracking-wide", transform: "uppercase", children: "WhatsApp Community" })] }), memberCount && (_jsxs(Row, { paddingY: "y-2xs-tall", className: "backdrop-blur-sm", surface: "default", padding: "x-md", align: "center", gap: "sm", rounded: "full", children: [_jsx(Users, { className: "w-4 h-4 text-white/80" }), _jsxs(Span, { color: "inverse", size: "sm", weight: "semibold", children: [memberCount.toLocaleString(getDefaultLocale()), "+ ", memberCountLabel] })] }))] }), _jsx(Heading, { color: "inverse", level: 2, className: "mb-3 leading-tight", smSize: "3xl", size: "2xl", weight: "bold", children: title }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none prose-invert", className: "text-white/80 text-[length:var(--appkit-text-base)] mb-6 max-w-2xl leading-relaxed" })), benefits.length > 0 && (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 mb-8 max-w-2xl", children: benefits.slice(0, 4).map((benefit, i) => (_jsxs(Row, { align: "start", gap: "2.5", children: [_jsx(Span, { layout: "flex-center", className: "mt-0.5 flex-shrink-0 w-5 h-5 bg-[rgba(255,255,255,0.2)]", rounded: "full", children: _jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }), _jsx(Span, { color: "inverse", size: "sm", className: "/90 leading-snug", children: benefit })] }, i))) })), testimonial && (_jsx(Blockquote, { paddingX: "x-md", className: "border-l-2 border-white/30 mb-8 max-w-xl", children: _jsxs(Text, { color: "inverse", variant: "none", className: "/75 italic leading-relaxed", size: "sm", children: ["\u201C", testimonial, "\u201D"] }) })), groupLink && (_jsxs(Button, { gap: "md", variant: "primary", size: "lg", onClick: handleJoin, "aria-label": buttonText, className: "!bg-[var(--appkit-color-whatsapp)] hover:!bg-[var(--appkit-color-whatsapp-light)] !text-white !border-0 font-[600]", shadow: "lg", children: [_jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }), buttonText] }))] })] }) }) }));
19
+ return (_jsx(Section, { paddingX: "x-md-2xl", className: className, surface: "subtle", paddingY: "y-4xl", children: _jsx(Div, { className: "w-full max-w-7xl mx-auto", children: _jsxs(Section, { color: "inverse", tone: "accent-banner", rounded: "2xl", shadow: "lg", className: "relative overflow-hidden", children: [_jsx(Div, { className: "absolute inset-0 opacity-[0.06] pointer-events-none bg-[repeating-linear-gradient(45deg,white_0,white_1px,transparent_0,transparent_50%)] bg-[size:10px_10px]", "aria-hidden": true }), _jsxs(Div, { className: "relative z-10 px-[1.5rem] sm:px-[2.5rem] sm:py-[3rem]", padding: "y-2xl", children: [_jsxs(Row, { align: "center", justify: "between", gap: "md", wrap: true, className: "mb-8", reverse: hand === "left", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "w-12 h-12 bg-[var(--appkit-color-whatsapp)] flex-shrink-0", align: "center", justify: "center", rounded: "xl", shadow: "lg", children: _jsx("svg", { className: "w-7 h-7 text-white", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }) }), _jsx(Span, { color: "inverse", size: "sm", weight: "medium", className: "/80 tracking-wide", transform: "uppercase", children: "WhatsApp Community" })] }), memberCount && (_jsxs(Row, { paddingY: "y-2xs-tall", className: "backdrop-blur-sm", surface: "default", padding: "x-md", align: "center", gap: "sm", rounded: "full", children: [_jsx(Users, { className: "w-4 h-4 text-white/80" }), _jsxs(Span, { color: "inverse", size: "sm", weight: "semibold", children: [memberCount.toLocaleString(getDefaultLocale()), "+ ", memberCountLabel] })] }))] }), _jsx(Heading, { color: "inverse", level: 2, className: "mb-3 leading-tight", smSize: "3xl", size: "2xl", weight: "bold", children: title }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none prose-invert", className: "text-white/80 text-[length:var(--appkit-text-base)] mb-6 max-w-2xl leading-relaxed" })), benefits.length > 0 && (_jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-1 sm:grid-cols-2 mb-8 max-w-2xl", children: benefits.slice(0, 4).map((benefit, i) => (_jsxs(Row, { align: "start", gap: "2.5", children: [_jsx(Span, { layout: "flex-center", className: "mt-0.5 flex-shrink-0 w-5 h-5 bg-[rgba(255,255,255,0.2)]", rounded: "full", children: _jsx("svg", { className: "w-3 h-3 text-white", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) }), _jsx(Span, { color: "inverse", size: "sm", className: "/90 leading-snug", children: benefit })] }, i))) })), testimonial && (_jsx(Blockquote, { paddingX: "x-md", className: "border-l-2 border-white/30 mb-8 max-w-xl", children: _jsxs(Text, { color: "inverse", variant: "none", className: "/75 italic leading-relaxed", size: "sm", children: ["\u201C", testimonial, "\u201D"] }) })), groupLink && (_jsxs(Button, { gap: "md", variant: "primary", size: "lg", onClick: handleJoin, "aria-label": buttonText, className: "!bg-[var(--appkit-color-whatsapp)] hover:!bg-[var(--appkit-color-whatsapp-light)] !text-white !border-0 font-[600]", shadow: "lg", children: [_jsx("svg", { className: "w-5 h-5", fill: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" }) }), buttonText] }))] })] }) }) }));
18
20
  }
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
5
5
  import { useEffect, useState } from "react";
6
6
  import { ChevronUp, X } from "lucide-react";
7
7
  import { Button, Div } from "../../ui";
8
+ import { useHandMode } from "../../_internal/client/hand-mode";
8
9
  /**
9
10
  * BackToTop — floating scroll-to-top button.
10
11
  * Appears after the page has scrolled past `threshold` pixels. A small
@@ -20,6 +21,7 @@ import { Button, Div } from "../../ui";
20
21
  */
21
22
  export function BackToTop({ threshold = 400, ariaLabel = "Back to top", className = "", }) {
22
23
  const pathname = usePathname();
24
+ const { hand } = useHandMode();
23
25
  const [visible, setVisible] = useState(false);
24
26
  const [dismissed, setDismissed] = useState(false);
25
27
  useEffect(() => {
@@ -38,7 +40,7 @@ export function BackToTop({ threshold = 400, ariaLabel = "Back to top", classNam
38
40
  const handleDismiss = () => {
39
41
  setDismissed(true);
40
42
  };
41
- return (_jsxs(Div, { layout: "flex", align: "center", gap: "1", className: `fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem))] right-4 z-[var(--appkit-z-bottom-nav)] ${className}`, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": ariaLabel, onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }), className: "flex items-center justify-center w-10 h-10 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors", children: _jsx(ChevronUp, { className: "w-5 h-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": "Hide back-to-top button", onClick: handleDismiss, className: "flex items-center justify-center w-6 h-6 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text)] transition-colors", children: _jsx(X, { className: "w-3 h-3", "aria-hidden": "true" }) })] }));
43
+ return (_jsxs(Div, { layout: "flex", align: "center", gap: "1", className: `fixed bottom-[calc(var(--keyboard-inset-height,0px)+var(--bottom-nav-height,4rem)+var(--appkit-space-4))] ${hand === "left" ? "left-4" : "right-4"} z-[var(--appkit-z-bottom-nav)] ${className}`, children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": ariaLabel, onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }), className: "flex items-center justify-center w-10 h-10 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors", children: _jsx(ChevronUp, { className: "w-5 h-5", "aria-hidden": "true" }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", "aria-label": "Hide back-to-top button", onClick: handleDismiss, className: "flex items-center justify-center w-6 h-6 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] shadow-md text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text)] transition-colors", children: _jsx(X, { className: "w-3 h-3", "aria-hidden": "true" }) })] }));
42
44
  }
43
45
  /** SkipToMain — visually hidden link that becomes visible on focus for keyboard users. */
44
46
  export function SkipToMain({ mainId = "main-content" }) {
@@ -1,6 +1,8 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { forwardRef } from "react";
3
4
  import { Aside, Div, Section } from "../../ui";
5
+ import { useHandMode } from "../../_internal/client/hand-mode";
4
6
  const __O = {
5
7
  yAuto: "overflow-y-auto",
6
8
  };
@@ -17,6 +19,10 @@ const __O = {
17
19
  * to the rendered `aside` element.
18
20
  */
19
21
  export const SidebarLayout = forwardRef(function SidebarLayout({ isOpen, ariaLabel, onClose, header, children, id = "secondary-sidebar", background }, ref) {
20
- return (_jsxs(_Fragment, { children: [isOpen && (_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] z-[var(--appkit-z-overlay)] transition-opacity duration-300", onClick: onClose, "aria-hidden": "true" })), _jsx(Aside, { border: "default", ref: ref, id: id, "aria-label": ariaLabel, className: `fixed inset-y-0 right-0 w-80 bg-[var(--appkit-color-surface)] border-l border-[var(--appkit-color-border-subtle)] shadow-2xl transform duration-300 ease-in-out z-50 ${isOpen ? "translate-x-0" : "translate-x-full"} flex flex-col`, children: _jsxs(Section, { tone: "plain", background: background, className: "flex flex-col flex-1 min-h-0", children: [_jsx(Div, { border: "default", className: "flex-shrink-0 py-[var(--appkit-space-5)] border-b border-[var(--appkit-color-border-subtle)]", padding: "x-lg", surface: "muted", children: header }), _jsx(Div, { className: `flex-1 ${__O.yAuto} scrollbar-thin`, padding: "inlineLg", children: children })] }) })] }));
22
+ const { hand } = useHandMode();
23
+ const edgeClass = hand === "left" ? "left-0" : "right-0";
24
+ const borderClass = hand === "left" ? "border-r" : "border-l";
25
+ const closedTransform = hand === "left" ? "-translate-x-full" : "translate-x-full";
26
+ return (_jsxs(_Fragment, { children: [isOpen && (_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] z-[var(--appkit-z-overlay)] transition-opacity duration-300", onClick: onClose, "aria-hidden": "true" })), _jsx(Aside, { border: "default", ref: ref, id: id, "aria-label": ariaLabel, className: `fixed inset-y-0 ${edgeClass} w-80 bg-[var(--appkit-color-surface)] ${borderClass} border-[var(--appkit-color-border-subtle)] shadow-2xl transform duration-300 ease-in-out z-50 ${isOpen ? "translate-x-0" : closedTransform} flex flex-col`, children: _jsxs(Section, { tone: "plain", background: background, className: "flex flex-col flex-1 min-h-0", children: [_jsx(Div, { border: "default", className: "flex-shrink-0 py-[var(--appkit-space-5)] border-b border-[var(--appkit-color-border-subtle)]", padding: "x-lg", surface: "muted", children: header }), _jsx(Div, { className: `flex-1 ${__O.yAuto} scrollbar-thin`, padding: "inlineLg", children: children })] }) })] }));
21
27
  });
22
28
  SidebarLayout.displayName = "SidebarLayout";
@@ -9,6 +9,8 @@ import { BottomSheet } from "../../layout/BottomSheet";
9
9
  import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
10
10
  import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
11
11
  import { findActiveNavGroup, findActiveNavItem } from "../../../_internal/client/features/layout/navActive";
12
+ import { getSidebarRailClasses, getSidebarOverlayClasses } from "../../../_internal/client/features/layout/sidebarPositionClasses";
13
+ import { useHandMode } from "../../../_internal/client/hand-mode";
12
14
  const __O = {
13
15
  hidden: "overflow-hidden",
14
16
  yAuto: "overflow-y-auto",
@@ -64,10 +66,14 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
64
66
  })] })] }));
65
67
  }
66
68
  function DrawerPanel({ title, onClose, children }) {
67
- return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: "fixed top-0 right-0 z-50 h-full w-64 border-l", surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", 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" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
69
+ const { hand } = useHandMode();
70
+ const { edgeClass, borderClass } = getSidebarOverlayClasses(hand);
71
+ return (_jsxs(Div, { className: "hidden lg:block", children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 z-40 backdrop-blur-sm", onClick: onClose, "aria-hidden": "true" }), _jsxs(Stack, { border: "default", shadow: "2xl", role: "dialog", "aria-modal": "true", "aria-label": title, className: `fixed top-0 ${edgeClass} z-50 h-full w-64 ${borderClass}`, surface: "default", children: [_jsxs(Row, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", align: "center", justify: "between", children: [_jsx(Span, { size: "xs", weight: "semibold", transform: "uppercase", color: "muted", children: title }), _jsx(IconButton, { "aria-label": "Close", variant: "ghost", size: "sm", onClick: onClose, icon: _jsx("svg", { className: "w-4 h-4", 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" }) }) })] }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: children })] })] }));
68
72
  }
69
73
  export function StoreSidebar({ items, groups, activeHref, storeName, storeLogoURL, mobileOpen = false, desktopOpen = false, variant = "overlay", onCloseMobile, onToggle, renderFooter, }) {
70
74
  const close = onCloseMobile ?? (() => { });
75
+ const { hand } = useHandMode();
76
+ const rail = getSidebarRailClasses(hand);
71
77
  const [mounted, setMounted] = useState(false);
72
78
  useEffect(() => { setMounted(true); }, []);
73
79
  useEffect(() => {
@@ -78,7 +84,7 @@ export function StoreSidebar({ items, groups, activeHref, storeName, storeLogoUR
78
84
  const navContent = groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close }));
79
85
  if (variant === "sidebar") {
80
86
  const handleToggle = onToggle ?? close;
81
- return (_jsxs(_Fragment, { children: [desktopOpen && (_jsx(Div, { surface: "overlay-xs", className: "hidden lg:block fixed inset-0 backdrop-blur-[2px] z-30", onClick: handleToggle, "aria-hidden": "true" })), _jsxs(Div, { className: `hidden lg:flex fixed left-0 z-40 transition-transform duration-300 top-[var(--header-height,3.5rem)] h-[calc(100vh-var(--header-height,3.5rem))] w-[18rem] ${desktopOpen ? "translate-x-0" : "-translate-x-[calc(100%-1.25rem)]"}`, children: [_jsxs(Stack, { border: "default", surface: "sidePanel", className: `flex-1 border-r ${__O.hidden}`, shadow: "xl", children: [_jsx(Div, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", children: _jsxs(Row, { className: "min-w-0", align: "center", gap: "3", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName ?? "", size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName?.[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName || panelTitle })] }) }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: navContent }), renderFooter && _jsx(Div, { border: "top", padding: "inline", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle })] }), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: panelTitle, children: groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) }) })] }));
87
+ return (_jsxs(_Fragment, { children: [desktopOpen && (_jsx(Div, { surface: "overlay-xs", className: "hidden lg:block fixed inset-0 backdrop-blur-[2px] z-30", onClick: handleToggle, "aria-hidden": "true" })), _jsxs(Div, { className: `hidden lg:flex fixed ${rail.edgeClass} z-40 transition-transform duration-300 top-[var(--header-height,3.5rem)] h-[calc(100vh-var(--header-height,3.5rem))] w-[18rem] ${desktopOpen ? "translate-x-0" : rail.collapsedTranslateClass}`, children: [_jsxs(Stack, { border: "default", surface: "sidePanel", className: `flex-1 ${rail.borderClass} ${__O.hidden}`, shadow: "xl", children: [_jsx(Div, { border: "bottom-subtle", paddingY: "y-sm-tall", className: "shrink-0", padding: "x-md", children: _jsxs(Row, { className: "min-w-0", align: "center", gap: "3", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName ?? "", size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName?.[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName || panelTitle })] }) }), _jsx(Div, { className: `flex-1 ${__O.yAuto}`, children: navContent }), renderFooter && _jsx(Div, { border: "top", padding: "inline", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle, edge: hand })] }), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: panelTitle, children: groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) }) })] }));
82
88
  }
83
89
  return (_jsxs(_Fragment, { children: [mounted && mobileOpen &&
84
90
  createPortal(_jsx(DrawerPanel, { title: panelTitle, onClose: close, children: navContent }), document.body), _jsx(Div, { className: "lg:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: panelTitle, children: groups ? (_jsx(GroupsContent, { groups: groups, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) : (_jsx(FlatContent, { items: items, activeHref: activeHref, storeName: storeName, storeLogoURL: storeLogoURL, onItemClick: close })) }) })] }));
@@ -7,6 +7,7 @@ import { IconButton } from "../../ui/components/IconButton";
7
7
  import { FormField } from "../../ui/components/FormField";
8
8
  import { Toggle } from "../../ui/components/Toggle";
9
9
  import { Div, Form, Row, Stack, Text } from "../../ui";
10
+ import { useHandMode } from "../../_internal/client/hand-mode";
10
11
  function initValues(fields, defaults = {}) {
11
12
  const out = {};
12
13
  for (const f of fields) {
@@ -19,6 +20,7 @@ function initValues(fields, defaults = {}) {
19
20
  }
20
21
  const FOCUSABLE = 'a[href],button:not([disabled]),textarea:not([disabled]),input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';
21
22
  export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues, onSubmit, submitLabel = "Save", cancelLabel = "Cancel", isLoading = false, schema, renderExtra, }) {
23
+ const { hand } = useHandMode();
22
24
  const drawerRef = useRef(null);
23
25
  const [values, setValues] = useState(() => initValues(fields, defaultValues));
24
26
  const [errors, setErrors] = useState({});
@@ -122,7 +124,7 @@ export function QuickFormDrawer({ isOpen, onClose, title, fields, defaultValues,
122
124
  if (!isOpen)
123
125
  return null;
124
126
  const busy = submitting || isLoading;
125
- return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] [z-index:calc(var(--appkit-z-modal)+1)]", "aria-hidden": "true", onClick: handleClose }), _jsxs(Div, { ref: drawerRef, role: "dialog", "aria-modal": "true", "aria-label": title, layout: "flex-col", shadow: "2xl", className: "fixed inset-y-0 right-0 bg-[var(--appkit-color-surface)] w-full lg:w-[40%] [z-index:calc(var(--appkit-z-modal)+2)]", children: [_jsxs(Row, { className: "flex-shrink-0 border-b border-[var(--appkit-color-border)]", padding: "md", align: "center", gap: "3", children: [_jsx(Text, { className: "flex-1 text-[var(--appkit-color-text)]", size: "base", weight: "semibold", children: title }), _jsx(IconButton, { type: "button", onClick: handleClose, "aria-label": "Close", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs(Form, { className: "flex-1", overflow: "y-auto", paddingX: "x-md", paddingY: "y-md-lg", spacing: "md", onSubmit: handleSubmit, noValidate: true, children: [fields.map((field) => {
127
+ return (_jsxs(_Fragment, { children: [_jsx(Div, { surface: "overlay-xs", className: "fixed inset-0 backdrop-blur-[2px] [z-index:calc(var(--appkit-z-modal)+1)]", "aria-hidden": "true", onClick: handleClose }), _jsxs(Div, { ref: drawerRef, role: "dialog", "aria-modal": "true", "aria-label": title, layout: "flex-col", shadow: "2xl", className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} bg-[var(--appkit-color-surface)] w-full lg:w-[40%] [z-index:calc(var(--appkit-z-modal)+2)]`, children: [_jsxs(Row, { className: "flex-shrink-0 border-b border-[var(--appkit-color-border)]", padding: "md", align: "center", gap: "3", reverse: hand === "left", children: [_jsx(Text, { className: "flex-1 text-[var(--appkit-color-text)]", size: "base", weight: "semibold", children: title }), _jsx(IconButton, { type: "button", onClick: handleClose, "aria-label": "Close", variant: "ghost", rounded: "lg", className: "text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-border-subtle)]", children: _jsx(X, { className: "w-5 h-5" }) })] }), _jsxs(Form, { className: "flex-1", overflow: "y-auto", paddingX: "x-md", paddingY: "y-md-lg", spacing: "md", onSubmit: handleSubmit, noValidate: true, children: [fields.map((field) => {
126
128
  const value = values[field.name];
127
129
  if (field.type === "toggle") {
128
130
  return (_jsxs(Stack, { gap: "xs", children: [_jsx(Toggle, { checked: !!value, onChange: (checked) => set(field.name, checked), label: field.label }), field.helperText && (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: field.helperText }))] }, field.name));
@@ -83,6 +83,16 @@ const rawTesterChecklistItems = [
83
83
  description: "Each of the three links should open your own public-facing profile page (/profile/[your uid]) — not the edit-profile page. If your profile visibility is set to Private, confirm the page still loads for you (the owner) even though other users would get a 404.",
84
84
  href: "/user",
85
85
  },
86
+ {
87
+ key: "hand-mode-toggle-exists",
88
+ label: "A \"Left-hand mode\" toggle is visible on the Appearance tab of Settings, and also on the /user/profile page, and switching it saves without a page reload",
89
+ href: "/user/settings",
90
+ },
91
+ {
92
+ key: "hand-mode-persists-reload",
93
+ label: "After turning on Left-hand mode, reloading the page (and logging out/back in) keeps the setting on",
94
+ href: "/user/settings",
95
+ },
86
96
  ],
87
97
  },
88
98
  {
@@ -156,6 +166,11 @@ const rawTesterChecklistItems = [
156
166
  label: "Applying multiple filters together (price range + brand + category + condition) narrows results correctly, and clearing filters restores the full list — on Products, Auctions, and Pre-Orders",
157
167
  href: "/products",
158
168
  },
169
+ {
170
+ key: "filter-drawer-flips-hand-mode",
171
+ label: "With Left-hand mode ON, the product/auction filter drawer opens from the left instead of the right",
172
+ href: "/products",
173
+ },
159
174
  {
160
175
  key: "search-filter-sort-combo",
161
176
  label: "Typing a search query, then applying a filter, then changing sort — all three stay applied together and pagination reflects the combined result count (not just the last action applied)",
@@ -447,6 +462,7 @@ const rawTesterChecklistItems = [
447
462
  { key: "list-preorder", label: "Listing a pre-order works" },
448
463
  { key: "edit-listing", label: "Editing an existing listing works" },
449
464
  { key: "media-upload", label: "Uploading product images/video during listing works" },
465
+ { key: "seller-quick-add-drawer-flips", label: "With Left-hand mode ON, the seller's quick-add-listing side drawer opens from the left instead of the right" },
450
466
  ],
451
467
  },
452
468
  {
@@ -734,6 +750,21 @@ const rawTesterChecklistItems = [
734
750
  },
735
751
  ],
736
752
  },
753
+ {
754
+ pageKey: "hand-mode-layout",
755
+ pageLabel: "Left-Hand Mode",
756
+ cases: [
757
+ { key: "sidebar-flips", label: "With Left-hand mode ON, the admin/store/user dashboard's persistent left navigation sidebar (and its collapse handle) moves to the right side of the screen; with it OFF (default), the sidebar stays on the left", href: "/admin/dashboard" },
758
+ { key: "drawers-flip", label: "With Left-hand mode ON, side drawers/panels (filters, quick-add forms, cart, edit/create panels) that normally slide in from the right now slide in from the left", href: "/products" },
759
+ { key: "close-buttons-flip", label: "With Left-hand mode ON, the X close button on drawers/panels/modals sits on the left edge of its header; with it OFF (default), it sits on the right edge" },
760
+ { key: "back-to-top-cta-flips", label: "With Left-hand mode ON, the floating \"back to top\" button appears in the bottom-left corner instead of bottom-right after scrolling down a long listing page", href: "/products" },
761
+ { key: "quick-links-unaffected", label: "Turning on Left-hand mode does not reorder or mirror the My Account / Admin dashboard quick-links tile grid — only its surrounding column shifts with the sidebar", href: "/user" },
762
+ { key: "hero-carousel-arrows-flip", label: "With Left-hand mode ON, the homepage hero carousel's Prev/Next arrow pair appears in the bottom-left corner instead of bottom-right; the dots stay centered either way", href: "/" },
763
+ { key: "gallery-arrows-unaffected", label: "Left-hand mode does NOT change a product image gallery/lightbox's Prev/Next arrows — Prev stays on the left edge and Next stays on the right edge in both modes" },
764
+ { key: "homepage-section-buttons-mirror", label: "With Left-hand mode ON, homepage section header rows (e.g. Brands, Featured Bundles, Google Reviews) have their \"View All\" button on the opposite side from default — title and button swap sides", href: "/" },
765
+ { key: "hand-mode-no-fouc", label: "On a hard page reload with Left-hand mode already ON, panels/sidebars render on the left immediately — there is no visible flash of them briefly appearing on the right before snapping left" },
766
+ ],
767
+ },
737
768
  {
738
769
  pageKey: "footer-theme",
739
770
  pageLabel: "Footer & Dark Mode",
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Public re-export of the appkit hand-mode primitives.
3
+ *
4
+ * `HandModeProvider`, `useHandMode`, and the related types live in
5
+ * `_internal/client/hand-mode/` because they are consumed by both the public
6
+ * `client.ts` barrel and other internal modules (drawers, sidebars). This
7
+ * file is the canonical public surface — `client.ts` re-exports through
8
+ * here so the `audit-appkit-reexports` rule (no `_internal/` in public
9
+ * barrels) stays green. Mirrors `theme/index.ts`.
10
+ */
11
+ export { HandModeProvider, useHandMode } from "../_internal/client/hand-mode";
12
+ export type { HandMode, HandModeContextValue, HandModeProviderProps, } from "../_internal/client/hand-mode";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public re-export of the appkit hand-mode primitives.
3
+ *
4
+ * `HandModeProvider`, `useHandMode`, and the related types live in
5
+ * `_internal/client/hand-mode/` because they are consumed by both the public
6
+ * `client.ts` barrel and other internal modules (drawers, sidebars). This
7
+ * file is the canonical public surface — `client.ts` re-exports through
8
+ * here so the `audit-appkit-reexports` rule (no `_internal/` in public
9
+ * barrels) stays green. Mirrors `theme/index.ts`.
10
+ */
11
+ export { HandModeProvider, useHandMode } from "../_internal/client/hand-mode";
@@ -191,7 +191,18 @@ const TOKEN_TO_OP = {
191
191
  ">=": ">=",
192
192
  "@=": "array-contains",
193
193
  };
194
- /** Best-effort scalar coercion for sieve filter values. */
194
+ /** Matches a bare `YYYY-MM-DD` date or a full ISO-8601 timestamp. */
195
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?$/;
196
+ /**
197
+ * Best-effort scalar coercion for sieve filter values, used by the legacy
198
+ * `findAll()` query path (no `SieveFieldConfig`/`parseValue` mechanism
199
+ * exists here — see `./sieve.ts`'s `sieveQuery`/`applySieveToFirestore` for
200
+ * that). An ISO-date-shaped string is converted to a real `Date` so a range
201
+ * filter (`>`/`<`/`>=`/`<=`) on a Firestore Timestamp field compares
202
+ * correctly — without this, the string reaches `.where(field, op, value)`
203
+ * unchanged and Firestore's type-ordering silently matches ZERO documents
204
+ * (CLAUDE.md Recurrent Root Cause Pattern #47).
205
+ */
195
206
  function coerceValue(raw) {
196
207
  if (raw === "true")
197
208
  return true;
@@ -199,6 +210,11 @@ function coerceValue(raw) {
199
210
  return false;
200
211
  if (raw === "null")
201
212
  return null;
213
+ if (ISO_DATE_RE.test(raw)) {
214
+ const d = new Date(raw);
215
+ if (!Number.isNaN(d.getTime()))
216
+ return d;
217
+ }
202
218
  const n = Number(raw);
203
219
  if (!Number.isNaN(n) && raw !== "")
204
220
  return n;
@@ -41,6 +41,11 @@ export interface SessionUser {
41
41
  permissions?: string[];
42
42
  /** Hash of the last dismissed announcement banner. */
43
43
  dismissedBannerHash?: string;
44
+ /** Persisted UI layout preferences (collapsed sections, left-hand mode, ...). */
45
+ uiPreferences?: {
46
+ collapsedSections?: string[];
47
+ handMode?: "left" | "right";
48
+ };
44
49
  }
45
50
  export interface SessionContextValue {
46
51
  user: SessionUser | null;
@@ -79,6 +79,7 @@ function buildSessionUser(authUser, serverData) {
79
79
  ? new Date(serverData.scamAwarenessAcknowledgedAt)
80
80
  : null,
81
81
  dismissedBannerHash: serverData.dismissedBannerHash,
82
+ uiPreferences: serverData.uiPreferences,
82
83
  };
83
84
  }
84
85
  // ---------------------------------------------------------------------------
@@ -150,6 +151,7 @@ export function SessionProvider({ children, initialUser, endpoints: endpointOver
150
151
  ? new Date(data.scamAwarenessAcknowledgedAt)
151
152
  : null,
152
153
  dismissedBannerHash: data.dismissedBannerHash,
154
+ uiPreferences: data.uiPreferences,
153
155
  };
154
156
  }
155
157
  catch (_err) {
@@ -122,12 +122,13 @@ export const siteSettingsSeedData = {
122
122
  teamSubtitle: "A small team obsessed with the same thing you are — the hunt for the next piece of the collection.",
123
123
  teamMembers: [
124
124
  {
125
- name: "Arjun Verma",
125
+ name: "Mohasin Chinnapattan",
126
126
  role: "Founder & Developer",
127
127
  bio: "Started LetItRip after one too many afternoons hunting for a specific 1st-edition card across a dozen disconnected forums and marketplaces. Built the entire platform — Next.js front end, Firebase backend, the auction engine, the whole thing — the same way most great collections start: one late night at a time. Still personally answers the support inbox.",
128
- photoUrl: seedExtMedia("https://picsum.photos/seed/letitrip-team-arjun-verma-20260819/400/400"),
128
+ photoUrl: seedExtMedia("https://picsum.photos/seed/letitrip-team-mohasin-chinnapattan-20260820/400/400"),
129
129
  isFounder: true,
130
130
  isDeveloper: true,
131
+ githubUrl: "https://github.com/mohasinac",
131
132
  },
132
133
  {
133
134
  name: "Ananya Rao",
@@ -62,7 +62,7 @@ export { renderScamOg, renderScamOgImage, type ScamOgData, getScammerForDetail,
62
62
  export { getSearchResults, searchAction, type SearchQuery, } from "./_internal/server/features/search/index";
63
63
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
64
64
  export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./_internal/server/features/grouped/index";
65
- export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, } from "./_internal/server/features/checkout/index";
65
+ export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./_internal/server/features/checkout/index";
66
66
  export { processRefundAction, type ProcessRefundInput, } from "./_internal/server/features/refunds/actions";
67
67
  export { createPaymentIntentAction, verifyPaymentSignatureAction, resolvePaymentFee, PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, PAYMENTS_RECEIPT_PREFIX, type CreatePaymentIntentInput, type CreatePaymentIntentResult, type VerifyPaymentSignatureInput, type ResolvedPaymentFee, } from "./_internal/server/features/payments/index";
68
68
  export { getSublistingCategoryForDetail } from "./_internal/server/features/sublisting-categories/index";
@@ -90,7 +90,7 @@ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedList
90
90
  // `categoriesRepository.findBySlugAndType(slug, "bundle")` /
91
91
  // `.listByType("bundle", opts)` / `.findById(id)` directly.
92
92
  // S4: checkout actions
93
- export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "./_internal/server/features/checkout/index";
93
+ export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "./_internal/server/features/checkout/index";
94
94
  // S-SBUNI-RULES: refund action
95
95
  export { processRefundAction, } from "./_internal/server/features/refunds/actions";
96
96
  // S4: payments actions