@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.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +5 -1
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +8 -2
- package/dist/_internal/client/features/layout/sidebarPositionClasses.d.ts +20 -0
- package/dist/_internal/client/features/layout/sidebarPositionClasses.js +20 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.d.ts +21 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.js +81 -0
- package/dist/_internal/client/hand-mode/index.d.ts +2 -0
- package/dist/_internal/client/hand-mode/index.js +1 -0
- package/dist/_internal/server/features/auctions/actions.js +3 -2
- package/dist/_internal/server/features/auctions/service.d.ts +4 -3
- package/dist/_internal/server/features/auctions/service.js +6 -6
- package/dist/_internal/server/features/bundles/data.d.ts +9 -0
- package/dist/_internal/server/features/bundles/data.js +14 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
- package/dist/_internal/server/features/catalogue/og.js +27 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +43 -0
- package/dist/_internal/server/features/checkout/actions.js +172 -81
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +12 -0
- package/dist/_internal/server/features/grouped/data.js +26 -1
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/products/data.d.ts +23 -0
- package/dist/_internal/server/features/products/data.js +159 -0
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/seo/robots.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +70 -5
- package/dist/_internal/shared/config/schema.d.ts +1 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
- package/dist/_internal/shared/features/auctions/config.js +31 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/art/config.js +1 -1
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/classified/config.js +2 -2
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +2 -2
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +2 -2
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
- package/dist/client.d.ts +4 -6
- package/dist/client.js +10 -4
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/about/components/AboutView.js +2 -2
- package/dist/features/about/components/DeveloperView.js +2 -2
- package/dist/features/about/schemas/firestore.d.ts +1 -0
- package/dist/features/account/components/UserSidebar.js +8 -2
- package/dist/features/account/hooks/useProfile.d.ts +2 -0
- package/dist/features/admin/components/AdminSidebar.js +8 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
- package/dist/features/admin/schemas/firestore.d.ts +9 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/actions/bid-actions.js +4 -1
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
- package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
- package/dist/features/auth/schemas/firestore.d.ts +3 -0
- package/dist/features/cart/components/CartDrawer.js +4 -1
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
- package/dist/features/catalogue/index.d.ts +2 -0
- package/dist/features/catalogue/index.js +1 -0
- package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
- package/dist/features/categories/components/BundleDetailView.js +3 -2
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
- package/dist/features/contact/components/ContactPageView.js +8 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
- package/dist/features/homepage/components/BrandsSection.js +3 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/SectionCarousel.js +5 -2
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
- package/dist/features/layout/BackToTop.js +3 -1
- package/dist/features/layout/BottomActions.js +4 -3
- package/dist/features/layout/BottomActionsContext.d.ts +10 -0
- package/dist/features/layout/BottomActionsContext.js +5 -1
- package/dist/features/layout/SidebarLayout.js +7 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
- package/dist/features/media/MediaVideo.js +17 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
- package/dist/features/products/components/ProductDetailPageView.js +11 -116
- package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
- package/dist/features/products/components/RelatedItemsSection.js +17 -0
- package/dist/features/seller/components/SellerProductShell.js +1 -1
- package/dist/features/seller/components/SellerSidebar.js +8 -2
- package/dist/features/shell/QuickFormDrawer.js +3 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +114 -3
- package/dist/hand-mode/index.d.ts +12 -0
- package/dist/hand-mode/index.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/react/contexts/SessionContext.d.ts +5 -0
- package/dist/react/contexts/SessionContext.js +2 -0
- package/dist/seed/bids-seed-data.js +48 -3
- package/dist/seed/grouped-listings-seed-data.js +167 -0
- package/dist/seed/payouts-seed-data.js +4 -4
- package/dist/seed/products-art-seed-data.js +188 -5
- package/dist/seed/products-auctions-seed-data.js +215 -6
- package/dist/seed/products-classifieds-seed-data.js +255 -8
- package/dist/seed/products-digital-codes-seed-data.js +258 -19
- package/dist/seed/products-live-items-seed-data.js +143 -10
- package/dist/seed/products-preorders-seed-data.js +213 -8
- package/dist/seed/products-prize-draws-seed-data.js +198 -0
- package/dist/seed/products-standard-seed-data.js +141 -0
- package/dist/seed/products-stickers-seed-data.js +178 -5
- package/dist/seed/site-settings-seed-data.js +13 -31
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +5 -2
- package/dist/server-entry.js +5 -2
- package/dist/server.d.ts +4 -2
- package/dist/server.js +4 -2
- package/dist/styles.css +30 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Drawer.js +7 -4
- package/dist/ui/components/FilterDrawer.js +1 -1
- package/dist/ui/components/HandModeRow.d.ts +12 -0
- package/dist/ui/components/HandModeRow.js +18 -0
- package/dist/ui/components/ImageLightbox.js +37 -3
- package/dist/ui/components/ImageLightbox.style.css +28 -0
- package/dist/ui/components/Layout.d.ts +5 -1
- package/dist/ui/components/Layout.js +2 -1
- package/dist/ui/components/Layout.style.css +1 -0
- package/dist/ui/components/ListingFilterDrawer.js +3 -1
- package/dist/ui/components/Modal.js +3 -1
- package/dist/ui/components/SideDrawer.js +8 -5
- package/dist/ui/components/SideModal.js +7 -4
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/media-url.d.ts +9 -0
- package/dist/utils/media-url.js +39 -0
- 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
|
|
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[
|
|
59
|
-
const isBottom =
|
|
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
|
|
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:
|
|
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],
|
|
@@ -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:
|
|
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
|
|
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
|
-
...(
|
|
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 =
|
|
151
|
-
const slideX =
|
|
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
|
|
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 =
|
|
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--${
|
|
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
|
}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/media-url.js
CHANGED
|
@@ -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
|
+
}
|