@raydenui/ui 0.9.7 → 0.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/Table-DCNSxua1.d.cts +31 -0
- package/dist/Table-DCNSxua1.d.ts +31 -0
- package/dist/blocks/ApplicationShellBlock.cjs +157 -0
- package/dist/blocks/ApplicationShellBlock.js +154 -0
- package/dist/blocks/CheckoutReviewBlock.cjs +16 -0
- package/dist/blocks/CheckoutReviewBlock.js +13 -0
- package/dist/blocks/CommandPaletteBlock.cjs +334 -0
- package/dist/blocks/CommandPaletteBlock.js +331 -0
- package/dist/blocks/CreateAccountBlock.cjs +147 -0
- package/dist/blocks/CreateAccountBlock.js +143 -0
- package/dist/blocks/EmptyStateBlock.cjs +19 -0
- package/dist/blocks/EmptyStateBlock.js +16 -0
- package/dist/blocks/FeatureOverviewBlock.cjs +54 -0
- package/dist/blocks/FeatureOverviewBlock.js +51 -0
- package/dist/blocks/HeaderBlock.cjs +230 -0
- package/dist/blocks/HeaderBlock.js +227 -0
- package/dist/blocks/KpiOverviewBlock.cjs +63 -0
- package/dist/blocks/KpiOverviewBlock.js +60 -0
- package/dist/blocks/LoginBlock.cjs +66 -0
- package/dist/blocks/LoginBlock.js +63 -0
- package/dist/blocks/NotificationsBlock.cjs +17 -0
- package/dist/blocks/NotificationsBlock.js +14 -0
- package/dist/blocks/PageHeaderBlock.cjs +121 -0
- package/dist/blocks/PageHeaderBlock.js +118 -0
- package/dist/blocks/PricingPlansBlock.cjs +146 -0
- package/dist/blocks/PricingPlansBlock.js +143 -0
- package/dist/blocks/ProductCollectionBlock.cjs +37 -0
- package/dist/blocks/ProductCollectionBlock.js +34 -0
- package/dist/blocks/ProductDetailBlock.cjs +30 -0
- package/dist/blocks/ProductDetailBlock.js +27 -0
- package/dist/blocks/ProductHeroBlock.cjs +83 -0
- package/dist/blocks/ProductHeroBlock.js +80 -0
- package/dist/blocks/ProfileSettingsBlock.cjs +130 -0
- package/dist/blocks/ProfileSettingsBlock.js +126 -0
- package/dist/blocks/QuickSendBlock.cjs +11 -0
- package/dist/blocks/QuickSendBlock.js +8 -0
- package/dist/blocks/RecentTransactionsBlock.cjs +13 -0
- package/dist/blocks/RecentTransactionsBlock.js +10 -0
- package/dist/blocks/SearchableTableBlock.cjs +91 -0
- package/dist/blocks/SearchableTableBlock.js +88 -0
- package/dist/blocks/ShoppingCartBlock.cjs +16 -0
- package/dist/blocks/ShoppingCartBlock.js +13 -0
- package/dist/blocks/SiteFooterBlock.cjs +69 -0
- package/dist/blocks/SiteFooterBlock.js +66 -0
- package/dist/blocks/TableBlock.cjs +20 -0
- package/dist/blocks/TableBlock.js +17 -0
- package/dist/blocks/TaskListBlock.cjs +198 -0
- package/dist/blocks/TaskListBlock.js +195 -0
- package/dist/blocks/WorkspaceSwitcherBlock.cjs +291 -0
- package/dist/blocks/WorkspaceSwitcherBlock.js +288 -0
- package/dist/blocks/commerce.cjs +44 -0
- package/dist/blocks/commerce.js +35 -0
- package/dist/blocks/index.cjs +53 -0
- package/dist/blocks/index.js +24 -0
- package/dist/blocks.d.cts +1820 -0
- package/dist/blocks.d.ts +1820 -0
- package/dist/chart.cjs +13 -0
- package/dist/chart.d.cts +59 -0
- package/dist/chart.d.ts +59 -0
- package/dist/chart.js +5 -0
- package/dist/components/Accordion/Accordion.cjs +94 -0
- package/dist/components/Accordion/Accordion.js +91 -0
- package/dist/components/Accordion/index.cjs +8 -0
- package/dist/components/Accordion/index.js +1 -0
- package/dist/components/ActivityFeed/ActivityContent.cjs +40 -0
- package/dist/components/ActivityFeed/ActivityContent.js +37 -0
- package/dist/components/ActivityFeed/ActivityItem.cjs +19 -0
- package/dist/components/ActivityFeed/ActivityItem.js +16 -0
- package/dist/components/ActivityFeed/index.cjs +7 -0
- package/dist/components/ActivityFeed/index.js +2 -0
- package/dist/components/Alert/Alert.cjs +42 -0
- package/dist/components/Alert/Alert.js +39 -0
- package/dist/components/Alert/index.cjs +5 -0
- package/dist/components/Alert/index.js +1 -0
- package/dist/components/Avatar/Avatar.cjs +101 -0
- package/dist/components/Avatar/Avatar.js +98 -0
- package/dist/components/Avatar/index.cjs +6 -0
- package/dist/components/Avatar/index.js +1 -0
- package/dist/components/Badge/Badge.cjs +53 -0
- package/dist/components/Badge/Badge.js +50 -0
- package/dist/components/Badge/index.cjs +5 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Banner/Banner.cjs +83 -0
- package/dist/components/Banner/Banner.js +80 -0
- package/dist/components/Banner/index.cjs +5 -0
- package/dist/components/Banner/index.js +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.cjs +31 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +28 -0
- package/dist/components/Breadcrumb/index.cjs +5 -0
- package/dist/components/Breadcrumb/index.js +1 -0
- package/dist/components/Button/Button.cjs +88 -0
- package/dist/components/Button/Button.js +85 -0
- package/dist/components/Button/index.cjs +5 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.cjs +8 -0
- package/dist/components/ButtonGroup/ButtonGroup.js +5 -0
- package/dist/components/ButtonGroup/ButtonGroupItem.cjs +15 -0
- package/dist/components/ButtonGroup/ButtonGroupItem.js +12 -0
- package/dist/components/ButtonGroup/index.cjs +7 -0
- package/dist/components/ButtonGroup/index.js +2 -0
- package/dist/components/Card/Card.cjs +94 -0
- package/dist/components/Card/Card.js +91 -0
- package/dist/components/Card/index.cjs +9 -0
- package/dist/components/Card/index.js +1 -0
- package/dist/components/Chart/Chart.cjs +160 -0
- package/dist/components/Chart/Chart.js +153 -0
- package/dist/components/Chart/index.cjs +10 -0
- package/dist/components/Chart/index.js +2 -0
- package/dist/components/Chart/theme.cjs +67 -0
- package/dist/components/Chart/theme.js +62 -0
- package/dist/components/Chip/Chip.cjs +18 -0
- package/dist/components/Chip/Chip.js +15 -0
- package/dist/components/Chip/index.cjs +5 -0
- package/dist/components/Chip/index.js +1 -0
- package/dist/components/Counter/Counter.cjs +74 -0
- package/dist/components/Counter/Counter.js +71 -0
- package/dist/components/Counter/index.cjs +6 -0
- package/dist/components/Counter/index.js +1 -0
- package/dist/components/DatePicker/DatePicker.cjs +343 -0
- package/dist/components/DatePicker/DatePicker.js +340 -0
- package/dist/components/DatePicker/index.cjs +5 -0
- package/dist/components/DatePicker/index.js +1 -0
- package/dist/components/Divider/Divider.cjs +31 -0
- package/dist/components/Divider/Divider.js +28 -0
- package/dist/components/Divider/index.cjs +5 -0
- package/dist/components/Divider/index.js +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.cjs +108 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +105 -0
- package/dist/components/DropdownMenu/index.cjs +11 -0
- package/dist/components/DropdownMenu/index.js +1 -0
- package/dist/components/EmptyStateIllustration/EmptyStateIllustration.cjs +42 -0
- package/dist/components/EmptyStateIllustration/EmptyStateIllustration.js +39 -0
- package/dist/components/EmptyStateIllustration/illustrations.cjs +3322 -0
- package/dist/components/EmptyStateIllustration/illustrations.js +3319 -0
- package/dist/components/EmptyStateIllustration/index.cjs +7 -0
- package/dist/components/EmptyStateIllustration/index.js +2 -0
- package/dist/components/FileUpload/FileUpload.cjs +116 -0
- package/dist/components/FileUpload/FileUpload.js +113 -0
- package/dist/components/FileUpload/FileUploadDropZone.cjs +58 -0
- package/dist/components/FileUpload/FileUploadDropZone.js +55 -0
- package/dist/components/FileUpload/FileUploadIcons.cjs +55 -0
- package/dist/components/FileUpload/FileUploadIcons.js +50 -0
- package/dist/components/FileUpload/FileUploadItem.cjs +34 -0
- package/dist/components/FileUpload/FileUploadItem.js +31 -0
- package/dist/components/FileUpload/index.cjs +9 -0
- package/dist/components/FileUpload/index.js +3 -0
- package/dist/components/FormControl/Checkbox.cjs +27 -0
- package/dist/components/FormControl/Checkbox.js +24 -0
- package/dist/components/FormControl/Radio.cjs +15 -0
- package/dist/components/FormControl/Radio.js +12 -0
- package/dist/components/FormControl/Toggle.cjs +15 -0
- package/dist/components/FormControl/Toggle.js +12 -0
- package/dist/components/FormControl/index.cjs +9 -0
- package/dist/components/FormControl/index.js +3 -0
- package/dist/components/Icon/Icon.cjs +70 -0
- package/dist/components/Icon/Icon.js +34 -0
- package/dist/components/Icon/catalog.cjs +1672 -0
- package/dist/components/Icon/catalog.js +1669 -0
- package/dist/components/Icon/icons.cjs +5621 -0
- package/dist/components/Icon/icons.js +5610 -0
- package/dist/components/Icon/index.cjs +8 -0
- package/dist/components/Icon/index.js +2 -0
- package/dist/components/Input/Input.cjs +64 -0
- package/dist/components/Input/Input.js +61 -0
- package/dist/components/Input/index.cjs +5 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/components/MetricsCard/MetricsCard.cjs +107 -0
- package/dist/components/MetricsCard/MetricsCard.js +104 -0
- package/dist/components/MetricsCard/index.cjs +5 -0
- package/dist/components/MetricsCard/index.js +1 -0
- package/dist/components/Modal/Modal.cjs +126 -0
- package/dist/components/Modal/Modal.js +123 -0
- package/dist/components/Modal/index.cjs +5 -0
- package/dist/components/Modal/index.js +1 -0
- package/dist/components/Pagination/Pagination.cjs +42 -0
- package/dist/components/Pagination/Pagination.js +39 -0
- package/dist/components/Pagination/index.cjs +5 -0
- package/dist/components/Pagination/index.js +1 -0
- package/dist/components/ProgressBar/ProgressBar.cjs +34 -0
- package/dist/components/ProgressBar/ProgressBar.js +31 -0
- package/dist/components/ProgressBar/index.cjs +5 -0
- package/dist/components/ProgressBar/index.js +1 -0
- package/dist/components/ProgressCircle/ProgressCircle.cjs +34 -0
- package/dist/components/ProgressCircle/ProgressCircle.js +31 -0
- package/dist/components/ProgressCircle/index.cjs +5 -0
- package/dist/components/ProgressCircle/index.js +1 -0
- package/dist/components/Select/Select.cjs +84 -0
- package/dist/components/Select/Select.js +80 -0
- package/dist/components/Select/SelectOption.cjs +26 -0
- package/dist/components/Select/SelectOption.js +23 -0
- package/dist/components/Select/index.cjs +7 -0
- package/dist/components/Select/index.js +2 -0
- package/dist/components/SidebarMenu/SidebarMenu.cjs +85 -0
- package/dist/components/SidebarMenu/SidebarMenu.js +81 -0
- package/dist/components/SidebarMenu/SidebarMenuItem.cjs +126 -0
- package/dist/components/SidebarMenu/SidebarMenuItem.js +122 -0
- package/dist/components/SidebarMenu/SidebarMenuSection.cjs +14 -0
- package/dist/components/SidebarMenu/SidebarMenuSection.js +11 -0
- package/dist/components/SidebarMenu/index.cjs +12 -0
- package/dist/components/SidebarMenu/index.js +4 -0
- package/dist/components/Slider/Slider.cjs +161 -0
- package/dist/components/Slider/Slider.js +158 -0
- package/dist/components/Slider/index.cjs +6 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/components/Spinner/Spinner.cjs +93 -0
- package/dist/components/Spinner/Spinner.js +90 -0
- package/dist/components/Spinner/index.cjs +5 -0
- package/dist/components/Spinner/index.js +1 -0
- package/dist/components/Stepper/Stepper.cjs +82 -0
- package/dist/components/Stepper/Stepper.js +79 -0
- package/dist/components/Stepper/index.cjs +7 -0
- package/dist/components/Stepper/index.js +1 -0
- package/dist/components/Table/Table.cjs +25 -0
- package/dist/components/Table/Table.js +22 -0
- package/dist/components/Table/index.cjs +10 -0
- package/dist/components/Table/index.js +1 -0
- package/dist/components/Table/useTableSelection.cjs +60 -0
- package/dist/components/Table/useTableSelection.js +57 -0
- package/dist/components/Tabs/Tab.cjs +70 -0
- package/dist/components/Tabs/Tab.js +67 -0
- package/dist/components/Tabs/Tabs.cjs +133 -0
- package/dist/components/Tabs/Tabs.js +129 -0
- package/dist/components/Tabs/index.cjs +7 -0
- package/dist/components/Tabs/index.js +2 -0
- package/dist/components/Tooltip/Tooltip.cjs +101 -0
- package/dist/components/Tooltip/Tooltip.js +98 -0
- package/dist/components/Tooltip/index.cjs +5 -0
- package/dist/components/Tooltip/index.js +1 -0
- package/dist/context/ThemeContext.cjs +97 -0
- package/dist/context/ThemeContext.js +92 -0
- package/dist/fonts/README.md +13 -0
- package/dist/fonts/hanken-grotesk-cyrillic-ext-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-cyrillic-ext-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-ext-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-ext-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-vietnamese-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-vietnamese-normal.woff2 +0 -0
- package/dist/fonts/hankengrotesk-LICENSE.txt +94 -0
- package/dist/fonts/manrope-LICENSE.txt +93 -0
- package/dist/fonts/manrope-cyrillic-ext-normal.woff2 +0 -0
- package/dist/fonts/manrope-cyrillic-normal.woff2 +0 -0
- package/dist/fonts/manrope-greek-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-ext-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-normal.woff2 +0 -0
- package/dist/fonts/manrope-vietnamese-normal.woff2 +0 -0
- package/dist/fonts.css +148 -0
- package/dist/hooks/form/index.cjs +11 -0
- package/dist/hooks/form/index.js +3 -0
- package/dist/hooks/form/useRaydenFormControl.cjs +81 -0
- package/dist/hooks/form/useRaydenFormControl.js +76 -0
- package/dist/hooks/form/useRaydenInput.cjs +38 -0
- package/dist/hooks/form/useRaydenInput.js +35 -0
- package/dist/hooks/form/useRaydenSelect.cjs +45 -0
- package/dist/hooks/form/useRaydenSelect.js +42 -0
- package/dist/hooks/index.cjs +17 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useBodyScrollLock.cjs +24 -0
- package/dist/hooks/useBodyScrollLock.js +21 -0
- package/dist/hooks/useCollisionAwareSide.cjs +72 -0
- package/dist/hooks/useCollisionAwareSide.js +69 -0
- package/dist/hooks/useControllableValue.cjs +16 -0
- package/dist/hooks/useControllableValue.js +13 -0
- package/dist/hooks/useDismissableLayer.cjs +100 -0
- package/dist/hooks/useDismissableLayer.js +97 -0
- package/dist/hooks/usePopupList.cjs +101 -0
- package/dist/hooks/usePopupList.js +98 -0
- package/dist/icons-8yD7I5jO.d.cts +430 -0
- package/dist/icons-8yD7I5jO.d.ts +430 -0
- package/dist/icons.cjs +2111 -418
- package/dist/icons.d.cts +1256 -429
- package/dist/icons.d.ts +1256 -429
- package/dist/icons.js +2089 -1
- package/dist/index.cjs +128 -15046
- package/dist/index.d.cts +697 -662
- package/dist/index.d.ts +697 -662
- package/dist/index.js +43 -14962
- package/dist/motion/Collapse.cjs +28 -0
- package/dist/motion/Collapse.js +25 -0
- package/dist/motion/MotionProvider.cjs +43 -0
- package/dist/motion/MotionProvider.js +38 -0
- package/dist/motion/Pressable.cjs +46 -0
- package/dist/motion/Pressable.js +43 -0
- package/dist/motion/Reveal.cjs +19 -0
- package/dist/motion/Reveal.js +16 -0
- package/dist/motion/SharedLayout.cjs +46 -0
- package/dist/motion/SharedLayout.js +43 -0
- package/dist/motion/index.cjs +19 -0
- package/dist/motion/index.js +6 -0
- package/dist/motion/presence.cjs +58 -0
- package/dist/motion/presence.js +55 -0
- package/dist/motion/presets.cjs +88 -0
- package/dist/motion/presets.js +84 -0
- package/dist/motion.d.cts +51 -0
- package/dist/motion.d.ts +51 -0
- package/dist/preset.cjs +356 -195
- package/dist/preset.d.cts +111 -32
- package/dist/preset.d.ts +111 -32
- package/dist/preset.js +353 -187
- package/dist/presets-Ba5QndHg.d.cts +144 -0
- package/dist/presets-Ba5QndHg.d.ts +144 -0
- package/dist/styles.css +1 -1
- package/dist/utils/cn.cjs +38 -0
- package/dist/utils/cn.js +35 -0
- package/dist/utils/resolveIcon.cjs +18 -0
- package/dist/utils/resolveIcon.js +15 -0
- package/package.json +79 -21
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProductCollectionBlock = ProductCollectionBlock;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const Button_1 = require("../components/Button/index.cjs");
|
|
7
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
8
|
+
const commerce_1 = require("./commerce.cjs");
|
|
9
|
+
function ProductCollectionBlock({ title = "Thoughtful everyday essentials", description, products, layout = "grid", onSelectProduct, onAddToCart, pendingProductIds = [], status = "ready", errorMessage = "We couldn’t load the collection.", onRetry, emptyMessage = "No products are available yet.", currency, locale, ...frame }) {
|
|
10
|
+
const id = (0, react_1.useId)();
|
|
11
|
+
const [query, setQuery] = (0, react_1.useState)("");
|
|
12
|
+
const [category, setCategory] = (0, react_1.useState)("");
|
|
13
|
+
const [sort, setSort] = (0, react_1.useState)("featured");
|
|
14
|
+
const ProductHeading = `h${Math.min(Number((frame.headingLevel ?? "h2").slice(1)) + 1, 6)}`;
|
|
15
|
+
const categories = [...new Set(products.flatMap((p) => (p.category ? [p.category] : [])))];
|
|
16
|
+
const currentCategory = categories.includes(category) ? category : "";
|
|
17
|
+
const visible = products.filter((p) => (!currentCategory || p.category === currentCategory) &&
|
|
18
|
+
`${p.name} ${p.description ?? ""}`
|
|
19
|
+
.toLocaleLowerCase(locale ?? "en-GB")
|
|
20
|
+
.includes(query.trim().toLocaleLowerCase(locale ?? "en-GB")));
|
|
21
|
+
if (sort !== "featured")
|
|
22
|
+
visible.sort((a, b) => (sort === "price-low" ? a.price - b.price : b.price - a.price));
|
|
23
|
+
return ((0, jsx_runtime_1.jsx)(commerce_1.CommerceFrame, { ...frame, title: title, description: description, busy: status === "loading", children: status === "loading" ? ((0, jsx_runtime_1.jsx)("p", { role: "status", className: "border-y border-surface-border py-12 text-grey-600", children: "Loading collection\u2026" })) : status === "error" ? ((0, jsx_runtime_1.jsxs)("div", { role: "alert", className: "space-y-4", children: [(0, jsx_runtime_1.jsx)("p", { children: errorMessage }), onRetry && ((0, jsx_runtime_1.jsx)(Button_1.Button, { className: "h-auto min-h-11", variant: "secondary", onClick: onRetry, children: "Try again" }))] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-5 grid grid-cols-2 items-end gap-3 border-y border-surface-border py-4 @min-[640px]:grid-cols-[minmax(160px,1fr)_minmax(140px,180px)_minmax(140px,180px)]", children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: `${id}-search`, className: "col-span-2 flex min-w-0 flex-col gap-2 text-sm text-grey-600 @min-[640px]:col-span-1", children: ["Search products", (0, jsx_runtime_1.jsx)("input", { id: `${id}-search`, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Find your next favourite", className: (0, cn_1.cn)(commerce_1.commerceControl, "w-full cursor-text") })] }), (0, jsx_runtime_1.jsxs)("label", { htmlFor: `${id}-category`, className: "flex min-w-0 flex-col gap-2 text-sm text-grey-600", children: ["Category", (0, jsx_runtime_1.jsxs)("select", { id: `${id}-category`, value: currentCategory, onChange: (e) => setCategory(e.target.value), className: (0, cn_1.cn)(commerce_1.commerceControl, "w-full text-sm"), children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "All categories" }), categories.map((c) => ((0, jsx_runtime_1.jsx)("option", { children: c }, c)))] })] }), (0, jsx_runtime_1.jsxs)("label", { htmlFor: `${id}-sort`, className: "flex min-w-0 flex-col gap-2 text-sm text-grey-600", children: ["Sort by", (0, jsx_runtime_1.jsxs)("select", { id: `${id}-sort`, value: sort, onChange: (e) => setSort(e.target.value), className: (0, cn_1.cn)(commerce_1.commerceControl, "w-full text-sm"), children: [(0, jsx_runtime_1.jsx)("option", { value: "featured", children: "Featured" }), (0, jsx_runtime_1.jsx)("option", { value: "price-low", children: "Price: low to high" }), (0, jsx_runtime_1.jsx)("option", { value: "price-high", children: "Price: high to low" })] })] })] }), (0, jsx_runtime_1.jsxs)("p", { role: "status", className: "mb-5 text-sm text-grey-600", children: [visible.length, " ", visible.length === 1 ? "product" : "products"] }), visible.length ? ((0, jsx_runtime_1.jsx)("ul", { className: (0, cn_1.cn)(layout === "grid"
|
|
24
|
+
? "grid gap-x-4 gap-y-8 @min-[300px]:grid-cols-2 @min-[880px]:grid-cols-4 @min-[760px]:gap-x-6"
|
|
25
|
+
: "divide-y divide-surface-border"), children: visible.map((product) => {
|
|
26
|
+
const productId = `${id}-${product.id}`;
|
|
27
|
+
const visual = ((0, jsx_runtime_1.jsxs)("div", { className: "relative overflow-hidden rounded-lg", children: [(0, jsx_runtime_1.jsx)(commerce_1.CommercePhoto, { image: product.image, className: (0, cn_1.cn)("motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.025]", layout === "grid" && "aspect-[4/5]") }), layout === "grid" && product.badge && ((0, jsx_runtime_1.jsx)("span", { className: "absolute top-3 left-3 max-w-[calc(100%-1.5rem)] rounded bg-surface px-2 py-1 text-left text-sm font-medium text-grey-900", children: product.badge }))] }));
|
|
28
|
+
return ((0, jsx_runtime_1.jsxs)("li", { className: (0, cn_1.cn)("min-w-0", layout === "grid"
|
|
29
|
+
? "flex flex-col"
|
|
30
|
+
: "grid grid-cols-[88px_minmax(0,1fr)] items-center gap-4 py-5 @min-[560px]:grid-cols-[112px_minmax(0,1fr)_auto]"), children: [product.href ? ((0, jsx_runtime_1.jsx)("a", { href: product.href, "aria-labelledby": productId, className: "group block rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-action-primary", children: visual })) : onSelectProduct ? ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => onSelectProduct(product), "aria-labelledby": productId, className: "group block w-full cursor-pointer rounded-lg text-left focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-action-primary", children: visual })) : (visual), (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("min-w-0", layout === "grid" && "flex flex-1 flex-col pt-4"), children: [product.category && ((0, jsx_runtime_1.jsx)("p", { className: "mb-1.5 text-sm text-grey-600", children: product.category })), (0, jsx_runtime_1.jsx)(ProductHeading, { id: productId, className: "text-base leading-snug font-semibold tracking-tight", children: product.name }), product.description && ((0, jsx_runtime_1.jsx)("p", { className: "mt-2 text-sm leading-relaxed text-grey-600", children: product.description })), (0, jsx_runtime_1.jsxs)("div", { className: "mt-auto flex flex-wrap items-center justify-between gap-2 pt-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "font-medium tabular-nums", children: (0, commerce_1.formatCommerceMoney)(product.price, currency, locale) }), product.stock === 0 && (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-grey-600", children: "Sold out" })] })] }), onAddToCart && ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "grey", appearance: "outlined", className: (0, cn_1.cn)("h-auto min-h-11 whitespace-normal", layout === "grid"
|
|
31
|
+
? "mt-4 w-full"
|
|
32
|
+
: "col-start-2 justify-self-start @min-[560px]:col-start-3 @min-[560px]:row-start-1"), disabled: product.stock === 0 || pendingProductIds.includes(product.id), onClick: () => onAddToCart(product), "aria-label": `Add ${product.name} to bag`, children: pendingProductIds.includes(product.id) ? "Adding…" : "Add to bag" }))] }, product.id));
|
|
33
|
+
}) })) : ((0, jsx_runtime_1.jsxs)("div", { className: "border-b border-surface-border px-4 py-12 text-center", children: [(0, jsx_runtime_1.jsx)("p", { children: products.length ? "No products match your search." : emptyMessage }), products.length > 0 && ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "secondary", className: "mt-4 h-auto min-h-11", onClick: () => {
|
|
34
|
+
setQuery("");
|
|
35
|
+
setCategory("");
|
|
36
|
+
}, children: "Clear filters" }))] }))] })) }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
import { Button } from "../components/Button/index.js";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
import { CommerceFrame, CommercePhoto, commerceControl, formatCommerceMoney, } from "./commerce.js";
|
|
6
|
+
export function ProductCollectionBlock({ title = "Thoughtful everyday essentials", description, products, layout = "grid", onSelectProduct, onAddToCart, pendingProductIds = [], status = "ready", errorMessage = "We couldn’t load the collection.", onRetry, emptyMessage = "No products are available yet.", currency, locale, ...frame }) {
|
|
7
|
+
const id = useId();
|
|
8
|
+
const [query, setQuery] = useState("");
|
|
9
|
+
const [category, setCategory] = useState("");
|
|
10
|
+
const [sort, setSort] = useState("featured");
|
|
11
|
+
const ProductHeading = `h${Math.min(Number((frame.headingLevel ?? "h2").slice(1)) + 1, 6)}`;
|
|
12
|
+
const categories = [...new Set(products.flatMap((p) => (p.category ? [p.category] : [])))];
|
|
13
|
+
const currentCategory = categories.includes(category) ? category : "";
|
|
14
|
+
const visible = products.filter((p) => (!currentCategory || p.category === currentCategory) &&
|
|
15
|
+
`${p.name} ${p.description ?? ""}`
|
|
16
|
+
.toLocaleLowerCase(locale ?? "en-GB")
|
|
17
|
+
.includes(query.trim().toLocaleLowerCase(locale ?? "en-GB")));
|
|
18
|
+
if (sort !== "featured")
|
|
19
|
+
visible.sort((a, b) => (sort === "price-low" ? a.price - b.price : b.price - a.price));
|
|
20
|
+
return (_jsx(CommerceFrame, { ...frame, title: title, description: description, busy: status === "loading", children: status === "loading" ? (_jsx("p", { role: "status", className: "border-y border-surface-border py-12 text-grey-600", children: "Loading collection\u2026" })) : status === "error" ? (_jsxs("div", { role: "alert", className: "space-y-4", children: [_jsx("p", { children: errorMessage }), onRetry && (_jsx(Button, { className: "h-auto min-h-11", variant: "secondary", onClick: onRetry, children: "Try again" }))] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "mb-5 grid grid-cols-2 items-end gap-3 border-y border-surface-border py-4 @min-[640px]:grid-cols-[minmax(160px,1fr)_minmax(140px,180px)_minmax(140px,180px)]", children: [_jsxs("label", { htmlFor: `${id}-search`, className: "col-span-2 flex min-w-0 flex-col gap-2 text-sm text-grey-600 @min-[640px]:col-span-1", children: ["Search products", _jsx("input", { id: `${id}-search`, type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Find your next favourite", className: cn(commerceControl, "w-full cursor-text") })] }), _jsxs("label", { htmlFor: `${id}-category`, className: "flex min-w-0 flex-col gap-2 text-sm text-grey-600", children: ["Category", _jsxs("select", { id: `${id}-category`, value: currentCategory, onChange: (e) => setCategory(e.target.value), className: cn(commerceControl, "w-full text-sm"), children: [_jsx("option", { value: "", children: "All categories" }), categories.map((c) => (_jsx("option", { children: c }, c)))] })] }), _jsxs("label", { htmlFor: `${id}-sort`, className: "flex min-w-0 flex-col gap-2 text-sm text-grey-600", children: ["Sort by", _jsxs("select", { id: `${id}-sort`, value: sort, onChange: (e) => setSort(e.target.value), className: cn(commerceControl, "w-full text-sm"), children: [_jsx("option", { value: "featured", children: "Featured" }), _jsx("option", { value: "price-low", children: "Price: low to high" }), _jsx("option", { value: "price-high", children: "Price: high to low" })] })] })] }), _jsxs("p", { role: "status", className: "mb-5 text-sm text-grey-600", children: [visible.length, " ", visible.length === 1 ? "product" : "products"] }), visible.length ? (_jsx("ul", { className: cn(layout === "grid"
|
|
21
|
+
? "grid gap-x-4 gap-y-8 @min-[300px]:grid-cols-2 @min-[880px]:grid-cols-4 @min-[760px]:gap-x-6"
|
|
22
|
+
: "divide-y divide-surface-border"), children: visible.map((product) => {
|
|
23
|
+
const productId = `${id}-${product.id}`;
|
|
24
|
+
const visual = (_jsxs("div", { className: "relative overflow-hidden rounded-lg", children: [_jsx(CommercePhoto, { image: product.image, className: cn("motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.025]", layout === "grid" && "aspect-[4/5]") }), layout === "grid" && product.badge && (_jsx("span", { className: "absolute top-3 left-3 max-w-[calc(100%-1.5rem)] rounded bg-surface px-2 py-1 text-left text-sm font-medium text-grey-900", children: product.badge }))] }));
|
|
25
|
+
return (_jsxs("li", { className: cn("min-w-0", layout === "grid"
|
|
26
|
+
? "flex flex-col"
|
|
27
|
+
: "grid grid-cols-[88px_minmax(0,1fr)] items-center gap-4 py-5 @min-[560px]:grid-cols-[112px_minmax(0,1fr)_auto]"), children: [product.href ? (_jsx("a", { href: product.href, "aria-labelledby": productId, className: "group block rounded-lg focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-action-primary", children: visual })) : onSelectProduct ? (_jsx("button", { type: "button", onClick: () => onSelectProduct(product), "aria-labelledby": productId, className: "group block w-full cursor-pointer rounded-lg text-left focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-action-primary", children: visual })) : (visual), _jsxs("div", { className: cn("min-w-0", layout === "grid" && "flex flex-1 flex-col pt-4"), children: [product.category && (_jsx("p", { className: "mb-1.5 text-sm text-grey-600", children: product.category })), _jsx(ProductHeading, { id: productId, className: "text-base leading-snug font-semibold tracking-tight", children: product.name }), product.description && (_jsx("p", { className: "mt-2 text-sm leading-relaxed text-grey-600", children: product.description })), _jsxs("div", { className: "mt-auto flex flex-wrap items-center justify-between gap-2 pt-3", children: [_jsx("p", { className: "font-medium tabular-nums", children: formatCommerceMoney(product.price, currency, locale) }), product.stock === 0 && _jsx("p", { className: "text-sm text-grey-600", children: "Sold out" })] })] }), onAddToCart && (_jsx(Button, { variant: "grey", appearance: "outlined", className: cn("h-auto min-h-11 whitespace-normal", layout === "grid"
|
|
28
|
+
? "mt-4 w-full"
|
|
29
|
+
: "col-start-2 justify-self-start @min-[560px]:col-start-3 @min-[560px]:row-start-1"), disabled: product.stock === 0 || pendingProductIds.includes(product.id), onClick: () => onAddToCart(product), "aria-label": `Add ${product.name} to bag`, children: pendingProductIds.includes(product.id) ? "Adding…" : "Add to bag" }))] }, product.id));
|
|
30
|
+
}) })) : (_jsxs("div", { className: "border-b border-surface-border px-4 py-12 text-center", children: [_jsx("p", { children: products.length ? "No products match your search." : emptyMessage }), products.length > 0 && (_jsx(Button, { variant: "secondary", className: "mt-4 h-auto min-h-11", onClick: () => {
|
|
31
|
+
setQuery("");
|
|
32
|
+
setCategory("");
|
|
33
|
+
}, children: "Clear filters" }))] }))] })) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProductDetailBlock = ProductDetailBlock;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const Button_1 = require("../components/Button/index.cjs");
|
|
7
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
8
|
+
const commerce_1 = require("./commerce.cjs");
|
|
9
|
+
/** Reset the selection when switching products. */
|
|
10
|
+
function ProductDetailBlock(props) {
|
|
11
|
+
return (0, jsx_runtime_1.jsx)(ProductDetailView, { ...props }, props.product.id);
|
|
12
|
+
}
|
|
13
|
+
function ProductDetailView({ product, title = product.name, description = product.description, images, layout = "split", options = [], optionLabel = "Finish", details = [], onAddToCart, pending = false, errorMessage, successMessage, deliveryNote, currency, locale, headingLevel, className, }) {
|
|
14
|
+
const id = (0, react_1.useId)();
|
|
15
|
+
const [optionId, setOptionId] = (0, react_1.useState)(options.find((o) => o.stock !== 0)?.id ?? options[0]?.id);
|
|
16
|
+
const [requestedQuantity, setQuantity] = (0, react_1.useState)(1);
|
|
17
|
+
const [imageIndex, setImageIndex] = (0, react_1.useState)(0);
|
|
18
|
+
const gallery = images?.length ? images : product.image ? [product.image] : [];
|
|
19
|
+
const activeImage = Math.min(imageIndex, Math.max(0, gallery.length - 1));
|
|
20
|
+
const option = options.find((o) => o.id === optionId) ?? options.find((o) => o.stock !== 0) ?? options[0];
|
|
21
|
+
const stock = Math.min(product.stock ?? 99, option?.stock ?? 99);
|
|
22
|
+
const quantity = Math.max(1, Math.min(requestedQuantity, stock));
|
|
23
|
+
return ((0, jsx_runtime_1.jsx)("section", { "aria-labelledby": `${id}-title`, "aria-busy": pending || undefined, className: (0, cn_1.cn)("@container w-full min-w-0 bg-surface text-grey-900 [overflow-wrap:anywhere]", className), children: (0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("grid items-start gap-8", layout === "split" &&
|
|
24
|
+
"@min-[700px]:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] @min-[900px]:gap-14"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 space-y-3", children: [(0, jsx_runtime_1.jsx)(commerce_1.CommercePhoto, { image: gallery[activeImage], priority: true, className: "aspect-[4/5]" }), gallery.length > 1 && ((0, jsx_runtime_1.jsx)("div", { role: "group", "aria-label": `Images of ${product.name}`, className: "flex flex-wrap gap-3", children: gallery.map((image, index) => ((0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": `View image ${index + 1}: ${image.alt}`, "aria-pressed": index === activeImage, onClick: () => setImageIndex(index), className: (0, cn_1.cn)("w-16 cursor-pointer rounded-lg border-2 p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary", index === activeImage
|
|
25
|
+
? "border-action-primary"
|
|
26
|
+
: "border-transparent hover:border-surface-border-strong"), children: (0, jsx_runtime_1.jsx)(commerce_1.CommercePhoto, { image: { ...image, alt: "" }, className: "rounded" }) }, `${image.src}-${index}`))) }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 @min-[700px]:py-2", children: [product.category && ((0, jsx_runtime_1.jsx)("p", { className: "mb-4 text-sm font-medium text-grey-600", children: product.category })), (0, jsx_runtime_1.jsx)(commerce_1.CommerceHeading, { id: `${id}-title`, title: title, description: description, headingLevel: headingLevel }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-6 flex flex-wrap items-center justify-between gap-3 border-b border-surface-border pb-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-2xl font-medium tracking-tight tabular-nums", children: (0, commerce_1.formatCommerceMoney)(product.price, currency, locale) }), (0, jsx_runtime_1.jsxs)("p", { className: "flex items-center gap-2 text-sm text-grey-600", children: [(0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", className: (0, cn_1.cn)("size-1.5 rounded-full", stock ? "bg-success-700" : "bg-grey-500") }), stock === 0 ? "Currently sold out" : "Available to order"] })] }), options.length > 0 && ((0, jsx_runtime_1.jsxs)("fieldset", { disabled: pending, className: "mt-6 space-y-3", children: [(0, jsx_runtime_1.jsxs)("legend", { className: "text-sm font-semibold", children: [optionLabel, option && ((0, jsx_runtime_1.jsxs)("span", { "aria-hidden": "true", className: "font-normal text-grey-600", children: [" ", "\u00B7 ", option.label] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-2", children: options.map((o) => ((0, jsx_runtime_1.jsxs)("label", { className: `${commerce_1.commerceControl} inline-flex items-center gap-2 has-[:checked]:border-action-primary has-[:checked]:bg-surface-muted has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50`, children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: `${id}-option`, value: o.id, checked: option?.id === o.id, disabled: o.stock === 0, onChange: () => {
|
|
27
|
+
setOptionId(o.id);
|
|
28
|
+
setQuantity(1);
|
|
29
|
+
}, className: "accent-action-primary" }), o.label, o.stock === 0 && " · Sold out"] }, o.id))) })] })), onAddToCart && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-6 space-y-3", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold", children: "Quantity" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap gap-3", children: [(0, jsx_runtime_1.jsx)(commerce_1.CommerceQuantity, { name: product.name, quantity: quantity, max: stock, disabled: pending || stock === 0, onChange: setQuantity }), (0, jsx_runtime_1.jsx)(Button_1.Button, { disabled: pending || stock === 0, className: "h-auto min-h-12 min-w-[140px] flex-1 whitespace-normal", onClick: () => onAddToCart({ product, option, quantity }), children: pending ? "Adding…" : stock === 0 ? "Sold out" : "Add to bag" })] })] })), errorMessage && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: "mt-4 text-sm text-error-700", children: errorMessage })), successMessage && ((0, jsx_runtime_1.jsx)("p", { role: "status", className: "mt-4 text-sm text-success-700", children: successMessage })), deliveryNote && ((0, jsx_runtime_1.jsx)("p", { className: "mt-6 border-y border-surface-border py-5 text-sm leading-relaxed text-grey-600", children: deliveryNote })), details.length > 0 && ((0, jsx_runtime_1.jsxs)("details", { className: "mt-4", open: true, children: [(0, jsx_runtime_1.jsxs)("summary", { className: "flex min-h-11 cursor-pointer items-center justify-between rounded text-sm font-semibold focus-visible:outline-2 focus-visible:outline-action-primary", children: ["Product details ", (0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true", children: "\u2195" })] }), (0, jsx_runtime_1.jsx)("dl", { className: "space-y-3 py-3 text-sm", children: details.map((detail) => ((0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-2 gap-4", children: [(0, jsx_runtime_1.jsx)("dt", { className: "text-grey-600", children: detail.label }), (0, jsx_runtime_1.jsx)("dd", { children: detail.value })] }, detail.label))) })] }))] })] }) }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useState } from "react";
|
|
3
|
+
import { Button } from "../components/Button/index.js";
|
|
4
|
+
import { cn } from "../utils/cn.js";
|
|
5
|
+
import { CommerceHeading, CommercePhoto, CommerceQuantity, commerceControl, formatCommerceMoney, } from "./commerce.js";
|
|
6
|
+
/** Reset the selection when switching products. */
|
|
7
|
+
export function ProductDetailBlock(props) {
|
|
8
|
+
return _jsx(ProductDetailView, { ...props }, props.product.id);
|
|
9
|
+
}
|
|
10
|
+
function ProductDetailView({ product, title = product.name, description = product.description, images, layout = "split", options = [], optionLabel = "Finish", details = [], onAddToCart, pending = false, errorMessage, successMessage, deliveryNote, currency, locale, headingLevel, className, }) {
|
|
11
|
+
const id = useId();
|
|
12
|
+
const [optionId, setOptionId] = useState(options.find((o) => o.stock !== 0)?.id ?? options[0]?.id);
|
|
13
|
+
const [requestedQuantity, setQuantity] = useState(1);
|
|
14
|
+
const [imageIndex, setImageIndex] = useState(0);
|
|
15
|
+
const gallery = images?.length ? images : product.image ? [product.image] : [];
|
|
16
|
+
const activeImage = Math.min(imageIndex, Math.max(0, gallery.length - 1));
|
|
17
|
+
const option = options.find((o) => o.id === optionId) ?? options.find((o) => o.stock !== 0) ?? options[0];
|
|
18
|
+
const stock = Math.min(product.stock ?? 99, option?.stock ?? 99);
|
|
19
|
+
const quantity = Math.max(1, Math.min(requestedQuantity, stock));
|
|
20
|
+
return (_jsx("section", { "aria-labelledby": `${id}-title`, "aria-busy": pending || undefined, className: cn("@container w-full min-w-0 bg-surface text-grey-900 [overflow-wrap:anywhere]", className), children: _jsxs("div", { className: cn("grid items-start gap-8", layout === "split" &&
|
|
21
|
+
"@min-[700px]:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)] @min-[900px]:gap-14"), children: [_jsxs("div", { className: "min-w-0 space-y-3", children: [_jsx(CommercePhoto, { image: gallery[activeImage], priority: true, className: "aspect-[4/5]" }), gallery.length > 1 && (_jsx("div", { role: "group", "aria-label": `Images of ${product.name}`, className: "flex flex-wrap gap-3", children: gallery.map((image, index) => (_jsx("button", { type: "button", "aria-label": `View image ${index + 1}: ${image.alt}`, "aria-pressed": index === activeImage, onClick: () => setImageIndex(index), className: cn("w-16 cursor-pointer rounded-lg border-2 p-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary", index === activeImage
|
|
22
|
+
? "border-action-primary"
|
|
23
|
+
: "border-transparent hover:border-surface-border-strong"), children: _jsx(CommercePhoto, { image: { ...image, alt: "" }, className: "rounded" }) }, `${image.src}-${index}`))) }))] }), _jsxs("div", { className: "min-w-0 @min-[700px]:py-2", children: [product.category && (_jsx("p", { className: "mb-4 text-sm font-medium text-grey-600", children: product.category })), _jsx(CommerceHeading, { id: `${id}-title`, title: title, description: description, headingLevel: headingLevel }), _jsxs("div", { className: "mt-6 flex flex-wrap items-center justify-between gap-3 border-b border-surface-border pb-6", children: [_jsx("p", { className: "text-2xl font-medium tracking-tight tabular-nums", children: formatCommerceMoney(product.price, currency, locale) }), _jsxs("p", { className: "flex items-center gap-2 text-sm text-grey-600", children: [_jsx("span", { "aria-hidden": "true", className: cn("size-1.5 rounded-full", stock ? "bg-success-700" : "bg-grey-500") }), stock === 0 ? "Currently sold out" : "Available to order"] })] }), options.length > 0 && (_jsxs("fieldset", { disabled: pending, className: "mt-6 space-y-3", children: [_jsxs("legend", { className: "text-sm font-semibold", children: [optionLabel, option && (_jsxs("span", { "aria-hidden": "true", className: "font-normal text-grey-600", children: [" ", "\u00B7 ", option.label] }))] }), _jsx("div", { className: "flex flex-wrap gap-2", children: options.map((o) => (_jsxs("label", { className: `${commerceControl} inline-flex items-center gap-2 has-[:checked]:border-action-primary has-[:checked]:bg-surface-muted has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50`, children: [_jsx("input", { type: "radio", name: `${id}-option`, value: o.id, checked: option?.id === o.id, disabled: o.stock === 0, onChange: () => {
|
|
24
|
+
setOptionId(o.id);
|
|
25
|
+
setQuantity(1);
|
|
26
|
+
}, className: "accent-action-primary" }), o.label, o.stock === 0 && " · Sold out"] }, o.id))) })] })), onAddToCart && (_jsxs("div", { className: "mt-6 space-y-3", children: [_jsx("p", { className: "text-sm font-semibold", children: "Quantity" }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsx(CommerceQuantity, { name: product.name, quantity: quantity, max: stock, disabled: pending || stock === 0, onChange: setQuantity }), _jsx(Button, { disabled: pending || stock === 0, className: "h-auto min-h-12 min-w-[140px] flex-1 whitespace-normal", onClick: () => onAddToCart({ product, option, quantity }), children: pending ? "Adding…" : stock === 0 ? "Sold out" : "Add to bag" })] })] })), errorMessage && (_jsx("p", { role: "alert", className: "mt-4 text-sm text-error-700", children: errorMessage })), successMessage && (_jsx("p", { role: "status", className: "mt-4 text-sm text-success-700", children: successMessage })), deliveryNote && (_jsx("p", { className: "mt-6 border-y border-surface-border py-5 text-sm leading-relaxed text-grey-600", children: deliveryNote })), details.length > 0 && (_jsxs("details", { className: "mt-4", open: true, children: [_jsxs("summary", { className: "flex min-h-11 cursor-pointer items-center justify-between rounded text-sm font-semibold focus-visible:outline-2 focus-visible:outline-action-primary", children: ["Product details ", _jsx("span", { "aria-hidden": "true", children: "\u2195" })] }), _jsx("dl", { className: "space-y-3 py-3 text-sm", children: details.map((detail) => (_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx("dt", { className: "text-grey-600", children: detail.label }), _jsx("dd", { children: detail.value })] }, detail.label))) })] }))] })] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProductHeroBlock = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
7
|
+
const Badge_1 = require("../components/Badge/index.cjs");
|
|
8
|
+
const Icon_1 = require("../components/Icon/index.cjs");
|
|
9
|
+
const HEADING_ORDER = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
10
|
+
function nextHeadingLevel(level) {
|
|
11
|
+
const index = HEADING_ORDER.indexOf(level);
|
|
12
|
+
return HEADING_ORDER[Math.min(index + 1, HEADING_ORDER.length - 1)];
|
|
13
|
+
}
|
|
14
|
+
function isLinkCta(cta) {
|
|
15
|
+
return typeof cta.href === "string";
|
|
16
|
+
}
|
|
17
|
+
/* ─── Shared call-to-action appearance ─────────────────────────────────────
|
|
18
|
+
One recipe drives both the anchor and the button so a destination and an
|
|
19
|
+
action can sit side by side without looking different. Heights are minimums,
|
|
20
|
+
not fixed values, so a long translated label grows instead of clipping.
|
|
21
|
+
------------------------------------------------------------------------ */
|
|
22
|
+
const CTA_BASE = "inline-flex max-w-full items-center justify-center gap-2 rounded-lg px-6 py-4 text-base font-semibold leading-[1.45] min-h-14 text-center break-words no-underline transition-colors cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text";
|
|
23
|
+
const CTA_TONE = {
|
|
24
|
+
primary: "bg-action-primary text-white hover:bg-action-primary-hover",
|
|
25
|
+
secondary: "bg-transparent text-action-primary-text border-[1.5px] border-current hover:bg-primary-50",
|
|
26
|
+
};
|
|
27
|
+
function HeroCta({ cta, tone }) {
|
|
28
|
+
const content = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [cta.icon && (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: cta.icon, size: "md", className: "shrink-0" }), (0, jsx_runtime_1.jsx)("span", { className: "min-w-0", children: cta.label })] }));
|
|
29
|
+
if (isLinkCta(cta)) {
|
|
30
|
+
const externalAttributes = cta.external
|
|
31
|
+
? { target: "_blank", rel: "noreferrer" }
|
|
32
|
+
: {};
|
|
33
|
+
return ((0, jsx_runtime_1.jsxs)("a", { href: cta.href, onClick: cta.onClick, className: (0, cn_1.cn)(CTA_BASE, CTA_TONE[tone]), ...externalAttributes, children: [content, cta.external && (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: "(opens in a new tab)" })] }));
|
|
34
|
+
}
|
|
35
|
+
return ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: cta.onClick, className: (0, cn_1.cn)(CTA_BASE, CTA_TONE[tone]), children: content }));
|
|
36
|
+
}
|
|
37
|
+
/* ─── Media ────────────────────────────────────────────────────────────── */
|
|
38
|
+
function MediaPlaceholder({ label }) {
|
|
39
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-h-[200px] w-full flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-surface-border-strong bg-surface-muted px-6 py-10 text-center", role: "img", "aria-label": label, children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "image", size: "xl", className: "text-grey-400", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("p", { className: "max-w-full text-sm break-words text-grey-600", children: label })] }));
|
|
40
|
+
}
|
|
41
|
+
const MEDIA_RATIO_CLASS = {
|
|
42
|
+
auto: "h-auto",
|
|
43
|
+
video: "aspect-video object-cover",
|
|
44
|
+
wide: "aspect-[16/10] object-cover",
|
|
45
|
+
square: "aspect-square object-cover",
|
|
46
|
+
};
|
|
47
|
+
function HeroMedia({ media, slot, fallback }) {
|
|
48
|
+
const [failed, setFailed] = (0, react_1.useState)(false);
|
|
49
|
+
if (slot) {
|
|
50
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "w-full min-w-0", children: slot });
|
|
51
|
+
}
|
|
52
|
+
if (!media)
|
|
53
|
+
return null;
|
|
54
|
+
const missing = !media.src || failed;
|
|
55
|
+
const placeholderLabel = media.alt || "Product example unavailable";
|
|
56
|
+
return ((0, jsx_runtime_1.jsxs)("figure", { className: "m-0 w-full min-w-0", children: [missing ? ((fallback ?? (0, jsx_runtime_1.jsx)(MediaPlaceholder, { label: placeholderLabel }))) : ((0, jsx_runtime_1.jsx)("img", { src: media.src, alt: media.alt, width: media.width, height: media.height, loading: "lazy", decoding: "async", onError: () => setFailed(true), className: (0, cn_1.cn)("w-full rounded-xl border border-surface-border bg-surface", MEDIA_RATIO_CLASS[media.aspectRatio ?? "auto"]) })), media.caption && ((0, jsx_runtime_1.jsx)("figcaption", { className: "mt-3 text-sm break-words text-grey-600", children: media.caption }))] }));
|
|
57
|
+
}
|
|
58
|
+
/* ─── Loading skeleton ─────────────────────────────────────────────────── */
|
|
59
|
+
function HeroSkeleton({ withMedia }) {
|
|
60
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("grid w-full gap-10", withMedia && "@min-[1136px]:grid-cols-2 @min-[1136px]:items-center @min-[1136px]:gap-16"), "aria-hidden": "true", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-5 w-32 max-w-full rounded-full bg-grey-200" }), (0, jsx_runtime_1.jsx)("div", { className: "h-10 w-full rounded-lg bg-grey-200" }), (0, jsx_runtime_1.jsx)("div", { className: "h-10 w-4/5 rounded-lg bg-grey-200" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-2 h-4 w-full rounded bg-grey-100" }), (0, jsx_runtime_1.jsx)("div", { className: "h-4 w-11/12 rounded bg-grey-100" }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-4 flex flex-wrap gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-14 w-40 max-w-full rounded-lg bg-grey-200" }), (0, jsx_runtime_1.jsx)("div", { className: "h-14 w-36 max-w-full rounded-lg bg-grey-100" })] })] }), withMedia && ((0, jsx_runtime_1.jsx)("div", { className: "aspect-[16/10] w-full min-w-0 rounded-xl bg-grey-100" }))] }));
|
|
61
|
+
}
|
|
62
|
+
/* ─── Block ────────────────────────────────────────────────────────────── */
|
|
63
|
+
/**
|
|
64
|
+
* Product Hero — explains the product, shows a relevant example, and leads to a
|
|
65
|
+
* primary next step.
|
|
66
|
+
*
|
|
67
|
+
* States: `default`, `loading`, `error`. An "empty" state is not meaningful (a
|
|
68
|
+
* hero without a headline is not a hero); missing media degrades to a labelled
|
|
69
|
+
* placeholder instead. No success state is manufactured — the block performs no
|
|
70
|
+
* submission.
|
|
71
|
+
*/
|
|
72
|
+
exports.ProductHeroBlock = (0, react_1.forwardRef)(({ eyebrow, eyebrowBadge, headline, description, headingLevel = "h1", primaryCta, secondaryCta, note, highlights, media, mediaSlot, mediaFallback, align = "start", state = "default", loadingLabel = "Loading product introduction", errorTitle = "We could not load this introduction", errorDescription, onRetry, retryLabel = "Try again", className, ...rest }, ref) => {
|
|
73
|
+
const headingId = (0, react_1.useId)();
|
|
74
|
+
const Heading = headingLevel;
|
|
75
|
+
const HighlightHeading = nextHeadingLevel(headingLevel);
|
|
76
|
+
const hasMedia = Boolean(mediaSlot || media);
|
|
77
|
+
const centred = align === "center" && !hasMedia;
|
|
78
|
+
const headingSize = headingLevel === "h1"
|
|
79
|
+
? "text-h3 @min-[600px]:text-h1 @min-[1136px]:text-display-sm"
|
|
80
|
+
: "text-h4 @min-[600px]:text-h3 @min-[1136px]:text-h2";
|
|
81
|
+
return ((0, jsx_runtime_1.jsx)("section", { ref: ref, "aria-labelledby": state === "loading" ? undefined : headingId, "aria-label": state === "loading" ? loadingLabel : undefined, "aria-busy": state === "loading" || undefined, className: (0, cn_1.cn)("@container relative w-full", className), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "mx-auto flex w-full max-w-[1136px] flex-col gap-10 px-4 py-12 @min-[600px]:px-8 @min-[600px]:py-16 @min-[1136px]:py-20", children: [state === "loading" && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("p", { role: "status", className: "sr-only", children: loadingLabel }), (0, jsx_runtime_1.jsx)(HeroSkeleton, { withMedia: hasMedia })] })), state === "error" && ((0, jsx_runtime_1.jsxs)("div", { role: "alert", className: "flex w-full min-w-0 flex-col items-start gap-4 rounded-xl border border-surface-border bg-surface px-5 py-6 @min-[600px]:px-8 @min-[600px]:py-10", children: [(0, jsx_runtime_1.jsx)("span", { className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-error-50 text-action-danger-text", children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "info-triangle", size: "lg", "aria-hidden": "true" }) }), (0, jsx_runtime_1.jsx)(Heading, { id: headingId, className: "text-h5 font-semibold break-words text-grey-900", children: errorTitle }), errorDescription && ((0, jsx_runtime_1.jsx)("p", { className: "max-w-[60ch] text-base break-words text-grey-600", children: errorDescription })), onRetry && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onRetry, className: (0, cn_1.cn)(CTA_BASE, CTA_TONE.secondary, "min-h-9 px-4 py-2 text-sm"), children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "refresh", size: "sm", className: "shrink-0", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("span", { className: "min-w-0", children: retryLabel })] }))] })), state === "default" && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("grid w-full gap-10", hasMedia && "@min-[1136px]:grid-cols-2 @min-[1136px]:items-center @min-[1136px]:gap-16"), children: [(0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex min-w-0 flex-col gap-5", centred && "mx-auto max-w-[68ch] items-center text-center"), children: [(eyebrow || eyebrowBadge) && ((0, jsx_runtime_1.jsxs)("p", { className: (0, cn_1.cn)("flex flex-wrap items-center gap-2 text-sm font-semibold break-words text-action-primary-text", centred && "justify-center"), children: [eyebrowBadge && ((0, jsx_runtime_1.jsx)(Badge_1.Badge, { color: "orange", type: "accent", size: "md", children: eyebrowBadge })), eyebrow] })), (0, jsx_runtime_1.jsx)(Heading, { id: headingId, className: (0, cn_1.cn)("font-semibold break-words text-balance text-grey-900", headingSize), children: headline }), description && ((0, jsx_runtime_1.jsx)("p", { className: (0, cn_1.cn)("max-w-[60ch] text-base break-words text-grey-600 @min-[600px]:text-body-lg", centred && "mx-auto"), children: description })), (primaryCta || secondaryCta) && ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("mt-1 flex w-full min-w-0 flex-col gap-3 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:items-center", centred && "@min-[600px]:justify-center"), children: [primaryCta && (0, jsx_runtime_1.jsx)(HeroCta, { cta: primaryCta, tone: "primary" }), secondaryCta && (0, jsx_runtime_1.jsx)(HeroCta, { cta: secondaryCta, tone: "secondary" })] })), note && (0, jsx_runtime_1.jsx)("p", { className: "text-sm break-words text-grey-500", children: note }), highlights && highlights.length > 0 && ((0, jsx_runtime_1.jsx)("ul", { className: (0, cn_1.cn)("mt-3 flex list-none flex-col gap-4 p-0 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:gap-8", centred && "@min-[600px]:justify-center"), children: highlights.map((highlight) => ((0, jsx_runtime_1.jsxs)("li", { className: "flex min-w-0 items-start gap-2", children: [highlight.icon && ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: highlight.icon, size: "md", className: "mt-0.5 shrink-0 text-action-primary-text", "aria-hidden": "true" })), (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col", children: [highlight.value !== undefined && ((0, jsx_runtime_1.jsx)(HighlightHeading, { className: "text-h6 font-semibold break-words text-grey-900", children: highlight.value })), (0, jsx_runtime_1.jsx)("span", { className: "text-sm break-words text-grey-600", children: highlight.label })] })] }, highlight.id))) }))] }), hasMedia && ((0, jsx_runtime_1.jsx)(HeroMedia, { media: media, slot: mediaSlot, fallback: mediaFallback }))] }))] }) }));
|
|
82
|
+
});
|
|
83
|
+
exports.ProductHeroBlock.displayName = "ProductHeroBlock";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId, useState, } from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
import { Badge } from "../components/Badge/index.js";
|
|
5
|
+
import { Icon } from "../components/Icon/index.js";
|
|
6
|
+
const HEADING_ORDER = ["h1", "h2", "h3", "h4", "h5", "h6"];
|
|
7
|
+
function nextHeadingLevel(level) {
|
|
8
|
+
const index = HEADING_ORDER.indexOf(level);
|
|
9
|
+
return HEADING_ORDER[Math.min(index + 1, HEADING_ORDER.length - 1)];
|
|
10
|
+
}
|
|
11
|
+
function isLinkCta(cta) {
|
|
12
|
+
return typeof cta.href === "string";
|
|
13
|
+
}
|
|
14
|
+
/* ─── Shared call-to-action appearance ─────────────────────────────────────
|
|
15
|
+
One recipe drives both the anchor and the button so a destination and an
|
|
16
|
+
action can sit side by side without looking different. Heights are minimums,
|
|
17
|
+
not fixed values, so a long translated label grows instead of clipping.
|
|
18
|
+
------------------------------------------------------------------------ */
|
|
19
|
+
const CTA_BASE = "inline-flex max-w-full items-center justify-center gap-2 rounded-lg px-6 py-4 text-base font-semibold leading-[1.45] min-h-14 text-center break-words no-underline transition-colors cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text";
|
|
20
|
+
const CTA_TONE = {
|
|
21
|
+
primary: "bg-action-primary text-white hover:bg-action-primary-hover",
|
|
22
|
+
secondary: "bg-transparent text-action-primary-text border-[1.5px] border-current hover:bg-primary-50",
|
|
23
|
+
};
|
|
24
|
+
function HeroCta({ cta, tone }) {
|
|
25
|
+
const content = (_jsxs(_Fragment, { children: [cta.icon && _jsx(Icon, { name: cta.icon, size: "md", className: "shrink-0" }), _jsx("span", { className: "min-w-0", children: cta.label })] }));
|
|
26
|
+
if (isLinkCta(cta)) {
|
|
27
|
+
const externalAttributes = cta.external
|
|
28
|
+
? { target: "_blank", rel: "noreferrer" }
|
|
29
|
+
: {};
|
|
30
|
+
return (_jsxs("a", { href: cta.href, onClick: cta.onClick, className: cn(CTA_BASE, CTA_TONE[tone]), ...externalAttributes, children: [content, cta.external && _jsx("span", { className: "sr-only", children: "(opens in a new tab)" })] }));
|
|
31
|
+
}
|
|
32
|
+
return (_jsx("button", { type: "button", onClick: cta.onClick, className: cn(CTA_BASE, CTA_TONE[tone]), children: content }));
|
|
33
|
+
}
|
|
34
|
+
/* ─── Media ────────────────────────────────────────────────────────────── */
|
|
35
|
+
function MediaPlaceholder({ label }) {
|
|
36
|
+
return (_jsxs("div", { className: "flex min-h-[200px] w-full flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-surface-border-strong bg-surface-muted px-6 py-10 text-center", role: "img", "aria-label": label, children: [_jsx(Icon, { name: "image", size: "xl", className: "text-grey-400", "aria-hidden": "true" }), _jsx("p", { className: "max-w-full text-sm break-words text-grey-600", children: label })] }));
|
|
37
|
+
}
|
|
38
|
+
const MEDIA_RATIO_CLASS = {
|
|
39
|
+
auto: "h-auto",
|
|
40
|
+
video: "aspect-video object-cover",
|
|
41
|
+
wide: "aspect-[16/10] object-cover",
|
|
42
|
+
square: "aspect-square object-cover",
|
|
43
|
+
};
|
|
44
|
+
function HeroMedia({ media, slot, fallback }) {
|
|
45
|
+
const [failed, setFailed] = useState(false);
|
|
46
|
+
if (slot) {
|
|
47
|
+
return _jsx("div", { className: "w-full min-w-0", children: slot });
|
|
48
|
+
}
|
|
49
|
+
if (!media)
|
|
50
|
+
return null;
|
|
51
|
+
const missing = !media.src || failed;
|
|
52
|
+
const placeholderLabel = media.alt || "Product example unavailable";
|
|
53
|
+
return (_jsxs("figure", { className: "m-0 w-full min-w-0", children: [missing ? ((fallback ?? _jsx(MediaPlaceholder, { label: placeholderLabel }))) : (_jsx("img", { src: media.src, alt: media.alt, width: media.width, height: media.height, loading: "lazy", decoding: "async", onError: () => setFailed(true), className: cn("w-full rounded-xl border border-surface-border bg-surface", MEDIA_RATIO_CLASS[media.aspectRatio ?? "auto"]) })), media.caption && (_jsx("figcaption", { className: "mt-3 text-sm break-words text-grey-600", children: media.caption }))] }));
|
|
54
|
+
}
|
|
55
|
+
/* ─── Loading skeleton ─────────────────────────────────────────────────── */
|
|
56
|
+
function HeroSkeleton({ withMedia }) {
|
|
57
|
+
return (_jsxs("div", { className: cn("grid w-full gap-10", withMedia && "@min-[1136px]:grid-cols-2 @min-[1136px]:items-center @min-[1136px]:gap-16"), "aria-hidden": "true", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-4", children: [_jsx("div", { className: "h-5 w-32 max-w-full rounded-full bg-grey-200" }), _jsx("div", { className: "h-10 w-full rounded-lg bg-grey-200" }), _jsx("div", { className: "h-10 w-4/5 rounded-lg bg-grey-200" }), _jsx("div", { className: "mt-2 h-4 w-full rounded bg-grey-100" }), _jsx("div", { className: "h-4 w-11/12 rounded bg-grey-100" }), _jsxs("div", { className: "mt-4 flex flex-wrap gap-3", children: [_jsx("div", { className: "h-14 w-40 max-w-full rounded-lg bg-grey-200" }), _jsx("div", { className: "h-14 w-36 max-w-full rounded-lg bg-grey-100" })] })] }), withMedia && (_jsx("div", { className: "aspect-[16/10] w-full min-w-0 rounded-xl bg-grey-100" }))] }));
|
|
58
|
+
}
|
|
59
|
+
/* ─── Block ────────────────────────────────────────────────────────────── */
|
|
60
|
+
/**
|
|
61
|
+
* Product Hero — explains the product, shows a relevant example, and leads to a
|
|
62
|
+
* primary next step.
|
|
63
|
+
*
|
|
64
|
+
* States: `default`, `loading`, `error`. An "empty" state is not meaningful (a
|
|
65
|
+
* hero without a headline is not a hero); missing media degrades to a labelled
|
|
66
|
+
* placeholder instead. No success state is manufactured — the block performs no
|
|
67
|
+
* submission.
|
|
68
|
+
*/
|
|
69
|
+
export const ProductHeroBlock = forwardRef(({ eyebrow, eyebrowBadge, headline, description, headingLevel = "h1", primaryCta, secondaryCta, note, highlights, media, mediaSlot, mediaFallback, align = "start", state = "default", loadingLabel = "Loading product introduction", errorTitle = "We could not load this introduction", errorDescription, onRetry, retryLabel = "Try again", className, ...rest }, ref) => {
|
|
70
|
+
const headingId = useId();
|
|
71
|
+
const Heading = headingLevel;
|
|
72
|
+
const HighlightHeading = nextHeadingLevel(headingLevel);
|
|
73
|
+
const hasMedia = Boolean(mediaSlot || media);
|
|
74
|
+
const centred = align === "center" && !hasMedia;
|
|
75
|
+
const headingSize = headingLevel === "h1"
|
|
76
|
+
? "text-h3 @min-[600px]:text-h1 @min-[1136px]:text-display-sm"
|
|
77
|
+
: "text-h4 @min-[600px]:text-h3 @min-[1136px]:text-h2";
|
|
78
|
+
return (_jsx("section", { ref: ref, "aria-labelledby": state === "loading" ? undefined : headingId, "aria-label": state === "loading" ? loadingLabel : undefined, "aria-busy": state === "loading" || undefined, className: cn("@container relative w-full", className), ...rest, children: _jsxs("div", { className: "mx-auto flex w-full max-w-[1136px] flex-col gap-10 px-4 py-12 @min-[600px]:px-8 @min-[600px]:py-16 @min-[1136px]:py-20", children: [state === "loading" && (_jsxs(_Fragment, { children: [_jsx("p", { role: "status", className: "sr-only", children: loadingLabel }), _jsx(HeroSkeleton, { withMedia: hasMedia })] })), state === "error" && (_jsxs("div", { role: "alert", className: "flex w-full min-w-0 flex-col items-start gap-4 rounded-xl border border-surface-border bg-surface px-5 py-6 @min-[600px]:px-8 @min-[600px]:py-10", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-error-50 text-action-danger-text", children: _jsx(Icon, { name: "info-triangle", size: "lg", "aria-hidden": "true" }) }), _jsx(Heading, { id: headingId, className: "text-h5 font-semibold break-words text-grey-900", children: errorTitle }), errorDescription && (_jsx("p", { className: "max-w-[60ch] text-base break-words text-grey-600", children: errorDescription })), onRetry && (_jsxs("button", { type: "button", onClick: onRetry, className: cn(CTA_BASE, CTA_TONE.secondary, "min-h-9 px-4 py-2 text-sm"), children: [_jsx(Icon, { name: "refresh", size: "sm", className: "shrink-0", "aria-hidden": "true" }), _jsx("span", { className: "min-w-0", children: retryLabel })] }))] })), state === "default" && (_jsxs("div", { className: cn("grid w-full gap-10", hasMedia && "@min-[1136px]:grid-cols-2 @min-[1136px]:items-center @min-[1136px]:gap-16"), children: [_jsxs("div", { className: cn("flex min-w-0 flex-col gap-5", centred && "mx-auto max-w-[68ch] items-center text-center"), children: [(eyebrow || eyebrowBadge) && (_jsxs("p", { className: cn("flex flex-wrap items-center gap-2 text-sm font-semibold break-words text-action-primary-text", centred && "justify-center"), children: [eyebrowBadge && (_jsx(Badge, { color: "orange", type: "accent", size: "md", children: eyebrowBadge })), eyebrow] })), _jsx(Heading, { id: headingId, className: cn("font-semibold break-words text-balance text-grey-900", headingSize), children: headline }), description && (_jsx("p", { className: cn("max-w-[60ch] text-base break-words text-grey-600 @min-[600px]:text-body-lg", centred && "mx-auto"), children: description })), (primaryCta || secondaryCta) && (_jsxs("div", { className: cn("mt-1 flex w-full min-w-0 flex-col gap-3 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:items-center", centred && "@min-[600px]:justify-center"), children: [primaryCta && _jsx(HeroCta, { cta: primaryCta, tone: "primary" }), secondaryCta && _jsx(HeroCta, { cta: secondaryCta, tone: "secondary" })] })), note && _jsx("p", { className: "text-sm break-words text-grey-500", children: note }), highlights && highlights.length > 0 && (_jsx("ul", { className: cn("mt-3 flex list-none flex-col gap-4 p-0 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:gap-8", centred && "@min-[600px]:justify-center"), children: highlights.map((highlight) => (_jsxs("li", { className: "flex min-w-0 items-start gap-2", children: [highlight.icon && (_jsx(Icon, { name: highlight.icon, size: "md", className: "mt-0.5 shrink-0 text-action-primary-text", "aria-hidden": "true" })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [highlight.value !== undefined && (_jsx(HighlightHeading, { className: "text-h6 font-semibold break-words text-grey-900", children: highlight.value })), _jsx("span", { className: "text-sm break-words text-grey-600", children: highlight.label })] })] }, highlight.id))) }))] }), hasMedia && (_jsx(HeroMedia, { media: media, slot: mediaSlot, fallback: mediaFallback }))] }))] }) }));
|
|
79
|
+
});
|
|
80
|
+
ProductHeroBlock.displayName = "ProductHeroBlock";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultProfileTimeZones = void 0;
|
|
4
|
+
exports.ProfileSettingsBlock = ProfileSettingsBlock;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
8
|
+
const Alert_1 = require("../components/Alert/index.cjs");
|
|
9
|
+
const Avatar_1 = require("../components/Avatar/index.cjs");
|
|
10
|
+
const Button_1 = require("../components/Button/index.cjs");
|
|
11
|
+
const Icon_1 = require("../components/Icon/index.cjs");
|
|
12
|
+
const Input_1 = require("../components/Input/index.cjs");
|
|
13
|
+
const Spinner_1 = require("../components/Spinner/index.cjs");
|
|
14
|
+
/* ─── Defaults ───────────────────────────────────────────────────────── */
|
|
15
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
16
|
+
const defaultLabels = {
|
|
17
|
+
firstName: "First name",
|
|
18
|
+
lastName: "Last name",
|
|
19
|
+
email: "Email address",
|
|
20
|
+
jobTitle: "Job title",
|
|
21
|
+
timeZone: "Time zone",
|
|
22
|
+
bio: "About you",
|
|
23
|
+
};
|
|
24
|
+
exports.defaultProfileTimeZones = [
|
|
25
|
+
{ value: "Europe/London", label: "London (GMT+01:00)" },
|
|
26
|
+
{ value: "Europe/Berlin", label: "Berlin (GMT+02:00)" },
|
|
27
|
+
{ value: "America/New_York", label: "New York (GMT−04:00)" },
|
|
28
|
+
{ value: "America/Los_Angeles", label: "Los Angeles (GMT−07:00)" },
|
|
29
|
+
{ value: "Africa/Lagos", label: "Lagos (GMT+01:00)" },
|
|
30
|
+
{ value: "Asia/Singapore", label: "Singapore (GMT+08:00)" },
|
|
31
|
+
];
|
|
32
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
33
|
+
function ProfileSettingsBlock({ title = "Profile", description = "These details appear next to your name across the workspace.", headingLevel = "h2", value, onSave, onCancel, onDirtyChange, status = "idle", errorMessage, successMessage, fieldErrors, canEdit = true, readOnlyMessage = "You can view this profile but not change it. Ask a workspace administrator for edit access.", readOnlyFields, readOnlyFieldMessages, avatar, onAvatarChange, onAvatarRemove, avatarStatus = "idle", avatarErrorMessage, avatarSlot, avatarAccept = "image/png,image/jpeg,image/webp", timeZones = exports.defaultProfileTimeZones, bioMaxLength = 280, labels, saveLabel = "Save changes", cancelLabel = "Cancel", className, }) {
|
|
34
|
+
const uid = (0, react_1.useId)();
|
|
35
|
+
const titleId = `${uid}-title`;
|
|
36
|
+
const bioId = `${uid}-bio`;
|
|
37
|
+
const bioCountId = `${uid}-bio-count`;
|
|
38
|
+
const serializedSaved = JSON.stringify(value);
|
|
39
|
+
const [draft, setDraft] = (0, react_1.useState)(value);
|
|
40
|
+
const [touched, setTouched] = (0, react_1.useState)({});
|
|
41
|
+
const [saveAttempted, setSaveAttempted] = (0, react_1.useState)(false);
|
|
42
|
+
const lastSavedRef = (0, react_1.useRef)(serializedSaved);
|
|
43
|
+
const fileInputRef = (0, react_1.useRef)(null);
|
|
44
|
+
// Saved values changed under us (a save landed, or the app swapped profile).
|
|
45
|
+
// Re-sync the draft during render rather than in an effect, so the form never
|
|
46
|
+
// paints one frame of stale content.
|
|
47
|
+
if (lastSavedRef.current !== serializedSaved) {
|
|
48
|
+
lastSavedRef.current = serializedSaved;
|
|
49
|
+
setDraft(value);
|
|
50
|
+
setTouched({});
|
|
51
|
+
setSaveAttempted(false);
|
|
52
|
+
}
|
|
53
|
+
const saving = status === "saving";
|
|
54
|
+
const loading = status === "loading";
|
|
55
|
+
const locked = !canEdit || saving || loading;
|
|
56
|
+
const dirty = (0, react_1.useMemo)(() => JSON.stringify(draft) !== serializedSaved, [draft, serializedSaved]);
|
|
57
|
+
const dirtyRef = (0, react_1.useRef)(dirty);
|
|
58
|
+
(0, react_1.useEffect)(() => {
|
|
59
|
+
if (dirtyRef.current !== dirty) {
|
|
60
|
+
dirtyRef.current = dirty;
|
|
61
|
+
onDirtyChange?.(dirty);
|
|
62
|
+
}
|
|
63
|
+
}, [dirty, onDirtyChange]);
|
|
64
|
+
const localErrors = (0, react_1.useMemo)(() => {
|
|
65
|
+
const next = {};
|
|
66
|
+
if (!draft.firstName.trim())
|
|
67
|
+
next.firstName = "Enter a first name.";
|
|
68
|
+
if (!draft.lastName.trim())
|
|
69
|
+
next.lastName = "Enter a last name.";
|
|
70
|
+
if (!draft.email.trim())
|
|
71
|
+
next.email = "Enter an email address.";
|
|
72
|
+
else if (!EMAIL_PATTERN.test(draft.email.trim()))
|
|
73
|
+
next.email = "Enter an email address such as name@example.com.";
|
|
74
|
+
if (draft.bio.length > bioMaxLength)
|
|
75
|
+
next.bio = `Shorten this to ${bioMaxLength} characters or fewer.`;
|
|
76
|
+
return next;
|
|
77
|
+
}, [draft, bioMaxLength]);
|
|
78
|
+
const errorFor = (0, react_1.useCallback)((field) => {
|
|
79
|
+
const serverError = fieldErrors?.[field];
|
|
80
|
+
if (serverError)
|
|
81
|
+
return serverError;
|
|
82
|
+
if (!saveAttempted && !touched[field])
|
|
83
|
+
return undefined;
|
|
84
|
+
return localErrors[field];
|
|
85
|
+
}, [fieldErrors, localErrors, saveAttempted, touched]);
|
|
86
|
+
const isFieldReadOnly = (0, react_1.useCallback)((field) => !canEdit || Boolean(readOnlyFields?.includes(field)), [canEdit, readOnlyFields]);
|
|
87
|
+
const setField = (0, react_1.useCallback)((field, next) => setDraft((prev) => ({ ...prev, [field]: next })), []);
|
|
88
|
+
const markTouched = (0, react_1.useCallback)((field) => setTouched((prev) => ({ ...prev, [field]: true })), []);
|
|
89
|
+
const handleSubmit = (0, react_1.useCallback)((event) => {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
// Guard duplicate submission and honour the permission contract.
|
|
92
|
+
if (locked || !dirty)
|
|
93
|
+
return;
|
|
94
|
+
setSaveAttempted(true);
|
|
95
|
+
if (Object.keys(localErrors).length > 0)
|
|
96
|
+
return;
|
|
97
|
+
onSave(draft);
|
|
98
|
+
}, [dirty, draft, localErrors, locked, onSave]);
|
|
99
|
+
const handleCancel = (0, react_1.useCallback)(() => {
|
|
100
|
+
if (saving)
|
|
101
|
+
return;
|
|
102
|
+
setDraft(value);
|
|
103
|
+
setTouched({});
|
|
104
|
+
setSaveAttempted(false);
|
|
105
|
+
onCancel?.();
|
|
106
|
+
}, [onCancel, saving, value]);
|
|
107
|
+
const handleAvatarInput = (0, react_1.useCallback)((event) => {
|
|
108
|
+
const file = event.target.files?.[0];
|
|
109
|
+
if (file)
|
|
110
|
+
onAvatarChange?.(file);
|
|
111
|
+
// Allow re-selecting the same file after a failed upload.
|
|
112
|
+
event.target.value = "";
|
|
113
|
+
}, [onAvatarChange]);
|
|
114
|
+
const Heading = headingLevel;
|
|
115
|
+
const label = (field) => labels?.[field] ?? defaultLabels[field];
|
|
116
|
+
const readOnlyNote = (field) => isFieldReadOnly(field) && canEdit ? readOnlyFieldMessages?.[field] : undefined;
|
|
117
|
+
const bioError = errorFor("bio");
|
|
118
|
+
const bioReadOnly = isFieldReadOnly("bio");
|
|
119
|
+
return ((0, jsx_runtime_1.jsx)("section", { "aria-labelledby": titleId, className: (0, cn_1.cn)("@container w-full", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-6 rounded-12 border border-surface-border bg-surface p-4 @min-[420px]:p-6", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(Heading, { id: titleId, className: "text-h6 font-semibold text-grey-900", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm text-grey-600", children: description })] }), !canEdit && ((0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "information", icon: "lock", role: "status", title: "View only", description: readOnlyMessage, className: "bg-surface dark:bg-surface" })), status === "error" && errorMessage && ((0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "error", icon: "info-triangle", role: "alert", title: "Your changes were not saved", description: errorMessage, className: "bg-surface dark:bg-surface" })), status === "success" && successMessage && ((0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "success", icon: "check-circle", role: "status", title: successMessage, className: "bg-surface dark:bg-surface" })), loading ? ((0, jsx_runtime_1.jsx)("div", { "aria-busy": "true", className: "flex min-h-40 flex-col items-center justify-center gap-3 rounded-8 border border-dashed border-surface-border-strong p-6", children: (0, jsx_runtime_1.jsx)(Spinner_1.Spinner, { size: "md", label: "Loading your profile\u2026", labelPosition: "below" }) })) : ((0, jsx_runtime_1.jsxs)("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3 border-b border-surface-border pb-6 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:gap-5", children: [(0, jsx_runtime_1.jsx)(Avatar_1.Avatar, { type: avatar?.src ? "image" : "initials", size: "2xl", src: avatar?.src, alt: avatar?.alt ?? `${draft.firstName} ${draft.lastName}`.trim(), initials: avatar?.initials ??
|
|
120
|
+
(`${draft.firstName.charAt(0)}${draft.lastName.charAt(0)}`.toUpperCase() || "?") }), (0, jsx_runtime_1.jsx)("div", { className: "flex min-w-0 flex-col gap-2", children: avatarSlot ?? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [onAvatarChange ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { ref: fileInputRef, id: `${uid}-avatar`, type: "file", accept: avatarAccept, disabled: locked || avatarStatus === "uploading", onChange: handleAvatarInput, className: "sr-only" }), (0, jsx_runtime_1.jsxs)("label", { htmlFor: `${uid}-avatar`, className: (0, cn_1.cn)("inline-flex h-9 cursor-pointer items-center gap-2 rounded-lg border-[1.5px] border-current px-4 text-sm font-semibold text-action-primary-text", "focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-action-primary-text", (locked || avatarStatus === "uploading") &&
|
|
121
|
+
"cursor-not-allowed opacity-50"), children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "image-add", size: "md" }), "Change photo"] }), onAvatarRemove && avatar?.src && ((0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", variant: "grey", appearance: "outlined", size: "sm", disabled: locked || avatarStatus === "uploading", onClick: onAvatarRemove, children: "Remove photo" }))] })) : ((0, jsx_runtime_1.jsx)("p", { className: "text-body-sm text-grey-600", children: "Your photo is managed outside this workspace." })), (0, jsx_runtime_1.jsx)("p", { className: "text-body-xs text-grey-600", children: "PNG, JPG or WebP. A square image works best." }), avatarStatus === "uploading" && ((0, jsx_runtime_1.jsxs)("output", { className: "flex items-center gap-2 text-body-sm text-grey-600", children: [(0, jsx_runtime_1.jsx)(Spinner_1.Spinner, { size: "xs" }), "Uploading your photo\u2026"] })), avatarStatus === "error" && avatarErrorMessage && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: "text-body-sm text-feedback-error", children: avatarErrorMessage }))] })) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 gap-4 @min-[520px]:grid-cols-2", children: [(0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-firstName`, name: "firstName", size: "md", label: label("firstName"), autoComplete: "given-name", required: true, value: draft.firstName, readOnly: isFieldReadOnly("firstName"), disabled: saving, error: errorFor("firstName"), helperText: readOnlyNote("firstName"), onChange: (e) => setField("firstName", e.target.value), onBlur: () => markTouched("firstName") }), (0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-lastName`, name: "lastName", size: "md", label: label("lastName"), autoComplete: "family-name", required: true, value: draft.lastName, readOnly: isFieldReadOnly("lastName"), disabled: saving, error: errorFor("lastName"), helperText: readOnlyNote("lastName"), onChange: (e) => setField("lastName", e.target.value), onBlur: () => markTouched("lastName") })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 gap-4 @min-[520px]:grid-cols-2", children: [(0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-email`, name: "email", type: "email", inputMode: "email", size: "md", label: label("email"), autoComplete: "email", required: true, value: draft.email, readOnly: isFieldReadOnly("email"), disabled: saving, error: errorFor("email"), helperText: readOnlyNote("email"), onChange: (e) => setField("email", e.target.value), onBlur: () => markTouched("email") }), (0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-jobTitle`, name: "jobTitle", size: "md", label: label("jobTitle"), autoComplete: "organization-title", value: draft.jobTitle, readOnly: isFieldReadOnly("jobTitle"), disabled: saving, error: errorFor("jobTitle"), helperText: readOnlyNote("jobTitle"), onChange: (e) => setField("jobTitle", e.target.value), onBlur: () => markTouched("jobTitle") })] }), isFieldReadOnly("timeZone") ? ((0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-timeZone`, name: "timeZone", size: "md", label: label("timeZone"), readOnly: true, value: timeZones.find((z) => z.value === draft.timeZone)?.label ?? draft.timeZone, helperText: readOnlyNote("timeZone"), onChange: () => undefined })) : ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${uid}-timeZone`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: label("timeZone") }), (0, jsx_runtime_1.jsx)("select", { id: `${uid}-timeZone`, name: "timeZone", disabled: saving, value: draft.timeZone, onChange: (e) => setField("timeZone", e.target.value), className: (0, cn_1.cn)("h-12 w-full rounded-lg border border-grey-300 bg-surface px-4 text-sm text-grey-900", "transition-colors hover:border-primary-100 focus-visible:border-primary-400", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", "disabled:cursor-not-allowed disabled:bg-grey-50"), children: timeZones.map((zone) => ((0, jsx_runtime_1.jsx)("option", { value: zone.value, children: zone.label }, zone.value))) })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex w-full flex-col gap-1", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: bioId, className: "text-sm font-medium leading-[1.45] text-grey-900", children: label("bio") }), (0, jsx_runtime_1.jsx)("textarea", { id: bioId, name: "bio", rows: 4, readOnly: bioReadOnly, disabled: saving, value: draft.bio, "aria-invalid": bioError ? true : undefined, "aria-describedby": bioCountId, onChange: (e) => setField("bio", e.target.value), onBlur: () => markTouched("bio"), className: (0, cn_1.cn)("w-full resize-y rounded-lg border px-4 py-3 text-sm text-grey-900 transition-colors", "placeholder:text-grey-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", bioError
|
|
122
|
+
? "border-error-400 bg-surface"
|
|
123
|
+
: bioReadOnly
|
|
124
|
+
? "border-grey-300 bg-grey-100"
|
|
125
|
+
: "border-grey-300 bg-surface hover:border-primary-100 focus-within:border-primary-400", saving && "cursor-not-allowed opacity-70") }), (0, jsx_runtime_1.jsx)("p", { id: bioCountId, className: (0, cn_1.cn)("text-sm leading-[1.45]", bioError ? "text-feedback-error" : "text-grey-600"), children: bioError ?? `${draft.bio.length} of ${bioMaxLength} characters used.` })] }), canEdit && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3 border-t border-surface-border pt-5 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:justify-between", children: [(0, jsx_runtime_1.jsx)("output", { className: "text-body-sm text-grey-600", children: saving
|
|
126
|
+
? "Saving your changes…"
|
|
127
|
+
: dirty
|
|
128
|
+
? "You have unsaved changes."
|
|
129
|
+
: "All changes saved." }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap gap-3", children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", variant: "grey", appearance: "outlined", size: "sm", disabled: !dirty || saving, onClick: handleCancel, children: cancelLabel }), (0, jsx_runtime_1.jsx)(Button_1.Button, { type: "submit", variant: "primary", size: "sm", disabled: !dirty || saving, children: saving ? "Saving…" : saveLabel })] })] }))] }))] }) }));
|
|
130
|
+
}
|