@gunjo/ui 0.0.1-alpha.1 → 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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +90 -0
  3. package/README.md +52 -91
  4. package/package.json +47 -6
  5. package/src/components/display/Accordion.tsx +185 -0
  6. package/src/components/display/AccordionGroup.tsx +155 -0
  7. package/src/components/display/ActionDataTable.tsx +413 -0
  8. package/src/components/display/ActivityTimelineCard.tsx +483 -0
  9. package/src/components/display/AnalyticsCard.tsx +167 -0
  10. package/src/components/display/AssetCard.tsx +242 -0
  11. package/src/components/display/AssetGrid.tsx +164 -0
  12. package/src/components/display/Avatar.tsx +127 -0
  13. package/src/components/display/AvatarGroup.tsx +131 -0
  14. package/src/components/{atoms → display}/Badge.tsx +3 -3
  15. package/src/components/display/BarChart.tsx +247 -0
  16. package/src/components/{molecules → display}/Card.tsx +1 -1
  17. package/src/components/display/Carousel.tsx +593 -0
  18. package/src/components/display/ChartLegend.tsx +124 -0
  19. package/src/components/display/ChatMessage.tsx +382 -0
  20. package/src/components/display/ChoroplethMap.tsx +613 -0
  21. package/src/components/display/Code.tsx +42 -0
  22. package/src/components/display/CodeBlock.tsx +338 -0
  23. package/src/components/display/ColorSwatch.tsx +71 -0
  24. package/src/components/display/ConcentricProgressCard.tsx +545 -0
  25. package/src/components/display/DataTable.tsx +522 -0
  26. package/src/components/display/DistributionBar.tsx +102 -0
  27. package/src/components/display/DocNote.tsx +36 -0
  28. package/src/components/display/DonutChart.tsx +257 -0
  29. package/src/components/display/EmptyState.tsx +44 -0
  30. package/src/components/display/FileTree.tsx +180 -0
  31. package/src/components/display/GaugeChart.tsx +219 -0
  32. package/src/components/display/HeatmapChart.tsx +266 -0
  33. package/src/components/display/Icon.tsx +66 -0
  34. package/src/components/display/ImagePreview.tsx +140 -0
  35. package/src/components/{atoms → display}/Img.tsx +46 -12
  36. package/src/components/display/LabeledDonutCard.tsx +475 -0
  37. package/src/components/display/LineChart.tsx +464 -0
  38. package/src/components/{molecules → display}/List.tsx +20 -13
  39. package/src/components/display/MarkdownRenderer.tsx +157 -0
  40. package/src/components/display/MetadataList.tsx +81 -0
  41. package/src/components/display/MiniDistributionBarCard.tsx +314 -0
  42. package/src/components/display/PieChart.tsx +234 -0
  43. package/src/components/display/QuadrantMatrix.tsx +330 -0
  44. package/src/components/display/RadarChart.tsx +335 -0
  45. package/src/components/display/RadialBarChart.tsx +264 -0
  46. package/src/components/display/RetentionCohortCard.tsx +350 -0
  47. package/src/components/display/RibbonChart.tsx +618 -0
  48. package/src/components/display/SearchableAccordion.tsx +270 -0
  49. package/src/components/display/SegmentTimelineCard.tsx +452 -0
  50. package/src/components/display/SegmentedGaugeCard.tsx +607 -0
  51. package/src/components/display/Spacer.tsx +51 -0
  52. package/src/components/display/SparklineChart.tsx +394 -0
  53. package/src/components/display/StackedBarChart.tsx +393 -0
  54. package/src/components/display/Statistic.tsx +70 -0
  55. package/src/components/{molecules → display}/Table.tsx +22 -7
  56. package/src/components/display/Tag.tsx +80 -0
  57. package/src/components/display/TagEditor.tsx +141 -0
  58. package/src/components/display/Timeline.tsx +121 -0
  59. package/src/components/{atoms → display}/ToolPill.tsx +42 -18
  60. package/src/components/display/TreeView.tsx +226 -0
  61. package/src/components/display/chart-tooltip.tsx +423 -0
  62. package/src/components/display/chart-utils.ts +71 -0
  63. package/src/components/display/circular-chart-utils.ts +147 -0
  64. package/src/components/display/generated/default-variant-keys.ts +90 -0
  65. package/src/components/display/generated/variant-keys.ts +169 -0
  66. package/src/components/{atoms → feedback}/Alert.tsx +12 -5
  67. package/src/components/feedback/Banner.tsx +90 -0
  68. package/src/components/{molecules → feedback}/NotificationCenter.tsx +64 -31
  69. package/src/components/feedback/ProgressWidget.tsx +44 -0
  70. package/src/components/{atoms → feedback}/Spinner.tsx +2 -2
  71. package/src/components/{molecules → feedback}/StatusBar.tsx +4 -4
  72. package/src/components/feedback/StatusScreen.tsx +148 -0
  73. package/src/components/{molecules → feedback}/Stepper.tsx +10 -5
  74. package/src/components/feedback/Toast.tsx +108 -0
  75. package/src/components/feedback/ToastProvider.tsx +78 -0
  76. package/src/components/feedback/generated/default-variant-keys.ts +16 -0
  77. package/src/components/feedback/generated/variant-keys.ts +21 -0
  78. package/src/components/generated/component-manifest.ts +1568 -454
  79. package/src/components/generated/component-style-hints.ts +1958 -718
  80. package/src/components/{atoms → inputs}/ButtonVariants.ts +13 -3
  81. package/src/components/inputs/Calendar.tsx +212 -0
  82. package/src/components/inputs/ChatComposer.tsx +75 -0
  83. package/src/components/inputs/ChatInput.tsx +528 -0
  84. package/src/components/{atoms → inputs}/Checkbox.tsx +2 -2
  85. package/src/components/inputs/Combobox.tsx +175 -0
  86. package/src/components/inputs/CopyButton.tsx +187 -0
  87. package/src/components/inputs/DatePicker.tsx +519 -0
  88. package/src/components/inputs/DateRangePicker.tsx +878 -0
  89. package/src/components/inputs/EditableField.tsx +182 -0
  90. package/src/components/{organisms → inputs}/FileUploader.tsx +24 -9
  91. package/src/components/inputs/FilterButton.tsx +163 -0
  92. package/src/components/{molecules → inputs}/Form.tsx +20 -3
  93. package/src/components/{atoms → inputs}/Input.tsx +2 -0
  94. package/src/components/inputs/InputOTP.tsx +75 -0
  95. package/src/components/inputs/Mention.tsx +279 -0
  96. package/src/components/inputs/NumberInput.tsx +109 -0
  97. package/src/components/inputs/PasswordGroup.tsx +138 -0
  98. package/src/components/inputs/PasswordInput.tsx +74 -0
  99. package/src/components/inputs/PasswordRequirementList.tsx +96 -0
  100. package/src/components/inputs/PasswordStrengthMeter.tsx +93 -0
  101. package/src/components/inputs/PhoneInput.tsx +99 -0
  102. package/src/components/inputs/PostalCodeInput.tsx +98 -0
  103. package/src/components/inputs/RangeSlider.tsx +129 -0
  104. package/src/components/inputs/SearchInput.tsx +76 -0
  105. package/src/components/inputs/Select.tsx +39 -0
  106. package/src/components/{atoms → inputs}/Slider.tsx +18 -5
  107. package/src/components/{molecules → inputs}/SortButton.tsx +5 -2
  108. package/src/components/{atoms → inputs}/Switch.tsx +15 -4
  109. package/src/components/inputs/TagInput.tsx +114 -0
  110. package/src/components/{atoms → inputs}/Textarea.tsx +1 -0
  111. package/src/components/inputs/TimePicker.tsx +150 -0
  112. package/src/components/inputs/Toggle.tsx +48 -0
  113. package/src/components/{atoms → inputs}/ToggleGroup.tsx +2 -2
  114. package/src/components/inputs/TooltipButton.tsx +148 -0
  115. package/src/components/inputs/VoiceInputButton.tsx +317 -0
  116. package/src/components/inputs/calendar-holidays.ts +56 -0
  117. package/src/components/inputs/generated/default-variant-keys.ts +32 -0
  118. package/src/components/{atoms → inputs}/generated/variant-keys.ts +19 -27
  119. package/src/components/layout/AspectRatio.tsx +12 -0
  120. package/src/components/layout/AssetInspectorPanel.tsx +416 -0
  121. package/src/components/layout/Cluster.tsx +56 -0
  122. package/src/components/layout/CollapsiblePanelToggle.tsx +94 -0
  123. package/src/components/layout/Container.tsx +43 -0
  124. package/src/components/layout/DeviceFrame.tsx +227 -0
  125. package/src/components/layout/Grid.tsx +65 -0
  126. package/src/components/layout/HStack.tsx +73 -0
  127. package/src/components/{organisms → layout}/InspectorPanel.tsx +6 -5
  128. package/src/components/layout/MarqueeFrame.tsx +158 -0
  129. package/src/components/layout/Resizable.tsx +94 -0
  130. package/src/components/layout/ScrollArea.tsx +71 -0
  131. package/src/components/{organisms → layout}/SpatialCanvas.tsx +12 -7
  132. package/src/components/layout/VStack.tsx +69 -0
  133. package/src/components/layout/generated/default-variant-keys.ts +16 -0
  134. package/src/components/layout/generated/variant-keys.ts +21 -0
  135. package/src/components/{molecules → navigation}/Breadcrumb.tsx +5 -4
  136. package/src/components/navigation/Command.tsx +266 -0
  137. package/src/components/navigation/CommandPalette.tsx +83 -0
  138. package/src/components/navigation/DocumentPager.tsx +171 -0
  139. package/src/components/navigation/Footer.tsx +88 -0
  140. package/src/components/navigation/Header.tsx +80 -0
  141. package/src/components/{molecules → navigation}/Menubar.tsx +45 -12
  142. package/src/components/navigation/NavigationMenu.tsx +128 -0
  143. package/src/components/navigation/PageAside.tsx +84 -0
  144. package/src/components/{molecules → navigation}/Pagination.tsx +60 -7
  145. package/src/components/{organisms → navigation}/RightRail.tsx +1 -1
  146. package/src/components/navigation/Sidebar.tsx +223 -0
  147. package/src/components/navigation/SidebarItem.tsx +160 -0
  148. package/src/components/{molecules → navigation}/Tabs.tsx +2 -2
  149. package/src/components/navigation/TextLink.tsx +71 -0
  150. package/src/components/navigation/generated/default-variant-keys.ts +12 -0
  151. package/src/components/navigation/generated/variant-keys.ts +13 -0
  152. package/src/components/overlay/AIChatInput.tsx +5 -0
  153. package/src/components/overlay/AIChatMessage.tsx +6 -0
  154. package/src/components/overlay/AlertDialog.tsx +145 -0
  155. package/src/components/overlay/ChatPanel.tsx +180 -0
  156. package/src/components/{molecules → overlay}/ContextMenu.tsx +65 -29
  157. package/src/components/{molecules → overlay}/Dialog.tsx +21 -13
  158. package/src/components/overlay/Drawer.tsx +131 -0
  159. package/src/components/{molecules → overlay}/DropdownMenu.tsx +52 -17
  160. package/src/components/overlay/FloatingPanel.tsx +90 -0
  161. package/src/components/overlay/HoverCard.tsx +36 -0
  162. package/src/components/overlay/MediaLightbox.tsx +403 -0
  163. package/src/components/overlay/MediaPickerDialog.tsx +198 -0
  164. package/src/components/overlay/Modal.tsx +103 -0
  165. package/src/components/overlay/OnboardingFlow.tsx +172 -0
  166. package/src/components/overlay/Popover.tsx +36 -0
  167. package/src/components/overlay/ShareModal.tsx +324 -0
  168. package/src/components/{molecules → overlay}/Sheet.tsx +76 -19
  169. package/src/components/overlay/Tooltip.tsx +130 -0
  170. package/src/components/overlay/generated/default-variant-keys.ts +14 -0
  171. package/src/components/overlay/generated/variant-keys.ts +17 -0
  172. package/src/components/patterns/BlogTemplate.tsx +46 -0
  173. package/src/components/{templates → patterns}/DashboardTemplate.tsx +2 -2
  174. package/src/components/patterns/DocsTemplate.tsx +41 -0
  175. package/src/components/{templates → patterns}/MediaLibraryTemplate.tsx +1 -1
  176. package/src/components/patterns/OnboardingTemplate.tsx +32 -0
  177. package/src/components/patterns/PricingTemplate.tsx +106 -0
  178. package/src/globals.css +173 -22
  179. package/src/index.ts +177 -76
  180. package/tailwind-theme-extend.cjs +48 -3
  181. package/design/atoms-metadata.json +0 -82
  182. package/design/molecules-metadata.json +0 -130
  183. package/design/organisms-metadata.json +0 -38
  184. package/design/templates-metadata.json +0 -38
  185. package/src/components/atoms/Avatar.tsx +0 -57
  186. package/src/components/atoms/Select.tsx +0 -28
  187. package/src/components/atoms/generated/default-variant-keys.ts +0 -36
  188. package/src/components/molecules/AIChatInput.tsx +0 -140
  189. package/src/components/molecules/AIChatMessage.tsx +0 -109
  190. package/src/components/molecules/Accordion.tsx +0 -99
  191. package/src/components/molecules/Calendar.tsx +0 -60
  192. package/src/components/molecules/Carousel.tsx +0 -261
  193. package/src/components/molecules/Command.tsx +0 -152
  194. package/src/components/molecules/FilterButton.tsx +0 -133
  195. package/src/components/molecules/HoverCard.tsx +0 -29
  196. package/src/components/molecules/Modal.tsx +0 -66
  197. package/src/components/molecules/Popover.tsx +0 -31
  198. package/src/components/molecules/ProgressWidget.tsx +0 -40
  199. package/src/components/molecules/Resizable.tsx +0 -47
  200. package/src/components/molecules/ScrollArea.tsx +0 -48
  201. package/src/components/molecules/SidebarItem.tsx +0 -134
  202. package/src/components/molecules/Toast.tsx +0 -57
  203. package/src/components/molecules/Tooltip.tsx +0 -30
  204. package/src/components/molecules/generated/default-variant-keys.ts +0 -22
  205. package/src/components/molecules/generated/variant-keys.ts +0 -33
  206. package/src/components/organisms/CommandPalette.tsx +0 -58
  207. package/src/components/organisms/FloatingPanel.tsx +0 -46
  208. package/src/components/organisms/ShareModal.tsx +0 -182
  209. package/src/components/organisms/ToastProvider.tsx +0 -49
  210. /package/src/components/{atoms → display}/Kbd.tsx +0 -0
  211. /package/src/components/{atoms → display}/Separator.tsx +0 -0
  212. /package/src/components/{atoms → display}/Skeleton.tsx +0 -0
  213. /package/src/components/{atoms → feedback}/Progress.tsx +0 -0
  214. /package/src/components/{atoms → inputs}/Button.tsx +0 -0
  215. /package/src/components/{atoms → inputs}/Label.tsx +0 -0
  216. /package/src/components/{atoms → inputs}/RadioGroup.tsx +0 -0
  217. /package/src/components/{organisms → navigation}/AppRail.tsx +0 -0
  218. /package/src/components/{templates → patterns}/AuthTemplate.tsx +0 -0
  219. /package/src/components/{templates → patterns}/BannalyzeTemplate.tsx +0 -0
  220. /package/src/components/{templates → patterns}/ChatTemplate.tsx +0 -0
  221. /package/src/components/{templates → patterns}/EditorTemplate.tsx +0 -0
  222. /package/src/components/{templates → patterns}/KanbanTemplate.tsx +0 -0
  223. /package/src/components/{templates → patterns}/LandingTemplate.tsx +0 -0
  224. /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