@dowel-ui/react 0.3.0 → 0.4.0
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 +9 -8
- package/dist/components/ai-disclosure/ai-disclosure.d.ts +92 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.js +122 -0
- package/dist/components/ai-disclosure/ai-disclosure.js.map +1 -0
- package/dist/components/ai-disclosure/index.d.ts +2 -0
- package/dist/components/ai-disclosure/index.js +2 -0
- package/dist/components/ai-disclosure/meta.js +17 -0
- package/dist/components/ai-disclosure/meta.js.map +1 -0
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/date-picker/date-picker.js +1 -1
- package/dist/components/diff-viewer/diff-model.d.ts +69 -0
- package/dist/components/diff-viewer/diff-model.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-model.js +180 -0
- package/dist/components/diff-viewer/diff-model.js.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts +42 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.js +212 -0
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -0
- package/dist/components/diff-viewer/index.d.ts +3 -0
- package/dist/components/diff-viewer/index.js +3 -0
- package/dist/components/diff-viewer/meta.js +17 -0
- package/dist/components/diff-viewer/meta.js.map +1 -0
- package/dist/components/log-viewer/index.d.ts +3 -0
- package/dist/components/log-viewer/index.js +3 -0
- package/dist/components/log-viewer/log-stream.d.ts +68 -0
- package/dist/components/log-viewer/log-stream.d.ts.map +1 -0
- package/dist/components/log-viewer/log-stream.js +120 -0
- package/dist/components/log-viewer/log-stream.js.map +1 -0
- package/dist/components/log-viewer/log-viewer.d.ts +51 -0
- package/dist/components/log-viewer/log-viewer.d.ts.map +1 -0
- package/dist/components/log-viewer/log-viewer.js +284 -0
- package/dist/components/log-viewer/log-viewer.js.map +1 -0
- package/dist/components/log-viewer/meta.js +17 -0
- package/dist/components/log-viewer/meta.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/time-range-picker/index.d.ts +3 -0
- package/dist/components/time-range-picker/index.js +3 -0
- package/dist/components/time-range-picker/meta.js +22 -0
- package/dist/components/time-range-picker/meta.js.map +1 -0
- package/dist/components/time-range-picker/time-expression.d.ts +88 -0
- package/dist/components/time-range-picker/time-expression.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-expression.js +335 -0
- package/dist/components/time-range-picker/time-expression.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts +65 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +377 -0
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +9 -2
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +58 -50
- package/dist/registry/components.js.map +1 -1
- package/package.json +6 -4
- package/src/components/ai-disclosure/ai-disclosure.tsx +244 -0
- package/src/components/ai-disclosure/index.ts +9 -0
- package/src/components/ai-disclosure/meta.ts +23 -0
- package/src/components/diff-viewer/diff-model.ts +256 -0
- package/src/components/diff-viewer/diff-viewer.tsx +339 -0
- package/src/components/diff-viewer/index.ts +17 -0
- package/src/components/diff-viewer/meta.ts +20 -0
- package/src/components/log-viewer/index.ts +21 -0
- package/src/components/log-viewer/log-stream.ts +176 -0
- package/src/components/log-viewer/log-viewer.tsx +424 -0
- package/src/components/log-viewer/meta.ts +21 -0
- package/src/components/time-range-picker/index.ts +25 -0
- package/src/components/time-range-picker/meta.ts +22 -0
- package/src/components/time-range-picker/time-expression.ts +491 -0
- package/src/components/time-range-picker/time-range-picker.tsx +522 -0
- package/src/index.ts +4 -0
- package/src/registry/components.ts +8 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useId,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
useSyncExternalStore,
|
|
11
|
+
type ComponentPropsWithRef,
|
|
12
|
+
type ReactNode,
|
|
13
|
+
} from "react";
|
|
14
|
+
import type { DateRange } from "react-day-picker";
|
|
15
|
+
|
|
16
|
+
import { Button } from "@/components/button";
|
|
17
|
+
import { Calendar } from "@/components/calendar";
|
|
18
|
+
import { Input } from "@/components/input";
|
|
19
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/popover";
|
|
20
|
+
import { focusRing } from "@/lib/styles";
|
|
21
|
+
import { cn } from "@/lib/utils";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_PRESETS,
|
|
25
|
+
TimeExpressionError,
|
|
26
|
+
absoluteExpression,
|
|
27
|
+
describeTimeRange,
|
|
28
|
+
formatResolvedRange,
|
|
29
|
+
resolveTimeRange,
|
|
30
|
+
type ResolvedRange,
|
|
31
|
+
type TimeRangePreset,
|
|
32
|
+
} from "./time-expression";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The control every observability product builds for itself.
|
|
36
|
+
*
|
|
37
|
+
* Grafana, Datadog, Sentry, PostHog, Vercel, Honeycomb, Cloudflare and
|
|
38
|
+
* Amplitude each maintain a bespoke one, and no React package ships it, because
|
|
39
|
+
* it looks like a date picker and is not one — see `time-expression.ts` for the
|
|
40
|
+
* value model that makes the difference.
|
|
41
|
+
*
|
|
42
|
+
* Composed rather than reimplemented: Popover supplies the dialog semantics,
|
|
43
|
+
* Calendar the absolute path, Input the expression entry.
|
|
44
|
+
*
|
|
45
|
+
* Two omissions are deliberate. No timezone picker — that is a 400-entry
|
|
46
|
+
* combobox and a decision an app makes once, so `timeZone` is a prop. And no
|
|
47
|
+
* comparison range; a second window belongs to whatever renders the chart.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
interface TimeRangeContextValue {
|
|
51
|
+
value: string;
|
|
52
|
+
setValue: (expression: string) => void;
|
|
53
|
+
now: Date;
|
|
54
|
+
timeZone?: string;
|
|
55
|
+
locale?: string;
|
|
56
|
+
presets: TimeRangePreset[];
|
|
57
|
+
resolved: ResolvedRange | null;
|
|
58
|
+
error: string | null;
|
|
59
|
+
/** False until the clock is known, which on a server render is never. */
|
|
60
|
+
clockReady: boolean;
|
|
61
|
+
close: () => void;
|
|
62
|
+
labelId: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Stands in until the clock is known. Never shown; see `clockReady`. */
|
|
66
|
+
const EPOCH = new Date(0);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The clock, for callers who did not supply one.
|
|
70
|
+
*
|
|
71
|
+
* `useSyncExternalStore` rather than an effect, because this is the case it
|
|
72
|
+
* exists for: a value the server cannot know. The server snapshot is null, so a
|
|
73
|
+
* server render and the hydration render both commit to nothing clock-dependent
|
|
74
|
+
* and cannot disagree; the real instant arrives in the re-render after.
|
|
75
|
+
*
|
|
76
|
+
* `getSnapshot` has to be referentially stable, so the instant is created once
|
|
77
|
+
* for the page. That means every picker without a `now` prop shares it and none
|
|
78
|
+
* of them advance — which is the whole reason the prop is there.
|
|
79
|
+
*/
|
|
80
|
+
const subscribeToNothing = () => () => undefined;
|
|
81
|
+
let sharedClock: Date | null = null;
|
|
82
|
+
const readSharedClock = () => (sharedClock ??= new Date());
|
|
83
|
+
const readNoClock = () => null;
|
|
84
|
+
|
|
85
|
+
const TimeRangeContext = createContext<TimeRangeContextValue | null>(null);
|
|
86
|
+
|
|
87
|
+
function useTimeRange(component: string): TimeRangeContextValue {
|
|
88
|
+
const context = useContext(TimeRangeContext);
|
|
89
|
+
if (!context) {
|
|
90
|
+
throw new Error(`${component} must be used inside <TimeRange>.`);
|
|
91
|
+
}
|
|
92
|
+
return context;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TimeRangeProps {
|
|
96
|
+
/** The expression, e.g. `now-6h..now`. Controlled. */
|
|
97
|
+
value?: string;
|
|
98
|
+
defaultValue?: string;
|
|
99
|
+
onValueChange?: (expression: string) => void;
|
|
100
|
+
/**
|
|
101
|
+
* The instant relative expressions are measured from. Supplied by the
|
|
102
|
+
* consumer so nothing here reads the clock during render. Pass a value that
|
|
103
|
+
* changes when you refresh; a stable one holds the window still between them.
|
|
104
|
+
*/
|
|
105
|
+
now?: Date;
|
|
106
|
+
/** IANA zone the snapping happens in. Defaults to the runtime zone. */
|
|
107
|
+
timeZone?: string;
|
|
108
|
+
locale?: string;
|
|
109
|
+
presets?: TimeRangePreset[];
|
|
110
|
+
open?: boolean;
|
|
111
|
+
onOpenChange?: (open: boolean) => void;
|
|
112
|
+
children: ReactNode;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function TimeRange({
|
|
116
|
+
value: valueProp,
|
|
117
|
+
defaultValue = "now-6h..now",
|
|
118
|
+
onValueChange,
|
|
119
|
+
now,
|
|
120
|
+
timeZone,
|
|
121
|
+
locale,
|
|
122
|
+
presets = DEFAULT_PRESETS,
|
|
123
|
+
open: openProp,
|
|
124
|
+
onOpenChange,
|
|
125
|
+
children,
|
|
126
|
+
}: TimeRangeProps) {
|
|
127
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
128
|
+
const value = valueProp ?? uncontrolledValue;
|
|
129
|
+
|
|
130
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
131
|
+
const open = openProp ?? uncontrolledOpen;
|
|
132
|
+
|
|
133
|
+
const labelId = useId();
|
|
134
|
+
|
|
135
|
+
// Reading the clock during render is what the `now` prop exists to avoid: the
|
|
136
|
+
// server would render one instant and hydration another, and React would
|
|
137
|
+
// throw out the tree. Until the fallback arrives, nothing clock-dependent is
|
|
138
|
+
// rendered — the relative label, "Last 6 hours", does not need one, which is
|
|
139
|
+
// why the trigger still says something useful in the meantime.
|
|
140
|
+
const fallbackClock = useSyncExternalStore(subscribeToNothing, readSharedClock, readNoClock);
|
|
141
|
+
|
|
142
|
+
const clock = now ?? fallbackClock;
|
|
143
|
+
// A placeholder only for expressions that never mention `now`; anything that
|
|
144
|
+
// does is gated on `clock` below rather than resolved against a fake instant.
|
|
145
|
+
const effectiveNow = clock ?? EPOCH;
|
|
146
|
+
|
|
147
|
+
const setOpen = useCallback(
|
|
148
|
+
(next: boolean) => {
|
|
149
|
+
if (openProp === undefined) setUncontrolledOpen(next);
|
|
150
|
+
onOpenChange?.(next);
|
|
151
|
+
},
|
|
152
|
+
[openProp, onOpenChange],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const setValue = useCallback(
|
|
156
|
+
(expression: string) => {
|
|
157
|
+
if (valueProp === undefined) setUncontrolledValue(expression);
|
|
158
|
+
onValueChange?.(expression);
|
|
159
|
+
},
|
|
160
|
+
[valueProp, onValueChange],
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const { resolved, error } = useMemo(() => {
|
|
164
|
+
// Claiming neither a window nor an error before the clock is known: an
|
|
165
|
+
// expression cannot be judged against an instant nobody has read yet.
|
|
166
|
+
if (!clock) return { resolved: null, error: null };
|
|
167
|
+
try {
|
|
168
|
+
return { resolved: resolveTimeRange(value, { now: clock, timeZone }), error: null };
|
|
169
|
+
} catch (thrown) {
|
|
170
|
+
return {
|
|
171
|
+
resolved: null,
|
|
172
|
+
error: thrown instanceof TimeExpressionError ? thrown.message : "Invalid time range.",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}, [value, clock, timeZone]);
|
|
176
|
+
|
|
177
|
+
const context = useMemo<TimeRangeContextValue>(
|
|
178
|
+
() => ({
|
|
179
|
+
value,
|
|
180
|
+
setValue,
|
|
181
|
+
now: effectiveNow,
|
|
182
|
+
timeZone,
|
|
183
|
+
locale,
|
|
184
|
+
presets,
|
|
185
|
+
resolved,
|
|
186
|
+
error,
|
|
187
|
+
clockReady: clock !== null,
|
|
188
|
+
close: () => {
|
|
189
|
+
setOpen(false);
|
|
190
|
+
},
|
|
191
|
+
labelId,
|
|
192
|
+
}),
|
|
193
|
+
[
|
|
194
|
+
value,
|
|
195
|
+
setValue,
|
|
196
|
+
effectiveNow,
|
|
197
|
+
clock,
|
|
198
|
+
timeZone,
|
|
199
|
+
locale,
|
|
200
|
+
presets,
|
|
201
|
+
resolved,
|
|
202
|
+
error,
|
|
203
|
+
setOpen,
|
|
204
|
+
labelId,
|
|
205
|
+
],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<TimeRangeContext.Provider value={context}>
|
|
210
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
211
|
+
{children}
|
|
212
|
+
</Popover>
|
|
213
|
+
</TimeRangeContext.Provider>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ClockIcon() {
|
|
218
|
+
return (
|
|
219
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="size-4 opacity-70">
|
|
220
|
+
<circle cx="12" cy="12" r="9" stroke="currentColor" strokeWidth="2" />
|
|
221
|
+
<path d="M12 7v5l3 2" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
222
|
+
</svg>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface TimeRangeTriggerProps extends ComponentPropsWithRef<"button"> {
|
|
227
|
+
/** Shows the resolved window under the label. Off by default; it is long. */
|
|
228
|
+
showResolved?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The button, named by the range it holds: "Last 6 hours", not two timestamps.
|
|
233
|
+
* The resolved window stays reachable as the title, and as `showResolved`.
|
|
234
|
+
*/
|
|
235
|
+
export function TimeRangeTrigger({
|
|
236
|
+
className,
|
|
237
|
+
showResolved = false,
|
|
238
|
+
...props
|
|
239
|
+
}: TimeRangeTriggerProps) {
|
|
240
|
+
const { value, now, timeZone, locale, presets, resolved, error } =
|
|
241
|
+
useTimeRange("TimeRangeTrigger");
|
|
242
|
+
|
|
243
|
+
const label = describeTimeRange(value, { now, timeZone, locale, presets });
|
|
244
|
+
const resolvedText = resolved ? formatResolvedRange(resolved, { locale, timeZone }) : null;
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<PopoverTrigger asChild>
|
|
248
|
+
<Button
|
|
249
|
+
data-slot="time-range-trigger"
|
|
250
|
+
variant="outline"
|
|
251
|
+
aria-invalid={error ? true : undefined}
|
|
252
|
+
title={resolvedText ?? undefined}
|
|
253
|
+
className={cn("justify-start gap-2 font-normal", className)}
|
|
254
|
+
{...props}
|
|
255
|
+
>
|
|
256
|
+
<ClockIcon />
|
|
257
|
+
<span className="flex flex-col items-start leading-tight">
|
|
258
|
+
<span>{label}</span>
|
|
259
|
+
{showResolved && resolvedText ? (
|
|
260
|
+
<span className="text-2xs font-normal text-muted-foreground">{resolvedText}</span>
|
|
261
|
+
) : null}
|
|
262
|
+
</span>
|
|
263
|
+
{/* The window the label stands for, for anyone who cannot see the
|
|
264
|
+
title attribute. Read after the label, not instead of it. */}
|
|
265
|
+
{resolvedText && !showResolved ? (
|
|
266
|
+
<span className="sr-only">{`, ${resolvedText}`}</span>
|
|
267
|
+
) : null}
|
|
268
|
+
</Button>
|
|
269
|
+
</PopoverTrigger>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface TimeRangeContentProps extends ComponentPropsWithRef<typeof PopoverContent> {
|
|
274
|
+
/** Names the dialog. */
|
|
275
|
+
label?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function TimeRangeContent({
|
|
279
|
+
className,
|
|
280
|
+
label = "Choose a time range",
|
|
281
|
+
children,
|
|
282
|
+
...props
|
|
283
|
+
}: TimeRangeContentProps) {
|
|
284
|
+
const { labelId } = useTimeRange("TimeRangeContent");
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<PopoverContent
|
|
288
|
+
data-slot="time-range-content"
|
|
289
|
+
aria-labelledby={labelId}
|
|
290
|
+
align="start"
|
|
291
|
+
className={cn("w-auto max-w-[min(38rem,calc(100vw-2rem))] p-0", className)}
|
|
292
|
+
{...props}
|
|
293
|
+
>
|
|
294
|
+
<h2 id={labelId} className="sr-only">
|
|
295
|
+
{label}
|
|
296
|
+
</h2>
|
|
297
|
+
<div className="flex flex-col gap-0 sm:flex-row sm:items-stretch">{children}</div>
|
|
298
|
+
</PopoverContent>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The named ranges, which is how most of these are actually used. Buttons
|
|
304
|
+
* rather than a radiogroup: choosing one both selects and dismisses, which is a
|
|
305
|
+
* command, and `aria-pressed` still carries which one matches.
|
|
306
|
+
*/
|
|
307
|
+
export function TimeRangePresets({
|
|
308
|
+
className,
|
|
309
|
+
...props
|
|
310
|
+
}: Omit<ComponentPropsWithRef<"div">, "children">) {
|
|
311
|
+
const { presets, value, setValue, close } = useTimeRange("TimeRangePresets");
|
|
312
|
+
|
|
313
|
+
const groups = useMemo(() => {
|
|
314
|
+
const collected = new Map<string, TimeRangePreset[]>();
|
|
315
|
+
for (const preset of presets) {
|
|
316
|
+
const existing = collected.get(preset.group);
|
|
317
|
+
if (existing) existing.push(preset);
|
|
318
|
+
else collected.set(preset.group, [preset]);
|
|
319
|
+
}
|
|
320
|
+
return [...collected];
|
|
321
|
+
}, [presets]);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
data-slot="time-range-presets"
|
|
326
|
+
className={cn(
|
|
327
|
+
"max-h-80 min-w-52 overflow-y-auto border-border p-1.5 sm:border-r",
|
|
328
|
+
className,
|
|
329
|
+
)}
|
|
330
|
+
{...props}
|
|
331
|
+
>
|
|
332
|
+
{groups.map(([group, items]) => (
|
|
333
|
+
<div key={group} role="group" aria-label={group} className="pb-1 last:pb-0">
|
|
334
|
+
<p className="px-2 py-1 text-2xs font-medium tracking-wide text-muted-foreground uppercase">
|
|
335
|
+
{group}
|
|
336
|
+
</p>
|
|
337
|
+
{items.map((preset) => (
|
|
338
|
+
<button
|
|
339
|
+
key={preset.expression}
|
|
340
|
+
type="button"
|
|
341
|
+
aria-pressed={preset.expression === value}
|
|
342
|
+
onClick={() => {
|
|
343
|
+
setValue(preset.expression);
|
|
344
|
+
close();
|
|
345
|
+
}}
|
|
346
|
+
className={cn(
|
|
347
|
+
"flex w-full items-center rounded-md px-2 py-1.5 text-left text-sm transition-colors",
|
|
348
|
+
preset.expression === value
|
|
349
|
+
? "bg-accent font-medium text-accent-foreground"
|
|
350
|
+
: "hover:bg-accent hover:text-accent-foreground",
|
|
351
|
+
focusRing,
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
{preset.label}
|
|
355
|
+
</button>
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Raw expression entry, with the resolved window shown as you type — the escape
|
|
365
|
+
* hatch that keeps the presets from being a ceiling.
|
|
366
|
+
*
|
|
367
|
+
* An expression that does not parse says why and is not applied: a chart
|
|
368
|
+
* quietly re-scoping itself to a window nobody asked for is worse than one that
|
|
369
|
+
* refuses.
|
|
370
|
+
*/
|
|
371
|
+
export function TimeRangeExpression({
|
|
372
|
+
className,
|
|
373
|
+
...props
|
|
374
|
+
}: Omit<ComponentPropsWithRef<"div">, "children">) {
|
|
375
|
+
const { value, setValue, now, timeZone, locale, clockReady, close } =
|
|
376
|
+
useTimeRange("TimeRangeExpression");
|
|
377
|
+
|
|
378
|
+
const [draft, setDraft] = useState(value);
|
|
379
|
+
// The expression can change under the input — a preset click, or the
|
|
380
|
+
// consumer setting it. Adjusting during render rather than in an effect keeps
|
|
381
|
+
// the field from showing a stale value for a frame.
|
|
382
|
+
const [lastValue, setLastValue] = useState(value);
|
|
383
|
+
if (value !== lastValue) {
|
|
384
|
+
setLastValue(value);
|
|
385
|
+
setDraft(value);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const feedbackId = useId();
|
|
389
|
+
const preview = useMemo(() => {
|
|
390
|
+
// Before the clock is known there is nothing to preview and nothing to
|
|
391
|
+
// reject — saying "invalid" here would be a verdict reached without a fact.
|
|
392
|
+
if (!clockReady) return { text: null, error: null as string | null };
|
|
393
|
+
try {
|
|
394
|
+
return {
|
|
395
|
+
text: formatResolvedRange(resolveTimeRange(draft, { now, timeZone }), {
|
|
396
|
+
locale,
|
|
397
|
+
timeZone,
|
|
398
|
+
}),
|
|
399
|
+
error: null as string | null,
|
|
400
|
+
};
|
|
401
|
+
} catch (thrown) {
|
|
402
|
+
return {
|
|
403
|
+
text: null,
|
|
404
|
+
error: thrown instanceof TimeExpressionError ? thrown.message : "Invalid time range.",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}, [draft, now, timeZone, locale, clockReady]);
|
|
408
|
+
|
|
409
|
+
const apply = () => {
|
|
410
|
+
if (preview.error || !clockReady) return;
|
|
411
|
+
setValue(draft.trim());
|
|
412
|
+
close();
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<div
|
|
417
|
+
data-slot="time-range-expression"
|
|
418
|
+
className={cn(
|
|
419
|
+
"flex flex-col gap-1.5 border-t border-border p-3 sm:border-t-0",
|
|
420
|
+
className,
|
|
421
|
+
)}
|
|
422
|
+
{...props}
|
|
423
|
+
>
|
|
424
|
+
<label htmlFor={feedbackId + "-input"} className="text-xs font-medium">
|
|
425
|
+
Expression
|
|
426
|
+
</label>
|
|
427
|
+
<div className="flex gap-2">
|
|
428
|
+
<Input
|
|
429
|
+
id={feedbackId + "-input"}
|
|
430
|
+
inputSize="sm"
|
|
431
|
+
value={draft}
|
|
432
|
+
spellCheck={false}
|
|
433
|
+
autoComplete="off"
|
|
434
|
+
aria-invalid={preview.error ? true : undefined}
|
|
435
|
+
aria-describedby={feedbackId}
|
|
436
|
+
onChange={(event) => {
|
|
437
|
+
setDraft(event.target.value);
|
|
438
|
+
}}
|
|
439
|
+
onKeyDown={(event) => {
|
|
440
|
+
if (event.key === "Enter") {
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
apply();
|
|
443
|
+
}
|
|
444
|
+
}}
|
|
445
|
+
className="font-mono"
|
|
446
|
+
/>
|
|
447
|
+
<Button size="sm" onClick={apply} disabled={Boolean(preview.error) || !clockReady}>
|
|
448
|
+
Apply
|
|
449
|
+
</Button>
|
|
450
|
+
</div>
|
|
451
|
+
{/* One element for both outcomes, so a screen reader hears the error
|
|
452
|
+
replace the preview rather than accumulating two descriptions. */}
|
|
453
|
+
<p
|
|
454
|
+
id={feedbackId}
|
|
455
|
+
className={cn(
|
|
456
|
+
"min-h-4 text-2xs",
|
|
457
|
+
preview.error ? "text-destructive" : "text-muted-foreground",
|
|
458
|
+
)}
|
|
459
|
+
>
|
|
460
|
+
{preview.error ?? preview.text}
|
|
461
|
+
</p>
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The absolute path, for a window with no relative meaning. Picking dates
|
|
468
|
+
* writes a concrete expression: "1 to 7 March" does not become relative just
|
|
469
|
+
* because it was chosen here.
|
|
470
|
+
*/
|
|
471
|
+
export function TimeRangeCalendar({
|
|
472
|
+
className,
|
|
473
|
+
numberOfMonths = 1,
|
|
474
|
+
...props
|
|
475
|
+
}: Omit<ComponentPropsWithRef<"div">, "children"> & { numberOfMonths?: number }) {
|
|
476
|
+
const { setValue, resolved, close } = useTimeRange("TimeRangeCalendar");
|
|
477
|
+
|
|
478
|
+
const [selection, setSelection] = useState<DateRange | undefined>(() =>
|
|
479
|
+
resolved ? { from: resolved.from, to: resolved.to } : undefined,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div
|
|
484
|
+
data-slot="time-range-calendar"
|
|
485
|
+
className={cn("border-t border-border p-1 sm:border-t-0 sm:border-l", className)}
|
|
486
|
+
{...props}
|
|
487
|
+
>
|
|
488
|
+
<Calendar
|
|
489
|
+
mode="range"
|
|
490
|
+
selected={selection}
|
|
491
|
+
defaultMonth={selection?.from}
|
|
492
|
+
numberOfMonths={numberOfMonths}
|
|
493
|
+
onSelect={(next) => {
|
|
494
|
+
setSelection(next);
|
|
495
|
+
// Only once both ends exist. One click is half an answer, and
|
|
496
|
+
// applying it would show a zero-length range mid-gesture.
|
|
497
|
+
if (next?.from && next.to) {
|
|
498
|
+
setValue(absoluteExpression(startOfDay(next.from), endOfDay(next.to)));
|
|
499
|
+
close();
|
|
500
|
+
}
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function startOfDay(date: Date): Date {
|
|
508
|
+
const start = new Date(date);
|
|
509
|
+
start.setHours(0, 0, 0, 0);
|
|
510
|
+
return start;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* A day picked as the end of a range means the whole of that day. Taking
|
|
515
|
+
* midnight instead silently drops the last day's data, which is the same
|
|
516
|
+
* off-by-one that `now/d..now/d` has to avoid.
|
|
517
|
+
*/
|
|
518
|
+
function endOfDay(date: Date): Date {
|
|
519
|
+
const end = new Date(date);
|
|
520
|
+
end.setHours(23, 59, 59, 999);
|
|
521
|
+
return end;
|
|
522
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./components/ai-agent-plan";
|
|
|
7
7
|
export * from "./components/ai-agent-status";
|
|
8
8
|
export * from "./components/ai-approval-request";
|
|
9
9
|
export * from "./components/ai-conversation";
|
|
10
|
+
export * from "./components/ai-disclosure";
|
|
10
11
|
export * from "./components/ai-message";
|
|
11
12
|
export * from "./components/ai-model-selector";
|
|
12
13
|
export * from "./components/ai-prompt-input";
|
|
@@ -28,6 +29,7 @@ export * from "./components/command";
|
|
|
28
29
|
export * from "./components/data-table";
|
|
29
30
|
export * from "./components/date-picker";
|
|
30
31
|
export * from "./components/dialog";
|
|
32
|
+
export * from "./components/diff-viewer";
|
|
31
33
|
export * from "./components/drawer";
|
|
32
34
|
export * from "./components/dropdown-menu";
|
|
33
35
|
export * from "./components/empty-state";
|
|
@@ -35,6 +37,7 @@ export * from "./components/file-upload";
|
|
|
35
37
|
export * from "./components/form";
|
|
36
38
|
export * from "./components/input";
|
|
37
39
|
export * from "./components/label";
|
|
40
|
+
export * from "./components/log-viewer";
|
|
38
41
|
export * from "./components/meter";
|
|
39
42
|
export * from "./components/metric-delta";
|
|
40
43
|
export * from "./components/record-diff";
|
|
@@ -52,6 +55,7 @@ export * from "./components/switch";
|
|
|
52
55
|
export * from "./components/table";
|
|
53
56
|
export * from "./components/tabs";
|
|
54
57
|
export * from "./components/tags-input";
|
|
58
|
+
export * from "./components/time-range-picker";
|
|
55
59
|
export * from "./components/toast";
|
|
56
60
|
export * from "./components/tooltip";
|
|
57
61
|
|
|
@@ -7,6 +7,7 @@ import { meta as aiAgentPlanMeta } from "@/components/ai-agent-plan/meta";
|
|
|
7
7
|
import { meta as aiAgentStatusMeta } from "@/components/ai-agent-status/meta";
|
|
8
8
|
import { meta as aiApprovalRequestMeta } from "@/components/ai-approval-request/meta";
|
|
9
9
|
import { meta as aiConversationMeta } from "@/components/ai-conversation/meta";
|
|
10
|
+
import { meta as aiDisclosureMeta } from "@/components/ai-disclosure/meta";
|
|
10
11
|
import { meta as aiInlineCompletionMeta } from "@/components/ai-inline-completion/meta";
|
|
11
12
|
import { meta as aiMessageMeta } from "@/components/ai-message/meta";
|
|
12
13
|
import { meta as aiModelSelectorMeta } from "@/components/ai-model-selector/meta";
|
|
@@ -30,6 +31,7 @@ import { meta as commandMeta } from "@/components/command/meta";
|
|
|
30
31
|
import { meta as dataTableMeta } from "@/components/data-table/meta";
|
|
31
32
|
import { meta as datePickerMeta } from "@/components/date-picker/meta";
|
|
32
33
|
import { meta as dialogMeta } from "@/components/dialog/meta";
|
|
34
|
+
import { meta as diffViewerMeta } from "@/components/diff-viewer/meta";
|
|
33
35
|
import { meta as drawerMeta } from "@/components/drawer/meta";
|
|
34
36
|
import { meta as dropdownMenuMeta } from "@/components/dropdown-menu/meta";
|
|
35
37
|
import { meta as emptyStateMeta } from "@/components/empty-state/meta";
|
|
@@ -37,6 +39,7 @@ import { meta as fileUploadMeta } from "@/components/file-upload/meta";
|
|
|
37
39
|
import { meta as formMeta } from "@/components/form/meta";
|
|
38
40
|
import { meta as inputMeta } from "@/components/input/meta";
|
|
39
41
|
import { meta as labelMeta } from "@/components/label/meta";
|
|
42
|
+
import { meta as logViewerMeta } from "@/components/log-viewer/meta";
|
|
40
43
|
import { meta as meterMeta } from "@/components/meter/meta";
|
|
41
44
|
import { meta as metricDeltaMeta } from "@/components/metric-delta/meta";
|
|
42
45
|
import { meta as paginationMeta } from "@/components/pagination/meta";
|
|
@@ -54,6 +57,7 @@ import { meta as switchMeta } from "@/components/switch/meta";
|
|
|
54
57
|
import { meta as tableMeta } from "@/components/table/meta";
|
|
55
58
|
import { meta as tabsMeta } from "@/components/tabs/meta";
|
|
56
59
|
import { meta as tagsInputMeta } from "@/components/tags-input/meta";
|
|
60
|
+
import { meta as timeRangePickerMeta } from "@/components/time-range-picker/meta";
|
|
57
61
|
import { meta as toastMeta } from "@/components/toast/meta";
|
|
58
62
|
import { meta as tooltipMeta } from "@/components/tooltip/meta";
|
|
59
63
|
|
|
@@ -76,6 +80,7 @@ export const componentMetas: ComponentMeta[] = [
|
|
|
76
80
|
aiAgentStatusMeta,
|
|
77
81
|
aiApprovalRequestMeta,
|
|
78
82
|
aiConversationMeta,
|
|
83
|
+
aiDisclosureMeta,
|
|
79
84
|
aiInlineCompletionMeta,
|
|
80
85
|
aiMessageMeta,
|
|
81
86
|
aiModelSelectorMeta,
|
|
@@ -99,6 +104,7 @@ export const componentMetas: ComponentMeta[] = [
|
|
|
99
104
|
dataTableMeta,
|
|
100
105
|
datePickerMeta,
|
|
101
106
|
dialogMeta,
|
|
107
|
+
diffViewerMeta,
|
|
102
108
|
drawerMeta,
|
|
103
109
|
dropdownMenuMeta,
|
|
104
110
|
emptyStateMeta,
|
|
@@ -106,6 +112,7 @@ export const componentMetas: ComponentMeta[] = [
|
|
|
106
112
|
formMeta,
|
|
107
113
|
inputMeta,
|
|
108
114
|
labelMeta,
|
|
115
|
+
logViewerMeta,
|
|
109
116
|
meterMeta,
|
|
110
117
|
metricDeltaMeta,
|
|
111
118
|
paginationMeta,
|
|
@@ -123,6 +130,7 @@ export const componentMetas: ComponentMeta[] = [
|
|
|
123
130
|
tableMeta,
|
|
124
131
|
tabsMeta,
|
|
125
132
|
tagsInputMeta,
|
|
133
|
+
timeRangePickerMeta,
|
|
126
134
|
toastMeta,
|
|
127
135
|
tooltipMeta,
|
|
128
136
|
];
|