@borisj74/bv-ds 0.1.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/LICENSE +21 -0
- package/README.md +94 -0
- package/dist/index.cjs +33885 -0
- package/dist/index.d.cts +2715 -0
- package/dist/index.d.ts +2715 -0
- package/dist/index.js +33717 -0
- package/package.json +67 -0
- package/src/components/ActivityFeed/ActivityFeed.tsx +48 -0
- package/src/components/ActivityFeed/index.ts +2 -0
- package/src/components/ActivityGauge/ActivityGauge.tsx +155 -0
- package/src/components/ActivityGauge/index.ts +7 -0
- package/src/components/AdvancedFilterBar/AdvancedFilterBar.tsx +80 -0
- package/src/components/AdvancedFilterBar/index.ts +2 -0
- package/src/components/Alert/Alert.tsx +210 -0
- package/src/components/Alert/index.ts +2 -0
- package/src/components/Avatar/Avatar.tsx +111 -0
- package/src/components/Avatar/index.ts +2 -0
- package/src/components/AvatarAddButton/AvatarAddButton.tsx +65 -0
- package/src/components/AvatarAddButton/index.ts +5 -0
- package/src/components/AvatarGroup/AvatarGroup.tsx +79 -0
- package/src/components/AvatarGroup/index.ts +6 -0
- package/src/components/AvatarLabelGroup/AvatarLabelGroup.tsx +62 -0
- package/src/components/AvatarLabelGroup/index.ts +5 -0
- package/src/components/AvatarProfilePhoto/AvatarProfilePhoto.tsx +117 -0
- package/src/components/AvatarProfilePhoto/index.ts +5 -0
- package/src/components/Badge/ColorBadge.tsx +36 -0
- package/src/components/Badge/ModernBadge.tsx +38 -0
- package/src/components/Badge/PillBadge.tsx +36 -0
- package/src/components/Badge/badgeShared.tsx +139 -0
- package/src/components/Badge/index.ts +7 -0
- package/src/components/BadgeCloseX/BadgeCloseX.tsx +64 -0
- package/src/components/BadgeCloseX/index.ts +2 -0
- package/src/components/BadgeGroup/BadgeGroup.tsx +61 -0
- package/src/components/BadgeGroup/index.ts +7 -0
- package/src/components/BreadcrumbButtonBase/BreadcrumbButtonBase.tsx +75 -0
- package/src/components/BreadcrumbButtonBase/index.ts +5 -0
- package/src/components/Breadcrumbs/Breadcrumbs.tsx +62 -0
- package/src/components/Breadcrumbs/index.ts +2 -0
- package/src/components/Button/Button.tsx +71 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/ButtonCloseX/ButtonCloseX.tsx +54 -0
- package/src/components/ButtonCloseX/index.ts +2 -0
- package/src/components/ButtonDestructive/ButtonDestructive.tsx +67 -0
- package/src/components/ButtonDestructive/index.ts +6 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +28 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/ButtonGroupSegment/ButtonGroupSegment.tsx +54 -0
- package/src/components/ButtonGroupSegment/index.ts +5 -0
- package/src/components/ButtonUtility/ButtonUtility.tsx +67 -0
- package/src/components/ButtonUtility/index.ts +6 -0
- package/src/components/CalendarCell/CalendarCell.tsx +82 -0
- package/src/components/CalendarCell/index.ts +2 -0
- package/src/components/CalendarCellDayWeekView/CalendarCellDayWeekView.tsx +56 -0
- package/src/components/CalendarCellDayWeekView/index.ts +2 -0
- package/src/components/CalendarColumnHeader/CalendarColumnHeader.tsx +45 -0
- package/src/components/CalendarColumnHeader/index.ts +5 -0
- package/src/components/CalendarDateIcon/CalendarDateIcon.tsx +25 -0
- package/src/components/CalendarDateIcon/index.ts +2 -0
- package/src/components/CalendarEvent/CalendarEvent.tsx +76 -0
- package/src/components/CalendarEvent/index.ts +2 -0
- package/src/components/CalendarEventDayWeekView/CalendarEventDayWeekView.tsx +76 -0
- package/src/components/CalendarEventDayWeekView/index.ts +2 -0
- package/src/components/CalendarHeader/CalendarHeader.tsx +47 -0
- package/src/components/CalendarHeader/index.ts +2 -0
- package/src/components/CalendarRowLabel/CalendarRowLabel.tsx +21 -0
- package/src/components/CalendarRowLabel/index.ts +2 -0
- package/src/components/CalendarTimemarker/CalendarTimemarker.tsx +46 -0
- package/src/components/CalendarTimemarker/index.ts +5 -0
- package/src/components/CalendarViewDropdown/CalendarViewDropdown.tsx +101 -0
- package/src/components/CalendarViewDropdown/index.ts +6 -0
- package/src/components/CardHeader/CardHeader.tsx +57 -0
- package/src/components/CardHeader/index.ts +2 -0
- package/src/components/CarouselArrow/CarouselArrow.tsx +47 -0
- package/src/components/CarouselArrow/index.ts +6 -0
- package/src/components/CarouselImage/CarouselImage.tsx +60 -0
- package/src/components/CarouselImage/index.ts +2 -0
- package/src/components/Change/Change.tsx +73 -0
- package/src/components/Change/index.ts +2 -0
- package/src/components/ChartLegend/ChartLegend.tsx +38 -0
- package/src/components/ChartLegend/index.ts +2 -0
- package/src/components/ChartMarker/ChartMarker.tsx +54 -0
- package/src/components/ChartMarker/index.ts +2 -0
- package/src/components/ChartMini/ChartMini.tsx +86 -0
- package/src/components/ChartMini/index.ts +2 -0
- package/src/components/ChartTooltip/ChartTooltip.tsx +44 -0
- package/src/components/ChartTooltip/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.tsx +65 -0
- package/src/components/Checkbox/checkboxBase.tsx +81 -0
- package/src/components/Checkbox/index.ts +3 -0
- package/src/components/CodeSnippet/CodeSnippet.tsx +94 -0
- package/src/components/CodeSnippet/index.ts +2 -0
- package/src/components/CodeSnippetTabs/CodeSnippetTabs.tsx +44 -0
- package/src/components/CodeSnippetTabs/index.ts +2 -0
- package/src/components/CommandBar/CommandBar.tsx +80 -0
- package/src/components/CommandBar/index.ts +2 -0
- package/src/components/CommandBarFooter/CommandBarFooter.tsx +125 -0
- package/src/components/CommandBarFooter/index.ts +5 -0
- package/src/components/CommandBarMenuSection/CommandBarMenuSection.tsx +28 -0
- package/src/components/CommandBarMenuSection/index.ts +2 -0
- package/src/components/CommandBarNavigationIcon/CommandBarNavigationIcon.tsx +47 -0
- package/src/components/CommandBarNavigationIcon/index.ts +2 -0
- package/src/components/CommandDropdownMenuItem/CommandDropdownMenuItem.tsx +51 -0
- package/src/components/CommandDropdownMenuItem/index.ts +2 -0
- package/src/components/CommandInput/CommandInput.tsx +74 -0
- package/src/components/CommandInput/index.ts +2 -0
- package/src/components/CommandShortcut/CommandShortcut.tsx +26 -0
- package/src/components/CommandShortcut/index.ts +2 -0
- package/src/components/ContentDivider/ContentDivider.tsx +80 -0
- package/src/components/ContentDivider/index.ts +6 -0
- package/src/components/ContentFeatureText/ContentFeatureText.tsx +60 -0
- package/src/components/ContentFeatureText/index.ts +5 -0
- package/src/components/ContentHeading/ContentHeading.tsx +43 -0
- package/src/components/ContentHeading/index.ts +5 -0
- package/src/components/ContentParagraph/ContentParagraph.tsx +39 -0
- package/src/components/ContentParagraph/index.ts +5 -0
- package/src/components/ContentQuote/ContentQuote.tsx +114 -0
- package/src/components/ContentQuote/index.ts +6 -0
- package/src/components/ContentRule/ContentRule.tsx +31 -0
- package/src/components/ContentRule/index.ts +2 -0
- package/src/components/ContextMenu/ContextMenu.tsx +67 -0
- package/src/components/ContextMenu/index.ts +7 -0
- package/src/components/ContextMenu/useContextMenu.ts +41 -0
- package/src/components/DatePickerCell/DatePickerCell.tsx +77 -0
- package/src/components/DatePickerCell/index.ts +2 -0
- package/src/components/DatePickerListItem/DatePickerListItem.tsx +39 -0
- package/src/components/DatePickerListItem/index.ts +2 -0
- package/src/components/DatePickerMenu/DatePickerMenu.tsx +131 -0
- package/src/components/DatePickerMenu/index.ts +2 -0
- package/src/components/DropdownAccountListItem/DropdownAccountListItem.tsx +69 -0
- package/src/components/DropdownAccountListItem/index.ts +2 -0
- package/src/components/DropdownMenuFooter/DropdownMenuFooter.tsx +50 -0
- package/src/components/DropdownMenuFooter/index.ts +5 -0
- package/src/components/DropdownMenuHeader/DropdownMenuHeader.tsx +93 -0
- package/src/components/DropdownMenuHeader/index.ts +5 -0
- package/src/components/DropdownMenuItemInsetIcon/DropdownMenuItemInsetIcon.tsx +89 -0
- package/src/components/DropdownMenuItemInsetIcon/index.ts +5 -0
- package/src/components/DropdownMenuListItem/DropdownMenuListItem.tsx +84 -0
- package/src/components/DropdownMenuListItem/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.tsx +65 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/FeedItemBase/FeedItemBase.tsx +135 -0
- package/src/components/FeedItemBase/index.ts +2 -0
- package/src/components/FileUpload/FileUpload.tsx +112 -0
- package/src/components/FileUpload/index.ts +2 -0
- package/src/components/FileUploadBase/FileUploadBase.tsx +69 -0
- package/src/components/FileUploadBase/index.ts +2 -0
- package/src/components/FileUploadItemBase/FileUploadItemBase.tsx +190 -0
- package/src/components/FileUploadItemBase/index.ts +7 -0
- package/src/components/FilterBar/FilterBar.tsx +62 -0
- package/src/components/FilterBar/index.ts +2 -0
- package/src/components/FilterTabs/FilterTabs.tsx +41 -0
- package/src/components/FilterTabs/index.ts +2 -0
- package/src/components/FiltersDropdownMenu/FiltersDropdownMenu.tsx +104 -0
- package/src/components/FiltersDropdownMenu/index.ts +2 -0
- package/src/components/FiltersSlideoutMenu/FiltersSlideoutMenu.tsx +71 -0
- package/src/components/FiltersSlideoutMenu/index.ts +2 -0
- package/src/components/HeaderNavigation/HeaderNavigation.tsx +178 -0
- package/src/components/HeaderNavigation/index.ts +6 -0
- package/src/components/HelpIcon/HelpIcon.tsx +49 -0
- package/src/components/HelpIcon/index.ts +2 -0
- package/src/components/InputField/InputField.tsx +108 -0
- package/src/components/InputField/index.ts +3 -0
- package/src/components/InputField/inputFieldShared.tsx +68 -0
- package/src/components/LeadingInputField/LeadingInputField.tsx +60 -0
- package/src/components/LeadingInputField/index.ts +2 -0
- package/src/components/LineAndBarChart/LineAndBarChart.tsx +96 -0
- package/src/components/LineAndBarChart/index.ts +2 -0
- package/src/components/LinkMessage/LinkMessage.tsx +52 -0
- package/src/components/LinkMessage/index.ts +2 -0
- package/src/components/LoadingIndicator/LoadingIndicator.tsx +108 -0
- package/src/components/LoadingIndicator/index.ts +6 -0
- package/src/components/MediaMessage/MediaMessage.tsx +109 -0
- package/src/components/MediaMessage/index.ts +2 -0
- package/src/components/MegaInputFieldBase/MegaInputFieldBase.tsx +49 -0
- package/src/components/MegaInputFieldBase/index.ts +5 -0
- package/src/components/Message/Message.tsx +85 -0
- package/src/components/Message/index.ts +3 -0
- package/src/components/Message/messageShared.tsx +73 -0
- package/src/components/MessageAction/MessageAction.tsx +221 -0
- package/src/components/MessageAction/index.ts +2 -0
- package/src/components/MessageActionButton/MessageActionButton.tsx +36 -0
- package/src/components/MessageActionButton/index.ts +2 -0
- package/src/components/MessageActionPanel/MessageActionPanel.tsx +36 -0
- package/src/components/MessageActionPanel/index.ts +2 -0
- package/src/components/MessageReaction/MessageReaction.tsx +37 -0
- package/src/components/MessageReaction/index.ts +2 -0
- package/src/components/MessageStatusIcon/MessageStatusIcon.tsx +54 -0
- package/src/components/MessageStatusIcon/index.ts +2 -0
- package/src/components/MetricItem/MetricItem.tsx +147 -0
- package/src/components/MetricItem/index.ts +2 -0
- package/src/components/ModalActions/ModalActions.tsx +57 -0
- package/src/components/ModalActions/index.ts +2 -0
- package/src/components/ModalHeader/ModalHeader.tsx +99 -0
- package/src/components/ModalHeader/index.ts +2 -0
- package/src/components/MultiSelect/MultiSelect.tsx +118 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/NavAccountCard/NavAccountCard.tsx +124 -0
- package/src/components/NavAccountCard/index.ts +2 -0
- package/src/components/NavAccountCardMenuItem/NavAccountCardMenuItem.tsx +101 -0
- package/src/components/NavAccountCardMenuItem/index.ts +5 -0
- package/src/components/NavButton/NavButton.tsx +50 -0
- package/src/components/NavButton/index.ts +2 -0
- package/src/components/NavFeaturedCard/NavFeaturedCard.tsx +82 -0
- package/src/components/NavFeaturedCard/index.ts +2 -0
- package/src/components/NavItemBase/NavItemBase.tsx +79 -0
- package/src/components/NavItemBase/index.ts +2 -0
- package/src/components/NavItemDropdownBase/NavItemDropdownBase.tsx +74 -0
- package/src/components/NavItemDropdownBase/index.ts +2 -0
- package/src/components/NavMenuButton/NavMenuButton.tsx +47 -0
- package/src/components/NavMenuButton/index.ts +2 -0
- package/src/components/Notification/Notification.tsx +102 -0
- package/src/components/Notification/index.ts +2 -0
- package/src/components/NumberInput/NumberInput.tsx +114 -0
- package/src/components/NumberInput/index.ts +2 -0
- package/src/components/PageHeader/PageHeader.tsx +88 -0
- package/src/components/PageHeader/index.ts +2 -0
- package/src/components/Pagination/Pagination.tsx +124 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/PaginationButtonGroupBase/PaginationButtonGroupBase.tsx +69 -0
- package/src/components/PaginationButtonGroupBase/index.ts +5 -0
- package/src/components/PaginationCards/PaginationCards.tsx +72 -0
- package/src/components/PaginationCards/index.ts +2 -0
- package/src/components/PaginationDotGroup/PaginationDotGroup.tsx +66 -0
- package/src/components/PaginationDotGroup/index.ts +2 -0
- package/src/components/PaginationDotIndicator/PaginationDotIndicator.tsx +39 -0
- package/src/components/PaginationDotIndicator/index.ts +6 -0
- package/src/components/PaginationNumberBase/PaginationNumberBase.tsx +42 -0
- package/src/components/PaginationNumberBase/index.ts +5 -0
- package/src/components/PieChart/PieChart.tsx +73 -0
- package/src/components/PieChart/index.ts +2 -0
- package/src/components/ProgressBar/ProgressBar.tsx +75 -0
- package/src/components/ProgressBar/index.ts +2 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +89 -0
- package/src/components/ProgressCircle/index.ts +6 -0
- package/src/components/RadarChart/RadarChart.tsx +62 -0
- package/src/components/RadarChart/index.ts +2 -0
- package/src/components/Radio/Radio.tsx +55 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/RadioGroup/RadioGroup.tsx +54 -0
- package/src/components/RadioGroup/index.ts +2 -0
- package/src/components/RadioGroupItem/RadioGroupItem.tsx +118 -0
- package/src/components/RadioGroupItem/index.ts +6 -0
- package/src/components/SectionFooter/SectionFooter.tsx +40 -0
- package/src/components/SectionFooter/index.ts +2 -0
- package/src/components/SectionHeader/SectionHeader.tsx +44 -0
- package/src/components/SectionHeader/index.ts +2 -0
- package/src/components/SectionLabel/SectionLabel.tsx +51 -0
- package/src/components/SectionLabel/index.ts +2 -0
- package/src/components/Select/Select.tsx +121 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/SelectMenuItem/SelectMenuItem.tsx +85 -0
- package/src/components/SelectMenuItem/index.ts +2 -0
- package/src/components/SidebarNavigation/SidebarNavigation.tsx +100 -0
- package/src/components/SidebarNavigation/index.ts +2 -0
- package/src/components/SlideOutMenuHeader/SlideOutMenuHeader.tsx +56 -0
- package/src/components/SlideOutMenuHeader/index.ts +2 -0
- package/src/components/Slider/Slider.tsx +125 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/SocialButton/SocialButton.tsx +88 -0
- package/src/components/SocialButton/index.ts +2 -0
- package/src/components/StatusIcon/StatusIcon.tsx +75 -0
- package/src/components/StatusIcon/index.ts +2 -0
- package/src/components/StepBase/StepBase.tsx +90 -0
- package/src/components/StepBase/index.ts +2 -0
- package/src/components/StepIconBase/StepIconBase.tsx +65 -0
- package/src/components/StepIconBase/index.ts +7 -0
- package/src/components/TabButtonBase/TabButtonBase.tsx +88 -0
- package/src/components/TabButtonBase/index.ts +2 -0
- package/src/components/TableCell/TableCell.tsx +44 -0
- package/src/components/TableCell/index.ts +2 -0
- package/src/components/TableHeaderCell/TableHeaderCell.tsx +34 -0
- package/src/components/TableHeaderCell/index.ts +2 -0
- package/src/components/TableHeaderLabel/TableHeaderLabel.tsx +37 -0
- package/src/components/TableHeaderLabel/index.ts +2 -0
- package/src/components/Tabs/Tabs.tsx +80 -0
- package/src/components/Tabs/index.ts +2 -0
- package/src/components/Tag/Tag.tsx +91 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/TagsInputField/TagsInputField.tsx +90 -0
- package/src/components/TagsInputField/index.ts +2 -0
- package/src/components/TextEditorToolbar/TextEditorToolbar.tsx +33 -0
- package/src/components/TextEditorToolbar/index.ts +2 -0
- package/src/components/TextEditorTooltip/TextEditorTooltip.tsx +28 -0
- package/src/components/TextEditorTooltip/index.ts +2 -0
- package/src/components/TextareaInputField/TextareaInputField.tsx +45 -0
- package/src/components/TextareaInputField/index.ts +2 -0
- package/src/components/Toggle/Toggle.tsx +87 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.tsx +59 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/components/TrailingInputField/TrailingInputField.tsx +62 -0
- package/src/components/TrailingInputField/index.ts +2 -0
- package/src/components/TreeView/TreeView.tsx +86 -0
- package/src/components/TreeView/index.ts +2 -0
- package/src/components/TreeViewConnector/TreeViewConnector.tsx +36 -0
- package/src/components/TreeViewConnector/index.ts +2 -0
- package/src/components/TreeViewItem/TreeViewItem.tsx +111 -0
- package/src/components/TreeViewItem/index.ts +2 -0
- package/src/components/VerificationCodeInput/VerificationCodeInput.tsx +114 -0
- package/src/components/VerificationCodeInput/index.ts +2 -0
- package/src/illustrations/BoxIllustration.tsx +13 -0
- package/src/illustrations/CloudIllustration.tsx +18 -0
- package/src/illustrations/CreditCardIllustration.tsx +13 -0
- package/src/illustrations/DocumentsIllustration.tsx +13 -0
- package/src/illustrations/index.ts +4 -0
- package/src/index.ts +147 -0
- package/src/internal/chartTheme.ts +30 -0
- package/src/internal/ringBase.tsx +82 -0
- package/src/styles.css +3 -0
- package/tailwind-preset.js +295 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export type TagSize = "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
export interface TagProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
size?: TagSize;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** Leading visual — dot / country flag / avatar (ReactNode slot). */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/** Render a leading checkbox (bare bordered box, distinct from `Checkbox`). */
|
|
12
|
+
checkbox?: boolean;
|
|
13
|
+
checked?: boolean;
|
|
14
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
15
|
+
/** Trailing count value (renders a divider + number). */
|
|
16
|
+
count?: ReactNode;
|
|
17
|
+
/** Trailing X-close button callback. */
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compact tag/chip. One component over the Figma matrix: `icon` (dot/country/
|
|
23
|
+
* avatar) slot, optional leading `checkbox`, trailing `count`, and an `onClose`
|
|
24
|
+
* X button. The checkbox/count/close sub-parts are inline (not separate exported
|
|
25
|
+
* components); `checkbox` is a bare bordered box, intentionally distinct from the
|
|
26
|
+
* full `Checkbox`/`Radio` controls.
|
|
27
|
+
*/
|
|
28
|
+
export function Tag({
|
|
29
|
+
size = "md",
|
|
30
|
+
children,
|
|
31
|
+
icon,
|
|
32
|
+
checkbox = false,
|
|
33
|
+
checked = false,
|
|
34
|
+
onCheckedChange,
|
|
35
|
+
count,
|
|
36
|
+
onClose,
|
|
37
|
+
className,
|
|
38
|
+
...rest
|
|
39
|
+
}: TagProps) {
|
|
40
|
+
const text = size === "sm" ? "text-xs" : "text-sm";
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className={clsx(
|
|
44
|
+
"inline-flex items-center gap-[5px] rounded-sm border border-border-primary bg-bg-primary font-medium text-text-secondary",
|
|
45
|
+
size === "lg" ? "px-md py-xs" : "px-xs py-xxs",
|
|
46
|
+
text,
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...rest}
|
|
50
|
+
>
|
|
51
|
+
{checkbox && (
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
role="checkbox"
|
|
55
|
+
aria-checked={checked}
|
|
56
|
+
onClick={() => onCheckedChange?.(!checked)}
|
|
57
|
+
className={clsx(
|
|
58
|
+
"flex size-4 shrink-0 items-center justify-center rounded-xs border",
|
|
59
|
+
checked ? "border-transparent bg-bg-brand-solid" : "border-border-primary bg-bg-primary",
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{checked && (
|
|
63
|
+
<svg viewBox="0 0 16 16" fill="none" className="size-3 text-fg-white" aria-hidden>
|
|
64
|
+
<path d="M13.33 4 6 11.33 2.67 8" stroke="currentColor" strokeWidth="1.67" strokeLinecap="round" strokeLinejoin="round" />
|
|
65
|
+
</svg>
|
|
66
|
+
)}
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
{icon && <span className="flex shrink-0 items-center">{icon}</span>}
|
|
70
|
+
<span className="whitespace-nowrap">{children}</span>
|
|
71
|
+
{count != null && (
|
|
72
|
+
<>
|
|
73
|
+
<span className="h-3 w-px bg-border-secondary" aria-hidden />
|
|
74
|
+
<span className="text-text-tertiary">{count}</span>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
{onClose && (
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={onClose}
|
|
81
|
+
aria-label="Remove"
|
|
82
|
+
className="-mr-0.5 flex shrink-0 items-center justify-center rounded-[3px] p-xxs text-fg-quaternary hover:bg-bg-primary-hover hover:text-fg-secondary"
|
|
83
|
+
>
|
|
84
|
+
<svg viewBox="0 0 12 12" fill="none" className="size-3" aria-hidden>
|
|
85
|
+
<path d="M9 3 3 9M3 3l6 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
)}
|
|
89
|
+
</span>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { FieldWrapper, type InputFieldSize } from "../InputField/inputFieldShared";
|
|
4
|
+
|
|
5
|
+
export type TagsInputVariant = "inner" | "outer";
|
|
6
|
+
|
|
7
|
+
export interface TagsInputFieldProps
|
|
8
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
9
|
+
/** Current tags. */
|
|
10
|
+
tags?: string[];
|
|
11
|
+
/** Remove handler (receives the tag index). Renders a close button per chip. */
|
|
12
|
+
onRemoveTag?: (index: number) => void;
|
|
13
|
+
/** `inner` keeps chips inside the field; `outer` shows them below it. */
|
|
14
|
+
variant?: TagsInputVariant;
|
|
15
|
+
size?: InputFieldSize;
|
|
16
|
+
label?: ReactNode;
|
|
17
|
+
hint?: ReactNode;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
destructive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TagChip({ label, onRemove }: { label: string; onRemove?: () => void }) {
|
|
23
|
+
return (
|
|
24
|
+
<span className="flex shrink-0 items-center gap-[3px] rounded-sm border border-border-primary bg-bg-primary py-xxs pl-[9px] pr-xs text-sm font-medium text-text-secondary">
|
|
25
|
+
{label}
|
|
26
|
+
{onRemove ? (
|
|
27
|
+
<button type="button" onClick={onRemove} aria-label={`Remove ${label}`} className="rounded-[3px] p-xxs text-fg-quaternary hover:text-fg-quaternary-hover">
|
|
28
|
+
<svg viewBox="0 0 12 12" fill="none" className="size-3" aria-hidden>
|
|
29
|
+
<path d="M9 3 3 9M3 3l6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
30
|
+
</svg>
|
|
31
|
+
</button>
|
|
32
|
+
) : null}
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Tag/chip input. `inner` renders the chips inside the field (wrapping with the
|
|
39
|
+
* text input); `outer` renders the field on top with the chips below it.
|
|
40
|
+
* Tag state is controlled by the consumer (`tags` + `onRemoveTag`).
|
|
41
|
+
*/
|
|
42
|
+
export function TagsInputField({
|
|
43
|
+
tags = [],
|
|
44
|
+
onRemoveTag,
|
|
45
|
+
variant = "inner",
|
|
46
|
+
size = "md",
|
|
47
|
+
label,
|
|
48
|
+
hint,
|
|
49
|
+
required,
|
|
50
|
+
destructive,
|
|
51
|
+
className,
|
|
52
|
+
placeholder = "Add tag",
|
|
53
|
+
...rest
|
|
54
|
+
}: TagsInputFieldProps) {
|
|
55
|
+
const chips = tags.map((t, i) => (
|
|
56
|
+
<TagChip key={i} label={t} onRemove={onRemoveTag ? () => onRemoveTag(i) : undefined} />
|
|
57
|
+
));
|
|
58
|
+
|
|
59
|
+
const box = (inner: ReactNode) => (
|
|
60
|
+
<div
|
|
61
|
+
className={clsx(
|
|
62
|
+
"flex w-full flex-wrap items-center gap-md rounded-md border bg-bg-primary px-lg py-md shadow-xs",
|
|
63
|
+
"focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-bg-primary",
|
|
64
|
+
destructive
|
|
65
|
+
? "border-border-error focus-within:ring-border-error"
|
|
66
|
+
: "border-border-primary focus-within:border-border-brand focus-within:ring-border-brand",
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{inner}
|
|
70
|
+
<input
|
|
71
|
+
placeholder={placeholder}
|
|
72
|
+
className="min-w-[80px] flex-1 bg-transparent text-md text-text-primary outline-none placeholder:text-text-placeholder"
|
|
73
|
+
{...rest}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<FieldWrapper label={label} required={required} hint={hint} destructive={destructive} className={className}>
|
|
80
|
+
{variant === "inner" ? (
|
|
81
|
+
box(chips)
|
|
82
|
+
) : (
|
|
83
|
+
<div className="flex w-full flex-col gap-md">
|
|
84
|
+
{box(null)}
|
|
85
|
+
{chips.length ? <div className="flex flex-wrap gap-sm">{chips}</div> : null}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</FieldWrapper>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export interface TextEditorToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Toolbar controls — typically `ButtonUtility` groups. */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Formatting-toolbar **chrome shell** for a rich-text editor (layout + tokens
|
|
11
|
+
* only, no editing logic). Horizontal bar; drop your formatting buttons in as
|
|
12
|
+
* `children` (use `TextEditorToolbarDivider` between groups). Pair with a real
|
|
13
|
+
* editor engine (TipTap/Quill) — the deferred `Text editor` row.
|
|
14
|
+
*/
|
|
15
|
+
export function TextEditorToolbar({ children, className, ...rest }: TextEditorToolbarProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={clsx(
|
|
19
|
+
"flex flex-wrap items-center gap-xs rounded-lg border border-border-secondary bg-bg-primary p-xs shadow-xs",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
role="toolbar"
|
|
23
|
+
{...rest}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Vertical divider between toolbar groups. */
|
|
31
|
+
export function TextEditorToolbarDivider({ className }: { className?: string }) {
|
|
32
|
+
return <span className={clsx("mx-xs h-5 w-px shrink-0 bg-border-secondary", className)} aria-hidden />;
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export interface TextEditorTooltipProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** Inline formatting controls shown for the current selection. */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Floating selection-toolbar **chrome shell** for a rich-text editor (layout +
|
|
11
|
+
* tokens only, no editing logic). A small elevated card meant to be positioned
|
|
12
|
+
* over a text selection; drop inline-format buttons in as `children`. Pair with a
|
|
13
|
+
* real editor engine — the deferred `Text editor` row.
|
|
14
|
+
*/
|
|
15
|
+
export function TextEditorTooltip({ children, className, ...rest }: TextEditorTooltipProps) {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={clsx(
|
|
19
|
+
"inline-flex items-center gap-xxs rounded-lg border border-border-secondary-alt bg-bg-primary p-xxs shadow-lg",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
role="toolbar"
|
|
23
|
+
{...rest}
|
|
24
|
+
>
|
|
25
|
+
{children}
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TextareaHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import {
|
|
4
|
+
FieldWrapper,
|
|
5
|
+
boxClasses,
|
|
6
|
+
inputSizeClasses,
|
|
7
|
+
type InputFieldSize,
|
|
8
|
+
} from "../InputField/inputFieldShared";
|
|
9
|
+
|
|
10
|
+
export interface TextareaInputFieldProps
|
|
11
|
+
extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "size"> {
|
|
12
|
+
size?: InputFieldSize;
|
|
13
|
+
label?: ReactNode;
|
|
14
|
+
hint?: ReactNode;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
destructive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Multiline text input. Reuses the Input field family's shared `FieldWrapper`
|
|
21
|
+
* (label/required/hint/destructive) and `boxClasses`/`inputSizeClasses` — the
|
|
22
|
+
* wrapper is NOT reimplemented. `rows` controls height (default 4).
|
|
23
|
+
*/
|
|
24
|
+
export function TextareaInputField({
|
|
25
|
+
size = "md",
|
|
26
|
+
label,
|
|
27
|
+
hint,
|
|
28
|
+
required,
|
|
29
|
+
destructive,
|
|
30
|
+
rows = 4,
|
|
31
|
+
className,
|
|
32
|
+
...rest
|
|
33
|
+
}: TextareaInputFieldProps) {
|
|
34
|
+
return (
|
|
35
|
+
<FieldWrapper label={label} required={required} hint={hint} destructive={destructive} className={className}>
|
|
36
|
+
<div className={clsx(boxClasses(destructive), "items-stretch", inputSizeClasses[size])}>
|
|
37
|
+
<textarea
|
|
38
|
+
rows={rows}
|
|
39
|
+
className="min-w-0 flex-1 resize-y bg-transparent text-text-primary outline-none placeholder:text-text-placeholder"
|
|
40
|
+
{...rest}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</FieldWrapper>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export type ToggleType = "default" | "slim";
|
|
5
|
+
export type ToggleSize = "sm" | "md";
|
|
6
|
+
|
|
7
|
+
export interface ToggleProps {
|
|
8
|
+
type?: ToggleType;
|
|
9
|
+
size?: ToggleSize;
|
|
10
|
+
checked?: boolean;
|
|
11
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
label?: ReactNode;
|
|
14
|
+
supportingText?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
"aria-label"?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const TRACK: Record<ToggleSize, string> = { sm: "h-5 w-9", md: "h-6 w-11" };
|
|
21
|
+
const KNOB: Record<ToggleSize, string> = { sm: "size-4", md: "size-5" };
|
|
22
|
+
const TRAVEL: Record<ToggleSize, string> = { sm: "translate-x-4", md: "translate-x-5" };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Switch toggle. `default` = filled track (`bg-brand-solid` when on); `slim` = a
|
|
26
|
+
* thinner track with a bordered knob (uses the `toggle-slim-border-pressed`
|
|
27
|
+
* tokens when on). The `_Toggle base` control is inline (not a separate export).
|
|
28
|
+
* Controlled via `checked`/`onCheckedChange`; optional `label`/`supportingText`.
|
|
29
|
+
*/
|
|
30
|
+
export function Toggle({
|
|
31
|
+
type = "default",
|
|
32
|
+
size = "sm",
|
|
33
|
+
checked = false,
|
|
34
|
+
onCheckedChange,
|
|
35
|
+
disabled = false,
|
|
36
|
+
label,
|
|
37
|
+
supportingText,
|
|
38
|
+
className,
|
|
39
|
+
id,
|
|
40
|
+
"aria-label": ariaLabel,
|
|
41
|
+
}: ToggleProps) {
|
|
42
|
+
const slim = type === "slim";
|
|
43
|
+
|
|
44
|
+
const control = (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
role="switch"
|
|
48
|
+
id={id}
|
|
49
|
+
aria-checked={checked}
|
|
50
|
+
aria-label={ariaLabel}
|
|
51
|
+
disabled={disabled}
|
|
52
|
+
onClick={() => onCheckedChange?.(!checked)}
|
|
53
|
+
className={clsx(
|
|
54
|
+
"relative inline-flex shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-utility-brand-500 focus-visible:ring-offset-2",
|
|
55
|
+
slim ? "h-2 w-9 border" : TRACK[size],
|
|
56
|
+
slim
|
|
57
|
+
? checked
|
|
58
|
+
? "border-toggle-slim-border-pressed bg-bg-brand-solid hover:border-toggle-slim-border-pressed-hover"
|
|
59
|
+
: "border-transparent bg-bg-quaternary"
|
|
60
|
+
: checked
|
|
61
|
+
? "bg-bg-brand-solid hover:bg-bg-brand-solid-hover"
|
|
62
|
+
: "bg-bg-quaternary",
|
|
63
|
+
disabled && "cursor-not-allowed opacity-60",
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
<span
|
|
67
|
+
className={clsx(
|
|
68
|
+
"pointer-events-none inline-block rounded-full bg-fg-white shadow-sm transition-transform",
|
|
69
|
+
slim ? "size-4 border border-border-primary" : KNOB[size],
|
|
70
|
+
checked ? (slim ? "translate-x-5" : TRAVEL[size]) : slim ? "-translate-x-px" : "translate-x-0.5",
|
|
71
|
+
)}
|
|
72
|
+
/>
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (!label && !supportingText) return <span className={className}>{control}</span>;
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={clsx("flex items-start gap-md", className)}>
|
|
80
|
+
{control}
|
|
81
|
+
<label htmlFor={id} className="flex flex-col">
|
|
82
|
+
{label && <span className="text-sm font-medium text-text-secondary">{label}</span>}
|
|
83
|
+
{supportingText && <span className="text-sm text-text-tertiary">{supportingText}</span>}
|
|
84
|
+
</label>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export type TooltipArrow =
|
|
5
|
+
| "none"
|
|
6
|
+
| "topCenter"
|
|
7
|
+
| "bottomCenter"
|
|
8
|
+
| "bottomLeft"
|
|
9
|
+
| "bottomRight"
|
|
10
|
+
| "left"
|
|
11
|
+
| "right";
|
|
12
|
+
|
|
13
|
+
export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
/** Tooltip heading text. */
|
|
15
|
+
text?: ReactNode;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
/** Secondary line below the heading. */
|
|
18
|
+
supportingText?: ReactNode;
|
|
19
|
+
/** Arrow position relative to the bubble. */
|
|
20
|
+
arrow?: TooltipArrow;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ARROW_POS: Record<Exclude<TooltipArrow, "none">, string> = {
|
|
24
|
+
topCenter: "left-1/2 top-0 -translate-x-1/2 -translate-y-1/2",
|
|
25
|
+
bottomCenter: "left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2",
|
|
26
|
+
bottomLeft: "left-3 bottom-0 translate-y-1/2",
|
|
27
|
+
bottomRight: "right-3 bottom-0 translate-y-1/2",
|
|
28
|
+
left: "left-0 top-1/2 -translate-x-1/2 -translate-y-1/2",
|
|
29
|
+
right: "right-0 top-1/2 translate-x-1/2 -translate-y-1/2",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Presentational tooltip bubble (dark `bg-primary-solid`, white text). `text` is
|
|
34
|
+
* the heading; `supportingText` adds a second `utility-neutral-300` line (max
|
|
35
|
+
* 296px). `arrow` positions a small pointer on the bubble edge (rendered as a
|
|
36
|
+
* rotated square). Positioning relative to a trigger is the caller's job.
|
|
37
|
+
*/
|
|
38
|
+
export function Tooltip({ text, children, supportingText, arrow = "none", className, ...rest }: TooltipProps) {
|
|
39
|
+
const hasSupporting = supportingText != null;
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
role="tooltip"
|
|
43
|
+
className={clsx(
|
|
44
|
+
"relative inline-flex flex-col rounded-md bg-bg-primary-solid shadow-lg",
|
|
45
|
+
hasSupporting ? "max-w-[320px] gap-xxs p-lg" : "px-lg py-md",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...rest}
|
|
49
|
+
>
|
|
50
|
+
<span className="text-xs font-semibold text-text-white">{text ?? children}</span>
|
|
51
|
+
{hasSupporting && (
|
|
52
|
+
<span className="max-w-[296px] text-xs font-medium text-utility-neutral-300">{supportingText}</span>
|
|
53
|
+
)}
|
|
54
|
+
{arrow !== "none" && (
|
|
55
|
+
<span className={clsx("absolute size-2 rotate-45 bg-bg-primary-solid", ARROW_POS[arrow])} aria-hidden />
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { FieldWrapper, type InputFieldSize } from "../InputField/inputFieldShared";
|
|
4
|
+
|
|
5
|
+
export interface TrailingInputFieldProps
|
|
6
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> {
|
|
7
|
+
/**
|
|
8
|
+
* Right add-on — a trailing button or dropdown. Covers the Trailing button /
|
|
9
|
+
* Trailing dropdown types.
|
|
10
|
+
*/
|
|
11
|
+
suffix?: ReactNode;
|
|
12
|
+
size?: InputFieldSize;
|
|
13
|
+
label?: ReactNode;
|
|
14
|
+
hint?: ReactNode;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
destructive?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pad: Record<InputFieldSize, string> = {
|
|
20
|
+
sm: "px-lg py-md text-sm",
|
|
21
|
+
md: "px-lg py-md text-md",
|
|
22
|
+
lg: "px-[14px] py-2.5 text-md",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Input with a right add-on (trailing button or dropdown). */
|
|
26
|
+
export function TrailingInputField({
|
|
27
|
+
suffix,
|
|
28
|
+
size = "md",
|
|
29
|
+
label,
|
|
30
|
+
hint,
|
|
31
|
+
required,
|
|
32
|
+
destructive,
|
|
33
|
+
className,
|
|
34
|
+
...rest
|
|
35
|
+
}: TrailingInputFieldProps) {
|
|
36
|
+
return (
|
|
37
|
+
<FieldWrapper label={label} required={required} hint={hint} destructive={destructive} className={className}>
|
|
38
|
+
<div
|
|
39
|
+
className={clsx(
|
|
40
|
+
"flex w-full items-stretch overflow-hidden rounded-md border bg-bg-primary shadow-xs",
|
|
41
|
+
"focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-bg-primary",
|
|
42
|
+
destructive
|
|
43
|
+
? "border-border-error focus-within:ring-border-error"
|
|
44
|
+
: "border-border-primary focus-within:border-border-brand focus-within:ring-border-brand",
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
className={clsx(
|
|
49
|
+
"min-w-0 flex-1 bg-transparent text-text-primary outline-none placeholder:text-text-placeholder",
|
|
50
|
+
pad[size],
|
|
51
|
+
)}
|
|
52
|
+
{...rest}
|
|
53
|
+
/>
|
|
54
|
+
{suffix ? (
|
|
55
|
+
<span className="flex shrink-0 items-center border-l border-border-primary">
|
|
56
|
+
{suffix}
|
|
57
|
+
</span>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
</FieldWrapper>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { TreeViewItem, type TreeViewItemSize } from "../TreeViewItem";
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
id: string;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
children?: TreeNode[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TreeViewProps {
|
|
13
|
+
data: TreeNode[];
|
|
14
|
+
size?: TreeViewItemSize;
|
|
15
|
+
/** Currently selected node id (controlled). */
|
|
16
|
+
selectedId?: string;
|
|
17
|
+
onSelect?: (id: string) => void;
|
|
18
|
+
/** Node ids expanded by default (uncontrolled expand state). */
|
|
19
|
+
defaultExpandedIds?: string[];
|
|
20
|
+
/** Show a checkbox on every row. */
|
|
21
|
+
checkboxes?: boolean;
|
|
22
|
+
/** Max nesting depth rendered — guards against runaway/cyclic data. */
|
|
23
|
+
maxDepth?: number;
|
|
24
|
+
className?: string;
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tree view composite — renders nested `TreeNode` data recursively as
|
|
30
|
+
* `TreeViewItem`s (which compose `TreeViewConnector` for indent guides). Expand
|
|
31
|
+
* state is internal (seeded by `defaultExpandedIds`); selection is controlled via
|
|
32
|
+
* `selectedId`/`onSelect`.
|
|
33
|
+
*/
|
|
34
|
+
export function TreeView({
|
|
35
|
+
data,
|
|
36
|
+
size = "sm",
|
|
37
|
+
selectedId,
|
|
38
|
+
onSelect,
|
|
39
|
+
defaultExpandedIds = [],
|
|
40
|
+
checkboxes = false,
|
|
41
|
+
maxDepth = 10,
|
|
42
|
+
className,
|
|
43
|
+
"aria-label": ariaLabel,
|
|
44
|
+
}: TreeViewProps) {
|
|
45
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set(defaultExpandedIds));
|
|
46
|
+
const toggle = (id: string) =>
|
|
47
|
+
setExpanded((prev) => {
|
|
48
|
+
const next = new Set(prev);
|
|
49
|
+
next.has(id) ? next.delete(id) : next.add(id);
|
|
50
|
+
return next;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const renderNodes = (nodes: TreeNode[], level: number): ReactNode => {
|
|
54
|
+
if (level >= maxDepth) return null;
|
|
55
|
+
return nodes.map((node) => {
|
|
56
|
+
const hasChildren = !!node.children?.length;
|
|
57
|
+
const isOpen = expanded.has(node.id);
|
|
58
|
+
return (
|
|
59
|
+
<li key={node.id} role="treeitem" aria-expanded={hasChildren ? isOpen : undefined} aria-selected={node.id === selectedId}>
|
|
60
|
+
<TreeViewItem
|
|
61
|
+
size={size}
|
|
62
|
+
level={level}
|
|
63
|
+
icon={node.icon}
|
|
64
|
+
expandable={hasChildren}
|
|
65
|
+
open={isOpen}
|
|
66
|
+
selected={node.id === selectedId}
|
|
67
|
+
checkbox={checkboxes}
|
|
68
|
+
onToggle={() => toggle(node.id)}
|
|
69
|
+
onClick={() => onSelect?.(node.id)}
|
|
70
|
+
>
|
|
71
|
+
{node.label}
|
|
72
|
+
</TreeViewItem>
|
|
73
|
+
{hasChildren && isOpen && (
|
|
74
|
+
<ul role="group">{renderNodes(node.children!, level + 1)}</ul>
|
|
75
|
+
)}
|
|
76
|
+
</li>
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ul role="tree" aria-label={ariaLabel} className={clsx("flex flex-col", className)}>
|
|
83
|
+
{renderNodes(data, 0)}
|
|
84
|
+
</ul>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type HTMLAttributes } from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export type TreeViewConnectorType = "default" | "endConnector" | "none";
|
|
5
|
+
export type TreeViewConnectorSize = "sm" | "md";
|
|
6
|
+
|
|
7
|
+
export interface TreeViewConnectorProps extends HTMLAttributes<HTMLSpanElement> {
|
|
8
|
+
type?: TreeViewConnectorType;
|
|
9
|
+
size?: TreeViewConnectorSize;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Indent guide for `TreeViewItem`/`TreeView`. `default` = a full-height vertical
|
|
14
|
+
* line (a passing-through branch); `endConnector` = an L-shape (vertical down to
|
|
15
|
+
* centre, then a stub to the right) for the last child; `none` = empty spacer.
|
|
16
|
+
* Drawn with borders rather than the Figma image asset.
|
|
17
|
+
*/
|
|
18
|
+
export function TreeViewConnector({ type = "default", size = "sm", className, ...rest }: TreeViewConnectorProps) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
className={clsx("relative inline-block shrink-0", size === "md" ? "size-5" : "size-4", className)}
|
|
22
|
+
aria-hidden
|
|
23
|
+
{...rest}
|
|
24
|
+
>
|
|
25
|
+
{type === "default" && (
|
|
26
|
+
<span className="absolute left-1/2 top-0 bottom-0 w-px -translate-x-1/2 bg-border-secondary" />
|
|
27
|
+
)}
|
|
28
|
+
{type === "endConnector" && (
|
|
29
|
+
<>
|
|
30
|
+
<span className="absolute left-1/2 top-0 h-1/2 w-px -translate-x-1/2 bg-border-secondary" />
|
|
31
|
+
<span className="absolute left-1/2 top-1/2 right-0 h-px bg-border-secondary" />
|
|
32
|
+
</>
|
|
33
|
+
)}
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|