@gunjo/ui 0.0.1-alpha.0 → 0.0.1-alpha.2
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.ja.md +90 -0
- package/README.md +52 -91
- package/package.json +47 -6
- package/src/components/display/Accordion.tsx +185 -0
- package/src/components/display/AccordionGroup.tsx +155 -0
- package/src/components/display/ActionDataTable.tsx +413 -0
- package/src/components/display/ActivityTimelineCard.tsx +483 -0
- package/src/components/display/AnalyticsCard.tsx +167 -0
- package/src/components/display/AssetCard.tsx +242 -0
- package/src/components/display/AssetGrid.tsx +164 -0
- package/src/components/display/Avatar.tsx +127 -0
- package/src/components/display/AvatarGroup.tsx +131 -0
- package/src/components/{atoms → display}/Badge.tsx +3 -3
- package/src/components/display/BarChart.tsx +247 -0
- package/src/components/{molecules → display}/Card.tsx +1 -1
- package/src/components/display/Carousel.tsx +593 -0
- package/src/components/display/ChartLegend.tsx +124 -0
- package/src/components/display/ChatMessage.tsx +382 -0
- package/src/components/display/ChoroplethMap.tsx +613 -0
- package/src/components/display/Code.tsx +42 -0
- package/src/components/display/CodeBlock.tsx +338 -0
- package/src/components/display/ColorSwatch.tsx +71 -0
- package/src/components/display/ConcentricProgressCard.tsx +545 -0
- package/src/components/display/DataTable.tsx +522 -0
- package/src/components/display/DistributionBar.tsx +102 -0
- package/src/components/display/DocNote.tsx +36 -0
- package/src/components/display/DonutChart.tsx +257 -0
- package/src/components/display/EmptyState.tsx +44 -0
- package/src/components/display/FileTree.tsx +180 -0
- package/src/components/display/GaugeChart.tsx +219 -0
- package/src/components/display/HeatmapChart.tsx +266 -0
- package/src/components/display/Icon.tsx +66 -0
- package/src/components/display/ImagePreview.tsx +140 -0
- package/src/components/{atoms → display}/Img.tsx +46 -12
- package/src/components/display/LabeledDonutCard.tsx +475 -0
- package/src/components/display/LineChart.tsx +464 -0
- package/src/components/{molecules → display}/List.tsx +20 -13
- package/src/components/display/MarkdownRenderer.tsx +157 -0
- package/src/components/display/MetadataList.tsx +81 -0
- package/src/components/display/MiniDistributionBarCard.tsx +314 -0
- package/src/components/display/PieChart.tsx +234 -0
- package/src/components/display/QuadrantMatrix.tsx +330 -0
- package/src/components/display/RadarChart.tsx +335 -0
- package/src/components/display/RadialBarChart.tsx +264 -0
- package/src/components/display/RetentionCohortCard.tsx +350 -0
- package/src/components/display/RibbonChart.tsx +618 -0
- package/src/components/display/SearchableAccordion.tsx +270 -0
- package/src/components/display/SegmentTimelineCard.tsx +452 -0
- package/src/components/display/SegmentedGaugeCard.tsx +607 -0
- package/src/components/display/Spacer.tsx +51 -0
- package/src/components/display/SparklineChart.tsx +394 -0
- package/src/components/display/StackedBarChart.tsx +393 -0
- package/src/components/display/Statistic.tsx +70 -0
- package/src/components/{molecules → display}/Table.tsx +22 -7
- package/src/components/display/Tag.tsx +80 -0
- package/src/components/display/TagEditor.tsx +141 -0
- package/src/components/display/Timeline.tsx +121 -0
- package/src/components/{atoms → display}/ToolPill.tsx +42 -18
- package/src/components/display/TreeView.tsx +226 -0
- package/src/components/display/chart-tooltip.tsx +423 -0
- package/src/components/display/chart-utils.ts +71 -0
- package/src/components/display/circular-chart-utils.ts +147 -0
- package/src/components/display/generated/default-variant-keys.ts +90 -0
- package/src/components/display/generated/variant-keys.ts +169 -0
- package/src/components/{atoms → feedback}/Alert.tsx +12 -5
- package/src/components/feedback/Banner.tsx +90 -0
- package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
- package/src/components/feedback/ProgressWidget.tsx +44 -0
- package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
- package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
- package/src/components/feedback/StatusScreen.tsx +148 -0
- package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
- package/src/components/feedback/Toast.tsx +108 -0
- package/src/components/feedback/ToastProvider.tsx +78 -0
- package/src/components/feedback/generated/default-variant-keys.ts +16 -0
- package/src/components/feedback/generated/variant-keys.ts +21 -0
- package/src/components/generated/component-manifest.ts +1568 -454
- package/src/components/generated/component-style-hints.ts +1958 -718
- package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
- package/src/components/inputs/Calendar.tsx +212 -0
- package/src/components/inputs/ChatComposer.tsx +75 -0
- package/src/components/inputs/ChatInput.tsx +528 -0
- package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
- package/src/components/inputs/Combobox.tsx +175 -0
- package/src/components/inputs/CopyButton.tsx +187 -0
- package/src/components/inputs/DatePicker.tsx +519 -0
- package/src/components/inputs/DateRangePicker.tsx +878 -0
- package/src/components/inputs/EditableField.tsx +182 -0
- package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
- package/src/components/inputs/FilterButton.tsx +163 -0
- package/src/components/{molecules → inputs}/Form.tsx +20 -3
- package/src/components/{atoms → inputs}/Input.tsx +2 -0
- package/src/components/inputs/InputOTP.tsx +75 -0
- package/src/components/inputs/Mention.tsx +279 -0
- package/src/components/inputs/NumberInput.tsx +109 -0
- package/src/components/inputs/PasswordGroup.tsx +138 -0
- package/src/components/inputs/PasswordInput.tsx +74 -0
- package/src/components/inputs/PasswordRequirementList.tsx +96 -0
- package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
- package/src/components/inputs/PhoneInput.tsx +99 -0
- package/src/components/inputs/PostalCodeInput.tsx +98 -0
- package/src/components/inputs/RangeSlider.tsx +129 -0
- package/src/components/inputs/SearchInput.tsx +76 -0
- package/src/components/inputs/Select.tsx +39 -0
- package/src/components/{atoms → inputs}/Slider.tsx +18 -5
- package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
- package/src/components/{atoms → inputs}/Switch.tsx +15 -4
- package/src/components/inputs/TagInput.tsx +114 -0
- package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
- package/src/components/inputs/TimePicker.tsx +150 -0
- package/src/components/inputs/Toggle.tsx +48 -0
- package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
- package/src/components/inputs/TooltipButton.tsx +148 -0
- package/src/components/inputs/VoiceInputButton.tsx +317 -0
- package/src/components/inputs/calendar-holidays.ts +56 -0
- package/src/components/inputs/generated/default-variant-keys.ts +32 -0
- package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
- package/src/components/layout/AspectRatio.tsx +12 -0
- package/src/components/layout/AssetInspectorPanel.tsx +416 -0
- package/src/components/layout/Cluster.tsx +56 -0
- package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
- package/src/components/layout/Container.tsx +43 -0
- package/src/components/layout/DeviceFrame.tsx +227 -0
- package/src/components/layout/Grid.tsx +65 -0
- package/src/components/layout/HStack.tsx +73 -0
- package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
- package/src/components/layout/MarqueeFrame.tsx +158 -0
- package/src/components/layout/Resizable.tsx +94 -0
- package/src/components/layout/ScrollArea.tsx +71 -0
- package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
- package/src/components/layout/VStack.tsx +69 -0
- package/src/components/layout/generated/default-variant-keys.ts +16 -0
- package/src/components/layout/generated/variant-keys.ts +21 -0
- package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
- package/src/components/navigation/Command.tsx +266 -0
- package/src/components/navigation/CommandPalette.tsx +83 -0
- package/src/components/navigation/DocumentPager.tsx +171 -0
- package/src/components/navigation/Footer.tsx +88 -0
- package/src/components/navigation/Header.tsx +80 -0
- package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
- package/src/components/navigation/NavigationMenu.tsx +128 -0
- package/src/components/navigation/PageAside.tsx +84 -0
- package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
- package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
- package/src/components/navigation/Sidebar.tsx +223 -0
- package/src/components/navigation/SidebarItem.tsx +160 -0
- package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
- package/src/components/navigation/TextLink.tsx +71 -0
- package/src/components/navigation/generated/default-variant-keys.ts +12 -0
- package/src/components/navigation/generated/variant-keys.ts +13 -0
- package/src/components/overlay/AIChatInput.tsx +5 -0
- package/src/components/overlay/AIChatMessage.tsx +6 -0
- package/src/components/overlay/AlertDialog.tsx +145 -0
- package/src/components/overlay/ChatPanel.tsx +180 -0
- package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
- package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
- package/src/components/overlay/Drawer.tsx +131 -0
- package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
- package/src/components/overlay/FloatingPanel.tsx +90 -0
- package/src/components/overlay/HoverCard.tsx +36 -0
- package/src/components/overlay/MediaLightbox.tsx +403 -0
- package/src/components/overlay/MediaPickerDialog.tsx +198 -0
- package/src/components/overlay/Modal.tsx +103 -0
- package/src/components/overlay/OnboardingFlow.tsx +172 -0
- package/src/components/overlay/Popover.tsx +36 -0
- package/src/components/overlay/ShareModal.tsx +324 -0
- package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
- package/src/components/overlay/Tooltip.tsx +130 -0
- package/src/components/overlay/generated/default-variant-keys.ts +14 -0
- package/src/components/overlay/generated/variant-keys.ts +17 -0
- package/src/components/patterns/BlogTemplate.tsx +46 -0
- package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
- package/src/components/patterns/DocsTemplate.tsx +41 -0
- package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
- package/src/components/patterns/OnboardingTemplate.tsx +32 -0
- package/src/components/patterns/PricingTemplate.tsx +106 -0
- package/src/globals.css +173 -22
- package/src/index.ts +177 -76
- package/tailwind-theme-extend.cjs +48 -3
- package/design/atoms-metadata.json +0 -82
- package/design/molecules-metadata.json +0 -130
- package/design/organisms-metadata.json +0 -38
- package/design/templates-metadata.json +0 -38
- package/src/components/atoms/Avatar.tsx +0 -57
- package/src/components/atoms/Select.tsx +0 -28
- package/src/components/atoms/generated/default-variant-keys.ts +0 -36
- package/src/components/molecules/AIChatInput.tsx +0 -140
- package/src/components/molecules/AIChatMessage.tsx +0 -109
- package/src/components/molecules/Accordion.tsx +0 -99
- package/src/components/molecules/Calendar.tsx +0 -60
- package/src/components/molecules/Carousel.tsx +0 -261
- package/src/components/molecules/Command.tsx +0 -152
- package/src/components/molecules/FilterButton.tsx +0 -133
- package/src/components/molecules/HoverCard.tsx +0 -29
- package/src/components/molecules/Modal.tsx +0 -66
- package/src/components/molecules/Popover.tsx +0 -31
- package/src/components/molecules/ProgressWidget.tsx +0 -40
- package/src/components/molecules/Resizable.tsx +0 -47
- package/src/components/molecules/ScrollArea.tsx +0 -48
- package/src/components/molecules/SidebarItem.tsx +0 -134
- package/src/components/molecules/Toast.tsx +0 -57
- package/src/components/molecules/Tooltip.tsx +0 -30
- package/src/components/molecules/generated/default-variant-keys.ts +0 -22
- package/src/components/molecules/generated/variant-keys.ts +0 -33
- package/src/components/organisms/CommandPalette.tsx +0 -58
- package/src/components/organisms/FloatingPanel.tsx +0 -46
- package/src/components/organisms/ShareModal.tsx +0 -182
- package/src/components/organisms/ToastProvider.tsx +0 -49
- /package/src/components/{atoms → display}/Kbd.tsx +0 -0
- /package/src/components/{atoms → display}/Separator.tsx +0 -0
- /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
- /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
- /package/src/components/{atoms → inputs}/Button.tsx +0 -0
- /package/src/components/{atoms → inputs}/Label.tsx +0 -0
- /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
- /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
- /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
- /package/src/components/{templates → patterns}/SettingsTemplate.tsx +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
IconArrowsMaximize as ArrowsMaximize,
|
|
6
|
+
IconArrowsMinimize as ArrowsMinimize,
|
|
7
|
+
} from "@tabler/icons-react"
|
|
8
|
+
|
|
9
|
+
import { cn } from "../../lib/utils"
|
|
10
|
+
import { TooltipButton } from "../inputs/TooltipButton"
|
|
11
|
+
import { Accordion } from "./Accordion"
|
|
12
|
+
import { accordionGroupDefaultVariantKey } from "./generated/default-variant-keys"
|
|
13
|
+
import type { AccordionGroupVariantKey } from "./generated/variant-keys"
|
|
14
|
+
import { Icon } from "./Icon"
|
|
15
|
+
|
|
16
|
+
export interface AccordionGroupProps
|
|
17
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "defaultValue" | "onChange"> {
|
|
18
|
+
/** Item values that should be included when the group expands all sections. */
|
|
19
|
+
values: string[]
|
|
20
|
+
/** Controlled open accordion item values. */
|
|
21
|
+
value?: string[]
|
|
22
|
+
/** Visual variant used by docs/design sync. */
|
|
23
|
+
variant?: AccordionGroupVariantKey
|
|
24
|
+
/** Initial open accordion item values for uncontrolled usage. */
|
|
25
|
+
defaultValue?: string[]
|
|
26
|
+
/** Called whenever open values change. */
|
|
27
|
+
onValueChange?: (value: string[]) => void
|
|
28
|
+
/** Group heading shown above the accordion. */
|
|
29
|
+
label?: React.ReactNode
|
|
30
|
+
/** Optional supporting text shown under the group heading. */
|
|
31
|
+
description?: React.ReactNode
|
|
32
|
+
/** Label and tooltip for the expand-all control. */
|
|
33
|
+
expandLabel?: string
|
|
34
|
+
/** Label and tooltip for the collapse-all control. */
|
|
35
|
+
collapseLabel?: string
|
|
36
|
+
/** Accessible label for the controls group. */
|
|
37
|
+
controlsLabel?: string
|
|
38
|
+
/** Whether to show the expand/collapse toggle control. */
|
|
39
|
+
showControls?: boolean
|
|
40
|
+
/** Class name forwarded to the inner Accordion root. */
|
|
41
|
+
accordionClassName?: string
|
|
42
|
+
children: React.ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const accordionGroupVariantClasses: Record<AccordionGroupVariantKey, string> = {
|
|
46
|
+
default: "space-y-3 p-0",
|
|
47
|
+
withDescription: "space-y-3 p-0",
|
|
48
|
+
withoutControls: "space-y-3 p-0",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const AccordionGroup = React.forwardRef<HTMLDivElement, AccordionGroupProps>(
|
|
52
|
+
(
|
|
53
|
+
{
|
|
54
|
+
values,
|
|
55
|
+
value,
|
|
56
|
+
variant = accordionGroupDefaultVariantKey,
|
|
57
|
+
defaultValue = [],
|
|
58
|
+
onValueChange,
|
|
59
|
+
label,
|
|
60
|
+
description,
|
|
61
|
+
expandLabel = "Open all",
|
|
62
|
+
collapseLabel = "Close all",
|
|
63
|
+
controlsLabel = "Accordion controls",
|
|
64
|
+
showControls = true,
|
|
65
|
+
accordionClassName,
|
|
66
|
+
className,
|
|
67
|
+
children,
|
|
68
|
+
...props
|
|
69
|
+
},
|
|
70
|
+
ref
|
|
71
|
+
) => {
|
|
72
|
+
const knownValues = React.useMemo(() => Array.from(new Set(values)), [values])
|
|
73
|
+
const isControlled = value !== undefined
|
|
74
|
+
const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue)
|
|
75
|
+
const openValues = value ?? internalValue
|
|
76
|
+
const allExpanded = knownValues.length > 0 && knownValues.every((item) => openValues.includes(item))
|
|
77
|
+
const controlLabel = allExpanded ? collapseLabel : expandLabel
|
|
78
|
+
const ControlIcon = allExpanded ? ArrowsMinimize : ArrowsMaximize
|
|
79
|
+
|
|
80
|
+
const setOpenValues = React.useCallback(
|
|
81
|
+
(nextValue: string[]) => {
|
|
82
|
+
const nextKnownValues = nextValue.filter((item) => knownValues.includes(item))
|
|
83
|
+
|
|
84
|
+
if (!isControlled) {
|
|
85
|
+
setInternalValue(nextKnownValues)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onValueChange?.(nextKnownValues)
|
|
89
|
+
},
|
|
90
|
+
[isControlled, knownValues, onValueChange]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const hasHeader = Boolean(label || description || showControls)
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div ref={ref} className={cn(accordionGroupVariantClasses[variant], className)} {...props}>
|
|
97
|
+
{hasHeader ? (
|
|
98
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
99
|
+
<div className="min-w-0 space-y-1">
|
|
100
|
+
{label ? (
|
|
101
|
+
<div className="text-sm font-medium leading-none text-foreground">
|
|
102
|
+
{label}
|
|
103
|
+
</div>
|
|
104
|
+
) : null}
|
|
105
|
+
{description ? (
|
|
106
|
+
<div className="text-sm leading-relaxed text-muted-foreground">
|
|
107
|
+
{description}
|
|
108
|
+
</div>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
{showControls ? (
|
|
112
|
+
<div
|
|
113
|
+
className="flex shrink-0 items-center gap-2"
|
|
114
|
+
role="group"
|
|
115
|
+
aria-label={controlsLabel}
|
|
116
|
+
>
|
|
117
|
+
<TooltipButton
|
|
118
|
+
type="button"
|
|
119
|
+
size="sm"
|
|
120
|
+
variant="outline"
|
|
121
|
+
tooltip={controlLabel}
|
|
122
|
+
onClick={() => setOpenValues(allExpanded ? [] : knownValues)}
|
|
123
|
+
>
|
|
124
|
+
<Icon icon={ControlIcon} size="sm" decorative />
|
|
125
|
+
<span className="grid">
|
|
126
|
+
<span aria-hidden className="invisible col-start-1 row-start-1 whitespace-nowrap">
|
|
127
|
+
{expandLabel}
|
|
128
|
+
</span>
|
|
129
|
+
<span aria-hidden className="invisible col-start-1 row-start-1 whitespace-nowrap">
|
|
130
|
+
{collapseLabel}
|
|
131
|
+
</span>
|
|
132
|
+
<span className="col-start-1 row-start-1 whitespace-nowrap">
|
|
133
|
+
{controlLabel}
|
|
134
|
+
</span>
|
|
135
|
+
</span>
|
|
136
|
+
</TooltipButton>
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
) : null}
|
|
141
|
+
<Accordion
|
|
142
|
+
type="multiple"
|
|
143
|
+
value={openValues}
|
|
144
|
+
onValueChange={setOpenValues}
|
|
145
|
+
className={cn("w-full", accordionClassName)}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</Accordion>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
AccordionGroup.displayName = "AccordionGroup"
|
|
154
|
+
|
|
155
|
+
export { AccordionGroup }
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { ColumnDef } from "@tanstack/react-table"
|
|
5
|
+
import {
|
|
6
|
+
IconBan as Ban,
|
|
7
|
+
IconCheck as Check,
|
|
8
|
+
IconDots as Dots,
|
|
9
|
+
IconPencil as Pencil,
|
|
10
|
+
IconTrash as Trash,
|
|
11
|
+
} from "@tabler/icons-react"
|
|
12
|
+
|
|
13
|
+
import { cn } from "../../lib/utils"
|
|
14
|
+
import { Button, type ButtonProps } from "../inputs/Button"
|
|
15
|
+
import { Checkbox } from "../inputs/Checkbox"
|
|
16
|
+
import { Select } from "../inputs/Select"
|
|
17
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../overlay/Tooltip"
|
|
18
|
+
import { Badge } from "./Badge"
|
|
19
|
+
import { DataTable, type DataTableLabels, type DataTableProps } from "./DataTable"
|
|
20
|
+
import { actionDataTableDefaultVariantKey } from "./generated/default-variant-keys"
|
|
21
|
+
import type { ActionDataTableVariantKey } from "./generated/variant-keys"
|
|
22
|
+
import { Icon, type IconGlyph } from "./Icon"
|
|
23
|
+
|
|
24
|
+
type ResolveValue<TData, TValue> = TValue | ((row: TData) => TValue)
|
|
25
|
+
type ResolveRowsValue<TData, TValue> = TValue | ((rows: TData[]) => TValue)
|
|
26
|
+
|
|
27
|
+
export interface ActionDataTableLabels extends DataTableLabels {
|
|
28
|
+
selectedRows?: (count: number) => string
|
|
29
|
+
selectedRowsLabel?: string
|
|
30
|
+
selectAllRows?: string
|
|
31
|
+
selectRow?: (label: string) => string
|
|
32
|
+
selectAllRowsSelected?: string
|
|
33
|
+
selectRowSelected?: (label: string) => string
|
|
34
|
+
clearSelection?: string
|
|
35
|
+
actions?: string
|
|
36
|
+
bulkActions?: string
|
|
37
|
+
bulkActionPlaceholder?: string
|
|
38
|
+
disabledAction?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ActionDataTableRowAction<TData> {
|
|
42
|
+
id: string
|
|
43
|
+
label: string
|
|
44
|
+
icon?: IconGlyph
|
|
45
|
+
variant?: ButtonProps["variant"]
|
|
46
|
+
disabled?: ResolveValue<TData, boolean>
|
|
47
|
+
disabledReason?: ResolveValue<TData, string>
|
|
48
|
+
onSelect?: (row: TData) => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ActionDataTableBulkAction<TData> {
|
|
52
|
+
id: string
|
|
53
|
+
label: string
|
|
54
|
+
icon?: IconGlyph
|
|
55
|
+
variant?: ButtonProps["variant"]
|
|
56
|
+
disabled?: ResolveRowsValue<TData, boolean>
|
|
57
|
+
disabledReason?: ResolveRowsValue<TData, string>
|
|
58
|
+
onSelect?: (rows: TData[]) => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ActionDataTableProps<TData, TValue>
|
|
62
|
+
extends Omit<DataTableProps<TData, TValue>, "columns" | "labels"> {
|
|
63
|
+
columns: ColumnDef<TData, TValue>[]
|
|
64
|
+
labels?: ActionDataTableLabels
|
|
65
|
+
getRowId?: (row: TData, index: number) => string
|
|
66
|
+
getRowLabel?: (row: TData, index: number) => string
|
|
67
|
+
variant?: ActionDataTableVariantKey
|
|
68
|
+
enableSelection?: boolean
|
|
69
|
+
rowActions?: ActionDataTableRowAction<TData>[]
|
|
70
|
+
bulkActions?: ActionDataTableBulkAction<TData>[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveValue<TData, TValue>(value: ResolveValue<TData, TValue> | undefined, row: TData) {
|
|
74
|
+
return typeof value === "function" ? (value as (row: TData) => TValue)(row) : value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveRowsValue<TData, TValue>(value: ResolveRowsValue<TData, TValue> | undefined, rows: TData[]) {
|
|
78
|
+
return typeof value === "function" ? (value as (rows: TData[]) => TValue)(rows) : value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ActionButton({
|
|
82
|
+
label,
|
|
83
|
+
disabled,
|
|
84
|
+
disabledReason,
|
|
85
|
+
icon,
|
|
86
|
+
variant = "ghost",
|
|
87
|
+
onClick,
|
|
88
|
+
}: {
|
|
89
|
+
label: string
|
|
90
|
+
disabled?: boolean
|
|
91
|
+
disabledReason?: string
|
|
92
|
+
icon?: IconGlyph
|
|
93
|
+
variant?: ButtonProps["variant"]
|
|
94
|
+
onClick?: () => void
|
|
95
|
+
}) {
|
|
96
|
+
const tooltip = disabled ? disabledReason ?? label : label
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Tooltip>
|
|
100
|
+
<TooltipTrigger asChild>
|
|
101
|
+
<span className="inline-flex">
|
|
102
|
+
<Button
|
|
103
|
+
type="button"
|
|
104
|
+
variant={variant}
|
|
105
|
+
size={icon ? "icon" : "sm"}
|
|
106
|
+
className={cn(icon && "h-8 w-8")}
|
|
107
|
+
disabled={disabled}
|
|
108
|
+
aria-label={label}
|
|
109
|
+
onClick={onClick}
|
|
110
|
+
>
|
|
111
|
+
{icon ? <Icon icon={icon} size="sm" /> : label}
|
|
112
|
+
</Button>
|
|
113
|
+
</span>
|
|
114
|
+
</TooltipTrigger>
|
|
115
|
+
<TooltipContent>{tooltip}</TooltipContent>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function BulkActionButton<TData>({
|
|
121
|
+
action,
|
|
122
|
+
rows,
|
|
123
|
+
onComplete,
|
|
124
|
+
}: {
|
|
125
|
+
action: ActionDataTableBulkAction<TData>
|
|
126
|
+
rows: TData[]
|
|
127
|
+
onComplete?: () => void
|
|
128
|
+
}) {
|
|
129
|
+
const disabled = rows.length === 0 || Boolean(resolveRowsValue(action.disabled, rows))
|
|
130
|
+
const disabledReason = resolveRowsValue(action.disabledReason, rows)
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<ActionButton
|
|
134
|
+
label={action.label}
|
|
135
|
+
icon={action.icon}
|
|
136
|
+
variant={action.variant ?? "outline"}
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
disabledReason={disabledReason}
|
|
139
|
+
onClick={() => {
|
|
140
|
+
action.onSelect?.(rows)
|
|
141
|
+
onComplete?.()
|
|
142
|
+
}}
|
|
143
|
+
/>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const actionDataTableVariantClasses: Record<ActionDataTableVariantKey, string> = {
|
|
148
|
+
default: "p-0",
|
|
149
|
+
rowActions: "p-0",
|
|
150
|
+
selection: "p-0",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function ActionDataTable<TData, TValue>({
|
|
154
|
+
columns,
|
|
155
|
+
data,
|
|
156
|
+
labels,
|
|
157
|
+
getRowId,
|
|
158
|
+
getRowLabel,
|
|
159
|
+
variant = actionDataTableDefaultVariantKey,
|
|
160
|
+
enableSelection = true,
|
|
161
|
+
rowActions,
|
|
162
|
+
bulkActions,
|
|
163
|
+
className,
|
|
164
|
+
...props
|
|
165
|
+
}: ActionDataTableProps<TData, TValue>) {
|
|
166
|
+
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(() => new Set())
|
|
167
|
+
const rowActionColumnSize = Math.max(120, (rowActions?.length ?? 0) * 40 + 24)
|
|
168
|
+
const getId = React.useCallback(
|
|
169
|
+
(row: TData, index: number) => getRowId?.(row, index) ?? String(index),
|
|
170
|
+
[getRowId]
|
|
171
|
+
)
|
|
172
|
+
const rowMeta = React.useMemo(
|
|
173
|
+
() =>
|
|
174
|
+
data.map((row, index) => ({
|
|
175
|
+
row,
|
|
176
|
+
id: getId(row, index),
|
|
177
|
+
label: getRowLabel?.(row, index) ?? String(index + 1),
|
|
178
|
+
})),
|
|
179
|
+
[data, getId, getRowLabel]
|
|
180
|
+
)
|
|
181
|
+
const selectedRows = React.useMemo(
|
|
182
|
+
() => rowMeta.filter((meta) => selectedIds.has(meta.id)).map((meta) => meta.row),
|
|
183
|
+
[rowMeta, selectedIds]
|
|
184
|
+
)
|
|
185
|
+
const allSelected = rowMeta.length > 0 && rowMeta.every((meta) => selectedIds.has(meta.id))
|
|
186
|
+
const partiallySelected = !allSelected && rowMeta.some((meta) => selectedIds.has(meta.id))
|
|
187
|
+
const bulkActionPlaceholder = labels?.bulkActionPlaceholder ?? labels?.bulkActions ?? "Bulk actions"
|
|
188
|
+
const selectAllLabel = allSelected
|
|
189
|
+
? labels?.selectAllRowsSelected ?? labels?.clearSelection ?? "Deselect all rows"
|
|
190
|
+
: labels?.selectAllRows ?? "Select all rows"
|
|
191
|
+
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
setSelectedIds((current) => {
|
|
194
|
+
const validIds = new Set(rowMeta.map((meta) => meta.id))
|
|
195
|
+
const next = new Set<string>()
|
|
196
|
+
let changed = false
|
|
197
|
+
for (const id of current) {
|
|
198
|
+
if (validIds.has(id)) {
|
|
199
|
+
next.add(id)
|
|
200
|
+
} else {
|
|
201
|
+
changed = true
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return changed || next.size !== current.size ? next : current
|
|
205
|
+
})
|
|
206
|
+
}, [rowMeta])
|
|
207
|
+
|
|
208
|
+
const actionColumns = React.useMemo<ColumnDef<TData, TValue>[]>(() => {
|
|
209
|
+
const nextColumns: ColumnDef<TData, TValue>[] = []
|
|
210
|
+
|
|
211
|
+
if (enableSelection) {
|
|
212
|
+
nextColumns.push({
|
|
213
|
+
id: "__action_select",
|
|
214
|
+
enableSorting: false,
|
|
215
|
+
size: 48,
|
|
216
|
+
minSize: 48,
|
|
217
|
+
maxSize: 48,
|
|
218
|
+
header: () => (
|
|
219
|
+
<Tooltip>
|
|
220
|
+
<TooltipTrigger asChild>
|
|
221
|
+
<span className="inline-flex">
|
|
222
|
+
<Checkbox
|
|
223
|
+
checked={allSelected}
|
|
224
|
+
aria-checked={partiallySelected ? "mixed" : allSelected}
|
|
225
|
+
aria-label={selectAllLabel}
|
|
226
|
+
className={cn(partiallySelected && "bg-foreground/60")}
|
|
227
|
+
onCheckedChange={(checked) => {
|
|
228
|
+
setSelectedIds(checked ? new Set(rowMeta.map((meta) => meta.id)) : new Set())
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
</span>
|
|
232
|
+
</TooltipTrigger>
|
|
233
|
+
<TooltipContent>{selectAllLabel}</TooltipContent>
|
|
234
|
+
</Tooltip>
|
|
235
|
+
),
|
|
236
|
+
cell: ({ row }) => {
|
|
237
|
+
const meta = rowMeta[row.index]
|
|
238
|
+
if (!meta) return null
|
|
239
|
+
const rowSelected = selectedIds.has(meta.id)
|
|
240
|
+
const rowSelectionLabel = rowSelected
|
|
241
|
+
? labels?.selectRowSelected?.(meta.label) ?? `Deselect ${meta.label}`
|
|
242
|
+
: labels?.selectRow?.(meta.label) ?? `Select ${meta.label}`
|
|
243
|
+
return (
|
|
244
|
+
<Tooltip>
|
|
245
|
+
<TooltipTrigger asChild>
|
|
246
|
+
<span className="inline-flex">
|
|
247
|
+
<Checkbox
|
|
248
|
+
checked={rowSelected}
|
|
249
|
+
aria-label={rowSelectionLabel}
|
|
250
|
+
onCheckedChange={(checked) => {
|
|
251
|
+
setSelectedIds((current) => {
|
|
252
|
+
const next = new Set(current)
|
|
253
|
+
if (checked) {
|
|
254
|
+
next.add(meta.id)
|
|
255
|
+
} else {
|
|
256
|
+
next.delete(meta.id)
|
|
257
|
+
}
|
|
258
|
+
return next
|
|
259
|
+
})
|
|
260
|
+
}}
|
|
261
|
+
/>
|
|
262
|
+
</span>
|
|
263
|
+
</TooltipTrigger>
|
|
264
|
+
<TooltipContent>{rowSelectionLabel}</TooltipContent>
|
|
265
|
+
</Tooltip>
|
|
266
|
+
)
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
nextColumns.push(...columns)
|
|
272
|
+
|
|
273
|
+
if (rowActions?.length) {
|
|
274
|
+
nextColumns.push({
|
|
275
|
+
id: "__action_row_actions",
|
|
276
|
+
enableSorting: false,
|
|
277
|
+
size: rowActionColumnSize,
|
|
278
|
+
minSize: rowActionColumnSize,
|
|
279
|
+
header: () => <span className="sr-only">{labels?.actions ?? "Actions"}</span>,
|
|
280
|
+
cell: ({ row }) => {
|
|
281
|
+
const meta = rowMeta[row.index]
|
|
282
|
+
if (!meta) return null
|
|
283
|
+
return (
|
|
284
|
+
<div className="flex items-center justify-end gap-1">
|
|
285
|
+
{rowActions.map((action) => {
|
|
286
|
+
const disabled = Boolean(resolveValue(action.disabled, meta.row))
|
|
287
|
+
return (
|
|
288
|
+
<ActionButton
|
|
289
|
+
key={action.id}
|
|
290
|
+
label={action.label}
|
|
291
|
+
icon={action.icon}
|
|
292
|
+
variant={action.variant}
|
|
293
|
+
disabled={disabled}
|
|
294
|
+
disabledReason={resolveValue(action.disabledReason, meta.row)}
|
|
295
|
+
onClick={() => action.onSelect?.(meta.row)}
|
|
296
|
+
/>
|
|
297
|
+
)
|
|
298
|
+
})}
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return nextColumns
|
|
306
|
+
}, [
|
|
307
|
+
allSelected,
|
|
308
|
+
columns,
|
|
309
|
+
enableSelection,
|
|
310
|
+
labels,
|
|
311
|
+
partiallySelected,
|
|
312
|
+
rowActionColumnSize,
|
|
313
|
+
rowActions,
|
|
314
|
+
rowMeta,
|
|
315
|
+
selectAllLabel,
|
|
316
|
+
selectedIds,
|
|
317
|
+
selectedRows,
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
const bulkToolbar = bulkActions?.length ? (
|
|
321
|
+
<div className="flex items-center gap-2">
|
|
322
|
+
<div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
|
|
323
|
+
<Badge variant={selectedRows.length > 0 ? "default" : "outline"}>
|
|
324
|
+
{selectedRows.length}
|
|
325
|
+
</Badge>
|
|
326
|
+
<span className="whitespace-nowrap">
|
|
327
|
+
{labels?.selectedRowsLabel ?? "selected"}
|
|
328
|
+
</span>
|
|
329
|
+
</div>
|
|
330
|
+
<div className="ml-auto flex shrink-0 items-center justify-end sm:ml-0">
|
|
331
|
+
<Tooltip>
|
|
332
|
+
<TooltipTrigger asChild>
|
|
333
|
+
<span className="inline-flex">
|
|
334
|
+
<Select
|
|
335
|
+
value=""
|
|
336
|
+
aria-label={bulkActionPlaceholder}
|
|
337
|
+
disabled={selectedRows.length === 0}
|
|
338
|
+
className="h-9 w-40 rounded-md py-1 text-sm"
|
|
339
|
+
onChange={(event) => {
|
|
340
|
+
const action = bulkActions.find((item) => item.id === event.target.value)
|
|
341
|
+
if (!action) return
|
|
342
|
+
const disabled = Boolean(resolveRowsValue(action.disabled, selectedRows))
|
|
343
|
+
if (disabled) return
|
|
344
|
+
action.onSelect?.(selectedRows)
|
|
345
|
+
setSelectedIds(new Set())
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<option value="">{bulkActionPlaceholder}</option>
|
|
349
|
+
{bulkActions.map((action) => (
|
|
350
|
+
<option
|
|
351
|
+
key={action.id}
|
|
352
|
+
value={action.id}
|
|
353
|
+
disabled={Boolean(resolveRowsValue(action.disabled, selectedRows))}
|
|
354
|
+
>
|
|
355
|
+
{action.label}
|
|
356
|
+
</option>
|
|
357
|
+
))}
|
|
358
|
+
</Select>
|
|
359
|
+
</span>
|
|
360
|
+
</TooltipTrigger>
|
|
361
|
+
<TooltipContent>
|
|
362
|
+
{selectedRows.length === 0
|
|
363
|
+
? labels?.disabledAction ?? "Select rows first"
|
|
364
|
+
: bulkActionPlaceholder}
|
|
365
|
+
</TooltipContent>
|
|
366
|
+
</Tooltip>
|
|
367
|
+
</div>
|
|
368
|
+
<div
|
|
369
|
+
className="ml-auto hidden shrink-0 items-center justify-end gap-1 sm:flex"
|
|
370
|
+
style={{ width: rowActionColumnSize }}
|
|
371
|
+
>
|
|
372
|
+
{bulkActions.map((action) => (
|
|
373
|
+
<BulkActionButton
|
|
374
|
+
key={action.id}
|
|
375
|
+
action={action}
|
|
376
|
+
rows={selectedRows}
|
|
377
|
+
onComplete={() => setSelectedIds(new Set())}
|
|
378
|
+
/>
|
|
379
|
+
))}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
) : null
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div className={cn("w-full space-y-3", actionDataTableVariantClasses[variant], className)}>
|
|
386
|
+
<DataTable
|
|
387
|
+
{...props}
|
|
388
|
+
columns={actionColumns}
|
|
389
|
+
data={data}
|
|
390
|
+
labels={labels}
|
|
391
|
+
headerActions={bulkToolbar}
|
|
392
|
+
getRowState={(_, index) => {
|
|
393
|
+
const meta = rowMeta[index]
|
|
394
|
+
return meta && selectedIds.has(meta.id) ? "selected" : undefined
|
|
395
|
+
}}
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ActionDataTable.displayName = "ActionDataTable"
|
|
402
|
+
|
|
403
|
+
export const actionDataTableDefaultRowActions = {
|
|
404
|
+
edit: Pencil,
|
|
405
|
+
delete: Trash,
|
|
406
|
+
more: Dots,
|
|
407
|
+
} as const
|
|
408
|
+
|
|
409
|
+
export const actionDataTableDefaultBulkActions = {
|
|
410
|
+
approve: Check,
|
|
411
|
+
reject: Ban,
|
|
412
|
+
delete: Trash,
|
|
413
|
+
} as const
|