@raydenui/ui 0.9.7 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Table-DCNSxua1.d.cts +31 -0
- package/dist/Table-DCNSxua1.d.ts +31 -0
- package/dist/blocks/ApplicationShellBlock.cjs +157 -0
- package/dist/blocks/ApplicationShellBlock.js +154 -0
- package/dist/blocks/CheckoutReviewBlock.cjs +16 -0
- package/dist/blocks/CheckoutReviewBlock.js +13 -0
- package/dist/blocks/CommandPaletteBlock.cjs +334 -0
- package/dist/blocks/CommandPaletteBlock.js +331 -0
- package/dist/blocks/CreateAccountBlock.cjs +147 -0
- package/dist/blocks/CreateAccountBlock.js +143 -0
- package/dist/blocks/EmptyStateBlock.cjs +19 -0
- package/dist/blocks/EmptyStateBlock.js +16 -0
- package/dist/blocks/FeatureOverviewBlock.cjs +54 -0
- package/dist/blocks/FeatureOverviewBlock.js +51 -0
- package/dist/blocks/HeaderBlock.cjs +230 -0
- package/dist/blocks/HeaderBlock.js +227 -0
- package/dist/blocks/KpiOverviewBlock.cjs +63 -0
- package/dist/blocks/KpiOverviewBlock.js +60 -0
- package/dist/blocks/LoginBlock.cjs +66 -0
- package/dist/blocks/LoginBlock.js +63 -0
- package/dist/blocks/NotificationsBlock.cjs +17 -0
- package/dist/blocks/NotificationsBlock.js +14 -0
- package/dist/blocks/PageHeaderBlock.cjs +121 -0
- package/dist/blocks/PageHeaderBlock.js +118 -0
- package/dist/blocks/PricingPlansBlock.cjs +146 -0
- package/dist/blocks/PricingPlansBlock.js +143 -0
- package/dist/blocks/ProductCollectionBlock.cjs +37 -0
- package/dist/blocks/ProductCollectionBlock.js +34 -0
- package/dist/blocks/ProductDetailBlock.cjs +30 -0
- package/dist/blocks/ProductDetailBlock.js +27 -0
- package/dist/blocks/ProductHeroBlock.cjs +83 -0
- package/dist/blocks/ProductHeroBlock.js +80 -0
- package/dist/blocks/ProfileSettingsBlock.cjs +130 -0
- package/dist/blocks/ProfileSettingsBlock.js +126 -0
- package/dist/blocks/QuickSendBlock.cjs +11 -0
- package/dist/blocks/QuickSendBlock.js +8 -0
- package/dist/blocks/RecentTransactionsBlock.cjs +13 -0
- package/dist/blocks/RecentTransactionsBlock.js +10 -0
- package/dist/blocks/SearchableTableBlock.cjs +91 -0
- package/dist/blocks/SearchableTableBlock.js +88 -0
- package/dist/blocks/ShoppingCartBlock.cjs +16 -0
- package/dist/blocks/ShoppingCartBlock.js +13 -0
- package/dist/blocks/SiteFooterBlock.cjs +69 -0
- package/dist/blocks/SiteFooterBlock.js +66 -0
- package/dist/blocks/TableBlock.cjs +20 -0
- package/dist/blocks/TableBlock.js +17 -0
- package/dist/blocks/TaskListBlock.cjs +198 -0
- package/dist/blocks/TaskListBlock.js +195 -0
- package/dist/blocks/WorkspaceSwitcherBlock.cjs +291 -0
- package/dist/blocks/WorkspaceSwitcherBlock.js +288 -0
- package/dist/blocks/commerce.cjs +44 -0
- package/dist/blocks/commerce.js +35 -0
- package/dist/blocks/index.cjs +53 -0
- package/dist/blocks/index.js +24 -0
- package/dist/blocks.d.cts +1820 -0
- package/dist/blocks.d.ts +1820 -0
- package/dist/chart.cjs +13 -0
- package/dist/chart.d.cts +59 -0
- package/dist/chart.d.ts +59 -0
- package/dist/chart.js +5 -0
- package/dist/components/Accordion/Accordion.cjs +94 -0
- package/dist/components/Accordion/Accordion.js +91 -0
- package/dist/components/Accordion/index.cjs +8 -0
- package/dist/components/Accordion/index.js +1 -0
- package/dist/components/ActivityFeed/ActivityContent.cjs +40 -0
- package/dist/components/ActivityFeed/ActivityContent.js +37 -0
- package/dist/components/ActivityFeed/ActivityItem.cjs +19 -0
- package/dist/components/ActivityFeed/ActivityItem.js +16 -0
- package/dist/components/ActivityFeed/index.cjs +7 -0
- package/dist/components/ActivityFeed/index.js +2 -0
- package/dist/components/Alert/Alert.cjs +42 -0
- package/dist/components/Alert/Alert.js +39 -0
- package/dist/components/Alert/index.cjs +5 -0
- package/dist/components/Alert/index.js +1 -0
- package/dist/components/Avatar/Avatar.cjs +101 -0
- package/dist/components/Avatar/Avatar.js +98 -0
- package/dist/components/Avatar/index.cjs +6 -0
- package/dist/components/Avatar/index.js +1 -0
- package/dist/components/Badge/Badge.cjs +53 -0
- package/dist/components/Badge/Badge.js +50 -0
- package/dist/components/Badge/index.cjs +5 -0
- package/dist/components/Badge/index.js +1 -0
- package/dist/components/Banner/Banner.cjs +83 -0
- package/dist/components/Banner/Banner.js +80 -0
- package/dist/components/Banner/index.cjs +5 -0
- package/dist/components/Banner/index.js +1 -0
- package/dist/components/Breadcrumb/Breadcrumb.cjs +31 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +28 -0
- package/dist/components/Breadcrumb/index.cjs +5 -0
- package/dist/components/Breadcrumb/index.js +1 -0
- package/dist/components/Button/Button.cjs +88 -0
- package/dist/components/Button/Button.js +85 -0
- package/dist/components/Button/index.cjs +5 -0
- package/dist/components/Button/index.js +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.cjs +8 -0
- package/dist/components/ButtonGroup/ButtonGroup.js +5 -0
- package/dist/components/ButtonGroup/ButtonGroupItem.cjs +15 -0
- package/dist/components/ButtonGroup/ButtonGroupItem.js +12 -0
- package/dist/components/ButtonGroup/index.cjs +7 -0
- package/dist/components/ButtonGroup/index.js +2 -0
- package/dist/components/Card/Card.cjs +94 -0
- package/dist/components/Card/Card.js +91 -0
- package/dist/components/Card/index.cjs +9 -0
- package/dist/components/Card/index.js +1 -0
- package/dist/components/Chart/Chart.cjs +160 -0
- package/dist/components/Chart/Chart.js +153 -0
- package/dist/components/Chart/index.cjs +10 -0
- package/dist/components/Chart/index.js +2 -0
- package/dist/components/Chart/theme.cjs +67 -0
- package/dist/components/Chart/theme.js +62 -0
- package/dist/components/Chip/Chip.cjs +18 -0
- package/dist/components/Chip/Chip.js +15 -0
- package/dist/components/Chip/index.cjs +5 -0
- package/dist/components/Chip/index.js +1 -0
- package/dist/components/Counter/Counter.cjs +74 -0
- package/dist/components/Counter/Counter.js +71 -0
- package/dist/components/Counter/index.cjs +6 -0
- package/dist/components/Counter/index.js +1 -0
- package/dist/components/DatePicker/DatePicker.cjs +343 -0
- package/dist/components/DatePicker/DatePicker.js +340 -0
- package/dist/components/DatePicker/index.cjs +5 -0
- package/dist/components/DatePicker/index.js +1 -0
- package/dist/components/Divider/Divider.cjs +31 -0
- package/dist/components/Divider/Divider.js +28 -0
- package/dist/components/Divider/index.cjs +5 -0
- package/dist/components/Divider/index.js +1 -0
- package/dist/components/DropdownMenu/DropdownMenu.cjs +108 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +105 -0
- package/dist/components/DropdownMenu/index.cjs +11 -0
- package/dist/components/DropdownMenu/index.js +1 -0
- package/dist/components/EmptyStateIllustration/EmptyStateIllustration.cjs +42 -0
- package/dist/components/EmptyStateIllustration/EmptyStateIllustration.js +39 -0
- package/dist/components/EmptyStateIllustration/illustrations.cjs +3322 -0
- package/dist/components/EmptyStateIllustration/illustrations.js +3319 -0
- package/dist/components/EmptyStateIllustration/index.cjs +7 -0
- package/dist/components/EmptyStateIllustration/index.js +2 -0
- package/dist/components/FileUpload/FileUpload.cjs +116 -0
- package/dist/components/FileUpload/FileUpload.js +113 -0
- package/dist/components/FileUpload/FileUploadDropZone.cjs +58 -0
- package/dist/components/FileUpload/FileUploadDropZone.js +55 -0
- package/dist/components/FileUpload/FileUploadIcons.cjs +55 -0
- package/dist/components/FileUpload/FileUploadIcons.js +50 -0
- package/dist/components/FileUpload/FileUploadItem.cjs +34 -0
- package/dist/components/FileUpload/FileUploadItem.js +31 -0
- package/dist/components/FileUpload/index.cjs +9 -0
- package/dist/components/FileUpload/index.js +3 -0
- package/dist/components/FormControl/Checkbox.cjs +27 -0
- package/dist/components/FormControl/Checkbox.js +24 -0
- package/dist/components/FormControl/Radio.cjs +15 -0
- package/dist/components/FormControl/Radio.js +12 -0
- package/dist/components/FormControl/Toggle.cjs +15 -0
- package/dist/components/FormControl/Toggle.js +12 -0
- package/dist/components/FormControl/index.cjs +9 -0
- package/dist/components/FormControl/index.js +3 -0
- package/dist/components/Icon/Icon.cjs +70 -0
- package/dist/components/Icon/Icon.js +34 -0
- package/dist/components/Icon/catalog.cjs +1672 -0
- package/dist/components/Icon/catalog.js +1669 -0
- package/dist/components/Icon/icons.cjs +5621 -0
- package/dist/components/Icon/icons.js +5610 -0
- package/dist/components/Icon/index.cjs +8 -0
- package/dist/components/Icon/index.js +2 -0
- package/dist/components/Input/Input.cjs +64 -0
- package/dist/components/Input/Input.js +61 -0
- package/dist/components/Input/index.cjs +5 -0
- package/dist/components/Input/index.js +1 -0
- package/dist/components/MetricsCard/MetricsCard.cjs +107 -0
- package/dist/components/MetricsCard/MetricsCard.js +104 -0
- package/dist/components/MetricsCard/index.cjs +5 -0
- package/dist/components/MetricsCard/index.js +1 -0
- package/dist/components/Modal/Modal.cjs +126 -0
- package/dist/components/Modal/Modal.js +123 -0
- package/dist/components/Modal/index.cjs +5 -0
- package/dist/components/Modal/index.js +1 -0
- package/dist/components/Pagination/Pagination.cjs +42 -0
- package/dist/components/Pagination/Pagination.js +39 -0
- package/dist/components/Pagination/index.cjs +5 -0
- package/dist/components/Pagination/index.js +1 -0
- package/dist/components/ProgressBar/ProgressBar.cjs +34 -0
- package/dist/components/ProgressBar/ProgressBar.js +31 -0
- package/dist/components/ProgressBar/index.cjs +5 -0
- package/dist/components/ProgressBar/index.js +1 -0
- package/dist/components/ProgressCircle/ProgressCircle.cjs +34 -0
- package/dist/components/ProgressCircle/ProgressCircle.js +31 -0
- package/dist/components/ProgressCircle/index.cjs +5 -0
- package/dist/components/ProgressCircle/index.js +1 -0
- package/dist/components/Select/Select.cjs +84 -0
- package/dist/components/Select/Select.js +80 -0
- package/dist/components/Select/SelectOption.cjs +26 -0
- package/dist/components/Select/SelectOption.js +23 -0
- package/dist/components/Select/index.cjs +7 -0
- package/dist/components/Select/index.js +2 -0
- package/dist/components/SidebarMenu/SidebarMenu.cjs +85 -0
- package/dist/components/SidebarMenu/SidebarMenu.js +81 -0
- package/dist/components/SidebarMenu/SidebarMenuItem.cjs +126 -0
- package/dist/components/SidebarMenu/SidebarMenuItem.js +122 -0
- package/dist/components/SidebarMenu/SidebarMenuSection.cjs +14 -0
- package/dist/components/SidebarMenu/SidebarMenuSection.js +11 -0
- package/dist/components/SidebarMenu/index.cjs +12 -0
- package/dist/components/SidebarMenu/index.js +4 -0
- package/dist/components/Slider/Slider.cjs +161 -0
- package/dist/components/Slider/Slider.js +158 -0
- package/dist/components/Slider/index.cjs +6 -0
- package/dist/components/Slider/index.js +1 -0
- package/dist/components/Spinner/Spinner.cjs +93 -0
- package/dist/components/Spinner/Spinner.js +90 -0
- package/dist/components/Spinner/index.cjs +5 -0
- package/dist/components/Spinner/index.js +1 -0
- package/dist/components/Stepper/Stepper.cjs +82 -0
- package/dist/components/Stepper/Stepper.js +79 -0
- package/dist/components/Stepper/index.cjs +7 -0
- package/dist/components/Stepper/index.js +1 -0
- package/dist/components/Table/Table.cjs +25 -0
- package/dist/components/Table/Table.js +22 -0
- package/dist/components/Table/index.cjs +10 -0
- package/dist/components/Table/index.js +1 -0
- package/dist/components/Table/useTableSelection.cjs +60 -0
- package/dist/components/Table/useTableSelection.js +57 -0
- package/dist/components/Tabs/Tab.cjs +70 -0
- package/dist/components/Tabs/Tab.js +67 -0
- package/dist/components/Tabs/Tabs.cjs +133 -0
- package/dist/components/Tabs/Tabs.js +129 -0
- package/dist/components/Tabs/index.cjs +7 -0
- package/dist/components/Tabs/index.js +2 -0
- package/dist/components/Tooltip/Tooltip.cjs +101 -0
- package/dist/components/Tooltip/Tooltip.js +98 -0
- package/dist/components/Tooltip/index.cjs +5 -0
- package/dist/components/Tooltip/index.js +1 -0
- package/dist/context/ThemeContext.cjs +97 -0
- package/dist/context/ThemeContext.js +92 -0
- package/dist/fonts/README.md +13 -0
- package/dist/fonts/hanken-grotesk-cyrillic-ext-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-cyrillic-ext-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-ext-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-ext-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-latin-normal.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-vietnamese-italic.woff2 +0 -0
- package/dist/fonts/hanken-grotesk-vietnamese-normal.woff2 +0 -0
- package/dist/fonts/hankengrotesk-LICENSE.txt +94 -0
- package/dist/fonts/manrope-LICENSE.txt +93 -0
- package/dist/fonts/manrope-cyrillic-ext-normal.woff2 +0 -0
- package/dist/fonts/manrope-cyrillic-normal.woff2 +0 -0
- package/dist/fonts/manrope-greek-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-ext-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-normal.woff2 +0 -0
- package/dist/fonts/manrope-vietnamese-normal.woff2 +0 -0
- package/dist/fonts.css +148 -0
- package/dist/hooks/form/index.cjs +11 -0
- package/dist/hooks/form/index.js +3 -0
- package/dist/hooks/form/useRaydenFormControl.cjs +81 -0
- package/dist/hooks/form/useRaydenFormControl.js +76 -0
- package/dist/hooks/form/useRaydenInput.cjs +38 -0
- package/dist/hooks/form/useRaydenInput.js +35 -0
- package/dist/hooks/form/useRaydenSelect.cjs +45 -0
- package/dist/hooks/form/useRaydenSelect.js +42 -0
- package/dist/hooks/index.cjs +17 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useBodyScrollLock.cjs +24 -0
- package/dist/hooks/useBodyScrollLock.js +21 -0
- package/dist/hooks/useCollisionAwareSide.cjs +72 -0
- package/dist/hooks/useCollisionAwareSide.js +69 -0
- package/dist/hooks/useControllableValue.cjs +16 -0
- package/dist/hooks/useControllableValue.js +13 -0
- package/dist/hooks/useDismissableLayer.cjs +100 -0
- package/dist/hooks/useDismissableLayer.js +97 -0
- package/dist/hooks/usePopupList.cjs +101 -0
- package/dist/hooks/usePopupList.js +98 -0
- package/dist/icons-8yD7I5jO.d.cts +430 -0
- package/dist/icons-8yD7I5jO.d.ts +430 -0
- package/dist/icons.cjs +2111 -418
- package/dist/icons.d.cts +1256 -429
- package/dist/icons.d.ts +1256 -429
- package/dist/icons.js +2089 -1
- package/dist/index.cjs +128 -15046
- package/dist/index.d.cts +697 -662
- package/dist/index.d.ts +697 -662
- package/dist/index.js +43 -14962
- package/dist/motion/Collapse.cjs +28 -0
- package/dist/motion/Collapse.js +25 -0
- package/dist/motion/MotionProvider.cjs +43 -0
- package/dist/motion/MotionProvider.js +38 -0
- package/dist/motion/Pressable.cjs +46 -0
- package/dist/motion/Pressable.js +43 -0
- package/dist/motion/Reveal.cjs +19 -0
- package/dist/motion/Reveal.js +16 -0
- package/dist/motion/SharedLayout.cjs +46 -0
- package/dist/motion/SharedLayout.js +43 -0
- package/dist/motion/index.cjs +19 -0
- package/dist/motion/index.js +6 -0
- package/dist/motion/presence.cjs +58 -0
- package/dist/motion/presence.js +55 -0
- package/dist/motion/presets.cjs +88 -0
- package/dist/motion/presets.js +84 -0
- package/dist/motion.d.cts +51 -0
- package/dist/motion.d.ts +51 -0
- package/dist/preset.cjs +356 -195
- package/dist/preset.d.cts +111 -32
- package/dist/preset.d.ts +111 -32
- package/dist/preset.js +353 -187
- package/dist/presets-Ba5QndHg.d.cts +144 -0
- package/dist/presets-Ba5QndHg.d.ts +144 -0
- package/dist/styles.css +1 -1
- package/dist/utils/cn.cjs +38 -0
- package/dist/utils/cn.js +35 -0
- package/dist/utils/resolveIcon.cjs +18 -0
- package/dist/utils/resolveIcon.js +15 -0
- package/package.json +79 -21
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useId, } from "react";
|
|
3
|
+
import { cn } from "../utils/cn.js";
|
|
4
|
+
import { Badge } from "../components/Badge/index.js";
|
|
5
|
+
import { Icon } from "../components/Icon/index.js";
|
|
6
|
+
/* ─── Appearance ───────────────────────────────────────────────────────── */
|
|
7
|
+
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";
|
|
8
|
+
/* Two columns below about 420px leaves roughly 130px per link, which forces long
|
|
9
|
+
compound words to break mid-word. Start at one column and add the second only
|
|
10
|
+
once the container can hold a full word. */
|
|
11
|
+
const GROUP_COLUMN_CLASS = {
|
|
12
|
+
1: "grid-cols-1",
|
|
13
|
+
2: "grid-cols-1 @min-[420px]:grid-cols-2",
|
|
14
|
+
3: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3",
|
|
15
|
+
4: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3 @min-[1136px]:grid-cols-4",
|
|
16
|
+
5: "grid-cols-1 @min-[420px]:grid-cols-2 @min-[600px]:grid-cols-3 @min-[1136px]:grid-cols-5",
|
|
17
|
+
};
|
|
18
|
+
function FooterLinkAnchor({ link, className }) {
|
|
19
|
+
const externalAttributes = link.external
|
|
20
|
+
? { target: "_blank", rel: "noreferrer" }
|
|
21
|
+
: {};
|
|
22
|
+
return (_jsxs("a", { href: link.href, onClick: link.onClick, className: cn(LINK_CLASS, className), ...externalAttributes, children: [_jsx("span", { className: "min-w-0", children: link.label }), link.badge && (_jsx(Badge, { color: "orange", type: "accent", size: "sm", children: link.badge })), link.external && _jsx("span", { className: "sr-only", children: "(opens in a new tab)" })] }));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A native select is used deliberately. It keeps the control usable at 320px,
|
|
26
|
+
* gives the platform picker on touch devices, and follows the semantic
|
|
27
|
+
* `--color-surface` role so a consumer theme reaches it.
|
|
28
|
+
*/
|
|
29
|
+
function LocaleSelect({ control }) {
|
|
30
|
+
const selectId = useId();
|
|
31
|
+
const hintId = useId();
|
|
32
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 max-w-[240px] flex-col gap-1.5", children: [_jsx("label", { htmlFor: selectId, className: "text-sm font-medium break-words text-grey-700", children: control.label }), _jsxs("div", { className: "relative w-full min-w-0", children: [_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) => (_jsx("option", { value: option.value, children: option.label }, option.value))) }), _jsx(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 && (_jsx("p", { id: hintId, className: "text-xs break-words text-grey-500", children: control.hint }))] }));
|
|
33
|
+
}
|
|
34
|
+
/* ─── Block ────────────────────────────────────────────────────────────── */
|
|
35
|
+
/**
|
|
36
|
+
* Site Footer — browse grouped site links, legal information, social
|
|
37
|
+
* destinations, and locale controls.
|
|
38
|
+
*
|
|
39
|
+
* Only a default state is meaningful: the footer presents destinations the
|
|
40
|
+
* consuming application already knows. There is nothing to load, submit, or
|
|
41
|
+
* confirm, so no loading, error, or success state is offered. Every section is
|
|
42
|
+
* optional; omitting one removes it rather than rendering an empty shell. A
|
|
43
|
+
* locale control requires an `onChange` handler, so the footer can never render
|
|
44
|
+
* a switcher that does nothing.
|
|
45
|
+
*/
|
|
46
|
+
export const SiteFooterBlock = forwardRef(({ label = "Site footer", brand, groups, groupHeadingLevel = "h2", social, socialLabel = "Social media", copyright, legalLinks, legalLabel = "Legal", localeControls, children, className, ...rest }, ref) => {
|
|
47
|
+
const groupIdPrefix = useId();
|
|
48
|
+
const GroupHeading = groupHeadingLevel;
|
|
49
|
+
const groupList = groups ?? [];
|
|
50
|
+
const socialList = social ?? [];
|
|
51
|
+
const localeList = localeControls ?? [];
|
|
52
|
+
const legalList = legalLinks ?? [];
|
|
53
|
+
const hasTop = Boolean(brand) || groupList.length > 0;
|
|
54
|
+
const hasBottom = Boolean(copyright) || legalList.length > 0 || socialList.length > 0 || localeList.length > 0;
|
|
55
|
+
const brandBody = brand && (_jsx(_Fragment, { children: brand.logo ??
|
|
56
|
+
(brand.name && (_jsx("span", { className: "text-body-lg font-semibold break-words text-grey-900", children: brand.name }))) }));
|
|
57
|
+
return (_jsx("footer", { ref: ref, "aria-label": label, className: cn("@container relative w-full border-t border-surface-border-strong bg-surface-muted", className), ...rest, children: _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 && (_jsxs("div", { className: "flex min-w-0 flex-col gap-10 @min-[1136px]:flex-row @min-[1136px]:gap-16", children: [brand && (_jsxs("div", { className: "flex min-w-0 flex-col gap-3 @min-[1136px]:max-w-[300px] @min-[1136px]:shrink-0", children: [brand.href ? (_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 })) : (_jsx("div", { className: "inline-flex max-w-full items-center gap-2", children: brandBody })), brand.description && (_jsx("p", { className: "max-w-[46ch] text-sm break-words text-grey-600", children: brand.description }))] })), groupList.length > 0 && (_jsx("div", { className: 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) => {
|
|
58
|
+
const headingId = `${groupIdPrefix}-${group.id}`;
|
|
59
|
+
return (_jsxs("nav", { "aria-labelledby": headingId, className: "flex min-w-0 flex-col gap-3", children: [_jsx(GroupHeading, { id: headingId, className: "text-sm font-semibold break-words text-grey-900", children: group.title }), _jsx("ul", { className: "m-0 flex list-none flex-col gap-2.5 p-0", children: group.links.map((link) => (_jsx("li", { className: "min-w-0", children: _jsx(FooterLinkAnchor, { link: link }) }, link.id))) })] }, group.id));
|
|
60
|
+
}) }))] })), localeList.length > 0 && (_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) => (_jsx(LocaleSelect, { control: control }, control.id))) })), children && _jsx("div", { className: "min-w-0", children: children }), hasBottom && (_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: [_jsxs("div", { className: "flex min-w-0 flex-col gap-3", children: [copyright && (_jsx("p", { className: "text-sm break-words text-grey-600", children: copyright })), legalList.length > 0 && (_jsx("nav", { "aria-label": legalLabel, className: "min-w-0", children: _jsx("ul", { className: "m-0 flex list-none flex-wrap gap-x-5 gap-y-2 p-0", children: legalList.map((link) => (_jsx("li", { className: "min-w-0", children: _jsx(FooterLinkAnchor, { link: link }) }, link.id))) }) }))] }), socialList.length > 0 && (_jsx("nav", { "aria-label": socialLabel, className: "min-w-0", children: _jsx("ul", { className: "m-0 flex list-none flex-wrap items-center gap-2 p-0", children: socialList.map((entry) => {
|
|
61
|
+
const isExternal = entry.external !== false;
|
|
62
|
+
const visual = entry.artwork ?? (entry.icon && _jsx(Icon, { name: entry.icon, size: "md", "aria-hidden": "true" }));
|
|
63
|
+
return (_jsx("li", { className: "min-w-0", children: _jsx("a", { href: entry.href, "aria-label": visual ? entry.label : undefined, className: 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 ?? _jsx("span", { className: "min-w-0", children: entry.label }) }) }, entry.id));
|
|
64
|
+
}) }) }))] }))] }) }));
|
|
65
|
+
});
|
|
66
|
+
SiteFooterBlock.displayName = "SiteFooterBlock";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TableBlock = TableBlock;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const cn_1 = require("../utils/cn.cjs");
|
|
6
|
+
const Table_1 = require("../components/Table/index.cjs");
|
|
7
|
+
const useTableSelection_1 = require("../components/Table/useTableSelection.cjs");
|
|
8
|
+
const FormControl_1 = require("../components/FormControl/index.cjs");
|
|
9
|
+
const Avatar_1 = require("../components/Avatar/index.cjs");
|
|
10
|
+
const Badge_1 = require("../components/Badge/index.cjs");
|
|
11
|
+
const Pagination_1 = require("../components/Pagination/index.cjs");
|
|
12
|
+
// ─── Component ───────────────────────────────────────────────────────
|
|
13
|
+
function TableBlock({ rows, selectedIds, onSelectionChange, onRowAction, page, totalPages, onPageChange, className, }) {
|
|
14
|
+
const rowIds = rows.map((row) => row.id);
|
|
15
|
+
const selection = (0, useTableSelection_1.useTableSelection)({ dataIds: rowIds, selectedIds, onSelectionChange });
|
|
16
|
+
// Every visible control needs somewhere to go. Pagination without a page handler is a
|
|
17
|
+
// row of buttons that silently do nothing, so it is omitted rather than rendered dead.
|
|
18
|
+
const showPagination = page != null && totalPages != null && onPageChange != null;
|
|
19
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, cn_1.cn)("flex flex-col", className), children: [(0, jsx_runtime_1.jsxs)(Table_1.Table, { children: [(0, jsx_runtime_1.jsx)(Table_1.TableHeader, { children: (0, jsx_runtime_1.jsxs)(Table_1.TableRow, { children: [(0, jsx_runtime_1.jsx)(Table_1.TableHead, { className: "w-[287px]", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { checked: selection.allSelected, indeterminate: selection.someSelected, onChange: selection.toggleAll, "aria-label": "Select all rows" }), (0, jsx_runtime_1.jsx)("span", { children: "Name" })] }) }), (0, jsx_runtime_1.jsx)(Table_1.TableHead, { children: "Amount" }), (0, jsx_runtime_1.jsx)(Table_1.TableHead, { children: "Payment Type" }), (0, jsx_runtime_1.jsx)(Table_1.TableHead, { children: "Date" }), (0, jsx_runtime_1.jsx)(Table_1.TableHead, { children: "Status" }), onRowAction && ((0, jsx_runtime_1.jsx)(Table_1.TableHead, { className: "w-[67px]", children: (0, jsx_runtime_1.jsx)("span", { className: "sr-only", children: "Actions" }) }))] }) }), (0, jsx_runtime_1.jsx)(Table_1.TableBody, { children: rows.map((row) => ((0, jsx_runtime_1.jsxs)(Table_1.TableRow, { selected: selection.isSelected(row.id), children: [(0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { checked: selection.isSelected(row.id), onChange: () => selection.toggleRow(row.id), "aria-label": `Select ${row.name}` }), (0, jsx_runtime_1.jsx)(Avatar_1.Avatar, { type: "initials", initials: row.initials, size: "sm" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm font-medium text-grey-900", children: row.name }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs text-grey-500 truncate max-w-[150px]", children: row.email })] })] }) }), (0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)("span", { className: "text-sm text-grey-700", children: row.amount }) }), (0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)(Badge_1.Badge, { color: "orange", type: "accent", size: "sm", children: row.paymentType }) }), (0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-grey-700", children: [row.date, (0, jsx_runtime_1.jsx)("span", { className: "text-grey-500 mx-2", children: "|" }), row.time] }) }), (0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)(Badge_1.Badge, { color: row.statusColor ?? "orange", type: "accent", size: "sm", children: row.status }) }), onRowAction && ((0, jsx_runtime_1.jsx)(Table_1.TableCell, { children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => onRowAction(row.id), "aria-label": `Actions for ${row.name}`, 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" })] }) }) }))] }, row.id))) })] }), showPagination && ((0, jsx_runtime_1.jsx)(Pagination_1.Pagination, { totalPages: totalPages, currentPage: page, onPageChange: onPageChange }))] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../utils/cn.js";
|
|
3
|
+
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../components/Table/index.js";
|
|
4
|
+
import { useTableSelection } from "../components/Table/useTableSelection.js";
|
|
5
|
+
import { Checkbox } from "../components/FormControl/index.js";
|
|
6
|
+
import { Avatar } from "../components/Avatar/index.js";
|
|
7
|
+
import { Badge } from "../components/Badge/index.js";
|
|
8
|
+
import { Pagination } from "../components/Pagination/index.js";
|
|
9
|
+
// ─── Component ───────────────────────────────────────────────────────
|
|
10
|
+
export function TableBlock({ rows, selectedIds, onSelectionChange, onRowAction, page, totalPages, onPageChange, className, }) {
|
|
11
|
+
const rowIds = rows.map((row) => row.id);
|
|
12
|
+
const selection = useTableSelection({ dataIds: rowIds, selectedIds, onSelectionChange });
|
|
13
|
+
// Every visible control needs somewhere to go. Pagination without a page handler is a
|
|
14
|
+
// row of buttons that silently do nothing, so it is omitted rather than rendered dead.
|
|
15
|
+
const showPagination = page != null && totalPages != null && onPageChange != null;
|
|
16
|
+
return (_jsxs("div", { className: cn("flex flex-col", className), children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-[287px]", children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Checkbox, { checked: selection.allSelected, indeterminate: selection.someSelected, onChange: selection.toggleAll, "aria-label": "Select all rows" }), _jsx("span", { children: "Name" })] }) }), _jsx(TableHead, { children: "Amount" }), _jsx(TableHead, { children: "Payment Type" }), _jsx(TableHead, { children: "Date" }), _jsx(TableHead, { children: "Status" }), onRowAction && (_jsx(TableHead, { className: "w-[67px]", children: _jsx("span", { className: "sr-only", children: "Actions" }) }))] }) }), _jsx(TableBody, { children: rows.map((row) => (_jsxs(TableRow, { selected: selection.isSelected(row.id), children: [_jsx(TableCell, { children: _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Checkbox, { checked: selection.isSelected(row.id), onChange: () => selection.toggleRow(row.id), "aria-label": `Select ${row.name}` }), _jsx(Avatar, { type: "initials", initials: row.initials, size: "sm" }), _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm font-medium text-grey-900", children: row.name }), _jsx("span", { className: "text-xs text-grey-500 truncate max-w-[150px]", children: row.email })] })] }) }), _jsx(TableCell, { children: _jsx("span", { className: "text-sm text-grey-700", children: row.amount }) }), _jsx(TableCell, { children: _jsx(Badge, { color: "orange", type: "accent", size: "sm", children: row.paymentType }) }), _jsx(TableCell, { children: _jsxs("span", { className: "text-sm text-grey-700", children: [row.date, _jsx("span", { className: "text-grey-500 mx-2", children: "|" }), row.time] }) }), _jsx(TableCell, { children: _jsx(Badge, { color: row.statusColor ?? "orange", type: "accent", size: "sm", children: row.status }) }), onRowAction && (_jsx(TableCell, { children: _jsx("button", { type: "button", onClick: () => onRowAction(row.id), "aria-label": `Actions for ${row.name}`, 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" })] }) }) }))] }, row.id))) })] }), showPagination && (_jsx(Pagination, { totalPages: totalPages, currentPage: page, onPageChange: onPageChange }))] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskListBlock = TaskListBlock;
|
|
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 Alert_1 = require("../components/Alert/index.cjs");
|
|
8
|
+
const Avatar_1 = require("../components/Avatar/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
|
+
/* ─── Date helpers ───────────────────────────────────────────────────── */
|
|
15
|
+
function startOfLocalDay(date) {
|
|
16
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
17
|
+
}
|
|
18
|
+
/** Parses `YYYY-MM-DD` in local time. `new Date("2026-09-22")` would be UTC. */
|
|
19
|
+
function parseIsoDate(iso) {
|
|
20
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
21
|
+
if (!match)
|
|
22
|
+
return null;
|
|
23
|
+
const [, year, month, day] = match;
|
|
24
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
25
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
26
|
+
}
|
|
27
|
+
function pluralDays(count) {
|
|
28
|
+
return count === 1 ? "1 day" : `${count} days`;
|
|
29
|
+
}
|
|
30
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
31
|
+
function TaskListBlock({ title = "Tasks", description, headingLevel = "h2", tasks, onToggleComplete, onAddTask, onDeleteTask, onAssignTask, assignees, selectedIds, onSelectionChange, onBulkComplete, onBulkDelete, status = "idle", errorMessage, onRetry, addStatus = "idle", addErrorMessage, canEdit = true, readOnlyMessage = "You can follow this list but not change it. Ask the list owner for edit access.", today, locale = "en-GB", formatDueDate, emptyTitle = "No tasks yet", emptyDescription = "Add the first task to start tracking this work.", noMatchesTitle = "No tasks match these filters", noMatchesDescription = "Nothing here matches your search and filter choices. The list still has tasks.", footer, className, }) {
|
|
32
|
+
const uid = (0, react_1.useId)();
|
|
33
|
+
const titleId = `${uid}-title`;
|
|
34
|
+
const statusGroupName = `${uid}-status-filter`;
|
|
35
|
+
const newTitleId = `${uid}-new-title`;
|
|
36
|
+
const addErrorId = `${uid}-add-error`;
|
|
37
|
+
const [statusFilter, setStatusFilter] = (0, react_1.useState)("all");
|
|
38
|
+
const [assigneeFilter, setAssigneeFilter] = (0, react_1.useState)("all");
|
|
39
|
+
const [query, setQuery] = (0, react_1.useState)("");
|
|
40
|
+
const [newTitle, setNewTitle] = (0, react_1.useState)("");
|
|
41
|
+
const [newAssigneeId, setNewAssigneeId] = (0, react_1.useState)("");
|
|
42
|
+
const [newDueDate, setNewDueDate] = (0, react_1.useState)("");
|
|
43
|
+
const [addTouched, setAddTouched] = (0, react_1.useState)(false);
|
|
44
|
+
const newTitleRef = (0, react_1.useRef)(null);
|
|
45
|
+
const loading = status === "loading";
|
|
46
|
+
const failed = status === "error";
|
|
47
|
+
const adding = addStatus === "submitting";
|
|
48
|
+
const editable = canEdit && !loading && !failed;
|
|
49
|
+
const referenceDay = (0, react_1.useMemo)(() => startOfLocalDay(today ?? new Date()), [today]);
|
|
50
|
+
const dateFormatter = (0, react_1.useMemo)(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" }), [locale]);
|
|
51
|
+
const assigneeById = (0, react_1.useMemo)(() => {
|
|
52
|
+
const map = new Map();
|
|
53
|
+
assignees?.forEach((person) => map.set(person.id, person));
|
|
54
|
+
return map;
|
|
55
|
+
}, [assignees]);
|
|
56
|
+
/* — Filtering ——————————————————————————————————————————— */
|
|
57
|
+
const normalisedQuery = query.trim().toLowerCase();
|
|
58
|
+
// Search + assignee first, so the status counts predict what each status
|
|
59
|
+
// choice will actually show.
|
|
60
|
+
const scoped = (0, react_1.useMemo)(() => tasks.filter((task) => {
|
|
61
|
+
if (assigneeFilter === "unassigned" && task.assigneeId)
|
|
62
|
+
return false;
|
|
63
|
+
if (assigneeFilter !== "all" &&
|
|
64
|
+
assigneeFilter !== "unassigned" &&
|
|
65
|
+
task.assigneeId !== assigneeFilter)
|
|
66
|
+
return false;
|
|
67
|
+
if (!normalisedQuery)
|
|
68
|
+
return true;
|
|
69
|
+
return (task.title.toLowerCase().includes(normalisedQuery) ||
|
|
70
|
+
(task.note?.toLowerCase().includes(normalisedQuery) ?? false));
|
|
71
|
+
}), [tasks, assigneeFilter, normalisedQuery]);
|
|
72
|
+
const counts = (0, react_1.useMemo)(() => ({
|
|
73
|
+
all: scoped.length,
|
|
74
|
+
active: scoped.filter((task) => !task.completed).length,
|
|
75
|
+
completed: scoped.filter((task) => task.completed).length,
|
|
76
|
+
}), [scoped]);
|
|
77
|
+
const visible = (0, react_1.useMemo)(() => {
|
|
78
|
+
if (statusFilter === "active")
|
|
79
|
+
return scoped.filter((task) => !task.completed);
|
|
80
|
+
if (statusFilter === "completed")
|
|
81
|
+
return scoped.filter((task) => task.completed);
|
|
82
|
+
return scoped;
|
|
83
|
+
}, [scoped, statusFilter]);
|
|
84
|
+
const filtersActive = statusFilter !== "all" || assigneeFilter !== "all" || normalisedQuery.length > 0;
|
|
85
|
+
const clearFilters = (0, react_1.useCallback)(() => {
|
|
86
|
+
setStatusFilter("all");
|
|
87
|
+
setAssigneeFilter("all");
|
|
88
|
+
setQuery("");
|
|
89
|
+
}, []);
|
|
90
|
+
/* — Selection ——————————————————————————————————————————— */
|
|
91
|
+
const selectionEnabled = Boolean(onSelectionChange) && editable;
|
|
92
|
+
const selectedSet = (0, react_1.useMemo)(() => new Set(selectedIds ?? []), [selectedIds]);
|
|
93
|
+
const visibleIds = (0, react_1.useMemo)(() => visible.map((task) => task.id), [visible]);
|
|
94
|
+
// Membership against the *currently visible* ids. Comparing set sizes would
|
|
95
|
+
// keep the header checked after the rows behind it were replaced.
|
|
96
|
+
const selectedVisibleIds = (0, react_1.useMemo)(() => visibleIds.filter((id) => selectedSet.has(id)), [visibleIds, selectedSet]);
|
|
97
|
+
const allVisibleSelected = visibleIds.length > 0 && selectedVisibleIds.length === visibleIds.length;
|
|
98
|
+
const someVisibleSelected = selectedVisibleIds.length > 0 && !allVisibleSelected;
|
|
99
|
+
const toggleSelectAll = (0, react_1.useCallback)(() => {
|
|
100
|
+
if (!onSelectionChange)
|
|
101
|
+
return;
|
|
102
|
+
const current = selectedIds ?? [];
|
|
103
|
+
if (allVisibleSelected) {
|
|
104
|
+
// Only drop the visible rows; selection made under other filters stays.
|
|
105
|
+
const visibleSet = new Set(visibleIds);
|
|
106
|
+
onSelectionChange(current.filter((id) => !visibleSet.has(id)));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
onSelectionChange(Array.from(new Set([...current, ...visibleIds])));
|
|
110
|
+
}, [allVisibleSelected, onSelectionChange, selectedIds, visibleIds]);
|
|
111
|
+
const toggleOne = (0, react_1.useCallback)((id, selected) => {
|
|
112
|
+
if (!onSelectionChange)
|
|
113
|
+
return;
|
|
114
|
+
const current = selectedIds ?? [];
|
|
115
|
+
onSelectionChange(selected ? Array.from(new Set([...current, id])) : current.filter((x) => x !== id));
|
|
116
|
+
}, [onSelectionChange, selectedIds]);
|
|
117
|
+
/* — Due-date context ———————————————————————————————————— */
|
|
118
|
+
const describeDue = (0, react_1.useCallback)((task) => {
|
|
119
|
+
if (!task.dueDate)
|
|
120
|
+
return null;
|
|
121
|
+
const date = parseIsoDate(task.dueDate);
|
|
122
|
+
if (!date)
|
|
123
|
+
return null;
|
|
124
|
+
const daysFromToday = Math.round((startOfLocalDay(date).getTime() - referenceDay.getTime()) / 86400000);
|
|
125
|
+
const overdue = !task.completed && daysFromToday < 0;
|
|
126
|
+
if (formatDueDate) {
|
|
127
|
+
return {
|
|
128
|
+
text: formatDueDate(task.dueDate, {
|
|
129
|
+
daysFromToday,
|
|
130
|
+
date,
|
|
131
|
+
completed: task.completed,
|
|
132
|
+
}),
|
|
133
|
+
overdue,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (task.completed)
|
|
137
|
+
return { text: `Due ${dateFormatter.format(date)}`, overdue: false };
|
|
138
|
+
if (daysFromToday < 0)
|
|
139
|
+
return { text: `Overdue by ${pluralDays(Math.abs(daysFromToday))}`, overdue: true };
|
|
140
|
+
if (daysFromToday === 0)
|
|
141
|
+
return { text: "Due today", overdue: false };
|
|
142
|
+
if (daysFromToday === 1)
|
|
143
|
+
return { text: "Due tomorrow", overdue: false };
|
|
144
|
+
if (daysFromToday <= 7)
|
|
145
|
+
return { text: `Due in ${pluralDays(daysFromToday)}`, overdue: false };
|
|
146
|
+
return { text: `Due ${dateFormatter.format(date)}`, overdue: false };
|
|
147
|
+
}, [dateFormatter, formatDueDate, referenceDay]);
|
|
148
|
+
/* — Add ————————————————————————————————————————————————— */
|
|
149
|
+
const trimmedNewTitle = newTitle.trim();
|
|
150
|
+
const addError = addTouched && !trimmedNewTitle ? "Give the task a name." : undefined;
|
|
151
|
+
const handleAdd = (0, react_1.useCallback)((event) => {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
// Guard duplicate submission while an add is in flight.
|
|
154
|
+
if (!onAddTask || adding || !editable)
|
|
155
|
+
return;
|
|
156
|
+
setAddTouched(true);
|
|
157
|
+
if (!trimmedNewTitle) {
|
|
158
|
+
newTitleRef.current?.focus();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
onAddTask({
|
|
162
|
+
title: trimmedNewTitle,
|
|
163
|
+
assigneeId: newAssigneeId || undefined,
|
|
164
|
+
dueDate: newDueDate || undefined,
|
|
165
|
+
});
|
|
166
|
+
setNewTitle("");
|
|
167
|
+
setNewDueDate("");
|
|
168
|
+
setAddTouched(false);
|
|
169
|
+
newTitleRef.current?.focus();
|
|
170
|
+
}, [adding, editable, newAssigneeId, newDueDate, onAddTask, trimmedNewTitle]);
|
|
171
|
+
const Heading = headingLevel;
|
|
172
|
+
const statusOptions = [
|
|
173
|
+
{ id: "all", label: "All", count: counts.all },
|
|
174
|
+
{ id: "active", label: "To do", count: counts.active },
|
|
175
|
+
{ id: "completed", label: "Done", count: counts.completed },
|
|
176
|
+
];
|
|
177
|
+
return ((0, jsx_runtime_1.jsx)("section", { "aria-labelledby": titleId, className: (0, cn_1.cn)("@container w-full", className), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-4 rounded-12 border border-surface-border bg-surface p-4 @min-[520px]:p-6", children: [(0, jsx_runtime_1.jsxs)("header", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(Heading, { id: titleId, className: "text-h6 font-semibold text-grey-900", children: title }), description && (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm text-grey-600", children: description })] }), !canEdit && ((0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "information", icon: "lock", role: "status", title: "View only", description: readOnlyMessage, className: "bg-surface dark:bg-surface" })), onAddTask && editable && ((0, jsx_runtime_1.jsxs)("form", { onSubmit: handleAdd, noValidate: true, className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3 @min-[760px]:flex-row @min-[760px]:items-end", children: [(0, jsx_runtime_1.jsx)("div", { className: "min-w-0 flex-1", children: (0, jsx_runtime_1.jsx)(Input_1.Input, { id: newTitleId, ref: newTitleRef, name: "newTask", size: "md", label: "New task", placeholder: "What needs doing?", value: newTitle, disabled: adding, error: addError, onChange: (event) => setNewTitle(event.target.value) }) }), assignees && assignees.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-1 @min-[760px]:w-48", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${uid}-new-assignee`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Assign to" }), (0, jsx_runtime_1.jsxs)("select", { id: `${uid}-new-assignee`, value: newAssigneeId, disabled: adding, onChange: (event) => setNewAssigneeId(event.target.value), className: "h-12 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text disabled:cursor-not-allowed disabled:bg-grey-50", children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Nobody yet" }), assignees.map((person) => ((0, jsx_runtime_1.jsx)("option", { value: person.id, children: person.name }, person.id)))] })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-1 @min-[760px]:w-44", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${uid}-new-due`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Due date" }), (0, jsx_runtime_1.jsx)("input", { id: `${uid}-new-due`, type: "date", value: newDueDate, disabled: adding, onChange: (event) => setNewDueDate(event.target.value), className: "h-12 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text disabled:cursor-not-allowed disabled:bg-grey-50" })] }), (0, jsx_runtime_1.jsx)(Button_1.Button, { type: "submit", variant: "primary", size: "lg", disabled: adding, className: "@min-[760px]:h-12 @min-[760px]:shrink-0 @min-[760px]:py-0", children: adding ? "Adding…" : "Add task" })] }), addStatus === "error" && addErrorMessage && ((0, jsx_runtime_1.jsx)("p", { id: addErrorId, role: "alert", className: "text-body-sm text-feedback-error", children: addErrorMessage }))] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3 border-t border-surface-border pt-4 @min-[640px]:flex-row @min-[640px]:flex-wrap @min-[640px]:items-end", children: [(0, jsx_runtime_1.jsxs)("fieldset", { className: "min-w-0 border-0 p-0", children: [(0, jsx_runtime_1.jsx)("legend", { className: "mb-1 text-sm font-medium leading-[1.45] text-grey-900", children: "Filter by status" }), (0, jsx_runtime_1.jsx)("div", { className: "inline-flex flex-wrap gap-1 rounded-lg border border-surface-border-strong p-1", children: statusOptions.map((option) => ((0, jsx_runtime_1.jsxs)("label", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: "radio", name: statusGroupName, value: option.id, checked: statusFilter === option.id, onChange: () => setStatusFilter(option.id), className: "peer sr-only" }), (0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("inline-flex cursor-pointer items-center gap-1 rounded px-3 py-1.5 text-body-sm font-medium text-grey-700", "peer-checked:bg-primary-50 peer-checked:text-action-primary-text", "peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-action-primary-text"), children: [option.label, (0, jsx_runtime_1.jsxs)("span", { className: "text-body-xs text-grey-600", children: ["(", option.count, ")"] })] })] }, option.id))) })] }), assignees && assignees.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-1 @min-[640px]:w-48", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: `${uid}-assignee-filter`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Filter by assignee" }), (0, jsx_runtime_1.jsxs)("select", { id: `${uid}-assignee-filter`, value: assigneeFilter, onChange: (event) => setAssigneeFilter(event.target.value), className: "h-10 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [(0, jsx_runtime_1.jsx)("option", { value: "all", children: "Everyone" }), (0, jsx_runtime_1.jsx)("option", { value: "unassigned", children: "Unassigned" }), assignees.map((person) => ((0, jsx_runtime_1.jsx)("option", { value: person.id, children: person.name }, person.id)))] })] })), (0, jsx_runtime_1.jsx)("div", { className: "min-w-0 flex-1 @min-[640px]:max-w-64", children: (0, jsx_runtime_1.jsx)(Input_1.Input, { id: `${uid}-search`, type: "search", size: "sm", label: "Search tasks", placeholder: "Search by name", leadingIcon: "search", value: query, onChange: (event) => setQuery(event.target.value) }) })] }), (0, jsx_runtime_1.jsx)("output", { className: "text-body-sm text-grey-600", children: loading
|
|
178
|
+
? `Loading tasks…`
|
|
179
|
+
: `Showing ${visible.length} of ${tasks.length} ${tasks.length === 1 ? "task" : "tasks"}.` }), selectionEnabled && visible.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3 rounded-8 border border-surface-border bg-surface-muted p-3 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:justify-between", children: [(0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { id: `${uid}-select-all`, checked: allVisibleSelected, indeterminate: someVisibleSelected, onChange: toggleSelectAll, "aria-label": `Select all ${visible.length} shown ${visible.length === 1 ? "task" : "tasks"}` }), (0, jsx_runtime_1.jsx)("span", { className: "text-body-sm text-grey-700", children: selectedVisibleIds.length > 0
|
|
180
|
+
? `${selectedVisibleIds.length} of ${visible.length} shown selected`
|
|
181
|
+
: `Select all ${visible.length} shown` })] }), selectedVisibleIds.length > 0 && (onBulkComplete || onBulkDelete) && ((0, jsx_runtime_1.jsxs)("span", { className: "flex flex-wrap gap-2", children: [onBulkComplete && ((0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", size: "sm", variant: "secondary", onClick: () => onBulkComplete(selectedVisibleIds), children: "Mark as done" })), onBulkDelete && ((0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", size: "sm", variant: "destructive", appearance: "outlined", onClick: () => onBulkDelete(selectedVisibleIds), children: "Delete" }))] }))] })), failed ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-3", children: [(0, jsx_runtime_1.jsx)(Alert_1.Alert, { state: "error", icon: "info-triangle", role: "alert", title: "These tasks could not be loaded", description: errorMessage ?? "The task service did not respond. Nothing was changed.", className: "bg-surface dark:bg-surface" }), onRetry && ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", variant: "secondary", size: "sm", onClick: onRetry, children: "Try again" }) }))] })) : loading ? ((0, jsx_runtime_1.jsx)("div", { "aria-busy": "true", className: "flex min-h-32 items-center justify-center rounded-8 border border-dashed border-surface-border-strong p-6", children: (0, jsx_runtime_1.jsx)(Spinner_1.Spinner, { size: "md", label: "Loading tasks\u2026", labelPosition: "below" }) })) : tasks.length === 0 ? (
|
|
182
|
+
/* No tasks exist at all. */
|
|
183
|
+
(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-2 rounded-8 border border-dashed border-surface-border-strong p-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-body-md font-semibold text-grey-900", children: emptyTitle }), (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm text-grey-600", children: emptyDescription }), onAddTask && editable && ((0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", size: "sm", variant: "secondary", onClick: () => newTitleRef.current?.focus(), children: "Add the first task" }))] })) : visible.length === 0 ? (
|
|
184
|
+
/* Tasks exist, but this filter combination matched none of them. */
|
|
185
|
+
(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col items-start gap-2 rounded-8 border border-dashed border-surface-border-strong p-6", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-body-md font-semibold text-grey-900", children: noMatchesTitle }), (0, jsx_runtime_1.jsx)("p", { className: "text-body-sm text-grey-600", children: noMatchesDescription }), filtersActive && ((0, jsx_runtime_1.jsx)(Button_1.Button, { type: "button", size: "sm", variant: "secondary", onClick: clearFilters, children: "Clear filters" }))] })) : ((0, jsx_runtime_1.jsx)("ul", { role: "list", className: "flex list-none flex-col gap-2 p-0", children: visible.map((task) => {
|
|
186
|
+
const checkboxId = `${uid}-task-${task.id}`;
|
|
187
|
+
const titleLabelId = `${checkboxId}-label`;
|
|
188
|
+
const due = describeDue(task);
|
|
189
|
+
const person = task.assigneeId ? assigneeById.get(task.assigneeId) : undefined;
|
|
190
|
+
const selected = selectedSet.has(task.id);
|
|
191
|
+
return ((0, jsx_runtime_1.jsxs)("li", { className: (0, cn_1.cn)("relative flex flex-col gap-3 rounded-8 border p-3 @min-[560px]:flex-row @min-[560px]:items-start", selected
|
|
192
|
+
? "border-primary-200 bg-primary-50"
|
|
193
|
+
: "border-surface-border bg-surface"), children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 items-start gap-3", children: [selectionEnabled && (
|
|
194
|
+
// A gutter keeps the row-selection control visually
|
|
195
|
+
// distinct from the completion control beside it.
|
|
196
|
+
(0, jsx_runtime_1.jsx)("span", { className: "flex self-stretch border-r border-surface-border pt-0.5 pr-3", children: (0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { checked: selected, onChange: (event) => toggleOne(task.id, event.target.checked), "aria-label": `Select ${task.title}` }) })), (0, jsx_runtime_1.jsx)("span", { className: "pt-0.5", children: (0, jsx_runtime_1.jsx)(FormControl_1.Checkbox, { id: checkboxId, checked: task.completed, disabled: !canEdit, "aria-labelledby": titleLabelId, onChange: (event) => onToggleComplete(task.id, event.target.checked) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-col gap-1", children: [(0, jsx_runtime_1.jsx)("label", { id: titleLabelId, htmlFor: checkboxId, className: (0, cn_1.cn)("text-body-sm font-medium break-words", canEdit && "cursor-pointer", task.completed ? "text-grey-600 line-through" : "text-grey-900"), children: task.title }), task.note && ((0, jsx_runtime_1.jsx)("p", { className: "text-body-xs break-words text-grey-600", children: task.note })), due && ((0, jsx_runtime_1.jsxs)("span", { className: (0, cn_1.cn)("inline-flex w-fit items-center gap-1 rounded px-1.5 py-0.5 text-body-xs font-medium", due.overdue ? "bg-error-50 text-error-700" : "text-grey-600"), children: [(0, jsx_runtime_1.jsx)(Icon_1.Icon, { name: due.overdue ? "info-triangle" : "calendar", size: "xs" }), due.text] }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex shrink-0 flex-wrap items-center gap-2 @min-[560px]:justify-end", children: [onAssignTask && canEdit && assignees && assignees.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: `${checkboxId}-assignee`, className: "sr-only", children: ["Assignee for ", task.title] }), (0, jsx_runtime_1.jsxs)("select", { id: `${checkboxId}-assignee`, value: task.assigneeId ?? "", onChange: (event) => onAssignTask(task.id, event.target.value), className: "h-9 max-w-40 rounded-lg border border-grey-300 bg-surface px-2 text-body-xs text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Unassigned" }), assignees.map((candidate) => ((0, jsx_runtime_1.jsx)("option", { value: candidate.id, children: candidate.name }, candidate.id)))] })] })) : ((0, jsx_runtime_1.jsxs)("span", { className: "flex min-w-0 items-center gap-2", children: [(0, jsx_runtime_1.jsx)(Avatar_1.Avatar, { size: "xs", type: person?.avatarSrc ? "image" : person ? "initials" : "icon", src: person?.avatarSrc, alt: person?.name ?? "", icon: "user", initials: person?.initials ?? person?.name.slice(0, 2).toUpperCase() ?? "?" }), (0, jsx_runtime_1.jsx)("span", { className: "truncate text-body-xs text-grey-600", children: person?.name ?? "Unassigned" })] })), onDeleteTask && canEdit && ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => onDeleteTask(task.id), className: "inline-flex size-9 cursor-pointer items-center justify-center rounded-lg border border-grey-300 text-grey-600 hover:bg-grey-100 hover:text-action-danger-text 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: "bin", size: "md" }), (0, jsx_runtime_1.jsxs)("span", { className: "sr-only", children: ["Delete ", task.title] })] }))] })] }, task.id));
|
|
197
|
+
}) })), footer && (0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap items-center gap-3", children: footer })] }) }));
|
|
198
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, 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 { Checkbox } from "../components/FormControl/index.js";
|
|
8
|
+
import { Icon } from "../components/Icon/index.js";
|
|
9
|
+
import { Input } from "../components/Input/index.js";
|
|
10
|
+
import { Spinner } from "../components/Spinner/index.js";
|
|
11
|
+
/* ─── Date helpers ───────────────────────────────────────────────────── */
|
|
12
|
+
function startOfLocalDay(date) {
|
|
13
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
14
|
+
}
|
|
15
|
+
/** Parses `YYYY-MM-DD` in local time. `new Date("2026-09-22")` would be UTC. */
|
|
16
|
+
function parseIsoDate(iso) {
|
|
17
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
18
|
+
if (!match)
|
|
19
|
+
return null;
|
|
20
|
+
const [, year, month, day] = match;
|
|
21
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
22
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
23
|
+
}
|
|
24
|
+
function pluralDays(count) {
|
|
25
|
+
return count === 1 ? "1 day" : `${count} days`;
|
|
26
|
+
}
|
|
27
|
+
/* ─── Component ──────────────────────────────────────────────────────── */
|
|
28
|
+
export function TaskListBlock({ title = "Tasks", description, headingLevel = "h2", tasks, onToggleComplete, onAddTask, onDeleteTask, onAssignTask, assignees, selectedIds, onSelectionChange, onBulkComplete, onBulkDelete, status = "idle", errorMessage, onRetry, addStatus = "idle", addErrorMessage, canEdit = true, readOnlyMessage = "You can follow this list but not change it. Ask the list owner for edit access.", today, locale = "en-GB", formatDueDate, emptyTitle = "No tasks yet", emptyDescription = "Add the first task to start tracking this work.", noMatchesTitle = "No tasks match these filters", noMatchesDescription = "Nothing here matches your search and filter choices. The list still has tasks.", footer, className, }) {
|
|
29
|
+
const uid = useId();
|
|
30
|
+
const titleId = `${uid}-title`;
|
|
31
|
+
const statusGroupName = `${uid}-status-filter`;
|
|
32
|
+
const newTitleId = `${uid}-new-title`;
|
|
33
|
+
const addErrorId = `${uid}-add-error`;
|
|
34
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
35
|
+
const [assigneeFilter, setAssigneeFilter] = useState("all");
|
|
36
|
+
const [query, setQuery] = useState("");
|
|
37
|
+
const [newTitle, setNewTitle] = useState("");
|
|
38
|
+
const [newAssigneeId, setNewAssigneeId] = useState("");
|
|
39
|
+
const [newDueDate, setNewDueDate] = useState("");
|
|
40
|
+
const [addTouched, setAddTouched] = useState(false);
|
|
41
|
+
const newTitleRef = useRef(null);
|
|
42
|
+
const loading = status === "loading";
|
|
43
|
+
const failed = status === "error";
|
|
44
|
+
const adding = addStatus === "submitting";
|
|
45
|
+
const editable = canEdit && !loading && !failed;
|
|
46
|
+
const referenceDay = useMemo(() => startOfLocalDay(today ?? new Date()), [today]);
|
|
47
|
+
const dateFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" }), [locale]);
|
|
48
|
+
const assigneeById = useMemo(() => {
|
|
49
|
+
const map = new Map();
|
|
50
|
+
assignees?.forEach((person) => map.set(person.id, person));
|
|
51
|
+
return map;
|
|
52
|
+
}, [assignees]);
|
|
53
|
+
/* — Filtering ——————————————————————————————————————————— */
|
|
54
|
+
const normalisedQuery = query.trim().toLowerCase();
|
|
55
|
+
// Search + assignee first, so the status counts predict what each status
|
|
56
|
+
// choice will actually show.
|
|
57
|
+
const scoped = useMemo(() => tasks.filter((task) => {
|
|
58
|
+
if (assigneeFilter === "unassigned" && task.assigneeId)
|
|
59
|
+
return false;
|
|
60
|
+
if (assigneeFilter !== "all" &&
|
|
61
|
+
assigneeFilter !== "unassigned" &&
|
|
62
|
+
task.assigneeId !== assigneeFilter)
|
|
63
|
+
return false;
|
|
64
|
+
if (!normalisedQuery)
|
|
65
|
+
return true;
|
|
66
|
+
return (task.title.toLowerCase().includes(normalisedQuery) ||
|
|
67
|
+
(task.note?.toLowerCase().includes(normalisedQuery) ?? false));
|
|
68
|
+
}), [tasks, assigneeFilter, normalisedQuery]);
|
|
69
|
+
const counts = useMemo(() => ({
|
|
70
|
+
all: scoped.length,
|
|
71
|
+
active: scoped.filter((task) => !task.completed).length,
|
|
72
|
+
completed: scoped.filter((task) => task.completed).length,
|
|
73
|
+
}), [scoped]);
|
|
74
|
+
const visible = useMemo(() => {
|
|
75
|
+
if (statusFilter === "active")
|
|
76
|
+
return scoped.filter((task) => !task.completed);
|
|
77
|
+
if (statusFilter === "completed")
|
|
78
|
+
return scoped.filter((task) => task.completed);
|
|
79
|
+
return scoped;
|
|
80
|
+
}, [scoped, statusFilter]);
|
|
81
|
+
const filtersActive = statusFilter !== "all" || assigneeFilter !== "all" || normalisedQuery.length > 0;
|
|
82
|
+
const clearFilters = useCallback(() => {
|
|
83
|
+
setStatusFilter("all");
|
|
84
|
+
setAssigneeFilter("all");
|
|
85
|
+
setQuery("");
|
|
86
|
+
}, []);
|
|
87
|
+
/* — Selection ——————————————————————————————————————————— */
|
|
88
|
+
const selectionEnabled = Boolean(onSelectionChange) && editable;
|
|
89
|
+
const selectedSet = useMemo(() => new Set(selectedIds ?? []), [selectedIds]);
|
|
90
|
+
const visibleIds = useMemo(() => visible.map((task) => task.id), [visible]);
|
|
91
|
+
// Membership against the *currently visible* ids. Comparing set sizes would
|
|
92
|
+
// keep the header checked after the rows behind it were replaced.
|
|
93
|
+
const selectedVisibleIds = useMemo(() => visibleIds.filter((id) => selectedSet.has(id)), [visibleIds, selectedSet]);
|
|
94
|
+
const allVisibleSelected = visibleIds.length > 0 && selectedVisibleIds.length === visibleIds.length;
|
|
95
|
+
const someVisibleSelected = selectedVisibleIds.length > 0 && !allVisibleSelected;
|
|
96
|
+
const toggleSelectAll = useCallback(() => {
|
|
97
|
+
if (!onSelectionChange)
|
|
98
|
+
return;
|
|
99
|
+
const current = selectedIds ?? [];
|
|
100
|
+
if (allVisibleSelected) {
|
|
101
|
+
// Only drop the visible rows; selection made under other filters stays.
|
|
102
|
+
const visibleSet = new Set(visibleIds);
|
|
103
|
+
onSelectionChange(current.filter((id) => !visibleSet.has(id)));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
onSelectionChange(Array.from(new Set([...current, ...visibleIds])));
|
|
107
|
+
}, [allVisibleSelected, onSelectionChange, selectedIds, visibleIds]);
|
|
108
|
+
const toggleOne = useCallback((id, selected) => {
|
|
109
|
+
if (!onSelectionChange)
|
|
110
|
+
return;
|
|
111
|
+
const current = selectedIds ?? [];
|
|
112
|
+
onSelectionChange(selected ? Array.from(new Set([...current, id])) : current.filter((x) => x !== id));
|
|
113
|
+
}, [onSelectionChange, selectedIds]);
|
|
114
|
+
/* — Due-date context ———————————————————————————————————— */
|
|
115
|
+
const describeDue = useCallback((task) => {
|
|
116
|
+
if (!task.dueDate)
|
|
117
|
+
return null;
|
|
118
|
+
const date = parseIsoDate(task.dueDate);
|
|
119
|
+
if (!date)
|
|
120
|
+
return null;
|
|
121
|
+
const daysFromToday = Math.round((startOfLocalDay(date).getTime() - referenceDay.getTime()) / 86400000);
|
|
122
|
+
const overdue = !task.completed && daysFromToday < 0;
|
|
123
|
+
if (formatDueDate) {
|
|
124
|
+
return {
|
|
125
|
+
text: formatDueDate(task.dueDate, {
|
|
126
|
+
daysFromToday,
|
|
127
|
+
date,
|
|
128
|
+
completed: task.completed,
|
|
129
|
+
}),
|
|
130
|
+
overdue,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (task.completed)
|
|
134
|
+
return { text: `Due ${dateFormatter.format(date)}`, overdue: false };
|
|
135
|
+
if (daysFromToday < 0)
|
|
136
|
+
return { text: `Overdue by ${pluralDays(Math.abs(daysFromToday))}`, overdue: true };
|
|
137
|
+
if (daysFromToday === 0)
|
|
138
|
+
return { text: "Due today", overdue: false };
|
|
139
|
+
if (daysFromToday === 1)
|
|
140
|
+
return { text: "Due tomorrow", overdue: false };
|
|
141
|
+
if (daysFromToday <= 7)
|
|
142
|
+
return { text: `Due in ${pluralDays(daysFromToday)}`, overdue: false };
|
|
143
|
+
return { text: `Due ${dateFormatter.format(date)}`, overdue: false };
|
|
144
|
+
}, [dateFormatter, formatDueDate, referenceDay]);
|
|
145
|
+
/* — Add ————————————————————————————————————————————————— */
|
|
146
|
+
const trimmedNewTitle = newTitle.trim();
|
|
147
|
+
const addError = addTouched && !trimmedNewTitle ? "Give the task a name." : undefined;
|
|
148
|
+
const handleAdd = useCallback((event) => {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
// Guard duplicate submission while an add is in flight.
|
|
151
|
+
if (!onAddTask || adding || !editable)
|
|
152
|
+
return;
|
|
153
|
+
setAddTouched(true);
|
|
154
|
+
if (!trimmedNewTitle) {
|
|
155
|
+
newTitleRef.current?.focus();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
onAddTask({
|
|
159
|
+
title: trimmedNewTitle,
|
|
160
|
+
assigneeId: newAssigneeId || undefined,
|
|
161
|
+
dueDate: newDueDate || undefined,
|
|
162
|
+
});
|
|
163
|
+
setNewTitle("");
|
|
164
|
+
setNewDueDate("");
|
|
165
|
+
setAddTouched(false);
|
|
166
|
+
newTitleRef.current?.focus();
|
|
167
|
+
}, [adding, editable, newAssigneeId, newDueDate, onAddTask, trimmedNewTitle]);
|
|
168
|
+
const Heading = headingLevel;
|
|
169
|
+
const statusOptions = [
|
|
170
|
+
{ id: "all", label: "All", count: counts.all },
|
|
171
|
+
{ id: "active", label: "To do", count: counts.active },
|
|
172
|
+
{ id: "completed", label: "Done", count: counts.completed },
|
|
173
|
+
];
|
|
174
|
+
return (_jsx("section", { "aria-labelledby": titleId, className: cn("@container w-full", className), children: _jsxs("div", { className: "flex flex-col gap-4 rounded-12 border border-surface-border bg-surface p-4 @min-[520px]: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" })), onAddTask && editable && (_jsxs("form", { onSubmit: handleAdd, noValidate: true, className: "flex flex-col gap-3", children: [_jsxs("div", { className: "flex flex-col gap-3 @min-[760px]:flex-row @min-[760px]:items-end", children: [_jsx("div", { className: "min-w-0 flex-1", children: _jsx(Input, { id: newTitleId, ref: newTitleRef, name: "newTask", size: "md", label: "New task", placeholder: "What needs doing?", value: newTitle, disabled: adding, error: addError, onChange: (event) => setNewTitle(event.target.value) }) }), assignees && assignees.length > 0 && (_jsxs("div", { className: "flex min-w-0 flex-col gap-1 @min-[760px]:w-48", children: [_jsx("label", { htmlFor: `${uid}-new-assignee`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Assign to" }), _jsxs("select", { id: `${uid}-new-assignee`, value: newAssigneeId, disabled: adding, onChange: (event) => setNewAssigneeId(event.target.value), className: "h-12 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text disabled:cursor-not-allowed disabled:bg-grey-50", children: [_jsx("option", { value: "", children: "Nobody yet" }), assignees.map((person) => (_jsx("option", { value: person.id, children: person.name }, person.id)))] })] })), _jsxs("div", { className: "flex min-w-0 flex-col gap-1 @min-[760px]:w-44", children: [_jsx("label", { htmlFor: `${uid}-new-due`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Due date" }), _jsx("input", { id: `${uid}-new-due`, type: "date", value: newDueDate, disabled: adding, onChange: (event) => setNewDueDate(event.target.value), className: "h-12 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text disabled:cursor-not-allowed disabled:bg-grey-50" })] }), _jsx(Button, { type: "submit", variant: "primary", size: "lg", disabled: adding, className: "@min-[760px]:h-12 @min-[760px]:shrink-0 @min-[760px]:py-0", children: adding ? "Adding…" : "Add task" })] }), addStatus === "error" && addErrorMessage && (_jsx("p", { id: addErrorId, role: "alert", className: "text-body-sm text-feedback-error", children: addErrorMessage }))] })), _jsxs("div", { className: "flex flex-col gap-3 border-t border-surface-border pt-4 @min-[640px]:flex-row @min-[640px]:flex-wrap @min-[640px]:items-end", children: [_jsxs("fieldset", { className: "min-w-0 border-0 p-0", children: [_jsx("legend", { className: "mb-1 text-sm font-medium leading-[1.45] text-grey-900", children: "Filter by status" }), _jsx("div", { className: "inline-flex flex-wrap gap-1 rounded-lg border border-surface-border-strong p-1", children: statusOptions.map((option) => (_jsxs("label", { className: "relative", children: [_jsx("input", { type: "radio", name: statusGroupName, value: option.id, checked: statusFilter === option.id, onChange: () => setStatusFilter(option.id), className: "peer sr-only" }), _jsxs("span", { className: cn("inline-flex cursor-pointer items-center gap-1 rounded px-3 py-1.5 text-body-sm font-medium text-grey-700", "peer-checked:bg-primary-50 peer-checked:text-action-primary-text", "peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-action-primary-text"), children: [option.label, _jsxs("span", { className: "text-body-xs text-grey-600", children: ["(", option.count, ")"] })] })] }, option.id))) })] }), assignees && assignees.length > 0 && (_jsxs("div", { className: "flex min-w-0 flex-col gap-1 @min-[640px]:w-48", children: [_jsx("label", { htmlFor: `${uid}-assignee-filter`, className: "text-sm font-medium leading-[1.45] text-grey-900", children: "Filter by assignee" }), _jsxs("select", { id: `${uid}-assignee-filter`, value: assigneeFilter, onChange: (event) => setAssigneeFilter(event.target.value), className: "h-10 w-full rounded-lg border border-grey-300 bg-surface px-3 text-sm text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [_jsx("option", { value: "all", children: "Everyone" }), _jsx("option", { value: "unassigned", children: "Unassigned" }), assignees.map((person) => (_jsx("option", { value: person.id, children: person.name }, person.id)))] })] })), _jsx("div", { className: "min-w-0 flex-1 @min-[640px]:max-w-64", children: _jsx(Input, { id: `${uid}-search`, type: "search", size: "sm", label: "Search tasks", placeholder: "Search by name", leadingIcon: "search", value: query, onChange: (event) => setQuery(event.target.value) }) })] }), _jsx("output", { className: "text-body-sm text-grey-600", children: loading
|
|
175
|
+
? `Loading tasks…`
|
|
176
|
+
: `Showing ${visible.length} of ${tasks.length} ${tasks.length === 1 ? "task" : "tasks"}.` }), selectionEnabled && visible.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 rounded-8 border border-surface-border bg-surface-muted p-3 @min-[520px]:flex-row @min-[520px]:items-center @min-[520px]:justify-between", children: [_jsxs("span", { className: "flex items-center gap-3", children: [_jsx(Checkbox, { id: `${uid}-select-all`, checked: allVisibleSelected, indeterminate: someVisibleSelected, onChange: toggleSelectAll, "aria-label": `Select all ${visible.length} shown ${visible.length === 1 ? "task" : "tasks"}` }), _jsx("span", { className: "text-body-sm text-grey-700", children: selectedVisibleIds.length > 0
|
|
177
|
+
? `${selectedVisibleIds.length} of ${visible.length} shown selected`
|
|
178
|
+
: `Select all ${visible.length} shown` })] }), selectedVisibleIds.length > 0 && (onBulkComplete || onBulkDelete) && (_jsxs("span", { className: "flex flex-wrap gap-2", children: [onBulkComplete && (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => onBulkComplete(selectedVisibleIds), children: "Mark as done" })), onBulkDelete && (_jsx(Button, { type: "button", size: "sm", variant: "destructive", appearance: "outlined", onClick: () => onBulkDelete(selectedVisibleIds), children: "Delete" }))] }))] })), failed ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(Alert, { state: "error", icon: "info-triangle", role: "alert", title: "These tasks could not be loaded", description: errorMessage ?? "The task service did not respond. Nothing was changed.", className: "bg-surface dark:bg-surface" }), onRetry && (_jsx("div", { children: _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onRetry, children: "Try again" }) }))] })) : loading ? (_jsx("div", { "aria-busy": "true", className: "flex min-h-32 items-center justify-center rounded-8 border border-dashed border-surface-border-strong p-6", children: _jsx(Spinner, { size: "md", label: "Loading tasks\u2026", labelPosition: "below" }) })) : tasks.length === 0 ? (
|
|
179
|
+
/* No tasks exist at all. */
|
|
180
|
+
_jsxs("div", { className: "flex flex-col items-start gap-2 rounded-8 border border-dashed border-surface-border-strong p-6", children: [_jsx("p", { className: "text-body-md font-semibold text-grey-900", children: emptyTitle }), _jsx("p", { className: "text-body-sm text-grey-600", children: emptyDescription }), onAddTask && editable && (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => newTitleRef.current?.focus(), children: "Add the first task" }))] })) : visible.length === 0 ? (
|
|
181
|
+
/* Tasks exist, but this filter combination matched none of them. */
|
|
182
|
+
_jsxs("div", { className: "flex flex-col items-start gap-2 rounded-8 border border-dashed border-surface-border-strong p-6", children: [_jsx("p", { className: "text-body-md font-semibold text-grey-900", children: noMatchesTitle }), _jsx("p", { className: "text-body-sm text-grey-600", children: noMatchesDescription }), filtersActive && (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: clearFilters, children: "Clear filters" }))] })) : (_jsx("ul", { role: "list", className: "flex list-none flex-col gap-2 p-0", children: visible.map((task) => {
|
|
183
|
+
const checkboxId = `${uid}-task-${task.id}`;
|
|
184
|
+
const titleLabelId = `${checkboxId}-label`;
|
|
185
|
+
const due = describeDue(task);
|
|
186
|
+
const person = task.assigneeId ? assigneeById.get(task.assigneeId) : undefined;
|
|
187
|
+
const selected = selectedSet.has(task.id);
|
|
188
|
+
return (_jsxs("li", { className: cn("relative flex flex-col gap-3 rounded-8 border p-3 @min-[560px]:flex-row @min-[560px]:items-start", selected
|
|
189
|
+
? "border-primary-200 bg-primary-50"
|
|
190
|
+
: "border-surface-border bg-surface"), children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-3", children: [selectionEnabled && (
|
|
191
|
+
// A gutter keeps the row-selection control visually
|
|
192
|
+
// distinct from the completion control beside it.
|
|
193
|
+
_jsx("span", { className: "flex self-stretch border-r border-surface-border pt-0.5 pr-3", children: _jsx(Checkbox, { checked: selected, onChange: (event) => toggleOne(task.id, event.target.checked), "aria-label": `Select ${task.title}` }) })), _jsx("span", { className: "pt-0.5", children: _jsx(Checkbox, { id: checkboxId, checked: task.completed, disabled: !canEdit, "aria-labelledby": titleLabelId, onChange: (event) => onToggleComplete(task.id, event.target.checked) }) }), _jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [_jsx("label", { id: titleLabelId, htmlFor: checkboxId, className: cn("text-body-sm font-medium break-words", canEdit && "cursor-pointer", task.completed ? "text-grey-600 line-through" : "text-grey-900"), children: task.title }), task.note && (_jsx("p", { className: "text-body-xs break-words text-grey-600", children: task.note })), due && (_jsxs("span", { className: cn("inline-flex w-fit items-center gap-1 rounded px-1.5 py-0.5 text-body-xs font-medium", due.overdue ? "bg-error-50 text-error-700" : "text-grey-600"), children: [_jsx(Icon, { name: due.overdue ? "info-triangle" : "calendar", size: "xs" }), due.text] }))] })] }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2 @min-[560px]:justify-end", children: [onAssignTask && canEdit && assignees && assignees.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("label", { htmlFor: `${checkboxId}-assignee`, className: "sr-only", children: ["Assignee for ", task.title] }), _jsxs("select", { id: `${checkboxId}-assignee`, value: task.assigneeId ?? "", onChange: (event) => onAssignTask(task.id, event.target.value), className: "h-9 max-w-40 rounded-lg border border-grey-300 bg-surface px-2 text-body-xs text-grey-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [_jsx("option", { value: "", children: "Unassigned" }), assignees.map((candidate) => (_jsx("option", { value: candidate.id, children: candidate.name }, candidate.id)))] })] })) : (_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Avatar, { size: "xs", type: person?.avatarSrc ? "image" : person ? "initials" : "icon", src: person?.avatarSrc, alt: person?.name ?? "", icon: "user", initials: person?.initials ?? person?.name.slice(0, 2).toUpperCase() ?? "?" }), _jsx("span", { className: "truncate text-body-xs text-grey-600", children: person?.name ?? "Unassigned" })] })), onDeleteTask && canEdit && (_jsxs("button", { type: "button", onClick: () => onDeleteTask(task.id), className: "inline-flex size-9 cursor-pointer items-center justify-center rounded-lg border border-grey-300 text-grey-600 hover:bg-grey-100 hover:text-action-danger-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-action-primary-text", children: [_jsx(Icon, { name: "bin", size: "md" }), _jsxs("span", { className: "sr-only", children: ["Delete ", task.title] })] }))] })] }, task.id));
|
|
194
|
+
}) })), footer && _jsx("div", { className: "flex flex-wrap items-center gap-3", children: footer })] }) }));
|
|
195
|
+
}
|