@gram-ai/elements 1.27.3 → 1.27.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +72 -60
  2. package/README.typedoc.md +6 -6
  3. package/bin/cli.js +74 -74
  4. package/dist/compat-shims-CO9JXXV4.cjs.map +1 -1
  5. package/dist/{compat-shims-BPJ7Q68c.js → compat-shims-DxtUrORi.js} +4 -2
  6. package/dist/compat-shims-DxtUrORi.js.map +1 -0
  7. package/dist/components/ShareButton/index.d.ts +2 -2
  8. package/dist/components/assistant-ui/message-feedback.d.ts +1 -1
  9. package/dist/components/assistant-ui/tooltip-icon-button.d.ts +2 -2
  10. package/dist/components/ui/avatar.d.ts +2 -2
  11. package/dist/components/ui/button.d.ts +1 -1
  12. package/dist/components/ui/calendar.d.ts +1 -1
  13. package/dist/components/ui/collapsible.d.ts +1 -1
  14. package/dist/components/ui/dialog.d.ts +4 -4
  15. package/dist/components/ui/popover.d.ts +2 -2
  16. package/dist/components/ui/skeleton.d.ts +1 -1
  17. package/dist/components/ui/time-range-picker.d.ts +4 -2
  18. package/dist/components/ui/tool-ui.d.ts +7 -7
  19. package/dist/components/ui/tooltip.d.ts +2 -2
  20. package/dist/contexts/ConnectionStatusContext.d.ts +1 -1
  21. package/dist/elements.cjs +1 -1
  22. package/dist/elements.css +1 -1
  23. package/dist/elements.js +2 -2
  24. package/dist/hooks/useDensity.d.ts +73 -73
  25. package/dist/hooks/useMCPTools.d.ts +1 -1
  26. package/dist/hooks/useRadius.d.ts +1 -1
  27. package/dist/{index-BpJstUh1.cjs → index-C4bFBGfl.cjs} +4 -4
  28. package/dist/{index-BpJstUh1.cjs.map → index-C4bFBGfl.cjs.map} +1 -1
  29. package/dist/{index-CUitXazZ.js → index-D93pV0_o.js} +55 -55
  30. package/dist/{index-CUitXazZ.js.map → index-D93pV0_o.js.map} +1 -1
  31. package/dist/{index-DBrhzauj.js → index-DuCQRbcQ.js} +6386 -6337
  32. package/dist/index-DuCQRbcQ.js.map +1 -0
  33. package/dist/{index-DxfW52oA.cjs → index-y_PNN5vK.cjs} +64 -46
  34. package/dist/index-y_PNN5vK.cjs.map +1 -0
  35. package/dist/lib/cassette.d.ts +4 -4
  36. package/dist/lib/errorTracking.d.ts +1 -1
  37. package/dist/lib/messageConverter.d.ts +1 -1
  38. package/dist/lib/models.d.ts +1 -1
  39. package/dist/plugins/chart/ui/bar-chart.d.ts +1 -1
  40. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +2 -2
  41. package/dist/plugins/generative-ui/ui/accordion.d.ts +1 -1
  42. package/dist/plugins/generative-ui/ui/action-button.d.ts +2 -2
  43. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +1 -1
  44. package/dist/plugins/generative-ui/ui/alert.d.ts +4 -4
  45. package/dist/plugins/generative-ui/ui/avatar.d.ts +5 -5
  46. package/dist/plugins/generative-ui/ui/badge.d.ts +2 -2
  47. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +2 -2
  48. package/dist/plugins/generative-ui/ui/button.d.ts +2 -2
  49. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +2 -2
  50. package/dist/plugins/generative-ui/ui/card.d.ts +8 -8
  51. package/dist/plugins/generative-ui/ui/checkbox.d.ts +1 -1
  52. package/dist/plugins/generative-ui/ui/data-table.d.ts +2 -2
  53. package/dist/plugins/generative-ui/ui/dialog.d.ts +3 -3
  54. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +3 -3
  55. package/dist/plugins/generative-ui/ui/grid.d.ts +3 -3
  56. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +1 -1
  57. package/dist/plugins/generative-ui/ui/input.d.ts +2 -2
  58. package/dist/plugins/generative-ui/ui/label.d.ts +1 -1
  59. package/dist/plugins/generative-ui/ui/metric.d.ts +3 -3
  60. package/dist/plugins/generative-ui/ui/pagination.d.ts +6 -6
  61. package/dist/plugins/generative-ui/ui/popover.d.ts +4 -4
  62. package/dist/plugins/generative-ui/ui/progress.d.ts +2 -2
  63. package/dist/plugins/generative-ui/ui/radio-group.d.ts +1 -1
  64. package/dist/plugins/generative-ui/ui/select.d.ts +2 -2
  65. package/dist/plugins/generative-ui/ui/separator.d.ts +1 -1
  66. package/dist/plugins/generative-ui/ui/skeleton.d.ts +1 -1
  67. package/dist/plugins/generative-ui/ui/stack.d.ts +6 -6
  68. package/dist/plugins/generative-ui/ui/switch.d.ts +2 -2
  69. package/dist/plugins/generative-ui/ui/table.d.ts +9 -9
  70. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +1 -1
  71. package/dist/plugins/generative-ui/ui/tabs.d.ts +1 -1
  72. package/dist/plugins/generative-ui/ui/text.d.ts +3 -3
  73. package/dist/plugins/generative-ui/ui/textarea.d.ts +2 -2
  74. package/dist/plugins/generative-ui/ui/tooltip.d.ts +1 -1
  75. package/dist/plugins.cjs +1 -1
  76. package/dist/plugins.js +1 -1
  77. package/dist/{profiler-D6ndqfsd.js → profiler-FpBY9eRv.js} +2 -2
  78. package/dist/{profiler-D6ndqfsd.js.map → profiler-FpBY9eRv.js.map} +1 -1
  79. package/dist/{profiler-DhnzZ34c.cjs → profiler-_mthyjvo.cjs} +2 -2
  80. package/dist/{profiler-DhnzZ34c.cjs.map → profiler-_mthyjvo.cjs.map} +1 -1
  81. package/dist/react-shim.js +1 -1
  82. package/dist/server/express.cjs.map +1 -1
  83. package/dist/server/express.js.map +1 -1
  84. package/dist/{startRecording-BwXmdmy1.cjs → startRecording-NJcpiHw-.cjs} +2 -2
  85. package/dist/{startRecording-BwXmdmy1.cjs.map → startRecording-NJcpiHw-.cjs.map} +1 -1
  86. package/dist/{startRecording-B_9CRZ_P.js → startRecording-r5MXQ2Dm.js} +2 -2
  87. package/dist/{startRecording-B_9CRZ_P.js.map → startRecording-r5MXQ2Dm.js.map} +1 -1
  88. package/dist/types/index.d.ts +2 -2
  89. package/package.json +1 -5
  90. package/src/compat-plugin.ts +14 -14
  91. package/src/compat-shims.ts +33 -31
  92. package/src/compat.test.ts +48 -48
  93. package/src/compat.ts +6 -6
  94. package/src/components/Chat/index.tsx +17 -17
  95. package/src/components/Chat/stories/Charts.stories.tsx +98 -98
  96. package/src/components/Chat/stories/Composer.stories.tsx +15 -15
  97. package/src/components/Chat/stories/ConnectionConfiguration.stories.tsx +44 -44
  98. package/src/components/Chat/stories/CustomComponents.stories.tsx +17 -17
  99. package/src/components/Chat/stories/Density.stories.tsx +20 -20
  100. package/src/components/Chat/stories/ErrorBoundary.stories.tsx +47 -47
  101. package/src/components/Chat/stories/FrontendTools.stories.tsx +39 -39
  102. package/src/components/Chat/stories/GenerativeUI.stories.tsx +48 -48
  103. package/src/components/Chat/stories/MessageFeedback.stories.tsx +52 -52
  104. package/src/components/Chat/stories/Modal.stories.tsx +28 -28
  105. package/src/components/Chat/stories/Model.stories.tsx +11 -11
  106. package/src/components/Chat/stories/Radius.stories.tsx +20 -20
  107. package/src/components/Chat/stories/Sidecar.stories.tsx +13 -13
  108. package/src/components/Chat/stories/StyleIsolation.stories.tsx +11 -11
  109. package/src/components/Chat/stories/Theme.stories.tsx +25 -25
  110. package/src/components/Chat/stories/Thread.stories.tsx +25 -25
  111. package/src/components/Chat/stories/ToolApproval.stories.tsx +55 -55
  112. package/src/components/Chat/stories/ToolMentions.stories.tsx +17 -17
  113. package/src/components/Chat/stories/Tools.stories.tsx +88 -88
  114. package/src/components/Chat/stories/Variants.stories.tsx +32 -32
  115. package/src/components/Chat/stories/Welcome.stories.tsx +14 -14
  116. package/src/components/ChatHistory.tsx +7 -7
  117. package/src/components/FrontendTools/index.tsx +5 -5
  118. package/src/components/Replay.stories.tsx +157 -157
  119. package/src/components/Replay.tsx +76 -73
  120. package/src/components/ShadowRoot.tsx +40 -40
  121. package/src/components/ShareButton/index.tsx +32 -32
  122. package/src/components/assistant-ui/assistant-modal.tsx +92 -87
  123. package/src/components/assistant-ui/assistant-sidecar.tsx +35 -35
  124. package/src/components/assistant-ui/attachment.tsx +80 -80
  125. package/src/components/assistant-ui/connection-status-indicator.tsx +33 -33
  126. package/src/components/assistant-ui/error-boundary.tsx +34 -34
  127. package/src/components/assistant-ui/follow-on-suggestions.tsx +26 -26
  128. package/src/components/assistant-ui/markdown-text.tsx +69 -69
  129. package/src/components/assistant-ui/mentioned-tools-badges.tsx +38 -38
  130. package/src/components/assistant-ui/message-feedback.tsx +74 -61
  131. package/src/components/assistant-ui/reasoning.tsx +83 -83
  132. package/src/components/assistant-ui/thread-list.tsx +45 -45
  133. package/src/components/assistant-ui/thread.tsx +278 -278
  134. package/src/components/assistant-ui/tool-fallback.tsx +37 -37
  135. package/src/components/assistant-ui/tool-group.tsx +26 -26
  136. package/src/components/assistant-ui/tool-mention-autocomplete.tsx +122 -122
  137. package/src/components/assistant-ui/tooltip-icon-button.tsx +18 -18
  138. package/src/components/ui/avatar.tsx +12 -12
  139. package/src/components/ui/button.tsx +12 -12
  140. package/src/components/ui/buttonVariants.ts +17 -17
  141. package/src/components/ui/calendar.tsx +106 -106
  142. package/src/components/ui/charts.stories.tsx +56 -56
  143. package/src/components/ui/collapsible.tsx +5 -5
  144. package/src/components/ui/dialog.tsx +30 -30
  145. package/src/components/ui/generative-ui.stories.tsx +200 -200
  146. package/src/components/ui/generative-ui.tsx +26 -26
  147. package/src/components/ui/popover.tsx +14 -14
  148. package/src/components/ui/skeleton.tsx +5 -5
  149. package/src/components/ui/time-range-picker.stories.tsx +80 -80
  150. package/src/components/ui/time-range-picker.tsx +272 -235
  151. package/src/components/ui/tool-ui.stories.tsx +37 -37
  152. package/src/components/ui/tool-ui.tsx +221 -215
  153. package/src/components/ui/tooltip.tsx +15 -15
  154. package/src/constants/tailwind.ts +1 -1
  155. package/src/contexts/ChatIdContext.tsx +7 -7
  156. package/src/contexts/ConnectionStatusContext.tsx +64 -64
  157. package/src/contexts/ElementsProvider.tsx +222 -211
  158. package/src/contexts/ReplayContext.ts +3 -3
  159. package/src/contexts/ToolApprovalContext.tsx +54 -54
  160. package/src/contexts/ToolExecutionContext.tsx +34 -34
  161. package/src/contexts/contexts.ts +7 -7
  162. package/src/contexts/portal-container-context.ts +2 -2
  163. package/src/contexts/portal-container.tsx +7 -7
  164. package/src/embedded.ts +1 -1
  165. package/src/global.css +25 -25
  166. package/src/hooks/useAuth.ts +72 -72
  167. package/src/hooks/useDensity.ts +79 -79
  168. package/src/hooks/useElements.ts +6 -6
  169. package/src/hooks/useExpanded.ts +12 -12
  170. package/src/hooks/useFollowOnSuggestions.ts +87 -82
  171. package/src/hooks/useGramThreadListAdapter.tsx +99 -99
  172. package/src/hooks/useMCPTools.ts +47 -47
  173. package/src/hooks/useModel.ts +14 -14
  174. package/src/hooks/usePluginComponents.ts +11 -11
  175. package/src/hooks/usePortalContainer.ts +5 -5
  176. package/src/hooks/useRadius.ts +23 -23
  177. package/src/hooks/useRecordCassette.ts +34 -34
  178. package/src/hooks/useSession.ts +11 -11
  179. package/src/hooks/useThemeProps.ts +13 -13
  180. package/src/hooks/useThreadId.ts +4 -4
  181. package/src/hooks/useToolApproval.ts +7 -7
  182. package/src/hooks/useToolMentions.ts +40 -40
  183. package/src/index.ts +26 -26
  184. package/src/lib/api.test.ts +61 -61
  185. package/src/lib/api.ts +4 -3
  186. package/src/lib/auth.ts +13 -13
  187. package/src/lib/cassette.ts +84 -84
  188. package/src/lib/easing.ts +1 -1
  189. package/src/lib/errorTracking.config.ts +5 -5
  190. package/src/lib/errorTracking.ts +29 -29
  191. package/src/lib/generative-ui.ts +7 -7
  192. package/src/lib/humanize.ts +3 -3
  193. package/src/lib/messageConverter.test.ts +130 -127
  194. package/src/lib/messageConverter.ts +196 -196
  195. package/src/lib/models.ts +21 -20
  196. package/src/lib/token.test.ts +56 -56
  197. package/src/lib/token.ts +14 -14
  198. package/src/lib/tool-mentions.ts +45 -45
  199. package/src/lib/tools.ts +66 -62
  200. package/src/lib/utils.ts +5 -5
  201. package/src/lib.d.ts +1 -1
  202. package/src/plugins/README.md +5 -5
  203. package/src/plugins/chart/catalog.ts +18 -18
  204. package/src/plugins/chart/chart.test.ts +31 -31
  205. package/src/plugins/chart/component.tsx +34 -34
  206. package/src/plugins/chart/index.ts +4 -4
  207. package/src/plugins/chart/ui/area-chart.tsx +42 -42
  208. package/src/plugins/chart/ui/bar-chart.tsx +46 -46
  209. package/src/plugins/chart/ui/donut-chart.tsx +48 -48
  210. package/src/plugins/chart/ui/index.ts +7 -7
  211. package/src/plugins/chart/ui/line-chart.tsx +43 -43
  212. package/src/plugins/chart/ui/pie-chart.tsx +44 -44
  213. package/src/plugins/chart/ui/radar-chart.tsx +33 -33
  214. package/src/plugins/chart/ui/scatter-chart.tsx +43 -43
  215. package/src/plugins/components/MacOSWindowFrame.tsx +15 -15
  216. package/src/plugins/components/PluginLoadingState.tsx +10 -10
  217. package/src/plugins/components/index.ts +1 -1
  218. package/src/plugins/generative-ui/catalog.ts +54 -54
  219. package/src/plugins/generative-ui/component.tsx +85 -85
  220. package/src/plugins/generative-ui/index.ts +4 -4
  221. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +16 -16
  222. package/src/plugins/generative-ui/ui/accordion.tsx +16 -16
  223. package/src/plugins/generative-ui/ui/action-button.tsx +28 -28
  224. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +8 -8
  225. package/src/plugins/generative-ui/ui/alert.tsx +20 -20
  226. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +7 -7
  227. package/src/plugins/generative-ui/ui/avatar.tsx +30 -30
  228. package/src/plugins/generative-ui/ui/badge.tsx +22 -22
  229. package/src/plugins/generative-ui/ui/button-wrapper.tsx +12 -12
  230. package/src/plugins/generative-ui/ui/button.tsx +28 -28
  231. package/src/plugins/generative-ui/ui/card-wrapper.tsx +8 -8
  232. package/src/plugins/generative-ui/ui/card.tsx +27 -27
  233. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +9 -9
  234. package/src/plugins/generative-ui/ui/checkbox.tsx +9 -9
  235. package/src/plugins/generative-ui/ui/data-table.tsx +8 -8
  236. package/src/plugins/generative-ui/ui/dialog.tsx +31 -31
  237. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +44 -44
  238. package/src/plugins/generative-ui/ui/grid.tsx +12 -12
  239. package/src/plugins/generative-ui/ui/index.ts +40 -40
  240. package/src/plugins/generative-ui/ui/input-wrapper.tsx +11 -11
  241. package/src/plugins/generative-ui/ui/input.tsx +9 -9
  242. package/src/plugins/generative-ui/ui/label.tsx +8 -8
  243. package/src/plugins/generative-ui/ui/list.tsx +11 -11
  244. package/src/plugins/generative-ui/ui/metric.tsx +23 -23
  245. package/src/plugins/generative-ui/ui/pagination.tsx +28 -28
  246. package/src/plugins/generative-ui/ui/popover.tsx +21 -21
  247. package/src/plugins/generative-ui/ui/progress.tsx +13 -13
  248. package/src/plugins/generative-ui/ui/radio-group.tsx +12 -12
  249. package/src/plugins/generative-ui/ui/select-wrapper.tsx +7 -7
  250. package/src/plugins/generative-ui/ui/select.tsx +37 -37
  251. package/src/plugins/generative-ui/ui/separator.tsx +9 -9
  252. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +10 -10
  253. package/src/plugins/generative-ui/ui/skeleton.tsx +5 -5
  254. package/src/plugins/generative-ui/ui/stack.tsx +28 -28
  255. package/src/plugins/generative-ui/ui/switch.tsx +11 -11
  256. package/src/plugins/generative-ui/ui/table.tsx +32 -32
  257. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +11 -11
  258. package/src/plugins/generative-ui/ui/tabs.tsx +26 -26
  259. package/src/plugins/generative-ui/ui/text.tsx +12 -12
  260. package/src/plugins/generative-ui/ui/textarea.tsx +7 -7
  261. package/src/plugins/generative-ui/ui/tooltip.tsx +12 -12
  262. package/src/plugins/index.ts +7 -7
  263. package/src/react-shim.ts +6 -6
  264. package/src/server/bun.ts +12 -12
  265. package/src/server/core.ts +25 -25
  266. package/src/server/express.ts +17 -15
  267. package/src/server/fastify.ts +14 -14
  268. package/src/server/hono.ts +9 -9
  269. package/src/server/nextjs.ts +12 -12
  270. package/src/server/tanstack-start.ts +12 -12
  271. package/src/server.ts +27 -27
  272. package/src/storybook.d.ts +4 -4
  273. package/src/types/index.ts +122 -122
  274. package/src/types/plugins.ts +7 -7
  275. package/src/vite-env.d.ts +12 -12
  276. package/dist/compat-shims-BPJ7Q68c.js.map +0 -1
  277. package/dist/index-DBrhzauj.js.map +0 -1
  278. package/dist/index-DxfW52oA.cjs.map +0 -1
@@ -1,55 +1,55 @@
1
1
  /* eslint-disable react-refresh/only-export-components */
2
- import * as React from 'react'
3
- import { CalendarIcon, ChevronDown, Zap } from 'lucide-react'
4
- import { generateObject } from 'ai'
5
- import { createOpenRouter } from '@openrouter/ai-sdk-provider'
6
- import { z } from 'zod'
2
+ import * as React from "react";
3
+ import { CalendarIcon, ChevronDown, Zap } from "lucide-react";
4
+ import { generateObject, LanguageModel } from "ai";
5
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
6
+ import { z } from "zod";
7
7
 
8
- import { cn } from '@/lib/utils'
9
- import { Popover, PopoverContent, PopoverTrigger } from './popover'
10
- import { Calendar } from './calendar'
8
+ import { cn } from "@/lib/utils";
9
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
10
+ import { Calendar } from "./calendar";
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Types
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  export interface TimeRange {
17
- from: Date
18
- to: Date
17
+ from: Date;
18
+ to: Date;
19
19
  }
20
20
 
21
21
  export type DateRangePreset =
22
- | '15m'
23
- | '1h'
24
- | '4h'
25
- | '1d'
26
- | '2d'
27
- | '3d'
28
- | '7d'
29
- | '15d'
30
- | '30d'
31
- | '90d'
22
+ | "15m"
23
+ | "1h"
24
+ | "4h"
25
+ | "1d"
26
+ | "2d"
27
+ | "3d"
28
+ | "7d"
29
+ | "15d"
30
+ | "30d"
31
+ | "90d";
32
32
 
33
33
  export interface TimeRangePreset {
34
- label: string
35
- shortLabel: string
36
- value: DateRangePreset
37
- getRange: () => TimeRange
34
+ label: string;
35
+ shortLabel: string;
36
+ value: DateRangePreset;
37
+ getRange: () => TimeRange;
38
38
  }
39
39
 
40
40
  // ---------------------------------------------------------------------------
41
41
  // Date Utilities (no external dependencies)
42
42
  // ---------------------------------------------------------------------------
43
43
 
44
- function formatDate(date: Date, pattern: 'short' | 'medium' = 'short'): string {
45
- if (pattern === 'short') {
46
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
44
+ function formatDate(date: Date, pattern: "short" | "medium" = "short"): string {
45
+ if (pattern === "short") {
46
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
47
47
  }
48
- return date.toLocaleDateString('en-US', {
49
- month: 'short',
50
- day: 'numeric',
51
- year: 'numeric',
52
- })
48
+ return date.toLocaleDateString("en-US", {
49
+ month: "short",
50
+ day: "numeric",
51
+ year: "numeric",
52
+ });
53
53
  }
54
54
 
55
55
  // ---------------------------------------------------------------------------
@@ -58,107 +58,107 @@ function formatDate(date: Date, pattern: 'short' | 'medium' = 'short'): string {
58
58
 
59
59
  export const PRESETS: TimeRangePreset[] = [
60
60
  {
61
- label: 'Past 15 Minutes',
62
- shortLabel: '15m',
63
- value: '15m',
61
+ label: "Past 15 Minutes",
62
+ shortLabel: "15m",
63
+ value: "15m",
64
64
  getRange: () => ({
65
65
  from: new Date(Date.now() - 15 * 60 * 1000),
66
66
  to: new Date(),
67
67
  }),
68
68
  },
69
69
  {
70
- label: 'Past 1 Hour',
71
- shortLabel: '1h',
72
- value: '1h',
70
+ label: "Past 1 Hour",
71
+ shortLabel: "1h",
72
+ value: "1h",
73
73
  getRange: () => ({
74
74
  from: new Date(Date.now() - 60 * 60 * 1000),
75
75
  to: new Date(),
76
76
  }),
77
77
  },
78
78
  {
79
- label: 'Past 4 Hours',
80
- shortLabel: '4h',
81
- value: '4h',
79
+ label: "Past 4 Hours",
80
+ shortLabel: "4h",
81
+ value: "4h",
82
82
  getRange: () => ({
83
83
  from: new Date(Date.now() - 4 * 60 * 60 * 1000),
84
84
  to: new Date(),
85
85
  }),
86
86
  },
87
87
  {
88
- label: 'Past 1 Day',
89
- shortLabel: '1d',
90
- value: '1d',
88
+ label: "Past 1 Day",
89
+ shortLabel: "1d",
90
+ value: "1d",
91
91
  getRange: () => ({
92
92
  from: new Date(Date.now() - 24 * 60 * 60 * 1000),
93
93
  to: new Date(),
94
94
  }),
95
95
  },
96
96
  {
97
- label: 'Past 2 Days',
98
- shortLabel: '2d',
99
- value: '2d',
97
+ label: "Past 2 Days",
98
+ shortLabel: "2d",
99
+ value: "2d",
100
100
  getRange: () => ({
101
101
  from: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
102
102
  to: new Date(),
103
103
  }),
104
104
  },
105
105
  {
106
- label: 'Past 3 Days',
107
- shortLabel: '3d',
108
- value: '3d',
106
+ label: "Past 3 Days",
107
+ shortLabel: "3d",
108
+ value: "3d",
109
109
  getRange: () => ({
110
110
  from: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
111
111
  to: new Date(),
112
112
  }),
113
113
  },
114
114
  {
115
- label: 'Past 7 Days',
116
- shortLabel: '1w',
117
- value: '7d',
115
+ label: "Past 7 Days",
116
+ shortLabel: "1w",
117
+ value: "7d",
118
118
  getRange: () => ({
119
119
  from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
120
120
  to: new Date(),
121
121
  }),
122
122
  },
123
123
  {
124
- label: 'Past 15 Days',
125
- shortLabel: '15d',
126
- value: '15d',
124
+ label: "Past 15 Days",
125
+ shortLabel: "15d",
126
+ value: "15d",
127
127
  getRange: () => ({
128
128
  from: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
129
129
  to: new Date(),
130
130
  }),
131
131
  },
132
132
  {
133
- label: 'Past 1 Month',
134
- shortLabel: '1mo',
135
- value: '30d',
133
+ label: "Past 1 Month",
134
+ shortLabel: "1mo",
135
+ value: "30d",
136
136
  getRange: () => ({
137
137
  from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
138
138
  to: new Date(),
139
139
  }),
140
140
  },
141
141
  {
142
- label: 'Past 3 Months',
143
- shortLabel: '3mo',
144
- value: '90d',
142
+ label: "Past 3 Months",
143
+ shortLabel: "3mo",
144
+ value: "90d",
145
145
  getRange: () => ({
146
146
  from: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
147
147
  to: new Date(),
148
148
  }),
149
149
  },
150
- ]
150
+ ];
151
151
 
152
152
  // Badge width class - shared between trigger and dropdown for alignment
153
- const BADGE_WIDTH = 'min-w-10'
153
+ const BADGE_WIDTH = "min-w-10";
154
154
 
155
155
  export function getPresetRange(preset: DateRangePreset): TimeRange {
156
- const p = PRESETS.find((p) => p.value === preset)
157
- return p ? p.getRange() : PRESETS[5].getRange() // Default to 3d
156
+ const p = PRESETS.find((p) => p.value === preset);
157
+ return p ? p.getRange() : PRESETS[5].getRange(); // Default to 3d
158
158
  }
159
159
 
160
160
  function getPresetByValue(value: DateRangePreset): TimeRangePreset | undefined {
161
- return PRESETS.find((p) => p.value === value)
161
+ return PRESETS.find((p) => p.value === value);
162
162
  }
163
163
 
164
164
  // ---------------------------------------------------------------------------
@@ -166,44 +166,74 @@ function getPresetByValue(value: DateRangePreset): TimeRangePreset | undefined {
166
166
  // ---------------------------------------------------------------------------
167
167
 
168
168
  type ParseResult =
169
- | { type: 'preset'; preset: DateRangePreset }
170
- | { type: 'custom'; range: TimeRange; label?: string }
171
- | null
169
+ | { type: "preset"; preset: DateRangePreset }
170
+ | { type: "custom"; range: TimeRange; label?: string }
171
+ | null;
172
172
 
173
173
  const timeRangeSchema = z.object({
174
- from: z.string().describe('ISO8601 start date/time'),
175
- to: z.string().describe('ISO8601 end date/time'),
176
- label: z.string().describe('Short semantic label for the range'),
177
- })
178
-
179
- const TIME_RANGE_MODEL = 'openai/gpt-4o-mini'
174
+ from: z.string().describe("ISO8601 start date/time"),
175
+ to: z.string().describe("ISO8601 end date/time"),
176
+ label: z.string().describe("Short semantic label for the range"),
177
+ });
178
+
179
+ const TIME_RANGE_MODEL = "openai/gpt-4o-mini";
180
+
181
+ /**
182
+ * Parse an ISO date string as a local date (ignoring timezone).
183
+ * This prevents timezone shifts when the AI returns dates like "2026-02-09T00:00:00Z"
184
+ * which would otherwise display as Feb 8 in US timezones.
185
+ */
186
+ function parseAsLocalDate(isoString: string): Date {
187
+ // Try to extract just the date part and create a local date
188
+ const dateMatch = isoString.match(/^(\d{4})-(\d{2})-(\d{2})/);
189
+ if (dateMatch) {
190
+ const [, year, month, day] = dateMatch;
191
+ // Check if there's a time component
192
+ const timeMatch = isoString.match(/T(\d{2}):(\d{2}):?(\d{2})?/);
193
+ if (timeMatch) {
194
+ const [, hours, minutes, seconds = "0"] = timeMatch;
195
+ return new Date(
196
+ parseInt(year),
197
+ parseInt(month) - 1,
198
+ parseInt(day),
199
+ parseInt(hours),
200
+ parseInt(minutes),
201
+ parseInt(seconds),
202
+ );
203
+ }
204
+ // Date only - use start of day local time
205
+ return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
206
+ }
207
+ // Fallback to standard parsing
208
+ return new Date(isoString);
209
+ }
180
210
 
181
211
  async function parseWithAI(
182
212
  input: string,
183
213
  apiUrl: string,
184
- projectSlug?: string
214
+ projectSlug?: string,
185
215
  ): Promise<ParseResult> {
186
216
  try {
187
- const now = new Date()
217
+ const now = new Date();
188
218
 
189
219
  // Create OpenRouter provider without X-Gram-Source header (so usage is billed)
190
- const headers: Record<string, string> = {}
220
+ const headers: Record<string, string> = {};
191
221
  if (projectSlug) {
192
- headers['Gram-Project'] = projectSlug
222
+ headers["Gram-Project"] = projectSlug;
193
223
  }
194
224
 
195
225
  const openRouter = createOpenRouter({
196
226
  baseURL: apiUrl,
197
- apiKey: 'unused',
227
+ apiKey: "unused",
198
228
  headers,
199
229
  fetch: (url, init) =>
200
230
  fetch(url, {
201
231
  ...init,
202
- credentials: 'include',
232
+ credentials: "include",
203
233
  }),
204
- })
234
+ });
205
235
 
206
- const model = openRouter.chat(TIME_RANGE_MODEL)
236
+ const model = openRouter.chat(TIME_RANGE_MODEL) as LanguageModel;
207
237
 
208
238
  const result = await generateObject({
209
239
  model,
@@ -217,6 +247,7 @@ KEY RULES:
217
247
  - "X years ago" = THE WHOLE YEAR (from: Jan 1, to: Dec 31)
218
248
  - "past X days" = RANGE from X days ago to now
219
249
  - "last wednesday" etc = that specific day (whole day)
250
+ - IMPORTANT: Return dates WITHOUT timezone suffix (no "Z"). Use format like "2026-02-09T00:00:00" not "2026-02-09T00:00:00Z"
220
251
 
221
252
  LABEL RULES - use semantic labels:
222
253
  - Duration presets: "15m", "1h", "4h", "1d", "2d", "3d", "7d", "15d", "30d"
@@ -234,32 +265,33 @@ Examples:
234
265
  - "jan 5 to jan 10" -> label: "1/5-1/10"
235
266
 
236
267
  User input: ${input}`,
237
- })
268
+ });
238
269
 
239
- const parsed = result.object
240
- const from = new Date(parsed.from)
241
- const to = new Date(parsed.to)
270
+ const parsed = result.object;
271
+ // Parse dates as local to avoid timezone shifts
272
+ const from = parseAsLocalDate(parsed.from);
273
+ const to = parseAsLocalDate(parsed.to);
242
274
 
243
275
  if (isNaN(from.getTime()) || isNaN(to.getTime())) {
244
- return null
276
+ return null;
245
277
  }
246
278
 
247
279
  // Normalize labels like "1w" -> "7d", "2w" -> "14d"
248
- let normalizedLabel = parsed.label
249
- if (normalizedLabel === '1w') normalizedLabel = '7d'
250
- if (normalizedLabel === '2w') normalizedLabel = '14d'
251
- if (normalizedLabel === '1mo') normalizedLabel = '30d'
252
- if (normalizedLabel === '3mo') normalizedLabel = '90d'
280
+ let normalizedLabel = parsed.label;
281
+ if (normalizedLabel === "1w") normalizedLabel = "7d";
282
+ if (normalizedLabel === "2w") normalizedLabel = "14d";
283
+ if (normalizedLabel === "1mo") normalizedLabel = "30d";
284
+ if (normalizedLabel === "3mo") normalizedLabel = "90d";
253
285
 
254
- const matchedPreset = PRESETS.find((p) => p.value === normalizedLabel)
286
+ const matchedPreset = PRESETS.find((p) => p.value === normalizedLabel);
255
287
  if (matchedPreset) {
256
- return { type: 'preset', preset: matchedPreset.value }
288
+ return { type: "preset", preset: matchedPreset.value };
257
289
  }
258
290
 
259
291
  // Use the semantic label from AI (e.g., "Mon", "Jan", "2024", "1/5-1/10")
260
- return { type: 'custom', range: { from, to }, label: parsed.label }
292
+ return { type: "custom", range: { from, to }, label: parsed.label };
261
293
  } catch {
262
- return null
294
+ return null;
263
295
  }
264
296
  }
265
297
 
@@ -269,31 +301,33 @@ User input: ${input}`,
269
301
 
270
302
  export interface TimeRangePickerProps {
271
303
  /** Current preset value */
272
- preset?: DateRangePreset | null
304
+ preset?: DateRangePreset | null;
273
305
  /** Current custom range */
274
- customRange?: TimeRange | null
306
+ customRange?: TimeRange | null;
275
307
  /** Called when a preset is selected */
276
- onPresetChange?: (preset: DateRangePreset) => void
308
+ onPresetChange?: (preset: DateRangePreset) => void;
277
309
  /** Called when a custom range is selected */
278
- onCustomRangeChange?: (from: Date, to: Date, label?: string) => void
310
+ onCustomRangeChange?: (from: Date, to: Date, label?: string) => void;
279
311
  /** Called to clear custom range */
280
- onClearCustomRange?: () => void
312
+ onClearCustomRange?: () => void;
281
313
  /** Initial label for custom range (from URL params) */
282
- customRangeLabel?: string | null
314
+ customRangeLabel?: string | null;
283
315
  /** Show LIVE mode option */
284
- showLive?: boolean
316
+ showLive?: boolean;
285
317
  /** Is LIVE mode active */
286
- isLive?: boolean
318
+ isLive?: boolean;
287
319
  /** Called when LIVE mode changes */
288
- onLiveChange?: (isLive: boolean) => void
320
+ onLiveChange?: (isLive: boolean) => void;
289
321
  /** Disabled state */
290
- disabled?: boolean
322
+ disabled?: boolean;
291
323
  /** Timezone display (e.g., "UTC-08:00") */
292
- timezone?: string
324
+ timezone?: string;
293
325
  /** API URL for AI parsing (defaults to window.location.origin) */
294
- apiUrl?: string
326
+ apiUrl?: string;
295
327
  /** Project slug for API authentication */
296
- projectSlug?: string
328
+ projectSlug?: string;
329
+ /** Additional class name for the trigger */
330
+ className?: string;
297
331
  }
298
332
 
299
333
  function TimeRangePicker({
@@ -310,174 +344,176 @@ function TimeRangePicker({
310
344
  timezone,
311
345
  apiUrl,
312
346
  projectSlug,
347
+ className,
313
348
  }: TimeRangePickerProps) {
314
- const [isOpen, setIsOpen] = React.useState(false)
315
- const [showCalendar, setShowCalendar] = React.useState(false)
316
- const [inputValue, setInputValue] = React.useState('')
317
- const [isEditing, setIsEditing] = React.useState(false)
318
- const [isParsing, setIsParsing] = React.useState(false)
349
+ const [isOpen, setIsOpen] = React.useState(false);
350
+ const [showCalendar, setShowCalendar] = React.useState(false);
351
+ const [inputValue, setInputValue] = React.useState("");
352
+ const [isEditing, setIsEditing] = React.useState(false);
353
+ const [isParsing, setIsParsing] = React.useState(false);
319
354
  const [customLabel, setCustomLabel] = React.useState<string | null>(
320
- initialCustomLabel || null
321
- )
322
- const inputRef = React.useRef<HTMLInputElement>(null)
355
+ initialCustomLabel || null,
356
+ );
357
+ const inputRef = React.useRef<HTMLInputElement>(null);
323
358
 
324
359
  // Sync custom label from props (e.g., when URL changes)
325
360
  React.useEffect(() => {
326
361
  if (initialCustomLabel !== undefined) {
327
- setCustomLabel(initialCustomLabel || null)
362
+ setCustomLabel(initialCustomLabel || null);
328
363
  }
329
- }, [initialCustomLabel])
364
+ }, [initialCustomLabel]);
330
365
 
331
366
  const effectiveApiUrl =
332
- apiUrl || (typeof window !== 'undefined' ? window.location.origin : '')
367
+ apiUrl || (typeof window !== "undefined" ? window.location.origin : "");
333
368
 
334
369
  const handlePresetClick = (p: TimeRangePreset) => {
335
- onPresetChange?.(p.value)
336
- setCustomLabel(null)
337
- setIsOpen(false)
338
- setInputValue('')
339
- }
370
+ onPresetChange?.(p.value);
371
+ setCustomLabel(null);
372
+ setIsOpen(false);
373
+ setInputValue("");
374
+ };
340
375
 
341
376
  const handleLiveClick = () => {
342
- onLiveChange?.(!isLive)
377
+ onLiveChange?.(!isLive);
343
378
  if (!isLive) {
344
379
  // When enabling LIVE, also select a default short preset
345
- onPresetChange?.('15m')
380
+ onPresetChange?.("15m");
346
381
  }
347
- setIsOpen(false)
348
- }
382
+ setIsOpen(false);
383
+ };
349
384
 
350
385
  const handleCalendarSelect = (range: { start: Date; end: Date | null }) => {
351
386
  if (range.start && range.end) {
352
- onCustomRangeChange?.(range.start, range.end)
353
- setCustomLabel(null) // Calendar selections don't have AI labels
354
- setIsOpen(false)
355
- setShowCalendar(false)
356
- setInputValue('')
387
+ onCustomRangeChange?.(range.start, range.end);
388
+ setCustomLabel(null); // Calendar selections don't have AI labels
389
+ setIsOpen(false);
390
+ setShowCalendar(false);
391
+ setInputValue("");
357
392
  }
358
- }
393
+ };
359
394
 
360
395
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
361
- setInputValue(e.target.value)
362
- }
396
+ setInputValue(e.target.value);
397
+ };
363
398
 
364
399
  const applyParseResult = (parsed: ParseResult) => {
365
400
  if (parsed) {
366
- if (parsed.type === 'preset') {
367
- onPresetChange?.(parsed.preset)
368
- setCustomLabel(null)
401
+ if (parsed.type === "preset") {
402
+ onPresetChange?.(parsed.preset);
403
+ setCustomLabel(null);
369
404
  } else {
370
- const label = parsed.label || undefined
371
- onCustomRangeChange?.(parsed.range.from, parsed.range.to, label)
372
- setCustomLabel(label || null)
405
+ const label = parsed.label || undefined;
406
+ onCustomRangeChange?.(parsed.range.from, parsed.range.to, label);
407
+ setCustomLabel(label || null);
373
408
  }
374
- setInputValue('')
375
- setIsOpen(false)
376
- setIsEditing(false)
377
- return true
409
+ setInputValue("");
410
+ setIsOpen(false);
411
+ setIsEditing(false);
412
+ return true;
378
413
  }
379
- return false
380
- }
414
+ return false;
415
+ };
381
416
 
382
417
  const handleInputKeyDown = async (
383
- e: React.KeyboardEvent<HTMLInputElement>
418
+ e: React.KeyboardEvent<HTMLInputElement>,
384
419
  ) => {
385
- if (e.key === 'Enter' && inputValue.trim() && !isParsing) {
420
+ if (e.key === "Enter" && inputValue.trim() && !isParsing) {
386
421
  // Use AI to parse natural language input
387
- setIsParsing(true)
422
+ setIsParsing(true);
388
423
  try {
389
424
  const aiParsed = await parseWithAI(
390
425
  inputValue,
391
426
  effectiveApiUrl,
392
- projectSlug
393
- )
394
- applyParseResult(aiParsed)
427
+ projectSlug,
428
+ );
429
+ applyParseResult(aiParsed);
395
430
  } finally {
396
- setIsParsing(false)
431
+ setIsParsing(false);
397
432
  }
398
- } else if (e.key === 'Escape') {
399
- setInputValue('')
400
- setIsEditing(false)
401
- setIsOpen(false)
402
- } else if (e.key === 'Backspace' && inputValue === '' && customRange) {
433
+ } else if (e.key === "Escape") {
434
+ setInputValue("");
435
+ setIsEditing(false);
436
+ setIsOpen(false);
437
+ } else if (e.key === "Backspace" && inputValue === "" && customRange) {
403
438
  // Clear custom range when backspacing on empty input
404
- e.preventDefault()
405
- onClearCustomRange?.()
439
+ e.preventDefault();
440
+ onClearCustomRange?.();
406
441
  }
407
- }
442
+ };
408
443
 
409
444
  const handleInputClick = (e: React.MouseEvent) => {
410
445
  // Prevent the popover trigger from toggling closed
411
- e.stopPropagation()
412
- setIsEditing(true)
413
- setIsOpen(true)
414
- }
446
+ e.stopPropagation();
447
+ setIsEditing(true);
448
+ setIsOpen(true);
449
+ };
415
450
 
416
451
  const handleInputFocus = () => {
417
- setIsEditing(true)
452
+ setIsEditing(true);
418
453
  // Don't set isOpen here - let the click handler or popover manage it
419
- }
454
+ };
420
455
 
421
456
  const handleInputBlur = () => {
422
457
  // Delay to allow click events on dropdown items
423
458
  setTimeout(() => {
424
459
  if (!inputValue) {
425
- setIsEditing(false)
460
+ setIsEditing(false);
426
461
  }
427
- }, 150)
428
- }
462
+ }, 150);
463
+ };
429
464
 
430
465
  // Determine current range for display
431
- const currentRange = customRange ?? (preset ? getPresetRange(preset) : null)
466
+ const currentRange = customRange ?? (preset ? getPresetRange(preset) : null);
432
467
 
433
468
  // Get short label for trigger badge
434
469
  const getShortLabel = () => {
435
- if (customRange) return customLabel || 'Custom'
470
+ if (customRange) return customLabel || "Custom";
436
471
  if (preset) {
437
- const presetObj = getPresetByValue(preset)
438
- return presetObj?.shortLabel ?? preset
472
+ const presetObj = getPresetByValue(preset);
473
+ return presetObj?.shortLabel ?? preset;
439
474
  }
440
- return '7d'
441
- }
475
+ return "7d";
476
+ };
442
477
 
443
478
  // Get label text (preset label or custom range description)
444
479
  const getLabelText = () => {
445
480
  if (customRange) {
446
- return `${formatDate(customRange.from)} – ${formatDate(customRange.to)}`
481
+ return `${formatDate(customRange.from)} – ${formatDate(customRange.to)}`;
447
482
  }
448
483
  if (preset) {
449
- const presetObj = getPresetByValue(preset)
450
- return presetObj?.label ?? 'Select time range'
484
+ const presetObj = getPresetByValue(preset);
485
+ return presetObj?.label ?? "Select time range";
451
486
  }
452
- return 'Select time range'
453
- }
487
+ return "Select time range";
488
+ };
454
489
 
455
490
  const handleOpenChange = (open: boolean) => {
456
491
  // If closing while editing, keep it open unless explicitly closed via selection
457
492
  if (!open && isEditing) {
458
- return
493
+ return;
459
494
  }
460
- setIsOpen(open)
495
+ setIsOpen(open);
461
496
  if (open && inputRef.current) {
462
497
  // Focus input when opening
463
- setTimeout(() => inputRef.current?.focus(), 0)
498
+ setTimeout(() => inputRef.current?.focus(), 0);
464
499
  }
465
- }
500
+ };
466
501
 
467
502
  return (
468
503
  <Popover open={isOpen} onOpenChange={handleOpenChange}>
469
504
  <PopoverTrigger asChild disabled={disabled}>
470
505
  <div
471
506
  className={cn(
472
- 'bg-background relative inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-all outline-none',
473
- 'border-border hover:border-border/80',
474
- disabled && 'cursor-not-allowed opacity-50',
475
- timezone && 'pt-4'
507
+ "relative inline-flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-all outline-none",
508
+ "border-border hover:border-border/80",
509
+ disabled && "cursor-not-allowed opacity-50",
510
+ timezone && "pt-4",
511
+ className,
476
512
  )}
477
513
  >
478
514
  {/* Floating timezone legend */}
479
515
  {timezone && (
480
- <span className="bg-background text-muted-foreground absolute -top-2 left-3 px-1 text-xs">
516
+ <span className="absolute -top-2 left-3 bg-background px-1 text-xs text-muted-foreground">
481
517
  {timezone}
482
518
  </span>
483
519
  )}
@@ -485,11 +521,11 @@ function TimeRangePicker({
485
521
  {/* Short badge */}
486
522
  <span
487
523
  className={cn(
488
- 'inline-flex h-6 items-center justify-center rounded px-2 py-1 text-xs font-semibold',
524
+ "inline-flex h-6 items-center justify-center rounded px-2 py-1 text-xs font-semibold",
489
525
  BADGE_WIDTH,
490
526
  isLive
491
- ? 'bg-green-500 text-white'
492
- : 'bg-muted text-muted-foreground'
527
+ ? "bg-green-500 text-white"
528
+ : "bg-muted text-muted-foreground",
493
529
  )}
494
530
  >
495
531
  {isParsing ? (
@@ -512,15 +548,15 @@ function TimeRangePicker({
512
548
  placeholder="e.g., 3 days ago, last week..."
513
549
  disabled={disabled}
514
550
  className={cn(
515
- 'min-w-[140px] flex-1 bg-transparent outline-none',
516
- 'placeholder:text-muted-foreground/60',
517
- !isEditing && 'cursor-pointer',
518
- disabled && 'cursor-not-allowed'
551
+ "min-w-[140px] flex-1 bg-transparent outline-none",
552
+ "placeholder:text-muted-foreground/60",
553
+ !isEditing && "cursor-pointer",
554
+ disabled && "cursor-not-allowed",
519
555
  )}
520
556
  />
521
557
 
522
558
  {/* Dropdown chevron */}
523
- <ChevronDown className="text-muted-foreground h-4 w-4 shrink-0" />
559
+ <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground" />
524
560
  </div>
525
561
  </PopoverTrigger>
526
562
 
@@ -529,22 +565,22 @@ function TimeRangePicker({
529
565
  align="start"
530
566
  onOpenAutoFocus={(e) => {
531
567
  // Prevent popover from stealing focus from the input
532
- e.preventDefault()
533
- inputRef.current?.focus()
568
+ e.preventDefault();
569
+ inputRef.current?.focus();
534
570
  }}
535
571
  >
536
572
  <div className="flex flex-col">
537
573
  {/* Calendar view */}
538
574
  {showCalendar ? (
539
575
  <>
540
- <div className="border-border/50 flex items-center justify-between border-b px-3 py-2">
541
- <span className="text-muted-foreground text-xs font-medium">
576
+ <div className="flex items-center justify-between border-b border-border/50 px-3 py-2">
577
+ <span className="text-xs font-medium text-muted-foreground">
542
578
  Select date range
543
579
  </span>
544
580
  <button
545
581
  type="button"
546
582
  onClick={() => setShowCalendar(false)}
547
- className="text-primary text-xs hover:underline"
583
+ className="text-xs text-primary hover:underline"
548
584
  >
549
585
  Back
550
586
  </button>
@@ -558,14 +594,14 @@ function TimeRangePicker({
558
594
  maxDate={new Date()}
559
595
  />
560
596
  {customRange && onClearCustomRange && (
561
- <div className="border-border/50 border-t p-2">
597
+ <div className="border-t border-border/50 p-2">
562
598
  <button
563
599
  type="button"
564
600
  onClick={() => {
565
- onClearCustomRange()
566
- setShowCalendar(false)
601
+ onClearCustomRange();
602
+ setShowCalendar(false);
567
603
  }}
568
- className="text-muted-foreground hover:text-foreground w-full text-xs transition-colors"
604
+ className="w-full text-xs text-muted-foreground transition-colors hover:text-foreground"
569
605
  >
570
606
  Clear custom range
571
607
  </button>
@@ -581,18 +617,18 @@ function TimeRangePicker({
581
617
  type="button"
582
618
  onClick={handleLiveClick}
583
619
  className={cn(
584
- 'flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors',
585
- 'hover:bg-muted',
586
- isLive && 'bg-blue-500/10'
620
+ "flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors",
621
+ "hover:bg-muted",
622
+ isLive && "bg-blue-500/10",
587
623
  )}
588
624
  >
589
625
  <span
590
626
  className={cn(
591
- 'inline-flex items-center justify-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold',
627
+ "inline-flex items-center justify-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold",
592
628
  BADGE_WIDTH,
593
629
  isLive
594
- ? 'bg-green-500 text-white'
595
- : 'bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400'
630
+ ? "bg-green-500 text-white"
631
+ : "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400",
596
632
  )}
597
633
  >
598
634
  <Zap className="h-3 w-3" />
@@ -604,37 +640,38 @@ function TimeRangePicker({
604
640
 
605
641
  {/* Preset options */}
606
642
  {PRESETS.map((p) => {
607
- const isSelected = preset === p.value && !customRange && !isLive
643
+ const isSelected =
644
+ preset === p.value && !customRange && !isLive;
608
645
  return (
609
646
  <button
610
647
  key={p.value}
611
648
  type="button"
612
649
  onClick={() => handlePresetClick(p)}
613
650
  className={cn(
614
- 'flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors',
615
- isSelected ? 'bg-blue-500 text-white' : 'hover:bg-muted'
651
+ "flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors",
652
+ isSelected ? "bg-blue-500 text-white" : "hover:bg-muted",
616
653
  )}
617
654
  >
618
655
  <span
619
656
  className={cn(
620
- 'inline-flex items-center justify-center rounded px-1.5 py-0.5 text-xs font-semibold',
657
+ "inline-flex items-center justify-center rounded px-1.5 py-0.5 text-xs font-semibold",
621
658
  BADGE_WIDTH,
622
659
  isSelected
623
- ? 'bg-white/20 text-white'
624
- : 'bg-muted text-muted-foreground'
660
+ ? "bg-white/20 text-white"
661
+ : "bg-muted text-muted-foreground",
625
662
  )}
626
663
  >
627
664
  {p.shortLabel}
628
665
  </span>
629
666
  <span
630
667
  className={
631
- isSelected ? 'text-white' : 'text-foreground/80'
668
+ isSelected ? "text-white" : "text-foreground/80"
632
669
  }
633
670
  >
634
671
  {p.label}
635
672
  </span>
636
673
  </button>
637
- )
674
+ );
638
675
  })}
639
676
 
640
677
  {/* Select from calendar */}
@@ -642,23 +679,23 @@ function TimeRangePicker({
642
679
  type="button"
643
680
  onClick={() => setShowCalendar(true)}
644
681
  className={cn(
645
- 'flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors',
646
- customRange ? 'bg-blue-500 text-white' : 'hover:bg-muted'
682
+ "flex w-full items-center gap-3 px-3 py-2 text-sm transition-colors",
683
+ customRange ? "bg-blue-500 text-white" : "hover:bg-muted",
647
684
  )}
648
685
  >
649
686
  <span
650
687
  className={cn(
651
- 'inline-flex items-center justify-center rounded px-1.5 py-0.5',
688
+ "inline-flex items-center justify-center rounded px-1.5 py-0.5",
652
689
  BADGE_WIDTH,
653
690
  customRange
654
- ? 'bg-white/20 text-white'
655
- : 'bg-muted text-muted-foreground'
691
+ ? "bg-white/20 text-white"
692
+ : "bg-muted text-muted-foreground",
656
693
  )}
657
694
  >
658
695
  <CalendarIcon className="h-4 w-4" />
659
696
  </span>
660
697
  <span
661
- className={customRange ? 'text-white' : 'text-foreground/80'}
698
+ className={customRange ? "text-white" : "text-foreground/80"}
662
699
  >
663
700
  Select from calendar...
664
701
  </span>
@@ -668,8 +705,8 @@ function TimeRangePicker({
668
705
  </div>
669
706
  </PopoverContent>
670
707
  </Popover>
671
- )
708
+ );
672
709
  }
673
- TimeRangePicker.displayName = 'TimeRangePicker'
710
+ TimeRangePicker.displayName = "TimeRangePicker";
674
711
 
675
- export { TimeRangePicker }
712
+ export { TimeRangePicker };