@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.
- 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 +4 -2
- 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-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-DBrhzauj.js → index-DuCQRbcQ.js} +6386 -6337
- package/dist/index-DuCQRbcQ.js.map +1 -0
- package/dist/{index-DxfW52oA.cjs → index-y_PNN5vK.cjs} +64 -46
- package/dist/index-y_PNN5vK.cjs.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-D6ndqfsd.js → profiler-FpBY9eRv.js} +2 -2
- package/dist/{profiler-D6ndqfsd.js.map → profiler-FpBY9eRv.js.map} +1 -1
- package/dist/{profiler-DhnzZ34c.cjs → profiler-_mthyjvo.cjs} +2 -2
- package/dist/{profiler-DhnzZ34c.cjs.map → profiler-_mthyjvo.cjs.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-BwXmdmy1.cjs → startRecording-NJcpiHw-.cjs} +2 -2
- package/dist/{startRecording-BwXmdmy1.cjs.map → startRecording-NJcpiHw-.cjs.map} +1 -1
- package/dist/{startRecording-B_9CRZ_P.js → startRecording-r5MXQ2Dm.js} +2 -2
- package/dist/{startRecording-B_9CRZ_P.js.map → startRecording-r5MXQ2Dm.js.map} +1 -1
- package/dist/types/index.d.ts +2 -2
- 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 +74 -61
- 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 +272 -235
- 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 +222 -211
- 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 +87 -82
- 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 +21 -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 +122 -122
- 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-DBrhzauj.js.map +0 -1
- 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
|
|
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,44 +166,74 @@ 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
|
-
})
|
|
178
|
-
|
|
179
|
-
const TIME_RANGE_MODEL =
|
|
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[
|
|
222
|
+
headers["Gram-Project"] = projectSlug;
|
|
193
223
|
}
|
|
194
224
|
|
|
195
225
|
const openRouter = createOpenRouter({
|
|
196
226
|
baseURL: apiUrl,
|
|
197
|
-
apiKey:
|
|
227
|
+
apiKey: "unused",
|
|
198
228
|
headers,
|
|
199
229
|
fetch: (url, init) =>
|
|
200
230
|
fetch(url, {
|
|
201
231
|
...init,
|
|
202
|
-
credentials:
|
|
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
|
-
|
|
241
|
-
const
|
|
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 ===
|
|
250
|
-
if (normalizedLabel ===
|
|
251
|
-
if (normalizedLabel ===
|
|
252
|
-
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";
|
|
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:
|
|
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:
|
|
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 !==
|
|
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?.(
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
399
|
-
setInputValue(
|
|
400
|
-
setIsEditing(false)
|
|
401
|
-
setIsOpen(false)
|
|
402
|
-
} 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) {
|
|
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 ||
|
|
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
|
|
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 ??
|
|
484
|
+
const presetObj = getPresetByValue(preset);
|
|
485
|
+
return presetObj?.label ?? "Select time range";
|
|
451
486
|
}
|
|
452
|
-
return
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
disabled &&
|
|
475
|
-
timezone &&
|
|
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="
|
|
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
|
-
|
|
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
|
-
?
|
|
492
|
-
:
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
!isEditing &&
|
|
518
|
-
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",
|
|
519
555
|
)}
|
|
520
556
|
/>
|
|
521
557
|
|
|
522
558
|
{/* Dropdown chevron */}
|
|
523
|
-
<ChevronDown className="
|
|
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="
|
|
541
|
-
<span className="text-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
isLive &&
|
|
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
|
-
|
|
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
|
-
?
|
|
595
|
-
:
|
|
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 =
|
|
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
|
-
|
|
615
|
-
isSelected ?
|
|
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
|
-
|
|
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
|
-
?
|
|
624
|
-
:
|
|
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 ?
|
|
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
|
-
|
|
646
|
-
customRange ?
|
|
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
|
-
|
|
688
|
+
"inline-flex items-center justify-center rounded px-1.5 py-0.5",
|
|
652
689
|
BADGE_WIDTH,
|
|
653
690
|
customRange
|
|
654
|
-
?
|
|
655
|
-
:
|
|
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 ?
|
|
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 =
|
|
710
|
+
TimeRangePicker.displayName = "TimeRangePicker";
|
|
674
711
|
|
|
675
|
-
export { TimeRangePicker }
|
|
712
|
+
export { TimeRangePicker };
|