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.
- package/CHANGELOG.md +13 -0
- package/dist/components/actions/action-menu.d.ts +11 -1
- package/dist/components/calendar/calendar-range-scheduler.cjs +1 -0
- package/dist/components/calendar/calendar-range-scheduler.d.ts +20 -0
- package/dist/components/calendar/calendar-range-scheduler.js +1 -0
- package/dist/components/calendar/date-time-picker.cjs +1 -0
- package/dist/components/calendar/date-time-picker.d.ts +15 -0
- package/dist/components/calendar/date-time-picker.js +1 -0
- package/dist/components/calendar/index.d.ts +2 -0
- package/dist/components/charts/chart-card.cjs +1 -0
- package/dist/components/charts/chart-card.d.ts +24 -0
- package/dist/components/charts/chart-card.js +1 -0
- package/dist/components/charts/index.d.ts +1 -0
- package/dist/components/command/advanced-command-menu.cjs +1 -0
- package/dist/components/command/advanced-command-menu.d.ts +25 -0
- package/dist/components/command/advanced-command-menu.js +1 -0
- package/dist/components/command/index.d.ts +1 -0
- package/dist/components/data-table/data-grid.cjs +1 -0
- package/dist/components/data-table/data-grid.d.ts +15 -0
- package/dist/components/data-table/data-grid.js +1 -0
- package/dist/components/data-table/index.d.ts +1 -0
- package/dist/components/data-table/public.d.ts +1 -0
- package/dist/components/display/index.d.ts +0 -2
- package/dist/components/feedback/async-boundary.cjs +1 -0
- package/dist/components/feedback/async-boundary.d.ts +12 -0
- package/dist/components/feedback/async-boundary.js +1 -0
- package/dist/components/feedback/index.d.ts +1 -0
- package/dist/components/filters/index.d.ts +0 -1
- package/dist/components/inputs/index.d.ts +1 -0
- package/dist/components/inputs/multi-select.cjs +1 -0
- package/dist/components/inputs/multi-select.d.ts +16 -0
- package/dist/components/inputs/multi-select.js +1 -0
- package/dist/components/inputs/public.d.ts +1 -0
- package/dist/components/inputs/time-picker.d.ts +11 -3
- package/dist/components/modern/file-manager.cjs +1 -0
- package/dist/components/modern/file-manager.d.ts +20 -0
- package/dist/components/modern/file-manager.js +1 -0
- package/dist/components/modern/index.d.ts +4 -1
- package/dist/components/modern/map.cjs +1 -0
- package/dist/components/modern/map.d.ts +29 -0
- package/dist/components/modern/map.js +1 -0
- package/dist/components/modern/media-player.cjs +1 -0
- package/dist/components/modern/media-player.d.ts +21 -0
- package/dist/components/modern/media-player.js +1 -0
- package/dist/components/modern/spreadsheet.cjs +1 -0
- package/dist/components/modern/spreadsheet.d.ts +35 -0
- package/dist/components/modern/spreadsheet.js +1 -0
- package/dist/components/notifications/action-toast.cjs +1 -0
- package/dist/components/notifications/action-toast.d.ts +16 -0
- package/dist/components/notifications/action-toast.js +1 -0
- package/dist/components/notifications/index.d.ts +1 -0
- package/dist/components/overlay/drawer.d.ts +3 -1
- package/dist/components/patterns/index.d.ts +1 -0
- package/dist/components/patterns/permissions-matrix.cjs +1 -0
- package/dist/components/patterns/permissions-matrix.d.ts +19 -0
- package/dist/components/patterns/permissions-matrix.js +1 -0
- package/dist/components/patterns/public.d.ts +1 -0
- package/dist/index.d.ts +0 -2
- package/dist/src/components/actions/action-menu.cjs +1 -1
- package/dist/src/components/actions/action-menu.js +47 -39
- package/dist/src/components/actions/copy-button.cjs +1 -1
- package/dist/src/components/actions/copy-button.js +4 -1
- package/dist/src/components/calendar/calendar-range-scheduler.cjs +1 -0
- package/dist/src/components/calendar/calendar-range-scheduler.js +75 -0
- package/dist/src/components/calendar/date-picker.cjs +1 -1
- package/dist/src/components/calendar/date-picker.js +5 -8
- package/dist/src/components/calendar/date-range-picker.cjs +1 -1
- package/dist/src/components/calendar/date-range-picker.js +12 -27
- package/dist/src/components/calendar/date-time-picker.cjs +1 -0
- package/dist/src/components/calendar/date-time-picker.js +36 -0
- package/dist/src/components/calendar/index.cjs +1 -1
- package/dist/src/components/calendar/index.js +5 -3
- package/dist/src/components/charts/chart-card.cjs +1 -0
- package/dist/src/components/charts/chart-card.js +55 -0
- package/dist/src/components/charts/index.cjs +1 -1
- package/dist/src/components/charts/index.js +3 -2
- package/dist/src/components/command/advanced-command-menu.cjs +1 -0
- package/dist/src/components/command/advanced-command-menu.js +57 -0
- package/dist/src/components/command/index.cjs +1 -1
- package/dist/src/components/command/index.js +3 -2
- package/dist/src/components/data-table/data-grid.cjs +1 -0
- package/dist/src/components/data-table/data-grid.js +32 -0
- package/dist/src/components/data-table/data-table-saved-filters.cjs +1 -1
- package/dist/src/components/data-table/data-table-saved-filters.js +11 -11
- package/dist/src/components/data-table/data-table.cjs +1 -1
- package/dist/src/components/data-table/data-table.js +175 -174
- package/dist/src/components/data-table/index.cjs +1 -1
- package/dist/src/components/data-table/index.js +15 -14
- package/dist/src/components/data-table/public.cjs +1 -1
- package/dist/src/components/data-table/public.js +3 -2
- package/dist/src/components/display/index.cjs +1 -1
- package/dist/src/components/display/index.js +9 -11
- package/dist/src/components/feedback/async-boundary.cjs +1 -0
- package/dist/src/components/feedback/async-boundary.js +22 -0
- package/dist/src/components/feedback/index.cjs +1 -1
- package/dist/src/components/feedback/index.js +2 -1
- package/dist/src/components/filters/index.cjs +1 -1
- package/dist/src/components/filters/index.js +2 -3
- package/dist/src/components/inputs/color-picker.cjs +1 -1
- package/dist/src/components/inputs/color-picker.js +63 -46
- package/dist/src/components/inputs/index.cjs +1 -1
- package/dist/src/components/inputs/index.js +13 -12
- package/dist/src/components/inputs/multi-select.cjs +1 -0
- package/dist/src/components/inputs/multi-select.js +80 -0
- package/dist/src/components/inputs/public.cjs +1 -1
- package/dist/src/components/inputs/public.js +13 -12
- package/dist/src/components/inputs/slider.cjs +1 -1
- package/dist/src/components/inputs/slider.js +3 -3
- package/dist/src/components/inputs/tag-input.cjs +1 -1
- package/dist/src/components/inputs/tag-input.js +7 -4
- package/dist/src/components/inputs/time-picker.cjs +1 -1
- package/dist/src/components/inputs/time-picker.js +90 -34
- package/dist/src/components/modern/file-manager.cjs +1 -0
- package/dist/src/components/modern/file-manager.js +75 -0
- package/dist/src/components/modern/index.cjs +1 -1
- package/dist/src/components/modern/index.js +7 -4
- package/dist/src/components/modern/map.cjs +1 -0
- package/dist/src/components/modern/map.js +158 -0
- package/dist/src/components/modern/media-player.cjs +1 -0
- package/dist/src/components/modern/media-player.js +176 -0
- package/dist/src/components/modern/resizable-panel.cjs +1 -1
- package/dist/src/components/modern/resizable-panel.js +39 -32
- package/dist/src/components/modern/spreadsheet.cjs +1 -0
- package/dist/src/components/modern/spreadsheet.js +97 -0
- package/dist/src/components/navigation/pagination.cjs +1 -1
- package/dist/src/components/navigation/pagination.js +5 -5
- package/dist/src/components/notifications/action-toast.cjs +1 -0
- package/dist/src/components/notifications/action-toast.js +49 -0
- package/dist/src/components/notifications/index.cjs +1 -1
- package/dist/src/components/notifications/index.js +3 -2
- package/dist/src/components/notifications/toast.cjs +1 -1
- package/dist/src/components/notifications/toast.js +68 -63
- package/dist/src/components/overlay/alert-dialog.cjs +1 -1
- package/dist/src/components/overlay/alert-dialog.js +8 -6
- package/dist/src/components/overlay/drawer.cjs +1 -1
- package/dist/src/components/overlay/drawer.js +38 -20
- package/dist/src/components/patterns/index.cjs +1 -1
- package/dist/src/components/patterns/index.js +2 -1
- package/dist/src/components/patterns/permissions-matrix.cjs +1 -0
- package/dist/src/components/patterns/permissions-matrix.js +43 -0
- package/dist/src/components/patterns/public.cjs +1 -1
- package/dist/src/components/patterns/public.js +2 -1
- package/dist/src/components/ui/checkbox/index.cjs +1 -1
- package/dist/src/components/ui/checkbox/index.js +2 -1
- package/dist/src/components/ui/dialog/index.cjs +1 -1
- package/dist/src/components/ui/dialog/index.js +8 -8
- package/dist/src/components/ui/dropdown-menu/index.cjs +1 -1
- package/dist/src/components/ui/dropdown-menu/index.js +21 -19
- package/dist/src/components/ui/input/group.cjs +1 -1
- package/dist/src/components/ui/input/group.js +8 -8
- package/dist/src/components/ui/popover/index.cjs +1 -1
- package/dist/src/components/ui/popover/index.js +10 -10
- package/dist/src/components/ui/tabs/index.cjs +1 -1
- package/dist/src/components/ui/tabs/index.js +6 -6
- package/dist/src/components/ui/toggle-group/index.cjs +1 -1
- package/dist/src/components/ui/toggle-group/index.js +2 -2
- package/dist/src/index.cjs +1 -1
- package/dist/src/index.js +111 -104
- package/dist/src/public-component-surface.cjs +1 -1
- package/dist/src/public-component-surface.js +59 -14
- package/package.json +1 -2
- package/packages/cli/dist/index.cjs +45 -165
- package/packages/cli/vendor/src/components/actions/action-menu.tsx +47 -19
- package/packages/cli/vendor/src/components/actions/copy-button.tsx +2 -2
- package/packages/cli/vendor/src/components/calendar/calendar-range-scheduler.tsx +72 -0
- package/packages/cli/vendor/src/components/calendar/date-picker.tsx +2 -5
- package/packages/cli/vendor/src/components/calendar/date-range-picker.tsx +5 -12
- package/packages/cli/vendor/src/components/calendar/date-time-picker.tsx +53 -0
- package/packages/cli/vendor/src/components/calendar/index.ts +5 -3
- package/packages/cli/vendor/src/components/charts/chart-card.tsx +81 -0
- package/packages/cli/vendor/src/components/charts/index.ts +1 -0
- package/packages/cli/vendor/src/components/command/advanced-command-menu.tsx +88 -0
- package/packages/cli/vendor/src/components/command/index.ts +2 -1
- package/packages/cli/vendor/src/components/data-table/data-grid.tsx +59 -0
- package/packages/cli/vendor/src/components/data-table/data-table.tsx +45 -34
- package/packages/cli/vendor/src/components/data-table/index.ts +3 -2
- package/packages/cli/vendor/src/components/data-table/public.ts +1 -0
- package/packages/cli/vendor/src/components/display/index.ts +3 -5
- package/packages/cli/vendor/src/components/feedback/async-boundary.tsx +36 -0
- package/packages/cli/vendor/src/components/feedback/index.ts +1 -0
- package/packages/cli/vendor/src/components/filters/index.ts +0 -1
- package/packages/cli/vendor/src/components/inputs/color-picker.tsx +15 -1
- package/packages/cli/vendor/src/components/inputs/index.ts +1 -0
- package/packages/cli/vendor/src/components/inputs/multi-select.tsx +75 -0
- package/packages/cli/vendor/src/components/inputs/public.ts +1 -0
- package/packages/cli/vendor/src/components/inputs/slider.tsx +3 -3
- package/packages/cli/vendor/src/components/inputs/tag-input.tsx +4 -4
- package/packages/cli/vendor/src/components/inputs/time-picker.tsx +91 -38
- package/packages/cli/vendor/src/components/modern/file-manager.tsx +79 -0
- package/packages/cli/vendor/src/components/modern/index.ts +4 -1
- package/packages/cli/vendor/src/components/modern/map.tsx +146 -0
- package/packages/cli/vendor/src/components/modern/media-player.tsx +182 -0
- package/packages/cli/vendor/src/components/modern/resizable-panel.tsx +15 -5
- package/packages/cli/vendor/src/components/modern/spreadsheet.tsx +155 -0
- package/packages/cli/vendor/src/components/navigation/pagination.tsx +9 -5
- package/packages/cli/vendor/src/components/notifications/action-toast.tsx +78 -0
- package/packages/cli/vendor/src/components/notifications/index.ts +2 -1
- package/packages/cli/vendor/src/components/notifications/toast.tsx +20 -10
- package/packages/cli/vendor/src/components/overlay/alert-dialog.tsx +3 -5
- package/packages/cli/vendor/src/components/overlay/drawer.tsx +22 -11
- package/packages/cli/vendor/src/components/patterns/index.ts +1 -0
- package/packages/cli/vendor/src/components/patterns/permissions-matrix.tsx +71 -0
- package/packages/cli/vendor/src/components/patterns/public.ts +1 -0
- package/packages/cli/vendor/src/components/ui/checkbox/index.tsx +3 -1
- package/packages/cli/vendor/src/components/ui/dialog/index.tsx +4 -4
- package/packages/cli/vendor/src/components/ui/dropdown-menu/index.tsx +18 -22
- package/packages/cli/vendor/src/components/ui/popover/index.tsx +4 -4
- package/packages/cli/vendor/src/components/ui/tabs/index.tsx +8 -3
- package/packages/cli/vendor/src/components/ui/toggle-group/index.tsx +6 -6
- package/packages/cli/vendor/src/index.ts +0 -2
- package/packages/cli/vendor/src/public-component-surface.ts +13 -4
- package/packages/cli/vendor/templates/showcase/src/showcase/data/registry.ts +6 -6
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +23 -8
- package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
- package/packages/cli/vendor/templates/showcase/src/showcase/sections/PatternsSection.tsx +1 -1
- package/packages/cli/vendor/templates/styles/globals.css +0 -120
- package/registry.json +3197 -2794
- package/dist/components/display/info-card.cjs +0 -1
- package/dist/components/display/info-card.d.ts +0 -51
- package/dist/components/display/info-card.js +0 -1
- package/dist/components/display/statistic.cjs +0 -1
- package/dist/components/display/statistic.d.ts +0 -25
- package/dist/components/display/statistic.js +0 -1
- package/dist/components/filters/filter-bar.cjs +0 -1
- package/dist/components/filters/filter-bar.d.ts +0 -28
- package/dist/components/filters/filter-bar.js +0 -1
- package/dist/components/modern/image-cropper.cjs +0 -1
- package/dist/components/modern/image-cropper.d.ts +0 -39
- package/dist/components/modern/image-cropper.js +0 -1
- package/dist/components/ui/number-field/index.cjs +0 -1
- package/dist/components/ui/number-field/index.d.ts +0 -18
- package/dist/components/ui/number-field/index.js +0 -1
- package/dist/components/ui/toolbar/index.cjs +0 -1
- package/dist/components/ui/toolbar/index.d.ts +0 -14
- package/dist/components/ui/toolbar/index.js +0 -1
- package/dist/src/components/display/info-card.cjs +0 -1
- package/dist/src/components/display/info-card.js +0 -124
- package/dist/src/components/display/statistic.cjs +0 -1
- package/dist/src/components/display/statistic.js +0 -104
- package/dist/src/components/filters/filter-bar.cjs +0 -1
- package/dist/src/components/filters/filter-bar.js +0 -103
- package/dist/src/components/modern/image-cropper.cjs +0 -1
- package/dist/src/components/modern/image-cropper.js +0 -143
- package/dist/src/components/ui/number-field/index.cjs +0 -1
- package/dist/src/components/ui/number-field/index.js +0 -73
- package/dist/src/components/ui/toolbar/index.cjs +0 -1
- package/dist/src/components/ui/toolbar/index.js +0 -50
- package/packages/cli/vendor/src/components/display/info-card.tsx +0 -195
- package/packages/cli/vendor/src/components/display/statistic.tsx +0 -114
- package/packages/cli/vendor/src/components/filters/filter-bar.tsx +0 -163
- package/packages/cli/vendor/src/components/modern/image-cropper.tsx +0 -226
- package/packages/cli/vendor/src/components/ui/number-field/index.tsx +0 -89
- package/packages/cli/vendor/src/components/ui/toolbar/index.tsx +0 -67
|
@@ -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 }
|
|
@@ -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
|
-
|
|
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>
|
|
@@ -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 }
|
|
@@ -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="
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 "./
|
|
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 }
|