@raydenui/ui 0.9.7 → 0.10.1
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/README.md +124 -49
- 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,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkspaceSwitcherBlock = WorkspaceSwitcherBlock;
|
|
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 Icon_1 = require("../components/Icon/index.cjs");
|
|
8
|
+
const Spinner_1 = require("../components/Spinner/index.cjs");
|
|
9
|
+
/* ─── Recipes ────────────────────────────────────────────────────────── */
|
|
10
|
+
const FOCUS_RING = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text";
|
|
11
|
+
/* ─── Logo ───────────────────────────────────────────────────────────── */
|
|
12
|
+
function WorkspaceMark({ item, size = "md", }) {
|
|
13
|
+
const box = size === "sm" ? "size-7 text-body-xs" : "size-9 text-body-sm";
|
|
14
|
+
if (item.logoSrc) {
|
|
15
|
+
return ((0, jsx_runtime_1.jsx)("img", { src: item.logoSrc, alt: "", className: (0, cn_1.cn)("shrink-0 rounded-lg border border-surface-border object-cover", box) }));
|
|
16
|
+
}
|
|
17
|
+
return ((0, jsx_runtime_1.jsx)("span", { "aria-hidden": "true",
|
|
18
|
+
// The colour comes last: `box` carries a `text-body-*` size utility, and
|
|
19
|
+
// class merging treats it as the same group, so a colour written first is
|
|
20
|
+
// dropped and the initials fall back to the inherited foreground.
|
|
21
|
+
className: (0, cn_1.cn)("inline-flex shrink-0 items-center justify-center rounded-lg bg-primary-50 font-semibold", box, "text-action-primary-text"), children: item.icon ? ((0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: item.icon, size: size === "sm" ? "sm" : "md" })) : ((item.initials ?? item.name.slice(0, 2).toUpperCase())) }));
|
|
22
|
+
}
|
|
23
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
24
|
+
function WorkspaceSwitcherBlock({ current, workspaces = [], onSelect, unavailableMessage = "Switching to this workspace is not available.", status = "idle", errorMessage = "Your workspaces could not be loaded.", onRetry, loadingMessage = "Loading workspaces", triggerLabel = "Switch workspace", triggerHint, placeholder = "Choose a workspace", panelLabel = "Workspaces", filterLabel = "Filter workspaces", filterPlaceholder = "Type to filter", filterThreshold = 6, emptyTitle = "You are not in any workspace yet", emptyDescription = "Create a workspace or ask an administrator for an invitation.", noMatchesTitle = "No workspace matches that filter", noMatchesDescription = "Check the spelling, or clear the filter to see every workspace.", footerActions = [], resultCountLabel = (count) => (count === 1 ? "1 workspace matches" : `${count} workspaces match`), defaultOpen = false, open: controlledOpen, onOpenChange, className, }) {
|
|
25
|
+
const uid = (0, react_1.useId)();
|
|
26
|
+
const panelId = `${uid}-panel`;
|
|
27
|
+
const listId = `${uid}-list`;
|
|
28
|
+
const filterId = `${uid}-filter`;
|
|
29
|
+
const [uncontrolledOpen, setUncontrolledOpen] = (0, react_1.useState)(defaultOpen);
|
|
30
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
31
|
+
const [query, setQuery] = (0, react_1.useState)("");
|
|
32
|
+
/** Roving tabindex: exactly one option is in the tab order at a time. */
|
|
33
|
+
const [activeIndex, setActiveIndex] = (0, react_1.useState)(0);
|
|
34
|
+
const rootRef = (0, react_1.useRef)(null);
|
|
35
|
+
const triggerRef = (0, react_1.useRef)(null);
|
|
36
|
+
/**
|
|
37
|
+
* The popover is wider than its trigger, so a trigger sitting near the right
|
|
38
|
+
* edge would push the popover past it and widen the document. The offset is
|
|
39
|
+
* measured from the trigger when the popover opens: start-aligned when it
|
|
40
|
+
* fits, end-aligned when it does not, and pinned to the viewport when neither
|
|
41
|
+
* side fits. Nothing is positioned until it is measured.
|
|
42
|
+
*/
|
|
43
|
+
const [panelBox, setPanelBox] = (0, react_1.useState)(null);
|
|
44
|
+
const filterRef = (0, react_1.useRef)(null);
|
|
45
|
+
const listRef = (0, react_1.useRef)(null);
|
|
46
|
+
const setOpen = (0, react_1.useCallback)((next) => {
|
|
47
|
+
if (controlledOpen === undefined)
|
|
48
|
+
setUncontrolledOpen(next);
|
|
49
|
+
onOpenChange?.(next);
|
|
50
|
+
}, [controlledOpen, onOpenChange]);
|
|
51
|
+
const close = (0, react_1.useCallback)((restoreFocus) => {
|
|
52
|
+
setOpen(false);
|
|
53
|
+
if (restoreFocus)
|
|
54
|
+
triggerRef.current?.focus();
|
|
55
|
+
}, [setOpen]);
|
|
56
|
+
/* Closing when the pointer lands outside keeps the popover from shadowing the
|
|
57
|
+
page; it is deliberately not a modal, so focus is never trapped here. */
|
|
58
|
+
(0, react_1.useEffect)(() => {
|
|
59
|
+
if (!open)
|
|
60
|
+
return;
|
|
61
|
+
function handlePointerDown(event) {
|
|
62
|
+
if (!rootRef.current)
|
|
63
|
+
return;
|
|
64
|
+
if (!rootRef.current.contains(event.target))
|
|
65
|
+
setOpen(false);
|
|
66
|
+
}
|
|
67
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
68
|
+
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
69
|
+
}, [open, setOpen]);
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
if (!open) {
|
|
72
|
+
setPanelBox(null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
function place() {
|
|
76
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
77
|
+
if (!rect)
|
|
78
|
+
return;
|
|
79
|
+
const viewport = document.documentElement.clientWidth;
|
|
80
|
+
const width = Math.min(320, Math.max(rect.width, viewport - 24));
|
|
81
|
+
let left = 0;
|
|
82
|
+
if (rect.left + width > viewport - 12) {
|
|
83
|
+
left = rect.width - width;
|
|
84
|
+
if (rect.left + left < 12)
|
|
85
|
+
left = 12 - rect.left;
|
|
86
|
+
}
|
|
87
|
+
setPanelBox({ left, width });
|
|
88
|
+
}
|
|
89
|
+
place();
|
|
90
|
+
window.addEventListener("resize", place);
|
|
91
|
+
// A window resize is not the only thing that moves the trigger: a root text
|
|
92
|
+
// size change, a sidebar collapsing, or a container query flipping all do it
|
|
93
|
+
// without firing `resize`.
|
|
94
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
|
|
95
|
+
if (observer && triggerRef.current)
|
|
96
|
+
observer.observe(triggerRef.current);
|
|
97
|
+
if (observer)
|
|
98
|
+
observer.observe(document.documentElement);
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener("resize", place);
|
|
101
|
+
observer?.disconnect();
|
|
102
|
+
};
|
|
103
|
+
}, [open]);
|
|
104
|
+
/* The filter is where a keyboard user expects to land, and it is where typing
|
|
105
|
+
already works, so it takes focus as the popover opens. */
|
|
106
|
+
(0, react_1.useEffect)(() => {
|
|
107
|
+
if (!open)
|
|
108
|
+
return;
|
|
109
|
+
const target = filterRef.current ?? listRef.current?.querySelector("[data-option]");
|
|
110
|
+
target?.focus();
|
|
111
|
+
}, [open]);
|
|
112
|
+
(0, react_1.useEffect)(() => {
|
|
113
|
+
if (!open)
|
|
114
|
+
setQuery("");
|
|
115
|
+
}, [open]);
|
|
116
|
+
(0, react_1.useEffect)(() => {
|
|
117
|
+
setActiveIndex(0);
|
|
118
|
+
}, [query, open]);
|
|
119
|
+
const matches = (0, react_1.useMemo)(() => {
|
|
120
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
121
|
+
if (!needle)
|
|
122
|
+
return workspaces;
|
|
123
|
+
return workspaces.filter((item) => `${item.name} ${item.detail ?? ""}`.toLocaleLowerCase().includes(needle));
|
|
124
|
+
}, [query, workspaces]);
|
|
125
|
+
const showFilter = workspaces.length >= filterThreshold;
|
|
126
|
+
const hasSource = workspaces.length > 0;
|
|
127
|
+
/* ── Roving focus across the options ─────────────────────────────── */
|
|
128
|
+
function optionNodes() {
|
|
129
|
+
if (!listRef.current)
|
|
130
|
+
return [];
|
|
131
|
+
return Array.from(listRef.current.querySelectorAll("[data-option]"));
|
|
132
|
+
}
|
|
133
|
+
function focusOptionAt(index) {
|
|
134
|
+
const nodes = optionNodes();
|
|
135
|
+
if (nodes.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
const wrapped = ((index % nodes.length) + nodes.length) % nodes.length;
|
|
138
|
+
setActiveIndex(wrapped);
|
|
139
|
+
nodes[wrapped]?.focus();
|
|
140
|
+
}
|
|
141
|
+
function handleFilterKeyDown(event) {
|
|
142
|
+
if (event.key === "ArrowDown") {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
focusOptionAt(0);
|
|
145
|
+
}
|
|
146
|
+
else if (event.key === "ArrowUp") {
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
focusOptionAt(-1);
|
|
149
|
+
}
|
|
150
|
+
else if (event.key === "Escape") {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
close(true);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function handleOptionKeyDown(event, index) {
|
|
156
|
+
switch (event.key) {
|
|
157
|
+
case "ArrowDown":
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
focusOptionAt(index + 1);
|
|
160
|
+
break;
|
|
161
|
+
case "ArrowUp":
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
focusOptionAt(index - 1);
|
|
164
|
+
break;
|
|
165
|
+
case "Home":
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
focusOptionAt(0);
|
|
168
|
+
break;
|
|
169
|
+
case "End":
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
focusOptionAt(-1);
|
|
172
|
+
break;
|
|
173
|
+
case "Escape":
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
close(true);
|
|
176
|
+
break;
|
|
177
|
+
default:
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function handleTriggerKeyDown(event) {
|
|
182
|
+
if (event.key === "ArrowDown" && !open) {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
setOpen(true);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function choose(item) {
|
|
188
|
+
if ("unavailableReason" in item && item.unavailableReason)
|
|
189
|
+
return;
|
|
190
|
+
if (!onSelect)
|
|
191
|
+
return;
|
|
192
|
+
onSelect(item.id);
|
|
193
|
+
close(true);
|
|
194
|
+
}
|
|
195
|
+
/* ── Option ──────────────────────────────────────────────────────── */
|
|
196
|
+
function renderOption(item, index) {
|
|
197
|
+
const unavailable = ("unavailableReason" in item ? item.unavailableReason : undefined) ||
|
|
198
|
+
(!item.href && !onSelect ? unavailableMessage : undefined);
|
|
199
|
+
const isCurrent = current?.id === item.id;
|
|
200
|
+
const reasonId = `${uid}-${item.id}-reason`;
|
|
201
|
+
const body = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(WorkspaceMark, { item: item, size: "sm" }), (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-left", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-body-sm font-medium break-words", children: item.name }), item.detail && ((0, jsx_runtime_1.jsx)("span", { className: "text-body-xs break-words text-grey-500", children: item.detail })), unavailable && ((0, jsx_runtime_1.jsx)("span", { id: reasonId, className: "text-body-xs break-words text-grey-500", children: unavailable }))] }), isCurrent && ((0, jsx_runtime_1.jsxs)("span", { className: "ml-auto inline-flex shrink-0 items-center gap-1 text-body-xs font-semibold text-action-primary-text", children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "check", size: "sm" }), (0, jsx_runtime_1.jsx)("span", { children: "Current" })] }))] }));
|
|
202
|
+
/**
|
|
203
|
+
* The current entry is marked by the "Current" chip, not by a tint. An
|
|
204
|
+
* earlier draft tinted the row `bg-primary-50`; the muted detail line then
|
|
205
|
+
* measured 4.28:1 on it, below AA, which is the same mistake the Batch A
|
|
206
|
+
* pricing cards made with `opacity-90`. `surface-muted` is a ground the
|
|
207
|
+
* muted foreground role is already paired with in both modes.
|
|
208
|
+
*/
|
|
209
|
+
const classes = (0, cn_1.cn)("flex min-h-11 w-full min-w-0 items-start gap-3 rounded-lg px-2 py-2 transition-colors", FOCUS_RING, unavailable
|
|
210
|
+
? "cursor-not-allowed text-grey-500"
|
|
211
|
+
: "cursor-pointer text-grey-900 hover:bg-surface-muted", isCurrent && !unavailable && "bg-surface-muted");
|
|
212
|
+
const shared = {
|
|
213
|
+
"data-option": "true",
|
|
214
|
+
role: "option",
|
|
215
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
216
|
+
"aria-selected": isCurrent,
|
|
217
|
+
onKeyDown: (event) => handleOptionKeyDown(event, index),
|
|
218
|
+
onFocus: () => setActiveIndex(index),
|
|
219
|
+
className: classes,
|
|
220
|
+
};
|
|
221
|
+
if (unavailable) {
|
|
222
|
+
return ((0, jsx_runtime_1.jsx)("div", { ...shared, "aria-disabled": "true", "aria-describedby": reasonId, children: body }, item.id));
|
|
223
|
+
}
|
|
224
|
+
if (item.href) {
|
|
225
|
+
// A workspace that is its own address stays a real anchor; the listbox
|
|
226
|
+
// role does not remove the href, so middle-click still opens a new tab.
|
|
227
|
+
return ((0, jsx_runtime_1.jsx)("a", { ...shared, href: item.href, onClick: () => {
|
|
228
|
+
onSelect?.(item.id);
|
|
229
|
+
close(false);
|
|
230
|
+
}, children: body }, item.id));
|
|
231
|
+
}
|
|
232
|
+
return ((0, jsx_runtime_1.jsx)("div", { ...shared, onClick: () => choose(item), onKeyDown: (event) => {
|
|
233
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
choose(item);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
handleOptionKeyDown(event, index);
|
|
239
|
+
}, children: body }, item.id));
|
|
240
|
+
}
|
|
241
|
+
/* ── Panel body ──────────────────────────────────────────────────── */
|
|
242
|
+
function renderBody() {
|
|
243
|
+
if (status === "loading") {
|
|
244
|
+
return ((0, jsx_runtime_1.jsxs)("p", { role: "status", className: "flex items-center gap-2 px-2 py-6 text-body-sm text-grey-500", children: [(0, jsx_runtime_1.jsx)(Spinner_1.Spinner, { size: "sm" }), (0, jsx_runtime_1.jsx)("span", { children: loadingMessage })] }));
|
|
245
|
+
}
|
|
246
|
+
if (status === "error") {
|
|
247
|
+
return ((0, jsx_runtime_1.jsxs)("div", { role: "alert", className: "m-1 flex flex-col gap-2 rounded-lg border border-error-200 bg-error-50 px-3 py-3 text-body-sm text-error-700", children: [(0, jsx_runtime_1.jsx)("p", { className: "break-words", children: errorMessage }), onRetry && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onRetry, className: (0, cn_1.cn)("inline-flex min-h-9 w-fit cursor-pointer items-center gap-2 rounded-lg border-[1.5px] border-current px-3 py-1.5 font-semibold", FOCUS_RING), children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "refresh", size: "sm" }), "Try again"] }))] }));
|
|
248
|
+
}
|
|
249
|
+
if (!hasSource) {
|
|
250
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 px-2 py-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: emptyTitle }), (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm break-words text-grey-500", children: emptyDescription })] }));
|
|
251
|
+
}
|
|
252
|
+
if (matches.length === 0) {
|
|
253
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-1 px-2 py-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: noMatchesTitle }), (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm break-words text-grey-500", children: noMatchesDescription })] }));
|
|
254
|
+
}
|
|
255
|
+
return ((0, jsx_runtime_1.jsx)("div", { ref: listRef, role: "listbox", "aria-label": panelLabel, className: "flex flex-col gap-0.5 p-1", children: matches.map((item, index) => renderOption(item, index)) }));
|
|
256
|
+
}
|
|
257
|
+
/* ── Trigger ─────────────────────────────────────────────────────── */
|
|
258
|
+
/**
|
|
259
|
+
* The spoken name repeats every visible word in the order it is drawn, so a
|
|
260
|
+
* voice-control user can say what they can see (WCAG 2.5.3), and then adds
|
|
261
|
+
* the purpose the icon alone would not carry.
|
|
262
|
+
*/
|
|
263
|
+
const triggerName = current
|
|
264
|
+
? `${triggerHint ? `${triggerHint}, ` : ""}${current.name}. ${triggerLabel}`
|
|
265
|
+
: `${placeholder}. ${triggerLabel}`;
|
|
266
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: rootRef,
|
|
267
|
+
/**
|
|
268
|
+
* Deliberately NOT an `@container`. `container-type: inline-size` makes an
|
|
269
|
+
* element's inline size independent of its own contents, so this root —
|
|
270
|
+
* a shrinkable flex item inside the shell's top bar — resolved to 0px at
|
|
271
|
+
* every width and the switcher disappeared. The block has no internal
|
|
272
|
+
* container query to justify the cost: the popover is placed from a
|
|
273
|
+
* measurement instead.
|
|
274
|
+
*/
|
|
275
|
+
className: (0, cn_1.cn)("relative w-full min-w-0", className), children: [(0, jsx_runtime_1.jsxs)("button", { ref: triggerRef, type: "button", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, "aria-label": triggerName, onClick: () => setOpen(!open), onKeyDown: handleTriggerKeyDown, className: (0, cn_1.cn)("flex min-h-11 w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg border border-surface-border bg-surface px-2 py-1.5 text-left text-grey-700 transition-colors hover:bg-surface-muted", FOCUS_RING), children: [current ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(WorkspaceMark, { item: current, size: "sm" }), (0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 flex-col", children: [triggerHint && (0, jsx_runtime_1.jsx)("span", { className: "text-body-xs text-grey-500", children: triggerHint }), (0, jsx_runtime_1.jsx)("span", { className: "line-clamp-1 text-body-sm font-semibold break-all text-grey-900", children: current.name })] })] })) : ((0, jsx_runtime_1.jsx)("span", { className: "line-clamp-1 text-body-sm font-medium break-words text-grey-500", children: placeholder })), (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chevron-down", size: "sm", className: "ml-auto shrink-0 text-grey-500" })] }), (0, jsx_runtime_1.jsxs)("div", { id: panelId, role: "dialog", "aria-label": panelLabel, onKeyDown: (event) => {
|
|
276
|
+
if (event.key === "Escape") {
|
|
277
|
+
event.preventDefault();
|
|
278
|
+
close(true);
|
|
279
|
+
}
|
|
280
|
+
}, style: panelBox ? { left: panelBox.left, width: panelBox.width } : undefined, className: (0, cn_1.cn)("absolute top-[calc(100%+4px)] left-0 z-30 flex w-full min-w-0 flex-col rounded-xl border border-surface-border bg-surface shadow-soft-md", !open && "hidden"), children: [showFilter && status === "idle" && hasSource && ((0, jsx_runtime_1.jsxs)("div", { className: "border-b border-surface-border p-2", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: filterId, className: "sr-only", children: filterLabel }), (0, jsx_runtime_1.jsx)("input", { ref: filterRef, id: filterId, type: "search", value: query, placeholder: filterPlaceholder, "aria-controls": listId, onChange: (event) => setQuery(event.target.value), onKeyDown: handleFilterKeyDown, className: (0, cn_1.cn)("w-full min-w-0 rounded-lg border border-surface-border-strong bg-surface px-3 py-2 text-body-sm text-grey-900 placeholder:text-grey-500", FOCUS_RING) })] })), (0, jsx_runtime_1.jsx)("div", { id: listId, className: "flex max-h-[320px] min-h-0 flex-col overflow-y-auto", children: renderBody() }), (0, jsx_runtime_1.jsx)("p", { role: "status", "aria-live": "polite", className: "sr-only", children: open && status === "idle" && hasSource ? resultCountLabel(matches.length) : "" }), footerActions.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-1 border-t border-surface-border p-1", children: footerActions.map((action) => {
|
|
281
|
+
const inner = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [action.icon && (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: action.icon, size: "sm", className: "shrink-0" }), (0, jsx_runtime_1.jsx)("span", { className: "break-words", children: action.label })] }));
|
|
282
|
+
const classes = (0, cn_1.cn)("flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-body-sm font-medium text-grey-700 hover:bg-surface-muted hover:text-grey-900", FOCUS_RING);
|
|
283
|
+
return action.href ? ((0, jsx_runtime_1.jsx)("a", { href: action.href, onClick: () => {
|
|
284
|
+
action.onClick?.();
|
|
285
|
+
close(false);
|
|
286
|
+
}, ...(action.external ? { target: "_blank", rel: "noreferrer" } : {}), className: classes, children: inner }, action.id)) : ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => {
|
|
287
|
+
action.onClick?.();
|
|
288
|
+
close(true);
|
|
289
|
+
}, className: classes, children: inner }, action.id));
|
|
290
|
+
}) }))] })] }));
|
|
291
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
import { Icon } from "../components/Icon/index.js";
|
|
5
|
+
import { Spinner } from "../components/Spinner/index.js";
|
|
6
|
+
/* ─── Recipes ────────────────────────────────────────────────────────── */
|
|
7
|
+
const FOCUS_RING = "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text";
|
|
8
|
+
/* ─── Logo ───────────────────────────────────────────────────────────── */
|
|
9
|
+
function WorkspaceMark({ item, size = "md", }) {
|
|
10
|
+
const box = size === "sm" ? "size-7 text-body-xs" : "size-9 text-body-sm";
|
|
11
|
+
if (item.logoSrc) {
|
|
12
|
+
return (_jsx("img", { src: item.logoSrc, alt: "", className: cn("shrink-0 rounded-lg border border-surface-border object-cover", box) }));
|
|
13
|
+
}
|
|
14
|
+
return (_jsx("span", { "aria-hidden": "true",
|
|
15
|
+
// The colour comes last: `box` carries a `text-body-*` size utility, and
|
|
16
|
+
// class merging treats it as the same group, so a colour written first is
|
|
17
|
+
// dropped and the initials fall back to the inherited foreground.
|
|
18
|
+
className: cn("inline-flex shrink-0 items-center justify-center rounded-lg bg-primary-50 font-semibold", box, "text-action-primary-text"), children: item.icon ? (_jsx(Icon, { name: item.icon, size: size === "sm" ? "sm" : "md" })) : ((item.initials ?? item.name.slice(0, 2).toUpperCase())) }));
|
|
19
|
+
}
|
|
20
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
21
|
+
export function WorkspaceSwitcherBlock({ current, workspaces = [], onSelect, unavailableMessage = "Switching to this workspace is not available.", status = "idle", errorMessage = "Your workspaces could not be loaded.", onRetry, loadingMessage = "Loading workspaces", triggerLabel = "Switch workspace", triggerHint, placeholder = "Choose a workspace", panelLabel = "Workspaces", filterLabel = "Filter workspaces", filterPlaceholder = "Type to filter", filterThreshold = 6, emptyTitle = "You are not in any workspace yet", emptyDescription = "Create a workspace or ask an administrator for an invitation.", noMatchesTitle = "No workspace matches that filter", noMatchesDescription = "Check the spelling, or clear the filter to see every workspace.", footerActions = [], resultCountLabel = (count) => (count === 1 ? "1 workspace matches" : `${count} workspaces match`), defaultOpen = false, open: controlledOpen, onOpenChange, className, }) {
|
|
22
|
+
const uid = useId();
|
|
23
|
+
const panelId = `${uid}-panel`;
|
|
24
|
+
const listId = `${uid}-list`;
|
|
25
|
+
const filterId = `${uid}-filter`;
|
|
26
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
27
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
28
|
+
const [query, setQuery] = useState("");
|
|
29
|
+
/** Roving tabindex: exactly one option is in the tab order at a time. */
|
|
30
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
31
|
+
const rootRef = useRef(null);
|
|
32
|
+
const triggerRef = useRef(null);
|
|
33
|
+
/**
|
|
34
|
+
* The popover is wider than its trigger, so a trigger sitting near the right
|
|
35
|
+
* edge would push the popover past it and widen the document. The offset is
|
|
36
|
+
* measured from the trigger when the popover opens: start-aligned when it
|
|
37
|
+
* fits, end-aligned when it does not, and pinned to the viewport when neither
|
|
38
|
+
* side fits. Nothing is positioned until it is measured.
|
|
39
|
+
*/
|
|
40
|
+
const [panelBox, setPanelBox] = useState(null);
|
|
41
|
+
const filterRef = useRef(null);
|
|
42
|
+
const listRef = useRef(null);
|
|
43
|
+
const setOpen = useCallback((next) => {
|
|
44
|
+
if (controlledOpen === undefined)
|
|
45
|
+
setUncontrolledOpen(next);
|
|
46
|
+
onOpenChange?.(next);
|
|
47
|
+
}, [controlledOpen, onOpenChange]);
|
|
48
|
+
const close = useCallback((restoreFocus) => {
|
|
49
|
+
setOpen(false);
|
|
50
|
+
if (restoreFocus)
|
|
51
|
+
triggerRef.current?.focus();
|
|
52
|
+
}, [setOpen]);
|
|
53
|
+
/* Closing when the pointer lands outside keeps the popover from shadowing the
|
|
54
|
+
page; it is deliberately not a modal, so focus is never trapped here. */
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!open)
|
|
57
|
+
return;
|
|
58
|
+
function handlePointerDown(event) {
|
|
59
|
+
if (!rootRef.current)
|
|
60
|
+
return;
|
|
61
|
+
if (!rootRef.current.contains(event.target))
|
|
62
|
+
setOpen(false);
|
|
63
|
+
}
|
|
64
|
+
document.addEventListener("mousedown", handlePointerDown);
|
|
65
|
+
return () => document.removeEventListener("mousedown", handlePointerDown);
|
|
66
|
+
}, [open, setOpen]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!open) {
|
|
69
|
+
setPanelBox(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
function place() {
|
|
73
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
74
|
+
if (!rect)
|
|
75
|
+
return;
|
|
76
|
+
const viewport = document.documentElement.clientWidth;
|
|
77
|
+
const width = Math.min(320, Math.max(rect.width, viewport - 24));
|
|
78
|
+
let left = 0;
|
|
79
|
+
if (rect.left + width > viewport - 12) {
|
|
80
|
+
left = rect.width - width;
|
|
81
|
+
if (rect.left + left < 12)
|
|
82
|
+
left = 12 - rect.left;
|
|
83
|
+
}
|
|
84
|
+
setPanelBox({ left, width });
|
|
85
|
+
}
|
|
86
|
+
place();
|
|
87
|
+
window.addEventListener("resize", place);
|
|
88
|
+
// A window resize is not the only thing that moves the trigger: a root text
|
|
89
|
+
// size change, a sidebar collapsing, or a container query flipping all do it
|
|
90
|
+
// without firing `resize`.
|
|
91
|
+
const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => place());
|
|
92
|
+
if (observer && triggerRef.current)
|
|
93
|
+
observer.observe(triggerRef.current);
|
|
94
|
+
if (observer)
|
|
95
|
+
observer.observe(document.documentElement);
|
|
96
|
+
return () => {
|
|
97
|
+
window.removeEventListener("resize", place);
|
|
98
|
+
observer?.disconnect();
|
|
99
|
+
};
|
|
100
|
+
}, [open]);
|
|
101
|
+
/* The filter is where a keyboard user expects to land, and it is where typing
|
|
102
|
+
already works, so it takes focus as the popover opens. */
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!open)
|
|
105
|
+
return;
|
|
106
|
+
const target = filterRef.current ?? listRef.current?.querySelector("[data-option]");
|
|
107
|
+
target?.focus();
|
|
108
|
+
}, [open]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!open)
|
|
111
|
+
setQuery("");
|
|
112
|
+
}, [open]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
setActiveIndex(0);
|
|
115
|
+
}, [query, open]);
|
|
116
|
+
const matches = useMemo(() => {
|
|
117
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
118
|
+
if (!needle)
|
|
119
|
+
return workspaces;
|
|
120
|
+
return workspaces.filter((item) => `${item.name} ${item.detail ?? ""}`.toLocaleLowerCase().includes(needle));
|
|
121
|
+
}, [query, workspaces]);
|
|
122
|
+
const showFilter = workspaces.length >= filterThreshold;
|
|
123
|
+
const hasSource = workspaces.length > 0;
|
|
124
|
+
/* ── Roving focus across the options ─────────────────────────────── */
|
|
125
|
+
function optionNodes() {
|
|
126
|
+
if (!listRef.current)
|
|
127
|
+
return [];
|
|
128
|
+
return Array.from(listRef.current.querySelectorAll("[data-option]"));
|
|
129
|
+
}
|
|
130
|
+
function focusOptionAt(index) {
|
|
131
|
+
const nodes = optionNodes();
|
|
132
|
+
if (nodes.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const wrapped = ((index % nodes.length) + nodes.length) % nodes.length;
|
|
135
|
+
setActiveIndex(wrapped);
|
|
136
|
+
nodes[wrapped]?.focus();
|
|
137
|
+
}
|
|
138
|
+
function handleFilterKeyDown(event) {
|
|
139
|
+
if (event.key === "ArrowDown") {
|
|
140
|
+
event.preventDefault();
|
|
141
|
+
focusOptionAt(0);
|
|
142
|
+
}
|
|
143
|
+
else if (event.key === "ArrowUp") {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
focusOptionAt(-1);
|
|
146
|
+
}
|
|
147
|
+
else if (event.key === "Escape") {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
close(true);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function handleOptionKeyDown(event, index) {
|
|
153
|
+
switch (event.key) {
|
|
154
|
+
case "ArrowDown":
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
focusOptionAt(index + 1);
|
|
157
|
+
break;
|
|
158
|
+
case "ArrowUp":
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
focusOptionAt(index - 1);
|
|
161
|
+
break;
|
|
162
|
+
case "Home":
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
focusOptionAt(0);
|
|
165
|
+
break;
|
|
166
|
+
case "End":
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
focusOptionAt(-1);
|
|
169
|
+
break;
|
|
170
|
+
case "Escape":
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
close(true);
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function handleTriggerKeyDown(event) {
|
|
179
|
+
if (event.key === "ArrowDown" && !open) {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
setOpen(true);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function choose(item) {
|
|
185
|
+
if ("unavailableReason" in item && item.unavailableReason)
|
|
186
|
+
return;
|
|
187
|
+
if (!onSelect)
|
|
188
|
+
return;
|
|
189
|
+
onSelect(item.id);
|
|
190
|
+
close(true);
|
|
191
|
+
}
|
|
192
|
+
/* ── Option ──────────────────────────────────────────────────────── */
|
|
193
|
+
function renderOption(item, index) {
|
|
194
|
+
const unavailable = ("unavailableReason" in item ? item.unavailableReason : undefined) ||
|
|
195
|
+
(!item.href && !onSelect ? unavailableMessage : undefined);
|
|
196
|
+
const isCurrent = current?.id === item.id;
|
|
197
|
+
const reasonId = `${uid}-${item.id}-reason`;
|
|
198
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(WorkspaceMark, { item: item, size: "sm" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5 text-left", children: [_jsx("span", { className: "text-body-sm font-medium break-words", children: item.name }), item.detail && (_jsx("span", { className: "text-body-xs break-words text-grey-500", children: item.detail })), unavailable && (_jsx("span", { id: reasonId, className: "text-body-xs break-words text-grey-500", children: unavailable }))] }), isCurrent && (_jsxs("span", { className: "ml-auto inline-flex shrink-0 items-center gap-1 text-body-xs font-semibold text-action-primary-text", children: [_jsx(Icon, { name: "check", size: "sm" }), _jsx("span", { children: "Current" })] }))] }));
|
|
199
|
+
/**
|
|
200
|
+
* The current entry is marked by the "Current" chip, not by a tint. An
|
|
201
|
+
* earlier draft tinted the row `bg-primary-50`; the muted detail line then
|
|
202
|
+
* measured 4.28:1 on it, below AA, which is the same mistake the Batch A
|
|
203
|
+
* pricing cards made with `opacity-90`. `surface-muted` is a ground the
|
|
204
|
+
* muted foreground role is already paired with in both modes.
|
|
205
|
+
*/
|
|
206
|
+
const classes = cn("flex min-h-11 w-full min-w-0 items-start gap-3 rounded-lg px-2 py-2 transition-colors", FOCUS_RING, unavailable
|
|
207
|
+
? "cursor-not-allowed text-grey-500"
|
|
208
|
+
: "cursor-pointer text-grey-900 hover:bg-surface-muted", isCurrent && !unavailable && "bg-surface-muted");
|
|
209
|
+
const shared = {
|
|
210
|
+
"data-option": "true",
|
|
211
|
+
role: "option",
|
|
212
|
+
tabIndex: index === activeIndex ? 0 : -1,
|
|
213
|
+
"aria-selected": isCurrent,
|
|
214
|
+
onKeyDown: (event) => handleOptionKeyDown(event, index),
|
|
215
|
+
onFocus: () => setActiveIndex(index),
|
|
216
|
+
className: classes,
|
|
217
|
+
};
|
|
218
|
+
if (unavailable) {
|
|
219
|
+
return (_jsx("div", { ...shared, "aria-disabled": "true", "aria-describedby": reasonId, children: body }, item.id));
|
|
220
|
+
}
|
|
221
|
+
if (item.href) {
|
|
222
|
+
// A workspace that is its own address stays a real anchor; the listbox
|
|
223
|
+
// role does not remove the href, so middle-click still opens a new tab.
|
|
224
|
+
return (_jsx("a", { ...shared, href: item.href, onClick: () => {
|
|
225
|
+
onSelect?.(item.id);
|
|
226
|
+
close(false);
|
|
227
|
+
}, children: body }, item.id));
|
|
228
|
+
}
|
|
229
|
+
return (_jsx("div", { ...shared, onClick: () => choose(item), onKeyDown: (event) => {
|
|
230
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
choose(item);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
handleOptionKeyDown(event, index);
|
|
236
|
+
}, children: body }, item.id));
|
|
237
|
+
}
|
|
238
|
+
/* ── Panel body ──────────────────────────────────────────────────── */
|
|
239
|
+
function renderBody() {
|
|
240
|
+
if (status === "loading") {
|
|
241
|
+
return (_jsxs("p", { role: "status", className: "flex items-center gap-2 px-2 py-6 text-body-sm text-grey-500", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: loadingMessage })] }));
|
|
242
|
+
}
|
|
243
|
+
if (status === "error") {
|
|
244
|
+
return (_jsxs("div", { role: "alert", className: "m-1 flex flex-col gap-2 rounded-lg border border-error-200 bg-error-50 px-3 py-3 text-body-sm text-error-700", children: [_jsx("p", { className: "break-words", children: errorMessage }), onRetry && (_jsxs("button", { type: "button", onClick: onRetry, className: cn("inline-flex min-h-9 w-fit cursor-pointer items-center gap-2 rounded-lg border-[1.5px] border-current px-3 py-1.5 font-semibold", FOCUS_RING), children: [_jsx(Icon, { name: "refresh", size: "sm" }), "Try again"] }))] }));
|
|
245
|
+
}
|
|
246
|
+
if (!hasSource) {
|
|
247
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-2 py-6", children: [_jsx("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: emptyTitle }), _jsx("p", { className: "text-body-sm break-words text-grey-500", children: emptyDescription })] }));
|
|
248
|
+
}
|
|
249
|
+
if (matches.length === 0) {
|
|
250
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-2 py-6", children: [_jsx("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: noMatchesTitle }), _jsx("p", { className: "text-body-sm break-words text-grey-500", children: noMatchesDescription })] }));
|
|
251
|
+
}
|
|
252
|
+
return (_jsx("div", { ref: listRef, role: "listbox", "aria-label": panelLabel, className: "flex flex-col gap-0.5 p-1", children: matches.map((item, index) => renderOption(item, index)) }));
|
|
253
|
+
}
|
|
254
|
+
/* ── Trigger ─────────────────────────────────────────────────────── */
|
|
255
|
+
/**
|
|
256
|
+
* The spoken name repeats every visible word in the order it is drawn, so a
|
|
257
|
+
* voice-control user can say what they can see (WCAG 2.5.3), and then adds
|
|
258
|
+
* the purpose the icon alone would not carry.
|
|
259
|
+
*/
|
|
260
|
+
const triggerName = current
|
|
261
|
+
? `${triggerHint ? `${triggerHint}, ` : ""}${current.name}. ${triggerLabel}`
|
|
262
|
+
: `${placeholder}. ${triggerLabel}`;
|
|
263
|
+
return (_jsxs("div", { ref: rootRef,
|
|
264
|
+
/**
|
|
265
|
+
* Deliberately NOT an `@container`. `container-type: inline-size` makes an
|
|
266
|
+
* element's inline size independent of its own contents, so this root —
|
|
267
|
+
* a shrinkable flex item inside the shell's top bar — resolved to 0px at
|
|
268
|
+
* every width and the switcher disappeared. The block has no internal
|
|
269
|
+
* container query to justify the cost: the popover is placed from a
|
|
270
|
+
* measurement instead.
|
|
271
|
+
*/
|
|
272
|
+
className: cn("relative w-full min-w-0", className), children: [_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": panelId, "aria-label": triggerName, onClick: () => setOpen(!open), onKeyDown: handleTriggerKeyDown, className: cn("flex min-h-11 w-full min-w-0 cursor-pointer items-center gap-2 rounded-lg border border-surface-border bg-surface px-2 py-1.5 text-left text-grey-700 transition-colors hover:bg-surface-muted", FOCUS_RING), children: [current ? (_jsxs(_Fragment, { children: [_jsx(WorkspaceMark, { item: current, size: "sm" }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [triggerHint && _jsx("span", { className: "text-body-xs text-grey-500", children: triggerHint }), _jsx("span", { className: "line-clamp-1 text-body-sm font-semibold break-all text-grey-900", children: current.name })] })] })) : (_jsx("span", { className: "line-clamp-1 text-body-sm font-medium break-words text-grey-500", children: placeholder })), _jsx(Icon, { name: "chevron-down", size: "sm", className: "ml-auto shrink-0 text-grey-500" })] }), _jsxs("div", { id: panelId, role: "dialog", "aria-label": panelLabel, onKeyDown: (event) => {
|
|
273
|
+
if (event.key === "Escape") {
|
|
274
|
+
event.preventDefault();
|
|
275
|
+
close(true);
|
|
276
|
+
}
|
|
277
|
+
}, style: panelBox ? { left: panelBox.left, width: panelBox.width } : undefined, className: cn("absolute top-[calc(100%+4px)] left-0 z-30 flex w-full min-w-0 flex-col rounded-xl border border-surface-border bg-surface shadow-soft-md", !open && "hidden"), children: [showFilter && status === "idle" && hasSource && (_jsxs("div", { className: "border-b border-surface-border p-2", children: [_jsx("label", { htmlFor: filterId, className: "sr-only", children: filterLabel }), _jsx("input", { ref: filterRef, id: filterId, type: "search", value: query, placeholder: filterPlaceholder, "aria-controls": listId, onChange: (event) => setQuery(event.target.value), onKeyDown: handleFilterKeyDown, className: cn("w-full min-w-0 rounded-lg border border-surface-border-strong bg-surface px-3 py-2 text-body-sm text-grey-900 placeholder:text-grey-500", FOCUS_RING) })] })), _jsx("div", { id: listId, className: "flex max-h-[320px] min-h-0 flex-col overflow-y-auto", children: renderBody() }), _jsx("p", { role: "status", "aria-live": "polite", className: "sr-only", children: open && status === "idle" && hasSource ? resultCountLabel(matches.length) : "" }), footerActions.length > 0 && (_jsx("div", { className: "flex flex-col gap-1 border-t border-surface-border p-1", children: footerActions.map((action) => {
|
|
278
|
+
const inner = (_jsxs(_Fragment, { children: [action.icon && _jsx(Icon, { name: action.icon, size: "sm", className: "shrink-0" }), _jsx("span", { className: "break-words", children: action.label })] }));
|
|
279
|
+
const classes = cn("flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-body-sm font-medium text-grey-700 hover:bg-surface-muted hover:text-grey-900", FOCUS_RING);
|
|
280
|
+
return action.href ? (_jsx("a", { href: action.href, onClick: () => {
|
|
281
|
+
action.onClick?.();
|
|
282
|
+
close(false);
|
|
283
|
+
}, ...(action.external ? { target: "_blank", rel: "noreferrer" } : {}), className: classes, children: inner }, action.id)) : (_jsx("button", { type: "button", onClick: () => {
|
|
284
|
+
action.onClick?.();
|
|
285
|
+
close(true);
|
|
286
|
+
}, className: classes, children: inner }, action.id));
|
|
287
|
+
}) }))] })] }));
|
|
288
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commerceAction = exports.commerceControl = void 0;
|
|
4
|
+
exports.formatCommerceMoney = formatCommerceMoney;
|
|
5
|
+
exports.CommerceHeading = CommerceHeading;
|
|
6
|
+
exports.CommerceFrame = CommerceFrame;
|
|
7
|
+
exports.CommercePhoto = CommercePhoto;
|
|
8
|
+
exports.CommerceQuantity = CommerceQuantity;
|
|
9
|
+
exports.CommerceTotals = CommerceTotals;
|
|
10
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
11
|
+
const react_1 = require("react");
|
|
12
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
13
|
+
exports.commerceControl = "cursor-pointer min-h-11 rounded-lg border border-surface-border-strong bg-surface px-3 py-2 text-base text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary motion-safe:transition-colors motion-safe:duration-150 hover:border-grey-500 disabled:cursor-not-allowed disabled:opacity-50";
|
|
14
|
+
exports.commerceAction = "inline-flex min-h-11 items-center justify-center rounded-lg bg-action-primary px-5 py-2.5 text-center text-sm font-semibold text-white hover:bg-action-primary-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary disabled:cursor-not-allowed disabled:opacity-50";
|
|
15
|
+
function formatCommerceMoney(amount, currency = "GBP", locale = "en-GB") {
|
|
16
|
+
const format = new Intl.NumberFormat(locale, { style: "currency", currency });
|
|
17
|
+
const digits = format.resolvedOptions().maximumFractionDigits ?? 2;
|
|
18
|
+
return format.format(amount / 10 ** digits);
|
|
19
|
+
}
|
|
20
|
+
function CommerceHeading({ title, description, headingLevel: Heading = "h2", id, }) {
|
|
21
|
+
return ((0, jsx_runtime_1.jsxs)("header", { className: "space-y-3", children: [(0, jsx_runtime_1.jsx)(Heading, { id: id, className: "max-w-[20ch] text-[clamp(1.75rem,4cqi,2.75rem)] leading-[1.12] font-semibold tracking-[-0.035em]", children: title }), description && ((0, jsx_runtime_1.jsx)("p", { className: "max-w-[56ch] text-base leading-relaxed text-grey-600", children: description }))] }));
|
|
22
|
+
}
|
|
23
|
+
function CommerceFrame({ title, description, headingLevel, className, children, busy, }) {
|
|
24
|
+
const id = (0, react_1.useId)();
|
|
25
|
+
return ((0, jsx_runtime_1.jsxs)("section", { "aria-labelledby": id, "aria-busy": busy || undefined, className: (0, cn_1.cn)("@container min-w-0 w-full bg-surface text-grey-900 [overflow-wrap:anywhere]", className), children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-8 @min-[760px]:mb-10", children: (0, jsx_runtime_1.jsx)(CommerceHeading, { id: id, title: title, description: description, headingLevel: headingLevel }) }), children] }));
|
|
26
|
+
}
|
|
27
|
+
function CommercePhoto({ image, className, priority = false, }) {
|
|
28
|
+
return image ? ((0, jsx_runtime_1.jsx)("img", { src: image.src, alt: image.alt, loading: priority ? "eager" : "lazy", width: 640, height: 640, className: (0, cn_1.cn)("aspect-square w-full rounded-lg bg-surface-muted object-cover", className) })) : ((0, jsx_runtime_1.jsx)("div", { role: "img", "aria-label": "Product image unavailable", className: (0, cn_1.cn)("flex aspect-square items-center justify-center rounded-xl bg-surface-muted p-4 text-center text-sm text-grey-600", className), children: "Image unavailable" }));
|
|
29
|
+
}
|
|
30
|
+
function CommerceQuantity({ name, quantity, max, disabled, onChange, }) {
|
|
31
|
+
return ((0, jsx_runtime_1.jsxs)("div", { role: "group", "aria-label": `Quantity for ${name}`, className: "inline-flex shrink-0 items-center rounded-lg border border-surface-border-strong bg-surface", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": `Decrease quantity for ${name}`, disabled: disabled || quantity <= 1, className: (0, cn_1.cn)(exports.commerceControl, "w-11 border-0 bg-transparent px-0 hover:bg-surface-muted"), onClick: () => onChange(quantity - 1), children: "\u2212" }), (0, jsx_runtime_1.jsx)("output", { "aria-label": `Quantity for ${name}`, className: "min-w-7 px-1 text-center tabular-nums", children: quantity }), (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": `Increase quantity for ${name}`, disabled: disabled || quantity >= (max ?? 99), className: (0, cn_1.cn)(exports.commerceControl, "w-11 border-0 bg-transparent px-0 hover:bg-surface-muted"), onClick: () => onChange(quantity + 1), children: "+" })] }));
|
|
32
|
+
}
|
|
33
|
+
function CommerceTotals({ items, shipping = 0, tax = 0, discount = 0, currency, locale, }) {
|
|
34
|
+
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
35
|
+
const total = Math.max(0, subtotal + shipping + tax - discount);
|
|
36
|
+
const rows = [
|
|
37
|
+
["Subtotal", subtotal],
|
|
38
|
+
["Delivery", shipping],
|
|
39
|
+
["Tax", tax],
|
|
40
|
+
...(discount ? [["Discount", -discount]] : []),
|
|
41
|
+
["Total", total],
|
|
42
|
+
];
|
|
43
|
+
return ((0, jsx_runtime_1.jsx)("dl", { className: "space-y-3 text-sm", children: rows.map(([label, amount]) => ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex items-baseline justify-between gap-4", label === "Total" && "border-t border-surface-border-strong pt-5 text-xl font-semibold"), children: [(0, jsx_runtime_1.jsx)("dt", { children: label }), (0, jsx_runtime_1.jsx)("dd", { className: "text-right tabular-nums", children: formatCommerceMoney(amount, currency, locale) })] }, label))) }));
|
|
44
|
+
}
|