@raydenui/ui 0.9.6 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/dist/Table-DCNSxua1.d.cts +31 -0
  2. package/dist/Table-DCNSxua1.d.ts +31 -0
  3. package/dist/blocks/ApplicationShellBlock.cjs +157 -0
  4. package/dist/blocks/ApplicationShellBlock.js +154 -0
  5. package/dist/blocks/CheckoutReviewBlock.cjs +16 -0
  6. package/dist/blocks/CheckoutReviewBlock.js +13 -0
  7. package/dist/blocks/CommandPaletteBlock.cjs +334 -0
  8. package/dist/blocks/CommandPaletteBlock.js +331 -0
  9. package/dist/blocks/CreateAccountBlock.cjs +147 -0
  10. package/dist/blocks/CreateAccountBlock.js +143 -0
  11. package/dist/blocks/EmptyStateBlock.cjs +19 -0
  12. package/dist/blocks/EmptyStateBlock.js +16 -0
  13. package/dist/blocks/FeatureOverviewBlock.cjs +54 -0
  14. package/dist/blocks/FeatureOverviewBlock.js +51 -0
  15. package/dist/blocks/HeaderBlock.cjs +230 -0
  16. package/dist/blocks/HeaderBlock.js +227 -0
  17. package/dist/blocks/KpiOverviewBlock.cjs +63 -0
  18. package/dist/blocks/KpiOverviewBlock.js +60 -0
  19. package/dist/blocks/LoginBlock.cjs +66 -0
  20. package/dist/blocks/LoginBlock.js +63 -0
  21. package/dist/blocks/NotificationsBlock.cjs +17 -0
  22. package/dist/blocks/NotificationsBlock.js +14 -0
  23. package/dist/blocks/PageHeaderBlock.cjs +121 -0
  24. package/dist/blocks/PageHeaderBlock.js +118 -0
  25. package/dist/blocks/PricingPlansBlock.cjs +146 -0
  26. package/dist/blocks/PricingPlansBlock.js +143 -0
  27. package/dist/blocks/ProductCollectionBlock.cjs +37 -0
  28. package/dist/blocks/ProductCollectionBlock.js +34 -0
  29. package/dist/blocks/ProductDetailBlock.cjs +30 -0
  30. package/dist/blocks/ProductDetailBlock.js +27 -0
  31. package/dist/blocks/ProductHeroBlock.cjs +83 -0
  32. package/dist/blocks/ProductHeroBlock.js +80 -0
  33. package/dist/blocks/ProfileSettingsBlock.cjs +130 -0
  34. package/dist/blocks/ProfileSettingsBlock.js +126 -0
  35. package/dist/blocks/QuickSendBlock.cjs +11 -0
  36. package/dist/blocks/QuickSendBlock.js +8 -0
  37. package/dist/blocks/RecentTransactionsBlock.cjs +13 -0
  38. package/dist/blocks/RecentTransactionsBlock.js +10 -0
  39. package/dist/blocks/SearchableTableBlock.cjs +91 -0
  40. package/dist/blocks/SearchableTableBlock.js +88 -0
  41. package/dist/blocks/ShoppingCartBlock.cjs +16 -0
  42. package/dist/blocks/ShoppingCartBlock.js +13 -0
  43. package/dist/blocks/SiteFooterBlock.cjs +69 -0
  44. package/dist/blocks/SiteFooterBlock.js +66 -0
  45. package/dist/blocks/TableBlock.cjs +20 -0
  46. package/dist/blocks/TableBlock.js +17 -0
  47. package/dist/blocks/TaskListBlock.cjs +198 -0
  48. package/dist/blocks/TaskListBlock.js +195 -0
  49. package/dist/blocks/WorkspaceSwitcherBlock.cjs +291 -0
  50. package/dist/blocks/WorkspaceSwitcherBlock.js +288 -0
  51. package/dist/blocks/commerce.cjs +44 -0
  52. package/dist/blocks/commerce.js +35 -0
  53. package/dist/blocks/index.cjs +53 -0
  54. package/dist/blocks/index.js +24 -0
  55. package/dist/blocks.d.cts +1820 -0
  56. package/dist/blocks.d.ts +1820 -0
  57. package/dist/chart.cjs +13 -0
  58. package/dist/chart.d.cts +59 -0
  59. package/dist/chart.d.ts +59 -0
  60. package/dist/chart.js +5 -0
  61. package/dist/components/Accordion/Accordion.cjs +94 -0
  62. package/dist/components/Accordion/Accordion.js +91 -0
  63. package/dist/components/Accordion/index.cjs +8 -0
  64. package/dist/components/Accordion/index.js +1 -0
  65. package/dist/components/ActivityFeed/ActivityContent.cjs +40 -0
  66. package/dist/components/ActivityFeed/ActivityContent.js +37 -0
  67. package/dist/components/ActivityFeed/ActivityItem.cjs +19 -0
  68. package/dist/components/ActivityFeed/ActivityItem.js +16 -0
  69. package/dist/components/ActivityFeed/index.cjs +7 -0
  70. package/dist/components/ActivityFeed/index.js +2 -0
  71. package/dist/components/Alert/Alert.cjs +42 -0
  72. package/dist/components/Alert/Alert.js +39 -0
  73. package/dist/components/Alert/index.cjs +5 -0
  74. package/dist/components/Alert/index.js +1 -0
  75. package/dist/components/Avatar/Avatar.cjs +101 -0
  76. package/dist/components/Avatar/Avatar.js +98 -0
  77. package/dist/components/Avatar/index.cjs +6 -0
  78. package/dist/components/Avatar/index.js +1 -0
  79. package/dist/components/Badge/Badge.cjs +53 -0
  80. package/dist/components/Badge/Badge.js +50 -0
  81. package/dist/components/Badge/index.cjs +5 -0
  82. package/dist/components/Badge/index.js +1 -0
  83. package/dist/components/Banner/Banner.cjs +83 -0
  84. package/dist/components/Banner/Banner.js +80 -0
  85. package/dist/components/Banner/index.cjs +5 -0
  86. package/dist/components/Banner/index.js +1 -0
  87. package/dist/components/Breadcrumb/Breadcrumb.cjs +31 -0
  88. package/dist/components/Breadcrumb/Breadcrumb.js +28 -0
  89. package/dist/components/Breadcrumb/index.cjs +5 -0
  90. package/dist/components/Breadcrumb/index.js +1 -0
  91. package/dist/components/Button/Button.cjs +88 -0
  92. package/dist/components/Button/Button.js +85 -0
  93. package/dist/components/Button/index.cjs +5 -0
  94. package/dist/components/Button/index.js +1 -0
  95. package/dist/components/ButtonGroup/ButtonGroup.cjs +8 -0
  96. package/dist/components/ButtonGroup/ButtonGroup.js +5 -0
  97. package/dist/components/ButtonGroup/ButtonGroupItem.cjs +15 -0
  98. package/dist/components/ButtonGroup/ButtonGroupItem.js +12 -0
  99. package/dist/components/ButtonGroup/index.cjs +7 -0
  100. package/dist/components/ButtonGroup/index.js +2 -0
  101. package/dist/components/Card/Card.cjs +94 -0
  102. package/dist/components/Card/Card.js +91 -0
  103. package/dist/components/Card/index.cjs +9 -0
  104. package/dist/components/Card/index.js +1 -0
  105. package/dist/components/Chart/Chart.cjs +160 -0
  106. package/dist/components/Chart/Chart.js +153 -0
  107. package/dist/components/Chart/index.cjs +10 -0
  108. package/dist/components/Chart/index.js +2 -0
  109. package/dist/components/Chart/theme.cjs +67 -0
  110. package/dist/components/Chart/theme.js +62 -0
  111. package/dist/components/Chip/Chip.cjs +18 -0
  112. package/dist/components/Chip/Chip.js +15 -0
  113. package/dist/components/Chip/index.cjs +5 -0
  114. package/dist/components/Chip/index.js +1 -0
  115. package/dist/components/Counter/Counter.cjs +74 -0
  116. package/dist/components/Counter/Counter.js +71 -0
  117. package/dist/components/Counter/index.cjs +6 -0
  118. package/dist/components/Counter/index.js +1 -0
  119. package/dist/components/DatePicker/DatePicker.cjs +343 -0
  120. package/dist/components/DatePicker/DatePicker.js +340 -0
  121. package/dist/components/DatePicker/index.cjs +5 -0
  122. package/dist/components/DatePicker/index.js +1 -0
  123. package/dist/components/Divider/Divider.cjs +31 -0
  124. package/dist/components/Divider/Divider.js +28 -0
  125. package/dist/components/Divider/index.cjs +5 -0
  126. package/dist/components/Divider/index.js +1 -0
  127. package/dist/components/DropdownMenu/DropdownMenu.cjs +108 -0
  128. package/dist/components/DropdownMenu/DropdownMenu.js +105 -0
  129. package/dist/components/DropdownMenu/index.cjs +11 -0
  130. package/dist/components/DropdownMenu/index.js +1 -0
  131. package/dist/components/EmptyStateIllustration/EmptyStateIllustration.cjs +42 -0
  132. package/dist/components/EmptyStateIllustration/EmptyStateIllustration.js +39 -0
  133. package/dist/components/EmptyStateIllustration/illustrations.cjs +3322 -0
  134. package/dist/components/EmptyStateIllustration/illustrations.js +3319 -0
  135. package/dist/components/EmptyStateIllustration/index.cjs +7 -0
  136. package/dist/components/EmptyStateIllustration/index.js +2 -0
  137. package/dist/components/FileUpload/FileUpload.cjs +116 -0
  138. package/dist/components/FileUpload/FileUpload.js +113 -0
  139. package/dist/components/FileUpload/FileUploadDropZone.cjs +58 -0
  140. package/dist/components/FileUpload/FileUploadDropZone.js +55 -0
  141. package/dist/components/FileUpload/FileUploadIcons.cjs +55 -0
  142. package/dist/components/FileUpload/FileUploadIcons.js +50 -0
  143. package/dist/components/FileUpload/FileUploadItem.cjs +34 -0
  144. package/dist/components/FileUpload/FileUploadItem.js +31 -0
  145. package/dist/components/FileUpload/index.cjs +9 -0
  146. package/dist/components/FileUpload/index.js +3 -0
  147. package/dist/components/FormControl/Checkbox.cjs +27 -0
  148. package/dist/components/FormControl/Checkbox.js +24 -0
  149. package/dist/components/FormControl/Radio.cjs +15 -0
  150. package/dist/components/FormControl/Radio.js +12 -0
  151. package/dist/components/FormControl/Toggle.cjs +15 -0
  152. package/dist/components/FormControl/Toggle.js +12 -0
  153. package/dist/components/FormControl/index.cjs +9 -0
  154. package/dist/components/FormControl/index.js +3 -0
  155. package/dist/components/Icon/Icon.cjs +70 -0
  156. package/dist/components/Icon/Icon.js +34 -0
  157. package/dist/components/Icon/catalog.cjs +1672 -0
  158. package/dist/components/Icon/catalog.js +1669 -0
  159. package/dist/components/Icon/icons.cjs +5621 -0
  160. package/dist/components/Icon/icons.js +5610 -0
  161. package/dist/components/Icon/index.cjs +8 -0
  162. package/dist/components/Icon/index.js +2 -0
  163. package/dist/components/Input/Input.cjs +64 -0
  164. package/dist/components/Input/Input.js +61 -0
  165. package/dist/components/Input/index.cjs +5 -0
  166. package/dist/components/Input/index.js +1 -0
  167. package/dist/components/MetricsCard/MetricsCard.cjs +107 -0
  168. package/dist/components/MetricsCard/MetricsCard.js +104 -0
  169. package/dist/components/MetricsCard/index.cjs +5 -0
  170. package/dist/components/MetricsCard/index.js +1 -0
  171. package/dist/components/Modal/Modal.cjs +126 -0
  172. package/dist/components/Modal/Modal.js +123 -0
  173. package/dist/components/Modal/index.cjs +5 -0
  174. package/dist/components/Modal/index.js +1 -0
  175. package/dist/components/Pagination/Pagination.cjs +42 -0
  176. package/dist/components/Pagination/Pagination.js +39 -0
  177. package/dist/components/Pagination/index.cjs +5 -0
  178. package/dist/components/Pagination/index.js +1 -0
  179. package/dist/components/ProgressBar/ProgressBar.cjs +34 -0
  180. package/dist/components/ProgressBar/ProgressBar.js +31 -0
  181. package/dist/components/ProgressBar/index.cjs +5 -0
  182. package/dist/components/ProgressBar/index.js +1 -0
  183. package/dist/components/ProgressCircle/ProgressCircle.cjs +34 -0
  184. package/dist/components/ProgressCircle/ProgressCircle.js +31 -0
  185. package/dist/components/ProgressCircle/index.cjs +5 -0
  186. package/dist/components/ProgressCircle/index.js +1 -0
  187. package/dist/components/Select/Select.cjs +84 -0
  188. package/dist/components/Select/Select.js +80 -0
  189. package/dist/components/Select/SelectOption.cjs +26 -0
  190. package/dist/components/Select/SelectOption.js +23 -0
  191. package/dist/components/Select/index.cjs +7 -0
  192. package/dist/components/Select/index.js +2 -0
  193. package/dist/components/SidebarMenu/SidebarMenu.cjs +85 -0
  194. package/dist/components/SidebarMenu/SidebarMenu.js +81 -0
  195. package/dist/components/SidebarMenu/SidebarMenuItem.cjs +126 -0
  196. package/dist/components/SidebarMenu/SidebarMenuItem.js +122 -0
  197. package/dist/components/SidebarMenu/SidebarMenuSection.cjs +14 -0
  198. package/dist/components/SidebarMenu/SidebarMenuSection.js +11 -0
  199. package/dist/components/SidebarMenu/index.cjs +12 -0
  200. package/dist/components/SidebarMenu/index.js +4 -0
  201. package/dist/components/Slider/Slider.cjs +161 -0
  202. package/dist/components/Slider/Slider.js +158 -0
  203. package/dist/components/Slider/index.cjs +6 -0
  204. package/dist/components/Slider/index.js +1 -0
  205. package/dist/components/Spinner/Spinner.cjs +93 -0
  206. package/dist/components/Spinner/Spinner.js +90 -0
  207. package/dist/components/Spinner/index.cjs +5 -0
  208. package/dist/components/Spinner/index.js +1 -0
  209. package/dist/components/Stepper/Stepper.cjs +82 -0
  210. package/dist/components/Stepper/Stepper.js +79 -0
  211. package/dist/components/Stepper/index.cjs +7 -0
  212. package/dist/components/Stepper/index.js +1 -0
  213. package/dist/components/Table/Table.cjs +25 -0
  214. package/dist/components/Table/Table.js +22 -0
  215. package/dist/components/Table/index.cjs +10 -0
  216. package/dist/components/Table/index.js +1 -0
  217. package/dist/components/Table/useTableSelection.cjs +60 -0
  218. package/dist/components/Table/useTableSelection.js +57 -0
  219. package/dist/components/Tabs/Tab.cjs +70 -0
  220. package/dist/components/Tabs/Tab.js +67 -0
  221. package/dist/components/Tabs/Tabs.cjs +133 -0
  222. package/dist/components/Tabs/Tabs.js +129 -0
  223. package/dist/components/Tabs/index.cjs +7 -0
  224. package/dist/components/Tabs/index.js +2 -0
  225. package/dist/components/Tooltip/Tooltip.cjs +101 -0
  226. package/dist/components/Tooltip/Tooltip.js +98 -0
  227. package/dist/components/Tooltip/index.cjs +5 -0
  228. package/dist/components/Tooltip/index.js +1 -0
  229. package/dist/context/ThemeContext.cjs +97 -0
  230. package/dist/context/ThemeContext.js +92 -0
  231. package/dist/fonts/README.md +13 -0
  232. package/dist/fonts/hanken-grotesk-cyrillic-ext-italic.woff2 +0 -0
  233. package/dist/fonts/hanken-grotesk-cyrillic-ext-normal.woff2 +0 -0
  234. package/dist/fonts/hanken-grotesk-latin-ext-italic.woff2 +0 -0
  235. package/dist/fonts/hanken-grotesk-latin-ext-normal.woff2 +0 -0
  236. package/dist/fonts/hanken-grotesk-latin-italic.woff2 +0 -0
  237. package/dist/fonts/hanken-grotesk-latin-normal.woff2 +0 -0
  238. package/dist/fonts/hanken-grotesk-vietnamese-italic.woff2 +0 -0
  239. package/dist/fonts/hanken-grotesk-vietnamese-normal.woff2 +0 -0
  240. package/dist/fonts/hankengrotesk-LICENSE.txt +94 -0
  241. package/dist/fonts/manrope-LICENSE.txt +93 -0
  242. package/dist/fonts/manrope-cyrillic-ext-normal.woff2 +0 -0
  243. package/dist/fonts/manrope-cyrillic-normal.woff2 +0 -0
  244. package/dist/fonts/manrope-greek-normal.woff2 +0 -0
  245. package/dist/fonts/manrope-latin-ext-normal.woff2 +0 -0
  246. package/dist/fonts/manrope-latin-normal.woff2 +0 -0
  247. package/dist/fonts/manrope-vietnamese-normal.woff2 +0 -0
  248. package/dist/fonts.css +148 -0
  249. package/dist/hooks/form/index.cjs +11 -0
  250. package/dist/hooks/form/index.js +3 -0
  251. package/dist/hooks/form/useRaydenFormControl.cjs +81 -0
  252. package/dist/hooks/form/useRaydenFormControl.js +76 -0
  253. package/dist/hooks/form/useRaydenInput.cjs +38 -0
  254. package/dist/hooks/form/useRaydenInput.js +35 -0
  255. package/dist/hooks/form/useRaydenSelect.cjs +45 -0
  256. package/dist/hooks/form/useRaydenSelect.js +42 -0
  257. package/dist/hooks/index.cjs +17 -0
  258. package/dist/hooks/index.js +1 -0
  259. package/dist/hooks/useBodyScrollLock.cjs +24 -0
  260. package/dist/hooks/useBodyScrollLock.js +21 -0
  261. package/dist/hooks/useCollisionAwareSide.cjs +72 -0
  262. package/dist/hooks/useCollisionAwareSide.js +69 -0
  263. package/dist/hooks/useControllableValue.cjs +16 -0
  264. package/dist/hooks/useControllableValue.js +13 -0
  265. package/dist/hooks/useDismissableLayer.cjs +100 -0
  266. package/dist/hooks/useDismissableLayer.js +97 -0
  267. package/dist/hooks/usePopupList.cjs +101 -0
  268. package/dist/hooks/usePopupList.js +98 -0
  269. package/dist/icons-8yD7I5jO.d.cts +430 -0
  270. package/dist/icons-8yD7I5jO.d.ts +430 -0
  271. package/dist/icons.cjs +2111 -418
  272. package/dist/icons.d.cts +1256 -429
  273. package/dist/icons.d.ts +1256 -429
  274. package/dist/icons.js +2089 -1
  275. package/dist/index.cjs +128 -15034
  276. package/dist/index.d.cts +698 -661
  277. package/dist/index.d.ts +698 -661
  278. package/dist/index.js +43 -14950
  279. package/dist/motion/Collapse.cjs +28 -0
  280. package/dist/motion/Collapse.js +25 -0
  281. package/dist/motion/MotionProvider.cjs +43 -0
  282. package/dist/motion/MotionProvider.js +38 -0
  283. package/dist/motion/Pressable.cjs +46 -0
  284. package/dist/motion/Pressable.js +43 -0
  285. package/dist/motion/Reveal.cjs +19 -0
  286. package/dist/motion/Reveal.js +16 -0
  287. package/dist/motion/SharedLayout.cjs +46 -0
  288. package/dist/motion/SharedLayout.js +43 -0
  289. package/dist/motion/index.cjs +19 -0
  290. package/dist/motion/index.js +6 -0
  291. package/dist/motion/presence.cjs +58 -0
  292. package/dist/motion/presence.js +55 -0
  293. package/dist/motion/presets.cjs +88 -0
  294. package/dist/motion/presets.js +84 -0
  295. package/dist/motion.d.cts +51 -0
  296. package/dist/motion.d.ts +51 -0
  297. package/dist/preset.cjs +356 -195
  298. package/dist/preset.d.cts +111 -32
  299. package/dist/preset.d.ts +111 -32
  300. package/dist/preset.js +353 -187
  301. package/dist/presets-Ba5QndHg.d.cts +144 -0
  302. package/dist/presets-Ba5QndHg.d.ts +144 -0
  303. package/dist/styles.css +1 -1
  304. package/dist/utils/cn.cjs +38 -0
  305. package/dist/utils/cn.js +35 -0
  306. package/dist/utils/resolveIcon.cjs +18 -0
  307. package/dist/utils/resolveIcon.js +15 -0
  308. package/package.json +79 -21
@@ -0,0 +1,126 @@
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 { Alert } from "../components/Alert/index.js";
5
+ import { Avatar } from "../components/Avatar/index.js";
6
+ import { Button } from "../components/Button/index.js";
7
+ import { Icon } from "../components/Icon/index.js";
8
+ import { Input } from "../components/Input/index.js";
9
+ import { Spinner } from "../components/Spinner/index.js";
10
+ /* ─── Defaults ───────────────────────────────────────────────────────── */
11
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+ const defaultLabels = {
13
+ firstName: "First name",
14
+ lastName: "Last name",
15
+ email: "Email address",
16
+ jobTitle: "Job title",
17
+ timeZone: "Time zone",
18
+ bio: "About you",
19
+ };
20
+ export const defaultProfileTimeZones = [
21
+ { value: "Europe/London", label: "London (GMT+01:00)" },
22
+ { value: "Europe/Berlin", label: "Berlin (GMT+02:00)" },
23
+ { value: "America/New_York", label: "New York (GMT−04:00)" },
24
+ { value: "America/Los_Angeles", label: "Los Angeles (GMT−07:00)" },
25
+ { value: "Africa/Lagos", label: "Lagos (GMT+01:00)" },
26
+ { value: "Asia/Singapore", label: "Singapore (GMT+08:00)" },
27
+ ];
28
+ /* ─── Component ──────────────────────────────────────────────────────── */
29
+ export function ProfileSettingsBlock({ title = "Profile", description = "These details appear next to your name across the workspace.", headingLevel = "h2", value, onSave, onCancel, onDirtyChange, status = "idle", errorMessage, successMessage, fieldErrors, canEdit = true, readOnlyMessage = "You can view this profile but not change it. Ask a workspace administrator for edit access.", readOnlyFields, readOnlyFieldMessages, avatar, onAvatarChange, onAvatarRemove, avatarStatus = "idle", avatarErrorMessage, avatarSlot, avatarAccept = "image/png,image/jpeg,image/webp", timeZones = defaultProfileTimeZones, bioMaxLength = 280, labels, saveLabel = "Save changes", cancelLabel = "Cancel", className, }) {
30
+ const uid = useId();
31
+ const titleId = `${uid}-title`;
32
+ const bioId = `${uid}-bio`;
33
+ const bioCountId = `${uid}-bio-count`;
34
+ const serializedSaved = JSON.stringify(value);
35
+ const [draft, setDraft] = useState(value);
36
+ const [touched, setTouched] = useState({});
37
+ const [saveAttempted, setSaveAttempted] = useState(false);
38
+ const lastSavedRef = useRef(serializedSaved);
39
+ const fileInputRef = useRef(null);
40
+ // Saved values changed under us (a save landed, or the app swapped profile).
41
+ // Re-sync the draft during render rather than in an effect, so the form never
42
+ // paints one frame of stale content.
43
+ if (lastSavedRef.current !== serializedSaved) {
44
+ lastSavedRef.current = serializedSaved;
45
+ setDraft(value);
46
+ setTouched({});
47
+ setSaveAttempted(false);
48
+ }
49
+ const saving = status === "saving";
50
+ const loading = status === "loading";
51
+ const locked = !canEdit || saving || loading;
52
+ const dirty = useMemo(() => JSON.stringify(draft) !== serializedSaved, [draft, serializedSaved]);
53
+ const dirtyRef = useRef(dirty);
54
+ useEffect(() => {
55
+ if (dirtyRef.current !== dirty) {
56
+ dirtyRef.current = dirty;
57
+ onDirtyChange?.(dirty);
58
+ }
59
+ }, [dirty, onDirtyChange]);
60
+ const localErrors = useMemo(() => {
61
+ const next = {};
62
+ if (!draft.firstName.trim())
63
+ next.firstName = "Enter a first name.";
64
+ if (!draft.lastName.trim())
65
+ next.lastName = "Enter a last name.";
66
+ if (!draft.email.trim())
67
+ next.email = "Enter an email address.";
68
+ else if (!EMAIL_PATTERN.test(draft.email.trim()))
69
+ next.email = "Enter an email address such as name@example.com.";
70
+ if (draft.bio.length > bioMaxLength)
71
+ next.bio = `Shorten this to ${bioMaxLength} characters or fewer.`;
72
+ return next;
73
+ }, [draft, bioMaxLength]);
74
+ const errorFor = useCallback((field) => {
75
+ const serverError = fieldErrors?.[field];
76
+ if (serverError)
77
+ return serverError;
78
+ if (!saveAttempted && !touched[field])
79
+ return undefined;
80
+ return localErrors[field];
81
+ }, [fieldErrors, localErrors, saveAttempted, touched]);
82
+ const isFieldReadOnly = useCallback((field) => !canEdit || Boolean(readOnlyFields?.includes(field)), [canEdit, readOnlyFields]);
83
+ const setField = useCallback((field, next) => setDraft((prev) => ({ ...prev, [field]: next })), []);
84
+ const markTouched = useCallback((field) => setTouched((prev) => ({ ...prev, [field]: true })), []);
85
+ const handleSubmit = useCallback((event) => {
86
+ event.preventDefault();
87
+ // Guard duplicate submission and honour the permission contract.
88
+ if (locked || !dirty)
89
+ return;
90
+ setSaveAttempted(true);
91
+ if (Object.keys(localErrors).length > 0)
92
+ return;
93
+ onSave(draft);
94
+ }, [dirty, draft, localErrors, locked, onSave]);
95
+ const handleCancel = useCallback(() => {
96
+ if (saving)
97
+ return;
98
+ setDraft(value);
99
+ setTouched({});
100
+ setSaveAttempted(false);
101
+ onCancel?.();
102
+ }, [onCancel, saving, value]);
103
+ const handleAvatarInput = useCallback((event) => {
104
+ const file = event.target.files?.[0];
105
+ if (file)
106
+ onAvatarChange?.(file);
107
+ // Allow re-selecting the same file after a failed upload.
108
+ event.target.value = "";
109
+ }, [onAvatarChange]);
110
+ const Heading = headingLevel;
111
+ const label = (field) => labels?.[field] ?? defaultLabels[field];
112
+ const readOnlyNote = (field) => isFieldReadOnly(field) && canEdit ? readOnlyFieldMessages?.[field] : undefined;
113
+ const bioError = errorFor("bio");
114
+ const bioReadOnly = isFieldReadOnly("bio");
115
+ return (_jsx("section", { "aria-labelledby": titleId, className: cn("@container w-full", className), children: _jsxs("div", { className: "flex flex-col gap-6 rounded-12 border border-surface-border bg-surface p-4 @min-[420px]:p-6", children: [_jsxs("header", { className: "flex flex-col gap-2", children: [_jsx(Heading, { id: titleId, className: "text-h6 font-semibold text-grey-900", children: title }), description && _jsx("p", { className: "text-body-sm text-grey-600", children: description })] }), !canEdit && (_jsx(Alert, { state: "information", icon: "lock", role: "status", title: "View only", description: readOnlyMessage, className: "bg-surface dark:bg-surface" })), status === "error" && errorMessage && (_jsx(Alert, { state: "error", icon: "info-triangle", role: "alert", title: "Your changes were not saved", description: errorMessage, className: "bg-surface dark:bg-surface" })), status === "success" && successMessage && (_jsx(Alert, { state: "success", icon: "check-circle", role: "status", title: successMessage, className: "bg-surface dark:bg-surface" })), loading ? (_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: _jsx(Spinner, { size: "md", label: "Loading your profile\u2026", labelPosition: "below" }) })) : (_jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-3 border-b border-surface-border pb-6 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:gap-5", children: [_jsx(Avatar, { type: avatar?.src ? "image" : "initials", size: "2xl", src: avatar?.src, alt: avatar?.alt ?? `${draft.firstName} ${draft.lastName}`.trim(), initials: avatar?.initials ??
116
+ (`${draft.firstName.charAt(0)}${draft.lastName.charAt(0)}`.toUpperCase() || "?") }), _jsx("div", { className: "flex min-w-0 flex-col gap-2", children: avatarSlot ?? (_jsxs(_Fragment, { children: [onAvatarChange ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("input", { ref: fileInputRef, id: `${uid}-avatar`, type: "file", accept: avatarAccept, disabled: locked || avatarStatus === "uploading", onChange: handleAvatarInput, className: "sr-only" }), _jsxs("label", { htmlFor: `${uid}-avatar`, className: cn("inline-flex h-9 cursor-pointer items-center gap-2 rounded-lg border-[1.5px] border-current px-4 text-sm font-semibold text-action-primary-text", "focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-action-primary-text", (locked || avatarStatus === "uploading") &&
117
+ "cursor-not-allowed opacity-50"), children: [_jsx(Icon, { name: "image-add", size: "md" }), "Change photo"] }), onAvatarRemove && avatar?.src && (_jsx(Button, { type: "button", variant: "grey", appearance: "outlined", size: "sm", disabled: locked || avatarStatus === "uploading", onClick: onAvatarRemove, children: "Remove photo" }))] })) : (_jsx("p", { className: "text-body-sm text-grey-600", children: "Your photo is managed outside this workspace." })), _jsx("p", { className: "text-body-xs text-grey-600", children: "PNG, JPG or WebP. A square image works best." }), avatarStatus === "uploading" && (_jsxs("output", { className: "flex items-center gap-2 text-body-sm text-grey-600", children: [_jsx(Spinner, { size: "xs" }), "Uploading your photo\u2026"] })), avatarStatus === "error" && avatarErrorMessage && (_jsx("p", { role: "alert", className: "text-body-sm text-feedback-error", children: avatarErrorMessage }))] })) })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 @min-[520px]:grid-cols-2", children: [_jsx(Input, { id: `${uid}-firstName`, name: "firstName", size: "md", label: label("firstName"), autoComplete: "given-name", required: true, value: draft.firstName, readOnly: isFieldReadOnly("firstName"), disabled: saving, error: errorFor("firstName"), helperText: readOnlyNote("firstName"), onChange: (e) => setField("firstName", e.target.value), onBlur: () => markTouched("firstName") }), _jsx(Input, { id: `${uid}-lastName`, name: "lastName", size: "md", label: label("lastName"), autoComplete: "family-name", required: true, value: draft.lastName, readOnly: isFieldReadOnly("lastName"), disabled: saving, error: errorFor("lastName"), helperText: readOnlyNote("lastName"), onChange: (e) => setField("lastName", e.target.value), onBlur: () => markTouched("lastName") })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 @min-[520px]:grid-cols-2", children: [_jsx(Input, { id: `${uid}-email`, name: "email", type: "email", inputMode: "email", size: "md", label: label("email"), autoComplete: "email", required: true, value: draft.email, readOnly: isFieldReadOnly("email"), disabled: saving, error: errorFor("email"), helperText: readOnlyNote("email"), onChange: (e) => setField("email", e.target.value), onBlur: () => markTouched("email") }), _jsx(Input, { id: `${uid}-jobTitle`, name: "jobTitle", size: "md", label: label("jobTitle"), autoComplete: "organization-title", value: draft.jobTitle, readOnly: isFieldReadOnly("jobTitle"), disabled: saving, error: errorFor("jobTitle"), helperText: readOnlyNote("jobTitle"), onChange: (e) => setField("jobTitle", e.target.value), onBlur: () => markTouched("jobTitle") })] }), isFieldReadOnly("timeZone") ? (_jsx(Input, { id: `${uid}-timeZone`, name: "timeZone", size: "md", label: label("timeZone"), readOnly: true, value: timeZones.find((z) => z.value === draft.timeZone)?.label ?? draft.timeZone, helperText: readOnlyNote("timeZone"), onChange: () => undefined })) : (_jsxs("div", { className: "flex w-full flex-col gap-1", children: [_jsx("label", { htmlFor: `${uid}-timeZone`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: label("timeZone") }), _jsx("select", { id: `${uid}-timeZone`, name: "timeZone", disabled: saving, value: draft.timeZone, onChange: (e) => setField("timeZone", e.target.value), className: cn("h-12 w-full rounded-lg border border-grey-300 bg-surface px-4 text-sm text-grey-900", "transition-colors hover:border-primary-100 focus-visible:border-primary-400", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", "disabled:cursor-not-allowed disabled:bg-grey-50"), children: timeZones.map((zone) => (_jsx("option", { value: zone.value, children: zone.label }, zone.value))) })] })), _jsxs("div", { className: "flex w-full flex-col gap-1", children: [_jsx("label", { htmlFor: bioId, className: "text-sm font-medium leading-[1.45] text-grey-900", children: label("bio") }), _jsx("textarea", { id: bioId, name: "bio", rows: 4, readOnly: bioReadOnly, disabled: saving, value: draft.bio, "aria-invalid": bioError ? true : undefined, "aria-describedby": bioCountId, onChange: (e) => setField("bio", e.target.value), onBlur: () => markTouched("bio"), className: cn("w-full resize-y rounded-lg border px-4 py-3 text-sm text-grey-900 transition-colors", "placeholder:text-grey-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", bioError
118
+ ? "border-error-400 bg-surface"
119
+ : bioReadOnly
120
+ ? "border-grey-300 bg-grey-100"
121
+ : "border-grey-300 bg-surface hover:border-primary-100 focus-within:border-primary-400", saving && "cursor-not-allowed opacity-70") }), _jsx("p", { id: bioCountId, className: cn("text-sm leading-[1.45]", bioError ? "text-feedback-error" : "text-grey-600"), children: bioError ?? `${draft.bio.length} of ${bioMaxLength} characters used.` })] }), canEdit && (_jsxs("div", { className: "flex flex-col gap-3 border-t border-surface-border pt-5 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:justify-between", children: [_jsx("output", { className: "text-body-sm text-grey-600", children: saving
122
+ ? "Saving your changes…"
123
+ : dirty
124
+ ? "You have unsaved changes."
125
+ : "All changes saved." }), _jsxs("div", { className: "flex flex-wrap gap-3", children: [_jsx(Button, { type: "button", variant: "grey", appearance: "outlined", size: "sm", disabled: !dirty || saving, onClick: handleCancel, children: cancelLabel }), _jsx(Button, { type: "submit", variant: "primary", size: "sm", disabled: !dirty || saving, children: saving ? "Saving…" : saveLabel })] })] }))] }))] }) }));
126
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QuickSendBlock = QuickSendBlock;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const cn_1 = require("../utils/cn.cjs");
6
+ const Avatar_1 = require("../components/Avatar/index.cjs");
7
+ const Icon_1 = require("../components/Icon/index.cjs");
8
+ // ─── Component ───────────────────────────────────────────────────────
9
+ function QuickSendBlock({ title = "Quick Send", seeAllLabel = "See all beneficiaries", onSeeAll, beneficiaries, onSelect, className, }) {
10
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col gap-3", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-grey-900", children: title }), onSeeAll && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onSeeAll, className: "flex items-center gap-1.5 rounded text-xs font-semibold text-action-primary-text cursor-pointer hover:underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [seeAllLabel, (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chevron-right", size: "xs" })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "bg-surface border border-surface-border rounded-10 px-6 py-6", children: (0, jsx_runtime_1.jsx)("div", { className: "flex gap-5 overflow-x-auto", children: beneficiaries.map((b) => ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => onSelect?.(b.id), className: "flex flex-col items-center gap-2 shrink-0 group", children: [b.avatar ?? ((0, jsx_runtime_1.jsx)(Avatar_1.Avatar, { type: "initials", initials: b.initials ?? b.name.slice(0, 2).toUpperCase(), size: "lg" })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-center", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-xs font-semibold text-grey-900 whitespace-nowrap max-w-[80px] truncate", children: b.name }), (0, jsx_runtime_1.jsxs)("span", { className: "text-xs text-grey-500 whitespace-nowrap", children: ["@", b.handle] })] })] }, b.id))) }) })] }));
11
+ }
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../utils/cn.js";
3
+ import { Avatar } from "../components/Avatar/index.js";
4
+ import { Icon } from "../components/Icon/index.js";
5
+ // ─── Component ───────────────────────────────────────────────────────
6
+ export function QuickSendBlock({ title = "Quick Send", seeAllLabel = "See all beneficiaries", onSeeAll, beneficiaries, onSelect, className, }) {
7
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", className), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-lg font-semibold text-grey-900", children: title }), onSeeAll && (_jsxs("button", { type: "button", onClick: onSeeAll, className: "flex items-center gap-1.5 rounded text-xs font-semibold text-action-primary-text cursor-pointer hover:underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [seeAllLabel, _jsx(Icon, { name: "chevron-right", size: "xs" })] }))] }), _jsx("div", { className: "bg-surface border border-surface-border rounded-10 px-6 py-6", children: _jsx("div", { className: "flex gap-5 overflow-x-auto", children: beneficiaries.map((b) => (_jsxs("button", { type: "button", onClick: () => onSelect?.(b.id), className: "flex flex-col items-center gap-2 shrink-0 group", children: [b.avatar ?? (_jsx(Avatar, { type: "initials", initials: b.initials ?? b.name.slice(0, 2).toUpperCase(), size: "lg" })), _jsxs("div", { className: "flex flex-col items-center", children: [_jsx("span", { className: "text-xs font-semibold text-grey-900 whitespace-nowrap max-w-[80px] truncate", children: b.name }), _jsxs("span", { className: "text-xs text-grey-500 whitespace-nowrap", children: ["@", b.handle] })] })] }, b.id))) }) })] }));
8
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RecentTransactionsBlock = RecentTransactionsBlock;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const cn_1 = require("../utils/cn.cjs");
6
+ const Icon_1 = require("../components/Icon/index.cjs");
7
+ // ─── Component ───────────────────────────────────────────────────────
8
+ function RecentTransactionsBlock({ title = "Recent Transactions", seeAllLabel = "See all", onSeeAll, transactions, onTransactionClick, className, }) {
9
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col gap-3", className), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-lg font-semibold text-grey-900", children: title }), onSeeAll && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onSeeAll, className: "flex items-center gap-1.5 rounded text-xs font-semibold text-action-primary-text cursor-pointer hover:underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [seeAllLabel, (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chevron-right", size: "xs" })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "bg-surface border border-surface-border rounded-10 px-5 py-6", children: (0, jsx_runtime_1.jsx)("div", { className: "flex flex-col gap-5", children: transactions.map((tx) => {
10
+ const isOutgoing = tx.direction === "outgoing";
11
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => onTransactionClick?.(tx.id), className: "flex items-center gap-3 group text-left", children: [(0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("flex items-center justify-center size-10 rounded-full shrink-0", isOutgoing ? "bg-error-50" : "bg-success-50"), children: (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: isOutgoing ? "arrow-up" : "arrow-down", size: "sm", className: isOutgoing ? "text-error-400" : "text-success-400" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-1 items-center justify-between min-w-0", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col min-w-0", children: [(0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-grey-900 truncate", children: [(0, jsx_runtime_1.jsx)("span", { className: "font-normal", children: isOutgoing ? "to" : "from" }), " ", (0, jsx_runtime_1.jsx)("span", { className: "font-semibold", children: tx.name })] }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-grey-500", children: tx.category })] }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm font-semibold text-grey-900 shrink-0 ml-4", children: [isOutgoing ? "-" : "+", " ", tx.amount] })] })] }, tx.id));
12
+ }) }) })] }));
13
+ }
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../utils/cn.js";
3
+ import { Icon } from "../components/Icon/index.js";
4
+ // ─── Component ───────────────────────────────────────────────────────
5
+ export function RecentTransactionsBlock({ title = "Recent Transactions", seeAllLabel = "See all", onSeeAll, transactions, onTransactionClick, className, }) {
6
+ return (_jsxs("div", { className: cn("flex flex-col gap-3", className), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-lg font-semibold text-grey-900", children: title }), onSeeAll && (_jsxs("button", { type: "button", onClick: onSeeAll, className: "flex items-center gap-1.5 rounded text-xs font-semibold text-action-primary-text cursor-pointer hover:underline underline-offset-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [seeAllLabel, _jsx(Icon, { name: "chevron-right", size: "xs" })] }))] }), _jsx("div", { className: "bg-surface border border-surface-border rounded-10 px-5 py-6", children: _jsx("div", { className: "flex flex-col gap-5", children: transactions.map((tx) => {
7
+ const isOutgoing = tx.direction === "outgoing";
8
+ return (_jsxs("button", { type: "button", onClick: () => onTransactionClick?.(tx.id), className: "flex items-center gap-3 group text-left", children: [_jsx("div", { className: cn("flex items-center justify-center size-10 rounded-full shrink-0", isOutgoing ? "bg-error-50" : "bg-success-50"), children: _jsx(Icon, { name: isOutgoing ? "arrow-up" : "arrow-down", size: "sm", className: isOutgoing ? "text-error-400" : "text-success-400" }) }), _jsxs("div", { className: "flex flex-1 items-center justify-between min-w-0", children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("span", { className: "text-sm text-grey-900 truncate", children: [_jsx("span", { className: "font-normal", children: isOutgoing ? "to" : "from" }), " ", _jsx("span", { className: "font-semibold", children: tx.name })] }), _jsx("span", { className: "text-xs font-medium text-grey-500", children: tx.category })] }), _jsxs("span", { className: "text-sm font-semibold text-grey-900 shrink-0 ml-4", children: [isOutgoing ? "-" : "+", " ", tx.amount] })] })] }, tx.id));
9
+ }) }) })] }));
10
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchableTableBlock = SearchableTableBlock;
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 Input_1 = require("../components/Input/index.cjs");
8
+ const Button_1 = require("../components/Button/index.cjs");
9
+ const Icon_1 = require("../components/Icon/index.cjs");
10
+ const Table_1 = require("../components/Table/index.cjs");
11
+ const useTableSelection_1 = require("../components/Table/useTableSelection.cjs");
12
+ const FormControl_1 = require("../components/FormControl/index.cjs");
13
+ const Pagination_1 = require("../components/Pagination/index.cjs");
14
+ // ─── Kebab Button ────────────────────────────────────────────────────
15
+ function KebabButton({ onClick, rowLabel }) {
16
+ return ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onClick, "aria-label": rowLabel ? `More actions for ${rowLabel}` : "More actions", className: "flex items-center justify-center size-8 rounded-md hover:bg-grey-50 text-grey-500", children: (0, jsx_runtime_1.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "3", r: "1.5", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "8", r: "1.5", fill: "currentColor" }), (0, jsx_runtime_1.jsx)("circle", { cx: "8", cy: "13", r: "1.5", fill: "currentColor" })] }) }));
17
+ }
18
+ // ─── Component ───────────────────────────────────────────────────────
19
+ function SearchableTableBlock({ title, columns, rows, searchPlaceholder = "Search here...", onSearch, onSort, onSearchClick, onSortClick, showFilter, onFilter, showDateSelector, onDateSelect, selectable = false, selectedIds, onSelectionChange, onRowAction, page, totalPages, onPageChange, className, }) {
20
+ const [searchQuery, setSearchQuery] = (0, react_1.useState)("");
21
+ const [sortKey, setSortKey] = (0, react_1.useState)(null);
22
+ const [sortDirection, setSortDirection] = (0, react_1.useState)(null);
23
+ // ─── Handlers ────────────────────────────────────────────────────
24
+ const handleSearch = (value) => {
25
+ setSearchQuery(value);
26
+ onSearch?.(value);
27
+ };
28
+ const handleSort = (key) => {
29
+ let nextKey = key;
30
+ let nextDirection;
31
+ if (sortKey === key) {
32
+ nextDirection = sortDirection === "asc" ? "desc" : sortDirection === "desc" ? null : "asc";
33
+ if (nextDirection === null)
34
+ nextKey = null;
35
+ }
36
+ else {
37
+ nextDirection = "asc";
38
+ }
39
+ setSortKey(nextKey);
40
+ setSortDirection(nextDirection);
41
+ onSort?.(key, nextDirection);
42
+ };
43
+ // Previously the indicator moved but the rows never did, so the table announced an
44
+ // order it was not in. Unless the consumer takes control via onSearch / onSort, do
45
+ // the filtering and ordering here so indicator and content always agree.
46
+ const searched = onSearch || !searchQuery
47
+ ? rows
48
+ : rows.filter((row) => columns.some((col) => String(row[col.key] ?? "")
49
+ .toLowerCase()
50
+ .includes(searchQuery.toLowerCase())));
51
+ const visibleRows = onSort || !sortKey || !sortDirection
52
+ ? searched
53
+ : [...searched].sort((a, b) => {
54
+ const left = a[sortKey];
55
+ const right = b[sortKey];
56
+ if (left == null && right == null)
57
+ return 0;
58
+ if (left == null)
59
+ return 1;
60
+ if (right == null)
61
+ return -1;
62
+ const result = typeof left === "number" && typeof right === "number"
63
+ ? left - right
64
+ : String(left).localeCompare(String(right), undefined, { numeric: true });
65
+ return sortDirection === "asc" ? result : -result;
66
+ });
67
+ // `dataIds` is the whole data set and `scopeIds` only the rows on screen, so locally
68
+ // filtering or sorting keeps a hidden row's selection while the header still reports
69
+ // the visible rows. Rows the consumer removes from `rows` are dropped.
70
+ const selection = (0, useTableSelection_1.useTableSelection)({
71
+ dataIds: rows.map((row) => row.id),
72
+ scopeIds: visibleRows.map((row) => row.id),
73
+ selectedIds,
74
+ onSelectionChange,
75
+ });
76
+ // Every visible control needs somewhere to go: a toolbar button with no handler is
77
+ // dead, so presence of the handler is what makes it available.
78
+ const filterAvailable = Boolean(onFilter) && showFilter !== false;
79
+ const dateSelectorAvailable = Boolean(onDateSelect) && showDateSelector !== false;
80
+ // ─── Toolbar ─────────────────────────────────────────────────────
81
+ const toolbar = title ? (
82
+ /* Title variant toolbar */
83
+ (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pb-5", children: [(0, jsx_runtime_1.jsx)("h3", { className: "min-w-0 text-xl font-semibold text-grey-900", children: title }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [onSearchClick && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onSearchClick, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "search", size: "xs" }), (0, jsx_runtime_1.jsx)("span", { children: "Search" })] })), filterAvailable && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onFilter, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "filter", size: "xs" }), (0, jsx_runtime_1.jsx)("span", { children: "Filter" })] })), onSortClick && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: onSortClick, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chevron-v", size: "xs" }), (0, jsx_runtime_1.jsx)("span", { children: "Sort" })] }))] })] })) : (
84
+ /* Search variant toolbar */
85
+ (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-3 p-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-2", children: [(0, jsx_runtime_1.jsx)(Input_1.Input, { size: "sm", placeholder: searchPlaceholder, leadingIcon: "search", value: searchQuery, onChange: (e) => handleSearch(e.target.value), wrapperClassName: "w-full min-w-0 sm:w-[260px]" }), filterAvailable && ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "grey", appearance: "outlined", size: "sm", icon: "filter", iconPosition: "leading", onClick: onFilter, children: "Filter" }))] }), dateSelectorAvailable && ((0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "grey", appearance: "outlined", size: "sm", icon: "calendar", iconPosition: "leading", onClick: onDateSelect, children: "Select dates" }))] }));
86
+ // ─── Render ──────────────────────────────────────────────────────
87
+ return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col", className), children: [toolbar, (0, jsx_runtime_1.jsxs)(Table_1.Table, { "aria-label": title || "Data table", children: [(0, jsx_runtime_1.jsx)(Table_1.TableHeader, { children: (0, jsx_runtime_1.jsxs)(Table_1.TableRow, { children: [selectable && ((0, jsx_runtime_1.jsx)(Table_1.TableHead, { className: "w-[52px]", children: (0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { checked: selection.allSelected, indeterminate: selection.someSelected, onChange: selection.toggleAll, "aria-label": "Select all rows" }) })), columns.map((col) => ((0, jsx_runtime_1.jsx)(Table_1.TableHead, { sortable: col.sortable, sortDirection: sortKey === col.key ? sortDirection : undefined, onSort: () => handleSort(col.key), style: col.width ? { width: col.width } : undefined, children: col.label }, col.key))), onRowAction && ((0, jsx_runtime_1.jsx)(Table_1.TableHead, { className: "w-[60px]", children: (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: "Actions" }) }))] }) }), (0, jsx_runtime_1.jsx)(Table_1.TableBody, { children: visibleRows.map((row) => {
88
+ const rowLabel = columns[0] ? String(row[columns[0].key] ?? row.id) : row.id;
89
+ return ((0, jsx_runtime_1.jsxs)(Table_1.TableRow, { selected: selection.isSelected(row.id), children: [selectable && ((0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { checked: selection.isSelected(row.id), onChange: () => selection.toggleRow(row.id), "aria-label": `Select ${rowLabel}` }) })), columns.map((col) => ((0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "") }, col.key))), onRowAction && ((0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)(KebabButton, { onClick: () => onRowAction(row.id), rowLabel: rowLabel }) }))] }, row.id));
90
+ }) })] }), totalPages != null && page != null && onPageChange && ((0, jsx_runtime_1.jsx)(Pagination_1.Pagination, { currentPage: page, totalPages: totalPages, onPageChange: onPageChange }))] }));
91
+ }
@@ -0,0 +1,88 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { cn } from "../utils/cn.js";
4
+ import { Input } from "../components/Input/index.js";
5
+ import { Button } from "../components/Button/index.js";
6
+ import { Icon } from "../components/Icon/index.js";
7
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../components/Table/index.js";
8
+ import { useTableSelection } from "../components/Table/useTableSelection.js";
9
+ import { Checkbox } from "../components/FormControl/index.js";
10
+ import { Pagination } from "../components/Pagination/index.js";
11
+ // ─── Kebab Button ────────────────────────────────────────────────────
12
+ function KebabButton({ onClick, rowLabel }) {
13
+ return (_jsx("button", { type: "button", onClick: onClick, "aria-label": rowLabel ? `More actions for ${rowLabel}` : "More actions", className: "flex items-center justify-center size-8 rounded-md hover:bg-grey-50 text-grey-500", children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("circle", { cx: "8", cy: "3", r: "1.5", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "8", r: "1.5", fill: "currentColor" }), _jsx("circle", { cx: "8", cy: "13", r: "1.5", fill: "currentColor" })] }) }));
14
+ }
15
+ // ─── Component ───────────────────────────────────────────────────────
16
+ export function SearchableTableBlock({ title, columns, rows, searchPlaceholder = "Search here...", onSearch, onSort, onSearchClick, onSortClick, showFilter, onFilter, showDateSelector, onDateSelect, selectable = false, selectedIds, onSelectionChange, onRowAction, page, totalPages, onPageChange, className, }) {
17
+ const [searchQuery, setSearchQuery] = useState("");
18
+ const [sortKey, setSortKey] = useState(null);
19
+ const [sortDirection, setSortDirection] = useState(null);
20
+ // ─── Handlers ────────────────────────────────────────────────────
21
+ const handleSearch = (value) => {
22
+ setSearchQuery(value);
23
+ onSearch?.(value);
24
+ };
25
+ const handleSort = (key) => {
26
+ let nextKey = key;
27
+ let nextDirection;
28
+ if (sortKey === key) {
29
+ nextDirection = sortDirection === "asc" ? "desc" : sortDirection === "desc" ? null : "asc";
30
+ if (nextDirection === null)
31
+ nextKey = null;
32
+ }
33
+ else {
34
+ nextDirection = "asc";
35
+ }
36
+ setSortKey(nextKey);
37
+ setSortDirection(nextDirection);
38
+ onSort?.(key, nextDirection);
39
+ };
40
+ // Previously the indicator moved but the rows never did, so the table announced an
41
+ // order it was not in. Unless the consumer takes control via onSearch / onSort, do
42
+ // the filtering and ordering here so indicator and content always agree.
43
+ const searched = onSearch || !searchQuery
44
+ ? rows
45
+ : rows.filter((row) => columns.some((col) => String(row[col.key] ?? "")
46
+ .toLowerCase()
47
+ .includes(searchQuery.toLowerCase())));
48
+ const visibleRows = onSort || !sortKey || !sortDirection
49
+ ? searched
50
+ : [...searched].sort((a, b) => {
51
+ const left = a[sortKey];
52
+ const right = b[sortKey];
53
+ if (left == null && right == null)
54
+ return 0;
55
+ if (left == null)
56
+ return 1;
57
+ if (right == null)
58
+ return -1;
59
+ const result = typeof left === "number" && typeof right === "number"
60
+ ? left - right
61
+ : String(left).localeCompare(String(right), undefined, { numeric: true });
62
+ return sortDirection === "asc" ? result : -result;
63
+ });
64
+ // `dataIds` is the whole data set and `scopeIds` only the rows on screen, so locally
65
+ // filtering or sorting keeps a hidden row's selection while the header still reports
66
+ // the visible rows. Rows the consumer removes from `rows` are dropped.
67
+ const selection = useTableSelection({
68
+ dataIds: rows.map((row) => row.id),
69
+ scopeIds: visibleRows.map((row) => row.id),
70
+ selectedIds,
71
+ onSelectionChange,
72
+ });
73
+ // Every visible control needs somewhere to go: a toolbar button with no handler is
74
+ // dead, so presence of the handler is what makes it available.
75
+ const filterAvailable = Boolean(onFilter) && showFilter !== false;
76
+ const dateSelectorAvailable = Boolean(onDateSelect) && showDateSelector !== false;
77
+ // ─── Toolbar ─────────────────────────────────────────────────────
78
+ const toolbar = title ? (
79
+ /* Title variant toolbar */
80
+ _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pb-5", children: [_jsx("h3", { className: "min-w-0 text-xl font-semibold text-grey-900", children: title }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-2", children: [onSearchClick && (_jsxs("button", { type: "button", onClick: onSearchClick, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [_jsx(Icon, { name: "search", size: "xs" }), _jsx("span", { children: "Search" })] })), filterAvailable && (_jsxs("button", { type: "button", onClick: onFilter, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [_jsx(Icon, { name: "filter", size: "xs" }), _jsx("span", { children: "Filter" })] })), onSortClick && (_jsxs("button", { type: "button", onClick: onSortClick, className: "flex items-center gap-1 text-sm text-grey-500 hover:text-grey-700", children: [_jsx(Icon, { name: "chevron-v", size: "xs" }), _jsx("span", { children: "Sort" })] }))] })] })) : (
81
+ /* Search variant toolbar */
82
+ _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 p-4", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-center gap-2", children: [_jsx(Input, { size: "sm", placeholder: searchPlaceholder, leadingIcon: "search", value: searchQuery, onChange: (e) => handleSearch(e.target.value), wrapperClassName: "w-full min-w-0 sm:w-[260px]" }), filterAvailable && (_jsx(Button, { variant: "grey", appearance: "outlined", size: "sm", icon: "filter", iconPosition: "leading", onClick: onFilter, children: "Filter" }))] }), dateSelectorAvailable && (_jsx(Button, { variant: "grey", appearance: "outlined", size: "sm", icon: "calendar", iconPosition: "leading", onClick: onDateSelect, children: "Select dates" }))] }));
83
+ // ─── Render ──────────────────────────────────────────────────────
84
+ return (_jsxs("div", { className: cn("flex flex-col", className), children: [toolbar, _jsxs(Table, { "aria-label": title || "Data table", children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selectable && (_jsx(TableHead, { className: "w-[52px]", children: _jsx(Checkbox, { checked: selection.allSelected, indeterminate: selection.someSelected, onChange: selection.toggleAll, "aria-label": "Select all rows" }) })), columns.map((col) => (_jsx(TableHead, { sortable: col.sortable, sortDirection: sortKey === col.key ? sortDirection : undefined, onSort: () => handleSort(col.key), style: col.width ? { width: col.width } : undefined, children: col.label }, col.key))), onRowAction && (_jsx(TableHead, { className: "w-[60px]", children: _jsx("span", { className: "sr-only", children: "Actions" }) }))] }) }), _jsx(TableBody, { children: visibleRows.map((row) => {
85
+ const rowLabel = columns[0] ? String(row[columns[0].key] ?? row.id) : row.id;
86
+ return (_jsxs(TableRow, { selected: selection.isSelected(row.id), children: [selectable && (_jsx(TableCell, { children: _jsx(Checkbox, { checked: selection.isSelected(row.id), onChange: () => selection.toggleRow(row.id), "aria-label": `Select ${rowLabel}` }) })), columns.map((col) => (_jsx(TableCell, { children: col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "") }, col.key))), onRowAction && (_jsx(TableCell, { children: _jsx(KebabButton, { onClick: () => onRowAction(row.id), rowLabel: rowLabel }) }))] }, row.id));
87
+ }) })] }), totalPages != null && page != null && onPageChange && (_jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: onPageChange }))] }));
88
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShoppingCartBlock = ShoppingCartBlock;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const Button_1 = require("../components/Button/index.cjs");
6
+ const cn_1 = require("../utils/cn.cjs");
7
+ const commerce_1 = require("./commerce.cjs");
8
+ function ShoppingCartBlock({ title = "Your bag", description, items, layout = "split", onQuantityChange, onRemoveItem, onCheckout, onContinueShopping, pending = false, errorMessage, totalsNote, shipping, tax, discount, currency, locale, ...frame }) {
9
+ const unavailable = items.some((item) => item.quantity < 1 ||
10
+ !Number.isInteger(item.quantity) ||
11
+ item.quantity > (item.stock ?? Infinity));
12
+ return ((0, jsx_runtime_1.jsxs)(commerce_1.CommerceFrame, { ...frame, title: title, description: description, busy: pending, children: [errorMessage && ((0, jsx_runtime_1.jsx)("p", { role: "alert", className: "mb-4 text-sm text-error-700", children: errorMessage })), items.length === 0 ? ((0, jsx_runtime_1.jsxs)("div", { className: "border-y border-surface-border px-4 py-12", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-lg font-medium", children: "Your bag is empty" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-2 text-sm text-grey-600", children: "Find something you\u2019ll enjoy every day." }), onContinueShopping && ((0, jsx_runtime_1.jsx)(Button_1.Button, { className: "mt-6 h-auto min-h-11", onClick: onContinueShopping, children: "Explore the collection" }))] })) : ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("grid items-start gap-8", layout === "split" &&
13
+ "@min-[760px]:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)] @min-[960px]:gap-14"), children: [(0, jsx_runtime_1.jsx)("ul", { className: "divide-y divide-surface-border", children: items.map((item) => ((0, jsx_runtime_1.jsxs)("li", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-4 py-6 first:pt-0 @min-[460px]:grid-cols-[120px_minmax(0,1fr)]", children: [(0, jsx_runtime_1.jsx)(commerce_1.CommercePhoto, { image: item.image }), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0 space-y-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold", children: item.name }), item.variant && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-grey-600", children: item.variant }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-1 text-sm text-grey-600", children: [(0, commerce_1.formatCommerceMoney)(item.price, currency, locale), " each"] })] }), (0, jsx_runtime_1.jsx)("p", { className: "font-semibold tabular-nums", children: (0, commerce_1.formatCommerceMoney)(item.price * item.quantity, currency, locale) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onQuantityChange ? ((0, jsx_runtime_1.jsx)(commerce_1.CommerceQuantity, { name: item.name + (item.variant ? `, ${item.variant}` : ""), quantity: item.quantity, max: item.stock, disabled: pending, onChange: (quantity) => onQuantityChange(item.id, quantity) })) : ((0, jsx_runtime_1.jsxs)("p", { className: "text-sm", children: ["Quantity: ", item.quantity] })), onRemoveItem && ((0, jsx_runtime_1.jsx)("button", { type: "button", disabled: pending, className: `${commerce_1.commerceControl} border-transparent bg-transparent px-2 text-sm text-grey-600 underline underline-offset-4 hover:border-transparent hover:text-grey-900`, "aria-label": `Remove ${item.name}${item.variant ? `, ${item.variant}` : ""}`, onClick: () => onRemoveItem(item.id), children: "Remove" }))] }), item.quantity > (item.stock ?? Infinity) && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-error-700", children: item.stock === 0
14
+ ? "This item is no longer available. Remove it to continue."
15
+ : `Only ${item.stock} available. Please reduce the quantity.` }))] })] }, item.id))) }), (0, jsx_runtime_1.jsxs)("div", { className: "rounded-xl border border-surface-border bg-surface-muted p-5 @min-[440px]:p-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "mb-6 text-lg font-semibold tracking-tight", children: "Order summary" }), (0, jsx_runtime_1.jsx)(commerce_1.CommerceTotals, { items: items, shipping: shipping, tax: tax, discount: discount, currency: currency, locale: locale }), totalsNote && ((0, jsx_runtime_1.jsx)("p", { className: "mt-4 text-sm leading-relaxed text-grey-600", children: totalsNote })), onCheckout && ((0, jsx_runtime_1.jsx)(Button_1.Button, { className: "mt-6 h-auto min-h-12 w-full whitespace-normal", disabled: pending || unavailable, onClick: onCheckout, children: pending ? "Updating bag…" : "Review checkout" })), unavailable && ((0, jsx_runtime_1.jsx)("p", { role: "status", className: "mt-3 text-sm text-error-700", children: "Update the unavailable quantities before checkout." })), onContinueShopping && ((0, jsx_runtime_1.jsx)("button", { type: "button", disabled: pending, className: `${commerce_1.commerceControl} mt-3 w-full border-transparent bg-transparent text-sm hover:border-transparent hover:underline`, onClick: onContinueShopping, children: "Continue shopping" }))] })] }))] }));
16
+ }
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "../components/Button/index.js";
3
+ import { cn } from "../utils/cn.js";
4
+ import { CommerceFrame, CommercePhoto, CommerceQuantity, CommerceTotals, commerceControl, formatCommerceMoney, } from "./commerce.js";
5
+ export function ShoppingCartBlock({ title = "Your bag", description, items, layout = "split", onQuantityChange, onRemoveItem, onCheckout, onContinueShopping, pending = false, errorMessage, totalsNote, shipping, tax, discount, currency, locale, ...frame }) {
6
+ const unavailable = items.some((item) => item.quantity < 1 ||
7
+ !Number.isInteger(item.quantity) ||
8
+ item.quantity > (item.stock ?? Infinity));
9
+ return (_jsxs(CommerceFrame, { ...frame, title: title, description: description, busy: pending, children: [errorMessage && (_jsx("p", { role: "alert", className: "mb-4 text-sm text-error-700", children: errorMessage })), items.length === 0 ? (_jsxs("div", { className: "border-y border-surface-border px-4 py-12", children: [_jsx("p", { className: "text-lg font-medium", children: "Your bag is empty" }), _jsx("p", { className: "mt-2 text-sm text-grey-600", children: "Find something you\u2019ll enjoy every day." }), onContinueShopping && (_jsx(Button, { className: "mt-6 h-auto min-h-11", onClick: onContinueShopping, children: "Explore the collection" }))] })) : (_jsxs("div", { className: cn("grid items-start gap-8", layout === "split" &&
10
+ "@min-[760px]:grid-cols-[minmax(0,1.6fr)_minmax(0,1fr)] @min-[960px]:gap-14"), children: [_jsx("ul", { className: "divide-y divide-surface-border", children: items.map((item) => (_jsxs("li", { className: "grid grid-cols-[80px_minmax(0,1fr)] gap-4 py-6 first:pt-0 @min-[460px]:grid-cols-[120px_minmax(0,1fr)]", children: [_jsx(CommercePhoto, { image: item.image }), _jsxs("div", { className: "min-w-0 space-y-3", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2", children: [_jsxs("div", { children: [_jsx("p", { className: "font-semibold", children: item.name }), item.variant && _jsx("p", { className: "mt-1 text-sm text-grey-600", children: item.variant }), _jsxs("p", { className: "mt-1 text-sm text-grey-600", children: [formatCommerceMoney(item.price, currency, locale), " each"] })] }), _jsx("p", { className: "font-semibold tabular-nums", children: formatCommerceMoney(item.price * item.quantity, currency, locale) })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [onQuantityChange ? (_jsx(CommerceQuantity, { name: item.name + (item.variant ? `, ${item.variant}` : ""), quantity: item.quantity, max: item.stock, disabled: pending, onChange: (quantity) => onQuantityChange(item.id, quantity) })) : (_jsxs("p", { className: "text-sm", children: ["Quantity: ", item.quantity] })), onRemoveItem && (_jsx("button", { type: "button", disabled: pending, className: `${commerceControl} border-transparent bg-transparent px-2 text-sm text-grey-600 underline underline-offset-4 hover:border-transparent hover:text-grey-900`, "aria-label": `Remove ${item.name}${item.variant ? `, ${item.variant}` : ""}`, onClick: () => onRemoveItem(item.id), children: "Remove" }))] }), item.quantity > (item.stock ?? Infinity) && (_jsx("p", { className: "text-sm text-error-700", children: item.stock === 0
11
+ ? "This item is no longer available. Remove it to continue."
12
+ : `Only ${item.stock} available. Please reduce the quantity.` }))] })] }, item.id))) }), _jsxs("div", { className: "rounded-xl border border-surface-border bg-surface-muted p-5 @min-[440px]:p-6", children: [_jsx("p", { className: "mb-6 text-lg font-semibold tracking-tight", children: "Order summary" }), _jsx(CommerceTotals, { items: items, shipping: shipping, tax: tax, discount: discount, currency: currency, locale: locale }), totalsNote && (_jsx("p", { className: "mt-4 text-sm leading-relaxed text-grey-600", children: totalsNote })), onCheckout && (_jsx(Button, { className: "mt-6 h-auto min-h-12 w-full whitespace-normal", disabled: pending || unavailable, onClick: onCheckout, children: pending ? "Updating bag…" : "Review checkout" })), unavailable && (_jsx("p", { role: "status", className: "mt-3 text-sm text-error-700", children: "Update the unavailable quantities before checkout." })), onContinueShopping && (_jsx("button", { type: "button", disabled: pending, className: `${commerceControl} mt-3 w-full border-transparent bg-transparent text-sm hover:border-transparent hover:underline`, onClick: onContinueShopping, children: "Continue shopping" }))] })] }))] }));
13
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SiteFooterBlock = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const cn_1 = require("../utils/cn.cjs");
7
+ const Badge_1 = require("../components/Badge/index.cjs");
8
+ const Icon_1 = require("../components/Icon/index.cjs");
9
+ /* ─── Appearance ───────────────────────────────────────────────────────── */
10
+ const LINK_CLASS = "inline-flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 rounded text-sm break-words text-grey-600 no-underline hover:text-grey-900 hover:underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text";
11
+ /* Two columns below about 420px leaves roughly 130px per link, which forces long
12
+ compound words to break mid-word. Start at one column and add the second only
13
+ once the container can hold a full word. */
14
+ const GROUP_COLUMN_CLASS = {
15
+ 1: "grid-cols-1",
16
+ 2: "grid-cols-1 @min-[420px]:grid-cols-2",
17
+ 3: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3",
18
+ 4: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3 @min-[1136px]:grid-cols-4",
19
+ 5: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3 @min-[1136px]:grid-cols-5",
20
+ };
21
+ function FooterLinkAnchor({ link, className }) {
22
+ const externalAttributes = link.external
23
+ ? { target: "_blank", rel: "noreferrer" }
24
+ : {};
25
+ return ((0, jsx_runtime_1.jsxs)("a", { href: link.href, onClick: link.onClick, className: (0, cn_1.cn)(LINK_CLASS, className), ...externalAttributes, children: [(0, jsx_runtime_1.jsx)("span", { className: "min-w-0", children: link.label }), link.badge && ((0, jsx_runtime_1.jsx)(Badge_1.Badge, { color: "orange", type: "accent", size: "sm", children: link.badge })), link.external && (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: "(opens in a new tab)" })] }));
26
+ }
27
+ /**
28
+ * A native select is used deliberately. It keeps the control usable at 320px,
29
+ * gives the platform picker on touch devices, and follows the semantic
30
+ * `--color-surface` role so a consumer theme reaches it.
31
+ */
32
+ function LocaleSelect({ control }) {
33
+ const selectId = (0, react_1.useId)();
34
+ const hintId = (0, react_1.useId)();
35
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "flex w-full min-w-0 max-w-[240px] flex-col gap-1.5", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: selectId, className: "text-sm font-medium break-words text-grey-700", children: control.label }), (0, jsx_runtime_1.jsxs)("div", { className: "relative w-full min-w-0", children: [(0, jsx_runtime_1.jsx)("select", { id: selectId, value: control.value, "aria-describedby": control.hint ? hintId : undefined, onChange: (event) => control.onChange(event.target.value), className: "h-11 w-full min-w-0 cursor-pointer appearance-none truncate rounded-md border border-grey-500 bg-surface py-2 pr-10 pl-3 text-sm text-grey-900 transition-colors hover:border-grey-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: control.options.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.value, children: option.label }, option.value))) }), (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: "chevron-down", size: "md", "aria-hidden": "true", className: "pointer-events-none absolute top-1/2 right-3 -translate-y-1/2 text-grey-500" })] }), control.hint && ((0, jsx_runtime_1.jsx)("p", { id: hintId, className: "text-xs break-words text-grey-500", children: control.hint }))] }));
36
+ }
37
+ /* ─── Block ────────────────────────────────────────────────────────────── */
38
+ /**
39
+ * Site Footer — browse grouped site links, legal information, social
40
+ * destinations, and locale controls.
41
+ *
42
+ * Only a default state is meaningful: the footer presents destinations the
43
+ * consuming application already knows. There is nothing to load, submit, or
44
+ * confirm, so no loading, error, or success state is offered. Every section is
45
+ * optional; omitting one removes it rather than rendering an empty shell. A
46
+ * locale control requires an `onChange` handler, so the footer can never render
47
+ * a switcher that does nothing.
48
+ */
49
+ exports.SiteFooterBlock = (0, react_1.forwardRef)(({ label = "Site footer", brand, groups, groupHeadingLevel = "h2", social, socialLabel = "Social media", copyright, legalLinks, legalLabel = "Legal", localeControls, children, className, ...rest }, ref) => {
50
+ const groupIdPrefix = (0, react_1.useId)();
51
+ const GroupHeading = groupHeadingLevel;
52
+ const groupList = groups ?? [];
53
+ const socialList = social ?? [];
54
+ const localeList = localeControls ?? [];
55
+ const legalList = legalLinks ?? [];
56
+ const hasTop = Boolean(brand) || groupList.length > 0;
57
+ const hasBottom = Boolean(copyright) || legalList.length > 0 || socialList.length > 0 || localeList.length > 0;
58
+ const brandBody = brand && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: brand.logo ??
59
+ (brand.name && ((0, jsx_runtime_1.jsx)("span", { className: "text-body-lg font-semibold break-words text-grey-900", children: brand.name }))) }));
60
+ return ((0, jsx_runtime_1.jsx)("footer", { ref: ref, "aria-label": label, className: (0, cn_1.cn)("@container relative w-full border-t border-surface-border-strong bg-surface-muted", className), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "mx-auto flex w-full max-w-[1136px] flex-col gap-10 px-4 py-10 @min-[600px]:px-8 @min-[600px]:py-14", children: [hasTop && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-10 @min-[1136px]:flex-row @min-[1136px]:gap-16", children: [brand && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-3 @min-[1136px]:max-w-[300px] @min-[1136px]:shrink-0", children: [brand.href ? ((0, jsx_runtime_1.jsx)("a", { href: brand.href, className: "inline-flex max-w-full items-center gap-2 rounded no-underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: brandBody })) : ((0, jsx_runtime_1.jsx)("div", { className: "inline-flex max-w-full items-center gap-2", children: brandBody })), brand.description && ((0, jsx_runtime_1.jsx)("p", { className: "max-w-[46ch] text-sm break-words text-grey-600", children: brand.description }))] })), groupList.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("grid min-w-0 gap-x-6 gap-y-8 @min-[1136px]:flex-1", GROUP_COLUMN_CLASS[Math.min(Math.max(groupList.length, 1), 5)]), children: groupList.map((group) => {
61
+ const headingId = `${groupIdPrefix}-${group.id}`;
62
+ return ((0, jsx_runtime_1.jsxs)("nav", { "aria-labelledby": headingId, className: "flex min-w-0 flex-col gap-3", children: [(0, jsx_runtime_1.jsx)(GroupHeading, { id: headingId, className: "text-sm font-semibold break-words text-grey-900", children: group.title }), (0, jsx_runtime_1.jsx)("ul", { className: "m-0 flex list-none flex-col gap-2.5 p-0", children: group.links.map((link) => ((0, jsx_runtime_1.jsx)("li", { className: "min-w-0", children: (0, jsx_runtime_1.jsx)(FooterLinkAnchor, { link: link }) }, link.id))) })] }, group.id));
63
+ }) }))] })), localeList.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "flex min-w-0 flex-col gap-4 border-t border-surface-border pt-8 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:gap-6", children: localeList.map((control) => ((0, jsx_runtime_1.jsx)(LocaleSelect, { control: control }, control.id))) })), children && (0, jsx_runtime_1.jsx)("div", { className: "min-w-0", children: children }), hasBottom && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-5 border-t border-surface-border pt-8 @min-[600px]:flex-row @min-[600px]:flex-wrap @min-[600px]:items-center @min-[600px]:justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-3", children: [copyright && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm break-words text-grey-600", children: copyright })), legalList.length > 0 && ((0, jsx_runtime_1.jsx)("nav", { "aria-label": legalLabel, className: "min-w-0", children: (0, jsx_runtime_1.jsx)("ul", { className: "m-0 flex list-none flex-wrap gap-x-5 gap-y-2 p-0", children: legalList.map((link) => ((0, jsx_runtime_1.jsx)("li", { className: "min-w-0", children: (0, jsx_runtime_1.jsx)(FooterLinkAnchor, { link: link }) }, link.id))) }) }))] }), socialList.length > 0 && ((0, jsx_runtime_1.jsx)("nav", { "aria-label": socialLabel, className: "min-w-0", children: (0, jsx_runtime_1.jsx)("ul", { className: "m-0 flex list-none flex-wrap items-center gap-2 p-0", children: socialList.map((entry) => {
64
+ const isExternal = entry.external !== false;
65
+ const visual = entry.artwork ?? (entry.icon && (0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: entry.icon, size: "md", "aria-hidden": "true" }));
66
+ return ((0, jsx_runtime_1.jsx)("li", { className: "min-w-0", children: (0, jsx_runtime_1.jsx)("a", { href: entry.href, "aria-label": visual ? entry.label : undefined, className: (0, cn_1.cn)("inline-flex min-h-11 min-w-11 max-w-full items-center justify-center gap-2 rounded-lg border border-transparent px-2 text-grey-600 no-underline transition-colors hover:border-surface-border-strong hover:bg-surface hover:text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", !visual && "text-sm font-medium break-words"), ...(isExternal ? { target: "_blank", rel: "noreferrer" } : {}), children: visual ?? (0, jsx_runtime_1.jsx)("span", { className: "min-w-0", children: entry.label }) }) }, entry.id));
67
+ }) }) }))] }))] }) }));
68
+ });
69
+ exports.SiteFooterBlock.displayName = "SiteFooterBlock";