@gram-ai/elements 1.27.4 → 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 +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.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-D0bAYNQy.js → index-DuCQRbcQ.js} +279 -265
- package/dist/index-DuCQRbcQ.js.map +1 -0
- package/dist/{index-KSX4Qjip.cjs → index-y_PNN5vK.cjs} +10 -10
- 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-BFkhZRxj.js → profiler-FpBY9eRv.js} +2 -2
- package/dist/{profiler-BFkhZRxj.js.map → profiler-FpBY9eRv.js.map} +1 -1
- package/dist/{profiler-CyzxBxVz.cjs → profiler-_mthyjvo.cjs} +2 -2
- package/dist/{profiler-CyzxBxVz.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-Dq92sEHf.cjs → startRecording-NJcpiHw-.cjs} +2 -2
- package/dist/{startRecording-Dq92sEHf.cjs.map → startRecording-NJcpiHw-.cjs.map} +1 -1
- package/dist/{startRecording-C-PPAs_Z.js → startRecording-r5MXQ2Dm.js} +2 -2
- package/dist/{startRecording-C-PPAs_Z.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 +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 +245 -244
- 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 +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-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-4o-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,15 +548,15 @@ 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
|
|
|
@@ -565,22 +565,22 @@ function TimeRangePicker({
|
|
|
565
565
|
align="start"
|
|
566
566
|
onOpenAutoFocus={(e) => {
|
|
567
567
|
// Prevent popover from stealing focus from the input
|
|
568
|
-
e.preventDefault()
|
|
569
|
-
inputRef.current?.focus()
|
|
568
|
+
e.preventDefault();
|
|
569
|
+
inputRef.current?.focus();
|
|
570
570
|
}}
|
|
571
571
|
>
|
|
572
572
|
<div className="flex flex-col">
|
|
573
573
|
{/* Calendar view */}
|
|
574
574
|
{showCalendar ? (
|
|
575
575
|
<>
|
|
576
|
-
<div className="
|
|
577
|
-
<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">
|
|
578
578
|
Select date range
|
|
579
579
|
</span>
|
|
580
580
|
<button
|
|
581
581
|
type="button"
|
|
582
582
|
onClick={() => setShowCalendar(false)}
|
|
583
|
-
className="text-
|
|
583
|
+
className="text-xs text-primary hover:underline"
|
|
584
584
|
>
|
|
585
585
|
Back
|
|
586
586
|
</button>
|
|
@@ -594,14 +594,14 @@ function TimeRangePicker({
|
|
|
594
594
|
maxDate={new Date()}
|
|
595
595
|
/>
|
|
596
596
|
{customRange && onClearCustomRange && (
|
|
597
|
-
<div className="border-border/50
|
|
597
|
+
<div className="border-t border-border/50 p-2">
|
|
598
598
|
<button
|
|
599
599
|
type="button"
|
|
600
600
|
onClick={() => {
|
|
601
|
-
onClearCustomRange()
|
|
602
|
-
setShowCalendar(false)
|
|
601
|
+
onClearCustomRange();
|
|
602
|
+
setShowCalendar(false);
|
|
603
603
|
}}
|
|
604
|
-
className="text-muted-foreground hover:text-foreground
|
|
604
|
+
className="w-full text-xs text-muted-foreground transition-colors hover:text-foreground"
|
|
605
605
|
>
|
|
606
606
|
Clear custom range
|
|
607
607
|
</button>
|
|
@@ -617,18 +617,18 @@ function TimeRangePicker({
|
|
|
617
617
|
type="button"
|
|
618
618
|
onClick={handleLiveClick}
|
|
619
619
|
className={cn(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
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",
|
|
623
623
|
)}
|
|
624
624
|
>
|
|
625
625
|
<span
|
|
626
626
|
className={cn(
|
|
627
|
-
|
|
627
|
+
"inline-flex items-center justify-center gap-1 rounded px-1.5 py-0.5 text-xs font-semibold",
|
|
628
628
|
BADGE_WIDTH,
|
|
629
629
|
isLive
|
|
630
|
-
?
|
|
631
|
-
:
|
|
630
|
+
? "bg-green-500 text-white"
|
|
631
|
+
: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-400",
|
|
632
632
|
)}
|
|
633
633
|
>
|
|
634
634
|
<Zap className="h-3 w-3" />
|
|
@@ -640,37 +640,38 @@ function TimeRangePicker({
|
|
|
640
640
|
|
|
641
641
|
{/* Preset options */}
|
|
642
642
|
{PRESETS.map((p) => {
|
|
643
|
-
const isSelected =
|
|
643
|
+
const isSelected =
|
|
644
|
+
preset === p.value && !customRange && !isLive;
|
|
644
645
|
return (
|
|
645
646
|
<button
|
|
646
647
|
key={p.value}
|
|
647
648
|
type="button"
|
|
648
649
|
onClick={() => handlePresetClick(p)}
|
|
649
650
|
className={cn(
|
|
650
|
-
|
|
651
|
-
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",
|
|
652
653
|
)}
|
|
653
654
|
>
|
|
654
655
|
<span
|
|
655
656
|
className={cn(
|
|
656
|
-
|
|
657
|
+
"inline-flex items-center justify-center rounded px-1.5 py-0.5 text-xs font-semibold",
|
|
657
658
|
BADGE_WIDTH,
|
|
658
659
|
isSelected
|
|
659
|
-
?
|
|
660
|
-
:
|
|
660
|
+
? "bg-white/20 text-white"
|
|
661
|
+
: "bg-muted text-muted-foreground",
|
|
661
662
|
)}
|
|
662
663
|
>
|
|
663
664
|
{p.shortLabel}
|
|
664
665
|
</span>
|
|
665
666
|
<span
|
|
666
667
|
className={
|
|
667
|
-
isSelected ?
|
|
668
|
+
isSelected ? "text-white" : "text-foreground/80"
|
|
668
669
|
}
|
|
669
670
|
>
|
|
670
671
|
{p.label}
|
|
671
672
|
</span>
|
|
672
673
|
</button>
|
|
673
|
-
)
|
|
674
|
+
);
|
|
674
675
|
})}
|
|
675
676
|
|
|
676
677
|
{/* Select from calendar */}
|
|
@@ -678,23 +679,23 @@ function TimeRangePicker({
|
|
|
678
679
|
type="button"
|
|
679
680
|
onClick={() => setShowCalendar(true)}
|
|
680
681
|
className={cn(
|
|
681
|
-
|
|
682
|
-
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",
|
|
683
684
|
)}
|
|
684
685
|
>
|
|
685
686
|
<span
|
|
686
687
|
className={cn(
|
|
687
|
-
|
|
688
|
+
"inline-flex items-center justify-center rounded px-1.5 py-0.5",
|
|
688
689
|
BADGE_WIDTH,
|
|
689
690
|
customRange
|
|
690
|
-
?
|
|
691
|
-
:
|
|
691
|
+
? "bg-white/20 text-white"
|
|
692
|
+
: "bg-muted text-muted-foreground",
|
|
692
693
|
)}
|
|
693
694
|
>
|
|
694
695
|
<CalendarIcon className="h-4 w-4" />
|
|
695
696
|
</span>
|
|
696
697
|
<span
|
|
697
|
-
className={customRange ?
|
|
698
|
+
className={customRange ? "text-white" : "text-foreground/80"}
|
|
698
699
|
>
|
|
699
700
|
Select from calendar...
|
|
700
701
|
</span>
|
|
@@ -704,8 +705,8 @@ function TimeRangePicker({
|
|
|
704
705
|
</div>
|
|
705
706
|
</PopoverContent>
|
|
706
707
|
</Popover>
|
|
707
|
-
)
|
|
708
|
+
);
|
|
708
709
|
}
|
|
709
|
-
TimeRangePicker.displayName =
|
|
710
|
+
TimeRangePicker.displayName = "TimeRangePicker";
|
|
710
711
|
|
|
711
|
-
export { TimeRangePicker }
|
|
712
|
+
export { TimeRangePicker };
|