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