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
@@ -0,0 +1,182 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { MaximizeIcon, PauseIcon, PlayIcon, RotateCcwIcon, Volume1Icon, Volume2Icon, VolumeXIcon } 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 MediaTrack = {
11
+ src: string
12
+ kind?: React.TrackHTMLAttributes<HTMLTrackElement>["kind"]
13
+ srcLang?: string
14
+ label?: string
15
+ default?: boolean
16
+ }
17
+
18
+ export type MediaPlayerProps = Omit<React.ComponentProps<"video">, "controls" | "children" | "onTimeUpdate" | "onVolumeChange"> & {
19
+ src: string
20
+ title?: React.ReactNode
21
+ description?: React.ReactNode
22
+ tracks?: MediaTrack[]
23
+ actions?: React.ReactNode
24
+ aspectRatio?: "video" | "square" | "wide" | string
25
+ showMeta?: boolean
26
+ onTimeChange?: (time: number, duration: number) => void
27
+ onVolumeValueChange?: (volume: number, muted: boolean) => void
28
+ }
29
+
30
+ function formatTime(seconds: number) {
31
+ if (!Number.isFinite(seconds) || seconds < 0) return "0:00"
32
+ const minutes = Math.floor(seconds / 60)
33
+ const rest = Math.floor(seconds % 60).toString().padStart(2, "0")
34
+ return `${minutes}:${rest}`
35
+ }
36
+
37
+ function getAspectRatioClassName(value: NonNullable<MediaPlayerProps["aspectRatio"]>) {
38
+ if (value === "video") return "aspect-video"
39
+ if (value === "square") return "aspect-square"
40
+ if (value === "wide") return "aspect-[21/9]"
41
+ return ""
42
+ }
43
+
44
+ function MediaPlayer({
45
+ src,
46
+ title,
47
+ description,
48
+ tracks = [],
49
+ actions,
50
+ aspectRatio = "video",
51
+ showMeta = true,
52
+ poster,
53
+ autoPlay,
54
+ muted: mutedProp,
55
+ loop,
56
+ playsInline = true,
57
+ preload = "metadata",
58
+ onTimeChange,
59
+ onVolumeValueChange,
60
+ className,
61
+ ...props
62
+ }: MediaPlayerProps) {
63
+ const ref = React.useRef<HTMLVideoElement>(null)
64
+ const [playing, setPlaying] = React.useState(Boolean(autoPlay))
65
+ const [duration, setDuration] = React.useState(0)
66
+ const [time, setTime] = React.useState(0)
67
+ const [volume, setVolume] = React.useState(1)
68
+ const [muted, setMuted] = React.useState(Boolean(mutedProp))
69
+ const [ready, setReady] = React.useState(false)
70
+
71
+ const syncFromElement = React.useCallback(() => {
72
+ const element = ref.current
73
+ if (!element) return
74
+ setPlaying(!element.paused)
75
+ setDuration(element.duration || 0)
76
+ setTime(element.currentTime || 0)
77
+ setVolume(element.volume)
78
+ setMuted(element.muted)
79
+ onTimeChange?.(element.currentTime || 0, element.duration || 0)
80
+ }, [onTimeChange])
81
+
82
+ const togglePlaying = async () => {
83
+ const element = ref.current
84
+ if (!element) return
85
+ if (element.paused) await element.play()
86
+ else element.pause()
87
+ syncFromElement()
88
+ }
89
+
90
+ const seek = (value: number) => {
91
+ const element = ref.current
92
+ if (!element) return
93
+ element.currentTime = value
94
+ setTime(value)
95
+ onTimeChange?.(value, duration)
96
+ }
97
+
98
+ const updateVolume = (nextVolume: number) => {
99
+ const element = ref.current
100
+ const safeVolume = Math.max(0, Math.min(1, nextVolume))
101
+ if (element) {
102
+ element.volume = safeVolume
103
+ element.muted = safeVolume === 0 ? true : muted
104
+ }
105
+ setVolume(safeVolume)
106
+ const nextMuted = safeVolume === 0 ? true : muted
107
+ setMuted(nextMuted)
108
+ onVolumeValueChange?.(safeVolume, nextMuted)
109
+ }
110
+
111
+ const toggleMuted = () => {
112
+ const element = ref.current
113
+ const nextMuted = !muted
114
+ if (element) element.muted = nextMuted
115
+ setMuted(nextMuted)
116
+ onVolumeValueChange?.(volume, nextMuted)
117
+ }
118
+
119
+ const VolumeIcon = muted || volume === 0 ? VolumeXIcon : volume < 0.6 ? Volume1Icon : Volume2Icon
120
+
121
+ return (
122
+ <section data-slot="media-player" className={cn("overflow-hidden rounded-lg border bg-card shadow-sm", className)}>
123
+ <div className={cn("relative bg-black", getAspectRatioClassName(aspectRatio))} style={!["video", "square", "wide"].includes(String(aspectRatio)) ? { aspectRatio: String(aspectRatio) } : undefined}>
124
+ <video
125
+ ref={ref}
126
+ src={src}
127
+ poster={poster}
128
+ autoPlay={autoPlay}
129
+ muted={muted}
130
+ loop={loop}
131
+ playsInline={playsInline}
132
+ preload={preload}
133
+ className="size-full object-cover"
134
+ onLoadedMetadata={() => { setReady(true); syncFromElement() }}
135
+ onPlay={() => setPlaying(true)}
136
+ onPause={() => setPlaying(false)}
137
+ onTimeUpdate={syncFromElement}
138
+ onVolumeChange={syncFromElement}
139
+ {...props}
140
+ >
141
+ {tracks.map((track) => <track key={`${track.src}-${track.srcLang ?? ""}`} {...track} />)}
142
+ </video>
143
+ <button type="button" aria-label={playing ? "Pause media" : "Play media"} className="absolute inset-0 grid place-items-center bg-black/0 text-white opacity-0 transition hover:bg-black/20 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" onClick={() => void togglePlaying()}>
144
+ <span className="grid size-14 place-items-center rounded-full bg-black/55 backdrop-blur">{playing ? <PauseIcon className="size-6" /> : <PlayIcon className="size-6 translate-x-0.5" />}</span>
145
+ </button>
146
+ </div>
147
+
148
+ <div className="grid gap-3 p-3">
149
+ {showMeta && (title || description || actions) ? (
150
+ <div className="flex flex-wrap items-start justify-between gap-3">
151
+ <div className="min-w-0">
152
+ {title ? <h3 className="truncate text-sm font-semibold">{title}</h3> : null}
153
+ {description ? <p className="mt-1 text-xs text-muted-foreground">{description}</p> : null}
154
+ </div>
155
+ <div className="flex items-center gap-2">
156
+ {!ready ? <Badge tone="muted" variant="soft">loading</Badge> : null}
157
+ {actions}
158
+ </div>
159
+ </div>
160
+ ) : null}
161
+
162
+ <div className="grid gap-2">
163
+ <input aria-label="Media progress" type="range" min={0} max={duration || 0} step={0.1} value={Math.min(time, duration || time)} onChange={(event) => seek(Number(event.target.value))} className="h-2 w-full cursor-pointer accent-primary" />
164
+ <div className="flex flex-wrap items-center justify-between gap-3">
165
+ <div className="flex items-center gap-1">
166
+ <Button type="button" size="icon-sm" variant="outline" iconOnly aria-label={playing ? "Pause" : "Play"} onClick={() => void togglePlaying()}>{playing ? <PauseIcon /> : <PlayIcon />}</Button>
167
+ <Button type="button" size="icon-sm" variant="ghost" iconOnly aria-label="Restart" onClick={() => seek(0)}><RotateCcwIcon /></Button>
168
+ <span className="min-w-20 text-xs tabular-nums text-muted-foreground">{formatTime(time)} / {formatTime(duration)}</span>
169
+ </div>
170
+ <div className="flex items-center gap-2">
171
+ <Button type="button" size="icon-sm" variant="ghost" iconOnly aria-label={muted ? "Unmute" : "Mute"} onClick={toggleMuted}><VolumeIcon /></Button>
172
+ <input aria-label="Volume" type="range" min={0} max={1} step={0.01} value={muted ? 0 : volume} onChange={(event) => updateVolume(Number(event.target.value))} className="h-2 w-24 cursor-pointer accent-primary" />
173
+ <Button type="button" size="icon-sm" variant="ghost" iconOnly aria-label="Fullscreen" onClick={() => ref.current?.requestFullscreen?.()}><MaximizeIcon /></Button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </section>
179
+ )
180
+ }
181
+
182
+ export { MediaPlayer }
@@ -123,7 +123,7 @@ function ResizablePanelGroup({ direction = "horizontal", className, children, ..
123
123
  ref={groupRef}
124
124
  data-slot="resizable-panel-group"
125
125
  data-direction={direction}
126
- className={cn("flex gap-2", direction === "horizontal" ? "flex-row items-stretch" : "flex-col", className)}
126
+ className={cn("flex", direction === "horizontal" ? "flex-row items-stretch" : "flex-col", className)}
127
127
  {...props}
128
128
  >
129
129
  {processedChildren}
@@ -140,7 +140,7 @@ function ResizablePanel({ className, style, defaultSize, minSize: _minSize, ...p
140
140
  return (
141
141
  <div
142
142
  data-slot="resizable-panel"
143
- className={cn("min-h-24 overflow-auto rounded-lg border bg-card p-3", className)}
143
+ className={cn("min-h-24 min-w-0 overflow-auto rounded-[var(--aui-card-radius,var(--radius-lg))] border border-[color:var(--aui-card-border,var(--border))] bg-card p-3 shadow-[var(--aui-card-shadow,0_10px_24px_rgba(15,23,42,0.07))]", className)}
144
144
  style={{ flexBasis: `${basis}%`, flexGrow: 0, flexShrink: 0, ...style }}
145
145
  {...props}
146
146
  />
@@ -157,9 +157,10 @@ function ResizableHandle({ className, ...props }: ResizableHandleProps) {
157
157
  type="button"
158
158
  data-slot="resizable-handle"
159
159
  aria-label="Resize panels"
160
+ aria-orientation={context?.direction ?? "horizontal"}
160
161
  className={cn(
161
- "relative flex shrink-0 items-center justify-center rounded-full border border-border/70 bg-background/85 text-muted-foreground shadow-sm transition-colors hover:border-ring/45 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
162
- context?.direction === "horizontal" ? "w-3 cursor-col-resize" : "h-3 cursor-row-resize",
162
+ "group relative flex shrink-0 items-center justify-center text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring",
163
+ context?.direction === "horizontal" ? "w-4 cursor-col-resize px-1" : "h-4 cursor-row-resize py-1",
163
164
  className
164
165
  )}
165
166
  onPointerDown={(event) => {
@@ -206,7 +207,16 @@ function ResizableHandle({ className, ...props }: ResizableHandleProps) {
206
207
  }}
207
208
  {...props}
208
209
  >
209
- <GripVerticalIcon className={cn("size-3", context?.direction === "vertical" && "rotate-90")} />
210
+ <span
211
+ aria-hidden="true"
212
+ className={cn(
213
+ "absolute rounded-full bg-border transition-colors group-hover:bg-ring/55",
214
+ context?.direction === "horizontal" ? "inset-y-2 left-1/2 w-px -translate-x-1/2" : "inset-x-2 top-1/2 h-px -translate-y-1/2"
215
+ )}
216
+ />
217
+ <span className="relative grid size-5 place-items-center rounded-full border border-border/75 bg-background shadow-sm transition-colors group-hover:border-ring/45">
218
+ <GripVerticalIcon className={cn("size-3", context?.direction === "vertical" && "rotate-90")} />
219
+ </span>
210
220
  </button>
211
221
  )
212
222
  }
@@ -0,0 +1,155 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { PlusIcon } from "lucide-react"
5
+
6
+ import { Button } from "@/components/ui/button"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ export type SpreadsheetColumn = {
10
+ key: string
11
+ label: React.ReactNode
12
+ width?: number | string
13
+ align?: "left" | "center" | "right"
14
+ readOnly?: boolean
15
+ formatter?: (value: string, row: SpreadsheetRow) => React.ReactNode
16
+ }
17
+
18
+ export type SpreadsheetRow = {
19
+ key: string
20
+ cells: Record<string, string>
21
+ readOnly?: boolean
22
+ }
23
+
24
+ export type SpreadsheetSelection = {
25
+ rowKey: string
26
+ columnKey: string
27
+ }
28
+
29
+ export type SpreadsheetProps = Omit<React.ComponentProps<"div">, "onChange"> & {
30
+ columns: SpreadsheetColumn[]
31
+ rows?: SpreadsheetRow[]
32
+ defaultRows?: SpreadsheetRow[]
33
+ onRowsChange?: (rows: SpreadsheetRow[], selection?: SpreadsheetSelection) => void
34
+ onCellChange?: (value: string, selection: SpreadsheetSelection, rows: SpreadsheetRow[]) => void
35
+ selectedCell?: SpreadsheetSelection
36
+ defaultSelectedCell?: SpreadsheetSelection
37
+ onSelectedCellChange?: (selection: SpreadsheetSelection) => void
38
+ readOnly?: boolean
39
+ showRowNumbers?: boolean
40
+ showAddRow?: boolean
41
+ addRowLabel?: React.ReactNode
42
+ empty?: React.ReactNode
43
+ }
44
+
45
+ function alignClassName(align: SpreadsheetColumn["align"]) {
46
+ if (align === "center") return "text-center"
47
+ if (align === "right") return "text-right"
48
+ return "text-left"
49
+ }
50
+
51
+ function createEmptyRow(columns: SpreadsheetColumn[], index: number): SpreadsheetRow {
52
+ return {
53
+ key: `row-${Date.now()}-${index}`,
54
+ cells: Object.fromEntries(columns.map((column) => [column.key, ""])),
55
+ }
56
+ }
57
+
58
+ function Spreadsheet({
59
+ columns,
60
+ rows: rowsProp,
61
+ defaultRows = [],
62
+ onRowsChange,
63
+ onCellChange,
64
+ selectedCell,
65
+ defaultSelectedCell,
66
+ onSelectedCellChange,
67
+ readOnly = false,
68
+ showRowNumbers = true,
69
+ showAddRow = true,
70
+ addRowLabel = "Add row",
71
+ empty = "No rows yet.",
72
+ className,
73
+ ...props
74
+ }: SpreadsheetProps) {
75
+ const [internalRows, setInternalRows] = React.useState(defaultRows)
76
+ const [internalSelectedCell, setInternalSelectedCell] = React.useState<SpreadsheetSelection | undefined>(defaultSelectedCell)
77
+ const rows = rowsProp ?? internalRows
78
+ const activeCell = selectedCell ?? internalSelectedCell
79
+
80
+ const commitRows = (nextRows: SpreadsheetRow[], selection?: SpreadsheetSelection) => {
81
+ if (rowsProp === undefined) setInternalRows(nextRows)
82
+ onRowsChange?.(nextRows, selection)
83
+ }
84
+
85
+ const selectCell = (selection: SpreadsheetSelection) => {
86
+ if (selectedCell === undefined) setInternalSelectedCell(selection)
87
+ onSelectedCellChange?.(selection)
88
+ }
89
+
90
+ const updateCell = (selection: SpreadsheetSelection, value: string) => {
91
+ const nextRows = rows.map((row) => row.key === selection.rowKey ? { ...row, cells: { ...row.cells, [selection.columnKey]: value } } : row)
92
+ commitRows(nextRows, selection)
93
+ onCellChange?.(value, selection, nextRows)
94
+ }
95
+
96
+ const addRow = () => {
97
+ const nextRows = [...rows, createEmptyRow(columns, rows.length + 1)]
98
+ commitRows(nextRows)
99
+ }
100
+
101
+ return (
102
+ <div data-slot="spreadsheet" className={cn("grid min-w-0 gap-3 rounded-lg border bg-card p-3 shadow-sm", className)} {...props}>
103
+ <div className="overflow-auto rounded-lg border">
104
+ <table className="w-full min-w-[48rem] border-collapse text-sm">
105
+ <thead className="sticky top-0 z-10 bg-muted/80 backdrop-blur">
106
+ <tr>
107
+ {showRowNumbers ? <th className="w-12 border-b border-r px-3 py-2 text-left text-xs font-medium text-muted-foreground">#</th> : null}
108
+ {columns.map((column) => (
109
+ <th key={column.key} className={cn("border-b border-r px-3 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground last:border-r-0", alignClassName(column.align))} style={{ width: column.width }}>
110
+ {column.label}
111
+ </th>
112
+ ))}
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ {rows.length ? rows.map((row, rowIndex) => (
117
+ <tr key={row.key} data-readonly={row.readOnly || undefined} className="odd:bg-background even:bg-muted/18">
118
+ {showRowNumbers ? <th className="border-r px-3 py-1.5 text-left text-xs font-medium text-muted-foreground">{rowIndex + 1}</th> : null}
119
+ {columns.map((column) => {
120
+ const selection = { rowKey: row.key, columnKey: column.key }
121
+ const selected = activeCell?.rowKey === row.key && activeCell.columnKey === column.key
122
+ const value = row.cells[column.key] ?? ""
123
+ const cellReadOnly = readOnly || row.readOnly || column.readOnly
124
+ return (
125
+ <td key={column.key} data-selected={selected || undefined} className="border-r border-t p-0 last:border-r-0 data-[selected=true]:ring-2 data-[selected=true]:ring-inset data-[selected=true]:ring-primary/55">
126
+ {cellReadOnly && column.formatter ? (
127
+ <div className={cn("min-h-10 px-3 py-2", alignClassName(column.align))}>{column.formatter(value, row)}</div>
128
+ ) : (
129
+ <input
130
+ aria-label={`${String(column.label)} row ${rowIndex + 1}`}
131
+ value={value}
132
+ readOnly={cellReadOnly}
133
+ className={cn("h-10 w-full bg-transparent px-3 outline-none transition focus:bg-background read-only:cursor-default", alignClassName(column.align))}
134
+ onFocus={() => selectCell(selection)}
135
+ onChange={(event) => updateCell(selection, event.target.value)}
136
+ />
137
+ )}
138
+ </td>
139
+ )
140
+ })}
141
+ </tr>
142
+ )) : (
143
+ <tr>
144
+ <td colSpan={columns.length + (showRowNumbers ? 1 : 0)} className="h-32 text-center text-sm text-muted-foreground">{empty}</td>
145
+ </tr>
146
+ )}
147
+ </tbody>
148
+ </table>
149
+ </div>
150
+ {showAddRow && !readOnly ? <Button type="button" className="w-fit" variant="outline" size="sm" leftIcon={<PlusIcon />} onClick={addRow}>{addRowLabel}</Button> : null}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ export { Spreadsheet }
@@ -105,13 +105,13 @@ function Pagination({
105
105
  data-slot="pagination"
106
106
  aria-label="Pagination"
107
107
  className={cn(
108
- "flex flex-wrap items-center justify-center gap-1 rounded-full border border-border/70 bg-background/76 p-1 shadow-none backdrop-blur",
108
+ "flex flex-wrap items-center justify-center gap-1.5 rounded-[var(--radius-lg)] border border-border/72 bg-background/84 p-1.5 shadow-[0_1px_2px_rgba(15,23,42,0.04)] backdrop-blur",
109
109
  className
110
110
  )}
111
111
  {...props}
112
112
  >
113
113
  {showSummary && totalCount !== undefined && pageSize ? (
114
- <div className="px-2 text-xs text-muted-foreground">
114
+ <div className="mr-1 rounded-[var(--radius-md)] bg-muted/55 px-2.5 py-1.5 text-xs font-medium text-muted-foreground">
115
115
  {Math.min((currentPage - 1) * pageSize + 1, totalCount)}-{Math.min(currentPage * pageSize, totalCount)} of {totalCount}
116
116
  </div>
117
117
  ) : null}
@@ -163,7 +163,11 @@ function Pagination({
163
163
  aria-current={item === currentPage ? "page" : undefined}
164
164
  aria-label={labels?.page?.(item) ?? `Page ${item}`}
165
165
  onClick={() => goToPage(item)}
166
- className={cn(item !== currentPage && "border-border/70 bg-background/80 shadow-none")}
166
+ className={cn(
167
+ "tabular-nums",
168
+ item === currentPage && "shadow-[0_8px_20px_rgba(15,23,42,0.14)]",
169
+ item !== currentPage && "border-border/70 bg-background/80 shadow-none hover:bg-muted/70"
170
+ )}
167
171
  >
168
172
  {item}
169
173
  </Button>
@@ -194,10 +198,10 @@ function Pagination({
194
198
  </Button>
195
199
  )}
196
200
  {pageSizeOptions && pageSize && onPageSizeChange ? (
197
- <label className="ml-1 inline-flex items-center gap-2 rounded-full border border-border/70 bg-background/90 px-3 py-1 text-xs text-muted-foreground">
201
+ <label className="ml-1 inline-flex h-9 items-center gap-2 rounded-[var(--radius-md)] border border-border/70 bg-background/90 px-3 text-xs font-medium text-muted-foreground">
198
202
  <span>Rows</span>
199
203
  <select
200
- className="bg-transparent text-foreground outline-none"
204
+ className="rounded-sm bg-transparent text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
201
205
  value={pageSize}
202
206
  onChange={(event) => onPageSizeChange(Number(event.target.value))}
203
207
  disabled={disabled}
@@ -0,0 +1,78 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { RotateCcwIcon } from "lucide-react"
5
+
6
+ import { Button } from "@/components/ui/button"
7
+ import { useToast, type CreateToastInput, type ToastShortcutInput } from "@/components/notifications/toast"
8
+
9
+ export type ActionToastOptions = Omit<CreateToastInput, "action"> & {
10
+ actionLabel?: React.ReactNode
11
+ onAction?: () => void
12
+ undoLabel?: React.ReactNode
13
+ onUndo?: () => void
14
+ }
15
+
16
+ export type ActionToastButtonProps = React.ComponentProps<typeof Button> & {
17
+ toast: ActionToastOptions
18
+ }
19
+
20
+ function ActionToastButton({ toast, children, onClick, ...props }: ActionToastButtonProps) {
21
+ const toaster = useToast()
22
+
23
+ return (
24
+ <Button
25
+ {...props}
26
+ onClick={(event) => {
27
+ onClick?.(event)
28
+ if (event.defaultPrevented) return
29
+ showActionToast(toaster, toast)
30
+ }}
31
+ >
32
+ {children}
33
+ </Button>
34
+ )
35
+ }
36
+
37
+ function ActionToastUndoButton({ onUndo, label = "Undo" }: { onUndo?: () => void; label?: React.ReactNode }) {
38
+ return (
39
+ <Button type="button" size="sm" variant="outline" leftIcon={<RotateCcwIcon className="size-3.5" />} onClick={onUndo}>
40
+ {label}
41
+ </Button>
42
+ )
43
+ }
44
+
45
+ function showActionToast(
46
+ toaster: ReturnType<typeof useToast>,
47
+ {
48
+ actionLabel = "Action",
49
+ onAction,
50
+ undoLabel,
51
+ onUndo,
52
+ ...toast
53
+ }: ActionToastOptions
54
+ ) {
55
+ const action = onUndo ? (
56
+ <ActionToastUndoButton onUndo={onUndo} label={undoLabel} />
57
+ ) : onAction ? (
58
+ <Button type="button" size="sm" variant="outline" onClick={onAction}>{actionLabel}</Button>
59
+ ) : undefined
60
+
61
+ return toaster.addToast({ ...toast, action })
62
+ }
63
+
64
+ function useActionToast() {
65
+ const toaster = useToast()
66
+
67
+ return React.useCallback(
68
+ (toast: ActionToastOptions | ToastShortcutInput) => {
69
+ if (typeof toast === "object" && toast !== null && !React.isValidElement(toast) && !Array.isArray(toast)) {
70
+ return showActionToast(toaster, toast as ActionToastOptions)
71
+ }
72
+ return toaster.info(toast)
73
+ },
74
+ [toaster]
75
+ )
76
+ }
77
+
78
+ export { ActionToastButton, showActionToast, useActionToast }
@@ -1,2 +1,3 @@
1
- export * from "./toast"
1
+ export * from "./toast"
2
2
  export * from "./notification-center"
3
+ export * from "./action-toast"
@@ -1,7 +1,8 @@
1
1
  "use client"
2
2
 
3
- import * as React from "react"
4
- import { AlertCircleIcon, CheckCircle2Icon, InfoIcon, Loader2Icon, TriangleAlertIcon, XIcon } from "lucide-react"
3
+ import * as React from "react"
4
+ import { createPortal } from "react-dom"
5
+ import { AlertCircleIcon, CheckCircle2Icon, InfoIcon, Loader2Icon, TriangleAlertIcon, XIcon } from "lucide-react"
5
6
 
6
7
  import { Button } from "@/components/ui/button"
7
8
  import { cn } from "@/lib/utils"
@@ -108,7 +109,12 @@ function ToastProvider({
108
109
  pauseOnHover = true,
109
110
  position = "top-right",
110
111
  }: ToastProviderProps) {
111
- const [toasts, setToasts] = React.useState<ToastItem[]>([])
112
+ const [toasts, setToasts] = React.useState<ToastItem[]>([])
113
+ const [portalTarget, setPortalTarget] = React.useState<HTMLElement | null>(null)
114
+
115
+ React.useEffect(() => {
116
+ setPortalTarget(document.body)
117
+ }, [])
112
118
 
113
119
  const dismissToast = React.useCallback((id: string) => {
114
120
  setToasts((current) => current.filter((toast) => toast.id !== id))
@@ -187,17 +193,21 @@ function ToastProvider({
187
193
  [addToast, clearToasts, dismissToast, error, info, loading, promise, success, toasts, updateToast, warning]
188
194
  )
189
195
 
190
- return (
191
- <ToastContext.Provider value={value}>
192
- {children}
196
+ const viewport = (
193
197
  <div
194
198
  data-slot="toast-viewport"
195
- className={cn("fixed z-[100] flex w-[min(100%-2rem,24rem)] flex-col gap-2", positionClassName[position])}
199
+ className={cn("fixed z-[100] flex w-[min(calc(100vw-2rem),24rem)] flex-col gap-2 pointer-events-none", positionClassName[position])}
196
200
  >
197
201
  {toasts.map((toast) => (
198
202
  <ToastCard key={toast.id} toast={toast} pauseOnHover={pauseOnHover} onDismiss={dismissToast} />
199
203
  ))}
200
204
  </div>
205
+ )
206
+
207
+ return (
208
+ <ToastContext.Provider value={value}>
209
+ {children}
210
+ {portalTarget ? createPortal(viewport, portalTarget) : null}
201
211
  </ToastContext.Provider>
202
212
  )
203
213
  }
@@ -225,9 +235,9 @@ function ToastCard({
225
235
 
226
236
  return (
227
237
  <div
228
- data-slot="toast"
229
- data-tone={tone}
230
- className={cn("flex gap-3 rounded-lg border p-3 shadow-lg backdrop-blur", toneClassName[tone])}
238
+ data-slot="toast"
239
+ data-tone={tone}
240
+ className={cn("pointer-events-auto flex gap-3 rounded-lg border p-3 shadow-lg backdrop-blur", toneClassName[tone])}
231
241
  role="status"
232
242
  aria-live="polite"
233
243
  onMouseEnter={() => setHovered(true)}
@@ -167,11 +167,9 @@ function AlertDialog({
167
167
  </div>
168
168
  ) : null}
169
169
  <DialogFooter>
170
- <DialogClose render={() => (
171
- <Button type="button" variant={cancelVariant} disabled={resolvedLoading} {...cancelButtonProps}>
172
- {cancelLabel}
173
- </Button>
174
- )} />
170
+ <DialogClose render={<Button type="button" variant={cancelVariant} disabled={resolvedLoading} {...cancelButtonProps} />}>
171
+ {cancelLabel}
172
+ </DialogClose>
175
173
  <Button
176
174
  type="button"
177
175
  variant={actionTone === "destructive" ? "destructive" : "default"}