@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,331 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Only elements Tab can actually reach. A roving-tabindex option is an
|
|
10
|
+
* `a[href]` parked at `tabindex="-1"`: focusable in code, skipped by Tab. Left
|
|
11
|
+
* in this list it became the computed final stop and focus escaped the dialog.
|
|
12
|
+
*/
|
|
13
|
+
const FOCUSABLE = [
|
|
14
|
+
'a[href]:not([tabindex="-1"])',
|
|
15
|
+
'button:not([disabled]):not([tabindex="-1"])',
|
|
16
|
+
'input:not([disabled]):not([tabindex="-1"])',
|
|
17
|
+
'select:not([disabled]):not([tabindex="-1"])',
|
|
18
|
+
'textarea:not([disabled]):not([tabindex="-1"])',
|
|
19
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
20
|
+
].join(", ");
|
|
21
|
+
function itemMatches(item, needle) {
|
|
22
|
+
const haystack = [item.label, item.description ?? "", ...(item.keywords ?? [])]
|
|
23
|
+
.join(" ")
|
|
24
|
+
.toLocaleLowerCase();
|
|
25
|
+
return haystack.includes(needle);
|
|
26
|
+
}
|
|
27
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
28
|
+
export function CommandPaletteBlock({ items = [], groups = [], suggestions, suggestionsLabel = "Suggestions", showAllWhenEmpty = false, filterMode = "local", onQueryChange, onSelect, status = "idle", errorTitle = "Search is unavailable", errorDescription = "The search service did not respond. Nothing has been changed.", onRetry, loadingMessage = "Searching", dialogLabel = "Command palette", searchLabel = "Search destinations and actions", searchPlaceholder = "Search or jump to…", searchHint = "Use the up and down arrow keys to move through results, then Enter to open one.", promptTitle = "Type to search", promptDescription = "Start typing to find a page, a record, or an action.", noResultsTitle = (query) => `No results for “${query}”`, noResultsDescription = "Check the spelling, or try a shorter search term.", emptySourceTitle = "Nothing is available to search yet", emptySourceDescription = "This workspace has no destinations or actions configured.", resultCountLabel = (count) => (count === 1 ? "1 result" : `${count} results`), showTrigger = true, triggerLabel = "Search", triggerClassName, shortcut = null, defaultOpen = false, open: controlledOpen, onOpenChange, closeLabel = "Close", lockScroll = true, footer, className, }) {
|
|
29
|
+
const uid = useId();
|
|
30
|
+
const dialogId = `${uid}-dialog`;
|
|
31
|
+
const listId = `${uid}-list`;
|
|
32
|
+
const inputId = `${uid}-input`;
|
|
33
|
+
const hintId = `${uid}-hint`;
|
|
34
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
35
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
36
|
+
const [query, setQuery] = useState("");
|
|
37
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
38
|
+
const triggerRef = useRef(null);
|
|
39
|
+
const dialogRef = useRef(null);
|
|
40
|
+
const inputRef = useRef(null);
|
|
41
|
+
const listRef = useRef(null);
|
|
42
|
+
/** Whatever had focus before the overlay opened, restored on close. */
|
|
43
|
+
const returnFocusRef = useRef(null);
|
|
44
|
+
const setOpen = useCallback((next) => {
|
|
45
|
+
if (controlledOpen === undefined)
|
|
46
|
+
setUncontrolledOpen(next);
|
|
47
|
+
onOpenChange?.(next);
|
|
48
|
+
}, [controlledOpen, onOpenChange]);
|
|
49
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
50
|
+
/* ── Result set ──────────────────────────────────────────────────── */
|
|
51
|
+
const trimmed = query.trim();
|
|
52
|
+
const needle = trimmed.toLocaleLowerCase();
|
|
53
|
+
const results = useMemo(() => {
|
|
54
|
+
if (!trimmed) {
|
|
55
|
+
if (suggestions && suggestions.length > 0)
|
|
56
|
+
return suggestions;
|
|
57
|
+
return showAllWhenEmpty ? items : [];
|
|
58
|
+
}
|
|
59
|
+
if (filterMode === "external")
|
|
60
|
+
return items;
|
|
61
|
+
return items.filter((item) => itemMatches(item, needle));
|
|
62
|
+
}, [trimmed, needle, items, suggestions, showAllWhenEmpty, filterMode]);
|
|
63
|
+
const showingSuggestions = !trimmed && Boolean(suggestions && suggestions.length > 0);
|
|
64
|
+
const hasSource = items.length > 0 || (suggestions?.length ?? 0) > 0;
|
|
65
|
+
const promptState = !trimmed && results.length === 0 && hasSource;
|
|
66
|
+
const noResultsState = Boolean(trimmed) && results.length === 0 && status === "idle";
|
|
67
|
+
/** Grouped for display, ordered by `groups`, with anything ungrouped first. */
|
|
68
|
+
const sections = useMemo(() => {
|
|
69
|
+
if (showingSuggestions) {
|
|
70
|
+
return [{ id: "__suggestions", label: suggestionsLabel, items: results }];
|
|
71
|
+
}
|
|
72
|
+
const ungrouped = results.filter((item) => !item.groupId);
|
|
73
|
+
const known = groups
|
|
74
|
+
.map((group) => ({
|
|
75
|
+
id: group.id,
|
|
76
|
+
label: group.label,
|
|
77
|
+
items: results.filter((item) => item.groupId === group.id),
|
|
78
|
+
}))
|
|
79
|
+
.filter((section) => section.items.length > 0);
|
|
80
|
+
const knownIds = new Set(groups.map((group) => group.id));
|
|
81
|
+
const orphans = results.filter((item) => item.groupId && !knownIds.has(item.groupId));
|
|
82
|
+
const head = ungrouped.length > 0 || orphans.length > 0
|
|
83
|
+
? [
|
|
84
|
+
{
|
|
85
|
+
id: "__ungrouped",
|
|
86
|
+
label: undefined,
|
|
87
|
+
items: [...ungrouped, ...orphans],
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: [];
|
|
91
|
+
return [...head, ...known];
|
|
92
|
+
}, [results, groups, showingSuggestions, suggestionsLabel]);
|
|
93
|
+
/** Flat order, so arrow keys cross group boundaries the way the eye does. */
|
|
94
|
+
const flatItems = useMemo(() => sections.flatMap((section) => section.items), [sections]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
setActiveIndex(0);
|
|
97
|
+
}, [query, open]);
|
|
98
|
+
/* ── Open / close side effects ───────────────────────────────────── */
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!open)
|
|
101
|
+
return;
|
|
102
|
+
returnFocusRef.current = document.activeElement ?? null;
|
|
103
|
+
// Focused in the same commit that removes `hidden`, so the field has focus
|
|
104
|
+
// by the time the click that opened the dialog has settled.
|
|
105
|
+
inputRef.current?.focus();
|
|
106
|
+
}, [open]);
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (open)
|
|
109
|
+
return;
|
|
110
|
+
setQuery("");
|
|
111
|
+
}, [open]);
|
|
112
|
+
/**
|
|
113
|
+
* Focus goes back to whatever opened the palette, which is the trigger when
|
|
114
|
+
* the block renders its own. A dialog that drops focus on the document body
|
|
115
|
+
* strands a keyboard user at the top of the page.
|
|
116
|
+
*/
|
|
117
|
+
const wasOpen = useRef(open);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (wasOpen.current && !open) {
|
|
120
|
+
const target = triggerRef.current ?? returnFocusRef.current;
|
|
121
|
+
target?.focus();
|
|
122
|
+
}
|
|
123
|
+
wasOpen.current = open;
|
|
124
|
+
}, [open]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!open || !lockScroll || typeof document === "undefined")
|
|
127
|
+
return;
|
|
128
|
+
const previous = document.body.style.overflow;
|
|
129
|
+
document.body.style.overflow = "hidden";
|
|
130
|
+
return () => {
|
|
131
|
+
document.body.style.overflow = previous;
|
|
132
|
+
};
|
|
133
|
+
}, [open, lockScroll]);
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!shortcut || typeof document === "undefined")
|
|
136
|
+
return;
|
|
137
|
+
const wanted = shortcut.key.toLowerCase();
|
|
138
|
+
const modifier = shortcut.modifier ?? "mod";
|
|
139
|
+
function handle(event) {
|
|
140
|
+
if (event.defaultPrevented || event.isComposing || event.repeat)
|
|
141
|
+
return;
|
|
142
|
+
if (event.key.toLowerCase() !== wanted)
|
|
143
|
+
return;
|
|
144
|
+
const target = event.target;
|
|
145
|
+
// Single-letter shortcuts must never consume text entry, including our search field.
|
|
146
|
+
if (modifier === "none" &&
|
|
147
|
+
(event.altKey ||
|
|
148
|
+
event.shiftKey ||
|
|
149
|
+
(target instanceof HTMLElement &&
|
|
150
|
+
(target.isContentEditable ||
|
|
151
|
+
target.closest("input, textarea, select, [role='textbox']")))))
|
|
152
|
+
return;
|
|
153
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
154
|
+
if (modifier === "mod" && !mod)
|
|
155
|
+
return;
|
|
156
|
+
if (modifier === "none" && mod)
|
|
157
|
+
return;
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
setOpen(true);
|
|
160
|
+
}
|
|
161
|
+
document.addEventListener("keydown", handle);
|
|
162
|
+
return () => document.removeEventListener("keydown", handle);
|
|
163
|
+
}, [shortcut, setOpen]);
|
|
164
|
+
/* ── Roving focus ────────────────────────────────────────────────── */
|
|
165
|
+
function optionNodes() {
|
|
166
|
+
if (!listRef.current)
|
|
167
|
+
return [];
|
|
168
|
+
return Array.from(listRef.current.querySelectorAll("[data-option]"));
|
|
169
|
+
}
|
|
170
|
+
function focusOptionAt(index) {
|
|
171
|
+
const nodes = optionNodes();
|
|
172
|
+
if (nodes.length === 0)
|
|
173
|
+
return;
|
|
174
|
+
const wrapped = ((index % nodes.length) + nodes.length) % nodes.length;
|
|
175
|
+
setActiveIndex(wrapped);
|
|
176
|
+
nodes[wrapped]?.focus();
|
|
177
|
+
}
|
|
178
|
+
function activateAt(index) {
|
|
179
|
+
const nodes = optionNodes();
|
|
180
|
+
nodes[index]?.click();
|
|
181
|
+
}
|
|
182
|
+
function handleInputKeyDown(event) {
|
|
183
|
+
if (event.key === "ArrowDown") {
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
focusOptionAt(0);
|
|
186
|
+
}
|
|
187
|
+
else if (event.key === "ArrowUp") {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
focusOptionAt(-1);
|
|
190
|
+
}
|
|
191
|
+
else if (event.key === "Enter" && flatItems.length > 0) {
|
|
192
|
+
event.preventDefault();
|
|
193
|
+
activateAt(activeIndex);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function handleOptionKeyDown(event, index) {
|
|
197
|
+
switch (event.key) {
|
|
198
|
+
case "ArrowDown":
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
focusOptionAt(index + 1);
|
|
201
|
+
break;
|
|
202
|
+
case "ArrowUp":
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
focusOptionAt(index - 1);
|
|
205
|
+
break;
|
|
206
|
+
case "Home":
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
focusOptionAt(0);
|
|
209
|
+
break;
|
|
210
|
+
case "End":
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
focusOptionAt(-1);
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Tab stays inside the overlay; Escape leaves it. */
|
|
219
|
+
function handleDialogKeyDown(event) {
|
|
220
|
+
if (event.key === "Escape") {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
close();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (event.key !== "Tab" || !dialogRef.current)
|
|
226
|
+
return;
|
|
227
|
+
const nodes = Array.from(dialogRef.current.querySelectorAll(FOCUSABLE)).filter((node) => node.offsetParent !== null || node === document.activeElement);
|
|
228
|
+
if (nodes.length === 0)
|
|
229
|
+
return;
|
|
230
|
+
const first = nodes[0];
|
|
231
|
+
const last = nodes[nodes.length - 1];
|
|
232
|
+
const active = document.activeElement;
|
|
233
|
+
if (event.shiftKey && (active === first || !dialogRef.current.contains(active))) {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
last.focus();
|
|
236
|
+
}
|
|
237
|
+
else if (!event.shiftKey && active === last) {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
first.focus();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/* ── Option ──────────────────────────────────────────────────────── */
|
|
243
|
+
function renderOption(item, index) {
|
|
244
|
+
const unavailable = "unavailableReason" in item ? item.unavailableReason : undefined;
|
|
245
|
+
const reasonId = `${uid}-${item.id}-reason`;
|
|
246
|
+
const isActive = index === activeIndex;
|
|
247
|
+
const body = (_jsxs(_Fragment, { children: [item.icon && (_jsx(Icon, { name: item.icon, size: "md", className: cn("mt-0.5 shrink-0", unavailable ? "text-grey-400" : "text-grey-500") })), _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.label }), item.description && (_jsx("span", { className: "text-body-xs break-words text-grey-500", children: item.description })), unavailable && (_jsx("span", { id: reasonId, className: "text-body-xs break-words text-grey-500", children: unavailable }))] }), item.shortcut && (_jsx("span", { "aria-hidden": "true", className: "ml-auto shrink-0 rounded border border-surface-border px-1.5 py-0.5 text-body-xs font-medium text-grey-500", children: item.shortcut }))] }));
|
|
248
|
+
const classes = cn("flex w-full min-w-0 items-start gap-3 rounded-lg px-3 py-2 transition-colors", FOCUS_RING, unavailable
|
|
249
|
+
? "cursor-not-allowed text-grey-500"
|
|
250
|
+
: "cursor-pointer text-grey-900 hover:bg-surface-muted");
|
|
251
|
+
const shared = {
|
|
252
|
+
"data-option": "true",
|
|
253
|
+
role: "option",
|
|
254
|
+
"aria-selected": isActive,
|
|
255
|
+
tabIndex: isActive ? 0 : -1,
|
|
256
|
+
className: classes,
|
|
257
|
+
};
|
|
258
|
+
if (unavailable) {
|
|
259
|
+
return (_jsx("div", { ...shared, "aria-disabled": "true", "aria-describedby": reasonId, onKeyDown: (event) => handleOptionKeyDown(event, index), onFocus: () => setActiveIndex(index), children: body }, item.id));
|
|
260
|
+
}
|
|
261
|
+
if (item.href) {
|
|
262
|
+
// Destinations stay anchors: Enter activates them natively, middle-click
|
|
263
|
+
// opens a new tab, and the href is never rebuilt into a dead button.
|
|
264
|
+
return (_jsx("a", { ...shared, href: item.href, ...(item.external ? { target: "_blank", rel: "noreferrer" } : {}), onClick: () => {
|
|
265
|
+
item.onSelect?.();
|
|
266
|
+
onSelect?.(item);
|
|
267
|
+
close();
|
|
268
|
+
}, onKeyDown: (event) => handleOptionKeyDown(event, index), onFocus: () => setActiveIndex(index), children: body }, item.id));
|
|
269
|
+
}
|
|
270
|
+
return (_jsx("div", { ...shared, onClick: () => {
|
|
271
|
+
item.onSelect?.();
|
|
272
|
+
onSelect?.(item);
|
|
273
|
+
close();
|
|
274
|
+
}, onKeyDown: (event) => {
|
|
275
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
item.onSelect?.();
|
|
278
|
+
onSelect?.(item);
|
|
279
|
+
close();
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
handleOptionKeyDown(event, index);
|
|
283
|
+
}, onFocus: () => setActiveIndex(index), children: body }, item.id));
|
|
284
|
+
}
|
|
285
|
+
/* ── Body ────────────────────────────────────────────────────────── */
|
|
286
|
+
function renderBody() {
|
|
287
|
+
if (status === "loading") {
|
|
288
|
+
return (_jsxs("p", { role: "status", className: "flex items-center gap-2 px-4 py-10 text-body-sm text-grey-500", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: loadingMessage })] }));
|
|
289
|
+
}
|
|
290
|
+
if (status === "error") {
|
|
291
|
+
return (_jsxs("div", { role: "alert", className: "m-3 flex flex-col gap-2 rounded-lg border border-error-200 bg-error-50 px-4 py-3 text-error-700", children: [_jsx("p", { className: "text-body-sm font-semibold break-words", children: errorTitle }), _jsx("p", { className: "text-body-sm break-words", children: errorDescription }), onRetry && (_jsxs("button", { type: "button", onClick: onRetry, className: cn("inline-flex min-h-10 w-fit cursor-pointer items-center gap-2 rounded-lg border-[1.5px] border-current px-4 py-2 text-body-sm font-semibold", FOCUS_RING), children: [_jsx(Icon, { name: "refresh", size: "sm" }), "Try again"] }))] }));
|
|
292
|
+
}
|
|
293
|
+
if (!hasSource) {
|
|
294
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-4 py-10 text-center", children: [_jsx("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: emptySourceTitle }), _jsx("p", { className: "text-body-sm break-words text-grey-500", children: emptySourceDescription })] }));
|
|
295
|
+
}
|
|
296
|
+
if (promptState) {
|
|
297
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-4 py-10 text-center", children: [_jsx("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: promptTitle }), _jsx("p", { className: "text-body-sm break-words text-grey-500", children: promptDescription })] }));
|
|
298
|
+
}
|
|
299
|
+
if (noResultsState) {
|
|
300
|
+
return (_jsxs("div", { className: "flex flex-col gap-1 px-4 py-10 text-center", children: [_jsx("p", { className: "text-body-sm font-semibold break-words text-grey-900", children: noResultsTitle(trimmed) }), _jsx("p", { className: "text-body-sm break-words text-grey-500", children: noResultsDescription })] }));
|
|
301
|
+
}
|
|
302
|
+
let cursor = -1;
|
|
303
|
+
return (_jsx("div", { ref: listRef, role: "listbox", "aria-label": dialogLabel, className: "flex flex-col gap-3 p-2", children: sections.map((section) => {
|
|
304
|
+
const sectionHeadingId = `${uid}-${section.id}-label`;
|
|
305
|
+
return (_jsxs("div", { role: "group", ...(section.label ? { "aria-labelledby": sectionHeadingId } : {}), className: "flex flex-col gap-0.5", children: [section.label && (_jsx("p", { id: sectionHeadingId, className: "px-3 pt-1 pb-1 text-caption-sm font-semibold break-words text-grey-500 uppercase", children: section.label })), section.items.map((item) => {
|
|
306
|
+
cursor += 1;
|
|
307
|
+
return renderOption(item, cursor);
|
|
308
|
+
})] }, section.id));
|
|
309
|
+
}) }));
|
|
310
|
+
}
|
|
311
|
+
const countText = status === "idle" && hasSource && !promptState ? resultCountLabel(flatItems.length) : "";
|
|
312
|
+
/* ── Render ──────────────────────────────────────────────────────── */
|
|
313
|
+
return (
|
|
314
|
+
// Not an `@container`: the trigger is a flex item in the shell's top bar,
|
|
315
|
+
// and inline-size containment would stop it sizing from its own contents.
|
|
316
|
+
// The overlay is viewport-anchored, so nothing here needs a container query.
|
|
317
|
+
_jsxs("div", { className: cn("relative w-full min-w-0", className), children: [showTrigger && (_jsxs("button", { ref: triggerRef, type: "button", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": dialogId, onClick: () => setOpen(true), 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-3 py-2 text-left text-body-sm text-grey-500 transition-colors hover:bg-surface-muted", FOCUS_RING, triggerClassName), children: [_jsx(Icon, { name: "search", size: "sm", className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: triggerLabel }), shortcut?.hint && (_jsx("span", { "aria-hidden": "true", className: "ml-auto hidden shrink-0 rounded border border-surface-border px-1.5 py-0.5 text-body-xs font-medium sm:inline", children: shortcut.hint }))] })), _jsxs("div", { id: dialogId, className: cn(
|
|
318
|
+
// The overlay is anchored to the viewport, not to the container the
|
|
319
|
+
// trigger sits in, so this is the one place in the batch where a
|
|
320
|
+
// viewport breakpoint is the correct signal. A `@min-[…]` here would
|
|
321
|
+
// read the trigger's width, which has nothing to do with the space
|
|
322
|
+
// the overlay actually has.
|
|
323
|
+
"fixed inset-0 z-50 flex items-start justify-center px-3 py-12 md:px-8 md:py-[12vh]", !open && "hidden"), children: [_jsx("div", { "aria-hidden": "true", onClick: close, className: "absolute inset-0 bg-[var(--color-overlay,rgba(16,25,40,0.56))]" }), _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-label": dialogLabel, onKeyDown: handleDialogKeyDown, className: "relative flex max-h-full w-full max-w-[640px] flex-col overflow-hidden rounded-xl border border-surface-border bg-surface shadow-soft-xl", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-surface-border px-3 py-2 text-on-surface-muted", children: [_jsx(Icon, { name: "search", size: "md", className: "shrink-0 text-grey-500" }), _jsx("label", { htmlFor: inputId, className: "sr-only", children: searchLabel }), _jsx("input", { ref: inputRef, id: inputId, type: "search", value: query, placeholder: searchPlaceholder, autoComplete: "off", "aria-controls": listId, "aria-describedby": hintId, onChange: (event) => {
|
|
324
|
+
setQuery(event.target.value);
|
|
325
|
+
onQueryChange?.(event.target.value);
|
|
326
|
+
}, onKeyDown: handleInputKeyDown, className: cn(
|
|
327
|
+
// No `focus:outline-none` here: it wins over the focus-visible
|
|
328
|
+
// ring below and leaves the field with no visible indicator at
|
|
329
|
+
// all, which a tab walk measured before this comment existed.
|
|
330
|
+
"min-h-10 w-full min-w-0 rounded-lg bg-surface px-1 py-2 text-body-md text-grey-900 placeholder:text-grey-500", FOCUS_RING) }), _jsxs("button", { type: "button", onClick: close, className: cn("inline-flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-lg text-grey-500 hover:bg-surface-muted hover:text-grey-900", FOCUS_RING), children: [_jsx(Icon, { name: "multiply", size: "md" }), _jsx("span", { className: "sr-only", children: closeLabel })] })] }), _jsx("p", { id: hintId, className: "sr-only", children: searchHint }), _jsx("div", { id: listId, className: "flex min-h-0 max-h-[min(60vh,420px)] flex-col overflow-y-auto", children: renderBody() }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-surface-border px-4 py-2 text-body-xs text-grey-500", children: [_jsx("p", { role: "status", "aria-live": "polite", className: "min-w-0 break-words", children: countText }), footer] })] })] })] }));
|
|
331
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultCreateAccountPasswordRules = void 0;
|
|
4
|
+
exports.CreateAccountBlock = CreateAccountBlock;
|
|
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 Button_1 = require("../components/Button/index.cjs");
|
|
10
|
+
const FormControl_1 = require("../components/FormControl/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
|
+
exports.defaultCreateAccountPasswordRules = [
|
|
16
|
+
{ id: "length", label: "At least 8 characters", test: (v) => v.length >= 8 },
|
|
17
|
+
{
|
|
18
|
+
id: "case",
|
|
19
|
+
label: "An upper and a lower case letter",
|
|
20
|
+
test: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v),
|
|
21
|
+
},
|
|
22
|
+
{ id: "number", label: "At least one number", test: (v) => /\d/.test(v) },
|
|
23
|
+
];
|
|
24
|
+
// Deliberately permissive: the authority on a deliverable address is the
|
|
25
|
+
// consuming application, not a regular expression.
|
|
26
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
27
|
+
const emptyValues = {
|
|
28
|
+
firstName: "",
|
|
29
|
+
lastName: "",
|
|
30
|
+
email: "",
|
|
31
|
+
password: "",
|
|
32
|
+
};
|
|
33
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
34
|
+
function CreateAccountBlock({ title = "Create your account", description = "Set up a free workspace. No card required.", headingLevel = "h2", onSubmit, status = "idle", errorMessage, successMessage, fieldErrors, defaultValues, passwordRules = exports.defaultCreateAccountPasswordRules, requireConfirmPassword = true, consentLabel, consentErrorMessage = "Please accept the terms before creating an account.", signIn, submitLabel = "Create account", submittingLabel = "Creating account…", disabled = false, disabledMessage = "Account creation is currently unavailable.", labels, className, }) {
|
|
35
|
+
const uid = (0, react_1.useId)();
|
|
36
|
+
const titleId = `${uid}-title`;
|
|
37
|
+
const consentId = `${uid}-consent`;
|
|
38
|
+
const consentTextId = `${uid}-consent-text`;
|
|
39
|
+
const consentErrorId = `${uid}-consent-error`;
|
|
40
|
+
const passwordRulesId = `${uid}-password-rules`;
|
|
41
|
+
const passwordErrorId = `${uid}-password-error`;
|
|
42
|
+
const unsavedRegionId = `${uid}-form-status`;
|
|
43
|
+
const [values, setValues] = (0, react_1.useState)({
|
|
44
|
+
...emptyValues,
|
|
45
|
+
...defaultValues,
|
|
46
|
+
});
|
|
47
|
+
const [confirmPassword, setConfirmPassword] = (0, react_1.useState)("");
|
|
48
|
+
const [consent, setConsent] = (0, react_1.useState)(false);
|
|
49
|
+
const [touched, setTouched] = (0, react_1.useState)({});
|
|
50
|
+
const [submitAttempted, setSubmitAttempted] = (0, react_1.useState)(false);
|
|
51
|
+
const [revealPassword, setRevealPassword] = (0, react_1.useState)(false);
|
|
52
|
+
const fieldRefs = (0, react_1.useRef)({});
|
|
53
|
+
const pending = status === "submitting";
|
|
54
|
+
const succeeded = status === "success";
|
|
55
|
+
const loading = status === "loading";
|
|
56
|
+
const locked = disabled || pending || succeeded;
|
|
57
|
+
const ruleResults = (0, react_1.useMemo)(() => passwordRules.map((rule) => ({ ...rule, met: rule.test(values.password) })), [passwordRules, values.password]);
|
|
58
|
+
const localErrors = (0, react_1.useMemo)(() => {
|
|
59
|
+
const next = {};
|
|
60
|
+
if (!values.firstName.trim())
|
|
61
|
+
next.firstName = "Enter your first name.";
|
|
62
|
+
if (!values.lastName.trim())
|
|
63
|
+
next.lastName = "Enter your last name.";
|
|
64
|
+
if (!values.email.trim())
|
|
65
|
+
next.email = "Enter your email address.";
|
|
66
|
+
else if (!EMAIL_PATTERN.test(values.email.trim()))
|
|
67
|
+
next.email = "Enter an email address such as name@example.com.";
|
|
68
|
+
if (!values.password)
|
|
69
|
+
next.password = "Choose a password.";
|
|
70
|
+
else if (ruleResults.some((rule) => !rule.met))
|
|
71
|
+
next.password = "Your password does not meet all of the requirements below.";
|
|
72
|
+
if (requireConfirmPassword) {
|
|
73
|
+
if (!confirmPassword)
|
|
74
|
+
next.confirmPassword = "Type your password again.";
|
|
75
|
+
else if (confirmPassword !== values.password)
|
|
76
|
+
next.confirmPassword = "The two passwords do not match.";
|
|
77
|
+
}
|
|
78
|
+
if (!consent)
|
|
79
|
+
next.consent = consentErrorMessage;
|
|
80
|
+
return next;
|
|
81
|
+
}, [values, confirmPassword, consent, consentErrorMessage, requireConfirmPassword, ruleResults]);
|
|
82
|
+
const errorFor = (0, react_1.useCallback)((field) => {
|
|
83
|
+
const serverError = fieldErrors?.[field];
|
|
84
|
+
if (serverError)
|
|
85
|
+
return serverError;
|
|
86
|
+
if (!submitAttempted && !touched[field])
|
|
87
|
+
return undefined;
|
|
88
|
+
return localErrors[field];
|
|
89
|
+
}, [fieldErrors, localErrors, submitAttempted, touched]);
|
|
90
|
+
const markTouched = (0, react_1.useCallback)((field) => setTouched((prev) => ({ ...prev, [field]: true })), []);
|
|
91
|
+
const handleSubmit = (0, react_1.useCallback)((event) => {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
// Guard duplicate submission: a second attempt while one is in flight is
|
|
94
|
+
// dropped rather than queued.
|
|
95
|
+
if (locked)
|
|
96
|
+
return;
|
|
97
|
+
setSubmitAttempted(true);
|
|
98
|
+
const order = [
|
|
99
|
+
"firstName",
|
|
100
|
+
"lastName",
|
|
101
|
+
"email",
|
|
102
|
+
"password",
|
|
103
|
+
"confirmPassword",
|
|
104
|
+
"consent",
|
|
105
|
+
];
|
|
106
|
+
const firstInvalid = order.find((field) => localErrors[field]);
|
|
107
|
+
if (firstInvalid) {
|
|
108
|
+
fieldRefs.current[firstInvalid]?.focus();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
onSubmit({ ...values, consent });
|
|
112
|
+
}, [consent, localErrors, locked, onSubmit, values]);
|
|
113
|
+
const Heading = headingLevel;
|
|
114
|
+
const passwordError = errorFor("password");
|
|
115
|
+
const consentError = errorFor("consent");
|
|
116
|
+
const resolvedConsentLabel = consentLabel ?? "I agree to the terms of service and privacy policy.";
|
|
117
|
+
return (
|
|
118
|
+
// `@container` makes the block adapt to the column it is placed in, not
|
|
119
|
+
// to the viewport: a 360px settings column at 1440px behaves like a phone.
|
|
120
|
+
(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 })] }), disabled && ((0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "information", icon: "info-circle", role: "status", title: disabledMessage, 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: "We could not create your account", description: errorMessage, className: "bg-surface dark:bg-surface" })), succeeded && 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: "Preparing the sign-up form\u2026", labelPosition: "below" }) })) : ((0, jsx_runtime_1.jsxs)("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-5", children: [(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`, ref: (node) => {
|
|
121
|
+
fieldRefs.current.firstName = node;
|
|
122
|
+
}, name: "firstName", label: labels?.firstName ?? "First name", autoComplete: "given-name", required: true, size: "md", disabled: locked, value: values.firstName, error: errorFor("firstName"), onChange: (e) => setValues((p) => ({ ...p, firstName: e.target.value })), onBlur: () => markTouched("firstName") }), (0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-lastName`, ref: (node) => {
|
|
123
|
+
fieldRefs.current.lastName = node;
|
|
124
|
+
}, name: "lastName", label: labels?.lastName ?? "Last name", autoComplete: "family-name", required: true, size: "md", disabled: locked, value: values.lastName, error: errorFor("lastName"), onChange: (e) => setValues((p) => ({ ...p, lastName: e.target.value })), onBlur: () => markTouched("lastName") })] }), (0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-email`, ref: (node) => {
|
|
125
|
+
fieldRefs.current.email = node;
|
|
126
|
+
}, name: "email", type: "email", inputMode: "email", label: labels?.email ?? "Work email", placeholder: "name@example.com", autoComplete: "email", required: true, size: "md", disabled: locked, value: values.email, error: errorFor("email"), onChange: (e) => setValues((p) => ({ ...p, email: e.target.value })), onBlur: () => markTouched("email") }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-password`, ref: (node) => {
|
|
127
|
+
fieldRefs.current.password = node;
|
|
128
|
+
}, name: "password", type: revealPassword ? "text" : "password", label: labels?.password ?? "Password", autoComplete: "new-password", required: true, size: "md", disabled: locked, value: values.password,
|
|
129
|
+
// Boolean error: the message and the guidance list are rendered
|
|
130
|
+
// here so a single `aria-describedby` can reference both.
|
|
131
|
+
error: Boolean(passwordError), "aria-describedby": [passwordError ? passwordErrorId : null, passwordRulesId]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(" "), onChange: (e) => setValues((p) => ({ ...p, password: e.target.value })), onBlur: () => markTouched("password"), trailingAction: (0, jsx_runtime_1.jsxs)("button", { type: "button",
|
|
134
|
+
// Stays available while a submission is in flight: it only
|
|
135
|
+
// changes how an already-entered value is displayed.
|
|
136
|
+
onClick: () => setRevealPassword((v) => !v), className: "inline-flex cursor-pointer items-center rounded p-1 text-grey-500 hover:text-grey-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: revealPassword ? "eye-slash" : "eye", size: "md" }), (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: revealPassword ? "Hide password" : "Show password" })] }) }), passwordError && ((0, jsx_runtime_1.jsx)("p", { id: passwordErrorId, className: "text-body-sm text-feedback-error", children: passwordError })), (0, jsx_runtime_1.jsx)("ul", { id: passwordRulesId, role: "list", className: "flex flex-col gap-1", children: ruleResults.map((rule) => ((0, jsx_runtime_1.jsxs)("li", { className: (0, cn_1.cn)("flex items-center gap-2 text-body-sm", rule.met ? "text-feedback-success" : "text-grey-600"), children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: rule.met ? "check-circle" : "minus-circle", size: "sm" }), (0, jsx_runtime_1.jsx)("span", { children: rule.label }), (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: rule.met ? "— met" : "— not met yet" })] }, rule.id))) })] }), requireConfirmPassword && ((0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-confirmPassword`, ref: (node) => {
|
|
137
|
+
fieldRefs.current.confirmPassword = node;
|
|
138
|
+
}, name: "confirmPassword", type: revealPassword ? "text" : "password", label: labels?.confirmPassword ?? "Confirm password", autoComplete: "new-password", required: true, size: "md", disabled: locked, value: confirmPassword, error: errorFor("confirmPassword"), onChange: (e) => setConfirmPassword(e.target.value), onBlur: () => markTouched("confirmPassword") })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-start gap-3", children: [(0, jsx_runtime_1.jsx)("span", { className: "pt-0.5", children: (0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { id: consentId, ref: (node) => {
|
|
139
|
+
fieldRefs.current.consent = node;
|
|
140
|
+
}, name: "consent", checked: consent, disabled: locked, required: true, "aria-labelledby": consentTextId, "aria-invalid": consentError ? true : undefined, "aria-describedby": consentError ? consentErrorId : undefined, onChange: (e) => {
|
|
141
|
+
setConsent(e.target.checked);
|
|
142
|
+
markTouched("consent");
|
|
143
|
+
} }) }), (0, jsx_runtime_1.jsx)("label", { id: consentTextId, htmlFor: consentId, className: (0, cn_1.cn)("text-body-sm text-grey-700", locked ? "opacity-60" : "cursor-pointer"), children: resolvedConsentLabel })] }), consentError && ((0, jsx_runtime_1.jsx)("p", { id: consentErrorId, className: "text-body-sm text-feedback-error", children: consentError }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { type: "submit", size: "lg", variant: "primary", className: "w-full", disabled: locked, children: pending ? submittingLabel : submitLabel }), (0, jsx_runtime_1.jsx)("output", { id: unsavedRegionId, className: "sr-only", children: pending ? submittingLabel : "" }), signIn && ((0, jsx_runtime_1.jsxs)("p", { className: "text-center text-body-sm text-grey-600", children: ["Already have an account?", " ", (0, jsx_runtime_1.jsx)("a", { href: signIn.href, "aria-disabled": pending || undefined, tabIndex: pending ? -1 : undefined, onClick: (event) => {
|
|
144
|
+
if (pending)
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
}, className: (0, cn_1.cn)("rounded font-semibold text-action-primary-text underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", pending && "pointer-events-none opacity-60"), children: signIn.label })] }))] })] }))] }) }));
|
|
147
|
+
}
|