tembro 6.0.3 → 6.1.1

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 (253) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/components/actions/action-menu.d.ts +11 -1
  3. package/dist/components/calendar/calendar-range-scheduler.cjs +1 -0
  4. package/dist/components/calendar/calendar-range-scheduler.d.ts +20 -0
  5. package/dist/components/calendar/calendar-range-scheduler.js +1 -0
  6. package/dist/components/calendar/date-time-picker.cjs +1 -0
  7. package/dist/components/calendar/date-time-picker.d.ts +15 -0
  8. package/dist/components/calendar/date-time-picker.js +1 -0
  9. package/dist/components/calendar/index.d.ts +2 -0
  10. package/dist/components/charts/chart-card.cjs +1 -0
  11. package/dist/components/charts/chart-card.d.ts +24 -0
  12. package/dist/components/charts/chart-card.js +1 -0
  13. package/dist/components/charts/index.d.ts +1 -0
  14. package/dist/components/command/advanced-command-menu.cjs +1 -0
  15. package/dist/components/command/advanced-command-menu.d.ts +25 -0
  16. package/dist/components/command/advanced-command-menu.js +1 -0
  17. package/dist/components/command/index.d.ts +1 -0
  18. package/dist/components/data-table/data-grid.cjs +1 -0
  19. package/dist/components/data-table/data-grid.d.ts +15 -0
  20. package/dist/components/data-table/data-grid.js +1 -0
  21. package/dist/components/data-table/index.d.ts +1 -0
  22. package/dist/components/data-table/public.d.ts +1 -0
  23. package/dist/components/display/index.d.ts +0 -2
  24. package/dist/components/feedback/async-boundary.cjs +1 -0
  25. package/dist/components/feedback/async-boundary.d.ts +12 -0
  26. package/dist/components/feedback/async-boundary.js +1 -0
  27. package/dist/components/feedback/index.d.ts +1 -0
  28. package/dist/components/filters/index.d.ts +0 -1
  29. package/dist/components/inputs/index.d.ts +1 -0
  30. package/dist/components/inputs/multi-select.cjs +1 -0
  31. package/dist/components/inputs/multi-select.d.ts +16 -0
  32. package/dist/components/inputs/multi-select.js +1 -0
  33. package/dist/components/inputs/public.d.ts +1 -0
  34. package/dist/components/inputs/time-picker.d.ts +11 -3
  35. package/dist/components/modern/file-manager.cjs +1 -0
  36. package/dist/components/modern/file-manager.d.ts +20 -0
  37. package/dist/components/modern/file-manager.js +1 -0
  38. package/dist/components/modern/index.d.ts +4 -1
  39. package/dist/components/modern/map.cjs +1 -0
  40. package/dist/components/modern/map.d.ts +29 -0
  41. package/dist/components/modern/map.js +1 -0
  42. package/dist/components/modern/media-player.cjs +1 -0
  43. package/dist/components/modern/media-player.d.ts +21 -0
  44. package/dist/components/modern/media-player.js +1 -0
  45. package/dist/components/modern/spreadsheet.cjs +1 -0
  46. package/dist/components/modern/spreadsheet.d.ts +35 -0
  47. package/dist/components/modern/spreadsheet.js +1 -0
  48. package/dist/components/notifications/action-toast.cjs +1 -0
  49. package/dist/components/notifications/action-toast.d.ts +16 -0
  50. package/dist/components/notifications/action-toast.js +1 -0
  51. package/dist/components/notifications/index.d.ts +1 -0
  52. package/dist/components/overlay/drawer.d.ts +3 -1
  53. package/dist/components/patterns/index.d.ts +1 -0
  54. package/dist/components/patterns/permissions-matrix.cjs +1 -0
  55. package/dist/components/patterns/permissions-matrix.d.ts +19 -0
  56. package/dist/components/patterns/permissions-matrix.js +1 -0
  57. package/dist/components/patterns/public.d.ts +1 -0
  58. package/dist/index.d.ts +0 -2
  59. package/dist/src/components/actions/action-menu.cjs +1 -1
  60. package/dist/src/components/actions/action-menu.js +47 -39
  61. package/dist/src/components/actions/copy-button.cjs +1 -1
  62. package/dist/src/components/actions/copy-button.js +4 -1
  63. package/dist/src/components/calendar/calendar-range-scheduler.cjs +1 -0
  64. package/dist/src/components/calendar/calendar-range-scheduler.js +75 -0
  65. package/dist/src/components/calendar/date-picker.cjs +1 -1
  66. package/dist/src/components/calendar/date-picker.js +5 -8
  67. package/dist/src/components/calendar/date-range-picker.cjs +1 -1
  68. package/dist/src/components/calendar/date-range-picker.js +12 -27
  69. package/dist/src/components/calendar/date-time-picker.cjs +1 -0
  70. package/dist/src/components/calendar/date-time-picker.js +36 -0
  71. package/dist/src/components/calendar/index.cjs +1 -1
  72. package/dist/src/components/calendar/index.js +5 -3
  73. package/dist/src/components/charts/chart-card.cjs +1 -0
  74. package/dist/src/components/charts/chart-card.js +55 -0
  75. package/dist/src/components/charts/index.cjs +1 -1
  76. package/dist/src/components/charts/index.js +3 -2
  77. package/dist/src/components/command/advanced-command-menu.cjs +1 -0
  78. package/dist/src/components/command/advanced-command-menu.js +57 -0
  79. package/dist/src/components/command/index.cjs +1 -1
  80. package/dist/src/components/command/index.js +3 -2
  81. package/dist/src/components/data-table/data-grid.cjs +1 -0
  82. package/dist/src/components/data-table/data-grid.js +32 -0
  83. package/dist/src/components/data-table/data-table-saved-filters.cjs +1 -1
  84. package/dist/src/components/data-table/data-table-saved-filters.js +11 -11
  85. package/dist/src/components/data-table/data-table.cjs +1 -1
  86. package/dist/src/components/data-table/data-table.js +175 -174
  87. package/dist/src/components/data-table/index.cjs +1 -1
  88. package/dist/src/components/data-table/index.js +15 -14
  89. package/dist/src/components/data-table/public.cjs +1 -1
  90. package/dist/src/components/data-table/public.js +3 -2
  91. package/dist/src/components/display/index.cjs +1 -1
  92. package/dist/src/components/display/index.js +9 -11
  93. package/dist/src/components/feedback/async-boundary.cjs +1 -0
  94. package/dist/src/components/feedback/async-boundary.js +22 -0
  95. package/dist/src/components/feedback/index.cjs +1 -1
  96. package/dist/src/components/feedback/index.js +2 -1
  97. package/dist/src/components/filters/index.cjs +1 -1
  98. package/dist/src/components/filters/index.js +2 -3
  99. package/dist/src/components/inputs/color-picker.cjs +1 -1
  100. package/dist/src/components/inputs/color-picker.js +63 -46
  101. package/dist/src/components/inputs/index.cjs +1 -1
  102. package/dist/src/components/inputs/index.js +13 -12
  103. package/dist/src/components/inputs/multi-select.cjs +1 -0
  104. package/dist/src/components/inputs/multi-select.js +80 -0
  105. package/dist/src/components/inputs/public.cjs +1 -1
  106. package/dist/src/components/inputs/public.js +13 -12
  107. package/dist/src/components/inputs/slider.cjs +1 -1
  108. package/dist/src/components/inputs/slider.js +3 -3
  109. package/dist/src/components/inputs/tag-input.cjs +1 -1
  110. package/dist/src/components/inputs/tag-input.js +7 -4
  111. package/dist/src/components/inputs/time-picker.cjs +1 -1
  112. package/dist/src/components/inputs/time-picker.js +90 -34
  113. package/dist/src/components/modern/file-manager.cjs +1 -0
  114. package/dist/src/components/modern/file-manager.js +75 -0
  115. package/dist/src/components/modern/index.cjs +1 -1
  116. package/dist/src/components/modern/index.js +7 -4
  117. package/dist/src/components/modern/map.cjs +1 -0
  118. package/dist/src/components/modern/map.js +158 -0
  119. package/dist/src/components/modern/media-player.cjs +1 -0
  120. package/dist/src/components/modern/media-player.js +176 -0
  121. package/dist/src/components/modern/resizable-panel.cjs +1 -1
  122. package/dist/src/components/modern/resizable-panel.js +39 -32
  123. package/dist/src/components/modern/spreadsheet.cjs +1 -0
  124. package/dist/src/components/modern/spreadsheet.js +97 -0
  125. package/dist/src/components/navigation/pagination.cjs +1 -1
  126. package/dist/src/components/navigation/pagination.js +5 -5
  127. package/dist/src/components/notifications/action-toast.cjs +1 -0
  128. package/dist/src/components/notifications/action-toast.js +49 -0
  129. package/dist/src/components/notifications/index.cjs +1 -1
  130. package/dist/src/components/notifications/index.js +3 -2
  131. package/dist/src/components/notifications/toast.cjs +1 -1
  132. package/dist/src/components/notifications/toast.js +68 -63
  133. package/dist/src/components/overlay/alert-dialog.cjs +1 -1
  134. package/dist/src/components/overlay/alert-dialog.js +8 -6
  135. package/dist/src/components/overlay/drawer.cjs +1 -1
  136. package/dist/src/components/overlay/drawer.js +38 -20
  137. package/dist/src/components/patterns/index.cjs +1 -1
  138. package/dist/src/components/patterns/index.js +2 -1
  139. package/dist/src/components/patterns/permissions-matrix.cjs +1 -0
  140. package/dist/src/components/patterns/permissions-matrix.js +43 -0
  141. package/dist/src/components/patterns/public.cjs +1 -1
  142. package/dist/src/components/patterns/public.js +2 -1
  143. package/dist/src/components/ui/checkbox/index.cjs +1 -1
  144. package/dist/src/components/ui/checkbox/index.js +2 -1
  145. package/dist/src/components/ui/dialog/index.cjs +1 -1
  146. package/dist/src/components/ui/dialog/index.js +8 -8
  147. package/dist/src/components/ui/dropdown-menu/index.cjs +1 -1
  148. package/dist/src/components/ui/dropdown-menu/index.js +21 -19
  149. package/dist/src/components/ui/input/group.cjs +1 -1
  150. package/dist/src/components/ui/input/group.js +8 -8
  151. package/dist/src/components/ui/popover/index.cjs +1 -1
  152. package/dist/src/components/ui/popover/index.js +10 -10
  153. package/dist/src/components/ui/tabs/index.cjs +1 -1
  154. package/dist/src/components/ui/tabs/index.js +6 -6
  155. package/dist/src/components/ui/toggle-group/index.cjs +1 -1
  156. package/dist/src/components/ui/toggle-group/index.js +2 -2
  157. package/dist/src/index.cjs +1 -1
  158. package/dist/src/index.js +111 -104
  159. package/dist/src/public-component-surface.cjs +1 -1
  160. package/dist/src/public-component-surface.js +59 -14
  161. package/package.json +1 -2
  162. package/packages/cli/dist/index.cjs +45 -165
  163. package/packages/cli/vendor/src/components/actions/action-menu.tsx +47 -19
  164. package/packages/cli/vendor/src/components/actions/copy-button.tsx +2 -2
  165. package/packages/cli/vendor/src/components/calendar/calendar-range-scheduler.tsx +72 -0
  166. package/packages/cli/vendor/src/components/calendar/date-picker.tsx +2 -5
  167. package/packages/cli/vendor/src/components/calendar/date-range-picker.tsx +5 -12
  168. package/packages/cli/vendor/src/components/calendar/date-time-picker.tsx +53 -0
  169. package/packages/cli/vendor/src/components/calendar/index.ts +5 -3
  170. package/packages/cli/vendor/src/components/charts/chart-card.tsx +81 -0
  171. package/packages/cli/vendor/src/components/charts/index.ts +1 -0
  172. package/packages/cli/vendor/src/components/command/advanced-command-menu.tsx +88 -0
  173. package/packages/cli/vendor/src/components/command/index.ts +2 -1
  174. package/packages/cli/vendor/src/components/data-table/data-grid.tsx +59 -0
  175. package/packages/cli/vendor/src/components/data-table/data-table.tsx +45 -34
  176. package/packages/cli/vendor/src/components/data-table/index.ts +3 -2
  177. package/packages/cli/vendor/src/components/data-table/public.ts +1 -0
  178. package/packages/cli/vendor/src/components/display/index.ts +3 -5
  179. package/packages/cli/vendor/src/components/feedback/async-boundary.tsx +36 -0
  180. package/packages/cli/vendor/src/components/feedback/index.ts +1 -0
  181. package/packages/cli/vendor/src/components/filters/index.ts +0 -1
  182. package/packages/cli/vendor/src/components/inputs/color-picker.tsx +15 -1
  183. package/packages/cli/vendor/src/components/inputs/index.ts +1 -0
  184. package/packages/cli/vendor/src/components/inputs/multi-select.tsx +75 -0
  185. package/packages/cli/vendor/src/components/inputs/public.ts +1 -0
  186. package/packages/cli/vendor/src/components/inputs/slider.tsx +3 -3
  187. package/packages/cli/vendor/src/components/inputs/tag-input.tsx +4 -4
  188. package/packages/cli/vendor/src/components/inputs/time-picker.tsx +91 -38
  189. package/packages/cli/vendor/src/components/modern/file-manager.tsx +79 -0
  190. package/packages/cli/vendor/src/components/modern/index.ts +4 -1
  191. package/packages/cli/vendor/src/components/modern/map.tsx +146 -0
  192. package/packages/cli/vendor/src/components/modern/media-player.tsx +182 -0
  193. package/packages/cli/vendor/src/components/modern/resizable-panel.tsx +15 -5
  194. package/packages/cli/vendor/src/components/modern/spreadsheet.tsx +155 -0
  195. package/packages/cli/vendor/src/components/navigation/pagination.tsx +9 -5
  196. package/packages/cli/vendor/src/components/notifications/action-toast.tsx +78 -0
  197. package/packages/cli/vendor/src/components/notifications/index.ts +2 -1
  198. package/packages/cli/vendor/src/components/notifications/toast.tsx +20 -10
  199. package/packages/cli/vendor/src/components/overlay/alert-dialog.tsx +3 -5
  200. package/packages/cli/vendor/src/components/overlay/drawer.tsx +22 -11
  201. package/packages/cli/vendor/src/components/patterns/index.ts +1 -0
  202. package/packages/cli/vendor/src/components/patterns/permissions-matrix.tsx +71 -0
  203. package/packages/cli/vendor/src/components/patterns/public.ts +1 -0
  204. package/packages/cli/vendor/src/components/ui/checkbox/index.tsx +3 -1
  205. package/packages/cli/vendor/src/components/ui/dialog/index.tsx +4 -4
  206. package/packages/cli/vendor/src/components/ui/dropdown-menu/index.tsx +18 -22
  207. package/packages/cli/vendor/src/components/ui/popover/index.tsx +4 -4
  208. package/packages/cli/vendor/src/components/ui/tabs/index.tsx +8 -3
  209. package/packages/cli/vendor/src/components/ui/toggle-group/index.tsx +6 -6
  210. package/packages/cli/vendor/src/index.ts +0 -2
  211. package/packages/cli/vendor/src/public-component-surface.ts +13 -4
  212. package/packages/cli/vendor/templates/showcase/src/showcase/data/registry.ts +6 -6
  213. package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +23 -8
  214. package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
  215. package/packages/cli/vendor/templates/showcase/src/showcase/sections/PatternsSection.tsx +1 -1
  216. package/packages/cli/vendor/templates/styles/globals.css +0 -120
  217. package/registry.json +3197 -2794
  218. package/dist/components/display/info-card.cjs +0 -1
  219. package/dist/components/display/info-card.d.ts +0 -51
  220. package/dist/components/display/info-card.js +0 -1
  221. package/dist/components/display/statistic.cjs +0 -1
  222. package/dist/components/display/statistic.d.ts +0 -25
  223. package/dist/components/display/statistic.js +0 -1
  224. package/dist/components/filters/filter-bar.cjs +0 -1
  225. package/dist/components/filters/filter-bar.d.ts +0 -28
  226. package/dist/components/filters/filter-bar.js +0 -1
  227. package/dist/components/modern/image-cropper.cjs +0 -1
  228. package/dist/components/modern/image-cropper.d.ts +0 -39
  229. package/dist/components/modern/image-cropper.js +0 -1
  230. package/dist/components/ui/number-field/index.cjs +0 -1
  231. package/dist/components/ui/number-field/index.d.ts +0 -18
  232. package/dist/components/ui/number-field/index.js +0 -1
  233. package/dist/components/ui/toolbar/index.cjs +0 -1
  234. package/dist/components/ui/toolbar/index.d.ts +0 -14
  235. package/dist/components/ui/toolbar/index.js +0 -1
  236. package/dist/src/components/display/info-card.cjs +0 -1
  237. package/dist/src/components/display/info-card.js +0 -124
  238. package/dist/src/components/display/statistic.cjs +0 -1
  239. package/dist/src/components/display/statistic.js +0 -104
  240. package/dist/src/components/filters/filter-bar.cjs +0 -1
  241. package/dist/src/components/filters/filter-bar.js +0 -103
  242. package/dist/src/components/modern/image-cropper.cjs +0 -1
  243. package/dist/src/components/modern/image-cropper.js +0 -143
  244. package/dist/src/components/ui/number-field/index.cjs +0 -1
  245. package/dist/src/components/ui/number-field/index.js +0 -73
  246. package/dist/src/components/ui/toolbar/index.cjs +0 -1
  247. package/dist/src/components/ui/toolbar/index.js +0 -50
  248. package/packages/cli/vendor/src/components/display/info-card.tsx +0 -195
  249. package/packages/cli/vendor/src/components/display/statistic.tsx +0 -114
  250. package/packages/cli/vendor/src/components/filters/filter-bar.tsx +0 -163
  251. package/packages/cli/vendor/src/components/modern/image-cropper.tsx +0 -226
  252. package/packages/cli/vendor/src/components/ui/number-field/index.tsx +0 -89
  253. package/packages/cli/vendor/src/components/ui/toolbar/index.tsx +0 -67
@@ -1 +1,2 @@
1
1
  export * from "./data-table"
2
+ export * from "./data-grid"
@@ -3,15 +3,13 @@ export * from "./activity-feed"
3
3
  export * from "./avatar"
4
4
  export * from "./kanban"
5
5
  export * from "./code-block"
6
- export * from "./data-state"
7
- export * from "./list"
8
- export * from "./progress"
9
- export * from "./statistic"
6
+ export * from "./data-state"
7
+ export * from "./list"
8
+ export * from "./progress"
10
9
  export * from "./status-dot"
11
10
  export * from "./status-legend"
12
11
  export * from "./timeline"
13
12
  export * from "./tree-view"
14
- export * from "./info-card"
15
13
  export * from "./virtual-list"
16
14
  export * from "./qr-code"
17
15
  export * from "./carousel"
@@ -0,0 +1,36 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { StateView } from "@/components/feedback/state-view"
6
+
7
+ export type AsyncBoundaryProps = React.PropsWithChildren<{
8
+ loading?: boolean
9
+ error?: unknown
10
+ empty?: boolean
11
+ loadingTitle?: React.ReactNode
12
+ errorTitle?: React.ReactNode
13
+ emptyTitle?: React.ReactNode
14
+ onRetry?: () => void
15
+ }>
16
+
17
+ function AsyncBoundary({
18
+ loading,
19
+ error,
20
+ empty,
21
+ loadingTitle = "Loading",
22
+ errorTitle = "Unable to load",
23
+ emptyTitle = "No data",
24
+ onRetry,
25
+ children,
26
+ }: AsyncBoundaryProps) {
27
+ if (loading) return <StateView status="loading" title={loadingTitle} loadingVariant="skeleton" />
28
+ if (error) {
29
+ const description = error instanceof Error ? error.message : "Try again or check the request."
30
+ return <StateView status="error" title={errorTitle} description={description} onRetry={onRetry} />
31
+ }
32
+ if (empty) return <StateView status="empty" title={emptyTitle} />
33
+ return <>{children}</>
34
+ }
35
+
36
+ export { AsyncBoundary }
@@ -1,2 +1,3 @@
1
1
  export * from "./alert"
2
2
  export * from "./state-view"
3
+ export * from "./async-boundary"
@@ -1,2 +1 @@
1
- export * from "./filter-bar"
2
1
  export * from "./saved-filter-select"
@@ -31,6 +31,14 @@ function withAlpha(value: string, alpha: number, includeAlpha: boolean) {
31
31
  return `${opaque}${channel}`
32
32
  }
33
33
 
34
+ function getAlphaTrackStyle(value: string): React.CSSProperties {
35
+ const opaque = getOpaqueColor(value)
36
+
37
+ return {
38
+ background: `linear-gradient(90deg, ${opaque}00 0%, ${opaque}ff 100%)`,
39
+ }
40
+ }
41
+
34
42
  export type ColorPickerProps = Omit<React.ComponentProps<"div">, "defaultValue" | "onChange"> & {
35
43
  value?: string
36
44
  defaultValue?: string
@@ -83,6 +91,11 @@ function ColorPicker({
83
91
  {label ? <div className="text-sm font-medium text-foreground">{label}</div> : null}
84
92
  <div className="flex min-w-0 items-center gap-2">
85
93
  <label className="relative size-10 shrink-0 overflow-hidden rounded-md border bg-background shadow-sm focus-within:ring-2 focus-within:ring-ring">
94
+ <span
95
+ aria-hidden="true"
96
+ className="absolute inset-0 bg-[linear-gradient(45deg,#e2e8f0_25%,transparent_25%),linear-gradient(-45deg,#e2e8f0_25%,transparent_25%),linear-gradient(45deg,transparent_75%,#e2e8f0_75%),linear-gradient(-45deg,transparent_75%,#e2e8f0_75%)] bg-[length:10px_10px] bg-[position:0_0,0_5px,5px_-5px,-5px_0]"
97
+ />
98
+ <span aria-hidden="true" className="absolute inset-0" style={{ backgroundColor: currentValue }} />
86
99
  <span className="sr-only">{labels?.color ?? "Choose color"}</span>
87
100
  <input
88
101
  type="color"
@@ -120,7 +133,8 @@ function ColorPicker({
120
133
  value={getAlphaPercent(currentValue)}
121
134
  disabled={disabled}
122
135
  aria-label={labels?.alpha ?? "Opacity"}
123
- className="w-full accent-foreground disabled:cursor-not-allowed"
136
+ style={getAlphaTrackStyle(currentValue)}
137
+ className="h-2 w-full cursor-pointer appearance-none rounded-full border border-border/70 outline-none focus-visible:ring-4 focus-visible:ring-ring/20 disabled:cursor-not-allowed [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-background [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:shadow-sm"
124
138
  onChange={(event) => updateValue(withAlpha(currentValue, Number(event.target.value), true))}
125
139
  />
126
140
  <span className="w-10 text-right tabular-nums">{getAlphaPercent(currentValue)}%</span>
@@ -1,4 +1,5 @@
1
1
  export * from "./async-select"
2
+ export * from "./multi-select"
2
3
  export * from "./combobox"
3
4
  export * from "./otp-input"
4
5
  export * from "./rating"
@@ -0,0 +1,75 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
5
+
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
9
+ import { cn } from "@/lib/utils"
10
+
11
+ export type MultiSelectOption = {
12
+ value: string
13
+ label: React.ReactNode
14
+ description?: React.ReactNode
15
+ disabled?: boolean
16
+ }
17
+
18
+ export type MultiSelectProps = React.ComponentProps<"div"> & {
19
+ options: MultiSelectOption[]
20
+ value?: string[]
21
+ onValueChange?: (value: string[]) => void
22
+ placeholder?: React.ReactNode
23
+ maxVisibleTags?: number
24
+ }
25
+
26
+ function MultiSelect({ options, value = [], onValueChange, placeholder = "Select options", maxVisibleTags = 3, className, ...props }: MultiSelectProps) {
27
+ const [open, setOpen] = React.useState(false)
28
+ const selected = options.filter((option) => value.includes(option.value))
29
+ const hiddenCount = Math.max(0, selected.length - maxVisibleTags)
30
+
31
+ const toggle = (optionValue: string) => {
32
+ onValueChange?.(value.includes(optionValue) ? value.filter((item) => item !== optionValue) : [...value, optionValue])
33
+ }
34
+
35
+ return (
36
+ <div data-slot="multi-select" className={cn("w-full", className)} {...props}>
37
+ <Popover open={open} onOpenChange={setOpen}>
38
+ <PopoverTrigger render={<Button type="button" variant="outline" className="min-h-10 w-full justify-between" />}>
39
+ <span className="flex min-w-0 flex-1 flex-wrap gap-1">
40
+ {selected.length ? selected.slice(0, maxVisibleTags).map((option) => (
41
+ <Badge key={option.value} label={option.label} variant="secondary" />
42
+ )) : <span className="text-muted-foreground">{placeholder}</span>}
43
+ {hiddenCount ? <Badge label={`+${hiddenCount}`} variant="secondary" /> : null}
44
+ </span>
45
+ <ChevronDownIcon className="size-4 opacity-70" />
46
+ </PopoverTrigger>
47
+ <PopoverContent align="start" className="w-[var(--anchor-width)] p-1">
48
+ <div className="grid gap-1">
49
+ {options.map((option) => {
50
+ const checked = value.includes(option.value)
51
+ return (
52
+ <button
53
+ key={option.value}
54
+ type="button"
55
+ disabled={option.disabled}
56
+ className="flex items-start gap-2 rounded-md px-2 py-2 text-left text-sm hover:bg-muted disabled:pointer-events-none disabled:opacity-50"
57
+ onClick={() => toggle(option.value)}
58
+ >
59
+ <span className="mt-0.5 grid size-4 place-items-center rounded border">{checked ? <CheckIcon className="size-3" /> : null}</span>
60
+ <span className="grid min-w-0 flex-1">
61
+ <span className="truncate font-medium">{option.label}</span>
62
+ {option.description ? <span className="truncate text-xs text-muted-foreground">{option.description}</span> : null}
63
+ </span>
64
+ </button>
65
+ )
66
+ })}
67
+ {value.length ? <Button type="button" size="sm" variant="ghost" leftIcon={<XIcon className="size-3.5" />} onClick={() => onValueChange?.([])}>Clear</Button> : null}
68
+ </div>
69
+ </PopoverContent>
70
+ </Popover>
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export { MultiSelect }
@@ -1,4 +1,5 @@
1
1
  export * from "./async-select"
2
+ export * from "./multi-select"
2
3
  export * from "./combobox"
3
4
  export * from "./otp-input"
4
5
  export * from "./rating"
@@ -83,7 +83,7 @@ function Slider({
83
83
  step={step}
84
84
  disabled={disabled}
85
85
  style={getSliderTrackStyle(currentValue, min, max)}
86
- className="h-3 w-full cursor-pointer appearance-none rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-control-bg)] shadow-[inset_0_1px_2px_rgba(15,23,42,0.08)] outline-none transition hover:brightness-[1.02] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-thumb]:transition [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
86
+ className="h-3 w-full cursor-pointer appearance-none rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-control-bg)] shadow-[inset_0_1px_2px_rgba(15,23,42,0.08)] outline-none transition hover:brightness-[1.02] focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-thumb]:transition [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
87
87
  onChange={(event) => updateValue(event.currentTarget.valueAsNumber)}
88
88
  {...props}
89
89
  />
@@ -170,7 +170,7 @@ function RangeSlider({
170
170
  max={max}
171
171
  step={step}
172
172
  disabled={disabled}
173
- className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
173
+ className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
174
174
  onChange={(event) => updateValue(0, event.currentTarget.valueAsNumber)}
175
175
  {...props}
176
176
  />
@@ -181,7 +181,7 @@ function RangeSlider({
181
181
  max={max}
182
182
  step={step}
183
183
  disabled={disabled}
184
- className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)]"
184
+ className="pointer-events-none absolute inset-0 h-6 w-full cursor-pointer appearance-none bg-transparent outline-none focus-visible:ring-4 focus-visible:ring-[color:color-mix(in_oklch,var(--aui-brand-strong),transparent_78%)] disabled:cursor-not-allowed disabled:opacity-50 [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-[3px] [&::-moz-range-thumb]:border-[color:var(--aui-page-bg)] [&::-moz-range-thumb]:bg-[color:var(--aui-brand-strong)] [&::-moz-range-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-transparent [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:mt-[-5px] [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-[3px] [&::-webkit-slider-thumb]:border-[color:var(--aui-page-bg)] [&::-webkit-slider-thumb]:bg-[color:var(--aui-brand-strong)] [&::-webkit-slider-thumb]:shadow-[0_8px_24px_rgba(16,185,129,0.24)] [&::-webkit-slider-thumb]:transition-transform hover:[&::-webkit-slider-thumb]:scale-105"
185
185
  onChange={(event) => updateValue(1, event.currentTarget.valueAsNumber)}
186
186
  {...props}
187
187
  />
@@ -80,13 +80,13 @@ function TagInput({
80
80
  <Badge
81
81
  key={tag}
82
82
  variant="secondary"
83
- className="h-8 gap-1 rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-page-bg)] px-3 pr-1 text-[13px] font-medium shadow-sm"
83
+ className="h-8 gap-1.5 rounded-full border border-[color:var(--aui-surface-border)] bg-[color:var(--aui-page-bg)] px-3 pr-1.5 text-[13px] font-medium leading-none shadow-sm"
84
84
  >
85
- {tag}
85
+ <span className="leading-none">{tag}</span>
86
86
  {!readOnly && !disabled && (
87
87
  <button
88
88
  type="button"
89
- className="grid size-5 shrink-0 place-items-center rounded-full p-0 leading-none text-muted-foreground transition hover:bg-[color:var(--aui-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
89
+ className="inline-flex size-5 shrink-0 items-center justify-center rounded-full p-0 leading-none text-muted-foreground transition hover:bg-[color:var(--aui-control-bg)] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
90
90
  aria-label={`Remove ${tag}`}
91
91
  onClick={(event) => {
92
92
  stopInteractivePropagation(event)
@@ -95,7 +95,7 @@ function TagInput({
95
95
  onMouseDown={stopInteractivePropagation}
96
96
  onDoubleClick={stopInteractivePropagation}
97
97
  >
98
- <XIcon className="size-3" strokeWidth={2} />
98
+ <XIcon className="size-3" strokeWidth={2.25} />
99
99
  </button>
100
100
  )}
101
101
  </Badge>
@@ -1,22 +1,31 @@
1
- import * as React from "react"
2
-
3
- import { cn } from "@/lib/utils"
4
-
5
- export type TimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
6
- label?: React.ReactNode
7
- }
8
-
9
- export type DateTimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
10
- label?: React.ReactNode
11
- }
1
+ import * as React from "react"
2
+ import { CalendarClockIcon, ClockIcon, XIcon } from "lucide-react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ export type TimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
7
+ label?: React.ReactNode
8
+ clearable?: boolean
9
+ onClear?: () => void
10
+ }
11
+
12
+ export type DateTimePickerProps = Omit<React.ComponentProps<"input">, "type"> & {
13
+ label?: React.ReactNode
14
+ clearable?: boolean
15
+ onClear?: () => void
16
+ }
12
17
 
13
18
  export type TimeRangePickerProps = React.ComponentProps<"div"> & {
14
19
  from?: string
15
20
  to?: string
16
21
  onFromChange?: (value: string) => void
17
- onToChange?: (value: string) => void
18
- disabled?: boolean
19
- }
22
+ onToChange?: (value: string) => void
23
+ disabled?: boolean
24
+ labels?: {
25
+ from?: React.ReactNode
26
+ to?: React.ReactNode
27
+ }
28
+ }
20
29
 
21
30
  function FieldShell({ label, className, children }: { label?: React.ReactNode; className?: string; children: React.ReactNode }) {
22
31
  return (
@@ -27,29 +36,73 @@ function FieldShell({ label, className, children }: { label?: React.ReactNode; c
27
36
  )
28
37
  }
29
38
 
30
- function TimePicker({ label, className, ...props }: TimePickerProps) {
31
- return (
32
- <FieldShell label={label}>
33
- <input type="time" className={cn("h-10 rounded-md border bg-background px-3 text-sm outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring", className)} {...props} />
34
- </FieldShell>
35
- )
36
- }
37
-
38
- function DateTimePicker({ label, className, ...props }: DateTimePickerProps) {
39
- return (
40
- <FieldShell label={label}>
41
- <input type="datetime-local" className={cn("h-10 rounded-md border bg-background px-3 text-sm outline-none ring-offset-background focus-visible:ring-2 focus-visible:ring-ring", className)} {...props} />
42
- </FieldShell>
43
- )
44
- }
45
-
46
- function TimeRangePicker({ from, to, onFromChange, onToChange, disabled, className, ...props }: TimeRangePickerProps) {
47
- return (
48
- <div data-slot="time-range-picker" className={cn("grid gap-2 sm:grid-cols-2", className)} {...props}>
49
- <TimePicker value={from} disabled={disabled} onChange={(event) => onFromChange?.(event.target.value)} />
50
- <TimePicker value={to} disabled={disabled} onChange={(event) => onToChange?.(event.target.value)} />
51
- </div>
52
- )
53
- }
39
+ function clearInput(event: React.MouseEvent<HTMLButtonElement>, onChange: React.ChangeEventHandler<HTMLInputElement> | undefined, onClear: (() => void) | undefined) {
40
+ event.preventDefault()
41
+ event.stopPropagation()
42
+ onChange?.({ target: { value: "" }, currentTarget: { value: "" } } as React.ChangeEvent<HTMLInputElement>)
43
+ onClear?.()
44
+ }
45
+
46
+ function TimePicker({ label, className, clearable = false, onClear, value, defaultValue, disabled, onChange, ...props }: TimePickerProps) {
47
+ const hasValue = value !== undefined ? String(value).length > 0 : defaultValue !== undefined && String(defaultValue).length > 0
48
+
49
+ return (
50
+ <FieldShell label={label}>
51
+ <span className="relative grid">
52
+ <ClockIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
53
+ <input
54
+ type="time"
55
+ value={value}
56
+ defaultValue={defaultValue}
57
+ disabled={disabled}
58
+ onChange={onChange}
59
+ className={cn("h-11 rounded-[var(--aui-control-radius,var(--radius-md))] border border-input/82 bg-background px-10 text-sm tabular-nums outline-none transition-[border-color,box-shadow] hover:border-ring/30 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/16 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground", className)}
60
+ {...props}
61
+ />
62
+ {clearable && hasValue && !disabled ? (
63
+ <button type="button" aria-label="Clear time" className="absolute right-2 top-1/2 grid size-7 -translate-y-1/2 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={(event) => clearInput(event, onChange, onClear)}>
64
+ <XIcon className="size-3.5" />
65
+ </button>
66
+ ) : null}
67
+ </span>
68
+ </FieldShell>
69
+ )
70
+ }
71
+
72
+ function DateTimePicker({ label, className, clearable = false, onClear, value, defaultValue, disabled, onChange, ...props }: DateTimePickerProps) {
73
+ const hasValue = value !== undefined ? String(value).length > 0 : defaultValue !== undefined && String(defaultValue).length > 0
74
+
75
+ return (
76
+ <FieldShell label={label}>
77
+ <span className="relative grid">
78
+ <CalendarClockIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
79
+ <input
80
+ type="datetime-local"
81
+ value={value}
82
+ defaultValue={defaultValue}
83
+ disabled={disabled}
84
+ onChange={onChange}
85
+ className={cn("h-11 rounded-[var(--aui-control-radius,var(--radius-md))] border border-input/82 bg-background px-10 text-sm tabular-nums outline-none transition-[border-color,box-shadow] hover:border-ring/30 focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/16 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground", className)}
86
+ {...props}
87
+ />
88
+ {clearable && hasValue && !disabled ? (
89
+ <button type="button" aria-label="Clear date and time" className="absolute right-2 top-1/2 grid size-7 -translate-y-1/2 place-items-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={(event) => clearInput(event, onChange, onClear)}>
90
+ <XIcon className="size-3.5" />
91
+ </button>
92
+ ) : null}
93
+ </span>
94
+ </FieldShell>
95
+ )
96
+ }
97
+
98
+ function TimeRangePicker({ from, to, onFromChange, onToChange, disabled, labels, className, ...props }: TimeRangePickerProps) {
99
+ return (
100
+ <div data-slot="time-range-picker" className={cn("grid gap-2 sm:grid-cols-[1fr_auto_1fr] sm:items-end", className)} {...props}>
101
+ <TimePicker label={labels?.from} value={from} clearable disabled={disabled} onChange={(event) => onFromChange?.(event.target.value)} onClear={() => onFromChange?.("")} />
102
+ <span className="hidden h-11 items-center text-sm text-muted-foreground sm:inline-flex">to</span>
103
+ <TimePicker label={labels?.to} value={to} clearable disabled={disabled} onChange={(event) => onToChange?.(event.target.value)} onClear={() => onToChange?.("")} />
104
+ </div>
105
+ )
106
+ }
54
107
 
55
108
  export { DateTimePicker, TimePicker, TimeRangePicker }
@@ -0,0 +1,79 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { FileIcon, FolderIcon, MoreHorizontalIcon } from "lucide-react"
5
+
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Input } from "@/components/ui/input"
9
+ import { ScrollBox } from "@/components/ui/scroll-box"
10
+ import { cn } from "@/lib/utils"
11
+
12
+ export type FileManagerItem = {
13
+ id: string
14
+ name: string
15
+ type: "file" | "folder"
16
+ size?: React.ReactNode
17
+ owner?: React.ReactNode
18
+ status?: React.ReactNode
19
+ }
20
+
21
+ export type FileManagerProps = Omit<React.ComponentProps<"div">, "onSelect"> & {
22
+ items: FileManagerItem[]
23
+ selectedId?: string
24
+ onSelect?: (item: FileManagerItem) => void
25
+ onOpen?: (item: FileManagerItem) => void
26
+ onSearchChange?: (value: string) => void
27
+ searchValue?: string
28
+ actions?: React.ReactNode
29
+ }
30
+
31
+ function FileManager({ items, selectedId, onSelect, onOpen, searchValue, onSearchChange, actions, className, ...props }: FileManagerProps) {
32
+ const [internalSearch, setInternalSearch] = React.useState("")
33
+ const query = searchValue ?? internalSearch
34
+ const filteredItems = items.filter((item) => item.name.toLowerCase().includes(query.toLowerCase()))
35
+
36
+ return (
37
+ <div data-slot="file-manager" className={cn("grid min-w-0 gap-3 rounded-lg border bg-card p-3", className)} {...props}>
38
+ <div className="flex flex-wrap items-center justify-between gap-2">
39
+ <Input
40
+ kind="search"
41
+ placeholder="Search files..."
42
+ value={query}
43
+ onValueChange={(value) => {
44
+ setInternalSearch(value)
45
+ onSearchChange?.(value)
46
+ }}
47
+ className="max-w-sm"
48
+ />
49
+ {actions}
50
+ </div>
51
+ <ScrollBox className="max-h-[420px]">
52
+ <div className="grid gap-1">
53
+ {filteredItems.map((item) => (
54
+ <button
55
+ key={item.id}
56
+ type="button"
57
+ data-selected={selectedId === item.id || undefined}
58
+ className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-md px-3 py-2 text-left transition hover:bg-muted/60 data-[selected=true]:bg-muted"
59
+ onClick={() => onSelect?.(item)}
60
+ onDoubleClick={() => onOpen?.(item)}
61
+ >
62
+ {item.type === "folder" ? <FolderIcon className="size-4 text-primary" /> : <FileIcon className="size-4 text-muted-foreground" />}
63
+ <span className="min-w-0">
64
+ <span className="block truncate text-sm font-medium">{item.name}</span>
65
+ <span className="text-xs text-muted-foreground">{item.owner ?? "Shared"} {item.size ? `- ${item.size}` : ""}</span>
66
+ </span>
67
+ <span className="flex items-center gap-2">
68
+ {item.status ? <Badge label={item.status} variant="soft" status="info" /> : null}
69
+ <Button type="button" variant="ghost" size="icon-xs" aria-label="File actions"><MoreHorizontalIcon /></Button>
70
+ </span>
71
+ </button>
72
+ ))}
73
+ </div>
74
+ </ScrollBox>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ export { FileManager }
@@ -2,4 +2,7 @@ export * from "./calendar-scheduler"
2
2
  export * from "./dual-list-picker"
3
3
  export * from "./resizable-panel"
4
4
  export * from "./rich-text-editor"
5
- export * from "./image-cropper"
5
+ export * from "./file-manager"
6
+ export * from "./map"
7
+ export * from "./media-player"
8
+ export * from "./spreadsheet"
@@ -0,0 +1,146 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { ExternalLinkIcon, LocateFixedIcon, MapPinIcon, MinusIcon, PlusIcon } from "lucide-react"
5
+
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Button } from "@/components/ui/button"
8
+ import { cn } from "@/lib/utils"
9
+
10
+ export type MapMarker = {
11
+ key: string
12
+ label: React.ReactNode
13
+ latitude: number
14
+ longitude: number
15
+ description?: React.ReactNode
16
+ tone?: "neutral" | "info" | "success" | "warning" | "danger" | "muted"
17
+ }
18
+
19
+ export type MapViewProps = React.ComponentProps<"section"> & {
20
+ center?: { latitude: number; longitude: number }
21
+ zoom?: number
22
+ markers?: MapMarker[]
23
+ title?: React.ReactNode
24
+ description?: React.ReactNode
25
+ height?: number | string
26
+ showMarkerList?: boolean
27
+ showControls?: boolean
28
+ iframeTitle?: string
29
+ mapUrl?: string
30
+ onMarkerSelect?: (marker: MapMarker) => void
31
+ selectedMarkerKey?: string
32
+ defaultSelectedMarkerKey?: string
33
+ }
34
+
35
+ function clampZoom(value: number) {
36
+ return Math.max(1, Math.min(19, value))
37
+ }
38
+
39
+ function toCoordinate(value: number) {
40
+ return Number.isFinite(value) ? value : 0
41
+ }
42
+
43
+ function getOpenStreetMapEmbedUrl(center: { latitude: number; longitude: number }, zoom: number, selected?: MapMarker) {
44
+ const lat = toCoordinate(selected?.latitude ?? center.latitude)
45
+ const lon = toCoordinate(selected?.longitude ?? center.longitude)
46
+ const delta = 0.012 * (20 - clampZoom(zoom))
47
+ const bbox = [lon - delta, lat - delta, lon + delta, lat + delta].map((value) => value.toFixed(6)).join("%2C")
48
+ return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${lat.toFixed(6)}%2C${lon.toFixed(6)}`
49
+ }
50
+
51
+ function getOpenStreetMapUrl(center: { latitude: number; longitude: number }, zoom: number, selected?: MapMarker) {
52
+ const lat = toCoordinate(selected?.latitude ?? center.latitude)
53
+ const lon = toCoordinate(selected?.longitude ?? center.longitude)
54
+ return `https://www.openstreetmap.org/?mlat=${lat.toFixed(6)}&mlon=${lon.toFixed(6)}#map=${clampZoom(zoom)}/${lat.toFixed(6)}/${lon.toFixed(6)}`
55
+ }
56
+
57
+ function MapView({
58
+ center = { latitude: 41.311081, longitude: 69.240562 },
59
+ zoom = 13,
60
+ markers = [],
61
+ title = "Map",
62
+ description,
63
+ height = 420,
64
+ showMarkerList = true,
65
+ showControls = true,
66
+ iframeTitle = "Interactive map",
67
+ mapUrl,
68
+ onMarkerSelect,
69
+ selectedMarkerKey,
70
+ defaultSelectedMarkerKey,
71
+ className,
72
+ ...props
73
+ }: MapViewProps) {
74
+ const [internalZoom, setInternalZoom] = React.useState(clampZoom(zoom))
75
+ const [internalSelectedKey, setInternalSelectedKey] = React.useState(defaultSelectedMarkerKey ?? markers[0]?.key)
76
+ const activeKey = selectedMarkerKey ?? internalSelectedKey
77
+ const selectedMarker = markers.find((marker) => marker.key === activeKey)
78
+ const resolvedUrl = mapUrl ?? getOpenStreetMapEmbedUrl(center, internalZoom, selectedMarker)
79
+ const externalUrl = getOpenStreetMapUrl(center, internalZoom, selectedMarker)
80
+
81
+ React.useEffect(() => {
82
+ setInternalZoom(clampZoom(zoom))
83
+ }, [zoom])
84
+
85
+ const selectMarker = (marker: MapMarker) => {
86
+ if (selectedMarkerKey === undefined) setInternalSelectedKey(marker.key)
87
+ onMarkerSelect?.(marker)
88
+ }
89
+
90
+ return (
91
+ <section data-slot="map-view" className={cn("grid min-w-0 gap-3 rounded-lg border bg-card p-3 shadow-sm", className)} {...props}>
92
+ <div className="flex flex-wrap items-start justify-between gap-3">
93
+ <div className="min-w-0">
94
+ {title ? <h3 className="text-base font-semibold tracking-tight">{title}</h3> : null}
95
+ {description ? <p className="mt-1 text-sm text-muted-foreground">{description}</p> : null}
96
+ </div>
97
+ {showControls ? (
98
+ <div className="flex items-center gap-1">
99
+ <Button type="button" size="icon-sm" variant="outline" iconOnly aria-label="Zoom out" onClick={() => setInternalZoom((value) => clampZoom(value - 1))}><MinusIcon /></Button>
100
+ <Badge variant="outline" tone="muted">{internalZoom}x</Badge>
101
+ <Button type="button" size="icon-sm" variant="outline" iconOnly aria-label="Zoom in" onClick={() => setInternalZoom((value) => clampZoom(value + 1))}><PlusIcon /></Button>
102
+ <Button type="button" size="icon-sm" variant="outline" iconOnly aria-label="Open in OpenStreetMap" onClick={() => window.open(externalUrl, "_blank", "noopener,noreferrer")}><ExternalLinkIcon /></Button>
103
+ </div>
104
+ ) : null}
105
+ </div>
106
+
107
+ <div className={cn("grid gap-3", showMarkerList && markers.length ? "lg:grid-cols-[minmax(0,1fr)_18rem]" : "")}>
108
+ <div className="relative overflow-hidden rounded-lg border bg-muted" style={{ height }}>
109
+ <iframe title={iframeTitle} src={resolvedUrl} className="absolute inset-0 size-full border-0" loading="lazy" referrerPolicy="no-referrer-when-downgrade" />
110
+ {selectedMarker ? (
111
+ <div className="absolute left-3 top-3 max-w-[calc(100%-1.5rem)] rounded-lg border bg-background/95 p-3 text-sm shadow-lg backdrop-blur">
112
+ <div className="flex items-center gap-2 font-medium"><LocateFixedIcon className="size-4 text-primary" />{selectedMarker.label}</div>
113
+ {selectedMarker.description ? <p className="mt-1 text-xs text-muted-foreground">{selectedMarker.description}</p> : null}
114
+ </div>
115
+ ) : null}
116
+ </div>
117
+
118
+ {showMarkerList && markers.length ? (
119
+ <div data-slot="map-marker-list" className="grid content-start gap-2">
120
+ {markers.map((marker) => {
121
+ const selected = marker.key === activeKey
122
+ return (
123
+ <button
124
+ key={marker.key}
125
+ type="button"
126
+ aria-pressed={selected}
127
+ className="grid gap-1 rounded-lg border bg-background p-3 text-left text-sm outline-none transition hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-primary aria-pressed:bg-primary/5"
128
+ onClick={() => selectMarker(marker)}
129
+ >
130
+ <span className="flex items-center justify-between gap-2">
131
+ <span className="flex min-w-0 items-center gap-2 font-medium"><MapPinIcon className="size-4 shrink-0 text-primary" /><span className="truncate">{marker.label}</span></span>
132
+ <Badge size="sm" tone={marker.tone ?? "muted"} showDot>{selected ? "selected" : "marker"}</Badge>
133
+ </span>
134
+ {marker.description ? <span className="text-xs text-muted-foreground">{marker.description}</span> : null}
135
+ <span className="text-[11px] text-muted-foreground">{marker.latitude.toFixed(4)}, {marker.longitude.toFixed(4)}</span>
136
+ </button>
137
+ )
138
+ })}
139
+ </div>
140
+ ) : null}
141
+ </div>
142
+ </section>
143
+ )
144
+ }
145
+
146
+ export { MapView }