@reopt-ai/opt-ui 1.6.0 → 1.7.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 (72) hide show
  1. package/COMPONENT_CATALOG.md +95 -12
  2. package/dist/app.css +24 -0
  3. package/dist/core/index.cjs +3 -3
  4. package/dist/core/index.d.cts +1 -1
  5. package/dist/core/index.d.ts +1 -1
  6. package/dist/core/index.js +2 -2
  7. package/dist/docs/02-components/03-shells.md +104 -11
  8. package/dist/docs/02-components/index.md +3 -3
  9. package/dist/docs/05-migration/01-breaking-changes.md +18 -1
  10. package/dist/field-sidebar-BTZCBviA.d.cts +1031 -0
  11. package/dist/field-sidebar-BTZCBviA.d.cts.map +1 -0
  12. package/dist/field-sidebar-CB8vjvUM.d.ts +1031 -0
  13. package/dist/field-sidebar-CB8vjvUM.d.ts.map +1 -0
  14. package/dist/{workflow-canvas-BGBAcjf-.js → field-sidebar-Cs-J-5VE.js} +1882 -52
  15. package/dist/field-sidebar-Cs-J-5VE.js.map +1 -0
  16. package/dist/{workflow-canvas-3Jeevkgy.cjs → field-sidebar-CzIQX70-.cjs} +2010 -48
  17. package/dist/id-registry.cjs +41 -1
  18. package/dist/id-registry.d.cts +1 -1
  19. package/dist/id-registry.d.cts.map +1 -1
  20. package/dist/id-registry.d.ts +1 -1
  21. package/dist/id-registry.d.ts.map +1 -1
  22. package/dist/id-registry.js +41 -1
  23. package/dist/id-registry.js.map +1 -1
  24. package/dist/id-registry.json +85 -0
  25. package/dist/index.cjs +142 -56
  26. package/dist/index.d.cts +149 -80
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +149 -80
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +73 -11
  31. package/dist/index.js.map +1 -1
  32. package/dist/meta.cjs +292 -0
  33. package/dist/meta.d.cts.map +1 -1
  34. package/dist/meta.d.ts.map +1 -1
  35. package/dist/meta.js +292 -0
  36. package/dist/meta.js.map +1 -1
  37. package/dist/{pagination-By7Ozv3U.js → pagination-DWbvACuN.js} +93 -31
  38. package/dist/pagination-DWbvACuN.js.map +1 -0
  39. package/dist/{pagination-Uv3CnGJz.cjs → pagination-DjfxBcjX.cjs} +98 -36
  40. package/dist/query.cjs +386 -0
  41. package/dist/query.d.cts +141 -0
  42. package/dist/query.d.cts.map +1 -0
  43. package/dist/query.d.ts +141 -0
  44. package/dist/query.d.ts.map +1 -0
  45. package/dist/query.js +371 -0
  46. package/dist/query.js.map +1 -0
  47. package/dist/{shader-surface-BWhO2xkk.js → shader-surface-BUPZ0P-n.js} +22 -83
  48. package/dist/shader-surface-BUPZ0P-n.js.map +1 -0
  49. package/dist/{shader-surface-DIVWtpFl.d.ts → shader-surface-BfTP9c1Q.d.ts} +139 -139
  50. package/dist/shader-surface-BfTP9c1Q.d.ts.map +1 -0
  51. package/dist/{shader-surface-BK1GMt5_.d.cts → shader-surface-DRrcLrWf.d.cts} +139 -139
  52. package/dist/shader-surface-DRrcLrWf.d.cts.map +1 -0
  53. package/dist/{shader-surface-DxLLfrE6.cjs → shader-surface-Fav5K9UC.cjs} +20 -87
  54. package/dist/shells/index.cjs +31 -24
  55. package/dist/shells/index.d.cts +2 -2
  56. package/dist/shells/index.d.ts +2 -2
  57. package/dist/shells/index.js +3 -2
  58. package/dist/theme/presets/default.css +2 -2
  59. package/dist/theme/presets/minimal.css +2 -2
  60. package/dist/theme/presets/mono-dark.css +2 -2
  61. package/dist/theme/presets/natural.css +1 -1
  62. package/dist/theme/presets/pro.css +2 -2
  63. package/package.json +17 -6
  64. package/dist/pagination-By7Ozv3U.js.map +0 -1
  65. package/dist/shader-surface-BK1GMt5_.d.cts.map +0 -1
  66. package/dist/shader-surface-BWhO2xkk.js.map +0 -1
  67. package/dist/shader-surface-DIVWtpFl.d.ts.map +0 -1
  68. package/dist/workflow-canvas-BGBAcjf-.js.map +0 -1
  69. package/dist/workflow-canvas-Bar_O2SQ.d.ts +0 -494
  70. package/dist/workflow-canvas-Bar_O2SQ.d.ts.map +0 -1
  71. package/dist/workflow-canvas-DghDRtHn.d.cts +0 -494
  72. package/dist/workflow-canvas-DghDRtHn.d.cts.map +0 -1
@@ -1,10 +1,11 @@
1
1
  "use client";
2
- import { $ as TooltipAnchor$1, A as FormSwitch$1, An as OPT_SURFACE_POPOVER, At as SelectPopover$1, Bn as _objectSpread2, Bt as Composite, C as FormRoot$1, Cn as OPT_FOCUS_VISIBLE, Dt as DialogRoot$1, E as FormSelectPopover, Et as DialogPanel$1, F as useFormStore, Fn as OPT_TEXT_LABEL, Ft as TabPanel$1, Hn as Kbd, Ht as CompositeProvider, In as OPT_TEXT_PRIMARY, It as TabsRoot$1, J as ToolbarButton$1, Jt as Spinner, Ln as OPT_TEXT_SECONDARY, Lt as DisclosureContent$1, M as FormTextarea, Mn as OPT_TD, Mt as SelectTrigger$1, Nn as OPT_TD_MUTED, Nt as Tab$1, On as OPT_ITEM_BASE, Ot as SelectItem$1, Pt as TabList$1, Q as Tooltip$1, R as TagInput, Rn as OPT_TEXT_TERTIARY, Rt as DisclosureRoot$1, Sn as OPT_FOCUS, T as FormSelectItem, Tn as OPT_INPUT, Tt as DialogHeading$1, Un as cn, Ut as CompositeRow, Vn as _objectWithoutProperties, Vt as CompositeItem, X as ToolbarRoot$1, Xt as EmptyState, Y as ToolbarContainer$1, Yt as Badge, Z as ToolbarSeparator$1, _n as OPT_DIVIDER, _t as ComboboxRoot$1, at as MenuPopover$1, b as FormRadioGroup$1, bn as OPT_DOT_RED, bt as DialogDescription$1, cn as OPT_ANIMATE_ENTER_EXIT, ct as MenuTrigger$1, dn as OPT_BADGE_NEUTRAL, dt as ComboboxEmpty, et as TooltipProvider$1, fn as OPT_BADGE_RED, gn as OPT_DISCLOSURE_TRIGGER, gt as ComboboxPopover$1, h as FormLabel$1, hn as OPT_DISCLOSURE_CONTENT, ht as ComboboxItem$1, i as FormCheckbox$1, it as MenuItemRadio$1, jn as OPT_TABLE_ROW_ACTIVE, jt as SelectRoot$1, k as FormSubmit$1, kt as SelectLabel$1, l as FormError$1, ln as OPT_BADGE_BLUE, lt as MenubarContainer$1, m as FormInput$1, mn as OPT_BORDER, mt as ComboboxInput$1, n as Checkbox, nt as MenuItem$1, on as OPT_ACTIVE_ITEM, ot as MenuRoot$1, pn as OPT_BADGE_YELLOW, qt as Alert, rt as MenuItemCheckbox$1, st as MenuSeparator$1, t as Pagination, tn as StatCard, tt as MenuButtonArrow$1, un as OPT_BADGE_GREEN, ut as MenubarRoot$1, w as FormSelect, xn as OPT_DOT_YELLOW, y as FormRadio$1, yn as OPT_DOT_GREEN, zn as OPT_TH, zt as DisclosureTrigger$1 } from "./pagination-By7Ozv3U.js";
2
+ import { $ as TooltipAnchor$1, A as FormSwitch$1, An as OPT_SURFACE_POPOVER, At as SelectPopover$1, Bn as _objectSpread2, Bt as Composite, C as FormRoot$1, Cn as OPT_FOCUS, Dt as DialogRoot$1, E as FormSelectPopover, En as OPT_INPUT, Et as DialogPanel$1, F as useFormStore, Fn as OPT_TEXT_LABEL, Ft as TabPanel$1, G as PopoverTrigger$1, Hn as Kbd, Ht as CompositeProvider, In as OPT_TEXT_PRIMARY, It as TabsRoot$1, J as ToolbarButton$1, Jt as Alert, Kt as Input, Ln as OPT_TEXT_SECONDARY, Lt as DisclosureContent$1, M as FormTextarea, Mn as OPT_TD, Mt as SelectTrigger$1, Nn as OPT_TD_MUTED, Nt as Tab$1, On as OPT_ITEM_BASE, Ot as SelectItem$1, Pt as TabList$1, Q as Tooltip$1, Qt as Button, R as TagInput, Rn as OPT_TEXT_TERTIARY, Rt as DisclosureRoot$1, Sn as OPT_DOT_YELLOW, T as FormSelectItem, Tt as DialogHeading$1, U as PopoverContent$1, Un as cn, Ut as CompositeRow, Vn as _objectWithoutProperties, Vt as CompositeItem, W as PopoverRoot$1, X as ToolbarRoot$1, Xt as Badge, Y as ToolbarContainer$1, Yt as Spinner, Z as ToolbarSeparator$1, Zt as EmptyState, _n as OPT_DISCLOSURE_TRIGGER, _t as ComboboxRoot$1, at as MenuPopover$1, b as FormRadioGroup$1, bn as OPT_DOT_GREEN, bt as DialogDescription$1, ct as MenuTrigger$1, dn as OPT_BADGE_GREEN, dt as ComboboxEmpty, et as TooltipProvider$1, fn as OPT_BADGE_NEUTRAL, gn as OPT_DISCLOSURE_CONTENT, gt as ComboboxPopover$1, h as FormLabel$1, hn as OPT_BORDER, ht as ComboboxItem$1, i as FormCheckbox$1, it as MenuItemRadio$1, jn as OPT_TABLE_ROW_ACTIVE, jt as SelectRoot$1, k as FormSubmit$1, kn as OPT_SURFACE, kt as SelectLabel$1, l as FormError$1, ln as OPT_ANIMATE_ENTER_EXIT, lt as MenubarContainer$1, m as FormInput$1, mn as OPT_BADGE_YELLOW, mt as ComboboxInput$1, n as Checkbox, nn as StatCard, nt as MenuItem$1, ot as MenuRoot$1, pn as OPT_BADGE_RED, rt as MenuItemCheckbox$1, sn as OPT_ACTIVE_ITEM, st as MenuSeparator$1, t as Pagination, tt as MenuButtonArrow$1, un as OPT_BADGE_BLUE, ut as MenubarRoot$1, vn as OPT_DIVIDER, w as FormSelect, wn as OPT_FOCUS_VISIBLE, xn as OPT_DOT_RED, y as FormRadio$1, zn as OPT_TH, zt as DisclosureTrigger$1 } from "./pagination-DWbvACuN.js";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import * as React from "react";
5
- import { Fragment as Fragment$1, createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { Fragment as Fragment$1, createContext, forwardRef, memo, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandRoot, CommandSeparator } from "@reopt-ai/opt-ui-primitives";
7
- import { SearchIcon } from "lucide-react";
7
+ import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Clock, Columns3, Minus, Plus, RefreshCw, Search, SearchIcon, Undo2, X, ZoomIn, ZoomOut } from "lucide-react";
8
+ import { TimeSeriesChart, formatCountUnit, formatDuration, formatInstant, isDateOnly, isExactMinutes, isRelativeToNow, measureTimeWindow, parseRelativeParts, parseTimeExpression, resolveTimeRange, shiftTimeRange, toAbsoluteRange, zoomTimeRange } from "@reopt-ai/opt-charts";
8
9
  import { matchSorter } from "match-sorter";
9
10
  import Link from "next/link";
10
11
  import { usePathname } from "next/navigation";
@@ -227,7 +228,6 @@ function CommandPalette({ commands, open, onOpenChange, onSelect, inline = false
227
228
  //#region src/shells/command-palette-trigger.tsx
228
229
  /** Renders the command palette trigger component. */
229
230
  function CommandPaletteTrigger({ commands, onSelect, labels }) {
230
- if (commands.length === 0) return null;
231
231
  const [open, setOpen] = useState(false);
232
232
  const handleKeyDown = useCallback((e) => {
233
233
  if ((e.metaKey || e.ctrlKey) && e.key === "k") {
@@ -236,9 +236,11 @@ function CommandPaletteTrigger({ commands, onSelect, labels }) {
236
236
  }
237
237
  }, []);
238
238
  useEffect(() => {
239
+ if (commands.length === 0) return;
239
240
  document.addEventListener("keydown", handleKeyDown);
240
241
  return () => document.removeEventListener("keydown", handleKeyDown);
241
- }, [handleKeyDown]);
242
+ }, [commands.length, handleKeyDown]);
243
+ if (commands.length === 0) return null;
242
244
  return /* @__PURE__ */ jsx(CommandPalette, {
243
245
  commands,
244
246
  open,
@@ -251,7 +253,7 @@ function CommandPaletteTrigger({ commands, onSelect, labels }) {
251
253
  }
252
254
  //#endregion
253
255
  //#region src/shells/dashboard-grid.tsx
254
- const defaultLabels$13 = { ariaLabel: "대시보드 통계" };
256
+ const defaultLabels$18 = { ariaLabel: "대시보드 통계" };
255
257
  const MIN_DASHBOARD_COLUMNS = 1;
256
258
  const MAX_DASHBOARD_COLUMNS = 12;
257
259
  function normalizeDashboardColumns(columns) {
@@ -260,7 +262,7 @@ function normalizeDashboardColumns(columns) {
260
262
  }
261
263
  /** Renders the dashboard grid component. */
262
264
  function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
263
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
265
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$18), customLabels);
264
266
  const columnCount = normalizeDashboardColumns(columns);
265
267
  const rowStyle = { "--opt-dashboard-grid-columns": `repeat(${columnCount}, minmax(0, 1fr))` };
266
268
  const rows = [];
@@ -288,13 +290,13 @@ function DashboardGrid({ stats, columns = 3, labels: customLabels }) {
288
290
  }
289
291
  //#endregion
290
292
  //#region src/shells/search-combobox.tsx
291
- const defaultLabels$12 = {
293
+ const defaultLabels$17 = {
292
294
  noResults: "결과가 없습니다.",
293
295
  loading: "Loading…"
294
296
  };
295
297
  /** Renders the search combobox component. */
296
298
  function SearchCombobox({ items, placeholder = "검색...", label = "검색", onSelect, labels: customLabels, async: isAsync = false, onSearch, debounceMs = 300 }) {
297
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels);
299
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$17), customLabels);
298
300
  const inputId = useId();
299
301
  const [query, setQuery] = useState("");
300
302
  const [asyncResults, setAsyncResults] = useState([]);
@@ -392,14 +394,14 @@ function SearchCombobox({ items, placeholder = "검색...", label = "검색", on
392
394
  }
393
395
  //#endregion
394
396
  //#region src/shells/status-select.tsx
395
- const defaultLabels$11 = {
397
+ const defaultLabels$16 = {
396
398
  selectLabel: "상태",
397
399
  emptyMessage: "사용 가능한 상태 옵션이 없습니다."
398
400
  };
399
401
  /** Renders the status select component. */
400
402
  function StatusSelect({ options, value: controlledValue, onChange, label, labels: customLabels }) {
401
403
  var _options$0$value, _options$;
402
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
404
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$16), customLabels);
403
405
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
404
406
  const [internalValue, setInternalValue] = useState((_options$0$value = (_options$ = options[0]) === null || _options$ === void 0 ? void 0 : _options$.value) !== null && _options$0$value !== void 0 ? _options$0$value : "");
405
407
  const value = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalValue;
@@ -466,7 +468,7 @@ function StatusSelect({ options, value: controlledValue, onChange, label, labels
466
468
  }
467
469
  //#endregion
468
470
  //#region src/shells/app-menubar.tsx
469
- const defaultLabels$10 = { ariaLabel: "Application menu" };
471
+ const defaultLabels$15 = { ariaLabel: "Application menu" };
470
472
  function MenuSection({ menu }) {
471
473
  return /* @__PURE__ */ jsxs(MenuRoot$1, { children: [/* @__PURE__ */ jsxs(MenuTrigger$1, { children: [menu.label, /* @__PURE__ */ jsx(MenuButtonArrow$1, {})] }), /* @__PURE__ */ jsx(MenuPopover$1, { children: menu.items.map((item, i) => {
472
474
  if (item.type === "separator") return /* @__PURE__ */ jsx(MenuSeparator$1, {}, `sep-${i}`);
@@ -510,7 +512,7 @@ function MenuSection({ menu }) {
510
512
  }
511
513
  /** Renders the app menubar component. */
512
514
  function AppMenubar({ menus, labels: customLabels }) {
513
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
515
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$15), customLabels);
514
516
  if (menus.length === 0) return null;
515
517
  return /* @__PURE__ */ jsx(MenubarRoot$1, {
516
518
  "data-opt-id": "BGKHW",
@@ -549,11 +551,10 @@ const sectionStyles = {
549
551
  Templates: "text-text-tertiary"
550
552
  };
551
553
  const defaultSectionStyle = "text-text-secondary";
552
- const defaultLabels$9 = { ariaLabel: "문서 네비게이션" };
554
+ const defaultLabels$14 = { ariaLabel: "문서 네비게이션" };
553
555
  /** Renders the sidebar nav component. */
554
556
  function SidebarNav({ items, labels: customLabels }) {
555
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$9), customLabels);
556
- if (items.length === 0) return null;
557
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$14), customLabels);
557
558
  const pathname = usePathname();
558
559
  const activeItem = items.find((item) => item.href === pathname);
559
560
  const [activeId, setActiveId] = useState(activeItem ? domId(activeItem.id) : void 0);
@@ -561,6 +562,7 @@ function SidebarNav({ items, labels: customLabels }) {
561
562
  const match = items.find((item) => item.href === pathname);
562
563
  if (match) setActiveId(domId(match.id));
563
564
  }, [pathname, items]);
565
+ if (items.length === 0) return null;
564
566
  return /* @__PURE__ */ jsx(CompositeProvider, {
565
567
  focusLoop: true,
566
568
  orientation: "vertical",
@@ -600,7 +602,7 @@ function SidebarNav({ items, labels: customLabels }) {
600
602
  }
601
603
  //#endregion
602
604
  //#region src/shells/editor-toolbar.tsx
603
- const defaultLabels$8 = { ariaLabel: "텍스트 편집기 툴바" };
605
+ const defaultLabels$13 = { ariaLabel: "텍스트 편집기 툴바" };
604
606
  function ToolbarButtonWithTooltip({ button, active, onAction }) {
605
607
  return /* @__PURE__ */ jsxs(TooltipProvider$1, { children: [/* @__PURE__ */ jsx(TooltipAnchor$1, { children: /* @__PURE__ */ jsx(ToolbarButton$1, {
606
608
  className: `text-text-primary data-[active-item]:bg-bg-subtle data-[focus-visible]:ring-ring rounded-md p-2 transition-colors outline-none data-[focus-visible]:ring-2 ${active ? "bg-bg-muted" : "hover:bg-bg-subtle"}`,
@@ -610,7 +612,7 @@ function ToolbarButtonWithTooltip({ button, active, onAction }) {
610
612
  }
611
613
  /** Renders the editor toolbar component. */
612
614
  function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLabels }) {
613
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
615
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$13), customLabels);
614
616
  const groups = [];
615
617
  let current = [];
616
618
  for (const btn of buttons) {
@@ -643,21 +645,21 @@ function EditorToolbar({ buttons, onAction, activeButtons = [], labels: customLa
643
645
  }
644
646
  //#endregion
645
647
  //#region src/shells/content-tabs.tsx
646
- const defaultLabels$7 = {
648
+ const defaultLabels$12 = {
647
649
  ariaLabel: "콘텐츠 탭",
648
650
  emptyMessage: "표시할 탭이 없습니다."
649
651
  };
650
652
  /** Renders the content tabs component. */
651
653
  function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
652
654
  var _tabs$, _tabs$find;
653
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
655
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$12), customLabels);
656
+ const [selectedTabId, setSelectedTabId] = useState(defaultTabId !== null && defaultTabId !== void 0 ? defaultTabId : (_tabs$ = tabs[0]) === null || _tabs$ === void 0 ? void 0 : _tabs$.id);
654
657
  if (tabs.length === 0) return /* @__PURE__ */ jsx("div", {
655
658
  className: "text-text-secondary text-sm",
656
659
  "data-opt-id": "9W97K",
657
660
  "data-opt-slug": "content-tabs.ContentTabs",
658
661
  children: labels.emptyMessage
659
662
  });
660
- const [selectedTabId, setSelectedTabId] = useState(defaultTabId !== null && defaultTabId !== void 0 ? defaultTabId : (_tabs$ = tabs[0]) === null || _tabs$ === void 0 ? void 0 : _tabs$.id);
661
663
  const handleSelectedTabId = (nextId) => {
662
664
  var _tabs$2;
663
665
  setSelectedTabId(nextId !== null && nextId !== void 0 ? nextId : (_tabs$2 = tabs[0]) === null || _tabs$2 === void 0 ? void 0 : _tabs$2.id);
@@ -682,13 +684,13 @@ function ContentTabs({ tabs, defaultTabId, labels: customLabels }) {
682
684
  }
683
685
  //#endregion
684
686
  //#region src/shells/faq-accordion.tsx
685
- const defaultLabels$6 = {
687
+ const defaultLabels$11 = {
686
688
  ariaLabel: "자주 묻는 질문",
687
689
  emptyMessage: "등록된 FAQ가 없습니다."
688
690
  };
689
691
  /** Renders the faq accordion component. */
690
692
  function FaqAccordion({ items, labels: customLabels }) {
691
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$6), customLabels);
693
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$11), customLabels);
692
694
  if (items.length === 0) return /* @__PURE__ */ jsx("div", {
693
695
  className: "text-text-secondary text-sm",
694
696
  "data-opt-id": "QRBHV",
@@ -728,21 +730,15 @@ function FaqAccordion({ items, labels: customLabels }) {
728
730
  }
729
731
  //#endregion
730
732
  //#region src/shells/settings-form.tsx
731
- const defaultLabels$5 = {
733
+ const defaultLabels$10 = {
732
734
  formAriaLabel: "설정 폼",
733
735
  submitButton: "저장",
734
736
  successMessage: "설정이 저장되었습니다."
735
737
  };
736
738
  /** Renders the settings form component. */
737
739
  function SettingsForm({ fields, onSubmit, labels: customLabels }) {
738
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
740
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$10), customLabels);
739
741
  const [submitted, setSubmitted] = useState(false);
740
- if (fields.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
741
- title: "설정 항목이 없습니다.",
742
- description: "표시할 설정 필드를 추가하면 이 폼이 렌더링됩니다.",
743
- "data-opt-id": "9TEEJ",
744
- "data-opt-slug": "settings-form.SettingsForm"
745
- });
746
742
  const defaultValues = {};
747
743
  for (const field of fields) {
748
744
  var _field$defaultValue;
@@ -773,6 +769,12 @@ function SettingsForm({ fields, onSubmit, labels: customLabels }) {
773
769
  onSubmit === null || onSubmit === void 0 || onSubmit(values);
774
770
  }
775
771
  });
772
+ if (fields.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
773
+ title: "설정 항목이 없습니다.",
774
+ description: "표시할 설정 필드를 추가하면 이 폼이 렌더링됩니다.",
775
+ "data-opt-id": "9TEEJ",
776
+ "data-opt-slug": "settings-form.SettingsForm"
777
+ });
776
778
  return /* @__PURE__ */ jsxs("div", {
777
779
  className: "gap-group flex flex-col",
778
780
  "data-opt-id": "9TEEJ",
@@ -1061,7 +1063,7 @@ function ProjectSwitcher({ projects, value, onChange, placeholder = "프로젝
1061
1063
  ProjectSwitcher.displayName = "ProjectSwitcher";
1062
1064
  //#endregion
1063
1065
  //#region src/shells/env-panel.tsx
1064
- const defaultLabels$4 = {
1066
+ const defaultLabels$9 = {
1065
1067
  varCount: (count) => `${count}개 변수`,
1066
1068
  envVarsLabel: (env) => `${env} 환경 변수`,
1067
1069
  secretBadge: "secret"
@@ -1073,7 +1075,7 @@ const envDot = {
1073
1075
  };
1074
1076
  /** Renders the env panel component. */
1075
1077
  function EnvPanel({ groups, labels: customLabels }) {
1076
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$4), customLabels);
1078
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$9), customLabels);
1077
1079
  return /* @__PURE__ */ jsx("div", {
1078
1080
  className: `rounded-lg ${OPT_BORDER} divide-y ${OPT_DIVIDER} bg-surface`,
1079
1081
  "data-opt-id": "ZBDWK",
@@ -1146,7 +1148,7 @@ function EnvPanel({ groups, labels: customLabels }) {
1146
1148
  }
1147
1149
  //#endregion
1148
1150
  //#region src/shells/branch-select.tsx
1149
- const defaultLabels$3 = {
1151
+ const defaultLabels$8 = {
1150
1152
  selectLabel: "브랜치",
1151
1153
  emptySelectionLabel: "선택...",
1152
1154
  emptyMessage: "사용 가능한 브랜치가 없습니다.",
@@ -1155,7 +1157,7 @@ const defaultLabels$3 = {
1155
1157
  /** Renders the branch select component. */
1156
1158
  function BranchSelect({ branches, value: controlledValue, onChange, label, labels: customLabels }) {
1157
1159
  var _ref, _defaultBranch$name, _branches$, _selected$name;
1158
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$3), customLabels);
1160
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$8), customLabels);
1159
1161
  const resolvedLabel = label !== null && label !== void 0 ? label : labels.selectLabel;
1160
1162
  const defaultBranch = branches.find((b) => b.isDefault);
1161
1163
  const [internalValue, setInternalValue] = useState((_ref = (_defaultBranch$name = defaultBranch === null || defaultBranch === void 0 ? void 0 : defaultBranch.name) !== null && _defaultBranch$name !== void 0 ? _defaultBranch$name : (_branches$ = branches[0]) === null || _branches$ === void 0 ? void 0 : _branches$.name) !== null && _ref !== void 0 ? _ref : "");
@@ -1264,7 +1266,7 @@ function BranchSelect({ branches, value: controlledValue, onChange, label, label
1264
1266
  BranchSelect.displayName = "BranchSelect";
1265
1267
  //#endregion
1266
1268
  //#region src/shells/domain-table.tsx
1267
- const defaultLabels$2 = {
1269
+ const defaultLabels$7 = {
1268
1270
  domain: "Domain",
1269
1271
  status: "Status",
1270
1272
  ssl: "SSL",
@@ -1294,7 +1296,7 @@ const sslLabel = {
1294
1296
  };
1295
1297
  /** Renders the domain table component. */
1296
1298
  function DomainTable({ domains, title = "도메인", labels: customLabels }) {
1297
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels$2), customLabels);
1299
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$7), customLabels);
1298
1300
  if (domains.length === 0) return /* @__PURE__ */ jsxs("div", {
1299
1301
  className: "gap-group flex flex-col",
1300
1302
  "data-opt-id": "A7SRM",
@@ -1900,7 +1902,7 @@ function NotificationToast({ toast, onDismiss, defaultDuration = 5e3 }) {
1900
1902
  }
1901
1903
  //#endregion
1902
1904
  //#region src/shells/failure-list.tsx
1903
- const defaultLabels$1 = {
1905
+ const defaultLabels$6 = {
1904
1906
  title: "최근 실패",
1905
1907
  emptyMessage: "실패 항목이 없습니다",
1906
1908
  moreItems: "{count}개 더 보기",
@@ -1999,20 +2001,20 @@ function FailureItemRow({ failure, isLast, onFailureClick, renderActions, merged
1999
2001
  function FailureList({ failures, title, maxItems, grouped = false, onFailureClick, onShowMore, labels: customLabels, locale = "ko-KR", loading = false, renderActions, className }) {
2000
2002
  var _customLabels$timeAgo, _customLabels$timeAgo2, _customLabels$timeAgo3, _customLabels$timeAgo4, _customLabels$timeAgo5, _customLabels$timeAgo6, _customLabels$timeAgo7, _customLabels$timeAgo8, _customLabels$severit, _customLabels$severit2, _customLabels$severit3, _customLabels$severit4, _customLabels$severit5, _customLabels$severit6, _customLabels$title, _customLabels$emptyMe, _customLabels$moreIte;
2001
2003
  const mergedTimeAgo = {
2002
- 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$1.timeAgo.justNow,
2003
- 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$1.timeAgo.minutesAgo,
2004
- 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$1.timeAgo.hoursAgo,
2005
- 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$1.timeAgo.daysAgo
2004
+ justNow: (_customLabels$timeAgo = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo2 = customLabels.timeAgo) === null || _customLabels$timeAgo2 === void 0 ? void 0 : _customLabels$timeAgo2.justNow) !== null && _customLabels$timeAgo !== void 0 ? _customLabels$timeAgo : defaultLabels$6.timeAgo.justNow,
2005
+ minutesAgo: (_customLabels$timeAgo3 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo4 = customLabels.timeAgo) === null || _customLabels$timeAgo4 === void 0 ? void 0 : _customLabels$timeAgo4.minutesAgo) !== null && _customLabels$timeAgo3 !== void 0 ? _customLabels$timeAgo3 : defaultLabels$6.timeAgo.minutesAgo,
2006
+ hoursAgo: (_customLabels$timeAgo5 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo6 = customLabels.timeAgo) === null || _customLabels$timeAgo6 === void 0 ? void 0 : _customLabels$timeAgo6.hoursAgo) !== null && _customLabels$timeAgo5 !== void 0 ? _customLabels$timeAgo5 : defaultLabels$6.timeAgo.hoursAgo,
2007
+ daysAgo: (_customLabels$timeAgo7 = customLabels === null || customLabels === void 0 || (_customLabels$timeAgo8 = customLabels.timeAgo) === null || _customLabels$timeAgo8 === void 0 ? void 0 : _customLabels$timeAgo8.daysAgo) !== null && _customLabels$timeAgo7 !== void 0 ? _customLabels$timeAgo7 : defaultLabels$6.timeAgo.daysAgo
2006
2008
  };
2007
2009
  const mergedSeverityLabels = {
2008
- 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$1.severityLabels.critical,
2009
- 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$1.severityLabels.error,
2010
- 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$1.severityLabels.warning
2010
+ critical: (_customLabels$severit = customLabels === null || customLabels === void 0 || (_customLabels$severit2 = customLabels.severityLabels) === null || _customLabels$severit2 === void 0 ? void 0 : _customLabels$severit2.critical) !== null && _customLabels$severit !== void 0 ? _customLabels$severit : defaultLabels$6.severityLabels.critical,
2011
+ error: (_customLabels$severit3 = customLabels === null || customLabels === void 0 || (_customLabels$severit4 = customLabels.severityLabels) === null || _customLabels$severit4 === void 0 ? void 0 : _customLabels$severit4.error) !== null && _customLabels$severit3 !== void 0 ? _customLabels$severit3 : defaultLabels$6.severityLabels.error,
2012
+ warning: (_customLabels$severit5 = customLabels === null || customLabels === void 0 || (_customLabels$severit6 = customLabels.severityLabels) === null || _customLabels$severit6 === void 0 ? void 0 : _customLabels$severit6.warning) !== null && _customLabels$severit5 !== void 0 ? _customLabels$severit5 : defaultLabels$6.severityLabels.warning
2011
2013
  };
2012
2014
  const labels = {
2013
- title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$1.title,
2014
- emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$1.emptyMessage,
2015
- moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$1.moreItems
2015
+ title: (_customLabels$title = customLabels === null || customLabels === void 0 ? void 0 : customLabels.title) !== null && _customLabels$title !== void 0 ? _customLabels$title : defaultLabels$6.title,
2016
+ emptyMessage: (_customLabels$emptyMe = customLabels === null || customLabels === void 0 ? void 0 : customLabels.emptyMessage) !== null && _customLabels$emptyMe !== void 0 ? _customLabels$emptyMe : defaultLabels$6.emptyMessage,
2017
+ moreItems: (_customLabels$moreIte = customLabels === null || customLabels === void 0 ? void 0 : customLabels.moreItems) !== null && _customLabels$moreIte !== void 0 ? _customLabels$moreIte : defaultLabels$6.moreItems
2016
2018
  };
2017
2019
  const displayTitle = title !== null && title !== void 0 ? title : labels.title;
2018
2020
  const allItems = maxItems ? failures.slice(0, maxItems) : failures;
@@ -2088,7 +2090,7 @@ function renderGrouped(items, remainingCount, rowProps, labels, onShowMore) {
2088
2090
  }
2089
2091
  //#endregion
2090
2092
  //#region src/shells/key-value-editor.tsx
2091
- const defaultLabels = {
2093
+ const defaultLabels$5 = {
2092
2094
  keyPlaceholder: "키",
2093
2095
  valuePlaceholder: "값",
2094
2096
  addButton: "추가",
@@ -2102,7 +2104,7 @@ function generateId() {
2102
2104
  }
2103
2105
  /** Renders the key value editor component. */
2104
2106
  function KeyValueEditor({ value: controlledValue, defaultValue, onChange, showSecretToggle = false, maxEntries, readOnly = false, labels: customLabels, renderActions, validateKey, className }) {
2105
- const labels = _objectSpread2(_objectSpread2({}, defaultLabels), customLabels);
2107
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$5), customLabels);
2106
2108
  const [internalEntries, setInternalEntries] = useState(() => defaultValue !== null && defaultValue !== void 0 ? defaultValue : []);
2107
2109
  const entries = controlledValue !== null && controlledValue !== void 0 ? controlledValue : internalEntries;
2108
2110
  const keyInputRef = useRef(null);
@@ -2398,6 +2400,1834 @@ function WorkflowCanvas({ steps, connections = [], rows = 9, cols = 9, cellSize
2398
2400
  }
2399
2401
  WorkflowCanvas.displayName = "WorkflowCanvas";
2400
2402
  //#endregion
2401
- export { CommandItem$1 as A, CommandPaletteTrigger as C, CommandEmpty$1 as D, CommandDialog as E, CommandSeparator$1 as M, CommandShortcut as N, CommandGroup$1 as O, DashboardGrid as S, Command as T, EditorToolbar as _, ToastProvider as a, StatusSelect as b, DataTable as c, EnvPanel as d, ProjectSwitcher as f, ContentTabs as g, FaqAccordion as h, NotificationToast as i, CommandList$1 as j, CommandInput$1 as k, DomainTable as l, SettingsForm as m, KeyValueEditor as n, toast as o, DeploymentTimeline as p, FailureList as r, useToast as s, WorkflowCanvas as t, BranchSelect as u, SidebarNav as v, CommandPalette as w, SearchCombobox as x, AppMenubar as y };
2403
+ //#region src/lib/use-auto-refresh.ts
2404
+ /**
2405
+ * A refresh timer that waits for its own work.
2406
+ *
2407
+ * A plain `setInterval` fires on a schedule regardless of whether the previous
2408
+ * query came back, so a five-second interval over a seven-second query stacks
2409
+ * requests until something gives. Chaining the next wait onto the settled
2410
+ * promise keeps exactly one refresh in flight, and a rejection stops the loop
2411
+ * instead of hammering an endpoint that is already failing.
2412
+ */
2413
+ function useAutoRefresh({ interval, paused = false, onRefresh }) {
2414
+ const callbackRef = useRef(onRefresh);
2415
+ callbackRef.current = onRefresh;
2416
+ useEffect(() => {
2417
+ if (paused || interval <= 0 || !callbackRef.current) return;
2418
+ let cancelled = false;
2419
+ let timer;
2420
+ const schedule = () => {
2421
+ timer = setTimeout(async () => {
2422
+ if (cancelled) return;
2423
+ try {
2424
+ var _callbackRef$current;
2425
+ await ((_callbackRef$current = callbackRef.current) === null || _callbackRef$current === void 0 ? void 0 : _callbackRef$current.call(callbackRef));
2426
+ } catch (_unused) {
2427
+ return;
2428
+ }
2429
+ if (!cancelled) schedule();
2430
+ }, interval);
2431
+ };
2432
+ schedule();
2433
+ return () => {
2434
+ cancelled = true;
2435
+ if (timer) clearTimeout(timer);
2436
+ };
2437
+ }, [interval, paused]);
2438
+ }
2439
+ //#endregion
2440
+ //#region src/shells/time-range-control.tsx
2441
+ const defaultLabels$4 = {
2442
+ trigger: "Select time range",
2443
+ invalid: "Invalid time range",
2444
+ last: (duration) => `Last ${duration}`,
2445
+ next: (duration) => `Next ${duration}`,
2446
+ quickSelect: "Quick select",
2447
+ apply: "Apply",
2448
+ update: "Update",
2449
+ refresh: "Refresh",
2450
+ commonlyUsed: "Commonly used",
2451
+ recentlyUsed: "Recently used",
2452
+ start: "Start",
2453
+ end: "End",
2454
+ startHint: "Absolute time or date math, e.g. now-7d",
2455
+ autoRefresh: "Auto refresh",
2456
+ autoRefreshOff: "Off",
2457
+ every: (duration) => `Every ${duration}`,
2458
+ previous: "Previous window",
2459
+ nextWindow: "Next window",
2460
+ zoomIn: "Zoom in",
2461
+ zoomOut: "Zoom out",
2462
+ thisUnit: (unit) => `This ${unit}`,
2463
+ units: {
2464
+ s: "seconds",
2465
+ m: "minutes",
2466
+ h: "hours",
2467
+ d: "days",
2468
+ w: "weeks",
2469
+ M: "months",
2470
+ y: "years"
2471
+ }
2472
+ };
2473
+ const QUICK_UNITS = [
2474
+ "m",
2475
+ "h",
2476
+ "d",
2477
+ "w",
2478
+ "M",
2479
+ "y"
2480
+ ];
2481
+ const REFRESH_INTERVALS = [
2482
+ 0,
2483
+ 5e3,
2484
+ 1e4,
2485
+ 3e4,
2486
+ 6e4,
2487
+ 3e5
2488
+ ];
2489
+ function selectValue(value) {
2490
+ var _value$;
2491
+ return Array.isArray(value) ? (_value$ = value[0]) !== null && _value$ !== void 0 ? _value$ : "" : value;
2492
+ }
2493
+ /**
2494
+ * What the trigger says.
2495
+ *
2496
+ * A window anchored to `now` deserves its intent ("Last 7 days"), not two
2497
+ * timestamps the reader has to subtract. Everything else gets both ends
2498
+ * spelled out. A duration that does not divide into whole minutes is prefixed
2499
+ * with `~` rather than rounded silently.
2500
+ */
2501
+ function describeRange(range, labels, options) {
2502
+ const window = measureTimeWindow(range, {
2503
+ timeZone: options.timeZone,
2504
+ now: options.now
2505
+ });
2506
+ if (window.isInvalid) return {
2507
+ text: labels.invalid,
2508
+ isInvalid: true
2509
+ };
2510
+ const fromParts = parseRelativeParts(range.from);
2511
+ const toParts = parseRelativeParts(range.to);
2512
+ if ((fromParts === null || fromParts === void 0 ? void 0 : fromParts.roundUnit) && fromParts.count === 0 && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === fromParts.roundUnit && toParts.count === 0) return {
2513
+ text: labels.thisUnit(formatCountUnit(1, fromParts.roundUnit, options.locale).replace(/^1\s*/, "")),
2514
+ isInvalid: false
2515
+ };
2516
+ if (isRelativeToNow(range)) {
2517
+ var _toParts$count;
2518
+ const anchoredAtEnd = ((_toParts$count = toParts === null || toParts === void 0 ? void 0 : toParts.count) !== null && _toParts$count !== void 0 ? _toParts$count : 0) === 0;
2519
+ const parts = anchoredAtEnd ? fromParts : toParts;
2520
+ const bothSnapped = (parts === null || parts === void 0 ? void 0 : parts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit) && (toParts === null || toParts === void 0 ? void 0 : toParts.roundUnit) === (parts === null || parts === void 0 ? void 0 : parts.unit);
2521
+ const counted = parts && parts.count > 0 ? formatCountUnit(parts.roundUnit && bothSnapped ? parts.count + 1 : parts.count, parts.unit, options.locale) : null;
2522
+ const exact = counted !== null && (!(parts === null || parts === void 0 ? void 0 : parts.roundUnit) || bothSnapped);
2523
+ const duration = exact ? counted : formatDuration(window.duration, options.locale);
2524
+ const prefix = exact || isExactMinutes(window.duration) ? "" : "~";
2525
+ return {
2526
+ text: anchoredAtEnd ? labels.last(`${prefix}${duration}`) : labels.next(`${prefix}${duration}`),
2527
+ isInvalid: false
2528
+ };
2529
+ }
2530
+ const dateOnly = isDateOnly(range.from) && isDateOnly(range.to);
2531
+ const from = formatInstant(window.from, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
2532
+ const to = formatInstant(window.to, _objectSpread2(_objectSpread2({}, options), {}, { dateOnly }));
2533
+ return {
2534
+ text: from === to ? from : `${from} – ${to}`,
2535
+ isInvalid: false
2536
+ };
2537
+ }
2538
+ /**
2539
+ * TimeRangeControl — the window as an expression, not two frozen instants.
2540
+ *
2541
+ * Holding `{ from: "now-7d", to: "now" }` instead of a resolved pair is what
2542
+ * makes a saved board, a shared link and an auto-refresh timer all mean the
2543
+ * same thing tomorrow that they mean today. Around that sit the moves people
2544
+ * actually make on a chart: jump to a named window, step the same width back
2545
+ * and forth to compare, widen when the interesting thing is just off screen,
2546
+ * and let it re-query on a timer while they watch.
2547
+ *
2548
+ * @example
2549
+ * ```tsx
2550
+ * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
2551
+ *
2552
+ * <TimeRangeControl
2553
+ * value={range}
2554
+ * onChange={setRange}
2555
+ * timeZone="Asia/Seoul"
2556
+ * showWindowButtons
2557
+ * onRefresh={() => refetch()}
2558
+ * />
2559
+ * ```
2560
+ */
2561
+ function TimeRangeControl({ value, onChange, presets, recentRanges, timeZone, locale, showUpdateButton = true, showWindowButtons = true, zoomFactor = .5, refresh, onRefreshChange, onRefresh, isLoading = false, disabled = false, className, labels: customLabels, "aria-label": ariaLabel }) {
2562
+ var _refresh$interval, _refresh$paused, _refresh$interval2;
2563
+ const labels = _objectSpread2(_objectSpread2(_objectSpread2({}, defaultLabels$4), customLabels), {}, { units: _objectSpread2(_objectSpread2({}, defaultLabels$4.units), customLabels === null || customLabels === void 0 ? void 0 : customLabels.units) });
2564
+ const [open, setOpen] = useState(false);
2565
+ const [draft, setDraft] = useState(null);
2566
+ const [quickCount, setQuickCount] = useState("15");
2567
+ const [quickUnit, setQuickUnit] = useState("m");
2568
+ const editing = draft !== null && draft !== void 0 ? draft : value;
2569
+ const hasChanges = draft !== null && (draft.from !== value.from || draft.to !== value.to);
2570
+ const description = useMemo(() => describeRange(value, labels, {
2571
+ locale,
2572
+ timeZone
2573
+ }), [
2574
+ value,
2575
+ locale,
2576
+ timeZone,
2577
+ customLabels
2578
+ ]);
2579
+ const boundState = useCallback((expression) => parseTimeExpression(expression, { timeZone }) === null ? labels.invalid : void 0, [timeZone, labels]);
2580
+ const commit = useCallback((range) => {
2581
+ setDraft(null);
2582
+ onChange(range);
2583
+ }, [onChange]);
2584
+ const apply = useCallback(() => {
2585
+ if (draft) commit(draft);
2586
+ else onRefresh === null || onRefresh === void 0 || onRefresh();
2587
+ }, [
2588
+ draft,
2589
+ commit,
2590
+ onRefresh
2591
+ ]);
2592
+ const commitOnEnter = useCallback((event) => {
2593
+ if (event.key !== "Enter") return;
2594
+ apply();
2595
+ setOpen(false);
2596
+ }, [apply]);
2597
+ const applyQuick = useCallback(() => {
2598
+ const count = Number(quickCount);
2599
+ if (!Number.isFinite(count) || count <= 0) return;
2600
+ commit({
2601
+ from: `now-${Math.round(count)}${quickUnit}`,
2602
+ to: "now"
2603
+ });
2604
+ setOpen(false);
2605
+ }, [
2606
+ quickCount,
2607
+ quickUnit,
2608
+ commit
2609
+ ]);
2610
+ const step = useCallback((direction) => {
2611
+ const next = shiftTimeRange(value, direction, { timeZone });
2612
+ if (next) commit(next);
2613
+ }, [
2614
+ value,
2615
+ timeZone,
2616
+ commit
2617
+ ]);
2618
+ const zoom = useCallback((factor) => {
2619
+ const next = zoomTimeRange(value, factor, { timeZone });
2620
+ if (next) commit(next);
2621
+ }, [
2622
+ value,
2623
+ timeZone,
2624
+ commit
2625
+ ]);
2626
+ useAutoRefresh({
2627
+ interval: (_refresh$interval = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval !== void 0 ? _refresh$interval : 0,
2628
+ paused: (_refresh$paused = refresh === null || refresh === void 0 ? void 0 : refresh.paused) !== null && _refresh$paused !== void 0 ? _refresh$paused : true,
2629
+ onRefresh
2630
+ });
2631
+ const refreshLabel = !refresh || refresh.paused || refresh.interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(refresh.interval, locale));
2632
+ return /* @__PURE__ */ jsxs("div", {
2633
+ className: cn("bg-surface inline-flex items-center rounded-lg border", OPT_BORDER, className),
2634
+ role: "group",
2635
+ "aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : labels.trigger,
2636
+ "data-opt-id": "3WJ8H",
2637
+ "data-opt-slug": "time-range-control.TimeRangeControl",
2638
+ children: [
2639
+ /* @__PURE__ */ jsxs(PopoverRoot$1, {
2640
+ open,
2641
+ onOpenChange: setOpen,
2642
+ children: [/* @__PURE__ */ jsxs(PopoverTrigger$1, {
2643
+ className: cn("text-text-primary h-10 gap-2", description.isInvalid && "text-danger-fg"),
2644
+ disabled,
2645
+ children: [/* @__PURE__ */ jsx(Clock, {
2646
+ className: "text-text-tertiary h-4 w-4",
2647
+ "aria-hidden": "true"
2648
+ }), /* @__PURE__ */ jsx("span", {
2649
+ className: "truncate text-sm",
2650
+ children: description.text
2651
+ })]
2652
+ }), /* @__PURE__ */ jsxs(PopoverContent$1, {
2653
+ className: "gap-group flex w-80 flex-col",
2654
+ children: [
2655
+ /* @__PURE__ */ jsxs("div", {
2656
+ className: "gap-element flex flex-col",
2657
+ children: [/* @__PURE__ */ jsx("p", {
2658
+ className: "text-text-primary text-xs font-medium",
2659
+ children: labels.quickSelect
2660
+ }), /* @__PURE__ */ jsxs("div", {
2661
+ className: "flex items-end gap-2",
2662
+ children: [
2663
+ /* @__PURE__ */ jsx(Input, {
2664
+ "aria-label": labels.quickSelect,
2665
+ type: "number",
2666
+ min: 1,
2667
+ size: "sm",
2668
+ className: "w-20",
2669
+ value: quickCount,
2670
+ onChange: (event) => setQuickCount(event.target.value),
2671
+ onKeyDown: (event) => {
2672
+ if (event.key === "Enter") applyQuick();
2673
+ }
2674
+ }),
2675
+ /* @__PURE__ */ jsxs(SelectRoot$1, {
2676
+ value: quickUnit,
2677
+ setValue: (next) => setQuickUnit(selectValue(next)),
2678
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
2679
+ minWidth: 110,
2680
+ "aria-label": labels.quickSelect
2681
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: QUICK_UNITS.map((unit) => /* @__PURE__ */ jsx(SelectItem$1, {
2682
+ value: unit,
2683
+ children: labels.units[unit]
2684
+ }, unit)) })]
2685
+ }),
2686
+ /* @__PURE__ */ jsx(Button, {
2687
+ variant: "secondary",
2688
+ size: "sm",
2689
+ onClick: applyQuick,
2690
+ children: labels.apply
2691
+ })
2692
+ ]
2693
+ })]
2694
+ }),
2695
+ /* @__PURE__ */ jsxs("div", {
2696
+ className: "gap-element flex flex-col",
2697
+ children: [/* @__PURE__ */ jsx(Input, {
2698
+ label: labels.start,
2699
+ size: "sm",
2700
+ value: editing.from,
2701
+ error: boundState(editing.from),
2702
+ hint: labels.startHint,
2703
+ onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { from: event.target.value })),
2704
+ onKeyDown: commitOnEnter
2705
+ }), /* @__PURE__ */ jsx(Input, {
2706
+ label: labels.end,
2707
+ size: "sm",
2708
+ value: editing.to,
2709
+ error: boundState(editing.to),
2710
+ onChange: (event) => setDraft(_objectSpread2(_objectSpread2({}, editing), {}, { to: event.target.value })),
2711
+ onKeyDown: commitOnEnter
2712
+ })]
2713
+ }),
2714
+ presets && presets.length > 0 && /* @__PURE__ */ jsx(RangeList, {
2715
+ title: labels.commonlyUsed,
2716
+ items: presets,
2717
+ onPick: (range) => {
2718
+ commit(range);
2719
+ setOpen(false);
2720
+ }
2721
+ }),
2722
+ recentRanges && recentRanges.length > 0 && /* @__PURE__ */ jsx(RangeList, {
2723
+ title: labels.recentlyUsed,
2724
+ items: recentRanges.slice(0, 5),
2725
+ onPick: (range) => {
2726
+ commit(range);
2727
+ setOpen(false);
2728
+ }
2729
+ }),
2730
+ onRefreshChange && /* @__PURE__ */ jsxs("div", {
2731
+ className: "gap-element flex flex-col",
2732
+ children: [/* @__PURE__ */ jsx("p", {
2733
+ className: "text-text-primary text-xs font-medium",
2734
+ children: labels.autoRefresh
2735
+ }), /* @__PURE__ */ jsxs(SelectRoot$1, {
2736
+ value: String((refresh === null || refresh === void 0 ? void 0 : refresh.paused) ? 0 : (_refresh$interval2 = refresh === null || refresh === void 0 ? void 0 : refresh.interval) !== null && _refresh$interval2 !== void 0 ? _refresh$interval2 : 0),
2737
+ setValue: (next) => {
2738
+ const interval = Number(selectValue(next));
2739
+ onRefreshChange({
2740
+ interval,
2741
+ paused: interval === 0
2742
+ });
2743
+ },
2744
+ children: [/* @__PURE__ */ jsx(SelectTrigger$1, {
2745
+ minWidth: 140,
2746
+ "aria-label": labels.autoRefresh
2747
+ }), /* @__PURE__ */ jsx(SelectPopover$1, { children: REFRESH_INTERVALS.map((interval) => /* @__PURE__ */ jsx(SelectItem$1, {
2748
+ value: String(interval),
2749
+ children: interval === 0 ? labels.autoRefreshOff : labels.every(formatDuration(interval, locale))
2750
+ }, interval)) })]
2751
+ })]
2752
+ })
2753
+ ]
2754
+ })]
2755
+ }),
2756
+ showWindowButtons && /* @__PURE__ */ jsxs("div", {
2757
+ className: cn("flex items-center border-l", "border border-border"),
2758
+ role: "group",
2759
+ "aria-label": labels.previous,
2760
+ children: [
2761
+ /* @__PURE__ */ jsx(IconButton, {
2762
+ label: labels.previous,
2763
+ disabled: disabled || description.isInvalid,
2764
+ onClick: () => step(-1),
2765
+ children: /* @__PURE__ */ jsx(ChevronLeft, {
2766
+ className: "h-4 w-4",
2767
+ "aria-hidden": "true"
2768
+ })
2769
+ }),
2770
+ /* @__PURE__ */ jsx(IconButton, {
2771
+ label: labels.zoomIn,
2772
+ disabled: disabled || description.isInvalid,
2773
+ onClick: () => zoom(-zoomFactor),
2774
+ children: /* @__PURE__ */ jsx(ZoomIn, {
2775
+ className: "h-4 w-4",
2776
+ "aria-hidden": "true"
2777
+ })
2778
+ }),
2779
+ /* @__PURE__ */ jsx(IconButton, {
2780
+ label: labels.zoomOut,
2781
+ disabled: disabled || description.isInvalid,
2782
+ onClick: () => zoom(zoomFactor),
2783
+ children: /* @__PURE__ */ jsx(ZoomOut, {
2784
+ className: "h-4 w-4",
2785
+ "aria-hidden": "true"
2786
+ })
2787
+ }),
2788
+ /* @__PURE__ */ jsx(IconButton, {
2789
+ label: labels.nextWindow,
2790
+ disabled: disabled || description.isInvalid,
2791
+ onClick: () => step(1),
2792
+ children: /* @__PURE__ */ jsx(ChevronRight, {
2793
+ className: "h-4 w-4",
2794
+ "aria-hidden": "true"
2795
+ })
2796
+ })
2797
+ ]
2798
+ }),
2799
+ (onRefresh || showUpdateButton) && /* @__PURE__ */ jsx("div", {
2800
+ className: cn("border-l pl-2 pr-2", "border border-border"),
2801
+ children: /* @__PURE__ */ jsx(Button, {
2802
+ variant: hasChanges ? "primary" : "secondary",
2803
+ size: "sm",
2804
+ loading: isLoading,
2805
+ disabled: disabled || description.isInvalid,
2806
+ onClick: apply,
2807
+ leftIcon: hasChanges ? void 0 : /* @__PURE__ */ jsx(RefreshCw, {
2808
+ className: "h-3.5 w-3.5",
2809
+ "aria-hidden": "true"
2810
+ }),
2811
+ title: refreshLabel,
2812
+ children: hasChanges ? labels.update : labels.refresh
2813
+ })
2814
+ })
2815
+ ]
2816
+ });
2817
+ }
2818
+ TimeRangeControl.displayName = "TimeRangeControl";
2819
+ function IconButton({ label, disabled, onClick, children }) {
2820
+ return /* @__PURE__ */ jsx(Button, {
2821
+ variant: "ghost",
2822
+ size: "sm",
2823
+ "aria-label": label,
2824
+ title: label,
2825
+ disabled,
2826
+ onClick,
2827
+ children
2828
+ });
2829
+ }
2830
+ function RangeList({ title, items, onPick }) {
2831
+ return /* @__PURE__ */ jsxs("div", {
2832
+ className: "gap-element flex flex-col",
2833
+ children: [/* @__PURE__ */ jsx("p", {
2834
+ className: "text-text-primary text-xs font-medium",
2835
+ children: title
2836
+ }), /* @__PURE__ */ jsx("ul", {
2837
+ className: "grid grid-cols-2 gap-1",
2838
+ children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
2839
+ type: "button",
2840
+ className: cn("hover:bg-bg-subtle w-full cursor-pointer rounded px-2 py-1 text-left text-xs", OPT_TEXT_SECONDARY),
2841
+ onClick: () => onPick(item.range),
2842
+ children: item.label
2843
+ }) }, `${item.label}-${index}`))
2844
+ })]
2845
+ });
2846
+ }
2847
+ /**
2848
+ * The windows most analytics tools open with. Exported so a caller gets a
2849
+ * sensible list without inventing date math of their own.
2850
+ */
2851
+ const DEFAULT_TIME_PRESETS = [
2852
+ {
2853
+ label: "Last 15 minutes",
2854
+ range: {
2855
+ from: "now-15m",
2856
+ to: "now"
2857
+ }
2858
+ },
2859
+ {
2860
+ label: "Last hour",
2861
+ range: {
2862
+ from: "now-1h",
2863
+ to: "now"
2864
+ }
2865
+ },
2866
+ {
2867
+ label: "Last 24 hours",
2868
+ range: {
2869
+ from: "now-24h",
2870
+ to: "now"
2871
+ }
2872
+ },
2873
+ {
2874
+ label: "Last 7 days",
2875
+ range: {
2876
+ from: "now-7d",
2877
+ to: "now"
2878
+ }
2879
+ },
2880
+ {
2881
+ label: "Last 30 days",
2882
+ range: {
2883
+ from: "now-30d",
2884
+ to: "now"
2885
+ }
2886
+ },
2887
+ {
2888
+ label: "Last 90 days",
2889
+ range: {
2890
+ from: "now-90d",
2891
+ to: "now"
2892
+ }
2893
+ },
2894
+ {
2895
+ label: "Today",
2896
+ range: {
2897
+ from: "now/d",
2898
+ to: "now/d"
2899
+ }
2900
+ },
2901
+ {
2902
+ label: "This week",
2903
+ range: {
2904
+ from: "now/w",
2905
+ to: "now/w"
2906
+ }
2907
+ }
2908
+ ];
2909
+ //#endregion
2910
+ //#region src/shells/time-series-panel.tsx
2911
+ const defaultLabels$3 = {
2912
+ back: "Back to previous range",
2913
+ legend: "Series"
2914
+ };
2915
+ /**
2916
+ * TimeSeriesPanel — a chart you can steer.
2917
+ *
2918
+ * The point of putting the control and the chart in one component is the
2919
+ * loop between them: drag across the plot and the window narrows to what you
2920
+ * dragged, which re-queries, which redraws the axis you just picked. That
2921
+ * only works if someone owns the step back out again — so zoom keeps a
2922
+ * history, and the header offers the way back rather than leaving the reader
2923
+ * to retype the range they came from.
2924
+ *
2925
+ * @example
2926
+ * ```tsx
2927
+ * const [range, setRange] = useState<TimeRange>({ from: "now-7d", to: "now" });
2928
+ *
2929
+ * <TimeSeriesPanel
2930
+ * title="Sessions"
2931
+ * data={points}
2932
+ * series={[{ dataKey: "sessions", name: "Sessions" }]}
2933
+ * range={range}
2934
+ * onRangeChange={setRange}
2935
+ * onRefresh={() => refetch()}
2936
+ * toggleableLegend
2937
+ * />
2938
+ * ```
2939
+ */
2940
+ function TimeSeriesPanel({ title, description, actions, data, series, range, onRangeChange, granularity = "auto", timeZone, locale, showRangeControl = true, presets, recentRanges, refresh, onRefreshChange, onRefresh, toggleableLegend = false, annotations, isLoading = false, height = 280, className, chartProps, labels: customLabels, "aria-label": ariaLabel }) {
2941
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$3), customLabels);
2942
+ const [history, setHistory] = useState([]);
2943
+ const [hidden, setHidden] = useState([]);
2944
+ const resolved = useMemo(() => resolveTimeRange(range, { timeZone }), [range, timeZone]);
2945
+ const visibleSeries = useMemo(() => series.filter((def) => !hidden.includes(def.dataKey)), [series, hidden]);
2946
+ const handleRangeSelect = useCallback((selection) => {
2947
+ setHistory((current) => [...current, range].slice(-10));
2948
+ onRangeChange(toAbsoluteRange(selection));
2949
+ }, [range, onRangeChange]);
2950
+ const goBack = useCallback(() => {
2951
+ setHistory((current) => {
2952
+ const previous = current[current.length - 1];
2953
+ if (previous) onRangeChange(previous);
2954
+ return current.slice(0, -1);
2955
+ });
2956
+ }, [onRangeChange]);
2957
+ const setRange = useCallback((next) => {
2958
+ setHistory([]);
2959
+ onRangeChange(next);
2960
+ }, [onRangeChange]);
2961
+ const toggleSeries = useCallback((dataKey) => {
2962
+ setHidden((current) => current.includes(dataKey) ? current.filter((key) => key !== dataKey) : [...current, dataKey]);
2963
+ }, []);
2964
+ return /* @__PURE__ */ jsxs("section", {
2965
+ className: cn(OPT_SURFACE, OPT_BORDER, "flex flex-col", className),
2966
+ "aria-label": ariaLabel,
2967
+ "data-opt-id": "Y0XCY",
2968
+ "data-opt-slug": "time-series-panel.TimeSeriesPanel",
2969
+ children: [/* @__PURE__ */ jsxs("header", {
2970
+ className: "gap-group flex flex-wrap items-start justify-between px-5 pt-5 pb-3",
2971
+ children: [/* @__PURE__ */ jsxs("div", {
2972
+ className: "gap-element flex min-w-0 flex-col",
2973
+ children: [title && /* @__PURE__ */ jsx("h3", {
2974
+ className: "text-text-primary truncate text-base font-semibold",
2975
+ children: title
2976
+ }), description && /* @__PURE__ */ jsx("p", {
2977
+ className: cn("text-text-secondary", "truncate text-sm"),
2978
+ children: description
2979
+ })]
2980
+ }), /* @__PURE__ */ jsxs("div", {
2981
+ className: "gap-element flex flex-wrap items-center",
2982
+ children: [
2983
+ history.length > 0 && /* @__PURE__ */ jsx(Button, {
2984
+ variant: "ghost",
2985
+ size: "sm",
2986
+ onClick: goBack,
2987
+ leftIcon: /* @__PURE__ */ jsx(Undo2, {
2988
+ className: "h-3.5 w-3.5",
2989
+ "aria-hidden": "true"
2990
+ }),
2991
+ children: labels.back
2992
+ }),
2993
+ actions,
2994
+ showRangeControl && /* @__PURE__ */ jsx(TimeRangeControl, {
2995
+ value: range,
2996
+ onChange: setRange,
2997
+ presets,
2998
+ recentRanges,
2999
+ timeZone,
3000
+ locale,
3001
+ refresh,
3002
+ onRefreshChange,
3003
+ onRefresh,
3004
+ isLoading,
3005
+ labels: customLabels === null || customLabels === void 0 ? void 0 : customLabels.control
3006
+ })
3007
+ ]
3008
+ })]
3009
+ }), /* @__PURE__ */ jsx("div", {
3010
+ className: "px-2 pb-4",
3011
+ children: /* @__PURE__ */ jsx(TimeSeriesChart, _objectSpread2({
3012
+ data,
3013
+ series: visibleSeries,
3014
+ from: resolved === null || resolved === void 0 ? void 0 : resolved.from,
3015
+ to: resolved === null || resolved === void 0 ? void 0 : resolved.to,
3016
+ granularity,
3017
+ timeZone,
3018
+ locale,
3019
+ height,
3020
+ selectable: true,
3021
+ onRangeSelect: handleRangeSelect,
3022
+ annotations,
3023
+ showLegend: series.length > 1,
3024
+ legendOptions: toggleableLegend ? {
3025
+ onClick: toggleSeries,
3026
+ inactiveKeys: hidden
3027
+ } : void 0,
3028
+ "aria-label": typeof title === "string" ? `${title} time series` : ariaLabel
3029
+ }, chartProps))
3030
+ })]
3031
+ });
3032
+ }
3033
+ TimeSeriesPanel.displayName = "TimeSeriesPanel";
3034
+ //#endregion
3035
+ //#region src/shells/log-table.tsx
3036
+ const HIDE_BELOW = {
3037
+ always: "",
3038
+ sm: "hidden @[40rem]/log:flex",
3039
+ md: "hidden @[46rem]/log:flex",
3040
+ lg: "hidden @[58rem]/log:flex",
3041
+ xl: "hidden @[70rem]/log:flex"
3042
+ };
3043
+ const ROW_HEIGHT = {
3044
+ compact: 26,
3045
+ normal: 32,
3046
+ comfortable: 40
3047
+ };
3048
+ const DENSITY_TEXT = {
3049
+ compact: "text-[11px]",
3050
+ normal: "text-xs",
3051
+ comfortable: "text-sm"
3052
+ };
3053
+ const defaultLabels$2 = {
3054
+ listLabel: "Log",
3055
+ loadingMore: "Loading earlier entries…",
3056
+ endOfResults: "That is every entry in this range.",
3057
+ filterFor: (value) => `Filter for ${value}`,
3058
+ filterOut: (value) => `Filter out ${value}`,
3059
+ sortAscending: "Sort ascending",
3060
+ sortDescending: "Sort descending"
3061
+ };
3062
+ /** Rows rendered beyond the viewport on each side, so a fast scroll stays filled. */
3063
+ const OVERSCAN = 10;
3064
+ /**
3065
+ * A stable hue per value. Colour does real work in a log — the eye groups by it
3066
+ * long before it reads the text — so the same event name has to land on the
3067
+ * same hue on every render and in every session, which rules out an index into
3068
+ * a palette.
3069
+ */
3070
+ function hueOf(value) {
3071
+ let hash = 0;
3072
+ for (let index = 0; index < value.length; index += 1) hash = hash * 31 + value.charCodeAt(index) | 0;
3073
+ return Math.abs(hash) % 360;
3074
+ }
3075
+ const EM_DASH = "—";
3076
+ /**
3077
+ * A column with nothing in it is a fact about the entry, not a value. Dimming
3078
+ * every absence the same way lets the eye skip the gaps while scanning, instead
3079
+ * of reading a dash as though it were data.
3080
+ */
3081
+ const ABSENT = "text-text-tertiary/50";
3082
+ function columnStyle(column) {
3083
+ return column.width ? {
3084
+ width: column.width,
3085
+ flex: "0 0 auto"
3086
+ } : {
3087
+ flex: "1 1 0%",
3088
+ minWidth: 0
3089
+ };
3090
+ }
3091
+ /**
3092
+ * LogTable — a windowed table for entries that keep arriving.
3093
+ *
3094
+ * Written by hand rather than delegated to a data grid for the two things a log
3095
+ * needs that a grid does not: rows are inserted at the *top* while the reader
3096
+ * is partway down, which needs a scroll correction applied before paint, and
3097
+ * "am I at the newest row?" has to be answerable from the scroll container to
3098
+ * decide whether to follow the tail or hold position.
3099
+ *
3100
+ * What it takes from a data grid is the part readers actually reach for:
3101
+ * columns are data, so a caller can let people add, drop and reorder them; a
3102
+ * cell offers to filter for or against its own value; and density is a setting
3103
+ * rather than a constant.
3104
+ *
3105
+ * @example
3106
+ * ```tsx
3107
+ * <LogTable
3108
+ * rows={rows}
3109
+ * getRowId={(row) => row.id}
3110
+ * columns={[
3111
+ * { id: "time", header: "Time", width: "9rem", mono: true, value: (r) => r.time },
3112
+ * { id: "name", header: "Event", width: "14rem", field: "name", marker: true, value: (r) => r.name },
3113
+ * { id: "path", header: "Path", field: "path", value: (r) => r.path },
3114
+ * ]}
3115
+ * onCellAction={(action) => setQuery(applyCellAction(query, action))}
3116
+ * />
3117
+ * ```
3118
+ */
3119
+ function LogTableInner({ rows, columns, getRowId, selectedId = null, onSelect, onLoadMore, hasMore = false, loadingMore = false, prepending = false, onFollowChange, isNew, sort = null, onSortChange, onCellAction, density = "normal", showHeader = true, emptyState, labels: customLabels, className }, ref) {
3120
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$2), customLabels);
3121
+ const rowHeight = ROW_HEIGHT[density];
3122
+ const scrollRef = useRef(null);
3123
+ const [scrollTop, setScrollTop] = useState(0);
3124
+ const [viewportHeight, setViewportHeight] = useState(0);
3125
+ const followingRef = useRef(true);
3126
+ const previousTopIdRef = useRef(null);
3127
+ const previousLengthRef = useRef(0);
3128
+ useImperativeHandle(ref, () => ({
3129
+ scrollToTop: () => {
3130
+ const container = scrollRef.current;
3131
+ if (!container) return;
3132
+ container.scrollTop = 0;
3133
+ setScrollTop(0);
3134
+ if (!followingRef.current) {
3135
+ followingRef.current = true;
3136
+ onFollowChange === null || onFollowChange === void 0 || onFollowChange(true);
3137
+ }
3138
+ },
3139
+ revealRow: (id) => {
3140
+ const index = rows.findIndex((row) => getRowId(row) === id);
3141
+ const container = scrollRef.current;
3142
+ if (index === -1 || !container) return;
3143
+ const top = index * rowHeight;
3144
+ const bottom = top + rowHeight;
3145
+ if (top < container.scrollTop) container.scrollTop = top;
3146
+ else if (bottom > container.scrollTop + container.clientHeight) container.scrollTop = bottom - container.clientHeight;
3147
+ }
3148
+ }), [
3149
+ onFollowChange,
3150
+ rows,
3151
+ getRowId,
3152
+ rowHeight
3153
+ ]);
3154
+ useEffect(() => {
3155
+ const container = scrollRef.current;
3156
+ if (!container) return;
3157
+ const observer = new ResizeObserver(() => setViewportHeight(container.clientHeight));
3158
+ observer.observe(container);
3159
+ setViewportHeight(container.clientHeight);
3160
+ return () => observer.disconnect();
3161
+ }, []);
3162
+ /**
3163
+ * Report whether the list is pinned to the newest row.
3164
+ *
3165
+ * Called from more than the scroll handler on purpose: the list can arrive at
3166
+ * the top without a scroll event — a reset, a jump to newest, a shorter
3167
+ * result set — and a stale "not following" would silently divert every live
3168
+ * row into a held-back pile.
3169
+ */
3170
+ const syncFollowing = useCallback((container) => {
3171
+ const following = container.scrollTop <= rowHeight;
3172
+ if (following === followingRef.current) return;
3173
+ followingRef.current = following;
3174
+ onFollowChange === null || onFollowChange === void 0 || onFollowChange(following);
3175
+ }, [onFollowChange, rowHeight]);
3176
+ /**
3177
+ * Hold the reader's place when rows arrive above them.
3178
+ *
3179
+ * Runs before paint: by the time the browser draws, the scroll offset has
3180
+ * already absorbed the inserted height, so the content under the cursor never
3181
+ * moves. Skipped while following, where the whole point is to stay at the top.
3182
+ */
3183
+ useLayoutEffect(() => {
3184
+ const container = scrollRef.current;
3185
+ const previousTopId = previousTopIdRef.current;
3186
+ previousTopIdRef.current = rows.length ? getRowId(rows[0]) : null;
3187
+ const previousLength = previousLengthRef.current;
3188
+ previousLengthRef.current = rows.length;
3189
+ if (!container) return;
3190
+ syncFollowing(container);
3191
+ if (!prepending || followingRef.current || !previousTopId) return;
3192
+ if (rows.length <= previousLength) return;
3193
+ const prepended = rows.findIndex((row) => getRowId(row) === previousTopId);
3194
+ if (prepended <= 0) return;
3195
+ container.scrollTop += prepended * rowHeight;
3196
+ setScrollTop(container.scrollTop);
3197
+ }, [
3198
+ rows,
3199
+ getRowId,
3200
+ prepending,
3201
+ rowHeight,
3202
+ syncFollowing
3203
+ ]);
3204
+ /**
3205
+ * Page forward when the rows do not reach the bottom of the viewport.
3206
+ *
3207
+ * Without this the list only advances on a scroll event, so a tall screen —
3208
+ * or a filter matching few rows per page — stops at the first page with no
3209
+ * way to ask for more.
3210
+ */
3211
+ useEffect(() => {
3212
+ const container = scrollRef.current;
3213
+ if (!container || !hasMore || loadingMore || !onLoadMore) return;
3214
+ if (container.scrollHeight <= container.clientHeight) onLoadMore();
3215
+ }, [
3216
+ hasMore,
3217
+ loadingMore,
3218
+ onLoadMore,
3219
+ rows.length,
3220
+ viewportHeight
3221
+ ]);
3222
+ const handleScroll = useCallback((event) => {
3223
+ const container = event.currentTarget;
3224
+ setScrollTop(container.scrollTop);
3225
+ syncFollowing(container);
3226
+ const remaining = container.scrollHeight - container.scrollTop - container.clientHeight;
3227
+ if (hasMore && !loadingMore && remaining < container.clientHeight) onLoadMore === null || onLoadMore === void 0 || onLoadMore();
3228
+ }, [
3229
+ hasMore,
3230
+ loadingMore,
3231
+ onLoadMore,
3232
+ syncFollowing
3233
+ ]);
3234
+ const totalHeight = rows.length * rowHeight;
3235
+ const firstVisible = Math.max(0, Math.floor(scrollTop / rowHeight) - OVERSCAN);
3236
+ const visibleCount = Math.ceil((viewportHeight || 480) / rowHeight) + 20;
3237
+ const lastVisible = Math.min(rows.length, firstVisible + visibleCount);
3238
+ const visibleRows = useMemo(() => rows.slice(firstVisible, lastVisible), [
3239
+ rows,
3240
+ firstVisible,
3241
+ lastVisible
3242
+ ]);
3243
+ return /* @__PURE__ */ jsxs("div", {
3244
+ className: cn("@container/log flex min-h-0 flex-col", className),
3245
+ "data-opt-id": "NJ4N9",
3246
+ "data-opt-slug": "log-table.LogTable",
3247
+ children: [showHeader && /* @__PURE__ */ jsx(LogTableHeader, {
3248
+ columns,
3249
+ sort,
3250
+ onSortChange,
3251
+ density,
3252
+ labels
3253
+ }), /* @__PURE__ */ jsxs("div", {
3254
+ ref: scrollRef,
3255
+ onScroll: handleScroll,
3256
+ className: "relative min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
3257
+ role: "listbox",
3258
+ "aria-label": labels.listLabel,
3259
+ "aria-activedescendant": selectedId ? `log-row-${selectedId}` : void 0,
3260
+ tabIndex: 0,
3261
+ children: [
3262
+ rows.length === 0 ? emptyState : /* @__PURE__ */ jsx("div", {
3263
+ style: { height: totalHeight },
3264
+ className: "relative",
3265
+ children: visibleRows.map((row, index) => {
3266
+ var _isNew;
3267
+ const id = getRowId(row);
3268
+ return /* @__PURE__ */ jsx(LogTableRow, {
3269
+ id,
3270
+ row,
3271
+ columns,
3272
+ top: (firstVisible + index) * rowHeight,
3273
+ height: rowHeight,
3274
+ density,
3275
+ selected: id === selectedId,
3276
+ isNew: (_isNew = isNew === null || isNew === void 0 ? void 0 : isNew(row)) !== null && _isNew !== void 0 ? _isNew : false,
3277
+ onSelect,
3278
+ onCellAction,
3279
+ labels
3280
+ }, id);
3281
+ })
3282
+ }),
3283
+ loadingMore && /* @__PURE__ */ jsxs("div", {
3284
+ className: "gap-element text-text-tertiary flex items-center justify-center py-3 text-xs",
3285
+ children: [/* @__PURE__ */ jsx(Spinner, {
3286
+ size: "sm",
3287
+ "aria-hidden": true
3288
+ }), labels.loadingMore]
3289
+ }),
3290
+ !hasMore && rows.length > 0 && /* @__PURE__ */ jsx("p", {
3291
+ className: cn("text-text-tertiary", "py-3 text-center text-xs"),
3292
+ children: labels.endOfResults
3293
+ })
3294
+ ]
3295
+ })]
3296
+ });
3297
+ }
3298
+ /**
3299
+ * LogTable — see {@link LogTableInner}.
3300
+ *
3301
+ * The cast preserves the row generic through `forwardRef`, which otherwise
3302
+ * widens it to its constraint and takes the column callbacks' types with it.
3303
+ */
3304
+ const LogTable = forwardRef(LogTableInner);
3305
+ function LogTableHeader({ columns, sort, onSortChange, density, labels }) {
3306
+ return /* @__PURE__ */ jsx("div", {
3307
+ role: "row",
3308
+ className: cn("border-border-subtle text-text-tertiary flex shrink-0 items-center gap-3 border-b px-3", DENSITY_TEXT[density]),
3309
+ style: { height: ROW_HEIGHT[density] },
3310
+ children: columns.map((column) => {
3311
+ var _column$priority;
3312
+ const active = (sort === null || sort === void 0 ? void 0 : sort.columnId) === column.id;
3313
+ const sortable = column.sortable && onSortChange;
3314
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
3315
+ column.marker && /* @__PURE__ */ jsx("span", {
3316
+ "aria-hidden": true,
3317
+ className: "size-1.5 shrink-0"
3318
+ }),
3319
+ /* @__PURE__ */ jsx("span", {
3320
+ className: "truncate",
3321
+ children: column.header
3322
+ }),
3323
+ active && (sort.direction === "asc" ? /* @__PURE__ */ jsx(ChevronUp, {
3324
+ className: "size-3 shrink-0",
3325
+ "aria-hidden": true
3326
+ }) : /* @__PURE__ */ jsx(ChevronDown, {
3327
+ className: "size-3 shrink-0",
3328
+ "aria-hidden": true
3329
+ }))
3330
+ ] });
3331
+ return /* @__PURE__ */ jsx("div", {
3332
+ role: "columnheader",
3333
+ "aria-sort": active ? sort.direction === "asc" ? "ascending" : "descending" : void 0,
3334
+ className: cn("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"]),
3335
+ style: columnStyle(column),
3336
+ children: sortable ? /* @__PURE__ */ jsx("button", {
3337
+ type: "button",
3338
+ className: "hover:text-text-primary flex cursor-pointer items-center gap-1.5 truncate",
3339
+ title: active && sort.direction === "desc" ? labels.sortAscending : labels.sortDescending,
3340
+ onClick: () => onSortChange({
3341
+ columnId: column.id,
3342
+ direction: active && sort.direction === "desc" ? "asc" : "desc"
3343
+ }),
3344
+ children: content
3345
+ }) : content
3346
+ }, column.id);
3347
+ })
3348
+ });
3349
+ }
3350
+ function LogTableRowImpl({ id, row, columns, top, height, density, selected, isNew, onSelect, onCellAction, labels }) {
3351
+ return /* @__PURE__ */ jsx("div", {
3352
+ role: "option",
3353
+ "aria-selected": selected,
3354
+ tabIndex: -1,
3355
+ id: `log-row-${id}`,
3356
+ "data-log-row-id": id,
3357
+ onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(id),
3358
+ onKeyDown: (event) => {
3359
+ if (event.key !== "Enter" && event.key !== " ") return;
3360
+ event.preventDefault();
3361
+ onSelect === null || onSelect === void 0 || onSelect(id);
3362
+ },
3363
+ style: {
3364
+ top,
3365
+ height
3366
+ },
3367
+ className: cn("border-border-subtle/60 absolute inset-x-0 flex items-center gap-3 border-b px-3", DENSITY_TEXT[density], onSelect && "cursor-pointer", "transition-colors duration-100", "hover:bg-surface-hover", selected && "bg-accent-subtle hover:bg-accent-subtle", isNew && !selected && "opt-log-row-enter"),
3368
+ children: columns.map((column) => {
3369
+ var _column$priority2;
3370
+ const raw = column.value(row);
3371
+ const text = raw === null || raw === void 0 || raw === "" ? "" : String(raw);
3372
+ return /* @__PURE__ */ jsxs("div", {
3373
+ className: cn("group/cell relative flex min-w-0 items-center gap-1.5", column.align === "right" && "justify-end", HIDE_BELOW[(_column$priority2 = column.priority) !== null && _column$priority2 !== void 0 ? _column$priority2 : "always"]),
3374
+ style: columnStyle(column),
3375
+ children: [
3376
+ column.marker && text && /* @__PURE__ */ jsx("span", {
3377
+ "aria-hidden": true,
3378
+ className: "size-1.5 shrink-0 rounded-full",
3379
+ style: { backgroundColor: `hsl(${hueOf(text)} 65% 55%)` }
3380
+ }),
3381
+ /* @__PURE__ */ jsx("span", {
3382
+ className: cn("truncate", column.mono && "font-mono", text ? "text-text-secondary" : ABSENT),
3383
+ title: text || void 0,
3384
+ children: column.render ? column.render(row) : text || EM_DASH
3385
+ }),
3386
+ onCellAction && column.field && text && /* @__PURE__ */ jsx(CellActions, {
3387
+ field: column.field,
3388
+ value: text,
3389
+ onAction: onCellAction,
3390
+ labels
3391
+ })
3392
+ ]
3393
+ }, column.id);
3394
+ })
3395
+ });
3396
+ }
3397
+ /**
3398
+ * Memoized because a live log re-renders several times a second while almost
3399
+ * every visible row is unchanged.
3400
+ */
3401
+ const LogTableRow = memo(LogTableRowImpl);
3402
+ /**
3403
+ * The two questions a reader has about a value they just spotted: show me only
3404
+ * these, and get these out of my way.
3405
+ *
3406
+ * Revealed on hover rather than always drawn — a log is scanned, and a pair of
3407
+ * buttons on every cell of every row is a wall of chrome. They stay in the tab
3408
+ * order regardless, since hover is not an interaction everyone has.
3409
+ */
3410
+ function CellActions({ field, value, onAction, labels }) {
3411
+ const button = "cursor-pointer rounded-sm p-0.5 text-text-tertiary hover:bg-bg-muted hover:text-text-primary";
3412
+ return /* @__PURE__ */ jsxs("span", {
3413
+ className: cn("bg-surface/90 ml-auto hidden shrink-0 items-center gap-0.5 rounded pl-1", "group-hover/cell:flex focus-within:flex"),
3414
+ children: [/* @__PURE__ */ jsx("button", {
3415
+ type: "button",
3416
+ className: button,
3417
+ title: labels.filterFor(value),
3418
+ "aria-label": labels.filterFor(value),
3419
+ onClick: (event) => {
3420
+ event.stopPropagation();
3421
+ onAction({
3422
+ field,
3423
+ value,
3424
+ intent: "include"
3425
+ });
3426
+ },
3427
+ children: /* @__PURE__ */ jsx(Plus, {
3428
+ className: "size-3",
3429
+ "aria-hidden": true
3430
+ })
3431
+ }), /* @__PURE__ */ jsx("button", {
3432
+ type: "button",
3433
+ className: button,
3434
+ title: labels.filterOut(value),
3435
+ "aria-label": labels.filterOut(value),
3436
+ onClick: (event) => {
3437
+ event.stopPropagation();
3438
+ onAction({
3439
+ field,
3440
+ value,
3441
+ intent: "exclude"
3442
+ });
3443
+ },
3444
+ children: /* @__PURE__ */ jsx(Minus, {
3445
+ className: "size-3",
3446
+ "aria-hidden": true
3447
+ })
3448
+ })]
3449
+ });
3450
+ }
3451
+ //#endregion
3452
+ //#region src/lib/query/query-ast.ts
3453
+ const EMPTY_QUERY = { clauses: [] };
3454
+ /** Whether a query constrains anything at all. */
3455
+ function isEmptyQuery(query) {
3456
+ return !query || query.clauses.length === 0;
3457
+ }
3458
+ function sameClause(a, b) {
3459
+ if (a.type !== b.type) return false;
3460
+ if (a.type === "term" && b.type === "term") return a.value === b.value && a.negated === b.negated;
3461
+ const fieldA = a;
3462
+ const fieldB = b;
3463
+ 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]);
3464
+ }
3465
+ /** Add a clause unless an identical one is already there. */
3466
+ function addClause(query, clause) {
3467
+ if (query.clauses.some((existing) => sameClause(existing, clause))) return query;
3468
+ return { clauses: [...query.clauses, clause] };
3469
+ }
3470
+ /**
3471
+ * Constrain a field to a value — what a cell's "filter for this" action does.
3472
+ *
3473
+ * Applying the opposite of a constraint already present replaces it rather than
3474
+ * adding it: `status:200` then "filter out 200" should read as one changed
3475
+ * mind, not as a query that can never match.
3476
+ */
3477
+ function withFieldValue(query, field, value, options = {}) {
3478
+ var _options$negated, _options$operator;
3479
+ const negated = (_options$negated = options.negated) !== null && _options$negated !== void 0 ? _options$negated : false;
3480
+ const operator = (_options$operator = options.operator) !== null && _options$operator !== void 0 ? _options$operator : "eq";
3481
+ return { clauses: [...query.clauses.filter((clause) => !(clause.type === "field" && clause.field === field && clause.operator === operator && clause.values.length === 1 && clause.values[0] === value)), {
3482
+ type: "field",
3483
+ field,
3484
+ operator,
3485
+ values: [value],
3486
+ negated
3487
+ }] };
3488
+ }
3489
+ /** Drop every clause naming `field`. */
3490
+ function withoutField(query, field) {
3491
+ return { clauses: query.clauses.filter((clause) => clause.type !== "field" || clause.field !== field) };
3492
+ }
3493
+ /** Drop the clause at `index`, for a removable chip. */
3494
+ function removeClause(query, index) {
3495
+ return { clauses: query.clauses.filter((_, at) => at !== index) };
3496
+ }
3497
+ /** Every field the query names, in the order they first appear. */
3498
+ function queryFields(query) {
3499
+ const seen = [];
3500
+ for (const clause of query.clauses) if (clause.type === "field" && !seen.includes(clause.field)) seen.push(clause.field);
3501
+ return seen;
3502
+ }
3503
+ /** The values a field is constrained to, if any — for showing a filter as active. */
3504
+ function fieldValues(query, field) {
3505
+ return query.clauses.flatMap((clause) => clause.type === "field" && clause.field === field && !clause.negated ? clause.values : []);
3506
+ }
3507
+ /**
3508
+ * Fold a cell action into the query.
3509
+ *
3510
+ * The pair of buttons on a log cell is the fastest path from "I noticed this
3511
+ * value" to a filtered view, and the reason it works is that the result is a
3512
+ * query the reader can then see, edit and share — not a hidden filter stack
3513
+ * with its own rules.
3514
+ */
3515
+ function applyCellAction(query, action) {
3516
+ return withFieldValue(query, action.field, action.value, { negated: action.intent === "exclude" });
3517
+ }
3518
+ //#endregion
3519
+ //#region src/lib/query/query-parse.ts
3520
+ const FIELD_NAME = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
3521
+ /**
3522
+ * Split on whitespace, but keep quoted runs and parenthesised groups whole.
3523
+ *
3524
+ * An unterminated quote or group swallows the rest of the line rather than
3525
+ * erroring: the reader is mid-word, and the alternative is the query flickering
3526
+ * between two different meanings as they type the closing character.
3527
+ */
3528
+ function tokenize(input) {
3529
+ const tokens = [];
3530
+ let index = 0;
3531
+ while (index < input.length) {
3532
+ while (index < input.length && /\s/.test(input[index])) index += 1;
3533
+ if (index >= input.length) break;
3534
+ const start = index;
3535
+ let text = "";
3536
+ let quoted = false;
3537
+ let atTokenStart = true;
3538
+ let depth = 0;
3539
+ while (index < input.length) {
3540
+ const char = input[index];
3541
+ if (char === "\"") {
3542
+ if (atTokenStart) quoted = true;
3543
+ atTokenStart = false;
3544
+ index += 1;
3545
+ while (index < input.length && input[index] !== "\"") {
3546
+ text += input[index];
3547
+ index += 1;
3548
+ }
3549
+ index += 1;
3550
+ continue;
3551
+ }
3552
+ if (char === "(") depth += 1;
3553
+ if (char === ")") depth = Math.max(0, depth - 1);
3554
+ if (/\s/.test(char) && depth === 0) break;
3555
+ atTokenStart = false;
3556
+ text += char;
3557
+ index += 1;
3558
+ }
3559
+ if (text.length > 0 || quoted) tokens.push({
3560
+ text,
3561
+ at: start,
3562
+ quoted
3563
+ });
3564
+ }
3565
+ return tokens;
3566
+ }
3567
+ /** `(a or b)` → `["a", "b"]`; anything else is a single value. */
3568
+ function readValues(raw) {
3569
+ const trimmed = raw.trim();
3570
+ if (!trimmed.startsWith("(")) return [stripQuotes(trimmed)];
3571
+ return trimmed.replace(/^\(/, "").replace(/\)$/, "").split(/\s+or\s+/i).map((part) => stripQuotes(part.trim())).filter((part) => part.length > 0);
3572
+ }
3573
+ function stripQuotes(value) {
3574
+ if (value.length >= 2 && value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
3575
+ return value.replace(/^"/, "");
3576
+ }
3577
+ const OPERATORS = [
3578
+ {
3579
+ symbol: ">=",
3580
+ operator: "gte"
3581
+ },
3582
+ {
3583
+ symbol: "<=",
3584
+ operator: "lte"
3585
+ },
3586
+ {
3587
+ symbol: ":",
3588
+ operator: "eq"
3589
+ },
3590
+ {
3591
+ symbol: ">",
3592
+ operator: "gt"
3593
+ },
3594
+ {
3595
+ symbol: "<",
3596
+ operator: "lt"
3597
+ }
3598
+ ];
3599
+ /** Where a field operator starts in the token, or `-1`. */
3600
+ function findOperator(text) {
3601
+ let best = null;
3602
+ for (const { symbol, operator } of OPERATORS) {
3603
+ const index = text.indexOf(symbol);
3604
+ if (index <= 0) continue;
3605
+ if (!best || index < best.index || index === best.index && symbol.length > best.symbol.length) best = {
3606
+ index,
3607
+ symbol,
3608
+ operator
3609
+ };
3610
+ }
3611
+ return best;
3612
+ }
3613
+ /** Parse, keeping whatever could not be understood as a plain term. */
3614
+ function parseQueryWithDiagnostics(input) {
3615
+ const diagnostics = [];
3616
+ const clauses = [];
3617
+ let trailing = null;
3618
+ for (const token of tokenize(input)) {
3619
+ let text = token.text;
3620
+ let negated = false;
3621
+ if (!token.quoted && text.startsWith("-") && text.length > 1) {
3622
+ negated = true;
3623
+ text = text.slice(1);
3624
+ }
3625
+ const operator = token.quoted ? null : findOperator(text);
3626
+ if (!operator) {
3627
+ if (text.length > 0) clauses.push({
3628
+ type: "term",
3629
+ value: text,
3630
+ negated
3631
+ });
3632
+ continue;
3633
+ }
3634
+ const field = text.slice(0, operator.index);
3635
+ const rawValue = text.slice(operator.index + operator.symbol.length);
3636
+ if (!FIELD_NAME.test(field)) {
3637
+ clauses.push({
3638
+ type: "term",
3639
+ value: text,
3640
+ negated
3641
+ });
3642
+ continue;
3643
+ }
3644
+ if (rawValue === "") {
3645
+ trailing = {
3646
+ field,
3647
+ operator: operator.operator,
3648
+ value: ""
3649
+ };
3650
+ continue;
3651
+ }
3652
+ if (rawValue === "*" && operator.operator === "eq") {
3653
+ clauses.push({
3654
+ type: "field",
3655
+ field,
3656
+ operator: "exists",
3657
+ values: [],
3658
+ negated
3659
+ });
3660
+ continue;
3661
+ }
3662
+ if (operator.operator === "eq" && rawValue.length > 2 && rawValue.startsWith("*") && rawValue.endsWith("*")) {
3663
+ clauses.push({
3664
+ type: "field",
3665
+ field,
3666
+ operator: "contains",
3667
+ values: [stripQuotes(rawValue.slice(1, -1))],
3668
+ negated
3669
+ });
3670
+ continue;
3671
+ }
3672
+ const values = readValues(rawValue);
3673
+ if (values.length === 0) {
3674
+ diagnostics.push({
3675
+ at: token.at,
3676
+ text: token.text,
3677
+ message: "Empty value group"
3678
+ });
3679
+ continue;
3680
+ }
3681
+ if (operator.operator !== "eq" && values.length > 1) diagnostics.push({
3682
+ at: token.at,
3683
+ text: token.text,
3684
+ message: "A comparison takes a single value"
3685
+ });
3686
+ const clause = {
3687
+ type: "field",
3688
+ field,
3689
+ operator: operator.operator,
3690
+ values: operator.operator === "eq" ? values : values.slice(0, 1),
3691
+ negated
3692
+ };
3693
+ clauses.push(clause);
3694
+ if (!rawValue.startsWith("(")) {
3695
+ var _values$;
3696
+ trailing = {
3697
+ field,
3698
+ operator: operator.operator,
3699
+ value: (_values$ = values[0]) !== null && _values$ !== void 0 ? _values$ : ""
3700
+ };
3701
+ }
3702
+ }
3703
+ return {
3704
+ query: { clauses },
3705
+ diagnostics,
3706
+ trailing
3707
+ };
3708
+ }
3709
+ /** Parse, discarding diagnostics. */
3710
+ function parseQuery(input) {
3711
+ return parseQueryWithDiagnostics(input).query;
3712
+ }
3713
+ //#endregion
3714
+ //#region src/lib/query/query-format.ts
3715
+ const OPERATOR_SYMBOL = {
3716
+ eq: ":",
3717
+ contains: ":",
3718
+ gt: ">",
3719
+ gte: ">=",
3720
+ lt: "<",
3721
+ lte: "<=",
3722
+ exists: ":"
3723
+ };
3724
+ /** Quote a value that would otherwise re-parse as something else. */
3725
+ function quoteValue(value) {
3726
+ return /[\s"()]/.test(value) || value === "" ? `"${value.replace(/"/g, "")}"` : value;
3727
+ }
3728
+ /** One clause as text. */
3729
+ function formatClause(clause) {
3730
+ var _clause$values$2;
3731
+ const prefix = clause.negated ? "-" : "";
3732
+ if (clause.type === "term") return `${prefix}${quoteValue(clause.value)}`;
3733
+ if (clause.operator === "exists") return `${prefix}${clause.field}:*`;
3734
+ if (clause.operator === "contains") {
3735
+ var _clause$values$;
3736
+ return `${prefix}${clause.field}:*${(_clause$values$ = clause.values[0]) !== null && _clause$values$ !== void 0 ? _clause$values$ : ""}*`;
3737
+ }
3738
+ const symbol = OPERATOR_SYMBOL[clause.operator];
3739
+ if (clause.values.length > 1) return `${prefix}${clause.field}${symbol}(${clause.values.map(quoteValue).join(" or ")})`;
3740
+ return `${prefix}${clause.field}${symbol}${quoteValue((_clause$values$2 = clause.values[0]) !== null && _clause$values$2 !== void 0 ? _clause$values$2 : "")}`;
3741
+ }
3742
+ /** The whole query as text. */
3743
+ function formatQuery(query) {
3744
+ return query.clauses.map(formatClause).join(" ");
3745
+ }
3746
+ /** A short human label for a clause chip. */
3747
+ function describeClause(clause) {
3748
+ if (clause.type === "term") return clause.negated ? `not "${clause.value}"` : `"${clause.value}"`;
3749
+ if (clause.operator === "exists") return clause.negated ? `${clause.field} missing` : `${clause.field} present`;
3750
+ if (clause.operator === "contains") {
3751
+ var _clause$values$3;
3752
+ return `${clause.negated ? "not " : ""}${clause.field} ⊃ ${(_clause$values$3 = clause.values[0]) !== null && _clause$values$3 !== void 0 ? _clause$values$3 : ""}`;
3753
+ }
3754
+ const values = clause.values.join(", ");
3755
+ const symbol = OPERATOR_SYMBOL[clause.operator];
3756
+ return `${clause.negated ? "not " : ""}${clause.field} ${symbol === ":" ? "=" : symbol} ${values}`;
3757
+ }
3758
+ //#endregion
3759
+ //#region src/shells/query-bar.tsx
3760
+ const defaultLabels$1 = {
3761
+ placeholder: "Search, or filter with field:value",
3762
+ ariaLabel: "Search query",
3763
+ clear: "Clear query",
3764
+ removeClause: (clause) => `Remove ${clause}`,
3765
+ fieldsHeading: "Fields",
3766
+ valuesHeading: "Values",
3767
+ syntaxHint: null
3768
+ };
3769
+ const MAX_SUGGESTIONS = 8;
3770
+ /**
3771
+ * QueryBar — one control for "find this" and "only these".
3772
+ *
3773
+ * A search box and a row of filter pickers answer halves of the same question
3774
+ * and neither can express the other, so people end up using both and holding
3775
+ * the combination in their head. Typing the filter is what closes that: the
3776
+ * whole question is one string, which means it can be read back, corrected in
3777
+ * place, pasted to a colleague, and stored in a URL.
3778
+ *
3779
+ * The text is the source of truth while the reader is typing; the parsed query
3780
+ * is committed on Enter or blur. Anything half-written parses to a plain term
3781
+ * rather than an error, so the bar never argues with someone mid-word.
3782
+ *
3783
+ * @example
3784
+ * ```tsx
3785
+ * <QueryBar
3786
+ * value={query}
3787
+ * onChange={setQuery}
3788
+ * fields={[{ name: "path", values: ["/checkout", "/cart"] }]}
3789
+ * showClauses
3790
+ * />
3791
+ * ```
3792
+ */
3793
+ function QueryBar({ value, onChange, fields = [], showClauses = false, live = false, disabled = false, className, labels: customLabels }) {
3794
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels$1), customLabels);
3795
+ const inputRef = useRef(null);
3796
+ const [text, setText] = useState(() => formatQuery(value));
3797
+ const [focused, setFocused] = useState(false);
3798
+ const [highlighted, setHighlighted] = useState(0);
3799
+ const committedRef = useRef(formatQuery(value));
3800
+ useEffect(() => {
3801
+ const next = formatQuery(value);
3802
+ if (next === committedRef.current) return;
3803
+ committedRef.current = next;
3804
+ if (!focused) setText(next);
3805
+ }, [value, focused]);
3806
+ const parsed = useMemo(() => parseQueryWithDiagnostics(text), [text]);
3807
+ const commit = useCallback((nextText) => {
3808
+ const nextQuery = parseQueryWithDiagnostics(nextText).query;
3809
+ committedRef.current = formatQuery(nextQuery);
3810
+ onChange(nextQuery);
3811
+ }, [onChange]);
3812
+ const suggestions = useSuggestions(text, parsed.trailing, fields);
3813
+ const applySuggestion = useCallback((suggestion) => {
3814
+ var _inputRef$current;
3815
+ const nextText = suggestion.replace(text);
3816
+ setText(nextText);
3817
+ if (suggestion.kind === "value") commit(nextText);
3818
+ (_inputRef$current = inputRef.current) === null || _inputRef$current === void 0 || _inputRef$current.focus();
3819
+ setHighlighted(0);
3820
+ }, [text, commit]);
3821
+ const handleKeyDown = (event) => {
3822
+ if (suggestions.length > 0 && focused) {
3823
+ if (event.key === "ArrowDown") {
3824
+ event.preventDefault();
3825
+ setHighlighted((current) => (current + 1) % suggestions.length);
3826
+ return;
3827
+ }
3828
+ if (event.key === "ArrowUp") {
3829
+ event.preventDefault();
3830
+ setHighlighted((current) => (current - 1 + suggestions.length) % suggestions.length);
3831
+ return;
3832
+ }
3833
+ if (event.key === "Tab" || event.key === "Enter" && highlighted > 0) {
3834
+ event.preventDefault();
3835
+ applySuggestion(suggestions[highlighted]);
3836
+ return;
3837
+ }
3838
+ }
3839
+ if (event.key === "Enter") {
3840
+ event.preventDefault();
3841
+ commit(text);
3842
+ return;
3843
+ }
3844
+ if (event.key === "Escape") {
3845
+ var _inputRef$current2;
3846
+ setText(committedRef.current);
3847
+ (_inputRef$current2 = inputRef.current) === null || _inputRef$current2 === void 0 || _inputRef$current2.blur();
3848
+ }
3849
+ };
3850
+ const showSuggestions = focused && suggestions.length > 0;
3851
+ return /* @__PURE__ */ jsxs("div", {
3852
+ className: cn("relative flex min-w-0 flex-col gap-1.5", className),
3853
+ "data-opt-id": "JSVZ1",
3854
+ "data-opt-slug": "query-bar.QueryBar",
3855
+ children: [
3856
+ /* @__PURE__ */ jsxs("div", {
3857
+ className: cn("bg-surface flex h-9 items-center gap-2 rounded-md border px-2.5", OPT_BORDER, "focus-within:border-accent transition-colors", disabled && "opacity-60"),
3858
+ children: [
3859
+ /* @__PURE__ */ jsx(Search, {
3860
+ className: "text-text-tertiary size-4 shrink-0",
3861
+ "aria-hidden": true
3862
+ }),
3863
+ /* @__PURE__ */ jsx("input", {
3864
+ ref: inputRef,
3865
+ type: "text",
3866
+ role: "combobox",
3867
+ "aria-expanded": showSuggestions,
3868
+ "aria-controls": "query-bar-suggestions",
3869
+ "aria-autocomplete": "list",
3870
+ "aria-label": labels.ariaLabel,
3871
+ placeholder: labels.placeholder,
3872
+ disabled,
3873
+ className: cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent font-mono text-sm outline-none focus-visible:outline-none"),
3874
+ value: text,
3875
+ onChange: (event) => {
3876
+ setText(event.target.value);
3877
+ setHighlighted(0);
3878
+ if (live) commit(event.target.value);
3879
+ },
3880
+ onFocus: () => setFocused(true),
3881
+ onBlur: () => {
3882
+ window.setTimeout(() => setFocused(false), 120);
3883
+ if (!live) commit(text);
3884
+ },
3885
+ onKeyDown: handleKeyDown
3886
+ }),
3887
+ text.length > 0 && /* @__PURE__ */ jsx("button", {
3888
+ type: "button",
3889
+ "aria-label": labels.clear,
3890
+ title: labels.clear,
3891
+ className: cn("text-text-tertiary hover:text-text-primary cursor-pointer rounded-sm p-0.5", "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-ring-offset"),
3892
+ onClick: () => {
3893
+ setText("");
3894
+ commit("");
3895
+ },
3896
+ children: /* @__PURE__ */ jsx(X, {
3897
+ className: "size-3.5",
3898
+ "aria-hidden": true
3899
+ })
3900
+ })
3901
+ ]
3902
+ }),
3903
+ showSuggestions && /* @__PURE__ */ jsx("ul", {
3904
+ id: "query-bar-suggestions",
3905
+ role: "listbox",
3906
+ className: cn("bg-surface-overlay absolute top-10 z-40 max-h-64 w-full overflow-y-auto rounded-md border p-1 shadow-lg", "border border-border"),
3907
+ children: suggestions.map((suggestion, index) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("button", {
3908
+ type: "button",
3909
+ role: "option",
3910
+ "aria-selected": index === highlighted,
3911
+ className: cn("flex w-full cursor-pointer items-center justify-between gap-3 rounded px-2 py-1 text-left text-sm", index === highlighted ? "bg-bg-subtle" : "hover:bg-bg-subtle"),
3912
+ onMouseEnter: () => setHighlighted(index),
3913
+ onMouseDown: (event) => event.preventDefault(),
3914
+ onClick: () => applySuggestion(suggestion),
3915
+ children: [/* @__PURE__ */ jsx("span", {
3916
+ className: "text-text-primary truncate font-mono",
3917
+ children: suggestion.label
3918
+ }), suggestion.hint && /* @__PURE__ */ jsx("span", {
3919
+ className: "text-text-tertiary shrink-0 text-xs",
3920
+ children: suggestion.hint
3921
+ })]
3922
+ }) }, `${suggestion.kind}-${suggestion.label}`))
3923
+ }),
3924
+ showClauses && value.clauses.length > 0 && /* @__PURE__ */ jsx("ul", {
3925
+ className: "flex flex-wrap items-center gap-1.5",
3926
+ children: value.clauses.map((clause, index) => {
3927
+ const label = describeClause(clause);
3928
+ return /* @__PURE__ */ jsxs("li", {
3929
+ className: cn("bg-bg-subtle text-text-secondary flex items-center gap-1 rounded px-1.5 py-0.5 text-xs", clause.negated && "text-danger-fg"),
3930
+ children: [/* @__PURE__ */ jsx("span", {
3931
+ className: "truncate",
3932
+ children: label
3933
+ }), /* @__PURE__ */ jsx("button", {
3934
+ type: "button",
3935
+ "aria-label": labels.removeClause(label),
3936
+ className: "hover:text-text-primary cursor-pointer",
3937
+ onClick: () => {
3938
+ const next = removeClause(value, index);
3939
+ setText(formatQuery(next));
3940
+ committedRef.current = formatQuery(next);
3941
+ onChange(next);
3942
+ },
3943
+ children: /* @__PURE__ */ jsx(X, {
3944
+ className: "size-3",
3945
+ "aria-hidden": true
3946
+ })
3947
+ })]
3948
+ }, `${label}-${index}`);
3949
+ })
3950
+ }),
3951
+ parsed.diagnostics.length > 0 && /* @__PURE__ */ jsx(QueryDiagnostics, { diagnostics: parsed.diagnostics }),
3952
+ labels.syntaxHint && !showSuggestions && /* @__PURE__ */ jsx("p", {
3953
+ className: "text-text-tertiary text-xs",
3954
+ children: labels.syntaxHint
3955
+ })
3956
+ ]
3957
+ });
3958
+ }
3959
+ QueryBar.displayName = "QueryBar";
3960
+ function QueryDiagnostics({ diagnostics }) {
3961
+ return /* @__PURE__ */ jsx("ul", {
3962
+ className: "text-warning-fg flex flex-col gap-0.5 text-xs",
3963
+ children: diagnostics.map((diagnostic, index) => /* @__PURE__ */ jsxs("li", { children: [
3964
+ /* @__PURE__ */ jsx("code", {
3965
+ className: "font-mono",
3966
+ children: diagnostic.text
3967
+ }),
3968
+ " —",
3969
+ " ",
3970
+ diagnostic.message
3971
+ ] }, `${diagnostic.at}-${index}`))
3972
+ });
3973
+ }
3974
+ /**
3975
+ * What to offer, based on the token under the cursor.
3976
+ *
3977
+ * Completing fields is the part that makes the syntax discoverable at all —
3978
+ * nobody guesses that `utm_source` is a field they can filter on. Once the
3979
+ * field is chosen, its known values are the second half of the same guess.
3980
+ */
3981
+ function useSuggestions(text, trailing, fields) {
3982
+ return useMemo(() => {
3983
+ const lastSpace = text.lastIndexOf(" ");
3984
+ const head = lastSpace === -1 ? "" : text.slice(0, lastSpace + 1);
3985
+ const token = text.slice(lastSpace + 1);
3986
+ if (trailing) {
3987
+ var _field$values;
3988
+ const field = fields.find((candidate) => candidate.name === trailing.field);
3989
+ const values = (_field$values = field === null || field === void 0 ? void 0 : field.values) !== null && _field$values !== void 0 ? _field$values : [];
3990
+ const typed = trailing.value.toLowerCase();
3991
+ return values.filter((value) => value.toLowerCase().includes(typed)).slice(0, MAX_SUGGESTIONS).map((value) => ({
3992
+ kind: "value",
3993
+ label: `${trailing.field}:${value}`,
3994
+ replace: () => `${head}${trailing.field}:${/[\s"()]/.test(value) ? `"${value}"` : value} `
3995
+ }));
3996
+ }
3997
+ const bare = token.replace(/^-/, "").toLowerCase();
3998
+ if (bare.length === 0) return [];
3999
+ return fields.filter((field) => field.name.toLowerCase().includes(bare)).slice(0, MAX_SUGGESTIONS).map((field) => ({
4000
+ kind: "field",
4001
+ label: `${field.name}:`,
4002
+ hint: field.hint,
4003
+ replace: () => `${head}${token.startsWith("-") ? "-" : ""}${field.name}:`
4004
+ }));
4005
+ }, [
4006
+ text,
4007
+ trailing,
4008
+ fields
4009
+ ]);
4010
+ }
4011
+ //#endregion
4012
+ //#region src/shells/field-sidebar.tsx
4013
+ const defaultLabels = {
4014
+ heading: "Fields",
4015
+ searchPlaceholder: "Find a field",
4016
+ searchLabel: "Find a field",
4017
+ empty: "No fields match.",
4018
+ addColumn: (field) => `Show ${field} as a column`,
4019
+ removeColumn: (field) => `Remove the ${field} column`,
4020
+ filterFor: (value) => `Filter for ${value}`,
4021
+ filterOut: (value) => `Filter out ${value}`,
4022
+ topValues: (count) => `Top ${count} values`,
4023
+ noValues: "No values in this range.",
4024
+ loadingValues: "Counting…",
4025
+ otherGroup: "Other"
4026
+ };
4027
+ /**
4028
+ * FieldSidebar — what is in this data, and what does it contain.
4029
+ *
4030
+ * Without it, a log answers only the questions you already knew to ask: you can
4031
+ * filter on `utm_source` if you remember it exists, and you can read a value if
4032
+ * you open a row and scroll to it. Listing the fields turns that around — the
4033
+ * data tells the reader what is available — and the value counts answer the
4034
+ * question that actually follows ("mostly which?") before they have to run a
4035
+ * query to find out.
4036
+ *
4037
+ * Three actions live here because they are the three things people do next:
4038
+ * put the field in the table, keep only rows with a value, drop rows with it.
4039
+ *
4040
+ * @example
4041
+ * ```tsx
4042
+ * <FieldSidebar
4043
+ * fields={fields}
4044
+ * expandedField={openField}
4045
+ * onExpandedFieldChange={setOpenField}
4046
+ * values={valuesQuery.data}
4047
+ * valuesLoading={valuesQuery.isLoading}
4048
+ * onToggleColumn={(field, next) => setColumns(...)}
4049
+ * onFilter={(field, value, intent) => setQuery(applyCellAction(query, { field, value, intent }))}
4050
+ * />
4051
+ * ```
4052
+ */
4053
+ function FieldSidebar({ fields, expandedField = null, onExpandedFieldChange, values, valuesLoading = false, sampledRows, onToggleColumn, onFilter, header, className, labels: customLabels }) {
4054
+ const labels = _objectSpread2(_objectSpread2({}, defaultLabels), customLabels);
4055
+ const [search, setSearch] = useState("");
4056
+ const groups = useMemo(() => {
4057
+ const needle = search.trim().toLowerCase();
4058
+ const matching = needle ? fields.filter((field) => field.name.toLowerCase().includes(needle)) : fields;
4059
+ const byGroup = /* @__PURE__ */ new Map();
4060
+ for (const field of matching) {
4061
+ var _field$group;
4062
+ const key = (_field$group = field.group) !== null && _field$group !== void 0 ? _field$group : "";
4063
+ const bucket = byGroup.get(key);
4064
+ if (bucket) bucket.push(field);
4065
+ else byGroup.set(key, [field]);
4066
+ }
4067
+ return [...byGroup.entries()];
4068
+ }, [fields, search]);
4069
+ const total = useMemo(() => (values !== null && values !== void 0 ? values : []).reduce((sum, entry) => sum + entry.count, 0), [values]);
4070
+ return /* @__PURE__ */ jsxs("aside", {
4071
+ className: cn("flex min-h-0 flex-col", className),
4072
+ "aria-label": labels.heading,
4073
+ "data-opt-id": "17D4Y",
4074
+ "data-opt-slug": "field-sidebar.FieldSidebar",
4075
+ children: [
4076
+ header,
4077
+ /* @__PURE__ */ jsxs("div", {
4078
+ className: cn("flex items-center gap-2 border-b px-3 py-2", "focus-within:border-accent transition-colors", OPT_BORDER),
4079
+ children: [/* @__PURE__ */ jsx(Search, {
4080
+ className: "text-text-tertiary size-3.5 shrink-0",
4081
+ "aria-hidden": true
4082
+ }), /* @__PURE__ */ jsx("input", {
4083
+ type: "search",
4084
+ "aria-label": labels.searchLabel,
4085
+ placeholder: labels.searchPlaceholder,
4086
+ className: cn("text-text-primary placeholder:text-text-tertiary min-w-0 flex-1", "bg-transparent text-sm outline-none focus-visible:outline-none"),
4087
+ value: search,
4088
+ onChange: (event) => setSearch(event.target.value)
4089
+ })]
4090
+ }),
4091
+ /* @__PURE__ */ jsx("div", {
4092
+ className: "min-h-0 flex-1 overflow-y-auto",
4093
+ children: groups.length === 0 ? /* @__PURE__ */ jsx("p", {
4094
+ className: cn(OPT_TEXT_TERTIARY, "px-3 py-4 text-xs"),
4095
+ children: labels.empty
4096
+ }) : groups.map(([group, groupFields]) => /* @__PURE__ */ jsxs("section", { children: [group && /* @__PURE__ */ jsx("h4", {
4097
+ className: cn("text-text-tertiary", "px-3 pt-3 pb-1 text-[11px] font-medium tracking-wide uppercase"),
4098
+ children: group
4099
+ }), /* @__PURE__ */ jsx("ul", { children: groupFields.map((field) => /* @__PURE__ */ jsx(FieldRow, {
4100
+ field,
4101
+ expanded: expandedField === field.name,
4102
+ onToggleExpanded: () => onExpandedFieldChange === null || onExpandedFieldChange === void 0 ? void 0 : onExpandedFieldChange(expandedField === field.name ? null : field.name),
4103
+ onToggleColumn,
4104
+ labels,
4105
+ children: expandedField === field.name && /* @__PURE__ */ jsx(FieldValues, {
4106
+ values,
4107
+ loading: valuesLoading,
4108
+ total,
4109
+ sampledRows,
4110
+ onFilter: onFilter ? (value, intent) => onFilter(field.name, value, intent) : void 0,
4111
+ labels
4112
+ })
4113
+ }, field.name)) })] }, group || "__ungrouped__"))
4114
+ })
4115
+ ]
4116
+ });
4117
+ }
4118
+ FieldSidebar.displayName = "FieldSidebar";
4119
+ function FieldRow({ field, expanded, onToggleExpanded, onToggleColumn, labels, children }) {
4120
+ var _field$columnable, _field$inTable;
4121
+ const columnable = (_field$columnable = field.columnable) !== null && _field$columnable !== void 0 ? _field$columnable : true;
4122
+ return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsxs("div", {
4123
+ className: "hover:bg-bg-subtle group/field flex items-center gap-1.5 px-3 py-1",
4124
+ children: [/* @__PURE__ */ jsxs("button", {
4125
+ type: "button",
4126
+ className: "flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left",
4127
+ "aria-expanded": expanded,
4128
+ onClick: onToggleExpanded,
4129
+ children: [field.type && /* @__PURE__ */ jsx("span", {
4130
+ className: cn("text-text-tertiary", "w-8 shrink-0 truncate font-mono text-[10px] uppercase"),
4131
+ children: field.type
4132
+ }), /* @__PURE__ */ jsx("span", {
4133
+ className: "text-text-primary truncate font-mono text-xs",
4134
+ children: field.name
4135
+ })]
4136
+ }), onToggleColumn && columnable && /* @__PURE__ */ jsx("button", {
4137
+ type: "button",
4138
+ className: cn("cursor-pointer rounded-sm p-0.5", field.inTable ? "text-accent" : "text-text-tertiary hover:text-text-primary opacity-0 group-hover/field:opacity-100 focus-visible:opacity-100"),
4139
+ "aria-label": field.inTable ? labels.removeColumn(field.name) : labels.addColumn(field.name),
4140
+ title: field.inTable ? labels.removeColumn(field.name) : labels.addColumn(field.name),
4141
+ "aria-pressed": (_field$inTable = field.inTable) !== null && _field$inTable !== void 0 ? _field$inTable : false,
4142
+ onClick: () => onToggleColumn(field.name, !field.inTable),
4143
+ children: /* @__PURE__ */ jsx(Columns3, {
4144
+ className: "size-3.5",
4145
+ "aria-hidden": true
4146
+ })
4147
+ })]
4148
+ }), children] });
4149
+ }
4150
+ /**
4151
+ * The distribution, as bars.
4152
+ *
4153
+ * A count alone ("342") means nothing without the denominator, and a reader
4154
+ * scanning for "what is this field mostly" reads the bar before the number.
4155
+ * The sample size is stated rather than implied — these are the top values in
4156
+ * the rows that were counted, not in all of history, and a chart that hides
4157
+ * that invites the wrong conclusion.
4158
+ */
4159
+ function FieldValues({ values, loading, total, sampledRows, onFilter, labels }) {
4160
+ if (loading) return /* @__PURE__ */ jsxs("p", {
4161
+ className: cn(OPT_TEXT_TERTIARY, "flex items-center gap-2 px-3 pb-2 text-xs"),
4162
+ children: [/* @__PURE__ */ jsx(Spinner, {
4163
+ size: "sm",
4164
+ "aria-hidden": true
4165
+ }), labels.loadingValues]
4166
+ });
4167
+ if (!values || values.length === 0) return /* @__PURE__ */ jsx("p", {
4168
+ className: cn(OPT_TEXT_TERTIARY, "px-3 pb-2 text-xs"),
4169
+ children: labels.noValues
4170
+ });
4171
+ const denominator = sampledRows !== null && sampledRows !== void 0 ? sampledRows : total;
4172
+ return /* @__PURE__ */ jsxs("div", {
4173
+ className: "flex flex-col gap-1 px-3 pt-1 pb-2",
4174
+ children: [/* @__PURE__ */ jsxs("p", {
4175
+ className: cn(OPT_TEXT_TERTIARY, "text-[11px]"),
4176
+ children: [labels.topValues(values.length), sampledRows ? ` · ${sampledRows.toLocaleString()}` : ""]
4177
+ }), values.map((entry) => {
4178
+ var _entry$share;
4179
+ const share = (_entry$share = entry.share) !== null && _entry$share !== void 0 ? _entry$share : denominator > 0 ? entry.count / denominator : 0;
4180
+ return /* @__PURE__ */ jsxs("div", {
4181
+ className: "group/value flex flex-col gap-0.5",
4182
+ children: [/* @__PURE__ */ jsxs("div", {
4183
+ className: "flex items-center justify-between gap-2",
4184
+ children: [
4185
+ /* @__PURE__ */ jsx("span", {
4186
+ className: "text-text-secondary min-w-0 flex-1 truncate font-mono text-[11px]",
4187
+ title: entry.value,
4188
+ children: entry.value || "—"
4189
+ }),
4190
+ /* @__PURE__ */ jsxs("span", {
4191
+ className: cn(OPT_TEXT_TERTIARY, "shrink-0 text-[11px] tabular-nums"),
4192
+ children: [Math.round(share * 100), "%"]
4193
+ }),
4194
+ onFilter && /* @__PURE__ */ jsxs("span", {
4195
+ className: "hidden shrink-0 items-center gap-0.5 group-hover/value:flex focus-within:flex",
4196
+ children: [/* @__PURE__ */ jsx("button", {
4197
+ type: "button",
4198
+ className: "text-text-tertiary hover:text-text-primary cursor-pointer",
4199
+ "aria-label": labels.filterFor(entry.value),
4200
+ title: labels.filterFor(entry.value),
4201
+ onClick: () => onFilter(entry.value, "include"),
4202
+ children: /* @__PURE__ */ jsx(Plus, {
4203
+ className: "size-3",
4204
+ "aria-hidden": true
4205
+ })
4206
+ }), /* @__PURE__ */ jsx("button", {
4207
+ type: "button",
4208
+ className: "text-text-tertiary hover:text-text-primary cursor-pointer",
4209
+ "aria-label": labels.filterOut(entry.value),
4210
+ title: labels.filterOut(entry.value),
4211
+ onClick: () => onFilter(entry.value, "exclude"),
4212
+ children: /* @__PURE__ */ jsx(Minus, {
4213
+ className: "size-3",
4214
+ "aria-hidden": true
4215
+ })
4216
+ })]
4217
+ })
4218
+ ]
4219
+ }), /* @__PURE__ */ jsx("div", {
4220
+ className: "bg-bg-muted h-1 w-full overflow-hidden rounded-full",
4221
+ children: /* @__PURE__ */ jsx("div", {
4222
+ className: "bg-accent h-full rounded-full",
4223
+ style: { width: `${Math.max(2, Math.round(share * 100))}%` }
4224
+ })
4225
+ })]
4226
+ }, entry.value);
4227
+ })]
4228
+ });
4229
+ }
4230
+ //#endregion
4231
+ export { CommandList$1 as $, DataTable as A, SidebarNav as B, WorkflowCanvas as C, ToastProvider as D, NotificationToast as E, DeploymentTimeline as F, CommandPaletteTrigger as G, StatusSelect as H, SettingsForm as I, CommandDialog as J, CommandPalette as K, FaqAccordion as L, BranchSelect as M, EnvPanel as N, toast as O, ProjectSwitcher as P, CommandItem$1 as Q, ContentTabs as R, useAutoRefresh as S, FailureList as T, SearchCombobox as U, AppMenubar as V, DashboardGrid as W, CommandGroup$1 as X, CommandEmpty$1 as Y, CommandInput$1 as Z, withoutField as _, formatQuery as a, DEFAULT_TIME_PRESETS as b, parseQueryWithDiagnostics as c, applyCellAction as d, CommandSeparator$1 as et, fieldValues as f, withFieldValue as g, removeClause as h, formatClause as i, DomainTable as j, useToast as k, EMPTY_QUERY as l, queryFields as m, QueryBar as n, quoteValue as o, isEmptyQuery as p, Command as q, describeClause as r, parseQuery as s, FieldSidebar as t, CommandShortcut as tt, addClause as u, LogTable as v, KeyValueEditor as w, TimeRangeControl as x, TimeSeriesPanel as y, EditorToolbar as z };
2402
4232
 
2403
- //# sourceMappingURL=workflow-canvas-BGBAcjf-.js.map
4233
+ //# sourceMappingURL=field-sidebar-Cs-J-5VE.js.map