tembro 6.1.0 → 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 (146) hide show
  1. package/dist/components/actions/action-menu.d.ts +11 -1
  2. package/dist/components/display/index.d.ts +0 -2
  3. package/dist/components/filters/index.d.ts +0 -1
  4. package/dist/components/inputs/time-picker.d.ts +11 -3
  5. package/dist/components/modern/file-manager.d.ts +1 -1
  6. package/dist/components/modern/index.d.ts +3 -1
  7. package/dist/components/modern/map.cjs +1 -0
  8. package/dist/components/modern/map.d.ts +29 -0
  9. package/dist/components/modern/map.js +1 -0
  10. package/dist/components/modern/media-player.cjs +1 -0
  11. package/dist/components/modern/media-player.d.ts +21 -0
  12. package/dist/components/modern/media-player.js +1 -0
  13. package/dist/components/modern/spreadsheet.cjs +1 -0
  14. package/dist/components/modern/spreadsheet.d.ts +35 -0
  15. package/dist/components/modern/spreadsheet.js +1 -0
  16. package/dist/components/overlay/drawer.d.ts +3 -1
  17. package/dist/index.d.ts +0 -2
  18. package/dist/src/components/actions/action-menu.cjs +1 -1
  19. package/dist/src/components/actions/action-menu.js +47 -39
  20. package/dist/src/components/actions/copy-button.cjs +1 -1
  21. package/dist/src/components/actions/copy-button.js +4 -1
  22. package/dist/src/components/calendar/date-picker.cjs +1 -1
  23. package/dist/src/components/calendar/date-picker.js +5 -8
  24. package/dist/src/components/calendar/date-range-picker.cjs +1 -1
  25. package/dist/src/components/calendar/date-range-picker.js +12 -27
  26. package/dist/src/components/data-table/data-table.cjs +1 -1
  27. package/dist/src/components/data-table/data-table.js +172 -171
  28. package/dist/src/components/display/index.cjs +1 -1
  29. package/dist/src/components/display/index.js +9 -11
  30. package/dist/src/components/filters/index.cjs +1 -1
  31. package/dist/src/components/filters/index.js +2 -3
  32. package/dist/src/components/inputs/color-picker.cjs +1 -1
  33. package/dist/src/components/inputs/color-picker.js +63 -46
  34. package/dist/src/components/inputs/slider.cjs +1 -1
  35. package/dist/src/components/inputs/slider.js +3 -3
  36. package/dist/src/components/inputs/tag-input.cjs +1 -1
  37. package/dist/src/components/inputs/tag-input.js +7 -4
  38. package/dist/src/components/inputs/time-picker.cjs +1 -1
  39. package/dist/src/components/inputs/time-picker.js +90 -34
  40. package/dist/src/components/modern/index.cjs +1 -1
  41. package/dist/src/components/modern/index.js +6 -4
  42. package/dist/src/components/modern/map.cjs +1 -0
  43. package/dist/src/components/modern/map.js +158 -0
  44. package/dist/src/components/modern/media-player.cjs +1 -0
  45. package/dist/src/components/modern/media-player.js +176 -0
  46. package/dist/src/components/modern/resizable-panel.cjs +1 -1
  47. package/dist/src/components/modern/resizable-panel.js +39 -32
  48. package/dist/src/components/modern/spreadsheet.cjs +1 -0
  49. package/dist/src/components/modern/spreadsheet.js +97 -0
  50. package/dist/src/components/navigation/pagination.cjs +1 -1
  51. package/dist/src/components/navigation/pagination.js +5 -5
  52. package/dist/src/components/notifications/toast.cjs +1 -1
  53. package/dist/src/components/notifications/toast.js +68 -63
  54. package/dist/src/components/overlay/alert-dialog.cjs +1 -1
  55. package/dist/src/components/overlay/alert-dialog.js +8 -6
  56. package/dist/src/components/overlay/drawer.cjs +1 -1
  57. package/dist/src/components/overlay/drawer.js +38 -20
  58. package/dist/src/components/ui/checkbox/index.cjs +1 -1
  59. package/dist/src/components/ui/checkbox/index.js +2 -1
  60. package/dist/src/components/ui/dialog/index.cjs +1 -1
  61. package/dist/src/components/ui/dialog/index.js +8 -8
  62. package/dist/src/components/ui/dropdown-menu/index.cjs +1 -1
  63. package/dist/src/components/ui/dropdown-menu/index.js +21 -19
  64. package/dist/src/components/ui/popover/index.cjs +1 -1
  65. package/dist/src/components/ui/popover/index.js +10 -10
  66. package/dist/src/components/ui/tabs/index.cjs +1 -1
  67. package/dist/src/components/ui/tabs/index.js +6 -6
  68. package/dist/src/components/ui/toggle-group/index.cjs +1 -1
  69. package/dist/src/components/ui/toggle-group/index.js +2 -2
  70. package/dist/src/index.cjs +1 -1
  71. package/dist/src/index.js +80 -83
  72. package/dist/src/public-component-surface.cjs +1 -1
  73. package/dist/src/public-component-surface.js +15 -20
  74. package/package.json +1 -2
  75. package/packages/cli/dist/index.cjs +58 -178
  76. package/packages/cli/vendor/src/components/actions/action-menu.tsx +47 -19
  77. package/packages/cli/vendor/src/components/actions/copy-button.tsx +2 -2
  78. package/packages/cli/vendor/src/components/calendar/date-picker.tsx +2 -5
  79. package/packages/cli/vendor/src/components/calendar/date-range-picker.tsx +5 -12
  80. package/packages/cli/vendor/src/components/data-table/data-table.tsx +45 -34
  81. package/packages/cli/vendor/src/components/display/index.ts +3 -5
  82. package/packages/cli/vendor/src/components/filters/index.ts +0 -1
  83. package/packages/cli/vendor/src/components/inputs/color-picker.tsx +15 -1
  84. package/packages/cli/vendor/src/components/inputs/slider.tsx +3 -3
  85. package/packages/cli/vendor/src/components/inputs/tag-input.tsx +4 -4
  86. package/packages/cli/vendor/src/components/inputs/time-picker.tsx +91 -38
  87. package/packages/cli/vendor/src/components/modern/file-manager.tsx +1 -1
  88. package/packages/cli/vendor/src/components/modern/index.ts +3 -1
  89. package/packages/cli/vendor/src/components/modern/map.tsx +146 -0
  90. package/packages/cli/vendor/src/components/modern/media-player.tsx +182 -0
  91. package/packages/cli/vendor/src/components/modern/resizable-panel.tsx +15 -5
  92. package/packages/cli/vendor/src/components/modern/spreadsheet.tsx +155 -0
  93. package/packages/cli/vendor/src/components/navigation/pagination.tsx +9 -5
  94. package/packages/cli/vendor/src/components/notifications/toast.tsx +20 -10
  95. package/packages/cli/vendor/src/components/overlay/alert-dialog.tsx +3 -5
  96. package/packages/cli/vendor/src/components/overlay/drawer.tsx +22 -11
  97. package/packages/cli/vendor/src/components/ui/checkbox/index.tsx +3 -1
  98. package/packages/cli/vendor/src/components/ui/dialog/index.tsx +4 -4
  99. package/packages/cli/vendor/src/components/ui/dropdown-menu/index.tsx +18 -22
  100. package/packages/cli/vendor/src/components/ui/popover/index.tsx +4 -4
  101. package/packages/cli/vendor/src/components/ui/tabs/index.tsx +8 -3
  102. package/packages/cli/vendor/src/components/ui/toggle-group/index.tsx +6 -6
  103. package/packages/cli/vendor/src/index.ts +0 -2
  104. package/packages/cli/vendor/src/public-component-surface.ts +3 -4
  105. package/packages/cli/vendor/templates/showcase/src/showcase/data/registry.ts +6 -6
  106. package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +23 -8
  107. package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
  108. package/packages/cli/vendor/templates/showcase/src/showcase/sections/PatternsSection.tsx +1 -1
  109. package/packages/cli/vendor/templates/styles/globals.css +0 -120
  110. package/registry.json +74 -141
  111. package/dist/components/display/info-card.cjs +0 -1
  112. package/dist/components/display/info-card.d.ts +0 -51
  113. package/dist/components/display/info-card.js +0 -1
  114. package/dist/components/display/statistic.cjs +0 -1
  115. package/dist/components/display/statistic.d.ts +0 -25
  116. package/dist/components/display/statistic.js +0 -1
  117. package/dist/components/filters/filter-bar.cjs +0 -1
  118. package/dist/components/filters/filter-bar.d.ts +0 -28
  119. package/dist/components/filters/filter-bar.js +0 -1
  120. package/dist/components/modern/image-cropper.cjs +0 -1
  121. package/dist/components/modern/image-cropper.d.ts +0 -39
  122. package/dist/components/modern/image-cropper.js +0 -1
  123. package/dist/components/ui/number-field/index.cjs +0 -1
  124. package/dist/components/ui/number-field/index.d.ts +0 -18
  125. package/dist/components/ui/number-field/index.js +0 -1
  126. package/dist/components/ui/toolbar/index.cjs +0 -1
  127. package/dist/components/ui/toolbar/index.d.ts +0 -14
  128. package/dist/components/ui/toolbar/index.js +0 -1
  129. package/dist/src/components/display/info-card.cjs +0 -1
  130. package/dist/src/components/display/info-card.js +0 -124
  131. package/dist/src/components/display/statistic.cjs +0 -1
  132. package/dist/src/components/display/statistic.js +0 -104
  133. package/dist/src/components/filters/filter-bar.cjs +0 -1
  134. package/dist/src/components/filters/filter-bar.js +0 -103
  135. package/dist/src/components/modern/image-cropper.cjs +0 -1
  136. package/dist/src/components/modern/image-cropper.js +0 -143
  137. package/dist/src/components/ui/number-field/index.cjs +0 -1
  138. package/dist/src/components/ui/number-field/index.js +0 -73
  139. package/dist/src/components/ui/toolbar/index.cjs +0 -1
  140. package/dist/src/components/ui/toolbar/index.js +0 -50
  141. package/packages/cli/vendor/src/components/display/info-card.tsx +0 -195
  142. package/packages/cli/vendor/src/components/display/statistic.tsx +0 -114
  143. package/packages/cli/vendor/src/components/filters/filter-bar.tsx +0 -163
  144. package/packages/cli/vendor/src/components/modern/image-cropper.tsx +0 -226
  145. package/packages/cli/vendor/src/components/ui/number-field/index.tsx +0 -89
  146. package/packages/cli/vendor/src/components/ui/toolbar/index.tsx +0 -67
@@ -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 }
@@ -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}
@@ -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"}