diff --git a/apps/web/src/components/dashboard-builder/widgets/chart-preview.tsx b/apps/web/src/components/dashboard-builder/widgets/chart-preview.tsx
index 71c0cab..2a3375f 100644
--- a/apps/web/src/components/dashboard-builder/widgets/chart-preview.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/chart-preview.tsx
@@ -1,5 +1,5 @@
import { Suspense, type ComponentType } from "react"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
interface ChartPreviewProps {
component: ComponentType<{ className?: string }>
diff --git a/apps/web/src/components/dashboard-builder/widgets/chart-widget.tsx b/apps/web/src/components/dashboard-builder/widgets/chart-widget.tsx
index ff0f05b..8647ff7 100644
--- a/apps/web/src/components/dashboard-builder/widgets/chart-widget.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/chart-widget.tsx
@@ -1,7 +1,7 @@
import { memo, Suspense } from "react"
-import { Skeleton } from "@/components/ui/skeleton"
-import { getChartById } from "@/components/charts/registry"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { getChartById } from "@maple/ui/components/charts/registry"
import { WidgetShell } from "@/components/dashboard-builder/widgets/widget-shell"
import type {
WidgetDataState,
diff --git a/apps/web/src/components/dashboard-builder/widgets/stat-widget.tsx b/apps/web/src/components/dashboard-builder/widgets/stat-widget.tsx
index b7739d1..f08292f 100644
--- a/apps/web/src/components/dashboard-builder/widgets/stat-widget.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/stat-widget.tsx
@@ -1,5 +1,5 @@
import { memo } from "react"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { WidgetShell } from "@/components/dashboard-builder/widgets/widget-shell"
import type {
WidgetDataState,
diff --git a/apps/web/src/components/dashboard-builder/widgets/table-widget.tsx b/apps/web/src/components/dashboard-builder/widgets/table-widget.tsx
index 243bc14..212db51 100644
--- a/apps/web/src/components/dashboard-builder/widgets/table-widget.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/table-widget.tsx
@@ -1,5 +1,5 @@
import { memo } from "react"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import {
Table,
TableBody,
@@ -7,7 +7,7 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
+} from "@maple/ui/components/ui/table"
import { WidgetShell } from "@/components/dashboard-builder/widgets/widget-shell"
import type {
WidgetDataState,
diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-edit-panel.tsx b/apps/web/src/components/dashboard-builder/widgets/widget-edit-panel.tsx
index 40943f3..b1616c6 100644
--- a/apps/web/src/components/dashboard-builder/widgets/widget-edit-panel.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/widget-edit-panel.tsx
@@ -1,5 +1,5 @@
-import { Input } from "@/components/ui/input"
-import { getChartById, getChartsByCategory } from "@/components/charts/registry"
+import { Input } from "@maple/ui/components/ui/input"
+import { getChartById, getChartsByCategory } from "@maple/ui/components/charts/registry"
import { ChartPreview } from "@/components/dashboard-builder/widgets/chart-preview"
import type {
DashboardWidget,
diff --git a/apps/web/src/components/dashboard-builder/widgets/widget-shell.tsx b/apps/web/src/components/dashboard-builder/widgets/widget-shell.tsx
index 8bc5990..eae7f90 100644
--- a/apps/web/src/components/dashboard-builder/widgets/widget-shell.tsx
+++ b/apps/web/src/components/dashboard-builder/widgets/widget-shell.tsx
@@ -1,9 +1,9 @@
import type { ReactNode } from "react"
import { GripDotsIcon, TrashIcon, GearIcon, PencilIcon } from "@/components/icons"
-import { Card, CardContent, CardHeader, CardTitle, CardAction } from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle } from "@/components/ui/popover"
+import { Card, CardContent, CardHeader, CardTitle, CardAction } from "@maple/ui/components/ui/card"
+import { Button } from "@maple/ui/components/ui/button"
+import { Popover, PopoverTrigger, PopoverContent, PopoverHeader, PopoverTitle } from "@maple/ui/components/ui/popover"
import type { WidgetMode } from "@/components/dashboard-builder/types"
interface WidgetShellProps {
diff --git a/apps/web/src/components/dashboard/app-sidebar.tsx b/apps/web/src/components/dashboard/app-sidebar.tsx
index a48a8e8..d8968e2 100644
--- a/apps/web/src/components/dashboard/app-sidebar.tsx
+++ b/apps/web/src/components/dashboard/app-sidebar.tsx
@@ -20,7 +20,7 @@ import {
Collapsible,
CollapsibleTrigger,
CollapsibleContent,
-} from "@/components/ui/collapsible"
+} from "@maple/ui/components/ui/collapsible"
import { OrgSwitcher } from "@/components/dashboard/org-switcher"
import {
DropdownMenu,
@@ -30,7 +30,7 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
+} from "@maple/ui/components/ui/dropdown-menu"
import {
Sidebar,
SidebarContent,
@@ -43,7 +43,7 @@ import {
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
-} from "@/components/ui/sidebar"
+} from "@maple/ui/components/ui/sidebar"
import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode"
import { clearSelfHostedSessionToken } from "@/lib/services/common/self-hosted-auth"
import { useQuickStart } from "@/hooks/use-quick-start"
diff --git a/apps/web/src/components/dashboard/metrics-grid.tsx b/apps/web/src/components/dashboard/metrics-grid.tsx
index e78570a..ddf8e66 100644
--- a/apps/web/src/components/dashboard/metrics-grid.tsx
+++ b/apps/web/src/components/dashboard/metrics-grid.tsx
@@ -1,12 +1,12 @@
import { Suspense } from "react"
-import { cn } from "@/lib/utils"
-import { Skeleton } from "@/components/ui/skeleton"
-import { getChartById } from "@/components/charts/registry"
+import { cn } from "@maple/ui/utils"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { getChartById } from "@maple/ui/components/charts/registry"
import type {
ChartLegendMode,
ChartTooltipMode,
-} from "@/components/charts/_shared/chart-types"
+} from "@maple/ui/components/charts/_shared/chart-types"
import { ReadonlyWidgetShell } from "@/components/dashboard-builder/widgets/widget-shell"
interface MetricsGridItem {
diff --git a/apps/web/src/components/dashboard/org-switcher.tsx b/apps/web/src/components/dashboard/org-switcher.tsx
index 54d8560..f8667fb 100644
--- a/apps/web/src/components/dashboard/org-switcher.tsx
+++ b/apps/web/src/components/dashboard/org-switcher.tsx
@@ -17,21 +17,21 @@ import {
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
+} from "@maple/ui/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
-} from "@/components/ui/sidebar"
+} from "@maple/ui/components/ui/sidebar"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Button } from "@/components/ui/button"
+} from "@maple/ui/components/ui/dialog"
+import { Input } from "@maple/ui/components/ui/input"
+import { Button } from "@maple/ui/components/ui/button"
import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode"
function OrgAvatar({
diff --git a/apps/web/src/components/dashboard/service-usage-cards.tsx b/apps/web/src/components/dashboard/service-usage-cards.tsx
index 12e0f15..e9f09b1 100644
--- a/apps/web/src/components/dashboard/service-usage-cards.tsx
+++ b/apps/web/src/components/dashboard/service-usage-cards.tsx
@@ -6,8 +6,8 @@ import {
DatabaseIcon,
} from "@/components/icons"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader, CardTitle } from "@maple/ui/components/ui/card"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { getServiceUsageResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
function formatNumber(num: number): string {
diff --git a/apps/web/src/components/errors/errors-by-type-table.tsx b/apps/web/src/components/errors/errors-by-type-table.tsx
index 7b9f4e2..cf0b5ce 100644
--- a/apps/web/src/components/errors/errors-by-type-table.tsx
+++ b/apps/web/src/components/errors/errors-by-type-table.tsx
@@ -11,9 +11,9 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Badge } from "@/components/ui/badge"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/table"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type GetErrorsByTypeInput, type ErrorByType } from "@/api/tinybird/errors"
import { formatDuration } from "@/lib/format"
import {
diff --git a/apps/web/src/components/errors/errors-filter-sidebar.tsx b/apps/web/src/components/errors/errors-filter-sidebar.tsx
index ff462be..7720a27 100644
--- a/apps/web/src/components/errors/errors-filter-sidebar.tsx
+++ b/apps/web/src/components/errors/errors-filter-sidebar.tsx
@@ -4,7 +4,7 @@ import { useNavigate } from "@tanstack/react-router"
import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import { FilterSection, SingleCheckboxFilter } from "@/components/traces/filter-section"
import { Route } from "@/routes/errors"
-import { Separator } from "@/components/ui/separator"
+import { Separator } from "@maple/ui/components/ui/separator"
import { getErrorsFacetsResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import {
FilterSidebarBody,
@@ -29,9 +29,6 @@ export function ErrorsFilterSidebar() {
data: {
startTime: effectiveStartTime,
endTime: effectiveEndTime,
- services: search.services,
- deploymentEnvs: search.deploymentEnvs,
- errorTypes: search.errorTypes,
showSpam: search.showSpam,
},
}),
diff --git a/apps/web/src/components/errors/errors-summary-cards.tsx b/apps/web/src/components/errors/errors-summary-cards.tsx
index e146699..c86e36e 100644
--- a/apps/web/src/components/errors/errors-summary-cards.tsx
+++ b/apps/web/src/components/errors/errors-summary-cards.tsx
@@ -6,8 +6,8 @@ import {
PulseIcon,
} from "@/components/icons"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader, CardTitle } from "@maple/ui/components/ui/card"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type GetErrorsSummaryInput } from "@/api/tinybird/errors"
import { getErrorsSummaryResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
diff --git a/apps/web/src/components/example.tsx b/apps/web/src/components/example.tsx
index 7883492..8873388 100644
--- a/apps/web/src/components/example.tsx
+++ b/apps/web/src/components/example.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
function ExampleWrapper({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/apps/web/src/components/filters/filter-section.tsx b/apps/web/src/components/filters/filter-section.tsx
index 31ca9ad..0b1be2e 100644
--- a/apps/web/src/components/filters/filter-section.tsx
+++ b/apps/web/src/components/filters/filter-section.tsx
@@ -1,14 +1,14 @@
import * as React from "react"
import { ChevronDownIcon, XmarkIcon, MagnifierIcon } from "@/components/icons"
-import { cn } from "@/lib/utils"
-import { Checkbox } from "@/components/ui/checkbox"
-import { Label } from "@/components/ui/label"
+import { cn } from "@maple/ui/utils"
+import { Checkbox } from "@maple/ui/components/ui/checkbox"
+import { Label } from "@maple/ui/components/ui/label"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
-} from "@/components/ui/collapsible"
+} from "@maple/ui/components/ui/collapsible"
export interface FilterOption {
name: string
diff --git a/apps/web/src/components/filters/filter-sidebar.tsx b/apps/web/src/components/filters/filter-sidebar.tsx
index e63134e..f068f10 100644
--- a/apps/web/src/components/filters/filter-sidebar.tsx
+++ b/apps/web/src/components/filters/filter-sidebar.tsx
@@ -1,9 +1,9 @@
import type { ReactNode } from "react"
-import { Separator } from "@/components/ui/separator"
-import { Skeleton } from "@/components/ui/skeleton"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { cn } from "@/lib/utils"
+import { Separator } from "@maple/ui/components/ui/separator"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { ScrollArea } from "@maple/ui/components/ui/scroll-area"
+import { cn } from "@maple/ui/utils"
interface FilterSidebarFrameProps {
children: ReactNode
diff --git a/apps/web/src/components/layout/dashboard-layout.tsx b/apps/web/src/components/layout/dashboard-layout.tsx
index 8b33c2c..41b0747 100644
--- a/apps/web/src/components/layout/dashboard-layout.tsx
+++ b/apps/web/src/components/layout/dashboard-layout.tsx
@@ -1,8 +1,8 @@
import * as React from "react"
import { AppSidebar } from "@/components/dashboard/app-sidebar"
-import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
-import { Separator } from "@/components/ui/separator"
+import { SidebarInset, SidebarProvider, SidebarTrigger } from "@maple/ui/components/ui/sidebar"
+import { Separator } from "@maple/ui/components/ui/separator"
import {
Breadcrumb,
BreadcrumbItem,
@@ -10,7 +10,7 @@ import {
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb"
+} from "@maple/ui/components/ui/breadcrumb"
import { Link } from "@tanstack/react-router"
export interface BreadcrumbItem {
diff --git a/apps/web/src/components/logs/log-detail-sheet.tsx b/apps/web/src/components/logs/log-detail-sheet.tsx
index a54350c..0d1507f 100644
--- a/apps/web/src/components/logs/log-detail-sheet.tsx
+++ b/apps/web/src/components/logs/log-detail-sheet.tsx
@@ -7,12 +7,12 @@ import {
SheetContent,
SheetTitle,
SheetClose,
-} from "@/components/ui/sheet"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { ScrollArea } from "@/components/ui/scroll-area"
+} from "@maple/ui/components/ui/sheet"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Button } from "@maple/ui/components/ui/button"
+import { ScrollArea } from "@maple/ui/components/ui/scroll-area"
import { SeverityBadge } from "./severity-badge"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import type { Log } from "@/api/tinybird/logs"
interface LogDetailSheetProps {
diff --git a/apps/web/src/components/logs/logs-filter-sidebar.tsx b/apps/web/src/components/logs/logs-filter-sidebar.tsx
index 2f04ab0..ac4f87e 100644
--- a/apps/web/src/components/logs/logs-filter-sidebar.tsx
+++ b/apps/web/src/components/logs/logs-filter-sidebar.tsx
@@ -9,7 +9,7 @@ import {
SearchableFilterSection,
} from "@/components/filters/filter-section"
import { Route } from "@/routes/logs"
-import { Separator } from "@/components/ui/separator"
+import { Separator } from "@maple/ui/components/ui/separator"
import { getLogsFacetsResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import {
FilterSidebarBody,
diff --git a/apps/web/src/components/logs/logs-table.tsx b/apps/web/src/components/logs/logs-table.tsx
index 45dccf6..cc7d883 100644
--- a/apps/web/src/components/logs/logs-table.tsx
+++ b/apps/web/src/components/logs/logs-table.tsx
@@ -8,8 +8,8 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/table"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type Log } from "@/api/tinybird/logs"
import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import { SeverityBadge } from "./severity-badge"
diff --git a/apps/web/src/components/logs/severity-badge.tsx b/apps/web/src/components/logs/severity-badge.tsx
index f1738e2..d7698b7 100644
--- a/apps/web/src/components/logs/severity-badge.tsx
+++ b/apps/web/src/components/logs/severity-badge.tsx
@@ -1,5 +1,5 @@
-import { Badge } from "@/components/ui/badge"
-import { cn } from "@/lib/utils"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { cn } from "@maple/ui/utils"
interface SeverityBadgeProps {
severity: string
diff --git a/apps/web/src/components/metrics/metric-type-badge.tsx b/apps/web/src/components/metrics/metric-type-badge.tsx
index 97aadbe..f538e68 100644
--- a/apps/web/src/components/metrics/metric-type-badge.tsx
+++ b/apps/web/src/components/metrics/metric-type-badge.tsx
@@ -1,4 +1,4 @@
-import { Badge } from "@/components/ui/badge"
+import { Badge } from "@maple/ui/components/ui/badge"
const metricTypeConfig: Record
= {
sum: {
diff --git a/apps/web/src/components/metrics/metrics-overview.tsx b/apps/web/src/components/metrics/metrics-overview.tsx
index de497d4..fe9a388 100644
--- a/apps/web/src/components/metrics/metrics-overview.tsx
+++ b/apps/web/src/components/metrics/metrics-overview.tsx
@@ -1,6 +1,6 @@
import { useState } from "react"
-import { Input } from "@/components/ui/input"
+import { Input } from "@maple/ui/components/ui/input"
import { MetricsSummaryCards, type MetricType } from "./metrics-summary-cards"
import { MetricsVolumeChart } from "./metrics-volume-chart"
import { MetricsTable } from "./metrics-table"
diff --git a/apps/web/src/components/metrics/metrics-summary-cards.tsx b/apps/web/src/components/metrics/metrics-summary-cards.tsx
index 033568b..5cfde4d 100644
--- a/apps/web/src/components/metrics/metrics-summary-cards.tsx
+++ b/apps/web/src/components/metrics/metrics-summary-cards.tsx
@@ -6,8 +6,8 @@ import {
ChartBarTrendUpIcon,
} from "@/components/icons"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Card, CardContent, CardHeader, CardTitle } from "@maple/ui/components/ui/card"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type ListMetricsInput } from "@/api/tinybird/metrics"
import { getMetricsSummaryResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
diff --git a/apps/web/src/components/metrics/metrics-table.tsx b/apps/web/src/components/metrics/metrics-table.tsx
index e19841f..7c6d2ee 100644
--- a/apps/web/src/components/metrics/metrics-table.tsx
+++ b/apps/web/src/components/metrics/metrics-table.tsx
@@ -7,9 +7,9 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Badge } from "@/components/ui/badge"
+} from "@maple/ui/components/ui/table"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { Badge } from "@maple/ui/components/ui/badge"
import { MetricTypeBadge } from "./metric-type-badge"
import { type Metric, type ListMetricsInput } from "@/api/tinybird/metrics"
import { listMetricsResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
diff --git a/apps/web/src/components/metrics/metrics-volume-chart.tsx b/apps/web/src/components/metrics/metrics-volume-chart.tsx
index 82961cb..5aa0be6 100644
--- a/apps/web/src/components/metrics/metrics-volume-chart.tsx
+++ b/apps/web/src/components/metrics/metrics-volume-chart.tsx
@@ -6,9 +6,9 @@ import {
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
-} from "@/components/ui/chart"
-import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/chart"
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@maple/ui/components/ui/card"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type GetMetricTimeSeriesInput, type MetricTimeSeriesResponse } from "@/api/tinybird/metrics"
import { disabledResultAtom } from "@/lib/services/atoms/disabled-result-atom"
import { getMetricTimeSeriesResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
diff --git a/apps/web/src/components/query-builder/query-builder-lab.tsx b/apps/web/src/components/query-builder/query-builder-lab.tsx
index 46af3f2..1519733 100644
--- a/apps/web/src/components/query-builder/query-builder-lab.tsx
+++ b/apps/web/src/components/query-builder/query-builder-lab.tsx
@@ -2,8 +2,8 @@ import * as React from "react"
import { Result, useAtomValue } from "@effect-atom/atom-react"
import { PulseIcon, XmarkIcon, PlusIcon, MagnifierIcon } from "@/components/icons"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Button } from "@maple/ui/components/ui/button"
import {
Card,
CardContent,
@@ -11,19 +11,19 @@ import {
CardFooter,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
-import { Checkbox } from "@/components/ui/checkbox"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Separator } from "@/components/ui/separator"
+} from "@maple/ui/components/ui/card"
+import { Checkbox } from "@maple/ui/components/ui/checkbox"
+import { Input } from "@maple/ui/components/ui/input"
+import { Label } from "@maple/ui/components/ui/label"
+import { ScrollArea } from "@maple/ui/components/ui/scroll-area"
+import { Separator } from "@maple/ui/components/ui/separator"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select"
+} from "@maple/ui/components/ui/select"
import {
Table,
TableBody,
@@ -31,8 +31,8 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { cn } from "@/lib/utils"
+} from "@maple/ui/components/ui/table"
+import { cn } from "@maple/ui/utils"
import { WhereClauseEditor } from "@/components/query-builder/where-clause-editor"
import {
getLogsFacetsResultAtom,
diff --git a/apps/web/src/components/query-builder/where-clause-editor.tsx b/apps/web/src/components/query-builder/where-clause-editor.tsx
index 52271b7..c268d88 100644
--- a/apps/web/src/components/query-builder/where-clause-editor.tsx
+++ b/apps/web/src/components/query-builder/where-clause-editor.tsx
@@ -1,22 +1,25 @@
import * as React from "react"
-import { Textarea } from "@/components/ui/textarea"
+import { Textarea } from "@maple/ui/components/ui/textarea"
import {
applyWhereClauseSuggestion,
getWhereClauseAutocomplete,
+ type WhereClauseAutocompleteScope,
type WhereClauseAutocompleteValues,
} from "@/lib/query-builder/where-clause-autocomplete"
import type { QueryBuilderDataSource } from "@/lib/query-builder/model"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
interface WhereClauseEditorProps {
dataSource: QueryBuilderDataSource
value: string
onChange: (value: string) => void
values?: WhereClauseAutocompleteValues
+ autocompleteScope?: WhereClauseAutocompleteScope
onActiveAttributeKey?: (key: string | null) => void
placeholder?: string
rows?: number
+ maxSuggestions?: number
className?: string
textareaClassName?: string
ariaLabel?: string
@@ -27,9 +30,11 @@ export function WhereClauseEditor({
value,
onChange,
values,
+ autocompleteScope,
onActiveAttributeKey,
placeholder,
rows = 2,
+ maxSuggestions,
className,
textareaClassName,
ariaLabel,
@@ -51,8 +56,10 @@ export function WhereClauseEditor({
cursor,
dataSource,
values,
+ scope: autocompleteScope,
+ maxSuggestions,
}),
- [cursor, dataSource, value, values],
+ [autocompleteScope, cursor, dataSource, maxSuggestions, value, values],
)
// Notify parent when user is editing a value for an attr.* key
@@ -143,6 +150,15 @@ export function WhereClauseEditor({
onSelect={(event) => syncCursor(event.currentTarget)}
onKeyUp={(event) => syncCursor(event.currentTarget)}
onKeyDown={(event) => {
+ // Always prevent Enter from inserting newlines (where clauses are single-line)
+ if (event.key === "Enter") {
+ event.preventDefault()
+ if (isOpen && suggestions.length > 0) {
+ applySuggestion(activeIndex)
+ }
+ return
+ }
+
if (!isOpen || suggestions.length === 0) {
return
}
@@ -161,7 +177,7 @@ export function WhereClauseEditor({
return
}
- if (event.key === "Enter" || event.key === "Tab") {
+ if (event.key === "Tab") {
event.preventDefault()
applySuggestion(activeIndex)
return
diff --git a/apps/web/src/components/quick-start/code-block.tsx b/apps/web/src/components/quick-start/code-block.tsx
index 642c0af..cd398cd 100644
--- a/apps/web/src/components/quick-start/code-block.tsx
+++ b/apps/web/src/components/quick-start/code-block.tsx
@@ -1,7 +1,7 @@
import { useState } from "react"
import { toast } from "sonner"
import { CopyIcon, CheckIcon } from "@/components/icons"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { highlightCode } from "@/lib/sugar-high"
interface CodeBlockProps {
diff --git a/apps/web/src/components/quick-start/package-manager-code-block.tsx b/apps/web/src/components/quick-start/package-manager-code-block.tsx
index d7c14f8..bf0fb9f 100644
--- a/apps/web/src/components/quick-start/package-manager-code-block.tsx
+++ b/apps/web/src/components/quick-start/package-manager-code-block.tsx
@@ -1,4 +1,4 @@
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@maple/ui/components/ui/tabs"
import { CodeBlock } from "@/components/quick-start/code-block"
const packageManagers = [
diff --git a/apps/web/src/components/route-error.tsx b/apps/web/src/components/route-error.tsx
index f6728f2..fad1c45 100644
--- a/apps/web/src/components/route-error.tsx
+++ b/apps/web/src/components/route-error.tsx
@@ -1,14 +1,14 @@
import type { ErrorComponentProps } from "@tanstack/react-router"
import { Link, useRouter } from "@tanstack/react-router"
import { AlertWarningIcon, CircleQuestionIcon, HouseIcon } from "@/components/icons"
-import { Button, buttonVariants } from "@/components/ui/button"
+import { Button, buttonVariants } from "@maple/ui/components/ui/button"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
-} from "@/components/ui/empty"
+} from "@maple/ui/components/ui/empty"
function RouteError({ error, reset }: ErrorComponentProps) {
const router = useRouter()
diff --git a/apps/web/src/components/service-map/service-map-edge.tsx b/apps/web/src/components/service-map/service-map-edge.tsx
index 9fd5cf8..7c3cfed 100644
--- a/apps/web/src/components/service-map/service-map-edge.tsx
+++ b/apps/web/src/components/service-map/service-map-edge.tsx
@@ -1,6 +1,6 @@
import { memo, useId } from "react"
import { getSmoothStepPath, type EdgeProps } from "@xyflow/react"
-import { getServiceLegendColor } from "@/lib/colors"
+import { getServiceLegendColor } from "@maple/ui/colors"
import { useReducedMotion } from "@/hooks/use-reduced-motion"
import type { ServiceEdgeData } from "./service-map-utils"
diff --git a/apps/web/src/components/service-map/service-map-node.tsx b/apps/web/src/components/service-map/service-map-node.tsx
index 2be668c..3a99fa4 100644
--- a/apps/web/src/components/service-map/service-map-node.tsx
+++ b/apps/web/src/components/service-map/service-map-node.tsx
@@ -1,12 +1,12 @@
import { memo } from "react"
import { Handle, Position } from "@xyflow/react"
-import { cn } from "@/lib/utils"
-import { getServiceLegendColor } from "@/lib/colors"
+import { cn } from "@maple/ui/utils"
+import { getServiceLegendColor } from "@maple/ui/colors"
import {
Tooltip,
TooltipTrigger,
TooltipContent,
-} from "@/components/ui/tooltip"
+} from "@maple/ui/components/ui/tooltip"
import type { ServiceNodeData } from "./service-map-utils"
function formatRate(value: number): string {
diff --git a/apps/web/src/components/service-map/service-map-view.tsx b/apps/web/src/components/service-map/service-map-view.tsx
index c65d38a..5a7e264 100644
--- a/apps/web/src/components/service-map/service-map-view.tsx
+++ b/apps/web/src/components/service-map/service-map-view.tsx
@@ -14,7 +14,7 @@ import "@xyflow/react/dist/style.css"
import { Result, useAtomValue } from "@effect-atom/atom-react"
-import { getServiceLegendColor } from "@/lib/colors"
+import { getServiceLegendColor } from "@maple/ui/colors"
import { getServiceMapResultAtom, getServiceOverviewResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import type { GetServiceMapInput, ServiceEdge } from "@/api/tinybird/service-map"
import type { GetServiceOverviewInput, ServiceOverview } from "@/api/tinybird/services"
diff --git a/apps/web/src/components/services/services-filter-sidebar.tsx b/apps/web/src/components/services/services-filter-sidebar.tsx
index d00c6b3..8f65750 100644
--- a/apps/web/src/components/services/services-filter-sidebar.tsx
+++ b/apps/web/src/components/services/services-filter-sidebar.tsx
@@ -4,7 +4,7 @@ import { useNavigate } from "@tanstack/react-router"
import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import { FilterSection } from "@/components/traces/filter-section"
import { Route } from "@/routes/services/index"
-import { Separator } from "@/components/ui/separator"
+import { Separator } from "@maple/ui/components/ui/separator"
import { getServicesFacetsResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import {
FilterSidebarBody,
diff --git a/apps/web/src/components/services/services-table.tsx b/apps/web/src/components/services/services-table.tsx
index 239695b..9731a2a 100644
--- a/apps/web/src/components/services/services-table.tsx
+++ b/apps/web/src/components/services/services-table.tsx
@@ -9,15 +9,15 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Badge } from "@/components/ui/badge"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Sparkline } from "@/components/ui/gradient-chart"
+} from "@maple/ui/components/ui/table"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { Sparkline } from "@maple/ui/components/ui/gradient-chart"
import {
Tooltip,
TooltipTrigger,
TooltipContent,
-} from "@/components/ui/tooltip"
+} from "@maple/ui/components/ui/tooltip"
import {
type ServiceOverview,
type CommitBreakdown,
diff --git a/apps/web/src/components/settings/api-keys-section.tsx b/apps/web/src/components/settings/api-keys-section.tsx
index 3632d66..4d3175e 100644
--- a/apps/web/src/components/settings/api-keys-section.tsx
+++ b/apps/web/src/components/settings/api-keys-section.tsx
@@ -3,17 +3,17 @@ import { useState } from "react"
import { Exit } from "effect"
import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
+import { Button } from "@maple/ui/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
-import { Badge } from "@/components/ui/badge"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+} from "@maple/ui/components/ui/card"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Input } from "@maple/ui/components/ui/input"
+import { Label } from "@maple/ui/components/ui/label"
import {
Dialog,
DialogContent,
@@ -21,7 +21,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog"
+} from "@maple/ui/components/ui/dialog"
import {
AlertDialog,
AlertDialogAction,
@@ -32,21 +32,21 @@ import {
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
+} from "@maple/ui/components/ui/alert-dialog"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
-} from "@/components/ui/input-group"
+} from "@maple/ui/components/ui/input-group"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
-} from "@/components/ui/empty"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/empty"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import {
AlertWarningIcon,
CheckIcon,
diff --git a/apps/web/src/components/settings/billing-section.tsx b/apps/web/src/components/settings/billing-section.tsx
index e98c13b..fbd8a44 100644
--- a/apps/web/src/components/settings/billing-section.tsx
+++ b/apps/web/src/components/settings/billing-section.tsx
@@ -4,8 +4,8 @@ import { useCustomer } from "autumn-js/react"
import { PricingCards } from "./pricing-cards"
import { format } from "date-fns"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { Card, CardContent, CardHeader, CardTitle } from "@maple/ui/components/ui/card"
import { getServiceUsageResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import { formatForTinybird } from "@/lib/time-utils"
import { aggregateUsage } from "@/lib/billing/usage"
diff --git a/apps/web/src/components/settings/members-section.tsx b/apps/web/src/components/settings/members-section.tsx
index a6dba8b..c26e83d 100644
--- a/apps/web/src/components/settings/members-section.tsx
+++ b/apps/web/src/components/settings/members-section.tsx
@@ -2,14 +2,14 @@ import { useOrganization, useAuth } from "@clerk/clerk-react"
import { useState } from "react"
import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
+import { Button } from "@maple/ui/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
+} from "@maple/ui/components/ui/card"
import {
Table,
TableBody,
@@ -17,9 +17,9 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
-import { Badge } from "@/components/ui/badge"
+} from "@maple/ui/components/ui/table"
+import { Avatar, AvatarFallback, AvatarImage } from "@maple/ui/components/ui/avatar"
+import { Badge } from "@maple/ui/components/ui/badge"
import {
Dialog,
DialogContent,
@@ -27,22 +27,22 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+} from "@maple/ui/components/ui/dialog"
+import { Input } from "@maple/ui/components/ui/input"
+import { Label } from "@maple/ui/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select"
+} from "@maple/ui/components/ui/select"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
+} from "@maple/ui/components/ui/dropdown-menu"
import {
AlertDialog,
AlertDialogAction,
@@ -53,15 +53,15 @@ import {
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/alert-dialog"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
-} from "@/components/ui/empty"
+} from "@maple/ui/components/ui/empty"
import {
PlusIcon,
DotsVerticalIcon,
diff --git a/apps/web/src/components/settings/pricing-cards.tsx b/apps/web/src/components/settings/pricing-cards.tsx
index 62d3ffd..f24245d 100644
--- a/apps/web/src/components/settings/pricing-cards.tsx
+++ b/apps/web/src/components/settings/pricing-cards.tsx
@@ -7,7 +7,7 @@ type Product = NonNullable<
>[number]
type ProductItem = Product["items"][number]
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { getPlanFeatures } from "@/lib/billing/plans"
import {
Card,
@@ -16,12 +16,12 @@ import {
CardHeader,
CardTitle,
CardDescription,
-} from "@/components/ui/card"
-import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Spinner } from "@/components/ui/spinner"
+} from "@maple/ui/components/ui/card"
+import { Button } from "@maple/ui/components/ui/button"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Separator } from "@maple/ui/components/ui/separator"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { Spinner } from "@maple/ui/components/ui/spinner"
import {
Dialog,
DialogContent,
@@ -29,7 +29,7 @@ import {
DialogTitle,
DialogDescription,
DialogFooter,
-} from "@/components/ui/dialog"
+} from "@maple/ui/components/ui/dialog"
import {
FileIcon,
PulseIcon,
diff --git a/apps/web/src/components/settings/scrape-targets-section.tsx b/apps/web/src/components/settings/scrape-targets-section.tsx
index 4296c39..940cb2e 100644
--- a/apps/web/src/components/settings/scrape-targets-section.tsx
+++ b/apps/web/src/components/settings/scrape-targets-section.tsx
@@ -12,15 +12,15 @@ import {
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Button } from "@/components/ui/button"
+} from "@maple/ui/components/ui/alert-dialog"
+import { Button } from "@maple/ui/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
+} from "@maple/ui/components/ui/card"
import {
Dialog,
DialogContent,
@@ -28,18 +28,18 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
-import { Switch } from "@/components/ui/switch"
-import { Badge } from "@/components/ui/badge"
+} from "@maple/ui/components/ui/dialog"
+import { Input } from "@maple/ui/components/ui/input"
+import { Label } from "@maple/ui/components/ui/label"
+import { Switch } from "@maple/ui/components/ui/switch"
+import { Badge } from "@maple/ui/components/ui/badge"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select"
+} from "@maple/ui/components/ui/select"
import {
CircleCheckIcon,
CircleXmarkIcon,
diff --git a/apps/web/src/components/settings/usage-meters.tsx b/apps/web/src/components/settings/usage-meters.tsx
index 464726f..8cef8a8 100644
--- a/apps/web/src/components/settings/usage-meters.tsx
+++ b/apps/web/src/components/settings/usage-meters.tsx
@@ -6,7 +6,7 @@ import {
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
+} from "@maple/ui/components/ui/card"
import {
FileIcon,
PulseIcon,
@@ -16,7 +16,7 @@ import {
import type { AggregatedUsage } from "@/lib/billing/usage"
import { formatGB, usagePercentage } from "@/lib/billing/usage"
import type { PlanLimits } from "@/lib/billing/plans"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
interface MeterRowProps {
icon: IconComponent
diff --git a/apps/web/src/components/time-range-picker/custom-range-picker.tsx b/apps/web/src/components/time-range-picker/custom-range-picker.tsx
index 7322de0..94a1ade 100644
--- a/apps/web/src/components/time-range-picker/custom-range-picker.tsx
+++ b/apps/web/src/components/time-range-picker/custom-range-picker.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react"
-import { Calendar } from "@/components/ui/calendar"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
+import { Calendar } from "@maple/ui/components/ui/calendar"
+import { Button } from "@maple/ui/components/ui/button"
+import { Input } from "@maple/ui/components/ui/input"
import { format, parse, isValid, setHours, setMinutes } from "date-fns"
import type { DateRange } from "react-day-picker"
import { formatForTinybird } from "@/lib/time-utils"
diff --git a/apps/web/src/components/time-range-picker/preset-list.tsx b/apps/web/src/components/time-range-picker/preset-list.tsx
index 46c68de..649b2c5 100644
--- a/apps/web/src/components/time-range-picker/preset-list.tsx
+++ b/apps/web/src/components/time-range-picker/preset-list.tsx
@@ -1,5 +1,5 @@
import { PRESET_OPTIONS, type TimePreset } from "@/lib/time-utils"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
interface PresetListProps {
selectedValue?: string
diff --git a/apps/web/src/components/time-range-picker/quick-select-grid.tsx b/apps/web/src/components/time-range-picker/quick-select-grid.tsx
index 8ce2652..632ba58 100644
--- a/apps/web/src/components/time-range-picker/quick-select-grid.tsx
+++ b/apps/web/src/components/time-range-picker/quick-select-grid.tsx
@@ -1,5 +1,5 @@
import { QUICK_SELECT_OPTIONS, relativeToAbsolute } from "@/lib/time-utils"
-import { Button } from "@/components/ui/button"
+import { Button } from "@maple/ui/components/ui/button"
interface QuickSelectGridProps {
onSelect: (range: { startTime: string; endTime: string }, value: string, label: string) => void
diff --git a/apps/web/src/components/time-range-picker/recently-used.tsx b/apps/web/src/components/time-range-picker/recently-used.tsx
index 2839fd3..a4bd17e 100644
--- a/apps/web/src/components/time-range-picker/recently-used.tsx
+++ b/apps/web/src/components/time-range-picker/recently-used.tsx
@@ -1,5 +1,5 @@
import type { RecentTimeRange } from "@/hooks/use-recently-used-times"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
interface RecentlyUsedProps {
recentTimes: RecentTimeRange[]
diff --git a/apps/web/src/components/time-range-picker/shorthand-input.tsx b/apps/web/src/components/time-range-picker/shorthand-input.tsx
index 9df1d93..83af032 100644
--- a/apps/web/src/components/time-range-picker/shorthand-input.tsx
+++ b/apps/web/src/components/time-range-picker/shorthand-input.tsx
@@ -1,5 +1,5 @@
import { useState, useCallback } from "react"
-import { Input } from "@/components/ui/input"
+import { Input } from "@maple/ui/components/ui/input"
import { relativeToAbsolute } from "@/lib/time-utils"
interface ShorthandInputProps {
diff --git a/apps/web/src/components/time-range-picker/time-range-picker.tsx b/apps/web/src/components/time-range-picker/time-range-picker.tsx
index 0cd4771..2d4d825 100644
--- a/apps/web/src/components/time-range-picker/time-range-picker.tsx
+++ b/apps/web/src/components/time-range-picker/time-range-picker.tsx
@@ -1,8 +1,8 @@
import { useState, useCallback } from "react"
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
-import { Button } from "@/components/ui/button"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { Separator } from "@/components/ui/separator"
+import { Popover, PopoverContent, PopoverTrigger } from "@maple/ui/components/ui/popover"
+import { Button } from "@maple/ui/components/ui/button"
+import { ScrollArea } from "@maple/ui/components/ui/scroll-area"
+import { Separator } from "@maple/ui/components/ui/separator"
import { ClockIcon } from "@/components/icons"
import { formatTimeRangeDisplay, presetLabel, type TimePreset, relativeToAbsolute } from "@/lib/time-utils"
diff --git a/apps/web/src/components/traces/advanced-filter-dialog.tsx b/apps/web/src/components/traces/advanced-filter-dialog.tsx
new file mode 100644
index 0000000..0c140d8
--- /dev/null
+++ b/apps/web/src/components/traces/advanced-filter-dialog.tsx
@@ -0,0 +1,135 @@
+import * as React from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@maple/ui/components/ui/dialog"
+import { Button } from "@maple/ui/components/ui/button"
+import { MagnifierIcon } from "@/components/icons"
+import { WhereClauseEditor } from "@/components/query-builder/where-clause-editor"
+import type { WhereClauseAutocompleteValues } from "@/lib/query-builder/where-clause-autocomplete"
+
+interface AdvancedFilterDialogProps {
+ initialValue: string
+ onApply: (value: string) => void
+ autocompleteValues?: WhereClauseAutocompleteValues
+ onActiveAttributeKey?: (key: string | null) => void
+}
+
+export function AdvancedFilterDialog({
+ initialValue,
+ onApply,
+ autocompleteValues,
+ onActiveAttributeKey,
+}: AdvancedFilterDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [value, setValue] = React.useState(initialValue)
+
+ React.useEffect(() => {
+ if (open) {
+ setValue(initialValue)
+ }
+ }, [open, initialValue])
+
+ React.useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ // Open with 'f' if not typing in an input
+ if (
+ e.key.toLowerCase() === "f" &&
+ !e.ctrlKey &&
+ !e.metaKey &&
+ !e.altKey &&
+ e.target instanceof Element &&
+ !["INPUT", "TEXTAREA", "SELECT"].includes(e.target.tagName) &&
+ !(e.target as HTMLElement).isContentEditable
+ ) {
+ e.preventDefault()
+ setOpen(true)
+ }
+
+ // Cmd+Enter to apply when modal is open
+ if (open && e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault()
+ onApply(value)
+ setOpen(false)
+ }
+ }
+ window.addEventListener("keydown", handleKeyDown)
+ return () => window.removeEventListener("keydown", handleKeyDown)
+ }, [open, value, onApply])
+
+ const handleApply = () => {
+ onApply(value)
+ setOpen(false)
+ }
+
+ const handleClear = () => {
+ setValue("")
+ onApply("")
+ setOpen(false)
+ }
+
+ const hasActiveFilter = initialValue.trim().length > 0
+
+ return (
+
+
+
+ Advanced Filter
+
+ F
+
+
+ }
+ />
+
+
+ Advanced Filter
+
+ Write SQL-like queries to filter traces. Use Ctrl+Space for autocomplete. Press Cmd+Enter to apply.
+
+
+
+
+
+
+
+
+ Clear Filter
+
+
+ setOpen(false)}>
+ Cancel
+
+ Apply Filter
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/traces/duration-range-filter.tsx b/apps/web/src/components/traces/duration-range-filter.tsx
index 0e3f71b..00fc770 100644
--- a/apps/web/src/components/traces/duration-range-filter.tsx
+++ b/apps/web/src/components/traces/duration-range-filter.tsx
@@ -1,14 +1,14 @@
import * as React from "react"
import { ChevronDownIcon } from "@/components/icons"
-import { cn } from "@/lib/utils"
-import { Input } from "@/components/ui/input"
-import { Label } from "@/components/ui/label"
+import { cn } from "@maple/ui/utils"
+import { Input } from "@maple/ui/components/ui/input"
+import { Label } from "@maple/ui/components/ui/label"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
-} from "@/components/ui/collapsible"
+} from "@maple/ui/components/ui/collapsible"
interface DurationRangeFilterProps {
minValue: number | undefined
diff --git a/apps/web/src/components/traces/flamegraph-minimap.tsx b/apps/web/src/components/traces/flamegraph-minimap.tsx
index ba344bf..30a83a0 100644
--- a/apps/web/src/components/traces/flamegraph-minimap.tsx
+++ b/apps/web/src/components/traces/flamegraph-minimap.tsx
@@ -1,5 +1,5 @@
import * as React from "react"
-import { getSpanColorStyle } from "@/lib/colors"
+import { getSpanColorStyle } from "@maple/ui/colors"
import type { SpanNode } from "@/api/tinybird/traces"
interface FlamegraphMinimapProps {
diff --git a/apps/web/src/components/traces/flamegraph-tooltip.tsx b/apps/web/src/components/traces/flamegraph-tooltip.tsx
index cfacd80..f981693 100644
--- a/apps/web/src/components/traces/flamegraph-tooltip.tsx
+++ b/apps/web/src/components/traces/flamegraph-tooltip.tsx
@@ -1,5 +1,5 @@
import { formatDuration } from "@/lib/format"
-import { getServiceLegendColor, calculateSelfTime } from "@/lib/colors"
+import { getServiceLegendColor, calculateSelfTime } from "@maple/ui/colors"
import type { SpanNode } from "@/api/tinybird/traces"
interface FlamegraphTooltipProps {
diff --git a/apps/web/src/components/traces/flamegraph.tsx b/apps/web/src/components/traces/flamegraph.tsx
index 6d2c51f..11b855b 100644
--- a/apps/web/src/components/traces/flamegraph.tsx
+++ b/apps/web/src/components/traces/flamegraph.tsx
@@ -2,13 +2,13 @@ import * as React from "react"
import { XmarkIcon } from "@/components/icons"
-import { Button } from "@/components/ui/button"
-import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
+import { Button } from "@maple/ui/components/ui/button"
+import { Tooltip, TooltipTrigger, TooltipContent } from "@maple/ui/components/ui/tooltip"
import { FlamegraphTooltipContent } from "./flamegraph-tooltip"
import { FlamegraphMinimap } from "./flamegraph-minimap"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { formatDuration } from "@/lib/format"
-import { getSpanColorStyle, getServiceLegendColor } from "@/lib/colors"
+import { getSpanColorStyle, getServiceLegendColor } from "@maple/ui/colors"
import { getCacheInfo } from "@/lib/cache"
import type { SpanNode } from "@/api/tinybird/traces"
diff --git a/apps/web/src/components/traces/flow-node.tsx b/apps/web/src/components/traces/flow-node.tsx
index dba3ccc..0d597ff 100644
--- a/apps/web/src/components/traces/flow-node.tsx
+++ b/apps/web/src/components/traces/flow-node.tsx
@@ -11,9 +11,9 @@ import {
} from "@/components/icons"
import type { IconComponent } from "@/components/icons"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { formatDuration } from "@/lib/format"
-import { getSpanColorStyle, extractClassName } from "@/lib/colors"
+import { getSpanColorStyle, extractClassName } from "@maple/ui/colors"
import { getCacheInfo, cacheResultStyles, CACHE_OPERATION_COLORS } from "@/lib/cache"
import type { CacheInfo } from "@/lib/cache"
import type { FlowNodeData, AggregatedDuration } from "./flow-utils"
diff --git a/apps/web/src/components/traces/flow-view.tsx b/apps/web/src/components/traces/flow-view.tsx
index 645a315..a4d87f5 100644
--- a/apps/web/src/components/traces/flow-view.tsx
+++ b/apps/web/src/components/traces/flow-view.tsx
@@ -13,8 +13,8 @@ import "@xyflow/react/dist/style.css"
import { EyeIcon } from "@/components/icons"
-import { Button } from "@/components/ui/button"
-import { getServiceLegendColor } from "@/lib/colors"
+import { Button } from "@maple/ui/components/ui/button"
+import { getServiceLegendColor } from "@maple/ui/colors"
import { FlowSpanNode } from "./flow-node"
import {
transformSpansToFlow,
diff --git a/apps/web/src/components/traces/span-detail-panel.tsx b/apps/web/src/components/traces/span-detail-panel.tsx
index fed2128..88dc439 100644
--- a/apps/web/src/components/traces/span-detail-panel.tsx
+++ b/apps/web/src/components/traces/span-detail-panel.tsx
@@ -3,16 +3,16 @@ import { Result, useAtomValue } from "@effect-atom/atom-react"
import { XmarkIcon, ClockIcon, CircleWarningIcon, ChevronDownIcon, ChevronUpIcon, CopyIcon } from "@/components/icons"
import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
-import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"
-import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible"
-import { Badge } from "@/components/ui/badge"
-import { Skeleton } from "@/components/ui/skeleton"
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
-import { ScrollArea } from "@/components/ui/scroll-area"
+import { Button } from "@maple/ui/components/ui/button"
+import { Alert, AlertTitle, AlertDescription } from "@maple/ui/components/ui/alert"
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@maple/ui/components/ui/collapsible"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@maple/ui/components/ui/tabs"
+import { ScrollArea } from "@maple/ui/components/ui/scroll-area"
import { type Log, type LogsResponse } from "@/api/tinybird/logs"
import { formatDuration } from "@/lib/format"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { getCacheInfo, cacheResultStyles } from "@/lib/cache"
import type { SpanNode } from "@/api/tinybird/traces"
import { disabledResultAtom } from "@/lib/services/atoms/disabled-result-atom"
diff --git a/apps/web/src/components/traces/span-row.tsx b/apps/web/src/components/traces/span-row.tsx
index 65004cb..df903a0 100644
--- a/apps/web/src/components/traces/span-row.tsx
+++ b/apps/web/src/components/traces/span-row.tsx
@@ -1,8 +1,8 @@
import { ChevronRightIcon, ChevronDownIcon } from "@/components/icons"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { cn } from "@/lib/utils"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Button } from "@maple/ui/components/ui/button"
+import { cn } from "@maple/ui/utils"
import { formatDuration } from "@/lib/format"
import { getCacheInfo, cacheResultStyles } from "@/lib/cache"
import type { SpanNode } from "@/api/tinybird/traces"
diff --git a/apps/web/src/components/traces/trace-view-tabs.tsx b/apps/web/src/components/traces/trace-view-tabs.tsx
index 20e9040..730e951 100644
--- a/apps/web/src/components/traces/trace-view-tabs.tsx
+++ b/apps/web/src/components/traces/trace-view-tabs.tsx
@@ -1,6 +1,6 @@
import { MenuIcon, FireIcon, NetworkNodesIcon } from "@/components/icons"
-import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@maple/ui/components/ui/tabs"
import { SpanHierarchy } from "./span-hierarchy"
import { Flamegraph } from "./flamegraph"
import { TraceFlowView } from "./flow-view"
diff --git a/apps/web/src/components/traces/traces-filter-sidebar.tsx b/apps/web/src/components/traces/traces-filter-sidebar.tsx
index b411b7f..c72f153 100644
--- a/apps/web/src/components/traces/traces-filter-sidebar.tsx
+++ b/apps/web/src/components/traces/traces-filter-sidebar.tsx
@@ -1,7 +1,6 @@
-import { Result, useAtomValue } from "@effect-atom/atom-react"
+import { Result } from "@effect-atom/atom-react"
import { useNavigate } from "@tanstack/react-router"
-import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import {
FilterSection,
SearchableFilterSection,
@@ -9,8 +8,8 @@ import {
} from "./filter-section"
import { DurationRangeFilter } from "./duration-range-filter"
import { Route } from "@/routes/traces"
-import { Separator } from "@/components/ui/separator"
-import { getTracesFacetsResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
+import { Separator } from "@maple/ui/components/ui/separator"
+import type { TracesFacetsResponse } from "@/api/tinybird/traces"
import {
FilterSidebarBody,
FilterSidebarError,
@@ -23,30 +22,13 @@ function LoadingState() {
return
}
-export function TracesFilterSidebar() {
+interface TracesFilterSidebarProps {
+ facetsResult: Result.Result
+}
+
+export function TracesFilterSidebar({ facetsResult }: TracesFilterSidebarProps) {
const navigate = useNavigate({ from: Route.fullPath })
const search = Route.useSearch()
- const { startTime: effectiveStartTime, endTime: effectiveEndTime } = useEffectiveTimeRange(
- search.startTime,
- search.endTime,
- )
-
- const facetsResult = useAtomValue(
- getTracesFacetsResultAtom({
- data: {
- startTime: effectiveStartTime,
- endTime: effectiveEndTime,
- service: search.services?.[0],
- spanName: search.spanNames?.[0],
- hasError: search.hasError,
- minDurationMs: search.minDurationMs,
- maxDurationMs: search.maxDurationMs,
- httpMethod: search.httpMethods?.[0],
- httpStatusCode: search.httpStatusCodes?.[0],
- deploymentEnv: search.deploymentEnvs?.[0],
- },
- }),
- )
const updateFilter = (
key: K,
@@ -55,9 +37,10 @@ export function TracesFilterSidebar() {
navigate({
search: (prev) => ({
...prev,
- [key]: value === undefined || (Array.isArray(value) && value.length === 0)
- ? undefined
- : value,
+ [key]:
+ value === undefined || (Array.isArray(value) && value.length === 0)
+ ? undefined
+ : value,
}),
})
}
@@ -67,7 +50,6 @@ export function TracesFilterSidebar() {
search: {
startTime: search.startTime,
endTime: search.endTime,
- rootOnly: search.rootOnly,
},
})
}
@@ -75,13 +57,13 @@ export function TracesFilterSidebar() {
const hasActiveFilters =
(search.services?.length ?? 0) > 0 ||
(search.spanNames?.length ?? 0) > 0 ||
+ (search.deploymentEnvs?.length ?? 0) > 0 ||
(search.httpMethods?.length ?? 0) > 0 ||
(search.httpStatusCodes?.length ?? 0) > 0 ||
- (search.deploymentEnvs?.length ?? 0) > 0 ||
- search.hasError ||
+ search.hasError !== undefined ||
search.minDurationMs !== undefined ||
search.maxDurationMs !== undefined ||
- search.rootOnly === false
+ search.attributeKey !== undefined
return Result.builder(facetsResult)
.onInitial(() => )
diff --git a/apps/web/src/components/traces/traces-table.tsx b/apps/web/src/components/traces/traces-table.tsx
index e0f2341..991c37b 100644
--- a/apps/web/src/components/traces/traces-table.tsx
+++ b/apps/web/src/components/traces/traces-table.tsx
@@ -9,9 +9,9 @@ import {
TableHead,
TableHeader,
TableRow,
-} from "@/components/ui/table"
-import { Badge } from "@/components/ui/badge"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "@maple/ui/components/ui/table"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import { type Trace } from "@/api/tinybird/traces"
import { listTracesResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
import type { TracesSearchParams } from "@/routes/traces"
@@ -125,6 +125,8 @@ export function TracesTable({ filters }: TracesTableProps) {
httpMethod: filters?.httpMethods?.[0],
httpStatusCode: filters?.httpStatusCodes?.[0],
deploymentEnv: filters?.deploymentEnvs?.[0],
+ attributeKey: filters?.attributeKey,
+ attributeValue: filters?.attributeValue,
startTime: effectiveStartTime,
endTime: effectiveEndTime,
rootOnly: filters?.rootOnly,
diff --git a/apps/web/src/lib/query-builder/where-clause-autocomplete.test.ts b/apps/web/src/lib/query-builder/where-clause-autocomplete.test.ts
index aa3e0ee..0bdf049 100644
--- a/apps/web/src/lib/query-builder/where-clause-autocomplete.test.ts
+++ b/apps/web/src/lib/query-builder/where-clause-autocomplete.test.ts
@@ -119,4 +119,51 @@ describe("where clause autocomplete", () => {
expect(applied.expression).toBe('service.name = "checkout" AND ')
})
+
+ it("supports trace_search key suggestions without changing default traces scope", () => {
+ const defaultScope = getWhereClauseAutocomplete({
+ expression: "http",
+ cursor: 4,
+ dataSource: "traces",
+ })
+ const traceScope = getWhereClauseAutocomplete({
+ expression: "http",
+ cursor: 4,
+ dataSource: "traces",
+ scope: "trace_search",
+ })
+
+ expect(
+ defaultScope.suggestions.some((item) => item.insertText === "http.method"),
+ ).toBe(false)
+ expect(
+ traceScope.suggestions.some((item) => item.insertText === "http.method"),
+ ).toBe(true)
+ })
+
+ it("suggests trace_search values for HTTP and booleans", () => {
+ const method = getWhereClauseAutocomplete({
+ expression: "http.method = ge",
+ cursor: "http.method = ge".length,
+ dataSource: "traces",
+ scope: "trace_search",
+ values: {
+ httpMethods: ["GET", "POST"],
+ },
+ })
+ const hasError = getWhereClauseAutocomplete({
+ expression: "has_error = ",
+ cursor: "has_error = ".length,
+ dataSource: "traces",
+ scope: "trace_search",
+ })
+
+ expect(method.context).toBe("value")
+ expect(method.suggestions[0]?.label).toBe("GET")
+ expect(method.suggestions[0]?.insertText).toBe('"GET"')
+ expect(hasError.suggestions.map((item) => item.insertText)).toEqual([
+ "true",
+ "false",
+ ])
+ })
})
diff --git a/apps/web/src/lib/query-builder/where-clause-autocomplete.ts b/apps/web/src/lib/query-builder/where-clause-autocomplete.ts
index 2a798ad..bb68be1 100644
--- a/apps/web/src/lib/query-builder/where-clause-autocomplete.ts
+++ b/apps/web/src/lib/query-builder/where-clause-autocomplete.ts
@@ -10,12 +10,16 @@ export type WhereClauseAutocompleteContext =
| "value"
| "conjunction"
+export type WhereClauseAutocompleteScope = "default" | "trace_search"
+
export interface WhereClauseAutocompleteValues {
services?: string[]
spanNames?: string[]
environments?: string[]
commitShas?: string[]
severities?: string[]
+ httpMethods?: string[]
+ httpStatusCodes?: string[]
metricTypes?: QueryBuilderMetricType[]
attributeKeys?: string[]
attributeValues?: string[]
@@ -43,6 +47,7 @@ interface WhereClauseAutocompleteInput {
cursor: number
dataSource: QueryBuilderDataSource
values?: WhereClauseAutocompleteValues
+ scope?: WhereClauseAutocompleteScope
maxSuggestions?: number
}
@@ -127,6 +132,59 @@ const KEY_DEFINITIONS: Record = {
],
}
+const TRACE_SEARCH_KEY_DEFINITIONS: KeyDefinition[] = [
+ {
+ label: "service.name",
+ insertText: "service.name",
+ description: "Filter by service",
+ },
+ {
+ label: "span.name",
+ insertText: "span.name",
+ description: "Filter by root span name",
+ },
+ {
+ label: "deployment.environment",
+ insertText: "deployment.environment",
+ description: "Filter by deployment environment",
+ },
+ {
+ label: "http.method",
+ insertText: "http.method",
+ description: "Filter by HTTP method",
+ },
+ {
+ label: "http.status_code",
+ insertText: "http.status_code",
+ description: "Filter by HTTP status code",
+ },
+ {
+ label: "has_error",
+ insertText: "has_error",
+ description: "true or false",
+ },
+ {
+ label: "root_only",
+ insertText: "root_only",
+ description: "true or false",
+ },
+ {
+ label: "min_duration_ms",
+ insertText: "min_duration_ms",
+ description: "Minimum duration in milliseconds",
+ },
+ {
+ label: "max_duration_ms",
+ insertText: "max_duration_ms",
+ description: "Maximum duration in milliseconds",
+ },
+ {
+ label: "attr.",
+ insertText: "attr.",
+ description: "Filter by a single span attribute",
+ },
+]
+
function isSpace(char: string | undefined): boolean {
return char == null || /\s/.test(char)
}
@@ -424,6 +482,26 @@ function normalizeKey(input: string | null): string {
return "root_only"
}
+ if (normalized === "has_error") {
+ return "has_error"
+ }
+
+ if (normalized === "http.method") {
+ return "http.method"
+ }
+
+ if (normalized === "http.status_code") {
+ return "http.status_code"
+ }
+
+ if (normalized === "min_duration_ms") {
+ return "min_duration_ms"
+ }
+
+ if (normalized === "max_duration_ms") {
+ return "max_duration_ms"
+ }
+
if (normalized.startsWith("attr.")) {
return "attr.*"
}
@@ -524,6 +602,7 @@ function buildValueSuggestions(
key: string | null,
dataSource: QueryBuilderDataSource,
values: WhereClauseAutocompleteValues | undefined,
+ scope: WhereClauseAutocompleteScope,
): WhereClauseAutocompleteSuggestion[] {
const normalizedKey = normalizeKey(key)
@@ -544,6 +623,23 @@ function buildValueSuggestions(
]
}
+ if (scope === "trace_search" && normalizedKey === "has_error") {
+ return [
+ {
+ id: "value:has_error:true",
+ kind: "value",
+ label: "true",
+ insertText: "true",
+ },
+ {
+ id: "value:has_error:false",
+ kind: "value",
+ label: "false",
+ insertText: "false",
+ },
+ ]
+ }
+
if (normalizedKey === "metric.type") {
const metricTypes =
values?.metricTypes && values.metricTypes.length > 0
@@ -559,6 +655,12 @@ function buildValueSuggestions(
"deployment.environment": uniqueValues(values?.environments ?? []),
"deployment.commit_sha": uniqueValues(values?.commitShas ?? []),
severity: uniqueValues(values?.severities ?? []),
+ ...(scope === "trace_search"
+ ? {
+ "http.method": uniqueValues(values?.httpMethods ?? []),
+ "http.status_code": uniqueValues(values?.httpStatusCodes ?? []),
+ }
+ : {}),
}
const explicit = mappedValues[normalizedKey]
@@ -619,6 +721,7 @@ function buildSuggestions(
parsed: ParsedWhereClauseContext,
dataSource: QueryBuilderDataSource,
values: WhereClauseAutocompleteValues | undefined,
+ scope: WhereClauseAutocompleteScope,
maxSuggestions: number,
): WhereClauseAutocompleteSuggestion[] {
if (parsed.context === "key") {
@@ -635,7 +738,12 @@ function buildSuggestions(
return filterAndRankSuggestions(attrSuggestions, query, maxSuggestions)
}
- const keySuggestions = KEY_DEFINITIONS[dataSource].map((keyDef) =>
+ const keyDefinitions =
+ scope === "trace_search" && dataSource === "traces"
+ ? TRACE_SEARCH_KEY_DEFINITIONS
+ : KEY_DEFINITIONS[dataSource]
+
+ const keySuggestions = keyDefinitions.map((keyDef) =>
toSuggestion(
{
id: `key:${keyDef.insertText}`,
@@ -665,7 +773,12 @@ function buildSuggestions(
}
if (parsed.context === "value") {
- const valueSuggestions = buildValueSuggestions(parsed.key, dataSource, values)
+ const valueSuggestions = buildValueSuggestions(
+ parsed.key,
+ dataSource,
+ values,
+ scope,
+ )
return filterAndRankSuggestions(valueSuggestions, parsed.query, maxSuggestions)
}
@@ -687,10 +800,11 @@ export function getWhereClauseAutocomplete({
cursor,
dataSource,
values,
+ scope = "default",
maxSuggestions = 8,
}: WhereClauseAutocompleteInput): WhereClauseAutocompleteResult {
const parsed = parseWhereClauseContext(expression, cursor)
- const suggestions = buildSuggestions(parsed, dataSource, values, maxSuggestions)
+ const suggestions = buildSuggestions(parsed, dataSource, values, scope, maxSuggestions)
return {
context: parsed.context,
diff --git a/apps/web/src/lib/traces/advanced-filter-sync.test.ts b/apps/web/src/lib/traces/advanced-filter-sync.test.ts
new file mode 100644
index 0000000..7ce15d1
--- /dev/null
+++ b/apps/web/src/lib/traces/advanced-filter-sync.test.ts
@@ -0,0 +1,206 @@
+import { describe, expect, it } from "vitest"
+
+import {
+ applyWhereClause,
+ parseWhereClause,
+ toWhereClause,
+} from "@/lib/traces/advanced-filter-sync"
+
+describe("parseWhereClause", () => {
+ it("parses service.name", () => {
+ const { filters } = parseWhereClause('service.name = "checkout"')
+ expect(filters.service).toBe("checkout")
+ })
+
+ it("parses service alias", () => {
+ const { filters } = parseWhereClause('service = "checkout"')
+ expect(filters.service).toBe("checkout")
+ })
+
+ it("parses span.name", () => {
+ const { filters } = parseWhereClause('span.name = "GET /orders"')
+ expect(filters.spanName).toBe("GET /orders")
+ })
+
+ it("parses deployment.environment and aliases", () => {
+ expect(parseWhereClause('deployment.environment = "production"').filters.deploymentEnv).toBe("production")
+ expect(parseWhereClause('environment = "staging"').filters.deploymentEnv).toBe("staging")
+ expect(parseWhereClause('env = "dev"').filters.deploymentEnv).toBe("dev")
+ })
+
+ it("parses http.method and http.status_code", () => {
+ const { filters } = parseWhereClause('http.method = "POST" AND http.status_code = "404"')
+ expect(filters.httpMethod).toBe("POST")
+ expect(filters.httpStatusCode).toBe("404")
+ })
+
+ it("parses has_error = true", () => {
+ const { filters } = parseWhereClause("has_error = true")
+ expect(filters.hasError).toBe(true)
+ })
+
+ it("drops has_error = false", () => {
+ const { filters } = parseWhereClause("has_error = false")
+ expect(filters.hasError).toBeUndefined()
+ })
+
+ it("parses root_only = false", () => {
+ const { filters } = parseWhereClause("root_only = false")
+ expect(filters.rootOnly).toBe(false)
+ })
+
+ it("drops root_only = true", () => {
+ const { filters } = parseWhereClause("root_only = true")
+ expect(filters.rootOnly).toBeUndefined()
+ })
+
+ it("parses duration bounds", () => {
+ const { filters } = parseWhereClause("min_duration_ms = 25 AND max_duration_ms = 1500")
+ expect(filters.minDurationMs).toBe(25)
+ expect(filters.maxDurationMs).toBe(1500)
+ })
+
+ it("parses attr.* keys", () => {
+ const { filters } = parseWhereClause('attr.http.route = "/orders/:id"')
+ expect(filters.attributeKey).toBe("http.route")
+ expect(filters.attributeValue).toBe("/orders/:id")
+ })
+
+ it("marks incomplete clauses for unclosed quotes", () => {
+ const result = parseWhereClause('service.name = "check')
+ expect(result.hasIncompleteClauses).toBe(true)
+ })
+
+ it("marks invalid number as incomplete", () => {
+ const result = parseWhereClause("min_duration_ms = nope")
+ expect(result.hasIncompleteClauses).toBe(true)
+ expect(result.filters.minDurationMs).toBeUndefined()
+ })
+
+ it("returns empty for empty input", () => {
+ const result = parseWhereClause("")
+ expect(result.filters).toEqual({})
+ expect(result.hasIncompleteClauses).toBe(false)
+ })
+
+ it("parses a full combined clause", () => {
+ const { filters } = parseWhereClause(
+ 'service = "checkout" AND span = "GET /orders" AND env = "production" AND http.method = "POST" AND http.status_code = "404" AND has_error = true AND root_only = false AND min_duration_ms = 12.5 AND max_duration_ms = 88 AND attr.http.route = "/api/orders"',
+ )
+
+ expect(filters.service).toBe("checkout")
+ expect(filters.spanName).toBe("GET /orders")
+ expect(filters.deploymentEnv).toBe("production")
+ expect(filters.httpMethod).toBe("POST")
+ expect(filters.httpStatusCode).toBe("404")
+ expect(filters.hasError).toBe(true)
+ expect(filters.rootOnly).toBe(false)
+ expect(filters.minDurationMs).toBe(12.5)
+ expect(filters.maxDurationMs).toBe(88)
+ expect(filters.attributeKey).toBe("http.route")
+ expect(filters.attributeValue).toBe("/api/orders")
+ })
+})
+
+describe("toWhereClause", () => {
+ it("builds a where clause from filters", () => {
+ const clause = toWhereClause({
+ service: "checkout",
+ spanName: "GET /orders",
+ hasError: true,
+ minDurationMs: 25,
+ })
+ expect(clause).toBe(
+ 'service.name = "checkout" AND span.name = "GET /orders" AND has_error = true AND min_duration_ms = 25',
+ )
+ })
+
+ it("returns undefined for empty filters", () => {
+ expect(toWhereClause({})).toBeUndefined()
+ })
+
+ it("includes attr.* clauses", () => {
+ const clause = toWhereClause({
+ attributeKey: "http.route",
+ attributeValue: "/orders/:id",
+ })
+ expect(clause).toBe('attr.http.route = "/orders/:id"')
+ })
+})
+
+describe("applyWhereClause", () => {
+ it("merges parsed values into search params", () => {
+ const result = applyWhereClause(
+ { startTime: "2026-02-01 00:00:00", endTime: "2026-02-01 01:00:00" },
+ 'service.name = "checkout" AND has_error = true',
+ )
+
+ expect(result.whereClause).toBe('service.name = "checkout" AND has_error = true')
+ expect(result.services).toEqual(["checkout"])
+ expect(result.hasError).toBe(true)
+ expect(result.startTime).toBe("2026-02-01 00:00:00")
+ expect(result.endTime).toBe("2026-02-01 01:00:00")
+ })
+
+ it("preserves existing search params when clause doesn't override them", () => {
+ const result = applyWhereClause(
+ {
+ services: ["billing"],
+ hasError: true,
+ startTime: "2026-02-01 00:00:00",
+ },
+ 'span.name = "POST /pay"',
+ )
+
+ expect(result.spanNames).toEqual(["POST /pay"])
+ expect(result.services).toEqual(["billing"])
+ expect(result.hasError).toBe(true)
+ })
+
+ it("overrides search params when clause includes them", () => {
+ const result = applyWhereClause(
+ { services: ["billing"] },
+ 'service.name = "checkout"',
+ )
+
+ expect(result.services).toEqual(["checkout"])
+ })
+
+ it("clears all filter params when clause is empty", () => {
+ const result = applyWhereClause(
+ {
+ services: ["checkout"],
+ hasError: true,
+ minDurationMs: 100,
+ startTime: "2026-02-01 00:00:00",
+ },
+ "",
+ )
+
+ expect(result.whereClause).toBeUndefined()
+ expect(result.services).toBeUndefined()
+ expect(result.hasError).toBeUndefined()
+ expect(result.minDurationMs).toBeUndefined()
+ expect(result.startTime).toBe("2026-02-01 00:00:00")
+ })
+
+ it("handles incomplete clauses gracefully", () => {
+ const result = applyWhereClause(
+ { services: ["billing"] },
+ 'service.name = "check',
+ )
+
+ expect(result.whereClause).toBe('service.name = "check')
+ expect(result.services).toEqual(["billing"])
+ })
+
+ it("handles whitespace-only clause as empty", () => {
+ const result = applyWhereClause(
+ { services: ["checkout"] },
+ " ",
+ )
+
+ expect(result.whereClause).toBeUndefined()
+ expect(result.services).toBeUndefined()
+ })
+})
diff --git a/apps/web/src/lib/traces/advanced-filter-sync.ts b/apps/web/src/lib/traces/advanced-filter-sync.ts
new file mode 100644
index 0000000..788e62c
--- /dev/null
+++ b/apps/web/src/lib/traces/advanced-filter-sync.ts
@@ -0,0 +1,293 @@
+export interface TracesSearchLike {
+ services?: string[]
+ spanNames?: string[]
+ hasError?: boolean
+ minDurationMs?: number
+ maxDurationMs?: number
+ httpMethods?: string[]
+ httpStatusCodes?: string[]
+ deploymentEnvs?: string[]
+ startTime?: string
+ endTime?: string
+ rootOnly?: boolean
+ whereClause?: string
+ attributeKey?: string
+ attributeValue?: string
+}
+
+export interface ParsedWhereClauseFilters {
+ service?: string
+ spanName?: string
+ deploymentEnv?: string
+ httpMethod?: string
+ httpStatusCode?: string
+ hasError?: true
+ rootOnly?: false
+ minDurationMs?: number
+ maxDurationMs?: number
+ attributeKey?: string
+ attributeValue?: string
+}
+
+const TRUE_VALUES = new Set(["1", "true", "yes", "y"])
+const FALSE_VALUES = new Set(["0", "false", "no", "n"])
+
+const CLAUSE_RE = /^([a-zA-Z0-9_.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s]+))$/
+
+function parseBoolean(value: string): boolean | null {
+ const normalized = value.trim().toLowerCase()
+ if (TRUE_VALUES.has(normalized)) {
+ return true
+ }
+
+ if (FALSE_VALUES.has(normalized)) {
+ return false
+ }
+
+ return null
+}
+
+function parseNumber(value: string): number | null {
+ if (!value.trim()) {
+ return null
+ }
+
+ const parsed = Number(value)
+ if (!Number.isFinite(parsed)) {
+ return null
+ }
+
+ return parsed
+}
+
+function quoteValue(value: string): string {
+ return `"${value.replace(/\\/g, "\\\\").replace(/\"/g, '\\\"')}"`
+}
+
+export function parseWhereClause(whereClause: string | undefined): {
+ filters: ParsedWhereClauseFilters
+ hasIncompleteClauses: boolean
+} {
+ if (!whereClause || !whereClause.trim()) {
+ return {
+ filters: {},
+ hasIncompleteClauses: false,
+ }
+ }
+
+ const parts = whereClause
+ .trim()
+ .split(/\s+AND\s+/i)
+ .map((part) => part.trim())
+ .filter(Boolean)
+
+ const parsed: ParsedWhereClauseFilters = {}
+ let hasIncompleteClauses = false
+
+ for (const part of parts) {
+ const match = part.match(CLAUSE_RE)
+ if (!match) {
+ hasIncompleteClauses = true
+ continue
+ }
+
+ const unquotedToken = match[4]
+ if (
+ unquotedToken &&
+ (unquotedToken.startsWith("\"") || unquotedToken.startsWith("'"))
+ ) {
+ hasIncompleteClauses = true
+ continue
+ }
+
+ const rawKey = match[1]?.trim().toLowerCase()
+ const rawValue = (match[2] ?? match[3] ?? match[4] ?? "").trim()
+ if (!rawKey || !rawValue) {
+ continue
+ }
+
+ if (rawKey === "service" || rawKey === "service.name") {
+ parsed.service = rawValue
+ continue
+ }
+
+ if (rawKey === "span" || rawKey === "span.name") {
+ parsed.spanName = rawValue
+ continue
+ }
+
+ if (
+ rawKey === "deployment.environment" ||
+ rawKey === "environment" ||
+ rawKey === "env"
+ ) {
+ parsed.deploymentEnv = rawValue
+ continue
+ }
+
+ if (rawKey === "http.method") {
+ parsed.httpMethod = rawValue
+ continue
+ }
+
+ if (rawKey === "http.status_code") {
+ parsed.httpStatusCode = rawValue
+ continue
+ }
+
+ if (rawKey === "has_error") {
+ const boolValue = parseBoolean(rawValue)
+ if (boolValue === null) {
+ hasIncompleteClauses = true
+ } else {
+ parsed.hasError = boolValue === true ? true : undefined
+ }
+ continue
+ }
+
+ if (rawKey === "root_only" || rawKey === "root.only") {
+ const boolValue = parseBoolean(rawValue)
+ if (boolValue === null) {
+ hasIncompleteClauses = true
+ } else {
+ parsed.rootOnly = boolValue === false ? false : undefined
+ }
+ continue
+ }
+
+ if (rawKey === "min_duration_ms") {
+ const numeric = parseNumber(rawValue)
+ if (numeric === null) {
+ hasIncompleteClauses = true
+ } else {
+ parsed.minDurationMs = numeric
+ }
+ continue
+ }
+
+ if (rawKey === "max_duration_ms") {
+ const numeric = parseNumber(rawValue)
+ if (numeric === null) {
+ hasIncompleteClauses = true
+ } else {
+ parsed.maxDurationMs = numeric
+ }
+ continue
+ }
+
+ if (rawKey.startsWith("attr.")) {
+ const attributeKey = rawKey.slice(5).trim()
+ if (!attributeKey || parsed.attributeKey) {
+ continue
+ }
+
+ parsed.attributeKey = attributeKey
+ parsed.attributeValue = rawValue
+ }
+ }
+
+ return {
+ filters: parsed,
+ hasIncompleteClauses,
+ }
+}
+
+export function toWhereClause(filters: ParsedWhereClauseFilters): string | undefined {
+ const clauses: string[] = []
+
+ if (filters.service) {
+ clauses.push(`service.name = ${quoteValue(filters.service)}`)
+ }
+
+ if (filters.spanName) {
+ clauses.push(`span.name = ${quoteValue(filters.spanName)}`)
+ }
+
+ if (filters.deploymentEnv) {
+ clauses.push(`deployment.environment = ${quoteValue(filters.deploymentEnv)}`)
+ }
+
+ if (filters.httpMethod) {
+ clauses.push(`http.method = ${quoteValue(filters.httpMethod)}`)
+ }
+
+ if (filters.httpStatusCode) {
+ clauses.push(`http.status_code = ${quoteValue(filters.httpStatusCode)}`)
+ }
+
+ if (filters.hasError === true) {
+ clauses.push("has_error = true")
+ }
+
+ if (filters.rootOnly === false) {
+ clauses.push("root_only = false")
+ }
+
+ if (typeof filters.minDurationMs === "number") {
+ clauses.push(`min_duration_ms = ${String(filters.minDurationMs)}`)
+ }
+
+ if (typeof filters.maxDurationMs === "number") {
+ clauses.push(`max_duration_ms = ${String(filters.maxDurationMs)}`)
+ }
+
+ if (filters.attributeKey && filters.attributeValue) {
+ clauses.push(
+ `attr.${filters.attributeKey} = ${quoteValue(filters.attributeValue)}`,
+ )
+ }
+
+ if (clauses.length === 0) {
+ return undefined
+ }
+
+ return clauses.join(" AND ")
+}
+
+/**
+ * One-way transform: parses a where clause string and merges the parsed
+ * filter values into the search params. Does NOT reverse-sync checkboxes
+ * back into whereClause text.
+ */
+export function applyWhereClause(
+ search: TracesSearchLike,
+ whereClause: string,
+): TracesSearchLike {
+ const trimmed = whereClause.trim()
+
+ if (!trimmed) {
+ return {
+ ...search,
+ whereClause: undefined,
+ services: undefined,
+ spanNames: undefined,
+ hasError: undefined,
+ minDurationMs: undefined,
+ maxDurationMs: undefined,
+ httpMethods: undefined,
+ httpStatusCodes: undefined,
+ deploymentEnvs: undefined,
+ rootOnly: undefined,
+ attributeKey: undefined,
+ attributeValue: undefined,
+ }
+ }
+
+ const { filters } = parseWhereClause(trimmed)
+
+ return {
+ ...search,
+ whereClause: trimmed,
+ services: filters.service ? [filters.service] : search.services,
+ spanNames: filters.spanName ? [filters.spanName] : search.spanNames,
+ hasError: filters.hasError ?? search.hasError,
+ minDurationMs: filters.minDurationMs ?? search.minDurationMs,
+ maxDurationMs: filters.maxDurationMs ?? search.maxDurationMs,
+ httpMethods: filters.httpMethod ? [filters.httpMethod] : search.httpMethods,
+ httpStatusCodes: filters.httpStatusCode ? [filters.httpStatusCode] : search.httpStatusCodes,
+ deploymentEnvs: filters.deploymentEnv ? [filters.deploymentEnv] : search.deploymentEnvs,
+ rootOnly: filters.rootOnly ?? search.rootOnly,
+ attributeKey: filters.attributeKey ?? search.attributeKey,
+ attributeValue: filters.attributeValue ?? search.attributeValue,
+ }
+}
diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts
deleted file mode 100644
index bd0c391..0000000
--- a/apps/web/src/lib/utils.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
-
-export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
-}
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
index ea7064d..471fd3f 100644
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -1,7 +1,7 @@
import { useAuth } from "@clerk/clerk-react"
import { useCustomer } from "autumn-js/react"
import { Navigate, Outlet, createRootRouteWithContext, redirect, useRouterState } from "@tanstack/react-router"
-import { Toaster } from "@/components/ui/sonner"
+import { Toaster } from "@maple/ui/components/ui/sonner"
import { hasSelectedPlan } from "@/lib/billing/plan-gating"
import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode"
import type { RouterAuthContext } from "@/router"
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index dd05bed..e5b9f73 100644
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -11,14 +11,14 @@ import {
SelectItem,
SelectTrigger,
SelectValue,
-} from "@/components/ui/select"
+} from "@maple/ui/components/ui/select"
import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import { ServiceUsageCards } from "@/components/dashboard/service-usage-cards"
import { MetricsGrid } from "@/components/dashboard/metrics-grid"
import type {
ChartLegendMode,
ChartTooltipMode,
-} from "@/components/charts/_shared/chart-types"
+} from "@maple/ui/components/charts/_shared/chart-types"
import {
getCustomChartTimeSeriesResultAtom,
getOverviewTimeSeriesResultAtom,
diff --git a/apps/web/src/routes/quick-start.tsx b/apps/web/src/routes/quick-start.tsx
index 9d3e99e..dc643de 100644
--- a/apps/web/src/routes/quick-start.tsx
+++ b/apps/web/src/routes/quick-start.tsx
@@ -9,9 +9,9 @@ import { DashboardLayout } from "@/components/layout/dashboard-layout"
import {
Card,
CardContent,
-} from "@/components/ui/card"
-import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"
-import { Button } from "@/components/ui/button"
+} from "@maple/ui/components/ui/card"
+import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@maple/ui/components/ui/input-group"
+import { Button } from "@maple/ui/components/ui/button"
import {
CheckIcon,
CopyIcon,
@@ -36,7 +36,7 @@ import { ingestUrl } from "@/lib/services/common/ingest-url"
import { MapleApiAtomClient } from "@/lib/services/common/atom-client"
import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
import { getServiceOverviewResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
-import { cn } from "@/lib/utils"
+import { cn } from "@maple/ui/utils"
import { useCustomer } from "autumn-js/react"
import { hasSelectedPlan } from "@/lib/billing/plan-gating"
diff --git a/apps/web/src/routes/services/$serviceName.tsx b/apps/web/src/routes/services/$serviceName.tsx
index ca9233c..3e86bf9 100644
--- a/apps/web/src/routes/services/$serviceName.tsx
+++ b/apps/web/src/routes/services/$serviceName.tsx
@@ -9,7 +9,7 @@ import { MetricsGrid } from "@/components/dashboard/metrics-grid"
import type {
ChartLegendMode,
ChartTooltipMode,
-} from "@/components/charts/_shared/chart-types"
+} from "@maple/ui/components/charts/_shared/chart-types"
import {
getCustomChartServiceDetailResultAtom,
getServiceApdexTimeSeriesResultAtom,
diff --git a/apps/web/src/routes/settings.tsx b/apps/web/src/routes/settings.tsx
index e814b14..5c21fec 100644
--- a/apps/web/src/routes/settings.tsx
+++ b/apps/web/src/routes/settings.tsx
@@ -11,13 +11,13 @@ import {
CardDescription,
CardHeader,
CardTitle,
-} from "@/components/ui/card"
+} from "@maple/ui/components/ui/card"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
-} from "@/components/ui/input-group"
+} from "@maple/ui/components/ui/input-group"
import {
AlertDialog,
AlertDialogAction,
@@ -28,10 +28,10 @@ import {
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
+} from "@maple/ui/components/ui/alert-dialog"
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@maple/ui/components/ui/tabs"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Separator } from "@maple/ui/components/ui/separator"
import {
AlertWarningIcon,
CheckIcon,
diff --git a/apps/web/src/routes/sign-in.tsx b/apps/web/src/routes/sign-in.tsx
index 26bdb56..99237e5 100644
--- a/apps/web/src/routes/sign-in.tsx
+++ b/apps/web/src/routes/sign-in.tsx
@@ -2,8 +2,8 @@ import { SignIn } from "@clerk/clerk-react"
import { FormEvent, useState } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { Schema } from "effect"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
+import { Button } from "@maple/ui/components/ui/button"
+import { Input } from "@maple/ui/components/ui/input"
import { apiBaseUrl } from "@/lib/services/common/api-base-url"
import { isClerkAuthEnabled } from "@/lib/services/common/auth-mode"
import { setSelfHostedSessionToken } from "@/lib/services/common/self-hosted-auth"
diff --git a/apps/web/src/routes/traces/$traceId.tsx b/apps/web/src/routes/traces/$traceId.tsx
index 0ab9868..e6b109a 100644
--- a/apps/web/src/routes/traces/$traceId.tsx
+++ b/apps/web/src/routes/traces/$traceId.tsx
@@ -6,13 +6,13 @@ import { toast } from "sonner"
import { DashboardLayout } from "@/components/layout/dashboard-layout"
import { TraceViewTabs } from "@/components/traces/trace-view-tabs"
import { SpanDetailPanel } from "@/components/traces/span-detail-panel"
-import { Badge } from "@/components/ui/badge"
-import { Skeleton } from "@/components/ui/skeleton"
+import { Badge } from "@maple/ui/components/ui/badge"
+import { Skeleton } from "@maple/ui/components/ui/skeleton"
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
-} from "@/components/ui/resizable"
+} from "@maple/ui/components/ui/resizable"
import { formatDuration } from "@/lib/format"
import { type Span, type SpanNode } from "@/api/tinybird/traces"
import { getSpanHierarchyResultAtom } from "@/lib/services/atoms/tinybird-query-atoms"
diff --git a/apps/web/src/routes/traces/index.tsx b/apps/web/src/routes/traces/index.tsx
index 5d9da6d..f045a17 100644
--- a/apps/web/src/routes/traces/index.tsx
+++ b/apps/web/src/routes/traces/index.tsx
@@ -1,3 +1,5 @@
+import * as React from "react"
+import { Result, useAtomValue } from "@effect-atom/atom-react"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { Schema } from "effect"
@@ -5,6 +7,16 @@ import { DashboardLayout } from "@/components/layout/dashboard-layout"
import { TracesTable } from "@/components/traces/traces-table"
import { TracesFilterSidebar } from "@/components/traces/traces-filter-sidebar"
import { TimeRangePicker } from "@/components/time-range-picker"
+import { AdvancedFilterDialog } from "@/components/traces/advanced-filter-dialog"
+import { MagnifierIcon, XmarkIcon } from "@/components/icons"
+import { Button } from "@maple/ui/components/ui/button"
+import { useEffectiveTimeRange } from "@/hooks/use-effective-time-range"
+import { applyWhereClause } from "@/lib/traces/advanced-filter-sync"
+import {
+ getTracesFacetsResultAtom,
+ getSpanAttributeKeysResultAtom,
+ getSpanAttributeValuesResultAtom,
+} from "@/lib/services/atoms/tinybird-query-atoms"
const tracesSearchSchema = Schema.Struct({
services: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
@@ -18,6 +30,9 @@ const tracesSearchSchema = Schema.Struct({
startTime: Schema.optional(Schema.String),
endTime: Schema.optional(Schema.String),
rootOnly: Schema.optional(Schema.Boolean),
+ whereClause: Schema.optional(Schema.String),
+ attributeKey: Schema.optional(Schema.String),
+ attributeValue: Schema.optional(Schema.String),
})
export type TracesSearchParams = Schema.Schema.Type
@@ -30,10 +45,107 @@ export const Route = createFileRoute("/traces/")({
function TracesPage() {
const search = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
+ const [activeAttributeKey, setActiveAttributeKey] = React.useState(null)
+
+ const handleApplyWhereClause = React.useCallback(
+ (newClause: string) => {
+ navigate({
+ search: (prev) => applyWhereClause(prev, newClause),
+ })
+ },
+ [navigate],
+ )
+
+ const { startTime: effectiveStartTime, endTime: effectiveEndTime } =
+ useEffectiveTimeRange(search.startTime, search.endTime)
+
+ const facetsResult = useAtomValue(
+ getTracesFacetsResultAtom({
+ data: {
+ startTime: effectiveStartTime,
+ endTime: effectiveEndTime,
+ },
+ }),
+ )
+
+ const spanAttributeKeysResult = useAtomValue(
+ getSpanAttributeKeysResultAtom({
+ data: {
+ startTime: effectiveStartTime,
+ endTime: effectiveEndTime,
+ },
+ }),
+ )
+
+ const spanAttributeValuesResult = useAtomValue(
+ getSpanAttributeValuesResultAtom({
+ data: {
+ startTime: effectiveStartTime,
+ endTime: effectiveEndTime,
+ attributeKey: activeAttributeKey ?? "",
+ },
+ }),
+ )
+
+ const attributeKeys = React.useMemo(
+ () =>
+ Result.builder(spanAttributeKeysResult)
+ .onSuccess((response) => response.data.map((row) => row.attributeKey))
+ .orElse(() => []),
+ [spanAttributeKeysResult],
+ )
+
+ const attributeValues = React.useMemo(
+ () =>
+ activeAttributeKey
+ ? Result.builder(spanAttributeValuesResult)
+ .onSuccess((response) => response.data.map((row) => row.attributeValue))
+ .orElse(() => [])
+ : [],
+ [activeAttributeKey, spanAttributeValuesResult],
+ )
+
+ const autocompleteValues = React.useMemo(() => {
+ const toNames = (items: Array<{ name: string }>): string[] => {
+ const seen = new Set()
+ const values: string[] = []
+ for (const item of items) {
+ const next = item.name.trim()
+ if (!next || seen.has(next)) continue
+ seen.add(next)
+ values.push(next)
+ }
+ return values
+ }
+
+ return Result.builder(facetsResult)
+ .onSuccess((response) => ({
+ services: toNames(response.data.services ?? []),
+ spanNames: toNames(response.data.spanNames ?? []),
+ environments: toNames(response.data.deploymentEnvs ?? []),
+ httpMethods: toNames(response.data.httpMethods ?? []),
+ httpStatusCodes: toNames(response.data.httpStatusCodes ?? []),
+ attributeKeys,
+ attributeValues,
+ }))
+ .orElse(() => ({
+ services: [] as string[],
+ spanNames: [] as string[],
+ environments: [] as string[],
+ httpMethods: [] as string[],
+ httpStatusCodes: [] as string[],
+ attributeKeys,
+ attributeValues,
+ }))
+ }, [facetsResult, attributeKeys, attributeValues])
const handleTimeChange = ({ startTime, endTime }: { startTime?: string; endTime?: string }) => {
navigate({
- search: (prev) => ({ ...prev, startTime, endTime }),
+ search: (prev) => ({
+ ...prev,
+ startTime,
+ endTime,
+ }),
})
}
@@ -43,18 +155,46 @@ function TracesPage() {
title="Traces"
description="View distributed traces across your services."
headerActions={
-
+
}
>
+ {search.whereClause && (
+
+
+
+
+ {search.whereClause}
+
+
+
handleApplyWhereClause("")}
+ className="shrink-0 text-muted-foreground hover:text-foreground"
+ title="Clear filter"
+ >
+
+ Clear filter
+
+
+ )}
)
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index 6a19dff..1a6e44a 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -2,6 +2,8 @@
@import "tw-animate-css";
@import "@fontsource-variable/geist-mono";
+@source "../../../packages/ui/src";
+
@custom-variant dark (&:is(.dark *));
:root {
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 6b454fd..3c68551 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -16,9 +16,9 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
- "baseUrl": ".",
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@maple/ui/*": ["../../packages/ui/src/*"]
},
"plugins": [
{
diff --git a/bun.lock b/bun.lock
index adfc508..86c724d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,6 +50,7 @@
"@clerk/clerk-react": "^5.60.0",
"@fontsource-variable/geist-mono": "^5.2.7",
"@maple/infra": "workspace:*",
+ "@maple/ui": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
@@ -60,8 +61,10 @@
"clsx": "^2.1.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "recharts": "2.15.4",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
+ "tw-animate-css": "^1.4.0",
},
},
"apps/web": {
@@ -74,6 +77,7 @@
"@fontsource-variable/geist-mono": "^5.2.7",
"@maple/domain": "workspace:*",
"@maple/infra": "workspace:*",
+ "@maple/ui": "workspace:*",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "^0.9.5",
"@tanstack/react-router": "^1.159.5",
@@ -165,6 +169,35 @@
"typescript": "^5.9.3",
},
},
+ "packages/ui": {
+ "name": "@maple/ui",
+ "dependencies": {
+ "@base-ui/react": "^1.1.0",
+ "@xyflow/react": "^12.10.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "embla-carousel-react": "^8.6.0",
+ "input-otp": "^1.4.2",
+ "next-themes": "^0.4.6",
+ "react-day-picker": "^9.13.0",
+ "react-resizable-panels": "^4.6.2",
+ "recharts": "2.15.4",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.4.0",
+ "vaul": "^1.1.2",
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.13",
+ "@types/react-dom": "^19.2.3",
+ "typescript": "^5.9.3",
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "tailwindcss": "^4.0.0",
+ },
+ },
},
"packages": {
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
@@ -587,6 +620,8 @@
"@maple/landing": ["@maple/landing@workspace:apps/landing"],
+ "@maple/ui": ["@maple/ui@workspace:packages/ui"],
+
"@maple/web": ["@maple/web@workspace:apps/web"],
"@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="],
diff --git a/packages/domain/src/tinybird/endpoints.ts b/packages/domain/src/tinybird/endpoints.ts
index d97c7ea..5daaf01 100644
--- a/packages/domain/src/tinybird/endpoints.ts
+++ b/packages/domain/src/tinybird/endpoints.ts
@@ -26,6 +26,8 @@ export const listTraces = defineEndpoint("list_traces", {
http_method: p.string().optional().describe("Filter by HTTP method"),
http_status_code: p.string().optional().describe("Filter by HTTP status code"),
deployment_env: p.string().optional().describe("Filter by deployment environment"),
+ attribute_filter_key: p.string().optional().describe("Filter where SpanAttributes[key] = value"),
+ attribute_filter_value: p.string().optional().describe("Value for attribute filter"),
root_only: p.boolean().optional().describe("Filter to root traces only (spans with no parent)"),
},
nodes: [
@@ -55,6 +57,15 @@ export const listTraces = defineEndpoint("list_traces", {
groupUniqArrayIf(SpanAttributes['http.method'], SpanAttributes['http.method'] != '') AS httpMethods,
groupUniqArrayIf(SpanAttributes['http.status_code'], SpanAttributes['http.status_code'] != '') AS httpStatusCodes,
groupUniqArrayIf(ResourceAttributes['deployment.environment'], ResourceAttributes['deployment.environment'] != '') AS deploymentEnvs,
+ {% if defined(attribute_filter_key) %}
+ max(if(
+ SpanAttributes[{{String(attribute_filter_key)}}] = {{String(attribute_filter_value, "")}},
+ 1,
+ 0
+ )) AS matchesAttributeFilter,
+ {% else %}
+ 1 AS matchesAttributeFilter,
+ {% end %}
countIf(ParentSpanId = '') AS rootSpanCount
FROM traces
WHERE TraceId != ''
@@ -109,6 +120,9 @@ export const listTraces = defineEndpoint("list_traces", {
{% if defined(deployment_env) %}
AND has(deploymentEnvs, {{String(deployment_env, "")}})
{% end %}
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
ORDER BY startTime DESC
LIMIT {{Int32(limit, 100)}}
OFFSET {{Int32(offset, 0)}}
@@ -1069,6 +1083,8 @@ export const tracesFacets = defineEndpoint("traces_facets", {
http_method: p.string().optional().describe("Filter by HTTP method"),
http_status_code: p.string().optional().describe("Filter by HTTP status code"),
deployment_env: p.string().optional().describe("Filter by deployment environment"),
+ attribute_filter_key: p.string().optional().describe("Filter where SpanAttributes[key] = value"),
+ attribute_filter_value: p.string().optional().describe("Value for attribute filter"),
},
nodes: [
node({
@@ -1093,6 +1109,15 @@ export const tracesFacets = defineEndpoint("traces_facets", {
OR (SpanAttributes['http.status_code'] != '' AND toUInt16OrZero(SpanAttributes['http.status_code']) >= 500),
1, 0
)) AS hasError,
+ {% if defined(attribute_filter_key) %}
+ max(if(
+ SpanAttributes[{{String(attribute_filter_key)}}] = {{String(attribute_filter_value, "")}},
+ 1,
+ 0
+ )) AS matchesAttributeFilter,
+ {% else %}
+ 1 AS matchesAttributeFilter,
+ {% end %}
(max(toUnixTimestamp64Nano(Timestamp) + Duration) - min(toUnixTimestamp64Nano(Timestamp))) / 1000000.0 AS durationMs
FROM traces
WHERE TraceId != ''
@@ -1116,6 +1141,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
FROM trace_summaries
ARRAY JOIN services AS service
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(span_name) %}
AND rootSpanName = {{String(span_name)}}
{% end %}
@@ -1151,6 +1179,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
'spanName' AS facetType
FROM trace_summaries
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(service) %}
AND has(services, {{String(service)}})
{% end %}
@@ -1187,6 +1218,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
FROM trace_summaries
ARRAY JOIN httpMethods AS method
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(service) %}
AND has(services, {{String(service)}})
{% end %}
@@ -1223,6 +1257,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
FROM trace_summaries
ARRAY JOIN httpStatusCodes AS status
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(service) %}
AND has(services, {{String(service)}})
{% end %}
@@ -1259,6 +1296,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
FROM trace_summaries
ARRAY JOIN deploymentEnvs AS env
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(service) %}
AND has(services, {{String(service)}})
{% end %}
@@ -1294,6 +1334,9 @@ export const tracesFacets = defineEndpoint("traces_facets", {
'errorCount' AS facetType
FROM trace_summaries
WHERE 1=1
+ {% if defined(attribute_filter_key) %}
+ AND matchesAttributeFilter = 1
+ {% end %}
{% if defined(service) %}
AND has(services, {{String(service)}})
{% end %}
@@ -1359,6 +1402,8 @@ export const tracesDurationStats = defineEndpoint("traces_duration_stats", {
http_method: p.string().optional().describe("Filter by HTTP method"),
http_status_code: p.string().optional().describe("Filter by HTTP status code"),
deployment_env: p.string().optional().describe("Filter by deployment environment"),
+ attribute_filter_key: p.string().optional().describe("Filter where SpanAttributes[key] = value"),
+ attribute_filter_value: p.string().optional().describe("Value for attribute filter"),
},
nodes: [
node({
@@ -1412,6 +1457,20 @@ export const tracesDurationStats = defineEndpoint("traces_duration_stats", {
{% if defined(deployment_env) %}
AND ResourceAttributes['deployment.environment'] = {{String(deployment_env)}}
{% end %}
+ {% if defined(attribute_filter_key) %}
+ AND TraceId IN (
+ SELECT TraceId FROM traces WHERE TraceId != ''
+ AND OrgId = {{String(org_id, "")}}
+ AND SpanAttributes[{{String(attribute_filter_key)}}] = {{String(attribute_filter_value, "")}}
+ {% if defined(start_time) %}
+ AND Timestamp >= {{DateTime(start_time, "2023-01-01 00:00:00")}}
+ {% end %}
+ {% if defined(end_time) %}
+ AND Timestamp <= {{DateTime(end_time, "2099-12-31 23:59:59")}}
+ {% end %}
+ GROUP BY TraceId
+ )
+ {% end %}
GROUP BY TraceId
)
`,
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 0000000..62949e2
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@maple/ui",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./utils": "./src/lib/utils.ts",
+ "./colors": "./src/lib/colors.ts",
+ "./types": "./src/lib/types.ts",
+ "./format": "./src/lib/format.ts",
+ "./*": "./src/*"
+ },
+ "scripts": {
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@base-ui/react": "^1.1.0",
+ "@xyflow/react": "^12.10.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "embla-carousel-react": "^8.6.0",
+ "input-otp": "^1.4.2",
+ "next-themes": "^0.4.6",
+ "react-day-picker": "^9.13.0",
+ "react-resizable-panels": "^4.6.2",
+ "recharts": "2.15.4",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.4.0",
+ "vaul": "^1.1.2"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "tailwindcss": "^4.0.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.13",
+ "@types/react-dom": "^19.2.3",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/apps/web/src/components/charts/_shared/build-chart-config.ts b/packages/ui/src/components/charts/_shared/build-chart-config.ts
similarity index 92%
rename from apps/web/src/components/charts/_shared/build-chart-config.ts
rename to packages/ui/src/components/charts/_shared/build-chart-config.ts
index eace736..9a5df30 100644
--- a/apps/web/src/components/charts/_shared/build-chart-config.ts
+++ b/packages/ui/src/components/charts/_shared/build-chart-config.ts
@@ -1,4 +1,4 @@
-import type { ChartConfig } from "@/components/ui/chart"
+import type { ChartConfig } from "../../ui/chart"
const CHART_COLORS = [
"var(--chart-1)",
diff --git a/apps/web/src/components/charts/_shared/chart-types.ts b/packages/ui/src/components/charts/_shared/chart-types.ts
similarity index 100%
rename from apps/web/src/components/charts/_shared/chart-types.ts
rename to packages/ui/src/components/charts/_shared/chart-types.ts
diff --git a/apps/web/src/components/charts/_shared/sample-data.ts b/packages/ui/src/components/charts/_shared/sample-data.ts
similarity index 100%
rename from apps/web/src/components/charts/_shared/sample-data.ts
rename to packages/ui/src/components/charts/_shared/sample-data.ts
diff --git a/apps/web/src/components/charts/_shared/svg-filters.tsx b/packages/ui/src/components/charts/_shared/svg-filters.tsx
similarity index 100%
rename from apps/web/src/components/charts/_shared/svg-filters.tsx
rename to packages/ui/src/components/charts/_shared/svg-filters.tsx
diff --git a/apps/web/src/components/charts/_shared/svg-patterns.tsx b/packages/ui/src/components/charts/_shared/svg-patterns.tsx
similarity index 100%
rename from apps/web/src/components/charts/_shared/svg-patterns.tsx
rename to packages/ui/src/components/charts/_shared/svg-patterns.tsx
diff --git a/apps/web/src/components/charts/_shared/use-dynamic-dasharray.ts b/packages/ui/src/components/charts/_shared/use-dynamic-dasharray.ts
similarity index 100%
rename from apps/web/src/components/charts/_shared/use-dynamic-dasharray.ts
rename to packages/ui/src/components/charts/_shared/use-dynamic-dasharray.ts
diff --git a/apps/web/src/components/charts/area/apdex-area-chart.tsx b/packages/ui/src/components/charts/area/apdex-area-chart.tsx
similarity index 89%
rename from apps/web/src/components/charts/area/apdex-area-chart.tsx
rename to packages/ui/src/components/charts/area/apdex-area-chart.tsx
index 3c5abb2..a5eea04 100644
--- a/apps/web/src/components/charts/area/apdex-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/apdex-area-chart.tsx
@@ -1,9 +1,9 @@
import { useId, useMemo } from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { apdexTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { VerticalGradient } from "@/components/charts/_shared/svg-patterns"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { apdexTimeSeriesData } from "../_shared/sample-data"
+import { VerticalGradient } from "../_shared/svg-patterns"
import {
type ChartConfig,
ChartContainer,
@@ -11,8 +11,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const chartConfig = {
apdexScore: { label: "Apdex", color: "var(--chart-5)" },
diff --git a/apps/web/src/components/charts/area/bar-pattern-area-chart.tsx b/packages/ui/src/components/charts/area/bar-pattern-area-chart.tsx
similarity index 82%
rename from apps/web/src/components/charts/area/bar-pattern-area-chart.tsx
rename to packages/ui/src/components/charts/area/bar-pattern-area-chart.tsx
index 7392d11..33fb4e7 100644
--- a/apps/web/src/components/charts/area/bar-pattern-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/bar-pattern-area-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { areaTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { BarPattern } from "@/components/charts/_shared/svg-patterns"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { areaTimeSeriesData } from "../_shared/sample-data"
+import { BarPattern } from "../_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/area/dotted-pattern-area-chart.tsx b/packages/ui/src/components/charts/area/dotted-pattern-area-chart.tsx
similarity index 82%
rename from apps/web/src/components/charts/area/dotted-pattern-area-chart.tsx
rename to packages/ui/src/components/charts/area/dotted-pattern-area-chart.tsx
index 7c93cac..e65ee7a 100644
--- a/apps/web/src/components/charts/area/dotted-pattern-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/dotted-pattern-area-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { areaTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { DottedPattern } from "@/components/charts/_shared/svg-patterns"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { areaTimeSeriesData } from "../_shared/sample-data"
+import { DottedPattern } from "../_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/area/error-rate-area-chart.tsx b/packages/ui/src/components/charts/area/error-rate-area-chart.tsx
similarity index 89%
rename from apps/web/src/components/charts/area/error-rate-area-chart.tsx
rename to packages/ui/src/components/charts/area/error-rate-area-chart.tsx
index 303a8c6..dd86efb 100644
--- a/apps/web/src/components/charts/area/error-rate-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/error-rate-area-chart.tsx
@@ -1,9 +1,9 @@
import { useId, useMemo } from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { errorRateTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { VerticalGradient } from "@/components/charts/_shared/svg-patterns"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { errorRateTimeSeriesData } from "../_shared/sample-data"
+import { VerticalGradient } from "../_shared/svg-patterns"
import {
type ChartConfig,
ChartContainer,
@@ -11,8 +11,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { formatErrorRate, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { formatErrorRate, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const chartConfig = {
errorRate: { label: "Error Rate", color: "var(--color-destructive, #ef4444)" },
diff --git a/apps/web/src/components/charts/area/gradient-area-chart.tsx b/packages/ui/src/components/charts/area/gradient-area-chart.tsx
similarity index 83%
rename from apps/web/src/components/charts/area/gradient-area-chart.tsx
rename to packages/ui/src/components/charts/area/gradient-area-chart.tsx
index 8ebd7c1..4f64b0e 100644
--- a/apps/web/src/components/charts/area/gradient-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/gradient-area-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { areaTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { VerticalGradient } from "@/components/charts/_shared/svg-patterns"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { areaTimeSeriesData } from "../_shared/sample-data"
+import { VerticalGradient } from "../_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/area/gradient-rounded-area-chart.tsx b/packages/ui/src/components/charts/area/gradient-rounded-area-chart.tsx
similarity index 83%
rename from apps/web/src/components/charts/area/gradient-rounded-area-chart.tsx
rename to packages/ui/src/components/charts/area/gradient-rounded-area-chart.tsx
index 93ceff4..3071108 100644
--- a/apps/web/src/components/charts/area/gradient-rounded-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/gradient-rounded-area-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { areaTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { VerticalGradient } from "@/components/charts/_shared/svg-patterns"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { areaTimeSeriesData } from "../_shared/sample-data"
+import { VerticalGradient } from "../_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/area/query-builder-area-chart.tsx b/packages/ui/src/components/charts/area/query-builder-area-chart.tsx
similarity index 97%
rename from apps/web/src/components/charts/area/query-builder-area-chart.tsx
rename to packages/ui/src/components/charts/area/query-builder-area-chart.tsx
index 7896c1d..1f86869 100644
--- a/apps/web/src/components/charts/area/query-builder-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/query-builder-area-chart.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
+import type { BaseChartProps } from "../_shared/chart-types"
import {
type ChartConfig,
ChartContainer,
@@ -9,8 +9,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const fallbackData: Record[] = [
{ bucket: "2026-01-01T00:00:00Z", A: 12, B: 8 },
diff --git a/apps/web/src/components/charts/area/throughput-area-chart.tsx b/packages/ui/src/components/charts/area/throughput-area-chart.tsx
similarity index 90%
rename from apps/web/src/components/charts/area/throughput-area-chart.tsx
rename to packages/ui/src/components/charts/area/throughput-area-chart.tsx
index e1d3ab6..504dbba 100644
--- a/apps/web/src/components/charts/area/throughput-area-chart.tsx
+++ b/packages/ui/src/components/charts/area/throughput-area-chart.tsx
@@ -1,9 +1,9 @@
import { useMemo, useId } from "react"
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { throughputTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { VerticalGradient } from "@/components/charts/_shared/svg-patterns"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { throughputTimeSeriesData } from "../_shared/sample-data"
+import { VerticalGradient } from "../_shared/svg-patterns"
import {
type ChartConfig,
ChartContainer,
@@ -11,8 +11,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { inferBucketSeconds, inferRangeMs, formatBucketLabel, bucketIntervalLabel, formatThroughput } from "@/lib/format"
+} from "../../ui/chart"
+import { inferBucketSeconds, inferRangeMs, formatBucketLabel, bucketIntervalLabel, formatThroughput } from "../../../lib/format"
export function ThroughputAreaChart({ data, className, legend, tooltip }: BaseChartProps) {
const id = useId()
diff --git a/apps/web/src/components/charts/bar/default-bar-chart.tsx b/packages/ui/src/components/charts/bar/default-bar-chart.tsx
similarity index 74%
rename from apps/web/src/components/charts/bar/default-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/default-bar-chart.tsx
index 52301a8..936a47c 100644
--- a/apps/web/src/components/charts/bar/default-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/default-bar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { defaultBarData } from "@/components/charts/_shared/sample-data"
-import { DottedPattern } from "@/components/charts/_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { defaultBarData } from "../_shared/sample-data"
+import { DottedPattern } from "../_shared/svg-patterns"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/default-multiple-bar-chart.tsx b/packages/ui/src/components/charts/bar/default-multiple-bar-chart.tsx
similarity index 81%
rename from apps/web/src/components/charts/bar/default-multiple-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/default-multiple-bar-chart.tsx
index c846688..6d6146c 100644
--- a/apps/web/src/components/charts/bar/default-multiple-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/default-multiple-bar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { multiBarData } from "@/components/charts/_shared/sample-data"
-import { DottedPattern } from "@/components/charts/_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { multiBarData } from "../_shared/sample-data"
+import { DottedPattern } from "../_shared/svg-patterns"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/duotone-bar-chart.tsx b/packages/ui/src/components/charts/bar/duotone-bar-chart.tsx
similarity index 83%
rename from apps/web/src/components/charts/bar/duotone-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/duotone-bar-chart.tsx
index 6bdf441..cac4239 100644
--- a/apps/web/src/components/charts/bar/duotone-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/duotone-bar-chart.tsx
@@ -2,9 +2,9 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { defaultBarData } from "@/components/charts/_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { defaultBarData } from "../_shared/sample-data"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/glowing-bar-chart.tsx b/packages/ui/src/components/charts/bar/glowing-bar-chart.tsx
similarity index 80%
rename from apps/web/src/components/charts/bar/glowing-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/glowing-bar-chart.tsx
index 3dcfe2c..9c75ddb 100644
--- a/apps/web/src/components/charts/bar/glowing-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/glowing-bar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { multiBarData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter } from "@/components/charts/_shared/svg-filters"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { multiBarData } from "../_shared/sample-data"
+import { GlowFilter } from "../_shared/svg-filters"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/gradient-bar-chart.tsx b/packages/ui/src/components/charts/bar/gradient-bar-chart.tsx
similarity index 87%
rename from apps/web/src/components/charts/bar/gradient-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/gradient-bar-chart.tsx
index 98d9b47..c8aadcf 100644
--- a/apps/web/src/components/charts/bar/gradient-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/gradient-bar-chart.tsx
@@ -2,9 +2,9 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { defaultBarData } from "@/components/charts/_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { defaultBarData } from "../_shared/sample-data"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/hatched-bar-chart.tsx b/packages/ui/src/components/charts/bar/hatched-bar-chart.tsx
similarity index 74%
rename from apps/web/src/components/charts/bar/hatched-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/hatched-bar-chart.tsx
index ee7bf01..cd68648 100644
--- a/apps/web/src/components/charts/bar/hatched-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/hatched-bar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { defaultBarData } from "@/components/charts/_shared/sample-data"
-import { HatchedPattern } from "@/components/charts/_shared/svg-patterns"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { defaultBarData } from "../_shared/sample-data"
+import { HatchedPattern } from "../_shared/svg-patterns"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/highlighted-bar-chart.tsx b/packages/ui/src/components/charts/bar/highlighted-bar-chart.tsx
similarity index 83%
rename from apps/web/src/components/charts/bar/highlighted-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/highlighted-bar-chart.tsx
index 341e7cd..61c6600 100644
--- a/apps/web/src/components/charts/bar/highlighted-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/highlighted-bar-chart.tsx
@@ -2,9 +2,9 @@
import { useState } from "react"
import { Bar, BarChart, CartesianGrid, Cell, XAxis } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { defaultBarData } from "@/components/charts/_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { defaultBarData } from "../_shared/sample-data"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/bar/query-builder-bar-chart.tsx b/packages/ui/src/components/charts/bar/query-builder-bar-chart.tsx
similarity index 96%
rename from apps/web/src/components/charts/bar/query-builder-bar-chart.tsx
rename to packages/ui/src/components/charts/bar/query-builder-bar-chart.tsx
index 8b00e85..59f0102 100644
--- a/apps/web/src/components/charts/bar/query-builder-bar-chart.tsx
+++ b/packages/ui/src/components/charts/bar/query-builder-bar-chart.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
+import type { BaseChartProps } from "../_shared/chart-types"
import {
type ChartConfig,
ChartContainer,
@@ -9,8 +9,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const fallbackData: Record[] = [
{ bucket: "2026-01-01T00:00:00Z", A: 12, B: 8 },
diff --git a/apps/web/src/components/charts/index.ts b/packages/ui/src/components/charts/index.ts
similarity index 100%
rename from apps/web/src/components/charts/index.ts
rename to packages/ui/src/components/charts/index.ts
diff --git a/apps/web/src/components/charts/line/dotted-line-chart.tsx b/packages/ui/src/components/charts/line/dotted-line-chart.tsx
similarity index 76%
rename from apps/web/src/components/charts/line/dotted-line-chart.tsx
rename to packages/ui/src/components/charts/line/dotted-line-chart.tsx
index 27cfff0..9407e46 100644
--- a/apps/web/src/components/charts/line/dotted-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/dotted-line-chart.tsx
@@ -1,8 +1,8 @@
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { lineTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { lineTimeSeriesData } from "../_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/dotted-multi-line-chart.tsx b/packages/ui/src/components/charts/line/dotted-multi-line-chart.tsx
similarity index 81%
rename from apps/web/src/components/charts/line/dotted-multi-line-chart.tsx
rename to packages/ui/src/components/charts/line/dotted-multi-line-chart.tsx
index b37ab5c..6cfc420 100644
--- a/apps/web/src/components/charts/line/dotted-multi-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/dotted-multi-line-chart.tsx
@@ -1,8 +1,8 @@
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { multiLineData } from "@/components/charts/_shared/sample-data"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { multiLineData } from "../_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
desktop: { label: "Desktop", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/glowing-line-chart.tsx b/packages/ui/src/components/charts/line/glowing-line-chart.tsx
similarity index 75%
rename from apps/web/src/components/charts/line/glowing-line-chart.tsx
rename to packages/ui/src/components/charts/line/glowing-line-chart.tsx
index 3cffcb6..6ab192a 100644
--- a/apps/web/src/components/charts/line/glowing-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/glowing-line-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { lineTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter } from "@/components/charts/_shared/svg-filters"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { lineTimeSeriesData } from "../_shared/sample-data"
+import { GlowFilter } from "../_shared/svg-filters"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/latency-line-chart.tsx b/packages/ui/src/components/charts/line/latency-line-chart.tsx
similarity index 92%
rename from apps/web/src/components/charts/line/latency-line-chart.tsx
rename to packages/ui/src/components/charts/line/latency-line-chart.tsx
index c6c24d2..55c4b98 100644
--- a/apps/web/src/components/charts/line/latency-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/latency-line-chart.tsx
@@ -1,8 +1,8 @@
import { useMemo } from "react"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { latencyTimeSeriesData } from "@/components/charts/_shared/sample-data"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { latencyTimeSeriesData } from "../_shared/sample-data"
import {
type ChartConfig,
ChartContainer,
@@ -10,8 +10,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { formatLatency, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { formatLatency, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const chartConfig = {
p99LatencyMs: { label: "P99", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/number-dot-line-chart.tsx b/packages/ui/src/components/charts/line/number-dot-line-chart.tsx
similarity index 83%
rename from apps/web/src/components/charts/line/number-dot-line-chart.tsx
rename to packages/ui/src/components/charts/line/number-dot-line-chart.tsx
index 63e0025..30b3fcc 100644
--- a/apps/web/src/components/charts/line/number-dot-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/number-dot-line-chart.tsx
@@ -1,8 +1,8 @@
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { lineTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { lineTimeSeriesData } from "../_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/partial-line-chart.tsx b/packages/ui/src/components/charts/line/partial-line-chart.tsx
similarity index 81%
rename from apps/web/src/components/charts/line/partial-line-chart.tsx
rename to packages/ui/src/components/charts/line/partial-line-chart.tsx
index f039008..7a07da2 100644
--- a/apps/web/src/components/charts/line/partial-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/partial-line-chart.tsx
@@ -1,9 +1,9 @@
import { CartesianGrid, Customized, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { partialLineData } from "@/components/charts/_shared/sample-data"
-import { useDynamicDasharray } from "@/components/charts/_shared/use-dynamic-dasharray"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { partialLineData } from "../_shared/sample-data"
+import { useDynamicDasharray } from "../_shared/use-dynamic-dasharray"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/pinging-dot-chart.tsx b/packages/ui/src/components/charts/line/pinging-dot-chart.tsx
similarity index 87%
rename from apps/web/src/components/charts/line/pinging-dot-chart.tsx
rename to packages/ui/src/components/charts/line/pinging-dot-chart.tsx
index 6c82267..b3cd2c8 100644
--- a/apps/web/src/components/charts/line/pinging-dot-chart.tsx
+++ b/packages/ui/src/components/charts/line/pinging-dot-chart.tsx
@@ -1,8 +1,8 @@
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { lineTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { lineTimeSeriesData } from "../_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/line/query-builder-line-chart.tsx b/packages/ui/src/components/charts/line/query-builder-line-chart.tsx
similarity index 97%
rename from apps/web/src/components/charts/line/query-builder-line-chart.tsx
rename to packages/ui/src/components/charts/line/query-builder-line-chart.tsx
index 78eff76..6d79508 100644
--- a/apps/web/src/components/charts/line/query-builder-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/query-builder-line-chart.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
+import type { BaseChartProps } from "../_shared/chart-types"
import {
type ChartConfig,
ChartContainer,
@@ -9,8 +9,8 @@ import {
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
-} from "@/components/ui/chart"
-import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "@/lib/format"
+} from "../../ui/chart"
+import { formatNumber, inferBucketSeconds, inferRangeMs, formatBucketLabel } from "../../../lib/format"
const fallbackData: Record[] = [
{ bucket: "2026-01-01T00:00:00Z", A: 12, B: 8 },
diff --git a/apps/web/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx b/packages/ui/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx
similarity index 76%
rename from apps/web/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx
rename to packages/ui/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx
index 80c8bce..d25c005 100644
--- a/apps/web/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx
+++ b/packages/ui/src/components/charts/line/rainbow-glow-gradient-line-chart.tsx
@@ -1,10 +1,10 @@
import { useId } from "react"
import { CartesianGrid, Line, LineChart, XAxis } from "recharts"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { lineTimeSeriesData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter, RainbowGradient } from "@/components/charts/_shared/svg-filters"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { lineTimeSeriesData } from "../_shared/sample-data"
+import { GlowFilter, RainbowGradient } from "../_shared/svg-filters"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
const chartConfig = {
value: { label: "Value", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/pie/default-radial-chart.tsx b/packages/ui/src/components/charts/pie/default-radial-chart.tsx
similarity index 69%
rename from apps/web/src/components/charts/pie/default-radial-chart.tsx
rename to packages/ui/src/components/charts/pie/default-radial-chart.tsx
index cf48dda..e668a8d 100644
--- a/apps/web/src/components/charts/pie/default-radial-chart.tsx
+++ b/packages/ui/src/components/charts/pie/default-radial-chart.tsx
@@ -2,10 +2,10 @@
import { useMemo } from "react"
import { RadialBar, RadialBarChart } from "recharts"
-import { ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { buildChartConfig } from "@/components/charts/_shared/build-chart-config"
-import { radialData } from "@/components/charts/_shared/sample-data"
+import { ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { buildChartConfig } from "../_shared/build-chart-config"
+import { radialData } from "../_shared/sample-data"
export function DefaultRadialChart({ data = radialData, className }: BaseChartProps) {
const { config, data: coloredData } = useMemo(() => buildChartConfig(data), [data])
diff --git a/apps/web/src/components/charts/pie/glowing-radial-chart.tsx b/packages/ui/src/components/charts/pie/glowing-radial-chart.tsx
similarity index 79%
rename from apps/web/src/components/charts/pie/glowing-radial-chart.tsx
rename to packages/ui/src/components/charts/pie/glowing-radial-chart.tsx
index 85ad3e2..76bc127 100644
--- a/apps/web/src/components/charts/pie/glowing-radial-chart.tsx
+++ b/packages/ui/src/components/charts/pie/glowing-radial-chart.tsx
@@ -2,11 +2,11 @@
import { useId, useMemo, useState } from "react"
import { RadialBar, RadialBarChart } from "recharts"
-import { ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { buildChartConfig } from "@/components/charts/_shared/build-chart-config"
-import { radialData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter } from "@/components/charts/_shared/svg-filters"
+import { ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { buildChartConfig } from "../_shared/build-chart-config"
+import { radialData } from "../_shared/sample-data"
+import { GlowFilter } from "../_shared/svg-filters"
export function GlowingRadialChart({ data = radialData, className }: BaseChartProps) {
const id = useId()
diff --git a/apps/web/src/components/charts/pie/increase-size-pie-chart.tsx b/packages/ui/src/components/charts/pie/increase-size-pie-chart.tsx
similarity index 77%
rename from apps/web/src/components/charts/pie/increase-size-pie-chart.tsx
rename to packages/ui/src/components/charts/pie/increase-size-pie-chart.tsx
index 4789fe4..bf2d022 100644
--- a/apps/web/src/components/charts/pie/increase-size-pie-chart.tsx
+++ b/packages/ui/src/components/charts/pie/increase-size-pie-chart.tsx
@@ -2,10 +2,10 @@
import { useMemo } from "react"
import { Cell, Pie, PieChart } from "recharts"
-import { ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { buildChartConfig } from "@/components/charts/_shared/build-chart-config"
-import { pieData } from "@/components/charts/_shared/sample-data"
+import { ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { buildChartConfig } from "../_shared/build-chart-config"
+import { pieData } from "../_shared/sample-data"
export function IncreaseSizePieChart({ data = pieData, className }: BaseChartProps) {
const { config, data: coloredData } = useMemo(() => buildChartConfig(data), [data])
diff --git a/apps/web/src/components/charts/pie/rounded-pie-chart.tsx b/packages/ui/src/components/charts/pie/rounded-pie-chart.tsx
similarity index 71%
rename from apps/web/src/components/charts/pie/rounded-pie-chart.tsx
rename to packages/ui/src/components/charts/pie/rounded-pie-chart.tsx
index 84b8aa6..aad852b 100644
--- a/apps/web/src/components/charts/pie/rounded-pie-chart.tsx
+++ b/packages/ui/src/components/charts/pie/rounded-pie-chart.tsx
@@ -2,10 +2,10 @@
import { useMemo } from "react"
import { LabelList, Pie, PieChart } from "recharts"
-import { ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { buildChartConfig } from "@/components/charts/_shared/build-chart-config"
-import { pieData } from "@/components/charts/_shared/sample-data"
+import { ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { buildChartConfig } from "../_shared/build-chart-config"
+import { pieData } from "../_shared/sample-data"
export function RoundedPieChart({ data = pieData, className }: BaseChartProps) {
const { config, data: coloredData } = useMemo(() => buildChartConfig(data), [data])
diff --git a/apps/web/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx b/packages/ui/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx
similarity index 80%
rename from apps/web/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx
rename to packages/ui/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx
index 8fe393c..5cfa32a 100644
--- a/apps/web/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx
+++ b/packages/ui/src/components/charts/radar/glowing-multiple-stroke-radar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { radarData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter } from "@/components/charts/_shared/svg-filters"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { radarData } from "../_shared/sample-data"
+import { GlowFilter } from "../_shared/svg-filters"
const chartConfig = {
a: { label: "Student A", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/radar/glowing-stroke-radar-chart.tsx b/packages/ui/src/components/charts/radar/glowing-stroke-radar-chart.tsx
similarity index 75%
rename from apps/web/src/components/charts/radar/glowing-stroke-radar-chart.tsx
rename to packages/ui/src/components/charts/radar/glowing-stroke-radar-chart.tsx
index ae1f7b5..f05f2d5 100644
--- a/apps/web/src/components/charts/radar/glowing-stroke-radar-chart.tsx
+++ b/packages/ui/src/components/charts/radar/glowing-stroke-radar-chart.tsx
@@ -2,10 +2,10 @@
import { useId } from "react"
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { radarData } from "@/components/charts/_shared/sample-data"
-import { GlowFilter } from "@/components/charts/_shared/svg-filters"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { radarData } from "../_shared/sample-data"
+import { GlowFilter } from "../_shared/svg-filters"
const chartConfig = {
a: { label: "Student A", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/radar/stroke-multiple-radar-chart.tsx b/packages/ui/src/components/charts/radar/stroke-multiple-radar-chart.tsx
similarity index 81%
rename from apps/web/src/components/charts/radar/stroke-multiple-radar-chart.tsx
rename to packages/ui/src/components/charts/radar/stroke-multiple-radar-chart.tsx
index c14e013..363b630 100644
--- a/apps/web/src/components/charts/radar/stroke-multiple-radar-chart.tsx
+++ b/packages/ui/src/components/charts/radar/stroke-multiple-radar-chart.tsx
@@ -1,9 +1,9 @@
"use client"
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { radarData } from "@/components/charts/_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { radarData } from "../_shared/sample-data"
const chartConfig = {
a: { label: "Student A", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/radar/stroke-radar-chart.tsx b/packages/ui/src/components/charts/radar/stroke-radar-chart.tsx
similarity index 76%
rename from apps/web/src/components/charts/radar/stroke-radar-chart.tsx
rename to packages/ui/src/components/charts/radar/stroke-radar-chart.tsx
index 1da0934..b7038bb 100644
--- a/apps/web/src/components/charts/radar/stroke-radar-chart.tsx
+++ b/packages/ui/src/components/charts/radar/stroke-radar-chart.tsx
@@ -1,9 +1,9 @@
"use client"
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from "recharts"
-import { type ChartConfig, ChartContainer } from "@/components/ui/chart"
-import type { BaseChartProps } from "@/components/charts/_shared/chart-types"
-import { radarData } from "@/components/charts/_shared/sample-data"
+import { type ChartConfig, ChartContainer } from "../../ui/chart"
+import type { BaseChartProps } from "../_shared/chart-types"
+import { radarData } from "../_shared/sample-data"
const chartConfig = {
a: { label: "Student A", color: "var(--chart-1)" },
diff --git a/apps/web/src/components/charts/registry.ts b/packages/ui/src/components/charts/registry.ts
similarity index 100%
rename from apps/web/src/components/charts/registry.ts
rename to packages/ui/src/components/charts/registry.ts
diff --git a/packages/ui/src/components/icons/alert-warning.tsx b/packages/ui/src/components/icons/alert-warning.tsx
new file mode 100644
index 0000000..724b809
--- /dev/null
+++ b/packages/ui/src/components/icons/alert-warning.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function AlertWarningIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { AlertWarningIcon }
diff --git a/packages/ui/src/components/icons/arrow-down.tsx b/packages/ui/src/components/icons/arrow-down.tsx
new file mode 100644
index 0000000..f4e3e05
--- /dev/null
+++ b/packages/ui/src/components/icons/arrow-down.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function ArrowDownIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { ArrowDownIcon }
diff --git a/packages/ui/src/components/icons/arrow-left.tsx b/packages/ui/src/components/icons/arrow-left.tsx
new file mode 100644
index 0000000..4a580c7
--- /dev/null
+++ b/packages/ui/src/components/icons/arrow-left.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function ArrowLeftIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { ArrowLeftIcon }
diff --git a/packages/ui/src/components/icons/arrow-right.tsx b/packages/ui/src/components/icons/arrow-right.tsx
new file mode 100644
index 0000000..54a2421
--- /dev/null
+++ b/packages/ui/src/components/icons/arrow-right.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function ArrowRightIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { ArrowRightIcon }
diff --git a/packages/ui/src/components/icons/check.tsx b/packages/ui/src/components/icons/check.tsx
new file mode 100644
index 0000000..76ba375
--- /dev/null
+++ b/packages/ui/src/components/icons/check.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function CheckIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { CheckIcon }
diff --git a/packages/ui/src/components/icons/chevron-down.tsx b/packages/ui/src/components/icons/chevron-down.tsx
new file mode 100644
index 0000000..cac0ec6
--- /dev/null
+++ b/packages/ui/src/components/icons/chevron-down.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function ChevronDownIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { ChevronDownIcon }
diff --git a/packages/ui/src/components/icons/chevron-expand-y.tsx b/packages/ui/src/components/icons/chevron-expand-y.tsx
new file mode 100644
index 0000000..ef41169
--- /dev/null
+++ b/packages/ui/src/components/icons/chevron-expand-y.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function ChevronExpandYIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { ChevronExpandYIcon }
diff --git a/packages/ui/src/components/icons/chevron-left.tsx b/packages/ui/src/components/icons/chevron-left.tsx
new file mode 100644
index 0000000..4baf07c
--- /dev/null
+++ b/packages/ui/src/components/icons/chevron-left.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function ChevronLeftIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { ChevronLeftIcon }
diff --git a/packages/ui/src/components/icons/chevron-right.tsx b/packages/ui/src/components/icons/chevron-right.tsx
new file mode 100644
index 0000000..0256431
--- /dev/null
+++ b/packages/ui/src/components/icons/chevron-right.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function ChevronRightIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { ChevronRightIcon }
diff --git a/packages/ui/src/components/icons/chevron-up.tsx b/packages/ui/src/components/icons/chevron-up.tsx
new file mode 100644
index 0000000..5e99452
--- /dev/null
+++ b/packages/ui/src/components/icons/chevron-up.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function ChevronUpIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { ChevronUpIcon }
diff --git a/packages/ui/src/components/icons/circle-check.tsx b/packages/ui/src/components/icons/circle-check.tsx
new file mode 100644
index 0000000..6863ec2
--- /dev/null
+++ b/packages/ui/src/components/icons/circle-check.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function CircleCheckIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { CircleCheckIcon }
diff --git a/packages/ui/src/components/icons/circle-info.tsx b/packages/ui/src/components/icons/circle-info.tsx
new file mode 100644
index 0000000..79effe5
--- /dev/null
+++ b/packages/ui/src/components/icons/circle-info.tsx
@@ -0,0 +1,13 @@
+import type { IconProps } from "./icon"
+
+function CircleInfoIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+
+ )
+}
+export { CircleInfoIcon }
diff --git a/packages/ui/src/components/icons/circle-xmark.tsx b/packages/ui/src/components/icons/circle-xmark.tsx
new file mode 100644
index 0000000..126c392
--- /dev/null
+++ b/packages/ui/src/components/icons/circle-xmark.tsx
@@ -0,0 +1,13 @@
+import type { IconProps } from "./icon"
+
+function CircleXmarkIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+
+ )
+}
+export { CircleXmarkIcon }
diff --git a/packages/ui/src/components/icons/code.tsx b/packages/ui/src/components/icons/code.tsx
new file mode 100644
index 0000000..e1dd563
--- /dev/null
+++ b/packages/ui/src/components/icons/code.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function CodeIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { CodeIcon }
diff --git a/packages/ui/src/components/icons/dots.tsx b/packages/ui/src/components/icons/dots.tsx
new file mode 100644
index 0000000..1f31c50
--- /dev/null
+++ b/packages/ui/src/components/icons/dots.tsx
@@ -0,0 +1,13 @@
+import type { IconProps } from "./icon"
+
+function DotsIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+
+ )
+}
+export { DotsIcon }
diff --git a/packages/ui/src/components/icons/eye.tsx b/packages/ui/src/components/icons/eye.tsx
new file mode 100644
index 0000000..ebbe36e
--- /dev/null
+++ b/packages/ui/src/components/icons/eye.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function EyeIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { EyeIcon }
diff --git a/packages/ui/src/components/icons/icon.tsx b/packages/ui/src/components/icons/icon.tsx
new file mode 100644
index 0000000..87d3788
--- /dev/null
+++ b/packages/ui/src/components/icons/icon.tsx
@@ -0,0 +1,7 @@
+import type { SVGProps, ComponentType } from "react"
+
+export interface IconProps extends SVGProps {
+ size?: number | string
+}
+
+export type IconComponent = ComponentType
diff --git a/packages/ui/src/components/icons/index.ts b/packages/ui/src/components/icons/index.ts
new file mode 100644
index 0000000..1f32292
--- /dev/null
+++ b/packages/ui/src/components/icons/index.ts
@@ -0,0 +1,26 @@
+export type { IconProps, IconComponent } from "./icon"
+
+export { AlertWarningIcon } from "./alert-warning"
+export { ArrowDownIcon } from "./arrow-down"
+export { ArrowLeftIcon } from "./arrow-left"
+export { ArrowRightIcon } from "./arrow-right"
+export { CheckIcon } from "./check"
+export { ChevronDownIcon } from "./chevron-down"
+export { ChevronExpandYIcon } from "./chevron-expand-y"
+export { ChevronLeftIcon } from "./chevron-left"
+export { ChevronRightIcon } from "./chevron-right"
+export { ChevronUpIcon } from "./chevron-up"
+export { CircleCheckIcon } from "./circle-check"
+export { CircleInfoIcon } from "./circle-info"
+export { CircleXmarkIcon } from "./circle-xmark"
+export { CodeIcon } from "./code"
+export { DotsIcon } from "./dots"
+export { EyeIcon } from "./eye"
+export { LoaderIcon } from "./loader"
+export { MagnifierIcon } from "./magnifier"
+export { MinusIcon } from "./minus"
+export { NetworkNodesIcon } from "./network-nodes"
+export { PulseIcon } from "./pulse"
+export { RadioCheckedIcon } from "./radio-checked"
+export { SidebarLeftIcon } from "./sidebar-left"
+export { XmarkIcon } from "./xmark"
diff --git a/packages/ui/src/components/icons/loader.tsx b/packages/ui/src/components/icons/loader.tsx
new file mode 100644
index 0000000..0ef8f9f
--- /dev/null
+++ b/packages/ui/src/components/icons/loader.tsx
@@ -0,0 +1,18 @@
+import type { IconProps } from "./icon"
+
+function LoaderIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+export { LoaderIcon }
diff --git a/packages/ui/src/components/icons/magnifier.tsx b/packages/ui/src/components/icons/magnifier.tsx
new file mode 100644
index 0000000..6530791
--- /dev/null
+++ b/packages/ui/src/components/icons/magnifier.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function MagnifierIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { MagnifierIcon }
diff --git a/packages/ui/src/components/icons/minus.tsx b/packages/ui/src/components/icons/minus.tsx
new file mode 100644
index 0000000..090c552
--- /dev/null
+++ b/packages/ui/src/components/icons/minus.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function MinusIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { MinusIcon }
diff --git a/packages/ui/src/components/icons/network-nodes.tsx b/packages/ui/src/components/icons/network-nodes.tsx
new file mode 100644
index 0000000..104270a
--- /dev/null
+++ b/packages/ui/src/components/icons/network-nodes.tsx
@@ -0,0 +1,19 @@
+import type { IconProps } from "./icon"
+
+function NetworkNodesIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+export { NetworkNodesIcon }
diff --git a/packages/ui/src/components/icons/pulse.tsx b/packages/ui/src/components/icons/pulse.tsx
new file mode 100644
index 0000000..8ef2cc4
--- /dev/null
+++ b/packages/ui/src/components/icons/pulse.tsx
@@ -0,0 +1,11 @@
+import type { IconProps } from "./icon"
+
+function PulseIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+ )
+}
+export { PulseIcon }
diff --git a/packages/ui/src/components/icons/radio-checked.tsx b/packages/ui/src/components/icons/radio-checked.tsx
new file mode 100644
index 0000000..63b3a77
--- /dev/null
+++ b/packages/ui/src/components/icons/radio-checked.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function RadioCheckedIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { RadioCheckedIcon }
diff --git a/packages/ui/src/components/icons/sidebar-left.tsx b/packages/ui/src/components/icons/sidebar-left.tsx
new file mode 100644
index 0000000..014deb8
--- /dev/null
+++ b/packages/ui/src/components/icons/sidebar-left.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function SidebarLeftIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { SidebarLeftIcon }
diff --git a/packages/ui/src/components/icons/xmark.tsx b/packages/ui/src/components/icons/xmark.tsx
new file mode 100644
index 0000000..43f6eb6
--- /dev/null
+++ b/packages/ui/src/components/icons/xmark.tsx
@@ -0,0 +1,12 @@
+import type { IconProps } from "./icon"
+
+function XmarkIcon({ size = 24, className, ...props }: IconProps) {
+ return (
+
+
+
+
+ )
+}
+export { XmarkIcon }
diff --git a/apps/landing/src/components/traces/flamegraph-minimap.tsx b/packages/ui/src/components/traces/flamegraph-minimap.tsx
similarity index 100%
rename from apps/landing/src/components/traces/flamegraph-minimap.tsx
rename to packages/ui/src/components/traces/flamegraph-minimap.tsx
diff --git a/apps/landing/src/components/traces/flamegraph-tooltip.tsx b/packages/ui/src/components/traces/flamegraph-tooltip.tsx
similarity index 100%
rename from apps/landing/src/components/traces/flamegraph-tooltip.tsx
rename to packages/ui/src/components/traces/flamegraph-tooltip.tsx
diff --git a/apps/landing/src/components/traces/flamegraph.tsx b/packages/ui/src/components/traces/flamegraph.tsx
similarity index 100%
rename from apps/landing/src/components/traces/flamegraph.tsx
rename to packages/ui/src/components/traces/flamegraph.tsx
diff --git a/apps/landing/src/components/traces/flow-node.tsx b/packages/ui/src/components/traces/flow-node.tsx
similarity index 100%
rename from apps/landing/src/components/traces/flow-node.tsx
rename to packages/ui/src/components/traces/flow-node.tsx
diff --git a/apps/landing/src/components/traces/flow-utils.ts b/packages/ui/src/components/traces/flow-utils.ts
similarity index 100%
rename from apps/landing/src/components/traces/flow-utils.ts
rename to packages/ui/src/components/traces/flow-utils.ts
diff --git a/apps/landing/src/components/traces/flow-view.tsx b/packages/ui/src/components/traces/flow-view.tsx
similarity index 100%
rename from apps/landing/src/components/traces/flow-view.tsx
rename to packages/ui/src/components/traces/flow-view.tsx
diff --git a/apps/web/src/components/ui/accordion.tsx b/packages/ui/src/components/ui/accordion.tsx
similarity index 96%
rename from apps/web/src/components/ui/accordion.tsx
rename to packages/ui/src/components/ui/accordion.tsx
index ec21e0c..ec45c0d 100644
--- a/apps/web/src/components/ui/accordion.tsx
+++ b/packages/ui/src/components/ui/accordion.tsx
@@ -1,7 +1,7 @@
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"
-import { cn } from "@/lib/utils"
-import { ChevronDownIcon, ChevronUpIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronDownIcon, ChevronUpIcon } from "../icons"
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return (
diff --git a/apps/web/src/components/ui/alert-dialog.tsx b/packages/ui/src/components/ui/alert-dialog.tsx
similarity index 98%
rename from apps/web/src/components/ui/alert-dialog.tsx
rename to packages/ui/src/components/ui/alert-dialog.tsx
index 35028c5..d35cab8 100644
--- a/apps/web/src/components/ui/alert-dialog.tsx
+++ b/packages/ui/src/components/ui/alert-dialog.tsx
@@ -1,8 +1,8 @@
import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/alert.tsx b/packages/ui/src/components/ui/alert.tsx
similarity index 98%
rename from apps/web/src/components/ui/alert.tsx
rename to packages/ui/src/components/ui/alert.tsx
index 08916e5..f0fd300 100644
--- a/apps/web/src/components/ui/alert.tsx
+++ b/packages/ui/src/components/ui/alert.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
const alertVariants = cva("grid gap-0.5 rounded-none border px-2.5 py-2 text-left text-xs has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert", {
variants: {
diff --git a/apps/web/src/components/ui/aspect-ratio.tsx b/packages/ui/src/components/ui/aspect-ratio.tsx
similarity index 90%
rename from apps/web/src/components/ui/aspect-ratio.tsx
rename to packages/ui/src/components/ui/aspect-ratio.tsx
index 4c0ebaa..400573e 100644
--- a/apps/web/src/components/ui/aspect-ratio.tsx
+++ b/packages/ui/src/components/ui/aspect-ratio.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function AspectRatio({
ratio,
diff --git a/apps/web/src/components/ui/avatar.tsx b/packages/ui/src/components/ui/avatar.tsx
similarity index 98%
rename from apps/web/src/components/ui/avatar.tsx
rename to packages/ui/src/components/ui/avatar.tsx
index 6c744e2..b1b78ba 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/packages/ui/src/components/ui/avatar.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Avatar({
className,
diff --git a/apps/web/src/components/ui/badge.tsx b/packages/ui/src/components/ui/badge.tsx
similarity index 98%
rename from apps/web/src/components/ui/badge.tsx
rename to packages/ui/src/components/ui/badge.tsx
index 6d23feb..b1a243b 100644
--- a/apps/web/src/components/ui/badge.tsx
+++ b/packages/ui/src/components/ui/badge.tsx
@@ -2,7 +2,7 @@ import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
const badgeVariants = cva(
"h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge",
diff --git a/apps/web/src/components/ui/breadcrumb.tsx b/packages/ui/src/components/ui/breadcrumb.tsx
similarity index 96%
rename from apps/web/src/components/ui/breadcrumb.tsx
rename to packages/ui/src/components/ui/breadcrumb.tsx
index 935f09c..ee8e24d 100644
--- a/apps/web/src/components/ui/breadcrumb.tsx
+++ b/packages/ui/src/components/ui/breadcrumb.tsx
@@ -2,8 +2,8 @@ import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
-import { cn } from "@/lib/utils"
-import { ChevronRightIcon, DotsIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronRightIcon, DotsIcon } from "../icons"
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
return (
diff --git a/apps/web/src/components/ui/button-group.tsx b/packages/ui/src/components/ui/button-group.tsx
similarity index 96%
rename from apps/web/src/components/ui/button-group.tsx
rename to packages/ui/src/components/ui/button-group.tsx
index 587c0af..70b74a4 100644
--- a/apps/web/src/components/ui/button-group.tsx
+++ b/packages/ui/src/components/ui/button-group.tsx
@@ -2,8 +2,8 @@ import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { Separator } from "@/components/ui/separator"
+import { cn } from "../../lib/utils"
+import { Separator } from "./separator"
const buttonGroupVariants = cva(
"rounded-none has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-none flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
diff --git a/apps/web/src/components/ui/button.tsx b/packages/ui/src/components/ui/button.tsx
similarity index 98%
rename from apps/web/src/components/ui/button.tsx
rename to packages/ui/src/components/ui/button.tsx
index c9ed23b..1c3d928 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/packages/ui/src/components/ui/button.tsx
@@ -1,7 +1,7 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
diff --git a/apps/web/src/components/ui/calendar.tsx b/packages/ui/src/components/ui/calendar.tsx
similarity index 97%
rename from apps/web/src/components/ui/calendar.tsx
rename to packages/ui/src/components/ui/calendar.tsx
index 51fcb56..f055d26 100644
--- a/apps/web/src/components/ui/calendar.tsx
+++ b/packages/ui/src/components/ui/calendar.tsx
@@ -8,9 +8,9 @@ import {
type Locale,
} from "react-day-picker"
-import { cn } from "@/lib/utils"
-import { Button, buttonVariants } from "@/components/ui/button"
-import { ArrowLeftIcon, ArrowRightIcon, ArrowDownIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { Button, buttonVariants } from "./button"
+import { ArrowLeftIcon, ArrowRightIcon, ArrowDownIcon } from "../icons"
function Calendar({
className,
diff --git a/apps/web/src/components/ui/card.tsx b/packages/ui/src/components/ui/card.tsx
similarity index 98%
rename from apps/web/src/components/ui/card.tsx
rename to packages/ui/src/components/ui/card.tsx
index d63cd65..4d4f51c 100644
--- a/apps/web/src/components/ui/card.tsx
+++ b/packages/ui/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Card({
className,
diff --git a/apps/web/src/components/ui/carousel.tsx b/packages/ui/src/components/ui/carousel.tsx
similarity index 97%
rename from apps/web/src/components/ui/carousel.tsx
rename to packages/ui/src/components/ui/carousel.tsx
index 2838e48..1028379 100644
--- a/apps/web/src/components/ui/carousel.tsx
+++ b/packages/ui/src/components/ui/carousel.tsx
@@ -3,9 +3,9 @@ import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { ChevronLeftIcon, ChevronRightIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { ChevronLeftIcon, ChevronRightIcon } from "../icons"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters
diff --git a/apps/web/src/components/ui/chart.tsx b/packages/ui/src/components/ui/chart.tsx
similarity index 99%
rename from apps/web/src/components/ui/chart.tsx
rename to packages/ui/src/components/ui/chart.tsx
index 19d8ada..3ceb097 100644
--- a/apps/web/src/components/ui/chart.tsx
+++ b/packages/ui/src/components/ui/chart.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import * as RechartsPrimitive from "recharts"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
diff --git a/apps/web/src/components/ui/checkbox.tsx b/packages/ui/src/components/ui/checkbox.tsx
similarity index 94%
rename from apps/web/src/components/ui/checkbox.tsx
rename to packages/ui/src/components/ui/checkbox.tsx
index a3b183c..22226f3 100644
--- a/apps/web/src/components/ui/checkbox.tsx
+++ b/packages/ui/src/components/ui/checkbox.tsx
@@ -2,8 +2,8 @@
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
-import { cn } from "@/lib/utils"
-import { CheckIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { CheckIcon } from "../icons"
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
return (
diff --git a/apps/web/src/components/ui/collapsible.tsx b/packages/ui/src/components/ui/collapsible.tsx
similarity index 100%
rename from apps/web/src/components/ui/collapsible.tsx
rename to packages/ui/src/components/ui/collapsible.tsx
diff --git a/apps/web/src/components/ui/combobox.tsx b/packages/ui/src/components/ui/combobox.tsx
similarity index 97%
rename from apps/web/src/components/ui/combobox.tsx
rename to packages/ui/src/components/ui/combobox.tsx
index 96f7463..e2dc1b6 100644
--- a/apps/web/src/components/ui/combobox.tsx
+++ b/packages/ui/src/components/ui/combobox.tsx
@@ -3,15 +3,15 @@
import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
-} from "@/components/ui/input-group"
-import { ChevronDownIcon, XmarkIcon, CheckIcon } from "@/components/icons"
+} from "./input-group"
+import { ChevronDownIcon, XmarkIcon, CheckIcon } from "../icons"
const Combobox = ComboboxPrimitive.Root
diff --git a/apps/web/src/components/ui/command.tsx b/packages/ui/src/components/ui/command.tsx
similarity index 96%
rename from apps/web/src/components/ui/command.tsx
rename to packages/ui/src/components/ui/command.tsx
index 278bc46..b9a786e 100644
--- a/apps/web/src/components/ui/command.tsx
+++ b/packages/ui/src/components/ui/command.tsx
@@ -1,19 +1,19 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
-} from "@/components/ui/dialog"
+} from "./dialog"
import {
InputGroup,
InputGroupAddon,
-} from "@/components/ui/input-group"
-import { MagnifierIcon, CheckIcon } from "@/components/icons"
+} from "./input-group"
+import { MagnifierIcon, CheckIcon } from "../icons"
function Command({
className,
diff --git a/apps/web/src/components/ui/context-menu.tsx b/packages/ui/src/components/ui/context-menu.tsx
similarity index 98%
rename from apps/web/src/components/ui/context-menu.tsx
rename to packages/ui/src/components/ui/context-menu.tsx
index 7891b30..ca25322 100644
--- a/apps/web/src/components/ui/context-menu.tsx
+++ b/packages/ui/src/components/ui/context-menu.tsx
@@ -3,8 +3,8 @@
import * as React from "react"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
-import { cn } from "@/lib/utils"
-import { ChevronRightIcon, CheckIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronRightIcon, CheckIcon } from "../icons"
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/dialog.tsx b/packages/ui/src/components/ui/dialog.tsx
similarity index 96%
rename from apps/web/src/components/ui/dialog.tsx
rename to packages/ui/src/components/ui/dialog.tsx
index 94c559e..dba9fba 100644
--- a/apps/web/src/components/ui/dialog.tsx
+++ b/packages/ui/src/components/ui/dialog.tsx
@@ -3,9 +3,9 @@
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { XmarkIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { XmarkIcon } from "../icons"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/direction.tsx b/packages/ui/src/components/ui/direction.tsx
similarity index 100%
rename from apps/web/src/components/ui/direction.tsx
rename to packages/ui/src/components/ui/direction.tsx
diff --git a/apps/web/src/components/ui/drawer.tsx b/packages/ui/src/components/ui/drawer.tsx
similarity index 99%
rename from apps/web/src/components/ui/drawer.tsx
rename to packages/ui/src/components/ui/drawer.tsx
index 34e2b77..ea96b19 100644
--- a/apps/web/src/components/ui/drawer.tsx
+++ b/packages/ui/src/components/ui/drawer.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Drawer({
...props
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx
similarity index 98%
rename from apps/web/src/components/ui/dropdown-menu.tsx
rename to packages/ui/src/components/ui/dropdown-menu.tsx
index 22e8adb..fa90972 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/packages/ui/src/components/ui/dropdown-menu.tsx
@@ -1,8 +1,8 @@
import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
-import { cn } from "@/lib/utils"
-import { ChevronRightIcon, CheckIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronRightIcon, CheckIcon } from "../icons"
function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/empty.tsx b/packages/ui/src/components/ui/empty.tsx
similarity index 98%
rename from apps/web/src/components/ui/empty.tsx
rename to packages/ui/src/components/ui/empty.tsx
index 56b9661..be0f5b4 100644
--- a/apps/web/src/components/ui/empty.tsx
+++ b/packages/ui/src/components/ui/empty.tsx
@@ -1,6 +1,6 @@
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/apps/web/src/components/ui/field.tsx b/packages/ui/src/components/ui/field.tsx
similarity index 97%
rename from apps/web/src/components/ui/field.tsx
rename to packages/ui/src/components/ui/field.tsx
index b81cab9..a6a656c 100644
--- a/apps/web/src/components/ui/field.tsx
+++ b/packages/ui/src/components/ui/field.tsx
@@ -3,9 +3,9 @@
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { Label } from "@/components/ui/label"
-import { Separator } from "@/components/ui/separator"
+import { cn } from "../../lib/utils"
+import { Label } from "./label"
+import { Separator } from "./separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
diff --git a/apps/web/src/components/ui/gradient-chart.tsx b/packages/ui/src/components/ui/gradient-chart.tsx
similarity index 97%
rename from apps/web/src/components/ui/gradient-chart.tsx
rename to packages/ui/src/components/ui/gradient-chart.tsx
index 41a846b..d159cff 100644
--- a/apps/web/src/components/ui/gradient-chart.tsx
+++ b/packages/ui/src/components/ui/gradient-chart.tsx
@@ -6,7 +6,7 @@ import { Area, AreaChart } from "recharts";
import {
type ChartConfig,
ChartContainer,
-} from "@/components/ui/chart";
+} from "./chart";
interface SparklineProps {
data: { value: number }[];
diff --git a/apps/web/src/components/ui/hover-card.tsx b/packages/ui/src/components/ui/hover-card.tsx
similarity index 98%
rename from apps/web/src/components/ui/hover-card.tsx
rename to packages/ui/src/components/ui/hover-card.tsx
index 5ffbb34..59eeb2a 100644
--- a/apps/web/src/components/ui/hover-card.tsx
+++ b/packages/ui/src/components/ui/hover-card.tsx
@@ -2,7 +2,7 @@
import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/input-group.tsx b/packages/ui/src/components/ui/input-group.tsx
similarity index 96%
rename from apps/web/src/components/ui/input-group.tsx
rename to packages/ui/src/components/ui/input-group.tsx
index c49e22b..5947a8d 100644
--- a/apps/web/src/components/ui/input-group.tsx
+++ b/packages/ui/src/components/ui/input-group.tsx
@@ -1,10 +1,10 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { Input } from "./input"
+import { Textarea } from "./textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/apps/web/src/components/ui/input-otp.tsx b/packages/ui/src/components/ui/input-otp.tsx
similarity index 96%
rename from apps/web/src/components/ui/input-otp.tsx
rename to packages/ui/src/components/ui/input-otp.tsx
index 84c1338..6321216 100644
--- a/apps/web/src/components/ui/input-otp.tsx
+++ b/packages/ui/src/components/ui/input-otp.tsx
@@ -1,8 +1,8 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
-import { cn } from "@/lib/utils"
-import { MinusIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { MinusIcon } from "../icons"
function InputOTP({
className,
diff --git a/apps/web/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx
similarity index 96%
rename from apps/web/src/components/ui/input.tsx
rename to packages/ui/src/components/ui/input.tsx
index 52219c4..617c22c 100644
--- a/apps/web/src/components/ui/input.tsx
+++ b/packages/ui/src/components/ui/input.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
diff --git a/apps/web/src/components/ui/item.tsx b/packages/ui/src/components/ui/item.tsx
similarity index 98%
rename from apps/web/src/components/ui/item.tsx
rename to packages/ui/src/components/ui/item.tsx
index f923049..5ec4920 100644
--- a/apps/web/src/components/ui/item.tsx
+++ b/packages/ui/src/components/ui/item.tsx
@@ -3,8 +3,8 @@ import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { Separator } from "@/components/ui/separator"
+import { cn } from "../../lib/utils"
+import { Separator } from "./separator"
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/apps/web/src/components/ui/kbd.tsx b/packages/ui/src/components/ui/kbd.tsx
similarity index 95%
rename from apps/web/src/components/ui/kbd.tsx
rename to packages/ui/src/components/ui/kbd.tsx
index 30efbc9..a909a35 100644
--- a/apps/web/src/components/ui/kbd.tsx
+++ b/packages/ui/src/components/ui/kbd.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
diff --git a/apps/web/src/components/ui/label.tsx b/packages/ui/src/components/ui/label.tsx
similarity index 92%
rename from apps/web/src/components/ui/label.tsx
rename to packages/ui/src/components/ui/label.tsx
index 138bee1..0c0a993 100644
--- a/apps/web/src/components/ui/label.tsx
+++ b/packages/ui/src/components/ui/label.tsx
@@ -2,7 +2,7 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
diff --git a/apps/web/src/components/ui/menubar.tsx b/packages/ui/src/components/ui/menubar.tsx
similarity index 98%
rename from apps/web/src/components/ui/menubar.tsx
rename to packages/ui/src/components/ui/menubar.tsx
index ac4b177..9cce45e 100644
--- a/apps/web/src/components/ui/menubar.tsx
+++ b/packages/ui/src/components/ui/menubar.tsx
@@ -2,7 +2,7 @@ import * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
import {
DropdownMenu,
DropdownMenuContent,
@@ -17,8 +17,8 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { CheckIcon } from "@/components/icons"
+} from "./dropdown-menu"
+import { CheckIcon } from "../icons"
function Menubar({ className, ...props }: MenubarPrimitive.Props) {
return (
diff --git a/apps/web/src/components/ui/native-select.tsx b/packages/ui/src/components/ui/native-select.tsx
similarity index 95%
rename from apps/web/src/components/ui/native-select.tsx
rename to packages/ui/src/components/ui/native-select.tsx
index 2168b4d..df0ce0f 100644
--- a/apps/web/src/components/ui/native-select.tsx
+++ b/packages/ui/src/components/ui/native-select.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
-import { ChevronExpandYIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronExpandYIcon } from "../icons"
type NativeSelectProps = Omit, "size"> & {
size?: "sm" | "default"
diff --git a/apps/web/src/components/ui/navigation-menu.tsx b/packages/ui/src/components/ui/navigation-menu.tsx
similarity index 98%
rename from apps/web/src/components/ui/navigation-menu.tsx
rename to packages/ui/src/components/ui/navigation-menu.tsx
index 88b4b84..9be1a2c 100644
--- a/apps/web/src/components/ui/navigation-menu.tsx
+++ b/packages/ui/src/components/ui/navigation-menu.tsx
@@ -1,8 +1,8 @@
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { cva } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { ChevronDownIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronDownIcon } from "../icons"
function NavigationMenu({
align = "start",
diff --git a/apps/web/src/components/ui/pagination.tsx b/packages/ui/src/components/ui/pagination.tsx
similarity index 94%
rename from apps/web/src/components/ui/pagination.tsx
rename to packages/ui/src/components/ui/pagination.tsx
index 0a83690..b25a0a6 100644
--- a/apps/web/src/components/ui/pagination.tsx
+++ b/packages/ui/src/components/ui/pagination.tsx
@@ -1,8 +1,8 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { ChevronLeftIcon, ChevronRightIcon, DotsIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { ChevronLeftIcon, ChevronRightIcon, DotsIcon } from "../icons"
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
diff --git a/apps/web/src/components/ui/popover.tsx b/packages/ui/src/components/ui/popover.tsx
similarity index 98%
rename from apps/web/src/components/ui/popover.tsx
rename to packages/ui/src/components/ui/popover.tsx
index ee2f867..ada37f0 100644
--- a/apps/web/src/components/ui/popover.tsx
+++ b/packages/ui/src/components/ui/popover.tsx
@@ -1,7 +1,7 @@
import * as React from "react"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/progress.tsx b/packages/ui/src/components/ui/progress.tsx
similarity index 97%
rename from apps/web/src/components/ui/progress.tsx
rename to packages/ui/src/components/ui/progress.tsx
index f37167e..020933e 100644
--- a/apps/web/src/components/ui/progress.tsx
+++ b/packages/ui/src/components/ui/progress.tsx
@@ -2,7 +2,7 @@
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Progress({
className,
diff --git a/apps/web/src/components/ui/radio-group.tsx b/packages/ui/src/components/ui/radio-group.tsx
similarity index 94%
rename from apps/web/src/components/ui/radio-group.tsx
rename to packages/ui/src/components/ui/radio-group.tsx
index 9d47f74..7843146 100644
--- a/apps/web/src/components/ui/radio-group.tsx
+++ b/packages/ui/src/components/ui/radio-group.tsx
@@ -1,8 +1,8 @@
import { Radio as RadioPrimitive } from "@base-ui/react/radio"
import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
-import { cn } from "@/lib/utils"
-import { RadioCheckedIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { RadioCheckedIcon } from "../icons"
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
return (
diff --git a/apps/web/src/components/ui/resizable.tsx b/packages/ui/src/components/ui/resizable.tsx
similarity index 97%
rename from apps/web/src/components/ui/resizable.tsx
rename to packages/ui/src/components/ui/resizable.tsx
index cb8dabb..9f255f1 100644
--- a/apps/web/src/components/ui/resizable.tsx
+++ b/packages/ui/src/components/ui/resizable.tsx
@@ -2,7 +2,7 @@
import * as ResizablePrimitive from "react-resizable-panels"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function ResizablePanelGroup({
className,
diff --git a/apps/web/src/components/ui/scroll-area.tsx b/packages/ui/src/components/ui/scroll-area.tsx
similarity index 97%
rename from apps/web/src/components/ui/scroll-area.tsx
rename to packages/ui/src/components/ui/scroll-area.tsx
index 8a90afb..56d25da 100644
--- a/apps/web/src/components/ui/scroll-area.tsx
+++ b/packages/ui/src/components/ui/scroll-area.tsx
@@ -1,6 +1,6 @@
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function ScrollArea({
className,
diff --git a/apps/web/src/components/ui/select.tsx b/packages/ui/src/components/ui/select.tsx
similarity index 98%
rename from apps/web/src/components/ui/select.tsx
rename to packages/ui/src/components/ui/select.tsx
index 3d13e83..ce54d7e 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/packages/ui/src/components/ui/select.tsx
@@ -3,8 +3,8 @@
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
-import { cn } from "@/lib/utils"
-import { ChevronExpandYIcon, CheckIcon, ChevronUpIcon, ChevronDownIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { ChevronExpandYIcon, CheckIcon, ChevronUpIcon, ChevronDownIcon } from "../icons"
const Select = SelectPrimitive.Root
diff --git a/apps/web/src/components/ui/separator.tsx b/packages/ui/src/components/ui/separator.tsx
similarity index 93%
rename from apps/web/src/components/ui/separator.tsx
rename to packages/ui/src/components/ui/separator.tsx
index 3703d32..b44d459 100644
--- a/apps/web/src/components/ui/separator.tsx
+++ b/packages/ui/src/components/ui/separator.tsx
@@ -1,6 +1,6 @@
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Separator({
className,
diff --git a/apps/web/src/components/ui/sheet.tsx b/packages/ui/src/components/ui/sheet.tsx
similarity index 97%
rename from apps/web/src/components/ui/sheet.tsx
rename to packages/ui/src/components/ui/sheet.tsx
index c84658e..fc3a86a 100644
--- a/apps/web/src/components/ui/sheet.tsx
+++ b/packages/ui/src/components/ui/sheet.tsx
@@ -1,9 +1,9 @@
import * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { XmarkIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { XmarkIcon } from "../icons"
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return
diff --git a/apps/web/src/components/ui/sidebar.tsx b/packages/ui/src/components/ui/sidebar.tsx
similarity index 98%
rename from apps/web/src/components/ui/sidebar.tsx
rename to packages/ui/src/components/ui/sidebar.tsx
index 3a38dea..6ce2007 100644
--- a/apps/web/src/components/ui/sidebar.tsx
+++ b/packages/ui/src/components/ui/sidebar.tsx
@@ -3,25 +3,25 @@ import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Separator } from "@/components/ui/separator"
+import { cn } from "../../lib/utils"
+import { Button } from "./button"
+import { Input } from "./input"
+import { Separator } from "./separator"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
-} from "@/components/ui/sheet"
-import { Skeleton } from "@/components/ui/skeleton"
+} from "./sheet"
+import { Skeleton } from "./skeleton"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
-} from "@/components/ui/tooltip"
-import { useIsMobile } from "@/hooks/use-mobile"
-import { SidebarLeftIcon } from "@/components/icons"
+} from "./tooltip"
+import { useIsMobile } from "../../hooks/use-mobile"
+import { SidebarLeftIcon } from "../icons"
const SIDEBAR_STORAGE_KEY = "maple-sidebar-expanded"
const SIDEBAR_WIDTH = "16rem"
diff --git a/apps/web/src/components/ui/skeleton.tsx b/packages/ui/src/components/ui/skeleton.tsx
similarity index 86%
rename from apps/web/src/components/ui/skeleton.tsx
rename to packages/ui/src/components/ui/skeleton.tsx
index f768775..38a8037 100644
--- a/apps/web/src/components/ui/skeleton.tsx
+++ b/packages/ui/src/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/apps/web/src/components/ui/slider.tsx b/packages/ui/src/components/ui/slider.tsx
similarity index 98%
rename from apps/web/src/components/ui/slider.tsx
rename to packages/ui/src/components/ui/slider.tsx
index 55b56be..a1ab3db 100644
--- a/apps/web/src/components/ui/slider.tsx
+++ b/packages/ui/src/components/ui/slider.tsx
@@ -3,7 +3,7 @@
import * as React from "react"
import { Slider as SliderPrimitive } from "@base-ui/react/slider"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Slider({
className,
diff --git a/apps/web/src/components/ui/sonner.tsx b/packages/ui/src/components/ui/sonner.tsx
similarity index 96%
rename from apps/web/src/components/ui/sonner.tsx
rename to packages/ui/src/components/ui/sonner.tsx
index 0302475..a056f82 100644
--- a/apps/web/src/components/ui/sonner.tsx
+++ b/packages/ui/src/components/ui/sonner.tsx
@@ -1,6 +1,6 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
-import { CircleCheckIcon, CircleInfoIcon, AlertWarningIcon, CircleXmarkIcon, LoaderIcon } from "@/components/icons"
+import { CircleCheckIcon, CircleInfoIcon, AlertWarningIcon, CircleXmarkIcon, LoaderIcon } from "../icons"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
diff --git a/apps/web/src/components/ui/spinner.tsx b/packages/ui/src/components/ui/spinner.tsx
similarity index 80%
rename from apps/web/src/components/ui/spinner.tsx
rename to packages/ui/src/components/ui/spinner.tsx
index 990173c..ab5bbc0 100644
--- a/apps/web/src/components/ui/spinner.tsx
+++ b/packages/ui/src/components/ui/spinner.tsx
@@ -1,5 +1,5 @@
-import { cn } from "@/lib/utils"
-import { LoaderIcon } from "@/components/icons"
+import { cn } from "../../lib/utils"
+import { LoaderIcon } from "../icons"
function Spinner({
className,
diff --git a/apps/web/src/components/ui/switch.tsx b/packages/ui/src/components/ui/switch.tsx
similarity index 97%
rename from apps/web/src/components/ui/switch.tsx
rename to packages/ui/src/components/ui/switch.tsx
index cfa13af..d474a0f 100644
--- a/apps/web/src/components/ui/switch.tsx
+++ b/packages/ui/src/components/ui/switch.tsx
@@ -2,7 +2,7 @@
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Switch({
className,
diff --git a/apps/web/src/components/ui/table.tsx b/packages/ui/src/components/ui/table.tsx
similarity index 98%
rename from apps/web/src/components/ui/table.tsx
rename to packages/ui/src/components/ui/table.tsx
index 4886601..6e6de38 100644
--- a/apps/web/src/components/ui/table.tsx
+++ b/packages/ui/src/components/ui/table.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
diff --git a/apps/web/src/components/ui/tabs.tsx b/packages/ui/src/components/ui/tabs.tsx
similarity index 98%
rename from apps/web/src/components/ui/tabs.tsx
rename to packages/ui/src/components/ui/tabs.tsx
index 3200099..429730e 100644
--- a/apps/web/src/components/ui/tabs.tsx
+++ b/packages/ui/src/components/ui/tabs.tsx
@@ -3,7 +3,7 @@
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function Tabs({
className,
diff --git a/apps/web/src/components/ui/textarea.tsx b/packages/ui/src/components/ui/textarea.tsx
similarity index 96%
rename from apps/web/src/components/ui/textarea.tsx
rename to packages/ui/src/components/ui/textarea.tsx
index 1692ac9..51d157a 100644
--- a/apps/web/src/components/ui/textarea.tsx
+++ b/packages/ui/src/components/ui/textarea.tsx
@@ -1,6 +1,6 @@
import * as React from "react"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
const Textarea = React.forwardRef>(
({ className, ...props }, ref) => {
diff --git a/apps/web/src/components/ui/toggle-group.tsx b/packages/ui/src/components/ui/toggle-group.tsx
similarity index 97%
rename from apps/web/src/components/ui/toggle-group.tsx
rename to packages/ui/src/components/ui/toggle-group.tsx
index 4fb45ba..51b5a7a 100644
--- a/apps/web/src/components/ui/toggle-group.tsx
+++ b/packages/ui/src/components/ui/toggle-group.tsx
@@ -5,8 +5,8 @@ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
import { type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
-import { toggleVariants } from "@/components/ui/toggle"
+import { cn } from "../../lib/utils"
+import { toggleVariants } from "./toggle"
const ToggleGroupContext = React.createContext<
VariantProps & {
diff --git a/apps/web/src/components/ui/toggle.tsx b/packages/ui/src/components/ui/toggle.tsx
similarity index 97%
rename from apps/web/src/components/ui/toggle.tsx
rename to packages/ui/src/components/ui/toggle.tsx
index 6ef1f5d..6986473 100644
--- a/apps/web/src/components/ui/toggle.tsx
+++ b/packages/ui/src/components/ui/toggle.tsx
@@ -1,7 +1,7 @@
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
const toggleVariants = cva(
"hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-none text-xs font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle hover:bg-muted inline-flex items-center justify-center whitespace-nowrap outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
diff --git a/apps/web/src/components/ui/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx
similarity index 98%
rename from apps/web/src/components/ui/tooltip.tsx
rename to packages/ui/src/components/ui/tooltip.tsx
index 6c608e2..11cd2d7 100644
--- a/apps/web/src/components/ui/tooltip.tsx
+++ b/packages/ui/src/components/ui/tooltip.tsx
@@ -2,7 +2,7 @@
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
-import { cn } from "@/lib/utils"
+import { cn } from "../../lib/utils"
function TooltipProvider({
delay = 0,
diff --git a/packages/ui/src/env.d.ts b/packages/ui/src/env.d.ts
new file mode 100644
index 0000000..2eeb318
--- /dev/null
+++ b/packages/ui/src/env.d.ts
@@ -0,0 +1,4 @@
+declare module "*.css" {
+ const content: string
+ export default content
+}
diff --git a/packages/ui/src/hooks/use-mobile.tsx b/packages/ui/src/hooks/use-mobile.tsx
new file mode 100644
index 0000000..2b0fe1d
--- /dev/null
+++ b/packages/ui/src/hooks/use-mobile.tsx
@@ -0,0 +1,19 @@
+import * as React from "react"
+
+const MOBILE_BREAKPOINT = 768
+
+export function useIsMobile() {
+ const [isMobile, setIsMobile] = React.useState(undefined)
+
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ }
+ mql.addEventListener("change", onChange)
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
+ return () => mql.removeEventListener("change", onChange)
+ }, [])
+
+ return !!isMobile
+}
diff --git a/apps/web/src/lib/colors.ts b/packages/ui/src/lib/colors.ts
similarity index 100%
rename from apps/web/src/lib/colors.ts
rename to packages/ui/src/lib/colors.ts
diff --git a/packages/ui/src/lib/format.ts b/packages/ui/src/lib/format.ts
new file mode 100644
index 0000000..9e9b8fc
--- /dev/null
+++ b/packages/ui/src/lib/format.ts
@@ -0,0 +1,165 @@
+/**
+ * Format a duration in milliseconds to a human-readable string.
+ * - < 1ms: displays in microseconds (μs)
+ * - 1ms - 1000ms: displays in milliseconds (ms)
+ * - >= 1000ms: displays in seconds (s)
+ */
+export function formatDuration(ms: number): string {
+ if (ms < 1) {
+ return `${(ms * 1000).toFixed(0)}μs`
+ }
+ if (ms < 1000) {
+ return `${ms.toFixed(1)}ms`
+ }
+ return `${(ms / 1000).toFixed(2)}s`
+}
+
+/**
+ * Format a number with compact notation.
+ * - >= 1M: displays as e.g. "1.2M"
+ * - >= 1K: displays as e.g. "3.4K"
+ * - < 1K: displays with locale formatting
+ */
+export function formatNumber(num: number): string {
+ if (num >= 1_000_000) {
+ return `${(num / 1_000_000).toFixed(1)}M`
+ }
+ if (num >= 1_000) {
+ return `${(num / 1_000).toFixed(1)}K`
+ }
+ return num.toLocaleString()
+}
+
+/**
+ * Format a latency value in milliseconds to a human-readable string.
+ */
+export function formatLatency(ms: number): string {
+ if (ms == null || Number.isNaN(ms)) {
+ return "-"
+ }
+ if (ms < 1) {
+ return `${(ms * 1000).toFixed(0)}μs`
+ }
+ if (ms < 1000) {
+ return `${ms.toFixed(1)}ms`
+ }
+ return `${(ms / 1000).toFixed(2)}s`
+}
+
+/**
+ * Format an error rate percentage.
+ */
+export function formatErrorRate(rate: number): string {
+ if (rate < 0.01) {
+ return "0%"
+ }
+ if (rate < 1) {
+ return `${rate.toFixed(2)}%`
+ }
+ return `${rate.toFixed(1)}%`
+}
+
+/**
+ * Infer the bucket interval in seconds from consecutive data points.
+ * Expects data with a `bucket` string timestamp field.
+ */
+export function inferBucketSeconds(data: Array<{ bucket: string }>): number | undefined {
+ if (data.length < 2) return undefined
+ const t0 = new Date(data[0].bucket).getTime()
+ const t1 = new Date(data[1].bucket).getTime()
+ const diffMs = t1 - t0
+ if (diffMs <= 0 || Number.isNaN(diffMs)) return undefined
+ return diffMs / 1000
+}
+
+/**
+ * Parse a bucket value to a millisecond timestamp.
+ */
+export function parseBucketMs(value: unknown): number | null {
+ if (typeof value !== "string") return null
+ const parsed = new Date(value).getTime()
+ return Number.isNaN(parsed) ? null : parsed
+}
+
+/**
+ * Infer the total time range in milliseconds from an array of data points with a `bucket` key.
+ */
+export function inferRangeMs(data: Array>): number {
+ const bucketTimes = data
+ .map((row) => parseBucketMs(row.bucket))
+ .filter((value): value is number => value != null)
+
+ if (bucketTimes.length < 2) return 0
+ return Math.max(...bucketTimes) - Math.min(...bucketTimes)
+}
+
+/**
+ * Format a bucket timestamp label that adapts based on the overall time range:
+ * - >= 24h with daily buckets: "Feb 14"
+ * - >= 24h with sub-day buckets: "Feb 14, 02:00 PM"
+ * - 30min - 24h: "02:00 PM"
+ * - <= 30min: "02:00:30 PM"
+ */
+export function formatBucketLabel(
+ value: unknown,
+ context: { rangeMs: number; bucketSeconds: number | undefined },
+ mode: "tick" | "tooltip",
+): string {
+ if (typeof value !== "string") return ""
+
+ const date = new Date(value)
+ if (Number.isNaN(date.getTime())) return value
+
+ const includeDate = context.rangeMs >= 24 * 60 * 60 * 1000 || (context.bucketSeconds ?? 0) >= 24 * 60 * 60
+ const includeSeconds = context.rangeMs <= 30 * 60 * 1000 && !includeDate
+
+ if (mode === "tooltip") {
+ return date.toLocaleString(undefined, {
+ year: includeDate ? "numeric" : undefined,
+ month: includeDate ? "short" : undefined,
+ day: includeDate ? "numeric" : undefined,
+ hour: "2-digit",
+ minute: "2-digit",
+ second: includeSeconds ? "2-digit" : undefined,
+ })
+ }
+
+ if (includeDate) {
+ if ((context.bucketSeconds ?? 0) >= 24 * 60 * 60) {
+ return date.toLocaleDateString(undefined, { month: "short", day: "numeric" })
+ }
+ return date.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
+ }
+
+ return date
+ .toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: includeSeconds ? "2-digit" : undefined,
+ })
+ .replace(/^24:/, "00:")
+}
+
+const bucketLabelMap: Record = {
+ 60: "/min",
+ 300: "/5min",
+ 900: "/15min",
+ 3600: "/h",
+ 14400: "/4h",
+ 86400: "/d",
+}
+
+/**
+ * Map bucket interval seconds to a human-readable rate suffix.
+ */
+export function bucketIntervalLabel(seconds: number | undefined): string {
+ if (seconds == null) return ""
+ return bucketLabelMap[seconds] ?? ""
+}
+
+/**
+ * Format a throughput value with a rate suffix for chart axes.
+ */
+export function formatThroughput(value: number, suffix: string): string {
+ return `${formatNumber(value)}${suffix}`
+}
diff --git a/apps/landing/src/lib/types.ts b/packages/ui/src/lib/types.ts
similarity index 100%
rename from apps/landing/src/lib/types.ts
rename to packages/ui/src/lib/types.ts
diff --git a/apps/landing/src/lib/utils.ts b/packages/ui/src/lib/utils.ts
similarity index 100%
rename from apps/landing/src/lib/utils.ts
rename to packages/ui/src/lib/utils.ts
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
new file mode 100644
index 0000000..7e4b9b5
--- /dev/null
+++ b/packages/ui/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ }
+}