diff --git a/src/design-system/layout/TopBar/TopBar.module.css b/src/design-system/layout/TopBar/TopBar.module.css
index fd52c21..59e4e2a 100644
--- a/src/design-system/layout/TopBar/TopBar.module.css
+++ b/src/design-system/layout/TopBar/TopBar.module.css
@@ -22,7 +22,7 @@
flex-shrink: 0;
}
-/* Center search trigger */
+/* Search trigger */
.search {
display: flex;
align-items: center;
@@ -36,9 +36,9 @@
font-family: var(--font-body);
cursor: pointer;
transition: border-color 0.15s;
- min-width: 180px;
- flex: 1;
- max-width: 280px;
+ width: 200px;
+ flex-shrink: 1;
+ min-width: 120px;
text-align: left;
}
diff --git a/src/design-system/layout/TopBar/TopBar.tsx b/src/design-system/layout/TopBar/TopBar.tsx
index 3d0df84..911bf20 100644
--- a/src/design-system/layout/TopBar/TopBar.tsx
+++ b/src/design-system/layout/TopBar/TopBar.tsx
@@ -39,23 +39,7 @@ export function TopBar({
{/* Left: Breadcrumb */}
- {/* Filters: time range + status pills */}
-
-
- {STATUS_PILLS.map(({ status, label }) => (
- globalFilters.toggleStatus(status)}
- />
- ))}
-
-
- {/* Center: Search trigger */}
+ {/* Search trigger */}
+ {/* Status pills */}
+
+ {STATUS_PILLS.map(({ status, label }) => (
+ globalFilters.toggleStatus(status)}
+ />
+ ))}
+
+
+ {/* Time range pills */}
+
+
{/* Right: env badge, user */}
{environment && (
diff --git a/src/design-system/primitives/ButtonGroup/ButtonGroup.module.css b/src/design-system/primitives/ButtonGroup/ButtonGroup.module.css
new file mode 100644
index 0000000..bc9e497
--- /dev/null
+++ b/src/design-system/primitives/ButtonGroup/ButtonGroup.module.css
@@ -0,0 +1,60 @@
+.group {
+ display: inline-flex;
+ isolation: isolate;
+}
+
+/* Horizontal (default) */
+.horizontal {
+ flex-direction: row;
+}
+
+.horizontal > :global(*) {
+ border-radius: 0;
+ margin-left: -1px;
+ position: relative;
+}
+
+.horizontal > :global(*:first-child) {
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+ margin-left: 0;
+}
+
+.horizontal > :global(*:last-child) {
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+}
+
+.horizontal > :global(*:only-child) {
+ border-radius: var(--radius-sm);
+}
+
+/* Vertical */
+.vertical {
+ flex-direction: column;
+}
+
+.vertical > :global(*) {
+ border-radius: 0;
+ margin-top: -1px;
+ position: relative;
+}
+
+.vertical > :global(*:first-child) {
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ margin-top: 0;
+}
+
+.vertical > :global(*:last-child) {
+ border-radius: 0 0 var(--radius-sm) var(--radius-sm);
+}
+
+.vertical > :global(*:only-child) {
+ border-radius: var(--radius-sm);
+}
+
+/* Active/hovered items sit above siblings so their borders win */
+.group > :global(*:hover),
+.group > :global(*:focus-visible),
+.group > :global(*[data-active="true"]),
+.group > :global(*.active) {
+ z-index: 1;
+}
diff --git a/src/design-system/primitives/ButtonGroup/ButtonGroup.tsx b/src/design-system/primitives/ButtonGroup/ButtonGroup.tsx
new file mode 100644
index 0000000..98e73dc
--- /dev/null
+++ b/src/design-system/primitives/ButtonGroup/ButtonGroup.tsx
@@ -0,0 +1,23 @@
+import { type ReactNode } from 'react'
+import styles from './ButtonGroup.module.css'
+
+interface ButtonGroupProps {
+ children: ReactNode
+ orientation?: 'horizontal' | 'vertical'
+ className?: string
+}
+
+export function ButtonGroup({
+ children,
+ orientation = 'horizontal',
+ className,
+}: ButtonGroupProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/design-system/primitives/FilterPill/FilterPill.tsx b/src/design-system/primitives/FilterPill/FilterPill.tsx
index 90173f6..8769073 100644
--- a/src/design-system/primitives/FilterPill/FilterPill.tsx
+++ b/src/design-system/primitives/FilterPill/FilterPill.tsx
@@ -1,3 +1,4 @@
+import { forwardRef } from 'react'
import styles from './FilterPill.module.css'
interface FilterPillProps {
@@ -10,33 +11,37 @@ interface FilterPillProps {
className?: string
}
-export function FilterPill({
- label,
- count,
- active = false,
- dot = false,
- dotColor,
- onClick,
- className,
-}: FilterPillProps) {
- const classes = [
- styles.pill,
- active ? styles.active : '',
- className ?? '',
- ].filter(Boolean).join(' ')
+export const FilterPill = forwardRef
(
+ ({
+ label,
+ count,
+ active = false,
+ dot = false,
+ dotColor,
+ onClick,
+ className,
+ }, ref) => {
+ const classes = [
+ styles.pill,
+ active ? styles.active : '',
+ className ?? '',
+ ].filter(Boolean).join(' ')
- return (
-
- )
-}
+ return (
+
+ )
+ },
+)
+
+FilterPill.displayName = 'FilterPill'
diff --git a/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.module.css b/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.module.css
index 5f7a8f2..b1c9a98 100644
--- a/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.module.css
+++ b/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.module.css
@@ -1,45 +1,78 @@
-.trigger {
- display: flex;
+/* ── Integrated readout cell ──────────────────────────────
+ First child of the ButtonGroup — styled as a recessed
+ instrument-panel display, not a clickable control. */
+
+.readout {
+ display: inline-flex;
align-items: center;
- gap: 4px;
- padding: 4px 10px;
+ padding: 4px 12px;
height: 28px;
border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- background: var(--bg-raised);
- color: var(--amber, var(--warning));
+ background: var(--bg-inset);
+ box-shadow: inset 0 1px 3px rgba(44, 37, 32, 0.06);
font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0.02em;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ cursor: default;
+ user-select: none;
+}
+
+[data-theme="dark"] .readout {
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+/* ── Custom date picker panel ────────────────────────── */
+
+.panel {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 12px;
+ min-width: 220px;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ z-index: 500;
+ animation: panelIn 150ms ease-out;
+}
+
+@keyframes panelIn {
+ from {
+ opacity: 0;
+ transform: scale(0.97);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.applyBtn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 5px 14px;
+ border: none;
+ border-radius: var(--radius-sm);
+ background: var(--amber);
+ color: #fff;
+ font-family: var(--font-body);
font-size: 12px;
font-weight: 600;
cursor: pointer;
- transition: border-color 0.15s, background 0.15s;
- white-space: nowrap;
+ transition: opacity 0.15s;
}
-.trigger:hover {
- border-color: var(--text-faint);
- background: var(--bg-surface);
+.applyBtn:hover {
+ opacity: 0.85;
}
-.icon {
- font-size: 13px;
- line-height: 1;
-}
-
-.label {
- line-height: 1;
-}
-
-.caret {
- font-size: 9px;
- opacity: 0.7;
- line-height: 1;
-}
-
-.presetList {
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 8px;
- min-width: 100px;
+.applyBtn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
}
diff --git a/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.tsx b/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.tsx
index b3fa356..46466f8 100644
--- a/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.tsx
+++ b/src/design-system/primitives/TimeRangeDropdown/TimeRangeDropdown.tsx
@@ -1,15 +1,46 @@
+import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
+import { createPortal } from 'react-dom'
import styles from './TimeRangeDropdown.module.css'
-import { Popover } from '../../composites/Popover/Popover'
import { FilterPill } from '../FilterPill/FilterPill'
-import { computePresetRange, PRESET_SHORT_LABELS } from '../../utils/timePresets'
+import { ButtonGroup } from '../ButtonGroup/ButtonGroup'
+import { DateTimePicker } from '../DateTimePicker/DateTimePicker'
+import { computePresetRange } from '../../utils/timePresets'
import type { TimeRange } from '../../providers/GlobalFilterProvider'
-const DROPDOWN_PRESETS = [
+function formatRangeLabel(range: TimeRange): string {
+ const start = range.preset ? computePresetRange(range.preset).start : range.start
+
+ const time = (d: Date) =>
+ d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', hour12: false })
+ const dateTime = (d: Date) =>
+ d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + '\u2009' + time(d)
+
+ // Preset ranges are open-ended ("since X"), so only show the start
+ if (range.preset) {
+ const now = new Date()
+ const sameDay =
+ start.getFullYear() === now.getFullYear() &&
+ start.getMonth() === now.getMonth() &&
+ start.getDate() === now.getDate()
+ return sameDay ? `${time(start)}\u2009\u2013\u2009now` : `${dateTime(start)}\u2009\u2013\u2009now`
+ }
+
+ // Custom range: show both ends
+ const end = range.end
+ const sameDay =
+ start.getFullYear() === end.getFullYear() &&
+ start.getMonth() === end.getMonth() &&
+ start.getDate() === end.getDate()
+
+ if (sameDay) return `${time(start)}\u2009\u2013\u2009${time(end)}`
+ return `${dateTime(start)}\u2009\u2013\u2009${dateTime(end)}`
+}
+
+const PRESETS = [
{ value: 'last-1h', label: '1h' },
{ value: 'last-3h', label: '3h' },
{ value: 'last-6h', label: '6h' },
{ value: 'today', label: 'Today' },
- { value: 'shift', label: 'Shift' },
{ value: 'last-24h', label: '24h' },
{ value: 'last-7d', label: '7d' },
]
@@ -21,35 +52,112 @@ interface TimeRangeDropdownProps {
}
export function TimeRangeDropdown({ value, onChange, className }: TimeRangeDropdownProps) {
- const activeLabel = value.preset ? (PRESET_SHORT_LABELS[value.preset] ?? value.preset) : 'Custom'
+ const [open, setOpen] = useState(false)
+ const [customFrom, setCustomFrom] = useState(value.start)
+ const [customTo, setCustomTo] = useState(value.end)
+ const customRef = useRef(null)
+ const panelRef = useRef(null)
+ const [panelPos, setPanelPos] = useState({ top: 0, left: 0 })
+ const isCustom = value.preset === null || value.preset === 'custom'
+
+ const reposition = useCallback(() => {
+ if (!customRef.current) return
+ const rect = customRef.current.getBoundingClientRect()
+ const panelWidth = panelRef.current?.offsetWidth ?? 240
+ setPanelPos({
+ top: rect.bottom + window.scrollY + 8,
+ left: rect.right + window.scrollX - panelWidth,
+ })
+ }, [])
+
+ useEffect(() => {
+ if (open) {
+ const id = requestAnimationFrame(reposition)
+ return () => cancelAnimationFrame(id)
+ }
+ }, [open, reposition])
+
+ useEffect(() => {
+ if (!open) return
+ function handleMouseDown(e: MouseEvent) {
+ if (
+ customRef.current?.contains(e.target as Node) ||
+ panelRef.current?.contains(e.target as Node)
+ ) return
+ setOpen(false)
+ }
+ function handleKey(e: KeyboardEvent) {
+ if (e.key === 'Escape') setOpen(false)
+ }
+ document.addEventListener('mousedown', handleMouseDown)
+ document.addEventListener('keydown', handleKey)
+ return () => {
+ document.removeEventListener('mousedown', handleMouseDown)
+ document.removeEventListener('keydown', handleKey)
+ }
+ }, [open])
+
+ const rangeLabel = useMemo(() => formatRangeLabel(value), [value])
return (
-
- ⏱
- {activeLabel}
- ▾
-
- }
- content={
-
- {DROPDOWN_PRESETS.map((preset) => (
- {
- const range = computePresetRange(preset.value)
- onChange({ ...range, preset: preset.value })
- }}
- />
- ))}
-
- }
- />
+ <>
+
+ {PRESETS.map((preset) => (
+ {
+ setOpen(false)
+ const range = computePresetRange(preset.value)
+ onChange({ ...range, preset: preset.value })
+ }}
+ />
+ ))}
+ setOpen((prev) => !prev)}
+ />
+
+ {rangeLabel}
+
+
+
+ {open && createPortal(
+
+ setCustomFrom(d)}
+ />
+ setCustomTo(d)}
+ />
+
+
,
+ document.body,
+ )}
+ >
)
}
diff --git a/src/design-system/primitives/index.ts b/src/design-system/primitives/index.ts
index 97aed32..5c3e4c7 100644
--- a/src/design-system/primitives/index.ts
+++ b/src/design-system/primitives/index.ts
@@ -2,6 +2,7 @@ export { Alert } from './Alert/Alert'
export { Avatar } from './Avatar/Avatar'
export { Badge } from './Badge/Badge'
export { Button } from './Button/Button'
+export { ButtonGroup } from './ButtonGroup/ButtonGroup'
export { Card } from './Card/Card'
export { Checkbox } from './Checkbox/Checkbox'
export { CodeBlock } from './CodeBlock/CodeBlock'
diff --git a/src/design-system/providers/GlobalFilterProvider.tsx b/src/design-system/providers/GlobalFilterProvider.tsx
index 024db38..ce52388 100644
--- a/src/design-system/providers/GlobalFilterProvider.tsx
+++ b/src/design-system/providers/GlobalFilterProvider.tsx
@@ -20,7 +20,7 @@ interface GlobalFilterContextValue {
const GlobalFilterContext = createContext(null)
-const DEFAULT_PRESET = 'last-3h'
+const DEFAULT_PRESET = 'last-1h'
function getDefaultTimeRange(): TimeRange {
const { start, end } = computePresetRange(DEFAULT_PRESET)