@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
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
5
5
  import { X } from "lucide-react";
6
6
  import { Heading } from "./Typography";
7
7
  import { Button } from "./Button";
8
+ import { useHandMode } from "../../_internal/client/hand-mode";
8
9
  const SIDE_TRANSLATE = {
9
10
  left: {
10
11
  closed: "appkit-drawer__panel--left-closed",
@@ -28,7 +29,9 @@ const SIDE_SIZE = {
28
29
  lg: "appkit-drawer__panel--lg",
29
30
  full: "appkit-drawer__panel--full",
30
31
  };
31
- export function Drawer({ isOpen, onClose, title, children, footer, side = "right", size = "md", showCloseButton = true, className = "", }) {
32
+ export function Drawer({ isOpen, onClose, title, children, footer, side, size = "md", showCloseButton = true, className = "", }) {
33
+ const { hand } = useHandMode();
34
+ const resolvedSide = side ?? hand;
32
35
  const panelRef = useRef(null);
33
36
  const prevFocusRef = useRef(null);
34
37
  // Body scroll lock
@@ -55,8 +58,8 @@ export function Drawer({ isOpen, onClose, title, children, footer, side = "right
55
58
  if (e.key === "Escape")
56
59
  onClose();
57
60
  }, [onClose]);
58
- const { closed, open, base } = SIDE_TRANSLATE[side];
59
- const isBottom = side === "bottom";
61
+ const { closed, open, base } = SIDE_TRANSLATE[resolvedSide];
62
+ const isBottom = resolvedSide === "bottom";
60
63
  if (typeof document === "undefined")
61
64
  return null;
62
65
  return createPortal(_jsxs("div", { className: `appkit-drawer ${isOpen ? "appkit-drawer--open" : "appkit-drawer--closed"}`, onKeyDown: handleKeyDown, "data-section": "drawer-div-480", children: [_jsx("div", { className: `appkit-drawer__backdrop ${isOpen ? "appkit-drawer__backdrop--open" : "appkit-drawer__backdrop--closed"}`, "aria-hidden": "true", onClick: onClose }), _jsxs("div", { ref: panelRef, tabIndex: -1, role: "dialog", "aria-modal": "true", className: [
@@ -67,5 +70,5 @@ export function Drawer({ isOpen, onClose, title, children, footer, side = "right
67
70
  : `${SIDE_SIZE[size]} appkit-drawer__panel--full-height`,
68
71
  isOpen ? open : closed,
69
72
  className,
70
- ].join(" "), "data-section": "drawer-div-481", children: [isBottom && (_jsx("div", { className: "appkit-drawer__drag-wrap", "data-section": "drawer-div-482", children: _jsx("div", { className: "appkit-drawer__drag", "aria-hidden": "true" }) })), (title || showCloseButton) && (_jsxs("div", { className: "appkit-drawer__header", "data-section": "drawer-div-483", children: [title && (_jsx(Heading, { level: 2, className: "appkit-drawer__title", children: title })), showCloseButton && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: "appkit-drawer__close", "aria-label": "Close", children: _jsx(X, { className: "appkit-drawer__close-icon" }) }))] })), _jsx("div", { className: "appkit-drawer__body", "data-section": "drawer-div-484", children: children }), footer && _jsx("div", { className: "appkit-drawer__footer", "data-section": "drawer-div-485", children: footer })] })] }), document.body);
73
+ ].join(" "), "data-section": "drawer-div-481", children: [isBottom && (_jsx("div", { className: "appkit-drawer__drag-wrap", "data-section": "drawer-div-482", children: _jsx("div", { className: "appkit-drawer__drag", "aria-hidden": "true" }) })), (title || showCloseButton) && (_jsxs("div", { className: hand === "left" ? "appkit-drawer__header flex-row-reverse" : "appkit-drawer__header", "data-section": "drawer-div-483", children: [title && (_jsx(Heading, { level: 2, className: "appkit-drawer__title", children: title })), showCloseButton && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: "appkit-drawer__close", "aria-label": "Close", children: _jsx(X, { className: "appkit-drawer__close-icon" }) }))] })), _jsx("div", { className: "appkit-drawer__body", "data-section": "drawer-div-484", children: children }), footer && _jsx("div", { className: "appkit-drawer__footer", "data-section": "drawer-div-485", children: footer })] })] }), document.body);
71
74
  }
@@ -5,7 +5,7 @@ import { Button } from "./Button";
5
5
  import { Drawer } from "./Drawer";
6
6
  import { Span } from "./Typography";
7
7
  import { sectionBackgroundStyle } from "./Semantic";
8
- export function FilterDrawer({ children, title = "Filters", open, onOpen, onClose, onApply, onReset, activeCount = 0, pendingCount, triggerLabel = "Filters", triggerClassName = "", hideTrigger = false, side = "right", size = "md", background, }) {
8
+ export function FilterDrawer({ children, title = "Filters", open, onOpen, onClose, onApply, onReset, activeCount = 0, pendingCount, triggerLabel = "Filters", triggerClassName = "", hideTrigger = false, side, size = "md", background, }) {
9
9
  const [internalOpen, setInternalOpen] = React.useState(false);
10
10
  const isControlled = open !== undefined;
11
11
  const isOpen = isControlled ? open : internalOpen;
@@ -0,0 +1,12 @@
1
+ import { type RowProps } from "./Layout";
2
+ /**
3
+ * HandModeRow — a `<Row>` that mirrors (flex-row-reverse) in left-hand mode.
4
+ *
5
+ * `<Row>` itself stays a plain server-safe primitive (no hooks) so it can
6
+ * keep rendering inside async Server Components. This thin client wrapper
7
+ * exists specifically for header rows (title + "View All" CTA) that live
8
+ * inside Server Component sections and still need to flip sides with the
9
+ * hand-mode preference — render it as a child of the server component
10
+ * instead of calling `useHandMode()` in the server component itself.
11
+ */
12
+ export declare function HandModeRow(props: RowProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Row } from "./Layout";
4
+ import { useHandMode } from "../../_internal/client/hand-mode";
5
+ /**
6
+ * HandModeRow — a `<Row>` that mirrors (flex-row-reverse) in left-hand mode.
7
+ *
8
+ * `<Row>` itself stays a plain server-safe primitive (no hooks) so it can
9
+ * keep rendering inside async Server Components. This thin client wrapper
10
+ * exists specifically for header rows (title + "View All" CTA) that live
11
+ * inside Server Component sections and still need to flip sides with the
12
+ * hand-mode preference — render it as a child of the server component
13
+ * instead of calling `useHandMode()` in the server component itself.
14
+ */
15
+ export function HandModeRow(props) {
16
+ const { hand } = useHandMode();
17
+ return _jsx(Row, { reverse: hand === "left", ...props });
18
+ }
@@ -2,11 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
- import { ChevronLeft, ChevronRight, X, ZoomIn, ZoomOut, RotateCw, Maximize2 } from "lucide-react";
5
+ import { ChevronLeft, ChevronRight, X, ZoomIn, ZoomOut, RotateCw, Maximize2, Minimize2 } from "lucide-react";
6
6
  import { Button } from "./Button";
7
7
  import { Div } from "./Div";
8
8
  import { Text, Span } from "./Typography";
9
9
  import { MediaImage } from "../../features/media/MediaImage";
10
+ import { getYouTubeVideoId } from "../../utils/media-url";
10
11
  const CLS_CLOSE_BTN = "w-10 h-10 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-error-surface text-white flex items-center justify-center";
11
12
  const MIN_ZOOM = 10;
12
13
  const MAX_ZOOM = 300;
@@ -15,6 +16,7 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
15
16
  const [currentIndex, setCurrentIndex] = useState(activeIndex ?? 0);
16
17
  const [zoom, setZoom] = useState(100);
17
18
  const [rotation, setRotation] = useState(0);
19
+ const [isFullscreen, setIsFullscreen] = useState(false);
18
20
  const overlayRef = useRef(null);
19
21
  // Sync external activeIndex and reset transforms
20
22
  useEffect(() => {
@@ -33,6 +35,37 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
33
35
  document.body.style.overflow = "hidden";
34
36
  return () => { document.body.style.overflow = prev; };
35
37
  }, [isOpen]);
38
+ // Track native Fullscreen API state so the toggle button + icon stay in sync
39
+ // even when the user exits via Esc/browser chrome rather than our button.
40
+ useEffect(() => {
41
+ const onFsChange = () => {
42
+ const doc = document;
43
+ const fsEl = document.fullscreenElement ?? doc.webkitFullscreenElement ?? null;
44
+ setIsFullscreen(fsEl === overlayRef.current);
45
+ };
46
+ document.addEventListener("fullscreenchange", onFsChange);
47
+ document.addEventListener("webkitfullscreenchange", onFsChange);
48
+ return () => {
49
+ document.removeEventListener("fullscreenchange", onFsChange);
50
+ document.removeEventListener("webkitfullscreenchange", onFsChange);
51
+ };
52
+ }, []);
53
+ // Exit native fullscreen when the lightbox itself closes.
54
+ useEffect(() => {
55
+ const doc = document;
56
+ if (!isOpen && (document.fullscreenElement || doc.webkitFullscreenElement)) {
57
+ (document.exitFullscreen?.() ?? doc.webkitExitFullscreen?.())?.catch?.(() => { });
58
+ }
59
+ }, [isOpen]);
60
+ const toggleFullscreen = useCallback(() => {
61
+ const doc = document;
62
+ if (document.fullscreenElement || doc.webkitFullscreenElement) {
63
+ (document.exitFullscreen?.() ?? doc.webkitExitFullscreen?.())?.catch?.(() => { });
64
+ return;
65
+ }
66
+ const el = overlayRef.current;
67
+ (el?.requestFullscreen?.() ?? el?.webkitRequestFullscreen?.())?.catch?.(() => { });
68
+ }, []);
36
69
  const navigate = useCallback((dir) => {
37
70
  setCurrentIndex((prev) => {
38
71
  const next = (prev + dir + images.length) % images.length;
@@ -89,11 +122,12 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
89
122
  return null;
90
123
  const image = images[currentIndex];
91
124
  const hasMultiple = images.length > 1;
125
+ const youtubeId = image.kind === "video" ? getYouTubeVideoId(image.src) : null;
92
126
  const iconBtnClass = "w-10 h-10 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white flex items-center justify-center";
93
- return createPortal(_jsxs("div", { ref: overlayRef, tabIndex: -1, className: "appkit-lightbox", role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", onKeyDown: handleKeyDown, onWheel: handleWheel, children: [_jsxs("div", { className: "absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-3 bg-gradient-to-b from-black/60 to-transparent", children: [_jsx(Span, { size: "sm", weight: "medium", className: "text-white/70", children: hasMultiple ? `${currentIndex + 1} / ${images.length}` : "" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(-ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom out", disabled: zoom <= MIN_ZOOM, children: _jsx(ZoomOut, { className: "w-4 h-4" }) }), _jsxs("button", { type: "button", onClick: () => { setZoom(100); setRotation(0); }, className: "text-white/70 hover:text-white text-xs font-mono min-w-[3rem] text-center", "aria-label": "Reset zoom", children: [zoom, "%"] }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom in", disabled: zoom >= MAX_ZOOM, children: _jsx(ZoomIn, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => setRotation((r) => (r + 90) % 360), className: iconBtnClass, "aria-label": "Rotate 90\u00B0", children: _jsx(RotateCw, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => { setZoom(100); setRotation(0); }, className: iconBtnClass, "aria-label": "Reset", children: _jsx(Maximize2, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: CLS_CLOSE_BTN, "aria-label": "Close lightbox", children: _jsx(X, { className: "w-5 h-5" }) })] })] }), hasMultiple && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => navigate(-1), className: "absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white z-10 flex items-center justify-center", "aria-label": "Previous image", children: _jsx(ChevronLeft, { className: "w-7 h-7" }) })), _jsxs("div", { className: "appkit-lightbox__image-wrap", style: { cursor: zoom > 100 ? "grab" : "default" }, children: [image.kind === "video" ? (_jsx("video", { src: image.src, poster: image.poster, controls: true, playsInline: true, "aria-label": image.alt ?? "Video", className: "appkit-lightbox__img", style: {
127
+ return createPortal(_jsxs("div", { ref: overlayRef, tabIndex: -1, className: "appkit-lightbox", role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", onKeyDown: handleKeyDown, onWheel: handleWheel, children: [_jsxs("div", { className: "absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-3 bg-gradient-to-b from-black/60 to-transparent", children: [_jsx(Span, { size: "sm", weight: "medium", className: "text-white/70", children: hasMultiple ? `${currentIndex + 1} / ${images.length}` : "" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(-ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom out", disabled: zoom <= MIN_ZOOM, children: _jsx(ZoomOut, { className: "w-4 h-4" }) }), _jsxs("button", { type: "button", onClick: () => { setZoom(100); setRotation(0); }, className: "text-white/70 hover:text-white text-xs font-mono min-w-[3rem] text-center", "aria-label": "Reset zoom", children: [zoom, "%"] }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom in", disabled: zoom >= MAX_ZOOM, children: _jsx(ZoomIn, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => setRotation((r) => (r + 90) % 360), className: iconBtnClass, "aria-label": "Rotate 90\u00B0", children: _jsx(RotateCw, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: toggleFullscreen, className: iconBtnClass, "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen", children: isFullscreen ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize2, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: CLS_CLOSE_BTN, "aria-label": "Close lightbox", children: _jsx(X, { className: "w-5 h-5" }) })] })] }), hasMultiple && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => navigate(-1), className: "absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white z-10 flex items-center justify-center", "aria-label": "Previous image", children: _jsx(ChevronLeft, { className: "w-7 h-7" }) })), _jsxs("div", { className: "appkit-lightbox__image-wrap", style: { cursor: zoom > 100 ? "grab" : "default" }, children: [image.kind === "video" ? (youtubeId ? (_jsx("iframe", { src: `https://www.youtube-nocookie.com/embed/${youtubeId}?playsinline=1&rel=0`, title: image.alt ?? "Video", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, className: "appkit-lightbox__img appkit-lightbox__video border-0" })) : (_jsx("video", { src: image.src, poster: image.poster, controls: true, playsInline: true, "aria-label": image.alt ?? "Video", className: "appkit-lightbox__img appkit-lightbox__video", style: {
94
128
  transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
95
129
  transition: "transform 0.2s ease",
96
- } })) : (_jsx(MediaImage, { src: image.src, alt: image.alt ?? "", size: "hero", objectFit: "contain", className: "appkit-lightbox__img", style: {
130
+ } }))) : (_jsx(MediaImage, { src: image.src, alt: image.alt ?? "", size: "hero", objectFit: "contain", className: "appkit-lightbox__img", style: {
97
131
  transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
98
132
  transition: "transform 0.2s ease",
99
133
  } })), image.badge && (_jsx(Div, { className: "absolute left-3 top-3 rounded bg-black/70 px-2 py-1 text-xs font-semibold text-white pointer-events-none", children: image.badge }))] }), (image.caption || image.sub) && (_jsxs("div", { className: "flex-shrink-0 text-center px-8 pb-2", children: [image.caption && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/80", children: image.caption })), image.sub && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/50", children: image.sub }))] })), showThumbnails && images.length > 1 && (_jsx("div", { className: "flex-shrink-0 flex gap-2 overflow-x-auto px-4 pb-3 justify-center", children: images.map((thumb, i) => (_jsx("button", { type: "button", onClick: () => {
@@ -37,6 +37,14 @@
37
37
  position: relative;
38
38
  }
39
39
 
40
+ @media (max-width: 640px) {
41
+ .appkit-lightbox__image-wrap {
42
+ /* Tighter than the desktop 4rem so a video's min-width (below) has room
43
+ to fit inside narrow phone viewports without forcing horizontal scroll. */
44
+ padding: 1.5rem 1rem;
45
+ }
46
+ }
47
+
40
48
  .appkit-lightbox__img {
41
49
  max-height: 100%;
42
50
  max-width: 100%;
@@ -44,6 +52,26 @@
44
52
  user-select: none;
45
53
  }
46
54
 
55
+ /* -- Video / YouTube-embed area ----------------------- */
56
+ /* Applied alongside .appkit-lightbox__img on the theater-mode <video>/<iframe>
57
+ so a video always occupies a real, tappable box — never the browser's tiny
58
+ ~300x150 default when a src fails to load or an embed reports no intrinsic
59
+ size (Recurrent Root Cause Pattern #27-adjacent: video needs its own sizing
60
+ contract, it can't just inherit the image one). */
61
+ .appkit-lightbox__video {
62
+ width: 100%;
63
+ height: 100%;
64
+ min-width: 260px;
65
+ min-height: 190px;
66
+ }
67
+
68
+ @media (min-width: 768px) {
69
+ .appkit-lightbox__video {
70
+ min-width: 480px;
71
+ min-height: 320px;
72
+ }
73
+ }
74
+
47
75
  /* -- Zoom hint --------------------------------------- */
48
76
 
49
77
  .appkit-lightbox__zoom-hint {
@@ -279,6 +279,10 @@ export interface RowProps extends React.HTMLAttributes<HTMLElement>, SurfaceProp
279
279
  color?: TextColorKey;
280
280
  /** Allow children to wrap onto multiple lines. */
281
281
  wrap?: boolean;
282
+ /** Reverse the visual order of children (`flex-direction: row-reverse`) without
283
+ * touching DOM/source order. Used for hand-mode-aware layouts (e.g. moving a
284
+ * close button to the opposite header edge) — replaces raw `flex-row-reverse`. */
285
+ reverse?: boolean;
282
286
  /** Zebra-stripe pattern when this Row is a repeating list element (rendered with `even:bg-*`). */
283
287
  oddEven?: "none" | "zebra";
284
288
  /**
@@ -315,7 +319,7 @@ declare const TEXT_COLOR_MAP: {
315
319
  readonly info: "appkit-color--info";
316
320
  };
317
321
  type TextColorKey = keyof typeof TEXT_COLOR_MAP;
318
- export declare function Row({ gap, centered, align, justify, textSize, textWeight, color, wrap, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: RowProps): React.JSX.Element;
322
+ export declare function Row({ gap, centered, align, justify, textSize, textWeight, color, wrap, reverse, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className, children, ...props }: RowProps): React.JSX.Element;
319
323
  /**
320
324
  * Responsive CSS grid. Use instead of `<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" data-section="layout-div-537">`.
321
325
  *
@@ -231,10 +231,11 @@ const ROW_ODD_EVEN_MAP = {
231
231
  none: "",
232
232
  zebra: "even:bg-[var(--appkit-color-surface-input)] dark:even:bg-[var(--appkit-color-bg)]",
233
233
  };
234
- export function Row({ gap = "md", centered = false, align = "center", justify = "start", textSize, textWeight, color, wrap = false, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
234
+ export function Row({ gap = "md", centered = false, align = "center", justify = "start", textSize, textWeight, color, wrap = false, reverse = false, oddEven, divide, as, surface, padding, paddingX, paddingY, rounded, roundedTop, roundedBottom, border, shadow, overflow, className = "", children, ...props }) {
235
235
  const Tag = (as ?? "div");
236
236
  const classes = [
237
237
  "appkit-row",
238
+ reverse ? "appkit-row--reverse" : "",
238
239
  centered ? "appkit-row--centered" : align !== "center" ? ITEMS_MAP[align] : "",
239
240
  !centered && justify !== "start" ? JUSTIFY_MAP[justify] : "",
240
241
  GAP_MAP[gap],
@@ -58,6 +58,7 @@
58
58
  }
59
59
 
60
60
  .appkit-row--wrap { flex-wrap: wrap; }
61
+ .appkit-row--reverse { flex-direction: row-reverse; }
61
62
  .appkit-stack--centered,
62
63
  .appkit-row--centered {
63
64
  align-items: center;
@@ -4,9 +4,11 @@ import { X } from "lucide-react";
4
4
  import { Button } from "./Button";
5
5
  import { Span } from "./Typography";
6
6
  import { sectionBackgroundStyle } from "./Semantic";
7
+ import { useHandMode } from "../../_internal/client/hand-mode";
7
8
  const CLS_CLEAR_BTN = "text-xs text-[var(--appkit-color-text-muted)] hover:text-error text-[var(--appkit-color-text-muted)] transition-colors min-h-0 h-auto p-0";
8
9
  export function ListingFilterDrawer({ open, onClose, onApply, onClear, activeCount, children, background, }) {
10
+ const { hand } = useHandMode();
9
11
  if (!open)
10
12
  return null;
11
- return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: onClose }), _jsxs("div", { className: `fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-[var(--appkit-color-surface)] shadow-2xl${background?.value ? " relative overflow-hidden" : ""}`, children: [background?.value && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, className: "absolute inset-0 -z-10", style: sectionBackgroundStyle(background) }), background.overlay?.enabled && (_jsx(Span, { "aria-hidden": true, className: "absolute inset-0 -z-10", style: { backgroundColor: background.overlay.color, opacity: background.overlay.opacity } }))] })), _jsxs("div", { className: "flex items-center justify-between border-b border-[var(--appkit-color-border)] px-4 py-3.5", children: [_jsx(Span, { size: "base", weight: "semibold", className: "text-[var(--appkit-color-text)]", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeCount > 0 && (_jsx(Button, { variant: "ghost", type: "button", onClick: onClear, className: CLS_CLEAR_BTN, children: "Clear all" })), _jsx(Button, { variant: "ghost", type: "button", onClick: onClose, "aria-label": "Close", className: "rounded-lg p-1.5 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] transition-colors min-h-0 h-auto", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: children }), _jsx("div", { className: "border-t border-[var(--appkit-color-border)] px-4 py-3.5", children: _jsxs(Button, { type: "button", onClick: onApply, className: "w-full", children: ["Apply Filters", activeCount > 0 ? ` (${activeCount})` : ""] }) })] })] }));
13
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: onClose }), _jsxs("div", { className: `fixed inset-y-0 ${hand === "left" ? "left-0" : "right-0"} z-50 flex w-80 flex-col bg-[var(--appkit-color-surface)] shadow-2xl${background?.value ? " relative overflow-hidden" : ""}`, children: [background?.value && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, className: "absolute inset-0 -z-10", style: sectionBackgroundStyle(background) }), background.overlay?.enabled && (_jsx(Span, { "aria-hidden": true, className: "absolute inset-0 -z-10", style: { backgroundColor: background.overlay.color, opacity: background.overlay.opacity } }))] })), _jsxs("div", { className: `flex items-center justify-between border-b border-[var(--appkit-color-border)] px-4 py-3.5${hand === "left" ? " flex-row-reverse" : ""}`, children: [_jsx(Span, { size: "base", weight: "semibold", className: "text-[var(--appkit-color-text)]", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeCount > 0 && (_jsx(Button, { variant: "ghost", type: "button", onClick: onClear, className: CLS_CLEAR_BTN, children: "Clear all" })), _jsx(Button, { variant: "ghost", type: "button", onClick: onClose, "aria-label": "Close", className: "rounded-lg p-1.5 text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)] transition-colors min-h-0 h-auto", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: children }), _jsx("div", { className: "border-t border-[var(--appkit-color-border)] px-4 py-3.5", children: _jsxs(Button, { type: "button", onClick: onApply, className: "w-full", children: ["Apply Filters", activeCount > 0 ? ` (${activeCount})` : ""] }) })] })] }));
12
14
  }
@@ -6,6 +6,7 @@ import { motion, AnimatePresence, useReducedMotion } from "motion/react";
6
6
  import { Heading } from "./Typography";
7
7
  import { Button } from "./Button";
8
8
  import { SPRING_GENTLE } from "../../tokens/motion";
9
+ import { useHandMode } from "../../_internal/client/hand-mode";
9
10
  const SIZE_CLASSES = {
10
11
  sm: "appkit-modal__panel--sm",
11
12
  md: "appkit-modal__panel--md",
@@ -15,6 +16,7 @@ const SIZE_CLASSES = {
15
16
  };
16
17
  export function Modal({ isOpen, open, onClose, title, children, size = "md", showCloseButton = true, actions, className = "", }) {
17
18
  const visible = open ?? isOpen ?? false;
19
+ const { hand } = useHandMode();
18
20
  const titleId = React.useId();
19
21
  const panelRef = React.useRef(null);
20
22
  const previousFocusRef = React.useRef(null);
@@ -69,7 +71,7 @@ export function Modal({ isOpen, open, onClose, title, children, size = "md", sho
69
71
  }
70
72
  return createPortal(_jsx(AnimatePresence, { children: visible && (_jsxs(motion.div, { className: "appkit-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, onKeyDown: handleKeyDown, initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15 }, "data-section": "modal-div-561", children: [_jsx("div", { className: "appkit-modal__backdrop", "aria-hidden": "true", onClick: onClose }), _jsxs(motion.div, { ref: panelRef, tabIndex: -1, className: ["appkit-modal__panel", SIZE_CLASSES[size], className]
71
73
  .filter(Boolean)
72
- .join(" "), initial: reduced ? false : { opacity: 0, scale: 0.95, y: 8 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.95, y: 8 }, transition: SPRING_GENTLE, "data-section": "modal-div-562", children: [(title || showCloseButton) && (_jsxs("div", { className: "appkit-modal__header", "data-section": "modal-div-563", children: [title ? (_jsx(Heading, { level: 2, id: titleId, className: "appkit-modal__title", children: title })) : (_jsx("span", {})), showCloseButton ? (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: "appkit-modal__close", "aria-label": "Close", children: _jsx("span", { className: "appkit-modal__close-icon", "aria-hidden": "true", children: "\u00D7" }) })) : null] })), _jsx("div", { className: "appkit-modal__body", "data-section": "modal-div-564", children: children }), actions ? _jsx(ModalFooter, { children: actions }) : null] })] })) }), document.body);
74
+ .join(" "), initial: reduced ? false : { opacity: 0, scale: 0.95, y: 8 }, animate: { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.95, y: 8 }, transition: SPRING_GENTLE, "data-section": "modal-div-562", children: [(title || showCloseButton) && (_jsxs("div", { className: hand === "left" ? "appkit-modal__header flex-row-reverse" : "appkit-modal__header", "data-section": "modal-div-563", children: [title ? (_jsx(Heading, { level: 2, id: titleId, className: "appkit-modal__title", children: title })) : (_jsx("span", {})), showCloseButton ? (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: "appkit-modal__close", "aria-label": "Close", children: _jsx("span", { className: "appkit-modal__close-icon", "aria-hidden": "true", children: "\u00D7" }) })) : null] })), _jsx("div", { className: "appkit-modal__body", "data-section": "modal-div-564", children: children }), actions ? _jsx(ModalFooter, { children: actions }) : null] })] })) }), document.body);
73
75
  }
74
76
  export function ModalFooter({ children, className = "", }) {
75
77
  return (_jsx("div", { className: [
@@ -9,6 +9,7 @@ import { Heading, Span } from "./Typography";
9
9
  import { useSwipe } from "../../react";
10
10
  import { Text } from "./Typography";
11
11
  import { SPRING_SNAPPY } from "../../tokens/motion";
12
+ import { useHandMode } from "../../_internal/client/hand-mode";
12
13
  const UI_SIDE_DRAWER = {
13
14
  backdrop: "appkit-side-drawer__backdrop",
14
15
  root: "appkit-side-drawer",
@@ -65,7 +66,9 @@ function handleFocusTrap(e, first, last) {
65
66
  * </SideDrawer>
66
67
  * ```
67
68
  */
68
- export function SideDrawer({ isOpen, onClose, title, children, footer, mode = "view", isDirty = false, side = "right", }) {
69
+ export function SideDrawer({ isOpen, onClose, title, children, footer, mode = "view", isDirty = false, side, }) {
70
+ const { hand } = useHandMode();
71
+ const resolvedSide = side ?? hand;
69
72
  const drawerRef = useRef(null);
70
73
  const [showUnsavedWarning, setShowUnsavedWarning] = useState(false);
71
74
  const tActions = useTranslations("actions");
@@ -141,16 +144,16 @@ export function SideDrawer({ isOpen, onClose, title, children, footer, mode = "v
141
144
  };
142
145
  }, [isOpen]);
143
146
  useSwipe(drawerRef, {
144
- ...(side === "left"
147
+ ...(resolvedSide === "left"
145
148
  ? { onSwipeLeft: () => attemptClose() }
146
149
  : { onSwipeRight: () => attemptClose() }),
147
150
  minSwipeDistance: 60,
148
151
  });
149
152
  const reduced = useReducedMotion();
150
- const positionClass = side === "left" ? UI_SIDE_DRAWER.left : UI_SIDE_DRAWER.right;
151
- const slideX = side === "left" ? "-100%" : "100%";
153
+ const positionClass = resolvedSide === "left" ? UI_SIDE_DRAWER.left : UI_SIDE_DRAWER.right;
154
+ const slideX = resolvedSide === "left" ? "-100%" : "100%";
152
155
  return (_jsx(AnimatePresence, { children: isOpen && (_jsxs(_Fragment, { children: [_jsx(motion.div, { className: UI_SIDE_DRAWER.backdrop, onClick: attemptClose, "aria-hidden": "true", initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }), _jsxs(motion.div, { ref: drawerRef, className: [UI_SIDE_DRAWER.root, positionClass].join(" "), role: "dialog", "aria-modal": "true", "aria-labelledby": "drawer-title", initial: reduced ? false : { x: slideX }, animate: { x: 0 }, exit: { x: slideX }, transition: SPRING_SNAPPY, "data-section": "sidedrawer-div-598", children: [_jsxs(Row, { justify: "between", gap: "none", className: [
153
156
  UI_SIDE_DRAWER.header,
154
157
  UI_SIDE_DRAWER.headerMode[mode],
155
- ].join(" "), children: [_jsxs(Row, { gap: "3", className: "min-w-0", children: [_jsx(Button, { variant: "ghost", onClick: attemptClose, className: UI_SIDE_DRAWER.closeBtn, "aria-label": tActions("close"), children: _jsx("svg", { className: UI_SIDE_DRAWER.closeIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) }), _jsx(Heading, { level: 3, id: "drawer-title", className: "truncate", children: title })] }), mode === "delete" && (_jsx(Span, { className: UI_SIDE_DRAWER.deleteBadge, children: tActions("delete") }))] }), _jsx("div", { className: UI_SIDE_DRAWER.content, "data-section": "sidedrawer-div-599", children: children }), footer && _jsx("div", { className: UI_SIDE_DRAWER.footer, "data-section": "sidedrawer-div-600", children: footer })] }), showUnsavedWarning && (_jsxs(_Fragment, { children: [_jsx("div", { className: UI_SIDE_DRAWER.warnBackdrop, onClick: cancelClose }), _jsxs("div", { className: UI_SIDE_DRAWER.warnDialog, "data-section": "sidedrawer-div-601", children: [_jsxs("div", { className: UI_SIDE_DRAWER.warnHeader, "data-section": "sidedrawer-div-602", children: [_jsx("div", { className: UI_SIDE_DRAWER.warnIcon, "data-section": "sidedrawer-div-603", children: _jsx("svg", { fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }) }), _jsxs("div", { "data-section": "sidedrawer-div-604", children: [_jsx(Heading, { level: 4, className: "mb-1", children: tConfirm("unsavedChangesTitle") }), _jsx(Text, { className: UI_SIDE_DRAWER.warnText, children: tConfirm("unsavedChangesDescription") })] })] }), _jsxs("div", { className: UI_SIDE_DRAWER.warnActions, "data-section": "sidedrawer-div-605", children: [_jsx(Button, { variant: "outline", onClick: cancelClose, size: "sm", children: tActions("keepEditing") }), _jsx(Button, { variant: "danger", onClick: confirmClose, size: "sm", children: tActions("discardChanges") })] })] })] }))] })) }));
158
+ ].join(" "), children: [_jsxs(Row, { gap: "3", className: "min-w-0", reverse: hand === "left", children: [_jsx(Button, { variant: "ghost", onClick: attemptClose, className: UI_SIDE_DRAWER.closeBtn, "aria-label": tActions("close"), children: _jsx("svg", { className: UI_SIDE_DRAWER.closeIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) }), _jsx(Heading, { level: 3, id: "drawer-title", className: "truncate", children: title })] }), mode === "delete" && (_jsx(Span, { className: UI_SIDE_DRAWER.deleteBadge, children: tActions("delete") }))] }), _jsx("div", { className: UI_SIDE_DRAWER.content, "data-section": "sidedrawer-div-599", children: children }), footer && _jsx("div", { className: UI_SIDE_DRAWER.footer, "data-section": "sidedrawer-div-600", children: footer })] }), showUnsavedWarning && (_jsxs(_Fragment, { children: [_jsx("div", { className: UI_SIDE_DRAWER.warnBackdrop, onClick: cancelClose }), _jsxs("div", { className: UI_SIDE_DRAWER.warnDialog, "data-section": "sidedrawer-div-601", children: [_jsxs("div", { className: UI_SIDE_DRAWER.warnHeader, "data-section": "sidedrawer-div-602", children: [_jsx("div", { className: UI_SIDE_DRAWER.warnIcon, "data-section": "sidedrawer-div-603", children: _jsx("svg", { fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }) }), _jsxs("div", { "data-section": "sidedrawer-div-604", children: [_jsx(Heading, { level: 4, className: "mb-1", children: tConfirm("unsavedChangesTitle") }), _jsx(Text, { className: UI_SIDE_DRAWER.warnText, children: tConfirm("unsavedChangesDescription") })] })] }), _jsxs("div", { className: UI_SIDE_DRAWER.warnActions, "data-section": "sidedrawer-div-605", children: [_jsx(Button, { variant: "outline", onClick: cancelClose, size: "sm", children: tActions("keepEditing") }), _jsx(Button, { variant: "danger", onClick: confirmClose, size: "sm", children: tActions("discardChanges") })] })] })] }))] })) }));
156
159
  }
@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
4
4
  import { motion, AnimatePresence, useReducedMotion } from "motion/react";
5
5
  import { Button, Div, Heading } from "..";
6
6
  import { SPRING_SNAPPY } from "../../tokens/motion";
7
+ import { useHandMode } from "../../_internal/client/hand-mode";
7
8
  /**
8
9
  * SideModal — slides in from the right (or left) on desktop (60 vw),
9
10
  * full-screen drawer on mobile (< sm breakpoint).
@@ -14,7 +15,9 @@ import { SPRING_SNAPPY } from "../../tokens/motion";
14
15
  * - backdrop click closes panel
15
16
  * - role="dialog" + aria-modal + aria-labelledby
16
17
  */
17
- export function SideModal({ isOpen, onClose, title, side = "right", children, className = "", }) {
18
+ export function SideModal({ isOpen, onClose, title, side, children, className = "", }) {
19
+ const { hand } = useHandMode();
20
+ const resolvedSide = side ?? hand;
18
21
  const panelRef = useRef(null);
19
22
  const titleId = useRef(`side-modal-title-${Math.random().toString(36).slice(2, 8)}`);
20
23
  // Escape key handler
@@ -53,12 +56,12 @@ export function SideModal({ isOpen, onClose, title, side = "right", children, cl
53
56
  };
54
57
  }, [isOpen]);
55
58
  const reduced = useReducedMotion();
56
- const slideX = side === "left" ? "-100%" : "100%";
59
+ const slideX = resolvedSide === "left" ? "-100%" : "100%";
57
60
  return (_jsx(AnimatePresence, { children: isOpen && (_jsxs(Div, { className: "appkit-side-modal", "aria-hidden": !isOpen, children: [_jsx(motion.div, { className: "appkit-side-modal__backdrop", onClick: onClose, "aria-hidden": "true", initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.2 } }), _jsxs(motion.div, { ref: panelRef, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId.current : undefined, className: [
58
61
  "appkit-side-modal__panel",
59
- `appkit-side-modal__panel--${side}`,
62
+ `appkit-side-modal__panel--${resolvedSide}`,
60
63
  className,
61
64
  ]
62
65
  .filter(Boolean)
63
- .join(" "), initial: reduced ? false : { x: slideX }, animate: { x: 0 }, exit: { x: slideX }, transition: SPRING_SNAPPY, children: [title && (_jsxs(Div, { className: "appkit-side-modal__header", children: [_jsx(Heading, { level: 2, id: titleId.current, className: "text-base font-semibold text-[var(--appkit-color-text)] dark:text-white truncate", children: title }), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close panel", className: "ml-2 flex-shrink-0", children: "\u2715" })] })), _jsx(Div, { className: "appkit-side-modal__body", children: children })] })] })) }));
66
+ .join(" "), initial: reduced ? false : { x: slideX }, animate: { x: 0 }, exit: { x: slideX }, transition: SPRING_SNAPPY, children: [title && (_jsxs(Div, { className: hand === "left" ? "appkit-side-modal__header flex-row-reverse" : "appkit-side-modal__header", children: [_jsx(Heading, { level: 2, id: titleId.current, className: "text-base font-semibold text-[var(--appkit-color-text)] dark:text-white truncate", children: title }), _jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close panel", className: hand === "left" ? "mr-2 flex-shrink-0" : "ml-2 flex-shrink-0", children: "\u2715" })] })), _jsx(Div, { className: "appkit-side-modal__body", children: children })] })] })) }));
64
67
  }
@@ -175,6 +175,7 @@ export type { SurfaceKey, PaddingKey, RoundedKey, BorderKey, ShadowKey, SurfaceP
175
175
  export { SURFACE_MAP, PADDING_MAP, ROUNDED_MAP, BORDER_MAP, SHADOW_MAP, buildSurfaceClasses, } from "./components/surface-tokens";
176
176
  export type { GapKey, ContainerSize, GridCols, ViewPortal, ContainerProps, StackProps, RowProps, GridProps, } from "./components/Layout";
177
177
  export { Container, Stack, Row, Grid, GRID_MAP } from "./components/Layout";
178
+ export { HandModeRow } from "./components/HandModeRow";
178
179
  export { classNames, mergeTailwindClasses } from "./style.helper";
179
180
  export type { SideModalProps } from "./components/SideModal";
180
181
  export { SideModal } from "./components/SideModal";
package/dist/ui/index.js CHANGED
@@ -90,6 +90,7 @@ export { DataTable } from "./DataTable";
90
90
  export { DEFAULT_TABLE_CONFIG, DEFAULT_PAGINATION_CONFIG, DEFAULT_STICKY_CONFIG, mergeTableConfig, } from "../contracts";
91
91
  export { SURFACE_MAP, PADDING_MAP, ROUNDED_MAP, BORDER_MAP, SHADOW_MAP, buildSurfaceClasses, } from "./components/surface-tokens";
92
92
  export { Container, Stack, Row, Grid, GRID_MAP } from "./components/Layout";
93
+ export { HandModeRow } from "./components/HandModeRow";
93
94
  // --- UI Helpers ---------------------------------------------------------------
94
95
  export { classNames, mergeTailwindClasses } from "./style.helper";
95
96
  export { SideModal } from "./components/SideModal";
@@ -16,3 +16,12 @@ export declare const GCS_HOST = "storage.googleapis.com";
16
16
  * - Falsy → undefined
17
17
  */
18
18
  export declare function resolveMediaUrl(url: string | null | undefined): string | undefined;
19
+ /**
20
+ * Extracts the 11-char YouTube video ID from a watch/share/embed URL, or
21
+ * `null` when `url` isn't a recognized YouTube URL. A video field can be
22
+ * sourced via `MediaUploadField`'s "YouTube" tab (see `showYoutube` there),
23
+ * which stores a `youtube.com/watch?v=...` URL — that URL is never a raw
24
+ * playable media file, so every `<video src>` renderer (`MediaVideo`,
25
+ * `ImageLightbox`) must check this first and fall back to an iframe embed.
26
+ */
27
+ export declare function getYouTubeVideoId(url: string | null | undefined): string | null;
@@ -37,3 +37,42 @@ export function resolveMediaUrl(url) {
37
37
  return url;
38
38
  }
39
39
  }
40
+ const YOUTUBE_HOSTS = new Set([
41
+ "youtube.com",
42
+ "m.youtube.com",
43
+ "youtu.be",
44
+ "youtube-nocookie.com",
45
+ ]);
46
+ /**
47
+ * Extracts the 11-char YouTube video ID from a watch/share/embed URL, or
48
+ * `null` when `url` isn't a recognized YouTube URL. A video field can be
49
+ * sourced via `MediaUploadField`'s "YouTube" tab (see `showYoutube` there),
50
+ * which stores a `youtube.com/watch?v=...` URL — that URL is never a raw
51
+ * playable media file, so every `<video src>` renderer (`MediaVideo`,
52
+ * `ImageLightbox`) must check this first and fall back to an iframe embed.
53
+ */
54
+ export function getYouTubeVideoId(url) {
55
+ if (!url)
56
+ return null;
57
+ try {
58
+ const parsed = new URL(url);
59
+ const host = parsed.hostname.replace(/^www\./, "");
60
+ if (!YOUTUBE_HOSTS.has(host))
61
+ return null;
62
+ if (host === "youtu.be") {
63
+ const id = parsed.pathname.slice(1);
64
+ return /^[\w-]{11}$/.test(id) ? id : null;
65
+ }
66
+ const fromQuery = parsed.searchParams.get("v");
67
+ if (fromQuery && /^[\w-]{11}$/.test(fromQuery))
68
+ return fromQuery;
69
+ const embedMatch = parsed.pathname.match(/\/embed\/([\w-]{11})/);
70
+ if (embedMatch)
71
+ return embedMatch[1];
72
+ return null;
73
+ }
74
+ catch (_err) {
75
+ void normalizeError(_err);
76
+ return null;
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.9.4",
3
+ "version": "4.10.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"