@reopt-ai/opt-ui 1.10.0 → 1.12.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.
Files changed (73) hide show
  1. package/COMPONENT_CATALOG.md +522 -218
  2. package/README.md +61 -7
  3. package/dist/core/index.cjs +10 -8
  4. package/dist/core/index.d.cts +3 -3
  5. package/dist/core/index.d.ts +3 -3
  6. package/dist/core/index.js +3 -3
  7. package/dist/docs/02-components/01-core.md +213 -96
  8. package/dist/docs/02-components/02-visuals.md +1 -1
  9. package/dist/docs/02-components/03-shells.md +334 -123
  10. package/dist/docs/02-components/04-surfaces.md +1 -1
  11. package/dist/docs/02-components/index.md +5 -5
  12. package/dist/docs/03-recipes/02-dashboards.md +126 -10
  13. package/dist/{field-sidebar-B6Ndr8HE.cjs → field-sidebar-BYgEmdy0.cjs} +2128 -1558
  14. package/dist/{field-sidebar-BXjvV7Cz.js → field-sidebar-BuO37l4c.js} +2111 -1561
  15. package/dist/{field-sidebar-nwcE9LC4.d.cts → field-sidebar-CrYlRZew.d.ts} +375 -154
  16. package/dist/{field-sidebar-DWfJAJlx.d.ts → field-sidebar-uXmoyvd7.d.cts} +375 -154
  17. package/dist/id-registry.cjs +124 -12
  18. package/dist/id-registry.d.cts +1 -2
  19. package/dist/id-registry.d.ts +1 -2
  20. package/dist/id-registry.js +124 -14
  21. package/dist/id-registry.json +265 -31
  22. package/dist/index.cjs +1162 -253
  23. package/dist/index.d.cts +573 -60
  24. package/dist/index.d.ts +573 -60
  25. package/dist/index.js +1134 -242
  26. package/dist/{key-pad-menu-DfZbeXhG.d.cts → key-pad-menu-CwcY_Vps.d.cts} +151 -7
  27. package/dist/{key-pad-menu-BQunU20e.js → key-pad-menu-Di2aRGbM.js} +388 -461
  28. package/dist/{key-pad-menu-D5mQcS5v.cjs → key-pad-menu-SItoHPLu.cjs} +490 -609
  29. package/dist/{key-pad-menu-BsgJMjD4.d.ts → key-pad-menu-WBJShCJn.d.ts} +152 -8
  30. package/dist/meta.cjs +1304 -168
  31. package/dist/meta.d.cts +12 -36
  32. package/dist/meta.d.ts +12 -36
  33. package/dist/meta.js +1304 -170
  34. package/dist/query.d.cts +1 -2
  35. package/dist/query.d.ts +1 -2
  36. package/dist/query.js +0 -2
  37. package/dist/shells/index.cjs +4 -1
  38. package/dist/shells/index.d.cts +2 -2
  39. package/dist/shells/index.d.ts +2 -2
  40. package/dist/shells/index.js +2 -2
  41. package/dist/{text-truncate-BGr7bB_8.d.cts → text-truncate-CQyfUZvq.d.cts} +48 -5
  42. package/dist/{text-truncate-BGr7bB_8.d.ts → text-truncate-CQyfUZvq.d.ts} +48 -5
  43. package/dist/{text-truncate-j6X3z7Pv.js → text-truncate-DSuZECy0.js} +446 -94
  44. package/dist/{text-truncate-CxdzAnhS.cjs → text-truncate-I1XLWmRa.cjs} +522 -90
  45. package/dist/theme/server.d.cts +1 -2
  46. package/dist/theme/server.d.ts +1 -2
  47. package/dist/theme/server.js +0 -2
  48. package/dist/visuals/index.js +0 -1
  49. package/package.json +4 -3
  50. package/dist/field-sidebar-BXjvV7Cz.js.map +0 -1
  51. package/dist/field-sidebar-DWfJAJlx.d.ts.map +0 -1
  52. package/dist/field-sidebar-nwcE9LC4.d.cts.map +0 -1
  53. package/dist/id-registry.d.cts.map +0 -1
  54. package/dist/id-registry.d.ts.map +0 -1
  55. package/dist/id-registry.js.map +0 -1
  56. package/dist/index.d.cts.map +0 -1
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/key-pad-menu-BQunU20e.js.map +0 -1
  60. package/dist/key-pad-menu-BsgJMjD4.d.ts.map +0 -1
  61. package/dist/key-pad-menu-DfZbeXhG.d.cts.map +0 -1
  62. package/dist/meta.d.cts.map +0 -1
  63. package/dist/meta.d.ts.map +0 -1
  64. package/dist/meta.js.map +0 -1
  65. package/dist/query.d.cts.map +0 -1
  66. package/dist/query.d.ts.map +0 -1
  67. package/dist/query.js.map +0 -1
  68. package/dist/text-truncate-BGr7bB_8.d.cts.map +0 -1
  69. package/dist/text-truncate-BGr7bB_8.d.ts.map +0 -1
  70. package/dist/text-truncate-j6X3z7Pv.js.map +0 -1
  71. package/dist/theme/server.d.cts.map +0 -1
  72. package/dist/theme/server.d.ts.map +0 -1
  73. package/dist/theme/server.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { $ as ToolbarButton$1, $t as Alert, A as FormSelectItem, At as DialogHeading$1, B as useFormStore, Bn as OPT_TEXT_LABEL, Bt as TabPanel$1, Cn as OPT_DIVIDER, Dn as OPT_DOT_YELLOW, En as OPT_DOT_RED, F as FormSwitch$1, Fn as OPT_SURFACE_POPOVER, Ft as SelectPopover$1, Gn as _objectSpread2, Gt as Composite, Hn as OPT_TEXT_SECONDARY, Ht as DisclosureContent$1, In as OPT_TABLE_ROW_ACTIVE, It as SelectRoot$1, J as PopoverContent$1, Jn as cn, Jt as CompositeRow, Kn as _objectWithoutProperties, Kt as CompositeItem, L as FormTextarea, Ln as OPT_TD, Lt as SelectTrigger$1, Mt as DialogRoot$1, Nn as OPT_ITEM_BASE, Nt as SelectItem$1, O as FormRoot$1, On as OPT_FOCUS, P as FormSubmit$1, Pn as OPT_SURFACE, Pt as SelectLabel$1, Rn as OPT_TD_MUTED, Rt as Tab$1, Sn as OPT_DISCLOSURE_TRIGGER, St as ComboboxRoot$1, T as FormRadioGroup$1, Tn as OPT_DOT_GREEN, Tt as DialogDescription$1, U as TagInput, Un as OPT_TEXT_TERTIARY, Ut as DisclosureRoot$1, Vn as OPT_TEXT_PRIMARY, Vt as TabsRoot$1, Wn as OPT_TH, Wt as DisclosureTrigger$1, X as PopoverTrigger$1, Y as PopoverRoot$1, Zt as Input, _n as OPT_BADGE_NEUTRAL, at as TooltipProvider$1, b as FormLabel$1, bn as OPT_BORDER, bt as ComboboxItem$1, ct as MenuItemCheckbox$1, dt as MenuRoot$1, en as Spinner, et as ToolbarContainer$1, fn as OPT_ACTIVE_ITEM, ft as MenuSeparator$1, gn as OPT_BADGE_GREEN, gt as ComboboxEmpty, hn as OPT_BADGE_BLUE, ht as MenubarRoot$1, i as FieldToken, it as TooltipAnchor$1, j as FormSelectPopover, jn as OPT_INPUT, jt as DialogPanel$1, k as FormSelect, kn as OPT_FOCUS_VISIBLE, l as FormCheckbox$1, lt as MenuItemRadio$1, m as FormError$1, mn as OPT_ANIMATE_ENTER_EXIT, mt as MenubarContainer$1, n as Highlight, nn as EmptyState, nt as ToolbarSeparator$1, o as Pagination, ot as MenuButtonArrow$1, pt as MenuTrigger$1, qn as Kbd, qt as CompositeProvider, rn as Button, rt as Tooltip$1, s as Checkbox, sn as StatCard, st as MenuItem$1, t as TextTruncate, tn as Badge, tt as ToolbarRoot$1, ut as MenuPopover$1, vn as OPT_BADGE_RED, w as FormRadio$1, xn as OPT_DISCLOSURE_CONTENT, xt as ComboboxPopover$1, y as FormInput$1, yn as OPT_BADGE_YELLOW, yt as ComboboxInput$1, zt as TabList$1 } from "./text-truncate-j6X3z7Pv.js";
2
+ import { $n as OPT_TEXT_LABEL, $t as CompositeProvider, A as FormRadio$1, An as OPT_BADGE_RED, B as FormSubmit$1, Bn as OPT_FOCUS, Bt as SelectLabel$1, Dn as OPT_BADGE_BLUE, Dt as ComboboxPopover$1, E as FormLabel$1, En as OPT_ANIMATE_ENTER_EXIT, Et as ComboboxItem$1, F as FormSelect, Fn as OPT_DIVIDER, Gn as OPT_ITEM_BASE, Gt as TabList$1, Ht as SelectRoot$1, I as FormSelectItem, It as DialogHeading$1, Jt as DisclosureContent$1, K as useFormStore, Kn as OPT_SURFACE, Kt as TabPanel$1, L as FormSelectPopover, Ln as OPT_DOT_GREEN, Lt as DialogPanel$1, Mn as OPT_BORDER, Nn as OPT_DISCLOSURE_CONTENT, On as OPT_BADGE_GREEN, Ot as ComboboxRoot$1, P as FormRoot$1, Pn as OPT_DISCLOSURE_TRIGGER, Qt as CompositeItem, Rn as OPT_DOT_RED, Rt as DialogRoot$1, St as ComboboxEmpty, T as FormInput$1, Tt as ComboboxInput$1, U as FormTextarea, Un as OPT_INPUT, Ut as SelectTrigger$1, V as FormSwitch$1, Vn as OPT_FOCUS_VISIBLE, Vt as SelectPopover$1, Wt as Tab$1, Xn as OPT_TD, Xt as DisclosureTrigger$1, Y as TagInput, Yn as OPT_TABLE_ROW_ACTIVE, Yt as DisclosureRoot$1, Zn as OPT_TD_MUTED, Zt as Composite, _t as MenuRoot$1, ar as _objectWithoutProperties, at as ToolbarButton$1, b as FormError$1, bt as MenubarContainer$1, c as Checkbox, cn as Badge, ct as ToolbarSeparator$1, dt as TooltipProvider$1, en as CompositeRow, er as OPT_TEXT_PRIMARY, et as PopoverContent$1, ft as MenuButtonArrow$1, gt as MenuPopover$1, h as FormCheckbox$1, hn as Skeleton, ht as MenuItemRadio$1, i as FieldToken, in as Input, ir as _objectSpread2, j as FormRadioGroup$1, jn as OPT_BADGE_YELLOW, jt as DialogDescription$1, kn as OPT_BADGE_NEUTRAL, ln as EmptyState, lt as Tooltip$1, m as formatOptTime, mn as StatCard, mt as MenuItemCheckbox$1, n as Highlight, nn as Meter$1, nr as OPT_TEXT_TERTIARY, nt as PopoverTrigger$1, on as Alert, or as Kbd, ot as ToolbarContainer$1, pt as MenuItem$1, qn as OPT_SURFACE_POPOVER, qt as TabsRoot$1, rr as OPT_TH, s as Pagination, sn as Spinner, sr as cn, st as ToolbarRoot$1, t as TextTruncate, tr as OPT_TEXT_SECONDARY, tt as PopoverRoot$1, u as OPT_UI_DEFAULT_LOCALE, un as Button, ut as TooltipAnchor$1, vt as MenuSeparator$1, wn as OPT_ACTIVE_ITEM, xt as MenubarRoot$1, yt as MenuTrigger$1, zn as OPT_DOT_YELLOW, zt as SelectItem$1 } from "./text-truncate-DSuZECy0.js";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as React from "react";
5
5
  import { Fragment as Fragment$1, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
@@ -253,7 +253,7 @@ function CommandPaletteTrigger({ commands, onSelect, labels }) {
253
253
  }
254
254
  //#endregion
255
255
  //#region src/shells/dashboard-grid.tsx
256
- const defaultLabels$18 = { ariaLabel: "대시보드 통계" };
256
+ const defaultLabels$21 = { ariaLabel: "대시보드 통계" };
257
257
  const MIN_DASHBOARD_COLUMNS = 1;
258
258
  const MAX_DASHBOARD_COLUMNS = 12;
259
259
  function normalizeDashboardColumns(columns) {
@@ -262,7 +262,7 @@ function normalizeDashboardColumns(columns) {
262
262
  }
263
263
  /** Renders the dashboard grid component. */
264
264
  function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
265
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$18), customLabels);
265
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$21), customLabels);
266
266
  const columnCount = normalizeDashboardColumns(columns);
267
267
  const rowStyle = { "--opt-dashboard-grid-columns": `repeat(${columnCount}, minmax(0, 1fr))` };
268
268
  const rows = [];
@@ -290,13 +290,13 @@ function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
290
290
  }
291
291
  //#endregion
292
292
  //#region src/shells/search-combobox.tsx
293
- const defaultLabels$17 = {
293
+ const defaultLabels$20 = {
294
294
  noResults: "결과가 없습니다.",
295
295
  loading: "Loading…"
296
296
  };
297
297
  /** Renders the search combobox component. */
298
298
  function SearchCombobox({ items, placeholder = "검색...", label = "검색", onSelect, labels: customLabels, async: isAsync = false, onSearch, debounceMs = 300 }) {
299
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$17), customLabels);
299
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$20), customLabels);
300
300
  const inputId = useId();
301
301
  const [query, setQuery] = useState("");
302
302
  const [asyncResults, setAsyncResults] = useState([]);
@@ -394,14 +394,14 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
394
394
  }
395
395
  //#endregion
396
396
  //#region src/shells/status-select.tsx
397
- const defaultLabels$16 = {
397
+ const defaultLabels$19 = {
398
398
  selectLabel: "상태",
399
399
  emptyMessage: "사용 가능한 상태 옵션이 없습니다."
400
400
  };
401
401
  /** Renders the status select component. */
402
402
  function StatusSelect({ options, value: controlledValue, onChange, label, labels: customLabels }) {
403
403
  var _options$0$value, _options$;
404
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$16), customLabels);
404
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$19), customLabels);
405
405
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
406
406
  const [internalValue, setInternalValue] = useState((_options$0$value = (_options$ = options[0]) === null || _options$ === void 0 ? void 0 : _options$.value) !== null && _options$0$value !== void 0 ? _options$0$value : "");
407
407
  const value = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalValue;
@@ -468,7 +468,7 @@ function StatusSelect({ options, value: controlledValue, onChange, label, labels
468
468
  }
469
469
  //#endregion
470
470
  //#region src/shells/app-menubar.tsx
471
- const defaultLabels$15 = { ariaLabel: "Application menu" };
471
+ const defaultLabels$18 = { ariaLabel: "Application menu" };
472
472
  function MenuSection({ menu }) {
473
473
  return /* @__PURE__ */ jsxs(MenuRoot$1, { children: [/* @__PURE__ */ jsxs(MenuTrigger$1, { children: [menu.label, /* @__PURE__ */ jsx(MenuButtonArrow$1, {})] }), /* @__PURE__ */ jsx(MenuPopover$1, { children: menu.items.map((item, i) => {
474
474
  if (item.type === "separator") return /* @__PURE__ */ jsx(MenuSeparator$1, {}, `sep-${i}`);
@@ -512,7 +512,7 @@ function MenuSection({ menu }) {
512
512
  }
513
513
  /** Renders the app menubar component. */
514
514
  function AppMenubar({ menus, labels: customLabels }) {
515
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$15), customLabels);
515
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$18), customLabels);
516
516
  if (menus.length === 0) return null;
517
517
  return /* @__PURE__ */ jsx(MenubarRoot$1, {
518
518
  "data-opt-id": "BGKHW",
@@ -551,10 +551,10 @@ const sectionStyles = {
551
551
  Templates: "text-text-tertiary"
552
552
  };
553
553
  const defaultSectionStyle = "text-text-secondary";
554
- const defaultLabels$14 = { ariaLabel: "문서 네비게이션" };
554
+ const defaultLabels$17 = { ariaLabel: "문서 네비게이션" };
555
555
  /** Renders the sidebar nav component. */
556
556
  function SidebarNav({ items, labels: customLabels }) {
557
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$14), customLabels);
557
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$17), customLabels);
558
558
  const pathname = usePathname();
559
559
  const activeItem = items.find((item) => item.href === pathname);
560
560
  const [activeId, setActiveId] = useState(activeItem ? domId(activeItem.id) : void 0);
@@ -602,7 +602,7 @@ function SidebarNav({ items, labels: customLabels }) {
602
602
  }
603
603
  //#endregion
604
604
  //#region src/shells/editor-toolbar.tsx
605
- const defaultLabels$13 = { ariaLabel: "텍스트 편집기 툴바" };
605
+ const defaultLabels$16 = { ariaLabel: "텍스트 편집기 툴바" };
606
606
  function ToolbarButtonWithTooltip({ button, active, onAction }) {
607
607
  return /* @__PURE__ */ jsxs(TooltipProvider$1, { children: [/* @__PURE__ */ jsx(TooltipAnchor$1, { children: /* @__PURE__ */ jsx(ToolbarButton$1, {
608
608
  className: `text-text-primary data-[active-item]:bg-bg-subtle data-[focus-visible]:ring-ring rounded-md p-2 transition-colors outline-none data-[focus-visible]:ring-2 ${active ? "bg-bg-muted" : "hover:bg-bg-subtle"}`,
@@ -612,7 +612,7 @@ function ToolbarButtonWithTooltip({ button, active, onAction }) {
612
612
  }
613
613
  /** Renders the editor toolbar component. */
614
614
  function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLabels }) {
615
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
615
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$16), customLabels);
616
616
  const groups = [];
617
617
  let current = [];
618
618
  for (const btn of buttons) {
@@ -645,14 +645,14 @@ function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLa
645
645
  }
646
646
  //#endregion
647
647
  //#region src/shells/content-tabs.tsx
648
- const defaultLabels$12 = {
648
+ const defaultLabels$15 = {
649
649
  ariaLabel: "콘텐츠 탭",
650
650
  emptyMessage: "표시할 탭이 없습니다."
651
651
  };
652
652
  /** Renders the content tabs component. */
653
653
  function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
654
654
  var _tabs$, _tabs$find;
655
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels);
655
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$15), customLabels);
656
656
  const [selectedTabId, setSelectedTabId] = useState(defaultTabId !== null && defaultTabId !== void 0 ? defaultTabId : (_tabs$ = tabs[0]) === null || _tabs$ === void 0 ? void 0 : _tabs$.id);
657
657
  if (tabs.length === 0) return /* @__PURE__ */ jsx("div", {
658
658
  className: "text-text-secondary text-sm",
@@ -684,13 +684,13 @@ function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
684
684
  }
685
685
  //#endregion
686
686
  //#region src/shells/faq-accordion.tsx
687
- const defaultLabels$11 = {
687
+ const defaultLabels$14 = {
688
688
  ariaLabel: "자주 묻는 질문",
689
689
  emptyMessage: "등록된 FAQ가 없습니다."
690
690
  };
691
691
  /** Renders the faq accordion component. */
692
692
  function FaqAccordion({ items, labels: customLabels }) {
693
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
693
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$14), customLabels);
694
694
  if (items.length === 0) return /* @__PURE__ */ jsx("div", {
695
695
  className: "text-text-secondary text-sm",
696
696
  "data-opt-id": "QRBHV",
@@ -730,14 +730,14 @@ function FaqAccordion({ items, labels: customLabels }) {
730
730
  }
731
731
  //#endregion
732
732
  //#region src/shells/settings-form.tsx
733
- const defaultLabels$10 = {
733
+ const defaultLabels$13 = {
734
734
  formAriaLabel: "설정 폼",
735
735
  submitButton: "저장",
736
736
  successMessage: "설정이 저장되었습니다."
737
737
  };
738
738
  /** Renders the settings form component. */
739
739
  function SettingsForm({ fields, onSubmit, labels: customLabels }) {
740
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
740
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
741
741
  const [submitted, setSubmitted] = useState(false);
742
742
  const defaultValues = {};
743
743
  for (const field of fields) {
@@ -1063,7 +1063,7 @@ function ProjectSwitcher({ projects, value, onChange, placeholder = "프로젝
1063
1063
  ProjectSwitcher.displayName = "ProjectSwitcher";
1064
1064
  //#endregion
1065
1065
  //#region src/shells/env-panel.tsx
1066
- const defaultLabels$9 = {
1066
+ const defaultLabels$12 = {
1067
1067
  varCount: (count) => `${count}개 변수`,
1068
1068
  envVarsLabel: (env) => `${env} 환경 변수`,
1069
1069
  secretBadge: "secret"
@@ -1075,7 +1075,7 @@ const envDot = {
1075
1075
  };
1076
1076
  /** Renders the env panel component. */
1077
1077
  function EnvPanel({ groups, labels: customLabels }) {
1078
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$9), customLabels);
1078
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels);
1079
1079
  return /* @__PURE__ */ jsx("div", {
1080
1080
  className: `rounded-lg ${OPT_BORDER} divide-y ${OPT_DIVIDER} bg-surface`,
1081
1081
  "data-opt-id": "ZBDWK",
@@ -1148,7 +1148,7 @@ function EnvPanel({ groups, labels: customLabels }) {
1148
1148
  }
1149
1149
  //#endregion
1150
1150
  //#region src/shells/branch-select.tsx
1151
- const defaultLabels$8 = {
1151
+ const defaultLabels$11 = {
1152
1152
  selectLabel: "브랜치",
1153
1153
  emptySelectionLabel: "선택...",
1154
1154
  emptyMessage: "사용 가능한 브랜치가 없습니다.",
@@ -1157,7 +1157,7 @@ const defaultLabels$8 = {
1157
1157
  /** Renders the branch select component. */
1158
1158
  function BranchSelect({ branches, value: controlledValue, onChange, label, labels: customLabels }) {
1159
1159
  var _ref, _defaultBranch$name, _branches$, _selected$name;
1160
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
1160
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
1161
1161
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
1162
1162
  const defaultBranch = branches.find((b) => b.isDefault);
1163
1163
  const [internalValue, setInternalValue] = useState((_ref = (_defaultBranch$name = defaultBranch === null || defaultBranch === void 0 ? void 0 : defaultBranch.name) !== null && _defaultBranch$name !== void 0 ? _defaultBranch$name : (_branches$ = branches[0]) === null || _branches$ === void 0 ? void 0 : _branches$.name) !== null && _ref !== void 0 ? _ref : "");
@@ -1266,7 +1266,7 @@ function BranchSelect({ branches, value: controlledValue, onChange, label, label
1266
1266
  BranchSelect.displayName = "BranchSelect";
1267
1267
  //#endregion
1268
1268
  //#region src/shells/domain-table.tsx
1269
- const defaultLabels$7 = {
1269
+ const defaultLabels$10 = {
1270
1270
  domain: "Domain",
1271
1271
  status: "Status",
1272
1272
  ssl: "SSL",
@@ -1296,7 +1296,7 @@ const sslLabel = {
1296
1296
  };
1297
1297
  /** Renders the domain table component. */
1298
1298
  function DomainTable({ domains, title = "도메인", labels: customLabels }) {
1299
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
1299
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
1300
1300
  if (domains.length === 0) return /* @__PURE__ */ jsxs("div", {
1301
1301
  className: "gap-group flex flex-col",
1302
1302
  "data-opt-id": "A7SRM",
@@ -1474,7 +1474,7 @@ function DataTableProgressBar({ value, barColor, label }) {
1474
1474
  });
1475
1475
  }
1476
1476
  /** Renders the data table component. */
1477
- function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowClick, emptyMessage = "No data available", activeRowId, sortable = false, selectable = false, selectedKeys, onSelectionChange, expandable = false, renderExpanded, pageSize, stickyHeader = false, resizableColumns = false, onSort: onSortExternal, className }) {
1477
+ function DataTable({ data, columns, keyExtractor, title, ariaLabel, onRowClick, emptyMessage = "No data available", activeRowId, sortable = false, selectable = false, selectedKeys, onSelectionChange, expandable = false, renderExpanded, pageSize, stickyHeader = false, resizableColumns = false, onSort: onSortExternal, className }) {
1478
1478
  const [sortCol, setSortCol] = useState(null);
1479
1479
  const [sortDir, setSortDir] = useState(null);
1480
1480
  const [expandedKeys, setExpandedKeys] = useState(/* @__PURE__ */ new Set());
@@ -1568,8 +1568,8 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
1568
1568
  focusWrap: true,
1569
1569
  children: /* @__PURE__ */ jsxs(Composite, {
1570
1570
  role: "grid",
1571
- "aria-label": title,
1572
- className: "w-full",
1571
+ "aria-label": title !== null && title !== void 0 ? title : ariaLabel,
1572
+ className: "w-full min-w-max",
1573
1573
  render: /* @__PURE__ */ jsx("table", {}),
1574
1574
  children: [/* @__PURE__ */ jsx("thead", {
1575
1575
  className: cn("border-border-subtle bg-surface-inset border-b", stickyHeader && "sticky top-0 z-10"),
@@ -1600,7 +1600,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
1600
1600
  tabIndex: isSortable ? 0 : void 0,
1601
1601
  "aria-sort": sortCol === col.id && sortDir ? sortDir === "asc" ? "ascending" : "descending" : void 0,
1602
1602
  children: [/* @__PURE__ */ jsxs("span", {
1603
- className: "flex items-center",
1603
+ className: "flex items-center whitespace-nowrap",
1604
1604
  children: [col.header, sortIndicator(col.id)]
1605
1605
  }), resizableColumns && /* @__PURE__ */ jsx("span", {
1606
1606
  role: "separator",
@@ -1664,7 +1664,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
1664
1664
  className: cn("px-3 py-2.5 text-sm text-text-primary whitespace-nowrap", "w-10"),
1665
1665
  role: "gridcell"
1666
1666
  }),
1667
- className: "outline-none data-[active-item]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
1667
+ className: "outline-none data-[focus-visible]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
1668
1668
  children: /* @__PURE__ */ jsx(Checkbox, {
1669
1669
  checked: isSelected,
1670
1670
  onChange: () => toggleSelectRow(key),
@@ -1676,7 +1676,7 @@ function DataTable({ data, columns, keyExtractor, title = "Data Table", onRowCli
1676
1676
  className: cn("px-3 py-2.5 text-sm text-text-primary whitespace-nowrap", "w-10"),
1677
1677
  role: "gridcell"
1678
1678
  }),
1679
- className: "outline-none data-[active-item]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
1679
+ className: "outline-none data-[focus-visible]:bg-accent-subtle data-[focus-visible]:outline-2 data-[focus-visible]:outline-ring data-[focus-visible]:-outline-offset-1",
1680
1680
  children: /* @__PURE__ */ jsx("button", {
1681
1681
  type: "button",
1682
1682
  onClick: (e) => {
@@ -1902,7 +1902,7 @@ function NotificationToast({ toast, onDismiss, defaultDuration = 5e3 }) {
1902
1902
  }
1903
1903
  //#endregion
1904
1904
  //#region src/shells/failure-list.tsx
1905
- const defaultLabels$6 = {
1905
+ const defaultLabels$9 = {
1906
1906
  title: "최근 실패",
1907
1907
  emptyMessage: "실패 항목이 없습니다",
1908
1908
  moreItems: "{count}개 더 보기",
@@ -2001,20 +2001,20 @@ function FailureItemRow({ failure, isLast, onFailureClick, renderActions, merged
2001
2001
  function FailureList({ failures, title, maxItems, grouped = false, onFailureClick, onShowMore, labels: customLabels, locale = "ko-KR", loading = false, renderActions, className }) {
2002
2002
  var _customLabels$timeAgo, _customLabels$timeAgo2, _customLabels$timeAgo3, _customLabels$timeAgo4, _customLabels$timeAgo5, _customLabels$timeAgo6, _customLabels$timeAgo7, _customLabels$timeAgo8, _customLabels$severit, _customLabels$severit2, _customLabels$severit3, _customLabels$severit4, _customLabels$severit5, _customLabels$severit6, _customLabels$title, _customLabels$emptyMe, _customLabels$moreIte;
2003
2003
  const mergedTimeAgo = {
2004
- justNow: (_customLabels$timeAgo = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo2 = customLabels.timeAgo) === null || _customLabels$timeAgo2 === void 0 ? void 0 : _customLabels$timeAgo2.justNow) !== null && _customLabels$timeAgo !== void 0 ? _customLabels$timeAgo : defaultLabels$6.timeAgo.justNow,
2005
- minutesAgo: (_customLabels$timeAgo3 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo4 = customLabels.timeAgo) === null || _customLabels$timeAgo4 === void 0 ? void 0 : _customLabels$timeAgo4.minutesAgo) !== null && _customLabels$timeAgo3 !== void 0 ? _customLabels$timeAgo3 : defaultLabels$6.timeAgo.minutesAgo,
2006
- hoursAgo: (_customLabels$timeAgo5 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo6 = customLabels.timeAgo) === null || _customLabels$timeAgo6 === void 0 ? void 0 : _customLabels$timeAgo6.hoursAgo) !== null && _customLabels$timeAgo5 !== void 0 ? _customLabels$timeAgo5 : defaultLabels$6.timeAgo.hoursAgo,
2007
- daysAgo: (_customLabels$timeAgo7 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo8 = customLabels.timeAgo) === null || _customLabels$timeAgo8 === void 0 ? void 0 : _customLabels$timeAgo8.daysAgo) !== null && _customLabels$timeAgo7 !== void 0 ? _customLabels$timeAgo7 : defaultLabels$6.timeAgo.daysAgo
2004
+ justNow: (_customLabels$timeAgo = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo2 = customLabels.timeAgo) === null || _customLabels$timeAgo2 === void 0 ? void 0 : _customLabels$timeAgo2.justNow) !== null && _customLabels$timeAgo !== void 0 ? _customLabels$timeAgo : defaultLabels$9.timeAgo.justNow,
2005
+ minutesAgo: (_customLabels$timeAgo3 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo4 = customLabels.timeAgo) === null || _customLabels$timeAgo4 === void 0 ? void 0 : _customLabels$timeAgo4.minutesAgo) !== null && _customLabels$timeAgo3 !== void 0 ? _customLabels$timeAgo3 : defaultLabels$9.timeAgo.minutesAgo,
2006
+ hoursAgo: (_customLabels$timeAgo5 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo6 = customLabels.timeAgo) === null || _customLabels$timeAgo6 === void 0 ? void 0 : _customLabels$timeAgo6.hoursAgo) !== null && _customLabels$timeAgo5 !== void 0 ? _customLabels$timeAgo5 : defaultLabels$9.timeAgo.hoursAgo,
2007
+ daysAgo: (_customLabels$timeAgo7 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo8 = customLabels.timeAgo) === null || _customLabels$timeAgo8 === void 0 ? void 0 : _customLabels$timeAgo8.daysAgo) !== null && _customLabels$timeAgo7 !== void 0 ? _customLabels$timeAgo7 : defaultLabels$9.timeAgo.daysAgo
2008
2008
  };
2009
2009
  const mergedSeverityLabels = {
2010
- critical: (_customLabels$severit = customLabels === null || customLabels === void 0 || (_customLabels$severit2 = customLabels.severityLabels) === null || _customLabels$severit2 === void 0 ? void 0 : _customLabels$severit2.critical) !== null && _customLabels$severit !== void 0 ? _customLabels$severit : defaultLabels$6.severityLabels.critical,
2011
- error: (_customLabels$severit3 = customLabels === null || customLabels === void 0 || (_customLabels$severit4 = customLabels.severityLabels) === null || _customLabels$severit4 === void 0 ? void 0 : _customLabels$severit4.error) !== null && _customLabels$severit3 !== void 0 ? _customLabels$severit3 : defaultLabels$6.severityLabels.error,
2012
- warning: (_customLabels$severit5 = customLabels === null || customLabels === void 0 || (_customLabels$severit6 = customLabels.severityLabels) === null || _customLabels$severit6 === void 0 ? void 0 : _customLabels$severit6.warning) !== null && _customLabels$severit5 !== void 0 ? _customLabels$severit5 : defaultLabels$6.severityLabels.warning
2010
+ critical: (_customLabels$severit = customLabels === null || customLabels === void 0 || (_customLabels$severit2 = customLabels.severityLabels) === null || _customLabels$severit2 === void 0 ? void 0 : _customLabels$severit2.critical) !== null && _customLabels$severit !== void 0 ? _customLabels$severit : defaultLabels$9.severityLabels.critical,
2011
+ error: (_customLabels$severit3 = customLabels === null || customLabels === void 0 || (_customLabels$severit4 = customLabels.severityLabels) === null || _customLabels$severit4 === void 0 ? void 0 : _customLabels$severit4.error) !== null && _customLabels$severit3 !== void 0 ? _customLabels$severit3 : defaultLabels$9.severityLabels.error,
2012
+ warning: (_customLabels$severit5 = customLabels === null || customLabels === void 0 || (_customLabels$severit6 = customLabels.severityLabels) === null || _customLabels$severit6 === void 0 ? void 0 : _customLabels$severit6.warning) !== null && _customLabels$severit5 !== void 0 ? _customLabels$severit5 : defaultLabels$9.severityLabels.warning
2013
2013
  };
2014
2014
  const labels = {
2015
- title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$6.title,
2016
- emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$6.emptyMessage,
2017
- moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$6.moreItems
2015
+ title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$9.title,
2016
+ emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$9.emptyMessage,
2017
+ moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$9.moreItems
2018
2018
  };
2019
2019
  const displayTitle = title !== null && title !== void 0 ? title : labels.title;
2020
2020
  const allItems = maxItems ? failures.slice(0, maxItems) : failures;
@@ -2090,7 +2090,7 @@ function renderGrouped(items, remainingCount, rowProps, labels, onShowMore) {
2090
2090
  }
2091
2091
  //#endregion
2092
2092
  //#region src/shells/key-value-editor.tsx
2093
- const defaultLabels$5 = {
2093
+ const defaultLabels$8 = {
2094
2094
  keyPlaceholder: "키",
2095
2095
  valuePlaceholder: "값",
2096
2096
  addButton: "추가",
@@ -2104,7 +2104,7 @@ function generateId() {
2104
2104
  }
2105
2105
  /** Renders the key value editor component. */
2106
2106
  function KeyValueEditor({ value: controlledValue, defaultValue, onChange, showSecretToggle = false, maxEntries, readOnly = false, labels: customLabels, renderActions, validateKey, className }) {
2107
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
2107
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
2108
2108
  const [internalEntries, setInternalEntries] = useState(() => defaultValue !== null && defaultValue !== void 0 ? defaultValue : []);
2109
2109
  const entries = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalEntries;
2110
2110
  const keyInputRef = useRef(null);
@@ -2400,1651 +2400,2203 @@ function WorkflowCanvas({ steps, connections = [], rows = 9, cols = 9, cellSize
2400
2400
  }
2401
2401
  WorkflowCanvas.displayName = "WorkflowCanvas";
2402
2402
  //#endregion
2403
- //#region src/lib/use-auto-refresh.ts
2403
+ //#region src/shells/event-timeline.tsx
2404
+ const defaultLabels$7 = {
2405
+ emptyTitle: "No events yet",
2406
+ emptyDescription: "Events appear here as soon as they arrive.",
2407
+ expand: "Show properties",
2408
+ collapse: "Hide properties",
2409
+ loadMore: "Load more",
2410
+ gap: (duration) => `+${duration}`,
2411
+ properties: "Event properties",
2412
+ openGroup: (title) => `Open ${title}`
2413
+ };
2404
2414
  /**
2405
- * A refresh timer that waits for its own work.
2406
- *
2407
- * A plain `setInterval` fires on a schedule regardless of whether the previous
2408
- * query came back, so a five-second interval over a seven-second query stacks
2409
- * requests until something gives. Chaining the next wait onto the settled
2410
- * promise keeps exactly one refresh in flight, and a rejection stops the loop
2411
- * instead of hammering an endpoint that is already failing.
2415
+ * A line of text and the time it happened have to read as one row. Past about
2416
+ * this width the eye stops connecting them, so the rail is capped rather than
2417
+ * stretched to whatever the page allows. Override with `className` where a
2418
+ * layout genuinely needs the full width.
2412
2419
  */
2413
- function useAutoRefresh({ interval, paused = false, onRefresh }) {
2414
- const callbackRef = useRef(onRefresh);
2415
- callbackRef.current = onRefresh;
2416
- useEffect(() => {
2417
- if (paused || interval <= 0 || !callbackRef.current) return;
2418
- let cancelled = false;
2419
- let timer;
2420
- const schedule = () => {
2421
- timer = setTimeout(async () => {
2422
- if (cancelled) return;
2423
- try {
2424
- var _callbackRef$current;
2425
- await ((_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef));
2426
- } catch (_unused) {
2427
- return;
2428
- }
2429
- if (!cancelled) schedule();
2430
- }, interval);
2431
- };
2432
- schedule();
2433
- return () => {
2434
- cancelled = true;
2435
- if (timer) clearTimeout(timer);
2436
- };
2437
- }, [interval, paused]);
2438
- }
2439
- //#endregion
2440
- //#region src/shells/time-range-control.tsx
2441
- const defaultLabels$4 = {
2442
- trigger: "Select time range",
2443
- invalid: "Invalid time range",
2444
- last: (duration) => `Last ${duration}`,
2445
- next: (duration) => `Next ${duration}`,
2446
- quickSelect: "Quick select",
2447
- apply: "Apply",
2448
- update: "Update",
2449
- refresh: "Refresh",
2450
- commonlyUsed: "Commonly used",
2451
- recentlyUsed: "Recently used",
2452
- start: "Start",
2453
- end: "End",
2454
- startHint: "Absolute time or date math, e.g. now-7d",
2455
- autoRefresh: "Auto refresh",
2456
- autoRefreshOff: "Off",
2457
- every: (duration) => `Every ${duration}`,
2458
- previous: "Previous window",
2459
- nextWindow: "Next window",
2460
- zoomIn: "Zoom in",
2461
- zoomOut: "Zoom out",
2462
- thisUnit: (unit) => `This ${unit}`,
2463
- units: {
2464
- s: "seconds",
2465
- m: "minutes",
2466
- h: "hours",
2467
- d: "days",
2468
- w: "weeks",
2469
- M: "months",
2470
- y: "years"
2471
- }
2420
+ const TIMELINE_WIDTH = "max-w-5xl";
2421
+ const toneDot = {
2422
+ default: "bg-text-tertiary",
2423
+ accent: "bg-accent",
2424
+ success: "bg-success",
2425
+ warning: "bg-warning",
2426
+ danger: "bg-danger"
2472
2427
  };
2473
- const QUICK_UNITS = [
2474
- "m",
2475
- "h",
2476
- "d",
2477
- "w",
2478
- "M",
2479
- "y"
2480
- ];
2481
- const REFRESH_INTERVALS = [
2482
- 0,
2483
- 5e3,
2484
- 1e4,
2485
- 3e4,
2486
- 6e4,
2487
- 3e5
2488
- ];
2489
- function selectValue(value) {
2490
- var _value$;
2491
- return Array.isArray(value) ? (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : "" : value;
2428
+ function toMillis(timestamp) {
2429
+ const parsed = Date.parse(timestamp);
2430
+ return Number.isNaN(parsed) ? 0 : parsed;
2492
2431
  }
2493
- /**
2494
- * What the trigger says.
2495
- *
2496
- * A window anchored to `now` deserves its intent ("Last 7 days"), not two
2497
- * timestamps the reader has to subtract. Everything else gets both ends
2498
- * spelled out. A duration that does not divide into whole minutes is prefixed
2499
- * with `~` rather than rounded silently.
2500
- */
2501
- function describeRange(range, labels, options) {
2502
- const window = measureTimeWindow(range, {
2503
- timeZone: options.timeZone,
2504
- now: options.now
2432
+ function stringifyValue(value) {
2433
+ if (value === null || value === void 0) return "—";
2434
+ if (typeof value === "string") return value;
2435
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
2436
+ return JSON.stringify(value);
2437
+ }
2438
+ function PropertyTable({ properties, ariaLabel }) {
2439
+ const entries = Object.entries(properties);
2440
+ if (entries.length === 0) return null;
2441
+ return /* @__PURE__ */ jsx("dl", {
2442
+ className: cn(OPT_BORDER, "gap-element bg-bg-subtle mt-2 grid grid-cols-1 rounded-lg p-2.5 sm:grid-cols-2"),
2443
+ "aria-label": ariaLabel,
2444
+ children: entries.map(([key, value]) => /* @__PURE__ */ jsxs("div", {
2445
+ className: "flex min-w-0 flex-col",
2446
+ children: [/* @__PURE__ */ jsx("dt", {
2447
+ className: cn(OPT_TEXT_TERTIARY, "truncate text-xs"),
2448
+ children: key
2449
+ }), /* @__PURE__ */ jsx("dd", {
2450
+ className: cn(OPT_TEXT_PRIMARY, "truncate text-xs"),
2451
+ children: stringifyValue(value)
2452
+ })]
2453
+ }, key))
2454
+ });
2455
+ }
2456
+ function TimelineRow({ item, gapLabel, labels, onItemSelect, isLast }) {
2457
+ var _item$tone, _item$content, _item$properties;
2458
+ const [open, setOpen] = useState(false);
2459
+ const expandable = Boolean(item.content || item.properties);
2460
+ const toggle = useCallback(() => setOpen((value) => !value), []);
2461
+ const headline = /* @__PURE__ */ jsx("span", {
2462
+ className: cn(OPT_TEXT_PRIMARY, "truncate text-sm font-medium"),
2463
+ children: item.name
2464
+ });
2465
+ return /* @__PURE__ */ jsxs("li", {
2466
+ className: "relative flex gap-3 pb-4 last:pb-0",
2467
+ children: [/* @__PURE__ */ jsxs("div", {
2468
+ className: "flex shrink-0 flex-col items-center",
2469
+ children: [/* @__PURE__ */ jsx("span", {
2470
+ className: cn("mt-1.5 size-2 shrink-0 rounded-full", toneDot[(_item$tone = item.tone) !== null && _item$tone !== void 0 ? _item$tone : "default"]),
2471
+ "aria-hidden": "true"
2472
+ }), !isLast && /* @__PURE__ */ jsx("span", {
2473
+ className: "bg-border mt-1 w-px grow",
2474
+ "aria-hidden": "true"
2475
+ })]
2476
+ }), /* @__PURE__ */ jsxs("div", {
2477
+ className: "flex min-w-0 grow flex-col",
2478
+ children: [
2479
+ gapLabel && /* @__PURE__ */ jsx("span", {
2480
+ className: cn("text-text-tertiary", "mb-1 text-xs tabular-nums"),
2481
+ children: labels.gap(gapLabel)
2482
+ }),
2483
+ /* @__PURE__ */ jsxs("div", {
2484
+ className: "flex min-w-0 items-baseline gap-2",
2485
+ children: [
2486
+ item.icon && /* @__PURE__ */ jsx("span", {
2487
+ className: "text-text-tertiary",
2488
+ "aria-hidden": "true",
2489
+ children: item.icon
2490
+ }),
2491
+ onItemSelect ? /* @__PURE__ */ jsx("button", {
2492
+ type: "button",
2493
+ onClick: () => onItemSelect(item),
2494
+ className: cn(OPT_FOCUS_VISIBLE, "hover:text-accent min-w-0 truncate rounded text-left"),
2495
+ children: headline
2496
+ }) : headline,
2497
+ /* @__PURE__ */ jsx("time", {
2498
+ dateTime: item.timestamp,
2499
+ className: cn(OPT_TEXT_TERTIARY, "ml-auto shrink-0 text-xs tabular-nums"),
2500
+ children: formatOptTime(item.timestamp)
2501
+ })
2502
+ ]
2503
+ }),
2504
+ item.detail && /* @__PURE__ */ jsx("span", {
2505
+ className: cn("text-text-secondary", "truncate text-xs"),
2506
+ children: item.detail
2507
+ }),
2508
+ expandable && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
2509
+ type: "button",
2510
+ onClick: toggle,
2511
+ "aria-expanded": open,
2512
+ className: cn("outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset", "text-text-tertiary", "hover:text-text-primary mt-1 flex w-fit items-center gap-1 rounded text-xs"),
2513
+ children: [open ? /* @__PURE__ */ jsx(ChevronDown, {
2514
+ className: "size-3",
2515
+ "aria-hidden": "true"
2516
+ }) : /* @__PURE__ */ jsx(ChevronRight, {
2517
+ className: "size-3",
2518
+ "aria-hidden": "true"
2519
+ }), open ? labels.collapse : labels.expand]
2520
+ }), open && ((_item$content = item.content) !== null && _item$content !== void 0 ? _item$content : /* @__PURE__ */ jsx(PropertyTable, {
2521
+ properties: (_item$properties = item.properties) !== null && _item$properties !== void 0 ? _item$properties : {},
2522
+ ariaLabel: labels.properties
2523
+ }))] })
2524
+ ]
2525
+ })]
2526
+ });
2527
+ }
2528
+ /** Builds the gap label for each row, given the reading order. */
2529
+ function useGapLabels(items, showGaps, order, minGapMs, locale) {
2530
+ return useMemo(() => {
2531
+ if (!showGaps) return items.map(() => null);
2532
+ return items.map((item, index) => {
2533
+ const neighbour = order === "desc" ? items[index + 1] : items[index - 1];
2534
+ if (!neighbour) return null;
2535
+ const delta = Math.abs(toMillis(item.timestamp) - toMillis(neighbour.timestamp));
2536
+ if (delta < minGapMs) return null;
2537
+ return formatDuration(delta, locale);
2538
+ });
2539
+ }, [
2540
+ items,
2541
+ showGaps,
2542
+ order,
2543
+ minGapMs,
2544
+ locale
2545
+ ]);
2546
+ }
2547
+ function TimelineList({ items, showGaps, order, minGapMs, locale, labels, onItemSelect }) {
2548
+ const gaps = useGapLabels(items, showGaps, order, minGapMs, locale);
2549
+ return /* @__PURE__ */ jsx("ol", {
2550
+ className: "flex flex-col",
2551
+ children: items.map((item, index) => {
2552
+ var _gaps$index;
2553
+ return /* @__PURE__ */ jsx(TimelineRow, {
2554
+ item,
2555
+ gapLabel: (_gaps$index = gaps[index]) !== null && _gaps$index !== void 0 ? _gaps$index : null,
2556
+ labels,
2557
+ onItemSelect,
2558
+ isLast: index === items.length - 1
2559
+ }, item.id);
2560
+ })
2561
+ });
2562
+ }
2563
+ function GroupBlock({ group, showGaps, order, minGapMs, locale, labels, onItemSelect }) {
2564
+ var _group$defaultOpen;
2565
+ const [open, setOpen] = useState((_group$defaultOpen = group.defaultOpen) !== null && _group$defaultOpen !== void 0 ? _group$defaultOpen : true);
2566
+ return /* @__PURE__ */ jsxs("section", {
2567
+ className: cn(OPT_BORDER, "rounded-xl"),
2568
+ children: [/* @__PURE__ */ jsxs("div", {
2569
+ className: "flex items-center gap-2 px-3 py-2",
2570
+ children: [
2571
+ /* @__PURE__ */ jsxs("button", {
2572
+ type: "button",
2573
+ onClick: () => setOpen((value) => !value),
2574
+ "aria-expanded": open,
2575
+ className: cn(OPT_FOCUS_VISIBLE, "flex min-w-0 grow items-center gap-2 rounded text-left"),
2576
+ children: [
2577
+ open ? /* @__PURE__ */ jsx(ChevronDown, {
2578
+ className: cn(OPT_TEXT_TERTIARY, "size-4 shrink-0"),
2579
+ "aria-hidden": "true"
2580
+ }) : /* @__PURE__ */ jsx(ChevronRight, {
2581
+ className: cn(OPT_TEXT_TERTIARY, "size-4 shrink-0"),
2582
+ "aria-hidden": "true"
2583
+ }),
2584
+ /* @__PURE__ */ jsx("span", {
2585
+ className: cn(OPT_TEXT_PRIMARY, "truncate text-sm font-medium"),
2586
+ children: group.title
2587
+ }),
2588
+ /* @__PURE__ */ jsx(Badge, {
2589
+ size: "sm",
2590
+ variant: "default",
2591
+ className: "shrink-0",
2592
+ children: group.items.length
2593
+ })
2594
+ ]
2595
+ }),
2596
+ group.meta && /* @__PURE__ */ jsx("span", {
2597
+ className: cn("text-text-tertiary", "shrink-0 text-xs"),
2598
+ children: group.meta
2599
+ }),
2600
+ group.href && /* @__PURE__ */ jsx("a", {
2601
+ href: group.href,
2602
+ className: cn("outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset", "text-accent shrink-0 rounded text-xs"),
2603
+ children: labels.openGroup(typeof group.title === "string" ? group.title : group.id)
2604
+ })
2605
+ ]
2606
+ }), open && /* @__PURE__ */ jsx("div", {
2607
+ className: "border-border-subtle border-t px-3 py-3",
2608
+ children: /* @__PURE__ */ jsx(TimelineList, {
2609
+ items: group.items,
2610
+ showGaps,
2611
+ order,
2612
+ minGapMs,
2613
+ locale,
2614
+ labels,
2615
+ onItemSelect
2616
+ })
2617
+ })]
2505
2618
  });
2506
- if (window.isInvalid) return {
2507
- text: labels.invalid,
2508
- isInvalid: true
2509
- };
2510
- const fromParts = parseRelativeParts(range.from);
2511
- const toParts = parseRelativeParts(range.to);
2512
- if ((fromParts === null || fromParts === void 0 ? void 0 : fromParts.roundUnit) && fromParts.count === 0 && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === fromParts.roundUnit && toParts.count === 0) return {
2513
- text: labels.thisUnit(formatCountUnit(1, fromParts.roundUnit, options.locale).replace(/^1\s*/, "")),
2514
- isInvalid: false
2515
- };
2516
- if (isRelativeToNow(range)) {
2517
- var _toParts$count;
2518
- const anchoredAtEnd = ((_toParts$count = toParts === null || toParts === void 0 ? void 0 : toParts.count) !== null && _toParts$count !== void 0 ? _toParts$count : 0) === 0;
2519
- const parts = anchoredAtEnd ? fromParts : toParts;
2520
- const bothSnapped = (parts === null || parts === void 0 ? void 0 : parts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit) && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit);
2521
- const counted = parts && parts.count > 0 ? formatCountUnit(parts.roundUnit && bothSnapped ? parts.count + 1 : parts.count, parts.unit, options.locale) : null;
2522
- const exact = counted !== null && (!(parts === null || parts === void 0 ? void 0 : parts.roundUnit) || bothSnapped);
2523
- const duration = exact ? counted : formatDuration(window.duration, options.locale);
2524
- const prefix = exact || isExactMinutes(window.duration) ? "" : "~";
2525
- return {
2526
- text: anchoredAtEnd ? labels.last(`${prefix}${duration}`) : labels.next(`${prefix}${duration}`),
2527
- isInvalid: false
2528
- };
2529
- }
2530
- const dateOnly = isDateOnly(range.from) && isDateOnly(range.to);
2531
- const from = formatInstant(window.from, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
2532
- const to = formatInstant(window.to, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
2533
- return {
2534
- text: from === to ? from : `${from} – ${to}`,
2535
- isInvalid: false
2536
- };
2537
2619
  }
2538
2620
  /**
2539
- * TimeRangeControlthe window as an expression, not two frozen instants.
2621
+ * EventTimelinea sequence of events, read as a sequence.
2540
2622
  *
2541
- * Holding `{ from: "now-7d", to: "now" }` instead of a resolved pair is what
2542
- * makes a saved board, a shared link and an auto-refresh timer all mean the
2543
- * same thing tomorrow that they mean today. Around that sit the moves people
2544
- * actually make on a chart: jump to a named window, step the same width back
2545
- * and forth to compare, widen when the interesting thing is just off screen,
2546
- * and let it re-query on a timer while they watch.
2623
+ * A table of events answers "what happened" and hides "how it went": the rows
2624
+ * are evenly spaced no matter whether they were two seconds or two hours
2625
+ * apart, and the payload that would explain a row lives in a column nobody
2626
+ * widened. This renders the same data as an ordered rail, labels the elapsed
2627
+ * time between adjacent events, and opens each payload in place. Grouping by
2628
+ * session restores the boundary that makes a history legible.
2547
2629
  *
2548
2630
  * @example
2549
2631
  * ```tsx
2550
- * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
2551
- *
2552
- * <TimeRangeControl
2553
- * value={range}
2554
- * onChange={setRange}
2555
- * timeZone="Asia/Seoul"
2556
- * showWindowButtons
2557
- * onRefresh={() => refetch()}
2632
+ * <EventTimeline
2633
+ * groups={sessions.map((session) => ({
2634
+ * id: session.id,
2635
+ * title: session.entryPath,
2636
+ * meta: `${session.duration} · ${session.events} events`,
2637
+ * items: session.events,
2638
+ * }))}
2639
+ * showGaps
2558
2640
  * />
2559
2641
  * ```
2560
2642
  */
2561
- function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, locale, showUpdateButton = true, showWindowButtons = true, zoomFactor = .5, refresh, onRefreshChange, onRefresh, isLoading = false, disabled = false, className, labels: customLabels, "aria-label": ariaLabel }) {
2562
- var _refresh$interval, _refresh$paused, _refresh$interval2;
2563
- const labels = _objectSpread2(_objectSpread2(_objectSpread2({}, defaultLabels$4), customLabels), {}, { units: _objectSpread2(_objectSpread2({}, defaultLabels$4.units), customLabels === null || customLabels === void 0 ? void 0 : customLabels.units) });
2564
- const [open, setOpen] = useState(false);
2565
- const [draft, setDraft] = useState(null);
2566
- const [quickCount, setQuickCount] = useState("15");
2567
- const [quickUnit, setQuickUnit] = useState("m");
2568
- const editing = draft !== null && draft !== void 0 ? draft : value;
2569
- const hasChanges = draft !== null && (draft.from !== value.from || draft.to !== value.to);
2570
- const description = useMemo(() => describeRange(value, labels, {
2571
- locale,
2572
- timeZone
2573
- }), [
2574
- value,
2575
- locale,
2576
- timeZone,
2577
- customLabels
2578
- ]);
2579
- const boundState = useCallback((expression) => parseTimeExpression(expression, { timeZone }) === null ? labels.invalid : void 0, [timeZone, labels]);
2580
- const commit = useCallback((range) => {
2581
- setDraft(null);
2582
- onChange(range);
2583
- }, [onChange]);
2584
- const apply = useCallback(() => {
2585
- if (draft) commit(draft);
2586
- else onRefresh === null || onRefresh === void 0 || onRefresh();
2587
- }, [
2588
- draft,
2589
- commit,
2590
- onRefresh
2591
- ]);
2592
- const commitOnEnter = useCallback((event) => {
2593
- if (event.key !== "Enter") return;
2594
- apply();
2595
- setOpen(false);
2596
- }, [apply]);
2597
- const applyQuick = useCallback(() => {
2598
- const count = Number(quickCount);
2599
- if (!Number.isFinite(count) || count <= 0) return;
2600
- commit({
2601
- from: `now-${Math.round(count)}${quickUnit}`,
2602
- to: "now"
2603
- });
2604
- setOpen(false);
2605
- }, [
2606
- quickCount,
2607
- quickUnit,
2608
- commit
2609
- ]);
2610
- const step = useCallback((direction) => {
2611
- const next = shiftTimeRange(value, direction, { timeZone });
2612
- if (next) commit(next);
2613
- }, [
2614
- value,
2615
- timeZone,
2616
- commit
2617
- ]);
2618
- const zoom = useCallback((factor) => {
2619
- const next = zoomTimeRange(value, factor, { timeZone });
2620
- if (next) commit(next);
2621
- }, [
2622
- value,
2623
- timeZone,
2624
- commit
2625
- ]);
2626
- useAutoRefresh({
2627
- interval: (_refresh$interval = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval !== void 0 ? _refresh$interval : 0,
2628
- paused: (_refresh$paused = refresh === null || refresh === void 0 ? void 0 : refresh.paused) !== null && _refresh$paused !== void 0 ? _refresh$paused : true,
2629
- onRefresh
2643
+ function EventTimeline({ items, groups, showGaps = true, order = "desc", minGapMs = 1e3, loading = false, hasMore = false, onLoadMore, onItemSelect, locale = OPT_UI_DEFAULT_LOCALE, labels: customLabels, className }) {
2644
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
2645
+ const flatItems = items !== null && items !== void 0 ? items : [];
2646
+ const hasContent = groups ? groups.length > 0 : flatItems.length > 0;
2647
+ if (loading) return /* @__PURE__ */ jsx("div", {
2648
+ className: cn(TIMELINE_WIDTH, "flex flex-col gap-3", className),
2649
+ "data-opt-id": "H6S6T",
2650
+ "data-opt-slug": "event-timeline.EventTimeline",
2651
+ children: Array.from({ length: 5 }).map((_, index) => /* @__PURE__ */ jsxs("div", {
2652
+ className: "flex gap-3",
2653
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 size-2 rounded-full" }), /* @__PURE__ */ jsxs("div", {
2654
+ className: "flex grow flex-col gap-1.5",
2655
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-40" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-64" })]
2656
+ })]
2657
+ }, index))
2658
+ });
2659
+ if (!hasContent) return /* @__PURE__ */ jsx("div", {
2660
+ className: cn(TIMELINE_WIDTH, className),
2661
+ "data-opt-id": "H6S6T",
2662
+ "data-opt-slug": "event-timeline.EventTimeline",
2663
+ children: /* @__PURE__ */ jsx(EmptyState, {
2664
+ title: labels.emptyTitle,
2665
+ description: labels.emptyDescription,
2666
+ tone: "subdued",
2667
+ hasBorder: true
2668
+ })
2630
2669
  });
2631
- const refreshLabel = !refresh || refresh.paused || refresh.interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(refresh.interval, locale));
2632
2670
  return /* @__PURE__ */ jsxs("div", {
2633
- className: cn("bg-surface inline-flex items-center rounded-lg border", OPT_BORDER, className),
2634
- role: "group",
2635
- "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.trigger,
2636
- "data-opt-id": "3WJ8H",
2637
- "data-opt-slug": "time-range-control.TimeRangeControl",
2638
- children: [
2639
- /* @__PURE__ */ jsxs(PopoverRoot$1, {
2640
- open,
2641
- onOpenChange: setOpen,
2642
- children: [/* @__PURE__ */ jsxs(PopoverTrigger$1, {
2643
- className: cn("text-text-primary h-10 gap-2", description.isInvalid && "text-danger-fg"),
2644
- disabled,
2645
- children: [/* @__PURE__ */ jsx(Clock, {
2646
- className: "text-text-tertiary h-4 w-4",
2647
- "aria-hidden": "true"
2648
- }), /* @__PURE__ */ jsx("span", {
2649
- className: "truncate text-sm",
2650
- children: description.text
2651
- })]
2652
- }), /* @__PURE__ */ jsxs(PopoverContent$1, {
2653
- className: "gap-group flex w-80 flex-col",
2671
+ className: cn(TIMELINE_WIDTH, "flex flex-col gap-3", className),
2672
+ "data-opt-id": "H6S6T",
2673
+ "data-opt-slug": "event-timeline.EventTimeline",
2674
+ children: [groups ? groups.map((group) => /* @__PURE__ */ jsx(GroupBlock, {
2675
+ group,
2676
+ showGaps,
2677
+ order,
2678
+ minGapMs,
2679
+ locale,
2680
+ labels,
2681
+ onItemSelect
2682
+ }, group.id)) : /* @__PURE__ */ jsx(TimelineList, {
2683
+ items: flatItems,
2684
+ showGaps,
2685
+ order,
2686
+ minGapMs,
2687
+ locale,
2688
+ labels,
2689
+ onItemSelect
2690
+ }), hasMore && onLoadMore && /* @__PURE__ */ jsx("div", {
2691
+ className: "flex justify-center pt-1",
2692
+ children: /* @__PURE__ */ jsx(Button, {
2693
+ variant: "secondary",
2694
+ size: "sm",
2695
+ onClick: onLoadMore,
2696
+ children: labels.loadMore
2697
+ })
2698
+ })]
2699
+ });
2700
+ }
2701
+ EventTimeline.displayName = "EventTimeline";
2702
+ //#endregion
2703
+ //#region src/shells/score-breakdown.tsx
2704
+ const defaultLabels$6 = {
2705
+ title: "Score",
2706
+ outOf: (max) => `out of ${max}`,
2707
+ weight: (percent) => `${percent} of total`,
2708
+ empty: "Not enough activity to score."
2709
+ };
2710
+ const toneText = {
2711
+ default: "text-text-primary",
2712
+ success: "text-success-fg",
2713
+ warning: "text-warning-fg",
2714
+ error: "text-danger-fg"
2715
+ };
2716
+ const toneMeter = {
2717
+ default: "default",
2718
+ success: "success",
2719
+ warning: "warning",
2720
+ error: "error"
2721
+ };
2722
+ /** Picks the band a score falls into — the highest `min` at or below it. */
2723
+ function resolveBand(score, bands) {
2724
+ return bands.filter((band) => score >= band.min).sort((left, right) => right.min - left.min)[0];
2725
+ }
2726
+ /**
2727
+ * ScoreBreakdown — a composite score you can argue with.
2728
+ *
2729
+ * Engagement, health and risk scores all collapse several signals into one
2730
+ * number, and the number alone tells nobody what to do about it. Showing the
2731
+ * axes and their weights beside the headline turns "43" into "recent, but
2732
+ * shallow", which is a finding rather than a verdict.
2733
+ */
2734
+ function ScoreBreakdown({ score, max = 100, axes, bands = [], formula, loading = false, labels: customLabels, className }) {
2735
+ var _band$tone;
2736
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$6), customLabels);
2737
+ const band = resolveBand(score, bands);
2738
+ const tone = (_band$tone = band === null || band === void 0 ? void 0 : band.tone) !== null && _band$tone !== void 0 ? _band$tone : "default";
2739
+ if (loading) return /* @__PURE__ */ jsxs("section", {
2740
+ className: cn(OPT_BORDER, "flex flex-col gap-3 rounded-xl p-4", className),
2741
+ "data-opt-id": "DYZSG",
2742
+ "data-opt-slug": "score-breakdown.ScoreBreakdown",
2743
+ children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-9 w-24" }), Array.from({ length: 4 }).map((_, index) => /* @__PURE__ */ jsx(Skeleton, { className: "h-6 w-full" }, index))]
2744
+ });
2745
+ return /* @__PURE__ */ jsxs("section", {
2746
+ className: cn(OPT_BORDER, "flex flex-col gap-4 rounded-xl p-4", className),
2747
+ "data-opt-id": "DYZSG",
2748
+ "data-opt-slug": "score-breakdown.ScoreBreakdown",
2749
+ children: [/* @__PURE__ */ jsxs("div", {
2750
+ className: "flex flex-col gap-1",
2751
+ children: [
2752
+ /* @__PURE__ */ jsx("span", {
2753
+ className: cn(OPT_TEXT_SECONDARY, "text-sm"),
2754
+ children: labels.title
2755
+ }),
2756
+ /* @__PURE__ */ jsxs("div", {
2757
+ className: "flex items-baseline gap-2",
2654
2758
  children: [
2655
- /* @__PURE__ */ jsxs("div", {
2656
- className: "gap-element flex flex-col",
2657
- children: [/* @__PURE__ */ jsx("p", {
2658
- className: "text-text-primary text-xs font-medium",
2659
- children: labels.quickSelect
2660
- }), /* @__PURE__ */ jsxs("div", {
2661
- className: "flex items-end gap-2",
2662
- children: [
2663
- /* @__PURE__ */ jsx(Input, {
2664
- "aria-label": labels.quickSelect,
2665
- type: "number",
2666
- min: 1,
2667
- size: "sm",
2668
- className: "w-20",
2669
- value: quickCount,
2670
- onChange: (event) => setQuickCount(event.target.value),
2671
- onKeyDown: (event) => {
2672
- if (event.key === "Enter") applyQuick();
2673
- }
2674
- }),
2675
- /* @__PURE__ */ jsxs(SelectRoot$1, {
2676
- value: quickUnit,
2677
- setValue: (next) => setQuickUnit(selectValue(next)),
2678
- children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
2679
- minWidth: 110,
2680
- "aria-label": labels.quickSelect
2681
- }), /* @__PURE__ */ jsx(SelectPopover$1, { children: QUICK_UNITS.map((unit) => /* @__PURE__ */ jsx(SelectItem$1, {
2682
- value: unit,
2683
- children: labels.units[unit]
2684
- }, unit)) })]
2685
- }),
2686
- /* @__PURE__ */ jsx(Button, {
2687
- variant: "secondary",
2688
- size: "sm",
2689
- onClick: applyQuick,
2690
- children: labels.apply
2691
- })
2692
- ]
2693
- })]
2694
- }),
2695
- /* @__PURE__ */ jsxs("div", {
2696
- className: "gap-element flex flex-col",
2697
- children: [/* @__PURE__ */ jsx(Input, {
2698
- label: labels.start,
2699
- size: "sm",
2700
- value: editing.from,
2701
- error: boundState(editing.from),
2702
- hint: labels.startHint,
2703
- onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { from: event.target.value })),
2704
- onKeyDown: commitOnEnter
2705
- }), /* @__PURE__ */ jsx(Input, {
2706
- label: labels.end,
2707
- size: "sm",
2708
- value: editing.to,
2709
- error: boundState(editing.to),
2710
- onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { to: event.target.value })),
2711
- onKeyDown: commitOnEnter
2712
- })]
2713
- }),
2714
- presets && presets.length > 0 && /* @__PURE__ */ jsx(RangeList, {
2715
- title: labels.commonlyUsed,
2716
- items: presets,
2717
- onPick: (range) => {
2718
- commit(range);
2719
- setOpen(false);
2720
- }
2759
+ /* @__PURE__ */ jsx("span", {
2760
+ className: cn("text-3xl font-bold tabular-nums", toneText[tone]),
2761
+ children: score
2721
2762
  }),
2722
- recentRanges && recentRanges.length > 0 && /* @__PURE__ */ jsx(RangeList, {
2723
- title: labels.recentlyUsed,
2724
- items: recentRanges.slice(0, 5),
2725
- onPick: (range) => {
2726
- commit(range);
2727
- setOpen(false);
2728
- }
2763
+ /* @__PURE__ */ jsx("span", {
2764
+ className: cn(OPT_TEXT_TERTIARY, "text-xs"),
2765
+ children: labels.outOf(max)
2729
2766
  }),
2730
- onRefreshChange && /* @__PURE__ */ jsxs("div", {
2731
- className: "gap-element flex flex-col",
2732
- children: [/* @__PURE__ */ jsx("p", {
2733
- className: "text-text-primary text-xs font-medium",
2734
- children: labels.autoRefresh
2735
- }), /* @__PURE__ */ jsxs(SelectRoot$1, {
2736
- value: String((refresh === null || refresh === void 0 ? void 0 : refresh.paused) ? 0 : (_refresh$interval2 = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval2 !== void 0 ? _refresh$interval2 : 0),
2737
- setValue: (next) => {
2738
- const interval = Number(selectValue(next));
2739
- onRefreshChange({
2740
- interval,
2741
- paused: interval === 0
2742
- });
2743
- },
2744
- children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
2745
- minWidth: 140,
2746
- "aria-label": labels.autoRefresh
2747
- }), /* @__PURE__ */ jsx(SelectPopover$1, { children: REFRESH_INTERVALS.map((interval) => /* @__PURE__ */ jsx(SelectItem$1, {
2748
- value: String(interval),
2749
- children: interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(interval, locale))
2750
- }, interval)) })]
2751
- })]
2767
+ band && /* @__PURE__ */ jsx("span", {
2768
+ className: cn("ml-1 text-sm font-medium", toneText[tone]),
2769
+ children: band.label
2752
2770
  })
2753
2771
  ]
2754
- })]
2755
- }),
2756
- showWindowButtons && /* @__PURE__ */ jsxs("div", {
2757
- className: cn("flex items-center border-l", "border border-border"),
2758
- role: "group",
2759
- "aria-label": labels.previous,
2772
+ }),
2773
+ formula && /* @__PURE__ */ jsx("p", {
2774
+ className: cn("text-text-tertiary", "text-xs"),
2775
+ children: formula
2776
+ })
2777
+ ]
2778
+ }), axes.length === 0 ? /* @__PURE__ */ jsx("p", {
2779
+ className: cn(OPT_TEXT_SECONDARY, "text-sm"),
2780
+ children: labels.empty
2781
+ }) : /* @__PURE__ */ jsx("dl", {
2782
+ className: "flex flex-col gap-3",
2783
+ children: axes.map((axis) => /* @__PURE__ */ jsxs("div", {
2784
+ className: "flex flex-col gap-1",
2760
2785
  children: [
2761
- /* @__PURE__ */ jsx(IconButton, {
2762
- label: labels.previous,
2763
- disabled: disabled || description.isInvalid,
2764
- onClick: () => step(-1),
2765
- children: /* @__PURE__ */ jsx(ChevronLeft, {
2766
- className: "h-4 w-4",
2767
- "aria-hidden": "true"
2768
- })
2769
- }),
2770
- /* @__PURE__ */ jsx(IconButton, {
2771
- label: labels.zoomIn,
2772
- disabled: disabled || description.isInvalid,
2773
- onClick: () => zoom(-zoomFactor),
2774
- children: /* @__PURE__ */ jsx(ZoomIn, {
2775
- className: "h-4 w-4",
2776
- "aria-hidden": "true"
2777
- })
2786
+ /* @__PURE__ */ jsxs("div", {
2787
+ className: "flex items-baseline justify-between gap-2",
2788
+ children: [/* @__PURE__ */ jsxs("dt", {
2789
+ className: cn(OPT_TEXT_PRIMARY, "text-sm"),
2790
+ title: axis.hint,
2791
+ children: [axis.label, axis.weight !== void 0 && /* @__PURE__ */ jsx("span", {
2792
+ className: cn("text-text-tertiary", "ml-1.5 text-xs"),
2793
+ children: labels.weight(`${Math.round(axis.weight * 100)}%`)
2794
+ })]
2795
+ }), /* @__PURE__ */ jsx("dd", {
2796
+ className: cn(OPT_TEXT_SECONDARY, "text-sm font-medium tabular-nums"),
2797
+ children: Math.round(axis.value)
2798
+ })]
2778
2799
  }),
2779
- /* @__PURE__ */ jsx(IconButton, {
2780
- label: labels.zoomOut,
2781
- disabled: disabled || description.isInvalid,
2782
- onClick: () => zoom(zoomFactor),
2783
- children: /* @__PURE__ */ jsx(ZoomOut, {
2784
- className: "h-4 w-4",
2785
- "aria-hidden": "true"
2786
- })
2800
+ /* @__PURE__ */ jsx(Meter$1, {
2801
+ value: axis.value,
2802
+ max,
2803
+ size: "sm",
2804
+ variant: toneMeter[tone],
2805
+ "aria-label": axis.label
2787
2806
  }),
2788
- /* @__PURE__ */ jsx(IconButton, {
2789
- label: labels.nextWindow,
2790
- disabled: disabled || description.isInvalid,
2791
- onClick: () => step(1),
2792
- children: /* @__PURE__ */ jsx(ChevronRight, {
2793
- className: "h-4 w-4",
2794
- "aria-hidden": "true"
2795
- })
2807
+ axis.hint && /* @__PURE__ */ jsx("p", {
2808
+ className: cn("text-text-tertiary", "text-xs"),
2809
+ children: axis.hint
2796
2810
  })
2797
2811
  ]
2798
- }),
2799
- (onRefresh || showUpdateButton) && /* @__PURE__ */ jsx("div", {
2800
- className: cn("border-l pl-2 pr-2", "border border-border"),
2801
- children: /* @__PURE__ */ jsx(Button, {
2802
- variant: hasChanges ? "primary" : "secondary",
2803
- size: "sm",
2804
- loading: isLoading,
2805
- disabled: disabled || description.isInvalid,
2806
- onClick: apply,
2807
- leftIcon: hasChanges ? void 0 : /* @__PURE__ */ jsx(RefreshCw, {
2808
- className: "h-3.5 w-3.5",
2809
- "aria-hidden": "true"
2810
- }),
2811
- title: refreshLabel,
2812
- children: hasChanges ? labels.update : labels.refresh
2812
+ }, axis.id))
2813
+ })]
2814
+ });
2815
+ }
2816
+ ScoreBreakdown.displayName = "ScoreBreakdown";
2817
+ //#endregion
2818
+ //#region src/shells/flyout.tsx
2819
+ const defaultLabels$5 = { close: "Close panel" };
2820
+ const sizeClass = {
2821
+ sm: "sm:w-[22rem]",
2822
+ md: "sm:w-[30rem]",
2823
+ lg: "sm:w-[42rem]"
2824
+ };
2825
+ /**
2826
+ * Flyout a detail panel that does not take the page away.
2827
+ *
2828
+ * Navigating from a list to a record and back costs the reader their scroll
2829
+ * position, their filters and their place in the scan. A flyout keeps the list
2830
+ * on screen and shows the record beside it, so comparing three rows is three
2831
+ * clicks rather than six navigations.
2832
+ *
2833
+ * Deliberately **not** a modal: no focus trap, no `aria-modal`, no inert page.
2834
+ * The list behind it is still the point, and a reader who tabs out of the panel
2835
+ * to change a filter is doing the expected thing. Escape closes; focus returns
2836
+ * to whatever opened it.
2837
+ *
2838
+ * @example
2839
+ * ```tsx
2840
+ * <Flyout open={Boolean(selected)} onClose={clear} title={selected?.name}>
2841
+ * <DescriptionList items={fields} />
2842
+ * </Flyout>
2843
+ * ```
2844
+ */
2845
+ function Flyout({ open, onClose, title, description, type = "overlay", side = "right", size = "md", actions, footer, scrim = false, closeOnOutsideClick = true, hideCloseButton = false, labels: customLabels, className, children }) {
2846
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
2847
+ const panelRef = useRef(null);
2848
+ const openerRef = useRef(null);
2849
+ useEffect(() => {
2850
+ var _panelRef$current;
2851
+ if (!open) return;
2852
+ openerRef.current = document.activeElement;
2853
+ (_panelRef$current = panelRef.current) === null || _panelRef$current === void 0 || _panelRef$current.focus({ preventScroll: true });
2854
+ return () => {
2855
+ var _openerRef$current, _openerRef$current$fo;
2856
+ (_openerRef$current = openerRef.current) === null || _openerRef$current === void 0 || (_openerRef$current$fo = _openerRef$current.focus) === null || _openerRef$current$fo === void 0 || _openerRef$current$fo.call(_openerRef$current, { preventScroll: true });
2857
+ };
2858
+ }, [open]);
2859
+ useEffect(() => {
2860
+ if (!open) return;
2861
+ const handleKeyDown = (event) => {
2862
+ if (event.key === "Escape") {
2863
+ event.stopPropagation();
2864
+ onClose();
2865
+ }
2866
+ };
2867
+ document.addEventListener("keydown", handleKeyDown);
2868
+ return () => document.removeEventListener("keydown", handleKeyDown);
2869
+ }, [open, onClose]);
2870
+ useEffect(() => {
2871
+ if (!open || !closeOnOutsideClick || type !== "overlay") return;
2872
+ const handlePointerDown = (event) => {
2873
+ const panel = panelRef.current;
2874
+ if (panel && !panel.contains(event.target)) onClose();
2875
+ };
2876
+ const timer = window.setTimeout(() => {
2877
+ document.addEventListener("pointerdown", handlePointerDown);
2878
+ }, 0);
2879
+ return () => {
2880
+ window.clearTimeout(timer);
2881
+ document.removeEventListener("pointerdown", handlePointerDown);
2882
+ };
2883
+ }, [
2884
+ open,
2885
+ closeOnOutsideClick,
2886
+ type,
2887
+ onClose
2888
+ ]);
2889
+ const handleClose = useCallback(() => onClose(), [onClose]);
2890
+ if (!open) return null;
2891
+ const header = (title || actions || !hideCloseButton) && /* @__PURE__ */ jsxs("header", {
2892
+ className: cn("border-border-subtle flex items-start gap-2 border-b px-4 py-3"),
2893
+ children: [
2894
+ /* @__PURE__ */ jsxs("div", {
2895
+ className: "flex min-w-0 grow flex-col gap-0.5",
2896
+ children: [title && /* @__PURE__ */ jsx("h2", {
2897
+ className: cn("text-text-primary", "truncate text-base font-semibold"),
2898
+ children: title
2899
+ }), description && /* @__PURE__ */ jsx("p", {
2900
+ className: cn("text-text-secondary", "truncate text-xs"),
2901
+ children: description
2902
+ })]
2903
+ }),
2904
+ actions,
2905
+ !hideCloseButton && /* @__PURE__ */ jsx("button", {
2906
+ type: "button",
2907
+ onClick: handleClose,
2908
+ "aria-label": labels.close,
2909
+ className: cn("outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset", "text-text-tertiary", "hover:text-text-primary shrink-0 rounded p-1 transition-colors"),
2910
+ children: /* @__PURE__ */ jsx(X, {
2911
+ className: "size-4",
2912
+ "aria-hidden": "true"
2813
2913
  })
2814
2914
  })
2815
2915
  ]
2816
2916
  });
2817
- }
2818
- TimeRangeControl.displayName = "TimeRangeControl";
2819
- function IconButton({ label, disabled, onClick, children }) {
2820
- return /* @__PURE__ */ jsx(Button, {
2821
- variant: "ghost",
2822
- size: "sm",
2823
- "aria-label": label,
2824
- title: label,
2825
- disabled,
2826
- onClick,
2827
- children
2917
+ const panel = /* @__PURE__ */ jsxs("aside", {
2918
+ ref: panelRef,
2919
+ role: "dialog",
2920
+ "aria-modal": false,
2921
+ "aria-label": typeof title === "string" ? title : void 0,
2922
+ tabIndex: -1,
2923
+ className: cn(OPT_FOCUS_VISIBLE, "flex min-h-0 flex-col", type === "overlay" && [
2924
+ "rounded-xl bg-surface-raised shadow-md",
2925
+ "fixed inset-y-0 z-40 w-full",
2926
+ sizeClass[size],
2927
+ side === "right" ? "right-0 border-l" : "left-0 border-r",
2928
+ "border-border"
2929
+ ], type === "inline" && ["border border-border", "rounded-xl bg-surface"], className),
2930
+ "data-opt-id": "85XVE",
2931
+ "data-opt-slug": "flyout.Flyout",
2932
+ children: [
2933
+ header,
2934
+ /* @__PURE__ */ jsx("div", {
2935
+ className: "min-h-0 grow overflow-y-auto px-4 py-4",
2936
+ children
2937
+ }),
2938
+ footer && /* @__PURE__ */ jsx("footer", {
2939
+ className: "border-border-subtle border-t px-4 py-3",
2940
+ children: footer
2941
+ })
2942
+ ]
2828
2943
  });
2944
+ if (type === "inline") return panel;
2945
+ return /* @__PURE__ */ jsxs(Fragment, { children: [scrim && /* @__PURE__ */ jsx("div", {
2946
+ className: "bg-overlay fixed inset-0 z-30",
2947
+ "aria-hidden": "true"
2948
+ }), panel] });
2829
2949
  }
2830
- function RangeList({ title, items, onPick }) {
2831
- return /* @__PURE__ */ jsxs("div", {
2832
- className: "gap-element flex flex-col",
2833
- children: [/* @__PURE__ */ jsx("p", {
2834
- className: "text-text-primary text-xs font-medium",
2835
- children: title
2836
- }), /* @__PURE__ */ jsx("ul", {
2837
- className: "grid grid-cols-2 gap-1",
2838
- children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2839
- type: "button",
2840
- className: cn("hover:bg-bg-subtle w-full cursor-pointer rounded px-2 py-1 text-left text-xs", OPT_TEXT_SECONDARY),
2841
- onClick: () => onPick(item.range),
2842
- children: item.label
2843
- }) }, `${item.label}-${index}`))
2844
- })]
2845
- });
2950
+ Flyout.displayName = "Flyout";
2951
+ //#endregion
2952
+ //#region src/lib/query/query-ast.ts
2953
+ const EMPTY_QUERY = { clauses: [] };
2954
+ /** Whether a query constrains anything at all. */
2955
+ function isEmptyQuery(query) {
2956
+ return !query || query.clauses.length === 0;
2957
+ }
2958
+ function sameClause(a, b) {
2959
+ if (a.type !== b.type) return false;
2960
+ if (a.type === "term" && b.type === "term") return a.value === b.value && a.negated === b.negated;
2961
+ const fieldA = a;
2962
+ const fieldB = b;
2963
+ return fieldA.field === fieldB.field && fieldA.operator === fieldB.operator && fieldA.negated === fieldB.negated && fieldA.values.length === fieldB.values.length && fieldA.values.every((value, index) => value === fieldB.values[index]);
2964
+ }
2965
+ /** Add a clause unless an identical one is already there. */
2966
+ function addClause(query, clause) {
2967
+ if (query.clauses.some((existing) => sameClause(existing, clause))) return query;
2968
+ return { clauses: [...query.clauses, clause] };
2846
2969
  }
2847
2970
  /**
2848
- * The windows most analytics tools open with. Exported so a caller gets a
2849
- * sensible list without inventing date math of their own.
2971
+ * Constrain a field to a value what a cell's "filter for this" action does.
2972
+ *
2973
+ * Applying the opposite of a constraint already present replaces it rather than
2974
+ * adding it: `status:200` then "filter out 200" should read as one changed
2975
+ * mind, not as a query that can never match.
2850
2976
  */
2851
- const DEFAULT_TIME_PRESETS = [
2852
- {
2853
- label: "Last 15 minutes",
2854
- range: {
2855
- from: "now-15m",
2856
- to: "now"
2857
- }
2858
- },
2859
- {
2860
- label: "Last hour",
2861
- range: {
2862
- from: "now-1h",
2863
- to: "now"
2864
- }
2865
- },
2866
- {
2867
- label: "Last 24 hours",
2868
- range: {
2869
- from: "now-24h",
2870
- to: "now"
2977
+ function withFieldValue(query, field, value, options = {}) {
2978
+ var _options$negated, _options$operator;
2979
+ const negated = (_options$negated = options.negated) !== null && _options$negated !== void 0 ? _options$negated : false;
2980
+ const operator = (_options$operator = options.operator) !== null && _options$operator !== void 0 ? _options$operator : "eq";
2981
+ return { clauses: [...query.clauses.filter((clause) => !(clause.type === "field" && clause.field === field && clause.operator === operator && clause.values.length === 1 && clause.values[0] === value)), {
2982
+ type: "field",
2983
+ field,
2984
+ operator,
2985
+ values: [value],
2986
+ negated
2987
+ }] };
2988
+ }
2989
+ /** Drop every clause naming `field`. */
2990
+ function withoutField(query, field) {
2991
+ return { clauses: query.clauses.filter((clause) => clause.type !== "field" || clause.field !== field) };
2992
+ }
2993
+ /** Drop the clause at `index`, for a removable chip. */
2994
+ function removeClause(query, index) {
2995
+ return { clauses: query.clauses.filter((_, at) => at !== index) };
2996
+ }
2997
+ /** Every field the query names, in the order they first appear. */
2998
+ function queryFields(query) {
2999
+ const seen = [];
3000
+ for (const clause of query.clauses) if (clause.type === "field" && !seen.includes(clause.field)) seen.push(clause.field);
3001
+ return seen;
3002
+ }
3003
+ /** The values a field is constrained to, if any — for showing a filter as active. */
3004
+ function fieldValues(query, field) {
3005
+ return query.clauses.flatMap((clause) => clause.type === "field" && clause.field === field && !clause.negated ? clause.values : []);
3006
+ }
3007
+ /**
3008
+ * Fold a cell action into the query.
3009
+ *
3010
+ * The pair of buttons on a log cell is the fastest path from "I noticed this
3011
+ * value" to a filtered view, and the reason it works is that the result is a
3012
+ * query the reader can then see, edit and share — not a hidden filter stack
3013
+ * with its own rules.
3014
+ */
3015
+ function applyCellAction(query, action) {
3016
+ return withFieldValue(query, action.field, action.value, { negated: action.intent === "exclude" });
3017
+ }
3018
+ //#endregion
3019
+ //#region src/lib/query/query-parse.ts
3020
+ const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
3021
+ /**
3022
+ * Split on whitespace, but keep quoted runs and parenthesised groups whole.
3023
+ *
3024
+ * An unterminated quote or group swallows the rest of the line rather than
3025
+ * erroring: the reader is mid-word, and the alternative is the query flickering
3026
+ * between two different meanings as they type the closing character.
3027
+ */
3028
+ function tokenize(input) {
3029
+ const tokens = [];
3030
+ let index = 0;
3031
+ while (index < input.length) {
3032
+ while (index < input.length && /\s/.test(input[index])) index += 1;
3033
+ if (index >= input.length) break;
3034
+ const start = index;
3035
+ let text = "";
3036
+ let quoted = false;
3037
+ let atTokenStart = true;
3038
+ let depth = 0;
3039
+ while (index < input.length) {
3040
+ const char = input[index];
3041
+ if (char === "\"") {
3042
+ if (atTokenStart) quoted = true;
3043
+ atTokenStart = false;
3044
+ index += 1;
3045
+ while (index < input.length && input[index] !== "\"") {
3046
+ text += input[index];
3047
+ index += 1;
3048
+ }
3049
+ index += 1;
3050
+ continue;
3051
+ }
3052
+ if (char === "(") depth += 1;
3053
+ if (char === ")") depth = Math.max(0, depth - 1);
3054
+ if (/\s/.test(char) && depth === 0) break;
3055
+ atTokenStart = false;
3056
+ text += char;
3057
+ index += 1;
2871
3058
  }
2872
- },
3059
+ if (text.length > 0 || quoted) tokens.push({
3060
+ text,
3061
+ at: start,
3062
+ quoted
3063
+ });
3064
+ }
3065
+ return tokens;
3066
+ }
3067
+ /** `(a or b)` → `["a", "b"]`; anything else is a single value. */
3068
+ function readValues(raw) {
3069
+ const trimmed = raw.trim();
3070
+ if (!trimmed.startsWith("(")) return [stripQuotes(trimmed)];
3071
+ return trimmed.replace(/^\(/, "").replace(/\)$/, "").split(/\s+or\s+/i).map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
3072
+ }
3073
+ function stripQuotes(value) {
3074
+ if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
3075
+ return value.replace(/^"/, "");
3076
+ }
3077
+ const OPERATORS = [
2873
3078
  {
2874
- label: "Last 7 days",
2875
- range: {
2876
- from: "now-7d",
2877
- to: "now"
2878
- }
3079
+ symbol: ">=",
3080
+ operator: "gte"
2879
3081
  },
2880
3082
  {
2881
- label: "Last 30 days",
2882
- range: {
2883
- from: "now-30d",
2884
- to: "now"
2885
- }
3083
+ symbol: "<=",
3084
+ operator: "lte"
2886
3085
  },
2887
3086
  {
2888
- label: "Last 90 days",
2889
- range: {
2890
- from: "now-90d",
2891
- to: "now"
2892
- }
3087
+ symbol: ":",
3088
+ operator: "eq"
2893
3089
  },
2894
3090
  {
2895
- label: "Today",
2896
- range: {
2897
- from: "now/d",
2898
- to: "now/d"
2899
- }
3091
+ symbol: ">",
3092
+ operator: "gt"
2900
3093
  },
2901
3094
  {
2902
- label: "This week",
2903
- range: {
2904
- from: "now/w",
2905
- to: "now/w"
2906
- }
3095
+ symbol: "<",
3096
+ operator: "lt"
2907
3097
  }
2908
3098
  ];
2909
- //#endregion
2910
- //#region src/shells/time-series-panel.tsx
2911
- const defaultLabels$3 = {
2912
- back: "Back to previous range",
2913
- legend: "Series"
3099
+ /** Where a field operator starts in the token, or `-1`. */
3100
+ function findOperator(text) {
3101
+ let best = null;
3102
+ for (const { symbol, operator } of OPERATORS) {
3103
+ const index = text.indexOf(symbol);
3104
+ if (index <= 0) continue;
3105
+ if (!best || index < best.index || index === best.index && symbol.length > best.symbol.length) best = {
3106
+ index,
3107
+ symbol,
3108
+ operator
3109
+ };
3110
+ }
3111
+ return best;
3112
+ }
3113
+ /** Parse, keeping whatever could not be understood as a plain term. */
3114
+ function parseQueryWithDiagnostics(input) {
3115
+ const diagnostics = [];
3116
+ const clauses = [];
3117
+ let trailing = null;
3118
+ for (const token of tokenize(input)) {
3119
+ let text = token.text;
3120
+ let negated = false;
3121
+ if (!token.quoted && text.startsWith("-") && text.length > 1) {
3122
+ negated = true;
3123
+ text = text.slice(1);
3124
+ }
3125
+ const operator = token.quoted ? null : findOperator(text);
3126
+ if (!operator) {
3127
+ if (text.length > 0) clauses.push({
3128
+ type: "term",
3129
+ value: text,
3130
+ negated
3131
+ });
3132
+ continue;
3133
+ }
3134
+ const field = text.slice(0, operator.index);
3135
+ const rawValue = text.slice(operator.index + operator.symbol.length);
3136
+ if (!FIELD_NAME.test(field)) {
3137
+ clauses.push({
3138
+ type: "term",
3139
+ value: text,
3140
+ negated
3141
+ });
3142
+ continue;
3143
+ }
3144
+ if (rawValue === "") {
3145
+ trailing = {
3146
+ field,
3147
+ operator: operator.operator,
3148
+ value: ""
3149
+ };
3150
+ continue;
3151
+ }
3152
+ if (rawValue === "*" && operator.operator === "eq") {
3153
+ clauses.push({
3154
+ type: "field",
3155
+ field,
3156
+ operator: "exists",
3157
+ values: [],
3158
+ negated
3159
+ });
3160
+ continue;
3161
+ }
3162
+ if (operator.operator === "eq" && rawValue.length > 2 && rawValue.startsWith("*") && rawValue.endsWith("*")) {
3163
+ clauses.push({
3164
+ type: "field",
3165
+ field,
3166
+ operator: "contains",
3167
+ values: [stripQuotes(rawValue.slice(1, -1))],
3168
+ negated
3169
+ });
3170
+ continue;
3171
+ }
3172
+ const values = readValues(rawValue);
3173
+ if (values.length === 0) {
3174
+ diagnostics.push({
3175
+ at: token.at,
3176
+ text: token.text,
3177
+ message: "Empty value group"
3178
+ });
3179
+ continue;
3180
+ }
3181
+ if (operator.operator !== "eq" && values.length > 1) diagnostics.push({
3182
+ at: token.at,
3183
+ text: token.text,
3184
+ message: "A comparison takes a single value"
3185
+ });
3186
+ const clause = {
3187
+ type: "field",
3188
+ field,
3189
+ operator: operator.operator,
3190
+ values: operator.operator === "eq" ? values : values.slice(0, 1),
3191
+ negated
3192
+ };
3193
+ clauses.push(clause);
3194
+ if (!rawValue.startsWith("(")) {
3195
+ var _values$;
3196
+ trailing = {
3197
+ field,
3198
+ operator: operator.operator,
3199
+ value: (_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : ""
3200
+ };
3201
+ }
3202
+ }
3203
+ return {
3204
+ query: { clauses },
3205
+ diagnostics,
3206
+ trailing
3207
+ };
3208
+ }
3209
+ /** Parse, discarding diagnostics. */
3210
+ function parseQuery(input) {
3211
+ return parseQueryWithDiagnostics(input).query;
3212
+ }
3213
+ //#endregion
3214
+ //#region src/lib/query/query-format.ts
3215
+ const OPERATOR_SYMBOL = {
3216
+ eq: ":",
3217
+ contains: ":",
3218
+ gt: ">",
3219
+ gte: ">=",
3220
+ lt: "<",
3221
+ lte: "<=",
3222
+ exists: ":"
3223
+ };
3224
+ /** Quote a value that would otherwise re-parse as something else. */
3225
+ function quoteValue(value) {
3226
+ return /[\s"()]/.test(value) || value === "" ? `"${value.replace(/"/g, "")}"` : value;
3227
+ }
3228
+ /** One clause as text. */
3229
+ function formatClause(clause) {
3230
+ var _clause$values$2;
3231
+ const prefix = clause.negated ? "-" : "";
3232
+ if (clause.type === "term") return `${prefix}${quoteValue(clause.value)}`;
3233
+ if (clause.operator === "exists") return `${prefix}${clause.field}:*`;
3234
+ if (clause.operator === "contains") {
3235
+ var _clause$values$;
3236
+ return `${prefix}${clause.field}:*${(_clause$values$ = clause.values[0]) !== null && _clause$values$ !== void 0 ? _clause$values$ : ""}*`;
3237
+ }
3238
+ const symbol = OPERATOR_SYMBOL[clause.operator];
3239
+ if (clause.values.length > 1) return `${prefix}${clause.field}${symbol}(${clause.values.map(quoteValue).join(" or ")})`;
3240
+ return `${prefix}${clause.field}${symbol}${quoteValue((_clause$values$2 = clause.values[0]) !== null && _clause$values$2 !== void 0 ? _clause$values$2 : "")}`;
3241
+ }
3242
+ /** The whole query as text. */
3243
+ function formatQuery(query) {
3244
+ return query.clauses.map(formatClause).join(" ");
3245
+ }
3246
+ /** A short human label for a clause chip. */
3247
+ function describeClause(clause) {
3248
+ if (clause.type === "term") return clause.negated ? `not "${clause.value}"` : `"${clause.value}"`;
3249
+ if (clause.operator === "exists") return clause.negated ? `${clause.field} missing` : `${clause.field} present`;
3250
+ if (clause.operator === "contains") {
3251
+ var _clause$values$3;
3252
+ return `${clause.negated ? "not " : ""}${clause.field} ⊃ ${(_clause$values$3 = clause.values[0]) !== null && _clause$values$3 !== void 0 ? _clause$values$3 : ""}`;
3253
+ }
3254
+ const values = clause.values.join(", ");
3255
+ const symbol = OPERATOR_SYMBOL[clause.operator];
3256
+ return `${clause.negated ? "not " : ""}${clause.field} ${symbol === ":" ? "=" : symbol} ${values}`;
3257
+ }
3258
+ //#endregion
3259
+ //#region src/shells/query-bar.tsx
3260
+ const defaultLabels$4 = {
3261
+ placeholder: "Search, or filter with field:value",
3262
+ ariaLabel: "Search query",
3263
+ clear: "Clear query",
3264
+ removeClause: (clause) => `Remove ${clause}`,
3265
+ fieldsHeading: "Fields",
3266
+ valuesHeading: "Values",
3267
+ syntaxHint: null
2914
3268
  };
3269
+ const MAX_SUGGESTIONS = 8;
2915
3270
  /**
2916
- * TimeSeriesPanela chart you can steer.
3271
+ * QueryBarone control for "find this" and "only these".
2917
3272
  *
2918
- * The point of putting the control and the chart in one component is the
2919
- * loop between them: drag across the plot and the window narrows to what you
2920
- * dragged, which re-queries, which redraws the axis you just picked. That
2921
- * only works if someone owns the step back out again so zoom keeps a
2922
- * history, and the header offers the way back rather than leaving the reader
2923
- * to retype the range they came from.
3273
+ * A search box and a row of filter pickers answer halves of the same question
3274
+ * and neither can express the other, so people end up using both and holding
3275
+ * the combination in their head. Typing the filter is what closes that: the
3276
+ * whole question is one string, which means it can be read back, corrected in
3277
+ * place, pasted to a colleague, and stored in a URL.
3278
+ *
3279
+ * The text is the source of truth while the reader is typing; the parsed query
3280
+ * is committed on Enter or blur. Anything half-written parses to a plain term
3281
+ * rather than an error, so the bar never argues with someone mid-word.
2924
3282
  *
2925
3283
  * @example
2926
3284
  * ```tsx
2927
- * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
2928
- *
2929
- * <TimeSeriesPanel
2930
- * title="Sessions"
2931
- * data={points}
2932
- * series={[{ dataKey: "sessions", name: "Sessions" }]}
2933
- * range={range}
2934
- * onRangeChange={setRange}
2935
- * onRefresh={() => refetch()}
2936
- * toggleableLegend
3285
+ * <QueryBar
3286
+ * value={query}
3287
+ * onChange={setQuery}
3288
+ * fields={[{ name: "path", values: ["/checkout", "/cart"] }]}
3289
+ * showClauses
2937
3290
  * />
2938
3291
  * ```
2939
3292
  */
2940
- function TimeSeriesPanel({ title, description, actions, data, series, range, onRangeChange, granularity = "auto", timeZone, locale, showRangeControl = true, presets, recentRanges, refresh, onRefreshChange, onRefresh, toggleableLegend = false, annotations, isLoading = false, height = 280, className, chartProps, labels: customLabels, "aria-label": ariaLabel }) {
2941
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$3), customLabels);
2942
- const [history, setHistory] = useState([]);
2943
- const [hidden, setHidden] = useState([]);
2944
- const resolved = useMemo(() => resolveTimeRange(range, { timeZone }), [range, timeZone]);
2945
- const visibleSeries = useMemo(() => series.filter((def) => !hidden.includes(def.dataKey)), [series, hidden]);
2946
- const handleRangeSelect = useCallback((selection) => {
2947
- setHistory((current) => [...current, range].slice(-10));
2948
- onRangeChange(toAbsoluteRange(selection));
2949
- }, [range, onRangeChange]);
2950
- const goBack = useCallback(() => {
2951
- setHistory((current) => {
2952
- const previous = current[current.length - 1];
2953
- if (previous) onRangeChange(previous);
2954
- return current.slice(0, -1);
2955
- });
2956
- }, [onRangeChange]);
2957
- const setRange = useCallback((next) => {
2958
- setHistory([]);
2959
- onRangeChange(next);
2960
- }, [onRangeChange]);
2961
- const toggleSeries = useCallback((dataKey) => {
2962
- setHidden((current) => current.includes(dataKey) ? current.filter((key) => key !== dataKey) : [...current, dataKey]);
2963
- }, []);
2964
- return /* @__PURE__ */ jsxs("section", {
2965
- className: cn(OPT_SURFACE, OPT_BORDER, "flex flex-col", className),
2966
- "aria-label": ariaLabel,
2967
- "data-opt-id": "Y0XCY",
2968
- "data-opt-slug": "time-series-panel.TimeSeriesPanel",
2969
- children: [/* @__PURE__ */ jsxs("header", {
2970
- className: "gap-group flex flex-wrap items-start justify-between px-5 pt-5 pb-3",
2971
- children: [/* @__PURE__ */ jsxs("div", {
2972
- className: "gap-element flex min-w-0 flex-col",
2973
- children: [title && /* @__PURE__ */ jsx("h3", {
2974
- className: "text-text-primary truncate text-base font-semibold",
2975
- children: title
2976
- }), description && /* @__PURE__ */ jsx("p", {
2977
- className: cn("text-text-secondary", "truncate text-sm"),
2978
- children: description
2979
- })]
2980
- }), /* @__PURE__ */ jsxs("div", {
2981
- className: "gap-element flex flex-wrap items-center",
2982
- children: [
2983
- history.length > 0 && /* @__PURE__ */ jsx(Button, {
2984
- variant: "ghost",
2985
- size: "sm",
2986
- onClick: goBack,
2987
- leftIcon: /* @__PURE__ */ jsx(Undo2, {
2988
- className: "h-3.5 w-3.5",
2989
- "aria-hidden": "true"
2990
- }),
2991
- children: labels.back
2992
- }),
2993
- actions,
2994
- showRangeControl && /* @__PURE__ */ jsx(TimeRangeControl, {
2995
- value: range,
2996
- onChange: setRange,
2997
- presets,
2998
- recentRanges,
2999
- timeZone,
3000
- locale,
3001
- refresh,
3002
- onRefreshChange,
3003
- onRefresh,
3004
- isLoading,
3005
- labels: customLabels === null || customLabels === void 0 ? void 0 : customLabels.control
3293
+ function QueryBar({ value, onChange, fields = [], showClauses = false, live = false, disabled = false, className, labels: customLabels }) {
3294
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$4), customLabels);
3295
+ const inputRef = useRef(null);
3296
+ const [text, setText] = useState(() => formatQuery(value));
3297
+ const [focused, setFocused] = useState(false);
3298
+ const [highlighted, setHighlighted] = useState(0);
3299
+ const committedRef = useRef(formatQuery(value));
3300
+ useEffect(() => {
3301
+ const next = formatQuery(value);
3302
+ if (next === committedRef.current) return;
3303
+ committedRef.current = next;
3304
+ if (!focused) setText(next);
3305
+ }, [value, focused]);
3306
+ const parsed = useMemo(() => parseQueryWithDiagnostics(text), [text]);
3307
+ const commit = useCallback((nextText) => {
3308
+ const nextQuery = parseQueryWithDiagnostics(nextText).query;
3309
+ committedRef.current = formatQuery(nextQuery);
3310
+ onChange(nextQuery);
3311
+ }, [onChange]);
3312
+ const suggestions = useSuggestions(text, parsed.trailing, fields);
3313
+ const applySuggestion = useCallback((suggestion) => {
3314
+ var _inputRef$current;
3315
+ const nextText = suggestion.replace(text);
3316
+ setText(nextText);
3317
+ if (suggestion.kind === "value") commit(nextText);
3318
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
3319
+ setHighlighted(0);
3320
+ }, [text, commit]);
3321
+ const handleKeyDown = (event) => {
3322
+ if (suggestions.length > 0 && focused) {
3323
+ if (event.key === "ArrowDown") {
3324
+ event.preventDefault();
3325
+ setHighlighted((current) => (current + 1) % suggestions.length);
3326
+ return;
3327
+ }
3328
+ if (event.key === "ArrowUp") {
3329
+ event.preventDefault();
3330
+ setHighlighted((current) => (current - 1 + suggestions.length) % suggestions.length);
3331
+ return;
3332
+ }
3333
+ if (event.key === "Tab" || event.key === "Enter" && highlighted > 0) {
3334
+ event.preventDefault();
3335
+ applySuggestion(suggestions[highlighted]);
3336
+ return;
3337
+ }
3338
+ }
3339
+ if (event.key === "Enter") {
3340
+ event.preventDefault();
3341
+ commit(text);
3342
+ return;
3343
+ }
3344
+ if (event.key === "Escape") {
3345
+ var _inputRef$current2;
3346
+ setText(committedRef.current);
3347
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
3348
+ }
3349
+ };
3350
+ const showSuggestions = focused && suggestions.length > 0;
3351
+ return /* @__PURE__ */ jsxs("div", {
3352
+ className: cn("relative flex min-w-0 flex-col gap-1.5", className),
3353
+ "data-opt-id": "JSVZ1",
3354
+ "data-opt-slug": "query-bar.QueryBar",
3355
+ children: [
3356
+ /* @__PURE__ */ jsxs("div", {
3357
+ className: cn("bg-surface flex h-9 items-center gap-2 rounded-md border px-2.5", OPT_BORDER, "focus-within:border-accent transition-colors", disabled && "opacity-60"),
3358
+ children: [
3359
+ /* @__PURE__ */ jsx(Search, {
3360
+ className: "text-text-tertiary size-4 shrink-0",
3361
+ "aria-hidden": true
3362
+ }),
3363
+ /* @__PURE__ */ jsx("input", {
3364
+ ref: inputRef,
3365
+ type: "text",
3366
+ role: "combobox",
3367
+ "aria-expanded": showSuggestions,
3368
+ "aria-controls": "query-bar-suggestions",
3369
+ "aria-autocomplete": "list",
3370
+ "aria-label": labels.ariaLabel,
3371
+ placeholder: labels.placeholder,
3372
+ disabled,
3373
+ className: cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent font-mono text-sm outline-none focus-visible:outline-none"),
3374
+ value: text,
3375
+ onChange: (event) => {
3376
+ setText(event.target.value);
3377
+ setHighlighted(0);
3378
+ if (live) commit(event.target.value);
3379
+ },
3380
+ onFocus: () => setFocused(true),
3381
+ onBlur: () => {
3382
+ window.setTimeout(() => setFocused(false), 120);
3383
+ if (!live) commit(text);
3384
+ },
3385
+ onKeyDown: handleKeyDown
3386
+ }),
3387
+ text.length > 0 && /* @__PURE__ */ jsx("button", {
3388
+ type: "button",
3389
+ "aria-label": labels.clear,
3390
+ title: labels.clear,
3391
+ className: cn("text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5", "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset"),
3392
+ onClick: () => {
3393
+ setText("");
3394
+ commit("");
3395
+ },
3396
+ children: /* @__PURE__ */ jsx(X, {
3397
+ className: "size-3.5",
3398
+ "aria-hidden": true
3399
+ })
3006
3400
  })
3007
3401
  ]
3008
- })]
3009
- }), /* @__PURE__ */ jsx("div", {
3010
- className: "px-2 pb-4",
3011
- children: /* @__PURE__ */ jsx(TimeSeriesChart, _objectSpread2({
3012
- data,
3013
- series: visibleSeries,
3014
- from: resolved === null || resolved === void 0 ? void 0 : resolved.from,
3015
- to: resolved === null || resolved === void 0 ? void 0 : resolved.to,
3016
- granularity,
3017
- timeZone,
3018
- locale,
3019
- height,
3020
- selectable: true,
3021
- onRangeSelect: handleRangeSelect,
3022
- annotations,
3023
- showLegend: series.length > 1,
3024
- legendOptions: toggleableLegend ? {
3025
- onClick: toggleSeries,
3026
- inactiveKeys: hidden
3027
- } : void 0,
3028
- "aria-label": typeof title === "string" ? `${title} time series` : ariaLabel
3029
- }, chartProps))
3030
- })]
3402
+ }),
3403
+ showSuggestions && /* @__PURE__ */ jsx("ul", {
3404
+ id: "query-bar-suggestions",
3405
+ role: "listbox",
3406
+ className: cn("bg-surface-overlay absolute top-10 z-40 max-h-64 w-full overflow-y-auto rounded-md border p-1 shadow-lg", "border border-border"),
3407
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
3408
+ type: "button",
3409
+ role: "option",
3410
+ "aria-selected": index === highlighted,
3411
+ className: cn("flex w-full cursor-pointer items-center justify-between gap-3 rounded px-2 py-1 text-left text-sm", index === highlighted ? "bg-bg-subtle" : "hover:bg-bg-subtle"),
3412
+ onMouseEnter: () => setHighlighted(index),
3413
+ onMouseDown: (event) => event.preventDefault(),
3414
+ onClick: () => applySuggestion(suggestion),
3415
+ children: [/* @__PURE__ */ jsx("span", {
3416
+ className: "text-text-primary truncate font-mono",
3417
+ children: suggestion.label
3418
+ }), suggestion.hint && /* @__PURE__ */ jsx("span", {
3419
+ className: "text-text-tertiary shrink-0 text-xs",
3420
+ children: suggestion.hint
3421
+ })]
3422
+ }) }, `${suggestion.kind}-${suggestion.label}`))
3423
+ }),
3424
+ showClauses && value.clauses.length > 0 && /* @__PURE__ */ jsx("ul", {
3425
+ className: "flex flex-wrap items-center gap-1.5",
3426
+ children: value.clauses.map((clause, index) => {
3427
+ const label = describeClause(clause);
3428
+ return /* @__PURE__ */ jsxs("li", {
3429
+ className: cn("bg-bg-subtle text-text-secondary flex items-center gap-1 rounded px-1.5 py-0.5 text-xs", clause.negated && "text-danger-fg"),
3430
+ children: [/* @__PURE__ */ jsx("span", {
3431
+ className: "truncate",
3432
+ children: label
3433
+ }), /* @__PURE__ */ jsx("button", {
3434
+ type: "button",
3435
+ "aria-label": labels.removeClause(label),
3436
+ className: "hover:text-text-primary cursor-pointer",
3437
+ onClick: () => {
3438
+ const next = removeClause(value, index);
3439
+ setText(formatQuery(next));
3440
+ committedRef.current = formatQuery(next);
3441
+ onChange(next);
3442
+ },
3443
+ children: /* @__PURE__ */ jsx(X, {
3444
+ className: "size-3",
3445
+ "aria-hidden": true
3446
+ })
3447
+ })]
3448
+ }, `${label}-${index}`);
3449
+ })
3450
+ }),
3451
+ parsed.diagnostics.length > 0 && /* @__PURE__ */ jsx(QueryDiagnostics, { diagnostics: parsed.diagnostics }),
3452
+ labels.syntaxHint && !showSuggestions && /* @__PURE__ */ jsx("p", {
3453
+ className: "text-text-tertiary text-xs",
3454
+ children: labels.syntaxHint
3455
+ })
3456
+ ]
3031
3457
  });
3032
3458
  }
3033
- TimeSeriesPanel.displayName = "TimeSeriesPanel";
3034
- //#endregion
3035
- //#region src/shells/log-table.tsx
3036
- const HIDE_BELOW = {
3037
- always: "",
3038
- sm: "hidden @[40rem]/log:flex",
3039
- md: "hidden @[46rem]/log:flex",
3040
- lg: "hidden @[58rem]/log:flex",
3041
- xl: "hidden @[70rem]/log:flex"
3042
- };
3043
- const ROW_HEIGHT = {
3044
- compact: 26,
3045
- normal: 32,
3046
- comfortable: 40
3047
- };
3048
- const DENSITY_TEXT = {
3049
- compact: "text-[11px]",
3050
- normal: "text-xs",
3051
- comfortable: "text-sm"
3052
- };
3053
- const defaultLabels$2 = {
3054
- listLabel: "Log",
3055
- loadingMore: "Loading earlier entries…",
3056
- endOfResults: "That is every entry in this range.",
3057
- filterFor: (value) => `Filter for ${value}`,
3058
- filterOut: (value) => `Filter out ${value}`,
3059
- sortAscending: "Sort ascending",
3060
- sortDescending: "Sort descending",
3061
- moveLeft: (column) => `Move ${column} left`,
3062
- moveRight: (column) => `Move ${column} right`
3063
- };
3064
- /** Rows rendered beyond the viewport on each side, so a fast scroll stays filled. */
3065
- const OVERSCAN = 10;
3066
- /**
3067
- * A stable hue per value. Colour does real work in a log — the eye groups by it
3068
- * long before it reads the text — so the same event name has to land on the
3069
- * same hue on every render and in every session, which rules out an index into
3070
- * a palette.
3071
- */
3072
- function hueOf(value) {
3073
- let hash = 0;
3074
- for (let index = 0; index < value.length; index += 1) hash = hash * 31 + value.charCodeAt(index) | 0;
3075
- return Math.abs(hash) % 360;
3459
+ QueryBar.displayName = "QueryBar";
3460
+ function QueryDiagnostics({ diagnostics }) {
3461
+ return /* @__PURE__ */ jsx("ul", {
3462
+ className: "text-warning-fg flex flex-col gap-0.5 text-xs",
3463
+ children: diagnostics.map((diagnostic, index) => /* @__PURE__ */ jsxs("li", { children: [
3464
+ /* @__PURE__ */ jsx("code", {
3465
+ className: "font-mono",
3466
+ children: diagnostic.text
3467
+ }),
3468
+ " —",
3469
+ " ",
3470
+ diagnostic.message
3471
+ ] }, `${diagnostic.at}-${index}`))
3472
+ });
3076
3473
  }
3077
- const EM_DASH = "—";
3078
3474
  /**
3079
- * A column with nothing in it is a fact about the entry, not a value. Dimming
3080
- * every absence the same way lets the eye skip the gaps while scanning, instead
3081
- * of reading a dash as though it were data.
3475
+ * What to offer, based on the token under the cursor.
3476
+ *
3477
+ * Completing fields is the part that makes the syntax discoverable at all —
3478
+ * nobody guesses that `utm_source` is a field they can filter on. Once the
3479
+ * field is chosen, its known values are the second half of the same guess.
3082
3480
  */
3083
- const ABSENT = "text-text-tertiary/50";
3084
- function columnStyle(column) {
3085
- return column.width ? {
3086
- width: column.width,
3087
- flex: "0 0 auto"
3088
- } : {
3089
- flex: "1 1 0%",
3090
- minWidth: 0
3091
- };
3481
+ function useSuggestions(text, trailing, fields) {
3482
+ return useMemo(() => {
3483
+ const lastSpace = text.lastIndexOf(" ");
3484
+ const head = lastSpace === -1 ? "" : text.slice(0, lastSpace + 1);
3485
+ const token = text.slice(lastSpace + 1);
3486
+ if (trailing) {
3487
+ var _field$values;
3488
+ const field = fields.find((candidate) => candidate.name === trailing.field);
3489
+ const values = (_field$values = field === null || field === void 0 ? void 0 : field.values) !== null && _field$values !== void 0 ? _field$values : [];
3490
+ const typed = trailing.value.toLowerCase();
3491
+ return values.filter((value) => value.toLowerCase().includes(typed)).slice(0, MAX_SUGGESTIONS).map((value) => ({
3492
+ kind: "value",
3493
+ label: `${trailing.field}:${value}`,
3494
+ replace: () => `${head}${trailing.field}:${/[\s"()]/.test(value) ? `"${value}"` : value} `
3495
+ }));
3496
+ }
3497
+ const bare = token.replace(/^-/, "").toLowerCase();
3498
+ if (bare.length === 0) return [];
3499
+ return fields.filter((field) => field.name.toLowerCase().includes(bare)).slice(0, MAX_SUGGESTIONS).map((field) => ({
3500
+ kind: "field",
3501
+ label: `${field.name}:`,
3502
+ hint: field.hint,
3503
+ replace: () => `${head}${token.startsWith("-") ? "-" : ""}${field.name}:`
3504
+ }));
3505
+ }, [
3506
+ text,
3507
+ trailing,
3508
+ fields
3509
+ ]);
3092
3510
  }
3511
+ //#endregion
3512
+ //#region src/lib/use-auto-refresh.ts
3093
3513
  /**
3094
- * LogTable a windowed table for entries that keep arriving.
3514
+ * A refresh timer that waits for its own work.
3095
3515
  *
3096
- * Written by hand rather than delegated to a data grid for the two things a log
3097
- * needs that a grid does not: rows are inserted at the *top* while the reader
3098
- * is partway down, which needs a scroll correction applied before paint, and
3099
- * "am I at the newest row?" has to be answerable from the scroll container to
3100
- * decide whether to follow the tail or hold position.
3516
+ * A plain `setInterval` fires on a schedule regardless of whether the previous
3517
+ * query came back, so a five-second interval over a seven-second query stacks
3518
+ * requests until something gives. Chaining the next wait onto the settled
3519
+ * promise keeps exactly one refresh in flight, and a rejection stops the loop
3520
+ * instead of hammering an endpoint that is already failing.
3521
+ */
3522
+ function useAutoRefresh({ interval, paused = false, onRefresh }) {
3523
+ const callbackRef = useRef(onRefresh);
3524
+ callbackRef.current = onRefresh;
3525
+ useEffect(() => {
3526
+ if (paused || interval <= 0 || !callbackRef.current) return;
3527
+ let cancelled = false;
3528
+ let timer;
3529
+ const schedule = () => {
3530
+ timer = setTimeout(async () => {
3531
+ if (cancelled) return;
3532
+ try {
3533
+ var _callbackRef$current;
3534
+ await ((_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef));
3535
+ } catch (_unused) {
3536
+ return;
3537
+ }
3538
+ if (!cancelled) schedule();
3539
+ }, interval);
3540
+ };
3541
+ schedule();
3542
+ return () => {
3543
+ cancelled = true;
3544
+ if (timer) clearTimeout(timer);
3545
+ };
3546
+ }, [interval, paused]);
3547
+ }
3548
+ //#endregion
3549
+ //#region src/shells/time-range-control.tsx
3550
+ const defaultLabels$3 = {
3551
+ trigger: "Select time range",
3552
+ invalid: "Invalid time range",
3553
+ last: (duration) => `Last ${duration}`,
3554
+ next: (duration) => `Next ${duration}`,
3555
+ quickSelect: "Quick select",
3556
+ apply: "Apply",
3557
+ update: "Update",
3558
+ refresh: "Refresh",
3559
+ commonlyUsed: "Commonly used",
3560
+ recentlyUsed: "Recently used",
3561
+ start: "Start",
3562
+ end: "End",
3563
+ startHint: "Absolute time or date math, e.g. now-7d",
3564
+ autoRefresh: "Auto refresh",
3565
+ autoRefreshOff: "Off",
3566
+ every: (duration) => `Every ${duration}`,
3567
+ previous: "Previous window",
3568
+ nextWindow: "Next window",
3569
+ zoomIn: "Zoom in",
3570
+ zoomOut: "Zoom out",
3571
+ thisUnit: (unit) => `This ${unit}`,
3572
+ units: {
3573
+ s: "seconds",
3574
+ m: "minutes",
3575
+ h: "hours",
3576
+ d: "days",
3577
+ w: "weeks",
3578
+ M: "months",
3579
+ y: "years"
3580
+ }
3581
+ };
3582
+ const QUICK_UNITS = [
3583
+ "m",
3584
+ "h",
3585
+ "d",
3586
+ "w",
3587
+ "M",
3588
+ "y"
3589
+ ];
3590
+ const REFRESH_INTERVALS = [
3591
+ 0,
3592
+ 5e3,
3593
+ 1e4,
3594
+ 3e4,
3595
+ 6e4,
3596
+ 3e5
3597
+ ];
3598
+ function selectValue(value) {
3599
+ var _value$;
3600
+ return Array.isArray(value) ? (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : "" : value;
3601
+ }
3602
+ /**
3603
+ * What the trigger says.
3101
3604
  *
3102
- * What it takes from a data grid is the part readers actually reach for:
3103
- * columns are data, so a caller can let people add, drop and reorder them; a
3104
- * cell offers to filter for or against its own value; and density is a setting
3105
- * rather than a constant.
3605
+ * A window anchored to `now` deserves its intent ("Last 7 days"), not two
3606
+ * timestamps the reader has to subtract. Everything else gets both ends
3607
+ * spelled out. A duration that does not divide into whole minutes is prefixed
3608
+ * with `~` rather than rounded silently.
3609
+ */
3610
+ function describeRange(range, labels, options) {
3611
+ const window = measureTimeWindow(range, {
3612
+ timeZone: options.timeZone,
3613
+ now: options.now
3614
+ });
3615
+ if (window.isInvalid) return {
3616
+ text: labels.invalid,
3617
+ isInvalid: true
3618
+ };
3619
+ const fromParts = parseRelativeParts(range.from);
3620
+ const toParts = parseRelativeParts(range.to);
3621
+ if ((fromParts === null || fromParts === void 0 ? void 0 : fromParts.roundUnit) && fromParts.count === 0 && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === fromParts.roundUnit && toParts.count === 0) return {
3622
+ text: labels.thisUnit(formatCountUnit(1, fromParts.roundUnit, options.locale).replace(/^1\s*/, "")),
3623
+ isInvalid: false
3624
+ };
3625
+ if (isRelativeToNow(range)) {
3626
+ var _toParts$count;
3627
+ const anchoredAtEnd = ((_toParts$count = toParts === null || toParts === void 0 ? void 0 : toParts.count) !== null && _toParts$count !== void 0 ? _toParts$count : 0) === 0;
3628
+ const parts = anchoredAtEnd ? fromParts : toParts;
3629
+ const bothSnapped = (parts === null || parts === void 0 ? void 0 : parts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit) && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit);
3630
+ const counted = parts && parts.count > 0 ? formatCountUnit(parts.roundUnit && bothSnapped ? parts.count + 1 : parts.count, parts.unit, options.locale) : null;
3631
+ const exact = counted !== null && (!(parts === null || parts === void 0 ? void 0 : parts.roundUnit) || bothSnapped);
3632
+ const duration = exact ? counted : formatDuration(window.duration, options.locale);
3633
+ const prefix = exact || isExactMinutes(window.duration) ? "" : "~";
3634
+ return {
3635
+ text: anchoredAtEnd ? labels.last(`${prefix}${duration}`) : labels.next(`${prefix}${duration}`),
3636
+ isInvalid: false
3637
+ };
3638
+ }
3639
+ const dateOnly = isDateOnly(range.from) && isDateOnly(range.to);
3640
+ const from = formatInstant(window.from, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
3641
+ const to = formatInstant(window.to, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
3642
+ return {
3643
+ text: from === to ? from : `${from} – ${to}`,
3644
+ isInvalid: false
3645
+ };
3646
+ }
3647
+ /**
3648
+ * TimeRangeControl — the window as an expression, not two frozen instants.
3649
+ *
3650
+ * Holding `{ from: "now-7d", to: "now" }` instead of a resolved pair is what
3651
+ * makes a saved board, a shared link and an auto-refresh timer all mean the
3652
+ * same thing tomorrow that they mean today. Around that sit the moves people
3653
+ * actually make on a chart: jump to a named window, step the same width back
3654
+ * and forth to compare, widen when the interesting thing is just off screen,
3655
+ * and let it re-query on a timer while they watch.
3106
3656
  *
3107
3657
  * @example
3108
3658
  * ```tsx
3109
- * <LogTable
3110
- * rows={rows}
3111
- * getRowId={(row) => row.id}
3112
- * columns={[
3113
- * { id: "time", header: "Time", width: "9rem", mono: true, value: (r) => r.time },
3114
- * { id: "name", header: "Event", width: "14rem", field: "name", marker: true, value: (r) => r.name },
3115
- * { id: "path", header: "Path", field: "path", value: (r) => r.path },
3116
- * ]}
3117
- * onCellAction={(action) => setQuery(applyCellAction(query, action))}
3659
+ * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
3660
+ *
3661
+ * <TimeRangeControl
3662
+ * value={range}
3663
+ * onChange={setRange}
3664
+ * timeZone="Asia/Seoul"
3665
+ * showWindowButtons
3666
+ * onRefresh={() => refetch()}
3118
3667
  * />
3119
3668
  * ```
3120
3669
  */
3121
- function LogTableInner({ rows, columns, getRowId, selectedId = null, onSelect, onLoadMore, hasMore = false, loadingMore = false, prepending = false, onFollowChange, isNew, sort = null, onSortChange, onCellAction, highlightTerms, onColumnMove, density = "normal", showHeader = true, emptyState, labels: customLabels, className }, ref) {
3122
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$2), customLabels);
3123
- const rowHeight = ROW_HEIGHT[density];
3124
- const scrollRef = useRef(null);
3125
- const [scrollTop, setScrollTop] = useState(0);
3126
- const [viewportHeight, setViewportHeight] = useState(0);
3127
- const followingRef = useRef(true);
3128
- const previousTopIdRef = useRef(null);
3129
- const previousLengthRef = useRef(0);
3130
- useImperativeHandle(ref, () => ({
3131
- scrollToTop: () => {
3132
- const container = scrollRef.current;
3133
- if (!container) return;
3134
- container.scrollTop = 0;
3135
- setScrollTop(0);
3136
- if (!followingRef.current) {
3137
- followingRef.current = true;
3138
- onFollowChange === null || onFollowChange === void 0 || onFollowChange(true);
3139
- }
3140
- },
3141
- revealRow: (id) => {
3142
- const index = rows.findIndex((row) => getRowId(row) === id);
3143
- const container = scrollRef.current;
3144
- if (index === -1 || !container) return;
3145
- const top = index * rowHeight;
3146
- const bottom = top + rowHeight;
3147
- if (top < container.scrollTop) container.scrollTop = top;
3148
- else if (bottom > container.scrollTop + container.clientHeight) container.scrollTop = bottom - container.clientHeight;
3149
- }
3670
+ function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, locale, showUpdateButton = true, showWindowButtons = true, zoomFactor = .5, refresh, onRefreshChange, onRefresh, isLoading = false, disabled = false, className, labels: customLabels, "aria-label": ariaLabel }) {
3671
+ var _refresh$interval, _refresh$paused, _refresh$interval2;
3672
+ const labels = _objectSpread2(_objectSpread2(_objectSpread2({}, defaultLabels$3), customLabels), {}, { units: _objectSpread2(_objectSpread2({}, defaultLabels$3.units), customLabels === null || customLabels === void 0 ? void 0 : customLabels.units) });
3673
+ const [open, setOpen] = useState(false);
3674
+ const [draft, setDraft] = useState(null);
3675
+ const [quickCount, setQuickCount] = useState("15");
3676
+ const [quickUnit, setQuickUnit] = useState("m");
3677
+ const editing = draft !== null && draft !== void 0 ? draft : value;
3678
+ const hasChanges = draft !== null && (draft.from !== value.from || draft.to !== value.to);
3679
+ const description = useMemo(() => describeRange(value, labels, {
3680
+ locale,
3681
+ timeZone
3150
3682
  }), [
3151
- onFollowChange,
3152
- rows,
3153
- getRowId,
3154
- rowHeight
3683
+ value,
3684
+ locale,
3685
+ timeZone,
3686
+ customLabels
3155
3687
  ]);
3156
- useEffect(() => {
3157
- const container = scrollRef.current;
3158
- if (!container) return;
3159
- const observer = new ResizeObserver(() => setViewportHeight(container.clientHeight));
3160
- observer.observe(container);
3161
- setViewportHeight(container.clientHeight);
3162
- return () => observer.disconnect();
3163
- }, []);
3164
- /**
3165
- * Report whether the list is pinned to the newest row.
3166
- *
3167
- * Called from more than the scroll handler on purpose: the list can arrive at
3168
- * the top without a scroll event — a reset, a jump to newest, a shorter
3169
- * result set — and a stale "not following" would silently divert every live
3170
- * row into a held-back pile.
3171
- */
3172
- const syncFollowing = useCallback((container) => {
3173
- const following = container.scrollTop <= rowHeight;
3174
- if (following === followingRef.current) return;
3175
- followingRef.current = following;
3176
- onFollowChange === null || onFollowChange === void 0 || onFollowChange(following);
3177
- }, [onFollowChange, rowHeight]);
3178
- /**
3179
- * Hold the reader's place when rows arrive above them.
3180
- *
3181
- * Runs before paint: by the time the browser draws, the scroll offset has
3182
- * already absorbed the inserted height, so the content under the cursor never
3183
- * moves. Skipped while following, where the whole point is to stay at the top.
3184
- */
3185
- useLayoutEffect(() => {
3186
- const container = scrollRef.current;
3187
- const previousTopId = previousTopIdRef.current;
3188
- previousTopIdRef.current = rows.length ? getRowId(rows[0]) : null;
3189
- const previousLength = previousLengthRef.current;
3190
- previousLengthRef.current = rows.length;
3191
- if (!container) return;
3192
- syncFollowing(container);
3193
- if (!prepending || followingRef.current || !previousTopId) return;
3194
- if (rows.length <= previousLength) return;
3195
- const prepended = rows.findIndex((row) => getRowId(row) === previousTopId);
3196
- if (prepended <= 0) return;
3197
- container.scrollTop += prepended * rowHeight;
3198
- setScrollTop(container.scrollTop);
3688
+ const boundState = useCallback((expression) => parseTimeExpression(expression, { timeZone }) === null ? labels.invalid : void 0, [timeZone, labels]);
3689
+ const commit = useCallback((range) => {
3690
+ setDraft(null);
3691
+ onChange(range);
3692
+ }, [onChange]);
3693
+ const apply = useCallback(() => {
3694
+ if (draft) commit(draft);
3695
+ else onRefresh === null || onRefresh === void 0 || onRefresh();
3199
3696
  }, [
3200
- rows,
3201
- getRowId,
3202
- prepending,
3203
- rowHeight,
3204
- syncFollowing
3697
+ draft,
3698
+ commit,
3699
+ onRefresh
3205
3700
  ]);
3206
- /**
3207
- * Page forward when the rows do not reach the bottom of the viewport.
3208
- *
3209
- * Without this the list only advances on a scroll event, so a tall screen —
3210
- * or a filter matching few rows per page — stops at the first page with no
3211
- * way to ask for more.
3212
- */
3213
- useEffect(() => {
3214
- const container = scrollRef.current;
3215
- if (!container || !hasMore || loadingMore || !onLoadMore) return;
3216
- if (container.scrollHeight <= container.clientHeight) onLoadMore();
3701
+ const commitOnEnter = useCallback((event) => {
3702
+ if (event.key !== "Enter") return;
3703
+ apply();
3704
+ setOpen(false);
3705
+ }, [apply]);
3706
+ const applyQuick = useCallback(() => {
3707
+ const count = Number(quickCount);
3708
+ if (!Number.isFinite(count) || count <= 0) return;
3709
+ commit({
3710
+ from: `now-${Math.round(count)}${quickUnit}`,
3711
+ to: "now"
3712
+ });
3713
+ setOpen(false);
3217
3714
  }, [
3218
- hasMore,
3219
- loadingMore,
3220
- onLoadMore,
3221
- rows.length,
3222
- viewportHeight
3715
+ quickCount,
3716
+ quickUnit,
3717
+ commit
3223
3718
  ]);
3224
- const handleScroll = useCallback((event) => {
3225
- const container = event.currentTarget;
3226
- setScrollTop(container.scrollTop);
3227
- syncFollowing(container);
3228
- const remaining = container.scrollHeight - container.scrollTop - container.clientHeight;
3229
- if (hasMore && !loadingMore && remaining < container.clientHeight) onLoadMore === null || onLoadMore === void 0 || onLoadMore();
3719
+ const step = useCallback((direction) => {
3720
+ const next = shiftTimeRange(value, direction, { timeZone });
3721
+ if (next) commit(next);
3230
3722
  }, [
3231
- hasMore,
3232
- loadingMore,
3233
- onLoadMore,
3234
- syncFollowing
3723
+ value,
3724
+ timeZone,
3725
+ commit
3235
3726
  ]);
3236
- const totalHeight = rows.length * rowHeight;
3237
- const firstVisible = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
3238
- const visibleCount = Math.ceil((viewportHeight || 480) / rowHeight) + 20;
3239
- const lastVisible = Math.min(rows.length, firstVisible + visibleCount);
3240
- const visibleRows = useMemo(() => rows.slice(firstVisible, lastVisible), [
3241
- rows,
3242
- firstVisible,
3243
- lastVisible
3727
+ const zoom = useCallback((factor) => {
3728
+ const next = zoomTimeRange(value, factor, { timeZone });
3729
+ if (next) commit(next);
3730
+ }, [
3731
+ value,
3732
+ timeZone,
3733
+ commit
3244
3734
  ]);
3245
- return /* @__PURE__ */ jsxs("div", {
3246
- className: cn("@container/log flex min-h-0 flex-col", className),
3247
- "data-opt-id": "3ZR82",
3248
- "data-opt-slug": "log-table.LogTable",
3249
- children: [showHeader && /* @__PURE__ */ jsx(LogTableHeader, {
3250
- columns,
3251
- sort,
3252
- onSortChange,
3253
- onColumnMove,
3254
- density,
3255
- labels
3256
- }), /* @__PURE__ */ jsxs("div", {
3257
- ref: scrollRef,
3258
- onScroll: handleScroll,
3259
- className: "relative min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
3260
- role: "listbox",
3261
- "aria-label": labels.listLabel,
3262
- "aria-activedescendant": selectedId ? `log-row-${selectedId}` : void 0,
3263
- tabIndex: 0,
3264
- children: [
3265
- rows.length === 0 ? emptyState : /* @__PURE__ */ jsx("div", {
3266
- style: { height: totalHeight },
3267
- className: "relative",
3268
- children: visibleRows.map((row, index) => {
3269
- var _isNew;
3270
- const id = getRowId(row);
3271
- return /* @__PURE__ */ jsx(LogTableRow, {
3272
- id,
3273
- row,
3274
- columns,
3275
- top: (firstVisible + index) * rowHeight,
3276
- height: rowHeight,
3277
- density,
3278
- selected: id === selectedId,
3279
- isNew: (_isNew = isNew === null || isNew === void 0 ? void 0 : isNew(row)) !== null && _isNew !== void 0 ? _isNew : false,
3280
- onSelect,
3281
- onCellAction,
3282
- highlightTerms,
3283
- labels
3284
- }, id);
3285
- })
3286
- }),
3287
- loadingMore && /* @__PURE__ */ jsxs("div", {
3288
- className: "gap-element text-text-tertiary flex items-center justify-center py-3 text-xs",
3289
- children: [/* @__PURE__ */ jsx(Spinner, {
3290
- size: "sm",
3291
- "aria-hidden": true
3292
- }), labels.loadingMore]
3293
- }),
3294
- !hasMore && rows.length > 0 && /* @__PURE__ */ jsx("p", {
3295
- className: cn("text-text-tertiary", "py-3 text-center text-xs"),
3296
- children: labels.endOfResults
3297
- })
3298
- ]
3299
- })]
3735
+ useAutoRefresh({
3736
+ interval: (_refresh$interval = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval !== void 0 ? _refresh$interval : 0,
3737
+ paused: (_refresh$paused = refresh === null || refresh === void 0 ? void 0 : refresh.paused) !== null && _refresh$paused !== void 0 ? _refresh$paused : true,
3738
+ onRefresh
3300
3739
  });
3301
- }
3302
- /**
3303
- * LogTable see {@link LogTableInner}.
3304
- *
3305
- * The cast preserves the row generic through `forwardRef`, which otherwise
3306
- * widens it to its constraint and takes the column callbacks' types with it.
3307
- */
3308
- const LogTable = forwardRef(LogTableInner);
3309
- function LogTableHeader({ columns, sort, onSortChange, onColumnMove, density, labels }) {
3310
- return /* @__PURE__ */ jsx("div", {
3311
- role: "row",
3312
- className: cn("border-border-subtle text-text-tertiary flex shrink-0 items-center gap-3 border-b px-3", DENSITY_TEXT[density]),
3313
- style: { height: ROW_HEIGHT[density] },
3314
- children: columns.map((column, index) => {
3315
- var _column$priority;
3316
- const active = (sort === null || sort === void 0 ? void 0 : sort.columnId) === column.id;
3317
- const sortable = column.sortable && onSortChange;
3318
- const columnLabel = typeof column.header === "string" ? column.header : column.id;
3319
- const content = /* @__PURE__ */ jsxs(Fragment, { children: [
3320
- column.marker && /* @__PURE__ */ jsx("span", {
3321
- "aria-hidden": true,
3322
- className: "size-1.5 shrink-0"
3323
- }),
3324
- /* @__PURE__ */ jsx("span", {
3325
- className: "truncate",
3326
- children: column.header
3327
- }),
3328
- active && (sort.direction === "asc" ? /* @__PURE__ */ jsx(ChevronUp, {
3329
- className: "size-3 shrink-0",
3330
- "aria-hidden": true
3331
- }) : /* @__PURE__ */ jsx(ChevronDown, {
3332
- className: "size-3 shrink-0",
3333
- "aria-hidden": true
3334
- }))
3335
- ] });
3336
- return /* @__PURE__ */ jsxs("div", {
3337
- role: "columnheader",
3338
- "aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
3339
- className: cn("group/header flex items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority = column.priority) !== null && _column$priority !== void 0 ? _column$priority : "always"]),
3340
- style: columnStyle(column),
3341
- children: [sortable ? /* @__PURE__ */ jsx("button", {
3342
- type: "button",
3343
- className: "hover:text-text-primary flex cursor-pointer items-center gap-1.5 truncate",
3344
- title: active && sort.direction === "desc" ? labels.sortAscending : labels.sortDescending,
3345
- onClick: () => onSortChange({
3346
- columnId: column.id,
3347
- direction: active && sort.direction === "desc" ? "asc" : "desc"
3348
- }),
3349
- children: content
3350
- }) : content, onColumnMove && /* @__PURE__ */ jsxs("span", {
3351
- className: cn("ml-auto flex shrink-0 items-center gap-0.5", "opacity-0 transition-opacity", "group-hover/header:opacity-100 focus-within:opacity-100"),
3352
- children: [index > 0 && /* @__PURE__ */ jsx("button", {
3353
- type: "button",
3354
- className: "hover:text-text-primary cursor-pointer",
3355
- "aria-label": labels.moveLeft(columnLabel),
3356
- title: labels.moveLeft(columnLabel),
3357
- onClick: () => onColumnMove(column.id, "left"),
3358
- children: /* @__PURE__ */ jsx(ChevronLeft, {
3359
- className: "size-3",
3360
- "aria-hidden": true
3361
- })
3362
- }), index < columns.length - 1 && /* @__PURE__ */ jsx("button", {
3363
- type: "button",
3364
- className: "hover:text-text-primary cursor-pointer",
3365
- "aria-label": labels.moveRight(columnLabel),
3366
- title: labels.moveRight(columnLabel),
3367
- onClick: () => onColumnMove(column.id, "right"),
3368
- children: /* @__PURE__ */ jsx(ChevronRight, {
3369
- className: "size-3",
3370
- "aria-hidden": true
3371
- })
3740
+ const refreshLabel = !refresh || refresh.paused || refresh.interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(refresh.interval, locale));
3741
+ return /* @__PURE__ */ jsxs("div", {
3742
+ className: cn("bg-surface inline-flex items-center rounded-lg border", OPT_BORDER, className),
3743
+ role: "group",
3744
+ "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.trigger,
3745
+ "data-opt-id": "3WJ8H",
3746
+ "data-opt-slug": "time-range-control.TimeRangeControl",
3747
+ children: [
3748
+ /* @__PURE__ */ jsxs(PopoverRoot$1, {
3749
+ open,
3750
+ onOpenChange: setOpen,
3751
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger$1, {
3752
+ className: cn("text-text-primary h-10 gap-2", description.isInvalid && "text-danger-fg"),
3753
+ disabled,
3754
+ children: [/* @__PURE__ */ jsx(Clock, {
3755
+ className: "text-text-tertiary h-4 w-4",
3756
+ "aria-hidden": "true"
3757
+ }), /* @__PURE__ */ jsx("span", {
3758
+ className: "truncate text-sm",
3759
+ children: description.text
3372
3760
  })]
3761
+ }), /* @__PURE__ */ jsxs(PopoverContent$1, {
3762
+ className: "gap-group flex w-80 flex-col",
3763
+ children: [
3764
+ /* @__PURE__ */ jsxs("div", {
3765
+ className: "gap-element flex flex-col",
3766
+ children: [/* @__PURE__ */ jsx("p", {
3767
+ className: "text-text-primary text-xs font-medium",
3768
+ children: labels.quickSelect
3769
+ }), /* @__PURE__ */ jsxs("div", {
3770
+ className: "flex items-end gap-2",
3771
+ children: [
3772
+ /* @__PURE__ */ jsx(Input, {
3773
+ "aria-label": labels.quickSelect,
3774
+ type: "number",
3775
+ min: 1,
3776
+ size: "sm",
3777
+ className: "w-20",
3778
+ value: quickCount,
3779
+ onChange: (event) => setQuickCount(event.target.value),
3780
+ onKeyDown: (event) => {
3781
+ if (event.key === "Enter") applyQuick();
3782
+ }
3783
+ }),
3784
+ /* @__PURE__ */ jsxs(SelectRoot$1, {
3785
+ value: quickUnit,
3786
+ setValue: (next) => setQuickUnit(selectValue(next)),
3787
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
3788
+ minWidth: 110,
3789
+ "aria-label": labels.quickSelect
3790
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: QUICK_UNITS.map((unit) => /* @__PURE__ */ jsx(SelectItem$1, {
3791
+ value: unit,
3792
+ children: labels.units[unit]
3793
+ }, unit)) })]
3794
+ }),
3795
+ /* @__PURE__ */ jsx(Button, {
3796
+ variant: "secondary",
3797
+ size: "sm",
3798
+ onClick: applyQuick,
3799
+ children: labels.apply
3800
+ })
3801
+ ]
3802
+ })]
3803
+ }),
3804
+ /* @__PURE__ */ jsxs("div", {
3805
+ className: "gap-element flex flex-col",
3806
+ children: [/* @__PURE__ */ jsx(Input, {
3807
+ label: labels.start,
3808
+ size: "sm",
3809
+ value: editing.from,
3810
+ error: boundState(editing.from),
3811
+ hint: labels.startHint,
3812
+ onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { from: event.target.value })),
3813
+ onKeyDown: commitOnEnter
3814
+ }), /* @__PURE__ */ jsx(Input, {
3815
+ label: labels.end,
3816
+ size: "sm",
3817
+ value: editing.to,
3818
+ error: boundState(editing.to),
3819
+ onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { to: event.target.value })),
3820
+ onKeyDown: commitOnEnter
3821
+ })]
3822
+ }),
3823
+ presets && presets.length > 0 && /* @__PURE__ */ jsx(RangeList, {
3824
+ title: labels.commonlyUsed,
3825
+ items: presets,
3826
+ onPick: (range) => {
3827
+ commit(range);
3828
+ setOpen(false);
3829
+ }
3830
+ }),
3831
+ recentRanges && recentRanges.length > 0 && /* @__PURE__ */ jsx(RangeList, {
3832
+ title: labels.recentlyUsed,
3833
+ items: recentRanges.slice(0, 5),
3834
+ onPick: (range) => {
3835
+ commit(range);
3836
+ setOpen(false);
3837
+ }
3838
+ }),
3839
+ onRefreshChange && /* @__PURE__ */ jsxs("div", {
3840
+ className: "gap-element flex flex-col",
3841
+ children: [/* @__PURE__ */ jsx("p", {
3842
+ className: "text-text-primary text-xs font-medium",
3843
+ children: labels.autoRefresh
3844
+ }), /* @__PURE__ */ jsxs(SelectRoot$1, {
3845
+ value: String((refresh === null || refresh === void 0 ? void 0 : refresh.paused) ? 0 : (_refresh$interval2 = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval2 !== void 0 ? _refresh$interval2 : 0),
3846
+ setValue: (next) => {
3847
+ const interval = Number(selectValue(next));
3848
+ onRefreshChange({
3849
+ interval,
3850
+ paused: interval === 0
3851
+ });
3852
+ },
3853
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
3854
+ minWidth: 140,
3855
+ "aria-label": labels.autoRefresh
3856
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: REFRESH_INTERVALS.map((interval) => /* @__PURE__ */ jsx(SelectItem$1, {
3857
+ value: String(interval),
3858
+ children: interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(interval, locale))
3859
+ }, interval)) })]
3860
+ })]
3861
+ })
3862
+ ]
3373
3863
  })]
3374
- }, column.id);
3375
- })
3376
- });
3377
- }
3378
- function LogTableRowImpl({ id, row, columns, top, height, density, selected, isNew, onSelect, onCellAction, highlightTerms, labels }) {
3379
- return /* @__PURE__ */ jsx("div", {
3380
- role: "option",
3381
- "aria-selected": selected,
3382
- tabIndex: -1,
3383
- id: `log-row-${id}`,
3384
- "data-log-row-id": id,
3385
- onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(id),
3386
- onKeyDown: (event) => {
3387
- if (event.key !== "Enter" && event.key !== " ") return;
3388
- event.preventDefault();
3389
- onSelect === null || onSelect === void 0 || onSelect(id);
3390
- },
3391
- style: {
3392
- top,
3393
- height
3394
- },
3395
- className: cn("border-border-subtle/60 absolute inset-x-0 flex items-center gap-3 border-b px-3", DENSITY_TEXT[density], onSelect && "cursor-pointer", "transition-colors duration-100", "hover:bg-surface-hover", selected && "bg-accent-subtle hover:bg-accent-subtle", isNew && !selected && "opt-log-row-enter"),
3396
- children: columns.map((column) => {
3397
- var _column$priority2;
3398
- const raw = column.value(row);
3399
- const text = raw === null || raw === void 0 || raw === "" ? "" : String(raw);
3400
- return /* @__PURE__ */ jsxs("div", {
3401
- className: cn("group/cell relative flex min-w-0 items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority2 = column.priority) !== null && _column$priority2 !== void 0 ? _column$priority2 : "always"]),
3402
- style: columnStyle(column),
3864
+ }),
3865
+ showWindowButtons && /* @__PURE__ */ jsxs("div", {
3866
+ className: cn("flex items-center border-l", "border border-border"),
3867
+ role: "group",
3868
+ "aria-label": labels.previous,
3403
3869
  children: [
3404
- column.marker && text && /* @__PURE__ */ jsx("span", {
3405
- "aria-hidden": true,
3406
- className: "size-1.5 shrink-0 rounded-full",
3407
- style: { backgroundColor: `hsl(${hueOf(text)} 65% 55%)` }
3870
+ /* @__PURE__ */ jsx(IconButton, {
3871
+ label: labels.previous,
3872
+ disabled: disabled || description.isInvalid,
3873
+ onClick: () => step(-1),
3874
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
3875
+ className: "h-4 w-4",
3876
+ "aria-hidden": "true"
3877
+ })
3408
3878
  }),
3409
- /* @__PURE__ */ jsx("span", {
3410
- className: cn("min-w-0 flex-1", !column.truncate && "truncate", column.mono && "font-mono", text ? "text-text-secondary" : ABSENT),
3411
- title: column.truncate ? void 0 : text || void 0,
3412
- children: column.render ? column.render(row) : text ? column.truncate ? /* @__PURE__ */ jsx(TextTruncate, {
3413
- text,
3414
- truncation: column.truncate,
3415
- children: (shown) => highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ jsx(Highlight, {
3416
- text: shown,
3417
- search: highlightTerms
3418
- }) : shown
3419
- }) : highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ jsx(Highlight, {
3420
- text,
3421
- search: highlightTerms
3422
- }) : text : EM_DASH
3879
+ /* @__PURE__ */ jsx(IconButton, {
3880
+ label: labels.zoomIn,
3881
+ disabled: disabled || description.isInvalid,
3882
+ onClick: () => zoom(-zoomFactor),
3883
+ children: /* @__PURE__ */ jsx(ZoomIn, {
3884
+ className: "h-4 w-4",
3885
+ "aria-hidden": "true"
3886
+ })
3423
3887
  }),
3424
- onCellAction && column.field && text && /* @__PURE__ */ jsx(CellActions, {
3425
- field: column.field,
3426
- value: text,
3427
- onAction: onCellAction,
3428
- labels
3888
+ /* @__PURE__ */ jsx(IconButton, {
3889
+ label: labels.zoomOut,
3890
+ disabled: disabled || description.isInvalid,
3891
+ onClick: () => zoom(zoomFactor),
3892
+ children: /* @__PURE__ */ jsx(ZoomOut, {
3893
+ className: "h-4 w-4",
3894
+ "aria-hidden": "true"
3895
+ })
3896
+ }),
3897
+ /* @__PURE__ */ jsx(IconButton, {
3898
+ label: labels.nextWindow,
3899
+ disabled: disabled || description.isInvalid,
3900
+ onClick: () => step(1),
3901
+ children: /* @__PURE__ */ jsx(ChevronRight, {
3902
+ className: "h-4 w-4",
3903
+ "aria-hidden": "true"
3904
+ })
3429
3905
  })
3430
3906
  ]
3431
- }, column.id);
3432
- })
3907
+ }),
3908
+ (onRefresh || showUpdateButton) && /* @__PURE__ */ jsx("div", {
3909
+ className: cn("border-l pl-2 pr-2", "border border-border"),
3910
+ children: /* @__PURE__ */ jsx(Button, {
3911
+ variant: hasChanges ? "primary" : "secondary",
3912
+ size: "sm",
3913
+ loading: isLoading,
3914
+ disabled: disabled || description.isInvalid,
3915
+ onClick: apply,
3916
+ leftIcon: hasChanges ? void 0 : /* @__PURE__ */ jsx(RefreshCw, {
3917
+ className: "h-3.5 w-3.5",
3918
+ "aria-hidden": "true"
3919
+ }),
3920
+ title: refreshLabel,
3921
+ children: hasChanges ? labels.update : labels.refresh
3922
+ })
3923
+ })
3924
+ ]
3433
3925
  });
3434
3926
  }
3435
- /**
3436
- * Memoized because a live log re-renders several times a second while almost
3437
- * every visible row is unchanged.
3438
- */
3439
- const LogTableRow = memo(LogTableRowImpl);
3440
- /**
3441
- * The two questions a reader has about a value they just spotted: show me only
3442
- * these, and get these out of my way.
3443
- *
3444
- * Revealed on hover rather than always drawn — a log is scanned, and a pair of
3445
- * buttons on every cell of every row is a wall of chrome. They stay in the tab
3446
- * order regardless, since hover is not an interaction everyone has.
3447
- */
3448
- function CellActions({ field, value, onAction, labels }) {
3449
- const button = "cursor-pointer rounded-sm p-0.5 text-text-tertiary hover:bg-bg-muted hover:text-text-primary";
3450
- return /* @__PURE__ */ jsxs("span", {
3451
- className: cn("bg-surface/90 ml-auto flex shrink-0 items-center gap-0.5 rounded pl-1", "opacity-0 transition-opacity", "group-hover/cell:opacity-100 focus-within:opacity-100"),
3452
- children: [/* @__PURE__ */ jsx("button", {
3453
- type: "button",
3454
- className: button,
3455
- title: labels.filterFor(value),
3456
- "aria-label": labels.filterFor(value),
3457
- onClick: (event) => {
3458
- event.stopPropagation();
3459
- onAction({
3460
- field,
3461
- value,
3462
- intent: "include"
3463
- });
3464
- },
3465
- children: /* @__PURE__ */ jsx(Plus, {
3466
- className: "size-3",
3467
- "aria-hidden": true
3468
- })
3469
- }), /* @__PURE__ */ jsx("button", {
3470
- type: "button",
3471
- className: button,
3472
- title: labels.filterOut(value),
3473
- "aria-label": labels.filterOut(value),
3474
- onClick: (event) => {
3475
- event.stopPropagation();
3476
- onAction({
3477
- field,
3478
- value,
3479
- intent: "exclude"
3480
- });
3481
- },
3482
- children: /* @__PURE__ */ jsx(Minus, {
3483
- className: "size-3",
3484
- "aria-hidden": true
3485
- })
3486
- })]
3927
+ TimeRangeControl.displayName = "TimeRangeControl";
3928
+ function IconButton({ label, disabled, onClick, children }) {
3929
+ return /* @__PURE__ */ jsx(Button, {
3930
+ variant: "ghost",
3931
+ size: "sm",
3932
+ "aria-label": label,
3933
+ title: label,
3934
+ disabled,
3935
+ onClick,
3936
+ children
3487
3937
  });
3488
3938
  }
3489
- //#endregion
3490
- //#region src/lib/query/query-ast.ts
3491
- const EMPTY_QUERY = { clauses: [] };
3492
- /** Whether a query constrains anything at all. */
3493
- function isEmptyQuery(query) {
3494
- return !query || query.clauses.length === 0;
3495
- }
3496
- function sameClause(a, b) {
3497
- if (a.type !== b.type) return false;
3498
- if (a.type === "term" && b.type === "term") return a.value === b.value && a.negated === b.negated;
3499
- const fieldA = a;
3500
- const fieldB = b;
3501
- return fieldA.field === fieldB.field && fieldA.operator === fieldB.operator && fieldA.negated === fieldB.negated && fieldA.values.length === fieldB.values.length && fieldA.values.every((value, index) => value === fieldB.values[index]);
3502
- }
3503
- /** Add a clause unless an identical one is already there. */
3504
- function addClause(query, clause) {
3505
- if (query.clauses.some((existing) => sameClause(existing, clause))) return query;
3506
- return { clauses: [...query.clauses, clause] };
3507
- }
3508
- /**
3509
- * Constrain a field to a value — what a cell's "filter for this" action does.
3510
- *
3511
- * Applying the opposite of a constraint already present replaces it rather than
3512
- * adding it: `status:200` then "filter out 200" should read as one changed
3513
- * mind, not as a query that can never match.
3514
- */
3515
- function withFieldValue(query, field, value, options = {}) {
3516
- var _options$negated, _options$operator;
3517
- const negated = (_options$negated = options.negated) !== null && _options$negated !== void 0 ? _options$negated : false;
3518
- const operator = (_options$operator = options.operator) !== null && _options$operator !== void 0 ? _options$operator : "eq";
3519
- return { clauses: [...query.clauses.filter((clause) => !(clause.type === "field" && clause.field === field && clause.operator === operator && clause.values.length === 1 && clause.values[0] === value)), {
3520
- type: "field",
3521
- field,
3522
- operator,
3523
- values: [value],
3524
- negated
3525
- }] };
3526
- }
3527
- /** Drop every clause naming `field`. */
3528
- function withoutField(query, field) {
3529
- return { clauses: query.clauses.filter((clause) => clause.type !== "field" || clause.field !== field) };
3530
- }
3531
- /** Drop the clause at `index`, for a removable chip. */
3532
- function removeClause(query, index) {
3533
- return { clauses: query.clauses.filter((_, at) => at !== index) };
3534
- }
3535
- /** Every field the query names, in the order they first appear. */
3536
- function queryFields(query) {
3537
- const seen = [];
3538
- for (const clause of query.clauses) if (clause.type === "field" && !seen.includes(clause.field)) seen.push(clause.field);
3539
- return seen;
3540
- }
3541
- /** The values a field is constrained to, if any — for showing a filter as active. */
3542
- function fieldValues(query, field) {
3543
- return query.clauses.flatMap((clause) => clause.type === "field" && clause.field === field && !clause.negated ? clause.values : []);
3544
- }
3545
- /**
3546
- * Fold a cell action into the query.
3547
- *
3548
- * The pair of buttons on a log cell is the fastest path from "I noticed this
3549
- * value" to a filtered view, and the reason it works is that the result is a
3550
- * query the reader can then see, edit and share — not a hidden filter stack
3551
- * with its own rules.
3552
- */
3553
- function applyCellAction(query, action) {
3554
- return withFieldValue(query, action.field, action.value, { negated: action.intent === "exclude" });
3939
+ function RangeList({ title, items, onPick }) {
3940
+ return /* @__PURE__ */ jsxs("div", {
3941
+ className: "gap-element flex flex-col",
3942
+ children: [/* @__PURE__ */ jsx("p", {
3943
+ className: "text-text-primary text-xs font-medium",
3944
+ children: title
3945
+ }), /* @__PURE__ */ jsx("ul", {
3946
+ className: "grid grid-cols-2 gap-1",
3947
+ children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
3948
+ type: "button",
3949
+ className: cn("hover:bg-bg-subtle w-full cursor-pointer rounded px-2 py-1 text-left text-xs", OPT_TEXT_SECONDARY),
3950
+ onClick: () => onPick(item.range),
3951
+ children: item.label
3952
+ }) }, `${item.label}-${index}`))
3953
+ })]
3954
+ });
3555
3955
  }
3556
- //#endregion
3557
- //#region src/lib/query/query-parse.ts
3558
- const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
3559
3956
  /**
3560
- * Split on whitespace, but keep quoted runs and parenthesised groups whole.
3561
- *
3562
- * An unterminated quote or group swallows the rest of the line rather than
3563
- * erroring: the reader is mid-word, and the alternative is the query flickering
3564
- * between two different meanings as they type the closing character.
3957
+ * The windows most analytics tools open with. Exported so a caller gets a
3958
+ * sensible list without inventing date math of their own.
3565
3959
  */
3566
- function tokenize(input) {
3567
- const tokens = [];
3568
- let index = 0;
3569
- while (index < input.length) {
3570
- while (index < input.length && /\s/.test(input[index])) index += 1;
3571
- if (index >= input.length) break;
3572
- const start = index;
3573
- let text = "";
3574
- let quoted = false;
3575
- let atTokenStart = true;
3576
- let depth = 0;
3577
- while (index < input.length) {
3578
- const char = input[index];
3579
- if (char === "\"") {
3580
- if (atTokenStart) quoted = true;
3581
- atTokenStart = false;
3582
- index += 1;
3583
- while (index < input.length && input[index] !== "\"") {
3584
- text += input[index];
3585
- index += 1;
3586
- }
3587
- index += 1;
3588
- continue;
3589
- }
3590
- if (char === "(") depth += 1;
3591
- if (char === ")") depth = Math.max(0, depth - 1);
3592
- if (/\s/.test(char) && depth === 0) break;
3593
- atTokenStart = false;
3594
- text += char;
3595
- index += 1;
3960
+ const DEFAULT_TIME_PRESETS = [
3961
+ {
3962
+ label: "Last 15 minutes",
3963
+ range: {
3964
+ from: "now-15m",
3965
+ to: "now"
3596
3966
  }
3597
- if (text.length > 0 || quoted) tokens.push({
3598
- text,
3599
- at: start,
3600
- quoted
3601
- });
3602
- }
3603
- return tokens;
3604
- }
3605
- /** `(a or b)` → `["a", "b"]`; anything else is a single value. */
3606
- function readValues(raw) {
3607
- const trimmed = raw.trim();
3608
- if (!trimmed.startsWith("(")) return [stripQuotes(trimmed)];
3609
- return trimmed.replace(/^\(/, "").replace(/\)$/, "").split(/\s+or\s+/i).map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
3610
- }
3611
- function stripQuotes(value) {
3612
- if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
3613
- return value.replace(/^"/, "");
3614
- }
3615
- const OPERATORS = [
3967
+ },
3616
3968
  {
3617
- symbol: ">=",
3618
- operator: "gte"
3969
+ label: "Last hour",
3970
+ range: {
3971
+ from: "now-1h",
3972
+ to: "now"
3973
+ }
3619
3974
  },
3620
3975
  {
3621
- symbol: "<=",
3622
- operator: "lte"
3976
+ label: "Last 24 hours",
3977
+ range: {
3978
+ from: "now-24h",
3979
+ to: "now"
3980
+ }
3623
3981
  },
3624
3982
  {
3625
- symbol: ":",
3626
- operator: "eq"
3983
+ label: "Last 7 days",
3984
+ range: {
3985
+ from: "now-7d",
3986
+ to: "now"
3987
+ }
3627
3988
  },
3628
3989
  {
3629
- symbol: ">",
3630
- operator: "gt"
3990
+ label: "Last 30 days",
3991
+ range: {
3992
+ from: "now-30d",
3993
+ to: "now"
3994
+ }
3631
3995
  },
3632
3996
  {
3633
- symbol: "<",
3634
- operator: "lt"
3997
+ label: "Last 90 days",
3998
+ range: {
3999
+ from: "now-90d",
4000
+ to: "now"
4001
+ }
4002
+ },
4003
+ {
4004
+ label: "Today",
4005
+ range: {
4006
+ from: "now/d",
4007
+ to: "now/d"
4008
+ }
4009
+ },
4010
+ {
4011
+ label: "This week",
4012
+ range: {
4013
+ from: "now/w",
4014
+ to: "now/w"
4015
+ }
3635
4016
  }
3636
4017
  ];
3637
- /** Where a field operator starts in the token, or `-1`. */
3638
- function findOperator(text) {
3639
- let best = null;
3640
- for (const { symbol, operator } of OPERATORS) {
3641
- const index = text.indexOf(symbol);
3642
- if (index <= 0) continue;
3643
- if (!best || index < best.index || index === best.index && symbol.length > best.symbol.length) best = {
3644
- index,
3645
- symbol,
3646
- operator
3647
- };
3648
- }
3649
- return best;
3650
- }
3651
- /** Parse, keeping whatever could not be understood as a plain term. */
3652
- function parseQueryWithDiagnostics(input) {
3653
- const diagnostics = [];
3654
- const clauses = [];
3655
- let trailing = null;
3656
- for (const token of tokenize(input)) {
3657
- let text = token.text;
3658
- let negated = false;
3659
- if (!token.quoted && text.startsWith("-") && text.length > 1) {
3660
- negated = true;
3661
- text = text.slice(1);
3662
- }
3663
- const operator = token.quoted ? null : findOperator(text);
3664
- if (!operator) {
3665
- if (text.length > 0) clauses.push({
3666
- type: "term",
3667
- value: text,
3668
- negated
3669
- });
3670
- continue;
3671
- }
3672
- const field = text.slice(0, operator.index);
3673
- const rawValue = text.slice(operator.index + operator.symbol.length);
3674
- if (!FIELD_NAME.test(field)) {
3675
- clauses.push({
3676
- type: "term",
3677
- value: text,
3678
- negated
3679
- });
3680
- continue;
3681
- }
3682
- if (rawValue === "") {
3683
- trailing = {
3684
- field,
3685
- operator: operator.operator,
3686
- value: ""
3687
- };
3688
- continue;
3689
- }
3690
- if (rawValue === "*" && operator.operator === "eq") {
3691
- clauses.push({
3692
- type: "field",
3693
- field,
3694
- operator: "exists",
3695
- values: [],
3696
- negated
3697
- });
3698
- continue;
3699
- }
3700
- if (operator.operator === "eq" && rawValue.length > 2 && rawValue.startsWith("*") && rawValue.endsWith("*")) {
3701
- clauses.push({
3702
- type: "field",
3703
- field,
3704
- operator: "contains",
3705
- values: [stripQuotes(rawValue.slice(1, -1))],
3706
- negated
3707
- });
3708
- continue;
3709
- }
3710
- const values = readValues(rawValue);
3711
- if (values.length === 0) {
3712
- diagnostics.push({
3713
- at: token.at,
3714
- text: token.text,
3715
- message: "Empty value group"
3716
- });
3717
- continue;
3718
- }
3719
- if (operator.operator !== "eq" && values.length > 1) diagnostics.push({
3720
- at: token.at,
3721
- text: token.text,
3722
- message: "A comparison takes a single value"
3723
- });
3724
- const clause = {
3725
- type: "field",
3726
- field,
3727
- operator: operator.operator,
3728
- values: operator.operator === "eq" ? values : values.slice(0, 1),
3729
- negated
3730
- };
3731
- clauses.push(clause);
3732
- if (!rawValue.startsWith("(")) {
3733
- var _values$;
3734
- trailing = {
3735
- field,
3736
- operator: operator.operator,
3737
- value: (_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : ""
3738
- };
3739
- }
3740
- }
3741
- return {
3742
- query: { clauses },
3743
- diagnostics,
3744
- trailing
3745
- };
3746
- }
3747
- /** Parse, discarding diagnostics. */
3748
- function parseQuery(input) {
3749
- return parseQueryWithDiagnostics(input).query;
3750
- }
3751
- //#endregion
3752
- //#region src/lib/query/query-format.ts
3753
- const OPERATOR_SYMBOL = {
3754
- eq: ":",
3755
- contains: ":",
3756
- gt: ">",
3757
- gte: ">=",
3758
- lt: "<",
3759
- lte: "<=",
3760
- exists: ":"
3761
- };
3762
- /** Quote a value that would otherwise re-parse as something else. */
3763
- function quoteValue(value) {
3764
- return /[\s"()]/.test(value) || value === "" ? `"${value.replace(/"/g, "")}"` : value;
3765
- }
3766
- /** One clause as text. */
3767
- function formatClause(clause) {
3768
- var _clause$values$2;
3769
- const prefix = clause.negated ? "-" : "";
3770
- if (clause.type === "term") return `${prefix}${quoteValue(clause.value)}`;
3771
- if (clause.operator === "exists") return `${prefix}${clause.field}:*`;
3772
- if (clause.operator === "contains") {
3773
- var _clause$values$;
3774
- return `${prefix}${clause.field}:*${(_clause$values$ = clause.values[0]) !== null && _clause$values$ !== void 0 ? _clause$values$ : ""}*`;
3775
- }
3776
- const symbol = OPERATOR_SYMBOL[clause.operator];
3777
- if (clause.values.length > 1) return `${prefix}${clause.field}${symbol}(${clause.values.map(quoteValue).join(" or ")})`;
3778
- return `${prefix}${clause.field}${symbol}${quoteValue((_clause$values$2 = clause.values[0]) !== null && _clause$values$2 !== void 0 ? _clause$values$2 : "")}`;
3779
- }
3780
- /** The whole query as text. */
3781
- function formatQuery(query) {
3782
- return query.clauses.map(formatClause).join(" ");
3783
- }
3784
- /** A short human label for a clause chip. */
3785
- function describeClause(clause) {
3786
- if (clause.type === "term") return clause.negated ? `not "${clause.value}"` : `"${clause.value}"`;
3787
- if (clause.operator === "exists") return clause.negated ? `${clause.field} missing` : `${clause.field} present`;
3788
- if (clause.operator === "contains") {
3789
- var _clause$values$3;
3790
- return `${clause.negated ? "not " : ""}${clause.field} ⊃ ${(_clause$values$3 = clause.values[0]) !== null && _clause$values$3 !== void 0 ? _clause$values$3 : ""}`;
3791
- }
3792
- const values = clause.values.join(", ");
3793
- const symbol = OPERATOR_SYMBOL[clause.operator];
3794
- return `${clause.negated ? "not " : ""}${clause.field} ${symbol === ":" ? "=" : symbol} ${values}`;
3795
- }
3796
4018
  //#endregion
3797
- //#region src/shells/query-bar.tsx
3798
- const defaultLabels$1 = {
3799
- placeholder: "Search, or filter with field:value",
3800
- ariaLabel: "Search query",
3801
- clear: "Clear query",
3802
- removeClause: (clause) => `Remove ${clause}`,
3803
- fieldsHeading: "Fields",
3804
- valuesHeading: "Values",
3805
- syntaxHint: null
4019
+ //#region src/shells/time-series-panel.tsx
4020
+ const defaultLabels$2 = {
4021
+ back: "Back to previous range",
4022
+ legend: "Series"
3806
4023
  };
3807
- const MAX_SUGGESTIONS = 8;
3808
4024
  /**
3809
- * QueryBarone control for "find this" and "only these".
3810
- *
3811
- * A search box and a row of filter pickers answer halves of the same question
3812
- * and neither can express the other, so people end up using both and holding
3813
- * the combination in their head. Typing the filter is what closes that: the
3814
- * whole question is one string, which means it can be read back, corrected in
3815
- * place, pasted to a colleague, and stored in a URL.
4025
+ * TimeSeriesPanela chart you can steer.
3816
4026
  *
3817
- * The text is the source of truth while the reader is typing; the parsed query
3818
- * is committed on Enter or blur. Anything half-written parses to a plain term
3819
- * rather than an error, so the bar never argues with someone mid-word.
4027
+ * The point of putting the control and the chart in one component is the
4028
+ * loop between them: drag across the plot and the window narrows to what you
4029
+ * dragged, which re-queries, which redraws the axis you just picked. That
4030
+ * only works if someone owns the step back out again — so zoom keeps a
4031
+ * history, and the header offers the way back rather than leaving the reader
4032
+ * to retype the range they came from.
3820
4033
  *
3821
4034
  * @example
3822
4035
  * ```tsx
3823
- * <QueryBar
3824
- * value={query}
3825
- * onChange={setQuery}
3826
- * fields={[{ name: "path", values: ["/checkout", "/cart"] }]}
3827
- * showClauses
4036
+ * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
4037
+ *
4038
+ * <TimeSeriesPanel
4039
+ * title="Sessions"
4040
+ * data={points}
4041
+ * series={[{ dataKey: "sessions", name: "Sessions" }]}
4042
+ * range={range}
4043
+ * onRangeChange={setRange}
4044
+ * onRefresh={() => refetch()}
4045
+ * toggleableLegend
3828
4046
  * />
3829
4047
  * ```
3830
4048
  */
3831
- function QueryBar({ value, onChange, fields = [], showClauses = false, live = false, disabled = false, className, labels: customLabels }) {
3832
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$1), customLabels);
3833
- const inputRef = useRef(null);
3834
- const [text, setText] = useState(() => formatQuery(value));
3835
- const [focused, setFocused] = useState(false);
3836
- const [highlighted, setHighlighted] = useState(0);
3837
- const committedRef = useRef(formatQuery(value));
3838
- useEffect(() => {
3839
- const next = formatQuery(value);
3840
- if (next === committedRef.current) return;
3841
- committedRef.current = next;
3842
- if (!focused) setText(next);
3843
- }, [value, focused]);
3844
- const parsed = useMemo(() => parseQueryWithDiagnostics(text), [text]);
3845
- const commit = useCallback((nextText) => {
3846
- const nextQuery = parseQueryWithDiagnostics(nextText).query;
3847
- committedRef.current = formatQuery(nextQuery);
3848
- onChange(nextQuery);
3849
- }, [onChange]);
3850
- const suggestions = useSuggestions(text, parsed.trailing, fields);
3851
- const applySuggestion = useCallback((suggestion) => {
3852
- var _inputRef$current;
3853
- const nextText = suggestion.replace(text);
3854
- setText(nextText);
3855
- if (suggestion.kind === "value") commit(nextText);
3856
- (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
3857
- setHighlighted(0);
3858
- }, [text, commit]);
3859
- const handleKeyDown = (event) => {
3860
- if (suggestions.length > 0 && focused) {
3861
- if (event.key === "ArrowDown") {
3862
- event.preventDefault();
3863
- setHighlighted((current) => (current + 1) % suggestions.length);
3864
- return;
3865
- }
3866
- if (event.key === "ArrowUp") {
3867
- event.preventDefault();
3868
- setHighlighted((current) => (current - 1 + suggestions.length) % suggestions.length);
3869
- return;
3870
- }
3871
- if (event.key === "Tab" || event.key === "Enter" && highlighted > 0) {
3872
- event.preventDefault();
3873
- applySuggestion(suggestions[highlighted]);
3874
- return;
3875
- }
3876
- }
3877
- if (event.key === "Enter") {
3878
- event.preventDefault();
3879
- commit(text);
3880
- return;
3881
- }
3882
- if (event.key === "Escape") {
3883
- var _inputRef$current2;
3884
- setText(committedRef.current);
3885
- (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
3886
- }
3887
- };
3888
- const showSuggestions = focused && suggestions.length > 0;
3889
- return /* @__PURE__ */ jsxs("div", {
3890
- className: cn("relative flex min-w-0 flex-col gap-1.5", className),
3891
- "data-opt-id": "JSVZ1",
3892
- "data-opt-slug": "query-bar.QueryBar",
3893
- children: [
3894
- /* @__PURE__ */ jsxs("div", {
3895
- className: cn("bg-surface flex h-9 items-center gap-2 rounded-md border px-2.5", OPT_BORDER, "focus-within:border-accent transition-colors", disabled && "opacity-60"),
3896
- children: [
3897
- /* @__PURE__ */ jsx(Search, {
3898
- className: "text-text-tertiary size-4 shrink-0",
3899
- "aria-hidden": true
3900
- }),
3901
- /* @__PURE__ */ jsx("input", {
3902
- ref: inputRef,
3903
- type: "text",
3904
- role: "combobox",
3905
- "aria-expanded": showSuggestions,
3906
- "aria-controls": "query-bar-suggestions",
3907
- "aria-autocomplete": "list",
3908
- "aria-label": labels.ariaLabel,
3909
- placeholder: labels.placeholder,
3910
- disabled,
3911
- className: cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent font-mono text-sm outline-none focus-visible:outline-none"),
3912
- value: text,
3913
- onChange: (event) => {
3914
- setText(event.target.value);
3915
- setHighlighted(0);
3916
- if (live) commit(event.target.value);
3917
- },
3918
- onFocus: () => setFocused(true),
3919
- onBlur: () => {
3920
- window.setTimeout(() => setFocused(false), 120);
3921
- if (!live) commit(text);
3922
- },
3923
- onKeyDown: handleKeyDown
4049
+ function TimeSeriesPanel({ title, description, actions, data, series, range, onRangeChange, granularity = "auto", timeZone, locale, showRangeControl = true, presets, recentRanges, refresh, onRefreshChange, onRefresh, toggleableLegend = false, annotations, isLoading = false, height = 280, className, chartProps, labels: customLabels, "aria-label": ariaLabel }) {
4050
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$2), customLabels);
4051
+ const titleId = useId();
4052
+ const [history, setHistory] = useState([]);
4053
+ const [hidden, setHidden] = useState([]);
4054
+ const resolved = useMemo(() => resolveTimeRange(range, { timeZone }), [range, timeZone]);
4055
+ const visibleSeries = useMemo(() => series.filter((def) => !hidden.includes(def.dataKey)), [series, hidden]);
4056
+ const handleRangeSelect = useCallback((selection) => {
4057
+ setHistory((current) => [...current, range].slice(-10));
4058
+ onRangeChange(toAbsoluteRange(selection));
4059
+ }, [range, onRangeChange]);
4060
+ const goBack = useCallback(() => {
4061
+ setHistory((current) => {
4062
+ const previous = current[current.length - 1];
4063
+ if (previous) onRangeChange(previous);
4064
+ return current.slice(0, -1);
4065
+ });
4066
+ }, [onRangeChange]);
4067
+ const setRange = useCallback((next) => {
4068
+ setHistory([]);
4069
+ onRangeChange(next);
4070
+ }, [onRangeChange]);
4071
+ const toggleSeries = useCallback((dataKey) => {
4072
+ setHidden((current) => current.includes(dataKey) ? current.filter((key) => key !== dataKey) : [...current, dataKey]);
4073
+ }, []);
4074
+ return /* @__PURE__ */ jsxs("section", {
4075
+ className: cn(OPT_SURFACE, OPT_BORDER, "flex flex-col", className),
4076
+ "aria-label": ariaLabel,
4077
+ "aria-labelledby": ariaLabel == null && title != null ? titleId : void 0,
4078
+ "data-opt-id": "9EHHC",
4079
+ "data-opt-slug": "time-series-panel.TimeSeriesPanel",
4080
+ children: [/* @__PURE__ */ jsxs("header", {
4081
+ className: "gap-group flex flex-wrap items-start justify-between px-5 pt-5 pb-3",
4082
+ children: [/* @__PURE__ */ jsxs("div", {
4083
+ className: "gap-element flex min-w-0 flex-col",
4084
+ children: [title && /* @__PURE__ */ jsx("h3", {
4085
+ id: titleId,
4086
+ className: "text-text-primary truncate text-base font-semibold",
4087
+ children: title
4088
+ }), description && /* @__PURE__ */ jsx("p", {
4089
+ className: cn("text-text-secondary", "truncate text-sm"),
4090
+ children: description
4091
+ })]
4092
+ }), /* @__PURE__ */ jsxs("div", {
4093
+ className: "gap-element flex flex-wrap items-center",
4094
+ children: [
4095
+ history.length > 0 && /* @__PURE__ */ jsx(Button, {
4096
+ variant: "ghost",
4097
+ size: "sm",
4098
+ onClick: goBack,
4099
+ leftIcon: /* @__PURE__ */ jsx(Undo2, {
4100
+ className: "h-3.5 w-3.5",
4101
+ "aria-hidden": "true"
4102
+ }),
4103
+ children: labels.back
4104
+ }),
4105
+ actions,
4106
+ showRangeControl && /* @__PURE__ */ jsx(TimeRangeControl, {
4107
+ value: range,
4108
+ onChange: setRange,
4109
+ presets,
4110
+ recentRanges,
4111
+ timeZone,
4112
+ locale,
4113
+ refresh,
4114
+ onRefreshChange,
4115
+ onRefresh,
4116
+ isLoading,
4117
+ labels: customLabels === null || customLabels === void 0 ? void 0 : customLabels.control
4118
+ })
4119
+ ]
4120
+ })]
4121
+ }), /* @__PURE__ */ jsx("div", {
4122
+ className: "px-2 pb-4",
4123
+ children: /* @__PURE__ */ jsx(TimeSeriesChart, _objectSpread2({
4124
+ data,
4125
+ series: visibleSeries,
4126
+ from: resolved === null || resolved === void 0 ? void 0 : resolved.from,
4127
+ to: resolved === null || resolved === void 0 ? void 0 : resolved.to,
4128
+ granularity,
4129
+ timeZone,
4130
+ locale,
4131
+ height,
4132
+ selectable: true,
4133
+ onRangeSelect: handleRangeSelect,
4134
+ annotations,
4135
+ showLegend: series.length > 1,
4136
+ legendOptions: toggleableLegend ? {
4137
+ onClick: toggleSeries,
4138
+ inactiveKeys: hidden
4139
+ } : void 0,
4140
+ "aria-label": typeof title === "string" ? `${title} time series` : ariaLabel
4141
+ }, chartProps))
4142
+ })]
4143
+ });
4144
+ }
4145
+ TimeSeriesPanel.displayName = "TimeSeriesPanel";
4146
+ //#endregion
4147
+ //#region src/shells/log-table.tsx
4148
+ const HIDE_BELOW = {
4149
+ always: "",
4150
+ sm: "hidden @[40rem]/log:flex",
4151
+ md: "hidden @[46rem]/log:flex",
4152
+ lg: "hidden @[58rem]/log:flex",
4153
+ xl: "hidden @[70rem]/log:flex"
4154
+ };
4155
+ const ROW_HEIGHT = {
4156
+ compact: 26,
4157
+ normal: 32,
4158
+ comfortable: 40
4159
+ };
4160
+ const DENSITY_TEXT = {
4161
+ compact: "text-[11px]",
4162
+ normal: "text-xs",
4163
+ comfortable: "text-sm"
4164
+ };
4165
+ const defaultLabels$1 = {
4166
+ listLabel: "Log",
4167
+ loadingMore: "Loading earlier entries…",
4168
+ endOfResults: "That is every entry in this range.",
4169
+ filterFor: (value) => `Filter for ${value}`,
4170
+ filterOut: (value) => `Filter out ${value}`,
4171
+ sortAscending: "Sort ascending",
4172
+ sortDescending: "Sort descending",
4173
+ moveLeft: (column) => `Move ${column} left`,
4174
+ moveRight: (column) => `Move ${column} right`
4175
+ };
4176
+ /** Rows rendered beyond the viewport on each side, so a fast scroll stays filled. */
4177
+ const OVERSCAN = 10;
4178
+ /**
4179
+ * A stable hue per value. Colour does real work in a log — the eye groups by it
4180
+ * long before it reads the text — so the same event name has to land on the
4181
+ * same hue on every render and in every session, which rules out an index into
4182
+ * a palette.
4183
+ */
4184
+ function hueOf(value) {
4185
+ let hash = 0;
4186
+ for (let index = 0; index < value.length; index += 1) hash = hash * 31 + value.charCodeAt(index) | 0;
4187
+ return Math.abs(hash) % 360;
4188
+ }
4189
+ const EM_DASH = "—";
4190
+ /**
4191
+ * A column with nothing in it is a fact about the entry, not a value. Dimming
4192
+ * every absence the same way lets the eye skip the gaps while scanning, instead
4193
+ * of reading a dash as though it were data.
4194
+ */
4195
+ const ABSENT = "text-text-tertiary/50";
4196
+ function columnStyle(column) {
4197
+ return column.width ? {
4198
+ width: column.width,
4199
+ flex: "0 0 auto"
4200
+ } : {
4201
+ flex: "1 1 0%",
4202
+ minWidth: 0
4203
+ };
4204
+ }
4205
+ /**
4206
+ * LogTable — a windowed table for entries that keep arriving.
4207
+ *
4208
+ * Written by hand rather than delegated to a data grid for the two things a log
4209
+ * needs that a grid does not: rows are inserted at the *top* while the reader
4210
+ * is partway down, which needs a scroll correction applied before paint, and
4211
+ * "am I at the newest row?" has to be answerable from the scroll container to
4212
+ * decide whether to follow the tail or hold position.
4213
+ *
4214
+ * What it takes from a data grid is the part readers actually reach for:
4215
+ * columns are data, so a caller can let people add, drop and reorder them; a
4216
+ * cell offers to filter for or against its own value; and density is a setting
4217
+ * rather than a constant.
4218
+ *
4219
+ * @example
4220
+ * ```tsx
4221
+ * <LogTable
4222
+ * rows={rows}
4223
+ * getRowId={(row) => row.id}
4224
+ * columns={[
4225
+ * { id: "time", header: "Time", width: "9rem", mono: true, value: (r) => r.time },
4226
+ * { id: "name", header: "Event", width: "14rem", field: "name", marker: true, value: (r) => r.name },
4227
+ * { id: "path", header: "Path", field: "path", value: (r) => r.path },
4228
+ * ]}
4229
+ * onCellAction={(action) => setQuery(applyCellAction(query, action))}
4230
+ * />
4231
+ * ```
4232
+ */
4233
+ function LogTableInner({ rows, columns, getRowId, selectedId = null, onSelect, onLoadMore, hasMore = false, loadingMore = false, prepending = false, onFollowChange, isNew, sort = null, onSortChange, onCellAction, highlightTerms, onColumnMove, density = "normal", showHeader = true, emptyState, labels: customLabels, className }, ref) {
4234
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$1), customLabels);
4235
+ const rowHeight = ROW_HEIGHT[density];
4236
+ const scrollRef = useRef(null);
4237
+ const [scrollTop, setScrollTop] = useState(0);
4238
+ const [viewportHeight, setViewportHeight] = useState(0);
4239
+ const followingRef = useRef(true);
4240
+ const previousTopIdRef = useRef(null);
4241
+ const previousLengthRef = useRef(0);
4242
+ useImperativeHandle(ref, () => ({
4243
+ scrollToTop: () => {
4244
+ const container = scrollRef.current;
4245
+ if (!container) return;
4246
+ container.scrollTop = 0;
4247
+ setScrollTop(0);
4248
+ if (!followingRef.current) {
4249
+ followingRef.current = true;
4250
+ onFollowChange === null || onFollowChange === void 0 || onFollowChange(true);
4251
+ }
4252
+ },
4253
+ revealRow: (id) => {
4254
+ const index = rows.findIndex((row) => getRowId(row) === id);
4255
+ const container = scrollRef.current;
4256
+ if (index === -1 || !container) return;
4257
+ const top = index * rowHeight;
4258
+ const bottom = top + rowHeight;
4259
+ if (top < container.scrollTop) container.scrollTop = top;
4260
+ else if (bottom > container.scrollTop + container.clientHeight) container.scrollTop = bottom - container.clientHeight;
4261
+ }
4262
+ }), [
4263
+ onFollowChange,
4264
+ rows,
4265
+ getRowId,
4266
+ rowHeight
4267
+ ]);
4268
+ useEffect(() => {
4269
+ const container = scrollRef.current;
4270
+ if (!container) return;
4271
+ const observer = new ResizeObserver(() => setViewportHeight(container.clientHeight));
4272
+ observer.observe(container);
4273
+ setViewportHeight(container.clientHeight);
4274
+ return () => observer.disconnect();
4275
+ }, []);
4276
+ /**
4277
+ * Report whether the list is pinned to the newest row.
4278
+ *
4279
+ * Called from more than the scroll handler on purpose: the list can arrive at
4280
+ * the top without a scroll event — a reset, a jump to newest, a shorter
4281
+ * result set — and a stale "not following" would silently divert every live
4282
+ * row into a held-back pile.
4283
+ */
4284
+ const syncFollowing = useCallback((container) => {
4285
+ const following = container.scrollTop <= rowHeight;
4286
+ if (following === followingRef.current) return;
4287
+ followingRef.current = following;
4288
+ onFollowChange === null || onFollowChange === void 0 || onFollowChange(following);
4289
+ }, [onFollowChange, rowHeight]);
4290
+ /**
4291
+ * Hold the reader's place when rows arrive above them.
4292
+ *
4293
+ * Runs before paint: by the time the browser draws, the scroll offset has
4294
+ * already absorbed the inserted height, so the content under the cursor never
4295
+ * moves. Skipped while following, where the whole point is to stay at the top.
4296
+ */
4297
+ useLayoutEffect(() => {
4298
+ const container = scrollRef.current;
4299
+ const previousTopId = previousTopIdRef.current;
4300
+ previousTopIdRef.current = rows.length ? getRowId(rows[0]) : null;
4301
+ const previousLength = previousLengthRef.current;
4302
+ previousLengthRef.current = rows.length;
4303
+ if (!container) return;
4304
+ syncFollowing(container);
4305
+ if (!prepending || followingRef.current || !previousTopId) return;
4306
+ if (rows.length <= previousLength) return;
4307
+ const prepended = rows.findIndex((row) => getRowId(row) === previousTopId);
4308
+ if (prepended <= 0) return;
4309
+ container.scrollTop += prepended * rowHeight;
4310
+ setScrollTop(container.scrollTop);
4311
+ }, [
4312
+ rows,
4313
+ getRowId,
4314
+ prepending,
4315
+ rowHeight,
4316
+ syncFollowing
4317
+ ]);
4318
+ /**
4319
+ * Page forward when the rows do not reach the bottom of the viewport.
4320
+ *
4321
+ * Without this the list only advances on a scroll event, so a tall screen —
4322
+ * or a filter matching few rows per page — stops at the first page with no
4323
+ * way to ask for more.
4324
+ */
4325
+ useEffect(() => {
4326
+ const container = scrollRef.current;
4327
+ if (!container || !hasMore || loadingMore || !onLoadMore) return;
4328
+ if (container.scrollHeight <= container.clientHeight) onLoadMore();
4329
+ }, [
4330
+ hasMore,
4331
+ loadingMore,
4332
+ onLoadMore,
4333
+ rows.length,
4334
+ viewportHeight
4335
+ ]);
4336
+ const handleScroll = useCallback((event) => {
4337
+ const container = event.currentTarget;
4338
+ setScrollTop(container.scrollTop);
4339
+ syncFollowing(container);
4340
+ const remaining = container.scrollHeight - container.scrollTop - container.clientHeight;
4341
+ if (hasMore && !loadingMore && remaining < container.clientHeight) onLoadMore === null || onLoadMore === void 0 || onLoadMore();
4342
+ }, [
4343
+ hasMore,
4344
+ loadingMore,
4345
+ onLoadMore,
4346
+ syncFollowing
4347
+ ]);
4348
+ const totalHeight = rows.length * rowHeight;
4349
+ const firstVisible = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
4350
+ const visibleCount = Math.ceil((viewportHeight || 480) / rowHeight) + 20;
4351
+ const lastVisible = Math.min(rows.length, firstVisible + visibleCount);
4352
+ const visibleRows = useMemo(() => rows.slice(firstVisible, lastVisible), [
4353
+ rows,
4354
+ firstVisible,
4355
+ lastVisible
4356
+ ]);
4357
+ return /* @__PURE__ */ jsxs("div", {
4358
+ className: cn("@container/log flex min-h-0 flex-col", className),
4359
+ "data-opt-id": "3ZR82",
4360
+ "data-opt-slug": "log-table.LogTable",
4361
+ children: [showHeader && /* @__PURE__ */ jsx(LogTableHeader, {
4362
+ columns,
4363
+ sort,
4364
+ onSortChange,
4365
+ onColumnMove,
4366
+ density,
4367
+ labels
4368
+ }), /* @__PURE__ */ jsxs("div", {
4369
+ ref: scrollRef,
4370
+ onScroll: handleScroll,
4371
+ className: "relative min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
4372
+ role: "listbox",
4373
+ "aria-label": labels.listLabel,
4374
+ "aria-activedescendant": selectedId ? `log-row-${selectedId}` : void 0,
4375
+ tabIndex: 0,
4376
+ children: [
4377
+ rows.length === 0 ? emptyState : /* @__PURE__ */ jsx("div", {
4378
+ style: { height: totalHeight },
4379
+ className: "relative",
4380
+ children: visibleRows.map((row, index) => {
4381
+ var _isNew;
4382
+ const id = getRowId(row);
4383
+ return /* @__PURE__ */ jsx(LogTableRow, {
4384
+ id,
4385
+ row,
4386
+ columns,
4387
+ top: (firstVisible + index) * rowHeight,
4388
+ height: rowHeight,
4389
+ density,
4390
+ selected: id === selectedId,
4391
+ isNew: (_isNew = isNew === null || isNew === void 0 ? void 0 : isNew(row)) !== null && _isNew !== void 0 ? _isNew : false,
4392
+ onSelect,
4393
+ onCellAction,
4394
+ highlightTerms,
4395
+ labels
4396
+ }, id);
4397
+ })
4398
+ }),
4399
+ loadingMore && /* @__PURE__ */ jsxs("div", {
4400
+ className: "gap-element text-text-tertiary flex items-center justify-center py-3 text-xs",
4401
+ children: [/* @__PURE__ */ jsx(Spinner, {
4402
+ size: "sm",
4403
+ "aria-hidden": true
4404
+ }), labels.loadingMore]
4405
+ }),
4406
+ !hasMore && rows.length > 0 && /* @__PURE__ */ jsx("p", {
4407
+ className: cn("text-text-tertiary", "py-3 text-center text-xs"),
4408
+ children: labels.endOfResults
4409
+ })
4410
+ ]
4411
+ })]
4412
+ });
4413
+ }
4414
+ /**
4415
+ * LogTable — see {@link LogTableInner}.
4416
+ *
4417
+ * The cast preserves the row generic through `forwardRef`, which otherwise
4418
+ * widens it to its constraint and takes the column callbacks' types with it.
4419
+ */
4420
+ const LogTable = forwardRef(LogTableInner);
4421
+ function LogTableHeader({ columns, sort, onSortChange, onColumnMove, density, labels }) {
4422
+ return /* @__PURE__ */ jsx("div", {
4423
+ role: "row",
4424
+ className: cn("border-border-subtle text-text-tertiary flex shrink-0 items-center gap-3 border-b px-3", DENSITY_TEXT[density]),
4425
+ style: { height: ROW_HEIGHT[density] },
4426
+ children: columns.map((column, index) => {
4427
+ var _column$priority;
4428
+ const active = (sort === null || sort === void 0 ? void 0 : sort.columnId) === column.id;
4429
+ const sortable = column.sortable && onSortChange;
4430
+ const columnLabel = typeof column.header === "string" ? column.header : column.id;
4431
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
4432
+ column.marker && /* @__PURE__ */ jsx("span", {
4433
+ "aria-hidden": true,
4434
+ className: "size-1.5 shrink-0"
4435
+ }),
4436
+ /* @__PURE__ */ jsx("span", {
4437
+ className: "truncate",
4438
+ children: column.header
4439
+ }),
4440
+ active && (sort.direction === "asc" ? /* @__PURE__ */ jsx(ChevronUp, {
4441
+ className: "size-3 shrink-0",
4442
+ "aria-hidden": true
4443
+ }) : /* @__PURE__ */ jsx(ChevronDown, {
4444
+ className: "size-3 shrink-0",
4445
+ "aria-hidden": true
4446
+ }))
4447
+ ] });
4448
+ return /* @__PURE__ */ jsxs("div", {
4449
+ role: "columnheader",
4450
+ "aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
4451
+ className: cn("group/header flex items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority = column.priority) !== null && _column$priority !== void 0 ? _column$priority : "always"]),
4452
+ style: columnStyle(column),
4453
+ children: [sortable ? /* @__PURE__ */ jsx("button", {
4454
+ type: "button",
4455
+ className: "hover:text-text-primary flex cursor-pointer items-center gap-1.5 truncate",
4456
+ title: active && sort.direction === "desc" ? labels.sortAscending : labels.sortDescending,
4457
+ onClick: () => onSortChange({
4458
+ columnId: column.id,
4459
+ direction: active && sort.direction === "desc" ? "asc" : "desc"
3924
4460
  }),
3925
- text.length > 0 && /* @__PURE__ */ jsx("button", {
4461
+ children: content
4462
+ }) : content, onColumnMove && /* @__PURE__ */ jsxs("span", {
4463
+ className: cn("ml-auto flex shrink-0 items-center gap-0.5", "opacity-0 transition-opacity", "group-hover/header:opacity-100 focus-within:opacity-100"),
4464
+ children: [index > 0 && /* @__PURE__ */ jsx("button", {
3926
4465
  type: "button",
3927
- "aria-label": labels.clear,
3928
- title: labels.clear,
3929
- className: cn("text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5", "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset"),
3930
- onClick: () => {
3931
- setText("");
3932
- commit("");
3933
- },
3934
- children: /* @__PURE__ */ jsx(X, {
3935
- className: "size-3.5",
4466
+ className: "hover:text-text-primary cursor-pointer",
4467
+ "aria-label": labels.moveLeft(columnLabel),
4468
+ title: labels.moveLeft(columnLabel),
4469
+ onClick: () => onColumnMove(column.id, "left"),
4470
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
4471
+ className: "size-3",
4472
+ "aria-hidden": true
4473
+ })
4474
+ }), index < columns.length - 1 && /* @__PURE__ */ jsx("button", {
4475
+ type: "button",
4476
+ className: "hover:text-text-primary cursor-pointer",
4477
+ "aria-label": labels.moveRight(columnLabel),
4478
+ title: labels.moveRight(columnLabel),
4479
+ onClick: () => onColumnMove(column.id, "right"),
4480
+ children: /* @__PURE__ */ jsx(ChevronRight, {
4481
+ className: "size-3",
3936
4482
  "aria-hidden": true
3937
4483
  })
3938
- })
3939
- ]
3940
- }),
3941
- showSuggestions && /* @__PURE__ */ jsx("ul", {
3942
- id: "query-bar-suggestions",
3943
- role: "listbox",
3944
- className: cn("bg-surface-overlay absolute top-10 z-40 max-h-64 w-full overflow-y-auto rounded-md border p-1 shadow-lg", "border border-border"),
3945
- children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
3946
- type: "button",
3947
- role: "option",
3948
- "aria-selected": index === highlighted,
3949
- className: cn("flex w-full cursor-pointer items-center justify-between gap-3 rounded px-2 py-1 text-left text-sm", index === highlighted ? "bg-bg-subtle" : "hover:bg-bg-subtle"),
3950
- onMouseEnter: () => setHighlighted(index),
3951
- onMouseDown: (event) => event.preventDefault(),
3952
- onClick: () => applySuggestion(suggestion),
3953
- children: [/* @__PURE__ */ jsx("span", {
3954
- className: "text-text-primary truncate font-mono",
3955
- children: suggestion.label
3956
- }), suggestion.hint && /* @__PURE__ */ jsx("span", {
3957
- className: "text-text-tertiary shrink-0 text-xs",
3958
- children: suggestion.hint
3959
4484
  })]
3960
- }) }, `${suggestion.kind}-${suggestion.label}`))
3961
- }),
3962
- showClauses && value.clauses.length > 0 && /* @__PURE__ */ jsx("ul", {
3963
- className: "flex flex-wrap items-center gap-1.5",
3964
- children: value.clauses.map((clause, index) => {
3965
- const label = describeClause(clause);
3966
- return /* @__PURE__ */ jsxs("li", {
3967
- className: cn("bg-bg-subtle text-text-secondary flex items-center gap-1 rounded px-1.5 py-0.5 text-xs", clause.negated && "text-danger-fg"),
3968
- children: [/* @__PURE__ */ jsx("span", {
3969
- className: "truncate",
3970
- children: label
3971
- }), /* @__PURE__ */ jsx("button", {
3972
- type: "button",
3973
- "aria-label": labels.removeClause(label),
3974
- className: "hover:text-text-primary cursor-pointer",
3975
- onClick: () => {
3976
- const next = removeClause(value, index);
3977
- setText(formatQuery(next));
3978
- committedRef.current = formatQuery(next);
3979
- onChange(next);
3980
- },
3981
- children: /* @__PURE__ */ jsx(X, {
3982
- className: "size-3",
3983
- "aria-hidden": true
3984
- })
3985
- })]
3986
- }, `${label}-${index}`);
3987
- })
3988
- }),
3989
- parsed.diagnostics.length > 0 && /* @__PURE__ */ jsx(QueryDiagnostics, { diagnostics: parsed.diagnostics }),
3990
- labels.syntaxHint && !showSuggestions && /* @__PURE__ */ jsx("p", {
3991
- className: "text-text-tertiary text-xs",
3992
- children: labels.syntaxHint
3993
- })
3994
- ]
4485
+ })]
4486
+ }, column.id);
4487
+ })
3995
4488
  });
3996
4489
  }
3997
- QueryBar.displayName = "QueryBar";
3998
- function QueryDiagnostics({ diagnostics }) {
3999
- return /* @__PURE__ */ jsx("ul", {
4000
- className: "text-warning-fg flex flex-col gap-0.5 text-xs",
4001
- children: diagnostics.map((diagnostic, index) => /* @__PURE__ */ jsxs("li", { children: [
4002
- /* @__PURE__ */ jsx("code", {
4003
- className: "font-mono",
4004
- children: diagnostic.text
4005
- }),
4006
- " ",
4007
- " ",
4008
- diagnostic.message
4009
- ] }, `${diagnostic.at}-${index}`))
4490
+ function LogTableRowImpl({ id, row, columns, top, height, density, selected, isNew, onSelect, onCellAction, highlightTerms, labels }) {
4491
+ return /* @__PURE__ */ jsx("div", {
4492
+ role: "option",
4493
+ "aria-selected": selected,
4494
+ tabIndex: -1,
4495
+ id: `log-row-${id}`,
4496
+ "data-log-row-id": id,
4497
+ onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(id),
4498
+ onKeyDown: (event) => {
4499
+ if (event.key !== "Enter" && event.key !== " ") return;
4500
+ event.preventDefault();
4501
+ onSelect === null || onSelect === void 0 || onSelect(id);
4502
+ },
4503
+ style: {
4504
+ top,
4505
+ height
4506
+ },
4507
+ className: cn("border-border-subtle/60 absolute inset-x-0 flex items-center gap-3 border-b px-3", DENSITY_TEXT[density], onSelect && "cursor-pointer", "transition-colors duration-100", "hover:bg-surface-hover", selected && "bg-accent-subtle hover:bg-accent-subtle", isNew && !selected && "opt-log-row-enter"),
4508
+ children: columns.map((column) => {
4509
+ var _column$priority2;
4510
+ const raw = column.value(row);
4511
+ const text = raw === null || raw === void 0 || raw === "" ? "" : String(raw);
4512
+ return /* @__PURE__ */ jsxs("div", {
4513
+ className: cn("group/cell relative flex min-w-0 items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority2 = column.priority) !== null && _column$priority2 !== void 0 ? _column$priority2 : "always"]),
4514
+ style: columnStyle(column),
4515
+ children: [
4516
+ column.marker && text && /* @__PURE__ */ jsx("span", {
4517
+ "aria-hidden": true,
4518
+ className: "size-1.5 shrink-0 rounded-full",
4519
+ style: { backgroundColor: `hsl(${hueOf(text)} 65% 55%)` }
4520
+ }),
4521
+ /* @__PURE__ */ jsx("span", {
4522
+ className: cn("min-w-0 flex-1", !column.truncate && "truncate", column.mono && "font-mono", text ? "text-text-secondary" : ABSENT),
4523
+ title: column.truncate ? void 0 : text || void 0,
4524
+ children: column.render ? column.render(row) : text ? column.truncate ? /* @__PURE__ */ jsx(TextTruncate, {
4525
+ text,
4526
+ truncation: column.truncate,
4527
+ children: (shown) => highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ jsx(Highlight, {
4528
+ text: shown,
4529
+ search: highlightTerms
4530
+ }) : shown
4531
+ }) : highlightTerms && highlightTerms.length > 0 ? /* @__PURE__ */ jsx(Highlight, {
4532
+ text,
4533
+ search: highlightTerms
4534
+ }) : text : EM_DASH
4535
+ }),
4536
+ onCellAction && column.field && text && /* @__PURE__ */ jsx(CellActions, {
4537
+ field: column.field,
4538
+ value: text,
4539
+ onAction: onCellAction,
4540
+ labels
4541
+ })
4542
+ ]
4543
+ }, column.id);
4544
+ })
4010
4545
  });
4011
4546
  }
4012
4547
  /**
4013
- * What to offer, based on the token under the cursor.
4548
+ * Memoized because a live log re-renders several times a second while almost
4549
+ * every visible row is unchanged.
4550
+ */
4551
+ const LogTableRow = memo(LogTableRowImpl);
4552
+ /**
4553
+ * The two questions a reader has about a value they just spotted: show me only
4554
+ * these, and get these out of my way.
4014
4555
  *
4015
- * Completing fields is the part that makes the syntax discoverable at all
4016
- * nobody guesses that `utm_source` is a field they can filter on. Once the
4017
- * field is chosen, its known values are the second half of the same guess.
4556
+ * Revealed on hover rather than always drawn a log is scanned, and a pair of
4557
+ * buttons on every cell of every row is a wall of chrome. They stay in the tab
4558
+ * order regardless, since hover is not an interaction everyone has.
4018
4559
  */
4019
- function useSuggestions(text, trailing, fields) {
4020
- return useMemo(() => {
4021
- const lastSpace = text.lastIndexOf(" ");
4022
- const head = lastSpace === -1 ? "" : text.slice(0, lastSpace + 1);
4023
- const token = text.slice(lastSpace + 1);
4024
- if (trailing) {
4025
- var _field$values;
4026
- const field = fields.find((candidate) => candidate.name === trailing.field);
4027
- const values = (_field$values = field === null || field === void 0 ? void 0 : field.values) !== null && _field$values !== void 0 ? _field$values : [];
4028
- const typed = trailing.value.toLowerCase();
4029
- return values.filter((value) => value.toLowerCase().includes(typed)).slice(0, MAX_SUGGESTIONS).map((value) => ({
4030
- kind: "value",
4031
- label: `${trailing.field}:${value}`,
4032
- replace: () => `${head}${trailing.field}:${/[\s"()]/.test(value) ? `"${value}"` : value} `
4033
- }));
4034
- }
4035
- const bare = token.replace(/^-/, "").toLowerCase();
4036
- if (bare.length === 0) return [];
4037
- return fields.filter((field) => field.name.toLowerCase().includes(bare)).slice(0, MAX_SUGGESTIONS).map((field) => ({
4038
- kind: "field",
4039
- label: `${field.name}:`,
4040
- hint: field.hint,
4041
- replace: () => `${head}${token.startsWith("-") ? "-" : ""}${field.name}:`
4042
- }));
4043
- }, [
4044
- text,
4045
- trailing,
4046
- fields
4047
- ]);
4560
+ function CellActions({ field, value, onAction, labels }) {
4561
+ const button = "cursor-pointer rounded-sm p-0.5 text-text-tertiary hover:bg-bg-muted hover:text-text-primary";
4562
+ return /* @__PURE__ */ jsxs("span", {
4563
+ className: cn("bg-surface/90 ml-auto flex shrink-0 items-center gap-0.5 rounded pl-1", "opacity-0 transition-opacity", "group-hover/cell:opacity-100 focus-within:opacity-100"),
4564
+ children: [/* @__PURE__ */ jsx("button", {
4565
+ type: "button",
4566
+ className: button,
4567
+ title: labels.filterFor(value),
4568
+ "aria-label": labels.filterFor(value),
4569
+ onClick: (event) => {
4570
+ event.stopPropagation();
4571
+ onAction({
4572
+ field,
4573
+ value,
4574
+ intent: "include"
4575
+ });
4576
+ },
4577
+ children: /* @__PURE__ */ jsx(Plus, {
4578
+ className: "size-3",
4579
+ "aria-hidden": true
4580
+ })
4581
+ }), /* @__PURE__ */ jsx("button", {
4582
+ type: "button",
4583
+ className: button,
4584
+ title: labels.filterOut(value),
4585
+ "aria-label": labels.filterOut(value),
4586
+ onClick: (event) => {
4587
+ event.stopPropagation();
4588
+ onAction({
4589
+ field,
4590
+ value,
4591
+ intent: "exclude"
4592
+ });
4593
+ },
4594
+ children: /* @__PURE__ */ jsx(Minus, {
4595
+ className: "size-3",
4596
+ "aria-hidden": true
4597
+ })
4598
+ })]
4599
+ });
4048
4600
  }
4049
4601
  //#endregion
4050
4602
  //#region src/shells/field-sidebar.tsx
@@ -4270,6 +4822,4 @@ function FieldValues({ values, loading, total, sampledRows, onFilter, labels })
4270
4822
  });
4271
4823
  }
4272
4824
  //#endregion
4273
- export { CommandList$1 as $, DataTable as A, SidebarNav as B, WorkflowCanvas as C, ToastProvider as D, NotificationToast as E, DeploymentTimeline as F, CommandPaletteTrigger as G, StatusSelect as H, SettingsForm as I, CommandDialog as J, CommandPalette as K, FaqAccordion as L, BranchSelect as M, EnvPanel as N, toast as O, ProjectSwitcher as P, CommandItem$1 as Q, ContentTabs as R, useAutoRefresh as S, FailureList as T, SearchCombobox as U, AppMenubar as V, DashboardGrid as W, CommandGroup$1 as X, CommandEmpty$1 as Y, CommandInput$1 as Z, withoutField as _, formatQuery as a, DEFAULT_TIME_PRESETS as b, parseQueryWithDiagnostics as c, applyCellAction as d, CommandSeparator$1 as et, fieldValues as f, withFieldValue as g, removeClause as h, formatClause as i, DomainTable as j, useToast as k, EMPTY_QUERY as l, queryFields as m, QueryBar as n, quoteValue as o, isEmptyQuery as p, Command as q, describeClause as r, parseQuery as s, FieldSidebar as t, CommandShortcut as tt, addClause as u, LogTable as v, KeyValueEditor as w, TimeRangeControl as x, TimeSeriesPanel as y, EditorToolbar as z };
4274
-
4275
- //# sourceMappingURL=field-sidebar-BXjvV7Cz.js.map
4825
+ export { CommandGroup$1 as $, ToastProvider as A, FaqAccordion as B, Flyout as C, KeyValueEditor as D, WorkflowCanvas as E, BranchSelect as F, StatusSelect as G, EditorToolbar as H, EnvPanel as I, CommandPaletteTrigger as J, SearchCombobox as K, ProjectSwitcher as L, useToast as M, DataTable as N, FailureList as O, DomainTable as P, CommandEmpty$1 as Q, DeploymentTimeline as R, withoutField as S, EventTimeline as T, SidebarNav as U, ContentTabs as V, AppMenubar as W, Command as X, CommandPalette as Y, CommandDialog as Z, fieldValues as _, TimeRangeControl as a, removeClause as b, describeClause as c, quoteValue as d, CommandInput$1 as et, parseQuery as f, applyCellAction as g, addClause as h, DEFAULT_TIME_PRESETS as i, CommandShortcut as it, toast as j, NotificationToast as k, formatClause as l, EMPTY_QUERY as m, LogTable as n, CommandList$1 as nt, useAutoRefresh as o, parseQueryWithDiagnostics as p, DashboardGrid as q, TimeSeriesPanel as r, CommandSeparator$1 as rt, QueryBar as s, FieldSidebar as t, CommandItem$1 as tt, formatQuery as u, isEmptyQuery as v, ScoreBreakdown as w, withFieldValue as x, queryFields as y, SettingsForm as z };