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